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