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        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}