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
7///
8/// # Errors
9///
10/// Returns an error if the configuration cannot be loaded, directories
11/// cannot be created, or system integration files fail to install.
12pub 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
29/// Execute the project-level init command (`ferrous-forge init --project`)
30///
31/// Writes project-level tooling files into the current directory:
32/// rustfmt.toml, clippy.toml, .vscode/settings.json, `Cargo.toml` `[lints]`,
33/// docs scaffold, .github/workflows/ci.yml, and git hooks.
34///
35/// # Errors
36///
37/// Returns an error if the current directory cannot be determined, no
38/// `Cargo.toml` is found, or any tooling file fails to write.
39pub 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    // Verify this is a Rust project
51    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
82// ── System init helpers ──────────────────────────────────────────────────────
83
84fn 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
191// ── Project init helpers ─────────────────────────────────────────────────────
192
193async 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    // Use the existing git_hooks installer
414    match crate::git_hooks::install_git_hooks(project_path).await {
415        Ok(()) => {}
416        Err(e) => {
417            // Not fatal — project might not have a git repo yet
418            println!("  ⚠️  Git hooks skipped: {}", e);
419        }
420    }
421    Ok(())
422}