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