use crate::config::models::ModelId;
use clap::{ArgAction, Args, ColorChoice, Parser, Subcommand, ValueEnum, ValueHint};
use colorchoice_clap::Color as ColorSelection;
use std::path::PathBuf;
pub fn long_version() -> String {
use crate::config::defaults::{get_config_dir, get_data_dir};
let git_info = option_env!("VT_CODE_GIT_INFO").unwrap_or(env!("CARGO_PKG_VERSION"));
let config_dir = get_config_dir()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "~/.vtcode/".to_string());
let data_dir = get_data_dir()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "~/.vtcode/cache/".to_string());
format!(
"{}\n\nAuthors: {}\nConfig directory: {}\nData directory: {}\n\nEnvironment variables:\n VTCODE_CONFIG - Override config directory\n VTCODE_DATA - Override data directory",
git_info,
env!("CARGO_PKG_AUTHORS"),
config_dir,
data_dir
)
}
fn parse_workspace_directory(raw: &str) -> Result<PathBuf, String> {
let candidate = PathBuf::from(raw);
if !candidate.exists() {
return Err(format!(
"Workspace path does not exist: {}",
candidate.display()
));
}
if !candidate.is_dir() {
return Err(format!(
"Workspace path is not a directory: {}",
candidate.display()
));
}
Ok(candidate)
}
#[derive(Parser, Debug, Clone)]
#[command(
name = "vtcode",
version,
about = "VT Code - AI coding assistant",
color = ColorChoice::Auto
)]
pub struct Cli {
#[command(flatten)]
pub color: ColorSelection,
#[arg(
value_name = "WORKSPACE",
value_hint = ValueHint::DirPath,
value_parser = parse_workspace_directory,
global = true
)]
pub workspace_path: Option<PathBuf>,
#[arg(long, global = true)]
pub model: Option<String>,
#[arg(long, global = true)]
pub provider: Option<String>,
#[arg(long, global = true, default_value = crate::config::constants::defaults::DEFAULT_API_KEY_ENV)]
pub api_key_env: String,
#[arg(
long,
global = true,
alias = "workspace-dir",
value_name = "PATH",
value_hint = ValueHint::DirPath,
value_parser = parse_workspace_directory
)]
pub workspace: Option<PathBuf>,
#[arg(long, global = true)]
pub research_preview: bool,
#[arg(long, global = true, default_value = "moderate")]
pub security_level: String,
#[arg(long, global = true)]
pub show_file_diffs: bool,
#[arg(long, global = true, default_value_t = 5)]
pub max_concurrent_ops: usize,
#[arg(long, global = true, default_value_t = 30)]
pub api_rate_limit: usize,
#[arg(long, global = true, default_value_t = 10)]
pub max_tool_calls: usize,
#[arg(long, global = true)]
pub debug: bool,
#[arg(long, global = true)]
pub verbose: bool,
#[arg(short, long, global = true)]
pub quiet: bool,
#[arg(
short = 'c',
long = "config",
value_name = "KEY=VALUE|PATH",
action = ArgAction::Append,
global = true
)]
pub config: Vec<String>,
#[arg(long, global = true, default_value = "info")]
pub log_level: String,
#[arg(long, global = true)]
pub no_color: bool,
#[arg(long, global = true, value_name = "THEME")]
pub theme: Option<String>,
#[arg(short = 't', long, default_value_t = 250)]
pub tick_rate: u64,
#[arg(short = 'f', long, default_value_t = 60)]
pub frame_rate: u64,
#[arg(long, global = true)]
pub enable_skills: bool,
#[arg(long, global = true)]
pub chrome: bool,
#[arg(long = "no-chrome", global = true, conflicts_with = "chrome")]
pub no_chrome: bool,
#[arg(long, global = true)]
pub skip_confirmations: bool,
#[arg(
long = "codex-experimental",
global = true,
conflicts_with = "no_codex_experimental"
)]
pub codex_experimental: bool,
#[arg(
long = "no-codex-experimental",
global = true,
conflicts_with = "codex_experimental"
)]
pub no_codex_experimental: bool,
#[arg(
short = 'p',
long = "print",
value_name = "PROMPT",
value_hint = ValueHint::Other,
num_args = 0..=1,
default_missing_value = "",
global = true,
conflicts_with_all = ["full_auto"]
)]
pub print: Option<String>,
#[arg(
long = "full-auto",
global = true,
value_name = "PROMPT",
num_args = 0..=1,
default_missing_value = "",
value_hint = ValueHint::Other
)]
pub full_auto: Option<String>,
#[arg(
short = 'r',
long = "resume",
global = true,
value_name = "SESSION_ID",
num_args = 0..=1,
default_missing_value = "__interactive__",
conflicts_with_all = ["continue_latest", "full_auto"]
)]
pub resume_session: Option<String>,
#[arg(
long = "continue",
visible_alias = "continue-session",
global = true,
conflicts_with_all = ["resume_session", "full_auto"]
)]
pub continue_latest: bool,
#[arg(
long = "fork-session",
global = true,
value_name = "SESSION_ID",
conflicts_with_all = ["resume_session", "continue_latest", "full_auto"]
)]
pub fork_session: Option<String>,
#[arg(long, global = true)]
pub all: bool,
#[arg(long = "session-id", global = true, value_name = "CUSTOM_SUFFIX")]
pub session_id: Option<String>,
#[arg(long, global = true)]
pub summarize: bool,
#[arg(long, global = true, value_name = "AGENT")]
pub agent: Option<String>,
#[arg(long = "allowed-tools", global = true, value_name = "TOOLS", action = ArgAction::Append)]
pub allowed_tools: Vec<String>,
#[arg(long = "disallowed-tools", global = true, value_name = "TOOLS", action = ArgAction::Append)]
pub disallowed_tools: Vec<String>,
#[arg(long = "dangerously-skip-permissions", global = true)]
pub dangerously_skip_permissions: bool,
#[arg(long, global = true)]
pub ide: bool,
#[arg(long, global = true, value_name = "MODE")]
pub permission_mode: Option<String>,
#[command(subcommand)]
pub command: Option<Commands>,
}
#[derive(Debug, Default, Clone)]
pub struct AskCommandOptions {
pub output_format: Option<AskOutputFormat>,
pub allowed_tools: Vec<String>,
pub disallowed_tools: Vec<String>,
pub skip_confirmations: bool,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
pub enum AskOutputFormat {
Json,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
pub enum SchemaOutputFormat {
Json,
Ndjson,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
pub enum SchemaMode {
Minimal,
Progressive,
Full,
}
#[derive(Subcommand, Debug, Clone)]
pub enum SchemaCommands {
Tools {
#[arg(long, value_enum, default_value_t = SchemaMode::Progressive)]
mode: SchemaMode,
#[arg(long, value_enum, default_value_t = SchemaOutputFormat::Json)]
format: SchemaOutputFormat,
#[arg(long = "name", value_name = "TOOL")]
names: Vec<String>,
},
}
#[derive(Subcommand, Debug, Clone)]
pub enum ExecSubcommand {
#[command(
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"
)]
Resume(ExecResumeArgs),
}
#[derive(Args, Debug, Clone)]
pub struct ExecResumeArgs {
#[arg(long)]
pub last: bool,
#[arg(long)]
pub all: bool,
#[arg(value_name = "SESSION_ID_OR_PROMPT", required_unless_present = "last")]
pub session_or_prompt: Option<String>,
#[arg(value_name = "PROMPT")]
pub prompt: Option<String>,
}
#[derive(Subcommand, Debug, Clone)]
pub enum ScheduleSubcommand {
#[command(
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\""
)]
Create(ScheduleCreateArgs),
List,
Delete {
#[arg(value_name = "TASK_ID")]
id: String,
},
Serve,
#[command(name = "install-service")]
InstallService,
#[command(name = "uninstall-service")]
UninstallService,
}
#[derive(Args, Debug, Clone)]
pub struct ScheduleCreateArgs {
#[arg(long, value_name = "NAME")]
pub name: Option<String>,
#[arg(long, value_name = "PROMPT", conflicts_with = "reminder")]
pub prompt: Option<String>,
#[arg(long, value_name = "TEXT", conflicts_with = "prompt")]
pub reminder: Option<String>,
#[arg(long, value_name = "DURATION", conflicts_with_all = ["cron", "at"])]
pub every: Option<String>,
#[arg(long, value_name = "EXPR", conflicts_with_all = ["every", "at"])]
pub cron: Option<String>,
#[arg(long, value_name = "TIME", conflicts_with_all = ["every", "cron"])]
pub at: Option<String>,
#[arg(long, value_name = "PATH", value_hint = ValueHint::DirPath)]
pub workspace: Option<PathBuf>,
}
#[derive(Args, Debug, Clone)]
pub struct ReviewArgs {
#[arg(long)]
pub json: bool,
#[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
pub events: Option<PathBuf>,
#[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
pub last_message_file: Option<PathBuf>,
#[arg(long, conflicts_with_all = ["target", "files"])]
pub last_diff: bool,
#[arg(long, value_name = "TARGET", conflicts_with = "files")]
pub target: Option<String>,
#[arg(long, value_name = "STYLE")]
pub style: Option<String>,
#[arg(
long = "file",
value_name = "FILE",
value_hint = ValueHint::FilePath,
conflicts_with_all = ["last_diff", "target"]
)]
pub files: Vec<PathBuf>,
}
#[derive(Args, Debug, Clone)]
pub struct BackgroundSubagentArgs {
#[arg(long = "agent-name", value_name = "NAME")]
pub agent_name: String,
#[arg(long = "parent-session-id", value_name = "SESSION_ID")]
pub parent_session_id: String,
#[arg(long = "session-id", value_name = "SESSION_ID")]
pub session_id: String,
#[arg(long = "prompt", value_name = "PROMPT")]
pub prompt: String,
#[arg(long = "max-turns", value_name = "COUNT")]
pub max_turns: Option<usize>,
#[arg(long = "model-override", value_name = "MODEL")]
pub model_override: Option<String>,
#[arg(long = "reasoning-override", value_name = "LEVEL")]
pub reasoning_override: Option<String>,
}
#[derive(Subcommand, Debug, Clone)]
pub enum Commands {
#[command(name = "acp")]
AgentClientProtocol {
#[arg(value_enum, default_value_t = AgentClientProtocolTarget::Zed)]
target: AgentClientProtocolTarget,
},
Chat,
Ask {
#[arg(value_name = "PROMPT")]
prompt: Option<String>,
#[arg(long = "output-format", value_enum, value_name = "FORMAT")]
output_format: Option<AskOutputFormat>,
},
Exec {
#[arg(long)]
json: bool,
#[arg(long)]
dry_run: bool,
#[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
events: Option<PathBuf>,
#[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
last_message_file: Option<PathBuf>,
#[command(subcommand)]
command: Option<ExecSubcommand>,
#[arg(value_name = "PROMPT")]
prompt: Option<String>,
},
Schedule {
#[command(subcommand)]
command: ScheduleSubcommand,
},
#[command(name = "background-subagent", hide = true)]
BackgroundSubagent(BackgroundSubagentArgs),
#[command(
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"
)]
Review(ReviewArgs),
Schema {
#[command(subcommand)]
command: SchemaCommands,
},
ChatVerbose,
Analyze {
#[arg(value_name = "TYPE", default_value = "full")]
analysis_type: String,
},
#[command(name = "trajectory")]
Trajectory {
#[arg(long)]
file: Option<PathBuf>,
#[arg(long, default_value_t = 10)]
top: usize,
},
Notify {
#[arg(long, value_name = "TITLE")]
title: Option<String>,
#[arg(value_name = "MESSAGE")]
message: String,
},
Benchmark {
#[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
task_file: Option<PathBuf>,
#[arg(long, value_name = "JSON")]
task: Option<String>,
#[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
output: Option<PathBuf>,
#[arg(long, value_name = "COUNT")]
max_tasks: Option<usize>,
},
CreateProject {
name: String,
#[arg(long = "feature", value_name = "FEATURE", action = ArgAction::Append)]
features: Vec<String>,
},
Revert {
#[arg(short, long)]
turn: usize,
#[arg(long)]
partial: Option<String>,
},
Snapshots,
#[command(name = "cleanup-snapshots")]
CleanupSnapshots {
#[arg(short, long, default_value_t = 50)]
max: usize,
},
Init {
#[arg(long, short = 'f')]
force: bool,
},
#[command(name = "init-project")]
InitProject {
#[arg(long)]
name: Option<String>,
#[arg(long)]
force: bool,
#[arg(long)]
migrate: bool,
},
Config {
#[arg(long)]
output: Option<PathBuf>,
#[arg(long)]
global: bool,
},
Login {
provider: String,
#[arg(long, default_value_t = false)]
device_code: bool,
},
Logout {
provider: String,
},
Auth {
provider: Option<String>,
},
#[command(name = "tool-policy")]
ToolPolicy {
#[command(subcommand)]
command: crate::cli::tool_policy_commands::ToolPolicyCommands,
},
#[command(name = "mcp")]
Mcp {
#[command(subcommand)]
command: crate::mcp::cli::McpCommands,
},
#[command(name = "a2a")]
A2a {
#[command(subcommand)]
command: super::super::a2a::cli::A2aCommands,
},
#[command(name = "app-server")]
AppServer {
#[arg(long, default_value = "stdio://")]
listen: String,
},
Models {
#[command(subcommand)]
command: ModelCommands,
},
#[command(name = "pods")]
Pods {
#[command(subcommand)]
command: PodsCommands,
},
Man {
command: Option<String>,
#[arg(short, long)]
output: Option<PathBuf>,
},
#[command(subcommand)]
Skills(SkillsSubcommand),
#[command(name = "list-skills", hide = true)]
ListSkills {},
#[command(name = "dependencies", visible_alias = "deps", subcommand)]
Dependencies(DependenciesSubcommand),
Check {
#[command(subcommand)]
command: CheckSubcommand,
},
#[command(name = "update")]
Update {
#[arg(long)]
check: bool,
#[arg(long)]
force: bool,
#[arg(long)]
list: bool,
#[arg(long, default_value_t = 10)]
limit: usize,
#[arg(long, value_name = "VERSION")]
pin: Option<String>,
#[arg(long)]
unpin: bool,
#[arg(long, value_name = "CHANNEL")]
channel: Option<String>,
#[arg(long)]
show_config: bool,
},
#[command(name = "anthropic-api")]
AnthropicApi {
#[arg(long, default_value = "11434")]
port: u16,
#[arg(long, default_value = "127.0.0.1")]
host: String,
},
}
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum AgentClientProtocolTarget {
Zed,
Standard,
}
#[derive(Subcommand, Debug, Clone)]
pub enum ModelCommands {
List,
#[command(name = "set-provider")]
SetProvider {
provider: String,
},
#[command(name = "set-model")]
SetModel {
model: String,
},
Config {
provider: String,
#[arg(long)]
api_key: Option<String>,
#[arg(long)]
base_url: Option<String>,
#[arg(long)]
model: Option<String>,
},
Test {
provider: String,
},
Compare,
Info {
model: String,
},
}
#[derive(Subcommand, Debug, Clone)]
pub enum PodsCommands {
Start {
#[arg(long)]
name: String,
#[arg(long)]
model: String,
#[arg(long = "pod-name")]
pod_name: Option<String>,
#[arg(long)]
ssh: Option<String>,
#[arg(long = "gpu", value_name = "ID:NAME", action = ArgAction::Append)]
gpus: Vec<String>,
#[arg(long = "models-path")]
models_path: Option<String>,
#[arg(long)]
profile: Option<String>,
#[arg(long = "gpus")]
gpus_count: Option<usize>,
#[arg(long)]
memory: Option<f32>,
#[arg(long)]
context: Option<String>,
},
Stop {
#[arg(long)]
name: String,
},
StopAll,
List,
Logs {
#[arg(long)]
name: String,
},
KnownModels,
}
#[derive(Debug, Subcommand, Clone)]
pub enum SkillsSubcommand {
#[command(name = "list")]
List {
#[arg(long)]
all: bool,
},
#[command(name = "load")]
Load {
name: String,
#[arg(long)]
path: Option<PathBuf>,
},
#[command(name = "unload")]
Unload {
name: String,
},
#[command(name = "info")]
Info {
name: String,
},
#[command(name = "create")]
Create {
path: PathBuf,
#[arg(long)]
template: Option<String>,
},
#[command(name = "validate")]
Validate {
path: PathBuf,
#[arg(long)]
strict: bool,
},
#[command(name = "check-compatibility")]
CheckCompatibility,
#[command(name = "config")]
Config,
#[command(name = "regenerate-index")]
RegenerateIndex,
#[command(name = "skills-ref", subcommand)]
SkillsRef(SkillsRefSubcommand),
}
#[derive(Debug, Subcommand, Clone)]
pub enum SkillsRefSubcommand {
#[command(name = "validate")]
Validate {
path: PathBuf,
},
#[command(name = "to-prompt")]
ToPrompt {
paths: Vec<PathBuf>,
},
#[command(name = "list")]
List {
path: Option<PathBuf>,
},
}
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
pub enum ManagedDependency {
#[value(name = "search-tools")]
SearchTools,
#[value(name = "ripgrep")]
Ripgrep,
#[value(name = "ast-grep")]
AstGrep,
}
#[derive(Debug, Subcommand, Clone)]
pub enum DependenciesSubcommand {
#[command(name = "install")]
Install {
dependency: ManagedDependency,
},
#[command(name = "status")]
Status {
dependency: ManagedDependency,
},
}
#[derive(Debug, Subcommand, Clone, PartialEq, Eq)]
pub enum CheckSubcommand {
#[command(name = "ast-grep")]
AstGrep,
}
#[derive(Debug)]
pub struct ConfigFile {
pub model: Option<String>,
pub provider: Option<String>,
pub api_key_env: Option<String>,
pub verbose: Option<bool>,
pub log_level: Option<String>,
pub workspace: Option<PathBuf>,
pub tools: Option<ToolConfig>,
pub context: Option<ContextConfig>,
pub logging: Option<LoggingConfig>,
pub performance: Option<PerformanceConfig>,
pub security: Option<SecurityConfig>,
}
#[derive(Debug, serde::Deserialize)]
pub struct ToolConfig {
pub enable_validation: Option<bool>,
pub max_execution_time_seconds: Option<u64>,
pub allow_file_creation: Option<bool>,
pub allow_file_deletion: Option<bool>,
}
#[derive(Debug, serde::Deserialize)]
pub struct ContextConfig {
pub max_context_length: Option<usize>,
}
#[derive(Debug, serde::Deserialize)]
pub struct LoggingConfig {
pub file_logging: Option<bool>,
pub log_directory: Option<String>,
pub max_log_files: Option<usize>,
pub max_log_size_mb: Option<usize>,
}
#[cfg(test)]
mod exec_command_tests {
use super::{
CheckSubcommand, Cli, Commands, DependenciesSubcommand, ExecSubcommand, ManagedDependency,
PodsCommands,
};
use clap::Parser;
use std::path::PathBuf;
#[test]
fn exec_shorthand_preserves_prompt() {
let cli = Cli::parse_from(["vtcode", "exec", "count files"]);
let Some(Commands::Exec {
command, prompt, ..
}) = cli.command
else {
panic!("expected exec command");
};
assert!(command.is_none());
assert_eq!(prompt.as_deref(), Some("count files"));
}
#[test]
fn exec_resume_parses_specific_session_and_prompt() {
let cli = Cli::parse_from(["vtcode", "exec", "resume", "session-123", "follow up"]);
let Some(Commands::Exec {
command: Some(ExecSubcommand::Resume(resume)),
prompt,
..
}) = cli.command
else {
panic!("expected exec resume command");
};
assert!(prompt.is_none());
assert!(!resume.last);
assert_eq!(resume.session_or_prompt.as_deref(), Some("session-123"));
assert_eq!(resume.prompt.as_deref(), Some("follow up"));
}
#[test]
fn exec_resume_parses_last_flag() {
let cli = Cli::parse_from(["vtcode", "exec", "resume", "--last", "continue"]);
let Some(Commands::Exec {
command: Some(ExecSubcommand::Resume(resume)),
..
}) = cli.command
else {
panic!("expected exec resume command");
};
assert!(resume.last);
assert_eq!(resume.session_or_prompt.as_deref(), Some("continue"));
assert!(resume.prompt.is_none());
}
#[test]
fn exec_resume_parses_all_flag() {
let cli = Cli::parse_from(["vtcode", "exec", "resume", "--last", "--all", "continue"]);
let Some(Commands::Exec {
command: Some(ExecSubcommand::Resume(resume)),
..
}) = cli.command
else {
panic!("expected exec resume command");
};
assert!(resume.last);
assert!(resume.all);
assert_eq!(resume.session_or_prompt.as_deref(), Some("continue"));
}
#[test]
fn exec_resume_allows_last_without_positional_for_stdin_prompt() {
let cli = Cli::parse_from(["vtcode", "exec", "resume", "--last"]);
let Some(Commands::Exec {
command: Some(ExecSubcommand::Resume(resume)),
..
}) = cli.command
else {
panic!("expected exec resume command");
};
assert!(resume.last);
assert!(resume.session_or_prompt.is_none());
assert!(resume.prompt.is_none());
}
#[test]
fn global_resume_and_continue_parse_all_flag() {
let resume_cli = Cli::parse_from(["vtcode", "--resume", "session-123", "--all"]);
assert_eq!(resume_cli.resume_session.as_deref(), Some("session-123"));
assert!(resume_cli.all);
let continue_cli = Cli::parse_from(["vtcode", "--continue", "--all"]);
assert!(continue_cli.continue_latest);
assert!(continue_cli.all);
}
#[test]
fn global_fork_flags_parse_summarize() {
let cli = Cli::parse_from(["vtcode", "--fork-session", "session-123", "--summarize"]);
assert_eq!(cli.fork_session.as_deref(), Some("session-123"));
assert!(cli.summarize);
}
#[test]
fn notify_parses_title_and_message() {
let cli = Cli::parse_from(["vtcode", "notify", "--title", "VT Code", "Session started"]);
let Some(Commands::Notify { title, message }) = cli.command else {
panic!("expected notify command");
};
assert_eq!(title.as_deref(), Some("VT Code"));
assert_eq!(message, "Session started");
}
#[test]
fn review_defaults_to_current_diff() {
let cli = Cli::parse_from(["vtcode", "review"]);
let Some(Commands::Review(review)) = cli.command else {
panic!("expected review command");
};
assert!(!review.last_diff);
assert!(review.target.is_none());
assert!(review.files.is_empty());
assert!(review.style.is_none());
}
#[test]
fn review_parses_target_and_style_flags() {
let cli = Cli::parse_from([
"vtcode",
"review",
"--target",
"HEAD~1..HEAD",
"--style",
"security",
]);
let Some(Commands::Review(review)) = cli.command else {
panic!("expected review command");
};
assert_eq!(review.target.as_deref(), Some("HEAD~1..HEAD"));
assert_eq!(review.style.as_deref(), Some("security"));
assert!(!review.last_diff);
}
#[test]
fn review_parses_files() {
let cli = Cli::parse_from([
"vtcode",
"review",
"--file",
"src/main.rs",
"--file",
"src/lib.rs",
]);
let Some(Commands::Review(review)) = cli.command else {
panic!("expected review command");
};
assert_eq!(review.files.len(), 2);
assert_eq!(review.files[0], PathBuf::from("src/main.rs"));
assert_eq!(review.files[1], PathBuf::from("src/lib.rs"));
}
#[test]
fn dependencies_install_parses_ast_grep() {
let cli = Cli::parse_from(["vtcode", "dependencies", "install", "ast-grep"]);
let Some(Commands::Dependencies(DependenciesSubcommand::Install { dependency })) =
cli.command
else {
panic!("expected dependencies install command");
};
assert_eq!(dependency, ManagedDependency::AstGrep);
}
#[test]
fn dependencies_install_parses_ripgrep() {
let cli = Cli::parse_from(["vtcode", "dependencies", "install", "ripgrep"]);
let Some(Commands::Dependencies(DependenciesSubcommand::Install { dependency })) =
cli.command
else {
panic!("expected dependencies install command");
};
assert_eq!(dependency, ManagedDependency::Ripgrep);
}
#[test]
fn dependencies_install_parses_search_tools() {
let cli = Cli::parse_from(["vtcode", "dependencies", "install", "search-tools"]);
let Some(Commands::Dependencies(DependenciesSubcommand::Install { dependency })) =
cli.command
else {
panic!("expected dependencies install command");
};
assert_eq!(dependency, ManagedDependency::SearchTools);
}
#[test]
fn deps_alias_parses_status_command() {
let cli = Cli::parse_from(["vtcode", "deps", "status", "ast-grep"]);
let Some(Commands::Dependencies(DependenciesSubcommand::Status { dependency })) =
cli.command
else {
panic!("expected deps status command");
};
assert_eq!(dependency, ManagedDependency::AstGrep);
}
#[test]
fn deps_alias_parses_ripgrep_status_command() {
let cli = Cli::parse_from(["vtcode", "deps", "status", "ripgrep"]);
let Some(Commands::Dependencies(DependenciesSubcommand::Status { dependency })) =
cli.command
else {
panic!("expected deps status command");
};
assert_eq!(dependency, ManagedDependency::Ripgrep);
}
#[test]
fn deps_alias_parses_search_tools_status_command() {
let cli = Cli::parse_from(["vtcode", "deps", "status", "search-tools"]);
let Some(Commands::Dependencies(DependenciesSubcommand::Status { dependency })) =
cli.command
else {
panic!("expected deps status command");
};
assert_eq!(dependency, ManagedDependency::SearchTools);
}
#[test]
fn check_parses_ast_grep_subcommand() {
let cli = Cli::parse_from(["vtcode", "check", "ast-grep"]);
let Some(Commands::Check { command }) = cli.command else {
panic!("expected check command");
};
assert_eq!(command, CheckSubcommand::AstGrep);
}
#[test]
fn pods_start_parses_model_and_gpu_flags() {
let cli = Cli::parse_from([
"vtcode",
"pods",
"start",
"--name",
"llama",
"--model",
"meta-llama/Llama-3.1-8B-Instruct",
"--pod-name",
"gpu-box",
"--ssh",
"ssh root@gpu.example.com",
"--gpu",
"0:A100",
"--gpu",
"1:A100",
"--gpus",
"2",
"--memory",
"90",
"--context",
"32k",
]);
let Some(Commands::Pods {
command:
PodsCommands::Start {
name,
model,
pod_name,
ssh,
gpus,
models_path,
profile,
gpus_count,
memory,
context,
},
}) = cli.command
else {
panic!("expected pods start command");
};
assert_eq!(name, "llama");
assert_eq!(model, "meta-llama/Llama-3.1-8B-Instruct");
assert_eq!(pod_name.as_deref(), Some("gpu-box"));
assert_eq!(ssh.as_deref(), Some("ssh root@gpu.example.com"));
assert_eq!(gpus, vec!["0:A100", "1:A100"]);
assert!(models_path.is_none());
assert!(profile.is_none());
assert_eq!(gpus_count, Some(2));
assert_eq!(memory, Some(90.0));
assert_eq!(context.as_deref(), Some("32k"));
}
}
#[derive(Debug, serde::Deserialize)]
pub struct PerformanceConfig {
pub enabled: Option<bool>,
pub track_token_usage: Option<bool>,
pub track_api_costs: Option<bool>,
pub track_response_times: Option<bool>,
pub enable_benchmarking: Option<bool>,
pub metrics_retention_days: Option<usize>,
}
#[derive(Debug, serde::Deserialize)]
pub struct SecurityConfig {
pub level: Option<String>,
pub enable_audit_logging: Option<bool>,
pub enable_vulnerability_scanning: Option<bool>,
pub allow_external_urls: Option<bool>,
pub max_file_access_depth: Option<usize>,
}
impl Default for Cli {
fn default() -> Self {
Self {
color: ColorSelection {
color: ColorChoice::Auto,
},
workspace_path: None,
model: Some(ModelId::default().to_string()),
provider: Some("gemini".to_owned()),
api_key_env: "GEMINI_API_KEY".to_owned(),
workspace: None,
research_preview: false,
security_level: "moderate".to_owned(),
show_file_diffs: false,
max_concurrent_ops: 5,
api_rate_limit: 30,
max_tool_calls: 10,
verbose: false,
quiet: false,
config: Vec::new(),
log_level: "info".to_owned(),
no_color: false,
theme: None,
skip_confirmations: false,
codex_experimental: false,
no_codex_experimental: false,
print: None,
full_auto: None,
resume_session: None,
continue_latest: false,
fork_session: None,
all: false,
session_id: None,
summarize: false,
debug: false,
enable_skills: false, tick_rate: 250, frame_rate: 60, agent: None, allowed_tools: Vec::new(), disallowed_tools: Vec::new(), dangerously_skip_permissions: false, ide: false, permission_mode: None, chrome: false, no_chrome: false, command: Some(Commands::Chat),
}
}
}
impl Cli {
pub fn get_model(&self) -> String {
self.model
.clone()
.unwrap_or_else(|| ModelId::default().to_string())
}
pub async fn load_config(&self) -> Result<ConfigFile, Box<dyn std::error::Error>> {
use std::path::Path;
use tokio::fs;
let explicit_path = self.config.iter().find_map(|entry| {
let trimmed = entry.trim();
if trimmed.contains('=') || trimmed.is_empty() {
None
} else {
Some(PathBuf::from(trimmed))
}
});
let path = if let Some(p) = explicit_path {
p
} else {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let primary = cwd.join("vtcode.toml");
let secondary = cwd.join(".vtcode.toml");
if fs::try_exists(&primary).await.unwrap_or(false) {
primary
} else if fs::try_exists(&secondary).await.unwrap_or(false) {
secondary
} else {
return Ok(ConfigFile {
model: None,
provider: None,
api_key_env: None,
verbose: None,
log_level: None,
workspace: None,
tools: None,
context: None,
logging: None,
performance: None,
security: None,
});
}
};
let text = fs::read_to_string(&path).await?;
let mut cfg = ConfigFile {
model: None,
provider: None,
api_key_env: None,
verbose: None,
log_level: None,
workspace: None,
tools: None,
context: None,
logging: None,
performance: None,
security: None,
};
for raw_line in text.lines() {
let line = raw_line.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with("//") {
continue;
}
let line = match line.find('#') {
Some(idx) => &line[..idx],
None => line,
}
.trim();
let mut parts = line.splitn(2, '=');
let key = parts.next().map(|s| s.trim()).unwrap_or("");
let val = parts.next().map(|s| s.trim()).unwrap_or("");
if key.is_empty() || val.is_empty() {
continue;
}
let unquote = |s: &str| -> String {
let s = s.trim();
if (s.starts_with('"') && s.ends_with('"'))
|| (s.starts_with('\'') && s.ends_with('\''))
{
s[1..s.len() - 1].to_owned()
} else {
s.to_owned()
}
};
match key {
"model" => cfg.model = Some(unquote(val)),
"api_key_env" => cfg.api_key_env = Some(unquote(val)),
"verbose" => {
let v = unquote(val).to_lowercase();
cfg.verbose = Some(matches!(v.as_str(), "true" | "1" | "yes"));
}
"log_level" => cfg.log_level = Some(unquote(val)),
"workspace" => {
let v = unquote(val);
let p = if Path::new(&v).is_absolute() {
PathBuf::from(v)
} else {
let base = path.parent().unwrap_or(Path::new("."));
base.join(v)
};
cfg.workspace = Some(p);
}
_ => {
}
}
}
Ok(cfg)
}
pub fn get_workspace(&self) -> PathBuf {
self.workspace
.clone()
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
}
pub fn get_api_key_env(&self) -> String {
crate::config::api_keys::resolve_api_key_env(
self.provider
.as_deref()
.unwrap_or(crate::config::constants::defaults::DEFAULT_PROVIDER),
&self.api_key_env,
)
}
pub fn is_verbose(&self) -> bool {
self.verbose
}
pub fn is_research_preview_enabled(&self) -> bool {
self.research_preview
}
pub fn get_security_level(&self) -> &str {
&self.security_level
}
pub fn is_debug_mode(&self) -> bool {
self.debug || self.verbose
}
pub fn codex_experimental_override(&self) -> Option<bool> {
if self.codex_experimental {
Some(true)
} else if self.no_codex_experimental {
Some(false)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::{Cli, long_version};
use clap::Parser;
#[test]
fn long_version_includes_expected_sections() {
let text = long_version();
assert!(text.contains("Authors:"));
assert!(text.contains("Config directory:"));
assert!(text.contains("Data directory:"));
assert!(text.contains("VTCODE_CONFIG"));
assert!(text.contains("VTCODE_DATA"));
}
#[test]
fn long_version_starts_with_build_git_info() {
let text = long_version();
let expected = option_env!("VT_CODE_GIT_INFO").unwrap_or(env!("CARGO_PKG_VERSION"));
assert!(text.starts_with(expected));
}
#[test]
fn config_file_api_key_env_uses_provider_default() {
let cli = Cli::parse_from(["vtcode", "--provider", "minimax"]);
assert_eq!(cli.get_api_key_env(), "MINIMAX_API_KEY");
}
#[test]
fn config_file_api_key_env_preserves_explicit_override() {
let cli = Cli::parse_from([
"vtcode",
"--provider",
"openai",
"--api-key-env",
"CUSTOM_OPENAI_KEY",
]);
assert_eq!(cli.get_api_key_env(), "CUSTOM_OPENAI_KEY");
}
#[test]
fn parses_app_server_command_with_stdio_listen_target() {
let cli = Cli::parse_from(["vtcode", "app-server", "--listen", "stdio://"]);
assert!(matches!(
cli.command,
Some(super::Commands::AppServer { ref listen }) if listen == "stdio://"
));
}
#[test]
fn parses_init_force_flag() {
let cli = Cli::parse_from(["vtcode", "init", "--force"]);
assert!(matches!(
cli.command,
Some(super::Commands::Init { force: true })
));
}
#[test]
fn parses_codex_login_device_code_flag() {
let cli = Cli::parse_from(["vtcode", "login", "codex", "--device-code"]);
assert!(matches!(
cli.command,
Some(super::Commands::Login {
ref provider,
device_code: true
}) if provider == "codex"
));
}
#[test]
fn parses_codex_experimental_flags() {
let enabled = Cli::parse_from(["vtcode", "--codex-experimental"]);
assert_eq!(enabled.codex_experimental_override(), Some(true));
let disabled = Cli::parse_from(["vtcode", "--no-codex-experimental"]);
assert_eq!(disabled.codex_experimental_override(), Some(false));
}
#[test]
fn codex_experimental_flags_conflict() {
let result =
Cli::try_parse_from(["vtcode", "--codex-experimental", "--no-codex-experimental"]);
assert!(result.is_err());
}
#[test]
fn parses_create_project_feature_flags() {
let cli = Cli::parse_from([
"vtcode",
"create-project",
"demo",
"--feature",
"web",
"--feature",
"db",
]);
assert!(matches!(
cli.command,
Some(super::Commands::CreateProject { ref name, ref features })
if name == "demo" && features == &vec!["web".to_string(), "db".to_string()]
));
}
#[test]
fn parses_revert_partial_long_flag() {
let cli = Cli::parse_from(["vtcode", "revert", "--turn", "3", "--partial", "code"]);
assert!(matches!(
cli.command,
Some(super::Commands::Revert {
turn: 3,
partial: Some(ref scope)
}) if scope == "code"
));
}
}