@workerstack/repo 0.0.0-stage → 0.1.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 (438) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE +21 -0
  3. package/README.md +0 -3
  4. package/agents/global-settings/antigravity.json +3 -0
  5. package/agents/global-settings/claude.json +6 -0
  6. package/agents/global-settings/codex.toml +2 -0
  7. package/agents/global-settings/cursor.json +3 -0
  8. package/agents/global-settings/grok.toml +36 -0
  9. package/agents/global-settings/manifest.json +36 -0
  10. package/agents/grok/hooks/orchestrate-parent.json +60 -0
  11. package/agents/grok/hooks/rtk.json +16 -0
  12. package/agents/grok/hooks/rulesync.json +3 -0
  13. package/agents/grok/roles/scout.toml +5 -0
  14. package/agents/hooks.jsonc +67 -0
  15. package/agents/mcp-policy.jsonc +16 -0
  16. package/agents/rules/overview.md +21 -0
  17. package/agents/settings/claude.json +5 -0
  18. package/agents/skills/ask/SKILL.md +13 -0
  19. package/agents/skills/auto/SKILL.md +13 -0
  20. package/agents/skills/away/SKILL.md +19 -0
  21. package/agents/skills/blast-radius/SKILL.md +54 -0
  22. package/agents/skills/bro/SKILL.md +9 -0
  23. package/agents/skills/browser/SKILL.md +16 -0
  24. package/agents/skills/clasp/SKILL.md +11 -0
  25. package/agents/skills/commit/SKILL.md +35 -0
  26. package/agents/skills/commit-and-push/SKILL.md +9 -0
  27. package/agents/skills/commit-push/SKILL.md +59 -0
  28. package/agents/skills/commit-push-deploy/SKILL.md +74 -0
  29. package/agents/skills/commit-push-release/SKILL.md +75 -0
  30. package/agents/skills/correct/SKILL.md +39 -0
  31. package/agents/skills/do/SKILL.md +21 -0
  32. package/agents/skills/explain/SKILL.md +13 -0
  33. package/agents/skills/google-workspace/SKILL.md +12 -0
  34. package/agents/skills/handoff/SKILL.md +81 -0
  35. package/agents/skills/implement-plan/SKILL.md +29 -0
  36. package/agents/skills/implement-roadmap/SKILL.md +27 -0
  37. package/agents/skills/jev/SKILL.md +17 -0
  38. package/agents/skills/listen/SKILL.md +15 -0
  39. package/agents/skills/pickup/SKILL.md +58 -0
  40. package/agents/skills/principle-attack-the-premise/SKILL.md +24 -0
  41. package/agents/skills/principle-boundary-discipline/SKILL.md +38 -0
  42. package/agents/skills/principle-build-the-lever/SKILL.md +28 -0
  43. package/agents/skills/principle-encode-lessons-in-structure/SKILL.md +15 -0
  44. package/agents/skills/principle-exhaust-the-design-space/SKILL.md +12 -0
  45. package/agents/skills/principle-experience-first/SKILL.md +22 -0
  46. package/agents/skills/principle-explain-the-number/SKILL.md +21 -0
  47. package/agents/skills/principle-fix-root-causes/SKILL.md +24 -0
  48. package/agents/skills/principle-foundational-thinking/SKILL.md +25 -0
  49. package/agents/skills/principle-guard-the-context-window/SKILL.md +16 -0
  50. package/agents/skills/principle-laziness-protocol/SKILL.md +13 -0
  51. package/agents/skills/principle-make-operations-idempotent/SKILL.md +25 -0
  52. package/agents/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +18 -0
  53. package/agents/skills/principle-minimize-reader-load/SKILL.md +22 -0
  54. package/agents/skills/principle-model-the-domain/SKILL.md +26 -0
  55. package/agents/skills/principle-never-block-on-the-human/SKILL.md +17 -0
  56. package/agents/skills/principle-outcome-oriented-execution/SKILL.md +25 -0
  57. package/agents/skills/principle-prove-it-works/SKILL.md +25 -0
  58. package/agents/skills/principle-redesign-from-first-principles/SKILL.md +18 -0
  59. package/agents/skills/principle-separate-before-serializing-shared-state/SKILL.md +20 -0
  60. package/agents/skills/principle-sequence-verifiable-units/SKILL.md +22 -0
  61. package/agents/skills/principle-subtract-before-you-add/SKILL.md +21 -0
  62. package/agents/skills/principle-test-behavior-not-implementation/SKILL.md +24 -0
  63. package/agents/skills/principle-type-system-discipline/SKILL.md +17 -0
  64. package/agents/skills/review-plan/SKILL.md +28 -0
  65. package/agents/skills/review-roadmap/SKILL.md +28 -0
  66. package/agents/skills/show-me/SKILL.md +21 -0
  67. package/agents/skills/speak/SKILL.md +15 -0
  68. package/agents/skills/su/SKILL.md +11 -0
  69. package/agents/skills/teach/SKILL.md +25 -0
  70. package/agents/skills/tmp/SKILL.md +54 -0
  71. package/agents/skills/typescript-best-practices/SKILL.md +32 -0
  72. package/agents/skills/typescript-best-practices/references/patterns.md +328 -0
  73. package/agents/skills/unslop/SKILL.md +62 -0
  74. package/agents/skills/write-plan/SKILL.md +33 -0
  75. package/agents/skills/write-roadmap/SKILL.md +66 -0
  76. package/agents/subagents/implementer-deep.md +57 -0
  77. package/agents/subagents/implementer-fast.md +57 -0
  78. package/agents/subagents/orchestrator.md +43 -0
  79. package/agents/subagents/researcher.md +47 -0
  80. package/agents/subagents/scout.md +44 -0
  81. package/bin/repo.ts +4 -0
  82. package/cli/actionlint-default.test.ts +38 -0
  83. package/cli/actionlint-download.test.ts +162 -0
  84. package/cli/actionlint-real.test.ts +33 -0
  85. package/cli/actionlint.constants.ts +43 -0
  86. package/cli/actionlint.test.ts +108 -0
  87. package/cli/actionlint.ts +178 -0
  88. package/cli/bin.test.ts +54 -0
  89. package/cli/bin.ts +40 -0
  90. package/cli/check-changelog.test.ts +40 -0
  91. package/cli/check-changelog.ts +12 -0
  92. package/cli/check-comments.test.ts +157 -0
  93. package/cli/check-comments.ts +123 -0
  94. package/cli/check-env-modules.test.ts +85 -0
  95. package/cli/check-env-modules.ts +56 -0
  96. package/cli/check-magic.test.ts +139 -0
  97. package/cli/check-magic.ts +120 -0
  98. package/cli/check-manifests.constants.ts +4 -0
  99. package/cli/check-manifests.test.ts +210 -0
  100. package/cli/check-manifests.ts +71 -0
  101. package/cli/check-nightly.test.ts +103 -0
  102. package/cli/check-nightly.ts +37 -0
  103. package/cli/check-ratchets-missing.test.ts +46 -0
  104. package/cli/check-ratchets-mutation.test.ts +49 -0
  105. package/cli/check-ratchets.constants.ts +20 -0
  106. package/cli/check-ratchets.test.ts +190 -0
  107. package/cli/check-ratchets.ts +193 -0
  108. package/cli/check-rejections.test.ts +220 -0
  109. package/cli/check-rejections.ts +153 -0
  110. package/cli/check-reports.ts +37 -0
  111. package/cli/check-tsconfig.test.ts +507 -0
  112. package/cli/check-tsconfig.ts +205 -0
  113. package/cli/commands.test.ts +114 -0
  114. package/cli/commands.ts +218 -0
  115. package/cli/commit-changelog.ts +27 -0
  116. package/cli/commit-msg-changelog.test.ts +93 -0
  117. package/cli/commit-msg-range.test.ts +126 -0
  118. package/cli/commit-msg-red-proof.test.ts +147 -0
  119. package/cli/commit-msg-repo.test.ts +153 -0
  120. package/cli/commit-msg.test.ts +101 -0
  121. package/cli/commit-msg.ts +194 -0
  122. package/cli/commit-range.constants.ts +17 -0
  123. package/cli/commit-range.ts +57 -0
  124. package/cli/config-knip.test.ts +70 -0
  125. package/cli/config-nightly.test.ts +84 -0
  126. package/cli/config-tdd.test.ts +46 -0
  127. package/cli/config.test.ts +576 -0
  128. package/cli/config.ts +261 -0
  129. package/cli/conventional.ts +9 -0
  130. package/cli/coverage-config.ts +62 -0
  131. package/cli/coverage.constants.ts +6 -0
  132. package/cli/coverage.test.ts +199 -0
  133. package/cli/coverage.ts +127 -0
  134. package/cli/env.constants.ts +10 -0
  135. package/cli/env.test.ts +56 -0
  136. package/cli/env.ts +72 -0
  137. package/cli/fixtures/base-config.ts +13 -0
  138. package/cli/fixtures/gate-world.ts +48 -0
  139. package/cli/fixtures/knip-run.ts +38 -0
  140. package/cli/fixtures/mutate-fakes.ts +137 -0
  141. package/cli/fixtures/planted-repo.ts +50 -0
  142. package/cli/fixtures/publish-world.ts +71 -0
  143. package/cli/fixtures/red-proof-repo.ts +72 -0
  144. package/cli/fixtures/release-context.ts +15 -0
  145. package/cli/fixtures/release-world.ts +111 -0
  146. package/cli/fixtures/stub-bin.ts +40 -0
  147. package/cli/guard-diagnostics.test.ts +157 -0
  148. package/cli/guard-diagnostics.ts +195 -0
  149. package/cli/init.test.ts +487 -0
  150. package/cli/init.ts +275 -0
  151. package/cli/knip-check-ignores.test.ts +106 -0
  152. package/cli/knip-check-real.test.ts +65 -0
  153. package/cli/knip-check.constants.ts +46 -0
  154. package/cli/knip-check.test.ts +114 -0
  155. package/cli/knip-check.ts +169 -0
  156. package/cli/knip-schema.ts +67 -0
  157. package/cli/known-failures.constants.ts +22 -0
  158. package/cli/known-failures.test.ts +138 -0
  159. package/cli/known-failures.ts +131 -0
  160. package/cli/lint/as-needs-reason.ts +41 -0
  161. package/cli/lint/boundaries.ts +73 -0
  162. package/cli/lint/class-file.ts +39 -0
  163. package/cli/lint/config-file.ts +32 -0
  164. package/cli/lint/entry-file.ts +54 -0
  165. package/cli/lint/env-in-config.ts +66 -0
  166. package/cli/lint/filename-matches-export.ts +63 -0
  167. package/cli/lint/index-file.ts +22 -0
  168. package/cli/lint/index.ts +47 -0
  169. package/cli/lint/is-reason.ts +6 -0
  170. package/cli/lint/manifest.ts +46 -0
  171. package/cli/lint/names.ts +59 -0
  172. package/cli/lint/no-commented-out-code.ts +105 -0
  173. package/cli/lint/no-mock-of-subject.ts +64 -0
  174. package/cli/lint/no-module-side-effects.ts +94 -0
  175. package/cli/lint/no-module-state.ts +58 -0
  176. package/cli/lint/no-side-effect-import.ts +17 -0
  177. package/cli/lint/no-v8-ignore.ts +18 -0
  178. package/cli/lint/script-rules.ts +9 -0
  179. package/cli/lint/skip-on-platform.constants.ts +8 -0
  180. package/cli/lint/skip-on-platform.ts +118 -0
  181. package/cli/lint/todo-needs-id.ts +63 -0
  182. package/cli/lint/types-file.ts +28 -0
  183. package/cli/lint-schema.ts +32 -0
  184. package/cli/magic/fixtures/magic-repo.ts +64 -0
  185. package/cli/magic/magic-hit.ts +2 -0
  186. package/cli/magic/magic-nodes.ts +44 -0
  187. package/cli/magic/magic-numbers.test.ts +99 -0
  188. package/cli/magic/magic-numbers.ts +117 -0
  189. package/cli/magic/magic-strings.test.ts +38 -0
  190. package/cli/magic/magic-strings.ts +81 -0
  191. package/cli/main.test.ts +853 -0
  192. package/cli/main.ts +292 -0
  193. package/cli/mutate-args.ts +79 -0
  194. package/cli/mutate-check.test.ts +147 -0
  195. package/cli/mutate-deps.ts +42 -0
  196. package/cli/mutate-edges.test.ts +150 -0
  197. package/cli/mutate-files.test.ts +100 -0
  198. package/cli/mutate-files.ts +31 -0
  199. package/cli/mutate-floors.test.ts +91 -0
  200. package/cli/mutate-floors.ts +104 -0
  201. package/cli/mutate-real-deps.ts +57 -0
  202. package/cli/mutate-real.test.ts +96 -0
  203. package/cli/mutate-report.ts +96 -0
  204. package/cli/mutate-run.ts +128 -0
  205. package/cli/mutate-shards.ts +64 -0
  206. package/cli/mutate.constants.ts +67 -0
  207. package/cli/mutate.test.ts +182 -0
  208. package/cli/mutate.ts +64 -0
  209. package/cli/nested-vitest-flags.ts +8 -0
  210. package/cli/nightly-config.constants.ts +14 -0
  211. package/cli/nightly-config.ts +62 -0
  212. package/cli/prompt.test.ts +202 -0
  213. package/cli/prompt.ts +1 -0
  214. package/cli/red-proof-edges.test.ts +78 -0
  215. package/cli/red-proof-failures.test.ts +82 -0
  216. package/cli/red-proof-tree.test.ts +157 -0
  217. package/cli/red-proof-verdict.test.ts +44 -0
  218. package/cli/red-proof.constants.ts +14 -0
  219. package/cli/red-proof.ts +202 -0
  220. package/cli/release/changelog-parse.ts +83 -0
  221. package/cli/release/changelog.test.ts +141 -0
  222. package/cli/release/changelog.ts +106 -0
  223. package/cli/release/gate-refusals.test.ts +85 -0
  224. package/cli/release/gate.test.ts +63 -0
  225. package/cli/release/gate.ts +70 -0
  226. package/cli/release/git.ts +66 -0
  227. package/cli/release/github.test.ts +128 -0
  228. package/cli/release/github.ts +46 -0
  229. package/cli/release/list-dir.test.ts +11 -0
  230. package/cli/release/list-dir.ts +6 -0
  231. package/cli/release/main.test.ts +48 -0
  232. package/cli/release/main.ts +51 -0
  233. package/cli/release/manifest.test.ts +24 -0
  234. package/cli/release/manifest.ts +34 -0
  235. package/cli/release/pack.test.ts +107 -0
  236. package/cli/release/pack.ts +76 -0
  237. package/cli/release/plan.ts +127 -0
  238. package/cli/release/publish-run.test.ts +98 -0
  239. package/cli/release/publish.test.ts +127 -0
  240. package/cli/release/publish.ts +125 -0
  241. package/cli/release/refuse.ts +5 -0
  242. package/cli/release/release-messages.constants.ts +96 -0
  243. package/cli/release/release-refusals.test.ts +149 -0
  244. package/cli/release/release.constants.ts +128 -0
  245. package/cli/release/release.test.ts +199 -0
  246. package/cli/release/release.ts +145 -0
  247. package/cli/release/version.test.ts +118 -0
  248. package/cli/release/version.ts +69 -0
  249. package/cli/release/wait.test.ts +101 -0
  250. package/cli/release/wait.ts +95 -0
  251. package/cli/script-diagnostics.test.ts +76 -0
  252. package/cli/script-diagnostics.ts +62 -0
  253. package/cli/scripts/board.ts +519 -0
  254. package/cli/scripts/browser.ts +994 -0
  255. package/cli/scripts/change-set.ts +105 -0
  256. package/cli/scripts/check-globals.ts +199 -0
  257. package/cli/scripts/ci-skips.constants.ts +17 -0
  258. package/cli/scripts/ci-skips.ts +118 -0
  259. package/cli/scripts/ci-wait-run.ts +28 -0
  260. package/cli/scripts/ci-wait.constants.ts +44 -0
  261. package/cli/scripts/ci-wait.ts +175 -0
  262. package/cli/scripts/ci.constants.ts +15 -0
  263. package/cli/scripts/ci.ts +208 -0
  264. package/cli/scripts/clean.ts +363 -0
  265. package/cli/scripts/comments-step.ts +15 -0
  266. package/cli/scripts/doctor.ts +828 -0
  267. package/cli/scripts/emit-agent-shell-tools.ts +91 -0
  268. package/cli/scripts/emit-mcp-policy.ts +402 -0
  269. package/cli/scripts/folder-map.ts +353 -0
  270. package/cli/scripts/gate-fat-tools.ts +1095 -0
  271. package/cli/scripts/gitignore-guard.ts +338 -0
  272. package/cli/scripts/google-workspace.ts +779 -0
  273. package/cli/scripts/hook-command.ts +72 -0
  274. package/cli/scripts/hook-input.ts +178 -0
  275. package/cli/scripts/hook-io.constants.ts +5 -0
  276. package/cli/scripts/hook-io.ts +46 -0
  277. package/cli/scripts/hooks-union.ts +469 -0
  278. package/cli/scripts/interactive.ts +30 -0
  279. package/cli/scripts/jev.ts +420 -0
  280. package/cli/scripts/job.ts +934 -0
  281. package/cli/scripts/jsonc.ts +9 -0
  282. package/cli/scripts/lessons.ts +564 -0
  283. package/cli/scripts/lint-baseline.ts +233 -0
  284. package/cli/scripts/lint-main.ts +24 -0
  285. package/cli/scripts/lint-skips.ts +139 -0
  286. package/cli/scripts/lint.constants.ts +6 -0
  287. package/cli/scripts/lint.ts +142 -0
  288. package/cli/scripts/listen.ts +962 -0
  289. package/cli/scripts/managed-text.ts +47 -0
  290. package/cli/scripts/orchestrate-parent.ts +1155 -0
  291. package/cli/scripts/parse-need.ts +81 -0
  292. package/cli/scripts/persist-handoff.ts +228 -0
  293. package/cli/scripts/persist-implementation.ts +276 -0
  294. package/cli/scripts/persist-plan.ts +888 -0
  295. package/cli/scripts/persist-roadmap.ts +478 -0
  296. package/cli/scripts/persist-tmp.ts +177 -0
  297. package/cli/scripts/persist.ts +54 -0
  298. package/cli/scripts/phase-files.ts +121 -0
  299. package/cli/scripts/prompt.ts +38 -0
  300. package/cli/scripts/purge-global-mcps.ts +313 -0
  301. package/cli/scripts/rejections-step.ts +37 -0
  302. package/cli/scripts/rtk-grok.ts +102 -0
  303. package/cli/scripts/scrub-spawn-prompt.ts +103 -0
  304. package/cli/scripts/scrub-text-lib.ts +58 -0
  305. package/cli/scripts/session-map.ts +69 -0
  306. package/cli/scripts/setup.ts +862 -0
  307. package/cli/scripts/skill-conflicts.ts +82 -0
  308. package/cli/scripts/speak.ts +370 -0
  309. package/cli/scripts/strategy.ts +168 -0
  310. package/cli/scripts/sync-global-settings.ts +316 -0
  311. package/cli/scripts/turn-steps.ts +249 -0
  312. package/cli/scripts/turn.ts +157 -0
  313. package/cli/scripts/typecheck-step.ts +43 -0
  314. package/cli/scripts/vscode-tasks.ts +175 -0
  315. package/cli/scripts/workspace-root.ts +160 -0
  316. package/cli/scripts/worktree.ts +806 -0
  317. package/cli/spawn.test.ts +34 -0
  318. package/cli/spawn.ts +27 -0
  319. package/cli/staged-change.constants.ts +11 -0
  320. package/cli/staged-change.ts +79 -0
  321. package/cli/stryker-preload.test.ts +50 -0
  322. package/cli/stryker-preload.ts +9 -0
  323. package/cli/sync/agents/frontmatter.test.ts +197 -0
  324. package/cli/sync/agents/frontmatter.ts +140 -0
  325. package/cli/sync/agents/hooks.test.ts +517 -0
  326. package/cli/sync/agents/hooks.ts +401 -0
  327. package/cli/sync/agents/rules.test.ts +229 -0
  328. package/cli/sync/agents/rules.ts +84 -0
  329. package/cli/sync/agents/sources.test.ts +375 -0
  330. package/cli/sync/agents/sources.ts +244 -0
  331. package/cli/sync/agents/subagents.test.ts +149 -0
  332. package/cli/sync/agents/subagents.ts +158 -0
  333. package/cli/sync/agents-text.test.ts +31 -0
  334. package/cli/sync/agents-text.ts +36 -0
  335. package/cli/sync/fixtures/copied.README.md +35 -0
  336. package/cli/sync/fixtures/copied.tgz +0 -0
  337. package/cli/sync/fixtures/maps/golden/INDEX.md +22 -0
  338. package/cli/sync/fixtures/maps-ws.tgz +0 -0
  339. package/cli/sync/git-files.test.ts +340 -0
  340. package/cli/sync/git-files.ts +197 -0
  341. package/cli/sync/git-hooks.test.ts +94 -0
  342. package/cli/sync/golden.test.ts +15 -0
  343. package/cli/sync/hooks-mode.test.ts +99 -0
  344. package/cli/sync/hosts.ts +28 -0
  345. package/cli/sync/knip-config.constants.ts +4 -0
  346. package/cli/sync/knip-config.test.ts +147 -0
  347. package/cli/sync/knip-config.ts +108 -0
  348. package/cli/sync/manifest.test.ts +78 -0
  349. package/cli/sync/manifest.ts +60 -0
  350. package/cli/sync/maps.test.ts +304 -0
  351. package/cli/sync/maps.ts +297 -0
  352. package/cli/sync/migrate.test.ts +2000 -0
  353. package/cli/sync/migrate.ts +1104 -0
  354. package/cli/sync/nightly-jobs.test.ts +63 -0
  355. package/cli/sync/nightly.constants.ts +48 -0
  356. package/cli/sync/nightly.test.ts +100 -0
  357. package/cli/sync/nightly.ts +75 -0
  358. package/cli/sync/outputs.test.ts +351 -0
  359. package/cli/sync/outputs.ts +168 -0
  360. package/cli/sync/oxlint-config.test.ts +178 -0
  361. package/cli/sync/oxlint-config.ts +168 -0
  362. package/cli/sync/release-jobs.test.ts +29 -0
  363. package/cli/sync/release-reports.test.ts +31 -0
  364. package/cli/sync/release-workflow.constants.ts +61 -0
  365. package/cli/sync/release-workflow.test.ts +192 -0
  366. package/cli/sync/release-workflow.ts +86 -0
  367. package/cli/sync/release.constants.ts +93 -0
  368. package/cli/sync/release.test.ts +187 -0
  369. package/cli/sync/release.ts +112 -0
  370. package/cli/sync/repo-dir.test.ts +371 -0
  371. package/cli/sync/repo-dir.ts +205 -0
  372. package/cli/sync/standards.constants.ts +8 -0
  373. package/cli/sync/standards.test.ts +206 -0
  374. package/cli/sync/standards.ts +179 -0
  375. package/cli/sync/stock-history.json +2796 -0
  376. package/cli/sync/stock-history.test.ts +66 -0
  377. package/cli/sync/stock-history.ts +84 -0
  378. package/cli/sync/stock.test.ts +47 -0
  379. package/cli/sync/stock.ts +24 -0
  380. package/cli/sync/trees.test.ts +621 -0
  381. package/cli/sync/trees.ts +280 -0
  382. package/cli/sync/validate.test.ts +299 -0
  383. package/cli/sync/validate.ts +402 -0
  384. package/cli/sync/workspace.test.ts +180 -0
  385. package/cli/sync/workspace.ts +138 -0
  386. package/cli/sync/write.test.ts +341 -0
  387. package/cli/sync/write.ts +231 -0
  388. package/cli/sync.test.ts +1495 -0
  389. package/cli/sync.ts +360 -0
  390. package/cli/tdd-scope.test.ts +36 -0
  391. package/cli/tdd-scope.ts +41 -0
  392. package/cli/test-files.ts +16 -0
  393. package/cli/ts-program.ts +69 -0
  394. package/cli/tsconfig.constants.ts +8 -0
  395. package/cli/workerstack-config.types.ts +21 -0
  396. package/dist/cli/env.d.ts +10 -0
  397. package/dist/cli/env.js +64 -0
  398. package/dist/presets/temp-root-env.d.ts +10 -0
  399. package/dist/presets/temp-root-env.js +20 -0
  400. package/dist/presets/temp-root.constants.d.ts +6 -0
  401. package/dist/presets/temp-root.constants.js +6 -0
  402. package/dist/presets/temp-root.d.ts +2 -0
  403. package/dist/presets/temp-root.js +60 -0
  404. package/dist/presets/vitest.constants.d.ts +16 -0
  405. package/dist/presets/vitest.constants.js +16 -0
  406. package/dist/presets/vitest.d.ts +11 -0
  407. package/dist/presets/vitest.js +67 -0
  408. package/dist/presets/workers-plugin.d.ts +10 -0
  409. package/dist/presets/workers-plugin.js +54 -0
  410. package/package.json +141 -4
  411. package/references/INDEX.md +100 -0
  412. package/references/browser.md +66 -0
  413. package/references/cascade.md +15 -0
  414. package/references/clasp.md +21 -0
  415. package/references/cloudflare.md +36 -0
  416. package/references/google-workspace.md +79 -0
  417. package/references/hooks.md +103 -0
  418. package/references/implementation-format.md +119 -0
  419. package/references/jev.md +56 -0
  420. package/references/lessons.md +73 -0
  421. package/references/listen.md +80 -0
  422. package/references/map.md +107 -0
  423. package/references/output.md +47 -0
  424. package/references/plan-format.md +264 -0
  425. package/references/review-loop.md +48 -0
  426. package/references/roadmap-format.md +104 -0
  427. package/references/rtk.md +27 -0
  428. package/references/scope.md +76 -0
  429. package/references/setup.md +42 -0
  430. package/references/speak.md +59 -0
  431. package/tsconfig/base.json +28 -0
  432. package/tsconfig/bun.d.ts +1 -0
  433. package/tsconfig/bun.json +8 -0
  434. package/tsconfig/reset.d.ts +1 -0
  435. package/tsconfig/strict.json +12 -0
  436. package/tsconfig/web.json +7 -0
  437. package/tsconfig/worker.d.ts +1 -0
  438. package/tsconfig/worker.json +8 -0
@@ -0,0 +1,149 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { removePlanted, tempDir, write } from "../fixtures/planted-repo.ts";
3
+ import { NAME, commit, fakeGh, feat, gitIn, release, snapshot, world, type World } from "../fixtures/release-world.ts";
4
+
5
+ afterEach(removePlanted);
6
+
7
+ const USAGE = "usage: repo release [--stable | --pre <id>] | wait | gate | pack | publish | github <name>@<version>";
8
+ /** What a refused release leaves: exit 1, the one line on stderr, nothing on stdout. */
9
+ const refusal = (line: string) => ({ code: 1, out: "", err: `repo release: ${line}\n`, wrote: false });
10
+
11
+ /** Runs a release and reports whether it wrote anything to the checkout or origin. */
12
+ async function attempt(w: World, argv: string[] = [], gh = fakeGh()) {
13
+ const before = snapshot(w);
14
+ const got = await release(w, argv, gh);
15
+ return { ...got, wrote: JSON.stringify(snapshot(w)) !== JSON.stringify(before) };
16
+ }
17
+
18
+ describe("repo release refuses the state of the checkout", () => {
19
+ it("a working tree with changes", async () => {
20
+ const w = world();
21
+ feat(w);
22
+ write(w.work, { "stray.txt": "x\n" });
23
+ expect(await attempt(w)).toEqual(refusal("the working tree has changes; commit or stash them first"));
24
+ });
25
+
26
+ it("a branch that is not main", async () => {
27
+ const w = world();
28
+ feat(w);
29
+ gitIn(w.work, "checkout", "-q", "-b", "wt/x");
30
+ expect(await attempt(w)).toEqual(refusal("release from main; this checkout is on wt/x"));
31
+ });
32
+
33
+ it("a main behind origin/main", async () => {
34
+ const w = world();
35
+ feat(w);
36
+ gitIn(w.work, "reset", "-q", "--hard", "HEAD~1");
37
+ const differ = "main and origin/main differ (1 behind, 0 ahead); pull or push first";
38
+ expect(await attempt(w)).toEqual(refusal(differ));
39
+ });
40
+
41
+ it("a main ahead of origin/main", async () => {
42
+ const w = world();
43
+ feat(w, false);
44
+ const differ = "main and origin/main differ (0 behind, 1 ahead); pull or push first";
45
+ expect(await attempt(w)).toEqual(refusal(differ));
46
+ });
47
+ });
48
+
49
+ describe("repo release refuses what the commits and files say", () => {
50
+ it("no feat, fix, perf, revert or breaking commit since the last tag", async () => {
51
+ const w = world();
52
+ commit(w, "docs: words", { "README.md": "x\n" });
53
+ const line = `nothing to release since ${NAME}@0.1.0: no feat, fix, perf, revert or breaking commit`;
54
+ expect(await attempt(w)).toEqual(refusal(line));
55
+ });
56
+
57
+ it("an empty Unreleased", async () => {
58
+ const w = world();
59
+ commit(w, "feat: add x", { "src/x.ts": "export const x = 1;\n" });
60
+ const line = "CHANGELOG.md has no entries under ## [Unreleased]; add them first";
61
+ expect(await attempt(w)).toEqual(refusal(line));
62
+ });
63
+
64
+ it("--stable on a version that is already 1.x", async () => {
65
+ const w = world({ "package.json": { name: NAME, version: "1.2.0" } }, false);
66
+ gitIn(w.work, "tag", "-a", `${NAME}@1.2.0`, "-m", "x");
67
+ gitIn(w.work, "push", "-q", "origin", "--tags");
68
+ feat(w);
69
+ const line = `--stable moves a 0.x version to 1.0.0; ${NAME} is 1.2.0`;
70
+ expect(await attempt(w, ["--stable"])).toEqual(refusal(line));
71
+ });
72
+
73
+ it("a tag the next version already has", async () => {
74
+ const w = world();
75
+ feat(w);
76
+ gitIn(w.work, "tag", "-a", `${NAME}@0.1.1`, "-m", "x");
77
+ gitIn(w.work, "push", "-q", "origin", "--tags");
78
+ const line = `the tag ${NAME}@0.1.1 already exists; a tag is never moved, so release the next version`;
79
+ expect(await attempt(w)).toEqual(refusal(line));
80
+ });
81
+ });
82
+
83
+ describe("repo release refuses the package and its config", () => {
84
+ it("a private package", async () => {
85
+ const w = world({ "package.json": { name: NAME, version: "0.1.0", private: true } });
86
+ feat(w);
87
+ expect(await attempt(w)).toEqual(refusal(`${NAME} is private; set "private": false to publish it`));
88
+ });
89
+
90
+ it("a repo without the release pack", async () => {
91
+ const w = world({ "workerstack.config.ts": "export default { packs: [] };\n" });
92
+ feat(w);
93
+ const line = 'the release pack is off; add "release" to packs in workerstack.config.ts';
94
+ expect(await attempt(w)).toEqual(refusal(line));
95
+ });
96
+ });
97
+
98
+ describe("repo release refuses a manifest it cannot use", () => {
99
+ it("a broken config", async () => {
100
+ const w = world({ "workerstack.config.ts": "export default 3;\n" });
101
+ const got = await attempt(w);
102
+ expect(got).toMatchObject({ code: 1, out: "", wrote: false });
103
+ expect(got.err).toMatch(/^repo release: .*default export/);
104
+ });
105
+
106
+ it("a manifest that is not JSON", async () => {
107
+ const w = world({ "package.json": "[" });
108
+ expect(await attempt(w)).toEqual(refusal("package.json needs a name and a version"));
109
+ });
110
+
111
+ it("a manifest without a version", async () => {
112
+ const w = world({ "package.json": { name: NAME } });
113
+ expect(await attempt(w)).toEqual(refusal("package.json needs a name and a version"));
114
+ });
115
+
116
+ it("a version that is not semver", async () => {
117
+ const w = world({ "package.json": { name: NAME, version: "latest" } });
118
+ expect(await attempt(w)).toEqual(refusal(`${NAME} is at latest, which is not semver`));
119
+ });
120
+ });
121
+
122
+ describe("repo release refuses where it runs", () => {
123
+ it("gh that is missing or logged out", async () => {
124
+ const w = world();
125
+ feat(w);
126
+ const line = "gh is not installed or not logged in; run `gh auth login`";
127
+ expect(await attempt(w, [], fakeGh({ auth: false }))).toEqual(refusal(line));
128
+ });
129
+
130
+ it("an origin that cannot be fetched", async () => {
131
+ const w = world();
132
+ gitIn(w.work, "remote", "set-url", "origin", "/nonexistent/remote.git");
133
+ const got = await attempt(w);
134
+ expect(got.code).toBe(1);
135
+ expect(got.err).toMatch(/^repo release: git fetch failed: /);
136
+ });
137
+
138
+ it("a directory that is not a git checkout", async () => {
139
+ const got = await release({ work: tempDir(), remote: "" }, []);
140
+ expect(got).toEqual({ code: 1, out: "", err: "repo release: run it inside a git checkout\n" });
141
+ });
142
+
143
+ it.each([[["--pre"]], [["--pre", "r c"]], [["--stable", "--pre", "rc"]], [["--bogus"]]])(
144
+ "the arguments %j",
145
+ async (argv) => {
146
+ expect(await attempt(world(), argv)).toEqual(refusal(USAGE));
147
+ },
148
+ );
149
+ });
@@ -0,0 +1,128 @@
1
+ /** Where the semver of a version string splits: major, minor, patch and an optional `-id.N` prerelease. */
2
+ export const SEMVER: RegExp = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z]+)\.(\d+))?$/;
3
+ export const FIRST_VERSION: string = "0.1.0";
4
+ export const STABLE_VERSION: string = "1.0.0";
5
+ export const STABLE_MAJOR: number = 1;
6
+ /** Commit types that make a release, and the bump each makes from 1.0. */
7
+ export const PATCH_TYPES: readonly string[] = ["fix", "perf", "revert"];
8
+ export const MINOR_TYPES: readonly string[] = ["feat"];
9
+
10
+ export const CHANGELOG_FILE: string = "CHANGELOG.md";
11
+ /** The sections of Keep a Changelog, in the order they are written. */
12
+ export const SECTIONS: readonly string[] = ["Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"];
13
+ export const UNRELEASED: string = "Unreleased";
14
+ export const DATE_FORMAT: string = "YYYY-MM-DD";
15
+ export const FENCE: string = "```";
16
+ export const HEADING: RegExp = /^(##|###) (.*)$/;
17
+ export const VERSION_HASHES: string = "##";
18
+ export const SECTION_LIST: string = "Added, Changed, Deprecated, Removed, Fixed or Security";
19
+ export const ZERO_VERSION: string = "0.0.0";
20
+ export const WAIT_COMMAND: string = "wait";
21
+ export const HELP_COMMAND: string = "help";
22
+ export const GATE_COMMAND: string = "gate";
23
+ export const PACK_COMMAND: string = "pack";
24
+ export const PUBLISH_COMMAND: string = "publish";
25
+ export const GITHUB_COMMAND: string = "github";
26
+ export const GITHUB_ACTIONS_ON: string = "true";
27
+ export const VERSION_HEADING: RegExp = /^\[([^\]]+)\]/;
28
+ export const DATED: RegExp = /^\[[^\]]+\] - \d{4}-\d{2}-\d{2}$/;
29
+
30
+ export const RELEASE_PACK: string = "release";
31
+ export const ARROW: string = "→";
32
+ export const FLAG_STABLE: string = "--stable";
33
+ export const FLAG_PRE: string = "--pre";
34
+ export const PRE_ID: RegExp = /^[0-9A-Za-z]+$/;
35
+ export const MAIN_FALLBACK: string = "main";
36
+ export const ORIGIN_HEAD_PREFIX: string = "refs/remotes/origin/";
37
+ export const RELEASE_SUBJECT = (tag: string): string => `chore: release ${tag}`;
38
+ export const TAG_OF = (name: string, version: string): string => `${name}@${version}`;
39
+ export const BARE_TAG_OF = (version: string): string => `v${version}`;
40
+ export const SHA_SHOWN: number = 7;
41
+ export const COMMIT_SEPARATOR: string = "\x1f";
42
+ export const COMMIT_END: string = "\x1e";
43
+ export const MANIFEST_FILE: string = "package.json";
44
+ export const VERSION_LINE: RegExp = /("version"\s*:\s*)"[^"]*"/;
45
+ export const GIT_TOPLEVEL: readonly string[] = ["rev-parse", "--show-toplevel"];
46
+ export const GIT_ORIGIN_HEAD: readonly string[] = ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"];
47
+ export const GIT_BRANCH: readonly string[] = ["rev-parse", "--abbrev-ref", "HEAD"];
48
+ export const GIT_HEAD: readonly string[] = ["rev-parse", "HEAD"];
49
+ export const GIT_STATUS: readonly string[] = ["status", "--porcelain"];
50
+ export const GIT_FETCH: readonly string[] = ["fetch", "--quiet", "--tags", "origin"];
51
+ export const GIT_RESET: readonly string[] = ["reset", "--quiet", "--hard", "HEAD"];
52
+ export const GIT_AHEAD_BEHIND = (branch: string): string[] => [
53
+ "rev-list",
54
+ "--left-right",
55
+ "--count",
56
+ `HEAD...origin/${branch}`,
57
+ ];
58
+ export const GIT_TAG_REF = (tag: string): string[] => ["rev-parse", "--quiet", "--verify", `refs/tags/${tag}^{commit}`];
59
+ export const GIT_RELEASE_LOG = (subject: string): string[] => [
60
+ "log",
61
+ "--fixed-strings",
62
+ `--grep=${subject}`,
63
+ "--format=%H%x09%s",
64
+ ];
65
+ export const GIT_COMMITS = (since: string | null): string[] => [
66
+ "log",
67
+ `--format=%s%x1f%b%x1e`,
68
+ ...(since === null ? [] : [`${since}..HEAD`]),
69
+ ];
70
+ export const GIT_ADD: readonly string[] = ["add", "--", MANIFEST_FILE, CHANGELOG_FILE];
71
+ export const GIT_COMMIT = (subject: string): string[] => ["commit", "--quiet", "-m", subject];
72
+ export const GIT_PUSH = (branch: string): string[] => ["push", "--quiet", "origin", branch];
73
+ export const GIT_TAG = (tag: string): string[] => ["tag", "-a", tag, "-m", tag];
74
+ export const GIT_PUSH_TAG = (tag: string): string[] => ["push", "--quiet", "origin", `refs/tags/${tag}`];
75
+ export const GIT_TAG_DELETE = (tag: string): string[] => ["tag", "-d", tag];
76
+ export const PREFIX_SEPARATOR: string = "\nrepo release: ";
77
+ export const REGISTRY: string = "https://registry.npmjs.org/";
78
+ export const SCOPE_SLASH: string = "/";
79
+ export const ENCODED_SLASH: string = "%2f";
80
+ export const WAIT_POLL_MS: number = 10_000;
81
+ export const WAIT_GIVE_UP_MS: number = 900_000;
82
+ export const WAIT_GIVE_UP_MINUTES: number = 15;
83
+ export const ISO_DATE_LENGTH: number = 10;
84
+ export const GIT_SHOW = (sha: string, file: string): string[] => ["show", `${sha}:${file}`];
85
+ export const GIT_ON_BRANCH = (sha: string, branch: string): string[] => [
86
+ "merge-base",
87
+ "--is-ancestor",
88
+ sha,
89
+ `origin/${branch}`,
90
+ ];
91
+ export const GH_DEFAULT_BRANCH: readonly string[] = ["repo", "view", "--json", "defaultBranchRef"];
92
+ export const GIT_ORIGIN_URL: readonly string[] = ["remote", "get-url", "origin"];
93
+ export const GITHUB_REPO: RegExp = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/;
94
+ export const REPO_PLACEHOLDER: string = "<owner>/<repo>";
95
+ export const RELEASE_WORKFLOW: string = "release.yml";
96
+ export const ACCESS_URL = (name: string): string => `https://www.npmjs.com/package/${name}/access`;
97
+ export const TARBALL_URL = (name: string, version: string): string =>
98
+ `${REGISTRY}${name}/-/${name.slice(name.lastIndexOf("/") + 1)}-${version}.tgz`;
99
+ export const NPM_PUBLISH = (tarball: string, prerelease: boolean): string[] => [
100
+ "publish",
101
+ tarball,
102
+ "--access",
103
+ "public",
104
+ "--ignore-scripts",
105
+ ...(prerelease ? ["--tag", "next"] : []),
106
+ ];
107
+ export const NPM_TAG = (prerelease: boolean): string => (prerelease ? "next" : "latest");
108
+ export const GH_RELEASE_VIEW = (tag: string): string[] => ["release", "view", tag];
109
+ export const GH_RELEASE_CREATE = (tag: string, notes: string, prerelease: boolean): string[] => [
110
+ "release",
111
+ "create",
112
+ tag,
113
+ "--verify-tag",
114
+ "--title",
115
+ tag,
116
+ "--notes",
117
+ notes,
118
+ ...(prerelease ? ["--prerelease"] : []),
119
+ ];
120
+ export const PACK_DIR_PREFIX: string = "repo-pack-";
121
+ export const BUN_PACK = (dir: string): string[] => ["pm", "pack", "--destination", dir, "--quiet"];
122
+ /** `tarball` is a name in the directory tar runs in. */
123
+ export const TAR_MANIFEST = (tarball: string): string[] => ["-xOzf", tarball, "package/package.json"];
124
+ /** Manifest versions that only mean something inside a workspace or against a catalog. */
125
+ export const UNPINNED: RegExp = /^(catalog|workspace):/;
126
+ export const NOT_FOUND: number = 404;
127
+ export const NOT_A_DOCUMENT: string = "an answer that is not a package document";
128
+ export const TARBALL_EXTENSION: string = ".tgz";
@@ -0,0 +1,199 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { afterEach, describe, expect, it } from "vitest";
4
+ import { removePlanted } from "../fixtures/planted-repo.ts";
5
+ import { NAME, commit, fakeGh, feat, gitIn, release, snapshot, world, type World } from "../fixtures/release-world.ts";
6
+
7
+ afterEach(removePlanted);
8
+
9
+ const TAG = `${NAME}@0.1.1`;
10
+ const SUBJECT = `chore: release ${TAG}`;
11
+ const AGAIN = "run bun repo release again";
12
+ const STAMPED =
13
+ "# Changelog\n\n## [Unreleased]\n\n## [0.1.1] - 2026-11-09\n\n### Added\n\n- x\n\n## [0.1.0] - 2026-10-01\n\n### Added\n\n- first\n";
14
+ const FIXED =
15
+ "# Changelog\n\n## [Unreleased]\n\n## [0.1.1] - 2026-11-09\n\n### Added\n\n- x\n\n### Fixed\n\n- y\n\n## [0.1.0] - 2026-10-01\n\n### Added\n\n- first\n";
16
+ const text = (w: World, file: string) => fs.readFileSync(path.join(w.work, file), "utf8");
17
+ const head = (w: World) => gitIn(w.work, "rev-parse", "HEAD");
18
+ const hookFile = (w: World, name: string) => path.join(w.work, ".git", "hooks", name);
19
+ /** Makes the checkout's next run of the git hook `name` refuse (for pre-push: only a push of tags). */
20
+ const refuse = (w: World, name: string, only = "") =>
21
+ fs.writeFileSync(hookFile(w, name), `#!/bin/sh\n${only}echo no >&2\nexit 1\n`, { mode: 0o755 });
22
+
23
+ /** A world with a feat released: the output of the first run and whether the tag was out when CI was watched. */
24
+ async function released() {
25
+ const w = world();
26
+ feat(w);
27
+ const seen: { tags: string; pushed: string }[] = [];
28
+ const onWatch = () =>
29
+ seen.push({ tags: gitIn(w.remote, "tag", "-l"), pushed: gitIn(w.remote, "log", "-1", "--format=%s") });
30
+ return { w, seen, got: await release(w, [], fakeGh({ onWatch })) };
31
+ }
32
+
33
+ describe("a release", () => {
34
+ it("prints the summary line and exits 0", async () => {
35
+ const { got } = await released();
36
+ const line = `${NAME} 0.1.0 → 0.1.1 · committed · pushed · CI passed · tagged ${TAG}\n`;
37
+ expect(got).toEqual({ code: 0, out: line, err: "" });
38
+ });
39
+
40
+ it("commits only the version line and the stamped changelog", async () => {
41
+ const { w } = await released();
42
+ expect(gitIn(w.work, "log", "-1", "--format=%s")).toBe(SUBJECT);
43
+ const files = gitIn(w.work, "show", "--format=", "--name-only", "HEAD").split("\n");
44
+ expect(files.sort()).toEqual(["CHANGELOG.md", "package.json"]);
45
+ expect(gitIn(w.work, "show", "--format=", "--numstat", "HEAD", "--", "package.json")).toBe("1\t1\tpackage.json");
46
+ expect(text(w, "CHANGELOG.md")).toBe(STAMPED);
47
+ });
48
+
49
+ it("pushes the commit and then tags it, the tag annotated and pushed after CI passed", async () => {
50
+ const { w, seen } = await released();
51
+ expect(seen).toEqual([{ tags: `${NAME}@0.1.0`, pushed: SUBJECT }]);
52
+ expect(gitIn(w.work, "cat-file", "-t", `refs/tags/${TAG}`)).toBe("tag");
53
+ expect(gitIn(w.work, "rev-parse", `${TAG}^{commit}`)).toBe(head(w));
54
+ expect(gitIn(w.remote, "rev-parse", `${TAG}^{commit}`)).toBe(head(w));
55
+ expect(gitIn(w.remote, "rev-parse", "main")).toBe(head(w));
56
+ });
57
+ });
58
+
59
+ describe("a release of another kind", () => {
60
+ it("releases 0.1.0 from 0.0.0 with no tag", async () => {
61
+ const w = world({ "package.json": { name: NAME, version: "0.0.0" } }, false);
62
+ commit(w, "feat: add x", { "CHANGELOG.md": "# Changelog\n\n## [Unreleased]\n\n### Added\n\n- x\n" });
63
+ const got = await release(w, []);
64
+ expect(got.out).toBe(`${NAME} 0.0.0 → 0.1.0 · committed · pushed · CI passed · tagged ${NAME}@0.1.0\n`);
65
+ });
66
+
67
+ it("takes main when origin names no default branch", async () => {
68
+ const w = world();
69
+ feat(w);
70
+ gitIn(w.work, "remote", "set-head", "origin", "-d");
71
+ expect((await release(w, [])).code).toBe(0);
72
+ });
73
+
74
+ it("moves 0.x to 1.0.0 with --stable", async () => {
75
+ const w = world();
76
+ feat(w);
77
+ expect((await release(w, ["--stable"])).out).toContain("0.1.0 → 1.0.0 ·");
78
+ });
79
+
80
+ it("starts a prerelease with --pre", async () => {
81
+ const w = world();
82
+ feat(w);
83
+ expect((await release(w, ["--pre", "rc"])).out).toContain("0.1.0 → 0.1.1-rc.0 ·");
84
+ expect(gitIn(w.work, "tag", "-l")).toContain(`${NAME}@0.1.1-rc.0`);
85
+ });
86
+ });
87
+
88
+ describe("a release CI does not pass", () => {
89
+ const failing = async (behavior: Parameters<typeof fakeGh>[0]) => {
90
+ const w = world();
91
+ feat(w);
92
+ const got = await release(w, [], fakeGh(behavior));
93
+ return { w, got, short: gitIn(w.work, "rev-parse", "--short=7", "HEAD") };
94
+ };
95
+
96
+ it("prints the jobs and the failed line, pushes, and leaves no tag", async () => {
97
+ const { w, got, short } = await failing({ conclusion: "failure" });
98
+ expect(got.out).toContain("failed: test; logs below\nboom\n");
99
+ const line = `CI failed on ${short} (jobs above); fix it in a new commit, push, and ${AGAIN}`;
100
+ expect(got).toMatchObject({ code: 1, err: `repo release: ${line}\n` });
101
+ expect(snapshot(w)).toMatchObject({ tags: `${NAME}@0.1.0`, remoteTags: `${NAME}@0.1.0` });
102
+ expect(gitIn(w.remote, "log", "-1", "--format=%s")).toBe(SUBJECT);
103
+ });
104
+
105
+ it("says a newer push cancelled the run", async () => {
106
+ const { w, got, short } = await failing({ conclusion: "cancelled" });
107
+ expect(got.err).toBe(`repo release: a newer push cancelled CI on ${short}; pull and ${AGAIN}\n`);
108
+ expect(snapshot(w).remoteTags).toBe(`${NAME}@0.1.0`);
109
+ });
110
+
111
+ it("says there was no CI result when no run appears", async () => {
112
+ const { w, got, short } = await failing({ run: false });
113
+ const line = `no CI result for ${short} (no ci run after 120s); it is pushed and untagged; ${AGAIN}`;
114
+ expect(got).toMatchObject({ code: 1, out: "", err: `repo release: ${line}\n` });
115
+ expect(snapshot(w).tags).toBe(`${NAME}@0.1.0`);
116
+ });
117
+ });
118
+
119
+ /** A world whose release commit is pushed and untagged because CI was red. */
120
+ async function pendingAfterRed() {
121
+ const w = world();
122
+ feat(w);
123
+ await release(w, [], fakeGh({ conclusion: "failure" }));
124
+ return w;
125
+ }
126
+
127
+ describe("a rerun after red CI", () => {
128
+ it("on the release commit adds no commit and tags it", async () => {
129
+ const w = await pendingAfterRed();
130
+ const before = head(w);
131
+ const got = await release(w, []);
132
+ expect(got).toEqual({ code: 0, out: `${NAME} 0.1.1 · already committed · CI passed · tagged ${TAG}\n`, err: "" });
133
+ expect(head(w)).toBe(before);
134
+ expect(gitIn(w.remote, "rev-parse", `${TAG}^{commit}`)).toBe(before);
135
+ });
136
+
137
+ it("after a fix commit folds its entry into the version and tags HEAD", async () => {
138
+ const w = await pendingAfterRed();
139
+ commit(w, "fix: y", {
140
+ "CHANGELOG.md": STAMPED.replace("## [Unreleased]\n", "## [Unreleased]\n\n### Fixed\n\n- y\n"),
141
+ });
142
+ const got = await release(w, []);
143
+ expect(got.out).toBe(`${NAME} 0.1.1 · committed · pushed · CI passed · tagged ${TAG}\n`);
144
+ expect(gitIn(w.work, "log", "-1", "--format=%s")).toBe(SUBJECT);
145
+ expect(text(w, "CHANGELOG.md")).toBe(FIXED);
146
+ expect(gitIn(w.remote, "rev-parse", `${TAG}^{commit}`)).toBe(head(w));
147
+ });
148
+
149
+ it("after a fix commit with nothing under Unreleased makes no commit and tags HEAD", async () => {
150
+ const w = await pendingAfterRed();
151
+ commit(w, "fix: y", { "src/y.ts": "export const y = 2;\n" });
152
+ const before = head(w);
153
+ const got = await release(w, []);
154
+ expect(got.out).toBe(`${NAME} 0.1.1 · already committed · CI passed · tagged ${TAG}\n`);
155
+ expect(gitIn(w.remote, "rev-parse", `${TAG}^{commit}`)).toBe(before);
156
+ });
157
+ });
158
+
159
+ describe("a rerun after a push that failed", () => {
160
+ it("pushes the release commit that never left, then tags it", async () => {
161
+ const w = world();
162
+ feat(w);
163
+ refuse(w, "pre-push");
164
+ expect((await release(w, [])).err).toMatch(/^repo release: git push failed: .*no/s);
165
+ expect(gitIn(w.work, "log", "-1", "--format=%s")).toBe(SUBJECT);
166
+ fs.rmSync(hookFile(w, "pre-push"));
167
+ const got = await release(w, []);
168
+ expect(got.out).toBe(`${NAME} 0.1.1 · already committed · pushed · CI passed · tagged ${TAG}\n`);
169
+ expect(gitIn(w.remote, "rev-parse", "main")).toBe(head(w));
170
+ });
171
+
172
+ it("deletes a local tag that did not reach origin, so a rerun makes it", async () => {
173
+ const w = world();
174
+ feat(w);
175
+ refuse(w, "pre-push", "grep -q refs/tags || exit 0\n");
176
+ expect((await release(w, [])).err).toMatch(/^repo release: git push failed: .*no/s);
177
+ expect(gitIn(w.work, "tag", "-l")).toBe(`${NAME}@0.1.0`);
178
+ fs.rmSync(hookFile(w, "pre-push"));
179
+ const got = await release(w, []);
180
+ expect(got.out).toBe(`${NAME} 0.1.1 · already committed · CI passed · tagged ${TAG}\n`);
181
+ });
182
+ });
183
+
184
+ describe("a release whose commit git refuses", () => {
185
+ it("prints the hook output, then one closing line, and restores the files", async () => {
186
+ const w = world();
187
+ feat(w);
188
+ refuse(w, "commit-msg");
189
+ const before = snapshot(w);
190
+ const got = await release(w, []);
191
+ expect(got).toMatchObject({ code: 1, out: "" });
192
+ const last =
193
+ "repo release: the hooks refused the release commit (output above); nothing was changed, so fix it and run bun repo release again\n";
194
+ expect(got.err).toMatch(/^no\n/);
195
+ expect(got.err.endsWith(`\n${last}`)).toBe(true);
196
+ expect(snapshot(w)).toEqual(before);
197
+ expect(snapshot(w).status).toBe("");
198
+ });
199
+ });
@@ -0,0 +1,145 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { waitFor, type Runner, type WaitOutcome } from "../scripts/ci-wait.ts";
4
+ import { stampChangelog } from "./changelog.ts";
5
+ import { must, read, type Tools } from "./git.ts";
6
+ import { writeVersion } from "./manifest.ts";
7
+ import { makePlan, type Plan } from "./plan.ts";
8
+ import {
9
+ ARROW,
10
+ CHANGELOG_FILE,
11
+ FLAG_PRE,
12
+ FLAG_STABLE,
13
+ GIT_ADD,
14
+ GIT_COMMIT,
15
+ GIT_HEAD,
16
+ GIT_PUSH,
17
+ GIT_PUSH_TAG,
18
+ GIT_RESET,
19
+ GIT_TAG,
20
+ GIT_TAG_DELETE,
21
+ GIT_TOPLEVEL,
22
+ ISO_DATE_LENGTH,
23
+ PRE_ID,
24
+ RELEASE_SUBJECT,
25
+ SHA_SHOWN,
26
+ } from "./release.constants.ts";
27
+ import {
28
+ HOOKS_REFUSED,
29
+ NOT_CHECKOUT,
30
+ PREFIX,
31
+ USAGE,
32
+ ciFailed,
33
+ ciNoResult,
34
+ ciSuperseded,
35
+ summary,
36
+ } from "./release-messages.constants.ts";
37
+ import type { Mode } from "./version.ts";
38
+
39
+ /** Where `repo release` runs and prints, and the git, gh, clock and sleep it works through. */
40
+ export type ReleaseIo = {
41
+ cwd: string;
42
+ stdout: (text: string) => void;
43
+ stderr: (text: string) => void;
44
+ git: Runner;
45
+ gh: Runner;
46
+ /** Milliseconds; the clock the wait for CI is measured on, and today's date in the changelog. */
47
+ now: () => number;
48
+ sleep: (ms: number) => Promise<void>;
49
+ };
50
+
51
+ /** The mode the arguments ask for, or the usage line. */
52
+ function parseMode(argv: readonly string[]): Mode | string {
53
+ const [flag, id, ...rest] = argv;
54
+ if (flag === undefined) return { kind: "auto" };
55
+ if (flag === FLAG_STABLE && id === undefined) return { kind: "stable" };
56
+ if (flag === FLAG_PRE && id !== undefined && PRE_ID.test(id) && rest.length === 0) return { kind: "pre", id };
57
+ return USAGE;
58
+ }
59
+
60
+ const refuse = (io: ReleaseIo, line: string): number => {
61
+ io.stderr(`${PREFIX}${line}\n`);
62
+ return 1;
63
+ };
64
+
65
+ /** Writes the stamped changelog (and the version, for a bump) and commits both; restores the files if git refuses. */
66
+ function commitRelease(t: Tools, io: ReleaseIo, plan: Plan): void {
67
+ const file = path.join(t.cwd, CHANGELOG_FILE);
68
+ fs.writeFileSync(
69
+ file,
70
+ stampChangelog(
71
+ fs.readFileSync(file, "utf8"),
72
+ plan.version,
73
+ new Date(io.now()).toISOString().slice(0, ISO_DATE_LENGTH),
74
+ ),
75
+ );
76
+ if (plan.action === "bump") writeVersion(t.cwd, plan.version);
77
+ try {
78
+ must(t, GIT_ADD);
79
+ const got = t.git(GIT_COMMIT(RELEASE_SUBJECT(plan.tag)), t.cwd);
80
+ if (got.code !== 0) {
81
+ io.stderr(`${got.stdout}${got.stderr}`);
82
+ throw new Error(HOOKS_REFUSED);
83
+ }
84
+ } catch (error) {
85
+ must(t, GIT_RESET);
86
+ throw error;
87
+ }
88
+ }
89
+
90
+ /** The refusal line for a CI wait that did not pass. */
91
+ function ciLine(outcome: Exclude<WaitOutcome, { kind: "passed" }>, short: string): string {
92
+ if (outcome.kind === "failed") return ciFailed(short);
93
+ return outcome.kind === "superseded" ? ciSuperseded(short) : ciNoResult(short, outcome.detail);
94
+ }
95
+
96
+ /** Tags HEAD and pushes the tag; a tag that did not reach origin is deleted again, so a rerun makes it. */
97
+ function publishTag(t: Tools, tag: string): void {
98
+ must(t, GIT_TAG(tag));
99
+ try {
100
+ must(t, GIT_PUSH_TAG(tag));
101
+ } catch (error) {
102
+ must(t, GIT_TAG_DELETE(tag));
103
+ throw error;
104
+ }
105
+ }
106
+
107
+ /** Commits and pushes what the plan needs, waits on CI for HEAD, and tags it when CI passed. */
108
+ async function carryOut(t: Tools, io: ReleaseIo, plan: Plan): Promise<number> {
109
+ const steps: string[] = [];
110
+ if (plan.action === "none") steps.push("already committed");
111
+ else {
112
+ commitRelease(t, io, plan);
113
+ steps.push("committed");
114
+ }
115
+ if (plan.action !== "none" || plan.ahead > 0) {
116
+ must(t, GIT_PUSH(plan.branch));
117
+ steps.push("pushed");
118
+ }
119
+ const short = read(t, GIT_HEAD).slice(0, SHA_SHOWN);
120
+ const printed: string[] = [];
121
+ const outcome = await waitFor({ ...io, cwd: t.cwd, stdout: (text) => printed.push(text), git: t.git, gh: t.gh });
122
+ if (outcome.kind !== "passed") {
123
+ io.stdout(printed.join(""));
124
+ return refuse(io, ciLine(outcome, short));
125
+ }
126
+ publishTag(t, plan.tag);
127
+ const move = plan.action === "bump" ? `${plan.from} ${ARROW} ${plan.version}` : plan.version;
128
+ io.stdout(`${summary({ name: plan.name, move, steps, tag: plan.tag })}\n`);
129
+ return 0;
130
+ }
131
+
132
+ /** `repo release [--stable | --pre <id>]`: the whole release of the root package, or the reason it stopped. */
133
+ export async function releaseMain(argv: readonly string[], io: ReleaseIo): Promise<number> {
134
+ const mode = parseMode(argv);
135
+ if (typeof mode === "string") return refuse(io, mode);
136
+ const root = io.git([...GIT_TOPLEVEL], io.cwd).stdout.trim();
137
+ if (root === "") return refuse(io, NOT_CHECKOUT);
138
+ const tools: Tools = { cwd: root, git: io.git, gh: io.gh };
139
+ try {
140
+ const plan = await makePlan(tools, mode);
141
+ return typeof plan === "string" ? refuse(io, plan) : await carryOut(tools, io, plan);
142
+ } catch (error) {
143
+ return refuse(io, (error /* only Errors are thrown here */ as Error).message);
144
+ }
145
+ }