@wrongstack/tools 0.270.0 → 0.272.1
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 +21 -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 +859 -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 +16 -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 +21 -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 +1418 -200
- package/dist/index.js.map +1 -1
- package/dist/install.js +21 -5
- package/dist/install.js.map +1 -1
- package/dist/lint.js +21 -5
- package/dist/lint.js.map +1 -1
- package/dist/outdated.js +11 -0
- package/dist/outdated.js.map +1 -1
- package/dist/pack.js +859 -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 +21 -5
- package/dist/test.js.map +1 -1
- package/dist/typecheck.js +21 -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)) {
|
|
@@ -657,6 +660,16 @@ function resolveWin32Command(cmd) {
|
|
|
657
660
|
}
|
|
658
661
|
return cmd;
|
|
659
662
|
}
|
|
663
|
+
var WIN32_SHELL_META = /[&|<>\r\n\0]/;
|
|
664
|
+
function assertSafeWin32ShellArgs(args) {
|
|
665
|
+
for (const a of args) {
|
|
666
|
+
if (typeof a === "string" && WIN32_SHELL_META.test(a)) {
|
|
667
|
+
throw new Error(
|
|
668
|
+
"win32 shell spawn: argument contains a shell metacharacter (one of & | < > or a newline) that could enable command injection through the .cmd/.bat wrapper \u2014 refusing to run. Offending argument: " + JSON.stringify(a)
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
660
673
|
|
|
661
674
|
// src/_spawn-stream.ts
|
|
662
675
|
var isWin = process.platform === "win32";
|
|
@@ -672,6 +685,7 @@ async function* spawnStream(opts) {
|
|
|
672
685
|
const resolved = resolveWin32Command(opts.cmd);
|
|
673
686
|
const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
674
687
|
const cmd = needsShell ? opts.cmd : resolved;
|
|
688
|
+
if (needsShell) assertSafeWin32ShellArgs(opts.args);
|
|
675
689
|
const child = spawn(cmd, opts.args, {
|
|
676
690
|
cwd: opts.cwd,
|
|
677
691
|
env: buildChildEnv(),
|
|
@@ -756,8 +770,10 @@ async function* spawnStream(opts) {
|
|
|
756
770
|
queue.push({ kind: "close", data: "", code: 124 });
|
|
757
771
|
wake();
|
|
758
772
|
};
|
|
759
|
-
if (
|
|
760
|
-
|
|
773
|
+
if (isWin) {
|
|
774
|
+
if (opts.signal.aborted) onAbort();
|
|
775
|
+
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
776
|
+
}
|
|
761
777
|
let exitCode = 0;
|
|
762
778
|
let spawnFailed = false;
|
|
763
779
|
try {
|
|
@@ -801,7 +817,7 @@ async function* spawnStream(opts) {
|
|
|
801
817
|
};
|
|
802
818
|
} finally {
|
|
803
819
|
spool.finalize();
|
|
804
|
-
opts.signal.removeEventListener("abort", onAbort);
|
|
820
|
+
if (isWin) opts.signal.removeEventListener("abort", onAbort);
|
|
805
821
|
child.stdout?.off("data", onOut);
|
|
806
822
|
child.stderr?.off("data", onErr);
|
|
807
823
|
child.stdout?.destroy();
|
|
@@ -856,13 +872,13 @@ function safeResolve(input, ctx) {
|
|
|
856
872
|
async function assertRealInsideRoot(absPath, ctx) {
|
|
857
873
|
if (ctx.allowOutsideProjectRoot) return;
|
|
858
874
|
const realRoots = await Promise.all(
|
|
859
|
-
allowedRoots(ctx).map((r) =>
|
|
875
|
+
allowedRoots(ctx).map((r) => fs2.realpath(r).catch(() => path3.resolve(r)))
|
|
860
876
|
);
|
|
861
877
|
let probe = absPath;
|
|
862
878
|
for (; ; ) {
|
|
863
879
|
let real;
|
|
864
880
|
try {
|
|
865
|
-
real = await
|
|
881
|
+
real = await fs2.realpath(probe);
|
|
866
882
|
} catch (err) {
|
|
867
883
|
if (err.code === "ENOENT") {
|
|
868
884
|
const parent = path3.dirname(probe);
|
|
@@ -1067,6 +1083,560 @@ function parseAuditOutput(json, exitCode) {
|
|
|
1067
1083
|
};
|
|
1068
1084
|
}
|
|
1069
1085
|
}
|
|
1086
|
+
var REGISTRY_FILE = ".wrongstack/process-registry.json";
|
|
1087
|
+
var HEARTBEAT_INTERVAL_MS = 5e3;
|
|
1088
|
+
var STALE_THRESHOLD_MS = 3e4;
|
|
1089
|
+
var LOCKFILE = ".wrongstack/.process-registry.lock";
|
|
1090
|
+
function generateInstanceId() {
|
|
1091
|
+
const hostname2 = os2.hostname();
|
|
1092
|
+
const pid = process.pid;
|
|
1093
|
+
const random = Math.random().toString(36).slice(2, 8);
|
|
1094
|
+
return `${hostname2}:${pid}:${random}`;
|
|
1095
|
+
}
|
|
1096
|
+
async function acquireLock(lockfilePath, timeoutMs = 5e3) {
|
|
1097
|
+
const start = Date.now();
|
|
1098
|
+
const pidStr = String(process.pid);
|
|
1099
|
+
const hostStr = os2.hostname();
|
|
1100
|
+
while (Date.now() - start < timeoutMs) {
|
|
1101
|
+
try {
|
|
1102
|
+
await fs2.writeFile(lockfilePath, `${pidStr}:${hostStr}:${Date.now()}`, { flag: "wx" });
|
|
1103
|
+
return async () => {
|
|
1104
|
+
try {
|
|
1105
|
+
await fs2.unlink(lockfilePath);
|
|
1106
|
+
} catch {
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
} catch (err) {
|
|
1110
|
+
if (err.code === "EEXIST") {
|
|
1111
|
+
try {
|
|
1112
|
+
const content = await fs2.readFile(lockfilePath, "utf-8");
|
|
1113
|
+
const parts = content.split(":");
|
|
1114
|
+
const lockPidStr = parts[0] ?? "0";
|
|
1115
|
+
const lockPid = parseInt(lockPidStr, 10);
|
|
1116
|
+
if (process.platform !== "win32") {
|
|
1117
|
+
try {
|
|
1118
|
+
process.kill(lockPid, 0);
|
|
1119
|
+
} catch {
|
|
1120
|
+
await fs2.unlink(lockfilePath);
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
} catch {
|
|
1125
|
+
try {
|
|
1126
|
+
await fs2.unlink(lockfilePath);
|
|
1127
|
+
} catch {
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
throw err;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
throw new Error(`Failed to acquire lock after ${timeoutMs}ms`);
|
|
1137
|
+
}
|
|
1138
|
+
async function readRegistryFile(filePath) {
|
|
1139
|
+
try {
|
|
1140
|
+
const content = await fs2.readFile(filePath, "utf-8");
|
|
1141
|
+
const parsed = JSON.parse(content);
|
|
1142
|
+
if (parsed.instances && Array.isArray(parsed.instances)) {
|
|
1143
|
+
parsed.instances = new Map(parsed.instances);
|
|
1144
|
+
}
|
|
1145
|
+
return parsed;
|
|
1146
|
+
} catch (err) {
|
|
1147
|
+
if (err.code === "ENOENT") {
|
|
1148
|
+
return {
|
|
1149
|
+
version: 1,
|
|
1150
|
+
instances: /* @__PURE__ */ new Map(),
|
|
1151
|
+
protectedPatterns: ["wrongstack", "node"],
|
|
1152
|
+
lastCleanup: Date.now()
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
throw err;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
async function writeRegistryFile(filePath, data) {
|
|
1159
|
+
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
1160
|
+
const content = JSON.stringify(data, (_k, v) => {
|
|
1161
|
+
if (v instanceof Map) {
|
|
1162
|
+
return Array.from(v.entries());
|
|
1163
|
+
}
|
|
1164
|
+
return v;
|
|
1165
|
+
}, 2);
|
|
1166
|
+
await fs2.writeFile(tmpPath, content, "utf-8");
|
|
1167
|
+
await fs2.rename(tmpPath, filePath);
|
|
1168
|
+
}
|
|
1169
|
+
var PersistentProcessRegistry = class {
|
|
1170
|
+
instanceId;
|
|
1171
|
+
registryPath;
|
|
1172
|
+
lockPath;
|
|
1173
|
+
baseRegistry;
|
|
1174
|
+
heartbeatInterval = null;
|
|
1175
|
+
isShuttingDown = false;
|
|
1176
|
+
constructor(baseRegistry) {
|
|
1177
|
+
this.instanceId = generateInstanceId();
|
|
1178
|
+
const homeDir = os2.homedir();
|
|
1179
|
+
this.registryPath = path3.join(homeDir, REGISTRY_FILE);
|
|
1180
|
+
this.lockPath = path3.join(homeDir, LOCKFILE);
|
|
1181
|
+
this.baseRegistry = baseRegistry ?? getProcessRegistry();
|
|
1182
|
+
this.ensureDirectory().catch((err) => {
|
|
1183
|
+
console.error("PersistentProcessRegistry: failed to create .wrongstack dir", err);
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
async ensureDirectory() {
|
|
1187
|
+
const dir = path3.dirname(this.registryPath);
|
|
1188
|
+
try {
|
|
1189
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
1190
|
+
} catch (err) {
|
|
1191
|
+
if (err.code !== "EEXIST") throw err;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Start the heartbeat and periodic cleanup tasks.
|
|
1196
|
+
*/
|
|
1197
|
+
start() {
|
|
1198
|
+
if (this.heartbeatInterval) return;
|
|
1199
|
+
this.syncToPersistent();
|
|
1200
|
+
this.heartbeatInterval = setInterval(() => {
|
|
1201
|
+
this.heartbeat();
|
|
1202
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
1203
|
+
this.heartbeatInterval.unref?.();
|
|
1204
|
+
setInterval(() => {
|
|
1205
|
+
this.cleanupStaleEntries();
|
|
1206
|
+
}, STALE_THRESHOLD_MS).unref?.();
|
|
1207
|
+
this.registerMainProcess();
|
|
1208
|
+
process.on("exit", () => this.syncToPersistent());
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Stop the heartbeat and clean up.
|
|
1212
|
+
*/
|
|
1213
|
+
stop() {
|
|
1214
|
+
this.isShuttingDown = true;
|
|
1215
|
+
if (this.heartbeatInterval) {
|
|
1216
|
+
clearInterval(this.heartbeatInterval);
|
|
1217
|
+
this.heartbeatInterval = null;
|
|
1218
|
+
}
|
|
1219
|
+
this.syncToPersistent();
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Register the main WrongStack process as protected.
|
|
1223
|
+
*/
|
|
1224
|
+
registerMainProcess() {
|
|
1225
|
+
const mainPid = process.pid;
|
|
1226
|
+
this.updatePersistentEntry({
|
|
1227
|
+
pid: mainPid,
|
|
1228
|
+
name: "wrongstack-main",
|
|
1229
|
+
command: process.argv.slice(0, 3).join(" "),
|
|
1230
|
+
startedAt: Date.now(),
|
|
1231
|
+
lastHeartbeat: Date.now(),
|
|
1232
|
+
instanceId: this.instanceId,
|
|
1233
|
+
hostname: os2.hostname(),
|
|
1234
|
+
protected: true,
|
|
1235
|
+
spawnMode: "main",
|
|
1236
|
+
parentPid: process.ppid,
|
|
1237
|
+
platform: process.platform
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Register a spawned child process with the persistent registry.
|
|
1242
|
+
*/
|
|
1243
|
+
registerChildProcess(pid, name, command, sessionId, spawnMode = "spawn") {
|
|
1244
|
+
const entry = {
|
|
1245
|
+
pid,
|
|
1246
|
+
name,
|
|
1247
|
+
command,
|
|
1248
|
+
startedAt: Date.now(),
|
|
1249
|
+
lastHeartbeat: Date.now(),
|
|
1250
|
+
instanceId: this.instanceId,
|
|
1251
|
+
hostname: os2.hostname(),
|
|
1252
|
+
protected: true,
|
|
1253
|
+
// All WrongStack child processes are protected by default
|
|
1254
|
+
spawnMode,
|
|
1255
|
+
parentPid: process.pid,
|
|
1256
|
+
platform: process.platform
|
|
1257
|
+
};
|
|
1258
|
+
if (sessionId) {
|
|
1259
|
+
entry.sessionId = sessionId;
|
|
1260
|
+
}
|
|
1261
|
+
this.updatePersistentEntry(entry);
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Update or add an entry in the persistent registry.
|
|
1265
|
+
*/
|
|
1266
|
+
async updatePersistentEntry(entry) {
|
|
1267
|
+
const release = await acquireLock(this.lockPath);
|
|
1268
|
+
try {
|
|
1269
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1270
|
+
data.instances.set(String(entry.pid), entry);
|
|
1271
|
+
this.baseRegistry.register({
|
|
1272
|
+
pid: entry.pid,
|
|
1273
|
+
name: entry.name,
|
|
1274
|
+
command: entry.command,
|
|
1275
|
+
startedAt: entry.startedAt,
|
|
1276
|
+
sessionId: entry.sessionId,
|
|
1277
|
+
protected: entry.protected,
|
|
1278
|
+
child: null
|
|
1279
|
+
// Main process has no child handle
|
|
1280
|
+
});
|
|
1281
|
+
await writeRegistryFile(this.registryPath, data);
|
|
1282
|
+
} finally {
|
|
1283
|
+
await release();
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Unregister a process from the persistent registry.
|
|
1288
|
+
*/
|
|
1289
|
+
async unregister(pid) {
|
|
1290
|
+
const release = await acquireLock(this.lockPath);
|
|
1291
|
+
try {
|
|
1292
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1293
|
+
data.instances.delete(String(pid));
|
|
1294
|
+
await writeRegistryFile(this.registryPath, data);
|
|
1295
|
+
} finally {
|
|
1296
|
+
await release();
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Send heartbeat to mark all this instance's processes as alive.
|
|
1301
|
+
*/
|
|
1302
|
+
heartbeat() {
|
|
1303
|
+
if (this.isShuttingDown) return;
|
|
1304
|
+
this.syncToPersistent();
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Sync this instance's processes to the persistent registry.
|
|
1308
|
+
*/
|
|
1309
|
+
async syncToPersistent() {
|
|
1310
|
+
const release = await acquireLock(this.lockPath);
|
|
1311
|
+
try {
|
|
1312
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1313
|
+
const now = Date.now();
|
|
1314
|
+
const updatedInstances = /* @__PURE__ */ new Map();
|
|
1315
|
+
for (const [_pidStr, entry] of data.instances) {
|
|
1316
|
+
if (entry.instanceId === this.instanceId) {
|
|
1317
|
+
entry.lastHeartbeat = now;
|
|
1318
|
+
}
|
|
1319
|
+
if (entry.instanceId === this.instanceId || now - entry.lastHeartbeat < STALE_THRESHOLD_MS) {
|
|
1320
|
+
updatedInstances.set(_pidStr, entry);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
data.instances = updatedInstances;
|
|
1324
|
+
data.lastCleanup = now;
|
|
1325
|
+
await writeRegistryFile(this.registryPath, data);
|
|
1326
|
+
} catch (err) {
|
|
1327
|
+
console.error("PersistentProcessRegistry: sync failed", err);
|
|
1328
|
+
} finally {
|
|
1329
|
+
await release();
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Remove entries for processes that are no longer running.
|
|
1334
|
+
*/
|
|
1335
|
+
async cleanupStaleEntries() {
|
|
1336
|
+
const release = await acquireLock(this.lockPath);
|
|
1337
|
+
try {
|
|
1338
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1339
|
+
const now = Date.now();
|
|
1340
|
+
const stalePids = [];
|
|
1341
|
+
for (const [_pidStr, entry] of data.instances) {
|
|
1342
|
+
const age = now - entry.lastHeartbeat;
|
|
1343
|
+
if (age > STALE_THRESHOLD_MS) {
|
|
1344
|
+
try {
|
|
1345
|
+
if (process.platform !== "win32") {
|
|
1346
|
+
process.kill(entry.pid, 0);
|
|
1347
|
+
} else {
|
|
1348
|
+
console.log(`PersistentProcessRegistry: checking stale pid ${entry.pid} (${age}ms old)`);
|
|
1349
|
+
}
|
|
1350
|
+
} catch {
|
|
1351
|
+
stalePids.push(_pidStr);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
if (stalePids.length > 0) {
|
|
1356
|
+
for (const pidStr of stalePids) {
|
|
1357
|
+
data.instances.delete(pidStr);
|
|
1358
|
+
}
|
|
1359
|
+
await writeRegistryFile(this.registryPath, data);
|
|
1360
|
+
}
|
|
1361
|
+
} catch (err) {
|
|
1362
|
+
console.error("PersistentProcessRegistry: cleanup failed", err);
|
|
1363
|
+
} finally {
|
|
1364
|
+
await release();
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Check if a PID belongs to a WrongStack process and should be protected.
|
|
1369
|
+
*/
|
|
1370
|
+
async isProtectedPid(pid) {
|
|
1371
|
+
const release = await acquireLock(this.lockPath);
|
|
1372
|
+
try {
|
|
1373
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1374
|
+
const entry = data.instances.get(String(pid));
|
|
1375
|
+
if (!entry) return false;
|
|
1376
|
+
if (Date.now() - entry.lastHeartbeat > STALE_THRESHOLD_MS) {
|
|
1377
|
+
return false;
|
|
1378
|
+
}
|
|
1379
|
+
return entry.protected;
|
|
1380
|
+
} finally {
|
|
1381
|
+
await release();
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Get all protected PIDs from all WrongStack instances.
|
|
1386
|
+
*/
|
|
1387
|
+
async getAllProtectedPids() {
|
|
1388
|
+
const release = await acquireLock(this.lockPath);
|
|
1389
|
+
try {
|
|
1390
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1391
|
+
const now = Date.now();
|
|
1392
|
+
const protectedPids = [];
|
|
1393
|
+
for (const [_pidStr, entry] of data.instances) {
|
|
1394
|
+
if (entry.protected && now - entry.lastHeartbeat < STALE_THRESHOLD_MS) {
|
|
1395
|
+
protectedPids.push(entry.pid);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return protectedPids;
|
|
1399
|
+
} finally {
|
|
1400
|
+
await release();
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Get complete status of all tracked processes across all instances.
|
|
1405
|
+
*/
|
|
1406
|
+
async getGlobalStatus() {
|
|
1407
|
+
const release = await acquireLock(this.lockPath);
|
|
1408
|
+
try {
|
|
1409
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1410
|
+
const now = Date.now();
|
|
1411
|
+
const instances = /* @__PURE__ */ new Map();
|
|
1412
|
+
let protectedCount = 0;
|
|
1413
|
+
let staleCount = 0;
|
|
1414
|
+
for (const [_pidStr, entry] of data.instances) {
|
|
1415
|
+
const instanceEntries = instances.get(entry.instanceId) ?? [];
|
|
1416
|
+
instanceEntries.push(entry);
|
|
1417
|
+
instances.set(entry.instanceId, instanceEntries);
|
|
1418
|
+
if (entry.protected) protectedCount++;
|
|
1419
|
+
if (now - entry.lastHeartbeat > STALE_THRESHOLD_MS) staleCount++;
|
|
1420
|
+
}
|
|
1421
|
+
return {
|
|
1422
|
+
instances,
|
|
1423
|
+
totalProcesses: data.instances.size,
|
|
1424
|
+
protectedCount,
|
|
1425
|
+
staleCount
|
|
1426
|
+
};
|
|
1427
|
+
} finally {
|
|
1428
|
+
await release();
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Get the instance ID for this process.
|
|
1433
|
+
*/
|
|
1434
|
+
getInstanceId() {
|
|
1435
|
+
return this.instanceId;
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Check if a kill command should be blocked.
|
|
1439
|
+
* Returns true if the kill should be blocked (target is a WrongStack process).
|
|
1440
|
+
*/
|
|
1441
|
+
async shouldBlockKill(pid) {
|
|
1442
|
+
const protectedPids = await this.getAllProtectedPids();
|
|
1443
|
+
return protectedPids.includes(pid);
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Add a pattern-based protection rule.
|
|
1447
|
+
* Processes whose command matches any protected pattern are protected.
|
|
1448
|
+
*/
|
|
1449
|
+
async addProtectedPattern(pattern) {
|
|
1450
|
+
const release = await acquireLock(this.lockPath);
|
|
1451
|
+
try {
|
|
1452
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1453
|
+
if (!data.protectedPatterns.includes(pattern)) {
|
|
1454
|
+
data.protectedPatterns.push(pattern);
|
|
1455
|
+
await writeRegistryFile(this.registryPath, data);
|
|
1456
|
+
}
|
|
1457
|
+
} finally {
|
|
1458
|
+
await release();
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
};
|
|
1462
|
+
var _persistentRegistry;
|
|
1463
|
+
function getPersistentProcessRegistry() {
|
|
1464
|
+
if (!_persistentRegistry) {
|
|
1465
|
+
_persistentRegistry = new PersistentProcessRegistry();
|
|
1466
|
+
}
|
|
1467
|
+
return _persistentRegistry;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// src/bash-kill-guard.ts
|
|
1471
|
+
function extractKillCommand(command) {
|
|
1472
|
+
const normalized = command.replace(/\s+/g, " ").trim();
|
|
1473
|
+
const shellCMatch = normalized.match(
|
|
1474
|
+
/^(?:\/\w+)?\/?(?:bin|usr)\/(?:ba)?sh\s+-[c]\s+['"](.+?)['"]$/
|
|
1475
|
+
);
|
|
1476
|
+
if (shellCMatch?.[1]) {
|
|
1477
|
+
const inner = shellCMatch[1].trim();
|
|
1478
|
+
return isKillRelatedCommand(inner) ? inner : null;
|
|
1479
|
+
}
|
|
1480
|
+
const shellCUnquoted = normalized.match(
|
|
1481
|
+
/^(?:\/\w+)?\/?(?:bin|usr)\/(?:ba)?sh\s+-[c]\s+(kill(?:\s+-[a-zA-Z]+)?(?:\s+\d+)+)$/
|
|
1482
|
+
);
|
|
1483
|
+
if (shellCUnquoted?.[1]) {
|
|
1484
|
+
return shellCUnquoted[1];
|
|
1485
|
+
}
|
|
1486
|
+
return null;
|
|
1487
|
+
}
|
|
1488
|
+
function isKillRelatedCommand(cmd) {
|
|
1489
|
+
const normalized = cmd.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1490
|
+
if (/^kill(\s|$)/.test(normalized)) return true;
|
|
1491
|
+
if (/^(pkill|killall|pgrep|skill)\s/.test(normalized)) return true;
|
|
1492
|
+
if (/^taskkill\s/i.test(normalized)) return true;
|
|
1493
|
+
if (/^tskill\s/i.test(normalized)) return true;
|
|
1494
|
+
if (/^\/proc\/\d+\/(?:kill|fd)/.test(normalized)) return true;
|
|
1495
|
+
return false;
|
|
1496
|
+
}
|
|
1497
|
+
function parseKillCommand(command) {
|
|
1498
|
+
const normalized = command.replace(/\s+/g, " ").trim();
|
|
1499
|
+
const simpleMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z]+)\s+)?(\d+|-?\d+)$/);
|
|
1500
|
+
if (simpleMatch) {
|
|
1501
|
+
const signal = simpleMatch[1] ?? "-TERM";
|
|
1502
|
+
const pidOrGroup = simpleMatch[2];
|
|
1503
|
+
if (!pidOrGroup) return null;
|
|
1504
|
+
const isGroupKill = pidOrGroup.startsWith("-");
|
|
1505
|
+
const pid = isGroupKill ? parseInt(pidOrGroup.slice(1), 10) : parseInt(pidOrGroup, 10);
|
|
1506
|
+
return {
|
|
1507
|
+
pid,
|
|
1508
|
+
signal: signal.slice(1),
|
|
1509
|
+
isGroupKill,
|
|
1510
|
+
isAllKill: false,
|
|
1511
|
+
originalCommand: command
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
const pkillMatch = normalized.match(/^pkill\s+(?:(-[a-zA-Z]+)\s+)?(.+)$/);
|
|
1515
|
+
if (pkillMatch?.[2]) {
|
|
1516
|
+
const name = pkillMatch[2];
|
|
1517
|
+
const signalMatch = pkillMatch[1];
|
|
1518
|
+
return {
|
|
1519
|
+
name,
|
|
1520
|
+
signal: signalMatch ? signalMatch.slice(1) : "TERM",
|
|
1521
|
+
isGroupKill: false,
|
|
1522
|
+
isAllKill: false,
|
|
1523
|
+
originalCommand: command
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
const killallMatch = normalized.match(/^killall\s+(?:(-[a-zA-Z]+)\s+)?(.+)$/);
|
|
1527
|
+
if (killallMatch?.[2]) {
|
|
1528
|
+
const name = killallMatch[2];
|
|
1529
|
+
const signalMatch = killallMatch[1];
|
|
1530
|
+
return {
|
|
1531
|
+
name,
|
|
1532
|
+
signal: signalMatch ? signalMatch.slice(1) : "TERM",
|
|
1533
|
+
isGroupKill: false,
|
|
1534
|
+
isAllKill: false,
|
|
1535
|
+
originalCommand: command
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
const pgrepMatch = normalized.match(/^pgrep\s+(.+)$/);
|
|
1539
|
+
if (pgrepMatch) {
|
|
1540
|
+
return null;
|
|
1541
|
+
}
|
|
1542
|
+
const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
|
|
1543
|
+
if (taskkillMatch?.[1]) {
|
|
1544
|
+
const pidStr = taskkillMatch[1];
|
|
1545
|
+
return {
|
|
1546
|
+
pid: parseInt(pidStr, 10),
|
|
1547
|
+
signal: normalized.includes("/F") ? "FORCE" : "TERM",
|
|
1548
|
+
isGroupKill: false,
|
|
1549
|
+
isAllKill: false,
|
|
1550
|
+
originalCommand: command
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
|
|
1554
|
+
if (tskillMatch?.[1]) {
|
|
1555
|
+
const pidStr = tskillMatch[1];
|
|
1556
|
+
return {
|
|
1557
|
+
pid: parseInt(pidStr, 10),
|
|
1558
|
+
signal: "TERM",
|
|
1559
|
+
isGroupKill: false,
|
|
1560
|
+
isAllKill: false,
|
|
1561
|
+
originalCommand: command
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
return null;
|
|
1565
|
+
}
|
|
1566
|
+
async function getProtectedEntries() {
|
|
1567
|
+
const registry = getPersistentProcessRegistry();
|
|
1568
|
+
const status = await registry.getGlobalStatus();
|
|
1569
|
+
const entries = [];
|
|
1570
|
+
for (const instanceEntries of status.instances.values()) {
|
|
1571
|
+
for (const entry of instanceEntries) {
|
|
1572
|
+
if (entry.protected && Date.now() - entry.lastHeartbeat < 3e4) {
|
|
1573
|
+
entries.push(entry);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
return entries;
|
|
1578
|
+
}
|
|
1579
|
+
async function isKillProtected(kill) {
|
|
1580
|
+
const registry = getPersistentProcessRegistry();
|
|
1581
|
+
if (kill.name) {
|
|
1582
|
+
const entries = await getProtectedEntries();
|
|
1583
|
+
const killNameLower = kill.name.toLowerCase();
|
|
1584
|
+
for (const entry of entries) {
|
|
1585
|
+
if (entry.name && entry.name.toLowerCase().includes(killNameLower)) {
|
|
1586
|
+
return true;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
if (killNameLower.includes("wrongstack")) {
|
|
1590
|
+
return true;
|
|
1591
|
+
}
|
|
1592
|
+
if (killNameLower.includes("node") && entries.length > 0) {
|
|
1593
|
+
return true;
|
|
1594
|
+
}
|
|
1595
|
+
return false;
|
|
1596
|
+
}
|
|
1597
|
+
if (kill.isGroupKill) {
|
|
1598
|
+
const protectedPids = await registry.getAllProtectedPids();
|
|
1599
|
+
return protectedPids.length > 0;
|
|
1600
|
+
}
|
|
1601
|
+
if (kill.pid !== void 0) {
|
|
1602
|
+
return registry.shouldBlockKill(kill.pid);
|
|
1603
|
+
}
|
|
1604
|
+
return false;
|
|
1605
|
+
}
|
|
1606
|
+
async function checkAndBlockKillCommand(command) {
|
|
1607
|
+
const normalized = command.replace(/\s+/g, " ").trim();
|
|
1608
|
+
const killCmd = extractKillCommand(normalized) || (isKillRelatedCommand(normalized) ? normalized : null);
|
|
1609
|
+
if (!killCmd) {
|
|
1610
|
+
return { blocked: false };
|
|
1611
|
+
}
|
|
1612
|
+
const parsed = parseKillCommand(killCmd);
|
|
1613
|
+
if (!parsed) {
|
|
1614
|
+
if (killCmd.includes("kill") && /kill\s+.*\|/.test(killCmd)) {
|
|
1615
|
+
return {
|
|
1616
|
+
blocked: true,
|
|
1617
|
+
reason: `Blocked: complex kill pipeline detected \u2014 "${killCmd.slice(0, 50)}..."`
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
return { blocked: false };
|
|
1621
|
+
}
|
|
1622
|
+
if (await isKillProtected(parsed)) {
|
|
1623
|
+
let target;
|
|
1624
|
+
if (parsed.name) {
|
|
1625
|
+
target = `process name "${parsed.name}"`;
|
|
1626
|
+
} else if (parsed.pid !== void 0) {
|
|
1627
|
+
target = `PID ${parsed.pid}`;
|
|
1628
|
+
} else {
|
|
1629
|
+
target = "(unknown target)";
|
|
1630
|
+
}
|
|
1631
|
+
const signal = parsed.signal ? ` (${parsed.signal})` : "";
|
|
1632
|
+
const groupNote = parsed.isGroupKill ? " (process group)" : "";
|
|
1633
|
+
return {
|
|
1634
|
+
blocked: true,
|
|
1635
|
+
reason: `Blocked: kill${signal} ${target}${groupNote} targets a protected WrongStack process.`
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
return { blocked: false };
|
|
1639
|
+
}
|
|
1070
1640
|
|
|
1071
1641
|
// src/bash.ts
|
|
1072
1642
|
var MAX_OUTPUT = 32768;
|
|
@@ -1136,6 +1706,20 @@ var bashTool = {
|
|
|
1136
1706
|
};
|
|
1137
1707
|
return;
|
|
1138
1708
|
}
|
|
1709
|
+
const killCheck = await checkAndBlockKillCommand(input.command);
|
|
1710
|
+
if (killCheck.blocked) {
|
|
1711
|
+
yield {
|
|
1712
|
+
type: "final",
|
|
1713
|
+
output: {
|
|
1714
|
+
output: "",
|
|
1715
|
+
exit_code: 1,
|
|
1716
|
+
timed_out: false,
|
|
1717
|
+
pid: null,
|
|
1718
|
+
error: killCheck.reason || "Kill command blocked: targets a protected WrongStack process."
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1139
1723
|
const PIPE_TO_SHELL_PATTERN = /\|\s*(sh|bash|ksh|zsh|fish|cmd|powershell|pwsh)/i;
|
|
1140
1724
|
if (PIPE_TO_SHELL_PATTERN.test(input.command)) {
|
|
1141
1725
|
console.warn(JSON.stringify({
|
|
@@ -1148,7 +1732,7 @@ var bashTool = {
|
|
|
1148
1732
|
}));
|
|
1149
1733
|
}
|
|
1150
1734
|
const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
|
|
1151
|
-
const isWin3 =
|
|
1735
|
+
const isWin3 = os2.platform() === "win32";
|
|
1152
1736
|
const shell = (() => {
|
|
1153
1737
|
const explicit = process.env[isWin3 ? "WRONGSTACK_COMSPEC" : "WRONGSTACK_SHELL"];
|
|
1154
1738
|
if (explicit) return explicit;
|
|
@@ -1762,7 +2346,7 @@ function loadDatabaseSync() {
|
|
|
1762
2346
|
DatabaseSyncCtor = req("node:sqlite").DatabaseSync;
|
|
1763
2347
|
} catch (err) {
|
|
1764
2348
|
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)}`
|
|
2349
|
+
`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
2350
|
);
|
|
1767
2351
|
}
|
|
1768
2352
|
return DatabaseSyncCtor;
|
|
@@ -1911,33 +2495,53 @@ var IndexStore = class {
|
|
|
1911
2495
|
}
|
|
1912
2496
|
}
|
|
1913
2497
|
// ─── Symbol CRUD ─────────────────────────────────────────────────────────────
|
|
1914
|
-
|
|
2498
|
+
/**
|
|
2499
|
+
* Insert symbols, assigning IDs atomically inside `BEGIN IMMEDIATE` /
|
|
2500
|
+
* `COMMIT`. The ID allocation (`SELECT MAX(id)`) and all `INSERT`s share
|
|
2501
|
+
* the same transaction, preventing UNIQUE constraint violations when two
|
|
2502
|
+
* processes index concurrently (each would see a different `MAX(id)` and
|
|
2503
|
+
* neither can insert with the other's IDs).
|
|
2504
|
+
*
|
|
2505
|
+
* @returns The symbols array with `id` fields populated so the caller can
|
|
2506
|
+
* use them for refs without re-reading from the DB.
|
|
2507
|
+
*/
|
|
2508
|
+
insertSymbols(symbols) {
|
|
1915
2509
|
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
|
|
2510
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
2511
|
+
try {
|
|
2512
|
+
const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
|
|
2513
|
+
let nextId = (maxRows[0]?.m ?? 0) + 1;
|
|
2514
|
+
const stmt = this.db.prepare(
|
|
2515
|
+
`INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
|
|
2516
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1936
2517
|
);
|
|
1937
|
-
ftsStmt
|
|
1938
|
-
|
|
2518
|
+
const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
|
|
2519
|
+
const result = [];
|
|
2520
|
+
for (const s of symbols) {
|
|
2521
|
+
const id = nextId++;
|
|
2522
|
+
stmt.run(
|
|
2523
|
+
id,
|
|
2524
|
+
s.lang,
|
|
2525
|
+
s.kind,
|
|
2526
|
+
s.name,
|
|
2527
|
+
s.file,
|
|
2528
|
+
s.line,
|
|
2529
|
+
s.col,
|
|
2530
|
+
s.signature,
|
|
2531
|
+
s.docComment,
|
|
2532
|
+
s.scope,
|
|
2533
|
+
s.text,
|
|
2534
|
+
s.file
|
|
2535
|
+
);
|
|
2536
|
+
ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
|
|
2537
|
+
result.push({ ...s, id });
|
|
2538
|
+
}
|
|
2539
|
+
this.db.exec("COMMIT");
|
|
2540
|
+
return result;
|
|
2541
|
+
} catch (err) {
|
|
2542
|
+
this.db.exec("ROLLBACK");
|
|
2543
|
+
throw err;
|
|
1939
2544
|
}
|
|
1940
|
-
return id;
|
|
1941
2545
|
});
|
|
1942
2546
|
}
|
|
1943
2547
|
deleteSymbolsForFile(file) {
|
|
@@ -2491,10 +3095,10 @@ function detectLang(file) {
|
|
|
2491
3095
|
if (idx < 0) return null;
|
|
2492
3096
|
return extToLang(file.slice(idx));
|
|
2493
3097
|
}
|
|
2494
|
-
function parseSymbols2(opts) {
|
|
3098
|
+
async function parseSymbols2(opts) {
|
|
2495
3099
|
const { file, content, lang } = opts;
|
|
2496
3100
|
try {
|
|
2497
|
-
return syncGoParse(file, content, lang);
|
|
3101
|
+
return await syncGoParse(file, content, lang);
|
|
2498
3102
|
} catch {
|
|
2499
3103
|
return { file, lang, symbols: [], mtimeMs: Date.now() };
|
|
2500
3104
|
}
|
|
@@ -2731,19 +3335,34 @@ func formatType(t ast.Expr) string {
|
|
|
2731
3335
|
}
|
|
2732
3336
|
}
|
|
2733
3337
|
`;
|
|
2734
|
-
function syncGoParse(filePath, content, lang) {
|
|
2735
|
-
const tmpDir = path3.join(
|
|
3338
|
+
async function syncGoParse(filePath, content, lang) {
|
|
3339
|
+
const tmpDir = path3.join(os2.tmpdir(), "ws-go-parse");
|
|
2736
3340
|
try {
|
|
2737
|
-
|
|
3341
|
+
await fs2.mkdir(tmpDir, { recursive: true });
|
|
2738
3342
|
const scriptPath = path3.join(tmpDir, "parse.go");
|
|
2739
|
-
|
|
2740
|
-
const
|
|
2741
|
-
|
|
2742
|
-
timeout: 15e3,
|
|
2743
|
-
encoding: "utf8",
|
|
3343
|
+
await fs2.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
|
|
3344
|
+
const proc = spawn("go", ["run", scriptPath], {
|
|
3345
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2744
3346
|
windowsHide: true
|
|
2745
3347
|
});
|
|
2746
|
-
|
|
3348
|
+
let stdout = "";
|
|
3349
|
+
proc.stdout?.on("data", (chunk) => {
|
|
3350
|
+
stdout += chunk.toString();
|
|
3351
|
+
});
|
|
3352
|
+
proc.stdin?.write(content);
|
|
3353
|
+
proc.stdin?.end();
|
|
3354
|
+
const { code } = await Promise.race([
|
|
3355
|
+
new Promise((resolve6) => {
|
|
3356
|
+
proc.on("close", (c) => resolve6({ code: c }));
|
|
3357
|
+
}),
|
|
3358
|
+
new Promise(
|
|
3359
|
+
(_, reject) => setTimeout(() => {
|
|
3360
|
+
proc.kill("SIGKILL");
|
|
3361
|
+
reject(new Error("timeout"));
|
|
3362
|
+
}, 15e3)
|
|
3363
|
+
)
|
|
3364
|
+
]).catch(() => ({ code: -1 }));
|
|
3365
|
+
if (code !== 0 || !stdout.trim()) {
|
|
2747
3366
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
2748
3367
|
}
|
|
2749
3368
|
const raw = JSON.parse(stdout.trim());
|
|
@@ -2765,10 +3384,10 @@ function syncGoParse(filePath, content, lang) {
|
|
|
2765
3384
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
2766
3385
|
}
|
|
2767
3386
|
}
|
|
2768
|
-
function parseSymbols3(opts) {
|
|
3387
|
+
async function parseSymbols3(opts) {
|
|
2769
3388
|
const { file, lang } = opts;
|
|
2770
3389
|
try {
|
|
2771
|
-
return syncPyParse(file, lang);
|
|
3390
|
+
return await syncPyParse(file, lang);
|
|
2772
3391
|
} catch {
|
|
2773
3392
|
return { file, lang, symbols: [], mtimeMs: Date.now() };
|
|
2774
3393
|
}
|
|
@@ -2977,18 +3596,32 @@ visitor.visit(tree)
|
|
|
2977
3596
|
|
|
2978
3597
|
print(json.dumps([s.to_dict() for s in syms]))
|
|
2979
3598
|
`;
|
|
2980
|
-
function syncPyParse(filePath, lang) {
|
|
3599
|
+
async function syncPyParse(filePath, lang) {
|
|
2981
3600
|
try {
|
|
2982
|
-
const tmpDir = path3.join(
|
|
2983
|
-
|
|
3601
|
+
const tmpDir = path3.join(os2.tmpdir(), "ws-py-parse");
|
|
3602
|
+
await fs2.mkdir(tmpDir, { recursive: true });
|
|
2984
3603
|
const scriptPath = path3.join(tmpDir, "parse.py");
|
|
2985
|
-
|
|
2986
|
-
const
|
|
2987
|
-
|
|
2988
|
-
encoding: "utf8",
|
|
3604
|
+
await fs2.writeFile(scriptPath, PY_PARSE_SCRIPT, "utf8");
|
|
3605
|
+
const proc = spawn("python", [scriptPath, filePath], {
|
|
3606
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2989
3607
|
windowsHide: true
|
|
2990
3608
|
});
|
|
2991
|
-
|
|
3609
|
+
let stdout = "";
|
|
3610
|
+
proc.stdout?.on("data", (chunk) => {
|
|
3611
|
+
stdout += chunk.toString();
|
|
3612
|
+
});
|
|
3613
|
+
const { code } = await Promise.race([
|
|
3614
|
+
new Promise((resolve6) => {
|
|
3615
|
+
proc.on("close", (c) => resolve6({ code: c }));
|
|
3616
|
+
}),
|
|
3617
|
+
new Promise(
|
|
3618
|
+
(_, reject) => setTimeout(() => {
|
|
3619
|
+
proc.kill("SIGKILL");
|
|
3620
|
+
reject(new Error("timeout"));
|
|
3621
|
+
}, 15e3)
|
|
3622
|
+
)
|
|
3623
|
+
]).catch(() => ({ code: -1 }));
|
|
3624
|
+
if (code !== 0 || !stdout.trim()) {
|
|
2992
3625
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
2993
3626
|
}
|
|
2994
3627
|
const raw = JSON.parse(stdout.trim());
|
|
@@ -3010,11 +3643,11 @@ function syncPyParse(filePath, lang) {
|
|
|
3010
3643
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
3011
3644
|
}
|
|
3012
3645
|
}
|
|
3013
|
-
function parseSymbols4(opts) {
|
|
3646
|
+
async function parseSymbols4(opts) {
|
|
3014
3647
|
const { file, content, lang } = opts;
|
|
3015
3648
|
const nativeAvailable = checkNativeParser();
|
|
3016
3649
|
if (nativeAvailable) {
|
|
3017
|
-
const result = tryNativeParse(file, content);
|
|
3650
|
+
const result = await tryNativeParse(file, content);
|
|
3018
3651
|
if (result) return result;
|
|
3019
3652
|
}
|
|
3020
3653
|
return regexParse({ file, content, lang });
|
|
@@ -3044,25 +3677,34 @@ function checkNativeParser() {
|
|
|
3044
3677
|
return false;
|
|
3045
3678
|
}
|
|
3046
3679
|
}
|
|
3047
|
-
function tryNativeParse(file, content) {
|
|
3680
|
+
async function tryNativeParse(file, content) {
|
|
3048
3681
|
try {
|
|
3049
3682
|
const toolsDir = path3.join(process.cwd(), "tools");
|
|
3050
3683
|
const crateDir = path3.join(toolsDir, "syn-parser");
|
|
3051
3684
|
const tmpFile = path3.join(crateDir, "src", "input.rs");
|
|
3052
|
-
|
|
3053
|
-
const
|
|
3054
|
-
|
|
3055
|
-
["
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3685
|
+
await fs2.writeFile(tmpFile, content, "utf8");
|
|
3686
|
+
const proc = spawn("cargo", ["run", "--manifest-path", path3.join(toolsDir, "Cargo.toml")], {
|
|
3687
|
+
cwd: process.cwd(),
|
|
3688
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3689
|
+
windowsHide: true
|
|
3690
|
+
});
|
|
3691
|
+
let stdout = "";
|
|
3692
|
+
proc.stdout?.on("data", (chunk) => {
|
|
3693
|
+
stdout += chunk.toString();
|
|
3694
|
+
});
|
|
3695
|
+
const { code } = await Promise.race([
|
|
3696
|
+
new Promise((resolve6) => {
|
|
3697
|
+
proc.on("close", (c) => resolve6({ code: c }));
|
|
3698
|
+
}),
|
|
3699
|
+
new Promise(
|
|
3700
|
+
(_, reject) => setTimeout(() => {
|
|
3701
|
+
proc.kill("SIGKILL");
|
|
3702
|
+
reject(new Error("timeout"));
|
|
3703
|
+
}, 15e3)
|
|
3704
|
+
)
|
|
3705
|
+
]).catch(() => ({ code: -1 }));
|
|
3706
|
+
if (code === 0 && stdout.trim()) {
|
|
3707
|
+
const symbols = JSON.parse(stdout.trim());
|
|
3066
3708
|
return {
|
|
3067
3709
|
file,
|
|
3068
3710
|
lang: "rs",
|
|
@@ -3531,7 +4173,7 @@ function compileGitignore(lines) {
|
|
|
3531
4173
|
async function loadGitignoreMatcher(projectRoot) {
|
|
3532
4174
|
let lines = [];
|
|
3533
4175
|
try {
|
|
3534
|
-
const raw = await
|
|
4176
|
+
const raw = await fs2.readFile(path3.join(projectRoot, ".gitignore"), "utf8");
|
|
3535
4177
|
lines = raw.split("\n");
|
|
3536
4178
|
} catch {
|
|
3537
4179
|
}
|
|
@@ -3589,7 +4231,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
3589
4231
|
}
|
|
3590
4232
|
let entries;
|
|
3591
4233
|
try {
|
|
3592
|
-
entries = await
|
|
4234
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
3593
4235
|
} catch {
|
|
3594
4236
|
return;
|
|
3595
4237
|
}
|
|
@@ -3687,7 +4329,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3687
4329
|
batchFiles.map(async (file) => {
|
|
3688
4330
|
let stat11;
|
|
3689
4331
|
try {
|
|
3690
|
-
stat11 = await
|
|
4332
|
+
stat11 = await fs2.stat(file, statOpts);
|
|
3691
4333
|
} catch (e) {
|
|
3692
4334
|
if (isAbortError(e)) throw e;
|
|
3693
4335
|
return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
|
|
@@ -3701,7 +4343,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3701
4343
|
}
|
|
3702
4344
|
let content;
|
|
3703
4345
|
try {
|
|
3704
|
-
content = await
|
|
4346
|
+
content = await fs2.readFile(file, { encoding: "utf8", signal });
|
|
3705
4347
|
} catch (e) {
|
|
3706
4348
|
if (isAbortError(e)) throw e;
|
|
3707
4349
|
return { file, stat: stat11, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
|
|
@@ -3751,9 +4393,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3751
4393
|
filesIndexed++;
|
|
3752
4394
|
continue;
|
|
3753
4395
|
}
|
|
3754
|
-
const
|
|
3755
|
-
const symbolsWithIds = parsed.symbols.map((s, i) => ({ ...s, id: nextId + i }));
|
|
3756
|
-
store.insertSymbols(symbolsWithIds, nextId);
|
|
4396
|
+
const symbolsWithIds = store.insertSymbols(parsed.symbols);
|
|
3757
4397
|
const count = symbolsWithIds.length;
|
|
3758
4398
|
symbolsIndexed += count;
|
|
3759
4399
|
langStats[lang] = (langStats[lang] ?? 0) + count;
|
|
@@ -3782,7 +4422,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3782
4422
|
}
|
|
3783
4423
|
for (const [file_] of existingMeta) {
|
|
3784
4424
|
try {
|
|
3785
|
-
await
|
|
4425
|
+
await fs2.stat(file_);
|
|
3786
4426
|
} catch {
|
|
3787
4427
|
store.deleteFile(file_);
|
|
3788
4428
|
}
|
|
@@ -4032,10 +4672,10 @@ function circuitOpenError() {
|
|
|
4032
4672
|
"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
4673
|
);
|
|
4034
4674
|
}
|
|
4035
|
-
function
|
|
4675
|
+
function isRecoverableConstraintError(err) {
|
|
4036
4676
|
if (err instanceof Error) {
|
|
4037
4677
|
const msg = err.message.toLowerCase();
|
|
4038
|
-
return msg.includes("unique constraint") || msg.includes("
|
|
4678
|
+
return msg.includes("unique constraint") || msg.includes("constraint failed");
|
|
4039
4679
|
}
|
|
4040
4680
|
return false;
|
|
4041
4681
|
}
|
|
@@ -4068,7 +4708,7 @@ async function runStartupIndex(opts) {
|
|
|
4068
4708
|
return result;
|
|
4069
4709
|
} catch (err) {
|
|
4070
4710
|
_lastError = err instanceof Error ? err.message : String(err);
|
|
4071
|
-
if (
|
|
4711
|
+
if (isRecoverableConstraintError(err) && !opts.force) {
|
|
4072
4712
|
_lastError = null;
|
|
4073
4713
|
const rebuildResult = await runStartupIndex({
|
|
4074
4714
|
...opts,
|
|
@@ -4428,9 +5068,9 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
4428
5068
|
const results = [];
|
|
4429
5069
|
for (const file of files) {
|
|
4430
5070
|
const absPath = safeResolve(file, ctx);
|
|
4431
|
-
const stat11 = await
|
|
5071
|
+
const stat11 = await fs2.stat(absPath).catch(() => null);
|
|
4432
5072
|
if (!stat11?.isFile()) continue;
|
|
4433
|
-
const content = await
|
|
5073
|
+
const content = await fs2.readFile(absPath, "utf8");
|
|
4434
5074
|
const lines = content.split(/\r?\n/);
|
|
4435
5075
|
results.push(formatWithLineNumbers(file, lines));
|
|
4436
5076
|
}
|
|
@@ -4490,7 +5130,7 @@ var documentTool = {
|
|
|
4490
5130
|
const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
|
|
4491
5131
|
for (const absPath of fileList) {
|
|
4492
5132
|
try {
|
|
4493
|
-
const content = await
|
|
5133
|
+
const content = await fs2.readFile(absPath, "utf8");
|
|
4494
5134
|
filesProcessed++;
|
|
4495
5135
|
const processed = processFile(
|
|
4496
5136
|
content,
|
|
@@ -4526,7 +5166,7 @@ async function resolveFiles(filesInput, cwd) {
|
|
|
4526
5166
|
for (const f of files) {
|
|
4527
5167
|
const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
|
|
4528
5168
|
try {
|
|
4529
|
-
const stat11 = await
|
|
5169
|
+
const stat11 = await fs2.stat(absPath);
|
|
4530
5170
|
if (stat11.isFile()) resolved.push(absPath);
|
|
4531
5171
|
} catch {
|
|
4532
5172
|
}
|
|
@@ -4619,7 +5259,7 @@ var editTool = {
|
|
|
4619
5259
|
if (input.new_string === void 0) throw new Error("edit: new_string is required");
|
|
4620
5260
|
if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
|
|
4621
5261
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
4622
|
-
const stat11 = await
|
|
5262
|
+
const stat11 = await fs2.stat(absPath).catch((err) => {
|
|
4623
5263
|
if (err.code === "ENOENT") {
|
|
4624
5264
|
throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
|
|
4625
5265
|
}
|
|
@@ -4627,8 +5267,8 @@ var editTool = {
|
|
|
4627
5267
|
});
|
|
4628
5268
|
if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
|
|
4629
5269
|
const autoRead = !ctx.hasRead(absPath);
|
|
4630
|
-
const original = await
|
|
4631
|
-
const updated = await
|
|
5270
|
+
const original = await fs2.readFile(absPath, "utf8");
|
|
5271
|
+
const updated = await fs2.stat(absPath);
|
|
4632
5272
|
const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
|
|
4633
5273
|
const lastReadMtime = ctx.lastReadMtime(absPath);
|
|
4634
5274
|
if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
|
|
@@ -4674,7 +5314,7 @@ var editTool = {
|
|
|
4674
5314
|
const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
|
|
4675
5315
|
const newFile = toStyle(newFileLf, style);
|
|
4676
5316
|
await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
|
|
4677
|
-
const written = await
|
|
5317
|
+
const written = await fs2.stat(absPath);
|
|
4678
5318
|
ctx.recordRead(absPath, written.mtimeMs);
|
|
4679
5319
|
ctx.session.recordFileChange({
|
|
4680
5320
|
path: absPath,
|
|
@@ -4930,6 +5570,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
4930
5570
|
const resolved = resolveWin32Command(cmd);
|
|
4931
5571
|
const needsShell = isWin2 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
4932
5572
|
const spawnCmd = needsShell ? cmd : resolved;
|
|
5573
|
+
if (needsShell) assertSafeWin32ShellArgs(args);
|
|
4933
5574
|
const child = spawn(spawnCmd, args, {
|
|
4934
5575
|
cwd,
|
|
4935
5576
|
env: buildChildEnv(sessionId),
|
|
@@ -5023,8 +5664,8 @@ if (ALLOW_PRIVATE && !process.env["CI"]) {
|
|
|
5023
5664
|
);
|
|
5024
5665
|
}
|
|
5025
5666
|
var combineSignals = (signals) => AbortSignal.any(signals);
|
|
5026
|
-
function guardedLookup(
|
|
5027
|
-
dns.lookup(
|
|
5667
|
+
function guardedLookup(hostname2, options, callback) {
|
|
5668
|
+
dns.lookup(hostname2, { all: true }).then((records) => {
|
|
5028
5669
|
const family = options?.family;
|
|
5029
5670
|
const byFamily = family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;
|
|
5030
5671
|
const list = byFamily.length > 0 ? byFamily : records;
|
|
@@ -5051,7 +5692,7 @@ function guardedLookup(hostname, options, callback) {
|
|
|
5051
5692
|
const first = list.at(0);
|
|
5052
5693
|
if (!first) {
|
|
5053
5694
|
callback(
|
|
5054
|
-
Object.assign(new Error(`fetch: no address for ${
|
|
5695
|
+
Object.assign(new Error(`fetch: no address for ${hostname2}`), { code: "ENOTFOUND" })
|
|
5055
5696
|
);
|
|
5056
5697
|
return;
|
|
5057
5698
|
}
|
|
@@ -5165,7 +5806,13 @@ var fetchTool = {
|
|
|
5165
5806
|
const timer = setTimeout(() => ctrl.abort(new Error("fetch timeout")), TIMEOUT_MS);
|
|
5166
5807
|
const combined = combineSignals([opts.signal, ctrl.signal]);
|
|
5167
5808
|
try {
|
|
5168
|
-
|
|
5809
|
+
let res;
|
|
5810
|
+
try {
|
|
5811
|
+
res = await guardedFetch(input.url, 5, combined);
|
|
5812
|
+
} catch (err) {
|
|
5813
|
+
if (opts.signal.aborted) throw err;
|
|
5814
|
+
throw describeFetchError(err, input.url, ctrl.signal.aborted);
|
|
5815
|
+
}
|
|
5169
5816
|
const ct = res.headers.get("content-type") ?? "application/octet-stream";
|
|
5170
5817
|
if (/^image\/|^audio\/|^video\/|application\/octet-stream/.test(ct)) {
|
|
5171
5818
|
throw new Error(`fetch: refusing to read binary content-type "${ct}"`);
|
|
@@ -5221,9 +5868,9 @@ var fetchTool = {
|
|
|
5221
5868
|
}
|
|
5222
5869
|
}
|
|
5223
5870
|
};
|
|
5224
|
-
async function assertNotPrivate(
|
|
5871
|
+
async function assertNotPrivate(hostname2) {
|
|
5225
5872
|
if (ALLOW_PRIVATE) return;
|
|
5226
|
-
const host =
|
|
5873
|
+
const host = hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
|
|
5227
5874
|
if (host === "localhost" || host.endsWith(".localhost")) {
|
|
5228
5875
|
throw new Error("fetch: blocked localhost target");
|
|
5229
5876
|
}
|
|
@@ -5250,6 +5897,23 @@ async function assertNotPrivate(hostname) {
|
|
|
5250
5897
|
}
|
|
5251
5898
|
}
|
|
5252
5899
|
}
|
|
5900
|
+
function describeFetchError(err, url, timedOut) {
|
|
5901
|
+
if (timedOut) {
|
|
5902
|
+
return new Error(`fetch: GET ${url} timed out after ${TIMEOUT_MS}ms`);
|
|
5903
|
+
}
|
|
5904
|
+
const parts = [];
|
|
5905
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5906
|
+
let cur = err;
|
|
5907
|
+
while (cur instanceof Error && !seen.has(cur)) {
|
|
5908
|
+
seen.add(cur);
|
|
5909
|
+
const code = cur.code;
|
|
5910
|
+
const label = code ? `${code}: ${cur.message}` : cur.message;
|
|
5911
|
+
if (label && label !== "fetch failed" && !parts.includes(label)) parts.push(label);
|
|
5912
|
+
cur = cur.cause;
|
|
5913
|
+
}
|
|
5914
|
+
const detail = parts.length > 0 ? parts.join(" \u2192 ") : "fetch failed";
|
|
5915
|
+
return new Error(`fetch: GET ${url} failed \u2014 ${detail}`);
|
|
5916
|
+
}
|
|
5253
5917
|
function prettyJson(s) {
|
|
5254
5918
|
try {
|
|
5255
5919
|
return JSON.stringify(JSON.parse(s), null, 2);
|
|
@@ -5681,7 +6345,7 @@ var globTool = {
|
|
|
5681
6345
|
}
|
|
5682
6346
|
let entries;
|
|
5683
6347
|
try {
|
|
5684
|
-
entries = await
|
|
6348
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
5685
6349
|
} catch {
|
|
5686
6350
|
return;
|
|
5687
6351
|
}
|
|
@@ -5697,7 +6361,7 @@ var globTool = {
|
|
|
5697
6361
|
} else if (e.isFile()) {
|
|
5698
6362
|
if (re.test(rel) || re.test(name)) {
|
|
5699
6363
|
try {
|
|
5700
|
-
const st = await
|
|
6364
|
+
const st = await fs2.stat(full);
|
|
5701
6365
|
results.push({ rel: full, mtime: st.mtimeMs });
|
|
5702
6366
|
if (results.length >= limit) {
|
|
5703
6367
|
truncated = true;
|
|
@@ -5716,7 +6380,7 @@ var globTool = {
|
|
|
5716
6380
|
};
|
|
5717
6381
|
async function readGitignore(dir) {
|
|
5718
6382
|
try {
|
|
5719
|
-
const raw = await
|
|
6383
|
+
const raw = await fs2.readFile(path3.join(dir, ".gitignore"), "utf8");
|
|
5720
6384
|
return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
5721
6385
|
} catch {
|
|
5722
6386
|
return [];
|
|
@@ -5770,6 +6434,7 @@ function capSubject(line) {
|
|
|
5770
6434
|
|
|
5771
6435
|
// src/grep.ts
|
|
5772
6436
|
var DEFAULT_IGNORE3 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
|
|
6437
|
+
var NATIVE_SCAN_CONCURRENCY = 32;
|
|
5773
6438
|
var grepTool = {
|
|
5774
6439
|
name: "grep",
|
|
5775
6440
|
category: "Search",
|
|
@@ -5997,14 +6662,52 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
5997
6662
|
const fileMatches = /* @__PURE__ */ new Map();
|
|
5998
6663
|
let total = 0;
|
|
5999
6664
|
let stopped = false;
|
|
6665
|
+
const scanFile = async (full, name) => {
|
|
6666
|
+
if (stopped || signal.aborted) return;
|
|
6667
|
+
if (globRe && !globRe.test(name) && !globRe.test(full)) return;
|
|
6668
|
+
if (globRe) globRe.lastIndex = 0;
|
|
6669
|
+
try {
|
|
6670
|
+
const stat11 = await fs2.stat(full);
|
|
6671
|
+
if (stat11.size > 1e6 || stopped || signal.aborted) return;
|
|
6672
|
+
const head = await fs2.readFile(full);
|
|
6673
|
+
if (isBinaryBuffer(head) || stopped || signal.aborted) return;
|
|
6674
|
+
const text = head.toString("utf8");
|
|
6675
|
+
const lines = text.split(/\r?\n/);
|
|
6676
|
+
let fileHits = 0;
|
|
6677
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6678
|
+
if (stopped || signal.aborted) break;
|
|
6679
|
+
const ln = capSubject(lines[i] ?? "");
|
|
6680
|
+
re.lastIndex = 0;
|
|
6681
|
+
if (re.test(ln)) {
|
|
6682
|
+
fileHits++;
|
|
6683
|
+
total++;
|
|
6684
|
+
if (mode === "content" && matches.length < limit) {
|
|
6685
|
+
matches.push(`${full}:${i + 1}:${ln}`);
|
|
6686
|
+
}
|
|
6687
|
+
}
|
|
6688
|
+
}
|
|
6689
|
+
if (fileHits > 0) {
|
|
6690
|
+
fileMatches.set(full, fileHits);
|
|
6691
|
+
if (mode === "files_with_matches" && matches.length < limit) {
|
|
6692
|
+
matches.push(full);
|
|
6693
|
+
}
|
|
6694
|
+
if (mode === "count" && matches.length < limit) {
|
|
6695
|
+
matches.push(`${full}:${fileHits}`);
|
|
6696
|
+
}
|
|
6697
|
+
}
|
|
6698
|
+
if (matches.length >= limit) stopped = true;
|
|
6699
|
+
} catch {
|
|
6700
|
+
}
|
|
6701
|
+
};
|
|
6000
6702
|
const walk = async (dir) => {
|
|
6001
6703
|
if (stopped || signal.aborted) return;
|
|
6002
6704
|
let entries;
|
|
6003
6705
|
try {
|
|
6004
|
-
entries = await
|
|
6706
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
6005
6707
|
} catch {
|
|
6006
6708
|
return;
|
|
6007
6709
|
}
|
|
6710
|
+
const files = [];
|
|
6008
6711
|
for (const e of entries) {
|
|
6009
6712
|
if (stopped) return;
|
|
6010
6713
|
if (DEFAULT_IGNORE3.includes(e.name)) continue;
|
|
@@ -6013,41 +6716,10 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
6013
6716
|
if (e.isDirectory()) {
|
|
6014
6717
|
await walk(full);
|
|
6015
6718
|
} 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
|
-
}
|
|
6719
|
+
files.push({ full, name: e.name });
|
|
6049
6720
|
}
|
|
6050
6721
|
}
|
|
6722
|
+
await mapWithConcurrency(files, NATIVE_SCAN_CONCURRENCY, ({ full, name }) => scanFile(full, name));
|
|
6051
6723
|
};
|
|
6052
6724
|
await walk(base);
|
|
6053
6725
|
return {
|
|
@@ -6057,6 +6729,20 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
6057
6729
|
used: "native"
|
|
6058
6730
|
};
|
|
6059
6731
|
}
|
|
6732
|
+
async function mapWithConcurrency(items, concurrency, fn) {
|
|
6733
|
+
if (items.length === 0) return;
|
|
6734
|
+
let next = 0;
|
|
6735
|
+
const workerCount = Math.min(Math.max(1, concurrency), items.length);
|
|
6736
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
6737
|
+
for (; ; ) {
|
|
6738
|
+
const idx = next++;
|
|
6739
|
+
if (idx >= items.length) return;
|
|
6740
|
+
const item = items[idx];
|
|
6741
|
+
if (item !== void 0) await fn(item);
|
|
6742
|
+
}
|
|
6743
|
+
});
|
|
6744
|
+
await Promise.all(workers);
|
|
6745
|
+
}
|
|
6060
6746
|
var installTool = {
|
|
6061
6747
|
name: "install",
|
|
6062
6748
|
category: "Package Management",
|
|
@@ -6231,7 +6917,7 @@ var jsonTool = {
|
|
|
6231
6917
|
let raw;
|
|
6232
6918
|
if (input.file) {
|
|
6233
6919
|
try {
|
|
6234
|
-
raw = await
|
|
6920
|
+
raw = await fs2.readFile(input.file, "utf8");
|
|
6235
6921
|
} catch {
|
|
6236
6922
|
return { data: null, formatted: "", type: "unknown", error: `Could not read file` };
|
|
6237
6923
|
}
|
|
@@ -6270,8 +6956,8 @@ var jsonTool = {
|
|
|
6270
6956
|
};
|
|
6271
6957
|
}
|
|
6272
6958
|
};
|
|
6273
|
-
function query(data,
|
|
6274
|
-
const parts =
|
|
6959
|
+
function query(data, path21) {
|
|
6960
|
+
const parts = path21.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
6275
6961
|
let current = data;
|
|
6276
6962
|
for (const part of parts) {
|
|
6277
6963
|
if (current === null || current === void 0) return void 0;
|
|
@@ -6548,7 +7234,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
6548
7234
|
}
|
|
6549
7235
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
6550
7236
|
var MAX_TAIL_LINES = 1e5;
|
|
6551
|
-
async function fileLogs(
|
|
7237
|
+
async function fileLogs(path21, lines, filterRe, stream) {
|
|
6552
7238
|
const { createInterface } = await import('node:readline');
|
|
6553
7239
|
const { createReadStream } = await import('node:fs');
|
|
6554
7240
|
const entries = [];
|
|
@@ -6557,7 +7243,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
|
|
|
6557
7243
|
let writeIdx = 0;
|
|
6558
7244
|
let totalLines = 0;
|
|
6559
7245
|
const rl = createInterface({
|
|
6560
|
-
input: createReadStream(
|
|
7246
|
+
input: createReadStream(path21),
|
|
6561
7247
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
6562
7248
|
});
|
|
6563
7249
|
for await (const line of rl) {
|
|
@@ -6578,7 +7264,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
|
|
|
6578
7264
|
if (parsed) entries.push(parsed);
|
|
6579
7265
|
}
|
|
6580
7266
|
return {
|
|
6581
|
-
source:
|
|
7267
|
+
source: path21,
|
|
6582
7268
|
entries,
|
|
6583
7269
|
total: entries.length,
|
|
6584
7270
|
truncated: totalLines > effLines,
|
|
@@ -6682,6 +7368,7 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
6682
7368
|
const resolved = resolveWin32Command(manager);
|
|
6683
7369
|
const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
6684
7370
|
const spawnCmd = needsShell ? manager : resolved;
|
|
7371
|
+
if (needsShell) assertSafeWin32ShellArgs(args);
|
|
6685
7372
|
const child = spawn(spawnCmd, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
|
|
6686
7373
|
child.stdout?.on("data", (c) => {
|
|
6687
7374
|
if (stdout.length < MAX) stdout += c.toString();
|
|
@@ -6779,12 +7466,12 @@ var patchTool = {
|
|
|
6779
7466
|
};
|
|
6780
7467
|
}
|
|
6781
7468
|
}
|
|
6782
|
-
const tmpDir = await
|
|
7469
|
+
const tmpDir = await fs2.mkdtemp(path3.join(os2.tmpdir(), ".wstack_patch_"));
|
|
6783
7470
|
try {
|
|
6784
|
-
await
|
|
7471
|
+
await fs2.chmod(tmpDir, 448).catch(() => {
|
|
6785
7472
|
});
|
|
6786
7473
|
const patchFile = path3.join(tmpDir, "in.diff");
|
|
6787
|
-
await
|
|
7474
|
+
await fs2.writeFile(patchFile, input.patch, { mode: 384 });
|
|
6788
7475
|
const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
|
|
6789
7476
|
const result = await runPatch(args, dir, opts.signal);
|
|
6790
7477
|
if (result.exitCode !== 0 && !dryRun) {
|
|
@@ -6805,7 +7492,7 @@ var patchTool = {
|
|
|
6805
7492
|
message: result.stdout || "patch applied"
|
|
6806
7493
|
};
|
|
6807
7494
|
} finally {
|
|
6808
|
-
await
|
|
7495
|
+
await fs2.rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
6809
7496
|
});
|
|
6810
7497
|
}
|
|
6811
7498
|
}
|
|
@@ -7151,7 +7838,7 @@ var readTool = {
|
|
|
7151
7838
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
7152
7839
|
let stat11;
|
|
7153
7840
|
try {
|
|
7154
|
-
stat11 = await
|
|
7841
|
+
stat11 = await fs2.stat(absPath);
|
|
7155
7842
|
} catch (err) {
|
|
7156
7843
|
const code = err.code;
|
|
7157
7844
|
if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
|
|
@@ -7176,7 +7863,7 @@ var readTool = {
|
|
|
7176
7863
|
note: "Repeated read suppressed to save tokens."
|
|
7177
7864
|
};
|
|
7178
7865
|
}
|
|
7179
|
-
const buf = await
|
|
7866
|
+
const buf = await fs2.readFile(absPath);
|
|
7180
7867
|
if (isBinaryBuffer(buf)) {
|
|
7181
7868
|
throw new Error(`read: "${input.path}" appears to be binary`);
|
|
7182
7869
|
}
|
|
@@ -7322,11 +8009,11 @@ var replaceTool = {
|
|
|
7322
8009
|
const dryRun = input.dry_run ?? false;
|
|
7323
8010
|
const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
|
|
7324
8011
|
const fileList = await resolveFiles2(filesInput, ctx, globRe);
|
|
7325
|
-
const realRoot = await
|
|
8012
|
+
const realRoot = await fs2.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
|
|
7326
8013
|
const results = [];
|
|
7327
8014
|
let totalReplacements = 0;
|
|
7328
8015
|
for (const absPath of fileList) {
|
|
7329
|
-
const lstat2 = await
|
|
8016
|
+
const lstat2 = await fs2.lstat(absPath).catch((err) => {
|
|
7330
8017
|
if (err.code === "ENOENT") return null;
|
|
7331
8018
|
throw err;
|
|
7332
8019
|
});
|
|
@@ -7334,17 +8021,17 @@ var replaceTool = {
|
|
|
7334
8021
|
if (lstat2.isSymbolicLink()) continue;
|
|
7335
8022
|
let realPath;
|
|
7336
8023
|
try {
|
|
7337
|
-
realPath = await
|
|
8024
|
+
realPath = await fs2.realpath(absPath);
|
|
7338
8025
|
} catch {
|
|
7339
8026
|
continue;
|
|
7340
8027
|
}
|
|
7341
8028
|
const rel = path3.relative(realRoot, realPath);
|
|
7342
8029
|
if (rel.startsWith("..") || path3.isAbsolute(rel)) continue;
|
|
7343
|
-
const stat11 = await
|
|
8030
|
+
const stat11 = await fs2.stat(realPath).catch(() => null);
|
|
7344
8031
|
if (!stat11 || !stat11.isFile()) continue;
|
|
7345
8032
|
let content;
|
|
7346
8033
|
try {
|
|
7347
|
-
const buf = await
|
|
8034
|
+
const buf = await fs2.readFile(realPath);
|
|
7348
8035
|
if (isBinaryBuffer(buf)) continue;
|
|
7349
8036
|
content = buf.toString("utf8");
|
|
7350
8037
|
} catch {
|
|
@@ -7396,7 +8083,7 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
7396
8083
|
const resolved = [];
|
|
7397
8084
|
for (const p of parts) {
|
|
7398
8085
|
const absPath = safeResolve(p, ctx);
|
|
7399
|
-
const stat11 = await
|
|
8086
|
+
const stat11 = await fs2.stat(absPath).catch(() => null);
|
|
7400
8087
|
if (stat11?.isFile()) {
|
|
7401
8088
|
resolved.push(absPath);
|
|
7402
8089
|
}
|
|
@@ -7452,7 +8139,7 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
7452
8139
|
const walk = async (dir) => {
|
|
7453
8140
|
let entries;
|
|
7454
8141
|
try {
|
|
7455
|
-
entries = await
|
|
8142
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
7456
8143
|
} catch {
|
|
7457
8144
|
return;
|
|
7458
8145
|
}
|
|
@@ -7460,7 +8147,7 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
7460
8147
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
7461
8148
|
const full = path3.join(dir, e.name);
|
|
7462
8149
|
try {
|
|
7463
|
-
const stat11 = await
|
|
8150
|
+
const stat11 = await fs2.lstat(full);
|
|
7464
8151
|
if (stat11.isSymbolicLink()) continue;
|
|
7465
8152
|
} catch {
|
|
7466
8153
|
continue;
|
|
@@ -7638,7 +8325,7 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
7638
8325
|
}
|
|
7639
8326
|
const fullPath = target;
|
|
7640
8327
|
if (!dryRun) {
|
|
7641
|
-
await
|
|
8328
|
+
await fs2.mkdir(path3.dirname(fullPath), { recursive: true });
|
|
7642
8329
|
await atomicWrite(fullPath, substituteVars(content, name, vars));
|
|
7643
8330
|
}
|
|
7644
8331
|
files.push(resolvedPath);
|
|
@@ -7751,7 +8438,7 @@ async function duckduckgoSearch(query2, num, signal) {
|
|
|
7751
8438
|
truncated: results.length >= num
|
|
7752
8439
|
};
|
|
7753
8440
|
} catch (err) {
|
|
7754
|
-
console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: toErrorMessage(err) }));
|
|
8441
|
+
console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: toErrorMessage$1(err) }));
|
|
7755
8442
|
return {
|
|
7756
8443
|
query: query2,
|
|
7757
8444
|
results: [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }],
|
|
@@ -7915,11 +8602,11 @@ var setWorkingDirTool = {
|
|
|
7915
8602
|
} catch (err) {
|
|
7916
8603
|
return {
|
|
7917
8604
|
current: ctx.workingDir,
|
|
7918
|
-
error: toErrorMessage(err)
|
|
8605
|
+
error: toErrorMessage$1(err)
|
|
7919
8606
|
};
|
|
7920
8607
|
}
|
|
7921
8608
|
try {
|
|
7922
|
-
await
|
|
8609
|
+
await fs2.access(resolved);
|
|
7923
8610
|
} catch {
|
|
7924
8611
|
try {
|
|
7925
8612
|
ctx.setWorkingDir(previous);
|
|
@@ -8975,7 +9662,7 @@ var treeTool = {
|
|
|
8975
9662
|
}
|
|
8976
9663
|
};
|
|
8977
9664
|
async function walkDir(dir, depth, opts) {
|
|
8978
|
-
const entries = await
|
|
9665
|
+
const entries = await fs2.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
8979
9666
|
const filtered = entries.filter((e) => {
|
|
8980
9667
|
if (!opts.showHidden && e.name.startsWith(".")) return false;
|
|
8981
9668
|
if (opts.exclude.has(e.name)) return false;
|
|
@@ -9133,14 +9820,14 @@ var writeTool = {
|
|
|
9133
9820
|
let existed = false;
|
|
9134
9821
|
let prev = "";
|
|
9135
9822
|
try {
|
|
9136
|
-
const stat12 = await
|
|
9823
|
+
const stat12 = await fs2.stat(absPath);
|
|
9137
9824
|
existed = stat12.isFile();
|
|
9138
9825
|
if (existed) {
|
|
9139
9826
|
if (!ctx.hasRead(absPath)) {
|
|
9140
|
-
prev = await
|
|
9827
|
+
prev = await fs2.readFile(absPath, "utf8");
|
|
9141
9828
|
ctx.recordRead(absPath, stat12.mtimeMs);
|
|
9142
9829
|
} else {
|
|
9143
|
-
prev = await
|
|
9830
|
+
prev = await fs2.readFile(absPath, "utf8");
|
|
9144
9831
|
}
|
|
9145
9832
|
}
|
|
9146
9833
|
} catch (err) {
|
|
@@ -9151,7 +9838,7 @@ var writeTool = {
|
|
|
9151
9838
|
await atomicWrite(absPath, input.content);
|
|
9152
9839
|
const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
9153
9840
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
9154
|
-
const stat11 = await
|
|
9841
|
+
const stat11 = await fs2.stat(absPath);
|
|
9155
9842
|
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
9156
9843
|
ctx.session.recordFileChange({
|
|
9157
9844
|
path: absPath,
|