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, name: 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 name {
174        for (alias, config) in configs.unwrap() {
175            println!("{}: {}", alias, config.auth_mode);
176        }
177    } else if plain {
178        for (alias, config) in configs.unwrap() {
179            println!("{}", alias);
180            println!("  Auth Mode: {}", config.auth_mode);
181            if let Some(ref key) = config.openai_api_key {
182                let truncated = if key.len() > 8 {
183                    format!("{}...", &key[..8])
184                } else {
185                    key.clone()
186                };
187                println!("  API Key: {}", truncated);
188            }
189            if let Some(ref token) = config.id_token {
190                let truncated = if token.len() > 8 {
191                    format!("{}...", &token[..8])
192                } else {
193                    token.clone()
194                };
195                println!("  ID Token: {}", truncated);
196            }
197            if let Some(ref id) = config.account_id {
198                println!("  Account ID: {}", id);
199            }
200        }
201    } else {
202        let json = serde_json::to_string_pretty(configs.unwrap())
203            .map_err(|e| anyhow!("Failed to serialize configurations: {}", e))?;
204        println!("{}", json);
205    }
206    Ok(())
207}
208
209/// Switch to a Codex configuration and launch Codex CLI
210pub fn handle_codex_use(
211    alias_name: String,
212    continue_flag: bool,
213    resume: Option<String>,
214    prompt: Vec<String>,
215    storage: &mut ConfigStorage,
216) -> Result<()> {
217    let config = storage
218        .get_codex_configuration(&alias_name)
219        .ok_or_else(|| anyhow!("Codex configuration '{}' not found.", alias_name))?
220        .clone();
221
222    write_auth_json(&config)?;
223    println!("Switched to Codex configuration '{}'", alias_name);
224
225    launch_codex(continue_flag, resume, prompt)?;
226    Ok(())
227}
228
229/// Launch Codex CLI with optional arguments
230fn launch_codex(continue_flag: bool, resume: Option<String>, prompt: Vec<String>) -> Result<()> {
231    let mut cmd = Command::new("codex");
232
233    if continue_flag {
234        cmd.arg("--continue");
235    }
236
237    if let Some(ref session_id) = resume {
238        cmd.arg("--resume").arg(session_id);
239    }
240
241    if !prompt.is_empty() {
242        cmd.args(prompt);
243    }
244
245    let status = cmd
246        .status()
247        .map_err(|e| anyhow!("Failed to launch Codex: {}", e))?;
248
249    if !status.success() {
250        std::process::exit(status.code().unwrap_or(1));
251    }
252
253    Ok(())
254}
255
256/// Remove Codex configurations
257pub fn handle_codex_remove(alias_names: Vec<String>, storage: &mut ConfigStorage) -> Result<()> {
258    let mut removed_count = 0;
259    let mut not_found_aliases = Vec::new();
260
261    for alias in &alias_names {
262        if storage.remove_codex_configuration(alias) {
263            removed_count += 1;
264            println!("Codex configuration '{}' removed successfully", alias);
265        } else {
266            not_found_aliases.push(alias.clone());
267            println!("Codex configuration '{}' not found", alias);
268        }
269    }
270
271    if removed_count > 0 {
272        storage.save()?;
273    }
274
275    if !not_found_aliases.is_empty() {
276        eprintln!(
277            "Warning: The following Codex configurations were not found: {}",
278            not_found_aliases.join(", ")
279        );
280    }
281
282    if removed_count > 0 {
283        println!("Successfully removed {removed_count} Codex configuration(s)");
284    }
285
286    Ok(())
287}
288
289/// Enter interactive mode for Codex configuration selection
290pub fn handle_codex_interactive(storage: &ConfigStorage) -> Result<()> {
291    crate::interactive::handle_codex_interactive_selection(storage)
292}