@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/builtin.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { spawn, execFileSync, spawnSync } from 'node:child_process';
|
|
2
2
|
import * as Core from '@wrongstack/core';
|
|
3
|
-
import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, loadTasks, emptyTaskFile, saveTasks, formatTaskList, formatPlan, mutateTasks, loadPlan, emptyPlan, savePlan, computeTaskItemProgress, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
3
|
+
import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, loadTasks, emptyTaskFile, saveTasks, formatTaskList, formatPlan, mutateTasks, loadPlan, emptyPlan, savePlan, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
4
4
|
import * as fs from 'node:fs';
|
|
5
|
-
import { statSync,
|
|
6
|
-
import * as path2 from 'node:path';
|
|
7
|
-
import { resolve, sep, dirname, join } from 'node:path';
|
|
5
|
+
import { statSync, mkdirSync, createWriteStream, writeFileSync } from 'node:fs';
|
|
8
6
|
import * as fs14 from 'node:fs/promises';
|
|
7
|
+
import * as path3 from 'node:path';
|
|
8
|
+
import { resolve, sep, dirname, join } from 'node:path';
|
|
9
9
|
import * as os from 'node:os';
|
|
10
10
|
import { createRequire } from 'node:module';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
@@ -17,370 +17,106 @@ import { Agent } from 'undici';
|
|
|
17
17
|
import { randomUUID } from 'node:crypto';
|
|
18
18
|
|
|
19
19
|
// src/_spawn-stream.ts
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
20
|
+
var SPOOL_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
21
|
+
var SPOOL_WRITE_HWM_BYTES = 4 * 1024 * 1024;
|
|
22
|
+
var sweepStarted = false;
|
|
23
|
+
function toolOutputDir() {
|
|
24
|
+
return path3.join(wstackGlobalRoot(), "tool-output");
|
|
25
|
+
}
|
|
26
|
+
function sweepOldSpoolFiles(dir) {
|
|
27
|
+
if (sweepStarted) return;
|
|
28
|
+
sweepStarted = true;
|
|
29
|
+
void (async () => {
|
|
30
|
+
try {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
for (const name of await fs14.readdir(dir)) {
|
|
33
|
+
if (!name.endsWith(".log")) continue;
|
|
34
|
+
const p = path3.join(dir, name);
|
|
35
|
+
try {
|
|
36
|
+
const st = await fs14.stat(p);
|
|
37
|
+
if (now - st.mtimeMs > SPOOL_RETENTION_MS) await fs14.unlink(p);
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
35
40
|
}
|
|
41
|
+
} catch {
|
|
36
42
|
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
let
|
|
48
|
-
let
|
|
49
|
-
let
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
};
|
|
70
|
-
const resume = () => {
|
|
71
|
-
if (paused && queue.length < maxQueue) {
|
|
72
|
-
paused = false;
|
|
73
|
-
child.stdout?.resume();
|
|
74
|
-
child.stderr?.resume();
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
child.stdout?.on("data", (c) => {
|
|
78
|
-
const s = c.toString();
|
|
79
|
-
if (stdout.length < max) stdout += s;
|
|
80
|
-
queue.push({ kind: "out", data: s });
|
|
81
|
-
wake();
|
|
82
|
-
if (!paused && queue.length >= maxQueue) {
|
|
83
|
-
paused = true;
|
|
84
|
-
child.stdout?.pause();
|
|
85
|
-
child.stderr?.pause();
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
child.stderr?.on("data", (c) => {
|
|
89
|
-
const s = c.toString();
|
|
90
|
-
if (stderr.length < max) stderr += s;
|
|
91
|
-
queue.push({ kind: "err", data: s });
|
|
92
|
-
wake();
|
|
93
|
-
if (!paused && queue.length >= maxQueue) {
|
|
94
|
-
paused = true;
|
|
95
|
-
child.stdout?.pause();
|
|
96
|
-
child.stderr?.pause();
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
child.on("error", (e) => {
|
|
100
|
-
error = e.message;
|
|
101
|
-
queue.push({ kind: "error", data: e.message });
|
|
102
|
-
wake();
|
|
103
|
-
});
|
|
104
|
-
child.on("close", (code) => {
|
|
105
|
-
queue.push({ kind: "close", data: "", code: code ?? 0 });
|
|
106
|
-
wake();
|
|
107
|
-
});
|
|
108
|
-
let exitCode = 0;
|
|
109
|
-
let spawnFailed = false;
|
|
110
|
-
for (; ; ) {
|
|
111
|
-
while (queue.length === 0) {
|
|
112
|
-
await new Promise((resolve7) => {
|
|
113
|
-
waiter = resolve7;
|
|
43
|
+
})();
|
|
44
|
+
}
|
|
45
|
+
function spoolNote(info) {
|
|
46
|
+
const dropped = info.droppedBytes > 0 ? `, ~${info.droppedBytes} bytes dropped under backpressure` : "";
|
|
47
|
+
return `
|
|
48
|
+
[output truncated \u2014 full ${info.bytes} bytes at ${info.path}${dropped}; read/grep that file selectively instead of re-running with more output]`;
|
|
49
|
+
}
|
|
50
|
+
function createOutputSpool(opts) {
|
|
51
|
+
const threshold = opts.thresholdBytes ?? 32768;
|
|
52
|
+
const safeTool = opts.tool.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 40) || "tool";
|
|
53
|
+
let head = "";
|
|
54
|
+
let headBytes = 0;
|
|
55
|
+
let totalBytes = 0;
|
|
56
|
+
let droppedBytes = 0;
|
|
57
|
+
let stream = null;
|
|
58
|
+
let filePath = null;
|
|
59
|
+
let failed = false;
|
|
60
|
+
let finalized = false;
|
|
61
|
+
const open = () => {
|
|
62
|
+
if (stream || failed) return;
|
|
63
|
+
try {
|
|
64
|
+
const dir = toolOutputDir();
|
|
65
|
+
mkdirSync(dir, { recursive: true });
|
|
66
|
+
sweepOldSpoolFiles(dir);
|
|
67
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
68
|
+
const rand = Math.random().toString(36).slice(2, 6);
|
|
69
|
+
filePath = path3.join(dir, `${stamp}-${safeTool}-${rand}.log`);
|
|
70
|
+
stream = createWriteStream(filePath, { flags: "w", encoding: "utf8" });
|
|
71
|
+
stream.on("error", () => {
|
|
72
|
+
failed = true;
|
|
73
|
+
stream = null;
|
|
74
|
+
filePath = null;
|
|
114
75
|
});
|
|
76
|
+
stream.write(head);
|
|
77
|
+
} catch {
|
|
78
|
+
failed = true;
|
|
79
|
+
stream = null;
|
|
80
|
+
filePath = null;
|
|
115
81
|
}
|
|
116
|
-
const chunk = queue.shift();
|
|
117
|
-
resume();
|
|
118
|
-
if (chunk.kind === "close") {
|
|
119
|
-
if (!spawnFailed) exitCode = chunk.code ?? 0;
|
|
120
|
-
break;
|
|
121
|
-
}
|
|
122
|
-
if (chunk.kind === "error") {
|
|
123
|
-
spawnFailed = true;
|
|
124
|
-
exitCode = 1;
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
pending2 += chunk.data;
|
|
128
|
-
if (pending2.length >= flushAt) {
|
|
129
|
-
yield { type: "partial_output", text: pending2 };
|
|
130
|
-
pending2 = "";
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
if (pending2.length > 0) {
|
|
134
|
-
yield { type: "partial_output", text: pending2 };
|
|
135
|
-
}
|
|
136
|
-
return {
|
|
137
|
-
stdout,
|
|
138
|
-
stderr,
|
|
139
|
-
exitCode,
|
|
140
|
-
truncated: stdout.length >= max || stderr.length >= max,
|
|
141
|
-
error
|
|
142
82
|
};
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
function resolvePath(input, ctx) {
|
|
159
|
-
return path2.isAbsolute(input) ? path2.normalize(input) : path2.resolve(ctx.workingDir ?? ctx.cwd, input);
|
|
160
|
-
}
|
|
161
|
-
function ensureInsideRoot(absPath, ctx) {
|
|
162
|
-
const root = path2.resolve(ctx.projectRoot);
|
|
163
|
-
const target = path2.resolve(absPath);
|
|
164
|
-
const rel = path2.relative(root, target);
|
|
165
|
-
if (rel.startsWith("..") || path2.isAbsolute(rel)) {
|
|
166
|
-
throw new Error(`Path "${absPath}" is outside project root "${root}"`);
|
|
167
|
-
}
|
|
168
|
-
return target;
|
|
169
|
-
}
|
|
170
|
-
function safeResolve(input, ctx) {
|
|
171
|
-
return ensureInsideRoot(resolvePath(input, ctx), ctx);
|
|
172
|
-
}
|
|
173
|
-
async function assertRealInsideRoot(absPath, ctx) {
|
|
174
|
-
const realRoot = await fs14.realpath(ctx.projectRoot).catch(() => path2.resolve(ctx.projectRoot));
|
|
175
|
-
let probe = absPath;
|
|
176
|
-
for (; ; ) {
|
|
177
|
-
let real;
|
|
178
|
-
try {
|
|
179
|
-
real = await fs14.realpath(probe);
|
|
180
|
-
} catch (err) {
|
|
181
|
-
if (err.code === "ENOENT") {
|
|
182
|
-
const parent = path2.dirname(probe);
|
|
183
|
-
if (parent === probe) return;
|
|
184
|
-
probe = parent;
|
|
185
|
-
continue;
|
|
83
|
+
return {
|
|
84
|
+
write(text) {
|
|
85
|
+
if (finalized || !text) return;
|
|
86
|
+
totalBytes += Buffer.byteLength(text, "utf8");
|
|
87
|
+
if (!stream && !failed) {
|
|
88
|
+
if (headBytes + text.length <= threshold) {
|
|
89
|
+
head += text;
|
|
90
|
+
headBytes += text.length;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
head += text;
|
|
94
|
+
open();
|
|
95
|
+
head = "";
|
|
96
|
+
return;
|
|
186
97
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
\u2026[truncated ${Buffer.byteLength(s, "utf8") - max} bytes from middle]\u2026
|
|
208
|
-
` + s.slice(-half);
|
|
209
|
-
}
|
|
210
|
-
function isBinaryBuffer(buf) {
|
|
211
|
-
const len = Math.min(buf.length, 8192);
|
|
212
|
-
for (let i = 0; i < len; i++) {
|
|
213
|
-
if (buf[i] === 0) return true;
|
|
214
|
-
}
|
|
215
|
-
return false;
|
|
216
|
-
}
|
|
217
|
-
var COMMAND_OUTPUT_MAX_BYTES = 32768;
|
|
218
|
-
var REPEAT_RUN_THRESHOLD = 3;
|
|
219
|
-
function collapseCarriageReturns(text) {
|
|
220
|
-
const lf = text.replace(/\r\n/g, "\n");
|
|
221
|
-
if (!lf.includes("\r")) return lf;
|
|
222
|
-
return lf.split("\n").map((line) => line.includes("\r") ? line.slice(line.lastIndexOf("\r") + 1) : line).join("\n");
|
|
223
|
-
}
|
|
224
|
-
function collapseConsecutiveDuplicates(text, minRun = REPEAT_RUN_THRESHOLD) {
|
|
225
|
-
const lines = text.split("\n");
|
|
226
|
-
const out = [];
|
|
227
|
-
let i = 0;
|
|
228
|
-
while (i < lines.length) {
|
|
229
|
-
let j = i + 1;
|
|
230
|
-
while (j < lines.length && lines[j] === lines[i]) j++;
|
|
231
|
-
const run = j - i;
|
|
232
|
-
if (run >= minRun) {
|
|
233
|
-
out.push(lines[i], `\u2026 \u27E8repeated ${run}\xD7\u27E9`);
|
|
234
|
-
} else {
|
|
235
|
-
for (let k = i; k < j; k++) out.push(lines[k]);
|
|
236
|
-
}
|
|
237
|
-
i = j;
|
|
238
|
-
}
|
|
239
|
-
return out.join("\n");
|
|
240
|
-
}
|
|
241
|
-
function takeHeadBytes(s, maxBytes) {
|
|
242
|
-
if (maxBytes <= 0) return "";
|
|
243
|
-
if (Buffer.byteLength(s, "utf8") <= maxBytes) return s;
|
|
244
|
-
let lo = 0;
|
|
245
|
-
let hi = s.length;
|
|
246
|
-
while (lo < hi) {
|
|
247
|
-
const mid = Math.ceil((lo + hi) / 2);
|
|
248
|
-
if (Buffer.byteLength(s.slice(0, mid), "utf8") <= maxBytes) lo = mid;
|
|
249
|
-
else hi = mid - 1;
|
|
250
|
-
}
|
|
251
|
-
return s.slice(0, lo);
|
|
252
|
-
}
|
|
253
|
-
function takeTailBytes(s, maxBytes) {
|
|
254
|
-
if (maxBytes <= 0) return "";
|
|
255
|
-
if (Buffer.byteLength(s, "utf8") <= maxBytes) return s;
|
|
256
|
-
let lo = 0;
|
|
257
|
-
let hi = s.length;
|
|
258
|
-
while (lo < hi) {
|
|
259
|
-
const mid = Math.ceil((lo + hi) / 2);
|
|
260
|
-
if (Buffer.byteLength(s.slice(s.length - mid), "utf8") <= maxBytes) lo = mid;
|
|
261
|
-
else hi = mid - 1;
|
|
262
|
-
}
|
|
263
|
-
return s.slice(s.length - lo);
|
|
264
|
-
}
|
|
265
|
-
function truncateHeadTail(s, maxBytes) {
|
|
266
|
-
const total = Buffer.byteLength(s, "utf8");
|
|
267
|
-
if (total <= maxBytes) return s;
|
|
268
|
-
const MARKER_RESERVE = 64;
|
|
269
|
-
const avail = Math.max(0, maxBytes - MARKER_RESERVE);
|
|
270
|
-
const headBudget = Math.floor(avail * 0.45);
|
|
271
|
-
const head = takeHeadBytes(s, headBudget);
|
|
272
|
-
const tail = takeTailBytes(s, avail - Buffer.byteLength(head, "utf8"));
|
|
273
|
-
const kept = Buffer.byteLength(head, "utf8") + Buffer.byteLength(tail, "utf8");
|
|
274
|
-
return `${head}
|
|
275
|
-
\u2026[truncated ${total - kept} bytes]\u2026
|
|
276
|
-
${tail}`;
|
|
277
|
-
}
|
|
278
|
-
function normalizeCommandOutput(raw, opts = {}) {
|
|
279
|
-
if (!raw) return raw;
|
|
280
|
-
let text = Core.stripAnsi(raw);
|
|
281
|
-
text = collapseCarriageReturns(text);
|
|
282
|
-
text = text.replace(/[ \t]+$/gm, "");
|
|
283
|
-
text = collapseConsecutiveDuplicates(text);
|
|
284
|
-
text = text.replace(/\n{3,}/g, "\n\n");
|
|
285
|
-
return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// src/audit.ts
|
|
289
|
-
var auditTool = {
|
|
290
|
-
name: "audit",
|
|
291
|
-
category: "Package Management",
|
|
292
|
-
description: "Run a security audit against project dependencies (using pnpm/npm audit). Reports known vulnerabilities with severity.",
|
|
293
|
-
usageHint: "CRITICAL SECURITY TOOL:\n\n- Run regularly and especially before any release.\n- Use `level` to focus on high/critical issues.\n- `fix` can attempt automatic remediation for some vulnerabilities.\nThis is one of the most important tools for supply chain security.",
|
|
294
|
-
permission: "confirm",
|
|
295
|
-
mutating: false,
|
|
296
|
-
timeoutMs: 6e4,
|
|
297
|
-
inputSchema: {
|
|
298
|
-
type: "object",
|
|
299
|
-
properties: {
|
|
300
|
-
cwd: { type: "string", description: "Working directory (default: cwd)" },
|
|
301
|
-
level: {
|
|
302
|
-
type: "string",
|
|
303
|
-
enum: ["low", "moderate", "high", "critical"],
|
|
304
|
-
description: "Minimum severity level to report"
|
|
305
|
-
},
|
|
306
|
-
fix: { type: "boolean", description: "Attempt to fix vulnerabilities (default: false)" },
|
|
307
|
-
packages: { type: "string", description: "Specific package(s) to audit (comma-separated)" }
|
|
308
|
-
}
|
|
309
|
-
},
|
|
310
|
-
async execute(input, ctx, opts) {
|
|
311
|
-
let final;
|
|
312
|
-
const executeStream = auditTool.executeStream;
|
|
313
|
-
if (!executeStream) throw new Error("auditTool: stream execution unavailable");
|
|
314
|
-
for await (const ev of executeStream(input, ctx, opts)) {
|
|
315
|
-
if (ev.type === "final") final = ev.output;
|
|
316
|
-
}
|
|
317
|
-
if (!final) throw new Error("audit: stream ended without final event");
|
|
318
|
-
return final;
|
|
319
|
-
},
|
|
320
|
-
async *executeStream(input, ctx, opts) {
|
|
321
|
-
const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
|
|
322
|
-
const manager = await detectPackageManager(cwd);
|
|
323
|
-
yield { type: "log", text: `Auditing with ${manager}\u2026`, data: { manager } };
|
|
324
|
-
const args = ["audit", "--json"];
|
|
325
|
-
if (input.fix) args.push("--fix");
|
|
326
|
-
if (input.packages) {
|
|
327
|
-
const pkgs = Array.isArray(input.packages) ? input.packages : input.packages.split(",");
|
|
328
|
-
args.push(...pkgs.map((p) => p.trim()));
|
|
329
|
-
}
|
|
330
|
-
const result = yield* spawnStream({
|
|
331
|
-
cmd: manager,
|
|
332
|
-
args,
|
|
333
|
-
cwd,
|
|
334
|
-
signal: opts.signal,
|
|
335
|
-
maxBytes: 1e5
|
|
336
|
-
});
|
|
337
|
-
yield { type: "final", output: parseAuditOutput(result.stdout, result.exitCode) };
|
|
338
|
-
}
|
|
339
|
-
};
|
|
340
|
-
function parseAuditOutput(json, exitCode) {
|
|
341
|
-
if (!json) {
|
|
342
|
-
return {
|
|
343
|
-
exit_code: exitCode,
|
|
344
|
-
vulnerabilities: [],
|
|
345
|
-
total: 0,
|
|
346
|
-
summary: exitCode === 0 ? "No vulnerabilities found" : "Audit failed",
|
|
347
|
-
output: "",
|
|
348
|
-
truncated: false
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
try {
|
|
352
|
-
const data = JSON.parse(json);
|
|
353
|
-
const advisories = [];
|
|
354
|
-
const ads = data.advisories ?? {};
|
|
355
|
-
for (const id of Object.keys(ads)) {
|
|
356
|
-
const adv = ads[id];
|
|
357
|
-
advisories.push({
|
|
358
|
-
severity: adv.severity ?? "unknown",
|
|
359
|
-
package: adv.module_name ?? id,
|
|
360
|
-
title: adv.title ?? "Unknown vulnerability",
|
|
361
|
-
url: adv.url ?? ""
|
|
362
|
-
});
|
|
98
|
+
if (stream) {
|
|
99
|
+
if (stream.writableLength > SPOOL_WRITE_HWM_BYTES) {
|
|
100
|
+
droppedBytes += Buffer.byteLength(text, "utf8");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
stream.write(text);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
finalize() {
|
|
107
|
+
if (finalized) {
|
|
108
|
+
return filePath ? { path: filePath, bytes: totalBytes, droppedBytes } : null;
|
|
109
|
+
}
|
|
110
|
+
finalized = true;
|
|
111
|
+
head = "";
|
|
112
|
+
if (!stream || !filePath) return null;
|
|
113
|
+
try {
|
|
114
|
+
stream.end();
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
return { path: filePath, bytes: totalBytes, droppedBytes };
|
|
363
118
|
}
|
|
364
|
-
|
|
365
|
-
const summary = total === 0 ? "No vulnerabilities found" : `Found ${total} vulnerabilities: ${advisories.filter((a) => a.severity === "critical").length} critical, ${advisories.filter((a) => a.severity === "high").length} high`;
|
|
366
|
-
return {
|
|
367
|
-
exit_code: exitCode,
|
|
368
|
-
vulnerabilities: advisories,
|
|
369
|
-
total,
|
|
370
|
-
summary,
|
|
371
|
-
output: json,
|
|
372
|
-
truncated: json.length >= 1e5
|
|
373
|
-
};
|
|
374
|
-
} catch {
|
|
375
|
-
return {
|
|
376
|
-
exit_code: exitCode,
|
|
377
|
-
vulnerabilities: [],
|
|
378
|
-
total: 0,
|
|
379
|
-
summary: "Could not parse audit output",
|
|
380
|
-
output: json,
|
|
381
|
-
truncated: false
|
|
382
|
-
};
|
|
383
|
-
}
|
|
119
|
+
};
|
|
384
120
|
}
|
|
385
121
|
|
|
386
122
|
// src/circuit-breaker.ts
|
|
@@ -726,43 +462,464 @@ var ProcessRegistryImpl = class {
|
|
|
726
462
|
}, graceMs);
|
|
727
463
|
timer.unref?.();
|
|
728
464
|
}
|
|
729
|
-
} catch {
|
|
465
|
+
} catch {
|
|
466
|
+
}
|
|
467
|
+
p.killed = true;
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Kill all tracked processes.
|
|
472
|
+
* Returns the PIDs that were kill()ed.
|
|
473
|
+
*/
|
|
474
|
+
killAll(opts = {}) {
|
|
475
|
+
const pids = Array.from(this.processes.keys());
|
|
476
|
+
const killed = [];
|
|
477
|
+
for (const pid of pids) {
|
|
478
|
+
const p = this.processes.get(pid);
|
|
479
|
+
if (p && !p.protected && this.kill(pid, opts)) killed.push(pid);
|
|
480
|
+
}
|
|
481
|
+
return killed;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Kill all processes for a specific session.
|
|
485
|
+
* Returns the PIDs that were kill()ed.
|
|
486
|
+
*/
|
|
487
|
+
killSession(sessionId, opts = {}) {
|
|
488
|
+
const pids = this.bySession(sessionId).map((p) => p.pid);
|
|
489
|
+
const killed = [];
|
|
490
|
+
for (const pid of pids) {
|
|
491
|
+
if (this.kill(pid, opts)) killed.push(pid);
|
|
492
|
+
}
|
|
493
|
+
return killed;
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
var _registry;
|
|
497
|
+
function getProcessRegistry() {
|
|
498
|
+
if (!_registry) {
|
|
499
|
+
_registry = new ProcessRegistryImpl();
|
|
500
|
+
}
|
|
501
|
+
return _registry;
|
|
502
|
+
}
|
|
503
|
+
function resolveWin32Command(cmd) {
|
|
504
|
+
if (process.platform !== "win32") return cmd;
|
|
505
|
+
if (cmd.includes("/") || cmd.includes("\\") || path3.extname(cmd)) {
|
|
506
|
+
return cmd;
|
|
507
|
+
}
|
|
508
|
+
const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
|
|
509
|
+
const pathDirs = (process.env["PATH"] ?? "").split(path3.delimiter);
|
|
510
|
+
for (const dir of pathDirs) {
|
|
511
|
+
const base = path3.join(dir, cmd);
|
|
512
|
+
for (const ext of pathext) {
|
|
513
|
+
const full = `${base}${ext}`;
|
|
514
|
+
try {
|
|
515
|
+
fs.accessSync(full, fs.constants.X_OK);
|
|
516
|
+
return full;
|
|
517
|
+
} catch {
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return cmd;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// src/_spawn-stream.ts
|
|
525
|
+
async function* spawnStream(opts) {
|
|
526
|
+
const max = opts.maxBytes ?? 2e5;
|
|
527
|
+
const flushAt = opts.flushBytes ?? 4 * 1024;
|
|
528
|
+
const maxQueue = opts.maxQueueSize ?? 500;
|
|
529
|
+
let stdout = "";
|
|
530
|
+
let stderr = "";
|
|
531
|
+
let pending2 = "";
|
|
532
|
+
let error;
|
|
533
|
+
const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
|
|
534
|
+
const cmd = resolveWin32Command(opts.cmd);
|
|
535
|
+
const isWin = process.platform === "win32";
|
|
536
|
+
const needsShell = isWin && (cmd.endsWith(".cmd") || cmd.endsWith(".bat"));
|
|
537
|
+
const child = spawn(cmd, opts.args, {
|
|
538
|
+
cwd: opts.cwd,
|
|
539
|
+
env: buildChildEnv(),
|
|
540
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
541
|
+
windowsHide: true,
|
|
542
|
+
...isWin ? {} : { signal: opts.signal },
|
|
543
|
+
...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
|
|
544
|
+
});
|
|
545
|
+
const registry = getProcessRegistry();
|
|
546
|
+
const pid = child.pid;
|
|
547
|
+
if (typeof pid === "number") {
|
|
548
|
+
registry.register({
|
|
549
|
+
pid,
|
|
550
|
+
name: opts.cmd,
|
|
551
|
+
command: redactCommand(`${opts.cmd} ${opts.args.join(" ")}`),
|
|
552
|
+
startedAt: Date.now(),
|
|
553
|
+
child
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
const queue = [];
|
|
557
|
+
let waiter;
|
|
558
|
+
let paused = false;
|
|
559
|
+
const wake = () => {
|
|
560
|
+
if (waiter) {
|
|
561
|
+
const w = waiter;
|
|
562
|
+
waiter = void 0;
|
|
563
|
+
w();
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
const resume = () => {
|
|
567
|
+
if (paused && queue.length < maxQueue) {
|
|
568
|
+
paused = false;
|
|
569
|
+
child.stdout?.resume();
|
|
570
|
+
child.stderr?.resume();
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
const onOut = (c) => {
|
|
574
|
+
const s = c.toString();
|
|
575
|
+
if (stdout.length < max) stdout += s;
|
|
576
|
+
spool.write(s);
|
|
577
|
+
queue.push({ kind: "out", data: s });
|
|
578
|
+
wake();
|
|
579
|
+
if (!paused && queue.length >= maxQueue) {
|
|
580
|
+
paused = true;
|
|
581
|
+
child.stdout?.pause();
|
|
582
|
+
child.stderr?.pause();
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
const onErr = (c) => {
|
|
586
|
+
const s = c.toString();
|
|
587
|
+
if (stderr.length < max) stderr += s;
|
|
588
|
+
spool.write(s);
|
|
589
|
+
queue.push({ kind: "err", data: s });
|
|
590
|
+
wake();
|
|
591
|
+
if (!paused && queue.length >= maxQueue) {
|
|
592
|
+
paused = true;
|
|
593
|
+
child.stdout?.pause();
|
|
594
|
+
child.stderr?.pause();
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
child.stdout?.on("data", onOut);
|
|
598
|
+
child.stderr?.on("data", onErr);
|
|
599
|
+
child.on("error", (e) => {
|
|
600
|
+
error = e.message;
|
|
601
|
+
queue.push({ kind: "error", data: e.message });
|
|
602
|
+
wake();
|
|
603
|
+
});
|
|
604
|
+
child.on("close", (code) => {
|
|
605
|
+
if (typeof pid === "number") registry.unregister(pid);
|
|
606
|
+
queue.push({ kind: "close", data: "", code: code ?? 0 });
|
|
607
|
+
wake();
|
|
608
|
+
});
|
|
609
|
+
const onAbort = () => {
|
|
610
|
+
if (typeof pid === "number") {
|
|
611
|
+
registry.kill(pid, { force: true });
|
|
612
|
+
} else {
|
|
613
|
+
try {
|
|
614
|
+
child.kill("SIGKILL");
|
|
615
|
+
} catch {
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
queue.push({ kind: "close", data: "", code: 124 });
|
|
619
|
+
wake();
|
|
620
|
+
};
|
|
621
|
+
if (opts.signal.aborted) onAbort();
|
|
622
|
+
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
623
|
+
let exitCode = 0;
|
|
624
|
+
let spawnFailed = false;
|
|
625
|
+
try {
|
|
626
|
+
for (; ; ) {
|
|
627
|
+
while (queue.length === 0) {
|
|
628
|
+
await new Promise((resolve7) => {
|
|
629
|
+
waiter = resolve7;
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
const chunk = queue.shift();
|
|
633
|
+
resume();
|
|
634
|
+
if (chunk.kind === "close") {
|
|
635
|
+
if (!spawnFailed) exitCode = chunk.code ?? 0;
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
if (chunk.kind === "error") {
|
|
639
|
+
spawnFailed = true;
|
|
640
|
+
exitCode = 1;
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
pending2 += chunk.data;
|
|
644
|
+
if (pending2.length >= flushAt) {
|
|
645
|
+
yield { type: "partial_output", text: pending2 };
|
|
646
|
+
pending2 = "";
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (pending2.length > 0) {
|
|
650
|
+
yield { type: "partial_output", text: pending2 };
|
|
651
|
+
}
|
|
652
|
+
const spooled = spool.finalize();
|
|
653
|
+
return {
|
|
654
|
+
// The marker rides on stdout's tail so every consumer's head+tail
|
|
655
|
+
// normalization keeps it without per-tool changes.
|
|
656
|
+
stdout: spooled ? stdout + spoolNote(spooled) : stdout,
|
|
657
|
+
stderr,
|
|
658
|
+
exitCode,
|
|
659
|
+
truncated: stdout.length >= max || stderr.length >= max,
|
|
660
|
+
error,
|
|
661
|
+
spoolPath: spooled?.path,
|
|
662
|
+
spoolBytes: spooled?.bytes
|
|
663
|
+
};
|
|
664
|
+
} finally {
|
|
665
|
+
spool.finalize();
|
|
666
|
+
opts.signal.removeEventListener("abort", onAbort);
|
|
667
|
+
child.stdout?.off("data", onOut);
|
|
668
|
+
child.stderr?.off("data", onErr);
|
|
669
|
+
child.stdout?.destroy();
|
|
670
|
+
child.stderr?.destroy();
|
|
671
|
+
if (child.exitCode === null && !child.killed) {
|
|
672
|
+
if (typeof pid === "number") {
|
|
673
|
+
registry.kill(pid, { force: true });
|
|
674
|
+
} else {
|
|
675
|
+
try {
|
|
676
|
+
child.kill("SIGKILL");
|
|
677
|
+
} catch {
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
async function detectPackageManager(cwd) {
|
|
684
|
+
const { stat: stat11 } = await import('node:fs/promises');
|
|
685
|
+
try {
|
|
686
|
+
await stat11(`${cwd}/pnpm-lock.yaml`);
|
|
687
|
+
return "pnpm";
|
|
688
|
+
} catch {
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
await stat11(`${cwd}/yarn.lock`);
|
|
692
|
+
return "yarn";
|
|
693
|
+
} catch {
|
|
694
|
+
}
|
|
695
|
+
return "npm";
|
|
696
|
+
}
|
|
697
|
+
function resolvePath(input, ctx) {
|
|
698
|
+
return path3.isAbsolute(input) ? path3.normalize(input) : path3.resolve(ctx.workingDir ?? ctx.cwd, input);
|
|
699
|
+
}
|
|
700
|
+
function ensureInsideRoot(absPath, ctx) {
|
|
701
|
+
const root = path3.resolve(ctx.projectRoot);
|
|
702
|
+
const target = path3.resolve(absPath);
|
|
703
|
+
const rel = path3.relative(root, target);
|
|
704
|
+
if (rel.startsWith("..") || path3.isAbsolute(rel)) {
|
|
705
|
+
throw new Error(`Path "${absPath}" is outside project root "${root}"`);
|
|
706
|
+
}
|
|
707
|
+
return target;
|
|
708
|
+
}
|
|
709
|
+
function safeResolve(input, ctx) {
|
|
710
|
+
return ensureInsideRoot(resolvePath(input, ctx), ctx);
|
|
711
|
+
}
|
|
712
|
+
async function assertRealInsideRoot(absPath, ctx) {
|
|
713
|
+
const realRoot = await fs14.realpath(ctx.projectRoot).catch(() => path3.resolve(ctx.projectRoot));
|
|
714
|
+
let probe = absPath;
|
|
715
|
+
for (; ; ) {
|
|
716
|
+
let real;
|
|
717
|
+
try {
|
|
718
|
+
real = await fs14.realpath(probe);
|
|
719
|
+
} catch (err) {
|
|
720
|
+
if (err.code === "ENOENT") {
|
|
721
|
+
const parent = path3.dirname(probe);
|
|
722
|
+
if (parent === probe) return;
|
|
723
|
+
probe = parent;
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
throw err;
|
|
730
727
|
}
|
|
731
|
-
|
|
732
|
-
|
|
728
|
+
const rel = path3.relative(realRoot, real);
|
|
729
|
+
if (rel.startsWith("..") || path3.isAbsolute(rel)) {
|
|
730
|
+
throw new Error(
|
|
731
|
+
`Path "${absPath}" resolves through a symlink outside project root "${realRoot}"`
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
return;
|
|
733
735
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
736
|
+
}
|
|
737
|
+
async function safeResolveReal(input, ctx) {
|
|
738
|
+
const abs = safeResolve(input, ctx);
|
|
739
|
+
await assertRealInsideRoot(abs, ctx);
|
|
740
|
+
return abs;
|
|
741
|
+
}
|
|
742
|
+
function truncateMiddle(s, max) {
|
|
743
|
+
if (Buffer.byteLength(s, "utf8") <= max) return s;
|
|
744
|
+
const half = Math.floor(max / 2);
|
|
745
|
+
return s.slice(0, half) + `
|
|
746
|
+
\u2026[truncated ${Buffer.byteLength(s, "utf8") - max} bytes from middle]\u2026
|
|
747
|
+
` + s.slice(-half);
|
|
748
|
+
}
|
|
749
|
+
function isBinaryBuffer(buf) {
|
|
750
|
+
const len = Math.min(buf.length, 8192);
|
|
751
|
+
for (let i = 0; i < len; i++) {
|
|
752
|
+
if (buf[i] === 0) return true;
|
|
753
|
+
}
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
var COMMAND_OUTPUT_MAX_BYTES = 32768;
|
|
757
|
+
var REPEAT_RUN_THRESHOLD = 3;
|
|
758
|
+
function collapseCarriageReturns(text) {
|
|
759
|
+
const lf = text.replace(/\r\n/g, "\n");
|
|
760
|
+
if (!lf.includes("\r")) return lf;
|
|
761
|
+
return lf.split("\n").map((line) => line.includes("\r") ? line.slice(line.lastIndexOf("\r") + 1) : line).join("\n");
|
|
762
|
+
}
|
|
763
|
+
function collapseConsecutiveDuplicates(text, minRun = REPEAT_RUN_THRESHOLD) {
|
|
764
|
+
const lines = text.split("\n");
|
|
765
|
+
const out = [];
|
|
766
|
+
let i = 0;
|
|
767
|
+
while (i < lines.length) {
|
|
768
|
+
let j = i + 1;
|
|
769
|
+
while (j < lines.length && lines[j] === lines[i]) j++;
|
|
770
|
+
const run = j - i;
|
|
771
|
+
if (run >= minRun) {
|
|
772
|
+
out.push(lines[i], `\u2026 \u27E8repeated ${run}\xD7\u27E9`);
|
|
773
|
+
} else {
|
|
774
|
+
for (let k = i; k < j; k++) out.push(lines[k]);
|
|
744
775
|
}
|
|
745
|
-
|
|
776
|
+
i = j;
|
|
746
777
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
778
|
+
return out.join("\n");
|
|
779
|
+
}
|
|
780
|
+
function takeHeadBytes(s, maxBytes) {
|
|
781
|
+
if (maxBytes <= 0) return "";
|
|
782
|
+
if (Buffer.byteLength(s, "utf8") <= maxBytes) return s;
|
|
783
|
+
let lo = 0;
|
|
784
|
+
let hi = s.length;
|
|
785
|
+
while (lo < hi) {
|
|
786
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
787
|
+
if (Buffer.byteLength(s.slice(0, mid), "utf8") <= maxBytes) lo = mid;
|
|
788
|
+
else hi = mid - 1;
|
|
789
|
+
}
|
|
790
|
+
return s.slice(0, lo);
|
|
791
|
+
}
|
|
792
|
+
function takeTailBytes(s, maxBytes) {
|
|
793
|
+
if (maxBytes <= 0) return "";
|
|
794
|
+
if (Buffer.byteLength(s, "utf8") <= maxBytes) return s;
|
|
795
|
+
let lo = 0;
|
|
796
|
+
let hi = s.length;
|
|
797
|
+
while (lo < hi) {
|
|
798
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
799
|
+
if (Buffer.byteLength(s.slice(s.length - mid), "utf8") <= maxBytes) lo = mid;
|
|
800
|
+
else hi = mid - 1;
|
|
801
|
+
}
|
|
802
|
+
return s.slice(s.length - lo);
|
|
803
|
+
}
|
|
804
|
+
function truncateHeadTail(s, maxBytes) {
|
|
805
|
+
const total = Buffer.byteLength(s, "utf8");
|
|
806
|
+
if (total <= maxBytes) return s;
|
|
807
|
+
const MARKER_RESERVE = 64;
|
|
808
|
+
const avail = Math.max(0, maxBytes - MARKER_RESERVE);
|
|
809
|
+
const headBudget = Math.floor(avail * 0.45);
|
|
810
|
+
const head = takeHeadBytes(s, headBudget);
|
|
811
|
+
const tail = takeTailBytes(s, avail - Buffer.byteLength(head, "utf8"));
|
|
812
|
+
const kept = Buffer.byteLength(head, "utf8") + Buffer.byteLength(tail, "utf8");
|
|
813
|
+
return `${head}
|
|
814
|
+
\u2026[truncated ${total - kept} bytes]\u2026
|
|
815
|
+
${tail}`;
|
|
816
|
+
}
|
|
817
|
+
function normalizeCommandOutput(raw, opts = {}) {
|
|
818
|
+
if (!raw) return raw;
|
|
819
|
+
let text = Core.stripAnsi(raw);
|
|
820
|
+
text = collapseCarriageReturns(text);
|
|
821
|
+
text = text.replace(/[ \t]+$/gm, "");
|
|
822
|
+
text = collapseConsecutiveDuplicates(text);
|
|
823
|
+
text = text.replace(/\n{3,}/g, "\n\n");
|
|
824
|
+
return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/audit.ts
|
|
828
|
+
var auditTool = {
|
|
829
|
+
name: "audit",
|
|
830
|
+
category: "Package Management",
|
|
831
|
+
description: "Run a security audit against project dependencies (using pnpm/npm audit). Reports known vulnerabilities with severity.",
|
|
832
|
+
usageHint: "CRITICAL SECURITY TOOL:\n\n- Run regularly and especially before any release.\n- Use `level` to focus on high/critical issues.\n- `fix` can attempt automatic remediation for some vulnerabilities.\nThis is one of the most important tools for supply chain security.",
|
|
833
|
+
permission: "confirm",
|
|
834
|
+
mutating: false,
|
|
835
|
+
timeoutMs: 6e4,
|
|
836
|
+
inputSchema: {
|
|
837
|
+
type: "object",
|
|
838
|
+
properties: {
|
|
839
|
+
cwd: { type: "string", description: "Working directory (default: cwd)" },
|
|
840
|
+
level: {
|
|
841
|
+
type: "string",
|
|
842
|
+
enum: ["low", "moderate", "high", "critical"],
|
|
843
|
+
description: "Minimum severity level to report"
|
|
844
|
+
},
|
|
845
|
+
fix: { type: "boolean", description: "Attempt to fix vulnerabilities (default: false)" },
|
|
846
|
+
packages: { type: "string", description: "Specific package(s) to audit (comma-separated)" }
|
|
756
847
|
}
|
|
757
|
-
|
|
848
|
+
},
|
|
849
|
+
async execute(input, ctx, opts) {
|
|
850
|
+
let final;
|
|
851
|
+
const executeStream = auditTool.executeStream;
|
|
852
|
+
if (!executeStream) throw new Error("auditTool: stream execution unavailable");
|
|
853
|
+
for await (const ev of executeStream(input, ctx, opts)) {
|
|
854
|
+
if (ev.type === "final") final = ev.output;
|
|
855
|
+
}
|
|
856
|
+
if (!final) throw new Error("audit: stream ended without final event");
|
|
857
|
+
return final;
|
|
858
|
+
},
|
|
859
|
+
async *executeStream(input, ctx, opts) {
|
|
860
|
+
const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
|
|
861
|
+
const manager = await detectPackageManager(cwd);
|
|
862
|
+
yield { type: "log", text: `Auditing with ${manager}\u2026`, data: { manager } };
|
|
863
|
+
const args = ["audit", "--json"];
|
|
864
|
+
if (input.fix) args.push("--fix");
|
|
865
|
+
if (input.packages) {
|
|
866
|
+
const pkgs = Array.isArray(input.packages) ? input.packages : input.packages.split(",");
|
|
867
|
+
args.push(...pkgs.map((p) => p.trim()));
|
|
868
|
+
}
|
|
869
|
+
const result = yield* spawnStream({
|
|
870
|
+
cmd: manager,
|
|
871
|
+
args,
|
|
872
|
+
cwd,
|
|
873
|
+
signal: opts.signal,
|
|
874
|
+
maxBytes: 1e5
|
|
875
|
+
});
|
|
876
|
+
yield { type: "final", output: parseAuditOutput(result.stdout, result.exitCode) };
|
|
758
877
|
}
|
|
759
878
|
};
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
879
|
+
function parseAuditOutput(json, exitCode) {
|
|
880
|
+
if (!json) {
|
|
881
|
+
return {
|
|
882
|
+
exit_code: exitCode,
|
|
883
|
+
vulnerabilities: [],
|
|
884
|
+
total: 0,
|
|
885
|
+
summary: exitCode === 0 ? "No vulnerabilities found" : "Audit failed",
|
|
886
|
+
output: "",
|
|
887
|
+
truncated: false
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
try {
|
|
891
|
+
const data = JSON.parse(json);
|
|
892
|
+
const advisories = [];
|
|
893
|
+
const ads = data.advisories ?? {};
|
|
894
|
+
for (const id of Object.keys(ads)) {
|
|
895
|
+
const adv = ads[id];
|
|
896
|
+
advisories.push({
|
|
897
|
+
severity: adv.severity ?? "unknown",
|
|
898
|
+
package: adv.module_name ?? id,
|
|
899
|
+
title: adv.title ?? "Unknown vulnerability",
|
|
900
|
+
url: adv.url ?? ""
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
const total = advisories.length;
|
|
904
|
+
const summary = total === 0 ? "No vulnerabilities found" : `Found ${total} vulnerabilities: ${advisories.filter((a) => a.severity === "critical").length} critical, ${advisories.filter((a) => a.severity === "high").length} high`;
|
|
905
|
+
return {
|
|
906
|
+
exit_code: exitCode,
|
|
907
|
+
vulnerabilities: advisories,
|
|
908
|
+
total,
|
|
909
|
+
summary,
|
|
910
|
+
output: json,
|
|
911
|
+
truncated: json.length >= 1e5
|
|
912
|
+
};
|
|
913
|
+
} catch {
|
|
914
|
+
return {
|
|
915
|
+
exit_code: exitCode,
|
|
916
|
+
vulnerabilities: [],
|
|
917
|
+
total: 0,
|
|
918
|
+
summary: "Could not parse audit output",
|
|
919
|
+
output: json,
|
|
920
|
+
truncated: false
|
|
921
|
+
};
|
|
764
922
|
}
|
|
765
|
-
return _registry;
|
|
766
923
|
}
|
|
767
924
|
|
|
768
925
|
// src/bash.ts
|
|
@@ -858,7 +1015,7 @@ var bashTool = {
|
|
|
858
1015
|
})();
|
|
859
1016
|
const args = isWin ? ["/c", input.command] : ["-c", input.command];
|
|
860
1017
|
const env = buildChildEnv(ctx.session?.id);
|
|
861
|
-
const detached = isWin
|
|
1018
|
+
const detached = !isWin;
|
|
862
1019
|
const startedAt = Date.now();
|
|
863
1020
|
if (input.background) {
|
|
864
1021
|
let buf2 = "";
|
|
@@ -867,10 +1024,14 @@ var bashTool = {
|
|
|
867
1024
|
cwd: ctx.projectRoot,
|
|
868
1025
|
env,
|
|
869
1026
|
stdio: ["ignore", "pipe", "pipe"],
|
|
870
|
-
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
//
|
|
1027
|
+
// win32: CreateProcess IGNORES CREATE_NO_WINDOW (windowsHide) when
|
|
1028
|
+
// DETACHED_PROCESS (detached: true) is set, so the console-less
|
|
1029
|
+
// cmd.exe's grandchildren (node, dev servers) each allocate a fresh
|
|
1030
|
+
// VISIBLE console window. detached: false lets CREATE_NO_WINDOW
|
|
1031
|
+
// apply: the child gets a hidden console that grandchildren inherit.
|
|
1032
|
+
// Windows children survive parent exit either way. POSIX keeps
|
|
1033
|
+
// detached for the process-group kill semantics.
|
|
1034
|
+
detached: !isWin,
|
|
874
1035
|
windowsHide: true,
|
|
875
1036
|
signal: opts.signal
|
|
876
1037
|
});
|
|
@@ -886,24 +1047,22 @@ var bashTool = {
|
|
|
886
1047
|
});
|
|
887
1048
|
child2.on("close", () => registry.unregister(pid2));
|
|
888
1049
|
}
|
|
889
|
-
|
|
890
|
-
if (
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
}
|
|
895
|
-
if (buf2.length >= MAX_OUTPUT) truncated = true;
|
|
1050
|
+
const onBgData = (chunk) => {
|
|
1051
|
+
if (truncated) return;
|
|
1052
|
+
const remain = MAX_OUTPUT - buf2.length;
|
|
1053
|
+
if (remain > 0) {
|
|
1054
|
+
buf2 += chunk.toString().slice(0, remain);
|
|
896
1055
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
if (remain > 0) {
|
|
902
|
-
buf2 += chunk.toString().slice(0, remain);
|
|
903
|
-
}
|
|
904
|
-
if (buf2.length >= MAX_OUTPUT) truncated = true;
|
|
1056
|
+
if (buf2.length >= MAX_OUTPUT) {
|
|
1057
|
+
truncated = true;
|
|
1058
|
+
child2.stdout?.off("data", onBgData);
|
|
1059
|
+
child2.stderr?.off("data", onBgData);
|
|
905
1060
|
}
|
|
906
|
-
}
|
|
1061
|
+
};
|
|
1062
|
+
child2.stdout?.on("data", onBgData);
|
|
1063
|
+
child2.stderr?.on("data", onBgData);
|
|
1064
|
+
child2.stdout?.unref?.();
|
|
1065
|
+
child2.stderr?.unref?.();
|
|
907
1066
|
child2.on("close", () => {
|
|
908
1067
|
registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
|
|
909
1068
|
});
|
|
@@ -942,6 +1101,7 @@ var bashTool = {
|
|
|
942
1101
|
let pending2 = "";
|
|
943
1102
|
let timedOut = false;
|
|
944
1103
|
const timers = [];
|
|
1104
|
+
const spool = createOutputSpool({ tool: "bash", thresholdBytes: MAX_OUTPUT });
|
|
945
1105
|
function killWithTimeout(child2, timeoutMs2) {
|
|
946
1106
|
if (isWin) {
|
|
947
1107
|
if (typeof child2.pid === "number" && child2.exitCode === null && killWin32Tree(child2.pid)) {
|
|
@@ -1047,6 +1207,7 @@ var bashTool = {
|
|
|
1047
1207
|
if (buf.length < MAX_OUTPUT) {
|
|
1048
1208
|
buf += text.slice(0, MAX_OUTPUT - buf.length);
|
|
1049
1209
|
}
|
|
1210
|
+
spool.write(text);
|
|
1050
1211
|
pending2 += text;
|
|
1051
1212
|
push({ kind: "data", text });
|
|
1052
1213
|
pauseIfFlooded();
|
|
@@ -1074,10 +1235,11 @@ var bashTool = {
|
|
|
1074
1235
|
if (remainder !== null) {
|
|
1075
1236
|
yield { type: "partial_output", text: remainder };
|
|
1076
1237
|
}
|
|
1238
|
+
const spooled = spool.finalize();
|
|
1077
1239
|
yield {
|
|
1078
1240
|
type: "final",
|
|
1079
1241
|
output: {
|
|
1080
|
-
output: normalizeCommandOutput(buf),
|
|
1242
|
+
output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : ""),
|
|
1081
1243
|
exit_code: c.code,
|
|
1082
1244
|
timed_out: timedOut
|
|
1083
1245
|
}
|
|
@@ -1092,6 +1254,7 @@ var bashTool = {
|
|
|
1092
1254
|
}
|
|
1093
1255
|
} finally {
|
|
1094
1256
|
for (const t of timers) clearTimeout(t);
|
|
1257
|
+
spool.finalize();
|
|
1095
1258
|
if (isWin) opts.signal.removeEventListener("abort", onAbort);
|
|
1096
1259
|
child.stdout?.off("data", onData);
|
|
1097
1260
|
child.stderr?.off("data", onData);
|
|
@@ -1515,7 +1678,7 @@ var IndexStore = class {
|
|
|
1515
1678
|
this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
|
|
1516
1679
|
fs.mkdirSync(this.indexDir, { recursive: true });
|
|
1517
1680
|
const Database = loadDatabaseSync();
|
|
1518
|
-
this.db = new Database(
|
|
1681
|
+
this.db = new Database(path3.join(this.indexDir, DB_FILE));
|
|
1519
1682
|
try {
|
|
1520
1683
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
1521
1684
|
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
@@ -1955,7 +2118,7 @@ var IndexStore = class {
|
|
|
1955
2118
|
}));
|
|
1956
2119
|
}
|
|
1957
2120
|
sizeBytes() {
|
|
1958
|
-
const dbPath =
|
|
2121
|
+
const dbPath = path3.join(this.indexDir, DB_FILE);
|
|
1959
2122
|
try {
|
|
1960
2123
|
return fs.statSync(dbPath).size;
|
|
1961
2124
|
} catch {
|
|
@@ -2394,10 +2557,10 @@ func formatType(t ast.Expr) string {
|
|
|
2394
2557
|
}
|
|
2395
2558
|
`;
|
|
2396
2559
|
function syncGoParse(filePath, content, lang) {
|
|
2397
|
-
const tmpDir =
|
|
2560
|
+
const tmpDir = path3.join(os.tmpdir(), "ws-go-parse");
|
|
2398
2561
|
try {
|
|
2399
2562
|
mkdirSync(tmpDir, { recursive: true });
|
|
2400
|
-
const scriptPath =
|
|
2563
|
+
const scriptPath = path3.join(tmpDir, "parse.go");
|
|
2401
2564
|
writeFileSync(scriptPath, GO_PARSE_SCRIPT, "utf8");
|
|
2402
2565
|
const stdout = execFileSync("go", ["run", scriptPath], {
|
|
2403
2566
|
input: content,
|
|
@@ -2641,9 +2804,9 @@ print(json.dumps([s.to_dict() for s in syms]))
|
|
|
2641
2804
|
`;
|
|
2642
2805
|
function syncPyParse(filePath, lang) {
|
|
2643
2806
|
try {
|
|
2644
|
-
const tmpDir =
|
|
2807
|
+
const tmpDir = path3.join(os.tmpdir(), "ws-py-parse");
|
|
2645
2808
|
mkdirSync(tmpDir, { recursive: true });
|
|
2646
|
-
const scriptPath =
|
|
2809
|
+
const scriptPath = path3.join(tmpDir, "parse.py");
|
|
2647
2810
|
writeFileSync(scriptPath, PY_PARSE_SCRIPT, "utf8");
|
|
2648
2811
|
const stdout = execFileSync("python", [scriptPath, filePath], {
|
|
2649
2812
|
timeout: 15e3,
|
|
@@ -2684,7 +2847,7 @@ function parseSymbols4(opts) {
|
|
|
2684
2847
|
function checkNativeParser() {
|
|
2685
2848
|
try {
|
|
2686
2849
|
execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
|
|
2687
|
-
const toolsDir =
|
|
2850
|
+
const toolsDir = path3.join(process.cwd(), "tools");
|
|
2688
2851
|
try {
|
|
2689
2852
|
execFileSync(
|
|
2690
2853
|
"cargo",
|
|
@@ -2694,7 +2857,7 @@ function checkNativeParser() {
|
|
|
2694
2857
|
"--format-version",
|
|
2695
2858
|
"1",
|
|
2696
2859
|
"--manifest-path",
|
|
2697
|
-
|
|
2860
|
+
path3.join(toolsDir, "Cargo.toml")
|
|
2698
2861
|
],
|
|
2699
2862
|
{ stdio: "pipe", windowsHide: true }
|
|
2700
2863
|
);
|
|
@@ -2708,13 +2871,13 @@ function checkNativeParser() {
|
|
|
2708
2871
|
}
|
|
2709
2872
|
function tryNativeParse(file, content) {
|
|
2710
2873
|
try {
|
|
2711
|
-
const toolsDir =
|
|
2712
|
-
const crateDir =
|
|
2713
|
-
const tmpFile =
|
|
2874
|
+
const toolsDir = path3.join(process.cwd(), "tools");
|
|
2875
|
+
const crateDir = path3.join(toolsDir, "syn-parser");
|
|
2876
|
+
const tmpFile = path3.join(crateDir, "src", "input.rs");
|
|
2714
2877
|
writeFileSync(tmpFile, content, "utf8");
|
|
2715
2878
|
const result = spawnSync(
|
|
2716
2879
|
"cargo",
|
|
2717
|
-
["run", "--manifest-path",
|
|
2880
|
+
["run", "--manifest-path", path3.join(toolsDir, "Cargo.toml")],
|
|
2718
2881
|
{
|
|
2719
2882
|
cwd: process.cwd(),
|
|
2720
2883
|
encoding: "utf8",
|
|
@@ -2813,7 +2976,7 @@ function parseSymbols5(opts) {
|
|
|
2813
2976
|
function regexParse2(opts) {
|
|
2814
2977
|
const { file, content, lang } = opts;
|
|
2815
2978
|
const symbols = [];
|
|
2816
|
-
const basename2 =
|
|
2979
|
+
const basename2 = path3.basename(file).toLowerCase();
|
|
2817
2980
|
const isPackageJson = basename2 === "package.json";
|
|
2818
2981
|
const isTsconfig = basename2 === "tsconfig.json" || basename2 === "tsconfig.build.json";
|
|
2819
2982
|
const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
|
|
@@ -2839,11 +3002,11 @@ function regexParse2(opts) {
|
|
|
2839
3002
|
const line = lineFromOffset(offset);
|
|
2840
3003
|
symbols.push(
|
|
2841
3004
|
makeSymbol({
|
|
2842
|
-
name:
|
|
3005
|
+
name: path3.basename(file),
|
|
2843
3006
|
kind: "object",
|
|
2844
3007
|
line,
|
|
2845
3008
|
col: 0,
|
|
2846
|
-
signature: `"${
|
|
3009
|
+
signature: `"${path3.basename(file)}" = { ... }`,
|
|
2847
3010
|
file,
|
|
2848
3011
|
lang
|
|
2849
3012
|
})
|
|
@@ -3193,7 +3356,7 @@ function compileGitignore(lines) {
|
|
|
3193
3356
|
async function loadGitignoreMatcher(projectRoot) {
|
|
3194
3357
|
let lines = [];
|
|
3195
3358
|
try {
|
|
3196
|
-
const raw = await fs14.readFile(
|
|
3359
|
+
const raw = await fs14.readFile(path3.join(projectRoot, ".gitignore"), "utf8");
|
|
3197
3360
|
lines = raw.split("\n");
|
|
3198
3361
|
} catch {
|
|
3199
3362
|
}
|
|
@@ -3257,14 +3420,14 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
3257
3420
|
dirCount++;
|
|
3258
3421
|
for (const e of entries) {
|
|
3259
3422
|
if (ignoreSet.has(e.name)) continue;
|
|
3260
|
-
const full =
|
|
3261
|
-
const rel =
|
|
3423
|
+
const full = path3.join(dir, e.name);
|
|
3424
|
+
const rel = path3.relative(projectRoot, full).replace(/\\/g, "/");
|
|
3262
3425
|
if (e.isDirectory()) {
|
|
3263
3426
|
if (isGitIgnored(rel, true)) continue;
|
|
3264
3427
|
await walk(full);
|
|
3265
3428
|
} else if (e.isFile()) {
|
|
3266
3429
|
if (isGitIgnored(rel, false)) continue;
|
|
3267
|
-
const ext =
|
|
3430
|
+
const ext = path3.extname(e.name);
|
|
3268
3431
|
for (const { ext: extName, pat } of globs) {
|
|
3269
3432
|
if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
|
|
3270
3433
|
results.push(full);
|
|
@@ -3319,7 +3482,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3319
3482
|
const isGitIgnored = await loadGitignoreMatcher(projectRoot);
|
|
3320
3483
|
let files;
|
|
3321
3484
|
if (opts.files && opts.files.length > 0) {
|
|
3322
|
-
files = opts.files.map((f) =>
|
|
3485
|
+
files = opts.files.map((f) => path3.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path3.relative(projectRoot, f).replace(/\\/g, "/"), false));
|
|
3323
3486
|
} else {
|
|
3324
3487
|
files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
|
|
3325
3488
|
}
|
|
@@ -3342,20 +3505,20 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3342
3505
|
await yieldEventLoop();
|
|
3343
3506
|
throwIfAborted(signal);
|
|
3344
3507
|
}
|
|
3345
|
-
let
|
|
3508
|
+
let stat11;
|
|
3346
3509
|
try {
|
|
3347
3510
|
const statOpts = signal ? { signal } : {};
|
|
3348
|
-
|
|
3511
|
+
stat11 = await fs14.stat(file, statOpts);
|
|
3349
3512
|
} catch (e) {
|
|
3350
3513
|
if (isAbortError(e)) throw e;
|
|
3351
3514
|
store.deleteFile(file);
|
|
3352
3515
|
continue;
|
|
3353
3516
|
}
|
|
3354
|
-
if (!
|
|
3517
|
+
if (!stat11.isFile()) continue;
|
|
3355
3518
|
const lang = detectLang(file);
|
|
3356
3519
|
if (!lang) continue;
|
|
3357
3520
|
const meta = existingMeta.get(file);
|
|
3358
|
-
if (!force && meta && meta.mtimeMs === Math.floor(
|
|
3521
|
+
if (!force && meta && meta.mtimeMs === Math.floor(stat11.mtimeMs)) {
|
|
3359
3522
|
langStats[lang] = (langStats[lang] ?? 0) + meta.symbolCount;
|
|
3360
3523
|
symbolsIndexed += meta.symbolCount;
|
|
3361
3524
|
filesIndexed++;
|
|
@@ -3382,7 +3545,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3382
3545
|
store.upsertFile({
|
|
3383
3546
|
file,
|
|
3384
3547
|
lang,
|
|
3385
|
-
mtimeMs: Math.floor(
|
|
3548
|
+
mtimeMs: Math.floor(stat11.mtimeMs),
|
|
3386
3549
|
symbolCount: 0,
|
|
3387
3550
|
lastIndexed: Date.now()
|
|
3388
3551
|
});
|
|
@@ -3408,7 +3571,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3408
3571
|
store.upsertFile({
|
|
3409
3572
|
file,
|
|
3410
3573
|
lang,
|
|
3411
|
-
mtimeMs: Math.floor(
|
|
3574
|
+
mtimeMs: Math.floor(stat11.mtimeMs),
|
|
3412
3575
|
symbolCount: count,
|
|
3413
3576
|
lastIndexed: Date.now()
|
|
3414
3577
|
});
|
|
@@ -3666,6 +3829,13 @@ function circuitOpenError() {
|
|
|
3666
3829
|
"Codebase indexing is temporarily paused after repeated failures" + (c.lastFailure ? ` (last: ${c.lastFailure})` : "") + (c.cooldownRemainingMs > 0 ? `; auto-retry in ${Math.ceil(c.cooldownRemainingMs / 1e3)}s` : "") + ". Use /codebase-reindex to retry now."
|
|
3667
3830
|
);
|
|
3668
3831
|
}
|
|
3832
|
+
function isUniqueConstraintError(err) {
|
|
3833
|
+
if (err instanceof Error) {
|
|
3834
|
+
const msg = err.message.toLowerCase();
|
|
3835
|
+
return msg.includes("unique constraint") || msg.includes("UNIQUE constraint");
|
|
3836
|
+
}
|
|
3837
|
+
return false;
|
|
3838
|
+
}
|
|
3669
3839
|
async function runStartupIndex(opts) {
|
|
3670
3840
|
if (!indexCircuitBreaker.allowRequest()) throw circuitOpenError();
|
|
3671
3841
|
_indexing = true;
|
|
@@ -3695,6 +3865,15 @@ async function runStartupIndex(opts) {
|
|
|
3695
3865
|
return result;
|
|
3696
3866
|
} catch (err) {
|
|
3697
3867
|
_lastError = err instanceof Error ? err.message : String(err);
|
|
3868
|
+
if (isUniqueConstraintError(err) && !opts.force) {
|
|
3869
|
+
_lastError = null;
|
|
3870
|
+
const rebuildResult = await runStartupIndex({
|
|
3871
|
+
...opts,
|
|
3872
|
+
force: true
|
|
3873
|
+
});
|
|
3874
|
+
_ready = true;
|
|
3875
|
+
return rebuildResult;
|
|
3876
|
+
}
|
|
3698
3877
|
_ready = true;
|
|
3699
3878
|
if (!opts.signal?.aborted) indexCircuitBreaker.recordFailure(err);
|
|
3700
3879
|
throw err;
|
|
@@ -3997,11 +4176,11 @@ function findGitDir(cwd) {
|
|
|
3997
4176
|
let dir = cwd;
|
|
3998
4177
|
for (let i = 0; i < 20; i++) {
|
|
3999
4178
|
try {
|
|
4000
|
-
const
|
|
4001
|
-
if (
|
|
4179
|
+
const stat11 = statSync(path3.join(dir, ".git"));
|
|
4180
|
+
if (stat11.isDirectory()) return dir;
|
|
4002
4181
|
} catch {
|
|
4003
4182
|
}
|
|
4004
|
-
const parent =
|
|
4183
|
+
const parent = path3.dirname(dir);
|
|
4005
4184
|
if (parent === dir) break;
|
|
4006
4185
|
dir = parent;
|
|
4007
4186
|
}
|
|
@@ -4042,8 +4221,8 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
4042
4221
|
const results = [];
|
|
4043
4222
|
for (const file of files) {
|
|
4044
4223
|
const absPath = safeResolve(file, ctx);
|
|
4045
|
-
const
|
|
4046
|
-
if (!
|
|
4224
|
+
const stat11 = await fs14.stat(absPath).catch(() => null);
|
|
4225
|
+
if (!stat11?.isFile()) continue;
|
|
4047
4226
|
const content = await fs14.readFile(absPath, "utf8");
|
|
4048
4227
|
const lines = content.split(/\r?\n/);
|
|
4049
4228
|
results.push(formatWithLineNumbers(file, lines));
|
|
@@ -4142,8 +4321,8 @@ async function resolveFiles(filesInput, cwd) {
|
|
|
4142
4321
|
for (const f of files) {
|
|
4143
4322
|
const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
|
|
4144
4323
|
try {
|
|
4145
|
-
const
|
|
4146
|
-
if (
|
|
4324
|
+
const stat11 = await fs14.stat(absPath);
|
|
4325
|
+
if (stat11.isFile()) resolved.push(absPath);
|
|
4147
4326
|
} catch {
|
|
4148
4327
|
}
|
|
4149
4328
|
}
|
|
@@ -4234,13 +4413,13 @@ var editTool = {
|
|
|
4234
4413
|
if (input.new_string === void 0) throw new Error("edit: new_string is required");
|
|
4235
4414
|
if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
|
|
4236
4415
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
4237
|
-
const
|
|
4416
|
+
const stat11 = await fs14.stat(absPath).catch((err) => {
|
|
4238
4417
|
if (err.code === "ENOENT") {
|
|
4239
4418
|
throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
|
|
4240
4419
|
}
|
|
4241
4420
|
throw err;
|
|
4242
4421
|
});
|
|
4243
|
-
if (!
|
|
4422
|
+
if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
|
|
4244
4423
|
if (!ctx.hasRead(absPath)) {
|
|
4245
4424
|
throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
|
|
4246
4425
|
}
|
|
@@ -4510,9 +4689,9 @@ var execTool = {
|
|
|
4510
4689
|
allowed: false
|
|
4511
4690
|
};
|
|
4512
4691
|
}
|
|
4513
|
-
const requestedCwd = input.cwd ?
|
|
4514
|
-
const rel =
|
|
4515
|
-
if (rel.startsWith("..") ||
|
|
4692
|
+
const requestedCwd = input.cwd ? path3.resolve(ctx.projectRoot, input.cwd) : ctx.cwd;
|
|
4693
|
+
const rel = path3.relative(ctx.projectRoot, requestedCwd);
|
|
4694
|
+
if (rel.startsWith("..") || path3.isAbsolute(rel)) {
|
|
4516
4695
|
return {
|
|
4517
4696
|
command: cmd,
|
|
4518
4697
|
args,
|
|
@@ -4534,6 +4713,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
4534
4713
|
let stderr = "";
|
|
4535
4714
|
let killed = false;
|
|
4536
4715
|
const startedAt = Date.now();
|
|
4716
|
+
const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
|
|
4537
4717
|
const resolved = resolveWin32Command(cmd);
|
|
4538
4718
|
const isWin = process.platform === "win32";
|
|
4539
4719
|
const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
@@ -4566,10 +4746,14 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
4566
4746
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
4567
4747
|
}
|
|
4568
4748
|
child.stdout?.on("data", (chunk) => {
|
|
4569
|
-
|
|
4749
|
+
const text = chunk.toString();
|
|
4750
|
+
if (stdout.length < MAX_OUTPUT2) stdout += text;
|
|
4751
|
+
spool.write(text);
|
|
4570
4752
|
});
|
|
4571
4753
|
child.stderr?.on("data", (chunk) => {
|
|
4572
|
-
|
|
4754
|
+
const text = chunk.toString();
|
|
4755
|
+
if (stderr.length < MAX_OUTPUT2) stderr += text;
|
|
4756
|
+
spool.write(text);
|
|
4573
4757
|
});
|
|
4574
4758
|
child.on("close", (code) => {
|
|
4575
4759
|
clearTimeout(timer);
|
|
@@ -4578,10 +4762,11 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
4578
4762
|
const durationMs = Date.now() - startedAt;
|
|
4579
4763
|
const exitCode = killed ? 124 : code ?? 1;
|
|
4580
4764
|
registry.afterCall(durationMs, exitCode !== 0);
|
|
4765
|
+
const spooled = spool.finalize();
|
|
4581
4766
|
resolve7({
|
|
4582
4767
|
command: cmd,
|
|
4583
4768
|
args,
|
|
4584
|
-
stdout: normalizeCommandOutput(stdout),
|
|
4769
|
+
stdout: normalizeCommandOutput(stdout) + (spooled ? spoolNote(spooled) : ""),
|
|
4585
4770
|
stderr: normalizeCommandOutput(stderr),
|
|
4586
4771
|
exitCode,
|
|
4587
4772
|
truncated: Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES || Buffer.byteLength(stderr, "utf8") > COMMAND_OUTPUT_MAX_BYTES,
|
|
@@ -4593,6 +4778,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
4593
4778
|
if (isWin) signal.removeEventListener("abort", onAbort);
|
|
4594
4779
|
if (typeof pid === "number") registry.unregister(pid);
|
|
4595
4780
|
registry.afterCall(Date.now() - startedAt, true);
|
|
4781
|
+
spool.finalize();
|
|
4596
4782
|
resolve7({
|
|
4597
4783
|
command: cmd,
|
|
4598
4784
|
args,
|
|
@@ -5023,13 +5209,13 @@ var formatTool = {
|
|
|
5023
5209
|
}
|
|
5024
5210
|
};
|
|
5025
5211
|
async function detectFixer(cwd) {
|
|
5026
|
-
const { stat:
|
|
5212
|
+
const { stat: stat11 } = await import('node:fs/promises');
|
|
5027
5213
|
try {
|
|
5028
|
-
await
|
|
5214
|
+
await stat11(`${cwd}/biome.json`);
|
|
5029
5215
|
return "biome";
|
|
5030
5216
|
} catch {
|
|
5031
5217
|
try {
|
|
5032
|
-
await
|
|
5218
|
+
await stat11(`${cwd}/.prettierrc`);
|
|
5033
5219
|
return "prettier";
|
|
5034
5220
|
} catch {
|
|
5035
5221
|
return "biome";
|
|
@@ -5172,8 +5358,8 @@ function findGitDir2(cwd, projectRoot) {
|
|
|
5172
5358
|
let dir = cwd;
|
|
5173
5359
|
for (let i = 0; i < 20; i++) {
|
|
5174
5360
|
try {
|
|
5175
|
-
const
|
|
5176
|
-
if (
|
|
5361
|
+
const stat11 = statSync(`${dir}/.git`);
|
|
5362
|
+
if (stat11.isDirectory() || stat11.isFile()) return dir;
|
|
5177
5363
|
} catch {
|
|
5178
5364
|
}
|
|
5179
5365
|
if (dir === root) break;
|
|
@@ -5347,7 +5533,7 @@ var globTool = {
|
|
|
5347
5533
|
if (DEFAULT_IGNORE2.includes(name)) continue;
|
|
5348
5534
|
if (ignored.includes(name)) continue;
|
|
5349
5535
|
const rel = relPrefix ? `${relPrefix}/${name}` : name;
|
|
5350
|
-
const full =
|
|
5536
|
+
const full = path3.join(dir, name);
|
|
5351
5537
|
if (e.isDirectory()) {
|
|
5352
5538
|
await walk(full, rel);
|
|
5353
5539
|
if (truncated) return;
|
|
@@ -5373,7 +5559,7 @@ var globTool = {
|
|
|
5373
5559
|
};
|
|
5374
5560
|
async function readGitignore(dir) {
|
|
5375
5561
|
try {
|
|
5376
|
-
const raw = await fs14.readFile(
|
|
5562
|
+
const raw = await fs14.readFile(path3.join(dir, ".gitignore"), "utf8");
|
|
5377
5563
|
return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
5378
5564
|
} catch {
|
|
5379
5565
|
return [];
|
|
@@ -5665,15 +5851,15 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
5665
5851
|
if (stopped) return;
|
|
5666
5852
|
if (DEFAULT_IGNORE3.includes(e.name)) continue;
|
|
5667
5853
|
if (e.isSymbolicLink()) continue;
|
|
5668
|
-
const full =
|
|
5854
|
+
const full = path3.join(dir, e.name);
|
|
5669
5855
|
if (e.isDirectory()) {
|
|
5670
5856
|
await walk(full);
|
|
5671
5857
|
} else if (e.isFile()) {
|
|
5672
5858
|
if (globRe && !globRe.test(e.name) && !globRe.test(full)) continue;
|
|
5673
5859
|
if (globRe) globRe.lastIndex = 0;
|
|
5674
5860
|
try {
|
|
5675
|
-
const
|
|
5676
|
-
if (
|
|
5861
|
+
const stat11 = await fs14.stat(full);
|
|
5862
|
+
if (stat11.size > 1e6) continue;
|
|
5677
5863
|
const head = await fs14.readFile(full);
|
|
5678
5864
|
if (isBinaryBuffer(head)) continue;
|
|
5679
5865
|
const text = head.toString("utf8");
|
|
@@ -5921,8 +6107,8 @@ var jsonTool = {
|
|
|
5921
6107
|
};
|
|
5922
6108
|
}
|
|
5923
6109
|
};
|
|
5924
|
-
function query(data,
|
|
5925
|
-
const parts =
|
|
6110
|
+
function query(data, path21) {
|
|
6111
|
+
const parts = path21.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
5926
6112
|
let current = data;
|
|
5927
6113
|
for (const part of parts) {
|
|
5928
6114
|
if (current === null || current === void 0) return void 0;
|
|
@@ -6051,11 +6237,11 @@ var lintTool = {
|
|
|
6051
6237
|
}
|
|
6052
6238
|
};
|
|
6053
6239
|
async function detectLinter(cwd) {
|
|
6054
|
-
const { stat:
|
|
6240
|
+
const { stat: stat11 } = await import('node:fs/promises');
|
|
6055
6241
|
const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
|
|
6056
6242
|
for (const f of checks) {
|
|
6057
6243
|
try {
|
|
6058
|
-
await
|
|
6244
|
+
await stat11(`${cwd}/${f}`);
|
|
6059
6245
|
if (f.includes("biome")) return "biome";
|
|
6060
6246
|
if (f.includes("eslint")) return "eslint";
|
|
6061
6247
|
if (f.includes("tslint")) return "tslint";
|
|
@@ -6193,7 +6379,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
6193
6379
|
}
|
|
6194
6380
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
6195
6381
|
var MAX_TAIL_LINES = 1e5;
|
|
6196
|
-
async function fileLogs(
|
|
6382
|
+
async function fileLogs(path21, lines, filterRe, stream) {
|
|
6197
6383
|
const { createInterface } = await import('node:readline');
|
|
6198
6384
|
const { createReadStream } = await import('node:fs');
|
|
6199
6385
|
const entries = [];
|
|
@@ -6202,7 +6388,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
|
|
|
6202
6388
|
let writeIdx = 0;
|
|
6203
6389
|
let totalLines = 0;
|
|
6204
6390
|
const rl = createInterface({
|
|
6205
|
-
input: createReadStream(
|
|
6391
|
+
input: createReadStream(path21),
|
|
6206
6392
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
6207
6393
|
});
|
|
6208
6394
|
for await (const line of rl) {
|
|
@@ -6223,7 +6409,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
|
|
|
6223
6409
|
if (parsed) entries.push(parsed);
|
|
6224
6410
|
}
|
|
6225
6411
|
return {
|
|
6226
|
-
source:
|
|
6412
|
+
source: path21,
|
|
6227
6413
|
entries,
|
|
6228
6414
|
total: entries.length,
|
|
6229
6415
|
truncated: totalLines > effLines,
|
|
@@ -6392,9 +6578,9 @@ var patchTool = {
|
|
|
6392
6578
|
for (const t of targets) {
|
|
6393
6579
|
const stripped = stripPathComponents(t, strip);
|
|
6394
6580
|
if (!stripped) continue;
|
|
6395
|
-
const candidate =
|
|
6396
|
-
const rel =
|
|
6397
|
-
if (rel.startsWith("..") ||
|
|
6581
|
+
const candidate = path3.resolve(dir, stripped);
|
|
6582
|
+
const rel = path3.relative(ctx.projectRoot, candidate);
|
|
6583
|
+
if (rel.startsWith("..") || path3.isAbsolute(rel)) {
|
|
6398
6584
|
return {
|
|
6399
6585
|
applied: 0,
|
|
6400
6586
|
rejected: 1,
|
|
@@ -6404,11 +6590,11 @@ var patchTool = {
|
|
|
6404
6590
|
};
|
|
6405
6591
|
}
|
|
6406
6592
|
}
|
|
6407
|
-
const tmpDir = await fs14.mkdtemp(
|
|
6593
|
+
const tmpDir = await fs14.mkdtemp(path3.join(os.tmpdir(), ".wstack_patch_"));
|
|
6408
6594
|
try {
|
|
6409
6595
|
await fs14.chmod(tmpDir, 448).catch(() => {
|
|
6410
6596
|
});
|
|
6411
|
-
const patchFile =
|
|
6597
|
+
const patchFile = path3.join(tmpDir, "in.diff");
|
|
6412
6598
|
await fs14.writeFile(patchFile, input.patch, { mode: 384 });
|
|
6413
6599
|
const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
|
|
6414
6600
|
const result = await runPatch(args, dir, opts.signal);
|
|
@@ -6730,9 +6916,9 @@ var readTool = {
|
|
|
6730
6916
|
async execute(input, ctx) {
|
|
6731
6917
|
if (!input?.path) throw new Error("read: path is required");
|
|
6732
6918
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
6733
|
-
let
|
|
6919
|
+
let stat11;
|
|
6734
6920
|
try {
|
|
6735
|
-
|
|
6921
|
+
stat11 = await fs14.stat(absPath);
|
|
6736
6922
|
} catch (err) {
|
|
6737
6923
|
const code = err.code;
|
|
6738
6924
|
if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
|
|
@@ -6740,9 +6926,9 @@ var readTool = {
|
|
|
6740
6926
|
`read: failed to stat "${input.path}": ${err instanceof Error ? err.message : String(err)}`
|
|
6741
6927
|
);
|
|
6742
6928
|
}
|
|
6743
|
-
if (!
|
|
6744
|
-
if (
|
|
6745
|
-
throw new Error(`read: file too large (${
|
|
6929
|
+
if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
|
|
6930
|
+
if (stat11.size > MAX_BYTES2) {
|
|
6931
|
+
throw new Error(`read: file too large (${stat11.size} bytes, limit ${MAX_BYTES2})`);
|
|
6746
6932
|
}
|
|
6747
6933
|
const buf = await fs14.readFile(absPath);
|
|
6748
6934
|
if (isBinaryBuffer(buf)) {
|
|
@@ -6754,14 +6940,14 @@ var readTool = {
|
|
|
6754
6940
|
const offset = Math.max(1, input.offset ?? 1);
|
|
6755
6941
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
6756
6942
|
if (limit === 0) {
|
|
6757
|
-
ctx.recordRead(absPath,
|
|
6943
|
+
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
6758
6944
|
return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
|
|
6759
6945
|
}
|
|
6760
6946
|
const slice = allLines.slice(offset - 1, offset - 1 + limit);
|
|
6761
6947
|
const truncated = offset - 1 + slice.length < total;
|
|
6762
6948
|
const width = String(offset + slice.length - 1).length;
|
|
6763
6949
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
6764
|
-
ctx.recordRead(absPath,
|
|
6950
|
+
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
6765
6951
|
return {
|
|
6766
6952
|
text: numbered,
|
|
6767
6953
|
total_lines: total,
|
|
@@ -6828,10 +7014,10 @@ var replaceTool = {
|
|
|
6828
7014
|
} catch {
|
|
6829
7015
|
continue;
|
|
6830
7016
|
}
|
|
6831
|
-
const rel =
|
|
6832
|
-
if (rel.startsWith("..") ||
|
|
6833
|
-
const
|
|
6834
|
-
if (!
|
|
7017
|
+
const rel = path3.relative(realRoot, realPath);
|
|
7018
|
+
if (rel.startsWith("..") || path3.isAbsolute(rel)) continue;
|
|
7019
|
+
const stat11 = await fs14.stat(realPath).catch(() => null);
|
|
7020
|
+
if (!stat11 || !stat11.isFile()) continue;
|
|
6835
7021
|
let content;
|
|
6836
7022
|
try {
|
|
6837
7023
|
const buf = await fs14.readFile(realPath);
|
|
@@ -6856,7 +7042,7 @@ var replaceTool = {
|
|
|
6856
7042
|
totalReplacements += count;
|
|
6857
7043
|
if (!dryRun) {
|
|
6858
7044
|
const newContent = toStyle(newContentLf, style);
|
|
6859
|
-
await atomicWrite(realPath, newContent, { mode:
|
|
7045
|
+
await atomicWrite(realPath, newContent, { mode: stat11.mode & 511 });
|
|
6860
7046
|
}
|
|
6861
7047
|
const diff = dryRun || matches.length > 0 ? unifiedDiff(content, toStyle(newContentLf, style), {
|
|
6862
7048
|
fromFile: absPath,
|
|
@@ -6886,8 +7072,8 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
6886
7072
|
const resolved = [];
|
|
6887
7073
|
for (const p of parts) {
|
|
6888
7074
|
const absPath = safeResolve(p, ctx);
|
|
6889
|
-
const
|
|
6890
|
-
if (
|
|
7075
|
+
const stat11 = await fs14.stat(absPath).catch(() => null);
|
|
7076
|
+
if (stat11?.isFile()) {
|
|
6891
7077
|
resolved.push(absPath);
|
|
6892
7078
|
}
|
|
6893
7079
|
}
|
|
@@ -6948,10 +7134,10 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
6948
7134
|
}
|
|
6949
7135
|
for (const e of entries) {
|
|
6950
7136
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
6951
|
-
const full =
|
|
7137
|
+
const full = path3.join(dir, e.name);
|
|
6952
7138
|
try {
|
|
6953
|
-
const
|
|
6954
|
-
if (
|
|
7139
|
+
const stat11 = await fs14.lstat(full);
|
|
7140
|
+
if (stat11.isSymbolicLink()) continue;
|
|
6955
7141
|
} catch {
|
|
6956
7142
|
continue;
|
|
6957
7143
|
}
|
|
@@ -7118,16 +7304,16 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
7118
7304
|
let filesCreated = 0;
|
|
7119
7305
|
for (const [filePath, content] of Object.entries(templateFiles)) {
|
|
7120
7306
|
const resolvedPath = substituteVars(filePath, name, vars);
|
|
7121
|
-
const joinedPath =
|
|
7122
|
-
const root =
|
|
7123
|
-
const target =
|
|
7124
|
-
const rel =
|
|
7125
|
-
if (rel.startsWith("..") ||
|
|
7307
|
+
const joinedPath = path3.join(cwd, resolvedPath);
|
|
7308
|
+
const root = path3.resolve(ctx.projectRoot);
|
|
7309
|
+
const target = path3.resolve(joinedPath);
|
|
7310
|
+
const rel = path3.relative(root, target);
|
|
7311
|
+
if (rel.startsWith("..") || path3.isAbsolute(rel)) {
|
|
7126
7312
|
throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
|
|
7127
7313
|
}
|
|
7128
7314
|
const fullPath = target;
|
|
7129
7315
|
if (!dryRun) {
|
|
7130
|
-
await fs14.mkdir(
|
|
7316
|
+
await fs14.mkdir(path3.dirname(fullPath), { recursive: true });
|
|
7131
7317
|
await atomicWrite(fullPath, substituteVars(content, name, vars));
|
|
7132
7318
|
}
|
|
7133
7319
|
files.push(resolvedPath);
|
|
@@ -7740,7 +7926,11 @@ var testTool = {
|
|
|
7740
7926
|
coverage: { type: "boolean", description: "Generate coverage report (default: false)" },
|
|
7741
7927
|
cwd: { type: "string", description: "Working directory (default: cwd)" },
|
|
7742
7928
|
grep: { type: "string", description: "Filter tests by name pattern (default: none)" },
|
|
7743
|
-
timeout: { type: "integer", description: "Test timeout in ms (default: 30000)" }
|
|
7929
|
+
timeout: { type: "integer", description: "Test timeout in ms (default: 30000)" },
|
|
7930
|
+
verbose: {
|
|
7931
|
+
type: "boolean",
|
|
7932
|
+
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)"
|
|
7933
|
+
}
|
|
7744
7934
|
}
|
|
7745
7935
|
},
|
|
7746
7936
|
async execute(input, ctx, opts) {
|
|
@@ -7788,11 +7978,11 @@ var testTool = {
|
|
|
7788
7978
|
}
|
|
7789
7979
|
};
|
|
7790
7980
|
async function detectRunner(cwd) {
|
|
7791
|
-
const { stat:
|
|
7981
|
+
const { stat: stat11 } = await import('node:fs/promises');
|
|
7792
7982
|
const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
|
|
7793
7983
|
for (const f of candidates) {
|
|
7794
7984
|
try {
|
|
7795
|
-
await
|
|
7985
|
+
await stat11(path3.join(cwd, f));
|
|
7796
7986
|
if (f.includes("vitest")) return "vitest";
|
|
7797
7987
|
if (f.includes("jest")) return "jest";
|
|
7798
7988
|
if (f.includes("mocha")) return "mocha";
|
|
@@ -7806,17 +7996,14 @@ function buildArgs2(runner, input) {
|
|
|
7806
7996
|
const timeout = input.timeout ?? 3e4;
|
|
7807
7997
|
switch (runner) {
|
|
7808
7998
|
case "vitest":
|
|
7809
|
-
args.push("
|
|
7810
|
-
if (input.
|
|
7811
|
-
args[1] = "";
|
|
7812
|
-
args.push("watch");
|
|
7813
|
-
}
|
|
7999
|
+
args.push(input.watch ? "watch" : "run");
|
|
8000
|
+
if (input.verbose) args.push("--reporter=verbose");
|
|
7814
8001
|
if (input.coverage) args.push("--coverage");
|
|
7815
8002
|
if (input.grep) args.push("--testNamePattern", input.grep);
|
|
7816
8003
|
args.push("--testTimeout", String(timeout));
|
|
7817
8004
|
break;
|
|
7818
8005
|
case "jest":
|
|
7819
|
-
args.push("--verbose");
|
|
8006
|
+
if (input.verbose) args.push("--verbose");
|
|
7820
8007
|
if (input.watch) args.push("--watch");
|
|
7821
8008
|
if (input.coverage) args.push("--coverage");
|
|
7822
8009
|
if (input.grep) args.push("--testPathPattern", input.grep);
|
|
@@ -7860,7 +8047,13 @@ function parseResult(runner, result, duration) {
|
|
|
7860
8047
|
passed,
|
|
7861
8048
|
failed,
|
|
7862
8049
|
duration_ms: duration,
|
|
7863
|
-
|
|
8050
|
+
// A passing run only needs the tail summary in chat history — counts are
|
|
8051
|
+
// already parsed above and the FULL log is on disk (spool marker rides
|
|
8052
|
+
// the stdout tail). Failures keep the standard command-output cap so
|
|
8053
|
+
// the agent sees the failure details inline.
|
|
8054
|
+
output: normalizeCommandOutput(result.stdout || result.error || "", {
|
|
8055
|
+
maxBytes: result.exitCode === 0 ? 4096 : void 0
|
|
8056
|
+
}),
|
|
7864
8057
|
truncated: result.truncated
|
|
7865
8058
|
};
|
|
7866
8059
|
}
|
|
@@ -8408,7 +8601,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
8408
8601
|
opts.lines.push(opts.prefix + branch + displayName);
|
|
8409
8602
|
if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
|
|
8410
8603
|
const childPrefix = opts.prefix + connector;
|
|
8411
|
-
await walkDir(
|
|
8604
|
+
await walkDir(path3.join(dir, entry.name), depth + 1, {
|
|
8412
8605
|
...opts,
|
|
8413
8606
|
prefix: childPrefix,
|
|
8414
8607
|
isLast
|
|
@@ -8487,12 +8680,12 @@ var typecheckTool = {
|
|
|
8487
8680
|
}
|
|
8488
8681
|
};
|
|
8489
8682
|
async function findTsConfig(cwd) {
|
|
8490
|
-
const { stat:
|
|
8683
|
+
const { stat: stat11 } = await import('node:fs/promises');
|
|
8491
8684
|
const candidates = ["tsconfig.json", "tsconfig.base.json"];
|
|
8492
8685
|
for (const f of candidates) {
|
|
8493
8686
|
try {
|
|
8494
|
-
const s = await
|
|
8495
|
-
if (s.isFile()) return
|
|
8687
|
+
const s = await stat11(path3.join(cwd, f));
|
|
8688
|
+
if (s.isFile()) return path3.join(cwd, f);
|
|
8496
8689
|
} catch {
|
|
8497
8690
|
}
|
|
8498
8691
|
}
|
|
@@ -8528,12 +8721,12 @@ var writeTool = {
|
|
|
8528
8721
|
let existed = false;
|
|
8529
8722
|
let prev = "";
|
|
8530
8723
|
try {
|
|
8531
|
-
const
|
|
8532
|
-
existed =
|
|
8724
|
+
const stat12 = await fs14.stat(absPath);
|
|
8725
|
+
existed = stat12.isFile();
|
|
8533
8726
|
if (existed) {
|
|
8534
8727
|
if (!ctx.hasRead(absPath)) {
|
|
8535
8728
|
prev = await fs14.readFile(absPath, "utf8");
|
|
8536
|
-
ctx.recordRead(absPath,
|
|
8729
|
+
ctx.recordRead(absPath, stat12.mtimeMs);
|
|
8537
8730
|
} else {
|
|
8538
8731
|
prev = await fs14.readFile(absPath, "utf8");
|
|
8539
8732
|
}
|
|
@@ -8546,8 +8739,8 @@ var writeTool = {
|
|
|
8546
8739
|
await atomicWrite(absPath, input.content);
|
|
8547
8740
|
const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
8548
8741
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
8549
|
-
const
|
|
8550
|
-
ctx.recordRead(absPath,
|
|
8742
|
+
const stat11 = await fs14.stat(absPath);
|
|
8743
|
+
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
8551
8744
|
ctx.session.recordFileChange({
|
|
8552
8745
|
path: absPath,
|
|
8553
8746
|
action: existed ? "modified" : "created",
|