Skip to main content

vtcode_core/cli/
args.rs

1//! CLI argument parsing and configuration
2
3use crate::config::models::ModelId;
4use clap::{ArgAction, Args, ColorChoice, Parser, Subcommand, ValueEnum, ValueHint};
5use colorchoice_clap::Color as ColorSelection;
6use std::path::PathBuf;
7
8/// Get the long version information following Ratatui recipe pattern
9///
10/// Displays version, authors, and directory information following the
11/// XDG Base Directory Specification for organized file storage.
12/// See: <https://ratatui.rs/recipes/apps/config-directories/>
13///
14/// This function is called at runtime to provide dynamic version info
15/// that includes actual resolved directory paths.
16pub 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/// Main CLI structure for vtcode with advanced features
56#[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    /// Color output selection (auto, always, never)
78    #[command(flatten)]
79    pub color: ColorSelection,
80
81    /// Optional positional path to run vtcode against a different workspace
82    #[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    /// LLM Model ID (e.g., gpt-5, claude-sonnet-4-6, gemini-3-flash-preview)
91    #[arg(long, global = true)]
92    pub model: Option<String>,
93
94    /// LLM Provider (gemini, openai, anthropic, deepseek, openrouter, codex, zai, moonshot, minimax, ollama, lmstudio)
95    #[arg(long, global = true)]
96    pub provider: Option<String>,
97
98    /// API key environment variable (auto-detects GEMINI_API_KEY, OPENAI_API_KEY, etc.)
99    #[arg(long, global = true, default_value = crate::config::constants::defaults::DEFAULT_API_KEY_ENV)]
100    pub api_key_env: String,
101
102    /// Workspace root directory (default: current directory)
103    #[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    /// Enable research-preview features
114    #[arg(long, global = true)]
115    pub research_preview: bool,
116
117    /// Security level for tool execution (strict, moderate, permissive)
118    #[arg(long, global = true, default_value = "moderate")]
119    pub security_level: String,
120
121    /// Show diffs for file changes in chat interface
122    #[arg(long, global = true)]
123    pub show_file_diffs: bool,
124
125    /// Maximum concurrent async operations
126    #[arg(long, global = true, default_value_t = 5)]
127    pub max_concurrent_ops: usize,
128
129    /// Maximum API requests per minute
130    #[arg(long, global = true, default_value_t = 30)]
131    pub api_rate_limit: usize,
132
133    /// Maximum tool calls per session
134    #[arg(long, global = true, default_value_t = 10)]
135    pub max_tool_calls: usize,
136
137    /// Enable debug output for troubleshooting
138    #[arg(long, global = true)]
139    pub debug: bool,
140
141    /// Enable verbose logging
142    #[arg(long, global = true)]
143    pub verbose: bool,
144
145    /// Suppress all non-essential output (for scripting, CI/CD)
146    #[arg(short, long, global = true)]
147    pub quiet: bool,
148
149    /// Configuration overrides or file path (KEY=VALUE or PATH)
150    #[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    /// Log level (error, warn, info, debug, trace)
160    #[arg(long, global = true, default_value = "info")]
161    pub log_level: String,
162
163    /// Disable color output (equivalent to `--color never`)
164    #[arg(long, global = true)]
165    pub no_color: bool,
166
167    /// Select UI theme (e.g., ciapre-dark, ciapre-blue)
168    #[arg(long, global = true, value_name = "THEME")]
169    pub theme: Option<String>,
170
171    /// App tick rate in milliseconds (default: 250)
172    #[arg(short = 't', long, default_value_t = 250)]
173    pub tick_rate: u64,
174
175    /// Frame rate in FPS (default: 60)
176    #[arg(short = 'f', long, default_value_t = 60)]
177    pub frame_rate: u64,
178
179    /// Enable skills system
180    #[arg(long, global = true)]
181    pub enable_skills: bool,
182
183    /// Enable Chrome browser integration for web automation
184    #[arg(long, global = true)]
185    pub chrome: bool,
186
187    /// Disable Chrome browser integration
188    #[arg(long = "no-chrome", global = true, conflicts_with = "chrome")]
189    pub no_chrome: bool,
190
191    /// Skip safety confirmations (use with caution)
192    #[arg(long, global = true)]
193    pub skip_confirmations: bool,
194
195    /// Enable experimental Codex app-server features for this run
196    #[arg(
197        long = "codex-experimental",
198        global = true,
199        conflicts_with = "no_codex_experimental"
200    )]
201    pub codex_experimental: bool,
202
203    /// Disable experimental Codex app-server features for this run
204    #[arg(
205        long = "no-codex-experimental",
206        global = true,
207        conflicts_with = "codex_experimental"
208    )]
209    pub no_codex_experimental: bool,
210
211    /// Print response without launching the interactive TUI
212    #[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    /// Enable full-auto mode (no interaction) or run a headless task
225    #[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    /// Resume a previous conversation (use without ID for interactive picker)
236    #[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    /// Continue the most recent conversation automatically
248    #[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    /// Fork an existing session with a new session ID
257    #[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    /// Show archived sessions from every workspace when resuming or forking
266    #[arg(long, global = true)]
267    pub all: bool,
268
269    /// Custom suffix for session identifier (alphanumeric, dash, underscore only, max 64 chars)
270    #[arg(long = "session-id", global = true, value_name = "CUSTOM_SUFFIX")]
271    pub session_id: Option<String>,
272
273    /// Use summarized history when forking a session
274    #[arg(long, global = true)]
275    pub summarize: bool,
276
277    /// Override the default agent model for this session
278    #[arg(long, global = true, value_name = "AGENT")]
279    pub agent: Option<String>,
280
281    /// Tools that execute without prompting (comma-separated, supports patterns like "Bash(git:*)")
282    #[arg(long = "allowed-tools", global = true, value_name = "TOOLS", action = ArgAction::Append)]
283    pub allowed_tools: Vec<String>,
284
285    /// Tools that cannot be used by the agent
286    #[arg(long = "disallowed-tools", global = true, value_name = "TOOLS", action = ArgAction::Append)]
287    pub disallowed_tools: Vec<String>,
288
289    /// Skip all permission prompts (reduces security - use with caution)
290    #[arg(long = "dangerously-skip-permissions", global = true)]
291    pub dangerously_skip_permissions: bool,
292
293    /// Explicitly connect to IDE on startup (auto-detects available IDEs)
294    #[arg(long, global = true)]
295    pub ide: bool,
296
297    /// Begin in a specified permission mode (default, accept_edits, auto, dont_ask, bypass_permissions, plus legacy ask/suggest/auto-approved/full-auto/trusted_auto/plan)
298    #[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/// Options for the `ask` command
306#[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/// Output format options for the `ask` subcommand.
315#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
316pub enum AskOutputFormat {
317    /// Emit the response as a structured JSON document.
318    Json,
319}
320
321/// Output format options for the `schema` command.
322#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
323pub enum SchemaOutputFormat {
324    /// Emit one JSON document with all selected schemas.
325    Json,
326    /// Emit one JSON object per line.
327    Ndjson,
328}
329
330/// Documentation detail level for the `schema` command.
331#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
332pub enum SchemaMode {
333    /// Minimal descriptions and compact parameter metadata.
334    Minimal,
335    /// Balanced descriptions for agent discovery.
336    Progressive,
337    /// Full descriptions and full parameter metadata.
338    Full,
339}
340
341/// Schema-focused subcommands.
342#[derive(Subcommand, Debug, Clone)]
343pub enum SchemaCommands {
344    /// List built-in VT Code tool schemas.
345    Tools {
346        /// Documentation detail level for tool descriptions.
347        #[arg(long, value_enum, default_value_t = SchemaMode::Progressive)]
348        mode: SchemaMode,
349        /// Output format for schema payloads.
350        #[arg(long, value_enum, default_value_t = SchemaOutputFormat::Json)]
351        format: SchemaOutputFormat,
352        /// Filter by tool name (repeatable).
353        #[arg(long = "name", value_name = "TOOL")]
354        names: Vec<String>,
355    },
356}
357
358/// `exec` subcommands.
359#[derive(Subcommand, Debug, Clone)]
360pub enum ExecSubcommand {
361    /// Resume a previous exec session with a follow-up prompt
362    #[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/// Arguments for `vtcode exec resume`.
369#[derive(Args, Debug, Clone)]
370pub struct ExecResumeArgs {
371    /// Resume the most recent archived exec session
372    #[arg(long)]
373    pub last: bool,
374    /// Search archived exec sessions across every workspace
375    #[arg(long)]
376    pub all: bool,
377    /// Archived session identifier to resume, or the prompt when `--last` is used
378    #[arg(value_name = "SESSION_ID_OR_PROMPT", required_unless_present = "last")]
379    pub session_or_prompt: Option<String>,
380    /// Follow-up prompt to execute when resuming a specific session. Use `-` to force reading from stdin.
381    #[arg(value_name = "PROMPT")]
382    pub prompt: Option<String>,
383}
384
385/// `schedule` subcommands.
386#[derive(Subcommand, Debug, Clone)]
387pub enum ScheduleSubcommand {
388    /// Create a durable scheduled task
389    #[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 durable scheduled tasks
394    List,
395    /// Delete a durable scheduled task by id
396    Delete {
397        #[arg(value_name = "TASK_ID")]
398        id: String,
399    },
400    /// Run the local durable scheduler daemon
401    Serve,
402    /// Install the scheduler as a user service
403    #[command(name = "install-service")]
404    InstallService,
405    /// Uninstall the scheduler user service
406    #[command(name = "uninstall-service")]
407    UninstallService,
408}
409
410/// Arguments for `vtcode schedule create`.
411#[derive(Args, Debug, Clone)]
412pub struct ScheduleCreateArgs {
413    /// Optional short label for the task
414    #[arg(long, value_name = "NAME")]
415    pub name: Option<String>,
416    /// Prompt to run with `vtcode exec`
417    #[arg(long, value_name = "PROMPT", conflicts_with = "reminder")]
418    pub prompt: Option<String>,
419    /// Local reminder text to surface without invoking the model
420    #[arg(long, value_name = "TEXT", conflicts_with = "prompt")]
421    pub reminder: Option<String>,
422    /// Fixed interval such as 10m, 2h, or 1d
423    #[arg(long, value_name = "DURATION", conflicts_with_all = ["cron", "at"])]
424    pub every: Option<String>,
425    /// Five-field cron expression
426    #[arg(long, value_name = "EXPR", conflicts_with_all = ["every", "at"])]
427    pub cron: Option<String>,
428    /// One-shot local time (RFC3339, YYYY-MM-DD HH:MM, or HH:MM)
429    #[arg(long, value_name = "TIME", conflicts_with_all = ["every", "cron"])]
430    pub at: Option<String>,
431    /// Workspace to use for prompt tasks
432    #[arg(long, value_name = "PATH", value_hint = ValueHint::DirPath)]
433    pub workspace: Option<PathBuf>,
434}
435
436/// Arguments for `vtcode review`.
437#[derive(Args, Debug, Clone)]
438pub struct ReviewArgs {
439    /// Emit structured JSON events to stdout (one per line)
440    #[arg(long)]
441    pub json: bool,
442    /// Optional path to write the JSONL transcript
443    #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
444    pub events: Option<PathBuf>,
445    /// Write the last agent message to this file
446    #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
447    pub last_message_file: Option<PathBuf>,
448    /// Review the last committed diff instead of the current diff
449    #[arg(long, conflicts_with_all = ["target", "files"])]
450    pub last_diff: bool,
451    /// Review a custom git target expression
452    #[arg(long, value_name = "TARGET", conflicts_with = "files")]
453    pub target: Option<String>,
454    /// Optional review style or focus area
455    #[arg(long, value_name = "STYLE")]
456    pub style: Option<String>,
457    /// Review specific files instead of a diff target (repeatable)
458    #[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/// Available commands
486#[derive(Subcommand, Debug, Clone)]
487pub enum Commands {
488    /// Start Agent Client Protocol bridge for IDE integrations
489    #[command(name = "acp")]
490    AgentClientProtocol {
491        /// Client to connect over ACP
492        #[arg(value_enum, default_value_t = AgentClientProtocolTarget::Zed)]
493        target: AgentClientProtocolTarget,
494    },
495
496    /// Interactive AI coding assistant
497    Chat,
498
499    /// Single prompt mode - prints model reply without tools
500    ///
501    /// Send a single prompt to the model and print the response. No tools are
502    /// invoked, no session is created, and the process exits after replying.
503    ///
504    /// Examples:
505    ///   vtcode ask "what is a monad?"
506    ///   echo "summarize this" | vtcode ask
507    ///   vtcode ask --output-format json "explain ownership in Rust"
508    Ask {
509        /// Prompt to ask. Use `-` to force reading from stdin.
510        #[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        /// Format the response using a structured representation.
516        #[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    /// Headless execution mode
525    ///
526    /// Run the agent in non-interactive mode. The agent executes the prompt,
527    /// runs tools, and exits when done. Ideal for CI/CD, scripting, and
528    /// agent-to-agent workflows.
529    ///
530    /// Examples:
531    ///   vtcode exec "explain this codebase"
532    ///   vtcode exec --json "fix the failing test"
533    ///   vtcode exec --dry-run "refactor auth module"
534    ///   cat file.rs | vtcode exec "review this code"
535    ///   vtcode exec resume --last
536    Exec {
537        /// Emit structured JSON events to stdout (one per line)
538        #[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        /// Run in read-only dry-run mode (blocks mutating tool calls)
544        #[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        /// Optional path to write the JSONL transcript
550        #[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        /// Write the last agent message to this file
553        #[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        /// Optional exec subcommand
556        #[command(subcommand)]
557        command: Option<ExecSubcommand>,
558        /// Prompt to execute. Use `-` to force reading from stdin.
559        #[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    /// Manage durable scheduled tasks
566    ///
567    /// Create, list, and delete scheduled tasks that run on a recurring or
568    /// one-shot basis. Tasks are stored persistently and survive restarts
569    /// when paired with `vtcode schedule install-service`.
570    ///
571    /// Examples:
572    ///   vtcode schedule create --name "daily-review" --cron "0 9 * * 1-5" --prompt "review recent changes"
573    ///   vtcode schedule create --name "reminder" --reminder "standup in 10 minutes" --at "09:50"
574    ///   vtcode schedule list
575    ///   vtcode schedule delete <task-id>
576    Schedule {
577        #[command(subcommand)]
578        command: ScheduleSubcommand,
579    },
580
581    /// Internal VT Code background subagent runner
582    #[command(name = "background-subagent", hide = true)]
583    BackgroundSubagent(BackgroundSubagentArgs),
584
585    /// Headless code review for the current diff, selected files, or a custom git target
586    #[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    /// Runtime schema introspection for built-in tools
592    Schema {
593        #[command(subcommand)]
594        command: SchemaCommands,
595    },
596
597    /// Verbose interactive chat with debug output
598    ChatVerbose,
599
600    /// Analyze workspace (structure, security, performance)
601    Analyze {
602        /// Type of analysis to perform
603        #[arg(value_name = "TYPE", default_value = "full")]
604        analysis_type: String,
605    },
606
607    /// Pretty-print trajectory logs
608    #[command(name = "trajectory")]
609    Trajectory {
610        /// Optional path to trajectory JSONL file
611        #[arg(long)]
612        file: Option<PathBuf>,
613        /// Number of top entries to show
614        #[arg(long, default_value_t = 10)]
615        top: usize,
616    },
617
618    /// Send a VT Code notification using the built-in notification system
619    Notify {
620        /// Optional notification title
621        #[arg(long, value_name = "TITLE")]
622        title: Option<String>,
623        /// Notification message
624        #[arg(value_name = "MESSAGE")]
625        message: String,
626    },
627
628    /// Benchmark against SWE-bench evaluation framework
629    Benchmark {
630        /// Path to a JSON benchmark specification
631        #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
632        task_file: Option<PathBuf>,
633        /// Inline JSON specification for quick experiments
634        #[arg(long, value_name = "JSON")]
635        task: Option<String>,
636        /// Optional path to write the structured benchmark report
637        #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
638        output: Option<PathBuf>,
639        /// Limit the number of tasks executed
640        #[arg(long, value_name = "COUNT")]
641        max_tasks: Option<usize>,
642    },
643
644    /// Create complete Rust project
645    CreateProject {
646        name: String,
647        #[arg(long = "feature", value_name = "FEATURE", action = ArgAction::Append)]
648        features: Vec<String>,
649    },
650
651    /// Revert agent to a previous snapshot
652    Revert {
653        /// Turn number to revert to
654        #[arg(short, long)]
655        turn: usize,
656        /// Scope of revert operation: conversation, code, full
657        #[arg(long)]
658        partial: Option<String>,
659    },
660
661    /// List all available snapshots
662    Snapshots,
663
664    /// Clean up old snapshots
665    ///
666    /// Features:
667    ///   • Remove snapshots beyond limit
668    ///   • Configurable retention policy
669    ///   • Safe deletion with confirmation
670    ///
671    /// Examples:
672    ///   vtcode cleanup-snapshots
673    ///   vtcode cleanup-snapshots --max 20
674    #[command(name = "cleanup-snapshots")]
675    CleanupSnapshots {
676        /// Maximum number of snapshots to keep
677        ///
678        /// Default: 50
679        /// Example: --max 20
680        #[arg(short, long, default_value_t = 50)]
681        max: usize,
682    },
683
684    /// Initialize project guidance and workspace scaffolding
685    ///
686    /// Bootstrap a workspace for use with VT Code. Creates vtcode.toml,
687    /// AGENTS.md, and other scaffolding. Run this once per project.
688    ///
689    /// Examples:
690    ///   vtcode init
691    ///   vtcode init --force
692    Init {
693        /// Overwrite an existing AGENTS.md without prompting
694        #[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    /// Initialize project in ~/.vtcode/projects/
703    ///
704    /// Create a new project entry in the VT Code projects directory.
705    /// This is separate from `vtcode init` which bootstraps a workspace.
706    ///
707    /// Examples:
708    ///   vtcode init-project
709    ///   vtcode init-project --name my-project
710    ///   vtcode init-project --force --migrate
711    #[command(name = "init-project")]
712    InitProject {
713        /// Project name - defaults to current directory name
714        #[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        /// Force initialization - overwrite existing project structure
720        #[arg(
721            long,
722            long_help = "Overwrite existing project structure without confirmation."
723        )]
724        force: bool,
725        /// Migrate existing files - move existing config/cache files to new structure
726        #[arg(
727            long,
728            long_help = "Move existing config and cache files into the new project structure."
729        )]
730        migrate: bool,
731    },
732
733    /// Generate configuration file
734    ///
735    /// Create a vtcode.toml configuration file with default settings.
736    /// Use --global to create in ~/.vtcode/ or specify an output path.
737    ///
738    /// Examples:
739    ///   vtcode config
740    ///   vtcode config --global
741    ///   vtcode config --output ./my-vtcode.toml
742    Config {
743        /// Output file path
744        #[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        /// Create in user home directory (~/.vtcode/vtcode.toml)
750        #[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    /// Authenticate with a supported provider
758    ///
759    /// Start an OAuth or API-key login flow for the given provider.
760    /// Credentials are stored securely in the OS keychain.
761    ///
762    /// Examples:
763    ///   vtcode login openai
764    ///   vtcode login openrouter
765    ///   vtcode login codex
766    ///   vtcode login codex --device-code
767    Login {
768        /// Provider name (`openai`, `openrouter`, `copilot`, or `codex`)
769        #[arg(
770            long_help = "The provider to authenticate with.\nSupported: openai, openrouter, copilot, codex"
771        )]
772        provider: String,
773        /// Use device-code login when the provider supports it (currently `codex` only)
774        #[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    /// Clear stored authentication credentials for a provider
783    ///
784    /// Remove stored OAuth tokens or API keys for the given provider.
785    ///
786    /// Examples:
787    ///   vtcode logout openai
788    ///   vtcode logout openrouter
789    Logout {
790        /// Provider name (`openai`, `openrouter`, `copilot`, or `codex`)
791        #[arg(
792            long_help = "The provider to deauthenticate.\nSupported: openai, openrouter, copilot, codex"
793        )]
794        provider: String,
795    },
796
797    /// Show authentication status for one provider or all supported providers
798    ///
799    /// Display whether each provider is authenticated, which credential type
800    /// is in use, and token/session metadata when available.
801    ///
802    /// Examples:
803    ///   vtcode auth
804    ///   vtcode auth openai
805    ///   vtcode auth openrouter
806    Auth {
807        /// Optional provider name (`openai`, `openrouter`, `copilot`, or `codex`)
808        #[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    /// Manage tool execution policies
815    #[command(name = "tool-policy")]
816    ToolPolicy {
817        #[command(subcommand)]
818        command: crate::cli::tool_policy_commands::ToolPolicyCommands,
819    },
820
821    /// Manage Model Context Protocol providers
822    #[command(name = "mcp")]
823    Mcp {
824        #[command(subcommand)]
825        command: crate::mcp::cli::McpCommands,
826    },
827
828    /// Agent2Agent (A2A) Protocol
829    #[command(name = "a2a")]
830    A2a {
831        #[command(subcommand)]
832        command: super::super::a2a::cli::A2aCommands,
833    },
834
835    /// Proxy to the official Codex app-server
836    #[command(name = "app-server")]
837    AppServer {
838        /// Transport listen target passed through to `codex app-server`
839        #[arg(long, default_value = "stdio://")]
840        listen: String,
841    },
842
843    /// Manage models and providers
844    Models {
845        #[command(subcommand)]
846        command: ModelCommands,
847    },
848
849    /// Manage GPU pod deployments
850    #[command(name = "pods")]
851    Pods {
852        #[command(subcommand)]
853        command: PodsCommands,
854    },
855
856    /// Generate or display man pages
857    Man {
858        /// Command name to generate man page for (optional)
859        command: Option<String>,
860        /// Output file path to save man page
861        #[arg(short, long)]
862        output: Option<PathBuf>,
863    },
864
865    /// Manage Agent Skills
866    ///
867    /// Skills are reusable instruction sets that extend the agent's capabilities.
868    /// Each skill is a directory containing a SKILL.md manifest and optional scripts.
869    ///
870    /// Examples:
871    ///   vtcode skills list
872    ///   vtcode skills create my-skill
873    ///   vtcode skills load my-skill
874    ///   vtcode skills info my-skill
875    ///   vtcode skills validate ./path/to/skill
876    #[command(subcommand)]
877    Skills(SkillsSubcommand),
878
879    /// List available skills (alias for `vtcode skills list`)
880    #[command(name = "list-skills", hide = true)]
881    ListSkills {},
882
883    /// Manage optional VT Code dependencies
884    ///
885    /// Install, update, or check the status of optional tools that VT Code
886    /// can use (ripgrep, ast-grep, search-tools bundle).
887    ///
888    /// Examples:
889    ///   vtcode dependencies status
890    ///   vtcode dependencies install search-tools
891    ///   vtcode deps install ripgrep
892    #[command(name = "dependencies", visible_alias = "deps", subcommand)]
893    Dependencies(DependenciesSubcommand),
894
895    /// Run built-in repository checks
896    ///
897    /// Execute repository-level checks such as ast-grep rule tests and scans.
898    ///
899    /// Examples:
900    ///   vtcode check ast-grep
901    Check {
902        #[command(subcommand)]
903        command: CheckSubcommand,
904    },
905
906    /// Check for and install binary updates from GitHub Releases
907    ///
908    /// Manage VT Code binary updates. By default checks for a new version
909    /// and offers to install it. Use flags to customize behavior.
910    ///
911    /// Examples:
912    ///   vtcode update
913    ///   vtcode update --check
914    ///   vtcode update --force
915    ///   vtcode update --list
916    ///   vtcode update --pin 0.120.0
917    ///   vtcode update --unpin
918    ///   vtcode update --channel beta
919    #[command(name = "update")]
920    Update {
921        /// Check for updates without installing
922        #[arg(
923            long,
924            long_help = "Check whether a newer version is available without installing it."
925        )]
926        check: bool,
927        /// Force update even if on latest version
928        #[arg(
929            long,
930            long_help = "Reinstall or downgrade even if the current version is already the latest."
931        )]
932        force: bool,
933        /// List available versions
934        #[arg(
935            long,
936            long_help = "Print available release versions from GitHub and exit."
937        )]
938        list: bool,
939        /// Number of versions to list (default: 10)
940        #[arg(
941            long,
942            default_value_t = 10,
943            long_help = "Maximum number of versions to display with --list."
944        )]
945        limit: usize,
946        /// Pin to a specific version
947        #[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        /// Unpin version
954        #[arg(
955            long,
956            long_help = "Remove a previously set version pin and resume auto-updates."
957        )]
958        unpin: bool,
959        /// Set release channel (stable, beta, nightly)
960        #[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        /// Show current update configuration
967        #[arg(
968            long,
969            long_help = "Display the current update configuration (channel, pin, intervals) and exit."
970        )]
971        show_config: bool,
972    },
973
974    /// Start Anthropic API compatibility server
975    #[command(name = "anthropic-api")]
976    AnthropicApi {
977        /// Port to run the server on
978        #[arg(long, default_value = "11434")]
979        port: u16,
980        /// Host address to bind to
981        #[arg(long, default_value = "127.0.0.1")]
982        host: String,
983    },
984}
985
986/// Supported Agent Client Protocol clients
987#[derive(Clone, Copy, Debug, ValueEnum)]
988pub enum AgentClientProtocolTarget {
989    /// Agent Client Protocol client (legacy Zed identifier)
990    Zed,
991    /// Standard Agent Client Protocol client
992    Standard,
993}
994
995/// Model management commands with concise, actionable help
996#[derive(Subcommand, Debug, Clone)]
997pub enum ModelCommands {
998    /// List all providers and models with status indicators
999    List,
1000
1001    /// Set default provider (gemini, openai, anthropic, deepseek)
1002    #[command(name = "set-provider")]
1003    SetProvider {
1004        /// Provider name to set as default
1005        provider: String,
1006    },
1007
1008    /// Set default model (e.g., deepseek-reasoner, gpt-5, claude-sonnet-4-6)
1009    #[command(name = "set-model")]
1010    SetModel {
1011        /// Model name to set as default
1012        model: String,
1013    },
1014
1015    /// Configure provider settings (API keys, base URLs, models)
1016    Config {
1017        /// Provider name to configure
1018        provider: String,
1019
1020        /// API key for the provider
1021        #[arg(long)]
1022        api_key: Option<String>,
1023
1024        /// Base URL for local providers
1025        #[arg(long)]
1026        base_url: Option<String>,
1027
1028        /// Default model for this provider
1029        #[arg(long)]
1030        model: Option<String>,
1031    },
1032
1033    /// Test provider connectivity and validate configuration
1034    Test {
1035        /// Provider name to test
1036        provider: String,
1037    },
1038
1039    /// Compare model performance across providers (coming soon)
1040    Compare,
1041
1042    /// Show detailed model information and specifications
1043    Info {
1044        /// Model name to get information about
1045        model: String,
1046    },
1047}
1048
1049/// GPU pod management commands.
1050#[derive(Subcommand, Debug, Clone)]
1051pub enum PodsCommands {
1052    /// Start a model on the active pod.
1053    Start {
1054        /// Local model name used for lookup and storage.
1055        #[arg(long)]
1056        name: String,
1057        /// Hugging Face or provider model identifier to launch.
1058        #[arg(long)]
1059        model: String,
1060        /// Optional explicit pod name to store as the active pod.
1061        #[arg(long = "pod-name")]
1062        pod_name: Option<String>,
1063        /// SSH connection string used for the pod.
1064        #[arg(long)]
1065        ssh: Option<String>,
1066        /// GPU identifiers on the pod, repeated as `ID:NAME`.
1067        #[arg(long = "gpu", value_name = "ID:NAME", action = ArgAction::Append)]
1068        gpus: Vec<String>,
1069        /// Optional remote models directory.
1070        #[arg(long = "models-path")]
1071        models_path: Option<String>,
1072        /// Optional exact profile name to use.
1073        #[arg(long)]
1074        profile: Option<String>,
1075        /// Optional requested GPU count.
1076        #[arg(long = "gpus")]
1077        gpus_count: Option<usize>,
1078        /// Optional override for `--gpu-memory-utilization` (percent).
1079        #[arg(long)]
1080        memory: Option<f32>,
1081        /// Optional override for `--max-model-len` (e.g. 4k, 32k, 131072).
1082        #[arg(long)]
1083        context: Option<String>,
1084    },
1085
1086    /// Stop a running model on the active pod.
1087    Stop {
1088        /// Local model name to stop.
1089        #[arg(long)]
1090        name: String,
1091    },
1092
1093    /// Stop every running model on the active pod.
1094    StopAll,
1095
1096    /// List running models on the active pod.
1097    List,
1098
1099    /// Stream logs for a running model on the active pod.
1100    Logs {
1101        /// Local model name whose logs should be streamed.
1102        #[arg(long)]
1103        name: String,
1104    },
1105
1106    /// Show compatible and incompatible known models for the active pod.
1107    KnownModels,
1108}
1109
1110/// Skills subcommands
1111#[derive(Debug, Subcommand, Clone)]
1112pub enum SkillsSubcommand {
1113    /// List available skills
1114    #[command(name = "list")]
1115    List {
1116        /// Show all skills including system skills
1117        #[arg(long)]
1118        all: bool,
1119    },
1120
1121    /// Load a skill for use in agent session
1122    #[command(name = "load")]
1123    Load {
1124        /// Skill name to load
1125        name: String,
1126        /// Optional path to skill directory
1127        #[arg(long)]
1128        path: Option<PathBuf>,
1129    },
1130
1131    /// Unload a skill from session
1132    #[command(name = "unload")]
1133    Unload {
1134        /// Skill name to unload
1135        name: String,
1136    },
1137
1138    /// Show skill details and instructions
1139    #[command(name = "info")]
1140    Info {
1141        /// Skill name to get information about
1142        name: String,
1143    },
1144
1145    /// Create a new skill from template
1146    #[command(name = "create")]
1147    Create {
1148        /// Path for new skill directory
1149        path: PathBuf,
1150        /// Optional template to use
1151        #[arg(long)]
1152        template: Option<String>,
1153    },
1154
1155    /// Validate SKILL.md manifest
1156    #[command(name = "validate")]
1157    Validate {
1158        /// Path to skill directory or SKILL.md file
1159        path: PathBuf,
1160        /// Enable strict validation (warnings become errors for routing quality checks)
1161        #[arg(long)]
1162        strict: bool,
1163    },
1164
1165    /// Validate all skills for container skills compatibility
1166    #[command(name = "check-compatibility")]
1167    CheckCompatibility,
1168
1169    /// Show skill configuration and search paths
1170    #[command(name = "config")]
1171    Config,
1172
1173    /// Regenerate skills index file
1174    #[command(name = "regenerate-index")]
1175    RegenerateIndex,
1176
1177    /// skills-ref compatible commands (agentskills.io spec)
1178    #[command(name = "skills-ref", subcommand)]
1179    SkillsRef(SkillsRefSubcommand),
1180}
1181
1182/// skills-ref compatible subcommands per agentskills.io specification
1183#[derive(Debug, Subcommand, Clone)]
1184pub enum SkillsRefSubcommand {
1185    /// Validate a skill directory
1186    #[command(name = "validate")]
1187    Validate {
1188        /// Path to skill directory
1189        path: PathBuf,
1190    },
1191
1192    /// Generate <available_skills> XML for agent prompts
1193    #[command(name = "to-prompt")]
1194    ToPrompt {
1195        /// Paths to skill directories
1196        paths: Vec<PathBuf>,
1197    },
1198
1199    /// List discovered skills
1200    #[command(name = "list")]
1201    List {
1202        /// Optional path to search (defaults to current directory)
1203        path: Option<PathBuf>,
1204    },
1205}
1206
1207/// Optional VT Code dependency names
1208#[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/// Dependency management subcommands
1219#[derive(Debug, Subcommand, Clone)]
1220pub enum DependenciesSubcommand {
1221    /// Install or update an optional dependency
1222    #[command(name = "install")]
1223    Install {
1224        /// Dependency to install
1225        dependency: ManagedDependency,
1226    },
1227
1228    /// Show current status for an optional dependency
1229    #[command(name = "status")]
1230    Status {
1231        /// Dependency to inspect
1232        dependency: ManagedDependency,
1233    },
1234}
1235
1236/// Built-in repository checks
1237#[derive(Debug, Subcommand, Clone, PartialEq, Eq)]
1238pub enum CheckSubcommand {
1239    /// Run ast-grep rule tests and scan for the current workspace
1240    #[command(name = "ast-grep")]
1241    AstGrep,
1242}
1243
1244/// Configuration file structure with latest features
1245#[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/// Tool configuration from config file
1261#[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/// Context management configuration
1270#[derive(Debug, serde::Deserialize)]
1271pub struct ContextConfig {
1272    pub max_context_length: Option<usize>,
1273}
1274
1275/// Logging configuration
1276#[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/// Performance monitoring configuration
1593#[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/// Security configuration
1604#[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,                // Skills disabled by default
1649            tick_rate: 250,                      // Default tick rate: 250ms
1650            frame_rate: 60,                      // Default frame rate: 60 FPS
1651            agent: None,                         // No agent override by default
1652            allowed_tools: Vec::new(),           // No tool restrictions by default
1653            disallowed_tools: Vec::new(),        // No tool restrictions by default
1654            dangerously_skip_permissions: false, // Safety confirmations enabled by default
1655            ide: false,                          // No auto IDE connection by default
1656            permission_mode: None,               // Use config permission mode by default
1657            chrome: false,                       // Chrome integration disabled by default
1658            no_chrome: false,                    // Chrome integration not explicitly disabled
1659            command: Some(Commands::Chat),
1660        }
1661    }
1662}
1663
1664impl Cli {
1665    /// Get the model to use, with fallback to default
1666    pub fn get_model(&self) -> String {
1667        self.model
1668            .clone()
1669            .unwrap_or_else(|| ModelId::default().to_string())
1670    }
1671
1672    /// Load configuration from a simple TOML-like file without external deps
1673    ///
1674    /// Supported keys (top-level): model, api_key_env, verbose, log_level, workspace
1675    /// Example:
1676    ///   model = "gemini-3-flash-preview"
1677    ///   api_key_env = "GEMINI_API_KEY"
1678    ///   verbose = true
1679    ///   log_level = "info"
1680    ///   workspace = "/path/to/workspace"
1681    pub async fn load_config(&self) -> Result<ConfigFile, Box<dyn std::error::Error>> {
1682        use std::path::Path;
1683        use tokio::fs;
1684
1685        // Resolve candidate path
1686        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                // No config file; return empty config
1707                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        // Very small parser: key = value, supports quoted strings, booleans, and plain paths
1726        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            // Strip inline comments after '#'
1746            let line = match line.find('#') {
1747                Some(idx) => &line[..idx],
1748                None => line,
1749            }
1750            .trim();
1751
1752            // Expect key = value
1753            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            // Remove surrounding quotes if present
1761            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                        // Resolve relative to config file directory
1786                        let base = path.parent().unwrap_or(Path::new("."));
1787                        base.join(v)
1788                    };
1789                    cfg.workspace = Some(p);
1790                }
1791                _ => {
1792                    // Ignore unknown keys in this minimal parser
1793                }
1794            }
1795        }
1796
1797        Ok(cfg)
1798    }
1799
1800    /// Get the effective workspace path
1801    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    /// Get the effective API key environment variable
1808    ///
1809    /// Automatically infers the API key environment variable based on the provider
1810    /// when the current value matches the default or is not explicitly set.
1811    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    /// Check if verbose mode is enabled
1821    pub fn is_verbose(&self) -> bool {
1822        self.verbose
1823    }
1824
1825    /// Check if performance monitoring is enabled
1826    /// Check if research-preview features are enabled
1827    pub fn is_research_preview_enabled(&self) -> bool {
1828        self.research_preview
1829    }
1830
1831    /// Get the security level
1832    pub fn get_security_level(&self) -> &str {
1833        &self.security_level
1834    }
1835
1836    /// Check if debug mode is enabled (includes verbose)
1837    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}