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    /// Run non-interactively with full-auto permission review
225    #[arg(
226        long = "full-auto",
227        global = true,
228        help = "Run non-interactively with full-auto permission review",
229        long_help = r#"Run non-interactively on top of the active primary agent.
230
231If no primary agent is explicitly selected or configured, VT Code selects the effective `auto` primary agent. Explicit choices, including `duck`, are honoured. Full-auto is an execution and permission layer, not a primary agent.
232
233Full-auto does not override explicit denies or grant tools outside `[automation.full_auto].allowed_tools`. Tools outside that allow-list are denied. Promptable actions inside the allow-list are routed through automatic permission review after deny and policy checks instead of asking. The run fails fast if it needs the defaulted `auto` primary agent and no effective `auto` exists."#,
234        value_name = "PROMPT",
235        num_args = 0..=1,
236        default_missing_value = "",
237        value_hint = ValueHint::Other
238    )]
239    pub full_auto: Option<String>,
240
241    /// Resume a previous conversation (use without ID for interactive picker)
242    #[arg(
243        short = 'r',
244        long = "resume",
245        global = true,
246        value_name = "SESSION_ID",
247        num_args = 0..=1,
248        default_missing_value = "__interactive__",
249        conflicts_with_all = ["continue_latest", "full_auto"]
250    )]
251    pub resume_session: Option<String>,
252
253    /// Continue the most recent conversation automatically
254    #[arg(
255        long = "continue",
256        visible_alias = "continue-session",
257        global = true,
258        conflicts_with_all = ["resume_session", "full_auto"]
259    )]
260    pub continue_latest: bool,
261
262    /// Fork an existing session with a new session ID
263    #[arg(
264        long = "fork-session",
265        global = true,
266        value_name = "SESSION_ID",
267        conflicts_with_all = ["resume_session", "continue_latest", "full_auto"]
268    )]
269    pub fork_session: Option<String>,
270
271    /// Show archived sessions from every workspace when resuming or forking
272    #[arg(long, global = true)]
273    pub all: bool,
274
275    /// Custom suffix for session identifier (alphanumeric, dash, underscore only, max 64 chars)
276    #[arg(long = "session-id", global = true, value_name = "CUSTOM_SUFFIX")]
277    pub session_id: Option<String>,
278
279    /// Use summarized history when forking a session
280    #[arg(long, global = true)]
281    pub summarize: bool,
282
283    /// Override the default agent model for this session
284    #[arg(long, global = true, value_name = "AGENT")]
285    pub agent: Option<String>,
286
287    /// Tools that execute without prompting (comma-separated, supports patterns like "Bash(git:*)")
288    #[arg(long = "allowed-tools", global = true, value_name = "TOOLS", action = ArgAction::Append)]
289    pub allowed_tools: Vec<String>,
290
291    /// Tools that cannot be used by the agent
292    #[arg(long = "disallowed-tools", global = true, value_name = "TOOLS", action = ArgAction::Append)]
293    pub disallowed_tools: Vec<String>,
294
295    /// Auto-approve promptable actions while respecting denies and policy blocks
296    #[arg(
297        long = "dangerously-skip-permissions",
298        global = true,
299        help = "Auto-approve promptable actions while respecting denies and policy blocks",
300        long_help = "Auto-approve promptable actions while still respecting explicit denies and policy blocks."
301    )]
302    pub dangerously_skip_permissions: bool,
303
304    /// Explicitly connect to IDE on startup (auto-detects available IDEs)
305    #[arg(long, global = true)]
306    pub ide: bool,
307
308    #[command(subcommand)]
309    pub command: Option<Commands>,
310}
311
312/// Options for the `ask` command
313#[derive(Debug, Default, Clone)]
314pub struct AskCommandOptions {
315    pub output_format: Option<AskOutputFormat>,
316    pub allowed_tools: Vec<String>,
317    pub disallowed_tools: Vec<String>,
318    pub skip_confirmations: bool,
319}
320
321/// Output format options for the `ask` subcommand.
322#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
323pub enum AskOutputFormat {
324    /// Emit the response as a structured JSON document.
325    Json,
326}
327
328/// Output format options for the `schema` command.
329#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
330pub enum SchemaOutputFormat {
331    /// Emit one JSON document with all selected schemas.
332    Json,
333    /// Emit one JSON object per line.
334    Ndjson,
335}
336
337/// Documentation detail level for the `schema` command.
338#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
339pub enum SchemaMode {
340    /// Minimal descriptions and compact parameter metadata.
341    Minimal,
342    /// Balanced descriptions for agent discovery.
343    Progressive,
344    /// Full descriptions and full parameter metadata.
345    Full,
346}
347
348/// Schema-focused subcommands.
349#[derive(Subcommand, Debug, Clone)]
350pub enum SchemaCommands {
351    /// List built-in VT Code tool schemas.
352    Tools {
353        /// Documentation detail level for tool descriptions.
354        #[arg(long, value_enum, default_value_t = SchemaMode::Progressive)]
355        mode: SchemaMode,
356        /// Output format for schema payloads.
357        #[arg(long, value_enum, default_value_t = SchemaOutputFormat::Json)]
358        format: SchemaOutputFormat,
359        /// Filter by tool name (repeatable).
360        #[arg(long = "name", value_name = "TOOL")]
361        names: Vec<String>,
362    },
363}
364
365/// `exec` subcommands.
366#[derive(Subcommand, Debug, Clone)]
367pub enum ExecSubcommand {
368    /// Resume a previous exec session with a follow-up prompt
369    #[command(
370        long_about = "Resume a previous exec session with a follow-up prompt.\n\nExamples:\n  vtcode exec resume session-123 \"continue from the prior investigation\"\n  vtcode exec resume --last \"continue from the prior investigation\"\n  echo \"continue from stdin\" | vtcode exec resume --last"
371    )]
372    Resume(ExecResumeArgs),
373}
374
375/// Arguments for `vtcode exec resume`.
376#[derive(Args, Debug, Clone)]
377pub struct ExecResumeArgs {
378    /// Resume the most recent archived exec session
379    #[arg(long)]
380    pub last: bool,
381    /// Search archived exec sessions across every workspace
382    #[arg(long)]
383    pub all: bool,
384    /// Archived session identifier to resume, or the prompt when `--last` is used
385    #[arg(value_name = "SESSION_ID_OR_PROMPT", required_unless_present = "last")]
386    pub session_or_prompt: Option<String>,
387    /// Follow-up prompt to execute when resuming a specific session. Use `-` to force reading from stdin.
388    #[arg(value_name = "PROMPT")]
389    pub prompt: Option<String>,
390}
391
392/// `schedule` subcommands.
393#[derive(Subcommand, Debug, Clone)]
394pub enum ScheduleSubcommand {
395    /// Create a durable scheduled task
396    #[command(
397        long_about = "Create a durable scheduled task.\n\nExamples:\n  vtcode schedule create --prompt \"check the deployment\" --every 10m\n  vtcode schedule create --prompt \"review the nightly build\" --cron \"0 9 * * 1-5\"\n  vtcode schedule create --reminder \"push the release branch\" --at \"15:00\""
398    )]
399    Create(ScheduleCreateArgs),
400    /// List durable scheduled tasks
401    List,
402    /// Delete a durable scheduled task by id
403    Delete {
404        #[arg(value_name = "TASK_ID")]
405        id: String,
406    },
407    /// Run the local durable scheduler daemon
408    Serve,
409    /// Install the scheduler as a user service
410    #[command(name = "install-service")]
411    InstallService,
412    /// Uninstall the scheduler user service
413    #[command(name = "uninstall-service")]
414    UninstallService,
415}
416
417/// Arguments for `vtcode schedule create`.
418#[derive(Args, Debug, Clone)]
419pub struct ScheduleCreateArgs {
420    /// Optional short label for the task
421    #[arg(long, value_name = "NAME")]
422    pub name: Option<String>,
423    /// Prompt to run with `vtcode exec`
424    #[arg(long, value_name = "PROMPT", conflicts_with = "reminder")]
425    pub prompt: Option<String>,
426    /// Local reminder text to surface without invoking the model
427    #[arg(long, value_name = "TEXT", conflicts_with = "prompt")]
428    pub reminder: Option<String>,
429    /// Fixed interval such as 10m, 2h, or 1d
430    #[arg(long, value_name = "DURATION", conflicts_with_all = ["cron", "at"])]
431    pub every: Option<String>,
432    /// Five-field cron expression
433    #[arg(long, value_name = "EXPR", conflicts_with_all = ["every", "at"])]
434    pub cron: Option<String>,
435    /// One-shot local time (RFC3339, YYYY-MM-DD HH:MM, or HH:MM)
436    #[arg(long, value_name = "TIME", conflicts_with_all = ["every", "cron"])]
437    pub at: Option<String>,
438    /// Workspace to use for prompt tasks
439    #[arg(long, value_name = "PATH", value_hint = ValueHint::DirPath)]
440    pub workspace: Option<PathBuf>,
441}
442
443/// Arguments for `vtcode review`.
444#[derive(Args, Debug, Clone)]
445pub struct ReviewArgs {
446    /// Emit structured JSON events to stdout (one per line)
447    #[arg(long)]
448    pub json: bool,
449    /// Optional path to write the JSONL transcript
450    #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
451    pub events: Option<PathBuf>,
452    /// Write the last agent message to this file
453    #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
454    pub last_message_file: Option<PathBuf>,
455    /// Review the last committed diff instead of the current diff
456    #[arg(long, conflicts_with_all = ["target", "files"])]
457    pub last_diff: bool,
458    /// Review a custom git target expression
459    #[arg(long, value_name = "TARGET", conflicts_with = "files")]
460    pub target: Option<String>,
461    /// Optional review style or focus area
462    #[arg(long, value_name = "STYLE")]
463    pub style: Option<String>,
464    /// Review specific files instead of a diff target (repeatable)
465    #[arg(
466        long = "file",
467        value_name = "FILE",
468        value_hint = ValueHint::FilePath,
469        conflicts_with_all = ["last_diff", "target"]
470    )]
471    pub files: Vec<PathBuf>,
472}
473
474#[derive(Args, Debug, Clone)]
475pub struct BackgroundSubagentArgs {
476    #[arg(long = "agent-name", value_name = "NAME")]
477    pub agent_name: String,
478    #[arg(long = "parent-session-id", value_name = "SESSION_ID")]
479    pub parent_session_id: String,
480    #[arg(long = "session-id", value_name = "SESSION_ID")]
481    pub session_id: String,
482    #[arg(long = "prompt", value_name = "PROMPT")]
483    pub prompt: String,
484    #[arg(long = "max-turns", value_name = "COUNT")]
485    pub max_turns: Option<usize>,
486    #[arg(long = "model-override", value_name = "MODEL")]
487    pub model_override: Option<String>,
488    #[arg(long = "reasoning-override", value_name = "LEVEL")]
489    pub reasoning_override: Option<String>,
490}
491
492/// Available commands
493#[derive(Subcommand, Debug, Clone)]
494pub enum Commands {
495    /// Start Agent Client Protocol bridge for IDE integrations
496    #[command(name = "acp")]
497    AgentClientProtocol {
498        /// Client to connect over ACP
499        #[arg(value_enum, default_value_t = AgentClientProtocolTarget::Zed)]
500        target: AgentClientProtocolTarget,
501    },
502
503    /// Interactive AI coding assistant
504    Chat,
505
506    /// Single prompt mode - prints model reply without tools
507    ///
508    /// Send a single prompt to the model and print the response. No tools are
509    /// invoked, no session is created, and the process exits after replying.
510    ///
511    /// Examples:
512    ///   vtcode ask "what is a monad?"
513    ///   echo "summarize this" | vtcode ask
514    ///   vtcode ask --output-format json "explain ownership in Rust"
515    Ask {
516        /// Prompt to ask. Use `-` to force reading from stdin.
517        #[arg(
518            value_name = "PROMPT",
519            long_help = "The prompt to send to the model.\n\nOmit to read from stdin (piped input).\nUse '-' to explicitly force reading from stdin."
520        )]
521        prompt: Option<String>,
522        /// Format the response using a structured representation.
523        #[arg(
524            long = "output-format",
525            value_enum,
526            value_name = "FORMAT",
527            long_help = "Output format for the response.\n\nCurrently supports:\n  json - Emit the response as a structured JSON document."
528        )]
529        output_format: Option<AskOutputFormat>,
530    },
531    /// Headless execution mode
532    ///
533    /// Run the agent in non-interactive mode. The agent executes the prompt,
534    /// runs tools, and exits when done. Ideal for CI/CD, scripting, and
535    /// agent-to-agent workflows.
536    ///
537    /// Examples:
538    ///   vtcode exec "explain this codebase"
539    ///   vtcode exec --json "fix the failing test"
540    ///   vtcode exec --dry-run "refactor auth module"
541    ///   cat file.rs | vtcode exec "review this code"
542    ///   vtcode exec resume --last
543    Exec {
544        /// Emit structured JSON events to stdout (one per line)
545        #[arg(
546            long,
547            long_help = "Stream newline-delimited JSON events to stdout.\nEach line is a JSON object representing an agent event (tool call, message, etc.).\nUseful for programmatic consumption and CI integration."
548        )]
549        json: bool,
550        /// Run a read-only dry-run execution (blocks mutating tool calls)
551        #[arg(
552            long,
553            long_help = "Simulate execution without making changes.\nThe agent plans tool calls but does not execute mutating operations (file writes, shell commands).\nUseful for previewing what the agent would do."
554        )]
555        dry_run: bool,
556        /// Optional path to write the JSONL transcript
557        #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath, long_help = "Write the full JSONL event transcript to this file.\nIncludes all agent events: tool calls, messages, errors, and metadata.")]
558        events: Option<PathBuf>,
559        /// Write the last agent message to this file
560        #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath, long_help = "Write only the final agent message to this file.\nUseful for piping the agent's response into other tools.")]
561        last_message_file: Option<PathBuf>,
562        /// Optional exec subcommand
563        #[command(subcommand)]
564        command: Option<ExecSubcommand>,
565        /// Prompt to execute. Use `-` to force reading from stdin.
566        #[arg(
567            value_name = "PROMPT",
568            long_help = "The prompt to execute.\n\nOmit to read from stdin (piped input).\nUse '-' to explicitly force reading from stdin.\nQuote multi-word prompts: vtcode exec \"fix the bug in auth.rs\""
569        )]
570        prompt: Option<String>,
571    },
572    /// Manage durable scheduled tasks
573    ///
574    /// Create, list, and delete scheduled tasks that run on a recurring or
575    /// one-shot basis. Tasks are stored persistently and survive restarts
576    /// when paired with `vtcode schedule install-service`.
577    ///
578    /// Examples:
579    ///   vtcode schedule create --name "daily-review" --cron "0 9 * * 1-5" --prompt "review recent changes"
580    ///   vtcode schedule create --name "reminder" --reminder "standup in 10 minutes" --at "09:50"
581    ///   vtcode schedule list
582    ///   vtcode schedule delete `<task-id>`
583    Schedule {
584        #[command(subcommand)]
585        command: ScheduleSubcommand,
586    },
587
588    /// Internal VT Code background subagent runner
589    #[command(name = "background-subagent", hide = true)]
590    BackgroundSubagent(BackgroundSubagentArgs),
591
592    /// Headless code review for the current diff, selected files, or a custom git target
593    #[command(
594        long_about = "Run a non-interactive code review.\n\nExamples:\n  vtcode review\n  vtcode review --last-diff\n  vtcode review --target HEAD~1..HEAD\n  vtcode review --file src/main.rs --file vtcode-core/src/lib.rs\n  vtcode review --style security"
595    )]
596    Review(ReviewArgs),
597
598    /// Runtime schema introspection for built-in tools
599    Schema {
600        #[command(subcommand)]
601        command: SchemaCommands,
602    },
603
604    /// Verbose interactive chat with debug output
605    ChatVerbose,
606
607    /// Analyze workspace (structure, security, performance)
608    Analyze {
609        /// Type of analysis to perform
610        #[arg(value_name = "TYPE", default_value = "full")]
611        analysis_type: String,
612    },
613
614    /// Pretty-print trajectory logs
615    #[command(name = "trajectory")]
616    Trajectory {
617        /// Optional path to trajectory JSONL file
618        #[arg(long)]
619        file: Option<PathBuf>,
620        /// Number of top entries to show
621        #[arg(long, default_value_t = 10)]
622        top: usize,
623    },
624
625    /// Send a VT Code notification using the built-in notification system
626    Notify {
627        /// Optional notification title
628        #[arg(long, value_name = "TITLE")]
629        title: Option<String>,
630        /// Notification message
631        #[arg(value_name = "MESSAGE")]
632        message: String,
633    },
634
635    /// Benchmark against SWE-bench evaluation framework
636    Benchmark {
637        /// Path to a JSON benchmark specification
638        #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
639        task_file: Option<PathBuf>,
640        /// Inline JSON specification for quick experiments
641        #[arg(long, value_name = "JSON")]
642        task: Option<String>,
643        /// Optional path to write the structured benchmark report
644        #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
645        output: Option<PathBuf>,
646        /// Limit the number of tasks executed
647        #[arg(long, value_name = "COUNT")]
648        max_tasks: Option<usize>,
649    },
650
651    /// Create complete Rust project
652    CreateProject {
653        name: String,
654        #[arg(long = "feature", value_name = "FEATURE", action = ArgAction::Append)]
655        features: Vec<String>,
656    },
657
658    /// Revert agent to a previous snapshot
659    Revert {
660        /// Turn number to revert to
661        #[arg(short, long)]
662        turn: usize,
663        /// Scope of revert operation: conversation, code, full
664        #[arg(long)]
665        partial: Option<String>,
666    },
667
668    /// List all available snapshots
669    Snapshots,
670
671    /// Clean up old snapshots
672    ///
673    /// Features:
674    ///   • Remove snapshots beyond limit
675    ///   • Configurable retention policy
676    ///   • Safe deletion with confirmation
677    ///
678    /// Examples:
679    ///   vtcode cleanup-snapshots
680    ///   vtcode cleanup-snapshots --max 20
681    #[command(name = "cleanup-snapshots")]
682    CleanupSnapshots {
683        /// Maximum number of snapshots to keep
684        ///
685        /// Default: 50
686        /// Example: --max 20
687        #[arg(short, long, default_value_t = 50)]
688        max: usize,
689    },
690
691    /// Initialize project guidance and workspace scaffolding
692    ///
693    /// Bootstrap a workspace for use with VT Code. Creates vtcode.toml,
694    /// AGENTS.md, and other scaffolding. Run this once per project.
695    ///
696    /// Examples:
697    ///   vtcode init
698    ///   vtcode init --force
699    Init {
700        /// Overwrite an existing AGENTS.md without prompting
701        #[arg(
702            long,
703            short = 'f',
704            long_help = "Overwrite AGENTS.md without confirmation.\nUse this in CI/CD or scripts where interactive prompts are not possible."
705        )]
706        force: bool,
707    },
708
709    /// Initialize project in ~/.vtcode/projects/
710    ///
711    /// Create a new project entry in the VT Code projects directory.
712    /// This is separate from `vtcode init` which bootstraps a workspace.
713    ///
714    /// Examples:
715    ///   vtcode init-project
716    ///   vtcode init-project --name my-project
717    ///   vtcode init-project --force --migrate
718    #[command(name = "init-project")]
719    InitProject {
720        /// Project name - defaults to current directory name
721        #[arg(
722            long,
723            long_help = "Name for the project.\nDefaults to the current directory name if not specified."
724        )]
725        name: Option<String>,
726        /// Force initialization - overwrite existing project structure
727        #[arg(
728            long,
729            long_help = "Overwrite existing project structure without confirmation."
730        )]
731        force: bool,
732        /// Migrate existing files - move existing config/cache files to new structure
733        #[arg(
734            long,
735            long_help = "Move existing config and cache files into the new project structure."
736        )]
737        migrate: bool,
738    },
739
740    /// Generate configuration file
741    ///
742    /// Create a vtcode.toml configuration file with default settings.
743    /// Use --global to create in ~/.vtcode/ or specify an output path.
744    ///
745    /// Examples:
746    ///   vtcode config
747    ///   vtcode config --global
748    ///   vtcode config --output ./my-vtcode.toml
749    Config {
750        /// Output file path
751        #[arg(
752            long,
753            long_help = "Write the configuration to this path.\nDefaults to ./vtcode.toml in the current directory."
754        )]
755        output: Option<PathBuf>,
756        /// Create in user home directory (~/.vtcode/vtcode.toml)
757        #[arg(
758            long,
759            long_help = "Write the configuration to ~/.vtcode/vtcode.toml.\nThis sets global defaults for all workspaces."
760        )]
761        global: bool,
762    },
763
764    /// Authenticate with a supported provider
765    ///
766    /// Start an OAuth or API-key login flow for the given provider.
767    /// Credentials are stored securely in the OS keychain.
768    ///
769    /// Examples:
770    ///   vtcode login openai
771    ///   vtcode login openrouter
772    ///   vtcode login codex
773    ///   vtcode login codex --device-code
774    Login {
775        /// Provider name (`openai`, `openrouter`, `copilot`, or `codex`)
776        #[arg(
777            long_help = "The provider to authenticate with.\nSupported: openai, openrouter, copilot, codex"
778        )]
779        provider: String,
780        /// Use device-code login when the provider supports it (currently `codex` only)
781        #[arg(
782            long,
783            default_value_t = false,
784            long_help = "Use the device-code OAuth flow.\nCurrently supported only for the `codex` provider.\nOpens a browser URL and asks you to enter a code."
785        )]
786        device_code: bool,
787    },
788
789    /// Clear stored authentication credentials for a provider
790    ///
791    /// Remove stored OAuth tokens or API keys for the given provider.
792    ///
793    /// Examples:
794    ///   vtcode logout openai
795    ///   vtcode logout openrouter
796    Logout {
797        /// Provider name (`openai`, `openrouter`, `copilot`, or `codex`)
798        #[arg(
799            long_help = "The provider to deauthenticate.\nSupported: openai, openrouter, copilot, codex"
800        )]
801        provider: String,
802    },
803
804    /// Show authentication status for one provider or all supported providers
805    ///
806    /// Display whether each provider is authenticated, which credential type
807    /// is in use, and token/session metadata when available.
808    ///
809    /// Examples:
810    ///   vtcode auth
811    ///   vtcode auth openai
812    ///   vtcode auth openrouter
813    Auth {
814        /// Optional provider name (`openai`, `openrouter`, `copilot`, or `codex`)
815        #[arg(
816            long_help = "Show status for a single provider.\nOmit to show status for all supported providers."
817        )]
818        provider: Option<String>,
819    },
820
821    /// Manage tool execution policies
822    #[command(name = "tool-policy")]
823    ToolPolicy {
824        #[command(subcommand)]
825        command: crate::cli::tool_policy_commands::ToolPolicyCommands,
826    },
827
828    /// Manage Model Context Protocol providers
829    #[command(name = "mcp")]
830    Mcp {
831        #[command(subcommand)]
832        command: crate::mcp::cli::McpCommands,
833    },
834
835    /// Agent2Agent (A2A) Protocol
836    #[command(name = "a2a")]
837    A2a {
838        #[command(subcommand)]
839        command: super::super::a2a::cli::A2aCommands,
840    },
841
842    /// Proxy to the official Codex app-server
843    #[command(name = "app-server")]
844    AppServer {
845        /// Transport listen target passed through to `codex app-server`
846        #[arg(long, default_value = "stdio://")]
847        listen: String,
848    },
849
850    /// Manage models and providers
851    Models {
852        #[command(subcommand)]
853        command: ModelCommands,
854    },
855
856    /// Manage GPU pod deployments
857    #[command(name = "pods")]
858    Pods {
859        #[command(subcommand)]
860        command: PodsCommands,
861    },
862
863    /// Generate or display man pages
864    Man {
865        /// Command name to generate man page for (optional)
866        command: Option<String>,
867        /// Output file path to save man page
868        #[arg(short, long)]
869        output: Option<PathBuf>,
870    },
871
872    /// Manage Agent Skills
873    ///
874    /// Skills are reusable instruction sets that extend the agent's capabilities.
875    /// Each skill is a directory containing a SKILL.md manifest and optional scripts.
876    ///
877    /// Examples:
878    ///   vtcode skills list
879    ///   vtcode skills create my-skill
880    ///   vtcode skills load my-skill
881    ///   vtcode skills info my-skill
882    ///   vtcode skills validate ./path/to/skill
883    #[command(subcommand)]
884    Skills(SkillsSubcommand),
885
886    /// List available skills (alias for `vtcode skills list`)
887    #[command(name = "list-skills", hide = true)]
888    ListSkills {},
889
890    /// Manage optional VT Code dependencies
891    ///
892    /// Install, update, or check the status of optional tools that VT Code
893    /// can use (ripgrep, ast-grep, search-tools bundle).
894    ///
895    /// Examples:
896    ///   vtcode dependencies status
897    ///   vtcode dependencies install search-tools
898    ///   vtcode deps install ripgrep
899    #[command(name = "dependencies", visible_alias = "deps", subcommand)]
900    Dependencies(DependenciesSubcommand),
901
902    /// Run built-in repository checks
903    ///
904    /// Execute repository-level checks such as ast-grep rule tests and scans.
905    ///
906    /// Examples:
907    ///   vtcode check ast-grep
908    Check {
909        #[command(subcommand)]
910        command: CheckSubcommand,
911    },
912
913    /// Check for and install binary updates from GitHub Releases
914    ///
915    /// Manage VT Code binary updates. By default checks for a new version
916    /// and offers to install it. Use flags to customize behavior.
917    ///
918    /// Examples:
919    ///   vtcode update
920    ///   vtcode update --check
921    ///   vtcode update --force
922    ///   vtcode update --list
923    ///   vtcode update --pin 0.120.0
924    ///   vtcode update --unpin
925    ///   vtcode update --channel beta
926    #[command(name = "update")]
927    Update {
928        /// Check for updates without installing
929        #[arg(
930            long,
931            long_help = "Check whether a newer version is available without installing it."
932        )]
933        check: bool,
934        /// Force update even if on latest version
935        #[arg(
936            long,
937            long_help = "Reinstall or downgrade even if the current version is already the latest."
938        )]
939        force: bool,
940        /// List available versions
941        #[arg(
942            long,
943            long_help = "Print available release versions from GitHub and exit."
944        )]
945        list: bool,
946        /// Number of versions to list (default: 10)
947        #[arg(
948            long,
949            default_value_t = 10,
950            long_help = "Maximum number of versions to display with --list."
951        )]
952        limit: usize,
953        /// Pin to a specific version
954        #[arg(
955            long,
956            value_name = "VERSION",
957            long_help = "Pin the binary to a specific version.\nAuto-updates are disabled until --unpin is used."
958        )]
959        pin: Option<String>,
960        /// Unpin version
961        #[arg(
962            long,
963            long_help = "Remove a previously set version pin and resume auto-updates."
964        )]
965        unpin: bool,
966        /// Set release channel (stable, beta, nightly)
967        #[arg(
968            long,
969            value_name = "CHANNEL",
970            long_help = "Switch the release channel.\nAccepted values: stable, beta, nightly."
971        )]
972        channel: Option<String>,
973        /// Show current update configuration
974        #[arg(
975            long,
976            long_help = "Display the current update configuration (channel, pin, intervals) and exit."
977        )]
978        show_config: bool,
979    },
980
981    /// Start Anthropic API compatibility server
982    #[command(name = "anthropic-api")]
983    AnthropicApi {
984        /// Port to run the server on
985        #[arg(long, default_value = "11434")]
986        port: u16,
987        /// Host address to bind to
988        #[arg(long, default_value = "127.0.0.1")]
989        host: String,
990    },
991}
992
993/// Supported Agent Client Protocol clients
994#[derive(Clone, Copy, Debug, ValueEnum)]
995pub enum AgentClientProtocolTarget {
996    /// Agent Client Protocol client (legacy Zed identifier)
997    Zed,
998    /// Standard Agent Client Protocol client
999    Standard,
1000}
1001
1002/// Model management commands with concise, actionable help
1003#[derive(Subcommand, Debug, Clone)]
1004pub enum ModelCommands {
1005    /// List all providers and models with status indicators
1006    List,
1007
1008    /// Set default provider (gemini, openai, anthropic, deepseek)
1009    #[command(name = "set-provider")]
1010    SetProvider {
1011        /// Provider name to set as default
1012        provider: String,
1013    },
1014
1015    /// Set default model (e.g., deepseek-reasoner, gpt-5, claude-sonnet-4-6)
1016    #[command(name = "set-model")]
1017    SetModel {
1018        /// Model name to set as default
1019        model: String,
1020    },
1021
1022    /// Configure provider settings (API keys, base URLs, models)
1023    Config {
1024        /// Provider name to configure
1025        provider: String,
1026
1027        /// API key for the provider
1028        #[arg(long)]
1029        api_key: Option<String>,
1030
1031        /// Base URL for local providers
1032        #[arg(long)]
1033        base_url: Option<String>,
1034
1035        /// Default model for this provider
1036        #[arg(long)]
1037        model: Option<String>,
1038    },
1039
1040    /// Test provider connectivity and validate configuration
1041    Test {
1042        /// Provider name to test
1043        provider: String,
1044    },
1045
1046    /// Compare model performance across providers (coming soon)
1047    Compare,
1048
1049    /// Show detailed model information and specifications
1050    Info {
1051        /// Model name to get information about
1052        model: String,
1053    },
1054}
1055
1056/// GPU pod management commands.
1057#[derive(Subcommand, Debug, Clone)]
1058pub enum PodsCommands {
1059    /// Start a model on the active pod.
1060    Start {
1061        /// Local model name used for lookup and storage.
1062        #[arg(long)]
1063        name: String,
1064        /// Hugging Face or provider model identifier to launch.
1065        #[arg(long)]
1066        model: String,
1067        /// Optional explicit pod name to store as the active pod.
1068        #[arg(long = "pod-name")]
1069        pod_name: Option<String>,
1070        /// SSH connection string used for the pod.
1071        #[arg(long)]
1072        ssh: Option<String>,
1073        /// GPU identifiers on the pod, repeated as `ID:NAME`.
1074        #[arg(long = "gpu", value_name = "ID:NAME", action = ArgAction::Append)]
1075        gpus: Vec<String>,
1076        /// Optional remote models directory.
1077        #[arg(long = "models-path")]
1078        models_path: Option<String>,
1079        /// Optional exact profile name to use.
1080        #[arg(long)]
1081        profile: Option<String>,
1082        /// Optional requested GPU count.
1083        #[arg(long = "gpus")]
1084        gpus_count: Option<usize>,
1085        /// Optional override for `--gpu-memory-utilization` (percent).
1086        #[arg(long)]
1087        memory: Option<f32>,
1088        /// Optional override for `--max-model-len` (e.g. 4k, 32k, 131072).
1089        #[arg(long)]
1090        context: Option<String>,
1091    },
1092
1093    /// Stop a running model on the active pod.
1094    Stop {
1095        /// Local model name to stop.
1096        #[arg(long)]
1097        name: String,
1098    },
1099
1100    /// Stop every running model on the active pod.
1101    StopAll,
1102
1103    /// List running models on the active pod.
1104    List,
1105
1106    /// Stream logs for a running model on the active pod.
1107    Logs {
1108        /// Local model name whose logs should be streamed.
1109        #[arg(long)]
1110        name: String,
1111    },
1112
1113    /// Show compatible and incompatible known models for the active pod.
1114    KnownModels,
1115}
1116
1117/// Skills subcommands
1118#[derive(Debug, Subcommand, Clone)]
1119pub enum SkillsSubcommand {
1120    /// List available skills
1121    #[command(name = "list")]
1122    List {
1123        /// Show all skills including system skills
1124        #[arg(long)]
1125        all: bool,
1126    },
1127
1128    /// Load a skill for use in agent session
1129    #[command(name = "load")]
1130    Load {
1131        /// Skill name to load
1132        name: String,
1133        /// Optional path to skill directory
1134        #[arg(long)]
1135        path: Option<PathBuf>,
1136    },
1137
1138    /// Unload a skill from session
1139    #[command(name = "unload")]
1140    Unload {
1141        /// Skill name to unload
1142        name: String,
1143    },
1144
1145    /// Show skill details and instructions
1146    #[command(name = "info")]
1147    Info {
1148        /// Skill name to get information about
1149        name: String,
1150    },
1151
1152    /// Create a new skill from template
1153    #[command(name = "create")]
1154    Create {
1155        /// Path for new skill directory
1156        path: PathBuf,
1157        /// Optional template to use
1158        #[arg(long)]
1159        template: Option<String>,
1160    },
1161
1162    /// Validate SKILL.md manifest
1163    #[command(name = "validate")]
1164    Validate {
1165        /// Path to skill directory or SKILL.md file
1166        path: PathBuf,
1167        /// Enable strict validation (warnings become errors for routing quality checks)
1168        #[arg(long)]
1169        strict: bool,
1170    },
1171
1172    /// Validate all skills for container skills compatibility
1173    #[command(name = "check-compatibility")]
1174    CheckCompatibility,
1175
1176    /// Show skill configuration and search paths
1177    #[command(name = "config")]
1178    Config,
1179
1180    /// Regenerate skills index file
1181    #[command(name = "regenerate-index")]
1182    RegenerateIndex,
1183
1184    /// skills-ref compatible commands (agentskills.io spec)
1185    #[command(name = "skills-ref", subcommand)]
1186    SkillsRef(SkillsRefSubcommand),
1187}
1188
1189/// skills-ref compatible subcommands per agentskills.io specification
1190#[derive(Debug, Subcommand, Clone)]
1191pub enum SkillsRefSubcommand {
1192    /// Validate a skill directory
1193    #[command(name = "validate")]
1194    Validate {
1195        /// Path to skill directory
1196        path: PathBuf,
1197    },
1198
1199    /// Generate <available_skills> XML for agent prompts
1200    #[command(name = "to-prompt")]
1201    ToPrompt {
1202        /// Paths to skill directories
1203        paths: Vec<PathBuf>,
1204    },
1205
1206    /// List discovered skills
1207    #[command(name = "list")]
1208    List {
1209        /// Optional path to search (defaults to current directory)
1210        path: Option<PathBuf>,
1211    },
1212}
1213
1214/// Optional VT Code dependency names
1215#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
1216pub enum ManagedDependency {
1217    #[value(name = "search-tools")]
1218    SearchTools,
1219    #[value(name = "ripgrep")]
1220    Ripgrep,
1221    #[value(name = "ast-grep")]
1222    AstGrep,
1223}
1224
1225/// Dependency management subcommands
1226#[derive(Debug, Subcommand, Clone)]
1227pub enum DependenciesSubcommand {
1228    /// Install or update an optional dependency
1229    #[command(name = "install")]
1230    Install {
1231        /// Dependency to install
1232        dependency: ManagedDependency,
1233    },
1234
1235    /// Show current status for an optional dependency
1236    #[command(name = "status")]
1237    Status {
1238        /// Dependency to inspect
1239        dependency: ManagedDependency,
1240    },
1241}
1242
1243/// Built-in repository checks
1244#[derive(Debug, Subcommand, Clone, PartialEq, Eq)]
1245pub enum CheckSubcommand {
1246    /// Run ast-grep rule tests and scan for the current workspace
1247    #[command(name = "ast-grep")]
1248    AstGrep,
1249}
1250
1251/// Configuration file structure with latest features
1252#[derive(Debug)]
1253pub struct ConfigFile {
1254    pub model: Option<String>,
1255    pub provider: Option<String>,
1256    pub api_key_env: Option<String>,
1257    pub verbose: Option<bool>,
1258    pub log_level: Option<String>,
1259    pub workspace: Option<PathBuf>,
1260    pub tools: Option<ToolConfig>,
1261    pub context: Option<ContextConfig>,
1262    pub logging: Option<LoggingConfig>,
1263    pub performance: Option<PerformanceConfig>,
1264    pub security: Option<SecurityConfig>,
1265}
1266
1267/// Tool configuration from config file
1268#[derive(Debug, serde::Deserialize)]
1269pub struct ToolConfig {
1270    pub enable_validation: Option<bool>,
1271    pub max_execution_time_seconds: Option<u64>,
1272    pub allow_file_creation: Option<bool>,
1273    pub allow_file_deletion: Option<bool>,
1274}
1275
1276/// Context management configuration
1277#[derive(Debug, serde::Deserialize)]
1278pub struct ContextConfig {
1279    pub max_context_length: Option<usize>,
1280}
1281
1282/// Logging configuration
1283#[derive(Debug, serde::Deserialize)]
1284pub struct LoggingConfig {
1285    pub file_logging: Option<bool>,
1286    pub log_directory: Option<String>,
1287    pub max_log_files: Option<usize>,
1288    pub max_log_size_mb: Option<usize>,
1289}
1290
1291#[cfg(test)]
1292mod exec_command_tests {
1293    use super::{
1294        CheckSubcommand, Cli, Commands, DependenciesSubcommand, ExecSubcommand, ManagedDependency,
1295        PodsCommands,
1296    };
1297    use clap::{CommandFactory, Parser};
1298    use std::path::PathBuf;
1299
1300    #[test]
1301    fn exec_shorthand_preserves_prompt() {
1302        let cli = Cli::parse_from(["vtcode", "exec", "count files"]);
1303        let Some(Commands::Exec {
1304            command, prompt, ..
1305        }) = cli.command
1306        else {
1307            panic!("expected exec command");
1308        };
1309
1310        assert!(command.is_none());
1311        assert_eq!(prompt.as_deref(), Some("count files"));
1312    }
1313
1314    #[test]
1315    fn exec_resume_parses_specific_session_and_prompt() {
1316        let cli = Cli::parse_from(["vtcode", "exec", "resume", "session-123", "follow up"]);
1317        let Some(Commands::Exec {
1318            command: Some(ExecSubcommand::Resume(resume)),
1319            prompt,
1320            ..
1321        }) = cli.command
1322        else {
1323            panic!("expected exec resume command");
1324        };
1325
1326        assert!(prompt.is_none());
1327        assert!(!resume.last);
1328        assert_eq!(resume.session_or_prompt.as_deref(), Some("session-123"));
1329        assert_eq!(resume.prompt.as_deref(), Some("follow up"));
1330    }
1331
1332    #[test]
1333    fn exec_resume_parses_last_flag() {
1334        let cli = Cli::parse_from(["vtcode", "exec", "resume", "--last", "continue"]);
1335        let Some(Commands::Exec {
1336            command: Some(ExecSubcommand::Resume(resume)),
1337            ..
1338        }) = cli.command
1339        else {
1340            panic!("expected exec resume command");
1341        };
1342
1343        assert!(resume.last);
1344        assert_eq!(resume.session_or_prompt.as_deref(), Some("continue"));
1345        assert!(resume.prompt.is_none());
1346    }
1347
1348    #[test]
1349    fn exec_resume_parses_all_flag() {
1350        let cli = Cli::parse_from(["vtcode", "exec", "resume", "--last", "--all", "continue"]);
1351        let Some(Commands::Exec {
1352            command: Some(ExecSubcommand::Resume(resume)),
1353            ..
1354        }) = cli.command
1355        else {
1356            panic!("expected exec resume command");
1357        };
1358
1359        assert!(resume.last);
1360        assert!(resume.all);
1361        assert_eq!(resume.session_or_prompt.as_deref(), Some("continue"));
1362    }
1363
1364    #[test]
1365    fn exec_resume_allows_last_without_positional_for_stdin_prompt() {
1366        let cli = Cli::parse_from(["vtcode", "exec", "resume", "--last"]);
1367        let Some(Commands::Exec {
1368            command: Some(ExecSubcommand::Resume(resume)),
1369            ..
1370        }) = cli.command
1371        else {
1372            panic!("expected exec resume command");
1373        };
1374
1375        assert!(resume.last);
1376        assert!(resume.session_or_prompt.is_none());
1377        assert!(resume.prompt.is_none());
1378    }
1379
1380    #[test]
1381    fn global_resume_and_continue_parse_all_flag() {
1382        let resume_cli = Cli::parse_from(["vtcode", "--resume", "session-123", "--all"]);
1383        assert_eq!(resume_cli.resume_session.as_deref(), Some("session-123"));
1384        assert!(resume_cli.all);
1385
1386        let continue_cli = Cli::parse_from(["vtcode", "--continue", "--all"]);
1387        assert!(continue_cli.continue_latest);
1388        assert!(continue_cli.all);
1389    }
1390
1391    #[test]
1392    fn global_fork_flags_parse_summarize() {
1393        let cli = Cli::parse_from(["vtcode", "--fork-session", "session-123", "--summarize"]);
1394        assert_eq!(cli.fork_session.as_deref(), Some("session-123"));
1395        assert!(cli.summarize);
1396    }
1397
1398    #[test]
1399    fn notify_parses_title_and_message() {
1400        let cli = Cli::parse_from(["vtcode", "notify", "--title", "VT Code", "Session started"]);
1401        let Some(Commands::Notify { title, message }) = cli.command else {
1402            panic!("expected notify command");
1403        };
1404
1405        assert_eq!(title.as_deref(), Some("VT Code"));
1406        assert_eq!(message, "Session started");
1407    }
1408
1409    #[test]
1410    fn review_defaults_to_current_diff() {
1411        let cli = Cli::parse_from(["vtcode", "review"]);
1412        let Some(Commands::Review(review)) = cli.command else {
1413            panic!("expected review command");
1414        };
1415
1416        assert!(!review.last_diff);
1417        assert!(review.target.is_none());
1418        assert!(review.files.is_empty());
1419        assert!(review.style.is_none());
1420    }
1421
1422    #[test]
1423    fn review_parses_target_and_style_flags() {
1424        let cli = Cli::parse_from([
1425            "vtcode",
1426            "review",
1427            "--target",
1428            "HEAD~1..HEAD",
1429            "--style",
1430            "security",
1431        ]);
1432        let Some(Commands::Review(review)) = cli.command else {
1433            panic!("expected review command");
1434        };
1435
1436        assert_eq!(review.target.as_deref(), Some("HEAD~1..HEAD"));
1437        assert_eq!(review.style.as_deref(), Some("security"));
1438        assert!(!review.last_diff);
1439    }
1440
1441    #[test]
1442    fn review_parses_files() {
1443        let cli = Cli::parse_from([
1444            "vtcode",
1445            "review",
1446            "--file",
1447            "src/main.rs",
1448            "--file",
1449            "src/lib.rs",
1450        ]);
1451        let Some(Commands::Review(review)) = cli.command else {
1452            panic!("expected review command");
1453        };
1454
1455        assert_eq!(review.files.len(), 2);
1456        assert_eq!(review.files[0], PathBuf::from("src/main.rs"));
1457        assert_eq!(review.files[1], PathBuf::from("src/lib.rs"));
1458    }
1459
1460    #[test]
1461    fn dependencies_install_parses_ast_grep() {
1462        let cli = Cli::parse_from(["vtcode", "dependencies", "install", "ast-grep"]);
1463        let Some(Commands::Dependencies(DependenciesSubcommand::Install { dependency })) =
1464            cli.command
1465        else {
1466            panic!("expected dependencies install command");
1467        };
1468
1469        assert_eq!(dependency, ManagedDependency::AstGrep);
1470    }
1471
1472    #[test]
1473    fn dependencies_install_parses_ripgrep() {
1474        let cli = Cli::parse_from(["vtcode", "dependencies", "install", "ripgrep"]);
1475        let Some(Commands::Dependencies(DependenciesSubcommand::Install { dependency })) =
1476            cli.command
1477        else {
1478            panic!("expected dependencies install command");
1479        };
1480
1481        assert_eq!(dependency, ManagedDependency::Ripgrep);
1482    }
1483
1484    #[test]
1485    fn dependencies_install_parses_search_tools() {
1486        let cli = Cli::parse_from(["vtcode", "dependencies", "install", "search-tools"]);
1487        let Some(Commands::Dependencies(DependenciesSubcommand::Install { dependency })) =
1488            cli.command
1489        else {
1490            panic!("expected dependencies install command");
1491        };
1492
1493        assert_eq!(dependency, ManagedDependency::SearchTools);
1494    }
1495
1496    #[test]
1497    fn deps_alias_parses_status_command() {
1498        let cli = Cli::parse_from(["vtcode", "deps", "status", "ast-grep"]);
1499        let Some(Commands::Dependencies(DependenciesSubcommand::Status { dependency })) =
1500            cli.command
1501        else {
1502            panic!("expected deps status command");
1503        };
1504
1505        assert_eq!(dependency, ManagedDependency::AstGrep);
1506    }
1507
1508    #[test]
1509    fn deps_alias_parses_ripgrep_status_command() {
1510        let cli = Cli::parse_from(["vtcode", "deps", "status", "ripgrep"]);
1511        let Some(Commands::Dependencies(DependenciesSubcommand::Status { dependency })) =
1512            cli.command
1513        else {
1514            panic!("expected deps status command");
1515        };
1516
1517        assert_eq!(dependency, ManagedDependency::Ripgrep);
1518    }
1519
1520    #[test]
1521    fn deps_alias_parses_search_tools_status_command() {
1522        let cli = Cli::parse_from(["vtcode", "deps", "status", "search-tools"]);
1523        let Some(Commands::Dependencies(DependenciesSubcommand::Status { dependency })) =
1524            cli.command
1525        else {
1526            panic!("expected deps status command");
1527        };
1528
1529        assert_eq!(dependency, ManagedDependency::SearchTools);
1530    }
1531
1532    #[test]
1533    fn check_parses_ast_grep_subcommand() {
1534        let cli = Cli::parse_from(["vtcode", "check", "ast-grep"]);
1535        let Some(Commands::Check { command }) = cli.command else {
1536            panic!("expected check command");
1537        };
1538
1539        assert_eq!(command, CheckSubcommand::AstGrep);
1540    }
1541
1542    #[test]
1543    fn pods_start_parses_model_and_gpu_flags() {
1544        let cli = Cli::parse_from([
1545            "vtcode",
1546            "pods",
1547            "start",
1548            "--name",
1549            "llama",
1550            "--model",
1551            "meta-llama/Llama-3.1-8B-Instruct",
1552            "--pod-name",
1553            "gpu-box",
1554            "--ssh",
1555            "ssh root@gpu.example.com",
1556            "--gpu",
1557            "0:A100",
1558            "--gpu",
1559            "1:A100",
1560            "--gpus",
1561            "2",
1562            "--memory",
1563            "90",
1564            "--context",
1565            "32k",
1566        ]);
1567        let Some(Commands::Pods {
1568            command:
1569                PodsCommands::Start {
1570                    name,
1571                    model,
1572                    pod_name,
1573                    ssh,
1574                    gpus,
1575                    models_path,
1576                    profile,
1577                    gpus_count,
1578                    memory,
1579                    context,
1580                },
1581        }) = cli.command
1582        else {
1583            panic!("expected pods start command");
1584        };
1585
1586        assert_eq!(name, "llama");
1587        assert_eq!(model, "meta-llama/Llama-3.1-8B-Instruct");
1588        assert_eq!(pod_name.as_deref(), Some("gpu-box"));
1589        assert_eq!(ssh.as_deref(), Some("ssh root@gpu.example.com"));
1590        assert_eq!(gpus, vec!["0:A100", "1:A100"]);
1591        assert!(models_path.is_none());
1592        assert!(profile.is_none());
1593        assert_eq!(gpus_count, Some(2));
1594        assert_eq!(memory, Some(90.0));
1595        assert_eq!(context.as_deref(), Some("32k"));
1596    }
1597}
1598
1599/// Performance monitoring configuration
1600#[derive(Debug, serde::Deserialize)]
1601pub struct PerformanceConfig {
1602    pub enabled: Option<bool>,
1603    pub track_token_usage: Option<bool>,
1604    pub track_api_costs: Option<bool>,
1605    pub track_response_times: Option<bool>,
1606    pub enable_benchmarking: Option<bool>,
1607    pub metrics_retention_days: Option<usize>,
1608}
1609
1610/// Security configuration
1611#[derive(Debug, serde::Deserialize)]
1612pub struct SecurityConfig {
1613    pub level: Option<String>,
1614    pub enable_audit_logging: Option<bool>,
1615    pub enable_vulnerability_scanning: Option<bool>,
1616    pub allow_external_urls: Option<bool>,
1617    pub max_file_access_depth: Option<usize>,
1618}
1619
1620impl Default for Cli {
1621    fn default() -> Self {
1622        Self {
1623            color: ColorSelection {
1624                color: ColorChoice::Auto,
1625            },
1626            workspace_path: None,
1627            model: Some(ModelId::default().to_string()),
1628            provider: Some("gemini".to_owned()),
1629            api_key_env: "GEMINI_API_KEY".to_owned(),
1630            workspace: None,
1631            research_preview: false,
1632            security_level: "moderate".to_owned(),
1633            show_file_diffs: false,
1634            max_concurrent_ops: 5,
1635            api_rate_limit: 30,
1636            max_tool_calls: 10,
1637            verbose: false,
1638            quiet: false,
1639            config: Vec::new(),
1640            log_level: "info".to_owned(),
1641            no_color: false,
1642            theme: None,
1643            skip_confirmations: false,
1644            codex_experimental: false,
1645            no_codex_experimental: false,
1646            print: None,
1647            full_auto: None,
1648            resume_session: None,
1649            continue_latest: false,
1650            fork_session: None,
1651            all: false,
1652            session_id: None,
1653            summarize: false,
1654            debug: false,
1655            enable_skills: false,                // Skills disabled by default
1656            tick_rate: 250,                      // Default tick rate: 250ms
1657            frame_rate: 60,                      // Default frame rate: 60 FPS
1658            agent: None,                         // No agent override by default
1659            allowed_tools: Vec::new(),           // No tool restrictions by default
1660            disallowed_tools: Vec::new(),        // No tool restrictions by default
1661            dangerously_skip_permissions: false, // Safety confirmations enabled by default
1662            ide: false,                          // No auto IDE connection by default
1663            chrome: false,                       // Chrome integration disabled by default
1664            no_chrome: false,                    // Chrome integration not explicitly disabled
1665            command: Some(Commands::Chat),
1666        }
1667    }
1668}
1669
1670impl Cli {
1671    /// Get the model to use, with fallback to default
1672    pub fn get_model(&self) -> String {
1673        self.model
1674            .clone()
1675            .unwrap_or_else(|| ModelId::default().to_string())
1676    }
1677
1678    /// Load configuration from a simple TOML-like file without external deps
1679    ///
1680    /// Supported keys (top-level): model, api_key_env, verbose, log_level, workspace
1681    /// Example:
1682    ///   model = "gemini-3-flash-preview"
1683    ///   api_key_env = "GEMINI_API_KEY"
1684    ///   verbose = true
1685    ///   log_level = "info"
1686    ///   workspace = "/path/to/workspace"
1687    pub async fn load_config(&self) -> Result<ConfigFile, Box<dyn std::error::Error>> {
1688        use std::path::Path;
1689        use tokio::fs;
1690
1691        // Resolve candidate path
1692        let explicit_path = self.config.iter().find_map(|entry| {
1693            let trimmed = entry.trim();
1694            if trimmed.contains('=') || trimmed.is_empty() {
1695                None
1696            } else {
1697                Some(PathBuf::from(trimmed))
1698            }
1699        });
1700
1701        let path = if let Some(p) = explicit_path {
1702            p
1703        } else {
1704            let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
1705            let primary = cwd.join("vtcode.toml");
1706            let secondary = cwd.join(".vtcode.toml");
1707            if fs::try_exists(&primary).await.unwrap_or(false) {
1708                primary
1709            } else if fs::try_exists(&secondary).await.unwrap_or(false) {
1710                secondary
1711            } else {
1712                // No config file; return empty config
1713                return Ok(ConfigFile {
1714                    model: None,
1715                    provider: None,
1716                    api_key_env: None,
1717                    verbose: None,
1718                    log_level: None,
1719                    workspace: None,
1720                    tools: None,
1721                    context: None,
1722                    logging: None,
1723                    performance: None,
1724                    security: None,
1725                });
1726            }
1727        };
1728
1729        let text = fs::read_to_string(&path).await?;
1730
1731        // Very small parser: key = value, supports quoted strings, booleans, and plain paths
1732        let mut cfg = ConfigFile {
1733            model: None,
1734            provider: None,
1735            api_key_env: None,
1736            verbose: None,
1737            log_level: None,
1738            workspace: None,
1739            tools: None,
1740            context: None,
1741            logging: None,
1742            performance: None,
1743            security: None,
1744        };
1745
1746        for raw_line in text.lines() {
1747            let line = raw_line.trim();
1748            if line.is_empty() || line.starts_with('#') || line.starts_with("//") {
1749                continue;
1750            }
1751            // Strip inline comments after '#'
1752            let line = match line.find('#') {
1753                Some(idx) => &line[..idx],
1754                None => line,
1755            }
1756            .trim();
1757
1758            // Expect key = value
1759            let mut parts = line.splitn(2, '=');
1760            let key = parts.next().map(|s| s.trim()).unwrap_or("");
1761            let val = parts.next().map(|s| s.trim()).unwrap_or("");
1762            if key.is_empty() || val.is_empty() {
1763                continue;
1764            }
1765
1766            // Remove surrounding quotes if present
1767            let unquote = |s: &str| -> String {
1768                let s = s.trim();
1769                if (s.starts_with('"') && s.ends_with('"'))
1770                    || (s.starts_with('\'') && s.ends_with('\''))
1771                {
1772                    s[1..s.len() - 1].to_owned()
1773                } else {
1774                    s.to_owned()
1775                }
1776            };
1777
1778            match key {
1779                "model" => cfg.model = Some(unquote(val)),
1780                "api_key_env" => cfg.api_key_env = Some(unquote(val)),
1781                "verbose" => {
1782                    let v = unquote(val).to_lowercase();
1783                    cfg.verbose = Some(matches!(v.as_str(), "true" | "1" | "yes"));
1784                }
1785                "log_level" => cfg.log_level = Some(unquote(val)),
1786                "workspace" => {
1787                    let v = unquote(val);
1788                    let p = if Path::new(&v).is_absolute() {
1789                        PathBuf::from(v)
1790                    } else {
1791                        // Resolve relative to config file directory
1792                        let base = path.parent().unwrap_or(Path::new("."));
1793                        base.join(v)
1794                    };
1795                    cfg.workspace = Some(p);
1796                }
1797                _ => {
1798                    // Ignore unknown keys in this minimal parser
1799                }
1800            }
1801        }
1802
1803        Ok(cfg)
1804    }
1805
1806    /// Get the effective workspace path
1807    pub fn get_workspace(&self) -> PathBuf {
1808        self.workspace
1809            .clone()
1810            .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
1811    }
1812
1813    /// Get the effective API key environment variable
1814    ///
1815    /// Automatically infers the API key environment variable based on the provider
1816    /// when the current value matches the default or is not explicitly set.
1817    pub fn get_api_key_env(&self) -> String {
1818        crate::config::api_keys::resolve_api_key_env(
1819            self.provider
1820                .as_deref()
1821                .unwrap_or(crate::config::constants::defaults::DEFAULT_PROVIDER),
1822            &self.api_key_env,
1823        )
1824    }
1825
1826    /// Check if verbose mode is enabled
1827    pub fn is_verbose(&self) -> bool {
1828        self.verbose
1829    }
1830
1831    /// Check if performance monitoring is enabled
1832    /// Check if research-preview features are enabled
1833    pub fn is_research_preview_enabled(&self) -> bool {
1834        self.research_preview
1835    }
1836
1837    /// Get the security level
1838    pub fn get_security_level(&self) -> &str {
1839        &self.security_level
1840    }
1841
1842    /// Check if debug mode is enabled (includes verbose)
1843    pub fn is_debug_mode(&self) -> bool {
1844        self.debug || self.verbose
1845    }
1846
1847    pub fn codex_experimental_override(&self) -> Option<bool> {
1848        if self.codex_experimental {
1849            Some(true)
1850        } else if self.no_codex_experimental {
1851            Some(false)
1852        } else {
1853            None
1854        }
1855    }
1856}
1857
1858#[cfg(test)]
1859mod tests {
1860    use super::{Cli, long_version};
1861    use clap::Parser;
1862
1863    #[test]
1864    fn long_version_includes_expected_sections() {
1865        let text = long_version();
1866        assert!(text.contains("Authors:"));
1867        assert!(text.contains("Config directory:"));
1868        assert!(text.contains("Data directory:"));
1869        assert!(text.contains("VTCODE_CONFIG"));
1870        assert!(text.contains("VTCODE_DATA"));
1871    }
1872
1873    #[test]
1874    fn long_version_starts_with_build_git_info() {
1875        let text = long_version();
1876        let expected = option_env!("VT_CODE_GIT_INFO").unwrap_or(env!("CARGO_PKG_VERSION"));
1877        assert!(text.starts_with(expected));
1878    }
1879
1880    #[test]
1881    fn config_file_api_key_env_uses_provider_default() {
1882        let cli = Cli::parse_from(["vtcode", "--provider", "minimax"]);
1883
1884        assert_eq!(cli.get_api_key_env(), "MINIMAX_API_KEY");
1885    }
1886
1887    #[test]
1888    fn config_file_api_key_env_preserves_explicit_override() {
1889        let cli = Cli::parse_from([
1890            "vtcode",
1891            "--provider",
1892            "openai",
1893            "--api-key-env",
1894            "CUSTOM_OPENAI_KEY",
1895        ]);
1896
1897        assert_eq!(cli.get_api_key_env(), "CUSTOM_OPENAI_KEY");
1898    }
1899
1900    #[test]
1901    fn parses_app_server_command_with_stdio_listen_target() {
1902        let cli = Cli::parse_from(["vtcode", "app-server", "--listen", "stdio://"]);
1903
1904        assert!(matches!(
1905            cli.command,
1906            Some(super::Commands::AppServer { ref listen }) if listen == "stdio://"
1907        ));
1908    }
1909
1910    #[test]
1911    fn parses_init_force_flag() {
1912        let cli = Cli::parse_from(["vtcode", "init", "--force"]);
1913
1914        assert!(matches!(
1915            cli.command,
1916            Some(super::Commands::Init { force: true })
1917        ));
1918    }
1919
1920    #[test]
1921    fn parses_codex_login_device_code_flag() {
1922        let cli = Cli::parse_from(["vtcode", "login", "codex", "--device-code"]);
1923
1924        assert!(matches!(
1925            cli.command,
1926            Some(super::Commands::Login {
1927                ref provider,
1928                device_code: true
1929            }) if provider == "codex"
1930        ));
1931    }
1932
1933    #[test]
1934    fn parses_codex_experimental_flags() {
1935        let enabled = Cli::parse_from(["vtcode", "--codex-experimental"]);
1936        assert_eq!(enabled.codex_experimental_override(), Some(true));
1937
1938        let disabled = Cli::parse_from(["vtcode", "--no-codex-experimental"]);
1939        assert_eq!(disabled.codex_experimental_override(), Some(false));
1940    }
1941
1942    #[test]
1943    fn codex_experimental_flags_conflict() {
1944        let result =
1945            Cli::try_parse_from(["vtcode", "--codex-experimental", "--no-codex-experimental"]);
1946
1947        result.unwrap_err();
1948    }
1949
1950    #[test]
1951    fn parses_create_project_feature_flags() {
1952        let cli = Cli::parse_from([
1953            "vtcode",
1954            "create-project",
1955            "demo",
1956            "--feature",
1957            "web",
1958            "--feature",
1959            "db",
1960        ]);
1961
1962        assert!(matches!(
1963            cli.command,
1964            Some(super::Commands::CreateProject { ref name, ref features })
1965                if name == "demo" && features == &vec!["web".to_string(), "db".to_string()]
1966        ));
1967    }
1968
1969    #[test]
1970    fn parses_revert_partial_long_flag() {
1971        let cli = Cli::parse_from(["vtcode", "revert", "--turn", "3", "--partial", "code"]);
1972
1973        assert!(matches!(
1974            cli.command,
1975            Some(super::Commands::Revert {
1976                turn: 3,
1977                partial: Some(ref scope)
1978            }) if scope == "code"
1979        ));
1980    }
1981}