use super::assignment::{assign_models_by_role, fetch_models_from_provider};
use super::presets::{PROVIDERS, cli_preset_to_provider, provider_short_name, scan_cli_presets};
use super::ui;
use super::ui::{WizardSignal, wizard_multi_select_ws, wizard_prompt_ws, wizard_select_ws};
use crate::common::{AgentError, Result};
use crate::config::agents::apply_roles_to_agents;
use crate::config::defaults::{scaffold_defaults, write_config_commented};
use crate::config::paths::ensure_config_dir;
use crate::config::secrets::{
decrypt_key, encrypt_key, load_secrets, save_secrets, scan_env_to_secrets,
update_gitignore_for_secrets,
};
use crate::config::types::{ConfigFile, ProviderEntry as ConfigProviderEntry};
pub(super) fn wizard_simple(
existing: Option<ConfigFile>,
registry: crate::registry::model::ModelRegistry,
) -> Result<()> {
use crate::config::wizard_style as s;
use std::io::IsTerminal;
if !std::io::stderr().is_terminal() {
return Err(AgentError::Config(
"Setup requires a terminal. Run interactively.".to_string(),
));
}
{
let env_secrets = scan_env_to_secrets();
let mut current = load_secrets();
if env_secrets.api.api_key_enc.is_some() {
current.api = env_secrets.api.clone();
}
for ep in &env_secrets.providers {
if let Some(sp) = current.providers.iter_mut().find(|p| p.name == ep.name) {
if ep.api_key_enc.is_some() {
sp.api_key_enc = ep.api_key_enc.clone();
}
} else if ep.api_key_enc.is_some() {
current.providers.push(ep.clone());
}
}
if env_secrets.web.password_enc.is_some() {
current.web = env_secrets.web;
}
if env_secrets.telegram.token_enc.is_some() {
current.telegram = env_secrets.telegram;
}
if env_secrets.slack.bot_token_enc.is_some() || env_secrets.slack.app_token_enc.is_some() {
current.slack = env_secrets.slack;
}
if env_secrets.discord.token_enc.is_some() {
current.discord = env_secrets.discord;
}
let _ = save_secrets(¤t);
update_gitignore_for_secrets();
}
eprintln!();
eprintln!(
" {}{}━━━ collet Quick Setup ━━━{}",
s::BOLD,
s::CYAN,
s::RESET
);
eprintln!(
" {}Mode: Quick Setup (provider + automatic model assignment){}",
s::BOLD,
s::RESET
);
eprintln!(
" {}For full control run: collet setup --advanced{}",
s::DIM,
s::RESET
);
eprintln!();
let existing_provider_default = existing
.as_ref()
.and_then(|cf| cf.providers.first().map(|p| p.name.clone()))
.and_then(|name| {
PROVIDERS.iter().position(|p| {
provider_short_name(p.label) == name || p.label.eq_ignore_ascii_case(&name)
})
})
.unwrap_or(0);
let provider_label_strings: Vec<String> = PROVIDERS
.iter()
.enumerate()
.map(|(i, p)| {
if i == existing_provider_default && existing.is_some() {
format!("{} (configured)", p.label)
} else {
p.label.to_string()
}
})
.collect();
let provider_labels: Vec<&str> = provider_label_strings.iter().map(|s| s.as_str()).collect();
let preset_idx = match wizard_select_ws(
"Select your AI provider",
&provider_labels,
existing_provider_default,
)? {
WizardSignal::Next(i) => i,
WizardSignal::Back | WizardSignal::Cancel => {
eprintln!(" {}Setup cancelled.{}", s::DIM, s::RESET);
return Ok(());
}
};
let preset = &PROVIDERS[preset_idx];
let provider_name = provider_short_name(preset.label);
eprintln!(" {}✓{} Provider: {}", s::GREEN, s::RESET, preset.label);
let is_same_provider = existing_provider_default == preset_idx && existing.is_some();
let existing_has_key = existing
.as_ref()
.and_then(|e| e.api.api_key_enc.as_ref())
.map(|enc| !enc.is_empty())
.unwrap_or(false);
if is_same_provider && existing_has_key {
eprintln!(
" {}Provider already configured (key: ***){}",
s::DIM,
s::RESET
);
eprintln!();
match wizard_select_ws(
"Provider settings",
&["Keep existing settings", "Reconfigure"],
0,
)? {
WizardSignal::Next(0) => {
eprintln!(
" {}✓{} Keeping existing provider configuration",
s::GREEN,
s::RESET
);
eprintln!();
let detected_clis = scan_cli_presets();
let cli_providers: Vec<ConfigProviderEntry> = if !detected_clis.is_empty() {
let existing_cli_names: Vec<String> = existing
.as_ref()
.map(|cf| {
cf.providers
.iter()
.filter(|p| p.cli.is_some())
.map(|p| p.name.clone())
.collect()
})
.unwrap_or_default();
let labels: Vec<String> = detected_clis
.iter()
.map(|p| {
if existing_cli_names.iter().any(|n| n == p.name) {
format!("{:<20} ({}) (configured)", p.label, p.binary)
} else {
format!("{:<20} ({})", p.label, p.binary)
}
})
.collect();
let label_refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
let pre_selected: Vec<bool> = detected_clis
.iter()
.map(|p| existing_cli_names.iter().any(|n| n == p.name))
.collect();
match wizard_multi_select_ws(
"Register CLI agents as providers",
&label_refs,
&pre_selected,
)? {
WizardSignal::Next(indices) => indices
.iter()
.map(|&i| {
let mut entry = cli_preset_to_provider(detected_clis[i]);
entry.models = vec!["default".to_string()];
entry
})
.collect(),
WizardSignal::Back | WizardSignal::Cancel => Vec::new(),
}
} else {
Vec::new()
};
let mut cfg = existing.unwrap_or_default();
if !cli_providers.is_empty() {
for cli_entry in cli_providers {
if !cfg.providers.iter().any(|p| p.name == cli_entry.name) {
cfg.providers.push(cli_entry);
}
}
}
let dir = ensure_config_dir()?;
let path = dir.join("config.toml");
write_config_commented(&path, &cfg)?;
eprintln!(
" {}✓{} Config saved to {}",
s::GREEN,
s::RESET,
path.display()
);
return Ok(());
}
WizardSignal::Next(_) => { }
WizardSignal::Back | WizardSignal::Cancel => {
eprintln!(" {}Setup cancelled.{}", s::DIM, s::RESET);
return Ok(());
}
}
}
let api_key = if preset.api_key_required {
let hint = if preset.env_key_hint.is_empty() {
"API key".to_string()
} else {
format!("API key ({})", preset.env_key_hint)
};
let existing_key = existing
.as_ref()
.and_then(|e| e.api.api_key_enc.as_ref())
.and_then(|enc| decrypt_key(enc).ok())
.unwrap_or_default();
if !existing_key.is_empty() {
match wizard_prompt_ws(&format!("{} (Enter to keep existing)", hint), "***")? {
WizardSignal::Next(input) => {
let t = input.trim().to_string();
if t.is_empty() || t == "***" {
existing_key
} else {
t
}
}
WizardSignal::Back => {
eprintln!(" {}Setup cancelled.{}", s::DIM, s::RESET);
return Ok(());
}
WizardSignal::Cancel => {
eprintln!(" {}Setup cancelled.{}", s::DIM, s::RESET);
return Ok(());
}
}
} else {
loop {
match wizard_prompt_ws(&hint, "")? {
WizardSignal::Next(input) => {
let t = input.trim().to_string();
if t.is_empty() {
eprintln!(
" {}API key cannot be empty. Try again.{}",
s::DIM,
s::RESET
);
continue;
}
break t;
}
WizardSignal::Back | WizardSignal::Cancel => {
eprintln!(" {}Setup cancelled.{}", s::DIM, s::RESET);
return Ok(());
}
}
}
}
} else {
String::new()
};
eprint!(
" {}⠸ Fetching available models from provider...{}",
s::DIM,
s::RESET
);
let available = fetch_models_from_provider(preset.base_url, &api_key);
let (available_models, fetch_ok) = match available {
Some(models) => {
eprintln!(
"\r {}✓{} Found {} models",
s::GREEN,
s::RESET,
models.len()
);
(models, true)
}
None => {
eprintln!(
"\r {}!{} Could not fetch models — using preset defaults",
s::DIM,
s::RESET
);
(
preset
.recommended_models
.iter()
.map(|s| s.to_string())
.collect(),
false,
)
}
};
if preset.api_key_required && !fetch_ok && !api_key.is_empty() {
eprintln!(
" {} Check that your API key is correct. You can re-run setup anytime.{}",
s::DIM,
s::RESET
);
}
let roles = assign_models_by_role(&available_models, ®istry, preset, &provider_name);
eprintln!(" {}✓{} Models assigned:", s::GREEN, s::RESET);
eprintln!(" architect → {}", roles.architect);
eprintln!(" code → {}", roles.code);
eprintln!(" ask → {}", roles.ask);
eprintln!();
let detected_clis = scan_cli_presets();
let cli_providers: Vec<ConfigProviderEntry> = if !detected_clis.is_empty() {
let existing_cli_names: Vec<String> = existing
.as_ref()
.map(|cf| {
cf.providers
.iter()
.filter(|p| p.cli.is_some())
.map(|p| p.name.clone())
.collect()
})
.unwrap_or_default();
eprintln!(" {}Detected CLI agents:{}", s::DIM, s::RESET);
for cli in &detected_clis {
let configured = existing_cli_names.iter().any(|n| n == cli.name);
if configured {
eprintln!(
" {}✓{} {} ({}) {}(configured){}",
s::GREEN,
s::RESET,
cli.label,
cli.binary,
s::DIM,
s::RESET
);
} else {
eprintln!(
" {}✓{} {} ({})",
s::GREEN,
s::RESET,
cli.label,
cli.binary
);
}
}
let labels: Vec<String> = detected_clis
.iter()
.map(|p| {
if existing_cli_names.iter().any(|n| n == p.name) {
format!("{:<20} ({}) (configured)", p.label, p.binary)
} else {
format!("{:<20} ({})", p.label, p.binary)
}
})
.collect();
let label_refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
let pre_selected: Vec<bool> = detected_clis
.iter()
.map(|p| existing_cli_names.iter().any(|n| n == p.name))
.collect();
eprintln!();
match wizard_multi_select_ws(
"Register CLI agents as providers",
&label_refs,
&pre_selected,
)? {
WizardSignal::Next(indices) => indices
.iter()
.map(|&i| {
let mut entry = cli_preset_to_provider(detected_clis[i]);
entry.models = vec!["default".to_string()];
entry
})
.collect(),
WizardSignal::Back | WizardSignal::Cancel => Vec::new(),
}
} else {
Vec::new()
};
eprintln!();
let alcove_installed = std::process::Command::new("which")
.arg("alcove")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if alcove_installed {
eprintln!(" {}✓{} Alcove already installed", s::GREEN, s::RESET);
} else {
eprint!(
" {}⠸ Installing Alcove (RAG backend)...{}",
s::DIM,
s::RESET
);
let install_ok = std::process::Command::new("cargo")
.args(["binstall", "-y", "alcove"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if install_ok {
eprintln!("\r {}✓{} Alcove installed", s::GREEN, s::RESET);
} else {
eprintln!(
"\r {}!{} Alcove install failed — skipping (run `cargo binstall alcove` later)",
s::DIM,
s::RESET
);
}
}
eprintln!();
let delta_installed = std::process::Command::new("delta")
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if delta_installed {
eprintln!(" {}✓{} delta already installed", s::GREEN, s::RESET);
} else {
eprint!(
" {}⠸ Installing delta (diff formatter)...{}",
s::DIM,
s::RESET
);
let install_ok = if cfg!(target_os = "macos") {
std::process::Command::new("brew")
.args(["install", "git-delta"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
} else {
let apt_ok = std::process::Command::new("apt-get")
.args(["install", "-y", "git-delta"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if apt_ok {
true
} else {
std::process::Command::new("cargo")
.args(["install", "git-delta"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
};
if install_ok {
eprintln!("\r {}✓{} delta installed", s::GREEN, s::RESET);
} else {
eprintln!(
"\r {}!{} delta install failed — skipping (install manually: brew install git-delta)",
s::DIM,
s::RESET
);
}
}
eprintln!();
let telemetry_items = &[
"Yes — help improve collet (anonymous usage stats, no code or file paths)",
"No — opt out",
];
let telemetry_enabled = matches!(
wizard_select_ws("Telemetry", telemetry_items, 0)?,
WizardSignal::Next(0)
);
eprintln!();
eprint!(" {}⠸ Writing config...{}", s::DIM, s::RESET);
let api_key_enc = if !api_key.is_empty() {
encrypt_key(&api_key).ok()
} else {
None
};
let provider_entry = ConfigProviderEntry {
name: provider_name.clone(),
base_url: preset.base_url.to_string(),
api_key_enc: api_key_enc.clone(),
models: {
let mut m = vec![roles.default.clone()];
if roles.code != roles.default {
m.push(roles.code.clone());
}
if roles.architect != roles.default && roles.architect != roles.code {
m.push(roles.architect.clone());
}
if roles.ask != roles.default && roles.ask != roles.code {
m.push(roles.ask.clone());
}
m
},
cli: None,
cli_args: Vec::new(),
cli_yolo_args: Vec::new(),
cli_model_env: None,
cli_skip_model: false,
cli_yolo_env: Vec::new(),
cli_max_turns_flag: None,
};
let mut cfg = existing.unwrap_or_default();
cfg.api.provider = Some(provider_name.clone());
cfg.api.model = Some(roles.default.clone());
cfg.api.base_url = if preset.base_url.is_empty() {
None
} else {
Some(preset.base_url.to_string())
};
if let Some(enc) = api_key_enc {
cfg.api.api_key_enc = Some(enc);
}
cfg.api.api_key = None;
cfg.telemetry.enabled = Some(telemetry_enabled);
cfg.telemetry.error_reporting = Some(telemetry_enabled);
cfg.telemetry.analytics = Some(telemetry_enabled);
let alcove_now = std::process::Command::new("which")
.arg("alcove")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if alcove_installed || alcove_now {
cfg.rag.enabled = Some(true);
cfg.rag.alcove.enabled = Some(true);
}
if let Some(pos) = cfg
.providers
.iter()
.position(|p| p.name == provider_entry.name)
{
cfg.providers[pos] = provider_entry;
} else {
cfg.providers.insert(0, provider_entry);
}
for cli_entry in cli_providers {
if !cfg.providers.iter().any(|p| p.name == cli_entry.name) {
cfg.providers.push(cli_entry);
}
}
cfg.agents.arbor = Some(format!("{provider_name}/{}", roles.architect));
cfg.agents.architect = Some(format!("{provider_name}/{}", roles.architect));
cfg.agents.code = Some(format!("{provider_name}/{}", roles.code));
cfg.agents.ask = Some(format!("{provider_name}/{}", roles.ask));
{
let mut seen: std::collections::HashSet<String> =
cfg.models.iter().map(|m| m.name.clone()).collect();
let model_names = {
let mut m = vec![roles.default.clone()];
if !m.contains(&roles.code) {
m.push(roles.code.clone());
}
if !m.contains(&roles.architect) {
m.push(roles.architect.clone());
}
if !m.contains(&roles.ask) {
m.push(roles.ask.clone());
}
m
};
for model_name in model_names {
if seen.insert(model_name.clone()) {
let profile = crate::api::model_profile::profile_for(&model_name);
let supports_tools = profile.supports_tool_use || preset.default_supports_tools;
cfg.models.push(crate::config::ModelOverride {
name: model_name,
context_window: Some(profile.context_window),
max_output_tokens: Some(profile.max_output_tokens),
supports_tools: Some(supports_tools),
supports_reasoning: Some(profile.supports_reasoning),
..Default::default()
});
}
}
}
if cfg.agent.tool_timeout_secs.is_none() {
cfg.agent.tool_timeout_secs = Some(300);
}
if cfg.agent.task_timeout_secs.is_none() {
cfg.agent.task_timeout_secs = Some(7200);
}
if cfg.agent.max_continuations.is_none() {
cfg.agent.max_continuations = Some(5);
}
if cfg.agent.circuit_breaker_threshold.is_none() {
cfg.agent.circuit_breaker_threshold = Some(3);
}
if cfg.agent.iteration_delay_ms.is_none() {
cfg.agent.iteration_delay_ms = Some(50);
}
if cfg.agent.auto_optimize.is_none() {
cfg.agent.auto_optimize = Some(true);
}
if cfg.agent.auto_route.is_none() {
cfg.agent.auto_route = Some(true);
}
if cfg.soul.enabled.is_none() {
cfg.soul.enabled = Some(true);
}
if cfg.context.max_tokens.is_none() {
cfg.context.max_tokens = Some(200_000);
}
if cfg.context.compaction_threshold.is_none() {
cfg.context.compaction_threshold = Some(0.8);
}
if cfg.hooks.auto_commit.is_none() {
cfg.hooks.auto_commit = Some(false);
}
if cfg.ui.theme.is_none() {
cfg.ui.theme = Some("default".to_string());
}
if cfg.ui.debug_mode.is_none() {
cfg.ui.debug_mode = Some(false);
}
if cfg.collaboration.mode.is_none() {
cfg.collaboration.mode = Some(crate::agent::swarm::config::CollaborationMode::Flock);
}
if cfg.collaboration.max_agents.is_none() {
cfg.collaboration.max_agents = Some(3);
}
if cfg.collaboration.worktree.is_none() {
cfg.collaboration.worktree = Some(true);
}
if cfg.collaboration.strategy.is_none() {
cfg.collaboration.strategy = Some(crate::agent::swarm::config::SwarmStrategy::AutoSplit);
}
if cfg.collaboration.auto_suggest.is_none() {
cfg.collaboration.auto_suggest = Some(true);
}
if cfg.collaboration.require_consensus.is_none() {
cfg.collaboration.require_consensus = Some(true);
}
if cfg.collaboration.conflict_resolution.is_none() {
cfg.collaboration.conflict_resolution =
Some(crate::agent::swarm::config::ConflictResolution::CoordinatorResolves);
}
if cfg.collaboration.coordinator_model.is_none() {
cfg.collaboration.coordinator_model =
Some(format!("{}/{}", provider_name, roles.architect));
}
if cfg.collaboration.worker_model.is_none() {
cfg.collaboration.worker_model = Some(format!("{}/{}", provider_name, roles.code));
}
if cfg.context.adaptive_compaction.is_none() {
cfg.context.adaptive_compaction = Some(true);
}
if cfg.remote.default_workspace.is_none() {
cfg.remote.default_workspace = Some("project".to_string());
}
if cfg.remote.workspace.is_none() {
cfg.remote.workspace = Some("~/.collet/workspace".to_string());
}
let dir = ensure_config_dir()?;
let config_path = dir.join("config.toml");
if !config_path.exists()
&& let (Some(enc), Some(burl), Some(mdl)) = (
cfg.api.api_key_enc.as_deref(),
cfg.api.base_url.as_deref(),
cfg.api.model.as_deref(),
)
{
let _ = ui::write_minimal_wizard_config(&config_path, enc, burl, mdl);
}
write_config_commented(&config_path, &cfg)?;
let agents_dir = dir.join("agents");
std::fs::create_dir_all(&agents_dir).ok();
let _ = scaffold_defaults(&dir);
apply_roles_to_agents(&agents_dir, &roles, &provider_name);
eprintln!("\r {}✓{} Config written", s::GREEN, s::RESET);
eprintln!();
eprintln!(" {}{}Setup complete!{}", s::BOLD, s::GREEN, s::RESET);
eprintln!();
eprintln!(" Provider {}", preset.label);
eprintln!(" architect {}", roles.architect);
eprintln!(" code {}", roles.code);
eprintln!(" ask {}", roles.ask);
eprintln!(
" Telemetry {}",
if telemetry_enabled {
"enabled"
} else {
"disabled"
}
);
eprintln!();
eprintln!(
" {}To customise agents: .collet/agents/{{code,architect,ask}}.md{}",
s::DIM,
s::RESET
);
eprintln!(
" {}For full options: collet setup --advanced{}",
s::DIM,
s::RESET
);
eprintln!();
Ok(())
}