@hominis/fireforge 0.34.2 → 0.36.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 (360) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +43 -5
  3. package/dist/bin/fireforge.d.ts +4 -0
  4. package/dist/bin/fireforge.js +25 -6
  5. package/dist/src/cli.js +22 -5
  6. package/dist/src/commands/bootstrap-checks.js +0 -1
  7. package/dist/src/commands/bootstrap.d.ts +2 -1
  8. package/dist/src/commands/bootstrap.js +16 -7
  9. package/dist/src/commands/build.js +17 -3
  10. package/dist/src/commands/config.js +30 -10
  11. package/dist/src/commands/discard.js +24 -5
  12. package/dist/src/commands/doctor/post-rebase-audit.js +0 -1
  13. package/dist/src/commands/doctor-check-core.js +0 -1
  14. package/dist/src/commands/doctor-external-toolchains.d.ts +3 -0
  15. package/dist/src/commands/doctor-external-toolchains.js +54 -0
  16. package/dist/src/commands/doctor-furnace-jar.js +0 -1
  17. package/dist/src/commands/doctor-furnace-manifest-sync.js +0 -1
  18. package/dist/src/commands/doctor-furnace.js +2 -2
  19. package/dist/src/commands/doctor-working-tree.js +0 -1
  20. package/dist/src/commands/doctor.js +4 -1
  21. package/dist/src/commands/download.js +51 -15
  22. package/dist/src/commands/export-all.js +5 -3
  23. package/dist/src/commands/export-flow.js +12 -7
  24. package/dist/src/commands/export-placement-gate.js +0 -1
  25. package/dist/src/commands/export-placement-policy.js +0 -1
  26. package/dist/src/commands/export-shared.d.ts +5 -1
  27. package/dist/src/commands/export-shared.js +18 -6
  28. package/dist/src/commands/export.js +15 -10
  29. package/dist/src/commands/furnace/apply.js +22 -6
  30. package/dist/src/commands/furnace/chrome-doc-remove.js +0 -1
  31. package/dist/src/commands/furnace/chrome-doc-templates.js +0 -1
  32. package/dist/src/commands/furnace/chrome-doc-tests.js +0 -1
  33. package/dist/src/commands/furnace/chrome-doc.js +0 -1
  34. package/dist/src/commands/furnace/create-browser-test.js +0 -1
  35. package/dist/src/commands/furnace/create-dry-run.js +0 -1
  36. package/dist/src/commands/furnace/create-features.js +0 -1
  37. package/dist/src/commands/furnace/create-mochikit.js +0 -1
  38. package/dist/src/commands/furnace/create-readback.js +0 -1
  39. package/dist/src/commands/furnace/create-templates.js +0 -1
  40. package/dist/src/commands/furnace/create-validation.js +0 -1
  41. package/dist/src/commands/furnace/create-xpcshell.js +0 -1
  42. package/dist/src/commands/furnace/create.js +0 -1
  43. package/dist/src/commands/furnace/deploy.js +7 -83
  44. package/dist/src/commands/furnace/diff.js +0 -1
  45. package/dist/src/commands/furnace/index.js +59 -25
  46. package/dist/src/commands/furnace/init.js +0 -1
  47. package/dist/src/commands/furnace/list.js +0 -1
  48. package/dist/src/commands/furnace/override.js +0 -1
  49. package/dist/src/commands/furnace/preview.js +3 -3
  50. package/dist/src/commands/furnace/refresh.js +0 -1
  51. package/dist/src/commands/furnace/remove-state.d.ts +15 -0
  52. package/dist/src/commands/furnace/remove-state.js +47 -1
  53. package/dist/src/commands/furnace/remove.js +3 -9
  54. package/dist/src/commands/furnace/rename-browser-test.js +1 -5
  55. package/dist/src/commands/furnace/rename-helpers.d.ts +6 -0
  56. package/dist/src/commands/furnace/rename-helpers.js +30 -1
  57. package/dist/src/commands/furnace/rename-xpcshell.js +7 -6
  58. package/dist/src/commands/furnace/rename.js +15 -36
  59. package/dist/src/commands/furnace/scan.js +0 -1
  60. package/dist/src/commands/furnace/status.js +0 -1
  61. package/dist/src/commands/furnace/sync.js +76 -4
  62. package/dist/src/commands/furnace/validate.js +0 -1
  63. package/dist/src/commands/furnace/validation-output.js +0 -1
  64. package/dist/src/commands/import.js +35 -9
  65. package/dist/src/commands/lint-per-patch.js +19 -7
  66. package/dist/src/commands/lint.js +13 -3
  67. package/dist/src/commands/manifest.js +1 -2
  68. package/dist/src/commands/package.js +2 -1
  69. package/dist/src/commands/patch/compact.js +1 -1
  70. package/dist/src/commands/patch/delete.js +1 -1
  71. package/dist/src/commands/patch/index.js +1 -1
  72. package/dist/src/commands/patch/lint-ignore.js +21 -6
  73. package/dist/src/commands/patch/move-files.js +1 -1
  74. package/dist/src/commands/patch/patch-context.js +0 -1
  75. package/dist/src/commands/patch/rename.js +4 -4
  76. package/dist/src/commands/patch/reorder.js +3 -4
  77. package/dist/src/commands/patch/split-plan.js +2 -1
  78. package/dist/src/commands/patch/split.js +4 -4
  79. package/dist/src/commands/patch/staged-dependency.js +1 -1
  80. package/dist/src/commands/patch/tier.js +18 -5
  81. package/dist/src/commands/re-export-bulk-scan.js +0 -1
  82. package/dist/src/commands/re-export-files.js +1 -1
  83. package/dist/src/commands/re-export-options.js +0 -1
  84. package/dist/src/commands/re-export-scan.js +0 -1
  85. package/dist/src/commands/re-export.js +6 -4
  86. package/dist/src/commands/rebase/abort.js +2 -9
  87. package/dist/src/commands/rebase/confirm.js +0 -1
  88. package/dist/src/commands/rebase/conflict-summary.js +0 -1
  89. package/dist/src/commands/rebase/continue.js +0 -1
  90. package/dist/src/commands/rebase/index.d.ts +1 -1
  91. package/dist/src/commands/rebase/index.js +10 -15
  92. package/dist/src/commands/rebase/patch-loop.js +38 -7
  93. package/dist/src/commands/rebase/summary.js +2 -3
  94. package/dist/src/commands/rebase.js +0 -1
  95. package/dist/src/commands/register.js +1 -1
  96. package/dist/src/commands/reset.js +16 -9
  97. package/dist/src/commands/resolve.js +2 -2
  98. package/dist/src/commands/run.js +18 -6
  99. package/dist/src/commands/setup-support.js +0 -1
  100. package/dist/src/commands/setup.js +1 -2
  101. package/dist/src/commands/source.js +1 -2
  102. package/dist/src/commands/status-output.js +1 -12
  103. package/dist/src/commands/status.js +147 -133
  104. package/dist/src/commands/test-appdir.js +0 -1
  105. package/dist/src/commands/test-diagnose.js +18 -2
  106. package/dist/src/commands/test-modes.d.ts +15 -0
  107. package/dist/src/commands/test-modes.js +61 -0
  108. package/dist/src/commands/test-register.d.ts +4 -0
  109. package/dist/src/commands/test-register.js +64 -0
  110. package/dist/src/commands/test-run.d.ts +23 -7
  111. package/dist/src/commands/test-run.js +15 -14
  112. package/dist/src/commands/test.d.ts +0 -4
  113. package/dist/src/commands/test.js +95 -56
  114. package/dist/src/commands/token-coverage.js +0 -1
  115. package/dist/src/commands/token.js +1 -2
  116. package/dist/src/commands/typecheck.js +1 -1
  117. package/dist/src/commands/verify.js +3 -9
  118. package/dist/src/commands/watch.js +5 -1
  119. package/dist/src/commands/wire.js +1 -1
  120. package/dist/src/core/ast-utils.js +0 -1
  121. package/dist/src/core/brand-validation.js +0 -1
  122. package/dist/src/core/branding.js +0 -1
  123. package/dist/src/core/browser-wire.js +0 -1
  124. package/dist/src/core/build-audit-platform.js +0 -1
  125. package/dist/src/core/build-audit-registration.js +0 -1
  126. package/dist/src/core/build-audit-resolve.js +0 -1
  127. package/dist/src/core/build-audit-transforms.js +0 -1
  128. package/dist/src/core/build-audit.js +0 -1
  129. package/dist/src/core/build-baseline-types.js +0 -1
  130. package/dist/src/core/build-baseline.js +0 -1
  131. package/dist/src/core/build-prepare.js +21 -7
  132. package/dist/src/core/config-mutate.js +0 -1
  133. package/dist/src/core/config-paths.d.ts +2 -2
  134. package/dist/src/core/config-paths.js +6 -1
  135. package/dist/src/core/config-state.js +0 -1
  136. package/dist/src/core/config-validate-patch-policy.js +0 -1
  137. package/dist/src/core/config-validate-test-toolchains.d.ts +8 -0
  138. package/dist/src/core/config-validate-test-toolchains.js +105 -0
  139. package/dist/src/core/config-validate.js +3 -1
  140. package/dist/src/core/config.js +0 -1
  141. package/dist/src/core/destructive.js +0 -1
  142. package/dist/src/core/diff-hunks.js +13 -7
  143. package/dist/src/core/engine-changes.js +0 -1
  144. package/dist/src/core/engine-session-lock.d.ts +12 -0
  145. package/dist/src/core/engine-session-lock.js +45 -0
  146. package/dist/src/core/file-lock.d.ts +8 -0
  147. package/dist/src/core/file-lock.js +157 -48
  148. package/dist/src/core/firefox-archive.d.ts +4 -0
  149. package/dist/src/core/firefox-archive.js +6 -1
  150. package/dist/src/core/firefox-cache.js +67 -8
  151. package/dist/src/core/firefox-download.js +0 -1
  152. package/dist/src/core/firefox-extract.d.ts +21 -1
  153. package/dist/src/core/firefox-extract.js +178 -9
  154. package/dist/src/core/firefox-ignorefile.js +0 -1
  155. package/dist/src/core/firefox.d.ts +12 -0
  156. package/dist/src/core/firefox.js +64 -2
  157. package/dist/src/core/furnace-apply-dry-run.js +0 -1
  158. package/dist/src/core/furnace-apply-ftl.d.ts +7 -3
  159. package/dist/src/core/furnace-apply-ftl.js +10 -5
  160. package/dist/src/core/furnace-apply-helpers.js +0 -1
  161. package/dist/src/core/furnace-apply-output.js +2 -2
  162. package/dist/src/core/furnace-apply.d.ts +7 -1
  163. package/dist/src/core/furnace-apply.js +47 -13
  164. package/dist/src/core/furnace-checksum-utils.js +0 -1
  165. package/dist/src/core/furnace-config-array-utils.js +0 -1
  166. package/dist/src/core/furnace-config-custom.js +0 -1
  167. package/dist/src/core/furnace-config-order.js +0 -1
  168. package/dist/src/core/furnace-config-tokens.js +0 -1
  169. package/dist/src/core/furnace-config-validate.js +0 -1
  170. package/dist/src/core/furnace-config.d.ts +11 -0
  171. package/dist/src/core/furnace-config.js +37 -1
  172. package/dist/src/core/furnace-constants.js +0 -1
  173. package/dist/src/core/furnace-css-fragments.js +0 -1
  174. package/dist/src/core/furnace-graph-utils.js +0 -1
  175. package/dist/src/core/furnace-jsconfig.d.ts +2 -3
  176. package/dist/src/core/furnace-jsconfig.js +25 -18
  177. package/dist/src/core/furnace-marker.js +0 -1
  178. package/dist/src/core/furnace-operation.js +0 -1
  179. package/dist/src/core/furnace-refresh.js +0 -1
  180. package/dist/src/core/furnace-registration-ast.d.ts +14 -0
  181. package/dist/src/core/furnace-registration-ast.js +1 -2
  182. package/dist/src/core/furnace-registration-remove.js +0 -1
  183. package/dist/src/core/furnace-registration-validate.js +1 -5
  184. package/dist/src/core/furnace-registration.js +0 -1
  185. package/dist/src/core/furnace-rollback.js +0 -1
  186. package/dist/src/core/furnace-scanner.js +0 -1
  187. package/dist/src/core/furnace-staleness.js +0 -1
  188. package/dist/src/core/furnace-state-persist.d.ts +72 -0
  189. package/dist/src/core/furnace-state-persist.js +118 -0
  190. package/dist/src/core/furnace-step-errors.d.ts +23 -0
  191. package/dist/src/core/furnace-step-errors.js +25 -0
  192. package/dist/src/core/furnace-stories.js +0 -1
  193. package/dist/src/core/furnace-validate-accessibility.js +0 -1
  194. package/dist/src/core/furnace-validate-checks.js +0 -1
  195. package/dist/src/core/furnace-validate-compatibility.js +0 -1
  196. package/dist/src/core/furnace-validate-helpers.js +0 -1
  197. package/dist/src/core/furnace-validate-registration.js +7 -7
  198. package/dist/src/core/furnace-validate-structure.d.ts +8 -0
  199. package/dist/src/core/furnace-validate-structure.js +11 -2
  200. package/dist/src/core/furnace-validate.js +0 -1
  201. package/dist/src/core/furnace-version-drift.d.ts +6 -3
  202. package/dist/src/core/furnace-version-drift.js +6 -4
  203. package/dist/src/core/git-base.js +0 -1
  204. package/dist/src/core/git-diff.js +0 -1
  205. package/dist/src/core/git-file-ops.js +0 -1
  206. package/dist/src/core/git-status.d.ts +13 -0
  207. package/dist/src/core/git-status.js +32 -2
  208. package/dist/src/core/git.d.ts +10 -1
  209. package/dist/src/core/git.js +65 -8
  210. package/dist/src/core/license-headers.d.ts +16 -0
  211. package/dist/src/core/license-headers.js +19 -1
  212. package/dist/src/core/lint-cache.js +0 -1
  213. package/dist/src/core/lint-projection.js +0 -1
  214. package/dist/src/core/mach-build-artifacts.js +0 -1
  215. package/dist/src/core/mach-error-hints.js +27 -1
  216. package/dist/src/core/mach-mozconfig.js +0 -1
  217. package/dist/src/core/mach-python.js +0 -1
  218. package/dist/src/core/mach-resource-shim.js +0 -1
  219. package/dist/src/core/mach.js +0 -1
  220. package/dist/src/core/manifest-helpers.js +0 -1
  221. package/dist/src/core/manifest-register.js +0 -1
  222. package/dist/src/core/manifest-rules.js +0 -1
  223. package/dist/src/core/manifest-tokenizers.js +0 -1
  224. package/dist/src/core/marionette-port.d.ts +8 -0
  225. package/dist/src/core/marionette-port.js +34 -3
  226. package/dist/src/core/marionette-preflight.js +0 -1
  227. package/dist/src/core/markdown-table.js +0 -1
  228. package/dist/src/core/ownership-table.js +2 -9
  229. package/dist/src/core/parser-fallback.js +0 -1
  230. package/dist/src/core/patch-apply-fuzz.d.ts +27 -9
  231. package/dist/src/core/patch-apply-fuzz.js +71 -34
  232. package/dist/src/core/patch-apply.d.ts +30 -0
  233. package/dist/src/core/patch-apply.js +152 -39
  234. package/dist/src/core/patch-artifact-normalize.d.ts +18 -5
  235. package/dist/src/core/patch-artifact-normalize.js +19 -15
  236. package/dist/src/core/patch-export-coverage.js +0 -1
  237. package/dist/src/core/patch-export-metadata.js +0 -1
  238. package/dist/src/core/patch-export-update.js +2 -3
  239. package/dist/src/core/patch-export.d.ts +2 -6
  240. package/dist/src/core/patch-export.js +24 -48
  241. package/dist/src/core/patch-files.js +0 -1
  242. package/dist/src/core/patch-identifier-suggest.js +0 -1
  243. package/dist/src/core/patch-lint-checkjs.js +0 -1
  244. package/dist/src/core/patch-lint-chrome-jsdoc.js +0 -1
  245. package/dist/src/core/patch-lint-cross.js +0 -1
  246. package/dist/src/core/patch-lint-css.js +0 -1
  247. package/dist/src/core/patch-lint-diff-tag.js +0 -1
  248. package/dist/src/core/patch-lint-diff.d.ts +2 -1
  249. package/dist/src/core/patch-lint-diff.js +17 -32
  250. package/dist/src/core/patch-lint-imports.js +0 -1
  251. package/dist/src/core/patch-lint-jsdoc.js +0 -1
  252. package/dist/src/core/patch-lint-observer.js +0 -1
  253. package/dist/src/core/patch-lint-ownership.js +0 -1
  254. package/dist/src/core/patch-lint-reexports.js +0 -1
  255. package/dist/src/core/patch-lint.d.ts +37 -2
  256. package/dist/src/core/patch-lint.js +48 -24
  257. package/dist/src/core/patch-lock.js +0 -1
  258. package/dist/src/core/patch-manifest-consistency.js +9 -1
  259. package/dist/src/core/patch-manifest-io.d.ts +16 -0
  260. package/dist/src/core/patch-manifest-io.js +37 -12
  261. package/dist/src/core/patch-manifest-query.d.ts +6 -0
  262. package/dist/src/core/patch-manifest-query.js +27 -12
  263. package/dist/src/core/patch-manifest-resolve.js +0 -1
  264. package/dist/src/core/patch-manifest-validate.js +0 -1
  265. package/dist/src/core/patch-manifest.d.ts +1 -1
  266. package/dist/src/core/patch-manifest.js +1 -2
  267. package/dist/src/core/patch-parse.d.ts +59 -0
  268. package/dist/src/core/patch-parse.js +209 -92
  269. package/dist/src/core/patch-policy.js +0 -1
  270. package/dist/src/core/patch-registration-refs.js +16 -26
  271. package/dist/src/core/patch-source-metadata.js +0 -1
  272. package/dist/src/core/patch-transform.d.ts +4 -0
  273. package/dist/src/core/patch-transform.js +30 -38
  274. package/dist/src/core/rebase-session.js +0 -1
  275. package/dist/src/core/register-browser-content.js +0 -1
  276. package/dist/src/core/register-module.js +0 -1
  277. package/dist/src/core/register-result.js +0 -1
  278. package/dist/src/core/register-scaffold.js +0 -1
  279. package/dist/src/core/register-shared-css.js +0 -1
  280. package/dist/src/core/register-test-manifest.js +0 -1
  281. package/dist/src/core/register-xpcshell-test.js +0 -1
  282. package/dist/src/core/shared-ftl.js +0 -1
  283. package/dist/src/core/signal-critical.js +0 -1
  284. package/dist/src/core/smoke-patterns.js +0 -1
  285. package/dist/src/core/state-file.js +0 -1
  286. package/dist/src/core/status-classify.d.ts +16 -0
  287. package/dist/src/core/status-classify.js +24 -16
  288. package/dist/src/core/test-harness-crash.d.ts +64 -7
  289. package/dist/src/core/test-harness-crash.js +196 -15
  290. package/dist/src/core/test-harness-output.js +0 -1
  291. package/dist/src/core/test-path-scope.d.ts +62 -0
  292. package/dist/src/core/test-path-scope.js +148 -0
  293. package/dist/src/core/test-stale-check.js +0 -1
  294. package/dist/src/core/test-stale-symlink.js +0 -1
  295. package/dist/src/core/test-xpcshell-retry.js +0 -1
  296. package/dist/src/core/token-coverage.js +0 -1
  297. package/dist/src/core/token-dark-mode.js +0 -1
  298. package/dist/src/core/token-docs.js +0 -1
  299. package/dist/src/core/token-manager.js +0 -1
  300. package/dist/src/core/token-scaffold.js +0 -1
  301. package/dist/src/core/token-variant.js +0 -1
  302. package/dist/src/core/toolchain-preflight.d.ts +85 -0
  303. package/dist/src/core/toolchain-preflight.js +256 -0
  304. package/dist/src/core/typecheck-shim.d.ts +4 -1
  305. package/dist/src/core/typecheck-shim.js +61 -18
  306. package/dist/src/core/typecheck.js +0 -1
  307. package/dist/src/core/wire-destroy.js +0 -1
  308. package/dist/src/core/wire-dom-fragment.js +0 -1
  309. package/dist/src/core/wire-init.js +0 -1
  310. package/dist/src/core/wire-subscript.js +0 -1
  311. package/dist/src/core/wire-targets.js +0 -1
  312. package/dist/src/core/wire-utils.js +0 -1
  313. package/dist/src/core/xpcshell-appdir.js +1 -4
  314. package/dist/src/errors/base.d.ts +29 -0
  315. package/dist/src/errors/base.js +35 -1
  316. package/dist/src/errors/build.js +0 -1
  317. package/dist/src/errors/codes.d.ts +6 -0
  318. package/dist/src/errors/codes.js +6 -1
  319. package/dist/src/errors/config.js +0 -1
  320. package/dist/src/errors/download.js +2 -1
  321. package/dist/src/errors/furnace.d.ts +11 -1
  322. package/dist/src/errors/furnace.js +6 -5
  323. package/dist/src/errors/git.js +0 -1
  324. package/dist/src/errors/patch.d.ts +17 -0
  325. package/dist/src/errors/patch.js +29 -1
  326. package/dist/src/errors/rebase.js +0 -1
  327. package/dist/src/errors/run.js +1 -1
  328. package/dist/src/index.js +0 -1
  329. package/dist/src/types/cli.js +0 -1
  330. package/dist/src/types/commands/index.js +0 -1
  331. package/dist/src/types/commands/options.d.ts +20 -5
  332. package/dist/src/types/commands/options.js +0 -1
  333. package/dist/src/types/commands/patches.d.ts +8 -0
  334. package/dist/src/types/commands/patches.js +0 -1
  335. package/dist/src/types/commands/project.d.ts +11 -2
  336. package/dist/src/types/commands/project.js +0 -1
  337. package/dist/src/types/config.d.ts +32 -0
  338. package/dist/src/types/config.js +0 -1
  339. package/dist/src/types/furnace.d.ts +10 -0
  340. package/dist/src/types/furnace.js +0 -1
  341. package/dist/src/types/index.js +0 -1
  342. package/dist/src/types/typecheck.js +0 -1
  343. package/dist/src/utils/elapsed.js +0 -1
  344. package/dist/src/utils/errors.js +0 -1
  345. package/dist/src/utils/fs.js +37 -5
  346. package/dist/src/utils/logger.d.ts +7 -0
  347. package/dist/src/utils/logger.js +85 -3
  348. package/dist/src/utils/options.d.ts +15 -0
  349. package/dist/src/utils/options.js +25 -1
  350. package/dist/src/utils/package-root.js +11 -4
  351. package/dist/src/utils/parse.js +0 -1
  352. package/dist/src/utils/paths.js +0 -1
  353. package/dist/src/utils/platform.js +0 -1
  354. package/dist/src/utils/process.d.ts +33 -4
  355. package/dist/src/utils/process.js +183 -28
  356. package/dist/src/utils/regex.d.ts +13 -3
  357. package/dist/src/utils/regex.js +53 -4
  358. package/dist/src/utils/validation.d.ts +0 -12
  359. package/dist/src/utils/validation.js +13 -26
  360. package/package.json +8 -7
@@ -0,0 +1,54 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { isAbsolute, resolve } from 'node:path';
3
+ import { pathExists } from '../utils/fs.js';
4
+ import { exec, findExecutable } from '../utils/process.js';
5
+ import { failure, warning } from './doctor-check-core.js';
6
+ async function resolveDeclaredTool(projectRoot, tool) {
7
+ if (tool.path !== undefined) {
8
+ const candidate = isAbsolute(tool.path) ? tool.path : resolve(projectRoot, tool.path);
9
+ return (await pathExists(candidate)) ? candidate : undefined;
10
+ }
11
+ if (tool.xcrun === true) {
12
+ try {
13
+ const found = await exec('xcrun', ['-find', tool.name], { timeout: 5000 });
14
+ const line = found.stdout.split(/\r?\n/).find((entry) => entry.trim().length > 0);
15
+ return found.exitCode === 0 ? line?.trim() : undefined;
16
+ }
17
+ catch {
18
+ return undefined;
19
+ }
20
+ }
21
+ return findExecutable(tool.name);
22
+ }
23
+ async function runExternalToolchainChecks(ctx) {
24
+ const toolchains = ctx.config?.externalToolchains;
25
+ if (!toolchains || toolchains.length === 0)
26
+ return [];
27
+ const rows = [];
28
+ for (const toolchain of toolchains) {
29
+ for (const tool of toolchain.tools) {
30
+ const label = `External toolchain: ${toolchain.name}/${tool.name}`;
31
+ const found = await resolveDeclaredTool(ctx.projectRoot, tool);
32
+ if (found !== undefined) {
33
+ rows.push({ name: label, passed: true, severity: 'ok', message: `OK (${found})` });
34
+ continue;
35
+ }
36
+ const required = tool.required !== false;
37
+ const location = tool.path
38
+ ? `at ${tool.path}`
39
+ : tool.xcrun
40
+ ? `via xcrun -find ${tool.name}`
41
+ : 'on PATH';
42
+ const message = `${tool.name} was not found ${location}.`;
43
+ const fix = `Install ${tool.name} or update fireforge.json externalToolchains for ${toolchain.name}.`;
44
+ rows.push(required ? failure(label, message, fix) : warning(label, message, fix));
45
+ }
46
+ }
47
+ return rows;
48
+ }
49
+ /** Doctor check for opt-in project-declared external asset/toolchain requirements. */
50
+ export const EXTERNAL_TOOLCHAIN_DOCTOR_CHECK = {
51
+ name: 'External toolchains',
52
+ dependsOn: ['fireforge.json is valid'],
53
+ run: runExternalToolchainChecks,
54
+ };
@@ -44,4 +44,3 @@ export const furnaceStaleJarRegistrationCheck = {
44
44
  }
45
45
  },
46
46
  };
47
- //# sourceMappingURL=doctor-furnace-jar.js.map
@@ -194,4 +194,3 @@ export const furnaceManifestSyncCheck = {
194
194
  'Nothing to repair (orphans surfaced but all were already recoverable).');
195
195
  },
196
196
  };
197
- //# sourceMappingURL=doctor-furnace-manifest-sync.js.map
@@ -6,6 +6,7 @@ import { hasCustomEngineDrift, hasOverrideEngineDrift } from '../core/furnace-ap
6
6
  import { getFurnacePaths, loadFurnaceConfig, loadFurnaceState, updateFurnaceState, } from '../core/furnace-config.js';
7
7
  import { CUSTOM_ELEMENTS_JS, JAR_MN, resolveFtlDir } from '../core/furnace-constants.js';
8
8
  import { getFurnaceLockPath, runFurnaceMutation } from '../core/furnace-operation.js';
9
+ import { hasBlockingStepErrors } from '../core/furnace-step-errors.js';
9
10
  import { validateAllComponents } from '../core/furnace-validate.js';
10
11
  import { toError } from '../utils/errors.js';
11
12
  import { pathExists } from '../utils/fs.js';
@@ -25,7 +26,7 @@ async function runRepairApply(projectRoot) {
25
26
  return runFurnaceMutation(projectRoot, 'apply-rollback', (ctx) => applyAllComponents(projectRoot, false, { operationContext: ctx }), { skipPendingRepairCheck: true });
26
27
  }
27
28
  function countApplyFailures(applyResult) {
28
- const appliedWithStepErrors = applyResult.applied.filter((entry) => (entry.stepErrors?.length ?? 0) > 0).length;
29
+ const appliedWithStepErrors = applyResult.applied.filter((entry) => hasBlockingStepErrors(entry)).length;
29
30
  return applyResult.errors.length + appliedWithStepErrors;
30
31
  }
31
32
  function firstApplyFailure(applyResult) {
@@ -512,4 +513,3 @@ export const FURNACE_DOCTOR_CHECKS = [
512
513
  furnaceComponentValidationCheck,
513
514
  furnaceManifestSyncCheck,
514
515
  ];
515
- //# sourceMappingURL=doctor-furnace.js.map
@@ -94,4 +94,3 @@ export async function inspectEngineWorkingTree(ctx) {
94
94
  : '';
95
95
  return warning('Engine working tree', `Engine working tree has ${counts.unmanaged} unmanaged change${counts.unmanaged === 1 ? '' : 's'}.${managedTail}`, 'Use "fireforge status --ownership" to separate patch-backed from unmanaged files, then export, discard, or reset only the unmanaged set.');
96
96
  }
97
- //# sourceMappingURL=doctor-working-tree.js.map
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { Command } from 'commander';
1
3
  import { configExists, getProjectPaths, loadConfig, loadState, updateState, } from '../core/config.js';
2
4
  import { furnaceConfigExists as checkFurnaceConfigExists } from '../core/furnace-config.js';
3
5
  import { getCurrentBranch, getHead, isGitRepository, isMissingHeadError } from '../core/git.js';
@@ -12,6 +14,7 @@ import { error, info, intro, outro, success, warn } from '../utils/logger.js';
12
14
  import { findExecutable } from '../utils/process.js';
13
15
  import { POST_REBASE_AUDIT_CHECK } from './doctor/post-rebase-audit.js';
14
16
  import { failure, ok, warning } from './doctor-check-core.js';
17
+ import { EXTERNAL_TOOLCHAIN_DOCTOR_CHECK } from './doctor-external-toolchains.js';
15
18
  import { FURNACE_DOCTOR_CHECKS } from './doctor-furnace.js';
16
19
  import { inspectEngineWorkingTree } from './doctor-working-tree.js';
17
20
  import { collectPatchQueueHealth } from './verify.js';
@@ -173,6 +176,7 @@ const DOCTOR_CHECKS = [
173
176
  },
174
177
  fix: 'Check fireforge.json for syntax errors or missing fields',
175
178
  },
179
+ EXTERNAL_TOOLCHAIN_DOCTOR_CHECK,
176
180
  {
177
181
  name: 'Engine directory exists',
178
182
  run: (ctx) => {
@@ -476,4 +480,3 @@ export function registerDoctor(program, { getProjectRoot, withErrorHandling }) {
476
480
  }
477
481
  }));
478
482
  }
479
- //# sourceMappingURL=doctor.js.map
@@ -2,14 +2,16 @@
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { rename } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
- import { getProjectPaths, loadConfig, updateState } from '../core/config.js';
5
+ import { Command } from 'commander';
6
+ import { getProjectPaths, loadConfig, loadState, updateState } from '../core/config.js';
6
7
  import { withFileLock } from '../core/file-lock.js';
7
- import { downloadFirefoxSource, formatBytes } from '../core/firefox.js';
8
- import { getFurnacePaths, updateFurnaceState } from '../core/furnace-config.js';
8
+ import { downloadFirefoxSource, formatBytes, sweepOrphanedEngineWorkDirs, } from '../core/firefox.js';
9
+ import { clearAppliedFurnaceState } from '../core/furnace-config.js';
9
10
  import { getHead, initRepository, isGitRepository, isMissingHeadError, resumeRepository, } from '../core/git.js';
10
11
  import { restoreTrackedPath } from '../core/git-file-ops.js';
11
12
  import { getDirtyFiles } from '../core/git-status.js';
12
13
  import { loadPatchesManifest } from '../core/patch-manifest.js';
14
+ import { formatMajorVersionHopNotice } from '../core/toolchain-preflight.js';
13
15
  import { EngineExistsError, PartialEngineExistsError } from '../errors/download.js';
14
16
  import { toError } from '../utils/errors.js';
15
17
  import { checkDiskSpace, ensureDir, pathExists, pathExistsStrict, removeDir } from '../utils/fs.js';
@@ -115,15 +117,8 @@ function closeRestoreSpinner(restoreSpinner, result) {
115
117
  }
116
118
  async function clearStaleFurnaceApplyState(projectRoot) {
117
119
  // --force installs a new baseCommit, which invalidates every applied
118
- // checksum in furnace-state.json. Preserve pendingRepair: authoring-side
119
- // rollback markers describe unresolved component workspace state and
120
- // should survive an engine refresh.
121
- const furnacePaths = getFurnacePaths(projectRoot);
122
- if (await pathExists(furnacePaths.furnaceState)) {
123
- await updateFurnaceState(projectRoot, (current) => ({
124
- ...(current.pendingRepair ? { pendingRepair: current.pendingRepair } : {}),
125
- }));
126
- }
120
+ // checksum in furnace-state.json.
121
+ await clearAppliedFurnaceState(projectRoot);
127
122
  }
128
123
  async function activateReplacementEngine(args) {
129
124
  const { engineDir, replacementDir, backupDir } = args;
@@ -196,8 +191,22 @@ async function downloadAndExtractFirefox(args) {
196
191
  throw error;
197
192
  }
198
193
  }
194
+ /**
195
+ * Prints the major-version-hop toolchain nudge when this download moved
196
+ * the engine across a Firefox MAJOR version (152.0b7 → 153.0b8 drill:
197
+ * the first post-hop build died in `mach configure` on a moved cbindgen
198
+ * minimum, and nothing in the download output suggested re-running
199
+ * `fireforge bootstrap`). Quiet on first downloads and same-major
200
+ * re-downloads.
201
+ */
202
+ function noteMajorVersionHop(previousVersion, version) {
203
+ const hopNotice = formatMajorVersionHopNotice(previousVersion, version);
204
+ if (hopNotice) {
205
+ info(hopNotice);
206
+ }
207
+ }
199
208
  async function initializeDownloadedEngine(args) {
200
- const { projectRoot, patchesDir, version, engineDir, replacementActivated, backupEngineDir } = args;
209
+ const { projectRoot, patchesDir, version, previousVersion, engineDir, replacementActivated, backupEngineDir, } = args;
201
210
  // Finding #17: the git indexing phase of `download` can block for
202
211
  // minutes on a ~600 MB Firefox tree. Emit a one-line heads-up banner
203
212
  // before the spinner starts so CI logs show the expected duration.
@@ -239,6 +248,7 @@ async function initializeDownloadedEngine(args) {
239
248
  baseCommit,
240
249
  });
241
250
  await noteUnappliedPatches(patchesDir);
251
+ noteMajorVersionHop(previousVersion, version);
242
252
  if (backupEngineDir) {
243
253
  await removeDir(backupEngineDir);
244
254
  }
@@ -264,9 +274,34 @@ export async function downloadCommand(projectRoot, options) {
264
274
  intro('FireForge Download');
265
275
  const config = await loadConfig(projectRoot), version = config.firefox.version;
266
276
  const paths = getProjectPaths(projectRoot);
277
+ // Captured BEFORE any state update so the post-download major-hop
278
+ // notice compares against what was actually on disk until now.
279
+ const previousVersion = (await loadState(projectRoot)).downloadedVersion;
267
280
  info(`Firefox version: ${version}`);
268
281
  await checkDiskSpace(projectRoot, 5 * 1024 * 1024 * 1024, warn);
282
+ // A legitimate holder of this lock runs for 10+ minutes (download +
283
+ // extract + git indexing). The previous default 30 s timeout failed the
284
+ // second invocation with "remove the lock directory if it is stale" advice
285
+ // while the lock was actively held — dangerous guidance mid-extraction.
286
+ // Wait generously instead: a dead holder is reaped within seconds by the
287
+ // PID-based stale probe, so a long timeout only ever waits on real work.
288
+ const downloadLockOptions = {
289
+ timeoutMs: 30 * 60_000,
290
+ onTimeoutMessage: 'Timed out waiting for another FireForge download to finish. ' +
291
+ 'If no other `fireforge download` is running, remove .fireforge/download.fireforge.lock and retry.',
292
+ onStaleLockMessage: (ageMs) => `Removing download lock left behind by a crashed run (${Math.round(ageMs / 1000)}s old).`,
293
+ };
269
294
  await withFileLock(join(paths.fireforgeDir, 'download.fireforge.lock'), async () => {
295
+ // Reclaim multi-GB partial trees left behind by interrupted runs.
296
+ // Safe under the download lock: any `.tmp-*`/`.replacement-*` dir
297
+ // present now is orphaned, since live ones only exist while a
298
+ // download holds this lock. `.backup-*` dirs are NOT swept — a
299
+ // backup can hold the previous engine after a failed forced
300
+ // replacement and is the operator's recovery copy.
301
+ const sweptDirs = await sweepOrphanedEngineWorkDirs(paths.engine);
302
+ if (sweptDirs.length > 0) {
303
+ info(`Removed ${sweptDirs.length} orphaned working director${sweptDirs.length === 1 ? 'y' : 'ies'} from interrupted downloads.`);
304
+ }
270
305
  let installEngineDir = paths.engine;
271
306
  let replacementEngineDir;
272
307
  let backupEngineDir;
@@ -318,6 +353,7 @@ export async function downloadCommand(projectRoot, options) {
318
353
  baseCommit,
319
354
  });
320
355
  await noteUnappliedPatches(paths.patches);
356
+ noteMajorVersionHop(previousVersion, version);
321
357
  outro(`Firefox ${version} is ready! (resumed from partial init)`);
322
358
  return;
323
359
  }
@@ -388,11 +424,12 @@ export async function downloadCommand(projectRoot, options) {
388
424
  projectRoot,
389
425
  patchesDir: paths.patches,
390
426
  version,
427
+ previousVersion,
391
428
  engineDir: installEngineDir,
392
429
  replacementActivated,
393
430
  ...(backupEngineDir !== undefined ? { backupEngineDir } : {}),
394
431
  });
395
- });
432
+ }, downloadLockOptions);
396
433
  }
397
434
  /** Registers the download command on the CLI program. */
398
435
  export function registerDownload(program, { getProjectRoot, withErrorHandling }) {
@@ -404,4 +441,3 @@ export function registerDownload(program, { getProjectRoot, withErrorHandling })
404
441
  await downloadCommand(getProjectRoot(), pickDefined(options));
405
442
  }));
406
443
  }
407
- //# sourceMappingURL=download.js.map
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { Command } from 'commander';
1
3
  import { isBrandingManagedPath } from '../core/branding.js';
2
4
  import { getProjectPaths, loadConfig } from '../core/config.js';
3
5
  import { collectFurnaceManagedPrefixes, furnaceConfigExists, loadFurnaceConfig, } from '../core/furnace-config.js';
@@ -228,8 +230,9 @@ export async function exportAllCommand(projectRoot, options = {}) {
228
230
  await checkDanglingFurnaceRegistrations(projectRoot, diff, furnaceExcluded);
229
231
  // Check for non-interactive mode
230
232
  const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
231
- // Auto-fix missing license headers on new files (interactive only)
232
- const headersAdded = await autoFixLicenseHeaders(paths.engine, diff, config, isInteractive);
233
+ // Auto-fix missing license headers on new files (interactive only;
234
+ // report-only under --dry-run so the preview never mutates engine/)
235
+ const headersAdded = await autoFixLicenseHeaders(paths.engine, diff, config, isInteractive, isDryRun);
233
236
  if (headersAdded) {
234
237
  diff = await getAllDiff(paths.engine);
235
238
  }
@@ -335,4 +338,3 @@ export function registerExportAll(program, { getProjectRoot, withErrorHandling }
335
338
  });
336
339
  }));
337
340
  }
338
- //# sourceMappingURL=export-all.js.map
@@ -8,10 +8,12 @@
8
8
  * testable without dragging the whole command harness along for the ride.
9
9
  */
10
10
  import { join } from 'node:path';
11
- import { findAllPatchesForFilesWithDetails, planExport, sanitizeName, } from '../core/patch-export.js';
11
+ import {} from '../core/destructive.js';
12
+ import { normalizePatchArtifact } from '../core/patch-artifact-normalize.js';
13
+ import { findAllPatchesForFilesWithDetails, patchNameSlug, planExport, } from '../core/patch-export.js';
12
14
  import { buildModifiedFileAdditionsFromDiff, buildPatchQueueContext, detectNewFilesInDiff, lintPatchQueue, } from '../core/patch-lint.js';
13
15
  import { withPatchDirectoryLock } from '../core/patch-lock.js';
14
- import { addPatchToManifest, loadPatchesManifest, renumberPatchesInManifest, resolvePatchIdentifier, savePatchesManifest, } from '../core/patch-manifest.js';
16
+ import { addPatchToManifest, loadPatchesManifest, loadPatchesManifestForWrite, renumberPatchesInManifest, resolvePatchIdentifier, savePatchesManifest, } from '../core/patch-manifest.js';
15
17
  import { applyRenameMapToManifest, buildProjectedManifest, enforcePatchPolicy, } from '../core/patch-policy.js';
16
18
  import { extractNewFileContentFromDiff } from '../core/patch-transform.js';
17
19
  import { GeneralError, InvalidArgumentError } from '../errors/base.js';
@@ -22,7 +24,7 @@ import { assertPlacementPreservesReservedRanges } from './export-placement-polic
22
24
  import { findPartialOwnershipOverlap } from './export-shared.js';
23
25
  function buildFilenameForPlacement(category, name, order, width) {
24
26
  const padded = String(order).padStart(Math.max(3, width), '0');
25
- return `${padded}-${category}-${sanitizeName(name)}.patch`;
27
+ return `${padded}-${category}-${patchNameSlug(name, category)}.patch`;
26
28
  }
27
29
  function prefixWidthForPatches(manifestPatches, requestedOrder) {
28
30
  return manifestPatches.reduce((width, patch) => {
@@ -113,7 +115,9 @@ export function computeExactPlacementPlan(manifestPatches, newPatchCategory, new
113
115
  * Resolves a placement plan from CLI flags against the current manifest.
114
116
  */
115
117
  export async function resolvePlacementPlan(patchesDir, options, category, name) {
116
- const manifest = await loadPatchesManifest(patchesDir);
118
+ // ForWrite: placement planning feeds a manifest rewrite; a corrupt
119
+ // manifest read as empty would allocate colliding orders.
120
+ const manifest = await loadPatchesManifestForWrite(patchesDir);
117
121
  const existingPatches = manifest?.patches ?? [];
118
122
  let targetOrder;
119
123
  if (options.order !== undefined) {
@@ -232,7 +236,7 @@ export async function commitPlacementExport(input) {
232
236
  're-run the export with an explicit file list that leaves those files with their owner — ' +
233
237
  'do NOT bypass with --force-unsafe. Pass --force-unsafe only for a reviewed placement conflict.', '--force-unsafe');
234
238
  }
235
- const originalManifest = await loadPatchesManifest(input.patchesDir);
239
+ const originalManifest = await loadPatchesManifestForWrite(input.patchesDir);
236
240
  if (originalManifest !== null) {
237
241
  assertPlacementPreservesReservedRanges(currentPlan, originalManifest.patches, input.config, input.category);
238
242
  }
@@ -269,7 +273,9 @@ export async function commitPlacementExport(input) {
269
273
  await renumberPatchesInManifest(input.patchesDir, currentPlan.renameMap);
270
274
  renumberApplied = true;
271
275
  }
272
- await writeText(patchPath, input.diff);
276
+ // Normalize identically to commitExportedPatch — the two export
277
+ // paths must produce one artifact contract for the same diff.
278
+ await writeText(patchPath, normalizePatchArtifact(input.diff));
273
279
  await addPatchToManifest(input.patchesDir, {
274
280
  ...input.metadata,
275
281
  filename: currentPlan.newFilename,
@@ -414,4 +420,3 @@ export async function renderDryRunPreview(input) {
414
420
  info('[dry-run] No cross-patch ownership overlap detected.');
415
421
  }
416
422
  }
417
- //# sourceMappingURL=export-flow.js.map
@@ -105,4 +105,3 @@ export async function gatePlacementPlan(args) {
105
105
  }
106
106
  return placementPlan;
107
107
  }
108
- //# sourceMappingURL=export-placement-gate.js.map
@@ -51,4 +51,3 @@ export function assertPlacementPreservesReservedRanges(plan, manifestPatches, co
51
51
  suggestionText, 'export placement');
52
52
  }
53
53
  }
54
- //# sourceMappingURL=export-placement-policy.js.map
@@ -56,9 +56,13 @@ export declare function confirmSupersedePatches(patchesDir: string, filesAffecte
56
56
  * @param diffContent - Current unified diff
57
57
  * @param config - Project configuration
58
58
  * @param isInteractive - Whether interactive prompts are available
59
+ * @param dryRun - When true, only REPORT missing headers, never prompt or
60
+ * write. Dry-run must stay read-only: before this flag existed, an
61
+ * interactive `export --dry-run` prompted (default Yes) and wrote license
62
+ * headers into engine/ files, then closed with "no changes made".
59
63
  * @returns true if files were modified on disk (caller must regenerate diff)
60
64
  */
61
- export declare function autoFixLicenseHeaders(engineDir: string, diffContent: string, config: FireForgeConfig, isInteractive: boolean): Promise<boolean>;
65
+ export declare function autoFixLicenseHeaders(engineDir: string, diffContent: string, config: FireForgeConfig, isInteractive: boolean, dryRun?: boolean): Promise<boolean>;
62
66
  /**
63
67
  * Maps every file in `filesAffected` to the existing patches that already
64
68
  * claim ownership of it, excluding the caller's own patch (when `newFilename`
@@ -1,9 +1,9 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { join } from 'node:path';
3
3
  import { confirm, select, text } from '@clack/prompts';
4
- import { addLicenseHeaderToFile, getLicenseHeader } from '../core/license-headers.js';
4
+ import { addLicenseHeaderToFile } from '../core/license-headers.js';
5
5
  import { findAllPatchesForFiles } from '../core/patch-export.js';
6
- import { commentStyleForFile, detectNewFilesInDiff, lintExportedPatch, resolvePatchSizeTier, } from '../core/patch-lint.js';
6
+ import { commentStyleForFile, detectNewFilesInDiff, isAcceptableNewFileHeader, lintExportedPatch, resolvePatchSizeTier, } from '../core/patch-lint.js';
7
7
  import { resolvePatchOwnedSysMjs } from '../core/patch-lint-ownership.js';
8
8
  import { loadPatchesManifest } from '../core/patch-manifest.js';
9
9
  import { getPatchPolicyCategories, isCategoryAllowedByConfig } from '../core/patch-policy.js';
@@ -206,9 +206,13 @@ export async function confirmSupersedePatches(patchesDir, filesAffected, superse
206
206
  * @param diffContent - Current unified diff
207
207
  * @param config - Project configuration
208
208
  * @param isInteractive - Whether interactive prompts are available
209
+ * @param dryRun - When true, only REPORT missing headers, never prompt or
210
+ * write. Dry-run must stay read-only: before this flag existed, an
211
+ * interactive `export --dry-run` prompted (default Yes) and wrote license
212
+ * headers into engine/ files, then closed with "no changes made".
209
213
  * @returns true if files were modified on disk (caller must regenerate diff)
210
214
  */
211
- export async function autoFixLicenseHeaders(engineDir, diffContent, config, isInteractive) {
215
+ export async function autoFixLicenseHeaders(engineDir, diffContent, config, isInteractive, dryRun = false) {
212
216
  const license = config.license ?? 'MPL-2.0';
213
217
  const newFiles = detectNewFilesInDiff(diffContent);
214
218
  if (newFiles.size === 0)
@@ -222,13 +226,22 @@ export async function autoFixLicenseHeaders(engineDir, diffContent, config, isIn
222
226
  if (!(await pathExists(filePath)))
223
227
  continue;
224
228
  const content = await readText(filePath);
225
- const expectedHeader = getLicenseHeader(license, style);
226
- if (!content.startsWith(expectedHeader)) {
229
+ // Same acceptance policy as the missing-license-header rule: offering
230
+ // to "fix" a file the lint already accepts (e.g. a verbatim upstream
231
+ // MPL block header on a derived JS/CSS file) would stack a second
232
+ // header on top of a legitimate one.
233
+ if (!isAcceptableNewFileHeader(file, content, style, license)) {
227
234
  filesToFix.push(file);
228
235
  }
229
236
  }
230
237
  if (filesToFix.length === 0)
231
238
  return false;
239
+ if (dryRun) {
240
+ const fileList = filesToFix.map((f) => ` - ${f}`).join('\n');
241
+ info(`[dry-run] ${filesToFix.length} new file(s) missing the ${license} license header ` +
242
+ `(a real export would offer to add them):\n${fileList}`);
243
+ return false;
244
+ }
232
245
  if (!isInteractive)
233
246
  return false;
234
247
  const fileList = filesToFix.map((f) => ` - ${f}`).join('\n');
@@ -360,4 +373,3 @@ export async function runSupersedeAndOverlapGates(args) {
360
373
  s,
361
374
  });
362
375
  }
363
- //# sourceMappingURL=export-shared.js.map
@@ -1,9 +1,10 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { stat } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
- import { Option } from 'commander';
4
+ import { Command, Option } from 'commander';
5
5
  import { getProjectPaths, loadConfig } from '../core/config.js';
6
6
  import { appendHistory } from '../core/destructive.js';
7
+ import { withEngineSessionLock } from '../core/engine-session-lock.js';
7
8
  import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
8
9
  import { getStatusWithCodes, isGitRepository } from '../core/git.js';
9
10
  import { generateBinaryFilePatch, generateFullFilePatch } from '../core/git-diff.js';
@@ -18,7 +19,7 @@ import { GeneralError, InvalidArgumentError } from '../errors/base.js';
18
19
  import { toError } from '../utils/errors.js';
19
20
  import { ensureDir, pathExists } from '../utils/fs.js';
20
21
  import { info, intro, outro, spinner, verbose, warn } from '../utils/logger.js';
21
- import { pickDefined } from '../utils/options.js';
22
+ import { commanderArgParser, pickDefined } from '../utils/options.js';
22
23
  import { stripEnginePrefix } from '../utils/paths.js';
23
24
  import { parsePositiveIntegerFlag } from '../utils/validation.js';
24
25
  import { commitPlacementExport, renderDryRunPreview } from './export-flow.js';
@@ -209,12 +210,16 @@ async function prepareExport(projectRoot, files, options) {
209
210
  }
210
211
  throw new GeneralError('The specified paths have no diff content to export.');
211
212
  }
212
- // Ensure patches directory exists
213
- await ensureDir(paths.patches);
213
+ // Ensure patches directory exists. Skip during a dry-run so the command
214
+ // is purely read-only (matching export-all's dry-run contract).
215
+ if (!options.dryRun) {
216
+ await ensureDir(paths.patches);
217
+ }
214
218
  const config = await loadConfig(projectRoot);
215
219
  const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
216
- // Auto-fix missing license headers on new files (interactive only)
217
- const headersAdded = await autoFixLicenseHeaders(paths.engine, diff, config, isInteractive);
220
+ // Auto-fix missing license headers on new files (interactive only;
221
+ // report-only under --dry-run so the preview never mutates engine/)
222
+ const headersAdded = await autoFixLicenseHeaders(paths.engine, diff, config, isInteractive, options.dryRun ?? false);
218
223
  if (headersAdded) {
219
224
  diff = await generatePatchDiff(paths.engine, allFiles);
220
225
  }
@@ -398,7 +403,7 @@ export function registerExport(program, { getProjectRoot, withErrorHandling }) {
398
403
  .option('--supersede', 'Allow superseding multiple existing patches')
399
404
  .option('--skip-lint', 'Skip patch lint checks (downgrade errors to warnings)')
400
405
  .option('--dry-run', 'Print the export plan (including supersede preview) without writing')
401
- .addOption(new Option('--order <N>', 'Place the new patch at this exact unused order without renumbering existing patches').argParser((v) => parsePositiveIntegerFlag('--order', v)))
406
+ .addOption(new Option('--order <N>', 'Place the new patch at this exact unused order without renumbering existing patches').argParser(commanderArgParser((v) => parsePositiveIntegerFlag('--order', v))))
402
407
  .option('--before <anchor>', 'Place the new patch immediately before <anchor>')
403
408
  .option('--after <anchor>', 'Place the new patch immediately after <anchor>')
404
409
  .option('-y, --yes', 'Skip confirmation for placement renumbers (required for non-TTY)')
@@ -409,12 +414,12 @@ export function registerExport(program, { getProjectRoot, withErrorHandling }) {
409
414
  .option('--lint-ignore <check-id>', 'Suppress a lint check on this patch (writes to PatchMetadata.lintIgnore; repeatable)', (value, prev) => [...prev, value], [])
410
415
  .action(withErrorHandling(async (paths, options) => {
411
416
  const { category, tier, lintIgnore, ...rest } = options;
412
- await exportCommand(getProjectRoot(), paths, {
417
+ const projectRoot = getProjectRoot();
418
+ await withEngineSessionLock(projectRoot, 'export', () => exportCommand(projectRoot, paths, {
413
419
  ...pickDefined(rest),
414
420
  ...(category !== undefined ? { category } : {}),
415
421
  ...(tier !== undefined ? { tier: tier } : {}),
416
422
  ...(lintIgnore !== undefined && lintIgnore.length > 0 ? { lintIgnore } : {}),
417
- });
423
+ }));
418
424
  }));
419
425
  }
420
- //# sourceMappingURL=export.js.map
@@ -6,6 +6,8 @@ import { logApplyResult } from '../../core/furnace-apply-output.js';
6
6
  import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, } from '../../core/furnace-config.js';
7
7
  import { isComponentSourceFile } from '../../core/furnace-constants.js';
8
8
  import { runFurnaceMutation } from '../../core/furnace-operation.js';
9
+ import { getPersistableAppliedEntry, persistSingleComponentState, shouldPersistSingleComponentState, } from '../../core/furnace-state-persist.js';
10
+ import { countEntriesWithBlockingStepErrors } from '../../core/furnace-step-errors.js';
9
11
  import { findOverrideBaseVersionDrift, formatOverrideBaseVersionDriftError, formatOverrideBaseVersionDriftWarning, } from '../../core/furnace-version-drift.js';
10
12
  import { FurnaceError } from '../../errors/furnace.js';
11
13
  import { pathExists } from '../../utils/fs.js';
@@ -282,17 +284,32 @@ export async function furnaceApplyCommand(projectRoot, name, options = {}) {
282
284
  throw new FurnaceError(formatOverrideBaseVersionDriftError(driftEntries));
283
285
  }
284
286
  const applySpinner = dryRun ? undefined : spinner('Applying components to engine...');
285
- const result = await runFurnaceMutation(projectRoot, 'apply-rollback', (ctx) => applyAllComponents(projectRoot, dryRun, {
286
- operationContext: ctx,
287
- ...(name !== undefined ? { componentName: name } : {}),
288
- }), { dryRun });
287
+ const result = await runFurnaceMutation(projectRoot, 'apply-rollback', async (ctx) => {
288
+ // `persistState: false` for a NAMED apply is load-bearing: the batch
289
+ // persist path replaces `appliedChecksums` wholesale with only this
290
+ // run's entries, and the batch loops filter to the named component —
291
+ // so routing a named apply through it persisted a state file
292
+ // containing ONLY that component, wiping every other component's
293
+ // checksums. Orphan detection and deleted-file undeploy both key on
294
+ // that state, so the wiped components' stale engine files became
295
+ // invisible to apply AND to `furnace validate`. Named apply merges
296
+ // per-component state below, exactly like `furnace deploy <name>`.
297
+ const applyResult = await applyAllComponents(projectRoot, dryRun, {
298
+ operationContext: ctx,
299
+ ...(name !== undefined ? { componentName: name, persistState: false } : {}),
300
+ });
301
+ if (name !== undefined && shouldPersistSingleComponentState(applyResult, dryRun)) {
302
+ await persistSingleComponentState(projectRoot, getPersistableAppliedEntry('Apply', name, applyResult.applied[0]), getFurnacePaths(projectRoot));
303
+ }
304
+ return applyResult;
305
+ }, { dryRun });
289
306
  if (applySpinner) {
290
307
  applySpinner.stop('Components applied');
291
308
  }
292
309
  logApplyResult(result, dryRun);
293
310
  const appliedWithStepErrorsCount = dryRun
294
311
  ? 0
295
- : result.applied.filter((entry) => (entry.stepErrors?.length ?? 0) > 0).length;
312
+ : countEntriesWithBlockingStepErrors(result.applied);
296
313
  const totalApplyFailures = result.errors.length + appliedWithStepErrorsCount;
297
314
  if (totalApplyFailures > 0) {
298
315
  throw new FurnaceError(`${totalApplyFailures} component${totalApplyFailures === 1 ? '' : 's'} failed to apply cleanly`);
@@ -310,4 +327,3 @@ export async function furnaceApplyCommand(projectRoot, name, options = {}) {
310
327
  await runWatchLoop(projectRoot);
311
328
  }
312
329
  }
313
- //# sourceMappingURL=apply.js.map
@@ -139,4 +139,3 @@ export async function furnaceChromeDocRemoveCommand(projectRoot, name, options =
139
139
  }
140
140
  outro('Chrome document removed');
141
141
  }
142
- //# sourceMappingURL=chrome-doc-remove.js.map
@@ -329,4 +329,3 @@ export function localeJarMnEntryForChromeDoc(name) {
329
329
  export function localesFtlWildcardCapturesScaffoldedName(jarMnContents) {
330
330
  return /\(%browser\/(?:\*\*\/)?\*\.ftl\)/.test(jarMnContents);
331
331
  }
332
- //# sourceMappingURL=chrome-doc-templates.js.map
@@ -149,4 +149,3 @@ firefox-appdir = "browser"
149
149
  ["${chromeDocPackagingTestFileName(name)}"]
150
150
  `;
151
151
  }
152
- //# sourceMappingURL=chrome-doc-tests.js.map
@@ -359,4 +359,3 @@ export async function furnaceChromeDocCreateCommand(projectRoot, name, options =
359
359
  ].join('\n'), name);
360
360
  outro('Chrome document created');
361
361
  }
362
- //# sourceMappingURL=chrome-doc.js.map
@@ -177,4 +177,3 @@ add_task(async function test_${underscored}_defined() {
177
177
  }
178
178
  return testFiles;
179
179
  }
180
- //# sourceMappingURL=create-browser-test.js.map
@@ -103,4 +103,3 @@ export function formatDryRunPlan(args) {
103
103
  }
104
104
  return plan;
105
105
  }
106
- //# sourceMappingURL=create-dry-run.js.map
@@ -53,4 +53,3 @@ export async function resolveCreateFeatures(isInteractive, options) {
53
53
  }
54
54
  return { localized, register };
55
55
  }
56
- //# sourceMappingURL=create-features.js.map
@@ -74,4 +74,3 @@ export async function scaffoldMochikitTestFiles(componentName, license, paths, j
74
74
  'Ensure `toolkit/content/tests/widgets/chrome.toml` is reachable from an existing test-harness registration (upstream TEST_HARNESS_FILES entries handle this by default).');
75
75
  return writtenFiles;
76
76
  }
77
- //# sourceMappingURL=create-mochikit.js.map
@@ -31,4 +31,3 @@ export async function assertCustomEntryPersisted(projectRoot, componentName) {
31
31
  're-run the command, or add the entry to furnace.json by hand.', componentName);
32
32
  }
33
33
  }
34
- //# sourceMappingURL=create-readback.js.map
@@ -293,4 +293,3 @@ export function generateMochikitChromeTomlSkeleton(header) {
293
293
 
294
294
  `;
295
295
  }
296
- //# sourceMappingURL=create-templates.js.map
@@ -57,4 +57,3 @@ export function validateCreateAgainstConfig(config, componentName, allowPrefixMi
57
57
  }
58
58
  validateComposesTargets(config, componentName, composes, stockAdditions);
59
59
  }
60
- //# sourceMappingURL=create-validation.js.map
@@ -78,4 +78,3 @@ export async function scaffoldXpcshellTestFiles(componentName, license, forgeCon
78
78
  'Add the directory to XPCSHELL_TESTS_MANIFESTS in the nearest moz.build to run it via "fireforge test".');
79
79
  return testFiles;
80
80
  }
81
- //# sourceMappingURL=create-xpcshell.js.map
@@ -381,4 +381,3 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
381
381
  }), componentName);
382
382
  outro('Component created');
383
383
  }
384
- //# sourceMappingURL=create.js.map