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}")))
}
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.");
}
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()
}