1use clap::{CommandFactory, Parser, Subcommand};
2use clap_complete::Shell;
3use std::io::Write;
4
5mod claude_temp_launch;
6mod codex_temp_launch;
7pub mod commands;
8pub mod editor;
9pub mod i18n;
10pub mod interactive;
11pub mod terminal;
12pub mod tui;
13pub mod ui;
14
15use crate::app_config::AppType;
16
17#[derive(Parser)]
18#[command(
19 name = "cc-switch-tui",
20 disable_version_flag = true,
21 about = "All-in-One Assistant for Claude Code, Codex, Gemini, OpenCode, OpenClaw & Hermes",
22 long_about = "Unified management for Claude Code, Codex, Gemini, OpenCode, OpenClaw, and Hermes provider configurations, MCP servers, skills, prompts, local proxy routes, and environment checks.\n\nRun without arguments to enter interactive mode."
23)]
24pub struct Cli {
25 #[arg(short = 'V', long = "version", global = true)]
27 pub version: bool,
28
29 #[arg(short, long, global = true, value_enum)]
31 pub app: Option<AppType>,
32
33 #[arg(short, long, global = true)]
35 pub verbose: bool,
36
37 #[command(subcommand)]
38 pub command: Option<Commands>,
39}
40
41#[derive(Subcommand)]
42pub enum Commands {
43 #[command(subcommand)]
45 Provider(commands::provider::ProviderCommand),
46
47 #[command(subcommand)]
49 Mcp(commands::mcp::McpCommand),
50
51 #[command(subcommand)]
53 Prompts(commands::prompts::PromptsCommand),
54
55 #[command(subcommand)]
57 Skills(commands::skills::SkillsCommand),
58
59 #[command(subcommand)]
61 Config(commands::config::ConfigCommand),
62
63 #[command(subcommand)]
65 Proxy(commands::proxy::ProxyCommand),
66
67 #[command(subcommand)]
69 Failover(commands::failover::FailoverCommand),
70
71 #[cfg(unix)]
73 #[command(subcommand)]
74 Start(commands::start::StartCommand),
75
76 #[command(subcommand)]
78 Env(commands::env::EnvCommand),
79
80 Update(commands::update::UpdateCommand),
82
83 #[command(alias = "ui")]
85 Interactive,
86
87 Completions(commands::completions::CompletionsCommand),
89
90 #[command(name = "internal", hide = true, subcommand)]
91 Internal(commands::internal::InternalCommand),
92}
93
94pub fn generate_completions(shell: Shell) {
96 let stdout = std::io::stdout();
97 let mut handle = stdout.lock();
98 generate_completions_to(shell, &mut handle);
99}
100
101pub(crate) fn generate_completions_to<W: Write>(shell: Shell, writer: &mut W) {
102 let mut cmd = Cli::command();
103 let name = cmd.get_name().to_string();
104 clap_complete::generate(shell, &mut cmd, name, writer);
105}
106
107fn non_empty(value: Option<&str>) -> Option<&str> {
108 value.map(str::trim).filter(|value| !value.is_empty())
109}
110
111fn format_version_string(
112 name: &str,
113 version: &str,
114 git_hash: Option<&str>,
115 build_time: Option<&str>,
116 is_clean_commit: bool,
117) -> String {
118 let mut metadata = Vec::new();
119
120 if let Some(git_hash) = non_empty(git_hash) {
121 if is_clean_commit {
122 metadata.push(git_hash.to_string());
123 } else {
124 metadata.push(format!("{git_hash}-dirty"));
125 }
126 }
127
128 if let Some(build_time) = non_empty(build_time) {
129 metadata.push(build_time.to_string());
130 }
131
132 if metadata.is_empty() {
133 format!("{name} {version}")
134 } else {
135 format!("{name} {version} ({})", metadata.join(" "))
136 }
137}
138
139pub fn version_string() -> String {
140 let version = non_empty(option_env!("CC_SWITCH_TUI_GIT_TAG_VERSION"))
141 .unwrap_or(env!("CARGO_PKG_VERSION"));
142
143 format_version_string(
144 env!("CARGO_PKG_NAME"),
145 version,
146 option_env!("CC_SWITCH_TUI_BUILD_GIT_HASH"),
147 option_env!("CC_SWITCH_TUI_BUILD_TIME"),
148 option_env!("CC_SWITCH_TUI_GIT_IS_CLEAN_COMMIT").is_some(),
149 )
150}
151
152#[cfg(test)]
153mod tests {
154 use clap::{CommandFactory, Parser};
155 use std::ffi::OsString;
156
157 use super::{format_version_string, Cli, Commands};
158 use crate::cli::commands::completions::{
159 CompletionLifecycleCommand, CompletionsAction, ManagedShellSelection,
160 };
161
162 #[test]
163 fn long_help_mentions_prompts_and_proxy_routes() {
164 let mut cmd = Cli::command();
165 let help = cmd.render_long_help().to_string();
166
167 assert!(help.contains("prompts, local proxy routes, and environment checks"));
168 }
169
170 #[test]
171 fn parses_manual_version_flags_without_stealing_verbose() {
172 let long = Cli::parse_from(["cc-switch-tui", "--version"]);
173 let short = Cli::parse_from(["cc-switch-tui", "-V"]);
174 let subcommand = Cli::parse_from(["cc-switch-tui", "provider", "list", "--version"]);
175 let verbose = Cli::parse_from(["cc-switch-tui", "-v"]);
176
177 assert!(long.version);
178 assert!(short.version);
179 assert!(subcommand.version);
180 assert!(!verbose.version);
181 assert!(verbose.verbose);
182 }
183
184 #[test]
185 fn version_string_includes_dirty_suffix_for_unclean_builds() {
186 let version = format_version_string(
187 "cc-switch-tui",
188 "0.1.1",
189 Some("abc1234"),
190 Some("2026-05-11 12:34:56 +08:00"),
191 false,
192 );
193
194 assert_eq!(
195 version,
196 "cc-switch-tui 0.1.1 (abc1234-dirty 2026-05-11 12:34:56 +08:00)"
197 );
198 }
199
200 #[test]
201 fn version_string_omits_metadata_when_build_env_is_missing() {
202 let version = format_version_string("cc-switch-tui", "0.1.1", None, None, true);
203
204 assert_eq!(version, "cc-switch-tui 0.1.1");
205 }
206
207 #[test]
208 fn skills_help_uses_current_storage_description() {
209 let mut cmd = Cli::command();
210 let skills = cmd
211 .find_subcommand_mut("skills")
212 .expect("skills subcommand should exist");
213 let help = skills.render_long_help().to_string();
214
215 assert!(!help.contains("skills.json"));
216 assert!(help.contains("SSOT + database state"));
217 }
218
219 #[test]
220 fn parses_proxy_serve_subcommand() {
221 let cli = Cli::parse_from(["cc-switch-tui", "proxy", "serve", "--listen-port", "0"]);
222
223 match cli.command {
224 Some(Commands::Proxy(super::commands::proxy::ProxyCommand::Serve {
225 listen_port,
226 ..
227 })) => {
228 assert_eq!(listen_port, Some(0));
229 }
230 _ => panic!("expected proxy serve command"),
231 }
232 }
233
234 #[test]
235 fn parses_proxy_serve_takeover_flags() {
236 let cli = Cli::parse_from([
237 "cc-switch-tui",
238 "proxy",
239 "serve",
240 "--takeover",
241 "claude",
242 "--takeover",
243 "codex",
244 ]);
245
246 match cli.command {
247 Some(Commands::Proxy(super::commands::proxy::ProxyCommand::Serve {
248 takeovers,
249 ..
250 })) => {
251 assert_eq!(
252 takeovers,
253 vec![super::AppType::Claude, super::AppType::Codex]
254 );
255 }
256 _ => panic!("expected proxy serve command with takeover flags"),
257 }
258 }
259
260 #[test]
261 fn parses_proxy_enable_subcommand() {
262 let cli = Cli::parse_from(["cc-switch-tui", "proxy", "enable"]);
263
264 match cli.command {
265 Some(Commands::Proxy(super::commands::proxy::ProxyCommand::Enable)) => {}
266 _ => panic!("expected proxy enable command"),
267 }
268 }
269
270 #[test]
271 fn parses_proxy_disable_subcommand() {
272 let cli = Cli::parse_from(["cc-switch-tui", "proxy", "disable"]);
273
274 match cli.command {
275 Some(Commands::Proxy(super::commands::proxy::ProxyCommand::Disable)) => {}
276 _ => panic!("expected proxy disable command"),
277 }
278 }
279
280 #[test]
281 fn parses_failover_enable_subcommand() {
282 let cli = Cli::parse_from(["cc-switch", "failover", "enable"]);
283
284 match cli.command {
285 Some(Commands::Failover(super::commands::failover::FailoverCommand::Enable)) => {}
286 _ => panic!("expected failover enable command"),
287 }
288 }
289
290 #[test]
291 fn parses_failover_disable_subcommand() {
292 let cli = Cli::parse_from(["cc-switch", "failover", "disable"]);
293
294 match cli.command {
295 Some(Commands::Failover(super::commands::failover::FailoverCommand::Disable)) => {}
296 _ => panic!("expected failover disable command"),
297 }
298 }
299
300 #[test]
301 fn parses_failover_list_subcommand() {
302 let cli = Cli::parse_from(["cc-switch", "failover", "list"]);
303
304 match cli.command {
305 Some(Commands::Failover(super::commands::failover::FailoverCommand::List)) => {}
306 _ => panic!("expected failover list command"),
307 }
308 }
309
310 #[test]
311 fn parses_failover_add_subcommand() {
312 let cli = Cli::parse_from(["cc-switch", "failover", "add", "p1"]);
313
314 match cli.command {
315 Some(Commands::Failover(super::commands::failover::FailoverCommand::Add { id })) => {
316 assert_eq!(id, "p1");
317 }
318 _ => panic!("expected failover add command"),
319 }
320 }
321
322 #[test]
323 fn parses_failover_remove_subcommand() {
324 let cli = Cli::parse_from(["cc-switch", "failover", "remove", "p1"]);
325
326 match cli.command {
327 Some(Commands::Failover(super::commands::failover::FailoverCommand::Remove { id })) => {
328 assert_eq!(id, "p1");
329 }
330 _ => panic!("expected failover remove command"),
331 }
332 }
333
334 #[test]
335 fn parses_failover_move_subcommand() {
336 let cli = Cli::parse_from(["cc-switch", "failover", "move", "p1", "up"]);
337
338 match cli.command {
339 Some(Commands::Failover(super::commands::failover::FailoverCommand::Move {
340 id,
341 direction,
342 })) => {
343 assert_eq!(id, "p1");
344 assert_eq!(
345 direction,
346 super::commands::failover::FailoverMoveDirection::Up
347 );
348 }
349 _ => panic!("expected failover move command"),
350 }
351 }
352
353 #[test]
354 fn parses_failover_clear_subcommand() {
355 let cli = Cli::parse_from(["cc-switch", "failover", "clear", "--yes"]);
356
357 match cli.command {
358 Some(Commands::Failover(super::commands::failover::FailoverCommand::Clear { yes })) => {
359 assert!(yes);
360 }
361 _ => panic!("expected failover clear command"),
362 }
363 }
364
365 #[test]
366 fn parses_failover_show_with_app() {
367 let cli = Cli::parse_from(["cc-switch", "--app", "codex", "failover", "show"]);
368
369 assert_eq!(cli.app, Some(super::AppType::Codex));
370 match cli.command {
371 Some(Commands::Failover(super::commands::failover::FailoverCommand::Show)) => {}
372 _ => panic!("expected failover show command"),
373 }
374 }
375
376 #[cfg(unix)]
377 #[test]
378 fn parses_start_claude_subcommand() {
379 let cli = Cli::parse_from(["cc-switch-tui", "start", "claude", "demo"]);
380
381 match cli.command {
382 Some(Commands::Start(super::commands::start::StartCommand::Claude {
383 selector,
384 native_args,
385 })) => {
386 assert_eq!(selector, "demo");
387 assert!(native_args.is_empty());
388 }
389 _ => panic!("expected start claude command"),
390 }
391 }
392
393 #[cfg(unix)]
394 #[test]
395 fn parses_start_claude_native_args_after_double_dash() {
396 let cli = Cli::parse_from([
397 "cc-switch-tui",
398 "start",
399 "claude",
400 "demo",
401 "--",
402 "--dangerously-skip-permissions",
403 ]);
404
405 match cli.command {
406 Some(Commands::Start(super::commands::start::StartCommand::Claude {
407 selector,
408 native_args,
409 })) => {
410 assert_eq!(selector, "demo");
411 assert_eq!(
412 native_args,
413 vec![OsString::from("--dangerously-skip-permissions")]
414 );
415 }
416 _ => panic!("expected start claude command with native args"),
417 }
418 }
419
420 #[cfg(unix)]
421 #[test]
422 fn rejects_start_claude_native_args_without_double_dash() {
423 let result = Cli::try_parse_from([
424 "cc-switch-tui",
425 "start",
426 "claude",
427 "demo",
428 "--dangerously-skip-permissions",
429 ]);
430 let rendered = match result {
431 Ok(_) => panic!("native args without `--` should be rejected"),
432 Err(err) => err.to_string(),
433 };
434
435 assert!(rendered.contains("-- --dangerously-skip-permissions"));
436 }
437
438 #[cfg(unix)]
439 #[test]
440 fn parses_start_codex_subcommand() {
441 let cli = Cli::parse_from(["cc-switch-tui", "start", "codex", "demo"]);
442
443 match cli.command {
444 Some(Commands::Start(super::commands::start::StartCommand::Codex {
445 selector,
446 native_args,
447 })) => {
448 assert_eq!(selector, "demo");
449 assert!(native_args.is_empty());
450 }
451 _ => panic!("expected start codex command"),
452 }
453 }
454
455 #[cfg(unix)]
456 #[test]
457 fn parses_start_codex_multiple_native_args_after_double_dash() {
458 let cli = Cli::parse_from([
459 "cc-switch-tui",
460 "start",
461 "codex",
462 "demo",
463 "--",
464 "--model",
465 "gpt-5.4",
466 "--profile",
467 "local",
468 ]);
469
470 match cli.command {
471 Some(Commands::Start(super::commands::start::StartCommand::Codex {
472 selector,
473 native_args,
474 })) => {
475 assert_eq!(selector, "demo");
476 assert_eq!(
477 native_args,
478 vec![
479 OsString::from("--model"),
480 OsString::from("gpt-5.4"),
481 OsString::from("--profile"),
482 OsString::from("local"),
483 ]
484 );
485 }
486 _ => panic!("expected start codex command with native args"),
487 }
488 }
489
490 #[cfg(unix)]
491 #[test]
492 fn start_claude_help_mentions_double_dash_passthrough_examples() {
493 let mut cmd = Cli::command();
494 let start = cmd
495 .find_subcommand_mut("start")
496 .expect("start subcommand should exist");
497 let claude = start
498 .find_subcommand_mut("claude")
499 .expect("claude subcommand should exist");
500 let help = claude.render_long_help().to_string();
501
502 assert!(help.contains("Native Claude CLI arguments to pass through after `--`"));
503 assert!(help.contains("cc-switch start claude demo -- --dangerously-skip-permissions"));
504 }
505
506 #[cfg(unix)]
507 #[test]
508 fn start_codex_help_mentions_double_dash_passthrough_examples() {
509 let mut cmd = Cli::command();
510 let start = cmd
511 .find_subcommand_mut("start")
512 .expect("start subcommand should exist");
513 let codex = start
514 .find_subcommand_mut("codex")
515 .expect("codex subcommand should exist");
516 let help = codex.render_long_help().to_string();
517
518 assert!(help.contains("Native Codex CLI arguments to pass through after `--`"));
519 assert!(help.contains("cc-switch start codex demo -- --model gpt-5.4"));
520 }
521
522 #[test]
523 fn parses_provider_stream_check_subcommand() {
524 let cli = Cli::parse_from(["cc-switch-tui", "provider", "stream-check", "demo"]);
525
526 match cli.command {
527 Some(Commands::Provider(super::commands::provider::ProviderCommand::StreamCheck {
528 id,
529 })) => {
530 assert_eq!(id, "demo");
531 }
532 _ => panic!("expected provider stream-check command"),
533 }
534 }
535
536 #[test]
537 fn parses_provider_fetch_models_subcommand() {
538 let cli = Cli::parse_from(["cc-switch-tui", "provider", "fetch-models", "demo"]);
539
540 match cli.command {
541 Some(Commands::Provider(super::commands::provider::ProviderCommand::FetchModels {
542 id,
543 })) => {
544 assert_eq!(id, "demo");
545 }
546 _ => panic!("expected provider fetch-models command"),
547 }
548 }
549
550 #[test]
551 fn parses_provider_export_subcommand() {
552 let cli = Cli::parse_from(["cc-switch-tui", "provider", "export", "demo"]);
553
554 match cli.command {
555 Some(Commands::Provider(super::commands::provider::ProviderCommand::Export {
556 id,
557 output,
558 })) => {
559 assert_eq!(id, "demo");
560 assert_eq!(output, None);
561 }
562 _ => panic!("expected provider export command"),
563 }
564 }
565
566 #[test]
567 fn parses_provider_export_with_output_subcommand() {
568 let cli = Cli::parse_from([
569 "cc-switch-tui",
570 "provider",
571 "export",
572 "demo",
573 "--output",
574 "/tmp/provider-settings.json",
575 ]);
576
577 match cli.command {
578 Some(Commands::Provider(super::commands::provider::ProviderCommand::Export {
579 id,
580 output,
581 })) => {
582 assert_eq!(id, "demo");
583 assert_eq!(
584 output,
585 Some(std::path::PathBuf::from("/tmp/provider-settings.json"))
586 );
587 }
588 _ => panic!("expected provider export command with output"),
589 }
590 }
591
592 #[test]
593 fn parses_config_webdav_show_subcommand() {
594 let cli = Cli::parse_from(["cc-switch-tui", "config", "webdav", "show"]);
595
596 match cli.command {
597 Some(Commands::Config(super::commands::config::ConfigCommand::WebDav(
598 super::commands::config_webdav::WebDavCommand::Show,
599 ))) => {}
600 _ => panic!("expected config webdav show command"),
601 }
602 }
603
604 #[test]
605 fn parses_config_webdav_set_subcommand() {
606 let cli = Cli::parse_from([
607 "cc-switch-tui",
608 "config",
609 "webdav",
610 "set",
611 "--base-url",
612 "https://dav.example.com/root",
613 "--username",
614 "demo",
615 "--password",
616 "secret",
617 "--enable",
618 ]);
619
620 match cli.command {
621 Some(Commands::Config(super::commands::config::ConfigCommand::WebDav(
622 super::commands::config_webdav::WebDavCommand::Set {
623 base_url,
624 username,
625 password,
626 enable,
627 ..
628 },
629 ))) => {
630 assert_eq!(base_url.as_deref(), Some("https://dav.example.com/root"));
631 assert_eq!(username.as_deref(), Some("demo"));
632 assert_eq!(password.as_deref(), Some("secret"));
633 assert!(enable);
634 }
635 _ => panic!("expected config webdav set command"),
636 }
637 }
638
639 #[test]
640 fn parses_config_webdav_check_connection_subcommand() {
641 let cli = Cli::parse_from(["cc-switch-tui", "config", "webdav", "check-connection"]);
642
643 match cli.command {
644 Some(Commands::Config(super::commands::config::ConfigCommand::WebDav(
645 super::commands::config_webdav::WebDavCommand::CheckConnection,
646 ))) => {}
647 _ => panic!("expected config webdav check-connection command"),
648 }
649 }
650
651 #[test]
652 fn config_common_set_help_describes_snippet_as_primary_contract() {
653 let mut cmd = Cli::command();
654 let config = cmd
655 .find_subcommand_mut("config")
656 .expect("config subcommand should exist");
657 let common = config
658 .find_subcommand_mut("common")
659 .expect("common subcommand should exist");
660 let set = common
661 .find_subcommand_mut("set")
662 .expect("set subcommand should exist");
663 let help = set.render_long_help().to_string();
664
665 assert!(help.contains("--snippet <SNIPPET>"));
666 assert!(help.contains("Inline snippet text"));
667 assert!(!help.contains("Compatibility flag for inline snippet text"));
668 assert!(help.contains("Compatibility:"));
669 assert!(help.contains("--json <SNIPPET>"));
670 assert!(help.contains("Legacy alias for --snippet <SNIPPET>"));
671 assert!(help.contains("Claude/Gemini"));
672 assert!(help.contains("OpenCode"));
673 assert!(help.contains("Codex"));
674 assert!(!help.contains("Apply to current provider immediately"));
675 assert!(help.contains("live config"));
676 assert!(help.contains("applicable"));
677 }
678
679 #[test]
680 fn parses_config_common_set_legacy_json_alias() {
681 let cli = Cli::parse_from(["cc-switch-tui", "config", "common", "set", "--json", "{}"]);
682
683 match cli.command {
684 Some(Commands::Config(super::commands::config::ConfigCommand::Common(_))) => {}
685 _ => panic!("expected config common set command"),
686 }
687 }
688
689 #[test]
690 fn config_common_clear_help_marks_apply_as_compatibility_flag() {
691 let mut cmd = Cli::command();
692 let config = cmd
693 .find_subcommand_mut("config")
694 .expect("config subcommand should exist");
695 let common = config
696 .find_subcommand_mut("common")
697 .expect("common subcommand should exist");
698 let clear = common
699 .find_subcommand_mut("clear")
700 .expect("clear subcommand should exist");
701 let help = clear.render_long_help().to_string();
702
703 assert!(!help.contains("Apply to current provider immediately"));
704 assert!(help.contains("Compatibility flag"));
705 assert!(help.contains("live config"));
706 assert!(help.contains("applicable"));
707 }
708
709 #[test]
710 fn parses_env_tools_subcommand() {
711 let cli = Cli::parse_from(["cc-switch-tui", "env", "tools"]);
712
713 match cli.command {
714 Some(Commands::Env(super::commands::env::EnvCommand::Tools)) => {}
715 _ => panic!("expected env tools command"),
716 }
717 }
718
719 #[test]
720 fn parses_skills_repo_enable_subcommand() {
721 let cli = Cli::parse_from(["cc-switch-tui", "skills", "repos", "enable", "foo/bar"]);
722
723 match cli.command {
724 Some(Commands::Skills(super::commands::skills::SkillsCommand::Repos(
725 super::commands::skills::SkillReposCommand::Enable { url },
726 ))) => {
727 assert_eq!(url, "foo/bar");
728 }
729 _ => panic!("expected skills repos enable command"),
730 }
731 }
732
733 #[test]
734 fn parses_skills_repo_disable_subcommand() {
735 let cli = Cli::parse_from(["cc-switch-tui", "skills", "repos", "disable", "foo/bar"]);
736
737 match cli.command {
738 Some(Commands::Skills(super::commands::skills::SkillsCommand::Repos(
739 super::commands::skills::SkillReposCommand::Disable { url },
740 ))) => {
741 assert_eq!(url, "foo/bar");
742 }
743 _ => panic!("expected skills repos disable command"),
744 }
745 }
746
747 #[test]
748 fn parses_completions_bash_generator_path() {
749 let cli = Cli::parse_from(["cc-switch-tui", "completions", "bash"]);
750
751 match cli.command {
752 Some(Commands::Completions(command)) => {
753 assert_eq!(command.shell, Some(clap_complete::Shell::Bash));
754 assert!(command.action.is_none());
755 }
756 _ => panic!("expected completions generator command"),
757 }
758 }
759
760 #[test]
761 fn parses_completions_zsh_generator_path() {
762 let cli = Cli::parse_from(["cc-switch-tui", "completions", "zsh"]);
763
764 match cli.command {
765 Some(Commands::Completions(command)) => {
766 assert_eq!(command.shell, Some(clap_complete::Shell::Zsh));
767 assert!(command.action.is_none());
768 }
769 _ => panic!("expected completions generator command"),
770 }
771 }
772
773 #[test]
774 fn parses_completions_install() {
775 let cli = Cli::parse_from(["cc-switch-tui", "completions", "install"]);
776
777 match cli.command {
778 Some(Commands::Completions(command)) => match command.action {
779 Some(CompletionsAction::Install(args)) => {
780 assert_eq!(args.shell, ManagedShellSelection::Auto);
781 assert!(!args.activate);
782 }
783 _ => panic!("expected completions install subcommand"),
784 },
785 _ => panic!("expected completions install command"),
786 }
787 }
788
789 #[test]
790 fn parses_completions_install_with_shell_and_activate() {
791 let cli = Cli::parse_from([
792 "cc-switch-tui",
793 "completions",
794 "install",
795 "--shell",
796 "zsh",
797 "--activate",
798 ]);
799
800 match cli.command {
801 Some(Commands::Completions(command)) => match command.action {
802 Some(CompletionsAction::Install(args)) => {
803 assert_eq!(args.shell, ManagedShellSelection::Zsh);
804 assert!(args.activate);
805 }
806 _ => panic!("expected completions install subcommand"),
807 },
808 _ => panic!("expected completions install command"),
809 }
810 }
811
812 #[test]
813 fn parses_completions_status() {
814 let cli = Cli::parse_from(["cc-switch-tui", "completions", "status"]);
815
816 match cli.command {
817 Some(Commands::Completions(command)) => match command.action {
818 Some(CompletionsAction::Status(CompletionLifecycleCommand { shell })) => {
819 assert_eq!(shell, ManagedShellSelection::Auto);
820 }
821 _ => panic!("expected completions status subcommand"),
822 },
823 _ => panic!("expected completions status command"),
824 }
825 }
826
827 #[test]
828 fn parses_completions_uninstall_with_explicit_shell() {
829 let cli = Cli::parse_from([
830 "cc-switch-tui",
831 "completions",
832 "uninstall",
833 "--shell",
834 "bash",
835 ]);
836
837 match cli.command {
838 Some(Commands::Completions(command)) => match command.action {
839 Some(CompletionsAction::Uninstall(CompletionLifecycleCommand { shell })) => {
840 assert_eq!(shell, ManagedShellSelection::Bash);
841 }
842 _ => panic!("expected completions uninstall subcommand"),
843 },
844 _ => panic!("expected completions uninstall command"),
845 }
846 }
847
848 #[test]
849 fn rejects_completions_generator_with_activate_flag() {
850 let err = match Cli::try_parse_from(["cc-switch-tui", "completions", "bash", "--activate"])
851 {
852 Ok(_) => panic!("generator path should reject lifecycle-only flags"),
853 Err(err) => err,
854 };
855 let rendered = err.to_string();
856
857 assert!(rendered.contains("--activate"));
858 assert!(rendered.contains("unexpected argument"));
859 }
860}