@hominis/fireforge 0.44.4 → 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 (378) hide show
  1. package/CHANGELOG.md +120 -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-source-pin.d.ts +29 -0
  18. package/dist/src/commands/doctor-source-pin.js +81 -0
  19. package/dist/src/commands/doctor.d.ts +2 -0
  20. package/dist/src/commands/doctor.js +9 -8
  21. package/dist/src/commands/download.js +35 -24
  22. package/dist/src/commands/export-all.js +21 -4
  23. package/dist/src/commands/export-flow.js +15 -60
  24. package/dist/src/commands/export-placement-conflicts.js +3 -3
  25. package/dist/src/commands/export-placement-gate.js +2 -8
  26. package/dist/src/commands/export-placement-policy.js +3 -5
  27. package/dist/src/commands/export-shared.d.ts +42 -23
  28. package/dist/src/commands/export-shared.js +4 -19
  29. package/dist/src/commands/export.js +10 -1
  30. package/dist/src/commands/furnace/authoring-config.d.ts +13 -0
  31. package/dist/src/commands/furnace/authoring-config.js +19 -0
  32. package/dist/src/commands/furnace/chrome-doc-remove.js +1 -1
  33. package/dist/src/commands/furnace/create-browser-test.js +3 -8
  34. package/dist/src/commands/furnace/create-dry-run.js +2 -5
  35. package/dist/src/commands/furnace/create-templates.d.ts +17 -1
  36. package/dist/src/commands/furnace/create-templates.js +2 -1
  37. package/dist/src/commands/furnace/create.js +25 -19
  38. package/dist/src/commands/furnace/deploy.js +11 -2
  39. package/dist/src/commands/furnace/init.js +2 -1
  40. package/dist/src/commands/furnace/override.js +28 -19
  41. package/dist/src/commands/furnace/refresh.js +9 -1
  42. package/dist/src/commands/furnace/remove-mutations.d.ts +6 -7
  43. package/dist/src/commands/furnace/remove-mutations.js +0 -1
  44. package/dist/src/commands/furnace/remove.js +2 -6
  45. package/dist/src/commands/furnace/rename-browser-test.js +1 -7
  46. package/dist/src/commands/furnace/rename.js +29 -29
  47. package/dist/src/commands/furnace/status.js +12 -9
  48. package/dist/src/commands/furnace/test-file-name.d.ts +28 -0
  49. package/dist/src/commands/furnace/test-file-name.js +39 -0
  50. package/dist/src/commands/furnace/validation-output.d.ts +21 -9
  51. package/dist/src/commands/furnace/validation-output.js +3 -8
  52. package/dist/src/commands/import.js +2 -9
  53. package/dist/src/commands/lint-per-patch.d.ts +2 -2
  54. package/dist/src/commands/lint-per-patch.js +19 -4
  55. package/dist/src/commands/lint-per-run-checkjs.d.ts +2 -2
  56. package/dist/src/commands/lint.d.ts +2 -1
  57. package/dist/src/commands/lint.js +1 -1
  58. package/dist/src/commands/manifest.js +1 -1
  59. package/dist/src/commands/package.js +2 -4
  60. package/dist/src/commands/patch/compact.js +14 -15
  61. package/dist/src/commands/patch/delete.js +15 -25
  62. package/dist/src/commands/patch/entry-projection.d.ts +13 -0
  63. package/dist/src/commands/patch/entry-projection.js +24 -0
  64. package/dist/src/commands/patch/lint-ignore.d.ts +0 -8
  65. package/dist/src/commands/patch/lint-ignore.js +19 -29
  66. package/dist/src/commands/patch/move-files-create.js +24 -10
  67. package/dist/src/commands/patch/move-files-into.js +38 -37
  68. package/dist/src/commands/patch/move-files.js +4 -13
  69. package/dist/src/commands/patch/rename.js +27 -41
  70. package/dist/src/commands/patch/reorder.js +25 -38
  71. package/dist/src/commands/patch/split-plan.d.ts +4 -5
  72. package/dist/src/commands/patch/split-plan.js +4 -11
  73. package/dist/src/commands/patch/split.js +45 -43
  74. package/dist/src/commands/patch/staged-dependency.js +16 -22
  75. package/dist/src/commands/patch/tier.js +15 -25
  76. package/dist/src/commands/patch-rename-map.d.ts +19 -0
  77. package/dist/src/commands/patch-rename-map.js +36 -0
  78. package/dist/src/commands/pending-resolution.d.ts +15 -0
  79. package/dist/src/commands/pending-resolution.js +25 -0
  80. package/dist/src/commands/re-export-adjacent.d.ts +32 -2
  81. package/dist/src/commands/re-export-adjacent.js +51 -10
  82. package/dist/src/commands/re-export-bulk-scan.js +6 -10
  83. package/dist/src/commands/re-export-drift.d.ts +8 -0
  84. package/dist/src/commands/re-export-drift.js +65 -4
  85. package/dist/src/commands/re-export-files.d.ts +2 -3
  86. package/dist/src/commands/re-export-files.js +38 -32
  87. package/dist/src/commands/re-export-lint.d.ts +3 -4
  88. package/dist/src/commands/re-export-lint.js +9 -2
  89. package/dist/src/commands/re-export-options.js +19 -5
  90. package/dist/src/commands/re-export-register.js +5 -1
  91. package/dist/src/commands/re-export-scan.js +8 -8
  92. package/dist/src/commands/re-export-single.d.ts +22 -3
  93. package/dist/src/commands/re-export-single.js +19 -11
  94. package/dist/src/commands/re-export.js +44 -27
  95. package/dist/src/commands/rebase/abort.js +3 -8
  96. package/dist/src/commands/rebase/continue.js +12 -12
  97. package/dist/src/commands/rebase/index.js +2 -2
  98. package/dist/src/commands/rebase/patch-loop.d.ts +2 -2
  99. package/dist/src/commands/rebase/patch-loop.js +2 -7
  100. package/dist/src/commands/resolve.js +18 -10
  101. package/dist/src/commands/run.d.ts +0 -13
  102. package/dist/src/commands/run.js +12 -25
  103. package/dist/src/commands/setup-support.d.ts +16 -4
  104. package/dist/src/commands/setup-support.js +16 -16
  105. package/dist/src/commands/setup.js +3 -9
  106. package/dist/src/commands/source.js +8 -1
  107. package/dist/src/commands/status-check.js +2 -2
  108. package/dist/src/commands/status-output.js +2 -2
  109. package/dist/src/commands/status.js +3 -4
  110. package/dist/src/commands/test-build-phase.d.ts +2 -2
  111. package/dist/src/commands/test-build-phase.js +33 -8
  112. package/dist/src/commands/test-diagnose.js +14 -5
  113. package/dist/src/commands/test-modes.d.ts +2 -2
  114. package/dist/src/commands/test-modes.js +0 -1
  115. package/dist/src/commands/test-register.js +6 -3
  116. package/dist/src/commands/test-run.js +29 -21
  117. package/dist/src/commands/test-stale-gate.js +8 -5
  118. package/dist/src/commands/test-verdict.d.ts +16 -2
  119. package/dist/src/commands/test-verdict.js +18 -3
  120. package/dist/src/commands/test.js +51 -7
  121. package/dist/src/commands/token-list.d.ts +26 -0
  122. package/dist/src/commands/token-list.js +153 -0
  123. package/dist/src/commands/token.js +17 -1
  124. package/dist/src/commands/tree.js +7 -2
  125. package/dist/src/commands/typecheck.js +9 -9
  126. package/dist/src/commands/wire.js +4 -3
  127. package/dist/src/core/branding.d.ts +0 -7
  128. package/dist/src/core/branding.js +3 -2
  129. package/dist/src/core/browser-wire.js +6 -3
  130. package/dist/src/core/build-audit-platform.d.ts +29 -0
  131. package/dist/src/core/build-audit-platform.js +140 -0
  132. package/dist/src/core/build-audit-registration.js +3 -2
  133. package/dist/src/core/build-audit-skip.d.ts +57 -0
  134. package/dist/src/core/build-audit-skip.js +112 -0
  135. package/dist/src/core/build-audit.d.ts +15 -2
  136. package/dist/src/core/build-audit.js +154 -12
  137. package/dist/src/core/build-baseline.d.ts +45 -28
  138. package/dist/src/core/build-baseline.js +6 -29
  139. package/dist/src/core/build-jar-escalation.d.ts +64 -0
  140. package/dist/src/core/build-jar-escalation.js +128 -0
  141. package/dist/src/core/build-overwrite-guard.js +1 -1
  142. package/dist/src/core/build-prepare.d.ts +5 -2
  143. package/dist/src/core/build-prepare.js +34 -6
  144. package/dist/src/core/config-paths.d.ts +2 -6
  145. package/dist/src/core/config-paths.js +4 -2
  146. package/dist/src/core/config-state.d.ts +0 -6
  147. package/dist/src/core/config-state.js +1 -19
  148. package/dist/src/core/config-validate-build-audit.d.ts +8 -0
  149. package/dist/src/core/config-validate-build-audit.js +80 -0
  150. package/dist/src/core/config-validate-file-size.d.ts +12 -0
  151. package/dist/src/core/config-validate-file-size.js +65 -0
  152. package/dist/src/core/config-validate-patch-policy.d.ts +2 -2
  153. package/dist/src/core/config-validate-patch-policy.js +3 -3
  154. package/dist/src/core/config-validate-test-toolchains.d.ts +2 -2
  155. package/dist/src/core/config-validate-test-toolchains.js +4 -5
  156. package/dist/src/core/config-validate.js +20 -3
  157. package/dist/src/core/config.d.ts +2 -2
  158. package/dist/src/core/config.js +11 -15
  159. package/dist/src/core/coverage-extend.js +4 -4
  160. package/dist/src/core/discard-baseline.js +7 -7
  161. package/dist/src/core/engine-precondition.d.ts +1 -9
  162. package/dist/src/core/engine-precondition.js +3 -8
  163. package/dist/src/core/engine-session-lock.js +13 -17
  164. package/dist/src/core/export-moved-code-shape.js +2 -2
  165. package/dist/src/core/file-lock-owner.d.ts +103 -0
  166. package/dist/src/core/file-lock-owner.js +259 -0
  167. package/dist/src/core/file-lock.d.ts +2 -7
  168. package/dist/src/core/file-lock.js +40 -91
  169. package/dist/src/core/firefox-cache.d.ts +13 -1
  170. package/dist/src/core/firefox-cache.js +59 -34
  171. package/dist/src/core/firefox-download.d.ts +15 -0
  172. package/dist/src/core/firefox-download.js +37 -21
  173. package/dist/src/core/firefox-extract.d.ts +6 -5
  174. package/dist/src/core/firefox-extract.js +36 -18
  175. package/dist/src/core/firefox.d.ts +30 -32
  176. package/dist/src/core/firefox.js +5 -36
  177. package/dist/src/core/furnace-apply-dry-run.d.ts +1 -6
  178. package/dist/src/core/furnace-apply-dry-run.js +1 -7
  179. package/dist/src/core/furnace-apply-helpers.d.ts +0 -11
  180. package/dist/src/core/furnace-apply-helpers.js +8 -19
  181. package/dist/src/core/furnace-apply-overwrite-warn.js +4 -2
  182. package/dist/src/core/furnace-apply.d.ts +1 -1
  183. package/dist/src/core/furnace-apply.js +30 -10
  184. package/dist/src/core/furnace-config.d.ts +1 -1
  185. package/dist/src/core/furnace-config.js +23 -36
  186. package/dist/src/core/furnace-constants.js +3 -2
  187. package/dist/src/core/furnace-dir-entry.d.ts +20 -0
  188. package/dist/src/core/furnace-dir-entry.js +21 -0
  189. package/dist/src/core/furnace-operation.d.ts +2 -0
  190. package/dist/src/core/furnace-operation.js +3 -1
  191. package/dist/src/core/furnace-refresh.d.ts +18 -6
  192. package/dist/src/core/furnace-refresh.js +3 -6
  193. package/dist/src/core/furnace-registration-ast.d.ts +0 -1
  194. package/dist/src/core/furnace-registration-ast.js +0 -3
  195. package/dist/src/core/furnace-registration.d.ts +2 -1
  196. package/dist/src/core/furnace-registration.js +22 -29
  197. package/dist/src/core/furnace-scanner.js +3 -6
  198. package/dist/src/core/furnace-state-persist.d.ts +2 -2
  199. package/dist/src/core/furnace-validate-registration.js +5 -7
  200. package/dist/src/core/git-base.d.ts +3 -1
  201. package/dist/src/core/git-base.js +25 -3
  202. package/dist/src/core/git-diff-new-file.d.ts +40 -0
  203. package/dist/src/core/git-diff-new-file.js +119 -0
  204. package/dist/src/core/git-diff.d.ts +9 -9
  205. package/dist/src/core/git-diff.js +88 -157
  206. package/dist/src/core/git-file-ops.d.ts +0 -7
  207. package/dist/src/core/git-file-ops.js +4 -28
  208. package/dist/src/core/git-readonly-index.d.ts +0 -2
  209. package/dist/src/core/git-readonly-index.js +0 -4
  210. package/dist/src/core/git-status.d.ts +7 -0
  211. package/dist/src/core/git-status.js +21 -9
  212. package/dist/src/core/git.d.ts +0 -6
  213. package/dist/src/core/git.js +14 -43
  214. package/dist/src/core/harness-orphans.d.ts +73 -0
  215. package/dist/src/core/harness-orphans.js +187 -0
  216. package/dist/src/core/history-log.d.ts +19 -0
  217. package/dist/src/core/history-log.js +29 -0
  218. package/dist/src/core/mach-error-hints.d.ts +0 -5
  219. package/dist/src/core/mach-error-hints.js +1 -1
  220. package/dist/src/core/mach-known-noise-filter.d.ts +25 -3
  221. package/dist/src/core/mach-known-noise-filter.js +31 -10
  222. package/dist/src/core/mach-mozconfig.js +2 -1
  223. package/dist/src/core/mach-python.d.ts +2 -0
  224. package/dist/src/core/mach-python.js +12 -7
  225. package/dist/src/core/mach-resource-shim.d.ts +13 -1
  226. package/dist/src/core/mach-resource-shim.js +112 -9
  227. package/dist/src/core/mach.d.ts +46 -53
  228. package/dist/src/core/mach.js +31 -83
  229. package/dist/src/core/marionette-port.d.ts +0 -17
  230. package/dist/src/core/marionette-port.js +12 -58
  231. package/dist/src/core/marionette-preflight.js +9 -27
  232. package/dist/src/core/mochitest-server-port.js +3 -3
  233. package/dist/src/core/moz-manifest-rules.js +37 -33
  234. package/dist/src/core/patch-apply-fuzz.js +4 -4
  235. package/dist/src/core/patch-apply.d.ts +4 -5
  236. package/dist/src/core/patch-apply.js +55 -32
  237. package/dist/src/core/patch-export-coverage.d.ts +0 -9
  238. package/dist/src/core/patch-export-coverage.js +1 -30
  239. package/dist/src/core/patch-export-update.d.ts +18 -1
  240. package/dist/src/core/patch-export-update.js +4 -3
  241. package/dist/src/core/patch-export.d.ts +11 -21
  242. package/dist/src/core/patch-export.js +44 -66
  243. package/dist/src/core/patch-files.d.ts +0 -4
  244. package/dist/src/core/patch-files.js +0 -11
  245. package/dist/src/core/patch-lint-binary.d.ts +6 -12
  246. package/dist/src/core/patch-lint-cache.d.ts +17 -1
  247. package/dist/src/core/patch-lint-cache.js +3 -6
  248. package/dist/src/core/patch-lint-checkjs.d.ts +33 -16
  249. package/dist/src/core/patch-lint-checkjs.js +43 -25
  250. package/dist/src/core/patch-lint-creators.d.ts +2 -6
  251. package/dist/src/core/patch-lint-cross.d.ts +16 -8
  252. package/dist/src/core/patch-lint-cross.js +49 -92
  253. package/dist/src/core/patch-lint-css.js +10 -6
  254. package/dist/src/core/patch-lint-file-size.d.ts +52 -0
  255. package/dist/src/core/patch-lint-file-size.js +53 -0
  256. package/dist/src/core/patch-lint-forward-registration.d.ts +58 -0
  257. package/dist/src/core/patch-lint-forward-registration.js +210 -0
  258. package/dist/src/core/patch-lint-jsdoc.js +36 -36
  259. package/dist/src/core/patch-lint-module-registration.d.ts +2 -8
  260. package/dist/src/core/patch-lint-prettier.d.ts +36 -0
  261. package/dist/src/core/patch-lint-prettier.js +126 -0
  262. package/dist/src/core/patch-lint-queue-types.d.ts +60 -0
  263. package/dist/src/core/patch-lint-queue-types.js +12 -0
  264. package/dist/src/core/patch-lint-staged-registration.d.ts +61 -0
  265. package/dist/src/core/patch-lint-staged-registration.js +90 -0
  266. package/dist/src/core/patch-lint-unmanaged-companion.d.ts +106 -0
  267. package/dist/src/core/patch-lint-unmanaged-companion.js +214 -0
  268. package/dist/src/core/patch-lint.d.ts +14 -9
  269. package/dist/src/core/patch-lint.js +27 -27
  270. package/dist/src/core/patch-lock.js +3 -3
  271. package/dist/src/core/patch-manifest-io.js +8 -7
  272. package/dist/src/core/patch-parse.d.ts +19 -8
  273. package/dist/src/core/patch-parse.js +25 -0
  274. package/dist/src/core/patch-policy.js +15 -27
  275. package/dist/src/core/patch-source-metadata.d.ts +1 -3
  276. package/dist/src/core/patch-source-metadata.js +0 -4
  277. package/dist/src/core/patch-transform.d.ts +21 -8
  278. package/dist/src/core/patch-transform.js +38 -10
  279. package/dist/src/core/process-cpu-time.js +1 -1
  280. package/dist/src/core/rebase-session.d.ts +2 -8
  281. package/dist/src/core/rebase-session.js +5 -17
  282. package/dist/src/core/register-browser-content.js +3 -2
  283. package/dist/src/core/register-browser-test.js +2 -1
  284. package/dist/src/core/register-module.js +2 -1
  285. package/dist/src/core/register-scaffold.js +5 -4
  286. package/dist/src/core/register-shared-css.js +2 -1
  287. package/dist/src/core/run-log-redact.d.ts +38 -0
  288. package/dist/src/core/run-log-redact.js +69 -0
  289. package/dist/src/core/run-log.d.ts +13 -2
  290. package/dist/src/core/run-log.js +67 -18
  291. package/dist/src/core/signal-critical.js +2 -4
  292. package/dist/src/core/smoke-patterns.d.ts +0 -1
  293. package/dist/src/core/smoke-patterns.js +18 -11
  294. package/dist/src/core/state-file.d.ts +12 -1
  295. package/dist/src/core/state-file.js +14 -1
  296. package/dist/src/core/status-classify.js +2 -1
  297. package/dist/src/core/test-changed-prefs.js +26 -11
  298. package/dist/src/core/test-harness-crash.js +61 -48
  299. package/dist/src/core/test-harness-verdict-notes.d.ts +55 -0
  300. package/dist/src/core/test-harness-verdict-notes.js +48 -0
  301. package/dist/src/core/test-stale-check.d.ts +23 -8
  302. package/dist/src/core/test-stale-check.js +77 -4
  303. package/dist/src/core/test-stall-triage.js +37 -19
  304. package/dist/src/core/test-xpcshell-retry.d.ts +26 -7
  305. package/dist/src/core/test-xpcshell-retry.js +14 -11
  306. package/dist/src/core/token-category.d.ts +33 -0
  307. package/dist/src/core/token-category.js +56 -48
  308. package/dist/src/core/token-dark-mode.js +2 -14
  309. package/dist/src/core/token-inventory.d.ts +56 -0
  310. package/dist/src/core/token-inventory.js +109 -0
  311. package/dist/src/core/token-manager.d.ts +1 -1
  312. package/dist/src/core/token-manager.js +1 -1
  313. package/dist/src/core/token-variant.js +21 -22
  314. package/dist/src/core/toolchain-preflight.js +1 -1
  315. package/dist/src/core/tree-cow.d.ts +0 -2
  316. package/dist/src/core/tree-cow.js +2 -2
  317. package/dist/src/core/tree-store.js +4 -5
  318. package/dist/src/core/typecheck.js +2 -2
  319. package/dist/src/core/wire-dom-fragment.js +6 -17
  320. package/dist/src/core/wire-utils.js +5 -17
  321. package/dist/src/core/xpcshell-appdir.js +2 -4
  322. package/dist/src/errors/base.d.ts +53 -1
  323. package/dist/src/errors/base.js +79 -11
  324. package/dist/src/errors/build.d.ts +28 -0
  325. package/dist/src/errors/build.js +51 -10
  326. package/dist/src/errors/config.d.ts +0 -14
  327. package/dist/src/errors/config.js +13 -34
  328. package/dist/src/errors/download.d.ts +13 -0
  329. package/dist/src/errors/download.js +53 -24
  330. package/dist/src/errors/furnace.js +8 -9
  331. package/dist/src/errors/git.d.ts +8 -8
  332. package/dist/src/errors/git.js +20 -30
  333. package/dist/src/errors/patch.js +8 -10
  334. package/dist/src/errors/rebase.js +7 -7
  335. package/dist/src/types/commands/options.d.ts +96 -123
  336. package/dist/src/types/commands/patches.d.ts +17 -6
  337. package/dist/src/types/config.d.ts +75 -2
  338. package/dist/src/types/index.d.ts +0 -1
  339. package/dist/src/utils/assert.d.ts +3 -15
  340. package/dist/src/utils/assert.js +1 -18
  341. package/dist/src/utils/build-info.d.ts +1 -1
  342. package/dist/src/utils/child-env.d.ts +2 -1
  343. package/dist/src/utils/elapsed.js +10 -1
  344. package/dist/src/utils/fs.d.ts +4 -2
  345. package/dist/src/utils/fs.js +11 -14
  346. package/dist/src/utils/hash.d.ts +12 -0
  347. package/dist/src/utils/hash.js +16 -0
  348. package/dist/src/utils/line-dispatch.d.ts +15 -0
  349. package/dist/src/utils/line-dispatch.js +48 -0
  350. package/dist/src/utils/options.d.ts +1 -7
  351. package/dist/src/utils/options.js +6 -7
  352. package/dist/src/utils/package-root.js +3 -12
  353. package/dist/src/utils/platform.d.ts +0 -4
  354. package/dist/src/utils/process-group.d.ts +2 -1
  355. package/dist/src/utils/process-group.js +20 -19
  356. package/dist/src/utils/process.d.ts +40 -26
  357. package/dist/src/utils/process.js +115 -166
  358. package/dist/src/utils/ps-duration.js +3 -3
  359. package/dist/src/utils/sleep.d.ts +26 -0
  360. package/dist/src/utils/sleep.js +31 -0
  361. package/dist/src/utils/validation.d.ts +2 -38
  362. package/dist/src/utils/validation.js +7 -16
  363. package/docs/configuration.md +136 -1
  364. package/docs/exit-codes.md +34 -20
  365. package/docs/lifecycle-invariants.md +18 -12
  366. package/docs/machine-output.md +31 -8
  367. package/docs/patch-workflow.md +2 -1
  368. package/docs/run-logs.md +44 -4
  369. package/docs/testing.md +70 -11
  370. package/package.json +8 -7
  371. package/dist/src/commands/rebase.d.ts +0 -4
  372. package/dist/src/commands/rebase.js +0 -5
  373. package/dist/src/core/patch-artifact-normalize.d.ts +0 -21
  374. package/dist/src/core/patch-artifact-normalize.js +0 -24
  375. package/dist/src/core/patch-lint-reexports.d.ts +0 -7
  376. package/dist/src/core/patch-lint-reexports.js +0 -8
  377. package/dist/src/core/wire-targets.d.ts +0 -7
  378. package/dist/src/core/wire-targets.js +0 -8
@@ -24,7 +24,7 @@
24
24
  * installed the probe returns `{ inUse: false }` rather than failing the
25
25
  * test run — it is a best-effort friendliness check, not a prerequisite.
26
26
  */
27
- import { GeneralError } from '../errors/base.js';
27
+ import { PreflightRefusalError } from '../errors/base.js';
28
28
  import { toError } from '../utils/errors.js';
29
29
  import { getPlatform } from '../utils/platform.js';
30
30
  import { exec } from '../utils/process.js';
@@ -133,10 +133,10 @@ export async function ensureLaunchableBrowserNotRunning(launchableBinary, option
133
133
  return;
134
134
  }
135
135
  catch (error) {
136
- throw new GeneralError(`A browser from this objdir is still running (PID ${holder.pid}), but FireForge could not terminate it: ${toError(error).message}`);
136
+ throw new PreflightRefusalError(`A browser from this objdir is still running (PID ${holder.pid}), but FireForge could not terminate it: ${toError(error).message}`, 'stale-browser-kill-failed');
137
137
  }
138
138
  }
139
- throw new GeneralError(describeRunningBundleRefusal(holder));
139
+ throw new PreflightRefusalError(describeRunningBundleRefusal(holder), 'stale-browser');
140
140
  }
141
141
  /**
142
142
  * True when the process was launched BY a test harness — its command line
@@ -349,18 +349,18 @@ export async function assertMarionettePortAvailable(port = DEFAULT_MARIONETTE_PO
349
349
  const killHint = process.platform === 'win32'
350
350
  ? `Stop-Process -Id ${holder.pid} -Force`
351
351
  : `kill ${holder.pid} # or "kill -9 ${holder.pid}" if it doesn't exit`;
352
- throw new GeneralError(`Marionette port ${port} is already in use by ${holder.command} (PID ${holder.pid}). ` +
352
+ throw new PreflightRefusalError(`Marionette port ${port} is already in use by ${holder.command} (PID ${holder.pid}). ` +
353
353
  `This is usually a browser left running by a previously interrupted "fireforge test" run. ` +
354
354
  `Kill it with "${killHint}", then retry. ` +
355
355
  `(If you expected ${holder.command} to be running on ${port}, stop it manually or pass ` +
356
- `"--marionette-port <port>" to launch mach test on a different port.)`);
356
+ `"--marionette-port <port>" to launch mach test on a different port.)`, 'marionette-port-busy');
357
357
  }
358
358
  // Non-browser holder: mach test will still fail to bind, but the
359
359
  // cause is not a stale FireForge-launched browser. Flag it
360
360
  // explicitly so the operator can decide what to do instead of
361
361
  // getting mach's bind error with no FireForge context.
362
- throw new GeneralError(`Marionette port ${port} is already in use by ${holder.command} (PID ${holder.pid}). ` +
363
- `This is not a FireForge-launched browser; stop the holder process or free the port before rerunning.`);
362
+ throw new PreflightRefusalError(`Marionette port ${port} is already in use by ${holder.command} (PID ${holder.pid}). ` +
363
+ `This is not a FireForge-launched browser; stop the holder process or free the port before rerunning.`, 'marionette-port-busy');
364
364
  }
365
365
  /**
366
366
  * Ensures the Marionette port is usable, optionally terminating a stale
@@ -390,8 +390,8 @@ export async function ensureMarionettePortAvailable(port = DEFAULT_MARIONETTE_PO
390
390
  }
391
391
  }
392
392
  catch (error) {
393
- throw new GeneralError(`Marionette port ${port} is held by stale browser ${holder.command} (PID ${holder.pid}), ` +
394
- `but FireForge could not terminate it: ${toError(error).message}`);
393
+ throw new PreflightRefusalError(`Marionette port ${port} is held by stale browser ${holder.command} (PID ${holder.pid}), ` +
394
+ `but FireForge could not terminate it: ${toError(error).message}`, 'stale-browser-kill-failed');
395
395
  }
396
396
  }
397
397
  /**
@@ -410,10 +410,7 @@ export async function ensureMarionettePortAvailable(port = DEFAULT_MARIONETTE_PO
410
410
  * `undefined`.
411
411
  */
412
412
  export function extractForwardedMarionettePort(machArgs) {
413
- for (let i = 0; i < machArgs.length; i++) {
414
- const arg = machArgs[i];
415
- if (arg === undefined)
416
- continue;
413
+ for (const [i, arg] of machArgs.entries()) {
417
414
  // `--marionette-port=NNNN`
418
415
  let match = /^--marionette-port=(\d+)$/.exec(arg);
419
416
  if (match?.[1]) {
@@ -452,10 +449,7 @@ export function extractForwardedMarionettePort(machArgs) {
452
449
  */
453
450
  export function forwardedMachArgsIncludeMarionetteClient(machArgs) {
454
451
  const valueLooksLikeHostPort = (token) => /:[0-9]+$/.test(token) || /^\[[^]]+\]:[0-9]+$/.test(token);
455
- for (let i = 0; i < machArgs.length; i++) {
456
- const arg = machArgs[i];
457
- if (arg === undefined)
458
- continue;
452
+ for (const [i, arg] of machArgs.entries()) {
459
453
  if (arg.startsWith('--marionette=') && !arg.startsWith('--marionette-port=')) {
460
454
  return true;
461
455
  }
@@ -473,8 +467,7 @@ export function forwardedMachArgsIncludeMarionetteClient(machArgs) {
473
467
  * for runs where the pref is ignored anyway.
474
468
  */
475
469
  export function hasExplicitXpcshellFlavor(machArgs) {
476
- for (let i = 0; i < machArgs.length; i += 1) {
477
- const arg = machArgs[i] ?? '';
470
+ for (const [i, arg] of machArgs.entries()) {
478
471
  if (/^--flavor=xpcshell\b/.test(arg) || arg === '--flavor=xpcshell-tests')
479
472
  return true;
480
473
  if (arg === '--flavor' && /^xpcshell(?:-tests)?$/.test(machArgs[i + 1] ?? ''))
@@ -493,42 +486,3 @@ export function hasExplicitXpcshellFlavor(machArgs) {
493
486
  export function shouldAutoForwardMarionettePortToMach(machArgs) {
494
487
  return !hasExplicitXpcshellFlavor(machArgs);
495
488
  }
496
- /**
497
- * Heuristic: do the test paths or forwarded mach args indicate a flavour
498
- * that actually launches a Marionette-driven browser? Browser-chrome and
499
- * mochitest do; xpcshell does not. A no-paths invocation (the default "run
500
- * all tests" shape) is treated as marionette-relevant since it includes
501
- * browser-chrome.
502
- *
503
- * Note: `fireforge test` auto-forward of `--marionette-port` to mach uses
504
- * {@link shouldAutoForwardMarionettePortToMach} (mach-arg flavor gate) rather
505
- * than this function alone, so toolkit paths without `/mochitest/` still get
506
- * the listener pref and harness `--marionette=127.0.0.1:<n>` when appropriate.
507
- *
508
- * @param testPaths - Engine-relative paths after `stripEnginePrefix`.
509
- * @param machArgs - Forwarded mach args (post-`--mach-arg`).
510
- * @returns `true` when the run is likely to bind a Marionette listener.
511
- */
512
- export function isMarionetteFlavor(testPaths, machArgs) {
513
- if (hasExplicitXpcshellFlavor(machArgs))
514
- return false;
515
- for (const arg of machArgs) {
516
- if (/^--flavor=(browser-chrome|mochitest|chrome|a11y)\b/.test(arg))
517
- return true;
518
- }
519
- if (testPaths.length === 0)
520
- return true;
521
- for (const path of testPaths) {
522
- const base = path.split('/').pop() ?? path;
523
- if (/^browser_.+\.(js|ini|toml)$/.test(base))
524
- return true;
525
- if (path.includes('/mochitest/') || path.startsWith('mochitest/'))
526
- return true;
527
- if (path.includes('/browser-chrome/') || path.startsWith('browser-chrome/'))
528
- return true;
529
- if (path.includes('toolkit/content/tests/') && !path.includes('/tests/xpcshell/')) {
530
- return true;
531
- }
532
- }
533
- return false;
534
- }
@@ -22,6 +22,7 @@ import { pathExists } from '../utils/fs.js';
22
22
  import { info, verbose, warn } from '../utils/logger.js';
23
23
  import { installGracefulShutdownForwarder, trackChildClosure } from '../utils/process.js';
24
24
  import { killProcessTree, sweepProcessGroup } from '../utils/process-group.js';
25
+ import { sleep } from '../utils/sleep.js';
25
26
  import { ensureMach } from './mach.js';
26
27
  import { getPython } from './mach-python.js';
27
28
  /** Marionette's default TCP port when the browser is launched with `--marionette`. */
@@ -143,7 +144,7 @@ export async function runMarionettePreflight(engineDir, options = {}) {
143
144
  const spawnedChild = child;
144
145
  closure = trackChildClosure();
145
146
  forwarder = installGracefulShutdownForwarder(spawnedChild, 500, (signal) => {
146
- killProcessGroup(spawnedChild, signal);
147
+ killProcessTree(spawnedChild, signal, process.platform !== 'win32');
147
148
  });
148
149
  child.stderr?.on('data', (data) => {
149
150
  const chunk = data.toString();
@@ -155,10 +156,10 @@ export async function runMarionettePreflight(engineDir, options = {}) {
155
156
  // never come.
156
157
  const settleDeadline = Math.min(spawnSettleMs, Math.max(0, timeoutMs - elapsed()));
157
158
  if (settleDeadline > 0) {
158
- await delay(settleDeadline);
159
+ await sleep(settleDeadline, { unref: true });
159
160
  }
160
161
  if (hasChildExited(spawnedChild)) {
161
- return fail('browser-spawns', `Browser process exited during spawn (exit code ${String(spawnedChild.exitCode)}, signal ${spawnedChild.signalCode ?? 'none'}). ` +
162
+ return fail('browser-spawns', `Browser process exited during spawn (exit code ${spawnedChild.exitCode}, signal ${spawnedChild.signalCode ?? 'none'}). ` +
162
163
  `stderr tail: ${stderrTail.trim().slice(-2_000) || '(empty)'}`, elapsed());
163
164
  }
164
165
  // Layer 6: marionette handshake within the remaining budget.
@@ -175,7 +176,7 @@ export async function runMarionettePreflight(engineDir, options = {}) {
175
176
  // Child may have exited before the socket was ever ready — surface that
176
177
  // distinctly from "socket never answered" so the operator has a lead.
177
178
  if (hasChildExited(spawnedChild)) {
178
- return fail('marionette-handshake', `Browser process exited before marionette handshake (exit code ${String(spawnedChild.exitCode)}, signal ${spawnedChild.signalCode ?? 'none'}). ` +
179
+ return fail('marionette-handshake', `Browser process exited before marionette handshake (exit code ${spawnedChild.exitCode}, signal ${spawnedChild.signalCode ?? 'none'}). ` +
179
180
  `stderr tail: ${stderrTail.trim().slice(-2_000) || '(empty)'}`, elapsed());
180
181
  }
181
182
  return fail('marionette-handshake', `Marionette socket on 127.0.0.1:${port} did not respond within ${timeoutMs}ms. ` +
@@ -203,12 +204,12 @@ async function teardownPreflightChild(child, forwarder, closure, profileDir) {
203
204
  forwarder?.dispose();
204
205
  const groupPid = child?.pid;
205
206
  if (child && !hasChildExited(child)) {
206
- killProcessGroup(child, 'SIGTERM');
207
+ killProcessTree(child, 'SIGTERM', process.platform !== 'win32');
207
208
  // Small escalation: if the child doesn't honour SIGTERM quickly, SIGKILL
208
209
  // so we don't leave a ghost mach process around after a failed probe.
209
- await delay(500);
210
+ await sleep(500, { unref: true });
210
211
  if (!hasChildExited(child)) {
211
- killProcessGroup(child, 'SIGKILL');
212
+ killProcessTree(child, 'SIGKILL', process.platform !== 'win32');
212
213
  }
213
214
  }
214
215
  // Reap anything that outlived the group signal. The file's own comments
@@ -237,19 +238,6 @@ async function teardownPreflightChild(child, forwarder, closure, profileDir) {
237
238
  warn(`Could not clean up marionette preflight profile: ${toError(error).message}`);
238
239
  }
239
240
  }
240
- /**
241
- * Sends `signal` to the child's whole process group when possible, falling
242
- * back to a direct `child.kill` for environments that don't support
243
- * `detached: true` (Windows in particular: Node still returns a pid, but
244
- * `kill(-pid, …)` is not a supported kernel primitive).
245
- */
246
- function killProcessGroup(child, signal) {
247
- // Delegates to the shared implementation, which is a strict superset of
248
- // the hand-rolled one this replaced: same POSIX `kill(-pid)` with the same
249
- // fall-through to a direct `child.kill`, plus a Windows `taskkill /T /F`
250
- // branch and an already-exited early return.
251
- killProcessTree(child, signal, process.platform !== 'win32');
252
- }
253
241
  function fail(layer, message, durationMs) {
254
242
  return {
255
243
  ok: false,
@@ -267,7 +255,7 @@ async function waitForMarionetteSocket(port, connectFn, keepTrying) {
267
255
  if (result.ok) {
268
256
  return { ok: true };
269
257
  }
270
- await delay(400);
258
+ await sleep(400, { unref: true });
271
259
  }
272
260
  return { ok: false };
273
261
  }
@@ -312,12 +300,6 @@ function attemptMarionetteConnect(port, connectFn) {
312
300
  });
313
301
  });
314
302
  }
315
- function delay(ms) {
316
- return new Promise((resolve) => {
317
- const timer = setTimeout(resolve, ms);
318
- timer.unref();
319
- });
320
- }
321
303
  /** Renders a PASS/FAIL banner to the CLI using the shared logger helpers. */
322
304
  export function reportMarionettePreflight(result) {
323
305
  if (result.ok) {
@@ -100,8 +100,8 @@ export function describeMochitestServerRefusal(port, holder, recognized) {
100
100
  const killHint = process.platform === 'win32'
101
101
  ? `Stop-Process -Id ${holder.pid} -Force`
102
102
  : `kill -9 ${holder.pid}`;
103
- const evidence = `The mochitest server port ${String(port)} is already held by ${holder.command} ` +
104
- `(PID ${String(holder.pid)}).\n` +
103
+ const evidence = `The mochitest server port ${port} is already held by ${holder.command} ` +
104
+ `(PID ${holder.pid}).\n` +
105
105
  ` command: ${holder.commandLine}\n` +
106
106
  " A browser launched now would connect to THAT server, which cannot serve this run's " +
107
107
  'manifest — the run would stall before TEST_START and die on the no-output timeout with ' +
@@ -115,5 +115,5 @@ export function describeMochitestServerRefusal(port, holder, recognized) {
115
115
  return (evidence +
116
116
  " This is NOT the mochitest harness's server.js, so FireForge will not offer to kill it. " +
117
117
  `Stop the holder yourself with "${killHint}" if it is yours, or free the port, then retry. ` +
118
- `Diagnose with "lsof -nP -iTCP:${String(port)}".`);
118
+ `Diagnose with "lsof -nP -iTCP:${port}".`);
119
119
  }
@@ -3,6 +3,7 @@ import { basename, join } from 'node:path';
3
3
  import { GeneralError, InvalidArgumentError } from '../errors/base.js';
4
4
  import { pathExists, readText } from '../utils/fs.js';
5
5
  import { warn } from '../utils/logger.js';
6
+ import { normalizePathSlashes } from '../utils/paths.js';
6
7
  import { escapeRegex } from '../utils/regex.js';
7
8
  import { getProjectPaths, loadConfig } from './config.js';
8
9
  import { registerBrowserContent, registerFireForgeModule, registerSharedCSS, registerTestManifest, registerToolkitWidget, } from './moz-manifest-register.js';
@@ -84,13 +85,30 @@ export function getRules(binaryName, createManifest = false) {
84
85
  },
85
86
  ];
86
87
  }
87
- async function isSharedCSSRegistered(engineDir, fileName) {
88
- const manifestPath = join(engineDir, 'browser/themes/shared/jar.inc.mn');
88
+ /**
89
+ * Reads an engine manifest and reports whether it contains any of `needles`.
90
+ *
91
+ * The five registration probes below share this shape: resolve the manifest
92
+ * relative to the engine, refuse with the manifest's own path when it is
93
+ * missing, then substring-match the entry each registrar writes.
94
+ *
95
+ * @param engineDir - Path to the engine directory
96
+ * @param manifestRel - Engine-relative manifest path (forward slashes; also
97
+ * the path named in the refusal message)
98
+ * @param needles - Entry forms that count as registered
99
+ * @returns True when the manifest contains at least one needle
100
+ * @throws GeneralError when the manifest does not exist
101
+ */
102
+ async function manifestIncludes(engineDir, manifestRel, needles) {
103
+ const manifestPath = join(engineDir, manifestRel);
89
104
  if (!(await pathExists(manifestPath))) {
90
- throw new GeneralError('Manifest not found: browser/themes/shared/jar.inc.mn');
105
+ throw new GeneralError(`Manifest not found: ${manifestRel}`);
91
106
  }
92
- const name = basename(fileName, '.css');
93
107
  const content = await readText(manifestPath);
108
+ return needles.some((needle) => content.includes(needle));
109
+ }
110
+ async function isSharedCSSRegistered(engineDir, fileName) {
111
+ const name = basename(fileName, '.css');
94
112
  // `register` writes the canonical `skin/classic/browser/<name>.css` form;
95
113
  // `furnace chrome-doc create` writes a `content/browser/<name>.css` entry
96
114
  // because the CSS is loaded by a chrome document via a `chrome://browser/
@@ -98,40 +116,26 @@ async function isSharedCSSRegistered(engineDir, fileName) {
98
116
  // chrome-doc scaffolder are not flagged as "potentially unregistered" by
99
117
  // `status` and so a re-run of `register` against the same file recognises
100
118
  // the existing entry instead of proposing a duplicate.
101
- return (content.includes(`skin/classic/browser/${name}.css`) ||
102
- content.includes(`content/browser/${name}.css`));
119
+ return manifestIncludes(engineDir, 'browser/themes/shared/jar.inc.mn', [
120
+ `skin/classic/browser/${name}.css`,
121
+ `content/browser/${name}.css`,
122
+ ]);
103
123
  }
104
124
  async function isBrowserContentRegistered(engineDir, fileName) {
105
- const manifestPath = join(engineDir, 'browser/base/jar.mn');
106
- if (!(await pathExists(manifestPath))) {
107
- throw new GeneralError('Manifest not found: browser/base/jar.mn');
108
- }
109
- const content = await readText(manifestPath);
110
- return content.includes(`content/browser/${fileName}`);
125
+ return manifestIncludes(engineDir, 'browser/base/jar.mn', [`content/browser/${fileName}`]);
111
126
  }
112
127
  async function isTestManifestRegistered(engineDir, testDir) {
113
- const manifestPath = join(engineDir, 'browser/base/moz.build');
114
- if (!(await pathExists(manifestPath))) {
115
- throw new GeneralError('Manifest not found: browser/base/moz.build');
116
- }
117
- const content = await readText(manifestPath);
118
- return content.includes(`content/test/${testDir}/browser.toml`);
128
+ return manifestIncludes(engineDir, 'browser/base/moz.build', [
129
+ `content/test/${testDir}/browser.toml`,
130
+ ]);
119
131
  }
120
132
  async function isFireForgeModuleRegistered(engineDir, fileName, moduleDir) {
121
- const manifestPath = join(engineDir, moduleDir, 'moz.build');
122
- if (!(await pathExists(manifestPath))) {
123
- throw new GeneralError(`Manifest not found: ${moduleDir}/moz.build`);
124
- }
125
- const content = await readText(manifestPath);
126
- return content.includes(`"${fileName}"`);
133
+ return manifestIncludes(engineDir, `${moduleDir}/moz.build`, [`"${fileName}"`]);
127
134
  }
128
135
  async function isToolkitWidgetRegistered(engineDir, _tagName, fileName) {
129
- const manifestPath = join(engineDir, 'toolkit/content/jar.mn');
130
- if (!(await pathExists(manifestPath))) {
131
- throw new GeneralError('Manifest not found: toolkit/content/jar.mn');
132
- }
133
- const content = await readText(manifestPath);
134
- return content.includes(`content/global/elements/${fileName}`);
136
+ return manifestIncludes(engineDir, 'toolkit/content/jar.mn', [
137
+ `content/global/elements/${fileName}`,
138
+ ]);
135
139
  }
136
140
  /**
137
141
  * Checks if a file path matches any known registrable pattern.
@@ -140,7 +144,7 @@ async function isToolkitWidgetRegistered(engineDir, _tagName, fileName) {
140
144
  * @returns True if the file matches a known registration pattern
141
145
  */
142
146
  export function matchesRegistrablePattern(filePath, binaryName) {
143
- const normalized = filePath.replace(/\\/g, '/');
147
+ const normalized = normalizePathSlashes(filePath);
144
148
  const rules = getRules(binaryName);
145
149
  return rules.some((rule) => rule.pattern.test(normalized));
146
150
  }
@@ -197,7 +201,7 @@ export async function isFileRegistered(root, filePath) {
197
201
  const { engine: engineDir } = getProjectPaths(root);
198
202
  const config = await loadConfig(root);
199
203
  const rules = getRules(config.binaryName);
200
- const normalizedPath = filePath.replace(/\\/g, '/');
204
+ const normalizedPath = normalizePathSlashes(filePath);
201
205
  for (const rule of rules) {
202
206
  const match = normalizedPath.match(rule.pattern);
203
207
  if (match) {
@@ -237,7 +241,7 @@ export async function registerFile(root, filePath, dryRun = false, after, option
237
241
  const config = await loadConfig(root);
238
242
  const rules = getRules(config.binaryName, options.createManifest === true);
239
243
  // Normalize path separators
240
- const normalizedPath = filePath.replace(/\\/g, '/');
244
+ const normalizedPath = normalizePathSlashes(filePath);
241
245
  for (const rule of rules) {
242
246
  const match = normalizedPath.match(rule.pattern);
243
247
  if (match) {
@@ -42,13 +42,13 @@ const GIT_DEFAULT_CONTEXT = 3;
42
42
  */
43
43
  export async function applyPatchWithFuzz(patchPath, engineDir, maxFuzz = 3) {
44
44
  if (!Number.isInteger(maxFuzz) || maxFuzz < 0) {
45
- throw new InvalidArgumentError(`maxFuzz must be a non-negative integer, got ${String(maxFuzz)}.`, 'maxFuzz');
45
+ throw new InvalidArgumentError(`maxFuzz must be a non-negative integer, got ${maxFuzz}.`, 'maxFuzz');
46
46
  }
47
47
  await ensureGit();
48
48
  // Try exact match first, then escalate context reduction.
49
49
  const maxSteps = Math.min(maxFuzz, GIT_DEFAULT_CONTEXT);
50
50
  for (let step = 0; step <= maxSteps; step++) {
51
- const contextArgs = step > 0 ? [`-C${String(GIT_DEFAULT_CONTEXT - step)}`] : [];
51
+ const contextArgs = step > 0 ? [`-C${GIT_DEFAULT_CONTEXT - step}`] : [];
52
52
  const check = await exec('git', ['apply', '--check', ...contextArgs, '--', patchPath], {
53
53
  cwd: engineDir,
54
54
  });
@@ -59,12 +59,12 @@ export async function applyPatchWithFuzz(patchPath, engineDir, maxFuzz = 3) {
59
59
  });
60
60
  if (apply.exitCode === 0) {
61
61
  if (step > 0) {
62
- verbose(`Patch applied with reduced context (-C${String(GIT_DEFAULT_CONTEXT - step)}): ${patchPath}`);
62
+ verbose(`Patch applied with reduced context (-C${GIT_DEFAULT_CONTEXT - step}): ${patchPath}`);
63
63
  }
64
64
  return { success: true, fuzzFactor: step };
65
65
  }
66
66
  // Unlikely: --check passed but apply failed; fall through to next step
67
- verbose(`git apply -C${String(GIT_DEFAULT_CONTEXT - step)} --check passed but apply failed: ${apply.stderr.trim()}`);
67
+ verbose(`git apply -C${GIT_DEFAULT_CONTEXT - step} --check passed but apply failed: ${apply.stderr.trim()}`);
68
68
  }
69
69
  }
70
70
  // All context-reduction levels failed → generate .rej files for manual resolution
@@ -4,10 +4,9 @@
4
4
  */
5
5
  import type { ImportSummary, PatchInfo, PatchMetadata } from '../types/commands/index.js';
6
6
  export { PatchError } from '../errors/patch.js';
7
- export { countPatches, discoverPatches, getAllTargetFilesFromPatch, getTargetFileFromPatch, isNewFilePatch, } from './patch-files.js';
7
+ export { countPatches, discoverPatches } from './patch-files.js';
8
8
  export { withPatchDirectoryLock } from './patch-lock.js';
9
- export { extractAffectedFiles, extractOrder, isNewFileInPatch } from './patch-parse.js';
10
- export { applyPatchToContent, extractNewFileContent } from './patch-transform.js';
9
+ export { extractAffectedFiles } from './patch-parse.js';
11
10
  /**
12
11
  * Options for {@link applyPatchesWithContinue}.
13
12
  */
@@ -47,10 +46,10 @@ export declare function matchesUntilFilename(patchFilename: string, needle: stri
47
46
  * on the first failure to keep the engine directory in a clean state.
48
47
  * @param patchesDir - Path to the patches directory
49
48
  * @param engineDir - Path to the engine directory
50
- * @param optionsOrContinue - Options object, or the legacy continueOnFailure boolean
49
+ * @param options - Application options
51
50
  * @returns Import summary with all results
52
51
  */
53
- export declare function applyPatchesWithContinue(patchesDir: string, engineDir: string, optionsOrContinue?: ApplyPatchesOptions | boolean): Promise<ImportSummary>;
52
+ export declare function applyPatchesWithContinue(patchesDir: string, engineDir: string, options?: ApplyPatchesOptions): Promise<ImportSummary>;
54
53
  /**
55
54
  * Batched patched-content computation shared by status classification,
56
55
  * import's unmanaged-dirty guard, and discard baseline planning.
@@ -3,25 +3,24 @@
3
3
  * Patch orchestration — coordinates patch discovery, application, and validation.
4
4
  * Pure parsing, content transformation, and lock management are in separate modules.
5
5
  */
6
- import { lstat, readlink, realpath } from 'node:fs/promises';
6
+ import { lstat, readFile, readlink, realpath, rm } from 'node:fs/promises';
7
7
  import { basename, dirname, join, resolve, sep } from 'node:path';
8
8
  import { PatchError } from '../errors/patch.js';
9
9
  import { toError } from '../utils/errors.js';
10
- import { pathExists, readText, writeText } from '../utils/fs.js';
10
+ import { pathExists, readText, writeFileAtomic, writeText } from '../utils/fs.js';
11
11
  import { verbose } from '../utils/logger.js';
12
12
  import { isContainedRelativePath } from '../utils/paths.js';
13
13
  import { applyPatchIdempotent, reversePatch } from './git.js';
14
- import { getFileContentFromHead } from './git-file-ops.js';
14
+ import { getFileContentAtRef } from './git-file-ops.js';
15
15
  import { discoverPatches } from './patch-files.js';
16
16
  import { loadPatchesManifest } from './patch-manifest.js';
17
- import { extractAffectedFiles, extractConflictingFiles, isNewFileInPatch } from './patch-parse.js';
18
- import { applyPatchTextToContent, extractNewFileContent } from './patch-transform.js';
17
+ import { extractAffectedFiles, extractConflictingFiles, isNewFileInPatch, parseDiffSections, } from './patch-parse.js';
18
+ import { applyPatchTextToContent, extractNewFileContentFromDiff } from './patch-transform.js';
19
19
  // Re-export from split modules so existing import sites continue working
20
20
  export { PatchError } from '../errors/patch.js';
21
- export { countPatches, discoverPatches, getAllTargetFilesFromPatch, getTargetFileFromPatch, isNewFilePatch, } from './patch-files.js';
21
+ export { countPatches, discoverPatches } from './patch-files.js';
22
22
  export { withPatchDirectoryLock } from './patch-lock.js';
23
- export { extractAffectedFiles, extractOrder, isNewFileInPatch } from './patch-parse.js';
24
- export { applyPatchToContent, extractNewFileContent } from './patch-transform.js';
23
+ export { extractAffectedFiles } from './patch-parse.js';
25
24
  /**
26
25
  * Applies a single patch.
27
26
  *
@@ -53,28 +52,57 @@ async function applySinglePatch(patch, engineDir, protectedFiles) {
53
52
  const applyError = toError(error);
54
53
  // Check if this is a resolvable "new file" conflict
55
54
  let resolvedNewFiles = false;
56
- // Save original content for files we might overwrite, so we can restore on failure
55
+ // Save original BYTES of files we might overwrite, so we can restore on
56
+ // failure. Raw buffers rather than decoded text: the same snapshot has to
57
+ // restore a binary target byte-for-byte, and a Buffer round-trips text
58
+ // losslessly where a utf-8 decode of a binary file does not.
57
59
  const savedContents = new Map();
60
+ // New-file sections keyed by target, so the binary discrimination below
61
+ // reads the ALREADY-parsed section instead of re-scanning the body per
62
+ // file.
63
+ const newFileSections = new Map(parseDiffSections(patchContent)
64
+ .filter((section) => section.isNewFile)
65
+ .map((section) => [section.targetPath, section]));
58
66
  try {
59
67
  for (const file of affectedFiles) {
60
- if (isNewFileInPatch(patchContent, file)) {
61
- const targetPath = join(engineDir, file);
62
- if (await pathExists(targetPath)) {
63
- savedContents.set(file, await readText(targetPath));
64
- const content = await extractNewFileContent(patch.path, file);
65
- await writeText(targetPath, content);
66
- resolvedNewFiles = true;
68
+ if (!isNewFileInPatch(patchContent, file))
69
+ continue;
70
+ const targetPath = join(engineDir, file);
71
+ if (!(await pathExists(targetPath)))
72
+ continue;
73
+ savedContents.set(file, await readFile(targetPath));
74
+ const section = newFileSections.get(file);
75
+ if (section?.isBinary === true) {
76
+ // A binary new file cannot be resolved by writing extracted text —
77
+ // the payload is base85, whose alphabet includes '+'. It does not
78
+ // need to be: `git apply` decodes a `GIT binary patch` itself. So
79
+ // the resolution is to REMOVE the blocking file and let the retry
80
+ // below create it from the payload, which is exactly what the
81
+ // text branch achieves by overwriting.
82
+ if (!section.hasBinaryDelta) {
83
+ // `Binary files … differ` records THAT the bytes changed and
84
+ // carries none of them, so nothing can recreate the file. Fail
85
+ // honestly rather than deleting a file we cannot put back.
86
+ throw new PatchError(`Cannot resolve the new-file conflict for ${file}: the patch's binary section ` +
87
+ 'carries no replayable payload (`Binary files … differ` rather than `GIT binary ' +
88
+ 'patch`), so the file cannot be recreated. Re-export the patch with a binary-aware ' +
89
+ 'diff.', file);
67
90
  }
91
+ await rm(targetPath, { force: true });
68
92
  }
93
+ else {
94
+ await writeText(targetPath, extractNewFileContentFromDiff(patchContent, file));
95
+ }
96
+ resolvedNewFiles = true;
69
97
  }
70
98
  }
71
99
  catch (extractError) {
72
- // extractNewFileContent threw for a LATER target (e.g. a binary
73
- // section) after earlier targets were already overwritten — restore
74
- // them before reporting failure, instead of letting the exception
75
- // skip the restore loop entirely and crash the whole import.
100
+ // Resolution threw for a LATER target after earlier targets were
101
+ // already overwritten or removed — restore them before reporting
102
+ // failure, instead of letting the exception skip the restore loop
103
+ // entirely and crash the whole import.
76
104
  for (const [file, originalContent] of savedContents) {
77
- await writeText(join(engineDir, file), originalContent);
105
+ await writeFileAtomic(join(engineDir, file), originalContent);
78
106
  }
79
107
  return { patch, success: false, error: toError(extractError).message };
80
108
  }
@@ -91,9 +119,9 @@ async function applySinglePatch(patch, engineDir, protectedFiles) {
91
119
  }
92
120
  catch (retryError) {
93
121
  verbose(`Auto-resolved new-file retry failed for ${patch.filename}: ${toError(retryError).message}`);
94
- // Restore original file content before falling through to --reject
122
+ // Restore original file bytes before falling through to --reject
95
123
  for (const [file, originalContent] of savedContents) {
96
- await writeText(join(engineDir, file), originalContent);
124
+ await writeFileAtomic(join(engineDir, file), originalContent);
97
125
  }
98
126
  }
99
127
  }
@@ -138,7 +166,7 @@ async function rollbackPatches(results, engineDir) {
138
166
  if (result.autoResolvedOriginals) {
139
167
  for (const [file, originalContent] of result.autoResolvedOriginals) {
140
168
  try {
141
- await writeText(join(engineDir, file), originalContent);
169
+ await writeFileAtomic(join(engineDir, file), originalContent);
142
170
  verbose(`Restored pre-existing content of ${file} after rollback`);
143
171
  }
144
172
  catch (restoreError) {
@@ -243,15 +271,10 @@ export function matchesUntilFilename(patchFilename, needle) {
243
271
  * on the first failure to keep the engine directory in a clean state.
244
272
  * @param patchesDir - Path to the patches directory
245
273
  * @param engineDir - Path to the engine directory
246
- * @param optionsOrContinue - Options object, or the legacy continueOnFailure boolean
274
+ * @param options - Application options
247
275
  * @returns Import summary with all results
248
276
  */
249
- export async function applyPatchesWithContinue(patchesDir, engineDir, optionsOrContinue = false) {
250
- // Accept both the legacy boolean positional and the new options object so
251
- // existing call sites (tests and rebase) keep working without a rewrite.
252
- const options = typeof optionsOrContinue === 'boolean'
253
- ? { continueOnFailure: optionsOrContinue }
254
- : optionsOrContinue;
277
+ export async function applyPatchesWithContinue(patchesDir, engineDir, options = {}) {
255
278
  const continueOnFailure = options.continueOnFailure ?? false;
256
279
  const untilFilename = options.untilFilename;
257
280
  const patches = await discoverPatches(patchesDir);
@@ -377,7 +400,7 @@ export async function createPatchedContentContext(patchesDir, engineDir) {
377
400
  getAffectingPatches,
378
401
  readPatchBody,
379
402
  computePatched: async (filePath) => {
380
- let content = await getFileContentFromHead(engineDir, filePath);
403
+ let content = await getFileContentAtRef(engineDir, filePath);
381
404
  for (const patch of getAffectingPatches(filePath)) {
382
405
  content = applyPatchTextToContent(content, await readPatchBody(patch), filePath);
383
406
  }
@@ -2,15 +2,6 @@
2
2
  * Patch coverage and supersession helpers used by export planning.
3
3
  */
4
4
  import type { PatchInfo, PatchMetadata } from '../types/commands/index.js';
5
- /**
6
- * Finds patches that are completely superseded by newer patches.
7
- * A patch is superseded if all its affected files are covered by newer patches.
8
- * @param patchesDir - Path to the patches directory
9
- * @param newPatchFiles - Files affected by the new patch
10
- * @param excludeFilename - Filename to exclude from results (the new patch itself)
11
- * @returns Superseded patches
12
- */
13
- export declare function findSupersededPatches(patchesDir: string, newPatchFiles: string[], excludeFilename?: string): Promise<PatchInfo[]>;
14
5
  /**
15
6
  * Report whether a patch is fully covered by a new export, and which of its
16
7
  * files caused the coverage.
@@ -2,37 +2,8 @@
2
2
  /**
3
3
  * Patch coverage and supersession helpers used by export planning.
4
4
  */
5
- import { discoverPatches, isNewFilePatch } from './patch-apply.js';
5
+ import { discoverPatches } from './patch-apply.js';
6
6
  import { loadPatchesManifest } from './patch-manifest.js';
7
- /**
8
- * Finds patches that are completely superseded by newer patches.
9
- * A patch is superseded if all its affected files are covered by newer patches.
10
- * @param patchesDir - Path to the patches directory
11
- * @param newPatchFiles - Files affected by the new patch
12
- * @param excludeFilename - Filename to exclude from results (the new patch itself)
13
- * @returns Superseded patches
14
- */
15
- export async function findSupersededPatches(patchesDir, newPatchFiles, excludeFilename) {
16
- const manifest = await loadPatchesManifest(patchesDir);
17
- if (!manifest)
18
- return [];
19
- const patches = await discoverPatches(patchesDir);
20
- const superseded = [];
21
- for (const metadata of manifest.patches) {
22
- if (excludeFilename && metadata.filename === excludeFilename)
23
- continue;
24
- if (metadata.filesAffected.length === 1) {
25
- const affectedFile = metadata.filesAffected[0];
26
- if (affectedFile && newPatchFiles.includes(affectedFile)) {
27
- const patch = patches.find((p) => p.filename === metadata.filename);
28
- if (patch && (await isNewFilePatch(patch.path))) {
29
- superseded.push(patch);
30
- }
31
- }
32
- }
33
- }
34
- return superseded;
35
- }
36
7
  /**
37
8
  * Checks whether a patch is fully covered by a new export.
38
9
  * A patch is fully covered when every file it affects is present in the new export.