@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,138 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { z } from "zod";
4
+
5
+ const globs = z.array(z.string());
6
+ const versions = z.record(z.string(), z.string({ error: "must be a version string" }), {
7
+ error: "must be an object of package versions",
8
+ });
9
+
10
+ const packageJsonSchema: z.ZodObject<
11
+ {
12
+ name: z.ZodOptional<z.ZodString>;
13
+ private: z.ZodOptional<z.ZodBoolean>;
14
+ version: z.ZodOptional<z.ZodUnknown>;
15
+ sideEffects: z.ZodOptional<z.ZodUnknown>;
16
+ packageManager: z.ZodOptional<z.ZodUnknown>;
17
+ bin: z.ZodOptional<z.ZodUnknown>;
18
+ exports: z.ZodOptional<z.ZodUnknown>;
19
+ scripts: z.ZodOptional<z.ZodUnknown>;
20
+ workspaces: z.ZodOptional<
21
+ z.ZodUnion<
22
+ readonly [
23
+ z.ZodArray<z.ZodString>,
24
+ z.ZodObject<
25
+ {
26
+ packages: z.ZodOptional<z.ZodArray<z.ZodString>>;
27
+ },
28
+ z.core.$strip
29
+ >,
30
+ ]
31
+ >
32
+ >;
33
+ dependencies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
34
+ devDependencies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
35
+ peerDependencies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
36
+ optionalDependencies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
37
+ },
38
+ z.core.$strip
39
+ > = z.object(
40
+ {
41
+ name: z.string({ error: "must be a string" }).optional(),
42
+ private: z.boolean({ error: "must be true or false" }).optional(),
43
+ // Kept as written: sync accepts any value and only `repo check` judges these.
44
+ version: z.unknown().optional(),
45
+ sideEffects: z.unknown().optional(),
46
+ packageManager: z.unknown().optional(),
47
+ // Kept for the lint config, which narrows them where it reads them.
48
+ bin: z.unknown().optional(),
49
+ exports: z.unknown().optional(),
50
+ // Kept as written; `repo check` narrows the root's scripts where it reads them.
51
+ scripts: z.unknown().optional(),
52
+ workspaces: z
53
+ .union([globs, z.object({ packages: globs.optional() })], {
54
+ error: 'must be a list of globs or { "packages": [...] }',
55
+ })
56
+ .optional(),
57
+ dependencies: versions.optional(),
58
+ devDependencies: versions.optional(),
59
+ peerDependencies: versions.optional(),
60
+ optionalDependencies: versions.optional(),
61
+ },
62
+ { error: "must be a JSON object" },
63
+ );
64
+
65
+ /** The package.json fields repo sync reads; `version`, `sideEffects`, `packageManager`, `bin` and `exports` stay unjudged `unknown`s; every other field is dropped at parse. */
66
+ export type PackageJson = z.output<typeof packageJsonSchema>;
67
+ export type WorkspacePackage = { dir: string; manifest: PackageJson };
68
+ /** A repo root and its workspace packages; `dir` is root-relative with POSIX separators. */
69
+ export type Workspace = { root: string; manifest: PackageJson; packages: WorkspacePackage[] };
70
+ export type WorkspaceResult = { ok: true; workspace: Workspace } | { ok: false; errors: string[] };
71
+ /** Expands one glob under `cwd` into the matching paths, relative to `cwd`. */
72
+ export type Glob = (pattern: string, cwd: string) => string[];
73
+
74
+ const globSync: Glob = (pattern, cwd) => fs.globSync(pattern, { cwd });
75
+
76
+ /** Reads the root package.json and every workspace package it declares, sorted by dir. */
77
+ export function readWorkspace(root: string, glob: Glob = globSync): WorkspaceResult {
78
+ const rootFile = path.join(root, "package.json");
79
+ // A root without package.json has no workspaces and no dependencies, as loadConfig reads it.
80
+ const rootManifest: ManifestResult = fs.existsSync(rootFile)
81
+ ? parseManifest("package.json", rootFile)
82
+ : { ok: true, manifest: {} };
83
+ if (!rootManifest.ok) return { ok: false, errors: rootManifest.errors };
84
+
85
+ const errors: string[] = [];
86
+ const packages: WorkspacePackage[] = [];
87
+ for (const dir of expandWorkspaces(root, rootManifest.manifest, glob)) {
88
+ const rel = `${dir}/package.json`;
89
+ const manifest = parseManifest(rel, path.join(root, rel));
90
+ if (manifest.ok) packages.push({ dir, manifest: manifest.manifest });
91
+ else errors.push(...manifest.errors);
92
+ }
93
+ if (errors.length > 0) return { ok: false, errors };
94
+ return { ok: true, workspace: { root, manifest: rootManifest.manifest, packages } };
95
+ }
96
+
97
+ /** Workspace dirs that hold a package.json, outside node_modules, deduped and sorted. */
98
+ function expandWorkspaces(root: string, manifest: PackageJson, glob: Glob): string[] {
99
+ const workspaces = manifest.workspaces;
100
+ const patterns = (Array.isArray(workspaces) ? workspaces : workspaces?.packages) ?? [];
101
+ const included = new Set<string>();
102
+ const excluded = new Set<string>();
103
+ for (const pattern of patterns) {
104
+ const negated = pattern.startsWith("!");
105
+ const target = negated ? excluded : included;
106
+ for (const match of glob(negated ? pattern.slice(1) : pattern, root)) target.add(toPosixDir(match));
107
+ }
108
+ return [...included]
109
+ .filter((dir) => !excluded.has(dir))
110
+ .filter((dir) => !dir.split("/").includes("node_modules"))
111
+ .filter((dir) => fs.existsSync(path.join(root, dir, "package.json")))
112
+ .sort();
113
+ }
114
+
115
+ /** `packages\api\` and `./packages/api/` both read `packages/api`. */
116
+ function toPosixDir(match: string): string {
117
+ return path.posix.normalize(match.replaceAll("\\", "/")).replace(/\/+$/, "");
118
+ }
119
+
120
+ type ManifestResult = { ok: true; manifest: PackageJson } | { ok: false; errors: string[] };
121
+
122
+ function parseManifest(rel: string, file: string): ManifestResult {
123
+ let raw: unknown;
124
+ try {
125
+ raw = JSON.parse(fs.readFileSync(file, "utf8"));
126
+ } catch (error) {
127
+ return { ok: false, errors: [`${rel}: not valid JSON: ${(error as Error).message}`] };
128
+ }
129
+ const parsed = packageJsonSchema.safeParse(raw);
130
+ if (parsed.success) return { ok: true, manifest: parsed.data };
131
+ return {
132
+ ok: false,
133
+ errors: parsed.error.issues.map((issue) => {
134
+ const at = issue.path.join(".");
135
+ return `${rel}: ${at ? `${at}: ` : ""}${issue.message}`;
136
+ }),
137
+ };
138
+ }
@@ -0,0 +1,341 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+ import { linkText } from "../../dev/test-paths.ts";
6
+ import { MANIFEST_REL, renderManifest } from "./manifest.ts";
7
+ import type { Output } from "./outputs.ts";
8
+ import { writeAll, writeOutput } from "./write.ts";
9
+
10
+ const temps: string[] = [];
11
+ afterEach(() => {
12
+ vi.restoreAllMocks();
13
+ for (const dir of temps.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
14
+ });
15
+
16
+ function tempRoot(): string {
17
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "workerstack-write-"));
18
+ temps.push(dir);
19
+ return dir;
20
+ }
21
+
22
+ function put(root: string, rel: string, text: string): void {
23
+ fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
24
+ fs.writeFileSync(path.join(root, rel), text);
25
+ }
26
+
27
+ /** Every path under `root` with its kind, bytes, link text and file mode, so a restore can be compared byte for byte. */
28
+ function tree(root: string): Record<string, string> {
29
+ const out: Record<string, string> = {};
30
+ for (const rel of fs.readdirSync(root, { recursive: true, encoding: "utf8" }).sort()) {
31
+ const abs = path.join(root, rel);
32
+ const stat = fs.lstatSync(abs);
33
+ if (stat.isSymbolicLink()) out[rel] = `link ${linkText(abs)}`;
34
+ else if (stat.isDirectory()) out[rel] = "dir";
35
+ else out[rel] = `file ${(stat.mode & 0o777).toString(8)} ${fs.readFileSync(abs, "latin1")}`;
36
+ }
37
+ return out;
38
+ }
39
+
40
+ const file = (p: string, content = `${p}\n`): Output => ({
41
+ kind: "file",
42
+ path: p,
43
+ content,
44
+ tracked: true,
45
+ managed: "whole",
46
+ });
47
+ const link = (p: string, target: string): Output => ({
48
+ kind: "link",
49
+ path: p,
50
+ target,
51
+ tracked: true,
52
+ managed: "whole",
53
+ });
54
+ const deps = { platform: process.platform, writeOutput };
55
+ const MANIFEST = renderManifest(["a.txt"]);
56
+
57
+ /** A tree with an old file, an old link and a prunable file that is the only thing in its dirs. */
58
+ function seeded(): string {
59
+ const root = tempRoot();
60
+ put(root, "a.txt", "old a\n");
61
+ fs.chmodSync(path.join(root, "a.txt"), 0o750);
62
+ fs.mkdirSync(path.join(root, "s1"));
63
+ fs.mkdirSync(path.join(root, "s2"));
64
+ fs.mkdirSync(path.join(root, "tree"));
65
+ fs.symlinkSync("../s1", path.join(root, "tree/lnk"), "dir");
66
+ put(root, "gone/deep/x.txt", "x\n");
67
+ put(root, MANIFEST_REL, renderManifest(["gone/deep/x.txt"]));
68
+ return root;
69
+ }
70
+
71
+ /** Fails the output at `path` with `message` and writes every other one for real. */
72
+ const failing = (target: string, message = "EACCES: permission denied") => ({
73
+ platform: process.platform,
74
+ writeOutput: (root: string, output: Output, platform: NodeJS.Platform) =>
75
+ output.path === target ? message : writeOutput(root, output, platform),
76
+ });
77
+
78
+ describe("writeAll", () => {
79
+ it("writes the changed outputs, removes the pruned paths and writes the manifest", () => {
80
+ const root = seeded();
81
+ const changed = [
82
+ file("a.txt", "new a\n"),
83
+ file("n/ew/b.txt"),
84
+ link("tree/lnk", "../s2"),
85
+ link("fresh/lnk", "../s2"),
86
+ ];
87
+ expect(writeAll(root, changed, ["gone/deep/x.txt"], MANIFEST, deps)).toEqual({ ok: true });
88
+ expect(linkText(path.join(root, "fresh/lnk"))).toBe("../s2");
89
+ expect(fs.readFileSync(path.join(root, "a.txt"), "utf8")).toBe("new a\n");
90
+ expect(fs.readFileSync(path.join(root, "n/ew/b.txt"), "utf8")).toBe("n/ew/b.txt\n");
91
+ expect(linkText(path.join(root, "tree/lnk"))).toBe("../s2");
92
+ expect(fs.existsSync(path.join(root, "gone"))).toBe(false);
93
+ expect(fs.readFileSync(path.join(root, MANIFEST_REL), "utf8")).toBe(MANIFEST);
94
+ });
95
+
96
+ it("leaves a manifest that is already current alone", () => {
97
+ const root = tempRoot();
98
+ put(root, MANIFEST_REL, MANIFEST);
99
+ const rename = vi.spyOn(fs, "renameSync");
100
+ expect(writeAll(root, [], [], MANIFEST, deps)).toEqual({ ok: true });
101
+ expect(rename).not.toHaveBeenCalled();
102
+ });
103
+
104
+ it("restores earlier files, links and created dirs byte for byte when a later output fails", () => {
105
+ const root = seeded();
106
+ const before = tree(root);
107
+ const changed = [file("a.txt", "new a\n"), file("n/ew/b.txt"), link("tree/lnk", "../s2"), file("c.txt")];
108
+ const result = writeAll(root, changed, ["gone/deep/x.txt"], MANIFEST, failing("c.txt"));
109
+ expect(result).toEqual({
110
+ ok: false,
111
+ errors: ["c.txt: cannot write: EACCES: permission denied; restored 3 outputs, nothing changed"],
112
+ refused: false,
113
+ });
114
+ expect(tree(root)).toEqual(before);
115
+ expect(fs.existsSync(path.join(root, "n"))).toBe(false);
116
+ });
117
+
118
+ it("says restored 1 output in the singular", () => {
119
+ const root = seeded();
120
+ const result = writeAll(root, [file("a.txt", "new\n"), file("c.txt")], [], MANIFEST, failing("c.txt", "boom"));
121
+ expect(result).toMatchObject({ errors: ["c.txt: cannot write: boom; restored 1 output, nothing changed"] });
122
+ });
123
+
124
+ it("restores outputs, removals and the dirs a removal emptied when a removal fails", () => {
125
+ const root = seeded();
126
+ put(root, "gone/other.txt", "other\n");
127
+ const before = tree(root);
128
+ const real = fs.rmSync;
129
+ let failed = false;
130
+ vi.spyOn(fs, "rmSync").mockImplementation((target, options) => {
131
+ if (String(target).endsWith("other.txt") && !failed) {
132
+ failed = true;
133
+ throw new Error("EBUSY: resource busy");
134
+ }
135
+ return real(target, options);
136
+ });
137
+ const result = writeAll(root, [file("a.txt", "new\n")], ["gone/deep/x.txt", "gone/other.txt"], MANIFEST, deps);
138
+ expect(result).toEqual({
139
+ ok: false,
140
+ errors: ["gone/other.txt: cannot remove: EBUSY: resource busy; restored 2 outputs, nothing changed"],
141
+ refused: false,
142
+ });
143
+ vi.restoreAllMocks();
144
+ expect(tree(root)).toEqual(before);
145
+ });
146
+
147
+ it("rolls every output and removal back when the manifest write fails", () => {
148
+ const root = seeded();
149
+ fs.rmSync(path.join(root, MANIFEST_REL));
150
+ fs.rmdirSync(path.join(root, ".repo/tmp"));
151
+ fs.rmdirSync(path.join(root, ".repo"));
152
+ const before = tree(root);
153
+ const real = fs.renameSync;
154
+ vi.spyOn(fs, "renameSync").mockImplementation((from, to) => {
155
+ if (String(to).endsWith("sync.json")) throw new Error("EACCES: permission denied, rename");
156
+ return real(from, to);
157
+ });
158
+ const changed = [file("a.txt", "new a\n"), file("n/ew/b.txt"), link("tree/lnk", "../s2")];
159
+ const result = writeAll(root, changed, ["gone/deep/x.txt"], MANIFEST, deps);
160
+ expect(result).toEqual({
161
+ ok: false,
162
+ errors: [`${MANIFEST_REL}: cannot write: EACCES: permission denied, rename; restored 4 outputs, nothing changed`],
163
+ refused: false,
164
+ });
165
+ vi.restoreAllMocks();
166
+ expect(tree(root)).toEqual(before);
167
+ });
168
+
169
+ it("does not try to remove a dir that is already gone when it undoes a failed manifest write", () => {
170
+ const root = tempRoot();
171
+ put(root, ".repo", "a file where the manifest dir belongs\n");
172
+ const result = writeAll(root, [], [], MANIFEST, deps);
173
+ expect(result).toMatchObject({ ok: false, refused: false });
174
+ expect(result).toMatchObject({
175
+ errors: [
176
+ expect.stringMatching(/^\.repo\/tmp\/sync\.json: cannot write: .*; restored 0 outputs, nothing changed$/),
177
+ ],
178
+ });
179
+ expect(fs.readFileSync(path.join(root, ".repo"), "utf8")).toBe("a file where the manifest dir belongs\n");
180
+ });
181
+
182
+ it("refuses a real dir at a skill link path before any write, naming the folder to move", () => {
183
+ const root = seeded();
184
+ put(root, ".claude/skills/ask/SKILL.md", "mine\n");
185
+ const before = tree(root);
186
+ const changed = [file("a.txt", "new\n"), link(".claude/skills/ask", "../../.agents/skills/ask")];
187
+ expect(writeAll(root, changed, ["gone/deep/x.txt"], MANIFEST, deps)).toEqual({
188
+ ok: false,
189
+ errors: [".claude/skills/ask is a folder where sync puts a link; move it to .repo/skills/ask"],
190
+ refused: true,
191
+ });
192
+ expect(tree(root)).toEqual(before);
193
+ });
194
+
195
+ it("refuses a real dir at any other link path with one fix", () => {
196
+ const root = seeded();
197
+ put(root, "tree/real/file.txt", "mine\n");
198
+ put(root, ".agents/skills/b/SKILL.md", "mine\n");
199
+ const before = tree(root);
200
+ const result = writeAll(
201
+ root,
202
+ [link("tree/real", "../s1"), link(".agents/skills/b", "../../s1")],
203
+ [],
204
+ MANIFEST,
205
+ deps,
206
+ );
207
+ expect(result).toEqual({
208
+ ok: false,
209
+ errors: [
210
+ "tree/real is a folder where sync puts a link; move or delete it",
211
+ ".agents/skills/b is a folder where sync puts a link; move it to .repo/skills/b",
212
+ ],
213
+ refused: true,
214
+ });
215
+ expect(tree(root)).toEqual(before);
216
+ });
217
+
218
+ it("converges on a rerun after a restore", () => {
219
+ const root = seeded();
220
+ const changed = [file("a.txt", "new a\n"), file("n/ew/b.txt"), link("tree/lnk", "../s2"), file("c.txt")];
221
+ expect(writeAll(root, changed, ["gone/deep/x.txt"], MANIFEST, failing("c.txt")).ok).toBe(false);
222
+ expect(writeAll(root, changed, ["gone/deep/x.txt"], MANIFEST, deps)).toEqual({ ok: true });
223
+
224
+ const clean = seeded();
225
+ expect(writeAll(clean, changed, ["gone/deep/x.txt"], MANIFEST, deps)).toEqual({ ok: true });
226
+ const strip = (t: Record<string, string>) =>
227
+ Object.fromEntries(Object.entries(t).filter(([rel]) => !rel.startsWith("..")));
228
+ expect(strip(tree(root))).toEqual(strip(tree(clean)));
229
+ });
230
+
231
+ it("leaves a real dir that stands where a file belongs and reports the failed write", () => {
232
+ const root = seeded();
233
+ put(root, "d/keep.txt", "mine\n");
234
+ const before = tree(root);
235
+ const result = writeAll(root, [file("a.txt", "new\n"), file("d")], [], MANIFEST, deps);
236
+ expect(result).toMatchObject({ ok: false, refused: false });
237
+ expect(result).toMatchObject({
238
+ errors: [expect.stringMatching(/^d: cannot write: .*; restored 1 output, nothing changed$/)],
239
+ });
240
+ expect(tree(root)).toEqual(before);
241
+ });
242
+
243
+ it("reports a file it cannot read before touching it", () => {
244
+ const root = seeded();
245
+ const real = fs.readFileSync;
246
+ vi.spyOn(fs, "readFileSync").mockImplementation(((target: fs.PathOrFileDescriptor, options?: unknown) => {
247
+ if (String(target).endsWith("a.txt")) throw new Error("EACCES: permission denied, open");
248
+ return (real as (t: unknown, o?: unknown) => unknown)(target, options);
249
+ }) as typeof fs.readFileSync);
250
+ const result = writeAll(root, [file("a.txt", "new\n")], [], MANIFEST, deps);
251
+ expect(result).toEqual({
252
+ ok: false,
253
+ errors: ["a.txt: cannot write: EACCES: permission denied, open; restored 0 outputs, nothing changed"],
254
+ refused: false,
255
+ });
256
+ vi.restoreAllMocks();
257
+ expect(fs.readFileSync(path.join(root, "a.txt"), "utf8")).toBe("old a\n");
258
+ });
259
+
260
+ it("names what it could not restore and says to rerun", () => {
261
+ const root = seeded();
262
+ const real = fs.writeFileSync;
263
+ vi.spyOn(fs, "writeFileSync").mockImplementation(((
264
+ target: fs.PathOrFileDescriptor,
265
+ data: unknown,
266
+ options?: unknown,
267
+ ) => {
268
+ if (String(target) === path.join(root, "a.txt") && data instanceof Buffer) throw new Error("EIO: i/o error");
269
+ return (real as (t: unknown, d: unknown, o?: unknown) => void)(target, data, options);
270
+ }) as typeof fs.writeFileSync);
271
+ const result = writeAll(root, [file("a.txt", "new a\n"), file("c.txt")], [], MANIFEST, failing("c.txt", "boom"));
272
+ expect(result).toEqual({
273
+ ok: false,
274
+ errors: ["c.txt: cannot write: boom; could not restore a.txt; run `bun repo sync` again"],
275
+ refused: false,
276
+ });
277
+ });
278
+
279
+ it("restores a replaced link with a symlink call that works on every platform", () => {
280
+ const root = seeded();
281
+ const symlink = vi.spyOn(fs, "symlinkSync");
282
+ const result = writeAll(root, [link("tree/lnk", "../s2"), file("c.txt")], [], MANIFEST, failing("c.txt"));
283
+ expect(result).toMatchObject({ ok: false });
284
+ expect(symlink).toHaveBeenLastCalledWith(path.normalize("../s1"), path.join(root, "tree/lnk"), "junction");
285
+ });
286
+
287
+ it("reports a link it cannot make and restores the old one", () => {
288
+ const root = seeded();
289
+ const before = tree(root);
290
+ vi.spyOn(fs, "symlinkSync").mockImplementationOnce(() => {
291
+ throw new Error("EPERM: operation not permitted, symlink");
292
+ });
293
+ const result = writeAll(root, [link("tree/lnk", "../s2")], [], MANIFEST, deps);
294
+ expect(result).toMatchObject({
295
+ errors: ["tree/lnk: cannot write: EPERM: operation not permitted, symlink; restored 0 outputs, nothing changed"],
296
+ });
297
+ expect(tree(root)).toEqual(before);
298
+ });
299
+
300
+ it("writes the default way, and a link as a junction with an absolute target on win32", () => {
301
+ const root = seeded();
302
+ expect(
303
+ writeAll(root, [file("a.txt", "new\n"), link("tree/lnk", "../s2")], [], MANIFEST, { platform: "win32" }),
304
+ ).toEqual({ ok: true });
305
+ expect(fs.readFileSync(path.join(root, "a.txt"), "utf8")).toBe("new\n");
306
+ expect(fs.readlinkSync(path.join(root, "tree/lnk"))).toBe(path.join(root, "s2"));
307
+ });
308
+ });
309
+
310
+ describe("writeOutput modes", () => {
311
+ const hook = (content: string): Output => ({
312
+ kind: "file",
313
+ path: ".githooks/pre-commit",
314
+ content,
315
+ tracked: true,
316
+ managed: "whole",
317
+ executable: true,
318
+ });
319
+ const mode = (root: string, rel: string) => fs.statSync(path.join(root, rel)).mode & 0o777;
320
+
321
+ // Windows has no execute bits; sync marks a hook in the index there instead.
322
+ it.skipIf(process.platform === "win32")(
323
+ "makes an executable output 0755 on POSIX, a new file and a rewritten one alike",
324
+ () => {
325
+ const root = tempRoot();
326
+ expect(writeOutput(root, hook("#!/bin/sh\n"), "linux")).toBeNull();
327
+ expect(mode(root, ".githooks/pre-commit")).toBe(0o755);
328
+ fs.chmodSync(path.join(root, ".githooks/pre-commit"), 0o644);
329
+ expect(writeOutput(root, hook("#!/bin/sh\nnew\n"), "darwin")).toBeNull();
330
+ expect(mode(root, ".githooks/pre-commit")).toBe(0o755);
331
+ },
332
+ );
333
+
334
+ it("leaves a plain output, and any output written for win32, which has no modes, without the execute bits", () => {
335
+ const root = tempRoot();
336
+ expect(writeOutput(root, file("a.txt"), "linux")).toBeNull();
337
+ expect(mode(root, "a.txt") & 0o111).toBe(0);
338
+ expect(writeOutput(root, hook("#!/bin/sh\n"), "win32")).toBeNull();
339
+ expect(mode(root, ".githooks/pre-commit") & 0o111).toBe(0);
340
+ });
341
+ });