use std::path::PathBuf;
use anyhow::{Context, Result, bail};
use vtcode_core::cli::args::Cli;
use vtcode_core::config::loader::{ConfigBuilder, VTCodeConfig, set_explicit_config_path};
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) primary_agent_explicitly_configured: bool,
}
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());
let resolved_explicit_path = match config_path_override {
Some(path_override) => {
let resolved_path = resolve_config_path(&workspace, &path_override);
builder = builder.config_file(resolved_path.clone());
Some(resolved_path)
}
None => None,
};
set_explicit_config_path(resolved_explicit_path.clone());
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 config_phase = std::time::Instant::now();
let manager = match builder.build() {
Ok(manager) => manager,
Err(error) if super::is_config_reset_command(args) => {
tracing::warn!("Configuration could not be loaded before reset: {error:#}");
return Ok(LoadedStartupConfig {
workspace,
config: VTCodeConfig::default(),
first_run_occurred: false,
full_auto_requested: false,
automation_prompt: None,
primary_agent_explicitly_configured: false,
});
}
Err(error) => return Err(error).context("Failed to load configuration"),
};
let config_duration = config_phase.elapsed();
if let Some(timing) = manager.phase_timing() {
tracing::debug!(target = "vtcode.startup", ?timing, "configuration phases recorded");
vtcode_commons::startup_trace::record_duration(
"config_path_resolution",
std::time::Duration::from_micros(timing.path_resolution_us),
);
vtcode_commons::startup_trace::record_duration(
"config_layer_loading",
std::time::Duration::from_micros(timing.layer_loading_us),
);
vtcode_commons::startup_trace::record_duration(
"config_merge_and_parse",
std::time::Duration::from_micros(timing.merge_and_parse_us),
);
vtcode_commons::startup_trace::record_duration(
"config_validation",
std::time::Duration::from_micros(timing.validation_us),
);
}
vtcode_commons::startup_trace::record_duration("config_loading", config_duration);
let primary_agent_explicitly_configured = has_explicit_default_primary_agent(&manager.effective_config());
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,
primary_agent_explicitly_configured,
})
}
pub(crate) fn has_explicit_default_primary_agent(config: &toml::Value) -> bool {
has_top_level_config_key(config, "default_primary_agent")
}
fn has_top_level_config_key(config: &toml::Value, key: &str) -> bool {
config.as_table().is_some_and(|table| table.contains_key(key))
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
use serial_test::serial;
use tempfile::TempDir;
use vtcode_config::loader::explicit_config_path;
use vtcode_core::cli::args::Cli;
struct SessionOverrideGuard;
impl SessionOverrideGuard {
fn reset() -> Self {
set_explicit_config_path(None);
Self
}
}
impl Drop for SessionOverrideGuard {
fn drop(&mut self) {
set_explicit_config_path(None);
}
}
#[tokio::test]
#[serial]
async fn cli_config_path_override_loads_requested_file() {
let _guard = SessionOverrideGuard::reset();
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);
}
#[tokio::test]
#[serial]
async fn env_config_path_override_loads_requested_file_and_captures_session_override() {
use vtcode_commons::env_lock;
let _guard = SessionOverrideGuard::reset();
let env_guard = env_lock::lock();
let previous = std::env::var_os("VTCODE_CONFIG_PATH");
let temp_dir = TempDir::new().expect("temp dir");
let workspace = temp_dir.path().join("workspace");
std::fs::create_dir_all(&workspace).expect("workspace dir");
std::fs::create_dir_all(workspace.join(".vtcode")).expect("workspace dot dir");
let config_path = temp_dir.path().join("custom-env-config.toml");
std::fs::write(&config_path, "[debug]\nenable_tracing = true\n").expect("custom env config");
env_guard.set_var("VTCODE_CONFIG_PATH", config_path.to_str().expect("config path"));
let args = Cli::parse_from(["vtcode", "--workspace", workspace.to_str().expect("workspace path")]);
let loaded = load_startup_config(&args).await;
let session_override = explicit_config_path();
env_guard.restore_var("VTCODE_CONFIG_PATH", previous);
let loaded = loaded.expect("startup config should load from VTCODE_CONFIG_PATH");
assert!(loaded.config.debug.enable_tracing);
assert_eq!(
session_override.and_then(|path| vtcode_commons::canonicalize(path).ok()),
vtcode_commons::canonicalize(&config_path).ok(),
"startup must capture the resolved env path as the session override"
);
}
#[test]
fn detects_explicit_default_primary_agent_key() {
let with_key = toml::Value::Table(r#"default_primary_agent = "duck""#.parse().expect("toml"));
let without_key = toml::Value::Table(
r#"[agent]
provider = "openai""#
.parse()
.expect("toml"),
);
assert!(has_explicit_default_primary_agent(&with_key));
assert!(!has_explicit_default_primary_agent(&without_key));
}
}