From 62dda0b041949c38ae43035dc49507904051aea8 Mon Sep 17 00:00:00 2001 From: "Rai (Michael Pokorny)" Date: Tue, 24 Jun 2025 13:14:51 -0700 Subject: [PATCH] cli(config): implement 'codex config' subcommand and document it --- codex-rs/README.md | 11 ++++ codex-rs/cli/src/main.rs | 128 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) diff --git a/codex-rs/README.md b/codex-rs/README.md index caa21639fb..9b18dda624 100644 --- a/codex-rs/README.md +++ b/codex-rs/README.md @@ -43,6 +43,17 @@ To run Codex non-interactively, run `codex exec PROMPT` (you can also pass the p Sometimes it is not convenient to `cd` to the directory you want Codex to use as the "working root" before running Codex. Fortunately, `codex` supports a `--cd` option so you can specify whatever folder you want. You can confirm that Codex is honoring `--cd` by double-checking the **workdir** it reports in the TUI at the start of a new session. +### `codex config` to manage your configuration file + +Codex now provides a built-in `config` subcommand for managing your `config.toml`: + +```shell +codex config edit # open ~/.codex/config.toml in $EDITOR (or vi) +codex config set KEY VALUE # set a config key to a TOML literal, e.g. tui.auto_mount_repo true +``` + +Use `codex config --help` for more details. + ### Experimenting with the Codex Sandbox To test to see what happens when a command is run under the sandbox provided by Codex, we provide the following subcommands in Codex CLI: diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index aade8a95e1..536d0bf563 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -7,6 +7,11 @@ use codex_common::CliConfigOverrides; use codex_exec::Cli as ExecCli; use codex_tui::Cli as TuiCli; use std::path::PathBuf; +use std::{env, fs, process}; +use std::io::ErrorKind; +use toml::{self, value::Table, Value}; +use serde::de::Error as SerdeError; +use codex_core::config::find_codex_home; use uuid::Uuid; use crate::proto::ProtoCli; @@ -32,6 +37,30 @@ struct MultitoolCli { subcommand: Option, } +/// Parse a raw TOML literal (e.g. `true`, `42`, `[1,2]`, `{a=1}`) into a TOML Value. +/// Wraps the literal under a sentinel key to satisfy the TOML parser. +fn parse_toml_value(raw: &str) -> Result { + let wrapped = format!("_x_ = {raw}"); + let table: Table = toml::from_str(&wrapped)?; + table.get("_x_") + .cloned() + .ok_or_else(|| SerdeError::custom("missing sentinel")) +} + +/// Subcommands for the `codex config` command. +#[derive(Debug, clap::Subcommand)] +enum ConfigCmd { + /// Open the config file in your editor ($EDITOR or vi). + Edit, + /// Set a configuration key to a TOML literal, e.g. `tui.auto_mount_repo true`. + Set { + /// Dotted path to the key (e.g. `tui.auto_mount_repo`). + key: String, + /// A TOML literal value (e.g. `true`, `42`, `"foo"`, `[1,2]`). + value: String, + }, +} + #[derive(Debug, clap::Subcommand)] enum Subcommand { /// Resume an existing TUI session by UUID. @@ -39,6 +68,9 @@ enum Subcommand { /// UUID of the session to resume session_id: Uuid, }, + /// Inspect or modify the CLI configuration file. + #[command(subcommand)] + Config(ConfigCmd), /// Run Codex non-interactively. #[clap(visible_alias = "e")] Exec(ExecCli), @@ -100,6 +132,46 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() prepend_config_flags(&mut tui_cli.config_overrides, cli.config_overrides); codex_tui::run_main(tui_cli, codex_linux_sandbox_exe)?; } + Some(Subcommand::Config(cmd)) => { + // Handle `codex config` subcommands: edit or set. + // Determine config directory and file path. + let codex_home = find_codex_home()?; + fs::create_dir_all(&codex_home)?; + let config_path = codex_home.join("config.toml"); + // Load existing config.toml into a Toml value, or start with empty table. + let mut doc = match fs::read_to_string(&config_path) { + Ok(s) => toml::from_str::(&s)?, + Err(e) if e.kind() == ErrorKind::NotFound => toml::Value::Table(Default::default()), + Err(e) => return Err(e.into()), + }; + match cmd { + ConfigCmd::Edit => { + // Ensure the config file exists. + if !config_path.exists() { + fs::write(&config_path, "")?; + } + // Open in editor from $EDITOR or fall back to vi. + let editor = env::var_os("EDITOR").unwrap_or_else(|| "vi".into()); + let status = process::Command::new(editor) + .arg(&config_path) + .status()?; + if !status.success() { + std::process::exit(status.code().unwrap_or(1)); + } + } + ConfigCmd::Set { key, value } => { + // Parse the provided TOML literal value. + let val = parse_toml_value(&value) + .map_err(|e| anyhow::anyhow!("TOML parse error for `{}`: {}", value, e))?; + // Apply the override into the document. + apply_override(&mut doc, &key, val); + // Serialize and write back to disk. + let s = toml::to_string_pretty(&doc)?; + fs::write(&config_path, s)?; + } + } + return Ok(()); + } 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?; @@ -148,3 +220,59 @@ fn prepend_config_flags( .raw_overrides .splice(0..0, cli_config_overrides.raw_overrides); } + +/// Apply a dotted-path override into a TOML document, creating tables as needed. +fn apply_override(root: &mut toml::Value, path: &str, value: toml::Value) { + use toml::value::Table; + let parts: Vec<&str> = path.split('.').collect(); + let mut current = root; + for (i, part) in parts.iter().enumerate() { + let last = i == parts.len() - 1; + if last { + match current { + toml::Value::Table(tbl) => { + tbl.insert((*part).to_string(), value); + } + _ => { + let mut tbl = Table::new(); + tbl.insert((*part).to_string(), value); + *current = toml::Value::Table(tbl); + } + } + return; + } + match current { + toml::Value::Table(tbl) => { + current = tbl.entry((*part).to_string()) + .or_insert_with(|| toml::Value::Table(Table::new())); + } + _ => { + *current = toml::Value::Table(Table::new()); + if let toml::Value::Table(tbl) = current { + current = tbl.entry((*part).to_string()) + .or_insert_with(|| toml::Value::Table(Table::new())); + } + } + } + } +} + +// --------------------- +// Tests for CLI parsing +// --------------------- +#[cfg(test)] +mod tests { + use super::MultitoolCli; + use clap::CommandFactory; + + #[test] + fn config_subcommands_help() { + let mut cmd = MultitoolCli::command(); + let cfg = cmd.find_subcommand_mut("config").expect("config subcommand not found"); + let mut buf = Vec::new(); + cfg.write_long_help(&mut buf).unwrap(); + let help = String::from_utf8(buf).unwrap(); + assert!(help.contains("edit"), "help missing 'edit': {}", help); + assert!(help.contains("set"), "help missing 'set': {}", help); + } +}