klasp 0.2.2

Block AI coding agents on the same quality gates your humans hit. See https://github.com/klasp-dev/klasp
//! `klasp init` โ€” scaffold a `klasp.toml` in the current repo.
//!
//! Resolves the repo root via the shared `resolve_repo_root` helper, then
//! atomically writes an example `klasp.toml` that parses cleanly through
//! `ConfigV1::parse`. Keep [`EXAMPLE_TOML`] in sync when the config schema
//! grows. See [docs/design.md ยง5].
//!
//! Pass `--force` to overwrite an existing file without prompting.

use std::path::{Path, PathBuf};
use std::process::ExitCode;

use anyhow::{anyhow, Context, Result};

use crate::cli::InitArgs;

/// Example `klasp.toml` written by `klasp init`. Every uncommented line
/// must parse cleanly via `klasp_core::ConfigV1::parse`.
const EXAMPLE_TOML: &str = r#"# klasp.toml โ€” generated by `klasp init`
# Docs: https://github.com/klasp-dev/klasp
# Verify this install: run `klasp doctor`

version = 1

[gate]
# Agent surfaces that klasp intercepts. Currently only "claude_code" is supported.
agents = ["claude_code"]
# Gate policy: "any_fail" blocks the agent if any check fails.
policy = "any_fail"

# [[checks]]
# name = "lint"
# triggers = [{ on = ["commit"] }]
# timeout_secs = 60
# [checks.source]
# type = "shell"
# command = "ruff check ."   # or: cargo clippy --all-targets -- -D warnings

# [[checks]]
# name = "test"
# triggers = [{ on = ["commit"] }]
# [checks.source]
# type = "shell"
# command = "pytest -q"      # or: cargo test --workspace
"#;

pub fn run(args: &InitArgs) -> ExitCode {
    match try_run(args) {
        Ok(path) => {
            println!("wrote {}", path.display());
            ExitCode::SUCCESS
        }
        Err(e) => {
            eprintln!("klasp init: {e:#}");
            ExitCode::FAILURE
        }
    }
}

fn try_run(args: &InitArgs) -> Result<PathBuf> {
    let repo_root = crate::cmd::install::resolve_repo_root(None).context("resolving repo root")?;
    let target = repo_root.join("klasp.toml");

    if target.exists() && !args.force {
        return Err(anyhow!(
            "klasp.toml already exists at {}; pass --force to overwrite",
            target.display()
        ));
    }

    atomic_write_text(&target, EXAMPLE_TOML)
        .with_context(|| format!("writing klasp.toml to {}", target.display()))?;

    Ok(target)
}

/// Atomic write via tempfile + rename. No chmod โ€” config files don't need
/// an execute bit.
fn atomic_write_text(path: &Path, contents: &str) -> std::io::Result<()> {
    use std::io::Write;
    let dir = path.parent().unwrap_or_else(|| Path::new("."));
    let mut tf = tempfile::NamedTempFile::new_in(dir)?;
    tf.write_all(contents.as_bytes())?;
    tf.flush()?;
    tf.persist(path).map_err(|e| e.error)?;
    Ok(())
}