use std::collections::HashMap;
use clap_usage::usage;
use clap_usage::usage::SpecCommandEffect::{self, Destructive, Read, Write};
pub const EFFECTS: &[(&str, SpecCommandEffect)] = &[
("activate", Read),
("api-schema", Read),
("boot", Read),
("boot disable", Write),
("boot enable", Write),
("boot status", Read),
("cd", Read),
("clean", Write),
("completion", Read),
("daemons", Read),
("daemons add", Write),
("daemons remove", Destructive),
("disable", Write),
("enable", Write),
("list", Read),
("log-sink", Write),
("logs", Read),
("project", Read),
("project enter", Write),
("project leave", Write),
("project list", Read),
("proxy", Read),
("proxy add", Write),
("proxy remove", Destructive),
("proxy status", Read),
("proxy trust", Write),
("proxy untrust", Write),
("schema", Read),
("settings", Read),
("settings get", Read),
("settings list", Read),
("settings set", Write),
("sponsors", Read),
("status", Read),
("stop", Write),
("supervisor", Read),
("supervisor status", Read),
("supervisor stop", Write),
("usage", Read),
("wait", Read),
];
#[cfg(test)]
pub const UNCLASSIFIED: &[(&str, &str)] = &[
("mcp", "serves tools that start and stop daemons on request"),
("restart", "stops then reruns a user-configured daemon"),
("run", "runs a one-off command as a daemon"),
("start", "runs a user-configured daemon"),
("supervisor run", "supervises user-configured daemons"),
("supervisor start", "supervises user-configured daemons"),
(
"tui",
"an interactive dashboard that can start and stop daemons",
),
];
pub const FLAG_EFFECTS: &[(&str, &str, SpecCommandEffect)] = &[
("logs", "clear", Destructive),
];
pub fn apply(spec: &mut usage::Spec) {
let effects: HashMap<&str, SpecCommandEffect> = EFFECTS.iter().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());
let full = path.join(" ");
if let Some(effect) = effects.get(full.as_str()) {
sub.effect = Some(*effect);
}
for (cmd_path, flag_name, effect) in FLAG_EFFECTS {
if *cmd_path != full {
continue;
}
if let Some(flag) = sub.flags.iter_mut().find(|f| f.name == *flag_name) {
flag.effect = Some(*effect);
}
}
annotate(sub, path, effects);
path.pop();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::Cli;
use clap::CommandFactory;
use std::collections::HashSet;
fn all_commands() -> Vec<String> {
let spec: usage::Spec = Cli::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()
.map(|(name, _)| *name)
.chain(UNCLASSIFIED.iter().map(|(name, _)| *name))
.collect()
}
#[test]
fn apply_annotates_commands_and_flags() {
let mut spec: usage::Spec = Cli::command().into();
apply(&mut spec);
let cmd = |name: &str| {
spec.cmd
.subcommands
.get(name)
.unwrap_or_else(|| panic!("no `pitchfork {name}`"))
};
let logs = cmd("logs");
assert_eq!(logs.effect, Some(Read));
let flag = |name: &str| {
logs.flags
.iter()
.find(|f| f.name == name)
.unwrap_or_else(|| panic!("no --{name}"))
};
assert_eq!(flag("clear").effect, Some(Destructive));
assert_eq!(flag("tail").effect, None);
assert_eq!(
cmd("daemons").subcommands["remove"].effect,
Some(Destructive)
);
assert_eq!(cmd("stop").effect, Some(Write));
assert_eq!(cmd("start").effect, None);
}
#[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 \
(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> = 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 every_flag_effect_matches_a_real_flag() {
let spec: usage::Spec = Cli::command().into();
let mut missing = vec![];
for (cmd_path, flag_name, _) in FLAG_EFFECTS {
match spec.cmd.subcommands.get(*cmd_path) {
Some(c) if c.flags.iter().any(|f| f.name == *flag_name) => {}
_ => missing.push(format!("{cmd_path} --{flag_name}")),
}
}
assert!(
missing.is_empty(),
"these FLAG_EFFECTS entries do not match a real flag:\n {}",
missing.join("\n ")
);
}
#[test]
fn classifications_are_not_duplicated() {
let mut seen = HashSet::new();
for name in EFFECTS
.iter()
.map(|(n, _)| *n)
.chain(UNCLASSIFIED.iter().map(|(n, _)| *n))
{
assert!(seen.insert(name), "{name} is classified twice");
}
}
}