@hominis/fireforge 0.45.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (359) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/dist/bin/fireforge.js +6 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/src/cli-usage-error.d.ts +46 -0
  5. package/dist/src/cli-usage-error.js +81 -0
  6. package/dist/src/cli.d.ts +5 -1
  7. package/dist/src/cli.js +20 -21
  8. package/dist/src/commands/bootstrap.js +1 -2
  9. package/dist/src/commands/build.js +15 -6
  10. package/dist/src/commands/destructive-decision.d.ts +19 -0
  11. package/dist/src/commands/destructive-decision.js +31 -0
  12. package/dist/src/commands/doctor-check-core.d.ts +0 -6
  13. package/dist/src/commands/doctor-check-core.js +0 -8
  14. package/dist/src/commands/doctor-orphaned-harness.d.ts +0 -6
  15. package/dist/src/commands/doctor-orphaned-harness.js +4 -12
  16. package/dist/src/commands/doctor-post-rebase-audit.js +1 -1
  17. package/dist/src/commands/doctor.d.ts +2 -0
  18. package/dist/src/commands/doctor.js +7 -8
  19. package/dist/src/commands/download.js +35 -24
  20. package/dist/src/commands/export-all.js +21 -4
  21. package/dist/src/commands/export-flow.js +15 -45
  22. package/dist/src/commands/export-placement-conflicts.js +3 -3
  23. package/dist/src/commands/export-placement-gate.js +2 -8
  24. package/dist/src/commands/export-placement-policy.js +3 -5
  25. package/dist/src/commands/export-shared.d.ts +42 -23
  26. package/dist/src/commands/export-shared.js +4 -19
  27. package/dist/src/commands/export.js +10 -1
  28. package/dist/src/commands/furnace/authoring-config.d.ts +13 -0
  29. package/dist/src/commands/furnace/authoring-config.js +19 -0
  30. package/dist/src/commands/furnace/chrome-doc-remove.js +1 -1
  31. package/dist/src/commands/furnace/create-browser-test.js +3 -8
  32. package/dist/src/commands/furnace/create-dry-run.js +2 -5
  33. package/dist/src/commands/furnace/create-templates.d.ts +17 -1
  34. package/dist/src/commands/furnace/create-templates.js +2 -1
  35. package/dist/src/commands/furnace/create.js +25 -19
  36. package/dist/src/commands/furnace/deploy.js +11 -2
  37. package/dist/src/commands/furnace/init.js +2 -1
  38. package/dist/src/commands/furnace/override.js +28 -19
  39. package/dist/src/commands/furnace/refresh.js +9 -1
  40. package/dist/src/commands/furnace/remove-mutations.d.ts +6 -7
  41. package/dist/src/commands/furnace/remove-mutations.js +0 -1
  42. package/dist/src/commands/furnace/remove.js +2 -6
  43. package/dist/src/commands/furnace/rename-browser-test.js +1 -7
  44. package/dist/src/commands/furnace/rename.js +29 -29
  45. package/dist/src/commands/furnace/status.js +12 -9
  46. package/dist/src/commands/furnace/test-file-name.d.ts +28 -0
  47. package/dist/src/commands/furnace/test-file-name.js +39 -0
  48. package/dist/src/commands/furnace/validation-output.d.ts +21 -9
  49. package/dist/src/commands/furnace/validation-output.js +3 -8
  50. package/dist/src/commands/import.js +2 -9
  51. package/dist/src/commands/lint-per-patch.d.ts +2 -2
  52. package/dist/src/commands/lint-per-patch.js +19 -4
  53. package/dist/src/commands/lint-per-run-checkjs.d.ts +2 -2
  54. package/dist/src/commands/lint.d.ts +2 -1
  55. package/dist/src/commands/lint.js +1 -1
  56. package/dist/src/commands/manifest.js +1 -1
  57. package/dist/src/commands/package.js +2 -4
  58. package/dist/src/commands/patch/compact.js +14 -15
  59. package/dist/src/commands/patch/delete.js +15 -25
  60. package/dist/src/commands/patch/entry-projection.d.ts +13 -0
  61. package/dist/src/commands/patch/entry-projection.js +24 -0
  62. package/dist/src/commands/patch/lint-ignore.d.ts +0 -8
  63. package/dist/src/commands/patch/lint-ignore.js +19 -29
  64. package/dist/src/commands/patch/move-files-create.js +24 -10
  65. package/dist/src/commands/patch/move-files-into.js +38 -34
  66. package/dist/src/commands/patch/move-files.js +4 -13
  67. package/dist/src/commands/patch/rename.js +27 -41
  68. package/dist/src/commands/patch/reorder.js +25 -38
  69. package/dist/src/commands/patch/split-plan.d.ts +4 -5
  70. package/dist/src/commands/patch/split-plan.js +4 -8
  71. package/dist/src/commands/patch/split.js +45 -43
  72. package/dist/src/commands/patch/staged-dependency.js +16 -22
  73. package/dist/src/commands/patch/tier.js +15 -25
  74. package/dist/src/commands/patch-rename-map.d.ts +19 -0
  75. package/dist/src/commands/patch-rename-map.js +36 -0
  76. package/dist/src/commands/pending-resolution.d.ts +15 -0
  77. package/dist/src/commands/pending-resolution.js +25 -0
  78. package/dist/src/commands/re-export-adjacent.d.ts +24 -2
  79. package/dist/src/commands/re-export-adjacent.js +32 -16
  80. package/dist/src/commands/re-export-bulk-scan.js +6 -10
  81. package/dist/src/commands/re-export-drift.js +4 -4
  82. package/dist/src/commands/re-export-files.d.ts +2 -3
  83. package/dist/src/commands/re-export-files.js +38 -29
  84. package/dist/src/commands/re-export-lint.d.ts +3 -4
  85. package/dist/src/commands/re-export-lint.js +9 -2
  86. package/dist/src/commands/re-export-scan.js +5 -2
  87. package/dist/src/commands/re-export-single.d.ts +22 -3
  88. package/dist/src/commands/re-export-single.js +19 -11
  89. package/dist/src/commands/re-export.js +28 -11
  90. package/dist/src/commands/rebase/abort.js +3 -8
  91. package/dist/src/commands/rebase/continue.js +12 -12
  92. package/dist/src/commands/rebase/index.js +2 -2
  93. package/dist/src/commands/rebase/patch-loop.d.ts +2 -2
  94. package/dist/src/commands/rebase/patch-loop.js +2 -7
  95. package/dist/src/commands/resolve.js +18 -10
  96. package/dist/src/commands/run.d.ts +0 -13
  97. package/dist/src/commands/run.js +12 -25
  98. package/dist/src/commands/setup-support.d.ts +16 -4
  99. package/dist/src/commands/setup-support.js +16 -16
  100. package/dist/src/commands/setup.js +3 -9
  101. package/dist/src/commands/source.js +8 -1
  102. package/dist/src/commands/status-check.js +2 -2
  103. package/dist/src/commands/status-output.js +2 -2
  104. package/dist/src/commands/status.js +3 -4
  105. package/dist/src/commands/test-build-phase.d.ts +2 -2
  106. package/dist/src/commands/test-build-phase.js +31 -7
  107. package/dist/src/commands/test-modes.d.ts +2 -2
  108. package/dist/src/commands/test-modes.js +0 -1
  109. package/dist/src/commands/test-register.js +5 -3
  110. package/dist/src/commands/test-run.js +25 -18
  111. package/dist/src/commands/test-stale-gate.js +4 -4
  112. package/dist/src/commands/test-verdict.d.ts +16 -2
  113. package/dist/src/commands/test-verdict.js +18 -3
  114. package/dist/src/commands/test.js +42 -5
  115. package/dist/src/commands/token-list.d.ts +26 -0
  116. package/dist/src/commands/token-list.js +153 -0
  117. package/dist/src/commands/token.js +17 -1
  118. package/dist/src/commands/tree.js +7 -2
  119. package/dist/src/commands/typecheck.js +9 -9
  120. package/dist/src/commands/wire.js +4 -3
  121. package/dist/src/core/branding.d.ts +0 -7
  122. package/dist/src/core/branding.js +3 -2
  123. package/dist/src/core/browser-wire.js +6 -3
  124. package/dist/src/core/build-audit-platform.d.ts +29 -0
  125. package/dist/src/core/build-audit-platform.js +140 -0
  126. package/dist/src/core/build-audit-registration.js +3 -2
  127. package/dist/src/core/build-audit-skip.d.ts +57 -0
  128. package/dist/src/core/build-audit-skip.js +112 -0
  129. package/dist/src/core/build-audit.d.ts +15 -2
  130. package/dist/src/core/build-audit.js +110 -10
  131. package/dist/src/core/build-baseline.d.ts +45 -28
  132. package/dist/src/core/build-baseline.js +6 -29
  133. package/dist/src/core/build-jar-escalation.js +3 -2
  134. package/dist/src/core/build-overwrite-guard.js +1 -1
  135. package/dist/src/core/build-prepare.d.ts +0 -8
  136. package/dist/src/core/build-prepare.js +7 -12
  137. package/dist/src/core/config-paths.d.ts +2 -6
  138. package/dist/src/core/config-paths.js +4 -2
  139. package/dist/src/core/config-state.d.ts +0 -6
  140. package/dist/src/core/config-state.js +1 -19
  141. package/dist/src/core/config-validate-build-audit.d.ts +8 -0
  142. package/dist/src/core/config-validate-build-audit.js +80 -0
  143. package/dist/src/core/config-validate-file-size.js +1 -1
  144. package/dist/src/core/config-validate-patch-policy.d.ts +2 -2
  145. package/dist/src/core/config-validate-patch-policy.js +3 -3
  146. package/dist/src/core/config-validate-test-toolchains.d.ts +2 -2
  147. package/dist/src/core/config-validate-test-toolchains.js +4 -5
  148. package/dist/src/core/config-validate.js +14 -3
  149. package/dist/src/core/config.d.ts +2 -2
  150. package/dist/src/core/config.js +11 -15
  151. package/dist/src/core/coverage-extend.js +4 -4
  152. package/dist/src/core/discard-baseline.js +7 -7
  153. package/dist/src/core/engine-precondition.d.ts +1 -9
  154. package/dist/src/core/engine-precondition.js +3 -8
  155. package/dist/src/core/engine-session-lock.js +13 -17
  156. package/dist/src/core/export-moved-code-shape.js +2 -2
  157. package/dist/src/core/file-lock-owner.d.ts +103 -0
  158. package/dist/src/core/file-lock-owner.js +259 -0
  159. package/dist/src/core/file-lock.d.ts +2 -7
  160. package/dist/src/core/file-lock.js +40 -91
  161. package/dist/src/core/firefox-cache.d.ts +13 -1
  162. package/dist/src/core/firefox-cache.js +59 -34
  163. package/dist/src/core/firefox-download.d.ts +15 -0
  164. package/dist/src/core/firefox-download.js +37 -21
  165. package/dist/src/core/firefox-extract.d.ts +6 -5
  166. package/dist/src/core/firefox-extract.js +36 -18
  167. package/dist/src/core/firefox.d.ts +30 -32
  168. package/dist/src/core/firefox.js +5 -36
  169. package/dist/src/core/furnace-apply-dry-run.d.ts +1 -6
  170. package/dist/src/core/furnace-apply-dry-run.js +1 -7
  171. package/dist/src/core/furnace-apply-helpers.d.ts +0 -11
  172. package/dist/src/core/furnace-apply-helpers.js +8 -19
  173. package/dist/src/core/furnace-apply-overwrite-warn.js +4 -2
  174. package/dist/src/core/furnace-apply.d.ts +1 -1
  175. package/dist/src/core/furnace-apply.js +30 -10
  176. package/dist/src/core/furnace-config.d.ts +1 -1
  177. package/dist/src/core/furnace-config.js +23 -36
  178. package/dist/src/core/furnace-constants.js +3 -2
  179. package/dist/src/core/furnace-dir-entry.d.ts +20 -0
  180. package/dist/src/core/furnace-dir-entry.js +21 -0
  181. package/dist/src/core/furnace-operation.d.ts +2 -0
  182. package/dist/src/core/furnace-operation.js +3 -1
  183. package/dist/src/core/furnace-refresh.d.ts +18 -6
  184. package/dist/src/core/furnace-refresh.js +3 -6
  185. package/dist/src/core/furnace-registration-ast.d.ts +0 -1
  186. package/dist/src/core/furnace-registration-ast.js +0 -3
  187. package/dist/src/core/furnace-registration.d.ts +2 -1
  188. package/dist/src/core/furnace-registration.js +22 -29
  189. package/dist/src/core/furnace-scanner.js +3 -6
  190. package/dist/src/core/furnace-state-persist.d.ts +2 -2
  191. package/dist/src/core/furnace-validate-registration.js +5 -7
  192. package/dist/src/core/git-base.d.ts +3 -1
  193. package/dist/src/core/git-base.js +25 -3
  194. package/dist/src/core/git-diff-new-file.d.ts +40 -0
  195. package/dist/src/core/git-diff-new-file.js +119 -0
  196. package/dist/src/core/git-diff.d.ts +9 -9
  197. package/dist/src/core/git-diff.js +88 -157
  198. package/dist/src/core/git-file-ops.d.ts +0 -7
  199. package/dist/src/core/git-file-ops.js +4 -28
  200. package/dist/src/core/git-readonly-index.d.ts +0 -2
  201. package/dist/src/core/git-readonly-index.js +0 -4
  202. package/dist/src/core/git-status.d.ts +7 -0
  203. package/dist/src/core/git-status.js +21 -9
  204. package/dist/src/core/git.d.ts +0 -6
  205. package/dist/src/core/git.js +14 -43
  206. package/dist/src/core/harness-orphans.js +8 -7
  207. package/dist/src/core/history-log.d.ts +19 -0
  208. package/dist/src/core/history-log.js +29 -0
  209. package/dist/src/core/mach-error-hints.d.ts +0 -5
  210. package/dist/src/core/mach-error-hints.js +1 -1
  211. package/dist/src/core/mach-known-noise-filter.d.ts +4 -3
  212. package/dist/src/core/mach-known-noise-filter.js +7 -9
  213. package/dist/src/core/mach-mozconfig.js +2 -1
  214. package/dist/src/core/mach-python.d.ts +2 -0
  215. package/dist/src/core/mach-python.js +12 -7
  216. package/dist/src/core/mach-resource-shim.d.ts +13 -1
  217. package/dist/src/core/mach-resource-shim.js +112 -9
  218. package/dist/src/core/mach.d.ts +39 -53
  219. package/dist/src/core/mach.js +13 -81
  220. package/dist/src/core/marionette-port.d.ts +0 -17
  221. package/dist/src/core/marionette-port.js +12 -58
  222. package/dist/src/core/marionette-preflight.js +9 -27
  223. package/dist/src/core/mochitest-server-port.js +3 -3
  224. package/dist/src/core/moz-manifest-rules.js +37 -33
  225. package/dist/src/core/patch-apply-fuzz.js +4 -4
  226. package/dist/src/core/patch-apply.d.ts +4 -5
  227. package/dist/src/core/patch-apply.js +6 -12
  228. package/dist/src/core/patch-export-coverage.d.ts +0 -9
  229. package/dist/src/core/patch-export-coverage.js +1 -30
  230. package/dist/src/core/patch-export-update.d.ts +18 -1
  231. package/dist/src/core/patch-export-update.js +4 -3
  232. package/dist/src/core/patch-export.d.ts +11 -21
  233. package/dist/src/core/patch-export.js +44 -66
  234. package/dist/src/core/patch-files.d.ts +0 -4
  235. package/dist/src/core/patch-files.js +0 -11
  236. package/dist/src/core/patch-lint-binary.d.ts +6 -12
  237. package/dist/src/core/patch-lint-cache.d.ts +17 -1
  238. package/dist/src/core/patch-lint-cache.js +3 -6
  239. package/dist/src/core/patch-lint-checkjs.d.ts +33 -16
  240. package/dist/src/core/patch-lint-checkjs.js +37 -24
  241. package/dist/src/core/patch-lint-creators.d.ts +2 -6
  242. package/dist/src/core/patch-lint-cross.d.ts +16 -8
  243. package/dist/src/core/patch-lint-cross.js +49 -92
  244. package/dist/src/core/patch-lint-css.js +10 -6
  245. package/dist/src/core/patch-lint-file-size.js +3 -3
  246. package/dist/src/core/patch-lint-forward-registration.d.ts +58 -0
  247. package/dist/src/core/patch-lint-forward-registration.js +210 -0
  248. package/dist/src/core/patch-lint-jsdoc.js +36 -36
  249. package/dist/src/core/patch-lint-module-registration.d.ts +2 -8
  250. package/dist/src/core/patch-lint-prettier.js +1 -1
  251. package/dist/src/core/patch-lint-queue-types.d.ts +60 -0
  252. package/dist/src/core/patch-lint-queue-types.js +12 -0
  253. package/dist/src/core/patch-lint-staged-registration.d.ts +61 -0
  254. package/dist/src/core/patch-lint-staged-registration.js +90 -0
  255. package/dist/src/core/patch-lint.d.ts +14 -9
  256. package/dist/src/core/patch-lint.js +4 -3
  257. package/dist/src/core/patch-lock.js +3 -3
  258. package/dist/src/core/patch-manifest-io.js +8 -7
  259. package/dist/src/core/patch-parse.d.ts +19 -8
  260. package/dist/src/core/patch-parse.js +25 -0
  261. package/dist/src/core/patch-policy.js +15 -27
  262. package/dist/src/core/patch-source-metadata.d.ts +1 -3
  263. package/dist/src/core/patch-source-metadata.js +0 -4
  264. package/dist/src/core/patch-transform.d.ts +0 -8
  265. package/dist/src/core/patch-transform.js +0 -10
  266. package/dist/src/core/process-cpu-time.js +1 -1
  267. package/dist/src/core/rebase-session.d.ts +2 -8
  268. package/dist/src/core/rebase-session.js +5 -17
  269. package/dist/src/core/register-browser-content.js +3 -2
  270. package/dist/src/core/register-browser-test.js +2 -1
  271. package/dist/src/core/register-module.js +2 -1
  272. package/dist/src/core/register-scaffold.js +5 -4
  273. package/dist/src/core/register-shared-css.js +2 -1
  274. package/dist/src/core/run-log-redact.d.ts +38 -0
  275. package/dist/src/core/run-log-redact.js +69 -0
  276. package/dist/src/core/run-log.d.ts +13 -2
  277. package/dist/src/core/run-log.js +67 -18
  278. package/dist/src/core/signal-critical.js +2 -4
  279. package/dist/src/core/smoke-patterns.d.ts +0 -1
  280. package/dist/src/core/smoke-patterns.js +6 -11
  281. package/dist/src/core/state-file.d.ts +12 -1
  282. package/dist/src/core/state-file.js +14 -1
  283. package/dist/src/core/status-classify.js +2 -1
  284. package/dist/src/core/test-changed-prefs.js +26 -11
  285. package/dist/src/core/test-harness-crash.js +2 -2
  286. package/dist/src/core/test-stale-check.d.ts +0 -2
  287. package/dist/src/core/test-stale-check.js +2 -2
  288. package/dist/src/core/test-xpcshell-retry.d.ts +26 -7
  289. package/dist/src/core/test-xpcshell-retry.js +14 -11
  290. package/dist/src/core/token-category.d.ts +33 -0
  291. package/dist/src/core/token-category.js +56 -48
  292. package/dist/src/core/token-dark-mode.js +2 -14
  293. package/dist/src/core/token-inventory.d.ts +56 -0
  294. package/dist/src/core/token-inventory.js +109 -0
  295. package/dist/src/core/token-manager.d.ts +1 -1
  296. package/dist/src/core/token-manager.js +1 -1
  297. package/dist/src/core/token-variant.js +21 -22
  298. package/dist/src/core/toolchain-preflight.js +1 -1
  299. package/dist/src/core/tree-cow.d.ts +0 -2
  300. package/dist/src/core/tree-cow.js +2 -2
  301. package/dist/src/core/tree-store.js +4 -5
  302. package/dist/src/core/typecheck.js +2 -2
  303. package/dist/src/core/wire-dom-fragment.js +6 -17
  304. package/dist/src/core/wire-utils.js +5 -17
  305. package/dist/src/core/xpcshell-appdir.js +2 -4
  306. package/dist/src/errors/base.d.ts +53 -1
  307. package/dist/src/errors/base.js +79 -11
  308. package/dist/src/errors/build.js +20 -28
  309. package/dist/src/errors/config.d.ts +0 -14
  310. package/dist/src/errors/config.js +13 -34
  311. package/dist/src/errors/download.d.ts +13 -0
  312. package/dist/src/errors/download.js +53 -24
  313. package/dist/src/errors/furnace.js +8 -9
  314. package/dist/src/errors/git.d.ts +8 -8
  315. package/dist/src/errors/git.js +20 -30
  316. package/dist/src/errors/patch.js +8 -10
  317. package/dist/src/errors/rebase.js +7 -7
  318. package/dist/src/types/commands/options.d.ts +77 -123
  319. package/dist/src/types/config.d.ts +34 -2
  320. package/dist/src/types/index.d.ts +0 -1
  321. package/dist/src/utils/assert.d.ts +3 -15
  322. package/dist/src/utils/assert.js +1 -18
  323. package/dist/src/utils/build-info.d.ts +1 -1
  324. package/dist/src/utils/child-env.d.ts +2 -1
  325. package/dist/src/utils/elapsed.js +10 -1
  326. package/dist/src/utils/fs.js +8 -13
  327. package/dist/src/utils/hash.d.ts +12 -0
  328. package/dist/src/utils/hash.js +16 -0
  329. package/dist/src/utils/line-dispatch.d.ts +15 -0
  330. package/dist/src/utils/line-dispatch.js +48 -0
  331. package/dist/src/utils/options.d.ts +1 -7
  332. package/dist/src/utils/options.js +6 -7
  333. package/dist/src/utils/package-root.js +3 -12
  334. package/dist/src/utils/platform.d.ts +0 -4
  335. package/dist/src/utils/process-group.d.ts +2 -1
  336. package/dist/src/utils/process-group.js +20 -19
  337. package/dist/src/utils/process.d.ts +40 -26
  338. package/dist/src/utils/process.js +115 -166
  339. package/dist/src/utils/ps-duration.js +3 -3
  340. package/dist/src/utils/sleep.d.ts +26 -0
  341. package/dist/src/utils/sleep.js +31 -0
  342. package/dist/src/utils/validation.d.ts +2 -38
  343. package/dist/src/utils/validation.js +7 -16
  344. package/docs/configuration.md +89 -1
  345. package/docs/exit-codes.md +31 -19
  346. package/docs/lifecycle-invariants.md +18 -12
  347. package/docs/machine-output.md +31 -8
  348. package/docs/patch-workflow.md +2 -1
  349. package/docs/run-logs.md +44 -4
  350. package/docs/testing.md +8 -1
  351. package/package.json +8 -7
  352. package/dist/src/commands/rebase.d.ts +0 -4
  353. package/dist/src/commands/rebase.js +0 -5
  354. package/dist/src/core/patch-artifact-normalize.d.ts +0 -21
  355. package/dist/src/core/patch-artifact-normalize.js +0 -24
  356. package/dist/src/core/patch-lint-reexports.d.ts +0 -7
  357. package/dist/src/core/patch-lint-reexports.js +0 -8
  358. package/dist/src/core/wire-targets.d.ts +0 -7
  359. package/dist/src/core/wire-targets.js +0 -8
@@ -4,6 +4,7 @@ import { constants as osConstants } from 'node:os';
4
4
  import { StringDecoder } from 'node:string_decoder';
5
5
  import { ExecTimeoutError } from '../errors/base.js';
6
6
  import { buildChildEnv } from './child-env.js';
7
+ import { dispatchCompleteLines } from './line-dispatch.js';
7
8
  import { killProcessTree, sweepProcessGroup } from './process-group.js';
8
9
  // 50 MB cap per stream to prevent OOM — large toolchain builds (e.g. Firefox, Chromium)
9
10
  // can easily blow past this, so we truncate rather than let the buffer grow unbounded.
@@ -57,12 +58,25 @@ function buildSignalFromTimeout(timeout) {
57
58
  * {@link ExecTimeoutError} naming the command and budget instead of Node's
58
59
  * opaque `AbortError: The operation was aborted`.
59
60
  */
60
- function toExecRejection(error, command, args, timeout) {
61
+ function toExecRejection(error, command, args, timeout, output) {
61
62
  if (timeout !== undefined && error.name === 'AbortError') {
62
- return new ExecTimeoutError(command, args, timeout, error);
63
+ return new ExecTimeoutError(command, args, timeout, error, output);
63
64
  }
64
65
  return error;
65
66
  }
67
+ /**
68
+ * Snapshot of what two collectors hold, for carrying onto a timeout
69
+ * rejection. Without it a `git add -A` killed at its budget rejected with
70
+ * nothing but the budget — the partial stdout/stderr that said what git was
71
+ * doing when it died was already collected and simply dropped.
72
+ */
73
+ function collectedOutput(out, err) {
74
+ return {
75
+ stdout: out.getText(),
76
+ stderr: err.getText(),
77
+ truncated: out.wasTruncated() || err.wasTruncated(),
78
+ };
79
+ }
66
80
  function exitCodeFromClose(code, signal) {
67
81
  if (code !== null) {
68
82
  return code;
@@ -95,7 +109,7 @@ export async function exec(command, args, options = {}) {
95
109
  child.stdout.on('data', out.onData);
96
110
  child.stderr.on('data', err.onData);
97
111
  child.on('error', (error) => {
98
- reject(toExecRejection(error, command, args, options.timeout));
112
+ reject(toExecRejection(error, command, args, options.timeout, collectedOutput(out, err)));
99
113
  });
100
114
  child.on('close', (code, signal) => {
101
115
  resolve({
@@ -109,48 +123,48 @@ export async function exec(command, args, options = {}) {
109
123
  });
110
124
  }
111
125
  /**
112
- * Executes a command and streams its output.
113
- * @param command - Command to execute
114
- * @param args - Command arguments
115
- * @param options - Execution options
116
- * @returns Exit code of the process
126
+ * The shared body of every spawn wrapper that outlives its call: detach
127
+ * decision, shutdown forwarding, closure tracking, process-group sweep and
128
+ * typed timeout rejection.
129
+ *
130
+ * `execStream`, `execInherit` and `execInheritCapture` each hand-rolled this
131
+ * sequence, and the copies drifted: `execStream` was the one that omitted
132
+ * {@link trackChildClosure}, so the bin signal handler did not wait for
133
+ * `runMachStream`'s children on Ctrl+C and the forwarder's SIGTERM → grace →
134
+ * SIGKILL escalation could not finish before the parent exited — the exact
135
+ * failure the tracking set was introduced to fix, left unwired in one copy.
136
+ * Keeping the sequence in one place is what stops that recurring.
137
+ *
138
+ * `exec` deliberately stays outside: it neither forwards signals nor tracks
139
+ * closure, because it is for short-lived helpers that no one Ctrl+Cs through.
140
+ * `execSmokeRun` also stays outside: its deadline owns the SIGTERM → grace →
141
+ * SIGKILL escalation directly, so it shares no forwarder with the others.
142
+ *
143
+ * @param spec - The variant's stdio, stream wiring, grace budget and result shape
144
+ * @returns Whatever `spec.result` builds from the child's `close` event
117
145
  */
118
- export async function execStream(command, args, options = {}) {
146
+ async function spawnTracked(spec) {
147
+ const { command, args, options } = spec;
119
148
  return new Promise((resolve, reject) => {
120
149
  const usesProcessGroup = options.processGroup === true && process.platform !== 'win32';
121
- const child = spawn(command, args, {
150
+ const child = spec.spawnChild({
122
151
  cwd: options.cwd,
123
152
  env: buildChildEnv(options),
124
- stdio: ['ignore', 'pipe', 'pipe'],
125
153
  signal: buildSignalFromTimeout(options.timeout),
126
154
  detached: usesProcessGroup,
127
155
  });
128
156
  const groupPid = usesProcessGroup ? child.pid : undefined;
129
- const stdoutDecoder = new StringDecoder('utf8');
130
- const stderrDecoder = new StringDecoder('utf8');
131
- child.stdout.on('data', (data) => {
132
- const chunk = stdoutDecoder.write(data);
133
- if (chunk.length > 0)
134
- options.onStdout?.(chunk);
135
- });
136
- child.stderr.on('data', (data) => {
137
- const chunk = stderrDecoder.write(data);
138
- if (chunk.length > 0)
139
- options.onStderr?.(chunk);
140
- });
141
- // A detached group leader no longer receives the terminal's Ctrl+C, so
142
- // the forwarder is mandatory (not just graceful-UX) when processGroup
143
- // is set. Group-aware: kills route to the whole tree.
144
- const forwarder = options.processGroup === true
145
- ? installGracefulShutdownForwarder(child, 1500, (signal) => {
157
+ spec.attach?.(child);
158
+ // Group-aware kills route to the whole tree; a non-group child is killed
159
+ // directly by the forwarder's own `child.kill`.
160
+ const killTarget = options.processGroup === true
161
+ ? (signal) => {
146
162
  killProcessTree(child, signal, usesProcessGroup);
147
- })
163
+ }
164
+ : undefined;
165
+ const forwarder = spec.forwardWithoutProcessGroup || options.processGroup === true
166
+ ? installGracefulShutdownForwarder(child, spec.graceMs, killTarget)
148
167
  : undefined;
149
- // Closure tracking: this was the one wrapper that omitted it, so the bin
150
- // signal handler did not wait for `runMachStream`'s children on Ctrl+C
151
- // and the forwarder's SIGTERM → grace → SIGKILL escalation above could
152
- // not finish before the parent exited — the exact failure the tracking
153
- // set was introduced to fix, left unwired here.
154
168
  const closure = trackChildClosure();
155
169
  child.on('error', (error) => {
156
170
  // Abort/startup failure: make sure a partially-started tree does not
@@ -161,13 +175,13 @@ export async function execStream(command, args, options = {}) {
161
175
  }
162
176
  forwarder?.dispose();
163
177
  closure.settle();
164
- reject(toExecRejection(error, command, args, options.timeout));
178
+ reject(toExecRejection(error, command, args, options.timeout, spec.rejectionOutput?.()));
165
179
  });
166
180
  child.on('close', (code, signal) => {
167
181
  forwarder?.dispose();
168
182
  const finish = () => {
169
183
  closure.settle();
170
- resolve(exitCodeFromClose(code, signal));
184
+ resolve(spec.result(code, signal));
171
185
  };
172
186
  if (groupPid !== undefined) {
173
187
  sweepProcessGroup(groupPid).then(finish, finish);
@@ -178,6 +192,41 @@ export async function execStream(command, args, options = {}) {
178
192
  });
179
193
  });
180
194
  }
195
+ /**
196
+ * Executes a command and streams its output.
197
+ * @param command - Command to execute
198
+ * @param args - Command arguments
199
+ * @param options - Execution options
200
+ * @returns Exit code of the process
201
+ */
202
+ export async function execStream(command, args, options = {}) {
203
+ return spawnTracked({
204
+ command,
205
+ args,
206
+ options,
207
+ graceMs: 1500,
208
+ // A detached group leader no longer receives the terminal's Ctrl+C, so
209
+ // the forwarder is mandatory (not just graceful-UX) when processGroup
210
+ // is set — but a plain streamed child does not get one.
211
+ forwardWithoutProcessGroup: false,
212
+ spawnChild: (base) => spawn(command, args, { ...base, stdio: ['ignore', 'pipe', 'pipe'] }),
213
+ attach: (child) => {
214
+ const stdoutDecoder = new StringDecoder('utf8');
215
+ const stderrDecoder = new StringDecoder('utf8');
216
+ child.stdout.on('data', (data) => {
217
+ const chunk = stdoutDecoder.write(data);
218
+ if (chunk.length > 0)
219
+ options.onStdout?.(chunk);
220
+ });
221
+ child.stderr.on('data', (data) => {
222
+ const chunk = stderrDecoder.write(data);
223
+ if (chunk.length > 0)
224
+ options.onStderr?.(chunk);
225
+ });
226
+ },
227
+ result: exitCodeFromClose,
228
+ });
229
+ }
181
230
  /**
182
231
  * Close-promises of children whose shutdown the CLI must wait for when a
183
232
  * termination signal arrives. A bin signal handler that calls `process.exit`
@@ -253,44 +302,14 @@ export async function waitForActiveChildShutdown(timeoutMs) {
253
302
  * @returns Exit code of the process
254
303
  */
255
304
  export async function execInherit(command, args, options = {}) {
256
- return new Promise((resolve, reject) => {
257
- const usesProcessGroup = options.processGroup === true && process.platform !== 'win32';
258
- const child = spawn(command, args, {
259
- cwd: options.cwd,
260
- env: buildChildEnv(options),
261
- stdio: 'inherit',
262
- signal: buildSignalFromTimeout(options.timeout),
263
- detached: usesProcessGroup,
264
- });
265
- const groupPid = usesProcessGroup ? child.pid : undefined;
266
- const graceMs = options.shutdownGraceMs ?? 1500;
267
- const { dispose } = installGracefulShutdownForwarder(child, graceMs, options.processGroup === true
268
- ? (signal) => {
269
- killProcessTree(child, signal, usesProcessGroup);
270
- }
271
- : undefined);
272
- const closure = trackChildClosure();
273
- child.on('error', (error) => {
274
- if (options.processGroup === true) {
275
- killProcessTree(child, 'SIGKILL', usesProcessGroup);
276
- }
277
- dispose();
278
- closure.settle();
279
- reject(toExecRejection(error, command, args, options.timeout));
280
- });
281
- child.on('close', (code, signal) => {
282
- dispose();
283
- const finish = () => {
284
- closure.settle();
285
- resolve(exitCodeFromClose(code, signal));
286
- };
287
- if (groupPid !== undefined) {
288
- sweepProcessGroup(groupPid).then(finish, finish);
289
- }
290
- else {
291
- finish();
292
- }
293
- });
305
+ return spawnTracked({
306
+ command,
307
+ args,
308
+ options,
309
+ graceMs: options.shutdownGraceMs ?? 1500,
310
+ forwardWithoutProcessGroup: true,
311
+ spawnChild: (base) => spawn(command, args, { ...base, stdio: 'inherit' }),
312
+ result: exitCodeFromClose,
294
313
  });
295
314
  }
296
315
  /**
@@ -365,54 +384,27 @@ export function installGracefulShutdownForwarder(child, graceMs, killTarget) {
365
384
  * @returns Execution result with stdout, stderr, and exit code
366
385
  */
367
386
  export async function execInheritCapture(command, args, options = {}) {
368
- return new Promise((resolve, reject) => {
369
- const usesProcessGroup = options.processGroup === true && process.platform !== 'win32';
370
- const child = spawn(command, args, {
371
- cwd: options.cwd,
372
- env: buildChildEnv(options),
373
- stdio: ['inherit', 'pipe', 'pipe'],
374
- signal: buildSignalFromTimeout(options.timeout),
375
- detached: usesProcessGroup,
376
- });
377
- const groupPid = usesProcessGroup ? child.pid : undefined;
378
- const out = createStreamCollector(options.mirror?.stdout ?? process.stdout);
379
- const err = createStreamCollector(options.mirror?.stderr ?? process.stderr);
380
- child.stdout.on('data', out.onData);
381
- child.stderr.on('data', err.onData);
382
- const graceMs = options.shutdownGraceMs ?? 1500;
383
- const { dispose } = installGracefulShutdownForwarder(child, graceMs, options.processGroup === true
384
- ? (signal) => {
385
- killProcessTree(child, signal, usesProcessGroup);
386
- }
387
- : undefined);
388
- const closure = trackChildClosure();
389
- child.on('error', (error) => {
390
- if (options.processGroup === true) {
391
- killProcessTree(child, 'SIGKILL', usesProcessGroup);
392
- }
393
- dispose();
394
- closure.settle();
395
- reject(toExecRejection(error, command, args, options.timeout));
396
- });
397
- child.on('close', (code, signal) => {
398
- dispose();
399
- const finish = () => {
400
- closure.settle();
401
- resolve({
402
- stdout: out.getText(),
403
- stderr: err.getText(),
404
- exitCode: exitCodeFromClose(code, signal),
405
- stdoutTruncated: out.wasTruncated(),
406
- stderrTruncated: err.wasTruncated(),
407
- });
408
- };
409
- if (groupPid !== undefined) {
410
- sweepProcessGroup(groupPid).then(finish, finish);
411
- }
412
- else {
413
- finish();
414
- }
415
- });
387
+ const out = createStreamCollector(options.mirror?.stdout ?? process.stdout);
388
+ const err = createStreamCollector(options.mirror?.stderr ?? process.stderr);
389
+ return spawnTracked({
390
+ command,
391
+ args,
392
+ options,
393
+ graceMs: options.shutdownGraceMs ?? 1500,
394
+ forwardWithoutProcessGroup: true,
395
+ spawnChild: (base) => spawn(command, args, { ...base, stdio: ['inherit', 'pipe', 'pipe'] }),
396
+ attach: (child) => {
397
+ child.stdout.on('data', out.onData);
398
+ child.stderr.on('data', err.onData);
399
+ },
400
+ result: (code, signal) => ({
401
+ stdout: out.getText(),
402
+ stderr: err.getText(),
403
+ exitCode: exitCodeFromClose(code, signal),
404
+ stdoutTruncated: out.wasTruncated(),
405
+ stderrTruncated: err.wasTruncated(),
406
+ }),
407
+ rejectionOutput: () => collectedOutput(out, err),
416
408
  });
417
409
  }
418
410
  /**
@@ -542,48 +534,6 @@ export async function execSmokeRun(command, args, options) {
542
534
  });
543
535
  });
544
536
  }
545
- /**
546
- * Cap on the partial-line tail kept between chunks. A child emitting one
547
- * enormous line with no terminator (minified JS dumped to stderr, a raw
548
- * binary blob) would otherwise grow the buffer without bound — the 50 MB cap
549
- * guards the collector, not these line buffers. When the tail exceeds the
550
- * cap it is dispatched as a synthetic (oversized) line so the matchers still
551
- * see its content, then the buffer resets.
552
- */
553
- const MAX_PARTIAL_LINE_SIZE = 1024 * 1024;
554
- /**
555
- * Drains complete lines from `buffer`, dispatching each to `cb`. Treats
556
- * `\n`, `\r\n`, and lone `\r` as line terminators — the lone-`\r` case
557
- * matters for progress-bar style output (mach, cargo) that repaints a line
558
- * with carriage returns and never sends a newline, which otherwise
559
- * accumulates indefinitely. A single trailing `\r` is held back since it may
560
- * be the first half of a `\r\n` pair split across chunks. Returns the
561
- * remaining partial line — callers keep accumulating into it.
562
- */
563
- function dispatchCompleteLines(buffer, cb) {
564
- let searchFrom = 0;
565
- for (;;) {
566
- const nl = buffer.indexOf('\n', searchFrom);
567
- const cr = buffer.indexOf('\r', searchFrom);
568
- const idx = nl === -1 ? cr : cr === -1 ? nl : Math.min(nl, cr);
569
- if (idx === -1)
570
- break;
571
- if (buffer[idx] === '\r' && idx === buffer.length - 1) {
572
- // Possible first half of a chunk-split \r\n — wait for the next chunk.
573
- break;
574
- }
575
- const line = buffer.slice(0, idx);
576
- const terminatorLength = buffer[idx] === '\r' && buffer[idx + 1] === '\n' ? 2 : 1;
577
- buffer = buffer.slice(idx + terminatorLength);
578
- searchFrom = 0;
579
- cb?.(line);
580
- }
581
- if (buffer.length > MAX_PARTIAL_LINE_SIZE) {
582
- cb?.(buffer);
583
- return '';
584
- }
585
- return buffer;
586
- }
587
537
  /**
588
538
  * Finds an executable in the system PATH.
589
539
  * @param name - Name of the executable
@@ -602,8 +552,7 @@ export async function findExecutable(name) {
602
552
  }
603
553
  return undefined;
604
554
  }
605
- catch (error) {
606
- void error;
555
+ catch {
607
556
  return undefined;
608
557
  }
609
558
  }
@@ -58,13 +58,13 @@ export function formatPsDuration(seconds) {
58
58
  if (Number.isNaN(seconds) || seconds < 0)
59
59
  return undefined;
60
60
  if (seconds < 60)
61
- return `${String(Math.floor(seconds))}s`;
61
+ return `${Math.floor(seconds)}s`;
62
62
  if (seconds < 3600) {
63
63
  const m = Math.floor(seconds / 60);
64
64
  const s = Math.floor(seconds % 60);
65
- return `${String(m)}m${String(s).padStart(2, '0')}s`;
65
+ return `${m}m${String(s).padStart(2, '0')}s`;
66
66
  }
67
67
  const h = Math.floor(seconds / 3600);
68
68
  const m = Math.floor((seconds % 3600) / 60);
69
- return `${String(h)}h${String(m).padStart(2, '0')}m`;
69
+ return `${h}h${String(m).padStart(2, '0')}m`;
70
70
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The single awaitable delay used across FireForge.
3
+ *
4
+ * Five modules had grown a private copy of this (`sleep`, `delay`,
5
+ * `sweepDelay`) and four more sites inlined
6
+ * `new Promise((resolve) => setTimeout(resolve, ms))`. The copies disagreed on
7
+ * the one property that actually matters — whether the pending timer holds the
8
+ * event loop open — and the disagreement was invisible at each call site. Both
9
+ * behaviours now live here, and callers state which one they need.
10
+ *
11
+ * The default is a ref'd timer, because the dangerous mistake is the other
12
+ * one: an unref'd delay awaited between a SIGTERM and its escalation lets Node
13
+ * exit mid-grace and skip the escalation entirely.
14
+ *
15
+ * Deliberately built on the global `setTimeout` rather than
16
+ * `node:timers/promises`: the suites that drive retry and grace-period logic
17
+ * use vitest fake timers, which patch the global but not the promisified
18
+ * module, so the tidier import would make every timing path untestable.
19
+ * @param ms - Milliseconds to wait.
20
+ * @param options - Pass `unref: true` when a pending delay must not keep the
21
+ * process alive (background probes, best-effort retries during shutdown).
22
+ * @returns A promise that resolves once the delay has elapsed.
23
+ */
24
+ export declare function sleep(ms: number, options?: {
25
+ unref?: boolean;
26
+ }): Promise<void>;
@@ -0,0 +1,31 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * The single awaitable delay used across FireForge.
4
+ *
5
+ * Five modules had grown a private copy of this (`sleep`, `delay`,
6
+ * `sweepDelay`) and four more sites inlined
7
+ * `new Promise((resolve) => setTimeout(resolve, ms))`. The copies disagreed on
8
+ * the one property that actually matters — whether the pending timer holds the
9
+ * event loop open — and the disagreement was invisible at each call site. Both
10
+ * behaviours now live here, and callers state which one they need.
11
+ *
12
+ * The default is a ref'd timer, because the dangerous mistake is the other
13
+ * one: an unref'd delay awaited between a SIGTERM and its escalation lets Node
14
+ * exit mid-grace and skip the escalation entirely.
15
+ *
16
+ * Deliberately built on the global `setTimeout` rather than
17
+ * `node:timers/promises`: the suites that drive retry and grace-period logic
18
+ * use vitest fake timers, which patch the global but not the promisified
19
+ * module, so the tidier import would make every timing path untestable.
20
+ * @param ms - Milliseconds to wait.
21
+ * @param options - Pass `unref: true` when a pending delay must not keep the
22
+ * process alive (background probes, best-effort retries during shutdown).
23
+ * @returns A promise that resolves once the delay has elapsed.
24
+ */
25
+ export function sleep(ms, options) {
26
+ return new Promise((resolve) => {
27
+ const timer = setTimeout(resolve, ms);
28
+ if (options?.unref === true)
29
+ timer.unref();
30
+ });
31
+ }
@@ -73,40 +73,12 @@ export declare function isValidFirefoxVersion(version: string): boolean;
73
73
  * Accepts formats like "build1", "build2", "build12" (no leading zero).
74
74
  */
75
75
  export declare function isValidFirefoxCandidate(candidate: string): boolean;
76
- /**
77
- * Naming contract for this module's checks.
78
- *
79
- * `validate*` is easy to spread across incompatible return contracts — some
80
- * throwing and returning void, some returning an issue list, some a parsed
81
- * value, some a boolean, some an error MESSAGE — and a caller cannot tell
82
- * which from the name. Passing a THROWING `validate*` to a clack `validate`
83
- * callback, which expects a returned message, kills the prompt instead of
84
- * re-prompting.
85
- *
86
- * The two dominant contracts keep the prefix:
87
- * - `validate*` — throws on failure, or returns an issue list.
88
- * - `is*` / `has*` — a type predicate or boolean.
89
- * The message-returning minority is `describe*Problem`, which reads as what
90
- * it is: a description of what is wrong, or `undefined`.
91
- *
92
- * `assert*` is uniform throughout: throw-or-continue.
93
- */
94
- /**
95
- * Valid Firefox product identifiers.
96
- *
97
- * `satisfies readonly FirefoxProduct[]` links the runtime list to the union
98
- * in ONE direction: it rejects an entry here that the union does not
99
- * declare. It cannot see a union member missing from this list — widening a
100
- * `readonly T[]` check never fails for being short. The reverse direction is
101
- * covered by the exhaustive switch in `utils/__tests__/validation.test.ts`,
102
- * which fails to compile when the union grows.
103
- */
104
76
  export declare const FIREFOX_PRODUCTS: readonly ["firefox", "firefox-esr", "firefox-beta", "firefox-devedition"];
105
77
  /**
106
78
  * Validates a Firefox product string.
107
79
  *
108
- * A type predicate, matching its siblings {@link isValidPatchCategory} and
109
- * {@link isValidProjectLicense}. Returning plain `boolean` would force
80
+ * A type predicate, matching its sibling {@link isValidProjectLicense}.
81
+ * Returning plain `boolean` would force
110
82
  * `as FirefoxProduct` casts at the call sites even though the `.includes`
111
83
  * check IS the runtime proof.
112
84
  */
@@ -141,10 +113,6 @@ export declare const isValidProjectLicense: (value: string) => value is "EUPL-1.
141
113
  * Valid patch categories.
142
114
  */
143
115
  export declare const PATCH_CATEGORIES: readonly ["branding", "ui", "privacy", "security", "infra"];
144
- /**
145
- * Validates a patch category string.
146
- */
147
- export declare const isValidPatchCategory: (value: string) => value is "branding" | "ui" | "privacy" | "security" | "infra";
148
116
  /**
149
117
  * Infers the Firefox product type from a version string.
150
118
  * Returns undefined if no clear inference can be made.
@@ -166,10 +134,6 @@ export declare function describeProductVersionIncompatibility(version: string, p
166
134
  * Accepts reverse-domain format like "org.example.browser"
167
135
  */
168
136
  export declare function isValidAppId(appId: string): boolean;
169
- /**
170
- * Checks if a value is defined (not undefined or null).
171
- */
172
- export declare function isDefined<T>(value: T | undefined | null): value is T;
173
137
  /**
174
138
  * Validates that a string is a legal CSS custom property identifier (the part after `--`).
175
139
  *
@@ -89,7 +89,7 @@ export function isArray(value) {
89
89
  * @returns True if value is a JSON object node
90
90
  */
91
91
  export function isJsonObject(value) {
92
- return typeof value === 'object' && value !== null && !Array.isArray(value);
92
+ return isObject(value);
93
93
  }
94
94
  /**
95
95
  * Validates a Firefox version string.
@@ -132,20 +132,21 @@ export function isValidFirefoxCandidate(candidate) {
132
132
  * in ONE direction: it rejects an entry here that the union does not
133
133
  * declare. It cannot see a union member missing from this list — widening a
134
134
  * `readonly T[]` check never fails for being short. The reverse direction is
135
- * covered by the exhaustive switch in `utils/__tests__/validation.test.ts`,
136
- * which fails to compile when the union grows.
135
+ * covered by the `ExhaustiveFirefoxProductList` operand below, which stops
136
+ * compiling when the union grows past this list.
137
137
  */
138
- export const FIREFOX_PRODUCTS = [
138
+ const FIREFOX_PRODUCT_VALUES = [
139
139
  'firefox',
140
140
  'firefox-esr',
141
141
  'firefox-beta',
142
142
  'firefox-devedition',
143
143
  ];
144
+ export const FIREFOX_PRODUCTS = FIREFOX_PRODUCT_VALUES;
144
145
  /**
145
146
  * Validates a Firefox product string.
146
147
  *
147
- * A type predicate, matching its siblings {@link isValidPatchCategory} and
148
- * {@link isValidProjectLicense}. Returning plain `boolean` would force
148
+ * A type predicate, matching its sibling {@link isValidProjectLicense}.
149
+ * Returning plain `boolean` would force
149
150
  * `as FirefoxProduct` casts at the call sites even though the `.includes`
150
151
  * check IS the runtime proof.
151
152
  */
@@ -182,10 +183,6 @@ export const isValidProjectLicense = makeEnumGuard(PROJECT_LICENSES);
182
183
  * Valid patch categories.
183
184
  */
184
185
  export const PATCH_CATEGORIES = ['branding', 'ui', 'privacy', 'security', 'infra'];
185
- /**
186
- * Validates a patch category string.
187
- */
188
- export const isValidPatchCategory = makeEnumGuard(PATCH_CATEGORIES);
189
186
  /**
190
187
  * Checks whether a Firefox version string has an ESR suffix.
191
188
  */
@@ -257,12 +254,6 @@ export function describeProductVersionIncompatibility(version, product) {
257
254
  export function isValidAppId(appId) {
258
255
  return /^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$/.test(appId);
259
256
  }
260
- /**
261
- * Checks if a value is defined (not undefined or null).
262
- */
263
- export function isDefined(value) {
264
- return value !== undefined && value !== null;
265
- }
266
257
  /**
267
258
  * Validates that a string is a legal CSS custom property identifier (the part after `--`).
268
259
  *