mirror of
https://github.com/rtk-ai/rtk.git
synced 2026-09-19 07:33:17 +08:00
1927ed043c
Replace ✓ (U+2713) with plain text across 19 files: - "ok ✓" → "ok" (git add/commit/push/pull) - "✓ cargo test: ..." → "cargo test: ..." (all tool summaries) - Preserved ✓ in input detection patterns and test fixtures LLMs cannot interpret emoji semantics; plain text is clearer. Signed-off-by: Patrick szymkowiak <patrick.szymkowiak@innovtech.eu>
237 lines
5.7 KiB
Rust
237 lines
5.7 KiB
Rust
use crate::tracking;
|
|
use crate::utils::resolved_command;
|
|
use anyhow::{Context, Result};
|
|
|
|
/// Known npm subcommands that should NOT get "run" injected.
|
|
/// Shared between production code and tests to avoid drift.
|
|
const NPM_SUBCOMMANDS: &[&str] = &[
|
|
"install",
|
|
"i",
|
|
"ci",
|
|
"uninstall",
|
|
"remove",
|
|
"rm",
|
|
"update",
|
|
"up",
|
|
"list",
|
|
"ls",
|
|
"outdated",
|
|
"init",
|
|
"create",
|
|
"publish",
|
|
"pack",
|
|
"link",
|
|
"audit",
|
|
"fund",
|
|
"exec",
|
|
"explain",
|
|
"why",
|
|
"search",
|
|
"view",
|
|
"info",
|
|
"show",
|
|
"config",
|
|
"set",
|
|
"get",
|
|
"cache",
|
|
"prune",
|
|
"dedupe",
|
|
"doctor",
|
|
"help",
|
|
"version",
|
|
"prefix",
|
|
"root",
|
|
"bin",
|
|
"bugs",
|
|
"docs",
|
|
"home",
|
|
"repo",
|
|
"ping",
|
|
"whoami",
|
|
"token",
|
|
"profile",
|
|
"team",
|
|
"access",
|
|
"owner",
|
|
"deprecate",
|
|
"dist-tag",
|
|
"star",
|
|
"stars",
|
|
"login",
|
|
"logout",
|
|
"adduser",
|
|
"unpublish",
|
|
"pkg",
|
|
"diff",
|
|
"rebuild",
|
|
"test",
|
|
"t",
|
|
"start",
|
|
"stop",
|
|
"restart",
|
|
];
|
|
|
|
pub fn run(args: &[String], verbose: u8, skip_env: bool) -> Result<()> {
|
|
let timer = tracking::TimedExecution::start();
|
|
|
|
let mut cmd = resolved_command("npm");
|
|
|
|
// Determine if this is "npm run <script>" or another npm subcommand (install, list, etc.)
|
|
// Only inject "run" when args look like a script name, not a known npm subcommand.
|
|
let first_arg = args.first().map(|s| s.as_str());
|
|
let is_run_explicit = first_arg == Some("run");
|
|
let is_npm_subcommand = first_arg
|
|
.map(|a| NPM_SUBCOMMANDS.contains(&a) || a.starts_with('-'))
|
|
.unwrap_or(false);
|
|
|
|
let effective_args = if is_run_explicit {
|
|
// "rtk npm run build" → "npm run build"
|
|
cmd.arg("run");
|
|
&args[1..]
|
|
} else if is_npm_subcommand {
|
|
// "rtk npm install express" → "npm install express"
|
|
args
|
|
} else {
|
|
// "rtk npm build" → "npm run build" (assume script name)
|
|
cmd.arg("run");
|
|
args
|
|
};
|
|
|
|
for arg in effective_args {
|
|
cmd.arg(arg);
|
|
}
|
|
|
|
if skip_env {
|
|
cmd.env("SKIP_ENV_VALIDATION", "1");
|
|
}
|
|
|
|
if verbose > 0 {
|
|
eprintln!("Running: npm {}", args.join(" "));
|
|
}
|
|
|
|
let output = cmd.output().context("Failed to run npm")?;
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
let raw = format!("{}\n{}", stdout, stderr);
|
|
|
|
let filtered = filter_npm_output(&raw);
|
|
println!("{}", filtered);
|
|
|
|
timer.track(
|
|
&format!("npm {}", args.join(" ")),
|
|
&format!("rtk npm {}", args.join(" ")),
|
|
&raw,
|
|
&filtered,
|
|
);
|
|
|
|
if !output.status.success() {
|
|
std::process::exit(output.status.code().unwrap_or(1));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Filter npm run output - strip boilerplate, progress bars, npm WARN
|
|
fn filter_npm_output(output: &str) -> String {
|
|
let mut result = Vec::new();
|
|
|
|
for line in output.lines() {
|
|
// Skip npm boilerplate
|
|
if line.starts_with('>') && line.contains('@') {
|
|
continue;
|
|
}
|
|
// Skip npm lifecycle scripts
|
|
if line.trim_start().starts_with("npm WARN") {
|
|
continue;
|
|
}
|
|
if line.trim_start().starts_with("npm notice") {
|
|
continue;
|
|
}
|
|
// Skip progress indicators
|
|
if line.contains("⸩") || line.contains("⸨") || line.contains("...") && line.len() < 10 {
|
|
continue;
|
|
}
|
|
// Skip empty lines
|
|
if line.trim().is_empty() {
|
|
continue;
|
|
}
|
|
|
|
result.push(line.to_string());
|
|
}
|
|
|
|
if result.is_empty() {
|
|
"ok".to_string()
|
|
} else {
|
|
result.join("\n")
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_filter_npm_output() {
|
|
let output = r#"
|
|
> project@1.0.0 build
|
|
> next build
|
|
|
|
npm WARN deprecated inflight@1.0.6: This module is not supported
|
|
npm notice
|
|
|
|
Creating an optimized production build...
|
|
✓ Build completed
|
|
"#;
|
|
let result = filter_npm_output(output);
|
|
assert!(!result.contains("npm WARN"));
|
|
assert!(!result.contains("npm notice"));
|
|
assert!(!result.contains("> project@"));
|
|
assert!(result.contains("Build completed"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_npm_subcommand_routing() {
|
|
// Uses the shared NPM_SUBCOMMANDS constant — no drift between prod and test
|
|
fn needs_run_injection(args: &[&str]) -> bool {
|
|
let first = args.first().copied();
|
|
let is_run_explicit = first == Some("run");
|
|
let is_subcommand = first
|
|
.map(|a| NPM_SUBCOMMANDS.contains(&a) || a.starts_with('-'))
|
|
.unwrap_or(false);
|
|
!is_run_explicit && !is_subcommand
|
|
}
|
|
|
|
// Known subcommands should NOT get "run" injected
|
|
for subcmd in NPM_SUBCOMMANDS {
|
|
assert!(
|
|
!needs_run_injection(&[subcmd]),
|
|
"'npm {}' should NOT inject 'run'",
|
|
subcmd
|
|
);
|
|
}
|
|
|
|
// Script names SHOULD get "run" injected
|
|
for script in &["build", "dev", "lint", "typecheck", "deploy"] {
|
|
assert!(
|
|
needs_run_injection(&[script]),
|
|
"'npm {}' SHOULD inject 'run'",
|
|
script
|
|
);
|
|
}
|
|
|
|
// Flags should NOT get "run" injected
|
|
assert!(!needs_run_injection(&["--version"]));
|
|
assert!(!needs_run_injection(&["-h"]));
|
|
|
|
// Explicit "run" should NOT inject another "run"
|
|
assert!(!needs_run_injection(&["run", "build"]));
|
|
}
|
|
|
|
#[test]
|
|
fn test_filter_npm_output_empty() {
|
|
let output = "\n\n\n";
|
|
let result = filter_npm_output(output);
|
|
assert_eq!(result, "ok");
|
|
}
|
|
}
|