@wrongstack/tools 0.274.0 → 0.275.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/install.js CHANGED
@@ -269,6 +269,7 @@ var CircuitBreaker = class {
269
269
  if (this.state === "open") return;
270
270
  this.state = "open";
271
271
  this.openedAt = Date.now();
272
+ this.window = [];
272
273
  try {
273
274
  this.onTrip?.();
274
275
  } catch {
@@ -308,7 +309,7 @@ var SENSITIVE_FLAG_PATTERNS = [
308
309
  /--(?: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,
309
310
  // -f "value" style short flags
310
311
  /(?<!\w)-t(?:\s+|\s*=\s*)[^\s,]+/,
311
- /(?<!\w)-p(?:ssword)?(?:\s+|\s*=\s*)[^\s,]+/gi,
312
+ /(?<!\w)-(?:p|password)(?:\s+|\s*=\s*)[^\s,]+/gi,
312
313
  // env var–style secrets: TOKEN=x, API_KEY=y, etc.
313
314
  /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
314
315
  // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
@@ -369,12 +370,35 @@ var ProcessRegistryImpl = class {
369
370
  register(info) {
370
371
  this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
371
372
  }
373
+ _isSafeSignalPid(pid) {
374
+ return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
375
+ }
376
+ _canSignalProcessGroup(p) {
377
+ return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && typeof p.child.pid === "number" && p.child.pid === p.pid;
378
+ }
379
+ _killChildDirect(p, signal) {
380
+ try {
381
+ p.child.kill(signal);
382
+ } catch {
383
+ }
384
+ }
385
+ _killPosix(p, signal) {
386
+ if (this._canSignalProcessGroup(p)) {
387
+ try {
388
+ process.kill(-p.pid, signal);
389
+ return;
390
+ } catch {
391
+ }
392
+ }
393
+ this._killChildDirect(p, signal);
394
+ }
372
395
  /** Unregister a process by PID. Called on 'close' / 'exit' events. */
373
396
  unregister(pid) {
374
397
  this.processes.delete(pid);
375
398
  }
376
399
  /** Get a single process by PID. */
377
400
  get(pid) {
401
+ this._pruneStale(pid);
378
402
  return this.processes.get(pid);
379
403
  }
380
404
  /** Get all tracked processes. */
@@ -538,6 +562,7 @@ var ProcessRegistryImpl = class {
538
562
  * Returns true if the process was found and kill was attempted.
539
563
  */
540
564
  kill(pid, opts = {}) {
565
+ this._pruneStale(pid);
541
566
  const p = this.processes.get(pid);
542
567
  if (!p) return false;
543
568
  if (p.killed) return true;
@@ -567,27 +592,12 @@ var ProcessRegistryImpl = class {
567
592
  }
568
593
  try {
569
594
  if (force) {
570
- try {
571
- process.kill(-pid, "SIGKILL");
572
- } catch {
573
- p.child.kill("SIGKILL");
574
- }
595
+ this._killPosix(p, "SIGKILL");
575
596
  } else {
576
- try {
577
- process.kill(-pid, "SIGTERM");
578
- } catch {
579
- p.child.kill("SIGTERM");
580
- }
597
+ this._killPosix(p, "SIGTERM");
581
598
  const timer = setTimeout(() => {
582
599
  if (this.processes.has(pid) && !p.child.killed) {
583
- try {
584
- process.kill(-pid, "SIGKILL");
585
- } catch {
586
- try {
587
- p.child.kill("SIGKILL");
588
- } catch {
589
- }
590
- }
600
+ this._killPosix(p, "SIGKILL");
591
601
  }
592
602
  }, graceMs);
593
603
  timer.unref?.();
@@ -622,6 +632,34 @@ var ProcessRegistryImpl = class {
622
632
  }
623
633
  return killed;
624
634
  }
635
+ /**
636
+ * Check whether a tracked process entry is stale — the child has exited
637
+ * (exitCode !== null) AND it's been in the registry long enough that the
638
+ * OS may have reused the PID for a new, unrelated process.
639
+ *
640
+ * P3 #24 (before-release.md): on POSIX, PIDs are reused after process
641
+ * exit. If a tracked process exits but its 'close' event hasn't fired yet
642
+ * (or was missed), the registry still holds the entry. A new process
643
+ * gets the same PID, and PID-based lookups (get, kill, shouldBlockKill)
644
+ * may incorrectly protect or target the wrong process.
645
+ *
646
+ * The 60s threshold is conservative — the OS typically waits much longer
647
+ * before reusing a PID, but we want to clean up before that becomes a risk.
648
+ */
649
+ _isStaleEntry(entry) {
650
+ return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
651
+ }
652
+ /**
653
+ * Remove a stale entry for a specific PID before any PID-based lookup.
654
+ * This prevents PID reuse from causing the registry to act on a dead
655
+ * process that has been replaced by a new one with the same PID.
656
+ */
657
+ _pruneStale(pid) {
658
+ const entry = this.processes.get(pid);
659
+ if (entry && this._isStaleEntry(entry)) {
660
+ this.processes.delete(pid);
661
+ }
662
+ }
625
663
  };
626
664
  var _registry;
627
665
  function getProcessRegistry() {
@@ -1055,6 +1093,14 @@ var installTool = {
1055
1093
  }
1056
1094
  }
1057
1095
  }
1096
+ ctx.recordSideEffect?.({
1097
+ toolUseId: `install-${Date.now()}`,
1098
+ toolName: "install",
1099
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1100
+ input: { packages: pkgList, cwd, dry_run: Boolean(input.dry_run) },
1101
+ outcome: output.dry_run ? "dry run" : result.exitCode === 0 ? `installed ${pkgList.length || "all"} packages` : `failed (exit ${result.exitCode})`,
1102
+ risk: "package"
1103
+ });
1058
1104
  yield { type: "final", output };
1059
1105
  }
1060
1106
  };