vtcode 0.99.1

A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers
use anyhow::{Context, Result};
use clap::builder::Styles;
use clap::builder::styling::{AnsiColor, Effects};
use clap::{ColorChoice as CliColorChoice, CommandFactory};
use vtcode_commons::color_policy::{self, ColorOutputPolicy, ColorOutputPolicySource};
use vtcode_core::cli::args::Cli;
use vtcode_core::config::loader::ConfigManager;

use crate::startup::StartupContext;

fn env_flag_enabled(var_name: &str) -> bool {
    std::env::var(var_name)
        .ok()
        .map(|value| {
            matches!(
                value.trim().to_ascii_lowercase().as_str(),
                "1" | "true" | "yes" | "on" | "debug"
            )
        })
        .unwrap_or(false)
}

pub(crate) fn debug_runtime_flag_enabled(debug_arg_enabled: bool, env_var: &str) -> bool {
    cfg!(debug_assertions) && (debug_arg_enabled || env_flag_enabled(env_var))
}

pub(crate) fn resolve_runtime_color_policy(args: &Cli) -> ColorOutputPolicy {
    if args.no_color {
        return ColorOutputPolicy {
            enabled: false,
            source: ColorOutputPolicySource::CliNoColor,
        };
    }

    match args.color.color {
        CliColorChoice::Always => ColorOutputPolicy {
            enabled: true,
            source: ColorOutputPolicySource::CliColorAlways,
        },
        CliColorChoice::Never => ColorOutputPolicy {
            enabled: false,
            source: ColorOutputPolicySource::CliColorNever,
        },
        CliColorChoice::Auto => {
            if color_policy::no_color_env_active() {
                ColorOutputPolicy {
                    enabled: false,
                    source: ColorOutputPolicySource::NoColorEnv,
                }
            } else {
                ColorOutputPolicy {
                    enabled: true,
                    source: ColorOutputPolicySource::DefaultAuto,
                }
            }
        }
    }
}

pub(crate) fn build_augmented_cli_command() -> clap::Command {
    let mut cmd = Cli::command();
    if let Some(choice) = requested_help_color_choice() {
        cmd = cmd.color(choice);
    }
    cmd = cmd.styles(clap_help_styles());
    let quick_start: &'static str = Box::leak(build_quick_start_help().into_boxed_str());
    cmd = cmd.before_help(quick_start);

    let version_info = vtcode_core::cli::args::long_version();
    let version_leak: &'static str = Box::leak(version_info.into_boxed_str());
    cmd = cmd.long_version(version_leak);

    let after_help = "\nSlash commands (type / in chat):\n  /init     - Guided AGENTS.md + workspace setup\n  /config   - Browse settings sections\n  /status   - Show current configuration\n  /doctor   - Diagnose setup issues (inline picker, or use --quick/--full)\n  /update   - Check for VT Code updates (use --list, --pin, --channel)\n  /plan     - Toggle read-only planning mode\n  /loop     - Schedule a recurring prompt in this session\n  /schedule - Open the durable scheduled-task manager\n  /theme    - Switch UI theme\n  /title    - Configure terminal title items\n  /history  - Open command history picker\n  /help     - Show all slash commands";
    cmd.after_help(after_help)
}

fn clap_help_styles() -> Styles {
    Styles::styled()
        .header(AnsiColor::BrightBlue.on_default().effects(Effects::BOLD))
        .usage(AnsiColor::BrightBlue.on_default().effects(Effects::BOLD))
        .literal(AnsiColor::BrightGreen.on_default().effects(Effects::BOLD))
        .placeholder(AnsiColor::BrightCyan.on_default())
}

fn requested_help_color_choice() -> Option<CliColorChoice> {
    let mut requested = None;
    let mut args = std::env::args().skip(1);

    while let Some(arg) = args.next() {
        if arg == "--no-color" {
            requested = Some(CliColorChoice::Never);
            continue;
        }

        if let Some(value) = arg.strip_prefix("--color=") {
            if let Some(choice) = parse_help_color_choice(value) {
                requested = Some(choice);
            }
            continue;
        }

        if arg == "--color"
            && let Some(value) = args.next()
            && let Some(choice) = parse_help_color_choice(&value)
        {
            requested = Some(choice);
        }
    }

    requested
}

fn build_quick_start_help() -> String {
    if has_provider_or_model_configuration() {
        "Quick start:\n  1. Start interactive chat: vtcode chat\n  2. Run one prompt directly: vtcode --print \"summarize this repository\"\n\nUse `vtcode <command> --help` for command-specific details.".to_string()
    } else {
        "Quick start:\n  1. Export your provider API key (examples: OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY)\n  2. Start chat with a provider/model: vtcode chat --provider openai --model gpt-5\n  3. Run one prompt directly: vtcode --provider anthropic --model claude-sonnet-4-5 --print \"summarize this repository\"\n\nUse `vtcode <command> --help` for command-specific details.".to_string()
    }
}

fn has_provider_or_model_configuration() -> bool {
    cli_args_include_provider_or_model() || config_includes_provider_or_model()
}

fn cli_args_include_provider_or_model() -> bool {
    std::env::args().skip(1).any(|arg| {
        arg == "--provider"
            || arg == "--model"
            || arg.starts_with("--provider=")
            || arg.starts_with("--model=")
    })
}

fn config_includes_provider_or_model() -> bool {
    let Ok(manager) = ConfigManager::load() else {
        return false;
    };
    has_provider_or_model_keys(&manager.effective_config())
}

fn has_provider_or_model_keys(config: &toml::Value) -> bool {
    let Some(root) = config.as_table() else {
        return false;
    };

    root.contains_key("provider")
        || root.contains_key("model")
        || root
            .get("agent")
            .and_then(toml::Value::as_table)
            .is_some_and(|agent| {
                agent.contains_key("provider")
                    || agent.contains_key("model")
                    || agent.contains_key("default_model")
            })
}

fn parse_help_color_choice(value: &str) -> Option<CliColorChoice> {
    match value.trim().to_ascii_lowercase().as_str() {
        "always" => Some(CliColorChoice::Always),
        "auto" => Some(CliColorChoice::Auto),
        "never" => Some(CliColorChoice::Never),
        _ => None,
    }
}

pub(crate) async fn resolve_startup_context(args: &Cli) -> Result<StartupContext> {
    let startup = StartupContext::from_cli_args(args)
        .await
        .context("failed to initialize VT Code startup context")?;
    Ok(startup)
}

#[cfg(test)]
mod tests {
    use super::build_augmented_cli_command;
    use clap::Parser;
    use vtcode_core::cli::args::Cli;

    #[test]
    fn invalid_positional_workspace_fails_during_cli_parse() {
        let mut command = build_augmented_cli_command();
        let err = command
            .try_get_matches_from_mut(["vtcode", "hellp"])
            .expect_err("invalid positional workspace should fail at clap parsing");
        let err_text = err.to_string();
        assert!(
            err_text.contains("Workspace path does not exist"),
            "unexpected clap error: {err_text}"
        );
    }

    #[test]
    fn invalid_positional_workspace_fails_with_derive_parser_too() {
        let err = Cli::try_parse_from(["vtcode", "hellp"])
            .expect_err("invalid positional workspace should fail at derive parser");
        let err_text = err.to_string();
        assert!(
            err_text.contains("Workspace path does not exist"),
            "unexpected clap error: {err_text}"
        );
    }
}