@wrongstack/tools 0.270.0 → 0.272.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 +10 -5
- package/dist/audit.js.map +1 -1
- package/dist/{background-indexer-CJ5JiV5i.d.ts → background-indexer-BoTUw0EM.d.ts} +1 -1
- package/dist/bash.js +580 -9
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +846 -172
- package/dist/builtin.js.map +1 -1
- package/dist/codebase-index/index.d.ts +13 -3
- package/dist/codebase-index/index.js +129 -74
- package/dist/codebase-index/index.js.map +1 -1
- package/dist/codebase-index/worker.js +126 -71
- package/dist/codebase-index/worker.js.map +1 -1
- package/dist/exec.js +5 -2
- package/dist/exec.js.map +1 -1
- package/dist/fetch.js +24 -1
- package/dist/fetch.js.map +1 -1
- package/dist/format.js +10 -5
- package/dist/format.js.map +1 -1
- package/dist/grep.js +55 -33
- package/dist/grep.js.map +1 -1
- package/dist/index.d.ts +317 -3
- package/dist/index.js +1405 -200
- package/dist/index.js.map +1 -1
- package/dist/install.js +10 -5
- package/dist/install.js.map +1 -1
- package/dist/lint.js +10 -5
- package/dist/lint.js.map +1 -1
- package/dist/pack.js +846 -172
- package/dist/pack.js.map +1 -1
- package/dist/process-registry.js +5 -2
- package/dist/process-registry.js.map +1 -1
- package/dist/read.js +2 -2
- package/dist/read.js.map +1 -1
- package/dist/search.js.map +1 -1
- package/dist/test.js +10 -5
- package/dist/test.js.map +1 -1
- package/dist/typecheck.js +10 -5
- package/dist/typecheck.js.map +1 -1
- package/package.json +2 -2
package/dist/builtin.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { spawn, execFileSync
|
|
1
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
2
2
|
import * as Core from '@wrongstack/core';
|
|
3
|
-
import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
3
|
+
import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, toErrorMessage, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
4
4
|
import * as fs from 'node:fs';
|
|
5
|
-
import { statSync, mkdirSync, createWriteStream
|
|
6
|
-
import * as
|
|
5
|
+
import { statSync, mkdirSync, createWriteStream } from 'node:fs';
|
|
6
|
+
import * as fs2 from 'node:fs/promises';
|
|
7
7
|
import * as path3 from 'node:path';
|
|
8
8
|
import { resolve, sep, dirname, join } from 'node:path';
|
|
9
|
-
import * as
|
|
10
|
-
import { toErrorMessage } from '@wrongstack/core/utils';
|
|
9
|
+
import * as os2 from 'node:os';
|
|
10
|
+
import { toErrorMessage as toErrorMessage$1 } from '@wrongstack/core/utils';
|
|
11
11
|
import { createRequire } from 'node:module';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { Worker } from 'node:worker_threads';
|
|
@@ -31,12 +31,12 @@ function sweepOldSpoolFiles(dir) {
|
|
|
31
31
|
void (async () => {
|
|
32
32
|
try {
|
|
33
33
|
const now = Date.now();
|
|
34
|
-
for (const name of await
|
|
34
|
+
for (const name of await fs2.readdir(dir)) {
|
|
35
35
|
if (!name.endsWith(".log")) continue;
|
|
36
36
|
const p = path3.join(dir, name);
|
|
37
37
|
try {
|
|
38
|
-
const st = await
|
|
39
|
-
if (now - st.mtimeMs > SPOOL_RETENTION_MS) await
|
|
38
|
+
const st = await fs2.stat(p);
|
|
39
|
+
if (now - st.mtimeMs > SPOOL_RETENTION_MS) await fs2.unlink(p);
|
|
40
40
|
} catch {
|
|
41
41
|
}
|
|
42
42
|
}
|
|
@@ -345,10 +345,13 @@ function redactCommand(cmd) {
|
|
|
345
345
|
var DEFAULT_GRACE_MS = 2e3;
|
|
346
346
|
function killWin32Tree(pid) {
|
|
347
347
|
try {
|
|
348
|
-
spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
348
|
+
const child = spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
349
349
|
stdio: "ignore",
|
|
350
350
|
windowsHide: true
|
|
351
|
-
})
|
|
351
|
+
});
|
|
352
|
+
child.on("error", () => {
|
|
353
|
+
});
|
|
354
|
+
child.unref();
|
|
352
355
|
return true;
|
|
353
356
|
} catch {
|
|
354
357
|
return false;
|
|
@@ -550,7 +553,7 @@ var ProcessRegistryImpl = class {
|
|
|
550
553
|
if (p.killed) return true;
|
|
551
554
|
if (p.protected) return false;
|
|
552
555
|
const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
|
|
553
|
-
const isWin3 =
|
|
556
|
+
const isWin3 = os2.platform() === "win32";
|
|
554
557
|
if (isWin3) {
|
|
555
558
|
const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
|
|
556
559
|
if (liveRealChild && killWin32Tree(pid)) {
|
|
@@ -756,8 +759,10 @@ async function* spawnStream(opts) {
|
|
|
756
759
|
queue.push({ kind: "close", data: "", code: 124 });
|
|
757
760
|
wake();
|
|
758
761
|
};
|
|
759
|
-
if (
|
|
760
|
-
|
|
762
|
+
if (isWin) {
|
|
763
|
+
if (opts.signal.aborted) onAbort();
|
|
764
|
+
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
765
|
+
}
|
|
761
766
|
let exitCode = 0;
|
|
762
767
|
let spawnFailed = false;
|
|
763
768
|
try {
|
|
@@ -801,7 +806,7 @@ async function* spawnStream(opts) {
|
|
|
801
806
|
};
|
|
802
807
|
} finally {
|
|
803
808
|
spool.finalize();
|
|
804
|
-
opts.signal.removeEventListener("abort", onAbort);
|
|
809
|
+
if (isWin) opts.signal.removeEventListener("abort", onAbort);
|
|
805
810
|
child.stdout?.off("data", onOut);
|
|
806
811
|
child.stderr?.off("data", onErr);
|
|
807
812
|
child.stdout?.destroy();
|
|
@@ -856,13 +861,13 @@ function safeResolve(input, ctx) {
|
|
|
856
861
|
async function assertRealInsideRoot(absPath, ctx) {
|
|
857
862
|
if (ctx.allowOutsideProjectRoot) return;
|
|
858
863
|
const realRoots = await Promise.all(
|
|
859
|
-
allowedRoots(ctx).map((r) =>
|
|
864
|
+
allowedRoots(ctx).map((r) => fs2.realpath(r).catch(() => path3.resolve(r)))
|
|
860
865
|
);
|
|
861
866
|
let probe = absPath;
|
|
862
867
|
for (; ; ) {
|
|
863
868
|
let real;
|
|
864
869
|
try {
|
|
865
|
-
real = await
|
|
870
|
+
real = await fs2.realpath(probe);
|
|
866
871
|
} catch (err) {
|
|
867
872
|
if (err.code === "ENOENT") {
|
|
868
873
|
const parent = path3.dirname(probe);
|
|
@@ -1067,6 +1072,560 @@ function parseAuditOutput(json, exitCode) {
|
|
|
1067
1072
|
};
|
|
1068
1073
|
}
|
|
1069
1074
|
}
|
|
1075
|
+
var REGISTRY_FILE = ".wrongstack/process-registry.json";
|
|
1076
|
+
var HEARTBEAT_INTERVAL_MS = 5e3;
|
|
1077
|
+
var STALE_THRESHOLD_MS = 3e4;
|
|
1078
|
+
var LOCKFILE = ".wrongstack/.process-registry.lock";
|
|
1079
|
+
function generateInstanceId() {
|
|
1080
|
+
const hostname2 = os2.hostname();
|
|
1081
|
+
const pid = process.pid;
|
|
1082
|
+
const random = Math.random().toString(36).slice(2, 8);
|
|
1083
|
+
return `${hostname2}:${pid}:${random}`;
|
|
1084
|
+
}
|
|
1085
|
+
async function acquireLock(lockfilePath, timeoutMs = 5e3) {
|
|
1086
|
+
const start = Date.now();
|
|
1087
|
+
const pidStr = String(process.pid);
|
|
1088
|
+
const hostStr = os2.hostname();
|
|
1089
|
+
while (Date.now() - start < timeoutMs) {
|
|
1090
|
+
try {
|
|
1091
|
+
await fs2.writeFile(lockfilePath, `${pidStr}:${hostStr}:${Date.now()}`, { flag: "wx" });
|
|
1092
|
+
return async () => {
|
|
1093
|
+
try {
|
|
1094
|
+
await fs2.unlink(lockfilePath);
|
|
1095
|
+
} catch {
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
} catch (err) {
|
|
1099
|
+
if (err.code === "EEXIST") {
|
|
1100
|
+
try {
|
|
1101
|
+
const content = await fs2.readFile(lockfilePath, "utf-8");
|
|
1102
|
+
const parts = content.split(":");
|
|
1103
|
+
const lockPidStr = parts[0] ?? "0";
|
|
1104
|
+
const lockPid = parseInt(lockPidStr, 10);
|
|
1105
|
+
if (process.platform !== "win32") {
|
|
1106
|
+
try {
|
|
1107
|
+
process.kill(lockPid, 0);
|
|
1108
|
+
} catch {
|
|
1109
|
+
await fs2.unlink(lockfilePath);
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
} catch {
|
|
1114
|
+
try {
|
|
1115
|
+
await fs2.unlink(lockfilePath);
|
|
1116
|
+
} catch {
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1122
|
+
throw err;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
throw new Error(`Failed to acquire lock after ${timeoutMs}ms`);
|
|
1126
|
+
}
|
|
1127
|
+
async function readRegistryFile(filePath) {
|
|
1128
|
+
try {
|
|
1129
|
+
const content = await fs2.readFile(filePath, "utf-8");
|
|
1130
|
+
const parsed = JSON.parse(content);
|
|
1131
|
+
if (parsed.instances && Array.isArray(parsed.instances)) {
|
|
1132
|
+
parsed.instances = new Map(parsed.instances);
|
|
1133
|
+
}
|
|
1134
|
+
return parsed;
|
|
1135
|
+
} catch (err) {
|
|
1136
|
+
if (err.code === "ENOENT") {
|
|
1137
|
+
return {
|
|
1138
|
+
version: 1,
|
|
1139
|
+
instances: /* @__PURE__ */ new Map(),
|
|
1140
|
+
protectedPatterns: ["wrongstack", "node"],
|
|
1141
|
+
lastCleanup: Date.now()
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
throw err;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
async function writeRegistryFile(filePath, data) {
|
|
1148
|
+
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
1149
|
+
const content = JSON.stringify(data, (_k, v) => {
|
|
1150
|
+
if (v instanceof Map) {
|
|
1151
|
+
return Array.from(v.entries());
|
|
1152
|
+
}
|
|
1153
|
+
return v;
|
|
1154
|
+
}, 2);
|
|
1155
|
+
await fs2.writeFile(tmpPath, content, "utf-8");
|
|
1156
|
+
await fs2.rename(tmpPath, filePath);
|
|
1157
|
+
}
|
|
1158
|
+
var PersistentProcessRegistry = class {
|
|
1159
|
+
instanceId;
|
|
1160
|
+
registryPath;
|
|
1161
|
+
lockPath;
|
|
1162
|
+
baseRegistry;
|
|
1163
|
+
heartbeatInterval = null;
|
|
1164
|
+
isShuttingDown = false;
|
|
1165
|
+
constructor(baseRegistry) {
|
|
1166
|
+
this.instanceId = generateInstanceId();
|
|
1167
|
+
const homeDir = os2.homedir();
|
|
1168
|
+
this.registryPath = path3.join(homeDir, REGISTRY_FILE);
|
|
1169
|
+
this.lockPath = path3.join(homeDir, LOCKFILE);
|
|
1170
|
+
this.baseRegistry = baseRegistry ?? getProcessRegistry();
|
|
1171
|
+
this.ensureDirectory().catch((err) => {
|
|
1172
|
+
console.error("PersistentProcessRegistry: failed to create .wrongstack dir", err);
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
async ensureDirectory() {
|
|
1176
|
+
const dir = path3.dirname(this.registryPath);
|
|
1177
|
+
try {
|
|
1178
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
1179
|
+
} catch (err) {
|
|
1180
|
+
if (err.code !== "EEXIST") throw err;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Start the heartbeat and periodic cleanup tasks.
|
|
1185
|
+
*/
|
|
1186
|
+
start() {
|
|
1187
|
+
if (this.heartbeatInterval) return;
|
|
1188
|
+
this.syncToPersistent();
|
|
1189
|
+
this.heartbeatInterval = setInterval(() => {
|
|
1190
|
+
this.heartbeat();
|
|
1191
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
1192
|
+
this.heartbeatInterval.unref?.();
|
|
1193
|
+
setInterval(() => {
|
|
1194
|
+
this.cleanupStaleEntries();
|
|
1195
|
+
}, STALE_THRESHOLD_MS).unref?.();
|
|
1196
|
+
this.registerMainProcess();
|
|
1197
|
+
process.on("exit", () => this.syncToPersistent());
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Stop the heartbeat and clean up.
|
|
1201
|
+
*/
|
|
1202
|
+
stop() {
|
|
1203
|
+
this.isShuttingDown = true;
|
|
1204
|
+
if (this.heartbeatInterval) {
|
|
1205
|
+
clearInterval(this.heartbeatInterval);
|
|
1206
|
+
this.heartbeatInterval = null;
|
|
1207
|
+
}
|
|
1208
|
+
this.syncToPersistent();
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Register the main WrongStack process as protected.
|
|
1212
|
+
*/
|
|
1213
|
+
registerMainProcess() {
|
|
1214
|
+
const mainPid = process.pid;
|
|
1215
|
+
this.updatePersistentEntry({
|
|
1216
|
+
pid: mainPid,
|
|
1217
|
+
name: "wrongstack-main",
|
|
1218
|
+
command: process.argv.slice(0, 3).join(" "),
|
|
1219
|
+
startedAt: Date.now(),
|
|
1220
|
+
lastHeartbeat: Date.now(),
|
|
1221
|
+
instanceId: this.instanceId,
|
|
1222
|
+
hostname: os2.hostname(),
|
|
1223
|
+
protected: true,
|
|
1224
|
+
spawnMode: "main",
|
|
1225
|
+
parentPid: process.ppid,
|
|
1226
|
+
platform: process.platform
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Register a spawned child process with the persistent registry.
|
|
1231
|
+
*/
|
|
1232
|
+
registerChildProcess(pid, name, command, sessionId, spawnMode = "spawn") {
|
|
1233
|
+
const entry = {
|
|
1234
|
+
pid,
|
|
1235
|
+
name,
|
|
1236
|
+
command,
|
|
1237
|
+
startedAt: Date.now(),
|
|
1238
|
+
lastHeartbeat: Date.now(),
|
|
1239
|
+
instanceId: this.instanceId,
|
|
1240
|
+
hostname: os2.hostname(),
|
|
1241
|
+
protected: true,
|
|
1242
|
+
// All WrongStack child processes are protected by default
|
|
1243
|
+
spawnMode,
|
|
1244
|
+
parentPid: process.pid,
|
|
1245
|
+
platform: process.platform
|
|
1246
|
+
};
|
|
1247
|
+
if (sessionId) {
|
|
1248
|
+
entry.sessionId = sessionId;
|
|
1249
|
+
}
|
|
1250
|
+
this.updatePersistentEntry(entry);
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Update or add an entry in the persistent registry.
|
|
1254
|
+
*/
|
|
1255
|
+
async updatePersistentEntry(entry) {
|
|
1256
|
+
const release = await acquireLock(this.lockPath);
|
|
1257
|
+
try {
|
|
1258
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1259
|
+
data.instances.set(String(entry.pid), entry);
|
|
1260
|
+
this.baseRegistry.register({
|
|
1261
|
+
pid: entry.pid,
|
|
1262
|
+
name: entry.name,
|
|
1263
|
+
command: entry.command,
|
|
1264
|
+
startedAt: entry.startedAt,
|
|
1265
|
+
sessionId: entry.sessionId,
|
|
1266
|
+
protected: entry.protected,
|
|
1267
|
+
child: null
|
|
1268
|
+
// Main process has no child handle
|
|
1269
|
+
});
|
|
1270
|
+
await writeRegistryFile(this.registryPath, data);
|
|
1271
|
+
} finally {
|
|
1272
|
+
await release();
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Unregister a process from the persistent registry.
|
|
1277
|
+
*/
|
|
1278
|
+
async unregister(pid) {
|
|
1279
|
+
const release = await acquireLock(this.lockPath);
|
|
1280
|
+
try {
|
|
1281
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1282
|
+
data.instances.delete(String(pid));
|
|
1283
|
+
await writeRegistryFile(this.registryPath, data);
|
|
1284
|
+
} finally {
|
|
1285
|
+
await release();
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Send heartbeat to mark all this instance's processes as alive.
|
|
1290
|
+
*/
|
|
1291
|
+
heartbeat() {
|
|
1292
|
+
if (this.isShuttingDown) return;
|
|
1293
|
+
this.syncToPersistent();
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Sync this instance's processes to the persistent registry.
|
|
1297
|
+
*/
|
|
1298
|
+
async syncToPersistent() {
|
|
1299
|
+
const release = await acquireLock(this.lockPath);
|
|
1300
|
+
try {
|
|
1301
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1302
|
+
const now = Date.now();
|
|
1303
|
+
const updatedInstances = /* @__PURE__ */ new Map();
|
|
1304
|
+
for (const [_pidStr, entry] of data.instances) {
|
|
1305
|
+
if (entry.instanceId === this.instanceId) {
|
|
1306
|
+
entry.lastHeartbeat = now;
|
|
1307
|
+
}
|
|
1308
|
+
if (entry.instanceId === this.instanceId || now - entry.lastHeartbeat < STALE_THRESHOLD_MS) {
|
|
1309
|
+
updatedInstances.set(_pidStr, entry);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
data.instances = updatedInstances;
|
|
1313
|
+
data.lastCleanup = now;
|
|
1314
|
+
await writeRegistryFile(this.registryPath, data);
|
|
1315
|
+
} catch (err) {
|
|
1316
|
+
console.error("PersistentProcessRegistry: sync failed", err);
|
|
1317
|
+
} finally {
|
|
1318
|
+
await release();
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Remove entries for processes that are no longer running.
|
|
1323
|
+
*/
|
|
1324
|
+
async cleanupStaleEntries() {
|
|
1325
|
+
const release = await acquireLock(this.lockPath);
|
|
1326
|
+
try {
|
|
1327
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1328
|
+
const now = Date.now();
|
|
1329
|
+
const stalePids = [];
|
|
1330
|
+
for (const [_pidStr, entry] of data.instances) {
|
|
1331
|
+
const age = now - entry.lastHeartbeat;
|
|
1332
|
+
if (age > STALE_THRESHOLD_MS) {
|
|
1333
|
+
try {
|
|
1334
|
+
if (process.platform !== "win32") {
|
|
1335
|
+
process.kill(entry.pid, 0);
|
|
1336
|
+
} else {
|
|
1337
|
+
console.log(`PersistentProcessRegistry: checking stale pid ${entry.pid} (${age}ms old)`);
|
|
1338
|
+
}
|
|
1339
|
+
} catch {
|
|
1340
|
+
stalePids.push(_pidStr);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
if (stalePids.length > 0) {
|
|
1345
|
+
for (const pidStr of stalePids) {
|
|
1346
|
+
data.instances.delete(pidStr);
|
|
1347
|
+
}
|
|
1348
|
+
await writeRegistryFile(this.registryPath, data);
|
|
1349
|
+
}
|
|
1350
|
+
} catch (err) {
|
|
1351
|
+
console.error("PersistentProcessRegistry: cleanup failed", err);
|
|
1352
|
+
} finally {
|
|
1353
|
+
await release();
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* Check if a PID belongs to a WrongStack process and should be protected.
|
|
1358
|
+
*/
|
|
1359
|
+
async isProtectedPid(pid) {
|
|
1360
|
+
const release = await acquireLock(this.lockPath);
|
|
1361
|
+
try {
|
|
1362
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1363
|
+
const entry = data.instances.get(String(pid));
|
|
1364
|
+
if (!entry) return false;
|
|
1365
|
+
if (Date.now() - entry.lastHeartbeat > STALE_THRESHOLD_MS) {
|
|
1366
|
+
return false;
|
|
1367
|
+
}
|
|
1368
|
+
return entry.protected;
|
|
1369
|
+
} finally {
|
|
1370
|
+
await release();
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Get all protected PIDs from all WrongStack instances.
|
|
1375
|
+
*/
|
|
1376
|
+
async getAllProtectedPids() {
|
|
1377
|
+
const release = await acquireLock(this.lockPath);
|
|
1378
|
+
try {
|
|
1379
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1380
|
+
const now = Date.now();
|
|
1381
|
+
const protectedPids = [];
|
|
1382
|
+
for (const [_pidStr, entry] of data.instances) {
|
|
1383
|
+
if (entry.protected && now - entry.lastHeartbeat < STALE_THRESHOLD_MS) {
|
|
1384
|
+
protectedPids.push(entry.pid);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return protectedPids;
|
|
1388
|
+
} finally {
|
|
1389
|
+
await release();
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Get complete status of all tracked processes across all instances.
|
|
1394
|
+
*/
|
|
1395
|
+
async getGlobalStatus() {
|
|
1396
|
+
const release = await acquireLock(this.lockPath);
|
|
1397
|
+
try {
|
|
1398
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1399
|
+
const now = Date.now();
|
|
1400
|
+
const instances = /* @__PURE__ */ new Map();
|
|
1401
|
+
let protectedCount = 0;
|
|
1402
|
+
let staleCount = 0;
|
|
1403
|
+
for (const [_pidStr, entry] of data.instances) {
|
|
1404
|
+
const instanceEntries = instances.get(entry.instanceId) ?? [];
|
|
1405
|
+
instanceEntries.push(entry);
|
|
1406
|
+
instances.set(entry.instanceId, instanceEntries);
|
|
1407
|
+
if (entry.protected) protectedCount++;
|
|
1408
|
+
if (now - entry.lastHeartbeat > STALE_THRESHOLD_MS) staleCount++;
|
|
1409
|
+
}
|
|
1410
|
+
return {
|
|
1411
|
+
instances,
|
|
1412
|
+
totalProcesses: data.instances.size,
|
|
1413
|
+
protectedCount,
|
|
1414
|
+
staleCount
|
|
1415
|
+
};
|
|
1416
|
+
} finally {
|
|
1417
|
+
await release();
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Get the instance ID for this process.
|
|
1422
|
+
*/
|
|
1423
|
+
getInstanceId() {
|
|
1424
|
+
return this.instanceId;
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Check if a kill command should be blocked.
|
|
1428
|
+
* Returns true if the kill should be blocked (target is a WrongStack process).
|
|
1429
|
+
*/
|
|
1430
|
+
async shouldBlockKill(pid) {
|
|
1431
|
+
const protectedPids = await this.getAllProtectedPids();
|
|
1432
|
+
return protectedPids.includes(pid);
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Add a pattern-based protection rule.
|
|
1436
|
+
* Processes whose command matches any protected pattern are protected.
|
|
1437
|
+
*/
|
|
1438
|
+
async addProtectedPattern(pattern) {
|
|
1439
|
+
const release = await acquireLock(this.lockPath);
|
|
1440
|
+
try {
|
|
1441
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1442
|
+
if (!data.protectedPatterns.includes(pattern)) {
|
|
1443
|
+
data.protectedPatterns.push(pattern);
|
|
1444
|
+
await writeRegistryFile(this.registryPath, data);
|
|
1445
|
+
}
|
|
1446
|
+
} finally {
|
|
1447
|
+
await release();
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
var _persistentRegistry;
|
|
1452
|
+
function getPersistentProcessRegistry() {
|
|
1453
|
+
if (!_persistentRegistry) {
|
|
1454
|
+
_persistentRegistry = new PersistentProcessRegistry();
|
|
1455
|
+
}
|
|
1456
|
+
return _persistentRegistry;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// src/bash-kill-guard.ts
|
|
1460
|
+
function extractKillCommand(command) {
|
|
1461
|
+
const normalized = command.replace(/\s+/g, " ").trim();
|
|
1462
|
+
const shellCMatch = normalized.match(
|
|
1463
|
+
/^(?:\/\w+)?\/?(?:bin|usr)\/(?:ba)?sh\s+-[c]\s+['"](.+?)['"]$/
|
|
1464
|
+
);
|
|
1465
|
+
if (shellCMatch?.[1]) {
|
|
1466
|
+
const inner = shellCMatch[1].trim();
|
|
1467
|
+
return isKillRelatedCommand(inner) ? inner : null;
|
|
1468
|
+
}
|
|
1469
|
+
const shellCUnquoted = normalized.match(
|
|
1470
|
+
/^(?:\/\w+)?\/?(?:bin|usr)\/(?:ba)?sh\s+-[c]\s+(kill(?:\s+-[a-zA-Z]+)?(?:\s+\d+)+)$/
|
|
1471
|
+
);
|
|
1472
|
+
if (shellCUnquoted?.[1]) {
|
|
1473
|
+
return shellCUnquoted[1];
|
|
1474
|
+
}
|
|
1475
|
+
return null;
|
|
1476
|
+
}
|
|
1477
|
+
function isKillRelatedCommand(cmd) {
|
|
1478
|
+
const normalized = cmd.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1479
|
+
if (/^kill(\s|$)/.test(normalized)) return true;
|
|
1480
|
+
if (/^(pkill|killall|pgrep|skill)\s/.test(normalized)) return true;
|
|
1481
|
+
if (/^taskkill\s/i.test(normalized)) return true;
|
|
1482
|
+
if (/^tskill\s/i.test(normalized)) return true;
|
|
1483
|
+
if (/^\/proc\/\d+\/(?:kill|fd)/.test(normalized)) return true;
|
|
1484
|
+
return false;
|
|
1485
|
+
}
|
|
1486
|
+
function parseKillCommand(command) {
|
|
1487
|
+
const normalized = command.replace(/\s+/g, " ").trim();
|
|
1488
|
+
const simpleMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z]+)\s+)?(\d+|-?\d+)$/);
|
|
1489
|
+
if (simpleMatch) {
|
|
1490
|
+
const signal = simpleMatch[1] ?? "-TERM";
|
|
1491
|
+
const pidOrGroup = simpleMatch[2];
|
|
1492
|
+
if (!pidOrGroup) return null;
|
|
1493
|
+
const isGroupKill = pidOrGroup.startsWith("-");
|
|
1494
|
+
const pid = isGroupKill ? parseInt(pidOrGroup.slice(1), 10) : parseInt(pidOrGroup, 10);
|
|
1495
|
+
return {
|
|
1496
|
+
pid,
|
|
1497
|
+
signal: signal.slice(1),
|
|
1498
|
+
isGroupKill,
|
|
1499
|
+
isAllKill: false,
|
|
1500
|
+
originalCommand: command
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
const pkillMatch = normalized.match(/^pkill\s+(?:(-[a-zA-Z]+)\s+)?(.+)$/);
|
|
1504
|
+
if (pkillMatch?.[2]) {
|
|
1505
|
+
const name = pkillMatch[2];
|
|
1506
|
+
const signalMatch = pkillMatch[1];
|
|
1507
|
+
return {
|
|
1508
|
+
name,
|
|
1509
|
+
signal: signalMatch ? signalMatch.slice(1) : "TERM",
|
|
1510
|
+
isGroupKill: false,
|
|
1511
|
+
isAllKill: false,
|
|
1512
|
+
originalCommand: command
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
const killallMatch = normalized.match(/^killall\s+(?:(-[a-zA-Z]+)\s+)?(.+)$/);
|
|
1516
|
+
if (killallMatch?.[2]) {
|
|
1517
|
+
const name = killallMatch[2];
|
|
1518
|
+
const signalMatch = killallMatch[1];
|
|
1519
|
+
return {
|
|
1520
|
+
name,
|
|
1521
|
+
signal: signalMatch ? signalMatch.slice(1) : "TERM",
|
|
1522
|
+
isGroupKill: false,
|
|
1523
|
+
isAllKill: false,
|
|
1524
|
+
originalCommand: command
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
const pgrepMatch = normalized.match(/^pgrep\s+(.+)$/);
|
|
1528
|
+
if (pgrepMatch) {
|
|
1529
|
+
return null;
|
|
1530
|
+
}
|
|
1531
|
+
const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
|
|
1532
|
+
if (taskkillMatch?.[1]) {
|
|
1533
|
+
const pidStr = taskkillMatch[1];
|
|
1534
|
+
return {
|
|
1535
|
+
pid: parseInt(pidStr, 10),
|
|
1536
|
+
signal: normalized.includes("/F") ? "FORCE" : "TERM",
|
|
1537
|
+
isGroupKill: false,
|
|
1538
|
+
isAllKill: false,
|
|
1539
|
+
originalCommand: command
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
|
|
1543
|
+
if (tskillMatch?.[1]) {
|
|
1544
|
+
const pidStr = tskillMatch[1];
|
|
1545
|
+
return {
|
|
1546
|
+
pid: parseInt(pidStr, 10),
|
|
1547
|
+
signal: "TERM",
|
|
1548
|
+
isGroupKill: false,
|
|
1549
|
+
isAllKill: false,
|
|
1550
|
+
originalCommand: command
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
return null;
|
|
1554
|
+
}
|
|
1555
|
+
async function getProtectedEntries() {
|
|
1556
|
+
const registry = getPersistentProcessRegistry();
|
|
1557
|
+
const status = await registry.getGlobalStatus();
|
|
1558
|
+
const entries = [];
|
|
1559
|
+
for (const instanceEntries of status.instances.values()) {
|
|
1560
|
+
for (const entry of instanceEntries) {
|
|
1561
|
+
if (entry.protected && Date.now() - entry.lastHeartbeat < 3e4) {
|
|
1562
|
+
entries.push(entry);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
return entries;
|
|
1567
|
+
}
|
|
1568
|
+
async function isKillProtected(kill) {
|
|
1569
|
+
const registry = getPersistentProcessRegistry();
|
|
1570
|
+
if (kill.name) {
|
|
1571
|
+
const entries = await getProtectedEntries();
|
|
1572
|
+
const killNameLower = kill.name.toLowerCase();
|
|
1573
|
+
for (const entry of entries) {
|
|
1574
|
+
if (entry.name && entry.name.toLowerCase().includes(killNameLower)) {
|
|
1575
|
+
return true;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
if (killNameLower.includes("wrongstack")) {
|
|
1579
|
+
return true;
|
|
1580
|
+
}
|
|
1581
|
+
if (killNameLower.includes("node") && entries.length > 0) {
|
|
1582
|
+
return true;
|
|
1583
|
+
}
|
|
1584
|
+
return false;
|
|
1585
|
+
}
|
|
1586
|
+
if (kill.isGroupKill) {
|
|
1587
|
+
const protectedPids = await registry.getAllProtectedPids();
|
|
1588
|
+
return protectedPids.length > 0;
|
|
1589
|
+
}
|
|
1590
|
+
if (kill.pid !== void 0) {
|
|
1591
|
+
return registry.shouldBlockKill(kill.pid);
|
|
1592
|
+
}
|
|
1593
|
+
return false;
|
|
1594
|
+
}
|
|
1595
|
+
async function checkAndBlockKillCommand(command) {
|
|
1596
|
+
const normalized = command.replace(/\s+/g, " ").trim();
|
|
1597
|
+
const killCmd = extractKillCommand(normalized) || (isKillRelatedCommand(normalized) ? normalized : null);
|
|
1598
|
+
if (!killCmd) {
|
|
1599
|
+
return { blocked: false };
|
|
1600
|
+
}
|
|
1601
|
+
const parsed = parseKillCommand(killCmd);
|
|
1602
|
+
if (!parsed) {
|
|
1603
|
+
if (killCmd.includes("kill") && /kill\s+.*\|/.test(killCmd)) {
|
|
1604
|
+
return {
|
|
1605
|
+
blocked: true,
|
|
1606
|
+
reason: `Blocked: complex kill pipeline detected \u2014 "${killCmd.slice(0, 50)}..."`
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
return { blocked: false };
|
|
1610
|
+
}
|
|
1611
|
+
if (await isKillProtected(parsed)) {
|
|
1612
|
+
let target;
|
|
1613
|
+
if (parsed.name) {
|
|
1614
|
+
target = `process name "${parsed.name}"`;
|
|
1615
|
+
} else if (parsed.pid !== void 0) {
|
|
1616
|
+
target = `PID ${parsed.pid}`;
|
|
1617
|
+
} else {
|
|
1618
|
+
target = "(unknown target)";
|
|
1619
|
+
}
|
|
1620
|
+
const signal = parsed.signal ? ` (${parsed.signal})` : "";
|
|
1621
|
+
const groupNote = parsed.isGroupKill ? " (process group)" : "";
|
|
1622
|
+
return {
|
|
1623
|
+
blocked: true,
|
|
1624
|
+
reason: `Blocked: kill${signal} ${target}${groupNote} targets a protected WrongStack process.`
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
return { blocked: false };
|
|
1628
|
+
}
|
|
1070
1629
|
|
|
1071
1630
|
// src/bash.ts
|
|
1072
1631
|
var MAX_OUTPUT = 32768;
|
|
@@ -1136,6 +1695,20 @@ var bashTool = {
|
|
|
1136
1695
|
};
|
|
1137
1696
|
return;
|
|
1138
1697
|
}
|
|
1698
|
+
const killCheck = await checkAndBlockKillCommand(input.command);
|
|
1699
|
+
if (killCheck.blocked) {
|
|
1700
|
+
yield {
|
|
1701
|
+
type: "final",
|
|
1702
|
+
output: {
|
|
1703
|
+
output: "",
|
|
1704
|
+
exit_code: 1,
|
|
1705
|
+
timed_out: false,
|
|
1706
|
+
pid: null,
|
|
1707
|
+
error: killCheck.reason || "Kill command blocked: targets a protected WrongStack process."
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1139
1712
|
const PIPE_TO_SHELL_PATTERN = /\|\s*(sh|bash|ksh|zsh|fish|cmd|powershell|pwsh)/i;
|
|
1140
1713
|
if (PIPE_TO_SHELL_PATTERN.test(input.command)) {
|
|
1141
1714
|
console.warn(JSON.stringify({
|
|
@@ -1148,7 +1721,7 @@ var bashTool = {
|
|
|
1148
1721
|
}));
|
|
1149
1722
|
}
|
|
1150
1723
|
const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
|
|
1151
|
-
const isWin3 =
|
|
1724
|
+
const isWin3 = os2.platform() === "win32";
|
|
1152
1725
|
const shell = (() => {
|
|
1153
1726
|
const explicit = process.env[isWin3 ? "WRONGSTACK_COMSPEC" : "WRONGSTACK_SHELL"];
|
|
1154
1727
|
if (explicit) return explicit;
|
|
@@ -1762,7 +2335,7 @@ function loadDatabaseSync() {
|
|
|
1762
2335
|
DatabaseSyncCtor = req("node:sqlite").DatabaseSync;
|
|
1763
2336
|
} catch (err) {
|
|
1764
2337
|
throw new Error(
|
|
1765
|
-
`The codebase index needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${toErrorMessage(err)}`
|
|
2338
|
+
`The codebase index needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${toErrorMessage$1(err)}`
|
|
1766
2339
|
);
|
|
1767
2340
|
}
|
|
1768
2341
|
return DatabaseSyncCtor;
|
|
@@ -1911,33 +2484,53 @@ var IndexStore = class {
|
|
|
1911
2484
|
}
|
|
1912
2485
|
}
|
|
1913
2486
|
// ─── Symbol CRUD ─────────────────────────────────────────────────────────────
|
|
1914
|
-
|
|
2487
|
+
/**
|
|
2488
|
+
* Insert symbols, assigning IDs atomically inside `BEGIN IMMEDIATE` /
|
|
2489
|
+
* `COMMIT`. The ID allocation (`SELECT MAX(id)`) and all `INSERT`s share
|
|
2490
|
+
* the same transaction, preventing UNIQUE constraint violations when two
|
|
2491
|
+
* processes index concurrently (each would see a different `MAX(id)` and
|
|
2492
|
+
* neither can insert with the other's IDs).
|
|
2493
|
+
*
|
|
2494
|
+
* @returns The symbols array with `id` fields populated so the caller can
|
|
2495
|
+
* use them for refs without re-reading from the DB.
|
|
2496
|
+
*/
|
|
2497
|
+
insertSymbols(symbols) {
|
|
1915
2498
|
return this.runWithRetry(() => {
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
stmt.run(
|
|
1924
|
-
id,
|
|
1925
|
-
s.lang,
|
|
1926
|
-
s.kind,
|
|
1927
|
-
s.name,
|
|
1928
|
-
s.file,
|
|
1929
|
-
s.line,
|
|
1930
|
-
s.col,
|
|
1931
|
-
s.signature,
|
|
1932
|
-
s.docComment,
|
|
1933
|
-
s.scope,
|
|
1934
|
-
s.text,
|
|
1935
|
-
s.file
|
|
2499
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
2500
|
+
try {
|
|
2501
|
+
const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
|
|
2502
|
+
let nextId = (maxRows[0]?.m ?? 0) + 1;
|
|
2503
|
+
const stmt = this.db.prepare(
|
|
2504
|
+
`INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
|
|
2505
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1936
2506
|
);
|
|
1937
|
-
ftsStmt
|
|
1938
|
-
|
|
2507
|
+
const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
|
|
2508
|
+
const result = [];
|
|
2509
|
+
for (const s of symbols) {
|
|
2510
|
+
const id = nextId++;
|
|
2511
|
+
stmt.run(
|
|
2512
|
+
id,
|
|
2513
|
+
s.lang,
|
|
2514
|
+
s.kind,
|
|
2515
|
+
s.name,
|
|
2516
|
+
s.file,
|
|
2517
|
+
s.line,
|
|
2518
|
+
s.col,
|
|
2519
|
+
s.signature,
|
|
2520
|
+
s.docComment,
|
|
2521
|
+
s.scope,
|
|
2522
|
+
s.text,
|
|
2523
|
+
s.file
|
|
2524
|
+
);
|
|
2525
|
+
ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
|
|
2526
|
+
result.push({ ...s, id });
|
|
2527
|
+
}
|
|
2528
|
+
this.db.exec("COMMIT");
|
|
2529
|
+
return result;
|
|
2530
|
+
} catch (err) {
|
|
2531
|
+
this.db.exec("ROLLBACK");
|
|
2532
|
+
throw err;
|
|
1939
2533
|
}
|
|
1940
|
-
return id;
|
|
1941
2534
|
});
|
|
1942
2535
|
}
|
|
1943
2536
|
deleteSymbolsForFile(file) {
|
|
@@ -2491,10 +3084,10 @@ function detectLang(file) {
|
|
|
2491
3084
|
if (idx < 0) return null;
|
|
2492
3085
|
return extToLang(file.slice(idx));
|
|
2493
3086
|
}
|
|
2494
|
-
function parseSymbols2(opts) {
|
|
3087
|
+
async function parseSymbols2(opts) {
|
|
2495
3088
|
const { file, content, lang } = opts;
|
|
2496
3089
|
try {
|
|
2497
|
-
return syncGoParse(file, content, lang);
|
|
3090
|
+
return await syncGoParse(file, content, lang);
|
|
2498
3091
|
} catch {
|
|
2499
3092
|
return { file, lang, symbols: [], mtimeMs: Date.now() };
|
|
2500
3093
|
}
|
|
@@ -2731,19 +3324,34 @@ func formatType(t ast.Expr) string {
|
|
|
2731
3324
|
}
|
|
2732
3325
|
}
|
|
2733
3326
|
`;
|
|
2734
|
-
function syncGoParse(filePath, content, lang) {
|
|
2735
|
-
const tmpDir = path3.join(
|
|
3327
|
+
async function syncGoParse(filePath, content, lang) {
|
|
3328
|
+
const tmpDir = path3.join(os2.tmpdir(), "ws-go-parse");
|
|
2736
3329
|
try {
|
|
2737
|
-
|
|
3330
|
+
await fs2.mkdir(tmpDir, { recursive: true });
|
|
2738
3331
|
const scriptPath = path3.join(tmpDir, "parse.go");
|
|
2739
|
-
|
|
2740
|
-
const
|
|
2741
|
-
|
|
2742
|
-
timeout: 15e3,
|
|
2743
|
-
encoding: "utf8",
|
|
3332
|
+
await fs2.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
|
|
3333
|
+
const proc = spawn("go", ["run", scriptPath], {
|
|
3334
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2744
3335
|
windowsHide: true
|
|
2745
3336
|
});
|
|
2746
|
-
|
|
3337
|
+
let stdout = "";
|
|
3338
|
+
proc.stdout?.on("data", (chunk) => {
|
|
3339
|
+
stdout += chunk.toString();
|
|
3340
|
+
});
|
|
3341
|
+
proc.stdin?.write(content);
|
|
3342
|
+
proc.stdin?.end();
|
|
3343
|
+
const { code } = await Promise.race([
|
|
3344
|
+
new Promise((resolve6) => {
|
|
3345
|
+
proc.on("close", (c) => resolve6({ code: c }));
|
|
3346
|
+
}),
|
|
3347
|
+
new Promise(
|
|
3348
|
+
(_, reject) => setTimeout(() => {
|
|
3349
|
+
proc.kill("SIGKILL");
|
|
3350
|
+
reject(new Error("timeout"));
|
|
3351
|
+
}, 15e3)
|
|
3352
|
+
)
|
|
3353
|
+
]).catch(() => ({ code: -1 }));
|
|
3354
|
+
if (code !== 0 || !stdout.trim()) {
|
|
2747
3355
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
2748
3356
|
}
|
|
2749
3357
|
const raw = JSON.parse(stdout.trim());
|
|
@@ -2765,10 +3373,10 @@ function syncGoParse(filePath, content, lang) {
|
|
|
2765
3373
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
2766
3374
|
}
|
|
2767
3375
|
}
|
|
2768
|
-
function parseSymbols3(opts) {
|
|
3376
|
+
async function parseSymbols3(opts) {
|
|
2769
3377
|
const { file, lang } = opts;
|
|
2770
3378
|
try {
|
|
2771
|
-
return syncPyParse(file, lang);
|
|
3379
|
+
return await syncPyParse(file, lang);
|
|
2772
3380
|
} catch {
|
|
2773
3381
|
return { file, lang, symbols: [], mtimeMs: Date.now() };
|
|
2774
3382
|
}
|
|
@@ -2977,18 +3585,32 @@ visitor.visit(tree)
|
|
|
2977
3585
|
|
|
2978
3586
|
print(json.dumps([s.to_dict() for s in syms]))
|
|
2979
3587
|
`;
|
|
2980
|
-
function syncPyParse(filePath, lang) {
|
|
3588
|
+
async function syncPyParse(filePath, lang) {
|
|
2981
3589
|
try {
|
|
2982
|
-
const tmpDir = path3.join(
|
|
2983
|
-
|
|
3590
|
+
const tmpDir = path3.join(os2.tmpdir(), "ws-py-parse");
|
|
3591
|
+
await fs2.mkdir(tmpDir, { recursive: true });
|
|
2984
3592
|
const scriptPath = path3.join(tmpDir, "parse.py");
|
|
2985
|
-
|
|
2986
|
-
const
|
|
2987
|
-
|
|
2988
|
-
encoding: "utf8",
|
|
3593
|
+
await fs2.writeFile(scriptPath, PY_PARSE_SCRIPT, "utf8");
|
|
3594
|
+
const proc = spawn("python", [scriptPath, filePath], {
|
|
3595
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2989
3596
|
windowsHide: true
|
|
2990
3597
|
});
|
|
2991
|
-
|
|
3598
|
+
let stdout = "";
|
|
3599
|
+
proc.stdout?.on("data", (chunk) => {
|
|
3600
|
+
stdout += chunk.toString();
|
|
3601
|
+
});
|
|
3602
|
+
const { code } = await Promise.race([
|
|
3603
|
+
new Promise((resolve6) => {
|
|
3604
|
+
proc.on("close", (c) => resolve6({ code: c }));
|
|
3605
|
+
}),
|
|
3606
|
+
new Promise(
|
|
3607
|
+
(_, reject) => setTimeout(() => {
|
|
3608
|
+
proc.kill("SIGKILL");
|
|
3609
|
+
reject(new Error("timeout"));
|
|
3610
|
+
}, 15e3)
|
|
3611
|
+
)
|
|
3612
|
+
]).catch(() => ({ code: -1 }));
|
|
3613
|
+
if (code !== 0 || !stdout.trim()) {
|
|
2992
3614
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
2993
3615
|
}
|
|
2994
3616
|
const raw = JSON.parse(stdout.trim());
|
|
@@ -3010,11 +3632,11 @@ function syncPyParse(filePath, lang) {
|
|
|
3010
3632
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
3011
3633
|
}
|
|
3012
3634
|
}
|
|
3013
|
-
function parseSymbols4(opts) {
|
|
3635
|
+
async function parseSymbols4(opts) {
|
|
3014
3636
|
const { file, content, lang } = opts;
|
|
3015
3637
|
const nativeAvailable = checkNativeParser();
|
|
3016
3638
|
if (nativeAvailable) {
|
|
3017
|
-
const result = tryNativeParse(file, content);
|
|
3639
|
+
const result = await tryNativeParse(file, content);
|
|
3018
3640
|
if (result) return result;
|
|
3019
3641
|
}
|
|
3020
3642
|
return regexParse({ file, content, lang });
|
|
@@ -3044,25 +3666,34 @@ function checkNativeParser() {
|
|
|
3044
3666
|
return false;
|
|
3045
3667
|
}
|
|
3046
3668
|
}
|
|
3047
|
-
function tryNativeParse(file, content) {
|
|
3669
|
+
async function tryNativeParse(file, content) {
|
|
3048
3670
|
try {
|
|
3049
3671
|
const toolsDir = path3.join(process.cwd(), "tools");
|
|
3050
3672
|
const crateDir = path3.join(toolsDir, "syn-parser");
|
|
3051
3673
|
const tmpFile = path3.join(crateDir, "src", "input.rs");
|
|
3052
|
-
|
|
3053
|
-
const
|
|
3054
|
-
|
|
3055
|
-
["
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3674
|
+
await fs2.writeFile(tmpFile, content, "utf8");
|
|
3675
|
+
const proc = spawn("cargo", ["run", "--manifest-path", path3.join(toolsDir, "Cargo.toml")], {
|
|
3676
|
+
cwd: process.cwd(),
|
|
3677
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3678
|
+
windowsHide: true
|
|
3679
|
+
});
|
|
3680
|
+
let stdout = "";
|
|
3681
|
+
proc.stdout?.on("data", (chunk) => {
|
|
3682
|
+
stdout += chunk.toString();
|
|
3683
|
+
});
|
|
3684
|
+
const { code } = await Promise.race([
|
|
3685
|
+
new Promise((resolve6) => {
|
|
3686
|
+
proc.on("close", (c) => resolve6({ code: c }));
|
|
3687
|
+
}),
|
|
3688
|
+
new Promise(
|
|
3689
|
+
(_, reject) => setTimeout(() => {
|
|
3690
|
+
proc.kill("SIGKILL");
|
|
3691
|
+
reject(new Error("timeout"));
|
|
3692
|
+
}, 15e3)
|
|
3693
|
+
)
|
|
3694
|
+
]).catch(() => ({ code: -1 }));
|
|
3695
|
+
if (code === 0 && stdout.trim()) {
|
|
3696
|
+
const symbols = JSON.parse(stdout.trim());
|
|
3066
3697
|
return {
|
|
3067
3698
|
file,
|
|
3068
3699
|
lang: "rs",
|
|
@@ -3531,7 +4162,7 @@ function compileGitignore(lines) {
|
|
|
3531
4162
|
async function loadGitignoreMatcher(projectRoot) {
|
|
3532
4163
|
let lines = [];
|
|
3533
4164
|
try {
|
|
3534
|
-
const raw = await
|
|
4165
|
+
const raw = await fs2.readFile(path3.join(projectRoot, ".gitignore"), "utf8");
|
|
3535
4166
|
lines = raw.split("\n");
|
|
3536
4167
|
} catch {
|
|
3537
4168
|
}
|
|
@@ -3589,7 +4220,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
3589
4220
|
}
|
|
3590
4221
|
let entries;
|
|
3591
4222
|
try {
|
|
3592
|
-
entries = await
|
|
4223
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
3593
4224
|
} catch {
|
|
3594
4225
|
return;
|
|
3595
4226
|
}
|
|
@@ -3687,7 +4318,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3687
4318
|
batchFiles.map(async (file) => {
|
|
3688
4319
|
let stat11;
|
|
3689
4320
|
try {
|
|
3690
|
-
stat11 = await
|
|
4321
|
+
stat11 = await fs2.stat(file, statOpts);
|
|
3691
4322
|
} catch (e) {
|
|
3692
4323
|
if (isAbortError(e)) throw e;
|
|
3693
4324
|
return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
|
|
@@ -3701,7 +4332,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3701
4332
|
}
|
|
3702
4333
|
let content;
|
|
3703
4334
|
try {
|
|
3704
|
-
content = await
|
|
4335
|
+
content = await fs2.readFile(file, { encoding: "utf8", signal });
|
|
3705
4336
|
} catch (e) {
|
|
3706
4337
|
if (isAbortError(e)) throw e;
|
|
3707
4338
|
return { file, stat: stat11, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
|
|
@@ -3751,9 +4382,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3751
4382
|
filesIndexed++;
|
|
3752
4383
|
continue;
|
|
3753
4384
|
}
|
|
3754
|
-
const
|
|
3755
|
-
const symbolsWithIds = parsed.symbols.map((s, i) => ({ ...s, id: nextId + i }));
|
|
3756
|
-
store.insertSymbols(symbolsWithIds, nextId);
|
|
4385
|
+
const symbolsWithIds = store.insertSymbols(parsed.symbols);
|
|
3757
4386
|
const count = symbolsWithIds.length;
|
|
3758
4387
|
symbolsIndexed += count;
|
|
3759
4388
|
langStats[lang] = (langStats[lang] ?? 0) + count;
|
|
@@ -3782,7 +4411,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3782
4411
|
}
|
|
3783
4412
|
for (const [file_] of existingMeta) {
|
|
3784
4413
|
try {
|
|
3785
|
-
await
|
|
4414
|
+
await fs2.stat(file_);
|
|
3786
4415
|
} catch {
|
|
3787
4416
|
store.deleteFile(file_);
|
|
3788
4417
|
}
|
|
@@ -4032,10 +4661,10 @@ function circuitOpenError() {
|
|
|
4032
4661
|
"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."
|
|
4033
4662
|
);
|
|
4034
4663
|
}
|
|
4035
|
-
function
|
|
4664
|
+
function isRecoverableConstraintError(err) {
|
|
4036
4665
|
if (err instanceof Error) {
|
|
4037
4666
|
const msg = err.message.toLowerCase();
|
|
4038
|
-
return msg.includes("unique constraint") || msg.includes("
|
|
4667
|
+
return msg.includes("unique constraint") || msg.includes("constraint failed");
|
|
4039
4668
|
}
|
|
4040
4669
|
return false;
|
|
4041
4670
|
}
|
|
@@ -4068,7 +4697,7 @@ async function runStartupIndex(opts) {
|
|
|
4068
4697
|
return result;
|
|
4069
4698
|
} catch (err) {
|
|
4070
4699
|
_lastError = err instanceof Error ? err.message : String(err);
|
|
4071
|
-
if (
|
|
4700
|
+
if (isRecoverableConstraintError(err) && !opts.force) {
|
|
4072
4701
|
_lastError = null;
|
|
4073
4702
|
const rebuildResult = await runStartupIndex({
|
|
4074
4703
|
...opts,
|
|
@@ -4428,9 +5057,9 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
4428
5057
|
const results = [];
|
|
4429
5058
|
for (const file of files) {
|
|
4430
5059
|
const absPath = safeResolve(file, ctx);
|
|
4431
|
-
const stat11 = await
|
|
5060
|
+
const stat11 = await fs2.stat(absPath).catch(() => null);
|
|
4432
5061
|
if (!stat11?.isFile()) continue;
|
|
4433
|
-
const content = await
|
|
5062
|
+
const content = await fs2.readFile(absPath, "utf8");
|
|
4434
5063
|
const lines = content.split(/\r?\n/);
|
|
4435
5064
|
results.push(formatWithLineNumbers(file, lines));
|
|
4436
5065
|
}
|
|
@@ -4490,7 +5119,7 @@ var documentTool = {
|
|
|
4490
5119
|
const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
|
|
4491
5120
|
for (const absPath of fileList) {
|
|
4492
5121
|
try {
|
|
4493
|
-
const content = await
|
|
5122
|
+
const content = await fs2.readFile(absPath, "utf8");
|
|
4494
5123
|
filesProcessed++;
|
|
4495
5124
|
const processed = processFile(
|
|
4496
5125
|
content,
|
|
@@ -4526,7 +5155,7 @@ async function resolveFiles(filesInput, cwd) {
|
|
|
4526
5155
|
for (const f of files) {
|
|
4527
5156
|
const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
|
|
4528
5157
|
try {
|
|
4529
|
-
const stat11 = await
|
|
5158
|
+
const stat11 = await fs2.stat(absPath);
|
|
4530
5159
|
if (stat11.isFile()) resolved.push(absPath);
|
|
4531
5160
|
} catch {
|
|
4532
5161
|
}
|
|
@@ -4619,7 +5248,7 @@ var editTool = {
|
|
|
4619
5248
|
if (input.new_string === void 0) throw new Error("edit: new_string is required");
|
|
4620
5249
|
if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
|
|
4621
5250
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
4622
|
-
const stat11 = await
|
|
5251
|
+
const stat11 = await fs2.stat(absPath).catch((err) => {
|
|
4623
5252
|
if (err.code === "ENOENT") {
|
|
4624
5253
|
throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
|
|
4625
5254
|
}
|
|
@@ -4627,8 +5256,8 @@ var editTool = {
|
|
|
4627
5256
|
});
|
|
4628
5257
|
if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
|
|
4629
5258
|
const autoRead = !ctx.hasRead(absPath);
|
|
4630
|
-
const original = await
|
|
4631
|
-
const updated = await
|
|
5259
|
+
const original = await fs2.readFile(absPath, "utf8");
|
|
5260
|
+
const updated = await fs2.stat(absPath);
|
|
4632
5261
|
const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
|
|
4633
5262
|
const lastReadMtime = ctx.lastReadMtime(absPath);
|
|
4634
5263
|
if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
|
|
@@ -4674,7 +5303,7 @@ var editTool = {
|
|
|
4674
5303
|
const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
|
|
4675
5304
|
const newFile = toStyle(newFileLf, style);
|
|
4676
5305
|
await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
|
|
4677
|
-
const written = await
|
|
5306
|
+
const written = await fs2.stat(absPath);
|
|
4678
5307
|
ctx.recordRead(absPath, written.mtimeMs);
|
|
4679
5308
|
ctx.session.recordFileChange({
|
|
4680
5309
|
path: absPath,
|
|
@@ -5023,8 +5652,8 @@ if (ALLOW_PRIVATE && !process.env["CI"]) {
|
|
|
5023
5652
|
);
|
|
5024
5653
|
}
|
|
5025
5654
|
var combineSignals = (signals) => AbortSignal.any(signals);
|
|
5026
|
-
function guardedLookup(
|
|
5027
|
-
dns.lookup(
|
|
5655
|
+
function guardedLookup(hostname2, options, callback) {
|
|
5656
|
+
dns.lookup(hostname2, { all: true }).then((records) => {
|
|
5028
5657
|
const family = options?.family;
|
|
5029
5658
|
const byFamily = family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;
|
|
5030
5659
|
const list = byFamily.length > 0 ? byFamily : records;
|
|
@@ -5051,7 +5680,7 @@ function guardedLookup(hostname, options, callback) {
|
|
|
5051
5680
|
const first = list.at(0);
|
|
5052
5681
|
if (!first) {
|
|
5053
5682
|
callback(
|
|
5054
|
-
Object.assign(new Error(`fetch: no address for ${
|
|
5683
|
+
Object.assign(new Error(`fetch: no address for ${hostname2}`), { code: "ENOTFOUND" })
|
|
5055
5684
|
);
|
|
5056
5685
|
return;
|
|
5057
5686
|
}
|
|
@@ -5165,7 +5794,13 @@ var fetchTool = {
|
|
|
5165
5794
|
const timer = setTimeout(() => ctrl.abort(new Error("fetch timeout")), TIMEOUT_MS);
|
|
5166
5795
|
const combined = combineSignals([opts.signal, ctrl.signal]);
|
|
5167
5796
|
try {
|
|
5168
|
-
|
|
5797
|
+
let res;
|
|
5798
|
+
try {
|
|
5799
|
+
res = await guardedFetch(input.url, 5, combined);
|
|
5800
|
+
} catch (err) {
|
|
5801
|
+
if (opts.signal.aborted) throw err;
|
|
5802
|
+
throw describeFetchError(err, input.url, ctrl.signal.aborted);
|
|
5803
|
+
}
|
|
5169
5804
|
const ct = res.headers.get("content-type") ?? "application/octet-stream";
|
|
5170
5805
|
if (/^image\/|^audio\/|^video\/|application\/octet-stream/.test(ct)) {
|
|
5171
5806
|
throw new Error(`fetch: refusing to read binary content-type "${ct}"`);
|
|
@@ -5221,9 +5856,9 @@ var fetchTool = {
|
|
|
5221
5856
|
}
|
|
5222
5857
|
}
|
|
5223
5858
|
};
|
|
5224
|
-
async function assertNotPrivate(
|
|
5859
|
+
async function assertNotPrivate(hostname2) {
|
|
5225
5860
|
if (ALLOW_PRIVATE) return;
|
|
5226
|
-
const host =
|
|
5861
|
+
const host = hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
|
|
5227
5862
|
if (host === "localhost" || host.endsWith(".localhost")) {
|
|
5228
5863
|
throw new Error("fetch: blocked localhost target");
|
|
5229
5864
|
}
|
|
@@ -5250,6 +5885,23 @@ async function assertNotPrivate(hostname) {
|
|
|
5250
5885
|
}
|
|
5251
5886
|
}
|
|
5252
5887
|
}
|
|
5888
|
+
function describeFetchError(err, url, timedOut) {
|
|
5889
|
+
if (timedOut) {
|
|
5890
|
+
return new Error(`fetch: GET ${url} timed out after ${TIMEOUT_MS}ms`);
|
|
5891
|
+
}
|
|
5892
|
+
const parts = [];
|
|
5893
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5894
|
+
let cur = err;
|
|
5895
|
+
while (cur instanceof Error && !seen.has(cur)) {
|
|
5896
|
+
seen.add(cur);
|
|
5897
|
+
const code = cur.code;
|
|
5898
|
+
const label = code ? `${code}: ${cur.message}` : cur.message;
|
|
5899
|
+
if (label && label !== "fetch failed" && !parts.includes(label)) parts.push(label);
|
|
5900
|
+
cur = cur.cause;
|
|
5901
|
+
}
|
|
5902
|
+
const detail = parts.length > 0 ? parts.join(" \u2192 ") : "fetch failed";
|
|
5903
|
+
return new Error(`fetch: GET ${url} failed \u2014 ${detail}`);
|
|
5904
|
+
}
|
|
5253
5905
|
function prettyJson(s) {
|
|
5254
5906
|
try {
|
|
5255
5907
|
return JSON.stringify(JSON.parse(s), null, 2);
|
|
@@ -5681,7 +6333,7 @@ var globTool = {
|
|
|
5681
6333
|
}
|
|
5682
6334
|
let entries;
|
|
5683
6335
|
try {
|
|
5684
|
-
entries = await
|
|
6336
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
5685
6337
|
} catch {
|
|
5686
6338
|
return;
|
|
5687
6339
|
}
|
|
@@ -5697,7 +6349,7 @@ var globTool = {
|
|
|
5697
6349
|
} else if (e.isFile()) {
|
|
5698
6350
|
if (re.test(rel) || re.test(name)) {
|
|
5699
6351
|
try {
|
|
5700
|
-
const st = await
|
|
6352
|
+
const st = await fs2.stat(full);
|
|
5701
6353
|
results.push({ rel: full, mtime: st.mtimeMs });
|
|
5702
6354
|
if (results.length >= limit) {
|
|
5703
6355
|
truncated = true;
|
|
@@ -5716,7 +6368,7 @@ var globTool = {
|
|
|
5716
6368
|
};
|
|
5717
6369
|
async function readGitignore(dir) {
|
|
5718
6370
|
try {
|
|
5719
|
-
const raw = await
|
|
6371
|
+
const raw = await fs2.readFile(path3.join(dir, ".gitignore"), "utf8");
|
|
5720
6372
|
return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
5721
6373
|
} catch {
|
|
5722
6374
|
return [];
|
|
@@ -5770,6 +6422,7 @@ function capSubject(line) {
|
|
|
5770
6422
|
|
|
5771
6423
|
// src/grep.ts
|
|
5772
6424
|
var DEFAULT_IGNORE3 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
|
|
6425
|
+
var NATIVE_SCAN_CONCURRENCY = 32;
|
|
5773
6426
|
var grepTool = {
|
|
5774
6427
|
name: "grep",
|
|
5775
6428
|
category: "Search",
|
|
@@ -5997,14 +6650,52 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
5997
6650
|
const fileMatches = /* @__PURE__ */ new Map();
|
|
5998
6651
|
let total = 0;
|
|
5999
6652
|
let stopped = false;
|
|
6653
|
+
const scanFile = async (full, name) => {
|
|
6654
|
+
if (stopped || signal.aborted) return;
|
|
6655
|
+
if (globRe && !globRe.test(name) && !globRe.test(full)) return;
|
|
6656
|
+
if (globRe) globRe.lastIndex = 0;
|
|
6657
|
+
try {
|
|
6658
|
+
const stat11 = await fs2.stat(full);
|
|
6659
|
+
if (stat11.size > 1e6 || stopped || signal.aborted) return;
|
|
6660
|
+
const head = await fs2.readFile(full);
|
|
6661
|
+
if (isBinaryBuffer(head) || stopped || signal.aborted) return;
|
|
6662
|
+
const text = head.toString("utf8");
|
|
6663
|
+
const lines = text.split(/\r?\n/);
|
|
6664
|
+
let fileHits = 0;
|
|
6665
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6666
|
+
if (stopped || signal.aborted) break;
|
|
6667
|
+
const ln = capSubject(lines[i] ?? "");
|
|
6668
|
+
re.lastIndex = 0;
|
|
6669
|
+
if (re.test(ln)) {
|
|
6670
|
+
fileHits++;
|
|
6671
|
+
total++;
|
|
6672
|
+
if (mode === "content" && matches.length < limit) {
|
|
6673
|
+
matches.push(`${full}:${i + 1}:${ln}`);
|
|
6674
|
+
}
|
|
6675
|
+
}
|
|
6676
|
+
}
|
|
6677
|
+
if (fileHits > 0) {
|
|
6678
|
+
fileMatches.set(full, fileHits);
|
|
6679
|
+
if (mode === "files_with_matches" && matches.length < limit) {
|
|
6680
|
+
matches.push(full);
|
|
6681
|
+
}
|
|
6682
|
+
if (mode === "count" && matches.length < limit) {
|
|
6683
|
+
matches.push(`${full}:${fileHits}`);
|
|
6684
|
+
}
|
|
6685
|
+
}
|
|
6686
|
+
if (matches.length >= limit) stopped = true;
|
|
6687
|
+
} catch {
|
|
6688
|
+
}
|
|
6689
|
+
};
|
|
6000
6690
|
const walk = async (dir) => {
|
|
6001
6691
|
if (stopped || signal.aborted) return;
|
|
6002
6692
|
let entries;
|
|
6003
6693
|
try {
|
|
6004
|
-
entries = await
|
|
6694
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
6005
6695
|
} catch {
|
|
6006
6696
|
return;
|
|
6007
6697
|
}
|
|
6698
|
+
const files = [];
|
|
6008
6699
|
for (const e of entries) {
|
|
6009
6700
|
if (stopped) return;
|
|
6010
6701
|
if (DEFAULT_IGNORE3.includes(e.name)) continue;
|
|
@@ -6013,41 +6704,10 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
6013
6704
|
if (e.isDirectory()) {
|
|
6014
6705
|
await walk(full);
|
|
6015
6706
|
} else if (e.isFile()) {
|
|
6016
|
-
|
|
6017
|
-
if (globRe) globRe.lastIndex = 0;
|
|
6018
|
-
try {
|
|
6019
|
-
const stat11 = await fs14.stat(full);
|
|
6020
|
-
if (stat11.size > 1e6) continue;
|
|
6021
|
-
const head = await fs14.readFile(full);
|
|
6022
|
-
if (isBinaryBuffer(head)) continue;
|
|
6023
|
-
const text = head.toString("utf8");
|
|
6024
|
-
const lines = text.split(/\r?\n/);
|
|
6025
|
-
let fileHits = 0;
|
|
6026
|
-
for (let i = 0; i < lines.length; i++) {
|
|
6027
|
-
const ln = capSubject(lines[i] ?? "");
|
|
6028
|
-
re.lastIndex = 0;
|
|
6029
|
-
if (re.test(ln)) {
|
|
6030
|
-
fileHits++;
|
|
6031
|
-
total++;
|
|
6032
|
-
if (mode === "content" && matches.length < limit) {
|
|
6033
|
-
matches.push(`${full}:${i + 1}:${ln}`);
|
|
6034
|
-
}
|
|
6035
|
-
}
|
|
6036
|
-
}
|
|
6037
|
-
if (fileHits > 0) {
|
|
6038
|
-
fileMatches.set(full, fileHits);
|
|
6039
|
-
if (mode === "files_with_matches" && matches.length < limit) {
|
|
6040
|
-
matches.push(full);
|
|
6041
|
-
}
|
|
6042
|
-
if (mode === "count" && matches.length < limit) {
|
|
6043
|
-
matches.push(`${full}:${fileHits}`);
|
|
6044
|
-
}
|
|
6045
|
-
}
|
|
6046
|
-
if (matches.length >= limit) stopped = true;
|
|
6047
|
-
} catch {
|
|
6048
|
-
}
|
|
6707
|
+
files.push({ full, name: e.name });
|
|
6049
6708
|
}
|
|
6050
6709
|
}
|
|
6710
|
+
await mapWithConcurrency(files, NATIVE_SCAN_CONCURRENCY, ({ full, name }) => scanFile(full, name));
|
|
6051
6711
|
};
|
|
6052
6712
|
await walk(base);
|
|
6053
6713
|
return {
|
|
@@ -6057,6 +6717,20 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
6057
6717
|
used: "native"
|
|
6058
6718
|
};
|
|
6059
6719
|
}
|
|
6720
|
+
async function mapWithConcurrency(items, concurrency, fn) {
|
|
6721
|
+
if (items.length === 0) return;
|
|
6722
|
+
let next = 0;
|
|
6723
|
+
const workerCount = Math.min(Math.max(1, concurrency), items.length);
|
|
6724
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
6725
|
+
for (; ; ) {
|
|
6726
|
+
const idx = next++;
|
|
6727
|
+
if (idx >= items.length) return;
|
|
6728
|
+
const item = items[idx];
|
|
6729
|
+
if (item !== void 0) await fn(item);
|
|
6730
|
+
}
|
|
6731
|
+
});
|
|
6732
|
+
await Promise.all(workers);
|
|
6733
|
+
}
|
|
6060
6734
|
var installTool = {
|
|
6061
6735
|
name: "install",
|
|
6062
6736
|
category: "Package Management",
|
|
@@ -6231,7 +6905,7 @@ var jsonTool = {
|
|
|
6231
6905
|
let raw;
|
|
6232
6906
|
if (input.file) {
|
|
6233
6907
|
try {
|
|
6234
|
-
raw = await
|
|
6908
|
+
raw = await fs2.readFile(input.file, "utf8");
|
|
6235
6909
|
} catch {
|
|
6236
6910
|
return { data: null, formatted: "", type: "unknown", error: `Could not read file` };
|
|
6237
6911
|
}
|
|
@@ -6270,8 +6944,8 @@ var jsonTool = {
|
|
|
6270
6944
|
};
|
|
6271
6945
|
}
|
|
6272
6946
|
};
|
|
6273
|
-
function query(data,
|
|
6274
|
-
const parts =
|
|
6947
|
+
function query(data, path21) {
|
|
6948
|
+
const parts = path21.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
6275
6949
|
let current = data;
|
|
6276
6950
|
for (const part of parts) {
|
|
6277
6951
|
if (current === null || current === void 0) return void 0;
|
|
@@ -6548,7 +7222,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
6548
7222
|
}
|
|
6549
7223
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
6550
7224
|
var MAX_TAIL_LINES = 1e5;
|
|
6551
|
-
async function fileLogs(
|
|
7225
|
+
async function fileLogs(path21, lines, filterRe, stream) {
|
|
6552
7226
|
const { createInterface } = await import('node:readline');
|
|
6553
7227
|
const { createReadStream } = await import('node:fs');
|
|
6554
7228
|
const entries = [];
|
|
@@ -6557,7 +7231,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
|
|
|
6557
7231
|
let writeIdx = 0;
|
|
6558
7232
|
let totalLines = 0;
|
|
6559
7233
|
const rl = createInterface({
|
|
6560
|
-
input: createReadStream(
|
|
7234
|
+
input: createReadStream(path21),
|
|
6561
7235
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
6562
7236
|
});
|
|
6563
7237
|
for await (const line of rl) {
|
|
@@ -6578,7 +7252,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
|
|
|
6578
7252
|
if (parsed) entries.push(parsed);
|
|
6579
7253
|
}
|
|
6580
7254
|
return {
|
|
6581
|
-
source:
|
|
7255
|
+
source: path21,
|
|
6582
7256
|
entries,
|
|
6583
7257
|
total: entries.length,
|
|
6584
7258
|
truncated: totalLines > effLines,
|
|
@@ -6779,12 +7453,12 @@ var patchTool = {
|
|
|
6779
7453
|
};
|
|
6780
7454
|
}
|
|
6781
7455
|
}
|
|
6782
|
-
const tmpDir = await
|
|
7456
|
+
const tmpDir = await fs2.mkdtemp(path3.join(os2.tmpdir(), ".wstack_patch_"));
|
|
6783
7457
|
try {
|
|
6784
|
-
await
|
|
7458
|
+
await fs2.chmod(tmpDir, 448).catch(() => {
|
|
6785
7459
|
});
|
|
6786
7460
|
const patchFile = path3.join(tmpDir, "in.diff");
|
|
6787
|
-
await
|
|
7461
|
+
await fs2.writeFile(patchFile, input.patch, { mode: 384 });
|
|
6788
7462
|
const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
|
|
6789
7463
|
const result = await runPatch(args, dir, opts.signal);
|
|
6790
7464
|
if (result.exitCode !== 0 && !dryRun) {
|
|
@@ -6805,7 +7479,7 @@ var patchTool = {
|
|
|
6805
7479
|
message: result.stdout || "patch applied"
|
|
6806
7480
|
};
|
|
6807
7481
|
} finally {
|
|
6808
|
-
await
|
|
7482
|
+
await fs2.rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
6809
7483
|
});
|
|
6810
7484
|
}
|
|
6811
7485
|
}
|
|
@@ -7151,7 +7825,7 @@ var readTool = {
|
|
|
7151
7825
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
7152
7826
|
let stat11;
|
|
7153
7827
|
try {
|
|
7154
|
-
stat11 = await
|
|
7828
|
+
stat11 = await fs2.stat(absPath);
|
|
7155
7829
|
} catch (err) {
|
|
7156
7830
|
const code = err.code;
|
|
7157
7831
|
if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
|
|
@@ -7176,7 +7850,7 @@ var readTool = {
|
|
|
7176
7850
|
note: "Repeated read suppressed to save tokens."
|
|
7177
7851
|
};
|
|
7178
7852
|
}
|
|
7179
|
-
const buf = await
|
|
7853
|
+
const buf = await fs2.readFile(absPath);
|
|
7180
7854
|
if (isBinaryBuffer(buf)) {
|
|
7181
7855
|
throw new Error(`read: "${input.path}" appears to be binary`);
|
|
7182
7856
|
}
|
|
@@ -7322,11 +7996,11 @@ var replaceTool = {
|
|
|
7322
7996
|
const dryRun = input.dry_run ?? false;
|
|
7323
7997
|
const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
|
|
7324
7998
|
const fileList = await resolveFiles2(filesInput, ctx, globRe);
|
|
7325
|
-
const realRoot = await
|
|
7999
|
+
const realRoot = await fs2.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
|
|
7326
8000
|
const results = [];
|
|
7327
8001
|
let totalReplacements = 0;
|
|
7328
8002
|
for (const absPath of fileList) {
|
|
7329
|
-
const lstat2 = await
|
|
8003
|
+
const lstat2 = await fs2.lstat(absPath).catch((err) => {
|
|
7330
8004
|
if (err.code === "ENOENT") return null;
|
|
7331
8005
|
throw err;
|
|
7332
8006
|
});
|
|
@@ -7334,17 +8008,17 @@ var replaceTool = {
|
|
|
7334
8008
|
if (lstat2.isSymbolicLink()) continue;
|
|
7335
8009
|
let realPath;
|
|
7336
8010
|
try {
|
|
7337
|
-
realPath = await
|
|
8011
|
+
realPath = await fs2.realpath(absPath);
|
|
7338
8012
|
} catch {
|
|
7339
8013
|
continue;
|
|
7340
8014
|
}
|
|
7341
8015
|
const rel = path3.relative(realRoot, realPath);
|
|
7342
8016
|
if (rel.startsWith("..") || path3.isAbsolute(rel)) continue;
|
|
7343
|
-
const stat11 = await
|
|
8017
|
+
const stat11 = await fs2.stat(realPath).catch(() => null);
|
|
7344
8018
|
if (!stat11 || !stat11.isFile()) continue;
|
|
7345
8019
|
let content;
|
|
7346
8020
|
try {
|
|
7347
|
-
const buf = await
|
|
8021
|
+
const buf = await fs2.readFile(realPath);
|
|
7348
8022
|
if (isBinaryBuffer(buf)) continue;
|
|
7349
8023
|
content = buf.toString("utf8");
|
|
7350
8024
|
} catch {
|
|
@@ -7396,7 +8070,7 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
7396
8070
|
const resolved = [];
|
|
7397
8071
|
for (const p of parts) {
|
|
7398
8072
|
const absPath = safeResolve(p, ctx);
|
|
7399
|
-
const stat11 = await
|
|
8073
|
+
const stat11 = await fs2.stat(absPath).catch(() => null);
|
|
7400
8074
|
if (stat11?.isFile()) {
|
|
7401
8075
|
resolved.push(absPath);
|
|
7402
8076
|
}
|
|
@@ -7452,7 +8126,7 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
7452
8126
|
const walk = async (dir) => {
|
|
7453
8127
|
let entries;
|
|
7454
8128
|
try {
|
|
7455
|
-
entries = await
|
|
8129
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
7456
8130
|
} catch {
|
|
7457
8131
|
return;
|
|
7458
8132
|
}
|
|
@@ -7460,7 +8134,7 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
7460
8134
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
7461
8135
|
const full = path3.join(dir, e.name);
|
|
7462
8136
|
try {
|
|
7463
|
-
const stat11 = await
|
|
8137
|
+
const stat11 = await fs2.lstat(full);
|
|
7464
8138
|
if (stat11.isSymbolicLink()) continue;
|
|
7465
8139
|
} catch {
|
|
7466
8140
|
continue;
|
|
@@ -7638,7 +8312,7 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
7638
8312
|
}
|
|
7639
8313
|
const fullPath = target;
|
|
7640
8314
|
if (!dryRun) {
|
|
7641
|
-
await
|
|
8315
|
+
await fs2.mkdir(path3.dirname(fullPath), { recursive: true });
|
|
7642
8316
|
await atomicWrite(fullPath, substituteVars(content, name, vars));
|
|
7643
8317
|
}
|
|
7644
8318
|
files.push(resolvedPath);
|
|
@@ -7751,7 +8425,7 @@ async function duckduckgoSearch(query2, num, signal) {
|
|
|
7751
8425
|
truncated: results.length >= num
|
|
7752
8426
|
};
|
|
7753
8427
|
} catch (err) {
|
|
7754
|
-
console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: toErrorMessage(err) }));
|
|
8428
|
+
console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: toErrorMessage$1(err) }));
|
|
7755
8429
|
return {
|
|
7756
8430
|
query: query2,
|
|
7757
8431
|
results: [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }],
|
|
@@ -7915,11 +8589,11 @@ var setWorkingDirTool = {
|
|
|
7915
8589
|
} catch (err) {
|
|
7916
8590
|
return {
|
|
7917
8591
|
current: ctx.workingDir,
|
|
7918
|
-
error: toErrorMessage(err)
|
|
8592
|
+
error: toErrorMessage$1(err)
|
|
7919
8593
|
};
|
|
7920
8594
|
}
|
|
7921
8595
|
try {
|
|
7922
|
-
await
|
|
8596
|
+
await fs2.access(resolved);
|
|
7923
8597
|
} catch {
|
|
7924
8598
|
try {
|
|
7925
8599
|
ctx.setWorkingDir(previous);
|
|
@@ -8975,7 +9649,7 @@ var treeTool = {
|
|
|
8975
9649
|
}
|
|
8976
9650
|
};
|
|
8977
9651
|
async function walkDir(dir, depth, opts) {
|
|
8978
|
-
const entries = await
|
|
9652
|
+
const entries = await fs2.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
8979
9653
|
const filtered = entries.filter((e) => {
|
|
8980
9654
|
if (!opts.showHidden && e.name.startsWith(".")) return false;
|
|
8981
9655
|
if (opts.exclude.has(e.name)) return false;
|
|
@@ -9133,14 +9807,14 @@ var writeTool = {
|
|
|
9133
9807
|
let existed = false;
|
|
9134
9808
|
let prev = "";
|
|
9135
9809
|
try {
|
|
9136
|
-
const stat12 = await
|
|
9810
|
+
const stat12 = await fs2.stat(absPath);
|
|
9137
9811
|
existed = stat12.isFile();
|
|
9138
9812
|
if (existed) {
|
|
9139
9813
|
if (!ctx.hasRead(absPath)) {
|
|
9140
|
-
prev = await
|
|
9814
|
+
prev = await fs2.readFile(absPath, "utf8");
|
|
9141
9815
|
ctx.recordRead(absPath, stat12.mtimeMs);
|
|
9142
9816
|
} else {
|
|
9143
|
-
prev = await
|
|
9817
|
+
prev = await fs2.readFile(absPath, "utf8");
|
|
9144
9818
|
}
|
|
9145
9819
|
}
|
|
9146
9820
|
} catch (err) {
|
|
@@ -9151,7 +9825,7 @@ var writeTool = {
|
|
|
9151
9825
|
await atomicWrite(absPath, input.content);
|
|
9152
9826
|
const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
9153
9827
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
9154
|
-
const stat11 = await
|
|
9828
|
+
const stat11 = await fs2.stat(absPath);
|
|
9155
9829
|
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
9156
9830
|
ctx.session.recordFileChange({
|
|
9157
9831
|
path: absPath,
|