dev_prune/commands/
skill.rs1use anyhow::{Context, Result};
6use std::fs;
7
8use crate::config::Registry;
9use crate::output;
10
11pub const EMBEDDED_SKILL_MD: &str = include_str!("../../.agents/skills/dev-prune/SKILL.md");
12
13pub const EMBEDDED_RULES_MD: &str = include_str!("../../.agents/rules/dev-prune.rules.md");
16
17#[derive(clap::ValueEnum, Clone, Copy, Debug)]
22pub enum AgentEditor {
23 Cursor,
25 Windsurf,
27 Antigravity,
29 Cline,
31 Copilot,
33 AgentsMd,
36}
37
38pub fn run(agent: Option<AgentEditor>) -> Result<()> {
41 if let Some(editor) = agent {
42 return write_agent_rules(editor);
43 }
44 output::print_header("dev-prune AI Agent Skill Integration");
45
46 let skill_path = {
49 let config_dir =
50 Registry::config_dir().context("could not resolve the config directory")?;
51 fs::create_dir_all(&config_dir)
52 .with_context(|| format!("could not create {}", output::clean_path(&config_dir)))?;
53 let target = config_dir.join("SKILL.md");
54 fs::write(&target, EMBEDDED_SKILL_MD)
55 .with_context(|| format!("could not write {}", output::clean_path(&target)))?;
56 output::clean_path(&target)
57 };
58
59 output::print_success(&format!("Bundled SKILL.md exported to `{skill_path}`"));
60
61 let agent_roots = crate::setup::agent_skill_roots();
64 match crate::setup::ensure_agent_skills() {
65 crate::setup::Outcome::Installed | crate::setup::Outcome::AlreadyPresent => {
66 for root in &agent_roots {
67 output::print_success(&format!(
68 "Skill installed for your AI agent at `{}`",
69 output::clean_path(root.join("SKILL.md"))
70 ));
71 }
72 }
73 crate::setup::Outcome::Skipped(_) => {
74 output::print_info(
75 "No AI agent skills directory was found — use the prompts below instead.",
76 );
77 }
78 crate::setup::Outcome::Failed(why) => {
79 output::print_warning(&format!(
80 "Could not install into the agent skills directory: {why}"
81 ));
82 }
83 }
84 println!();
85 output::print_header("🤖 AI Agent Onboarding Prompts (Copy & Paste to your AI Assistant)");
86 println!();
87 output::print_info("Prompt 1: Initial Workspace Discovery & Onboarding");
88 println!("```markdown");
89 println!(
90 "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."
91 );
92 println!("```");
93 println!();
94 output::print_info(
95 "Prompt 2: Universal Skill Import (Antigravity, Claude Code, Cursor, Windsurf, Copilot, OpenClaw)",
96 );
97 println!("```markdown");
98 println!(
99 "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."
100 );
101 println!("```");
102
103 Ok(())
104}
105
106fn write_agent_rules(editor: AgentEditor) -> Result<()> {
112 let cwd = std::env::current_dir().context("could not read the current directory")?;
113 if !crate::scanner::is_git_repo(&cwd) {
114 anyhow::bail!(
115 "`--agent` writes rules into a repository, and the current directory is not \
116 one. Run it from the repository root."
117 );
118 }
119
120 let written = match editor {
121 AgentEditor::Cursor => {
122 let target = cwd.join(crate::constants::CURSOR_RULES_FILE);
123 let content = format!(
124 "---\ndescription: dev-prune (devp) — reclaiming disk space from idle \
125 repositories safely\nalwaysApply: false\n---\n\n{EMBEDDED_RULES_MD}"
126 );
127 write_rules_file(&target, &content)?;
128 target
129 }
130 AgentEditor::Windsurf => {
131 let target = cwd.join(crate::constants::WINDSURF_RULES_FILE);
132 write_rules_file(&target, EMBEDDED_RULES_MD)?;
133 target
134 }
135 AgentEditor::Antigravity => {
136 let target = cwd.join(crate::constants::ANTIGRAVITY_RULES_FILE);
137 write_rules_file(&target, EMBEDDED_RULES_MD)?;
138 target
139 }
140 AgentEditor::Cline => {
141 let target = cwd.join(crate::constants::CLINE_RULES_FILE);
142 write_rules_file(&target, EMBEDDED_RULES_MD)?;
143 target
144 }
145 AgentEditor::Copilot => {
149 let target = cwd.join(crate::constants::COPILOT_INSTRUCTIONS_FILE);
150 let existing = fs::read_to_string(&target).unwrap_or_default();
151 write_rules_file(&target, &upsert_marked_block(&existing))?;
152 target
153 }
154 AgentEditor::AgentsMd => {
155 let target = cwd.join(crate::constants::AGENTS_MD_FILE);
156 let existing = fs::read_to_string(&target).unwrap_or_default();
157 write_rules_file(&target, &upsert_marked_block(&existing))?;
158 target
159 }
160 };
161
162 output::print_success(&format!("Rules written: {}", output::clean_path(&written)));
163 output::print_info(
164 "Commit the file if the whole team's agents should have it; it is inert data \
165 and safe to share.",
166 );
167 Ok(())
168}
169
170fn upsert_marked_block(existing: &str) -> String {
173 let block = format!(
174 "{}\n{EMBEDDED_RULES_MD}{}\n",
175 crate::constants::RULES_BLOCK_START,
176 crate::constants::RULES_BLOCK_END
177 );
178 match (
179 existing.find(crate::constants::RULES_BLOCK_START),
180 existing.find(crate::constants::RULES_BLOCK_END),
181 ) {
182 (Some(start), Some(end)) if end > start => {
183 let after = end + crate::constants::RULES_BLOCK_END.len();
184 let after = if existing[after..].starts_with('\n') {
186 after + 1
187 } else {
188 after
189 };
190 format!("{}{block}{}", &existing[..start], &existing[after..])
191 }
192 _ if existing.is_empty() => block,
193 _ => format!("{}\n\n{block}", existing.trim_end_matches('\n')),
194 }
195}
196
197fn write_rules_file(target: &std::path::Path, content: &str) -> Result<()> {
198 if let Some(parent) = target.parent() {
199 fs::create_dir_all(parent)
200 .with_context(|| format!("could not create {}", output::clean_path(parent)))?;
201 }
202 fs::write(target, content)
203 .with_context(|| format!("could not write {}", output::clean_path(target)))
204}