@hominis/fireforge 0.45.0 → 0.46.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 (359) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/dist/bin/fireforge.js +6 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/src/cli-usage-error.d.ts +46 -0
  5. package/dist/src/cli-usage-error.js +81 -0
  6. package/dist/src/cli.d.ts +5 -1
  7. package/dist/src/cli.js +20 -21
  8. package/dist/src/commands/bootstrap.js +1 -2
  9. package/dist/src/commands/build.js +15 -6
  10. package/dist/src/commands/destructive-decision.d.ts +19 -0
  11. package/dist/src/commands/destructive-decision.js +31 -0
  12. package/dist/src/commands/doctor-check-core.d.ts +0 -6
  13. package/dist/src/commands/doctor-check-core.js +0 -8
  14. package/dist/src/commands/doctor-orphaned-harness.d.ts +0 -6
  15. package/dist/src/commands/doctor-orphaned-harness.js +4 -12
  16. package/dist/src/commands/doctor-post-rebase-audit.js +1 -1
  17. package/dist/src/commands/doctor.d.ts +2 -0
  18. package/dist/src/commands/doctor.js +7 -8
  19. package/dist/src/commands/download.js +35 -24
  20. package/dist/src/commands/export-all.js +21 -4
  21. package/dist/src/commands/export-flow.js +15 -45
  22. package/dist/src/commands/export-placement-conflicts.js +3 -3
  23. package/dist/src/commands/export-placement-gate.js +2 -8
  24. package/dist/src/commands/export-placement-policy.js +3 -5
  25. package/dist/src/commands/export-shared.d.ts +42 -23
  26. package/dist/src/commands/export-shared.js +4 -19
  27. package/dist/src/commands/export.js +10 -1
  28. package/dist/src/commands/furnace/authoring-config.d.ts +13 -0
  29. package/dist/src/commands/furnace/authoring-config.js +19 -0
  30. package/dist/src/commands/furnace/chrome-doc-remove.js +1 -1
  31. package/dist/src/commands/furnace/create-browser-test.js +3 -8
  32. package/dist/src/commands/furnace/create-dry-run.js +2 -5
  33. package/dist/src/commands/furnace/create-templates.d.ts +17 -1
  34. package/dist/src/commands/furnace/create-templates.js +2 -1
  35. package/dist/src/commands/furnace/create.js +25 -19
  36. package/dist/src/commands/furnace/deploy.js +11 -2
  37. package/dist/src/commands/furnace/init.js +2 -1
  38. package/dist/src/commands/furnace/override.js +28 -19
  39. package/dist/src/commands/furnace/refresh.js +9 -1
  40. package/dist/src/commands/furnace/remove-mutations.d.ts +6 -7
  41. package/dist/src/commands/furnace/remove-mutations.js +0 -1
  42. package/dist/src/commands/furnace/remove.js +2 -6
  43. package/dist/src/commands/furnace/rename-browser-test.js +1 -7
  44. package/dist/src/commands/furnace/rename.js +29 -29
  45. package/dist/src/commands/furnace/status.js +12 -9
  46. package/dist/src/commands/furnace/test-file-name.d.ts +28 -0
  47. package/dist/src/commands/furnace/test-file-name.js +39 -0
  48. package/dist/src/commands/furnace/validation-output.d.ts +21 -9
  49. package/dist/src/commands/furnace/validation-output.js +3 -8
  50. package/dist/src/commands/import.js +2 -9
  51. package/dist/src/commands/lint-per-patch.d.ts +2 -2
  52. package/dist/src/commands/lint-per-patch.js +19 -4
  53. package/dist/src/commands/lint-per-run-checkjs.d.ts +2 -2
  54. package/dist/src/commands/lint.d.ts +2 -1
  55. package/dist/src/commands/lint.js +1 -1
  56. package/dist/src/commands/manifest.js +1 -1
  57. package/dist/src/commands/package.js +2 -4
  58. package/dist/src/commands/patch/compact.js +14 -15
  59. package/dist/src/commands/patch/delete.js +15 -25
  60. package/dist/src/commands/patch/entry-projection.d.ts +13 -0
  61. package/dist/src/commands/patch/entry-projection.js +24 -0
  62. package/dist/src/commands/patch/lint-ignore.d.ts +0 -8
  63. package/dist/src/commands/patch/lint-ignore.js +19 -29
  64. package/dist/src/commands/patch/move-files-create.js +24 -10
  65. package/dist/src/commands/patch/move-files-into.js +38 -34
  66. package/dist/src/commands/patch/move-files.js +4 -13
  67. package/dist/src/commands/patch/rename.js +27 -41
  68. package/dist/src/commands/patch/reorder.js +25 -38
  69. package/dist/src/commands/patch/split-plan.d.ts +4 -5
  70. package/dist/src/commands/patch/split-plan.js +4 -8
  71. package/dist/src/commands/patch/split.js +45 -43
  72. package/dist/src/commands/patch/staged-dependency.js +16 -22
  73. package/dist/src/commands/patch/tier.js +15 -25
  74. package/dist/src/commands/patch-rename-map.d.ts +19 -0
  75. package/dist/src/commands/patch-rename-map.js +36 -0
  76. package/dist/src/commands/pending-resolution.d.ts +15 -0
  77. package/dist/src/commands/pending-resolution.js +25 -0
  78. package/dist/src/commands/re-export-adjacent.d.ts +24 -2
  79. package/dist/src/commands/re-export-adjacent.js +32 -16
  80. package/dist/src/commands/re-export-bulk-scan.js +6 -10
  81. package/dist/src/commands/re-export-drift.js +4 -4
  82. package/dist/src/commands/re-export-files.d.ts +2 -3
  83. package/dist/src/commands/re-export-files.js +38 -29
  84. package/dist/src/commands/re-export-lint.d.ts +3 -4
  85. package/dist/src/commands/re-export-lint.js +9 -2
  86. package/dist/src/commands/re-export-scan.js +5 -2
  87. package/dist/src/commands/re-export-single.d.ts +22 -3
  88. package/dist/src/commands/re-export-single.js +19 -11
  89. package/dist/src/commands/re-export.js +28 -11
  90. package/dist/src/commands/rebase/abort.js +3 -8
  91. package/dist/src/commands/rebase/continue.js +12 -12
  92. package/dist/src/commands/rebase/index.js +2 -2
  93. package/dist/src/commands/rebase/patch-loop.d.ts +2 -2
  94. package/dist/src/commands/rebase/patch-loop.js +2 -7
  95. package/dist/src/commands/resolve.js +18 -10
  96. package/dist/src/commands/run.d.ts +0 -13
  97. package/dist/src/commands/run.js +12 -25
  98. package/dist/src/commands/setup-support.d.ts +16 -4
  99. package/dist/src/commands/setup-support.js +16 -16
  100. package/dist/src/commands/setup.js +3 -9
  101. package/dist/src/commands/source.js +8 -1
  102. package/dist/src/commands/status-check.js +2 -2
  103. package/dist/src/commands/status-output.js +2 -2
  104. package/dist/src/commands/status.js +3 -4
  105. package/dist/src/commands/test-build-phase.d.ts +2 -2
  106. package/dist/src/commands/test-build-phase.js +31 -7
  107. package/dist/src/commands/test-modes.d.ts +2 -2
  108. package/dist/src/commands/test-modes.js +0 -1
  109. package/dist/src/commands/test-register.js +5 -3
  110. package/dist/src/commands/test-run.js +25 -18
  111. package/dist/src/commands/test-stale-gate.js +4 -4
  112. package/dist/src/commands/test-verdict.d.ts +16 -2
  113. package/dist/src/commands/test-verdict.js +18 -3
  114. package/dist/src/commands/test.js +42 -5
  115. package/dist/src/commands/token-list.d.ts +26 -0
  116. package/dist/src/commands/token-list.js +153 -0
  117. package/dist/src/commands/token.js +17 -1
  118. package/dist/src/commands/tree.js +7 -2
  119. package/dist/src/commands/typecheck.js +9 -9
  120. package/dist/src/commands/wire.js +4 -3
  121. package/dist/src/core/branding.d.ts +0 -7
  122. package/dist/src/core/branding.js +3 -2
  123. package/dist/src/core/browser-wire.js +6 -3
  124. package/dist/src/core/build-audit-platform.d.ts +29 -0
  125. package/dist/src/core/build-audit-platform.js +140 -0
  126. package/dist/src/core/build-audit-registration.js +3 -2
  127. package/dist/src/core/build-audit-skip.d.ts +57 -0
  128. package/dist/src/core/build-audit-skip.js +112 -0
  129. package/dist/src/core/build-audit.d.ts +15 -2
  130. package/dist/src/core/build-audit.js +110 -10
  131. package/dist/src/core/build-baseline.d.ts +45 -28
  132. package/dist/src/core/build-baseline.js +6 -29
  133. package/dist/src/core/build-jar-escalation.js +3 -2
  134. package/dist/src/core/build-overwrite-guard.js +1 -1
  135. package/dist/src/core/build-prepare.d.ts +0 -8
  136. package/dist/src/core/build-prepare.js +7 -12
  137. package/dist/src/core/config-paths.d.ts +2 -6
  138. package/dist/src/core/config-paths.js +4 -2
  139. package/dist/src/core/config-state.d.ts +0 -6
  140. package/dist/src/core/config-state.js +1 -19
  141. package/dist/src/core/config-validate-build-audit.d.ts +8 -0
  142. package/dist/src/core/config-validate-build-audit.js +80 -0
  143. package/dist/src/core/config-validate-file-size.js +1 -1
  144. package/dist/src/core/config-validate-patch-policy.d.ts +2 -2
  145. package/dist/src/core/config-validate-patch-policy.js +3 -3
  146. package/dist/src/core/config-validate-test-toolchains.d.ts +2 -2
  147. package/dist/src/core/config-validate-test-toolchains.js +4 -5
  148. package/dist/src/core/config-validate.js +14 -3
  149. package/dist/src/core/config.d.ts +2 -2
  150. package/dist/src/core/config.js +11 -15
  151. package/dist/src/core/coverage-extend.js +4 -4
  152. package/dist/src/core/discard-baseline.js +7 -7
  153. package/dist/src/core/engine-precondition.d.ts +1 -9
  154. package/dist/src/core/engine-precondition.js +3 -8
  155. package/dist/src/core/engine-session-lock.js +13 -17
  156. package/dist/src/core/export-moved-code-shape.js +2 -2
  157. package/dist/src/core/file-lock-owner.d.ts +103 -0
  158. package/dist/src/core/file-lock-owner.js +259 -0
  159. package/dist/src/core/file-lock.d.ts +2 -7
  160. package/dist/src/core/file-lock.js +40 -91
  161. package/dist/src/core/firefox-cache.d.ts +13 -1
  162. package/dist/src/core/firefox-cache.js +59 -34
  163. package/dist/src/core/firefox-download.d.ts +15 -0
  164. package/dist/src/core/firefox-download.js +37 -21
  165. package/dist/src/core/firefox-extract.d.ts +6 -5
  166. package/dist/src/core/firefox-extract.js +36 -18
  167. package/dist/src/core/firefox.d.ts +30 -32
  168. package/dist/src/core/firefox.js +5 -36
  169. package/dist/src/core/furnace-apply-dry-run.d.ts +1 -6
  170. package/dist/src/core/furnace-apply-dry-run.js +1 -7
  171. package/dist/src/core/furnace-apply-helpers.d.ts +0 -11
  172. package/dist/src/core/furnace-apply-helpers.js +8 -19
  173. package/dist/src/core/furnace-apply-overwrite-warn.js +4 -2
  174. package/dist/src/core/furnace-apply.d.ts +1 -1
  175. package/dist/src/core/furnace-apply.js +30 -10
  176. package/dist/src/core/furnace-config.d.ts +1 -1
  177. package/dist/src/core/furnace-config.js +23 -36
  178. package/dist/src/core/furnace-constants.js +3 -2
  179. package/dist/src/core/furnace-dir-entry.d.ts +20 -0
  180. package/dist/src/core/furnace-dir-entry.js +21 -0
  181. package/dist/src/core/furnace-operation.d.ts +2 -0
  182. package/dist/src/core/furnace-operation.js +3 -1
  183. package/dist/src/core/furnace-refresh.d.ts +18 -6
  184. package/dist/src/core/furnace-refresh.js +3 -6
  185. package/dist/src/core/furnace-registration-ast.d.ts +0 -1
  186. package/dist/src/core/furnace-registration-ast.js +0 -3
  187. package/dist/src/core/furnace-registration.d.ts +2 -1
  188. package/dist/src/core/furnace-registration.js +22 -29
  189. package/dist/src/core/furnace-scanner.js +3 -6
  190. package/dist/src/core/furnace-state-persist.d.ts +2 -2
  191. package/dist/src/core/furnace-validate-registration.js +5 -7
  192. package/dist/src/core/git-base.d.ts +3 -1
  193. package/dist/src/core/git-base.js +25 -3
  194. package/dist/src/core/git-diff-new-file.d.ts +40 -0
  195. package/dist/src/core/git-diff-new-file.js +119 -0
  196. package/dist/src/core/git-diff.d.ts +9 -9
  197. package/dist/src/core/git-diff.js +88 -157
  198. package/dist/src/core/git-file-ops.d.ts +0 -7
  199. package/dist/src/core/git-file-ops.js +4 -28
  200. package/dist/src/core/git-readonly-index.d.ts +0 -2
  201. package/dist/src/core/git-readonly-index.js +0 -4
  202. package/dist/src/core/git-status.d.ts +7 -0
  203. package/dist/src/core/git-status.js +21 -9
  204. package/dist/src/core/git.d.ts +0 -6
  205. package/dist/src/core/git.js +14 -43
  206. package/dist/src/core/harness-orphans.js +8 -7
  207. package/dist/src/core/history-log.d.ts +19 -0
  208. package/dist/src/core/history-log.js +29 -0
  209. package/dist/src/core/mach-error-hints.d.ts +0 -5
  210. package/dist/src/core/mach-error-hints.js +1 -1
  211. package/dist/src/core/mach-known-noise-filter.d.ts +4 -3
  212. package/dist/src/core/mach-known-noise-filter.js +7 -9
  213. package/dist/src/core/mach-mozconfig.js +2 -1
  214. package/dist/src/core/mach-python.d.ts +2 -0
  215. package/dist/src/core/mach-python.js +12 -7
  216. package/dist/src/core/mach-resource-shim.d.ts +13 -1
  217. package/dist/src/core/mach-resource-shim.js +112 -9
  218. package/dist/src/core/mach.d.ts +39 -53
  219. package/dist/src/core/mach.js +13 -81
  220. package/dist/src/core/marionette-port.d.ts +0 -17
  221. package/dist/src/core/marionette-port.js +12 -58
  222. package/dist/src/core/marionette-preflight.js +9 -27
  223. package/dist/src/core/mochitest-server-port.js +3 -3
  224. package/dist/src/core/moz-manifest-rules.js +37 -33
  225. package/dist/src/core/patch-apply-fuzz.js +4 -4
  226. package/dist/src/core/patch-apply.d.ts +4 -5
  227. package/dist/src/core/patch-apply.js +6 -12
  228. package/dist/src/core/patch-export-coverage.d.ts +0 -9
  229. package/dist/src/core/patch-export-coverage.js +1 -30
  230. package/dist/src/core/patch-export-update.d.ts +18 -1
  231. package/dist/src/core/patch-export-update.js +4 -3
  232. package/dist/src/core/patch-export.d.ts +11 -21
  233. package/dist/src/core/patch-export.js +44 -66
  234. package/dist/src/core/patch-files.d.ts +0 -4
  235. package/dist/src/core/patch-files.js +0 -11
  236. package/dist/src/core/patch-lint-binary.d.ts +6 -12
  237. package/dist/src/core/patch-lint-cache.d.ts +17 -1
  238. package/dist/src/core/patch-lint-cache.js +3 -6
  239. package/dist/src/core/patch-lint-checkjs.d.ts +33 -16
  240. package/dist/src/core/patch-lint-checkjs.js +37 -24
  241. package/dist/src/core/patch-lint-creators.d.ts +2 -6
  242. package/dist/src/core/patch-lint-cross.d.ts +16 -8
  243. package/dist/src/core/patch-lint-cross.js +49 -92
  244. package/dist/src/core/patch-lint-css.js +10 -6
  245. package/dist/src/core/patch-lint-file-size.js +3 -3
  246. package/dist/src/core/patch-lint-forward-registration.d.ts +58 -0
  247. package/dist/src/core/patch-lint-forward-registration.js +210 -0
  248. package/dist/src/core/patch-lint-jsdoc.js +36 -36
  249. package/dist/src/core/patch-lint-module-registration.d.ts +2 -8
  250. package/dist/src/core/patch-lint-prettier.js +1 -1
  251. package/dist/src/core/patch-lint-queue-types.d.ts +60 -0
  252. package/dist/src/core/patch-lint-queue-types.js +12 -0
  253. package/dist/src/core/patch-lint-staged-registration.d.ts +61 -0
  254. package/dist/src/core/patch-lint-staged-registration.js +90 -0
  255. package/dist/src/core/patch-lint.d.ts +14 -9
  256. package/dist/src/core/patch-lint.js +4 -3
  257. package/dist/src/core/patch-lock.js +3 -3
  258. package/dist/src/core/patch-manifest-io.js +8 -7
  259. package/dist/src/core/patch-parse.d.ts +19 -8
  260. package/dist/src/core/patch-parse.js +25 -0
  261. package/dist/src/core/patch-policy.js +15 -27
  262. package/dist/src/core/patch-source-metadata.d.ts +1 -3
  263. package/dist/src/core/patch-source-metadata.js +0 -4
  264. package/dist/src/core/patch-transform.d.ts +0 -8
  265. package/dist/src/core/patch-transform.js +0 -10
  266. package/dist/src/core/process-cpu-time.js +1 -1
  267. package/dist/src/core/rebase-session.d.ts +2 -8
  268. package/dist/src/core/rebase-session.js +5 -17
  269. package/dist/src/core/register-browser-content.js +3 -2
  270. package/dist/src/core/register-browser-test.js +2 -1
  271. package/dist/src/core/register-module.js +2 -1
  272. package/dist/src/core/register-scaffold.js +5 -4
  273. package/dist/src/core/register-shared-css.js +2 -1
  274. package/dist/src/core/run-log-redact.d.ts +38 -0
  275. package/dist/src/core/run-log-redact.js +69 -0
  276. package/dist/src/core/run-log.d.ts +13 -2
  277. package/dist/src/core/run-log.js +67 -18
  278. package/dist/src/core/signal-critical.js +2 -4
  279. package/dist/src/core/smoke-patterns.d.ts +0 -1
  280. package/dist/src/core/smoke-patterns.js +6 -11
  281. package/dist/src/core/state-file.d.ts +12 -1
  282. package/dist/src/core/state-file.js +14 -1
  283. package/dist/src/core/status-classify.js +2 -1
  284. package/dist/src/core/test-changed-prefs.js +26 -11
  285. package/dist/src/core/test-harness-crash.js +2 -2
  286. package/dist/src/core/test-stale-check.d.ts +0 -2
  287. package/dist/src/core/test-stale-check.js +2 -2
  288. package/dist/src/core/test-xpcshell-retry.d.ts +26 -7
  289. package/dist/src/core/test-xpcshell-retry.js +14 -11
  290. package/dist/src/core/token-category.d.ts +33 -0
  291. package/dist/src/core/token-category.js +56 -48
  292. package/dist/src/core/token-dark-mode.js +2 -14
  293. package/dist/src/core/token-inventory.d.ts +56 -0
  294. package/dist/src/core/token-inventory.js +109 -0
  295. package/dist/src/core/token-manager.d.ts +1 -1
  296. package/dist/src/core/token-manager.js +1 -1
  297. package/dist/src/core/token-variant.js +21 -22
  298. package/dist/src/core/toolchain-preflight.js +1 -1
  299. package/dist/src/core/tree-cow.d.ts +0 -2
  300. package/dist/src/core/tree-cow.js +2 -2
  301. package/dist/src/core/tree-store.js +4 -5
  302. package/dist/src/core/typecheck.js +2 -2
  303. package/dist/src/core/wire-dom-fragment.js +6 -17
  304. package/dist/src/core/wire-utils.js +5 -17
  305. package/dist/src/core/xpcshell-appdir.js +2 -4
  306. package/dist/src/errors/base.d.ts +53 -1
  307. package/dist/src/errors/base.js +79 -11
  308. package/dist/src/errors/build.js +20 -28
  309. package/dist/src/errors/config.d.ts +0 -14
  310. package/dist/src/errors/config.js +13 -34
  311. package/dist/src/errors/download.d.ts +13 -0
  312. package/dist/src/errors/download.js +53 -24
  313. package/dist/src/errors/furnace.js +8 -9
  314. package/dist/src/errors/git.d.ts +8 -8
  315. package/dist/src/errors/git.js +20 -30
  316. package/dist/src/errors/patch.js +8 -10
  317. package/dist/src/errors/rebase.js +7 -7
  318. package/dist/src/types/commands/options.d.ts +77 -123
  319. package/dist/src/types/config.d.ts +34 -2
  320. package/dist/src/types/index.d.ts +0 -1
  321. package/dist/src/utils/assert.d.ts +3 -15
  322. package/dist/src/utils/assert.js +1 -18
  323. package/dist/src/utils/build-info.d.ts +1 -1
  324. package/dist/src/utils/child-env.d.ts +2 -1
  325. package/dist/src/utils/elapsed.js +10 -1
  326. package/dist/src/utils/fs.js +8 -13
  327. package/dist/src/utils/hash.d.ts +12 -0
  328. package/dist/src/utils/hash.js +16 -0
  329. package/dist/src/utils/line-dispatch.d.ts +15 -0
  330. package/dist/src/utils/line-dispatch.js +48 -0
  331. package/dist/src/utils/options.d.ts +1 -7
  332. package/dist/src/utils/options.js +6 -7
  333. package/dist/src/utils/package-root.js +3 -12
  334. package/dist/src/utils/platform.d.ts +0 -4
  335. package/dist/src/utils/process-group.d.ts +2 -1
  336. package/dist/src/utils/process-group.js +20 -19
  337. package/dist/src/utils/process.d.ts +40 -26
  338. package/dist/src/utils/process.js +115 -166
  339. package/dist/src/utils/ps-duration.js +3 -3
  340. package/dist/src/utils/sleep.d.ts +26 -0
  341. package/dist/src/utils/sleep.js +31 -0
  342. package/dist/src/utils/validation.d.ts +2 -38
  343. package/dist/src/utils/validation.js +7 -16
  344. package/docs/configuration.md +89 -1
  345. package/docs/exit-codes.md +31 -19
  346. package/docs/lifecycle-invariants.md +18 -12
  347. package/docs/machine-output.md +31 -8
  348. package/docs/patch-workflow.md +2 -1
  349. package/docs/run-logs.md +44 -4
  350. package/docs/testing.md +8 -1
  351. package/package.json +8 -7
  352. package/dist/src/commands/rebase.d.ts +0 -4
  353. package/dist/src/commands/rebase.js +0 -5
  354. package/dist/src/core/patch-artifact-normalize.d.ts +0 -21
  355. package/dist/src/core/patch-artifact-normalize.js +0 -24
  356. package/dist/src/core/patch-lint-reexports.d.ts +0 -7
  357. package/dist/src/core/patch-lint-reexports.js +0 -8
  358. package/dist/src/core/wire-targets.d.ts +0 -7
  359. package/dist/src/core/wire-targets.js +0 -8
@@ -1,9 +1,9 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
- import { createHash } from 'node:crypto';
3
2
  import { readdir } from 'node:fs/promises';
4
3
  import { join } from 'node:path';
5
4
  import { toError } from '../utils/errors.js';
6
5
  import { pathExists, readText } from '../utils/fs.js';
6
+ import { sha256Hex } from '../utils/hash.js';
7
7
  import { warn } from '../utils/logger.js';
8
8
  import { escapeRegex, stripJsComments } from '../utils/regex.js';
9
9
  import { getProjectPaths, loadConfig } from './config.js';
@@ -129,8 +129,8 @@ export async function checkRegistrationConsistency(root, name, config, ftlDir) {
129
129
  // comparisons hashed raw bytes, so on a CRLF checkout `furnace status`
130
130
  // and `furnace validate` reported drift for files apply had just
131
131
  // decided were identical.
132
- const srcHash = createHash('sha256').update(normalizeForChecksum(srcContent)).digest('hex');
133
- const destHash = createHash('sha256').update(normalizeForChecksum(destContent)).digest('hex');
132
+ const srcHash = sha256Hex(normalizeForChecksum(srcContent));
133
+ const destHash = sha256Hex(normalizeForChecksum(destContent));
134
134
  if (srcHash !== destHash) {
135
135
  status.driftedFiles.push(entry.name);
136
136
  status.filesInSync = false;
@@ -155,10 +155,8 @@ export async function checkRegistrationConsistency(root, name, config, ftlDir) {
155
155
  else {
156
156
  const srcContent = await readText(ftlSrc);
157
157
  const destContent = await readText(ftlDest);
158
- const srcHash = createHash('sha256').update(normalizeForChecksum(srcContent)).digest('hex');
159
- const destHash = createHash('sha256')
160
- .update(normalizeForChecksum(destContent))
161
- .digest('hex');
158
+ const srcHash = sha256Hex(normalizeForChecksum(srcContent));
159
+ const destHash = sha256Hex(normalizeForChecksum(destContent));
162
160
  if (srcHash !== destHash) {
163
161
  status.driftedFiles.push(ftlName);
164
162
  status.filesInSync = false;
@@ -47,7 +47,9 @@ export interface GitStatusEntry {
47
47
  isDeleted: boolean;
48
48
  }
49
49
  /**
50
- * Ensures git is available in the system.
50
+ * Ensures git is available in the system. Memoised after the first successful
51
+ * probe; {@link git} calls it, so callers that go through that wrapper do not
52
+ * need to.
51
53
  * @throws GitNotFoundError if git is not installed
52
54
  */
53
55
  export declare function ensureGit(): Promise<void>;
@@ -43,12 +43,33 @@ export const GIT_ADD_TIMEOUT_MS = resolveTimeoutFromEnv(GIT_ADD_TIMEOUT_ENV_VAR,
43
43
  */
44
44
  export const GIT_ADD_CHUNK_TIMEOUT_MS = resolveTimeoutFromEnv(GIT_ADD_CHUNK_TIMEOUT_ENV_VAR, DEFAULT_GIT_ADD_CHUNK_TIMEOUT_MS);
45
45
  /**
46
- * Ensures git is available in the system.
46
+ * In-flight or settled successful `git` availability probe.
47
+ *
48
+ * The probe spawns `which git` (or `where` on Windows), and every git wrapper
49
+ * in this layer used to run it before each command — dozens of extra spawns
50
+ * per `fireforge export`. Only SUCCESS is cached: a rejection clears the slot
51
+ * so a run that installs git mid-flight (or a test that flips the probe) sees
52
+ * the new answer instead of a sticky refusal.
53
+ */
54
+ let gitAvailability;
55
+ /**
56
+ * Ensures git is available in the system. Memoised after the first successful
57
+ * probe; {@link git} calls it, so callers that go through that wrapper do not
58
+ * need to.
47
59
  * @throws GitNotFoundError if git is not installed
48
60
  */
49
61
  export async function ensureGit() {
50
- if (!(await executableExists('git'))) {
51
- throw new GitNotFoundError();
62
+ gitAvailability ??= (async () => {
63
+ if (!(await executableExists('git'))) {
64
+ throw new GitNotFoundError();
65
+ }
66
+ })();
67
+ try {
68
+ await gitAvailability;
69
+ }
70
+ catch (error) {
71
+ gitAvailability = undefined;
72
+ throw error;
52
73
  }
53
74
  }
54
75
  /**
@@ -58,6 +79,7 @@ export async function ensureGit() {
58
79
  * @returns Command output
59
80
  */
60
81
  export async function git(args, cwd, options) {
82
+ await ensureGit();
61
83
  const execOptions = { cwd };
62
84
  if (options?.timeout !== undefined) {
63
85
  execOptions.timeout = options.timeout;
@@ -0,0 +1,40 @@
1
+ /** How a new file is carried in a patch, plus its text when it is text. */
2
+ export type NewFileContent = {
3
+ readonly binary: true;
4
+ } | {
5
+ readonly binary: false;
6
+ readonly content: string;
7
+ };
8
+ /**
9
+ * Reads a new (untracked) file and classifies it for patch generation.
10
+ *
11
+ * A file is binary when git's NUL-byte heuristic says so
12
+ * ({@link isBinaryFile}) OR when its bytes are not valid UTF-8 — see the
13
+ * module comment for why the second arm exists. Text files come back with
14
+ * their exact decoded content so callers do not read the file twice.
15
+ *
16
+ * @param repoDir - Repository directory
17
+ * @param filePath - File path (relative to repo root)
18
+ * @returns The classification, with content for text files
19
+ */
20
+ export declare function readNewFileContent(repoDir: string, filePath: string): Promise<NewFileContent>;
21
+ /**
22
+ * Runs `task` with git config arguments that force `filePath` to be diffed
23
+ * as binary.
24
+ *
25
+ * `git diff --binary` only emits a `GIT binary patch` for a file git itself
26
+ * classifies as binary, and git's classifier is the NUL heuristic — a
27
+ * Latin-1 file passes it and comes out as a TEXT diff whose high bytes are
28
+ * then mangled by the UTF-8 decode every subprocess result goes through.
29
+ * The `-diff` attribute is how git is told otherwise, and a throwaway
30
+ * `core.attributesFile` naming just this path is the narrowest way to set it
31
+ * without touching the tree's own `.gitattributes`. (An in-tree attribute
32
+ * that explicitly sets `diff` for the path would still win; the tree's own
33
+ * attributes are its business.)
34
+ *
35
+ * @param filePath - File path (relative to repo root)
36
+ * @param task - Receives `['-c', 'core.attributesFile=...']` to splice ahead
37
+ * of the git subcommand
38
+ * @returns Whatever `task` returns
39
+ */
40
+ export declare function withForcedBinaryAttribute<T>(filePath: string, task: (gitConfigArgs: string[]) => Promise<T>): Promise<T>;
@@ -0,0 +1,119 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Decides how a NEW (untracked) file travels inside a patch: as a synthesized
4
+ * `+`-line text body, or as a `GIT binary patch`.
5
+ *
6
+ * The text body is built from a JavaScript string, and the `index` line next
7
+ * to it comes from `git hash-object` over the file's real bytes. Those two
8
+ * agree only when the bytes decode losslessly as UTF-8. A Latin-1
9
+ * `.properties` or `.rc` file has no NUL byte — git's own binary heuristic
10
+ * calls it text — but a UTF-8 decode turns every high byte into U+FFFD, so
11
+ * the patch body and its blob hash disagree, `git apply` writes different
12
+ * bytes than the tree holds, and every drift check flags the file forever.
13
+ * A binary patch carries the bytes verbatim, so it is the faithful choice for
14
+ * anything that is not valid UTF-8. This module is the single place that
15
+ * decision is made.
16
+ */
17
+ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
18
+ import { tmpdir } from 'node:os';
19
+ import { join } from 'node:path';
20
+ import { isBinaryFile } from './git-file-ops.js';
21
+ // `ignoreBOM: true` keeps a leading U+FEFF in the output instead of silently
22
+ // consuming it, matching `readFile(path, 'utf-8')`, so a BOM-prefixed file
23
+ // still renders the byte the blob hash was computed over.
24
+ const strictUtf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
25
+ /**
26
+ * Decodes bytes as UTF-8, or reports that they are not valid UTF-8.
27
+ * @param bytes - Raw file bytes
28
+ * @returns The decoded text, or undefined when any sequence is malformed
29
+ */
30
+ function decodeStrictUtf8(bytes) {
31
+ try {
32
+ return strictUtf8.decode(bytes);
33
+ }
34
+ catch (error) {
35
+ void error;
36
+ return undefined;
37
+ }
38
+ }
39
+ /**
40
+ * Reads a new (untracked) file and classifies it for patch generation.
41
+ *
42
+ * A file is binary when git's NUL-byte heuristic says so
43
+ * ({@link isBinaryFile}) OR when its bytes are not valid UTF-8 — see the
44
+ * module comment for why the second arm exists. Text files come back with
45
+ * their exact decoded content so callers do not read the file twice.
46
+ *
47
+ * @param repoDir - Repository directory
48
+ * @param filePath - File path (relative to repo root)
49
+ * @returns The classification, with content for text files
50
+ */
51
+ export async function readNewFileContent(repoDir, filePath) {
52
+ if (await isBinaryFile(repoDir, filePath)) {
53
+ return { binary: true };
54
+ }
55
+ const content = decodeStrictUtf8(await readFile(join(repoDir, filePath)));
56
+ if (content === undefined) {
57
+ return { binary: true };
58
+ }
59
+ return { binary: false, content };
60
+ }
61
+ /**
62
+ * Renders one repo-relative path as an anchored, literal gitattributes
63
+ * pattern: the whole thing is C-quoted (which gitattributes accepts for
64
+ * patterns holding spaces or `#`) and glob metacharacters carry a backslash
65
+ * that survives the unquoting, so git matches them literally.
66
+ *
67
+ * Both escapes are applied in a single pass, keyed on the source character.
68
+ * Escaping in two passes would re-escape the backslashes the first pass just
69
+ * added, and a backslash that is part of the filename would have to be
70
+ * escaped for the glob layer as well as the quoting layer — which a
71
+ * glob-only first pass cannot tell apart from one it emitted itself.
72
+ *
73
+ * @param filePath - File path (relative to repo root)
74
+ * @returns A pattern matching exactly that path
75
+ */
76
+ function toLiteralAttributePattern(filePath) {
77
+ // `"` only needs the quoting layer, so one backslash. A glob
78
+ // metacharacter takes a backslash for the glob layer, which the quoting
79
+ // layer then doubles into two. A literal `\` is both: the glob layer
80
+ // doubles it, then the quoting layer doubles that again, for four.
81
+ const escaped = filePath.replace(/["\\*?[\]]/g, (char) => {
82
+ if (char === '"')
83
+ return '\\"';
84
+ if (char === '\\')
85
+ return '\\\\\\\\';
86
+ return `\\\\${char}`;
87
+ });
88
+ return `"/${escaped}"`;
89
+ }
90
+ /**
91
+ * Runs `task` with git config arguments that force `filePath` to be diffed
92
+ * as binary.
93
+ *
94
+ * `git diff --binary` only emits a `GIT binary patch` for a file git itself
95
+ * classifies as binary, and git's classifier is the NUL heuristic — a
96
+ * Latin-1 file passes it and comes out as a TEXT diff whose high bytes are
97
+ * then mangled by the UTF-8 decode every subprocess result goes through.
98
+ * The `-diff` attribute is how git is told otherwise, and a throwaway
99
+ * `core.attributesFile` naming just this path is the narrowest way to set it
100
+ * without touching the tree's own `.gitattributes`. (An in-tree attribute
101
+ * that explicitly sets `diff` for the path would still win; the tree's own
102
+ * attributes are its business.)
103
+ *
104
+ * @param filePath - File path (relative to repo root)
105
+ * @param task - Receives `['-c', 'core.attributesFile=...']` to splice ahead
106
+ * of the git subcommand
107
+ * @returns Whatever `task` returns
108
+ */
109
+ export async function withForcedBinaryAttribute(filePath, task) {
110
+ const tempDir = await mkdtemp(join(tmpdir(), 'fireforge-attr-'));
111
+ try {
112
+ const attributesFile = join(tempDir, 'attributes');
113
+ await writeFile(attributesFile, `${toLiteralAttributePattern(filePath)} -diff\n`);
114
+ return await task(['-c', `core.attributesFile=${attributesFile}`]);
115
+ }
116
+ finally {
117
+ await rm(tempDir, { recursive: true, force: true });
118
+ }
119
+ }
@@ -12,7 +12,10 @@
12
12
  */
13
13
  export declare function getFileDiff(repoDir: string, filePath: string): Promise<string>;
14
14
  /**
15
- * Generates a unified diff for a new (untracked) file.
15
+ * Generates a unified diff for a new (untracked) file. A file that is binary
16
+ * or not valid UTF-8 (see {@link readNewFileContent}) is routed through
17
+ * {@link generateBinaryFilePatch} instead, because a text body cannot carry
18
+ * its bytes faithfully.
16
19
  * @param repoDir - Repository directory
17
20
  * @param filePath - Path to the file (relative to repo)
18
21
  * @returns Diff content in unified diff format
@@ -28,14 +31,6 @@ export declare function generateNewFileDiff(repoDir: string, filePath: string):
28
31
  * @returns Diff content in unified diff format
29
32
  */
30
33
  export declare function generateFullFilePatch(repoDir: string, filePath: string): Promise<string>;
31
- /**
32
- * Generates a unified diff between base content and current file content.
33
- * @param repoDir - Repository directory
34
- * @param filePath - Path to the file (relative to repo)
35
- * @param baseContent - The base content to diff against
36
- * @returns Unified diff in git format
37
- */
38
- export declare function generateModificationDiff(repoDir: string, filePath: string, baseContent: string): Promise<string>;
39
34
  /**
40
35
  * Gets the diff for all modified files, including untracked (new) files.
41
36
  *
@@ -75,6 +70,11 @@ export declare function getStagedDiffForFiles(repoDir: string, files: string[]):
75
70
  * Generates a GIT binary patch for a binary file.
76
71
  * For tracked files, uses `git diff --binary HEAD`.
77
72
  * For untracked files, temporarily stages with `--intent-to-add` to produce a diff.
73
+ *
74
+ * Every `git diff` here runs under {@link withForcedBinaryAttribute}: a
75
+ * caller sends a file down this arm because a text body cannot carry its
76
+ * bytes (NUL, or not valid UTF-8), and git's own NUL-only heuristic would
77
+ * otherwise still render the non-UTF-8 case as text.
78
78
  * @param repoDir - Repository directory
79
79
  * @param filePath - File path (relative to repo root)
80
80
  * @returns The binary diff string, or empty string if no diff