@wrongstack/tools 0.273.1 → 0.275.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit.js +57 -19
- package/dist/audit.js.map +1 -1
- package/dist/bash.js +166 -83
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +669 -189
- package/dist/builtin.js.map +1 -1
- package/dist/circuit-breaker.js +1 -0
- package/dist/circuit-breaker.js.map +1 -1
- package/dist/codebase-index/index.js +24 -16
- package/dist/codebase-index/index.js.map +1 -1
- package/dist/codebase-index/worker.js +24 -16
- package/dist/codebase-index/worker.js.map +1 -1
- package/dist/edit.js +1 -1
- package/dist/edit.js.map +1 -1
- package/dist/exec.js +136 -38
- package/dist/exec.js.map +1 -1
- package/dist/fetch.js +9 -1
- package/dist/fetch.js.map +1 -1
- package/dist/format.js +57 -19
- package/dist/format.js.map +1 -1
- package/dist/glob.js +61 -11
- package/dist/glob.js.map +1 -1
- package/dist/grep.js +92 -39
- package/dist/grep.js.map +1 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +680 -200
- package/dist/index.js.map +1 -1
- package/dist/install.js +65 -19
- package/dist/install.js.map +1 -1
- package/dist/lint.js +57 -19
- package/dist/lint.js.map +1 -1
- package/dist/pack.js +667 -188
- package/dist/pack.js.map +1 -1
- package/dist/process-registry.d.ts +30 -0
- package/dist/process-registry.js +57 -19
- package/dist/process-registry.js.map +1 -1
- package/dist/search.js.map +1 -1
- package/dist/test.js +57 -19
- package/dist/test.js.map +1 -1
- package/dist/typecheck.js +57 -19
- package/dist/typecheck.js.map +1 -1
- package/dist/write.js +2 -2
- package/dist/write.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs7 from 'node:fs/promises';
|
|
2
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
|
+
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, getDesignKitLoader, isDesignStack, loadActiveKit, applyTokenOverrides, setActiveKit, recordKitChoice, recordOverrides, setDesignOverrides, materializeTokens, runDesignVerify, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
5
|
import { resolve, sep, dirname, join } from 'node:path';
|
|
6
6
|
import { spawn, execFileSync } from 'node:child_process';
|
|
@@ -375,7 +375,7 @@ var writeTool = {
|
|
|
375
375
|
if (existed) {
|
|
376
376
|
if (!ctx.hasRead(absPath)) {
|
|
377
377
|
prev = await fs7.readFile(absPath, "utf8");
|
|
378
|
-
ctx.recordRead(absPath, stat12.mtimeMs);
|
|
378
|
+
ctx.recordRead(absPath, stat12.mtimeMs, "write");
|
|
379
379
|
} else {
|
|
380
380
|
prev = await fs7.readFile(absPath, "utf8");
|
|
381
381
|
}
|
|
@@ -389,7 +389,7 @@ var writeTool = {
|
|
|
389
389
|
const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
390
390
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
391
391
|
const stat11 = await fs7.stat(absPath);
|
|
392
|
-
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
392
|
+
ctx.recordRead(absPath, stat11.mtimeMs, "write");
|
|
393
393
|
ctx.session.recordFileChange({
|
|
394
394
|
path: absPath,
|
|
395
395
|
action: existed ? "modified" : "created",
|
|
@@ -486,7 +486,7 @@ var editTool = {
|
|
|
486
486
|
const newFile = toStyle(newFileLf, style);
|
|
487
487
|
await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
|
|
488
488
|
const written = await fs7.stat(absPath);
|
|
489
|
-
ctx.recordRead(absPath, written.mtimeMs);
|
|
489
|
+
ctx.recordRead(absPath, written.mtimeMs, "write");
|
|
490
490
|
ctx.session.recordFileChange({
|
|
491
491
|
path: absPath,
|
|
492
492
|
action: "modified",
|
|
@@ -778,7 +778,27 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
778
778
|
await walk(base);
|
|
779
779
|
return results;
|
|
780
780
|
}
|
|
781
|
+
|
|
782
|
+
// src/_concurrency.ts
|
|
783
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
784
|
+
if (items.length === 0) return [];
|
|
785
|
+
const effectiveLimit = Math.max(1, Math.min(limit | 0, items.length));
|
|
786
|
+
const results = new Array(items.length);
|
|
787
|
+
let nextIndex = 0;
|
|
788
|
+
const worker2 = async () => {
|
|
789
|
+
while (true) {
|
|
790
|
+
const i = nextIndex++;
|
|
791
|
+
if (i >= items.length) return;
|
|
792
|
+
results[i] = await fn(items[i]);
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
await Promise.all(Array.from({ length: effectiveLimit }, worker2));
|
|
796
|
+
return results;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/glob.ts
|
|
781
800
|
var DEFAULT_IGNORE2 = ["node_modules", ".git", "dist", "build", ".next", "coverage", ".turbo"];
|
|
801
|
+
var WALK_CONCURRENCY = 16;
|
|
782
802
|
var globTool = {
|
|
783
803
|
name: "glob",
|
|
784
804
|
category: "Filesystem",
|
|
@@ -816,6 +836,22 @@ var globTool = {
|
|
|
816
836
|
const re = compileGlob(input.pattern);
|
|
817
837
|
const results = [];
|
|
818
838
|
let truncated = false;
|
|
839
|
+
const pushResult = async (full) => {
|
|
840
|
+
if (truncated || results.length >= limit) {
|
|
841
|
+
truncated = true;
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
try {
|
|
845
|
+
const st = await fs7.stat(full);
|
|
846
|
+
if (truncated || results.length >= limit) {
|
|
847
|
+
truncated = true;
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
results.push({ rel: full, mtime: st.mtimeMs });
|
|
851
|
+
if (results.length >= limit) truncated = true;
|
|
852
|
+
} catch {
|
|
853
|
+
}
|
|
854
|
+
};
|
|
819
855
|
const walk = async (dir, relPrefix) => {
|
|
820
856
|
if (results.length >= limit) {
|
|
821
857
|
truncated = true;
|
|
@@ -827,6 +863,8 @@ var globTool = {
|
|
|
827
863
|
} catch {
|
|
828
864
|
return;
|
|
829
865
|
}
|
|
866
|
+
const subdirs = [];
|
|
867
|
+
const matchedFiles = [];
|
|
830
868
|
for (const e of entries) {
|
|
831
869
|
const name = e.name;
|
|
832
870
|
if (DEFAULT_IGNORE2.includes(name)) continue;
|
|
@@ -834,22 +872,36 @@ var globTool = {
|
|
|
834
872
|
const rel = relPrefix ? `${relPrefix}/${name}` : name;
|
|
835
873
|
const full = path.join(dir, name);
|
|
836
874
|
if (e.isDirectory()) {
|
|
837
|
-
|
|
838
|
-
if (truncated) return;
|
|
875
|
+
subdirs.push({ full, rel });
|
|
839
876
|
} else if (e.isFile()) {
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
877
|
+
re.lastIndex = 0;
|
|
878
|
+
const relMatch = re.test(rel);
|
|
879
|
+
re.lastIndex = 0;
|
|
880
|
+
const nameMatch = re.test(name);
|
|
881
|
+
if (relMatch || nameMatch) {
|
|
882
|
+
matchedFiles.push(full);
|
|
883
|
+
}
|
|
884
|
+
} else if (e.isSymbolicLink()) {
|
|
885
|
+
try {
|
|
886
|
+
const st = await fs7.stat(full);
|
|
887
|
+
if (st.isDirectory()) {
|
|
888
|
+
subdirs.push({ full, rel });
|
|
889
|
+
} else if (st.isFile()) {
|
|
890
|
+
re.lastIndex = 0;
|
|
891
|
+
const relMatch = re.test(rel);
|
|
892
|
+
re.lastIndex = 0;
|
|
893
|
+
const nameMatch = re.test(name);
|
|
894
|
+
if (relMatch || nameMatch) matchedFiles.push(full);
|
|
849
895
|
}
|
|
896
|
+
} catch {
|
|
850
897
|
}
|
|
851
898
|
}
|
|
899
|
+
if (truncated) return;
|
|
852
900
|
}
|
|
901
|
+
await mapWithConcurrency(matchedFiles, WALK_CONCURRENCY, pushResult);
|
|
902
|
+
if (truncated) return;
|
|
903
|
+
const remainingSubdirs = truncated ? [] : subdirs;
|
|
904
|
+
await mapWithConcurrency(remainingSubdirs, WALK_CONCURRENCY, ({ full, rel }) => walk(full, rel));
|
|
853
905
|
};
|
|
854
906
|
await walk(base, "");
|
|
855
907
|
results.sort((a, b) => b.mtime - a.mtime);
|
|
@@ -866,6 +918,8 @@ async function readGitignore(dir) {
|
|
|
866
918
|
}
|
|
867
919
|
var DEFAULT_IGNORE3 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
|
|
868
920
|
var NATIVE_SCAN_CONCURRENCY = 32;
|
|
921
|
+
var NATIVE_READ_CHUNK_BYTES = 64 * 1024;
|
|
922
|
+
var NATIVE_MAX_FILE_BYTES = 1e6;
|
|
869
923
|
var grepTool = {
|
|
870
924
|
name: "grep",
|
|
871
925
|
category: "Search",
|
|
@@ -1090,7 +1144,8 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
1090
1144
|
const re = compiled.regex;
|
|
1091
1145
|
const globRe = input.glob ? compileGlob(input.glob) : null;
|
|
1092
1146
|
const matches = [];
|
|
1093
|
-
const
|
|
1147
|
+
const countOnlyFirstHit = mode === "count" && limit === 1;
|
|
1148
|
+
const maxBytes = mode === "content" ? NATIVE_MAX_FILE_BYTES : Math.min(NATIVE_MAX_FILE_BYTES, 256 * 1024);
|
|
1094
1149
|
let total = 0;
|
|
1095
1150
|
let stopped = false;
|
|
1096
1151
|
const scanFile = async (full, name) => {
|
|
@@ -1099,34 +1154,79 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
1099
1154
|
if (globRe) globRe.lastIndex = 0;
|
|
1100
1155
|
try {
|
|
1101
1156
|
const stat11 = await fs7.stat(full);
|
|
1102
|
-
if (stat11.size >
|
|
1103
|
-
const
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1157
|
+
if (!stat11.isFile() || stat11.size > maxBytes || stopped || signal.aborted) return;
|
|
1158
|
+
const file = await fs7.open(full, "r");
|
|
1159
|
+
try {
|
|
1160
|
+
let bytesReadTotal = 0;
|
|
1161
|
+
let lineNumber = 0;
|
|
1162
|
+
let fileHits = 0;
|
|
1163
|
+
let leftover = "";
|
|
1164
|
+
let binaryChecked = false;
|
|
1165
|
+
const buffer = Buffer.allocUnsafe(Math.min(NATIVE_READ_CHUNK_BYTES, maxBytes));
|
|
1166
|
+
while (!stopped && !signal.aborted && bytesReadTotal < maxBytes) {
|
|
1167
|
+
const remaining = maxBytes - bytesReadTotal;
|
|
1168
|
+
const { bytesRead } = await file.read(buffer, 0, Math.min(buffer.length, remaining), null);
|
|
1169
|
+
if (bytesRead === 0) break;
|
|
1170
|
+
const chunk = buffer.subarray(0, bytesRead);
|
|
1171
|
+
if (!binaryChecked) {
|
|
1172
|
+
binaryChecked = true;
|
|
1173
|
+
if (isBinaryBuffer(chunk)) return;
|
|
1174
|
+
}
|
|
1175
|
+
bytesReadTotal += bytesRead;
|
|
1176
|
+
const text = leftover + chunk.toString("utf8");
|
|
1177
|
+
const lines = text.split(/\r?\n/);
|
|
1178
|
+
leftover = lines.pop() ?? "";
|
|
1179
|
+
for (const rawLine of lines) {
|
|
1180
|
+
if (stopped || signal.aborted) break;
|
|
1181
|
+
lineNumber++;
|
|
1182
|
+
const ln = capSubject(rawLine);
|
|
1183
|
+
re.lastIndex = 0;
|
|
1184
|
+
if (!re.test(ln)) continue;
|
|
1185
|
+
fileHits++;
|
|
1186
|
+
total++;
|
|
1187
|
+
if (mode === "content") {
|
|
1188
|
+
if (matches.length < limit) matches.push(`${full}:${lineNumber}:${ln}`);
|
|
1189
|
+
} else if (mode === "files_with_matches") {
|
|
1190
|
+
if (fileHits === 1 && matches.length < limit) matches.push(full);
|
|
1191
|
+
break;
|
|
1192
|
+
} else if (fileHits === 1 && matches.length < limit) {
|
|
1193
|
+
matches.push(`${full}:${countOnlyFirstHit ? 1 : 0}`);
|
|
1194
|
+
}
|
|
1195
|
+
if (countOnlyFirstHit || mode !== "content" && matches.length >= limit) {
|
|
1196
|
+
stopped = true;
|
|
1197
|
+
break;
|
|
1198
|
+
}
|
|
1117
1199
|
}
|
|
1200
|
+
if (mode === "files_with_matches" && fileHits > 0) break;
|
|
1118
1201
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1202
|
+
if (!stopped && !signal.aborted && leftover.length > 0) {
|
|
1203
|
+
lineNumber++;
|
|
1204
|
+
const ln = capSubject(leftover);
|
|
1205
|
+
re.lastIndex = 0;
|
|
1206
|
+
if (re.test(ln)) {
|
|
1207
|
+
fileHits++;
|
|
1208
|
+
total++;
|
|
1209
|
+
if (mode === "content") {
|
|
1210
|
+
if (matches.length < limit) matches.push(`${full}:${lineNumber}:${ln}`);
|
|
1211
|
+
} else if (mode === "files_with_matches") {
|
|
1212
|
+
if (matches.length < limit) matches.push(full);
|
|
1213
|
+
} else if (matches.length < limit) {
|
|
1214
|
+
matches.push(`${full}:${countOnlyFirstHit ? 1 : fileHits}`);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1124
1217
|
}
|
|
1125
|
-
if (
|
|
1126
|
-
|
|
1218
|
+
if (fileHits > 0) {
|
|
1219
|
+
if (mode === "count") {
|
|
1220
|
+
const idx = matches.findIndex((entry) => entry.startsWith(`${full}:`));
|
|
1221
|
+
if (idx !== -1) matches[idx] = `${full}:${fileHits}`;
|
|
1222
|
+
}
|
|
1223
|
+
if (mode === "files_with_matches" && matches.length >= limit) stopped = true;
|
|
1127
1224
|
}
|
|
1225
|
+
if (mode === "content" && matches.length >= limit) stopped = true;
|
|
1226
|
+
if (mode === "count" && matches.length >= limit && (countOnlyFirstHit || mode !== "count")) stopped = true;
|
|
1227
|
+
} finally {
|
|
1228
|
+
await file.close();
|
|
1128
1229
|
}
|
|
1129
|
-
if (matches.length >= limit) stopped = true;
|
|
1130
1230
|
} catch {
|
|
1131
1231
|
}
|
|
1132
1232
|
};
|
|
@@ -1139,18 +1239,20 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
1139
1239
|
return;
|
|
1140
1240
|
}
|
|
1141
1241
|
const files = [];
|
|
1242
|
+
const subdirs = [];
|
|
1142
1243
|
for (const e of entries) {
|
|
1143
1244
|
if (stopped) return;
|
|
1144
1245
|
if (DEFAULT_IGNORE3.includes(e.name)) continue;
|
|
1145
1246
|
if (e.isSymbolicLink()) continue;
|
|
1146
1247
|
const full = path.join(dir, e.name);
|
|
1147
1248
|
if (e.isDirectory()) {
|
|
1148
|
-
|
|
1249
|
+
subdirs.push(full);
|
|
1149
1250
|
} else if (e.isFile()) {
|
|
1150
1251
|
files.push({ full, name: e.name });
|
|
1151
1252
|
}
|
|
1152
1253
|
}
|
|
1153
1254
|
await mapWithConcurrency(files, NATIVE_SCAN_CONCURRENCY, ({ full, name }) => scanFile(full, name));
|
|
1255
|
+
await mapWithConcurrency(subdirs, Math.min(16, NATIVE_SCAN_CONCURRENCY), walk);
|
|
1154
1256
|
};
|
|
1155
1257
|
await walk(base);
|
|
1156
1258
|
return {
|
|
@@ -1160,20 +1262,6 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
1160
1262
|
used: "native"
|
|
1161
1263
|
};
|
|
1162
1264
|
}
|
|
1163
|
-
async function mapWithConcurrency(items, concurrency, fn) {
|
|
1164
|
-
if (items.length === 0) return;
|
|
1165
|
-
let next = 0;
|
|
1166
|
-
const workerCount = Math.min(Math.max(1, concurrency), items.length);
|
|
1167
|
-
const workers = Array.from({ length: workerCount }, async () => {
|
|
1168
|
-
for (; ; ) {
|
|
1169
|
-
const idx = next++;
|
|
1170
|
-
if (idx >= items.length) return;
|
|
1171
|
-
const item = items[idx];
|
|
1172
|
-
if (item !== void 0) await fn(item);
|
|
1173
|
-
}
|
|
1174
|
-
});
|
|
1175
|
-
await Promise.all(workers);
|
|
1176
|
-
}
|
|
1177
1265
|
var SPOOL_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1178
1266
|
var SPOOL_WRITE_HWM_BYTES = 4 * 1024 * 1024;
|
|
1179
1267
|
var sweepStarted = false;
|
|
@@ -1215,7 +1303,7 @@ function createOutputSpool(opts) {
|
|
|
1215
1303
|
let filePath = null;
|
|
1216
1304
|
let failed = false;
|
|
1217
1305
|
let finalized = false;
|
|
1218
|
-
const
|
|
1306
|
+
const open2 = () => {
|
|
1219
1307
|
if (stream || failed) return;
|
|
1220
1308
|
try {
|
|
1221
1309
|
const dir = toolOutputDir();
|
|
@@ -1248,7 +1336,7 @@ function createOutputSpool(opts) {
|
|
|
1248
1336
|
return;
|
|
1249
1337
|
}
|
|
1250
1338
|
head += text;
|
|
1251
|
-
|
|
1339
|
+
open2();
|
|
1252
1340
|
head = "";
|
|
1253
1341
|
return;
|
|
1254
1342
|
}
|
|
@@ -1434,6 +1522,7 @@ var CircuitBreaker = class {
|
|
|
1434
1522
|
if (this.state === "open") return;
|
|
1435
1523
|
this.state = "open";
|
|
1436
1524
|
this.openedAt = Date.now();
|
|
1525
|
+
this.window = [];
|
|
1437
1526
|
try {
|
|
1438
1527
|
this.onTrip?.();
|
|
1439
1528
|
} catch {
|
|
@@ -1473,7 +1562,7 @@ var SENSITIVE_FLAG_PATTERNS = [
|
|
|
1473
1562
|
/--(?:token|password|passwd|pwd|secret|api[-_]?key|api[-_]?secret|auth|credential|private[-_]?key|access[-_]?key|github[-_]?token|gh[-_]?token|bearer|jwt|oauth|pin|pincode|passphrase|access[-_]?token)(?:[=\s,][^\s]*)?/gi,
|
|
1474
1563
|
// -f "value" style short flags
|
|
1475
1564
|
/(?<!\w)-t(?:\s+|\s*=\s*)[^\s,]+/,
|
|
1476
|
-
/(?<!\w)-
|
|
1565
|
+
/(?<!\w)-(?:p|password)(?:\s+|\s*=\s*)[^\s,]+/gi,
|
|
1477
1566
|
// env var–style secrets: TOKEN=x, API_KEY=y, etc.
|
|
1478
1567
|
/(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
|
|
1479
1568
|
// Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
|
|
@@ -1534,12 +1623,35 @@ var ProcessRegistryImpl = class {
|
|
|
1534
1623
|
register(info) {
|
|
1535
1624
|
this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
|
|
1536
1625
|
}
|
|
1626
|
+
_isSafeSignalPid(pid) {
|
|
1627
|
+
return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
|
|
1628
|
+
}
|
|
1629
|
+
_canSignalProcessGroup(p) {
|
|
1630
|
+
return os2.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && typeof p.child.pid === "number" && p.child.pid === p.pid;
|
|
1631
|
+
}
|
|
1632
|
+
_killChildDirect(p, signal) {
|
|
1633
|
+
try {
|
|
1634
|
+
p.child.kill(signal);
|
|
1635
|
+
} catch {
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
_killPosix(p, signal) {
|
|
1639
|
+
if (this._canSignalProcessGroup(p)) {
|
|
1640
|
+
try {
|
|
1641
|
+
process.kill(-p.pid, signal);
|
|
1642
|
+
return;
|
|
1643
|
+
} catch {
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
this._killChildDirect(p, signal);
|
|
1647
|
+
}
|
|
1537
1648
|
/** Unregister a process by PID. Called on 'close' / 'exit' events. */
|
|
1538
1649
|
unregister(pid) {
|
|
1539
1650
|
this.processes.delete(pid);
|
|
1540
1651
|
}
|
|
1541
1652
|
/** Get a single process by PID. */
|
|
1542
1653
|
get(pid) {
|
|
1654
|
+
this._pruneStale(pid);
|
|
1543
1655
|
return this.processes.get(pid);
|
|
1544
1656
|
}
|
|
1545
1657
|
/** Get all tracked processes. */
|
|
@@ -1703,13 +1815,14 @@ var ProcessRegistryImpl = class {
|
|
|
1703
1815
|
* Returns true if the process was found and kill was attempted.
|
|
1704
1816
|
*/
|
|
1705
1817
|
kill(pid, opts = {}) {
|
|
1818
|
+
this._pruneStale(pid);
|
|
1706
1819
|
const p = this.processes.get(pid);
|
|
1707
1820
|
if (!p) return false;
|
|
1708
1821
|
if (p.killed) return true;
|
|
1709
1822
|
if (p.protected) return false;
|
|
1710
1823
|
const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
|
|
1711
|
-
const
|
|
1712
|
-
if (
|
|
1824
|
+
const isWin4 = os2.platform() === "win32";
|
|
1825
|
+
if (isWin4) {
|
|
1713
1826
|
const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
|
|
1714
1827
|
if (liveRealChild && killWin32Tree(pid)) {
|
|
1715
1828
|
const fallback = setTimeout(() => {
|
|
@@ -1732,27 +1845,12 @@ var ProcessRegistryImpl = class {
|
|
|
1732
1845
|
}
|
|
1733
1846
|
try {
|
|
1734
1847
|
if (force) {
|
|
1735
|
-
|
|
1736
|
-
process.kill(-pid, "SIGKILL");
|
|
1737
|
-
} catch {
|
|
1738
|
-
p.child.kill("SIGKILL");
|
|
1739
|
-
}
|
|
1848
|
+
this._killPosix(p, "SIGKILL");
|
|
1740
1849
|
} else {
|
|
1741
|
-
|
|
1742
|
-
process.kill(-pid, "SIGTERM");
|
|
1743
|
-
} catch {
|
|
1744
|
-
p.child.kill("SIGTERM");
|
|
1745
|
-
}
|
|
1850
|
+
this._killPosix(p, "SIGTERM");
|
|
1746
1851
|
const timer = setTimeout(() => {
|
|
1747
1852
|
if (this.processes.has(pid) && !p.child.killed) {
|
|
1748
|
-
|
|
1749
|
-
process.kill(-pid, "SIGKILL");
|
|
1750
|
-
} catch {
|
|
1751
|
-
try {
|
|
1752
|
-
p.child.kill("SIGKILL");
|
|
1753
|
-
} catch {
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1853
|
+
this._killPosix(p, "SIGKILL");
|
|
1756
1854
|
}
|
|
1757
1855
|
}, graceMs);
|
|
1758
1856
|
timer.unref?.();
|
|
@@ -1787,6 +1885,34 @@ var ProcessRegistryImpl = class {
|
|
|
1787
1885
|
}
|
|
1788
1886
|
return killed;
|
|
1789
1887
|
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Check whether a tracked process entry is stale — the child has exited
|
|
1890
|
+
* (exitCode !== null) AND it's been in the registry long enough that the
|
|
1891
|
+
* OS may have reused the PID for a new, unrelated process.
|
|
1892
|
+
*
|
|
1893
|
+
* P3 #24 (before-release.md): on POSIX, PIDs are reused after process
|
|
1894
|
+
* exit. If a tracked process exits but its 'close' event hasn't fired yet
|
|
1895
|
+
* (or was missed), the registry still holds the entry. A new process
|
|
1896
|
+
* gets the same PID, and PID-based lookups (get, kill, shouldBlockKill)
|
|
1897
|
+
* may incorrectly protect or target the wrong process.
|
|
1898
|
+
*
|
|
1899
|
+
* The 60s threshold is conservative — the OS typically waits much longer
|
|
1900
|
+
* before reusing a PID, but we want to clean up before that becomes a risk.
|
|
1901
|
+
*/
|
|
1902
|
+
_isStaleEntry(entry) {
|
|
1903
|
+
return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
|
|
1904
|
+
}
|
|
1905
|
+
/**
|
|
1906
|
+
* Remove a stale entry for a specific PID before any PID-based lookup.
|
|
1907
|
+
* This prevents PID reuse from causing the registry to act on a dead
|
|
1908
|
+
* process that has been replaced by a new one with the same PID.
|
|
1909
|
+
*/
|
|
1910
|
+
_pruneStale(pid) {
|
|
1911
|
+
const entry = this.processes.get(pid);
|
|
1912
|
+
if (entry && this._isStaleEntry(entry)) {
|
|
1913
|
+
this.processes.delete(pid);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1790
1916
|
};
|
|
1791
1917
|
var _registry;
|
|
1792
1918
|
function getProcessRegistry() {
|
|
@@ -2211,17 +2337,18 @@ function resetPersistentProcessRegistry() {
|
|
|
2211
2337
|
}
|
|
2212
2338
|
|
|
2213
2339
|
// src/bash-kill-guard.ts
|
|
2340
|
+
var isWin = os2.platform() === "win32";
|
|
2214
2341
|
function extractKillCommand(command) {
|
|
2215
2342
|
const normalized = command.replace(/\s+/g, " ").trim();
|
|
2216
2343
|
const shellCMatch = normalized.match(
|
|
2217
|
-
/^(
|
|
2344
|
+
/^(?:\S+(?:\s+\S+)?)?\s+-c\s+['"](.+?)['"]$/
|
|
2218
2345
|
);
|
|
2219
2346
|
if (shellCMatch?.[1]) {
|
|
2220
2347
|
const inner = shellCMatch[1].trim();
|
|
2221
2348
|
return isKillRelatedCommand(inner) ? inner : null;
|
|
2222
2349
|
}
|
|
2223
2350
|
const shellCUnquoted = normalized.match(
|
|
2224
|
-
/^(
|
|
2351
|
+
/^(?:\S+(?:\s+\S+)?)?\s+-c\s+(kill(?:\s+-[a-zA-Z]+)?(?:\s+\d+)+)$/
|
|
2225
2352
|
);
|
|
2226
2353
|
if (shellCUnquoted?.[1]) {
|
|
2227
2354
|
return shellCUnquoted[1];
|
|
@@ -2230,15 +2357,43 @@ function extractKillCommand(command) {
|
|
|
2230
2357
|
}
|
|
2231
2358
|
function isKillRelatedCommand(cmd) {
|
|
2232
2359
|
const normalized = cmd.toLowerCase().replace(/\s+/g, " ").trim();
|
|
2360
|
+
if (isWin) {
|
|
2361
|
+
if (/^taskkill\s/i.test(normalized)) return true;
|
|
2362
|
+
if (/^tskill\s/i.test(normalized)) return true;
|
|
2363
|
+
return false;
|
|
2364
|
+
}
|
|
2233
2365
|
if (/^kill(\s|$)/.test(normalized)) return true;
|
|
2234
2366
|
if (/^(pkill|killall|pgrep|skill)\s/.test(normalized)) return true;
|
|
2235
|
-
if (/^taskkill\s/i.test(normalized)) return true;
|
|
2236
|
-
if (/^tskill\s/i.test(normalized)) return true;
|
|
2237
2367
|
if (/^\/proc\/\d+\/(?:kill|fd)/.test(normalized)) return true;
|
|
2238
2368
|
return false;
|
|
2239
2369
|
}
|
|
2240
2370
|
function parseKillCommand(command) {
|
|
2241
2371
|
const normalized = command.replace(/\s+/g, " ").trim();
|
|
2372
|
+
if (isWin) {
|
|
2373
|
+
const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
|
|
2374
|
+
if (taskkillMatch?.[1]) {
|
|
2375
|
+
const pidStr = taskkillMatch[1];
|
|
2376
|
+
return {
|
|
2377
|
+
pid: parseInt(pidStr, 10),
|
|
2378
|
+
signal: normalized.includes("/F") ? "FORCE" : "TERM",
|
|
2379
|
+
isGroupKill: false,
|
|
2380
|
+
isAllKill: false,
|
|
2381
|
+
originalCommand: command
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
2384
|
+
const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
|
|
2385
|
+
if (tskillMatch?.[1]) {
|
|
2386
|
+
const pidStr = tskillMatch[1];
|
|
2387
|
+
return {
|
|
2388
|
+
pid: parseInt(pidStr, 10),
|
|
2389
|
+
signal: "TERM",
|
|
2390
|
+
isGroupKill: false,
|
|
2391
|
+
isAllKill: false,
|
|
2392
|
+
originalCommand: command
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
return null;
|
|
2396
|
+
}
|
|
2242
2397
|
const simpleMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z]+)\s+)?(\d+|-?\d+)$/);
|
|
2243
2398
|
if (simpleMatch) {
|
|
2244
2399
|
const signal = simpleMatch[1] ?? "-TERM";
|
|
@@ -2282,28 +2437,6 @@ function parseKillCommand(command) {
|
|
|
2282
2437
|
if (pgrepMatch) {
|
|
2283
2438
|
return null;
|
|
2284
2439
|
}
|
|
2285
|
-
const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
|
|
2286
|
-
if (taskkillMatch?.[1]) {
|
|
2287
|
-
const pidStr = taskkillMatch[1];
|
|
2288
|
-
return {
|
|
2289
|
-
pid: parseInt(pidStr, 10),
|
|
2290
|
-
signal: normalized.includes("/F") ? "FORCE" : "TERM",
|
|
2291
|
-
isGroupKill: false,
|
|
2292
|
-
isAllKill: false,
|
|
2293
|
-
originalCommand: command
|
|
2294
|
-
};
|
|
2295
|
-
}
|
|
2296
|
-
const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
|
|
2297
|
-
if (tskillMatch?.[1]) {
|
|
2298
|
-
const pidStr = tskillMatch[1];
|
|
2299
|
-
return {
|
|
2300
|
-
pid: parseInt(pidStr, 10),
|
|
2301
|
-
signal: "TERM",
|
|
2302
|
-
isGroupKill: false,
|
|
2303
|
-
isAllKill: false,
|
|
2304
|
-
originalCommand: command
|
|
2305
|
-
};
|
|
2306
|
-
}
|
|
2307
2440
|
return null;
|
|
2308
2441
|
}
|
|
2309
2442
|
async function getProtectedEntries() {
|
|
@@ -2380,7 +2513,7 @@ async function checkAndBlockKillCommand(command) {
|
|
|
2380
2513
|
}
|
|
2381
2514
|
return { blocked: false };
|
|
2382
2515
|
}
|
|
2383
|
-
function pickShell(
|
|
2516
|
+
function pickShell(platform5, command, env) {
|
|
2384
2517
|
const override = env.get("WRONGSTACK_SHELL")?.trim().toLowerCase();
|
|
2385
2518
|
if (override === "cmd" || override === "cmd.exe") return "cmd";
|
|
2386
2519
|
if (override === "powershell" || override === "powershell.exe") return "powershell";
|
|
@@ -2434,7 +2567,7 @@ function looksLikePowerShellExtended(command) {
|
|
|
2434
2567
|
}
|
|
2435
2568
|
function wrapPowerShellScript(command) {
|
|
2436
2569
|
const bootstrap = "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8;$ConfirmPreference='None';$WhatIfPreference=$false";
|
|
2437
|
-
return
|
|
2570
|
+
return bootstrap + "\n$ErrorActionPreference='Stop'\n" + command + "\nif ($LASTEXITCODE -is [int]) { exit $LASTEXITCODE }";
|
|
2438
2571
|
}
|
|
2439
2572
|
var PS_VERB_RE = new RegExp(
|
|
2440
2573
|
// Boundaries: start-of-string, whitespace, `;`, `&`, `|`, `(`, `{`, `,`.
|
|
@@ -2624,10 +2757,10 @@ var bashTool = {
|
|
|
2624
2757
|
}));
|
|
2625
2758
|
}
|
|
2626
2759
|
const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
|
|
2627
|
-
const
|
|
2760
|
+
const isWin4 = os2.platform() === "win32";
|
|
2628
2761
|
let plan;
|
|
2629
2762
|
let winShellKind;
|
|
2630
|
-
if (
|
|
2763
|
+
if (isWin4) {
|
|
2631
2764
|
const shell2 = pickShell("win32", input.command, {
|
|
2632
2765
|
get: (k) => process.env[k]
|
|
2633
2766
|
});
|
|
@@ -2656,7 +2789,7 @@ var bashTool = {
|
|
|
2656
2789
|
const shell = plan.bin;
|
|
2657
2790
|
const args = plan.useStdin ? [...plan.argv] : [...plan.argv, input.command];
|
|
2658
2791
|
const env = buildChildEnv(ctx.session?.id);
|
|
2659
|
-
const detached = !
|
|
2792
|
+
const detached = !isWin4;
|
|
2660
2793
|
const startedAt = Date.now();
|
|
2661
2794
|
if (input.background) {
|
|
2662
2795
|
let buf2 = "";
|
|
@@ -2674,7 +2807,7 @@ var bashTool = {
|
|
|
2674
2807
|
// apply: the child gets a hidden console that grandchildren inherit.
|
|
2675
2808
|
// Windows children survive parent exit either way. POSIX keeps
|
|
2676
2809
|
// detached for the process-group kill semantics.
|
|
2677
|
-
detached: !
|
|
2810
|
+
detached: !isWin4,
|
|
2678
2811
|
windowsHide: true
|
|
2679
2812
|
});
|
|
2680
2813
|
if (plan.useStdin) {
|
|
@@ -2692,7 +2825,8 @@ var bashTool = {
|
|
|
2692
2825
|
command: redactCommand(input.command),
|
|
2693
2826
|
startedAt: Date.now(),
|
|
2694
2827
|
sessionId: ctx.session?.id,
|
|
2695
|
-
child: child2
|
|
2828
|
+
child: child2,
|
|
2829
|
+
processGroupLeader: detached && child2.pid === pid2
|
|
2696
2830
|
});
|
|
2697
2831
|
child2.on("close", () => registry.unregister(pid2));
|
|
2698
2832
|
}
|
|
@@ -2733,6 +2867,14 @@ var bashTool = {
|
|
|
2733
2867
|
pid: pid2
|
|
2734
2868
|
}
|
|
2735
2869
|
};
|
|
2870
|
+
ctx.recordSideEffect?.({
|
|
2871
|
+
toolUseId: `bash-bg-${Date.now()}`,
|
|
2872
|
+
toolName: "bash",
|
|
2873
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2874
|
+
input: { command: redactCommand(input.command), background: true },
|
|
2875
|
+
outcome: `launched (pid ${pid2 ?? "unknown"})`,
|
|
2876
|
+
risk: "shell"
|
|
2877
|
+
});
|
|
2736
2878
|
return;
|
|
2737
2879
|
}
|
|
2738
2880
|
const child = spawn(shell, args, {
|
|
@@ -2743,7 +2885,7 @@ var bashTool = {
|
|
|
2743
2885
|
stdio: [plan.useStdin ? "pipe" : "ignore", "pipe", "pipe"],
|
|
2744
2886
|
detached,
|
|
2745
2887
|
windowsHide: true,
|
|
2746
|
-
...
|
|
2888
|
+
...isWin4 ? {} : { signal: opts.signal }
|
|
2747
2889
|
});
|
|
2748
2890
|
if (plan.useStdin) {
|
|
2749
2891
|
try {
|
|
@@ -2760,7 +2902,8 @@ var bashTool = {
|
|
|
2760
2902
|
command: redactCommand(input.command),
|
|
2761
2903
|
startedAt: Date.now(),
|
|
2762
2904
|
sessionId: ctx.session?.id,
|
|
2763
|
-
child
|
|
2905
|
+
child,
|
|
2906
|
+
processGroupLeader: detached && child.pid === pid
|
|
2764
2907
|
});
|
|
2765
2908
|
}
|
|
2766
2909
|
let buf = "";
|
|
@@ -2769,7 +2912,7 @@ var bashTool = {
|
|
|
2769
2912
|
const timers = [];
|
|
2770
2913
|
const spool = createOutputSpool({ tool: "bash", thresholdBytes: MAX_OUTPUT });
|
|
2771
2914
|
function killWithTimeout(child2, timeoutMs2) {
|
|
2772
|
-
if (
|
|
2915
|
+
if (isWin4) {
|
|
2773
2916
|
if (typeof child2.pid === "number" && child2.exitCode === null && killWin32Tree(child2.pid)) {
|
|
2774
2917
|
const fallback = setTimeout(() => {
|
|
2775
2918
|
if (child2.exitCode === null) {
|
|
@@ -2789,34 +2932,22 @@ var bashTool = {
|
|
|
2789
2932
|
}
|
|
2790
2933
|
return;
|
|
2791
2934
|
}
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
} catch {
|
|
2797
|
-
child2.kill("SIGTERM");
|
|
2798
|
-
}
|
|
2799
|
-
} else {
|
|
2935
|
+
if (typeof child2.pid === "number") {
|
|
2936
|
+
registry.kill(child2.pid, { graceMs: timeoutMs2 });
|
|
2937
|
+
} else {
|
|
2938
|
+
try {
|
|
2800
2939
|
child2.kill("SIGTERM");
|
|
2940
|
+
} catch {
|
|
2801
2941
|
}
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
const killTimer = setTimeout(() => {
|
|
2805
|
-
try {
|
|
2806
|
-
if (typeof child2.pid === "number") {
|
|
2807
|
-
try {
|
|
2808
|
-
process.kill(-child2.pid, "SIGKILL");
|
|
2809
|
-
} catch {
|
|
2810
|
-
child2.kill("SIGKILL");
|
|
2811
|
-
}
|
|
2812
|
-
} else {
|
|
2942
|
+
const killTimer = setTimeout(() => {
|
|
2943
|
+
try {
|
|
2813
2944
|
child2.kill("SIGKILL");
|
|
2945
|
+
} catch {
|
|
2814
2946
|
}
|
|
2815
|
-
}
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
killTimer.unref?.();
|
|
2947
|
+
}, timeoutMs2);
|
|
2948
|
+
timers.push(killTimer);
|
|
2949
|
+
killTimer.unref?.();
|
|
2950
|
+
}
|
|
2820
2951
|
}
|
|
2821
2952
|
const timer = setTimeout(() => {
|
|
2822
2953
|
timedOut = true;
|
|
@@ -2825,7 +2956,7 @@ var bashTool = {
|
|
|
2825
2956
|
timers.push(timer);
|
|
2826
2957
|
timer.unref?.();
|
|
2827
2958
|
const onAbort = () => killWithTimeout(child, 2e3);
|
|
2828
|
-
if (
|
|
2959
|
+
if (isWin4) {
|
|
2829
2960
|
if (opts.signal.aborted) onAbort();
|
|
2830
2961
|
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
2831
2962
|
}
|
|
@@ -2913,6 +3044,14 @@ ${hint}` : ""),
|
|
|
2913
3044
|
timed_out: timedOut
|
|
2914
3045
|
}
|
|
2915
3046
|
};
|
|
3047
|
+
ctx.recordSideEffect?.({
|
|
3048
|
+
toolUseId: `bash-${Date.now()}`,
|
|
3049
|
+
toolName: "bash",
|
|
3050
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3051
|
+
input: { command: redactCommand(input.command) },
|
|
3052
|
+
outcome: timedOut ? `timed out (exit ${c.code})` : `exit ${c.code}`,
|
|
3053
|
+
risk: "shell"
|
|
3054
|
+
});
|
|
2916
3055
|
return;
|
|
2917
3056
|
}
|
|
2918
3057
|
const now2 = Date.now();
|
|
@@ -2924,7 +3063,7 @@ ${hint}` : ""),
|
|
|
2924
3063
|
} finally {
|
|
2925
3064
|
for (const t of timers) clearTimeout(t);
|
|
2926
3065
|
spool.finalize();
|
|
2927
|
-
if (
|
|
3066
|
+
if (isWin4) opts.signal.removeEventListener("abort", onAbort);
|
|
2928
3067
|
child.stdout?.off("data", onData);
|
|
2929
3068
|
child.stderr?.off("data", onData);
|
|
2930
3069
|
child.stdout?.destroy();
|
|
@@ -2934,6 +3073,38 @@ ${hint}` : ""),
|
|
|
2934
3073
|
else killWithTimeout(child, 2e3);
|
|
2935
3074
|
}
|
|
2936
3075
|
}
|
|
3076
|
+
},
|
|
3077
|
+
/**
|
|
3078
|
+
* Tool-level teardown fired by `ToolExecutor.runToolCleanup()` when the
|
|
3079
|
+
* tool's run is aborted/timeout'd. The generator's `finally` block above
|
|
3080
|
+
* already force-kills the direct child, but that only runs if the
|
|
3081
|
+
* executor closes the async iterator (via `iter.return()`). When the
|
|
3082
|
+
* executor tears down without iterating — or a re-entrant abort races
|
|
3083
|
+
* with the generator — a bash-spawned process tree can survive in the
|
|
3084
|
+
* ProcessRegistry with `killed === false`, continuing to write files,
|
|
3085
|
+
* consume CPU, or hold inherited stdio pipes open for the rest of the
|
|
3086
|
+
* session.
|
|
3087
|
+
*
|
|
3088
|
+
* This is the defensive layer the executor calls via `tool.cleanup()`
|
|
3089
|
+
* (see `types/tool.ts`): kill every bash-owned process still tracked
|
|
3090
|
+
* for this session that hasn't exited yet. `registry.kill()` already
|
|
3091
|
+
* handles process-group / taskkill tree-kill and the SIGTERM→SIGKILL
|
|
3092
|
+
* grace window, so this just scopes the registry's existing kill path
|
|
3093
|
+
* to "this session's runaway bash children". Idempotent — a process
|
|
3094
|
+
* that already exited is skipped by `kill()` (it returns false), and a
|
|
3095
|
+
* `protected` infrastructure process (dev server the user intentionally
|
|
3096
|
+
* backgrounded) is left alone by design.
|
|
3097
|
+
*/
|
|
3098
|
+
async cleanup(_input, ctx) {
|
|
3099
|
+
const registry = getProcessRegistry();
|
|
3100
|
+
const sessionId = ctx.session?.id;
|
|
3101
|
+
if (!sessionId) return;
|
|
3102
|
+
for (const entry of registry.bySession(sessionId)) {
|
|
3103
|
+
if (entry.name !== "bash") continue;
|
|
3104
|
+
if (entry.child.exitCode !== null) continue;
|
|
3105
|
+
if (entry.protected) continue;
|
|
3106
|
+
registry.kill(entry.pid, { force: true });
|
|
3107
|
+
}
|
|
2937
3108
|
}
|
|
2938
3109
|
};
|
|
2939
3110
|
|
|
@@ -2945,8 +3116,8 @@ function normalizeShell(value) {
|
|
|
2945
3116
|
if (v === "pwsh" || v === "pwsh.exe") return "pwsh";
|
|
2946
3117
|
return void 0;
|
|
2947
3118
|
}
|
|
2948
|
-
function resolveSessionShell(
|
|
2949
|
-
if (
|
|
3119
|
+
function resolveSessionShell(platform5, env, deps = {}) {
|
|
3120
|
+
if (platform5 !== "win32") return void 0;
|
|
2950
3121
|
const override = normalizeShell(env.get("WRONGSTACK_SHELL"));
|
|
2951
3122
|
if (override) return override;
|
|
2952
3123
|
const hasBinary = deps.hasBinary ?? ((bin) => resolveWin32Command(bin) !== bin);
|
|
@@ -2956,15 +3127,15 @@ function resolveSessionShell(platform4, env, deps = {}) {
|
|
|
2956
3127
|
}
|
|
2957
3128
|
function ensureSessionShell(opts = {}) {
|
|
2958
3129
|
const env = opts.env ?? process.env;
|
|
2959
|
-
const
|
|
2960
|
-
if (
|
|
3130
|
+
const platform5 = opts.platform ?? process.platform;
|
|
3131
|
+
if (platform5 !== "win32") return void 0;
|
|
2961
3132
|
const existing = normalizeShell(env["WRONGSTACK_SHELL"]);
|
|
2962
3133
|
if (existing) return existing;
|
|
2963
|
-
const chosen = resolveSessionShell(
|
|
3134
|
+
const chosen = resolveSessionShell(platform5, { get: (k) => env[k] }, { hasBinary: opts.hasBinary }) ?? "cmd";
|
|
2964
3135
|
env["WRONGSTACK_SHELL"] = chosen;
|
|
2965
3136
|
return chosen;
|
|
2966
3137
|
}
|
|
2967
|
-
var
|
|
3138
|
+
var isWin2 = process.platform === "win32";
|
|
2968
3139
|
var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
2969
3140
|
// JS / TS toolchain
|
|
2970
3141
|
"node",
|
|
@@ -2974,12 +3145,25 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
2974
3145
|
"npx",
|
|
2975
3146
|
"bun",
|
|
2976
3147
|
"deno",
|
|
3148
|
+
"corepack",
|
|
2977
3149
|
"tsc",
|
|
3150
|
+
"tsx",
|
|
3151
|
+
"ts-node",
|
|
3152
|
+
"vite",
|
|
2978
3153
|
"vitest",
|
|
2979
3154
|
"jest",
|
|
2980
3155
|
"biome",
|
|
2981
3156
|
"eslint",
|
|
2982
3157
|
"prettier",
|
|
3158
|
+
"turbo",
|
|
3159
|
+
"nx",
|
|
3160
|
+
"webpack",
|
|
3161
|
+
"rollup",
|
|
3162
|
+
"parcel",
|
|
3163
|
+
"next",
|
|
3164
|
+
"astro",
|
|
3165
|
+
"playwright",
|
|
3166
|
+
"cypress",
|
|
2983
3167
|
// version control
|
|
2984
3168
|
"git",
|
|
2985
3169
|
// Rust
|
|
@@ -2992,10 +3176,22 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
2992
3176
|
"python3",
|
|
2993
3177
|
"pip",
|
|
2994
3178
|
"pip3",
|
|
3179
|
+
"pytest",
|
|
3180
|
+
"ruff",
|
|
3181
|
+
"mypy",
|
|
3182
|
+
"uv",
|
|
3183
|
+
"uvx",
|
|
3184
|
+
"poetry",
|
|
3185
|
+
"hatch",
|
|
3186
|
+
"tox",
|
|
2995
3187
|
// Ruby
|
|
2996
3188
|
"ruby",
|
|
2997
3189
|
"gem",
|
|
2998
3190
|
"bundle",
|
|
3191
|
+
// PHP
|
|
3192
|
+
"php",
|
|
3193
|
+
"composer",
|
|
3194
|
+
"phpunit",
|
|
2999
3195
|
// JVM
|
|
3000
3196
|
"java",
|
|
3001
3197
|
"javac",
|
|
@@ -3007,18 +3203,25 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
3007
3203
|
// C / C++ / native build
|
|
3008
3204
|
"make",
|
|
3009
3205
|
"cmake",
|
|
3010
|
-
// containers / orchestration
|
|
3206
|
+
// containers / orchestration
|
|
3011
3207
|
"docker",
|
|
3208
|
+
"podman",
|
|
3012
3209
|
"kubectl",
|
|
3013
3210
|
// common POSIX file/text utilities
|
|
3211
|
+
"pwd",
|
|
3014
3212
|
"ls",
|
|
3015
3213
|
"cat",
|
|
3016
3214
|
"head",
|
|
3017
3215
|
"tail",
|
|
3018
3216
|
"wc",
|
|
3019
3217
|
"grep",
|
|
3218
|
+
"rg",
|
|
3020
3219
|
"find",
|
|
3021
3220
|
"echo",
|
|
3221
|
+
"sort",
|
|
3222
|
+
"uniq",
|
|
3223
|
+
"sed",
|
|
3224
|
+
"awk",
|
|
3022
3225
|
"mkdir",
|
|
3023
3226
|
"cp",
|
|
3024
3227
|
"mv",
|
|
@@ -3049,8 +3252,7 @@ var MAX_ARGS = 20;
|
|
|
3049
3252
|
var MAX_OUTPUT2 = 2e5;
|
|
3050
3253
|
var DEFAULT_TIMEOUT_MS2 = 3e4;
|
|
3051
3254
|
var BLOCKED_ARG_PATTERNS = {
|
|
3052
|
-
|
|
3053
|
-
python: [/-c$/, /^--command$/, /^-m$/, /^--module$/],
|
|
3255
|
+
python: [],
|
|
3054
3256
|
// git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack;
|
|
3055
3257
|
// -C <dir> changes working directory, bypassing cwd sandbox;
|
|
3056
3258
|
// -c/--config <k>=<v> injects config that runs commands
|
|
@@ -3066,15 +3268,10 @@ var BLOCKED_ARG_PATTERNS = {
|
|
|
3066
3268
|
/^--config=/,
|
|
3067
3269
|
/^--config-env=/
|
|
3068
3270
|
],
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
// bun --preload is similar to node --require
|
|
3074
|
-
bun: [/^--preload$/, /^run$/, /^bunx$/, /^create$/, /^init$/],
|
|
3075
|
-
// docker build/run can create containers with host access;
|
|
3076
|
-
// only allow read-only commands (ps, images, version)
|
|
3077
|
-
docker: [/^build$/, /^run$/, /^exec$/, /^push$/, /^pull$/],
|
|
3271
|
+
node: [],
|
|
3272
|
+
go: [],
|
|
3273
|
+
bun: [],
|
|
3274
|
+
docker: [],
|
|
3078
3275
|
// find -exec/-ok/-execdir execute arbitrary commands
|
|
3079
3276
|
find: [/^-exec$/, /^-exec;$/, /^-ok$/, /^-ok;$/, /^-execdir$/, /^-execdir;$/, /^-exec=/, /^-ok=/, /^-execdir=/],
|
|
3080
3277
|
// rm -rf / is catastrophic — block absolute paths, home, dot-dirs,
|
|
@@ -3082,15 +3279,49 @@ var BLOCKED_ARG_PATTERNS = {
|
|
|
3082
3279
|
// `rm -rf ./src/*` expands to project files; `rm -rf ../../` escapes upward;
|
|
3083
3280
|
// `rm -rf /*` targets the filesystem root. All are blocked.
|
|
3084
3281
|
rm: [/^\//, /^~\//, /^~$/, /^\.$/, /^\.\.$/, /\*$/, /\/$/, /\/\*$/, /\.\//],
|
|
3085
|
-
// npm
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
pnpm: [
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3282
|
+
// npm/pnpm subcommands are checked separately below. Matching every arg here
|
|
3283
|
+
// over-blocked normal dev flows such as `pnpm vitest run ...`.
|
|
3284
|
+
npm: [],
|
|
3285
|
+
pnpm: [],
|
|
3286
|
+
npx: []
|
|
3287
|
+
};
|
|
3288
|
+
var BLOCKED_SUBCOMMANDS = {
|
|
3289
|
+
docker: /* @__PURE__ */ new Set(["push"]),
|
|
3290
|
+
podman: /* @__PURE__ */ new Set(["push"]),
|
|
3291
|
+
npm: /* @__PURE__ */ new Set(["publish", "deploy"]),
|
|
3292
|
+
pnpm: /* @__PURE__ */ new Set(["publish", "deploy"]),
|
|
3293
|
+
yarn: /* @__PURE__ */ new Set(["publish"])
|
|
3294
|
+
};
|
|
3295
|
+
var BLOCKED_SUBCOMMAND_SEQUENCES = {
|
|
3296
|
+
yarn: [["npm", "publish"]]
|
|
3092
3297
|
};
|
|
3298
|
+
function firstSubcommand(args) {
|
|
3299
|
+
for (const arg of args) {
|
|
3300
|
+
if (arg === "--") return null;
|
|
3301
|
+
if (!arg.startsWith("-")) return arg;
|
|
3302
|
+
}
|
|
3303
|
+
return null;
|
|
3304
|
+
}
|
|
3305
|
+
function subcommandArgs(args) {
|
|
3306
|
+
const out = [];
|
|
3307
|
+
for (const arg of args) {
|
|
3308
|
+
if (arg === "--") break;
|
|
3309
|
+
if (!arg.startsWith("-")) out.push(arg);
|
|
3310
|
+
}
|
|
3311
|
+
return out;
|
|
3312
|
+
}
|
|
3093
3313
|
function validateArgs(cmd, args) {
|
|
3314
|
+
const blockedSubcommands = BLOCKED_SUBCOMMANDS[cmd];
|
|
3315
|
+
const subcommand = firstSubcommand(args);
|
|
3316
|
+
if (blockedSubcommands && subcommand && blockedSubcommands.has(subcommand)) {
|
|
3317
|
+
return `Blocked subcommand "${subcommand}" for command "${cmd}"`;
|
|
3318
|
+
}
|
|
3319
|
+
const blockedSequences = BLOCKED_SUBCOMMAND_SEQUENCES[cmd];
|
|
3320
|
+
if (blockedSequences) {
|
|
3321
|
+
const actual = subcommandArgs(args);
|
|
3322
|
+
const blocked2 = blockedSequences.find((seq) => seq.every((part, idx) => actual[idx] === part));
|
|
3323
|
+
if (blocked2) return `Blocked subcommand "${blocked2.join(" ")}" for command "${cmd}"`;
|
|
3324
|
+
}
|
|
3094
3325
|
const blocked = BLOCKED_ARG_PATTERNS[cmd];
|
|
3095
3326
|
if (!blocked) return null;
|
|
3096
3327
|
for (const arg of args) {
|
|
@@ -3217,7 +3448,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
3217
3448
|
const startedAt = Date.now();
|
|
3218
3449
|
const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
|
|
3219
3450
|
const resolved = resolveWin32Command(cmd);
|
|
3220
|
-
const needsShell =
|
|
3451
|
+
const needsShell = isWin2 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
3221
3452
|
const spawnCmd = needsShell ? cmd : resolved;
|
|
3222
3453
|
if (needsShell) assertSafeWin32ShellArgs(args);
|
|
3223
3454
|
let child;
|
|
@@ -3227,7 +3458,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
3227
3458
|
env: buildChildEnv(sessionId),
|
|
3228
3459
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3229
3460
|
windowsHide: true,
|
|
3230
|
-
...
|
|
3461
|
+
...isWin2 ? {} : { signal },
|
|
3231
3462
|
...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
|
|
3232
3463
|
});
|
|
3233
3464
|
} catch (err) {
|
|
@@ -3247,7 +3478,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
3247
3478
|
const isAbort = err && err.code === "ABORT_ERR";
|
|
3248
3479
|
const stderrText = isAbort ? `Aborted: ${err.message}` : err.message;
|
|
3249
3480
|
clearTimeout(timer);
|
|
3250
|
-
if (
|
|
3481
|
+
if (isWin2) signal.removeEventListener("abort", onAbort);
|
|
3251
3482
|
if (typeof pid === "number") registry.unregister(pid);
|
|
3252
3483
|
registry.afterCall(Date.now() - startedAt, true);
|
|
3253
3484
|
spool.finalize();
|
|
@@ -3277,7 +3508,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
3277
3508
|
if (typeof pid === "number") registry.kill(pid, { force: true });
|
|
3278
3509
|
else child.kill("SIGTERM");
|
|
3279
3510
|
};
|
|
3280
|
-
if (
|
|
3511
|
+
if (isWin2) {
|
|
3281
3512
|
if (signal.aborted) onAbort();
|
|
3282
3513
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
3283
3514
|
}
|
|
@@ -3293,7 +3524,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
3293
3524
|
});
|
|
3294
3525
|
child.on("close", (code) => {
|
|
3295
3526
|
clearTimeout(timer);
|
|
3296
|
-
if (
|
|
3527
|
+
if (isWin2) signal.removeEventListener("abort", onAbort);
|
|
3297
3528
|
if (typeof pid === "number") registry.unregister(pid);
|
|
3298
3529
|
const durationMs = Date.now() - startedAt;
|
|
3299
3530
|
const exitCode = killed ? 124 : code ?? 1;
|
|
@@ -3457,7 +3688,7 @@ var fetchTool = {
|
|
|
3457
3688
|
if (!final) throw new Error("fetch: stream ended without final event");
|
|
3458
3689
|
return final;
|
|
3459
3690
|
},
|
|
3460
|
-
async *executeStream(input,
|
|
3691
|
+
async *executeStream(input, ctx, opts) {
|
|
3461
3692
|
if (!input?.url) throw new Error("fetch: url is required");
|
|
3462
3693
|
const u = new URL(input.url);
|
|
3463
3694
|
if (u.protocol !== "https:" && u.protocol !== "http:") {
|
|
@@ -3529,6 +3760,14 @@ var fetchTool = {
|
|
|
3529
3760
|
url: res.url
|
|
3530
3761
|
}
|
|
3531
3762
|
};
|
|
3763
|
+
ctx.recordSideEffect?.({
|
|
3764
|
+
toolUseId: `fetch-${Date.now()}`,
|
|
3765
|
+
toolName: "fetch",
|
|
3766
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3767
|
+
input: { url: input.url, format: input.format },
|
|
3768
|
+
outcome: `HTTP ${res.status} (${ct})`,
|
|
3769
|
+
risk: "network"
|
|
3770
|
+
});
|
|
3532
3771
|
} finally {
|
|
3533
3772
|
clearTimeout(timer);
|
|
3534
3773
|
}
|
|
@@ -4161,13 +4400,13 @@ ${formatTaskList(taskFile.tasks)}`
|
|
|
4161
4400
|
}
|
|
4162
4401
|
};
|
|
4163
4402
|
function mkResult(plan, ok, message, todos) {
|
|
4164
|
-
const
|
|
4403
|
+
const open2 = plan.items.filter((i) => i.status !== "done").length;
|
|
4165
4404
|
const result = {
|
|
4166
4405
|
ok,
|
|
4167
4406
|
message,
|
|
4168
4407
|
plan: formatPlan(plan),
|
|
4169
4408
|
count: plan.items.length,
|
|
4170
|
-
open
|
|
4409
|
+
open: open2
|
|
4171
4410
|
};
|
|
4172
4411
|
if (todos !== void 0) result.todos = todos;
|
|
4173
4412
|
return result;
|
|
@@ -4636,8 +4875,8 @@ var jsonTool = {
|
|
|
4636
4875
|
};
|
|
4637
4876
|
}
|
|
4638
4877
|
};
|
|
4639
|
-
function query(data,
|
|
4640
|
-
const parts =
|
|
4878
|
+
function query(data, path22) {
|
|
4879
|
+
const parts = path22.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
4641
4880
|
let current = data;
|
|
4642
4881
|
for (const part of parts) {
|
|
4643
4882
|
if (current === null || current === void 0) return void 0;
|
|
@@ -5009,7 +5248,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
5009
5248
|
}
|
|
5010
5249
|
}
|
|
5011
5250
|
}
|
|
5012
|
-
var
|
|
5251
|
+
var isWin3 = process.platform === "win32";
|
|
5013
5252
|
async function* spawnStream(opts) {
|
|
5014
5253
|
const max = opts.maxBytes ?? 2e5;
|
|
5015
5254
|
const flushAt = opts.flushBytes ?? 4 * 1024;
|
|
@@ -5020,7 +5259,7 @@ async function* spawnStream(opts) {
|
|
|
5020
5259
|
let error;
|
|
5021
5260
|
const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
|
|
5022
5261
|
const resolved = resolveWin32Command(opts.cmd);
|
|
5023
|
-
const needsShell =
|
|
5262
|
+
const needsShell = isWin3 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
5024
5263
|
const cmd = needsShell ? opts.cmd : resolved;
|
|
5025
5264
|
if (needsShell) assertSafeWin32ShellArgs(opts.args);
|
|
5026
5265
|
const child = spawn(cmd, opts.args, {
|
|
@@ -5028,7 +5267,7 @@ async function* spawnStream(opts) {
|
|
|
5028
5267
|
env: buildChildEnv(),
|
|
5029
5268
|
stdio: ["ignore", "pipe", "pipe"],
|
|
5030
5269
|
windowsHide: true,
|
|
5031
|
-
...
|
|
5270
|
+
...isWin3 ? {} : { signal: opts.signal },
|
|
5032
5271
|
...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
|
|
5033
5272
|
});
|
|
5034
5273
|
const registry = getProcessRegistry();
|
|
@@ -5107,7 +5346,7 @@ async function* spawnStream(opts) {
|
|
|
5107
5346
|
queue.push({ kind: "close", data: "", code: 124 });
|
|
5108
5347
|
wake();
|
|
5109
5348
|
};
|
|
5110
|
-
if (
|
|
5349
|
+
if (isWin3) {
|
|
5111
5350
|
if (opts.signal.aborted) onAbort();
|
|
5112
5351
|
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
5113
5352
|
}
|
|
@@ -5154,7 +5393,7 @@ async function* spawnStream(opts) {
|
|
|
5154
5393
|
};
|
|
5155
5394
|
} finally {
|
|
5156
5395
|
spool.finalize();
|
|
5157
|
-
if (
|
|
5396
|
+
if (isWin3) opts.signal.removeEventListener("abort", onAbort);
|
|
5158
5397
|
child.stdout?.off("data", onOut);
|
|
5159
5398
|
child.stderr?.off("data", onErr);
|
|
5160
5399
|
child.stdout?.destroy();
|
|
@@ -5740,6 +5979,14 @@ var installTool = {
|
|
|
5740
5979
|
}
|
|
5741
5980
|
}
|
|
5742
5981
|
}
|
|
5982
|
+
ctx.recordSideEffect?.({
|
|
5983
|
+
toolUseId: `install-${Date.now()}`,
|
|
5984
|
+
toolName: "install",
|
|
5985
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5986
|
+
input: { packages: pkgList, cwd, dry_run: Boolean(input.dry_run) },
|
|
5987
|
+
outcome: output.dry_run ? "dry run" : result.exitCode === 0 ? `installed ${pkgList.length || "all"} packages` : `failed (exit ${result.exitCode})`,
|
|
5988
|
+
risk: "package"
|
|
5989
|
+
});
|
|
5743
5990
|
yield { type: "final", output };
|
|
5744
5991
|
}
|
|
5745
5992
|
};
|
|
@@ -6106,7 +6353,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
6106
6353
|
}
|
|
6107
6354
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
6108
6355
|
var MAX_TAIL_LINES = 1e5;
|
|
6109
|
-
async function fileLogs(
|
|
6356
|
+
async function fileLogs(path22, lines, filterRe, stream) {
|
|
6110
6357
|
const { createInterface } = await import('node:readline');
|
|
6111
6358
|
const { createReadStream } = await import('node:fs');
|
|
6112
6359
|
const entries = [];
|
|
@@ -6115,7 +6362,7 @@ async function fileLogs(path21, lines, filterRe, stream) {
|
|
|
6115
6362
|
let writeIdx = 0;
|
|
6116
6363
|
let totalLines = 0;
|
|
6117
6364
|
const rl = createInterface({
|
|
6118
|
-
input: createReadStream(
|
|
6365
|
+
input: createReadStream(path22),
|
|
6119
6366
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
6120
6367
|
});
|
|
6121
6368
|
for await (const line of rl) {
|
|
@@ -6136,7 +6383,7 @@ async function fileLogs(path21, lines, filterRe, stream) {
|
|
|
6136
6383
|
if (parsed) entries.push(parsed);
|
|
6137
6384
|
}
|
|
6138
6385
|
return {
|
|
6139
|
-
source:
|
|
6386
|
+
source: path22,
|
|
6140
6387
|
entries,
|
|
6141
6388
|
total: entries.length,
|
|
6142
6389
|
truncated: totalLines > effLines,
|
|
@@ -6508,6 +6755,229 @@ function substituteVars(content, name, vars) {
|
|
|
6508
6755
|
}
|
|
6509
6756
|
return result;
|
|
6510
6757
|
}
|
|
6758
|
+
function normalizeOverrides(set) {
|
|
6759
|
+
const out = {};
|
|
6760
|
+
if (set && typeof set === "object") {
|
|
6761
|
+
for (const [k, v] of Object.entries(set)) {
|
|
6762
|
+
if (typeof v === "string") out[k] = v;
|
|
6763
|
+
}
|
|
6764
|
+
}
|
|
6765
|
+
return out;
|
|
6766
|
+
}
|
|
6767
|
+
var designTool = {
|
|
6768
|
+
name: "design",
|
|
6769
|
+
category: "Design",
|
|
6770
|
+
description: 'Browse, load, customize, and enforce curated frontend/mobile UI design kits. Use BEFORE writing UI code to commit to one coherent, modern, responsive, dark/light, accessible design. Actions: "list" (menu), "use" (load+pin a kit for a stack), "foundations" (baseline), "set" (override kit colors/tokens), "materialize" (write the tokens to a real theme file \u2014 CSS @theme/OKLCH or native), "verify" (scan UI files for off-palette colors).',
|
|
6771
|
+
usageHint: 'Flow: `design {action:"use", kit:"minimal-clarity", stack:"web"}` \u2192 optionally `design {action:"set", set:{primary:"oklch(62% 0.2 25)"}}` \u2192 `design {action:"materialize"}` to write tokens to disk \u2192 implement against them \u2192 `design {action:"verify"}`.',
|
|
6772
|
+
permission: "auto",
|
|
6773
|
+
mutating: false,
|
|
6774
|
+
capabilities: [],
|
|
6775
|
+
timeoutMs: 15e3,
|
|
6776
|
+
inputSchema: {
|
|
6777
|
+
type: "object",
|
|
6778
|
+
properties: {
|
|
6779
|
+
action: {
|
|
6780
|
+
type: "string",
|
|
6781
|
+
enum: ["list", "use", "foundations", "set", "materialize", "verify"],
|
|
6782
|
+
description: "list = menu; use = load+pin a kit; foundations = baseline; set = override colors/tokens; materialize = write tokens to a theme file; verify = scan UI for off-palette colors. Default: list."
|
|
6783
|
+
},
|
|
6784
|
+
kit: {
|
|
6785
|
+
type: "string",
|
|
6786
|
+
description: 'Kit id (required for "use"), e.g. "minimal-clarity", "neo-brutalist".'
|
|
6787
|
+
},
|
|
6788
|
+
stack: {
|
|
6789
|
+
type: "string",
|
|
6790
|
+
enum: ["web", "react-native", "flutter", "swiftui", "compose"],
|
|
6791
|
+
description: "Target stack \u2014 narrows guidance + materialize format. Default: web."
|
|
6792
|
+
},
|
|
6793
|
+
set: {
|
|
6794
|
+
type: "object",
|
|
6795
|
+
description: 'Token overrides for "set"/"use": { "primary": "oklch(\u2026)", "dark.bg": "#111" }. Bare key = both themes; "light."/"dark." prefix = that theme only. Empty value clears an override.',
|
|
6796
|
+
additionalProperties: { type: "string" }
|
|
6797
|
+
},
|
|
6798
|
+
out: {
|
|
6799
|
+
type: "string",
|
|
6800
|
+
description: "Materialize output path (project-relative). Defaults to a per-stack convention."
|
|
6801
|
+
},
|
|
6802
|
+
force: {
|
|
6803
|
+
type: "boolean",
|
|
6804
|
+
description: "Materialize: overwrite an existing file (default false \u2014 refuses to clobber)."
|
|
6805
|
+
},
|
|
6806
|
+
files: {
|
|
6807
|
+
type: "array",
|
|
6808
|
+
items: { type: "string" },
|
|
6809
|
+
description: "Verify: explicit project-relative files to scan. Default: a bounded UI-file walk."
|
|
6810
|
+
}
|
|
6811
|
+
},
|
|
6812
|
+
required: []
|
|
6813
|
+
},
|
|
6814
|
+
async execute(input, ctx) {
|
|
6815
|
+
const loader = getDesignKitLoader(ctx.projectRoot);
|
|
6816
|
+
const action = input.action ?? "list";
|
|
6817
|
+
const stack = input.stack && isDesignStack(input.stack) ? input.stack : void 0;
|
|
6818
|
+
if (action === "foundations") {
|
|
6819
|
+
const text = await loader.foundationsText(stack);
|
|
6820
|
+
return { action, stack, output: text || "No foundations document is installed." };
|
|
6821
|
+
}
|
|
6822
|
+
if (action === "use") {
|
|
6823
|
+
const kitId = input.kit?.trim();
|
|
6824
|
+
if (!kitId) {
|
|
6825
|
+
const menu2 = await loader.menuText();
|
|
6826
|
+
return { action, output: `No kit id provided.
|
|
6827
|
+
|
|
6828
|
+
${menu2}` };
|
|
6829
|
+
}
|
|
6830
|
+
const manifest = await loader.find(kitId);
|
|
6831
|
+
if (!manifest) {
|
|
6832
|
+
const menu2 = await loader.menuText();
|
|
6833
|
+
return { action, kit: kitId, output: `Kit "${kitId}" not found.
|
|
6834
|
+
|
|
6835
|
+
${menu2}` };
|
|
6836
|
+
}
|
|
6837
|
+
const resolvedStack = stack ?? manifest.stacks[0] ?? "web";
|
|
6838
|
+
const body = await loader.readBody(manifest.id, resolvedStack);
|
|
6839
|
+
const rawTokens = await loader.readTokens(manifest.id);
|
|
6840
|
+
const persisted = await loadActiveKit(ctx.projectRoot);
|
|
6841
|
+
const keepOverrides = persisted?.kit === manifest.id ? persisted.overrides ?? {} : {};
|
|
6842
|
+
const overrides = { ...keepOverrides, ...normalizeOverrides(input.set) };
|
|
6843
|
+
const tokens = rawTokens ? applyTokenOverrides(rawTokens, overrides) : rawTokens;
|
|
6844
|
+
setActiveKit(ctx, manifest.id, resolvedStack, overrides);
|
|
6845
|
+
await recordKitChoice(
|
|
6846
|
+
ctx.projectRoot,
|
|
6847
|
+
manifest.id,
|
|
6848
|
+
resolvedStack,
|
|
6849
|
+
"design-tool",
|
|
6850
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
6851
|
+
Object.keys(overrides).length ? overrides : void 0
|
|
6852
|
+
);
|
|
6853
|
+
const ovLine = Object.keys(overrides).length ? `
|
|
6854
|
+
Active color overrides: ${Object.entries(overrides).map(([k, v]) => `${k}=${v}`).join(", ")}
|
|
6855
|
+
` : "";
|
|
6856
|
+
const header = `# Active design kit: ${manifest.name} (${manifest.id}) \u2014 stack: ${resolvedStack}
|
|
6857
|
+
${manifest.aesthetic}
|
|
6858
|
+
${ovLine}
|
|
6859
|
+
Implement the UI faithfully to this spec. Keep light/dark, responsive, and WCAG AA.
|
|
6860
|
+
`;
|
|
6861
|
+
const tokenBlock = tokens ? `
|
|
6862
|
+
## Token snapshot (overrides applied)
|
|
6863
|
+
\`\`\`json
|
|
6864
|
+
${JSON.stringify(tokens, null, 2)}
|
|
6865
|
+
\`\`\`
|
|
6866
|
+
Tip: run \`design {action:"materialize"}\` to write these tokens to a real theme file.
|
|
6867
|
+
` : "";
|
|
6868
|
+
return {
|
|
6869
|
+
action,
|
|
6870
|
+
kit: manifest.id,
|
|
6871
|
+
stack: resolvedStack,
|
|
6872
|
+
output: `${header}${tokenBlock}
|
|
6873
|
+
${body}`
|
|
6874
|
+
};
|
|
6875
|
+
}
|
|
6876
|
+
if (action === "set") {
|
|
6877
|
+
const patch = normalizeOverrides(input.set);
|
|
6878
|
+
if (Object.keys(patch).length === 0) {
|
|
6879
|
+
return { action, output: 'No overrides given. Pass set:{ "primary": "oklch(\u2026)" }.' };
|
|
6880
|
+
}
|
|
6881
|
+
const merged = await recordOverrides(ctx.projectRoot, patch, (/* @__PURE__ */ new Date()).toISOString());
|
|
6882
|
+
if (!merged) {
|
|
6883
|
+
return {
|
|
6884
|
+
action,
|
|
6885
|
+
output: 'No active kit. Pick one first: `design {action:"use", kit:"<id>"}`.'
|
|
6886
|
+
};
|
|
6887
|
+
}
|
|
6888
|
+
setDesignOverrides(ctx, merged);
|
|
6889
|
+
return {
|
|
6890
|
+
action,
|
|
6891
|
+
output: `Overrides updated. Active overrides: ${Object.entries(merged).map(([k, v]) => `${k}=${v}`).join(", ")}
|
|
6892
|
+
These win over kit tokens. Run \`design {action:"materialize"}\` to write them to a theme file.`
|
|
6893
|
+
};
|
|
6894
|
+
}
|
|
6895
|
+
if (action === "materialize") {
|
|
6896
|
+
const active = await loadActiveKit(ctx.projectRoot);
|
|
6897
|
+
if (!active) {
|
|
6898
|
+
return {
|
|
6899
|
+
action,
|
|
6900
|
+
output: 'No active kit. Pick one first: `design {action:"use", kit:"<id>"}`.'
|
|
6901
|
+
};
|
|
6902
|
+
}
|
|
6903
|
+
const resolvedStack = stack ?? (active.stack && isDesignStack(active.stack) ? active.stack : "web");
|
|
6904
|
+
const rawTokens = await loader.readTokens(active.kit);
|
|
6905
|
+
if (!rawTokens) {
|
|
6906
|
+
return { action, kit: active.kit, output: `Kit "${active.kit}" has no tokens.json.` };
|
|
6907
|
+
}
|
|
6908
|
+
const tokens = applyTokenOverrides(rawTokens, active.overrides);
|
|
6909
|
+
const result = materializeTokens({
|
|
6910
|
+
tokens,
|
|
6911
|
+
stack: resolvedStack,
|
|
6912
|
+
kitId: active.kit,
|
|
6913
|
+
outPath: input.out
|
|
6914
|
+
});
|
|
6915
|
+
const abs = path.join(ctx.projectRoot, result.path);
|
|
6916
|
+
let exists = false;
|
|
6917
|
+
try {
|
|
6918
|
+
await fs7.access(abs);
|
|
6919
|
+
exists = true;
|
|
6920
|
+
} catch {
|
|
6921
|
+
}
|
|
6922
|
+
if (exists && !input.force) {
|
|
6923
|
+
return {
|
|
6924
|
+
action,
|
|
6925
|
+
kit: active.kit,
|
|
6926
|
+
stack: resolvedStack,
|
|
6927
|
+
path: result.path,
|
|
6928
|
+
output: `${result.path} already exists. Re-run with force:true to overwrite, or write this ${result.format} yourself:
|
|
6929
|
+
|
|
6930
|
+
\`\`\`
|
|
6931
|
+
${result.content}
|
|
6932
|
+
\`\`\``
|
|
6933
|
+
};
|
|
6934
|
+
}
|
|
6935
|
+
await fs7.mkdir(path.dirname(abs), { recursive: true });
|
|
6936
|
+
await fs7.writeFile(abs, result.content);
|
|
6937
|
+
return {
|
|
6938
|
+
action,
|
|
6939
|
+
kit: active.kit,
|
|
6940
|
+
stack: resolvedStack,
|
|
6941
|
+
path: result.path,
|
|
6942
|
+
output: `Wrote ${result.format} to ${result.path}. Import these tokens in your UI so the kit palette is the source of truth. ${exists ? "(overwrote existing file)" : ""}`
|
|
6943
|
+
};
|
|
6944
|
+
}
|
|
6945
|
+
if (action === "verify") {
|
|
6946
|
+
const active = await loadActiveKit(ctx.projectRoot);
|
|
6947
|
+
if (!active) {
|
|
6948
|
+
return {
|
|
6949
|
+
action,
|
|
6950
|
+
output: 'No active kit to verify against. Pick one: `design {action:"use", kit:"<id>"}`.'
|
|
6951
|
+
};
|
|
6952
|
+
}
|
|
6953
|
+
const rawTokens = await loader.readTokens(active.kit);
|
|
6954
|
+
if (!rawTokens) {
|
|
6955
|
+
return { action, kit: active.kit, output: `Kit "${active.kit}" has no tokens.json.` };
|
|
6956
|
+
}
|
|
6957
|
+
const tokens = applyTokenOverrides(rawTokens, active.overrides);
|
|
6958
|
+
const report = await runDesignVerify(ctx.projectRoot, tokens, input.files);
|
|
6959
|
+
const pct = Math.round(report.score * 100);
|
|
6960
|
+
const top = report.violations.slice(0, 25).map((v) => ` ${v.file}:${v.line} \u2014 ${v.reason}: ${v.snippet}`).join("\n");
|
|
6961
|
+
const summary = `Adherence: ${pct}% on-palette across ${report.filesScanned} file(s). ${report.violations.length} violation(s).` + (report.violations.length ? `
|
|
6962
|
+
${top}${report.violations.length > 25 ? `
|
|
6963
|
+
\u2026and ${report.violations.length - 25} more` : ""}
|
|
6964
|
+
|
|
6965
|
+
Replace off-palette colors with kit tokens (or the materialized CSS vars / token utilities).` : "\nNo off-palette colors found \u2014 UI adheres to the kit palette.");
|
|
6966
|
+
return {
|
|
6967
|
+
action,
|
|
6968
|
+
kit: active.kit,
|
|
6969
|
+
output: summary,
|
|
6970
|
+
score: report.score,
|
|
6971
|
+
violations: report.violations.length
|
|
6972
|
+
};
|
|
6973
|
+
}
|
|
6974
|
+
const menu = await loader.menuText();
|
|
6975
|
+
return {
|
|
6976
|
+
action: "list",
|
|
6977
|
+
output: (menu || "No design kits are installed.") + '\n\nLoad one with `design {action:"use", kit:"<id>", stack:"<stack>"}`.'
|
|
6978
|
+
};
|
|
6979
|
+
}
|
|
6980
|
+
};
|
|
6511
6981
|
|
|
6512
6982
|
// src/tool-search.ts
|
|
6513
6983
|
var toolSearchTool = {
|
|
@@ -8039,6 +8509,8 @@ var IndexStore = class {
|
|
|
8039
8509
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_kind ON symbols(kind)");
|
|
8040
8510
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_lang ON symbols(lang)");
|
|
8041
8511
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_file ON symbols(file)");
|
|
8512
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_lang_kind ON symbols(lang, kind)");
|
|
8513
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_file_fk ON symbols(file_fk)");
|
|
8042
8514
|
this.db.exec(`
|
|
8043
8515
|
CREATE TABLE IF NOT EXISTS refs (
|
|
8044
8516
|
id INTEGER PRIMARY KEY,
|
|
@@ -8951,9 +9423,9 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
8951
9423
|
}
|
|
8952
9424
|
}
|
|
8953
9425
|
async function parseSymbols3(opts) {
|
|
8954
|
-
const { file, lang } = opts;
|
|
9426
|
+
const { file, content, lang } = opts;
|
|
8955
9427
|
try {
|
|
8956
|
-
return await syncPyParse(file, lang);
|
|
9428
|
+
return await syncPyParse(file, content, lang);
|
|
8957
9429
|
} catch {
|
|
8958
9430
|
return { file, lang, symbols: [], mtimeMs: Date.now() };
|
|
8959
9431
|
}
|
|
@@ -9021,8 +9493,7 @@ syms = []
|
|
|
9021
9493
|
errors = []
|
|
9022
9494
|
|
|
9023
9495
|
try:
|
|
9024
|
-
|
|
9025
|
-
source = f.read()
|
|
9496
|
+
source = sys.stdin.read()
|
|
9026
9497
|
tree = ast.parse(source, filename=sys.argv[1])
|
|
9027
9498
|
except Exception as e:
|
|
9028
9499
|
errors.append(str(e))
|
|
@@ -9162,16 +9633,21 @@ visitor.visit(tree)
|
|
|
9162
9633
|
|
|
9163
9634
|
print(json.dumps([s.to_dict() for s in syms]))
|
|
9164
9635
|
`;
|
|
9165
|
-
|
|
9636
|
+
var _cachedScriptPath = null;
|
|
9637
|
+
async function syncPyParse(filePath, content, lang) {
|
|
9166
9638
|
try {
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
9639
|
+
if (!_cachedScriptPath) {
|
|
9640
|
+
const tmpDir = path.join(os2.tmpdir(), "ws-py-parse");
|
|
9641
|
+
await fs7.mkdir(tmpDir, { recursive: true });
|
|
9642
|
+
_cachedScriptPath = path.join(tmpDir, "parse.py");
|
|
9643
|
+
await fs7.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
|
|
9644
|
+
}
|
|
9645
|
+
const proc = spawn("python", [_cachedScriptPath, filePath], {
|
|
9172
9646
|
stdio: ["pipe", "pipe", "pipe"],
|
|
9173
9647
|
windowsHide: true
|
|
9174
9648
|
});
|
|
9649
|
+
proc.stdin?.write(content);
|
|
9650
|
+
proc.stdin?.end();
|
|
9175
9651
|
let stdout = "";
|
|
9176
9652
|
proc.stdout?.on("data", (chunk) => {
|
|
9177
9653
|
stdout += chunk.toString();
|
|
@@ -9838,7 +10314,7 @@ async function parseFile(file, content, lang) {
|
|
|
9838
10314
|
case "go":
|
|
9839
10315
|
return parseSymbols2({ file, content, lang: "go" });
|
|
9840
10316
|
case "py":
|
|
9841
|
-
return parseSymbols3({ file, lang: "py" });
|
|
10317
|
+
return parseSymbols3({ file, content, lang: "py" });
|
|
9842
10318
|
case "rs":
|
|
9843
10319
|
return parseSymbols4({ file, content, lang: "rs" });
|
|
9844
10320
|
case "json":
|
|
@@ -9869,10 +10345,12 @@ async function runIndexerWithStore(store, opts) {
|
|
|
9869
10345
|
let symbolsIndexed = 0;
|
|
9870
10346
|
const isGitIgnored = await loadGitignoreMatcher(projectRoot);
|
|
9871
10347
|
let files;
|
|
10348
|
+
let discoveredFiles = null;
|
|
9872
10349
|
if (opts.files && opts.files.length > 0) {
|
|
9873
10350
|
files = opts.files.map((f) => path.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path.relative(projectRoot, f).replace(/\\/g, "/"), false));
|
|
9874
10351
|
} else {
|
|
9875
10352
|
files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
|
|
10353
|
+
discoveredFiles = new Set(files);
|
|
9876
10354
|
}
|
|
9877
10355
|
if (langs && langs.length > 0) {
|
|
9878
10356
|
const langSet = new Set(langs);
|
|
@@ -9990,11 +10468,11 @@ async function runIndexerWithStore(store, opts) {
|
|
|
9990
10468
|
filesIndexed++;
|
|
9991
10469
|
}
|
|
9992
10470
|
}
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
|
|
9996
|
-
|
|
9997
|
-
|
|
10471
|
+
if (discoveredFiles) {
|
|
10472
|
+
for (const [file_] of existingMeta) {
|
|
10473
|
+
if (!discoveredFiles.has(file_)) {
|
|
10474
|
+
store.deleteFile(file_);
|
|
10475
|
+
}
|
|
9998
10476
|
}
|
|
9999
10477
|
}
|
|
10000
10478
|
const durationMs = Date.now() - startMs;
|
|
@@ -11031,7 +11509,8 @@ var TIER2_TOOLS = [
|
|
|
11031
11509
|
planTool,
|
|
11032
11510
|
taskTool,
|
|
11033
11511
|
installTool,
|
|
11034
|
-
auditTool
|
|
11512
|
+
auditTool,
|
|
11513
|
+
designTool
|
|
11035
11514
|
];
|
|
11036
11515
|
var TIER3_TOOLS = [
|
|
11037
11516
|
outdatedTool,
|
|
@@ -11076,6 +11555,7 @@ var builtinTools = [
|
|
|
11076
11555
|
logsTool,
|
|
11077
11556
|
documentTool,
|
|
11078
11557
|
scaffoldTool,
|
|
11558
|
+
designTool,
|
|
11079
11559
|
toolSearchTool,
|
|
11080
11560
|
toolUseTool,
|
|
11081
11561
|
batchToolUseTool,
|
|
@@ -11249,6 +11729,6 @@ var TOOL_ICON_CONFIG = {
|
|
|
11249
11729
|
};
|
|
11250
11730
|
var FALLBACK_ICON = "fallback";
|
|
11251
11731
|
|
|
11252
|
-
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, configureExecPolicy, createGlobalPsSlashCommand, createModeTool, diffTool, documentTool, editTool, enqueueReindex, ensureSessionShell, execTool, fetchTool, forgetTool, formatGlobalStatus, formatInstanceList, formatInstanceSummary, formatTool, getExecAllowlist, getIndexState, getInstanceCount, getPersistentProcessRegistry, getProcessGuardian, getProcessRegistry, getToolIcon, gitTool, globTool, grepTool, indexCircuitBreaker, installTool, isExecCommandAllowed, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, listInstances, logsTool, normalizeShell, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, relatedMemoryTool, rememberTool, replaceTool, resetExecPolicy, resetIndexCircuitBreaker, resetPersistentProcessRegistry, resolveSessionShell, runStartupIndex, scaffoldTool, searchCodebaseIndex, searchMemoryTool, searchTool, shutdownCodebaseIndexHost, startProcessGuardian, stopProcessGuardian, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
|
|
11732
|
+
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, configureExecPolicy, createGlobalPsSlashCommand, createModeTool, designTool, diffTool, documentTool, editTool, enqueueReindex, ensureSessionShell, execTool, fetchTool, forgetTool, formatGlobalStatus, formatInstanceList, formatInstanceSummary, formatTool, getExecAllowlist, getIndexState, getInstanceCount, getPersistentProcessRegistry, getProcessGuardian, getProcessRegistry, getToolIcon, gitTool, globTool, grepTool, indexCircuitBreaker, installTool, isExecCommandAllowed, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, listInstances, logsTool, normalizeShell, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, relatedMemoryTool, rememberTool, replaceTool, resetExecPolicy, resetIndexCircuitBreaker, resetPersistentProcessRegistry, resolveSessionShell, runStartupIndex, scaffoldTool, searchCodebaseIndex, searchMemoryTool, searchTool, shutdownCodebaseIndexHost, startProcessGuardian, stopProcessGuardian, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
|
|
11253
11733
|
//# sourceMappingURL=index.js.map
|
|
11254
11734
|
//# sourceMappingURL=index.js.map
|