@hominis/fireforge 0.49.1 → 0.50.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 (336) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +1 -1
  3. package/dist/bin/fireforge.js +6 -0
  4. package/dist/build-info.json +4 -4
  5. package/dist/src/commands/build-prelock.d.ts +20 -0
  6. package/dist/src/commands/build-prelock.js +50 -0
  7. package/dist/src/commands/build.d.ts +3 -1
  8. package/dist/src/commands/build.js +13 -6
  9. package/dist/src/commands/check-report.d.ts +28 -0
  10. package/dist/src/commands/check-report.js +78 -0
  11. package/dist/src/commands/check-run.d.ts +28 -0
  12. package/dist/src/commands/check-run.js +104 -0
  13. package/dist/src/commands/check-steps.d.ts +67 -0
  14. package/dist/src/commands/check-steps.js +175 -0
  15. package/dist/src/commands/check.d.ts +32 -0
  16. package/dist/src/commands/check.js +160 -0
  17. package/dist/src/commands/config.js +5 -6
  18. package/dist/src/commands/doctor-engine-eslint.js +6 -1
  19. package/dist/src/commands/doctor-source-pin.js +4 -1
  20. package/dist/src/commands/export-adopt-check.d.ts +38 -0
  21. package/dist/src/commands/export-adopt-check.js +98 -0
  22. package/dist/src/commands/export-flow.js +29 -14
  23. package/dist/src/commands/export-placement-gate.d.ts +6 -2
  24. package/dist/src/commands/export-shared.d.ts +2 -1
  25. package/dist/src/commands/export-staged-registration.d.ts +46 -0
  26. package/dist/src/commands/export-staged-registration.js +91 -0
  27. package/dist/src/commands/forward-import-remedy.d.ts +32 -0
  28. package/dist/src/commands/forward-import-remedy.js +63 -0
  29. package/dist/src/commands/furnace/apply.js +3 -3
  30. package/dist/src/commands/furnace/chrome-doc-remove.d.ts +2 -1
  31. package/dist/src/commands/furnace/chrome-doc-remove.js +2 -2
  32. package/dist/src/commands/furnace/chrome-doc.d.ts +2 -1
  33. package/dist/src/commands/furnace/chrome-doc.js +2 -2
  34. package/dist/src/commands/furnace/create-templates.d.ts +8 -3
  35. package/dist/src/commands/furnace/create-templates.js +8 -23
  36. package/dist/src/commands/furnace/create.js +2 -2
  37. package/dist/src/commands/furnace/deploy-named.d.ts +48 -0
  38. package/dist/src/commands/furnace/deploy-named.js +107 -0
  39. package/dist/src/commands/furnace/deploy.d.ts +3 -2
  40. package/dist/src/commands/furnace/deploy.js +50 -86
  41. package/dist/src/commands/furnace/diff.js +26 -3
  42. package/dist/src/commands/furnace/index.js +66 -41
  43. package/dist/src/commands/furnace/init.js +13 -6
  44. package/dist/src/commands/furnace/override.js +4 -2
  45. package/dist/src/commands/furnace/preview.js +4 -3
  46. package/dist/src/commands/furnace/refresh.js +2 -2
  47. package/dist/src/commands/furnace/remove.js +4 -3
  48. package/dist/src/commands/furnace/rename-dry-run.d.ts +29 -0
  49. package/dist/src/commands/furnace/rename-dry-run.js +127 -0
  50. package/dist/src/commands/furnace/rename-helpers.d.ts +7 -0
  51. package/dist/src/commands/furnace/rename-helpers.js +15 -1
  52. package/dist/src/commands/furnace/rename-registrations.d.ts +41 -0
  53. package/dist/src/commands/furnace/rename-registrations.js +93 -0
  54. package/dist/src/commands/furnace/rename.d.ts +2 -1
  55. package/dist/src/commands/furnace/rename.js +28 -66
  56. package/dist/src/commands/furnace/scan.d.ts +9 -4
  57. package/dist/src/commands/furnace/scan.js +11 -9
  58. package/dist/src/commands/furnace/sync.js +3 -2
  59. package/dist/src/commands/furnace/validate-report.d.ts +45 -0
  60. package/dist/src/commands/furnace/validate-report.js +91 -0
  61. package/dist/src/commands/furnace/validate.d.ts +1 -1
  62. package/dist/src/commands/furnace/validate.js +127 -114
  63. package/dist/src/commands/furnace/validation-output.js +14 -3
  64. package/dist/src/commands/lint-per-patch.js +31 -60
  65. package/dist/src/commands/lint-report.d.ts +8 -4
  66. package/dist/src/commands/lint-report.js +15 -5
  67. package/dist/src/commands/lint.js +15 -34
  68. package/dist/src/commands/manifest.js +2 -0
  69. package/dist/src/commands/patch/delete-gap-notice.js +3 -1
  70. package/dist/src/commands/patch/delete.d.ts +1 -1
  71. package/dist/src/commands/patch/delete.js +13 -98
  72. package/dist/src/commands/patch/index.d.ts +1 -1
  73. package/dist/src/commands/patch/index.js +4 -2
  74. package/dist/src/commands/patch/lint-ignore.d.ts +5 -1
  75. package/dist/src/commands/patch/lint-ignore.js +65 -28
  76. package/dist/src/commands/patch/move-files-create.js +26 -17
  77. package/dist/src/commands/patch/move-files-into.js +5 -3
  78. package/dist/src/commands/patch/move-files.js +3 -1
  79. package/dist/src/commands/patch/removal-dependents.d.ts +63 -0
  80. package/dist/src/commands/patch/removal-dependents.js +133 -0
  81. package/dist/src/commands/patch/rename.js +7 -0
  82. package/dist/src/commands/patch/retire.d.ts +34 -0
  83. package/dist/src/commands/patch/retire.js +128 -0
  84. package/dist/src/commands/patch/split-plan.d.ts +7 -7
  85. package/dist/src/commands/patch/split-plan.js +45 -28
  86. package/dist/src/commands/patch/split-staged.d.ts +108 -0
  87. package/dist/src/commands/patch/split-staged.js +202 -0
  88. package/dist/src/commands/patch/split.d.ts +5 -0
  89. package/dist/src/commands/patch/split.js +51 -15
  90. package/dist/src/commands/patch/staged-dependency-batch.d.ts +48 -0
  91. package/dist/src/commands/patch/staged-dependency-batch.js +176 -0
  92. package/dist/src/commands/patch/staged-dependency-entries.d.ts +36 -0
  93. package/dist/src/commands/patch/staged-dependency-entries.js +52 -0
  94. package/dist/src/commands/patch/staged-dependency-from.d.ts +14 -0
  95. package/dist/src/commands/patch/staged-dependency-from.js +108 -0
  96. package/dist/src/commands/patch/staged-dependency.js +14 -41
  97. package/dist/src/commands/re-export-adjacent-registrations.d.ts +20 -0
  98. package/dist/src/commands/re-export-adjacent-registrations.js +75 -0
  99. package/dist/src/commands/re-export-adjacent.d.ts +45 -2
  100. package/dist/src/commands/re-export-adjacent.js +156 -37
  101. package/dist/src/commands/re-export-drift.d.ts +36 -8
  102. package/dist/src/commands/re-export-drift.js +73 -49
  103. package/dist/src/commands/re-export-files.js +11 -8
  104. package/dist/src/commands/re-export-lint.d.ts +2 -1
  105. package/dist/src/commands/re-export-lint.js +23 -10
  106. package/dist/src/commands/re-export-options.js +45 -4
  107. package/dist/src/commands/re-export-preflight.d.ts +23 -0
  108. package/dist/src/commands/re-export-preflight.js +160 -0
  109. package/dist/src/commands/re-export-register.js +42 -17
  110. package/dist/src/commands/re-export-removed.d.ts +12 -1
  111. package/dist/src/commands/re-export-removed.js +41 -11
  112. package/dist/src/commands/re-export-scan-owner.d.ts +44 -0
  113. package/dist/src/commands/re-export-scan-owner.js +116 -0
  114. package/dist/src/commands/re-export-scan.d.ts +17 -9
  115. package/dist/src/commands/re-export-scan.js +56 -23
  116. package/dist/src/commands/re-export-single.js +22 -6
  117. package/dist/src/commands/re-export-summary.d.ts +28 -0
  118. package/dist/src/commands/re-export-summary.js +36 -0
  119. package/dist/src/commands/re-export.js +37 -24
  120. package/dist/src/commands/rebase/index.js +17 -17
  121. package/dist/src/commands/run.js +16 -2
  122. package/dist/src/commands/status-check.d.ts +55 -16
  123. package/dist/src/commands/status-check.js +92 -37
  124. package/dist/src/commands/status-json.d.ts +22 -9
  125. package/dist/src/commands/status-json.js +47 -32
  126. package/dist/src/commands/status-output.js +3 -1
  127. package/dist/src/commands/status-scope.d.ts +37 -0
  128. package/dist/src/commands/status-scope.js +54 -0
  129. package/dist/src/commands/status.js +66 -23
  130. package/dist/src/commands/test-browser-preflight.js +19 -3
  131. package/dist/src/commands/test-build-furnace.d.ts +21 -0
  132. package/dist/src/commands/test-build-furnace.js +48 -0
  133. package/dist/src/commands/test-build-phase.js +47 -41
  134. package/dist/src/commands/test-checkjs.js +7 -0
  135. package/dist/src/commands/test-diagnose.js +7 -0
  136. package/dist/src/commands/test-modes.d.ts +16 -1
  137. package/dist/src/commands/test-modes.js +24 -1
  138. package/dist/src/commands/test-multi-verdict.d.ts +61 -0
  139. package/dist/src/commands/test-multi-verdict.js +111 -0
  140. package/dist/src/commands/test-orchestrate.d.ts +43 -0
  141. package/dist/src/commands/test-orchestrate.js +222 -0
  142. package/dist/src/commands/test-perf-scope.d.ts +29 -0
  143. package/dist/src/commands/test-perf-scope.js +83 -0
  144. package/dist/src/commands/test-prelock.d.ts +51 -0
  145. package/dist/src/commands/test-prelock.js +177 -0
  146. package/dist/src/commands/test-register.js +54 -10
  147. package/dist/src/commands/test-run.js +4 -1
  148. package/dist/src/commands/test-stale-gate.js +57 -27
  149. package/dist/src/commands/test-verdict.d.ts +10 -1
  150. package/dist/src/commands/test-verdict.js +28 -1
  151. package/dist/src/commands/test.d.ts +6 -1
  152. package/dist/src/commands/test.js +66 -60
  153. package/dist/src/commands/token-lint.d.ts +14 -0
  154. package/dist/src/commands/token-lint.js +111 -0
  155. package/dist/src/commands/token.js +10 -0
  156. package/dist/src/commands/verify-scope.d.ts +28 -0
  157. package/dist/src/commands/verify-scope.js +67 -0
  158. package/dist/src/commands/verify.d.ts +10 -2
  159. package/dist/src/commands/verify.js +59 -35
  160. package/dist/src/core/build-baseline-types.d.ts +46 -0
  161. package/dist/src/core/build-baseline-validate.js +19 -0
  162. package/dist/src/core/build-baseline.d.ts +5 -1
  163. package/dist/src/core/build-baseline.js +3 -1
  164. package/dist/src/core/build-overwrite-guard.d.ts +50 -0
  165. package/dist/src/core/build-overwrite-guard.js +84 -4
  166. package/dist/src/core/build-prepare.d.ts +7 -0
  167. package/dist/src/core/build-prepare.js +14 -8
  168. package/dist/src/core/build-run-record.d.ts +74 -0
  169. package/dist/src/core/build-run-record.js +136 -0
  170. package/dist/src/core/checkjs-support-heads.d.ts +41 -0
  171. package/dist/src/core/checkjs-support-heads.js +104 -0
  172. package/dist/src/core/config-paths.d.ts +2 -2
  173. package/dist/src/core/config-paths.js +8 -0
  174. package/dist/src/core/config-validate-check.d.ts +8 -0
  175. package/dist/src/core/config-validate-check.js +143 -0
  176. package/dist/src/core/config-validate-file-size.d.ts +9 -6
  177. package/dist/src/core/config-validate-file-size.js +91 -2
  178. package/dist/src/core/config-validate-history.d.ts +11 -0
  179. package/dist/src/core/config-validate-history.js +38 -0
  180. package/dist/src/core/config-validate-patch-policy.js +100 -0
  181. package/dist/src/core/config-validate-test-toolchains.js +7 -0
  182. package/dist/src/core/config-validate-token-scales.d.ts +6 -0
  183. package/dist/src/core/config-validate-token-scales.js +50 -0
  184. package/dist/src/core/config-validate.js +14 -5
  185. package/dist/src/core/config.d.ts +5 -0
  186. package/dist/src/core/config.js +16 -0
  187. package/dist/src/core/coverage-dispatch.d.ts +24 -0
  188. package/dist/src/core/coverage-dispatch.js +44 -0
  189. package/dist/src/core/coverage-extend.d.ts +70 -45
  190. package/dist/src/core/coverage-extend.js +199 -144
  191. package/dist/src/core/coverage-records.d.ts +97 -0
  192. package/dist/src/core/coverage-records.js +350 -0
  193. package/dist/src/core/destructive.d.ts +7 -4
  194. package/dist/src/core/destructive.js +10 -10
  195. package/dist/src/core/engine-session-lock.d.ts +10 -1
  196. package/dist/src/core/engine-session-lock.js +28 -8
  197. package/dist/src/core/export-moved-code-shape.d.ts +49 -11
  198. package/dist/src/core/export-moved-code-shape.js +135 -44
  199. package/dist/src/core/file-lock-queue.d.ts +67 -0
  200. package/dist/src/core/file-lock-queue.js +221 -0
  201. package/dist/src/core/file-lock.d.ts +4 -25
  202. package/dist/src/core/file-lock.js +105 -138
  203. package/dist/src/core/furnace-apply-output.d.ts +10 -0
  204. package/dist/src/core/furnace-apply-output.js +31 -1
  205. package/dist/src/core/furnace-apply.js +12 -2
  206. package/dist/src/core/furnace-composed-source.d.ts +76 -0
  207. package/dist/src/core/furnace-composed-source.js +167 -0
  208. package/dist/src/core/furnace-config-validate-exceptions.d.ts +9 -0
  209. package/dist/src/core/furnace-config-validate-exceptions.js +71 -0
  210. package/dist/src/core/furnace-config-validate.js +8 -0
  211. package/dist/src/core/furnace-css-fragments.d.ts +10 -0
  212. package/dist/src/core/furnace-css-fragments.js +26 -0
  213. package/dist/src/core/furnace-operation.d.ts +3 -3
  214. package/dist/src/core/furnace-registration-remove.d.ts +22 -1
  215. package/dist/src/core/furnace-registration-remove.js +53 -17
  216. package/dist/src/core/furnace-registration-rename.d.ts +26 -0
  217. package/dist/src/core/furnace-registration-rename.js +77 -0
  218. package/dist/src/core/furnace-registration.d.ts +14 -4
  219. package/dist/src/core/furnace-registration.js +44 -10
  220. package/dist/src/core/furnace-staleness.d.ts +7 -0
  221. package/dist/src/core/furnace-staleness.js +34 -24
  222. package/dist/src/core/furnace-validate-accessibility.d.ts +4 -2
  223. package/dist/src/core/furnace-validate-accessibility.js +10 -1
  224. package/dist/src/core/furnace-validate-exceptions.d.ts +40 -0
  225. package/dist/src/core/furnace-validate-exceptions.js +43 -0
  226. package/dist/src/core/furnace-validate-icon-button.d.ts +7 -0
  227. package/dist/src/core/furnace-validate-icon-button.js +49 -0
  228. package/dist/src/core/furnace-validate.js +1 -1
  229. package/dist/src/core/harness-port-fallback.d.ts +60 -0
  230. package/dist/src/core/harness-port-fallback.js +96 -0
  231. package/dist/src/core/history-log-file.d.ts +61 -0
  232. package/dist/src/core/history-log-file.js +157 -0
  233. package/dist/src/core/history-log.d.ts +2 -1
  234. package/dist/src/core/history-log.js +2 -1
  235. package/dist/src/core/json-preserving-edit.d.ts +22 -0
  236. package/dist/src/core/json-preserving-edit.js +199 -0
  237. package/dist/src/core/mach.js +8 -1
  238. package/dist/src/core/negative-control-diff.d.ts +39 -0
  239. package/dist/src/core/negative-control-diff.js +136 -0
  240. package/dist/src/core/negative-control.d.ts +138 -0
  241. package/dist/src/core/negative-control.js +356 -0
  242. package/dist/src/core/objdir-install-manifests.d.ts +73 -0
  243. package/dist/src/core/objdir-install-manifests.js +226 -0
  244. package/dist/src/core/patch-export-update.d.ts +2 -0
  245. package/dist/src/core/patch-export-update.js +1 -0
  246. package/dist/src/core/patch-export.js +3 -4
  247. package/dist/src/core/patch-lint-checkjs-any-import.d.ts +37 -0
  248. package/dist/src/core/patch-lint-checkjs-any-import.js +61 -0
  249. package/dist/src/core/patch-lint-checkjs.js +8 -11
  250. package/dist/src/core/patch-lint-code-lines.d.ts +59 -0
  251. package/dist/src/core/patch-lint-code-lines.js +105 -0
  252. package/dist/src/core/patch-lint-cross.js +12 -4
  253. package/dist/src/core/patch-lint-css.d.ts +8 -2
  254. package/dist/src/core/patch-lint-css.js +10 -4
  255. package/dist/src/core/patch-lint-file-size.d.ts +12 -5
  256. package/dist/src/core/patch-lint-file-size.js +22 -11
  257. package/dist/src/core/patch-lint-forward-registration-extended.d.ts +18 -0
  258. package/dist/src/core/patch-lint-forward-registration-extended.js +35 -10
  259. package/dist/src/core/patch-lint-forward-registration.js +10 -0
  260. package/dist/src/core/patch-lint-ignore.d.ts +152 -0
  261. package/dist/src/core/patch-lint-ignore.js +299 -0
  262. package/dist/src/core/patch-lint-size-thresholds.d.ts +52 -0
  263. package/dist/src/core/patch-lint-size-thresholds.js +110 -0
  264. package/dist/src/core/patch-lint-size.d.ts +117 -0
  265. package/dist/src/core/patch-lint-size.js +323 -0
  266. package/dist/src/core/patch-lint-staged-registration.d.ts +18 -1
  267. package/dist/src/core/patch-lint-staged-registration.js +20 -0
  268. package/dist/src/core/patch-lint-token-scale.d.ts +39 -0
  269. package/dist/src/core/patch-lint-token-scale.js +109 -0
  270. package/dist/src/core/patch-lint.d.ts +11 -83
  271. package/dist/src/core/patch-lint.js +64 -286
  272. package/dist/src/core/patch-lock.js +7 -6
  273. package/dist/src/core/patch-manifest-consistency.js +7 -6
  274. package/dist/src/core/patch-manifest-io.d.ts +6 -3
  275. package/dist/src/core/patch-manifest-io.js +19 -8
  276. package/dist/src/core/patch-manifest-validate.js +34 -2
  277. package/dist/src/core/patch-order-legal.d.ts +25 -0
  278. package/dist/src/core/patch-order-legal.js +83 -0
  279. package/dist/src/core/patch-policy-content.d.ts +40 -0
  280. package/dist/src/core/patch-policy-content.js +216 -0
  281. package/dist/src/core/patch-policy.d.ts +16 -3
  282. package/dist/src/core/patch-policy.js +16 -9
  283. package/dist/src/core/patch-retired.d.ts +35 -0
  284. package/dist/src/core/patch-retired.js +54 -0
  285. package/dist/src/core/patch-subset.d.ts +16 -0
  286. package/dist/src/core/patch-subset.js +66 -0
  287. package/dist/src/core/preflight-wait.d.ts +20 -0
  288. package/dist/src/core/preflight-wait.js +38 -0
  289. package/dist/src/core/rebase-from-version.d.ts +46 -0
  290. package/dist/src/core/rebase-from-version.js +71 -0
  291. package/dist/src/core/run-log.d.ts +20 -0
  292. package/dist/src/core/run-log.js +89 -3
  293. package/dist/src/core/run-profile.d.ts +7 -0
  294. package/dist/src/core/run-profile.js +7 -1
  295. package/dist/src/core/smoke-patterns.d.ts +5 -0
  296. package/dist/src/core/smoke-patterns.js +13 -0
  297. package/dist/src/core/test-harness-crash.d.ts +18 -1
  298. package/dist/src/core/test-harness-crash.js +6 -4
  299. package/dist/src/core/test-harness-signature.d.ts +17 -0
  300. package/dist/src/core/test-harness-signature.js +4 -0
  301. package/dist/src/core/test-run-completeness.d.ts +57 -0
  302. package/dist/src/core/test-run-completeness.js +144 -0
  303. package/dist/src/core/test-stale-check.d.ts +27 -1
  304. package/dist/src/core/test-stale-check.js +39 -49
  305. package/dist/src/core/test-stale-exempt.d.ts +40 -0
  306. package/dist/src/core/test-stale-exempt.js +70 -0
  307. package/dist/src/core/token-lint.d.ts +50 -0
  308. package/dist/src/core/token-lint.js +126 -0
  309. package/dist/src/core/tree-guard.js +3 -0
  310. package/dist/src/core/typecheck-shim.d.ts +1 -1
  311. package/dist/src/core/typecheck-shim.js +90 -2
  312. package/dist/src/index.d.ts +8 -3
  313. package/dist/src/index.js +3 -1
  314. package/dist/src/types/commands/index.d.ts +2 -2
  315. package/dist/src/types/commands/options.d.ts +148 -21
  316. package/dist/src/types/commands/patches.d.ts +75 -1
  317. package/dist/src/types/config.d.ts +192 -0
  318. package/dist/src/types/config.js +9 -0
  319. package/dist/src/types/furnace.d.ts +43 -1
  320. package/dist/src/types/furnace.js +7 -0
  321. package/dist/src/types/index.d.ts +1 -1
  322. package/dist/src/utils/options.d.ts +34 -6
  323. package/dist/src/utils/options.js +86 -5
  324. package/dist/src/utils/path-glob.d.ts +15 -0
  325. package/dist/src/utils/path-glob.js +47 -0
  326. package/dist/src/utils/regex.d.ts +15 -3
  327. package/dist/src/utils/regex.js +37 -16
  328. package/docs/configuration.md +282 -9
  329. package/docs/exit-codes.md +18 -1
  330. package/docs/furnace.md +97 -3
  331. package/docs/lifecycle-invariants.md +52 -19
  332. package/docs/machine-output.md +74 -12
  333. package/docs/patch-workflow.md +297 -22
  334. package/docs/run-logs.md +20 -1
  335. package/docs/testing.md +276 -20
  336. package/package.json +2 -2
@@ -1,17 +1,18 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { loadConfig } from '../../core/config.js';
3
3
  import { applyAllComponents } from '../../core/furnace-apply.js';
4
- import { logApplyResult } from '../../core/furnace-apply-output.js';
4
+ import { formatSkipBreakdown, logApplyResult } from '../../core/furnace-apply-output.js';
5
5
  import { getFurnacePaths } from '../../core/furnace-config.js';
6
6
  import { reportJsconfigPathsSync } from '../../core/furnace-jsconfig.js';
7
- import { runFurnaceMutation, waitLockMutationOptions, } from '../../core/furnace-operation.js';
7
+ import { runFurnaceMutation, waitLockMutationOptions } from '../../core/furnace-operation.js';
8
8
  import { assertFurnaceReady } from '../../core/furnace-precondition.js';
9
- import { getPersistableAppliedEntry, getStepFailureCount, persistSingleComponentState, shouldPersistSingleComponentState, } from '../../core/furnace-state-persist.js';
9
+ import { getStepFailureCount } from '../../core/furnace-state-persist.js';
10
10
  import { hasBlockingStepErrors } from '../../core/furnace-step-errors.js';
11
11
  import { findOverrideBaseVersionDrift, formatOverrideBaseVersionDriftError, formatOverrideBaseVersionDriftWarning, } from '../../core/furnace-version-drift.js';
12
12
  import { FurnaceError } from '../../errors/furnace.js';
13
13
  import { info, intro, note, notice, outro, spinner, warn } from '../../utils/logger.js';
14
- import { formatFragmentIncluderNotice, mergeApplyResults, refreshStaleFragmentIncluders, } from './deploy-fragment-includers.js';
14
+ import { formatFragmentIncluderNotice } from './deploy-fragment-includers.js';
15
+ import { deployNamedComponents } from './deploy-named.js';
15
16
  import { runDeployValidation } from './validation-output.js';
16
17
  /**
17
18
  * Builds the final deploy failure summary from apply and validation error counts.
@@ -39,51 +40,6 @@ function getFailedComponentNames(result) {
39
40
  }
40
41
  return failed;
41
42
  }
42
- /**
43
- * Applies a single named override or custom component in targeted deploy
44
- * mode.
45
- *
46
- * Delegates to {@link applyAllComponents} with a `componentName` filter so
47
- * targeted deploys run the exact same pipeline as deploy-all, including
48
- * workspace-deletion detection, engine orphan undeploy, and jar.mn /
49
- * customElements.js re-sync. Calling the per-component apply helpers
50
- * directly never prunes: renaming a helper file in the workspace then leaves
51
- * the old deployed file and its stale jar.mn line in the engine.
52
- *
53
- * `persistState: false` matters here: the batch persist path replaces
54
- * `appliedChecksums` wholesale with only this run's entries, which for a
55
- * named deploy would wipe every other component's state. Named deploy keeps
56
- * its per-component state merge ({@link persistSingleComponentState}) and
57
- * its atomicity gate ({@link shouldPersistSingleComponentState}) at the call
58
- * site. Rollback on failure happens inside `applyAllComponents`. The journal
59
- * returned on success is ignored (the deploy keeps its files).
60
- *
61
- * The caller then runs the fragment-includer follow-up
62
- * ({@link refreshStaleFragmentIncluders}) ONLY after this component's state
63
- * is persisted: each includer is applied and persisted as its own unit, so
64
- * a signal mid-loop restores at most the includer in flight and leaves every
65
- * earlier unit committed. Moving the persist after the includer loop would
66
- * silently break that atomicity.
67
- *
68
- * @param name - Component name to apply
69
- * @param config - Loaded Furnace configuration
70
- * @param isDryRun - Whether file writes should be skipped
71
- * @returns Apply result for the named component, or `stock` for stock-only
72
- * entries
73
- */
74
- async function applyNamedComponent(name, config, isDryRun, projectRoot, operationContext) {
75
- if (!(name in config.overrides) && !(name in config.custom)) {
76
- if (config.stock.includes(name)) {
77
- return 'stock';
78
- }
79
- throw new FurnaceError(`Component "${name}" not found in furnace.json.`, name);
80
- }
81
- return applyAllComponents(projectRoot, isDryRun, {
82
- componentName: name,
83
- persistState: false,
84
- ...(operationContext ? { operationContext } : {}),
85
- });
86
- }
87
43
  /**
88
44
  * Prints the deploy summary after apply and validation complete.
89
45
  * @param result - Aggregate apply result
@@ -109,7 +65,7 @@ function printDeploymentSummary(result, totalErrors, totalWarnings, componentCou
109
65
  : ''), 'Dry Run Summary');
110
66
  }
111
67
  else {
112
- note(`Applied ${appliedCount}, skipped ${skippedCount}\n` +
68
+ note(`Applied ${appliedCount}, skipped ${skippedCount}${formatSkipBreakdown(result.skipped)}\n` +
113
69
  `${applyErrors} apply error(s)` +
114
70
  (stepErrorCount > 0 ? `, ${stepErrorCount} registration step error(s)` : '') +
115
71
  `\n` +
@@ -132,14 +88,25 @@ function enforceScopedOverrideVersionDriftPreflight(scopedDrift, force) {
132
88
  throw new FurnaceError(formatOverrideBaseVersionDriftError(scopedDrift));
133
89
  }
134
90
  }
91
+ /**
92
+ * Normalises the positional argument: a single name (programmatic callers)
93
+ * or the variadic list commander passes. An empty list means "deploy all".
94
+ */
95
+ function normalizeDeployNames(names) {
96
+ if (names === undefined)
97
+ return [];
98
+ return typeof names === 'string' ? [names] : [...names];
99
+ }
135
100
  /**
136
101
  * Runs the furnace deploy command: apply components then validate in one step.
137
102
  * @param projectRoot - Root directory of the project
138
- * @param name - Optional component name to deploy (deploys all if omitted)
103
+ * @param names - Component name(s) to deploy, all under one lock hold
104
+ * (deploys all when omitted or empty)
139
105
  * @param options - Command options
140
106
  */
141
- export async function furnaceDeployCommand(projectRoot, name, options = {}) {
107
+ export async function furnaceDeployCommand(projectRoot, names, options = {}) {
142
108
  const isDryRun = options.dryRun ?? false;
109
+ const requested = normalizeDeployNames(names);
143
110
  intro(isDryRun ? 'Furnace Deploy (dry run)' : 'Furnace Deploy');
144
111
  const { config } = await assertFurnaceReady(projectRoot);
145
112
  const furnacePaths = getFurnacePaths(projectRoot);
@@ -157,7 +124,9 @@ export async function furnaceDeployCommand(projectRoot, name, options = {}) {
157
124
  const forgeConfig = await loadConfig(projectRoot);
158
125
  const driftEntries = findOverrideBaseVersionDrift(config, forgeConfig.firefox.version);
159
126
  const force = options.force ?? false;
160
- const scopedDrift = name ? driftEntries.filter((entry) => entry.name === name) : driftEntries;
127
+ const scopedDrift = requested.length > 0
128
+ ? driftEntries.filter((entry) => requested.includes(entry.name))
129
+ : driftEntries;
161
130
  enforceScopedOverrideVersionDriftPreflight(scopedDrift, force);
162
131
  // --- Step 1: Apply ---
163
132
  const applySpinner = spinner(isDryRun ? 'Calculating planned actions...' : 'Applying components to engine...');
@@ -166,55 +135,37 @@ export async function furnaceDeployCommand(projectRoot, name, options = {}) {
166
135
  // phase below is read-only and runs outside the lock so two concurrent
167
136
  // `furnace deploy` runs only contend on the actual mutation.
168
137
  const applyOutcome = await runFurnaceMutation(projectRoot, 'deploy-rollback', async (ctx) => {
169
- if (name) {
170
- const namedApplyResult = await applyNamedComponent(name, config, isDryRun, projectRoot, ctx);
171
- if (namedApplyResult === 'stock') {
172
- return { kind: 'stock' };
173
- }
174
- // Named deploy is atomic: state is persisted only when every apply
175
- // step succeeded. Any rollback triggered by applyNamedComponent has
176
- // already restored the engine to its pre-deploy state, so persisting
177
- // partial checksums here would mis-report the next status/apply run
178
- // against a workspace that was never actually deployed.
179
- const primaryClean = shouldPersistSingleComponentState(namedApplyResult, isDryRun);
180
- if (primaryClean) {
181
- await persistSingleComponentState(projectRoot, getPersistableAppliedEntry('Deploy', name, namedApplyResult.applied[0]), furnacePaths);
182
- }
183
- // Other deployed includers of a fragment this component includes
184
- // would otherwise keep a stale expansion. Only after a clean,
185
- // persisted primary apply (dry-run always previews).
186
- if (!primaryClean && !isDryRun) {
187
- return { kind: 'result', result: namedApplyResult };
188
- }
189
- const includers = await refreshStaleFragmentIncluders({
138
+ if (requested.length > 0) {
139
+ return deployNamedComponents({
190
140
  projectRoot,
191
- name,
141
+ names: requested,
192
142
  config,
193
143
  furnacePaths,
194
144
  isDryRun,
195
145
  operationContext: ctx,
196
146
  });
197
- return {
198
- kind: 'result',
199
- result: mergeApplyResults(namedApplyResult, includers.results),
200
- includers,
201
- };
202
147
  }
203
148
  const allResult = await applyAllComponents(projectRoot, isDryRun, {
204
149
  operationContext: ctx,
205
150
  });
206
- return { kind: 'result', result: allResult };
151
+ return { kind: 'all', result: allResult };
207
152
  }, { dryRun: isDryRun, ...waitLockMutationOptions(options.waitLockSeconds) });
153
+ const warnStock = () => {
154
+ for (const stockName of applyOutcome.kind === 'all' ? [] : applyOutcome.stock) {
155
+ warn(`"${stockName}" is a stock component. Stock components are not applied locally.`);
156
+ }
157
+ };
208
158
  if (applyOutcome.kind === 'stock') {
209
159
  applySpinner.stop('Apply skipped');
210
- warn(`"${name}" is a stock component. Stock components are not applied locally.`);
160
+ warnStock();
211
161
  outro(isDryRun ? 'Dry run complete (no files modified)' : 'Deploy complete');
212
162
  return;
213
163
  }
214
164
  const result = applyOutcome.result;
215
165
  applySpinner.stop(isDryRun ? 'Planned actions calculated' : 'Components applied');
166
+ warnStock();
216
167
  logApplyResult(result, isDryRun);
217
- if (applyOutcome.includers) {
168
+ if (applyOutcome.kind === 'result') {
218
169
  for (const line of formatFragmentIncluderNotice(applyOutcome.includers, isDryRun)) {
219
170
  notice(line);
220
171
  }
@@ -235,20 +186,33 @@ export async function furnaceDeployCommand(projectRoot, name, options = {}) {
235
186
  }
236
187
  const validateSpinner = spinner(isDryRun ? 'Validating (read-only)...' : 'Validating...');
237
188
  const failedComponents = getFailedComponentNames(result);
189
+ // A named deploy validates every named component that applied, then the
190
+ // includers it refreshed. The first clean name leads (the validation
191
+ // runner skips the whole named branch when its lead failed), and every
192
+ // failed name is counted as skipped.
193
+ const deployed = applyOutcome.kind === 'result' ? applyOutcome.deployed : [];
194
+ const failedNamed = deployed.filter((tag) => failedComponents.has(tag));
195
+ const lead = deployed.find((tag) => !failedComponents.has(tag)) ?? deployed[0];
238
196
  const validation = await runDeployValidation({
239
197
  validateSpinner,
240
- name,
198
+ name: lead,
241
199
  config,
242
200
  furnacePaths,
243
201
  failedComponents,
244
202
  isDryRun,
245
203
  projectRoot,
246
204
  dryRunActions: result.actions,
247
- alsoValidate: applyOutcome.includers?.refreshed ?? [],
205
+ alsoValidate: applyOutcome.kind === 'result'
206
+ ? [...deployed.filter((tag) => tag !== lead), ...applyOutcome.includers.refreshed]
207
+ : [],
248
208
  });
249
209
  if (validation.done)
250
210
  return;
251
- const { totalErrors, totalWarnings, componentCount, skippedValidationCount } = validation;
211
+ const { totalErrors, totalWarnings, componentCount } = validation;
212
+ // The runner counts a failed lead itself; failed extras it drops silently.
213
+ const skippedValidationCount = lead !== undefined && failedComponents.has(lead)
214
+ ? failedNamed.length
215
+ : validation.skippedValidationCount + failedNamed.length;
252
216
  // --- Step 3: Summary ---
253
217
  printDeploymentSummary(result, totalErrors, totalWarnings, componentCount, skippedValidationCount, isDryRun);
254
218
  }
@@ -4,6 +4,8 @@ import { join } from 'node:path';
4
4
  import { getProjectPaths, loadState } from '../../core/config.js';
5
5
  import { diffLines, renderHunks } from '../../core/diff-hunks.js';
6
6
  import { getOverrideEngineTargetPath } from '../../core/furnace-apply-helpers.js';
7
+ import { normalizeForChecksum } from '../../core/furnace-checksum-utils.js';
8
+ import { composeComponentSource } from '../../core/furnace-composed-source.js';
7
9
  import { getFurnacePaths, loadFurnaceConfig } from '../../core/furnace-config.js';
8
10
  import { isComponentSourceFile, resolveFtlDir } from '../../core/furnace-constants.js';
9
11
  import { getFileContentAtRef } from '../../core/git-file-ops.js';
@@ -108,6 +110,14 @@ async function diffOverride(name, projectRoot, config) {
108
110
  * copy. Shows what would change (or has changed) on the next
109
111
  * `furnace apply`.
110
112
  *
113
+ * The workspace side is the Furnace-composed source: a stylesheet with
114
+ * `@fireforge-include` directives is compared in its fragment-expanded
115
+ * form, exactly as apply deploys it. Comparing the raw source instead
116
+ * reported every freshly deployed fragment includer as a large difference
117
+ * (the inlined fragment bodies and end markers), so the diff could not show
118
+ * whether a deploy would change anything. Line endings and a BOM are
119
+ * ignored, as apply's own drift check ignores them.
120
+ *
111
121
  * `.ftl` files deploy to `engine/<ftlDir>/<name>.ftl` via
112
122
  * `applyCustomFtlFile`, not to `customConfig.targetPath`, so the
113
123
  * deployment-target lookup has to branch on extension. Without the branch, a
@@ -136,7 +146,19 @@ async function diffCustom(name, projectRoot, config) {
136
146
  if (!isComponentSourceFile(entry.name))
137
147
  continue;
138
148
  const workspacePath = join(customDir, entry.name);
139
- const workspaceContent = await readText(workspacePath);
149
+ const rawWorkspace = await readText(workspacePath);
150
+ let workspaceContent = rawWorkspace;
151
+ let composedNote = '';
152
+ try {
153
+ workspaceContent = await composeComponentSource(rawWorkspace, entry.name, furnacePaths.sharedDir);
154
+ if (workspaceContent !== rawWorkspace)
155
+ composedNote = ' (composed: fragments expanded)';
156
+ }
157
+ catch (error) {
158
+ // A missing fragment: no composed form exists, so show the raw
159
+ // source and say why; `furnace validate` reports the fragment itself.
160
+ composedNote = ` (raw: ${toError(error).message})`;
161
+ }
140
162
  // `.ftl` files deploy to the locale tree, not the component's
141
163
  // targetPath. Mirror `applyCustomFtlFile`'s target computation so the
142
164
  // diff header and the existence probe name the same path apply
@@ -154,13 +176,14 @@ async function diffCustom(name, projectRoot, config) {
154
176
  hasDifferences = true;
155
177
  continue;
156
178
  }
157
- const deployedContent = await readText(deployedPath);
179
+ const deployedContent = normalizeForChecksum(await readText(deployedPath));
180
+ workspaceContent = normalizeForChecksum(workspaceContent);
158
181
  if (workspaceContent === deployedContent) {
159
182
  continue;
160
183
  }
161
184
  hasDifferences = true;
162
185
  info(`--- ${deployedDisplayPath}`);
163
- info(`+++ components/custom/${name}/${entry.name}`);
186
+ info(`+++ components/custom/${name}/${entry.name}${composedNote}`);
164
187
  for (const line of formatUnifiedDiff(deployedContent, workspaceContent)) {
165
188
  info(line);
166
189
  }
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { Command, Option } from 'commander';
3
3
  import { withEngineSessionLock } from '../../core/engine-session-lock.js';
4
- import { addWaitLockOption, pickDefined, resolveWaitLockSeconds } from '../../utils/options.js';
4
+ import { addWaitLockOption, pickDefined, resolveWaitLockSeconds, } from '../../utils/options.js';
5
5
  import { furnaceApplyCommand } from './apply.js';
6
6
  import { furnaceChromeDocCreateCommand } from './chrome-doc.js';
7
7
  import { furnaceChromeDocRemoveCommand } from './chrome-doc-remove.js';
@@ -19,6 +19,18 @@ import { furnaceScanCommand } from './scan.js';
19
19
  import { furnaceStatusCommand } from './status.js';
20
20
  import { furnaceSyncCommand } from './sync.js';
21
21
  import { furnaceValidateCommand } from './validate.js';
22
+ import { parseMaxWarnings } from './validate-report.js';
23
+ /**
24
+ * Splits the raw `--wait-lock` value off a furnace leaf's parsed options and
25
+ * resolves it into `waitLockSeconds` (flag, else `FIREFORGE_WAIT_LOCK`, else
26
+ * undefined) for the furnace lock `runFurnaceMutation` takes. Used by the
27
+ * leaves that take only Furnace's lock, where the flag used to be accepted
28
+ * and ignored.
29
+ */
30
+ function withFurnaceLockBudget(options) {
31
+ const { waitLock, ...rest } = options;
32
+ return pickDefined({ ...rest, waitLockSeconds: resolveWaitLockSeconds(waitLock) });
33
+ }
22
34
  async function runEngineLockedFurnaceCommand(context, command, operation, waitLockSeconds) {
23
35
  const projectRoot = context.getProjectRoot();
24
36
  await withEngineSessionLock(projectRoot, command, () => operation(projectRoot), {
@@ -51,16 +63,24 @@ function registerFurnaceApplyCommand(furnace, context) {
51
63
  function registerFurnaceDeployCommand(furnace, context) {
52
64
  const { getProjectRoot, withErrorHandling } = context;
53
65
  const deploy = furnace
54
- .command('deploy [name]')
55
- .description('Apply components and validate in one step')
66
+ .command('deploy [names...]')
67
+ .description('Apply components and validate in one step (name several to deploy them under one lock hold)')
56
68
  .option('--dry-run', 'Show what would be changed without writing (reads may overlap concurrent mutations)')
57
69
  .option('--force', 'Proceed despite baseVersion drift (stale overrides)')
58
- .option('--skip-validate', 'Skip the validation step (apply only)');
59
- addWaitLockOption(deploy).action(withErrorHandling(async (name, options) => {
70
+ .option('--skip-validate', 'Skip the validation step (apply only)')
71
+ .addHelpText('after', [
72
+ '',
73
+ 'Deploy registers every .mjs and .css file in a custom component directory in',
74
+ 'toolkit/content/jar.mn, including extra modules beside the main one (e.g.',
75
+ 'components/custom/<tag>/<tag>-rows.mjs). Add the file to the directory and run',
76
+ '`fireforge furnace deploy <tag>` instead of writing the jar.mn line by hand; a line',
77
+ 'that already exists anywhere in jar.mn is kept and never duplicated.',
78
+ ].join('\n'));
79
+ addWaitLockOption(deploy).action(withErrorHandling(async (names, options) => {
60
80
  const { waitLock, ...rest } = options ?? {};
61
81
  const waitLockSeconds = resolveWaitLockSeconds(waitLock);
62
82
  const parsed = pickDefined({ ...rest, waitLockSeconds });
63
- const run = (projectRoot) => furnaceDeployCommand(projectRoot, name, parsed);
83
+ const run = (projectRoot) => furnaceDeployCommand(projectRoot, names, parsed);
64
84
  if (parsed.dryRun === true) {
65
85
  await run(getProjectRoot());
66
86
  return;
@@ -103,15 +123,15 @@ function registerFurnaceInfoCommands(furnace, context) {
103
123
  }));
104
124
  registerFurnaceApplyCommand(furnace, context);
105
125
  registerFurnaceDeployCommand(furnace, context);
106
- furnace
126
+ const scan = furnace
107
127
  .command('scan')
108
128
  .description('Scan engine for available components. Report-only by default: persist the discovered ' +
109
129
  'inventory into the stock section of furnace.json with --track (or the interactive ' +
110
130
  'prompt); deploy/validate consume the inventory from there.')
111
131
  .option('--deep', 'Search additional Firefox directories beyond the default widgets path')
112
- .option('--track', 'Persist every discovered untracked component into the stock section of furnace.json (non-interactive)')
113
- .action(withErrorHandling(async (options) => {
114
- await furnaceScanCommand(getProjectRoot(), pickDefined(options));
132
+ .option('--track', 'Persist every discovered untracked component into the stock section of furnace.json (non-interactive)');
133
+ addWaitLockOption(scan).action(withErrorHandling(async (options) => {
134
+ await furnaceScanCommand(getProjectRoot(), withFurnaceLockBudget(options));
115
135
  }));
116
136
  furnace
117
137
  .command('init')
@@ -122,7 +142,7 @@ function registerFurnaceInfoCommands(furnace, context) {
122
142
  .action(withErrorHandling(async (options) => {
123
143
  await furnaceInitCommand(getProjectRoot(), options);
124
144
  }));
125
- furnace
145
+ const create = furnace
126
146
  .command('create [name]')
127
147
  .description('Create a new custom component')
128
148
  .option('-d, --description <desc>', 'Component description')
@@ -140,9 +160,9 @@ function registerFurnaceInfoCommands(furnace, context) {
140
160
  .option('--compose <tags>', 'Record stock tags composed internally (metadata only, comma-separated)', (val) => val.split(',').map((s) => s.trim()))
141
161
  .option('--shared-ftl <path>', 'Participate in an existing feature-scoped .ftl at this path (e.g. "browser/mybrowser-dock.ftl"); skips the per-component .ftl scaffold (implies --localized)')
142
162
  .option('--dry-run', 'Show the planned file set and furnace.json changes without writing')
143
- .option('--allow-prefix-mismatch', 'Create the component even when its name does not start with the configured `componentPrefix` in furnace.json. Without this flag the command refuses to write anything on a prefix mismatch.')
144
- .action(withErrorHandling(async (name, options) => {
145
- await furnaceCreateCommand(getProjectRoot(), name, options);
163
+ .option('--allow-prefix-mismatch', 'Create the component even when its name does not start with the configured `componentPrefix` in furnace.json. Without this flag the command refuses to write anything on a prefix mismatch.');
164
+ addWaitLockOption(create).action(withErrorHandling(async (name, options) => {
165
+ await furnaceCreateCommand(getProjectRoot(), name, withFurnaceLockBudget(options));
146
166
  }));
147
167
  registerChromeDocCommands(furnace, context);
148
168
  }
@@ -151,23 +171,23 @@ function registerChromeDocCommands(furnace, context) {
151
171
  const chromeDoc = furnace
152
172
  .command('chrome-doc')
153
173
  .description('Scaffold top-level chrome documents (xhtml + js + css + ftl + jar.mn)');
154
- chromeDoc
174
+ const chromeDocCreate = chromeDoc
155
175
  .command('create <name>')
156
176
  .description('Scaffold a new top-level chrome document')
157
177
  .option('--no-titlebar', 'Frameless overlay-style document (omits titlebar-buttonbox)')
158
178
  .option('--browser-window', 'Emit the browser.xhtml-like main-window skeleton (<html id="main-window"> with windowtype/chromehidden/persist root attributes platform C++ reads before scripts run) for the document configured as the fork\'s main browser window; implies the titlebar markup')
159
179
  .option('--with-tests', 'Scaffold an xpcshell packaging-verification test that probes XCurProcD/chrome/browser/... directly (bypasses the xpcshell chrome:// URI limitation).')
160
- .option('--dry-run', 'Show the chrome-doc scaffold plan without writing')
161
- .action(withErrorHandling(async (name, options) => {
162
- await furnaceChromeDocCreateCommand(getProjectRoot(), name, pickDefined(options));
180
+ .option('--dry-run', 'Show the chrome-doc scaffold plan without writing');
181
+ addWaitLockOption(chromeDocCreate).action(withErrorHandling(async (name, options) => {
182
+ await furnaceChromeDocCreateCommand(getProjectRoot(), name, withFurnaceLockBudget(options));
163
183
  }));
164
- chromeDoc
184
+ const chromeDocRemove = chromeDoc
165
185
  .command('remove <name>')
166
186
  .description('Remove a scaffolded top-level chrome document')
167
187
  .option('-y, --yes', 'Skip confirmation')
168
- .option('--dry-run', 'Show the chrome-doc removal plan without writing')
169
- .action(withErrorHandling(async (name, options) => {
170
- await furnaceChromeDocRemoveCommand(getProjectRoot(), name, pickDefined(options));
188
+ .option('--dry-run', 'Show the chrome-doc removal plan without writing');
189
+ addWaitLockOption(chromeDocRemove).action(withErrorHandling(async (name, options) => {
190
+ await furnaceChromeDocRemoveCommand(getProjectRoot(), name, withFurnaceLockBudget(options));
171
191
  }));
172
192
  }
173
193
  /**
@@ -178,12 +198,13 @@ function registerChromeDocCommands(furnace, context) {
178
198
  */
179
199
  function registerFurnaceModifyCommands(furnace, context) {
180
200
  const { getProjectRoot, withErrorHandling } = context;
181
- furnace
201
+ const override = furnace
182
202
  .command('override [names...]')
183
203
  .description('Fork one or more existing components for modification')
184
204
  .addOption(new Option('-t, --type <type>', 'Override type').choices(['css-only', 'full']))
185
- .option('-d, --description <desc>', 'Description')
186
- .action(withErrorHandling(async (names, options) => {
205
+ .option('-d, --description <desc>', 'Description');
206
+ addWaitLockOption(override).action(withErrorHandling(async (names, rawOptions) => {
207
+ const options = withFurnaceLockBudget(rawOptions);
187
208
  if (names.length <= 1) {
188
209
  await furnaceOverrideCommand(getProjectRoot(), names[0], options);
189
210
  }
@@ -198,26 +219,29 @@ function registerFurnaceModifyCommands(furnace, context) {
198
219
  .action(withErrorHandling(async (options) => {
199
220
  await furnaceListCommand(getProjectRoot(), options);
200
221
  }));
201
- furnace
222
+ const remove = furnace
202
223
  .command('remove <name>')
203
224
  .description('Remove a component from the workspace')
204
- .option('-y, --yes', 'Skip confirmation')
205
- .action(withErrorHandling(async (name, options) => {
206
- await furnaceRemoveCommand(getProjectRoot(), name, options);
225
+ .option('-y, --yes', 'Skip confirmation');
226
+ addWaitLockOption(remove).action(withErrorHandling(async (name, options) => {
227
+ await furnaceRemoveCommand(getProjectRoot(), name, withFurnaceLockBudget(options));
207
228
  }));
208
- furnace
229
+ const preview = furnace
209
230
  .command('preview')
210
231
  .description('Start component preview (Storybook)')
211
- .option('--install', 'Force reinstall Storybook dependencies')
212
- .action(withErrorHandling(async (options) => {
213
- await furnacePreviewCommand(getProjectRoot(), options);
232
+ .option('--install', 'Force reinstall Storybook dependencies');
233
+ addWaitLockOption(preview).action(withErrorHandling(async (options) => {
234
+ await furnacePreviewCommand(getProjectRoot(), withFurnaceLockBudget(options));
214
235
  }));
215
236
  furnace
216
237
  .command('validate [name]')
217
238
  .description('Run accessibility and compatibility checks')
218
239
  .option('--fix', 'Auto-fix registration issues (missing jar.mn entries, customElements.js)')
240
+ .option('--json', 'Write the result as one JSON document (findings, excepted findings, stale exceptions, verdict) to stdout')
241
+ .option('--max-warnings <n>', 'Fail when more than <n> warnings remain after furnace.json validationExceptions (0 for a warning-clean gate)')
219
242
  .action(withErrorHandling(async (name, options) => {
220
- await furnaceValidateCommand(getProjectRoot(), name, pickDefined(options ?? {}));
243
+ const { maxWarnings, ...rest } = options ?? {};
244
+ await furnaceValidateCommand(getProjectRoot(), name, pickDefined({ ...rest, maxWarnings: parseMaxWarnings(maxWarnings) }));
221
245
  }));
222
246
  furnace
223
247
  .command('diff [name]')
@@ -225,21 +249,22 @@ function registerFurnaceModifyCommands(furnace, context) {
225
249
  .action(withErrorHandling(async (name) => {
226
250
  await furnaceDiffCommand(getProjectRoot(), name);
227
251
  }));
228
- furnace
252
+ const refresh = furnace
229
253
  .command('refresh [name]')
230
254
  .description('Merge upstream Firefox changes into overrides (three-way merge)')
231
255
  .option('--dry-run', 'Show what would change without modifying files')
232
256
  .option('-a, --all', 'Refresh all overrides in a single batch')
233
257
  .addOption(new Option('-s, --strategy <strategy>', 'Auto-resolve conflicts (ours = keep local, theirs = accept upstream)').choices(['ours', 'theirs']))
234
- .option('--reset-base', 'Reset baseline to current engine HEAD (skips three-way merge, recovers from missing baseCommit)')
235
- .action(withErrorHandling(async (name, options) => {
236
- await furnaceRefreshCommand(getProjectRoot(), name, pickDefined(options));
258
+ .option('--reset-base', 'Reset baseline to current engine HEAD (skips three-way merge, recovers from missing baseCommit)');
259
+ addWaitLockOption(refresh).action(withErrorHandling(async (name, options) => {
260
+ await furnaceRefreshCommand(getProjectRoot(), name, withFurnaceLockBudget(options));
237
261
  }));
238
- furnace
262
+ const rename = furnace
239
263
  .command('rename <old-name> <new-name>')
240
264
  .description('Rename a component (updates files, config, and registrations)')
241
- .action(withErrorHandling(async (oldName, newName) => {
242
- await furnaceRenameCommand(getProjectRoot(), oldName, newName);
265
+ .option('--dry-run', 'Show the planned file moves, furnace.json change and engine registration edits without writing');
266
+ addWaitLockOption(rename).action(withErrorHandling(async (oldName, newName, options) => {
267
+ await furnaceRenameCommand(getProjectRoot(), oldName, newName, withFurnaceLockBudget(options));
243
268
  }));
244
269
  registerFurnaceSyncCommand(furnace, context);
245
270
  }
@@ -2,7 +2,7 @@
2
2
  import { stat } from 'node:fs/promises';
3
3
  import { basename, dirname, isAbsolute, join, posix } from 'node:path';
4
4
  import { text } from '@clack/prompts';
5
- import { getProjectPaths, loadConfig, mutateConfig, writeConfig } from '../../core/config.js';
5
+ import { getProjectPaths, loadConfig, loadRawConfigDocument, mutateConfig, validateConfig, withConfigFileLock, writeConfigDocument, } from '../../core/config.js';
6
6
  import { stdioIsInteractive } from '../../core/destructive.js';
7
7
  import { createDefaultFurnaceConfig, furnaceConfigExists, writeFurnaceConfig, } from '../../core/furnace-config.js';
8
8
  import { DEFAULT_LICENSE } from '../../core/license-headers.js';
@@ -239,14 +239,21 @@ async function scaffoldTokensCss(projectRoot) {
239
239
  // `fireforge lint` after a `token add` fails on raw-color-value issues for
240
240
  // tokens the operator just created. The add is idempotent, so re-running
241
241
  // `furnace init --force` does not duplicate the entry.
242
+ // The edit seeds from the raw document under the config lock, like
243
+ // `fireforge config`: a validated round-trip would drop keys written with
244
+ // `config --force`, and an unlocked one could lose a concurrent write.
242
245
  try {
243
246
  const existingAllowlist = forgeConfig.patchLint?.rawColorAllowlist ?? [];
244
247
  if (!existingAllowlist.includes(tokensCssPath)) {
245
- const updatedConfig = mutateConfig(forgeConfig, 'patchLint.rawColorAllowlist', [
246
- ...existingAllowlist,
247
- tokensCssPath,
248
- ]);
249
- await writeConfig(projectRoot, updatedConfig);
248
+ await withConfigFileLock(projectRoot, async () => {
249
+ const raw = await loadRawConfigDocument(projectRoot);
250
+ const current = validateConfig(raw).patchLint?.rawColorAllowlist ?? [];
251
+ if (current.includes(tokensCssPath))
252
+ return;
253
+ const updated = mutateConfig(raw, 'patchLint.rawColorAllowlist', [...current, tokensCssPath], true);
254
+ validateConfig(updated);
255
+ await writeConfigDocument(projectRoot, updated);
256
+ });
250
257
  info(`Added ${tokensCssPath} to patchLint.rawColorAllowlist`);
251
258
  }
252
259
  }
@@ -6,7 +6,7 @@ import { getProjectPaths, loadConfig, loadState } from '../../core/config.js';
6
6
  import { stdioIsInteractive } from '../../core/destructive.js';
7
7
  import { getFurnacePaths, writeFurnaceConfig } from '../../core/furnace-config.js';
8
8
  import { resolveFtlDir } from '../../core/furnace-constants.js';
9
- import { completeJournalRollback, runFurnaceMutation } from '../../core/furnace-operation.js';
9
+ import { completeJournalRollback, runFurnaceMutation, waitLockMutationOptions, } from '../../core/furnace-operation.js';
10
10
  import { assertFurnaceEngineReady } from '../../core/furnace-precondition.js';
11
11
  import { CUSTOM_ELEMENT_TAG_PATTERN, CUSTOM_ELEMENT_TAG_RULES, } from '../../core/furnace-registration-validate.js';
12
12
  import { createRollbackJournal, recordCreatedDir, snapshotFile, } from '../../core/furnace-rollback.js';
@@ -160,7 +160,7 @@ async function performOverrideMutations(args) {
160
160
  subject: `component "${args.componentName}"`,
161
161
  });
162
162
  }
163
- });
163
+ }, waitLockMutationOptions(args.waitLockSeconds));
164
164
  }
165
165
  /**
166
166
  * Throws if `componentName` is already classified as something `override`
@@ -322,6 +322,7 @@ export async function furnaceOverrideCommand(projectRoot, name, options = {}) {
322
322
  ftlDir,
323
323
  firefoxVersion: forgeConfig.firefox.version,
324
324
  ...(state.baseCommit ? { baseCommit: state.baseCommit } : {}),
325
+ waitLockSeconds: options.waitLockSeconds,
325
326
  });
326
327
  // --- Success ---
327
328
  note(`Files copied to components/overrides/${componentName}/:\n` +
@@ -421,6 +422,7 @@ export async function furnaceBatchOverrideCommand(projectRoot, names, options =
421
422
  ftlDir,
422
423
  firefoxVersion: forgeConfig.firefox.version,
423
424
  ...(state.baseCommit ? { baseCommit: state.baseCommit } : {}),
425
+ waitLockSeconds: options.waitLockSeconds,
424
426
  });
425
427
  succeeded.push(componentName);
426
428
  }
@@ -2,7 +2,7 @@
2
2
  import { join } from 'node:path';
3
3
  import { applyAllComponents } from '../../core/furnace-apply.js';
4
4
  import { updateFurnaceState } from '../../core/furnace-config.js';
5
- import { runFurnaceMutation } from '../../core/furnace-operation.js';
5
+ import { runFurnaceMutation, waitLockMutationOptions, } from '../../core/furnace-operation.js';
6
6
  import { assertFurnaceReady } from '../../core/furnace-precondition.js';
7
7
  import { restoreRollbackJournal } from '../../core/furnace-rollback.js';
8
8
  import { countEntriesWithBlockingStepErrors } from '../../core/furnace-step-errors.js';
@@ -224,7 +224,7 @@ export async function furnacePreviewCommand(projectRoot, options = {}) {
224
224
  // entrypoint's global signal handlers consulting the lifecycle registry.
225
225
  // The body's own try/catch + teardown path handles the normal exit case
226
226
  // (mach storybook returns or throws).
227
- await runFurnaceMutation(projectRoot, 'preview-teardown', async (ctx) => {
227
+ const mutate = async (ctx) => {
228
228
  // Register the stories cleanup as an extra teardown hook so the signal
229
229
  // handler can wipe the staged stories directory in addition to the
230
230
  // journal restore.
@@ -336,7 +336,8 @@ export async function furnacePreviewCommand(projectRoot, options = {}) {
336
336
  // holds and downstream formatters always see a message/stack pair.
337
337
  throw toError(primaryError);
338
338
  }
339
- });
339
+ };
340
+ await runFurnaceMutation(projectRoot, 'preview-teardown', mutate, waitLockMutationOptions(options.waitLockSeconds));
340
341
  if (previewResult &&
341
342
  previewResult.exitCode !== 0 &&
342
343
  previewResult.exitCode !== 130 &&
@@ -6,7 +6,7 @@ import { getOverrideEngineTargetPath } from '../../core/furnace-apply-helpers.js
6
6
  import { getFurnacePaths, loadFurnaceConfig, writeFurnaceConfig, } from '../../core/furnace-config.js';
7
7
  import { resolveFtlDir } from '../../core/furnace-constants.js';
8
8
  import { isComponentSourceFile } from '../../core/furnace-constants.js';
9
- import { completeJournalRollback, runFurnaceMutation } from '../../core/furnace-operation.js';
9
+ import { completeJournalRollback, runFurnaceMutation, waitLockMutationOptions, } from '../../core/furnace-operation.js';
10
10
  import { assertFurnaceEngineReady } from '../../core/furnace-precondition.js';
11
11
  import { refreshOverrideFile } from '../../core/furnace-refresh.js';
12
12
  import { createRollbackJournal, restoreRollbackJournal, snapshotDir, snapshotFile, } from '../../core/furnace-rollback.js';
@@ -165,7 +165,7 @@ async function refreshSingleOverride(projectRoot, name, options = {}) {
165
165
  }
166
166
  throw error;
167
167
  }
168
- }, { dryRun });
168
+ }, { dryRun, ...waitLockMutationOptions(options.waitLockSeconds) });
169
169
  return { results, currentVersion };
170
170
  }
171
171
  /**