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
69 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
85fn 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
194async 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
204imports_granularity = "Crate"
205group_imports = "StdExternalCrate"
206edition = "2024"
207"#;
208 tokio::fs::write(&path, content).await?;
209 println!(" ā
Written: rustfmt.toml");
210 Ok(())
211}
212
213async fn write_clippy_toml(project_path: &std::path::Path) -> Result<()> {
214 let path = project_path.join("clippy.toml");
215 if path.exists() {
216 println!(" ā clippy.toml already exists, skipping");
217 return Ok(());
218 }
219 let content = r#"# Ferrous Forge project clippy configuration
220too-many-lines-threshold = 50
221cognitive-complexity-threshold = 25
222"#;
223 tokio::fs::write(&path, content).await?;
224 println!(" ā
Written: clippy.toml");
225 Ok(())
226}
227
228async fn write_vscode_settings(project_path: &std::path::Path) -> Result<()> {
229 let vscode_dir = project_path.join(".vscode");
230 let settings_path = vscode_dir.join("settings.json");
231
232 if settings_path.exists() {
233 println!(" ā .vscode/settings.json already exists, skipping");
234 return Ok(());
235 }
236
237 tokio::fs::create_dir_all(&vscode_dir).await?;
238
239 let content = r#"{
240 "rust-analyzer.checkOnSave.command": "clippy",
241 "rust-analyzer.checkOnSave.extraArgs": [
242 "--",
243 "-W", "clippy::missing_docs_in_private_items",
244 "-W", "rustdoc::broken_intra_doc_links",
245 "-W", "rustdoc::missing_crate_level_docs"
246 ],
247 "editor.formatOnSave": true,
248 "[rust]": {
249 "editor.defaultFormatter": "rust-lang.rust-analyzer"
250 }
251}
252"#;
253 tokio::fs::write(&settings_path, content).await?;
254 println!(" ā
Written: .vscode/settings.json");
255 Ok(())
256}
257
258async fn inject_cargo_toml_lints(cargo_toml: &std::path::Path) -> Result<()> {
259 let content = tokio::fs::read_to_string(cargo_toml).await?;
260
261 if content.contains("[lints]") || content.contains("[lints.rust]") {
262 println!(" ā Cargo.toml already has [lints] section, skipping");
263 return Ok(());
264 }
265
266 let lints_block = r#"
267[lints.rust]
268missing_docs = "warn"
269unsafe_code = "forbid"
270
271[lints.rustdoc]
272broken_intra_doc_links = "deny"
273invalid_html_tags = "deny"
274missing_crate_level_docs = "warn"
275bare_urls = "warn"
276redundant_explicit_links = "warn"
277unescaped_backticks = "warn"
278
279[lints.clippy]
280missing_safety_doc = "deny"
281missing_errors_doc = "warn"
282missing_panics_doc = "warn"
283empty_docs = "warn"
284doc_markdown = "warn"
285needless_doctest_main = "warn"
286suspicious_doc_comments = "warn"
287too_long_first_doc_paragraph = "warn"
288unwrap_used = "deny"
289expect_used = "deny"
290"#;
291
292 let mut new_content = content;
293 new_content.push_str(lints_block);
294 tokio::fs::write(cargo_toml, new_content).await?;
295 println!(" ā
Injected [lints] block into Cargo.toml");
296 Ok(())
297}
298
299async fn write_ferrous_config(project_path: &std::path::Path) -> Result<()> {
300 let ff_dir = project_path.join(".ferrous-forge");
301 let config_path = ff_dir.join("config.toml");
302
303 if config_path.exists() {
304 println!(" ā .ferrous-forge/config.toml already exists, skipping");
305 return Ok(());
306 }
307
308 tokio::fs::create_dir_all(&ff_dir).await?;
309
310 let content = r#"# Ferrous Forge project configuration
311# These values are LOCKED ā LLM agents must not modify them without human approval.
312
313[validation]
314max_file_lines = 300
315max_function_lines = 50
316required_edition = "2024"
317required_rust_version = "1.85.0"
318ban_underscore_bandaid = true
319require_documentation = true
320"#;
321 tokio::fs::write(&config_path, content).await?;
322 println!(" ā
Written: .ferrous-forge/config.toml");
323 Ok(())
324}
325
326async fn create_docs_scaffold(project_path: &std::path::Path) -> Result<()> {
327 let adr_dir = project_path.join("docs").join("dev").join("adr");
328 let specs_dir = project_path.join("docs").join("dev").join("specs");
329
330 if !adr_dir.exists() {
331 tokio::fs::create_dir_all(&adr_dir).await?;
332 let readme = adr_dir.join("README.md");
333 tokio::fs::write(
334 &readme,
335 "# Architecture Decision Records\n\n\
336 This directory tracks architectural decisions for this project.\n\n\
337 ## Format\n\n\
338 Each ADR is a markdown file named `NNNN-short-title.md`.\n",
339 )
340 .await?;
341 println!(" ā
Created: docs/dev/adr/README.md");
342 } else {
343 println!(" ā docs/dev/adr already exists, skipping");
344 }
345
346 if !specs_dir.exists() {
347 tokio::fs::create_dir_all(&specs_dir).await?;
348 println!(" ā
Created: docs/dev/specs/");
349 }
350
351 Ok(())
352}
353
354async fn write_ci_workflow(project_path: &std::path::Path) -> Result<()> {
355 let workflow_dir = project_path.join(".github").join("workflows");
356 let ci_path = workflow_dir.join("ci.yml");
357
358 if ci_path.exists() {
359 println!(" ā .github/workflows/ci.yml already exists, skipping");
360 return Ok(());
361 }
362
363 tokio::fs::create_dir_all(&workflow_dir).await?;
364
365 let content = r#"name: CI
366
367on:
368 push:
369 branches: [main, master]
370 pull_request:
371
372env:
373 CARGO_TERM_COLOR: always
374 RUSTFLAGS: "-D warnings"
375
376jobs:
377 ci:
378 name: Build & Test
379 runs-on: ubuntu-latest
380 steps:
381 - uses: actions/checkout@v4
382
383 - name: Install Rust
384 uses: dtolnay/rust-toolchain@stable
385 with:
386 components: rustfmt, clippy
387
388 - name: Cache
389 uses: Swatinem/rust-cache@v2
390
391 - name: Format check
392 run: cargo fmt --check
393
394 - name: Clippy (with doc lints)
395 run: cargo clippy --all-features -- -D warnings
396
397 - name: Tests
398 run: cargo test --all-features
399
400 - name: Doc build
401 run: cargo doc --no-deps --all-features
402 env:
403 RUSTDOCFLAGS: "-D warnings"
404
405 - name: Security audit
406 run: |
407 cargo install cargo-audit --quiet
408 cargo audit
409"#;
410 tokio::fs::write(&ci_path, content).await?;
411 println!(" ā
Written: .github/workflows/ci.yml");
412 Ok(())
413}
414
415async fn install_project_git_hooks(project_path: &std::path::Path) -> Result<()> {
416 match crate::git_hooks::install_git_hooks(project_path).await {
418 Ok(()) => {}
419 Err(e) => {
420 println!(" ā ļø Git hooks skipped: {}", e);
422 }
423 }
424 Ok(())
425}