1 #![forbid(unsafe_code)]2 //! f0rge spec: print/write the embedded canonical spec/schema.md.3 //! Single source of truth: include_str! of the repo spec, no scrubbed copy.4 use crate::CliError;5 6 /// Embedded canonical spec: single source, no drift.7 pub const SPEC_TEXT: &str = include_str!("../../../../spec/schema.md");8 9 pub fn help() -> &'static str {10 "f0rge spec: schema\nusage: f0rge spec [--write]\nprints embedded spec/schema.md\ndocs: spec/schema.md §Spec\ncontract: f0rge --skill\n"11 }12 13 pub fn run(args: &[String], _fg: &f0rge_core::Forge) -> Result<(), CliError> {14 let mut write = false;15 for a in args {16 if a == "-h" || a == "--help" {17 crate::output::out(help());18 return Ok(());19 }20 if a == "--write" {21 write = true;22 continue;23 }24 if a.starts_with('-') {25 return Err(CliError::Usage(format!("unknown flag: {a}")));26 }27 return Err(CliError::Usage("spec [--write]".to_string()));28 }29 if write {30 let cwd = std::env::current_dir().map_err(|_| CliError::Io("cwd: io error".to_string()))?;31 write_spec(&cwd).map_err(|e| CliError::Io(e.to_string()))?;32 return Ok(());33 }34 crate::output::out(SPEC_TEXT);35 if !SPEC_TEXT.ends_with('\n') {36 crate::output::out("\n");37 }38 Ok(())39 }40 41 fn io_err(file: &str) -> std::io::Error {42 std::io::Error::other(file.to_string())43 }44 45 /// Overwrite spec/schema.md (atomic tmp+rename). Creates spec/ dir.46 fn write_spec(target: &std::path::Path) -> Result<(), std::io::Error> {47 use std::io::Write;48 let dir = target.join("spec");49 std::fs::create_dir_all(&dir)?;50 let path = dir.join("schema.md");51 let tmp = path.with_extension("tmp");52 let mut f = std::fs::File::create(&tmp).map_err(|_| io_err("schema.md"))?;53 f.write_all(SPEC_TEXT.as_bytes()).map_err(|_| io_err("schema.md"))?;54 f.sync_all().map_err(|_| io_err("schema.md"))?;55 drop(f);56 std::fs::rename(&tmp, &path).map_err(|_| io_err("schema.md"))?;57 Ok(())58 }