@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/index.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import
|
|
1
|
+
import * as fs7 from 'node:fs/promises';
|
|
2
|
+
import * as Core from '@wrongstack/core';
|
|
3
|
+
import { toErrorMessage, atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, isPrivateIPv4, isPrivateIPv6, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, mutateTasks, formatTaskList, formatPlan, assessCommitSafety, recordPackageAction, detectPackageEcosystem, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
3
4
|
import * as path from 'node:path';
|
|
4
5
|
import { resolve, sep, dirname, join } from 'node:path';
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import * as fs7 from 'node:fs';
|
|
10
|
-
import { statSync, mkdirSync, createWriteStream, writeFileSync } from 'node:fs';
|
|
6
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
7
|
+
import * as os2 from 'node:os';
|
|
8
|
+
import * as fs8 from 'node:fs';
|
|
9
|
+
import { statSync, mkdirSync, createWriteStream } from 'node:fs';
|
|
11
10
|
import * as dns from 'node:dns/promises';
|
|
12
11
|
import * as net from 'node:net';
|
|
13
12
|
import { Agent } from 'undici';
|
|
14
13
|
import TurndownService from 'turndown';
|
|
14
|
+
import { toErrorMessage as toErrorMessage$1 } from '@wrongstack/core/utils';
|
|
15
15
|
import { randomUUID } from 'node:crypto';
|
|
16
16
|
import { createRequire } from 'node:module';
|
|
17
17
|
import { fileURLToPath } from 'node:url';
|
|
@@ -57,13 +57,13 @@ function safeResolve(input, ctx) {
|
|
|
57
57
|
async function assertRealInsideRoot(absPath, ctx) {
|
|
58
58
|
if (ctx.allowOutsideProjectRoot) return;
|
|
59
59
|
const realRoots = await Promise.all(
|
|
60
|
-
allowedRoots(ctx).map((r) =>
|
|
60
|
+
allowedRoots(ctx).map((r) => fs7.realpath(r).catch(() => path.resolve(r)))
|
|
61
61
|
);
|
|
62
62
|
let probe = absPath;
|
|
63
63
|
for (; ; ) {
|
|
64
64
|
let real;
|
|
65
65
|
try {
|
|
66
|
-
real = await
|
|
66
|
+
real = await fs7.realpath(probe);
|
|
67
67
|
} catch (err) {
|
|
68
68
|
if (err.code === "ENOENT") {
|
|
69
69
|
const parent = path.dirname(probe);
|
|
@@ -210,7 +210,7 @@ var readTool = {
|
|
|
210
210
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
211
211
|
let stat11;
|
|
212
212
|
try {
|
|
213
|
-
stat11 = await
|
|
213
|
+
stat11 = await fs7.stat(absPath);
|
|
214
214
|
} catch (err) {
|
|
215
215
|
const code = err.code;
|
|
216
216
|
if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
|
|
@@ -235,7 +235,7 @@ var readTool = {
|
|
|
235
235
|
note: "Repeated read suppressed to save tokens."
|
|
236
236
|
};
|
|
237
237
|
}
|
|
238
|
-
const buf = await
|
|
238
|
+
const buf = await fs7.readFile(absPath);
|
|
239
239
|
if (isBinaryBuffer(buf)) {
|
|
240
240
|
throw new Error(`read: "${input.path}" appears to be binary`);
|
|
241
241
|
}
|
|
@@ -369,14 +369,14 @@ var writeTool = {
|
|
|
369
369
|
let existed = false;
|
|
370
370
|
let prev = "";
|
|
371
371
|
try {
|
|
372
|
-
const stat12 = await
|
|
372
|
+
const stat12 = await fs7.stat(absPath);
|
|
373
373
|
existed = stat12.isFile();
|
|
374
374
|
if (existed) {
|
|
375
375
|
if (!ctx.hasRead(absPath)) {
|
|
376
|
-
prev = await
|
|
376
|
+
prev = await fs7.readFile(absPath, "utf8");
|
|
377
377
|
ctx.recordRead(absPath, stat12.mtimeMs);
|
|
378
378
|
} else {
|
|
379
|
-
prev = await
|
|
379
|
+
prev = await fs7.readFile(absPath, "utf8");
|
|
380
380
|
}
|
|
381
381
|
}
|
|
382
382
|
} catch (err) {
|
|
@@ -387,7 +387,7 @@ var writeTool = {
|
|
|
387
387
|
await atomicWrite(absPath, input.content);
|
|
388
388
|
const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
389
389
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
390
|
-
const stat11 = await
|
|
390
|
+
const stat11 = await fs7.stat(absPath);
|
|
391
391
|
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
392
392
|
ctx.session.recordFileChange({
|
|
393
393
|
path: absPath,
|
|
@@ -429,7 +429,7 @@ var editTool = {
|
|
|
429
429
|
if (input.new_string === void 0) throw new Error("edit: new_string is required");
|
|
430
430
|
if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
|
|
431
431
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
432
|
-
const stat11 = await
|
|
432
|
+
const stat11 = await fs7.stat(absPath).catch((err) => {
|
|
433
433
|
if (err.code === "ENOENT") {
|
|
434
434
|
throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
|
|
435
435
|
}
|
|
@@ -437,8 +437,8 @@ var editTool = {
|
|
|
437
437
|
});
|
|
438
438
|
if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
|
|
439
439
|
const autoRead = !ctx.hasRead(absPath);
|
|
440
|
-
const original = await
|
|
441
|
-
const updated = await
|
|
440
|
+
const original = await fs7.readFile(absPath, "utf8");
|
|
441
|
+
const updated = await fs7.stat(absPath);
|
|
442
442
|
const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
|
|
443
443
|
const lastReadMtime = ctx.lastReadMtime(absPath);
|
|
444
444
|
if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
|
|
@@ -484,7 +484,7 @@ var editTool = {
|
|
|
484
484
|
const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
|
|
485
485
|
const newFile = toStyle(newFileLf, style);
|
|
486
486
|
await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
|
|
487
|
-
const written = await
|
|
487
|
+
const written = await fs7.stat(absPath);
|
|
488
488
|
ctx.recordRead(absPath, written.mtimeMs);
|
|
489
489
|
ctx.session.recordFileChange({
|
|
490
490
|
path: absPath,
|
|
@@ -618,11 +618,11 @@ var replaceTool = {
|
|
|
618
618
|
const dryRun = input.dry_run ?? false;
|
|
619
619
|
const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
|
|
620
620
|
const fileList = await resolveFiles(filesInput, ctx, globRe);
|
|
621
|
-
const realRoot = await
|
|
621
|
+
const realRoot = await fs7.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
|
|
622
622
|
const results = [];
|
|
623
623
|
let totalReplacements = 0;
|
|
624
624
|
for (const absPath of fileList) {
|
|
625
|
-
const lstat2 = await
|
|
625
|
+
const lstat2 = await fs7.lstat(absPath).catch((err) => {
|
|
626
626
|
if (err.code === "ENOENT") return null;
|
|
627
627
|
throw err;
|
|
628
628
|
});
|
|
@@ -630,17 +630,17 @@ var replaceTool = {
|
|
|
630
630
|
if (lstat2.isSymbolicLink()) continue;
|
|
631
631
|
let realPath;
|
|
632
632
|
try {
|
|
633
|
-
realPath = await
|
|
633
|
+
realPath = await fs7.realpath(absPath);
|
|
634
634
|
} catch {
|
|
635
635
|
continue;
|
|
636
636
|
}
|
|
637
637
|
const rel = path.relative(realRoot, realPath);
|
|
638
638
|
if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
|
|
639
|
-
const stat11 = await
|
|
639
|
+
const stat11 = await fs7.stat(realPath).catch(() => null);
|
|
640
640
|
if (!stat11 || !stat11.isFile()) continue;
|
|
641
641
|
let content;
|
|
642
642
|
try {
|
|
643
|
-
const buf = await
|
|
643
|
+
const buf = await fs7.readFile(realPath);
|
|
644
644
|
if (isBinaryBuffer(buf)) continue;
|
|
645
645
|
content = buf.toString("utf8");
|
|
646
646
|
} catch {
|
|
@@ -692,7 +692,7 @@ async function resolveFiles(filesInput, ctx, extraGlob) {
|
|
|
692
692
|
const resolved = [];
|
|
693
693
|
for (const p of parts) {
|
|
694
694
|
const absPath = safeResolve(p, ctx);
|
|
695
|
-
const stat11 = await
|
|
695
|
+
const stat11 = await fs7.stat(absPath).catch(() => null);
|
|
696
696
|
if (stat11?.isFile()) {
|
|
697
697
|
resolved.push(absPath);
|
|
698
698
|
}
|
|
@@ -748,7 +748,7 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
748
748
|
const walk = async (dir) => {
|
|
749
749
|
let entries;
|
|
750
750
|
try {
|
|
751
|
-
entries = await
|
|
751
|
+
entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
752
752
|
} catch {
|
|
753
753
|
return;
|
|
754
754
|
}
|
|
@@ -756,7 +756,7 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
756
756
|
if (DEFAULT_IGNORE.includes(e.name)) continue;
|
|
757
757
|
const full = path.join(dir, e.name);
|
|
758
758
|
try {
|
|
759
|
-
const stat11 = await
|
|
759
|
+
const stat11 = await fs7.lstat(full);
|
|
760
760
|
if (stat11.isSymbolicLink()) continue;
|
|
761
761
|
} catch {
|
|
762
762
|
continue;
|
|
@@ -822,7 +822,7 @@ var globTool = {
|
|
|
822
822
|
}
|
|
823
823
|
let entries;
|
|
824
824
|
try {
|
|
825
|
-
entries = await
|
|
825
|
+
entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
826
826
|
} catch {
|
|
827
827
|
return;
|
|
828
828
|
}
|
|
@@ -838,7 +838,7 @@ var globTool = {
|
|
|
838
838
|
} else if (e.isFile()) {
|
|
839
839
|
if (re.test(rel) || re.test(name)) {
|
|
840
840
|
try {
|
|
841
|
-
const st = await
|
|
841
|
+
const st = await fs7.stat(full);
|
|
842
842
|
results.push({ rel: full, mtime: st.mtimeMs });
|
|
843
843
|
if (results.length >= limit) {
|
|
844
844
|
truncated = true;
|
|
@@ -857,13 +857,14 @@ var globTool = {
|
|
|
857
857
|
};
|
|
858
858
|
async function readGitignore(dir) {
|
|
859
859
|
try {
|
|
860
|
-
const raw = await
|
|
860
|
+
const raw = await fs7.readFile(path.join(dir, ".gitignore"), "utf8");
|
|
861
861
|
return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
862
862
|
} catch {
|
|
863
863
|
return [];
|
|
864
864
|
}
|
|
865
865
|
}
|
|
866
866
|
var DEFAULT_IGNORE3 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
|
|
867
|
+
var NATIVE_SCAN_CONCURRENCY = 32;
|
|
867
868
|
var grepTool = {
|
|
868
869
|
name: "grep",
|
|
869
870
|
category: "Search",
|
|
@@ -1091,14 +1092,52 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
1091
1092
|
const fileMatches = /* @__PURE__ */ new Map();
|
|
1092
1093
|
let total = 0;
|
|
1093
1094
|
let stopped = false;
|
|
1095
|
+
const scanFile = async (full, name) => {
|
|
1096
|
+
if (stopped || signal.aborted) return;
|
|
1097
|
+
if (globRe && !globRe.test(name) && !globRe.test(full)) return;
|
|
1098
|
+
if (globRe) globRe.lastIndex = 0;
|
|
1099
|
+
try {
|
|
1100
|
+
const stat11 = await fs7.stat(full);
|
|
1101
|
+
if (stat11.size > 1e6 || stopped || signal.aborted) return;
|
|
1102
|
+
const head = await fs7.readFile(full);
|
|
1103
|
+
if (isBinaryBuffer(head) || stopped || signal.aborted) return;
|
|
1104
|
+
const text = head.toString("utf8");
|
|
1105
|
+
const lines = text.split(/\r?\n/);
|
|
1106
|
+
let fileHits = 0;
|
|
1107
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1108
|
+
if (stopped || signal.aborted) break;
|
|
1109
|
+
const ln = capSubject(lines[i] ?? "");
|
|
1110
|
+
re.lastIndex = 0;
|
|
1111
|
+
if (re.test(ln)) {
|
|
1112
|
+
fileHits++;
|
|
1113
|
+
total++;
|
|
1114
|
+
if (mode === "content" && matches.length < limit) {
|
|
1115
|
+
matches.push(`${full}:${i + 1}:${ln}`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
if (fileHits > 0) {
|
|
1120
|
+
fileMatches.set(full, fileHits);
|
|
1121
|
+
if (mode === "files_with_matches" && matches.length < limit) {
|
|
1122
|
+
matches.push(full);
|
|
1123
|
+
}
|
|
1124
|
+
if (mode === "count" && matches.length < limit) {
|
|
1125
|
+
matches.push(`${full}:${fileHits}`);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
if (matches.length >= limit) stopped = true;
|
|
1129
|
+
} catch {
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1094
1132
|
const walk = async (dir) => {
|
|
1095
1133
|
if (stopped || signal.aborted) return;
|
|
1096
1134
|
let entries;
|
|
1097
1135
|
try {
|
|
1098
|
-
entries = await
|
|
1136
|
+
entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
1099
1137
|
} catch {
|
|
1100
1138
|
return;
|
|
1101
1139
|
}
|
|
1140
|
+
const files = [];
|
|
1102
1141
|
for (const e of entries) {
|
|
1103
1142
|
if (stopped) return;
|
|
1104
1143
|
if (DEFAULT_IGNORE3.includes(e.name)) continue;
|
|
@@ -1107,41 +1146,10 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
1107
1146
|
if (e.isDirectory()) {
|
|
1108
1147
|
await walk(full);
|
|
1109
1148
|
} else if (e.isFile()) {
|
|
1110
|
-
|
|
1111
|
-
if (globRe) globRe.lastIndex = 0;
|
|
1112
|
-
try {
|
|
1113
|
-
const stat11 = await fs4.stat(full);
|
|
1114
|
-
if (stat11.size > 1e6) continue;
|
|
1115
|
-
const head = await fs4.readFile(full);
|
|
1116
|
-
if (isBinaryBuffer(head)) continue;
|
|
1117
|
-
const text = head.toString("utf8");
|
|
1118
|
-
const lines = text.split(/\r?\n/);
|
|
1119
|
-
let fileHits = 0;
|
|
1120
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1121
|
-
const ln = capSubject(lines[i] ?? "");
|
|
1122
|
-
re.lastIndex = 0;
|
|
1123
|
-
if (re.test(ln)) {
|
|
1124
|
-
fileHits++;
|
|
1125
|
-
total++;
|
|
1126
|
-
if (mode === "content" && matches.length < limit) {
|
|
1127
|
-
matches.push(`${full}:${i + 1}:${ln}`);
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
if (fileHits > 0) {
|
|
1132
|
-
fileMatches.set(full, fileHits);
|
|
1133
|
-
if (mode === "files_with_matches" && matches.length < limit) {
|
|
1134
|
-
matches.push(full);
|
|
1135
|
-
}
|
|
1136
|
-
if (mode === "count" && matches.length < limit) {
|
|
1137
|
-
matches.push(`${full}:${fileHits}`);
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
if (matches.length >= limit) stopped = true;
|
|
1141
|
-
} catch {
|
|
1142
|
-
}
|
|
1149
|
+
files.push({ full, name: e.name });
|
|
1143
1150
|
}
|
|
1144
1151
|
}
|
|
1152
|
+
await mapWithConcurrency(files, NATIVE_SCAN_CONCURRENCY, ({ full, name }) => scanFile(full, name));
|
|
1145
1153
|
};
|
|
1146
1154
|
await walk(base);
|
|
1147
1155
|
return {
|
|
@@ -1151,6 +1159,20 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
1151
1159
|
used: "native"
|
|
1152
1160
|
};
|
|
1153
1161
|
}
|
|
1162
|
+
async function mapWithConcurrency(items, concurrency, fn) {
|
|
1163
|
+
if (items.length === 0) return;
|
|
1164
|
+
let next = 0;
|
|
1165
|
+
const workerCount = Math.min(Math.max(1, concurrency), items.length);
|
|
1166
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
1167
|
+
for (; ; ) {
|
|
1168
|
+
const idx = next++;
|
|
1169
|
+
if (idx >= items.length) return;
|
|
1170
|
+
const item = items[idx];
|
|
1171
|
+
if (item !== void 0) await fn(item);
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
await Promise.all(workers);
|
|
1175
|
+
}
|
|
1154
1176
|
var SPOOL_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1155
1177
|
var SPOOL_WRITE_HWM_BYTES = 4 * 1024 * 1024;
|
|
1156
1178
|
var sweepStarted = false;
|
|
@@ -1162,13 +1184,13 @@ function sweepOldSpoolFiles(dir) {
|
|
|
1162
1184
|
sweepStarted = true;
|
|
1163
1185
|
void (async () => {
|
|
1164
1186
|
try {
|
|
1165
|
-
const
|
|
1166
|
-
for (const name of await
|
|
1187
|
+
const now2 = Date.now();
|
|
1188
|
+
for (const name of await fs7.readdir(dir)) {
|
|
1167
1189
|
if (!name.endsWith(".log")) continue;
|
|
1168
1190
|
const p = path.join(dir, name);
|
|
1169
1191
|
try {
|
|
1170
|
-
const st = await
|
|
1171
|
-
if (
|
|
1192
|
+
const st = await fs7.stat(p);
|
|
1193
|
+
if (now2 - st.mtimeMs > SPOOL_RETENTION_MS) await fs7.unlink(p);
|
|
1172
1194
|
} catch {
|
|
1173
1195
|
}
|
|
1174
1196
|
}
|
|
@@ -1323,10 +1345,10 @@ var CircuitBreaker = class {
|
|
|
1323
1345
|
*/
|
|
1324
1346
|
snapshot() {
|
|
1325
1347
|
this._checkStateTransition();
|
|
1326
|
-
const
|
|
1348
|
+
const now2 = Date.now();
|
|
1327
1349
|
let cooldownRemaining = null;
|
|
1328
1350
|
if (this.openedAt !== null && this.state === "open") {
|
|
1329
|
-
const elapsed =
|
|
1351
|
+
const elapsed = now2 - this.openedAt;
|
|
1330
1352
|
cooldownRemaining = Math.max(0, this.cooldownMs - elapsed);
|
|
1331
1353
|
}
|
|
1332
1354
|
return {
|
|
@@ -1366,7 +1388,7 @@ var CircuitBreaker = class {
|
|
|
1366
1388
|
*/
|
|
1367
1389
|
afterCall(durationMs, failed, bypass = false) {
|
|
1368
1390
|
if (bypass || !this.enabled) return;
|
|
1369
|
-
const
|
|
1391
|
+
const now2 = Date.now();
|
|
1370
1392
|
if (this.state === "half-open") {
|
|
1371
1393
|
if (failed) {
|
|
1372
1394
|
this._trip();
|
|
@@ -1375,12 +1397,12 @@ var CircuitBreaker = class {
|
|
|
1375
1397
|
this._reset();
|
|
1376
1398
|
return;
|
|
1377
1399
|
}
|
|
1378
|
-
this._pruneWindow(
|
|
1400
|
+
this._pruneWindow(now2);
|
|
1379
1401
|
const slow = durationMs >= this.slowCallThresholdMs;
|
|
1380
|
-
this.window.push({ at:
|
|
1402
|
+
this.window.push({ at: now2, failed, slow });
|
|
1381
1403
|
if (failed) {
|
|
1382
1404
|
this.consecutiveFailures++;
|
|
1383
|
-
this.lastFailureAt =
|
|
1405
|
+
this.lastFailureAt = now2;
|
|
1384
1406
|
if (this.consecutiveFailures >= this.maxConsecutiveFailures) {
|
|
1385
1407
|
this._trip();
|
|
1386
1408
|
}
|
|
@@ -1388,7 +1410,7 @@ var CircuitBreaker = class {
|
|
|
1388
1410
|
}
|
|
1389
1411
|
this.consecutiveFailures = 0;
|
|
1390
1412
|
if (slow) {
|
|
1391
|
-
this.lastSlowAt =
|
|
1413
|
+
this.lastSlowAt = now2;
|
|
1392
1414
|
const slowCount = this.window.filter((c) => c.slow).length;
|
|
1393
1415
|
if (slowCount >= this.maxSlowCalls) {
|
|
1394
1416
|
this._trip();
|
|
@@ -1438,8 +1460,8 @@ var CircuitBreaker = class {
|
|
|
1438
1460
|
this.openedAt = null;
|
|
1439
1461
|
}
|
|
1440
1462
|
}
|
|
1441
|
-
_pruneWindow(
|
|
1442
|
-
const cutoff =
|
|
1463
|
+
_pruneWindow(now2) {
|
|
1464
|
+
const cutoff = now2 - this.windowMs;
|
|
1443
1465
|
this.window = this.window.filter((c) => c.at >= cutoff);
|
|
1444
1466
|
}
|
|
1445
1467
|
};
|
|
@@ -1477,10 +1499,13 @@ function redactCommand(cmd) {
|
|
|
1477
1499
|
var DEFAULT_GRACE_MS = 2e3;
|
|
1478
1500
|
function killWin32Tree(pid) {
|
|
1479
1501
|
try {
|
|
1480
|
-
spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
1502
|
+
const child = spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
1481
1503
|
stdio: "ignore",
|
|
1482
1504
|
windowsHide: true
|
|
1483
|
-
})
|
|
1505
|
+
});
|
|
1506
|
+
child.on("error", () => {
|
|
1507
|
+
});
|
|
1508
|
+
child.unref();
|
|
1484
1509
|
return true;
|
|
1485
1510
|
} catch {
|
|
1486
1511
|
return false;
|
|
@@ -1682,7 +1707,7 @@ var ProcessRegistryImpl = class {
|
|
|
1682
1707
|
if (p.killed) return true;
|
|
1683
1708
|
if (p.protected) return false;
|
|
1684
1709
|
const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
|
|
1685
|
-
const isWin3 =
|
|
1710
|
+
const isWin3 = os2.platform() === "win32";
|
|
1686
1711
|
if (isWin3) {
|
|
1687
1712
|
const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
|
|
1688
1713
|
if (liveRealChild && killWin32Tree(pid)) {
|
|
@@ -1772,6 +1797,566 @@ function getProcessRegistry() {
|
|
|
1772
1797
|
function _resetProcessRegistry() {
|
|
1773
1798
|
_registry = void 0;
|
|
1774
1799
|
}
|
|
1800
|
+
var REGISTRY_FILE = ".wrongstack/process-registry.json";
|
|
1801
|
+
var HEARTBEAT_INTERVAL_MS = 5e3;
|
|
1802
|
+
var STALE_THRESHOLD_MS = 3e4;
|
|
1803
|
+
var LOCKFILE = ".wrongstack/.process-registry.lock";
|
|
1804
|
+
function generateInstanceId() {
|
|
1805
|
+
const hostname4 = os2.hostname();
|
|
1806
|
+
const pid = process.pid;
|
|
1807
|
+
const random = Math.random().toString(36).slice(2, 8);
|
|
1808
|
+
return `${hostname4}:${pid}:${random}`;
|
|
1809
|
+
}
|
|
1810
|
+
async function acquireLock(lockfilePath, timeoutMs = 5e3) {
|
|
1811
|
+
const start = Date.now();
|
|
1812
|
+
const pidStr = String(process.pid);
|
|
1813
|
+
const hostStr = os2.hostname();
|
|
1814
|
+
while (Date.now() - start < timeoutMs) {
|
|
1815
|
+
try {
|
|
1816
|
+
await fs7.writeFile(lockfilePath, `${pidStr}:${hostStr}:${Date.now()}`, { flag: "wx" });
|
|
1817
|
+
return async () => {
|
|
1818
|
+
try {
|
|
1819
|
+
await fs7.unlink(lockfilePath);
|
|
1820
|
+
} catch {
|
|
1821
|
+
}
|
|
1822
|
+
};
|
|
1823
|
+
} catch (err) {
|
|
1824
|
+
if (err.code === "EEXIST") {
|
|
1825
|
+
try {
|
|
1826
|
+
const content = await fs7.readFile(lockfilePath, "utf-8");
|
|
1827
|
+
const parts = content.split(":");
|
|
1828
|
+
const lockPidStr = parts[0] ?? "0";
|
|
1829
|
+
const lockPid = parseInt(lockPidStr, 10);
|
|
1830
|
+
if (process.platform !== "win32") {
|
|
1831
|
+
try {
|
|
1832
|
+
process.kill(lockPid, 0);
|
|
1833
|
+
} catch {
|
|
1834
|
+
await fs7.unlink(lockfilePath);
|
|
1835
|
+
continue;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
} catch {
|
|
1839
|
+
try {
|
|
1840
|
+
await fs7.unlink(lockfilePath);
|
|
1841
|
+
} catch {
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1847
|
+
throw err;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
throw new Error(`Failed to acquire lock after ${timeoutMs}ms`);
|
|
1851
|
+
}
|
|
1852
|
+
async function readRegistryFile(filePath) {
|
|
1853
|
+
try {
|
|
1854
|
+
const content = await fs7.readFile(filePath, "utf-8");
|
|
1855
|
+
const parsed = JSON.parse(content);
|
|
1856
|
+
if (parsed.instances && Array.isArray(parsed.instances)) {
|
|
1857
|
+
parsed.instances = new Map(parsed.instances);
|
|
1858
|
+
}
|
|
1859
|
+
return parsed;
|
|
1860
|
+
} catch (err) {
|
|
1861
|
+
if (err.code === "ENOENT") {
|
|
1862
|
+
return {
|
|
1863
|
+
version: 1,
|
|
1864
|
+
instances: /* @__PURE__ */ new Map(),
|
|
1865
|
+
protectedPatterns: ["wrongstack", "node"],
|
|
1866
|
+
lastCleanup: Date.now()
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
throw err;
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
async function writeRegistryFile(filePath, data) {
|
|
1873
|
+
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
1874
|
+
const content = JSON.stringify(data, (_k, v) => {
|
|
1875
|
+
if (v instanceof Map) {
|
|
1876
|
+
return Array.from(v.entries());
|
|
1877
|
+
}
|
|
1878
|
+
return v;
|
|
1879
|
+
}, 2);
|
|
1880
|
+
await fs7.writeFile(tmpPath, content, "utf-8");
|
|
1881
|
+
await fs7.rename(tmpPath, filePath);
|
|
1882
|
+
}
|
|
1883
|
+
var PersistentProcessRegistry = class {
|
|
1884
|
+
instanceId;
|
|
1885
|
+
registryPath;
|
|
1886
|
+
lockPath;
|
|
1887
|
+
baseRegistry;
|
|
1888
|
+
heartbeatInterval = null;
|
|
1889
|
+
isShuttingDown = false;
|
|
1890
|
+
constructor(baseRegistry) {
|
|
1891
|
+
this.instanceId = generateInstanceId();
|
|
1892
|
+
const homeDir = os2.homedir();
|
|
1893
|
+
this.registryPath = path.join(homeDir, REGISTRY_FILE);
|
|
1894
|
+
this.lockPath = path.join(homeDir, LOCKFILE);
|
|
1895
|
+
this.baseRegistry = baseRegistry ?? getProcessRegistry();
|
|
1896
|
+
this.ensureDirectory().catch((err) => {
|
|
1897
|
+
console.error("PersistentProcessRegistry: failed to create .wrongstack dir", err);
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
async ensureDirectory() {
|
|
1901
|
+
const dir = path.dirname(this.registryPath);
|
|
1902
|
+
try {
|
|
1903
|
+
await fs7.mkdir(dir, { recursive: true });
|
|
1904
|
+
} catch (err) {
|
|
1905
|
+
if (err.code !== "EEXIST") throw err;
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* Start the heartbeat and periodic cleanup tasks.
|
|
1910
|
+
*/
|
|
1911
|
+
start() {
|
|
1912
|
+
if (this.heartbeatInterval) return;
|
|
1913
|
+
this.syncToPersistent();
|
|
1914
|
+
this.heartbeatInterval = setInterval(() => {
|
|
1915
|
+
this.heartbeat();
|
|
1916
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
1917
|
+
this.heartbeatInterval.unref?.();
|
|
1918
|
+
setInterval(() => {
|
|
1919
|
+
this.cleanupStaleEntries();
|
|
1920
|
+
}, STALE_THRESHOLD_MS).unref?.();
|
|
1921
|
+
this.registerMainProcess();
|
|
1922
|
+
process.on("exit", () => this.syncToPersistent());
|
|
1923
|
+
}
|
|
1924
|
+
/**
|
|
1925
|
+
* Stop the heartbeat and clean up.
|
|
1926
|
+
*/
|
|
1927
|
+
stop() {
|
|
1928
|
+
this.isShuttingDown = true;
|
|
1929
|
+
if (this.heartbeatInterval) {
|
|
1930
|
+
clearInterval(this.heartbeatInterval);
|
|
1931
|
+
this.heartbeatInterval = null;
|
|
1932
|
+
}
|
|
1933
|
+
this.syncToPersistent();
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* Register the main WrongStack process as protected.
|
|
1937
|
+
*/
|
|
1938
|
+
registerMainProcess() {
|
|
1939
|
+
const mainPid = process.pid;
|
|
1940
|
+
this.updatePersistentEntry({
|
|
1941
|
+
pid: mainPid,
|
|
1942
|
+
name: "wrongstack-main",
|
|
1943
|
+
command: process.argv.slice(0, 3).join(" "),
|
|
1944
|
+
startedAt: Date.now(),
|
|
1945
|
+
lastHeartbeat: Date.now(),
|
|
1946
|
+
instanceId: this.instanceId,
|
|
1947
|
+
hostname: os2.hostname(),
|
|
1948
|
+
protected: true,
|
|
1949
|
+
spawnMode: "main",
|
|
1950
|
+
parentPid: process.ppid,
|
|
1951
|
+
platform: process.platform
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
/**
|
|
1955
|
+
* Register a spawned child process with the persistent registry.
|
|
1956
|
+
*/
|
|
1957
|
+
registerChildProcess(pid, name, command, sessionId, spawnMode = "spawn") {
|
|
1958
|
+
const entry = {
|
|
1959
|
+
pid,
|
|
1960
|
+
name,
|
|
1961
|
+
command,
|
|
1962
|
+
startedAt: Date.now(),
|
|
1963
|
+
lastHeartbeat: Date.now(),
|
|
1964
|
+
instanceId: this.instanceId,
|
|
1965
|
+
hostname: os2.hostname(),
|
|
1966
|
+
protected: true,
|
|
1967
|
+
// All WrongStack child processes are protected by default
|
|
1968
|
+
spawnMode,
|
|
1969
|
+
parentPid: process.pid,
|
|
1970
|
+
platform: process.platform
|
|
1971
|
+
};
|
|
1972
|
+
if (sessionId) {
|
|
1973
|
+
entry.sessionId = sessionId;
|
|
1974
|
+
}
|
|
1975
|
+
this.updatePersistentEntry(entry);
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* Update or add an entry in the persistent registry.
|
|
1979
|
+
*/
|
|
1980
|
+
async updatePersistentEntry(entry) {
|
|
1981
|
+
const release = await acquireLock(this.lockPath);
|
|
1982
|
+
try {
|
|
1983
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1984
|
+
data.instances.set(String(entry.pid), entry);
|
|
1985
|
+
this.baseRegistry.register({
|
|
1986
|
+
pid: entry.pid,
|
|
1987
|
+
name: entry.name,
|
|
1988
|
+
command: entry.command,
|
|
1989
|
+
startedAt: entry.startedAt,
|
|
1990
|
+
sessionId: entry.sessionId,
|
|
1991
|
+
protected: entry.protected,
|
|
1992
|
+
child: null
|
|
1993
|
+
// Main process has no child handle
|
|
1994
|
+
});
|
|
1995
|
+
await writeRegistryFile(this.registryPath, data);
|
|
1996
|
+
} finally {
|
|
1997
|
+
await release();
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
/**
|
|
2001
|
+
* Unregister a process from the persistent registry.
|
|
2002
|
+
*/
|
|
2003
|
+
async unregister(pid) {
|
|
2004
|
+
const release = await acquireLock(this.lockPath);
|
|
2005
|
+
try {
|
|
2006
|
+
const data = await readRegistryFile(this.registryPath);
|
|
2007
|
+
data.instances.delete(String(pid));
|
|
2008
|
+
await writeRegistryFile(this.registryPath, data);
|
|
2009
|
+
} finally {
|
|
2010
|
+
await release();
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
/**
|
|
2014
|
+
* Send heartbeat to mark all this instance's processes as alive.
|
|
2015
|
+
*/
|
|
2016
|
+
heartbeat() {
|
|
2017
|
+
if (this.isShuttingDown) return;
|
|
2018
|
+
this.syncToPersistent();
|
|
2019
|
+
}
|
|
2020
|
+
/**
|
|
2021
|
+
* Sync this instance's processes to the persistent registry.
|
|
2022
|
+
*/
|
|
2023
|
+
async syncToPersistent() {
|
|
2024
|
+
const release = await acquireLock(this.lockPath);
|
|
2025
|
+
try {
|
|
2026
|
+
const data = await readRegistryFile(this.registryPath);
|
|
2027
|
+
const now2 = Date.now();
|
|
2028
|
+
const updatedInstances = /* @__PURE__ */ new Map();
|
|
2029
|
+
for (const [_pidStr, entry] of data.instances) {
|
|
2030
|
+
if (entry.instanceId === this.instanceId) {
|
|
2031
|
+
entry.lastHeartbeat = now2;
|
|
2032
|
+
}
|
|
2033
|
+
if (entry.instanceId === this.instanceId || now2 - entry.lastHeartbeat < STALE_THRESHOLD_MS) {
|
|
2034
|
+
updatedInstances.set(_pidStr, entry);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
data.instances = updatedInstances;
|
|
2038
|
+
data.lastCleanup = now2;
|
|
2039
|
+
await writeRegistryFile(this.registryPath, data);
|
|
2040
|
+
} catch (err) {
|
|
2041
|
+
console.error("PersistentProcessRegistry: sync failed", err);
|
|
2042
|
+
} finally {
|
|
2043
|
+
await release();
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Remove entries for processes that are no longer running.
|
|
2048
|
+
*/
|
|
2049
|
+
async cleanupStaleEntries() {
|
|
2050
|
+
const release = await acquireLock(this.lockPath);
|
|
2051
|
+
try {
|
|
2052
|
+
const data = await readRegistryFile(this.registryPath);
|
|
2053
|
+
const now2 = Date.now();
|
|
2054
|
+
const stalePids = [];
|
|
2055
|
+
for (const [_pidStr, entry] of data.instances) {
|
|
2056
|
+
const age = now2 - entry.lastHeartbeat;
|
|
2057
|
+
if (age > STALE_THRESHOLD_MS) {
|
|
2058
|
+
try {
|
|
2059
|
+
if (process.platform !== "win32") {
|
|
2060
|
+
process.kill(entry.pid, 0);
|
|
2061
|
+
} else {
|
|
2062
|
+
console.log(`PersistentProcessRegistry: checking stale pid ${entry.pid} (${age}ms old)`);
|
|
2063
|
+
}
|
|
2064
|
+
} catch {
|
|
2065
|
+
stalePids.push(_pidStr);
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
if (stalePids.length > 0) {
|
|
2070
|
+
for (const pidStr of stalePids) {
|
|
2071
|
+
data.instances.delete(pidStr);
|
|
2072
|
+
}
|
|
2073
|
+
await writeRegistryFile(this.registryPath, data);
|
|
2074
|
+
}
|
|
2075
|
+
} catch (err) {
|
|
2076
|
+
console.error("PersistentProcessRegistry: cleanup failed", err);
|
|
2077
|
+
} finally {
|
|
2078
|
+
await release();
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
/**
|
|
2082
|
+
* Check if a PID belongs to a WrongStack process and should be protected.
|
|
2083
|
+
*/
|
|
2084
|
+
async isProtectedPid(pid) {
|
|
2085
|
+
const release = await acquireLock(this.lockPath);
|
|
2086
|
+
try {
|
|
2087
|
+
const data = await readRegistryFile(this.registryPath);
|
|
2088
|
+
const entry = data.instances.get(String(pid));
|
|
2089
|
+
if (!entry) return false;
|
|
2090
|
+
if (Date.now() - entry.lastHeartbeat > STALE_THRESHOLD_MS) {
|
|
2091
|
+
return false;
|
|
2092
|
+
}
|
|
2093
|
+
return entry.protected;
|
|
2094
|
+
} finally {
|
|
2095
|
+
await release();
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
/**
|
|
2099
|
+
* Get all protected PIDs from all WrongStack instances.
|
|
2100
|
+
*/
|
|
2101
|
+
async getAllProtectedPids() {
|
|
2102
|
+
const release = await acquireLock(this.lockPath);
|
|
2103
|
+
try {
|
|
2104
|
+
const data = await readRegistryFile(this.registryPath);
|
|
2105
|
+
const now2 = Date.now();
|
|
2106
|
+
const protectedPids = [];
|
|
2107
|
+
for (const [_pidStr, entry] of data.instances) {
|
|
2108
|
+
if (entry.protected && now2 - entry.lastHeartbeat < STALE_THRESHOLD_MS) {
|
|
2109
|
+
protectedPids.push(entry.pid);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
return protectedPids;
|
|
2113
|
+
} finally {
|
|
2114
|
+
await release();
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
/**
|
|
2118
|
+
* Get complete status of all tracked processes across all instances.
|
|
2119
|
+
*/
|
|
2120
|
+
async getGlobalStatus() {
|
|
2121
|
+
const release = await acquireLock(this.lockPath);
|
|
2122
|
+
try {
|
|
2123
|
+
const data = await readRegistryFile(this.registryPath);
|
|
2124
|
+
const now2 = Date.now();
|
|
2125
|
+
const instances = /* @__PURE__ */ new Map();
|
|
2126
|
+
let protectedCount = 0;
|
|
2127
|
+
let staleCount = 0;
|
|
2128
|
+
for (const [_pidStr, entry] of data.instances) {
|
|
2129
|
+
const instanceEntries = instances.get(entry.instanceId) ?? [];
|
|
2130
|
+
instanceEntries.push(entry);
|
|
2131
|
+
instances.set(entry.instanceId, instanceEntries);
|
|
2132
|
+
if (entry.protected) protectedCount++;
|
|
2133
|
+
if (now2 - entry.lastHeartbeat > STALE_THRESHOLD_MS) staleCount++;
|
|
2134
|
+
}
|
|
2135
|
+
return {
|
|
2136
|
+
instances,
|
|
2137
|
+
totalProcesses: data.instances.size,
|
|
2138
|
+
protectedCount,
|
|
2139
|
+
staleCount
|
|
2140
|
+
};
|
|
2141
|
+
} finally {
|
|
2142
|
+
await release();
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
/**
|
|
2146
|
+
* Get the instance ID for this process.
|
|
2147
|
+
*/
|
|
2148
|
+
getInstanceId() {
|
|
2149
|
+
return this.instanceId;
|
|
2150
|
+
}
|
|
2151
|
+
/**
|
|
2152
|
+
* Check if a kill command should be blocked.
|
|
2153
|
+
* Returns true if the kill should be blocked (target is a WrongStack process).
|
|
2154
|
+
*/
|
|
2155
|
+
async shouldBlockKill(pid) {
|
|
2156
|
+
const protectedPids = await this.getAllProtectedPids();
|
|
2157
|
+
return protectedPids.includes(pid);
|
|
2158
|
+
}
|
|
2159
|
+
/**
|
|
2160
|
+
* Add a pattern-based protection rule.
|
|
2161
|
+
* Processes whose command matches any protected pattern are protected.
|
|
2162
|
+
*/
|
|
2163
|
+
async addProtectedPattern(pattern) {
|
|
2164
|
+
const release = await acquireLock(this.lockPath);
|
|
2165
|
+
try {
|
|
2166
|
+
const data = await readRegistryFile(this.registryPath);
|
|
2167
|
+
if (!data.protectedPatterns.includes(pattern)) {
|
|
2168
|
+
data.protectedPatterns.push(pattern);
|
|
2169
|
+
await writeRegistryFile(this.registryPath, data);
|
|
2170
|
+
}
|
|
2171
|
+
} finally {
|
|
2172
|
+
await release();
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
};
|
|
2176
|
+
var _persistentRegistry;
|
|
2177
|
+
function getPersistentProcessRegistry() {
|
|
2178
|
+
if (!_persistentRegistry) {
|
|
2179
|
+
_persistentRegistry = new PersistentProcessRegistry();
|
|
2180
|
+
}
|
|
2181
|
+
return _persistentRegistry;
|
|
2182
|
+
}
|
|
2183
|
+
function resetPersistentProcessRegistry() {
|
|
2184
|
+
if (_persistentRegistry) {
|
|
2185
|
+
_persistentRegistry.stop();
|
|
2186
|
+
_persistentRegistry = void 0;
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
// src/bash-kill-guard.ts
|
|
2191
|
+
function extractKillCommand(command) {
|
|
2192
|
+
const normalized = command.replace(/\s+/g, " ").trim();
|
|
2193
|
+
const shellCMatch = normalized.match(
|
|
2194
|
+
/^(?:\/\w+)?\/?(?:bin|usr)\/(?:ba)?sh\s+-[c]\s+['"](.+?)['"]$/
|
|
2195
|
+
);
|
|
2196
|
+
if (shellCMatch?.[1]) {
|
|
2197
|
+
const inner = shellCMatch[1].trim();
|
|
2198
|
+
return isKillRelatedCommand(inner) ? inner : null;
|
|
2199
|
+
}
|
|
2200
|
+
const shellCUnquoted = normalized.match(
|
|
2201
|
+
/^(?:\/\w+)?\/?(?:bin|usr)\/(?:ba)?sh\s+-[c]\s+(kill(?:\s+-[a-zA-Z]+)?(?:\s+\d+)+)$/
|
|
2202
|
+
);
|
|
2203
|
+
if (shellCUnquoted?.[1]) {
|
|
2204
|
+
return shellCUnquoted[1];
|
|
2205
|
+
}
|
|
2206
|
+
return null;
|
|
2207
|
+
}
|
|
2208
|
+
function isKillRelatedCommand(cmd) {
|
|
2209
|
+
const normalized = cmd.toLowerCase().replace(/\s+/g, " ").trim();
|
|
2210
|
+
if (/^kill(\s|$)/.test(normalized)) return true;
|
|
2211
|
+
if (/^(pkill|killall|pgrep|skill)\s/.test(normalized)) return true;
|
|
2212
|
+
if (/^taskkill\s/i.test(normalized)) return true;
|
|
2213
|
+
if (/^tskill\s/i.test(normalized)) return true;
|
|
2214
|
+
if (/^\/proc\/\d+\/(?:kill|fd)/.test(normalized)) return true;
|
|
2215
|
+
return false;
|
|
2216
|
+
}
|
|
2217
|
+
function parseKillCommand(command) {
|
|
2218
|
+
const normalized = command.replace(/\s+/g, " ").trim();
|
|
2219
|
+
const simpleMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z]+)\s+)?(\d+|-?\d+)$/);
|
|
2220
|
+
if (simpleMatch) {
|
|
2221
|
+
const signal = simpleMatch[1] ?? "-TERM";
|
|
2222
|
+
const pidOrGroup = simpleMatch[2];
|
|
2223
|
+
if (!pidOrGroup) return null;
|
|
2224
|
+
const isGroupKill = pidOrGroup.startsWith("-");
|
|
2225
|
+
const pid = isGroupKill ? parseInt(pidOrGroup.slice(1), 10) : parseInt(pidOrGroup, 10);
|
|
2226
|
+
return {
|
|
2227
|
+
pid,
|
|
2228
|
+
signal: signal.slice(1),
|
|
2229
|
+
isGroupKill,
|
|
2230
|
+
isAllKill: false,
|
|
2231
|
+
originalCommand: command
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
const pkillMatch = normalized.match(/^pkill\s+(?:(-[a-zA-Z]+)\s+)?(.+)$/);
|
|
2235
|
+
if (pkillMatch?.[2]) {
|
|
2236
|
+
const name = pkillMatch[2];
|
|
2237
|
+
const signalMatch = pkillMatch[1];
|
|
2238
|
+
return {
|
|
2239
|
+
name,
|
|
2240
|
+
signal: signalMatch ? signalMatch.slice(1) : "TERM",
|
|
2241
|
+
isGroupKill: false,
|
|
2242
|
+
isAllKill: false,
|
|
2243
|
+
originalCommand: command
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
const killallMatch = normalized.match(/^killall\s+(?:(-[a-zA-Z]+)\s+)?(.+)$/);
|
|
2247
|
+
if (killallMatch?.[2]) {
|
|
2248
|
+
const name = killallMatch[2];
|
|
2249
|
+
const signalMatch = killallMatch[1];
|
|
2250
|
+
return {
|
|
2251
|
+
name,
|
|
2252
|
+
signal: signalMatch ? signalMatch.slice(1) : "TERM",
|
|
2253
|
+
isGroupKill: false,
|
|
2254
|
+
isAllKill: false,
|
|
2255
|
+
originalCommand: command
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
const pgrepMatch = normalized.match(/^pgrep\s+(.+)$/);
|
|
2259
|
+
if (pgrepMatch) {
|
|
2260
|
+
return null;
|
|
2261
|
+
}
|
|
2262
|
+
const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
|
|
2263
|
+
if (taskkillMatch?.[1]) {
|
|
2264
|
+
const pidStr = taskkillMatch[1];
|
|
2265
|
+
return {
|
|
2266
|
+
pid: parseInt(pidStr, 10),
|
|
2267
|
+
signal: normalized.includes("/F") ? "FORCE" : "TERM",
|
|
2268
|
+
isGroupKill: false,
|
|
2269
|
+
isAllKill: false,
|
|
2270
|
+
originalCommand: command
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
|
|
2274
|
+
if (tskillMatch?.[1]) {
|
|
2275
|
+
const pidStr = tskillMatch[1];
|
|
2276
|
+
return {
|
|
2277
|
+
pid: parseInt(pidStr, 10),
|
|
2278
|
+
signal: "TERM",
|
|
2279
|
+
isGroupKill: false,
|
|
2280
|
+
isAllKill: false,
|
|
2281
|
+
originalCommand: command
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
return null;
|
|
2285
|
+
}
|
|
2286
|
+
async function getProtectedEntries() {
|
|
2287
|
+
const registry = getPersistentProcessRegistry();
|
|
2288
|
+
const status = await registry.getGlobalStatus();
|
|
2289
|
+
const entries = [];
|
|
2290
|
+
for (const instanceEntries of status.instances.values()) {
|
|
2291
|
+
for (const entry of instanceEntries) {
|
|
2292
|
+
if (entry.protected && Date.now() - entry.lastHeartbeat < 3e4) {
|
|
2293
|
+
entries.push(entry);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
return entries;
|
|
2298
|
+
}
|
|
2299
|
+
async function isKillProtected(kill) {
|
|
2300
|
+
const registry = getPersistentProcessRegistry();
|
|
2301
|
+
if (kill.name) {
|
|
2302
|
+
const entries = await getProtectedEntries();
|
|
2303
|
+
const killNameLower = kill.name.toLowerCase();
|
|
2304
|
+
for (const entry of entries) {
|
|
2305
|
+
if (entry.name && entry.name.toLowerCase().includes(killNameLower)) {
|
|
2306
|
+
return true;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
if (killNameLower.includes("wrongstack")) {
|
|
2310
|
+
return true;
|
|
2311
|
+
}
|
|
2312
|
+
if (killNameLower.includes("node") && entries.length > 0) {
|
|
2313
|
+
return true;
|
|
2314
|
+
}
|
|
2315
|
+
return false;
|
|
2316
|
+
}
|
|
2317
|
+
if (kill.isGroupKill) {
|
|
2318
|
+
const protectedPids = await registry.getAllProtectedPids();
|
|
2319
|
+
return protectedPids.length > 0;
|
|
2320
|
+
}
|
|
2321
|
+
if (kill.pid !== void 0) {
|
|
2322
|
+
return registry.shouldBlockKill(kill.pid);
|
|
2323
|
+
}
|
|
2324
|
+
return false;
|
|
2325
|
+
}
|
|
2326
|
+
async function checkAndBlockKillCommand(command) {
|
|
2327
|
+
const normalized = command.replace(/\s+/g, " ").trim();
|
|
2328
|
+
const killCmd = extractKillCommand(normalized) || (isKillRelatedCommand(normalized) ? normalized : null);
|
|
2329
|
+
if (!killCmd) {
|
|
2330
|
+
return { blocked: false };
|
|
2331
|
+
}
|
|
2332
|
+
const parsed = parseKillCommand(killCmd);
|
|
2333
|
+
if (!parsed) {
|
|
2334
|
+
if (killCmd.includes("kill") && /kill\s+.*\|/.test(killCmd)) {
|
|
2335
|
+
return {
|
|
2336
|
+
blocked: true,
|
|
2337
|
+
reason: `Blocked: complex kill pipeline detected \u2014 "${killCmd.slice(0, 50)}..."`
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
return { blocked: false };
|
|
2341
|
+
}
|
|
2342
|
+
if (await isKillProtected(parsed)) {
|
|
2343
|
+
let target;
|
|
2344
|
+
if (parsed.name) {
|
|
2345
|
+
target = `process name "${parsed.name}"`;
|
|
2346
|
+
} else if (parsed.pid !== void 0) {
|
|
2347
|
+
target = `PID ${parsed.pid}`;
|
|
2348
|
+
} else {
|
|
2349
|
+
target = "(unknown target)";
|
|
2350
|
+
}
|
|
2351
|
+
const signal = parsed.signal ? ` (${parsed.signal})` : "";
|
|
2352
|
+
const groupNote = parsed.isGroupKill ? " (process group)" : "";
|
|
2353
|
+
return {
|
|
2354
|
+
blocked: true,
|
|
2355
|
+
reason: `Blocked: kill${signal} ${target}${groupNote} targets a protected WrongStack process.`
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
return { blocked: false };
|
|
2359
|
+
}
|
|
1775
2360
|
|
|
1776
2361
|
// src/bash.ts
|
|
1777
2362
|
var MAX_OUTPUT = 32768;
|
|
@@ -1841,6 +2426,20 @@ var bashTool = {
|
|
|
1841
2426
|
};
|
|
1842
2427
|
return;
|
|
1843
2428
|
}
|
|
2429
|
+
const killCheck = await checkAndBlockKillCommand(input.command);
|
|
2430
|
+
if (killCheck.blocked) {
|
|
2431
|
+
yield {
|
|
2432
|
+
type: "final",
|
|
2433
|
+
output: {
|
|
2434
|
+
output: "",
|
|
2435
|
+
exit_code: 1,
|
|
2436
|
+
timed_out: false,
|
|
2437
|
+
pid: null,
|
|
2438
|
+
error: killCheck.reason || "Kill command blocked: targets a protected WrongStack process."
|
|
2439
|
+
}
|
|
2440
|
+
};
|
|
2441
|
+
return;
|
|
2442
|
+
}
|
|
1844
2443
|
const PIPE_TO_SHELL_PATTERN = /\|\s*(sh|bash|ksh|zsh|fish|cmd|powershell|pwsh)/i;
|
|
1845
2444
|
if (PIPE_TO_SHELL_PATTERN.test(input.command)) {
|
|
1846
2445
|
console.warn(JSON.stringify({
|
|
@@ -1853,7 +2452,7 @@ var bashTool = {
|
|
|
1853
2452
|
}));
|
|
1854
2453
|
}
|
|
1855
2454
|
const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
|
|
1856
|
-
const isWin3 =
|
|
2455
|
+
const isWin3 = os2.platform() === "win32";
|
|
1857
2456
|
const shell = (() => {
|
|
1858
2457
|
const explicit = process.env[isWin3 ? "WRONGSTACK_COMSPEC" : "WRONGSTACK_SHELL"];
|
|
1859
2458
|
if (explicit) return explicit;
|
|
@@ -2105,8 +2704,8 @@ var bashTool = {
|
|
|
2105
2704
|
};
|
|
2106
2705
|
return;
|
|
2107
2706
|
}
|
|
2108
|
-
const
|
|
2109
|
-
if (pending2.length >= STREAM_FLUSH_BYTES ||
|
|
2707
|
+
const now2 = Date.now();
|
|
2708
|
+
if (pending2.length >= STREAM_FLUSH_BYTES || now2 - lastFlush >= STREAM_FLUSH_INTERVAL_MS) {
|
|
2110
2709
|
const text = flush();
|
|
2111
2710
|
if (text) yield { type: "partial_output", text };
|
|
2112
2711
|
}
|
|
@@ -2138,7 +2737,7 @@ function resolveWin32Command(cmd) {
|
|
|
2138
2737
|
for (const ext of pathext) {
|
|
2139
2738
|
const full = `${base}${ext}`;
|
|
2140
2739
|
try {
|
|
2141
|
-
|
|
2740
|
+
fs8.accessSync(full, fs8.constants.X_OK);
|
|
2142
2741
|
return full;
|
|
2143
2742
|
} catch {
|
|
2144
2743
|
}
|
|
@@ -2146,6 +2745,16 @@ function resolveWin32Command(cmd) {
|
|
|
2146
2745
|
}
|
|
2147
2746
|
return cmd;
|
|
2148
2747
|
}
|
|
2748
|
+
var WIN32_SHELL_META = /[&|<>\r\n\0]/;
|
|
2749
|
+
function assertSafeWin32ShellArgs(args) {
|
|
2750
|
+
for (const a of args) {
|
|
2751
|
+
if (typeof a === "string" && WIN32_SHELL_META.test(a)) {
|
|
2752
|
+
throw new Error(
|
|
2753
|
+
"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)
|
|
2754
|
+
);
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2149
2758
|
|
|
2150
2759
|
// src/exec.ts
|
|
2151
2760
|
var isWin = process.platform === "win32";
|
|
@@ -2360,6 +2969,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
2360
2969
|
const resolved = resolveWin32Command(cmd);
|
|
2361
2970
|
const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
2362
2971
|
const spawnCmd = needsShell ? cmd : resolved;
|
|
2972
|
+
if (needsShell) assertSafeWin32ShellArgs(args);
|
|
2363
2973
|
const child = spawn(spawnCmd, args, {
|
|
2364
2974
|
cwd,
|
|
2365
2975
|
env: buildChildEnv(sessionId),
|
|
@@ -2453,8 +3063,8 @@ if (ALLOW_PRIVATE && !process.env["CI"]) {
|
|
|
2453
3063
|
);
|
|
2454
3064
|
}
|
|
2455
3065
|
var combineSignals = (signals) => AbortSignal.any(signals);
|
|
2456
|
-
function guardedLookup(
|
|
2457
|
-
dns.lookup(
|
|
3066
|
+
function guardedLookup(hostname4, options, callback) {
|
|
3067
|
+
dns.lookup(hostname4, { all: true }).then((records) => {
|
|
2458
3068
|
const family = options?.family;
|
|
2459
3069
|
const byFamily = family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;
|
|
2460
3070
|
const list = byFamily.length > 0 ? byFamily : records;
|
|
@@ -2481,7 +3091,7 @@ function guardedLookup(hostname, options, callback) {
|
|
|
2481
3091
|
const first = list.at(0);
|
|
2482
3092
|
if (!first) {
|
|
2483
3093
|
callback(
|
|
2484
|
-
Object.assign(new Error(`fetch: no address for ${
|
|
3094
|
+
Object.assign(new Error(`fetch: no address for ${hostname4}`), { code: "ENOTFOUND" })
|
|
2485
3095
|
);
|
|
2486
3096
|
return;
|
|
2487
3097
|
}
|
|
@@ -2595,7 +3205,13 @@ var fetchTool = {
|
|
|
2595
3205
|
const timer = setTimeout(() => ctrl.abort(new Error("fetch timeout")), TIMEOUT_MS);
|
|
2596
3206
|
const combined = combineSignals([opts.signal, ctrl.signal]);
|
|
2597
3207
|
try {
|
|
2598
|
-
|
|
3208
|
+
let res;
|
|
3209
|
+
try {
|
|
3210
|
+
res = await guardedFetch(input.url, 5, combined);
|
|
3211
|
+
} catch (err) {
|
|
3212
|
+
if (opts.signal.aborted) throw err;
|
|
3213
|
+
throw describeFetchError(err, input.url, ctrl.signal.aborted);
|
|
3214
|
+
}
|
|
2599
3215
|
const ct = res.headers.get("content-type") ?? "application/octet-stream";
|
|
2600
3216
|
if (/^image\/|^audio\/|^video\/|application\/octet-stream/.test(ct)) {
|
|
2601
3217
|
throw new Error(`fetch: refusing to read binary content-type "${ct}"`);
|
|
@@ -2651,9 +3267,9 @@ var fetchTool = {
|
|
|
2651
3267
|
}
|
|
2652
3268
|
}
|
|
2653
3269
|
};
|
|
2654
|
-
async function assertNotPrivate(
|
|
3270
|
+
async function assertNotPrivate(hostname4) {
|
|
2655
3271
|
if (ALLOW_PRIVATE) return;
|
|
2656
|
-
const host =
|
|
3272
|
+
const host = hostname4.startsWith("[") && hostname4.endsWith("]") ? hostname4.slice(1, -1) : hostname4;
|
|
2657
3273
|
if (host === "localhost" || host.endsWith(".localhost")) {
|
|
2658
3274
|
throw new Error("fetch: blocked localhost target");
|
|
2659
3275
|
}
|
|
@@ -2680,6 +3296,23 @@ async function assertNotPrivate(hostname) {
|
|
|
2680
3296
|
}
|
|
2681
3297
|
}
|
|
2682
3298
|
}
|
|
3299
|
+
function describeFetchError(err, url, timedOut) {
|
|
3300
|
+
if (timedOut) {
|
|
3301
|
+
return new Error(`fetch: GET ${url} timed out after ${TIMEOUT_MS}ms`);
|
|
3302
|
+
}
|
|
3303
|
+
const parts = [];
|
|
3304
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3305
|
+
let cur = err;
|
|
3306
|
+
while (cur instanceof Error && !seen.has(cur)) {
|
|
3307
|
+
seen.add(cur);
|
|
3308
|
+
const code = cur.code;
|
|
3309
|
+
const label = code ? `${code}: ${cur.message}` : cur.message;
|
|
3310
|
+
if (label && label !== "fetch failed" && !parts.includes(label)) parts.push(label);
|
|
3311
|
+
cur = cur.cause;
|
|
3312
|
+
}
|
|
3313
|
+
const detail = parts.length > 0 ? parts.join(" \u2192 ") : "fetch failed";
|
|
3314
|
+
return new Error(`fetch: GET ${url} failed \u2014 ${detail}`);
|
|
3315
|
+
}
|
|
2683
3316
|
function prettyJson(s) {
|
|
2684
3317
|
try {
|
|
2685
3318
|
return JSON.stringify(JSON.parse(s), null, 2);
|
|
@@ -2773,7 +3406,7 @@ async function duckduckgoSearch(query2, num, signal) {
|
|
|
2773
3406
|
truncated: results.length >= num
|
|
2774
3407
|
};
|
|
2775
3408
|
} catch (err) {
|
|
2776
|
-
console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: toErrorMessage(err) }));
|
|
3409
|
+
console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: toErrorMessage$1(err) }));
|
|
2777
3410
|
return {
|
|
2778
3411
|
query: query2,
|
|
2779
3412
|
results: [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }],
|
|
@@ -3232,7 +3865,7 @@ var planTool = {
|
|
|
3232
3865
|
const lastSep = Math.max(taskPath.lastIndexOf("/"), taskPath.lastIndexOf("\\"));
|
|
3233
3866
|
taskPath = lastSep >= 0 ? taskPath.slice(0, lastSep + 1) + "backlog.tasks.json" : "backlog.tasks.json";
|
|
3234
3867
|
}
|
|
3235
|
-
const
|
|
3868
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
3236
3869
|
try {
|
|
3237
3870
|
const taskFile = await mutateTasks(taskPath, sessionId, (f) => {
|
|
3238
3871
|
f.tasks.push({
|
|
@@ -3242,8 +3875,8 @@ var planTool = {
|
|
|
3242
3875
|
type: "feature",
|
|
3243
3876
|
priority: "medium",
|
|
3244
3877
|
status: "pending",
|
|
3245
|
-
createdAt:
|
|
3246
|
-
updatedAt:
|
|
3878
|
+
createdAt: now2,
|
|
3879
|
+
updatedAt: now2
|
|
3247
3880
|
});
|
|
3248
3881
|
return f;
|
|
3249
3882
|
});
|
|
@@ -3589,12 +4222,12 @@ var patchTool = {
|
|
|
3589
4222
|
};
|
|
3590
4223
|
}
|
|
3591
4224
|
}
|
|
3592
|
-
const tmpDir = await
|
|
4225
|
+
const tmpDir = await fs7.mkdtemp(path.join(os2.tmpdir(), ".wstack_patch_"));
|
|
3593
4226
|
try {
|
|
3594
|
-
await
|
|
4227
|
+
await fs7.chmod(tmpDir, 448).catch(() => {
|
|
3595
4228
|
});
|
|
3596
4229
|
const patchFile = path.join(tmpDir, "in.diff");
|
|
3597
|
-
await
|
|
4230
|
+
await fs7.writeFile(patchFile, input.patch, { mode: 384 });
|
|
3598
4231
|
const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
|
|
3599
4232
|
const result = await runPatch(args, dir, opts.signal);
|
|
3600
4233
|
if (result.exitCode !== 0 && !dryRun) {
|
|
@@ -3615,7 +4248,7 @@ var patchTool = {
|
|
|
3615
4248
|
message: result.stdout || "patch applied"
|
|
3616
4249
|
};
|
|
3617
4250
|
} finally {
|
|
3618
|
-
await
|
|
4251
|
+
await fs7.rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
3619
4252
|
});
|
|
3620
4253
|
}
|
|
3621
4254
|
}
|
|
@@ -3697,7 +4330,7 @@ var jsonTool = {
|
|
|
3697
4330
|
let raw;
|
|
3698
4331
|
if (input.file) {
|
|
3699
4332
|
try {
|
|
3700
|
-
raw = await
|
|
4333
|
+
raw = await fs7.readFile(input.file, "utf8");
|
|
3701
4334
|
} catch {
|
|
3702
4335
|
return { data: null, formatted: "", type: "unknown", error: `Could not read file` };
|
|
3703
4336
|
}
|
|
@@ -3736,8 +4369,8 @@ var jsonTool = {
|
|
|
3736
4369
|
};
|
|
3737
4370
|
}
|
|
3738
4371
|
};
|
|
3739
|
-
function query(data,
|
|
3740
|
-
const parts =
|
|
4372
|
+
function query(data, path21) {
|
|
4373
|
+
const parts = path21.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
3741
4374
|
let current = data;
|
|
3742
4375
|
for (const part of parts) {
|
|
3743
4376
|
if (current === null || current === void 0) return void 0;
|
|
@@ -3912,9 +4545,9 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
3912
4545
|
const results = [];
|
|
3913
4546
|
for (const file of files) {
|
|
3914
4547
|
const absPath = safeResolve(file, ctx);
|
|
3915
|
-
const stat11 = await
|
|
4548
|
+
const stat11 = await fs7.stat(absPath).catch(() => null);
|
|
3916
4549
|
if (!stat11?.isFile()) continue;
|
|
3917
|
-
const content = await
|
|
4550
|
+
const content = await fs7.readFile(absPath, "utf8");
|
|
3918
4551
|
const lines = content.split(/\r?\n/);
|
|
3919
4552
|
results.push(formatWithLineNumbers(file, lines));
|
|
3920
4553
|
}
|
|
@@ -4071,7 +4704,7 @@ var treeTool = {
|
|
|
4071
4704
|
}
|
|
4072
4705
|
};
|
|
4073
4706
|
async function walkDir(dir, depth, opts) {
|
|
4074
|
-
const entries = await
|
|
4707
|
+
const entries = await fs7.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
4075
4708
|
const filtered = entries.filter((e) => {
|
|
4076
4709
|
if (!opts.showHidden && e.name.startsWith(".")) return false;
|
|
4077
4710
|
if (opts.exclude.has(e.name)) return false;
|
|
@@ -4122,6 +4755,7 @@ async function* spawnStream(opts) {
|
|
|
4122
4755
|
const resolved = resolveWin32Command(opts.cmd);
|
|
4123
4756
|
const needsShell = isWin2 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
4124
4757
|
const cmd = needsShell ? opts.cmd : resolved;
|
|
4758
|
+
if (needsShell) assertSafeWin32ShellArgs(opts.args);
|
|
4125
4759
|
const child = spawn(cmd, opts.args, {
|
|
4126
4760
|
cwd: opts.cwd,
|
|
4127
4761
|
env: buildChildEnv(),
|
|
@@ -4206,8 +4840,10 @@ async function* spawnStream(opts) {
|
|
|
4206
4840
|
queue.push({ kind: "close", data: "", code: 124 });
|
|
4207
4841
|
wake();
|
|
4208
4842
|
};
|
|
4209
|
-
if (
|
|
4210
|
-
|
|
4843
|
+
if (isWin2) {
|
|
4844
|
+
if (opts.signal.aborted) onAbort();
|
|
4845
|
+
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
4846
|
+
}
|
|
4211
4847
|
let exitCode = 0;
|
|
4212
4848
|
let spawnFailed = false;
|
|
4213
4849
|
try {
|
|
@@ -4251,7 +4887,7 @@ async function* spawnStream(opts) {
|
|
|
4251
4887
|
};
|
|
4252
4888
|
} finally {
|
|
4253
4889
|
spool.finalize();
|
|
4254
|
-
opts.signal.removeEventListener("abort", onAbort);
|
|
4890
|
+
if (isWin2) opts.signal.removeEventListener("abort", onAbort);
|
|
4255
4891
|
child.stdout?.off("data", onOut);
|
|
4256
4892
|
child.stderr?.off("data", onErr);
|
|
4257
4893
|
child.stdout?.destroy();
|
|
@@ -5013,6 +5649,7 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
5013
5649
|
const resolved = resolveWin32Command(manager);
|
|
5014
5650
|
const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
5015
5651
|
const spawnCmd = needsShell ? manager : resolved;
|
|
5652
|
+
if (needsShell) assertSafeWin32ShellArgs(args);
|
|
5016
5653
|
const child = spawn(spawnCmd, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
|
|
5017
5654
|
child.stdout?.on("data", (c) => {
|
|
5018
5655
|
if (stdout.length < MAX) stdout += c.toString();
|
|
@@ -5202,7 +5839,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
5202
5839
|
}
|
|
5203
5840
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
5204
5841
|
var MAX_TAIL_LINES = 1e5;
|
|
5205
|
-
async function fileLogs(
|
|
5842
|
+
async function fileLogs(path21, lines, filterRe, stream) {
|
|
5206
5843
|
const { createInterface } = await import('node:readline');
|
|
5207
5844
|
const { createReadStream } = await import('node:fs');
|
|
5208
5845
|
const entries = [];
|
|
@@ -5211,7 +5848,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
|
|
|
5211
5848
|
let writeIdx = 0;
|
|
5212
5849
|
let totalLines = 0;
|
|
5213
5850
|
const rl = createInterface({
|
|
5214
|
-
input: createReadStream(
|
|
5851
|
+
input: createReadStream(path21),
|
|
5215
5852
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
5216
5853
|
});
|
|
5217
5854
|
for await (const line of rl) {
|
|
@@ -5232,7 +5869,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
|
|
|
5232
5869
|
if (parsed) entries.push(parsed);
|
|
5233
5870
|
}
|
|
5234
5871
|
return {
|
|
5235
|
-
source:
|
|
5872
|
+
source: path21,
|
|
5236
5873
|
entries,
|
|
5237
5874
|
total: entries.length,
|
|
5238
5875
|
truncated: totalLines > effLines,
|
|
@@ -5317,7 +5954,7 @@ var documentTool = {
|
|
|
5317
5954
|
const fileList = input.files ? await resolveFiles2(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
|
|
5318
5955
|
for (const absPath of fileList) {
|
|
5319
5956
|
try {
|
|
5320
|
-
const content = await
|
|
5957
|
+
const content = await fs7.readFile(absPath, "utf8");
|
|
5321
5958
|
filesProcessed++;
|
|
5322
5959
|
const processed = processFile(
|
|
5323
5960
|
content,
|
|
@@ -5353,7 +5990,7 @@ async function resolveFiles2(filesInput, cwd) {
|
|
|
5353
5990
|
for (const f of files) {
|
|
5354
5991
|
const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
|
|
5355
5992
|
try {
|
|
5356
|
-
const stat11 = await
|
|
5993
|
+
const stat11 = await fs7.stat(absPath);
|
|
5357
5994
|
if (stat11.isFile()) resolved.push(absPath);
|
|
5358
5995
|
} catch {
|
|
5359
5996
|
}
|
|
@@ -5577,7 +6214,7 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
5577
6214
|
}
|
|
5578
6215
|
const fullPath = target;
|
|
5579
6216
|
if (!dryRun) {
|
|
5580
|
-
await
|
|
6217
|
+
await fs7.mkdir(path.dirname(fullPath), { recursive: true });
|
|
5581
6218
|
await atomicWrite(fullPath, substituteVars(content, name, vars));
|
|
5582
6219
|
}
|
|
5583
6220
|
files.push(resolvedPath);
|
|
@@ -6245,6 +6882,531 @@ ${mode.description}`
|
|
|
6245
6882
|
}
|
|
6246
6883
|
};
|
|
6247
6884
|
}
|
|
6885
|
+
var ProcessGuardian = class {
|
|
6886
|
+
registry;
|
|
6887
|
+
config;
|
|
6888
|
+
protectedProcesses = /* @__PURE__ */ new Map();
|
|
6889
|
+
heartbeatTimer = null;
|
|
6890
|
+
isRunning = false;
|
|
6891
|
+
instanceId;
|
|
6892
|
+
constructor(config = {}) {
|
|
6893
|
+
this.registry = getPersistentProcessRegistry();
|
|
6894
|
+
this.config = {
|
|
6895
|
+
heartbeatIntervalMs: config.heartbeatIntervalMs ?? 5e3,
|
|
6896
|
+
autoResurrect: config.autoResurrect ?? false,
|
|
6897
|
+
// Disabled by default
|
|
6898
|
+
maxResurrectionAttempts: config.maxResurrectionAttempts ?? 3,
|
|
6899
|
+
protectedPatterns: config.protectedPatterns ?? ["node", "wrongstack"]
|
|
6900
|
+
};
|
|
6901
|
+
this.instanceId = this.registry.getInstanceId();
|
|
6902
|
+
}
|
|
6903
|
+
/**
|
|
6904
|
+
* Start the guardian - begins monitoring and registration.
|
|
6905
|
+
*/
|
|
6906
|
+
start() {
|
|
6907
|
+
if (this.isRunning) return;
|
|
6908
|
+
this.isRunning = true;
|
|
6909
|
+
this.registerProcess(process.pid, "wrongstack-main");
|
|
6910
|
+
this.registerExistingChildren();
|
|
6911
|
+
this.heartbeatTimer = setInterval(() => {
|
|
6912
|
+
this.heartbeat();
|
|
6913
|
+
}, this.config.heartbeatIntervalMs);
|
|
6914
|
+
this.heartbeatTimer.unref?.();
|
|
6915
|
+
this.setupProcessHandlers();
|
|
6916
|
+
console.log(JSON.stringify({
|
|
6917
|
+
level: "info",
|
|
6918
|
+
event: "process_guardian.started",
|
|
6919
|
+
instanceId: this.instanceId,
|
|
6920
|
+
mainPid: process.pid,
|
|
6921
|
+
hostname: os2.hostname(),
|
|
6922
|
+
platform: process.platform
|
|
6923
|
+
}));
|
|
6924
|
+
}
|
|
6925
|
+
/**
|
|
6926
|
+
* Stop the guardian gracefully.
|
|
6927
|
+
*/
|
|
6928
|
+
stop() {
|
|
6929
|
+
if (!this.isRunning) return;
|
|
6930
|
+
this.isRunning = false;
|
|
6931
|
+
if (this.heartbeatTimer) {
|
|
6932
|
+
clearInterval(this.heartbeatTimer);
|
|
6933
|
+
this.heartbeatTimer = null;
|
|
6934
|
+
}
|
|
6935
|
+
this.registry.stop();
|
|
6936
|
+
console.log(JSON.stringify({
|
|
6937
|
+
level: "info",
|
|
6938
|
+
event: "process_guardian.stopped",
|
|
6939
|
+
instanceId: this.instanceId,
|
|
6940
|
+
mainPid: process.pid
|
|
6941
|
+
}));
|
|
6942
|
+
}
|
|
6943
|
+
/**
|
|
6944
|
+
* Register a process with the guardian.
|
|
6945
|
+
*/
|
|
6946
|
+
registerProcess(pid, name) {
|
|
6947
|
+
this.protectedProcesses.set(pid, {
|
|
6948
|
+
pid,
|
|
6949
|
+
name,
|
|
6950
|
+
lastSeen: Date.now(),
|
|
6951
|
+
resurrectionAttempts: 0
|
|
6952
|
+
});
|
|
6953
|
+
this.registry.registerChildProcess(pid, name, name, void 0, "spawn");
|
|
6954
|
+
console.log(JSON.stringify({
|
|
6955
|
+
level: "info",
|
|
6956
|
+
event: "process_guardian.registered",
|
|
6957
|
+
pid,
|
|
6958
|
+
name,
|
|
6959
|
+
instanceId: this.instanceId
|
|
6960
|
+
}));
|
|
6961
|
+
}
|
|
6962
|
+
/**
|
|
6963
|
+
* Unregister a process (e.g., when it exits normally).
|
|
6964
|
+
*/
|
|
6965
|
+
unregisterProcess(pid) {
|
|
6966
|
+
this.protectedProcesses.delete(pid);
|
|
6967
|
+
this.registry.unregister(pid).catch((err) => {
|
|
6968
|
+
console.error(JSON.stringify({
|
|
6969
|
+
level: "error",
|
|
6970
|
+
event: "process_guardian.unregister_failed",
|
|
6971
|
+
pid,
|
|
6972
|
+
error: err.message
|
|
6973
|
+
}));
|
|
6974
|
+
});
|
|
6975
|
+
}
|
|
6976
|
+
/**
|
|
6977
|
+
* Register all child processes that already exist.
|
|
6978
|
+
*/
|
|
6979
|
+
registerExistingChildren() {
|
|
6980
|
+
this.syncWithProcessRegistry();
|
|
6981
|
+
}
|
|
6982
|
+
/**
|
|
6983
|
+
* Sync protected processes with the base ProcessRegistry.
|
|
6984
|
+
*/
|
|
6985
|
+
syncWithProcessRegistry() {
|
|
6986
|
+
}
|
|
6987
|
+
/**
|
|
6988
|
+
* Heartbeat - updates timestamps and checks for anomalies.
|
|
6989
|
+
*/
|
|
6990
|
+
heartbeat() {
|
|
6991
|
+
const now2 = Date.now();
|
|
6992
|
+
for (const [_pid, proc] of this.protectedProcesses) {
|
|
6993
|
+
proc.lastSeen = now2;
|
|
6994
|
+
}
|
|
6995
|
+
if (Math.random() < 0.1) {
|
|
6996
|
+
console.log(JSON.stringify({
|
|
6997
|
+
level: "debug",
|
|
6998
|
+
event: "process_guardian.heartbeat",
|
|
6999
|
+
protectedCount: this.protectedProcesses.size,
|
|
7000
|
+
instanceId: this.instanceId
|
|
7001
|
+
}));
|
|
7002
|
+
}
|
|
7003
|
+
}
|
|
7004
|
+
/**
|
|
7005
|
+
* Set up process-level event handlers.
|
|
7006
|
+
*/
|
|
7007
|
+
setupProcessHandlers() {
|
|
7008
|
+
process.on("exit", (code) => {
|
|
7009
|
+
console.log(JSON.stringify({
|
|
7010
|
+
level: "info",
|
|
7011
|
+
event: "process_guardian.process_exiting",
|
|
7012
|
+
pid: process.pid,
|
|
7013
|
+
code,
|
|
7014
|
+
instanceId: this.instanceId
|
|
7015
|
+
}));
|
|
7016
|
+
this.stop();
|
|
7017
|
+
});
|
|
7018
|
+
process.on("uncaughtException", (err) => {
|
|
7019
|
+
console.error(JSON.stringify({
|
|
7020
|
+
level: "error",
|
|
7021
|
+
event: "process_guardian.uncaught_exception",
|
|
7022
|
+
error: err.message,
|
|
7023
|
+
stack: err.stack,
|
|
7024
|
+
instanceId: this.instanceId
|
|
7025
|
+
}));
|
|
7026
|
+
});
|
|
7027
|
+
process.on("unhandledRejection", (reason) => {
|
|
7028
|
+
console.error(JSON.stringify({
|
|
7029
|
+
level: "error",
|
|
7030
|
+
event: "process_guardian.unhandled_rejection",
|
|
7031
|
+
reason: String(reason),
|
|
7032
|
+
instanceId: this.instanceId
|
|
7033
|
+
}));
|
|
7034
|
+
});
|
|
7035
|
+
process.on("SIGTERM", (origin) => {
|
|
7036
|
+
console.log(JSON.stringify({
|
|
7037
|
+
level: "warn",
|
|
7038
|
+
event: "process_guardian.sigterm_received",
|
|
7039
|
+
origin,
|
|
7040
|
+
pid: process.pid,
|
|
7041
|
+
instanceId: this.instanceId,
|
|
7042
|
+
message: "SIGTERM received but ignored - use graceful shutdown instead"
|
|
7043
|
+
}));
|
|
7044
|
+
});
|
|
7045
|
+
process.on("SIGHUP", () => {
|
|
7046
|
+
console.log(JSON.stringify({
|
|
7047
|
+
level: "warn",
|
|
7048
|
+
event: "process_guardian.sighup_received",
|
|
7049
|
+
pid: process.pid,
|
|
7050
|
+
instanceId: this.instanceId,
|
|
7051
|
+
message: "SIGHUP received but ignored - WrongStack continues running"
|
|
7052
|
+
}));
|
|
7053
|
+
});
|
|
7054
|
+
}
|
|
7055
|
+
/**
|
|
7056
|
+
* Check if a PID is protected by this guardian.
|
|
7057
|
+
*/
|
|
7058
|
+
isProtected(pid) {
|
|
7059
|
+
return this.protectedProcesses.has(pid);
|
|
7060
|
+
}
|
|
7061
|
+
/**
|
|
7062
|
+
* Get all PIDs protected by this guardian.
|
|
7063
|
+
*/
|
|
7064
|
+
getProtectedPids() {
|
|
7065
|
+
return Array.from(this.protectedProcesses.keys());
|
|
7066
|
+
}
|
|
7067
|
+
/**
|
|
7068
|
+
* Get status information for monitoring.
|
|
7069
|
+
*/
|
|
7070
|
+
getStatus() {
|
|
7071
|
+
return {
|
|
7072
|
+
instanceId: this.instanceId,
|
|
7073
|
+
mainPid: process.pid,
|
|
7074
|
+
protectedCount: this.protectedProcesses.size,
|
|
7075
|
+
platform: os2.platform(),
|
|
7076
|
+
hostname: os2.hostname(),
|
|
7077
|
+
uptime: process.uptime()
|
|
7078
|
+
};
|
|
7079
|
+
}
|
|
7080
|
+
};
|
|
7081
|
+
var _guardian;
|
|
7082
|
+
function getProcessGuardian() {
|
|
7083
|
+
if (!_guardian) {
|
|
7084
|
+
_guardian = new ProcessGuardian();
|
|
7085
|
+
}
|
|
7086
|
+
return _guardian;
|
|
7087
|
+
}
|
|
7088
|
+
function startProcessGuardian(config) {
|
|
7089
|
+
const guardian = new ProcessGuardian(config);
|
|
7090
|
+
guardian.start();
|
|
7091
|
+
_guardian = guardian;
|
|
7092
|
+
return guardian;
|
|
7093
|
+
}
|
|
7094
|
+
function stopProcessGuardian() {
|
|
7095
|
+
if (_guardian) {
|
|
7096
|
+
_guardian.stop();
|
|
7097
|
+
_guardian = void 0;
|
|
7098
|
+
}
|
|
7099
|
+
}
|
|
7100
|
+
var IDLE_THRESHOLD_MS = 2 * 6e4;
|
|
7101
|
+
var STALE_THRESHOLD_MS2 = 5 * 6e4;
|
|
7102
|
+
function now() {
|
|
7103
|
+
return Date.now();
|
|
7104
|
+
}
|
|
7105
|
+
function formatAge(ms) {
|
|
7106
|
+
if (ms < 1e3) return "0s";
|
|
7107
|
+
const seconds = Math.floor(ms / 1e3);
|
|
7108
|
+
if (seconds < 60) return `${seconds}s`;
|
|
7109
|
+
const minutes = Math.floor(seconds / 60);
|
|
7110
|
+
if (minutes < 60) return `${minutes}m`;
|
|
7111
|
+
const hours = Math.floor(minutes / 60);
|
|
7112
|
+
if (hours < 24) return `${hours}h`;
|
|
7113
|
+
const days = Math.floor(hours / 24);
|
|
7114
|
+
return `${days}d`;
|
|
7115
|
+
}
|
|
7116
|
+
function formatUptime(ms) {
|
|
7117
|
+
return formatAge(ms);
|
|
7118
|
+
}
|
|
7119
|
+
function matchGlob(pattern, value) {
|
|
7120
|
+
const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
7121
|
+
try {
|
|
7122
|
+
const regex = new RegExp(`^${regexPattern}$`, "i");
|
|
7123
|
+
return regex.test(value);
|
|
7124
|
+
} catch {
|
|
7125
|
+
return false;
|
|
7126
|
+
}
|
|
7127
|
+
}
|
|
7128
|
+
async function listInstances(options = {}) {
|
|
7129
|
+
const { includeStale = false, hostname: hostname4, status } = options;
|
|
7130
|
+
const timestamp = now();
|
|
7131
|
+
const registry = getPersistentProcessRegistry();
|
|
7132
|
+
const globalStatus = await registry.getGlobalStatus();
|
|
7133
|
+
const instances = [];
|
|
7134
|
+
const instanceMap = globalStatus.instances;
|
|
7135
|
+
for (const [instanceId, processes] of instanceMap) {
|
|
7136
|
+
if (processes.length === 0) continue;
|
|
7137
|
+
const mainProc = processes.find((p) => p.spawnMode === "main");
|
|
7138
|
+
const firstProc = processes.at(0);
|
|
7139
|
+
const mainPid = mainProc?.pid ?? firstProc?.pid ?? 0;
|
|
7140
|
+
const hostname_ = firstProc?.hostname ?? os2.hostname();
|
|
7141
|
+
const startedAt = Math.min(...processes.map((p) => p.startedAt));
|
|
7142
|
+
const lastActivity = Math.max(...processes.map((p) => p.lastHeartbeat));
|
|
7143
|
+
const age = timestamp - lastActivity;
|
|
7144
|
+
let instanceStatus = "stale";
|
|
7145
|
+
if (age < IDLE_THRESHOLD_MS) instanceStatus = "active";
|
|
7146
|
+
else if (age < STALE_THRESHOLD_MS2) instanceStatus = "idle";
|
|
7147
|
+
const sessionIds = /* @__PURE__ */ new Set();
|
|
7148
|
+
for (const proc of processes) {
|
|
7149
|
+
if (proc.sessionId) {
|
|
7150
|
+
sessionIds.add(proc.sessionId);
|
|
7151
|
+
}
|
|
7152
|
+
}
|
|
7153
|
+
if (!includeStale && instanceStatus === "stale") continue;
|
|
7154
|
+
if (hostname4 && !matchGlob(hostname4, hostname_)) continue;
|
|
7155
|
+
if (status && status !== "all" && instanceStatus !== status) continue;
|
|
7156
|
+
instances.push({
|
|
7157
|
+
instanceId,
|
|
7158
|
+
hostname: hostname_,
|
|
7159
|
+
mainPid,
|
|
7160
|
+
startedAt,
|
|
7161
|
+
lastActivity,
|
|
7162
|
+
status: instanceStatus,
|
|
7163
|
+
processCount: processes.length,
|
|
7164
|
+
processes,
|
|
7165
|
+
sessionIds
|
|
7166
|
+
});
|
|
7167
|
+
}
|
|
7168
|
+
instances.sort((a, b) => b.lastActivity - a.lastActivity);
|
|
7169
|
+
return instances;
|
|
7170
|
+
}
|
|
7171
|
+
async function getInstanceCount() {
|
|
7172
|
+
const instances = await listInstances({ includeStale: true });
|
|
7173
|
+
const byHostname = /* @__PURE__ */ new Map();
|
|
7174
|
+
let active = 0;
|
|
7175
|
+
let idle = 0;
|
|
7176
|
+
let stale = 0;
|
|
7177
|
+
for (const inst of instances) {
|
|
7178
|
+
const current = byHostname.get(inst.hostname) ?? 0;
|
|
7179
|
+
byHostname.set(inst.hostname, current + 1);
|
|
7180
|
+
switch (inst.status) {
|
|
7181
|
+
case "active":
|
|
7182
|
+
active++;
|
|
7183
|
+
break;
|
|
7184
|
+
case "idle":
|
|
7185
|
+
idle++;
|
|
7186
|
+
break;
|
|
7187
|
+
case "stale":
|
|
7188
|
+
stale++;
|
|
7189
|
+
break;
|
|
7190
|
+
}
|
|
7191
|
+
}
|
|
7192
|
+
return {
|
|
7193
|
+
total: instances.length,
|
|
7194
|
+
active,
|
|
7195
|
+
idle,
|
|
7196
|
+
stale,
|
|
7197
|
+
byHostname
|
|
7198
|
+
};
|
|
7199
|
+
}
|
|
7200
|
+
async function getGlobalProcessStatus() {
|
|
7201
|
+
const timestamp = now();
|
|
7202
|
+
const registry = getPersistentProcessRegistry();
|
|
7203
|
+
const globalStatus = await registry.getGlobalStatus();
|
|
7204
|
+
const instances = await listInstances({ includeStale: true });
|
|
7205
|
+
const localInstanceId = registry.getInstanceId();
|
|
7206
|
+
const localInstance = instances.find((i) => i.instanceId === localInstanceId);
|
|
7207
|
+
let localProtectedCount = 0;
|
|
7208
|
+
if (localInstance) {
|
|
7209
|
+
localProtectedCount = localInstance.processes.filter((p) => p.protected).length;
|
|
7210
|
+
}
|
|
7211
|
+
let activeInstanceCount = 0;
|
|
7212
|
+
for (const inst of instances) {
|
|
7213
|
+
if (inst.status === "active") activeInstanceCount++;
|
|
7214
|
+
}
|
|
7215
|
+
return {
|
|
7216
|
+
localInstance: localInstance ? {
|
|
7217
|
+
instanceId: localInstance.instanceId,
|
|
7218
|
+
mainPid: localInstance.mainPid,
|
|
7219
|
+
protectedCount: localProtectedCount,
|
|
7220
|
+
platform: process.platform,
|
|
7221
|
+
hostname: localInstance.hostname,
|
|
7222
|
+
uptime: timestamp - localInstance.startedAt
|
|
7223
|
+
} : {
|
|
7224
|
+
instanceId: localInstanceId,
|
|
7225
|
+
mainPid: process.pid,
|
|
7226
|
+
protectedCount: 0,
|
|
7227
|
+
platform: process.platform,
|
|
7228
|
+
hostname: os2.hostname(),
|
|
7229
|
+
uptime: 0
|
|
7230
|
+
},
|
|
7231
|
+
allInstances: instances.map((inst) => ({
|
|
7232
|
+
instanceId: inst.instanceId,
|
|
7233
|
+
hostname: inst.hostname,
|
|
7234
|
+
mainPid: inst.mainPid,
|
|
7235
|
+
processes: inst.processes,
|
|
7236
|
+
startedAt: inst.startedAt,
|
|
7237
|
+
lastActivity: inst.lastActivity
|
|
7238
|
+
})),
|
|
7239
|
+
summary: {
|
|
7240
|
+
totalProcesses: globalStatus.totalProcesses,
|
|
7241
|
+
protectedCount: globalStatus.protectedCount,
|
|
7242
|
+
staleCount: globalStatus.staleCount,
|
|
7243
|
+
instanceCount: instances.length,
|
|
7244
|
+
activeInstanceCount
|
|
7245
|
+
},
|
|
7246
|
+
timestamp
|
|
7247
|
+
};
|
|
7248
|
+
}
|
|
7249
|
+
async function formatGlobalStatus() {
|
|
7250
|
+
const status = await getGlobalProcessStatus();
|
|
7251
|
+
const lines = [];
|
|
7252
|
+
lines.push("=== WrongStack Global Process Status ===");
|
|
7253
|
+
lines.push(`Updated: ${new Date(status.timestamp).toISOString()}`);
|
|
7254
|
+
lines.push("");
|
|
7255
|
+
lines.push("Summary:");
|
|
7256
|
+
lines.push(` Total processes: ${status.summary.totalProcesses}`);
|
|
7257
|
+
lines.push(` Protected: ${status.summary.protectedCount}`);
|
|
7258
|
+
lines.push(` Stale entries: ${status.summary.staleCount}`);
|
|
7259
|
+
lines.push(` Instances: ${status.summary.instanceCount} (${status.summary.activeInstanceCount} active)`);
|
|
7260
|
+
lines.push("");
|
|
7261
|
+
lines.push(`This instance (${status.localInstance.instanceId}):`);
|
|
7262
|
+
lines.push(` Main PID: ${status.localInstance.mainPid}`);
|
|
7263
|
+
lines.push(` Protected processes: ${status.localInstance.protectedCount}`);
|
|
7264
|
+
lines.push(` Platform: ${status.localInstance.platform} (${status.localInstance.hostname})`);
|
|
7265
|
+
lines.push(` Uptime: ${formatUptime(status.localInstance.uptime)}`);
|
|
7266
|
+
lines.push("");
|
|
7267
|
+
for (const instance of status.allInstances) {
|
|
7268
|
+
if (instance.instanceId === status.localInstance.instanceId) continue;
|
|
7269
|
+
const age = Math.round((status.timestamp - instance.lastActivity) / 1e3);
|
|
7270
|
+
lines.push(`Instance ${instance.instanceId} (${instance.hostname}):`);
|
|
7271
|
+
for (const proc of instance.processes) {
|
|
7272
|
+
const procAge = formatAge(status.timestamp - proc.startedAt);
|
|
7273
|
+
const heartbeatAge = formatAge(status.timestamp - proc.lastHeartbeat);
|
|
7274
|
+
const protected_ = proc.protected ? "[P]" : " ";
|
|
7275
|
+
lines.push(
|
|
7276
|
+
` ${protected_} ${String(proc.pid).padStart(6)} ${proc.name.padEnd(20)} started ${procAge.padStart(8)} heartbeat ${heartbeatAge.padStart(6)} ${proc.spawnMode}`
|
|
7277
|
+
);
|
|
7278
|
+
}
|
|
7279
|
+
lines.push(` Last activity: ${age}s ago`);
|
|
7280
|
+
lines.push("");
|
|
7281
|
+
}
|
|
7282
|
+
lines.push("Legend:");
|
|
7283
|
+
lines.push(" [P] = Protected (cannot be killed via bash)");
|
|
7284
|
+
lines.push(" main = Main WrongStack process");
|
|
7285
|
+
lines.push(" spawn = Spawned child process");
|
|
7286
|
+
lines.push(" fork = Forked process (e.g., worker threads)");
|
|
7287
|
+
return lines.join("\n");
|
|
7288
|
+
}
|
|
7289
|
+
async function formatInstanceList(options = {}) {
|
|
7290
|
+
const instances = await listInstances(options);
|
|
7291
|
+
const count = await getInstanceCount();
|
|
7292
|
+
const lines = [];
|
|
7293
|
+
lines.push("=== WrongStack Instances ===");
|
|
7294
|
+
lines.push(`Total: ${count.total} instances (${count.active} active, ${count.idle} idle, ${count.stale} stale)`);
|
|
7295
|
+
lines.push("");
|
|
7296
|
+
if (count.byHostname.size > 1) {
|
|
7297
|
+
lines.push("By hostname:");
|
|
7298
|
+
for (const [host, num] of count.byHostname) {
|
|
7299
|
+
lines.push(` ${host}: ${num} instance${num !== 1 ? "s" : ""}`);
|
|
7300
|
+
}
|
|
7301
|
+
lines.push("");
|
|
7302
|
+
}
|
|
7303
|
+
if (instances.length === 0) {
|
|
7304
|
+
lines.push("No instances found matching the filter.");
|
|
7305
|
+
return lines.join("\n");
|
|
7306
|
+
}
|
|
7307
|
+
lines.push("INSTANCES:");
|
|
7308
|
+
lines.push(" " + [
|
|
7309
|
+
"STATUS".padEnd(7),
|
|
7310
|
+
"HOSTNAME".padEnd(16),
|
|
7311
|
+
"MAIN PID".padEnd(9),
|
|
7312
|
+
"PROCS".padEnd(6),
|
|
7313
|
+
"SESSIONS".padEnd(8),
|
|
7314
|
+
"UPTIME".padEnd(8),
|
|
7315
|
+
"LAST ACTIVITY"
|
|
7316
|
+
].join(" "));
|
|
7317
|
+
lines.push(" " + "-".repeat(80));
|
|
7318
|
+
for (const inst of instances) {
|
|
7319
|
+
const uptime = formatAge(Date.now() - inst.startedAt);
|
|
7320
|
+
const lastAct = formatAge(Date.now() - inst.lastActivity);
|
|
7321
|
+
const statusIcon = inst.status === "active" ? "[*]" : inst.status === "idle" ? "[-]" : "[ ]";
|
|
7322
|
+
lines.push(
|
|
7323
|
+
" " + [
|
|
7324
|
+
`${statusIcon} ${inst.status}`.padEnd(7),
|
|
7325
|
+
inst.hostname.padEnd(16),
|
|
7326
|
+
String(inst.mainPid).padEnd(9),
|
|
7327
|
+
String(inst.processCount).padEnd(6),
|
|
7328
|
+
String(inst.sessionIds.size).padEnd(8),
|
|
7329
|
+
uptime.padEnd(8),
|
|
7330
|
+
`${lastAct} ago`
|
|
7331
|
+
].join(" ")
|
|
7332
|
+
);
|
|
7333
|
+
}
|
|
7334
|
+
lines.push("");
|
|
7335
|
+
lines.push("Use /ps full for detailed process listing per instance.");
|
|
7336
|
+
return lines.join("\n");
|
|
7337
|
+
}
|
|
7338
|
+
async function formatInstanceSummary() {
|
|
7339
|
+
const count = await getInstanceCount();
|
|
7340
|
+
const instances = await listInstances({ includeStale: false });
|
|
7341
|
+
if (instances.length === 0) {
|
|
7342
|
+
return "No active WrongStack instances.";
|
|
7343
|
+
}
|
|
7344
|
+
const lines = [];
|
|
7345
|
+
lines.push(`${count.total} instance${count.total !== 1 ? "s" : ""}`);
|
|
7346
|
+
const byStatus = /* @__PURE__ */ new Map();
|
|
7347
|
+
for (const inst of instances) {
|
|
7348
|
+
byStatus.set(inst.status, (byStatus.get(inst.status) ?? 0) + 1);
|
|
7349
|
+
}
|
|
7350
|
+
const parts = [];
|
|
7351
|
+
if (byStatus.get("active")) parts.push(`${byStatus.get("active")} active`);
|
|
7352
|
+
if (byStatus.get("idle")) parts.push(`${byStatus.get("idle")} idle`);
|
|
7353
|
+
if (byStatus.get("stale")) parts.push(`${byStatus.get("stale")} stale`);
|
|
7354
|
+
lines.push(`(${parts.join(", ")})`);
|
|
7355
|
+
const totalProcs = instances.reduce((sum, inst) => sum + inst.processCount, 0);
|
|
7356
|
+
lines.push(`${totalProcs} total processes`);
|
|
7357
|
+
return lines.join(" ");
|
|
7358
|
+
}
|
|
7359
|
+
function createGlobalPsSlashCommand() {
|
|
7360
|
+
return {
|
|
7361
|
+
name: "ps",
|
|
7362
|
+
description: "List all WrongStack instances and their processes",
|
|
7363
|
+
async handler(input) {
|
|
7364
|
+
try {
|
|
7365
|
+
const trimmed = input.trim();
|
|
7366
|
+
const parts = trimmed.split(/\s+/);
|
|
7367
|
+
const sub = parts[0]?.toLowerCase() ?? "";
|
|
7368
|
+
if (sub === "list" || sub === "ls" || sub === "") {
|
|
7369
|
+
const output = await formatInstanceList();
|
|
7370
|
+
return { message: output };
|
|
7371
|
+
}
|
|
7372
|
+
if (sub === "summary" || sub === "sum") {
|
|
7373
|
+
const output = await formatInstanceSummary();
|
|
7374
|
+
return { message: output };
|
|
7375
|
+
}
|
|
7376
|
+
if (sub === "full" || sub === "detail") {
|
|
7377
|
+
const output = await formatGlobalStatus();
|
|
7378
|
+
return { message: output };
|
|
7379
|
+
}
|
|
7380
|
+
if (sub === "count" || sub === "num") {
|
|
7381
|
+
const count = await getInstanceCount();
|
|
7382
|
+
return {
|
|
7383
|
+
message: `${count.total} instance${count.total !== 1 ? "s" : ""} (${count.active} active, ${count.idle} idle, ${count.stale} stale)`
|
|
7384
|
+
};
|
|
7385
|
+
}
|
|
7386
|
+
if (sub === "hostname" || sub === "host") {
|
|
7387
|
+
const pattern = parts.slice(1).join(" ");
|
|
7388
|
+
if (!pattern) {
|
|
7389
|
+
return { message: "Usage: /ps hostname <pattern> (e.g., /ps hostname workstation*)" };
|
|
7390
|
+
}
|
|
7391
|
+
const output = await formatInstanceList({ hostname: pattern });
|
|
7392
|
+
return { message: output };
|
|
7393
|
+
}
|
|
7394
|
+
if (sub === "status" || sub === "state") {
|
|
7395
|
+
const filterStatus = parts[1]?.toLowerCase();
|
|
7396
|
+
if (!["active", "idle", "stale", "all"].includes(filterStatus ?? "")) {
|
|
7397
|
+
return { message: "Usage: /ps status <active|idle|stale|all>" };
|
|
7398
|
+
}
|
|
7399
|
+
const output = await formatInstanceList({ status: filterStatus });
|
|
7400
|
+
return { message: output };
|
|
7401
|
+
}
|
|
7402
|
+
return { message: "Usage: /ps [list|summary|count|full|hostname <pattern>|status <state>]" };
|
|
7403
|
+
} catch (err) {
|
|
7404
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7405
|
+
return { message: `Error getting process status: ${message}` };
|
|
7406
|
+
}
|
|
7407
|
+
}
|
|
7408
|
+
};
|
|
7409
|
+
}
|
|
6248
7410
|
|
|
6249
7411
|
// src/codebase-index/circuit-breaker.ts
|
|
6250
7412
|
var CircuitOpenError = class extends Error {
|
|
@@ -6483,7 +7645,7 @@ function loadDatabaseSync() {
|
|
|
6483
7645
|
DatabaseSyncCtor = req("node:sqlite").DatabaseSync;
|
|
6484
7646
|
} catch (err) {
|
|
6485
7647
|
throw new Error(
|
|
6486
|
-
`The codebase index needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${toErrorMessage(err)}`
|
|
7648
|
+
`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)}`
|
|
6487
7649
|
);
|
|
6488
7650
|
}
|
|
6489
7651
|
return DatabaseSyncCtor;
|
|
@@ -6553,7 +7715,7 @@ var IndexStore = class {
|
|
|
6553
7715
|
}
|
|
6554
7716
|
constructor(projectRoot, opts = {}) {
|
|
6555
7717
|
this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
|
|
6556
|
-
|
|
7718
|
+
fs8.mkdirSync(this.indexDir, { recursive: true });
|
|
6557
7719
|
const Database = loadDatabaseSync();
|
|
6558
7720
|
this.db = new Database(path.join(this.indexDir, DB_FILE));
|
|
6559
7721
|
try {
|
|
@@ -6632,33 +7794,53 @@ var IndexStore = class {
|
|
|
6632
7794
|
}
|
|
6633
7795
|
}
|
|
6634
7796
|
// ─── Symbol CRUD ─────────────────────────────────────────────────────────────
|
|
6635
|
-
|
|
7797
|
+
/**
|
|
7798
|
+
* Insert symbols, assigning IDs atomically inside `BEGIN IMMEDIATE` /
|
|
7799
|
+
* `COMMIT`. The ID allocation (`SELECT MAX(id)`) and all `INSERT`s share
|
|
7800
|
+
* the same transaction, preventing UNIQUE constraint violations when two
|
|
7801
|
+
* processes index concurrently (each would see a different `MAX(id)` and
|
|
7802
|
+
* neither can insert with the other's IDs).
|
|
7803
|
+
*
|
|
7804
|
+
* @returns The symbols array with `id` fields populated so the caller can
|
|
7805
|
+
* use them for refs without re-reading from the DB.
|
|
7806
|
+
*/
|
|
7807
|
+
insertSymbols(symbols) {
|
|
6636
7808
|
return this.runWithRetry(() => {
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
stmt.run(
|
|
6645
|
-
id,
|
|
6646
|
-
s.lang,
|
|
6647
|
-
s.kind,
|
|
6648
|
-
s.name,
|
|
6649
|
-
s.file,
|
|
6650
|
-
s.line,
|
|
6651
|
-
s.col,
|
|
6652
|
-
s.signature,
|
|
6653
|
-
s.docComment,
|
|
6654
|
-
s.scope,
|
|
6655
|
-
s.text,
|
|
6656
|
-
s.file
|
|
7809
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
7810
|
+
try {
|
|
7811
|
+
const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
|
|
7812
|
+
let nextId = (maxRows[0]?.m ?? 0) + 1;
|
|
7813
|
+
const stmt = this.db.prepare(
|
|
7814
|
+
`INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
|
|
7815
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
6657
7816
|
);
|
|
6658
|
-
ftsStmt
|
|
6659
|
-
|
|
7817
|
+
const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
|
|
7818
|
+
const result = [];
|
|
7819
|
+
for (const s of symbols) {
|
|
7820
|
+
const id = nextId++;
|
|
7821
|
+
stmt.run(
|
|
7822
|
+
id,
|
|
7823
|
+
s.lang,
|
|
7824
|
+
s.kind,
|
|
7825
|
+
s.name,
|
|
7826
|
+
s.file,
|
|
7827
|
+
s.line,
|
|
7828
|
+
s.col,
|
|
7829
|
+
s.signature,
|
|
7830
|
+
s.docComment,
|
|
7831
|
+
s.scope,
|
|
7832
|
+
s.text,
|
|
7833
|
+
s.file
|
|
7834
|
+
);
|
|
7835
|
+
ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
|
|
7836
|
+
result.push({ ...s, id });
|
|
7837
|
+
}
|
|
7838
|
+
this.db.exec("COMMIT");
|
|
7839
|
+
return result;
|
|
7840
|
+
} catch (err) {
|
|
7841
|
+
this.db.exec("ROLLBACK");
|
|
7842
|
+
throw err;
|
|
6660
7843
|
}
|
|
6661
|
-
return id;
|
|
6662
7844
|
});
|
|
6663
7845
|
}
|
|
6664
7846
|
deleteSymbolsForFile(file) {
|
|
@@ -7016,7 +8198,7 @@ var IndexStore = class {
|
|
|
7016
8198
|
sizeBytes() {
|
|
7017
8199
|
const dbPath = path.join(this.indexDir, DB_FILE);
|
|
7018
8200
|
try {
|
|
7019
|
-
return
|
|
8201
|
+
return fs8.statSync(dbPath).size;
|
|
7020
8202
|
} catch {
|
|
7021
8203
|
return 0;
|
|
7022
8204
|
}
|
|
@@ -7212,10 +8394,10 @@ function detectLang(file) {
|
|
|
7212
8394
|
if (idx < 0) return null;
|
|
7213
8395
|
return extToLang(file.slice(idx));
|
|
7214
8396
|
}
|
|
7215
|
-
function parseSymbols2(opts) {
|
|
8397
|
+
async function parseSymbols2(opts) {
|
|
7216
8398
|
const { file, content, lang } = opts;
|
|
7217
8399
|
try {
|
|
7218
|
-
return syncGoParse(file, content, lang);
|
|
8400
|
+
return await syncGoParse(file, content, lang);
|
|
7219
8401
|
} catch {
|
|
7220
8402
|
return { file, lang, symbols: [], mtimeMs: Date.now() };
|
|
7221
8403
|
}
|
|
@@ -7452,19 +8634,34 @@ func formatType(t ast.Expr) string {
|
|
|
7452
8634
|
}
|
|
7453
8635
|
}
|
|
7454
8636
|
`;
|
|
7455
|
-
function syncGoParse(filePath, content, lang) {
|
|
7456
|
-
const tmpDir = path.join(
|
|
8637
|
+
async function syncGoParse(filePath, content, lang) {
|
|
8638
|
+
const tmpDir = path.join(os2.tmpdir(), "ws-go-parse");
|
|
7457
8639
|
try {
|
|
7458
|
-
|
|
8640
|
+
await fs7.mkdir(tmpDir, { recursive: true });
|
|
7459
8641
|
const scriptPath = path.join(tmpDir, "parse.go");
|
|
7460
|
-
|
|
7461
|
-
const
|
|
7462
|
-
|
|
7463
|
-
timeout: 15e3,
|
|
7464
|
-
encoding: "utf8",
|
|
8642
|
+
await fs7.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
|
|
8643
|
+
const proc = spawn("go", ["run", scriptPath], {
|
|
8644
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
7465
8645
|
windowsHide: true
|
|
7466
8646
|
});
|
|
7467
|
-
|
|
8647
|
+
let stdout = "";
|
|
8648
|
+
proc.stdout?.on("data", (chunk) => {
|
|
8649
|
+
stdout += chunk.toString();
|
|
8650
|
+
});
|
|
8651
|
+
proc.stdin?.write(content);
|
|
8652
|
+
proc.stdin?.end();
|
|
8653
|
+
const { code } = await Promise.race([
|
|
8654
|
+
new Promise((resolve6) => {
|
|
8655
|
+
proc.on("close", (c) => resolve6({ code: c }));
|
|
8656
|
+
}),
|
|
8657
|
+
new Promise(
|
|
8658
|
+
(_, reject) => setTimeout(() => {
|
|
8659
|
+
proc.kill("SIGKILL");
|
|
8660
|
+
reject(new Error("timeout"));
|
|
8661
|
+
}, 15e3)
|
|
8662
|
+
)
|
|
8663
|
+
]).catch(() => ({ code: -1 }));
|
|
8664
|
+
if (code !== 0 || !stdout.trim()) {
|
|
7468
8665
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
7469
8666
|
}
|
|
7470
8667
|
const raw = JSON.parse(stdout.trim());
|
|
@@ -7486,10 +8683,10 @@ function syncGoParse(filePath, content, lang) {
|
|
|
7486
8683
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
7487
8684
|
}
|
|
7488
8685
|
}
|
|
7489
|
-
function parseSymbols3(opts) {
|
|
8686
|
+
async function parseSymbols3(opts) {
|
|
7490
8687
|
const { file, lang } = opts;
|
|
7491
8688
|
try {
|
|
7492
|
-
return syncPyParse(file, lang);
|
|
8689
|
+
return await syncPyParse(file, lang);
|
|
7493
8690
|
} catch {
|
|
7494
8691
|
return { file, lang, symbols: [], mtimeMs: Date.now() };
|
|
7495
8692
|
}
|
|
@@ -7698,18 +8895,32 @@ visitor.visit(tree)
|
|
|
7698
8895
|
|
|
7699
8896
|
print(json.dumps([s.to_dict() for s in syms]))
|
|
7700
8897
|
`;
|
|
7701
|
-
function syncPyParse(filePath, lang) {
|
|
8898
|
+
async function syncPyParse(filePath, lang) {
|
|
7702
8899
|
try {
|
|
7703
|
-
const tmpDir = path.join(
|
|
7704
|
-
|
|
8900
|
+
const tmpDir = path.join(os2.tmpdir(), "ws-py-parse");
|
|
8901
|
+
await fs7.mkdir(tmpDir, { recursive: true });
|
|
7705
8902
|
const scriptPath = path.join(tmpDir, "parse.py");
|
|
7706
|
-
|
|
7707
|
-
const
|
|
7708
|
-
|
|
7709
|
-
encoding: "utf8",
|
|
8903
|
+
await fs7.writeFile(scriptPath, PY_PARSE_SCRIPT, "utf8");
|
|
8904
|
+
const proc = spawn("python", [scriptPath, filePath], {
|
|
8905
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
7710
8906
|
windowsHide: true
|
|
7711
8907
|
});
|
|
7712
|
-
|
|
8908
|
+
let stdout = "";
|
|
8909
|
+
proc.stdout?.on("data", (chunk) => {
|
|
8910
|
+
stdout += chunk.toString();
|
|
8911
|
+
});
|
|
8912
|
+
const { code } = await Promise.race([
|
|
8913
|
+
new Promise((resolve6) => {
|
|
8914
|
+
proc.on("close", (c) => resolve6({ code: c }));
|
|
8915
|
+
}),
|
|
8916
|
+
new Promise(
|
|
8917
|
+
(_, reject) => setTimeout(() => {
|
|
8918
|
+
proc.kill("SIGKILL");
|
|
8919
|
+
reject(new Error("timeout"));
|
|
8920
|
+
}, 15e3)
|
|
8921
|
+
)
|
|
8922
|
+
]).catch(() => ({ code: -1 }));
|
|
8923
|
+
if (code !== 0 || !stdout.trim()) {
|
|
7713
8924
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
7714
8925
|
}
|
|
7715
8926
|
const raw = JSON.parse(stdout.trim());
|
|
@@ -7731,11 +8942,11 @@ function syncPyParse(filePath, lang) {
|
|
|
7731
8942
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
7732
8943
|
}
|
|
7733
8944
|
}
|
|
7734
|
-
function parseSymbols4(opts) {
|
|
8945
|
+
async function parseSymbols4(opts) {
|
|
7735
8946
|
const { file, content, lang } = opts;
|
|
7736
8947
|
const nativeAvailable = checkNativeParser();
|
|
7737
8948
|
if (nativeAvailable) {
|
|
7738
|
-
const result = tryNativeParse(file, content);
|
|
8949
|
+
const result = await tryNativeParse(file, content);
|
|
7739
8950
|
if (result) return result;
|
|
7740
8951
|
}
|
|
7741
8952
|
return regexParse({ file, content, lang });
|
|
@@ -7765,25 +8976,34 @@ function checkNativeParser() {
|
|
|
7765
8976
|
return false;
|
|
7766
8977
|
}
|
|
7767
8978
|
}
|
|
7768
|
-
function tryNativeParse(file, content) {
|
|
8979
|
+
async function tryNativeParse(file, content) {
|
|
7769
8980
|
try {
|
|
7770
8981
|
const toolsDir = path.join(process.cwd(), "tools");
|
|
7771
8982
|
const crateDir = path.join(toolsDir, "syn-parser");
|
|
7772
8983
|
const tmpFile = path.join(crateDir, "src", "input.rs");
|
|
7773
|
-
|
|
7774
|
-
const
|
|
7775
|
-
|
|
7776
|
-
["
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
8984
|
+
await fs7.writeFile(tmpFile, content, "utf8");
|
|
8985
|
+
const proc = spawn("cargo", ["run", "--manifest-path", path.join(toolsDir, "Cargo.toml")], {
|
|
8986
|
+
cwd: process.cwd(),
|
|
8987
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
8988
|
+
windowsHide: true
|
|
8989
|
+
});
|
|
8990
|
+
let stdout = "";
|
|
8991
|
+
proc.stdout?.on("data", (chunk) => {
|
|
8992
|
+
stdout += chunk.toString();
|
|
8993
|
+
});
|
|
8994
|
+
const { code } = await Promise.race([
|
|
8995
|
+
new Promise((resolve6) => {
|
|
8996
|
+
proc.on("close", (c) => resolve6({ code: c }));
|
|
8997
|
+
}),
|
|
8998
|
+
new Promise(
|
|
8999
|
+
(_, reject) => setTimeout(() => {
|
|
9000
|
+
proc.kill("SIGKILL");
|
|
9001
|
+
reject(new Error("timeout"));
|
|
9002
|
+
}, 15e3)
|
|
9003
|
+
)
|
|
9004
|
+
]).catch(() => ({ code: -1 }));
|
|
9005
|
+
if (code === 0 && stdout.trim()) {
|
|
9006
|
+
const symbols = JSON.parse(stdout.trim());
|
|
7787
9007
|
return {
|
|
7788
9008
|
file,
|
|
7789
9009
|
lang: "rs",
|
|
@@ -8252,7 +9472,7 @@ function compileGitignore(lines) {
|
|
|
8252
9472
|
async function loadGitignoreMatcher(projectRoot) {
|
|
8253
9473
|
let lines = [];
|
|
8254
9474
|
try {
|
|
8255
|
-
const raw = await
|
|
9475
|
+
const raw = await fs7.readFile(path.join(projectRoot, ".gitignore"), "utf8");
|
|
8256
9476
|
lines = raw.split("\n");
|
|
8257
9477
|
} catch {
|
|
8258
9478
|
}
|
|
@@ -8310,7 +9530,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
8310
9530
|
}
|
|
8311
9531
|
let entries;
|
|
8312
9532
|
try {
|
|
8313
|
-
entries = await
|
|
9533
|
+
entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
8314
9534
|
} catch {
|
|
8315
9535
|
return;
|
|
8316
9536
|
}
|
|
@@ -8408,7 +9628,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
8408
9628
|
batchFiles.map(async (file) => {
|
|
8409
9629
|
let stat11;
|
|
8410
9630
|
try {
|
|
8411
|
-
stat11 = await
|
|
9631
|
+
stat11 = await fs7.stat(file, statOpts);
|
|
8412
9632
|
} catch (e) {
|
|
8413
9633
|
if (isAbortError(e)) throw e;
|
|
8414
9634
|
return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
|
|
@@ -8422,7 +9642,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
8422
9642
|
}
|
|
8423
9643
|
let content;
|
|
8424
9644
|
try {
|
|
8425
|
-
content = await
|
|
9645
|
+
content = await fs7.readFile(file, { encoding: "utf8", signal });
|
|
8426
9646
|
} catch (e) {
|
|
8427
9647
|
if (isAbortError(e)) throw e;
|
|
8428
9648
|
return { file, stat: stat11, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
|
|
@@ -8472,9 +9692,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
8472
9692
|
filesIndexed++;
|
|
8473
9693
|
continue;
|
|
8474
9694
|
}
|
|
8475
|
-
const
|
|
8476
|
-
const symbolsWithIds = parsed.symbols.map((s, i) => ({ ...s, id: nextId + i }));
|
|
8477
|
-
store.insertSymbols(symbolsWithIds, nextId);
|
|
9695
|
+
const symbolsWithIds = store.insertSymbols(parsed.symbols);
|
|
8478
9696
|
const count = symbolsWithIds.length;
|
|
8479
9697
|
symbolsIndexed += count;
|
|
8480
9698
|
langStats[lang] = (langStats[lang] ?? 0) + count;
|
|
@@ -8503,7 +9721,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
8503
9721
|
}
|
|
8504
9722
|
for (const [file_] of existingMeta) {
|
|
8505
9723
|
try {
|
|
8506
|
-
await
|
|
9724
|
+
await fs7.stat(file_);
|
|
8507
9725
|
} catch {
|
|
8508
9726
|
store.deleteFile(file_);
|
|
8509
9727
|
}
|
|
@@ -8618,7 +9836,7 @@ function resolveWorkerUrl() {
|
|
|
8618
9836
|
for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
|
|
8619
9837
|
try {
|
|
8620
9838
|
const url = new URL(rel, import.meta.url);
|
|
8621
|
-
if (url.protocol === "file:" &&
|
|
9839
|
+
if (url.protocol === "file:" && fs8.existsSync(fileURLToPath(url))) return url;
|
|
8622
9840
|
} catch {
|
|
8623
9841
|
}
|
|
8624
9842
|
}
|
|
@@ -8776,10 +9994,10 @@ function debounceKey(indexDir, file) {
|
|
|
8776
9994
|
function isIndexableFile(filePath) {
|
|
8777
9995
|
return detectLang(filePath) !== null;
|
|
8778
9996
|
}
|
|
8779
|
-
function
|
|
9997
|
+
function isRecoverableConstraintError(err) {
|
|
8780
9998
|
if (err instanceof Error) {
|
|
8781
9999
|
const msg = err.message.toLowerCase();
|
|
8782
|
-
return msg.includes("unique constraint") || msg.includes("
|
|
10000
|
+
return msg.includes("unique constraint") || msg.includes("constraint failed");
|
|
8783
10001
|
}
|
|
8784
10002
|
return false;
|
|
8785
10003
|
}
|
|
@@ -8812,7 +10030,7 @@ async function runStartupIndex(opts) {
|
|
|
8812
10030
|
return result;
|
|
8813
10031
|
} catch (err) {
|
|
8814
10032
|
_lastError = err instanceof Error ? err.message : String(err);
|
|
8815
|
-
if (
|
|
10033
|
+
if (isRecoverableConstraintError(err) && !opts.force) {
|
|
8816
10034
|
_lastError = null;
|
|
8817
10035
|
const rebuildResult = await runStartupIndex({
|
|
8818
10036
|
...opts,
|
|
@@ -9114,11 +10332,11 @@ var setWorkingDirTool = {
|
|
|
9114
10332
|
} catch (err) {
|
|
9115
10333
|
return {
|
|
9116
10334
|
current: ctx.workingDir,
|
|
9117
|
-
error: toErrorMessage(err)
|
|
10335
|
+
error: toErrorMessage$1(err)
|
|
9118
10336
|
};
|
|
9119
10337
|
}
|
|
9120
10338
|
try {
|
|
9121
|
-
await
|
|
10339
|
+
await fs7.access(resolved);
|
|
9122
10340
|
} catch {
|
|
9123
10341
|
try {
|
|
9124
10342
|
ctx.setWorkingDir(previous);
|
|
@@ -9289,11 +10507,11 @@ var taskTool = {
|
|
|
9289
10507
|
}
|
|
9290
10508
|
}
|
|
9291
10509
|
}
|
|
9292
|
-
const
|
|
10510
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
9293
10511
|
f.tasks = input.tasks.map((t) => ({
|
|
9294
10512
|
...t,
|
|
9295
|
-
createdAt: t.createdAt ||
|
|
9296
|
-
updatedAt:
|
|
10513
|
+
createdAt: t.createdAt || now2,
|
|
10514
|
+
updatedAt: now2
|
|
9297
10515
|
}));
|
|
9298
10516
|
break;
|
|
9299
10517
|
}
|
|
@@ -9317,7 +10535,7 @@ var taskTool = {
|
|
|
9317
10535
|
return f;
|
|
9318
10536
|
}
|
|
9319
10537
|
}
|
|
9320
|
-
const
|
|
10538
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
9321
10539
|
const newTask = {
|
|
9322
10540
|
id: `task_${Date.now()}_${randomUUID().slice(0, 8)}`,
|
|
9323
10541
|
title: t.title,
|
|
@@ -9329,8 +10547,8 @@ var taskTool = {
|
|
|
9329
10547
|
assignee: t.assignee,
|
|
9330
10548
|
estimateHours: t.estimateHours,
|
|
9331
10549
|
tags: t.tags,
|
|
9332
|
-
createdAt:
|
|
9333
|
-
updatedAt:
|
|
10550
|
+
createdAt: now2,
|
|
10551
|
+
updatedAt: now2
|
|
9334
10552
|
};
|
|
9335
10553
|
f.tasks.push(newTask);
|
|
9336
10554
|
break;
|
|
@@ -9756,6 +10974,6 @@ var TOOL_ICON_CONFIG = {
|
|
|
9756
10974
|
};
|
|
9757
10975
|
var FALLBACK_ICON = "fallback";
|
|
9758
10976
|
|
|
9759
|
-
export { CircuitBreaker, CircuitOpenError, FALLBACK_ICON, IndexCircuitBreaker, IndexTimeoutError, OPTIONAL_TOOLS, TIER1_TOOLS, TIER2_TOOLS, TIER3_TOOLS, TOOL_ICON_CONFIG, TOOL_ICON_MAP, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, cancelPendingReindexes, codebaseIndexStats, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, createModeTool, diffTool, documentTool, editTool, enqueueReindex, execTool, fetchTool, forgetTool, formatTool, getIndexState, getProcessRegistry, getToolIcon, gitTool, globTool, grepTool, indexCircuitBreaker, installTool, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, logsTool, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, relatedMemoryTool, rememberTool, replaceTool, resetIndexCircuitBreaker, runStartupIndex, scaffoldTool, searchCodebaseIndex, searchMemoryTool, searchTool, shutdownCodebaseIndexHost, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
|
|
10977
|
+
export { CircuitBreaker, CircuitOpenError, FALLBACK_ICON, IndexCircuitBreaker, IndexTimeoutError, OPTIONAL_TOOLS, TIER1_TOOLS, TIER2_TOOLS, TIER3_TOOLS, TOOL_ICON_CONFIG, TOOL_ICON_MAP, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, cancelPendingReindexes, codebaseIndexStats, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, createGlobalPsSlashCommand, createModeTool, diffTool, documentTool, editTool, enqueueReindex, execTool, fetchTool, forgetTool, formatGlobalStatus, formatInstanceList, formatInstanceSummary, formatTool, getIndexState, getInstanceCount, getPersistentProcessRegistry, getProcessGuardian, getProcessRegistry, getToolIcon, gitTool, globTool, grepTool, indexCircuitBreaker, installTool, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, listInstances, logsTool, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, relatedMemoryTool, rememberTool, replaceTool, resetIndexCircuitBreaker, resetPersistentProcessRegistry, runStartupIndex, scaffoldTool, searchCodebaseIndex, searchMemoryTool, searchTool, shutdownCodebaseIndexHost, startProcessGuardian, stopProcessGuardian, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
|
|
9760
10978
|
//# sourceMappingURL=index.js.map
|
|
9761
10979
|
//# sourceMappingURL=index.js.map
|