@wrongstack/tools 0.236.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 +591 -48
- package/dist/audit.js.map +1 -1
- package/dist/background-indexer-CJ5JiV5i.d.ts +365 -0
- package/dist/bash.js +135 -20
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +1840 -1109
- package/dist/builtin.js.map +1 -1
- package/dist/codebase-index/index.d.ts +53 -2
- package/dist/codebase-index/index.js +870 -364
- package/dist/codebase-index/index.js.map +1 -1
- package/dist/codebase-index/worker.d.ts +2 -0
- package/dist/codebase-index/worker.js +2326 -0
- package/dist/codebase-index/worker.js.map +1 -0
- package/dist/diff.js +2 -1
- package/dist/diff.js.map +1 -1
- package/dist/exec.js +116 -5
- package/dist/exec.js.map +1 -1
- package/dist/format.js +591 -48
- package/dist/format.js.map +1 -1
- package/dist/git.js +2 -1
- package/dist/git.js.map +1 -1
- package/dist/grep.js +2 -2
- package/dist/grep.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1189 -496
- package/dist/index.js.map +1 -1
- package/dist/install.js +591 -48
- package/dist/install.js.map +1 -1
- package/dist/lint.js +590 -47
- package/dist/lint.js.map +1 -1
- package/dist/logs.js +1 -1
- package/dist/logs.js.map +1 -1
- package/dist/outdated.js +1 -1
- package/dist/outdated.js.map +1 -1
- package/dist/pack.js +1840 -1109
- package/dist/pack.js.map +1 -1
- package/dist/patch.js +1 -1
- package/dist/patch.js.map +1 -1
- package/dist/replace.js +3 -2
- package/dist/replace.js.map +1 -1
- package/dist/test.d.ts +1 -0
- package/dist/test.js +605 -55
- package/dist/test.js.map +1 -1
- package/dist/typecheck.js +591 -48
- package/dist/typecheck.js.map +1 -1
- package/package.json +3 -3
- package/dist/background-indexer-CtbgPExj.d.ts +0 -228
package/dist/pack.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { spawn, execFileSync, spawnSync } from 'node:child_process';
|
|
2
2
|
import * as Core from '@wrongstack/core';
|
|
3
|
-
import { buildChildEnv,
|
|
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
|
|
5
|
+
import { statSync, mkdirSync, createWriteStream, writeFileSync } from 'node:fs';
|
|
6
|
+
import * as fs14 from 'node:fs/promises';
|
|
7
|
+
import * as path3 from 'node:path';
|
|
7
8
|
import { resolve, sep, dirname, join } from 'node:path';
|
|
8
|
-
import * as fs13 from 'node:fs/promises';
|
|
9
9
|
import * as os from 'node:os';
|
|
10
10
|
import { createRequire } from 'node:module';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { Worker } from 'node:worker_threads';
|
|
11
13
|
import * as ts from 'typescript';
|
|
12
14
|
import * as dns from 'node:dns/promises';
|
|
13
15
|
import * as net from 'node:net';
|
|
@@ -15,751 +17,909 @@ import { Agent } from 'undici';
|
|
|
15
17
|
import { randomUUID } from 'node:crypto';
|
|
16
18
|
|
|
17
19
|
// src/_spawn-stream.ts
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
}
|
|
33
40
|
}
|
|
41
|
+
} catch {
|
|
34
42
|
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
let
|
|
46
|
-
let
|
|
47
|
-
let
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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;
|
|
75
|
+
});
|
|
76
|
+
stream.write(head);
|
|
77
|
+
} catch {
|
|
78
|
+
failed = true;
|
|
79
|
+
stream = null;
|
|
80
|
+
filePath = null;
|
|
65
81
|
}
|
|
66
82
|
};
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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;
|
|
97
|
+
}
|
|
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 };
|
|
72
118
|
}
|
|
73
119
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/circuit-breaker.ts
|
|
123
|
+
var DEFAULT_MAX_CONSECUTIVE_FAILURES = 5;
|
|
124
|
+
var DEFAULT_SLOW_CALL_THRESHOLD_MS = 18e4;
|
|
125
|
+
var DEFAULT_MAX_SLOW_CALLS = 3;
|
|
126
|
+
var DEFAULT_WINDOW_MS = 6e4;
|
|
127
|
+
var DEFAULT_MAX_CALLS_PER_WINDOW = 30;
|
|
128
|
+
var DEFAULT_COOLDOWN_MS = 3e4;
|
|
129
|
+
var CircuitBreaker = class {
|
|
130
|
+
maxConsecutiveFailures;
|
|
131
|
+
slowCallThresholdMs;
|
|
132
|
+
maxSlowCalls;
|
|
133
|
+
windowMs;
|
|
134
|
+
maxCallsPerWindow;
|
|
135
|
+
cooldownMs;
|
|
136
|
+
state = "closed";
|
|
137
|
+
consecutiveFailures = 0;
|
|
138
|
+
window = [];
|
|
139
|
+
lastFailureAt = null;
|
|
140
|
+
lastSlowAt = null;
|
|
141
|
+
/** Timestamp when the breaker was opened (for cooldown calculation). */
|
|
142
|
+
openedAt = null;
|
|
143
|
+
constructor(config = {}) {
|
|
144
|
+
this.maxConsecutiveFailures = config.maxConsecutiveFailures ?? DEFAULT_MAX_CONSECUTIVE_FAILURES;
|
|
145
|
+
this.slowCallThresholdMs = config.slowCallThresholdMs ?? DEFAULT_SLOW_CALL_THRESHOLD_MS;
|
|
146
|
+
this.maxSlowCalls = config.maxSlowCalls ?? DEFAULT_MAX_SLOW_CALLS;
|
|
147
|
+
this.windowMs = config.windowMs ?? DEFAULT_WINDOW_MS;
|
|
148
|
+
this.maxCallsPerWindow = config.maxCallsPerWindow ?? DEFAULT_MAX_CALLS_PER_WINDOW;
|
|
149
|
+
this.cooldownMs = config.cooldownMs ?? DEFAULT_COOLDOWN_MS;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Returns true if the circuit allows a new call to proceed.
|
|
153
|
+
* When false, callers should abort the tool call and return a
|
|
154
|
+
* circuit-breaker error instead of spawning a process.
|
|
155
|
+
*/
|
|
156
|
+
get canProceed() {
|
|
157
|
+
this._checkStateTransition();
|
|
158
|
+
return this.state !== "open";
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Snapshot of the current breaker state for observability (`/kill`).
|
|
162
|
+
*/
|
|
163
|
+
snapshot() {
|
|
164
|
+
this._checkStateTransition();
|
|
165
|
+
const now = Date.now();
|
|
166
|
+
let cooldownRemaining = null;
|
|
167
|
+
if (this.openedAt !== null && this.state === "open") {
|
|
168
|
+
const elapsed = now - this.openedAt;
|
|
169
|
+
cooldownRemaining = Math.max(0, this.cooldownMs - elapsed);
|
|
94
170
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
171
|
+
return {
|
|
172
|
+
state: this.state,
|
|
173
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
174
|
+
slowCallsInWindow: this.window.filter((c) => c.slow).length,
|
|
175
|
+
callsInWindow: this.window.length,
|
|
176
|
+
windowMs: this.windowMs,
|
|
177
|
+
cooldownRemainingMs: cooldownRemaining,
|
|
178
|
+
lastFailureAt: this.lastFailureAt,
|
|
179
|
+
lastSlowAt: this.lastSlowAt
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Call this BEFORE spawning a bash/exec process.
|
|
184
|
+
* Returns true if the call is allowed; false if the breaker is open.
|
|
185
|
+
* When false, callers MUST NOT spawn a process.
|
|
186
|
+
*
|
|
187
|
+
* @param bypass - If true, skip the circuit breaker check entirely.
|
|
188
|
+
* Use for background/fire-and-forget processes that should
|
|
189
|
+
* not affect breaker state.
|
|
190
|
+
*/
|
|
191
|
+
beforeCall(bypass = false) {
|
|
192
|
+
if (bypass) return true;
|
|
193
|
+
this._checkStateTransition();
|
|
194
|
+
if (this.state === "open") return false;
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Call this AFTER a bash/exec process finishes (success or failure).
|
|
199
|
+
* `durationMs` is the wall-clock time the process ran.
|
|
200
|
+
* `failed` is true when the process returned a non-zero exit code or
|
|
201
|
+
* threw an exception before spawning.
|
|
202
|
+
*
|
|
203
|
+
* @param bypass - If true, do not update breaker state.
|
|
204
|
+
* Use for background/fire-and-forget processes.
|
|
205
|
+
*/
|
|
206
|
+
afterCall(durationMs, failed, bypass = false) {
|
|
207
|
+
if (bypass) return;
|
|
208
|
+
const now = Date.now();
|
|
209
|
+
if (this.state === "half-open") {
|
|
210
|
+
if (failed) {
|
|
211
|
+
this._trip();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
this._reset();
|
|
215
|
+
return;
|
|
112
216
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
217
|
+
this._pruneWindow(now);
|
|
218
|
+
const slow = durationMs >= this.slowCallThresholdMs;
|
|
219
|
+
this.window.push({ at: now, failed, slow });
|
|
220
|
+
if (failed) {
|
|
221
|
+
this.consecutiveFailures++;
|
|
222
|
+
this.lastFailureAt = now;
|
|
223
|
+
if (this.consecutiveFailures >= this.maxConsecutiveFailures) {
|
|
224
|
+
this._trip();
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
118
227
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
228
|
+
this.consecutiveFailures = 0;
|
|
229
|
+
if (slow) {
|
|
230
|
+
this.lastSlowAt = now;
|
|
231
|
+
const slowCount = this.window.filter((c) => c.slow).length;
|
|
232
|
+
if (slowCount >= this.maxSlowCalls) {
|
|
233
|
+
this._trip();
|
|
234
|
+
}
|
|
123
235
|
}
|
|
124
|
-
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
pending = "";
|
|
236
|
+
const callCount = this.window.length;
|
|
237
|
+
if (callCount >= this.maxCallsPerWindow) {
|
|
238
|
+
this._trip();
|
|
128
239
|
}
|
|
129
240
|
}
|
|
130
|
-
|
|
131
|
-
|
|
241
|
+
/** Force the breaker open. Used by /kill force and Ctrl+C. */
|
|
242
|
+
forceOpen() {
|
|
243
|
+
this._trip();
|
|
132
244
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
exitCode,
|
|
137
|
-
truncated: stdout.length >= max || stderr.length >= max,
|
|
138
|
-
error
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
async function detectPackageManager(cwd) {
|
|
142
|
-
const { stat: stat10 } = await import('node:fs/promises');
|
|
143
|
-
try {
|
|
144
|
-
await stat10(`${cwd}/pnpm-lock.yaml`);
|
|
145
|
-
return "pnpm";
|
|
146
|
-
} catch {
|
|
245
|
+
/** Force a reset to closed. Used by tests and /kill reset. */
|
|
246
|
+
forceReset() {
|
|
247
|
+
this._reset();
|
|
147
248
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
249
|
+
_trip() {
|
|
250
|
+
if (this.state === "open") return;
|
|
251
|
+
this.state = "open";
|
|
252
|
+
this.openedAt = Date.now();
|
|
152
253
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
function ensureInsideRoot(absPath, ctx) {
|
|
159
|
-
const root = path2.resolve(ctx.projectRoot);
|
|
160
|
-
const target = path2.resolve(absPath);
|
|
161
|
-
const rel = path2.relative(root, target);
|
|
162
|
-
if (rel.startsWith("..") || path2.isAbsolute(rel)) {
|
|
163
|
-
throw new Error(`Path "${absPath}" is outside project root "${root}"`);
|
|
254
|
+
_reset() {
|
|
255
|
+
this.state = "closed";
|
|
256
|
+
this.consecutiveFailures = 0;
|
|
257
|
+
this.window = [];
|
|
258
|
+
this.openedAt = null;
|
|
164
259
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
let probe = absPath;
|
|
173
|
-
for (; ; ) {
|
|
174
|
-
let real;
|
|
175
|
-
try {
|
|
176
|
-
real = await fs13.realpath(probe);
|
|
177
|
-
} catch (err) {
|
|
178
|
-
if (err.code === "ENOENT") {
|
|
179
|
-
const parent = path2.dirname(probe);
|
|
180
|
-
if (parent === probe) return;
|
|
181
|
-
probe = parent;
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
throw err;
|
|
185
|
-
}
|
|
186
|
-
const rel = path2.relative(realRoot, real);
|
|
187
|
-
if (rel.startsWith("..") || path2.isAbsolute(rel)) {
|
|
188
|
-
throw new Error(
|
|
189
|
-
`Path "${absPath}" resolves through a symlink outside project root "${realRoot}"`
|
|
190
|
-
);
|
|
260
|
+
/** Transition from open → half-open when cooldown elapses. */
|
|
261
|
+
_checkStateTransition() {
|
|
262
|
+
if (this.state !== "open" || this.openedAt === null) return;
|
|
263
|
+
const elapsed = Date.now() - this.openedAt;
|
|
264
|
+
if (elapsed >= this.cooldownMs) {
|
|
265
|
+
this.state = "half-open";
|
|
266
|
+
this.openedAt = null;
|
|
191
267
|
}
|
|
192
|
-
return;
|
|
193
268
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
await assertRealInsideRoot(abs, ctx);
|
|
198
|
-
return abs;
|
|
199
|
-
}
|
|
200
|
-
function truncateMiddle(s, max) {
|
|
201
|
-
if (Buffer.byteLength(s, "utf8") <= max) return s;
|
|
202
|
-
const half = Math.floor(max / 2);
|
|
203
|
-
return s.slice(0, half) + `
|
|
204
|
-
\u2026[truncated ${Buffer.byteLength(s, "utf8") - max} bytes from middle]\u2026
|
|
205
|
-
` + s.slice(-half);
|
|
206
|
-
}
|
|
207
|
-
function isBinaryBuffer(buf) {
|
|
208
|
-
const len = Math.min(buf.length, 8192);
|
|
209
|
-
for (let i = 0; i < len; i++) {
|
|
210
|
-
if (buf[i] === 0) return true;
|
|
269
|
+
_pruneWindow(now) {
|
|
270
|
+
const cutoff = now - this.windowMs;
|
|
271
|
+
this.window = this.window.filter((c) => c.at >= cutoff);
|
|
211
272
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
var
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// src/process-registry.ts
|
|
276
|
+
var SENSITIVE_FLAG_PATTERNS = [
|
|
277
|
+
// --flag=value or --flag "value" (value captured up to next space or comma)
|
|
278
|
+
/--(?:token|password|passwd|pwd|secret|api[-_]?key|api[-_]?secret|auth|credential|private[-_]?key|access[-_]?key|github[-_]?token|gh[-_]?token|bearer|jwt|oauth|pin|pincode|passphrase|access[-_]?token)(?:[=\s,][^\s]*)?/gi,
|
|
279
|
+
// -f "value" style short flags
|
|
280
|
+
/(?<!\w)-t(?:\s+|\s*=\s*)[^\s,]+/g,
|
|
281
|
+
/(?<!\w)-p(?:ssword)?(?:\s+|\s*=\s*)[^\s,]+/gi,
|
|
282
|
+
// env var–style secrets: TOKEN=x, API_KEY=y, etc.
|
|
283
|
+
/(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
|
|
284
|
+
// Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
|
|
285
|
+
// when preceded by a flag name (e.g. --github-token=EyJ...).
|
|
286
|
+
/--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/
|
|
287
|
+
];
|
|
288
|
+
function redactCommand(cmd) {
|
|
289
|
+
let result = cmd;
|
|
290
|
+
for (const pattern of SENSITIVE_FLAG_PATTERNS) {
|
|
291
|
+
result = result.replace(pattern, (match) => {
|
|
292
|
+
const eq = match.indexOf("=");
|
|
293
|
+
const sp = match.search(/\s/);
|
|
294
|
+
const delim = eq !== -1 ? "=" : sp !== -1 ? match[sp] : null;
|
|
295
|
+
if (delim !== null) {
|
|
296
|
+
const flag = match.slice(0, match.indexOf(expectDefined(delim)) + 1);
|
|
297
|
+
return `${flag}[REDACTED]`;
|
|
298
|
+
}
|
|
299
|
+
const flagEnd = match.match(/^--?[a-zA-Z][a-zA-Z0-9_-]*/)?.[0] ?? match;
|
|
300
|
+
return `${flagEnd}=**redacted**`;
|
|
301
|
+
});
|
|
235
302
|
}
|
|
236
|
-
return
|
|
303
|
+
return result;
|
|
237
304
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
305
|
+
var DEFAULT_GRACE_MS = 2e3;
|
|
306
|
+
function killWin32Tree(pid) {
|
|
307
|
+
try {
|
|
308
|
+
spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
309
|
+
stdio: "ignore",
|
|
310
|
+
windowsHide: true
|
|
311
|
+
}).unref();
|
|
312
|
+
return true;
|
|
313
|
+
} catch {
|
|
314
|
+
return false;
|
|
247
315
|
}
|
|
248
|
-
return s.slice(0, lo);
|
|
249
316
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
while (lo < hi) {
|
|
256
|
-
const mid = Math.ceil((lo + hi) / 2);
|
|
257
|
-
if (Buffer.byteLength(s.slice(s.length - mid), "utf8") <= maxBytes) lo = mid;
|
|
258
|
-
else hi = mid - 1;
|
|
317
|
+
var ProcessRegistryImpl = class {
|
|
318
|
+
processes = /* @__PURE__ */ new Map();
|
|
319
|
+
breaker;
|
|
320
|
+
constructor(breakerConfig) {
|
|
321
|
+
this.breaker = new CircuitBreaker(breakerConfig);
|
|
259
322
|
}
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
category: "Package Management",
|
|
289
|
-
description: "Run a security audit against project dependencies (using pnpm/npm audit). Reports known vulnerabilities with severity.",
|
|
290
|
-
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.",
|
|
291
|
-
permission: "confirm",
|
|
292
|
-
mutating: false,
|
|
293
|
-
timeoutMs: 6e4,
|
|
294
|
-
inputSchema: {
|
|
295
|
-
type: "object",
|
|
296
|
-
properties: {
|
|
297
|
-
cwd: { type: "string", description: "Working directory (default: cwd)" },
|
|
298
|
-
level: {
|
|
299
|
-
type: "string",
|
|
300
|
-
enum: ["low", "moderate", "high", "critical"],
|
|
301
|
-
description: "Minimum severity level to report"
|
|
302
|
-
},
|
|
303
|
-
fix: { type: "boolean", description: "Attempt to fix vulnerabilities (default: false)" },
|
|
304
|
-
packages: { type: "string", description: "Specific package(s) to audit (comma-separated)" }
|
|
305
|
-
}
|
|
306
|
-
},
|
|
307
|
-
async execute(input, ctx, opts) {
|
|
308
|
-
let final;
|
|
309
|
-
const executeStream = auditTool.executeStream;
|
|
310
|
-
if (!executeStream) throw new Error("auditTool: stream execution unavailable");
|
|
311
|
-
for await (const ev of executeStream(input, ctx, opts)) {
|
|
312
|
-
if (ev.type === "final") final = ev.output;
|
|
313
|
-
}
|
|
314
|
-
if (!final) throw new Error("audit: stream ended without final event");
|
|
315
|
-
return final;
|
|
316
|
-
},
|
|
317
|
-
async *executeStream(input, ctx, opts) {
|
|
318
|
-
const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
|
|
319
|
-
const manager = await detectPackageManager(cwd);
|
|
320
|
-
yield { type: "log", text: `Auditing with ${manager}\u2026`, data: { manager } };
|
|
321
|
-
const args = ["audit", "--json"];
|
|
322
|
-
if (input.fix) args.push("--fix");
|
|
323
|
-
if (input.packages) {
|
|
324
|
-
const pkgs = Array.isArray(input.packages) ? input.packages : input.packages.split(",");
|
|
325
|
-
args.push(...pkgs.map((p) => p.trim()));
|
|
323
|
+
register(info) {
|
|
324
|
+
this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
|
|
325
|
+
}
|
|
326
|
+
/** Unregister a process by PID. Called on 'close' / 'exit' events. */
|
|
327
|
+
unregister(pid) {
|
|
328
|
+
this.processes.delete(pid);
|
|
329
|
+
}
|
|
330
|
+
/** Get a single process by PID. */
|
|
331
|
+
get(pid) {
|
|
332
|
+
return this.processes.get(pid);
|
|
333
|
+
}
|
|
334
|
+
/** Get all tracked processes. */
|
|
335
|
+
list() {
|
|
336
|
+
return Array.from(this.processes.values());
|
|
337
|
+
}
|
|
338
|
+
/** Get processes filtered by name (e.g. 'bash', 'exec'). */
|
|
339
|
+
byName(name) {
|
|
340
|
+
return this.list().filter((p) => p.name === name);
|
|
341
|
+
}
|
|
342
|
+
/** Get processes filtered by session. */
|
|
343
|
+
bySession(sessionId) {
|
|
344
|
+
return this.list().filter((p) => p.sessionId === sessionId);
|
|
345
|
+
}
|
|
346
|
+
/** Count of active (non-killed) processes. */
|
|
347
|
+
get activeCount() {
|
|
348
|
+
let n = 0;
|
|
349
|
+
for (const p of this.processes.values()) {
|
|
350
|
+
if (!p.killed) n++;
|
|
326
351
|
}
|
|
327
|
-
|
|
328
|
-
cmd: manager,
|
|
329
|
-
args,
|
|
330
|
-
cwd,
|
|
331
|
-
signal: opts.signal,
|
|
332
|
-
maxBytes: 1e5
|
|
333
|
-
});
|
|
334
|
-
yield { type: "final", output: parseAuditOutput(result.stdout, result.exitCode) };
|
|
352
|
+
return n;
|
|
335
353
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Combined stats for observability — used by /ps and the TUI status bar.
|
|
356
|
+
*/
|
|
357
|
+
stats() {
|
|
339
358
|
return {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
summary: exitCode === 0 ? "No vulnerabilities found" : "Audit failed",
|
|
344
|
-
output: "",
|
|
345
|
-
truncated: false
|
|
359
|
+
activeCount: this.activeCount,
|
|
360
|
+
totalCount: this.processes.size,
|
|
361
|
+
breaker: this.breaker.snapshot()
|
|
346
362
|
};
|
|
347
363
|
}
|
|
348
|
-
try {
|
|
349
|
-
const data = JSON.parse(json);
|
|
350
|
-
const advisories = [];
|
|
351
|
-
const ads = data.advisories ?? {};
|
|
352
|
-
for (const id of Object.keys(ads)) {
|
|
353
|
-
const adv = ads[id];
|
|
354
|
-
advisories.push({
|
|
355
|
-
severity: adv.severity ?? "unknown",
|
|
356
|
-
package: adv.module_name ?? id,
|
|
357
|
-
title: adv.title ?? "Unknown vulnerability",
|
|
358
|
-
url: adv.url ?? ""
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
const total = advisories.length;
|
|
362
|
-
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`;
|
|
363
|
-
return {
|
|
364
|
-
exit_code: exitCode,
|
|
365
|
-
vulnerabilities: advisories,
|
|
366
|
-
total,
|
|
367
|
-
summary,
|
|
368
|
-
output: json,
|
|
369
|
-
truncated: json.length >= 1e5
|
|
370
|
-
};
|
|
371
|
-
} catch {
|
|
372
|
-
return {
|
|
373
|
-
exit_code: exitCode,
|
|
374
|
-
vulnerabilities: [],
|
|
375
|
-
total: 0,
|
|
376
|
-
summary: "Could not parse audit output",
|
|
377
|
-
output: json,
|
|
378
|
-
truncated: false
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// src/circuit-breaker.ts
|
|
384
|
-
var DEFAULT_MAX_CONSECUTIVE_FAILURES = 5;
|
|
385
|
-
var DEFAULT_SLOW_CALL_THRESHOLD_MS = 18e4;
|
|
386
|
-
var DEFAULT_MAX_SLOW_CALLS = 3;
|
|
387
|
-
var DEFAULT_WINDOW_MS = 6e4;
|
|
388
|
-
var DEFAULT_MAX_CALLS_PER_WINDOW = 30;
|
|
389
|
-
var DEFAULT_COOLDOWN_MS = 3e4;
|
|
390
|
-
var CircuitBreaker = class {
|
|
391
|
-
maxConsecutiveFailures;
|
|
392
|
-
slowCallThresholdMs;
|
|
393
|
-
maxSlowCalls;
|
|
394
|
-
windowMs;
|
|
395
|
-
maxCallsPerWindow;
|
|
396
|
-
cooldownMs;
|
|
397
|
-
state = "closed";
|
|
398
|
-
consecutiveFailures = 0;
|
|
399
|
-
window = [];
|
|
400
|
-
lastFailureAt = null;
|
|
401
|
-
lastSlowAt = null;
|
|
402
|
-
/** Timestamp when the breaker was opened (for cooldown calculation). */
|
|
403
|
-
openedAt = null;
|
|
404
|
-
constructor(config = {}) {
|
|
405
|
-
this.maxConsecutiveFailures = config.maxConsecutiveFailures ?? DEFAULT_MAX_CONSECUTIVE_FAILURES;
|
|
406
|
-
this.slowCallThresholdMs = config.slowCallThresholdMs ?? DEFAULT_SLOW_CALL_THRESHOLD_MS;
|
|
407
|
-
this.maxSlowCalls = config.maxSlowCalls ?? DEFAULT_MAX_SLOW_CALLS;
|
|
408
|
-
this.windowMs = config.windowMs ?? DEFAULT_WINDOW_MS;
|
|
409
|
-
this.maxCallsPerWindow = config.maxCallsPerWindow ?? DEFAULT_MAX_CALLS_PER_WINDOW;
|
|
410
|
-
this.cooldownMs = config.cooldownMs ?? DEFAULT_COOLDOWN_MS;
|
|
411
|
-
}
|
|
412
364
|
/**
|
|
413
|
-
* Returns true if the circuit allows a new call to proceed.
|
|
414
|
-
* When false, callers
|
|
415
|
-
* circuit-breaker error instead of spawning a process.
|
|
365
|
+
* Returns true if the circuit allows a new bash/exec call to proceed.
|
|
366
|
+
* When false, callers MUST NOT spawn a process.
|
|
416
367
|
*/
|
|
417
368
|
get canProceed() {
|
|
418
|
-
this.
|
|
419
|
-
return this.state !== "open";
|
|
420
|
-
}
|
|
421
|
-
/**
|
|
422
|
-
* Snapshot of the current breaker state for observability (`/kill`).
|
|
423
|
-
*/
|
|
424
|
-
snapshot() {
|
|
425
|
-
this._checkStateTransition();
|
|
426
|
-
const now = Date.now();
|
|
427
|
-
let cooldownRemaining = null;
|
|
428
|
-
if (this.openedAt !== null && this.state === "open") {
|
|
429
|
-
const elapsed = now - this.openedAt;
|
|
430
|
-
cooldownRemaining = Math.max(0, this.cooldownMs - elapsed);
|
|
431
|
-
}
|
|
432
|
-
return {
|
|
433
|
-
state: this.state,
|
|
434
|
-
consecutiveFailures: this.consecutiveFailures,
|
|
435
|
-
slowCallsInWindow: this.window.filter((c) => c.slow).length,
|
|
436
|
-
callsInWindow: this.window.length,
|
|
437
|
-
windowMs: this.windowMs,
|
|
438
|
-
cooldownRemainingMs: cooldownRemaining,
|
|
439
|
-
lastFailureAt: this.lastFailureAt,
|
|
440
|
-
lastSlowAt: this.lastSlowAt
|
|
441
|
-
};
|
|
369
|
+
return this.breaker.canProceed;
|
|
442
370
|
}
|
|
443
371
|
/**
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
* When false, callers MUST NOT spawn a process.
|
|
372
|
+
* Called before spawning a process. Returns true if allowed; false if
|
|
373
|
+
* the circuit breaker is open.
|
|
447
374
|
*
|
|
448
|
-
* @param bypass - If true, skip
|
|
449
|
-
* Use for background/fire-and-forget processes that should
|
|
450
|
-
* not affect breaker state.
|
|
375
|
+
* @param bypass - If true, skip circuit breaker check (for background processes).
|
|
451
376
|
*/
|
|
452
377
|
beforeCall(bypass = false) {
|
|
453
|
-
|
|
454
|
-
this._checkStateTransition();
|
|
455
|
-
if (this.state === "open") return false;
|
|
456
|
-
return true;
|
|
378
|
+
return this.breaker.beforeCall(bypass);
|
|
457
379
|
}
|
|
458
380
|
/**
|
|
459
|
-
*
|
|
460
|
-
* `
|
|
461
|
-
* `failed` is true when the process returned a non-zero exit code or
|
|
462
|
-
* threw an exception before spawning.
|
|
381
|
+
* Called after a process finishes. `durationMs` is wall-clock time;
|
|
382
|
+
* `failed` is true for non-zero exit codes.
|
|
463
383
|
*
|
|
464
|
-
* @param bypass - If true, do not update breaker state.
|
|
465
|
-
* Use for background/fire-and-forget processes.
|
|
384
|
+
* @param bypass - If true, do not update circuit breaker state (for background processes).
|
|
466
385
|
*/
|
|
467
386
|
afterCall(durationMs, failed, bypass = false) {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
387
|
+
this.breaker.afterCall(durationMs, failed, bypass);
|
|
388
|
+
}
|
|
389
|
+
/** Force-open the circuit breaker (Ctrl+C, /kill force). */
|
|
390
|
+
forceBreakerOpen() {
|
|
391
|
+
this.breaker.forceOpen();
|
|
392
|
+
}
|
|
393
|
+
/** Force-reset the circuit breaker to closed (/kill reset). */
|
|
394
|
+
forceBreakerReset() {
|
|
395
|
+
this.breaker.forceReset();
|
|
396
|
+
}
|
|
397
|
+
/** Kill a single process by PID.
|
|
398
|
+
*
|
|
399
|
+
* On POSIX: sends SIGTERM to the *process group* (-pid) so that
|
|
400
|
+
* runaway grandchild processes (`sleep 9999 & disown`) are also killed.
|
|
401
|
+
* After `graceMs` a SIGKILL is sent if the process hasn't exited.
|
|
402
|
+
*
|
|
403
|
+
* On Windows: `child.kill()` maps to TerminateProcess — process groups
|
|
404
|
+
* are not meaningfully supported. A second `force=true` call sends
|
|
405
|
+
* SIGKILL (which maps to TerminateProcess again — the distinction is
|
|
406
|
+
* in the exit code, not the signal).
|
|
407
|
+
*
|
|
408
|
+
* Returns true if the process was found and kill was attempted.
|
|
409
|
+
*/
|
|
410
|
+
kill(pid, opts = {}) {
|
|
411
|
+
const p = this.processes.get(pid);
|
|
412
|
+
if (!p) return false;
|
|
413
|
+
if (p.killed) return true;
|
|
414
|
+
if (p.protected) return false;
|
|
415
|
+
const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
|
|
416
|
+
const isWin = os.platform() === "win32";
|
|
417
|
+
if (isWin) {
|
|
418
|
+
const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
|
|
419
|
+
if (liveRealChild && killWin32Tree(pid)) {
|
|
420
|
+
const fallback = setTimeout(() => {
|
|
421
|
+
if (p.child.exitCode === null) {
|
|
422
|
+
try {
|
|
423
|
+
p.child.kill("SIGKILL");
|
|
424
|
+
} catch {
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}, graceMs);
|
|
428
|
+
fallback.unref?.();
|
|
429
|
+
} else {
|
|
430
|
+
try {
|
|
431
|
+
p.child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
432
|
+
} catch {
|
|
433
|
+
}
|
|
474
434
|
}
|
|
475
|
-
|
|
476
|
-
return;
|
|
435
|
+
p.killed = true;
|
|
436
|
+
return true;
|
|
477
437
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
438
|
+
try {
|
|
439
|
+
if (force) {
|
|
440
|
+
try {
|
|
441
|
+
process.kill(-pid, "SIGKILL");
|
|
442
|
+
} catch {
|
|
443
|
+
p.child.kill("SIGKILL");
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
try {
|
|
447
|
+
process.kill(-pid, "SIGTERM");
|
|
448
|
+
} catch {
|
|
449
|
+
p.child.kill("SIGTERM");
|
|
450
|
+
}
|
|
451
|
+
const timer = setTimeout(() => {
|
|
452
|
+
if (this.processes.has(pid) && !p.child.killed) {
|
|
453
|
+
try {
|
|
454
|
+
process.kill(-pid, "SIGKILL");
|
|
455
|
+
} catch {
|
|
456
|
+
try {
|
|
457
|
+
p.child.kill("SIGKILL");
|
|
458
|
+
} catch {
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}, graceMs);
|
|
463
|
+
timer.unref?.();
|
|
486
464
|
}
|
|
487
|
-
|
|
465
|
+
} catch {
|
|
488
466
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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);
|
|
496
480
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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);
|
|
500
492
|
}
|
|
493
|
+
return killed;
|
|
501
494
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
495
|
+
};
|
|
496
|
+
var _registry;
|
|
497
|
+
function getProcessRegistry() {
|
|
498
|
+
if (!_registry) {
|
|
499
|
+
_registry = new ProcessRegistryImpl();
|
|
505
500
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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;
|
|
509
507
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
/** Transition from open → half-open when cooldown elapses. */
|
|
522
|
-
_checkStateTransition() {
|
|
523
|
-
if (this.state !== "open" || this.openedAt === null) return;
|
|
524
|
-
const elapsed = Date.now() - this.openedAt;
|
|
525
|
-
if (elapsed >= this.cooldownMs) {
|
|
526
|
-
this.state = "half-open";
|
|
527
|
-
this.openedAt = null;
|
|
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
|
+
}
|
|
528
519
|
}
|
|
529
520
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
this.window = this.window.filter((c) => c.at >= cutoff);
|
|
533
|
-
}
|
|
534
|
-
};
|
|
521
|
+
return cmd;
|
|
522
|
+
}
|
|
535
523
|
|
|
536
|
-
// src/
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
|
562
554
|
});
|
|
563
555
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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;
|
|
568
625
|
try {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
+
}
|
|
576
681
|
}
|
|
577
682
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
}
|
|
584
|
-
register(info) {
|
|
585
|
-
this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
|
|
586
|
-
}
|
|
587
|
-
/** Unregister a process by PID. Called on 'close' / 'exit' events. */
|
|
588
|
-
unregister(pid) {
|
|
589
|
-
this.processes.delete(pid);
|
|
590
|
-
}
|
|
591
|
-
/** Get a single process by PID. */
|
|
592
|
-
get(pid) {
|
|
593
|
-
return this.processes.get(pid);
|
|
594
|
-
}
|
|
595
|
-
/** Get all tracked processes. */
|
|
596
|
-
list() {
|
|
597
|
-
return Array.from(this.processes.values());
|
|
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 {
|
|
598
689
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
return
|
|
690
|
+
try {
|
|
691
|
+
await stat11(`${cwd}/yarn.lock`);
|
|
692
|
+
return "yarn";
|
|
693
|
+
} catch {
|
|
602
694
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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}"`);
|
|
606
706
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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;
|
|
612
727
|
}
|
|
613
|
-
|
|
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;
|
|
614
735
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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;
|
|
624
753
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
this.breaker.afterCall(durationMs, failed, bypass);
|
|
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]);
|
|
775
|
+
}
|
|
776
|
+
i = j;
|
|
649
777
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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;
|
|
653
789
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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;
|
|
657
801
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
} catch {
|
|
704
|
-
p.child.kill("SIGKILL");
|
|
705
|
-
}
|
|
706
|
-
} else {
|
|
707
|
-
try {
|
|
708
|
-
process.kill(-pid, "SIGTERM");
|
|
709
|
-
} catch {
|
|
710
|
-
p.child.kill("SIGTERM");
|
|
711
|
-
}
|
|
712
|
-
const timer = setTimeout(() => {
|
|
713
|
-
if (this.processes.has(pid) && !p.child.killed) {
|
|
714
|
-
try {
|
|
715
|
-
process.kill(-pid, "SIGKILL");
|
|
716
|
-
} catch {
|
|
717
|
-
try {
|
|
718
|
-
p.child.kill("SIGKILL");
|
|
719
|
-
} catch {
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
}, graceMs);
|
|
724
|
-
timer.unref?.();
|
|
725
|
-
}
|
|
726
|
-
} catch {
|
|
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)" }
|
|
727
847
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
killAll(opts = {}) {
|
|
736
|
-
const pids = Array.from(this.processes.keys());
|
|
737
|
-
const killed = [];
|
|
738
|
-
for (const pid of pids) {
|
|
739
|
-
const p = this.processes.get(pid);
|
|
740
|
-
if (p && !p.protected && this.kill(pid, opts)) killed.push(pid);
|
|
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;
|
|
741
855
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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()));
|
|
753
868
|
}
|
|
754
|
-
|
|
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) };
|
|
755
877
|
}
|
|
756
878
|
};
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
+
};
|
|
761
922
|
}
|
|
762
|
-
return _registry;
|
|
763
923
|
}
|
|
764
924
|
|
|
765
925
|
// src/bash.ts
|
|
@@ -855,7 +1015,7 @@ var bashTool = {
|
|
|
855
1015
|
})();
|
|
856
1016
|
const args = isWin ? ["/c", input.command] : ["-c", input.command];
|
|
857
1017
|
const env = buildChildEnv(ctx.session?.id);
|
|
858
|
-
const detached = isWin
|
|
1018
|
+
const detached = !isWin;
|
|
859
1019
|
const startedAt = Date.now();
|
|
860
1020
|
if (input.background) {
|
|
861
1021
|
let buf2 = "";
|
|
@@ -864,7 +1024,15 @@ var bashTool = {
|
|
|
864
1024
|
cwd: ctx.projectRoot,
|
|
865
1025
|
env,
|
|
866
1026
|
stdio: ["ignore", "pipe", "pipe"],
|
|
867
|
-
|
|
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,
|
|
1035
|
+
windowsHide: true,
|
|
868
1036
|
signal: opts.signal
|
|
869
1037
|
});
|
|
870
1038
|
const pid2 = child2.pid;
|
|
@@ -879,24 +1047,22 @@ var bashTool = {
|
|
|
879
1047
|
});
|
|
880
1048
|
child2.on("close", () => registry.unregister(pid2));
|
|
881
1049
|
}
|
|
882
|
-
|
|
883
|
-
if (
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
}
|
|
888
|
-
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);
|
|
889
1055
|
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
if (remain > 0) {
|
|
895
|
-
buf2 += chunk.toString().slice(0, remain);
|
|
896
|
-
}
|
|
897
|
-
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);
|
|
898
1060
|
}
|
|
899
|
-
}
|
|
1061
|
+
};
|
|
1062
|
+
child2.stdout?.on("data", onBgData);
|
|
1063
|
+
child2.stderr?.on("data", onBgData);
|
|
1064
|
+
child2.stdout?.unref?.();
|
|
1065
|
+
child2.stderr?.unref?.();
|
|
900
1066
|
child2.on("close", () => {
|
|
901
1067
|
registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
|
|
902
1068
|
});
|
|
@@ -917,6 +1083,7 @@ var bashTool = {
|
|
|
917
1083
|
env,
|
|
918
1084
|
stdio: ["ignore", "pipe", "pipe"],
|
|
919
1085
|
detached,
|
|
1086
|
+
windowsHide: true,
|
|
920
1087
|
...isWin ? {} : { signal: opts.signal }
|
|
921
1088
|
});
|
|
922
1089
|
const pid = child.pid;
|
|
@@ -931,9 +1098,10 @@ var bashTool = {
|
|
|
931
1098
|
});
|
|
932
1099
|
}
|
|
933
1100
|
let buf = "";
|
|
934
|
-
let
|
|
1101
|
+
let pending2 = "";
|
|
935
1102
|
let timedOut = false;
|
|
936
1103
|
const timers = [];
|
|
1104
|
+
const spool = createOutputSpool({ tool: "bash", thresholdBytes: MAX_OUTPUT });
|
|
937
1105
|
function killWithTimeout(child2, timeoutMs2) {
|
|
938
1106
|
if (isWin) {
|
|
939
1107
|
if (typeof child2.pid === "number" && child2.exitCode === null && killWin32Tree(child2.pid)) {
|
|
@@ -1013,9 +1181,9 @@ var bashTool = {
|
|
|
1013
1181
|
});
|
|
1014
1182
|
let lastFlush = Date.now();
|
|
1015
1183
|
const flush = () => {
|
|
1016
|
-
if (
|
|
1017
|
-
const text =
|
|
1018
|
-
|
|
1184
|
+
if (pending2.length === 0) return null;
|
|
1185
|
+
const text = pending2;
|
|
1186
|
+
pending2 = "";
|
|
1019
1187
|
lastFlush = Date.now();
|
|
1020
1188
|
return text;
|
|
1021
1189
|
};
|
|
@@ -1039,7 +1207,8 @@ var bashTool = {
|
|
|
1039
1207
|
if (buf.length < MAX_OUTPUT) {
|
|
1040
1208
|
buf += text.slice(0, MAX_OUTPUT - buf.length);
|
|
1041
1209
|
}
|
|
1042
|
-
|
|
1210
|
+
spool.write(text);
|
|
1211
|
+
pending2 += text;
|
|
1043
1212
|
push({ kind: "data", text });
|
|
1044
1213
|
pauseIfFlooded();
|
|
1045
1214
|
};
|
|
@@ -1066,10 +1235,11 @@ var bashTool = {
|
|
|
1066
1235
|
if (remainder !== null) {
|
|
1067
1236
|
yield { type: "partial_output", text: remainder };
|
|
1068
1237
|
}
|
|
1238
|
+
const spooled = spool.finalize();
|
|
1069
1239
|
yield {
|
|
1070
1240
|
type: "final",
|
|
1071
1241
|
output: {
|
|
1072
|
-
output: normalizeCommandOutput(buf),
|
|
1242
|
+
output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : ""),
|
|
1073
1243
|
exit_code: c.code,
|
|
1074
1244
|
timed_out: timedOut
|
|
1075
1245
|
}
|
|
@@ -1077,13 +1247,14 @@ var bashTool = {
|
|
|
1077
1247
|
return;
|
|
1078
1248
|
}
|
|
1079
1249
|
const now = Date.now();
|
|
1080
|
-
if (
|
|
1250
|
+
if (pending2.length >= STREAM_FLUSH_BYTES || now - lastFlush >= STREAM_FLUSH_INTERVAL_MS) {
|
|
1081
1251
|
const text = flush();
|
|
1082
1252
|
if (text) yield { type: "partial_output", text };
|
|
1083
1253
|
}
|
|
1084
1254
|
}
|
|
1085
1255
|
} finally {
|
|
1086
1256
|
for (const t of timers) clearTimeout(t);
|
|
1257
|
+
spool.finalize();
|
|
1087
1258
|
if (isWin) opts.signal.removeEventListener("abort", onAbort);
|
|
1088
1259
|
child.stdout?.off("data", onData);
|
|
1089
1260
|
child.stderr?.off("data", onData);
|
|
@@ -1201,8 +1372,88 @@ async function executeSingle(call, ctx, opts) {
|
|
|
1201
1372
|
}
|
|
1202
1373
|
}
|
|
1203
1374
|
|
|
1375
|
+
// src/codebase-index/circuit-breaker.ts
|
|
1376
|
+
var CircuitOpenError = class extends Error {
|
|
1377
|
+
name = "CircuitOpenError";
|
|
1378
|
+
};
|
|
1379
|
+
var IndexTimeoutError = class extends Error {
|
|
1380
|
+
name = "IndexTimeoutError";
|
|
1381
|
+
};
|
|
1382
|
+
var LockError = class extends Error {
|
|
1383
|
+
name = "LockError";
|
|
1384
|
+
};
|
|
1385
|
+
var IndexCircuitBreaker = class {
|
|
1386
|
+
failureThreshold;
|
|
1387
|
+
cooldownMs;
|
|
1388
|
+
now;
|
|
1389
|
+
state = "closed";
|
|
1390
|
+
consecutiveFailures = 0;
|
|
1391
|
+
openedAt = 0;
|
|
1392
|
+
lastFailure = null;
|
|
1393
|
+
probeInFlight = false;
|
|
1394
|
+
constructor(opts = {}) {
|
|
1395
|
+
this.failureThreshold = opts.failureThreshold ?? 3;
|
|
1396
|
+
this.cooldownMs = opts.cooldownMs ?? 6e4;
|
|
1397
|
+
this.now = opts.now ?? Date.now;
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* True when a run may proceed. An open circuit transitions to half-open once
|
|
1401
|
+
* the cooldown has elapsed, admitting exactly one probe; further requests
|
|
1402
|
+
* are rejected until that probe settles via recordSuccess/recordFailure.
|
|
1403
|
+
*/
|
|
1404
|
+
allowRequest() {
|
|
1405
|
+
if (this.state === "closed") return true;
|
|
1406
|
+
if (this.state === "open") {
|
|
1407
|
+
if (this.now() - this.openedAt < this.cooldownMs) return false;
|
|
1408
|
+
this.state = "half-open";
|
|
1409
|
+
this.probeInFlight = true;
|
|
1410
|
+
return true;
|
|
1411
|
+
}
|
|
1412
|
+
if (this.probeInFlight) return false;
|
|
1413
|
+
this.probeInFlight = true;
|
|
1414
|
+
return true;
|
|
1415
|
+
}
|
|
1416
|
+
recordSuccess() {
|
|
1417
|
+
this.state = "closed";
|
|
1418
|
+
this.consecutiveFailures = 0;
|
|
1419
|
+
this.lastFailure = null;
|
|
1420
|
+
this.probeInFlight = false;
|
|
1421
|
+
}
|
|
1422
|
+
recordFailure(err) {
|
|
1423
|
+
if (err instanceof LockError) {
|
|
1424
|
+
this.lastFailure = `[transient/lock] ${err.message}`;
|
|
1425
|
+
this.probeInFlight = false;
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
this.lastFailure = err instanceof Error ? err.message : String(err);
|
|
1429
|
+
this.probeInFlight = false;
|
|
1430
|
+
this.consecutiveFailures++;
|
|
1431
|
+
if (this.state === "half-open" || this.consecutiveFailures >= this.failureThreshold) {
|
|
1432
|
+
this.state = "open";
|
|
1433
|
+
this.openedAt = this.now();
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
/** Force-close the circuit (manual recovery: `/codebase-reindex`). */
|
|
1437
|
+
reset() {
|
|
1438
|
+
this.state = "closed";
|
|
1439
|
+
this.consecutiveFailures = 0;
|
|
1440
|
+
this.lastFailure = null;
|
|
1441
|
+
this.probeInFlight = false;
|
|
1442
|
+
this.openedAt = 0;
|
|
1443
|
+
}
|
|
1444
|
+
snapshot() {
|
|
1445
|
+
return {
|
|
1446
|
+
state: this.state,
|
|
1447
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
1448
|
+
lastFailure: this.lastFailure,
|
|
1449
|
+
cooldownRemainingMs: this.state === "open" ? Math.max(0, this.cooldownMs - (this.now() - this.openedAt)) : 0
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
};
|
|
1453
|
+
var indexCircuitBreaker = new IndexCircuitBreaker();
|
|
1454
|
+
|
|
1204
1455
|
// src/codebase-index/schema.ts
|
|
1205
|
-
var SCHEMA_VERSION =
|
|
1456
|
+
var SCHEMA_VERSION = 2;
|
|
1206
1457
|
|
|
1207
1458
|
// src/codebase-index/lsp-kind.ts
|
|
1208
1459
|
function lspKindToInternalKind(k) {
|
|
@@ -1237,6 +1488,94 @@ function lspKindToInternalKind(k) {
|
|
|
1237
1488
|
}
|
|
1238
1489
|
}
|
|
1239
1490
|
|
|
1491
|
+
// src/codebase-index/bm25.ts
|
|
1492
|
+
var K1 = 1.5;
|
|
1493
|
+
var B = 0.75;
|
|
1494
|
+
function tokenise(text) {
|
|
1495
|
+
const sanitised = text.replace(/[^\p{L}\p{N}$'_]/gu, " ").replace(/_/g, " ");
|
|
1496
|
+
return sanitised.toLowerCase().split(" ").filter(Boolean);
|
|
1497
|
+
}
|
|
1498
|
+
function splitName(name) {
|
|
1499
|
+
return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
1500
|
+
}
|
|
1501
|
+
function buildIndexableText(name, signature, docComment) {
|
|
1502
|
+
return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
|
|
1503
|
+
}
|
|
1504
|
+
function buildBm25Index(docs) {
|
|
1505
|
+
const documents = docs.map((d) => {
|
|
1506
|
+
const tokens = tokenise(d.text);
|
|
1507
|
+
return { id: d.id, tokens, raw: d.text, len: tokens.length };
|
|
1508
|
+
});
|
|
1509
|
+
const df = {};
|
|
1510
|
+
for (const doc of documents) {
|
|
1511
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1512
|
+
for (const t of doc.tokens) {
|
|
1513
|
+
if (!seen.has(t)) {
|
|
1514
|
+
df[t] = (df[t] ?? 0) + 1;
|
|
1515
|
+
seen.add(t);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
const N = documents.length;
|
|
1520
|
+
const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
|
|
1521
|
+
const avgLen = N === 0 ? 0 : totalLen / N;
|
|
1522
|
+
return new Bm25Index(documents, df, N, avgLen);
|
|
1523
|
+
}
|
|
1524
|
+
var Bm25Index = class {
|
|
1525
|
+
constructor(documents, df, N, avgLen) {
|
|
1526
|
+
this.documents = documents;
|
|
1527
|
+
this.df = df;
|
|
1528
|
+
this.N = N;
|
|
1529
|
+
this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
|
|
1530
|
+
}
|
|
1531
|
+
documents;
|
|
1532
|
+
df;
|
|
1533
|
+
N;
|
|
1534
|
+
safeAvgLen;
|
|
1535
|
+
score(query2, filter) {
|
|
1536
|
+
const qTokens = tokenise(query2);
|
|
1537
|
+
if (qTokens.length === 0) return [];
|
|
1538
|
+
const results = [];
|
|
1539
|
+
for (const doc of this.documents) {
|
|
1540
|
+
if (filter && !filter(doc.id)) continue;
|
|
1541
|
+
let docScore = 0;
|
|
1542
|
+
for (const qTerm of qTokens) {
|
|
1543
|
+
let tf = 0;
|
|
1544
|
+
for (const t of doc.tokens) {
|
|
1545
|
+
if (t === qTerm) tf++;
|
|
1546
|
+
}
|
|
1547
|
+
if (tf === 0) continue;
|
|
1548
|
+
const dfVal = this.df[qTerm] ?? 0;
|
|
1549
|
+
if (dfVal === 0) continue;
|
|
1550
|
+
const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
|
|
1551
|
+
const lenRatio = B * (doc.len / this.safeAvgLen);
|
|
1552
|
+
const tfComponent = tf * (K1 + 1) / (tf + K1 * (1 - B + lenRatio));
|
|
1553
|
+
docScore += idf * tfComponent;
|
|
1554
|
+
}
|
|
1555
|
+
if (docScore > 0) results.push({ id: doc.id, score: docScore });
|
|
1556
|
+
}
|
|
1557
|
+
return results;
|
|
1558
|
+
}
|
|
1559
|
+
getDoc(id) {
|
|
1560
|
+
return this.documents.find((d) => d.id === id);
|
|
1561
|
+
}
|
|
1562
|
+
extractSnippet(docId, queryTokens, radius = 40) {
|
|
1563
|
+
const doc = this.getDoc(docId);
|
|
1564
|
+
if (!doc) return "";
|
|
1565
|
+
for (const tok of queryTokens) {
|
|
1566
|
+
const idx = doc.raw.toLowerCase().indexOf(tok);
|
|
1567
|
+
if (idx !== -1) {
|
|
1568
|
+
const start = Math.max(0, idx - radius);
|
|
1569
|
+
const end = Math.min(doc.raw.length, idx + tok.length + radius);
|
|
1570
|
+
const excerpt = doc.raw.slice(start, end);
|
|
1571
|
+
const ellipsis = "\u2026";
|
|
1572
|
+
return (start > 0 ? ellipsis : "") + excerpt + (end < doc.raw.length ? ellipsis : "");
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
return doc.raw.slice(0, radius * 2) + (doc.raw.length > radius * 2 ? "\u2026" : "");
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1578
|
+
|
|
1240
1579
|
// src/codebase-index/writer.ts
|
|
1241
1580
|
var DB_FILE = "index.db";
|
|
1242
1581
|
function resolveIndexDir(projectRoot, override) {
|
|
@@ -1272,15 +1611,79 @@ function loadDatabaseSync() {
|
|
|
1272
1611
|
}
|
|
1273
1612
|
return DatabaseSyncCtor;
|
|
1274
1613
|
}
|
|
1614
|
+
var MAX_LOCK_RETRIES = 3;
|
|
1615
|
+
var LOCK_RETRY_BASE_DELAY_MS = 50;
|
|
1616
|
+
var LOCK_RETRY_MAX_DELAY_MS = 500;
|
|
1617
|
+
function isLockError(err) {
|
|
1618
|
+
if (!(err instanceof Error)) return false;
|
|
1619
|
+
const e = err;
|
|
1620
|
+
const code = e.code ?? e.sqliteCode;
|
|
1621
|
+
if (typeof code === "string" && /SQLITE_(BUSY|LOCKED)/.test(code)) return true;
|
|
1622
|
+
if (typeof code === "number" && (code === 5 || code === 6)) return true;
|
|
1623
|
+
if (/SQLITE_(BUSY|LOCKED)/.test(err.message)) return true;
|
|
1624
|
+
return false;
|
|
1625
|
+
}
|
|
1626
|
+
function sleepSync(ms) {
|
|
1627
|
+
try {
|
|
1628
|
+
const sab = new SharedArrayBuffer(4);
|
|
1629
|
+
const view = new Int32Array(sab);
|
|
1630
|
+
Atomics.wait(view, 0, 0, ms);
|
|
1631
|
+
} catch {
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1275
1634
|
var IndexStore = class {
|
|
1276
1635
|
db;
|
|
1277
1636
|
/** Absolute path to this project's index directory. */
|
|
1278
1637
|
indexDir;
|
|
1638
|
+
/**
|
|
1639
|
+
* True when the SQLite build provides FTS5 (Node's bundled SQLite does).
|
|
1640
|
+
* When false, ranked search falls back to the LIKE + in-process BM25 path.
|
|
1641
|
+
*/
|
|
1642
|
+
ftsAvailable = false;
|
|
1643
|
+
/**
|
|
1644
|
+
* Execute a SQLite write operation with automatic retry on lock conflicts.
|
|
1645
|
+
*
|
|
1646
|
+
* When another wstack process is holding the write lock the statement first
|
|
1647
|
+
* waits up to `busy_timeout` ms, then throws SQLITE_BUSY. This wrapper catches
|
|
1648
|
+
* that error and retries (up to MAX_LOCK_RETRIES) with exponential backoff,
|
|
1649
|
+
* giving the competing writer time to finish and release the lock.
|
|
1650
|
+
*
|
|
1651
|
+
* @param fn The write operation to execute. Can return a value which is
|
|
1652
|
+
* returned to the caller on success.
|
|
1653
|
+
* @throws {@link LockError} when all retries are exhausted on a lock conflict
|
|
1654
|
+
* (non-lock errors always propagate on the first attempt).
|
|
1655
|
+
*/
|
|
1656
|
+
runWithRetry(fn) {
|
|
1657
|
+
let lastError;
|
|
1658
|
+
for (let attempt = 0; attempt <= MAX_LOCK_RETRIES; attempt++) {
|
|
1659
|
+
try {
|
|
1660
|
+
return fn();
|
|
1661
|
+
} catch (err) {
|
|
1662
|
+
lastError = err;
|
|
1663
|
+
if (!isLockError(err)) throw err;
|
|
1664
|
+
if (attempt === MAX_LOCK_RETRIES) {
|
|
1665
|
+
const msg = lastError instanceof Error ? lastError.message : String(lastError);
|
|
1666
|
+
throw new LockError(`SQLite lock conflict after ${MAX_LOCK_RETRIES} retries: ${msg}`);
|
|
1667
|
+
}
|
|
1668
|
+
const delay = Math.min(
|
|
1669
|
+
LOCK_RETRY_BASE_DELAY_MS * Math.pow(2, attempt),
|
|
1670
|
+
LOCK_RETRY_MAX_DELAY_MS
|
|
1671
|
+
);
|
|
1672
|
+
sleepSync(delay);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
throw lastError;
|
|
1676
|
+
}
|
|
1279
1677
|
constructor(projectRoot, opts = {}) {
|
|
1280
1678
|
this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
|
|
1281
1679
|
fs.mkdirSync(this.indexDir, { recursive: true });
|
|
1282
1680
|
const Database = loadDatabaseSync();
|
|
1283
|
-
this.db = new Database(
|
|
1681
|
+
this.db = new Database(path3.join(this.indexDir, DB_FILE));
|
|
1682
|
+
try {
|
|
1683
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
1684
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
1685
|
+
} catch {
|
|
1686
|
+
}
|
|
1284
1687
|
this.initSchema();
|
|
1285
1688
|
}
|
|
1286
1689
|
initSchema() {
|
|
@@ -1289,6 +1692,21 @@ var IndexStore = class {
|
|
|
1289
1692
|
key TEXT PRIMARY KEY,
|
|
1290
1693
|
value TEXT NOT NULL
|
|
1291
1694
|
);
|
|
1695
|
+
`);
|
|
1696
|
+
const storedRows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all("version");
|
|
1697
|
+
const storedVersion = storedRows.length ? Number(storedRows[0]?.value) : null;
|
|
1698
|
+
if (storedVersion !== null && storedVersion !== SCHEMA_VERSION) {
|
|
1699
|
+
this.db.exec(`
|
|
1700
|
+
DROP TABLE IF EXISTS symbols;
|
|
1701
|
+
DROP TABLE IF EXISTS files;
|
|
1702
|
+
DROP TABLE IF EXISTS refs;
|
|
1703
|
+
`);
|
|
1704
|
+
this.db.exec("DROP TABLE IF EXISTS symbols_fts");
|
|
1705
|
+
this.db.prepare("UPDATE metadata SET value = ? WHERE key = ?").run(String(SCHEMA_VERSION), "version");
|
|
1706
|
+
} else if (storedVersion === null) {
|
|
1707
|
+
this.db.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
|
|
1708
|
+
}
|
|
1709
|
+
this.db.exec(`
|
|
1292
1710
|
CREATE TABLE IF NOT EXISTS files (
|
|
1293
1711
|
file TEXT PRIMARY KEY,
|
|
1294
1712
|
lang TEXT NOT NULL,
|
|
@@ -1329,53 +1747,76 @@ var IndexStore = class {
|
|
|
1329
1747
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)");
|
|
1330
1748
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)");
|
|
1331
1749
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)");
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
this.
|
|
1750
|
+
try {
|
|
1751
|
+
this.db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')");
|
|
1752
|
+
this.ftsAvailable = true;
|
|
1753
|
+
} catch {
|
|
1754
|
+
this.ftsAvailable = false;
|
|
1335
1755
|
}
|
|
1336
1756
|
}
|
|
1337
1757
|
// ─── Symbol CRUD ─────────────────────────────────────────────────────────────
|
|
1338
1758
|
insertSymbols(symbols, nextId) {
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
let id = nextId;
|
|
1344
|
-
for (const s of symbols) {
|
|
1345
|
-
stmt.run(
|
|
1346
|
-
id++,
|
|
1347
|
-
s.lang,
|
|
1348
|
-
s.kind,
|
|
1349
|
-
s.name,
|
|
1350
|
-
s.file,
|
|
1351
|
-
s.line,
|
|
1352
|
-
s.col,
|
|
1353
|
-
s.signature,
|
|
1354
|
-
s.docComment,
|
|
1355
|
-
s.scope,
|
|
1356
|
-
s.text,
|
|
1357
|
-
s.file
|
|
1759
|
+
return this.runWithRetry(() => {
|
|
1760
|
+
const stmt = this.db.prepare(
|
|
1761
|
+
`INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
|
|
1762
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1358
1763
|
);
|
|
1359
|
-
|
|
1360
|
-
|
|
1764
|
+
const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
|
|
1765
|
+
let id = nextId;
|
|
1766
|
+
for (const s of symbols) {
|
|
1767
|
+
stmt.run(
|
|
1768
|
+
id,
|
|
1769
|
+
s.lang,
|
|
1770
|
+
s.kind,
|
|
1771
|
+
s.name,
|
|
1772
|
+
s.file,
|
|
1773
|
+
s.line,
|
|
1774
|
+
s.col,
|
|
1775
|
+
s.signature,
|
|
1776
|
+
s.docComment,
|
|
1777
|
+
s.scope,
|
|
1778
|
+
s.text,
|
|
1779
|
+
s.file
|
|
1780
|
+
);
|
|
1781
|
+
ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
|
|
1782
|
+
id++;
|
|
1783
|
+
}
|
|
1784
|
+
return id;
|
|
1785
|
+
});
|
|
1361
1786
|
}
|
|
1362
1787
|
deleteSymbolsForFile(file) {
|
|
1363
|
-
this.
|
|
1788
|
+
this.runWithRetry(() => {
|
|
1789
|
+
if (this.ftsAvailable) {
|
|
1790
|
+
this.db.prepare("DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
|
|
1791
|
+
}
|
|
1792
|
+
this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
|
|
1793
|
+
});
|
|
1364
1794
|
}
|
|
1795
|
+
/**
|
|
1796
|
+
* Remove every trace of a file (refs, symbols, FTS rows, file meta). Used
|
|
1797
|
+
* when a source file disappears between index runs — previously this only
|
|
1798
|
+
* dropped the `files` row, leaving its symbols orphaned but still searchable.
|
|
1799
|
+
*/
|
|
1365
1800
|
deleteFile(file) {
|
|
1366
|
-
this.
|
|
1801
|
+
this.runWithRetry(() => {
|
|
1802
|
+
this.deleteRefsForFile(file);
|
|
1803
|
+
this.deleteSymbolsForFile(file);
|
|
1804
|
+
this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
|
|
1805
|
+
});
|
|
1367
1806
|
}
|
|
1368
1807
|
// ─── File metadata ──────────────────────────────────────────────────────────
|
|
1369
1808
|
upsertFile(meta) {
|
|
1370
|
-
this.
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1809
|
+
this.runWithRetry(() => {
|
|
1810
|
+
this.db.prepare(
|
|
1811
|
+
`INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
|
|
1812
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1813
|
+
ON CONFLICT(file) DO UPDATE SET
|
|
1814
|
+
lang = excluded.lang,
|
|
1815
|
+
mtime_ms = excluded.mtime_ms,
|
|
1816
|
+
symbol_count = excluded.symbol_count,
|
|
1817
|
+
last_indexed = excluded.last_indexed`
|
|
1818
|
+
).run(meta.file, meta.lang, meta.mtimeMs, meta.symbolCount, meta.lastIndexed);
|
|
1819
|
+
});
|
|
1379
1820
|
}
|
|
1380
1821
|
getFileMeta(file) {
|
|
1381
1822
|
const rows = this.db.prepare(
|
|
@@ -1442,6 +1883,94 @@ var IndexStore = class {
|
|
|
1442
1883
|
lspKind: filter?.lspKind
|
|
1443
1884
|
}));
|
|
1444
1885
|
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Ranked search — the one-stop query the codebase-search tool and plug-lsp
|
|
1888
|
+
* use. With FTS5 this is a single indexed `MATCH` ranked by SQLite's native
|
|
1889
|
+
* `bm25()` with a built-in `snippet()`; without FTS5 it falls back to the
|
|
1890
|
+
* legacy LIKE scan + in-process BM25 (identical semantics, slower).
|
|
1891
|
+
*
|
|
1892
|
+
* Tokens are matched as prefixes (`"tok"*`), mirroring the old
|
|
1893
|
+
* `LIKE '%tok%'` recall for the common symbol-search shapes ("user" finds
|
|
1894
|
+
* "users", camelCase-split text makes "complex" find "complexOperation").
|
|
1895
|
+
*/
|
|
1896
|
+
searchRanked(query2, filter, limit) {
|
|
1897
|
+
const tokens = tokenise(query2);
|
|
1898
|
+
if (tokens.length === 0 || !this.ftsAvailable) {
|
|
1899
|
+
return this.searchRankedFallback(query2, filter, limit);
|
|
1900
|
+
}
|
|
1901
|
+
let effectiveKind = filter?.kind;
|
|
1902
|
+
if (filter?.lspKind !== void 0) {
|
|
1903
|
+
const mapped = lspKindToInternalKind(filter.lspKind);
|
|
1904
|
+
if (mapped === null) return { results: [], total: 0 };
|
|
1905
|
+
effectiveKind = mapped;
|
|
1906
|
+
}
|
|
1907
|
+
const match = tokens.map((t) => `"${t.replaceAll('"', "")}"*`).join(" OR ");
|
|
1908
|
+
const conditions = ["symbols_fts MATCH ?"];
|
|
1909
|
+
const values = [match];
|
|
1910
|
+
if (effectiveKind) {
|
|
1911
|
+
conditions.push("s.kind = ?");
|
|
1912
|
+
values.push(effectiveKind);
|
|
1913
|
+
}
|
|
1914
|
+
if (filter?.lang) {
|
|
1915
|
+
conditions.push("s.lang = ?");
|
|
1916
|
+
values.push(filter.lang);
|
|
1917
|
+
}
|
|
1918
|
+
if (filter?.file) {
|
|
1919
|
+
conditions.push("s.file LIKE ?");
|
|
1920
|
+
values.push(`%${filter.file}%`);
|
|
1921
|
+
}
|
|
1922
|
+
const where = conditions.join(" AND ");
|
|
1923
|
+
const countRows = this.db.prepare(`SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`).all(...values);
|
|
1924
|
+
const total = countRows[0] ? Number(countRows[0].n) : 0;
|
|
1925
|
+
if (total === 0) return { results: [], total: 0 };
|
|
1926
|
+
const rows = this.db.prepare(
|
|
1927
|
+
`SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
|
|
1928
|
+
-bm25(symbols_fts) AS score,
|
|
1929
|
+
snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
|
|
1930
|
+
FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
|
|
1931
|
+
WHERE ${where}
|
|
1932
|
+
ORDER BY bm25(symbols_fts)
|
|
1933
|
+
LIMIT ?`
|
|
1934
|
+
).all(...values, limit);
|
|
1935
|
+
return {
|
|
1936
|
+
results: rows.map((r) => ({
|
|
1937
|
+
id: r.id,
|
|
1938
|
+
lang: r.lang,
|
|
1939
|
+
kind: r.kind,
|
|
1940
|
+
name: r.name,
|
|
1941
|
+
file: r.file,
|
|
1942
|
+
line: r.line,
|
|
1943
|
+
col: r.col,
|
|
1944
|
+
signature: r.signature,
|
|
1945
|
+
docComment: r.doc_comment,
|
|
1946
|
+
// bm25() is negative-is-better; negate so callers keep "higher is
|
|
1947
|
+
// better" and clamp so a match never reports a zero score.
|
|
1948
|
+
score: Math.max(1e-4, r.score),
|
|
1949
|
+
snippet: r.snippet,
|
|
1950
|
+
lspKind: filter?.lspKind
|
|
1951
|
+
})),
|
|
1952
|
+
total
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
/** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
|
|
1956
|
+
searchRankedFallback(query2, filter, limit) {
|
|
1957
|
+
const candidates = this.search(query2, filter);
|
|
1958
|
+
if (candidates.length === 0) return { results: [], total: 0 };
|
|
1959
|
+
if (!query2.trim()) {
|
|
1960
|
+
return { results: candidates.slice(0, limit), total: candidates.length };
|
|
1961
|
+
}
|
|
1962
|
+
const bm25 = buildBm25Index(
|
|
1963
|
+
candidates.map((c) => ({ id: c.id, text: buildIndexableText(c.name, c.signature, c.docComment) }))
|
|
1964
|
+
);
|
|
1965
|
+
const scored = bm25.score(query2, (id) => candidates.some((c) => c.id === id));
|
|
1966
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1967
|
+
const qTokens = tokenise(query2);
|
|
1968
|
+
const results = scored.slice(0, limit).map(({ id, score }) => {
|
|
1969
|
+
const c = expectDefined(candidates.find((cand) => cand.id === id));
|
|
1970
|
+
return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
|
|
1971
|
+
});
|
|
1972
|
+
return { results, total: candidates.length };
|
|
1973
|
+
}
|
|
1445
1974
|
getAllIndexable() {
|
|
1446
1975
|
return this.db.prepare("SELECT id, text FROM symbols").all().map(
|
|
1447
1976
|
({ id, text }) => ({ id, text })
|
|
@@ -1491,14 +2020,19 @@ var IndexStore = class {
|
|
|
1491
2020
|
};
|
|
1492
2021
|
}
|
|
1493
2022
|
setLastIndexed(ts2) {
|
|
1494
|
-
this.
|
|
1495
|
-
|
|
1496
|
-
|
|
2023
|
+
this.runWithRetry(() => {
|
|
2024
|
+
this.db.prepare(
|
|
2025
|
+
"INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)"
|
|
2026
|
+
).run(String(ts2));
|
|
2027
|
+
});
|
|
1497
2028
|
}
|
|
1498
2029
|
clearAll() {
|
|
1499
|
-
this.
|
|
1500
|
-
|
|
1501
|
-
|
|
2030
|
+
this.runWithRetry(() => {
|
|
2031
|
+
this.db.exec("DELETE FROM symbols");
|
|
2032
|
+
this.db.exec("DELETE FROM files");
|
|
2033
|
+
this.db.exec("DELETE FROM refs");
|
|
2034
|
+
if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
|
|
2035
|
+
});
|
|
1502
2036
|
}
|
|
1503
2037
|
// ─── Ref CRUD ────────────────────────────────────────────────────────────────
|
|
1504
2038
|
/**
|
|
@@ -1506,46 +2040,52 @@ var IndexStore = class {
|
|
|
1506
2040
|
* Replaces any existing refs from the same source (idempotent on re-index).
|
|
1507
2041
|
*/
|
|
1508
2042
|
insertRefs(fromId, refs) {
|
|
1509
|
-
this.
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
2043
|
+
this.runWithRetry(() => {
|
|
2044
|
+
this.db.prepare("DELETE FROM refs WHERE from_id = ?").run(fromId);
|
|
2045
|
+
if (refs.length === 0) return;
|
|
2046
|
+
const stmt = this.db.prepare(
|
|
2047
|
+
`INSERT INTO refs(from_id, to_name, to_id, call_type, line)
|
|
2048
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
2049
|
+
);
|
|
2050
|
+
for (const ref of refs) {
|
|
2051
|
+
stmt.run(fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
|
|
2052
|
+
}
|
|
2053
|
+
});
|
|
1518
2054
|
}
|
|
1519
2055
|
/**
|
|
1520
2056
|
* Delete all refs whose source symbols are in a given file.
|
|
1521
2057
|
* Used when re-indexing a file to clear stale refs.
|
|
1522
2058
|
*/
|
|
1523
2059
|
deleteRefsForFile(file) {
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
2060
|
+
this.runWithRetry(() => {
|
|
2061
|
+
const ids = this.db.prepare(
|
|
2062
|
+
"SELECT id FROM symbols WHERE file = ?"
|
|
2063
|
+
).all(file);
|
|
2064
|
+
if (!ids.length) return;
|
|
2065
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
2066
|
+
this.db.prepare(`DELETE FROM refs WHERE from_id IN (${placeholders})`).run(...ids.map((r) => r.id));
|
|
2067
|
+
});
|
|
1530
2068
|
}
|
|
1531
2069
|
/**
|
|
1532
2070
|
* Resolve `to_name` → `to_id` for all refs that have a name but no id.
|
|
1533
2071
|
* Call this after all symbols have been inserted to fill in cross-references.
|
|
1534
2072
|
*/
|
|
1535
2073
|
resolveRefs() {
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
2074
|
+
return this.runWithRetry(() => {
|
|
2075
|
+
const unresolved = this.db.prepare(
|
|
2076
|
+
"SELECT id, to_name FROM refs WHERE to_id IS NULL AND to_name IS NOT NULL"
|
|
2077
|
+
).all();
|
|
2078
|
+
let resolved = 0;
|
|
2079
|
+
for (const row of unresolved) {
|
|
2080
|
+
const target = this.db.prepare("SELECT id FROM symbols WHERE name = ? LIMIT 1").all(row.to_name);
|
|
2081
|
+
const first = target[0];
|
|
2082
|
+
if (first) {
|
|
2083
|
+
this.db.prepare("UPDATE refs SET to_id = ? WHERE id = ?").run(first.id, row.id);
|
|
2084
|
+
resolved++;
|
|
2085
|
+
}
|
|
1546
2086
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
2087
|
+
return resolved;
|
|
2088
|
+
});
|
|
1549
2089
|
}
|
|
1550
2090
|
/**
|
|
1551
2091
|
* Find all references TO a given symbol (who calls / uses this symbol?).
|
|
@@ -1578,7 +2118,7 @@ var IndexStore = class {
|
|
|
1578
2118
|
}));
|
|
1579
2119
|
}
|
|
1580
2120
|
sizeBytes() {
|
|
1581
|
-
const dbPath =
|
|
2121
|
+
const dbPath = path3.join(this.indexDir, DB_FILE);
|
|
1582
2122
|
try {
|
|
1583
2123
|
return fs.statSync(dbPath).size;
|
|
1584
2124
|
} catch {
|
|
@@ -2017,10 +2557,10 @@ func formatType(t ast.Expr) string {
|
|
|
2017
2557
|
}
|
|
2018
2558
|
`;
|
|
2019
2559
|
function syncGoParse(filePath, content, lang) {
|
|
2020
|
-
const tmpDir =
|
|
2560
|
+
const tmpDir = path3.join(os.tmpdir(), "ws-go-parse");
|
|
2021
2561
|
try {
|
|
2022
2562
|
mkdirSync(tmpDir, { recursive: true });
|
|
2023
|
-
const scriptPath =
|
|
2563
|
+
const scriptPath = path3.join(tmpDir, "parse.go");
|
|
2024
2564
|
writeFileSync(scriptPath, GO_PARSE_SCRIPT, "utf8");
|
|
2025
2565
|
const stdout = execFileSync("go", ["run", scriptPath], {
|
|
2026
2566
|
input: content,
|
|
@@ -2264,9 +2804,9 @@ print(json.dumps([s.to_dict() for s in syms]))
|
|
|
2264
2804
|
`;
|
|
2265
2805
|
function syncPyParse(filePath, lang) {
|
|
2266
2806
|
try {
|
|
2267
|
-
const tmpDir =
|
|
2807
|
+
const tmpDir = path3.join(os.tmpdir(), "ws-py-parse");
|
|
2268
2808
|
mkdirSync(tmpDir, { recursive: true });
|
|
2269
|
-
const scriptPath =
|
|
2809
|
+
const scriptPath = path3.join(tmpDir, "parse.py");
|
|
2270
2810
|
writeFileSync(scriptPath, PY_PARSE_SCRIPT, "utf8");
|
|
2271
2811
|
const stdout = execFileSync("python", [scriptPath, filePath], {
|
|
2272
2812
|
timeout: 15e3,
|
|
@@ -2306,8 +2846,8 @@ function parseSymbols4(opts) {
|
|
|
2306
2846
|
}
|
|
2307
2847
|
function checkNativeParser() {
|
|
2308
2848
|
try {
|
|
2309
|
-
execFileSync("rustc", ["--version"], { stdio: "pipe" });
|
|
2310
|
-
const toolsDir =
|
|
2849
|
+
execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
|
|
2850
|
+
const toolsDir = path3.join(process.cwd(), "tools");
|
|
2311
2851
|
try {
|
|
2312
2852
|
execFileSync(
|
|
2313
2853
|
"cargo",
|
|
@@ -2317,9 +2857,9 @@ function checkNativeParser() {
|
|
|
2317
2857
|
"--format-version",
|
|
2318
2858
|
"1",
|
|
2319
2859
|
"--manifest-path",
|
|
2320
|
-
|
|
2860
|
+
path3.join(toolsDir, "Cargo.toml")
|
|
2321
2861
|
],
|
|
2322
|
-
{ stdio: "pipe" }
|
|
2862
|
+
{ stdio: "pipe", windowsHide: true }
|
|
2323
2863
|
);
|
|
2324
2864
|
return true;
|
|
2325
2865
|
} catch {
|
|
@@ -2331,18 +2871,19 @@ function checkNativeParser() {
|
|
|
2331
2871
|
}
|
|
2332
2872
|
function tryNativeParse(file, content) {
|
|
2333
2873
|
try {
|
|
2334
|
-
const toolsDir =
|
|
2335
|
-
const crateDir =
|
|
2336
|
-
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");
|
|
2337
2877
|
writeFileSync(tmpFile, content, "utf8");
|
|
2338
2878
|
const result = spawnSync(
|
|
2339
2879
|
"cargo",
|
|
2340
|
-
["run", "--manifest-path",
|
|
2880
|
+
["run", "--manifest-path", path3.join(toolsDir, "Cargo.toml")],
|
|
2341
2881
|
{
|
|
2342
2882
|
cwd: process.cwd(),
|
|
2343
2883
|
encoding: "utf8",
|
|
2344
2884
|
timeout: 15e3,
|
|
2345
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
2885
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2886
|
+
windowsHide: true
|
|
2346
2887
|
}
|
|
2347
2888
|
);
|
|
2348
2889
|
if (result.status === 0 && result.stdout) {
|
|
@@ -2435,7 +2976,7 @@ function parseSymbols5(opts) {
|
|
|
2435
2976
|
function regexParse2(opts) {
|
|
2436
2977
|
const { file, content, lang } = opts;
|
|
2437
2978
|
const symbols = [];
|
|
2438
|
-
const basename2 =
|
|
2979
|
+
const basename2 = path3.basename(file).toLowerCase();
|
|
2439
2980
|
const isPackageJson = basename2 === "package.json";
|
|
2440
2981
|
const isTsconfig = basename2 === "tsconfig.json" || basename2 === "tsconfig.build.json";
|
|
2441
2982
|
const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
|
|
@@ -2461,11 +3002,11 @@ function regexParse2(opts) {
|
|
|
2461
3002
|
const line = lineFromOffset(offset);
|
|
2462
3003
|
symbols.push(
|
|
2463
3004
|
makeSymbol({
|
|
2464
|
-
name:
|
|
3005
|
+
name: path3.basename(file),
|
|
2465
3006
|
kind: "object",
|
|
2466
3007
|
line,
|
|
2467
3008
|
col: 0,
|
|
2468
|
-
signature: `"${
|
|
3009
|
+
signature: `"${path3.basename(file)}" = { ... }`,
|
|
2469
3010
|
file,
|
|
2470
3011
|
lang
|
|
2471
3012
|
})
|
|
@@ -2756,10 +3297,6 @@ function isScalar(value) {
|
|
|
2756
3297
|
if (/^'[^']*'$/.test(value) || /^"[^"]*"$/.test(value)) return true;
|
|
2757
3298
|
return false;
|
|
2758
3299
|
}
|
|
2759
|
-
function truncate(s, max) {
|
|
2760
|
-
if (s.length <= max) return s;
|
|
2761
|
-
return s.slice(0, max) + "...";
|
|
2762
|
-
}
|
|
2763
3300
|
function makeSymbol2(opts) {
|
|
2764
3301
|
return {
|
|
2765
3302
|
id: 0,
|
|
@@ -2814,47 +3351,17 @@ function compileGitignore(lines) {
|
|
|
2814
3351
|
if (re.test(p)) ignored = !r.negated;
|
|
2815
3352
|
}
|
|
2816
3353
|
return ignored;
|
|
2817
|
-
};
|
|
2818
|
-
}
|
|
2819
|
-
async function loadGitignoreMatcher(projectRoot) {
|
|
2820
|
-
let lines = [];
|
|
2821
|
-
try {
|
|
2822
|
-
const raw = await
|
|
2823
|
-
lines = raw.split("\n");
|
|
2824
|
-
} catch {
|
|
2825
|
-
}
|
|
2826
|
-
return compileGitignore(lines);
|
|
2827
|
-
}
|
|
2828
|
-
|
|
2829
|
-
// src/codebase-index/background-indexer.ts
|
|
2830
|
-
var _ready = false;
|
|
2831
|
-
var _indexing = false;
|
|
2832
|
-
var _currentFile = 0;
|
|
2833
|
-
var _totalFiles = 0;
|
|
2834
|
-
var _lastError = null;
|
|
2835
|
-
function setIndexReady() {
|
|
2836
|
-
_ready = true;
|
|
2837
|
-
}
|
|
2838
|
-
function getIndexState() {
|
|
2839
|
-
return {
|
|
2840
|
-
ready: _ready,
|
|
2841
|
-
indexing: _indexing,
|
|
2842
|
-
currentFile: _currentFile,
|
|
2843
|
-
totalFiles: _totalFiles,
|
|
2844
|
-
lastError: _lastError
|
|
2845
|
-
};
|
|
2846
|
-
}
|
|
2847
|
-
var _listeners = [];
|
|
2848
|
-
function emitState() {
|
|
2849
|
-
const state = getIndexState();
|
|
2850
|
-
for (const l of _listeners) l(state);
|
|
2851
|
-
}
|
|
2852
|
-
function _setIndexProgress(current, total) {
|
|
2853
|
-
_currentFile = current;
|
|
2854
|
-
_totalFiles = total;
|
|
2855
|
-
emitState();
|
|
3354
|
+
};
|
|
3355
|
+
}
|
|
3356
|
+
async function loadGitignoreMatcher(projectRoot) {
|
|
3357
|
+
let lines = [];
|
|
3358
|
+
try {
|
|
3359
|
+
const raw = await fs14.readFile(path3.join(projectRoot, ".gitignore"), "utf8");
|
|
3360
|
+
lines = raw.split("\n");
|
|
3361
|
+
} catch {
|
|
3362
|
+
}
|
|
3363
|
+
return compileGitignore(lines);
|
|
2856
3364
|
}
|
|
2857
|
-
Promise.resolve();
|
|
2858
3365
|
|
|
2859
3366
|
// src/codebase-index/indexer.ts
|
|
2860
3367
|
var YIELD_EVERY_N = 50;
|
|
@@ -2906,21 +3413,21 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
2906
3413
|
}
|
|
2907
3414
|
let entries;
|
|
2908
3415
|
try {
|
|
2909
|
-
entries = await
|
|
3416
|
+
entries = await fs14.readdir(dir, { withFileTypes: true });
|
|
2910
3417
|
} catch {
|
|
2911
3418
|
return;
|
|
2912
3419
|
}
|
|
2913
3420
|
dirCount++;
|
|
2914
3421
|
for (const e of entries) {
|
|
2915
3422
|
if (ignoreSet.has(e.name)) continue;
|
|
2916
|
-
const full =
|
|
2917
|
-
const rel =
|
|
3423
|
+
const full = path3.join(dir, e.name);
|
|
3424
|
+
const rel = path3.relative(projectRoot, full).replace(/\\/g, "/");
|
|
2918
3425
|
if (e.isDirectory()) {
|
|
2919
3426
|
if (isGitIgnored(rel, true)) continue;
|
|
2920
3427
|
await walk(full);
|
|
2921
3428
|
} else if (e.isFile()) {
|
|
2922
3429
|
if (isGitIgnored(rel, false)) continue;
|
|
2923
|
-
const ext =
|
|
3430
|
+
const ext = path3.extname(e.name);
|
|
2924
3431
|
for (const { ext: extName, pat } of globs) {
|
|
2925
3432
|
if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
|
|
2926
3433
|
results.push(full);
|
|
@@ -2975,7 +3482,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2975
3482
|
const isGitIgnored = await loadGitignoreMatcher(projectRoot);
|
|
2976
3483
|
let files;
|
|
2977
3484
|
if (opts.files && opts.files.length > 0) {
|
|
2978
|
-
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));
|
|
2979
3486
|
} else {
|
|
2980
3487
|
files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
|
|
2981
3488
|
}
|
|
@@ -2993,25 +3500,25 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2993
3500
|
}
|
|
2994
3501
|
for (let fi = 0; fi < files.length; fi++) {
|
|
2995
3502
|
const file = expectDefined(files[fi]);
|
|
2996
|
-
|
|
3503
|
+
opts.onProgress?.(fi + 1, files.length);
|
|
2997
3504
|
if (fi > 0 && fi % YIELD_EVERY_N === 0) {
|
|
2998
3505
|
await yieldEventLoop();
|
|
2999
3506
|
throwIfAborted(signal);
|
|
3000
3507
|
}
|
|
3001
|
-
let
|
|
3508
|
+
let stat11;
|
|
3002
3509
|
try {
|
|
3003
3510
|
const statOpts = signal ? { signal } : {};
|
|
3004
|
-
|
|
3511
|
+
stat11 = await fs14.stat(file, statOpts);
|
|
3005
3512
|
} catch (e) {
|
|
3006
3513
|
if (isAbortError(e)) throw e;
|
|
3007
3514
|
store.deleteFile(file);
|
|
3008
3515
|
continue;
|
|
3009
3516
|
}
|
|
3010
|
-
if (!
|
|
3517
|
+
if (!stat11.isFile()) continue;
|
|
3011
3518
|
const lang = detectLang(file);
|
|
3012
3519
|
if (!lang) continue;
|
|
3013
3520
|
const meta = existingMeta.get(file);
|
|
3014
|
-
if (!force && meta && meta.mtimeMs === Math.floor(
|
|
3521
|
+
if (!force && meta && meta.mtimeMs === Math.floor(stat11.mtimeMs)) {
|
|
3015
3522
|
langStats[lang] = (langStats[lang] ?? 0) + meta.symbolCount;
|
|
3016
3523
|
symbolsIndexed += meta.symbolCount;
|
|
3017
3524
|
filesIndexed++;
|
|
@@ -3021,7 +3528,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3021
3528
|
store.deleteSymbolsForFile(file);
|
|
3022
3529
|
let content;
|
|
3023
3530
|
try {
|
|
3024
|
-
content = await
|
|
3531
|
+
content = await fs14.readFile(file, { encoding: "utf8", signal });
|
|
3025
3532
|
} catch (e) {
|
|
3026
3533
|
if (isAbortError(e)) throw e;
|
|
3027
3534
|
errors.push(`read error: ${file}: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -3038,7 +3545,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3038
3545
|
store.upsertFile({
|
|
3039
3546
|
file,
|
|
3040
3547
|
lang,
|
|
3041
|
-
mtimeMs: Math.floor(
|
|
3548
|
+
mtimeMs: Math.floor(stat11.mtimeMs),
|
|
3042
3549
|
symbolCount: 0,
|
|
3043
3550
|
lastIndexed: Date.now()
|
|
3044
3551
|
});
|
|
@@ -3064,7 +3571,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3064
3571
|
store.upsertFile({
|
|
3065
3572
|
file,
|
|
3066
3573
|
lang,
|
|
3067
|
-
mtimeMs: Math.floor(
|
|
3574
|
+
mtimeMs: Math.floor(stat11.mtimeMs),
|
|
3068
3575
|
symbolCount: count,
|
|
3069
3576
|
lastIndexed: Date.now()
|
|
3070
3577
|
});
|
|
@@ -3072,7 +3579,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3072
3579
|
}
|
|
3073
3580
|
for (const [file_] of existingMeta) {
|
|
3074
3581
|
try {
|
|
3075
|
-
await
|
|
3582
|
+
await fs14.stat(file_);
|
|
3076
3583
|
} catch {
|
|
3077
3584
|
store.deleteFile(file_);
|
|
3078
3585
|
}
|
|
@@ -3088,6 +3595,306 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3088
3595
|
};
|
|
3089
3596
|
}
|
|
3090
3597
|
|
|
3598
|
+
// src/codebase-index/index-service.ts
|
|
3599
|
+
function stubCtx(projectRoot) {
|
|
3600
|
+
return {
|
|
3601
|
+
projectRoot,
|
|
3602
|
+
cwd: projectRoot,
|
|
3603
|
+
messages: [],
|
|
3604
|
+
todos: [],
|
|
3605
|
+
readFiles: /* @__PURE__ */ new Set(),
|
|
3606
|
+
fileMtimes: /* @__PURE__ */ new Map()
|
|
3607
|
+
};
|
|
3608
|
+
}
|
|
3609
|
+
async function indexService(args, hooks = {}) {
|
|
3610
|
+
return runIndexer(stubCtx(args.projectRoot), {
|
|
3611
|
+
projectRoot: args.projectRoot,
|
|
3612
|
+
indexDir: args.indexDir,
|
|
3613
|
+
files: args.files,
|
|
3614
|
+
force: args.force,
|
|
3615
|
+
langs: args.langs,
|
|
3616
|
+
ignore: args.ignore,
|
|
3617
|
+
signal: hooks.signal,
|
|
3618
|
+
onProgress: hooks.onProgress
|
|
3619
|
+
});
|
|
3620
|
+
}
|
|
3621
|
+
function searchService(args) {
|
|
3622
|
+
const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
|
|
3623
|
+
try {
|
|
3624
|
+
return store.searchRanked(
|
|
3625
|
+
args.query,
|
|
3626
|
+
{
|
|
3627
|
+
kind: args.kind,
|
|
3628
|
+
lang: args.lang,
|
|
3629
|
+
file: args.file,
|
|
3630
|
+
lspKind: args.lspKind
|
|
3631
|
+
},
|
|
3632
|
+
args.limit
|
|
3633
|
+
);
|
|
3634
|
+
} finally {
|
|
3635
|
+
store.close();
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
function statsService(args) {
|
|
3639
|
+
const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
|
|
3640
|
+
try {
|
|
3641
|
+
return store.getStats();
|
|
3642
|
+
} finally {
|
|
3643
|
+
store.close();
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
// src/codebase-index/background-indexer.ts
|
|
3648
|
+
var DEFAULT_FULL_INDEX_TIMEOUT_MS = 12e4;
|
|
3649
|
+
var DEFAULT_QUERY_TIMEOUT_MS = 8e3;
|
|
3650
|
+
var _ready = false;
|
|
3651
|
+
var _indexing = false;
|
|
3652
|
+
var _currentFile = 0;
|
|
3653
|
+
var _totalFiles = 0;
|
|
3654
|
+
var _lastError = null;
|
|
3655
|
+
function isIndexing() {
|
|
3656
|
+
return _indexing;
|
|
3657
|
+
}
|
|
3658
|
+
function getIndexState() {
|
|
3659
|
+
return {
|
|
3660
|
+
ready: _ready,
|
|
3661
|
+
indexing: _indexing,
|
|
3662
|
+
currentFile: _currentFile,
|
|
3663
|
+
totalFiles: _totalFiles,
|
|
3664
|
+
lastError: _lastError,
|
|
3665
|
+
circuit: indexCircuitBreaker.snapshot()
|
|
3666
|
+
};
|
|
3667
|
+
}
|
|
3668
|
+
var _listeners = [];
|
|
3669
|
+
function emitState() {
|
|
3670
|
+
const state = getIndexState();
|
|
3671
|
+
for (const l of _listeners) l(state);
|
|
3672
|
+
}
|
|
3673
|
+
function setIndexProgress(current, total) {
|
|
3674
|
+
_currentFile = current;
|
|
3675
|
+
_totalFiles = total;
|
|
3676
|
+
emitState();
|
|
3677
|
+
}
|
|
3678
|
+
var worker = null;
|
|
3679
|
+
var workerUnavailable = false;
|
|
3680
|
+
var nextRpcId = 1;
|
|
3681
|
+
var pending = /* @__PURE__ */ new Map();
|
|
3682
|
+
function resolveWorkerUrl() {
|
|
3683
|
+
if (process.env["WRONGSTACK_INDEX_INLINE"]) return null;
|
|
3684
|
+
for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
|
|
3685
|
+
try {
|
|
3686
|
+
const url = new URL(rel, import.meta.url);
|
|
3687
|
+
if (url.protocol === "file:" && fs.existsSync(fileURLToPath(url))) return url;
|
|
3688
|
+
} catch {
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
return null;
|
|
3692
|
+
}
|
|
3693
|
+
function failAllPending(err) {
|
|
3694
|
+
const entries = [...pending.values()];
|
|
3695
|
+
pending.clear();
|
|
3696
|
+
for (const p of entries) p.reject(err);
|
|
3697
|
+
}
|
|
3698
|
+
function ensureWorker() {
|
|
3699
|
+
if (worker) return worker;
|
|
3700
|
+
if (workerUnavailable) return null;
|
|
3701
|
+
const url = resolveWorkerUrl();
|
|
3702
|
+
if (!url) {
|
|
3703
|
+
workerUnavailable = true;
|
|
3704
|
+
return null;
|
|
3705
|
+
}
|
|
3706
|
+
try {
|
|
3707
|
+
const w = new Worker(url, { name: "wstack-codebase-index" });
|
|
3708
|
+
w.unref();
|
|
3709
|
+
w.on("message", (msg) => {
|
|
3710
|
+
if (msg.type === "progress") {
|
|
3711
|
+
pending.get(msg.id)?.onProgress?.(msg.current, msg.total);
|
|
3712
|
+
return;
|
|
3713
|
+
}
|
|
3714
|
+
const entry = pending.get(msg.id);
|
|
3715
|
+
if (!entry) return;
|
|
3716
|
+
pending.delete(msg.id);
|
|
3717
|
+
if (msg.ok) entry.resolve(msg.result);
|
|
3718
|
+
else entry.reject(new Error(msg.error));
|
|
3719
|
+
});
|
|
3720
|
+
w.on("error", (err) => {
|
|
3721
|
+
worker = null;
|
|
3722
|
+
failAllPending(err);
|
|
3723
|
+
});
|
|
3724
|
+
w.on("exit", () => {
|
|
3725
|
+
if (worker === w) worker = null;
|
|
3726
|
+
failAllPending(new Error("codebase-index worker exited"));
|
|
3727
|
+
});
|
|
3728
|
+
worker = w;
|
|
3729
|
+
return w;
|
|
3730
|
+
} catch {
|
|
3731
|
+
workerUnavailable = true;
|
|
3732
|
+
return null;
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
function terminateWorker(reason) {
|
|
3736
|
+
const w = worker;
|
|
3737
|
+
worker = null;
|
|
3738
|
+
failAllPending(reason);
|
|
3739
|
+
if (w) void w.terminate().catch(() => {
|
|
3740
|
+
});
|
|
3741
|
+
}
|
|
3742
|
+
function callIndexOp(op, args, opts) {
|
|
3743
|
+
const w = ensureWorker();
|
|
3744
|
+
if (!w) return callInline(op, args, opts);
|
|
3745
|
+
return new Promise((resolve7, reject) => {
|
|
3746
|
+
const id = nextRpcId++;
|
|
3747
|
+
const timer = setTimeout(() => {
|
|
3748
|
+
pending.delete(id);
|
|
3749
|
+
const err = new IndexTimeoutError(
|
|
3750
|
+
`Index ${op} exceeded its ${opts.timeoutMs}ms watchdog timeout`
|
|
3751
|
+
);
|
|
3752
|
+
terminateWorker(err);
|
|
3753
|
+
reject(err);
|
|
3754
|
+
}, opts.timeoutMs);
|
|
3755
|
+
timer.unref?.();
|
|
3756
|
+
const onAbort = () => {
|
|
3757
|
+
w.postMessage({ type: "cancel", id });
|
|
3758
|
+
};
|
|
3759
|
+
if (opts.signal?.aborted) onAbort();
|
|
3760
|
+
else opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
3761
|
+
const cleanup = () => {
|
|
3762
|
+
clearTimeout(timer);
|
|
3763
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
3764
|
+
};
|
|
3765
|
+
pending.set(id, {
|
|
3766
|
+
resolve: (v) => {
|
|
3767
|
+
cleanup();
|
|
3768
|
+
resolve7(v);
|
|
3769
|
+
},
|
|
3770
|
+
reject: (e) => {
|
|
3771
|
+
cleanup();
|
|
3772
|
+
reject(e);
|
|
3773
|
+
},
|
|
3774
|
+
onProgress: opts.onProgress
|
|
3775
|
+
});
|
|
3776
|
+
w.postMessage({ type: "request", id, op, args });
|
|
3777
|
+
});
|
|
3778
|
+
}
|
|
3779
|
+
async function callInline(op, args, opts) {
|
|
3780
|
+
const ac = new AbortController();
|
|
3781
|
+
const onOuterAbort = () => ac.abort(opts.signal?.reason ?? new Error("Indexing cancelled"));
|
|
3782
|
+
if (opts.signal?.aborted) onOuterAbort();
|
|
3783
|
+
else opts.signal?.addEventListener("abort", onOuterAbort, { once: true });
|
|
3784
|
+
let timer;
|
|
3785
|
+
const watchdog = new Promise((_, reject) => {
|
|
3786
|
+
timer = setTimeout(() => {
|
|
3787
|
+
const err = new IndexTimeoutError(
|
|
3788
|
+
`Index ${op} exceeded its ${opts.timeoutMs}ms watchdog timeout`
|
|
3789
|
+
);
|
|
3790
|
+
ac.abort(err);
|
|
3791
|
+
reject(err);
|
|
3792
|
+
}, opts.timeoutMs);
|
|
3793
|
+
timer.unref?.();
|
|
3794
|
+
});
|
|
3795
|
+
const job = async () => {
|
|
3796
|
+
switch (op) {
|
|
3797
|
+
case "index":
|
|
3798
|
+
return await indexService(args, {
|
|
3799
|
+
signal: ac.signal,
|
|
3800
|
+
onProgress: opts.onProgress
|
|
3801
|
+
});
|
|
3802
|
+
case "search":
|
|
3803
|
+
return searchService(args);
|
|
3804
|
+
case "stats":
|
|
3805
|
+
return statsService(args);
|
|
3806
|
+
default:
|
|
3807
|
+
throw new Error(`unknown index op: ${String(op)}`);
|
|
3808
|
+
}
|
|
3809
|
+
};
|
|
3810
|
+
try {
|
|
3811
|
+
return await Promise.race([job(), watchdog]);
|
|
3812
|
+
} finally {
|
|
3813
|
+
if (timer) clearTimeout(timer);
|
|
3814
|
+
opts.signal?.removeEventListener("abort", onOuterAbort);
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
var chain = Promise.resolve();
|
|
3818
|
+
function withMutex(job) {
|
|
3819
|
+
const run = chain.then(job, job);
|
|
3820
|
+
chain = run.then(
|
|
3821
|
+
() => void 0,
|
|
3822
|
+
() => void 0
|
|
3823
|
+
);
|
|
3824
|
+
return run;
|
|
3825
|
+
}
|
|
3826
|
+
function circuitOpenError() {
|
|
3827
|
+
const c = indexCircuitBreaker.snapshot();
|
|
3828
|
+
return new CircuitOpenError(
|
|
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."
|
|
3830
|
+
);
|
|
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
|
+
}
|
|
3839
|
+
async function runStartupIndex(opts) {
|
|
3840
|
+
if (!indexCircuitBreaker.allowRequest()) throw circuitOpenError();
|
|
3841
|
+
_indexing = true;
|
|
3842
|
+
emitState();
|
|
3843
|
+
try {
|
|
3844
|
+
const result = await withMutex(() => {
|
|
3845
|
+
_currentFile = 0;
|
|
3846
|
+
_totalFiles = 0;
|
|
3847
|
+
_lastError = null;
|
|
3848
|
+
return callIndexOp(
|
|
3849
|
+
"index",
|
|
3850
|
+
{
|
|
3851
|
+
projectRoot: opts.projectRoot,
|
|
3852
|
+
indexDir: opts.indexDir,
|
|
3853
|
+
force: opts.force,
|
|
3854
|
+
langs: opts.langs
|
|
3855
|
+
},
|
|
3856
|
+
{
|
|
3857
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_FULL_INDEX_TIMEOUT_MS,
|
|
3858
|
+
signal: opts.signal,
|
|
3859
|
+
onProgress: setIndexProgress
|
|
3860
|
+
}
|
|
3861
|
+
);
|
|
3862
|
+
});
|
|
3863
|
+
_ready = true;
|
|
3864
|
+
indexCircuitBreaker.recordSuccess();
|
|
3865
|
+
return result;
|
|
3866
|
+
} catch (err) {
|
|
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
|
+
}
|
|
3877
|
+
_ready = true;
|
|
3878
|
+
if (!opts.signal?.aborted) indexCircuitBreaker.recordFailure(err);
|
|
3879
|
+
throw err;
|
|
3880
|
+
} finally {
|
|
3881
|
+
_indexing = false;
|
|
3882
|
+
emitState();
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
async function searchCodebaseIndex(args, opts = {}) {
|
|
3886
|
+
return callIndexOp("search", args, {
|
|
3887
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
|
|
3888
|
+
signal: opts.signal
|
|
3889
|
+
});
|
|
3890
|
+
}
|
|
3891
|
+
async function codebaseIndexStats(args, opts = {}) {
|
|
3892
|
+
return callIndexOp("stats", args, {
|
|
3893
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
|
|
3894
|
+
signal: opts.signal
|
|
3895
|
+
});
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3091
3898
|
// src/codebase-index/codebase-index-tool.ts
|
|
3092
3899
|
var codebaseIndexTool = {
|
|
3093
3900
|
name: "codebase-index",
|
|
@@ -3113,103 +3920,34 @@ var codebaseIndexTool = {
|
|
|
3113
3920
|
}
|
|
3114
3921
|
},
|
|
3115
3922
|
async execute(input, ctx, execOpts) {
|
|
3116
|
-
|
|
3923
|
+
if (isIndexing()) {
|
|
3924
|
+
return {
|
|
3925
|
+
filesIndexed: 0,
|
|
3926
|
+
symbolsIndexed: 0,
|
|
3927
|
+
langStats: {},
|
|
3928
|
+
durationMs: 0,
|
|
3929
|
+
errors: [],
|
|
3930
|
+
note: "A full index is already in progress. Retry codebase-index after it completes (check codebase-stats)."
|
|
3931
|
+
};
|
|
3932
|
+
}
|
|
3933
|
+
const circuit = indexCircuitBreaker.snapshot();
|
|
3934
|
+
if (circuit.state === "open" && circuit.cooldownRemainingMs > 0) {
|
|
3935
|
+
return {
|
|
3936
|
+
filesIndexed: 0,
|
|
3937
|
+
symbolsIndexed: 0,
|
|
3938
|
+
langStats: {},
|
|
3939
|
+
durationMs: 0,
|
|
3940
|
+
errors: [],
|
|
3941
|
+
note: `Codebase indexing is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}). Auto-retry possible in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s; the user can run /codebase-reindex to retry immediately.`
|
|
3942
|
+
};
|
|
3943
|
+
}
|
|
3944
|
+
return await runStartupIndex({
|
|
3117
3945
|
projectRoot: ctx.projectRoot,
|
|
3118
3946
|
force: input.force ?? false,
|
|
3119
3947
|
langs: input.langs,
|
|
3120
3948
|
indexDir: codebaseIndexDirOverride(ctx),
|
|
3121
3949
|
signal: execOpts?.signal
|
|
3122
3950
|
});
|
|
3123
|
-
setIndexReady();
|
|
3124
|
-
return result;
|
|
3125
|
-
}
|
|
3126
|
-
};
|
|
3127
|
-
|
|
3128
|
-
// src/codebase-index/bm25.ts
|
|
3129
|
-
var K1 = 1.5;
|
|
3130
|
-
var B = 0.75;
|
|
3131
|
-
function tokenise(text) {
|
|
3132
|
-
const sanitised = text.replace(/[^\p{L}\p{N}$'_]/gu, " ").replace(/_/g, " ");
|
|
3133
|
-
return sanitised.toLowerCase().split(" ").filter(Boolean);
|
|
3134
|
-
}
|
|
3135
|
-
function splitName(name) {
|
|
3136
|
-
return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
3137
|
-
}
|
|
3138
|
-
function buildIndexableText(name, signature, docComment) {
|
|
3139
|
-
return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
|
|
3140
|
-
}
|
|
3141
|
-
function buildBm25Index(docs) {
|
|
3142
|
-
const documents = docs.map((d) => {
|
|
3143
|
-
const tokens = tokenise(d.text);
|
|
3144
|
-
return { id: d.id, tokens, raw: d.text, len: tokens.length };
|
|
3145
|
-
});
|
|
3146
|
-
const df = {};
|
|
3147
|
-
for (const doc of documents) {
|
|
3148
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3149
|
-
for (const t of doc.tokens) {
|
|
3150
|
-
if (!seen.has(t)) {
|
|
3151
|
-
df[t] = (df[t] ?? 0) + 1;
|
|
3152
|
-
seen.add(t);
|
|
3153
|
-
}
|
|
3154
|
-
}
|
|
3155
|
-
}
|
|
3156
|
-
const N = documents.length;
|
|
3157
|
-
const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
|
|
3158
|
-
const avgLen = N === 0 ? 0 : totalLen / N;
|
|
3159
|
-
return new Bm25Index(documents, df, N, avgLen);
|
|
3160
|
-
}
|
|
3161
|
-
var Bm25Index = class {
|
|
3162
|
-
constructor(documents, df, N, avgLen) {
|
|
3163
|
-
this.documents = documents;
|
|
3164
|
-
this.df = df;
|
|
3165
|
-
this.N = N;
|
|
3166
|
-
this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
|
|
3167
|
-
}
|
|
3168
|
-
documents;
|
|
3169
|
-
df;
|
|
3170
|
-
N;
|
|
3171
|
-
safeAvgLen;
|
|
3172
|
-
score(query2, filter) {
|
|
3173
|
-
const qTokens = tokenise(query2);
|
|
3174
|
-
if (qTokens.length === 0) return [];
|
|
3175
|
-
const results = [];
|
|
3176
|
-
for (const doc of this.documents) {
|
|
3177
|
-
if (filter && !filter(doc.id)) continue;
|
|
3178
|
-
let docScore = 0;
|
|
3179
|
-
for (const qTerm of qTokens) {
|
|
3180
|
-
let tf = 0;
|
|
3181
|
-
for (const t of doc.tokens) {
|
|
3182
|
-
if (t === qTerm) tf++;
|
|
3183
|
-
}
|
|
3184
|
-
if (tf === 0) continue;
|
|
3185
|
-
const dfVal = this.df[qTerm] ?? 0;
|
|
3186
|
-
if (dfVal === 0) continue;
|
|
3187
|
-
const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
|
|
3188
|
-
const lenRatio = B * (doc.len / this.safeAvgLen);
|
|
3189
|
-
const tfComponent = tf * (K1 + 1) / (tf + K1 * (1 - B + lenRatio));
|
|
3190
|
-
docScore += idf * tfComponent;
|
|
3191
|
-
}
|
|
3192
|
-
if (docScore > 0) results.push({ id: doc.id, score: docScore });
|
|
3193
|
-
}
|
|
3194
|
-
return results;
|
|
3195
|
-
}
|
|
3196
|
-
getDoc(id) {
|
|
3197
|
-
return this.documents.find((d) => d.id === id);
|
|
3198
|
-
}
|
|
3199
|
-
extractSnippet(docId, queryTokens, radius = 40) {
|
|
3200
|
-
const doc = this.getDoc(docId);
|
|
3201
|
-
if (!doc) return "";
|
|
3202
|
-
for (const tok of queryTokens) {
|
|
3203
|
-
const idx = doc.raw.toLowerCase().indexOf(tok);
|
|
3204
|
-
if (idx !== -1) {
|
|
3205
|
-
const start = Math.max(0, idx - radius);
|
|
3206
|
-
const end = Math.min(doc.raw.length, idx + tok.length + radius);
|
|
3207
|
-
const excerpt = doc.raw.slice(start, end);
|
|
3208
|
-
const ellipsis = "\u2026";
|
|
3209
|
-
return (start > 0 ? ellipsis : "") + excerpt + (end < doc.raw.length ? ellipsis : "");
|
|
3210
|
-
}
|
|
3211
|
-
}
|
|
3212
|
-
return doc.raw.slice(0, radius * 2) + (doc.raw.length > radius * 2 ? "\u2026" : "");
|
|
3213
3951
|
}
|
|
3214
3952
|
};
|
|
3215
3953
|
|
|
@@ -3255,7 +3993,7 @@ var codebaseSearchTool = {
|
|
|
3255
3993
|
},
|
|
3256
3994
|
required: ["query"]
|
|
3257
3995
|
},
|
|
3258
|
-
async execute(input, ctx) {
|
|
3996
|
+
async execute(input, ctx, execOpts) {
|
|
3259
3997
|
const state = getIndexState();
|
|
3260
3998
|
if (!state.ready) {
|
|
3261
3999
|
return {
|
|
@@ -3274,51 +4012,30 @@ var codebaseSearchTool = {
|
|
|
3274
4012
|
};
|
|
3275
4013
|
}
|
|
3276
4014
|
if (state.lastError) {
|
|
4015
|
+
const circuit = state.circuit;
|
|
4016
|
+
const retryHint = circuit.state === "open" ? `Indexing is paused (circuit open, retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s); the user can run /codebase-reindex to retry now.` : "Try /codebase-reindex.";
|
|
3277
4017
|
return {
|
|
3278
4018
|
results: [],
|
|
3279
4019
|
total: 0,
|
|
3280
4020
|
query: input.query,
|
|
3281
|
-
indexStatus: `Index build failed: ${state.lastError}.
|
|
4021
|
+
indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
|
|
3282
4022
|
};
|
|
3283
4023
|
}
|
|
3284
|
-
const
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
4024
|
+
const limit = Math.min(input.limit ?? 20, 100);
|
|
4025
|
+
const { results, total } = await searchCodebaseIndex(
|
|
4026
|
+
{
|
|
4027
|
+
projectRoot: ctx.projectRoot,
|
|
4028
|
+
indexDir: codebaseIndexDirOverride(ctx),
|
|
4029
|
+
query: input.query,
|
|
3288
4030
|
kind: input.kind,
|
|
3289
4031
|
lang: input.lang,
|
|
3290
4032
|
file: input.file,
|
|
3291
|
-
lspKind: input.lspKind
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
id: c.id,
|
|
3298
|
-
text: buildIndexableText(c.name, c.signature, c.docComment)
|
|
3299
|
-
}));
|
|
3300
|
-
const bm25 = buildBm25Index(indexable);
|
|
3301
|
-
const scored = bm25.score(input.query, (id) => candidates.some((c) => c.id === id));
|
|
3302
|
-
scored.sort((a, b) => b.score - a.score);
|
|
3303
|
-
const top = scored.slice(0, limit);
|
|
3304
|
-
const qTokens = tokenise(input.query);
|
|
3305
|
-
const results = top.map(({ id, score }) => {
|
|
3306
|
-
const c = expectDefined(candidates.find((c2) => c2.id === id));
|
|
3307
|
-
const snippet = bm25.extractSnippet(id, qTokens);
|
|
3308
|
-
return {
|
|
3309
|
-
...c,
|
|
3310
|
-
score,
|
|
3311
|
-
snippet
|
|
3312
|
-
};
|
|
3313
|
-
});
|
|
3314
|
-
return {
|
|
3315
|
-
results,
|
|
3316
|
-
total: candidates.length,
|
|
3317
|
-
query: input.query
|
|
3318
|
-
};
|
|
3319
|
-
} finally {
|
|
3320
|
-
store.close();
|
|
3321
|
-
}
|
|
4033
|
+
lspKind: input.lspKind,
|
|
4034
|
+
limit
|
|
4035
|
+
},
|
|
4036
|
+
{ signal: execOpts?.signal }
|
|
4037
|
+
);
|
|
4038
|
+
return { results, total, query: input.query };
|
|
3322
4039
|
}
|
|
3323
4040
|
};
|
|
3324
4041
|
|
|
@@ -3337,7 +4054,7 @@ var codebaseStatsTool = {
|
|
|
3337
4054
|
properties: {},
|
|
3338
4055
|
additionalProperties: false
|
|
3339
4056
|
},
|
|
3340
|
-
async execute(_input, ctx) {
|
|
4057
|
+
async execute(_input, ctx, execOpts) {
|
|
3341
4058
|
const idxState = getIndexState();
|
|
3342
4059
|
if (!idxState.ready) {
|
|
3343
4060
|
return {
|
|
@@ -3352,34 +4069,30 @@ var codebaseStatsTool = {
|
|
|
3352
4069
|
indexStatus: idxState.indexing ? `Indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files).` : "Index not yet built."
|
|
3353
4070
|
};
|
|
3354
4071
|
}
|
|
4072
|
+
const stats = await codebaseIndexStats(
|
|
4073
|
+
{ projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
|
|
4074
|
+
{ signal: execOpts?.signal }
|
|
4075
|
+
);
|
|
3355
4076
|
if (idxState.indexing) {
|
|
3356
|
-
const store2 = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
|
|
3357
|
-
try {
|
|
3358
|
-
const stats = store2.getStats();
|
|
3359
|
-
return {
|
|
3360
|
-
...stats,
|
|
3361
|
-
indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
|
|
3362
|
-
};
|
|
3363
|
-
} finally {
|
|
3364
|
-
store2.close();
|
|
3365
|
-
}
|
|
3366
|
-
}
|
|
3367
|
-
const store = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
|
|
3368
|
-
try {
|
|
3369
|
-
const stats = store.getStats();
|
|
3370
4077
|
return {
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
byLang: stats.byLang,
|
|
3374
|
-
byKind: stats.byKind,
|
|
3375
|
-
lastIndexed: stats.lastIndexed,
|
|
3376
|
-
sizeBytes: stats.sizeBytes,
|
|
3377
|
-
indexPath: stats.indexPath,
|
|
3378
|
-
version: stats.version
|
|
4078
|
+
...stats,
|
|
4079
|
+
indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
|
|
3379
4080
|
};
|
|
3380
|
-
} finally {
|
|
3381
|
-
store.close();
|
|
3382
4081
|
}
|
|
4082
|
+
const circuit = idxState.circuit;
|
|
4083
|
+
return {
|
|
4084
|
+
totalSymbols: stats.totalSymbols,
|
|
4085
|
+
totalFiles: stats.totalFiles,
|
|
4086
|
+
byLang: stats.byLang,
|
|
4087
|
+
byKind: stats.byKind,
|
|
4088
|
+
lastIndexed: stats.lastIndexed,
|
|
4089
|
+
sizeBytes: stats.sizeBytes,
|
|
4090
|
+
indexPath: stats.indexPath,
|
|
4091
|
+
version: stats.version,
|
|
4092
|
+
...circuit.state === "open" ? {
|
|
4093
|
+
indexStatus: `Indexing is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}); auto-retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s, or run /codebase-reindex. Stats reflect the last successful build.`
|
|
4094
|
+
} : {}
|
|
4095
|
+
};
|
|
3383
4096
|
}
|
|
3384
4097
|
};
|
|
3385
4098
|
var diffTool = {
|
|
@@ -3463,11 +4176,11 @@ function findGitDir(cwd) {
|
|
|
3463
4176
|
let dir = cwd;
|
|
3464
4177
|
for (let i = 0; i < 20; i++) {
|
|
3465
4178
|
try {
|
|
3466
|
-
const
|
|
3467
|
-
if (
|
|
4179
|
+
const stat11 = statSync(path3.join(dir, ".git"));
|
|
4180
|
+
if (stat11.isDirectory()) return dir;
|
|
3468
4181
|
} catch {
|
|
3469
4182
|
}
|
|
3470
|
-
const parent =
|
|
4183
|
+
const parent = path3.dirname(dir);
|
|
3471
4184
|
if (parent === dir) break;
|
|
3472
4185
|
dir = parent;
|
|
3473
4186
|
}
|
|
@@ -3481,7 +4194,8 @@ function runGit(args, cwd, signal) {
|
|
|
3481
4194
|
cwd,
|
|
3482
4195
|
signal,
|
|
3483
4196
|
env: buildChildEnv(),
|
|
3484
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
4197
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4198
|
+
windowsHide: true
|
|
3485
4199
|
});
|
|
3486
4200
|
child.stdout?.on("data", (c) => {
|
|
3487
4201
|
stdout += c.toString();
|
|
@@ -3507,9 +4221,9 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
3507
4221
|
const results = [];
|
|
3508
4222
|
for (const file of files) {
|
|
3509
4223
|
const absPath = safeResolve(file, ctx);
|
|
3510
|
-
const
|
|
3511
|
-
if (!
|
|
3512
|
-
const content = await
|
|
4224
|
+
const stat11 = await fs14.stat(absPath).catch(() => null);
|
|
4225
|
+
if (!stat11?.isFile()) continue;
|
|
4226
|
+
const content = await fs14.readFile(absPath, "utf8");
|
|
3513
4227
|
const lines = content.split(/\r?\n/);
|
|
3514
4228
|
results.push(formatWithLineNumbers(file, lines));
|
|
3515
4229
|
}
|
|
@@ -3571,7 +4285,7 @@ var documentTool = {
|
|
|
3571
4285
|
const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
|
|
3572
4286
|
for (const absPath of fileList) {
|
|
3573
4287
|
try {
|
|
3574
|
-
const content = await
|
|
4288
|
+
const content = await fs14.readFile(absPath, "utf8");
|
|
3575
4289
|
filesProcessed++;
|
|
3576
4290
|
const processed = processFile(
|
|
3577
4291
|
content,
|
|
@@ -3607,8 +4321,8 @@ async function resolveFiles(filesInput, cwd) {
|
|
|
3607
4321
|
for (const f of files) {
|
|
3608
4322
|
const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
|
|
3609
4323
|
try {
|
|
3610
|
-
const
|
|
3611
|
-
if (
|
|
4324
|
+
const stat11 = await fs14.stat(absPath);
|
|
4325
|
+
if (stat11.isFile()) resolved.push(absPath);
|
|
3612
4326
|
} catch {
|
|
3613
4327
|
}
|
|
3614
4328
|
}
|
|
@@ -3699,18 +4413,18 @@ var editTool = {
|
|
|
3699
4413
|
if (input.new_string === void 0) throw new Error("edit: new_string is required");
|
|
3700
4414
|
if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
|
|
3701
4415
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
3702
|
-
const
|
|
4416
|
+
const stat11 = await fs14.stat(absPath).catch((err) => {
|
|
3703
4417
|
if (err.code === "ENOENT") {
|
|
3704
4418
|
throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
|
|
3705
4419
|
}
|
|
3706
4420
|
throw err;
|
|
3707
4421
|
});
|
|
3708
|
-
if (!
|
|
4422
|
+
if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
|
|
3709
4423
|
if (!ctx.hasRead(absPath)) {
|
|
3710
4424
|
throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
|
|
3711
4425
|
}
|
|
3712
|
-
const original = await
|
|
3713
|
-
const updated = await
|
|
4426
|
+
const original = await fs14.readFile(absPath, "utf8");
|
|
4427
|
+
const updated = await fs14.stat(absPath);
|
|
3714
4428
|
const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
|
|
3715
4429
|
const lastReadMtime = ctx.lastReadMtime(absPath);
|
|
3716
4430
|
if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
|
|
@@ -3750,7 +4464,7 @@ var editTool = {
|
|
|
3750
4464
|
const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
|
|
3751
4465
|
const newFile = toStyle(newFileLf, style);
|
|
3752
4466
|
await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
|
|
3753
|
-
const written = await
|
|
4467
|
+
const written = await fs14.stat(absPath);
|
|
3754
4468
|
ctx.recordRead(absPath, written.mtimeMs);
|
|
3755
4469
|
ctx.session.recordFileChange({
|
|
3756
4470
|
path: absPath,
|
|
@@ -3975,9 +4689,9 @@ var execTool = {
|
|
|
3975
4689
|
allowed: false
|
|
3976
4690
|
};
|
|
3977
4691
|
}
|
|
3978
|
-
const requestedCwd = input.cwd ?
|
|
3979
|
-
const rel =
|
|
3980
|
-
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)) {
|
|
3981
4695
|
return {
|
|
3982
4696
|
command: cmd,
|
|
3983
4697
|
args,
|
|
@@ -3999,6 +4713,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
3999
4713
|
let stderr = "";
|
|
4000
4714
|
let killed = false;
|
|
4001
4715
|
const startedAt = Date.now();
|
|
4716
|
+
const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
|
|
4002
4717
|
const resolved = resolveWin32Command(cmd);
|
|
4003
4718
|
const isWin = process.platform === "win32";
|
|
4004
4719
|
const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
@@ -4006,6 +4721,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
4006
4721
|
cwd,
|
|
4007
4722
|
env: buildChildEnv(sessionId),
|
|
4008
4723
|
stdio: ["ignore", "pipe", "pipe"],
|
|
4724
|
+
windowsHide: true,
|
|
4009
4725
|
...isWin ? {} : { signal },
|
|
4010
4726
|
...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
|
|
4011
4727
|
});
|
|
@@ -4030,10 +4746,14 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
4030
4746
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
4031
4747
|
}
|
|
4032
4748
|
child.stdout?.on("data", (chunk) => {
|
|
4033
|
-
|
|
4749
|
+
const text = chunk.toString();
|
|
4750
|
+
if (stdout.length < MAX_OUTPUT2) stdout += text;
|
|
4751
|
+
spool.write(text);
|
|
4034
4752
|
});
|
|
4035
4753
|
child.stderr?.on("data", (chunk) => {
|
|
4036
|
-
|
|
4754
|
+
const text = chunk.toString();
|
|
4755
|
+
if (stderr.length < MAX_OUTPUT2) stderr += text;
|
|
4756
|
+
spool.write(text);
|
|
4037
4757
|
});
|
|
4038
4758
|
child.on("close", (code) => {
|
|
4039
4759
|
clearTimeout(timer);
|
|
@@ -4042,10 +4762,11 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
4042
4762
|
const durationMs = Date.now() - startedAt;
|
|
4043
4763
|
const exitCode = killed ? 124 : code ?? 1;
|
|
4044
4764
|
registry.afterCall(durationMs, exitCode !== 0);
|
|
4765
|
+
const spooled = spool.finalize();
|
|
4045
4766
|
resolve7({
|
|
4046
4767
|
command: cmd,
|
|
4047
4768
|
args,
|
|
4048
|
-
stdout: normalizeCommandOutput(stdout),
|
|
4769
|
+
stdout: normalizeCommandOutput(stdout) + (spooled ? spoolNote(spooled) : ""),
|
|
4049
4770
|
stderr: normalizeCommandOutput(stderr),
|
|
4050
4771
|
exitCode,
|
|
4051
4772
|
truncated: Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES || Buffer.byteLength(stderr, "utf8") > COMMAND_OUTPUT_MAX_BYTES,
|
|
@@ -4057,6 +4778,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
4057
4778
|
if (isWin) signal.removeEventListener("abort", onAbort);
|
|
4058
4779
|
if (typeof pid === "number") registry.unregister(pid);
|
|
4059
4780
|
registry.afterCall(Date.now() - startedAt, true);
|
|
4781
|
+
spool.finalize();
|
|
4060
4782
|
resolve7({
|
|
4061
4783
|
command: cmd,
|
|
4062
4784
|
args,
|
|
@@ -4487,13 +5209,13 @@ var formatTool = {
|
|
|
4487
5209
|
}
|
|
4488
5210
|
};
|
|
4489
5211
|
async function detectFixer(cwd) {
|
|
4490
|
-
const { stat:
|
|
5212
|
+
const { stat: stat11 } = await import('node:fs/promises');
|
|
4491
5213
|
try {
|
|
4492
|
-
await
|
|
5214
|
+
await stat11(`${cwd}/biome.json`);
|
|
4493
5215
|
return "biome";
|
|
4494
5216
|
} catch {
|
|
4495
5217
|
try {
|
|
4496
|
-
await
|
|
5218
|
+
await stat11(`${cwd}/.prettierrc`);
|
|
4497
5219
|
return "prettier";
|
|
4498
5220
|
} catch {
|
|
4499
5221
|
return "biome";
|
|
@@ -4636,8 +5358,8 @@ function findGitDir2(cwd, projectRoot) {
|
|
|
4636
5358
|
let dir = cwd;
|
|
4637
5359
|
for (let i = 0; i < 20; i++) {
|
|
4638
5360
|
try {
|
|
4639
|
-
const
|
|
4640
|
-
if (
|
|
5361
|
+
const stat11 = statSync(`${dir}/.git`);
|
|
5362
|
+
if (stat11.isDirectory() || stat11.isFile()) return dir;
|
|
4641
5363
|
} catch {
|
|
4642
5364
|
}
|
|
4643
5365
|
if (dir === root) break;
|
|
@@ -4725,7 +5447,8 @@ function runGit2(args, cwd, signal) {
|
|
|
4725
5447
|
cwd,
|
|
4726
5448
|
signal,
|
|
4727
5449
|
env: buildChildEnv(),
|
|
4728
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
5450
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
5451
|
+
windowsHide: true
|
|
4729
5452
|
});
|
|
4730
5453
|
child.stdout?.on("data", (chunk) => {
|
|
4731
5454
|
if (stdout.length < MAX_OUTPUT3) {
|
|
@@ -4801,7 +5524,7 @@ var globTool = {
|
|
|
4801
5524
|
}
|
|
4802
5525
|
let entries;
|
|
4803
5526
|
try {
|
|
4804
|
-
entries = await
|
|
5527
|
+
entries = await fs14.readdir(dir, { withFileTypes: true });
|
|
4805
5528
|
} catch {
|
|
4806
5529
|
return;
|
|
4807
5530
|
}
|
|
@@ -4810,14 +5533,14 @@ var globTool = {
|
|
|
4810
5533
|
if (DEFAULT_IGNORE2.includes(name)) continue;
|
|
4811
5534
|
if (ignored.includes(name)) continue;
|
|
4812
5535
|
const rel = relPrefix ? `${relPrefix}/${name}` : name;
|
|
4813
|
-
const full =
|
|
5536
|
+
const full = path3.join(dir, name);
|
|
4814
5537
|
if (e.isDirectory()) {
|
|
4815
5538
|
await walk(full, rel);
|
|
4816
5539
|
if (truncated) return;
|
|
4817
5540
|
} else if (e.isFile()) {
|
|
4818
5541
|
if (re.test(rel) || re.test(name)) {
|
|
4819
5542
|
try {
|
|
4820
|
-
const st = await
|
|
5543
|
+
const st = await fs14.stat(full);
|
|
4821
5544
|
results.push({ rel: full, mtime: st.mtimeMs });
|
|
4822
5545
|
if (results.length >= limit) {
|
|
4823
5546
|
truncated = true;
|
|
@@ -4836,7 +5559,7 @@ var globTool = {
|
|
|
4836
5559
|
};
|
|
4837
5560
|
async function readGitignore(dir) {
|
|
4838
5561
|
try {
|
|
4839
|
-
const raw = await
|
|
5562
|
+
const raw = await fs14.readFile(path3.join(dir, ".gitignore"), "utf8");
|
|
4840
5563
|
return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
4841
5564
|
} catch {
|
|
4842
5565
|
return [];
|
|
@@ -4970,7 +5693,7 @@ var grepTool = {
|
|
|
4970
5693
|
async function detectRg(signal) {
|
|
4971
5694
|
return new Promise((resolve7) => {
|
|
4972
5695
|
try {
|
|
4973
|
-
const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", signal });
|
|
5696
|
+
const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", signal, windowsHide: true });
|
|
4974
5697
|
p.on("error", () => resolve7(false));
|
|
4975
5698
|
p.on("close", (code) => resolve7(code === 0));
|
|
4976
5699
|
} catch {
|
|
@@ -5000,7 +5723,7 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
5000
5723
|
const FLUSH_AT = 16;
|
|
5001
5724
|
const MAX_BUF_BYTES = 1e6;
|
|
5002
5725
|
let bufOverflow = false;
|
|
5003
|
-
const child = spawn("rg", args, { signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"] });
|
|
5726
|
+
const child = spawn("rg", args, { signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
5004
5727
|
const queue = [];
|
|
5005
5728
|
let waiter;
|
|
5006
5729
|
const wake = () => {
|
|
@@ -5120,7 +5843,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
5120
5843
|
if (stopped || signal.aborted) return;
|
|
5121
5844
|
let entries;
|
|
5122
5845
|
try {
|
|
5123
|
-
entries = await
|
|
5846
|
+
entries = await fs14.readdir(dir, { withFileTypes: true });
|
|
5124
5847
|
} catch {
|
|
5125
5848
|
return;
|
|
5126
5849
|
}
|
|
@@ -5128,16 +5851,16 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
5128
5851
|
if (stopped) return;
|
|
5129
5852
|
if (DEFAULT_IGNORE3.includes(e.name)) continue;
|
|
5130
5853
|
if (e.isSymbolicLink()) continue;
|
|
5131
|
-
const full =
|
|
5854
|
+
const full = path3.join(dir, e.name);
|
|
5132
5855
|
if (e.isDirectory()) {
|
|
5133
5856
|
await walk(full);
|
|
5134
5857
|
} else if (e.isFile()) {
|
|
5135
5858
|
if (globRe && !globRe.test(e.name) && !globRe.test(full)) continue;
|
|
5136
5859
|
if (globRe) globRe.lastIndex = 0;
|
|
5137
5860
|
try {
|
|
5138
|
-
const
|
|
5139
|
-
if (
|
|
5140
|
-
const head = await
|
|
5861
|
+
const stat11 = await fs14.stat(full);
|
|
5862
|
+
if (stat11.size > 1e6) continue;
|
|
5863
|
+
const head = await fs14.readFile(full);
|
|
5141
5864
|
if (isBinaryBuffer(head)) continue;
|
|
5142
5865
|
const text = head.toString("utf8");
|
|
5143
5866
|
const lines = text.split(/\r?\n/);
|
|
@@ -5346,7 +6069,7 @@ var jsonTool = {
|
|
|
5346
6069
|
let raw;
|
|
5347
6070
|
if (input.file) {
|
|
5348
6071
|
try {
|
|
5349
|
-
raw = await
|
|
6072
|
+
raw = await fs14.readFile(input.file, "utf8");
|
|
5350
6073
|
} catch {
|
|
5351
6074
|
return { data: null, formatted: "", type: "unknown", error: `Could not read file` };
|
|
5352
6075
|
}
|
|
@@ -5384,8 +6107,8 @@ var jsonTool = {
|
|
|
5384
6107
|
};
|
|
5385
6108
|
}
|
|
5386
6109
|
};
|
|
5387
|
-
function query(data,
|
|
5388
|
-
const parts =
|
|
6110
|
+
function query(data, path21) {
|
|
6111
|
+
const parts = path21.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
5389
6112
|
let current = data;
|
|
5390
6113
|
for (const part of parts) {
|
|
5391
6114
|
if (current === null || current === void 0) return void 0;
|
|
@@ -5514,11 +6237,11 @@ var lintTool = {
|
|
|
5514
6237
|
}
|
|
5515
6238
|
};
|
|
5516
6239
|
async function detectLinter(cwd) {
|
|
5517
|
-
const { stat:
|
|
6240
|
+
const { stat: stat11 } = await import('node:fs/promises');
|
|
5518
6241
|
const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
|
|
5519
6242
|
for (const f of checks) {
|
|
5520
6243
|
try {
|
|
5521
|
-
await
|
|
6244
|
+
await stat11(`${cwd}/${f}`);
|
|
5522
6245
|
if (f.includes("biome")) return "biome";
|
|
5523
6246
|
if (f.includes("eslint")) return "eslint";
|
|
5524
6247
|
if (f.includes("tslint")) return "tslint";
|
|
@@ -5625,7 +6348,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
5625
6348
|
clearTimeout(timer);
|
|
5626
6349
|
resolve7(result);
|
|
5627
6350
|
};
|
|
5628
|
-
const child = spawn("docker", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"] });
|
|
6351
|
+
const child = spawn("docker", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
5629
6352
|
const timer = setTimeout(() => {
|
|
5630
6353
|
child.kill("SIGTERM");
|
|
5631
6354
|
finish(empty());
|
|
@@ -5656,7 +6379,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
5656
6379
|
}
|
|
5657
6380
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
5658
6381
|
var MAX_TAIL_LINES = 1e5;
|
|
5659
|
-
async function fileLogs(
|
|
6382
|
+
async function fileLogs(path21, lines, filterRe, stream) {
|
|
5660
6383
|
const { createInterface } = await import('node:readline');
|
|
5661
6384
|
const { createReadStream } = await import('node:fs');
|
|
5662
6385
|
const entries = [];
|
|
@@ -5665,7 +6388,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
|
|
|
5665
6388
|
let writeIdx = 0;
|
|
5666
6389
|
let totalLines = 0;
|
|
5667
6390
|
const rl = createInterface({
|
|
5668
|
-
input: createReadStream(
|
|
6391
|
+
input: createReadStream(path21),
|
|
5669
6392
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
5670
6393
|
});
|
|
5671
6394
|
for await (const line of rl) {
|
|
@@ -5686,7 +6409,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
|
|
|
5686
6409
|
if (parsed) entries.push(parsed);
|
|
5687
6410
|
}
|
|
5688
6411
|
return {
|
|
5689
|
-
source:
|
|
6412
|
+
source: path21,
|
|
5690
6413
|
entries,
|
|
5691
6414
|
total: entries.length,
|
|
5692
6415
|
truncated: totalLines > effLines,
|
|
@@ -5771,7 +6494,7 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
5771
6494
|
const MAX = 1e5;
|
|
5772
6495
|
const resolved = resolveWin32Command(manager);
|
|
5773
6496
|
const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
5774
|
-
const child = spawn(resolved, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
|
|
6497
|
+
const child = spawn(resolved, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
|
|
5775
6498
|
child.stdout?.on("data", (c) => {
|
|
5776
6499
|
if (stdout.length < MAX) stdout += c.toString();
|
|
5777
6500
|
});
|
|
@@ -5855,9 +6578,9 @@ var patchTool = {
|
|
|
5855
6578
|
for (const t of targets) {
|
|
5856
6579
|
const stripped = stripPathComponents(t, strip);
|
|
5857
6580
|
if (!stripped) continue;
|
|
5858
|
-
const candidate =
|
|
5859
|
-
const rel =
|
|
5860
|
-
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)) {
|
|
5861
6584
|
return {
|
|
5862
6585
|
applied: 0,
|
|
5863
6586
|
rejected: 1,
|
|
@@ -5867,12 +6590,12 @@ var patchTool = {
|
|
|
5867
6590
|
};
|
|
5868
6591
|
}
|
|
5869
6592
|
}
|
|
5870
|
-
const tmpDir = await
|
|
6593
|
+
const tmpDir = await fs14.mkdtemp(path3.join(os.tmpdir(), ".wstack_patch_"));
|
|
5871
6594
|
try {
|
|
5872
|
-
await
|
|
6595
|
+
await fs14.chmod(tmpDir, 448).catch(() => {
|
|
5873
6596
|
});
|
|
5874
|
-
const patchFile =
|
|
5875
|
-
await
|
|
6597
|
+
const patchFile = path3.join(tmpDir, "in.diff");
|
|
6598
|
+
await fs14.writeFile(patchFile, input.patch, { mode: 384 });
|
|
5876
6599
|
const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
|
|
5877
6600
|
const result = await runPatch(args, dir, opts.signal);
|
|
5878
6601
|
if (result.exitCode !== 0 && !dryRun) {
|
|
@@ -5893,7 +6616,7 @@ var patchTool = {
|
|
|
5893
6616
|
message: result.stdout || "patch applied"
|
|
5894
6617
|
};
|
|
5895
6618
|
} finally {
|
|
5896
|
-
await
|
|
6619
|
+
await fs14.rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
5897
6620
|
});
|
|
5898
6621
|
}
|
|
5899
6622
|
}
|
|
@@ -5918,7 +6641,7 @@ function runPatch(args, cwd, signal) {
|
|
|
5918
6641
|
let stdout = "";
|
|
5919
6642
|
let stderr = "";
|
|
5920
6643
|
const env = { ...buildChildEnv(), LANG: "C", LC_ALL: "C" };
|
|
5921
|
-
const child = spawn("patch", args, { cwd, signal, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
6644
|
+
const child = spawn("patch", args, { cwd, signal, env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
5922
6645
|
child.stdout?.on("data", (c) => {
|
|
5923
6646
|
stdout += c.toString();
|
|
5924
6647
|
});
|
|
@@ -6193,9 +6916,9 @@ var readTool = {
|
|
|
6193
6916
|
async execute(input, ctx) {
|
|
6194
6917
|
if (!input?.path) throw new Error("read: path is required");
|
|
6195
6918
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
6196
|
-
let
|
|
6919
|
+
let stat11;
|
|
6197
6920
|
try {
|
|
6198
|
-
|
|
6921
|
+
stat11 = await fs14.stat(absPath);
|
|
6199
6922
|
} catch (err) {
|
|
6200
6923
|
const code = err.code;
|
|
6201
6924
|
if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
|
|
@@ -6203,11 +6926,11 @@ var readTool = {
|
|
|
6203
6926
|
`read: failed to stat "${input.path}": ${err instanceof Error ? err.message : String(err)}`
|
|
6204
6927
|
);
|
|
6205
6928
|
}
|
|
6206
|
-
if (!
|
|
6207
|
-
if (
|
|
6208
|
-
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})`);
|
|
6209
6932
|
}
|
|
6210
|
-
const buf = await
|
|
6933
|
+
const buf = await fs14.readFile(absPath);
|
|
6211
6934
|
if (isBinaryBuffer(buf)) {
|
|
6212
6935
|
throw new Error(`read: "${input.path}" appears to be binary`);
|
|
6213
6936
|
}
|
|
@@ -6217,14 +6940,14 @@ var readTool = {
|
|
|
6217
6940
|
const offset = Math.max(1, input.offset ?? 1);
|
|
6218
6941
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
6219
6942
|
if (limit === 0) {
|
|
6220
|
-
ctx.recordRead(absPath,
|
|
6943
|
+
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
6221
6944
|
return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
|
|
6222
6945
|
}
|
|
6223
6946
|
const slice = allLines.slice(offset - 1, offset - 1 + limit);
|
|
6224
6947
|
const truncated = offset - 1 + slice.length < total;
|
|
6225
6948
|
const width = String(offset + slice.length - 1).length;
|
|
6226
6949
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
6227
|
-
ctx.recordRead(absPath,
|
|
6950
|
+
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
6228
6951
|
return {
|
|
6229
6952
|
text: numbered,
|
|
6230
6953
|
total_lines: total,
|
|
@@ -6275,11 +6998,11 @@ var replaceTool = {
|
|
|
6275
6998
|
const dryRun = input.dry_run ?? false;
|
|
6276
6999
|
const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
|
|
6277
7000
|
const fileList = await resolveFiles2(filesInput, ctx, globRe);
|
|
6278
|
-
const realRoot = await
|
|
7001
|
+
const realRoot = await fs14.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
|
|
6279
7002
|
const results = [];
|
|
6280
7003
|
let totalReplacements = 0;
|
|
6281
7004
|
for (const absPath of fileList) {
|
|
6282
|
-
const lstat2 = await
|
|
7005
|
+
const lstat2 = await fs14.lstat(absPath).catch((err) => {
|
|
6283
7006
|
if (err.code === "ENOENT") return null;
|
|
6284
7007
|
throw err;
|
|
6285
7008
|
});
|
|
@@ -6287,17 +7010,17 @@ var replaceTool = {
|
|
|
6287
7010
|
if (lstat2.isSymbolicLink()) continue;
|
|
6288
7011
|
let realPath;
|
|
6289
7012
|
try {
|
|
6290
|
-
realPath = await
|
|
7013
|
+
realPath = await fs14.realpath(absPath);
|
|
6291
7014
|
} catch {
|
|
6292
7015
|
continue;
|
|
6293
7016
|
}
|
|
6294
|
-
const rel =
|
|
6295
|
-
if (rel.startsWith("..") ||
|
|
6296
|
-
const
|
|
6297
|
-
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;
|
|
6298
7021
|
let content;
|
|
6299
7022
|
try {
|
|
6300
|
-
const buf = await
|
|
7023
|
+
const buf = await fs14.readFile(realPath);
|
|
6301
7024
|
if (isBinaryBuffer(buf)) continue;
|
|
6302
7025
|
content = buf.toString("utf8");
|
|
6303
7026
|
} catch {
|
|
@@ -6319,7 +7042,7 @@ var replaceTool = {
|
|
|
6319
7042
|
totalReplacements += count;
|
|
6320
7043
|
if (!dryRun) {
|
|
6321
7044
|
const newContent = toStyle(newContentLf, style);
|
|
6322
|
-
await atomicWrite(realPath, newContent, { mode:
|
|
7045
|
+
await atomicWrite(realPath, newContent, { mode: stat11.mode & 511 });
|
|
6323
7046
|
}
|
|
6324
7047
|
const diff = dryRun || matches.length > 0 ? unifiedDiff(content, toStyle(newContentLf, style), {
|
|
6325
7048
|
fromFile: absPath,
|
|
@@ -6349,8 +7072,8 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
6349
7072
|
const resolved = [];
|
|
6350
7073
|
for (const p of parts) {
|
|
6351
7074
|
const absPath = safeResolve(p, ctx);
|
|
6352
|
-
const
|
|
6353
|
-
if (
|
|
7075
|
+
const stat11 = await fs14.stat(absPath).catch(() => null);
|
|
7076
|
+
if (stat11?.isFile()) {
|
|
6354
7077
|
resolved.push(absPath);
|
|
6355
7078
|
}
|
|
6356
7079
|
}
|
|
@@ -6370,7 +7093,7 @@ async function globFiles(pattern, base, extraGlob) {
|
|
|
6370
7093
|
function checkRg() {
|
|
6371
7094
|
return new Promise((resolve7) => {
|
|
6372
7095
|
try {
|
|
6373
|
-
const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore" });
|
|
7096
|
+
const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", windowsHide: true });
|
|
6374
7097
|
p.on("error", () => resolve7(false));
|
|
6375
7098
|
p.on("close", (code) => resolve7(code === 0));
|
|
6376
7099
|
} catch {
|
|
@@ -6383,7 +7106,8 @@ function spawnRgFind(pattern, base) {
|
|
|
6383
7106
|
const child = spawn("rg", args, {
|
|
6384
7107
|
signal: AbortSignal.timeout(3e4),
|
|
6385
7108
|
env: buildChildEnv(),
|
|
6386
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
7109
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
7110
|
+
windowsHide: true
|
|
6387
7111
|
});
|
|
6388
7112
|
let buf = "";
|
|
6389
7113
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -6404,16 +7128,16 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
6404
7128
|
const walk = async (dir) => {
|
|
6405
7129
|
let entries;
|
|
6406
7130
|
try {
|
|
6407
|
-
entries = await
|
|
7131
|
+
entries = await fs14.readdir(dir, { withFileTypes: true });
|
|
6408
7132
|
} catch {
|
|
6409
7133
|
return;
|
|
6410
7134
|
}
|
|
6411
7135
|
for (const e of entries) {
|
|
6412
7136
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
6413
|
-
const full =
|
|
7137
|
+
const full = path3.join(dir, e.name);
|
|
6414
7138
|
try {
|
|
6415
|
-
const
|
|
6416
|
-
if (
|
|
7139
|
+
const stat11 = await fs14.lstat(full);
|
|
7140
|
+
if (stat11.isSymbolicLink()) continue;
|
|
6417
7141
|
} catch {
|
|
6418
7142
|
continue;
|
|
6419
7143
|
}
|
|
@@ -6580,16 +7304,16 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
6580
7304
|
let filesCreated = 0;
|
|
6581
7305
|
for (const [filePath, content] of Object.entries(templateFiles)) {
|
|
6582
7306
|
const resolvedPath = substituteVars(filePath, name, vars);
|
|
6583
|
-
const joinedPath =
|
|
6584
|
-
const root =
|
|
6585
|
-
const target =
|
|
6586
|
-
const rel =
|
|
6587
|
-
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)) {
|
|
6588
7312
|
throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
|
|
6589
7313
|
}
|
|
6590
7314
|
const fullPath = target;
|
|
6591
7315
|
if (!dryRun) {
|
|
6592
|
-
await
|
|
7316
|
+
await fs14.mkdir(path3.dirname(fullPath), { recursive: true });
|
|
6593
7317
|
await atomicWrite(fullPath, substituteVars(content, name, vars));
|
|
6594
7318
|
}
|
|
6595
7319
|
files.push(resolvedPath);
|
|
@@ -6867,7 +7591,7 @@ var setWorkingDirTool = {
|
|
|
6867
7591
|
};
|
|
6868
7592
|
}
|
|
6869
7593
|
try {
|
|
6870
|
-
await
|
|
7594
|
+
await fs14.access(resolved);
|
|
6871
7595
|
} catch {
|
|
6872
7596
|
try {
|
|
6873
7597
|
ctx.setWorkingDir(previous);
|
|
@@ -7202,7 +7926,11 @@ var testTool = {
|
|
|
7202
7926
|
coverage: { type: "boolean", description: "Generate coverage report (default: false)" },
|
|
7203
7927
|
cwd: { type: "string", description: "Working directory (default: cwd)" },
|
|
7204
7928
|
grep: { type: "string", description: "Filter tests by name pattern (default: none)" },
|
|
7205
|
-
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
|
+
}
|
|
7206
7934
|
}
|
|
7207
7935
|
},
|
|
7208
7936
|
async execute(input, ctx, opts) {
|
|
@@ -7250,11 +7978,11 @@ var testTool = {
|
|
|
7250
7978
|
}
|
|
7251
7979
|
};
|
|
7252
7980
|
async function detectRunner(cwd) {
|
|
7253
|
-
const { stat:
|
|
7981
|
+
const { stat: stat11 } = await import('node:fs/promises');
|
|
7254
7982
|
const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
|
|
7255
7983
|
for (const f of candidates) {
|
|
7256
7984
|
try {
|
|
7257
|
-
await
|
|
7985
|
+
await stat11(path3.join(cwd, f));
|
|
7258
7986
|
if (f.includes("vitest")) return "vitest";
|
|
7259
7987
|
if (f.includes("jest")) return "jest";
|
|
7260
7988
|
if (f.includes("mocha")) return "mocha";
|
|
@@ -7268,17 +7996,14 @@ function buildArgs2(runner, input) {
|
|
|
7268
7996
|
const timeout = input.timeout ?? 3e4;
|
|
7269
7997
|
switch (runner) {
|
|
7270
7998
|
case "vitest":
|
|
7271
|
-
args.push("
|
|
7272
|
-
if (input.
|
|
7273
|
-
args[1] = "";
|
|
7274
|
-
args.push("watch");
|
|
7275
|
-
}
|
|
7999
|
+
args.push(input.watch ? "watch" : "run");
|
|
8000
|
+
if (input.verbose) args.push("--reporter=verbose");
|
|
7276
8001
|
if (input.coverage) args.push("--coverage");
|
|
7277
8002
|
if (input.grep) args.push("--testNamePattern", input.grep);
|
|
7278
8003
|
args.push("--testTimeout", String(timeout));
|
|
7279
8004
|
break;
|
|
7280
8005
|
case "jest":
|
|
7281
|
-
args.push("--verbose");
|
|
8006
|
+
if (input.verbose) args.push("--verbose");
|
|
7282
8007
|
if (input.watch) args.push("--watch");
|
|
7283
8008
|
if (input.coverage) args.push("--coverage");
|
|
7284
8009
|
if (input.grep) args.push("--testPathPattern", input.grep);
|
|
@@ -7322,7 +8047,13 @@ function parseResult(runner, result, duration) {
|
|
|
7322
8047
|
passed,
|
|
7323
8048
|
failed,
|
|
7324
8049
|
duration_ms: duration,
|
|
7325
|
-
|
|
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
|
+
}),
|
|
7326
8057
|
truncated: result.truncated
|
|
7327
8058
|
};
|
|
7328
8059
|
}
|
|
@@ -7840,7 +8571,7 @@ var treeTool = {
|
|
|
7840
8571
|
}
|
|
7841
8572
|
};
|
|
7842
8573
|
async function walkDir(dir, depth, opts) {
|
|
7843
|
-
const entries = await
|
|
8574
|
+
const entries = await fs14.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
7844
8575
|
const filtered = entries.filter((e) => {
|
|
7845
8576
|
if (!opts.showHidden && e.name.startsWith(".")) return false;
|
|
7846
8577
|
if (opts.exclude.has(e.name)) return false;
|
|
@@ -7870,7 +8601,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
7870
8601
|
opts.lines.push(opts.prefix + branch + displayName);
|
|
7871
8602
|
if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
|
|
7872
8603
|
const childPrefix = opts.prefix + connector;
|
|
7873
|
-
await walkDir(
|
|
8604
|
+
await walkDir(path3.join(dir, entry.name), depth + 1, {
|
|
7874
8605
|
...opts,
|
|
7875
8606
|
prefix: childPrefix,
|
|
7876
8607
|
isLast
|
|
@@ -7949,12 +8680,12 @@ var typecheckTool = {
|
|
|
7949
8680
|
}
|
|
7950
8681
|
};
|
|
7951
8682
|
async function findTsConfig(cwd) {
|
|
7952
|
-
const { stat:
|
|
8683
|
+
const { stat: stat11 } = await import('node:fs/promises');
|
|
7953
8684
|
const candidates = ["tsconfig.json", "tsconfig.base.json"];
|
|
7954
8685
|
for (const f of candidates) {
|
|
7955
8686
|
try {
|
|
7956
|
-
const s = await
|
|
7957
|
-
if (s.isFile()) return
|
|
8687
|
+
const s = await stat11(path3.join(cwd, f));
|
|
8688
|
+
if (s.isFile()) return path3.join(cwd, f);
|
|
7958
8689
|
} catch {
|
|
7959
8690
|
}
|
|
7960
8691
|
}
|
|
@@ -7990,14 +8721,14 @@ var writeTool = {
|
|
|
7990
8721
|
let existed = false;
|
|
7991
8722
|
let prev = "";
|
|
7992
8723
|
try {
|
|
7993
|
-
const
|
|
7994
|
-
existed =
|
|
8724
|
+
const stat12 = await fs14.stat(absPath);
|
|
8725
|
+
existed = stat12.isFile();
|
|
7995
8726
|
if (existed) {
|
|
7996
8727
|
if (!ctx.hasRead(absPath)) {
|
|
7997
|
-
prev = await
|
|
7998
|
-
ctx.recordRead(absPath,
|
|
8728
|
+
prev = await fs14.readFile(absPath, "utf8");
|
|
8729
|
+
ctx.recordRead(absPath, stat12.mtimeMs);
|
|
7999
8730
|
} else {
|
|
8000
|
-
prev = await
|
|
8731
|
+
prev = await fs14.readFile(absPath, "utf8");
|
|
8001
8732
|
}
|
|
8002
8733
|
}
|
|
8003
8734
|
} catch (err) {
|
|
@@ -8008,8 +8739,8 @@ var writeTool = {
|
|
|
8008
8739
|
await atomicWrite(absPath, input.content);
|
|
8009
8740
|
const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
8010
8741
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
8011
|
-
const
|
|
8012
|
-
ctx.recordRead(absPath,
|
|
8742
|
+
const stat11 = await fs14.stat(absPath);
|
|
8743
|
+
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
8013
8744
|
ctx.session.recordFileChange({
|
|
8014
8745
|
path: absPath,
|
|
8015
8746
|
action: existed ? "modified" : "created",
|