#[derive(Debug, PartialEq, Eq)]
pub enum Invocation {
List,
Run {
name: String,
args: Vec<String>,
confirmed: bool,
},
Help,
Version,
Health,
Clean,
Security,
Workflow(String),
Updates,
UnknownFlag(String),
}
pub fn parse<I, S>(args: I) -> Invocation
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let mut args = args.into_iter().map(Into::into);
let mut confirmed = false;
let first = loop {
let Some(arg) = args.next() else {
return Invocation::List;
};
if arg == "--yes" || arg == "-y" {
confirmed = true;
continue;
}
break arg;
};
match first.as_str() {
"-h" | "--help" => Invocation::Help,
"-V" | "--version" => Invocation::Version,
"--health" => Invocation::Health,
"--clean" => Invocation::Clean,
"--security" => Invocation::Security,
"--updates" => Invocation::Updates,
"--check" => args
.next()
.map_or(Invocation::UnknownFlag("--check".to_owned()), |name| {
Invocation::Workflow(name)
}),
_ if first.starts_with('-') => Invocation::UnknownFlag(first),
_ => {
let mut rest: Vec<String> = args.collect();
if rest.first().is_some_and(|arg| arg == "--") {
rest.remove(0);
}
Invocation::Run {
name: first,
args: rest,
confirmed,
}
}
}
}
pub fn help(version: &str) -> String {
format!(
"opi {version} — Operations Interface
Usage:
opi List what this project can do
opi <script> [args…] Run a script, passing args on to it
opi <member>/<script> Run a workspace member's script
In the list, ↑↓ move, Enter runs, / filters, H checks, C cleans,
S scans for secrets and vulnerable dependencies, U shows updates.
Options:
-y, --yes Answer a script's confirmation in advance
--health Run the project's checks
--clean Show and remove build artefacts
--security Scan for secrets and vulnerable dependencies
--updates Show dependencies with newer versions
--check <workflow> Run a workflow: commit or release
-h, --help Show this help
-V, --version Show the version
Entries come from package.json and Cargo.toml. A project's own scripts
always take precedence over built-in names, so a project with a script
called \"clean\" keeps working — every built-in area is a flag or a
hotkey instead."
)
}
#[cfg(test)]
mod tests {
use super::*;
fn run(name: &str, args: &[&str]) -> Invocation {
Invocation::Run {
name: name.to_owned(),
args: args.iter().map(|arg| (*arg).to_owned()).collect(),
confirmed: false,
}
}
fn confirmed(name: &str) -> Invocation {
Invocation::Run {
name: name.to_owned(),
args: Vec::new(),
confirmed: true,
}
}
#[test]
fn no_arguments_lists() {
assert_eq!(parse(Vec::<String>::new()), Invocation::List);
}
#[test]
fn a_bare_word_is_a_script() {
assert_eq!(parse(["dev"]), run("dev", &[]));
}
#[test]
fn arguments_after_the_script_belong_to_the_script() {
assert_eq!(
parse(["build", "--verbose", "--out", "dist"]),
run("build", &["--verbose", "--out", "dist"])
);
}
#[test]
fn a_separator_before_script_args_is_dropped() {
assert_eq!(
parse(["build", "--", "--verbose"]),
run("build", &["--verbose"])
);
}
#[test]
fn only_the_first_separator_is_dropped() {
assert_eq!(
parse(["build", "--", "--", "-x"]),
run("build", &["--", "-x"])
);
}
#[test]
fn a_script_named_like_a_flag_word_still_wins() {
assert_eq!(parse(["help"]), run("help", &[]));
assert_eq!(parse(["version"]), run("version", &[]));
}
#[test]
fn help_and_version_are_recognised() {
assert_eq!(parse(["--help"]), Invocation::Help);
assert_eq!(parse(["-h"]), Invocation::Help);
assert_eq!(parse(["--version"]), Invocation::Version);
assert_eq!(parse(["-V"]), Invocation::Version);
}
#[test]
fn yes_answers_in_advance_and_leaves_the_script_alone() {
assert_eq!(parse(["--yes", "deploy"]), confirmed("deploy"));
assert_eq!(parse(["-y", "deploy"]), confirmed("deploy"));
assert_eq!(parse(["deploy"]), run("deploy", &[]));
}
#[test]
fn yes_after_the_script_belongs_to_the_script() {
assert_eq!(parse(["deploy", "--yes"]), run("deploy", &["--yes"]));
}
#[test]
fn yes_alone_still_lists() {
assert_eq!(parse(["--yes"]), Invocation::List);
}
#[test]
fn a_workflow_is_named_after_its_flag() {
assert_eq!(
parse(["--check", "release"]),
Invocation::Workflow("release".to_owned())
);
assert_eq!(parse(["release"]), run("release", &[]));
}
#[test]
fn a_workflow_flag_without_a_name_is_rejected() {
assert_eq!(
parse(["--check"]),
Invocation::UnknownFlag("--check".to_owned())
);
}
#[test]
fn built_in_areas_are_flags_so_a_script_can_own_the_word() {
assert_eq!(parse(["--health"]), Invocation::Health);
assert_eq!(parse(["--clean"]), Invocation::Clean);
assert_eq!(parse(["--security"]), Invocation::Security);
assert_eq!(parse(["--updates"]), Invocation::Updates);
assert_eq!(parse(["health"]), run("health", &[]));
assert_eq!(parse(["clean"]), run("clean", &[]));
}
#[test]
fn an_unknown_leading_flag_is_reported() {
assert_eq!(
parse(["--nope"]),
Invocation::UnknownFlag("--nope".to_owned())
);
}
#[test]
fn quoted_arguments_survive_as_single_values() {
assert_eq!(
parse(["test", "--grep", "two words"]),
run("test", &["--grep", "two words"])
);
}
}