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 config;
14mod dependencies;
15mod exec;
16mod models;
17mod plugins;
18mod pods;
19mod review;
20mod schedule;
21mod schema;
22mod secret;
23mod session_store;
24mod skills;
25mod webmcp;
26
27pub use acp::AgentClientProtocolTarget;
28pub use ask::{AskCommandOptions, AskOutputFormat};
29pub use background::BackgroundSubagentArgs;
30pub use bench::BenchAllocatorArgs;
31pub use check::CheckSubcommand;
32pub use config::{
33 ConfigCommand, ConfigFile, ConfigResetArgs, ContextConfig, LoggingConfig, PerformanceConfig, SecurityConfig,
34 ToolConfig,
35};
36pub use dependencies::{DependenciesSubcommand, ManagedDependency};
37pub use exec::{EvalOutputFormat, ExecEvalArgs, ExecResumeArgs, ExecSubcommand};
38pub use models::ModelCommands;
39pub use plugins::PluginsSubcommand;
40pub use pods::PodsCommands;
41pub use review::ReviewArgs;
42pub use schedule::{ScheduleCreateArgs, ScheduleSubcommand};
43pub use schema::{SchemaCommands, SchemaMode, SchemaOutputFormat};
44pub use secret::{MigrateArgs, SecretArgs, SecretProvider, SecretSubcommand};
45pub use session_store::SessionStoreCommand;
46pub use skills::{SkillsRefSubcommand, SkillsSubcommand};
47pub use webmcp::{WebmcpCommand, WebmcpPairArgs, WebmcpServeArgs};
48
49#[derive(Parser, Debug, Clone)]
50pub struct Cli {
51 #[command(flatten)]
53 pub color: ColorSelection,
54
55 #[arg(
57 value_name = "WORKSPACE",
58 value_hint = ValueHint::DirPath,
59 value_parser = parse_workspace_directory,
60 global = true,
61 last = true
62 )]
63 pub workspace_path: Option<PathBuf>,
64
65 #[arg(long, global = true)]
67 pub model: Option<String>,
68
69 #[arg(long, global = true)]
71 pub provider: Option<String>,
72
73 #[arg(long, global = true, default_value = crate::config::constants::defaults::DEFAULT_API_KEY_ENV)]
75 pub api_key_env: String,
76
77 #[arg(
79 long,
80 global = true,
81 alias = "workspace-dir",
82 value_name = "PATH",
83 value_hint = ValueHint::DirPath,
84 value_parser = parse_workspace_directory
85 )]
86 pub workspace: Option<PathBuf>,
87
88 #[arg(long, global = true)]
90 pub research_preview: bool,
91
92 #[arg(long, global = true, default_value = "moderate")]
94 pub security_level: String,
95
96 #[arg(long, global = true)]
98 pub show_file_diffs: bool,
99
100 #[arg(long, global = true, default_value_t = 5)]
102 pub max_concurrent_ops: usize,
103
104 #[arg(long, global = true, default_value_t = 30)]
106 pub api_rate_limit: usize,
107
108 #[arg(long, global = true, default_value_t = 10)]
110 pub max_tool_calls: usize,
111
112 #[arg(long, global = true)]
114 pub debug: bool,
115
116 #[arg(long, global = true)]
118 pub verbose: bool,
119
120 #[arg(short, long, global = true)]
122 pub quiet: bool,
123
124 #[arg(
126 short = 'c',
127 long = "config",
128 value_name = "KEY=VALUE|PATH",
129 action = ArgAction::Append,
130 global = true
131 )]
132 pub config: Vec<String>,
133
134 #[arg(long, global = true, default_value = "info")]
136 pub log_level: String,
137
138 #[arg(long, global = true)]
140 pub no_color: bool,
141
142 #[arg(long, global = true, value_name = "THEME")]
144 pub theme: Option<String>,
145
146 #[arg(short = 't', long, default_value_t = 250)]
148 pub tick_rate: u64,
149
150 #[arg(short = 'f', long, default_value_t = 60)]
152 pub frame_rate: u64,
153
154 #[arg(long, global = true)]
156 pub enable_skills: bool,
157
158 #[arg(long, global = true)]
160 pub chrome: bool,
161
162 #[arg(long = "no-chrome", global = true, conflicts_with = "chrome")]
164 pub no_chrome: bool,
165
166 #[arg(long, global = true)]
168 pub skip_confirmations: bool,
169
170 #[arg(long = "codex-experimental", global = true, conflicts_with = "no_codex_experimental")]
172 pub codex_experimental: bool,
173
174 #[arg(long = "no-codex-experimental", global = true, conflicts_with = "codex_experimental")]
176 pub no_codex_experimental: bool,
177
178 #[arg(
180 short = 'p',
181 long = "print",
182 value_name = "PROMPT",
183 value_hint = ValueHint::Other,
184 num_args = 0..=1,
185 default_missing_value = "",
186 global = true,
187 conflicts_with_all = ["full_auto"]
188 )]
189 pub print: Option<String>,
190
191 #[arg(
193 long = "full-auto",
194 global = true,
195 help = "Run non-interactively with full-auto permission review",
196 long_help = r#"Run non-interactively on top of the active primary agent.
197
198If 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.
199
200Full-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."#,
201 value_name = "PROMPT",
202 num_args = 0..=1,
203 default_missing_value = "",
204 value_hint = ValueHint::Other
205 )]
206 pub full_auto: Option<String>,
207
208 #[arg(
210 short = 'r',
211 long = "resume",
212 global = true,
213 value_name = "SESSION_ID",
214 num_args = 0..=1,
215 default_missing_value = "__interactive__",
216 conflicts_with_all = ["continue_latest", "full_auto"]
217 )]
218 pub resume_session: Option<String>,
219
220 #[arg(
222 long = "continue",
223 visible_alias = "continue-session",
224 global = true,
225 conflicts_with_all = ["resume_session", "full_auto"]
226 )]
227 pub continue_latest: bool,
228
229 #[arg(
231 long = "fork-session",
232 global = true,
233 value_name = "SESSION_ID",
234 conflicts_with_all = ["resume_session", "continue_latest", "full_auto"]
235 )]
236 pub fork_session: Option<String>,
237
238 #[arg(long, global = true)]
240 pub all: bool,
241
242 #[arg(long = "session-id", global = true, value_name = "CUSTOM_SUFFIX")]
244 pub session_id: Option<String>,
245
246 #[arg(long, global = true)]
248 pub summarize: bool,
249
250 #[arg(long, global = true, value_name = "AGENT")]
252 pub agent: Option<String>,
253
254 #[arg(long = "allowed-tools", global = true, value_name = "TOOLS", action = ArgAction::Append)]
256 pub allowed_tools: Vec<String>,
257
258 #[arg(long = "disallowed-tools", global = true, value_name = "TOOLS", action = ArgAction::Append)]
260 pub disallowed_tools: Vec<String>,
261
262 #[arg(
264 long = "dangerously-skip-permissions",
265 global = true,
266 help = "Auto-approve promptable actions while respecting denies and policy blocks",
267 long_help = "Auto-approve promptable actions while still respecting explicit denies and policy blocks."
268 )]
269 pub dangerously_skip_permissions: bool,
270
271 #[arg(long, global = true)]
273 pub ide: bool,
274
275 #[command(subcommand)]
276 pub command: Option<Commands>,
277}
278
279#[derive(Subcommand, Debug, Clone)]
280pub enum Commands {
281 #[command(name = "acp")]
283 AgentClientProtocol {
284 #[arg(value_enum, default_value_t = AgentClientProtocolTarget::Zed)]
286 target: AgentClientProtocolTarget,
287 },
288
289 #[command(name = "session-store")]
293 SessionStore {
294 #[command(subcommand)]
295 command: SessionStoreCommand,
296 },
297
298 Chat,
300
301 Continue,
312
313 Ask {
323 #[arg(
325 value_name = "PROMPT",
326 long_help = "The prompt to send to the model.\n\nOmit to read from stdin (piped input).\nUse '-' to explicitly force reading from stdin."
327 )]
328 prompt: Option<String>,
329 #[arg(
331 long = "output-format",
332 value_enum,
333 value_name = "FORMAT",
334 long_help = "Output format for the response.\n\nCurrently supports:\n json - Emit the response as a structured JSON document."
335 )]
336 output_format: Option<AskOutputFormat>,
337 },
338 Exec {
351 #[arg(
353 long,
354 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."
355 )]
356 json: bool,
357 #[arg(
359 long,
360 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."
361 )]
362 dry_run: bool,
363 #[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.")]
365 events: Option<PathBuf>,
366 #[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.")]
368 last_message_file: Option<PathBuf>,
369 #[command(subcommand)]
371 command: Option<ExecSubcommand>,
372 #[arg(
374 value_name = "PROMPT",
375 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\""
376 )]
377 prompt: Option<String>,
378 },
379 Eval(ExecEvalArgs),
389 Schedule {
401 #[command(subcommand)]
402 command: ScheduleSubcommand,
403 },
404
405 #[command(name = "background-subagent", hide = true)]
407 BackgroundSubagent(BackgroundSubagentArgs),
408
409 #[command(
411 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\""
412 )]
413 Review(ReviewArgs),
414
415 Schema {
417 #[command(subcommand)]
418 command: SchemaCommands,
419 },
420
421 ChatVerbose,
423
424 Analyze {
426 #[arg(value_name = "TYPE", default_value = "full")]
428 analysis_type: String,
429 },
430
431 #[command(name = "trajectory")]
433 Trajectory {
434 #[arg(long)]
436 file: Option<PathBuf>,
437 #[arg(long, default_value_t = 10)]
439 top: usize,
440 },
441
442 Notify {
444 #[arg(long, value_name = "TITLE")]
446 title: Option<String>,
447 #[arg(value_name = "MESSAGE")]
449 message: String,
450 },
451
452 Benchmark {
454 #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
456 task_file: Option<PathBuf>,
457 #[arg(long, value_name = "JSON")]
459 task: Option<String>,
460 #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
462 output: Option<PathBuf>,
463 #[arg(long, value_name = "COUNT")]
465 max_tasks: Option<usize>,
466 },
467
468 #[command(name = "bench-allocator")]
476 BenchAllocator(BenchAllocatorArgs),
477
478 CreateProject {
480 name: String,
481 #[arg(long = "feature", value_name = "FEATURE", action = ArgAction::Append)]
482 features: Vec<String>,
483 },
484
485 Revert {
487 #[arg(short, long)]
489 turn: usize,
490 #[arg(long)]
492 partial: Option<String>,
493 },
494
495 Snapshots,
497
498 #[command(name = "cleanup-snapshots")]
509 CleanupSnapshots {
510 #[arg(short, long, default_value_t = 50)]
515 max: usize,
516 },
517
518 Init {
527 #[arg(
529 long,
530 short = 'f',
531 long_help = "Overwrite AGENTS.md without confirmation.\nUse this in CI/CD or scripts where interactive prompts are not possible."
532 )]
533 force: bool,
534 },
535
536 #[command(name = "init-project")]
546 InitProject {
547 #[arg(
549 long,
550 long_help = "Name for the project.\nDefaults to the current directory name if not specified."
551 )]
552 name: Option<String>,
553 #[arg(long, long_help = "Overwrite existing project structure without confirmation.")]
555 force: bool,
556 #[arg(
558 long,
559 long_help = "Move existing config and cache files into the new project structure."
560 )]
561 migrate: bool,
562 },
563
564 Config {
578 #[command(subcommand)]
579 command: Option<ConfigCommand>,
580 #[arg(
582 long,
583 long_help = "Write the configuration to this path.\nDefaults to ./vtcode.toml in the current directory."
584 )]
585 output: Option<PathBuf>,
586 #[arg(
588 long,
589 long_help = "Write the configuration to the canonical user config directory.\nThis sets global defaults for all workspaces."
590 )]
591 global: bool,
592 },
593
594 Login {
606 #[arg(long_help = "The provider to authenticate with.\nSupported: openai, openrouter, copilot, codex")]
608 provider: String,
609 #[arg(
611 long,
612 default_value_t = false,
613 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."
614 )]
615 device_code: bool,
616 #[arg(
619 long,
620 default_value_t = false,
621 long_help = "Import ChatGPT OAuth tokens from Codex's auth.json.\n\
622 Requires that you have already run `codex login`.\n\
623 Currently supported only for the `openai` provider."
624 )]
625 from_codex: bool,
626 },
627
628 Logout {
636 #[arg(long_help = "The provider to deauthenticate.\nSupported: openai, openrouter, copilot, codex")]
638 provider: String,
639 },
640
641 Auth {
651 #[arg(long_help = "Show status for a single provider.\nOmit to show status for all supported providers.")]
653 provider: Option<String>,
654 },
655
656 #[command(name = "tool-policy")]
658 ToolPolicy {
659 #[command(subcommand)]
660 command: crate::cli::tool_policy_commands::ToolPolicyCommands,
661 },
662
663 #[command(name = "mcp")]
665 Mcp {
666 #[command(subcommand)]
667 command: crate::mcp::cli::McpCommands,
668 },
669
670 #[command(name = "a2a")]
672 A2a {
673 #[command(subcommand)]
674 command: super::super::a2a::cli::A2aCommands,
675 },
676
677 #[command(name = "webmcp")]
679 Webmcp {
680 #[command(subcommand)]
681 command: WebmcpCommand,
682 },
683
684 #[command(name = "app-server")]
686 AppServer {
687 #[arg(long, default_value = "stdio://")]
689 listen: String,
690 },
691
692 Models {
694 #[command(subcommand)]
695 command: ModelCommands,
696 },
697
698 #[command(name = "pods")]
700 Pods {
701 #[command(subcommand)]
702 command: PodsCommands,
703 },
704
705 Man {
707 command: Option<String>,
709 #[arg(short, long)]
711 output: Option<PathBuf>,
712 },
713
714 #[command(subcommand)]
726 Skills(SkillsSubcommand),
727
728 #[command(subcommand)]
740 Plugins(PluginsSubcommand),
741
742 #[command(name = "list-skills", hide = true)]
744 ListSkills {},
745
746 #[command(name = "dependencies", visible_alias = "deps", subcommand)]
756 Dependencies(DependenciesSubcommand),
757
758 Secret(SecretArgs),
770
771 Check {
778 #[command(subcommand)]
779 command: CheckSubcommand,
780 },
781
782 #[command(name = "update")]
795 Update {
796 #[arg(
798 long,
799 long_help = "Check whether a newer version is available without installing it."
800 )]
801 check: bool,
802 #[arg(
804 long,
805 long_help = "Reinstall or downgrade even if the current version is already the latest."
806 )]
807 force: bool,
808 #[arg(long, long_help = "Print available release versions from GitHub and exit.")]
810 list: bool,
811 #[arg(
813 long,
814 default_value = "10",
815 long_help = "Maximum number of versions to display with --list."
816 )]
817 limit: usize,
818 #[arg(
820 long,
821 value_name = "VERSION",
822 long_help = "Pin the binary to a specific version.\nAuto-updates are disabled until --unpin is used."
823 )]
824 pin: Option<String>,
825 #[arg(long, long_help = "Remove a previously set version pin and resume auto-updates.")]
827 unpin: bool,
828 #[arg(
830 long,
831 value_name = "CHANNEL",
832 long_help = "Switch the release channel.\nAccepted values: stable, beta, nightly."
833 )]
834 channel: Option<String>,
835 #[arg(
837 long,
838 long_help = "Display the current update configuration (channel, pin, intervals) and exit."
839 )]
840 show_config: bool,
841 },
842
843 #[command(name = "anthropic-api")]
845 AnthropicApi {
846 #[arg(long, default_value = "11434")]
848 port: u16,
849 #[arg(long, default_value = "127.0.0.1")]
851 host: String,
852 },
853}
854
855impl Default for Cli {
856 fn default() -> Self {
857 Self {
858 color: ColorSelection { color: ColorChoice::Auto },
859 workspace_path: None,
860 model: Some(ModelId::default().to_string()),
861 provider: Some("gemini".to_owned()),
862 api_key_env: "GEMINI_API_KEY".to_owned(),
863 workspace: None,
864 research_preview: false,
865 security_level: "moderate".to_owned(),
866 show_file_diffs: false,
867 max_concurrent_ops: 5,
868 api_rate_limit: 30,
869 max_tool_calls: 10,
870 verbose: false,
871 quiet: false,
872 config: Vec::new(),
873 log_level: "info".to_owned(),
874 no_color: false,
875 theme: None,
876 skip_confirmations: false,
877 codex_experimental: false,
878 no_codex_experimental: false,
879 print: None,
880 full_auto: None,
881 resume_session: None,
882 continue_latest: false,
883 fork_session: None,
884 all: false,
885 session_id: None,
886 summarize: false,
887 debug: false,
888 enable_skills: false,
889 tick_rate: 250,
890 frame_rate: 60,
891 agent: None,
892 allowed_tools: Vec::new(),
893 disallowed_tools: Vec::new(),
894 dangerously_skip_permissions: false,
895 ide: false,
896 chrome: false,
897 no_chrome: false,
898 command: Some(Commands::Chat),
899 }
900 }
901}
902
903impl Cli {
904 pub fn get_api_key_env(&self) -> String {
909 crate::config::api_keys::resolve_api_key_env(
910 self.provider
911 .as_deref()
912 .unwrap_or(crate::config::constants::defaults::DEFAULT_PROVIDER),
913 &self.api_key_env,
914 )
915 }
916
917 pub fn codex_experimental_override(&self) -> Option<bool> {
918 if self.codex_experimental {
919 Some(true)
920 } else if self.no_codex_experimental {
921 Some(false)
922 } else {
923 None
924 }
925 }
926}
927
928fn parse_workspace_directory(raw: &str) -> Result<PathBuf, String> {
929 let candidate = PathBuf::from(raw);
930 if !candidate.exists() {
931 return Err(format!(
932 "'{}' is not a valid workspace path or subcommand.\n\
933 Run `vtcode --help` to see available commands and options.",
934 raw
935 ));
936 }
937
938 Ok(candidate)
939}
940
941pub fn long_version() -> String {
942 let git_info = option_env!("VT_CODE_GIT_INFO").unwrap_or(env!("CARGO_PKG_VERSION"));
943 let path_diagnostics = match vtcode_commons::VtCodePaths::resolve() {
944 Ok(paths) => format!(
945 "Config directory: {}\nData directory: {}\nState directory: {}\nCache directory: {}\nRuntime directory: {}\nExecutable directory: {}\nLegacy directory: {}\nMigration marker: {}\nMigration report: {}",
946 paths.config_dir().display(),
947 paths.data_dir().display(),
948 paths.state_dir().display(),
949 paths.cache_dir().display(),
950 paths.runtime_dir().display(),
951 paths.executable_dir().display(),
952 paths.legacy_dir().display(),
953 paths.migration_marker_path().display(),
954 paths.migration_report_path().display(),
955 ),
956 Err(error) => format!("Storage paths: unavailable ({error:#})"),
957 };
958 let environment = [
959 "HOME",
960 "VTCODE_HOME",
961 "VTCODE_CONFIG",
962 "VTCODE_CONFIG_PATH",
963 "VTCODE_DATA",
964 "CODEX_HOME",
965 "XDG_CONFIG_HOME",
966 "XDG_DATA_HOME",
967 "XDG_STATE_HOME",
968 "XDG_CACHE_HOME",
969 "XDG_RUNTIME_DIR",
970 "XDG_BIN_HOME",
971 "XDG_CONFIG_DIRS",
972 "XDG_DATA_DIRS",
973 ]
974 .iter()
975 .map(|name| {
976 let value = env::var_os(name)
977 .map(|value| value.to_string_lossy().into_owned())
978 .unwrap_or_else(|| "<not set>".to_string());
979 format!(" {name}={value}")
980 })
981 .collect::<Vec<_>>()
982 .join("\n");
983
984 format!(
985 "{}\n\nAuthors: {}\n{}\n\nEnvironment variables:\n{}",
986 git_info,
987 env!("CARGO_PKG_AUTHORS"),
988 path_diagnostics,
989 environment,
990 )
991}
992
993#[cfg(test)]
994mod tests {
995 use super::*;
996 use clap::Parser;
997
998 #[test]
999 fn long_version_includes_expected_sections() {
1000 let text = long_version();
1001 assert!(text.contains("Authors:"));
1002 assert!(text.contains("Config directory:"));
1003 assert!(text.contains("Data directory:"));
1004 assert!(text.contains("State directory:"));
1005 assert!(text.contains("Cache directory:"));
1006 assert!(text.contains("Runtime directory:"));
1007 assert!(text.contains("Executable directory:"));
1008 assert!(text.contains("Legacy directory:"));
1009 assert!(text.contains("Migration marker:"));
1010 assert!(text.contains("Migration report:"));
1011 assert!(text.contains("VTCODE_CONFIG"));
1012 assert!(text.contains("VTCODE_DATA"));
1013 }
1014
1015 #[test]
1016 fn long_version_starts_with_build_git_info() {
1017 let text = long_version();
1018 let expected = option_env!("VT_CODE_GIT_INFO").unwrap_or(env!("CARGO_PKG_VERSION"));
1019 assert!(text.starts_with(expected));
1020 }
1021
1022 #[test]
1023 fn config_file_api_key_env_uses_provider_default() {
1024 let cli = Cli::parse_from(["vtcode", "--provider", "minimax"]);
1025
1026 assert_eq!(cli.get_api_key_env(), "MINIMAX_API_KEY");
1027 }
1028
1029 #[test]
1030 fn config_file_api_key_env_preserves_explicit_override() {
1031 let cli = Cli::parse_from(["vtcode", "--provider", "openai", "--api-key-env", "CUSTOM_OPENAI_KEY"]);
1032
1033 assert_eq!(cli.get_api_key_env(), "CUSTOM_OPENAI_KEY");
1034 }
1035
1036 #[test]
1037 fn parses_app_server_command_with_stdio_listen_target() {
1038 let cli = Cli::parse_from(["vtcode", "app-server", "--listen", "stdio://"]);
1039
1040 assert!(matches!(
1041 cli.command,
1042 Some(Commands::AppServer { ref listen }) if listen == "stdio://"
1043 ));
1044 }
1045
1046 #[test]
1047 fn parses_init_force_flag() {
1048 let cli = Cli::parse_from(["vtcode", "init", "--force"]);
1049
1050 assert!(matches!(cli.command, Some(Commands::Init { force: true })));
1051 }
1052
1053 #[test]
1054 fn parses_codex_login_device_code_flag() {
1055 let cli = Cli::parse_from(["vtcode", "login", "codex", "--device-code"]);
1056
1057 assert!(matches!(
1058 cli.command,
1059 Some(Commands::Login {
1060 ref provider,
1061 device_code: true,
1062 from_codex: false
1063 }) if provider == "codex"
1064 ));
1065 }
1066
1067 #[test]
1068 fn parses_codex_experimental_flags() {
1069 let enabled = Cli::parse_from(["vtcode", "--codex-experimental"]);
1070 assert_eq!(enabled.codex_experimental_override(), Some(true));
1071
1072 let disabled = Cli::parse_from(["vtcode", "--no-codex-experimental"]);
1073 assert_eq!(disabled.codex_experimental_override(), Some(false));
1074 }
1075
1076 #[test]
1077 fn codex_experimental_flags_conflict() {
1078 let result = Cli::try_parse_from(["vtcode", "--codex-experimental", "--no-codex-experimental"]);
1079
1080 result.unwrap_err();
1081 }
1082
1083 #[test]
1084 fn parses_create_project_feature_flags() {
1085 let cli = Cli::parse_from([
1086 "vtcode",
1087 "create-project",
1088 "demo",
1089 "--feature",
1090 "web",
1091 "--feature",
1092 "db",
1093 ]);
1094
1095 assert!(matches!(
1096 cli.command,
1097 Some(Commands::CreateProject { ref name, ref features })
1098 if name == "demo" && features == &vec!["web".to_string(), "db".to_string()]
1099 ));
1100 }
1101
1102 #[test]
1103 fn parses_revert_partial_long_flag() {
1104 let cli = Cli::parse_from(["vtcode", "revert", "--turn", "3", "--partial", "code"]);
1105
1106 assert!(matches!(
1107 cli.command,
1108 Some(Commands::Revert {
1109 turn: 3,
1110 partial: Some(ref scope)
1111 }) if scope == "code"
1112 ));
1113 }
1114
1115 #[test]
1116 fn parses_config_reset_workspace_target_by_default() {
1117 let cli = Cli::parse_from(["vtcode", "config", "reset"]);
1118
1119 assert!(matches!(
1120 cli.command,
1121 Some(Commands::Config {
1122 command: Some(ConfigCommand::Reset(ConfigResetArgs { global: false, project: false })),
1123 global: false,
1124 ..
1125 })
1126 ));
1127 }
1128
1129 #[test]
1130 fn parses_config_reset_global_and_project_targets() {
1131 let global = Cli::parse_from(["vtcode", "config", "reset", "--global"]);
1132 assert!(matches!(
1133 global.command,
1134 Some(Commands::Config {
1135 command: Some(ConfigCommand::Reset(ConfigResetArgs { global: true, project: false })),
1136 ..
1137 })
1138 ));
1139
1140 let project = Cli::parse_from(["vtcode", "config", "reset", "--project"]);
1141 assert!(matches!(
1142 project.command,
1143 Some(Commands::Config {
1144 command: Some(ConfigCommand::Reset(ConfigResetArgs { global: false, project: true })),
1145 ..
1146 })
1147 ));
1148 }
1149
1150 #[test]
1151 fn parses_parent_global_flag_for_config_reset() {
1152 let cli = Cli::parse_from(["vtcode", "config", "--global", "reset"]);
1153
1154 assert!(matches!(
1155 cli.command,
1156 Some(Commands::Config {
1157 command: Some(ConfigCommand::Reset(ConfigResetArgs { global: false, project: false })),
1158 global: true,
1159 ..
1160 })
1161 ));
1162 }
1163
1164 #[test]
1165 fn rejects_config_reset_with_conflicting_layer_flags() {
1166 let result = Cli::try_parse_from(["vtcode", "config", "reset", "--global", "--project"]);
1167
1168 assert!(result.is_err());
1169 }
1170}