Skip to main content

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