CLI 入口とサブコマンドディスパッチ
codex コマンドの実行ファイルは一本のバイナリで、全機能はここを通る。サブコマンドなしの場合は TUI インタラクティブモードに入り、サブコマンド付きなら exec、login、mcp、app-server など数十のサブパスにディスパッチする。このレイヤーは引数のパース、config の組み立て、制御を対応 crate に渡すことだけを担い、実際のビジネスロジックはサブコマンドが呼ぶ下流 crate にある。
責務
- トップレベル CLI のパース:
MultitoolCliが clap でグローバル flag(--config、--enable、--remote、TUI オプション)と任意のサブコマンドを一本のツリーに統合する。subcommand_negates_reqs = trueによりサブコマンドが現れたら TUI の必須項目を無効化する。codex-rs/cli/src/main.rs:106-121 Subcommand列挙型の定義:可視的な全サブコマンド(exec、login、mcp、app-server、sandbox、debug、applyなど)を列挙し、dispatch に使う。codex-rs/cli/src/main.rs:123-212match subcommandでディスパッチ:各分岐でcli_config_overridesをサブコマンドに折り畳んでから対応するrun_*を呼ぶ。サブコマンドなしの時はrun_interactive_tuiに進む。codex-rs/cli/src/main.rs:987-1001arg0ディスパッチの処理:プラットフォームやインストール経路によって異なるバイナリ名で codex が呼ばれることがあり(codex-x86_64-...など)、arg0_dispatch_or_elseが入口を統一し、専用スタックサイズのスレッド上で走らせる。codex-rs/arg0/src/lib.rs:208-232
設計動機
codex は初期にはインタラクティブ TUI だけだったが、後に exec の非対話実行、login、mcp 管理、app-server のバックグラウンドプロセス、debug ツールを順次追加した。これらを codex-tui、codex-exec などの複数バイナリに分けるより一本のバイナリに詰め込む方が楽で、ユーザは codex 一つをインストールするだけで済み、ドキュメントと shell completion も一つで済む。
clap の subcommand_negates_reqs = true により、インタラクティブ TUI 専用の必須 flag(例えば prompt)はサブコマンド付きの場合には要求されなくなる。これで codex login が prompt 未指定でエラーになるのを避ける。override_usage はヘルプで codex [OPTIONS] [PROMPT] と codex [OPTIONS] <COMMAND> の二つの使い方を並べて示し、「裸で走らせれば TUI、サブコマンド付きならサブフロー」という関係を直感的に表す。
arg0_dispatch_or_else が存在するのは、一部の Linux インストールでプラットフォームごとにバイナリ名を変えることがあるから(例: codex-x86_64-unknown-linux-musl)。内部 dispatch でこのような呼び出しも主フローに正しくルーティングする。同時に Tokio runtime を専用スタックサイズのスレッドで走らせ、トップレベルの block_on がメインスレッドのスタックを食い尽くすのを防ぐ。
主要ファイル
codex-rs/cli/src/main.rs:106-121 — MultitoolCli 構造体。config override、feature toggle、TUI オプション、サブコマンドの四つを一つの clap ツリーに flatten する。codex-rs/cli/src/main.rs:123-212 — Subcommand 列挙型。30 以上のバリアントが全サブコマンドに対応する。codex-rs/cli/src/main.rs:956-974 — fn main + cli_main 入口。feature toggle を config override に折り畳んでから dispatch する。codex-rs/cli/src/main.rs:1339-1382 — Subcommand::Login 分岐。サブコマンドがどう root config override を折り畳むか、--with-api-key / --device-auth などで異なるログインパスを選ぶかを示す。codex-rs/cli/src/main.rs:2052-2075 — prepend_config_flags + reject_remote_mode_for_subcommand。サブコマンド共通の境界ゲート。codex-rs/cli/src/main.rs:2236-2263 — run_interactive_tui。サブコマンドなし時のデフォルトパス。TERM=dumb などのターミナル互換問題を処理する。codex-rs/cli/src/app_cmd.rs:1-25 — App サブコマンド(macOS/Windows)。run_app はプラットフォーム dispatch だけを行う。MultitoolCli は全入口 flag を一つの構造体にまとめ、subcommand: Option<Subcommand> が核心となる——None なら TUI、Some(...) ならサブコマンドに進む。subcommand_negates_reqs によりサブコマンド付きの場合 TUI の必須項目が無効化される。
// cli/src/main.rs:106-121 — 顶层 CLI 结构
struct MultitoolCli {
#[clap(flatten)]
pub config_overrides: CliConfigOverrides,
#[clap(flatten)]
pub feature_toggles: FeatureToggles,
#[clap(flatten)]
remote: InteractiveRemoteOptions,
#[clap(flatten)]
interactive: TuiCli,
#[clap(subcommand)]
subcommand: Option<Subcommand>,
}dispatch の核心は match subcommand で、各分岐が個別に config override の折り畳みと remote モードの検証を処理する。下は login 分岐の簡略版で、「root override を折り畳む → remote を検証 → flag ごとにパスを選ぶ」の標準三ステップを示す。
// cli/src/main.rs:1339-1379 — login 分支的 dispatch
Some(Subcommand::Login(mut login_cli)) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"login",
)?;
prepend_config_flags(
&mut login_cli.config_overrides,
root_config_overrides.clone(),
);
match login_cli.action {
Some(LoginSubcommand::Status) => {
run_login_status(login_cli.config_overrides).await;
}
None => {
if login_cli.with_api_key && login_cli.with_access_token { /* 报错 */ }
else if login_cli.use_device_code {
run_login_with_device_code(/* ... */).await;
} else if login_cli.with_api_key {
let api_key = read_api_key_from_stdin();
run_login_with_api_key(/* ... */).await;
} else {
run_login_with_chatgpt(login_cli.config_overrides).await;
}
}
}
}fn main は意図的に薄く作られ、remote_control_disabled env を取ったら arg0_dispatch_or_else に任せ、実際のパースとディスパッチは cli_main で行う。これにより arg0 モジュールは「スレッド/スタック/runtime」のようなプロセスレベルの関心事だけを担い、cli_main は clap とサブコマンドだけを気にする。
// cli/src/main.rs:956-974 — 入口故意薄,真正工作下推
fn main() -> anyhow::Result<()> {
let remote_control_disabled = codex_app_server::take_remote_control_disabled_env();
arg0_dispatch_or_else(move |arg0_paths: Arg0DispatchPaths| async move {
cli_main(arg0_paths, remote_control_disabled).await?;
Ok(())
})
}
async fn cli_main(arg0_paths: Arg0DispatchPaths, remote_control_disabled: bool) -> anyhow::Result<()> {
let MultitoolCli { /* ... */ } = MultitoolCli::parse();
// feature toggle 折叠进 config overrides,再流向所有子命令
let toggle_overrides = feature_toggles.to_overrides()?;
root_config_overrides.raw_overrides.extend(toggle_overrides);
// ...
match subcommand { /* dispatch */ }
}データフロー
境界と失敗
--strict-configは普遍ではない:reject_root_strict_config_for_subcommandはlogin/logout/updateなど config.toml を読まないサブコマンドではこの flag を拒否し、検証が効いていると誤解するのを防ぐ。codex-rs/cli/src/main.rs:2077-2091--api-keyは非推奨:codex login --api-key <KEY>はもうサポートされず、dispatch で stderr + exit 1 を返し、printenv OPENAI_API_KEY | codex login --with-api-keyで stdin から鍵を流し込むよう促す。これで鍵が shell history に入るのを避ける。codex-rs/cli/src/main.rs:1366-1370- プラットフォームサブコマンド:
Appサブコマンドは macOS/Windows でのみコンパイルされる(#[cfg(any(target_os = "macos", target_os = "windows"))])。Linux ユーザには見えない。codex-rs/cli/src/main.rs:154-155 - TERM=dumb で TUI 起動を拒否:dumb ターミナルかつ stdin/stderr がどちらも TTY でない時、
run_interactive_tuiは fatal を返し、ratatui を起動しようとして画面が乱れるのを防ぐ。codex-rs/cli/src/main.rs:2247-2263
まとめ
cli crate は codex バイナリの総入口で、責務は「parse + dispatch」:コマンドライン引数を MultitoolCli にパースし、サブコマンドごとに制御を tui / exec / login / app-server などの下流 crate に渡す。このレイヤーは意図的に薄く保たれ、ビジネスロジックは全て下流にある。ただし config override の折り畳み、strict-config/remote モードのゲートといったサブコマンド横断の共通ロジックはここに置かれ、全サブコマンドの挙動が揃うようにする。ログインフローの詳細は ChatGPT ログインと認証、実際にモデルを走らせる agent メインループは Agent メインループ を参照。