1use crate::config::models::ModelId;
4use clap::{ArgAction, Args, ColorChoice, Parser, Subcommand, ValueEnum, ValueHint};
5use colorchoice_clap::Color as ColorSelection;
6use std::path::PathBuf;
7
8pub fn long_version() -> String {
17 use crate::config::defaults::{get_config_dir, get_data_dir};
18
19 let git_info = option_env!("VT_CODE_GIT_INFO").unwrap_or(env!("CARGO_PKG_VERSION"));
20
21 let config_dir = get_config_dir()
22 .map(|p| p.display().to_string())
23 .unwrap_or_else(|| "~/.vtcode/".to_string());
24
25 let data_dir = get_data_dir()
26 .map(|p| p.display().to_string())
27 .unwrap_or_else(|| "~/.vtcode/cache/".to_string());
28
29 format!(
30 "{}\n\nAuthors: {}\nConfig directory: {}\nData directory: {}\n\nEnvironment variables:\n VTCODE_CONFIG - Override config directory\n VTCODE_DATA - Override data directory",
31 git_info,
32 env!("CARGO_PKG_AUTHORS"),
33 config_dir,
34 data_dir
35 )
36}
37
38fn parse_workspace_directory(raw: &str) -> Result<PathBuf, String> {
39 let candidate = PathBuf::from(raw);
40 if !candidate.exists() {
41 return Err(format!(
42 "Workspace path does not exist: {}",
43 candidate.display()
44 ));
45 }
46 if !candidate.is_dir() {
47 return Err(format!(
48 "Workspace path is not a directory: {}",
49 candidate.display()
50 ));
51 }
52 Ok(candidate)
53}
54
55#[derive(Parser, Debug, Clone)]
57#[command(
58 name = "vtcode",
59 version,
60 about = "VT Code - AI coding assistant",
61 long_about = "VT Code - AI coding assistant\n\n\
62An AI-powered coding agent that can read, write, and execute code in your workspace.\n\n\
63Quick start:\n\
64 vtcode Launch interactive chat\n\
65 vtcode ask \"question\" Single prompt, no tools\n\
66 vtcode exec \"task\" Headless execution with tools\n\
67 vtcode init Bootstrap workspace\n\n\
68Configuration:\n\
69 Settings are read from vtcode.toml in the workspace root.\n\
70 Run `vtcode config` to generate a starter config file.\n\n\
71Authentication:\n\
72 Run `vtcode login <provider>` to store API credentials.\n\
73 Supported providers: openai, openrouter, copilot, codex.",
74 color = ColorChoice::Auto
75)]
76pub struct Cli {
77 #[command(flatten)]
79 pub color: ColorSelection,
80
81 #[arg(
83 value_name = "WORKSPACE",
84 value_hint = ValueHint::DirPath,
85 value_parser = parse_workspace_directory,
86 global = true
87 )]
88 pub workspace_path: Option<PathBuf>,
89
90 #[arg(long, global = true)]
92 pub model: Option<String>,
93
94 #[arg(long, global = true)]
96 pub provider: Option<String>,
97
98 #[arg(long, global = true, default_value = crate::config::constants::defaults::DEFAULT_API_KEY_ENV)]
100 pub api_key_env: String,
101
102 #[arg(
104 long,
105 global = true,
106 alias = "workspace-dir",
107 value_name = "PATH",
108 value_hint = ValueHint::DirPath,
109 value_parser = parse_workspace_directory
110 )]
111 pub workspace: Option<PathBuf>,
112
113 #[arg(long, global = true)]
115 pub research_preview: bool,
116
117 #[arg(long, global = true, default_value = "moderate")]
119 pub security_level: String,
120
121 #[arg(long, global = true)]
123 pub show_file_diffs: bool,
124
125 #[arg(long, global = true, default_value_t = 5)]
127 pub max_concurrent_ops: usize,
128
129 #[arg(long, global = true, default_value_t = 30)]
131 pub api_rate_limit: usize,
132
133 #[arg(long, global = true, default_value_t = 10)]
135 pub max_tool_calls: usize,
136
137 #[arg(long, global = true)]
139 pub debug: bool,
140
141 #[arg(long, global = true)]
143 pub verbose: bool,
144
145 #[arg(short, long, global = true)]
147 pub quiet: bool,
148
149 #[arg(
151 short = 'c',
152 long = "config",
153 value_name = "KEY=VALUE|PATH",
154 action = ArgAction::Append,
155 global = true
156 )]
157 pub config: Vec<String>,
158
159 #[arg(long, global = true, default_value = "info")]
161 pub log_level: String,
162
163 #[arg(long, global = true)]
165 pub no_color: bool,
166
167 #[arg(long, global = true, value_name = "THEME")]
169 pub theme: Option<String>,
170
171 #[arg(short = 't', long, default_value_t = 250)]
173 pub tick_rate: u64,
174
175 #[arg(short = 'f', long, default_value_t = 60)]
177 pub frame_rate: u64,
178
179 #[arg(long, global = true)]
181 pub enable_skills: bool,
182
183 #[arg(long, global = true)]
185 pub chrome: bool,
186
187 #[arg(long = "no-chrome", global = true, conflicts_with = "chrome")]
189 pub no_chrome: bool,
190
191 #[arg(long, global = true)]
193 pub skip_confirmations: bool,
194
195 #[arg(
197 long = "codex-experimental",
198 global = true,
199 conflicts_with = "no_codex_experimental"
200 )]
201 pub codex_experimental: bool,
202
203 #[arg(
205 long = "no-codex-experimental",
206 global = true,
207 conflicts_with = "codex_experimental"
208 )]
209 pub no_codex_experimental: bool,
210
211 #[arg(
213 short = 'p',
214 long = "print",
215 value_name = "PROMPT",
216 value_hint = ValueHint::Other,
217 num_args = 0..=1,
218 default_missing_value = "",
219 global = true,
220 conflicts_with_all = ["full_auto"]
221 )]
222 pub print: Option<String>,
223
224 #[arg(
226 long = "full-auto",
227 global = true,
228 help = "Run non-interactively with full-auto permission review",
229 long_help = r#"Run non-interactively on top of the active primary agent.
230
231If 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.
232
233Full-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."#,
234 value_name = "PROMPT",
235 num_args = 0..=1,
236 default_missing_value = "",
237 value_hint = ValueHint::Other
238 )]
239 pub full_auto: Option<String>,
240
241 #[arg(
243 short = 'r',
244 long = "resume",
245 global = true,
246 value_name = "SESSION_ID",
247 num_args = 0..=1,
248 default_missing_value = "__interactive__",
249 conflicts_with_all = ["continue_latest", "full_auto"]
250 )]
251 pub resume_session: Option<String>,
252
253 #[arg(
255 long = "continue",
256 visible_alias = "continue-session",
257 global = true,
258 conflicts_with_all = ["resume_session", "full_auto"]
259 )]
260 pub continue_latest: bool,
261
262 #[arg(
264 long = "fork-session",
265 global = true,
266 value_name = "SESSION_ID",
267 conflicts_with_all = ["resume_session", "continue_latest", "full_auto"]
268 )]
269 pub fork_session: Option<String>,
270
271 #[arg(long, global = true)]
273 pub all: bool,
274
275 #[arg(long = "session-id", global = true, value_name = "CUSTOM_SUFFIX")]
277 pub session_id: Option<String>,
278
279 #[arg(long, global = true)]
281 pub summarize: bool,
282
283 #[arg(long, global = true, value_name = "AGENT")]
285 pub agent: Option<String>,
286
287 #[arg(long = "allowed-tools", global = true, value_name = "TOOLS", action = ArgAction::Append)]
289 pub allowed_tools: Vec<String>,
290
291 #[arg(long = "disallowed-tools", global = true, value_name = "TOOLS", action = ArgAction::Append)]
293 pub disallowed_tools: Vec<String>,
294
295 #[arg(
297 long = "dangerously-skip-permissions",
298 global = true,
299 help = "Auto-approve promptable actions while respecting denies and policy blocks",
300 long_help = "Auto-approve promptable actions while still respecting explicit denies and policy blocks."
301 )]
302 pub dangerously_skip_permissions: bool,
303
304 #[arg(long, global = true)]
306 pub ide: bool,
307
308 #[command(subcommand)]
309 pub command: Option<Commands>,
310}
311
312#[derive(Debug, Default, Clone)]
314pub struct AskCommandOptions {
315 pub output_format: Option<AskOutputFormat>,
316 pub allowed_tools: Vec<String>,
317 pub disallowed_tools: Vec<String>,
318 pub skip_confirmations: bool,
319}
320
321#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
323pub enum AskOutputFormat {
324 Json,
326}
327
328#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
330pub enum SchemaOutputFormat {
331 Json,
333 Ndjson,
335}
336
337#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
339pub enum SchemaMode {
340 Minimal,
342 Progressive,
344 Full,
346}
347
348#[derive(Subcommand, Debug, Clone)]
350pub enum SchemaCommands {
351 Tools {
353 #[arg(long, value_enum, default_value_t = SchemaMode::Progressive)]
355 mode: SchemaMode,
356 #[arg(long, value_enum, default_value_t = SchemaOutputFormat::Json)]
358 format: SchemaOutputFormat,
359 #[arg(long = "name", value_name = "TOOL")]
361 names: Vec<String>,
362 },
363}
364
365#[derive(Subcommand, Debug, Clone)]
367pub enum ExecSubcommand {
368 #[command(
370 long_about = "Resume a previous exec session with a follow-up prompt.\n\nExamples:\n vtcode exec resume session-123 \"continue from the prior investigation\"\n vtcode exec resume --last \"continue from the prior investigation\"\n echo \"continue from stdin\" | vtcode exec resume --last"
371 )]
372 Resume(ExecResumeArgs),
373}
374
375#[derive(Args, Debug, Clone)]
377pub struct ExecResumeArgs {
378 #[arg(long)]
380 pub last: bool,
381 #[arg(long)]
383 pub all: bool,
384 #[arg(value_name = "SESSION_ID_OR_PROMPT", required_unless_present = "last")]
386 pub session_or_prompt: Option<String>,
387 #[arg(value_name = "PROMPT")]
389 pub prompt: Option<String>,
390}
391
392#[derive(Subcommand, Debug, Clone)]
394pub enum ScheduleSubcommand {
395 #[command(
397 long_about = "Create a durable scheduled task.\n\nExamples:\n vtcode schedule create --prompt \"check the deployment\" --every 10m\n vtcode schedule create --prompt \"review the nightly build\" --cron \"0 9 * * 1-5\"\n vtcode schedule create --reminder \"push the release branch\" --at \"15:00\""
398 )]
399 Create(ScheduleCreateArgs),
400 List,
402 Delete {
404 #[arg(value_name = "TASK_ID")]
405 id: String,
406 },
407 Serve,
409 #[command(name = "install-service")]
411 InstallService,
412 #[command(name = "uninstall-service")]
414 UninstallService,
415}
416
417#[derive(Args, Debug, Clone)]
419pub struct ScheduleCreateArgs {
420 #[arg(long, value_name = "NAME")]
422 pub name: Option<String>,
423 #[arg(long, value_name = "PROMPT", conflicts_with = "reminder")]
425 pub prompt: Option<String>,
426 #[arg(long, value_name = "TEXT", conflicts_with = "prompt")]
428 pub reminder: Option<String>,
429 #[arg(long, value_name = "DURATION", conflicts_with_all = ["cron", "at"])]
431 pub every: Option<String>,
432 #[arg(long, value_name = "EXPR", conflicts_with_all = ["every", "at"])]
434 pub cron: Option<String>,
435 #[arg(long, value_name = "TIME", conflicts_with_all = ["every", "cron"])]
437 pub at: Option<String>,
438 #[arg(long, value_name = "PATH", value_hint = ValueHint::DirPath)]
440 pub workspace: Option<PathBuf>,
441}
442
443#[derive(Args, Debug, Clone)]
445pub struct ReviewArgs {
446 #[arg(long)]
448 pub json: bool,
449 #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
451 pub events: Option<PathBuf>,
452 #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
454 pub last_message_file: Option<PathBuf>,
455 #[arg(long, conflicts_with_all = ["target", "files"])]
457 pub last_diff: bool,
458 #[arg(long, value_name = "TARGET", conflicts_with = "files")]
460 pub target: Option<String>,
461 #[arg(long, value_name = "STYLE")]
463 pub style: Option<String>,
464 #[arg(
466 long = "file",
467 value_name = "FILE",
468 value_hint = ValueHint::FilePath,
469 conflicts_with_all = ["last_diff", "target"]
470 )]
471 pub files: Vec<PathBuf>,
472}
473
474#[derive(Args, Debug, Clone)]
475pub struct BackgroundSubagentArgs {
476 #[arg(long = "agent-name", value_name = "NAME")]
477 pub agent_name: String,
478 #[arg(long = "parent-session-id", value_name = "SESSION_ID")]
479 pub parent_session_id: String,
480 #[arg(long = "session-id", value_name = "SESSION_ID")]
481 pub session_id: String,
482 #[arg(long = "prompt", value_name = "PROMPT")]
483 pub prompt: String,
484 #[arg(long = "max-turns", value_name = "COUNT")]
485 pub max_turns: Option<usize>,
486 #[arg(long = "model-override", value_name = "MODEL")]
487 pub model_override: Option<String>,
488 #[arg(long = "reasoning-override", value_name = "LEVEL")]
489 pub reasoning_override: Option<String>,
490}
491
492#[derive(Subcommand, Debug, Clone)]
494pub enum Commands {
495 #[command(name = "acp")]
497 AgentClientProtocol {
498 #[arg(value_enum, default_value_t = AgentClientProtocolTarget::Zed)]
500 target: AgentClientProtocolTarget,
501 },
502
503 Chat,
505
506 Ask {
516 #[arg(
518 value_name = "PROMPT",
519 long_help = "The prompt to send to the model.\n\nOmit to read from stdin (piped input).\nUse '-' to explicitly force reading from stdin."
520 )]
521 prompt: Option<String>,
522 #[arg(
524 long = "output-format",
525 value_enum,
526 value_name = "FORMAT",
527 long_help = "Output format for the response.\n\nCurrently supports:\n json - Emit the response as a structured JSON document."
528 )]
529 output_format: Option<AskOutputFormat>,
530 },
531 Exec {
544 #[arg(
546 long,
547 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."
548 )]
549 json: bool,
550 #[arg(
552 long,
553 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."
554 )]
555 dry_run: bool,
556 #[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.")]
558 events: Option<PathBuf>,
559 #[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.")]
561 last_message_file: Option<PathBuf>,
562 #[command(subcommand)]
564 command: Option<ExecSubcommand>,
565 #[arg(
567 value_name = "PROMPT",
568 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\""
569 )]
570 prompt: Option<String>,
571 },
572 Schedule {
584 #[command(subcommand)]
585 command: ScheduleSubcommand,
586 },
587
588 #[command(name = "background-subagent", hide = true)]
590 BackgroundSubagent(BackgroundSubagentArgs),
591
592 #[command(
594 long_about = "Run a non-interactive code review.\n\nExamples:\n vtcode review\n vtcode review --last-diff\n vtcode review --target HEAD~1..HEAD\n vtcode review --file src/main.rs --file vtcode-core/src/lib.rs\n vtcode review --style security"
595 )]
596 Review(ReviewArgs),
597
598 Schema {
600 #[command(subcommand)]
601 command: SchemaCommands,
602 },
603
604 ChatVerbose,
606
607 Analyze {
609 #[arg(value_name = "TYPE", default_value = "full")]
611 analysis_type: String,
612 },
613
614 #[command(name = "trajectory")]
616 Trajectory {
617 #[arg(long)]
619 file: Option<PathBuf>,
620 #[arg(long, default_value_t = 10)]
622 top: usize,
623 },
624
625 Notify {
627 #[arg(long, value_name = "TITLE")]
629 title: Option<String>,
630 #[arg(value_name = "MESSAGE")]
632 message: String,
633 },
634
635 Benchmark {
637 #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
639 task_file: Option<PathBuf>,
640 #[arg(long, value_name = "JSON")]
642 task: Option<String>,
643 #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
645 output: Option<PathBuf>,
646 #[arg(long, value_name = "COUNT")]
648 max_tasks: Option<usize>,
649 },
650
651 CreateProject {
653 name: String,
654 #[arg(long = "feature", value_name = "FEATURE", action = ArgAction::Append)]
655 features: Vec<String>,
656 },
657
658 Revert {
660 #[arg(short, long)]
662 turn: usize,
663 #[arg(long)]
665 partial: Option<String>,
666 },
667
668 Snapshots,
670
671 #[command(name = "cleanup-snapshots")]
682 CleanupSnapshots {
683 #[arg(short, long, default_value_t = 50)]
688 max: usize,
689 },
690
691 Init {
700 #[arg(
702 long,
703 short = 'f',
704 long_help = "Overwrite AGENTS.md without confirmation.\nUse this in CI/CD or scripts where interactive prompts are not possible."
705 )]
706 force: bool,
707 },
708
709 #[command(name = "init-project")]
719 InitProject {
720 #[arg(
722 long,
723 long_help = "Name for the project.\nDefaults to the current directory name if not specified."
724 )]
725 name: Option<String>,
726 #[arg(
728 long,
729 long_help = "Overwrite existing project structure without confirmation."
730 )]
731 force: bool,
732 #[arg(
734 long,
735 long_help = "Move existing config and cache files into the new project structure."
736 )]
737 migrate: bool,
738 },
739
740 Config {
750 #[arg(
752 long,
753 long_help = "Write the configuration to this path.\nDefaults to ./vtcode.toml in the current directory."
754 )]
755 output: Option<PathBuf>,
756 #[arg(
758 long,
759 long_help = "Write the configuration to ~/.vtcode/vtcode.toml.\nThis sets global defaults for all workspaces."
760 )]
761 global: bool,
762 },
763
764 Login {
775 #[arg(
777 long_help = "The provider to authenticate with.\nSupported: openai, openrouter, copilot, codex"
778 )]
779 provider: String,
780 #[arg(
782 long,
783 default_value_t = false,
784 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."
785 )]
786 device_code: bool,
787 },
788
789 Logout {
797 #[arg(
799 long_help = "The provider to deauthenticate.\nSupported: openai, openrouter, copilot, codex"
800 )]
801 provider: String,
802 },
803
804 Auth {
814 #[arg(
816 long_help = "Show status for a single provider.\nOmit to show status for all supported providers."
817 )]
818 provider: Option<String>,
819 },
820
821 #[command(name = "tool-policy")]
823 ToolPolicy {
824 #[command(subcommand)]
825 command: crate::cli::tool_policy_commands::ToolPolicyCommands,
826 },
827
828 #[command(name = "mcp")]
830 Mcp {
831 #[command(subcommand)]
832 command: crate::mcp::cli::McpCommands,
833 },
834
835 #[command(name = "a2a")]
837 A2a {
838 #[command(subcommand)]
839 command: super::super::a2a::cli::A2aCommands,
840 },
841
842 #[command(name = "app-server")]
844 AppServer {
845 #[arg(long, default_value = "stdio://")]
847 listen: String,
848 },
849
850 Models {
852 #[command(subcommand)]
853 command: ModelCommands,
854 },
855
856 #[command(name = "pods")]
858 Pods {
859 #[command(subcommand)]
860 command: PodsCommands,
861 },
862
863 Man {
865 command: Option<String>,
867 #[arg(short, long)]
869 output: Option<PathBuf>,
870 },
871
872 #[command(subcommand)]
884 Skills(SkillsSubcommand),
885
886 #[command(name = "list-skills", hide = true)]
888 ListSkills {},
889
890 #[command(name = "dependencies", visible_alias = "deps", subcommand)]
900 Dependencies(DependenciesSubcommand),
901
902 Check {
909 #[command(subcommand)]
910 command: CheckSubcommand,
911 },
912
913 #[command(name = "update")]
927 Update {
928 #[arg(
930 long,
931 long_help = "Check whether a newer version is available without installing it."
932 )]
933 check: bool,
934 #[arg(
936 long,
937 long_help = "Reinstall or downgrade even if the current version is already the latest."
938 )]
939 force: bool,
940 #[arg(
942 long,
943 long_help = "Print available release versions from GitHub and exit."
944 )]
945 list: bool,
946 #[arg(
948 long,
949 default_value_t = 10,
950 long_help = "Maximum number of versions to display with --list."
951 )]
952 limit: usize,
953 #[arg(
955 long,
956 value_name = "VERSION",
957 long_help = "Pin the binary to a specific version.\nAuto-updates are disabled until --unpin is used."
958 )]
959 pin: Option<String>,
960 #[arg(
962 long,
963 long_help = "Remove a previously set version pin and resume auto-updates."
964 )]
965 unpin: bool,
966 #[arg(
968 long,
969 value_name = "CHANNEL",
970 long_help = "Switch the release channel.\nAccepted values: stable, beta, nightly."
971 )]
972 channel: Option<String>,
973 #[arg(
975 long,
976 long_help = "Display the current update configuration (channel, pin, intervals) and exit."
977 )]
978 show_config: bool,
979 },
980
981 #[command(name = "anthropic-api")]
983 AnthropicApi {
984 #[arg(long, default_value = "11434")]
986 port: u16,
987 #[arg(long, default_value = "127.0.0.1")]
989 host: String,
990 },
991}
992
993#[derive(Clone, Copy, Debug, ValueEnum)]
995pub enum AgentClientProtocolTarget {
996 Zed,
998 Standard,
1000}
1001
1002#[derive(Subcommand, Debug, Clone)]
1004pub enum ModelCommands {
1005 List,
1007
1008 #[command(name = "set-provider")]
1010 SetProvider {
1011 provider: String,
1013 },
1014
1015 #[command(name = "set-model")]
1017 SetModel {
1018 model: String,
1020 },
1021
1022 Config {
1024 provider: String,
1026
1027 #[arg(long)]
1029 api_key: Option<String>,
1030
1031 #[arg(long)]
1033 base_url: Option<String>,
1034
1035 #[arg(long)]
1037 model: Option<String>,
1038 },
1039
1040 Test {
1042 provider: String,
1044 },
1045
1046 Compare,
1048
1049 Info {
1051 model: String,
1053 },
1054}
1055
1056#[derive(Subcommand, Debug, Clone)]
1058pub enum PodsCommands {
1059 Start {
1061 #[arg(long)]
1063 name: String,
1064 #[arg(long)]
1066 model: String,
1067 #[arg(long = "pod-name")]
1069 pod_name: Option<String>,
1070 #[arg(long)]
1072 ssh: Option<String>,
1073 #[arg(long = "gpu", value_name = "ID:NAME", action = ArgAction::Append)]
1075 gpus: Vec<String>,
1076 #[arg(long = "models-path")]
1078 models_path: Option<String>,
1079 #[arg(long)]
1081 profile: Option<String>,
1082 #[arg(long = "gpus")]
1084 gpus_count: Option<usize>,
1085 #[arg(long)]
1087 memory: Option<f32>,
1088 #[arg(long)]
1090 context: Option<String>,
1091 },
1092
1093 Stop {
1095 #[arg(long)]
1097 name: String,
1098 },
1099
1100 StopAll,
1102
1103 List,
1105
1106 Logs {
1108 #[arg(long)]
1110 name: String,
1111 },
1112
1113 KnownModels,
1115}
1116
1117#[derive(Debug, Subcommand, Clone)]
1119pub enum SkillsSubcommand {
1120 #[command(name = "list")]
1122 List {
1123 #[arg(long)]
1125 all: bool,
1126 },
1127
1128 #[command(name = "load")]
1130 Load {
1131 name: String,
1133 #[arg(long)]
1135 path: Option<PathBuf>,
1136 },
1137
1138 #[command(name = "unload")]
1140 Unload {
1141 name: String,
1143 },
1144
1145 #[command(name = "info")]
1147 Info {
1148 name: String,
1150 },
1151
1152 #[command(name = "create")]
1154 Create {
1155 path: PathBuf,
1157 #[arg(long)]
1159 template: Option<String>,
1160 },
1161
1162 #[command(name = "validate")]
1164 Validate {
1165 path: PathBuf,
1167 #[arg(long)]
1169 strict: bool,
1170 },
1171
1172 #[command(name = "check-compatibility")]
1174 CheckCompatibility,
1175
1176 #[command(name = "config")]
1178 Config,
1179
1180 #[command(name = "regenerate-index")]
1182 RegenerateIndex,
1183
1184 #[command(name = "skills-ref", subcommand)]
1186 SkillsRef(SkillsRefSubcommand),
1187}
1188
1189#[derive(Debug, Subcommand, Clone)]
1191pub enum SkillsRefSubcommand {
1192 #[command(name = "validate")]
1194 Validate {
1195 path: PathBuf,
1197 },
1198
1199 #[command(name = "to-prompt")]
1201 ToPrompt {
1202 paths: Vec<PathBuf>,
1204 },
1205
1206 #[command(name = "list")]
1208 List {
1209 path: Option<PathBuf>,
1211 },
1212}
1213
1214#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
1216pub enum ManagedDependency {
1217 #[value(name = "search-tools")]
1218 SearchTools,
1219 #[value(name = "ripgrep")]
1220 Ripgrep,
1221 #[value(name = "ast-grep")]
1222 AstGrep,
1223}
1224
1225#[derive(Debug, Subcommand, Clone)]
1227pub enum DependenciesSubcommand {
1228 #[command(name = "install")]
1230 Install {
1231 dependency: ManagedDependency,
1233 },
1234
1235 #[command(name = "status")]
1237 Status {
1238 dependency: ManagedDependency,
1240 },
1241}
1242
1243#[derive(Debug, Subcommand, Clone, PartialEq, Eq)]
1245pub enum CheckSubcommand {
1246 #[command(name = "ast-grep")]
1248 AstGrep,
1249}
1250
1251#[derive(Debug)]
1253pub struct ConfigFile {
1254 pub model: Option<String>,
1255 pub provider: Option<String>,
1256 pub api_key_env: Option<String>,
1257 pub verbose: Option<bool>,
1258 pub log_level: Option<String>,
1259 pub workspace: Option<PathBuf>,
1260 pub tools: Option<ToolConfig>,
1261 pub context: Option<ContextConfig>,
1262 pub logging: Option<LoggingConfig>,
1263 pub performance: Option<PerformanceConfig>,
1264 pub security: Option<SecurityConfig>,
1265}
1266
1267#[derive(Debug, serde::Deserialize)]
1269pub struct ToolConfig {
1270 pub enable_validation: Option<bool>,
1271 pub max_execution_time_seconds: Option<u64>,
1272 pub allow_file_creation: Option<bool>,
1273 pub allow_file_deletion: Option<bool>,
1274}
1275
1276#[derive(Debug, serde::Deserialize)]
1278pub struct ContextConfig {
1279 pub max_context_length: Option<usize>,
1280}
1281
1282#[derive(Debug, serde::Deserialize)]
1284pub struct LoggingConfig {
1285 pub file_logging: Option<bool>,
1286 pub log_directory: Option<String>,
1287 pub max_log_files: Option<usize>,
1288 pub max_log_size_mb: Option<usize>,
1289}
1290
1291#[cfg(test)]
1292mod exec_command_tests {
1293 use super::{
1294 CheckSubcommand, Cli, Commands, DependenciesSubcommand, ExecSubcommand, ManagedDependency,
1295 PodsCommands,
1296 };
1297 use clap::{CommandFactory, Parser};
1298 use std::path::PathBuf;
1299
1300 #[test]
1301 fn exec_shorthand_preserves_prompt() {
1302 let cli = Cli::parse_from(["vtcode", "exec", "count files"]);
1303 let Some(Commands::Exec {
1304 command, prompt, ..
1305 }) = cli.command
1306 else {
1307 panic!("expected exec command");
1308 };
1309
1310 assert!(command.is_none());
1311 assert_eq!(prompt.as_deref(), Some("count files"));
1312 }
1313
1314 #[test]
1315 fn exec_resume_parses_specific_session_and_prompt() {
1316 let cli = Cli::parse_from(["vtcode", "exec", "resume", "session-123", "follow up"]);
1317 let Some(Commands::Exec {
1318 command: Some(ExecSubcommand::Resume(resume)),
1319 prompt,
1320 ..
1321 }) = cli.command
1322 else {
1323 panic!("expected exec resume command");
1324 };
1325
1326 assert!(prompt.is_none());
1327 assert!(!resume.last);
1328 assert_eq!(resume.session_or_prompt.as_deref(), Some("session-123"));
1329 assert_eq!(resume.prompt.as_deref(), Some("follow up"));
1330 }
1331
1332 #[test]
1333 fn exec_resume_parses_last_flag() {
1334 let cli = Cli::parse_from(["vtcode", "exec", "resume", "--last", "continue"]);
1335 let Some(Commands::Exec {
1336 command: Some(ExecSubcommand::Resume(resume)),
1337 ..
1338 }) = cli.command
1339 else {
1340 panic!("expected exec resume command");
1341 };
1342
1343 assert!(resume.last);
1344 assert_eq!(resume.session_or_prompt.as_deref(), Some("continue"));
1345 assert!(resume.prompt.is_none());
1346 }
1347
1348 #[test]
1349 fn exec_resume_parses_all_flag() {
1350 let cli = Cli::parse_from(["vtcode", "exec", "resume", "--last", "--all", "continue"]);
1351 let Some(Commands::Exec {
1352 command: Some(ExecSubcommand::Resume(resume)),
1353 ..
1354 }) = cli.command
1355 else {
1356 panic!("expected exec resume command");
1357 };
1358
1359 assert!(resume.last);
1360 assert!(resume.all);
1361 assert_eq!(resume.session_or_prompt.as_deref(), Some("continue"));
1362 }
1363
1364 #[test]
1365 fn exec_resume_allows_last_without_positional_for_stdin_prompt() {
1366 let cli = Cli::parse_from(["vtcode", "exec", "resume", "--last"]);
1367 let Some(Commands::Exec {
1368 command: Some(ExecSubcommand::Resume(resume)),
1369 ..
1370 }) = cli.command
1371 else {
1372 panic!("expected exec resume command");
1373 };
1374
1375 assert!(resume.last);
1376 assert!(resume.session_or_prompt.is_none());
1377 assert!(resume.prompt.is_none());
1378 }
1379
1380 #[test]
1381 fn global_resume_and_continue_parse_all_flag() {
1382 let resume_cli = Cli::parse_from(["vtcode", "--resume", "session-123", "--all"]);
1383 assert_eq!(resume_cli.resume_session.as_deref(), Some("session-123"));
1384 assert!(resume_cli.all);
1385
1386 let continue_cli = Cli::parse_from(["vtcode", "--continue", "--all"]);
1387 assert!(continue_cli.continue_latest);
1388 assert!(continue_cli.all);
1389 }
1390
1391 #[test]
1392 fn global_fork_flags_parse_summarize() {
1393 let cli = Cli::parse_from(["vtcode", "--fork-session", "session-123", "--summarize"]);
1394 assert_eq!(cli.fork_session.as_deref(), Some("session-123"));
1395 assert!(cli.summarize);
1396 }
1397
1398 #[test]
1399 fn notify_parses_title_and_message() {
1400 let cli = Cli::parse_from(["vtcode", "notify", "--title", "VT Code", "Session started"]);
1401 let Some(Commands::Notify { title, message }) = cli.command else {
1402 panic!("expected notify command");
1403 };
1404
1405 assert_eq!(title.as_deref(), Some("VT Code"));
1406 assert_eq!(message, "Session started");
1407 }
1408
1409 #[test]
1410 fn review_defaults_to_current_diff() {
1411 let cli = Cli::parse_from(["vtcode", "review"]);
1412 let Some(Commands::Review(review)) = cli.command else {
1413 panic!("expected review command");
1414 };
1415
1416 assert!(!review.last_diff);
1417 assert!(review.target.is_none());
1418 assert!(review.files.is_empty());
1419 assert!(review.style.is_none());
1420 }
1421
1422 #[test]
1423 fn review_parses_target_and_style_flags() {
1424 let cli = Cli::parse_from([
1425 "vtcode",
1426 "review",
1427 "--target",
1428 "HEAD~1..HEAD",
1429 "--style",
1430 "security",
1431 ]);
1432 let Some(Commands::Review(review)) = cli.command else {
1433 panic!("expected review command");
1434 };
1435
1436 assert_eq!(review.target.as_deref(), Some("HEAD~1..HEAD"));
1437 assert_eq!(review.style.as_deref(), Some("security"));
1438 assert!(!review.last_diff);
1439 }
1440
1441 #[test]
1442 fn review_parses_files() {
1443 let cli = Cli::parse_from([
1444 "vtcode",
1445 "review",
1446 "--file",
1447 "src/main.rs",
1448 "--file",
1449 "src/lib.rs",
1450 ]);
1451 let Some(Commands::Review(review)) = cli.command else {
1452 panic!("expected review command");
1453 };
1454
1455 assert_eq!(review.files.len(), 2);
1456 assert_eq!(review.files[0], PathBuf::from("src/main.rs"));
1457 assert_eq!(review.files[1], PathBuf::from("src/lib.rs"));
1458 }
1459
1460 #[test]
1461 fn dependencies_install_parses_ast_grep() {
1462 let cli = Cli::parse_from(["vtcode", "dependencies", "install", "ast-grep"]);
1463 let Some(Commands::Dependencies(DependenciesSubcommand::Install { dependency })) =
1464 cli.command
1465 else {
1466 panic!("expected dependencies install command");
1467 };
1468
1469 assert_eq!(dependency, ManagedDependency::AstGrep);
1470 }
1471
1472 #[test]
1473 fn dependencies_install_parses_ripgrep() {
1474 let cli = Cli::parse_from(["vtcode", "dependencies", "install", "ripgrep"]);
1475 let Some(Commands::Dependencies(DependenciesSubcommand::Install { dependency })) =
1476 cli.command
1477 else {
1478 panic!("expected dependencies install command");
1479 };
1480
1481 assert_eq!(dependency, ManagedDependency::Ripgrep);
1482 }
1483
1484 #[test]
1485 fn dependencies_install_parses_search_tools() {
1486 let cli = Cli::parse_from(["vtcode", "dependencies", "install", "search-tools"]);
1487 let Some(Commands::Dependencies(DependenciesSubcommand::Install { dependency })) =
1488 cli.command
1489 else {
1490 panic!("expected dependencies install command");
1491 };
1492
1493 assert_eq!(dependency, ManagedDependency::SearchTools);
1494 }
1495
1496 #[test]
1497 fn deps_alias_parses_status_command() {
1498 let cli = Cli::parse_from(["vtcode", "deps", "status", "ast-grep"]);
1499 let Some(Commands::Dependencies(DependenciesSubcommand::Status { dependency })) =
1500 cli.command
1501 else {
1502 panic!("expected deps status command");
1503 };
1504
1505 assert_eq!(dependency, ManagedDependency::AstGrep);
1506 }
1507
1508 #[test]
1509 fn deps_alias_parses_ripgrep_status_command() {
1510 let cli = Cli::parse_from(["vtcode", "deps", "status", "ripgrep"]);
1511 let Some(Commands::Dependencies(DependenciesSubcommand::Status { dependency })) =
1512 cli.command
1513 else {
1514 panic!("expected deps status command");
1515 };
1516
1517 assert_eq!(dependency, ManagedDependency::Ripgrep);
1518 }
1519
1520 #[test]
1521 fn deps_alias_parses_search_tools_status_command() {
1522 let cli = Cli::parse_from(["vtcode", "deps", "status", "search-tools"]);
1523 let Some(Commands::Dependencies(DependenciesSubcommand::Status { dependency })) =
1524 cli.command
1525 else {
1526 panic!("expected deps status command");
1527 };
1528
1529 assert_eq!(dependency, ManagedDependency::SearchTools);
1530 }
1531
1532 #[test]
1533 fn check_parses_ast_grep_subcommand() {
1534 let cli = Cli::parse_from(["vtcode", "check", "ast-grep"]);
1535 let Some(Commands::Check { command }) = cli.command else {
1536 panic!("expected check command");
1537 };
1538
1539 assert_eq!(command, CheckSubcommand::AstGrep);
1540 }
1541
1542 #[test]
1543 fn pods_start_parses_model_and_gpu_flags() {
1544 let cli = Cli::parse_from([
1545 "vtcode",
1546 "pods",
1547 "start",
1548 "--name",
1549 "llama",
1550 "--model",
1551 "meta-llama/Llama-3.1-8B-Instruct",
1552 "--pod-name",
1553 "gpu-box",
1554 "--ssh",
1555 "ssh root@gpu.example.com",
1556 "--gpu",
1557 "0:A100",
1558 "--gpu",
1559 "1:A100",
1560 "--gpus",
1561 "2",
1562 "--memory",
1563 "90",
1564 "--context",
1565 "32k",
1566 ]);
1567 let Some(Commands::Pods {
1568 command:
1569 PodsCommands::Start {
1570 name,
1571 model,
1572 pod_name,
1573 ssh,
1574 gpus,
1575 models_path,
1576 profile,
1577 gpus_count,
1578 memory,
1579 context,
1580 },
1581 }) = cli.command
1582 else {
1583 panic!("expected pods start command");
1584 };
1585
1586 assert_eq!(name, "llama");
1587 assert_eq!(model, "meta-llama/Llama-3.1-8B-Instruct");
1588 assert_eq!(pod_name.as_deref(), Some("gpu-box"));
1589 assert_eq!(ssh.as_deref(), Some("ssh root@gpu.example.com"));
1590 assert_eq!(gpus, vec!["0:A100", "1:A100"]);
1591 assert!(models_path.is_none());
1592 assert!(profile.is_none());
1593 assert_eq!(gpus_count, Some(2));
1594 assert_eq!(memory, Some(90.0));
1595 assert_eq!(context.as_deref(), Some("32k"));
1596 }
1597}
1598
1599#[derive(Debug, serde::Deserialize)]
1601pub struct PerformanceConfig {
1602 pub enabled: Option<bool>,
1603 pub track_token_usage: Option<bool>,
1604 pub track_api_costs: Option<bool>,
1605 pub track_response_times: Option<bool>,
1606 pub enable_benchmarking: Option<bool>,
1607 pub metrics_retention_days: Option<usize>,
1608}
1609
1610#[derive(Debug, serde::Deserialize)]
1612pub struct SecurityConfig {
1613 pub level: Option<String>,
1614 pub enable_audit_logging: Option<bool>,
1615 pub enable_vulnerability_scanning: Option<bool>,
1616 pub allow_external_urls: Option<bool>,
1617 pub max_file_access_depth: Option<usize>,
1618}
1619
1620impl Default for Cli {
1621 fn default() -> Self {
1622 Self {
1623 color: ColorSelection {
1624 color: ColorChoice::Auto,
1625 },
1626 workspace_path: None,
1627 model: Some(ModelId::default().to_string()),
1628 provider: Some("gemini".to_owned()),
1629 api_key_env: "GEMINI_API_KEY".to_owned(),
1630 workspace: None,
1631 research_preview: false,
1632 security_level: "moderate".to_owned(),
1633 show_file_diffs: false,
1634 max_concurrent_ops: 5,
1635 api_rate_limit: 30,
1636 max_tool_calls: 10,
1637 verbose: false,
1638 quiet: false,
1639 config: Vec::new(),
1640 log_level: "info".to_owned(),
1641 no_color: false,
1642 theme: None,
1643 skip_confirmations: false,
1644 codex_experimental: false,
1645 no_codex_experimental: false,
1646 print: None,
1647 full_auto: None,
1648 resume_session: None,
1649 continue_latest: false,
1650 fork_session: None,
1651 all: false,
1652 session_id: None,
1653 summarize: false,
1654 debug: false,
1655 enable_skills: false, tick_rate: 250, frame_rate: 60, agent: None, allowed_tools: Vec::new(), disallowed_tools: Vec::new(), dangerously_skip_permissions: false, ide: false, chrome: false, no_chrome: false, command: Some(Commands::Chat),
1666 }
1667 }
1668}
1669
1670impl Cli {
1671 pub fn get_model(&self) -> String {
1673 self.model
1674 .clone()
1675 .unwrap_or_else(|| ModelId::default().to_string())
1676 }
1677
1678 pub async fn load_config(&self) -> Result<ConfigFile, Box<dyn std::error::Error>> {
1688 use std::path::Path;
1689 use tokio::fs;
1690
1691 let explicit_path = self.config.iter().find_map(|entry| {
1693 let trimmed = entry.trim();
1694 if trimmed.contains('=') || trimmed.is_empty() {
1695 None
1696 } else {
1697 Some(PathBuf::from(trimmed))
1698 }
1699 });
1700
1701 let path = if let Some(p) = explicit_path {
1702 p
1703 } else {
1704 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
1705 let primary = cwd.join("vtcode.toml");
1706 let secondary = cwd.join(".vtcode.toml");
1707 if fs::try_exists(&primary).await.unwrap_or(false) {
1708 primary
1709 } else if fs::try_exists(&secondary).await.unwrap_or(false) {
1710 secondary
1711 } else {
1712 return Ok(ConfigFile {
1714 model: None,
1715 provider: None,
1716 api_key_env: None,
1717 verbose: None,
1718 log_level: None,
1719 workspace: None,
1720 tools: None,
1721 context: None,
1722 logging: None,
1723 performance: None,
1724 security: None,
1725 });
1726 }
1727 };
1728
1729 let text = fs::read_to_string(&path).await?;
1730
1731 let mut cfg = ConfigFile {
1733 model: None,
1734 provider: None,
1735 api_key_env: None,
1736 verbose: None,
1737 log_level: None,
1738 workspace: None,
1739 tools: None,
1740 context: None,
1741 logging: None,
1742 performance: None,
1743 security: None,
1744 };
1745
1746 for raw_line in text.lines() {
1747 let line = raw_line.trim();
1748 if line.is_empty() || line.starts_with('#') || line.starts_with("//") {
1749 continue;
1750 }
1751 let line = match line.find('#') {
1753 Some(idx) => &line[..idx],
1754 None => line,
1755 }
1756 .trim();
1757
1758 let mut parts = line.splitn(2, '=');
1760 let key = parts.next().map(|s| s.trim()).unwrap_or("");
1761 let val = parts.next().map(|s| s.trim()).unwrap_or("");
1762 if key.is_empty() || val.is_empty() {
1763 continue;
1764 }
1765
1766 let unquote = |s: &str| -> String {
1768 let s = s.trim();
1769 if (s.starts_with('"') && s.ends_with('"'))
1770 || (s.starts_with('\'') && s.ends_with('\''))
1771 {
1772 s[1..s.len() - 1].to_owned()
1773 } else {
1774 s.to_owned()
1775 }
1776 };
1777
1778 match key {
1779 "model" => cfg.model = Some(unquote(val)),
1780 "api_key_env" => cfg.api_key_env = Some(unquote(val)),
1781 "verbose" => {
1782 let v = unquote(val).to_lowercase();
1783 cfg.verbose = Some(matches!(v.as_str(), "true" | "1" | "yes"));
1784 }
1785 "log_level" => cfg.log_level = Some(unquote(val)),
1786 "workspace" => {
1787 let v = unquote(val);
1788 let p = if Path::new(&v).is_absolute() {
1789 PathBuf::from(v)
1790 } else {
1791 let base = path.parent().unwrap_or(Path::new("."));
1793 base.join(v)
1794 };
1795 cfg.workspace = Some(p);
1796 }
1797 _ => {
1798 }
1800 }
1801 }
1802
1803 Ok(cfg)
1804 }
1805
1806 pub fn get_workspace(&self) -> PathBuf {
1808 self.workspace
1809 .clone()
1810 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
1811 }
1812
1813 pub fn get_api_key_env(&self) -> String {
1818 crate::config::api_keys::resolve_api_key_env(
1819 self.provider
1820 .as_deref()
1821 .unwrap_or(crate::config::constants::defaults::DEFAULT_PROVIDER),
1822 &self.api_key_env,
1823 )
1824 }
1825
1826 pub fn is_verbose(&self) -> bool {
1828 self.verbose
1829 }
1830
1831 pub fn is_research_preview_enabled(&self) -> bool {
1834 self.research_preview
1835 }
1836
1837 pub fn get_security_level(&self) -> &str {
1839 &self.security_level
1840 }
1841
1842 pub fn is_debug_mode(&self) -> bool {
1844 self.debug || self.verbose
1845 }
1846
1847 pub fn codex_experimental_override(&self) -> Option<bool> {
1848 if self.codex_experimental {
1849 Some(true)
1850 } else if self.no_codex_experimental {
1851 Some(false)
1852 } else {
1853 None
1854 }
1855 }
1856}
1857
1858#[cfg(test)]
1859mod tests {
1860 use super::{Cli, long_version};
1861 use clap::Parser;
1862
1863 #[test]
1864 fn long_version_includes_expected_sections() {
1865 let text = long_version();
1866 assert!(text.contains("Authors:"));
1867 assert!(text.contains("Config directory:"));
1868 assert!(text.contains("Data directory:"));
1869 assert!(text.contains("VTCODE_CONFIG"));
1870 assert!(text.contains("VTCODE_DATA"));
1871 }
1872
1873 #[test]
1874 fn long_version_starts_with_build_git_info() {
1875 let text = long_version();
1876 let expected = option_env!("VT_CODE_GIT_INFO").unwrap_or(env!("CARGO_PKG_VERSION"));
1877 assert!(text.starts_with(expected));
1878 }
1879
1880 #[test]
1881 fn config_file_api_key_env_uses_provider_default() {
1882 let cli = Cli::parse_from(["vtcode", "--provider", "minimax"]);
1883
1884 assert_eq!(cli.get_api_key_env(), "MINIMAX_API_KEY");
1885 }
1886
1887 #[test]
1888 fn config_file_api_key_env_preserves_explicit_override() {
1889 let cli = Cli::parse_from([
1890 "vtcode",
1891 "--provider",
1892 "openai",
1893 "--api-key-env",
1894 "CUSTOM_OPENAI_KEY",
1895 ]);
1896
1897 assert_eq!(cli.get_api_key_env(), "CUSTOM_OPENAI_KEY");
1898 }
1899
1900 #[test]
1901 fn parses_app_server_command_with_stdio_listen_target() {
1902 let cli = Cli::parse_from(["vtcode", "app-server", "--listen", "stdio://"]);
1903
1904 assert!(matches!(
1905 cli.command,
1906 Some(super::Commands::AppServer { ref listen }) if listen == "stdio://"
1907 ));
1908 }
1909
1910 #[test]
1911 fn parses_init_force_flag() {
1912 let cli = Cli::parse_from(["vtcode", "init", "--force"]);
1913
1914 assert!(matches!(
1915 cli.command,
1916 Some(super::Commands::Init { force: true })
1917 ));
1918 }
1919
1920 #[test]
1921 fn parses_codex_login_device_code_flag() {
1922 let cli = Cli::parse_from(["vtcode", "login", "codex", "--device-code"]);
1923
1924 assert!(matches!(
1925 cli.command,
1926 Some(super::Commands::Login {
1927 ref provider,
1928 device_code: true
1929 }) if provider == "codex"
1930 ));
1931 }
1932
1933 #[test]
1934 fn parses_codex_experimental_flags() {
1935 let enabled = Cli::parse_from(["vtcode", "--codex-experimental"]);
1936 assert_eq!(enabled.codex_experimental_override(), Some(true));
1937
1938 let disabled = Cli::parse_from(["vtcode", "--no-codex-experimental"]);
1939 assert_eq!(disabled.codex_experimental_override(), Some(false));
1940 }
1941
1942 #[test]
1943 fn codex_experimental_flags_conflict() {
1944 let result =
1945 Cli::try_parse_from(["vtcode", "--codex-experimental", "--no-codex-experimental"]);
1946
1947 result.unwrap_err();
1948 }
1949
1950 #[test]
1951 fn parses_create_project_feature_flags() {
1952 let cli = Cli::parse_from([
1953 "vtcode",
1954 "create-project",
1955 "demo",
1956 "--feature",
1957 "web",
1958 "--feature",
1959 "db",
1960 ]);
1961
1962 assert!(matches!(
1963 cli.command,
1964 Some(super::Commands::CreateProject { ref name, ref features })
1965 if name == "demo" && features == &vec!["web".to_string(), "db".to_string()]
1966 ));
1967 }
1968
1969 #[test]
1970 fn parses_revert_partial_long_flag() {
1971 let cli = Cli::parse_from(["vtcode", "revert", "--turn", "3", "--partial", "code"]);
1972
1973 assert!(matches!(
1974 cli.command,
1975 Some(super::Commands::Revert {
1976 turn: 3,
1977 partial: Some(ref scope)
1978 }) if scope == "code"
1979 ));
1980 }
1981}