lean_ctx/rules_inject/
skills.rs1use std::path::PathBuf;
4
5use super::detect::command_exists;
6
7pub(super) const SKILL_TEMPLATE: &str = include_str!("../templates/SKILL.md");
12
13pub(super) struct SkillTarget {
14 agent_key: &'static str,
15 display_name: &'static str,
16 skill_dir: PathBuf,
17}
18
19pub(super) fn build_skill_targets(home: &std::path::Path) -> Vec<SkillTarget> {
20 vec![
21 SkillTarget {
22 agent_key: "claude",
23 display_name: "Claude Code",
24 skill_dir: crate::setup::claude_config_dir(home).join("skills/lean-ctx"),
25 },
26 SkillTarget {
27 agent_key: "codebuddy",
28 display_name: "CodeBuddy",
29 skill_dir: crate::core::editor_registry::codebuddy_state_dir(home)
30 .join("skills/lean-ctx"),
31 },
32 SkillTarget {
33 agent_key: "cursor",
34 display_name: "Cursor",
35 skill_dir: home.join(".cursor/skills/lean-ctx"),
36 },
37 SkillTarget {
38 agent_key: "codex",
39 display_name: "Codex CLI",
40 skill_dir: crate::core::home::resolve_codex_dir()
41 .unwrap_or_else(|| home.join(".codex"))
42 .join("skills/lean-ctx"),
43 },
44 SkillTarget {
45 agent_key: "grok",
46 display_name: "Grok",
47 skill_dir: home.join(".grok/skills/lean-ctx"),
48 },
49 SkillTarget {
50 agent_key: "copilot",
51 display_name: "GitHub Copilot",
52 skill_dir: home.join(".copilot/skills/lean-ctx"),
53 },
54 SkillTarget {
55 agent_key: "openclaw",
56 display_name: "OpenClaw",
57 skill_dir: home.join(".openclaw/skills/lean-ctx"),
58 },
59 SkillTarget {
60 agent_key: "opencode",
61 display_name: "OpenCode",
62 skill_dir: home.join(".config/opencode/skills/lean-ctx"),
63 },
64 ]
65}
66
67fn is_skill_agent_detected(agent_key: &str, home: &std::path::Path) -> bool {
68 match agent_key {
69 "claude" => {
70 command_exists("claude")
71 || crate::core::editor_registry::claude_mcp_json_path(home).exists()
72 || crate::core::editor_registry::claude_state_dir(home).exists()
73 }
74 "codebuddy" => {
75 command_exists("codebuddy")
76 || crate::core::editor_registry::codebuddy_mcp_json_path(home).exists()
77 || crate::core::editor_registry::codebuddy_state_dir(home).exists()
78 }
79 "cursor" => home.join(".cursor").exists(),
80 "codex" => {
81 let codex_dir =
82 crate::core::home::resolve_codex_dir().unwrap_or_else(|| home.join(".codex"));
83 codex_dir.exists() || command_exists("codex")
84 }
85 "grok" => home.join(".grok").exists() || command_exists("grok"),
86 "copilot" => {
87 home.join(".copilot").exists()
88 || home.join(".copilot/mcp-config.json").exists()
89 || command_exists("copilot")
90 }
91 "openclaw" => home.join(".openclaw").exists() || command_exists("openclaw"),
92 "opencode" => home.join(".config/opencode").exists() || command_exists("opencode"),
93 _ => false,
94 }
95}
96
97pub fn install_skill_for_agent(home: &std::path::Path, agent_key: &str) -> Result<PathBuf, String> {
99 let targets = build_skill_targets(home);
100 let target = targets
101 .into_iter()
102 .find(|t| t.agent_key == agent_key)
103 .ok_or_else(|| format!("No skill target for agent '{agent_key}'"))?;
104
105 let skill_path = target.skill_dir.join("SKILL.md");
106 std::fs::create_dir_all(&target.skill_dir).map_err(|e| e.to_string())?;
107
108 if skill_path.exists() {
109 let existing = std::fs::read_to_string(&skill_path).unwrap_or_default();
110 if existing == SKILL_TEMPLATE {
111 return Ok(skill_path);
112 }
113 }
114
115 crate::config_io::write_atomic_with_backup(&skill_path, SKILL_TEMPLATE)?;
116 Ok(skill_path)
117}
118
119pub fn install_all_skills(home: &std::path::Path) -> Vec<(String, bool)> {
122 if crate::core::config::Config::load().rules_injection_effective()
126 == crate::core::config::RulesInjection::Off
127 {
128 return Vec::new();
129 }
130 let targets = build_skill_targets(home);
131 let mut results = Vec::new();
132
133 for target in &targets {
134 if !is_skill_agent_detected(target.agent_key, home) {
135 continue;
136 }
137
138 let skill_path = target.skill_dir.join("SKILL.md");
139 let already_current = skill_path.exists()
140 && std::fs::read_to_string(&skill_path).is_ok_and(|c| c == SKILL_TEMPLATE);
141
142 if already_current {
143 results.push((target.display_name.to_string(), false));
144 continue;
145 }
146
147 if let Err(e) = std::fs::create_dir_all(&target.skill_dir) {
148 tracing::warn!(
149 "Failed to create skill dir for {}: {e}",
150 target.display_name
151 );
152 continue;
153 }
154
155 match crate::config_io::write_atomic_with_backup(&skill_path, SKILL_TEMPLATE) {
156 Ok(()) => results.push((target.display_name.to_string(), true)),
157 Err(e) => {
158 tracing::warn!("Failed to write SKILL.md for {}: {e}", target.display_name);
159 }
160 }
161 }
162
163 results
164}