@wrongstack/tools 0.291.0 → 0.291.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/exec.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  emitProcessOutput,
6
6
  emitProcessStarted
7
7
  } from "@wrongstack/core";
8
- import { toErrorMessage } from "@wrongstack/core/utils/error";
8
+ import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils/error";
9
9
 
10
10
  // src/_env.ts
11
11
  import { buildChildEnv } from "@wrongstack/core";
@@ -734,8 +734,8 @@ var ProcessRegistryImpl = class {
734
734
  if (p.killed) return true;
735
735
  if (p.protected) return false;
736
736
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
737
- const isWin2 = os.platform() === "win32";
738
- if (isWin2) {
737
+ const isWin3 = os.platform() === "win32";
738
+ if (isWin3) {
739
739
  const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
740
740
  const directFallback = () => {
741
741
  if (p.child.exitCode === null) {
@@ -1164,8 +1164,655 @@ function levelRank(level) {
1164
1164
  }
1165
1165
  }
1166
1166
 
1167
+ // src/exec-kill-guard.ts
1168
+ import * as os3 from "node:os";
1169
+ import * as path5 from "node:path";
1170
+
1171
+ // src/process-registry-persistent.ts
1172
+ import * as fs2 from "node:fs/promises";
1173
+ import * as os2 from "node:os";
1174
+ import * as path4 from "node:path";
1175
+ var REGISTRY_FILE = ".wrongstack/process-registry.json";
1176
+ function toErrorMessage(err) {
1177
+ return err instanceof Error ? err.message : String(err);
1178
+ }
1179
+ function emitStructuredLog(level, event, message, error) {
1180
+ const payload = {
1181
+ level,
1182
+ event,
1183
+ message,
1184
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1185
+ };
1186
+ if (error !== void 0) {
1187
+ payload.error = toErrorMessage(error);
1188
+ }
1189
+ console.log(JSON.stringify(payload));
1190
+ }
1191
+ var HEARTBEAT_INTERVAL_MS = 5e3;
1192
+ var STALE_THRESHOLD_MS = 3e4;
1193
+ var LOCKFILE = ".wrongstack/.process-registry.lock";
1194
+ function generateInstanceId() {
1195
+ const hostname2 = os2.hostname();
1196
+ const pid = process.pid;
1197
+ const random = Math.random().toString(36).slice(2, 8);
1198
+ return `${hostname2}:${pid}:${random}`;
1199
+ }
1200
+ function isNodeError(err) {
1201
+ return typeof err === "object" && err !== null && "code" in err;
1202
+ }
1203
+ async function acquireLock(lockfilePath, timeoutMs = 5e3) {
1204
+ const start = Date.now();
1205
+ const pidStr = String(process.pid);
1206
+ const hostStr = os2.hostname();
1207
+ while (Date.now() - start < timeoutMs) {
1208
+ try {
1209
+ await fs2.writeFile(lockfilePath, `${pidStr}:${hostStr}:${Date.now()}`, { flag: "wx" });
1210
+ return async () => {
1211
+ try {
1212
+ await fs2.unlink(lockfilePath);
1213
+ } catch {
1214
+ }
1215
+ };
1216
+ } catch (err) {
1217
+ if (isNodeError(err) && err.code === "EEXIST") {
1218
+ try {
1219
+ const content = await fs2.readFile(lockfilePath, "utf-8");
1220
+ const parts = content.split(":");
1221
+ const lockPidStr = parts[0] ?? "0";
1222
+ const lockPid = parseInt(lockPidStr, 10);
1223
+ if (process.platform !== "win32") {
1224
+ try {
1225
+ process.kill(lockPid, 0);
1226
+ } catch {
1227
+ await fs2.unlink(lockfilePath);
1228
+ continue;
1229
+ }
1230
+ }
1231
+ } catch {
1232
+ try {
1233
+ await fs2.unlink(lockfilePath);
1234
+ } catch {
1235
+ }
1236
+ }
1237
+ await new Promise((r) => setTimeout(r, 100));
1238
+ continue;
1239
+ }
1240
+ throw err;
1241
+ }
1242
+ }
1243
+ throw new Error(`Failed to acquire lock after ${timeoutMs}ms`);
1244
+ }
1245
+ async function readRegistryFile(filePath) {
1246
+ try {
1247
+ const content = await fs2.readFile(filePath, "utf-8");
1248
+ const parsed = JSON.parse(content);
1249
+ if (parsed.instances && Array.isArray(parsed.instances)) {
1250
+ parsed.instances = new Map(parsed.instances);
1251
+ }
1252
+ return parsed;
1253
+ } catch (err) {
1254
+ if (isNodeError(err) && err.code === "ENOENT") {
1255
+ return {
1256
+ version: 1,
1257
+ instances: /* @__PURE__ */ new Map(),
1258
+ protectedPatterns: ["wrongstack", "node"],
1259
+ lastCleanup: Date.now()
1260
+ };
1261
+ }
1262
+ throw err;
1263
+ }
1264
+ }
1265
+ async function writeRegistryFile(filePath, data) {
1266
+ const tmpPath = `${filePath}.tmp.${process.pid}`;
1267
+ const content = JSON.stringify(data, (_k, v) => {
1268
+ if (v instanceof Map) {
1269
+ return Array.from(v.entries());
1270
+ }
1271
+ return v;
1272
+ }, 2);
1273
+ await fs2.writeFile(tmpPath, content, "utf-8");
1274
+ await fs2.rename(tmpPath, filePath);
1275
+ }
1276
+ var PersistentProcessRegistry = class {
1277
+ instanceId;
1278
+ registryPath;
1279
+ lockPath;
1280
+ baseRegistry;
1281
+ heartbeatInterval = null;
1282
+ isShuttingDown = false;
1283
+ constructor(baseRegistry) {
1284
+ this.instanceId = generateInstanceId();
1285
+ const homeDir = os2.homedir();
1286
+ this.registryPath = path4.join(homeDir, REGISTRY_FILE);
1287
+ this.lockPath = path4.join(homeDir, LOCKFILE);
1288
+ this.baseRegistry = baseRegistry ?? getProcessRegistry();
1289
+ this.ensureDirectory().catch((err) => {
1290
+ emitStructuredLog("warn", "process_registry.dir_create_failed", "PersistentProcessRegistry: failed to create .wrongstack directory", err);
1291
+ });
1292
+ }
1293
+ async ensureDirectory() {
1294
+ const dir = path4.dirname(this.registryPath);
1295
+ try {
1296
+ await fs2.mkdir(dir, { recursive: true });
1297
+ } catch (err) {
1298
+ if (!isNodeError(err) || err.code !== "EEXIST") throw err;
1299
+ }
1300
+ }
1301
+ /**
1302
+ * Start the heartbeat and periodic cleanup tasks.
1303
+ */
1304
+ start() {
1305
+ if (this.heartbeatInterval) return;
1306
+ this.syncToPersistent();
1307
+ this.heartbeatInterval = setInterval(() => {
1308
+ this.heartbeat();
1309
+ }, HEARTBEAT_INTERVAL_MS);
1310
+ this.heartbeatInterval.unref?.();
1311
+ setInterval(() => {
1312
+ this.cleanupStaleEntries();
1313
+ }, STALE_THRESHOLD_MS).unref?.();
1314
+ this.registerMainProcess();
1315
+ process.on("exit", () => this.syncToPersistent());
1316
+ }
1317
+ /**
1318
+ * Stop the heartbeat and clean up.
1319
+ */
1320
+ stop() {
1321
+ this.isShuttingDown = true;
1322
+ if (this.heartbeatInterval) {
1323
+ clearInterval(this.heartbeatInterval);
1324
+ this.heartbeatInterval = null;
1325
+ }
1326
+ this.syncToPersistent();
1327
+ }
1328
+ /**
1329
+ * Register the main WrongStack process as protected.
1330
+ */
1331
+ registerMainProcess() {
1332
+ const mainPid = process.pid;
1333
+ this.updatePersistentEntry({
1334
+ pid: mainPid,
1335
+ name: "wrongstack-main",
1336
+ command: process.argv.slice(0, 3).join(" "),
1337
+ startedAt: Date.now(),
1338
+ lastHeartbeat: Date.now(),
1339
+ instanceId: this.instanceId,
1340
+ hostname: os2.hostname(),
1341
+ protected: true,
1342
+ spawnMode: "main",
1343
+ parentPid: process.ppid,
1344
+ platform: process.platform
1345
+ });
1346
+ }
1347
+ /**
1348
+ * Register a spawned child process with the persistent registry.
1349
+ */
1350
+ registerChildProcess(pid, name, command, sessionId, spawnMode = "spawn") {
1351
+ const entry = {
1352
+ pid,
1353
+ name,
1354
+ command,
1355
+ startedAt: Date.now(),
1356
+ lastHeartbeat: Date.now(),
1357
+ instanceId: this.instanceId,
1358
+ hostname: os2.hostname(),
1359
+ protected: true,
1360
+ // All WrongStack child processes are protected by default
1361
+ spawnMode,
1362
+ parentPid: process.pid,
1363
+ platform: process.platform
1364
+ };
1365
+ if (sessionId) {
1366
+ entry.sessionId = sessionId;
1367
+ }
1368
+ this.updatePersistentEntry(entry);
1369
+ }
1370
+ /**
1371
+ * Update or add an entry in the persistent registry.
1372
+ */
1373
+ async updatePersistentEntry(entry) {
1374
+ const release = await acquireLock(this.lockPath);
1375
+ try {
1376
+ const data = await readRegistryFile(this.registryPath);
1377
+ data.instances.set(String(entry.pid), entry);
1378
+ const child = null;
1379
+ this.baseRegistry.register({
1380
+ pid: entry.pid,
1381
+ name: entry.name,
1382
+ command: entry.command,
1383
+ startedAt: entry.startedAt,
1384
+ sessionId: entry.sessionId,
1385
+ protected: entry.protected,
1386
+ child
1387
+ });
1388
+ await writeRegistryFile(this.registryPath, data);
1389
+ } finally {
1390
+ await release();
1391
+ }
1392
+ }
1393
+ /**
1394
+ * Unregister a process from the persistent registry.
1395
+ */
1396
+ async unregister(pid) {
1397
+ const release = await acquireLock(this.lockPath);
1398
+ try {
1399
+ const data = await readRegistryFile(this.registryPath);
1400
+ data.instances.delete(String(pid));
1401
+ await writeRegistryFile(this.registryPath, data);
1402
+ } finally {
1403
+ await release();
1404
+ }
1405
+ }
1406
+ /**
1407
+ * Send heartbeat to mark all this instance's processes as alive.
1408
+ */
1409
+ heartbeat() {
1410
+ if (this.isShuttingDown) return;
1411
+ this.syncToPersistent();
1412
+ }
1413
+ /**
1414
+ * Sync this instance's processes to the persistent registry.
1415
+ */
1416
+ async syncToPersistent() {
1417
+ const release = await acquireLock(this.lockPath);
1418
+ try {
1419
+ const data = await readRegistryFile(this.registryPath);
1420
+ const now = Date.now();
1421
+ const updatedInstances = /* @__PURE__ */ new Map();
1422
+ for (const [_pidStr, entry] of data.instances) {
1423
+ if (entry.instanceId === this.instanceId) {
1424
+ entry.lastHeartbeat = now;
1425
+ }
1426
+ if (entry.instanceId === this.instanceId || now - entry.lastHeartbeat < STALE_THRESHOLD_MS) {
1427
+ updatedInstances.set(_pidStr, entry);
1428
+ }
1429
+ }
1430
+ data.instances = updatedInstances;
1431
+ data.lastCleanup = now;
1432
+ await writeRegistryFile(this.registryPath, data);
1433
+ } catch (err) {
1434
+ emitStructuredLog("warn", "process_registry.sync_failed", "PersistentProcessRegistry: sync failed", err);
1435
+ } finally {
1436
+ await release();
1437
+ }
1438
+ }
1439
+ /**
1440
+ * Remove entries for processes that are no longer running.
1441
+ */
1442
+ async cleanupStaleEntries() {
1443
+ const release = await acquireLock(this.lockPath);
1444
+ try {
1445
+ const data = await readRegistryFile(this.registryPath);
1446
+ const now = Date.now();
1447
+ const stalePids = [];
1448
+ for (const [_pidStr, entry] of data.instances) {
1449
+ const age = now - entry.lastHeartbeat;
1450
+ if (age > STALE_THRESHOLD_MS) {
1451
+ try {
1452
+ if (process.platform !== "win32") {
1453
+ process.kill(entry.pid, 0);
1454
+ } else {
1455
+ emitStructuredLog(
1456
+ "debug",
1457
+ "process_registry.stale_pid_check",
1458
+ `PersistentProcessRegistry: checking stale pid ${entry.pid} (${age}ms old)`
1459
+ );
1460
+ }
1461
+ } catch {
1462
+ stalePids.push(_pidStr);
1463
+ }
1464
+ }
1465
+ }
1466
+ if (stalePids.length > 0) {
1467
+ for (const pidStr of stalePids) {
1468
+ data.instances.delete(pidStr);
1469
+ }
1470
+ await writeRegistryFile(this.registryPath, data);
1471
+ }
1472
+ } catch (err) {
1473
+ emitStructuredLog("warn", "process_registry.cleanup_failed", "PersistentProcessRegistry: cleanup failed", err);
1474
+ } finally {
1475
+ await release();
1476
+ }
1477
+ }
1478
+ /**
1479
+ * Check if a PID belongs to a WrongStack process and should be protected.
1480
+ */
1481
+ async isProtectedPid(pid) {
1482
+ const release = await acquireLock(this.lockPath);
1483
+ try {
1484
+ const data = await readRegistryFile(this.registryPath);
1485
+ const entry = data.instances.get(String(pid));
1486
+ if (!entry) return false;
1487
+ if (Date.now() - entry.lastHeartbeat > STALE_THRESHOLD_MS) {
1488
+ return false;
1489
+ }
1490
+ return entry.protected;
1491
+ } finally {
1492
+ await release();
1493
+ }
1494
+ }
1495
+ /**
1496
+ * Get all protected PIDs from all WrongStack instances.
1497
+ */
1498
+ async getAllProtectedPids() {
1499
+ const release = await acquireLock(this.lockPath);
1500
+ try {
1501
+ const data = await readRegistryFile(this.registryPath);
1502
+ const now = Date.now();
1503
+ const protectedPids = [];
1504
+ for (const [_pidStr, entry] of data.instances) {
1505
+ if (entry.protected && now - entry.lastHeartbeat < STALE_THRESHOLD_MS) {
1506
+ protectedPids.push(entry.pid);
1507
+ }
1508
+ }
1509
+ return protectedPids;
1510
+ } finally {
1511
+ await release();
1512
+ }
1513
+ }
1514
+ /**
1515
+ * Get complete status of all tracked processes across all instances.
1516
+ */
1517
+ async getGlobalStatus() {
1518
+ const release = await acquireLock(this.lockPath);
1519
+ try {
1520
+ const data = await readRegistryFile(this.registryPath);
1521
+ const now = Date.now();
1522
+ const instances = /* @__PURE__ */ new Map();
1523
+ let protectedCount = 0;
1524
+ let staleCount = 0;
1525
+ for (const [_pidStr, entry] of data.instances) {
1526
+ const instanceEntries = instances.get(entry.instanceId) ?? [];
1527
+ instanceEntries.push(entry);
1528
+ instances.set(entry.instanceId, instanceEntries);
1529
+ if (entry.protected) protectedCount++;
1530
+ if (now - entry.lastHeartbeat > STALE_THRESHOLD_MS) staleCount++;
1531
+ }
1532
+ return {
1533
+ instances,
1534
+ totalProcesses: data.instances.size,
1535
+ protectedCount,
1536
+ staleCount
1537
+ };
1538
+ } finally {
1539
+ await release();
1540
+ }
1541
+ }
1542
+ /**
1543
+ * Get the instance ID for this process.
1544
+ */
1545
+ getInstanceId() {
1546
+ return this.instanceId;
1547
+ }
1548
+ /**
1549
+ * Check if a kill command should be blocked.
1550
+ * Returns true if the kill should be blocked (target is a WrongStack process).
1551
+ */
1552
+ async shouldBlockKill(pid) {
1553
+ const protectedPids = await this.getAllProtectedPids();
1554
+ return protectedPids.includes(pid);
1555
+ }
1556
+ /**
1557
+ * Add a pattern-based protection rule.
1558
+ * Processes whose command matches any protected pattern are protected.
1559
+ */
1560
+ async addProtectedPattern(pattern) {
1561
+ const release = await acquireLock(this.lockPath);
1562
+ try {
1563
+ const data = await readRegistryFile(this.registryPath);
1564
+ if (!data.protectedPatterns.includes(pattern)) {
1565
+ data.protectedPatterns.push(pattern);
1566
+ await writeRegistryFile(this.registryPath, data);
1567
+ }
1568
+ } finally {
1569
+ await release();
1570
+ }
1571
+ }
1572
+ };
1573
+ var _persistentRegistry;
1574
+ function getPersistentProcessRegistry() {
1575
+ if (!_persistentRegistry) {
1576
+ _persistentRegistry = new PersistentProcessRegistry();
1577
+ }
1578
+ return _persistentRegistry;
1579
+ }
1580
+
1581
+ // src/exec-kill-guard.ts
1582
+ var isWin = os3.platform() === "win32";
1583
+ async function checkExecKillCommand(cmd, args) {
1584
+ if (!cmd) return { blocked: false };
1585
+ const cmdLower = cmd.toLowerCase().trim();
1586
+ const fullCommand = [cmdLower, ...args].join(" ").replace(/\s+/g, " ").trim();
1587
+ if (isWin) {
1588
+ if (cmdLower === "taskkill" || cmdLower === "taskkill.exe") {
1589
+ const hasForce = args.some((a) => a.toUpperCase() === "/F" || a.toUpperCase() === "-F");
1590
+ const signal = hasForce ? "FORCE" : "TERM";
1591
+ for (let i = 0; i < args.length; i++) {
1592
+ const a = args[i];
1593
+ if (a.toUpperCase() === "/IM" || a.toUpperCase() === "-IM") {
1594
+ const nameArg = args[i + 1];
1595
+ if (nameArg) {
1596
+ const result = await checkKillTarget({ name: nameArg, signal, cmd: fullCommand });
1597
+ if (result.blocked) return result;
1598
+ }
1599
+ }
1600
+ }
1601
+ for (let i = 0; i < args.length; i++) {
1602
+ const a = args[i];
1603
+ if (a.toUpperCase() === "/PID" || a.toUpperCase() === "-PID") {
1604
+ const pidArg = args[i + 1];
1605
+ if (pidArg && /^\d+$/.test(pidArg)) {
1606
+ const result = await checkKillTarget({
1607
+ pid: parseInt(pidArg, 10),
1608
+ signal,
1609
+ cmd: fullCommand
1610
+ });
1611
+ if (result.blocked) return result;
1612
+ }
1613
+ }
1614
+ }
1615
+ for (let i = 0; i < args.length; i++) {
1616
+ const a = args[i];
1617
+ if (a.toUpperCase() === "/FI" || a.toUpperCase() === "-FI") {
1618
+ const filterArg = args[i + 1];
1619
+ if (filterArg) {
1620
+ const nameMatch = filterArg.match(/IMAGENAME\s+eq\s+"?([^"\s]+)/i);
1621
+ if (nameMatch?.[1]) {
1622
+ const result = await checkKillTarget({ name: nameMatch[1], signal, cmd: fullCommand });
1623
+ if (result.blocked) return result;
1624
+ }
1625
+ }
1626
+ }
1627
+ }
1628
+ return { blocked: false };
1629
+ }
1630
+ if (cmdLower === "powershell" || cmdLower === "powershell.exe" || cmdLower === "pwsh" || cmdLower === "pwsh.exe" || cmdLower === "cmd" || cmdLower === "cmd.exe") {
1631
+ const shellFlagIndex = args.findIndex((arg) => {
1632
+ const lower = arg.toLowerCase();
1633
+ return lower === "-c" || lower === "-command" || lower === "/c";
1634
+ });
1635
+ if (shellFlagIndex >= 0) {
1636
+ const innerTokens = tokenizeShellCommand(args.slice(shellFlagIndex + 1).join(" "));
1637
+ const innerCommand = innerTokens[0];
1638
+ if (innerCommand) {
1639
+ const result = await checkExecKillCommand(innerCommand, innerTokens.slice(1));
1640
+ if (result.blocked) return result;
1641
+ }
1642
+ }
1643
+ }
1644
+ if (cmdLower === "stop-process" || cmdLower === "kill") {
1645
+ for (let i = 0; i < args.length; i++) {
1646
+ const a = args[i];
1647
+ if (a === "-Name" || a === "-n") {
1648
+ const nameArg = args[i + 1]?.replace(/^['"]|['"]$/g, "");
1649
+ if (nameArg) {
1650
+ const result = await checkKillTarget({
1651
+ name: nameArg,
1652
+ signal: "FORCE",
1653
+ cmd: fullCommand
1654
+ });
1655
+ if (result.blocked) return result;
1656
+ }
1657
+ }
1658
+ if (a === "-Id" || a === "-PID" || a === "-pid") {
1659
+ const pidArg = args[i + 1];
1660
+ if (pidArg && /^\d+$/.test(pidArg)) {
1661
+ const result = await checkKillTarget({
1662
+ pid: parseInt(pidArg, 10),
1663
+ signal: "FORCE",
1664
+ cmd: fullCommand
1665
+ });
1666
+ if (result.blocked) return result;
1667
+ }
1668
+ }
1669
+ }
1670
+ const firstNonFlag = args.find((a) => !a.startsWith("-"));
1671
+ if (firstNonFlag) {
1672
+ const name = firstNonFlag.replace(/^['"]|['"]$/g, "");
1673
+ const result = await checkKillTarget({ name, signal: "TERM", cmd: fullCommand });
1674
+ if (result.blocked) return result;
1675
+ }
1676
+ }
1677
+ if (cmdLower === "wmic" || cmdLower === "wmic.exe") {
1678
+ const joined = args.join(" ").toLowerCase();
1679
+ if (/\bprocess\b/.test(joined) && /\bdelete\b/.test(joined)) {
1680
+ const nameMatch = joined.match(/name\s*=\s*['"]?([^'"]+)/);
1681
+ if (nameMatch?.[1]) {
1682
+ const result = await checkKillTarget({
1683
+ name: nameMatch[1].trim(),
1684
+ signal: "FORCE",
1685
+ cmd: fullCommand
1686
+ });
1687
+ if (result.blocked) return result;
1688
+ }
1689
+ return {
1690
+ blocked: true,
1691
+ reason: "Blocked: wmic process delete targets all matched processes \u2014 would include protected WrongStack processes."
1692
+ };
1693
+ }
1694
+ }
1695
+ if (cmdLower === "node" || cmdLower === "node.exe") {
1696
+ if (args.includes("-e") || args.includes("--eval")) {
1697
+ const evalIdx = args.indexOf("-e") !== -1 ? args.indexOf("-e") : args.indexOf("--eval");
1698
+ const evalCode = args[evalIdx + 1] ?? "";
1699
+ if (/\bprocess\.kill\s*\(/.test(evalCode)) {
1700
+ const pidMatch = evalCode.match(/process\.kill\s*\(\s*(\d+)/);
1701
+ if (pidMatch?.[1]) {
1702
+ const pid = parseInt(pidMatch[1], 10);
1703
+ const result = await checkKillTarget({ pid, signal: "SIGTERM", cmd: fullCommand });
1704
+ if (result.blocked) return result;
1705
+ }
1706
+ return {
1707
+ blocked: true,
1708
+ reason: "Blocked: node -e with process.kill() \u2014 would target protected WrongStack process(es)."
1709
+ };
1710
+ }
1711
+ }
1712
+ }
1713
+ } else {
1714
+ if (cmdLower === "kill") {
1715
+ for (const a of args) {
1716
+ const num = a.replace(/^-/, "");
1717
+ if (/^\d+$/.test(num)) {
1718
+ const pid = parseInt(num, 10);
1719
+ const result = await checkKillTarget({ pid, signal: "SIGTERM", cmd: fullCommand });
1720
+ if (result.blocked) return result;
1721
+ }
1722
+ }
1723
+ }
1724
+ if (cmdLower === "pkill" || cmdLower === "killall") {
1725
+ const firstNonFlag = args.find((a) => !a.startsWith("-"));
1726
+ if (firstNonFlag) {
1727
+ const result = await checkKillTarget({
1728
+ name: firstNonFlag,
1729
+ signal: "SIGTERM",
1730
+ cmd: fullCommand
1731
+ });
1732
+ if (result.blocked) return result;
1733
+ }
1734
+ }
1735
+ }
1736
+ return { blocked: false };
1737
+ }
1738
+ function tokenizeShellCommand(command) {
1739
+ const tokens = [];
1740
+ let current = "";
1741
+ let quote = null;
1742
+ for (const char of command.trim()) {
1743
+ if (quote) {
1744
+ if (char === quote) quote = null;
1745
+ else current += char;
1746
+ continue;
1747
+ }
1748
+ if (char === '"' || char === "'") {
1749
+ quote = char;
1750
+ } else if (/\s/.test(char)) {
1751
+ if (current) {
1752
+ tokens.push(current);
1753
+ current = "";
1754
+ }
1755
+ } else {
1756
+ current += char;
1757
+ }
1758
+ }
1759
+ if (current) tokens.push(current);
1760
+ return tokens;
1761
+ }
1762
+ async function checkKillTarget(target) {
1763
+ const registry = getPersistentProcessRegistry();
1764
+ if (target.pid !== void 0) {
1765
+ const blocked = await registry.shouldBlockKill(target.pid);
1766
+ if (blocked) {
1767
+ return {
1768
+ blocked: true,
1769
+ reason: `Blocked: kill ${target.signal} PID ${target.pid} targets a protected WrongStack process (${target.cmd.slice(0, 80)}).`
1770
+ };
1771
+ }
1772
+ if (target.pid === process.pid) {
1773
+ return {
1774
+ blocked: true,
1775
+ reason: "Blocked: cannot kill the current WrongStack process."
1776
+ };
1777
+ }
1778
+ if (target.pid === process.ppid) {
1779
+ return {
1780
+ blocked: true,
1781
+ reason: "Blocked: cannot kill the parent terminal hosting WrongStack."
1782
+ };
1783
+ }
1784
+ return { blocked: false };
1785
+ }
1786
+ if (target.name) {
1787
+ const nameLower = target.name.toLowerCase().replace(/\.exe$/, "");
1788
+ if (nameLower.includes("wrongstack")) {
1789
+ return {
1790
+ blocked: true,
1791
+ reason: `Blocked: kill ${target.signal} '${target.name}' targets a WrongStack process name.`
1792
+ };
1793
+ }
1794
+ const currentImage = path5.basename(process.execPath).toLowerCase().replace(/\.exe$/, "");
1795
+ const targetsNodeRuntime = nameLower === "node" || nameLower.startsWith("node");
1796
+ if (targetsNodeRuntime && currentImage === "node") {
1797
+ return {
1798
+ blocked: true,
1799
+ reason: `Blocked: kill ${target.signal} '${target.name}' would kill the active WrongStack node.exe runtime.`
1800
+ };
1801
+ }
1802
+ const protectedPids = await registry.getAllProtectedPids();
1803
+ if (protectedPids.length > 0 && targetsNodeRuntime) {
1804
+ return {
1805
+ blocked: true,
1806
+ reason: `Blocked: kill ${target.signal} '${target.name}' would kill all node.exe processes including active WrongStack instance(s).`
1807
+ };
1808
+ }
1809
+ return { blocked: false };
1810
+ }
1811
+ return { blocked: false };
1812
+ }
1813
+
1167
1814
  // src/exec.ts
1168
- var isWin = process.platform === "win32";
1815
+ var isWin2 = process.platform === "win32";
1169
1816
  var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
1170
1817
  // JS / TS toolchain
1171
1818
  "node",
@@ -1970,6 +2617,19 @@ var execTool = {
1970
2617
  const args = (input.args ?? []).slice(0, MAX_ARGS);
1971
2618
  const timeout = Math.max(1, Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS, DEFAULT_TIMEOUT_MS));
1972
2619
  const danger = detectDanger(cmd, args, dangerBypass);
2620
+ const killCheck = await checkExecKillCommand(cmd, args);
2621
+ if (killCheck.blocked) {
2622
+ return {
2623
+ command: cmd,
2624
+ args,
2625
+ stdout: "",
2626
+ stderr: killCheck.reason ?? "Kill command blocked: targets a protected WrongStack process.",
2627
+ exitCode: 1,
2628
+ truncated: false,
2629
+ allowed: false,
2630
+ danger
2631
+ };
2632
+ }
1973
2633
  const argError = validateArgs(cmd, args);
1974
2634
  if (argError) {
1975
2635
  return {
@@ -2020,7 +2680,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
2020
2680
  let timedOut = false;
2021
2681
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT });
2022
2682
  const resolved = resolveWin32Command(cmd);
2023
- const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
2683
+ const needsShell = isWin2 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
2024
2684
  const shim = needsShell ? buildWin32CmdShimInvocation(resolved, args) : null;
2025
2685
  const spawnCmd = shim?.command ?? resolved;
2026
2686
  const spawnArgs = shim?.args ?? args;
@@ -2045,7 +2705,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
2045
2705
  env: buildChildEnv(sessionId),
2046
2706
  stdio: ["ignore", "pipe", "pipe"],
2047
2707
  windowsHide: true,
2048
- ...isWin ? {} : { signal },
2708
+ ...isWin2 ? {} : { signal },
2049
2709
  ...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
2050
2710
  });
2051
2711
  } catch (err) {
@@ -2063,7 +2723,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
2063
2723
  command: cmd,
2064
2724
  args,
2065
2725
  stdout: "",
2066
- stderr: `spawn failed: ${toErrorMessage(err)}`,
2726
+ stderr: `spawn failed: ${toErrorMessage2(err)}`,
2067
2727
  exitCode: 1,
2068
2728
  truncated: false,
2069
2729
  allowed: true,
@@ -2084,7 +2744,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
2084
2744
  const isAbort = err && err.code === "ABORT_ERR";
2085
2745
  const stderrText = isAbort ? `Aborted: ${err.message}` : err.message;
2086
2746
  clearTimeout(timer);
2087
- if (isWin) signal.removeEventListener("abort", onAbort);
2747
+ if (isWin2) signal.removeEventListener("abort", onAbort);
2088
2748
  if (typeof pid === "number") registry.unregister(pid);
2089
2749
  registry.afterCall(Date.now() - startedAt, true);
2090
2750
  spool.finalize();
@@ -2117,7 +2777,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
2117
2777
  if (typeof pid === "number") registry.kill(pid, { force: true });
2118
2778
  else child.kill("SIGTERM");
2119
2779
  };
2120
- if (isWin) {
2780
+ if (isWin2) {
2121
2781
  if (signal.aborted) onAbort();
2122
2782
  else signal.addEventListener("abort", onAbort, { once: true });
2123
2783
  }
@@ -2137,7 +2797,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
2137
2797
  });
2138
2798
  child.on("close", (code) => {
2139
2799
  clearTimeout(timer);
2140
- if (isWin) signal.removeEventListener("abort", onAbort);
2800
+ if (isWin2) signal.removeEventListener("abort", onAbort);
2141
2801
  if (typeof pid === "number") registry.unregister(pid);
2142
2802
  const durationMs = Date.now() - startedAt;
2143
2803
  const exitCode = killed ? 124 : code ?? 1;