Skip to main content

ferro_cli/commands/
boost_install.rs

1//! boost:install command - Generate MCP configuration and AI guidelines
2
3use console::style;
4use std::fs;
5use std::path::Path;
6
7use crate::templates;
8
9pub fn run(editor: Option<String>) {
10    // Verify we're in a Ferro project directory
11    if !Path::new("Cargo.toml").exists() {
12        eprintln!("{} Cargo.toml not found", style("Error:").red().bold());
13        eprintln!(
14            "{}",
15            style("Make sure you're in a Ferro project root directory.").dim()
16        );
17        std::process::exit(1);
18    }
19
20    // Detect or use specified editor
21    let target_editor = editor.unwrap_or_else(detect_editor);
22
23    println!(
24        "{} Installing AI development boost for {}...",
25        style("⚡").cyan(),
26        style(&target_editor).yellow()
27    );
28    println!();
29
30    // Generate MCP configuration
31    generate_mcp_config(&target_editor);
32
33    // Generate AI guidelines
34    generate_ai_guidelines(&target_editor);
35
36    // Print success message
37    println!();
38    println!(
39        "{}",
40        style("AI development boost installed successfully!")
41            .green()
42            .bold()
43    );
44    println!();
45
46    // Print editor-specific instructions
47    match target_editor.as_str() {
48        "cursor" => {
49            println!("To activate MCP in Cursor:");
50            println!("  1. Open Command Palette (Cmd+Shift+P / Ctrl+Shift+P)");
51            println!("  2. Search for 'Reload Window'");
52            println!("  3. The Ferro MCP tools will now be available");
53        }
54        "claude" => {
55            println!("MCP configuration written to {}", style(".mcp.json").cyan());
56            println!("CLAUDE.md updated with Ferro framework guidelines.");
57            println!();
58            println!("Claude Code will automatically use these configurations.");
59        }
60        "vscode" => {
61            println!(
62                "AI guidelines written to {}",
63                style(".ai/guidelines/").cyan()
64            );
65            println!("GitHub Copilot will use these guidelines for context.");
66        }
67        _ => {
68            println!("Configuration files have been generated.");
69        }
70    }
71
72    println!();
73}
74
75fn detect_editor() -> String {
76    // Check for editor-specific files/directories
77    if Path::new(".cursor").exists() {
78        return "cursor".to_string();
79    }
80
81    if Path::new("CLAUDE.md").exists() || std::env::var("CLAUDE_CODE").is_ok() {
82        return "claude".to_string();
83    }
84
85    if Path::new(".vscode").exists() {
86        return "vscode".to_string();
87    }
88
89    // Default to claude as it's the most common for MCP
90    "claude".to_string()
91}
92
93fn generate_mcp_config(editor: &str) {
94    let config_path = match editor {
95        "cursor" => {
96            // Cursor uses .cursor/mcp.json
97            fs::create_dir_all(".cursor").ok();
98            ".cursor/mcp.json"
99        }
100        _ => {
101            // Claude and others use .mcp.json at root
102            ".mcp.json"
103        }
104    };
105
106    // Try to find the ferro binary path
107    let ferro_command = find_ferro_binary();
108    let config_content = format!(
109        r#"{{
110  "mcpServers": {{
111    "ferro": {{
112      "command": "{}",
113      "args": ["mcp"],
114      "env": {{}}
115    }}
116  }}
117}}
118"#,
119        ferro_command.replace('\\', "\\\\").replace('"', "\\\"")
120    );
121
122    if Path::new(config_path).exists() {
123        println!(
124            "{} {} already exists, skipping",
125            style("→").dim(),
126            config_path
127        );
128    } else {
129        if let Err(e) = fs::write(config_path, &config_content) {
130            eprintln!(
131                "{} Failed to write {}: {}",
132                style("Error:").red().bold(),
133                config_path,
134                e
135            );
136            return;
137        }
138        println!("{} Created {}", style("✓").green(), config_path);
139    }
140}
141
142fn find_ferro_binary() -> String {
143    // First, check if ferro is in PATH
144    if let Ok(output) = std::process::Command::new("which").arg("ferro").output() {
145        if output.status.success() {
146            if let Ok(path) = String::from_utf8(output.stdout) {
147                let path = path.trim();
148                if !path.is_empty() {
149                    return path.to_string();
150                }
151            }
152        }
153    }
154
155    // On Windows, try where instead
156    #[cfg(windows)]
157    if let Ok(output) = std::process::Command::new("where").arg("ferro").output() {
158        if output.status.success() {
159            if let Ok(path) = String::from_utf8(output.stdout) {
160                if let Some(first_line) = path.lines().next() {
161                    return first_line.to_string();
162                }
163            }
164        }
165    }
166
167    // Try to get the current executable's directory
168    if let Ok(current_exe) = std::env::current_exe() {
169        if let Some(exe_dir) = current_exe.parent() {
170            let ferro_in_same_dir = exe_dir.join("ferro");
171            if ferro_in_same_dir.exists() {
172                return ferro_in_same_dir.to_string_lossy().to_string();
173            }
174        }
175        // If this IS the ferro binary, use its path
176        if current_exe
177            .file_name()
178            .map(|n| n == "ferro")
179            .unwrap_or(false)
180        {
181            return current_exe.to_string_lossy().to_string();
182        }
183    }
184
185    // Fall back to just "ferro" and hope it's in PATH
186    "ferro".to_string()
187}
188
189fn generate_ai_guidelines(editor: &str) {
190    // Create .ai/guidelines directory
191    let guidelines_dir = Path::new(".ai/guidelines");
192    if let Err(e) = fs::create_dir_all(guidelines_dir) {
193        eprintln!(
194            "{} Failed to create .ai/guidelines: {}",
195            style("Error:").red().bold(),
196            e
197        );
198        return;
199    }
200
201    // Generate Ferro framework guidelines
202    let ferro_md_path = guidelines_dir.join("ferro.md");
203    if !ferro_md_path.exists() {
204        let content = templates::ferro_guidelines_template();
205        if let Err(e) = fs::write(&ferro_md_path, content) {
206            eprintln!(
207                "{} Failed to write ferro.md: {}",
208                style("Error:").red().bold(),
209                e
210            );
211        } else {
212            println!("{} Created .ai/guidelines/ferro.md", style("✓").green());
213        }
214    } else {
215        println!(
216            "{} .ai/guidelines/ferro.md already exists, skipping",
217            style("→").dim()
218        );
219    }
220
221    // Generate editor-specific rules
222    match editor {
223        "cursor" => {
224            let cursor_rules_path = Path::new(".cursorrules");
225            if !cursor_rules_path.exists() {
226                let content = templates::cursor_rules_template();
227                if let Err(e) = fs::write(cursor_rules_path, content) {
228                    eprintln!(
229                        "{} Failed to write .cursorrules: {}",
230                        style("Error:").red().bold(),
231                        e
232                    );
233                } else {
234                    println!("{} Created .cursorrules", style("✓").green());
235                }
236            } else {
237                println!("{} .cursorrules already exists, skipping", style("→").dim());
238            }
239        }
240        "claude" => {
241            let claude_md_path = Path::new("CLAUDE.md");
242            if !claude_md_path.exists() {
243                let content = templates::claude_md_template();
244                if let Err(e) = fs::write(claude_md_path, content) {
245                    eprintln!(
246                        "{} Failed to write CLAUDE.md: {}",
247                        style("Error:").red().bold(),
248                        e
249                    );
250                } else {
251                    println!("{} Created CLAUDE.md", style("✓").green());
252                }
253            } else {
254                // Append Ferro-specific instructions if not already present
255                let existing = fs::read_to_string(claude_md_path).unwrap_or_default();
256                if !existing.contains("Ferro Framework") {
257                    let ferro_section = templates::claude_md_ferro_section();
258                    if let Err(e) = fs::write(
259                        claude_md_path,
260                        format!("{}\n\n{}", existing.trim(), ferro_section),
261                    ) {
262                        eprintln!(
263                            "{} Failed to update CLAUDE.md: {}",
264                            style("Error:").red().bold(),
265                            e
266                        );
267                    } else {
268                        println!(
269                            "{} Updated CLAUDE.md with Ferro guidelines",
270                            style("✓").green()
271                        );
272                    }
273                } else {
274                    println!(
275                        "{} CLAUDE.md already contains Ferro guidelines, skipping",
276                        style("→").dim()
277                    );
278                }
279            }
280        }
281        "vscode" => {
282            let copilot_path = guidelines_dir.join("copilot.md");
283            if !copilot_path.exists() {
284                let content = templates::copilot_instructions_template();
285                if let Err(e) = fs::write(&copilot_path, content) {
286                    eprintln!(
287                        "{} Failed to write copilot.md: {}",
288                        style("Error:").red().bold(),
289                        e
290                    );
291                } else {
292                    println!("{} Created .ai/guidelines/copilot.md", style("✓").green());
293                }
294            } else {
295                println!(
296                    "{} .ai/guidelines/copilot.md already exists, skipping",
297                    style("→").dim()
298                );
299            }
300        }
301        _ => {}
302    }
303}