vtcode 0.99.1

A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers
use std::path::PathBuf;

use anyhow::{Context, Result, bail};
use vtcode_core::cli::args::Cli;
use vtcode_core::config::loader::{ConfigBuilder, VTCodeConfig};
use vtcode_core::utils::validation::validate_path_exists;

use super::first_run::maybe_run_first_run_setup;
use super::validation::{parse_cli_config_entries, resolve_config_path, resolve_workspace_path};

pub(super) struct LoadedStartupConfig {
    pub(super) workspace: PathBuf,
    pub(super) config: VTCodeConfig,
    pub(super) first_run_occurred: bool,
    pub(super) full_auto_requested: bool,
    pub(super) automation_prompt: Option<String>,
}

pub(super) async fn load_startup_config(args: &Cli) -> Result<LoadedStartupConfig> {
    let workspace_override = args
        .workspace_path
        .clone()
        .or_else(|| args.workspace.clone());

    let workspace = resolve_workspace_path(workspace_override)
        .context("Failed to resolve workspace directory")?;
    if args.workspace_path.is_some() {
        validate_path_exists(&workspace, "Workspace")?;
    }

    let (cli_config_path_override, inline_config_overrides) =
        parse_cli_config_entries(&args.config);
    let env_config_path_override = std::env::var("VTCODE_CONFIG_PATH").ok().and_then(|value| {
        let trimmed = value.trim();
        if trimmed.is_empty() {
            None
        } else {
            Some(PathBuf::from(trimmed))
        }
    });
    let config_path_override = cli_config_path_override.or(env_config_path_override);

    let mut builder = ConfigBuilder::new().workspace(workspace.clone());
    if let Some(path_override) = config_path_override {
        let resolved_path = resolve_config_path(&workspace, &path_override);
        builder = builder.config_file(resolved_path);
    }

    if !inline_config_overrides.is_empty() {
        builder = builder.cli_overrides(&inline_config_overrides);
    }

    if let Some(ref model) = args.model {
        builder = builder.cli_override(
            "agent.default_model".to_owned(),
            toml::Value::String(model.clone()),
        );
    }
    if let Some(ref provider) = args.provider {
        builder = builder.cli_override(
            "agent.provider".to_owned(),
            toml::Value::String(provider.clone()),
        );
    }

    let manager = builder.build().context("Failed to load configuration")?;
    let mut config = manager.config().clone();

    let (full_auto_requested, automation_prompt) = match args.full_auto.clone() {
        Some(value) if value.trim().is_empty() => (true, None),
        Some(value) => (true, Some(value)),
        None => (false, None),
    };

    let first_run_occurred = maybe_run_first_run_setup(args, &workspace, &mut config).await?;

    if automation_prompt.is_some() && args.command.is_some() {
        bail!(
            "--auto/--full-auto with a prompt cannot be combined with other commands. Provide only the prompt."
        );
    }

    Ok(LoadedStartupConfig {
        workspace,
        config,
        first_run_occurred,
        full_auto_requested,
        automation_prompt,
    })
}

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

    #[tokio::test]
    async fn cli_config_path_override_loads_requested_file() {
        let temp_dir = TempDir::new().expect("temp dir");
        let workspace = temp_dir.path().join("workspace");
        std::fs::create_dir(&workspace).expect("workspace dir");
        std::fs::create_dir(workspace.join(".vtcode")).expect("workspace dot dir");

        let config_path = temp_dir.path().join("custom-config.toml");
        std::fs::write(
            &config_path,
            r#"
[debug]
enable_tracing = true
"#,
        )
        .expect("custom config");

        let args = Cli::parse_from([
            "vtcode",
            "--workspace",
            workspace.to_str().expect("workspace path"),
            "--config",
            config_path.to_str().expect("config path"),
        ]);

        let loaded = load_startup_config(&args)
            .await
            .expect("startup config should load");

        assert!(loaded.config.debug.enable_tracing);
    }
}