Skip to main content

codex_profiles/
lib.rs

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}