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