Skip to main content

lean_ctx/
rules_inject.rs

1use std::path::PathBuf;
2
3const MARKER: &str = "# lean-ctx — Context Engineering Layer";
4
5const RULES_MD: &str = r#"# lean-ctx — Context Engineering Layer
6
7Always use lean-ctx MCP tools instead of native equivalents:
8- ctx_read instead of Read/cat for file reads
9- ctx_shell instead of Shell/bash for commands
10- ctx_search instead of Grep/search for code search
11- ctx_tree instead of ls/find for directory listing
12
13Keep using native tools for: Write, StrReplace, Delete, Glob (no lean-ctx replacement).
14Do NOT fall back to native tools for reading, shell, search, or tree operations."#;
15
16const RULES_CURSORRULES: &str = r#"# lean-ctx — Context Engineering Layer
17
18Always use lean-ctx MCP tools. Never use the standard equivalents.
19
20## Tool Mapping
21
22| Instead of | Use |
23|---|-----|
24| `Read` | `ctx_read` |
25| `Shell` | `ctx_shell` |
26| `Grep` | `ctx_search` |
27| `ls`, `find` | `ctx_tree` |
28
29The `Write`, `StrReplace`, `Delete`, `Glob` tools have no lean-ctx equivalent — use them normally."#;
30
31struct RulesTarget {
32    name: &'static str,
33    path: PathBuf,
34    format: RulesFormat,
35}
36
37enum RulesFormat {
38    Markdown,
39    CursorMdc,
40}
41
42pub fn inject_all_rules(home: &std::path::Path) -> (Vec<String>, Vec<String>, Vec<String>) {
43    let targets = build_rules_targets(home);
44
45    let mut injected = Vec::new();
46    let mut already = Vec::new();
47    let mut errors = Vec::new();
48
49    for target in &targets {
50        if !is_tool_detected(target, home) {
51            continue;
52        }
53
54        match inject_rules(target) {
55            Ok(RulesResult::Injected) => injected.push(target.name.to_string()),
56            Ok(RulesResult::AlreadyPresent) => already.push(target.name.to_string()),
57            Err(e) => errors.push(format!("{}: {e}", target.name)),
58        }
59    }
60
61    (injected, already, errors)
62}
63
64enum RulesResult {
65    Injected,
66    AlreadyPresent,
67}
68
69fn inject_rules(target: &RulesTarget) -> Result<RulesResult, String> {
70    if target.path.exists() {
71        let content = std::fs::read_to_string(&target.path).map_err(|e| e.to_string())?;
72        if content.contains(MARKER) {
73            return Ok(RulesResult::AlreadyPresent);
74        }
75    }
76
77    if let Some(parent) = target.path.parent() {
78        std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
79    }
80
81    match target.format {
82        RulesFormat::Markdown => append_markdown(&target.path),
83        RulesFormat::CursorMdc => write_cursor_mdc(&target.path),
84    }
85}
86
87fn append_markdown(path: &std::path::Path) -> Result<RulesResult, String> {
88    let mut content = if path.exists() {
89        std::fs::read_to_string(path).map_err(|e| e.to_string())?
90    } else {
91        String::new()
92    };
93
94    if !content.is_empty() && !content.ends_with('\n') {
95        content.push('\n');
96    }
97    if !content.is_empty() {
98        content.push('\n');
99    }
100    content.push_str(RULES_MD);
101    content.push('\n');
102
103    std::fs::write(path, content).map_err(|e| e.to_string())?;
104    Ok(RulesResult::Injected)
105}
106
107fn write_cursor_mdc(path: &std::path::Path) -> Result<RulesResult, String> {
108    if path.exists() {
109        let content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
110        if content.contains(MARKER) {
111            return Ok(RulesResult::AlreadyPresent);
112        }
113    }
114
115    std::fs::write(path, RULES_CURSORRULES).map_err(|e| e.to_string())?;
116    Ok(RulesResult::Injected)
117}
118
119fn is_tool_detected(target: &RulesTarget, home: &std::path::Path) -> bool {
120    match target.name {
121        "Claude Code" => {
122            if let Ok(output) = std::process::Command::new("which").arg("claude").output() {
123                if output.status.success() {
124                    return true;
125                }
126            }
127            home.join(".claude.json").exists()
128        }
129        "Codex CLI" => {
130            home.join(".codex").exists() || {
131                std::process::Command::new("which")
132                    .arg("codex")
133                    .output()
134                    .map(|o| o.status.success())
135                    .unwrap_or(false)
136            }
137        }
138        "Cursor" => home.join(".cursor").exists(),
139        "Windsurf" => home.join(".codeium/windsurf").exists(),
140        "Gemini CLI" => home.join(".gemini").exists(),
141        "VS Code / Copilot" => detect_vscode_installed(),
142        "Zed" => home.join(".config/zed").exists(),
143        "Cline" => detect_cline_installed(),
144        "Roo Code" => detect_roo_installed(),
145        "OpenCode" => home.join(".config/opencode").exists(),
146        _ => false,
147    }
148}
149
150fn detect_vscode_installed() -> bool {
151    if let Some(home) = dirs::home_dir() {
152        #[cfg(target_os = "macos")]
153        if home
154            .join("Library/Application Support/Code/User/settings.json")
155            .exists()
156        {
157            return true;
158        }
159        #[cfg(target_os = "linux")]
160        if home.join(".config/Code/User/settings.json").exists() {
161            return true;
162        }
163    }
164    false
165}
166
167fn detect_cline_installed() -> bool {
168    if let Some(home) = dirs::home_dir() {
169        #[cfg(target_os = "macos")]
170        {
171            if home
172                .join("Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev")
173                .exists()
174            {
175                return true;
176            }
177        }
178        #[cfg(target_os = "linux")]
179        {
180            if home
181                .join(".config/Code/User/globalStorage/saoudrizwan.claude-dev")
182                .exists()
183            {
184                return true;
185            }
186        }
187    }
188    false
189}
190
191fn detect_roo_installed() -> bool {
192    if let Some(home) = dirs::home_dir() {
193        #[cfg(target_os = "macos")]
194        {
195            if home
196                .join("Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline")
197                .exists()
198            {
199                return true;
200            }
201        }
202        #[cfg(target_os = "linux")]
203        {
204            if home
205                .join(".config/Code/User/globalStorage/rooveterinaryinc.roo-cline")
206                .exists()
207            {
208                return true;
209            }
210        }
211    }
212    false
213}
214
215fn build_rules_targets(home: &std::path::Path) -> Vec<RulesTarget> {
216    vec![
217        RulesTarget {
218            name: "Claude Code",
219            path: home.join(".claude/CLAUDE.md"),
220            format: RulesFormat::Markdown,
221        },
222        RulesTarget {
223            name: "Codex CLI",
224            path: home.join(".codex/instructions.md"),
225            format: RulesFormat::Markdown,
226        },
227        RulesTarget {
228            name: "Cursor",
229            path: home.join(".cursor/rules/lean-ctx.mdc"),
230            format: RulesFormat::CursorMdc,
231        },
232        RulesTarget {
233            name: "Windsurf",
234            path: home.join(".codeium/windsurf/rules/lean-ctx.md"),
235            format: RulesFormat::Markdown,
236        },
237        RulesTarget {
238            name: "Gemini CLI",
239            path: home.join(".gemini/GEMINI.md"),
240            format: RulesFormat::Markdown,
241        },
242        RulesTarget {
243            name: "VS Code / Copilot",
244            path: copilot_instructions_path(home),
245            format: RulesFormat::Markdown,
246        },
247        RulesTarget {
248            name: "Zed",
249            path: home.join(".config/zed/rules/lean-ctx.md"),
250            format: RulesFormat::Markdown,
251        },
252        RulesTarget {
253            name: "Cline",
254            path: home.join(".cline/rules/lean-ctx.md"),
255            format: RulesFormat::Markdown,
256        },
257        RulesTarget {
258            name: "Roo Code",
259            path: home.join(".roo/rules/lean-ctx.md"),
260            format: RulesFormat::Markdown,
261        },
262        RulesTarget {
263            name: "OpenCode",
264            path: home.join(".config/opencode/rules/lean-ctx.md"),
265            format: RulesFormat::Markdown,
266        },
267    ]
268}
269
270fn copilot_instructions_path(home: &std::path::Path) -> PathBuf {
271    #[cfg(target_os = "macos")]
272    {
273        return home.join("Library/Application Support/Code/User/github-copilot-instructions.md");
274    }
275    #[cfg(target_os = "linux")]
276    {
277        return home.join(".config/Code/User/github-copilot-instructions.md");
278    }
279    #[cfg(target_os = "windows")]
280    {
281        if let Ok(appdata) = std::env::var("APPDATA") {
282            return PathBuf::from(appdata).join("Code/User/github-copilot-instructions.md");
283        }
284    }
285    #[allow(unreachable_code)]
286    home.join(".config/Code/User/github-copilot-instructions.md")
287}