@hominis/fireforge 0.43.1 → 0.44.1

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 (478) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/README.md +3 -3
  3. package/dist/bin/fireforge.d.ts +4 -9
  4. package/dist/bin/fireforge.js +42 -40
  5. package/dist/build-info.json +4 -4
  6. package/dist/src/cli.js +74 -8
  7. package/dist/src/commands/bootstrap.js +17 -20
  8. package/dist/src/commands/build.js +25 -32
  9. package/dist/src/commands/config.js +34 -41
  10. package/dist/src/commands/discard.js +8 -9
  11. package/dist/src/commands/doctor-check-core.d.ts +13 -18
  12. package/dist/src/commands/doctor-check-core.js +8 -35
  13. package/dist/src/commands/doctor-external-toolchains.js +2 -2
  14. package/dist/src/commands/doctor-furnace-config-sync.d.ts +18 -0
  15. package/dist/src/commands/{doctor-furnace-manifest-sync.js → doctor-furnace-config-sync.js} +27 -26
  16. package/dist/src/commands/doctor-furnace-jar.d.ts +7 -9
  17. package/dist/src/commands/doctor-furnace-jar.js +7 -9
  18. package/dist/src/commands/doctor-furnace.js +21 -24
  19. package/dist/src/commands/doctor-orphaned-harness.d.ts +16 -20
  20. package/dist/src/commands/doctor-orphaned-harness.js +18 -44
  21. package/dist/src/commands/doctor-patch-manifest.d.ts +8 -0
  22. package/dist/src/commands/doctor-patch-manifest.js +155 -0
  23. package/dist/src/commands/doctor-post-rebase-audit.d.ts +2 -0
  24. package/dist/src/commands/{doctor/post-rebase-audit.js → doctor-post-rebase-audit.js} +3 -3
  25. package/dist/src/commands/doctor-working-tree.d.ts +10 -12
  26. package/dist/src/commands/doctor-working-tree.js +11 -18
  27. package/dist/src/commands/doctor.d.ts +1 -1
  28. package/dist/src/commands/doctor.js +64 -96
  29. package/dist/src/commands/download.js +32 -48
  30. package/dist/src/commands/export-all.js +29 -33
  31. package/dist/src/commands/export-flow.js +6 -6
  32. package/dist/src/commands/export-placement-conflicts.js +10 -9
  33. package/dist/src/commands/export-placement-gate.js +5 -5
  34. package/dist/src/commands/export-shared.d.ts +30 -33
  35. package/dist/src/commands/export-shared.js +45 -43
  36. package/dist/src/commands/export.js +21 -27
  37. package/dist/src/commands/furnace/apply.js +14 -23
  38. package/dist/src/commands/furnace/chrome-doc-remove.js +14 -21
  39. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +23 -23
  40. package/dist/src/commands/furnace/chrome-doc-templates.js +23 -23
  41. package/dist/src/commands/furnace/chrome-doc-tests.js +26 -29
  42. package/dist/src/commands/furnace/chrome-doc.d.ts +16 -17
  43. package/dist/src/commands/furnace/chrome-doc.js +31 -41
  44. package/dist/src/commands/furnace/create-browser-test.d.ts +4 -4
  45. package/dist/src/commands/furnace/create-browser-test.js +8 -8
  46. package/dist/src/commands/furnace/create-mochikit.d.ts +6 -7
  47. package/dist/src/commands/furnace/create-mochikit.js +8 -9
  48. package/dist/src/commands/furnace/create-readback.d.ts +12 -13
  49. package/dist/src/commands/furnace/create-readback.js +12 -13
  50. package/dist/src/commands/furnace/create-templates.d.ts +22 -28
  51. package/dist/src/commands/furnace/create-templates.js +35 -30
  52. package/dist/src/commands/furnace/create-xpcshell.js +2 -2
  53. package/dist/src/commands/furnace/create.js +33 -23
  54. package/dist/src/commands/furnace/deploy.js +16 -24
  55. package/dist/src/commands/furnace/diff.js +15 -10
  56. package/dist/src/commands/furnace/init.js +33 -36
  57. package/dist/src/commands/furnace/override.js +23 -25
  58. package/dist/src/commands/furnace/preview.js +47 -67
  59. package/dist/src/commands/furnace/refresh.js +12 -16
  60. package/dist/src/commands/furnace/remove-mutations.d.ts +3 -6
  61. package/dist/src/commands/furnace/remove-mutations.js +14 -17
  62. package/dist/src/commands/furnace/remove-state.js +1 -1
  63. package/dist/src/commands/furnace/remove.js +33 -38
  64. package/dist/src/commands/furnace/rename-xpcshell.d.ts +8 -11
  65. package/dist/src/commands/furnace/rename-xpcshell.js +8 -11
  66. package/dist/src/commands/furnace/rename.js +59 -69
  67. package/dist/src/commands/furnace/scan.js +19 -33
  68. package/dist/src/commands/furnace/status.js +12 -9
  69. package/dist/src/commands/furnace/sync.js +9 -16
  70. package/dist/src/commands/furnace/validate.js +20 -20
  71. package/dist/src/commands/import.js +48 -37
  72. package/dist/src/commands/lint-per-patch.js +15 -10
  73. package/dist/src/commands/lint-per-run-checkjs.d.ts +13 -13
  74. package/dist/src/commands/lint-per-run-checkjs.js +13 -13
  75. package/dist/src/commands/lint-report.js +6 -7
  76. package/dist/src/commands/lint.d.ts +15 -16
  77. package/dist/src/commands/lint.js +86 -102
  78. package/dist/src/commands/package.js +6 -11
  79. package/dist/src/commands/patch/compact.js +1 -1
  80. package/dist/src/commands/patch/delete.js +24 -33
  81. package/dist/src/commands/patch/index.d.ts +5 -6
  82. package/dist/src/commands/patch/index.js +10 -12
  83. package/dist/src/commands/patch/lint-ignore.js +14 -13
  84. package/dist/src/commands/patch/move-files-create.d.ts +6 -6
  85. package/dist/src/commands/patch/move-files-create.js +8 -8
  86. package/dist/src/commands/patch/move-files-into.d.ts +6 -6
  87. package/dist/src/commands/patch/move-files-into.js +8 -8
  88. package/dist/src/commands/patch/patch-context.d.ts +2 -3
  89. package/dist/src/commands/patch/patch-context.js +2 -3
  90. package/dist/src/commands/patch/rename.d.ts +10 -11
  91. package/dist/src/commands/patch/rename.js +11 -12
  92. package/dist/src/commands/patch/reorder.js +8 -9
  93. package/dist/src/commands/patch/split-plan.js +1 -1
  94. package/dist/src/commands/patch/split.d.ts +8 -10
  95. package/dist/src/commands/patch/split.js +9 -11
  96. package/dist/src/commands/patch/staged-dependency-validate.js +8 -8
  97. package/dist/src/commands/patch/staged-dependency.js +7 -3
  98. package/dist/src/commands/patch/tier.d.ts +9 -9
  99. package/dist/src/commands/patch/tier.js +18 -15
  100. package/dist/src/commands/re-export-adjacent.js +9 -8
  101. package/dist/src/commands/re-export-bulk-scan.d.ts +9 -9
  102. package/dist/src/commands/re-export-bulk-scan.js +22 -21
  103. package/dist/src/commands/re-export-drift.js +10 -10
  104. package/dist/src/commands/re-export-files.d.ts +6 -7
  105. package/dist/src/commands/re-export-files.js +34 -44
  106. package/dist/src/commands/re-export-lint.d.ts +10 -13
  107. package/dist/src/commands/re-export-lint.js +16 -14
  108. package/dist/src/commands/re-export-options.d.ts +6 -6
  109. package/dist/src/commands/re-export-options.js +6 -6
  110. package/dist/src/commands/re-export-register.d.ts +4 -10
  111. package/dist/src/commands/re-export-register.js +5 -11
  112. package/dist/src/commands/re-export-scan.js +1 -1
  113. package/dist/src/commands/re-export-single.d.ts +3 -3
  114. package/dist/src/commands/re-export-single.js +33 -25
  115. package/dist/src/commands/re-export.js +13 -21
  116. package/dist/src/commands/rebase/abort.js +3 -3
  117. package/dist/src/commands/rebase/confirm.js +2 -1
  118. package/dist/src/commands/rebase/continue.d.ts +1 -1
  119. package/dist/src/commands/rebase/continue.js +14 -9
  120. package/dist/src/commands/rebase/index.js +11 -11
  121. package/dist/src/commands/rebase/patch-loop.d.ts +7 -1
  122. package/dist/src/commands/rebase/patch-loop.js +60 -39
  123. package/dist/src/commands/rebase/summary.js +1 -1
  124. package/dist/src/commands/register.js +5 -7
  125. package/dist/src/commands/reset.js +2 -1
  126. package/dist/src/commands/resolve.d.ts +16 -13
  127. package/dist/src/commands/resolve.js +30 -33
  128. package/dist/src/commands/run.js +33 -35
  129. package/dist/src/commands/setup-support.js +15 -19
  130. package/dist/src/commands/setup.js +13 -9
  131. package/dist/src/commands/status-check.d.ts +6 -6
  132. package/dist/src/commands/status-json.d.ts +6 -6
  133. package/dist/src/commands/status-json.js +5 -0
  134. package/dist/src/commands/status-output.d.ts +5 -5
  135. package/dist/src/commands/status-output.js +6 -6
  136. package/dist/src/commands/status-ownership.d.ts +16 -14
  137. package/dist/src/commands/status-ownership.js +16 -14
  138. package/dist/src/commands/status.js +42 -34
  139. package/dist/src/commands/test-build-phase.js +26 -13
  140. package/dist/src/commands/test-diagnose.js +44 -44
  141. package/dist/src/commands/test-register.js +1 -0
  142. package/dist/src/commands/test-run.d.ts +14 -16
  143. package/dist/src/commands/test-run.js +21 -13
  144. package/dist/src/commands/test-stale-gate.d.ts +15 -16
  145. package/dist/src/commands/test-stale-gate.js +15 -16
  146. package/dist/src/commands/test-verdict.d.ts +20 -3
  147. package/dist/src/commands/test-verdict.js +30 -0
  148. package/dist/src/commands/test.js +83 -76
  149. package/dist/src/commands/token-coverage.js +3 -4
  150. package/dist/src/commands/token.js +16 -18
  151. package/dist/src/commands/tree.js +47 -31
  152. package/dist/src/commands/typecheck.d.ts +10 -12
  153. package/dist/src/commands/typecheck.js +24 -27
  154. package/dist/src/commands/verify.js +13 -15
  155. package/dist/src/commands/watch.js +21 -23
  156. package/dist/src/commands/wire.js +52 -62
  157. package/dist/src/core/ast-utils.d.ts +8 -7
  158. package/dist/src/core/ast-utils.js +8 -7
  159. package/dist/src/core/branding.d.ts +9 -14
  160. package/dist/src/core/branding.js +7 -11
  161. package/dist/src/core/browser-wire.d.ts +1 -1
  162. package/dist/src/core/browser-wire.js +14 -19
  163. package/dist/src/core/build-audit-platform.js +25 -33
  164. package/dist/src/core/build-audit-registration.js +17 -25
  165. package/dist/src/core/build-audit-resolve.d.ts +15 -17
  166. package/dist/src/core/build-audit-resolve.js +39 -36
  167. package/dist/src/core/build-audit-transforms.js +18 -19
  168. package/dist/src/core/build-audit.js +28 -42
  169. package/dist/src/core/build-baseline-types.d.ts +29 -35
  170. package/dist/src/core/build-baseline.d.ts +5 -4
  171. package/dist/src/core/build-baseline.js +14 -14
  172. package/dist/src/core/build-overwrite-guard.d.ts +7 -14
  173. package/dist/src/core/build-overwrite-guard.js +17 -27
  174. package/dist/src/core/build-prepare.d.ts +2 -2
  175. package/dist/src/core/build-prepare.js +8 -9
  176. package/dist/src/core/config-mutate.js +20 -19
  177. package/dist/src/core/config-state.js +12 -4
  178. package/dist/src/core/config-validate.js +8 -5
  179. package/dist/src/core/config.d.ts +5 -8
  180. package/dist/src/core/config.js +7 -16
  181. package/dist/src/core/coverage-extend.d.ts +15 -13
  182. package/dist/src/core/coverage-extend.js +15 -13
  183. package/dist/src/core/destructive.d.ts +31 -9
  184. package/dist/src/core/destructive.js +29 -26
  185. package/dist/src/core/diff-hunks.js +10 -12
  186. package/dist/src/core/discard-baseline.d.ts +8 -8
  187. package/dist/src/core/discard-baseline.js +16 -15
  188. package/dist/src/core/display-state.d.ts +12 -14
  189. package/dist/src/core/display-state.js +12 -14
  190. package/dist/src/core/engine-changes.d.ts +2 -4
  191. package/dist/src/core/engine-changes.js +2 -4
  192. package/dist/src/core/engine-precondition.d.ts +28 -13
  193. package/dist/src/core/engine-precondition.js +25 -10
  194. package/dist/src/core/engine-session-lock.d.ts +47 -21
  195. package/dist/src/core/engine-session-lock.js +168 -33
  196. package/dist/src/core/export-moved-code-shape.d.ts +10 -10
  197. package/dist/src/core/export-moved-code-shape.js +10 -10
  198. package/dist/src/core/file-lock.d.ts +62 -5
  199. package/dist/src/core/file-lock.js +196 -59
  200. package/dist/src/core/firefox-cache.js +1 -3
  201. package/dist/src/core/firefox-extract.js +10 -14
  202. package/dist/src/core/firefox.d.ts +7 -5
  203. package/dist/src/core/firefox.js +4 -5
  204. package/dist/src/core/furnace-apply-dry-run.d.ts +2 -2
  205. package/dist/src/core/furnace-apply-dry-run.js +3 -2
  206. package/dist/src/core/furnace-apply-ftl.d.ts +7 -9
  207. package/dist/src/core/furnace-apply-ftl.js +16 -13
  208. package/dist/src/core/furnace-apply-helpers.d.ts +7 -7
  209. package/dist/src/core/furnace-apply-helpers.js +30 -21
  210. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +8 -9
  211. package/dist/src/core/furnace-apply-overwrite-warn.js +8 -9
  212. package/dist/src/core/furnace-apply-undeploy-actions.d.ts +3 -4
  213. package/dist/src/core/furnace-apply-undeploy-actions.js +3 -4
  214. package/dist/src/core/furnace-apply.d.ts +5 -8
  215. package/dist/src/core/furnace-apply.js +19 -24
  216. package/dist/src/core/furnace-checksum-utils.d.ts +14 -0
  217. package/dist/src/core/furnace-checksum-utils.js +16 -0
  218. package/dist/src/core/furnace-config.d.ts +21 -30
  219. package/dist/src/core/furnace-config.js +32 -54
  220. package/dist/src/core/furnace-constants.d.ts +8 -0
  221. package/dist/src/core/furnace-constants.js +10 -2
  222. package/dist/src/core/furnace-css-fragments.d.ts +5 -5
  223. package/dist/src/core/furnace-css-fragments.js +5 -5
  224. package/dist/src/core/furnace-jsconfig.d.ts +11 -12
  225. package/dist/src/core/furnace-jsconfig.js +11 -12
  226. package/dist/src/core/furnace-marker.d.ts +7 -8
  227. package/dist/src/core/furnace-marker.js +7 -8
  228. package/dist/src/core/furnace-operation.d.ts +31 -0
  229. package/dist/src/core/furnace-operation.js +54 -17
  230. package/dist/src/core/furnace-precondition.d.ts +61 -0
  231. package/dist/src/core/furnace-precondition.js +67 -0
  232. package/dist/src/core/furnace-registration-remove.d.ts +5 -6
  233. package/dist/src/core/furnace-registration-remove.js +6 -7
  234. package/dist/src/core/furnace-registration-validate.d.ts +13 -0
  235. package/dist/src/core/furnace-registration-validate.js +20 -1
  236. package/dist/src/core/furnace-registration.d.ts +5 -6
  237. package/dist/src/core/furnace-registration.js +10 -12
  238. package/dist/src/core/furnace-rollback.js +7 -7
  239. package/dist/src/core/furnace-scanner.d.ts +1 -0
  240. package/dist/src/core/furnace-scanner.js +1 -2
  241. package/dist/src/core/furnace-stale-export.d.ts +10 -10
  242. package/dist/src/core/furnace-stale-export.js +10 -10
  243. package/dist/src/core/furnace-state-persist.d.ts +6 -8
  244. package/dist/src/core/furnace-state-persist.js +6 -8
  245. package/dist/src/core/furnace-validate-accessibility.d.ts +22 -0
  246. package/dist/src/core/furnace-validate-accessibility.js +310 -1
  247. package/dist/src/core/furnace-validate-compatibility.js +141 -1
  248. package/dist/src/core/furnace-validate-helpers.d.ts +5 -116
  249. package/dist/src/core/furnace-validate-helpers.js +0 -491
  250. package/dist/src/core/furnace-validate-registration.d.ts +7 -0
  251. package/dist/src/core/furnace-validate-registration.js +115 -41
  252. package/dist/src/core/furnace-validate.d.ts +16 -0
  253. package/dist/src/core/furnace-validate.js +188 -145
  254. package/dist/src/core/furnace-version-drift.d.ts +10 -11
  255. package/dist/src/core/furnace-version-drift.js +10 -11
  256. package/dist/src/core/git-base.d.ts +21 -9
  257. package/dist/src/core/git-base.js +24 -9
  258. package/dist/src/core/git-diff.d.ts +20 -9
  259. package/dist/src/core/git-diff.js +148 -63
  260. package/dist/src/core/git-file-ops.d.ts +19 -19
  261. package/dist/src/core/git-file-ops.js +19 -19
  262. package/dist/src/core/git-readonly-index.d.ts +53 -19
  263. package/dist/src/core/git-readonly-index.js +75 -31
  264. package/dist/src/core/git-status.d.ts +5 -6
  265. package/dist/src/core/git-status.js +5 -6
  266. package/dist/src/core/git.d.ts +9 -10
  267. package/dist/src/core/git.js +57 -71
  268. package/dist/src/core/license-headers.d.ts +17 -23
  269. package/dist/src/core/license-headers.js +17 -23
  270. package/dist/src/core/mach-build-artifacts.d.ts +9 -9
  271. package/dist/src/core/mach-build-artifacts.js +57 -19
  272. package/dist/src/core/mach-error-hints.js +42 -29
  273. package/dist/src/core/mach-known-noise-filter.d.ts +15 -16
  274. package/dist/src/core/mach-known-noise-filter.js +18 -20
  275. package/dist/src/core/mach-mozconfig.d.ts +14 -0
  276. package/dist/src/core/mach-mozconfig.js +30 -0
  277. package/dist/src/core/mach-resource-shim.d.ts +43 -58
  278. package/dist/src/core/mach-resource-shim.js +50 -67
  279. package/dist/src/core/mach.d.ts +37 -41
  280. package/dist/src/core/mach.js +50 -51
  281. package/dist/src/core/marionette-port.d.ts +36 -1
  282. package/dist/src/core/marionette-port.js +96 -31
  283. package/dist/src/core/marionette-preflight.d.ts +8 -12
  284. package/dist/src/core/marionette-preflight.js +86 -52
  285. package/dist/src/core/mochitest-server-port.d.ts +42 -0
  286. package/dist/src/core/mochitest-server-port.js +119 -0
  287. package/dist/src/core/module-resolution-preflight.d.ts +9 -11
  288. package/dist/src/core/module-resolution-preflight.js +9 -11
  289. package/dist/src/core/moz-manifest-helpers.d.ts +79 -0
  290. package/dist/src/core/moz-manifest-helpers.js +163 -0
  291. package/dist/src/core/{manifest-register.d.ts → moz-manifest-register.d.ts} +1 -1
  292. package/dist/src/core/{manifest-register.js → moz-manifest-register.js} +13 -28
  293. package/dist/src/core/{manifest-rules.d.ts → moz-manifest-rules.d.ts} +10 -1
  294. package/dist/src/core/{manifest-rules.js → moz-manifest-rules.js} +31 -24
  295. package/dist/src/core/{manifest-tokenizers.d.ts → moz-manifest-tokenizers.d.ts} +3 -5
  296. package/dist/src/core/{manifest-tokenizers.js → moz-manifest-tokenizers.js} +9 -12
  297. package/dist/src/core/ownership-table.d.ts +14 -16
  298. package/dist/src/core/ownership-table.js +14 -16
  299. package/dist/src/core/parser-fallback.d.ts +15 -1
  300. package/dist/src/core/parser-fallback.js +22 -3
  301. package/dist/src/core/patch-apply-fuzz.d.ts +10 -12
  302. package/dist/src/core/patch-apply-fuzz.js +15 -18
  303. package/dist/src/core/patch-apply.d.ts +13 -34
  304. package/dist/src/core/patch-apply.js +16 -76
  305. package/dist/src/core/patch-artifact-normalize.d.ts +13 -15
  306. package/dist/src/core/patch-artifact-normalize.js +13 -15
  307. package/dist/src/core/patch-export-metadata.d.ts +12 -2
  308. package/dist/src/core/patch-export-metadata.js +13 -4
  309. package/dist/src/core/patch-export-update.d.ts +13 -1
  310. package/dist/src/core/patch-export-update.js +25 -8
  311. package/dist/src/core/patch-export.d.ts +15 -8
  312. package/dist/src/core/patch-export.js +10 -11
  313. package/dist/src/core/patch-files.d.ts +5 -6
  314. package/dist/src/core/patch-files.js +5 -6
  315. package/dist/src/core/patch-identifier-suggest.d.ts +7 -12
  316. package/dist/src/core/patch-identifier-suggest.js +14 -19
  317. package/dist/src/core/patch-lint-binary.d.ts +51 -0
  318. package/dist/src/core/patch-lint-binary.js +39 -0
  319. package/dist/src/core/{lint-cache.d.ts → patch-lint-cache.d.ts} +5 -5
  320. package/dist/src/core/{lint-cache.js → patch-lint-cache.js} +21 -23
  321. package/dist/src/core/patch-lint-checkjs.d.ts +16 -16
  322. package/dist/src/core/patch-lint-checkjs.js +41 -20
  323. package/dist/src/core/patch-lint-chrome-jsdoc.d.ts +6 -9
  324. package/dist/src/core/patch-lint-chrome-jsdoc.js +10 -14
  325. package/dist/src/core/patch-lint-creators.d.ts +8 -8
  326. package/dist/src/core/patch-lint-creators.js +8 -8
  327. package/dist/src/core/patch-lint-cross.d.ts +17 -20
  328. package/dist/src/core/patch-lint-cross.js +16 -16
  329. package/dist/src/core/patch-lint-css.d.ts +3 -5
  330. package/dist/src/core/patch-lint-css.js +17 -22
  331. package/dist/src/core/patch-lint-diff-tag.d.ts +14 -16
  332. package/dist/src/core/patch-lint-diff-tag.js +14 -16
  333. package/dist/src/core/patch-lint-jsdoc.d.ts +7 -8
  334. package/dist/src/core/patch-lint-jsdoc.js +26 -19
  335. package/dist/src/core/patch-lint-mozbuild.js +20 -20
  336. package/dist/src/core/patch-lint-observer.d.ts +7 -7
  337. package/dist/src/core/patch-lint-observer.js +7 -7
  338. package/dist/src/core/patch-lint.d.ts +38 -29
  339. package/dist/src/core/patch-lint.js +89 -121
  340. package/dist/src/core/patch-lock.d.ts +1 -1
  341. package/dist/src/core/patch-lock.js +45 -9
  342. package/dist/src/core/patch-manifest-consistency.d.ts +57 -6
  343. package/dist/src/core/patch-manifest-consistency.js +111 -40
  344. package/dist/src/core/patch-manifest-files-affected.d.ts +43 -0
  345. package/dist/src/core/patch-manifest-files-affected.js +77 -0
  346. package/dist/src/core/patch-manifest-io.js +14 -18
  347. package/dist/src/core/patch-manifest-resolve.d.ts +9 -12
  348. package/dist/src/core/patch-manifest-resolve.js +9 -12
  349. package/dist/src/core/patch-manifest-validate.d.ts +6 -1
  350. package/dist/src/core/patch-manifest-validate.js +25 -13
  351. package/dist/src/core/patch-manifest.d.ts +2 -1
  352. package/dist/src/core/patch-manifest.js +2 -1
  353. package/dist/src/core/patch-parse.d.ts +24 -13
  354. package/dist/src/core/patch-parse.js +19 -14
  355. package/dist/src/core/patch-policy.d.ts +2 -2
  356. package/dist/src/core/patch-policy.js +2 -2
  357. package/dist/src/core/patch-registration-refs.d.ts +7 -8
  358. package/dist/src/core/patch-registration-refs.js +9 -9
  359. package/dist/src/core/process-cpu-time.d.ts +12 -0
  360. package/dist/src/core/process-cpu-time.js +52 -0
  361. package/dist/src/core/rebase-session.d.ts +38 -26
  362. package/dist/src/core/rebase-session.js +55 -28
  363. package/dist/src/core/register-browser-content.js +9 -74
  364. package/dist/src/core/{register-test-manifest.js → register-browser-test.js} +7 -42
  365. package/dist/src/core/register-module.js +10 -43
  366. package/dist/src/core/register-scaffold.d.ts +8 -8
  367. package/dist/src/core/register-scaffold.js +10 -10
  368. package/dist/src/core/register-shared-css.d.ts +9 -12
  369. package/dist/src/core/register-shared-css.js +17 -87
  370. package/dist/src/core/register-xpcshell-test.d.ts +6 -7
  371. package/dist/src/core/register-xpcshell-test.js +7 -8
  372. package/dist/src/core/shared-ftl.js +12 -4
  373. package/dist/src/core/state-file.d.ts +45 -0
  374. package/dist/src/core/state-file.js +34 -0
  375. package/dist/src/core/status-binary.d.ts +18 -11
  376. package/dist/src/core/status-binary.js +19 -12
  377. package/dist/src/core/status-classify.d.ts +30 -36
  378. package/dist/src/core/status-classify.js +21 -26
  379. package/dist/src/core/test-changed-prefs.d.ts +30 -0
  380. package/dist/src/core/test-changed-prefs.js +78 -0
  381. package/dist/src/core/test-harness-crash.d.ts +25 -63
  382. package/dist/src/core/test-harness-crash.js +121 -142
  383. package/dist/src/core/test-harness-signature.d.ts +18 -0
  384. package/dist/src/core/test-harness-signature.js +13 -0
  385. package/dist/src/core/test-path-scope.d.ts +12 -15
  386. package/dist/src/core/test-path-scope.js +15 -18
  387. package/dist/src/core/test-stale-check.d.ts +17 -17
  388. package/dist/src/core/test-stale-check.js +50 -43
  389. package/dist/src/core/test-stall-triage.d.ts +51 -0
  390. package/dist/src/core/test-stall-triage.js +126 -0
  391. package/dist/src/core/token-category.js +66 -14
  392. package/dist/src/core/token-coverage.js +9 -9
  393. package/dist/src/core/token-dark-mode.d.ts +9 -11
  394. package/dist/src/core/token-dark-mode.js +29 -13
  395. package/dist/src/core/token-docs.d.ts +2 -0
  396. package/dist/src/core/token-docs.js +3 -1
  397. package/dist/src/core/token-manager.d.ts +10 -2
  398. package/dist/src/core/token-manager.js +17 -8
  399. package/dist/src/core/token-scaffold.d.ts +7 -7
  400. package/dist/src/core/token-scaffold.js +7 -7
  401. package/dist/src/core/token-variant.d.ts +4 -4
  402. package/dist/src/core/token-variant.js +28 -6
  403. package/dist/src/core/toolchain-preflight.d.ts +20 -21
  404. package/dist/src/core/toolchain-preflight.js +28 -31
  405. package/dist/src/core/tree-guard.js +12 -2
  406. package/dist/src/core/tree-store.d.ts +41 -27
  407. package/dist/src/core/tree-store.js +68 -43
  408. package/dist/src/core/typecheck-shim.d.ts +23 -24
  409. package/dist/src/core/typecheck-shim.js +23 -24
  410. package/dist/src/core/typecheck.js +15 -9
  411. package/dist/src/core/wire-destroy.d.ts +3 -3
  412. package/dist/src/core/wire-destroy.js +7 -15
  413. package/dist/src/core/wire-dom-fragment.d.ts +9 -17
  414. package/dist/src/core/wire-dom-fragment.js +11 -44
  415. package/dist/src/core/wire-init.d.ts +2 -3
  416. package/dist/src/core/wire-init.js +8 -19
  417. package/dist/src/core/wire-subscript.d.ts +2 -3
  418. package/dist/src/core/wire-subscript.js +4 -13
  419. package/dist/src/core/wire-utils.d.ts +7 -8
  420. package/dist/src/core/wire-utils.js +7 -8
  421. package/dist/src/core/xpcshell-appdir.d.ts +38 -45
  422. package/dist/src/core/xpcshell-appdir.js +38 -45
  423. package/dist/src/errors/base.d.ts +37 -16
  424. package/dist/src/errors/base.js +37 -16
  425. package/dist/src/errors/build.d.ts +13 -1
  426. package/dist/src/errors/build.js +14 -2
  427. package/dist/src/errors/codes.d.ts +16 -0
  428. package/dist/src/errors/codes.js +16 -0
  429. package/dist/src/errors/furnace.d.ts +4 -4
  430. package/dist/src/errors/git.d.ts +6 -10
  431. package/dist/src/errors/git.js +6 -10
  432. package/dist/src/errors/patch.d.ts +7 -7
  433. package/dist/src/errors/patch.js +7 -7
  434. package/dist/src/errors/rebase.d.ts +4 -4
  435. package/dist/src/errors/rebase.js +4 -4
  436. package/dist/src/index.d.ts +7 -1
  437. package/dist/src/index.js +5 -0
  438. package/dist/src/types/commands/index.d.ts +2 -2
  439. package/dist/src/types/commands/options.d.ts +111 -70
  440. package/dist/src/types/commands/patches.d.ts +24 -34
  441. package/dist/src/types/commands/project.d.ts +5 -34
  442. package/dist/src/types/config.d.ts +25 -23
  443. package/dist/src/types/config.js +6 -1
  444. package/dist/src/types/furnace.d.ts +37 -13
  445. package/dist/src/types/furnace.js +19 -1
  446. package/dist/src/types/index.d.ts +3 -3
  447. package/dist/src/utils/build-info.d.ts +7 -8
  448. package/dist/src/utils/build-info.js +12 -11
  449. package/dist/src/utils/errors.d.ts +6 -8
  450. package/dist/src/utils/errors.js +6 -8
  451. package/dist/src/utils/logger.d.ts +27 -10
  452. package/dist/src/utils/logger.js +28 -16
  453. package/dist/src/utils/machine-output.d.ts +12 -0
  454. package/dist/src/utils/machine-output.js +33 -0
  455. package/dist/src/utils/options.d.ts +31 -14
  456. package/dist/src/utils/options.js +86 -30
  457. package/dist/src/utils/paths.d.ts +10 -8
  458. package/dist/src/utils/paths.js +10 -8
  459. package/dist/src/utils/platform.d.ts +0 -25
  460. package/dist/src/utils/platform.js +1 -42
  461. package/dist/src/utils/process-group.d.ts +9 -4
  462. package/dist/src/utils/process-group.js +19 -5
  463. package/dist/src/utils/process.d.ts +42 -7
  464. package/dist/src/utils/process.js +44 -17
  465. package/dist/src/utils/ps-duration.d.ts +31 -0
  466. package/dist/src/utils/ps-duration.js +70 -0
  467. package/dist/src/utils/regex.d.ts +9 -10
  468. package/dist/src/utils/regex.js +9 -10
  469. package/dist/src/utils/validation.d.ts +58 -28
  470. package/dist/src/utils/validation.js +60 -33
  471. package/package.json +3 -3
  472. package/dist/src/commands/doctor/post-rebase-audit.d.ts +0 -2
  473. package/dist/src/commands/doctor-furnace-manifest-sync.d.ts +0 -18
  474. package/dist/src/core/manifest-helpers.d.ts +0 -38
  475. package/dist/src/core/manifest-helpers.js +0 -120
  476. /package/dist/src/core/{lint-projection.d.ts → patch-lint-projection.d.ts} +0 -0
  477. /package/dist/src/core/{lint-projection.js → patch-lint-projection.js} +0 -0
  478. /package/dist/src/core/{register-test-manifest.d.ts → register-browser-test.d.ts} +0 -0
@@ -5,21 +5,20 @@
5
5
  * Overrides are forks of Firefox source files taken at a specific point in
6
6
  * time. If Firefox moves forward and the override's `baseVersion` is not
7
7
  * refreshed, the override may apply against a file whose upstream shape has
8
- * changed — which is the single biggest silent failure mode for furnace.
8
+ * changed — the single biggest silent failure mode for furnace.
9
9
  *
10
- * This module is deliberately pure and string-only: it does no I/O and does
11
- * not parse Firefox version components. Comparing by string equality is
12
- * sufficient because `fireforge.json` stores a canonical version string
13
- * (e.g. `"140.9.0esr"`) and overrides are created with exactly that string
14
- * copied from `forgeConfig.firefox.version`. Any string mismatch is worth
15
- * surfacing — even "140.0" vs "140.9.0esr" is a real drift signal.
10
+ * Deliberately pure and string-only: no I/O, and no parsing of Firefox
11
+ * version components. String equality is sufficient because `fireforge.json`
12
+ * stores a canonical version string (e.g. `"140.9.0esr"`) and overrides are
13
+ * created with exactly that string copied from
14
+ * `forgeConfig.firefox.version`. Any mismatch is worth surfacing — even
15
+ * "140.0" vs "140.9.0esr" is a real drift signal.
16
16
  *
17
17
  * The result GATES apply/deploy: both warn and then FAIL without --force
18
18
  * (see furnaceApplyCommand / furnaceDeployCommand), and `furnace sync`
19
- * re-checks it after refresh before applying. Status reports drift
20
- * alongside the component overview. Keep this doc in sync with the gates —
21
- * an earlier version claimed the result was advisory, which invited
22
- * re-breaking the gate.
19
+ * re-checks it after refresh before applying. Status reports drift alongside
20
+ * the component overview. Keep this doc in sync with the gates — it is not
21
+ * advisory.
23
22
  */
24
23
  import type { FurnaceConfig } from '../types/furnace.js';
25
24
  /** Severity of the version drift between an override's base and the current Firefox version. */
@@ -6,21 +6,20 @@
6
6
  * Overrides are forks of Firefox source files taken at a specific point in
7
7
  * time. If Firefox moves forward and the override's `baseVersion` is not
8
8
  * refreshed, the override may apply against a file whose upstream shape has
9
- * changed — which is the single biggest silent failure mode for furnace.
9
+ * changed — the single biggest silent failure mode for furnace.
10
10
  *
11
- * This module is deliberately pure and string-only: it does no I/O and does
12
- * not parse Firefox version components. Comparing by string equality is
13
- * sufficient because `fireforge.json` stores a canonical version string
14
- * (e.g. `"140.9.0esr"`) and overrides are created with exactly that string
15
- * copied from `forgeConfig.firefox.version`. Any string mismatch is worth
16
- * surfacing — even "140.0" vs "140.9.0esr" is a real drift signal.
11
+ * Deliberately pure and string-only: no I/O, and no parsing of Firefox
12
+ * version components. String equality is sufficient because `fireforge.json`
13
+ * stores a canonical version string (e.g. `"140.9.0esr"`) and overrides are
14
+ * created with exactly that string copied from
15
+ * `forgeConfig.firefox.version`. Any mismatch is worth surfacing — even
16
+ * "140.0" vs "140.9.0esr" is a real drift signal.
17
17
  *
18
18
  * The result GATES apply/deploy: both warn and then FAIL without --force
19
19
  * (see furnaceApplyCommand / furnaceDeployCommand), and `furnace sync`
20
- * re-checks it after refresh before applying. Status reports drift
21
- * alongside the component overview. Keep this doc in sync with the gates —
22
- * an earlier version claimed the result was advisory, which invited
23
- * re-breaking the gate.
20
+ * re-checks it after refresh before applying. Status reports drift alongside
21
+ * the component overview. Keep this doc in sync with the gates — it is not
22
+ * advisory.
24
23
  */
25
24
  /**
26
25
  * Parses a version string into its major, minor, and patch numeric
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * Environment variable that overrides the monolithic `git add -A` timeout
3
- * (milliseconds). 2026-04-24 eval Finding 10: operators on slow or loaded
4
- * filesystems legitimately exceeded the 10-minute default during a
5
- * 140.10.0esr baseline indexing pass; making the cap overridable lets
6
- * them retry without recompiling.
3
+ * (milliseconds). Operators on slow or loaded filesystems can legitimately
4
+ * exceed the 10-minute default during a baseline indexing pass; making the
5
+ * cap overridable lets them retry without recompiling.
7
6
  */
8
7
  export declare const GIT_ADD_TIMEOUT_ENV_VAR = "FIREFORGE_GIT_ADD_TIMEOUT_MS";
9
8
  /**
@@ -20,11 +19,10 @@ export declare const GIT_ADD_CHUNK_TIMEOUT_ENV_VAR = "FIREFORGE_GIT_ADD_CHUNK_TI
20
19
  */
21
20
  export declare const GIT_ADD_TIMEOUT_MS: number;
22
21
  /**
23
- * Resolved timeout for each chunk of the chunked fallback path. Grew
24
- * from 20 to 30 minutes in 0.18.1 because the fallback is already the
25
- * last line of defence before aborting — erring on the side of "complete
26
- * the indexing" over "fail fast" matches the real-world recovery
27
- * workflow.
22
+ * Resolved timeout for each chunk of the chunked fallback path. Generous by
23
+ * design: the fallback is already the last line of defence before aborting,
24
+ * so erring toward "complete the indexing" over "fail fast" matches the
25
+ * real-world recovery workflow.
28
26
  */
29
27
  export declare const GIT_ADD_CHUNK_TIMEOUT_MS: number;
30
28
  /**
@@ -85,3 +83,17 @@ export declare function chunkPathspecs(paths: string[], budgetBytes?: number): s
85
83
  * repositories with hundreds of thousands of files.
86
84
  */
87
85
  export declare function configureGitPerformance(repoDir: string): Promise<void>;
86
+ /**
87
+ * Pins the engine repository's line-ending policy to "store and check out
88
+ * exactly what upstream shipped".
89
+ *
90
+ * Git's Windows installer defaults to a global `core.autocrlf=true`, which
91
+ * rewrites LF to CRLF on checkout and back on staging. FireForge cannot
92
+ * tolerate that: patch bodies are byte diffs of the working tree, and
93
+ * `hashObjectBatch` deliberately delegates to git so `.gitattributes` and
94
+ * `core.autocrlf` DO apply to the hashes `status` compares against. Under the
95
+ * global default the same tree hashes differently on Windows than everywhere
96
+ * else, so exported patches and drift detection disagree across machines.
97
+ * Setting it per-repository leaves the developer's global config alone.
98
+ */
99
+ export declare function configureGitLineEndings(repoDir: string): Promise<void>;
@@ -4,10 +4,9 @@ import { exec, executableExists } from '../utils/process.js';
4
4
  import { readOnlyGitIndexEnv } from './git-readonly-index.js';
5
5
  /**
6
6
  * Environment variable that overrides the monolithic `git add -A` timeout
7
- * (milliseconds). 2026-04-24 eval Finding 10: operators on slow or loaded
8
- * filesystems legitimately exceeded the 10-minute default during a
9
- * 140.10.0esr baseline indexing pass; making the cap overridable lets
10
- * them retry without recompiling.
7
+ * (milliseconds). Operators on slow or loaded filesystems can legitimately
8
+ * exceed the 10-minute default during a baseline indexing pass; making the
9
+ * cap overridable lets them retry without recompiling.
11
10
  */
12
11
  export const GIT_ADD_TIMEOUT_ENV_VAR = 'FIREFORGE_GIT_ADD_TIMEOUT_MS';
13
12
  /**
@@ -37,11 +36,10 @@ function resolveTimeoutFromEnv(envVar, fallbackMs) {
37
36
  */
38
37
  export const GIT_ADD_TIMEOUT_MS = resolveTimeoutFromEnv(GIT_ADD_TIMEOUT_ENV_VAR, DEFAULT_GIT_ADD_TIMEOUT_MS);
39
38
  /**
40
- * Resolved timeout for each chunk of the chunked fallback path. Grew
41
- * from 20 to 30 minutes in 0.18.1 because the fallback is already the
42
- * last line of defence before aborting — erring on the side of "complete
43
- * the indexing" over "fail fast" matches the real-world recovery
44
- * workflow.
39
+ * Resolved timeout for each chunk of the chunked fallback path. Generous by
40
+ * design: the fallback is already the last line of defence before aborting,
41
+ * so erring toward "complete the indexing" over "fail fast" matches the
42
+ * real-world recovery workflow.
45
43
  */
46
44
  export const GIT_ADD_CHUNK_TIMEOUT_MS = resolveTimeoutFromEnv(GIT_ADD_CHUNK_TIMEOUT_ENV_VAR, DEFAULT_GIT_ADD_CHUNK_TIMEOUT_MS);
47
45
  /**
@@ -122,3 +120,20 @@ export async function configureGitPerformance(repoDir) {
122
120
  await git(['config', 'core.fsmonitor', 'false'], repoDir);
123
121
  await git(['config', 'feature.manyFiles', 'true'], repoDir);
124
122
  }
123
+ /**
124
+ * Pins the engine repository's line-ending policy to "store and check out
125
+ * exactly what upstream shipped".
126
+ *
127
+ * Git's Windows installer defaults to a global `core.autocrlf=true`, which
128
+ * rewrites LF to CRLF on checkout and back on staging. FireForge cannot
129
+ * tolerate that: patch bodies are byte diffs of the working tree, and
130
+ * `hashObjectBatch` deliberately delegates to git so `.gitattributes` and
131
+ * `core.autocrlf` DO apply to the hashes `status` compares against. Under the
132
+ * global default the same tree hashes differently on Windows than everywhere
133
+ * else, so exported patches and drift detection disagree across machines.
134
+ * Setting it per-repository leaves the developer's global config alone.
135
+ */
136
+ export async function configureGitLineEndings(repoDir) {
137
+ await git(['config', 'core.autocrlf', 'false'], repoDir);
138
+ await git(['config', 'core.eol', 'lf'], repoDir);
139
+ }
@@ -1,5 +1,11 @@
1
1
  /**
2
2
  * Gets the diff for a specific file.
3
+ *
4
+ * `--binary` is load-bearing: without it git degrades a binary file's section
5
+ * to the informational `Binary files a/x and b/x differ`, which carries none
6
+ * of the bytes and cannot be replayed by `git apply`. It only expands the
7
+ * BINARY section's index line to full hashes (text sections keep their
8
+ * abbreviated form), so text output stays byte-identical.
3
9
  * @param repoDir - Repository directory
4
10
  * @param filePath - Path to the file (relative to repo)
5
11
  * @returns Diff content
@@ -32,22 +38,27 @@ export declare function generateFullFilePatch(repoDir: string, filePath: string)
32
38
  export declare function generateModificationDiff(repoDir: string, filePath: string, baseContent: string): Promise<string>;
33
39
  /**
34
40
  * Gets the diff for all modified files, including untracked (new) files.
41
+ *
42
+ * Tracked binary files round-trip through `git diff --binary`; untracked ones
43
+ * go through {@link generateBinaryFilePatch} below. Both arms must stay
44
+ * binary-aware — see {@link getFileDiff} for why.
35
45
  * @param repoDir - Repository directory
36
46
  * @returns Diff content
37
47
  */
38
48
  export declare function getAllDiff(repoDir: string): Promise<string>;
39
49
  /**
40
50
  * Builds a combined diff against HEAD for the provided files without touching
41
- * the real git index. Tracked files use `git diff HEAD`; untracked files use
42
- * synthesized new-file diffs.
51
+ * the real git index. Tracked files use `git diff --binary HEAD` (binary
52
+ * sections included); untracked files use synthesized new-file diffs, or
53
+ * {@link generateBinaryFilePatch} when the file is binary.
54
+ *
55
+ * Performance: the work is batched into a handful of `git` invocations (one
56
+ * `ls-tree` to classify, one `diff` over all tracked files, one `hash-object`
57
+ * over all new text files) rather than ~2 spawns per file, which dominates
58
+ * the cold-run cost on a Firefox-sized checkout. Binary, directory, and
59
+ * recursion paths stay per-file because they are rare and (for binary)
60
+ * mutate the index.
43
61
  *
44
- * Performance: the work is batched into a handful of `git` invocations
45
- * (one `ls-tree` to classify, one `diff` over all tracked files, one
46
- * `hash-object` over all new text files) rather than the ~2 spawns per file the
47
- * previous per-file loop issued — that fan-out dominated the cold-run cost on a
48
- * Firefox-sized checkout (~700 serial spawns, ~99s). Binary, directory, and
49
- * recursion paths stay per-file because they are rare and (for binary) mutate
50
- * the index.
51
62
  * @param repoDir - Repository directory
52
63
  * @param files - File paths to diff (relative to repo root)
53
64
  * @returns Combined diff content
@@ -9,17 +9,24 @@ import { verbose } from '../utils/logger.js';
9
9
  import { exec } from '../utils/process.js';
10
10
  import { chunkPathspecs, ensureGit, git } from './git-base.js';
11
11
  import { fileExistsInHead, hashObjectBatch, isBinaryFile, listTrackedInHead, } from './git-file-ops.js';
12
- import { readOnlyGitIndexEnv } from './git-readonly-index.js';
12
+ import { mintDisposableGitIndex, readOnlyGitIndexEnv } from './git-readonly-index.js';
13
13
  import { getUntrackedFiles, getUntrackedFilesInDir } from './git-status.js';
14
- async function execGitWithAllowedExitCodes(repoDir, args, allowedExitCodes = [0]) {
14
+ async function execGitWithAllowedExitCodes(repoDir, args, allowedExitCodes = [0], envOverride) {
15
15
  // Honour a read-only command's private-index scope: the `--intent-to-add`
16
16
  // / `update-index` staging dance below is a genuine
17
17
  // index write, and inside a read-only command it must land on the
18
18
  // private index, not the primary checkout's.
19
+ //
20
+ // `envOverride` wins over that scope: a caller that minted an index for
21
+ // one specific invocation has said something more specific than the
22
+ // ambient scope did.
19
23
  const readOnlyIndexEnv = readOnlyGitIndexEnv(repoDir);
24
+ const env = readOnlyIndexEnv !== undefined || envOverride !== undefined
25
+ ? { ...readOnlyIndexEnv, ...envOverride }
26
+ : undefined;
20
27
  const result = await exec('git', args, {
21
28
  cwd: repoDir,
22
- ...(readOnlyIndexEnv !== undefined ? { env: readOnlyIndexEnv } : {}),
29
+ ...(env !== undefined ? { env } : {}),
23
30
  });
24
31
  if (allowedExitCodes.includes(result.exitCode)) {
25
32
  return result;
@@ -28,13 +35,19 @@ async function execGitWithAllowedExitCodes(repoDir, args, allowedExitCodes = [0]
28
35
  }
29
36
  /**
30
37
  * Gets the diff for a specific file.
38
+ *
39
+ * `--binary` is load-bearing: without it git degrades a binary file's section
40
+ * to the informational `Binary files a/x and b/x differ`, which carries none
41
+ * of the bytes and cannot be replayed by `git apply`. It only expands the
42
+ * BINARY section's index line to full hashes (text sections keep their
43
+ * abbreviated form), so text output stays byte-identical.
31
44
  * @param repoDir - Repository directory
32
45
  * @param filePath - Path to the file (relative to repo)
33
46
  * @returns Diff content
34
47
  */
35
48
  export async function getFileDiff(repoDir, filePath) {
36
49
  await ensureGit();
37
- return git(['diff', 'HEAD', '--', filePath], repoDir);
50
+ return git(['diff', '--binary', 'HEAD', '--', filePath], repoDir);
38
51
  }
39
52
  /**
40
53
  * Abbreviates a full git blob hash to the 10-character form used in the
@@ -51,13 +64,14 @@ function abbreviateBlobHash(fullHash) {
51
64
  return '0000000000';
52
65
  }
53
66
  /**
54
- * Pure formatter for a new (untracked) file's unified diff. Extracted from
55
- * {@link generateNewFileDiff} so the batched cold-run path in
56
- * {@link getDiffForFilesAgainstHead} and the standalone path share one source of
57
- * truth — the only thing that differs between them is where `blobHash` comes
58
- * from (a single batched `git hash-object` vs a per-file one), never the
59
- * formatting. Preserves the empty-file form, the trailing-newline handling, and
60
- * the "No newline at end of file" marker byte-for-byte.
67
+ * Pure formatter for a new (untracked) file's unified diff. Shared by the
68
+ * batched cold-run path in {@link getDiffForFilesAgainstHead} and the
69
+ * standalone {@link generateNewFileDiff} — the only thing that differs is
70
+ * where `blobHash` comes from (a single batched `git hash-object` vs a
71
+ * per-file one), never the formatting. Preserves the empty-file form, the
72
+ * trailing-newline handling, and the "No newline at end of file" marker
73
+ * byte-for-byte.
74
+ *
61
75
  * @param filePath - Path to the file (relative to repo)
62
76
  * @param content - File content
63
77
  * @param blobHash - Abbreviated blob hash for the index line
@@ -106,12 +120,10 @@ function buildNewFileDiffBody(filePath, content, blobHash) {
106
120
  */
107
121
  export async function generateNewFileDiff(repoDir, filePath) {
108
122
  const fullPath = join(repoDir, filePath);
109
- // Defensive check: a directory here means a caller bypassed the
110
- // expansion layers and handed the leaf reader a path it cannot
111
- // read. Surface it with an actionable message naming the offending
112
- // path rather than the raw `EISDIR` that `readText` would throw —
113
- // recurring bug class (see the belt-and-suspenders note in
114
- // `getDiffForFilesAgainstHead`).
123
+ // Defensive check: a directory here means a caller bypassed the expansion
124
+ // layers and handed the leaf reader a path it cannot read. Surface it with
125
+ // an actionable message naming the offending path rather than the raw
126
+ // `EISDIR` that `readText` would throw.
115
127
  const fileStat = await stat(fullPath);
116
128
  if (fileStat.isDirectory()) {
117
129
  throw new GitError(`expected a file but found a directory at '${filePath}' — caller must expand directory entries before diffing`, `hash-object ${filePath}`);
@@ -199,22 +211,65 @@ export async function generateModificationDiff(repoDir, filePath, baseContent) {
199
211
  }
200
212
  /**
201
213
  * Gets the diff for all modified files, including untracked (new) files.
214
+ *
215
+ * Tracked binary files round-trip through `git diff --binary`; untracked ones
216
+ * go through {@link generateBinaryFilePatch} below. Both arms must stay
217
+ * binary-aware — see {@link getFileDiff} for why.
202
218
  * @param repoDir - Repository directory
203
219
  * @returns Diff content
204
220
  */
205
221
  export async function getAllDiff(repoDir) {
206
222
  await ensureGit();
207
223
  // Get diff for tracked files
208
- const trackedDiff = await git(['diff', 'HEAD'], repoDir);
224
+ const trackedDiff = await git(['diff', '--binary', 'HEAD'], repoDir);
209
225
  // Get untracked files (properly expanded, not directories)
210
226
  const untrackedFiles = await getUntrackedFiles(repoDir);
211
- // Generate diffs for untracked files
227
+ // Classify first. `isBinaryFile` reads the file's first 8 KB directly and
228
+ // spawns nothing, so the only per-file process in this loop is the
229
+ // `git hash-object` inside `generateNewFileDiff` — one spawn per untracked
230
+ // TEXT file, uncapped, against a Firefox tree where "untracked" can mean a
231
+ // very large number. Hence classify → one batched hash → render.
232
+ //
233
+ // Everything else here is deliberately untouched: `git diff HEAD` (with
234
+ // renames, unlike the `--no-renames` scoped sibling), the `ls-files`
235
+ // ordering, tracked-block-first emission, and the `'\n'` empty-result
236
+ // sentinel are all observable. `getAllDiff`'s output is SHA-256'd into
237
+ // every tree fingerprint (tree-store.ts) and written verbatim into `.patch`
238
+ // files (export-all.ts), so its bytes are a contract.
239
+ const binaryFiles = [];
240
+ const textFiles = [];
241
+ for (const file of untrackedFiles) {
242
+ if (await isBinaryFile(repoDir, file)) {
243
+ binaryFiles.push(file);
244
+ }
245
+ else {
246
+ textFiles.push(file);
247
+ }
248
+ }
249
+ const textHashes = await hashObjectBatch(repoDir, textFiles.map((file) => join(repoDir, file)));
250
+ // Binary patches stage into the index and must stay serial; text patches
251
+ // are pure formatting over the already-batched hashes.
252
+ const binaryDiffs = new Map();
253
+ for (const file of binaryFiles) {
254
+ binaryDiffs.set(file, await generateBinaryFilePatch(repoDir, file));
255
+ }
212
256
  const untrackedDiffs = [];
213
257
  for (const file of untrackedFiles) {
214
- const diff = (await isBinaryFile(repoDir, file))
215
- ? await generateBinaryFilePatch(repoDir, file)
216
- : await generateNewFileDiff(repoDir, file);
217
- untrackedDiffs.push(diff);
258
+ const binary = binaryDiffs.get(file);
259
+ if (binary !== undefined) {
260
+ untrackedDiffs.push(binary);
261
+ continue;
262
+ }
263
+ const fullPath = join(repoDir, file);
264
+ // Same defensive directory check `generateNewFileDiff` carries: a
265
+ // directory here means a caller bypassed the expansion layers, and its
266
+ // comment records this as a recurring bug class.
267
+ const fileStat = await stat(fullPath);
268
+ if (fileStat.isDirectory()) {
269
+ throw new GitError(`expected a file but found a directory at '${file}' — caller must expand directory entries before diffing`, `hash-object ${file}`);
270
+ }
271
+ const content = await readText(fullPath);
272
+ untrackedDiffs.push(buildNewFileDiffBody(file, content, abbreviateBlobHash(textHashes.get(fullPath))));
218
273
  }
219
274
  // Combine all diffs — each already ends with \n, so concatenate directly
220
275
  // to avoid inserting blank lines between diff sections.
@@ -249,9 +304,15 @@ function splitDiffSections(combined) {
249
304
  return sections;
250
305
  }
251
306
  /**
252
- * Runs one `git diff --no-renames HEAD` over the tracked files (chunked under
253
- * ARG_MAX) and returns a `Map<path, section>` whose sections are byte-identical
254
- * to the per-file `git diff HEAD -- <file>` they replace.
307
+ * Runs one `git diff --binary --no-renames HEAD` over the tracked files
308
+ * (chunked under ARG_MAX) and returns a `Map<path, section>` whose sections are
309
+ * byte-identical to the per-file `git diff --binary HEAD -- <file>` they
310
+ * replace.
311
+ *
312
+ * `--binary` is load-bearing: a tracked binary file would otherwise degrade to
313
+ * the un-appliable `Binary files a/x and b/x differ` stub. It expands only the
314
+ * binary section's index line to full hashes, so text sections are unaffected
315
+ * — see {@link getFileDiff}.
255
316
  *
256
317
  * `--no-renames` is load-bearing: a multi-path diff under a user's
257
318
  * `diff.renames=true`/`=copies` could otherwise emit a single 2-path rename
@@ -274,7 +335,7 @@ function splitDiffSections(combined) {
274
335
  async function buildTrackedSections(repoDir, trackedFiles) {
275
336
  const sectionsByPath = new Map();
276
337
  for (const chunk of chunkPathspecs(trackedFiles)) {
277
- const combined = await git(['diff', '--no-renames', 'HEAD', '--', ...chunk], repoDir);
338
+ const combined = await git(['diff', '--binary', '--no-renames', 'HEAD', '--', ...chunk], repoDir);
278
339
  const namesOutput = await git(['diff', '--no-renames', 'HEAD', '-z', '--name-only', '--', ...chunk], repoDir);
279
340
  const names = namesOutput.split('\0').filter((name) => name.length > 0);
280
341
  const sections = splitDiffSections(combined);
@@ -299,16 +360,17 @@ async function buildTrackedSections(repoDir, trackedFiles) {
299
360
  }
300
361
  /**
301
362
  * Builds a combined diff against HEAD for the provided files without touching
302
- * the real git index. Tracked files use `git diff HEAD`; untracked files use
303
- * synthesized new-file diffs.
363
+ * the real git index. Tracked files use `git diff --binary HEAD` (binary
364
+ * sections included); untracked files use synthesized new-file diffs, or
365
+ * {@link generateBinaryFilePatch} when the file is binary.
366
+ *
367
+ * Performance: the work is batched into a handful of `git` invocations (one
368
+ * `ls-tree` to classify, one `diff` over all tracked files, one `hash-object`
369
+ * over all new text files) rather than ~2 spawns per file, which dominates
370
+ * the cold-run cost on a Firefox-sized checkout. Binary, directory, and
371
+ * recursion paths stay per-file because they are rare and (for binary)
372
+ * mutate the index.
304
373
  *
305
- * Performance: the work is batched into a handful of `git` invocations
306
- * (one `ls-tree` to classify, one `diff` over all tracked files, one
307
- * `hash-object` over all new text files) rather than the ~2 spawns per file the
308
- * previous per-file loop issued — that fan-out dominated the cold-run cost on a
309
- * Firefox-sized checkout (~700 serial spawns, ~99s). Binary, directory, and
310
- * recursion paths stay per-file because they are rare and (for binary) mutate
311
- * the index.
312
374
  * @param repoDir - Repository directory
313
375
  * @param files - File paths to diff (relative to repo root)
314
376
  * @returns Combined diff content
@@ -318,12 +380,10 @@ export async function getDiffForFilesAgainstHead(repoDir, files) {
318
380
  // Expand any directory entries (paths ending with `/`) into their
319
381
  // individual untracked files before diffing. `git status --porcelain=v1`
320
382
  // reports collapsed untracked directories as `?? dir/`, and every caller
321
- // that feeds the aggregate working-tree state into this function must
322
- // not trigger an EISDIR when the diff pass reads `dir/` as if it were a
323
- // file. Belt-and-suspenders: the caller-side expansion in `lint.ts`
324
- // and `export-all.ts` covers the common path, but a single bad call
325
- // site re-introduced the bug in 0.17.0 — guarding here makes the
326
- // regression impossible at this layer.
383
+ // that feeds the aggregate working-tree state into this function must not
384
+ // trigger an EISDIR when the diff pass reads `dir/` as if it were a file.
385
+ // The caller-side expansion in `lint.ts` and `export-all.ts` covers the
386
+ // common path; guarding here makes a single bad call site harmless.
327
387
  const expandedFiles = [];
328
388
  for (const file of files) {
329
389
  if (file.endsWith('/')) {
@@ -357,15 +417,13 @@ export async function getDiffForFilesAgainstHead(repoDir, files) {
357
417
  if (!(await pathExists(fullPath))) {
358
418
  continue;
359
419
  }
360
- // Second defence against the EISDIR regression: a non-HEAD path
361
- // that exists on disk is usually a new file, but can also be a
362
- // directory that arrived without the trailing slash
363
- // `expandUntrackedDirectoryEntries` would have produced (caller
364
- // stripped it, submodule entry, tracked-file-replaced-by-dir).
365
- // Expand it via the same helper used by the slash branch and
366
- // recurse so each contained file is diffed individually; fail
367
- // loud when the directory has no readable content rather than
368
- // silently skipping it.
420
+ // Second defence against EISDIR: a non-HEAD path that exists on disk is
421
+ // usually a new file, but can also be a directory that arrived without
422
+ // the trailing slash `expandUntrackedDirectoryEntries` would have
423
+ // produced (caller stripped it, submodule entry, tracked-file-replaced-
424
+ // by-dir). Expand it via the same helper used by the slash branch and
425
+ // recurse so each contained file is diffed individually; fail loud when
426
+ // the directory has no readable content rather than silently skipping it.
369
427
  const fileStat = await stat(fullPath);
370
428
  if (fileStat.isDirectory()) {
371
429
  const innerFiles = await getUntrackedFilesInDir(repoDir, file);
@@ -427,7 +485,7 @@ export async function getDiffForFilesAgainstHead(repoDir, files) {
427
485
  */
428
486
  export async function getStagedDiffForFiles(repoDir, files) {
429
487
  await ensureGit();
430
- return git(['diff', '--cached', 'HEAD', '--', ...files], repoDir);
488
+ return git(['diff', '--binary', '--cached', 'HEAD', '--', ...files], repoDir);
431
489
  }
432
490
  /**
433
491
  * Serializes the index-mutating untracked-binary path. The bounded per-patch
@@ -465,18 +523,45 @@ export async function generateBinaryFilePatch(repoDir, filePath) {
465
523
  ]);
466
524
  if (result.stdout.trim())
467
525
  return result.stdout;
468
- // For untracked files, stage temporarily to produce a binary diff. The
469
- // stage/unstage pair mutates the index, so it must not interleave with
470
- // another concurrent binary patch (see runWithBinaryStagingLock).
526
+ // The diff for an untracked file has to come from an index entry, so one
527
+ // has to be written. Writing it to a PRIVATE index is what keeps that
528
+ // write invisible: the primary `.git/index` never moves, so a concurrent
529
+ // `fireforge test` fingerprinting `engine/` with `git status` cannot
530
+ // observe the transient `--intent-to-add` entry and void its own verdict
531
+ // on our bookkeeping. That was a real failure mode — parallel gate lanes
532
+ // killed three runs with `FAIL reason=inconclusive` on branding PNGs
533
+ // flapping between `A` and `??`.
534
+ //
535
+ // Nothing is restored afterwards because nothing shared was changed, and
536
+ // no lock is taken because two concurrent callers get two indexes. Both
537
+ // the restore dance and the in-process lock survive below purely for the
538
+ // fallback, where the primary index really is the one being written.
539
+ const disposable = await mintDisposableGitIndex(repoDir);
540
+ if (disposable !== undefined) {
541
+ try {
542
+ await execGitWithAllowedExitCodes(repoDir, ['add', '--intent-to-add', '--', filePath], [0], disposable.env);
543
+ const diffResult = await execGitWithAllowedExitCodes(repoDir, ['diff', '--binary', '--', filePath], [0], disposable.env);
544
+ return diffResult.stdout;
545
+ }
546
+ finally {
547
+ await disposable.dispose();
548
+ }
549
+ }
550
+ // Fallback: no private index could be minted (a non-git directory, an
551
+ // unreadable index, no writable tmpdir). Degrading to the shared index is
552
+ // the pre-existing behaviour rather than a new risk, and refusing instead
553
+ // would fail an export over an optimisation. The stage/unstage pair
554
+ // mutates the index, so it must not interleave with another concurrent
555
+ // binary patch (see runWithBinaryStagingLock).
556
+ verbose(`Binary patch for ${filePath} is staging on the shared index (no private index).`);
471
557
  return runWithBinaryStagingLock(async () => {
472
- // Snapshot the path's exact index entry before touching it. The cleanup
473
- // used to be a blanket `git reset HEAD -- <file>`, which restores the
474
- // path to its HEAD state — for a path absent from HEAD that means
475
- // *removing* whatever entry the index carried, so any staged state that
476
- // arrived between the read-only diff above and this staging block (a
477
- // concurrent writer, or a staged-add whose worktree file was deleted)
478
- // was silently discarded. Restoring the captured entry
479
- // instead makes the cleanup exact regardless of how the entry got there.
558
+ // Snapshot the path's exact index entry before touching it. A blanket
559
+ // `git reset HEAD -- <file>` cleanup restores the path to its HEAD state
560
+ // — for a path absent from HEAD that means *removing* whatever entry the
561
+ // index carried, silently discarding any staged state that arrived
562
+ // between the read-only diff above and this staging block (a concurrent
563
+ // writer, or a staged-add whose worktree file was deleted). Restoring
564
+ // the captured entry is exact regardless of how the entry got there.
480
565
  const priorEntry = (await execGitWithAllowedExitCodes(repoDir, ['ls-files', '--stage', '--', filePath])).stdout;
481
566
  try {
482
567
  await execGitWithAllowedExitCodes(repoDir, ['add', '--intent-to-add', '--', filePath]);
@@ -31,17 +31,17 @@ export declare function unstageFiles(repoDir: string, files: string[]): Promise<
31
31
  */
32
32
  export declare function fileExistsInHead(repoDir: string, filePath: string): Promise<boolean>;
33
33
  /**
34
- * Batched equivalent of {@link fileExistsInHead}: returns the subset of `files`
35
- * that are tracked in HEAD, using a single `git ls-tree` per ARG_MAX chunk
36
- * instead of one spawn per file. This is the cold-run hot path — a Firefox-sized
37
- * checkout has hundreds of affected files, and the old per-file fan-out spent
38
- * ~99s in serial `git ls-tree`/`git diff` spawns.
34
+ * Batched equivalent of {@link fileExistsInHead}: returns the subset of
35
+ * `files` that are tracked in HEAD, using a single `git ls-tree` per ARG_MAX
36
+ * chunk instead of one spawn per file. This is the cold-run hot path — a
37
+ * Firefox-sized checkout has hundreds of affected files.
38
+ *
39
+ * `-r` lists nested blobs by full repo-relative path; `--name-only -z` makes
40
+ * the output a trivial NUL-split with no quoting to undo. Membership in the
41
+ * returned Set is exactly `await fileExistsInHead(repoDir, file)` for any
42
+ * non-directory `file`. Throws (via {@link git}) when HEAD itself is
43
+ * unresolvable, matching the per-file helper's failure mode.
39
44
  *
40
- * `-r` lists nested blobs by full repo-relative path; `--name-only -z` makes the
41
- * output a trivial NUL-split with no quoting to undo. Membership in the returned
42
- * Set is exactly `await fileExistsInHead(repoDir, file)` for any non-directory
43
- * `file`. Throws (via {@link git}) when HEAD itself is unresolvable, matching the
44
- * per-file helper's failure mode.
45
45
  * @param repoDir - Repository directory
46
46
  * @param files - Repo-relative paths to classify
47
47
  * @returns The subset of `files` present in HEAD
@@ -55,15 +55,15 @@ export declare function listTrackedInHead(repoDir: string, files: string[]): Pro
55
55
  *
56
56
  * Uses {@link import('../utils/process.js').exec} rather than {@link git}
57
57
  * (which throws on a non-zero exit) because `git hash-object f1 f2 …` is
58
- * all-or-nothing: it aborts at the first unreadable path and emits nothing for
59
- * the rest. To preserve the old per-file contract — where one bad path zeroed
60
- * only its own index line — a chunk that does not return exactly one hash per
61
- * input falls back to hashing that chunk's paths individually. A path that is
62
- * still unhashable is simply left out of the map; the caller applies the
63
- * `0000000000` zero-hash fallback (and the same verbose log) for any miss, so
64
- * the blob hash is byte-identical to `git hash-object` (filters/.gitattributes
65
- * are applied per path) without the risk of in-process hashing, which would
66
- * diverge under `core.autocrlf`/`text` attributes.
58
+ * all-or-nothing: it aborts at the first unreadable path and emits nothing
59
+ * for the rest. To keep the per-file contract — where one bad path zeroes
60
+ * only its own index line — a chunk that does not return exactly one hash
61
+ * per input falls back to hashing that chunk's paths individually. A path
62
+ * that is still unhashable is left out of the map; the caller applies the
63
+ * `0000000000` zero-hash fallback for any miss. Hashing stays in git rather
64
+ * than in-process so filters and `.gitattributes` are applied per path and
65
+ * the result cannot diverge under `core.autocrlf`/`text` attributes.
66
+ *
67
67
  * @param repoDir - Repository directory
68
68
  * @param fullPaths - Absolute file paths to hash
69
69
  * @returns Map from each input path to its full blob hash (misses omitted)