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 let check_for_update_on_startup = std::env::var_os("CODEX_PROFILES_SKIP_UPDATE").is_none();
57 let update_config = UpdateConfig {
58 codex_home: paths.codex.clone(),
59 check_for_update_on_startup,
60 };
61 match run_update_prompt_if_needed(&update_config)? {
62 UpdatePromptOutcome::Continue => {}
63 UpdatePromptOutcome::RunUpdate(action) => {
64 return run_update_action(action);
65 }
66 }
67
68 match cli.command {
69 Commands::Save { label } => save_profile(&paths, label),
70 Commands::Load { label } => load_profile(&paths, label),
71 Commands::List => list_profiles(&paths),
72 Commands::Status { all, show_errors } => status_profiles(&paths, all, show_errors),
73 Commands::Delete { yes, label } => delete_profile(&paths, yes, label),
74 }
75}
76
77fn run_update_action(action: UpdateAction) -> Result<(), String> {
78 let (command, args) = action.command_args();
79 let status = ProcessCommand::new(command)
80 .args(args)
81 .status()
82 .map_err(|err| crate::msg1(crate::CMD_ERR_UPDATE_RUN, err))?;
83 if status.success() {
84 Ok(())
85 } else {
86 Err(crate::msg1(
87 crate::CMD_ERR_UPDATE_FAILED,
88 action.command_str(),
89 ))
90 }
91}
92mod auth;
93mod cli;
94mod common;
95mod messages;
96mod profiles;
97#[cfg(test)]
98mod test_utils;
99mod ui;
100mod updates;
101mod usage;
102
103pub(crate) use auth::*;
104pub(crate) use common::*;
105pub(crate) use messages::*;
106pub(crate) use profiles::*;
107pub(crate) use ui::*;
108pub(crate) use updates::*;
109pub(crate) use usage::*;
110
111pub use auth::{AuthFile, Tokens, extract_email_and_plan};
112pub use updates::{
113 InstallSource, detect_install_source_inner, extract_version_from_cask,
114 extract_version_from_latest_tag, is_newer,
115};
116pub use usage::parse_config_value;
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::test_utils::{make_paths, set_env_guard};
122 use std::ffi::OsString;
123 use std::fs;
124 use std::os::unix::fs::PermissionsExt;
125
126 #[test]
127 fn run_cli_with_args_help() {
128 let args = vec![OsString::from("codex-profiles")];
129 run_cli_with_args(args).unwrap();
130 }
131
132 #[test]
133 fn run_cli_with_args_display_help() {
134 let args = vec![OsString::from("codex-profiles"), OsString::from("--help")];
135 run_cli_with_args(args).unwrap();
136 }
137
138 #[test]
139 fn run_cli_with_args_errors() {
140 let args = vec![OsString::from("codex-profiles"), OsString::from("nope")];
141 let err = run_cli_with_args(args).unwrap_err();
142 assert!(err.contains("error"));
143 }
144
145 #[test]
146 fn run_update_action_paths() {
147 let dir = tempfile::tempdir().expect("tempdir");
148 let bin = dir.path().join("npm");
149 fs::write(&bin, "#!/bin/sh\nexit 0\n").unwrap();
150 let mut perms = fs::metadata(&bin).unwrap().permissions();
151 perms.set_mode(0o755);
152 fs::set_permissions(&bin, perms).unwrap();
153 let path = dir.path().to_string_lossy().into_owned();
154 {
155 let _env = set_env_guard("PATH", Some(&path));
156 run_update_action(UpdateAction::NpmGlobalLatest).unwrap();
157 }
158 {
159 let _env = set_env_guard("PATH", Some(""));
160 let err = run_update_action(UpdateAction::NpmGlobalLatest).unwrap_err();
161 assert!(err.contains("Could not run update command"));
162 }
163 }
164
165 #[test]
166 fn run_cli_list_command() {
167 let dir = tempfile::tempdir().expect("tempdir");
168 let paths = make_paths(dir.path());
169 fs::create_dir_all(&paths.profiles).unwrap();
170 let home = dir.path().to_string_lossy().into_owned();
171 let _home = set_env_guard("CODEX_PROFILES_HOME", Some(&home));
172 let _skip = set_env_guard("CODEX_PROFILES_SKIP_UPDATE", Some("1"));
173 let cli = Cli {
174 plain: true,
175 command: Commands::List,
176 };
177 run(cli).unwrap();
178 }
179}