Skip to main content

cc_switch/codex/
commands.rs

1use crate::codex::{CodexConfiguration, write_auth_json};
2use crate::config::{ConfigStorage, validate_alias_name};
3use anyhow::{Result, anyhow};
4use std::fs;
5use std::process::Command;
6
7/// Add a Codex configuration
8pub fn handle_codex_add(
9    alias_name: String,
10    api_key: Option<String>,
11    force: bool,
12    interactive: bool,
13    from_file: Option<String>,
14    storage: &mut ConfigStorage,
15) -> Result<()> {
16    validate_alias_name(&alias_name)?;
17
18    if storage.get_codex_configuration(&alias_name).is_some() {
19        if !force {
20            eprintln!(
21                "Warning: Configuration '{}' already exists. Use --force to overwrite.",
22                alias_name
23            );
24            return Ok(());
25        }
26        eprintln!("Overwriting existing configuration '{}'", alias_name);
27    }
28
29    let config = if let Some(file_path) = from_file {
30        parse_auth_json_file(&file_path, &alias_name)?
31    } else if interactive {
32        parse_interactive_codex_config(&alias_name)?
33    } else {
34        let key = api_key.ok_or_else(|| {
35            anyhow!(
36                "API key is required. Use -k <key>, --from-file <path>, or -i for interactive mode."
37            )
38        })?;
39        CodexConfiguration {
40            alias_name: alias_name.clone(),
41            auth_mode: "apikey".to_string(),
42            openai_api_key: Some(key),
43            id_token: None,
44            access_token: None,
45            refresh_token: None,
46            account_id: None,
47            last_refresh: None,
48        }
49    };
50
51    storage.add_codex_configuration(config);
52    storage.save()?;
53    println!("Configuration '{}' added successfully.", alias_name);
54    Ok(())
55}
56
57/// Parse an existing auth.json file into a CodexConfiguration
58pub fn parse_auth_json_file(file_path: &str, alias_name: &str) -> Result<CodexConfiguration> {
59    let content = fs::read_to_string(file_path)
60        .map_err(|e| anyhow!("Failed to read auth.json file '{}': {}", file_path, e))?;
61
62    let json: serde_json::Value = serde_json::from_str(&content)
63        .map_err(|e| anyhow!("Failed to parse auth.json file '{}': {}", file_path, e))?;
64
65    let auth_mode = json["auth_mode"]
66        .as_str()
67        .ok_or_else(|| {
68            anyhow!(
69                "Missing 'auth_mode' field in auth.json file '{}'",
70                file_path
71            )
72        })?
73        .to_string();
74
75    let openai_api_key = json["OPENAI_API_KEY"].as_str().map(|s| s.to_string());
76
77    let tokens = &json["tokens"];
78    let id_token = tokens["id_token"].as_str().map(|s| s.to_string());
79    let access_token = tokens["access_token"].as_str().map(|s| s.to_string());
80    let refresh_token = tokens["refresh_token"].as_str().map(|s| s.to_string());
81    let account_id = tokens["account_id"].as_str().map(|s| s.to_string());
82    let last_refresh = json["last_refresh"].as_str().map(|s| s.to_string());
83
84    Ok(CodexConfiguration {
85        alias_name: alias_name.to_string(),
86        auth_mode,
87        openai_api_key,
88        id_token,
89        access_token,
90        refresh_token,
91        account_id,
92        last_refresh,
93    })
94}
95
96/// Interactive mode for creating a Codex configuration
97fn parse_interactive_codex_config(alias_name: &str) -> Result<CodexConfiguration> {
98    use crate::interactive::{read_input, read_sensitive_input};
99
100    let mode_input = read_input("Auth mode (chatgpt/apikey) [chatgpt]: ")?;
101    let auth_mode = if mode_input.is_empty() {
102        "chatgpt".to_string()
103    } else {
104        mode_input.to_lowercase()
105    };
106
107    match auth_mode.as_str() {
108        "chatgpt" => {
109            let id_token = read_sensitive_input("ID Token: ")?;
110            let access_token = read_sensitive_input("Access Token: ")?;
111            let refresh_token = read_sensitive_input("Refresh Token: ")?;
112            let account_id = read_input("Account ID: ")?;
113
114            Ok(CodexConfiguration {
115                alias_name: alias_name.to_string(),
116                auth_mode: "chatgpt".to_string(),
117                openai_api_key: None,
118                id_token: if id_token.is_empty() {
119                    None
120                } else {
121                    Some(id_token)
122                },
123                access_token: if access_token.is_empty() {
124                    None
125                } else {
126                    Some(access_token)
127                },
128                refresh_token: if refresh_token.is_empty() {
129                    None
130                } else {
131                    Some(refresh_token)
132                },
133                account_id: if account_id.is_empty() {
134                    None
135                } else {
136                    Some(account_id)
137                },
138                last_refresh: None,
139            })
140        }
141        "apikey" => {
142            let api_key = read_sensitive_input("OpenAI API Key: ")?;
143            if api_key.is_empty() {
144                return Err(anyhow!("API key cannot be empty"));
145            }
146            Ok(CodexConfiguration {
147                alias_name: alias_name.to_string(),
148                auth_mode: "apikey".to_string(),
149                openai_api_key: Some(api_key),
150                id_token: None,
151                access_token: None,
152                refresh_token: None,
153                account_id: None,
154                last_refresh: None,
155            })
156        }
157        _ => Err(anyhow!(
158            "Invalid auth mode '{}'. Use 'chatgpt' or 'apikey'.",
159            auth_mode
160        )),
161    }
162}
163
164/// List Codex configurations
165pub fn handle_codex_list(plain: bool, storage: &ConfigStorage) -> Result<()> {
166    let configs = storage.codex_configurations.as_ref();
167
168    if configs.is_none() || configs.unwrap().is_empty() {
169        println!("No Codex configurations found.");
170        return Ok(());
171    }
172
173    if plain {
174        for (alias, config) in configs.unwrap() {
175            println!("{}", alias);
176            println!("  Auth Mode: {}", config.auth_mode);
177            if let Some(ref key) = config.openai_api_key {
178                let truncated = if key.len() > 8 {
179                    format!("{}...", &key[..8])
180                } else {
181                    key.clone()
182                };
183                println!("  API Key: {}", truncated);
184            }
185            if let Some(ref token) = config.id_token {
186                let truncated = if token.len() > 8 {
187                    format!("{}...", &token[..8])
188                } else {
189                    token.clone()
190                };
191                println!("  ID Token: {}", truncated);
192            }
193            if let Some(ref id) = config.account_id {
194                println!("  Account ID: {}", id);
195            }
196        }
197    } else {
198        let json = serde_json::to_string_pretty(configs.unwrap())
199            .map_err(|e| anyhow!("Failed to serialize configurations: {}", e))?;
200        println!("{}", json);
201    }
202    Ok(())
203}
204
205/// Switch to a Codex configuration and launch Codex CLI
206pub fn handle_codex_use(
207    alias_name: String,
208    continue_flag: bool,
209    resume: Option<String>,
210    prompt: Vec<String>,
211    storage: &mut ConfigStorage,
212) -> Result<()> {
213    let config = storage
214        .get_codex_configuration(&alias_name)
215        .ok_or_else(|| anyhow!("Codex configuration '{}' not found.", alias_name))?
216        .clone();
217
218    write_auth_json(&config)?;
219    println!("Switched to Codex configuration '{}'", alias_name);
220
221    launch_codex(continue_flag, resume, prompt)?;
222    Ok(())
223}
224
225/// Launch Codex CLI with optional arguments
226fn launch_codex(continue_flag: bool, resume: Option<String>, prompt: Vec<String>) -> Result<()> {
227    let mut cmd = Command::new("codex");
228
229    if continue_flag {
230        cmd.arg("--continue");
231    }
232
233    if let Some(ref session_id) = resume {
234        cmd.arg("--resume").arg(session_id);
235    }
236
237    if !prompt.is_empty() {
238        cmd.args(prompt);
239    }
240
241    let status = cmd
242        .status()
243        .map_err(|e| anyhow!("Failed to launch Codex: {}", e))?;
244
245    if !status.success() {
246        std::process::exit(status.code().unwrap_or(1));
247    }
248
249    Ok(())
250}
251
252/// Remove Codex configurations
253pub fn handle_codex_remove(alias_names: Vec<String>, storage: &mut ConfigStorage) -> Result<()> {
254    let mut removed_count = 0;
255    let mut not_found_aliases = Vec::new();
256
257    for alias in &alias_names {
258        if storage.remove_codex_configuration(alias) {
259            removed_count += 1;
260            println!("Codex configuration '{}' removed successfully", alias);
261        } else {
262            not_found_aliases.push(alias.clone());
263            println!("Codex configuration '{}' not found", alias);
264        }
265    }
266
267    if removed_count > 0 {
268        storage.save()?;
269    }
270
271    if !not_found_aliases.is_empty() {
272        eprintln!(
273            "Warning: The following Codex configurations were not found: {}",
274            not_found_aliases.join(", ")
275        );
276    }
277
278    if removed_count > 0 {
279        println!("Successfully removed {removed_count} Codex configuration(s)");
280    }
281
282    Ok(())
283}
284
285/// Enter interactive mode for Codex configuration selection
286pub fn handle_codex_interactive(storage: &ConfigStorage) -> Result<()> {
287    crate::interactive::handle_codex_interactive_selection(storage)
288}