use std::collections::HashMap;
use usage::SpecCommandEffect::{self, Destructive, Read, Write};
pub const EFFECTS: &[(&str, SpecCommandEffect)] = &[
("activate", Read),
("backends", Read),
("backends ls", Read),
("bin-paths", Read),
("bootstrap", Destructive),
("bootstrap __apply-account-plan", Destructive),
("bootstrap __apply-firewall-plan", Destructive),
("bootstrap __apply-service-plan", Destructive),
("bootstrap __apply-system-plan", Destructive),
("bootstrap __inspect-firewall-plan", Read),
("bootstrap __inspect-system-files", Read),
("bootstrap accounts", Read),
("bootstrap accounts apply", Destructive),
("bootstrap accounts status", Read),
("bootstrap compose", Read),
("bootstrap compose apply", Destructive),
("bootstrap compose status", Read),
("bootstrap dotfiles", Read),
("bootstrap files", Read),
("bootstrap files apply", Destructive),
("bootstrap files status", Read),
("bootstrap firewall", Read),
("bootstrap firewall apply", Destructive),
("bootstrap firewall status", Read),
("bootstrap services", Read),
("bootstrap services apply", Destructive),
("bootstrap services status", Read),
("bootstrap launchd", Read),
("bootstrap launchd apply", Write),
("bootstrap launchd status", Read),
("bootstrap macos-defaults", Read),
("bootstrap macos-defaults apply", Write),
("bootstrap macos-defaults status", Read),
("bootstrap systemd", Read),
("bootstrap systemd apply", Write),
("bootstrap systemd status", Read),
("bootstrap dotfiles add", Write),
("bootstrap dotfiles apply", Write),
("bootstrap dotfiles edit", Write),
("bootstrap dotfiles status", Read),
("bootstrap dotfiles unapply", Destructive),
("bootstrap linux", Read),
("bootstrap linux systemd-units", Read),
("bootstrap linux systemd-units apply", Write),
("bootstrap linux systemd-units status", Read),
("bootstrap macos", Read),
("bootstrap macos defaults", Read),
("bootstrap macos defaults apply", Write),
("bootstrap macos defaults status", Read),
("bootstrap macos launchd-agents", Read),
("bootstrap macos launchd-agents apply", Write),
("bootstrap macos launchd-agents status", Read),
("bootstrap mise-shell-activate", Read),
("bootstrap mise-shell-activate apply", Write),
("bootstrap mise-shell-activate status", Read),
("bootstrap packages", Read),
("bootstrap packages apply", Write),
("bootstrap packages import", Write),
("bootstrap packages prune", Destructive),
("bootstrap packages status", Read),
("bootstrap packages upgrade", Write),
("bootstrap packages use", Write),
("bootstrap plan", Read),
("bootstrap plugins", Read),
("bootstrap plugins apply", Write),
("bootstrap plugins status", Read),
("bootstrap remote", Destructive),
("bootstrap repos", Read),
("bootstrap repos apply", Write),
("bootstrap repos status", Read),
("bootstrap repos update", Write),
("bootstrap secrets", Read),
("bootstrap secrets status", Read),
("bootstrap status", Read),
("bootstrap user", Read),
("bootstrap user apply", Write),
("bootstrap user status", Read),
("cache", Read),
("cache clear", Write),
("cache path", Read),
("cache prune", Write),
("cache task", Read),
("completion", Read),
("config", Read),
("config get", Read),
("config ls", Read),
("config set", Write),
("current", Read),
("deactivate", Read),
("deps", Write),
("deps add", Write),
("deps install", Write),
("deps remove", Destructive),
("direnv", Read),
("direnv activate", Read),
("direnv envrc", Write),
("doctor", Read),
("doctor path", Read),
("dotfiles", Read),
("dotfiles add", Write),
("dotfiles apply", Write),
("dotfiles edit", Write),
("dotfiles status", Read),
("dotfiles unapply", Destructive),
("edit", Write),
("env", Read),
("fmt", Write),
("generate", Read),
("generate bootstrap", Write),
("generate config", Write),
("generate devcontainer", Write),
("generate git-pre-commit", Write),
("generate github-action", Write),
("generate task-docs", Write),
("generate task-stubs", Write),
("generate tool-stub", Write),
("github", Read),
("github token", Read),
("global", Write),
("hook-env", Read),
("hook-not-found", Write),
("implode", Destructive),
("install", Write),
("install-into", Write),
("latest", Read),
("link", Write),
("lock", Write),
("local", Write),
("ls", Read),
("ls-remote", Read),
("oci", Read),
("oci build", Write),
("oci push", Write),
("outdated", Read),
("patrons", Read),
("plugins", Read),
("plugins install", Write),
("plugins link", Write),
("plugins ls", Read),
("plugins ls-remote", Read),
("plugins uninstall", Destructive),
("plugins update", Write),
("prune", Destructive),
("registry", Read),
("reshim", Write),
("search", Read),
("self-update", Write),
("set", Write),
("settings", Write),
("settings add", Write),
("settings get", Read),
("settings ls", Read),
("settings set", Write),
("settings unset", Write),
("shell", Read),
("shell-alias", Read),
("shell-alias get", Read),
("shell-alias ls", Read),
("shell-alias set", Write),
("shell-alias unset", Write),
("sponsors", Read),
("sync", Read),
("sync node", Write),
("sync python", Write),
("sync ruby", Write),
("tasks", Read),
("tasks add", Write),
("tasks deps", Read),
("tasks edit", Write),
("tasks graph", Read),
("tasks info", Read),
("tasks ls", Read),
("tasks validate", Read),
("token", Read),
("token forgejo", Read),
("token github", Read),
("token gitlab", Read),
("tool", Read),
("tool-alias", Read),
("tool-alias get", Read),
("tool-alias ls", Read),
("tool-alias set", Write),
("tool-alias unset", Write),
("trust", Write),
("uninstall", Destructive),
("unset", Write),
("untrust", Write),
("unuse", Destructive),
("upgrade", Write),
("usage", Read),
("use", Write),
("version", Read),
("where", Read),
("which", Read),
];
pub const PLATFORM_EFFECTS: &[(&str, SpecCommandEffect)] = &[
#[cfg(unix)]
("bootstrap packages brew", Read),
#[cfg(unix)]
("bootstrap packages brew tap", Write),
#[cfg(unix)]
("bootstrap packages brew untap", Write),
#[cfg(debug_assertions)]
("render-help", Write),
];
#[cfg(test)]
pub const UNCLASSIFIED: &[(&str, &str)] = &[
("asdf", "proxies whatever asdf command a plugin invoked"),
(
"bootstrap repos exec",
"runs an arbitrary command in each repo",
),
("direnv exec", "runs an arbitrary command"),
("en", "starts an interactive shell"),
("exec", "runs an arbitrary command"),
("mcp", "serves tools that run tasks on request"),
("oci run", "runs an arbitrary command in a container"),
("run", "runs project tasks"),
("tasks run", "runs project tasks"),
("test-tool", "installs and executes a tool"),
("tool-stub", "executes the tool a stub points at"),
("watch", "runs project tasks on change"),
];
pub fn apply(spec: &mut usage::Spec) {
let effects: HashMap<&str, SpecCommandEffect> =
EFFECTS.iter().chain(PLATFORM_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 clap::CommandFactory;
use std::collections::HashSet;
fn all_commands() -> Vec<String> {
let command = crate::cli::expand_deferred_subcommands(
crate::cli::Cli::command().disable_help_subcommand(true),
);
let spec: usage::Spec = 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();
}
}
#[test]
fn apply_annotates_the_spec() {
let command = crate::cli::expand_deferred_subcommands(
crate::cli::Cli::command().disable_help_subcommand(true),
);
let mut spec: usage::Spec = command.into();
apply(&mut spec);
let cmd = |name: &str| {
spec.cmd
.subcommands
.get(name)
.unwrap_or_else(|| panic!("no `mise {name}`"))
};
assert_eq!(cmd("uninstall").effect, Some(Destructive));
assert_eq!(cmd("ls").effect, Some(Read));
assert_eq!(cmd("use").effect, Some(Write));
assert_eq!(
cmd("plugins").subcommands["uninstall"].effect,
Some(Destructive)
);
assert_eq!(cmd("run").effect, None);
assert_eq!(cmd("exec").effect, None);
}
#[test]
fn every_visible_command_is_classified() {
let classified: HashSet<&str> = EFFECTS
.iter()
.chain(PLATFORM_EFFECTS)
.map(|(name, _)| *name)
.chain(UNCLASSIFIED.iter().map(|(name, _)| *name))
.collect();
let missing: Vec<String> = all_commands()
.into_iter()
.filter(|cmd| !classified.contains(cmd.as_str()))
.collect();
assert!(
missing.is_empty(),
"these commands have no entry in EFFECTS or UNCLASSIFIED \
(src/cli/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> = EFFECTS
.iter()
.chain(PLATFORM_EFFECTS)
.map(|(name, _)| *name)
.chain(UNCLASSIFIED.iter().map(|(name, _)| *name))
.filter(|name| !present.contains(*name))
.collect();
assert!(
stale.is_empty(),
"these entries in src/cli/command_effects.rs 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(PLATFORM_EFFECTS)
.map(|(n, e)| (*n, Some(*e)))
.chain(
UNCLASSIFIED
.iter()
.map(|(n, _)| (*n, None::<SpecCommandEffect>)),
)
{
assert!(seen.insert(name), "{name} is classified twice");
}
}
}