Skip to main content

vtcode_core/cli/args/
mod.rs

1use clap::{ArgAction, ColorChoice, Parser, Subcommand, ValueHint};
2use colorchoice_clap::Color as ColorSelection;
3use std::env;
4use std::path::PathBuf;
5
6use crate::config::models::ModelId;
7
8mod acp;
9mod ask;
10mod background;
11mod bench;
12mod check;
13mod config;
14mod dependencies;
15mod exec;
16mod models;
17mod plugins;
18mod pods;
19mod review;
20mod schedule;
21mod schema;
22mod secret;
23mod session_store;
24mod skills;
25mod webmcp;
26
27pub use acp::AgentClientProtocolTarget;
28pub use ask::{AskCommandOptions, AskOutputFormat};
29pub use background::BackgroundSubagentArgs;
30pub use bench::BenchAllocatorArgs;
31pub use check::CheckSubcommand;
32pub use config::{
33    ConfigCommand, ConfigFile, ConfigResetArgs, ContextConfig, LoggingConfig, PerformanceConfig, SecurityConfig,
34    ToolConfig,
35};
36pub use dependencies::{DependenciesSubcommand, ManagedDependency};
37pub use exec::{EvalOutputFormat, ExecEvalArgs, ExecResumeArgs, ExecSubcommand};
38pub use models::ModelCommands;
39pub use plugins::PluginsSubcommand;
40pub use pods::PodsCommands;
41pub use review::ReviewArgs;
42pub use schedule::{ScheduleCreateArgs, ScheduleSubcommand};
43pub use schema::{SchemaCommands, SchemaMode, SchemaOutputFormat};
44pub use secret::{MigrateArgs, SecretArgs, SecretProvider, SecretSubcommand};
45pub use session_store::SessionStoreCommand;
46pub use skills::{SkillsRefSubcommand, SkillsSubcommand};
47pub use webmcp::{WebmcpCommand, WebmcpPairArgs, WebmcpServeArgs};
48
49#[derive(Parser, Debug, Clone)]
50pub struct Cli {
51    /// Color output selection (auto, always, never)
52    #[command(flatten)]
53    pub color: ColorSelection,
54
55    /// Optional positional path to run vtcode against a different workspace
56    #[arg(
57        value_name = "WORKSPACE",
58        value_hint = ValueHint::DirPath,
59        value_parser = parse_workspace_directory,
60        global = true,
61        last = true
62    )]
63    pub workspace_path: Option<PathBuf>,
64
65    /// LLM Model ID (e.g., gpt-5, claude-sonnet-4-6, gemini-3-flash-preview)
66    #[arg(long, global = true)]
67    pub model: Option<String>,
68
69    /// LLM Provider (gemini, openai, anthropic, deepseek, meta, openrouter, merge-gateway, codex, zai, moonshot, minimax, ollama, lmstudio)
70    #[arg(long, global = true)]
71    pub provider: Option<String>,
72
73    /// API key environment variable (auto-detects GEMINI_API_KEY, OPENAI_API_KEY, etc.)
74    #[arg(long, global = true, default_value = crate::config::constants::defaults::DEFAULT_API_KEY_ENV)]
75    pub api_key_env: String,
76
77    /// Workspace root directory (default: current directory)
78    #[arg(
79        long,
80        global = true,
81        alias = "workspace-dir",
82        value_name = "PATH",
83        value_hint = ValueHint::DirPath,
84        value_parser = parse_workspace_directory
85    )]
86    pub workspace: Option<PathBuf>,
87
88    /// Enable research-preview features
89    #[arg(long, global = true)]
90    pub research_preview: bool,
91
92    /// Security level for tool execution (strict, moderate, permissive)
93    #[arg(long, global = true, default_value = "moderate")]
94    pub security_level: String,
95
96    /// Show diffs for file changes in chat interface
97    #[arg(long, global = true)]
98    pub show_file_diffs: bool,
99
100    /// Maximum concurrent async operations
101    #[arg(long, global = true, default_value_t = 5)]
102    pub max_concurrent_ops: usize,
103
104    /// Maximum API requests per minute
105    #[arg(long, global = true, default_value_t = 30)]
106    pub api_rate_limit: usize,
107
108    /// Maximum tool calls per session
109    #[arg(long, global = true, default_value_t = 10)]
110    pub max_tool_calls: usize,
111
112    /// Enable debug output for troubleshooting
113    #[arg(long, global = true)]
114    pub debug: bool,
115
116    /// Enable verbose logging
117    #[arg(long, global = true)]
118    pub verbose: bool,
119
120    /// Suppress all non-essential output (for scripting, CI/CD)
121    #[arg(short, long, global = true)]
122    pub quiet: bool,
123
124    /// Configuration overrides or file path (KEY=VALUE or PATH)
125    #[arg(
126        short = 'c',
127        long = "config",
128        value_name = "KEY=VALUE|PATH",
129        action = ArgAction::Append,
130        global = true
131    )]
132    pub config: Vec<String>,
133
134    /// Log level (error, warn, info, debug, trace)
135    #[arg(long, global = true, default_value = "info")]
136    pub log_level: String,
137
138    /// Disable color output (equivalent to `--color never`)
139    #[arg(long, global = true)]
140    pub no_color: bool,
141
142    /// Select UI theme (e.g., ciapre-dark, ciapre-blue)
143    #[arg(long, global = true, value_name = "THEME")]
144    pub theme: Option<String>,
145
146    /// App tick rate in milliseconds (default: 250)
147    #[arg(short = 't', long, default_value_t = 250)]
148    pub tick_rate: u64,
149
150    /// Frame rate in FPS (default: 60)
151    #[arg(short = 'f', long, default_value_t = 60)]
152    pub frame_rate: u64,
153
154    /// Enable skills system
155    #[arg(long, global = true)]
156    pub enable_skills: bool,
157
158    /// Enable Chrome browser integration for web automation
159    #[arg(long, global = true)]
160    pub chrome: bool,
161
162    /// Disable Chrome browser integration
163    #[arg(long = "no-chrome", global = true, conflicts_with = "chrome")]
164    pub no_chrome: bool,
165
166    /// Skip safety confirmations (use with caution)
167    #[arg(long, global = true)]
168    pub skip_confirmations: bool,
169
170    /// Enable experimental Codex app-server features for this run
171    #[arg(long = "codex-experimental", global = true, conflicts_with = "no_codex_experimental")]
172    pub codex_experimental: bool,
173
174    /// Disable experimental Codex app-server features for this run
175    #[arg(long = "no-codex-experimental", global = true, conflicts_with = "codex_experimental")]
176    pub no_codex_experimental: bool,
177
178    /// Print response without launching the interactive TUI
179    #[arg(
180        short = 'p',
181        long = "print",
182        value_name = "PROMPT",
183        value_hint = ValueHint::Other,
184        num_args = 0..=1,
185        default_missing_value = "",
186        global = true,
187        conflicts_with_all = ["full_auto"]
188    )]
189    pub print: Option<String>,
190
191    /// Run non-interactively with full-auto permission review
192    #[arg(
193        long = "full-auto",
194        global = true,
195        help = "Run non-interactively with full-auto permission review",
196        long_help = r#"Run non-interactively on top of the active primary agent.
197
198If no primary agent is explicitly selected or configured, VT Code selects the effective `auto` primary agent. Explicit choices, including `duck`, are honoured. Full-auto is an execution and permission layer, not a primary agent.
199
200Full-auto does not override explicit denies or grant tools outside `[automation.full_auto].allowed_tools`. Tools outside that allow-list are denied. Promptable actions inside the allow-list are routed through automatic permission review after deny and policy checks instead of asking. The run fails fast if it needs the defaulted `auto` primary agent and no effective `auto` exists."#,
201        value_name = "PROMPT",
202        num_args = 0..=1,
203        default_missing_value = "",
204        value_hint = ValueHint::Other
205    )]
206    pub full_auto: Option<String>,
207
208    /// Resume a previous conversation (use without ID for interactive picker)
209    #[arg(
210        short = 'r',
211        long = "resume",
212        global = true,
213        value_name = "SESSION_ID",
214        num_args = 0..=1,
215        default_missing_value = "__interactive__",
216        conflicts_with_all = ["continue_latest", "full_auto"]
217    )]
218    pub resume_session: Option<String>,
219
220    /// Continue the most recent conversation automatically
221    #[arg(
222        long = "continue",
223        visible_alias = "continue-session",
224        global = true,
225        conflicts_with_all = ["resume_session", "full_auto"]
226    )]
227    pub continue_latest: bool,
228
229    /// Fork an existing session with a new session ID
230    #[arg(
231        long = "fork-session",
232        global = true,
233        value_name = "SESSION_ID",
234        conflicts_with_all = ["resume_session", "continue_latest", "full_auto"]
235    )]
236    pub fork_session: Option<String>,
237
238    /// Show archived sessions from every workspace when resuming or forking
239    #[arg(long, global = true)]
240    pub all: bool,
241
242    /// Custom suffix for session identifier (alphanumeric, dash, underscore only, max 64 chars)
243    #[arg(long = "session-id", global = true, value_name = "CUSTOM_SUFFIX")]
244    pub session_id: Option<String>,
245
246    /// Use summarized history when forking a session
247    #[arg(long, global = true)]
248    pub summarize: bool,
249
250    /// Override the default agent model for this session
251    #[arg(long, global = true, value_name = "AGENT")]
252    pub agent: Option<String>,
253
254    /// Tools that execute without prompting (comma-separated, supports patterns like "Bash(git:*)")
255    #[arg(long = "allowed-tools", global = true, value_name = "TOOLS", action = ArgAction::Append)]
256    pub allowed_tools: Vec<String>,
257
258    /// Tools that cannot be used by the agent
259    #[arg(long = "disallowed-tools", global = true, value_name = "TOOLS", action = ArgAction::Append)]
260    pub disallowed_tools: Vec<String>,
261
262    /// Auto-approve promptable actions while respecting denies and policy blocks
263    #[arg(
264        long = "dangerously-skip-permissions",
265        global = true,
266        help = "Auto-approve promptable actions while respecting denies and policy blocks",
267        long_help = "Auto-approve promptable actions while still respecting explicit denies and policy blocks."
268    )]
269    pub dangerously_skip_permissions: bool,
270
271    /// Explicitly connect to IDE on startup (auto-detects available IDEs)
272    #[arg(long, global = true)]
273    pub ide: bool,
274
275    #[command(subcommand)]
276    pub command: Option<Commands>,
277}
278
279#[derive(Subcommand, Debug, Clone)]
280pub enum Commands {
281    /// Start Agent Client Protocol bridge for IDE integrations
282    #[command(name = "acp")]
283    AgentClientProtocol {
284        /// Client to connect over ACP
285        #[arg(value_enum, default_value_t = AgentClientProtocolTarget::Zed)]
286        target: AgentClientProtocolTarget,
287    },
288
289    /// Unified per-session state store (single source of truth for state,
290    /// context, and history). Consolidates the legacy `checkpoints/`, `logs/`,
291    /// and `history/` stores.
292    #[command(name = "session-store")]
293    SessionStore {
294        #[command(subcommand)]
295        command: SessionStoreCommand,
296    },
297
298    /// Interactive AI coding assistant
299    Chat,
300
301    /// Resume the most recent conversation automatically
302    ///
303    /// Equivalent to `vtcode --continue`. Loads the latest archived session in
304    /// the current workspace (or across all workspaces with `--all`) and resumes
305    /// it without showing the interactive picker.
306    ///
307    /// Examples:
308    ///   vtcode continue
309    ///   vtcode continue --all
310    ///   vtcode continue --session-id my-fork   # fork latest into a new session
311    Continue,
312
313    /// Single prompt mode - prints model reply without tools
314    ///
315    /// Send a single prompt to the model and print the response. No tools are
316    /// invoked, no session is created, and the process exits after replying.
317    ///
318    /// Examples:
319    ///   vtcode ask "what is a monad?"
320    ///   echo "summarize this" | vtcode ask
321    ///   vtcode ask --output-format json "explain ownership in Rust"
322    Ask {
323        /// Prompt to ask. Use `-` to force reading from stdin.
324        #[arg(
325            value_name = "PROMPT",
326            long_help = "The prompt to send to the model.\n\nOmit to read from stdin (piped input).\nUse '-' to explicitly force reading from stdin."
327        )]
328        prompt: Option<String>,
329        /// Format the response using a structured representation.
330        #[arg(
331            long = "output-format",
332            value_enum,
333            value_name = "FORMAT",
334            long_help = "Output format for the response.\n\nCurrently supports:\n  json - Emit the response as a structured JSON document."
335        )]
336        output_format: Option<AskOutputFormat>,
337    },
338    /// Headless execution mode
339    ///
340    /// Run the agent in non-interactive mode. The agent executes the prompt,
341    /// runs tools, and exits when done. Ideal for CI/CD, scripting, and
342    /// agent-to-agent workflows.
343    ///
344    /// Examples:
345    ///   vtcode exec "explain this codebase"
346    ///   vtcode exec --json "fix the failing test"
347    ///   vtcode exec --dry-run "refactor auth module"
348    ///   cat file.rs | vtcode exec "review this code"
349    ///   vtcode exec resume --last
350    Exec {
351        /// Emit structured JSON events to stdout (one per line)
352        #[arg(
353            long,
354            long_help = "Stream newline-delimited JSON events to stdout.\nEach line is a JSON object representing an agent event (tool call, message, etc.).\nUseful for programmatic consumption and CI integration."
355        )]
356        json: bool,
357        /// Run a read-only dry-run execution (blocks mutating tool calls)
358        #[arg(
359            long,
360            long_help = "Simulate execution without making changes.\nThe agent plans tool calls but does not execute mutating operations (file writes, shell commands).\nUseful for previewing what the agent would do."
361        )]
362        dry_run: bool,
363        /// Optional path to write the JSONL transcript
364        #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath, long_help = "Write the full JSONL event transcript to this file.\nIncludes all agent events: tool calls, messages, errors, and metadata.")]
365        events: Option<PathBuf>,
366        /// Write the last agent message to this file
367        #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath, long_help = "Write only the final agent message to this file.\nUseful for piping the agent's response into other tools.")]
368        last_message_file: Option<PathBuf>,
369        /// Optional exec subcommand
370        #[command(subcommand)]
371        command: Option<ExecSubcommand>,
372        /// Prompt to execute. Use `-` to force reading from stdin.
373        #[arg(
374            value_name = "PROMPT",
375            long_help = "The prompt to execute.\n\nOmit to read from stdin (piped input).\nUse '-' to explicitly force reading from stdin.\nQuote multi-word prompts: vtcode exec \"fix the bug in auth.rs\""
376        )]
377        prompt: Option<String>,
378    },
379    /// Run an evaluation suite (shorthand for `vtcode exec eval`)
380    ///
381    /// Executes each task in the suite autonomously in an isolated worktree,
382    /// verifies the outcome with environment probes, and aggregates pass@k /
383    /// pass^k metrics into a markdown or JSON report.
384    ///
385    /// Examples:
386    ///   vtcode eval --suite suite.json
387    ///   vtcode eval --suite suite.json --format json --output report.json
388    Eval(ExecEvalArgs),
389    /// Manage durable scheduled tasks
390    ///
391    /// Create, list, and delete scheduled tasks that run on a recurring or
392    /// one-shot basis. Tasks are stored persistently and survive restarts
393    /// when paired with `vtcode schedule install-service`.
394    ///
395    /// Examples:
396    ///   vtcode schedule create --name "daily-review" --cron "0 9 * * 1-5" --prompt "review recent changes"
397    ///   vtcode schedule create --name "reminder" --reminder "standup in 10 minutes" --at "09:50"
398    ///   vtcode schedule list
399    ///   vtcode schedule delete `<task-id>`
400    Schedule {
401        #[command(subcommand)]
402        command: ScheduleSubcommand,
403    },
404
405    /// Internal VT Code background subagent runner
406    #[command(name = "background-subagent", hide = true)]
407    BackgroundSubagent(BackgroundSubagentArgs),
408
409    /// Headless code review for the current diff, selected files, or a custom git target
410    #[command(
411        long_about = "Run a non-interactive code review.\n\nExamples:\n  vtcode review\n  vtcode review \"Review the full diff for correctness and regressions\"\n  vtcode review --last-diff\n  vtcode review --target HEAD~1..HEAD\n  vtcode review --file src/main.rs --file crates/codegen/vtcode-core/src/lib.rs\n  vtcode review --style security \"Focus on auth bypass and injection\""
412    )]
413    Review(ReviewArgs),
414
415    /// Runtime schema introspection for built-in tools
416    Schema {
417        #[command(subcommand)]
418        command: SchemaCommands,
419    },
420
421    /// Verbose interactive chat with debug output
422    ChatVerbose,
423
424    /// Analyze workspace (structure, security, performance)
425    Analyze {
426        /// Type of analysis to perform
427        #[arg(value_name = "TYPE", default_value = "full")]
428        analysis_type: String,
429    },
430
431    /// Pretty-print trajectory logs
432    #[command(name = "trajectory")]
433    Trajectory {
434        /// Optional path to trajectory JSONL file
435        #[arg(long)]
436        file: Option<PathBuf>,
437        /// Number of top entries to show
438        #[arg(long, default_value_t = 10)]
439        top: usize,
440    },
441
442    /// Send a VT Code notification using the built-in notification system
443    Notify {
444        /// Optional notification title
445        #[arg(long, value_name = "TITLE")]
446        title: Option<String>,
447        /// Notification message
448        #[arg(value_name = "MESSAGE")]
449        message: String,
450    },
451
452    /// Benchmark against SWE-bench evaluation framework
453    Benchmark {
454        /// Path to a JSON benchmark specification
455        #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
456        task_file: Option<PathBuf>,
457        /// Inline JSON specification for quick experiments
458        #[arg(long, value_name = "JSON")]
459        task: Option<String>,
460        /// Optional path to write the structured benchmark report
461        #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
462        output: Option<PathBuf>,
463        /// Limit the number of tasks executed
464        #[arg(long, value_name = "COUNT")]
465        max_tasks: Option<usize>,
466    },
467
468    /// Measure allocator RSS behavior under a bursty/sparse Tokio workload
469    ///
470    /// Reproduces the mimalloc-vs-jemalloc analysis pattern: many short-lived
471    /// tasks allocated across Tokio worker threads, with idle gaps between
472    /// bursts. Reports the RSS trajectory so you can see whether the global
473    /// allocator returns memory to the OS (jemalloc) or pins it (mimalloc/glibc).
474    /// Build with `--features allocator-jemalloc` to compare allocators.
475    #[command(name = "bench-allocator")]
476    BenchAllocator(BenchAllocatorArgs),
477
478    /// Create complete Rust project
479    CreateProject {
480        name: String,
481        #[arg(long = "feature", value_name = "FEATURE", action = ArgAction::Append)]
482        features: Vec<String>,
483    },
484
485    /// Revert agent to a previous snapshot
486    Revert {
487        /// Turn number to revert to
488        #[arg(short, long)]
489        turn: usize,
490        /// Scope of revert operation: conversation, code, full
491        #[arg(long)]
492        partial: Option<String>,
493    },
494
495    /// List all available snapshots
496    Snapshots,
497
498    /// Clean up old snapshots
499    ///
500    /// Features:
501    ///   • Remove snapshots beyond limit
502    ///   • Configurable retention policy
503    ///   • Safe deletion with confirmation
504    ///
505    /// Examples:
506    ///   vtcode cleanup-snapshots
507    ///   vtcode cleanup-snapshots --max 20
508    #[command(name = "cleanup-snapshots")]
509    CleanupSnapshots {
510        /// Maximum number of snapshots to keep
511        ///
512        /// Default: 50
513        /// Example: --max 20
514        #[arg(short, long, default_value_t = 50)]
515        max: usize,
516    },
517
518    /// Initialize project guidance and workspace scaffolding
519    ///
520    /// Bootstrap a workspace for use with VT Code. Creates vtcode.toml,
521    /// AGENTS.md, and other scaffolding. Run this once per project.
522    ///
523    /// Examples:
524    ///   vtcode init
525    ///   vtcode init --force
526    Init {
527        /// Overwrite an existing AGENTS.md without prompting
528        #[arg(
529            long,
530            short = 'f',
531            long_help = "Overwrite AGENTS.md without confirmation.\nUse this in CI/CD or scripts where interactive prompts are not possible."
532        )]
533        force: bool,
534    },
535
536    /// Initialize project in the user state directory's `projects/` path.
537    ///
538    /// Create a new project entry in the VT Code projects directory.
539    /// This is separate from `vtcode init` which bootstraps a workspace.
540    ///
541    /// Examples:
542    ///   vtcode init-project
543    ///   vtcode init-project --name my-project
544    ///   vtcode init-project --force --migrate
545    #[command(name = "init-project")]
546    InitProject {
547        /// Project name - defaults to current directory name
548        #[arg(
549            long,
550            long_help = "Name for the project.\nDefaults to the current directory name if not specified."
551        )]
552        name: Option<String>,
553        /// Force initialization - overwrite existing project structure
554        #[arg(long, long_help = "Overwrite existing project structure without confirmation.")]
555        force: bool,
556        /// Migrate existing files - move existing config/cache files to new structure
557        #[arg(
558            long,
559            long_help = "Move existing config and cache files into the new project structure."
560        )]
561        migrate: bool,
562    },
563
564    /// Generate or manage configuration files
565    ///
566    /// Create a vtcode.toml configuration file with default settings, or use
567    /// `reset` to clear one configuration layer.
568    /// Use --global to create in the canonical user config directory or specify an output path.
569    ///
570    /// Examples:
571    ///   vtcode config
572    ///   vtcode config --global
573    ///   vtcode config --output ./my-vtcode.toml
574    ///   vtcode config reset
575    ///   vtcode config reset --global
576    ///   vtcode config reset --project
577    Config {
578        #[command(subcommand)]
579        command: Option<ConfigCommand>,
580        /// Output file path
581        #[arg(
582            long,
583            long_help = "Write the configuration to this path.\nDefaults to ./vtcode.toml in the current directory."
584        )]
585        output: Option<PathBuf>,
586        /// Create in the canonical user config directory.
587        #[arg(
588            long,
589            long_help = "Write the configuration to the canonical user config directory.\nThis sets global defaults for all workspaces."
590        )]
591        global: bool,
592    },
593
594    /// Authenticate with a supported provider
595    ///
596    /// Start an OAuth or API-key login flow for the given provider.
597    /// Credentials are stored securely in the OS keychain.
598    ///
599    /// Examples:
600    ///   vtcode login openai
601    ///   vtcode login openai --from-codex
602    ///   vtcode login openrouter
603    ///   vtcode login codex
604    ///   vtcode login codex --device-code
605    Login {
606        /// Provider name (`openai`, `openrouter`, `copilot`, or `codex`)
607        #[arg(long_help = "The provider to authenticate with.\nSupported: openai, openrouter, copilot, codex")]
608        provider: String,
609        /// Use device-code login when the provider supports it (currently `codex` only)
610        #[arg(
611            long,
612            default_value_t = false,
613            long_help = "Use the device-code OAuth flow.\nCurrently supported only for the `codex` provider.\nOpens a browser URL and asks you to enter a code."
614        )]
615        device_code: bool,
616        /// Import ChatGPT credentials from Codex's `~/.codex/auth.json` instead
617        /// of running the browser OAuth flow (currently `openai` only)
618        #[arg(
619            long,
620            default_value_t = false,
621            long_help = "Import ChatGPT OAuth tokens from Codex's auth.json.\n\
622                         Requires that you have already run `codex login`.\n\
623                         Currently supported only for the `openai` provider."
624        )]
625        from_codex: bool,
626    },
627
628    /// Clear stored authentication credentials for a provider
629    ///
630    /// Remove stored OAuth tokens or API keys for the given provider.
631    ///
632    /// Examples:
633    ///   vtcode logout openai
634    ///   vtcode logout openrouter
635    Logout {
636        /// Provider name (`openai`, `openrouter`, `copilot`, or `codex`)
637        #[arg(long_help = "The provider to deauthenticate.\nSupported: openai, openrouter, copilot, codex")]
638        provider: String,
639    },
640
641    /// Show authentication status for one provider or all supported providers
642    ///
643    /// Display whether each provider is authenticated, which credential type
644    /// is in use, and token/session metadata when available.
645    ///
646    /// Examples:
647    ///   vtcode auth
648    ///   vtcode auth openai
649    ///   vtcode auth openrouter
650    Auth {
651        /// Optional provider name (`openai`, `openrouter`, `copilot`, or `codex`)
652        #[arg(long_help = "Show status for a single provider.\nOmit to show status for all supported providers.")]
653        provider: Option<String>,
654    },
655
656    /// Manage tool execution policies
657    #[command(name = "tool-policy")]
658    ToolPolicy {
659        #[command(subcommand)]
660        command: crate::cli::tool_policy_commands::ToolPolicyCommands,
661    },
662
663    /// Manage Model Context Protocol providers
664    #[command(name = "mcp")]
665    Mcp {
666        #[command(subcommand)]
667        command: crate::mcp::cli::McpCommands,
668    },
669
670    /// Agent2Agent (A2A) Protocol
671    #[command(name = "a2a")]
672    A2a {
673        #[command(subcommand)]
674        command: super::super::a2a::cli::A2aCommands,
675    },
676
677    /// Authenticated browser editor bridge
678    #[command(name = "webmcp")]
679    Webmcp {
680        #[command(subcommand)]
681        command: WebmcpCommand,
682    },
683
684    /// Proxy to the official Codex app-server
685    #[command(name = "app-server")]
686    AppServer {
687        /// Transport listen target passed through to `codex app-server`
688        #[arg(long, default_value = "stdio://")]
689        listen: String,
690    },
691
692    /// Manage models and providers
693    Models {
694        #[command(subcommand)]
695        command: ModelCommands,
696    },
697
698    /// Manage GPU pod deployments
699    #[command(name = "pods")]
700    Pods {
701        #[command(subcommand)]
702        command: PodsCommands,
703    },
704
705    /// Generate or display man pages
706    Man {
707        /// Command name to generate man page for (optional)
708        command: Option<String>,
709        /// Output file path to save man page
710        #[arg(short, long)]
711        output: Option<PathBuf>,
712    },
713
714    /// Manage Agent Skills
715    ///
716    /// Skills are reusable instruction sets that extend the agent's capabilities.
717    /// Each skill is a directory containing a SKILL.md manifest and optional scripts.
718    ///
719    /// Examples:
720    ///   vtcode skills list
721    ///   vtcode skills create my-skill
722    ///   vtcode skills load my-skill
723    ///   vtcode skills info my-skill
724    ///   vtcode skills validate ./path/to/skill
725    #[command(subcommand)]
726    Skills(SkillsSubcommand),
727
728    /// Manage Agent Plugins
729    ///
730    /// Agent Plugins are portable packages that bundle Agent Skills and MCP servers
731    /// under a root plugin.json manifest, following the Agent Plugins spec.
732    ///
733    /// Examples:
734    ///   vtcode plugins list
735    ///   vtcode plugins info my-plugin
736    ///   vtcode plugins validate ./path/to/plugin
737    ///   vtcode plugins add <https://github.com/example/plugin>
738    ///   vtcode plugins remove my-plugin
739    #[command(subcommand)]
740    Plugins(PluginsSubcommand),
741
742    /// List available skills (alias for `vtcode skills list`)
743    #[command(name = "list-skills", hide = true)]
744    ListSkills {},
745
746    /// Manage optional VT Code dependencies
747    ///
748    /// Install, update, or check the status of optional tools that VT Code
749    /// can use (ripgrep, ast-grep, search-tools bundle).
750    ///
751    /// Examples:
752    ///   vtcode dependencies status
753    ///   vtcode dependencies install search-tools
754    ///   vtcode deps install ripgrep
755    #[command(name = "dependencies", visible_alias = "deps", subcommand)]
756    Dependencies(DependenciesSubcommand),
757
758    /// Manage API keys in secure storage (OS keyring or encrypted file)
759    ///
760    /// Store, inspect, and delete provider API keys without exposing them
761    /// in shell history or workspace files.
762    ///
763    /// Examples:
764    ///   vtcode secret
765    ///   vtcode secret list
766    ///   vtcode secret status openai
767    ///   vtcode secret add openai
768    ///   vtcode secret delete openai
769    Secret(SecretArgs),
770
771    /// Run built-in repository checks
772    ///
773    /// Execute repository-level checks such as ast-grep rule tests and scans.
774    ///
775    /// Examples:
776    ///   vtcode check ast-grep
777    Check {
778        #[command(subcommand)]
779        command: CheckSubcommand,
780    },
781
782    /// Check for and install binary updates from GitHub Releases
783    ///
784    /// Manage VT Code binary updates. By default checks for a new version
785    /// and offers to install it. Use flags to customize behavior.
786    ///
787    /// Examples:
788    ///   vtcode update
789    ///   vtcode update --check
790    ///   vtcode update --force
791    ///   vtcode update --list
792    ///   vtcode update --pin 0.120.0
793    ///   vtcode update --unpin
794    #[command(name = "update")]
795    Update {
796        /// Check for updates without installing
797        #[arg(
798            long,
799            long_help = "Check whether a newer version is available without installing it."
800        )]
801        check: bool,
802        /// Force update even if on latest version
803        #[arg(
804            long,
805            long_help = "Reinstall or downgrade even if the current version is already the latest."
806        )]
807        force: bool,
808        /// List available versions
809        #[arg(long, long_help = "Print available release versions from GitHub and exit.")]
810        list: bool,
811        /// Number of versions to list (default: 10)
812        #[arg(
813            long,
814            default_value = "10",
815            long_help = "Maximum number of versions to display with --list."
816        )]
817        limit: usize,
818        /// Pin to a specific version
819        #[arg(
820            long,
821            value_name = "VERSION",
822            long_help = "Pin the binary to a specific version.\nAuto-updates are disabled until --unpin is used."
823        )]
824        pin: Option<String>,
825        /// Unpin version
826        #[arg(long, long_help = "Remove a previously set version pin and resume auto-updates.")]
827        unpin: bool,
828        /// Set release channel (stable, beta, nightly)
829        #[arg(
830            long,
831            value_name = "CHANNEL",
832            long_help = "Switch the release channel.\nAccepted values: stable, beta, nightly."
833        )]
834        channel: Option<String>,
835        /// Show current update configuration
836        #[arg(
837            long,
838            long_help = "Display the current update configuration (channel, pin, intervals) and exit."
839        )]
840        show_config: bool,
841    },
842
843    /// Start Anthropic API compatibility server
844    #[command(name = "anthropic-api")]
845    AnthropicApi {
846        /// Port to run the server on
847        #[arg(long, default_value = "11434")]
848        port: u16,
849        /// Host address to bind to
850        #[arg(long, default_value = "127.0.0.1")]
851        host: String,
852    },
853}
854
855impl Default for Cli {
856    fn default() -> Self {
857        Self {
858            color: ColorSelection { color: ColorChoice::Auto },
859            workspace_path: None,
860            model: Some(ModelId::default().to_string()),
861            provider: Some("gemini".to_owned()),
862            api_key_env: "GEMINI_API_KEY".to_owned(),
863            workspace: None,
864            research_preview: false,
865            security_level: "moderate".to_owned(),
866            show_file_diffs: false,
867            max_concurrent_ops: 5,
868            api_rate_limit: 30,
869            max_tool_calls: 10,
870            verbose: false,
871            quiet: false,
872            config: Vec::new(),
873            log_level: "info".to_owned(),
874            no_color: false,
875            theme: None,
876            skip_confirmations: false,
877            codex_experimental: false,
878            no_codex_experimental: false,
879            print: None,
880            full_auto: None,
881            resume_session: None,
882            continue_latest: false,
883            fork_session: None,
884            all: false,
885            session_id: None,
886            summarize: false,
887            debug: false,
888            enable_skills: false,
889            tick_rate: 250,
890            frame_rate: 60,
891            agent: None,
892            allowed_tools: Vec::new(),
893            disallowed_tools: Vec::new(),
894            dangerously_skip_permissions: false,
895            ide: false,
896            chrome: false,
897            no_chrome: false,
898            command: Some(Commands::Chat),
899        }
900    }
901}
902
903impl Cli {
904    /// Get the effective API key environment variable
905    ///
906    /// Automatically infers the API key environment variable based on the provider
907    /// when the current value matches the default or is not explicitly set.
908    pub fn get_api_key_env(&self) -> String {
909        crate::config::api_keys::resolve_api_key_env(
910            self.provider
911                .as_deref()
912                .unwrap_or(crate::config::constants::defaults::DEFAULT_PROVIDER),
913            &self.api_key_env,
914        )
915    }
916
917    pub fn codex_experimental_override(&self) -> Option<bool> {
918        if self.codex_experimental {
919            Some(true)
920        } else if self.no_codex_experimental {
921            Some(false)
922        } else {
923            None
924        }
925    }
926}
927
928fn parse_workspace_directory(raw: &str) -> Result<PathBuf, String> {
929    let candidate = PathBuf::from(raw);
930    if !candidate.exists() {
931        return Err(format!(
932            "'{}' is not a valid workspace path or subcommand.\n\
933             Run `vtcode --help` to see available commands and options.",
934            raw
935        ));
936    }
937
938    Ok(candidate)
939}
940
941pub fn long_version() -> String {
942    let git_info = option_env!("VT_CODE_GIT_INFO").unwrap_or(env!("CARGO_PKG_VERSION"));
943    let path_diagnostics = match vtcode_commons::VtCodePaths::resolve() {
944        Ok(paths) => format!(
945            "Config directory: {}\nData directory: {}\nState directory: {}\nCache directory: {}\nRuntime directory: {}\nExecutable directory: {}\nLegacy directory: {}\nMigration marker: {}\nMigration report: {}",
946            paths.config_dir().display(),
947            paths.data_dir().display(),
948            paths.state_dir().display(),
949            paths.cache_dir().display(),
950            paths.runtime_dir().display(),
951            paths.executable_dir().display(),
952            paths.legacy_dir().display(),
953            paths.migration_marker_path().display(),
954            paths.migration_report_path().display(),
955        ),
956        Err(error) => format!("Storage paths: unavailable ({error:#})"),
957    };
958    let environment = [
959        "HOME",
960        "VTCODE_HOME",
961        "VTCODE_CONFIG",
962        "VTCODE_CONFIG_PATH",
963        "VTCODE_DATA",
964        "CODEX_HOME",
965        "XDG_CONFIG_HOME",
966        "XDG_DATA_HOME",
967        "XDG_STATE_HOME",
968        "XDG_CACHE_HOME",
969        "XDG_RUNTIME_DIR",
970        "XDG_BIN_HOME",
971        "XDG_CONFIG_DIRS",
972        "XDG_DATA_DIRS",
973    ]
974    .iter()
975    .map(|name| {
976        let value = env::var_os(name)
977            .map(|value| value.to_string_lossy().into_owned())
978            .unwrap_or_else(|| "<not set>".to_string());
979        format!("  {name}={value}")
980    })
981    .collect::<Vec<_>>()
982    .join("\n");
983
984    format!(
985        "{}\n\nAuthors: {}\n{}\n\nEnvironment variables:\n{}",
986        git_info,
987        env!("CARGO_PKG_AUTHORS"),
988        path_diagnostics,
989        environment,
990    )
991}
992
993#[cfg(test)]
994mod tests {
995    use super::*;
996    use clap::Parser;
997
998    #[test]
999    fn long_version_includes_expected_sections() {
1000        let text = long_version();
1001        assert!(text.contains("Authors:"));
1002        assert!(text.contains("Config directory:"));
1003        assert!(text.contains("Data directory:"));
1004        assert!(text.contains("State directory:"));
1005        assert!(text.contains("Cache directory:"));
1006        assert!(text.contains("Runtime directory:"));
1007        assert!(text.contains("Executable directory:"));
1008        assert!(text.contains("Legacy directory:"));
1009        assert!(text.contains("Migration marker:"));
1010        assert!(text.contains("Migration report:"));
1011        assert!(text.contains("VTCODE_CONFIG"));
1012        assert!(text.contains("VTCODE_DATA"));
1013    }
1014
1015    #[test]
1016    fn long_version_starts_with_build_git_info() {
1017        let text = long_version();
1018        let expected = option_env!("VT_CODE_GIT_INFO").unwrap_or(env!("CARGO_PKG_VERSION"));
1019        assert!(text.starts_with(expected));
1020    }
1021
1022    #[test]
1023    fn config_file_api_key_env_uses_provider_default() {
1024        let cli = Cli::parse_from(["vtcode", "--provider", "minimax"]);
1025
1026        assert_eq!(cli.get_api_key_env(), "MINIMAX_API_KEY");
1027    }
1028
1029    #[test]
1030    fn config_file_api_key_env_preserves_explicit_override() {
1031        let cli = Cli::parse_from(["vtcode", "--provider", "openai", "--api-key-env", "CUSTOM_OPENAI_KEY"]);
1032
1033        assert_eq!(cli.get_api_key_env(), "CUSTOM_OPENAI_KEY");
1034    }
1035
1036    #[test]
1037    fn parses_app_server_command_with_stdio_listen_target() {
1038        let cli = Cli::parse_from(["vtcode", "app-server", "--listen", "stdio://"]);
1039
1040        assert!(matches!(
1041            cli.command,
1042            Some(Commands::AppServer { ref listen }) if listen == "stdio://"
1043        ));
1044    }
1045
1046    #[test]
1047    fn parses_init_force_flag() {
1048        let cli = Cli::parse_from(["vtcode", "init", "--force"]);
1049
1050        assert!(matches!(cli.command, Some(Commands::Init { force: true })));
1051    }
1052
1053    #[test]
1054    fn parses_codex_login_device_code_flag() {
1055        let cli = Cli::parse_from(["vtcode", "login", "codex", "--device-code"]);
1056
1057        assert!(matches!(
1058            cli.command,
1059            Some(Commands::Login {
1060                ref provider,
1061                device_code: true,
1062                from_codex: false
1063            }) if provider == "codex"
1064        ));
1065    }
1066
1067    #[test]
1068    fn parses_codex_experimental_flags() {
1069        let enabled = Cli::parse_from(["vtcode", "--codex-experimental"]);
1070        assert_eq!(enabled.codex_experimental_override(), Some(true));
1071
1072        let disabled = Cli::parse_from(["vtcode", "--no-codex-experimental"]);
1073        assert_eq!(disabled.codex_experimental_override(), Some(false));
1074    }
1075
1076    #[test]
1077    fn codex_experimental_flags_conflict() {
1078        let result = Cli::try_parse_from(["vtcode", "--codex-experimental", "--no-codex-experimental"]);
1079
1080        result.unwrap_err();
1081    }
1082
1083    #[test]
1084    fn parses_create_project_feature_flags() {
1085        let cli = Cli::parse_from([
1086            "vtcode",
1087            "create-project",
1088            "demo",
1089            "--feature",
1090            "web",
1091            "--feature",
1092            "db",
1093        ]);
1094
1095        assert!(matches!(
1096            cli.command,
1097            Some(Commands::CreateProject { ref name, ref features })
1098                if name == "demo" && features == &vec!["web".to_string(), "db".to_string()]
1099        ));
1100    }
1101
1102    #[test]
1103    fn parses_revert_partial_long_flag() {
1104        let cli = Cli::parse_from(["vtcode", "revert", "--turn", "3", "--partial", "code"]);
1105
1106        assert!(matches!(
1107            cli.command,
1108            Some(Commands::Revert {
1109                turn: 3,
1110                partial: Some(ref scope)
1111            }) if scope == "code"
1112        ));
1113    }
1114
1115    #[test]
1116    fn parses_config_reset_workspace_target_by_default() {
1117        let cli = Cli::parse_from(["vtcode", "config", "reset"]);
1118
1119        assert!(matches!(
1120            cli.command,
1121            Some(Commands::Config {
1122                command: Some(ConfigCommand::Reset(ConfigResetArgs { global: false, project: false })),
1123                global: false,
1124                ..
1125            })
1126        ));
1127    }
1128
1129    #[test]
1130    fn parses_config_reset_global_and_project_targets() {
1131        let global = Cli::parse_from(["vtcode", "config", "reset", "--global"]);
1132        assert!(matches!(
1133            global.command,
1134            Some(Commands::Config {
1135                command: Some(ConfigCommand::Reset(ConfigResetArgs { global: true, project: false })),
1136                ..
1137            })
1138        ));
1139
1140        let project = Cli::parse_from(["vtcode", "config", "reset", "--project"]);
1141        assert!(matches!(
1142            project.command,
1143            Some(Commands::Config {
1144                command: Some(ConfigCommand::Reset(ConfigResetArgs { global: false, project: true })),
1145                ..
1146            })
1147        ));
1148    }
1149
1150    #[test]
1151    fn parses_parent_global_flag_for_config_reset() {
1152        let cli = Cli::parse_from(["vtcode", "config", "--global", "reset"]);
1153
1154        assert!(matches!(
1155            cli.command,
1156            Some(Commands::Config {
1157                command: Some(ConfigCommand::Reset(ConfigResetArgs { global: false, project: false })),
1158                global: true,
1159                ..
1160            })
1161        ));
1162    }
1163
1164    #[test]
1165    fn rejects_config_reset_with_conflicting_layer_flags() {
1166        let result = Cli::try_parse_from(["vtcode", "config", "reset", "--global", "--project"]);
1167
1168        assert!(result.is_err());
1169    }
1170}