@hominis/fireforge 0.40.0 → 0.41.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 (253) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +16 -10
  3. package/dist/bin/fireforge.js +25 -4
  4. package/dist/build-info.json +9 -0
  5. package/dist/src/cli.js +20 -3
  6. package/dist/src/commands/bootstrap.js +5 -1
  7. package/dist/src/commands/discard.js +2 -22
  8. package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
  9. package/dist/src/commands/doctor-check-core.d.ts +20 -0
  10. package/dist/src/commands/doctor-check-core.js +28 -0
  11. package/dist/src/commands/doctor-external-toolchains.js +2 -0
  12. package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
  13. package/dist/src/commands/doctor-furnace.js +13 -11
  14. package/dist/src/commands/doctor-orphaned-harness.js +2 -1
  15. package/dist/src/commands/doctor.js +2 -2
  16. package/dist/src/commands/export-all.js +9 -1
  17. package/dist/src/commands/export-flow.js +13 -3
  18. package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
  19. package/dist/src/commands/export-placement-conflicts.js +74 -0
  20. package/dist/src/commands/export-placement-gate.js +1 -1
  21. package/dist/src/commands/export-shared.d.ts +8 -1
  22. package/dist/src/commands/export-shared.js +16 -7
  23. package/dist/src/commands/export.js +2 -2
  24. package/dist/src/commands/furnace/apply.js +4 -3
  25. package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
  26. package/dist/src/commands/furnace/chrome-doc.js +13 -2
  27. package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
  28. package/dist/src/commands/furnace/create-browser-test.js +5 -0
  29. package/dist/src/commands/furnace/create.js +2 -2
  30. package/dist/src/commands/furnace/diff.js +2 -1
  31. package/dist/src/commands/furnace/init.js +4 -6
  32. package/dist/src/commands/furnace/override.js +2 -2
  33. package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
  34. package/dist/src/commands/furnace/remove-mutations.js +171 -0
  35. package/dist/src/commands/furnace/remove.js +21 -128
  36. package/dist/src/commands/furnace/rename-browser-test.js +54 -5
  37. package/dist/src/commands/furnace/scan.js +27 -36
  38. package/dist/src/commands/furnace/status.js +30 -28
  39. package/dist/src/commands/furnace/validate.js +25 -18
  40. package/dist/src/commands/import.js +6 -20
  41. package/dist/src/commands/lint-per-patch.js +12 -59
  42. package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
  43. package/dist/src/commands/lint-per-run-checkjs.js +102 -0
  44. package/dist/src/commands/lint.js +4 -4
  45. package/dist/src/commands/manifest.js +1 -1
  46. package/dist/src/commands/package.js +9 -16
  47. package/dist/src/commands/patch/compact.js +5 -5
  48. package/dist/src/commands/patch/delete.js +5 -5
  49. package/dist/src/commands/patch/lint-ignore.js +3 -2
  50. package/dist/src/commands/patch/move-files-create.js +22 -4
  51. package/dist/src/commands/patch/move-files-into.js +14 -10
  52. package/dist/src/commands/patch/move-files.js +6 -5
  53. package/dist/src/commands/patch/rename.d.ts +2 -2
  54. package/dist/src/commands/patch/rename.js +151 -50
  55. package/dist/src/commands/patch/reorder.d.ts +9 -0
  56. package/dist/src/commands/patch/reorder.js +14 -10
  57. package/dist/src/commands/patch/split-plan.d.ts +13 -6
  58. package/dist/src/commands/patch/split-plan.js +22 -12
  59. package/dist/src/commands/patch/split.d.ts +1 -1
  60. package/dist/src/commands/patch/split.js +23 -9
  61. package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
  62. package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
  63. package/dist/src/commands/patch/staged-dependency.js +10 -0
  64. package/dist/src/commands/re-export-adjacent.js +5 -6
  65. package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
  66. package/dist/src/commands/re-export-bulk-scan.js +142 -2
  67. package/dist/src/commands/re-export-drift.d.ts +74 -0
  68. package/dist/src/commands/re-export-drift.js +234 -0
  69. package/dist/src/commands/re-export-files.js +5 -3
  70. package/dist/src/commands/re-export-lint.d.ts +97 -0
  71. package/dist/src/commands/re-export-lint.js +210 -0
  72. package/dist/src/commands/re-export-options.js +8 -0
  73. package/dist/src/commands/re-export-register.d.ts +18 -0
  74. package/dist/src/commands/re-export-register.js +61 -0
  75. package/dist/src/commands/re-export-scan.js +13 -7
  76. package/dist/src/commands/re-export-single.d.ts +20 -0
  77. package/dist/src/commands/re-export-single.js +261 -0
  78. package/dist/src/commands/re-export.d.ts +0 -4
  79. package/dist/src/commands/re-export.js +113 -268
  80. package/dist/src/commands/rebase/abort.js +13 -4
  81. package/dist/src/commands/rebase/continue.js +11 -4
  82. package/dist/src/commands/rebase/index.js +19 -29
  83. package/dist/src/commands/rebase/patch-loop.js +12 -0
  84. package/dist/src/commands/reset.js +4 -23
  85. package/dist/src/commands/resolve.js +2 -9
  86. package/dist/src/commands/run.js +11 -21
  87. package/dist/src/commands/source.js +3 -9
  88. package/dist/src/commands/status-check.d.ts +13 -0
  89. package/dist/src/commands/status-check.js +31 -13
  90. package/dist/src/commands/status-json.d.ts +31 -0
  91. package/dist/src/commands/status-json.js +76 -0
  92. package/dist/src/commands/status-output.d.ts +1 -0
  93. package/dist/src/commands/status-output.js +10 -1
  94. package/dist/src/commands/status-ownership.d.ts +52 -0
  95. package/dist/src/commands/status-ownership.js +57 -0
  96. package/dist/src/commands/status.js +184 -175
  97. package/dist/src/commands/test-build-phase.d.ts +19 -0
  98. package/dist/src/commands/test-build-phase.js +168 -0
  99. package/dist/src/commands/test-diagnose.js +30 -2
  100. package/dist/src/commands/test-modes.d.ts +9 -1
  101. package/dist/src/commands/test-modes.js +46 -13
  102. package/dist/src/commands/test-register.js +59 -27
  103. package/dist/src/commands/test-run.d.ts +29 -3
  104. package/dist/src/commands/test-run.js +41 -4
  105. package/dist/src/commands/test-verdict.d.ts +32 -0
  106. package/dist/src/commands/test-verdict.js +48 -0
  107. package/dist/src/commands/test.d.ts +9 -0
  108. package/dist/src/commands/test.js +100 -123
  109. package/dist/src/commands/token-coverage.js +4 -9
  110. package/dist/src/commands/tree.d.ts +3 -0
  111. package/dist/src/commands/tree.js +171 -25
  112. package/dist/src/commands/verify.js +2 -2
  113. package/dist/src/commands/watch.js +22 -21
  114. package/dist/src/commands/wire.js +1 -1
  115. package/dist/src/core/ast-utils.d.ts +11 -0
  116. package/dist/src/core/ast-utils.js +11 -0
  117. package/dist/src/core/build-audit-platform.js +6 -1
  118. package/dist/src/core/build-audit-registration.js +2 -0
  119. package/dist/src/core/build-audit-resolve.d.ts +13 -0
  120. package/dist/src/core/build-audit-resolve.js +34 -13
  121. package/dist/src/core/build-audit.js +4 -20
  122. package/dist/src/core/build-baseline-types.d.ts +29 -8
  123. package/dist/src/core/build-baseline-types.js +2 -1
  124. package/dist/src/core/build-baseline.d.ts +10 -2
  125. package/dist/src/core/build-baseline.js +39 -8
  126. package/dist/src/core/build-prepare.d.ts +4 -0
  127. package/dist/src/core/build-prepare.js +7 -1
  128. package/dist/src/core/config-paths.d.ts +1 -1
  129. package/dist/src/core/config-paths.js +2 -0
  130. package/dist/src/core/config-validate-test-toolchains.js +3 -2
  131. package/dist/src/core/config-validate.js +34 -36
  132. package/dist/src/core/config.js +16 -3
  133. package/dist/src/core/coverage-extend.d.ts +89 -0
  134. package/dist/src/core/coverage-extend.js +188 -0
  135. package/dist/src/core/destructive.js +3 -1
  136. package/dist/src/core/discard-baseline.js +18 -4
  137. package/dist/src/core/engine-precondition.d.ts +35 -0
  138. package/dist/src/core/engine-precondition.js +50 -0
  139. package/dist/src/core/engine-session-lock.d.ts +38 -1
  140. package/dist/src/core/engine-session-lock.js +78 -4
  141. package/dist/src/core/file-lock.d.ts +7 -0
  142. package/dist/src/core/file-lock.js +20 -30
  143. package/dist/src/core/firefox.js +5 -2
  144. package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
  145. package/dist/src/core/furnace-apply-helpers.js +12 -3
  146. package/dist/src/core/furnace-apply-output.js +9 -0
  147. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
  148. package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
  149. package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
  150. package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
  151. package/dist/src/core/furnace-apply.d.ts +16 -4
  152. package/dist/src/core/furnace-apply.js +39 -43
  153. package/dist/src/core/furnace-config.js +2 -0
  154. package/dist/src/core/furnace-css-fragments.js +2 -0
  155. package/dist/src/core/furnace-jsconfig.js +14 -7
  156. package/dist/src/core/furnace-registration-ast.js +8 -8
  157. package/dist/src/core/furnace-registration-remove.js +6 -3
  158. package/dist/src/core/furnace-registration.js +9 -12
  159. package/dist/src/core/furnace-rollback.js +1 -1
  160. package/dist/src/core/furnace-scanner.js +2 -1
  161. package/dist/src/core/furnace-stale-export.js +2 -1
  162. package/dist/src/core/furnace-state-persist.d.ts +4 -6
  163. package/dist/src/core/furnace-validate-compatibility.js +4 -7
  164. package/dist/src/core/furnace-validate-helpers.js +33 -1
  165. package/dist/src/core/furnace-validate-registration.js +16 -0
  166. package/dist/src/core/furnace-validate.js +4 -2
  167. package/dist/src/core/git-diff.js +37 -2
  168. package/dist/src/core/lint-cache.d.ts +1 -1
  169. package/dist/src/core/lint-cache.js +33 -10
  170. package/dist/src/core/mach-build-artifacts.d.ts +63 -1
  171. package/dist/src/core/mach-build-artifacts.js +108 -2
  172. package/dist/src/core/mach-resource-shim.js +1 -0
  173. package/dist/src/core/marionette-port.d.ts +16 -0
  174. package/dist/src/core/marionette-port.js +93 -8
  175. package/dist/src/core/marionette-preflight.js +6 -5
  176. package/dist/src/core/patch-apply.d.ts +20 -13
  177. package/dist/src/core/patch-apply.js +32 -33
  178. package/dist/src/core/patch-export.js +3 -5
  179. package/dist/src/core/patch-lint-checkjs.d.ts +25 -3
  180. package/dist/src/core/patch-lint-checkjs.js +78 -7
  181. package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
  182. package/dist/src/core/patch-lint-creators.d.ts +30 -0
  183. package/dist/src/core/patch-lint-creators.js +45 -0
  184. package/dist/src/core/patch-lint-cross.d.ts +1 -17
  185. package/dist/src/core/patch-lint-cross.js +16 -32
  186. package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
  187. package/dist/src/core/patch-lint-jsdoc.js +15 -5
  188. package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
  189. package/dist/src/core/patch-lint-module-registration.js +88 -0
  190. package/dist/src/core/patch-lint-mozbuild.js +99 -11
  191. package/dist/src/core/patch-lint-reexports.d.ts +1 -0
  192. package/dist/src/core/patch-lint-reexports.js +1 -0
  193. package/dist/src/core/patch-lint.d.ts +8 -0
  194. package/dist/src/core/patch-lint.js +84 -6
  195. package/dist/src/core/patch-lock.d.ts +12 -1
  196. package/dist/src/core/patch-lock.js +40 -4
  197. package/dist/src/core/patch-manifest-consistency.js +15 -3
  198. package/dist/src/core/patch-parse.d.ts +8 -0
  199. package/dist/src/core/patch-parse.js +10 -0
  200. package/dist/src/core/patch-policy.d.ts +7 -0
  201. package/dist/src/core/patch-policy.js +4 -1
  202. package/dist/src/core/patch-transform.d.ts +10 -0
  203. package/dist/src/core/patch-transform.js +13 -2
  204. package/dist/src/core/rebase-session.d.ts +44 -2
  205. package/dist/src/core/rebase-session.js +114 -17
  206. package/dist/src/core/register-browser-content.js +2 -1
  207. package/dist/src/core/register-shared-css.js +2 -1
  208. package/dist/src/core/smoke-patterns.d.ts +0 -8
  209. package/dist/src/core/smoke-patterns.js +3 -2
  210. package/dist/src/core/status-binary.d.ts +55 -0
  211. package/dist/src/core/status-binary.js +124 -0
  212. package/dist/src/core/status-classify.d.ts +6 -1
  213. package/dist/src/core/status-classify.js +101 -73
  214. package/dist/src/core/test-harness-crash.d.ts +32 -1
  215. package/dist/src/core/test-harness-crash.js +59 -4
  216. package/dist/src/core/test-path-scope.js +2 -0
  217. package/dist/src/core/test-stale-check.js +1 -20
  218. package/dist/src/core/toolchain-preflight.js +24 -26
  219. package/dist/src/core/tree-guard.d.ts +1 -1
  220. package/dist/src/core/tree-guard.js +67 -12
  221. package/dist/src/core/tree-store.d.ts +86 -10
  222. package/dist/src/core/tree-store.js +310 -72
  223. package/dist/src/core/typecheck.js +2 -1
  224. package/dist/src/core/wire-destroy.js +14 -3
  225. package/dist/src/core/wire-init.js +37 -40
  226. package/dist/src/core/wire-subscript.js +11 -3
  227. package/dist/src/core/wire-utils.js +4 -4
  228. package/dist/src/core/xpcshell-appdir.js +3 -0
  229. package/dist/src/errors/base.d.ts +13 -0
  230. package/dist/src/errors/base.js +13 -0
  231. package/dist/src/errors/rebase.d.ts +13 -0
  232. package/dist/src/errors/rebase.js +17 -0
  233. package/dist/src/types/commands/options.d.ts +86 -1
  234. package/dist/src/types/commands/patches.d.ts +8 -0
  235. package/dist/src/types/furnace.d.ts +7 -0
  236. package/dist/src/utils/build-info.d.ts +31 -0
  237. package/dist/src/utils/build-info.js +98 -0
  238. package/dist/src/utils/concurrency.d.ts +8 -0
  239. package/dist/src/utils/concurrency.js +21 -0
  240. package/dist/src/utils/errors.d.ts +39 -0
  241. package/dist/src/utils/errors.js +55 -0
  242. package/dist/src/utils/fs.js +5 -12
  243. package/dist/src/utils/logger.d.ts +14 -2
  244. package/dist/src/utils/logger.js +43 -13
  245. package/dist/src/utils/options.d.ts +21 -4
  246. package/dist/src/utils/options.js +26 -6
  247. package/dist/src/utils/package-root.js +7 -4
  248. package/dist/src/utils/process-group.js +13 -2
  249. package/dist/src/utils/stdio-drain.d.ts +28 -0
  250. package/dist/src/utils/stdio-drain.js +54 -0
  251. package/dist/src/utils/validation.d.ts +23 -2
  252. package/dist/src/utils/validation.js +29 -3
  253. package/package.json +9 -9
@@ -10,6 +10,8 @@ export declare function setVerbose(enabled: boolean): void;
10
10
  export declare function setMachineOutputMode(enabled: boolean): void;
11
11
  /** True when machine-output mode is active. */
12
12
  export declare function isMachineOutputMode(): boolean;
13
+ /** Seals (or unseals) stdout after a machine-readable final line. */
14
+ export declare function setStdoutSealed(sealed: boolean): void;
13
15
  /**
14
16
  * Displays a verbose/debug message (only shown if verbose mode is enabled).
15
17
  * @param message - Message to display
@@ -54,7 +56,17 @@ export declare function formatErrorText(message: string): string;
54
56
  export declare function spinner(initialMessage: string): SpinnerHandle;
55
57
  /** Emits a cancellation message. */
56
58
  export declare function cancel(message: string): void;
57
- /** Checks whether a prompt result represents a user cancellation. */
58
- export declare function isCancel(value: unknown): boolean;
59
+ /**
60
+ * Checks whether a prompt result represents a user cancellation.
61
+ *
62
+ * Narrows to `symbol` — clack returns its cancel sentinel as a symbol and
63
+ * `p.isCancel` is itself a type predicate. This wrapper erased that narrowing
64
+ * by returning plain `boolean` until 0.41.0, which forced eleven `as string`
65
+ * / `as PatchCategory` / `as OverrideType` casts across six command modules
66
+ * on the *non*-cancelled branch, where the value is already known not to be
67
+ * the sentinel. Keep the predicate form: it is the single highest-leverage
68
+ * signature in the codebase for escape-hatch count.
69
+ */
70
+ export declare function isCancel(value: unknown): value is symbol;
59
71
  /** Displays a titled note block for follow-up details. */
60
72
  export declare function note(message: string, title?: string): void;
@@ -34,6 +34,26 @@ export function setMachineOutputMode(enabled) {
34
34
  export function isMachineOutputMode() {
35
35
  return machineOutputMode;
36
36
  }
37
+ /**
38
+ * Whether stdout is sealed behind a machine-readable final line.
39
+ *
40
+ * The `FIREFORGE-VERDICT:` contract promises that line as the run's LAST
41
+ * stdout write — but emit-then-throw failure paths rethrow into
42
+ * `withErrorHandling`, whose clack error/cancel rendering lands on stdout
43
+ * in non-machine mode, displacing the verdict from the final line. The
44
+ * verdict sink seals stdout the moment it writes; every later logger call
45
+ * routes to stderr through the same diagnostic channel machine mode uses.
46
+ * `withErrorHandling`'s finally clears the seal (alongside machine mode).
47
+ */
48
+ let stdoutSealed = false;
49
+ /** Seals (or unseals) stdout after a machine-readable final line. */
50
+ export function setStdoutSealed(sealed) {
51
+ stdoutSealed = sealed;
52
+ }
53
+ /** True when human output must avoid stdout. */
54
+ function routeToStderr() {
55
+ return machineOutputMode || stdoutSealed;
56
+ }
37
57
  /** Writes a plain diagnostic line to stderr (machine-mode side channel). */
38
58
  function writeDiagnostic(prefix, message) {
39
59
  process.stderr.write(`${prefix}${message}\n`);
@@ -52,7 +72,7 @@ function isVerbose() {
52
72
  export function verbose(message) {
53
73
  if (!isVerbose())
54
74
  return;
55
- if (machineOutputMode) {
75
+ if (routeToStderr()) {
56
76
  writeDiagnostic('[debug] ', message);
57
77
  return;
58
78
  }
@@ -63,19 +83,19 @@ function supportsInteractiveSpinner() {
63
83
  }
64
84
  /** Displays the top-level intro banner for a command. */
65
85
  export function intro(message) {
66
- if (machineOutputMode)
86
+ if (routeToStderr())
67
87
  return;
68
88
  p.intro(message);
69
89
  }
70
90
  /** Displays the closing outro banner for a command. */
71
91
  export function outro(message) {
72
- if (machineOutputMode)
92
+ if (routeToStderr())
73
93
  return;
74
94
  p.outro(message);
75
95
  }
76
96
  /** Logs an informational message. */
77
97
  export function info(message) {
78
- if (machineOutputMode) {
98
+ if (routeToStderr()) {
79
99
  writeDiagnostic('', message);
80
100
  return;
81
101
  }
@@ -83,7 +103,7 @@ export function info(message) {
83
103
  }
84
104
  /** Logs a success message. */
85
105
  export function success(message) {
86
- if (machineOutputMode) {
106
+ if (routeToStderr()) {
87
107
  writeDiagnostic('', message);
88
108
  return;
89
109
  }
@@ -91,7 +111,7 @@ export function success(message) {
91
111
  }
92
112
  /** Logs a warning message. */
93
113
  export function warn(message) {
94
- if (machineOutputMode) {
114
+ if (routeToStderr()) {
95
115
  writeDiagnostic('warning: ', message);
96
116
  return;
97
117
  }
@@ -99,7 +119,7 @@ export function warn(message) {
99
119
  }
100
120
  /** Logs an error message. */
101
121
  export function error(message) {
102
- if (machineOutputMode) {
122
+ if (routeToStderr()) {
103
123
  writeDiagnostic('error: ', message);
104
124
  return;
105
125
  }
@@ -107,7 +127,7 @@ export function error(message) {
107
127
  }
108
128
  /** Logs an in-progress step message. */
109
129
  export function step(message) {
110
- if (machineOutputMode) {
130
+ if (routeToStderr()) {
111
131
  writeDiagnostic('', message);
112
132
  return;
113
133
  }
@@ -115,7 +135,7 @@ export function step(message) {
115
135
  }
116
136
  /** Logs a plain message without a status prefix. */
117
137
  export function message(message) {
118
- if (machineOutputMode) {
138
+ if (routeToStderr()) {
119
139
  writeDiagnostic('', message);
120
140
  return;
121
141
  }
@@ -135,7 +155,7 @@ export function formatErrorText(message) {
135
155
  * @returns Spinner handle with message(), stop(), and error() methods
136
156
  */
137
157
  export function spinner(initialMessage) {
138
- if (machineOutputMode) {
158
+ if (routeToStderr()) {
139
159
  // Spinner progress is diagnostics; keep stdout clean for the payload.
140
160
  return {
141
161
  message: (msg) => {
@@ -182,19 +202,29 @@ export function spinner(initialMessage) {
182
202
  }
183
203
  /** Emits a cancellation message. */
184
204
  export function cancel(message) {
185
- if (machineOutputMode) {
205
+ if (routeToStderr()) {
186
206
  writeDiagnostic('cancelled: ', message);
187
207
  return;
188
208
  }
189
209
  p.cancel(message);
190
210
  }
191
- /** Checks whether a prompt result represents a user cancellation. */
211
+ /**
212
+ * Checks whether a prompt result represents a user cancellation.
213
+ *
214
+ * Narrows to `symbol` — clack returns its cancel sentinel as a symbol and
215
+ * `p.isCancel` is itself a type predicate. This wrapper erased that narrowing
216
+ * by returning plain `boolean` until 0.41.0, which forced eleven `as string`
217
+ * / `as PatchCategory` / `as OverrideType` casts across six command modules
218
+ * on the *non*-cancelled branch, where the value is already known not to be
219
+ * the sentinel. Keep the predicate form: it is the single highest-leverage
220
+ * signature in the codebase for escape-hatch count.
221
+ */
192
222
  export function isCancel(value) {
193
223
  return p.isCancel(value);
194
224
  }
195
225
  /** Displays a titled note block for follow-up details. */
196
226
  export function note(message, title) {
197
- if (machineOutputMode) {
227
+ if (routeToStderr()) {
198
228
  writeDiagnostic('', title ? `${title}: ${message}` : message);
199
229
  return;
200
230
  }
@@ -22,6 +22,21 @@ import type { Command } from 'commander';
22
22
  * helper (or throw commander's InvalidArgumentError directly).
23
23
  */
24
24
  export declare function commanderArgParser<T>(parse: (raw: string) => T): (raw: string) => T;
25
+ /**
26
+ * Commander plumbing for a repeatable string option, as a spreadable
27
+ * `[accumulator, default]` pair:
28
+ *
29
+ * ```ts
30
+ * .option('--mach-arg <arg>', 'Repeatable.', ...stringListOption())
31
+ * ```
32
+ *
33
+ * Seven call sites open-coded this, each with its own `[] as string[]`
34
+ * default — commander types that slot as `unknown`, so every one carried the
35
+ * cast. Two of the seven also mutated the accumulator in place rather than
36
+ * returning a new array. Returning a fresh array per call additionally means
37
+ * the default cannot be shared between two `.option()` registrations.
38
+ */
39
+ export declare function stringListOption(): [(value: string, previous: string[]) => string[], string[]];
25
40
  /**
26
41
  * Resolves the parsed `--wait-lock [seconds]` option value into a wait budget
27
42
  * in seconds for `withEngineSessionLock`:
@@ -35,9 +50,11 @@ export declare function commanderArgParser<T>(parse: (raw: string) => T): (raw:
35
50
  */
36
51
  export declare function resolveWaitLockSeconds(value: string | number | boolean | undefined): number | undefined;
37
52
  /**
38
- * Registers the shared `--wait-lock [seconds]` flag on an engine-mutating
39
- * command. The parsed option value is `true` for the bare flag or a validated
40
- * integer; feed it through {@link resolveWaitLockSeconds} at the call site to
41
- * obtain the `waitLockSeconds` for `withEngineSessionLock`.
53
+ * Registers the shared `--wait-lock [seconds]` flag on a lock-taking
54
+ * command (the engine session lock for engine-mutating commands, the patch
55
+ * directory lock for queue-mutating ones). The parsed option value is `true`
56
+ * for the bare flag or a validated integer; feed it through
57
+ * {@link resolveWaitLockSeconds} at the call site to obtain the
58
+ * `waitLockSeconds` for `withEngineSessionLock`/`withPatchDirectoryLock`.
42
59
  */
43
60
  export declare function addWaitLockOption(command: Command): Command;
@@ -15,6 +15,7 @@ export function pickDefined(obj) {
15
15
  }
16
16
  import { InvalidArgumentError as CommanderInvalidArgumentError } from 'commander';
17
17
  import { GeneralError } from '../errors/base.js';
18
+ import { toError } from './errors.js';
18
19
  /**
19
20
  * Wraps an option-argument parser so its failures surface through
20
21
  * commander's own invalid-argument channel.
@@ -35,10 +36,27 @@ export function commanderArgParser(parse) {
35
36
  return parse(raw);
36
37
  }
37
38
  catch (error) {
38
- throw new CommanderInvalidArgumentError(error instanceof Error ? error.message : String(error));
39
+ throw new CommanderInvalidArgumentError(toError(error).message);
39
40
  }
40
41
  };
41
42
  }
43
+ /**
44
+ * Commander plumbing for a repeatable string option, as a spreadable
45
+ * `[accumulator, default]` pair:
46
+ *
47
+ * ```ts
48
+ * .option('--mach-arg <arg>', 'Repeatable.', ...stringListOption())
49
+ * ```
50
+ *
51
+ * Seven call sites open-coded this, each with its own `[] as string[]`
52
+ * default — commander types that slot as `unknown`, so every one carried the
53
+ * cast. Two of the seven also mutated the accumulator in place rather than
54
+ * returning a new array. Returning a fresh array per call additionally means
55
+ * the default cannot be shared between two `.option()` registrations.
56
+ */
57
+ export function stringListOption() {
58
+ return [(value, previous) => [...previous, value], []];
59
+ }
42
60
  /** Wait budget applied when `--wait-lock` is passed without a value. */
43
61
  const DEFAULT_WAIT_LOCK_SECONDS = 60;
44
62
  /**
@@ -69,11 +87,13 @@ export function resolveWaitLockSeconds(value) {
69
87
  return n;
70
88
  }
71
89
  /**
72
- * Registers the shared `--wait-lock [seconds]` flag on an engine-mutating
73
- * command. The parsed option value is `true` for the bare flag or a validated
74
- * integer; feed it through {@link resolveWaitLockSeconds} at the call site to
75
- * obtain the `waitLockSeconds` for `withEngineSessionLock`.
90
+ * Registers the shared `--wait-lock [seconds]` flag on a lock-taking
91
+ * command (the engine session lock for engine-mutating commands, the patch
92
+ * directory lock for queue-mutating ones). The parsed option value is `true`
93
+ * for the bare flag or a validated integer; feed it through
94
+ * {@link resolveWaitLockSeconds} at the call site to obtain the
95
+ * `waitLockSeconds` for `withEngineSessionLock`/`withPatchDirectoryLock`.
76
96
  */
77
97
  export function addWaitLockOption(command) {
78
- return command.option('--wait-lock [seconds]', 'Wait up to this many seconds (default 60) for another FireForge engine-mutating command to release the engine lock, instead of failing immediately', commanderArgParser((raw) => resolveWaitLockSeconds(raw)));
98
+ return command.option('--wait-lock [seconds]', 'Wait up to this many seconds (default 60) for another FireForge command to release the contended lock, instead of failing on the default budget', commanderArgParser((raw) => resolveWaitLockSeconds(raw)));
79
99
  }
@@ -2,6 +2,8 @@
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
+ import { getNodeErrorCode, toError } from './errors.js';
6
+ import { isObject } from './validation.js';
5
7
  function validatePackageMetadata(data, filePath) {
6
8
  if (typeof data !== 'object' || data === null) {
7
9
  throw new Error(`Invalid package metadata in ${filePath}: expected an object`);
@@ -48,10 +50,11 @@ export function getPackageRoot() {
48
50
  // to a wrong ancestor package (any parent exposing bin.fireforge) or
49
51
  // end in the unhelpful generic "could not locate" error, hiding the
50
52
  // actual syntax problem.
51
- const code = error.code;
53
+ const code = getNodeErrorCode(error);
52
54
  if (code !== 'ENOENT' && code !== 'ENOTDIR') {
53
- throw new Error(`Found ${packagePath} but could not parse it: ` +
54
- (error instanceof Error ? error.message : String(error)), { cause: error });
55
+ throw new Error(`Found ${packagePath} but could not parse it: ` + toError(error).message, {
56
+ cause: error,
57
+ });
55
58
  }
56
59
  }
57
60
  const parent = dirname(current);
@@ -63,7 +66,7 @@ export function getPackageRoot() {
63
66
  }
64
67
  /** @internal */
65
68
  export function isFireForgePackageMetadata(pkg) {
66
- if (typeof pkg.bin !== 'object' || pkg.bin === null || Array.isArray(pkg.bin)) {
69
+ if (!isObject(pkg.bin)) {
67
70
  return false;
68
71
  }
69
72
  const bin = pkg.bin;
@@ -6,6 +6,7 @@
6
6
  * two modules do not import each other cyclically.
7
7
  */
8
8
  import { spawn } from 'node:child_process';
9
+ import { getNodeErrorCode } from './errors.js';
9
10
  import { verbose, warn } from './logger.js';
10
11
  /**
11
12
  * Sends `signal` to the child's whole tree: the process GROUP on POSIX
@@ -99,8 +100,14 @@ async function listGroupSurvivors(pgid) {
99
100
  process.kill(-pgid, 0);
100
101
  return [{ pid: -1, command: 'unknown (pgrep unavailable; group still has live members)' }];
101
102
  }
102
- catch {
103
- return [];
103
+ catch (error) {
104
+ // Same errno semantics as `isProcessAlive` (which cannot be called here —
105
+ // it probes a single positive pid): only ESRCH proves the group is gone.
106
+ // EPERM means it exists under another uid, and an unrecognized failure is
107
+ // not evidence of absence — report survivors in both cases.
108
+ if (getNodeErrorCode(error) === 'ESRCH')
109
+ return [];
110
+ return [{ pid: -1, command: 'unknown (pgrep unavailable; group liveness probe denied)' }];
104
111
  }
105
112
  }
106
113
  function describeSurvivors(list) {
@@ -133,6 +140,8 @@ export async function sweepProcessGroup(pgid, graceMs = SWEEP_GRACE_MS) {
133
140
  process.kill(-pgid, 'SIGTERM');
134
141
  }
135
142
  catch {
143
+ // The group vanished between the listing and the signal — nothing left to
144
+ // escalate against, so report the survivors collected so far.
136
145
  return { survivors };
137
146
  }
138
147
  await sweepDelay(graceMs);
@@ -142,6 +151,8 @@ export async function sweepProcessGroup(pgid, graceMs = SWEEP_GRACE_MS) {
142
151
  process.kill(-pgid, 'SIGKILL');
143
152
  }
144
153
  catch {
154
+ // The group died during the grace period, so the SIGKILL escalation has
155
+ // nothing to signal.
145
156
  return { survivors };
146
157
  }
147
158
  await sweepDelay(Math.min(200, graceMs));
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Bounded stdio drain for delayed process exits (FORGE I1).
3
+ *
4
+ * `process.exit()` discards whatever is still queued in an async writable —
5
+ * and stdout IS async when it is a pipe, with a 64 KiB kernel buffer on
6
+ * macOS/Linux. The observed failure: `status --json --fail-on` writes a
7
+ * 176 KB payload, the refusal makes `bin/fireforge.ts` exit non-zero, and a
8
+ * piped consumer receives exactly 65 536 bytes. The bin entry point awaits
9
+ * this helper before every delayed exit; the helper itself never calls
10
+ * `process.exit` (the process-boundary test keeps that a bin-only right).
11
+ */
12
+ /**
13
+ * The structural slice of `Writable` the drain wait needs — accepted instead
14
+ * of `NodeJS.WriteStream` so unit tests can drive synthetic streams.
15
+ */
16
+ export interface DrainableStream {
17
+ readonly destroyed: boolean;
18
+ readonly writableFinished: boolean;
19
+ readonly writableLength: number;
20
+ on(event: 'drain' | 'error' | 'close', listener: () => void): unknown;
21
+ off(event: 'drain' | 'error' | 'close', listener: () => void): unknown;
22
+ }
23
+ /**
24
+ * Waits (bounded by `timeoutMs`) for every given stream — stdout and stderr
25
+ * by default — to drain. Resolution is unconditional; the caller exits
26
+ * either way, the bound only decides how much queued output survives.
27
+ */
28
+ export declare function waitForStdioDrain(timeoutMs: number, streams?: readonly DrainableStream[]): Promise<void>;
@@ -0,0 +1,54 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Bounded stdio drain for delayed process exits (FORGE I1).
4
+ *
5
+ * `process.exit()` discards whatever is still queued in an async writable —
6
+ * and stdout IS async when it is a pipe, with a 64 KiB kernel buffer on
7
+ * macOS/Linux. The observed failure: `status --json --fail-on` writes a
8
+ * 176 KB payload, the refusal makes `bin/fireforge.ts` exit non-zero, and a
9
+ * piped consumer receives exactly 65 536 bytes. The bin entry point awaits
10
+ * this helper before every delayed exit; the helper itself never calls
11
+ * `process.exit` (the process-boundary test keeps that a bin-only right).
12
+ */
13
+ /** True when nothing the process is about to lose remains queued. */
14
+ function isFlushed(stream) {
15
+ return stream.destroyed || stream.writableFinished || stream.writableLength === 0;
16
+ }
17
+ /**
18
+ * Resolves once `stream` has drained its queued writes, and always within
19
+ * `timeoutMs`. Never rejects: `'error'` and `'close'` (an EPIPE'd pipe —
20
+ * `installBrokenPipeHandler` already swallows the error itself) resolve
21
+ * immediately, because a destroyed pipe can never drain and must not stall
22
+ * the exit behind it.
23
+ */
24
+ function waitForStreamDrain(stream, timeoutMs) {
25
+ if (isFlushed(stream)) {
26
+ return Promise.resolve();
27
+ }
28
+ return new Promise((resolve) => {
29
+ const settle = () => {
30
+ clearTimeout(timer);
31
+ stream.off('drain', onDrain);
32
+ stream.off('error', settle);
33
+ stream.off('close', settle);
34
+ resolve();
35
+ };
36
+ const onDrain = () => {
37
+ if (isFlushed(stream)) {
38
+ settle();
39
+ }
40
+ };
41
+ const timer = setTimeout(settle, timeoutMs);
42
+ stream.on('drain', onDrain);
43
+ stream.on('error', settle);
44
+ stream.on('close', settle);
45
+ });
46
+ }
47
+ /**
48
+ * Waits (bounded by `timeoutMs`) for every given stream — stdout and stderr
49
+ * by default — to drain. Resolution is unconditional; the caller exits
50
+ * either way, the bound only decides how much queued output survives.
51
+ */
52
+ export async function waitForStdioDrain(timeoutMs, streams = [process.stdout, process.stderr]) {
53
+ await Promise.all(streams.map((stream) => waitForStreamDrain(stream, timeoutMs)));
54
+ }
@@ -1,3 +1,4 @@
1
+ import type { FirefoxProduct } from '../types/config.js';
1
2
  /**
2
3
  * Checks whether a value is a string.
3
4
  * @param value - Value to check
@@ -62,11 +63,31 @@ export declare function isValidFirefoxVersion(version: string): boolean;
62
63
  * Accepts formats like "build1", "build2", "build12" (no leading zero).
63
64
  */
64
65
  export declare function isValidFirefoxCandidate(candidate: string): boolean;
66
+ /**
67
+ * Valid Firefox product identifiers.
68
+ *
69
+ * `satisfies readonly FirefoxProduct[]` links the runtime list to the union at
70
+ * `types/config.ts:5` in ONE direction: it rejects an entry here that the union
71
+ * does not declare. It cannot see a union member missing from this list —
72
+ * widening a `readonly T[]` check never fails for being short. The reverse
73
+ * direction is covered by the exhaustive switch in
74
+ * `utils/__tests__/validation.test.ts`, which fails to compile when the union
75
+ * grows.
76
+ *
77
+ * Two further copies of this list existed before 0.41.0 — an inline literal
78
+ * inside the validator below and `SOURCE_PRODUCTS` in `commands/source.ts` —
79
+ * neither linked to the union.
80
+ */
81
+ export declare const FIREFOX_PRODUCTS: readonly ["firefox", "firefox-esr", "firefox-beta", "firefox-devedition"];
65
82
  /**
66
83
  * Validates a Firefox product string.
67
- * Accepts: firefox, firefox-esr, firefox-beta, firefox-devedition
84
+ *
85
+ * A type predicate, matching its siblings {@link isValidPatchCategory} and
86
+ * {@link isValidProjectLicense}. It returned plain `boolean` until 0.41.0,
87
+ * which forced `as FirefoxProduct` casts at both call sites even though the
88
+ * `.includes` check IS the runtime proof.
68
89
  */
69
- export declare function isValidFirefoxProduct(product: string): boolean;
90
+ export declare function isValidFirefoxProduct(product: string): product is FirefoxProduct;
70
91
  /**
71
92
  * Valid project license SPDX identifiers.
72
93
  */
@@ -4,6 +4,7 @@
4
4
  * Used to safely narrow types from unknown values.
5
5
  */
6
6
  import { InvalidArgumentError } from '../errors/base.js';
7
+ import { escapeRegex } from './regex.js';
7
8
  /**
8
9
  * Checks whether a value is a string.
9
10
  * @param value - Value to check
@@ -94,12 +95,37 @@ export function isValidFirefoxVersion(version) {
94
95
  export function isValidFirefoxCandidate(candidate) {
95
96
  return /^build[1-9]\d{0,3}$/.test(candidate);
96
97
  }
98
+ /**
99
+ * Valid Firefox product identifiers.
100
+ *
101
+ * `satisfies readonly FirefoxProduct[]` links the runtime list to the union at
102
+ * `types/config.ts:5` in ONE direction: it rejects an entry here that the union
103
+ * does not declare. It cannot see a union member missing from this list —
104
+ * widening a `readonly T[]` check never fails for being short. The reverse
105
+ * direction is covered by the exhaustive switch in
106
+ * `utils/__tests__/validation.test.ts`, which fails to compile when the union
107
+ * grows.
108
+ *
109
+ * Two further copies of this list existed before 0.41.0 — an inline literal
110
+ * inside the validator below and `SOURCE_PRODUCTS` in `commands/source.ts` —
111
+ * neither linked to the union.
112
+ */
113
+ export const FIREFOX_PRODUCTS = [
114
+ 'firefox',
115
+ 'firefox-esr',
116
+ 'firefox-beta',
117
+ 'firefox-devedition',
118
+ ];
97
119
  /**
98
120
  * Validates a Firefox product string.
99
- * Accepts: firefox, firefox-esr, firefox-beta, firefox-devedition
121
+ *
122
+ * A type predicate, matching its siblings {@link isValidPatchCategory} and
123
+ * {@link isValidProjectLicense}. It returned plain `boolean` until 0.41.0,
124
+ * which forced `as FirefoxProduct` casts at both call sites even though the
125
+ * `.includes` check IS the runtime proof.
100
126
  */
101
127
  export function isValidFirefoxProduct(product) {
102
- return ['firefox', 'firefox-esr', 'firefox-beta', 'firefox-devedition'].includes(product);
128
+ return FIREFOX_PRODUCTS.includes(product);
103
129
  }
104
130
  /**
105
131
  * Valid project license SPDX identifiers.
@@ -262,7 +288,7 @@ export function normalizeTokenName(name) {
262
288
  */
263
289
  export function normalizePatchDisplayName(name, category) {
264
290
  const stem = name.trim().replace(/\.patch$/i, '');
265
- const escaped = category.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
291
+ const escaped = escapeRegex(category);
266
292
  const prefixes = [
267
293
  new RegExp(`^\\d+[-_ ]+${escaped}[-_ ]+`, 'i'),
268
294
  new RegExp(`^${escaped}[-_ ]+`, 'i'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hominis/fireforge",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "description": "FireForge — a build tool for customizing Firefox",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -18,11 +18,11 @@
18
18
  },
19
19
  "scripts": {
20
20
  "fireforge": "tsx bin/fireforge.ts",
21
- "build": "npm run clean && tsc -p tsconfig.build.json",
21
+ "build": "npm run clean && tsc -p tsconfig.build.json && node ./scripts/generate-build-info.mjs",
22
22
  "clean": "node ./scripts/clean.mjs",
23
- "lint": "eslint src/ bin/ scripts/ eslint.config.js vitest.config.ts vitest.pack.config.ts",
24
- "lint:ci": "eslint src/ bin/ scripts/ eslint.config.js vitest.config.ts vitest.pack.config.ts --max-warnings 0",
25
- "lint:fix": "eslint src/ bin/ scripts/ eslint.config.js vitest.config.ts vitest.pack.config.ts --fix",
23
+ "lint": "eslint src/ bin/ scripts/ eslint-rules/ eslint.config.js vitest.config.ts vitest.pack.config.ts",
24
+ "lint:ci": "eslint src/ bin/ scripts/ eslint-rules/ eslint.config.js vitest.config.ts vitest.pack.config.ts --max-warnings 0",
25
+ "lint:fix": "eslint src/ bin/ scripts/ eslint-rules/ eslint.config.js vitest.config.ts vitest.pack.config.ts --fix",
26
26
  "typecheck": "tsc --noEmit -p tsconfig.json",
27
27
  "deadcode:check": "knip",
28
28
  "cycles:check": "dpdm --no-output --no-tree --no-warning --exit-code circular:1 src/index.ts bin/fireforge.ts",
@@ -34,10 +34,10 @@
34
34
  "whitespace:check": "node ./scripts/check-worktree-whitespace.mjs",
35
35
  "pack:verify": "vitest run --config vitest.pack.config.ts",
36
36
  "pack:dry-run": "npm pack --dry-run --json --silent",
37
- "format": "prettier --write --ignore-unknown \"src/**/*.ts\" \"bin/**/*.ts\" \"scripts/**/*.mjs\" \"package.json\" \"eslint.config.js\" \"vitest.config.ts\" \"vitest.pack.config.ts\" \"tsconfig.json\" \"tsconfig.build.json\" \".lintstagedrc.json\" \".prettierignore\" \".prettierrc\" \".gitignore\" \"README.md\" \"CHANGELOG.md\" \"docs/**/*.md\" \"knip.json\" \"docs/**/*.md\" \"knip.json\"",
38
- "format:check": "prettier --check --ignore-unknown \"src/**/*.ts\" \"bin/**/*.ts\" \"scripts/**/*.mjs\" \"package.json\" \"eslint.config.js\" \"vitest.config.ts\" \"vitest.pack.config.ts\" \"tsconfig.json\" \"tsconfig.build.json\" \".lintstagedrc.json\" \".prettierignore\" \".prettierrc\" \".gitignore\" \"README.md\" \"CHANGELOG.md\" \"docs/**/*.md\" \"knip.json\" \"docs/**/*.md\" \"knip.json\"",
37
+ "format": "prettier --write --ignore-unknown \"src/**/*.ts\" \"bin/**/*.ts\" \"scripts/**/*.mjs\" \"eslint-rules/**/*.js\" \"package.json\" \"eslint.config.js\" \"vitest.config.ts\" \"vitest.pack.config.ts\" \"tsconfig.json\" \"tsconfig.build.json\" \".lintstagedrc.json\" \".prettierignore\" \".prettierrc\" \".gitignore\" \"README.md\" \"CHANGELOG.md\" \"docs/**/*.md\" \"knip.json\"",
38
+ "format:check": "prettier --check --ignore-unknown \"src/**/*.ts\" \"bin/**/*.ts\" \"scripts/**/*.mjs\" \"eslint-rules/**/*.js\" \"package.json\" \"eslint.config.js\" \"vitest.config.ts\" \"vitest.pack.config.ts\" \"tsconfig.json\" \"tsconfig.build.json\" \".lintstagedrc.json\" \".prettierignore\" \".prettierrc\" \".gitignore\" \"README.md\" \"CHANGELOG.md\" \"docs/**/*.md\" \"knip.json\"",
39
39
  "prepare": "node -e \"import('./scripts/prepare.mjs').catch((error) => { if (error && typeof error === 'object' && 'code' in error && error.code === 'ERR_MODULE_NOT_FOUND') process.exit(0); throw error; })\"",
40
- "prepack": "npm run build",
40
+ "prepack": "npm run build && node ./scripts/generate-build-info.mjs --loud",
41
41
  "release:check": "npm run format:check && npm run whitespace:check && npm run lint:ci && npm run typecheck && npm run deadcode:check && npm run cycles:check && npm run test:coverage && npm run pack:verify && npm run pack:dry-run",
42
42
  "prepublishOnly": "npm run release:check"
43
43
  },
@@ -73,7 +73,7 @@
73
73
  "eslint-plugin-simple-import-sort": "^14.0.0",
74
74
  "fast-check": "^4.6.0",
75
75
  "husky": "^9.1.7",
76
- "knip": "6.29.0",
76
+ "knip": "6.32.0",
77
77
  "lint-staged": "^17.0.4",
78
78
  "prettier": "3.9.6",
79
79
  "tsx": "^4.7.0",