@huseynsnmz/pi-board 0.15.3 → 0.17.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 (216) hide show
  1. package/CHANGELOG.md +24 -4
  2. package/CONTRIBUTING.md +7 -0
  3. package/README.md +55 -419
  4. package/SECURITY.md +19 -0
  5. package/docs/how-it-works.md +31 -0
  6. package/docs/reference.md +277 -0
  7. package/package.json +5 -11
  8. package/src/commands/agent-board.ts +26 -2
  9. package/src/commands/attach-flow.ts +4 -0
  10. package/src/core/double-press.mjs +6 -2
  11. package/src/core/slash-commands.mjs +52 -0
  12. package/src/index.ts +1 -1
  13. package/src/runtime/service.mjs +12 -0
  14. package/src/ui/dashboard.ts +59 -25
  15. package/src/ui/pty-attach.ts +8 -3
  16. package/IMPLEMENTATION_PLAN.md +0 -920
  17. package/PRD.md +0 -484
  18. package/PROGRESS.md +0 -142
  19. package/VERIFY.md +0 -115
  20. package/docs/BATCH_SELECTION_READ_FLOW.md +0 -277
  21. package/docs/EXPLORATION.md +0 -187
  22. package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +0 -581
  23. package/docs/RELEASE.md +0 -148
  24. package/docs/superpowers/plans/2026-08-15-screenlog-gc.md +0 -704
  25. package/docs/superpowers/plans/2026-08-16-attach-double-cursor-jiggle-retry.md +0 -499
  26. package/docs/superpowers/plans/2026-08-21-attach-coldstart-jiggle-rearm.md +0 -797
  27. package/docs/superpowers/plans/2026-08-21-dashboard-keypress-lag.md +0 -366
  28. package/docs/superpowers/plans/2026-08-21-issue-14-needs-input-no-auto-done.md +0 -576
  29. package/docs/superpowers/plans/2026-08-22-jiggle-shrink-and-hold.md +0 -337
  30. package/docs/superpowers/plans/2026-08-22-launch-cwd-favorites.md +0 -916
  31. package/docs/superpowers/plans/2026-08-22-question-tool-grouping.md +0 -211
  32. package/docs/superpowers/plans/2026-08-22-rename-state-labels.md +0 -182
  33. package/docs/superpowers/plans/2026-08-22-stable-list-order.md +0 -51
  34. package/docs/superpowers/plans/2026-08-27-locks-acquirelock-spin.md +0 -614
  35. package/docs/superpowers/plans/2026-08-27-pty-runner-test-flaky.md +0 -32
  36. package/docs/superpowers/plans/2026-08-29-code-refs-badges.md +0 -223
  37. package/docs/superpowers/plans/2026-08-30-circular-navigation.md +0 -308
  38. package/docs/superpowers/plans/2026-08-30-post-exit-timing-fix.md +0 -37
  39. package/docs/superpowers/plans/2026-08-30-pty-attach-quality-debt.md +0 -311
  40. package/docs/superpowers/plans/2026-08-30-readme-v2.md +0 -294
  41. package/docs/superpowers/plans/2026-09-01-attach-detach-gate-cursor-anchor.md +0 -284
  42. package/docs/superpowers/plans/2026-09-02-attach-detach-editor-state.md +0 -722
  43. package/docs/superpowers/plans/2026-09-03-code-refs-pr-backlink-narrow.md +0 -551
  44. package/docs/superpowers/plans/2026-09-03-detach-gate-glyph-fallback.md +0 -146
  45. package/docs/superpowers/plans/2026-09-04-evidence-outputpreview.md +0 -209
  46. package/docs/superpowers/plans/2026-09-04-warm-host-reclaim.md +0 -796
  47. package/docs/superpowers/plans/2026-09-05-issue-13-drainnextfollowup-pty-probe.md +0 -114
  48. package/docs/superpowers/plans/2026-09-05-issue-38-windows-wezterm-ime-cursor.md +0 -73
  49. package/docs/superpowers/plans/2026-09-05-issue-39-truncate-codepoint-boundary.md +0 -143
  50. package/docs/superpowers/plans/2026-09-05-issue-61-mention-fallback-guards.md +0 -226
  51. package/docs/superpowers/plans/2026-09-05-issue-63-flaky-manual-completion.md +0 -87
  52. package/docs/superpowers/plans/2026-09-05-issue-64-changelog-release-helper.md +0 -53
  53. package/docs/superpowers/plans/2026-09-05-pty-host-stacking-sock-race.md +0 -731
  54. package/docs/superpowers/plans/2026-09-08-attach-ctrl-left-detach.md +0 -30
  55. package/docs/superpowers/plans/2026-09-08-dashboard-shrink-repaint.md +0 -68
  56. package/docs/superpowers/plans/2026-09-08-issue-11-attach-runtime-desync-heal.md +0 -917
  57. package/docs/superpowers/plans/2026-09-08-legacy-stale-host-recovery.md +0 -125
  58. package/docs/superpowers/plans/2026-09-08-spawn-async-error-swallow.md +0 -56
  59. package/docs/superpowers/plans/2026-09-08-stale-model-attach-guard.md +0 -96
  60. package/docs/superpowers/plans/2026-09-09-claimpid-blocks-replace.md +0 -267
  61. package/docs/superpowers/plans/2026-09-09-harden-runner-architecture.md +0 -603
  62. package/docs/superpowers/plans/2026-09-09-single-writer-completion.md +0 -252
  63. package/docs/superpowers/plans/2026-09-10-reader-consistency.md +0 -115
  64. package/docs/superpowers/plans/2026-09-14-attach-cursor-dectcem-gate.md +0 -469
  65. package/docs/superpowers/plans/2026-09-14-attach-snapshot.md +0 -92
  66. package/docs/superpowers/plans/2026-09-14-host-meta-orphan-lock.md +0 -771
  67. package/docs/superpowers/plans/2026-09-14-issue-106-terminal-frame-cognition.md +0 -299
  68. package/docs/superpowers/plans/2026-09-14-issue-113-foreground-preview-race.md +0 -609
  69. package/docs/superpowers/plans/2026-09-14-terminal-model.md +0 -145
  70. package/docs/superpowers/plans/2026-09-15-coordinator-pipe-root-normalize.md +0 -224
  71. package/docs/superpowers/plans/2026-09-15-lease-publish-eprem-reclaim.md +0 -341
  72. package/docs/superpowers/plans/2026-09-18-detach-anchor-reporter-endpoint.md +0 -875
  73. package/docs/superpowers/plans/2026-09-20-control-lifecycle.md +0 -116
  74. package/docs/superpowers/plans/2026-09-20-issue-121-perf-gate-out-of-coverage.md +0 -517
  75. package/docs/superpowers/plans/2026-09-23-a5-mid-stream-assertion.md +0 -25
  76. package/docs/superpowers/plans/2026-09-23-cwd-browse-tab-drill.md +0 -393
  77. package/docs/superpowers/plans/2026-09-23-terminal-query-forwarding.md +0 -51
  78. package/docs/superpowers/plans/2026-09-23-unconditional-detach-key.md +0 -58
  79. package/docs/superpowers/plans/2026-09-24-control-reconcile-flake.md +0 -698
  80. package/docs/superpowers/plans/2026-09-24-probe-attached-clients.md +0 -605
  81. package/docs/superpowers/plans/2026-09-29-issue-145-holding-semantic-state.md +0 -1183
  82. package/docs/superpowers/plans/2026-09-29-issue-145-user-verification.md +0 -66
  83. package/docs/superpowers/plans/2026-09-30-board-delete-key-x-y-confirm.md +0 -649
  84. package/docs/superpowers/plans/2026-09-30-issue-153-runner-stop-boot-window.md +0 -618
  85. package/docs/superpowers/plans/2026-09-30-issue-95-test-discipline.md +0 -555
  86. package/docs/superpowers/plans/2026-09-30-publish-dispatch-tag-check.md +0 -259
  87. package/docs/superpowers/plans/2026-10-06-attach-settle-scheme-delivery.md +0 -751
  88. package/docs/superpowers/plans/2026-10-09-issue-167-attach-middle-paste.md +0 -509
  89. package/docs/superpowers/plans/2026-10-09-issue-169-fullscreen-mouse-ownership.md +0 -346
  90. package/docs/superpowers/plans/2026-10-10-attach-delete-in-place.md +0 -832
  91. package/docs/superpowers/plans/2026-10-10-issue-174-selection-mirror-paste-source.md +0 -510
  92. package/docs/superpowers/plans/2026-10-14-issue-170-attach-reply-guard.md +0 -771
  93. package/docs/superpowers/plans/2026-10-14-issue-178-accept-registration-race.md +0 -1257
  94. package/docs/superpowers/plans/2026-10-14-issue-184-snapshot-window-contract.md +0 -657
  95. package/docs/superpowers/plans/2026-10-14-issue-185-attach-wheel-alt-screen.md +0 -47
  96. package/docs/superpowers/plans/2026-10-14-issue-187-motionless-drag.md +0 -931
  97. package/docs/superpowers/plans/2026-10-14-issue-189-mixed-input-segments.md +0 -823
  98. package/docs/superpowers/plans/2026-10-15-issue-188-x10-mouse-release.md +0 -526
  99. package/docs/superpowers/plans/2026-10-16-issue-198-attach-wheel-forward-fallback.md +0 -32
  100. package/docs/superpowers/plans/2026-10-18-attach-wheel-replay-pseudo-scrollback.md +0 -437
  101. package/docs/superpowers/plans/2026-10-19-issue-213-reply-guard-csi-key.md +0 -257
  102. package/docs/superpowers/plans/2026-10-19-issue-214-attach-forward-alt-modifier.md +0 -202
  103. package/docs/superpowers/plans/2026-10-21-issue-221-ask-user-question.md +0 -198
  104. package/docs/superpowers/plans/2026-10-23-issue-222-attach-ready.md +0 -290
  105. package/docs/superpowers/plans/2026-10-23-issue-225-monotonic-foreground-state.md +0 -450
  106. package/docs/superpowers/plans/2026-10-23-issue-229-vim-jkl.md +0 -449
  107. package/docs/superpowers/plans/2026-10-23-issue-232-attach-bg-only-cells.md +0 -153
  108. package/docs/superpowers/plans/2026-10-23-lease-heartbeat-staleness.md +0 -537
  109. package/docs/superpowers/plans/2026-10-23-selected-row-highlight.md +0 -405
  110. package/docs/superpowers/plans/2026-10-24-issue-217-windows-pid-reuse-stopping.md +0 -367
  111. package/docs/superpowers/plans/2026-10-24-issue-230-warm-host-background-runs.md +0 -806
  112. package/docs/superpowers/plans/2026-10-24-issue-238-windows-process-identity-token.md +0 -490
  113. package/docs/superpowers/plans/2026-10-24-issue-241-coordinator-root-exit.md +0 -461
  114. package/docs/superpowers/plans/2026-10-25-issue-239-coordinator-replacement-identity.md +0 -504
  115. package/docs/superpowers/plans/2026-10-26-issue-245-key-hints-doc-sync.md +0 -244
  116. package/docs/superpowers/plans/2026-10-27-issue-172-fullscreen-selection-clear.md +0 -344
  117. package/docs/superpowers/plans/2026-10-27-issue-252-select-key-hints.md +0 -209
  118. package/docs/superpowers/plans/2026-10-28-issue-255-coordinator-endpoint-zombie.md +0 -442
  119. package/docs/superpowers/plans/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup.md +0 -256
  120. package/docs/superpowers/plans/2026-11-03-cwd-favorites-full-pool.md +0 -266
  121. package/docs/superpowers/specs/2026-08-15-screenlog-gc-design.md +0 -105
  122. package/docs/superpowers/specs/2026-08-16-attach-double-cursor-jiggle-retry-design.md +0 -142
  123. package/docs/superpowers/specs/2026-08-21-attach-coldstart-jiggle-rearm-design.md +0 -110
  124. package/docs/superpowers/specs/2026-08-21-dashboard-keypress-lag-design.md +0 -59
  125. package/docs/superpowers/specs/2026-08-21-issue-14-needs-input-no-auto-done-design.md +0 -109
  126. package/docs/superpowers/specs/2026-08-22-cwd-favorites-design.md +0 -131
  127. package/docs/superpowers/specs/2026-08-22-jiggle-shrink-and-hold-design.md +0 -111
  128. package/docs/superpowers/specs/2026-08-22-question-tool-grouping-design.md +0 -114
  129. package/docs/superpowers/specs/2026-08-22-rename-state-labels-design.md +0 -80
  130. package/docs/superpowers/specs/2026-08-27-locks-acquirelock-spin-design.md +0 -97
  131. package/docs/superpowers/specs/2026-08-27-pty-runner-test-flaky-design.md +0 -35
  132. package/docs/superpowers/specs/2026-08-29-code-refs-badges-design.md +0 -128
  133. package/docs/superpowers/specs/2026-08-30-circular-navigation-design.md +0 -47
  134. package/docs/superpowers/specs/2026-08-30-eprm-atomicwrite-race-design.md +0 -77
  135. package/docs/superpowers/specs/2026-08-30-post-exit-timing-fix-design.md +0 -80
  136. package/docs/superpowers/specs/2026-08-30-pty-attach-quality-debt-design.md +0 -105
  137. package/docs/superpowers/specs/2026-08-30-readme-v2-design.md +0 -115
  138. package/docs/superpowers/specs/2026-09-01-attach-detach-gate-cursor-anchor-design.md +0 -78
  139. package/docs/superpowers/specs/2026-09-02-attach-detach-editor-state-design.md +0 -120
  140. package/docs/superpowers/specs/2026-09-03-code-refs-pr-backlink-narrow-design.md +0 -92
  141. package/docs/superpowers/specs/2026-09-03-detach-gate-glyph-fallback-design.md +0 -99
  142. package/docs/superpowers/specs/2026-09-04-evidence-outputpreview-design.md +0 -50
  143. package/docs/superpowers/specs/2026-09-04-warm-host-reclaim-design.md +0 -106
  144. package/docs/superpowers/specs/2026-09-05-issue-13-drainnextfollowup-pty-probe-design.md +0 -64
  145. package/docs/superpowers/specs/2026-09-05-issue-38-windows-wezterm-ime-design.md +0 -48
  146. package/docs/superpowers/specs/2026-09-05-issue-39-truncate-codepoint-boundary-design.md +0 -64
  147. package/docs/superpowers/specs/2026-09-05-issue-61-mention-fallback-design.md +0 -71
  148. package/docs/superpowers/specs/2026-09-05-issue-63-flaky-manual-completion-design.md +0 -49
  149. package/docs/superpowers/specs/2026-09-05-issue-64-changelog-helper-design.md +0 -76
  150. package/docs/superpowers/specs/2026-09-05-pty-host-stacking-sock-race-design.md +0 -510
  151. package/docs/superpowers/specs/2026-09-07-issue-11-attach-runtime-desync-heal-design.md +0 -130
  152. package/docs/superpowers/specs/2026-09-08-attach-ctrl-left-detach-design.md +0 -58
  153. package/docs/superpowers/specs/2026-09-08-dashboard-shrink-repaint-design.md +0 -52
  154. package/docs/superpowers/specs/2026-09-08-legacy-stale-host-recovery-design.md +0 -87
  155. package/docs/superpowers/specs/2026-09-08-spawn-async-error-swallow-design.md +0 -56
  156. package/docs/superpowers/specs/2026-09-08-stale-model-attach-guard-design.md +0 -79
  157. package/docs/superpowers/specs/2026-09-09-claimpid-blocks-replace-design.md +0 -147
  158. package/docs/superpowers/specs/2026-09-09-harden-runner-architecture-design.md +0 -304
  159. package/docs/superpowers/specs/2026-09-14-attach-cursor-dectcem-gate-design.md +0 -114
  160. package/docs/superpowers/specs/2026-09-14-host-meta-orphan-lock-design.md +0 -120
  161. package/docs/superpowers/specs/2026-09-14-issue-106-terminal-frame-cognition-design.md +0 -146
  162. package/docs/superpowers/specs/2026-09-14-issue-113-foreground-preview-race-design.md +0 -116
  163. package/docs/superpowers/specs/2026-09-15-coordinator-pipe-root-normalize-design.md +0 -84
  164. package/docs/superpowers/specs/2026-09-15-lease-publish-eprem-reclaim-design.md +0 -92
  165. package/docs/superpowers/specs/2026-09-18-detach-anchor-reporter-endpoint-design.md +0 -123
  166. package/docs/superpowers/specs/2026-09-20-issue-121-perf-gate-out-of-coverage-design.md +0 -204
  167. package/docs/superpowers/specs/2026-09-23-a5-mid-stream-assertion-design.md +0 -43
  168. package/docs/superpowers/specs/2026-09-23-cwd-browse-tab-drill-design.md +0 -95
  169. package/docs/superpowers/specs/2026-09-23-terminal-query-forwarding-design.md +0 -63
  170. package/docs/superpowers/specs/2026-09-23-unconditional-detach-key-design.md +0 -57
  171. package/docs/superpowers/specs/2026-09-24-control-reconcile-flake-design.md +0 -98
  172. package/docs/superpowers/specs/2026-09-24-probe-attached-clients-design.md +0 -190
  173. package/docs/superpowers/specs/2026-09-29-issue-145-holding-semantic-state-design.md +0 -394
  174. package/docs/superpowers/specs/2026-09-30-board-delete-key-x-y-confirm-design.md +0 -86
  175. package/docs/superpowers/specs/2026-09-30-issue-153-runner-stop-boot-window-design.md +0 -75
  176. package/docs/superpowers/specs/2026-09-30-issue-95-test-discipline-design.md +0 -226
  177. package/docs/superpowers/specs/2026-09-30-publish-dispatch-tag-check-design.md +0 -129
  178. package/docs/superpowers/specs/2026-10-06-attach-settle-scheme-delivery-design.md +0 -196
  179. package/docs/superpowers/specs/2026-10-09-issue-167-attach-middle-paste-design.md +0 -125
  180. package/docs/superpowers/specs/2026-10-09-issue-169-fullscreen-mouse-ownership-design.md +0 -130
  181. package/docs/superpowers/specs/2026-10-10-attach-delete-in-place-design.md +0 -224
  182. package/docs/superpowers/specs/2026-10-10-issue-174-selection-mirror-paste-source-design.md +0 -154
  183. package/docs/superpowers/specs/2026-10-14-issue-170-attach-reply-guard-design.md +0 -195
  184. package/docs/superpowers/specs/2026-10-14-issue-178-accept-registration-race-design.md +0 -287
  185. package/docs/superpowers/specs/2026-10-14-issue-184-snapshot-window-contract-design.md +0 -288
  186. package/docs/superpowers/specs/2026-10-14-issue-185-attach-wheel-alt-screen-design.md +0 -126
  187. package/docs/superpowers/specs/2026-10-14-issue-187-motionless-drag-design.md +0 -269
  188. package/docs/superpowers/specs/2026-10-14-issue-189-mixed-input-segments-design.md +0 -369
  189. package/docs/superpowers/specs/2026-10-15-issue-188-x10-mouse-release-design.md +0 -252
  190. package/docs/superpowers/specs/2026-10-16-issue-198-attach-wheel-forward-fallback-design.md +0 -67
  191. package/docs/superpowers/specs/2026-10-18-attach-wheel-replay-pseudo-scrollback-design.md +0 -250
  192. package/docs/superpowers/specs/2026-10-19-issue-213-reply-guard-csi-key-design.md +0 -165
  193. package/docs/superpowers/specs/2026-10-19-issue-214-attach-forward-alt-modifier-design.md +0 -131
  194. package/docs/superpowers/specs/2026-10-20-issue-221-ask-user-question-design.md +0 -270
  195. package/docs/superpowers/specs/2026-10-22-issue-225-monotonic-foreground-state-design.md +0 -225
  196. package/docs/superpowers/specs/2026-10-23-issue-222-attach-ready-design.md +0 -204
  197. package/docs/superpowers/specs/2026-10-23-issue-229-vim-jkl-design.md +0 -90
  198. package/docs/superpowers/specs/2026-10-23-issue-232-attach-bg-only-cells-design.md +0 -146
  199. package/docs/superpowers/specs/2026-10-23-lease-heartbeat-staleness-design.md +0 -271
  200. package/docs/superpowers/specs/2026-10-23-selected-row-highlight-design.md +0 -191
  201. package/docs/superpowers/specs/2026-10-24-issue-217-windows-pid-reuse-stopping-design.md +0 -334
  202. package/docs/superpowers/specs/2026-10-24-issue-230-warm-host-background-runs-design.md +0 -283
  203. package/docs/superpowers/specs/2026-10-24-issue-238-windows-process-identity-token-design.md +0 -286
  204. package/docs/superpowers/specs/2026-10-24-issue-241-coordinator-root-exit-design.md +0 -381
  205. package/docs/superpowers/specs/2026-10-25-issue-239-coordinator-replacement-identity-design.md +0 -331
  206. package/docs/superpowers/specs/2026-10-26-issue-245-key-hints-doc-sync-design.md +0 -72
  207. package/docs/superpowers/specs/2026-10-27-issue-172-fullscreen-selection-clear-design.md +0 -248
  208. package/docs/superpowers/specs/2026-10-27-issue-252-select-key-hints-design.md +0 -95
  209. package/docs/superpowers/specs/2026-10-28-issue-255-coordinator-endpoint-zombie-design.md +0 -380
  210. package/docs/superpowers/specs/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup-design.md +0 -197
  211. package/docs/superpowers/specs/2026-11-03-cwd-favorites-full-pool-design.md +0 -106
  212. package/scripts/budget-audit.mjs +0 -135
  213. package/scripts/release.mjs +0 -446
  214. package/scripts/release_helper.mjs +0 -309
  215. package/scripts/run-perf-gate.mjs +0 -40
  216. package/scripts/run-tests.mjs +0 -375
@@ -1,446 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Release orchestrator — the single entry point for a release, from a clean
4
- * `main` to a published npm version.
5
- *
6
- * Flow (prose version: docs/RELEASE.md):
7
- *
8
- * precheck → prepare <bump> → open-pr <version>
9
- * → merge the PR with a MERGE COMMIT
10
- * → finish <version> (creates the GitHub Release)
11
- * → .github/workflows/publish.yml publishes to npm with provenance
12
- *
13
- * Two ordering rules are baked in, because breaking either one ships a version
14
- * that `main` does not contain, or skips a version's notes:
15
- *
16
- * 1. The CHANGELOG section is generated BEFORE `npm version`, because
17
- * `npm version` commits and tags the bump and would empty the commit range
18
- * the section is rendered from.
19
- * 2. The GitHub Release is created AFTER the release PR is merged, and only
20
- * once the tag is an ancestor of `origin/main`. A squash-merged release PR
21
- * leaves the tag orphaned; `finish` refuses to continue in that case
22
- * instead of publishing a version whose bump commit never reached main.
23
- *
24
- * Modes:
25
- *
26
- * node scripts/release.mjs precheck
27
- * Read-only gate: on main, clean tree, in sync with origin, no open
28
- * release/* PR, last tag reachable from origin/main, and at least one
29
- * functional commit to release.
30
- * node scripts/release.mjs prepare <patch|minor|major|X.Y.Z> [--dry-run]
31
- * Runs the gate, renders the CHANGELOG section from conventional commits,
32
- * commits it, runs `npm version` (bump commit + tag), and switches to
33
- * `release/vX.Y.Z`. Nothing leaves the machine.
34
- * node scripts/release.mjs open-pr <version> [--dry-run]
35
- * Pushes the release branch and its tag, then opens the release PR.
36
- * node scripts/release.mjs finish <version> [--dry-run] [--sync-main]
37
- * After the PR is merged: checks tag ancestry and CHANGELOG sync, then
38
- * creates the GitHub Release — the CI publish trigger. `--sync-main`
39
- * additionally resets a clean local main to origin/main.
40
- * node scripts/release.mjs notes <version>
41
- * Prints the CHANGELOG section for a version (the GitHub Release notes).
42
- *
43
- * Every mode is fail-closed: a failed check stops the run with exit code 1 and
44
- * a JSON body naming the failure. All modes print JSON except `notes`, which
45
- * prints the section itself so it can be redirected to a file.
46
- *
47
- * Changelog rendering is delegated to the pure, unit-tested functions in
48
- * release_helper.mjs; the git/npm/gh side effects live here and nowhere else.
49
- */
50
- import { execFileSync } from "node:child_process";
51
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
52
- import { tmpdir } from "node:os";
53
- import { dirname, join } from "node:path";
54
- import { fileURLToPath } from "node:url";
55
- import {
56
- functionalLines,
57
- generateChangelog,
58
- hasSection,
59
- insertSection,
60
- nextVersion,
61
- parseCommitLines,
62
- verifyFrom,
63
- } from "./release_helper.mjs";
64
-
65
- const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
66
- const CHANGELOG_MD = join(REPO_ROOT, "CHANGELOG.md");
67
- const PACKAGE_JSON = join(REPO_ROOT, "package.json");
68
- const MAIN_BRANCH = "main";
69
- const PACKAGE_NAME = "@zhuxixi/pi-agent-board";
70
- const COMMANDS = new Set(["precheck", "prepare", "open-pr", "finish", "notes"]);
71
- const FLAGS = new Set(["--dry-run", "--sync-main"]);
72
-
73
- /* ------------------------------------------------------------------ *
74
- * Pure helpers (unit-tested in test/release-flow.test.mjs)
75
- * ------------------------------------------------------------------ */
76
-
77
- /** Branch a version is prepared on: `release/v0.9.0`. */
78
- export function releaseBranchName(version) {
79
- return `release/v${version}`;
80
- }
81
-
82
- /**
83
- * The `## [version]` section of a CHANGELOG, without the trailing link
84
- * reference definition — i.e. the body used as GitHub Release notes.
85
- * @param {string} changelogText
86
- * @param {string} version
87
- * @returns {string|null} null when the section is absent
88
- */
89
- export function extractReleaseSection(changelogText, version) {
90
- const text = String(changelogText ?? "");
91
- const escaped = String(version).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
92
- const heading = new RegExp(`^##\\s*\\[${escaped}]`, "m");
93
- const match = heading.exec(text);
94
- if (!match) return null;
95
- const rest = text.slice(match.index);
96
- // The next `## [` heading ends the section; +1 because the search runs on a
97
- // sliced string, so its index is relative to rest[1].
98
- const nextHeading = rest.slice(1).search(/^##\s*\[/m);
99
- const section = nextHeading === -1 ? rest : rest.slice(0, nextHeading + 1);
100
- return section
101
- .replace(/^\[[^\]]+]:\s+\S+\s*$/gm, "")
102
- .replace(/\s+$/, "")
103
- .replace(/^\n+/, "");
104
- }
105
-
106
- /**
107
- * Decide whether a release may start, from a collected snapshot of repo state.
108
- * Pure so the rules can be tested without a repository.
109
- * @param {{branch: string, clean: boolean, ahead: number, behind: number, openReleasePrs: number|null, lastTag: string|null, lastTagReachable: boolean, functionalCommits: number}} state
110
- * @returns {{ok: boolean, failures: string[]}}
111
- */
112
- export function evaluatePreconditions(state) {
113
- const failures = [];
114
- if (state.branch !== MAIN_BRANCH) {
115
- failures.push(`must run on ${MAIN_BRANCH} (currently on "${state.branch || "detached HEAD"}")`);
116
- }
117
- if (!state.clean) failures.push("working tree is dirty — commit or stash first");
118
- if (state.behind > 0) failures.push(`local ${MAIN_BRANCH} is behind origin/${MAIN_BRANCH} by ${state.behind} commit(s) — pull first`);
119
- if (state.ahead > 0) failures.push(`local ${MAIN_BRANCH} is ahead of origin/${MAIN_BRANCH} by ${state.ahead} commit(s)`);
120
- if ((state.openReleasePrs ?? 0) > 0) failures.push(`${state.openReleasePrs} open release/* PR(s) — finish or close them first`);
121
- if (state.lastTag && !state.lastTagReachable) {
122
- failures.push(`tag ${state.lastTag} is not an ancestor of origin/${MAIN_BRANCH} (orphan tag) — re-point it before releasing`);
123
- }
124
- if (state.functionalCommits === 0) failures.push("no functional commits since the last tag — nothing to release");
125
- return { ok: failures.length === 0, failures };
126
- }
127
-
128
- /**
129
- * Parse the CLI argv into a command, its positional argument, and flags.
130
- * @param {string[]} argv
131
- * @returns {{command: string, arg: string|undefined, dryRun: boolean, syncMain: boolean}}
132
- */
133
- export function parseArgs(argv) {
134
- const flags = [];
135
- const positional = [];
136
- for (const raw of argv) {
137
- if (raw.startsWith("--")) flags.push(raw);
138
- else positional.push(raw);
139
- }
140
- const [command, arg] = positional;
141
- if (!COMMANDS.has(command ?? "")) {
142
- throw new Error(`unknown command "${command ?? ""}" — expected one of ${[...COMMANDS].join(", ")}`);
143
- }
144
- for (const flag of flags) {
145
- if (!FLAGS.has(flag)) throw new Error(`unknown flag "${flag}" — expected ${[...FLAGS].join(", ")}`);
146
- }
147
- return { command, arg, dryRun: flags.includes("--dry-run"), syncMain: flags.includes("--sync-main") };
148
- }
149
-
150
- /* ------------------------------------------------------------------ *
151
- * Repo plumbing
152
- * ------------------------------------------------------------------ */
153
-
154
- /**
155
- * Run a command, capturing both streams. stderr is folded into the thrown error
156
- * so a failure names the command and what it said, while a successful run stays
157
- * quiet (git's "Switched to a new branch" chatter included).
158
- * @param {string} cmd
159
- * @param {string[]} args
160
- * @returns {string} stdout
161
- */
162
- function run(cmd, args) {
163
- try {
164
- return execFileSync(cmd, args, { cwd: REPO_ROOT, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
165
- } catch (err) {
166
- const stderr = String(err?.stderr ?? "").trim();
167
- const detail = stderr ? `: ${stderr}` : "";
168
- throw new Error(`${cmd} ${args.join(" ")} failed${detail}`);
169
- }
170
- }
171
-
172
- /** @param {string[]} args @returns {string} git stdout */
173
- function git(args) {
174
- return run("git", args);
175
- }
176
-
177
- /** @param {string[]} args @returns {boolean} true when the command succeeded */
178
- function gitOk(args) {
179
- try {
180
- git(args);
181
- return true;
182
- } catch {
183
- return false;
184
- }
185
- }
186
-
187
- /** @param {string[]} args @returns {string} npm stdout */
188
- function npm(args) {
189
- return run("npm", args);
190
- }
191
-
192
- /** @param {string[]} args @returns {string} gh stdout */
193
- function gh(args) {
194
- return run("gh", args);
195
- }
196
-
197
- /** @returns {string} version from package.json */
198
- function packageVersion() {
199
- const version = JSON.parse(readFileSync(PACKAGE_JSON, "utf8")).version;
200
- if (!/^\d+\.\d+\.\d+$/.test(version ?? "")) throw new Error(`package.json has no semver version: ${version}`);
201
- return version;
202
- }
203
-
204
- /** @returns {string|null} newest reachable `v*` tag */
205
- function latestTag() {
206
- try {
207
- const tag = git(["describe", "--tags", "--abbrev=0", "--match", "v*"]).trim();
208
- return tag || null;
209
- } catch {
210
- return null;
211
- }
212
- }
213
-
214
- /** @param {string} range @returns {string[]} commit subjects in the range */
215
- function subjects(range) {
216
- return git(["log", range, "--format=%s"])
217
- .split("\n")
218
- .map((s) => s.trim())
219
- .filter(Boolean);
220
- }
221
-
222
- /** @returns {boolean} whether the tag exists locally */
223
- function tagExists(tag) {
224
- return gitOk(["rev-parse", "--verify", "--quiet", `refs/tags/${tag}`]);
225
- }
226
-
227
- /** @param {string} ref @returns {boolean} whether ref exists locally */
228
- function refExists(ref) {
229
- return gitOk(["rev-parse", "--verify", "--quiet", ref]);
230
- }
231
-
232
- /** @param {string} tag @param {string} ref @returns {boolean} */
233
- function isAncestor(tag, ref) {
234
- return gitOk(["merge-base", "--is-ancestor", tag, ref]);
235
- }
236
-
237
- /**
238
- * Count open `release/*` PRs. null means the check could not run (no `gh`, no
239
- * auth, or not a GitHub remote) — reported but never a hard failure, so a
240
- * missing `gh` cannot block a release that does not need it.
241
- * @returns {number|null}
242
- */
243
- function openReleasePrCount() {
244
- try {
245
- const json = gh(["pr", "list", "--state", "open", "--limit", "50", "--json", "headRefName"]);
246
- return JSON.parse(json).filter((pr) => String(pr.headRefName ?? "").startsWith("release/")).length;
247
- } catch {
248
- return null;
249
- }
250
- }
251
-
252
- /** @returns {object} the state consumed by evaluatePreconditions */
253
- function collectState() {
254
- git(["fetch", "origin", "--quiet"]);
255
- const [behind, ahead] = git(["rev-list", "--left-right", "--count", `origin/${MAIN_BRANCH}...HEAD`])
256
- .trim()
257
- .split(/\s+/)
258
- .map(Number);
259
- const lastTag = latestTag();
260
- const functionalCommits = functionalLines(subjects(lastTag ? `${lastTag}..HEAD` : "HEAD")).length;
261
- return {
262
- branch: git(["branch", "--show-current"]).trim(),
263
- clean: git(["status", "--porcelain"]).trim() === "",
264
- ahead,
265
- behind,
266
- openReleasePrs: openReleasePrCount(),
267
- lastTag,
268
- lastTagReachable: lastTag ? isAncestor(lastTag, `origin/${MAIN_BRANCH}`) : true,
269
- functionalCommits,
270
- };
271
- }
272
-
273
- /** Throw unless the release may start; returns the collected state. */
274
- function assertPreconditions() {
275
- const state = collectState();
276
- const { ok, failures } = evaluatePreconditions(state);
277
- if (!ok) throw new Error(`precheck failed:\n- ${failures.join("\n- ")}`);
278
- return state;
279
- }
280
-
281
- /* ------------------------------------------------------------------ *
282
- * Commands
283
- * ------------------------------------------------------------------ */
284
-
285
- /** @returns {object} precheck report (never throws on a failed gate) */
286
- function cmdPrecheck() {
287
- const state = collectState();
288
- const { ok, failures } = evaluatePreconditions(state);
289
- return { ok, failures, ...state, prCheck: state.openReleasePrs === null ? "unavailable (gh not authenticated or not a GitHub remote)" : "ok" };
290
- }
291
-
292
- /**
293
- * @param {string|undefined} bump patch|minor|major|X.Y.Z
294
- * @param {boolean} dryRun
295
- * @returns {object}
296
- */
297
- function cmdPrepare(bump, dryRun) {
298
- const state = assertPreconditions();
299
- const version = nextVersion(bump ?? "patch", packageVersion());
300
- const branch = releaseBranchName(version);
301
- const existing = readFileSync(CHANGELOG_MD, "utf8");
302
- if (hasSection(existing, version)) {
303
- throw new Error(`CHANGELOG.md already has a [${version}] section — remove the stale section before re-applying`);
304
- }
305
- const range = state.lastTag ? `${state.lastTag}..HEAD` : "HEAD";
306
- const entries = parseCommitLines(subjects(range));
307
- const section = generateChangelog({
308
- version,
309
- date: new Date().toISOString().slice(0, 10),
310
- entries,
311
- prevTag: state.lastTag,
312
- });
313
- const plan = [
314
- `git add CHANGELOG.md && git commit -m "docs(changelog): add ${version} section"`,
315
- `npm version ${bump ?? "patch"}`,
316
- `git switch -c ${branch}`,
317
- ];
318
- const next = `node scripts/release.mjs open-pr ${version}`;
319
- if (dryRun) {
320
- return { ok: true, dry_run: true, current_version: packageVersion(), new_version: version, last_tag: state.lastTag, changelog_preview: section, plan, next };
321
- }
322
- writeFileSync(CHANGELOG_MD, insertSection(existing, section));
323
- git(["add", "CHANGELOG.md"]);
324
- git(["commit", "-m", `docs(changelog): add ${version} section`]);
325
- npm(["version", bump ?? "patch"]);
326
- git(["switch", "-c", branch]);
327
- return { ok: true, version, branch, tag: `v${version}`, next };
328
- }
329
-
330
- /**
331
- * @param {string|undefined} version
332
- * @param {boolean} dryRun
333
- * @returns {object}
334
- */
335
- function cmdOpenPr(version, dryRun) {
336
- if (!version) throw new Error("open-pr requires a version (node scripts/release.mjs open-pr 0.9.0)");
337
- const branch = releaseBranchName(version);
338
- const tag = `v${version}`;
339
- const current = git(["branch", "--show-current"]).trim();
340
- if (packageVersion() !== version) throw new Error(`package.json is at ${packageVersion()}, expected ${version}`);
341
- if (current !== branch) throw new Error(`expected to be on ${branch} (currently on "${current || "detached HEAD"}")`);
342
- if (!tagExists(tag)) throw new Error(`tag ${tag} does not exist — run prepare first`);
343
- const notes = extractReleaseSection(readFileSync(CHANGELOG_MD, "utf8"), version);
344
- if (!notes) throw new Error(`CHANGELOG.md has no [${version}] section`);
345
- const body = `${notes}\n\n---\n\nMerge with a **merge commit** — squashing would orphan the \`${tag}\` tag and break the changelog range for the next release.`;
346
- const commands = [
347
- `git push -u origin ${branch}`,
348
- `git push origin ${tag}`,
349
- `gh pr create --base ${MAIN_BRANCH} --head ${branch} --title "chore: release ${version}"`,
350
- ];
351
- if (dryRun) return { ok: true, dry_run: true, version, branch, tag, commands };
352
- git(["push", "-u", "origin", branch]);
353
- git(["push", "origin", tag]);
354
- const url = gh(["pr", "create", "--base", MAIN_BRANCH, "--head", branch, "--title", `chore: release ${version}`, "--body", body]).trim();
355
- return { ok: true, version, branch, tag, pr: url, next: `merge with a merge commit, then: node scripts/release.mjs finish ${version} --sync-main` };
356
- }
357
-
358
- /**
359
- * @param {string|undefined} version
360
- * @param {{dryRun: boolean, syncMain: boolean}} opts
361
- * @returns {object}
362
- */
363
- function cmdFinish(version, { dryRun, syncMain }) {
364
- if (!version) throw new Error("finish requires a version (node scripts/release.mjs finish 0.9.0)");
365
- const tag = `v${version}`;
366
- if (packageVersion() !== version) throw new Error(`package.json is at ${packageVersion()}, expected ${version}`);
367
- git(["fetch", "origin", "--quiet"]);
368
- if (!tagExists(tag)) throw new Error(`tag ${tag} is missing — push it with open-pr first`);
369
- if (!isAncestor(tag, `origin/${MAIN_BRANCH}`)) {
370
- throw new Error(
371
- `tag ${tag} is not an ancestor of origin/${MAIN_BRANCH} — the release PR was probably squash-merged. ` +
372
- `Point the tag at the content-equivalent commit on main (git tag -f ${tag} <sha> && git push origin ${tag} --force) and retry`,
373
- );
374
- }
375
- const changelogText = readFileSync(CHANGELOG_MD, "utf8");
376
- const drift = verifyFrom({ lines: subjects(`${tag}..origin/${MAIN_BRANCH}`), changelogText });
377
- if (!drift.ok) {
378
- throw new Error(
379
- `CHANGELOG is missing functional PR(s) ${drift.missing.join(", ")} merged after the bump — ` +
380
- `add them to the ${version} section in a docs(changelog) PR, then retry`,
381
- );
382
- }
383
- const notes = extractReleaseSection(changelogText, version);
384
- if (!notes) throw new Error(`CHANGELOG.md has no [${version}] section`);
385
- if (dryRun) {
386
- return { ok: true, dry_run: true, tag, notes, commands: [`gh release create ${tag} --title ${tag} --notes-file <notes>`], syncMain };
387
- }
388
- const dir = mkdtempSync(join(tmpdir(), "release-notes-"));
389
- const notesFile = join(dir, `${tag}.md`);
390
- try {
391
- writeFileSync(notesFile, `${notes}\n`);
392
- gh(["release", "create", tag, "--title", tag, "--notes-file", notesFile]);
393
- } finally {
394
- rmSync(dir, { recursive: true, force: true });
395
- }
396
- let syncedMain = false;
397
- if (syncMain && git(["status", "--porcelain"]).trim() === "") {
398
- git(["switch", MAIN_BRANCH]);
399
- git(["reset", "--hard", `origin/${MAIN_BRANCH}`]);
400
- syncedMain = true;
401
- }
402
- return {
403
- ok: true,
404
- version,
405
- tag,
406
- release: `https://github.com/zhuxixi/pi-agent-board/releases/tag/${tag}`,
407
- package: `${PACKAGE_NAME}@${version}`,
408
- synced_main: syncedMain,
409
- publish: "CI publishes on release — watch it with: gh run list --workflow=publish.yml --limit 1",
410
- };
411
- }
412
-
413
- /**
414
- * @param {string|undefined} version
415
- * @returns {string} the release notes text
416
- */
417
- function cmdNotes(version) {
418
- if (!version) throw new Error("notes requires a version (node scripts/release.mjs notes 0.9.0)");
419
- const notes = extractReleaseSection(readFileSync(CHANGELOG_MD, "utf8"), version);
420
- if (!notes) throw new Error(`CHANGELOG.md has no [${version}] section`);
421
- return notes;
422
- }
423
-
424
- /* ------------------------------------------------------------------ *
425
- * CLI
426
- * ------------------------------------------------------------------ */
427
-
428
- function main(argv) {
429
- try {
430
- const { command, arg, dryRun, syncMain } = parseArgs(argv);
431
- if (command === "notes") {
432
- process.stdout.write(`${cmdNotes(arg)}\n`);
433
- return;
434
- }
435
- const result = command === "precheck" ? cmdPrecheck() : command === "prepare" ? cmdPrepare(arg, dryRun) : command === "open-pr" ? cmdOpenPr(arg, dryRun) : cmdFinish(arg, { dryRun, syncMain });
436
- console.log(JSON.stringify(result, null, 1));
437
- if (result.ok === false) process.exitCode = 1;
438
- } catch (err) {
439
- console.log(JSON.stringify({ ok: false, error: String(err?.message ?? err) }));
440
- process.exitCode = 1;
441
- }
442
- }
443
-
444
- if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
445
- main(process.argv.slice(2));
446
- }