crates/cli/src/main.rs 3.2 KiB 117 lines Raw
1 #![forbid(unsafe_code)]2 //! f0rge CLI spine (PLAN M2): dispatch, flags, output, errors.3 //! Feature agents: replace one stub body + one dispatch line. Nothing else.4 5 mod args;6 mod cmd;7 mod output;8 9 use std::fmt;10 use std::path::PathBuf;11 12 // ---- errors: one line on stderr, no paths beyond basename (SEC-10) ----13 14 #[derive(Debug, Clone, PartialEq, Eq)]15 pub enum CliError {16     Usage(String),17     Runtime(String),18     Io(String),19 }20 21 impl fmt::Display for CliError {22     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {23         match self {24             Self::Usage(m) => write!(f, "usage: {m}"),25             Self::Runtime(m) => write!(f, "error: {m}"),26             Self::Io(m) => write!(f, "error: {m}"),27         }28     }29 }30 31 impl CliError {32     pub fn code(&self) -> i32 {33         match self {34             Self::Usage(_) => 2,35             Self::Runtime(_) | Self::Io(_) => 1,36         }37     }38 }39 40 impl From<f0rge_core::ForgeError> for CliError {41     fn from(e: f0rge_core::ForgeError) -> Self {42         Self::Runtime(e.to_string())43     }44 }45 46 fn main() {47     std::process::exit(real_main());48 }49 50 fn real_main() -> i32 {51     let argv: Vec<String> = std::env::args().skip(1).collect();52     match run(&argv) {53         Ok(()) => 0,54         Err(e) => {55             eprintln!("{e}");56             e.code()57         }58     }59 }60 61 fn run(argv: &[String]) -> Result<(), CliError> {62     let g = args::parse_global(argv)?;63     // --version wins over everything.64     if g.version {65         output::out(concat!("f0rge ", env!("CARGO_PKG_VERSION"), "\n"));66         return Ok(());67     }68     // --skill wins over everything.69     if g.skill {70         let out = crate::cmd::skill::SKILL_TEXT;71         output::emit_text(out, g.jq.as_deref());72         return Ok(());73     }74     let Some(sub) = g.sub.clone() else {75         // no args, or only globals (-h/--help): root help to stdout (EPIPE-safe).76         output::out(crate::cmd::ROOT_HELP);77         return Ok(());78     };79     // resolve forge root: cwd (init handles its own dir).80     let cwd: PathBuf = std::env::current_dir()81         .map_err(|_| CliError::Io("cwd: io error".to_string()))?;82     let fg = f0rge_core::Forge::open(&cwd);83     crate::cmd::dispatch(&sub, &g.rest, &fg, &g)84 }85 86 #[cfg(test)]87 mod budget_tests {88     fn lines_and_bytes(s: &str) -> (usize, usize) {89         (s.lines().count(), s.len())90     }91 92     #[test]93     fn root_help_within_budget() {94         let (lines, bytes) = lines_and_bytes(crate::cmd::ROOT_HELP);95         assert!(lines <= 12, "root help {lines} lines > 12");96         assert!(bytes <= 300, "root help {bytes} bytes > 300");97     }98 99     #[test]100     fn skill_within_budget() {101         let t = crate::cmd::skill::SKILL_TEXT;102         let (lines, bytes) = lines_and_bytes(t);103         assert!(lines <= 30, "skill {lines} lines > 30");104         assert!(bytes <= 1000, "skill {bytes} bytes > 1000");105     }106 107     #[test]108     fn all_sub_helps_within_budget() {109         let helps = crate::cmd::all_helps();110         assert_eq!(helps.len(), 12, "expected 12 sub helps");111         for (name, h) in helps {112             let (lines, bytes) = lines_and_bytes(h);113             assert!(lines <= 8, "{name} -h {lines} lines > 8");114             assert!(bytes <= 250, "{name} -h {bytes} bytes > 250");115         }116     }117 }