cumulus-sdd 0.1.0

Token-efficient spec-driven development CLI (embedded prompts + slash-command generators).
//! cumulus — standalone Rust CLI for the cumulus-sdd framework.
//!
//! Subcommands:
//!   init             Write embedded agents + templates + skills into the repo, create docs/sdd/.
//!   skills <target>  Generate slash commands (copilot | claude | gemini | all).

mod embed;
mod skills;

use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::exit;

const BOLD: &str = "\x1b[1m";
const BLUE: &str = "\x1b[34m";
const GREEN: &str = "\x1b[32m";
const RED: &str = "\x1b[31m";
const RESET: &str = "\x1b[0m";

fn log(msg: &str) {
    println!("{BLUE}{BOLD}[cumulus]{RESET} {msg}");
}
fn ok(msg: &str) {
    println!("  {GREEN}OK{RESET}   {msg}");
}
fn die(msg: &str) -> ! {
    eprintln!("{RED}{BOLD}[cumulus] error:{RESET} {msg}");
    exit(1);
}

fn main() {
    let args: Vec<String> = env::args().skip(1).collect();
    let cmd = args.first().map(String::as_str).unwrap_or("help");
    match cmd {
        "init" => cmd_init(),
        "skills" => cmd_skills(args.get(1).map(String::as_str)),
        "-V" | "--version" => println!("cumulus {}", env!("CARGO_PKG_VERSION")),
        "help" | "-h" | "--help" => print_help(),
        other => {
            eprintln!("unknown command '{other}'\n");
            print_help();
            exit(1);
        }
    }
}

fn print_help() {
    println!(
        "cumulus — token-efficient spec-driven development (standalone binary)\n\n\
         USAGE:\n  cumulus <command>\n\n\
         COMMANDS:\n\
         \x20 init              Write embedded framework (.cumulus-sdd/ + docs/sdd/) into this repo.\n\
         \x20 skills [target]   Generate slash commands: copilot | claude | gemini | all (default: all).\n\
         \x20 help              Show this help.\n"
    );
}

fn cwd() -> PathBuf {
    env::current_dir().unwrap_or_else(|e| die(&format!("cannot read cwd: {e}")))
}

// ── init ─────────────────────────────────────────────────────────────────────
fn cmd_init() {
    let root = cwd();
    log(&format!("Initializing cumulus-sdd in {}", root.display()));

    for asset in embed::ASSETS {
        let dest = root.join(asset.dest);
        if let Some(parent) = dest.parent() {
            fs::create_dir_all(parent).unwrap_or_else(|e| die(&e.to_string()));
        }
        fs::write(&dest, asset.body).unwrap_or_else(|e| die(&e.to_string()));
    }
    ok(".cumulus-sdd/agents/");
    ok(".cumulus-sdd/templates/ (+ skills/)");

    let sdd = root.join("docs/sdd");
    fs::create_dir_all(sdd.join("stories")).unwrap_or_else(|e| die(&e.to_string()));
    let gitkeep = sdd.join(".gitkeep");
    if !gitkeep.exists() {
        let _ = fs::write(&gitkeep, "");
    }
    ok("docs/sdd/ (+ stories/)");

    log("Done. Next: `cumulus skills <copilot|claude|gemini|all>` to emit slash commands.");
}

// ── skills ───────────────────────────────────────────────────────────────────
fn cmd_skills(target: Option<&str>) {
    let target = target.unwrap_or("all");
    let root = cwd();
    log(&format!("Generating '{target}' slash commands"));

    match skills::emit(target, &root) {
        Ok(result) => {
            for (t, files) in &result {
                ok(&format!("{t}: {} command(s)", files.len()));
                for f in files {
                    println!("       {}", rel(&root, f));
                }
            }
            log("Done. Reload your editor/agent to pick up the new slash commands.");
        }
        Err(e) => die(&e.to_string()),
    }
}

fn rel(root: &Path, p: &Path) -> String {
    p.strip_prefix(root)
        .unwrap_or(p)
        .to_string_lossy()
        .into_owned()
}