use std::ffi::OsString;
use std::path::PathBuf;
use amont_runtime::check::Stage;
use amont_runtime::{pushrefs, registry, ListOptions};
const USAGE: &str = "\
usage: amont <subcommand> | amont --hooks-dir <dir> <hook-name> [args…]
list what would run in this repository, and why not
[--json] [--stage pre-commit|pre-push] [--pushed]
install turn hooks on here: copy the binary if needed, bake the shims
[--force] replaces a hook that is not ours
init wire up THIS repository only — the verb a package manager
calls from `prepare`; never copies a binary, never prompts
uninstall remove OUR five shims and nothing else [--binary: the binary too]
setup the commit-style questions, current values as defaults
[--local|--global] [--dry-run]
trust show what amont.conf declares, and accept it
[--show: what is trusted] [--revoke: forget it]
run rehearse checks without committing: the pre-commit stage,
`run pre-push` for the push gate, or one check by name
[--all-files] [--hooks-dir <dir>]
restore bring back unstaged changes a killed pre-commit left parked
agents-md write the agent-guidance block into AGENTS.md, plus the
CLAUDE.md signpost pointing at it (both generated, both
checked) [--check: report drift only] [--path <file>]
enroll the machine-level standing grant: every future clone gets
the hooks [--conventions declared|everywhere]
attest `attest covered` — print the gate names a VALID signed
attestation covers for the tree checked out here (CI's
one-liner; prints nothing and exits 0 on any failure)
[--signers <file>] [--principal <id>]
[--platform <arch-os>|any: which leg is asking; defaults
to this machine's, so a matrix leg only skips work that
really ran on ITS platform]
--help this text --version the binary's version
Hook mode (what the shims call): amont --hooks-dir <dir> <hook-name> [args…]
The full manual: https://fredericrous.github.io/amont/
";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Sub {
List,
Setup,
Install,
Init,
Uninstall,
Run,
Trust,
Restore,
AgentsMd,
Enroll,
Attest,
}
impl Sub {
#[cfg(test)]
const fn index(self) -> usize {
match self {
Sub::List => 0,
Sub::Setup => 1,
Sub::Install => 2,
Sub::Init => 3,
Sub::Uninstall => 4,
Sub::Run => 5,
Sub::Trust => 6,
Sub::Restore => 7,
Sub::AgentsMd => 8,
Sub::Enroll => 9,
Sub::Attest => 10,
}
}
}
const SUBCOMMANDS: [(&str, Sub); 11] = [
("list", Sub::List),
("setup", Sub::Setup),
("install", Sub::Install),
("init", Sub::Init),
("uninstall", Sub::Uninstall),
("run", Sub::Run),
("trust", Sub::Trust),
("restore", Sub::Restore),
("agents-md", Sub::AgentsMd),
("enroll", Sub::Enroll),
("attest", Sub::Attest),
];
fn subcommand(arg: &str) -> Option<Sub> {
SUBCOMMANDS
.iter()
.find(|(name, _)| *name == arg)
.map(|(_, sub)| *sub)
}
#[derive(Debug, PartialEq, Eq)]
enum Invocation {
Sub {
name: Sub,
args: Vec<OsString>,
},
Hook {
hooks_dir: PathBuf,
name: String,
args: Vec<OsString>,
},
Help,
Version,
Usage(String),
}
fn parse(argv: Vec<OsString>) -> Invocation {
let Some(first) = argv.first() else {
return Invocation::Usage("no arguments".to_string());
};
if first == "--help" || first == "-h" {
return Invocation::Help;
}
if first == "--version" || first == "-V" {
return Invocation::Version;
}
if let Some(sub) = first.to_str().and_then(subcommand) {
return Invocation::Sub {
name: sub,
args: argv[1..].to_vec(),
};
}
let mut hooks_dir: Option<PathBuf> = None;
let mut name: Option<String> = None;
let mut args: Vec<OsString> = Vec::new();
let mut it = argv.into_iter();
while let Some(a) = it.next() {
if name.is_some() {
args.push(a);
continue;
}
if a == "--hooks-dir" {
let Some(v) = it.next() else {
return Invocation::Usage("--hooks-dir requires a value".to_string());
};
hooks_dir = Some(PathBuf::from(v));
continue;
}
let Some(s) = a.to_str() else {
return Invocation::Usage(format!("hook name is not valid UTF-8: {a:?}"));
};
name = Some(s.to_owned());
}
match (hooks_dir, name) {
(Some(hooks_dir), Some(name)) => Invocation::Hook {
hooks_dir,
name,
args,
},
(None, Some(name)) => Invocation::Usage(format!(
"{name:?} is not a subcommand, and hook mode needs --hooks-dir <dir> before the hook name"
)),
_ => Invocation::Usage("no hook name".to_string()),
}
}
#[cfg(unix)]
fn die_on_sigpipe() {
extern "C" {
fn signal(signum: i32, handler: usize) -> usize;
}
const SIGPIPE: i32 = 13;
const SIG_DFL: usize = 0;
unsafe {
signal(SIGPIPE, SIG_DFL);
}
}
#[cfg(not(unix))]
fn die_on_sigpipe() {}
fn main() {
die_on_sigpipe();
match parse(std::env::args_os().skip(1).collect()) {
Invocation::Help => {
print!("{USAGE}");
std::process::exit(0);
}
Invocation::Version => {
println!("amont {}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
Invocation::Usage(why) => {
eprintln!("amont: {why}");
eprint!("{USAGE}");
std::process::exit(2);
}
Invocation::Sub { name, args } => std::process::exit(run_sub(name, &args)),
Invocation::Hook {
hooks_dir,
name,
args,
} => std::process::exit(run_hook(&hooks_dir, &name, &args)),
}
}
fn known_flags(sub: Sub) -> (&'static [&'static str], &'static [&'static str]) {
match sub {
Sub::List => (&["--json", "--pushed"], &["--stage"]),
Sub::Setup => (&["--local", "--global", "--dry-run"], &[]),
Sub::Install => (&["--force"], &[]),
Sub::Init | Sub::Restore => (&[], &[]),
Sub::Uninstall => (&["--binary"], &[]),
Sub::Run => (&["--all-files"], &["--hooks-dir"]),
Sub::Trust => (&["--show", "--revoke"], &[]),
Sub::AgentsMd => (&["--check"], &["--path"]),
Sub::Enroll => (&[], &["--conventions"]),
Sub::Attest => (&[], &["--signers", "--principal", "--platform"]),
}
}
fn unknown_flag(args: &[OsString], flags: &[&str], valued: &[&str]) -> Option<String> {
let mut skip_value = false;
for a in args {
if skip_value {
skip_value = false;
continue;
}
let Some(a) = a.to_str() else { continue };
if !a.starts_with('-') {
continue;
}
if valued.contains(&a) {
skip_value = true;
continue;
}
if !flags.contains(&a) {
return Some(a.to_string());
}
}
None
}
fn verb_name(sub: Sub) -> &'static str {
SUBCOMMANDS
.iter()
.find(|(_, s)| *s == sub)
.map(|(n, _)| *n)
.unwrap_or("?")
}
fn run_sub(sub: Sub, args: &[OsString]) -> i32 {
if args.iter().any(|a| a == "--help" || a == "-h") {
print!("{USAGE}");
return 0;
}
if args.iter().any(|a| a == "--version" || a == "-V") {
println!("amont {}", env!("CARGO_PKG_VERSION"));
return 0;
}
let (flags, valued) = known_flags(sub);
if let Some(bad) = unknown_flag(args, flags, valued) {
eprintln!("amont: unknown flag {bad:?} for `{}`", verb_name(sub));
eprint!("{USAGE}");
return 2;
}
match sub {
Sub::List => {
let stage = match stage_flag(args) {
Ok(s) => s,
Err(msg) => {
eprintln!("amont: {msg}");
return 2;
}
};
amont_runtime::list_checks(ListOptions {
json: args.iter().any(|a| a == "--json"),
stage,
pushed: args.iter().any(|a| a == "--pushed"),
})
}
Sub::Install => report(amont_runtime::install::run(
args.iter().any(|a| a == "--force"),
)),
Sub::Init => report(amont_runtime::install::init()),
Sub::Uninstall => report(amont_runtime::install::uninstall(
args.iter().any(|a| a == "--binary"),
)),
Sub::Setup => report(amont_runtime::setup::command(args)),
Sub::Restore => report(amont_runtime::staged_only::restore_command()),
Sub::Trust => report(amont_runtime::trust::command(args)),
Sub::Run => run_mode(args),
Sub::AgentsMd => agents_md(args),
Sub::Enroll => {
let conventions = match flag_value(args, "--conventions") {
Ok(v) => v,
Err(msg) => {
eprintln!("amont: {msg}");
return 2;
}
};
report(amont_runtime::install::enroll(conventions.as_deref()))
}
Sub::Attest => {
if args.first().map(|a| a != "covered").unwrap_or(true) {
eprintln!("amont: attest takes the subaction `covered`");
eprint!("{USAGE}");
return 2;
}
let (signers, principal, platform) = match (
flag_value(args, "--signers"),
flag_value(args, "--principal"),
flag_value(args, "--platform"),
) {
(Ok(s), Ok(p), Ok(pl)) => (s, p, pl),
(Err(msg), _, _) | (_, Err(msg), _) | (_, _, Err(msg)) => {
eprintln!("amont: {msg}");
return 2;
}
};
let Some(signers) = signers
.map(PathBuf::from)
.or_else(amont_runtime::attest::default_signers)
else {
return 0; };
let Some(principal) =
principal.or_else(|| amont_runtime::attest::first_principal(&signers))
else {
return 0; };
let want = match platform.as_deref() {
Some("any") => None,
Some(explicit) => Some(explicit.to_string()),
None => Some(amont_runtime::attest::platform()),
};
if let Some(gates) =
amont_runtime::attest::covered(&signers, &principal, want.as_deref())
{
println!("{gates}");
}
0
}
}
}
fn flag_value(rest: &[OsString], flag: &str) -> Result<Option<String>, String> {
let mut iter = rest.iter();
while let Some(a) = iter.next() {
if a == flag {
return iter
.next()
.and_then(|v| v.to_str())
.map(|v| Some(v.to_string()))
.ok_or_else(|| format!("{flag} requires a value"));
}
}
Ok(None)
}
fn report(r: Result<(), String>) -> i32 {
match r {
Ok(()) => 0,
Err(e) => {
eprintln!("{e}");
1
}
}
}
fn run_mode(args: &[OsString]) -> i32 {
let all_files = args.iter().any(|a| a == "--all-files");
let named = args
.iter()
.filter_map(|a| a.to_str())
.find(|a| !a.starts_with("--"))
.map(str::to_owned);
let manifest = amont_runtime::manifest::load(std::path::Path::new(
&amont_runtime::hooks::common::repo_root(),
));
amont_runtime::policy::install(manifest.policy.clone());
let named = match named {
Some(n) if amont_runtime::registry::lookup(&n, &manifest).is_none() => {
match amont_runtime::dispatch::resolve_check_name(&n, &manifest) {
amont_runtime::dispatch::Named2::Resolved(id) => Some(id),
amont_runtime::dispatch::Named2::Unknown => {
eprintln!("amont: unknown check {n:?} — try `amont list`");
return 2;
}
amont_runtime::dispatch::Named2::Ambiguous(ids) => {
eprintln!(
"amont: {n:?} names more than one check — pick one: {}",
ids.join(", ")
);
return 2;
}
}
}
other => other,
};
let push = if named
.as_deref()
.is_some_and(|n| needs_synthetic_push_refs(n, &manifest))
{
match pushrefs::synthetic_from_upstream() {
Ok(r) => pushrefs::PushRefs::preloaded(vec![r]),
Err(e) => {
eprintln!("amont: {e}");
return 2;
}
}
} else {
pushrefs::PushRefs::default()
};
let hooks_dir = match hooks_dir_flag(args) {
Ok(Some(d)) => d,
Ok(None) => PathBuf::from(".git/hooks"),
Err(e) => {
eprintln!("amont: {e}");
return 2;
}
};
let name = named.clone().unwrap_or_else(|| "pre-commit".to_string());
let ctx = registry::Ctx {
name: &name,
args: &[],
hooks_dir: &hooks_dir,
push: &push,
manifest: &manifest,
};
let verdict = match named {
Some(check) => match amont_runtime::dispatch::run_named(&ctx, &check, all_files) {
amont_runtime::dispatch::Named::Ran(v) => v,
amont_runtime::dispatch::Named::Unknown => {
eprintln!("amont: unknown check {check:?} — try `amont list`");
return 2;
}
amont_runtime::dispatch::Named::Ambiguous(ids) => {
eprintln!(
"amont: {check:?} names more than one check — pick one: {}",
ids.join(", ")
);
return 2;
}
},
None => amont_runtime::dispatch::run_all(&ctx, all_files),
};
verdict.exit_code()
}
fn agents_md(args: &[OsString]) -> i32 {
let check_only = args.iter().any(|a| a == "--check");
let path = match path_flag(args) {
Ok(Some(p)) => p,
Ok(None) => match amont_runtime::hooks::common::repo_root_checked() {
Ok(root) => PathBuf::from(root).join("AGENTS.md"),
Err(e) => {
eprintln!("amont: {e}");
return 2;
}
},
Err(e) => {
eprintln!("amont: {e}");
return 2;
}
};
if check_only {
let pointer_code = pointer_path(&path).map_or(0, |p| {
match amont_runtime::agents_md::check_pointer(&p) {
Ok(amont_runtime::agents_md::CheckResult::Drifted) => {
eprintln!(
"{}: signpost drifted from the generated one — run `amont agents-md`",
p.display()
);
1
}
Err(e) => {
eprintln!("{e}");
1
}
Ok(_) => 0,
}
});
let block_code = match amont_runtime::agents_md::check(&path) {
Ok(amont_runtime::agents_md::CheckResult::NotPresent) => {
println!(
"{}: not present (opt-in — run without --check to add it)",
path.display()
);
0
}
Ok(amont_runtime::agents_md::CheckResult::MatchesGenerated) => {
println!("{}: up to date", path.display());
0
}
Ok(amont_runtime::agents_md::CheckResult::Drifted) => {
eprintln!(
"{}: drifted from the generated block — run `amont agents-md`",
path.display()
);
1
}
Err(e) => {
eprintln!("{e}");
1
}
};
block_code.max(pointer_code)
} else {
match amont_runtime::agents_md::write(&path) {
Ok(()) => {
println!("wrote {}", path.display());
pointer_path(&path).map_or(0, |p| {
match amont_runtime::agents_md::write_pointer(&p) {
Ok(()) => {
println!("wrote {} (signpost)", p.display());
0
}
Err(e) => {
eprintln!("{e}");
1
}
}
})
}
Err(e) => {
eprintln!("{e}");
1
}
}
}
}
fn pointer_path(block: &std::path::Path) -> Option<PathBuf> {
if block.file_name()? == "CLAUDE.md" {
return None;
}
Some(block.with_file_name("CLAUDE.md"))
}
fn run_hook(hooks_dir: &std::path::Path, hook: &str, args: &[OsString]) -> i32 {
let push = pushrefs::PushRefs::default();
let manifest = amont_runtime::manifest::load(std::path::Path::new(
&amont_runtime::hooks::common::repo_root(),
));
amont_runtime::policy::install(manifest.policy.clone());
let ctx = registry::Ctx {
name: hook,
args,
hooks_dir,
push: &push,
manifest: &manifest,
};
match registry::lookup(hook, &manifest) {
Some(run_hook) => run_hook(&ctx).exit_code(),
None => amont_runtime::skew::absorb_newer_hook(hook),
}
}
fn stage_flag(rest: &[OsString]) -> Result<Option<Stage>, String> {
let mut iter = rest.iter();
while let Some(a) = iter.next() {
if a == "--stage" {
let v = iter
.next()
.and_then(|v| v.to_str())
.ok_or_else(|| "--stage requires a value".to_string())?;
return match v {
"pre-commit" => Ok(Some(Stage::PreCommit)),
"pre-push" => Ok(Some(Stage::PrePush)),
other => Err(format!(
"--stage must be `pre-commit` or `pre-push`, got {other:?}"
)),
};
}
}
Ok(None)
}
fn path_flag(rest: &[OsString]) -> Result<Option<PathBuf>, String> {
value_flag(rest, "--path")
}
fn hooks_dir_flag(rest: &[OsString]) -> Result<Option<PathBuf>, String> {
value_flag(rest, "--hooks-dir")
}
fn value_flag(rest: &[OsString], flag: &str) -> Result<Option<PathBuf>, String> {
let mut iter = rest.iter();
while let Some(a) = iter.next() {
if a == flag {
let v = iter
.next()
.ok_or_else(|| format!("{flag} requires a value"))?;
return Ok(Some(PathBuf::from(v)));
}
}
Ok(None)
}
fn needs_synthetic_push_refs(name: &str, manifest: &amont_runtime::manifest::Manifest) -> bool {
if name == "pre-push" {
return true;
}
if let Some(c) = registry::one_named(name, manifest) {
return c.stage() == Stage::PrePush;
}
registry::CHECKS
.iter()
.any(|c| c.stage == Stage::PrePush && amont_runtime::skip_suppresses(c.name, name))
}
#[cfg(test)]
mod tests {
use super::*;
fn argv(items: &[&str]) -> Vec<OsString> {
items.iter().map(OsString::from).collect()
}
#[test]
fn a_hook_argument_never_names_a_subcommand() {
for (spelling, _) in SUBCOMMANDS {
let got = parse(argv(&[
"--hooks-dir",
"/d",
"pre-push",
spelling,
"https://example.test/r.git",
]));
assert_eq!(
got,
Invocation::Hook {
hooks_dir: PathBuf::from("/d"),
name: "pre-push".to_string(),
args: argv(&[spelling, "https://example.test/r.git"]),
},
"a remote named {spelling:?} hijacked dispatch"
);
let got = parse(argv(&["--hooks-dir", "/d", "commit-msg", spelling]));
assert_eq!(
got,
Invocation::Hook {
hooks_dir: PathBuf::from("/d"),
name: "commit-msg".to_string(),
args: argv(&[spelling]),
},
"a commit-message file named {spelling:?} hijacked dispatch"
);
}
}
#[test]
fn the_subcommand_table_is_exhaustive() {
for (i, (spelling, sub)) in SUBCOMMANDS.iter().enumerate() {
assert_eq!(sub.index(), i, "{spelling} is at the wrong position");
assert_eq!(
subcommand(spelling),
Some(*sub),
"{spelling} does not round-trip"
);
}
let mut names: Vec<&str> = SUBCOMMANDS.iter().map(|(n, _)| *n).collect();
names.sort_unstable();
let before = names.len();
names.dedup();
assert_eq!(before, names.len(), "a spelling appears twice");
}
#[test]
fn a_subcommand_passes_its_arguments_through_untouched() {
assert_eq!(
parse(argv(&["run", "pre-commit-prettier", "--all-files"])),
Invocation::Sub {
name: Sub::Run,
args: argv(&["pre-commit-prettier", "--all-files"]),
}
);
assert_eq!(
parse(argv(&["agents-md", "--path", "install"])),
Invocation::Sub {
name: Sub::AgentsMd,
args: argv(&["--path", "install"]),
}
);
}
#[test]
fn hooks_dir_after_the_hook_name_is_an_argument_not_a_flag() {
assert_eq!(
parse(argv(&[
"--hooks-dir",
"/d",
"pre-push",
"--hooks-dir",
"origin"
])),
Invocation::Hook {
hooks_dir: PathBuf::from("/d"),
name: "pre-push".to_string(),
args: argv(&["--hooks-dir", "origin"]),
}
);
}
#[test]
fn help_and_emptiness_are_their_own_answers() {
assert_eq!(parse(argv(&["--help"])), Invocation::Help);
assert_eq!(parse(argv(&["-h"])), Invocation::Help);
assert!(matches!(parse(argv(&[])), Invocation::Usage(_)));
assert!(matches!(
parse(argv(&["--hooks-dir"])),
Invocation::Usage(_)
));
assert!(matches!(parse(argv(&["pre-push"])), Invocation::Usage(_)));
}
#[test]
fn the_usage_block_names_every_subcommand() {
for (spelling, _) in SUBCOMMANDS {
assert!(USAGE.contains(spelling), "usage never mentions {spelling}");
}
assert!(USAGE.contains("--hooks-dir"));
assert!(USAGE.contains("--stage"));
}
}