@wrongstack/tools 0.236.0 → 0.250.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 +1 -0
- package/dist/audit.js.map +1 -1
- package/dist/background-indexer-DwJsyAB0.d.ts +373 -0
- package/dist/bash.js +5 -0
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +865 -327
- package/dist/builtin.js.map +1 -1
- package/dist/codebase-index/index.d.ts +53 -2
- package/dist/codebase-index/index.js +854 -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 +2321 -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 +1 -0
- package/dist/exec.js.map +1 -1
- package/dist/format.js +1 -0
- 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 +886 -386
- package/dist/index.js.map +1 -1
- package/dist/install.js +1 -0
- package/dist/install.js.map +1 -1
- package/dist/lint.js +1 -0
- 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 +865 -327
- 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.js +1 -0
- package/dist/test.js.map +1 -1
- package/dist/typecheck.js +1 -0
- package/dist/typecheck.js.map +1 -1
- package/package.json +2 -2
- package/dist/background-indexer-CtbgPExj.d.ts +0 -228
package/dist/builtin.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { spawn, execFileSync, spawnSync } from 'node:child_process';
|
|
2
2
|
import * as Core from '@wrongstack/core';
|
|
3
|
-
import { buildChildEnv,
|
|
3
|
+
import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, loadTasks, emptyTaskFile, saveTasks, formatTaskList, formatPlan, mutateTasks, loadPlan, emptyPlan, savePlan, computeTaskItemProgress, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
4
4
|
import * as fs from 'node:fs';
|
|
5
5
|
import { statSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
6
6
|
import * as path2 from 'node:path';
|
|
7
7
|
import { resolve, sep, dirname, join } from 'node:path';
|
|
8
|
-
import * as
|
|
8
|
+
import * as fs14 from 'node:fs/promises';
|
|
9
9
|
import * as os from 'node:os';
|
|
10
10
|
import { createRequire } from 'node:module';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { Worker } from 'node:worker_threads';
|
|
11
13
|
import * as ts from 'typescript';
|
|
12
14
|
import * as dns from 'node:dns/promises';
|
|
13
15
|
import * as net from 'node:net';
|
|
@@ -43,7 +45,7 @@ async function* spawnStream(opts) {
|
|
|
43
45
|
const maxQueue = opts.maxQueueSize ?? 500;
|
|
44
46
|
let stdout = "";
|
|
45
47
|
let stderr = "";
|
|
46
|
-
let
|
|
48
|
+
let pending2 = "";
|
|
47
49
|
let error;
|
|
48
50
|
const cmd = resolveWin32Command(opts.cmd);
|
|
49
51
|
const needsShell = process.platform === "win32" && (cmd.endsWith(".cmd") || cmd.endsWith(".bat"));
|
|
@@ -52,6 +54,7 @@ async function* spawnStream(opts) {
|
|
|
52
54
|
signal: opts.signal,
|
|
53
55
|
env: buildChildEnv(),
|
|
54
56
|
stdio: ["ignore", "pipe", "pipe"],
|
|
57
|
+
windowsHide: true,
|
|
55
58
|
...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
|
|
56
59
|
});
|
|
57
60
|
const queue = [];
|
|
@@ -121,14 +124,14 @@ async function* spawnStream(opts) {
|
|
|
121
124
|
exitCode = 1;
|
|
122
125
|
continue;
|
|
123
126
|
}
|
|
124
|
-
|
|
125
|
-
if (
|
|
126
|
-
yield { type: "partial_output", text:
|
|
127
|
-
|
|
127
|
+
pending2 += chunk.data;
|
|
128
|
+
if (pending2.length >= flushAt) {
|
|
129
|
+
yield { type: "partial_output", text: pending2 };
|
|
130
|
+
pending2 = "";
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
|
-
if (
|
|
131
|
-
yield { type: "partial_output", text:
|
|
133
|
+
if (pending2.length > 0) {
|
|
134
|
+
yield { type: "partial_output", text: pending2 };
|
|
132
135
|
}
|
|
133
136
|
return {
|
|
134
137
|
stdout,
|
|
@@ -168,12 +171,12 @@ function safeResolve(input, ctx) {
|
|
|
168
171
|
return ensureInsideRoot(resolvePath(input, ctx), ctx);
|
|
169
172
|
}
|
|
170
173
|
async function assertRealInsideRoot(absPath, ctx) {
|
|
171
|
-
const realRoot = await
|
|
174
|
+
const realRoot = await fs14.realpath(ctx.projectRoot).catch(() => path2.resolve(ctx.projectRoot));
|
|
172
175
|
let probe = absPath;
|
|
173
176
|
for (; ; ) {
|
|
174
177
|
let real;
|
|
175
178
|
try {
|
|
176
|
-
real = await
|
|
179
|
+
real = await fs14.realpath(probe);
|
|
177
180
|
} catch (err) {
|
|
178
181
|
if (err.code === "ENOENT") {
|
|
179
182
|
const parent = path2.dirname(probe);
|
|
@@ -865,6 +868,10 @@ var bashTool = {
|
|
|
865
868
|
env,
|
|
866
869
|
stdio: ["ignore", "pipe", "pipe"],
|
|
867
870
|
detached: true,
|
|
871
|
+
// Detached console children on Windows allocate their own VISIBLE
|
|
872
|
+
// console window (one per background command — test suites flash
|
|
873
|
+
// dozens). CREATE_NO_WINDOW suppresses it; no-op elsewhere.
|
|
874
|
+
windowsHide: true,
|
|
868
875
|
signal: opts.signal
|
|
869
876
|
});
|
|
870
877
|
const pid2 = child2.pid;
|
|
@@ -917,6 +924,7 @@ var bashTool = {
|
|
|
917
924
|
env,
|
|
918
925
|
stdio: ["ignore", "pipe", "pipe"],
|
|
919
926
|
detached,
|
|
927
|
+
windowsHide: true,
|
|
920
928
|
...isWin ? {} : { signal: opts.signal }
|
|
921
929
|
});
|
|
922
930
|
const pid = child.pid;
|
|
@@ -931,7 +939,7 @@ var bashTool = {
|
|
|
931
939
|
});
|
|
932
940
|
}
|
|
933
941
|
let buf = "";
|
|
934
|
-
let
|
|
942
|
+
let pending2 = "";
|
|
935
943
|
let timedOut = false;
|
|
936
944
|
const timers = [];
|
|
937
945
|
function killWithTimeout(child2, timeoutMs2) {
|
|
@@ -1013,9 +1021,9 @@ var bashTool = {
|
|
|
1013
1021
|
});
|
|
1014
1022
|
let lastFlush = Date.now();
|
|
1015
1023
|
const flush = () => {
|
|
1016
|
-
if (
|
|
1017
|
-
const text =
|
|
1018
|
-
|
|
1024
|
+
if (pending2.length === 0) return null;
|
|
1025
|
+
const text = pending2;
|
|
1026
|
+
pending2 = "";
|
|
1019
1027
|
lastFlush = Date.now();
|
|
1020
1028
|
return text;
|
|
1021
1029
|
};
|
|
@@ -1039,7 +1047,7 @@ var bashTool = {
|
|
|
1039
1047
|
if (buf.length < MAX_OUTPUT) {
|
|
1040
1048
|
buf += text.slice(0, MAX_OUTPUT - buf.length);
|
|
1041
1049
|
}
|
|
1042
|
-
|
|
1050
|
+
pending2 += text;
|
|
1043
1051
|
push({ kind: "data", text });
|
|
1044
1052
|
pauseIfFlooded();
|
|
1045
1053
|
};
|
|
@@ -1077,7 +1085,7 @@ var bashTool = {
|
|
|
1077
1085
|
return;
|
|
1078
1086
|
}
|
|
1079
1087
|
const now = Date.now();
|
|
1080
|
-
if (
|
|
1088
|
+
if (pending2.length >= STREAM_FLUSH_BYTES || now - lastFlush >= STREAM_FLUSH_INTERVAL_MS) {
|
|
1081
1089
|
const text = flush();
|
|
1082
1090
|
if (text) yield { type: "partial_output", text };
|
|
1083
1091
|
}
|
|
@@ -1201,8 +1209,88 @@ async function executeSingle(call, ctx, opts) {
|
|
|
1201
1209
|
}
|
|
1202
1210
|
}
|
|
1203
1211
|
|
|
1212
|
+
// src/codebase-index/circuit-breaker.ts
|
|
1213
|
+
var CircuitOpenError = class extends Error {
|
|
1214
|
+
name = "CircuitOpenError";
|
|
1215
|
+
};
|
|
1216
|
+
var IndexTimeoutError = class extends Error {
|
|
1217
|
+
name = "IndexTimeoutError";
|
|
1218
|
+
};
|
|
1219
|
+
var LockError = class extends Error {
|
|
1220
|
+
name = "LockError";
|
|
1221
|
+
};
|
|
1222
|
+
var IndexCircuitBreaker = class {
|
|
1223
|
+
failureThreshold;
|
|
1224
|
+
cooldownMs;
|
|
1225
|
+
now;
|
|
1226
|
+
state = "closed";
|
|
1227
|
+
consecutiveFailures = 0;
|
|
1228
|
+
openedAt = 0;
|
|
1229
|
+
lastFailure = null;
|
|
1230
|
+
probeInFlight = false;
|
|
1231
|
+
constructor(opts = {}) {
|
|
1232
|
+
this.failureThreshold = opts.failureThreshold ?? 3;
|
|
1233
|
+
this.cooldownMs = opts.cooldownMs ?? 6e4;
|
|
1234
|
+
this.now = opts.now ?? Date.now;
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* True when a run may proceed. An open circuit transitions to half-open once
|
|
1238
|
+
* the cooldown has elapsed, admitting exactly one probe; further requests
|
|
1239
|
+
* are rejected until that probe settles via recordSuccess/recordFailure.
|
|
1240
|
+
*/
|
|
1241
|
+
allowRequest() {
|
|
1242
|
+
if (this.state === "closed") return true;
|
|
1243
|
+
if (this.state === "open") {
|
|
1244
|
+
if (this.now() - this.openedAt < this.cooldownMs) return false;
|
|
1245
|
+
this.state = "half-open";
|
|
1246
|
+
this.probeInFlight = true;
|
|
1247
|
+
return true;
|
|
1248
|
+
}
|
|
1249
|
+
if (this.probeInFlight) return false;
|
|
1250
|
+
this.probeInFlight = true;
|
|
1251
|
+
return true;
|
|
1252
|
+
}
|
|
1253
|
+
recordSuccess() {
|
|
1254
|
+
this.state = "closed";
|
|
1255
|
+
this.consecutiveFailures = 0;
|
|
1256
|
+
this.lastFailure = null;
|
|
1257
|
+
this.probeInFlight = false;
|
|
1258
|
+
}
|
|
1259
|
+
recordFailure(err) {
|
|
1260
|
+
if (err instanceof LockError) {
|
|
1261
|
+
this.lastFailure = `[transient/lock] ${err.message}`;
|
|
1262
|
+
this.probeInFlight = false;
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
this.lastFailure = err instanceof Error ? err.message : String(err);
|
|
1266
|
+
this.probeInFlight = false;
|
|
1267
|
+
this.consecutiveFailures++;
|
|
1268
|
+
if (this.state === "half-open" || this.consecutiveFailures >= this.failureThreshold) {
|
|
1269
|
+
this.state = "open";
|
|
1270
|
+
this.openedAt = this.now();
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
/** Force-close the circuit (manual recovery: `/codebase-reindex`). */
|
|
1274
|
+
reset() {
|
|
1275
|
+
this.state = "closed";
|
|
1276
|
+
this.consecutiveFailures = 0;
|
|
1277
|
+
this.lastFailure = null;
|
|
1278
|
+
this.probeInFlight = false;
|
|
1279
|
+
this.openedAt = 0;
|
|
1280
|
+
}
|
|
1281
|
+
snapshot() {
|
|
1282
|
+
return {
|
|
1283
|
+
state: this.state,
|
|
1284
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
1285
|
+
lastFailure: this.lastFailure,
|
|
1286
|
+
cooldownRemainingMs: this.state === "open" ? Math.max(0, this.cooldownMs - (this.now() - this.openedAt)) : 0
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
var indexCircuitBreaker = new IndexCircuitBreaker();
|
|
1291
|
+
|
|
1204
1292
|
// src/codebase-index/schema.ts
|
|
1205
|
-
var SCHEMA_VERSION =
|
|
1293
|
+
var SCHEMA_VERSION = 2;
|
|
1206
1294
|
|
|
1207
1295
|
// src/codebase-index/lsp-kind.ts
|
|
1208
1296
|
function lspKindToInternalKind(k) {
|
|
@@ -1237,6 +1325,94 @@ function lspKindToInternalKind(k) {
|
|
|
1237
1325
|
}
|
|
1238
1326
|
}
|
|
1239
1327
|
|
|
1328
|
+
// src/codebase-index/bm25.ts
|
|
1329
|
+
var K1 = 1.5;
|
|
1330
|
+
var B = 0.75;
|
|
1331
|
+
function tokenise(text) {
|
|
1332
|
+
const sanitised = text.replace(/[^\p{L}\p{N}$'_]/gu, " ").replace(/_/g, " ");
|
|
1333
|
+
return sanitised.toLowerCase().split(" ").filter(Boolean);
|
|
1334
|
+
}
|
|
1335
|
+
function splitName(name) {
|
|
1336
|
+
return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
1337
|
+
}
|
|
1338
|
+
function buildIndexableText(name, signature, docComment) {
|
|
1339
|
+
return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
|
|
1340
|
+
}
|
|
1341
|
+
function buildBm25Index(docs) {
|
|
1342
|
+
const documents = docs.map((d) => {
|
|
1343
|
+
const tokens = tokenise(d.text);
|
|
1344
|
+
return { id: d.id, tokens, raw: d.text, len: tokens.length };
|
|
1345
|
+
});
|
|
1346
|
+
const df = {};
|
|
1347
|
+
for (const doc of documents) {
|
|
1348
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1349
|
+
for (const t of doc.tokens) {
|
|
1350
|
+
if (!seen.has(t)) {
|
|
1351
|
+
df[t] = (df[t] ?? 0) + 1;
|
|
1352
|
+
seen.add(t);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
const N = documents.length;
|
|
1357
|
+
const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
|
|
1358
|
+
const avgLen = N === 0 ? 0 : totalLen / N;
|
|
1359
|
+
return new Bm25Index(documents, df, N, avgLen);
|
|
1360
|
+
}
|
|
1361
|
+
var Bm25Index = class {
|
|
1362
|
+
constructor(documents, df, N, avgLen) {
|
|
1363
|
+
this.documents = documents;
|
|
1364
|
+
this.df = df;
|
|
1365
|
+
this.N = N;
|
|
1366
|
+
this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
|
|
1367
|
+
}
|
|
1368
|
+
documents;
|
|
1369
|
+
df;
|
|
1370
|
+
N;
|
|
1371
|
+
safeAvgLen;
|
|
1372
|
+
score(query2, filter) {
|
|
1373
|
+
const qTokens = tokenise(query2);
|
|
1374
|
+
if (qTokens.length === 0) return [];
|
|
1375
|
+
const results = [];
|
|
1376
|
+
for (const doc of this.documents) {
|
|
1377
|
+
if (filter && !filter(doc.id)) continue;
|
|
1378
|
+
let docScore = 0;
|
|
1379
|
+
for (const qTerm of qTokens) {
|
|
1380
|
+
let tf = 0;
|
|
1381
|
+
for (const t of doc.tokens) {
|
|
1382
|
+
if (t === qTerm) tf++;
|
|
1383
|
+
}
|
|
1384
|
+
if (tf === 0) continue;
|
|
1385
|
+
const dfVal = this.df[qTerm] ?? 0;
|
|
1386
|
+
if (dfVal === 0) continue;
|
|
1387
|
+
const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
|
|
1388
|
+
const lenRatio = B * (doc.len / this.safeAvgLen);
|
|
1389
|
+
const tfComponent = tf * (K1 + 1) / (tf + K1 * (1 - B + lenRatio));
|
|
1390
|
+
docScore += idf * tfComponent;
|
|
1391
|
+
}
|
|
1392
|
+
if (docScore > 0) results.push({ id: doc.id, score: docScore });
|
|
1393
|
+
}
|
|
1394
|
+
return results;
|
|
1395
|
+
}
|
|
1396
|
+
getDoc(id) {
|
|
1397
|
+
return this.documents.find((d) => d.id === id);
|
|
1398
|
+
}
|
|
1399
|
+
extractSnippet(docId, queryTokens, radius = 40) {
|
|
1400
|
+
const doc = this.getDoc(docId);
|
|
1401
|
+
if (!doc) return "";
|
|
1402
|
+
for (const tok of queryTokens) {
|
|
1403
|
+
const idx = doc.raw.toLowerCase().indexOf(tok);
|
|
1404
|
+
if (idx !== -1) {
|
|
1405
|
+
const start = Math.max(0, idx - radius);
|
|
1406
|
+
const end = Math.min(doc.raw.length, idx + tok.length + radius);
|
|
1407
|
+
const excerpt = doc.raw.slice(start, end);
|
|
1408
|
+
const ellipsis = "\u2026";
|
|
1409
|
+
return (start > 0 ? ellipsis : "") + excerpt + (end < doc.raw.length ? ellipsis : "");
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
return doc.raw.slice(0, radius * 2) + (doc.raw.length > radius * 2 ? "\u2026" : "");
|
|
1413
|
+
}
|
|
1414
|
+
};
|
|
1415
|
+
|
|
1240
1416
|
// src/codebase-index/writer.ts
|
|
1241
1417
|
var DB_FILE = "index.db";
|
|
1242
1418
|
function resolveIndexDir(projectRoot, override) {
|
|
@@ -1272,15 +1448,79 @@ function loadDatabaseSync() {
|
|
|
1272
1448
|
}
|
|
1273
1449
|
return DatabaseSyncCtor;
|
|
1274
1450
|
}
|
|
1451
|
+
var MAX_LOCK_RETRIES = 3;
|
|
1452
|
+
var LOCK_RETRY_BASE_DELAY_MS = 50;
|
|
1453
|
+
var LOCK_RETRY_MAX_DELAY_MS = 500;
|
|
1454
|
+
function isLockError(err) {
|
|
1455
|
+
if (!(err instanceof Error)) return false;
|
|
1456
|
+
const e = err;
|
|
1457
|
+
const code = e.code ?? e.sqliteCode;
|
|
1458
|
+
if (typeof code === "string" && /SQLITE_(BUSY|LOCKED)/.test(code)) return true;
|
|
1459
|
+
if (typeof code === "number" && (code === 5 || code === 6)) return true;
|
|
1460
|
+
if (/SQLITE_(BUSY|LOCKED)/.test(err.message)) return true;
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1463
|
+
function sleepSync(ms) {
|
|
1464
|
+
try {
|
|
1465
|
+
const sab = new SharedArrayBuffer(4);
|
|
1466
|
+
const view = new Int32Array(sab);
|
|
1467
|
+
Atomics.wait(view, 0, 0, ms);
|
|
1468
|
+
} catch {
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1275
1471
|
var IndexStore = class {
|
|
1276
1472
|
db;
|
|
1277
1473
|
/** Absolute path to this project's index directory. */
|
|
1278
1474
|
indexDir;
|
|
1475
|
+
/**
|
|
1476
|
+
* True when the SQLite build provides FTS5 (Node's bundled SQLite does).
|
|
1477
|
+
* When false, ranked search falls back to the LIKE + in-process BM25 path.
|
|
1478
|
+
*/
|
|
1479
|
+
ftsAvailable = false;
|
|
1480
|
+
/**
|
|
1481
|
+
* Execute a SQLite write operation with automatic retry on lock conflicts.
|
|
1482
|
+
*
|
|
1483
|
+
* When another wstack process is holding the write lock the statement first
|
|
1484
|
+
* waits up to `busy_timeout` ms, then throws SQLITE_BUSY. This wrapper catches
|
|
1485
|
+
* that error and retries (up to MAX_LOCK_RETRIES) with exponential backoff,
|
|
1486
|
+
* giving the competing writer time to finish and release the lock.
|
|
1487
|
+
*
|
|
1488
|
+
* @param fn The write operation to execute. Can return a value which is
|
|
1489
|
+
* returned to the caller on success.
|
|
1490
|
+
* @throws {@link LockError} when all retries are exhausted on a lock conflict
|
|
1491
|
+
* (non-lock errors always propagate on the first attempt).
|
|
1492
|
+
*/
|
|
1493
|
+
runWithRetry(fn) {
|
|
1494
|
+
let lastError;
|
|
1495
|
+
for (let attempt = 0; attempt <= MAX_LOCK_RETRIES; attempt++) {
|
|
1496
|
+
try {
|
|
1497
|
+
return fn();
|
|
1498
|
+
} catch (err) {
|
|
1499
|
+
lastError = err;
|
|
1500
|
+
if (!isLockError(err)) throw err;
|
|
1501
|
+
if (attempt === MAX_LOCK_RETRIES) {
|
|
1502
|
+
const msg = lastError instanceof Error ? lastError.message : String(lastError);
|
|
1503
|
+
throw new LockError(`SQLite lock conflict after ${MAX_LOCK_RETRIES} retries: ${msg}`);
|
|
1504
|
+
}
|
|
1505
|
+
const delay = Math.min(
|
|
1506
|
+
LOCK_RETRY_BASE_DELAY_MS * Math.pow(2, attempt),
|
|
1507
|
+
LOCK_RETRY_MAX_DELAY_MS
|
|
1508
|
+
);
|
|
1509
|
+
sleepSync(delay);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
throw lastError;
|
|
1513
|
+
}
|
|
1279
1514
|
constructor(projectRoot, opts = {}) {
|
|
1280
1515
|
this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
|
|
1281
1516
|
fs.mkdirSync(this.indexDir, { recursive: true });
|
|
1282
1517
|
const Database = loadDatabaseSync();
|
|
1283
1518
|
this.db = new Database(path2.join(this.indexDir, DB_FILE));
|
|
1519
|
+
try {
|
|
1520
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
1521
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
1522
|
+
} catch {
|
|
1523
|
+
}
|
|
1284
1524
|
this.initSchema();
|
|
1285
1525
|
}
|
|
1286
1526
|
initSchema() {
|
|
@@ -1289,6 +1529,21 @@ var IndexStore = class {
|
|
|
1289
1529
|
key TEXT PRIMARY KEY,
|
|
1290
1530
|
value TEXT NOT NULL
|
|
1291
1531
|
);
|
|
1532
|
+
`);
|
|
1533
|
+
const storedRows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all("version");
|
|
1534
|
+
const storedVersion = storedRows.length ? Number(storedRows[0]?.value) : null;
|
|
1535
|
+
if (storedVersion !== null && storedVersion !== SCHEMA_VERSION) {
|
|
1536
|
+
this.db.exec(`
|
|
1537
|
+
DROP TABLE IF EXISTS symbols;
|
|
1538
|
+
DROP TABLE IF EXISTS files;
|
|
1539
|
+
DROP TABLE IF EXISTS refs;
|
|
1540
|
+
`);
|
|
1541
|
+
this.db.exec("DROP TABLE IF EXISTS symbols_fts");
|
|
1542
|
+
this.db.prepare("UPDATE metadata SET value = ? WHERE key = ?").run(String(SCHEMA_VERSION), "version");
|
|
1543
|
+
} else if (storedVersion === null) {
|
|
1544
|
+
this.db.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
|
|
1545
|
+
}
|
|
1546
|
+
this.db.exec(`
|
|
1292
1547
|
CREATE TABLE IF NOT EXISTS files (
|
|
1293
1548
|
file TEXT PRIMARY KEY,
|
|
1294
1549
|
lang TEXT NOT NULL,
|
|
@@ -1329,53 +1584,76 @@ var IndexStore = class {
|
|
|
1329
1584
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)");
|
|
1330
1585
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)");
|
|
1331
1586
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)");
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
this.
|
|
1587
|
+
try {
|
|
1588
|
+
this.db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')");
|
|
1589
|
+
this.ftsAvailable = true;
|
|
1590
|
+
} catch {
|
|
1591
|
+
this.ftsAvailable = false;
|
|
1335
1592
|
}
|
|
1336
1593
|
}
|
|
1337
1594
|
// ─── Symbol CRUD ─────────────────────────────────────────────────────────────
|
|
1338
1595
|
insertSymbols(symbols, nextId) {
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
let id = nextId;
|
|
1344
|
-
for (const s of symbols) {
|
|
1345
|
-
stmt.run(
|
|
1346
|
-
id++,
|
|
1347
|
-
s.lang,
|
|
1348
|
-
s.kind,
|
|
1349
|
-
s.name,
|
|
1350
|
-
s.file,
|
|
1351
|
-
s.line,
|
|
1352
|
-
s.col,
|
|
1353
|
-
s.signature,
|
|
1354
|
-
s.docComment,
|
|
1355
|
-
s.scope,
|
|
1356
|
-
s.text,
|
|
1357
|
-
s.file
|
|
1596
|
+
return this.runWithRetry(() => {
|
|
1597
|
+
const stmt = this.db.prepare(
|
|
1598
|
+
`INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
|
|
1599
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1358
1600
|
);
|
|
1359
|
-
|
|
1360
|
-
|
|
1601
|
+
const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
|
|
1602
|
+
let id = nextId;
|
|
1603
|
+
for (const s of symbols) {
|
|
1604
|
+
stmt.run(
|
|
1605
|
+
id,
|
|
1606
|
+
s.lang,
|
|
1607
|
+
s.kind,
|
|
1608
|
+
s.name,
|
|
1609
|
+
s.file,
|
|
1610
|
+
s.line,
|
|
1611
|
+
s.col,
|
|
1612
|
+
s.signature,
|
|
1613
|
+
s.docComment,
|
|
1614
|
+
s.scope,
|
|
1615
|
+
s.text,
|
|
1616
|
+
s.file
|
|
1617
|
+
);
|
|
1618
|
+
ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
|
|
1619
|
+
id++;
|
|
1620
|
+
}
|
|
1621
|
+
return id;
|
|
1622
|
+
});
|
|
1361
1623
|
}
|
|
1362
1624
|
deleteSymbolsForFile(file) {
|
|
1363
|
-
this.
|
|
1625
|
+
this.runWithRetry(() => {
|
|
1626
|
+
if (this.ftsAvailable) {
|
|
1627
|
+
this.db.prepare("DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
|
|
1628
|
+
}
|
|
1629
|
+
this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
|
|
1630
|
+
});
|
|
1364
1631
|
}
|
|
1632
|
+
/**
|
|
1633
|
+
* Remove every trace of a file (refs, symbols, FTS rows, file meta). Used
|
|
1634
|
+
* when a source file disappears between index runs — previously this only
|
|
1635
|
+
* dropped the `files` row, leaving its symbols orphaned but still searchable.
|
|
1636
|
+
*/
|
|
1365
1637
|
deleteFile(file) {
|
|
1366
|
-
this.
|
|
1638
|
+
this.runWithRetry(() => {
|
|
1639
|
+
this.deleteRefsForFile(file);
|
|
1640
|
+
this.deleteSymbolsForFile(file);
|
|
1641
|
+
this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
|
|
1642
|
+
});
|
|
1367
1643
|
}
|
|
1368
1644
|
// ─── File metadata ──────────────────────────────────────────────────────────
|
|
1369
1645
|
upsertFile(meta) {
|
|
1370
|
-
this.
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1646
|
+
this.runWithRetry(() => {
|
|
1647
|
+
this.db.prepare(
|
|
1648
|
+
`INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
|
|
1649
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1650
|
+
ON CONFLICT(file) DO UPDATE SET
|
|
1651
|
+
lang = excluded.lang,
|
|
1652
|
+
mtime_ms = excluded.mtime_ms,
|
|
1653
|
+
symbol_count = excluded.symbol_count,
|
|
1654
|
+
last_indexed = excluded.last_indexed`
|
|
1655
|
+
).run(meta.file, meta.lang, meta.mtimeMs, meta.symbolCount, meta.lastIndexed);
|
|
1656
|
+
});
|
|
1379
1657
|
}
|
|
1380
1658
|
getFileMeta(file) {
|
|
1381
1659
|
const rows = this.db.prepare(
|
|
@@ -1442,6 +1720,94 @@ var IndexStore = class {
|
|
|
1442
1720
|
lspKind: filter?.lspKind
|
|
1443
1721
|
}));
|
|
1444
1722
|
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Ranked search — the one-stop query the codebase-search tool and plug-lsp
|
|
1725
|
+
* use. With FTS5 this is a single indexed `MATCH` ranked by SQLite's native
|
|
1726
|
+
* `bm25()` with a built-in `snippet()`; without FTS5 it falls back to the
|
|
1727
|
+
* legacy LIKE scan + in-process BM25 (identical semantics, slower).
|
|
1728
|
+
*
|
|
1729
|
+
* Tokens are matched as prefixes (`"tok"*`), mirroring the old
|
|
1730
|
+
* `LIKE '%tok%'` recall for the common symbol-search shapes ("user" finds
|
|
1731
|
+
* "users", camelCase-split text makes "complex" find "complexOperation").
|
|
1732
|
+
*/
|
|
1733
|
+
searchRanked(query2, filter, limit) {
|
|
1734
|
+
const tokens = tokenise(query2);
|
|
1735
|
+
if (tokens.length === 0 || !this.ftsAvailable) {
|
|
1736
|
+
return this.searchRankedFallback(query2, filter, limit);
|
|
1737
|
+
}
|
|
1738
|
+
let effectiveKind = filter?.kind;
|
|
1739
|
+
if (filter?.lspKind !== void 0) {
|
|
1740
|
+
const mapped = lspKindToInternalKind(filter.lspKind);
|
|
1741
|
+
if (mapped === null) return { results: [], total: 0 };
|
|
1742
|
+
effectiveKind = mapped;
|
|
1743
|
+
}
|
|
1744
|
+
const match = tokens.map((t) => `"${t.replaceAll('"', "")}"*`).join(" OR ");
|
|
1745
|
+
const conditions = ["symbols_fts MATCH ?"];
|
|
1746
|
+
const values = [match];
|
|
1747
|
+
if (effectiveKind) {
|
|
1748
|
+
conditions.push("s.kind = ?");
|
|
1749
|
+
values.push(effectiveKind);
|
|
1750
|
+
}
|
|
1751
|
+
if (filter?.lang) {
|
|
1752
|
+
conditions.push("s.lang = ?");
|
|
1753
|
+
values.push(filter.lang);
|
|
1754
|
+
}
|
|
1755
|
+
if (filter?.file) {
|
|
1756
|
+
conditions.push("s.file LIKE ?");
|
|
1757
|
+
values.push(`%${filter.file}%`);
|
|
1758
|
+
}
|
|
1759
|
+
const where = conditions.join(" AND ");
|
|
1760
|
+
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);
|
|
1761
|
+
const total = countRows[0] ? Number(countRows[0].n) : 0;
|
|
1762
|
+
if (total === 0) return { results: [], total: 0 };
|
|
1763
|
+
const rows = this.db.prepare(
|
|
1764
|
+
`SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
|
|
1765
|
+
-bm25(symbols_fts) AS score,
|
|
1766
|
+
snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
|
|
1767
|
+
FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
|
|
1768
|
+
WHERE ${where}
|
|
1769
|
+
ORDER BY bm25(symbols_fts)
|
|
1770
|
+
LIMIT ?`
|
|
1771
|
+
).all(...values, limit);
|
|
1772
|
+
return {
|
|
1773
|
+
results: rows.map((r) => ({
|
|
1774
|
+
id: r.id,
|
|
1775
|
+
lang: r.lang,
|
|
1776
|
+
kind: r.kind,
|
|
1777
|
+
name: r.name,
|
|
1778
|
+
file: r.file,
|
|
1779
|
+
line: r.line,
|
|
1780
|
+
col: r.col,
|
|
1781
|
+
signature: r.signature,
|
|
1782
|
+
docComment: r.doc_comment,
|
|
1783
|
+
// bm25() is negative-is-better; negate so callers keep "higher is
|
|
1784
|
+
// better" and clamp so a match never reports a zero score.
|
|
1785
|
+
score: Math.max(1e-4, r.score),
|
|
1786
|
+
snippet: r.snippet,
|
|
1787
|
+
lspKind: filter?.lspKind
|
|
1788
|
+
})),
|
|
1789
|
+
total
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
/** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
|
|
1793
|
+
searchRankedFallback(query2, filter, limit) {
|
|
1794
|
+
const candidates = this.search(query2, filter);
|
|
1795
|
+
if (candidates.length === 0) return { results: [], total: 0 };
|
|
1796
|
+
if (!query2.trim()) {
|
|
1797
|
+
return { results: candidates.slice(0, limit), total: candidates.length };
|
|
1798
|
+
}
|
|
1799
|
+
const bm25 = buildBm25Index(
|
|
1800
|
+
candidates.map((c) => ({ id: c.id, text: buildIndexableText(c.name, c.signature, c.docComment) }))
|
|
1801
|
+
);
|
|
1802
|
+
const scored = bm25.score(query2, (id) => candidates.some((c) => c.id === id));
|
|
1803
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1804
|
+
const qTokens = tokenise(query2);
|
|
1805
|
+
const results = scored.slice(0, limit).map(({ id, score }) => {
|
|
1806
|
+
const c = expectDefined(candidates.find((cand) => cand.id === id));
|
|
1807
|
+
return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
|
|
1808
|
+
});
|
|
1809
|
+
return { results, total: candidates.length };
|
|
1810
|
+
}
|
|
1445
1811
|
getAllIndexable() {
|
|
1446
1812
|
return this.db.prepare("SELECT id, text FROM symbols").all().map(
|
|
1447
1813
|
({ id, text }) => ({ id, text })
|
|
@@ -1491,14 +1857,19 @@ var IndexStore = class {
|
|
|
1491
1857
|
};
|
|
1492
1858
|
}
|
|
1493
1859
|
setLastIndexed(ts2) {
|
|
1494
|
-
this.
|
|
1495
|
-
|
|
1496
|
-
|
|
1860
|
+
this.runWithRetry(() => {
|
|
1861
|
+
this.db.prepare(
|
|
1862
|
+
"INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)"
|
|
1863
|
+
).run(String(ts2));
|
|
1864
|
+
});
|
|
1497
1865
|
}
|
|
1498
1866
|
clearAll() {
|
|
1499
|
-
this.
|
|
1500
|
-
|
|
1501
|
-
|
|
1867
|
+
this.runWithRetry(() => {
|
|
1868
|
+
this.db.exec("DELETE FROM symbols");
|
|
1869
|
+
this.db.exec("DELETE FROM files");
|
|
1870
|
+
this.db.exec("DELETE FROM refs");
|
|
1871
|
+
if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
|
|
1872
|
+
});
|
|
1502
1873
|
}
|
|
1503
1874
|
// ─── Ref CRUD ────────────────────────────────────────────────────────────────
|
|
1504
1875
|
/**
|
|
@@ -1506,46 +1877,52 @@ var IndexStore = class {
|
|
|
1506
1877
|
* Replaces any existing refs from the same source (idempotent on re-index).
|
|
1507
1878
|
*/
|
|
1508
1879
|
insertRefs(fromId, refs) {
|
|
1509
|
-
this.
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1880
|
+
this.runWithRetry(() => {
|
|
1881
|
+
this.db.prepare("DELETE FROM refs WHERE from_id = ?").run(fromId);
|
|
1882
|
+
if (refs.length === 0) return;
|
|
1883
|
+
const stmt = this.db.prepare(
|
|
1884
|
+
`INSERT INTO refs(from_id, to_name, to_id, call_type, line)
|
|
1885
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
1886
|
+
);
|
|
1887
|
+
for (const ref of refs) {
|
|
1888
|
+
stmt.run(fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
|
|
1889
|
+
}
|
|
1890
|
+
});
|
|
1518
1891
|
}
|
|
1519
1892
|
/**
|
|
1520
1893
|
* Delete all refs whose source symbols are in a given file.
|
|
1521
1894
|
* Used when re-indexing a file to clear stale refs.
|
|
1522
1895
|
*/
|
|
1523
1896
|
deleteRefsForFile(file) {
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1897
|
+
this.runWithRetry(() => {
|
|
1898
|
+
const ids = this.db.prepare(
|
|
1899
|
+
"SELECT id FROM symbols WHERE file = ?"
|
|
1900
|
+
).all(file);
|
|
1901
|
+
if (!ids.length) return;
|
|
1902
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
1903
|
+
this.db.prepare(`DELETE FROM refs WHERE from_id IN (${placeholders})`).run(...ids.map((r) => r.id));
|
|
1904
|
+
});
|
|
1530
1905
|
}
|
|
1531
1906
|
/**
|
|
1532
1907
|
* Resolve `to_name` → `to_id` for all refs that have a name but no id.
|
|
1533
1908
|
* Call this after all symbols have been inserted to fill in cross-references.
|
|
1534
1909
|
*/
|
|
1535
1910
|
resolveRefs() {
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1911
|
+
return this.runWithRetry(() => {
|
|
1912
|
+
const unresolved = this.db.prepare(
|
|
1913
|
+
"SELECT id, to_name FROM refs WHERE to_id IS NULL AND to_name IS NOT NULL"
|
|
1914
|
+
).all();
|
|
1915
|
+
let resolved = 0;
|
|
1916
|
+
for (const row of unresolved) {
|
|
1917
|
+
const target = this.db.prepare("SELECT id FROM symbols WHERE name = ? LIMIT 1").all(row.to_name);
|
|
1918
|
+
const first = target[0];
|
|
1919
|
+
if (first) {
|
|
1920
|
+
this.db.prepare("UPDATE refs SET to_id = ? WHERE id = ?").run(first.id, row.id);
|
|
1921
|
+
resolved++;
|
|
1922
|
+
}
|
|
1546
1923
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1924
|
+
return resolved;
|
|
1925
|
+
});
|
|
1549
1926
|
}
|
|
1550
1927
|
/**
|
|
1551
1928
|
* Find all references TO a given symbol (who calls / uses this symbol?).
|
|
@@ -2306,7 +2683,7 @@ function parseSymbols4(opts) {
|
|
|
2306
2683
|
}
|
|
2307
2684
|
function checkNativeParser() {
|
|
2308
2685
|
try {
|
|
2309
|
-
execFileSync("rustc", ["--version"], { stdio: "pipe" });
|
|
2686
|
+
execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
|
|
2310
2687
|
const toolsDir = path2.join(process.cwd(), "tools");
|
|
2311
2688
|
try {
|
|
2312
2689
|
execFileSync(
|
|
@@ -2319,7 +2696,7 @@ function checkNativeParser() {
|
|
|
2319
2696
|
"--manifest-path",
|
|
2320
2697
|
path2.join(toolsDir, "Cargo.toml")
|
|
2321
2698
|
],
|
|
2322
|
-
{ stdio: "pipe" }
|
|
2699
|
+
{ stdio: "pipe", windowsHide: true }
|
|
2323
2700
|
);
|
|
2324
2701
|
return true;
|
|
2325
2702
|
} catch {
|
|
@@ -2342,7 +2719,8 @@ function tryNativeParse(file, content) {
|
|
|
2342
2719
|
cwd: process.cwd(),
|
|
2343
2720
|
encoding: "utf8",
|
|
2344
2721
|
timeout: 15e3,
|
|
2345
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
2722
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2723
|
+
windowsHide: true
|
|
2346
2724
|
}
|
|
2347
2725
|
);
|
|
2348
2726
|
if (result.status === 0 && result.stdout) {
|
|
@@ -2756,10 +3134,6 @@ function isScalar(value) {
|
|
|
2756
3134
|
if (/^'[^']*'$/.test(value) || /^"[^"]*"$/.test(value)) return true;
|
|
2757
3135
|
return false;
|
|
2758
3136
|
}
|
|
2759
|
-
function truncate(s, max) {
|
|
2760
|
-
if (s.length <= max) return s;
|
|
2761
|
-
return s.slice(0, max) + "...";
|
|
2762
|
-
}
|
|
2763
3137
|
function makeSymbol2(opts) {
|
|
2764
3138
|
return {
|
|
2765
3139
|
id: 0,
|
|
@@ -2819,43 +3193,13 @@ function compileGitignore(lines) {
|
|
|
2819
3193
|
async function loadGitignoreMatcher(projectRoot) {
|
|
2820
3194
|
let lines = [];
|
|
2821
3195
|
try {
|
|
2822
|
-
const raw = await
|
|
3196
|
+
const raw = await fs14.readFile(path2.join(projectRoot, ".gitignore"), "utf8");
|
|
2823
3197
|
lines = raw.split("\n");
|
|
2824
3198
|
} catch {
|
|
2825
3199
|
}
|
|
2826
3200
|
return compileGitignore(lines);
|
|
2827
3201
|
}
|
|
2828
3202
|
|
|
2829
|
-
// src/codebase-index/background-indexer.ts
|
|
2830
|
-
var _ready = false;
|
|
2831
|
-
var _indexing = false;
|
|
2832
|
-
var _currentFile = 0;
|
|
2833
|
-
var _totalFiles = 0;
|
|
2834
|
-
var _lastError = null;
|
|
2835
|
-
function setIndexReady() {
|
|
2836
|
-
_ready = true;
|
|
2837
|
-
}
|
|
2838
|
-
function getIndexState() {
|
|
2839
|
-
return {
|
|
2840
|
-
ready: _ready,
|
|
2841
|
-
indexing: _indexing,
|
|
2842
|
-
currentFile: _currentFile,
|
|
2843
|
-
totalFiles: _totalFiles,
|
|
2844
|
-
lastError: _lastError
|
|
2845
|
-
};
|
|
2846
|
-
}
|
|
2847
|
-
var _listeners = [];
|
|
2848
|
-
function emitState() {
|
|
2849
|
-
const state = getIndexState();
|
|
2850
|
-
for (const l of _listeners) l(state);
|
|
2851
|
-
}
|
|
2852
|
-
function _setIndexProgress(current, total) {
|
|
2853
|
-
_currentFile = current;
|
|
2854
|
-
_totalFiles = total;
|
|
2855
|
-
emitState();
|
|
2856
|
-
}
|
|
2857
|
-
Promise.resolve();
|
|
2858
|
-
|
|
2859
3203
|
// src/codebase-index/indexer.ts
|
|
2860
3204
|
var YIELD_EVERY_N = 50;
|
|
2861
3205
|
function yieldEventLoop() {
|
|
@@ -2906,7 +3250,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
2906
3250
|
}
|
|
2907
3251
|
let entries;
|
|
2908
3252
|
try {
|
|
2909
|
-
entries = await
|
|
3253
|
+
entries = await fs14.readdir(dir, { withFileTypes: true });
|
|
2910
3254
|
} catch {
|
|
2911
3255
|
return;
|
|
2912
3256
|
}
|
|
@@ -2993,7 +3337,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2993
3337
|
}
|
|
2994
3338
|
for (let fi = 0; fi < files.length; fi++) {
|
|
2995
3339
|
const file = expectDefined(files[fi]);
|
|
2996
|
-
|
|
3340
|
+
opts.onProgress?.(fi + 1, files.length);
|
|
2997
3341
|
if (fi > 0 && fi % YIELD_EVERY_N === 0) {
|
|
2998
3342
|
await yieldEventLoop();
|
|
2999
3343
|
throwIfAborted(signal);
|
|
@@ -3001,7 +3345,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3001
3345
|
let stat10;
|
|
3002
3346
|
try {
|
|
3003
3347
|
const statOpts = signal ? { signal } : {};
|
|
3004
|
-
stat10 = await
|
|
3348
|
+
stat10 = await fs14.stat(file, statOpts);
|
|
3005
3349
|
} catch (e) {
|
|
3006
3350
|
if (isAbortError(e)) throw e;
|
|
3007
3351
|
store.deleteFile(file);
|
|
@@ -3021,7 +3365,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3021
3365
|
store.deleteSymbolsForFile(file);
|
|
3022
3366
|
let content;
|
|
3023
3367
|
try {
|
|
3024
|
-
content = await
|
|
3368
|
+
content = await fs14.readFile(file, { encoding: "utf8", signal });
|
|
3025
3369
|
} catch (e) {
|
|
3026
3370
|
if (isAbortError(e)) throw e;
|
|
3027
3371
|
errors.push(`read error: ${file}: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -3072,7 +3416,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3072
3416
|
}
|
|
3073
3417
|
for (const [file_] of existingMeta) {
|
|
3074
3418
|
try {
|
|
3075
|
-
await
|
|
3419
|
+
await fs14.stat(file_);
|
|
3076
3420
|
} catch {
|
|
3077
3421
|
store.deleteFile(file_);
|
|
3078
3422
|
}
|
|
@@ -3088,6 +3432,290 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3088
3432
|
};
|
|
3089
3433
|
}
|
|
3090
3434
|
|
|
3435
|
+
// src/codebase-index/index-service.ts
|
|
3436
|
+
function stubCtx(projectRoot) {
|
|
3437
|
+
return {
|
|
3438
|
+
projectRoot,
|
|
3439
|
+
cwd: projectRoot,
|
|
3440
|
+
messages: [],
|
|
3441
|
+
todos: [],
|
|
3442
|
+
readFiles: /* @__PURE__ */ new Set(),
|
|
3443
|
+
fileMtimes: /* @__PURE__ */ new Map()
|
|
3444
|
+
};
|
|
3445
|
+
}
|
|
3446
|
+
async function indexService(args, hooks = {}) {
|
|
3447
|
+
return runIndexer(stubCtx(args.projectRoot), {
|
|
3448
|
+
projectRoot: args.projectRoot,
|
|
3449
|
+
indexDir: args.indexDir,
|
|
3450
|
+
files: args.files,
|
|
3451
|
+
force: args.force,
|
|
3452
|
+
langs: args.langs,
|
|
3453
|
+
ignore: args.ignore,
|
|
3454
|
+
signal: hooks.signal,
|
|
3455
|
+
onProgress: hooks.onProgress
|
|
3456
|
+
});
|
|
3457
|
+
}
|
|
3458
|
+
function searchService(args) {
|
|
3459
|
+
const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
|
|
3460
|
+
try {
|
|
3461
|
+
return store.searchRanked(
|
|
3462
|
+
args.query,
|
|
3463
|
+
{
|
|
3464
|
+
kind: args.kind,
|
|
3465
|
+
lang: args.lang,
|
|
3466
|
+
file: args.file,
|
|
3467
|
+
lspKind: args.lspKind
|
|
3468
|
+
},
|
|
3469
|
+
args.limit
|
|
3470
|
+
);
|
|
3471
|
+
} finally {
|
|
3472
|
+
store.close();
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
function statsService(args) {
|
|
3476
|
+
const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
|
|
3477
|
+
try {
|
|
3478
|
+
return store.getStats();
|
|
3479
|
+
} finally {
|
|
3480
|
+
store.close();
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
// src/codebase-index/background-indexer.ts
|
|
3485
|
+
var DEFAULT_FULL_INDEX_TIMEOUT_MS = 12e4;
|
|
3486
|
+
var DEFAULT_QUERY_TIMEOUT_MS = 8e3;
|
|
3487
|
+
var _ready = false;
|
|
3488
|
+
var _indexing = false;
|
|
3489
|
+
var _currentFile = 0;
|
|
3490
|
+
var _totalFiles = 0;
|
|
3491
|
+
var _lastError = null;
|
|
3492
|
+
function isIndexing() {
|
|
3493
|
+
return _indexing;
|
|
3494
|
+
}
|
|
3495
|
+
function getIndexState() {
|
|
3496
|
+
return {
|
|
3497
|
+
ready: _ready,
|
|
3498
|
+
indexing: _indexing,
|
|
3499
|
+
currentFile: _currentFile,
|
|
3500
|
+
totalFiles: _totalFiles,
|
|
3501
|
+
lastError: _lastError,
|
|
3502
|
+
circuit: indexCircuitBreaker.snapshot()
|
|
3503
|
+
};
|
|
3504
|
+
}
|
|
3505
|
+
var _listeners = [];
|
|
3506
|
+
function emitState() {
|
|
3507
|
+
const state = getIndexState();
|
|
3508
|
+
for (const l of _listeners) l(state);
|
|
3509
|
+
}
|
|
3510
|
+
function setIndexProgress(current, total) {
|
|
3511
|
+
_currentFile = current;
|
|
3512
|
+
_totalFiles = total;
|
|
3513
|
+
emitState();
|
|
3514
|
+
}
|
|
3515
|
+
var worker = null;
|
|
3516
|
+
var workerUnavailable = false;
|
|
3517
|
+
var nextRpcId = 1;
|
|
3518
|
+
var pending = /* @__PURE__ */ new Map();
|
|
3519
|
+
function resolveWorkerUrl() {
|
|
3520
|
+
if (process.env["WRONGSTACK_INDEX_INLINE"]) return null;
|
|
3521
|
+
for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
|
|
3522
|
+
try {
|
|
3523
|
+
const url = new URL(rel, import.meta.url);
|
|
3524
|
+
if (url.protocol === "file:" && fs.existsSync(fileURLToPath(url))) return url;
|
|
3525
|
+
} catch {
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
return null;
|
|
3529
|
+
}
|
|
3530
|
+
function failAllPending(err) {
|
|
3531
|
+
const entries = [...pending.values()];
|
|
3532
|
+
pending.clear();
|
|
3533
|
+
for (const p of entries) p.reject(err);
|
|
3534
|
+
}
|
|
3535
|
+
function ensureWorker() {
|
|
3536
|
+
if (worker) return worker;
|
|
3537
|
+
if (workerUnavailable) return null;
|
|
3538
|
+
const url = resolveWorkerUrl();
|
|
3539
|
+
if (!url) {
|
|
3540
|
+
workerUnavailable = true;
|
|
3541
|
+
return null;
|
|
3542
|
+
}
|
|
3543
|
+
try {
|
|
3544
|
+
const w = new Worker(url, { name: "wstack-codebase-index" });
|
|
3545
|
+
w.unref();
|
|
3546
|
+
w.on("message", (msg) => {
|
|
3547
|
+
if (msg.type === "progress") {
|
|
3548
|
+
pending.get(msg.id)?.onProgress?.(msg.current, msg.total);
|
|
3549
|
+
return;
|
|
3550
|
+
}
|
|
3551
|
+
const entry = pending.get(msg.id);
|
|
3552
|
+
if (!entry) return;
|
|
3553
|
+
pending.delete(msg.id);
|
|
3554
|
+
if (msg.ok) entry.resolve(msg.result);
|
|
3555
|
+
else entry.reject(new Error(msg.error));
|
|
3556
|
+
});
|
|
3557
|
+
w.on("error", (err) => {
|
|
3558
|
+
worker = null;
|
|
3559
|
+
failAllPending(err);
|
|
3560
|
+
});
|
|
3561
|
+
w.on("exit", () => {
|
|
3562
|
+
if (worker === w) worker = null;
|
|
3563
|
+
failAllPending(new Error("codebase-index worker exited"));
|
|
3564
|
+
});
|
|
3565
|
+
worker = w;
|
|
3566
|
+
return w;
|
|
3567
|
+
} catch {
|
|
3568
|
+
workerUnavailable = true;
|
|
3569
|
+
return null;
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
function terminateWorker(reason) {
|
|
3573
|
+
const w = worker;
|
|
3574
|
+
worker = null;
|
|
3575
|
+
failAllPending(reason);
|
|
3576
|
+
if (w) void w.terminate().catch(() => {
|
|
3577
|
+
});
|
|
3578
|
+
}
|
|
3579
|
+
function callIndexOp(op, args, opts) {
|
|
3580
|
+
const w = ensureWorker();
|
|
3581
|
+
if (!w) return callInline(op, args, opts);
|
|
3582
|
+
return new Promise((resolve7, reject) => {
|
|
3583
|
+
const id = nextRpcId++;
|
|
3584
|
+
const timer = setTimeout(() => {
|
|
3585
|
+
pending.delete(id);
|
|
3586
|
+
const err = new IndexTimeoutError(
|
|
3587
|
+
`Index ${op} exceeded its ${opts.timeoutMs}ms watchdog timeout`
|
|
3588
|
+
);
|
|
3589
|
+
terminateWorker(err);
|
|
3590
|
+
reject(err);
|
|
3591
|
+
}, opts.timeoutMs);
|
|
3592
|
+
timer.unref?.();
|
|
3593
|
+
const onAbort = () => {
|
|
3594
|
+
w.postMessage({ type: "cancel", id });
|
|
3595
|
+
};
|
|
3596
|
+
if (opts.signal?.aborted) onAbort();
|
|
3597
|
+
else opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
3598
|
+
const cleanup = () => {
|
|
3599
|
+
clearTimeout(timer);
|
|
3600
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
3601
|
+
};
|
|
3602
|
+
pending.set(id, {
|
|
3603
|
+
resolve: (v) => {
|
|
3604
|
+
cleanup();
|
|
3605
|
+
resolve7(v);
|
|
3606
|
+
},
|
|
3607
|
+
reject: (e) => {
|
|
3608
|
+
cleanup();
|
|
3609
|
+
reject(e);
|
|
3610
|
+
},
|
|
3611
|
+
onProgress: opts.onProgress
|
|
3612
|
+
});
|
|
3613
|
+
w.postMessage({ type: "request", id, op, args });
|
|
3614
|
+
});
|
|
3615
|
+
}
|
|
3616
|
+
async function callInline(op, args, opts) {
|
|
3617
|
+
const ac = new AbortController();
|
|
3618
|
+
const onOuterAbort = () => ac.abort(opts.signal?.reason ?? new Error("Indexing cancelled"));
|
|
3619
|
+
if (opts.signal?.aborted) onOuterAbort();
|
|
3620
|
+
else opts.signal?.addEventListener("abort", onOuterAbort, { once: true });
|
|
3621
|
+
let timer;
|
|
3622
|
+
const watchdog = new Promise((_, reject) => {
|
|
3623
|
+
timer = setTimeout(() => {
|
|
3624
|
+
const err = new IndexTimeoutError(
|
|
3625
|
+
`Index ${op} exceeded its ${opts.timeoutMs}ms watchdog timeout`
|
|
3626
|
+
);
|
|
3627
|
+
ac.abort(err);
|
|
3628
|
+
reject(err);
|
|
3629
|
+
}, opts.timeoutMs);
|
|
3630
|
+
timer.unref?.();
|
|
3631
|
+
});
|
|
3632
|
+
const job = async () => {
|
|
3633
|
+
switch (op) {
|
|
3634
|
+
case "index":
|
|
3635
|
+
return await indexService(args, {
|
|
3636
|
+
signal: ac.signal,
|
|
3637
|
+
onProgress: opts.onProgress
|
|
3638
|
+
});
|
|
3639
|
+
case "search":
|
|
3640
|
+
return searchService(args);
|
|
3641
|
+
case "stats":
|
|
3642
|
+
return statsService(args);
|
|
3643
|
+
default:
|
|
3644
|
+
throw new Error(`unknown index op: ${String(op)}`);
|
|
3645
|
+
}
|
|
3646
|
+
};
|
|
3647
|
+
try {
|
|
3648
|
+
return await Promise.race([job(), watchdog]);
|
|
3649
|
+
} finally {
|
|
3650
|
+
if (timer) clearTimeout(timer);
|
|
3651
|
+
opts.signal?.removeEventListener("abort", onOuterAbort);
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
var chain = Promise.resolve();
|
|
3655
|
+
function withMutex(job) {
|
|
3656
|
+
const run = chain.then(job, job);
|
|
3657
|
+
chain = run.then(
|
|
3658
|
+
() => void 0,
|
|
3659
|
+
() => void 0
|
|
3660
|
+
);
|
|
3661
|
+
return run;
|
|
3662
|
+
}
|
|
3663
|
+
function circuitOpenError() {
|
|
3664
|
+
const c = indexCircuitBreaker.snapshot();
|
|
3665
|
+
return new CircuitOpenError(
|
|
3666
|
+
"Codebase indexing is temporarily paused after repeated failures" + (c.lastFailure ? ` (last: ${c.lastFailure})` : "") + (c.cooldownRemainingMs > 0 ? `; auto-retry in ${Math.ceil(c.cooldownRemainingMs / 1e3)}s` : "") + ". Use /codebase-reindex to retry now."
|
|
3667
|
+
);
|
|
3668
|
+
}
|
|
3669
|
+
async function runStartupIndex(opts) {
|
|
3670
|
+
if (!indexCircuitBreaker.allowRequest()) throw circuitOpenError();
|
|
3671
|
+
_indexing = true;
|
|
3672
|
+
emitState();
|
|
3673
|
+
try {
|
|
3674
|
+
const result = await withMutex(() => {
|
|
3675
|
+
_currentFile = 0;
|
|
3676
|
+
_totalFiles = 0;
|
|
3677
|
+
_lastError = null;
|
|
3678
|
+
return callIndexOp(
|
|
3679
|
+
"index",
|
|
3680
|
+
{
|
|
3681
|
+
projectRoot: opts.projectRoot,
|
|
3682
|
+
indexDir: opts.indexDir,
|
|
3683
|
+
force: opts.force,
|
|
3684
|
+
langs: opts.langs
|
|
3685
|
+
},
|
|
3686
|
+
{
|
|
3687
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_FULL_INDEX_TIMEOUT_MS,
|
|
3688
|
+
signal: opts.signal,
|
|
3689
|
+
onProgress: setIndexProgress
|
|
3690
|
+
}
|
|
3691
|
+
);
|
|
3692
|
+
});
|
|
3693
|
+
_ready = true;
|
|
3694
|
+
indexCircuitBreaker.recordSuccess();
|
|
3695
|
+
return result;
|
|
3696
|
+
} catch (err) {
|
|
3697
|
+
_lastError = err instanceof Error ? err.message : String(err);
|
|
3698
|
+
_ready = true;
|
|
3699
|
+
if (!opts.signal?.aborted) indexCircuitBreaker.recordFailure(err);
|
|
3700
|
+
throw err;
|
|
3701
|
+
} finally {
|
|
3702
|
+
_indexing = false;
|
|
3703
|
+
emitState();
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3706
|
+
async function searchCodebaseIndex(args, opts = {}) {
|
|
3707
|
+
return callIndexOp("search", args, {
|
|
3708
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
|
|
3709
|
+
signal: opts.signal
|
|
3710
|
+
});
|
|
3711
|
+
}
|
|
3712
|
+
async function codebaseIndexStats(args, opts = {}) {
|
|
3713
|
+
return callIndexOp("stats", args, {
|
|
3714
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
|
|
3715
|
+
signal: opts.signal
|
|
3716
|
+
});
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3091
3719
|
// src/codebase-index/codebase-index-tool.ts
|
|
3092
3720
|
var codebaseIndexTool = {
|
|
3093
3721
|
name: "codebase-index",
|
|
@@ -3113,103 +3741,34 @@ var codebaseIndexTool = {
|
|
|
3113
3741
|
}
|
|
3114
3742
|
},
|
|
3115
3743
|
async execute(input, ctx, execOpts) {
|
|
3116
|
-
|
|
3744
|
+
if (isIndexing()) {
|
|
3745
|
+
return {
|
|
3746
|
+
filesIndexed: 0,
|
|
3747
|
+
symbolsIndexed: 0,
|
|
3748
|
+
langStats: {},
|
|
3749
|
+
durationMs: 0,
|
|
3750
|
+
errors: [],
|
|
3751
|
+
note: "A full index is already in progress. Retry codebase-index after it completes (check codebase-stats)."
|
|
3752
|
+
};
|
|
3753
|
+
}
|
|
3754
|
+
const circuit = indexCircuitBreaker.snapshot();
|
|
3755
|
+
if (circuit.state === "open" && circuit.cooldownRemainingMs > 0) {
|
|
3756
|
+
return {
|
|
3757
|
+
filesIndexed: 0,
|
|
3758
|
+
symbolsIndexed: 0,
|
|
3759
|
+
langStats: {},
|
|
3760
|
+
durationMs: 0,
|
|
3761
|
+
errors: [],
|
|
3762
|
+
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.`
|
|
3763
|
+
};
|
|
3764
|
+
}
|
|
3765
|
+
return await runStartupIndex({
|
|
3117
3766
|
projectRoot: ctx.projectRoot,
|
|
3118
3767
|
force: input.force ?? false,
|
|
3119
3768
|
langs: input.langs,
|
|
3120
3769
|
indexDir: codebaseIndexDirOverride(ctx),
|
|
3121
3770
|
signal: execOpts?.signal
|
|
3122
3771
|
});
|
|
3123
|
-
setIndexReady();
|
|
3124
|
-
return result;
|
|
3125
|
-
}
|
|
3126
|
-
};
|
|
3127
|
-
|
|
3128
|
-
// src/codebase-index/bm25.ts
|
|
3129
|
-
var K1 = 1.5;
|
|
3130
|
-
var B = 0.75;
|
|
3131
|
-
function tokenise(text) {
|
|
3132
|
-
const sanitised = text.replace(/[^\p{L}\p{N}$'_]/gu, " ").replace(/_/g, " ");
|
|
3133
|
-
return sanitised.toLowerCase().split(" ").filter(Boolean);
|
|
3134
|
-
}
|
|
3135
|
-
function splitName(name) {
|
|
3136
|
-
return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
3137
|
-
}
|
|
3138
|
-
function buildIndexableText(name, signature, docComment) {
|
|
3139
|
-
return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
|
|
3140
|
-
}
|
|
3141
|
-
function buildBm25Index(docs) {
|
|
3142
|
-
const documents = docs.map((d) => {
|
|
3143
|
-
const tokens = tokenise(d.text);
|
|
3144
|
-
return { id: d.id, tokens, raw: d.text, len: tokens.length };
|
|
3145
|
-
});
|
|
3146
|
-
const df = {};
|
|
3147
|
-
for (const doc of documents) {
|
|
3148
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3149
|
-
for (const t of doc.tokens) {
|
|
3150
|
-
if (!seen.has(t)) {
|
|
3151
|
-
df[t] = (df[t] ?? 0) + 1;
|
|
3152
|
-
seen.add(t);
|
|
3153
|
-
}
|
|
3154
|
-
}
|
|
3155
|
-
}
|
|
3156
|
-
const N = documents.length;
|
|
3157
|
-
const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
|
|
3158
|
-
const avgLen = N === 0 ? 0 : totalLen / N;
|
|
3159
|
-
return new Bm25Index(documents, df, N, avgLen);
|
|
3160
|
-
}
|
|
3161
|
-
var Bm25Index = class {
|
|
3162
|
-
constructor(documents, df, N, avgLen) {
|
|
3163
|
-
this.documents = documents;
|
|
3164
|
-
this.df = df;
|
|
3165
|
-
this.N = N;
|
|
3166
|
-
this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
|
|
3167
|
-
}
|
|
3168
|
-
documents;
|
|
3169
|
-
df;
|
|
3170
|
-
N;
|
|
3171
|
-
safeAvgLen;
|
|
3172
|
-
score(query2, filter) {
|
|
3173
|
-
const qTokens = tokenise(query2);
|
|
3174
|
-
if (qTokens.length === 0) return [];
|
|
3175
|
-
const results = [];
|
|
3176
|
-
for (const doc of this.documents) {
|
|
3177
|
-
if (filter && !filter(doc.id)) continue;
|
|
3178
|
-
let docScore = 0;
|
|
3179
|
-
for (const qTerm of qTokens) {
|
|
3180
|
-
let tf = 0;
|
|
3181
|
-
for (const t of doc.tokens) {
|
|
3182
|
-
if (t === qTerm) tf++;
|
|
3183
|
-
}
|
|
3184
|
-
if (tf === 0) continue;
|
|
3185
|
-
const dfVal = this.df[qTerm] ?? 0;
|
|
3186
|
-
if (dfVal === 0) continue;
|
|
3187
|
-
const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
|
|
3188
|
-
const lenRatio = B * (doc.len / this.safeAvgLen);
|
|
3189
|
-
const tfComponent = tf * (K1 + 1) / (tf + K1 * (1 - B + lenRatio));
|
|
3190
|
-
docScore += idf * tfComponent;
|
|
3191
|
-
}
|
|
3192
|
-
if (docScore > 0) results.push({ id: doc.id, score: docScore });
|
|
3193
|
-
}
|
|
3194
|
-
return results;
|
|
3195
|
-
}
|
|
3196
|
-
getDoc(id) {
|
|
3197
|
-
return this.documents.find((d) => d.id === id);
|
|
3198
|
-
}
|
|
3199
|
-
extractSnippet(docId, queryTokens, radius = 40) {
|
|
3200
|
-
const doc = this.getDoc(docId);
|
|
3201
|
-
if (!doc) return "";
|
|
3202
|
-
for (const tok of queryTokens) {
|
|
3203
|
-
const idx = doc.raw.toLowerCase().indexOf(tok);
|
|
3204
|
-
if (idx !== -1) {
|
|
3205
|
-
const start = Math.max(0, idx - radius);
|
|
3206
|
-
const end = Math.min(doc.raw.length, idx + tok.length + radius);
|
|
3207
|
-
const excerpt = doc.raw.slice(start, end);
|
|
3208
|
-
const ellipsis = "\u2026";
|
|
3209
|
-
return (start > 0 ? ellipsis : "") + excerpt + (end < doc.raw.length ? ellipsis : "");
|
|
3210
|
-
}
|
|
3211
|
-
}
|
|
3212
|
-
return doc.raw.slice(0, radius * 2) + (doc.raw.length > radius * 2 ? "\u2026" : "");
|
|
3213
3772
|
}
|
|
3214
3773
|
};
|
|
3215
3774
|
|
|
@@ -3255,7 +3814,7 @@ var codebaseSearchTool = {
|
|
|
3255
3814
|
},
|
|
3256
3815
|
required: ["query"]
|
|
3257
3816
|
},
|
|
3258
|
-
async execute(input, ctx) {
|
|
3817
|
+
async execute(input, ctx, execOpts) {
|
|
3259
3818
|
const state = getIndexState();
|
|
3260
3819
|
if (!state.ready) {
|
|
3261
3820
|
return {
|
|
@@ -3274,51 +3833,30 @@ var codebaseSearchTool = {
|
|
|
3274
3833
|
};
|
|
3275
3834
|
}
|
|
3276
3835
|
if (state.lastError) {
|
|
3836
|
+
const circuit = state.circuit;
|
|
3837
|
+
const retryHint = circuit.state === "open" ? `Indexing is paused (circuit open, retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s); the user can run /codebase-reindex to retry now.` : "Try /codebase-reindex.";
|
|
3277
3838
|
return {
|
|
3278
3839
|
results: [],
|
|
3279
3840
|
total: 0,
|
|
3280
3841
|
query: input.query,
|
|
3281
|
-
indexStatus: `Index build failed: ${state.lastError}.
|
|
3842
|
+
indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
|
|
3282
3843
|
};
|
|
3283
3844
|
}
|
|
3284
|
-
const
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3845
|
+
const limit = Math.min(input.limit ?? 20, 100);
|
|
3846
|
+
const { results, total } = await searchCodebaseIndex(
|
|
3847
|
+
{
|
|
3848
|
+
projectRoot: ctx.projectRoot,
|
|
3849
|
+
indexDir: codebaseIndexDirOverride(ctx),
|
|
3850
|
+
query: input.query,
|
|
3288
3851
|
kind: input.kind,
|
|
3289
3852
|
lang: input.lang,
|
|
3290
3853
|
file: input.file,
|
|
3291
|
-
lspKind: input.lspKind
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
id: c.id,
|
|
3298
|
-
text: buildIndexableText(c.name, c.signature, c.docComment)
|
|
3299
|
-
}));
|
|
3300
|
-
const bm25 = buildBm25Index(indexable);
|
|
3301
|
-
const scored = bm25.score(input.query, (id) => candidates.some((c) => c.id === id));
|
|
3302
|
-
scored.sort((a, b) => b.score - a.score);
|
|
3303
|
-
const top = scored.slice(0, limit);
|
|
3304
|
-
const qTokens = tokenise(input.query);
|
|
3305
|
-
const results = top.map(({ id, score }) => {
|
|
3306
|
-
const c = expectDefined(candidates.find((c2) => c2.id === id));
|
|
3307
|
-
const snippet = bm25.extractSnippet(id, qTokens);
|
|
3308
|
-
return {
|
|
3309
|
-
...c,
|
|
3310
|
-
score,
|
|
3311
|
-
snippet
|
|
3312
|
-
};
|
|
3313
|
-
});
|
|
3314
|
-
return {
|
|
3315
|
-
results,
|
|
3316
|
-
total: candidates.length,
|
|
3317
|
-
query: input.query
|
|
3318
|
-
};
|
|
3319
|
-
} finally {
|
|
3320
|
-
store.close();
|
|
3321
|
-
}
|
|
3854
|
+
lspKind: input.lspKind,
|
|
3855
|
+
limit
|
|
3856
|
+
},
|
|
3857
|
+
{ signal: execOpts?.signal }
|
|
3858
|
+
);
|
|
3859
|
+
return { results, total, query: input.query };
|
|
3322
3860
|
}
|
|
3323
3861
|
};
|
|
3324
3862
|
|
|
@@ -3337,7 +3875,7 @@ var codebaseStatsTool = {
|
|
|
3337
3875
|
properties: {},
|
|
3338
3876
|
additionalProperties: false
|
|
3339
3877
|
},
|
|
3340
|
-
async execute(_input, ctx) {
|
|
3878
|
+
async execute(_input, ctx, execOpts) {
|
|
3341
3879
|
const idxState = getIndexState();
|
|
3342
3880
|
if (!idxState.ready) {
|
|
3343
3881
|
return {
|
|
@@ -3352,34 +3890,30 @@ var codebaseStatsTool = {
|
|
|
3352
3890
|
indexStatus: idxState.indexing ? `Indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files).` : "Index not yet built."
|
|
3353
3891
|
};
|
|
3354
3892
|
}
|
|
3893
|
+
const stats = await codebaseIndexStats(
|
|
3894
|
+
{ projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
|
|
3895
|
+
{ signal: execOpts?.signal }
|
|
3896
|
+
);
|
|
3355
3897
|
if (idxState.indexing) {
|
|
3356
|
-
const store2 = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
|
|
3357
|
-
try {
|
|
3358
|
-
const stats = store2.getStats();
|
|
3359
|
-
return {
|
|
3360
|
-
...stats,
|
|
3361
|
-
indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
|
|
3362
|
-
};
|
|
3363
|
-
} finally {
|
|
3364
|
-
store2.close();
|
|
3365
|
-
}
|
|
3366
|
-
}
|
|
3367
|
-
const store = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
|
|
3368
|
-
try {
|
|
3369
|
-
const stats = store.getStats();
|
|
3370
3898
|
return {
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
byLang: stats.byLang,
|
|
3374
|
-
byKind: stats.byKind,
|
|
3375
|
-
lastIndexed: stats.lastIndexed,
|
|
3376
|
-
sizeBytes: stats.sizeBytes,
|
|
3377
|
-
indexPath: stats.indexPath,
|
|
3378
|
-
version: stats.version
|
|
3899
|
+
...stats,
|
|
3900
|
+
indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
|
|
3379
3901
|
};
|
|
3380
|
-
} finally {
|
|
3381
|
-
store.close();
|
|
3382
3902
|
}
|
|
3903
|
+
const circuit = idxState.circuit;
|
|
3904
|
+
return {
|
|
3905
|
+
totalSymbols: stats.totalSymbols,
|
|
3906
|
+
totalFiles: stats.totalFiles,
|
|
3907
|
+
byLang: stats.byLang,
|
|
3908
|
+
byKind: stats.byKind,
|
|
3909
|
+
lastIndexed: stats.lastIndexed,
|
|
3910
|
+
sizeBytes: stats.sizeBytes,
|
|
3911
|
+
indexPath: stats.indexPath,
|
|
3912
|
+
version: stats.version,
|
|
3913
|
+
...circuit.state === "open" ? {
|
|
3914
|
+
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.`
|
|
3915
|
+
} : {}
|
|
3916
|
+
};
|
|
3383
3917
|
}
|
|
3384
3918
|
};
|
|
3385
3919
|
var diffTool = {
|
|
@@ -3481,7 +4015,8 @@ function runGit(args, cwd, signal) {
|
|
|
3481
4015
|
cwd,
|
|
3482
4016
|
signal,
|
|
3483
4017
|
env: buildChildEnv(),
|
|
3484
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
4018
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4019
|
+
windowsHide: true
|
|
3485
4020
|
});
|
|
3486
4021
|
child.stdout?.on("data", (c) => {
|
|
3487
4022
|
stdout += c.toString();
|
|
@@ -3507,9 +4042,9 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
3507
4042
|
const results = [];
|
|
3508
4043
|
for (const file of files) {
|
|
3509
4044
|
const absPath = safeResolve(file, ctx);
|
|
3510
|
-
const stat10 = await
|
|
4045
|
+
const stat10 = await fs14.stat(absPath).catch(() => null);
|
|
3511
4046
|
if (!stat10?.isFile()) continue;
|
|
3512
|
-
const content = await
|
|
4047
|
+
const content = await fs14.readFile(absPath, "utf8");
|
|
3513
4048
|
const lines = content.split(/\r?\n/);
|
|
3514
4049
|
results.push(formatWithLineNumbers(file, lines));
|
|
3515
4050
|
}
|
|
@@ -3571,7 +4106,7 @@ var documentTool = {
|
|
|
3571
4106
|
const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
|
|
3572
4107
|
for (const absPath of fileList) {
|
|
3573
4108
|
try {
|
|
3574
|
-
const content = await
|
|
4109
|
+
const content = await fs14.readFile(absPath, "utf8");
|
|
3575
4110
|
filesProcessed++;
|
|
3576
4111
|
const processed = processFile(
|
|
3577
4112
|
content,
|
|
@@ -3607,7 +4142,7 @@ async function resolveFiles(filesInput, cwd) {
|
|
|
3607
4142
|
for (const f of files) {
|
|
3608
4143
|
const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
|
|
3609
4144
|
try {
|
|
3610
|
-
const stat10 = await
|
|
4145
|
+
const stat10 = await fs14.stat(absPath);
|
|
3611
4146
|
if (stat10.isFile()) resolved.push(absPath);
|
|
3612
4147
|
} catch {
|
|
3613
4148
|
}
|
|
@@ -3699,7 +4234,7 @@ var editTool = {
|
|
|
3699
4234
|
if (input.new_string === void 0) throw new Error("edit: new_string is required");
|
|
3700
4235
|
if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
|
|
3701
4236
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
3702
|
-
const stat10 = await
|
|
4237
|
+
const stat10 = await fs14.stat(absPath).catch((err) => {
|
|
3703
4238
|
if (err.code === "ENOENT") {
|
|
3704
4239
|
throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
|
|
3705
4240
|
}
|
|
@@ -3709,8 +4244,8 @@ var editTool = {
|
|
|
3709
4244
|
if (!ctx.hasRead(absPath)) {
|
|
3710
4245
|
throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
|
|
3711
4246
|
}
|
|
3712
|
-
const original = await
|
|
3713
|
-
const updated = await
|
|
4247
|
+
const original = await fs14.readFile(absPath, "utf8");
|
|
4248
|
+
const updated = await fs14.stat(absPath);
|
|
3714
4249
|
const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
|
|
3715
4250
|
const lastReadMtime = ctx.lastReadMtime(absPath);
|
|
3716
4251
|
if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
|
|
@@ -3750,7 +4285,7 @@ var editTool = {
|
|
|
3750
4285
|
const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
|
|
3751
4286
|
const newFile = toStyle(newFileLf, style);
|
|
3752
4287
|
await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
|
|
3753
|
-
const written = await
|
|
4288
|
+
const written = await fs14.stat(absPath);
|
|
3754
4289
|
ctx.recordRead(absPath, written.mtimeMs);
|
|
3755
4290
|
ctx.session.recordFileChange({
|
|
3756
4291
|
path: absPath,
|
|
@@ -4006,6 +4541,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
4006
4541
|
cwd,
|
|
4007
4542
|
env: buildChildEnv(sessionId),
|
|
4008
4543
|
stdio: ["ignore", "pipe", "pipe"],
|
|
4544
|
+
windowsHide: true,
|
|
4009
4545
|
...isWin ? {} : { signal },
|
|
4010
4546
|
...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
|
|
4011
4547
|
});
|
|
@@ -4725,7 +5261,8 @@ function runGit2(args, cwd, signal) {
|
|
|
4725
5261
|
cwd,
|
|
4726
5262
|
signal,
|
|
4727
5263
|
env: buildChildEnv(),
|
|
4728
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
5264
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
5265
|
+
windowsHide: true
|
|
4729
5266
|
});
|
|
4730
5267
|
child.stdout?.on("data", (chunk) => {
|
|
4731
5268
|
if (stdout.length < MAX_OUTPUT3) {
|
|
@@ -4801,7 +5338,7 @@ var globTool = {
|
|
|
4801
5338
|
}
|
|
4802
5339
|
let entries;
|
|
4803
5340
|
try {
|
|
4804
|
-
entries = await
|
|
5341
|
+
entries = await fs14.readdir(dir, { withFileTypes: true });
|
|
4805
5342
|
} catch {
|
|
4806
5343
|
return;
|
|
4807
5344
|
}
|
|
@@ -4817,7 +5354,7 @@ var globTool = {
|
|
|
4817
5354
|
} else if (e.isFile()) {
|
|
4818
5355
|
if (re.test(rel) || re.test(name)) {
|
|
4819
5356
|
try {
|
|
4820
|
-
const st = await
|
|
5357
|
+
const st = await fs14.stat(full);
|
|
4821
5358
|
results.push({ rel: full, mtime: st.mtimeMs });
|
|
4822
5359
|
if (results.length >= limit) {
|
|
4823
5360
|
truncated = true;
|
|
@@ -4836,7 +5373,7 @@ var globTool = {
|
|
|
4836
5373
|
};
|
|
4837
5374
|
async function readGitignore(dir) {
|
|
4838
5375
|
try {
|
|
4839
|
-
const raw = await
|
|
5376
|
+
const raw = await fs14.readFile(path2.join(dir, ".gitignore"), "utf8");
|
|
4840
5377
|
return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
4841
5378
|
} catch {
|
|
4842
5379
|
return [];
|
|
@@ -4970,7 +5507,7 @@ var grepTool = {
|
|
|
4970
5507
|
async function detectRg(signal) {
|
|
4971
5508
|
return new Promise((resolve7) => {
|
|
4972
5509
|
try {
|
|
4973
|
-
const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", signal });
|
|
5510
|
+
const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", signal, windowsHide: true });
|
|
4974
5511
|
p.on("error", () => resolve7(false));
|
|
4975
5512
|
p.on("close", (code) => resolve7(code === 0));
|
|
4976
5513
|
} catch {
|
|
@@ -5000,7 +5537,7 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
5000
5537
|
const FLUSH_AT = 16;
|
|
5001
5538
|
const MAX_BUF_BYTES = 1e6;
|
|
5002
5539
|
let bufOverflow = false;
|
|
5003
|
-
const child = spawn("rg", args, { signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"] });
|
|
5540
|
+
const child = spawn("rg", args, { signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
5004
5541
|
const queue = [];
|
|
5005
5542
|
let waiter;
|
|
5006
5543
|
const wake = () => {
|
|
@@ -5120,7 +5657,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
5120
5657
|
if (stopped || signal.aborted) return;
|
|
5121
5658
|
let entries;
|
|
5122
5659
|
try {
|
|
5123
|
-
entries = await
|
|
5660
|
+
entries = await fs14.readdir(dir, { withFileTypes: true });
|
|
5124
5661
|
} catch {
|
|
5125
5662
|
return;
|
|
5126
5663
|
}
|
|
@@ -5135,9 +5672,9 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
5135
5672
|
if (globRe && !globRe.test(e.name) && !globRe.test(full)) continue;
|
|
5136
5673
|
if (globRe) globRe.lastIndex = 0;
|
|
5137
5674
|
try {
|
|
5138
|
-
const stat10 = await
|
|
5675
|
+
const stat10 = await fs14.stat(full);
|
|
5139
5676
|
if (stat10.size > 1e6) continue;
|
|
5140
|
-
const head = await
|
|
5677
|
+
const head = await fs14.readFile(full);
|
|
5141
5678
|
if (isBinaryBuffer(head)) continue;
|
|
5142
5679
|
const text = head.toString("utf8");
|
|
5143
5680
|
const lines = text.split(/\r?\n/);
|
|
@@ -5346,7 +5883,7 @@ var jsonTool = {
|
|
|
5346
5883
|
let raw;
|
|
5347
5884
|
if (input.file) {
|
|
5348
5885
|
try {
|
|
5349
|
-
raw = await
|
|
5886
|
+
raw = await fs14.readFile(input.file, "utf8");
|
|
5350
5887
|
} catch {
|
|
5351
5888
|
return { data: null, formatted: "", type: "unknown", error: `Could not read file` };
|
|
5352
5889
|
}
|
|
@@ -5625,7 +6162,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
5625
6162
|
clearTimeout(timer);
|
|
5626
6163
|
resolve7(result);
|
|
5627
6164
|
};
|
|
5628
|
-
const child = spawn("docker", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"] });
|
|
6165
|
+
const child = spawn("docker", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
5629
6166
|
const timer = setTimeout(() => {
|
|
5630
6167
|
child.kill("SIGTERM");
|
|
5631
6168
|
finish(empty());
|
|
@@ -5771,7 +6308,7 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
5771
6308
|
const MAX = 1e5;
|
|
5772
6309
|
const resolved = resolveWin32Command(manager);
|
|
5773
6310
|
const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
5774
|
-
const child = spawn(resolved, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
|
|
6311
|
+
const child = spawn(resolved, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
|
|
5775
6312
|
child.stdout?.on("data", (c) => {
|
|
5776
6313
|
if (stdout.length < MAX) stdout += c.toString();
|
|
5777
6314
|
});
|
|
@@ -5867,12 +6404,12 @@ var patchTool = {
|
|
|
5867
6404
|
};
|
|
5868
6405
|
}
|
|
5869
6406
|
}
|
|
5870
|
-
const tmpDir = await
|
|
6407
|
+
const tmpDir = await fs14.mkdtemp(path2.join(os.tmpdir(), ".wstack_patch_"));
|
|
5871
6408
|
try {
|
|
5872
|
-
await
|
|
6409
|
+
await fs14.chmod(tmpDir, 448).catch(() => {
|
|
5873
6410
|
});
|
|
5874
6411
|
const patchFile = path2.join(tmpDir, "in.diff");
|
|
5875
|
-
await
|
|
6412
|
+
await fs14.writeFile(patchFile, input.patch, { mode: 384 });
|
|
5876
6413
|
const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
|
|
5877
6414
|
const result = await runPatch(args, dir, opts.signal);
|
|
5878
6415
|
if (result.exitCode !== 0 && !dryRun) {
|
|
@@ -5893,7 +6430,7 @@ var patchTool = {
|
|
|
5893
6430
|
message: result.stdout || "patch applied"
|
|
5894
6431
|
};
|
|
5895
6432
|
} finally {
|
|
5896
|
-
await
|
|
6433
|
+
await fs14.rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
5897
6434
|
});
|
|
5898
6435
|
}
|
|
5899
6436
|
}
|
|
@@ -5918,7 +6455,7 @@ function runPatch(args, cwd, signal) {
|
|
|
5918
6455
|
let stdout = "";
|
|
5919
6456
|
let stderr = "";
|
|
5920
6457
|
const env = { ...buildChildEnv(), LANG: "C", LC_ALL: "C" };
|
|
5921
|
-
const child = spawn("patch", args, { cwd, signal, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
6458
|
+
const child = spawn("patch", args, { cwd, signal, env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
5922
6459
|
child.stdout?.on("data", (c) => {
|
|
5923
6460
|
stdout += c.toString();
|
|
5924
6461
|
});
|
|
@@ -6195,7 +6732,7 @@ var readTool = {
|
|
|
6195
6732
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
6196
6733
|
let stat10;
|
|
6197
6734
|
try {
|
|
6198
|
-
stat10 = await
|
|
6735
|
+
stat10 = await fs14.stat(absPath);
|
|
6199
6736
|
} catch (err) {
|
|
6200
6737
|
const code = err.code;
|
|
6201
6738
|
if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
|
|
@@ -6207,7 +6744,7 @@ var readTool = {
|
|
|
6207
6744
|
if (stat10.size > MAX_BYTES2) {
|
|
6208
6745
|
throw new Error(`read: file too large (${stat10.size} bytes, limit ${MAX_BYTES2})`);
|
|
6209
6746
|
}
|
|
6210
|
-
const buf = await
|
|
6747
|
+
const buf = await fs14.readFile(absPath);
|
|
6211
6748
|
if (isBinaryBuffer(buf)) {
|
|
6212
6749
|
throw new Error(`read: "${input.path}" appears to be binary`);
|
|
6213
6750
|
}
|
|
@@ -6275,11 +6812,11 @@ var replaceTool = {
|
|
|
6275
6812
|
const dryRun = input.dry_run ?? false;
|
|
6276
6813
|
const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
|
|
6277
6814
|
const fileList = await resolveFiles2(filesInput, ctx, globRe);
|
|
6278
|
-
const realRoot = await
|
|
6815
|
+
const realRoot = await fs14.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
|
|
6279
6816
|
const results = [];
|
|
6280
6817
|
let totalReplacements = 0;
|
|
6281
6818
|
for (const absPath of fileList) {
|
|
6282
|
-
const lstat2 = await
|
|
6819
|
+
const lstat2 = await fs14.lstat(absPath).catch((err) => {
|
|
6283
6820
|
if (err.code === "ENOENT") return null;
|
|
6284
6821
|
throw err;
|
|
6285
6822
|
});
|
|
@@ -6287,17 +6824,17 @@ var replaceTool = {
|
|
|
6287
6824
|
if (lstat2.isSymbolicLink()) continue;
|
|
6288
6825
|
let realPath;
|
|
6289
6826
|
try {
|
|
6290
|
-
realPath = await
|
|
6827
|
+
realPath = await fs14.realpath(absPath);
|
|
6291
6828
|
} catch {
|
|
6292
6829
|
continue;
|
|
6293
6830
|
}
|
|
6294
6831
|
const rel = path2.relative(realRoot, realPath);
|
|
6295
6832
|
if (rel.startsWith("..") || path2.isAbsolute(rel)) continue;
|
|
6296
|
-
const stat10 = await
|
|
6833
|
+
const stat10 = await fs14.stat(realPath).catch(() => null);
|
|
6297
6834
|
if (!stat10 || !stat10.isFile()) continue;
|
|
6298
6835
|
let content;
|
|
6299
6836
|
try {
|
|
6300
|
-
const buf = await
|
|
6837
|
+
const buf = await fs14.readFile(realPath);
|
|
6301
6838
|
if (isBinaryBuffer(buf)) continue;
|
|
6302
6839
|
content = buf.toString("utf8");
|
|
6303
6840
|
} catch {
|
|
@@ -6349,7 +6886,7 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
6349
6886
|
const resolved = [];
|
|
6350
6887
|
for (const p of parts) {
|
|
6351
6888
|
const absPath = safeResolve(p, ctx);
|
|
6352
|
-
const stat10 = await
|
|
6889
|
+
const stat10 = await fs14.stat(absPath).catch(() => null);
|
|
6353
6890
|
if (stat10?.isFile()) {
|
|
6354
6891
|
resolved.push(absPath);
|
|
6355
6892
|
}
|
|
@@ -6370,7 +6907,7 @@ async function globFiles(pattern, base, extraGlob) {
|
|
|
6370
6907
|
function checkRg() {
|
|
6371
6908
|
return new Promise((resolve7) => {
|
|
6372
6909
|
try {
|
|
6373
|
-
const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore" });
|
|
6910
|
+
const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", windowsHide: true });
|
|
6374
6911
|
p.on("error", () => resolve7(false));
|
|
6375
6912
|
p.on("close", (code) => resolve7(code === 0));
|
|
6376
6913
|
} catch {
|
|
@@ -6383,7 +6920,8 @@ function spawnRgFind(pattern, base) {
|
|
|
6383
6920
|
const child = spawn("rg", args, {
|
|
6384
6921
|
signal: AbortSignal.timeout(3e4),
|
|
6385
6922
|
env: buildChildEnv(),
|
|
6386
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
6923
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
6924
|
+
windowsHide: true
|
|
6387
6925
|
});
|
|
6388
6926
|
let buf = "";
|
|
6389
6927
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -6404,7 +6942,7 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
6404
6942
|
const walk = async (dir) => {
|
|
6405
6943
|
let entries;
|
|
6406
6944
|
try {
|
|
6407
|
-
entries = await
|
|
6945
|
+
entries = await fs14.readdir(dir, { withFileTypes: true });
|
|
6408
6946
|
} catch {
|
|
6409
6947
|
return;
|
|
6410
6948
|
}
|
|
@@ -6412,7 +6950,7 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
6412
6950
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
6413
6951
|
const full = path2.join(dir, e.name);
|
|
6414
6952
|
try {
|
|
6415
|
-
const stat10 = await
|
|
6953
|
+
const stat10 = await fs14.lstat(full);
|
|
6416
6954
|
if (stat10.isSymbolicLink()) continue;
|
|
6417
6955
|
} catch {
|
|
6418
6956
|
continue;
|
|
@@ -6589,7 +7127,7 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
6589
7127
|
}
|
|
6590
7128
|
const fullPath = target;
|
|
6591
7129
|
if (!dryRun) {
|
|
6592
|
-
await
|
|
7130
|
+
await fs14.mkdir(path2.dirname(fullPath), { recursive: true });
|
|
6593
7131
|
await atomicWrite(fullPath, substituteVars(content, name, vars));
|
|
6594
7132
|
}
|
|
6595
7133
|
files.push(resolvedPath);
|
|
@@ -6867,7 +7405,7 @@ var setWorkingDirTool = {
|
|
|
6867
7405
|
};
|
|
6868
7406
|
}
|
|
6869
7407
|
try {
|
|
6870
|
-
await
|
|
7408
|
+
await fs14.access(resolved);
|
|
6871
7409
|
} catch {
|
|
6872
7410
|
try {
|
|
6873
7411
|
ctx.setWorkingDir(previous);
|
|
@@ -7840,7 +8378,7 @@ var treeTool = {
|
|
|
7840
8378
|
}
|
|
7841
8379
|
};
|
|
7842
8380
|
async function walkDir(dir, depth, opts) {
|
|
7843
|
-
const entries = await
|
|
8381
|
+
const entries = await fs14.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
7844
8382
|
const filtered = entries.filter((e) => {
|
|
7845
8383
|
if (!opts.showHidden && e.name.startsWith(".")) return false;
|
|
7846
8384
|
if (opts.exclude.has(e.name)) return false;
|
|
@@ -7990,14 +8528,14 @@ var writeTool = {
|
|
|
7990
8528
|
let existed = false;
|
|
7991
8529
|
let prev = "";
|
|
7992
8530
|
try {
|
|
7993
|
-
const stat11 = await
|
|
8531
|
+
const stat11 = await fs14.stat(absPath);
|
|
7994
8532
|
existed = stat11.isFile();
|
|
7995
8533
|
if (existed) {
|
|
7996
8534
|
if (!ctx.hasRead(absPath)) {
|
|
7997
|
-
prev = await
|
|
8535
|
+
prev = await fs14.readFile(absPath, "utf8");
|
|
7998
8536
|
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
7999
8537
|
} else {
|
|
8000
|
-
prev = await
|
|
8538
|
+
prev = await fs14.readFile(absPath, "utf8");
|
|
8001
8539
|
}
|
|
8002
8540
|
}
|
|
8003
8541
|
} catch (err) {
|
|
@@ -8008,7 +8546,7 @@ var writeTool = {
|
|
|
8008
8546
|
await atomicWrite(absPath, input.content);
|
|
8009
8547
|
const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
8010
8548
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
8011
|
-
const stat10 = await
|
|
8549
|
+
const stat10 = await fs14.stat(absPath);
|
|
8012
8550
|
ctx.recordRead(absPath, stat10.mtimeMs);
|
|
8013
8551
|
ctx.session.recordFileChange({
|
|
8014
8552
|
path: absPath,
|