diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 313c782f26f..ebf0a3d1a26 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1296,10 +1296,24 @@ version = "0.0.0" dependencies = [ "anyhow", "clap", + "clap_complete", "codex-arg0", + "codex-chatgpt", + "codex-cli", + "codex-common", + "codex-core", + "codex-exec", + "codex-login", + "codex-mcp-server", + "codex-protocol", + "codex-protocol-ts", "codex-tui", "eframe", "pretty_assertions", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", ] [[package]] diff --git a/codex-rs/gui/Cargo.toml b/codex-rs/gui/Cargo.toml index b12c7377548..12a1a325aad 100644 --- a/codex-rs/gui/Cargo.toml +++ b/codex-rs/gui/Cargo.toml @@ -17,9 +17,29 @@ workspace = true [dependencies] anyhow = "1" clap = { version = "4", features = ["derive"] } +clap_complete = "4" codex-arg0 = { path = "../arg0" } +codex-chatgpt = { path = "../chatgpt" } +codex-cli = { path = "../cli" } +codex-common = { path = "../common", features = ["cli"] } +codex-core = { path = "../core" } +codex-exec = { path = "../exec" } +codex-login = { path = "../login" } +codex-mcp-server = { path = "../mcp-server" } +codex-protocol = { path = "../protocol" } +codex-protocol-ts = { path = "../protocol-ts" } codex-tui = { path = "../tui" } eframe = "0.27" +serde_json = "1" +tokio = { version = "1", features = [ + "io-std", + "macros", + "process", + "rt-multi-thread", + "signal", +] } +tracing = "0.1.41" +tracing-subscriber = "0.3.19" [dev-dependencies] pretty_assertions = "1" diff --git a/codex-rs/gui/src/lib.rs b/codex-rs/gui/src/lib.rs index d01c235b9b4..88d4fae4f6d 100644 --- a/codex-rs/gui/src/lib.rs +++ b/codex-rs/gui/src/lib.rs @@ -1,5 +1,21 @@ use anyhow::Result; +use clap::CommandFactory; use clap::Parser; +use clap_complete::Shell; +use clap_complete::generate; +use codex_chatgpt::apply_command::ApplyCommand; +use codex_chatgpt::apply_command::run_apply_command; +use codex_cli::LandlockCommand; +use codex_cli::SeatbeltCommand; +use codex_cli::login::run_login_status; +use codex_cli::login::run_login_with_api_key; +use codex_cli::login::run_login_with_chatgpt; +use codex_cli::login::run_logout; +use codex_cli::proto; +use codex_cli::proto::ProtoCli; +use codex_common::CliConfigOverrides; +use codex_core::protocol::FinalOutput; +use codex_exec::Cli as ExecCli; use codex_tui::Cli as TuiCli; use eframe::egui::Align; use eframe::egui::Color32; @@ -15,27 +31,211 @@ use std::mem; use std::path::PathBuf; /// Command line interface for the graphical Codex client. +/// +/// If no subcommand is specified, options will be forwarded to the interactive UI. #[derive(Debug, Parser)] -#[command(version)] +#[clap(author, version, subcommand_negates_reqs = true, bin_name = "codex")] pub struct Cli { + #[clap(flatten)] + pub config_overrides: CliConfigOverrides, + /// Launch the terminal UI instead of the graphical interface. #[arg(long = "tui-mode", default_value_t = false)] pub tui_mode: bool, #[clap(flatten)] - pub tui: TuiCli, + interactive: TuiCli, + + #[clap(subcommand)] + subcommand: Option, +} + +#[derive(Debug, clap::Subcommand)] +enum Subcommand { + /// Run Codex non-interactively. + #[clap(visible_alias = "e")] + Exec(ExecCli), + + /// Manage login. + Login(LoginCommand), + + /// Remove stored authentication credentials. + Logout(LogoutCommand), + + /// Experimental: run Codex as an MCP server. + Mcp, + + /// Run the Protocol stream via stdin/stdout. + #[clap(visible_alias = "p")] + Proto(ProtoCli), + + /// Generate shell completion scripts. + Completion(CompletionCommand), + + /// Internal debugging commands. + Debug(DebugArgs), + + /// Apply the latest diff produced by Codex agent as a `git apply` to your local working tree. + #[clap(visible_alias = "a")] + Apply(ApplyCommand), + + /// Internal: generate TypeScript protocol bindings. + #[clap(hide = true)] + GenerateTs(GenerateTsCommand), +} + +#[derive(Debug, Parser)] +struct CompletionCommand { + /// Shell to generate completions for + #[clap(value_enum, default_value_t = Shell::Bash)] + shell: Shell, +} + +#[derive(Debug, Parser)] +struct DebugArgs { + #[command(subcommand)] + cmd: DebugCommand, +} + +#[derive(Debug, clap::Subcommand)] +enum DebugCommand { + /// Run a command under Seatbelt (macOS only). + Seatbelt(SeatbeltCommand), + + /// Run a command under Landlock+seccomp (Linux only). + Landlock(LandlockCommand), +} + +#[derive(Debug, Parser)] +struct LoginCommand { + #[clap(skip)] + config_overrides: CliConfigOverrides, + + #[arg(long = "api-key", value_name = "API_KEY")] + api_key: Option, + + #[command(subcommand)] + action: Option, +} + +#[derive(Debug, clap::Subcommand)] +enum LoginSubcommand { + /// Show login status. + Status, +} + +#[derive(Debug, Parser)] +struct LogoutCommand { + #[clap(skip)] + config_overrides: CliConfigOverrides, +} + +#[derive(Debug, Parser)] +struct GenerateTsCommand { + /// Output directory where .ts files will be written + #[arg(short = 'o', long = "out", value_name = "DIR")] + out_dir: PathBuf, + + /// Optional path to the Prettier executable to format generated files + #[arg(short = 'p', long = "prettier", value_name = "PRETTIER_BIN")] + prettier: Option, } /// Entry point for the graphical Codex client. pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> Result<()> { - if cli.tui_mode { - let _ = codex_tui::run_main(cli.tui, codex_linux_sandbox_exe).await?; - } else if let Err(err) = run_gui() { - eprintln!("failed to start GUI: {err}"); + match cli.subcommand { + None => { + let mut tui_cli = cli.interactive; + prepend_config_flags(&mut tui_cli.config_overrides, cli.config_overrides); + if cli.tui_mode { + let usage = codex_tui::run_main(tui_cli, codex_linux_sandbox_exe).await?; + if !usage.is_zero() { + println!("{}", FinalOutput::from(usage)); + } + } else if let Err(err) = run_gui() { + eprintln!("failed to start GUI: {err}"); + } + } + Some(Subcommand::Exec(mut exec_cli)) => { + prepend_config_flags(&mut exec_cli.config_overrides, cli.config_overrides); + codex_exec::run_main(exec_cli, codex_linux_sandbox_exe).await?; + } + Some(Subcommand::Mcp) => { + codex_mcp_server::run_main(codex_linux_sandbox_exe, cli.config_overrides).await?; + } + Some(Subcommand::Login(mut login_cli)) => { + prepend_config_flags(&mut login_cli.config_overrides, cli.config_overrides); + match login_cli.action { + Some(LoginSubcommand::Status) => { + run_login_status(login_cli.config_overrides).await; + } + None => { + if let Some(api_key) = login_cli.api_key { + run_login_with_api_key(login_cli.config_overrides, api_key).await; + } else { + run_login_with_chatgpt(login_cli.config_overrides).await; + } + } + } + } + Some(Subcommand::Logout(mut logout_cli)) => { + prepend_config_flags(&mut logout_cli.config_overrides, cli.config_overrides); + run_logout(logout_cli.config_overrides).await; + } + Some(Subcommand::Proto(mut proto_cli)) => { + prepend_config_flags(&mut proto_cli.config_overrides, cli.config_overrides); + proto::run_main(proto_cli).await?; + } + Some(Subcommand::Completion(completion_cli)) => { + print_completion(completion_cli); + } + Some(Subcommand::Debug(debug_args)) => match debug_args.cmd { + DebugCommand::Seatbelt(mut seatbelt_cli) => { + prepend_config_flags(&mut seatbelt_cli.config_overrides, cli.config_overrides); + codex_cli::debug_sandbox::run_command_under_seatbelt( + seatbelt_cli, + codex_linux_sandbox_exe, + ) + .await?; + } + DebugCommand::Landlock(mut landlock_cli) => { + prepend_config_flags(&mut landlock_cli.config_overrides, cli.config_overrides); + codex_cli::debug_sandbox::run_command_under_landlock( + landlock_cli, + codex_linux_sandbox_exe, + ) + .await?; + } + }, + Some(Subcommand::Apply(mut apply_cli)) => { + prepend_config_flags(&mut apply_cli.config_overrides, cli.config_overrides); + run_apply_command(apply_cli, None).await?; + } + Some(Subcommand::GenerateTs(gen_cli)) => { + codex_protocol_ts::generate_ts(&gen_cli.out_dir, gen_cli.prettier.as_deref())?; + } } + Ok(()) } +/// Prepend root-level overrides so they have lower precedence than +/// CLI-specific ones specified after the subcommand (if any). +fn prepend_config_flags( + subcommand_config_overrides: &mut CliConfigOverrides, + cli_config_overrides: CliConfigOverrides, +) { + subcommand_config_overrides + .raw_overrides + .splice(0..0, cli_config_overrides.raw_overrides); +} + +fn print_completion(cmd: CompletionCommand) { + let mut app = Cli::command(); + let name = "codex"; + generate(cmd.shell, &mut app, name, &mut std::io::stdout()); +} + fn run_gui() -> eframe::Result<()> { let options = eframe::NativeOptions::default(); eframe::run_native("Codex", options, Box::new(|cc| Box::new(CodexGui::new(cc)))) @@ -46,6 +246,7 @@ struct Message { color: Color32, } +#[derive(Default)] struct CodexGui { sessions: Vec, notes: Vec, @@ -56,20 +257,6 @@ struct CodexGui { prompt: String, } -impl Default for CodexGui { - fn default() -> Self { - Self { - sessions: Vec::new(), - notes: Vec::new(), - messages: Vec::new(), - input: String::new(), - new_session: String::new(), - new_note: String::new(), - prompt: String::new(), - } - } -} - impl CodexGui { fn new(_cc: &eframe::CreationContext<'_>) -> Self { Self { @@ -218,17 +405,21 @@ mod tests { #[test] fn add_session_appends() { - let mut gui = CodexGui::default(); - gui.new_session = "S3".into(); + let mut gui = CodexGui { + new_session: "S3".into(), + ..Default::default() + }; gui.add_session(); - assert_eq!(gui.sessions.len(), 3); - assert_eq!(gui.sessions[2], "S3"); + assert_eq!(gui.sessions.len(), 1); + assert_eq!(gui.sessions[0], "S3"); } #[test] fn send_message_updates_state() { - let mut gui = CodexGui::default(); - gui.input = "hi".into(); + let mut gui = CodexGui { + input: "hi".into(), + ..Default::default() + }; gui.send_message(Color32::from_rgb(1, 2, 3)); assert_eq!(gui.prompt, "hi"); assert_eq!(gui.messages.last().unwrap().text, "hi");