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_version_bump_flag(args: Vec<String>) -> Vec<String> {
if args.len() < 5 {
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 = ["bump", "show", "set", "edit", "merge"];
if args
.get(2)
.map(|s| known_subcommands.contains(&s.as_str()))
.unwrap_or(false)
{
return args;
}
let bump_pos = args.iter().position(|s| s == "--bump");
let Some(bump_pos) = bump_pos else {
return args;
};
let Some(bump_type) = args.get(bump_pos + 1) else {
return args;
};
let mut result = vec![
args[0].clone(), args[1].clone(), "bump".to_string(), ];
for arg in &args[2..bump_pos] {
result.push(arg.clone());
}
result.push(bump_type.clone());
for arg in &args[bump_pos + 2..] {
result.push(arg.clone());
}
result
}
pub fn normalize_changelog_component(args: Vec<String>) -> Vec<String> {
let is_changelog_add = args.len() >= 4
&& args.get(1).map(|s| s == "changelog").unwrap_or(false)
&& args.get(2).map(|s| s == "add").unwrap_or(false);
if !is_changelog_add {
return args;
}
let component_pos = args.iter().position(|s| s == "--component");
let Some(component_pos) = component_pos else {
return args;
};
let Some(component_value) = args.get(component_pos + 1) else {
return args;
};
let mut result = vec![
args[0].clone(), args[1].clone(), args[2].clone(), component_value.clone(), ];
for (i, arg) in args.iter().enumerate().skip(3) {
if i == component_pos || i == component_pos + 1 {
continue;
}
result.push(arg.clone());
}
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_bump_flag(args); let args = normalize_version_show(args);
let args = normalize_changelog_component(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");
}
#[test]
fn test_version_bump_flag_to_subcommand() {
let args = vec![
"homeboy".into(),
"version".into(),
"data-machine".into(),
"--bump".into(),
"patch".into(),
];
let result = normalize_version_bump_flag(args);
assert_eq!(
result,
vec!["homeboy", "version", "bump", "data-machine", "patch"]
);
}
#[test]
fn test_version_bump_flag_with_extra_flags() {
let args = vec![
"homeboy".into(),
"version".into(),
"data-machine".into(),
"--bump".into(),
"minor".into(),
"--dry-run".into(),
];
let result = normalize_version_bump_flag(args);
assert_eq!(
result,
vec![
"homeboy",
"version",
"bump",
"data-machine",
"minor",
"--dry-run"
]
);
}
#[test]
fn test_version_bump_subcommand_unchanged() {
let args = vec![
"homeboy".into(),
"version".into(),
"bump".into(),
"data-machine".into(),
"patch".into(),
];
let result = normalize_version_bump_flag(args.clone());
assert_eq!(result, args);
}
#[test]
fn test_changelog_component_flag_to_positional() {
let args = vec![
"homeboy".into(),
"changelog".into(),
"add".into(),
"--component".into(),
"my-plugin".into(),
"-m".into(),
"message".into(),
];
let result = normalize_changelog_component(args);
assert_eq!(
result,
vec!["homeboy", "changelog", "add", "my-plugin", "-m", "message"]
);
}
#[test]
fn test_changelog_positional_unchanged() {
let args = vec![
"homeboy".into(),
"changelog".into(),
"add".into(),
"my-plugin".into(),
"-m".into(),
"message".into(),
];
let result = normalize_changelog_component(args.clone());
assert_eq!(result, args);
}
}