@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,126 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { execFileSync } from "node:child_process";
5
+ import { afterEach, expect, it } from "vitest";
6
+ import { main } from "./commit-msg.ts";
7
+ import { CONFIG_FILE } from "./config.ts";
8
+
9
+ /** `repo commit-msg --range` on planted repos. */
10
+ const temps: string[] = [];
11
+ afterEach(() => {
12
+ for (const dir of temps.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
13
+ });
14
+ const git = (cwd: string, ...args: string[]) =>
15
+ execFileSync("git", ["-c", "user.email=a@b", "-c", "user.name=a", ...args], { cwd, encoding: "utf8" }).trim();
16
+ const lines = (n: number) => Array.from({ length: n }, (_, i) => `line ${i}\n`).join("");
17
+
18
+ /** A repo with a base commit; `commit` adds files and commits with `message`, returning the sha. */
19
+ function plant() {
20
+ const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "commit-range-")));
21
+ temps.push(root);
22
+ git(root, "init", "-q", "-b", "main");
23
+ const commit = (files: Record<string, string | Buffer>, message: string) => {
24
+ for (const [rel, text] of Object.entries(files)) {
25
+ fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
26
+ fs.writeFileSync(path.join(root, rel), text);
27
+ }
28
+ git(root, "add", "-A");
29
+ git(root, "commit", "-q", "--no-verify", "-m", message);
30
+ return git(root, "rev-parse", "HEAD");
31
+ };
32
+ const base = commit(
33
+ { "package.json": JSON.stringify({ name: "app", private: true }), [CONFIG_FILE]: "export default {}\n" },
34
+ "chore: base",
35
+ );
36
+ return { root, commit, base };
37
+ }
38
+ async function range(root: string, ...args: string[]) {
39
+ const err: string[] = [];
40
+ const code = await main(args, { cwd: root, stderr: (text) => err.push(text) });
41
+ return { code, stderr: err.join("") };
42
+ }
43
+ const NEEDS_TEST = "a feat commit must include a test change (a *.test.* or *.spec.* file, or a file under e2e/)";
44
+ const HEADER = 'the header must be "type(scope)!: subject", e.g. "fix(paths): compare hook paths canonically"';
45
+ const USAGE =
46
+ "repo commit-msg: name the commit message file as git's commit-msg hook does or pass --range <base>..<head>\n";
47
+
48
+ /** Plants a side branch merged into main with a merge commit on top. */
49
+ function merged(plantd: ReturnType<typeof plant>) {
50
+ const { root, commit } = plantd;
51
+ git(root, "checkout", "-q", "-b", "side");
52
+ commit({ "s.ts": "s\n" }, "chore: side");
53
+ git(root, "checkout", "-q", "main");
54
+ commit({ "m.ts": "m\n" }, "chore: main");
55
+ git(root, "merge", "-q", "--no-ff", "-m", "Merge side", "side");
56
+ }
57
+
58
+ it("passes a clean range, a lockfile-heavy commit and skips merges", async () => {
59
+ const planted = plant();
60
+ const { root, commit, base } = planted;
61
+ commit({ "a.ts": "a\n", "a.test.ts": "t\n" }, "feat: add a");
62
+ commit(
63
+ { "bun.lock": lines(2000), "gen/x.ts": lines(2000), ".gitattributes": "gen/** linguist-generated\n" },
64
+ "chore: lock",
65
+ );
66
+ merged(planted);
67
+ expect(await range(root, "--range", `${base}..HEAD`)).toEqual({ code: 0, stderr: "" });
68
+ });
69
+
70
+ it("names sha and subject of each failing commit, oldest first", async () => {
71
+ const { root, commit, base } = plant();
72
+ const one = commit({ "a.ts": "a\n" }, "feat: add a");
73
+ const two = commit({ "b.ts": "b\n" }, "Bad header");
74
+ const three = commit({ "big.ts": lines(1100) }, "chore: big");
75
+ const four = commit({ "c.ts": "c\n" }, "chore(Nope): x");
76
+ const result = await range(root, "--range", `${base}..HEAD`);
77
+ expect(result.code).toBe(1);
78
+ expect(result.stderr.split("\n")).toEqual([
79
+ `commit-msg: ${one.slice(0, 7)} feat: add a: ${NEEDS_TEST}`,
80
+ `commit-msg: ${two.slice(0, 7)} Bad header: ${HEADER}`,
81
+ `commit-msg: ${three.slice(0, 7)} chore: big: the commit changes 1100 lines, over the limit of 1000; add a body line saying why, or split it`,
82
+ `commit-msg: ${four.slice(0, 7)} chore(Nope): x: the scope "Nope" must be lowercase kebab-case`,
83
+ "",
84
+ ]);
85
+ });
86
+
87
+ it("checks HEAD alone for a zero, empty, missing or non-ancestor base", async () => {
88
+ const { root, commit, base } = plant();
89
+ git(root, "checkout", "-q", "-b", "other");
90
+ const other = commit({ "o.ts": "o\n" }, "chore: other");
91
+ git(root, "checkout", "-q", "main");
92
+ const bad = commit({ "a.ts": "a\n" }, "feat: add a");
93
+ const problem = `commit-msg: ${bad.slice(0, 7)} feat: add a: ${NEEDS_TEST}\n`;
94
+ expect(await range(root, "--range", `${"0".repeat(40)}..HEAD`)).toEqual({ code: 1, stderr: problem });
95
+ expect(await range(root, "--range", "..HEAD")).toEqual({ code: 1, stderr: problem });
96
+ expect(await range(root, "--range", `${other}..HEAD`)).toEqual({
97
+ code: 1,
98
+ stderr: `commit-msg: ${other.slice(0, 7)} is not an ancestor of HEAD (a force push?); checking HEAD alone\n${problem}`,
99
+ });
100
+ const gone = await range(root, "--range", `${"1".repeat(40)}..HEAD`);
101
+ expect(gone.stderr).toContain("1111111 is not an ancestor of HEAD (a force push?); checking HEAD alone\n");
102
+ expect(await range(root, "--range", `${base}..`)).toEqual({ code: 1, stderr: problem });
103
+ });
104
+
105
+ it("checks nothing when the head alone is a merge", async () => {
106
+ const planted = plant();
107
+ merged(planted);
108
+ expect(await range(planted.root, "--range", "..HEAD")).toEqual({ code: 0, stderr: "" });
109
+ });
110
+
111
+ it("refuses an unknown range and no arguments", async () => {
112
+ const { root } = plant();
113
+ const bads = ["nope..HEAD", "HEAD..nope", "HEAD", "a..b..c"];
114
+ const results = await Promise.all(bads.map((bad) => range(root, "--range", bad)));
115
+ expect(results).toEqual(
116
+ bads.map((bad) => ({ code: 1, stderr: `repo commit-msg: no commit range "${bad}"; use <base>..<head>\n` })),
117
+ );
118
+ expect(await range(root, "--range")).toEqual({ code: 1, stderr: USAGE });
119
+ expect(await range(root, "--range", "a..b", "c")).toEqual({ code: 1, stderr: USAGE });
120
+ });
121
+
122
+ it("refuses on a config it cannot read", async () => {
123
+ const { root, commit, base } = plant();
124
+ commit({ [CONFIG_FILE]: "export default { commit: 5 }\n" }, "chore: x");
125
+ expect((await range(root, "--range", `${base}..HEAD`)).code).toBe(1);
126
+ });
@@ -0,0 +1,147 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { afterEach, describe, expect, it } from "vitest";
4
+ import { CONFIG_FILE } from "./config.ts";
5
+ import { main } from "./commit-msg.ts";
6
+ import { removePlanted } from "./fixtures/planted-repo.ts";
7
+ import { FAIL, plant, stage } from "./fixtures/red-proof-repo.ts";
8
+ import type { NameStatus, RedProofResult } from "./red-proof.ts";
9
+
10
+ /** The hook's red-proof step: when it runs, what it refuses, what it prints. */
11
+ afterEach(removePlanted);
12
+
13
+ const TDD = "export default { tdd: true }\n";
14
+ const PLAIN = { [CONFIG_FILE]: "export default {}\n" };
15
+ const PRIVATE = { name: "p", private: true };
16
+ const PROBLEM = "red-proof: no changed test file fails on HEAD's source (a.test.ts passed); needs a failing one";
17
+ const LINE = "red-proof: 1 of 1 changed test files failed on HEAD";
18
+
19
+ /** Runs `repo commit-msg` with a red-proof stand-in; what it was given, the exit code and stderr. */
20
+ async function hook(root: string, message: string, result: RedProofResult = { ok: true, line: LINE }) {
21
+ fs.writeFileSync(path.join(root, ".git", "COMMIT_EDITMSG"), message);
22
+ const calls: { root: string; change: NameStatus[] }[] = [];
23
+ const err: string[] = [];
24
+ const redProof = (at: string, change: NameStatus[]) => (calls.push({ root: at, change }), result);
25
+ const code = await main([".git/COMMIT_EDITMSG"], { cwd: root, stderr: (text) => err.push(text), redProof });
26
+ return { calls, code, stderr: err.join("") };
27
+ }
28
+ const feat = (root: string) => hook(root, "feat: add x\n");
29
+ const SRC_AND_TEST = { "src/x.ts": "export const x = 1;\n", "src/x.test.ts": "t\n", "CHANGELOG.md": "# Changelog\n" };
30
+
31
+ describe("commit-msg red-proof in a public package", () => {
32
+ it("refuses a feat with the problem in the refused list", async () => {
33
+ const root = plant(PLAIN);
34
+ stage(root, SRC_AND_TEST);
35
+ const result = await hook(root, "feat: add x\n", { ok: false, problem: PROBLEM });
36
+ expect(result.code).toBe(1);
37
+ expect(result.stderr).toBe(`commit-msg: the message was refused\n - ${PROBLEM}\n`);
38
+ expect(result.calls).toEqual([
39
+ {
40
+ root,
41
+ change: [
42
+ { status: "A", path: "CHANGELOG.md" },
43
+ { status: "A", path: "src/x.test.ts" },
44
+ { status: "A", path: "src/x.ts" },
45
+ ],
46
+ },
47
+ ]);
48
+ });
49
+
50
+ it("lets a fix through and prints the red-proof line", async () => {
51
+ const root = plant(PLAIN);
52
+ stage(root, SRC_AND_TEST);
53
+ expect(await hook(root, "fix: handle x\n")).toMatchObject({ code: 0, stderr: `${LINE}\n` });
54
+ });
55
+ });
56
+
57
+ describe("commit-msg red-proof in a test-only change", () => {
58
+ it("runs from a test-only change, using the test files' owner", async () => {
59
+ const root = plant(PLAIN);
60
+ stage(root, { "x.test.ts": "t\n" });
61
+ expect((await feat(root)).calls).toHaveLength(1);
62
+ });
63
+
64
+ it("does not count a fixture as source, so a private owner of the tests decides", async () => {
65
+ const root = plant({ ...PLAIN, "package.json": PRIVATE });
66
+ stage(root, { "fixtures/a.ts": "x\n", "x.test.ts": "t\n" });
67
+ expect((await feat(root)).calls).toHaveLength(0);
68
+ });
69
+ });
70
+
71
+ describe("commit-msg red-proof by package", () => {
72
+ const mono = () =>
73
+ plant({
74
+ ...PLAIN,
75
+ "package.json": { name: "mono", private: true, workspaces: ["packages/*"] },
76
+ "packages/pub/package.json": { name: "pub" },
77
+ "packages/priv/package.json": { name: "priv", private: true },
78
+ });
79
+
80
+ it("runs for a private package only with tdd: true", async () => {
81
+ const root = mono();
82
+ stage(root, { "packages/priv/x.ts": "x\n", "packages/priv/x.test.ts": "t\n" });
83
+ expect((await hook(root, "fix(priv): x\n")).calls).toHaveLength(0);
84
+ fs.writeFileSync(path.join(root, CONFIG_FILE), TDD);
85
+ expect((await hook(root, "fix(priv): x\n")).calls).toHaveLength(1);
86
+ });
87
+
88
+ it("runs when any changed source file belongs to a public package", async () => {
89
+ const root = mono();
90
+ stage(root, { "packages/priv/x.ts": "x\n", "packages/pub/y.ts": "y\n", "packages/priv/x.test.ts": "t\n" });
91
+ expect((await hook(root, "feat(pub): y\n")).calls).toHaveLength(1);
92
+ });
93
+
94
+ it("looks at the test files' owners only when no source file changed", async () => {
95
+ const root = mono();
96
+ stage(root, { "packages/pub/y.test.ts": "t\n", "packages/priv/x.test.ts": "t\n" });
97
+ expect((await hook(root, "feat(pub): y\n")).calls).toHaveLength(1);
98
+ });
99
+ });
100
+
101
+ describe("commit-msg red-proof in a single-package repo", () => {
102
+ it("uses the root package.json of a single-package repo, private or not", async () => {
103
+ const root = plant({ ...PLAIN, "package.json": PRIVATE });
104
+ stage(root, SRC_AND_TEST);
105
+ expect((await feat(root)).calls).toHaveLength(0);
106
+ fs.writeFileSync(path.join(root, "package.json"), '{ "name": "p" }\n');
107
+ expect((await feat(root)).calls).toHaveLength(1);
108
+ });
109
+
110
+ it("takes a repo with no workerstack.config.ts as tdd false, by its root package.json", async () => {
111
+ const root = plant({ "package.json": PRIVATE });
112
+ stage(root, SRC_AND_TEST);
113
+ expect((await feat(root)).calls).toHaveLength(0);
114
+ fs.writeFileSync(path.join(root, "package.json"), '{ "name": "p" }\n');
115
+ expect((await feat(root)).calls).toHaveLength(1);
116
+ });
117
+ });
118
+
119
+ describe("commit-msg red-proof never runs", () => {
120
+ it.each(["test: cover x\n", "docs: x\n", "chore: x\n", "feat: Add x\n", "feat(Bad): x\n"])(
121
+ "for the message %j: another type, or a header already refused",
122
+ async (message) => {
123
+ const root = plant({ [CONFIG_FILE]: TDD });
124
+ stage(root, SRC_AND_TEST);
125
+ expect((await hook(root, message)).calls).toEqual([]);
126
+ },
127
+ );
128
+
129
+ it("when the staged change has no test, which the message check refuses first", async () => {
130
+ const root = plant({ [CONFIG_FILE]: TDD });
131
+ stage(root, { "src/x.ts": "x\n" });
132
+ const result = await feat(root);
133
+ expect(result.calls).toEqual([]);
134
+ expect(result.code).toBe(1);
135
+ });
136
+ });
137
+
138
+ describe("commit-msg red-proof end to end", () => {
139
+ it("runs the real vitest on HEAD and lands a feat whose test fails there", async () => {
140
+ const root = plant(PLAIN);
141
+ stage(root, { "x.ts": "export const x = 1;\n", "x.test.ts": FAIL, "CHANGELOG.md": "# Changelog\n" });
142
+ fs.writeFileSync(path.join(root, ".git", "COMMIT_EDITMSG"), "feat: add x\n");
143
+ const err: string[] = [];
144
+ expect(await main([".git/COMMIT_EDITMSG"], { cwd: root, stderr: (text) => err.push(text) })).toBe(0);
145
+ expect(err.join("")).toBe("red-proof: 1 of 1 changed test files failed on HEAD\n");
146
+ }, 60_000);
147
+ });
@@ -0,0 +1,153 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { execFileSync } from "node:child_process";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { CONFIG_FILE } from "./config.ts";
7
+ import { NAME_A_SOURCE } from "./commit-range.constants.ts";
8
+ import { main } from "./commit-msg.ts";
9
+ import { commitChange, stagedChange } from "./staged-change.ts";
10
+
11
+ /** `repo commit-msg` and the staged change it reads, on temp repos. */
12
+ const OVER = "the commit changes 1240 lines, over the limit of 1000; add a body line saying why, or split it";
13
+
14
+ const temps: string[] = [];
15
+ afterEach(() => {
16
+ for (const dir of temps.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
17
+ });
18
+ const git = (cwd: string, ...args: string[]) =>
19
+ execFileSync("git", ["-c", "user.email=a@b", "-c", "user.name=a", ...args], { cwd, encoding: "utf8" });
20
+ const lines = (n: number) => Array.from({ length: n }, (_, i) => `line ${i}\n`).join("");
21
+ const put = (root: string, files: Record<string, string | Buffer>) => {
22
+ for (const [rel, text] of Object.entries(files)) {
23
+ fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
24
+ fs.writeFileSync(path.join(root, rel), text);
25
+ }
26
+ };
27
+ const tempDir = () => {
28
+ const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "commit-msg-")));
29
+ temps.push(dir);
30
+ return dir;
31
+ };
32
+
33
+ /** A temp repo with `base` committed and `staged` written and added on top. */
34
+ function repo(base: Record<string, string | Buffer>, staged: Record<string, string | Buffer>): string {
35
+ const root = tempDir();
36
+ git(root, "init", "-q");
37
+ put(root, {
38
+ "package.json": JSON.stringify({ name: "app", private: true }),
39
+ [CONFIG_FILE]: "export default {}\n",
40
+ ...base,
41
+ });
42
+ git(root, "add", "-A");
43
+ git(root, "commit", "-q", "--no-verify", "-m", "base");
44
+ put(root, staged);
45
+ git(root, "add", "-A");
46
+ return root;
47
+ }
48
+
49
+ describe("stagedChange", () => {
50
+ it("counts staged insertions and deletions, and lists every staged path", () => {
51
+ const root = repo({ "a.ts": lines(10), "old.ts": lines(3) }, { "a.ts": lines(4), "b.test.ts": lines(5) });
52
+ git(root, "mv", "old.ts", "new.ts");
53
+ expect(stagedChange(root)).toEqual({ files: ["a.ts", "b.test.ts", "new.ts"], changedLines: 11 });
54
+ });
55
+
56
+ it("leaves out lockfiles, the lint baseline, binaries and linguist-generated paths", () => {
57
+ const skipped = ["bun.lock", "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "api/bun.lock"];
58
+ const staged = Object.fromEntries(
59
+ [...skipped, ".repo/lint-baseline.json", "gen/out.ts"].map((rel) => [rel, lines(50)]),
60
+ );
61
+ const root = repo(
62
+ { ".gitattributes": "gen/** linguist-generated\n" },
63
+ { ...staged, "bun.lockb": Buffer.from([0, 1, 2]), "img.bin": Buffer.from([0, 9, 0]), "src/a.ts": lines(7) },
64
+ );
65
+ expect(stagedChange(root).changedLines).toBe(7);
66
+ });
67
+
68
+ it("reads linguist-generated from the staged .gitattributes", () => {
69
+ const root = repo({}, { ".gitattributes": "gen/** linguist-generated=true\n", "gen/out.ts": lines(50) });
70
+ expect(stagedChange(root).changedLines).toBe(1);
71
+ });
72
+
73
+ it("finds nothing staged outside a git repo", () => {
74
+ expect(stagedChange(tempDir())).toEqual({ files: [], changedLines: 0 });
75
+ expect(commitChange(tempDir(), "HEAD")).toEqual({ files: [], changedLines: 0 });
76
+ });
77
+ });
78
+
79
+ /** Runs `repo commit-msg` on `message` in `root`; its exit code and what it printed. */
80
+ async function commitMsg(root: string, message: string) {
81
+ fs.writeFileSync(path.join(root, ".git", "COMMIT_EDITMSG"), message);
82
+ const err: string[] = [];
83
+ const code = await main([".git/COMMIT_EDITMSG"], { cwd: root, stderr: (text) => err.push(text) });
84
+ return { code, stderr: err.join("") };
85
+ }
86
+ const refused = (...problems: string[]) => ({
87
+ code: 1,
88
+ stderr: `commit-msg: the message was refused\n${problems.map((problem) => ` - ${problem}\n`).join("")}`,
89
+ });
90
+ const PASSED = { code: 0, stderr: "" };
91
+ const NEEDS_TEST = "a feat commit must include a test change (a *.test.* or *.spec.* file, or a file under e2e/)";
92
+
93
+ describe("repo commit-msg scopes", () => {
94
+ it("takes scopes from the last segment of each workspace package name, or its folder", async () => {
95
+ const root = repo(
96
+ {
97
+ "package.json": JSON.stringify({ name: "mono", private: true, workspaces: ["packages/*"] }),
98
+ "packages/api/package.json": JSON.stringify({ name: "@acme/api" }),
99
+ "packages/app/package.json": "{}",
100
+ },
101
+ { "src/x.test.ts": "x\n" },
102
+ );
103
+ expect(await commitMsg(root, "feat(web): add x\n")).toEqual(
104
+ refused('unknown scope "web"; use a workspace package name or one of repo, deps, ci (api, app)'),
105
+ );
106
+ expect(await commitMsg(root, "feat(api): add x\n")).toEqual(PASSED);
107
+ expect(await commitMsg(root, "chore(deps): bump\n")).toEqual(PASSED);
108
+ });
109
+
110
+ it("lets a single-package repo use any kebab scope, and still needs a test for a feat", async () => {
111
+ const root = repo({}, { "src/x.ts": "x\n" });
112
+ expect(await commitMsg(root, "chore(web): x\n")).toEqual(PASSED);
113
+ expect(await commitMsg(root, "feat(web): x\n")).toEqual(refused(NEEDS_TEST));
114
+ });
115
+ });
116
+
117
+ describe("repo commit-msg size and setup", () => {
118
+ it("refuses an oversized commit until its message says why, at the configured limit", async () => {
119
+ const root = repo({}, { "src/a.ts": lines(1236), "src/a.test.ts": lines(4) });
120
+ expect(await commitMsg(root, "fix: handle y\n")).toEqual(refused(OVER));
121
+ expect(await commitMsg(root, "fix: handle y\n\nReformat from the oxfmt upgrade.\n")).toEqual(PASSED);
122
+ put(root, { [CONFIG_FILE]: "export default { commit: { maxChangedLines: 1500 } }\n" });
123
+ expect(await commitMsg(root, "fix: handle y\n")).toEqual(PASSED);
124
+ });
125
+
126
+ it("says how to call it, and refuses on a config it cannot read", async () => {
127
+ const write = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
128
+ expect(await main(["a", "b"])).toBe(1);
129
+ expect(write).toHaveBeenCalledWith(NAME_A_SOURCE);
130
+ write.mockRestore();
131
+ const err: string[] = [];
132
+ expect(await main([], { cwd: os.tmpdir(), stderr: (text) => err.push(text) })).toBe(1);
133
+ expect(err).toEqual([NAME_A_SOURCE]);
134
+ const root = repo({}, { [CONFIG_FILE]: "export default { commit: 5 }\n" });
135
+ expect(await commitMsg(root, "chore: x\n")).toEqual({
136
+ code: 1,
137
+ stderr: "repo commit-msg: workerstack.config.ts: commit: must be an object like { maxChangedLines: 1000 }\n",
138
+ });
139
+ });
140
+
141
+ it("checks with the defaults where there is no config", async () => {
142
+ const root = repo({}, { "src/a.ts": lines(1240) });
143
+ fs.rmSync(path.join(root, CONFIG_FILE));
144
+ expect(await commitMsg(root, "chore: x\n")).toEqual(refused(OVER));
145
+ });
146
+ });
147
+
148
+ it("repo commit-msg refuses on a package.json it cannot read, naming it", async () => {
149
+ const root = repo({}, { "package.json": "{ nope" });
150
+ const result = await commitMsg(root, "chore: x\n");
151
+ expect(result.code).toBe(1);
152
+ expect(result.stderr).toMatch(/^repo commit-msg: package\.json: /);
153
+ });
@@ -0,0 +1,101 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { checkCommitMessage } from "./commit-msg.ts";
3
+ import { isTestFile } from "./test-files.ts";
4
+
5
+ const ok = (message: string, stagedFiles: string[] = ["src/a.test.ts"], scopes?: string[]) =>
6
+ checkCommitMessage(message, { stagedFiles, scopes, changedLines: 0, maxChangedLines: 1000 });
7
+ const sized = (message: string, changedLines: number) =>
8
+ checkCommitMessage(message, { stagedFiles: [], changedLines, maxChangedLines: 1000 });
9
+ const OVER = "the commit changes 1240 lines, over the limit of 1000; add a body line saying why, or split it";
10
+
11
+ describe("checkCommitMessage", () => {
12
+ it("accepts every listed type", () => {
13
+ for (const type of ["feat", "fix", "perf", "refactor", "test", "docs", "build", "ci", "chore", "revert"]) {
14
+ expect(ok(`${type}: do the thing`)).toEqual([]);
15
+ }
16
+ });
17
+
18
+ it("accepts a scope and a breaking marker", () => {
19
+ expect(ok("feat(paths)!: drop the old spelling")).toEqual([]);
20
+ });
21
+
22
+ it("rejects a header that is not type(scope)!: subject", () => {
23
+ expect(ok("added stuff")).toEqual([
24
+ 'the header must be "type(scope)!: subject", e.g. "fix(paths): compare hook paths canonically"',
25
+ ]);
26
+ });
27
+
28
+ it("rejects an unknown type", () => {
29
+ expect(ok("feature: x")).toEqual([
30
+ 'unknown type "feature"; use one of feat, fix, perf, refactor, test, docs, build, ci, chore, revert',
31
+ ]);
32
+ });
33
+
34
+ it("rejects a scope outside the workspace when scopes are given", () => {
35
+ expect(ok("chore(nope): x", [], ["backend"])).toEqual([
36
+ 'unknown scope "nope"; use a workspace package name or one of repo, deps, ci (backend)',
37
+ ]);
38
+ expect(ok("fix(backend): x", ["a.test.ts"], ["backend"])).toEqual([]);
39
+ expect(ok("chore(deps): x", [], ["backend"])).toEqual([]);
40
+ });
41
+
42
+ it("rejects a scope that is not lowercase kebab-case", () => {
43
+ expect(ok("fix(Paths): x")).toEqual(['the scope "Paths" must be lowercase kebab-case']);
44
+ });
45
+
46
+ it("rejects a subject that is capitalised, ends in a period or makes the header too long", () => {
47
+ expect(ok("fix: Compare paths")).toEqual(["the subject must start lowercase"]);
48
+ expect(ok("fix: compare paths.")).toEqual(["the subject must not end with a period"]);
49
+ expect(ok(`fix: ${"a".repeat(96)}`)).toEqual(["the header is 101 characters; keep it to 100"]);
50
+ });
51
+
52
+ it("needs a test change for feat and fix", () => {
53
+ expect(ok("feat: x", ["src/a.ts"])).toEqual([
54
+ "a feat commit must include a test change (a *.test.* or *.spec.* file, or a file under e2e/)",
55
+ ]);
56
+ expect(ok("fix: x", [])).toEqual([
57
+ "a fix commit must include a test change (a *.test.* or *.spec.* file, or a file under e2e/)",
58
+ ]);
59
+ expect(ok("docs: x", [])).toEqual([]);
60
+ });
61
+
62
+ it("treats a BREAKING CHANGE footer like a breaking marker", () => {
63
+ expect(ok("refactor: x\n\nBREAKING CHANGE: gone", [])).toEqual([]);
64
+ });
65
+
66
+ it("ignores git's comment lines and leading blank lines", () => {
67
+ expect(ok("\n# Please enter the commit message\nchore: x\n# On branch main", [])).toEqual([]);
68
+ });
69
+
70
+ it("rejects an empty message", () => {
71
+ expect(ok("# only comments\n", [])).toEqual(["the commit message is empty"]);
72
+ });
73
+
74
+ it("lets git's own merge commits through", () => {
75
+ expect(ok("Merge branch 'x' into main", [])).toEqual([]);
76
+ });
77
+ });
78
+
79
+ describe("isTestFile", () => {
80
+ it("knows test files", () => {
81
+ expect(isTestFile("cli/a.test.ts")).toBe(true);
82
+ expect(isTestFile("src/a.spec.tsx")).toBe(true);
83
+ expect(isTestFile("e2e/run.ts")).toBe(true);
84
+ expect(isTestFile("packages/x/e2e/run.ts")).toBe(true);
85
+ expect(isTestFile("src/attest.ts")).toBe(false);
86
+ });
87
+ });
88
+
89
+ describe("the commit size limit", () => {
90
+ it("refuses a commit over the limit with no body, and takes a body line as the reason", () => {
91
+ expect(sized("chore: x", 1000)).toEqual([]);
92
+ expect(sized("chore: x", 1240)).toEqual([OVER]);
93
+ expect(sized("chore: x\n\nReformat from the oxfmt upgrade.", 1240)).toEqual([]);
94
+ });
95
+
96
+ it("does not take a comment or git's verbose diff as the reason", () => {
97
+ expect(sized("chore: x\n\n# why\n", 1240)).toEqual([OVER]);
98
+ const verbose = "chore: x\n# ------------------------ >8 ------------------------\ndiff --git a/x b/x\n+y\n";
99
+ expect(sized(verbose, 1240)).toEqual([OVER]);
100
+ });
101
+ });