use std::collections::HashMap;
use usage::SpecCommandEffect::{self, Destructive, Read, Write};
pub(super) 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 config-roots", 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 remove", 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 diff", Read),
("bootstrap dotfiles edit", Write),
("bootstrap dotfiles exclude", Write),
("bootstrap dotfiles history", Read),
("bootstrap dotfiles history describe", Write),
("bootstrap dotfiles history diff", Read),
("bootstrap dotfiles history ls", Read),
("bootstrap dotfiles history show", Read),
("bootstrap dotfiles include", Write),
("bootstrap dotfiles origin", Destructive),
("bootstrap dotfiles origin set", Write),
("bootstrap dotfiles paths", Read),
("bootstrap dotfiles pull", Destructive),
("bootstrap dotfiles rollback", Destructive),
("bootstrap dotfiles recover", Destructive),
("bootstrap dotfiles save", Write),
("bootstrap dotfiles status", Read),
("bootstrap dotfiles sync", Write),
("bootstrap dotfiles track", Write),
("bootstrap dotfiles unapply", Destructive),
("bootstrap dotfiles undo", Destructive),
("bootstrap dotfiles untrack", Write),
("bootstrap dotfiles watch", Write),
("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", Destructive),
("bootstrap packages export", Read),
("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 diff", Read),
("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 install-script", 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),
("packslip", Read),
("packslip forget", Write),
("packslip pins", 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),
("skills", Read),
("skills ls", Read),
("skills sync", 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(super) 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(crate) const UNCLASSIFIED: &[(&str, &str)] = &[
("asdf", "proxies whatever asdf command a plugin invoked"),
(
"bootstrap dotfiles capture",
"runs an arbitrary command while recording file history",
),
(
"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"),
("ssh", "runs an arbitrary command on a remote host"),
("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(super) 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 std::collections::HashSet;
fn all_commands() -> Vec<String> {
let spec: usage::Spec = crate::cli::Cli::to_kdl()
.parse()
.expect("generated mise usage spec");
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 mut spec: usage::Spec = crate::cli::Cli::to_kdl()
.parse()
.expect("generated mise usage spec");
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("bootstrap").subcommands["packages"].subcommands["apply"].effect,
Some(Destructive)
);
assert_eq!(cmd("run").effect, None);
assert_eq!(cmd("exec").effect, None);
}
#[test]
fn installing_a_completion_script_is_a_write() {
let completion = crate::cli::Cli::spec()
.root
.subcommands
.iter()
.find(|command| command.cmd.name == "completion")
.expect("completion");
let flag = |name: &str| {
completion
.flags
.iter()
.find(|f| f.flag.name == name)
.unwrap_or_else(|| panic!("`mise completion` has no --{name}"))
};
assert_eq!(flag("install").effect, Some(usage_rs::spec::Effect::Write));
assert_eq!(flag("force").effect, Some(usage_rs::spec::Effect::Write));
assert_eq!(flag("include-bash-completion-lib").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");
}
}
}