use super::*;
pub(super) fn plan_context_artifacts(
config: &Config,
diffs: &[Diff],
checks: &[CheckResult],
) -> Vec<ContextArtifactDecision> {
let mut decisions = Vec::new();
if config.profile.has_tsconfig {
decisions.push(plan_tsc_trace_artifact(config, diffs, checks));
}
if has_tauri_context(config) {
decisions.push(plan_tauri_info_artifact(config, diffs));
}
decisions
}
pub(super) fn plan_tsc_trace_artifact(
config: &Config,
diffs: &[Diff],
checks: &[CheckResult],
) -> ContextArtifactDecision {
if !config.is_fast_remote_only_standard() {
return ContextArtifactDecision {
key: "tsc_trace",
path: "30_context/tsc-trace.log",
generated: true,
recommended: false,
reason: "generated by default for this run mode".to_string(),
};
}
let mut reasons = Vec::new();
if let Some(reason) = detect_typescript_resolution_signal(checks) {
reasons.push(reason);
}
let changed_resolution_files = find_ts_resolution_related_changes(diffs);
if !changed_resolution_files.is_empty() {
reasons.push(format!(
"resolution-related files changed ({})",
changed_resolution_files.join(", ")
));
}
if reasons.is_empty() {
ContextArtifactDecision {
key: "tsc_trace",
path: "30_context/tsc-trace.log",
generated: false,
recommended: false,
reason: "skipped by default in fast remote-only runs; no TypeScript resolution signals detected"
.to_string(),
}
} else {
ContextArtifactDecision {
key: "tsc_trace",
path: "30_context/tsc-trace.log",
generated: false,
recommended: true,
reason: format!(
"skipped by default in fast remote-only runs; generate when investigating because {}",
reasons.join("; ")
),
}
}
}
pub(super) fn detect_typescript_resolution_signal(checks: &[CheckResult]) -> Option<String> {
let typescript = checks
.iter()
.find(|check| check.name.eq_ignore_ascii_case("TypeScript"))?;
if !typescript.is_failure() {
return None;
}
let output = typescript.output.to_lowercase();
const RESOLUTION_NEEDLES: &[&str] = &[
"cannot find module",
"cannot resolve module",
"module resolution",
"did you mean to set the module resolution option",
"paths option",
"baseurl",
];
if RESOLUTION_NEEDLES
.iter()
.any(|needle| output.contains(needle))
{
Some("TypeScript check failed with module-resolution-style errors".to_string())
} else {
None
}
}
pub(super) fn find_ts_resolution_related_changes(diffs: &[Diff]) -> Vec<String> {
let mut matches = Vec::new();
for file in diffs.iter().flat_map(|diff| &diff.files) {
let path = file.path.as_str();
let file_name = Path::new(path)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(path);
let is_tsconfig = file_name.starts_with("tsconfig") && file_name.ends_with(".json");
let is_resolution_config = matches!(
file_name,
"package.json"
| "pnpm-lock.yaml"
| "package-lock.json"
| "yarn.lock"
| "vite.config.ts"
| "vite.config.js"
| "vite.config.mjs"
| "vite.config.cjs"
| "webpack.config.js"
| "webpack.config.ts"
| "tsup.config.ts"
| "tsup.config.js"
);
if is_tsconfig || is_resolution_config {
matches.push(path.to_string());
}
}
matches.sort();
matches.dedup();
matches.truncate(3);
matches
}
pub(super) fn has_tauri_context(config: &Config) -> bool {
if !config.profile.has_cargo {
return false;
}
is_tauri_project(&config.repo_root)
}
pub(super) fn is_tauri_project(repo_root: &Path) -> bool {
let conf_files = [
repo_root.join("tauri.conf.json"),
repo_root.join("tauri.conf.toml"),
repo_root.join("src-tauri").join("tauri.conf.json"),
repo_root.join("src-tauri").join("tauri.conf.toml"),
];
if conf_files.iter().any(|p| p.exists()) {
return true;
}
if repo_root.join("src-tauri").join("Cargo.toml").exists() {
return true;
}
let cargo_toml_path = repo_root.join("Cargo.toml");
if let Ok(content) = fs::read_to_string(&cargo_toml_path)
&& content.contains("tauri")
{
return true;
}
false
}
pub(super) fn plan_tauri_info_artifact(config: &Config, diffs: &[Diff]) -> ContextArtifactDecision {
if !config.is_fast_remote_only_standard() {
return ContextArtifactDecision {
key: "tauri_info",
path: "30_context/tauri-info.log",
generated: true,
recommended: false,
reason: "generated by default for this run mode".to_string(),
};
}
let changed_tauri_files = find_tauri_diagnostic_changes(diffs);
if changed_tauri_files.is_empty() {
ContextArtifactDecision {
key: "tauri_info",
path: "30_context/tauri-info.log",
generated: false,
recommended: false,
reason:
"skipped by default in fast remote-only runs; no Tauri config/build signals detected"
.to_string(),
}
} else {
ContextArtifactDecision {
key: "tauri_info",
path: "30_context/tauri-info.log",
generated: false,
recommended: true,
reason: format!(
"skipped by default in fast remote-only runs; generate when investigating because Tauri config/build files changed ({})",
changed_tauri_files.join(", ")
),
}
}
}
pub(super) fn find_tauri_diagnostic_changes(diffs: &[Diff]) -> Vec<String> {
let mut matches = Vec::new();
for file in diffs.iter().flat_map(|diff| &diff.files) {
let path = file.path.as_str();
let file_name = Path::new(path)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(path);
let is_tauri_config = matches!(
file_name,
"tauri.conf.json"
| "tauri.linux.conf.json"
| "tauri.macos.conf.json"
| "tauri.windows.conf.json"
| "build.rs"
| "Cargo.toml"
);
let is_tauri_capability = path.starts_with("src-tauri/capabilities/");
if is_tauri_config || is_tauri_capability {
matches.push(path.to_string());
}
}
matches.sort();
matches.dedup();
matches.truncate(3);
matches
}
pub(super) struct ContextCmd {
pub(super) label: String,
pub(super) cmd: String,
pub(super) args: Vec<String>,
pub(super) cwd: PathBuf,
pub(super) out_dir: PathBuf,
pub(super) out_file: String,
}
fn tauri_info_cmd(repo_root: &Path, has_pnpm: bool) -> Option<(String, Vec<String>)> {
if let Some(bin) = crate::checks::local_js_bin("tauri", repo_root) {
Some((bin.to_string_lossy().into_owned(), vec!["info".into()]))
} else if has_pnpm {
Some(("pnpm".to_string(), vec!["tauri".into(), "info".into()]))
} else {
None
}
}
fn js_exec_cmd(
tool: &str,
tool_args: Vec<String>,
repo_root: &Path,
has_pnpm: bool,
) -> Option<(String, Vec<String>)> {
if let Some(bin) = crate::checks::local_js_bin(tool, repo_root) {
Some((bin.to_string_lossy().into_owned(), tool_args))
} else if has_pnpm {
let mut args = vec!["exec".to_string(), tool.to_string()];
args.extend(tool_args);
Some(("pnpm".to_string(), args))
} else {
None
}
}
pub(super) fn generate_context_artifacts(
config: &Config,
checks: &[CheckResult],
context_dir: &Path,
emit_human_stdout: bool,
decisions: &[ContextArtifactDecision],
) -> Result<Vec<ContextCommandTiming>> {
let ctx = context_dir.to_path_buf();
let repo_root = config.repo_root.clone();
let has_pnpm = which::which("pnpm").is_ok();
let mut cmds: Vec<ContextCmd> = Vec::new();
let tauri_info = decisions
.iter()
.find(|decision| decision.key == "tauri_info");
if config.profile.has_cargo {
let cwd = config
.profile
.cargo_root
.as_ref()
.unwrap_or(&config.repo_root)
.clone();
cmds.push(ContextCmd {
label: "cargo tree".into(),
cmd: "cargo".into(),
args: vec!["tree".into(), "--depth".into(), "2".into()],
cwd: cwd.clone(),
out_dir: ctx.clone(),
out_file: "cargo-tree.txt".into(),
});
cmds.push(ContextCmd {
label: "cargo sbom".into(),
cmd: "cargo".into(),
args: vec!["tree".into(), "--format".into(), "{p} {l}".into()],
cwd: cwd.clone(),
out_dir: ctx.clone(),
out_file: "cargo-sbom.txt".into(),
});
if let Some(cargo_root) = &config.profile.cargo_root {
let tauri_dir = if cargo_root.ends_with("src-tauri") {
cargo_root.clone()
} else {
config.repo_root.join("src-tauri")
};
if is_tauri_project(&config.repo_root) && tauri_dir.exists() {
if tauri_info.is_none_or(|decision| decision.generated) {
if let Some((cmd, args)) = tauri_info_cmd(&repo_root, has_pnpm) {
cmds.push(ContextCmd {
label: "tauri info".into(),
cmd,
args,
cwd: repo_root.clone(),
out_dir: ctx.clone(),
out_file: "tauri-info.log".into(),
});
}
} else if let Some(decision) = tauri_info
&& emit_human_stdout
{
use colored::Colorize;
let marker = if decision.recommended {
"ℹ".yellow()
} else {
"ℹ".blue()
};
println!(" {} tauri-info.log: skipped ({})", marker, decision.reason);
}
}
}
}
let tsc_trace = decisions
.iter()
.find(|decision| decision.key == "tsc_trace");
if config.profile.has_tsconfig && tsc_trace.is_none_or(|decision| decision.generated) {
if let Some((cmd, args)) = js_exec_cmd(
"tsc",
vec!["--noEmit".into(), "--traceResolution".into()],
&repo_root,
has_pnpm,
) {
cmds.push(ContextCmd {
label: "tsc trace".into(),
cmd,
args,
cwd: repo_root.clone(),
out_dir: ctx.clone(),
out_file: "tsc-trace.log".into(),
});
}
} else if let Some(decision) = tsc_trace
&& emit_human_stdout
{
use colored::Colorize;
let marker = if decision.recommended {
"ℹ".yellow()
} else {
"ℹ".blue()
};
println!(" {} tsc-trace.log: skipped ({})", marker, decision.reason);
}
if config.profile.has_package_json {
let (sbom_cmd, sbom_args) = if has_pnpm {
("pnpm", vec!["list".into(), "--all".into()])
} else {
("npm", vec!["ls".into(), "--all".into()])
};
cmds.push(ContextCmd {
label: "npm sbom".into(),
cmd: sbom_cmd.into(),
args: sbom_args,
cwd: repo_root.clone(),
out_dir: ctx.clone(),
out_file: "npm-sbom.txt".into(),
});
let checks_ran_eslint = checks
.iter()
.any(|c| c.name.to_lowercase().contains("eslint"));
let checks_ran_stylelint = checks
.iter()
.any(|c| c.name.to_lowercase().contains("stylelint"));
let checks_ran_vitest = checks
.iter()
.any(|c| c.name.to_lowercase().contains("vitest"));
if !checks_ran_eslint {
if let Some((cmd, args)) = js_exec_cmd(
"eslint",
vec![
".".into(),
"--ext".into(),
".ts,.tsx,.js,.jsx".into(),
"-f".into(),
"json".into(),
],
&repo_root,
has_pnpm,
) {
cmds.push(ContextCmd {
label: "eslint json".into(),
cmd,
args,
cwd: repo_root.clone(),
out_dir: ctx.clone(),
out_file: "eslint.json".into(),
});
}
}
if repo_root.join("node_modules/.bin/stylelint").exists() && !checks_ran_stylelint {
cmds.push(ContextCmd {
label: "stylelint json".into(),
cmd: "sh".into(),
args: vec![
"-c".into(),
"pnpm exec stylelint 'src/**/*.css' -f json --allow-empty-input".into(),
],
cwd: repo_root.clone(),
out_dir: ctx.clone(),
out_file: "stylelint.json".into(),
});
} else if checks_ran_stylelint && emit_human_stdout {
use colored::Colorize;
println!(
" {} stylelint.json: skipped (stylelint check already captured this signal)",
"ℹ".blue()
);
}
if !checks_ran_vitest && emit_human_stdout {
use colored::Colorize;
println!(
" {} vitest-report.json: skipped (use checks for test results)",
"ℹ".blue()
);
}
if repo_root.join("node_modules/.bin/esbuild").exists() {
let entry = if repo_root.join("src/main.tsx").exists() {
Some("src/main.tsx")
} else if repo_root.join("src/main.ts").exists() {
Some("src/main.ts")
} else {
None
};
if let Some(entry) = entry {
let meta_path = ctx.join("esbuild-meta.json");
let meta_arg = format!("--metafile={}", meta_path.display());
if let Some((cmd, args)) = js_exec_cmd(
"esbuild",
vec![
entry.into(),
"--bundle".into(),
meta_arg,
"--log-level=error".into(),
],
&repo_root,
has_pnpm,
) {
cmds.push(ContextCmd {
label: "esbuild meta".into(),
cmd,
args,
cwd: repo_root.clone(),
out_dir: ctx.clone(),
out_file: String::new(),
});
}
}
}
}
if cmds.is_empty() {
return Ok(Vec::new());
}
Ok(run_context_cmds_parallel(
&cmds,
CONTEXT_GEN_TIMEOUT_SECS,
emit_human_stdout,
))
}
pub(super) fn run_context_cmds_parallel(
cmds: &[ContextCmd],
timeout_secs: u64,
emit: bool,
) -> Vec<ContextCommandTiming> {
use std::time::Duration;
struct RunningCmd {
label: String,
child: std::process::Child,
started_at: Instant,
deadline: Instant,
out_dir: PathBuf,
out_file: String,
stdout_path: PathBuf,
stderr_path: PathBuf,
done: bool,
}
let mut running: Vec<RunningCmd> = Vec::new();
let mut timings = Vec::new();
for cmd in cmds {
let args: Vec<&str> = cmd.args.iter().map(|s| s.as_str()).collect();
let idx = running.len();
let stdout_path = cmd.out_dir.join(format!(".context-cmd-{idx}.stdout.tmp"));
let stderr_path = cmd.out_dir.join(format!(".context-cmd-{idx}.stderr.tmp"));
let stdout_file = match File::create(&stdout_path) {
Ok(file) => file,
Err(_) => continue,
};
let stderr_file = match File::create(&stderr_path) {
Ok(file) => file,
Err(_) => {
let _ = fs::remove_file(&stdout_path);
continue;
}
};
match Command::new(&cmd.cmd)
.args(&args)
.current_dir(&cmd.cwd)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::from(stdout_file))
.stderr(std::process::Stdio::from(stderr_file))
.spawn()
{
Ok(child) => {
let started_at = Instant::now();
running.push(RunningCmd {
label: cmd.label.clone(),
child,
started_at,
deadline: started_at + Duration::from_secs(timeout_secs),
out_dir: cmd.out_dir.clone(),
out_file: cmd.out_file.clone(),
stdout_path,
stderr_path,
done: false,
});
}
Err(_) => {
let _ = fs::remove_file(&stdout_path);
let _ = fs::remove_file(&stderr_path);
timings.push(ContextCommandTiming {
label: cmd.label.clone(),
artifact: None,
status: "spawn_failed",
duration_secs: 0.0,
});
}
}
}
let poll_interval = Duration::from_millis(200);
while running.iter().any(|r| !r.done) {
for r in running.iter_mut().filter(|r| !r.done) {
match r.child.try_wait() {
Ok(Some(exit)) => {
r.done = true;
collect_cmd_output(&r.stdout_path, &r.stderr_path, &r.out_dir, &r.out_file);
timings.push(ContextCommandTiming {
label: r.label.clone(),
artifact: (!r.out_file.is_empty()).then(|| {
Path::new("30_context")
.join(&r.out_file)
.display()
.to_string()
}),
status: if exit.success() {
"completed"
} else {
"failed"
},
duration_secs: r.started_at.elapsed().as_secs_f32(),
});
}
Ok(None) => {
if Instant::now() >= r.deadline {
let _ = r.child.kill();
let _ = r.child.wait();
r.done = true;
timings.push(ContextCommandTiming {
label: r.label.clone(),
artifact: (!r.out_file.is_empty()).then(|| {
Path::new("30_context")
.join(&r.out_file)
.display()
.to_string()
}),
status: "timed_out",
duration_secs: r.started_at.elapsed().as_secs_f32(),
});
if emit {
use colored::Colorize;
eprintln!(
" {} {}: killed (>{}s timeout)",
"â—‹".dimmed(),
r.label,
timeout_secs,
);
}
}
}
Err(_) => {
r.done = true;
timings.push(ContextCommandTiming {
label: r.label.clone(),
artifact: (!r.out_file.is_empty()).then(|| {
Path::new("30_context")
.join(&r.out_file)
.display()
.to_string()
}),
status: "error",
duration_secs: r.started_at.elapsed().as_secs_f32(),
});
}
}
}
if running.iter().any(|r| !r.done) {
std::thread::sleep(poll_interval);
}
}
timings.sort_by(|a, b| b.duration_secs.total_cmp(&a.duration_secs));
timings
}
pub(super) fn collect_cmd_output(
stdout_path: &Path,
stderr_path: &Path,
out_dir: &Path,
out_file: &str,
) {
let stdout = fs::read_to_string(stdout_path).unwrap_or_default();
let stderr = fs::read_to_string(stderr_path).unwrap_or_default();
let _ = fs::remove_file(stdout_path);
let _ = fs::remove_file(stderr_path);
if out_file.is_empty() {
return;
}
let combined = format!("{}\n{}", stdout, stderr);
let content = if combined.len() > MAX_TSC_TRACE_BYTES {
let truncated = truncate_on_char_boundary(&combined, MAX_TSC_TRACE_BYTES);
format!(
"{}\n\n... (truncated, {} total bytes)",
truncated,
combined.len()
)
} else {
combined
};
if out_file.ends_with(".json") {
if let Some(json) = extract_valid_json(&stdout, &content)
&& let Err(e) = fs::write(out_dir.join(out_file), json)
{
eprintln!(" warning: failed to write artifact {out_file}: {e}");
}
} else if let Err(e) = fs::write(out_dir.join(out_file), &content) {
eprintln!(" warning: failed to write artifact {out_file}: {e}");
}
}
pub(super) fn extract_valid_json(stdout: &str, combined: &str) -> Option<String> {
[stdout.trim(), combined.trim()]
.into_iter()
.find(|candidate| {
!candidate.is_empty()
&& (candidate.starts_with('[') || candidate.starts_with('{'))
&& serde_json::from_str::<serde_json::Value>(candidate).is_ok()
})
.map(str::to_owned)
}
pub(super) fn build_regression_patch_text(patch_texts: &[String]) -> Option<String> {
if patch_texts.is_empty() {
return None;
}
let joined = patch_texts.join("\n");
if joined.len() <= MAX_PATCH_TEXT_BYTES {
return Some(joined);
}
let mut truncated = truncate_on_char_boundary(&joined, MAX_PATCH_TEXT_BYTES).to_string();
truncated
.push_str("\n\n# [prview] Patch text truncated (>2 MB), some findings may be incomplete\n");
Some(truncated)
}
pub(super) fn truncate_on_char_boundary(input: &str, max_bytes: usize) -> &str {
if input.len() <= max_bytes {
return input;
}
let mut end = max_bytes;
while end > 0 && !input.is_char_boundary(end) {
end -= 1;
}
&input[..end]
}
#[cfg(test)]
mod tests {
use super::{js_exec_cmd, tauri_info_cmd};
use std::fs;
#[test]
fn tauri_info_prefers_local_binary_over_npx() {
let tmp = tempfile::tempdir().unwrap();
let bin_dir = tmp.path().join("node_modules/.bin");
fs::create_dir_all(&bin_dir).unwrap();
fs::write(bin_dir.join("tauri"), "#!/bin/sh\n").unwrap();
let (cmd, args) = tauri_info_cmd(tmp.path(), true).expect("local binary resolves");
assert!(
cmd.ends_with("node_modules/.bin/tauri"),
"expected a direct local exec, got {cmd}"
);
assert_eq!(args, vec!["info".to_string()]);
assert!(
!args.iter().any(|a| a == "--no-install"),
"a resolved binary must never carry npx flags"
);
}
#[test]
fn tauri_info_falls_back_to_pnpm_without_local_binary() {
let tmp = tempfile::tempdir().unwrap();
let (cmd, args) = tauri_info_cmd(tmp.path(), true).expect("pnpm fallback");
assert_eq!(cmd, "pnpm");
assert_eq!(args, vec!["tauri".to_string(), "info".to_string()]);
}
#[test]
fn tauri_info_skips_when_no_local_binary_and_no_pnpm() {
let tmp = tempfile::tempdir().unwrap();
assert!(tauri_info_cmd(tmp.path(), false).is_none());
}
#[test]
fn js_exec_prefers_local_binary_over_npx() {
for tool in ["tsc", "eslint", "esbuild"] {
let tmp = tempfile::tempdir().unwrap();
let bin_dir = tmp.path().join("node_modules/.bin");
fs::create_dir_all(&bin_dir).unwrap();
fs::write(bin_dir.join(tool), "#!/bin/sh\n").unwrap();
let (cmd, args) =
js_exec_cmd(tool, vec!["--flag".into()], tmp.path(), true).expect("local resolves");
assert!(
cmd.ends_with(&format!("node_modules/.bin/{tool}")),
"expected a direct local exec for {tool}, got {cmd}"
);
assert_eq!(args, vec!["--flag".to_string()], "tool={tool}");
assert!(
!args.iter().any(|a| a == "--no-install" || a == "exec"),
"a resolved binary must never carry launcher flags (tool={tool})"
);
}
}
#[test]
fn js_exec_falls_back_to_pnpm_exec_without_local_binary() {
let tmp = tempfile::tempdir().unwrap();
let (cmd, args) =
js_exec_cmd("eslint", vec!["--flag".into()], tmp.path(), true).expect("pnpm fallback");
assert_eq!(cmd, "pnpm");
assert_eq!(
args,
vec![
"exec".to_string(),
"eslint".to_string(),
"--flag".to_string()
]
);
assert!(
!args.iter().any(|a| a == "--no-install"),
"pnpm exec fallback must never carry npx flags"
);
}
#[test]
fn js_exec_skips_when_no_local_binary_and_no_pnpm() {
let tmp = tempfile::tempdir().unwrap();
assert!(js_exec_cmd("tsc", vec!["--flag".into()], tmp.path(), false).is_none());
}
}