use std::path::Path;
use std::process::Command;
use super::ToolContext;
use super::output_util;
use super::test_result_cache::{
TestCacheKey, TestCacheKind, fingerprint_npm_package_dir, npm_test_args_fingerprint,
store_cached, try_get_cached, wrap_cache_hit,
};
use super::tool_param_types::{NpmInstallArgs, NpmRunArgs, NpxRunArgs, TscCheckArgs};
const MAX_OUTPUT_LINES: usize = 800;
pub fn npm_install(args_json: &str, workspace_root: &Path, max_output_len: usize) -> String {
let v = match crate::cm_tools::tools::parse_args_json(args_json) {
Ok(v) => v,
Err(e) => return e,
};
let args: NpmInstallArgs = match serde_json::from_value(v) {
Ok(a) => a,
Err(e) => return format!("参数 JSON 与 npm_install 形状不一致: {e}"),
};
let subdir = normalize_npm_subdir(args.subdir.as_deref());
let ci = args.ci;
let production = args.production;
let dir = match resolve_npm_package_dir(workspace_root, subdir) {
Ok(d) => d,
Err(e) => return format!("npm install: {e}"),
};
let mut cmd = Command::new("npm");
if ci {
cmd.arg("ci");
} else {
cmd.arg("install");
}
if production {
cmd.arg("--production");
}
cmd.current_dir(&dir);
let title = if ci { "npm ci" } else { "npm install" };
run_and_format(cmd, max_output_len, title)
}
pub fn npm_run(
args_json: &str,
workspace_root: &Path,
max_output_len: usize,
ctx: &ToolContext<'_>,
) -> String {
let v = match crate::cm_tools::tools::parse_args_json(args_json) {
Ok(v) => v,
Err(e) => return e,
};
let args: NpmRunArgs = match serde_json::from_value(v) {
Ok(a) => a,
Err(e) => return format!("参数 JSON 与 npm_run 形状不一致: {e}"),
};
let subdir = normalize_npm_subdir(args.subdir.as_deref());
if let Err(e) = validate_npm_script(&args.script) {
return e;
}
let script = args.script.trim();
let dir = match resolve_npm_package_dir(workspace_root, subdir) {
Ok(d) => d,
Err(_) => {
return format!("npm run {}: 跳过({}/package.json 不存在)", script, subdir);
}
};
let extra_args = args.args;
let run = || {
let mut cmd = Command::new("npm");
cmd.arg("run").arg(script);
if !extra_args.is_empty() {
cmd.arg("--");
for a in &extra_args {
cmd.arg(a);
}
}
cmd.current_dir(&dir);
run_and_format(cmd, max_output_len, &format!("npm run {}", script))
};
if ctx.test_result_cache_enabled
&& script == "test"
&& let Some(inputs_fp) = fingerprint_npm_package_dir(workspace_root, subdir)
{
let args_fp = npm_test_args_fingerprint(subdir, script, &extra_args);
let key = TestCacheKey {
workspace_root: workspace_root.to_path_buf(),
kind: TestCacheKind::NpmTest {
package_subdir: subdir.to_string(),
},
args_fingerprint: args_fp,
inputs_fingerprint: inputs_fp.clone(),
};
if let Some(hit) = try_get_cached(
ctx.test_result_cache_enabled,
ctx.test_result_cache_max_entries,
&key,
) {
return wrap_cache_hit(&inputs_fp, &hit);
}
let out = run();
store_cached(
ctx.test_result_cache_enabled,
ctx.test_result_cache_max_entries,
key,
out.clone(),
);
return out;
}
run()
}
pub fn npx_run(args_json: &str, workspace_root: &Path, max_output_len: usize) -> String {
let v = match crate::cm_tools::tools::parse_args_json(args_json) {
Ok(v) => v,
Err(e) => return e,
};
let args: NpxRunArgs = match serde_json::from_value(v) {
Ok(a) => a,
Err(e) => return format!("参数 JSON 与 npx_run 形状不一致: {e}"),
};
let subdir = args
.subdir
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.unwrap_or(".");
let package = args.package.trim();
if package.is_empty() {
return "错误:缺少 package 参数".to_string();
}
if package.contains("..") || package.starts_with('/') {
return "错误:package 参数不安全".to_string();
}
if let Some(e) = check_subdir(subdir) {
return e;
}
let dir = workspace_root.join(subdir);
if !dir.join("package.json").is_file() {
return format!("npx {}: 跳过({}/package.json 不存在)", package, subdir);
}
let extra_args: Vec<&str> = args.args.iter().map(|s| s.as_str()).collect();
let mut cmd = Command::new("npx");
cmd.arg("--yes").arg(package);
for a in &extra_args {
cmd.arg(a);
}
cmd.current_dir(&dir);
run_and_format(cmd, max_output_len, &format!("npx {}", package))
}
pub fn tsc_check(args_json: &str, workspace_root: &Path, max_output_len: usize) -> String {
let v = match crate::cm_tools::tools::parse_args_json(args_json) {
Ok(v) => v,
Err(e) => return e,
};
let args: TscCheckArgs = match serde_json::from_value(v) {
Ok(a) => a,
Err(e) => return format!("参数 JSON 与 tsc_check 形状不一致: {e}"),
};
let subdir = args
.subdir
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.unwrap_or(".");
let project = args
.project
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty());
let strict = args.strict;
if let Some(e) = check_subdir(subdir) {
return e;
}
let dir = workspace_root.join(subdir);
if !dir.join("package.json").is_file() && !dir.join("tsconfig.json").is_file() {
return format!(
"tsc: 跳过({} 下未找到 package.json 或 tsconfig.json)",
subdir
);
}
let mut cmd = Command::new("npx");
cmd.arg("tsc");
if let Some(p) = project {
if p.contains("..") || p.starts_with('/') {
return "错误:project 参数不安全".to_string();
}
cmd.arg("-p").arg(p);
} else {
cmd.arg("-b");
}
cmd.arg("--noEmit");
if strict {
cmd.arg("--strict");
}
cmd.current_dir(&dir);
run_and_format(cmd, max_output_len, "npx tsc --noEmit")
}
fn normalize_npm_subdir(raw: Option<&str>) -> &str {
raw.map(str::trim).filter(|s| !s.is_empty()).unwrap_or(".")
}
fn resolve_npm_package_dir(
workspace_root: &Path,
subdir: &str,
) -> Result<std::path::PathBuf, String> {
if let Some(e) = check_subdir(subdir) {
return Err(e);
}
let dir = workspace_root.join(subdir);
if !dir.join("package.json").is_file() {
return Err(format!("npm: 跳过({}/package.json 不存在)", subdir));
}
Ok(dir)
}
fn validate_npm_script(script: &str) -> Result<(), String> {
let script = script.trim();
if script.is_empty() {
return Err("错误:缺少 script 参数".to_string());
}
if script.contains(char::is_whitespace) {
return Err("错误:script 参数无效(不能包含空白字符)".to_string());
}
Ok(())
}
fn check_subdir(subdir: &str) -> Option<String> {
if subdir.starts_with('/') || subdir.contains("..") {
Some("错误:subdir 必须是工作区内相对路径,且不能包含 ..".to_string())
} else {
None
}
}
fn run_and_format(cmd: Command, max_output_len: usize, title: &str) -> String {
output_util::run_command_output_formatted(
cmd,
title,
max_output_len,
MAX_OUTPUT_LINES,
output_util::ProcessOutputMerge::ConcatStdoutStderr,
output_util::CommandSpawnErrorStyle::CannotStartCommand,
)
}