use std::collections::HashMap;
use clap_usage::usage;
use clap_usage::usage::SpecCommandEffect::{self, Destructive, Read, Write};
pub const EFFECTS: &[(&str, SpecCommandEffect)] = &[
("__node-gyp-bootstrap", Write),
("access", Read),
("access get", Read),
("access get status", Read),
("access grant", Write),
("access list", Read),
("access list collaborators", Read),
("access list packages", Read),
("access ls", Read),
("access revoke", Destructive),
("access set", Write),
("activate", Write),
("add", Write),
("approve-builds", Write),
("audit", Read),
("bin", Read),
("bugs", Read),
("cache", Read),
("cache delete", Write),
("cache list", Read),
("cache list-registries", Read),
("cache path", Read),
("cache prune", Write),
("cache view", Read),
("cat-file", Read),
("cat-index", Read),
("check", Read),
("ci", Write),
("clean", Write),
("completion", Read),
("config", Read),
("config delete", Destructive),
("config explain", Read),
("config find", Read),
("config get", Read),
("config list", Read),
("config set", Write),
("config tui", Write),
("dedupe", Write),
("deploy", Write),
("deprecate", Write),
("deprecations", Read),
("diag", Read),
("diag analyze", Read),
("diag compare", Read),
("dist-tag", Read),
("dist-tag add", Write),
("dist-tag ls", Read),
("dist-tag rm", Destructive),
("doctor", Read),
("fetch", Write),
("find-hash", Read),
("get", Read),
("ignored-builds", Read),
("import", Write),
("init", Write),
("install", Write),
("la", Read),
("licenses", Read),
("link", Write),
("list", Read),
("ll", Read),
("login", Write),
("logout", Destructive),
("outdated", Read),
("pack", Write),
("patch", Write),
("patch-commit", Write),
("patch-remove", Destructive),
("peers", Read),
("peers check", Read),
("prefix", Read),
("prune", Write),
("purge", Write),
("query", Read),
("rebuild", Write),
("remove", Destructive),
("root", Read),
("runtime", Read),
("runtime list", Read),
("runtime set", Write),
("sbom", Read),
("sponsors", Read),
("store", Read),
("store add", Write),
("store path", Read),
("store prune", Write),
("store status", Read),
("trust", Read),
("trust check", Read),
("undeprecate", Write),
("unlink", Write),
("unpublish", Destructive),
("update", Write),
("version", Write),
("view", Read),
("why", Read),
("owner", Read),
("pkg", Read),
("search", Read),
("set-script", Read),
("stage", Read),
("token", Read),
("whoami", Read),
("set", Write),
];
pub const FEATURE_EFFECTS: &[(&str, SpecCommandEffect)] = &[
#[cfg(feature = "publish")]
("publish", Write),
];
#[cfg(test)]
pub const UNCLASSIFIED: &[(&str, &str)] = &[
(
"create",
"runs a create-* starter kit fetched from the registry",
),
("dlx", "fetches a package and runs its binary"),
("exec", "runs a locally installed binary"),
(
"install-test",
"installs, then runs the package's test script",
),
("node", "runs Node.js with whatever arguments are given"),
("recursive", "runs another command across every workspace"),
("restart", "runs the package's restart script"),
("run", "runs a script defined in package.json"),
("start", "runs the package's start script"),
("stop", "runs the package's stop script"),
("test", "runs the package's test script"),
];
pub fn apply(spec: &mut usage::Spec) {
let effects: HashMap<&str, SpecCommandEffect> =
EFFECTS.iter().chain(FEATURE_EFFECTS).copied().collect();
annotate(&mut spec.cmd, &mut vec![], &effects);
}
fn annotate(
cmd: &mut usage::SpecCommand,
path: &mut Vec<String>,
effects: &HashMap<&str, SpecCommandEffect>,
) {
for (name, sub) in cmd.subcommands.iter_mut() {
path.push(name.clone());
if let Some(effect) = effects.get(path.join(" ").as_str()) {
sub.effect = Some(*effect);
}
annotate(sub, path, effects);
path.pop();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
fn all_commands() -> Vec<String> {
let spec: usage::Spec = crate::command().into();
let mut out = vec![];
collect(&spec.cmd, &mut vec![], &mut out);
out
}
fn collect(cmd: &usage::SpecCommand, path: &mut Vec<String>, out: &mut Vec<String>) {
for (name, sub) in &cmd.subcommands {
path.push(name.clone());
out.push(path.join(" "));
collect(sub, path, out);
path.pop();
}
}
fn classified() -> HashSet<&'static str> {
EFFECTS
.iter()
.chain(FEATURE_EFFECTS)
.map(|(name, _)| *name)
.chain(UNCLASSIFIED.iter().map(|(name, _)| *name))
.collect()
}
#[test]
fn every_command_is_classified() {
let known = classified();
let missing: Vec<String> = all_commands()
.into_iter()
.filter(|cmd| !known.contains(cmd.as_str()))
.collect();
assert!(
missing.is_empty(),
"these commands have no entry in EFFECTS or UNCLASSIFIED \
(crates/aube/src/command_effects.rs) — decide whether each is \
read, write, destructive, or genuinely unclassifiable:\n {}",
missing.join("\n ")
);
}
#[test]
fn no_classification_refers_to_a_missing_command() {
let present: HashSet<String> = all_commands().into_iter().collect();
let stale: Vec<&str> = classified()
.into_iter()
.filter(|name| !present.contains(*name))
.collect();
assert!(
stale.is_empty(),
"these entries no longer match a command:\n {}",
stale.join("\n ")
);
}
#[test]
fn classifications_are_not_duplicated() {
let mut seen = HashSet::new();
for name in EFFECTS
.iter()
.chain(FEATURE_EFFECTS)
.map(|(n, _)| *n)
.chain(UNCLASSIFIED.iter().map(|(n, _)| *n))
{
assert!(seen.insert(name), "{name} is classified twice");
}
}
#[test]
fn apply_annotates_the_spec() {
let mut spec: usage::Spec = crate::command().into();
apply(&mut spec);
let cmd = |name: &str| {
spec.cmd
.subcommands
.get(name)
.unwrap_or_else(|| panic!("no `{name}`"))
};
assert_eq!(cmd("unpublish").effect, Some(Destructive));
assert_eq!(cmd("remove").effect, Some(Destructive));
assert_eq!(cmd("list").effect, Some(Read));
assert_eq!(cmd("install").effect, Some(Write));
assert_eq!(cmd("dist-tag").subcommands["rm"].effect, Some(Destructive));
assert_eq!(cmd("run").effect, None);
assert_eq!(cmd("dlx").effect, None);
}
}