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