1use clap::{FromArgMatches, error::ErrorKind};
2use std::process::Command as ProcessCommand;
3
4use crate::cli::{Cli, Commands, command_with_examples};
5
6pub fn run_cli() {
7 let args: Vec<std::ffi::OsString> = std::env::args_os().collect();
8 if let Err(message) = run_cli_with_args(args) {
9 eprintln!("{message}");
10 std::process::exit(1);
11 }
12}
13
14fn run_cli_with_args(args: Vec<std::ffi::OsString>) -> Result<(), String> {
15 if args.len() == 1 {
16 print_version_header();
17 let mut cmd = command_with_examples();
18 let _ = cmd.print_help();
19 println!();
20 return Ok(());
21 }
22 let cmd = command_with_examples();
23 let matches = match cmd.clone().try_get_matches_from(args) {
24 Ok(matches) => matches,
25 Err(err) => {
26 if err.kind() == ErrorKind::DisplayHelp {
27 print_version_header();
28 let _ = err.print();
29 println!();
30 return Ok(());
31 }
32 if err.kind() == ErrorKind::DisplayVersion {
33 let _ = err.print();
34 return Ok(());
35 }
36 return Err(err.to_string());
37 }
38 };
39 let cli = Cli::from_arg_matches(&matches).map_err(|err| err.to_string())?;
40 set_plain(cli.plain);
41 if let Err(message) = run(cli) {
42 if message == CANCELLED_MESSAGE {
43 let message = format_cancel(use_color_stdout());
44 print_output_block(&message);
45 return Ok(());
46 }
47 return Err(message);
48 }
49 Ok(())
50}
51
52fn print_version_header() {
53 let name = package_command_name();
54 println!("{name} {}", env!("CARGO_PKG_VERSION"));
55 println!();
56}
57
58fn run(cli: Cli) -> Result<(), String> {
59 let paths = resolve_paths()?;
60 let json = cli.json;
61 let is_doctor = matches!(&cli.command, Commands::Doctor { .. });
62 if !is_doctor {
63 ensure_paths(&paths)?;
64 let check_for_update_on_startup = std::env::var_os("CODEXSWITCH_CLI_SKIP_UPDATE")
65 .or_else(|| std::env::var_os("CODEX_PROFILES_SKIP_UPDATE"))
66 .is_none();
67 let update_config = UpdateConfig {
68 codex_home: paths.codex.clone(),
69 check_for_update_on_startup,
70 };
71 match run_update_prompt_if_needed(&update_config)? {
72 UpdatePromptOutcome::Continue => {}
73 UpdatePromptOutcome::RunUpdate(action) => {
74 return run_update_action(action);
75 }
76 }
77 }
78
79 match cli.command {
80 Commands::Save {
81 label,
82 include_config,
83 } => save_profile(&paths, label, include_config, json),
84 Commands::Load { label, id, force } => load_profile(&paths, label, id, force, json),
85 Commands::List { show_id } => list_profiles(&paths, json, show_id),
86 Commands::Export { label, id, output } => export_profiles(&paths, label, id, output, json),
87 Commands::Import { input } => import_profiles(&paths, input, json),
88 Commands::Doctor { fix } => doctor(&paths, fix, json),
89 Commands::Label { command } => match command {
90 crate::cli::LabelCommands::Set { selector, to } => {
91 let (label, id) = require_saved_profile_selector(
92 selector,
93 crate::cli::label_set_usage(command_name()),
94 )?;
95 set_profile_label(&paths, label, id, to, json)
96 }
97 crate::cli::LabelCommands::Clear { selector } => {
98 let (label, id) = require_saved_profile_selector(
99 selector,
100 crate::cli::label_clear_usage(command_name()),
101 )?;
102 clear_profile_label(&paths, label, id, json)
103 }
104 crate::cli::LabelCommands::Rename { label, to } => {
105 rename_profile_label(&paths, label, to, json)
106 }
107 },
108 Commands::Status { all, label, id } => status_profiles(&paths, all, label, id, json),
109 Commands::Delete { yes, label, id } => delete_profile(&paths, yes, label, id, json),
110 }
111}
112
113fn require_saved_profile_selector(
114 selector: crate::cli::SavedProfileSelector,
115 usage: String,
116) -> Result<(Option<String>, Option<String>), String> {
117 if selector.label.is_none() && selector.id.is_none() {
118 return Err(format!(
119 "error: exactly one of `--label <label>` or `--id <profile-id>` is required.\n\nUsage: {usage}\n\nFor more information, try '--help'."
120 ));
121 }
122 Ok((selector.label, selector.id))
123}
124
125fn run_update_action(action: UpdateAction) -> Result<(), String> {
126 let (command, args) = action.command_args();
127 let status = ProcessCommand::new(command)
128 .args(args)
129 .status()
130 .map_err(|err| crate::msg1(crate::CMD_ERR_UPDATE_RUN, err))?;
131 if status.success() {
132 Ok(())
133 } else {
134 Err(crate::msg1(
135 crate::CMD_ERR_UPDATE_FAILED,
136 action.command_str(),
137 ))
138 }
139}
140mod auth;
141mod cli;
142mod common;
143mod doctor;
144mod json_response;
145mod messages;
146mod profiles;
147#[cfg(test)]
148mod test_utils;
149mod ui;
150mod updates;
151mod usage;
152
153pub(crate) use auth::*;
154pub(crate) use common::*;
155pub(crate) use doctor::*;
156pub(crate) use messages::*;
157pub(crate) use profiles::*;
158pub(crate) use ui::*;
159pub(crate) use updates::*;
160pub(crate) use usage::*;
161
162pub use auth::{AuthFile, Tokens, extract_email_and_plan};
163pub use updates::{
164 InstallSource, detect_install_source_inner, extract_version_from_cask,
165 extract_version_from_latest_tag, is_newer,
166};
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::test_utils::{make_paths, set_env_guard};
172 use std::ffi::OsString;
173 use std::fs;
174 use std::os::unix::fs::PermissionsExt;
175
176 #[test]
177 fn run_cli_with_args_help() {
178 let args = vec![OsString::from("codexswitch-cli")];
179 run_cli_with_args(args).unwrap();
180 }
181
182 #[test]
183 fn run_cli_with_args_display_help() {
184 let args = vec![OsString::from("codexswitch-cli"), OsString::from("--help")];
185 run_cli_with_args(args).unwrap();
186 }
187
188 #[test]
189 fn run_cli_with_args_display_version() {
190 let args = vec![
191 OsString::from("codexswitch-cli"),
192 OsString::from("--version"),
193 ];
194 run_cli_with_args(args).unwrap();
195 }
196
197 #[test]
198 fn run_cli_with_args_errors() {
199 let args = vec![OsString::from("codexswitch-cli"), OsString::from("nope")];
200 let err = run_cli_with_args(args).unwrap_err();
201 assert!(err.contains("error"));
202 }
203
204 #[test]
205 fn run_update_action_paths() {
206 let dir = tempfile::tempdir().expect("tempdir");
207 let bin = dir.path().join("npm");
208 fs::write(&bin, "#!/bin/sh\nexit 0\n").unwrap();
209 let mut perms = fs::metadata(&bin).unwrap().permissions();
210 perms.set_mode(0o755);
211 fs::set_permissions(&bin, perms).unwrap();
212 let path = dir.path().to_string_lossy().into_owned();
213 {
214 let _env = set_env_guard("PATH", Some(&path));
215 run_update_action(UpdateAction::NpmGlobalLatest).unwrap();
216 }
217 {
218 let _env = set_env_guard("PATH", Some(""));
219 let err = run_update_action(UpdateAction::NpmGlobalLatest).unwrap_err();
220 assert!(err.contains("Could not run update command"));
221 }
222 }
223
224 #[test]
225 fn run_cli_list_command() {
226 let dir = tempfile::tempdir().expect("tempdir");
227 let paths = make_paths(dir.path());
228 fs::create_dir_all(&paths.profiles).unwrap();
229 let home = dir.path().to_string_lossy().into_owned();
230 let _home = set_env_guard("CODEXSWITCH_CLI_HOME", Some(&home));
231 let _skip = set_env_guard("CODEXSWITCH_CLI_SKIP_UPDATE", Some("1"));
232 let cli = Cli {
233 plain: true,
234 json: false,
235 command: Commands::List { show_id: false },
236 };
237 run(cli).unwrap();
238 }
239}