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
8pub 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 -k <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
58pub 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
97fn 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
165pub 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
210pub 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
230fn 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
257pub 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
290pub fn handle_codex_interactive(storage: &ConfigStorage) -> Result<()> {
292 crate::interactive::handle_codex_interactive_selection(storage)
293}