kata 0.2.0

Multi-project template applier with AI-delegated merge
Documentation
//! `kata apply [--at <dir>] [--dry-run] [--var name=val]`
//!
//! Re-apply this project's recorded templates. Reads
//! `.kata/applied.toml` to know what to apply.

use camino::Utf8PathBuf;

use crate::ai::{agent_for_kind, resolve_backend};
use crate::applied::AppliedState;
use crate::config::ProjectEntry;
use crate::error::{Error, Result};
use crate::manifest::{AgentKind, AiMode};
use crate::preset::TemplateRef;
use crate::runner::{PjApplyOptions, apply_to_pj};
use crate::ui;

use super::{parse_cli_vars, resolve_ai_concurrency, resolve_pj_root};

#[allow(clippy::too_many_arguments)]
pub async fn run(
    at: Option<Utf8PathBuf>,
    dry_run: bool,
    vars: Vec<String>,
    ai_kind: AgentKind,
    no_ai: bool,
    yes: bool,
    ai_prompt: Option<String>,
    ai_mode_override: Option<AiMode>,
    ai_concurrency_override: Option<usize>,
    interactive: bool,
    no_color: bool,
) -> Result<()> {
    let cwd = resolve_pj_root(at)?;
    let pj_root = crate::paths::find_pj_root(&cwd).ok_or_else(|| {
        Error::Config(format!(
            "no .kata/applied.toml found at or above {cwd}; run `kata init` first"
        ))
    })?;

    let applied = AppliedState::load(&pj_root)?;
    if applied.templates.is_empty() {
        return Err(Error::Config(format!(
            "{pj_root}: applied.toml has no templates recorded"
        )));
    }

    // Convert AppliedTemplate back to TemplateRef. Restoring `subdir`
    // is essential — without it, re-apply would load from the wrong
    // root for templates originally specified with `//<subdir>`.
    let templates: Vec<TemplateRef> = applied
        .templates
        .iter()
        .map(|t| TemplateRef {
            source: t.source.clone(),
            rev: Some(t.rev.clone()),
            subdir: t.subdir.clone(),
        })
        .collect();

    let project = ProjectEntry {
        name: pj_root.file_name().unwrap_or("kata-project").to_string(),
        path: pj_root.clone(),
        tags: vec![],
        overrides: None,
    };

    // Re-resolve relative template sources against the base_dir
    // recorded at init time. Without this, `source = "../pj-base"`
    // would be resolved against cwd and fail (Phase 1 bug B from the
    // dogfood story).
    let base_dir = applied.base_dir.clone().unwrap_or(cwd);

    let agent = if no_ai { None } else { agent_for_kind(ai_kind) };
    let agent_backend = if no_ai {
        None
    } else {
        resolve_backend(ai_kind)
    };

    let ai_concurrency = resolve_ai_concurrency(ai_concurrency_override);

    let opts = PjApplyOptions {
        dry_run,
        no_ai,
        interactive,
        cli_vars: parse_cli_vars(vars)?,
        force_once: false,
        yes_all: yes,
        ai_prompt,
        agent_backend,
        ai_mode_override,
        ai_concurrency,
    };
    let result = apply_to_pj(
        project,
        pj_root.clone(),
        templates,
        base_dir,
        toml::Table::new(),
        applied.preset.clone(),
        opts,
        agent,
    )
    .await?;

    ui::print_pj_header(&result.project_name, pj_root.as_str(), no_color);
    for (dst, kind) in &result.actions {
        ui::print_outcome(dst, *kind, no_color);
    }
    if !result.errors.is_empty() {
        eprintln!("\nerrors:");
        for (dst, msg) in &result.errors {
            eprintln!("  {dst}: {msg}");
        }
        return Err(Error::Other(anyhow::anyhow!(
            "{} file(s) failed to apply",
            result.errors.len()
        )));
    }
    Ok(())
}