use anyhow::{anyhow, bail, Context, Result};
use clap::ValueEnum;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
mod deck;
#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum Agent {
Claude,
Gemini,
Codex,
Opencode,
}
impl Agent {
fn label(&self) -> &'static str {
match self {
Agent::Claude => "Claude Code",
Agent::Gemini => "Gemini CLI",
Agent::Codex => "Codex",
Agent::Opencode => "OpenCode",
}
}
fn bin(&self) -> &'static str {
match self {
Agent::Claude => "claude",
Agent::Gemini => "gemini",
Agent::Codex => "codex",
Agent::Opencode => "opencode",
}
}
fn command(&self, prompt: &str) -> Command {
let mut c = Command::new(self.bin());
match self {
Agent::Claude | Agent::Gemini => {
c.arg("-p").arg(prompt);
}
Agent::Codex => {
c.arg("exec").arg(prompt);
}
Agent::Opencode => {
c.arg("run").arg(prompt);
}
}
c
}
const AUTO_DETECT_ORDER: &'static [Agent] =
&[Agent::Claude, Agent::Gemini, Agent::Codex, Agent::Opencode];
}
pub struct Wish {
pub name: &'static str,
pub description: &'static str,
pub default_out: &'static str,
pub questions_md: &'static str,
pub readme_md: &'static str,
pub references: &'static [(&'static str, &'static str)],
pub agent_prompt: &'static str,
pub yolo_prompt: fn(Option<&str>) -> String,
pub stdout_markdown: &'static str,
}
fn all() -> &'static [&'static Wish] {
const ALL: &[&Wish] = &[&deck::DECK];
ALL
}
fn find(name: &str) -> Option<&'static Wish> {
all().iter().copied().find(|w| w.name == name)
}
pub fn run(
name: &str,
out: Option<PathBuf>,
agent: Option<Agent>,
stdout_mode: bool,
dry_run: bool,
yolo: Option<String>,
) -> Result<()> {
if name == "list" {
return list();
}
let wish =
find(name).ok_or_else(|| anyhow!("unknown wish '{}'. Try `kazam wish list`", name))?;
if stdout_mode {
print!("{}", wish.stdout_markdown);
return Ok(());
}
if let Some(topic) = yolo {
let topic = topic.trim();
let topic = if topic.is_empty() { None } else { Some(topic) };
return grant_yolo(wish, topic, out, agent, dry_run);
}
let workspace = PathBuf::from(format!("wish-{}", wish.name));
if !workspace.exists() {
scaffold(wish, &workspace)?;
return Ok(());
}
grant(wish, &workspace, out, agent, dry_run)
}
fn scaffold(wish: &Wish, workspace: &Path) -> Result<()> {
fs::create_dir_all(workspace)
.with_context(|| format!("creating workspace {}", workspace.display()))?;
fs::write(workspace.join("questions.md"), wish.questions_md)?;
fs::write(workspace.join("README.md"), wish.readme_md)?;
if !wish.references.is_empty() {
let ref_dir = workspace.join("reference");
fs::create_dir_all(&ref_dir).with_context(|| format!("creating {}", ref_dir.display()))?;
for (name, contents) in wish.references {
fs::write(ref_dir.join(name), contents)
.with_context(|| format!("writing reference/{}", name))?;
}
}
println!();
println!(" ✨ Making your {} wish — {}", wish.name, wish.description);
println!();
println!(" Created workspace: {}/", workspace.display());
println!(" questions.md structured prompts — fill in what you know");
println!(" README.md what to drop in this folder");
if !wish.references.is_empty() {
println!(" reference/ kazam schema + worked example (read-only to the agent)");
}
println!();
println!(" Next:");
println!(
" 1. Fill in {}/questions.md with whatever you want to",
workspace.display()
);
println!(" answer up front (blanks are fine — the agent will infer).");
println!(" 2. Drop any real context (docs, notes, transcripts, last");
println!(" quarter's deck) into {}/.", workspace.display());
println!(" 3. Run `kazam wish {}` again to grant it.", wish.name);
println!();
Ok(())
}
fn grant(
wish: &Wish,
workspace: &Path,
out: Option<PathBuf>,
agent: Option<Agent>,
dry_run: bool,
) -> Result<()> {
if dry_run {
print!("{}", wish.agent_prompt);
return Ok(());
}
let out_path = out.unwrap_or_else(|| PathBuf::from(wish.default_out));
if out_path.exists() {
bail!(
"'{}' already exists — pass `--out <path>` to write elsewhere",
out_path.display()
);
}
let agent = agent.or_else(detect_agent).ok_or_else(|| {
anyhow!(
"no agent found on $PATH (looked for claude, gemini, codex, opencode). \
Install one, pass --agent explicitly, or use --dry-run to print the prompt."
)
})?;
println!();
println!(
" ✨ Granting your {} wish via {}",
wish.name,
agent.label()
);
println!();
println!(
" Running {} in {} (can take 30-90s)…",
agent.bin(),
workspace.display()
);
println!();
let yaml = run_agent(agent, wish.agent_prompt, workspace)?;
fs::write(&out_path, &yaml).with_context(|| format!("writing {}", out_path.display()))?;
let page_url = out_path
.file_stem()
.and_then(|s| s.to_str())
.map(|stem| format!("http://localhost:3000/{}.html", stem))
.unwrap_or_else(|| "http://localhost:3000/".to_string());
println!();
println!(" ✓ Wrote {}", out_path.display());
println!();
println!(" Next:");
println!(" kazam dev .");
println!(" → open {}", page_url);
println!();
Ok(())
}
fn grant_yolo(
wish: &Wish,
topic: Option<&str>,
out: Option<PathBuf>,
agent: Option<Agent>,
dry_run: bool,
) -> Result<()> {
let prompt = (wish.yolo_prompt)(topic);
if dry_run {
print!("{}", prompt);
return Ok(());
}
let out_path = out.unwrap_or_else(|| PathBuf::from(wish.default_out));
if out_path.exists() {
bail!(
"'{}' already exists — pass `--out <path>` to write elsewhere",
out_path.display()
);
}
let agent = agent.or_else(detect_agent).ok_or_else(|| {
anyhow!(
"no agent found on $PATH (looked for claude, gemini, codex, opencode). \
Install one, pass --agent explicitly, or use --dry-run to print the prompt."
)
})?;
let cwd = std::env::current_dir().context("reading current directory")?;
println!();
match topic {
Some(t) => println!(
" 🎲 Granting a YOLO {} wish via {} — topic: {}",
wish.name,
agent.label(),
t
),
None => println!(
" 🎲 Granting a YOLO {} wish via {} — topic: surprise me",
wish.name,
agent.label()
),
}
println!();
println!(" Running {} (can take 30-90s)…", agent.bin());
println!();
let yaml = run_agent(agent, &prompt, &cwd)?;
fs::write(&out_path, &yaml).with_context(|| format!("writing {}", out_path.display()))?;
let page_url = out_path
.file_stem()
.and_then(|s| s.to_str())
.map(|stem| format!("http://localhost:3000/{}.html", stem))
.unwrap_or_else(|| "http://localhost:3000/".to_string());
println!();
println!(" ✓ Wrote {}", out_path.display());
println!();
println!(" Next:");
println!(" kazam dev .");
println!(" → open {}", page_url);
println!();
Ok(())
}
fn detect_agent() -> Option<Agent> {
Agent::AUTO_DETECT_ORDER
.iter()
.copied()
.find(|a| cmd_on_path(a.bin()))
}
fn cmd_on_path(name: &str) -> bool {
let path = match std::env::var_os("PATH") {
Some(p) => p,
None => return false,
};
for dir in std::env::split_paths(&path) {
if dir.join(name).is_file() {
return true;
}
}
false
}
fn run_agent(agent: Agent, prompt: &str, cwd: &Path) -> Result<String> {
let mut child = agent
.command(prompt)
.current_dir(cwd)
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.with_context(|| {
format!(
"could not start `{}` — is it installed and on your $PATH?",
agent.bin()
)
})?;
let mut stdout = child.stdout.take().expect("captured stdout");
let mut raw = String::new();
stdout.read_to_string(&mut raw)?;
let status = child.wait()?;
if !status.success() {
bail!("{} exited with {}", agent.label(), status);
}
Ok(strip_code_fence(&raw))
}
fn strip_code_fence(s: &str) -> String {
let trimmed = s.trim();
if let Some(rest) = trimmed.strip_prefix("```") {
let after_lang = rest.find('\n').map(|i| &rest[i + 1..]).unwrap_or(rest);
if let Some(body) = after_lang.rsplit_once("```") {
return body.0.trim_end().to_string() + "\n";
}
}
if !trimmed.ends_with('\n') {
format!("{}\n", trimmed)
} else {
trimmed.to_string()
}
}
fn list() -> Result<()> {
println!();
println!(" Available wishes:");
println!();
for w in all() {
println!(" kazam wish {:<10} {}", w.name, w.description);
}
println!();
println!(" Flags:");
println!(" --agent <claude|gemini|codex|opencode> Force a specific agent");
println!(" --yolo [topic] Skip workspace, let the agent invent everything");
println!(" --dry-run Print the prompt instead of running the agent");
println!(" --stdout Print the portable wish markdown");
println!(" --out <path> Where to write the populated YAML");
println!();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_code_fence_removes_yaml_wrapper() {
let raw = "```yaml\ntitle: hi\nshell: deck\n```\n";
let s = strip_code_fence(raw);
assert!(s.starts_with("title: hi"));
assert!(!s.contains("```"));
}
#[test]
fn strip_code_fence_passes_through_plain_yaml() {
let raw = "title: hi\nshell: deck\n";
let s = strip_code_fence(raw);
assert!(s.starts_with("title: hi"));
}
#[test]
fn all_wishes_have_unique_names() {
let mut names: Vec<&str> = all().iter().map(|w| w.name).collect();
names.sort();
let before = names.len();
names.dedup();
assert_eq!(before, names.len(), "duplicate wish names");
}
}