use cc_switch_lib::cli::{Cli, Commands};
use cc_switch_lib::AppError;
use clap::Parser;
use std::io::{self, BufRead, Write};
use std::process;
fn main() {
let cli = Cli::parse();
if cli.version {
println!("{}", cc_switch_lib::cli::version_string());
return;
}
let log_level = if cli.verbose {
"debug"
} else {
"error" };
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(log_level)).init();
if let Err(e) = run(cli) {
eprintln!("Error: {}", e);
process::exit(1);
}
}
fn run(cli: Cli) -> Result<(), AppError> {
prompt_legacy_config_migration();
initialize_startup_state_if_needed(&cli.command)?;
match cli.command {
None | Some(Commands::Interactive) => cc_switch_lib::cli::interactive::run(cli.app),
Some(Commands::Provider(cmd)) => {
cc_switch_lib::cli::commands::provider::execute(cmd, cli.app)
}
Some(Commands::Mcp(cmd)) => cc_switch_lib::cli::commands::mcp::execute(cmd, cli.app),
Some(Commands::Prompts(cmd)) => {
cc_switch_lib::cli::commands::prompts::execute(cmd, cli.app)
}
Some(Commands::Skills(cmd)) => cc_switch_lib::cli::commands::skills::execute(cmd, cli.app),
Some(Commands::Config(cmd)) => cc_switch_lib::cli::commands::config::execute(cmd, cli.app),
Some(Commands::Proxy(cmd)) => cc_switch_lib::cli::commands::proxy::execute(cmd),
Some(Commands::Failover(cmd)) => {
cc_switch_lib::cli::commands::failover::execute(cmd, cli.app)
}
#[cfg(unix)]
Some(Commands::Start(cmd)) => cc_switch_lib::cli::commands::start::execute(cmd),
Some(Commands::Env(cmd)) => cc_switch_lib::cli::commands::env::execute(cmd, cli.app),
Some(Commands::Update(cmd)) => cc_switch_lib::cli::commands::update::execute(cmd),
Some(Commands::Completions(cmd)) => cc_switch_lib::cli::commands::completions::execute(cmd),
Some(Commands::Internal(cmd)) => cc_switch_lib::cli::commands::internal::execute(cmd),
}
}
fn prompt_legacy_config_migration() {
let Some((old_dir, new_dir)) = cc_switch_lib::legacy_config_migration_paths() else {
return;
};
eprintln!(
"Detected legacy config at {}\n\
Migrate config to {}? (old directory will be preserved)",
old_dir.display(),
new_dir.display()
);
eprint!("[Y/n] ");
let _ = io::stderr().flush();
let should_migrate = read_legacy_migration_answer(io::stdin().lock());
if should_migrate {
cc_switch_lib::migrate_legacy_config_dir_if_needed();
return;
}
cc_switch_lib::skip_legacy_config_dir_migration();
eprintln!("cc-switch: migration skipped (marker written)");
}
fn read_legacy_migration_answer<R: BufRead>(mut reader: R) -> bool {
let mut input = String::new();
if reader.read_line(&mut input).is_err() {
return true;
}
let answer = input.trim().to_lowercase();
answer.is_empty() || answer == "y" || answer == "yes"
}
fn command_requires_startup_state(command: &Option<Commands>) -> bool {
match command {
Some(Commands::Completions(_))
| Some(Commands::Update(_))
| Some(Commands::Internal(_)) => false,
_ => true,
}
}
fn initialize_startup_state_if_needed(command: &Option<Commands>) -> Result<(), AppError> {
if command_requires_startup_state(command) {
let _state = cc_switch_lib::AppState::try_new_with_startup_recovery()?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{command_requires_startup_state, initialize_startup_state_if_needed};
use cc_switch_lib::cli::Cli;
use clap::Parser;
use serial_test::serial;
use std::{env, ffi::OsString, path::Path};
struct ConfigDirEnvGuard {
original_legacy: Option<OsString>,
original_tui: Option<OsString>,
}
impl ConfigDirEnvGuard {
fn set(path: &Path) -> Self {
let original_legacy = env::var_os("CC_SWITCH_CONFIG_DIR");
let original_tui = env::var_os("CC_SWITCH_TUI_CONFIG_DIR");
unsafe {
env::set_var("CC_SWITCH_CONFIG_DIR", path);
env::remove_var("CC_SWITCH_TUI_CONFIG_DIR");
}
Self {
original_legacy,
original_tui,
}
}
}
impl Drop for ConfigDirEnvGuard {
fn drop(&mut self) {
match self.original_legacy.as_ref() {
Some(value) => unsafe { env::set_var("CC_SWITCH_CONFIG_DIR", value) },
None => unsafe { env::remove_var("CC_SWITCH_CONFIG_DIR") },
}
match self.original_tui.as_ref() {
Some(value) => unsafe { env::set_var("CC_SWITCH_TUI_CONFIG_DIR", value) },
None => unsafe { env::remove_var("CC_SWITCH_TUI_CONFIG_DIR") },
}
}
}
fn seed_future_schema_database(config_dir: &Path) {
std::fs::create_dir_all(config_dir).expect("create config dir");
let db_path = config_dir.join("cc-switch.db");
let conn = rusqlite::Connection::open(&db_path).expect("open sqlite db");
conn.execute("PRAGMA user_version = 999;", [])
.expect("set future schema version");
}
#[test]
fn update_and_completions_skip_startup_state() {
let update = Cli::parse_from(["cc-switch-tui", "update"]);
let completions_generate = Cli::parse_from(["cc-switch-tui", "completions", "bash"]);
let completions_install = Cli::parse_from(["cc-switch-tui", "completions", "install"]);
let completions_status = Cli::parse_from(["cc-switch-tui", "completions", "status"]);
let completions_uninstall = Cli::parse_from([
"cc-switch-tui",
"completions",
"uninstall",
"--shell",
"bash",
]);
let internal_capture = Cli::parse_from([
"cc-switch-tui",
"internal",
"capture-codex-temp",
"official",
"/tmp/codex-home",
]);
let provider = Cli::parse_from(["cc-switch-tui", "provider", "list"]);
assert!(!command_requires_startup_state(&update.command));
assert!(!command_requires_startup_state(
&completions_generate.command
));
assert!(!command_requires_startup_state(
&completions_install.command
));
assert!(!command_requires_startup_state(&completions_status.command));
assert!(!command_requires_startup_state(
&completions_uninstall.command
));
assert!(!command_requires_startup_state(&internal_capture.command));
assert!(command_requires_startup_state(&provider.command));
}
#[test]
#[serial]
fn update_bypasses_future_schema_database_gate() {
let temp = tempfile::tempdir().expect("create temp dir");
seed_future_schema_database(temp.path());
let _guard = ConfigDirEnvGuard::set(temp.path());
let cli = Cli::parse_from(["cc-switch-tui", "update"]);
initialize_startup_state_if_needed(&cli.command)
.expect("update should not touch startup state");
}
#[test]
#[serial]
fn internal_commands_bypass_future_schema_database_gate() {
let temp = tempfile::tempdir().expect("create temp dir");
seed_future_schema_database(temp.path());
let _guard = ConfigDirEnvGuard::set(temp.path());
let cli = Cli::parse_from([
"cc-switch-tui",
"internal",
"capture-codex-temp",
"official",
"/tmp/codex-home",
]);
initialize_startup_state_if_needed(&cli.command)
.expect("internal commands should not touch startup state");
}
#[test]
#[serial]
fn provider_commands_still_fail_on_future_schema_database() {
let temp = tempfile::tempdir().expect("create temp dir");
seed_future_schema_database(temp.path());
let _guard = ConfigDirEnvGuard::set(temp.path());
let cli = Cli::parse_from(["cc-switch-tui", "provider", "list"]);
let err = initialize_startup_state_if_needed(&cli.command)
.expect_err("provider command should still require startup state");
assert!(
err.to_string().contains("由较新版本的 CC Switch 创建"),
"unexpected error: {err}"
);
}
}