@wrongstack/tools 0.269.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 +591 -11
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +857 -174
- 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 +1416 -202
- 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 +857 -174
- 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/pack.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;
|
|
@@ -1179,8 +1752,7 @@ var bashTool = {
|
|
|
1179
1752
|
// Windows children survive parent exit either way. POSIX keeps
|
|
1180
1753
|
// detached for the process-group kill semantics.
|
|
1181
1754
|
detached: !isWin3,
|
|
1182
|
-
windowsHide: true
|
|
1183
|
-
signal: opts.signal
|
|
1755
|
+
windowsHide: true
|
|
1184
1756
|
});
|
|
1185
1757
|
const pid2 = child2.pid;
|
|
1186
1758
|
if (typeof pid2 === "number") {
|
|
@@ -1208,7 +1780,17 @@ var bashTool = {
|
|
|
1208
1780
|
};
|
|
1209
1781
|
child2.stdout?.on("data", onBgData);
|
|
1210
1782
|
child2.stderr?.on("data", onBgData);
|
|
1783
|
+
const cleanupBackground = () => {
|
|
1784
|
+
child2.stdout?.off("data", onBgData);
|
|
1785
|
+
child2.stderr?.off("data", onBgData);
|
|
1786
|
+
};
|
|
1787
|
+
child2.on("error", () => {
|
|
1788
|
+
cleanupBackground();
|
|
1789
|
+
if (typeof pid2 === "number") registry.unregister(pid2);
|
|
1790
|
+
registry.afterCall(Date.now() - startedAt, true, bypassBreaker);
|
|
1791
|
+
});
|
|
1211
1792
|
child2.on("close", () => {
|
|
1793
|
+
cleanupBackground();
|
|
1212
1794
|
registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
|
|
1213
1795
|
});
|
|
1214
1796
|
if (typeof pid2 === "number") child2.unref();
|
|
@@ -1753,7 +2335,7 @@ function loadDatabaseSync() {
|
|
|
1753
2335
|
DatabaseSyncCtor = req("node:sqlite").DatabaseSync;
|
|
1754
2336
|
} catch (err) {
|
|
1755
2337
|
throw new Error(
|
|
1756
|
-
`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)}`
|
|
1757
2339
|
);
|
|
1758
2340
|
}
|
|
1759
2341
|
return DatabaseSyncCtor;
|
|
@@ -1902,33 +2484,53 @@ var IndexStore = class {
|
|
|
1902
2484
|
}
|
|
1903
2485
|
}
|
|
1904
2486
|
// ─── Symbol CRUD ─────────────────────────────────────────────────────────────
|
|
1905
|
-
|
|
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) {
|
|
1906
2498
|
return this.runWithRetry(() => {
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
stmt.run(
|
|
1915
|
-
id,
|
|
1916
|
-
s.lang,
|
|
1917
|
-
s.kind,
|
|
1918
|
-
s.name,
|
|
1919
|
-
s.file,
|
|
1920
|
-
s.line,
|
|
1921
|
-
s.col,
|
|
1922
|
-
s.signature,
|
|
1923
|
-
s.docComment,
|
|
1924
|
-
s.scope,
|
|
1925
|
-
s.text,
|
|
1926
|
-
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1927
2506
|
);
|
|
1928
|
-
ftsStmt
|
|
1929
|
-
|
|
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;
|
|
1930
2533
|
}
|
|
1931
|
-
return id;
|
|
1932
2534
|
});
|
|
1933
2535
|
}
|
|
1934
2536
|
deleteSymbolsForFile(file) {
|
|
@@ -2482,10 +3084,10 @@ function detectLang(file) {
|
|
|
2482
3084
|
if (idx < 0) return null;
|
|
2483
3085
|
return extToLang(file.slice(idx));
|
|
2484
3086
|
}
|
|
2485
|
-
function parseSymbols2(opts) {
|
|
3087
|
+
async function parseSymbols2(opts) {
|
|
2486
3088
|
const { file, content, lang } = opts;
|
|
2487
3089
|
try {
|
|
2488
|
-
return syncGoParse(file, content, lang);
|
|
3090
|
+
return await syncGoParse(file, content, lang);
|
|
2489
3091
|
} catch {
|
|
2490
3092
|
return { file, lang, symbols: [], mtimeMs: Date.now() };
|
|
2491
3093
|
}
|
|
@@ -2722,19 +3324,34 @@ func formatType(t ast.Expr) string {
|
|
|
2722
3324
|
}
|
|
2723
3325
|
}
|
|
2724
3326
|
`;
|
|
2725
|
-
function syncGoParse(filePath, content, lang) {
|
|
2726
|
-
const tmpDir = path3.join(
|
|
3327
|
+
async function syncGoParse(filePath, content, lang) {
|
|
3328
|
+
const tmpDir = path3.join(os2.tmpdir(), "ws-go-parse");
|
|
2727
3329
|
try {
|
|
2728
|
-
|
|
3330
|
+
await fs2.mkdir(tmpDir, { recursive: true });
|
|
2729
3331
|
const scriptPath = path3.join(tmpDir, "parse.go");
|
|
2730
|
-
|
|
2731
|
-
const
|
|
2732
|
-
|
|
2733
|
-
timeout: 15e3,
|
|
2734
|
-
encoding: "utf8",
|
|
3332
|
+
await fs2.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
|
|
3333
|
+
const proc = spawn("go", ["run", scriptPath], {
|
|
3334
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2735
3335
|
windowsHide: true
|
|
2736
3336
|
});
|
|
2737
|
-
|
|
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()) {
|
|
2738
3355
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
2739
3356
|
}
|
|
2740
3357
|
const raw = JSON.parse(stdout.trim());
|
|
@@ -2756,10 +3373,10 @@ function syncGoParse(filePath, content, lang) {
|
|
|
2756
3373
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
2757
3374
|
}
|
|
2758
3375
|
}
|
|
2759
|
-
function parseSymbols3(opts) {
|
|
3376
|
+
async function parseSymbols3(opts) {
|
|
2760
3377
|
const { file, lang } = opts;
|
|
2761
3378
|
try {
|
|
2762
|
-
return syncPyParse(file, lang);
|
|
3379
|
+
return await syncPyParse(file, lang);
|
|
2763
3380
|
} catch {
|
|
2764
3381
|
return { file, lang, symbols: [], mtimeMs: Date.now() };
|
|
2765
3382
|
}
|
|
@@ -2968,18 +3585,32 @@ visitor.visit(tree)
|
|
|
2968
3585
|
|
|
2969
3586
|
print(json.dumps([s.to_dict() for s in syms]))
|
|
2970
3587
|
`;
|
|
2971
|
-
function syncPyParse(filePath, lang) {
|
|
3588
|
+
async function syncPyParse(filePath, lang) {
|
|
2972
3589
|
try {
|
|
2973
|
-
const tmpDir = path3.join(
|
|
2974
|
-
|
|
3590
|
+
const tmpDir = path3.join(os2.tmpdir(), "ws-py-parse");
|
|
3591
|
+
await fs2.mkdir(tmpDir, { recursive: true });
|
|
2975
3592
|
const scriptPath = path3.join(tmpDir, "parse.py");
|
|
2976
|
-
|
|
2977
|
-
const
|
|
2978
|
-
|
|
2979
|
-
encoding: "utf8",
|
|
3593
|
+
await fs2.writeFile(scriptPath, PY_PARSE_SCRIPT, "utf8");
|
|
3594
|
+
const proc = spawn("python", [scriptPath, filePath], {
|
|
3595
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2980
3596
|
windowsHide: true
|
|
2981
3597
|
});
|
|
2982
|
-
|
|
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()) {
|
|
2983
3614
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
2984
3615
|
}
|
|
2985
3616
|
const raw = JSON.parse(stdout.trim());
|
|
@@ -3001,11 +3632,11 @@ function syncPyParse(filePath, lang) {
|
|
|
3001
3632
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
3002
3633
|
}
|
|
3003
3634
|
}
|
|
3004
|
-
function parseSymbols4(opts) {
|
|
3635
|
+
async function parseSymbols4(opts) {
|
|
3005
3636
|
const { file, content, lang } = opts;
|
|
3006
3637
|
const nativeAvailable = checkNativeParser();
|
|
3007
3638
|
if (nativeAvailable) {
|
|
3008
|
-
const result = tryNativeParse(file, content);
|
|
3639
|
+
const result = await tryNativeParse(file, content);
|
|
3009
3640
|
if (result) return result;
|
|
3010
3641
|
}
|
|
3011
3642
|
return regexParse({ file, content, lang });
|
|
@@ -3035,25 +3666,34 @@ function checkNativeParser() {
|
|
|
3035
3666
|
return false;
|
|
3036
3667
|
}
|
|
3037
3668
|
}
|
|
3038
|
-
function tryNativeParse(file, content) {
|
|
3669
|
+
async function tryNativeParse(file, content) {
|
|
3039
3670
|
try {
|
|
3040
3671
|
const toolsDir = path3.join(process.cwd(), "tools");
|
|
3041
3672
|
const crateDir = path3.join(toolsDir, "syn-parser");
|
|
3042
3673
|
const tmpFile = path3.join(crateDir, "src", "input.rs");
|
|
3043
|
-
|
|
3044
|
-
const
|
|
3045
|
-
|
|
3046
|
-
["
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
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());
|
|
3057
3697
|
return {
|
|
3058
3698
|
file,
|
|
3059
3699
|
lang: "rs",
|
|
@@ -3522,7 +4162,7 @@ function compileGitignore(lines) {
|
|
|
3522
4162
|
async function loadGitignoreMatcher(projectRoot) {
|
|
3523
4163
|
let lines = [];
|
|
3524
4164
|
try {
|
|
3525
|
-
const raw = await
|
|
4165
|
+
const raw = await fs2.readFile(path3.join(projectRoot, ".gitignore"), "utf8");
|
|
3526
4166
|
lines = raw.split("\n");
|
|
3527
4167
|
} catch {
|
|
3528
4168
|
}
|
|
@@ -3580,7 +4220,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
3580
4220
|
}
|
|
3581
4221
|
let entries;
|
|
3582
4222
|
try {
|
|
3583
|
-
entries = await
|
|
4223
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
3584
4224
|
} catch {
|
|
3585
4225
|
return;
|
|
3586
4226
|
}
|
|
@@ -3678,7 +4318,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3678
4318
|
batchFiles.map(async (file) => {
|
|
3679
4319
|
let stat11;
|
|
3680
4320
|
try {
|
|
3681
|
-
stat11 = await
|
|
4321
|
+
stat11 = await fs2.stat(file, statOpts);
|
|
3682
4322
|
} catch (e) {
|
|
3683
4323
|
if (isAbortError(e)) throw e;
|
|
3684
4324
|
return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
|
|
@@ -3692,7 +4332,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3692
4332
|
}
|
|
3693
4333
|
let content;
|
|
3694
4334
|
try {
|
|
3695
|
-
content = await
|
|
4335
|
+
content = await fs2.readFile(file, { encoding: "utf8", signal });
|
|
3696
4336
|
} catch (e) {
|
|
3697
4337
|
if (isAbortError(e)) throw e;
|
|
3698
4338
|
return { file, stat: stat11, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
|
|
@@ -3742,9 +4382,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3742
4382
|
filesIndexed++;
|
|
3743
4383
|
continue;
|
|
3744
4384
|
}
|
|
3745
|
-
const
|
|
3746
|
-
const symbolsWithIds = parsed.symbols.map((s, i) => ({ ...s, id: nextId + i }));
|
|
3747
|
-
store.insertSymbols(symbolsWithIds, nextId);
|
|
4385
|
+
const symbolsWithIds = store.insertSymbols(parsed.symbols);
|
|
3748
4386
|
const count = symbolsWithIds.length;
|
|
3749
4387
|
symbolsIndexed += count;
|
|
3750
4388
|
langStats[lang] = (langStats[lang] ?? 0) + count;
|
|
@@ -3773,7 +4411,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3773
4411
|
}
|
|
3774
4412
|
for (const [file_] of existingMeta) {
|
|
3775
4413
|
try {
|
|
3776
|
-
await
|
|
4414
|
+
await fs2.stat(file_);
|
|
3777
4415
|
} catch {
|
|
3778
4416
|
store.deleteFile(file_);
|
|
3779
4417
|
}
|
|
@@ -4023,10 +4661,10 @@ function circuitOpenError() {
|
|
|
4023
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."
|
|
4024
4662
|
);
|
|
4025
4663
|
}
|
|
4026
|
-
function
|
|
4664
|
+
function isRecoverableConstraintError(err) {
|
|
4027
4665
|
if (err instanceof Error) {
|
|
4028
4666
|
const msg = err.message.toLowerCase();
|
|
4029
|
-
return msg.includes("unique constraint") || msg.includes("
|
|
4667
|
+
return msg.includes("unique constraint") || msg.includes("constraint failed");
|
|
4030
4668
|
}
|
|
4031
4669
|
return false;
|
|
4032
4670
|
}
|
|
@@ -4059,7 +4697,7 @@ async function runStartupIndex(opts) {
|
|
|
4059
4697
|
return result;
|
|
4060
4698
|
} catch (err) {
|
|
4061
4699
|
_lastError = err instanceof Error ? err.message : String(err);
|
|
4062
|
-
if (
|
|
4700
|
+
if (isRecoverableConstraintError(err) && !opts.force) {
|
|
4063
4701
|
_lastError = null;
|
|
4064
4702
|
const rebuildResult = await runStartupIndex({
|
|
4065
4703
|
...opts,
|
|
@@ -4419,9 +5057,9 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
4419
5057
|
const results = [];
|
|
4420
5058
|
for (const file of files) {
|
|
4421
5059
|
const absPath = safeResolve(file, ctx);
|
|
4422
|
-
const stat11 = await
|
|
5060
|
+
const stat11 = await fs2.stat(absPath).catch(() => null);
|
|
4423
5061
|
if (!stat11?.isFile()) continue;
|
|
4424
|
-
const content = await
|
|
5062
|
+
const content = await fs2.readFile(absPath, "utf8");
|
|
4425
5063
|
const lines = content.split(/\r?\n/);
|
|
4426
5064
|
results.push(formatWithLineNumbers(file, lines));
|
|
4427
5065
|
}
|
|
@@ -4481,7 +5119,7 @@ var documentTool = {
|
|
|
4481
5119
|
const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
|
|
4482
5120
|
for (const absPath of fileList) {
|
|
4483
5121
|
try {
|
|
4484
|
-
const content = await
|
|
5122
|
+
const content = await fs2.readFile(absPath, "utf8");
|
|
4485
5123
|
filesProcessed++;
|
|
4486
5124
|
const processed = processFile(
|
|
4487
5125
|
content,
|
|
@@ -4517,7 +5155,7 @@ async function resolveFiles(filesInput, cwd) {
|
|
|
4517
5155
|
for (const f of files) {
|
|
4518
5156
|
const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
|
|
4519
5157
|
try {
|
|
4520
|
-
const stat11 = await
|
|
5158
|
+
const stat11 = await fs2.stat(absPath);
|
|
4521
5159
|
if (stat11.isFile()) resolved.push(absPath);
|
|
4522
5160
|
} catch {
|
|
4523
5161
|
}
|
|
@@ -4610,7 +5248,7 @@ var editTool = {
|
|
|
4610
5248
|
if (input.new_string === void 0) throw new Error("edit: new_string is required");
|
|
4611
5249
|
if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
|
|
4612
5250
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
4613
|
-
const stat11 = await
|
|
5251
|
+
const stat11 = await fs2.stat(absPath).catch((err) => {
|
|
4614
5252
|
if (err.code === "ENOENT") {
|
|
4615
5253
|
throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
|
|
4616
5254
|
}
|
|
@@ -4618,8 +5256,8 @@ var editTool = {
|
|
|
4618
5256
|
});
|
|
4619
5257
|
if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
|
|
4620
5258
|
const autoRead = !ctx.hasRead(absPath);
|
|
4621
|
-
const original = await
|
|
4622
|
-
const updated = await
|
|
5259
|
+
const original = await fs2.readFile(absPath, "utf8");
|
|
5260
|
+
const updated = await fs2.stat(absPath);
|
|
4623
5261
|
const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
|
|
4624
5262
|
const lastReadMtime = ctx.lastReadMtime(absPath);
|
|
4625
5263
|
if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
|
|
@@ -4665,7 +5303,7 @@ var editTool = {
|
|
|
4665
5303
|
const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
|
|
4666
5304
|
const newFile = toStyle(newFileLf, style);
|
|
4667
5305
|
await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
|
|
4668
|
-
const written = await
|
|
5306
|
+
const written = await fs2.stat(absPath);
|
|
4669
5307
|
ctx.recordRead(absPath, written.mtimeMs);
|
|
4670
5308
|
ctx.session.recordFileChange({
|
|
4671
5309
|
path: absPath,
|
|
@@ -5014,8 +5652,8 @@ if (ALLOW_PRIVATE && !process.env["CI"]) {
|
|
|
5014
5652
|
);
|
|
5015
5653
|
}
|
|
5016
5654
|
var combineSignals = (signals) => AbortSignal.any(signals);
|
|
5017
|
-
function guardedLookup(
|
|
5018
|
-
dns.lookup(
|
|
5655
|
+
function guardedLookup(hostname2, options, callback) {
|
|
5656
|
+
dns.lookup(hostname2, { all: true }).then((records) => {
|
|
5019
5657
|
const family = options?.family;
|
|
5020
5658
|
const byFamily = family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;
|
|
5021
5659
|
const list = byFamily.length > 0 ? byFamily : records;
|
|
@@ -5042,7 +5680,7 @@ function guardedLookup(hostname, options, callback) {
|
|
|
5042
5680
|
const first = list.at(0);
|
|
5043
5681
|
if (!first) {
|
|
5044
5682
|
callback(
|
|
5045
|
-
Object.assign(new Error(`fetch: no address for ${
|
|
5683
|
+
Object.assign(new Error(`fetch: no address for ${hostname2}`), { code: "ENOTFOUND" })
|
|
5046
5684
|
);
|
|
5047
5685
|
return;
|
|
5048
5686
|
}
|
|
@@ -5156,7 +5794,13 @@ var fetchTool = {
|
|
|
5156
5794
|
const timer = setTimeout(() => ctrl.abort(new Error("fetch timeout")), TIMEOUT_MS);
|
|
5157
5795
|
const combined = combineSignals([opts.signal, ctrl.signal]);
|
|
5158
5796
|
try {
|
|
5159
|
-
|
|
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
|
+
}
|
|
5160
5804
|
const ct = res.headers.get("content-type") ?? "application/octet-stream";
|
|
5161
5805
|
if (/^image\/|^audio\/|^video\/|application\/octet-stream/.test(ct)) {
|
|
5162
5806
|
throw new Error(`fetch: refusing to read binary content-type "${ct}"`);
|
|
@@ -5212,9 +5856,9 @@ var fetchTool = {
|
|
|
5212
5856
|
}
|
|
5213
5857
|
}
|
|
5214
5858
|
};
|
|
5215
|
-
async function assertNotPrivate(
|
|
5859
|
+
async function assertNotPrivate(hostname2) {
|
|
5216
5860
|
if (ALLOW_PRIVATE) return;
|
|
5217
|
-
const host =
|
|
5861
|
+
const host = hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
|
|
5218
5862
|
if (host === "localhost" || host.endsWith(".localhost")) {
|
|
5219
5863
|
throw new Error("fetch: blocked localhost target");
|
|
5220
5864
|
}
|
|
@@ -5241,6 +5885,23 @@ async function assertNotPrivate(hostname) {
|
|
|
5241
5885
|
}
|
|
5242
5886
|
}
|
|
5243
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
|
+
}
|
|
5244
5905
|
function prettyJson(s) {
|
|
5245
5906
|
try {
|
|
5246
5907
|
return JSON.stringify(JSON.parse(s), null, 2);
|
|
@@ -5672,7 +6333,7 @@ var globTool = {
|
|
|
5672
6333
|
}
|
|
5673
6334
|
let entries;
|
|
5674
6335
|
try {
|
|
5675
|
-
entries = await
|
|
6336
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
5676
6337
|
} catch {
|
|
5677
6338
|
return;
|
|
5678
6339
|
}
|
|
@@ -5688,7 +6349,7 @@ var globTool = {
|
|
|
5688
6349
|
} else if (e.isFile()) {
|
|
5689
6350
|
if (re.test(rel) || re.test(name)) {
|
|
5690
6351
|
try {
|
|
5691
|
-
const st = await
|
|
6352
|
+
const st = await fs2.stat(full);
|
|
5692
6353
|
results.push({ rel: full, mtime: st.mtimeMs });
|
|
5693
6354
|
if (results.length >= limit) {
|
|
5694
6355
|
truncated = true;
|
|
@@ -5707,7 +6368,7 @@ var globTool = {
|
|
|
5707
6368
|
};
|
|
5708
6369
|
async function readGitignore(dir) {
|
|
5709
6370
|
try {
|
|
5710
|
-
const raw = await
|
|
6371
|
+
const raw = await fs2.readFile(path3.join(dir, ".gitignore"), "utf8");
|
|
5711
6372
|
return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
5712
6373
|
} catch {
|
|
5713
6374
|
return [];
|
|
@@ -5761,6 +6422,7 @@ function capSubject(line) {
|
|
|
5761
6422
|
|
|
5762
6423
|
// src/grep.ts
|
|
5763
6424
|
var DEFAULT_IGNORE3 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
|
|
6425
|
+
var NATIVE_SCAN_CONCURRENCY = 32;
|
|
5764
6426
|
var grepTool = {
|
|
5765
6427
|
name: "grep",
|
|
5766
6428
|
category: "Search",
|
|
@@ -5988,14 +6650,52 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
5988
6650
|
const fileMatches = /* @__PURE__ */ new Map();
|
|
5989
6651
|
let total = 0;
|
|
5990
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
|
+
};
|
|
5991
6690
|
const walk = async (dir) => {
|
|
5992
6691
|
if (stopped || signal.aborted) return;
|
|
5993
6692
|
let entries;
|
|
5994
6693
|
try {
|
|
5995
|
-
entries = await
|
|
6694
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
5996
6695
|
} catch {
|
|
5997
6696
|
return;
|
|
5998
6697
|
}
|
|
6698
|
+
const files = [];
|
|
5999
6699
|
for (const e of entries) {
|
|
6000
6700
|
if (stopped) return;
|
|
6001
6701
|
if (DEFAULT_IGNORE3.includes(e.name)) continue;
|
|
@@ -6004,41 +6704,10 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
6004
6704
|
if (e.isDirectory()) {
|
|
6005
6705
|
await walk(full);
|
|
6006
6706
|
} else if (e.isFile()) {
|
|
6007
|
-
|
|
6008
|
-
if (globRe) globRe.lastIndex = 0;
|
|
6009
|
-
try {
|
|
6010
|
-
const stat11 = await fs14.stat(full);
|
|
6011
|
-
if (stat11.size > 1e6) continue;
|
|
6012
|
-
const head = await fs14.readFile(full);
|
|
6013
|
-
if (isBinaryBuffer(head)) continue;
|
|
6014
|
-
const text = head.toString("utf8");
|
|
6015
|
-
const lines = text.split(/\r?\n/);
|
|
6016
|
-
let fileHits = 0;
|
|
6017
|
-
for (let i = 0; i < lines.length; i++) {
|
|
6018
|
-
const ln = capSubject(lines[i] ?? "");
|
|
6019
|
-
re.lastIndex = 0;
|
|
6020
|
-
if (re.test(ln)) {
|
|
6021
|
-
fileHits++;
|
|
6022
|
-
total++;
|
|
6023
|
-
if (mode === "content" && matches.length < limit) {
|
|
6024
|
-
matches.push(`${full}:${i + 1}:${ln}`);
|
|
6025
|
-
}
|
|
6026
|
-
}
|
|
6027
|
-
}
|
|
6028
|
-
if (fileHits > 0) {
|
|
6029
|
-
fileMatches.set(full, fileHits);
|
|
6030
|
-
if (mode === "files_with_matches" && matches.length < limit) {
|
|
6031
|
-
matches.push(full);
|
|
6032
|
-
}
|
|
6033
|
-
if (mode === "count" && matches.length < limit) {
|
|
6034
|
-
matches.push(`${full}:${fileHits}`);
|
|
6035
|
-
}
|
|
6036
|
-
}
|
|
6037
|
-
if (matches.length >= limit) stopped = true;
|
|
6038
|
-
} catch {
|
|
6039
|
-
}
|
|
6707
|
+
files.push({ full, name: e.name });
|
|
6040
6708
|
}
|
|
6041
6709
|
}
|
|
6710
|
+
await mapWithConcurrency(files, NATIVE_SCAN_CONCURRENCY, ({ full, name }) => scanFile(full, name));
|
|
6042
6711
|
};
|
|
6043
6712
|
await walk(base);
|
|
6044
6713
|
return {
|
|
@@ -6048,6 +6717,20 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
6048
6717
|
used: "native"
|
|
6049
6718
|
};
|
|
6050
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
|
+
}
|
|
6051
6734
|
var installTool = {
|
|
6052
6735
|
name: "install",
|
|
6053
6736
|
category: "Package Management",
|
|
@@ -6222,7 +6905,7 @@ var jsonTool = {
|
|
|
6222
6905
|
let raw;
|
|
6223
6906
|
if (input.file) {
|
|
6224
6907
|
try {
|
|
6225
|
-
raw = await
|
|
6908
|
+
raw = await fs2.readFile(input.file, "utf8");
|
|
6226
6909
|
} catch {
|
|
6227
6910
|
return { data: null, formatted: "", type: "unknown", error: `Could not read file` };
|
|
6228
6911
|
}
|
|
@@ -6261,8 +6944,8 @@ var jsonTool = {
|
|
|
6261
6944
|
};
|
|
6262
6945
|
}
|
|
6263
6946
|
};
|
|
6264
|
-
function query(data,
|
|
6265
|
-
const parts =
|
|
6947
|
+
function query(data, path21) {
|
|
6948
|
+
const parts = path21.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
6266
6949
|
let current = data;
|
|
6267
6950
|
for (const part of parts) {
|
|
6268
6951
|
if (current === null || current === void 0) return void 0;
|
|
@@ -6539,7 +7222,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
6539
7222
|
}
|
|
6540
7223
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
6541
7224
|
var MAX_TAIL_LINES = 1e5;
|
|
6542
|
-
async function fileLogs(
|
|
7225
|
+
async function fileLogs(path21, lines, filterRe, stream) {
|
|
6543
7226
|
const { createInterface } = await import('node:readline');
|
|
6544
7227
|
const { createReadStream } = await import('node:fs');
|
|
6545
7228
|
const entries = [];
|
|
@@ -6548,7 +7231,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
|
|
|
6548
7231
|
let writeIdx = 0;
|
|
6549
7232
|
let totalLines = 0;
|
|
6550
7233
|
const rl = createInterface({
|
|
6551
|
-
input: createReadStream(
|
|
7234
|
+
input: createReadStream(path21),
|
|
6552
7235
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
6553
7236
|
});
|
|
6554
7237
|
for await (const line of rl) {
|
|
@@ -6569,7 +7252,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
|
|
|
6569
7252
|
if (parsed) entries.push(parsed);
|
|
6570
7253
|
}
|
|
6571
7254
|
return {
|
|
6572
|
-
source:
|
|
7255
|
+
source: path21,
|
|
6573
7256
|
entries,
|
|
6574
7257
|
total: entries.length,
|
|
6575
7258
|
truncated: totalLines > effLines,
|
|
@@ -6770,12 +7453,12 @@ var patchTool = {
|
|
|
6770
7453
|
};
|
|
6771
7454
|
}
|
|
6772
7455
|
}
|
|
6773
|
-
const tmpDir = await
|
|
7456
|
+
const tmpDir = await fs2.mkdtemp(path3.join(os2.tmpdir(), ".wstack_patch_"));
|
|
6774
7457
|
try {
|
|
6775
|
-
await
|
|
7458
|
+
await fs2.chmod(tmpDir, 448).catch(() => {
|
|
6776
7459
|
});
|
|
6777
7460
|
const patchFile = path3.join(tmpDir, "in.diff");
|
|
6778
|
-
await
|
|
7461
|
+
await fs2.writeFile(patchFile, input.patch, { mode: 384 });
|
|
6779
7462
|
const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
|
|
6780
7463
|
const result = await runPatch(args, dir, opts.signal);
|
|
6781
7464
|
if (result.exitCode !== 0 && !dryRun) {
|
|
@@ -6796,7 +7479,7 @@ var patchTool = {
|
|
|
6796
7479
|
message: result.stdout || "patch applied"
|
|
6797
7480
|
};
|
|
6798
7481
|
} finally {
|
|
6799
|
-
await
|
|
7482
|
+
await fs2.rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
6800
7483
|
});
|
|
6801
7484
|
}
|
|
6802
7485
|
}
|
|
@@ -7142,7 +7825,7 @@ var readTool = {
|
|
|
7142
7825
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
7143
7826
|
let stat11;
|
|
7144
7827
|
try {
|
|
7145
|
-
stat11 = await
|
|
7828
|
+
stat11 = await fs2.stat(absPath);
|
|
7146
7829
|
} catch (err) {
|
|
7147
7830
|
const code = err.code;
|
|
7148
7831
|
if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
|
|
@@ -7167,7 +7850,7 @@ var readTool = {
|
|
|
7167
7850
|
note: "Repeated read suppressed to save tokens."
|
|
7168
7851
|
};
|
|
7169
7852
|
}
|
|
7170
|
-
const buf = await
|
|
7853
|
+
const buf = await fs2.readFile(absPath);
|
|
7171
7854
|
if (isBinaryBuffer(buf)) {
|
|
7172
7855
|
throw new Error(`read: "${input.path}" appears to be binary`);
|
|
7173
7856
|
}
|
|
@@ -7313,11 +7996,11 @@ var replaceTool = {
|
|
|
7313
7996
|
const dryRun = input.dry_run ?? false;
|
|
7314
7997
|
const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
|
|
7315
7998
|
const fileList = await resolveFiles2(filesInput, ctx, globRe);
|
|
7316
|
-
const realRoot = await
|
|
7999
|
+
const realRoot = await fs2.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
|
|
7317
8000
|
const results = [];
|
|
7318
8001
|
let totalReplacements = 0;
|
|
7319
8002
|
for (const absPath of fileList) {
|
|
7320
|
-
const lstat2 = await
|
|
8003
|
+
const lstat2 = await fs2.lstat(absPath).catch((err) => {
|
|
7321
8004
|
if (err.code === "ENOENT") return null;
|
|
7322
8005
|
throw err;
|
|
7323
8006
|
});
|
|
@@ -7325,17 +8008,17 @@ var replaceTool = {
|
|
|
7325
8008
|
if (lstat2.isSymbolicLink()) continue;
|
|
7326
8009
|
let realPath;
|
|
7327
8010
|
try {
|
|
7328
|
-
realPath = await
|
|
8011
|
+
realPath = await fs2.realpath(absPath);
|
|
7329
8012
|
} catch {
|
|
7330
8013
|
continue;
|
|
7331
8014
|
}
|
|
7332
8015
|
const rel = path3.relative(realRoot, realPath);
|
|
7333
8016
|
if (rel.startsWith("..") || path3.isAbsolute(rel)) continue;
|
|
7334
|
-
const stat11 = await
|
|
8017
|
+
const stat11 = await fs2.stat(realPath).catch(() => null);
|
|
7335
8018
|
if (!stat11 || !stat11.isFile()) continue;
|
|
7336
8019
|
let content;
|
|
7337
8020
|
try {
|
|
7338
|
-
const buf = await
|
|
8021
|
+
const buf = await fs2.readFile(realPath);
|
|
7339
8022
|
if (isBinaryBuffer(buf)) continue;
|
|
7340
8023
|
content = buf.toString("utf8");
|
|
7341
8024
|
} catch {
|
|
@@ -7387,7 +8070,7 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
7387
8070
|
const resolved = [];
|
|
7388
8071
|
for (const p of parts) {
|
|
7389
8072
|
const absPath = safeResolve(p, ctx);
|
|
7390
|
-
const stat11 = await
|
|
8073
|
+
const stat11 = await fs2.stat(absPath).catch(() => null);
|
|
7391
8074
|
if (stat11?.isFile()) {
|
|
7392
8075
|
resolved.push(absPath);
|
|
7393
8076
|
}
|
|
@@ -7443,7 +8126,7 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
7443
8126
|
const walk = async (dir) => {
|
|
7444
8127
|
let entries;
|
|
7445
8128
|
try {
|
|
7446
|
-
entries = await
|
|
8129
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
7447
8130
|
} catch {
|
|
7448
8131
|
return;
|
|
7449
8132
|
}
|
|
@@ -7451,7 +8134,7 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
7451
8134
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
7452
8135
|
const full = path3.join(dir, e.name);
|
|
7453
8136
|
try {
|
|
7454
|
-
const stat11 = await
|
|
8137
|
+
const stat11 = await fs2.lstat(full);
|
|
7455
8138
|
if (stat11.isSymbolicLink()) continue;
|
|
7456
8139
|
} catch {
|
|
7457
8140
|
continue;
|
|
@@ -7629,7 +8312,7 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
7629
8312
|
}
|
|
7630
8313
|
const fullPath = target;
|
|
7631
8314
|
if (!dryRun) {
|
|
7632
|
-
await
|
|
8315
|
+
await fs2.mkdir(path3.dirname(fullPath), { recursive: true });
|
|
7633
8316
|
await atomicWrite(fullPath, substituteVars(content, name, vars));
|
|
7634
8317
|
}
|
|
7635
8318
|
files.push(resolvedPath);
|
|
@@ -7742,7 +8425,7 @@ async function duckduckgoSearch(query2, num, signal) {
|
|
|
7742
8425
|
truncated: results.length >= num
|
|
7743
8426
|
};
|
|
7744
8427
|
} catch (err) {
|
|
7745
|
-
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) }));
|
|
7746
8429
|
return {
|
|
7747
8430
|
query: query2,
|
|
7748
8431
|
results: [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }],
|
|
@@ -7906,11 +8589,11 @@ var setWorkingDirTool = {
|
|
|
7906
8589
|
} catch (err) {
|
|
7907
8590
|
return {
|
|
7908
8591
|
current: ctx.workingDir,
|
|
7909
|
-
error: toErrorMessage(err)
|
|
8592
|
+
error: toErrorMessage$1(err)
|
|
7910
8593
|
};
|
|
7911
8594
|
}
|
|
7912
8595
|
try {
|
|
7913
|
-
await
|
|
8596
|
+
await fs2.access(resolved);
|
|
7914
8597
|
} catch {
|
|
7915
8598
|
try {
|
|
7916
8599
|
ctx.setWorkingDir(previous);
|
|
@@ -8966,7 +9649,7 @@ var treeTool = {
|
|
|
8966
9649
|
}
|
|
8967
9650
|
};
|
|
8968
9651
|
async function walkDir(dir, depth, opts) {
|
|
8969
|
-
const entries = await
|
|
9652
|
+
const entries = await fs2.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
8970
9653
|
const filtered = entries.filter((e) => {
|
|
8971
9654
|
if (!opts.showHidden && e.name.startsWith(".")) return false;
|
|
8972
9655
|
if (opts.exclude.has(e.name)) return false;
|
|
@@ -9124,14 +9807,14 @@ var writeTool = {
|
|
|
9124
9807
|
let existed = false;
|
|
9125
9808
|
let prev = "";
|
|
9126
9809
|
try {
|
|
9127
|
-
const stat12 = await
|
|
9810
|
+
const stat12 = await fs2.stat(absPath);
|
|
9128
9811
|
existed = stat12.isFile();
|
|
9129
9812
|
if (existed) {
|
|
9130
9813
|
if (!ctx.hasRead(absPath)) {
|
|
9131
|
-
prev = await
|
|
9814
|
+
prev = await fs2.readFile(absPath, "utf8");
|
|
9132
9815
|
ctx.recordRead(absPath, stat12.mtimeMs);
|
|
9133
9816
|
} else {
|
|
9134
|
-
prev = await
|
|
9817
|
+
prev = await fs2.readFile(absPath, "utf8");
|
|
9135
9818
|
}
|
|
9136
9819
|
}
|
|
9137
9820
|
} catch (err) {
|
|
@@ -9142,7 +9825,7 @@ var writeTool = {
|
|
|
9142
9825
|
await atomicWrite(absPath, input.content);
|
|
9143
9826
|
const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
9144
9827
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
9145
|
-
const stat11 = await
|
|
9828
|
+
const stat11 = await fs2.stat(absPath);
|
|
9146
9829
|
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
9147
9830
|
ctx.session.recordFileChange({
|
|
9148
9831
|
path: absPath,
|