@wrongstack/tools 0.273.0 → 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';
@@ -1274,6 +1275,145 @@ async function checkAndBlockKillCommand(command) {
1274
1275
  }
1275
1276
  return { blocked: false };
1276
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
+ }
1277
1417
 
1278
1418
  // src/bash.ts
1279
1419
  var MAX_OUTPUT = 32768;
@@ -1370,18 +1510,36 @@ var bashTool = {
1370
1510
  }
1371
1511
  const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
1372
1512
  const isWin = os2.platform() === "win32";
1373
- const shell = (() => {
1374
- const explicit = process.env[isWin ? "WRONGSTACK_COMSPEC" : "WRONGSTACK_SHELL"];
1375
- if (explicit) return explicit;
1376
- if (isWin) return process.env["COMSPEC"] ?? "cmd.exe";
1377
- const fromEnv = process.env["SHELL"];
1378
- if (fromEnv) {
1379
- const name = fromEnv.split("/").pop() ?? "";
1380
- 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";
1381
1538
  }
1382
- return "/bin/bash";
1383
- })();
1384
- 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];
1385
1543
  const env = buildChildEnv(ctx.session?.id);
1386
1544
  const detached = !isWin;
1387
1545
  const startedAt = Date.now();
@@ -1391,7 +1549,9 @@ var bashTool = {
1391
1549
  const child2 = spawn(shell, args, {
1392
1550
  cwd: ctx.projectRoot,
1393
1551
  env,
1394
- 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"],
1395
1555
  // win32: CreateProcess IGNORES CREATE_NO_WINDOW (windowsHide) when
1396
1556
  // DETACHED_PROCESS (detached: true) is set, so the console-less
1397
1557
  // cmd.exe's grandchildren (node, dev servers) each allocate a fresh
@@ -1402,6 +1562,13 @@ var bashTool = {
1402
1562
  detached: !isWin,
1403
1563
  windowsHide: true
1404
1564
  });
1565
+ if (plan.useStdin) {
1566
+ try {
1567
+ child2.stdin?.write(plan.stdinBody ?? input.command);
1568
+ child2.stdin?.end();
1569
+ } catch {
1570
+ }
1571
+ }
1405
1572
  const pid2 = child2.pid;
1406
1573
  if (typeof pid2 === "number") {
1407
1574
  registry.register({
@@ -1456,11 +1623,20 @@ var bashTool = {
1456
1623
  const child = spawn(shell, args, {
1457
1624
  cwd: ctx.projectRoot,
1458
1625
  env,
1459
- 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"],
1460
1629
  detached,
1461
1630
  windowsHide: true,
1462
1631
  ...isWin ? {} : { signal: opts.signal }
1463
1632
  });
1633
+ if (plan.useStdin) {
1634
+ try {
1635
+ child.stdin?.write(plan.stdinBody ?? input.command);
1636
+ child.stdin?.end();
1637
+ } catch {
1638
+ }
1639
+ }
1464
1640
  const pid = child.pid;
1465
1641
  if (typeof pid === "number") {
1466
1642
  registry.register({
@@ -1611,10 +1787,13 @@ var bashTool = {
1611
1787
  yield { type: "partial_output", text: remainder };
1612
1788
  }
1613
1789
  const spooled = spool.finalize();
1790
+ const hint = !timedOut && typeof c.code === "number" && c.code !== 0 && winShellKind ? diagnoseBashism(input.command, winShellKind) : void 0;
1614
1791
  yield {
1615
1792
  type: "final",
1616
1793
  output: {
1617
- output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : ""),
1794
+ output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : "") + (hint ? `
1795
+
1796
+ ${hint}` : ""),
1618
1797
  exit_code: c.code,
1619
1798
  timed_out: timedOut
1620
1799
  }