@huseynsnmz/pi-board 0.15.1 → 0.16.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.
Files changed (215) hide show
  1. package/CHANGELOG.md +26 -4
  2. package/CONTRIBUTING.md +7 -0
  3. package/README.md +53 -419
  4. package/SECURITY.md +19 -0
  5. package/docs/how-it-works.md +31 -0
  6. package/docs/reference.md +276 -0
  7. package/package.json +18 -19
  8. package/src/index.ts +9 -1
  9. package/src/runtime/service.mjs +12 -0
  10. package/src/ui/dashboard.ts +14 -8
  11. package/src/ui/double-left.ts +5 -1
  12. package/src/ui/focus.ts +7 -1
  13. package/src/ui/waiting.ts +10 -5
  14. package/IMPLEMENTATION_PLAN.md +0 -920
  15. package/PRD.md +0 -484
  16. package/PROGRESS.md +0 -142
  17. package/VERIFY.md +0 -115
  18. package/docs/BATCH_SELECTION_READ_FLOW.md +0 -277
  19. package/docs/EXPLORATION.md +0 -187
  20. package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +0 -581
  21. package/docs/RELEASE.md +0 -148
  22. package/docs/superpowers/plans/2026-08-15-screenlog-gc.md +0 -704
  23. package/docs/superpowers/plans/2026-08-16-attach-double-cursor-jiggle-retry.md +0 -499
  24. package/docs/superpowers/plans/2026-08-21-attach-coldstart-jiggle-rearm.md +0 -797
  25. package/docs/superpowers/plans/2026-08-21-dashboard-keypress-lag.md +0 -366
  26. package/docs/superpowers/plans/2026-08-21-issue-14-needs-input-no-auto-done.md +0 -576
  27. package/docs/superpowers/plans/2026-08-22-jiggle-shrink-and-hold.md +0 -337
  28. package/docs/superpowers/plans/2026-08-22-launch-cwd-favorites.md +0 -916
  29. package/docs/superpowers/plans/2026-08-22-question-tool-grouping.md +0 -211
  30. package/docs/superpowers/plans/2026-08-22-rename-state-labels.md +0 -182
  31. package/docs/superpowers/plans/2026-08-22-stable-list-order.md +0 -51
  32. package/docs/superpowers/plans/2026-08-27-locks-acquirelock-spin.md +0 -614
  33. package/docs/superpowers/plans/2026-08-27-pty-runner-test-flaky.md +0 -32
  34. package/docs/superpowers/plans/2026-08-29-code-refs-badges.md +0 -223
  35. package/docs/superpowers/plans/2026-08-30-circular-navigation.md +0 -308
  36. package/docs/superpowers/plans/2026-08-30-post-exit-timing-fix.md +0 -37
  37. package/docs/superpowers/plans/2026-08-30-pty-attach-quality-debt.md +0 -311
  38. package/docs/superpowers/plans/2026-08-30-readme-v2.md +0 -294
  39. package/docs/superpowers/plans/2026-09-01-attach-detach-gate-cursor-anchor.md +0 -284
  40. package/docs/superpowers/plans/2026-09-02-attach-detach-editor-state.md +0 -722
  41. package/docs/superpowers/plans/2026-09-03-code-refs-pr-backlink-narrow.md +0 -551
  42. package/docs/superpowers/plans/2026-09-03-detach-gate-glyph-fallback.md +0 -146
  43. package/docs/superpowers/plans/2026-09-04-evidence-outputpreview.md +0 -209
  44. package/docs/superpowers/plans/2026-09-04-warm-host-reclaim.md +0 -796
  45. package/docs/superpowers/plans/2026-09-05-issue-13-drainnextfollowup-pty-probe.md +0 -114
  46. package/docs/superpowers/plans/2026-09-05-issue-38-windows-wezterm-ime-cursor.md +0 -73
  47. package/docs/superpowers/plans/2026-09-05-issue-39-truncate-codepoint-boundary.md +0 -143
  48. package/docs/superpowers/plans/2026-09-05-issue-61-mention-fallback-guards.md +0 -226
  49. package/docs/superpowers/plans/2026-09-05-issue-63-flaky-manual-completion.md +0 -87
  50. package/docs/superpowers/plans/2026-09-05-issue-64-changelog-release-helper.md +0 -53
  51. package/docs/superpowers/plans/2026-09-05-pty-host-stacking-sock-race.md +0 -731
  52. package/docs/superpowers/plans/2026-09-08-attach-ctrl-left-detach.md +0 -30
  53. package/docs/superpowers/plans/2026-09-08-dashboard-shrink-repaint.md +0 -68
  54. package/docs/superpowers/plans/2026-09-08-issue-11-attach-runtime-desync-heal.md +0 -917
  55. package/docs/superpowers/plans/2026-09-08-legacy-stale-host-recovery.md +0 -125
  56. package/docs/superpowers/plans/2026-09-08-spawn-async-error-swallow.md +0 -56
  57. package/docs/superpowers/plans/2026-09-08-stale-model-attach-guard.md +0 -96
  58. package/docs/superpowers/plans/2026-09-09-claimpid-blocks-replace.md +0 -267
  59. package/docs/superpowers/plans/2026-09-09-harden-runner-architecture.md +0 -603
  60. package/docs/superpowers/plans/2026-09-09-single-writer-completion.md +0 -252
  61. package/docs/superpowers/plans/2026-09-10-reader-consistency.md +0 -115
  62. package/docs/superpowers/plans/2026-09-14-attach-cursor-dectcem-gate.md +0 -469
  63. package/docs/superpowers/plans/2026-09-14-attach-snapshot.md +0 -92
  64. package/docs/superpowers/plans/2026-09-14-host-meta-orphan-lock.md +0 -771
  65. package/docs/superpowers/plans/2026-09-14-issue-106-terminal-frame-cognition.md +0 -299
  66. package/docs/superpowers/plans/2026-09-14-issue-113-foreground-preview-race.md +0 -609
  67. package/docs/superpowers/plans/2026-09-14-terminal-model.md +0 -145
  68. package/docs/superpowers/plans/2026-09-15-coordinator-pipe-root-normalize.md +0 -224
  69. package/docs/superpowers/plans/2026-09-15-lease-publish-eprem-reclaim.md +0 -341
  70. package/docs/superpowers/plans/2026-09-18-detach-anchor-reporter-endpoint.md +0 -875
  71. package/docs/superpowers/plans/2026-09-20-control-lifecycle.md +0 -116
  72. package/docs/superpowers/plans/2026-09-20-issue-121-perf-gate-out-of-coverage.md +0 -517
  73. package/docs/superpowers/plans/2026-09-23-a5-mid-stream-assertion.md +0 -25
  74. package/docs/superpowers/plans/2026-09-23-cwd-browse-tab-drill.md +0 -393
  75. package/docs/superpowers/plans/2026-09-23-terminal-query-forwarding.md +0 -51
  76. package/docs/superpowers/plans/2026-09-23-unconditional-detach-key.md +0 -58
  77. package/docs/superpowers/plans/2026-09-24-control-reconcile-flake.md +0 -698
  78. package/docs/superpowers/plans/2026-09-24-probe-attached-clients.md +0 -605
  79. package/docs/superpowers/plans/2026-09-29-issue-145-holding-semantic-state.md +0 -1183
  80. package/docs/superpowers/plans/2026-09-29-issue-145-user-verification.md +0 -66
  81. package/docs/superpowers/plans/2026-09-30-board-delete-key-x-y-confirm.md +0 -649
  82. package/docs/superpowers/plans/2026-09-30-issue-153-runner-stop-boot-window.md +0 -618
  83. package/docs/superpowers/plans/2026-09-30-issue-95-test-discipline.md +0 -555
  84. package/docs/superpowers/plans/2026-09-30-publish-dispatch-tag-check.md +0 -259
  85. package/docs/superpowers/plans/2026-10-06-attach-settle-scheme-delivery.md +0 -751
  86. package/docs/superpowers/plans/2026-10-09-issue-167-attach-middle-paste.md +0 -509
  87. package/docs/superpowers/plans/2026-10-09-issue-169-fullscreen-mouse-ownership.md +0 -346
  88. package/docs/superpowers/plans/2026-10-10-attach-delete-in-place.md +0 -832
  89. package/docs/superpowers/plans/2026-10-10-issue-174-selection-mirror-paste-source.md +0 -510
  90. package/docs/superpowers/plans/2026-10-14-issue-170-attach-reply-guard.md +0 -771
  91. package/docs/superpowers/plans/2026-10-14-issue-178-accept-registration-race.md +0 -1257
  92. package/docs/superpowers/plans/2026-10-14-issue-184-snapshot-window-contract.md +0 -657
  93. package/docs/superpowers/plans/2026-10-14-issue-185-attach-wheel-alt-screen.md +0 -47
  94. package/docs/superpowers/plans/2026-10-14-issue-187-motionless-drag.md +0 -931
  95. package/docs/superpowers/plans/2026-10-14-issue-189-mixed-input-segments.md +0 -823
  96. package/docs/superpowers/plans/2026-10-15-issue-188-x10-mouse-release.md +0 -526
  97. package/docs/superpowers/plans/2026-10-16-issue-198-attach-wheel-forward-fallback.md +0 -32
  98. package/docs/superpowers/plans/2026-10-18-attach-wheel-replay-pseudo-scrollback.md +0 -437
  99. package/docs/superpowers/plans/2026-10-19-issue-213-reply-guard-csi-key.md +0 -257
  100. package/docs/superpowers/plans/2026-10-19-issue-214-attach-forward-alt-modifier.md +0 -202
  101. package/docs/superpowers/plans/2026-10-21-issue-221-ask-user-question.md +0 -198
  102. package/docs/superpowers/plans/2026-10-23-issue-222-attach-ready.md +0 -290
  103. package/docs/superpowers/plans/2026-10-23-issue-225-monotonic-foreground-state.md +0 -450
  104. package/docs/superpowers/plans/2026-10-23-issue-229-vim-jkl.md +0 -449
  105. package/docs/superpowers/plans/2026-10-23-issue-232-attach-bg-only-cells.md +0 -153
  106. package/docs/superpowers/plans/2026-10-23-lease-heartbeat-staleness.md +0 -537
  107. package/docs/superpowers/plans/2026-10-23-selected-row-highlight.md +0 -405
  108. package/docs/superpowers/plans/2026-10-24-issue-217-windows-pid-reuse-stopping.md +0 -367
  109. package/docs/superpowers/plans/2026-10-24-issue-230-warm-host-background-runs.md +0 -806
  110. package/docs/superpowers/plans/2026-10-24-issue-238-windows-process-identity-token.md +0 -490
  111. package/docs/superpowers/plans/2026-10-24-issue-241-coordinator-root-exit.md +0 -461
  112. package/docs/superpowers/plans/2026-10-25-issue-239-coordinator-replacement-identity.md +0 -504
  113. package/docs/superpowers/plans/2026-10-26-issue-245-key-hints-doc-sync.md +0 -244
  114. package/docs/superpowers/plans/2026-10-27-issue-172-fullscreen-selection-clear.md +0 -344
  115. package/docs/superpowers/plans/2026-10-27-issue-252-select-key-hints.md +0 -209
  116. package/docs/superpowers/plans/2026-10-28-issue-255-coordinator-endpoint-zombie.md +0 -442
  117. package/docs/superpowers/plans/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup.md +0 -256
  118. package/docs/superpowers/plans/2026-11-03-cwd-favorites-full-pool.md +0 -266
  119. package/docs/superpowers/specs/2026-08-15-screenlog-gc-design.md +0 -105
  120. package/docs/superpowers/specs/2026-08-16-attach-double-cursor-jiggle-retry-design.md +0 -142
  121. package/docs/superpowers/specs/2026-08-21-attach-coldstart-jiggle-rearm-design.md +0 -110
  122. package/docs/superpowers/specs/2026-08-21-dashboard-keypress-lag-design.md +0 -59
  123. package/docs/superpowers/specs/2026-08-21-issue-14-needs-input-no-auto-done-design.md +0 -109
  124. package/docs/superpowers/specs/2026-08-22-cwd-favorites-design.md +0 -131
  125. package/docs/superpowers/specs/2026-08-22-jiggle-shrink-and-hold-design.md +0 -111
  126. package/docs/superpowers/specs/2026-08-22-question-tool-grouping-design.md +0 -114
  127. package/docs/superpowers/specs/2026-08-22-rename-state-labels-design.md +0 -80
  128. package/docs/superpowers/specs/2026-08-27-locks-acquirelock-spin-design.md +0 -97
  129. package/docs/superpowers/specs/2026-08-27-pty-runner-test-flaky-design.md +0 -35
  130. package/docs/superpowers/specs/2026-08-29-code-refs-badges-design.md +0 -128
  131. package/docs/superpowers/specs/2026-08-30-circular-navigation-design.md +0 -47
  132. package/docs/superpowers/specs/2026-08-30-eprm-atomicwrite-race-design.md +0 -77
  133. package/docs/superpowers/specs/2026-08-30-post-exit-timing-fix-design.md +0 -80
  134. package/docs/superpowers/specs/2026-08-30-pty-attach-quality-debt-design.md +0 -105
  135. package/docs/superpowers/specs/2026-08-30-readme-v2-design.md +0 -115
  136. package/docs/superpowers/specs/2026-09-01-attach-detach-gate-cursor-anchor-design.md +0 -78
  137. package/docs/superpowers/specs/2026-09-02-attach-detach-editor-state-design.md +0 -120
  138. package/docs/superpowers/specs/2026-09-03-code-refs-pr-backlink-narrow-design.md +0 -92
  139. package/docs/superpowers/specs/2026-09-03-detach-gate-glyph-fallback-design.md +0 -99
  140. package/docs/superpowers/specs/2026-09-04-evidence-outputpreview-design.md +0 -50
  141. package/docs/superpowers/specs/2026-09-04-warm-host-reclaim-design.md +0 -106
  142. package/docs/superpowers/specs/2026-09-05-issue-13-drainnextfollowup-pty-probe-design.md +0 -64
  143. package/docs/superpowers/specs/2026-09-05-issue-38-windows-wezterm-ime-design.md +0 -48
  144. package/docs/superpowers/specs/2026-09-05-issue-39-truncate-codepoint-boundary-design.md +0 -64
  145. package/docs/superpowers/specs/2026-09-05-issue-61-mention-fallback-design.md +0 -71
  146. package/docs/superpowers/specs/2026-09-05-issue-63-flaky-manual-completion-design.md +0 -49
  147. package/docs/superpowers/specs/2026-09-05-issue-64-changelog-helper-design.md +0 -76
  148. package/docs/superpowers/specs/2026-09-05-pty-host-stacking-sock-race-design.md +0 -510
  149. package/docs/superpowers/specs/2026-09-07-issue-11-attach-runtime-desync-heal-design.md +0 -130
  150. package/docs/superpowers/specs/2026-09-08-attach-ctrl-left-detach-design.md +0 -58
  151. package/docs/superpowers/specs/2026-09-08-dashboard-shrink-repaint-design.md +0 -52
  152. package/docs/superpowers/specs/2026-09-08-legacy-stale-host-recovery-design.md +0 -87
  153. package/docs/superpowers/specs/2026-09-08-spawn-async-error-swallow-design.md +0 -56
  154. package/docs/superpowers/specs/2026-09-08-stale-model-attach-guard-design.md +0 -79
  155. package/docs/superpowers/specs/2026-09-09-claimpid-blocks-replace-design.md +0 -147
  156. package/docs/superpowers/specs/2026-09-09-harden-runner-architecture-design.md +0 -304
  157. package/docs/superpowers/specs/2026-09-14-attach-cursor-dectcem-gate-design.md +0 -114
  158. package/docs/superpowers/specs/2026-09-14-host-meta-orphan-lock-design.md +0 -120
  159. package/docs/superpowers/specs/2026-09-14-issue-106-terminal-frame-cognition-design.md +0 -146
  160. package/docs/superpowers/specs/2026-09-14-issue-113-foreground-preview-race-design.md +0 -116
  161. package/docs/superpowers/specs/2026-09-15-coordinator-pipe-root-normalize-design.md +0 -84
  162. package/docs/superpowers/specs/2026-09-15-lease-publish-eprem-reclaim-design.md +0 -92
  163. package/docs/superpowers/specs/2026-09-18-detach-anchor-reporter-endpoint-design.md +0 -123
  164. package/docs/superpowers/specs/2026-09-20-issue-121-perf-gate-out-of-coverage-design.md +0 -204
  165. package/docs/superpowers/specs/2026-09-23-a5-mid-stream-assertion-design.md +0 -43
  166. package/docs/superpowers/specs/2026-09-23-cwd-browse-tab-drill-design.md +0 -95
  167. package/docs/superpowers/specs/2026-09-23-terminal-query-forwarding-design.md +0 -63
  168. package/docs/superpowers/specs/2026-09-23-unconditional-detach-key-design.md +0 -57
  169. package/docs/superpowers/specs/2026-09-24-control-reconcile-flake-design.md +0 -98
  170. package/docs/superpowers/specs/2026-09-24-probe-attached-clients-design.md +0 -190
  171. package/docs/superpowers/specs/2026-09-29-issue-145-holding-semantic-state-design.md +0 -394
  172. package/docs/superpowers/specs/2026-09-30-board-delete-key-x-y-confirm-design.md +0 -86
  173. package/docs/superpowers/specs/2026-09-30-issue-153-runner-stop-boot-window-design.md +0 -75
  174. package/docs/superpowers/specs/2026-09-30-issue-95-test-discipline-design.md +0 -226
  175. package/docs/superpowers/specs/2026-09-30-publish-dispatch-tag-check-design.md +0 -129
  176. package/docs/superpowers/specs/2026-10-06-attach-settle-scheme-delivery-design.md +0 -196
  177. package/docs/superpowers/specs/2026-10-09-issue-167-attach-middle-paste-design.md +0 -125
  178. package/docs/superpowers/specs/2026-10-09-issue-169-fullscreen-mouse-ownership-design.md +0 -130
  179. package/docs/superpowers/specs/2026-10-10-attach-delete-in-place-design.md +0 -224
  180. package/docs/superpowers/specs/2026-10-10-issue-174-selection-mirror-paste-source-design.md +0 -154
  181. package/docs/superpowers/specs/2026-10-14-issue-170-attach-reply-guard-design.md +0 -195
  182. package/docs/superpowers/specs/2026-10-14-issue-178-accept-registration-race-design.md +0 -287
  183. package/docs/superpowers/specs/2026-10-14-issue-184-snapshot-window-contract-design.md +0 -288
  184. package/docs/superpowers/specs/2026-10-14-issue-185-attach-wheel-alt-screen-design.md +0 -126
  185. package/docs/superpowers/specs/2026-10-14-issue-187-motionless-drag-design.md +0 -269
  186. package/docs/superpowers/specs/2026-10-14-issue-189-mixed-input-segments-design.md +0 -369
  187. package/docs/superpowers/specs/2026-10-15-issue-188-x10-mouse-release-design.md +0 -252
  188. package/docs/superpowers/specs/2026-10-16-issue-198-attach-wheel-forward-fallback-design.md +0 -67
  189. package/docs/superpowers/specs/2026-10-18-attach-wheel-replay-pseudo-scrollback-design.md +0 -250
  190. package/docs/superpowers/specs/2026-10-19-issue-213-reply-guard-csi-key-design.md +0 -165
  191. package/docs/superpowers/specs/2026-10-19-issue-214-attach-forward-alt-modifier-design.md +0 -131
  192. package/docs/superpowers/specs/2026-10-20-issue-221-ask-user-question-design.md +0 -270
  193. package/docs/superpowers/specs/2026-10-22-issue-225-monotonic-foreground-state-design.md +0 -225
  194. package/docs/superpowers/specs/2026-10-23-issue-222-attach-ready-design.md +0 -204
  195. package/docs/superpowers/specs/2026-10-23-issue-229-vim-jkl-design.md +0 -90
  196. package/docs/superpowers/specs/2026-10-23-issue-232-attach-bg-only-cells-design.md +0 -146
  197. package/docs/superpowers/specs/2026-10-23-lease-heartbeat-staleness-design.md +0 -271
  198. package/docs/superpowers/specs/2026-10-23-selected-row-highlight-design.md +0 -191
  199. package/docs/superpowers/specs/2026-10-24-issue-217-windows-pid-reuse-stopping-design.md +0 -334
  200. package/docs/superpowers/specs/2026-10-24-issue-230-warm-host-background-runs-design.md +0 -283
  201. package/docs/superpowers/specs/2026-10-24-issue-238-windows-process-identity-token-design.md +0 -286
  202. package/docs/superpowers/specs/2026-10-24-issue-241-coordinator-root-exit-design.md +0 -381
  203. package/docs/superpowers/specs/2026-10-25-issue-239-coordinator-replacement-identity-design.md +0 -331
  204. package/docs/superpowers/specs/2026-10-26-issue-245-key-hints-doc-sync-design.md +0 -72
  205. package/docs/superpowers/specs/2026-10-27-issue-172-fullscreen-selection-clear-design.md +0 -248
  206. package/docs/superpowers/specs/2026-10-27-issue-252-select-key-hints-design.md +0 -95
  207. package/docs/superpowers/specs/2026-10-28-issue-255-coordinator-endpoint-zombie-design.md +0 -380
  208. package/docs/superpowers/specs/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup-design.md +0 -197
  209. package/docs/superpowers/specs/2026-11-03-cwd-favorites-full-pool-design.md +0 -106
  210. package/scripts/budget-audit.mjs +0 -135
  211. package/scripts/patch-vulns.mjs +0 -59
  212. package/scripts/release.mjs +0 -446
  213. package/scripts/release_helper.mjs +0 -309
  214. package/scripts/run-perf-gate.mjs +0 -40
  215. package/scripts/run-tests.mjs +0 -375
@@ -1,375 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Suite gate for issue #241: run every test file through `node --test` and fail
4
- * the run when a detached state-coordinator leaked past the test process.
5
- *
6
- * Why this exists: every coordinator integration test creates a throwaway board
7
- * root under the OS temp dir and spawns a detached coordinator. A coordinator
8
- * that fails to step down stays alive forever and pins its root, so leaks
9
- * accumulate on a developer box instead of showing up as a test failure.
10
- *
11
- * Design (spec §3.3):
12
- * - Snapshot the matching coordinators BEFORE the run. Pre-existing ones are
13
- * warn-only: they are not this run's fault and are never killed.
14
- * - After the run, poll up to GRACE_MS for every coordinator above the snapshot
15
- * to be gone (the 1 s self-exit window plus load slack).
16
- * - Survivors: print pid + root, SIGTERM, wait 2 s, SIGKILL leftovers, exit 1.
17
- * - A test run killed by a signal (`exitCode === null`) is never a success.
18
- * - Enumeration unavailable: warn and skip the residue check (fail-open). A
19
- * missing `pwsh`/`powershell`/`ps` must not turn into a red suite.
20
- *
21
- * Selection is deliberately narrow: the command line must contain THIS
22
- * checkout's absolute `runner/state-coordinator.mjs` and its root argument must
23
- * resolve under `os.tmpdir()`. Another checkout's test coordinator never
24
- * matches (its script path differs), and a default production board
25
- * (`~/.pi/agent/agent-board`) never matches either. Accepted limit (spec §7): a
26
- * board whose `AGENT_BOARD_ROOT` points under `os.tmpdir()` and whose runner is
27
- * this checkout is indistinguishable from a test coordinator, so the gate would
28
- * kill that live board.
29
- *
30
- * Known limit (final-review m7): POSIX selection is token-based and `ps` output
31
- * is unquoted, so a checkout path containing spaces splits and its coordinators
32
- * are not selected (the gate silently no-ops instead of failing). win32 CIM
33
- * output quotes paths with spaces, so the tokenizer handles it there.
34
- *
35
- * This module must stay import-side-effect free: `test/run-tests-wrapper.test.mjs`
36
- * imports the pure functions and `runSuite`, and a spurious `main()` at import
37
- * time would recursively start the whole suite. Only the entry-point guard at
38
- * the bottom of this file may call `main()`.
39
- */
40
- import { execFileSync, spawn } from "node:child_process";
41
- import { readdirSync } from "node:fs";
42
- import { tmpdir } from "node:os";
43
- import { join, posix, resolve } from "node:path";
44
- import { fileURLToPath, pathToFileURL } from "node:url";
45
-
46
- /** The only coordinator script this checkout's suite spawns. */
47
- const COORDINATOR_RELATIVE_SCRIPT = "runner/state-coordinator.mjs";
48
- const DEFAULT_GRACE_MS = 10_000;
49
- const DEFAULT_POLL_MS = 100;
50
- /** SIGTERM → SIGKILL pause. On win32 SIGTERM is TerminateProcess; POSIX needs the grace. */
51
- const SIGKILL_DELAY_MS = 2_000;
52
- const SIGTERM = "SIGTERM";
53
- const SIGKILL = "SIGKILL";
54
-
55
- /**
56
- * Normalize a path for comparison without depending on the host platform:
57
- * forward slashes, dot segments removed, no trailing separator (a `tmpRoot` like
58
- * `C:\Temp\` or `/tmp/` must select exactly like its un-suffixed form), and (on
59
- * win32) case-folded. Filesystem roots keep their separator (`/` stays `/`,
60
- * `C:/` stays `C:/`): stripping it would turn a drive root into the
61
- * drive-relative `C:`. The platform is explicit so the unit tests can exercise
62
- * win32 samples on POSIX CI.
63
- * @param {string} value
64
- * @param {string} platform
65
- * @returns {string}
66
- */
67
- function canonicalPath(value, platform) {
68
- const text = String(value ?? "").replace(/\\/g, "/");
69
- if (!text) return "";
70
- let normalized = posix.normalize(text);
71
- if (normalized.length > 1 && normalized.endsWith("/")) {
72
- const trimmed = normalized.replace(/\/+$/, "");
73
- normalized = trimmed.endsWith(":") ? `${trimmed}/` : trimmed;
74
- }
75
- return platform === "win32" ? normalized.toLowerCase() : normalized;
76
- }
77
-
78
- /**
79
- * True when `candidate` is `directory` itself or one of its descendants, using
80
- * the already-canonical spellings. The trailing-slash check keeps a sibling
81
- * such as `/tmpfoo` from matching `/tmp`; a filesystem root already ends in the
82
- * separator, so `${directory}/` would become `//` and match nothing there.
83
- * @param {string} candidate
84
- * @param {string} directory
85
- * @returns {boolean}
86
- */
87
- function isUnderDirectory(candidate, directory) {
88
- if (!candidate || !directory) return false;
89
- if (candidate === directory) return true;
90
- return candidate.startsWith(directory.endsWith("/") ? directory : `${directory}/`);
91
- }
92
-
93
- /**
94
- * Split a process command line into argv-shaped tokens, honoring the double
95
- * quotes CIM emits around paths with spaces. Node's own argv never needs
96
- * backslash-escaped quotes inside a path, so this covers the coordinator shape.
97
- * @param {string} commandLine
98
- * @returns {string[]}
99
- */
100
- function tokenizeCommandLine(commandLine) {
101
- const tokens = [];
102
- for (const match of commandLine.matchAll(/"([^"]*)"|(\S+)/g)) tokens.push(match[1] ?? match[2]);
103
- return tokens;
104
- }
105
-
106
- /**
107
- * Parse the platform's process-enumeration output. Never throws: unparsable
108
- * output and unparsable lines are skipped.
109
- * @param {string} raw
110
- * @param {string} platform
111
- * @returns {Array<{pid: number, commandLine: string}>}
112
- */
113
- export function parseProcessList(raw, platform) {
114
- const text = String(raw ?? "").trim();
115
- if (!text) return [];
116
- if (platform === "win32") {
117
- let data;
118
- try {
119
- data = JSON.parse(text);
120
- } catch {
121
- return [];
122
- }
123
- // ConvertTo-Json emits a single object when exactly one process matches.
124
- const rows = Array.isArray(data) ? data : [data];
125
- return rows
126
- .map((row) => ({ pid: Number(row?.ProcessId ?? 0), commandLine: String(row?.CommandLine ?? "") }))
127
- .filter((row) => Number.isFinite(row.pid) && row.pid > 0 && row.commandLine);
128
- }
129
- const out = [];
130
- for (const line of text.split(/\r?\n/)) {
131
- const match = /^\s*(\d+)\s+(.+)$/.exec(line);
132
- if (match) out.push({ pid: Number(match[1]), commandLine: match[2] });
133
- }
134
- return out;
135
- }
136
-
137
- /**
138
- * Keep only coordinators this checkout's suite could have spawned: the command
139
- * line runs `<repoRoot>/runner/state-coordinator.mjs` and the token after that
140
- * script (the root argument, quotes stripped by the tokenizer) resolves under
141
- * `tmpRoot`. Production roots and other checkouts never match.
142
- * @param {{
143
- * processes?: Array<{pid: number, commandLine: string}>,
144
- * repoRoot: string,
145
- * tmpRoot: string,
146
- * platform: string,
147
- * }} args
148
- * @returns {Array<{pid: number, root: string}>} deterministic order by pid
149
- */
150
- export function selectOrphanCoordinators({ processes, repoRoot, tmpRoot, platform }) {
151
- const scriptPath = canonicalPath(`${repoRoot}/${COORDINATOR_RELATIVE_SCRIPT}`, platform);
152
- const tmpPath = canonicalPath(tmpRoot, platform);
153
- if (!scriptPath || !tmpPath) return [];
154
- const out = [];
155
- for (const proc of processes ?? []) {
156
- const pid = Number(proc?.pid ?? 0);
157
- if (!Number.isFinite(pid) || pid <= 0) continue;
158
- const tokens = tokenizeCommandLine(String(proc?.commandLine ?? ""));
159
- const scriptIndex = tokens.findIndex((token) => canonicalPath(token, platform) === scriptPath);
160
- if (scriptIndex < 0) continue;
161
- const root = tokens[scriptIndex + 1];
162
- if (!root) continue;
163
- if (!isUnderDirectory(canonicalPath(root, platform), tmpPath)) continue;
164
- out.push({ pid, root });
165
- }
166
- return out.sort((left, right) => left.pid - right.pid);
167
- }
168
-
169
- /**
170
- * @param {unknown} error
171
- * @returns {string}
172
- */
173
- function describeError(error) {
174
- return error instanceof Error ? error.message : String(error);
175
- }
176
-
177
- /**
178
- * Orchestrate one suite run and its residue check. Every side effect is
179
- * injected so the unit tests drive every branch without spawning or killing
180
- * anything; the real shell lives in `main()`.
181
- * @param {{
182
- * spawnTest: () => Promise<{exitCode: number|null, signal?: string|null}>,
183
- * listProcesses: () => Array<{pid: number, commandLine: string}>|Promise<Array<{pid: number, commandLine: string}>>,
184
- * kill: (pid: number, signal: string) => void,
185
- * sleep: (ms: number) => Promise<void>,
186
- * log: (line: string) => void,
187
- * repoRoot: string,
188
- * tmpRoot: string,
189
- * platform: string,
190
- * graceMs?: number,
191
- * pollMs?: number,
192
- * now?: () => number,
193
- * }} deps
194
- * @returns {Promise<{exitCode: number, survivors: number[]}>}
195
- */
196
- export async function runSuite(deps) {
197
- const {
198
- spawnTest,
199
- listProcesses,
200
- kill,
201
- sleep,
202
- log,
203
- repoRoot,
204
- tmpRoot,
205
- platform,
206
- graceMs = DEFAULT_GRACE_MS,
207
- pollMs = DEFAULT_POLL_MS,
208
- now = Date.now,
209
- } = deps;
210
- const select = (processes) => selectOrphanCoordinators({ processes, repoRoot, tmpRoot, platform });
211
-
212
- // Snapshot first: pre-existing coordinators predate this run, so they are
213
- // warned about but never killed and never fail the suite.
214
- /** @type {Set<number>} */
215
- const preexisting = new Set();
216
- let canCheckResidue = true;
217
- try {
218
- for (const proc of select(await listProcesses())) preexisting.add(proc.pid);
219
- } catch (error) {
220
- canCheckResidue = false;
221
- log(`run-tests: WARNING: cannot enumerate node processes (${describeError(error)}); skipping the coordinator residue check`);
222
- }
223
- if (preexisting.size > 0) {
224
- log(`run-tests: WARNING: ${preexisting.size} pre-existing state-coordinator process(es) before the run; not killed, not a failure`);
225
- }
226
-
227
- const run = await spawnTest();
228
-
229
- // Poll for post-run residue. The last successful enumeration is kept when a
230
- // later poll fails: an already observed survivor still fails the run, while
231
- // never having enumerated at all stays fail-open.
232
- /** @type {Array<{pid: number, root: string}>} */
233
- let survivors = [];
234
- let residueChecked = false;
235
- if (canCheckResidue) {
236
- const deadline = now() + graceMs;
237
- for (;;) {
238
- let current;
239
- try {
240
- current = select(await listProcesses());
241
- residueChecked = true;
242
- } catch (error) {
243
- canCheckResidue = false;
244
- log(`run-tests: WARNING: cannot enumerate node processes (${describeError(error)}); skipping the coordinator residue check`);
245
- break;
246
- }
247
- survivors = current.filter((proc) => !preexisting.has(proc.pid));
248
- if (survivors.length === 0) break;
249
- if (now() >= deadline) break;
250
- await sleep(pollMs);
251
- }
252
- }
253
-
254
- /** @type {number[]} */
255
- const survivorPids = [];
256
- if (survivors.length > 0) {
257
- for (const proc of survivors) log(`run-tests: leaked state-coordinator pid=${proc.pid} root=${proc.root}`);
258
- for (const proc of survivors) kill(proc.pid, SIGTERM);
259
- await sleep(SIGKILL_DELAY_MS);
260
- let leftovers = survivors;
261
- if (canCheckResidue) {
262
- try {
263
- leftovers = select(await listProcesses()).filter((proc) => !preexisting.has(proc.pid));
264
- } catch (error) {
265
- log(`run-tests: WARNING: cannot enumerate node processes after SIGTERM (${describeError(error)}); sending SIGKILL to the known survivors`);
266
- }
267
- }
268
- for (const proc of leftovers) {
269
- log(`run-tests: survivor pid=${proc.pid} ignored SIGTERM; sending SIGKILL`);
270
- kill(proc.pid, SIGKILL);
271
- }
272
- survivorPids.push(...survivors.map((proc) => proc.pid));
273
- } else if (residueChecked) {
274
- log("run-tests: no leaked coordinators");
275
- }
276
-
277
- let exitCode;
278
- if (run.exitCode === null) {
279
- log(`run-tests: the test run ended by signal ${run.signal ?? "unknown"}; treating it as a failure`);
280
- exitCode = 1;
281
- } else {
282
- exitCode = run.exitCode;
283
- }
284
- if (survivors.length > 0) exitCode = 1;
285
- return { exitCode, survivors: survivorPids };
286
- }
287
-
288
- /**
289
- * Enumerate node processes with their command lines. Throws when the platform's
290
- * enumeration tool is unavailable; `runSuite` turns that into a warning and
291
- * skips the residue check (fail-open).
292
- * @returns {Array<{pid: number, commandLine: string}>}
293
- */
294
- function listNodeProcesses() {
295
- if (process.platform === "win32") {
296
- // Forced UTF-8: ConvertTo-Json otherwise emits code-page text that mangles
297
- // non-ASCII root paths, and CIM may quote paths containing spaces.
298
- const prefix = "[Console]::OutputEncoding=[Text.Encoding]::UTF8; $OutputEncoding=[Text.Encoding]::UTF8;";
299
- const query = "Get-CimInstance Win32_Process -Filter \"Name='node.exe'\" | Select-Object ProcessId,CommandLine | ConvertTo-Json -Compress";
300
- const args = ["-NoProfile", "-NonInteractive", "-Command", `${prefix} ${query}`];
301
- for (const shell of ["pwsh", "powershell"]) {
302
- try {
303
- return parseProcessList(execFileSync(shell, args, { encoding: "utf8" }), process.platform);
304
- } catch (error) {
305
- // Only a missing shell falls through to the next candidate; an
306
- // enumeration failure is not masked by retrying another binary.
307
- if (error?.code !== "ENOENT") throw error;
308
- }
309
- }
310
- throw new Error("neither pwsh nor powershell is available");
311
- }
312
- // -ww matters: without unlimited width, ps truncates the command line before
313
- // the root argument and a leaked coordinator silently escapes selection.
314
- return parseProcessList(execFileSync("ps", ["-e", "-ww", "-o", "pid=,args="], { encoding: "utf8" }), process.platform);
315
- }
316
-
317
- /**
318
- * Run the suite files through `node --test` with inherited stdio.
319
- * @param {string} repoRoot
320
- * @param {string[]} files repoRoot-relative test paths
321
- * @returns {Promise<{exitCode: number|null, signal: string|null}>}
322
- */
323
- function spawnSuite(repoRoot, files) {
324
- return new Promise((settle) => {
325
- let settled = false;
326
- const finish = (value) => {
327
- if (settled) return;
328
- settled = true;
329
- settle(value);
330
- };
331
- const child = spawn(process.execPath, ["--test", ...files], { cwd: repoRoot, stdio: "inherit", windowsHide: true });
332
- child.on("error", (error) => {
333
- console.error(`run-tests: failed to spawn the test runner: ${describeError(error)}`);
334
- finish({ exitCode: 1, signal: null });
335
- });
336
- child.on("exit", (code, signal) => finish({ exitCode: code, signal: signal ?? null }));
337
- });
338
- }
339
-
340
- /**
341
- * Real shell: derive this checkout from the module location, enumerate the test
342
- * files, run the suite, grade the residue, and set `process.exitCode`.
343
- * @returns {Promise<{exitCode: number, survivors: number[]}>}
344
- */
345
- export async function main() {
346
- const repoRoot = fileURLToPath(new URL("..", import.meta.url));
347
- // Sorted so a failing run's file ordering is stable across platforms.
348
- const files = readdirSync(join(repoRoot, "test"))
349
- .filter((name) => name.endsWith(".test.mjs"))
350
- .sort()
351
- .map((name) => join("test", name));
352
- const result = await runSuite({
353
- repoRoot,
354
- tmpRoot: tmpdir(),
355
- platform: process.platform,
356
- spawnTest: () => spawnSuite(repoRoot, files),
357
- listProcesses: () => listNodeProcesses(),
358
- kill: (pid, signal) => {
359
- try {
360
- process.kill(pid, signal);
361
- } catch { /* already gone */ }
362
- },
363
- sleep: (ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms)),
364
- log: (line) => console.log(line),
365
- });
366
- process.exitCode = result.exitCode;
367
- return result;
368
- }
369
-
370
- // Import safety: see the module JSDoc. `main()` must never run while a test
371
- // imports the pure functions, or `node --test` would recurse into itself.
372
- const invokedPath = process.argv[1];
373
- if (invokedPath !== undefined && import.meta.url === pathToFileURL(resolve(invokedPath)).href) {
374
- await main();
375
- }