ferrous_forge/commands/
init.rs1use crate::{Result, config::Config};
4use console::style;
5
6pub async fn execute(force: bool) -> Result<()> {
13 println!(
14 "{}",
15 style("🔨 Initializing Ferrous Forge...").bold().cyan()
16 );
17
18 let config = Config::load_or_default().await?;
19 if check_already_initialized(&config, force)? {
20 return Ok(());
21 }
22
23 perform_initialization(config).await?;
24 print_completion_message();
25
26 Ok(())
27}
28
29pub async fn execute_project() -> Result<()> {
40 println!(
41 "{}",
42 style("🔨 Setting up Ferrous Forge project tooling...")
43 .bold()
44 .cyan()
45 );
46
47 let project_path = std::env::current_dir()
48 .map_err(|e| crate::Error::config(format!("Failed to get current directory: {}", e)))?;
49
50 let cargo_toml = project_path.join("Cargo.toml");
52 if !cargo_toml.exists() {
53 return Err(crate::Error::config(
54 "No Cargo.toml found. Run 'ferrous-forge init --project' inside a Rust project.",
55 ));
56 }
57
58 println!("📁 Project: {}", project_path.display());
59 println!();
60
61 write_rustfmt_toml(&project_path).await?;
62 write_clippy_toml(&project_path).await?;
63 write_vscode_settings(&project_path).await?;
64 inject_cargo_toml_lints(&cargo_toml).await?;
65 write_ferrous_config(&project_path).await?;
66 create_docs_scaffold(&project_path).await?;
67 write_ci_workflow(&project_path).await?;
68 install_project_git_hooks(&project_path).await?;
69
70 println!();
71 println!("{}", style("🎉 Project tooling installed!").bold().green());
72 println!();
73 println!("Next steps:");
74 println!(" cargo fmt — format code");
75 println!(" cargo clippy — enforce doc + quality lints (now configured)");
76 println!(" cargo doc --no-deps — verify documentation builds");
77 println!(" ferrous-forge validate — validate against Ferrous Forge standards");
78
79 Ok(())
80}
81
82fn check_already_initialized(config: &Config, force: bool) -> Result<bool> {
85 if config.is_initialized() && !force {
86 println!(
87 "{}",
88 style("✅ Ferrous Forge is already initialized!").green()
89 );
90 println!("Use --force to reinitialize.");
91 return Ok(true);
92 }
93 Ok(false)
94}
95
96async fn perform_initialization(config: Config) -> Result<()> {
97 println!("📁 Creating configuration directories...");
98 config.ensure_directories().await?;
99
100 println!("🔧 Setting up cargo command hijacking...");
101 install_cargo_hijacking().await?;
102
103 println!("📋 Installing clippy configuration...");
104 install_clippy_config().await?;
105
106 println!("🐚 Installing shell integration...");
107 install_shell_integration().await?;
108
109 let mut config = config;
110 config.mark_initialized();
111 config.save().await?;
112
113 Ok(())
114}
115
116fn print_completion_message() {
117 println!(
118 "{}",
119 style("🎉 Ferrous Forge initialization complete!")
120 .bold()
121 .green()
122 );
123 println!();
124 println!("Next steps:");
125 println!("• Restart your shell or run: source ~/.bashrc");
126 println!("• Create a new project: cargo new my-project");
127 println!("• All new projects will automatically use Edition 2024 + strict standards!");
128}
129
130async fn install_cargo_hijacking() -> Result<()> {
131 let home_dir =
132 dirs::home_dir().ok_or_else(|| crate::Error::config("Could not find home directory"))?;
133
134 let bin_dir = home_dir.join(".local").join("bin");
135 tokio::fs::create_dir_all(&bin_dir).await?;
136
137 let cargo_wrapper = include_str!("../../templates/cargo-wrapper.sh");
138 let cargo_path = bin_dir.join("cargo");
139 tokio::fs::write(&cargo_path, cargo_wrapper).await?;
140
141 #[cfg(unix)]
142 {
143 use std::os::unix::fs::PermissionsExt;
144 let mut perms = tokio::fs::metadata(&cargo_path).await?.permissions();
145 perms.set_mode(0o755);
146 tokio::fs::set_permissions(&cargo_path, perms).await?;
147 }
148
149 Ok(())
150}
151
152async fn install_clippy_config() -> Result<()> {
153 let home_dir =
154 dirs::home_dir().ok_or_else(|| crate::Error::config("Could not find home directory"))?;
155
156 let clippy_config = include_str!("../../templates/clippy.toml");
157 let clippy_path = home_dir.join(".clippy.toml");
158 tokio::fs::write(&clippy_path, clippy_config).await?;
159
160 Ok(())
161}
162
163async fn install_shell_integration() -> Result<()> {
164 let home_dir =
165 dirs::home_dir().ok_or_else(|| crate::Error::config("Could not find home directory"))?;
166
167 let shell_config = format!(
168 r#"
169# Ferrous Forge - Rust Development Standards Enforcer
170export PATH="$HOME/.local/bin:$PATH"
171
172# Enable Ferrous Forge for all Rust development
173export FERROUS_FORGE_ENABLED=1
174"#
175 );
176
177 for shell_file in &[".bashrc", ".zshrc", ".profile"] {
178 let shell_path = home_dir.join(shell_file);
179 if shell_path.exists() {
180 let mut contents = tokio::fs::read_to_string(&shell_path).await?;
181 if !contents.contains("Ferrous Forge") {
182 contents.push_str(&shell_config);
183 tokio::fs::write(&shell_path, contents).await?;
184 }
185 }
186 }
187
188 Ok(())
189}
190
191async fn write_rustfmt_toml(project_path: &std::path::Path) -> Result<()> {
194 let path = project_path.join("rustfmt.toml");
195 if path.exists() {
196 println!(" ⏭ rustfmt.toml already exists, skipping");
197 return Ok(());
198 }
199 let content = r#"# Ferrous Forge project rustfmt configuration
200max_width = 100
201imports_granularity = "Crate"
202group_imports = "StdExternalCrate"
203edition = "2024"
204"#;
205 tokio::fs::write(&path, content).await?;
206 println!(" ✅ Written: rustfmt.toml");
207 Ok(())
208}
209
210async fn write_clippy_toml(project_path: &std::path::Path) -> Result<()> {
211 let path = project_path.join("clippy.toml");
212 if path.exists() {
213 println!(" ⏭ clippy.toml already exists, skipping");
214 return Ok(());
215 }
216 let content = r#"# Ferrous Forge project clippy configuration
217too-many-lines-threshold = 50
218cognitive-complexity-threshold = 25
219"#;
220 tokio::fs::write(&path, content).await?;
221 println!(" ✅ Written: clippy.toml");
222 Ok(())
223}
224
225async fn write_vscode_settings(project_path: &std::path::Path) -> Result<()> {
226 let vscode_dir = project_path.join(".vscode");
227 let settings_path = vscode_dir.join("settings.json");
228
229 if settings_path.exists() {
230 println!(" ⏭ .vscode/settings.json already exists, skipping");
231 return Ok(());
232 }
233
234 tokio::fs::create_dir_all(&vscode_dir).await?;
235
236 let content = r#"{
237 "rust-analyzer.checkOnSave.command": "clippy",
238 "rust-analyzer.checkOnSave.extraArgs": [
239 "--",
240 "-W", "clippy::missing_docs_in_private_items",
241 "-W", "rustdoc::broken_intra_doc_links",
242 "-W", "rustdoc::missing_crate_level_docs"
243 ],
244 "editor.formatOnSave": true,
245 "[rust]": {
246 "editor.defaultFormatter": "rust-lang.rust-analyzer"
247 }
248}
249"#;
250 tokio::fs::write(&settings_path, content).await?;
251 println!(" ✅ Written: .vscode/settings.json");
252 Ok(())
253}
254
255async fn inject_cargo_toml_lints(cargo_toml: &std::path::Path) -> Result<()> {
256 let content = tokio::fs::read_to_string(cargo_toml).await?;
257
258 if content.contains("[lints]") || content.contains("[lints.rust]") {
259 println!(" ⏭ Cargo.toml already has [lints] section, skipping");
260 return Ok(());
261 }
262
263 let lints_block = r#"
264[lints.rust]
265missing_docs = "warn"
266unsafe_code = "forbid"
267
268[lints.rustdoc]
269broken_intra_doc_links = "deny"
270invalid_html_tags = "deny"
271missing_crate_level_docs = "warn"
272bare_urls = "warn"
273redundant_explicit_links = "warn"
274unescaped_backticks = "warn"
275
276[lints.clippy]
277missing_safety_doc = "deny"
278missing_errors_doc = "warn"
279missing_panics_doc = "warn"
280empty_docs = "warn"
281doc_markdown = "warn"
282needless_doctest_main = "warn"
283suspicious_doc_comments = "warn"
284too_long_first_doc_paragraph = "warn"
285unwrap_used = "deny"
286expect_used = "deny"
287"#;
288
289 let mut new_content = content;
290 new_content.push_str(lints_block);
291 tokio::fs::write(cargo_toml, new_content).await?;
292 println!(" ✅ Injected [lints] block into Cargo.toml");
293 Ok(())
294}
295
296async fn write_ferrous_config(project_path: &std::path::Path) -> Result<()> {
297 let ff_dir = project_path.join(".ferrous-forge");
298 let config_path = ff_dir.join("config.toml");
299
300 if config_path.exists() {
301 println!(" ⏭ .ferrous-forge/config.toml already exists, skipping");
302 return Ok(());
303 }
304
305 tokio::fs::create_dir_all(&ff_dir).await?;
306
307 let content = r#"# Ferrous Forge project configuration
308# These values are LOCKED — LLM agents must not modify them without human approval.
309
310[validation]
311max_file_lines = 300
312max_function_lines = 50
313required_edition = "2024"
314required_rust_version = "1.85.0"
315ban_underscore_bandaid = true
316require_documentation = true
317"#;
318 tokio::fs::write(&config_path, content).await?;
319 println!(" ✅ Written: .ferrous-forge/config.toml");
320 Ok(())
321}
322
323async fn create_docs_scaffold(project_path: &std::path::Path) -> Result<()> {
324 let adr_dir = project_path.join("docs").join("dev").join("adr");
325 let specs_dir = project_path.join("docs").join("dev").join("specs");
326
327 if !adr_dir.exists() {
328 tokio::fs::create_dir_all(&adr_dir).await?;
329 let readme = adr_dir.join("README.md");
330 tokio::fs::write(
331 &readme,
332 "# Architecture Decision Records\n\n\
333 This directory tracks architectural decisions for this project.\n\n\
334 ## Format\n\n\
335 Each ADR is a markdown file named `NNNN-short-title.md`.\n",
336 )
337 .await?;
338 println!(" ✅ Created: docs/dev/adr/README.md");
339 } else {
340 println!(" ⏭ docs/dev/adr already exists, skipping");
341 }
342
343 if !specs_dir.exists() {
344 tokio::fs::create_dir_all(&specs_dir).await?;
345 println!(" ✅ Created: docs/dev/specs/");
346 }
347
348 Ok(())
349}
350
351async fn write_ci_workflow(project_path: &std::path::Path) -> Result<()> {
352 let workflow_dir = project_path.join(".github").join("workflows");
353 let ci_path = workflow_dir.join("ci.yml");
354
355 if ci_path.exists() {
356 println!(" ⏭ .github/workflows/ci.yml already exists, skipping");
357 return Ok(());
358 }
359
360 tokio::fs::create_dir_all(&workflow_dir).await?;
361
362 let content = r#"name: CI
363
364on:
365 push:
366 branches: [main, master]
367 pull_request:
368
369env:
370 CARGO_TERM_COLOR: always
371 RUSTFLAGS: "-D warnings"
372
373jobs:
374 ci:
375 name: Build & Test
376 runs-on: ubuntu-latest
377 steps:
378 - uses: actions/checkout@v4
379
380 - name: Install Rust
381 uses: dtolnay/rust-toolchain@stable
382 with:
383 components: rustfmt, clippy
384
385 - name: Cache
386 uses: Swatinem/rust-cache@v2
387
388 - name: Format check
389 run: cargo fmt --check
390
391 - name: Clippy (with doc lints)
392 run: cargo clippy --all-features -- -D warnings
393
394 - name: Tests
395 run: cargo test --all-features
396
397 - name: Doc build
398 run: cargo doc --no-deps --all-features
399 env:
400 RUSTDOCFLAGS: "-D warnings"
401
402 - name: Security audit
403 run: |
404 cargo install cargo-audit --quiet
405 cargo audit
406"#;
407 tokio::fs::write(&ci_path, content).await?;
408 println!(" ✅ Written: .github/workflows/ci.yml");
409 Ok(())
410}
411
412async fn install_project_git_hooks(project_path: &std::path::Path) -> Result<()> {
413 match crate::git_hooks::install_git_hooks(project_path).await {
415 Ok(()) => {}
416 Err(e) => {
417 println!(" ⚠️ Git hooks skipped: {}", e);
419 }
420 }
421 Ok(())
422}