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
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, 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
209pub 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
229fn 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
256pub 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
289pub fn handle_codex_interactive(storage: &ConfigStorage) -> Result<()> {
291 crate::interactive::handle_codex_interactive_selection(storage)
292}