devist 0.1.1

Project bootstrap CLI for AI-assisted development. Spin up new projects from templates, manage backends, and keep your codebase comprehensible.
use anyhow::{anyhow, Result};
use console::style;

use crate::registry::{Project, Registry};
use crate::runner;
use crate::state::State;
use crate::template::{Template, TemplateManifest};

pub fn run(name: String, dev: bool) -> Result<()> {
    let reg = Registry::load()?;
    let project = reg
        .find(&name)
        .ok_or_else(|| anyhow!("Project not found: {}. Run `devist projects list`.", name))?
        .clone();

    if !project.path.exists() {
        return Err(anyhow!(
            "Project directory missing: {}. Run `devist projects forget {}` to clean up.",
            project.path.display(),
            name
        ));
    }

    println!("{}", style("devist start").bold());
    println!();
    println!(
        "  {} {} {}",
        style("[PROJ]").cyan(),
        style(&project.name).bold(),
        style(format!("at {}", project.path.display())).dim()
    );

    // 1. Stop other active project's backend (if any, and not us)
    let mut state = State::load()?;
    if let Some(active) = state.active_project.clone() {
        if active != project.name {
            stop_active_backend(&reg, &active)?;
            state.set_active(None);
            state.save()?;
        }
    }

    // 2. Load this project's template manifest
    let manifest = load_manifest_for(&project)?;

    // 3. Start backend (if defined)
    if let Some(cmd) = &manifest.commands.backend_start {
        println!(
            "  {} {}",
            style("[UP]").cyan(),
            style(format!("backend: {}", cmd)).dim()
        );
        runner::run_in(&project.path, cmd)?;
        state.set_active(Some(project.name.clone()));
        state.save()?;
    } else {
        println!("  {} no backend command defined", style("[SKIP]").dim());
    }

    // 4. Optionally run dev server (foreground)
    if dev {
        if let Some(cmd) = &manifest.commands.dev {
            println!(
                "  {} {}",
                style("[DEV]").cyan(),
                style(format!("dev: {}", cmd)).dim()
            );
            println!();
            runner::run_in(&project.path, cmd)?;
        } else {
            println!("  {} no dev command defined", style("[SKIP]").dim());
        }
    } else {
        println!();
        println!("{}", style("Started.").green());
        if manifest.commands.dev.is_some() {
            println!(
                "  Run dev server: {}",
                style(format!(
                    "cd {} && {}",
                    project.path.display(),
                    manifest.commands.dev.as_deref().unwrap_or("")
                ))
                .cyan()
            );
        }
    }

    Ok(())
}

fn stop_active_backend(reg: &Registry, active_name: &str) -> Result<()> {
    let other = match reg.find(active_name) {
        Some(p) => p,
        None => {
            // Stale state — registry no longer has this project. Skip.
            println!(
                "  {} previously active project gone: {}",
                style("[WARN]").yellow(),
                active_name
            );
            return Ok(());
        }
    };

    if !other.path.exists() {
        println!(
            "  {} previously active project missing on disk: {}",
            style("[WARN]").yellow(),
            active_name
        );
        return Ok(());
    }

    let manifest = match load_manifest_for(other) {
        Ok(m) => m,
        Err(e) => {
            println!(
                "  {} cannot read template for {}: {}",
                style("[WARN]").yellow(),
                active_name,
                e
            );
            return Ok(());
        }
    };

    if let Some(cmd) = &manifest.commands.backend_stop {
        println!(
            "  {} stopping {} ({})",
            style("[DOWN]").yellow(),
            style(active_name).bold(),
            style(cmd).dim()
        );
        if let Err(e) = runner::run_in_quiet(&other.path, cmd) {
            println!(
                "  {} stop command failed (continuing): {}",
                style("[WARN]").yellow(),
                e
            );
        }
    }
    Ok(())
}

fn load_manifest_for(project: &Project) -> Result<TemplateManifest> {
    let templates = Template::list_all()?;
    let tpl = templates
        .into_iter()
        .find(|t| t.manifest.meta.name == project.template)
        .ok_or_else(|| {
            anyhow!(
                "Template `{}` not installed. Add it with `devist template add ...`",
                project.template
            )
        })?;
    Ok(tpl.manifest)
}