use anyhow::{Context, Result};
use std::fs;
use crate::config::Registry;
use crate::output;
pub const EMBEDDED_SKILL_MD: &str = include_str!("../../.agents/skills/dev-prune/SKILL.md");
pub const EMBEDDED_RULES_MD: &str = include_str!("../../.agents/rules/dev-prune.rules.md");
#[derive(clap::ValueEnum, Clone, Copy, Debug)]
pub enum AgentEditor {
Cursor,
Windsurf,
Antigravity,
Cline,
Copilot,
AgentsMd,
}
pub fn run(agent: Option<AgentEditor>) -> Result<()> {
if let Some(editor) = agent {
return write_agent_rules(editor);
}
output::print_header("dev-prune AI Agent Skill Integration");
let skill_path = {
let config_dir =
Registry::config_dir().context("could not resolve the config directory")?;
fs::create_dir_all(&config_dir)
.with_context(|| format!("could not create {}", output::clean_path(&config_dir)))?;
let target = config_dir.join("SKILL.md");
fs::write(&target, EMBEDDED_SKILL_MD)
.with_context(|| format!("could not write {}", output::clean_path(&target)))?;
output::clean_path(&target)
};
output::print_success(&format!("Bundled SKILL.md exported to `{skill_path}`"));
let agent_roots = crate::setup::agent_skill_roots();
match crate::setup::ensure_agent_skills() {
crate::setup::Outcome::Installed | crate::setup::Outcome::AlreadyPresent => {
for root in &agent_roots {
output::print_success(&format!(
"Skill installed for your AI agent at `{}`",
output::clean_path(root.join("SKILL.md"))
));
}
}
crate::setup::Outcome::Skipped(_) => {
output::print_info(
"No AI agent skills directory was found — use the prompts below instead.",
);
}
crate::setup::Outcome::Failed(why) => {
output::print_warning(&format!(
"Could not install into the agent skills directory: {why}"
));
}
}
println!();
output::print_header("🤖 AI Agent Onboarding Prompts (Copy & Paste to your AI Assistant)");
println!();
output::print_info("Prompt 1: Initial Workspace Discovery & Onboarding");
println!("```markdown");
println!(
"Read the dev-prune AI skill at file://{skill_path} and run `devp init` to scan, register, and onboard all Git repositories in my workspace."
);
println!("```");
println!();
output::print_info(
"Prompt 2: Universal Skill Import (Antigravity, Claude Code, Cursor, Windsurf, Copilot, OpenClaw)",
);
println!("```markdown");
println!(
"I have installed `dev-prune` on my machine. Read the skill at file://{skill_path} and import it into your agent skills folder so you can autonomously maintain lockfiles and prune bloat directories."
);
println!("```");
Ok(())
}
fn write_agent_rules(editor: AgentEditor) -> Result<()> {
let cwd = std::env::current_dir().context("could not read the current directory")?;
if !crate::scanner::is_git_repo(&cwd) {
anyhow::bail!(
"`--agent` writes rules into a repository, and the current directory is not \
one. Run it from the repository root."
);
}
let written = match editor {
AgentEditor::Cursor => {
let target = cwd.join(crate::constants::CURSOR_RULES_FILE);
let content = format!(
"---\ndescription: dev-prune (devp) — reclaiming disk space from idle \
repositories safely\nalwaysApply: false\n---\n\n{EMBEDDED_RULES_MD}"
);
write_rules_file(&target, &content)?;
target
}
AgentEditor::Windsurf => {
let target = cwd.join(crate::constants::WINDSURF_RULES_FILE);
write_rules_file(&target, EMBEDDED_RULES_MD)?;
target
}
AgentEditor::Antigravity => {
let target = cwd.join(crate::constants::ANTIGRAVITY_RULES_FILE);
write_rules_file(&target, EMBEDDED_RULES_MD)?;
target
}
AgentEditor::Cline => {
let target = cwd.join(crate::constants::CLINE_RULES_FILE);
write_rules_file(&target, EMBEDDED_RULES_MD)?;
target
}
AgentEditor::Copilot => {
let target = cwd.join(crate::constants::COPILOT_INSTRUCTIONS_FILE);
let existing = fs::read_to_string(&target).unwrap_or_default();
write_rules_file(&target, &upsert_marked_block(&existing))?;
target
}
AgentEditor::AgentsMd => {
let target = cwd.join(crate::constants::AGENTS_MD_FILE);
let existing = fs::read_to_string(&target).unwrap_or_default();
write_rules_file(&target, &upsert_marked_block(&existing))?;
target
}
};
output::print_success(&format!("Rules written: {}", output::clean_path(&written)));
output::print_info(
"Commit the file if the whole team's agents should have it; it is inert data \
and safe to share.",
);
Ok(())
}
fn upsert_marked_block(existing: &str) -> String {
let block = format!(
"{}\n{EMBEDDED_RULES_MD}{}\n",
crate::constants::RULES_BLOCK_START,
crate::constants::RULES_BLOCK_END
);
match (
existing.find(crate::constants::RULES_BLOCK_START),
existing.find(crate::constants::RULES_BLOCK_END),
) {
(Some(start), Some(end)) if end > start => {
let after = end + crate::constants::RULES_BLOCK_END.len();
let after = if existing[after..].starts_with('\n') {
after + 1
} else {
after
};
format!("{}{block}{}", &existing[..start], &existing[after..])
}
_ if existing.is_empty() => block,
_ => format!("{}\n\n{block}", existing.trim_end_matches('\n')),
}
}
fn write_rules_file(target: &std::path::Path, content: &str) -> Result<()> {
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("could not create {}", output::clean_path(parent)))?;
}
fs::write(target, content)
.with_context(|| format!("could not write {}", output::clean_path(target)))
}