codex_utils_cli/
config_override.rs1use clap::ArgAction;
11use clap::Parser;
12use serde::de::Error as SerdeError;
13use toml::Value;
14
15#[derive(Parser, Debug, Default, Clone)]
19pub struct CliConfigOverrides {
20 #[arg(
30 short = 'c',
31 long = "config",
32 value_name = "key=value",
33 action = ArgAction::Append,
34 global = true,
35 )]
36 pub raw_overrides: Vec<String>,
37}
38
39impl CliConfigOverrides {
40 pub fn prepend_root_overrides(&mut self, root_overrides: Self) {
43 self.raw_overrides
44 .splice(0..0, root_overrides.raw_overrides);
45 }
46
47 pub fn parse_overrides(&self) -> Result<Vec<(String, Value)>, String> {
50 self.raw_overrides
51 .iter()
52 .map(|s| {
53 let mut parts = s.splitn(2, '=');
56 let key = match parts.next() {
57 Some(k) => k.trim(),
58 None => return Err("Override missing key".to_string()),
59 };
60 let value_str = parts
61 .next()
62 .ok_or_else(|| format!("Invalid override (missing '='): {s}"))?
63 .trim();
64
65 if key.is_empty() {
66 return Err(format!("Empty key in override: {s}"));
67 }
68
69 let value: Value = match parse_toml_value(value_str) {
73 Ok(v) => v,
74 Err(_) => {
75 let trimmed = value_str.trim().trim_matches(|c| c == '"' || c == '\'');
77 Value::String(trimmed.to_string())
78 }
79 };
80
81 Ok((canonicalize_override_key(key), value))
82 })
83 .collect()
84 }
85}
86
87fn canonicalize_override_key(key: &str) -> String {
88 if key == "use_legacy_landlock" {
89 "features.use_legacy_landlock".to_string()
90 } else {
91 key.to_string()
92 }
93}
94
95fn parse_toml_value(raw: &str) -> Result<Value, toml::de::Error> {
96 let wrapped = format!("_x_ = {raw}");
97 let table: toml::Table = toml::from_str(&wrapped)?;
98 table
99 .get("_x_")
100 .cloned()
101 .ok_or_else(|| SerdeError::custom("missing sentinel key"))
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn parses_basic_scalar() {
110 let v = parse_toml_value("42").expect("parse");
111 assert_eq!(v.as_integer(), Some(42));
112 }
113
114 #[test]
115 fn parses_bool() {
116 let true_literal = parse_toml_value("true").expect("parse");
117 assert_eq!(true_literal.as_bool(), Some(true));
118
119 let false_literal = parse_toml_value("false").expect("parse");
120 assert_eq!(false_literal.as_bool(), Some(false));
121 }
122
123 #[test]
124 fn fails_on_unquoted_string() {
125 assert!(parse_toml_value("hello").is_err());
126 }
127
128 #[test]
129 fn parses_array() {
130 let v = parse_toml_value("[1, 2, 3]").expect("parse");
131 let arr = v.as_array().expect("array");
132 assert_eq!(arr.len(), 3);
133 }
134
135 #[test]
136 fn canonicalizes_use_legacy_landlock_alias() {
137 let overrides = CliConfigOverrides {
138 raw_overrides: vec!["use_legacy_landlock=true".to_string()],
139 };
140 let parsed = overrides.parse_overrides().expect("parse_overrides");
141 assert_eq!(parsed[0].0.as_str(), "features.use_legacy_landlock");
142 assert_eq!(parsed[0].1.as_bool(), Some(true));
143 }
144
145 #[test]
146 fn prepends_root_overrides() {
147 let mut subcommand_overrides = CliConfigOverrides {
148 raw_overrides: vec![r#"model="gpt-5.2""#.to_string()],
149 };
150 subcommand_overrides.prepend_root_overrides(CliConfigOverrides {
151 raw_overrides: vec![r#"model="gpt-5.1""#.to_string()],
152 });
153
154 assert_eq!(
155 subcommand_overrides.raw_overrides,
156 vec![
157 r#"model="gpt-5.1""#.to_string(),
158 r#"model="gpt-5.2""#.to_string(),
159 ]
160 );
161 }
162
163 #[test]
164 fn parses_inline_table() {
165 let v = parse_toml_value("{a = 1, b = 2}").expect("parse");
166 let tbl = v.as_table().expect("table");
167 assert_eq!(tbl.get("a").unwrap().as_integer(), Some(1));
168 assert_eq!(tbl.get("b").unwrap().as_integer(), Some(2));
169 }
170}