Skip to main content

cc_switch/codex/
commands.rs

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