use std::path::PathBuf;
use std::process::ExitCode;
use clap::{CommandFactory, ValueEnum};
use clap_complete::{Generator, Shell};
use murk_cli::cli::Cli;
fn manifest_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn base_command() -> clap::Command {
Cli::command().version(None::<&'static str>)
}
const BANNER: &str = "<!-- Generated by \
`cargo run --features doc-gen --bin gen-docs` from the clap model and doc-only \
registry. Do not edit by hand; CI checks it. -->\n\n";
fn cli_reference() -> (PathBuf, String) {
let cmd = base_command();
let options = clap_markdown::MarkdownOptions::new()
.title("murk command reference".to_string())
.show_footer(false)
.show_table_of_contents(true);
let body = clap_markdown::help_markdown_command_custom(&cmd, &options);
(
manifest_dir().join("docs/cli-reference.md"),
format!("{BANNER}{body}"),
)
}
struct EnvVar {
name: &'static str,
summary: &'static str,
}
const ENV_GROUPS: &[(&str, &[EnvVar])] = &[
(
"Identity and vault selection",
&[
EnvVar {
name: "MURK_KEY",
summary: "Your raw age private key (`AGE-SECRET-KEY-1…`), inline. Rejected for hardware-plugin identity strings (`AGE-PLUGIN-…`) — use `MURK_KEY_FILE` for those.",
},
EnvVar {
name: "MURK_KEY_FILE",
summary: "Path to a private-key file: a raw age key, an SSH PEM key, or an age plugin identity file. `murk init` writes this reference into `.env`.",
},
EnvVar {
name: "MURK_VAULT",
summary: "Vault filename, defaulting to `.murk`. Equivalent to passing `--vault` on every command (the one variable in the clap model).",
},
],
),
(
"Safety and agent context",
&[
EnvVar {
name: "MURK_STRICT",
summary: "Truthy fails closed rather than let a secret touch disk, and disables the automatic key lookup under `~/.config/murk/keys`.",
},
EnvVar {
name: "MURK_AGENT",
summary: "Marks the process as running for an AI agent. Forces strict mode unconditionally; `murk agent exec` sets it on its child.",
},
EnvVar {
name: "MURK_SELF_SCOPE",
summary: "Truthy holds your own key to the vault's agent allow-tag policy, as if you were an agent. Implied inside an agent context.",
},
EnvVar {
name: "MURK_NO_SIGNER_PIN",
summary: "Opts out of signer-registry pinning (TOFU) on load — an escape hatch for a deliberate signer-key change, not for everyday use.",
},
],
),
(
"Also honored",
&[
EnvVar {
name: "CI",
summary: "Truthy prints a one-line nudge toward the scoped-agent path when a pipeline decrypts with a personal key. Advisory only.",
},
EnvVar {
name: "EDITOR, VISUAL",
summary: "Editor launched by `murk edit`, checking `EDITOR` then `VISUAL`, falling back to `vi`.",
},
EnvVar {
name: "XDG_RUNTIME_DIR",
summary: "Preferred (typically tmpfs) scratch location for `murk edit`'s temporary file, over `/tmp`.",
},
EnvVar {
name: "HOME (USERPROFILE on Windows)",
summary: "Base directory for murk state: `~/.config/murk/keys`, `agent-keys`, and `signer-pins`.",
},
],
),
];
fn env_reference() -> (PathBuf, String) {
let mut body = String::from(
"# murk environment variable reference\n\n\
The environment variables murk reads, and the CLI flags they mirror \
where one exists. This is the terse reference; the narrative version \
with resolution order and interactions lives in the Environment \
variables concept page.\n",
);
for (group, vars) in ENV_GROUPS {
body.push_str(&format!(
"\n## {group}\n\n| Variable | Description |\n| --- | --- |\n"
));
for v in *vars {
body.push_str(&format!("| `{}` | {} |\n", v.name, v.summary));
}
}
(
manifest_dir().join("docs/env-reference.md"),
format!("{BANNER}{body}"),
)
}
fn completions() -> Vec<(PathBuf, String)> {
let mut out = Vec::new();
for shell in Shell::value_variants() {
let mut cmd = base_command();
let mut buf = Vec::new();
clap_complete::generate(*shell, &mut cmd, "murk", &mut buf);
let file = manifest_dir()
.join("docs/completions")
.join(shell.file_name("murk"));
out.push((file, String::from_utf8(buf).expect("utf8 completion")));
}
out
}
fn man_pages() -> Vec<(PathBuf, String)> {
let mut out = Vec::new();
collect_man(&base_command(), &["murk"], &mut out);
out
}
fn collect_man(cmd: &clap::Command, path: &[&str], out: &mut Vec<(PathBuf, String)>) {
let render_cmd = cmd.clone().bin_name(path.join(" "));
let mut buf = Vec::new();
clap_mangen::Man::new(render_cmd)
.render(&mut buf)
.expect("render man page");
out.push((
manifest_dir()
.join("docs/man")
.join(format!("{}.1", path.join("-"))),
String::from_utf8(buf).expect("utf8 man page"),
));
for sub in cmd.get_subcommands() {
if sub.is_hide_set() {
continue;
}
let mut child = path.to_vec();
child.push(sub.get_name());
collect_man(sub, &child, out);
}
}
const MANAGED_DIRS: &[&str] = &["docs/completions", "docs/man"];
fn artifacts() -> Vec<(PathBuf, String)> {
let mut a = vec![cli_reference(), env_reference()];
a.extend(completions());
a.extend(man_pages());
a
}
fn main() -> ExitCode {
let check = std::env::args().any(|a| a == "--check");
let all = artifacts();
let expected: std::collections::HashSet<PathBuf> = all.iter().map(|(p, _)| p.clone()).collect();
let mut stale = Vec::new();
if !check {
for d in MANAGED_DIRS {
let dir = manifest_dir().join(d);
if dir.exists() {
std::fs::remove_dir_all(&dir)
.unwrap_or_else(|e| panic!("clear {}: {e}", dir.display()));
}
}
}
for (path, generated) in &all {
if check {
let current = std::fs::read_to_string(path).unwrap_or_default();
if current != *generated {
stale.push(path.clone());
}
} else {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("create docs directory");
}
std::fs::write(path, generated)
.unwrap_or_else(|e| panic!("write {}: {e}", path.display()));
eprintln!("wrote {}", path.display());
}
}
if check {
for d in MANAGED_DIRS {
if let Ok(entries) = std::fs::read_dir(manifest_dir().join(d)) {
for entry in entries.flatten() {
let p = entry.path();
if p.is_file() && !expected.contains(&p) {
stale.push(p);
}
}
}
}
if stale.is_empty() {
eprintln!("generated docs are up to date.");
return ExitCode::SUCCESS;
}
stale.sort();
stale.dedup();
for p in &stale {
eprintln!("out of date: {}", p.display());
}
eprintln!("Regenerate with: cargo run --features doc-gen --bin gen-docs");
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}