use std::ffi::{OsStr, OsString};
const SUBCOMMANDS: [&str; 12] = [
"spawn",
"run",
"send",
"ask",
"wait",
"cancel",
"watch",
"inbox",
"fingerprint",
"serve",
"__daemon",
"help",
];
const TOP_LEVEL_FLAGS: [&str; 4] = ["-h", "--help", "-V", "--version"];
pub(crate) fn normalize(argv: impl IntoIterator<Item = OsString>) -> Vec<OsString> {
let mut argv: Vec<OsString> = argv.into_iter().collect();
let Some(first) = argv.get(1) else {
return argv;
};
if starts_a_run(first) {
argv.insert(1, OsString::from("spawn"));
}
argv
}
fn starts_a_run(first: &OsStr) -> bool {
match first.to_str() {
None => true,
Some(word) => !SUBCOMMANDS.contains(&word) && !TOP_LEVEL_FLAGS.contains(&word),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use clap::Parser;
use crate::{
cli::{Cli, Command, RunArgs},
exit::EXIT_USAGE,
};
fn normalized(argv: &[&str]) -> Vec<String> {
normalize(argv.iter().map(OsString::from))
.into_iter()
.map(|argument| argument.to_string_lossy().into_owned())
.collect()
}
fn shorthand(argv: &[&str]) -> RunArgs {
let parsed = Cli::try_parse_from(normalize(argv.iter().map(OsString::from)))
.expect("the shorthand should parse");
let Some(Command::Spawn(args)) = parsed.command else {
panic!("a prompt should have become a spawn: {argv:?}");
};
args
}
#[test]
fn a_positional_that_is_not_a_subcommand_is_a_prompt() {
assert_eq!(
shorthand(&["basis", "fix the failing test"]).prompt,
"fix the failing test"
);
}
#[test]
fn the_shorthand_is_exactly_the_spawn_subcommand() {
assert_eq!(
normalized(&["basis", "fix the failing test"]),
["basis", "spawn", "fix the failing test"]
);
}
#[test]
fn flags_pass_through_the_shorthand() {
let args = shorthand(&["basis", "--json", "--model", "gpt-5", "hi"]);
assert!(args.json);
assert_eq!(args.model.as_deref(), Some("gpt-5"));
assert_eq!(args.prompt, "hi");
}
#[test]
fn a_prompt_that_starts_with_a_dash_is_still_a_prompt() {
assert_eq!(shorthand(&["basis", "-"]).prompt, "-");
assert_eq!(
shorthand(&["basis", "-C", "/repo", "hi"]).workspace,
Some(PathBuf::from("/repo"))
);
}
#[test]
fn a_prompt_that_names_a_subcommand_needs_the_escape() {
let ambiguous =
Cli::try_parse_from(normalize(["basis", "spawn"].iter().map(OsString::from)))
.expect_err("a bare subcommand name is not a prompt");
assert_eq!(ambiguous.exit_code(), i32::from(EXIT_USAGE));
assert_eq!(shorthand(&["basis", "--", "run"]).prompt, "run");
assert_eq!(shorthand(&["basis", "--", "serve"]).prompt, "serve");
}
#[test]
fn lifecycle_verbs_are_never_rewritten_as_prompts() {
for verb in ["send", "wait", "cancel", "watch", "inbox"] {
assert_eq!(
normalized(&["basis", verb, "task"])[1],
verb,
"{verb} must reach clap as a subcommand"
);
}
}
#[test]
fn a_prompt_that_merely_begins_with_a_subcommand_name_needs_nothing() {
assert_eq!(
shorthand(&["basis", "run the tests and summarize"]).prompt,
"run the tests and summarize"
);
}
#[test]
fn bare_lan_is_left_alone_for_usage_to_handle() {
assert_eq!(normalized(&["basis"]), ["basis"]);
let parsed = Cli::try_parse_from(["basis"]).expect("parses");
assert!(
parsed.command.is_none(),
"no subcommand is handled by main as usage, never as a server"
);
}
#[test]
fn every_subcommand_still_reaches_itself() {
for subcommand in SUBCOMMANDS {
assert_eq!(
normalized(&["basis", subcommand]),
["basis", subcommand],
"{subcommand} must not be rewritten as a prompt"
);
}
}
#[test]
fn the_top_level_still_answers_for_help_and_version() {
for flag in TOP_LEVEL_FLAGS {
assert_eq!(normalized(&["basis", flag]), ["basis", flag]);
}
}
}