@huseynsnmz/pi-board 0.15.3 → 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 (211) hide show
  1. package/CHANGELOG.md +13 -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 +5 -11
  8. package/src/index.ts +1 -1
  9. package/src/runtime/service.mjs +12 -0
  10. package/src/ui/dashboard.ts +14 -8
  11. package/IMPLEMENTATION_PLAN.md +0 -920
  12. package/PRD.md +0 -484
  13. package/PROGRESS.md +0 -142
  14. package/VERIFY.md +0 -115
  15. package/docs/BATCH_SELECTION_READ_FLOW.md +0 -277
  16. package/docs/EXPLORATION.md +0 -187
  17. package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +0 -581
  18. package/docs/RELEASE.md +0 -148
  19. package/docs/superpowers/plans/2026-08-15-screenlog-gc.md +0 -704
  20. package/docs/superpowers/plans/2026-08-16-attach-double-cursor-jiggle-retry.md +0 -499
  21. package/docs/superpowers/plans/2026-08-21-attach-coldstart-jiggle-rearm.md +0 -797
  22. package/docs/superpowers/plans/2026-08-21-dashboard-keypress-lag.md +0 -366
  23. package/docs/superpowers/plans/2026-08-21-issue-14-needs-input-no-auto-done.md +0 -576
  24. package/docs/superpowers/plans/2026-08-22-jiggle-shrink-and-hold.md +0 -337
  25. package/docs/superpowers/plans/2026-08-22-launch-cwd-favorites.md +0 -916
  26. package/docs/superpowers/plans/2026-08-22-question-tool-grouping.md +0 -211
  27. package/docs/superpowers/plans/2026-08-22-rename-state-labels.md +0 -182
  28. package/docs/superpowers/plans/2026-08-22-stable-list-order.md +0 -51
  29. package/docs/superpowers/plans/2026-08-27-locks-acquirelock-spin.md +0 -614
  30. package/docs/superpowers/plans/2026-08-27-pty-runner-test-flaky.md +0 -32
  31. package/docs/superpowers/plans/2026-08-29-code-refs-badges.md +0 -223
  32. package/docs/superpowers/plans/2026-08-30-circular-navigation.md +0 -308
  33. package/docs/superpowers/plans/2026-08-30-post-exit-timing-fix.md +0 -37
  34. package/docs/superpowers/plans/2026-08-30-pty-attach-quality-debt.md +0 -311
  35. package/docs/superpowers/plans/2026-08-30-readme-v2.md +0 -294
  36. package/docs/superpowers/plans/2026-09-01-attach-detach-gate-cursor-anchor.md +0 -284
  37. package/docs/superpowers/plans/2026-09-02-attach-detach-editor-state.md +0 -722
  38. package/docs/superpowers/plans/2026-09-03-code-refs-pr-backlink-narrow.md +0 -551
  39. package/docs/superpowers/plans/2026-09-03-detach-gate-glyph-fallback.md +0 -146
  40. package/docs/superpowers/plans/2026-09-04-evidence-outputpreview.md +0 -209
  41. package/docs/superpowers/plans/2026-09-04-warm-host-reclaim.md +0 -796
  42. package/docs/superpowers/plans/2026-09-05-issue-13-drainnextfollowup-pty-probe.md +0 -114
  43. package/docs/superpowers/plans/2026-09-05-issue-38-windows-wezterm-ime-cursor.md +0 -73
  44. package/docs/superpowers/plans/2026-09-05-issue-39-truncate-codepoint-boundary.md +0 -143
  45. package/docs/superpowers/plans/2026-09-05-issue-61-mention-fallback-guards.md +0 -226
  46. package/docs/superpowers/plans/2026-09-05-issue-63-flaky-manual-completion.md +0 -87
  47. package/docs/superpowers/plans/2026-09-05-issue-64-changelog-release-helper.md +0 -53
  48. package/docs/superpowers/plans/2026-09-05-pty-host-stacking-sock-race.md +0 -731
  49. package/docs/superpowers/plans/2026-09-08-attach-ctrl-left-detach.md +0 -30
  50. package/docs/superpowers/plans/2026-09-08-dashboard-shrink-repaint.md +0 -68
  51. package/docs/superpowers/plans/2026-09-08-issue-11-attach-runtime-desync-heal.md +0 -917
  52. package/docs/superpowers/plans/2026-09-08-legacy-stale-host-recovery.md +0 -125
  53. package/docs/superpowers/plans/2026-09-08-spawn-async-error-swallow.md +0 -56
  54. package/docs/superpowers/plans/2026-09-08-stale-model-attach-guard.md +0 -96
  55. package/docs/superpowers/plans/2026-09-09-claimpid-blocks-replace.md +0 -267
  56. package/docs/superpowers/plans/2026-09-09-harden-runner-architecture.md +0 -603
  57. package/docs/superpowers/plans/2026-09-09-single-writer-completion.md +0 -252
  58. package/docs/superpowers/plans/2026-09-10-reader-consistency.md +0 -115
  59. package/docs/superpowers/plans/2026-09-14-attach-cursor-dectcem-gate.md +0 -469
  60. package/docs/superpowers/plans/2026-09-14-attach-snapshot.md +0 -92
  61. package/docs/superpowers/plans/2026-09-14-host-meta-orphan-lock.md +0 -771
  62. package/docs/superpowers/plans/2026-09-14-issue-106-terminal-frame-cognition.md +0 -299
  63. package/docs/superpowers/plans/2026-09-14-issue-113-foreground-preview-race.md +0 -609
  64. package/docs/superpowers/plans/2026-09-14-terminal-model.md +0 -145
  65. package/docs/superpowers/plans/2026-09-15-coordinator-pipe-root-normalize.md +0 -224
  66. package/docs/superpowers/plans/2026-09-15-lease-publish-eprem-reclaim.md +0 -341
  67. package/docs/superpowers/plans/2026-09-18-detach-anchor-reporter-endpoint.md +0 -875
  68. package/docs/superpowers/plans/2026-09-20-control-lifecycle.md +0 -116
  69. package/docs/superpowers/plans/2026-09-20-issue-121-perf-gate-out-of-coverage.md +0 -517
  70. package/docs/superpowers/plans/2026-09-23-a5-mid-stream-assertion.md +0 -25
  71. package/docs/superpowers/plans/2026-09-23-cwd-browse-tab-drill.md +0 -393
  72. package/docs/superpowers/plans/2026-09-23-terminal-query-forwarding.md +0 -51
  73. package/docs/superpowers/plans/2026-09-23-unconditional-detach-key.md +0 -58
  74. package/docs/superpowers/plans/2026-09-24-control-reconcile-flake.md +0 -698
  75. package/docs/superpowers/plans/2026-09-24-probe-attached-clients.md +0 -605
  76. package/docs/superpowers/plans/2026-09-29-issue-145-holding-semantic-state.md +0 -1183
  77. package/docs/superpowers/plans/2026-09-29-issue-145-user-verification.md +0 -66
  78. package/docs/superpowers/plans/2026-09-30-board-delete-key-x-y-confirm.md +0 -649
  79. package/docs/superpowers/plans/2026-09-30-issue-153-runner-stop-boot-window.md +0 -618
  80. package/docs/superpowers/plans/2026-09-30-issue-95-test-discipline.md +0 -555
  81. package/docs/superpowers/plans/2026-09-30-publish-dispatch-tag-check.md +0 -259
  82. package/docs/superpowers/plans/2026-10-06-attach-settle-scheme-delivery.md +0 -751
  83. package/docs/superpowers/plans/2026-10-09-issue-167-attach-middle-paste.md +0 -509
  84. package/docs/superpowers/plans/2026-10-09-issue-169-fullscreen-mouse-ownership.md +0 -346
  85. package/docs/superpowers/plans/2026-10-10-attach-delete-in-place.md +0 -832
  86. package/docs/superpowers/plans/2026-10-10-issue-174-selection-mirror-paste-source.md +0 -510
  87. package/docs/superpowers/plans/2026-10-14-issue-170-attach-reply-guard.md +0 -771
  88. package/docs/superpowers/plans/2026-10-14-issue-178-accept-registration-race.md +0 -1257
  89. package/docs/superpowers/plans/2026-10-14-issue-184-snapshot-window-contract.md +0 -657
  90. package/docs/superpowers/plans/2026-10-14-issue-185-attach-wheel-alt-screen.md +0 -47
  91. package/docs/superpowers/plans/2026-10-14-issue-187-motionless-drag.md +0 -931
  92. package/docs/superpowers/plans/2026-10-14-issue-189-mixed-input-segments.md +0 -823
  93. package/docs/superpowers/plans/2026-10-15-issue-188-x10-mouse-release.md +0 -526
  94. package/docs/superpowers/plans/2026-10-16-issue-198-attach-wheel-forward-fallback.md +0 -32
  95. package/docs/superpowers/plans/2026-10-18-attach-wheel-replay-pseudo-scrollback.md +0 -437
  96. package/docs/superpowers/plans/2026-10-19-issue-213-reply-guard-csi-key.md +0 -257
  97. package/docs/superpowers/plans/2026-10-19-issue-214-attach-forward-alt-modifier.md +0 -202
  98. package/docs/superpowers/plans/2026-10-21-issue-221-ask-user-question.md +0 -198
  99. package/docs/superpowers/plans/2026-10-23-issue-222-attach-ready.md +0 -290
  100. package/docs/superpowers/plans/2026-10-23-issue-225-monotonic-foreground-state.md +0 -450
  101. package/docs/superpowers/plans/2026-10-23-issue-229-vim-jkl.md +0 -449
  102. package/docs/superpowers/plans/2026-10-23-issue-232-attach-bg-only-cells.md +0 -153
  103. package/docs/superpowers/plans/2026-10-23-lease-heartbeat-staleness.md +0 -537
  104. package/docs/superpowers/plans/2026-10-23-selected-row-highlight.md +0 -405
  105. package/docs/superpowers/plans/2026-10-24-issue-217-windows-pid-reuse-stopping.md +0 -367
  106. package/docs/superpowers/plans/2026-10-24-issue-230-warm-host-background-runs.md +0 -806
  107. package/docs/superpowers/plans/2026-10-24-issue-238-windows-process-identity-token.md +0 -490
  108. package/docs/superpowers/plans/2026-10-24-issue-241-coordinator-root-exit.md +0 -461
  109. package/docs/superpowers/plans/2026-10-25-issue-239-coordinator-replacement-identity.md +0 -504
  110. package/docs/superpowers/plans/2026-10-26-issue-245-key-hints-doc-sync.md +0 -244
  111. package/docs/superpowers/plans/2026-10-27-issue-172-fullscreen-selection-clear.md +0 -344
  112. package/docs/superpowers/plans/2026-10-27-issue-252-select-key-hints.md +0 -209
  113. package/docs/superpowers/plans/2026-10-28-issue-255-coordinator-endpoint-zombie.md +0 -442
  114. package/docs/superpowers/plans/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup.md +0 -256
  115. package/docs/superpowers/plans/2026-11-03-cwd-favorites-full-pool.md +0 -266
  116. package/docs/superpowers/specs/2026-08-15-screenlog-gc-design.md +0 -105
  117. package/docs/superpowers/specs/2026-08-16-attach-double-cursor-jiggle-retry-design.md +0 -142
  118. package/docs/superpowers/specs/2026-08-21-attach-coldstart-jiggle-rearm-design.md +0 -110
  119. package/docs/superpowers/specs/2026-08-21-dashboard-keypress-lag-design.md +0 -59
  120. package/docs/superpowers/specs/2026-08-21-issue-14-needs-input-no-auto-done-design.md +0 -109
  121. package/docs/superpowers/specs/2026-08-22-cwd-favorites-design.md +0 -131
  122. package/docs/superpowers/specs/2026-08-22-jiggle-shrink-and-hold-design.md +0 -111
  123. package/docs/superpowers/specs/2026-08-22-question-tool-grouping-design.md +0 -114
  124. package/docs/superpowers/specs/2026-08-22-rename-state-labels-design.md +0 -80
  125. package/docs/superpowers/specs/2026-08-27-locks-acquirelock-spin-design.md +0 -97
  126. package/docs/superpowers/specs/2026-08-27-pty-runner-test-flaky-design.md +0 -35
  127. package/docs/superpowers/specs/2026-08-29-code-refs-badges-design.md +0 -128
  128. package/docs/superpowers/specs/2026-08-30-circular-navigation-design.md +0 -47
  129. package/docs/superpowers/specs/2026-08-30-eprm-atomicwrite-race-design.md +0 -77
  130. package/docs/superpowers/specs/2026-08-30-post-exit-timing-fix-design.md +0 -80
  131. package/docs/superpowers/specs/2026-08-30-pty-attach-quality-debt-design.md +0 -105
  132. package/docs/superpowers/specs/2026-08-30-readme-v2-design.md +0 -115
  133. package/docs/superpowers/specs/2026-09-01-attach-detach-gate-cursor-anchor-design.md +0 -78
  134. package/docs/superpowers/specs/2026-09-02-attach-detach-editor-state-design.md +0 -120
  135. package/docs/superpowers/specs/2026-09-03-code-refs-pr-backlink-narrow-design.md +0 -92
  136. package/docs/superpowers/specs/2026-09-03-detach-gate-glyph-fallback-design.md +0 -99
  137. package/docs/superpowers/specs/2026-09-04-evidence-outputpreview-design.md +0 -50
  138. package/docs/superpowers/specs/2026-09-04-warm-host-reclaim-design.md +0 -106
  139. package/docs/superpowers/specs/2026-09-05-issue-13-drainnextfollowup-pty-probe-design.md +0 -64
  140. package/docs/superpowers/specs/2026-09-05-issue-38-windows-wezterm-ime-design.md +0 -48
  141. package/docs/superpowers/specs/2026-09-05-issue-39-truncate-codepoint-boundary-design.md +0 -64
  142. package/docs/superpowers/specs/2026-09-05-issue-61-mention-fallback-design.md +0 -71
  143. package/docs/superpowers/specs/2026-09-05-issue-63-flaky-manual-completion-design.md +0 -49
  144. package/docs/superpowers/specs/2026-09-05-issue-64-changelog-helper-design.md +0 -76
  145. package/docs/superpowers/specs/2026-09-05-pty-host-stacking-sock-race-design.md +0 -510
  146. package/docs/superpowers/specs/2026-09-07-issue-11-attach-runtime-desync-heal-design.md +0 -130
  147. package/docs/superpowers/specs/2026-09-08-attach-ctrl-left-detach-design.md +0 -58
  148. package/docs/superpowers/specs/2026-09-08-dashboard-shrink-repaint-design.md +0 -52
  149. package/docs/superpowers/specs/2026-09-08-legacy-stale-host-recovery-design.md +0 -87
  150. package/docs/superpowers/specs/2026-09-08-spawn-async-error-swallow-design.md +0 -56
  151. package/docs/superpowers/specs/2026-09-08-stale-model-attach-guard-design.md +0 -79
  152. package/docs/superpowers/specs/2026-09-09-claimpid-blocks-replace-design.md +0 -147
  153. package/docs/superpowers/specs/2026-09-09-harden-runner-architecture-design.md +0 -304
  154. package/docs/superpowers/specs/2026-09-14-attach-cursor-dectcem-gate-design.md +0 -114
  155. package/docs/superpowers/specs/2026-09-14-host-meta-orphan-lock-design.md +0 -120
  156. package/docs/superpowers/specs/2026-09-14-issue-106-terminal-frame-cognition-design.md +0 -146
  157. package/docs/superpowers/specs/2026-09-14-issue-113-foreground-preview-race-design.md +0 -116
  158. package/docs/superpowers/specs/2026-09-15-coordinator-pipe-root-normalize-design.md +0 -84
  159. package/docs/superpowers/specs/2026-09-15-lease-publish-eprem-reclaim-design.md +0 -92
  160. package/docs/superpowers/specs/2026-09-18-detach-anchor-reporter-endpoint-design.md +0 -123
  161. package/docs/superpowers/specs/2026-09-20-issue-121-perf-gate-out-of-coverage-design.md +0 -204
  162. package/docs/superpowers/specs/2026-09-23-a5-mid-stream-assertion-design.md +0 -43
  163. package/docs/superpowers/specs/2026-09-23-cwd-browse-tab-drill-design.md +0 -95
  164. package/docs/superpowers/specs/2026-09-23-terminal-query-forwarding-design.md +0 -63
  165. package/docs/superpowers/specs/2026-09-23-unconditional-detach-key-design.md +0 -57
  166. package/docs/superpowers/specs/2026-09-24-control-reconcile-flake-design.md +0 -98
  167. package/docs/superpowers/specs/2026-09-24-probe-attached-clients-design.md +0 -190
  168. package/docs/superpowers/specs/2026-09-29-issue-145-holding-semantic-state-design.md +0 -394
  169. package/docs/superpowers/specs/2026-09-30-board-delete-key-x-y-confirm-design.md +0 -86
  170. package/docs/superpowers/specs/2026-09-30-issue-153-runner-stop-boot-window-design.md +0 -75
  171. package/docs/superpowers/specs/2026-09-30-issue-95-test-discipline-design.md +0 -226
  172. package/docs/superpowers/specs/2026-09-30-publish-dispatch-tag-check-design.md +0 -129
  173. package/docs/superpowers/specs/2026-10-06-attach-settle-scheme-delivery-design.md +0 -196
  174. package/docs/superpowers/specs/2026-10-09-issue-167-attach-middle-paste-design.md +0 -125
  175. package/docs/superpowers/specs/2026-10-09-issue-169-fullscreen-mouse-ownership-design.md +0 -130
  176. package/docs/superpowers/specs/2026-10-10-attach-delete-in-place-design.md +0 -224
  177. package/docs/superpowers/specs/2026-10-10-issue-174-selection-mirror-paste-source-design.md +0 -154
  178. package/docs/superpowers/specs/2026-10-14-issue-170-attach-reply-guard-design.md +0 -195
  179. package/docs/superpowers/specs/2026-10-14-issue-178-accept-registration-race-design.md +0 -287
  180. package/docs/superpowers/specs/2026-10-14-issue-184-snapshot-window-contract-design.md +0 -288
  181. package/docs/superpowers/specs/2026-10-14-issue-185-attach-wheel-alt-screen-design.md +0 -126
  182. package/docs/superpowers/specs/2026-10-14-issue-187-motionless-drag-design.md +0 -269
  183. package/docs/superpowers/specs/2026-10-14-issue-189-mixed-input-segments-design.md +0 -369
  184. package/docs/superpowers/specs/2026-10-15-issue-188-x10-mouse-release-design.md +0 -252
  185. package/docs/superpowers/specs/2026-10-16-issue-198-attach-wheel-forward-fallback-design.md +0 -67
  186. package/docs/superpowers/specs/2026-10-18-attach-wheel-replay-pseudo-scrollback-design.md +0 -250
  187. package/docs/superpowers/specs/2026-10-19-issue-213-reply-guard-csi-key-design.md +0 -165
  188. package/docs/superpowers/specs/2026-10-19-issue-214-attach-forward-alt-modifier-design.md +0 -131
  189. package/docs/superpowers/specs/2026-10-20-issue-221-ask-user-question-design.md +0 -270
  190. package/docs/superpowers/specs/2026-10-22-issue-225-monotonic-foreground-state-design.md +0 -225
  191. package/docs/superpowers/specs/2026-10-23-issue-222-attach-ready-design.md +0 -204
  192. package/docs/superpowers/specs/2026-10-23-issue-229-vim-jkl-design.md +0 -90
  193. package/docs/superpowers/specs/2026-10-23-issue-232-attach-bg-only-cells-design.md +0 -146
  194. package/docs/superpowers/specs/2026-10-23-lease-heartbeat-staleness-design.md +0 -271
  195. package/docs/superpowers/specs/2026-10-23-selected-row-highlight-design.md +0 -191
  196. package/docs/superpowers/specs/2026-10-24-issue-217-windows-pid-reuse-stopping-design.md +0 -334
  197. package/docs/superpowers/specs/2026-10-24-issue-230-warm-host-background-runs-design.md +0 -283
  198. package/docs/superpowers/specs/2026-10-24-issue-238-windows-process-identity-token-design.md +0 -286
  199. package/docs/superpowers/specs/2026-10-24-issue-241-coordinator-root-exit-design.md +0 -381
  200. package/docs/superpowers/specs/2026-10-25-issue-239-coordinator-replacement-identity-design.md +0 -331
  201. package/docs/superpowers/specs/2026-10-26-issue-245-key-hints-doc-sync-design.md +0 -72
  202. package/docs/superpowers/specs/2026-10-27-issue-172-fullscreen-selection-clear-design.md +0 -248
  203. package/docs/superpowers/specs/2026-10-27-issue-252-select-key-hints-design.md +0 -95
  204. package/docs/superpowers/specs/2026-10-28-issue-255-coordinator-endpoint-zombie-design.md +0 -380
  205. package/docs/superpowers/specs/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup-design.md +0 -197
  206. package/docs/superpowers/specs/2026-11-03-cwd-favorites-full-pool-design.md +0 -106
  207. package/scripts/budget-audit.mjs +0 -135
  208. package/scripts/release.mjs +0 -446
  209. package/scripts/release_helper.mjs +0 -309
  210. package/scripts/run-perf-gate.mjs +0 -40
  211. 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
- }