Skip to main content

ferrous_forge/commands/
init.rs

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