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(short, long)]
720 output: Option<PathBuf>,
721 },
722
723 #[command(subcommand)]
735 Skills(SkillsSubcommand),
736
737 #[command(subcommand)]
749 Plugins(PluginsSubcommand),
750
751 #[command(name = "list-skills", hide = true)]
753 ListSkills {},
754
755 #[command(name = "dependencies", visible_alias = "deps", subcommand)]
765 Dependencies(DependenciesSubcommand),
766
767 Secret(SecretArgs),
779
780 Check {
787 #[command(subcommand)]
788 command: CheckSubcommand,
789 },
790
791 #[command(name = "update")]
804 Update {
805 #[arg(
807 long,
808 long_help = "Check whether a newer version is available without installing it."
809 )]
810 check: bool,
811 #[arg(
813 long,
814 long_help = "Reinstall or downgrade even if the current version is already the latest."
815 )]
816 force: bool,
817 #[arg(long, long_help = "Print available release versions from GitHub and exit.")]
819 list: bool,
820 #[arg(
822 long,
823 default_value = "10",
824 long_help = "Maximum number of versions to display with --list."
825 )]
826 limit: usize,
827 #[arg(
829 long,
830 value_name = "VERSION",
831 long_help = "Pin the binary to a specific version.\nAuto-updates are disabled until --unpin is used."
832 )]
833 pin: Option<String>,
834 #[arg(long, long_help = "Remove a previously set version pin and resume auto-updates.")]
836 unpin: bool,
837 #[arg(
839 long,
840 value_name = "CHANNEL",
841 long_help = "Switch the release channel.\nAccepted values: stable, beta, nightly."
842 )]
843 channel: Option<String>,
844 #[arg(
846 long,
847 long_help = "Display the current update configuration (channel, pin, intervals) and exit."
848 )]
849 show_config: bool,
850 },
851
852 #[command(name = "anthropic-api")]
854 AnthropicApi {
855 #[arg(long, default_value = "11434")]
857 port: u16,
858 #[arg(long, default_value = "127.0.0.1")]
860 host: String,
861 },
862}
863
864impl Default for Cli {
865 fn default() -> Self {
866 Self {
867 color: ColorSelection { color: ColorChoice::Auto },
868 workspace_path: None,
869 model: Some(ModelId::default().to_string()),
870 provider: Some("gemini".to_owned()),
871 api_key_env: "GEMINI_API_KEY".to_owned(),
872 workspace: None,
873 research_preview: false,
874 security_level: "moderate".to_owned(),
875 show_file_diffs: false,
876 max_concurrent_ops: 5,
877 api_rate_limit: 30,
878 max_tool_calls: 10,
879 verbose: false,
880 quiet: false,
881 config: Vec::new(),
882 log_level: "info".to_owned(),
883 no_color: false,
884 theme: None,
885 skip_confirmations: false,
886 codex_experimental: false,
887 no_codex_experimental: false,
888 print: None,
889 full_auto: None,
890 resume_session: None,
891 continue_latest: false,
892 fork_session: None,
893 all: false,
894 session_id: None,
895 summarize: false,
896 debug: false,
897 enable_skills: false,
898 tick_rate: 250,
899 frame_rate: 60,
900 agent: None,
901 allowed_tools: Vec::new(),
902 disallowed_tools: Vec::new(),
903 dangerously_skip_permissions: false,
904 ide: false,
905 chrome: false,
906 no_chrome: false,
907 command: Some(Commands::Chat),
908 }
909 }
910}
911
912impl Cli {
913 pub fn get_api_key_env(&self) -> String {
918 crate::config::api_keys::resolve_api_key_env(
919 self.provider
920 .as_deref()
921 .unwrap_or(crate::config::constants::defaults::DEFAULT_PROVIDER),
922 &self.api_key_env,
923 )
924 }
925
926 pub fn codex_experimental_override(&self) -> Option<bool> {
927 if self.codex_experimental {
928 Some(true)
929 } else if self.no_codex_experimental {
930 Some(false)
931 } else {
932 None
933 }
934 }
935}
936
937fn parse_workspace_directory(raw: &str) -> Result<PathBuf, String> {
938 let candidate = PathBuf::from(raw);
939 if !candidate.exists() {
940 return Err(format!(
941 "'{}' is not a valid workspace path or subcommand.\n\
942 Run `vtcode --help` to see available commands and options.",
943 raw
944 ));
945 }
946
947 Ok(candidate)
948}
949
950pub fn long_version() -> String {
951 let git_info = option_env!("VT_CODE_GIT_INFO").unwrap_or(env!("CARGO_PKG_VERSION"));
952 let path_diagnostics = match vtcode_commons::VtCodePaths::resolve() {
953 Ok(paths) => format!(
954 "Config directory: {}\nData directory: {}\nState directory: {}\nCache directory: {}\nRuntime directory: {}\nExecutable directory: {}\nLegacy directory: {}\nMigration marker: {}\nMigration report: {}",
955 paths.config_dir().display(),
956 paths.data_dir().display(),
957 paths.state_dir().display(),
958 paths.cache_dir().display(),
959 paths.runtime_dir().display(),
960 paths.executable_dir().display(),
961 paths.legacy_dir().display(),
962 paths.migration_marker_path().display(),
963 paths.migration_report_path().display(),
964 ),
965 Err(error) => format!("Storage paths: unavailable ({error:#})"),
966 };
967 let environment = [
968 "HOME",
969 "VTCODE_HOME",
970 "VTCODE_CONFIG",
971 "VTCODE_CONFIG_PATH",
972 "VTCODE_DATA",
973 "CODEX_HOME",
974 "XDG_CONFIG_HOME",
975 "XDG_DATA_HOME",
976 "XDG_STATE_HOME",
977 "XDG_CACHE_HOME",
978 "XDG_RUNTIME_DIR",
979 "XDG_BIN_HOME",
980 "XDG_CONFIG_DIRS",
981 "XDG_DATA_DIRS",
982 ]
983 .iter()
984 .map(|name| {
985 let value = env::var_os(name)
986 .map(|value| value.to_string_lossy().into_owned())
987 .unwrap_or_else(|| "<not set>".to_string());
988 format!(" {name}={value}")
989 })
990 .collect::<Vec<_>>()
991 .join("\n");
992
993 format!(
994 "{}\n\nAuthors: {}\n{}\n\nEnvironment variables:\n{}",
995 git_info,
996 env!("CARGO_PKG_AUTHORS"),
997 path_diagnostics,
998 environment,
999 )
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004 use super::*;
1005 use clap::Parser;
1006
1007 #[test]
1008 fn long_version_includes_expected_sections() {
1009 let text = long_version();
1010 assert!(text.contains("Authors:"));
1011 assert!(text.contains("Config directory:"));
1012 assert!(text.contains("Data directory:"));
1013 assert!(text.contains("State directory:"));
1014 assert!(text.contains("Cache directory:"));
1015 assert!(text.contains("Runtime directory:"));
1016 assert!(text.contains("Executable directory:"));
1017 assert!(text.contains("Legacy directory:"));
1018 assert!(text.contains("Migration marker:"));
1019 assert!(text.contains("Migration report:"));
1020 assert!(text.contains("VTCODE_CONFIG"));
1021 assert!(text.contains("VTCODE_DATA"));
1022 }
1023
1024 #[test]
1025 fn long_version_starts_with_build_git_info() {
1026 let text = long_version();
1027 let expected = option_env!("VT_CODE_GIT_INFO").unwrap_or(env!("CARGO_PKG_VERSION"));
1028 assert!(text.starts_with(expected));
1029 }
1030
1031 #[test]
1032 fn config_file_api_key_env_uses_provider_default() {
1033 let cli = Cli::parse_from(["vtcode", "--provider", "minimax"]);
1034
1035 assert_eq!(cli.get_api_key_env(), "MINIMAX_API_KEY");
1036 }
1037
1038 #[test]
1039 fn config_file_api_key_env_preserves_explicit_override() {
1040 let cli = Cli::parse_from(["vtcode", "--provider", "openai", "--api-key-env", "CUSTOM_OPENAI_KEY"]);
1041
1042 assert_eq!(cli.get_api_key_env(), "CUSTOM_OPENAI_KEY");
1043 }
1044
1045 #[test]
1046 fn parses_app_server_command_with_stdio_listen_target() {
1047 let cli = Cli::parse_from(["vtcode", "app-server", "--listen", "stdio://"]);
1048
1049 assert!(matches!(
1050 cli.command,
1051 Some(Commands::AppServer { ref listen }) if listen == "stdio://"
1052 ));
1053 }
1054
1055 #[test]
1056 fn parses_init_force_flag() {
1057 let cli = Cli::parse_from(["vtcode", "init", "--force"]);
1058
1059 assert!(matches!(cli.command, Some(Commands::Init { force: true })));
1060 }
1061
1062 #[test]
1063 fn parses_codex_login_device_code_flag() {
1064 let cli = Cli::parse_from(["vtcode", "login", "codex", "--device-code"]);
1065
1066 assert!(matches!(
1067 cli.command,
1068 Some(Commands::Login {
1069 ref provider,
1070 device_code: true,
1071 from_codex: false
1072 }) if provider == "codex"
1073 ));
1074 }
1075
1076 #[test]
1077 fn parses_codex_experimental_flags() {
1078 let enabled = Cli::parse_from(["vtcode", "--codex-experimental"]);
1079 assert_eq!(enabled.codex_experimental_override(), Some(true));
1080
1081 let disabled = Cli::parse_from(["vtcode", "--no-codex-experimental"]);
1082 assert_eq!(disabled.codex_experimental_override(), Some(false));
1083 }
1084
1085 #[test]
1086 fn codex_experimental_flags_conflict() {
1087 let result = Cli::try_parse_from(["vtcode", "--codex-experimental", "--no-codex-experimental"]);
1088
1089 result.unwrap_err();
1090 }
1091
1092 #[test]
1093 fn parses_create_project_feature_flags() {
1094 let cli = Cli::parse_from([
1095 "vtcode",
1096 "create-project",
1097 "demo",
1098 "--feature",
1099 "web",
1100 "--feature",
1101 "db",
1102 ]);
1103
1104 assert!(matches!(
1105 cli.command,
1106 Some(Commands::CreateProject { ref name, ref features })
1107 if name == "demo" && features == &vec!["web".to_string(), "db".to_string()]
1108 ));
1109 }
1110
1111 #[test]
1112 fn parses_revert_partial_long_flag() {
1113 let cli = Cli::parse_from(["vtcode", "revert", "--turn", "3", "--partial", "code"]);
1114
1115 assert!(matches!(
1116 cli.command,
1117 Some(Commands::Revert {
1118 turn: 3,
1119 partial: Some(ref scope)
1120 }) if scope == "code"
1121 ));
1122 }
1123
1124 #[test]
1125 fn parses_config_reset_workspace_target_by_default() {
1126 let cli = Cli::parse_from(["vtcode", "config", "reset"]);
1127
1128 assert!(matches!(
1129 cli.command,
1130 Some(Commands::Config {
1131 command: Some(ConfigCommand::Reset(ConfigResetArgs { global: false, project: false })),
1132 global: false,
1133 ..
1134 })
1135 ));
1136 }
1137
1138 #[test]
1139 fn parses_config_reset_global_and_project_targets() {
1140 let global = Cli::parse_from(["vtcode", "config", "reset", "--global"]);
1141 assert!(matches!(
1142 global.command,
1143 Some(Commands::Config {
1144 command: Some(ConfigCommand::Reset(ConfigResetArgs { global: true, project: false })),
1145 ..
1146 })
1147 ));
1148
1149 let project = Cli::parse_from(["vtcode", "config", "reset", "--project"]);
1150 assert!(matches!(
1151 project.command,
1152 Some(Commands::Config {
1153 command: Some(ConfigCommand::Reset(ConfigResetArgs { global: false, project: true })),
1154 ..
1155 })
1156 ));
1157 }
1158
1159 #[test]
1160 fn parses_parent_global_flag_for_config_reset() {
1161 let cli = Cli::parse_from(["vtcode", "config", "--global", "reset"]);
1162
1163 assert!(matches!(
1164 cli.command,
1165 Some(Commands::Config {
1166 command: Some(ConfigCommand::Reset(ConfigResetArgs { global: false, project: false })),
1167 global: true,
1168 ..
1169 })
1170 ));
1171 }
1172
1173 #[test]
1174 fn rejects_config_reset_with_conflicting_layer_flags() {
1175 let result = Cli::try_parse_from(["vtcode", "config", "reset", "--global", "--project"]);
1176
1177 assert!(result.is_err());
1178 }
1179}