#[inline]
fn is_known_subcommand(s: &str) -> bool {
matches!(
s,
"serve"
| "bench"
| "config"
| "doctor"
| "web-bearer"
| "models"
| "probe"
| "mcp"
| "save-session"
| "export-session"
| "tool-replay"
| "sse-replay"
| "plugin"
| "workflow"
| "e2e"
)
}
fn normalize_help_argv(prog: String, rest: &[String]) -> Option<Vec<String>> {
if rest.first().is_none_or(|s| s != "help") {
return None;
}
Some(match rest.len() {
1 => vec![prog, "--help".into()],
_ if is_known_subcommand(rest[1].as_str()) => {
vec![prog, rest[1].clone(), "--help".into()]
}
_ => vec![prog, "--help".into()],
})
}
fn rewrite_dry_run_to_config(prog: &str, rest: &[String]) -> Option<Vec<String>> {
if !rest.iter().any(|a| a == "--dry-run") {
return None;
}
let mut out = vec![prog.to_string(), "config".into()];
for a in rest {
if a != "--dry-run" {
out.push(a.clone());
}
}
out.push("--dry-run".into());
Some(out)
}
fn rewrite_serve_subcommand(prog: String, rest: &[String]) -> Option<Vec<String>> {
if !rest.iter().any(|a| a == "--serve") {
return None;
}
let mut new_rest: Vec<String> = Vec::new();
let mut i = 0;
while i < rest.len() {
if rest[i] == "--serve" {
i += 1;
if i < rest.len() && !rest[i].starts_with('-') {
i += 1;
}
continue;
}
new_rest.push(rest[i].clone());
i += 1;
}
let mut out = vec![prog, "serve".into()];
i = 0;
while i < rest.len() {
if rest[i] == "--serve" {
i += 1;
if i < rest.len() && !rest[i].starts_with('-') {
out.push(rest[i].clone());
}
break;
}
i += 1;
}
out.extend(new_rest);
Some(out)
}
#[inline]
fn flag_or_eq_value(arg: &str, base: &str) -> bool {
arg == base
|| (arg.len() > base.len()
&& arg.starts_with(base)
&& arg.as_bytes().get(base.len()) == Some(&b'='))
}
const LEGACY_BENCH_FLAG_BASES: &[&str] = &[
"--benchmark",
"--batch",
"--batch-output",
"--task-timeout",
"--max-tool-rounds",
"--samples",
"--bench-system-prompt",
];
fn rest_has_legacy_bench_flag(rest: &[String]) -> bool {
rest.iter().any(|a| {
a.as_str() == "--resume"
|| LEGACY_BENCH_FLAG_BASES
.iter()
.any(|base| flag_or_eq_value(a, base))
})
}
pub fn normalize_legacy_argv(args: Vec<String>) -> Vec<String> {
if args.len() <= 1 {
return args;
}
let prog = args[0].clone();
let rest = &args[1..];
if let Some(v) = normalize_help_argv(prog.clone(), rest) {
return v;
}
if rest.iter().any(|a| is_known_subcommand(a.as_str())) {
return args;
}
if rest
.iter()
.any(|a| matches!(a.as_str(), "-h" | "--help" | "-V" | "--version"))
{
return args;
}
if let Some(out) = rewrite_dry_run_to_config(&prog, rest) {
return out;
}
if let Some(out) = rewrite_serve_subcommand(prog.clone(), rest) {
return out;
}
if rest_has_legacy_bench_flag(rest) {
let mut out = vec![prog, "bench".into()];
out.extend(rest.iter().cloned());
return out;
}
args
}
#[cfg(test)]
mod legacy_argv_tests {
use super::normalize_legacy_argv;
use crate::cm_config::cli::definitions::{Commands, ExtraCliCommand, RootCli};
use crate::cm_config::cli::parse::parse_args_from_argv;
use clap::Parser;
fn norm(args: &[&str]) -> Vec<String> {
normalize_legacy_argv(args.iter().map(|s| (*s).to_string()).collect())
}
#[test]
fn explicit_subcommand_unchanged() {
let v = norm(&["crabmate", "serve", "3000"]);
assert_eq!(v, vec!["crabmate", "serve", "3000"]);
}
#[test]
fn help_save_session_routes_to_subcommand_help() {
let v = norm(&["crabmate", "help", "save-session"]);
assert_eq!(v, vec!["crabmate", "save-session", "--help"]);
}
#[test]
fn help_export_session_alias_routes_to_subcommand_help() {
let v = norm(&["crabmate", "help", "export-session"]);
assert_eq!(v, vec!["crabmate", "export-session", "--help"]);
}
#[test]
fn explicit_doctor_subcommand_not_prefixed_with_repl() {
let v = norm(&["crabmate", "doctor"]);
assert_eq!(v, vec!["crabmate", "doctor"]);
}
#[test]
fn explicit_web_bearer_subcommand_not_prefixed_with_repl() {
let v = norm(&["crabmate", "web-bearer", "status"]);
assert_eq!(v, vec!["crabmate", "web-bearer", "status"]);
}
#[test]
fn explicit_workflow_validate_not_prefixed_with_repl() {
let v = norm(&[
"crabmate",
"workflow",
"validate",
"fixtures/workflows/01_serial_after.yaml",
]);
assert_eq!(
v,
vec![
"crabmate",
"workflow",
"validate",
"fixtures/workflows/01_serial_after.yaml",
]
);
}
#[test]
fn try_parse_root_doctor_subcommand() {
let r = RootCli::try_parse_from(vec!["crabmate".to_string(), "doctor".to_string()]);
assert!(r.is_ok(), "{:?}", r.as_ref().err());
assert!(matches!(r.unwrap().command, Commands::Doctor));
}
#[test]
fn parse_args_from_argv_doctor_matches_extra_cli() {
let p = parse_args_from_argv(vec!["crabmate".to_string(), "doctor".to_string()]).unwrap();
assert_eq!(p.extra_cli, ExtraCliCommand::Doctor);
}
#[test]
fn parse_serve_default_is_api_only() {
let p = parse_args_from_argv(vec!["crabmate".to_string(), "serve".to_string()]).unwrap();
assert_eq!(p.serve_port, Some(8080));
assert!(!p.with_web);
}
#[test]
fn parse_serve_with_web_enables_ui_mount() {
let p = parse_args_from_argv(vec![
"crabmate".to_string(),
"serve".to_string(),
"--with-web".to_string(),
])
.unwrap();
assert!(p.with_web);
}
#[test]
fn parse_serve_no_web_is_rejected() {
let err = parse_args_from_argv(vec![
"crabmate".to_string(),
"serve".to_string(),
"--no-web".to_string(),
])
.expect_err("--no-web must be unknown");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
}
#[test]
fn parse_config_default_skips_web_static_check() {
let p = parse_args_from_argv(vec!["crabmate".to_string(), "config".to_string()]).unwrap();
assert!(p.dry_run);
assert!(!p.with_web);
}
#[test]
fn legacy_serve_with_port() {
let v = norm(&["crabmate", "--serve", "3000"]);
assert_eq!(v, vec!["crabmate", "serve", "3000"]);
}
#[test]
fn legacy_serve_default_port() {
let v = norm(&["crabmate", "--serve"]);
assert_eq!(v, vec!["crabmate", "serve"]);
}
#[test]
fn legacy_serve_then_host() {
let v = norm(&["crabmate", "--serve", "--host", "0.0.0.0"]);
assert_eq!(v, vec!["crabmate", "serve", "--host", "0.0.0.0"]);
}
#[test]
fn legacy_no_longer_inserts_repl_or_chat() {
let v = norm(&["crabmate", "--no-stream"]);
assert_eq!(v, vec!["crabmate", "--no-stream"]);
let v = norm(&["crabmate", "--query", "hi"]);
assert_eq!(v, vec!["crabmate", "--query", "hi"]);
let v = norm(&["crabmate", "--message-file", "cases.jsonl"]);
assert_eq!(v, vec!["crabmate", "--message-file", "cases.jsonl"]);
}
#[test]
fn parse_removed_chat_subcommand_fails() {
let err = parse_args_from_argv(vec![
"crabmate".to_string(),
"chat".to_string(),
"--query".to_string(),
"hi".to_string(),
])
.expect_err("chat entry removed");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
}
#[test]
fn legacy_config_dry_run() {
let v = norm(&["crabmate", "--dry-run"]);
assert_eq!(v, vec!["crabmate", "config", "--dry-run"]);
}
#[test]
fn help_not_wrapped() {
let v = norm(&["crabmate", "--help"]);
assert_eq!(v, vec!["crabmate", "--help"]);
}
#[test]
fn help_subcommand_maps_to_root_help() {
let v = norm(&["crabmate", "help"]);
assert_eq!(v, vec!["crabmate", "--help"]);
}
#[test]
fn help_known_subcommand_maps_to_subcommand_help() {
let v = norm(&["crabmate", "help", "serve"]);
assert_eq!(v, vec!["crabmate", "serve", "--help"]);
}
#[test]
fn help_doctor_maps_to_subcommand_help() {
let v = norm(&["crabmate", "help", "doctor"]);
assert_eq!(v, vec!["crabmate", "doctor", "--help"]);
}
#[test]
fn help_unknown_second_token_falls_back_to_root_help() {
let v = norm(&["crabmate", "help", "nope"]);
assert_eq!(v, vec!["crabmate", "--help"]);
}
}