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 value_name = "PROMPT",
229 num_args = 0..=1,
230 default_missing_value = "",
231 value_hint = ValueHint::Other
232 )]
233 pub full_auto: Option<String>,
234
235 #[arg(
237 short = 'r',
238 long = "resume",
239 global = true,
240 value_name = "SESSION_ID",
241 num_args = 0..=1,
242 default_missing_value = "__interactive__",
243 conflicts_with_all = ["continue_latest", "full_auto"]
244 )]
245 pub resume_session: Option<String>,
246
247 #[arg(
249 long = "continue",
250 visible_alias = "continue-session",
251 global = true,
252 conflicts_with_all = ["resume_session", "full_auto"]
253 )]
254 pub continue_latest: bool,
255
256 #[arg(
258 long = "fork-session",
259 global = true,
260 value_name = "SESSION_ID",
261 conflicts_with_all = ["resume_session", "continue_latest", "full_auto"]
262 )]
263 pub fork_session: Option<String>,
264
265 #[arg(long, global = true)]
267 pub all: bool,
268
269 #[arg(long = "session-id", global = true, value_name = "CUSTOM_SUFFIX")]
271 pub session_id: Option<String>,
272
273 #[arg(long, global = true)]
275 pub summarize: bool,
276
277 #[arg(long, global = true, value_name = "AGENT")]
279 pub agent: Option<String>,
280
281 #[arg(long = "allowed-tools", global = true, value_name = "TOOLS", action = ArgAction::Append)]
283 pub allowed_tools: Vec<String>,
284
285 #[arg(long = "disallowed-tools", global = true, value_name = "TOOLS", action = ArgAction::Append)]
287 pub disallowed_tools: Vec<String>,
288
289 #[arg(long = "dangerously-skip-permissions", global = true)]
291 pub dangerously_skip_permissions: bool,
292
293 #[arg(long, global = true)]
295 pub ide: bool,
296
297 #[arg(long, global = true, value_name = "MODE")]
299 pub permission_mode: Option<String>,
300
301 #[command(subcommand)]
302 pub command: Option<Commands>,
303}
304
305#[derive(Debug, Default, Clone)]
307pub struct AskCommandOptions {
308 pub output_format: Option<AskOutputFormat>,
309 pub allowed_tools: Vec<String>,
310 pub disallowed_tools: Vec<String>,
311 pub skip_confirmations: bool,
312}
313
314#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
316pub enum AskOutputFormat {
317 Json,
319}
320
321#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
323pub enum SchemaOutputFormat {
324 Json,
326 Ndjson,
328}
329
330#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
332pub enum SchemaMode {
333 Minimal,
335 Progressive,
337 Full,
339}
340
341#[derive(Subcommand, Debug, Clone)]
343pub enum SchemaCommands {
344 Tools {
346 #[arg(long, value_enum, default_value_t = SchemaMode::Progressive)]
348 mode: SchemaMode,
349 #[arg(long, value_enum, default_value_t = SchemaOutputFormat::Json)]
351 format: SchemaOutputFormat,
352 #[arg(long = "name", value_name = "TOOL")]
354 names: Vec<String>,
355 },
356}
357
358#[derive(Subcommand, Debug, Clone)]
360pub enum ExecSubcommand {
361 #[command(
363 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"
364 )]
365 Resume(ExecResumeArgs),
366}
367
368#[derive(Args, Debug, Clone)]
370pub struct ExecResumeArgs {
371 #[arg(long)]
373 pub last: bool,
374 #[arg(long)]
376 pub all: bool,
377 #[arg(value_name = "SESSION_ID_OR_PROMPT", required_unless_present = "last")]
379 pub session_or_prompt: Option<String>,
380 #[arg(value_name = "PROMPT")]
382 pub prompt: Option<String>,
383}
384
385#[derive(Subcommand, Debug, Clone)]
387pub enum ScheduleSubcommand {
388 #[command(
390 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\""
391 )]
392 Create(ScheduleCreateArgs),
393 List,
395 Delete {
397 #[arg(value_name = "TASK_ID")]
398 id: String,
399 },
400 Serve,
402 #[command(name = "install-service")]
404 InstallService,
405 #[command(name = "uninstall-service")]
407 UninstallService,
408}
409
410#[derive(Args, Debug, Clone)]
412pub struct ScheduleCreateArgs {
413 #[arg(long, value_name = "NAME")]
415 pub name: Option<String>,
416 #[arg(long, value_name = "PROMPT", conflicts_with = "reminder")]
418 pub prompt: Option<String>,
419 #[arg(long, value_name = "TEXT", conflicts_with = "prompt")]
421 pub reminder: Option<String>,
422 #[arg(long, value_name = "DURATION", conflicts_with_all = ["cron", "at"])]
424 pub every: Option<String>,
425 #[arg(long, value_name = "EXPR", conflicts_with_all = ["every", "at"])]
427 pub cron: Option<String>,
428 #[arg(long, value_name = "TIME", conflicts_with_all = ["every", "cron"])]
430 pub at: Option<String>,
431 #[arg(long, value_name = "PATH", value_hint = ValueHint::DirPath)]
433 pub workspace: Option<PathBuf>,
434}
435
436#[derive(Args, Debug, Clone)]
438pub struct ReviewArgs {
439 #[arg(long)]
441 pub json: bool,
442 #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
444 pub events: Option<PathBuf>,
445 #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
447 pub last_message_file: Option<PathBuf>,
448 #[arg(long, conflicts_with_all = ["target", "files"])]
450 pub last_diff: bool,
451 #[arg(long, value_name = "TARGET", conflicts_with = "files")]
453 pub target: Option<String>,
454 #[arg(long, value_name = "STYLE")]
456 pub style: Option<String>,
457 #[arg(
459 long = "file",
460 value_name = "FILE",
461 value_hint = ValueHint::FilePath,
462 conflicts_with_all = ["last_diff", "target"]
463 )]
464 pub files: Vec<PathBuf>,
465}
466
467#[derive(Args, Debug, Clone)]
468pub struct BackgroundSubagentArgs {
469 #[arg(long = "agent-name", value_name = "NAME")]
470 pub agent_name: String,
471 #[arg(long = "parent-session-id", value_name = "SESSION_ID")]
472 pub parent_session_id: String,
473 #[arg(long = "session-id", value_name = "SESSION_ID")]
474 pub session_id: String,
475 #[arg(long = "prompt", value_name = "PROMPT")]
476 pub prompt: String,
477 #[arg(long = "max-turns", value_name = "COUNT")]
478 pub max_turns: Option<usize>,
479 #[arg(long = "model-override", value_name = "MODEL")]
480 pub model_override: Option<String>,
481 #[arg(long = "reasoning-override", value_name = "LEVEL")]
482 pub reasoning_override: Option<String>,
483}
484
485#[derive(Subcommand, Debug, Clone)]
487pub enum Commands {
488 #[command(name = "acp")]
490 AgentClientProtocol {
491 #[arg(value_enum, default_value_t = AgentClientProtocolTarget::Zed)]
493 target: AgentClientProtocolTarget,
494 },
495
496 Chat,
498
499 Ask {
509 #[arg(
511 value_name = "PROMPT",
512 long_help = "The prompt to send to the model.\n\nOmit to read from stdin (piped input).\nUse '-' to explicitly force reading from stdin."
513 )]
514 prompt: Option<String>,
515 #[arg(
517 long = "output-format",
518 value_enum,
519 value_name = "FORMAT",
520 long_help = "Output format for the response.\n\nCurrently supports:\n json - Emit the response as a structured JSON document."
521 )]
522 output_format: Option<AskOutputFormat>,
523 },
524 Exec {
537 #[arg(
539 long,
540 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."
541 )]
542 json: bool,
543 #[arg(
545 long,
546 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."
547 )]
548 dry_run: bool,
549 #[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.")]
551 events: Option<PathBuf>,
552 #[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.")]
554 last_message_file: Option<PathBuf>,
555 #[command(subcommand)]
557 command: Option<ExecSubcommand>,
558 #[arg(
560 value_name = "PROMPT",
561 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\""
562 )]
563 prompt: Option<String>,
564 },
565 Schedule {
577 #[command(subcommand)]
578 command: ScheduleSubcommand,
579 },
580
581 #[command(name = "background-subagent", hide = true)]
583 BackgroundSubagent(BackgroundSubagentArgs),
584
585 #[command(
587 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"
588 )]
589 Review(ReviewArgs),
590
591 Schema {
593 #[command(subcommand)]
594 command: SchemaCommands,
595 },
596
597 ChatVerbose,
599
600 Analyze {
602 #[arg(value_name = "TYPE", default_value = "full")]
604 analysis_type: String,
605 },
606
607 #[command(name = "trajectory")]
609 Trajectory {
610 #[arg(long)]
612 file: Option<PathBuf>,
613 #[arg(long, default_value_t = 10)]
615 top: usize,
616 },
617
618 Notify {
620 #[arg(long, value_name = "TITLE")]
622 title: Option<String>,
623 #[arg(value_name = "MESSAGE")]
625 message: String,
626 },
627
628 Benchmark {
630 #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
632 task_file: Option<PathBuf>,
633 #[arg(long, value_name = "JSON")]
635 task: Option<String>,
636 #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
638 output: Option<PathBuf>,
639 #[arg(long, value_name = "COUNT")]
641 max_tasks: Option<usize>,
642 },
643
644 CreateProject {
646 name: String,
647 #[arg(long = "feature", value_name = "FEATURE", action = ArgAction::Append)]
648 features: Vec<String>,
649 },
650
651 Revert {
653 #[arg(short, long)]
655 turn: usize,
656 #[arg(long)]
658 partial: Option<String>,
659 },
660
661 Snapshots,
663
664 #[command(name = "cleanup-snapshots")]
675 CleanupSnapshots {
676 #[arg(short, long, default_value_t = 50)]
681 max: usize,
682 },
683
684 Init {
693 #[arg(
695 long,
696 short = 'f',
697 long_help = "Overwrite AGENTS.md without confirmation.\nUse this in CI/CD or scripts where interactive prompts are not possible."
698 )]
699 force: bool,
700 },
701
702 #[command(name = "init-project")]
712 InitProject {
713 #[arg(
715 long,
716 long_help = "Name for the project.\nDefaults to the current directory name if not specified."
717 )]
718 name: Option<String>,
719 #[arg(
721 long,
722 long_help = "Overwrite existing project structure without confirmation."
723 )]
724 force: bool,
725 #[arg(
727 long,
728 long_help = "Move existing config and cache files into the new project structure."
729 )]
730 migrate: bool,
731 },
732
733 Config {
743 #[arg(
745 long,
746 long_help = "Write the configuration to this path.\nDefaults to ./vtcode.toml in the current directory."
747 )]
748 output: Option<PathBuf>,
749 #[arg(
751 long,
752 long_help = "Write the configuration to ~/.vtcode/vtcode.toml.\nThis sets global defaults for all workspaces."
753 )]
754 global: bool,
755 },
756
757 Login {
768 #[arg(
770 long_help = "The provider to authenticate with.\nSupported: openai, openrouter, copilot, codex"
771 )]
772 provider: String,
773 #[arg(
775 long,
776 default_value_t = false,
777 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."
778 )]
779 device_code: bool,
780 },
781
782 Logout {
790 #[arg(
792 long_help = "The provider to deauthenticate.\nSupported: openai, openrouter, copilot, codex"
793 )]
794 provider: String,
795 },
796
797 Auth {
807 #[arg(
809 long_help = "Show status for a single provider.\nOmit to show status for all supported providers."
810 )]
811 provider: Option<String>,
812 },
813
814 #[command(name = "tool-policy")]
816 ToolPolicy {
817 #[command(subcommand)]
818 command: crate::cli::tool_policy_commands::ToolPolicyCommands,
819 },
820
821 #[command(name = "mcp")]
823 Mcp {
824 #[command(subcommand)]
825 command: crate::mcp::cli::McpCommands,
826 },
827
828 #[command(name = "a2a")]
830 A2a {
831 #[command(subcommand)]
832 command: super::super::a2a::cli::A2aCommands,
833 },
834
835 #[command(name = "app-server")]
837 AppServer {
838 #[arg(long, default_value = "stdio://")]
840 listen: String,
841 },
842
843 Models {
845 #[command(subcommand)]
846 command: ModelCommands,
847 },
848
849 #[command(name = "pods")]
851 Pods {
852 #[command(subcommand)]
853 command: PodsCommands,
854 },
855
856 Man {
858 command: Option<String>,
860 #[arg(short, long)]
862 output: Option<PathBuf>,
863 },
864
865 #[command(subcommand)]
877 Skills(SkillsSubcommand),
878
879 #[command(name = "list-skills", hide = true)]
881 ListSkills {},
882
883 #[command(name = "dependencies", visible_alias = "deps", subcommand)]
893 Dependencies(DependenciesSubcommand),
894
895 Check {
902 #[command(subcommand)]
903 command: CheckSubcommand,
904 },
905
906 #[command(name = "update")]
920 Update {
921 #[arg(
923 long,
924 long_help = "Check whether a newer version is available without installing it."
925 )]
926 check: bool,
927 #[arg(
929 long,
930 long_help = "Reinstall or downgrade even if the current version is already the latest."
931 )]
932 force: bool,
933 #[arg(
935 long,
936 long_help = "Print available release versions from GitHub and exit."
937 )]
938 list: bool,
939 #[arg(
941 long,
942 default_value_t = 10,
943 long_help = "Maximum number of versions to display with --list."
944 )]
945 limit: usize,
946 #[arg(
948 long,
949 value_name = "VERSION",
950 long_help = "Pin the binary to a specific version.\nAuto-updates are disabled until --unpin is used."
951 )]
952 pin: Option<String>,
953 #[arg(
955 long,
956 long_help = "Remove a previously set version pin and resume auto-updates."
957 )]
958 unpin: bool,
959 #[arg(
961 long,
962 value_name = "CHANNEL",
963 long_help = "Switch the release channel.\nAccepted values: stable, beta, nightly."
964 )]
965 channel: Option<String>,
966 #[arg(
968 long,
969 long_help = "Display the current update configuration (channel, pin, intervals) and exit."
970 )]
971 show_config: bool,
972 },
973
974 #[command(name = "anthropic-api")]
976 AnthropicApi {
977 #[arg(long, default_value = "11434")]
979 port: u16,
980 #[arg(long, default_value = "127.0.0.1")]
982 host: String,
983 },
984}
985
986#[derive(Clone, Copy, Debug, ValueEnum)]
988pub enum AgentClientProtocolTarget {
989 Zed,
991 Standard,
993}
994
995#[derive(Subcommand, Debug, Clone)]
997pub enum ModelCommands {
998 List,
1000
1001 #[command(name = "set-provider")]
1003 SetProvider {
1004 provider: String,
1006 },
1007
1008 #[command(name = "set-model")]
1010 SetModel {
1011 model: String,
1013 },
1014
1015 Config {
1017 provider: String,
1019
1020 #[arg(long)]
1022 api_key: Option<String>,
1023
1024 #[arg(long)]
1026 base_url: Option<String>,
1027
1028 #[arg(long)]
1030 model: Option<String>,
1031 },
1032
1033 Test {
1035 provider: String,
1037 },
1038
1039 Compare,
1041
1042 Info {
1044 model: String,
1046 },
1047}
1048
1049#[derive(Subcommand, Debug, Clone)]
1051pub enum PodsCommands {
1052 Start {
1054 #[arg(long)]
1056 name: String,
1057 #[arg(long)]
1059 model: String,
1060 #[arg(long = "pod-name")]
1062 pod_name: Option<String>,
1063 #[arg(long)]
1065 ssh: Option<String>,
1066 #[arg(long = "gpu", value_name = "ID:NAME", action = ArgAction::Append)]
1068 gpus: Vec<String>,
1069 #[arg(long = "models-path")]
1071 models_path: Option<String>,
1072 #[arg(long)]
1074 profile: Option<String>,
1075 #[arg(long = "gpus")]
1077 gpus_count: Option<usize>,
1078 #[arg(long)]
1080 memory: Option<f32>,
1081 #[arg(long)]
1083 context: Option<String>,
1084 },
1085
1086 Stop {
1088 #[arg(long)]
1090 name: String,
1091 },
1092
1093 StopAll,
1095
1096 List,
1098
1099 Logs {
1101 #[arg(long)]
1103 name: String,
1104 },
1105
1106 KnownModels,
1108}
1109
1110#[derive(Debug, Subcommand, Clone)]
1112pub enum SkillsSubcommand {
1113 #[command(name = "list")]
1115 List {
1116 #[arg(long)]
1118 all: bool,
1119 },
1120
1121 #[command(name = "load")]
1123 Load {
1124 name: String,
1126 #[arg(long)]
1128 path: Option<PathBuf>,
1129 },
1130
1131 #[command(name = "unload")]
1133 Unload {
1134 name: String,
1136 },
1137
1138 #[command(name = "info")]
1140 Info {
1141 name: String,
1143 },
1144
1145 #[command(name = "create")]
1147 Create {
1148 path: PathBuf,
1150 #[arg(long)]
1152 template: Option<String>,
1153 },
1154
1155 #[command(name = "validate")]
1157 Validate {
1158 path: PathBuf,
1160 #[arg(long)]
1162 strict: bool,
1163 },
1164
1165 #[command(name = "check-compatibility")]
1167 CheckCompatibility,
1168
1169 #[command(name = "config")]
1171 Config,
1172
1173 #[command(name = "regenerate-index")]
1175 RegenerateIndex,
1176
1177 #[command(name = "skills-ref", subcommand)]
1179 SkillsRef(SkillsRefSubcommand),
1180}
1181
1182#[derive(Debug, Subcommand, Clone)]
1184pub enum SkillsRefSubcommand {
1185 #[command(name = "validate")]
1187 Validate {
1188 path: PathBuf,
1190 },
1191
1192 #[command(name = "to-prompt")]
1194 ToPrompt {
1195 paths: Vec<PathBuf>,
1197 },
1198
1199 #[command(name = "list")]
1201 List {
1202 path: Option<PathBuf>,
1204 },
1205}
1206
1207#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
1209pub enum ManagedDependency {
1210 #[value(name = "search-tools")]
1211 SearchTools,
1212 #[value(name = "ripgrep")]
1213 Ripgrep,
1214 #[value(name = "ast-grep")]
1215 AstGrep,
1216}
1217
1218#[derive(Debug, Subcommand, Clone)]
1220pub enum DependenciesSubcommand {
1221 #[command(name = "install")]
1223 Install {
1224 dependency: ManagedDependency,
1226 },
1227
1228 #[command(name = "status")]
1230 Status {
1231 dependency: ManagedDependency,
1233 },
1234}
1235
1236#[derive(Debug, Subcommand, Clone, PartialEq, Eq)]
1238pub enum CheckSubcommand {
1239 #[command(name = "ast-grep")]
1241 AstGrep,
1242}
1243
1244#[derive(Debug)]
1246pub struct ConfigFile {
1247 pub model: Option<String>,
1248 pub provider: Option<String>,
1249 pub api_key_env: Option<String>,
1250 pub verbose: Option<bool>,
1251 pub log_level: Option<String>,
1252 pub workspace: Option<PathBuf>,
1253 pub tools: Option<ToolConfig>,
1254 pub context: Option<ContextConfig>,
1255 pub logging: Option<LoggingConfig>,
1256 pub performance: Option<PerformanceConfig>,
1257 pub security: Option<SecurityConfig>,
1258}
1259
1260#[derive(Debug, serde::Deserialize)]
1262pub struct ToolConfig {
1263 pub enable_validation: Option<bool>,
1264 pub max_execution_time_seconds: Option<u64>,
1265 pub allow_file_creation: Option<bool>,
1266 pub allow_file_deletion: Option<bool>,
1267}
1268
1269#[derive(Debug, serde::Deserialize)]
1271pub struct ContextConfig {
1272 pub max_context_length: Option<usize>,
1273}
1274
1275#[derive(Debug, serde::Deserialize)]
1277pub struct LoggingConfig {
1278 pub file_logging: Option<bool>,
1279 pub log_directory: Option<String>,
1280 pub max_log_files: Option<usize>,
1281 pub max_log_size_mb: Option<usize>,
1282}
1283
1284#[cfg(test)]
1285mod exec_command_tests {
1286 use super::{
1287 CheckSubcommand, Cli, Commands, DependenciesSubcommand, ExecSubcommand, ManagedDependency,
1288 PodsCommands,
1289 };
1290 use clap::Parser;
1291 use std::path::PathBuf;
1292
1293 #[test]
1294 fn exec_shorthand_preserves_prompt() {
1295 let cli = Cli::parse_from(["vtcode", "exec", "count files"]);
1296 let Some(Commands::Exec {
1297 command, prompt, ..
1298 }) = cli.command
1299 else {
1300 panic!("expected exec command");
1301 };
1302
1303 assert!(command.is_none());
1304 assert_eq!(prompt.as_deref(), Some("count files"));
1305 }
1306
1307 #[test]
1308 fn exec_resume_parses_specific_session_and_prompt() {
1309 let cli = Cli::parse_from(["vtcode", "exec", "resume", "session-123", "follow up"]);
1310 let Some(Commands::Exec {
1311 command: Some(ExecSubcommand::Resume(resume)),
1312 prompt,
1313 ..
1314 }) = cli.command
1315 else {
1316 panic!("expected exec resume command");
1317 };
1318
1319 assert!(prompt.is_none());
1320 assert!(!resume.last);
1321 assert_eq!(resume.session_or_prompt.as_deref(), Some("session-123"));
1322 assert_eq!(resume.prompt.as_deref(), Some("follow up"));
1323 }
1324
1325 #[test]
1326 fn exec_resume_parses_last_flag() {
1327 let cli = Cli::parse_from(["vtcode", "exec", "resume", "--last", "continue"]);
1328 let Some(Commands::Exec {
1329 command: Some(ExecSubcommand::Resume(resume)),
1330 ..
1331 }) = cli.command
1332 else {
1333 panic!("expected exec resume command");
1334 };
1335
1336 assert!(resume.last);
1337 assert_eq!(resume.session_or_prompt.as_deref(), Some("continue"));
1338 assert!(resume.prompt.is_none());
1339 }
1340
1341 #[test]
1342 fn exec_resume_parses_all_flag() {
1343 let cli = Cli::parse_from(["vtcode", "exec", "resume", "--last", "--all", "continue"]);
1344 let Some(Commands::Exec {
1345 command: Some(ExecSubcommand::Resume(resume)),
1346 ..
1347 }) = cli.command
1348 else {
1349 panic!("expected exec resume command");
1350 };
1351
1352 assert!(resume.last);
1353 assert!(resume.all);
1354 assert_eq!(resume.session_or_prompt.as_deref(), Some("continue"));
1355 }
1356
1357 #[test]
1358 fn exec_resume_allows_last_without_positional_for_stdin_prompt() {
1359 let cli = Cli::parse_from(["vtcode", "exec", "resume", "--last"]);
1360 let Some(Commands::Exec {
1361 command: Some(ExecSubcommand::Resume(resume)),
1362 ..
1363 }) = cli.command
1364 else {
1365 panic!("expected exec resume command");
1366 };
1367
1368 assert!(resume.last);
1369 assert!(resume.session_or_prompt.is_none());
1370 assert!(resume.prompt.is_none());
1371 }
1372
1373 #[test]
1374 fn global_resume_and_continue_parse_all_flag() {
1375 let resume_cli = Cli::parse_from(["vtcode", "--resume", "session-123", "--all"]);
1376 assert_eq!(resume_cli.resume_session.as_deref(), Some("session-123"));
1377 assert!(resume_cli.all);
1378
1379 let continue_cli = Cli::parse_from(["vtcode", "--continue", "--all"]);
1380 assert!(continue_cli.continue_latest);
1381 assert!(continue_cli.all);
1382 }
1383
1384 #[test]
1385 fn global_fork_flags_parse_summarize() {
1386 let cli = Cli::parse_from(["vtcode", "--fork-session", "session-123", "--summarize"]);
1387 assert_eq!(cli.fork_session.as_deref(), Some("session-123"));
1388 assert!(cli.summarize);
1389 }
1390
1391 #[test]
1392 fn notify_parses_title_and_message() {
1393 let cli = Cli::parse_from(["vtcode", "notify", "--title", "VT Code", "Session started"]);
1394 let Some(Commands::Notify { title, message }) = cli.command else {
1395 panic!("expected notify command");
1396 };
1397
1398 assert_eq!(title.as_deref(), Some("VT Code"));
1399 assert_eq!(message, "Session started");
1400 }
1401
1402 #[test]
1403 fn review_defaults_to_current_diff() {
1404 let cli = Cli::parse_from(["vtcode", "review"]);
1405 let Some(Commands::Review(review)) = cli.command else {
1406 panic!("expected review command");
1407 };
1408
1409 assert!(!review.last_diff);
1410 assert!(review.target.is_none());
1411 assert!(review.files.is_empty());
1412 assert!(review.style.is_none());
1413 }
1414
1415 #[test]
1416 fn review_parses_target_and_style_flags() {
1417 let cli = Cli::parse_from([
1418 "vtcode",
1419 "review",
1420 "--target",
1421 "HEAD~1..HEAD",
1422 "--style",
1423 "security",
1424 ]);
1425 let Some(Commands::Review(review)) = cli.command else {
1426 panic!("expected review command");
1427 };
1428
1429 assert_eq!(review.target.as_deref(), Some("HEAD~1..HEAD"));
1430 assert_eq!(review.style.as_deref(), Some("security"));
1431 assert!(!review.last_diff);
1432 }
1433
1434 #[test]
1435 fn review_parses_files() {
1436 let cli = Cli::parse_from([
1437 "vtcode",
1438 "review",
1439 "--file",
1440 "src/main.rs",
1441 "--file",
1442 "src/lib.rs",
1443 ]);
1444 let Some(Commands::Review(review)) = cli.command else {
1445 panic!("expected review command");
1446 };
1447
1448 assert_eq!(review.files.len(), 2);
1449 assert_eq!(review.files[0], PathBuf::from("src/main.rs"));
1450 assert_eq!(review.files[1], PathBuf::from("src/lib.rs"));
1451 }
1452
1453 #[test]
1454 fn dependencies_install_parses_ast_grep() {
1455 let cli = Cli::parse_from(["vtcode", "dependencies", "install", "ast-grep"]);
1456 let Some(Commands::Dependencies(DependenciesSubcommand::Install { dependency })) =
1457 cli.command
1458 else {
1459 panic!("expected dependencies install command");
1460 };
1461
1462 assert_eq!(dependency, ManagedDependency::AstGrep);
1463 }
1464
1465 #[test]
1466 fn dependencies_install_parses_ripgrep() {
1467 let cli = Cli::parse_from(["vtcode", "dependencies", "install", "ripgrep"]);
1468 let Some(Commands::Dependencies(DependenciesSubcommand::Install { dependency })) =
1469 cli.command
1470 else {
1471 panic!("expected dependencies install command");
1472 };
1473
1474 assert_eq!(dependency, ManagedDependency::Ripgrep);
1475 }
1476
1477 #[test]
1478 fn dependencies_install_parses_search_tools() {
1479 let cli = Cli::parse_from(["vtcode", "dependencies", "install", "search-tools"]);
1480 let Some(Commands::Dependencies(DependenciesSubcommand::Install { dependency })) =
1481 cli.command
1482 else {
1483 panic!("expected dependencies install command");
1484 };
1485
1486 assert_eq!(dependency, ManagedDependency::SearchTools);
1487 }
1488
1489 #[test]
1490 fn deps_alias_parses_status_command() {
1491 let cli = Cli::parse_from(["vtcode", "deps", "status", "ast-grep"]);
1492 let Some(Commands::Dependencies(DependenciesSubcommand::Status { dependency })) =
1493 cli.command
1494 else {
1495 panic!("expected deps status command");
1496 };
1497
1498 assert_eq!(dependency, ManagedDependency::AstGrep);
1499 }
1500
1501 #[test]
1502 fn deps_alias_parses_ripgrep_status_command() {
1503 let cli = Cli::parse_from(["vtcode", "deps", "status", "ripgrep"]);
1504 let Some(Commands::Dependencies(DependenciesSubcommand::Status { dependency })) =
1505 cli.command
1506 else {
1507 panic!("expected deps status command");
1508 };
1509
1510 assert_eq!(dependency, ManagedDependency::Ripgrep);
1511 }
1512
1513 #[test]
1514 fn deps_alias_parses_search_tools_status_command() {
1515 let cli = Cli::parse_from(["vtcode", "deps", "status", "search-tools"]);
1516 let Some(Commands::Dependencies(DependenciesSubcommand::Status { dependency })) =
1517 cli.command
1518 else {
1519 panic!("expected deps status command");
1520 };
1521
1522 assert_eq!(dependency, ManagedDependency::SearchTools);
1523 }
1524
1525 #[test]
1526 fn check_parses_ast_grep_subcommand() {
1527 let cli = Cli::parse_from(["vtcode", "check", "ast-grep"]);
1528 let Some(Commands::Check { command }) = cli.command else {
1529 panic!("expected check command");
1530 };
1531
1532 assert_eq!(command, CheckSubcommand::AstGrep);
1533 }
1534
1535 #[test]
1536 fn pods_start_parses_model_and_gpu_flags() {
1537 let cli = Cli::parse_from([
1538 "vtcode",
1539 "pods",
1540 "start",
1541 "--name",
1542 "llama",
1543 "--model",
1544 "meta-llama/Llama-3.1-8B-Instruct",
1545 "--pod-name",
1546 "gpu-box",
1547 "--ssh",
1548 "ssh root@gpu.example.com",
1549 "--gpu",
1550 "0:A100",
1551 "--gpu",
1552 "1:A100",
1553 "--gpus",
1554 "2",
1555 "--memory",
1556 "90",
1557 "--context",
1558 "32k",
1559 ]);
1560 let Some(Commands::Pods {
1561 command:
1562 PodsCommands::Start {
1563 name,
1564 model,
1565 pod_name,
1566 ssh,
1567 gpus,
1568 models_path,
1569 profile,
1570 gpus_count,
1571 memory,
1572 context,
1573 },
1574 }) = cli.command
1575 else {
1576 panic!("expected pods start command");
1577 };
1578
1579 assert_eq!(name, "llama");
1580 assert_eq!(model, "meta-llama/Llama-3.1-8B-Instruct");
1581 assert_eq!(pod_name.as_deref(), Some("gpu-box"));
1582 assert_eq!(ssh.as_deref(), Some("ssh root@gpu.example.com"));
1583 assert_eq!(gpus, vec!["0:A100", "1:A100"]);
1584 assert!(models_path.is_none());
1585 assert!(profile.is_none());
1586 assert_eq!(gpus_count, Some(2));
1587 assert_eq!(memory, Some(90.0));
1588 assert_eq!(context.as_deref(), Some("32k"));
1589 }
1590}
1591
1592#[derive(Debug, serde::Deserialize)]
1594pub struct PerformanceConfig {
1595 pub enabled: Option<bool>,
1596 pub track_token_usage: Option<bool>,
1597 pub track_api_costs: Option<bool>,
1598 pub track_response_times: Option<bool>,
1599 pub enable_benchmarking: Option<bool>,
1600 pub metrics_retention_days: Option<usize>,
1601}
1602
1603#[derive(Debug, serde::Deserialize)]
1605pub struct SecurityConfig {
1606 pub level: Option<String>,
1607 pub enable_audit_logging: Option<bool>,
1608 pub enable_vulnerability_scanning: Option<bool>,
1609 pub allow_external_urls: Option<bool>,
1610 pub max_file_access_depth: Option<usize>,
1611}
1612
1613impl Default for Cli {
1614 fn default() -> Self {
1615 Self {
1616 color: ColorSelection {
1617 color: ColorChoice::Auto,
1618 },
1619 workspace_path: None,
1620 model: Some(ModelId::default().to_string()),
1621 provider: Some("gemini".to_owned()),
1622 api_key_env: "GEMINI_API_KEY".to_owned(),
1623 workspace: None,
1624 research_preview: false,
1625 security_level: "moderate".to_owned(),
1626 show_file_diffs: false,
1627 max_concurrent_ops: 5,
1628 api_rate_limit: 30,
1629 max_tool_calls: 10,
1630 verbose: false,
1631 quiet: false,
1632 config: Vec::new(),
1633 log_level: "info".to_owned(),
1634 no_color: false,
1635 theme: None,
1636 skip_confirmations: false,
1637 codex_experimental: false,
1638 no_codex_experimental: false,
1639 print: None,
1640 full_auto: None,
1641 resume_session: None,
1642 continue_latest: false,
1643 fork_session: None,
1644 all: false,
1645 session_id: None,
1646 summarize: false,
1647 debug: false,
1648 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, permission_mode: None, chrome: false, no_chrome: false, command: Some(Commands::Chat),
1660 }
1661 }
1662}
1663
1664impl Cli {
1665 pub fn get_model(&self) -> String {
1667 self.model
1668 .clone()
1669 .unwrap_or_else(|| ModelId::default().to_string())
1670 }
1671
1672 pub async fn load_config(&self) -> Result<ConfigFile, Box<dyn std::error::Error>> {
1682 use std::path::Path;
1683 use tokio::fs;
1684
1685 let explicit_path = self.config.iter().find_map(|entry| {
1687 let trimmed = entry.trim();
1688 if trimmed.contains('=') || trimmed.is_empty() {
1689 None
1690 } else {
1691 Some(PathBuf::from(trimmed))
1692 }
1693 });
1694
1695 let path = if let Some(p) = explicit_path {
1696 p
1697 } else {
1698 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
1699 let primary = cwd.join("vtcode.toml");
1700 let secondary = cwd.join(".vtcode.toml");
1701 if fs::try_exists(&primary).await.unwrap_or(false) {
1702 primary
1703 } else if fs::try_exists(&secondary).await.unwrap_or(false) {
1704 secondary
1705 } else {
1706 return Ok(ConfigFile {
1708 model: None,
1709 provider: None,
1710 api_key_env: None,
1711 verbose: None,
1712 log_level: None,
1713 workspace: None,
1714 tools: None,
1715 context: None,
1716 logging: None,
1717 performance: None,
1718 security: None,
1719 });
1720 }
1721 };
1722
1723 let text = fs::read_to_string(&path).await?;
1724
1725 let mut cfg = ConfigFile {
1727 model: None,
1728 provider: None,
1729 api_key_env: None,
1730 verbose: None,
1731 log_level: None,
1732 workspace: None,
1733 tools: None,
1734 context: None,
1735 logging: None,
1736 performance: None,
1737 security: None,
1738 };
1739
1740 for raw_line in text.lines() {
1741 let line = raw_line.trim();
1742 if line.is_empty() || line.starts_with('#') || line.starts_with("//") {
1743 continue;
1744 }
1745 let line = match line.find('#') {
1747 Some(idx) => &line[..idx],
1748 None => line,
1749 }
1750 .trim();
1751
1752 let mut parts = line.splitn(2, '=');
1754 let key = parts.next().map(|s| s.trim()).unwrap_or("");
1755 let val = parts.next().map(|s| s.trim()).unwrap_or("");
1756 if key.is_empty() || val.is_empty() {
1757 continue;
1758 }
1759
1760 let unquote = |s: &str| -> String {
1762 let s = s.trim();
1763 if (s.starts_with('"') && s.ends_with('"'))
1764 || (s.starts_with('\'') && s.ends_with('\''))
1765 {
1766 s[1..s.len() - 1].to_owned()
1767 } else {
1768 s.to_owned()
1769 }
1770 };
1771
1772 match key {
1773 "model" => cfg.model = Some(unquote(val)),
1774 "api_key_env" => cfg.api_key_env = Some(unquote(val)),
1775 "verbose" => {
1776 let v = unquote(val).to_lowercase();
1777 cfg.verbose = Some(matches!(v.as_str(), "true" | "1" | "yes"));
1778 }
1779 "log_level" => cfg.log_level = Some(unquote(val)),
1780 "workspace" => {
1781 let v = unquote(val);
1782 let p = if Path::new(&v).is_absolute() {
1783 PathBuf::from(v)
1784 } else {
1785 let base = path.parent().unwrap_or(Path::new("."));
1787 base.join(v)
1788 };
1789 cfg.workspace = Some(p);
1790 }
1791 _ => {
1792 }
1794 }
1795 }
1796
1797 Ok(cfg)
1798 }
1799
1800 pub fn get_workspace(&self) -> PathBuf {
1802 self.workspace
1803 .clone()
1804 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
1805 }
1806
1807 pub fn get_api_key_env(&self) -> String {
1812 crate::config::api_keys::resolve_api_key_env(
1813 self.provider
1814 .as_deref()
1815 .unwrap_or(crate::config::constants::defaults::DEFAULT_PROVIDER),
1816 &self.api_key_env,
1817 )
1818 }
1819
1820 pub fn is_verbose(&self) -> bool {
1822 self.verbose
1823 }
1824
1825 pub fn is_research_preview_enabled(&self) -> bool {
1828 self.research_preview
1829 }
1830
1831 pub fn get_security_level(&self) -> &str {
1833 &self.security_level
1834 }
1835
1836 pub fn is_debug_mode(&self) -> bool {
1838 self.debug || self.verbose
1839 }
1840
1841 pub fn codex_experimental_override(&self) -> Option<bool> {
1842 if self.codex_experimental {
1843 Some(true)
1844 } else if self.no_codex_experimental {
1845 Some(false)
1846 } else {
1847 None
1848 }
1849 }
1850}
1851
1852#[cfg(test)]
1853mod tests {
1854 use super::{Cli, long_version};
1855 use clap::Parser;
1856
1857 #[test]
1858 fn long_version_includes_expected_sections() {
1859 let text = long_version();
1860 assert!(text.contains("Authors:"));
1861 assert!(text.contains("Config directory:"));
1862 assert!(text.contains("Data directory:"));
1863 assert!(text.contains("VTCODE_CONFIG"));
1864 assert!(text.contains("VTCODE_DATA"));
1865 }
1866
1867 #[test]
1868 fn long_version_starts_with_build_git_info() {
1869 let text = long_version();
1870 let expected = option_env!("VT_CODE_GIT_INFO").unwrap_or(env!("CARGO_PKG_VERSION"));
1871 assert!(text.starts_with(expected));
1872 }
1873
1874 #[test]
1875 fn config_file_api_key_env_uses_provider_default() {
1876 let cli = Cli::parse_from(["vtcode", "--provider", "minimax"]);
1877
1878 assert_eq!(cli.get_api_key_env(), "MINIMAX_API_KEY");
1879 }
1880
1881 #[test]
1882 fn config_file_api_key_env_preserves_explicit_override() {
1883 let cli = Cli::parse_from([
1884 "vtcode",
1885 "--provider",
1886 "openai",
1887 "--api-key-env",
1888 "CUSTOM_OPENAI_KEY",
1889 ]);
1890
1891 assert_eq!(cli.get_api_key_env(), "CUSTOM_OPENAI_KEY");
1892 }
1893
1894 #[test]
1895 fn parses_app_server_command_with_stdio_listen_target() {
1896 let cli = Cli::parse_from(["vtcode", "app-server", "--listen", "stdio://"]);
1897
1898 assert!(matches!(
1899 cli.command,
1900 Some(super::Commands::AppServer { ref listen }) if listen == "stdio://"
1901 ));
1902 }
1903
1904 #[test]
1905 fn parses_init_force_flag() {
1906 let cli = Cli::parse_from(["vtcode", "init", "--force"]);
1907
1908 assert!(matches!(
1909 cli.command,
1910 Some(super::Commands::Init { force: true })
1911 ));
1912 }
1913
1914 #[test]
1915 fn parses_codex_login_device_code_flag() {
1916 let cli = Cli::parse_from(["vtcode", "login", "codex", "--device-code"]);
1917
1918 assert!(matches!(
1919 cli.command,
1920 Some(super::Commands::Login {
1921 ref provider,
1922 device_code: true
1923 }) if provider == "codex"
1924 ));
1925 }
1926
1927 #[test]
1928 fn parses_codex_experimental_flags() {
1929 let enabled = Cli::parse_from(["vtcode", "--codex-experimental"]);
1930 assert_eq!(enabled.codex_experimental_override(), Some(true));
1931
1932 let disabled = Cli::parse_from(["vtcode", "--no-codex-experimental"]);
1933 assert_eq!(disabled.codex_experimental_override(), Some(false));
1934 }
1935
1936 #[test]
1937 fn codex_experimental_flags_conflict() {
1938 let result =
1939 Cli::try_parse_from(["vtcode", "--codex-experimental", "--no-codex-experimental"]);
1940
1941 result.unwrap_err();
1942 }
1943
1944 #[test]
1945 fn parses_create_project_feature_flags() {
1946 let cli = Cli::parse_from([
1947 "vtcode",
1948 "create-project",
1949 "demo",
1950 "--feature",
1951 "web",
1952 "--feature",
1953 "db",
1954 ]);
1955
1956 assert!(matches!(
1957 cli.command,
1958 Some(super::Commands::CreateProject { ref name, ref features })
1959 if name == "demo" && features == &vec!["web".to_string(), "db".to_string()]
1960 ));
1961 }
1962
1963 #[test]
1964 fn parses_revert_partial_long_flag() {
1965 let cli = Cli::parse_from(["vtcode", "revert", "--turn", "3", "--partial", "code"]);
1966
1967 assert!(matches!(
1968 cli.command,
1969 Some(super::Commands::Revert {
1970 turn: 3,
1971 partial: Some(ref scope)
1972 }) if scope == "code"
1973 ));
1974 }
1975}