@wrongstack/tools 0.250.0 → 0.255.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit.js +590 -48
- package/dist/audit.js.map +1 -1
- package/dist/{background-indexer-DwJsyAB0.d.ts → background-indexer-CJ5JiV5i.d.ts} +0 -8
- package/dist/bash.js +133 -23
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +710 -517
- package/dist/builtin.js.map +1 -1
- package/dist/codebase-index/index.d.ts +2 -2
- package/dist/codebase-index/index.js +16 -0
- package/dist/codebase-index/index.js.map +1 -1
- package/dist/codebase-index/worker.js +11 -6
- package/dist/codebase-index/worker.js.map +1 -1
- package/dist/exec.js +115 -5
- package/dist/exec.js.map +1 -1
- package/dist/format.js +590 -48
- package/dist/format.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +317 -124
- package/dist/index.js.map +1 -1
- package/dist/install.js +590 -48
- package/dist/install.js.map +1 -1
- package/dist/lint.js +589 -47
- package/dist/lint.js.map +1 -1
- package/dist/pack.js +710 -517
- package/dist/pack.js.map +1 -1
- package/dist/test.d.ts +1 -0
- package/dist/test.js +604 -55
- package/dist/test.js.map +1 -1
- package/dist/typecheck.js +590 -48
- package/dist/typecheck.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -2,11 +2,11 @@ import * as fs4 from 'node:fs/promises';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { resolve, sep, dirname, join } from 'node:path';
|
|
4
4
|
import * as Core from '@wrongstack/core';
|
|
5
|
-
import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, emptyTaskFile, formatTaskList, formatPlan, recordPackageAction, detectPackageEcosystem, mutateTasks, emptyPlan, computeTaskItemProgress, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
5
|
+
import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, emptyTaskFile, formatTaskList, formatPlan, recordPackageAction, detectPackageEcosystem, mutateTasks, emptyPlan, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
6
6
|
import { spawn, execFileSync, spawnSync } from 'node:child_process';
|
|
7
7
|
import * as os from 'node:os';
|
|
8
8
|
import * as fs7 from 'node:fs';
|
|
9
|
-
import { statSync,
|
|
9
|
+
import { statSync, mkdirSync, createWriteStream, writeFileSync } from 'node:fs';
|
|
10
10
|
import * as dns from 'node:dns/promises';
|
|
11
11
|
import * as net from 'node:net';
|
|
12
12
|
import { Agent } from 'undici';
|
|
@@ -18,14 +18,14 @@ import { randomUUID } from 'node:crypto';
|
|
|
18
18
|
|
|
19
19
|
// src/read.ts
|
|
20
20
|
async function detectPackageManager(cwd) {
|
|
21
|
-
const { stat:
|
|
21
|
+
const { stat: stat11 } = await import('node:fs/promises');
|
|
22
22
|
try {
|
|
23
|
-
await
|
|
23
|
+
await stat11(`${cwd}/pnpm-lock.yaml`);
|
|
24
24
|
return "pnpm";
|
|
25
25
|
} catch {
|
|
26
26
|
}
|
|
27
27
|
try {
|
|
28
|
-
await
|
|
28
|
+
await stat11(`${cwd}/yarn.lock`);
|
|
29
29
|
return "yarn";
|
|
30
30
|
} catch {
|
|
31
31
|
}
|
|
@@ -194,9 +194,9 @@ var readTool = {
|
|
|
194
194
|
async execute(input, ctx) {
|
|
195
195
|
if (!input?.path) throw new Error("read: path is required");
|
|
196
196
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
197
|
-
let
|
|
197
|
+
let stat11;
|
|
198
198
|
try {
|
|
199
|
-
|
|
199
|
+
stat11 = await fs4.stat(absPath);
|
|
200
200
|
} catch (err) {
|
|
201
201
|
const code = err.code;
|
|
202
202
|
if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
|
|
@@ -204,9 +204,9 @@ var readTool = {
|
|
|
204
204
|
`read: failed to stat "${input.path}": ${err instanceof Error ? err.message : String(err)}`
|
|
205
205
|
);
|
|
206
206
|
}
|
|
207
|
-
if (!
|
|
208
|
-
if (
|
|
209
|
-
throw new Error(`read: file too large (${
|
|
207
|
+
if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
|
|
208
|
+
if (stat11.size > MAX_BYTES) {
|
|
209
|
+
throw new Error(`read: file too large (${stat11.size} bytes, limit ${MAX_BYTES})`);
|
|
210
210
|
}
|
|
211
211
|
const buf = await fs4.readFile(absPath);
|
|
212
212
|
if (isBinaryBuffer(buf)) {
|
|
@@ -218,14 +218,14 @@ var readTool = {
|
|
|
218
218
|
const offset = Math.max(1, input.offset ?? 1);
|
|
219
219
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
220
220
|
if (limit === 0) {
|
|
221
|
-
ctx.recordRead(absPath,
|
|
221
|
+
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
222
222
|
return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
|
|
223
223
|
}
|
|
224
224
|
const slice = allLines.slice(offset - 1, offset - 1 + limit);
|
|
225
225
|
const truncated = offset - 1 + slice.length < total;
|
|
226
226
|
const width = String(offset + slice.length - 1).length;
|
|
227
227
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
228
|
-
ctx.recordRead(absPath,
|
|
228
|
+
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
229
229
|
return {
|
|
230
230
|
text: numbered,
|
|
231
231
|
total_lines: total,
|
|
@@ -264,12 +264,12 @@ var writeTool = {
|
|
|
264
264
|
let existed = false;
|
|
265
265
|
let prev = "";
|
|
266
266
|
try {
|
|
267
|
-
const
|
|
268
|
-
existed =
|
|
267
|
+
const stat12 = await fs4.stat(absPath);
|
|
268
|
+
existed = stat12.isFile();
|
|
269
269
|
if (existed) {
|
|
270
270
|
if (!ctx.hasRead(absPath)) {
|
|
271
271
|
prev = await fs4.readFile(absPath, "utf8");
|
|
272
|
-
ctx.recordRead(absPath,
|
|
272
|
+
ctx.recordRead(absPath, stat12.mtimeMs);
|
|
273
273
|
} else {
|
|
274
274
|
prev = await fs4.readFile(absPath, "utf8");
|
|
275
275
|
}
|
|
@@ -282,8 +282,8 @@ var writeTool = {
|
|
|
282
282
|
await atomicWrite(absPath, input.content);
|
|
283
283
|
const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
284
284
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
285
|
-
const
|
|
286
|
-
ctx.recordRead(absPath,
|
|
285
|
+
const stat11 = await fs4.stat(absPath);
|
|
286
|
+
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
287
287
|
ctx.session.recordFileChange({
|
|
288
288
|
path: absPath,
|
|
289
289
|
action: existed ? "modified" : "created",
|
|
@@ -323,13 +323,13 @@ var editTool = {
|
|
|
323
323
|
if (input.new_string === void 0) throw new Error("edit: new_string is required");
|
|
324
324
|
if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
|
|
325
325
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
326
|
-
const
|
|
326
|
+
const stat11 = await fs4.stat(absPath).catch((err) => {
|
|
327
327
|
if (err.code === "ENOENT") {
|
|
328
328
|
throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
|
|
329
329
|
}
|
|
330
330
|
throw err;
|
|
331
331
|
});
|
|
332
|
-
if (!
|
|
332
|
+
if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
|
|
333
333
|
if (!ctx.hasRead(absPath)) {
|
|
334
334
|
throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
|
|
335
335
|
}
|
|
@@ -524,8 +524,8 @@ var replaceTool = {
|
|
|
524
524
|
}
|
|
525
525
|
const rel = path.relative(realRoot, realPath);
|
|
526
526
|
if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
|
|
527
|
-
const
|
|
528
|
-
if (!
|
|
527
|
+
const stat11 = await fs4.stat(realPath).catch(() => null);
|
|
528
|
+
if (!stat11 || !stat11.isFile()) continue;
|
|
529
529
|
let content;
|
|
530
530
|
try {
|
|
531
531
|
const buf = await fs4.readFile(realPath);
|
|
@@ -550,7 +550,7 @@ var replaceTool = {
|
|
|
550
550
|
totalReplacements += count;
|
|
551
551
|
if (!dryRun) {
|
|
552
552
|
const newContent = toStyle(newContentLf, style);
|
|
553
|
-
await atomicWrite(realPath, newContent, { mode:
|
|
553
|
+
await atomicWrite(realPath, newContent, { mode: stat11.mode & 511 });
|
|
554
554
|
}
|
|
555
555
|
const diff = dryRun || matches.length > 0 ? unifiedDiff(content, toStyle(newContentLf, style), {
|
|
556
556
|
fromFile: absPath,
|
|
@@ -580,8 +580,8 @@ async function resolveFiles(filesInput, ctx, extraGlob) {
|
|
|
580
580
|
const resolved = [];
|
|
581
581
|
for (const p of parts) {
|
|
582
582
|
const absPath = safeResolve(p, ctx);
|
|
583
|
-
const
|
|
584
|
-
if (
|
|
583
|
+
const stat11 = await fs4.stat(absPath).catch(() => null);
|
|
584
|
+
if (stat11?.isFile()) {
|
|
585
585
|
resolved.push(absPath);
|
|
586
586
|
}
|
|
587
587
|
}
|
|
@@ -644,8 +644,8 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
644
644
|
if (DEFAULT_IGNORE.includes(e.name)) continue;
|
|
645
645
|
const full = path.join(dir, e.name);
|
|
646
646
|
try {
|
|
647
|
-
const
|
|
648
|
-
if (
|
|
647
|
+
const stat11 = await fs4.lstat(full);
|
|
648
|
+
if (stat11.isSymbolicLink()) continue;
|
|
649
649
|
} catch {
|
|
650
650
|
continue;
|
|
651
651
|
}
|
|
@@ -996,8 +996,8 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
996
996
|
if (globRe && !globRe.test(e.name) && !globRe.test(full)) continue;
|
|
997
997
|
if (globRe) globRe.lastIndex = 0;
|
|
998
998
|
try {
|
|
999
|
-
const
|
|
1000
|
-
if (
|
|
999
|
+
const stat11 = await fs4.stat(full);
|
|
1000
|
+
if (stat11.size > 1e6) continue;
|
|
1001
1001
|
const head = await fs4.readFile(full);
|
|
1002
1002
|
if (isBinaryBuffer(head)) continue;
|
|
1003
1003
|
const text = head.toString("utf8");
|
|
@@ -1037,6 +1037,107 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
1037
1037
|
used: "native"
|
|
1038
1038
|
};
|
|
1039
1039
|
}
|
|
1040
|
+
var SPOOL_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1041
|
+
var SPOOL_WRITE_HWM_BYTES = 4 * 1024 * 1024;
|
|
1042
|
+
var sweepStarted = false;
|
|
1043
|
+
function toolOutputDir() {
|
|
1044
|
+
return path.join(wstackGlobalRoot(), "tool-output");
|
|
1045
|
+
}
|
|
1046
|
+
function sweepOldSpoolFiles(dir) {
|
|
1047
|
+
if (sweepStarted) return;
|
|
1048
|
+
sweepStarted = true;
|
|
1049
|
+
void (async () => {
|
|
1050
|
+
try {
|
|
1051
|
+
const now = Date.now();
|
|
1052
|
+
for (const name of await fs4.readdir(dir)) {
|
|
1053
|
+
if (!name.endsWith(".log")) continue;
|
|
1054
|
+
const p = path.join(dir, name);
|
|
1055
|
+
try {
|
|
1056
|
+
const st = await fs4.stat(p);
|
|
1057
|
+
if (now - st.mtimeMs > SPOOL_RETENTION_MS) await fs4.unlink(p);
|
|
1058
|
+
} catch {
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
} catch {
|
|
1062
|
+
}
|
|
1063
|
+
})();
|
|
1064
|
+
}
|
|
1065
|
+
function spoolNote(info) {
|
|
1066
|
+
const dropped = info.droppedBytes > 0 ? `, ~${info.droppedBytes} bytes dropped under backpressure` : "";
|
|
1067
|
+
return `
|
|
1068
|
+
[output truncated \u2014 full ${info.bytes} bytes at ${info.path}${dropped}; read/grep that file selectively instead of re-running with more output]`;
|
|
1069
|
+
}
|
|
1070
|
+
function createOutputSpool(opts) {
|
|
1071
|
+
const threshold = opts.thresholdBytes ?? 32768;
|
|
1072
|
+
const safeTool = opts.tool.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 40) || "tool";
|
|
1073
|
+
let head = "";
|
|
1074
|
+
let headBytes = 0;
|
|
1075
|
+
let totalBytes = 0;
|
|
1076
|
+
let droppedBytes = 0;
|
|
1077
|
+
let stream = null;
|
|
1078
|
+
let filePath = null;
|
|
1079
|
+
let failed = false;
|
|
1080
|
+
let finalized = false;
|
|
1081
|
+
const open = () => {
|
|
1082
|
+
if (stream || failed) return;
|
|
1083
|
+
try {
|
|
1084
|
+
const dir = toolOutputDir();
|
|
1085
|
+
mkdirSync(dir, { recursive: true });
|
|
1086
|
+
sweepOldSpoolFiles(dir);
|
|
1087
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1088
|
+
const rand = Math.random().toString(36).slice(2, 6);
|
|
1089
|
+
filePath = path.join(dir, `${stamp}-${safeTool}-${rand}.log`);
|
|
1090
|
+
stream = createWriteStream(filePath, { flags: "w", encoding: "utf8" });
|
|
1091
|
+
stream.on("error", () => {
|
|
1092
|
+
failed = true;
|
|
1093
|
+
stream = null;
|
|
1094
|
+
filePath = null;
|
|
1095
|
+
});
|
|
1096
|
+
stream.write(head);
|
|
1097
|
+
} catch {
|
|
1098
|
+
failed = true;
|
|
1099
|
+
stream = null;
|
|
1100
|
+
filePath = null;
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
return {
|
|
1104
|
+
write(text) {
|
|
1105
|
+
if (finalized || !text) return;
|
|
1106
|
+
totalBytes += Buffer.byteLength(text, "utf8");
|
|
1107
|
+
if (!stream && !failed) {
|
|
1108
|
+
if (headBytes + text.length <= threshold) {
|
|
1109
|
+
head += text;
|
|
1110
|
+
headBytes += text.length;
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
head += text;
|
|
1114
|
+
open();
|
|
1115
|
+
head = "";
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
if (stream) {
|
|
1119
|
+
if (stream.writableLength > SPOOL_WRITE_HWM_BYTES) {
|
|
1120
|
+
droppedBytes += Buffer.byteLength(text, "utf8");
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
stream.write(text);
|
|
1124
|
+
}
|
|
1125
|
+
},
|
|
1126
|
+
finalize() {
|
|
1127
|
+
if (finalized) {
|
|
1128
|
+
return filePath ? { path: filePath, bytes: totalBytes, droppedBytes } : null;
|
|
1129
|
+
}
|
|
1130
|
+
finalized = true;
|
|
1131
|
+
head = "";
|
|
1132
|
+
if (!stream || !filePath) return null;
|
|
1133
|
+
try {
|
|
1134
|
+
stream.end();
|
|
1135
|
+
} catch {
|
|
1136
|
+
}
|
|
1137
|
+
return { path: filePath, bytes: totalBytes, droppedBytes };
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1040
1141
|
|
|
1041
1142
|
// src/circuit-breaker.ts
|
|
1042
1143
|
var DEFAULT_MAX_CONSECUTIVE_FAILURES = 5;
|
|
@@ -1516,7 +1617,7 @@ var bashTool = {
|
|
|
1516
1617
|
})();
|
|
1517
1618
|
const args = isWin ? ["/c", input.command] : ["-c", input.command];
|
|
1518
1619
|
const env = buildChildEnv(ctx.session?.id);
|
|
1519
|
-
const detached = isWin
|
|
1620
|
+
const detached = !isWin;
|
|
1520
1621
|
const startedAt = Date.now();
|
|
1521
1622
|
if (input.background) {
|
|
1522
1623
|
let buf2 = "";
|
|
@@ -1525,10 +1626,14 @@ var bashTool = {
|
|
|
1525
1626
|
cwd: ctx.projectRoot,
|
|
1526
1627
|
env,
|
|
1527
1628
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1528
|
-
|
|
1529
|
-
//
|
|
1530
|
-
//
|
|
1531
|
-
//
|
|
1629
|
+
// win32: CreateProcess IGNORES CREATE_NO_WINDOW (windowsHide) when
|
|
1630
|
+
// DETACHED_PROCESS (detached: true) is set, so the console-less
|
|
1631
|
+
// cmd.exe's grandchildren (node, dev servers) each allocate a fresh
|
|
1632
|
+
// VISIBLE console window. detached: false lets CREATE_NO_WINDOW
|
|
1633
|
+
// apply: the child gets a hidden console that grandchildren inherit.
|
|
1634
|
+
// Windows children survive parent exit either way. POSIX keeps
|
|
1635
|
+
// detached for the process-group kill semantics.
|
|
1636
|
+
detached: !isWin,
|
|
1532
1637
|
windowsHide: true,
|
|
1533
1638
|
signal: opts.signal
|
|
1534
1639
|
});
|
|
@@ -1544,24 +1649,22 @@ var bashTool = {
|
|
|
1544
1649
|
});
|
|
1545
1650
|
child2.on("close", () => registry.unregister(pid2));
|
|
1546
1651
|
}
|
|
1547
|
-
|
|
1548
|
-
if (
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
}
|
|
1553
|
-
if (buf2.length >= MAX_OUTPUT) truncated = true;
|
|
1652
|
+
const onBgData = (chunk) => {
|
|
1653
|
+
if (truncated) return;
|
|
1654
|
+
const remain = MAX_OUTPUT - buf2.length;
|
|
1655
|
+
if (remain > 0) {
|
|
1656
|
+
buf2 += chunk.toString().slice(0, remain);
|
|
1554
1657
|
}
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
if (remain > 0) {
|
|
1560
|
-
buf2 += chunk.toString().slice(0, remain);
|
|
1561
|
-
}
|
|
1562
|
-
if (buf2.length >= MAX_OUTPUT) truncated = true;
|
|
1658
|
+
if (buf2.length >= MAX_OUTPUT) {
|
|
1659
|
+
truncated = true;
|
|
1660
|
+
child2.stdout?.off("data", onBgData);
|
|
1661
|
+
child2.stderr?.off("data", onBgData);
|
|
1563
1662
|
}
|
|
1564
|
-
}
|
|
1663
|
+
};
|
|
1664
|
+
child2.stdout?.on("data", onBgData);
|
|
1665
|
+
child2.stderr?.on("data", onBgData);
|
|
1666
|
+
child2.stdout?.unref?.();
|
|
1667
|
+
child2.stderr?.unref?.();
|
|
1565
1668
|
child2.on("close", () => {
|
|
1566
1669
|
registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
|
|
1567
1670
|
});
|
|
@@ -1600,6 +1703,7 @@ var bashTool = {
|
|
|
1600
1703
|
let pending2 = "";
|
|
1601
1704
|
let timedOut = false;
|
|
1602
1705
|
const timers = [];
|
|
1706
|
+
const spool = createOutputSpool({ tool: "bash", thresholdBytes: MAX_OUTPUT });
|
|
1603
1707
|
function killWithTimeout(child2, timeoutMs2) {
|
|
1604
1708
|
if (isWin) {
|
|
1605
1709
|
if (typeof child2.pid === "number" && child2.exitCode === null && killWin32Tree(child2.pid)) {
|
|
@@ -1705,6 +1809,7 @@ var bashTool = {
|
|
|
1705
1809
|
if (buf.length < MAX_OUTPUT) {
|
|
1706
1810
|
buf += text.slice(0, MAX_OUTPUT - buf.length);
|
|
1707
1811
|
}
|
|
1812
|
+
spool.write(text);
|
|
1708
1813
|
pending2 += text;
|
|
1709
1814
|
push({ kind: "data", text });
|
|
1710
1815
|
pauseIfFlooded();
|
|
@@ -1732,10 +1837,11 @@ var bashTool = {
|
|
|
1732
1837
|
if (remainder !== null) {
|
|
1733
1838
|
yield { type: "partial_output", text: remainder };
|
|
1734
1839
|
}
|
|
1840
|
+
const spooled = spool.finalize();
|
|
1735
1841
|
yield {
|
|
1736
1842
|
type: "final",
|
|
1737
1843
|
output: {
|
|
1738
|
-
output: normalizeCommandOutput(buf),
|
|
1844
|
+
output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : ""),
|
|
1739
1845
|
exit_code: c.code,
|
|
1740
1846
|
timed_out: timedOut
|
|
1741
1847
|
}
|
|
@@ -1750,6 +1856,7 @@ var bashTool = {
|
|
|
1750
1856
|
}
|
|
1751
1857
|
} finally {
|
|
1752
1858
|
for (const t of timers) clearTimeout(t);
|
|
1859
|
+
spool.finalize();
|
|
1753
1860
|
if (isWin) opts.signal.removeEventListener("abort", onAbort);
|
|
1754
1861
|
child.stdout?.off("data", onData);
|
|
1755
1862
|
child.stderr?.off("data", onData);
|
|
@@ -1990,6 +2097,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
1990
2097
|
let stderr = "";
|
|
1991
2098
|
let killed = false;
|
|
1992
2099
|
const startedAt = Date.now();
|
|
2100
|
+
const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
|
|
1993
2101
|
const resolved = resolveWin32Command(cmd);
|
|
1994
2102
|
const isWin = process.platform === "win32";
|
|
1995
2103
|
const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
@@ -2022,10 +2130,14 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
2022
2130
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
2023
2131
|
}
|
|
2024
2132
|
child.stdout?.on("data", (chunk) => {
|
|
2025
|
-
|
|
2133
|
+
const text = chunk.toString();
|
|
2134
|
+
if (stdout.length < MAX_OUTPUT2) stdout += text;
|
|
2135
|
+
spool.write(text);
|
|
2026
2136
|
});
|
|
2027
2137
|
child.stderr?.on("data", (chunk) => {
|
|
2028
|
-
|
|
2138
|
+
const text = chunk.toString();
|
|
2139
|
+
if (stderr.length < MAX_OUTPUT2) stderr += text;
|
|
2140
|
+
spool.write(text);
|
|
2029
2141
|
});
|
|
2030
2142
|
child.on("close", (code) => {
|
|
2031
2143
|
clearTimeout(timer);
|
|
@@ -2034,10 +2146,11 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
2034
2146
|
const durationMs = Date.now() - startedAt;
|
|
2035
2147
|
const exitCode = killed ? 124 : code ?? 1;
|
|
2036
2148
|
registry.afterCall(durationMs, exitCode !== 0);
|
|
2149
|
+
const spooled = spool.finalize();
|
|
2037
2150
|
resolve7({
|
|
2038
2151
|
command: cmd,
|
|
2039
2152
|
args,
|
|
2040
|
-
stdout: normalizeCommandOutput(stdout),
|
|
2153
|
+
stdout: normalizeCommandOutput(stdout) + (spooled ? spoolNote(spooled) : ""),
|
|
2041
2154
|
stderr: normalizeCommandOutput(stderr),
|
|
2042
2155
|
exitCode,
|
|
2043
2156
|
truncated: Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES || Buffer.byteLength(stderr, "utf8") > COMMAND_OUTPUT_MAX_BYTES,
|
|
@@ -2049,6 +2162,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
2049
2162
|
if (isWin) signal.removeEventListener("abort", onAbort);
|
|
2050
2163
|
if (typeof pid === "number") registry.unregister(pid);
|
|
2051
2164
|
registry.afterCall(Date.now() - startedAt, true);
|
|
2165
|
+
spool.finalize();
|
|
2052
2166
|
resolve7({
|
|
2053
2167
|
command: cmd,
|
|
2054
2168
|
args,
|
|
@@ -3071,8 +3185,8 @@ function findGitDir(cwd, projectRoot) {
|
|
|
3071
3185
|
let dir = cwd;
|
|
3072
3186
|
for (let i = 0; i < 20; i++) {
|
|
3073
3187
|
try {
|
|
3074
|
-
const
|
|
3075
|
-
if (
|
|
3188
|
+
const stat11 = statSync(`${dir}/.git`);
|
|
3189
|
+
if (stat11.isDirectory() || stat11.isFile()) return dir;
|
|
3076
3190
|
} catch {
|
|
3077
3191
|
}
|
|
3078
3192
|
if (dir === root) break;
|
|
@@ -3375,8 +3489,8 @@ var jsonTool = {
|
|
|
3375
3489
|
};
|
|
3376
3490
|
}
|
|
3377
3491
|
};
|
|
3378
|
-
function query(data,
|
|
3379
|
-
const parts =
|
|
3492
|
+
function query(data, path21) {
|
|
3493
|
+
const parts = path21.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
3380
3494
|
let current = data;
|
|
3381
3495
|
for (const part of parts) {
|
|
3382
3496
|
if (current === null || current === void 0) return void 0;
|
|
@@ -3505,8 +3619,8 @@ function findGitDir2(cwd) {
|
|
|
3505
3619
|
let dir = cwd;
|
|
3506
3620
|
for (let i = 0; i < 20; i++) {
|
|
3507
3621
|
try {
|
|
3508
|
-
const
|
|
3509
|
-
if (
|
|
3622
|
+
const stat11 = statSync(path.join(dir, ".git"));
|
|
3623
|
+
if (stat11.isDirectory()) return dir;
|
|
3510
3624
|
} catch {
|
|
3511
3625
|
}
|
|
3512
3626
|
const parent = path.dirname(dir);
|
|
@@ -3550,8 +3664,8 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
3550
3664
|
const results = [];
|
|
3551
3665
|
for (const file of files) {
|
|
3552
3666
|
const absPath = safeResolve(file, ctx);
|
|
3553
|
-
const
|
|
3554
|
-
if (!
|
|
3667
|
+
const stat11 = await fs4.stat(absPath).catch(() => null);
|
|
3668
|
+
if (!stat11?.isFile()) continue;
|
|
3555
3669
|
const content = await fs4.readFile(absPath, "utf8");
|
|
3556
3670
|
const lines = content.split(/\r?\n/);
|
|
3557
3671
|
results.push(formatWithLineNumbers(file, lines));
|
|
@@ -3754,16 +3868,29 @@ async function* spawnStream(opts) {
|
|
|
3754
3868
|
let stderr = "";
|
|
3755
3869
|
let pending2 = "";
|
|
3756
3870
|
let error;
|
|
3871
|
+
const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
|
|
3757
3872
|
const cmd = resolveWin32Command(opts.cmd);
|
|
3758
|
-
const
|
|
3873
|
+
const isWin = process.platform === "win32";
|
|
3874
|
+
const needsShell = isWin && (cmd.endsWith(".cmd") || cmd.endsWith(".bat"));
|
|
3759
3875
|
const child = spawn(cmd, opts.args, {
|
|
3760
3876
|
cwd: opts.cwd,
|
|
3761
|
-
signal: opts.signal,
|
|
3762
3877
|
env: buildChildEnv(),
|
|
3763
3878
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3764
3879
|
windowsHide: true,
|
|
3880
|
+
...isWin ? {} : { signal: opts.signal },
|
|
3765
3881
|
...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
|
|
3766
3882
|
});
|
|
3883
|
+
const registry = getProcessRegistry();
|
|
3884
|
+
const pid = child.pid;
|
|
3885
|
+
if (typeof pid === "number") {
|
|
3886
|
+
registry.register({
|
|
3887
|
+
pid,
|
|
3888
|
+
name: opts.cmd,
|
|
3889
|
+
command: redactCommand(`${opts.cmd} ${opts.args.join(" ")}`),
|
|
3890
|
+
startedAt: Date.now(),
|
|
3891
|
+
child
|
|
3892
|
+
});
|
|
3893
|
+
}
|
|
3767
3894
|
const queue = [];
|
|
3768
3895
|
let waiter;
|
|
3769
3896
|
let paused = false;
|
|
@@ -3781,9 +3908,10 @@ async function* spawnStream(opts) {
|
|
|
3781
3908
|
child.stderr?.resume();
|
|
3782
3909
|
}
|
|
3783
3910
|
};
|
|
3784
|
-
|
|
3911
|
+
const onOut = (c) => {
|
|
3785
3912
|
const s = c.toString();
|
|
3786
3913
|
if (stdout.length < max) stdout += s;
|
|
3914
|
+
spool.write(s);
|
|
3787
3915
|
queue.push({ kind: "out", data: s });
|
|
3788
3916
|
wake();
|
|
3789
3917
|
if (!paused && queue.length >= maxQueue) {
|
|
@@ -3791,10 +3919,11 @@ async function* spawnStream(opts) {
|
|
|
3791
3919
|
child.stdout?.pause();
|
|
3792
3920
|
child.stderr?.pause();
|
|
3793
3921
|
}
|
|
3794
|
-
}
|
|
3795
|
-
|
|
3922
|
+
};
|
|
3923
|
+
const onErr = (c) => {
|
|
3796
3924
|
const s = c.toString();
|
|
3797
3925
|
if (stderr.length < max) stderr += s;
|
|
3926
|
+
spool.write(s);
|
|
3798
3927
|
queue.push({ kind: "err", data: s });
|
|
3799
3928
|
wake();
|
|
3800
3929
|
if (!paused && queue.length >= maxQueue) {
|
|
@@ -3802,51 +3931,92 @@ async function* spawnStream(opts) {
|
|
|
3802
3931
|
child.stdout?.pause();
|
|
3803
3932
|
child.stderr?.pause();
|
|
3804
3933
|
}
|
|
3805
|
-
}
|
|
3934
|
+
};
|
|
3935
|
+
child.stdout?.on("data", onOut);
|
|
3936
|
+
child.stderr?.on("data", onErr);
|
|
3806
3937
|
child.on("error", (e) => {
|
|
3807
3938
|
error = e.message;
|
|
3808
3939
|
queue.push({ kind: "error", data: e.message });
|
|
3809
3940
|
wake();
|
|
3810
3941
|
});
|
|
3811
3942
|
child.on("close", (code) => {
|
|
3943
|
+
if (typeof pid === "number") registry.unregister(pid);
|
|
3812
3944
|
queue.push({ kind: "close", data: "", code: code ?? 0 });
|
|
3813
3945
|
wake();
|
|
3814
3946
|
});
|
|
3947
|
+
const onAbort = () => {
|
|
3948
|
+
if (typeof pid === "number") {
|
|
3949
|
+
registry.kill(pid, { force: true });
|
|
3950
|
+
} else {
|
|
3951
|
+
try {
|
|
3952
|
+
child.kill("SIGKILL");
|
|
3953
|
+
} catch {
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
queue.push({ kind: "close", data: "", code: 124 });
|
|
3957
|
+
wake();
|
|
3958
|
+
};
|
|
3959
|
+
if (opts.signal.aborted) onAbort();
|
|
3960
|
+
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
3815
3961
|
let exitCode = 0;
|
|
3816
3962
|
let spawnFailed = false;
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
if (
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3963
|
+
try {
|
|
3964
|
+
for (; ; ) {
|
|
3965
|
+
while (queue.length === 0) {
|
|
3966
|
+
await new Promise((resolve7) => {
|
|
3967
|
+
waiter = resolve7;
|
|
3968
|
+
});
|
|
3969
|
+
}
|
|
3970
|
+
const chunk = queue.shift();
|
|
3971
|
+
resume();
|
|
3972
|
+
if (chunk.kind === "close") {
|
|
3973
|
+
if (!spawnFailed) exitCode = chunk.code ?? 0;
|
|
3974
|
+
break;
|
|
3975
|
+
}
|
|
3976
|
+
if (chunk.kind === "error") {
|
|
3977
|
+
spawnFailed = true;
|
|
3978
|
+
exitCode = 1;
|
|
3979
|
+
continue;
|
|
3980
|
+
}
|
|
3981
|
+
pending2 += chunk.data;
|
|
3982
|
+
if (pending2.length >= flushAt) {
|
|
3983
|
+
yield { type: "partial_output", text: pending2 };
|
|
3984
|
+
pending2 = "";
|
|
3985
|
+
}
|
|
3833
3986
|
}
|
|
3834
|
-
pending2
|
|
3835
|
-
if (pending2.length >= flushAt) {
|
|
3987
|
+
if (pending2.length > 0) {
|
|
3836
3988
|
yield { type: "partial_output", text: pending2 };
|
|
3837
|
-
|
|
3989
|
+
}
|
|
3990
|
+
const spooled = spool.finalize();
|
|
3991
|
+
return {
|
|
3992
|
+
// The marker rides on stdout's tail so every consumer's head+tail
|
|
3993
|
+
// normalization keeps it without per-tool changes.
|
|
3994
|
+
stdout: spooled ? stdout + spoolNote(spooled) : stdout,
|
|
3995
|
+
stderr,
|
|
3996
|
+
exitCode,
|
|
3997
|
+
truncated: stdout.length >= max || stderr.length >= max,
|
|
3998
|
+
error,
|
|
3999
|
+
spoolPath: spooled?.path,
|
|
4000
|
+
spoolBytes: spooled?.bytes
|
|
4001
|
+
};
|
|
4002
|
+
} finally {
|
|
4003
|
+
spool.finalize();
|
|
4004
|
+
opts.signal.removeEventListener("abort", onAbort);
|
|
4005
|
+
child.stdout?.off("data", onOut);
|
|
4006
|
+
child.stderr?.off("data", onErr);
|
|
4007
|
+
child.stdout?.destroy();
|
|
4008
|
+
child.stderr?.destroy();
|
|
4009
|
+
if (child.exitCode === null && !child.killed) {
|
|
4010
|
+
if (typeof pid === "number") {
|
|
4011
|
+
registry.kill(pid, { force: true });
|
|
4012
|
+
} else {
|
|
4013
|
+
try {
|
|
4014
|
+
child.kill("SIGKILL");
|
|
4015
|
+
} catch {
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
3838
4018
|
}
|
|
3839
4019
|
}
|
|
3840
|
-
if (pending2.length > 0) {
|
|
3841
|
-
yield { type: "partial_output", text: pending2 };
|
|
3842
|
-
}
|
|
3843
|
-
return {
|
|
3844
|
-
stdout,
|
|
3845
|
-
stderr,
|
|
3846
|
-
exitCode,
|
|
3847
|
-
truncated: stdout.length >= max || stderr.length >= max,
|
|
3848
|
-
error
|
|
3849
|
-
};
|
|
3850
4020
|
}
|
|
3851
4021
|
|
|
3852
4022
|
// src/lint.ts
|
|
@@ -3929,11 +4099,11 @@ var lintTool = {
|
|
|
3929
4099
|
}
|
|
3930
4100
|
};
|
|
3931
4101
|
async function detectLinter(cwd) {
|
|
3932
|
-
const { stat:
|
|
4102
|
+
const { stat: stat11 } = await import('node:fs/promises');
|
|
3933
4103
|
const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
|
|
3934
4104
|
for (const f of checks) {
|
|
3935
4105
|
try {
|
|
3936
|
-
await
|
|
4106
|
+
await stat11(`${cwd}/${f}`);
|
|
3937
4107
|
if (f.includes("biome")) return "biome";
|
|
3938
4108
|
if (f.includes("eslint")) return "eslint";
|
|
3939
4109
|
if (f.includes("tslint")) return "tslint";
|
|
@@ -4031,13 +4201,13 @@ var formatTool = {
|
|
|
4031
4201
|
}
|
|
4032
4202
|
};
|
|
4033
4203
|
async function detectFixer(cwd) {
|
|
4034
|
-
const { stat:
|
|
4204
|
+
const { stat: stat11 } = await import('node:fs/promises');
|
|
4035
4205
|
try {
|
|
4036
|
-
await
|
|
4206
|
+
await stat11(`${cwd}/biome.json`);
|
|
4037
4207
|
return "biome";
|
|
4038
4208
|
} catch {
|
|
4039
4209
|
try {
|
|
4040
|
-
await
|
|
4210
|
+
await stat11(`${cwd}/.prettierrc`);
|
|
4041
4211
|
return "prettier";
|
|
4042
4212
|
} catch {
|
|
4043
4213
|
return "biome";
|
|
@@ -4115,11 +4285,11 @@ var typecheckTool = {
|
|
|
4115
4285
|
}
|
|
4116
4286
|
};
|
|
4117
4287
|
async function findTsConfig(cwd) {
|
|
4118
|
-
const { stat:
|
|
4288
|
+
const { stat: stat11 } = await import('node:fs/promises');
|
|
4119
4289
|
const candidates = ["tsconfig.json", "tsconfig.base.json"];
|
|
4120
4290
|
for (const f of candidates) {
|
|
4121
4291
|
try {
|
|
4122
|
-
const s = await
|
|
4292
|
+
const s = await stat11(path.join(cwd, f));
|
|
4123
4293
|
if (s.isFile()) return path.join(cwd, f);
|
|
4124
4294
|
} catch {
|
|
4125
4295
|
}
|
|
@@ -4150,7 +4320,11 @@ var testTool = {
|
|
|
4150
4320
|
coverage: { type: "boolean", description: "Generate coverage report (default: false)" },
|
|
4151
4321
|
cwd: { type: "string", description: "Working directory (default: cwd)" },
|
|
4152
4322
|
grep: { type: "string", description: "Filter tests by name pattern (default: none)" },
|
|
4153
|
-
timeout: { type: "integer", description: "Test timeout in ms (default: 30000)" }
|
|
4323
|
+
timeout: { type: "integer", description: "Test timeout in ms (default: 30000)" },
|
|
4324
|
+
verbose: {
|
|
4325
|
+
type: "boolean",
|
|
4326
|
+
description: "Per-test verbose reporter output (default: false \u2014 the summary reporter is used; full output is always saved to a log file referenced in the result)"
|
|
4327
|
+
}
|
|
4154
4328
|
}
|
|
4155
4329
|
},
|
|
4156
4330
|
async execute(input, ctx, opts) {
|
|
@@ -4198,11 +4372,11 @@ var testTool = {
|
|
|
4198
4372
|
}
|
|
4199
4373
|
};
|
|
4200
4374
|
async function detectRunner(cwd) {
|
|
4201
|
-
const { stat:
|
|
4375
|
+
const { stat: stat11 } = await import('node:fs/promises');
|
|
4202
4376
|
const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
|
|
4203
4377
|
for (const f of candidates) {
|
|
4204
4378
|
try {
|
|
4205
|
-
await
|
|
4379
|
+
await stat11(path.join(cwd, f));
|
|
4206
4380
|
if (f.includes("vitest")) return "vitest";
|
|
4207
4381
|
if (f.includes("jest")) return "jest";
|
|
4208
4382
|
if (f.includes("mocha")) return "mocha";
|
|
@@ -4216,17 +4390,14 @@ function buildArgs2(runner, input) {
|
|
|
4216
4390
|
const timeout = input.timeout ?? 3e4;
|
|
4217
4391
|
switch (runner) {
|
|
4218
4392
|
case "vitest":
|
|
4219
|
-
args.push("
|
|
4220
|
-
if (input.
|
|
4221
|
-
args[1] = "";
|
|
4222
|
-
args.push("watch");
|
|
4223
|
-
}
|
|
4393
|
+
args.push(input.watch ? "watch" : "run");
|
|
4394
|
+
if (input.verbose) args.push("--reporter=verbose");
|
|
4224
4395
|
if (input.coverage) args.push("--coverage");
|
|
4225
4396
|
if (input.grep) args.push("--testNamePattern", input.grep);
|
|
4226
4397
|
args.push("--testTimeout", String(timeout));
|
|
4227
4398
|
break;
|
|
4228
4399
|
case "jest":
|
|
4229
|
-
args.push("--verbose");
|
|
4400
|
+
if (input.verbose) args.push("--verbose");
|
|
4230
4401
|
if (input.watch) args.push("--watch");
|
|
4231
4402
|
if (input.coverage) args.push("--coverage");
|
|
4232
4403
|
if (input.grep) args.push("--testPathPattern", input.grep);
|
|
@@ -4270,7 +4441,13 @@ function parseResult(runner, result, duration) {
|
|
|
4270
4441
|
passed,
|
|
4271
4442
|
failed,
|
|
4272
4443
|
duration_ms: duration,
|
|
4273
|
-
|
|
4444
|
+
// A passing run only needs the tail summary in chat history — counts are
|
|
4445
|
+
// already parsed above and the FULL log is on disk (spool marker rides
|
|
4446
|
+
// the stdout tail). Failures keep the standard command-output cap so
|
|
4447
|
+
// the agent sees the failure details inline.
|
|
4448
|
+
output: normalizeCommandOutput(result.stdout || result.error || "", {
|
|
4449
|
+
maxBytes: result.exitCode === 0 ? 4096 : void 0
|
|
4450
|
+
}),
|
|
4274
4451
|
truncated: result.truncated
|
|
4275
4452
|
};
|
|
4276
4453
|
}
|
|
@@ -4736,7 +4913,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
4736
4913
|
}
|
|
4737
4914
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
4738
4915
|
var MAX_TAIL_LINES = 1e5;
|
|
4739
|
-
async function fileLogs(
|
|
4916
|
+
async function fileLogs(path21, lines, filterRe, stream) {
|
|
4740
4917
|
const { createInterface } = await import('node:readline');
|
|
4741
4918
|
const { createReadStream } = await import('node:fs');
|
|
4742
4919
|
const entries = [];
|
|
@@ -4745,7 +4922,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
|
|
|
4745
4922
|
let writeIdx = 0;
|
|
4746
4923
|
let totalLines = 0;
|
|
4747
4924
|
const rl = createInterface({
|
|
4748
|
-
input: createReadStream(
|
|
4925
|
+
input: createReadStream(path21),
|
|
4749
4926
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
4750
4927
|
});
|
|
4751
4928
|
for await (const line of rl) {
|
|
@@ -4766,7 +4943,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
|
|
|
4766
4943
|
if (parsed) entries.push(parsed);
|
|
4767
4944
|
}
|
|
4768
4945
|
return {
|
|
4769
|
-
source:
|
|
4946
|
+
source: path21,
|
|
4770
4947
|
entries,
|
|
4771
4948
|
total: entries.length,
|
|
4772
4949
|
truncated: totalLines > effLines,
|
|
@@ -4889,8 +5066,8 @@ async function resolveFiles2(filesInput, cwd) {
|
|
|
4889
5066
|
for (const f of files) {
|
|
4890
5067
|
const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
|
|
4891
5068
|
try {
|
|
4892
|
-
const
|
|
4893
|
-
if (
|
|
5069
|
+
const stat11 = await fs4.stat(absPath);
|
|
5070
|
+
if (stat11.isFile()) resolved.push(absPath);
|
|
4894
5071
|
} catch {
|
|
4895
5072
|
}
|
|
4896
5073
|
}
|
|
@@ -7902,20 +8079,20 @@ async function runIndexerWithStore(store, opts) {
|
|
|
7902
8079
|
await yieldEventLoop();
|
|
7903
8080
|
throwIfAborted(signal);
|
|
7904
8081
|
}
|
|
7905
|
-
let
|
|
8082
|
+
let stat11;
|
|
7906
8083
|
try {
|
|
7907
8084
|
const statOpts = signal ? { signal } : {};
|
|
7908
|
-
|
|
8085
|
+
stat11 = await fs4.stat(file, statOpts);
|
|
7909
8086
|
} catch (e) {
|
|
7910
8087
|
if (isAbortError(e)) throw e;
|
|
7911
8088
|
store.deleteFile(file);
|
|
7912
8089
|
continue;
|
|
7913
8090
|
}
|
|
7914
|
-
if (!
|
|
8091
|
+
if (!stat11.isFile()) continue;
|
|
7915
8092
|
const lang = detectLang(file);
|
|
7916
8093
|
if (!lang) continue;
|
|
7917
8094
|
const meta = existingMeta.get(file);
|
|
7918
|
-
if (!force && meta && meta.mtimeMs === Math.floor(
|
|
8095
|
+
if (!force && meta && meta.mtimeMs === Math.floor(stat11.mtimeMs)) {
|
|
7919
8096
|
langStats[lang] = (langStats[lang] ?? 0) + meta.symbolCount;
|
|
7920
8097
|
symbolsIndexed += meta.symbolCount;
|
|
7921
8098
|
filesIndexed++;
|
|
@@ -7942,7 +8119,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
7942
8119
|
store.upsertFile({
|
|
7943
8120
|
file,
|
|
7944
8121
|
lang,
|
|
7945
|
-
mtimeMs: Math.floor(
|
|
8122
|
+
mtimeMs: Math.floor(stat11.mtimeMs),
|
|
7946
8123
|
symbolCount: 0,
|
|
7947
8124
|
lastIndexed: Date.now()
|
|
7948
8125
|
});
|
|
@@ -7968,7 +8145,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
7968
8145
|
store.upsertFile({
|
|
7969
8146
|
file,
|
|
7970
8147
|
lang,
|
|
7971
|
-
mtimeMs: Math.floor(
|
|
8148
|
+
mtimeMs: Math.floor(stat11.mtimeMs),
|
|
7972
8149
|
symbolCount: count,
|
|
7973
8150
|
lastIndexed: Date.now()
|
|
7974
8151
|
});
|
|
@@ -8249,6 +8426,13 @@ function debounceKey(indexDir, file) {
|
|
|
8249
8426
|
function isIndexableFile(filePath) {
|
|
8250
8427
|
return detectLang(filePath) !== null;
|
|
8251
8428
|
}
|
|
8429
|
+
function isUniqueConstraintError(err) {
|
|
8430
|
+
if (err instanceof Error) {
|
|
8431
|
+
const msg = err.message.toLowerCase();
|
|
8432
|
+
return msg.includes("unique constraint") || msg.includes("UNIQUE constraint");
|
|
8433
|
+
}
|
|
8434
|
+
return false;
|
|
8435
|
+
}
|
|
8252
8436
|
async function runStartupIndex(opts) {
|
|
8253
8437
|
if (!indexCircuitBreaker.allowRequest()) throw circuitOpenError();
|
|
8254
8438
|
_indexing = true;
|
|
@@ -8278,6 +8462,15 @@ async function runStartupIndex(opts) {
|
|
|
8278
8462
|
return result;
|
|
8279
8463
|
} catch (err) {
|
|
8280
8464
|
_lastError = err instanceof Error ? err.message : String(err);
|
|
8465
|
+
if (isUniqueConstraintError(err) && !opts.force) {
|
|
8466
|
+
_lastError = null;
|
|
8467
|
+
const rebuildResult = await runStartupIndex({
|
|
8468
|
+
...opts,
|
|
8469
|
+
force: true
|
|
8470
|
+
});
|
|
8471
|
+
_ready = true;
|
|
8472
|
+
return rebuildResult;
|
|
8473
|
+
}
|
|
8281
8474
|
_ready = true;
|
|
8282
8475
|
if (!opts.signal?.aborted) indexCircuitBreaker.recordFailure(err);
|
|
8283
8476
|
throw err;
|