collet 0.1.0

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
mod assignment;
mod full;
mod key_only;
pub mod presets;
mod simple;
pub mod ui;

pub use assignment::{RoleModels, compute_optimal_assignments};

use full::wizard_full;
use key_only::wizard_key_only;
use simple::wizard_simple;

use super::loader::load_config_file;
use super::paths::config_file_path;
use crate::common::Result;

// Re-export wizard_style so it can be used in commands.rs as `super::wizard::wizard_style`
pub use crate::config::wizard_style;

// ── Subcommands: setup / secure / status ────────────────────────────────────

/// `collet setup` — create default config file.
pub fn cmd_setup(advanced: bool) -> Result<()> {
    use crate::config::wizard_style as s;
    let path = config_file_path();
    let existing = if path.exists() {
        load_config_file().ok()
    } else {
        None
    };

    // Fetch fresh litellm registry at bootstrap — setup always needs up-to-date
    // model metadata regardless of cache age.
    eprint!(
        "  {}⠸ Fetching model registry (litellm)...{}",
        s::DIM,
        s::RESET
    );
    let registry = match crate::registry::cache::fetch_and_cache_blocking() {
        Ok(r) => {
            eprintln!(
                "\r  {}{} Model registry loaded ({} models)",
                s::GREEN,
                s::RESET,
                r.len()
            );
            r
        }
        Err(_) => {
            eprintln!(
                "\r  {}!{} Registry unavailable — using cached data",
                s::DIM,
                s::RESET
            );
            crate::registry::cache::load_cached()
        }
    };
    eprintln!();

    if advanced {
        wizard_full(existing, registry)
    } else {
        wizard_simple(existing, registry)
    }
}

/// Check conditions and run the appropriate wizard if needed.
///
/// Returns `true` if a wizard ran (caller should print a blank separator line).
/// Returns `false` if the wizard was skipped.
///
/// Skip conditions (checked in order):
/// 1. Headless mode (has_prompt)
/// 2. Session restore flags (has_session_flag)
/// 3. Env var API key already set
/// 4. stdin is not a TTY
///
/// Run conditions:
/// - Config file missing → full wizard
/// - Config file present but no key → key-only wizard
pub fn run_setup_wizard_if_needed(has_prompt: bool, has_session_flag: bool) -> Result<bool> {
    use std::io::IsTerminal;

    // Skip: headless or session restore
    if has_prompt || has_session_flag {
        return Ok(false);
    }

    // Skip: env var API key present — auto-seed a minimal config if none exists yet
    // so that CI / Docker environments work out-of-the-box without running the wizard.
    if let Ok(key) = std::env::var("COLLET_API_KEY")
        && !key.is_empty()
    {
        let path = config_file_path();
        if !path.exists() {
            if let Some(dir) = path.parent() {
                let _ = std::fs::create_dir_all(dir);
            }
            let base_url = std::env::var("COLLET_BASE_URL")
                .unwrap_or_else(|_| "https://api.anthropic.com/v1".to_string());
            let model =
                std::env::var("COLLET_MODEL").unwrap_or_else(|_| "claude-sonnet-4-6".to_string());
            if let Ok(enc) = super::secrets::encrypt_key(&key) {
                let _ = ui::write_minimal_wizard_config(&path, &enc, &base_url, &model);
            }
        }
        return Ok(false);
    }

    // Skip: not a terminal (CI, pipes, tests)
    if !std::io::stdin().is_terminal() {
        return Ok(false);
    }

    let path = config_file_path();

    if !path.exists() {
        let registry = crate::registry::cache::fetch_and_cache_blocking()
            .unwrap_or_else(|_| crate::registry::cache::load_cached());
        wizard_full(None, registry)?;
        return Ok(true);
    }

    // Config file exists — check if API key is resolvable
    let file = load_config_file().unwrap_or_default();
    let has_key = file
        .api
        .api_key_enc
        .as_ref()
        .map(|s| !s.is_empty())
        .unwrap_or(false)
        || file
            .api
            .api_key
            .as_ref()
            .map(|s| !s.is_empty())
            .unwrap_or(false);

    if !has_key {
        wizard_key_only()?;
        return Ok(true);
    }

    Ok(false)
}