ferro_cli/commands/
boost_install.rs1use console::style;
4use std::fs;
5use std::path::Path;
6
7use crate::templates;
8
9pub fn run(editor: Option<String>) {
10 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 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_config(&target_editor);
32
33 generate_ai_guidelines(&target_editor);
35
36 println!();
38 println!(
39 "{}",
40 style("AI development boost installed successfully!")
41 .green()
42 .bold()
43 );
44 println!();
45
46 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 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 "claude".to_string()
91}
92
93fn generate_mcp_config(editor: &str) {
94 let config_path = match editor {
95 "cursor" => {
96 fs::create_dir_all(".cursor").ok();
98 ".cursor/mcp.json"
99 }
100 _ => {
101 ".mcp.json"
103 }
104 };
105
106 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 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 #[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 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 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 "ferro".to_string()
187}
188
189fn generate_ai_guidelines(editor: &str) {
190 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 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 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 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}