use std::{
fs::{create_dir_all, write},
io::IsTerminal,
path::{Path, PathBuf},
};
use anyhow::{Context, Result};
use clap::{Arg, ArgMatches, Command};
use dialoguer::{Select, theme::ColorfulTheme};
use include_dir::{Dir, DirEntry, File, include_dir};
use crate::{CliCommand, core::command::command};
static SKILLS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/assets/forklaunch-skills");
const AGENTS: [(&str, &str, &str); 5] = [
("claude", "Claude Code", ".claude/skills/"),
("codex", "Codex", "AGENTS.md"),
("cursor", "Cursor", ".cursor/rules/forklaunch.mdc"),
("windsurf", "Windsurf", ".windsurf/rules/forklaunch.md"),
("generic", "Something else / not sure", "AGENTS.md"),
];
const DEFAULT_AGENT: &str = "claude";
fn resolve_agent(matches: &ArgMatches) -> Result<String> {
if let Some(agent) = matches.get_one::<String>("agent") {
return Ok(agent.clone());
}
if !std::io::stdin().is_terminal() {
return Ok(DEFAULT_AGENT.to_string());
}
let items: Vec<String> = AGENTS
.iter()
.map(|(_, label, dest)| format!("{label} → {dest}"))
.collect();
let default_index = AGENTS
.iter()
.position(|(id, _, _)| *id == DEFAULT_AGENT)
.unwrap_or(0);
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Which coding agent are you using?")
.items(&items)
.default(default_index)
.interact()
.context("Failed to read the coding agent selection")?;
Ok(AGENTS[selection].0.to_string())
}
pub(crate) struct ContextCommand;
impl ContextCommand {
pub(crate) fn new() -> Self {
Self
}
}
impl CliCommand for ContextCommand {
fn command(&self) -> Command {
command(
"context",
"Drop the ForkLaunch skill/context pack for a coding agent (Claude Code, Cursor, …).",
)
.alias("skills")
.arg(
Arg::new("agent")
.short('a')
.long("agent")
.help(
"Target coding agent — determines where the context files are written. \
Omit it to be asked interactively (defaults to claude when not on a TTY)",
)
.value_parser(
AGENTS
.iter()
.map(|(id, _, _)| *id)
.collect::<Vec<&'static str>>(),
),
)
.arg(
Arg::new("path")
.short('p')
.long("path")
.help("Output directory (defaults to the current directory)"),
)
}
fn handler(&self, matches: &ArgMatches) -> Result<()> {
let agent = resolve_agent(matches)?;
let agent = agent.as_str();
let out = match matches.get_one::<String>("path") {
Some(p) => PathBuf::from(p),
None => std::env::current_dir().context("could not resolve the current directory")?,
};
let files = collect_md_files(&SKILLS);
match agent {
"claude" => {
for f in &files {
let dest = out.join(".claude").join("skills").join(f.path());
if let Some(parent) = dest.parent() {
create_dir_all(parent)?;
}
write(&dest, f.contents())?;
}
eprintln!(
"✓ Wrote {} ForkLaunch skill files to .claude/skills/",
files.len()
);
}
"cursor" => {
let dest = out.join(".cursor").join("rules").join("forklaunch.mdc");
write_bundle(&dest, &files, true)?;
eprintln!("✓ Wrote ForkLaunch rules to .cursor/rules/forklaunch.mdc");
}
"windsurf" => {
let dest = out.join(".windsurf").join("rules").join("forklaunch.md");
write_bundle(&dest, &files, false)?;
eprintln!("✓ Wrote ForkLaunch rules to .windsurf/rules/forklaunch.md");
}
_ => {
let dest = out.join("AGENTS.md");
write_bundle(&dest, &files, false)?;
eprintln!("✓ Wrote ForkLaunch context to AGENTS.md");
}
}
Ok(())
}
}
fn collect_md_files<'a>(dir: &'a Dir<'a>) -> Vec<&'a File<'a>> {
let mut out = Vec::new();
collect_into(dir, &mut out);
out.sort_by_key(|f| f.path().to_path_buf());
out
}
fn collect_into<'a>(dir: &'a Dir<'a>, out: &mut Vec<&'a File<'a>>) {
for entry in dir.entries() {
match entry {
DirEntry::File(f) => {
if f.path().extension().and_then(|e| e.to_str()) == Some("md") {
out.push(f);
}
}
DirEntry::Dir(d) => collect_into(d, out),
}
}
}
fn write_bundle(dest: &Path, files: &[&File], mdc: bool) -> Result<()> {
if let Some(parent) = dest.parent() {
create_dir_all(parent)?;
}
let mut body = String::new();
if mdc {
body.push_str("---\ndescription: ForkLaunch framework conventions and skills\nalwaysApply: true\n---\n\n");
}
body.push_str("# ForkLaunch Skill Pack\n\nFollow these conventions when building this ForkLaunch app.\n");
for f in files {
body.push_str(&format!(
"\n\n<!-- ===== {} ===== -->\n\n",
f.path().display()
));
body.push_str(f.contents_utf8().unwrap_or(""));
}
write(dest, body)?;
Ok(())
}