1use clap::{ArgAction, ColorChoice, Parser, Subcommand, ValueHint};
2use colorchoice_clap::Color as ColorSelection;
3use std::env;
4use std::path::PathBuf;
5
6use crate::config::models::ModelId;
7
8mod acp;
9mod ask;
10mod background;
11mod bench;
12mod check;
13mod companion;
14mod config;
15mod dependencies;
16mod exec;
17mod models;
18mod plugins;
19mod pods;
20mod review;
21mod schedule;
22mod schema;
23mod secret;
24mod session_store;
25mod skills;
26mod webmcp;
27
28pub use acp::AgentClientProtocolTarget;
29pub use ask::{AskCommandOptions, AskOutputFormat};
30pub use background::BackgroundSubagentArgs;
31pub use bench::BenchAllocatorArgs;
32pub use check::CheckSubcommand;
33pub use companion::{CompanionCommand, CompanionSetupArgs};
34pub use config::{
35 ConfigCommand, ConfigFile, ConfigResetArgs, ContextConfig, LoggingConfig, PerformanceConfig, SecurityConfig,
36 ToolConfig,
37};
38pub use dependencies::{DependenciesSubcommand, ManagedDependency};
39pub use exec::{EvalOutputFormat, ExecEvalArgs, ExecResumeArgs, ExecSubcommand};
40pub use models::ModelCommands;
41pub use plugins::PluginsSubcommand;
42pub use pods::PodsCommands;
43pub use review::ReviewArgs;
44pub use schedule::{ScheduleCreateArgs, ScheduleSubcommand};
45pub use schema::{SchemaCommands, SchemaMode, SchemaOutputFormat};
46pub use secret::{MigrateArgs, SecretArgs, SecretProvider, SecretSubcommand};
47pub use session_store::SessionStoreCommand;
48pub use skills::{SkillsRefSubcommand, SkillsSubcommand};
49pub use webmcp::{WebmcpCommand, WebmcpPairArgs, WebmcpServeArgs};
50
51#[derive(Parser, Debug, Clone)]
52pub struct Cli {
53 #[command(flatten)]
55 pub color: ColorSelection,
56
57 #[arg(
59 value_name = "WORKSPACE",
60 value_hint = ValueHint::DirPath,
61 value_parser = parse_workspace_directory,
62 global = true,
63 last = true
64 )]
65 pub workspace_path: Option<PathBuf>,
66
67 #[arg(long, global = true)]
69 pub model: Option<String>,
70
71 #[arg(long, global = true)]
73 pub provider: Option<String>,
74
75 #[arg(long, global = true, default_value = crate::config::constants::defaults::DEFAULT_API_KEY_ENV)]
77 pub api_key_env: String,
78
79 #[arg(
81 long,
82 global = true,
83 alias = "workspace-dir",
84 value_name = "PATH",
85 value_hint = ValueHint::DirPath,
86 value_parser = parse_workspace_directory
87 )]
88 pub workspace: Option<PathBuf>,
89
90 #[arg(long, global = true)]
92 pub research_preview: bool,
93
94 #[arg(long, global = true, default_value = "moderate")]
96 pub security_level: String,
97
98 #[arg(long, global = true)]
100 pub show_file_diffs: bool,
101
102 #[arg(long, global = true, default_value_t = 5)]
104 pub max_concurrent_ops: usize,
105
106 #[arg(long, global = true, default_value_t = 30)]
108 pub api_rate_limit: usize,
109
110 #[arg(long, global = true, default_value_t = 10)]
112 pub max_tool_calls: usize,
113
114 #[arg(long, global = true)]
116 pub debug: bool,
117
118 #[arg(long, global = true)]
120 pub verbose: bool,
121
122 #[arg(short, long, global = true)]
124 pub quiet: bool,
125
126 #[arg(
128 short = 'c',
129 long = "config",
130 value_name = "KEY=VALUE|PATH",
131 action = ArgAction::Append,
132 global = true
133 )]
134 pub config: Vec<String>,
135
136 #[arg(long, global = true, default_value = "info")]
138 pub log_level: String,
139
140 #[arg(long, global = true)]
142 pub no_color: bool,
143
144 #[arg(long, global = true, value_name = "THEME")]
146 pub theme: Option<String>,
147
148 #[arg(short = 't', long, default_value_t = 250)]
150 pub tick_rate: u64,
151
152 #[arg(short = 'f', long, default_value_t = 60)]
154 pub frame_rate: u64,
155
156 #[arg(long, global = true)]
158 pub enable_skills: bool,
159
160 #[arg(long, global = true)]
162 pub chrome: bool,
163
164 #[arg(long = "no-chrome", global = true, conflicts_with = "chrome")]
166 pub no_chrome: bool,
167
168 #[arg(long, global = true)]
170 pub skip_confirmations: bool,
171
172 #[arg(long = "codex-experimental", global = true, conflicts_with = "no_codex_experimental")]
174 pub codex_experimental: bool,
175
176 #[arg(long = "no-codex-experimental", global = true, conflicts_with = "codex_experimental")]
178 pub no_codex_experimental: bool,
179
180 #[arg(
182 short = 'p',
183 long = "print",
184 value_name = "PROMPT",
185 value_hint = ValueHint::Other,
186 num_args = 0..=1,
187 default_missing_value = "",
188 global = true,
189 conflicts_with_all = ["full_auto"]
190 )]
191 pub print: Option<String>,
192
193 #[arg(
195 long = "full-auto",
196 global = true,
197 help = "Run non-interactively with full-auto permission review",
198 long_help = r#"Run non-interactively on top of the active primary agent.
199
200If no primary agent is explicitly selected or configured, VT Code selects the effective `auto` primary agent. Explicit choices, including `duck`, are honoured. Full-auto is an execution and permission layer, not a primary agent.
201
202Full-auto does not override explicit denies or grant tools outside `[automation.full_auto].allowed_tools`. Tools outside that allow-list are denied. Promptable actions inside the allow-list are routed through automatic permission review after deny and policy checks instead of asking. The run fails fast if it needs the defaulted `auto` primary agent and no effective `auto` exists."#,
203 value_name = "PROMPT",
204 num_args = 0..=1,
205 default_missing_value = "",
206 value_hint = ValueHint::Other
207 )]
208 pub full_auto: Option<String>,
209
210 #[arg(
212 short = 'r',
213 long = "resume",
214 global = true,
215 value_name = "SESSION_ID",
216 num_args = 0..=1,
217 default_missing_value = "__interactive__",
218 conflicts_with_all = ["continue_latest", "full_auto"]
219 )]
220 pub resume_session: Option<String>,
221
222 #[arg(
224 long = "continue",
225 visible_alias = "continue-session",
226 global = true,
227 conflicts_with_all = ["resume_session", "full_auto"]
228 )]
229 pub continue_latest: bool,
230
231 #[arg(
233 long = "fork-session",
234 global = true,
235 value_name = "SESSION_ID",
236 conflicts_with_all = ["resume_session", "continue_latest", "full_auto"]
237 )]
238 pub fork_session: Option<String>,
239
240 #[arg(long, global = true)]
242 pub all: bool,
243
244 #[arg(long = "session-id", global = true, value_name = "CUSTOM_SUFFIX")]
246 pub session_id: Option<String>,
247
248 #[arg(long, global = true)]
250 pub summarize: bool,
251
252 #[arg(long, global = true, value_name = "AGENT")]
254 pub agent: Option<String>,
255
256 #[arg(long = "allowed-tools", global = true, value_name = "TOOLS", action = ArgAction::Append)]
258 pub allowed_tools: Vec<String>,
259
260 #[arg(long = "disallowed-tools", global = true, value_name = "TOOLS", action = ArgAction::Append)]
262 pub disallowed_tools: Vec<String>,
263
264 #[arg(
266 long = "dangerously-skip-permissions",
267 global = true,
268 help = "Auto-approve promptable actions while respecting denies and policy blocks",
269 long_help = "Auto-approve promptable actions while still respecting explicit denies and policy blocks."
270 )]
271 pub dangerously_skip_permissions: bool,
272
273 #[arg(long, global = true)]
275 pub ide: bool,
276
277 #[command(subcommand)]
278 pub command: Option<Commands>,
279}
280
281#[derive(Subcommand, Debug, Clone)]
282pub enum Commands {
283 #[command(name = "companion")]
285 Companion {
286 #[command(subcommand)]
287 command: CompanionCommand,
288 },
289
290 #[command(name = "acp")]
292 AgentClientProtocol {
293 #[arg(value_enum, default_value_t = AgentClientProtocolTarget::Zed)]
295 target: AgentClientProtocolTarget,
296 },
297
298 #[command(name = "session-store")]
302 SessionStore {
303 #[command(subcommand)]
304 command: SessionStoreCommand,
305 },
306
307 Chat,
309
310 Continue,
321
322 Ask {
332 #[arg(
334 value_name = "PROMPT",
335 long_help = "The prompt to send to the model.\n\nOmit to read from stdin (piped input).\nUse '-' to explicitly force reading from stdin."
336 )]
337 prompt: Option<String>,
338 #[arg(
340 long = "output-format",
341 value_enum,
342 value_name = "FORMAT",
343 long_help = "Output format for the response.\n\nCurrently supports:\n json - Emit the response as a structured JSON document."
344 )]
345 output_format: Option<AskOutputFormat>,
346 },
347 Exec {
360 #[arg(
362 long,
363 long_help = "Stream newline-delimited JSON events to stdout.\nEach line is a JSON object representing an agent event (tool call, message, etc.).\nUseful for programmatic consumption and CI integration."
364 )]
365 json: bool,
366 #[arg(
368 long,
369 long_help = "Simulate execution without making changes.\nThe agent plans tool calls but does not execute mutating operations (file writes, shell commands).\nUseful for previewing what the agent would do."
370 )]
371 dry_run: bool,
372 #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath, long_help = "Write the full JSONL event transcript to this file.\nIncludes all agent events: tool calls, messages, errors, and metadata.")]
374 events: Option<PathBuf>,
375 #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath, long_help = "Write only the final agent message to this file.\nUseful for piping the agent's response into other tools.")]
377 last_message_file: Option<PathBuf>,
378 #[command(subcommand)]
380 command: Option<ExecSubcommand>,
381 #[arg(
383 value_name = "PROMPT",
384 long_help = "The prompt to execute.\n\nOmit to read from stdin (piped input).\nUse '-' to explicitly force reading from stdin.\nQuote multi-word prompts: vtcode exec \"fix the bug in auth.rs\""
385 )]
386 prompt: Option<String>,
387 },
388 Eval(ExecEvalArgs),
398 Schedule {
410 #[command(subcommand)]
411 command: ScheduleSubcommand,
412 },
413
414 #[command(name = "background-subagent", hide = true)]
416 BackgroundSubagent(BackgroundSubagentArgs),
417
418 #[command(
420 long_about = "Run a non-interactive code review.\n\nExamples:\n vtcode review\n vtcode review \"Review the full diff for correctness and regressions\"\n vtcode review --last-diff\n vtcode review --target HEAD~1..HEAD\n vtcode review --file src/main.rs --file crates/codegen/vtcode-core/src/lib.rs\n vtcode review --style security \"Focus on auth bypass and injection\""
421 )]
422 Review(ReviewArgs),
423
424 Schema {
426 #[command(subcommand)]
427 command: SchemaCommands,
428 },
429
430 ChatVerbose,
432
433 Analyze {
435 #[arg(value_name = "TYPE", default_value = "full")]
437 analysis_type: String,
438 },
439
440 #[command(name = "trajectory")]
442 Trajectory {
443 #[arg(long)]
445 file: Option<PathBuf>,
446 #[arg(long, default_value_t = 10)]
448 top: usize,
449 },
450
451 Notify {
453 #[arg(long, value_name = "TITLE")]
455 title: Option<String>,
456 #[arg(value_name = "MESSAGE")]
458 message: String,
459 },
460
461 Benchmark {
463 #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
465 task_file: Option<PathBuf>,
466 #[arg(long, value_name = "JSON")]
468 task: Option<String>,
469 #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
471 output: Option<PathBuf>,
472 #[arg(long, value_name = "COUNT")]
474 max_tasks: Option<usize>,
475 },
476
477 #[command(name = "bench-allocator")]
485 BenchAllocator(BenchAllocatorArgs),
486
487 CreateProject {
489 name: String,
490 #[arg(long = "feature", value_name = "FEATURE", action = ArgAction::Append)]
491 features: Vec<String>,
492 },
493
494 Revert {
496 #[arg(short, long)]
498 turn: usize,
499 #[arg(long)]
501 partial: Option<String>,
502 },
503
504 Snapshots,
506
507 #[command(name = "cleanup-snapshots")]
518 CleanupSnapshots {
519 #[arg(short, long, default_value_t = 50)]
524 max: usize,
525 },
526
527 Init {
536 #[arg(
538 long,
539 short = 'f',
540 long_help = "Overwrite AGENTS.md without confirmation.\nUse this in CI/CD or scripts where interactive prompts are not possible."
541 )]
542 force: bool,
543 },
544
545 #[command(name = "init-project")]
555 InitProject {
556 #[arg(
558 long,
559 long_help = "Name for the project.\nDefaults to the current directory name if not specified."
560 )]
561 name: Option<String>,
562 #[arg(long, long_help = "Overwrite existing project structure without confirmation.")]
564 force: bool,
565 #[arg(
567 long,
568 long_help = "Move existing config and cache files into the new project structure."
569 )]
570 migrate: bool,
571 },
572
573 Config {
587 #[command(subcommand)]
588 command: Option<ConfigCommand>,
589 #[arg(
591 long,
592 long_help = "Write the configuration to this path.\nDefaults to ./vtcode.toml in the current directory."
593 )]
594 output: Option<PathBuf>,
595 #[arg(
597 long,
598 long_help = "Write the configuration to the canonical user config directory.\nThis sets global defaults for all workspaces."
599 )]
600 global: bool,
601 },
602
603 Login {
615 #[arg(long_help = "The provider to authenticate with.\nSupported: openai, openrouter, copilot, codex")]
617 provider: String,
618 #[arg(
620 long,
621 default_value_t = false,
622 long_help = "Use the device-code OAuth flow.\nCurrently supported only for the `codex` provider.\nOpens a browser URL and asks you to enter a code."
623 )]
624 device_code: bool,
625 #[arg(
628 long,
629 default_value_t = false,
630 long_help = "Import ChatGPT OAuth tokens from Codex's auth.json.\n\
631 Requires that you have already run `codex login`.\n\
632 Currently supported only for the `openai` provider."
633 )]
634 from_codex: bool,
635 },
636
637 Logout {
645 #[arg(long_help = "The provider to deauthenticate.\nSupported: openai, openrouter, copilot, codex")]
647 provider: String,
648 },
649
650 Auth {
660 #[arg(long_help = "Show status for a single provider.\nOmit to show status for all supported providers.")]
662 provider: Option<String>,
663 },
664
665 #[command(name = "tool-policy")]
667 ToolPolicy {
668 #[command(subcommand)]
669 command: crate::cli::tool_policy_commands::ToolPolicyCommands,
670 },
671
672 #[command(name = "mcp")]
674 Mcp {
675 #[command(subcommand)]
676 command: crate::mcp::cli::McpCommands,
677 },
678
679 #[command(name = "a2a")]
681 A2a {
682 #[command(subcommand)]
683 command: super::super::a2a::cli::A2aCommands,
684 },
685
686 #[command(name = "webmcp")]
688 Webmcp {
689 #[command(subcommand)]
690 command: WebmcpCommand,
691 },
692
693 #[command(name = "app-server")]
695 AppServer {
696 #[arg(long, default_value = "stdio://")]
698 listen: String,
699 },
700
701 Models {
703 #[command(subcommand)]
704 command: ModelCommands,
705 },
706
707 #[command(name = "pods")]
709 Pods {
710 #[command(subcommand)]
711 command: PodsCommands,
712 },
713
714 Man {
716 command: Option<String>,
718 #[arg(long, conflicts_with = "command", requires = "output")]
720 all_pages: bool,
721 #[arg(short, long)]
723 output: Option<PathBuf>,
724 },
725
726 #[command(subcommand)]
738 Skills(SkillsSubcommand),
739
740 #[command(subcommand)]
752 Plugins(PluginsSubcommand),
753
754 #[command(name = "list-skills", hide = true)]
756 ListSkills {},
757
758 #[command(name = "dependencies", visible_alias = "deps", subcommand)]
768 Dependencies(DependenciesSubcommand),
769
770 Secret(SecretArgs),
782
783 Check {
790 #[command(subcommand)]
791 command: CheckSubcommand,
792 },
793
794 #[command(name = "update")]
807 Update {
808 #[arg(
810 long,
811 long_help = "Check whether a newer version is available without installing it."
812 )]
813 check: bool,
814 #[arg(
816 long,
817 long_help = "Reinstall or downgrade even if the current version is already the latest."
818 )]
819 force: bool,
820 #[arg(long, long_help = "Print available release versions from GitHub and exit.")]
822 list: bool,
823 #[arg(
825 long,
826 default_value = "10",
827 long_help = "Maximum number of versions to display with --list."
828 )]
829 limit: usize,
830 #[arg(
832 long,
833 value_name = "VERSION",
834 long_help = "Pin the binary to a specific version.\nAuto-updates are disabled until --unpin is used."
835 )]
836 pin: Option<String>,
837 #[arg(long, long_help = "Remove a previously set version pin and resume auto-updates.")]
839 unpin: bool,
840 #[arg(
842 long,
843 value_name = "CHANNEL",
844 long_help = "Switch the release channel.\nAccepted values: stable, beta, nightly."
845 )]
846 channel: Option<String>,
847 #[arg(
849 long,
850 long_help = "Display the current update configuration (channel, pin, intervals) and exit."
851 )]
852 show_config: bool,
853 },
854
855 #[command(name = "anthropic-api")]
857 AnthropicApi {
858 #[arg(long, default_value = "11434")]
860 port: u16,
861 #[arg(long, default_value = "127.0.0.1")]
863 host: String,
864 },
865}
866
867impl Default for Cli {
868 fn default() -> Self {
869 Self {
870 color: ColorSelection { color: ColorChoice::Auto },
871 workspace_path: None,
872 model: Some(ModelId::default().to_string()),
873 provider: Some("gemini".to_owned()),
874 api_key_env: "GEMINI_API_KEY".to_owned(),
875 workspace: None,
876 research_preview: false,
877 security_level: "moderate".to_owned(),
878 show_file_diffs: false,
879 max_concurrent_ops: 5,
880 api_rate_limit: 30,
881 max_tool_calls: 10,
882 verbose: false,
883 quiet: false,
884 config: Vec::new(),
885 log_level: "info".to_owned(),
886 no_color: false,
887 theme: None,
888 skip_confirmations: false,
889 codex_experimental: false,
890 no_codex_experimental: false,
891 print: None,
892 full_auto: None,
893 resume_session: None,
894 continue_latest: false,
895 fork_session: None,
896 all: false,
897 session_id: None,
898 summarize: false,
899 debug: false,
900 enable_skills: false,
901 tick_rate: 250,
902 frame_rate: 60,
903 agent: None,
904 allowed_tools: Vec::new(),
905 disallowed_tools: Vec::new(),
906 dangerously_skip_permissions: false,
907 ide: false,
908 chrome: false,
909 no_chrome: false,
910 command: Some(Commands::Chat),
911 }
912 }
913}
914
915impl Cli {
916 pub fn get_api_key_env(&self) -> String {
921 crate::config::api_keys::resolve_api_key_env(
922 self.provider
923 .as_deref()
924 .unwrap_or(crate::config::constants::defaults::DEFAULT_PROVIDER),
925 &self.api_key_env,
926 )
927 }
928
929 pub fn codex_experimental_override(&self) -> Option<bool> {
930 if self.codex_experimental {
931 Some(true)
932 } else if self.no_codex_experimental {
933 Some(false)
934 } else {
935 None
936 }
937 }
938}
939
940fn parse_workspace_directory(raw: &str) -> Result<PathBuf, String> {
941 let candidate = PathBuf::from(raw);
942 if !candidate.exists() {
943 return Err(format!(
944 "'{}' is not a valid workspace path or subcommand.\n\
945 Run `vtcode --help` to see available commands and options.",
946 raw
947 ));
948 }
949
950 Ok(candidate)
951}
952
953pub fn long_version() -> String {
954 let git_info = option_env!("VT_CODE_GIT_INFO").unwrap_or(env!("CARGO_PKG_VERSION"));
955 let path_diagnostics = match vtcode_commons::VtCodePaths::resolve() {
956 Ok(paths) => format!(
957 "Config directory: {}\nData directory: {}\nState directory: {}\nCache directory: {}\nRuntime directory: {}\nExecutable directory: {}\nLegacy directory: {}\nMigration marker: {}\nMigration report: {}",
958 paths.config_dir().display(),
959 paths.data_dir().display(),
960 paths.state_dir().display(),
961 paths.cache_dir().display(),
962 paths.runtime_dir().display(),
963 paths.executable_dir().display(),
964 paths.legacy_dir().display(),
965 paths.migration_marker_path().display(),
966 paths.migration_report_path().display(),
967 ),
968 Err(error) => format!("Storage paths: unavailable ({error:#})"),
969 };
970 let environment = [
971 "HOME",
972 "VTCODE_HOME",
973 "VTCODE_CONFIG",
974 "VTCODE_CONFIG_PATH",
975 "VTCODE_DATA",
976 "CODEX_HOME",
977 "XDG_CONFIG_HOME",
978 "XDG_DATA_HOME",
979 "XDG_STATE_HOME",
980 "XDG_CACHE_HOME",
981 "XDG_RUNTIME_DIR",
982 "XDG_BIN_HOME",
983 "XDG_CONFIG_DIRS",
984 "XDG_DATA_DIRS",
985 ]
986 .iter()
987 .map(|name| {
988 let value = env::var_os(name)
989 .map(|value| value.to_string_lossy().into_owned())
990 .unwrap_or_else(|| "<not set>".to_string());
991 format!(" {name}={value}")
992 })
993 .collect::<Vec<_>>()
994 .join("\n");
995
996 format!(
997 "{}\n\nAuthors: {}\n{}\n\nEnvironment variables:\n{}",
998 git_info,
999 env!("CARGO_PKG_AUTHORS"),
1000 path_diagnostics,
1001 environment,
1002 )
1003}
1004
1005#[cfg(test)]
1006mod tests {
1007 use super::*;
1008 use clap::Parser;
1009
1010 #[test]
1011 fn long_version_includes_expected_sections() {
1012 let text = long_version();
1013 assert!(text.contains("Authors:"));
1014 assert!(text.contains("Config directory:"));
1015 assert!(text.contains("Data directory:"));
1016 assert!(text.contains("State directory:"));
1017 assert!(text.contains("Cache directory:"));
1018 assert!(text.contains("Runtime directory:"));
1019 assert!(text.contains("Executable directory:"));
1020 assert!(text.contains("Legacy directory:"));
1021 assert!(text.contains("Migration marker:"));
1022 assert!(text.contains("Migration report:"));
1023 assert!(text.contains("VTCODE_CONFIG"));
1024 assert!(text.contains("VTCODE_DATA"));
1025 }
1026
1027 #[test]
1028 fn long_version_starts_with_build_git_info() {
1029 let text = long_version();
1030 let expected = option_env!("VT_CODE_GIT_INFO").unwrap_or(env!("CARGO_PKG_VERSION"));
1031 assert!(text.starts_with(expected));
1032 }
1033
1034 #[test]
1035 fn config_file_api_key_env_uses_provider_default() {
1036 let cli = Cli::parse_from(["vtcode", "--provider", "minimax"]);
1037
1038 assert_eq!(cli.get_api_key_env(), "MINIMAX_API_KEY");
1039 }
1040
1041 #[test]
1042 fn config_file_api_key_env_preserves_explicit_override() {
1043 let cli = Cli::parse_from(["vtcode", "--provider", "openai", "--api-key-env", "CUSTOM_OPENAI_KEY"]);
1044
1045 assert_eq!(cli.get_api_key_env(), "CUSTOM_OPENAI_KEY");
1046 }
1047
1048 #[test]
1049 fn parses_app_server_command_with_stdio_listen_target() {
1050 let cli = Cli::parse_from(["vtcode", "app-server", "--listen", "stdio://"]);
1051
1052 assert!(matches!(
1053 cli.command,
1054 Some(Commands::AppServer { ref listen }) if listen == "stdio://"
1055 ));
1056 }
1057
1058 #[test]
1059 fn parses_init_force_flag() {
1060 let cli = Cli::parse_from(["vtcode", "init", "--force"]);
1061
1062 assert!(matches!(cli.command, Some(Commands::Init { force: true })));
1063 }
1064
1065 #[test]
1066 fn parses_codex_login_device_code_flag() {
1067 let cli = Cli::parse_from(["vtcode", "login", "codex", "--device-code"]);
1068
1069 assert!(matches!(
1070 cli.command,
1071 Some(Commands::Login {
1072 ref provider,
1073 device_code: true,
1074 from_codex: false
1075 }) if provider == "codex"
1076 ));
1077 }
1078
1079 #[test]
1080 fn parses_codex_experimental_flags() {
1081 let enabled = Cli::parse_from(["vtcode", "--codex-experimental"]);
1082 assert_eq!(enabled.codex_experimental_override(), Some(true));
1083
1084 let disabled = Cli::parse_from(["vtcode", "--no-codex-experimental"]);
1085 assert_eq!(disabled.codex_experimental_override(), Some(false));
1086 }
1087
1088 #[test]
1089 fn codex_experimental_flags_conflict() {
1090 let result = Cli::try_parse_from(["vtcode", "--codex-experimental", "--no-codex-experimental"]);
1091
1092 result.unwrap_err();
1093 }
1094
1095 #[test]
1096 fn parses_create_project_feature_flags() {
1097 let cli = Cli::parse_from([
1098 "vtcode",
1099 "create-project",
1100 "demo",
1101 "--feature",
1102 "web",
1103 "--feature",
1104 "db",
1105 ]);
1106
1107 assert!(matches!(
1108 cli.command,
1109 Some(Commands::CreateProject { ref name, ref features })
1110 if name == "demo" && features == &vec!["web".to_string(), "db".to_string()]
1111 ));
1112 }
1113
1114 #[test]
1115 fn parses_revert_partial_long_flag() {
1116 let cli = Cli::parse_from(["vtcode", "revert", "--turn", "3", "--partial", "code"]);
1117
1118 assert!(matches!(
1119 cli.command,
1120 Some(Commands::Revert {
1121 turn: 3,
1122 partial: Some(ref scope)
1123 }) if scope == "code"
1124 ));
1125 }
1126
1127 #[test]
1128 fn parses_config_reset_workspace_target_by_default() {
1129 let cli = Cli::parse_from(["vtcode", "config", "reset"]);
1130
1131 assert!(matches!(
1132 cli.command,
1133 Some(Commands::Config {
1134 command: Some(ConfigCommand::Reset(ConfigResetArgs { global: false, project: false })),
1135 global: false,
1136 ..
1137 })
1138 ));
1139 }
1140
1141 #[test]
1142 fn parses_config_reset_global_and_project_targets() {
1143 let global = Cli::parse_from(["vtcode", "config", "reset", "--global"]);
1144 assert!(matches!(
1145 global.command,
1146 Some(Commands::Config {
1147 command: Some(ConfigCommand::Reset(ConfigResetArgs { global: true, project: false })),
1148 ..
1149 })
1150 ));
1151
1152 let project = Cli::parse_from(["vtcode", "config", "reset", "--project"]);
1153 assert!(matches!(
1154 project.command,
1155 Some(Commands::Config {
1156 command: Some(ConfigCommand::Reset(ConfigResetArgs { global: false, project: true })),
1157 ..
1158 })
1159 ));
1160 }
1161
1162 #[test]
1163 fn parses_parent_global_flag_for_config_reset() {
1164 let cli = Cli::parse_from(["vtcode", "config", "--global", "reset"]);
1165
1166 assert!(matches!(
1167 cli.command,
1168 Some(Commands::Config {
1169 command: Some(ConfigCommand::Reset(ConfigResetArgs { global: false, project: false })),
1170 global: true,
1171 ..
1172 })
1173 ));
1174 }
1175
1176 #[test]
1177 fn rejects_config_reset_with_conflicting_layer_flags() {
1178 let result = Cli::try_parse_from(["vtcode", "config", "reset", "--global", "--project"]);
1179
1180 assert!(result.is_err());
1181 }
1182
1183 #[test]
1184 fn man_all_pages_requires_directory_and_rejects_single_command() {
1185 let cli = Cli::try_parse_from(["vtcode", "man", "--all-pages", "--output", "man/man1"])
1186 .expect("all pages with an output directory");
1187 assert!(!cli.all, "man-page generation must not enable session selection");
1188 assert!(matches!(cli.command, Some(Commands::Man { all_pages: true, command: None, output: Some(_) })));
1189 assert!(Cli::try_parse_from(["vtcode", "man", "--all-pages"]).is_err());
1190 assert!(Cli::try_parse_from(["vtcode", "man", "chat", "--all-pages", "--output", "man"]).is_err());
1191 assert!(matches!(
1192 Cli::try_parse_from(["vtcode", "man", "chat"]).expect("single page").command,
1193 Some(Commands::Man { all_pages: false, .. })
1194 ));
1195 }
1196}