pub fn normalize_version_bump(args: Vec<String>) -> Vec<String> {
let is_version_bump = args.len() >= 3
&& args.get(1).map(|s| s == "version").unwrap_or(false)
&& args.get(2).map(|s| s == "bump").unwrap_or(false);
if !is_version_bump {
return args;
}
let bump_flags = ["--patch", "--minor", "--major"];
let mut result = Vec::new();
let mut found_bump_type: Option<String> = None;
for arg in args {
if bump_flags.contains(&arg.as_str()) && found_bump_type.is_none() {
found_bump_type = Some(arg.trim_start_matches('-').to_string());
} else {
result.push(arg);
}
}
if let Some(bump_type) = found_bump_type {
result.push("--".to_string());
result.push(bump_type);
}
result
}
pub fn normalize_version_show(args: Vec<String>) -> Vec<String> {
if args.len() < 3 {
return args;
}
let is_version_cmd = args.get(1).map(|s| s == "version").unwrap_or(false);
if !is_version_cmd {
return args;
}
let known_subcommands = ["show", "set", "bump", "edit", "merge", "--help", "-h", "help"];
let second_arg = args.get(2).map(|s| s.as_str()).unwrap_or("");
if known_subcommands.contains(&second_arg) || second_arg.starts_with('-') {
return args;
}
let mut result = Vec::with_capacity(args.len() + 1);
result.push(args[0].clone()); result.push(args[1].clone()); result.push("show".to_string()); result.extend(args[2..].iter().cloned());
result
}
pub fn normalize_trailing_flags(args: Vec<String>) -> Vec<String> {
let commands: &[(&str, &str, &[&str])] = &[
(
"component",
"set",
&["--json", "--replace", "--help", "-h"],
),
(
"component",
"edit",
&["--json", "--replace", "--help", "-h"],
),
(
"component",
"merge",
&["--json", "--replace", "--help", "-h"],
),
("server", "set", &["--json", "--replace", "--help", "-h"]),
("server", "edit", &["--json", "--replace", "--help", "-h"]),
("server", "merge", &["--json", "--replace", "--help", "-h"]),
(
"test",
"",
&["--skip-lint", "--setting", "--json", "--help", "-h"],
),
];
let known_flags = commands.iter().find_map(|(cmd, subcmd, flags)| {
let matches = if subcmd.is_empty() {
args.get(1).map(|s| s == *cmd).unwrap_or(false)
} else {
args.get(1).map(|s| s == *cmd).unwrap_or(false)
&& args.get(2).map(|s| s == *subcmd).unwrap_or(false)
};
if matches {
Some(*flags)
} else {
None
}
});
let Some(known_flags) = known_flags else {
return args;
};
let mut result = Vec::new();
let mut found_separator = false;
let mut insert_position: Option<usize> = None;
for (i, arg) in args.iter().enumerate() {
if arg == "--" {
found_separator = true;
}
if !found_separator
&& arg.starts_with("--")
&& !known_flags.contains(&arg.as_str())
&& !known_flags
.iter()
.any(|f| arg.starts_with(&format!("{}=", f)))
&& insert_position.is_none()
{
insert_position = Some(i);
}
result.push(arg.clone());
}
if let Some(pos) = insert_position {
result.insert(pos, "--".to_string());
}
result
}
pub fn normalize(args: Vec<String>) -> Vec<String> {
let args = normalize_version_bump(args);
let args = normalize_version_show(args);
normalize_trailing_flags(args)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_component_set_inserts_separator() {
let args = vec![
"homeboy".into(),
"component".into(),
"set".into(),
"my-plugin".into(),
"--changelog_target".into(),
"docs/CHANGELOG.md".into(),
];
let result = normalize_trailing_flags(args);
assert_eq!(result[4], "--");
assert_eq!(result[5], "--changelog_target");
}
#[test]
fn test_component_set_preserves_existing_separator() {
let args = vec![
"homeboy".into(),
"component".into(),
"set".into(),
"my-plugin".into(),
"--".into(),
"--changelog_target".into(),
"docs/CHANGELOG.md".into(),
];
let result = normalize_trailing_flags(args.clone());
assert_eq!(result, args);
}
#[test]
fn test_component_set_allows_known_flags() {
let args = vec![
"homeboy".into(),
"component".into(),
"set".into(),
"my-plugin".into(),
"--json".into(),
"{}".into(),
];
let result = normalize_trailing_flags(args.clone());
assert_eq!(result, args); }
#[test]
fn test_test_command_inserts_separator() {
let args = vec![
"homeboy".into(),
"test".into(),
"my-component".into(),
"--verbose".into(),
"--filter=test_foo".into(),
];
let result = normalize_trailing_flags(args);
assert_eq!(result[3], "--");
assert_eq!(result[4], "--verbose");
}
#[test]
fn test_test_command_allows_known_flags() {
let args = vec![
"homeboy".into(),
"test".into(),
"my-component".into(),
"--skip-lint".into(),
];
let result = normalize_trailing_flags(args.clone());
assert_eq!(result, args); }
#[test]
fn test_syntax_agnostic_both_styles_equivalent() {
let without_sep = vec![
"homeboy".into(),
"component".into(),
"set".into(),
"my-plugin".into(),
"--field".into(),
"value".into(),
];
let with_sep = vec![
"homeboy".into(),
"component".into(),
"set".into(),
"my-plugin".into(),
"--".into(),
"--field".into(),
"value".into(),
];
let result1 = normalize_trailing_flags(without_sep);
let result2 = normalize_trailing_flags(with_sep.clone());
assert_eq!(result1, with_sep);
assert_eq!(result2, with_sep);
}
#[test]
fn test_version_bump_normalization() {
let args = vec![
"homeboy".into(),
"version".into(),
"bump".into(),
"my-plugin".into(),
"--patch".into(),
];
let result = normalize_version_bump(args);
assert_eq!(
result,
vec!["homeboy", "version", "bump", "my-plugin", "--", "patch"]
);
}
#[test]
fn test_version_show_normalization() {
let args = vec!["homeboy".into(), "version".into(), "my-plugin".into()];
let result = normalize_version_show(args);
assert_eq!(result, vec!["homeboy", "version", "show", "my-plugin"]);
}
#[test]
fn test_version_show_preserves_subcommand() {
let args = vec![
"homeboy".into(),
"version".into(),
"bump".into(),
"my-plugin".into(),
];
let result = normalize_version_show(args.clone());
assert_eq!(result, args);
}
#[test]
fn test_full_normalize_pipeline() {
let args = vec![
"homeboy".into(),
"component".into(),
"set".into(),
"my-plugin".into(),
"--build_command".into(),
"npm run build".into(),
];
let result = normalize(args);
assert_eq!(result[4], "--");
assert_eq!(result[5], "--build_command");
}
}