diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a27b42..90553e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,6 +58,14 @@ jobs: if-no-files-found: ignore retention-days: 7 + - name: Upload CLI Cheatsheet + uses: actions/upload-artifact@v4 + with: + name: marlin-cli-cheatsheet + path: docs/cli_cheatsheet.md + if-no-files-found: warn + retention-days: 7 + coverage: name: Code Coverage (Tarpaulin) needs: comprehensive-tests diff --git a/README.md b/README.md index 1249495..d1e5e13 100644 --- a/README.md +++ b/README.md @@ -78,3 +78,7 @@ Before a milestone is declared **shipped**: | 3 | **DP‑001 Schema v1.1** draft | @carol | **30 May 25** | | 4 | backup prune CLI + nightly job | @dave | **05 Jun 25** | +## CLI Cheatsheet + +The full command reference is generated during the build of the CLI. See +[docs/cli_cheatsheet.md](docs/cli_cheatsheet.md). diff --git a/cli-bin/Cargo.toml b/cli-bin/Cargo.toml index fd3ea0f..68eae29 100644 --- a/cli-bin/Cargo.toml +++ b/cli-bin/Cargo.toml @@ -32,3 +32,7 @@ dirs = "5" [features] # Enable JSON output with `--features json` json = ["serde_json"] + +[build-dependencies] +serde = { version = "1", features = ["derive"] } +serde_yaml = "0.9" diff --git a/cli-bin/build.rs b/cli-bin/build.rs index 8364828..22f9fac 100644 --- a/cli-bin/build.rs +++ b/cli-bin/build.rs @@ -1,11 +1,59 @@ // cli-bin/build.rs // -// The CLI currently needs no build-time code-generation, but Cargo -// insists on rerunning any build-script each compile. Tell it to -// rebuild only if this file itself changes. +// Build script to generate the CLI cheatsheet at compile time. It +// parses `src/cli/commands.yaml` and emits a simple Markdown table of +// commands and flags to `docs/cli_cheatsheet.md`. + +use std::{fs, path::Path}; + +use serde_yaml::Value; fn main() { - // If you later add code-gen (e.g. embed completions or YAML), add - // further `cargo:rerun-if-changed=` lines here. println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=src/cli/commands.yaml"); + + if let Err(e) = generate_cheatsheet() { + eprintln!("Failed to generate CLI cheatsheet: {e}"); + std::process::exit(1); + } +} + +fn generate_cheatsheet() -> Result<(), Box> { + let yaml_str = fs::read_to_string("src/cli/commands.yaml")?; + let parsed: Value = serde_yaml::from_str(&yaml_str)?; + + let mut table = String::from("| Command | Flags |\n| ------- | ----- |\n"); + + if let Value::Mapping(cmds) = parsed { + for (cmd_name_val, cmd_details_val) in cmds { + let cmd_name = cmd_name_val.as_str().unwrap_or(""); + if let Value::Mapping(cmd_details) = cmd_details_val { + if let Some(Value::Mapping(actions)) = cmd_details.get(&Value::String("actions".into())) { + for (action_name_val, action_body_val) in actions { + let action_name = action_name_val.as_str().unwrap_or(""); + let flags = if let Value::Mapping(action_map) = action_body_val { + match action_map.get(&Value::String("flags".into())) { + Some(Value::Sequence(seq)) => seq + .iter() + .filter_map(|v| v.as_str()) + .collect::>() + .join(", "), + _ => String::new(), + } + } else { + String::new() + }; + + let flags_disp = if flags.is_empty() { "—" } else { &flags }; + table.push_str(&format!("| `{}` | {} |\n", format!("{} {}", cmd_name, action_name), flags_disp)); + } + } + } + } + } + + fs::create_dir_all(Path::new("docs"))?; + fs::write("docs/cli_cheatsheet.md", table)?; + + Ok(()) }