@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/index.js
CHANGED
|
@@ -2,28 +2,30 @@ import * as fs4 from 'node:fs/promises';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { resolve, sep, dirname, join } from 'node:path';
|
|
4
4
|
import * as Core from '@wrongstack/core';
|
|
5
|
-
import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, emptyTaskFile, formatTaskList, formatPlan, recordPackageAction, detectPackageEcosystem, mutateTasks, emptyPlan, computeTaskItemProgress, resolveWstackPaths } from '@wrongstack/core';
|
|
5
|
+
import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, emptyTaskFile, formatTaskList, formatPlan, recordPackageAction, detectPackageEcosystem, mutateTasks, emptyPlan, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
6
6
|
import { spawn, execFileSync, spawnSync } from 'node:child_process';
|
|
7
7
|
import * as os from 'node:os';
|
|
8
8
|
import * as fs7 from 'node:fs';
|
|
9
|
-
import { statSync,
|
|
9
|
+
import { statSync, mkdirSync, createWriteStream, writeFileSync } from 'node:fs';
|
|
10
10
|
import * as dns from 'node:dns/promises';
|
|
11
11
|
import * as net from 'node:net';
|
|
12
12
|
import { Agent } from 'undici';
|
|
13
13
|
import { createRequire } from 'node:module';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { Worker } from 'node:worker_threads';
|
|
14
16
|
import * as ts from 'typescript';
|
|
15
17
|
import { randomUUID } from 'node:crypto';
|
|
16
18
|
|
|
17
19
|
// src/read.ts
|
|
18
20
|
async function detectPackageManager(cwd) {
|
|
19
|
-
const { stat:
|
|
21
|
+
const { stat: stat11 } = await import('node:fs/promises');
|
|
20
22
|
try {
|
|
21
|
-
await
|
|
23
|
+
await stat11(`${cwd}/pnpm-lock.yaml`);
|
|
22
24
|
return "pnpm";
|
|
23
25
|
} catch {
|
|
24
26
|
}
|
|
25
27
|
try {
|
|
26
|
-
await
|
|
28
|
+
await stat11(`${cwd}/yarn.lock`);
|
|
27
29
|
return "yarn";
|
|
28
30
|
} catch {
|
|
29
31
|
}
|
|
@@ -192,9 +194,9 @@ var readTool = {
|
|
|
192
194
|
async execute(input, ctx) {
|
|
193
195
|
if (!input?.path) throw new Error("read: path is required");
|
|
194
196
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
195
|
-
let
|
|
197
|
+
let stat11;
|
|
196
198
|
try {
|
|
197
|
-
|
|
199
|
+
stat11 = await fs4.stat(absPath);
|
|
198
200
|
} catch (err) {
|
|
199
201
|
const code = err.code;
|
|
200
202
|
if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
|
|
@@ -202,9 +204,9 @@ var readTool = {
|
|
|
202
204
|
`read: failed to stat "${input.path}": ${err instanceof Error ? err.message : String(err)}`
|
|
203
205
|
);
|
|
204
206
|
}
|
|
205
|
-
if (!
|
|
206
|
-
if (
|
|
207
|
-
throw new Error(`read: file too large (${
|
|
207
|
+
if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
|
|
208
|
+
if (stat11.size > MAX_BYTES) {
|
|
209
|
+
throw new Error(`read: file too large (${stat11.size} bytes, limit ${MAX_BYTES})`);
|
|
208
210
|
}
|
|
209
211
|
const buf = await fs4.readFile(absPath);
|
|
210
212
|
if (isBinaryBuffer(buf)) {
|
|
@@ -216,14 +218,14 @@ var readTool = {
|
|
|
216
218
|
const offset = Math.max(1, input.offset ?? 1);
|
|
217
219
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
218
220
|
if (limit === 0) {
|
|
219
|
-
ctx.recordRead(absPath,
|
|
221
|
+
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
220
222
|
return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
|
|
221
223
|
}
|
|
222
224
|
const slice = allLines.slice(offset - 1, offset - 1 + limit);
|
|
223
225
|
const truncated = offset - 1 + slice.length < total;
|
|
224
226
|
const width = String(offset + slice.length - 1).length;
|
|
225
227
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
226
|
-
ctx.recordRead(absPath,
|
|
228
|
+
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
227
229
|
return {
|
|
228
230
|
text: numbered,
|
|
229
231
|
total_lines: total,
|
|
@@ -262,12 +264,12 @@ var writeTool = {
|
|
|
262
264
|
let existed = false;
|
|
263
265
|
let prev = "";
|
|
264
266
|
try {
|
|
265
|
-
const
|
|
266
|
-
existed =
|
|
267
|
+
const stat12 = await fs4.stat(absPath);
|
|
268
|
+
existed = stat12.isFile();
|
|
267
269
|
if (existed) {
|
|
268
270
|
if (!ctx.hasRead(absPath)) {
|
|
269
271
|
prev = await fs4.readFile(absPath, "utf8");
|
|
270
|
-
ctx.recordRead(absPath,
|
|
272
|
+
ctx.recordRead(absPath, stat12.mtimeMs);
|
|
271
273
|
} else {
|
|
272
274
|
prev = await fs4.readFile(absPath, "utf8");
|
|
273
275
|
}
|
|
@@ -280,8 +282,8 @@ var writeTool = {
|
|
|
280
282
|
await atomicWrite(absPath, input.content);
|
|
281
283
|
const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
282
284
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
283
|
-
const
|
|
284
|
-
ctx.recordRead(absPath,
|
|
285
|
+
const stat11 = await fs4.stat(absPath);
|
|
286
|
+
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
285
287
|
ctx.session.recordFileChange({
|
|
286
288
|
path: absPath,
|
|
287
289
|
action: existed ? "modified" : "created",
|
|
@@ -321,13 +323,13 @@ var editTool = {
|
|
|
321
323
|
if (input.new_string === void 0) throw new Error("edit: new_string is required");
|
|
322
324
|
if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
|
|
323
325
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
324
|
-
const
|
|
326
|
+
const stat11 = await fs4.stat(absPath).catch((err) => {
|
|
325
327
|
if (err.code === "ENOENT") {
|
|
326
328
|
throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
|
|
327
329
|
}
|
|
328
330
|
throw err;
|
|
329
331
|
});
|
|
330
|
-
if (!
|
|
332
|
+
if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
|
|
331
333
|
if (!ctx.hasRead(absPath)) {
|
|
332
334
|
throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
|
|
333
335
|
}
|
|
@@ -522,8 +524,8 @@ var replaceTool = {
|
|
|
522
524
|
}
|
|
523
525
|
const rel = path.relative(realRoot, realPath);
|
|
524
526
|
if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
|
|
525
|
-
const
|
|
526
|
-
if (!
|
|
527
|
+
const stat11 = await fs4.stat(realPath).catch(() => null);
|
|
528
|
+
if (!stat11 || !stat11.isFile()) continue;
|
|
527
529
|
let content;
|
|
528
530
|
try {
|
|
529
531
|
const buf = await fs4.readFile(realPath);
|
|
@@ -548,7 +550,7 @@ var replaceTool = {
|
|
|
548
550
|
totalReplacements += count;
|
|
549
551
|
if (!dryRun) {
|
|
550
552
|
const newContent = toStyle(newContentLf, style);
|
|
551
|
-
await atomicWrite(realPath, newContent, { mode:
|
|
553
|
+
await atomicWrite(realPath, newContent, { mode: stat11.mode & 511 });
|
|
552
554
|
}
|
|
553
555
|
const diff = dryRun || matches.length > 0 ? unifiedDiff(content, toStyle(newContentLf, style), {
|
|
554
556
|
fromFile: absPath,
|
|
@@ -578,8 +580,8 @@ async function resolveFiles(filesInput, ctx, extraGlob) {
|
|
|
578
580
|
const resolved = [];
|
|
579
581
|
for (const p of parts) {
|
|
580
582
|
const absPath = safeResolve(p, ctx);
|
|
581
|
-
const
|
|
582
|
-
if (
|
|
583
|
+
const stat11 = await fs4.stat(absPath).catch(() => null);
|
|
584
|
+
if (stat11?.isFile()) {
|
|
583
585
|
resolved.push(absPath);
|
|
584
586
|
}
|
|
585
587
|
}
|
|
@@ -599,7 +601,7 @@ async function globFiles(pattern, base, extraGlob) {
|
|
|
599
601
|
function checkRg() {
|
|
600
602
|
return new Promise((resolve7) => {
|
|
601
603
|
try {
|
|
602
|
-
const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore" });
|
|
604
|
+
const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", windowsHide: true });
|
|
603
605
|
p.on("error", () => resolve7(false));
|
|
604
606
|
p.on("close", (code) => resolve7(code === 0));
|
|
605
607
|
} catch {
|
|
@@ -612,7 +614,8 @@ function spawnRgFind(pattern, base) {
|
|
|
612
614
|
const child = spawn("rg", args, {
|
|
613
615
|
signal: AbortSignal.timeout(3e4),
|
|
614
616
|
env: buildChildEnv(),
|
|
615
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
617
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
618
|
+
windowsHide: true
|
|
616
619
|
});
|
|
617
620
|
let buf = "";
|
|
618
621
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -641,8 +644,8 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
641
644
|
if (DEFAULT_IGNORE.includes(e.name)) continue;
|
|
642
645
|
const full = path.join(dir, e.name);
|
|
643
646
|
try {
|
|
644
|
-
const
|
|
645
|
-
if (
|
|
647
|
+
const stat11 = await fs4.lstat(full);
|
|
648
|
+
if (stat11.isSymbolicLink()) continue;
|
|
646
649
|
} catch {
|
|
647
650
|
continue;
|
|
648
651
|
}
|
|
@@ -828,7 +831,7 @@ var grepTool = {
|
|
|
828
831
|
async function detectRg(signal) {
|
|
829
832
|
return new Promise((resolve7) => {
|
|
830
833
|
try {
|
|
831
|
-
const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", signal });
|
|
834
|
+
const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", signal, windowsHide: true });
|
|
832
835
|
p.on("error", () => resolve7(false));
|
|
833
836
|
p.on("close", (code) => resolve7(code === 0));
|
|
834
837
|
} catch {
|
|
@@ -858,7 +861,7 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
858
861
|
const FLUSH_AT = 16;
|
|
859
862
|
const MAX_BUF_BYTES = 1e6;
|
|
860
863
|
let bufOverflow = false;
|
|
861
|
-
const child = spawn("rg", args, { signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"] });
|
|
864
|
+
const child = spawn("rg", args, { signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
862
865
|
const queue = [];
|
|
863
866
|
let waiter;
|
|
864
867
|
const wake = () => {
|
|
@@ -993,8 +996,8 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
993
996
|
if (globRe && !globRe.test(e.name) && !globRe.test(full)) continue;
|
|
994
997
|
if (globRe) globRe.lastIndex = 0;
|
|
995
998
|
try {
|
|
996
|
-
const
|
|
997
|
-
if (
|
|
999
|
+
const stat11 = await fs4.stat(full);
|
|
1000
|
+
if (stat11.size > 1e6) continue;
|
|
998
1001
|
const head = await fs4.readFile(full);
|
|
999
1002
|
if (isBinaryBuffer(head)) continue;
|
|
1000
1003
|
const text = head.toString("utf8");
|
|
@@ -1034,6 +1037,107 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
1034
1037
|
used: "native"
|
|
1035
1038
|
};
|
|
1036
1039
|
}
|
|
1040
|
+
var SPOOL_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1041
|
+
var SPOOL_WRITE_HWM_BYTES = 4 * 1024 * 1024;
|
|
1042
|
+
var sweepStarted = false;
|
|
1043
|
+
function toolOutputDir() {
|
|
1044
|
+
return path.join(wstackGlobalRoot(), "tool-output");
|
|
1045
|
+
}
|
|
1046
|
+
function sweepOldSpoolFiles(dir) {
|
|
1047
|
+
if (sweepStarted) return;
|
|
1048
|
+
sweepStarted = true;
|
|
1049
|
+
void (async () => {
|
|
1050
|
+
try {
|
|
1051
|
+
const now = Date.now();
|
|
1052
|
+
for (const name of await fs4.readdir(dir)) {
|
|
1053
|
+
if (!name.endsWith(".log")) continue;
|
|
1054
|
+
const p = path.join(dir, name);
|
|
1055
|
+
try {
|
|
1056
|
+
const st = await fs4.stat(p);
|
|
1057
|
+
if (now - st.mtimeMs > SPOOL_RETENTION_MS) await fs4.unlink(p);
|
|
1058
|
+
} catch {
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
} catch {
|
|
1062
|
+
}
|
|
1063
|
+
})();
|
|
1064
|
+
}
|
|
1065
|
+
function spoolNote(info) {
|
|
1066
|
+
const dropped = info.droppedBytes > 0 ? `, ~${info.droppedBytes} bytes dropped under backpressure` : "";
|
|
1067
|
+
return `
|
|
1068
|
+
[output truncated \u2014 full ${info.bytes} bytes at ${info.path}${dropped}; read/grep that file selectively instead of re-running with more output]`;
|
|
1069
|
+
}
|
|
1070
|
+
function createOutputSpool(opts) {
|
|
1071
|
+
const threshold = opts.thresholdBytes ?? 32768;
|
|
1072
|
+
const safeTool = opts.tool.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 40) || "tool";
|
|
1073
|
+
let head = "";
|
|
1074
|
+
let headBytes = 0;
|
|
1075
|
+
let totalBytes = 0;
|
|
1076
|
+
let droppedBytes = 0;
|
|
1077
|
+
let stream = null;
|
|
1078
|
+
let filePath = null;
|
|
1079
|
+
let failed = false;
|
|
1080
|
+
let finalized = false;
|
|
1081
|
+
const open = () => {
|
|
1082
|
+
if (stream || failed) return;
|
|
1083
|
+
try {
|
|
1084
|
+
const dir = toolOutputDir();
|
|
1085
|
+
mkdirSync(dir, { recursive: true });
|
|
1086
|
+
sweepOldSpoolFiles(dir);
|
|
1087
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1088
|
+
const rand = Math.random().toString(36).slice(2, 6);
|
|
1089
|
+
filePath = path.join(dir, `${stamp}-${safeTool}-${rand}.log`);
|
|
1090
|
+
stream = createWriteStream(filePath, { flags: "w", encoding: "utf8" });
|
|
1091
|
+
stream.on("error", () => {
|
|
1092
|
+
failed = true;
|
|
1093
|
+
stream = null;
|
|
1094
|
+
filePath = null;
|
|
1095
|
+
});
|
|
1096
|
+
stream.write(head);
|
|
1097
|
+
} catch {
|
|
1098
|
+
failed = true;
|
|
1099
|
+
stream = null;
|
|
1100
|
+
filePath = null;
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
return {
|
|
1104
|
+
write(text) {
|
|
1105
|
+
if (finalized || !text) return;
|
|
1106
|
+
totalBytes += Buffer.byteLength(text, "utf8");
|
|
1107
|
+
if (!stream && !failed) {
|
|
1108
|
+
if (headBytes + text.length <= threshold) {
|
|
1109
|
+
head += text;
|
|
1110
|
+
headBytes += text.length;
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
head += text;
|
|
1114
|
+
open();
|
|
1115
|
+
head = "";
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
if (stream) {
|
|
1119
|
+
if (stream.writableLength > SPOOL_WRITE_HWM_BYTES) {
|
|
1120
|
+
droppedBytes += Buffer.byteLength(text, "utf8");
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
stream.write(text);
|
|
1124
|
+
}
|
|
1125
|
+
},
|
|
1126
|
+
finalize() {
|
|
1127
|
+
if (finalized) {
|
|
1128
|
+
return filePath ? { path: filePath, bytes: totalBytes, droppedBytes } : null;
|
|
1129
|
+
}
|
|
1130
|
+
finalized = true;
|
|
1131
|
+
head = "";
|
|
1132
|
+
if (!stream || !filePath) return null;
|
|
1133
|
+
try {
|
|
1134
|
+
stream.end();
|
|
1135
|
+
} catch {
|
|
1136
|
+
}
|
|
1137
|
+
return { path: filePath, bytes: totalBytes, droppedBytes };
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1037
1141
|
|
|
1038
1142
|
// src/circuit-breaker.ts
|
|
1039
1143
|
var DEFAULT_MAX_CONSECUTIVE_FAILURES = 5;
|
|
@@ -1513,7 +1617,7 @@ var bashTool = {
|
|
|
1513
1617
|
})();
|
|
1514
1618
|
const args = isWin ? ["/c", input.command] : ["-c", input.command];
|
|
1515
1619
|
const env = buildChildEnv(ctx.session?.id);
|
|
1516
|
-
const detached = isWin
|
|
1620
|
+
const detached = !isWin;
|
|
1517
1621
|
const startedAt = Date.now();
|
|
1518
1622
|
if (input.background) {
|
|
1519
1623
|
let buf2 = "";
|
|
@@ -1522,7 +1626,15 @@ var bashTool = {
|
|
|
1522
1626
|
cwd: ctx.projectRoot,
|
|
1523
1627
|
env,
|
|
1524
1628
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1525
|
-
|
|
1629
|
+
// win32: CreateProcess IGNORES CREATE_NO_WINDOW (windowsHide) when
|
|
1630
|
+
// DETACHED_PROCESS (detached: true) is set, so the console-less
|
|
1631
|
+
// cmd.exe's grandchildren (node, dev servers) each allocate a fresh
|
|
1632
|
+
// VISIBLE console window. detached: false lets CREATE_NO_WINDOW
|
|
1633
|
+
// apply: the child gets a hidden console that grandchildren inherit.
|
|
1634
|
+
// Windows children survive parent exit either way. POSIX keeps
|
|
1635
|
+
// detached for the process-group kill semantics.
|
|
1636
|
+
detached: !isWin,
|
|
1637
|
+
windowsHide: true,
|
|
1526
1638
|
signal: opts.signal
|
|
1527
1639
|
});
|
|
1528
1640
|
const pid2 = child2.pid;
|
|
@@ -1537,24 +1649,22 @@ var bashTool = {
|
|
|
1537
1649
|
});
|
|
1538
1650
|
child2.on("close", () => registry.unregister(pid2));
|
|
1539
1651
|
}
|
|
1540
|
-
|
|
1541
|
-
if (
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
}
|
|
1546
|
-
if (buf2.length >= MAX_OUTPUT) truncated = true;
|
|
1652
|
+
const onBgData = (chunk) => {
|
|
1653
|
+
if (truncated) return;
|
|
1654
|
+
const remain = MAX_OUTPUT - buf2.length;
|
|
1655
|
+
if (remain > 0) {
|
|
1656
|
+
buf2 += chunk.toString().slice(0, remain);
|
|
1547
1657
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
if (remain > 0) {
|
|
1553
|
-
buf2 += chunk.toString().slice(0, remain);
|
|
1554
|
-
}
|
|
1555
|
-
if (buf2.length >= MAX_OUTPUT) truncated = true;
|
|
1658
|
+
if (buf2.length >= MAX_OUTPUT) {
|
|
1659
|
+
truncated = true;
|
|
1660
|
+
child2.stdout?.off("data", onBgData);
|
|
1661
|
+
child2.stderr?.off("data", onBgData);
|
|
1556
1662
|
}
|
|
1557
|
-
}
|
|
1663
|
+
};
|
|
1664
|
+
child2.stdout?.on("data", onBgData);
|
|
1665
|
+
child2.stderr?.on("data", onBgData);
|
|
1666
|
+
child2.stdout?.unref?.();
|
|
1667
|
+
child2.stderr?.unref?.();
|
|
1558
1668
|
child2.on("close", () => {
|
|
1559
1669
|
registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
|
|
1560
1670
|
});
|
|
@@ -1575,6 +1685,7 @@ var bashTool = {
|
|
|
1575
1685
|
env,
|
|
1576
1686
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1577
1687
|
detached,
|
|
1688
|
+
windowsHide: true,
|
|
1578
1689
|
...isWin ? {} : { signal: opts.signal }
|
|
1579
1690
|
});
|
|
1580
1691
|
const pid = child.pid;
|
|
@@ -1589,9 +1700,10 @@ var bashTool = {
|
|
|
1589
1700
|
});
|
|
1590
1701
|
}
|
|
1591
1702
|
let buf = "";
|
|
1592
|
-
let
|
|
1703
|
+
let pending2 = "";
|
|
1593
1704
|
let timedOut = false;
|
|
1594
1705
|
const timers = [];
|
|
1706
|
+
const spool = createOutputSpool({ tool: "bash", thresholdBytes: MAX_OUTPUT });
|
|
1595
1707
|
function killWithTimeout(child2, timeoutMs2) {
|
|
1596
1708
|
if (isWin) {
|
|
1597
1709
|
if (typeof child2.pid === "number" && child2.exitCode === null && killWin32Tree(child2.pid)) {
|
|
@@ -1671,9 +1783,9 @@ var bashTool = {
|
|
|
1671
1783
|
});
|
|
1672
1784
|
let lastFlush = Date.now();
|
|
1673
1785
|
const flush = () => {
|
|
1674
|
-
if (
|
|
1675
|
-
const text =
|
|
1676
|
-
|
|
1786
|
+
if (pending2.length === 0) return null;
|
|
1787
|
+
const text = pending2;
|
|
1788
|
+
pending2 = "";
|
|
1677
1789
|
lastFlush = Date.now();
|
|
1678
1790
|
return text;
|
|
1679
1791
|
};
|
|
@@ -1697,7 +1809,8 @@ var bashTool = {
|
|
|
1697
1809
|
if (buf.length < MAX_OUTPUT) {
|
|
1698
1810
|
buf += text.slice(0, MAX_OUTPUT - buf.length);
|
|
1699
1811
|
}
|
|
1700
|
-
|
|
1812
|
+
spool.write(text);
|
|
1813
|
+
pending2 += text;
|
|
1701
1814
|
push({ kind: "data", text });
|
|
1702
1815
|
pauseIfFlooded();
|
|
1703
1816
|
};
|
|
@@ -1724,10 +1837,11 @@ var bashTool = {
|
|
|
1724
1837
|
if (remainder !== null) {
|
|
1725
1838
|
yield { type: "partial_output", text: remainder };
|
|
1726
1839
|
}
|
|
1840
|
+
const spooled = spool.finalize();
|
|
1727
1841
|
yield {
|
|
1728
1842
|
type: "final",
|
|
1729
1843
|
output: {
|
|
1730
|
-
output: normalizeCommandOutput(buf),
|
|
1844
|
+
output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : ""),
|
|
1731
1845
|
exit_code: c.code,
|
|
1732
1846
|
timed_out: timedOut
|
|
1733
1847
|
}
|
|
@@ -1735,13 +1849,14 @@ var bashTool = {
|
|
|
1735
1849
|
return;
|
|
1736
1850
|
}
|
|
1737
1851
|
const now = Date.now();
|
|
1738
|
-
if (
|
|
1852
|
+
if (pending2.length >= STREAM_FLUSH_BYTES || now - lastFlush >= STREAM_FLUSH_INTERVAL_MS) {
|
|
1739
1853
|
const text = flush();
|
|
1740
1854
|
if (text) yield { type: "partial_output", text };
|
|
1741
1855
|
}
|
|
1742
1856
|
}
|
|
1743
1857
|
} finally {
|
|
1744
1858
|
for (const t of timers) clearTimeout(t);
|
|
1859
|
+
spool.finalize();
|
|
1745
1860
|
if (isWin) opts.signal.removeEventListener("abort", onAbort);
|
|
1746
1861
|
child.stdout?.off("data", onData);
|
|
1747
1862
|
child.stderr?.off("data", onData);
|
|
@@ -1982,6 +2097,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
1982
2097
|
let stderr = "";
|
|
1983
2098
|
let killed = false;
|
|
1984
2099
|
const startedAt = Date.now();
|
|
2100
|
+
const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
|
|
1985
2101
|
const resolved = resolveWin32Command(cmd);
|
|
1986
2102
|
const isWin = process.platform === "win32";
|
|
1987
2103
|
const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
@@ -1989,6 +2105,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
1989
2105
|
cwd,
|
|
1990
2106
|
env: buildChildEnv(sessionId),
|
|
1991
2107
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2108
|
+
windowsHide: true,
|
|
1992
2109
|
...isWin ? {} : { signal },
|
|
1993
2110
|
...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
|
|
1994
2111
|
});
|
|
@@ -2013,10 +2130,14 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
2013
2130
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
2014
2131
|
}
|
|
2015
2132
|
child.stdout?.on("data", (chunk) => {
|
|
2016
|
-
|
|
2133
|
+
const text = chunk.toString();
|
|
2134
|
+
if (stdout.length < MAX_OUTPUT2) stdout += text;
|
|
2135
|
+
spool.write(text);
|
|
2017
2136
|
});
|
|
2018
2137
|
child.stderr?.on("data", (chunk) => {
|
|
2019
|
-
|
|
2138
|
+
const text = chunk.toString();
|
|
2139
|
+
if (stderr.length < MAX_OUTPUT2) stderr += text;
|
|
2140
|
+
spool.write(text);
|
|
2020
2141
|
});
|
|
2021
2142
|
child.on("close", (code) => {
|
|
2022
2143
|
clearTimeout(timer);
|
|
@@ -2025,10 +2146,11 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
2025
2146
|
const durationMs = Date.now() - startedAt;
|
|
2026
2147
|
const exitCode = killed ? 124 : code ?? 1;
|
|
2027
2148
|
registry.afterCall(durationMs, exitCode !== 0);
|
|
2149
|
+
const spooled = spool.finalize();
|
|
2028
2150
|
resolve7({
|
|
2029
2151
|
command: cmd,
|
|
2030
2152
|
args,
|
|
2031
|
-
stdout: normalizeCommandOutput(stdout),
|
|
2153
|
+
stdout: normalizeCommandOutput(stdout) + (spooled ? spoolNote(spooled) : ""),
|
|
2032
2154
|
stderr: normalizeCommandOutput(stderr),
|
|
2033
2155
|
exitCode,
|
|
2034
2156
|
truncated: Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES || Buffer.byteLength(stderr, "utf8") > COMMAND_OUTPUT_MAX_BYTES,
|
|
@@ -2040,6 +2162,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
2040
2162
|
if (isWin) signal.removeEventListener("abort", onAbort);
|
|
2041
2163
|
if (typeof pid === "number") registry.unregister(pid);
|
|
2042
2164
|
registry.afterCall(Date.now() - startedAt, true);
|
|
2165
|
+
spool.finalize();
|
|
2043
2166
|
resolve7({
|
|
2044
2167
|
command: cmd,
|
|
2045
2168
|
args,
|
|
@@ -3062,8 +3185,8 @@ function findGitDir(cwd, projectRoot) {
|
|
|
3062
3185
|
let dir = cwd;
|
|
3063
3186
|
for (let i = 0; i < 20; i++) {
|
|
3064
3187
|
try {
|
|
3065
|
-
const
|
|
3066
|
-
if (
|
|
3188
|
+
const stat11 = statSync(`${dir}/.git`);
|
|
3189
|
+
if (stat11.isDirectory() || stat11.isFile()) return dir;
|
|
3067
3190
|
} catch {
|
|
3068
3191
|
}
|
|
3069
3192
|
if (dir === root) break;
|
|
@@ -3151,7 +3274,8 @@ function runGit(args, cwd, signal) {
|
|
|
3151
3274
|
cwd,
|
|
3152
3275
|
signal,
|
|
3153
3276
|
env: buildChildEnv(),
|
|
3154
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3277
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3278
|
+
windowsHide: true
|
|
3155
3279
|
});
|
|
3156
3280
|
child.stdout?.on("data", (chunk) => {
|
|
3157
3281
|
if (stdout.length < MAX_OUTPUT3) {
|
|
@@ -3274,7 +3398,7 @@ function runPatch(args, cwd, signal) {
|
|
|
3274
3398
|
let stdout = "";
|
|
3275
3399
|
let stderr = "";
|
|
3276
3400
|
const env = { ...buildChildEnv(), LANG: "C", LC_ALL: "C" };
|
|
3277
|
-
const child = spawn("patch", args, { cwd, signal, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
3401
|
+
const child = spawn("patch", args, { cwd, signal, env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
3278
3402
|
child.stdout?.on("data", (c) => {
|
|
3279
3403
|
stdout += c.toString();
|
|
3280
3404
|
});
|
|
@@ -3365,8 +3489,8 @@ var jsonTool = {
|
|
|
3365
3489
|
};
|
|
3366
3490
|
}
|
|
3367
3491
|
};
|
|
3368
|
-
function query(data,
|
|
3369
|
-
const parts =
|
|
3492
|
+
function query(data, path21) {
|
|
3493
|
+
const parts = path21.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
3370
3494
|
let current = data;
|
|
3371
3495
|
for (const part of parts) {
|
|
3372
3496
|
if (current === null || current === void 0) return void 0;
|
|
@@ -3495,8 +3619,8 @@ function findGitDir2(cwd) {
|
|
|
3495
3619
|
let dir = cwd;
|
|
3496
3620
|
for (let i = 0; i < 20; i++) {
|
|
3497
3621
|
try {
|
|
3498
|
-
const
|
|
3499
|
-
if (
|
|
3622
|
+
const stat11 = statSync(path.join(dir, ".git"));
|
|
3623
|
+
if (stat11.isDirectory()) return dir;
|
|
3500
3624
|
} catch {
|
|
3501
3625
|
}
|
|
3502
3626
|
const parent = path.dirname(dir);
|
|
@@ -3513,7 +3637,8 @@ function runGit2(args, cwd, signal) {
|
|
|
3513
3637
|
cwd,
|
|
3514
3638
|
signal,
|
|
3515
3639
|
env: buildChildEnv(),
|
|
3516
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3640
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3641
|
+
windowsHide: true
|
|
3517
3642
|
});
|
|
3518
3643
|
child.stdout?.on("data", (c) => {
|
|
3519
3644
|
stdout += c.toString();
|
|
@@ -3539,8 +3664,8 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
3539
3664
|
const results = [];
|
|
3540
3665
|
for (const file of files) {
|
|
3541
3666
|
const absPath = safeResolve(file, ctx);
|
|
3542
|
-
const
|
|
3543
|
-
if (!
|
|
3667
|
+
const stat11 = await fs4.stat(absPath).catch(() => null);
|
|
3668
|
+
if (!stat11?.isFile()) continue;
|
|
3544
3669
|
const content = await fs4.readFile(absPath, "utf8");
|
|
3545
3670
|
const lines = content.split(/\r?\n/);
|
|
3546
3671
|
results.push(formatWithLineNumbers(file, lines));
|
|
@@ -3741,17 +3866,31 @@ async function* spawnStream(opts) {
|
|
|
3741
3866
|
const maxQueue = opts.maxQueueSize ?? 500;
|
|
3742
3867
|
let stdout = "";
|
|
3743
3868
|
let stderr = "";
|
|
3744
|
-
let
|
|
3869
|
+
let pending2 = "";
|
|
3745
3870
|
let error;
|
|
3871
|
+
const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
|
|
3746
3872
|
const cmd = resolveWin32Command(opts.cmd);
|
|
3747
|
-
const
|
|
3873
|
+
const isWin = process.platform === "win32";
|
|
3874
|
+
const needsShell = isWin && (cmd.endsWith(".cmd") || cmd.endsWith(".bat"));
|
|
3748
3875
|
const child = spawn(cmd, opts.args, {
|
|
3749
3876
|
cwd: opts.cwd,
|
|
3750
|
-
signal: opts.signal,
|
|
3751
3877
|
env: buildChildEnv(),
|
|
3752
3878
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3879
|
+
windowsHide: true,
|
|
3880
|
+
...isWin ? {} : { signal: opts.signal },
|
|
3753
3881
|
...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
|
|
3754
3882
|
});
|
|
3883
|
+
const registry = getProcessRegistry();
|
|
3884
|
+
const pid = child.pid;
|
|
3885
|
+
if (typeof pid === "number") {
|
|
3886
|
+
registry.register({
|
|
3887
|
+
pid,
|
|
3888
|
+
name: opts.cmd,
|
|
3889
|
+
command: redactCommand(`${opts.cmd} ${opts.args.join(" ")}`),
|
|
3890
|
+
startedAt: Date.now(),
|
|
3891
|
+
child
|
|
3892
|
+
});
|
|
3893
|
+
}
|
|
3755
3894
|
const queue = [];
|
|
3756
3895
|
let waiter;
|
|
3757
3896
|
let paused = false;
|
|
@@ -3769,9 +3908,10 @@ async function* spawnStream(opts) {
|
|
|
3769
3908
|
child.stderr?.resume();
|
|
3770
3909
|
}
|
|
3771
3910
|
};
|
|
3772
|
-
|
|
3911
|
+
const onOut = (c) => {
|
|
3773
3912
|
const s = c.toString();
|
|
3774
3913
|
if (stdout.length < max) stdout += s;
|
|
3914
|
+
spool.write(s);
|
|
3775
3915
|
queue.push({ kind: "out", data: s });
|
|
3776
3916
|
wake();
|
|
3777
3917
|
if (!paused && queue.length >= maxQueue) {
|
|
@@ -3779,10 +3919,11 @@ async function* spawnStream(opts) {
|
|
|
3779
3919
|
child.stdout?.pause();
|
|
3780
3920
|
child.stderr?.pause();
|
|
3781
3921
|
}
|
|
3782
|
-
}
|
|
3783
|
-
|
|
3922
|
+
};
|
|
3923
|
+
const onErr = (c) => {
|
|
3784
3924
|
const s = c.toString();
|
|
3785
3925
|
if (stderr.length < max) stderr += s;
|
|
3926
|
+
spool.write(s);
|
|
3786
3927
|
queue.push({ kind: "err", data: s });
|
|
3787
3928
|
wake();
|
|
3788
3929
|
if (!paused && queue.length >= maxQueue) {
|
|
@@ -3790,51 +3931,92 @@ async function* spawnStream(opts) {
|
|
|
3790
3931
|
child.stdout?.pause();
|
|
3791
3932
|
child.stderr?.pause();
|
|
3792
3933
|
}
|
|
3793
|
-
}
|
|
3934
|
+
};
|
|
3935
|
+
child.stdout?.on("data", onOut);
|
|
3936
|
+
child.stderr?.on("data", onErr);
|
|
3794
3937
|
child.on("error", (e) => {
|
|
3795
3938
|
error = e.message;
|
|
3796
3939
|
queue.push({ kind: "error", data: e.message });
|
|
3797
3940
|
wake();
|
|
3798
3941
|
});
|
|
3799
3942
|
child.on("close", (code) => {
|
|
3943
|
+
if (typeof pid === "number") registry.unregister(pid);
|
|
3800
3944
|
queue.push({ kind: "close", data: "", code: code ?? 0 });
|
|
3801
3945
|
wake();
|
|
3802
3946
|
});
|
|
3947
|
+
const onAbort = () => {
|
|
3948
|
+
if (typeof pid === "number") {
|
|
3949
|
+
registry.kill(pid, { force: true });
|
|
3950
|
+
} else {
|
|
3951
|
+
try {
|
|
3952
|
+
child.kill("SIGKILL");
|
|
3953
|
+
} catch {
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
queue.push({ kind: "close", data: "", code: 124 });
|
|
3957
|
+
wake();
|
|
3958
|
+
};
|
|
3959
|
+
if (opts.signal.aborted) onAbort();
|
|
3960
|
+
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
3803
3961
|
let exitCode = 0;
|
|
3804
3962
|
let spawnFailed = false;
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
if (
|
|
3815
|
-
|
|
3963
|
+
try {
|
|
3964
|
+
for (; ; ) {
|
|
3965
|
+
while (queue.length === 0) {
|
|
3966
|
+
await new Promise((resolve7) => {
|
|
3967
|
+
waiter = resolve7;
|
|
3968
|
+
});
|
|
3969
|
+
}
|
|
3970
|
+
const chunk = queue.shift();
|
|
3971
|
+
resume();
|
|
3972
|
+
if (chunk.kind === "close") {
|
|
3973
|
+
if (!spawnFailed) exitCode = chunk.code ?? 0;
|
|
3974
|
+
break;
|
|
3975
|
+
}
|
|
3976
|
+
if (chunk.kind === "error") {
|
|
3977
|
+
spawnFailed = true;
|
|
3978
|
+
exitCode = 1;
|
|
3979
|
+
continue;
|
|
3980
|
+
}
|
|
3981
|
+
pending2 += chunk.data;
|
|
3982
|
+
if (pending2.length >= flushAt) {
|
|
3983
|
+
yield { type: "partial_output", text: pending2 };
|
|
3984
|
+
pending2 = "";
|
|
3985
|
+
}
|
|
3816
3986
|
}
|
|
3817
|
-
if (
|
|
3818
|
-
|
|
3819
|
-
exitCode = 1;
|
|
3820
|
-
continue;
|
|
3987
|
+
if (pending2.length > 0) {
|
|
3988
|
+
yield { type: "partial_output", text: pending2 };
|
|
3821
3989
|
}
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3990
|
+
const spooled = spool.finalize();
|
|
3991
|
+
return {
|
|
3992
|
+
// The marker rides on stdout's tail so every consumer's head+tail
|
|
3993
|
+
// normalization keeps it without per-tool changes.
|
|
3994
|
+
stdout: spooled ? stdout + spoolNote(spooled) : stdout,
|
|
3995
|
+
stderr,
|
|
3996
|
+
exitCode,
|
|
3997
|
+
truncated: stdout.length >= max || stderr.length >= max,
|
|
3998
|
+
error,
|
|
3999
|
+
spoolPath: spooled?.path,
|
|
4000
|
+
spoolBytes: spooled?.bytes
|
|
4001
|
+
};
|
|
4002
|
+
} finally {
|
|
4003
|
+
spool.finalize();
|
|
4004
|
+
opts.signal.removeEventListener("abort", onAbort);
|
|
4005
|
+
child.stdout?.off("data", onOut);
|
|
4006
|
+
child.stderr?.off("data", onErr);
|
|
4007
|
+
child.stdout?.destroy();
|
|
4008
|
+
child.stderr?.destroy();
|
|
4009
|
+
if (child.exitCode === null && !child.killed) {
|
|
4010
|
+
if (typeof pid === "number") {
|
|
4011
|
+
registry.kill(pid, { force: true });
|
|
4012
|
+
} else {
|
|
4013
|
+
try {
|
|
4014
|
+
child.kill("SIGKILL");
|
|
4015
|
+
} catch {
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
3826
4018
|
}
|
|
3827
4019
|
}
|
|
3828
|
-
if (pending.length > 0) {
|
|
3829
|
-
yield { type: "partial_output", text: pending };
|
|
3830
|
-
}
|
|
3831
|
-
return {
|
|
3832
|
-
stdout,
|
|
3833
|
-
stderr,
|
|
3834
|
-
exitCode,
|
|
3835
|
-
truncated: stdout.length >= max || stderr.length >= max,
|
|
3836
|
-
error
|
|
3837
|
-
};
|
|
3838
4020
|
}
|
|
3839
4021
|
|
|
3840
4022
|
// src/lint.ts
|
|
@@ -3917,11 +4099,11 @@ var lintTool = {
|
|
|
3917
4099
|
}
|
|
3918
4100
|
};
|
|
3919
4101
|
async function detectLinter(cwd) {
|
|
3920
|
-
const { stat:
|
|
4102
|
+
const { stat: stat11 } = await import('node:fs/promises');
|
|
3921
4103
|
const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
|
|
3922
4104
|
for (const f of checks) {
|
|
3923
4105
|
try {
|
|
3924
|
-
await
|
|
4106
|
+
await stat11(`${cwd}/${f}`);
|
|
3925
4107
|
if (f.includes("biome")) return "biome";
|
|
3926
4108
|
if (f.includes("eslint")) return "eslint";
|
|
3927
4109
|
if (f.includes("tslint")) return "tslint";
|
|
@@ -4019,13 +4201,13 @@ var formatTool = {
|
|
|
4019
4201
|
}
|
|
4020
4202
|
};
|
|
4021
4203
|
async function detectFixer(cwd) {
|
|
4022
|
-
const { stat:
|
|
4204
|
+
const { stat: stat11 } = await import('node:fs/promises');
|
|
4023
4205
|
try {
|
|
4024
|
-
await
|
|
4206
|
+
await stat11(`${cwd}/biome.json`);
|
|
4025
4207
|
return "biome";
|
|
4026
4208
|
} catch {
|
|
4027
4209
|
try {
|
|
4028
|
-
await
|
|
4210
|
+
await stat11(`${cwd}/.prettierrc`);
|
|
4029
4211
|
return "prettier";
|
|
4030
4212
|
} catch {
|
|
4031
4213
|
return "biome";
|
|
@@ -4103,11 +4285,11 @@ var typecheckTool = {
|
|
|
4103
4285
|
}
|
|
4104
4286
|
};
|
|
4105
4287
|
async function findTsConfig(cwd) {
|
|
4106
|
-
const { stat:
|
|
4288
|
+
const { stat: stat11 } = await import('node:fs/promises');
|
|
4107
4289
|
const candidates = ["tsconfig.json", "tsconfig.base.json"];
|
|
4108
4290
|
for (const f of candidates) {
|
|
4109
4291
|
try {
|
|
4110
|
-
const s = await
|
|
4292
|
+
const s = await stat11(path.join(cwd, f));
|
|
4111
4293
|
if (s.isFile()) return path.join(cwd, f);
|
|
4112
4294
|
} catch {
|
|
4113
4295
|
}
|
|
@@ -4138,7 +4320,11 @@ var testTool = {
|
|
|
4138
4320
|
coverage: { type: "boolean", description: "Generate coverage report (default: false)" },
|
|
4139
4321
|
cwd: { type: "string", description: "Working directory (default: cwd)" },
|
|
4140
4322
|
grep: { type: "string", description: "Filter tests by name pattern (default: none)" },
|
|
4141
|
-
timeout: { type: "integer", description: "Test timeout in ms (default: 30000)" }
|
|
4323
|
+
timeout: { type: "integer", description: "Test timeout in ms (default: 30000)" },
|
|
4324
|
+
verbose: {
|
|
4325
|
+
type: "boolean",
|
|
4326
|
+
description: "Per-test verbose reporter output (default: false \u2014 the summary reporter is used; full output is always saved to a log file referenced in the result)"
|
|
4327
|
+
}
|
|
4142
4328
|
}
|
|
4143
4329
|
},
|
|
4144
4330
|
async execute(input, ctx, opts) {
|
|
@@ -4186,11 +4372,11 @@ var testTool = {
|
|
|
4186
4372
|
}
|
|
4187
4373
|
};
|
|
4188
4374
|
async function detectRunner(cwd) {
|
|
4189
|
-
const { stat:
|
|
4375
|
+
const { stat: stat11 } = await import('node:fs/promises');
|
|
4190
4376
|
const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
|
|
4191
4377
|
for (const f of candidates) {
|
|
4192
4378
|
try {
|
|
4193
|
-
await
|
|
4379
|
+
await stat11(path.join(cwd, f));
|
|
4194
4380
|
if (f.includes("vitest")) return "vitest";
|
|
4195
4381
|
if (f.includes("jest")) return "jest";
|
|
4196
4382
|
if (f.includes("mocha")) return "mocha";
|
|
@@ -4204,17 +4390,14 @@ function buildArgs2(runner, input) {
|
|
|
4204
4390
|
const timeout = input.timeout ?? 3e4;
|
|
4205
4391
|
switch (runner) {
|
|
4206
4392
|
case "vitest":
|
|
4207
|
-
args.push("
|
|
4208
|
-
if (input.
|
|
4209
|
-
args[1] = "";
|
|
4210
|
-
args.push("watch");
|
|
4211
|
-
}
|
|
4393
|
+
args.push(input.watch ? "watch" : "run");
|
|
4394
|
+
if (input.verbose) args.push("--reporter=verbose");
|
|
4212
4395
|
if (input.coverage) args.push("--coverage");
|
|
4213
4396
|
if (input.grep) args.push("--testNamePattern", input.grep);
|
|
4214
4397
|
args.push("--testTimeout", String(timeout));
|
|
4215
4398
|
break;
|
|
4216
4399
|
case "jest":
|
|
4217
|
-
args.push("--verbose");
|
|
4400
|
+
if (input.verbose) args.push("--verbose");
|
|
4218
4401
|
if (input.watch) args.push("--watch");
|
|
4219
4402
|
if (input.coverage) args.push("--coverage");
|
|
4220
4403
|
if (input.grep) args.push("--testPathPattern", input.grep);
|
|
@@ -4258,7 +4441,13 @@ function parseResult(runner, result, duration) {
|
|
|
4258
4441
|
passed,
|
|
4259
4442
|
failed,
|
|
4260
4443
|
duration_ms: duration,
|
|
4261
|
-
|
|
4444
|
+
// A passing run only needs the tail summary in chat history — counts are
|
|
4445
|
+
// already parsed above and the FULL log is on disk (spool marker rides
|
|
4446
|
+
// the stdout tail). Failures keep the standard command-output cap so
|
|
4447
|
+
// the agent sees the failure details inline.
|
|
4448
|
+
output: normalizeCommandOutput(result.stdout || result.error || "", {
|
|
4449
|
+
maxBytes: result.exitCode === 0 ? 4096 : void 0
|
|
4450
|
+
}),
|
|
4262
4451
|
truncated: result.truncated
|
|
4263
4452
|
};
|
|
4264
4453
|
}
|
|
@@ -4539,7 +4728,7 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
4539
4728
|
const MAX = 1e5;
|
|
4540
4729
|
const resolved = resolveWin32Command(manager);
|
|
4541
4730
|
const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
4542
|
-
const child = spawn(resolved, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
|
|
4731
|
+
const child = spawn(resolved, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
|
|
4543
4732
|
child.stdout?.on("data", (c) => {
|
|
4544
4733
|
if (stdout.length < MAX) stdout += c.toString();
|
|
4545
4734
|
});
|
|
@@ -4693,7 +4882,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
4693
4882
|
clearTimeout(timer);
|
|
4694
4883
|
resolve7(result);
|
|
4695
4884
|
};
|
|
4696
|
-
const child = spawn("docker", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"] });
|
|
4885
|
+
const child = spawn("docker", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
4697
4886
|
const timer = setTimeout(() => {
|
|
4698
4887
|
child.kill("SIGTERM");
|
|
4699
4888
|
finish(empty());
|
|
@@ -4724,7 +4913,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
4724
4913
|
}
|
|
4725
4914
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
4726
4915
|
var MAX_TAIL_LINES = 1e5;
|
|
4727
|
-
async function fileLogs(
|
|
4916
|
+
async function fileLogs(path21, lines, filterRe, stream) {
|
|
4728
4917
|
const { createInterface } = await import('node:readline');
|
|
4729
4918
|
const { createReadStream } = await import('node:fs');
|
|
4730
4919
|
const entries = [];
|
|
@@ -4733,7 +4922,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
|
|
|
4733
4922
|
let writeIdx = 0;
|
|
4734
4923
|
let totalLines = 0;
|
|
4735
4924
|
const rl = createInterface({
|
|
4736
|
-
input: createReadStream(
|
|
4925
|
+
input: createReadStream(path21),
|
|
4737
4926
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
4738
4927
|
});
|
|
4739
4928
|
for await (const line of rl) {
|
|
@@ -4754,7 +4943,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
|
|
|
4754
4943
|
if (parsed) entries.push(parsed);
|
|
4755
4944
|
}
|
|
4756
4945
|
return {
|
|
4757
|
-
source:
|
|
4946
|
+
source: path21,
|
|
4758
4947
|
entries,
|
|
4759
4948
|
total: entries.length,
|
|
4760
4949
|
truncated: totalLines > effLines,
|
|
@@ -4877,8 +5066,8 @@ async function resolveFiles2(filesInput, cwd) {
|
|
|
4877
5066
|
for (const f of files) {
|
|
4878
5067
|
const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
|
|
4879
5068
|
try {
|
|
4880
|
-
const
|
|
4881
|
-
if (
|
|
5069
|
+
const stat11 = await fs4.stat(absPath);
|
|
5070
|
+
if (stat11.isFile()) resolved.push(absPath);
|
|
4882
5071
|
} catch {
|
|
4883
5072
|
}
|
|
4884
5073
|
}
|
|
@@ -5754,8 +5943,91 @@ ${mode.description}`
|
|
|
5754
5943
|
};
|
|
5755
5944
|
}
|
|
5756
5945
|
|
|
5946
|
+
// src/codebase-index/circuit-breaker.ts
|
|
5947
|
+
var CircuitOpenError = class extends Error {
|
|
5948
|
+
name = "CircuitOpenError";
|
|
5949
|
+
};
|
|
5950
|
+
var IndexTimeoutError = class extends Error {
|
|
5951
|
+
name = "IndexTimeoutError";
|
|
5952
|
+
};
|
|
5953
|
+
var LockError = class extends Error {
|
|
5954
|
+
name = "LockError";
|
|
5955
|
+
};
|
|
5956
|
+
var IndexCircuitBreaker = class {
|
|
5957
|
+
failureThreshold;
|
|
5958
|
+
cooldownMs;
|
|
5959
|
+
now;
|
|
5960
|
+
state = "closed";
|
|
5961
|
+
consecutiveFailures = 0;
|
|
5962
|
+
openedAt = 0;
|
|
5963
|
+
lastFailure = null;
|
|
5964
|
+
probeInFlight = false;
|
|
5965
|
+
constructor(opts = {}) {
|
|
5966
|
+
this.failureThreshold = opts.failureThreshold ?? 3;
|
|
5967
|
+
this.cooldownMs = opts.cooldownMs ?? 6e4;
|
|
5968
|
+
this.now = opts.now ?? Date.now;
|
|
5969
|
+
}
|
|
5970
|
+
/**
|
|
5971
|
+
* True when a run may proceed. An open circuit transitions to half-open once
|
|
5972
|
+
* the cooldown has elapsed, admitting exactly one probe; further requests
|
|
5973
|
+
* are rejected until that probe settles via recordSuccess/recordFailure.
|
|
5974
|
+
*/
|
|
5975
|
+
allowRequest() {
|
|
5976
|
+
if (this.state === "closed") return true;
|
|
5977
|
+
if (this.state === "open") {
|
|
5978
|
+
if (this.now() - this.openedAt < this.cooldownMs) return false;
|
|
5979
|
+
this.state = "half-open";
|
|
5980
|
+
this.probeInFlight = true;
|
|
5981
|
+
return true;
|
|
5982
|
+
}
|
|
5983
|
+
if (this.probeInFlight) return false;
|
|
5984
|
+
this.probeInFlight = true;
|
|
5985
|
+
return true;
|
|
5986
|
+
}
|
|
5987
|
+
recordSuccess() {
|
|
5988
|
+
this.state = "closed";
|
|
5989
|
+
this.consecutiveFailures = 0;
|
|
5990
|
+
this.lastFailure = null;
|
|
5991
|
+
this.probeInFlight = false;
|
|
5992
|
+
}
|
|
5993
|
+
recordFailure(err) {
|
|
5994
|
+
if (err instanceof LockError) {
|
|
5995
|
+
this.lastFailure = `[transient/lock] ${err.message}`;
|
|
5996
|
+
this.probeInFlight = false;
|
|
5997
|
+
return;
|
|
5998
|
+
}
|
|
5999
|
+
this.lastFailure = err instanceof Error ? err.message : String(err);
|
|
6000
|
+
this.probeInFlight = false;
|
|
6001
|
+
this.consecutiveFailures++;
|
|
6002
|
+
if (this.state === "half-open" || this.consecutiveFailures >= this.failureThreshold) {
|
|
6003
|
+
this.state = "open";
|
|
6004
|
+
this.openedAt = this.now();
|
|
6005
|
+
}
|
|
6006
|
+
}
|
|
6007
|
+
/** Force-close the circuit (manual recovery: `/codebase-reindex`). */
|
|
6008
|
+
reset() {
|
|
6009
|
+
this.state = "closed";
|
|
6010
|
+
this.consecutiveFailures = 0;
|
|
6011
|
+
this.lastFailure = null;
|
|
6012
|
+
this.probeInFlight = false;
|
|
6013
|
+
this.openedAt = 0;
|
|
6014
|
+
}
|
|
6015
|
+
snapshot() {
|
|
6016
|
+
return {
|
|
6017
|
+
state: this.state,
|
|
6018
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
6019
|
+
lastFailure: this.lastFailure,
|
|
6020
|
+
cooldownRemainingMs: this.state === "open" ? Math.max(0, this.cooldownMs - (this.now() - this.openedAt)) : 0
|
|
6021
|
+
};
|
|
6022
|
+
}
|
|
6023
|
+
};
|
|
6024
|
+
var indexCircuitBreaker = new IndexCircuitBreaker();
|
|
6025
|
+
function resetIndexCircuitBreaker() {
|
|
6026
|
+
indexCircuitBreaker.reset();
|
|
6027
|
+
}
|
|
6028
|
+
|
|
5757
6029
|
// src/codebase-index/schema.ts
|
|
5758
|
-
var SCHEMA_VERSION =
|
|
6030
|
+
var SCHEMA_VERSION = 2;
|
|
5759
6031
|
|
|
5760
6032
|
// src/codebase-index/lsp-kind.ts
|
|
5761
6033
|
function lspKindToInternalKind(k) {
|
|
@@ -5790,6 +6062,94 @@ function lspKindToInternalKind(k) {
|
|
|
5790
6062
|
}
|
|
5791
6063
|
}
|
|
5792
6064
|
|
|
6065
|
+
// src/codebase-index/bm25.ts
|
|
6066
|
+
var K1 = 1.5;
|
|
6067
|
+
var B = 0.75;
|
|
6068
|
+
function tokenise(text) {
|
|
6069
|
+
const sanitised = text.replace(/[^\p{L}\p{N}$'_]/gu, " ").replace(/_/g, " ");
|
|
6070
|
+
return sanitised.toLowerCase().split(" ").filter(Boolean);
|
|
6071
|
+
}
|
|
6072
|
+
function splitName(name) {
|
|
6073
|
+
return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
6074
|
+
}
|
|
6075
|
+
function buildIndexableText(name, signature, docComment) {
|
|
6076
|
+
return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
|
|
6077
|
+
}
|
|
6078
|
+
function buildBm25Index(docs) {
|
|
6079
|
+
const documents = docs.map((d) => {
|
|
6080
|
+
const tokens = tokenise(d.text);
|
|
6081
|
+
return { id: d.id, tokens, raw: d.text, len: tokens.length };
|
|
6082
|
+
});
|
|
6083
|
+
const df = {};
|
|
6084
|
+
for (const doc of documents) {
|
|
6085
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6086
|
+
for (const t of doc.tokens) {
|
|
6087
|
+
if (!seen.has(t)) {
|
|
6088
|
+
df[t] = (df[t] ?? 0) + 1;
|
|
6089
|
+
seen.add(t);
|
|
6090
|
+
}
|
|
6091
|
+
}
|
|
6092
|
+
}
|
|
6093
|
+
const N = documents.length;
|
|
6094
|
+
const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
|
|
6095
|
+
const avgLen = N === 0 ? 0 : totalLen / N;
|
|
6096
|
+
return new Bm25Index(documents, df, N, avgLen);
|
|
6097
|
+
}
|
|
6098
|
+
var Bm25Index = class {
|
|
6099
|
+
constructor(documents, df, N, avgLen) {
|
|
6100
|
+
this.documents = documents;
|
|
6101
|
+
this.df = df;
|
|
6102
|
+
this.N = N;
|
|
6103
|
+
this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
|
|
6104
|
+
}
|
|
6105
|
+
documents;
|
|
6106
|
+
df;
|
|
6107
|
+
N;
|
|
6108
|
+
safeAvgLen;
|
|
6109
|
+
score(query2, filter) {
|
|
6110
|
+
const qTokens = tokenise(query2);
|
|
6111
|
+
if (qTokens.length === 0) return [];
|
|
6112
|
+
const results = [];
|
|
6113
|
+
for (const doc of this.documents) {
|
|
6114
|
+
if (filter && !filter(doc.id)) continue;
|
|
6115
|
+
let docScore = 0;
|
|
6116
|
+
for (const qTerm of qTokens) {
|
|
6117
|
+
let tf = 0;
|
|
6118
|
+
for (const t of doc.tokens) {
|
|
6119
|
+
if (t === qTerm) tf++;
|
|
6120
|
+
}
|
|
6121
|
+
if (tf === 0) continue;
|
|
6122
|
+
const dfVal = this.df[qTerm] ?? 0;
|
|
6123
|
+
if (dfVal === 0) continue;
|
|
6124
|
+
const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
|
|
6125
|
+
const lenRatio = B * (doc.len / this.safeAvgLen);
|
|
6126
|
+
const tfComponent = tf * (K1 + 1) / (tf + K1 * (1 - B + lenRatio));
|
|
6127
|
+
docScore += idf * tfComponent;
|
|
6128
|
+
}
|
|
6129
|
+
if (docScore > 0) results.push({ id: doc.id, score: docScore });
|
|
6130
|
+
}
|
|
6131
|
+
return results;
|
|
6132
|
+
}
|
|
6133
|
+
getDoc(id) {
|
|
6134
|
+
return this.documents.find((d) => d.id === id);
|
|
6135
|
+
}
|
|
6136
|
+
extractSnippet(docId, queryTokens, radius = 40) {
|
|
6137
|
+
const doc = this.getDoc(docId);
|
|
6138
|
+
if (!doc) return "";
|
|
6139
|
+
for (const tok of queryTokens) {
|
|
6140
|
+
const idx = doc.raw.toLowerCase().indexOf(tok);
|
|
6141
|
+
if (idx !== -1) {
|
|
6142
|
+
const start = Math.max(0, idx - radius);
|
|
6143
|
+
const end = Math.min(doc.raw.length, idx + tok.length + radius);
|
|
6144
|
+
const excerpt = doc.raw.slice(start, end);
|
|
6145
|
+
const ellipsis = "\u2026";
|
|
6146
|
+
return (start > 0 ? ellipsis : "") + excerpt + (end < doc.raw.length ? ellipsis : "");
|
|
6147
|
+
}
|
|
6148
|
+
}
|
|
6149
|
+
return doc.raw.slice(0, radius * 2) + (doc.raw.length > radius * 2 ? "\u2026" : "");
|
|
6150
|
+
}
|
|
6151
|
+
};
|
|
6152
|
+
|
|
5793
6153
|
// src/codebase-index/writer.ts
|
|
5794
6154
|
var DB_FILE = "index.db";
|
|
5795
6155
|
function resolveIndexDir(projectRoot, override) {
|
|
@@ -5825,15 +6185,79 @@ function loadDatabaseSync() {
|
|
|
5825
6185
|
}
|
|
5826
6186
|
return DatabaseSyncCtor;
|
|
5827
6187
|
}
|
|
6188
|
+
var MAX_LOCK_RETRIES = 3;
|
|
6189
|
+
var LOCK_RETRY_BASE_DELAY_MS = 50;
|
|
6190
|
+
var LOCK_RETRY_MAX_DELAY_MS = 500;
|
|
6191
|
+
function isLockError(err) {
|
|
6192
|
+
if (!(err instanceof Error)) return false;
|
|
6193
|
+
const e = err;
|
|
6194
|
+
const code = e.code ?? e.sqliteCode;
|
|
6195
|
+
if (typeof code === "string" && /SQLITE_(BUSY|LOCKED)/.test(code)) return true;
|
|
6196
|
+
if (typeof code === "number" && (code === 5 || code === 6)) return true;
|
|
6197
|
+
if (/SQLITE_(BUSY|LOCKED)/.test(err.message)) return true;
|
|
6198
|
+
return false;
|
|
6199
|
+
}
|
|
6200
|
+
function sleepSync(ms) {
|
|
6201
|
+
try {
|
|
6202
|
+
const sab = new SharedArrayBuffer(4);
|
|
6203
|
+
const view = new Int32Array(sab);
|
|
6204
|
+
Atomics.wait(view, 0, 0, ms);
|
|
6205
|
+
} catch {
|
|
6206
|
+
}
|
|
6207
|
+
}
|
|
5828
6208
|
var IndexStore = class {
|
|
5829
6209
|
db;
|
|
5830
6210
|
/** Absolute path to this project's index directory. */
|
|
5831
6211
|
indexDir;
|
|
6212
|
+
/**
|
|
6213
|
+
* True when the SQLite build provides FTS5 (Node's bundled SQLite does).
|
|
6214
|
+
* When false, ranked search falls back to the LIKE + in-process BM25 path.
|
|
6215
|
+
*/
|
|
6216
|
+
ftsAvailable = false;
|
|
6217
|
+
/**
|
|
6218
|
+
* Execute a SQLite write operation with automatic retry on lock conflicts.
|
|
6219
|
+
*
|
|
6220
|
+
* When another wstack process is holding the write lock the statement first
|
|
6221
|
+
* waits up to `busy_timeout` ms, then throws SQLITE_BUSY. This wrapper catches
|
|
6222
|
+
* that error and retries (up to MAX_LOCK_RETRIES) with exponential backoff,
|
|
6223
|
+
* giving the competing writer time to finish and release the lock.
|
|
6224
|
+
*
|
|
6225
|
+
* @param fn The write operation to execute. Can return a value which is
|
|
6226
|
+
* returned to the caller on success.
|
|
6227
|
+
* @throws {@link LockError} when all retries are exhausted on a lock conflict
|
|
6228
|
+
* (non-lock errors always propagate on the first attempt).
|
|
6229
|
+
*/
|
|
6230
|
+
runWithRetry(fn) {
|
|
6231
|
+
let lastError;
|
|
6232
|
+
for (let attempt = 0; attempt <= MAX_LOCK_RETRIES; attempt++) {
|
|
6233
|
+
try {
|
|
6234
|
+
return fn();
|
|
6235
|
+
} catch (err) {
|
|
6236
|
+
lastError = err;
|
|
6237
|
+
if (!isLockError(err)) throw err;
|
|
6238
|
+
if (attempt === MAX_LOCK_RETRIES) {
|
|
6239
|
+
const msg = lastError instanceof Error ? lastError.message : String(lastError);
|
|
6240
|
+
throw new LockError(`SQLite lock conflict after ${MAX_LOCK_RETRIES} retries: ${msg}`);
|
|
6241
|
+
}
|
|
6242
|
+
const delay = Math.min(
|
|
6243
|
+
LOCK_RETRY_BASE_DELAY_MS * Math.pow(2, attempt),
|
|
6244
|
+
LOCK_RETRY_MAX_DELAY_MS
|
|
6245
|
+
);
|
|
6246
|
+
sleepSync(delay);
|
|
6247
|
+
}
|
|
6248
|
+
}
|
|
6249
|
+
throw lastError;
|
|
6250
|
+
}
|
|
5832
6251
|
constructor(projectRoot, opts = {}) {
|
|
5833
6252
|
this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
|
|
5834
6253
|
fs7.mkdirSync(this.indexDir, { recursive: true });
|
|
5835
6254
|
const Database = loadDatabaseSync();
|
|
5836
6255
|
this.db = new Database(path.join(this.indexDir, DB_FILE));
|
|
6256
|
+
try {
|
|
6257
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
6258
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
6259
|
+
} catch {
|
|
6260
|
+
}
|
|
5837
6261
|
this.initSchema();
|
|
5838
6262
|
}
|
|
5839
6263
|
initSchema() {
|
|
@@ -5842,6 +6266,21 @@ var IndexStore = class {
|
|
|
5842
6266
|
key TEXT PRIMARY KEY,
|
|
5843
6267
|
value TEXT NOT NULL
|
|
5844
6268
|
);
|
|
6269
|
+
`);
|
|
6270
|
+
const storedRows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all("version");
|
|
6271
|
+
const storedVersion = storedRows.length ? Number(storedRows[0]?.value) : null;
|
|
6272
|
+
if (storedVersion !== null && storedVersion !== SCHEMA_VERSION) {
|
|
6273
|
+
this.db.exec(`
|
|
6274
|
+
DROP TABLE IF EXISTS symbols;
|
|
6275
|
+
DROP TABLE IF EXISTS files;
|
|
6276
|
+
DROP TABLE IF EXISTS refs;
|
|
6277
|
+
`);
|
|
6278
|
+
this.db.exec("DROP TABLE IF EXISTS symbols_fts");
|
|
6279
|
+
this.db.prepare("UPDATE metadata SET value = ? WHERE key = ?").run(String(SCHEMA_VERSION), "version");
|
|
6280
|
+
} else if (storedVersion === null) {
|
|
6281
|
+
this.db.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
|
|
6282
|
+
}
|
|
6283
|
+
this.db.exec(`
|
|
5845
6284
|
CREATE TABLE IF NOT EXISTS files (
|
|
5846
6285
|
file TEXT PRIMARY KEY,
|
|
5847
6286
|
lang TEXT NOT NULL,
|
|
@@ -5882,53 +6321,76 @@ var IndexStore = class {
|
|
|
5882
6321
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)");
|
|
5883
6322
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)");
|
|
5884
6323
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)");
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
this.
|
|
6324
|
+
try {
|
|
6325
|
+
this.db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')");
|
|
6326
|
+
this.ftsAvailable = true;
|
|
6327
|
+
} catch {
|
|
6328
|
+
this.ftsAvailable = false;
|
|
5888
6329
|
}
|
|
5889
6330
|
}
|
|
5890
6331
|
// ─── Symbol CRUD ─────────────────────────────────────────────────────────────
|
|
5891
6332
|
insertSymbols(symbols, nextId) {
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
let id = nextId;
|
|
5897
|
-
for (const s of symbols) {
|
|
5898
|
-
stmt.run(
|
|
5899
|
-
id++,
|
|
5900
|
-
s.lang,
|
|
5901
|
-
s.kind,
|
|
5902
|
-
s.name,
|
|
5903
|
-
s.file,
|
|
5904
|
-
s.line,
|
|
5905
|
-
s.col,
|
|
5906
|
-
s.signature,
|
|
5907
|
-
s.docComment,
|
|
5908
|
-
s.scope,
|
|
5909
|
-
s.text,
|
|
5910
|
-
s.file
|
|
6333
|
+
return this.runWithRetry(() => {
|
|
6334
|
+
const stmt = this.db.prepare(
|
|
6335
|
+
`INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
|
|
6336
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
5911
6337
|
);
|
|
5912
|
-
|
|
5913
|
-
|
|
6338
|
+
const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
|
|
6339
|
+
let id = nextId;
|
|
6340
|
+
for (const s of symbols) {
|
|
6341
|
+
stmt.run(
|
|
6342
|
+
id,
|
|
6343
|
+
s.lang,
|
|
6344
|
+
s.kind,
|
|
6345
|
+
s.name,
|
|
6346
|
+
s.file,
|
|
6347
|
+
s.line,
|
|
6348
|
+
s.col,
|
|
6349
|
+
s.signature,
|
|
6350
|
+
s.docComment,
|
|
6351
|
+
s.scope,
|
|
6352
|
+
s.text,
|
|
6353
|
+
s.file
|
|
6354
|
+
);
|
|
6355
|
+
ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
|
|
6356
|
+
id++;
|
|
6357
|
+
}
|
|
6358
|
+
return id;
|
|
6359
|
+
});
|
|
5914
6360
|
}
|
|
5915
6361
|
deleteSymbolsForFile(file) {
|
|
5916
|
-
this.
|
|
6362
|
+
this.runWithRetry(() => {
|
|
6363
|
+
if (this.ftsAvailable) {
|
|
6364
|
+
this.db.prepare("DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
|
|
6365
|
+
}
|
|
6366
|
+
this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
|
|
6367
|
+
});
|
|
5917
6368
|
}
|
|
6369
|
+
/**
|
|
6370
|
+
* Remove every trace of a file (refs, symbols, FTS rows, file meta). Used
|
|
6371
|
+
* when a source file disappears between index runs — previously this only
|
|
6372
|
+
* dropped the `files` row, leaving its symbols orphaned but still searchable.
|
|
6373
|
+
*/
|
|
5918
6374
|
deleteFile(file) {
|
|
5919
|
-
this.
|
|
6375
|
+
this.runWithRetry(() => {
|
|
6376
|
+
this.deleteRefsForFile(file);
|
|
6377
|
+
this.deleteSymbolsForFile(file);
|
|
6378
|
+
this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
|
|
6379
|
+
});
|
|
5920
6380
|
}
|
|
5921
6381
|
// ─── File metadata ──────────────────────────────────────────────────────────
|
|
5922
6382
|
upsertFile(meta) {
|
|
5923
|
-
this.
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
6383
|
+
this.runWithRetry(() => {
|
|
6384
|
+
this.db.prepare(
|
|
6385
|
+
`INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
|
|
6386
|
+
VALUES (?, ?, ?, ?, ?)
|
|
6387
|
+
ON CONFLICT(file) DO UPDATE SET
|
|
6388
|
+
lang = excluded.lang,
|
|
6389
|
+
mtime_ms = excluded.mtime_ms,
|
|
6390
|
+
symbol_count = excluded.symbol_count,
|
|
6391
|
+
last_indexed = excluded.last_indexed`
|
|
6392
|
+
).run(meta.file, meta.lang, meta.mtimeMs, meta.symbolCount, meta.lastIndexed);
|
|
6393
|
+
});
|
|
5932
6394
|
}
|
|
5933
6395
|
getFileMeta(file) {
|
|
5934
6396
|
const rows = this.db.prepare(
|
|
@@ -5995,15 +6457,103 @@ var IndexStore = class {
|
|
|
5995
6457
|
lspKind: filter?.lspKind
|
|
5996
6458
|
}));
|
|
5997
6459
|
}
|
|
5998
|
-
getAllIndexable() {
|
|
5999
|
-
return this.db.prepare("SELECT id, text FROM symbols").all().map(
|
|
6000
|
-
({ id, text }) => ({ id, text })
|
|
6001
|
-
);
|
|
6002
|
-
}
|
|
6003
6460
|
/**
|
|
6004
|
-
*
|
|
6005
|
-
*
|
|
6006
|
-
*
|
|
6461
|
+
* Ranked search — the one-stop query the codebase-search tool and plug-lsp
|
|
6462
|
+
* use. With FTS5 this is a single indexed `MATCH` ranked by SQLite's native
|
|
6463
|
+
* `bm25()` with a built-in `snippet()`; without FTS5 it falls back to the
|
|
6464
|
+
* legacy LIKE scan + in-process BM25 (identical semantics, slower).
|
|
6465
|
+
*
|
|
6466
|
+
* Tokens are matched as prefixes (`"tok"*`), mirroring the old
|
|
6467
|
+
* `LIKE '%tok%'` recall for the common symbol-search shapes ("user" finds
|
|
6468
|
+
* "users", camelCase-split text makes "complex" find "complexOperation").
|
|
6469
|
+
*/
|
|
6470
|
+
searchRanked(query2, filter, limit) {
|
|
6471
|
+
const tokens = tokenise(query2);
|
|
6472
|
+
if (tokens.length === 0 || !this.ftsAvailable) {
|
|
6473
|
+
return this.searchRankedFallback(query2, filter, limit);
|
|
6474
|
+
}
|
|
6475
|
+
let effectiveKind = filter?.kind;
|
|
6476
|
+
if (filter?.lspKind !== void 0) {
|
|
6477
|
+
const mapped = lspKindToInternalKind(filter.lspKind);
|
|
6478
|
+
if (mapped === null) return { results: [], total: 0 };
|
|
6479
|
+
effectiveKind = mapped;
|
|
6480
|
+
}
|
|
6481
|
+
const match = tokens.map((t) => `"${t.replaceAll('"', "")}"*`).join(" OR ");
|
|
6482
|
+
const conditions = ["symbols_fts MATCH ?"];
|
|
6483
|
+
const values = [match];
|
|
6484
|
+
if (effectiveKind) {
|
|
6485
|
+
conditions.push("s.kind = ?");
|
|
6486
|
+
values.push(effectiveKind);
|
|
6487
|
+
}
|
|
6488
|
+
if (filter?.lang) {
|
|
6489
|
+
conditions.push("s.lang = ?");
|
|
6490
|
+
values.push(filter.lang);
|
|
6491
|
+
}
|
|
6492
|
+
if (filter?.file) {
|
|
6493
|
+
conditions.push("s.file LIKE ?");
|
|
6494
|
+
values.push(`%${filter.file}%`);
|
|
6495
|
+
}
|
|
6496
|
+
const where = conditions.join(" AND ");
|
|
6497
|
+
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);
|
|
6498
|
+
const total = countRows[0] ? Number(countRows[0].n) : 0;
|
|
6499
|
+
if (total === 0) return { results: [], total: 0 };
|
|
6500
|
+
const rows = this.db.prepare(
|
|
6501
|
+
`SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
|
|
6502
|
+
-bm25(symbols_fts) AS score,
|
|
6503
|
+
snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
|
|
6504
|
+
FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
|
|
6505
|
+
WHERE ${where}
|
|
6506
|
+
ORDER BY bm25(symbols_fts)
|
|
6507
|
+
LIMIT ?`
|
|
6508
|
+
).all(...values, limit);
|
|
6509
|
+
return {
|
|
6510
|
+
results: rows.map((r) => ({
|
|
6511
|
+
id: r.id,
|
|
6512
|
+
lang: r.lang,
|
|
6513
|
+
kind: r.kind,
|
|
6514
|
+
name: r.name,
|
|
6515
|
+
file: r.file,
|
|
6516
|
+
line: r.line,
|
|
6517
|
+
col: r.col,
|
|
6518
|
+
signature: r.signature,
|
|
6519
|
+
docComment: r.doc_comment,
|
|
6520
|
+
// bm25() is negative-is-better; negate so callers keep "higher is
|
|
6521
|
+
// better" and clamp so a match never reports a zero score.
|
|
6522
|
+
score: Math.max(1e-4, r.score),
|
|
6523
|
+
snippet: r.snippet,
|
|
6524
|
+
lspKind: filter?.lspKind
|
|
6525
|
+
})),
|
|
6526
|
+
total
|
|
6527
|
+
};
|
|
6528
|
+
}
|
|
6529
|
+
/** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
|
|
6530
|
+
searchRankedFallback(query2, filter, limit) {
|
|
6531
|
+
const candidates = this.search(query2, filter);
|
|
6532
|
+
if (candidates.length === 0) return { results: [], total: 0 };
|
|
6533
|
+
if (!query2.trim()) {
|
|
6534
|
+
return { results: candidates.slice(0, limit), total: candidates.length };
|
|
6535
|
+
}
|
|
6536
|
+
const bm25 = buildBm25Index(
|
|
6537
|
+
candidates.map((c) => ({ id: c.id, text: buildIndexableText(c.name, c.signature, c.docComment) }))
|
|
6538
|
+
);
|
|
6539
|
+
const scored = bm25.score(query2, (id) => candidates.some((c) => c.id === id));
|
|
6540
|
+
scored.sort((a, b) => b.score - a.score);
|
|
6541
|
+
const qTokens = tokenise(query2);
|
|
6542
|
+
const results = scored.slice(0, limit).map(({ id, score }) => {
|
|
6543
|
+
const c = expectDefined(candidates.find((cand) => cand.id === id));
|
|
6544
|
+
return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
|
|
6545
|
+
});
|
|
6546
|
+
return { results, total: candidates.length };
|
|
6547
|
+
}
|
|
6548
|
+
getAllIndexable() {
|
|
6549
|
+
return this.db.prepare("SELECT id, text FROM symbols").all().map(
|
|
6550
|
+
({ id, text }) => ({ id, text })
|
|
6551
|
+
);
|
|
6552
|
+
}
|
|
6553
|
+
/**
|
|
6554
|
+
* Largest symbol id currently in the table (0 when empty). New ids must be
|
|
6555
|
+
* allocated from this, NOT from `COUNT(*)`: incremental reindexes delete a
|
|
6556
|
+
* changed file's rows, so the row count drops below the max id and a
|
|
6007
6557
|
* count-based id would collide with a surviving row (UNIQUE constraint on
|
|
6008
6558
|
* `symbols.id`). Ids may have gaps — that is fine.
|
|
6009
6559
|
*/
|
|
@@ -6044,14 +6594,19 @@ var IndexStore = class {
|
|
|
6044
6594
|
};
|
|
6045
6595
|
}
|
|
6046
6596
|
setLastIndexed(ts2) {
|
|
6047
|
-
this.
|
|
6048
|
-
|
|
6049
|
-
|
|
6597
|
+
this.runWithRetry(() => {
|
|
6598
|
+
this.db.prepare(
|
|
6599
|
+
"INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)"
|
|
6600
|
+
).run(String(ts2));
|
|
6601
|
+
});
|
|
6050
6602
|
}
|
|
6051
6603
|
clearAll() {
|
|
6052
|
-
this.
|
|
6053
|
-
|
|
6054
|
-
|
|
6604
|
+
this.runWithRetry(() => {
|
|
6605
|
+
this.db.exec("DELETE FROM symbols");
|
|
6606
|
+
this.db.exec("DELETE FROM files");
|
|
6607
|
+
this.db.exec("DELETE FROM refs");
|
|
6608
|
+
if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
|
|
6609
|
+
});
|
|
6055
6610
|
}
|
|
6056
6611
|
// ─── Ref CRUD ────────────────────────────────────────────────────────────────
|
|
6057
6612
|
/**
|
|
@@ -6059,46 +6614,52 @@ var IndexStore = class {
|
|
|
6059
6614
|
* Replaces any existing refs from the same source (idempotent on re-index).
|
|
6060
6615
|
*/
|
|
6061
6616
|
insertRefs(fromId, refs) {
|
|
6062
|
-
this.
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6617
|
+
this.runWithRetry(() => {
|
|
6618
|
+
this.db.prepare("DELETE FROM refs WHERE from_id = ?").run(fromId);
|
|
6619
|
+
if (refs.length === 0) return;
|
|
6620
|
+
const stmt = this.db.prepare(
|
|
6621
|
+
`INSERT INTO refs(from_id, to_name, to_id, call_type, line)
|
|
6622
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
6623
|
+
);
|
|
6624
|
+
for (const ref of refs) {
|
|
6625
|
+
stmt.run(fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
|
|
6626
|
+
}
|
|
6627
|
+
});
|
|
6071
6628
|
}
|
|
6072
6629
|
/**
|
|
6073
6630
|
* Delete all refs whose source symbols are in a given file.
|
|
6074
6631
|
* Used when re-indexing a file to clear stale refs.
|
|
6075
6632
|
*/
|
|
6076
6633
|
deleteRefsForFile(file) {
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6634
|
+
this.runWithRetry(() => {
|
|
6635
|
+
const ids = this.db.prepare(
|
|
6636
|
+
"SELECT id FROM symbols WHERE file = ?"
|
|
6637
|
+
).all(file);
|
|
6638
|
+
if (!ids.length) return;
|
|
6639
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
6640
|
+
this.db.prepare(`DELETE FROM refs WHERE from_id IN (${placeholders})`).run(...ids.map((r) => r.id));
|
|
6641
|
+
});
|
|
6083
6642
|
}
|
|
6084
6643
|
/**
|
|
6085
6644
|
* Resolve `to_name` → `to_id` for all refs that have a name but no id.
|
|
6086
6645
|
* Call this after all symbols have been inserted to fill in cross-references.
|
|
6087
6646
|
*/
|
|
6088
6647
|
resolveRefs() {
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6648
|
+
return this.runWithRetry(() => {
|
|
6649
|
+
const unresolved = this.db.prepare(
|
|
6650
|
+
"SELECT id, to_name FROM refs WHERE to_id IS NULL AND to_name IS NOT NULL"
|
|
6651
|
+
).all();
|
|
6652
|
+
let resolved = 0;
|
|
6653
|
+
for (const row of unresolved) {
|
|
6654
|
+
const target = this.db.prepare("SELECT id FROM symbols WHERE name = ? LIMIT 1").all(row.to_name);
|
|
6655
|
+
const first = target[0];
|
|
6656
|
+
if (first) {
|
|
6657
|
+
this.db.prepare("UPDATE refs SET to_id = ? WHERE id = ?").run(first.id, row.id);
|
|
6658
|
+
resolved++;
|
|
6659
|
+
}
|
|
6099
6660
|
}
|
|
6100
|
-
|
|
6101
|
-
|
|
6661
|
+
return resolved;
|
|
6662
|
+
});
|
|
6102
6663
|
}
|
|
6103
6664
|
/**
|
|
6104
6665
|
* Find all references TO a given symbol (who calls / uses this symbol?).
|
|
@@ -6859,7 +7420,7 @@ function parseSymbols4(opts) {
|
|
|
6859
7420
|
}
|
|
6860
7421
|
function checkNativeParser() {
|
|
6861
7422
|
try {
|
|
6862
|
-
execFileSync("rustc", ["--version"], { stdio: "pipe" });
|
|
7423
|
+
execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
|
|
6863
7424
|
const toolsDir = path.join(process.cwd(), "tools");
|
|
6864
7425
|
try {
|
|
6865
7426
|
execFileSync(
|
|
@@ -6872,7 +7433,7 @@ function checkNativeParser() {
|
|
|
6872
7433
|
"--manifest-path",
|
|
6873
7434
|
path.join(toolsDir, "Cargo.toml")
|
|
6874
7435
|
],
|
|
6875
|
-
{ stdio: "pipe" }
|
|
7436
|
+
{ stdio: "pipe", windowsHide: true }
|
|
6876
7437
|
);
|
|
6877
7438
|
return true;
|
|
6878
7439
|
} catch {
|
|
@@ -6895,7 +7456,8 @@ function tryNativeParse(file, content) {
|
|
|
6895
7456
|
cwd: process.cwd(),
|
|
6896
7457
|
encoding: "utf8",
|
|
6897
7458
|
timeout: 15e3,
|
|
6898
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
7459
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
7460
|
+
windowsHide: true
|
|
6899
7461
|
}
|
|
6900
7462
|
);
|
|
6901
7463
|
if (result.status === 0 && result.stdout) {
|
|
@@ -7309,10 +7871,6 @@ function isScalar(value) {
|
|
|
7309
7871
|
if (/^'[^']*'$/.test(value) || /^"[^"]*"$/.test(value)) return true;
|
|
7310
7872
|
return false;
|
|
7311
7873
|
}
|
|
7312
|
-
function truncate(s, max) {
|
|
7313
|
-
if (s.length <= max) return s;
|
|
7314
|
-
return s.slice(0, max) + "...";
|
|
7315
|
-
}
|
|
7316
7874
|
function makeSymbol2(opts) {
|
|
7317
7875
|
return {
|
|
7318
7876
|
id: 0,
|
|
@@ -7379,126 +7937,6 @@ async function loadGitignoreMatcher(projectRoot) {
|
|
|
7379
7937
|
return compileGitignore(lines);
|
|
7380
7938
|
}
|
|
7381
7939
|
|
|
7382
|
-
// src/codebase-index/background-indexer.ts
|
|
7383
|
-
var _ready = false;
|
|
7384
|
-
var _indexing = false;
|
|
7385
|
-
var _currentFile = 0;
|
|
7386
|
-
var _totalFiles = 0;
|
|
7387
|
-
var _lastError = null;
|
|
7388
|
-
function isIndexReady() {
|
|
7389
|
-
return _ready;
|
|
7390
|
-
}
|
|
7391
|
-
function setIndexReady() {
|
|
7392
|
-
_ready = true;
|
|
7393
|
-
}
|
|
7394
|
-
function isIndexing() {
|
|
7395
|
-
return _indexing;
|
|
7396
|
-
}
|
|
7397
|
-
function getIndexState() {
|
|
7398
|
-
return {
|
|
7399
|
-
ready: _ready,
|
|
7400
|
-
indexing: _indexing,
|
|
7401
|
-
currentFile: _currentFile,
|
|
7402
|
-
totalFiles: _totalFiles,
|
|
7403
|
-
lastError: _lastError
|
|
7404
|
-
};
|
|
7405
|
-
}
|
|
7406
|
-
var _listeners = [];
|
|
7407
|
-
function onIndexStateChange(listener) {
|
|
7408
|
-
_listeners.push(listener);
|
|
7409
|
-
return () => {
|
|
7410
|
-
_listeners = _listeners.filter((l) => l !== listener);
|
|
7411
|
-
};
|
|
7412
|
-
}
|
|
7413
|
-
function emitState() {
|
|
7414
|
-
const state = getIndexState();
|
|
7415
|
-
for (const l of _listeners) l(state);
|
|
7416
|
-
}
|
|
7417
|
-
function _setIndexProgress(current, total) {
|
|
7418
|
-
_currentFile = current;
|
|
7419
|
-
_totalFiles = total;
|
|
7420
|
-
emitState();
|
|
7421
|
-
}
|
|
7422
|
-
function stubCtx(projectRoot) {
|
|
7423
|
-
return {
|
|
7424
|
-
projectRoot,
|
|
7425
|
-
cwd: projectRoot,
|
|
7426
|
-
messages: [],
|
|
7427
|
-
todos: [],
|
|
7428
|
-
readFiles: /* @__PURE__ */ new Set(),
|
|
7429
|
-
fileMtimes: /* @__PURE__ */ new Map()
|
|
7430
|
-
};
|
|
7431
|
-
}
|
|
7432
|
-
var chain = Promise.resolve();
|
|
7433
|
-
function withMutex(job) {
|
|
7434
|
-
const run = chain.then(job, job);
|
|
7435
|
-
chain = run.then(
|
|
7436
|
-
() => void 0,
|
|
7437
|
-
() => void 0
|
|
7438
|
-
);
|
|
7439
|
-
return run;
|
|
7440
|
-
}
|
|
7441
|
-
var DEFAULT_DEBOUNCE_MS = 400;
|
|
7442
|
-
var debounceTimers = /* @__PURE__ */ new Map();
|
|
7443
|
-
function debounceKey(indexDir, file) {
|
|
7444
|
-
return `${indexDir ?? ""}|${file}`;
|
|
7445
|
-
}
|
|
7446
|
-
function isIndexableFile(filePath) {
|
|
7447
|
-
return detectLang(filePath) !== null;
|
|
7448
|
-
}
|
|
7449
|
-
async function runStartupIndex(opts) {
|
|
7450
|
-
_indexing = true;
|
|
7451
|
-
emitState();
|
|
7452
|
-
try {
|
|
7453
|
-
const result = await withMutex(() => {
|
|
7454
|
-
_currentFile = 0;
|
|
7455
|
-
_totalFiles = 0;
|
|
7456
|
-
_lastError = null;
|
|
7457
|
-
return runIndexer(stubCtx(opts.projectRoot), {
|
|
7458
|
-
projectRoot: opts.projectRoot,
|
|
7459
|
-
indexDir: opts.indexDir,
|
|
7460
|
-
force: opts.force,
|
|
7461
|
-
signal: opts.signal
|
|
7462
|
-
});
|
|
7463
|
-
});
|
|
7464
|
-
_ready = true;
|
|
7465
|
-
return result;
|
|
7466
|
-
} catch (err) {
|
|
7467
|
-
_lastError = err instanceof Error ? err.message : String(err);
|
|
7468
|
-
_ready = true;
|
|
7469
|
-
throw err;
|
|
7470
|
-
} finally {
|
|
7471
|
-
_indexing = false;
|
|
7472
|
-
emitState();
|
|
7473
|
-
}
|
|
7474
|
-
}
|
|
7475
|
-
function enqueueReindex(opts) {
|
|
7476
|
-
const files = opts.files.filter(isIndexableFile);
|
|
7477
|
-
if (files.length === 0) return;
|
|
7478
|
-
const ms = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
7479
|
-
for (const file of files) {
|
|
7480
|
-
const key = debounceKey(opts.indexDir, file);
|
|
7481
|
-
const existing = debounceTimers.get(key);
|
|
7482
|
-
if (existing) clearTimeout(existing);
|
|
7483
|
-
const timer = setTimeout(() => {
|
|
7484
|
-
debounceTimers.delete(key);
|
|
7485
|
-
void withMutex(
|
|
7486
|
-
() => runIndexer(stubCtx(opts.projectRoot), {
|
|
7487
|
-
projectRoot: opts.projectRoot,
|
|
7488
|
-
files: [file],
|
|
7489
|
-
indexDir: opts.indexDir
|
|
7490
|
-
})
|
|
7491
|
-
).catch((err) => opts.onError?.(err));
|
|
7492
|
-
}, ms);
|
|
7493
|
-
timer.unref?.();
|
|
7494
|
-
debounceTimers.set(key, timer);
|
|
7495
|
-
}
|
|
7496
|
-
}
|
|
7497
|
-
function cancelPendingReindexes() {
|
|
7498
|
-
for (const t of debounceTimers.values()) clearTimeout(t);
|
|
7499
|
-
debounceTimers.clear();
|
|
7500
|
-
}
|
|
7501
|
-
|
|
7502
7940
|
// src/codebase-index/indexer.ts
|
|
7503
7941
|
var YIELD_EVERY_N = 50;
|
|
7504
7942
|
function yieldEventLoop() {
|
|
@@ -7636,25 +8074,25 @@ async function runIndexerWithStore(store, opts) {
|
|
|
7636
8074
|
}
|
|
7637
8075
|
for (let fi = 0; fi < files.length; fi++) {
|
|
7638
8076
|
const file = expectDefined(files[fi]);
|
|
7639
|
-
|
|
8077
|
+
opts.onProgress?.(fi + 1, files.length);
|
|
7640
8078
|
if (fi > 0 && fi % YIELD_EVERY_N === 0) {
|
|
7641
8079
|
await yieldEventLoop();
|
|
7642
8080
|
throwIfAborted(signal);
|
|
7643
8081
|
}
|
|
7644
|
-
let
|
|
8082
|
+
let stat11;
|
|
7645
8083
|
try {
|
|
7646
8084
|
const statOpts = signal ? { signal } : {};
|
|
7647
|
-
|
|
8085
|
+
stat11 = await fs4.stat(file, statOpts);
|
|
7648
8086
|
} catch (e) {
|
|
7649
8087
|
if (isAbortError(e)) throw e;
|
|
7650
8088
|
store.deleteFile(file);
|
|
7651
8089
|
continue;
|
|
7652
8090
|
}
|
|
7653
|
-
if (!
|
|
8091
|
+
if (!stat11.isFile()) continue;
|
|
7654
8092
|
const lang = detectLang(file);
|
|
7655
8093
|
if (!lang) continue;
|
|
7656
8094
|
const meta = existingMeta.get(file);
|
|
7657
|
-
if (!force && meta && meta.mtimeMs === Math.floor(
|
|
8095
|
+
if (!force && meta && meta.mtimeMs === Math.floor(stat11.mtimeMs)) {
|
|
7658
8096
|
langStats[lang] = (langStats[lang] ?? 0) + meta.symbolCount;
|
|
7659
8097
|
symbolsIndexed += meta.symbolCount;
|
|
7660
8098
|
filesIndexed++;
|
|
@@ -7681,7 +8119,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
7681
8119
|
store.upsertFile({
|
|
7682
8120
|
file,
|
|
7683
8121
|
lang,
|
|
7684
|
-
mtimeMs: Math.floor(
|
|
8122
|
+
mtimeMs: Math.floor(stat11.mtimeMs),
|
|
7685
8123
|
symbolCount: 0,
|
|
7686
8124
|
lastIndexed: Date.now()
|
|
7687
8125
|
});
|
|
@@ -7707,7 +8145,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
7707
8145
|
store.upsertFile({
|
|
7708
8146
|
file,
|
|
7709
8147
|
lang,
|
|
7710
|
-
mtimeMs: Math.floor(
|
|
8148
|
+
mtimeMs: Math.floor(stat11.mtimeMs),
|
|
7711
8149
|
symbolCount: count,
|
|
7712
8150
|
lastIndexed: Date.now()
|
|
7713
8151
|
});
|
|
@@ -7731,6 +8169,365 @@ async function runIndexerWithStore(store, opts) {
|
|
|
7731
8169
|
};
|
|
7732
8170
|
}
|
|
7733
8171
|
|
|
8172
|
+
// src/codebase-index/index-service.ts
|
|
8173
|
+
function stubCtx(projectRoot) {
|
|
8174
|
+
return {
|
|
8175
|
+
projectRoot,
|
|
8176
|
+
cwd: projectRoot,
|
|
8177
|
+
messages: [],
|
|
8178
|
+
todos: [],
|
|
8179
|
+
readFiles: /* @__PURE__ */ new Set(),
|
|
8180
|
+
fileMtimes: /* @__PURE__ */ new Map()
|
|
8181
|
+
};
|
|
8182
|
+
}
|
|
8183
|
+
async function indexService(args, hooks = {}) {
|
|
8184
|
+
return runIndexer(stubCtx(args.projectRoot), {
|
|
8185
|
+
projectRoot: args.projectRoot,
|
|
8186
|
+
indexDir: args.indexDir,
|
|
8187
|
+
files: args.files,
|
|
8188
|
+
force: args.force,
|
|
8189
|
+
langs: args.langs,
|
|
8190
|
+
ignore: args.ignore,
|
|
8191
|
+
signal: hooks.signal,
|
|
8192
|
+
onProgress: hooks.onProgress
|
|
8193
|
+
});
|
|
8194
|
+
}
|
|
8195
|
+
function searchService(args) {
|
|
8196
|
+
const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
|
|
8197
|
+
try {
|
|
8198
|
+
return store.searchRanked(
|
|
8199
|
+
args.query,
|
|
8200
|
+
{
|
|
8201
|
+
kind: args.kind,
|
|
8202
|
+
lang: args.lang,
|
|
8203
|
+
file: args.file,
|
|
8204
|
+
lspKind: args.lspKind
|
|
8205
|
+
},
|
|
8206
|
+
args.limit
|
|
8207
|
+
);
|
|
8208
|
+
} finally {
|
|
8209
|
+
store.close();
|
|
8210
|
+
}
|
|
8211
|
+
}
|
|
8212
|
+
function statsService(args) {
|
|
8213
|
+
const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
|
|
8214
|
+
try {
|
|
8215
|
+
return store.getStats();
|
|
8216
|
+
} finally {
|
|
8217
|
+
store.close();
|
|
8218
|
+
}
|
|
8219
|
+
}
|
|
8220
|
+
|
|
8221
|
+
// src/codebase-index/background-indexer.ts
|
|
8222
|
+
var DEFAULT_FULL_INDEX_TIMEOUT_MS = 12e4;
|
|
8223
|
+
var DEFAULT_INCREMENTAL_TIMEOUT_MS = 3e4;
|
|
8224
|
+
var DEFAULT_QUERY_TIMEOUT_MS = 8e3;
|
|
8225
|
+
var _ready = false;
|
|
8226
|
+
var _indexing = false;
|
|
8227
|
+
var _currentFile = 0;
|
|
8228
|
+
var _totalFiles = 0;
|
|
8229
|
+
var _lastError = null;
|
|
8230
|
+
function isIndexReady() {
|
|
8231
|
+
return _ready;
|
|
8232
|
+
}
|
|
8233
|
+
function isIndexing() {
|
|
8234
|
+
return _indexing;
|
|
8235
|
+
}
|
|
8236
|
+
function getIndexState() {
|
|
8237
|
+
return {
|
|
8238
|
+
ready: _ready,
|
|
8239
|
+
indexing: _indexing,
|
|
8240
|
+
currentFile: _currentFile,
|
|
8241
|
+
totalFiles: _totalFiles,
|
|
8242
|
+
lastError: _lastError,
|
|
8243
|
+
circuit: indexCircuitBreaker.snapshot()
|
|
8244
|
+
};
|
|
8245
|
+
}
|
|
8246
|
+
var _listeners = [];
|
|
8247
|
+
function onIndexStateChange(listener) {
|
|
8248
|
+
_listeners.push(listener);
|
|
8249
|
+
return () => {
|
|
8250
|
+
_listeners = _listeners.filter((l) => l !== listener);
|
|
8251
|
+
};
|
|
8252
|
+
}
|
|
8253
|
+
function emitState() {
|
|
8254
|
+
const state = getIndexState();
|
|
8255
|
+
for (const l of _listeners) l(state);
|
|
8256
|
+
}
|
|
8257
|
+
function setIndexProgress(current, total) {
|
|
8258
|
+
_currentFile = current;
|
|
8259
|
+
_totalFiles = total;
|
|
8260
|
+
emitState();
|
|
8261
|
+
}
|
|
8262
|
+
var worker = null;
|
|
8263
|
+
var workerUnavailable = false;
|
|
8264
|
+
var nextRpcId = 1;
|
|
8265
|
+
var pending = /* @__PURE__ */ new Map();
|
|
8266
|
+
function resolveWorkerUrl() {
|
|
8267
|
+
if (process.env["WRONGSTACK_INDEX_INLINE"]) return null;
|
|
8268
|
+
for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
|
|
8269
|
+
try {
|
|
8270
|
+
const url = new URL(rel, import.meta.url);
|
|
8271
|
+
if (url.protocol === "file:" && fs7.existsSync(fileURLToPath(url))) return url;
|
|
8272
|
+
} catch {
|
|
8273
|
+
}
|
|
8274
|
+
}
|
|
8275
|
+
return null;
|
|
8276
|
+
}
|
|
8277
|
+
function failAllPending(err) {
|
|
8278
|
+
const entries = [...pending.values()];
|
|
8279
|
+
pending.clear();
|
|
8280
|
+
for (const p of entries) p.reject(err);
|
|
8281
|
+
}
|
|
8282
|
+
function ensureWorker() {
|
|
8283
|
+
if (worker) return worker;
|
|
8284
|
+
if (workerUnavailable) return null;
|
|
8285
|
+
const url = resolveWorkerUrl();
|
|
8286
|
+
if (!url) {
|
|
8287
|
+
workerUnavailable = true;
|
|
8288
|
+
return null;
|
|
8289
|
+
}
|
|
8290
|
+
try {
|
|
8291
|
+
const w = new Worker(url, { name: "wstack-codebase-index" });
|
|
8292
|
+
w.unref();
|
|
8293
|
+
w.on("message", (msg) => {
|
|
8294
|
+
if (msg.type === "progress") {
|
|
8295
|
+
pending.get(msg.id)?.onProgress?.(msg.current, msg.total);
|
|
8296
|
+
return;
|
|
8297
|
+
}
|
|
8298
|
+
const entry = pending.get(msg.id);
|
|
8299
|
+
if (!entry) return;
|
|
8300
|
+
pending.delete(msg.id);
|
|
8301
|
+
if (msg.ok) entry.resolve(msg.result);
|
|
8302
|
+
else entry.reject(new Error(msg.error));
|
|
8303
|
+
});
|
|
8304
|
+
w.on("error", (err) => {
|
|
8305
|
+
worker = null;
|
|
8306
|
+
failAllPending(err);
|
|
8307
|
+
});
|
|
8308
|
+
w.on("exit", () => {
|
|
8309
|
+
if (worker === w) worker = null;
|
|
8310
|
+
failAllPending(new Error("codebase-index worker exited"));
|
|
8311
|
+
});
|
|
8312
|
+
worker = w;
|
|
8313
|
+
return w;
|
|
8314
|
+
} catch {
|
|
8315
|
+
workerUnavailable = true;
|
|
8316
|
+
return null;
|
|
8317
|
+
}
|
|
8318
|
+
}
|
|
8319
|
+
function terminateWorker(reason) {
|
|
8320
|
+
const w = worker;
|
|
8321
|
+
worker = null;
|
|
8322
|
+
failAllPending(reason);
|
|
8323
|
+
if (w) void w.terminate().catch(() => {
|
|
8324
|
+
});
|
|
8325
|
+
}
|
|
8326
|
+
function shutdownCodebaseIndexHost() {
|
|
8327
|
+
cancelPendingReindexes();
|
|
8328
|
+
terminateWorker(new Error("codebase-index host shut down"));
|
|
8329
|
+
workerUnavailable = false;
|
|
8330
|
+
}
|
|
8331
|
+
function callIndexOp(op, args, opts) {
|
|
8332
|
+
const w = ensureWorker();
|
|
8333
|
+
if (!w) return callInline(op, args, opts);
|
|
8334
|
+
return new Promise((resolve7, reject) => {
|
|
8335
|
+
const id = nextRpcId++;
|
|
8336
|
+
const timer = setTimeout(() => {
|
|
8337
|
+
pending.delete(id);
|
|
8338
|
+
const err = new IndexTimeoutError(
|
|
8339
|
+
`Index ${op} exceeded its ${opts.timeoutMs}ms watchdog timeout`
|
|
8340
|
+
);
|
|
8341
|
+
terminateWorker(err);
|
|
8342
|
+
reject(err);
|
|
8343
|
+
}, opts.timeoutMs);
|
|
8344
|
+
timer.unref?.();
|
|
8345
|
+
const onAbort = () => {
|
|
8346
|
+
w.postMessage({ type: "cancel", id });
|
|
8347
|
+
};
|
|
8348
|
+
if (opts.signal?.aborted) onAbort();
|
|
8349
|
+
else opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
8350
|
+
const cleanup = () => {
|
|
8351
|
+
clearTimeout(timer);
|
|
8352
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
8353
|
+
};
|
|
8354
|
+
pending.set(id, {
|
|
8355
|
+
resolve: (v) => {
|
|
8356
|
+
cleanup();
|
|
8357
|
+
resolve7(v);
|
|
8358
|
+
},
|
|
8359
|
+
reject: (e) => {
|
|
8360
|
+
cleanup();
|
|
8361
|
+
reject(e);
|
|
8362
|
+
},
|
|
8363
|
+
onProgress: opts.onProgress
|
|
8364
|
+
});
|
|
8365
|
+
w.postMessage({ type: "request", id, op, args });
|
|
8366
|
+
});
|
|
8367
|
+
}
|
|
8368
|
+
async function callInline(op, args, opts) {
|
|
8369
|
+
const ac = new AbortController();
|
|
8370
|
+
const onOuterAbort = () => ac.abort(opts.signal?.reason ?? new Error("Indexing cancelled"));
|
|
8371
|
+
if (opts.signal?.aborted) onOuterAbort();
|
|
8372
|
+
else opts.signal?.addEventListener("abort", onOuterAbort, { once: true });
|
|
8373
|
+
let timer;
|
|
8374
|
+
const watchdog = new Promise((_, reject) => {
|
|
8375
|
+
timer = setTimeout(() => {
|
|
8376
|
+
const err = new IndexTimeoutError(
|
|
8377
|
+
`Index ${op} exceeded its ${opts.timeoutMs}ms watchdog timeout`
|
|
8378
|
+
);
|
|
8379
|
+
ac.abort(err);
|
|
8380
|
+
reject(err);
|
|
8381
|
+
}, opts.timeoutMs);
|
|
8382
|
+
timer.unref?.();
|
|
8383
|
+
});
|
|
8384
|
+
const job = async () => {
|
|
8385
|
+
switch (op) {
|
|
8386
|
+
case "index":
|
|
8387
|
+
return await indexService(args, {
|
|
8388
|
+
signal: ac.signal,
|
|
8389
|
+
onProgress: opts.onProgress
|
|
8390
|
+
});
|
|
8391
|
+
case "search":
|
|
8392
|
+
return searchService(args);
|
|
8393
|
+
case "stats":
|
|
8394
|
+
return statsService(args);
|
|
8395
|
+
default:
|
|
8396
|
+
throw new Error(`unknown index op: ${String(op)}`);
|
|
8397
|
+
}
|
|
8398
|
+
};
|
|
8399
|
+
try {
|
|
8400
|
+
return await Promise.race([job(), watchdog]);
|
|
8401
|
+
} finally {
|
|
8402
|
+
if (timer) clearTimeout(timer);
|
|
8403
|
+
opts.signal?.removeEventListener("abort", onOuterAbort);
|
|
8404
|
+
}
|
|
8405
|
+
}
|
|
8406
|
+
var chain = Promise.resolve();
|
|
8407
|
+
function withMutex(job) {
|
|
8408
|
+
const run = chain.then(job, job);
|
|
8409
|
+
chain = run.then(
|
|
8410
|
+
() => void 0,
|
|
8411
|
+
() => void 0
|
|
8412
|
+
);
|
|
8413
|
+
return run;
|
|
8414
|
+
}
|
|
8415
|
+
function circuitOpenError() {
|
|
8416
|
+
const c = indexCircuitBreaker.snapshot();
|
|
8417
|
+
return new CircuitOpenError(
|
|
8418
|
+
"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."
|
|
8419
|
+
);
|
|
8420
|
+
}
|
|
8421
|
+
var DEFAULT_DEBOUNCE_MS = 400;
|
|
8422
|
+
var debounceTimers = /* @__PURE__ */ new Map();
|
|
8423
|
+
function debounceKey(indexDir, file) {
|
|
8424
|
+
return `${indexDir ?? ""}|${file}`;
|
|
8425
|
+
}
|
|
8426
|
+
function isIndexableFile(filePath) {
|
|
8427
|
+
return detectLang(filePath) !== null;
|
|
8428
|
+
}
|
|
8429
|
+
function isUniqueConstraintError(err) {
|
|
8430
|
+
if (err instanceof Error) {
|
|
8431
|
+
const msg = err.message.toLowerCase();
|
|
8432
|
+
return msg.includes("unique constraint") || msg.includes("UNIQUE constraint");
|
|
8433
|
+
}
|
|
8434
|
+
return false;
|
|
8435
|
+
}
|
|
8436
|
+
async function runStartupIndex(opts) {
|
|
8437
|
+
if (!indexCircuitBreaker.allowRequest()) throw circuitOpenError();
|
|
8438
|
+
_indexing = true;
|
|
8439
|
+
emitState();
|
|
8440
|
+
try {
|
|
8441
|
+
const result = await withMutex(() => {
|
|
8442
|
+
_currentFile = 0;
|
|
8443
|
+
_totalFiles = 0;
|
|
8444
|
+
_lastError = null;
|
|
8445
|
+
return callIndexOp(
|
|
8446
|
+
"index",
|
|
8447
|
+
{
|
|
8448
|
+
projectRoot: opts.projectRoot,
|
|
8449
|
+
indexDir: opts.indexDir,
|
|
8450
|
+
force: opts.force,
|
|
8451
|
+
langs: opts.langs
|
|
8452
|
+
},
|
|
8453
|
+
{
|
|
8454
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_FULL_INDEX_TIMEOUT_MS,
|
|
8455
|
+
signal: opts.signal,
|
|
8456
|
+
onProgress: setIndexProgress
|
|
8457
|
+
}
|
|
8458
|
+
);
|
|
8459
|
+
});
|
|
8460
|
+
_ready = true;
|
|
8461
|
+
indexCircuitBreaker.recordSuccess();
|
|
8462
|
+
return result;
|
|
8463
|
+
} catch (err) {
|
|
8464
|
+
_lastError = err instanceof Error ? err.message : String(err);
|
|
8465
|
+
if (isUniqueConstraintError(err) && !opts.force) {
|
|
8466
|
+
_lastError = null;
|
|
8467
|
+
const rebuildResult = await runStartupIndex({
|
|
8468
|
+
...opts,
|
|
8469
|
+
force: true
|
|
8470
|
+
});
|
|
8471
|
+
_ready = true;
|
|
8472
|
+
return rebuildResult;
|
|
8473
|
+
}
|
|
8474
|
+
_ready = true;
|
|
8475
|
+
if (!opts.signal?.aborted) indexCircuitBreaker.recordFailure(err);
|
|
8476
|
+
throw err;
|
|
8477
|
+
} finally {
|
|
8478
|
+
_indexing = false;
|
|
8479
|
+
emitState();
|
|
8480
|
+
}
|
|
8481
|
+
}
|
|
8482
|
+
function enqueueReindex(opts) {
|
|
8483
|
+
const files = opts.files.filter(isIndexableFile);
|
|
8484
|
+
if (files.length === 0) return;
|
|
8485
|
+
const ms = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
8486
|
+
for (const file of files) {
|
|
8487
|
+
const key = debounceKey(opts.indexDir, file);
|
|
8488
|
+
const existing = debounceTimers.get(key);
|
|
8489
|
+
if (existing) clearTimeout(existing);
|
|
8490
|
+
const timer = setTimeout(() => {
|
|
8491
|
+
debounceTimers.delete(key);
|
|
8492
|
+
if (!indexCircuitBreaker.allowRequest()) {
|
|
8493
|
+
opts.onError?.(circuitOpenError());
|
|
8494
|
+
return;
|
|
8495
|
+
}
|
|
8496
|
+
void withMutex(
|
|
8497
|
+
() => callIndexOp(
|
|
8498
|
+
"index",
|
|
8499
|
+
{ projectRoot: opts.projectRoot, files: [file], indexDir: opts.indexDir },
|
|
8500
|
+
{ timeoutMs: opts.timeoutMs ?? DEFAULT_INCREMENTAL_TIMEOUT_MS }
|
|
8501
|
+
)
|
|
8502
|
+
).then(
|
|
8503
|
+
() => indexCircuitBreaker.recordSuccess(),
|
|
8504
|
+
(err) => {
|
|
8505
|
+
indexCircuitBreaker.recordFailure(err);
|
|
8506
|
+
opts.onError?.(err);
|
|
8507
|
+
}
|
|
8508
|
+
);
|
|
8509
|
+
}, ms);
|
|
8510
|
+
timer.unref?.();
|
|
8511
|
+
debounceTimers.set(key, timer);
|
|
8512
|
+
}
|
|
8513
|
+
}
|
|
8514
|
+
function cancelPendingReindexes() {
|
|
8515
|
+
for (const t of debounceTimers.values()) clearTimeout(t);
|
|
8516
|
+
debounceTimers.clear();
|
|
8517
|
+
}
|
|
8518
|
+
async function searchCodebaseIndex(args, opts = {}) {
|
|
8519
|
+
return callIndexOp("search", args, {
|
|
8520
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
|
|
8521
|
+
signal: opts.signal
|
|
8522
|
+
});
|
|
8523
|
+
}
|
|
8524
|
+
async function codebaseIndexStats(args, opts = {}) {
|
|
8525
|
+
return callIndexOp("stats", args, {
|
|
8526
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
|
|
8527
|
+
signal: opts.signal
|
|
8528
|
+
});
|
|
8529
|
+
}
|
|
8530
|
+
|
|
7734
8531
|
// src/codebase-index/codebase-index-tool.ts
|
|
7735
8532
|
var codebaseIndexTool = {
|
|
7736
8533
|
name: "codebase-index",
|
|
@@ -7766,103 +8563,24 @@ var codebaseIndexTool = {
|
|
|
7766
8563
|
note: "A full index is already in progress. Retry codebase-index after it completes (check codebase-stats)."
|
|
7767
8564
|
};
|
|
7768
8565
|
}
|
|
7769
|
-
const
|
|
8566
|
+
const circuit = indexCircuitBreaker.snapshot();
|
|
8567
|
+
if (circuit.state === "open" && circuit.cooldownRemainingMs > 0) {
|
|
8568
|
+
return {
|
|
8569
|
+
filesIndexed: 0,
|
|
8570
|
+
symbolsIndexed: 0,
|
|
8571
|
+
langStats: {},
|
|
8572
|
+
durationMs: 0,
|
|
8573
|
+
errors: [],
|
|
8574
|
+
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.`
|
|
8575
|
+
};
|
|
8576
|
+
}
|
|
8577
|
+
return await runStartupIndex({
|
|
7770
8578
|
projectRoot: ctx.projectRoot,
|
|
7771
8579
|
force: input.force ?? false,
|
|
7772
8580
|
langs: input.langs,
|
|
7773
8581
|
indexDir: codebaseIndexDirOverride(ctx),
|
|
7774
8582
|
signal: execOpts?.signal
|
|
7775
8583
|
});
|
|
7776
|
-
setIndexReady();
|
|
7777
|
-
return result;
|
|
7778
|
-
}
|
|
7779
|
-
};
|
|
7780
|
-
|
|
7781
|
-
// src/codebase-index/bm25.ts
|
|
7782
|
-
var K1 = 1.5;
|
|
7783
|
-
var B = 0.75;
|
|
7784
|
-
function tokenise(text) {
|
|
7785
|
-
const sanitised = text.replace(/[^\p{L}\p{N}$'_]/gu, " ").replace(/_/g, " ");
|
|
7786
|
-
return sanitised.toLowerCase().split(" ").filter(Boolean);
|
|
7787
|
-
}
|
|
7788
|
-
function splitName(name) {
|
|
7789
|
-
return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
7790
|
-
}
|
|
7791
|
-
function buildIndexableText(name, signature, docComment) {
|
|
7792
|
-
return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
|
|
7793
|
-
}
|
|
7794
|
-
function buildBm25Index(docs) {
|
|
7795
|
-
const documents = docs.map((d) => {
|
|
7796
|
-
const tokens = tokenise(d.text);
|
|
7797
|
-
return { id: d.id, tokens, raw: d.text, len: tokens.length };
|
|
7798
|
-
});
|
|
7799
|
-
const df = {};
|
|
7800
|
-
for (const doc of documents) {
|
|
7801
|
-
const seen = /* @__PURE__ */ new Set();
|
|
7802
|
-
for (const t of doc.tokens) {
|
|
7803
|
-
if (!seen.has(t)) {
|
|
7804
|
-
df[t] = (df[t] ?? 0) + 1;
|
|
7805
|
-
seen.add(t);
|
|
7806
|
-
}
|
|
7807
|
-
}
|
|
7808
|
-
}
|
|
7809
|
-
const N = documents.length;
|
|
7810
|
-
const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
|
|
7811
|
-
const avgLen = N === 0 ? 0 : totalLen / N;
|
|
7812
|
-
return new Bm25Index(documents, df, N, avgLen);
|
|
7813
|
-
}
|
|
7814
|
-
var Bm25Index = class {
|
|
7815
|
-
constructor(documents, df, N, avgLen) {
|
|
7816
|
-
this.documents = documents;
|
|
7817
|
-
this.df = df;
|
|
7818
|
-
this.N = N;
|
|
7819
|
-
this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
|
|
7820
|
-
}
|
|
7821
|
-
documents;
|
|
7822
|
-
df;
|
|
7823
|
-
N;
|
|
7824
|
-
safeAvgLen;
|
|
7825
|
-
score(query2, filter) {
|
|
7826
|
-
const qTokens = tokenise(query2);
|
|
7827
|
-
if (qTokens.length === 0) return [];
|
|
7828
|
-
const results = [];
|
|
7829
|
-
for (const doc of this.documents) {
|
|
7830
|
-
if (filter && !filter(doc.id)) continue;
|
|
7831
|
-
let docScore = 0;
|
|
7832
|
-
for (const qTerm of qTokens) {
|
|
7833
|
-
let tf = 0;
|
|
7834
|
-
for (const t of doc.tokens) {
|
|
7835
|
-
if (t === qTerm) tf++;
|
|
7836
|
-
}
|
|
7837
|
-
if (tf === 0) continue;
|
|
7838
|
-
const dfVal = this.df[qTerm] ?? 0;
|
|
7839
|
-
if (dfVal === 0) continue;
|
|
7840
|
-
const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
|
|
7841
|
-
const lenRatio = B * (doc.len / this.safeAvgLen);
|
|
7842
|
-
const tfComponent = tf * (K1 + 1) / (tf + K1 * (1 - B + lenRatio));
|
|
7843
|
-
docScore += idf * tfComponent;
|
|
7844
|
-
}
|
|
7845
|
-
if (docScore > 0) results.push({ id: doc.id, score: docScore });
|
|
7846
|
-
}
|
|
7847
|
-
return results;
|
|
7848
|
-
}
|
|
7849
|
-
getDoc(id) {
|
|
7850
|
-
return this.documents.find((d) => d.id === id);
|
|
7851
|
-
}
|
|
7852
|
-
extractSnippet(docId, queryTokens, radius = 40) {
|
|
7853
|
-
const doc = this.getDoc(docId);
|
|
7854
|
-
if (!doc) return "";
|
|
7855
|
-
for (const tok of queryTokens) {
|
|
7856
|
-
const idx = doc.raw.toLowerCase().indexOf(tok);
|
|
7857
|
-
if (idx !== -1) {
|
|
7858
|
-
const start = Math.max(0, idx - radius);
|
|
7859
|
-
const end = Math.min(doc.raw.length, idx + tok.length + radius);
|
|
7860
|
-
const excerpt = doc.raw.slice(start, end);
|
|
7861
|
-
const ellipsis = "\u2026";
|
|
7862
|
-
return (start > 0 ? ellipsis : "") + excerpt + (end < doc.raw.length ? ellipsis : "");
|
|
7863
|
-
}
|
|
7864
|
-
}
|
|
7865
|
-
return doc.raw.slice(0, radius * 2) + (doc.raw.length > radius * 2 ? "\u2026" : "");
|
|
7866
8584
|
}
|
|
7867
8585
|
};
|
|
7868
8586
|
|
|
@@ -7908,7 +8626,7 @@ var codebaseSearchTool = {
|
|
|
7908
8626
|
},
|
|
7909
8627
|
required: ["query"]
|
|
7910
8628
|
},
|
|
7911
|
-
async execute(input, ctx) {
|
|
8629
|
+
async execute(input, ctx, execOpts) {
|
|
7912
8630
|
const state = getIndexState();
|
|
7913
8631
|
if (!state.ready) {
|
|
7914
8632
|
return {
|
|
@@ -7927,51 +8645,30 @@ var codebaseSearchTool = {
|
|
|
7927
8645
|
};
|
|
7928
8646
|
}
|
|
7929
8647
|
if (state.lastError) {
|
|
8648
|
+
const circuit = state.circuit;
|
|
8649
|
+
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.";
|
|
7930
8650
|
return {
|
|
7931
8651
|
results: [],
|
|
7932
8652
|
total: 0,
|
|
7933
8653
|
query: input.query,
|
|
7934
|
-
indexStatus: `Index build failed: ${state.lastError}.
|
|
8654
|
+
indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
|
|
7935
8655
|
};
|
|
7936
8656
|
}
|
|
7937
|
-
const
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
|
|
8657
|
+
const limit = Math.min(input.limit ?? 20, 100);
|
|
8658
|
+
const { results, total } = await searchCodebaseIndex(
|
|
8659
|
+
{
|
|
8660
|
+
projectRoot: ctx.projectRoot,
|
|
8661
|
+
indexDir: codebaseIndexDirOverride(ctx),
|
|
8662
|
+
query: input.query,
|
|
7941
8663
|
kind: input.kind,
|
|
7942
8664
|
lang: input.lang,
|
|
7943
8665
|
file: input.file,
|
|
7944
|
-
lspKind: input.lspKind
|
|
7945
|
-
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
id: c.id,
|
|
7951
|
-
text: buildIndexableText(c.name, c.signature, c.docComment)
|
|
7952
|
-
}));
|
|
7953
|
-
const bm25 = buildBm25Index(indexable);
|
|
7954
|
-
const scored = bm25.score(input.query, (id) => candidates.some((c) => c.id === id));
|
|
7955
|
-
scored.sort((a, b) => b.score - a.score);
|
|
7956
|
-
const top = scored.slice(0, limit);
|
|
7957
|
-
const qTokens = tokenise(input.query);
|
|
7958
|
-
const results = top.map(({ id, score }) => {
|
|
7959
|
-
const c = expectDefined(candidates.find((c2) => c2.id === id));
|
|
7960
|
-
const snippet = bm25.extractSnippet(id, qTokens);
|
|
7961
|
-
return {
|
|
7962
|
-
...c,
|
|
7963
|
-
score,
|
|
7964
|
-
snippet
|
|
7965
|
-
};
|
|
7966
|
-
});
|
|
7967
|
-
return {
|
|
7968
|
-
results,
|
|
7969
|
-
total: candidates.length,
|
|
7970
|
-
query: input.query
|
|
7971
|
-
};
|
|
7972
|
-
} finally {
|
|
7973
|
-
store.close();
|
|
7974
|
-
}
|
|
8666
|
+
lspKind: input.lspKind,
|
|
8667
|
+
limit
|
|
8668
|
+
},
|
|
8669
|
+
{ signal: execOpts?.signal }
|
|
8670
|
+
);
|
|
8671
|
+
return { results, total, query: input.query };
|
|
7975
8672
|
}
|
|
7976
8673
|
};
|
|
7977
8674
|
|
|
@@ -7990,7 +8687,7 @@ var codebaseStatsTool = {
|
|
|
7990
8687
|
properties: {},
|
|
7991
8688
|
additionalProperties: false
|
|
7992
8689
|
},
|
|
7993
|
-
async execute(_input, ctx) {
|
|
8690
|
+
async execute(_input, ctx, execOpts) {
|
|
7994
8691
|
const idxState = getIndexState();
|
|
7995
8692
|
if (!idxState.ready) {
|
|
7996
8693
|
return {
|
|
@@ -8005,34 +8702,30 @@ var codebaseStatsTool = {
|
|
|
8005
8702
|
indexStatus: idxState.indexing ? `Indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files).` : "Index not yet built."
|
|
8006
8703
|
};
|
|
8007
8704
|
}
|
|
8705
|
+
const stats = await codebaseIndexStats(
|
|
8706
|
+
{ projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
|
|
8707
|
+
{ signal: execOpts?.signal }
|
|
8708
|
+
);
|
|
8008
8709
|
if (idxState.indexing) {
|
|
8009
|
-
const store2 = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
|
|
8010
|
-
try {
|
|
8011
|
-
const stats = store2.getStats();
|
|
8012
|
-
return {
|
|
8013
|
-
...stats,
|
|
8014
|
-
indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
|
|
8015
|
-
};
|
|
8016
|
-
} finally {
|
|
8017
|
-
store2.close();
|
|
8018
|
-
}
|
|
8019
|
-
}
|
|
8020
|
-
const store = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
|
|
8021
|
-
try {
|
|
8022
|
-
const stats = store.getStats();
|
|
8023
8710
|
return {
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
byLang: stats.byLang,
|
|
8027
|
-
byKind: stats.byKind,
|
|
8028
|
-
lastIndexed: stats.lastIndexed,
|
|
8029
|
-
sizeBytes: stats.sizeBytes,
|
|
8030
|
-
indexPath: stats.indexPath,
|
|
8031
|
-
version: stats.version
|
|
8711
|
+
...stats,
|
|
8712
|
+
indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
|
|
8032
8713
|
};
|
|
8033
|
-
} finally {
|
|
8034
|
-
store.close();
|
|
8035
8714
|
}
|
|
8715
|
+
const circuit = idxState.circuit;
|
|
8716
|
+
return {
|
|
8717
|
+
totalSymbols: stats.totalSymbols,
|
|
8718
|
+
totalFiles: stats.totalFiles,
|
|
8719
|
+
byLang: stats.byLang,
|
|
8720
|
+
byKind: stats.byKind,
|
|
8721
|
+
lastIndexed: stats.lastIndexed,
|
|
8722
|
+
sizeBytes: stats.sizeBytes,
|
|
8723
|
+
indexPath: stats.indexPath,
|
|
8724
|
+
version: stats.version,
|
|
8725
|
+
...circuit.state === "open" ? {
|
|
8726
|
+
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.`
|
|
8727
|
+
} : {}
|
|
8728
|
+
};
|
|
8036
8729
|
}
|
|
8037
8730
|
};
|
|
8038
8731
|
var setWorkingDirTool = {
|
|
@@ -8430,6 +9123,6 @@ var builtinToolsPack = {
|
|
|
8430
9123
|
tools: builtinTools
|
|
8431
9124
|
};
|
|
8432
9125
|
|
|
8433
|
-
export { CircuitBreaker, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, cancelPendingReindexes, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, createModeTool, diffTool, documentTool, editTool, enqueueReindex, execTool, fetchTool, forgetTool, formatTool, getIndexState, getProcessRegistry, gitTool, globTool, grepTool, installTool, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, logsTool, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, relatedMemoryTool, rememberTool, replaceTool, runStartupIndex, scaffoldTool, searchMemoryTool, searchTool, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
|
|
9126
|
+
export { CircuitBreaker, CircuitOpenError, IndexCircuitBreaker, IndexTimeoutError, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, cancelPendingReindexes, codebaseIndexStats, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, createModeTool, diffTool, documentTool, editTool, enqueueReindex, execTool, fetchTool, forgetTool, formatTool, getIndexState, getProcessRegistry, gitTool, globTool, grepTool, indexCircuitBreaker, installTool, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, logsTool, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, relatedMemoryTool, rememberTool, replaceTool, resetIndexCircuitBreaker, runStartupIndex, scaffoldTool, searchCodebaseIndex, searchMemoryTool, searchTool, shutdownCodebaseIndexHost, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
|
|
8434
9127
|
//# sourceMappingURL=index.js.map
|
|
8435
9128
|
//# sourceMappingURL=index.js.map
|