@wrongstack/tools 0.272.2 → 0.273.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/bash.js CHANGED
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
2
2
  import * as os2 from 'node:os';
3
3
  import * as Core from '@wrongstack/core';
4
4
  import { buildChildEnv, expectDefined, wstackGlobalRoot } from '@wrongstack/core';
5
+ import * as fs2 from 'node:fs';
5
6
  import { mkdirSync, createWriteStream } from 'node:fs';
6
7
  import * as fs from 'node:fs/promises';
7
8
  import * as path from 'node:path';
@@ -699,6 +700,21 @@ function getProcessRegistry() {
699
700
  return _registry;
700
701
  }
701
702
  var REGISTRY_FILE = ".wrongstack/process-registry.json";
703
+ function toErrorMessage(err) {
704
+ return err instanceof Error ? err.message : String(err);
705
+ }
706
+ function emitStructuredLog(level, event, message, error) {
707
+ const payload = {
708
+ level,
709
+ event,
710
+ message,
711
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
712
+ };
713
+ if (error !== void 0) {
714
+ payload.error = toErrorMessage(error);
715
+ }
716
+ console.log(JSON.stringify(payload));
717
+ }
702
718
  var HEARTBEAT_INTERVAL_MS = 5e3;
703
719
  var STALE_THRESHOLD_MS = 3e4;
704
720
  var LOCKFILE = ".wrongstack/.process-registry.lock";
@@ -708,6 +724,9 @@ function generateInstanceId() {
708
724
  const random = Math.random().toString(36).slice(2, 8);
709
725
  return `${hostname2}:${pid}:${random}`;
710
726
  }
727
+ function isNodeError(err) {
728
+ return typeof err === "object" && err !== null && "code" in err;
729
+ }
711
730
  async function acquireLock(lockfilePath, timeoutMs = 5e3) {
712
731
  const start = Date.now();
713
732
  const pidStr = String(process.pid);
@@ -722,7 +741,7 @@ async function acquireLock(lockfilePath, timeoutMs = 5e3) {
722
741
  }
723
742
  };
724
743
  } catch (err) {
725
- if (err.code === "EEXIST") {
744
+ if (isNodeError(err) && err.code === "EEXIST") {
726
745
  try {
727
746
  const content = await fs.readFile(lockfilePath, "utf-8");
728
747
  const parts = content.split(":");
@@ -759,7 +778,7 @@ async function readRegistryFile(filePath) {
759
778
  }
760
779
  return parsed;
761
780
  } catch (err) {
762
- if (err.code === "ENOENT") {
781
+ if (isNodeError(err) && err.code === "ENOENT") {
763
782
  return {
764
783
  version: 1,
765
784
  instances: /* @__PURE__ */ new Map(),
@@ -795,7 +814,7 @@ var PersistentProcessRegistry = class {
795
814
  this.lockPath = path.join(homeDir, LOCKFILE);
796
815
  this.baseRegistry = baseRegistry ?? getProcessRegistry();
797
816
  this.ensureDirectory().catch((err) => {
798
- console.error("PersistentProcessRegistry: failed to create .wrongstack dir", err);
817
+ emitStructuredLog("warn", "process_registry.dir_create_failed", "PersistentProcessRegistry: failed to create .wrongstack directory", err);
799
818
  });
800
819
  }
801
820
  async ensureDirectory() {
@@ -803,7 +822,7 @@ var PersistentProcessRegistry = class {
803
822
  try {
804
823
  await fs.mkdir(dir, { recursive: true });
805
824
  } catch (err) {
806
- if (err.code !== "EEXIST") throw err;
825
+ if (!isNodeError(err) || err.code !== "EEXIST") throw err;
807
826
  }
808
827
  }
809
828
  /**
@@ -883,6 +902,7 @@ var PersistentProcessRegistry = class {
883
902
  try {
884
903
  const data = await readRegistryFile(this.registryPath);
885
904
  data.instances.set(String(entry.pid), entry);
905
+ const child = null;
886
906
  this.baseRegistry.register({
887
907
  pid: entry.pid,
888
908
  name: entry.name,
@@ -890,8 +910,7 @@ var PersistentProcessRegistry = class {
890
910
  startedAt: entry.startedAt,
891
911
  sessionId: entry.sessionId,
892
912
  protected: entry.protected,
893
- child: null
894
- // Main process has no child handle
913
+ child
895
914
  });
896
915
  await writeRegistryFile(this.registryPath, data);
897
916
  } finally {
@@ -939,7 +958,7 @@ var PersistentProcessRegistry = class {
939
958
  data.lastCleanup = now;
940
959
  await writeRegistryFile(this.registryPath, data);
941
960
  } catch (err) {
942
- console.error("PersistentProcessRegistry: sync failed", err);
961
+ emitStructuredLog("warn", "process_registry.sync_failed", "PersistentProcessRegistry: sync failed", err);
943
962
  } finally {
944
963
  await release();
945
964
  }
@@ -960,7 +979,11 @@ var PersistentProcessRegistry = class {
960
979
  if (process.platform !== "win32") {
961
980
  process.kill(entry.pid, 0);
962
981
  } else {
963
- console.log(`PersistentProcessRegistry: checking stale pid ${entry.pid} (${age}ms old)`);
982
+ emitStructuredLog(
983
+ "debug",
984
+ "process_registry.stale_pid_check",
985
+ `PersistentProcessRegistry: checking stale pid ${entry.pid} (${age}ms old)`
986
+ );
964
987
  }
965
988
  } catch {
966
989
  stalePids.push(_pidStr);
@@ -974,7 +997,7 @@ var PersistentProcessRegistry = class {
974
997
  await writeRegistryFile(this.registryPath, data);
975
998
  }
976
999
  } catch (err) {
977
- console.error("PersistentProcessRegistry: cleanup failed", err);
1000
+ emitStructuredLog("warn", "process_registry.cleanup_failed", "PersistentProcessRegistry: cleanup failed", err);
978
1001
  } finally {
979
1002
  await release();
980
1003
  }
@@ -1197,7 +1220,7 @@ async function isKillProtected(kill) {
1197
1220
  const entries = await getProtectedEntries();
1198
1221
  const killNameLower = kill.name.toLowerCase();
1199
1222
  for (const entry of entries) {
1200
- if (entry.name && entry.name.toLowerCase().includes(killNameLower)) {
1223
+ if (entry.name?.toLowerCase().includes(killNameLower)) {
1201
1224
  return true;
1202
1225
  }
1203
1226
  }
@@ -1252,6 +1275,145 @@ async function checkAndBlockKillCommand(command) {
1252
1275
  }
1253
1276
  return { blocked: false };
1254
1277
  }
1278
+ function pickShell(platform3, command, env) {
1279
+ const override = env.get("WRONGSTACK_SHELL")?.trim().toLowerCase();
1280
+ if (override === "cmd" || override === "cmd.exe") return "cmd";
1281
+ if (override === "powershell" || override === "powershell.exe") return "powershell";
1282
+ if (override === "pwsh" || override === "pwsh.exe") return "pwsh";
1283
+ if (looksLikePowerShell(command)) return "pwsh";
1284
+ return "cmd";
1285
+ }
1286
+ function looksLikePowerShell(command) {
1287
+ if (!command) return false;
1288
+ const trimmed = command.trimStart();
1289
+ if (/\.ps1\b/i.test(trimmed)) return true;
1290
+ if (/^\s*#requires\s/i.test(trimmed)) return true;
1291
+ if (/^\s*param\s*\(/i.test(trimmed)) return true;
1292
+ if (/\$[\w:{]/i.test(trimmed)) return true;
1293
+ if (/\$\(/.test(trimmed)) return true;
1294
+ if (/@\s*['"]/.test(trimmed)) return true;
1295
+ if (/&\s+\$/.test(trimmed)) return true;
1296
+ if (/(^|\s)@\s*\(/.test(trimmed)) return true;
1297
+ if (/(^|\s)@\{/.test(trimmed)) return true;
1298
+ if (/(?:^|[\s\[\(\{,;])(?:-eq|-ne|-lt|-gt|-le|-ge|-like|-notlike|-match|-notmatch|-contains|-notcontains|-in|-notin|-and|-or|-not|-band|-bor|-bxor|-replace|-isplit|-csplit|-osplit|-join|-is|-as|-f)(?:$|[\s\]\)\},;])/i.test(trimmed)) {
1299
+ return true;
1300
+ }
1301
+ if (PS_VERB_RE.test(trimmed)) return true;
1302
+ if (/(?:^|[\s;&|])(gci|gi|gp|gcm|gps|sl|rm|cat|cp|mv)\b/i.test(trimmed)) {
1303
+ return true;
1304
+ }
1305
+ if (looksLikePowerShellExtended(command)) return true;
1306
+ return false;
1307
+ }
1308
+ function looksLikePowerShellExtended(command) {
1309
+ if (!command) return false;
1310
+ const trimmed = command.trimStart();
1311
+ if (/(?:^|\s)[-/](?:WhatIf|Confirm|ErrorAction)(?::[^\s]+|\s|=|$)/i.test(trimmed)) {
1312
+ return true;
1313
+ }
1314
+ if (/(?:^|[\s;&|])(Where-Object|ForEach-Object|Select-Object|Sort-Object|Group-Object|Measure-Object|Compare-Object|Tee-Object)(?:\s|$)/i.test(trimmed)) {
1315
+ return true;
1316
+ }
1317
+ if (/\bWrite-(?:Host|Output|Error|Warning|Verbose|Debug|Information)(?:\s|$)/i.test(trimmed)) {
1318
+ return true;
1319
+ }
1320
+ if (/HK(?:LM|CU|CR|U|CC|DD|PD):\\/i.test(trimmed)) return true;
1321
+ if (/\[(?:string|int|bool|xml|double|float|decimal|char|byte|long|System\.)/i.test(trimmed)) {
1322
+ return true;
1323
+ }
1324
+ if (/^\s*<#|#>\s*$/m.test(trimmed)) return true;
1325
+ if (/(?:^|\s)[-/\/](?:AsPlainText|PipelineVariable|pv|FilterHashtable|OutVariable|ov)(?:\s|=|$)/i.test(trimmed)) {
1326
+ return true;
1327
+ }
1328
+ return false;
1329
+ }
1330
+ function wrapPowerShellScript(command) {
1331
+ const bootstrap = "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8;$ConfirmPreference='None';$WhatIfPreference=$false";
1332
+ return "\uFEFF" + bootstrap + "\ntry {\n" + command + "\n} finally { exit $LASTEXITCODE }";
1333
+ }
1334
+ var PS_VERB_RE = new RegExp(
1335
+ // Boundaries: start-of-string, whitespace, `;`, `&`, `|`, `(`, `{`, `,`.
1336
+ "(?:^|[\\s;&|\\(\\{,])(?:Get|Set|New|Remove|Add|Clear|Copy|Move|Rename|Test|Update|Write|Read|Push|Pop|Invoke|Start|Stop|Wait|Out|Format|Group|Measure|Compare|Resolve|ConvertTo|ConvertFrom|Convert|Import|Export|Select|Where|ForEach|Sort|Tee|Split|Join|Limit|Skip|Step|Trace|Debug|Register|Unregister|Enable|Disable|Restart|Suspend|Resume|Save|Open|Close|Lock|Unlock|Mount|Dismount|Enter|Exit|Use|Show|Hide|Find|Search|Watch|Initialize|Optimize|Compress|Expand|Merge|Checkpoint|Undo|Redo|Approve|Deny|Block|Grant|Revoke|Assert|Confirm|Receive|Send|Connect|Disconnect|Reset|Backup|Restore|Publish|Unpublish|Install|Uninstall|Build|Rebuild|Deploy|Submit|Process|Complete|Approve|Revoke|Pay|Refund|Decline|Receive|Send)-[A-Za-z][A-Za-z0-9]+(?:[\\-\\+][A-Za-z][A-Za-z0-9]+)*(?:$|[\\s\\-\\;\\&\\|\\(\\)\\{\\},])",
1337
+ "i"
1338
+ );
1339
+ function shellArgs(shell) {
1340
+ if (shell === "powershell" || shell === "pwsh") {
1341
+ return ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", "-"];
1342
+ }
1343
+ return ["/c"];
1344
+ }
1345
+ function diagnoseBashism(command, shell) {
1346
+ if (!command) return void 0;
1347
+ const isCmd = shell === "cmd";
1348
+ const hints = [];
1349
+ const add = (h) => {
1350
+ if (!hints.includes(h)) hints.push(h);
1351
+ };
1352
+ if (/\/dev\/null/.test(command)) {
1353
+ add(
1354
+ isCmd ? "use `nul` instead of `/dev/null` (e.g. `2>nul`)" : "use `$null` instead of `/dev/null` (e.g. `2>$null`)"
1355
+ );
1356
+ }
1357
+ if (/(^|[;&|]\s*)export\s+[A-Za-z_]\w*=/.test(command)) {
1358
+ add(
1359
+ isCmd ? "set env vars with `set NAME=value`, not `export`" : "set env vars with `$env:NAME = 'value'`, not `export`"
1360
+ );
1361
+ }
1362
+ if (/<<-?\s*['"]?[A-Za-z_]\w*/.test(command)) {
1363
+ add(
1364
+ isCmd ? "cmd has no heredocs \u2014 write the content to a file or use multiple `echo` lines" : "PowerShell has no heredocs \u2014 use a single-quoted here-string `@'\u2026'@` (closing `'@` at column 0)"
1365
+ );
1366
+ }
1367
+ if (shell === "powershell" && /(&&|\|\|)/.test(command)) {
1368
+ add("Windows PowerShell 5.1 has no `&&`/`||` \u2014 separate commands with `;` (check `$LASTEXITCODE`)");
1369
+ }
1370
+ if (/\brm\s+-[A-Za-z]*[rf]/.test(command)) {
1371
+ add(
1372
+ isCmd ? "`rm` is not a cmd builtin \u2014 use `del` (files) or `rmdir /s /q` (dirs)" : "use `Remove-Item -Recurse -Force` \u2014 the `rm -rf` bash flags don't exist in PowerShell"
1373
+ );
1374
+ }
1375
+ if (/\bwhich\s+\S/.test(command)) {
1376
+ add(isCmd ? "use `where <cmd>` instead of `which`" : "use `Get-Command <cmd>` instead of `which`");
1377
+ }
1378
+ if (hints.length === 0) return void 0;
1379
+ const label = isCmd ? "cmd.exe" : shell === "pwsh" ? "PowerShell 7" : "Windows PowerShell";
1380
+ return `[wrongstack] This command failed and contains bash/POSIX syntax that ${label} does not accept \u2014 ${hints.join("; ")}. Rewrite it in ${isCmd ? "cmd" : "PowerShell"} syntax and retry.`;
1381
+ }
1382
+ function resolveWin32Command(cmd) {
1383
+ if (process.platform !== "win32") return cmd;
1384
+ if (cmd.includes("/") || cmd.includes("\\") || path.extname(cmd.replace(/\//g, "\\"))) {
1385
+ return cmd;
1386
+ }
1387
+ const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
1388
+ const pathDirs = (process.env["PATH"] ?? "").split(path.delimiter);
1389
+ for (const dir of pathDirs) {
1390
+ const base = path.join(dir, cmd);
1391
+ for (const ext of pathext) {
1392
+ const full = `${base}${ext}`;
1393
+ try {
1394
+ fs2.accessSync(full, fs2.constants.X_OK);
1395
+ return full;
1396
+ } catch {
1397
+ }
1398
+ }
1399
+ }
1400
+ return cmd;
1401
+ }
1402
+ function resolvePowerShell(cmd) {
1403
+ if (process.platform !== "win32") return cmd;
1404
+ const lower = cmd.toLowerCase();
1405
+ if (lower !== "pwsh" && lower !== "powershell" && lower !== "pwsh.exe" && lower !== "powershell.exe") {
1406
+ return resolveWin32Command(cmd);
1407
+ }
1408
+ const primary = lower.startsWith("pwsh") ? "pwsh.exe" : "powershell.exe";
1409
+ const fallback = lower.startsWith("pwsh") ? "powershell.exe" : "pwsh.exe";
1410
+ const resolved = resolveWin32Command(primary);
1411
+ if (resolved !== primary) {
1412
+ const fb = resolveWin32Command(fallback);
1413
+ return fb === fallback ? cmd : fb;
1414
+ }
1415
+ return resolved;
1416
+ }
1255
1417
 
1256
1418
  // src/bash.ts
1257
1419
  var MAX_OUTPUT = 32768;
@@ -1348,18 +1510,36 @@ var bashTool = {
1348
1510
  }
1349
1511
  const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
1350
1512
  const isWin = os2.platform() === "win32";
1351
- const shell = (() => {
1352
- const explicit = process.env[isWin ? "WRONGSTACK_COMSPEC" : "WRONGSTACK_SHELL"];
1353
- if (explicit) return explicit;
1354
- if (isWin) return process.env["COMSPEC"] ?? "cmd.exe";
1355
- const fromEnv = process.env["SHELL"];
1356
- if (fromEnv) {
1357
- const name = fromEnv.split("/").pop() ?? "";
1358
- if (["bash", "zsh", "sh", "dash", "fish"].includes(name)) return fromEnv;
1513
+ let plan;
1514
+ let winShellKind;
1515
+ if (isWin) {
1516
+ const shell2 = pickShell("win32", input.command, {
1517
+ get: (k) => process.env[k]
1518
+ });
1519
+ winShellKind = shell2;
1520
+ const bin = shell2 === "powershell" ? resolvePowerShell("powershell.exe") : shell2 === "pwsh" ? resolvePowerShell("pwsh.exe") : process.env["COMSPEC"] ?? "cmd.exe";
1521
+ plan = {
1522
+ bin,
1523
+ argv: shellArgs(shell2),
1524
+ useStdin: shell2 === "powershell" || shell2 === "pwsh",
1525
+ stdinBody: shell2 === "powershell" || shell2 === "pwsh" ? wrapPowerShellScript(input.command) : void 0
1526
+ };
1527
+ } else {
1528
+ const explicit = process.env["WRONGSTACK_SHELL"];
1529
+ let bin;
1530
+ if (explicit) bin = explicit;
1531
+ else {
1532
+ const fromEnv = process.env["SHELL"];
1533
+ if (fromEnv) {
1534
+ const name = fromEnv.split("/").pop() ?? "";
1535
+ if (["bash", "zsh", "sh", "dash", "fish"].includes(name)) bin = fromEnv;
1536
+ else bin = "/bin/bash";
1537
+ } else bin = "/bin/bash";
1359
1538
  }
1360
- return "/bin/bash";
1361
- })();
1362
- const args = isWin ? ["/c", input.command] : ["-c", input.command];
1539
+ plan = { bin, argv: ["-c"], useStdin: false, stdinBody: void 0 };
1540
+ }
1541
+ const shell = plan.bin;
1542
+ const args = plan.useStdin ? [...plan.argv] : [...plan.argv, input.command];
1363
1543
  const env = buildChildEnv(ctx.session?.id);
1364
1544
  const detached = !isWin;
1365
1545
  const startedAt = Date.now();
@@ -1369,7 +1549,9 @@ var bashTool = {
1369
1549
  const child2 = spawn(shell, args, {
1370
1550
  cwd: ctx.projectRoot,
1371
1551
  env,
1372
- stdio: ["ignore", "pipe", "pipe"],
1552
+ // PowerShell takes the script on stdin (no argv quoting); cmd.exe
1553
+ // and POSIX shells ignore stdin when given the command inline.
1554
+ stdio: [plan.useStdin ? "pipe" : "ignore", "pipe", "pipe"],
1373
1555
  // win32: CreateProcess IGNORES CREATE_NO_WINDOW (windowsHide) when
1374
1556
  // DETACHED_PROCESS (detached: true) is set, so the console-less
1375
1557
  // cmd.exe's grandchildren (node, dev servers) each allocate a fresh
@@ -1380,6 +1562,13 @@ var bashTool = {
1380
1562
  detached: !isWin,
1381
1563
  windowsHide: true
1382
1564
  });
1565
+ if (plan.useStdin) {
1566
+ try {
1567
+ child2.stdin?.write(plan.stdinBody ?? input.command);
1568
+ child2.stdin?.end();
1569
+ } catch {
1570
+ }
1571
+ }
1383
1572
  const pid2 = child2.pid;
1384
1573
  if (typeof pid2 === "number") {
1385
1574
  registry.register({
@@ -1434,11 +1623,20 @@ var bashTool = {
1434
1623
  const child = spawn(shell, args, {
1435
1624
  cwd: ctx.projectRoot,
1436
1625
  env,
1437
- stdio: ["ignore", "pipe", "pipe"],
1626
+ // PowerShell takes the script on stdin (no argv quoting); cmd.exe
1627
+ // and POSIX shells ignore stdin when given the command inline.
1628
+ stdio: [plan.useStdin ? "pipe" : "ignore", "pipe", "pipe"],
1438
1629
  detached,
1439
1630
  windowsHide: true,
1440
1631
  ...isWin ? {} : { signal: opts.signal }
1441
1632
  });
1633
+ if (plan.useStdin) {
1634
+ try {
1635
+ child.stdin?.write(plan.stdinBody ?? input.command);
1636
+ child.stdin?.end();
1637
+ } catch {
1638
+ }
1639
+ }
1442
1640
  const pid = child.pid;
1443
1641
  if (typeof pid === "number") {
1444
1642
  registry.register({
@@ -1589,10 +1787,13 @@ var bashTool = {
1589
1787
  yield { type: "partial_output", text: remainder };
1590
1788
  }
1591
1789
  const spooled = spool.finalize();
1790
+ const hint = !timedOut && typeof c.code === "number" && c.code !== 0 && winShellKind ? diagnoseBashism(input.command, winShellKind) : void 0;
1592
1791
  yield {
1593
1792
  type: "final",
1594
1793
  output: {
1595
- output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : ""),
1794
+ output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : "") + (hint ? `
1795
+
1796
+ ${hint}` : ""),
1596
1797
  exit_code: c.code,
1597
1798
  timed_out: timedOut
1598
1799
  }