From 98f8fffc8e67f5b413a356a8364d7fc78e95af4b Mon Sep 17 00:00:00 2001 From: AxelRay <110029405+axelray-dev@users.noreply.github.com> Date: Sun, 13 Sep 2026 13:20:38 +0800 Subject: [PATCH 1/2] fix(discover): require token boundaries for rewrite rules --- src/discover/registry.rs | 83 +++++++++++++++++++++++++++++++++ src/discover/rules.rs | 42 ++++++++--------- src/filters/pio-run.toml | 2 +- src/filters/quarto-render.toml | 2 +- src/filters/shopify-theme.toml | 2 +- src/filters/terraform-plan.toml | 2 +- src/filters/trunk-build.toml | 2 +- 7 files changed, 109 insertions(+), 26 deletions(-) diff --git a/src/discover/registry.rs b/src/discover/registry.rs index e16da48dca..53720465a8 100644 --- a/src/discover/registry.rs +++ b/src/discover/registry.rs @@ -3162,6 +3162,89 @@ mod tests { ); } + #[test] + fn test_subcommand_rules_require_token_boundaries() { + // The pnpm case is covered separately. The sbt rule is included here + // because it is already boundary-safe and guards the full issue family + // against future regressions. + let false_positives = [ + "git branchless status", + "gh prs", + "glab mrs", + "cargo builder", + "prettierish", + "next builder", + "playwrighting", + "prismax", + "docker psql", + "kubectl getall", + "oc status-check", + "ruff checker", + "sqlfluff linting", + "pip installer", + "uv pip installer", + "go vetting", + "sbt tester", + "rake tester", + "rails tester", + "pio runner", + "quarto renderer", + "shopify themepark", + "terraform planner", + "trunk builder", + ]; + + for command in false_positives { + assert!( + matches!( + classify_command(command), + Classification::Unsupported { .. } + ), + "{command} must not classify as a supported command" + ); + assert_eq!( + rewrite_command_no_prefixes(command, &[]), + None, + "{command} must not be rewritten" + ); + } + + let valid_commands = [ + ("git branch status", "rtk git"), + ("gh pr list", "rtk gh"), + ("glab mr list", "rtk glab"), + ("cargo build --release", "rtk cargo"), + ("prettier --check .", "rtk prettier"), + ("next build --turbo", "rtk next"), + ("playwright test", "rtk playwright"), + ("prisma migrate status", "rtk prisma"), + ("docker ps", "rtk docker"), + ("kubectl get pods", "rtk kubectl"), + ("oc status", "rtk oc"), + ("ruff check .", "rtk ruff"), + ("sqlfluff lint .", "rtk sqlfluff"), + ("pip install flask", "rtk pip"), + ("uv pip install flask", "rtk uv"), + ("go test ./...", "rtk go"), + ("sbt test", "rtk sbt"), + ("rake test", "rtk rake"), + ("pio run", "rtk pio"), + ("quarto render docs", "rtk quarto"), + ("shopify theme push", "rtk shopify"), + ("terraform plan", "rtk terraform"), + ("trunk build", "rtk trunk"), + ]; + + for (command, expected_rtk_command) in valid_commands { + match classify_command(command) { + Classification::Supported { rtk_equivalent, .. } => { + assert_eq!(rtk_equivalent, expected_rtk_command, "{command}"); + } + classification => panic!("{command} classified as {classification:?}"), + } + } + } + #[test] fn test_rewrite_playwright() { let commands = vec![ diff --git a/src/discover/rules.rs b/src/discover/rules.rs index a7894c05be..c47c5fa568 100644 --- a/src/discover/rules.rs +++ b/src/discover/rules.rs @@ -56,7 +56,7 @@ impl Default for RtkRule { pub const RULES: &[RtkRule] = &[ RtkRule { - pattern: r"^(?:git|yadm)\s+(?:-[Cc]\s+\S+\s+)*(status|log|diff|show|add|commit|checkout|push|pull|branch|fetch|stash|worktree)", + pattern: r"^(?:git|yadm)\s+(?:-[Cc]\s+\S+\s+)*(status|log|diff|show|add|commit|checkout|push|pull|branch|fetch|stash|worktree)(?:\s|$|[;|&()<>])", rtk_cmd: "rtk git", pipeline_safety: PipelineSafety::ProducerOnly, rewrite_prefixes: &["git", "yadm"], @@ -71,7 +71,7 @@ pub const RULES: &[RtkRule] = &[ ..RtkRule::DEFAULT }, RtkRule { - pattern: r"^gh\s+(pr|issue|run|repo|api|release)", + pattern: r"^gh\s+(pr|issue|run|repo|api|release)(?:\s|$|[;|&()<>])", rtk_cmd: "rtk gh", rewrite_prefixes: &["gh"], category: "GitHub", @@ -80,7 +80,7 @@ pub const RULES: &[RtkRule] = &[ ..RtkRule::DEFAULT }, RtkRule { - pattern: r"^glab\s+(mr|issue|ci|pipeline|api|release)", + pattern: r"^glab\s+(mr|issue|ci|pipeline|api|release)(?:\s|$|[;|&()<>])", rtk_cmd: "rtk glab", rewrite_prefixes: &["glab"], category: "GitLab", @@ -89,7 +89,7 @@ pub const RULES: &[RtkRule] = &[ ..RtkRule::DEFAULT }, RtkRule { - pattern: r"^cargo\s+(build|test|clippy|check|fmt|install)", + pattern: r"^cargo\s+(build|test|clippy|check|fmt|install)(?:\s|$|[;|&()<>])", rtk_cmd: "rtk cargo", pipeline_safety: PipelineSafety::ProducerOnly, rewrite_prefixes: &["cargo"], @@ -241,7 +241,7 @@ pub const RULES: &[RtkRule] = &[ ..RtkRule::DEFAULT }, RtkRule { - pattern: r"^((p?np(m|x)|p?npm\s+(exec|run|run-script)|npm\s+(rum|urn|x)|pnpm\s+dlx)\s+)?prettier", + pattern: r"^((p?np(m|x)|p?npm\s+(exec|run|run-script)|npm\s+(rum|urn|x)|pnpm\s+dlx)\s+)?prettier(?:\s|$|[;|&()<>])", rtk_cmd: "rtk prettier", pipeline_safety: PipelineSafety::ProducerOnly, rewrite_prefixes: &[ @@ -266,7 +266,7 @@ pub const RULES: &[RtkRule] = &[ ..RtkRule::DEFAULT }, RtkRule { - pattern: r"^((p?np(m|x)|p?npm\s+(exec|run|run-script)|npm\s+(rum|urn|x)|pnpm\s+dlx)\s+)?next\s+build", + pattern: r"^((p?np(m|x)|p?npm\s+(exec|run|run-script)|npm\s+(rum|urn|x)|pnpm\s+dlx)\s+)?next\s+build(?:\s|$|[;|&()<>])", rtk_cmd: "rtk next", pipeline_safety: PipelineSafety::ProducerOnly, rewrite_prefixes: &[ @@ -377,7 +377,7 @@ pub const RULES: &[RtkRule] = &[ ..RtkRule::DEFAULT }, RtkRule { - pattern: r"^((p?np(m|x)|p?npm\s+(exec|run|run-script)|npm\s+(rum|urn|x)|pnpm\s+dlx)\s+)?playwright", + pattern: r"^((p?np(m|x)|p?npm\s+(exec|run|run-script)|npm\s+(rum|urn|x)|pnpm\s+dlx)\s+)?playwright(?:\s|$|[;|&()<>])", rtk_cmd: "rtk playwright", rewrite_prefixes: &[ "npm exec playwright", @@ -401,7 +401,7 @@ pub const RULES: &[RtkRule] = &[ ..RtkRule::DEFAULT }, RtkRule { - pattern: r"^((p?np(m|x)|p?npm\s+(exec|run|run-script)|npm\s+(rum|urn|x)|pnpm\s+dlx)\s+)?prisma", + pattern: r"^((p?np(m|x)|p?npm\s+(exec|run|run-script)|npm\s+(rum|urn|x)|pnpm\s+dlx)\s+)?prisma(?:\s|$|[;|&()<>])", rtk_cmd: "rtk prisma", rewrite_prefixes: &[ "npm exec prisma", @@ -425,7 +425,7 @@ pub const RULES: &[RtkRule] = &[ ..RtkRule::DEFAULT }, RtkRule { - pattern: r"^docker\s+(ps|images|logs|run|exec|build|compose\s+(ps|logs|build))", + pattern: r"^docker\s+(ps|images|logs|run|exec|build|compose\s+(ps|logs|build))(?:\s|$|[;|&()<>])", rtk_cmd: "rtk docker", rewrite_prefixes: &["docker"], category: "Infra", @@ -433,7 +433,7 @@ pub const RULES: &[RtkRule] = &[ ..RtkRule::DEFAULT }, RtkRule { - pattern: r"^kubectl\s+(get|logs|describe|apply)", + pattern: r"^kubectl\s+(get|logs|describe|apply)(?:\s|$|[;|&()<>])", rtk_cmd: "rtk kubectl", rewrite_prefixes: &["kubectl"], category: "Infra", @@ -441,7 +441,7 @@ pub const RULES: &[RtkRule] = &[ ..RtkRule::DEFAULT }, RtkRule { - pattern: r"^oc\s+(get|logs|describe|apply|status|adm)", + pattern: r"^oc\s+(get|logs|describe|apply|status|adm)(?:\s|$|[;|&()<>])", rtk_cmd: "rtk oc", rewrite_prefixes: &["oc"], category: "Infra", @@ -491,7 +491,7 @@ pub const RULES: &[RtkRule] = &[ ..RtkRule::DEFAULT }, RtkRule { - pattern: r"^ruff\s+(check|format)", + pattern: r"^ruff\s+(check|format)(?:\s|$|[;|&()<>])", rtk_cmd: "rtk ruff", pipeline_safety: PipelineSafety::ProducerOnly, rewrite_prefixes: &["ruff"], @@ -501,7 +501,7 @@ pub const RULES: &[RtkRule] = &[ ..RtkRule::DEFAULT }, RtkRule { - pattern: r"^sqlfluff\s+lint", + pattern: r"^sqlfluff\s+lint(?:\s|$|[;|&()<>])", rtk_cmd: "rtk sqlfluff", rewrite_prefixes: &["sqlfluff"], category: "Python", @@ -518,7 +518,7 @@ pub const RULES: &[RtkRule] = &[ ..RtkRule::DEFAULT }, RtkRule { - pattern: r"^(pip3?|uv\s+pip)\s+(list|outdated|install|show)", + pattern: r"^(pip3?|uv\s+pip)\s+(list|outdated|install|show)(?:\s|$|[;|&()<>])", rtk_cmd: "rtk pip", pipeline_safety: PipelineSafety::ProducerOnly, rewrite_prefixes: &["pip3", "pip", "uv pip"], @@ -536,7 +536,7 @@ pub const RULES: &[RtkRule] = &[ ..RtkRule::DEFAULT }, RtkRule { - pattern: r"^go\s+(test|build|vet)", + pattern: r"^go\s+(test|build|vet)(?:\s|$|[;|&()<>])", rtk_cmd: "rtk go", pipeline_safety: PipelineSafety::ProducerOnly, rewrite_prefixes: &["go"], @@ -574,7 +574,7 @@ pub const RULES: &[RtkRule] = &[ ..RtkRule::DEFAULT }, RtkRule { - pattern: r"^(?:bundle\s+exec\s+)?(?:bin/)?(?:rake|rails)\s+test", + pattern: r"^(?:bundle\s+exec\s+)?(?:bin/)?(?:rake|rails)\s+test(?:\s|$|[;|&()<>])", rtk_cmd: "rtk rake", pipeline_safety: PipelineSafety::ProducerOnly, rewrite_prefixes: &[ @@ -930,7 +930,7 @@ pub const RULES: &[RtkRule] = &[ ..RtkRule::DEFAULT }, RtkRule { - pattern: r"^pio\s+run", + pattern: r"^pio\s+run(?:\s|$|[;|&()<>])", rtk_cmd: "rtk pio", pipeline_safety: PipelineSafety::ProducerOnly, rewrite_prefixes: &["pio"], @@ -980,7 +980,7 @@ pub const RULES: &[RtkRule] = &[ ..RtkRule::DEFAULT }, RtkRule { - pattern: r"^quarto\s+render", + pattern: r"^quarto\s+render(?:\s|$|[;|&()<>])", rtk_cmd: "rtk quarto", pipeline_safety: PipelineSafety::ProducerOnly, rewrite_prefixes: &["quarto"], @@ -1006,7 +1006,7 @@ pub const RULES: &[RtkRule] = &[ ..RtkRule::DEFAULT }, RtkRule { - pattern: r"^shopify\s+theme\s+(push|pull)", + pattern: r"^shopify\s+theme\s+(push|pull)(?:\s|$|[;|&()<>])", rtk_cmd: "rtk shopify", pipeline_safety: PipelineSafety::ProducerOnly, rewrite_prefixes: &["shopify"], @@ -1041,7 +1041,7 @@ pub const RULES: &[RtkRule] = &[ ..RtkRule::DEFAULT }, RtkRule { - pattern: r"^terraform\s+plan", + pattern: r"^terraform\s+plan(?:\s|$|[;|&()<>])", rtk_cmd: "rtk terraform", pipeline_safety: PipelineSafety::ProducerOnly, rewrite_prefixes: &["terraform"], @@ -1059,7 +1059,7 @@ pub const RULES: &[RtkRule] = &[ ..RtkRule::DEFAULT }, RtkRule { - pattern: r"^trunk\s+build", + pattern: r"^trunk\s+build(?:\s|$|[;|&()<>])", rtk_cmd: "rtk trunk", pipeline_safety: PipelineSafety::ProducerOnly, rewrite_prefixes: &["trunk"], diff --git a/src/filters/pio-run.toml b/src/filters/pio-run.toml index 863df636f5..54d2471ff0 100644 --- a/src/filters/pio-run.toml +++ b/src/filters/pio-run.toml @@ -1,6 +1,6 @@ [filters.pio-run] description = "Compact PlatformIO build output" -match_command = "^pio\\s+run" +match_command = "^pio\\s+run(\\s|$|[;|&()<>])" strip_ansi = true strip_lines_matching = [ "^\\s*$", diff --git a/src/filters/quarto-render.toml b/src/filters/quarto-render.toml index ad59e7a00a..e8520d4b7a 100644 --- a/src/filters/quarto-render.toml +++ b/src/filters/quarto-render.toml @@ -1,6 +1,6 @@ [filters.quarto-render] description = "Compact quarto render output" -match_command = "^quarto\\s+render" +match_command = "^quarto\\s+render(\\s|$|[;|&()<>])" strip_ansi = true strip_lines_matching = [ "^\\s*$", diff --git a/src/filters/shopify-theme.toml b/src/filters/shopify-theme.toml index a6e6cee5a1..1eb016cb69 100644 --- a/src/filters/shopify-theme.toml +++ b/src/filters/shopify-theme.toml @@ -1,6 +1,6 @@ [filters.shopify-theme] description = "Compact shopify theme push/pull output" -match_command = "^shopify\\s+theme\\s+(push|pull)" +match_command = "^shopify\\s+theme\\s+(push|pull)(\\s|$|[;|&()<>])" strip_ansi = true strip_lines_matching = [ "^\\s*$", diff --git a/src/filters/terraform-plan.toml b/src/filters/terraform-plan.toml index 726cd9dd7e..7ee22f35f5 100644 --- a/src/filters/terraform-plan.toml +++ b/src/filters/terraform-plan.toml @@ -1,6 +1,6 @@ [filters.terraform-plan] description = "Compact Terraform plan output" -match_command = "^terraform\\s+plan" +match_command = "^terraform\\s+plan(\\s|$|[;|&()<>])" strip_ansi = true strip_lines_matching = [ "^Refreshing state", diff --git a/src/filters/trunk-build.toml b/src/filters/trunk-build.toml index 21b4a62777..2baf5b4da2 100644 --- a/src/filters/trunk-build.toml +++ b/src/filters/trunk-build.toml @@ -1,6 +1,6 @@ [filters.trunk-build] description = "Compact trunk build output" -match_command = "^trunk\\s+build" +match_command = "^trunk\\s+build(\\s|$|[;|&()<>])" strip_ansi = true strip_lines_matching = [ "^\\s*$", From e5518dcc1335da3f5faa75a4d716a0fcafe8cfb9 Mon Sep 17 00:00:00 2001 From: AxelRay <110029405+axelray-dev@users.noreply.github.com> Date: Sun, 20 Sep 2026 03:05:46 +0800 Subject: [PATCH 2/2] fix(discover): preserve namespaced rake test rewrites --- src/discover/registry.rs | 4 ++++ src/discover/rules.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/discover/registry.rs b/src/discover/registry.rs index 53720465a8..f441fc4f90 100644 --- a/src/discover/registry.rs +++ b/src/discover/registry.rs @@ -3228,6 +3228,10 @@ mod tests { ("go test ./...", "rtk go"), ("sbt test", "rtk sbt"), ("rake test", "rtk rake"), + ("rake test:unit", "rtk rake"), + ("rails test:system", "rtk rake"), + ("bundle exec rake test:models", "rtk rake"), + ("bin/rails test:integration", "rtk rake"), ("pio run", "rtk pio"), ("quarto render docs", "rtk quarto"), ("shopify theme push", "rtk shopify"), diff --git a/src/discover/rules.rs b/src/discover/rules.rs index c47c5fa568..1dbc9facc9 100644 --- a/src/discover/rules.rs +++ b/src/discover/rules.rs @@ -574,7 +574,7 @@ pub const RULES: &[RtkRule] = &[ ..RtkRule::DEFAULT }, RtkRule { - pattern: r"^(?:bundle\s+exec\s+)?(?:bin/)?(?:rake|rails)\s+test(?:\s|$|[;|&()<>])", + pattern: r"^(?:bundle\s+exec\s+)?(?:bin/)?(?:rake|rails)\s+test(?:[\s:]|$|[;|&()<>])", rtk_cmd: "rtk rake", pipeline_safety: PipelineSafety::ProducerOnly, rewrite_prefixes: &[