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 let name = package_command_name();
17 println!("{name} {}", env!("CARGO_PKG_VERSION"));
18 println!();
19 let mut cmd = command_with_examples();
20 let _ = cmd.print_help();
21 println!();
22 return Ok(());
23 }
24 let cmd = command_with_examples();
25 let matches = match cmd.clone().try_get_matches_from(args) {
26 Ok(matches) => matches,
27 Err(err) => {
28 if err.kind() == ErrorKind::DisplayHelp {
29 let name = package_command_name();
30 println!("{name} {}", env!("CARGO_PKG_VERSION"));
31 println!();
32 let _ = err.print();
33 println!();
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 run(cli: Cli) -> Result<(), String> {
53 let paths = resolve_paths()?;
54 ensure_paths(&paths)?;
55
56 ensure_codex_cli(detect_install_source())?;
57
58 let check_for_update_on_startup = std::env::var_os("CODEX_PROFILES_SKIP_UPDATE").is_none();
59 let update_config = UpdateConfig {
60 codex_home: paths.codex.clone(),
61 check_for_update_on_startup,
62 };
63 match run_update_prompt_if_needed(&update_config)? {
64 UpdatePromptOutcome::Continue => {}
65 UpdatePromptOutcome::RunUpdate(action) => {
66 return run_update_action(action);
67 }
68 }
69
70 let _ = sync_current_readonly(&paths);
71
72 match cli.command {
73 Commands::Save { label } => save_profile(&paths, label),
74 Commands::Load { label } => load_profile(&paths, label),
75 Commands::List => list_profiles(&paths, false, false, false, false),
76 Commands::Status { all, label } => {
77 if label.is_some() && all {
78 return Err("Error: --label cannot be combined with --all.".to_string());
79 }
80 if let Some(label) = label {
81 status_label(&paths, &label)
82 } else {
83 status_profiles(&paths, all)
84 }
85 }
86 Commands::Delete { yes, label } => delete_profile(&paths, yes, label),
87 }
88}
89
90fn run_update_action(action: UpdateAction) -> Result<(), String> {
91 let (command, args) = action.command_args();
92 let status = ProcessCommand::new(command)
93 .args(args)
94 .status()
95 .map_err(|err| format!("Error: failed to run update command: {err}"))?;
96 if status.success() {
97 Ok(())
98 } else {
99 Err(format!(
100 "Error: update command failed: {}",
101 action.command_str()
102 ))
103 }
104}
105mod auth;
106mod cli;
107mod common;
108mod profiles;
109mod requirements;
110#[cfg(test)]
111mod test_utils;
112mod ui;
113mod updates;
114mod usage;
115
116pub use auth::*;
117pub use common::*;
118pub use profiles::*;
119pub use requirements::*;
120pub use ui::*;
121pub use updates::*;
122pub use usage::*;
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::test_utils::{make_paths, set_env_guard};
128 use std::ffi::OsString;
129 use std::fs;
130 use std::os::unix::fs::PermissionsExt;
131
132 #[test]
133 fn run_cli_with_args_help() {
134 let args = vec![OsString::from("codex-profiles")];
135 run_cli_with_args(args).unwrap();
136 }
137
138 #[test]
139 fn run_cli_with_args_display_help() {
140 let args = vec![OsString::from("codex-profiles"), OsString::from("--help")];
141 run_cli_with_args(args).unwrap();
142 }
143
144 #[test]
145 fn run_cli_with_args_errors() {
146 let args = vec![OsString::from("codex-profiles"), OsString::from("nope")];
147 let err = run_cli_with_args(args).unwrap_err();
148 assert!(err.contains("error"));
149 }
150
151 #[test]
152 fn run_update_action_paths() {
153 let dir = tempfile::tempdir().expect("tempdir");
154 let bin = dir.path().join("npm");
155 fs::write(&bin, "#!/bin/sh\nexit 0\n").unwrap();
156 let mut perms = fs::metadata(&bin).unwrap().permissions();
157 perms.set_mode(0o755);
158 fs::set_permissions(&bin, perms).unwrap();
159 let path = dir.path().to_string_lossy().into_owned();
160 {
161 let _env = set_env_guard("PATH", Some(&path));
162 run_update_action(UpdateAction::NpmGlobalLatest).unwrap();
163 }
164 {
165 let _env = set_env_guard("PATH", Some(""));
166 let err = run_update_action(UpdateAction::NpmGlobalLatest).unwrap_err();
167 assert!(err.contains("failed to run update command"));
168 }
169 }
170
171 #[test]
172 fn run_cli_list_command() {
173 let dir = tempfile::tempdir().expect("tempdir");
174 let paths = make_paths(dir.path());
175 fs::create_dir_all(&paths.profiles).unwrap();
176 let home = dir.path().to_string_lossy().into_owned();
177 let _home = set_env_guard("CODEX_PROFILES_HOME", Some(&home));
178 let _skip = set_env_guard("CODEX_PROFILES_SKIP_UPDATE", Some("1"));
179 let cli = Cli {
180 plain: true,
181 command: Commands::List,
182 };
183 run(cli).unwrap();
184 }
185}