cc_switch/codex/
commands.rs1use 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
7pub 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
57pub 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
96fn 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
164pub 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
205pub 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
225fn 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
252pub 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
285pub fn handle_codex_interactive(storage: &ConfigStorage) -> Result<()> {
287 crate::interactive::handle_codex_interactive_selection(storage)
288}