@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
@@ -16,12 +16,12 @@ const PATCH_DIRECTORY_LOCK = '.fireforge-patches.lock';
16
16
  * session lock's progress line so operators read one shape for both locks.
17
17
  */
18
18
  function formatWaitProgressLine(waitedMs, timeoutMs, holder) {
19
- const progress = `${String(Math.round(waitedMs / 1000))}s of up to ${String(Math.round(timeoutMs / 1000))}s`;
19
+ const progress = `${Math.round(waitedMs / 1000)}s of up to ${Math.round(timeoutMs / 1000)}s`;
20
20
  if (holder === undefined) {
21
21
  return `Waiting for the FireForge patch directory lock — ${progress}.`;
22
22
  }
23
23
  const details = holder.metadata.length > 0 ? ` (${holder.metadata.join(', ')})` : '';
24
- return `Waiting for the FireForge patch directory lock held by PID ${String(holder.pid)}${details} — ${progress}.`;
24
+ return `Waiting for the FireForge patch directory lock held by PID ${holder.pid}${details} — ${progress}.`;
25
25
  }
26
26
  /**
27
27
  * Asserts that this process is inside {@link withPatchDirectoryLock} for the
@@ -47,7 +47,7 @@ export async function assertPatchDirectoryLockHeld(patchesDir, context) {
47
47
  const status = await readLockStatus(join(patchesDir, PATCH_DIRECTORY_LOCK));
48
48
  assert(status.held, () => `patch directory lock is held before ${context}`);
49
49
  assert(status.holder === undefined || status.holder.pid === process.pid, () => `patch directory lock is owned by this process before ${context} ` +
50
- `(held by PID ${String(status.holder?.pid)}, we are ${String(process.pid)})`);
50
+ `(held by PID ${status.holder?.pid}, we are ${process.pid})`);
51
51
  }
52
52
  /**
53
53
  * Runs a patch directory mutation while holding an exclusive filesystem lock.
@@ -98,17 +98,18 @@ export async function mutatePatchRowsInManifest(patchesDir, filenames, mutator)
98
98
  if (!(await pathExistsStrict(manifestPath)))
99
99
  return null;
100
100
  const rawManifest = await readJson(manifestPath);
101
+ // `validatePatchesManifest` has already rejected a manifest that is not an
102
+ // object with an array of object rows (it throws the corruption error the
103
+ // caller expects), so the shape re-checks below are FireForge's own
104
+ // invariants, not input validation: failing them means the validator and
105
+ // this walker disagree about the manifest schema, which is a bug here.
101
106
  const beforeManifest = validatePatchesManifest(rawManifest);
102
- if (!isObject(rawManifest) || !isArray(rawManifest['patches'])) {
103
- throw new Error('patches.json must be a JSON object with a patches array');
104
- }
107
+ assert(isObject(rawManifest) && isArray(rawManifest['patches']), 'validated patches.json is a JSON object with a patches array');
105
108
  const filenameSet = new Set(filenames);
106
109
  if (filenameSet.size === 0)
107
110
  return [];
108
- const rawPatches = rawManifest['patches'].map((entry) => {
109
- if (!isObject(entry)) {
110
- throw new Error('patches.json patches entries must be objects');
111
- }
111
+ const rawPatches = rawManifest['patches'].map((entry, index) => {
112
+ assert(isObject(entry), () => `validated patches.json row ${index} is an object`);
112
113
  return { ...entry };
113
114
  });
114
115
  const changedIndexes = [];
@@ -1,14 +1,25 @@
1
1
  /**
2
- * Pure parsing functions for extracting information from patch files. All
3
- * functions are synchronous and operate on string content.
2
+ * Renders a patch order as the zero-padded prefix used in filenames and in
3
+ * every message that names an order.
4
4
  *
5
- * This module is the ONE unified-diff walker in the codebase. Hand-rolled
6
- * `diff --git` walkers diverge in ways that matter: CRLF handling (a
7
- * CRLF-saved patch file silently fails target-file matching and
8
- * `\ No newline` detection on Windows), quoted-path support, and greedy path
9
- * captures that mis-split any path containing ` b/`. New diff-shaped parsing
10
- * must build on {@link parseDiffSections} rather than re-walking lines.
5
+ * Eleven call sites across `patch-policy`, `patch-export`, `export-flow`,
6
+ * `export-placement-policy` and `patch move-files` spelled this as
7
+ * `String(order).padStart(3, '0')`, so the padding width lived in eleven
8
+ * places while the filenames it has to agree with are produced in one.
9
+ * @param order - Numeric patch order.
10
+ * @param width - Prefix width; defaults to the on-disk convention.
11
+ * @returns The zero-padded order, e.g. `007`.
11
12
  */
13
+ export declare function formatPatchOrder(order: number, width?: number): string;
14
+ /**
15
+ * Renders an inclusive order range the way `patchPolicy` diagnostics name it.
16
+ * @param range - Inclusive `from`/`to` order bounds.
17
+ * @returns The range label, e.g. `100-199`.
18
+ */
19
+ export declare function formatOrderRange(range: {
20
+ from: number;
21
+ to: number;
22
+ }): string;
12
23
  /**
13
24
  * Extracts the order number from a patch filename.
14
25
  * Expects format like "001-description.patch"
@@ -11,6 +11,31 @@ import { assert } from '../utils/assert.js';
11
11
  * captures that mis-split any path containing ` b/`. New diff-shaped parsing
12
12
  * must build on {@link parseDiffSections} rather than re-walking lines.
13
13
  */
14
+ /** Width of the zero-padded numeric prefix on a patch filename ("001-…"). */
15
+ const PATCH_ORDER_WIDTH = 3;
16
+ /**
17
+ * Renders a patch order as the zero-padded prefix used in filenames and in
18
+ * every message that names an order.
19
+ *
20
+ * Eleven call sites across `patch-policy`, `patch-export`, `export-flow`,
21
+ * `export-placement-policy` and `patch move-files` spelled this as
22
+ * `String(order).padStart(3, '0')`, so the padding width lived in eleven
23
+ * places while the filenames it has to agree with are produced in one.
24
+ * @param order - Numeric patch order.
25
+ * @param width - Prefix width; defaults to the on-disk convention.
26
+ * @returns The zero-padded order, e.g. `007`.
27
+ */
28
+ export function formatPatchOrder(order, width = PATCH_ORDER_WIDTH) {
29
+ return String(order).padStart(width, '0');
30
+ }
31
+ /**
32
+ * Renders an inclusive order range the way `patchPolicy` diagnostics name it.
33
+ * @param range - Inclusive `from`/`to` order bounds.
34
+ * @returns The range label, e.g. `100-199`.
35
+ */
36
+ export function formatOrderRange(range) {
37
+ return `${formatPatchOrder(range.from)}-${formatPatchOrder(range.to)}`;
38
+ }
14
39
  /**
15
40
  * Extracts the order number from a patch filename.
16
41
  * Expects format like "001-description.patch"
@@ -11,6 +11,7 @@ import { InvalidArgumentError } from '../errors/base.js';
11
11
  import { warn } from '../utils/logger.js';
12
12
  import { PATCH_CATEGORIES } from '../utils/validation.js';
13
13
  import { rewriteStagedDependencyOwners } from './patch-manifest-io.js';
14
+ import { formatOrderRange, formatPatchOrder } from './patch-parse.js';
14
15
  /** Default patch filename contract used when a policy omits `filenamePattern`. */
15
16
  const DEFAULT_PATCH_POLICY_FILENAME_PATTERN = '^(?<order>\\d{3})-(?<category>[a-z][a-z0-9-]*)-(?<slug>[a-z0-9-]+)\\.patch$';
16
17
  function policy(config) {
@@ -19,13 +20,6 @@ function policy(config) {
19
20
  function mutationMode(config) {
20
21
  return policy(config)?.mutationMode ?? 'error';
21
22
  }
22
- function issueSeverity(config) {
23
- return mutationMode(config) === 'warn' ? 'warning' : 'error';
24
- }
25
- /** Returns true when the loaded config includes an opt-in patch policy. */
26
- function hasPatchPolicy(config) {
27
- return policy(config) !== undefined;
28
- }
29
23
  /** Returns valid categories for prompts and CLI validation under the config. */
30
24
  export function getPatchPolicyCategories(config) {
31
25
  const cfg = policy(config);
@@ -42,9 +36,6 @@ export function isCategoryAllowedByConfig(config, category) {
42
36
  return PATCH_CATEGORIES.includes(category);
43
37
  return cfg.ranges.some((range) => range.category === category);
44
38
  }
45
- function rangeLabel(range) {
46
- return `${String(range.from).padStart(3, '0')}-${String(range.to).padStart(3, '0')}`;
47
- }
48
39
  /**
49
40
  * Human-readable label for a category's configured ranges, e.g. `300-399`.
50
41
  * Exported for the forward-import hint, which must render the
@@ -54,7 +45,7 @@ export function categoryRangeLabel(ranges, category) {
54
45
  const matches = ranges.filter((range) => range.category === category);
55
46
  if (matches.length === 0)
56
47
  return '(no configured range)';
57
- return matches.map(rangeLabel).join(', ');
48
+ return matches.map(formatOrderRange).join(', ');
58
49
  }
59
50
  function reservedRangeForOrder(cfg, order) {
60
51
  return cfg.reservedRanges?.find((range) => order >= range.from && order <= range.to) ?? null;
@@ -73,9 +64,6 @@ export function categoryRangeForOrder(cfg, category, order) {
73
64
  function anyRangeForOrder(cfg, order) {
74
65
  return cfg.ranges.find((range) => order >= range.from && order <= range.to) ?? null;
75
66
  }
76
- function compileFilenamePattern(cfg) {
77
- return new RegExp(cfg.filenamePattern ?? DEFAULT_PATCH_POLICY_FILENAME_PATTERN);
78
- }
79
67
  function parseFilenameWithPolicy(cfg, patch, severity) {
80
68
  if (cfg.filenamePattern === undefined) {
81
69
  const defaultMatch = /^(?<order>\d{3})-(?<rest>[a-z0-9-]+)\.patch$/.exec(patch.filename);
@@ -117,7 +105,7 @@ function parseFilenameWithPolicy(cfg, patch, severity) {
117
105
  }
118
106
  return { order: Number.parseInt(orderRaw, 10), category };
119
107
  }
120
- const pattern = compileFilenamePattern(cfg);
108
+ const pattern = new RegExp(cfg.filenamePattern ?? DEFAULT_PATCH_POLICY_FILENAME_PATTERN);
121
109
  const match = pattern.exec(patch.filename);
122
110
  if (!match) {
123
111
  return [
@@ -188,7 +176,7 @@ function evaluatePatchMetadata(cfg, patch, severity) {
188
176
  code: 'reserved-range',
189
177
  filename: patch.filename,
190
178
  severity,
191
- message: `${patch.filename} is in reserved range ${rangeLabel(reserved)}. ` +
179
+ message: `${patch.filename} is in reserved range ${formatOrderRange(reserved)}. ` +
192
180
  'Reserved ranges require an exact patchPolicy.reservedRanges[].allowed filename exception.',
193
181
  });
194
182
  return issues;
@@ -198,7 +186,7 @@ function evaluatePatchMetadata(cfg, patch, severity) {
198
186
  code: 'reserved-documentation',
199
187
  filename: patch.filename,
200
188
  severity,
201
- message: `${patch.filename} is allowlisted for reserved range ${rangeLabel(reserved)}, ` +
189
+ message: `${patch.filename} is allowlisted for reserved range ${formatOrderRange(reserved)}, ` +
202
190
  'but the allowlist entry must include either "adr" or "documentation".',
203
191
  });
204
192
  }
@@ -210,7 +198,7 @@ function evaluatePatchMetadata(cfg, patch, severity) {
210
198
  code: 'reserved-files',
211
199
  filename: patch.filename,
212
200
  severity,
213
- message: `${patch.filename} is allowlisted for reserved range ${rangeLabel(reserved)}, ` +
201
+ message: `${patch.filename} is allowlisted for reserved range ${formatOrderRange(reserved)}, ` +
214
202
  `but touches file(s) outside its reserved allowlist: ${extraFiles.join(', ')}.`,
215
203
  });
216
204
  }
@@ -222,8 +210,8 @@ function evaluatePatchMetadata(cfg, patch, severity) {
222
210
  const owner = anyRangeForOrder(cfg, patch.order);
223
211
  const expected = categoryRangeLabel(cfg.ranges, patch.category);
224
212
  const actual = owner !== null
225
- ? `${String(patch.order).padStart(3, '0')} is configured for ${owner.category} (${rangeLabel(owner)})`
226
- : `${String(patch.order).padStart(3, '0')} is outside all configured ranges`;
213
+ ? `${formatPatchOrder(patch.order)} is configured for ${owner.category} (${formatOrderRange(owner)})`
214
+ : `${formatPatchOrder(patch.order)} is outside all configured ranges`;
227
215
  issues.push({
228
216
  code: 'category-range',
229
217
  filename: patch.filename,
@@ -262,10 +250,10 @@ function evaluateGaps(cfg, patches, severity) {
262
250
  if (missing.length > 0) {
263
251
  issues.push({
264
252
  code: 'numeric-gap',
265
- filename: `${range.category}:${rangeLabel(range)}`,
253
+ filename: `${range.category}:${formatOrderRange(range)}`,
266
254
  severity,
267
- message: `${range.category} range ${rangeLabel(range)} has numeric gap(s): ` +
268
- missing.map((order) => String(order).padStart(3, '0')).join(', ') +
255
+ message: `${range.category} range ${formatOrderRange(range)} has numeric gap(s): ` +
256
+ missing.map((order) => formatPatchOrder(order)).join(', ') +
269
257
  '. patchPolicy.allowGaps is false.',
270
258
  });
271
259
  }
@@ -286,9 +274,9 @@ function evaluateOrderCollisions(patches, severity) {
286
274
  const filenames = matches.map((patch) => patch.filename).sort((a, b) => a.localeCompare(b));
287
275
  issues.push({
288
276
  code: 'order-collision',
289
- filename: String(order).padStart(3, '0'),
277
+ filename: formatPatchOrder(order),
290
278
  severity,
291
- message: `patchPolicy requires unique numeric orders; order ${String(order).padStart(3, '0')} ` +
279
+ message: `patchPolicy requires unique numeric orders; order ${formatPatchOrder(order)} ` +
292
280
  `is used by: ${filenames.join(', ')}.`,
293
281
  });
294
282
  }
@@ -299,7 +287,7 @@ export function evaluatePatchPolicy(config, manifest) {
299
287
  const cfg = policy(config);
300
288
  if (!cfg)
301
289
  return [];
302
- const severity = issueSeverity(config);
290
+ const severity = mutationMode(config) === 'warn' ? 'warning' : 'error';
303
291
  const issues = manifest.patches.flatMap((patch) => evaluatePatchMetadata(cfg, patch, severity));
304
292
  issues.push(...evaluateOrderCollisions(manifest.patches, severity));
305
293
  issues.push(...evaluateGaps(cfg, manifest.patches, severity));
@@ -335,7 +323,7 @@ export function applyRenameMapToManifest(manifest, renameMap) {
335
323
  }
336
324
  /** Enforces patch policy according to the configured mutation mode. */
337
325
  export function enforcePatchPolicy(input) {
338
- if (!hasPatchPolicy(input.config))
326
+ if (policy(input.config) === undefined)
339
327
  return;
340
328
  const issues = evaluatePatchPolicy(input.config, input.manifest);
341
329
  if (issues.length === 0)
@@ -1,8 +1,6 @@
1
1
  import type { PatchMetadata } from '../types/commands/index.js';
2
- import type { FirefoxConfig, FirefoxProduct } from '../types/config.js';
2
+ import type { FirefoxConfig } from '../types/config.js';
3
3
  /** Metadata fields stamped on new or refreshed patch entries. */
4
4
  export declare function buildPatchSourceMetadata(firefox: Pick<FirefoxConfig, 'product' | 'version'>): Pick<PatchMetadata, 'sourceEsrVersion' | 'sourceProduct' | 'sourceVersion'>;
5
5
  /** Backward-compatible source version reader for legacy manifests. */
6
6
  export declare function getPatchSourceVersion(patch: Pick<PatchMetadata, 'sourceEsrVersion' | 'sourceVersion'>): string;
7
- /** Backward-compatible source product reader for legacy manifests. */
8
- export declare function getPatchSourceProduct(patch: Pick<PatchMetadata, 'sourceProduct'>): FirefoxProduct | undefined;
@@ -10,7 +10,3 @@ export function buildPatchSourceMetadata(firefox) {
10
10
  export function getPatchSourceVersion(patch) {
11
11
  return patch.sourceVersion ?? patch.sourceEsrVersion;
12
12
  }
13
- /** Backward-compatible source product reader for legacy manifests. */
14
- export function getPatchSourceProduct(patch) {
15
- return patch.sourceProduct;
16
- }
@@ -48,14 +48,6 @@ export declare function buildNewFileTextProjection(diff: string): Map<string, st
48
48
  * @returns The file content that the patch would create
49
49
  */
50
50
  export declare function extractNewFileContent(patchPath: string, targetFile?: string): Promise<string>;
51
- /**
52
- * Applies a patch's changes to content.
53
- * @param content - Original content (null for new files)
54
- * @param patchPath - Path to the patch file
55
- * @param targetFile - The file path within the patch
56
- * @returns Modified content
57
- */
58
- export declare function applyPatchToContent(content: string | null, patchPath: string, targetFile: string): Promise<string>;
59
51
  /**
60
52
  * Applies an already-read patch body to content. Batched callers that hold
61
53
  * many (patch, file) pairs use this to read each patch body once instead of
@@ -101,16 +101,6 @@ export async function extractNewFileContent(patchPath, targetFile) {
101
101
  const content = await readText(patchPath);
102
102
  return extractNewFileContentFromDiff(content, targetFile);
103
103
  }
104
- /**
105
- * Applies a patch's changes to content.
106
- * @param content - Original content (null for new files)
107
- * @param patchPath - Path to the patch file
108
- * @param targetFile - The file path within the patch
109
- * @returns Modified content
110
- */
111
- export async function applyPatchToContent(content, patchPath, targetFile) {
112
- return applyPatchTextToContent(content, await readText(patchPath), targetFile);
113
- }
114
104
  /**
115
105
  * Applies an already-read patch body to content. Batched callers that hold
116
106
  * many (patch, file) pairs use this to read each patch body once instead of
@@ -34,7 +34,7 @@ export async function readProcessCpuSeconds(pid) {
34
34
  '-NoProfile',
35
35
  '-NonInteractive',
36
36
  '-Command',
37
- `(Get-Process -Id ${String(pid)} -ErrorAction SilentlyContinue).CPU`,
37
+ `(Get-Process -Id ${pid} -ErrorAction SilentlyContinue).CPU`,
38
38
  ]);
39
39
  const seconds = Number.parseFloat(result.stdout.trim());
40
40
  return Number.isFinite(seconds) ? seconds : undefined;
@@ -52,6 +52,8 @@ export interface RebaseSession {
52
52
  /** Index of the next patch to process (resume point). */
53
53
  currentIndex: number;
54
54
  }
55
+ /** Absolute path of the session file, for messages that must name it. */
56
+ export declare function getRebaseSessionPath(projectRoot: string): string;
55
57
  /**
56
58
  * Outcome of reading the session file, following the {@link LockOwner}
57
59
  * discriminated-union form rather than collapsing three states into `null`.
@@ -74,8 +76,6 @@ export type RebaseSessionRead = {
74
76
  valid: false;
75
77
  reason: string;
76
78
  };
77
- /** Absolute path of the session file, for messages that must name it. */
78
- export declare function getRebaseSessionPath(projectRoot: string): string;
79
79
  /**
80
80
  * Reads the session file, reporting absent, valid, and corrupt as three
81
81
  * distinct outcomes. Never throws for a malformed file: `readJson` calls
@@ -90,12 +90,6 @@ export declare function getRebaseSessionPath(projectRoot: string): string;
90
90
  * markers.
91
91
  */
92
92
  export declare function readRebaseSession(projectRoot: string): Promise<RebaseSessionRead>;
93
- /**
94
- * Loads an existing rebase session, or returns `null` when none exists or the
95
- * file on disk is unusable. Callers that must tell those two apart — every
96
- * command that reports to an operator — should use {@link readRebaseSession}.
97
- */
98
- export declare function tryReadRebaseSession(projectRoot: string): Promise<RebaseSession | null>;
99
93
  /**
100
94
  * Persists a rebase session atomically.
101
95
  */
@@ -13,7 +13,8 @@ import { getProjectPaths } from './config-paths.js';
13
13
  import { createSiblingLockPath, withFileLock } from './file-lock.js';
14
14
  // ── Helpers ──
15
15
  const SESSION_FILENAME = 'rebase-session.json';
16
- function sessionPath(projectRoot) {
16
+ /** Absolute path of the session file, for messages that must name it. */
17
+ export function getRebaseSessionPath(projectRoot) {
17
18
  return join(getProjectPaths(projectRoot).fireforgeDir, SESSION_FILENAME);
18
19
  }
19
20
  const REBASE_PATCH_STATUSES = [
@@ -116,10 +117,6 @@ function isValidSession(data) {
116
117
  return false;
117
118
  return currentIndex >= 0 && currentIndex <= patches.length;
118
119
  }
119
- /** Absolute path of the session file, for messages that must name it. */
120
- export function getRebaseSessionPath(projectRoot) {
121
- return sessionPath(projectRoot);
122
- }
123
120
  /**
124
121
  * Reads the session file, reporting absent, valid, and corrupt as three
125
122
  * distinct outcomes. Never throws for a malformed file: `readJson` calls
@@ -134,7 +131,7 @@ export function getRebaseSessionPath(projectRoot) {
134
131
  * markers.
135
132
  */
136
133
  export async function readRebaseSession(projectRoot) {
137
- const path = sessionPath(projectRoot);
134
+ const path = getRebaseSessionPath(projectRoot);
138
135
  let data;
139
136
  try {
140
137
  data = await readJson(path);
@@ -159,20 +156,11 @@ export async function readRebaseSession(projectRoot) {
159
156
  session: { ...data, patches: data.patches.map(normalizeEntry) },
160
157
  };
161
158
  }
162
- /**
163
- * Loads an existing rebase session, or returns `null` when none exists or the
164
- * file on disk is unusable. Callers that must tell those two apart — every
165
- * command that reports to an operator — should use {@link readRebaseSession}.
166
- */
167
- export async function tryReadRebaseSession(projectRoot) {
168
- const result = await readRebaseSession(projectRoot);
169
- return result.present && result.valid ? result.session : null;
170
- }
171
159
  /**
172
160
  * Persists a rebase session atomically.
173
161
  */
174
162
  export async function saveRebaseSession(projectRoot, session) {
175
- const path = sessionPath(projectRoot);
163
+ const path = getRebaseSessionPath(projectRoot);
176
164
  await withFileLock(createSiblingLockPath(path, '.rebase-session.lock'), async () => {
177
165
  await writeJson(path, session);
178
166
  });
@@ -181,7 +169,7 @@ export async function saveRebaseSession(projectRoot, session) {
181
169
  * Removes the rebase session file.
182
170
  */
183
171
  export async function clearRebaseSession(projectRoot) {
184
- const path = sessionPath(projectRoot);
172
+ const path = getRebaseSessionPath(projectRoot);
185
173
  if (await pathExists(path)) {
186
174
  await removeFile(path);
187
175
  }
@@ -5,6 +5,7 @@
5
5
  import { join } from 'node:path';
6
6
  import { GeneralError } from '../errors/base.js';
7
7
  import { pathExists, readText, writeText } from '../utils/fs.js';
8
+ import { normalizePathSlashes } from '../utils/paths.js';
8
9
  import { insertJarMnEntry } from './moz-manifest-helpers.js';
9
10
  /**
10
11
  * Tokenizer-based implementation for browser content registration.
@@ -30,8 +31,8 @@ export async function registerBrowserContent(engineDir, fileName, after, sourceP
30
31
  if (!(await pathExists(manifestPath))) {
31
32
  throw new GeneralError(`Manifest not found: ${manifest}`);
32
33
  }
33
- const source = (sourcePath ?? `content/${fileName}`).replace(/\\/g, '/');
34
- const entry = ` content/browser/${fileName} (${source})`.replace(/\\/g, '/');
34
+ const source = normalizePathSlashes(sourcePath ?? `content/${fileName}`);
35
+ const entry = normalizePathSlashes(` content/browser/${fileName} (${source})`);
35
36
  const content = await readText(manifestPath);
36
37
  // Idempotency check
37
38
  if (content.includes(`content/browser/${fileName}`)) {
@@ -5,6 +5,7 @@
5
5
  import { join } from 'node:path';
6
6
  import { GeneralError } from '../errors/base.js';
7
7
  import { pathExists, readText, writeText } from '../utils/fs.js';
8
+ import { normalizePathSlashes } from '../utils/paths.js';
8
9
  import { insertMozBuildListEntry } from './moz-manifest-helpers.js';
9
10
  /**
10
11
  * Tokenizer-based implementation for test manifest registration.
@@ -28,7 +29,7 @@ export async function registerTestManifest(engineDir, testDir, dryRun = false) {
28
29
  if (!(await pathExists(manifestPath))) {
29
30
  throw new GeneralError(`Manifest not found: ${manifest}`);
30
31
  }
31
- const entry = ` "content/test/${testDir}/browser.toml",`.replace(/\\/g, '/');
32
+ const entry = normalizePathSlashes(` "content/test/${testDir}/browser.toml",`);
32
33
  const content = await readText(manifestPath);
33
34
  // Idempotency check
34
35
  if (content.includes(`content/test/${testDir}/browser.toml`)) {
@@ -5,6 +5,7 @@
5
5
  import { join } from 'node:path';
6
6
  import { GeneralError } from '../errors/base.js';
7
7
  import { pathExists, readText, writeText } from '../utils/fs.js';
8
+ import { normalizePathSlashes } from '../utils/paths.js';
8
9
  import { insertMozBuildListEntry } from './moz-manifest-helpers.js';
9
10
  import { scaffoldModuleMozBuild } from './register-scaffold.js';
10
11
  /**
@@ -41,7 +42,7 @@ export async function registerFireForgeModule(engineDir, fileName, moduleDir, dr
41
42
  }
42
43
  throw new GeneralError(`Manifest not found: ${manifest}. Pass --create-manifest to scaffold it and wire the parent DIRS entry.`);
43
44
  }
44
- const entry = ` "${fileName}",`.replace(/\\/g, '/');
45
+ const entry = normalizePathSlashes(` "${fileName}",`);
45
46
  const content = await readText(manifestPath);
46
47
  // Idempotency check
47
48
  if (content.includes(`"${fileName}"`)) {
@@ -19,6 +19,7 @@
19
19
  import { dirname, join } from 'node:path';
20
20
  import { GeneralError } from '../errors/base.js';
21
21
  import { pathExists, readText, writeText } from '../utils/fs.js';
22
+ import { normalizePathSlashes } from '../utils/paths.js';
22
23
  import { getLicenseHeader } from './license-headers.js';
23
24
  import { findAlphabeticalMozBuildPosition } from './moz-manifest-helpers.js';
24
25
  import { tokenizeMozBuildList } from './moz-manifest-tokenizers.js';
@@ -64,7 +65,7 @@ export function upsertMozBuildListEntry(content, directive, value) {
64
65
  export async function ensureParentDirsWiring(engineDir, childRelDir, dryRun) {
65
66
  const actions = [];
66
67
  let childName = childRelDir.split('/').at(-1) ?? childRelDir;
67
- let parentRel = dirname(childRelDir).replace(/\\/g, '/');
68
+ let parentRel = normalizePathSlashes(dirname(childRelDir));
68
69
  for (;;) {
69
70
  if (parentRel === '.' || parentRel === '' || parentRel === '/') {
70
71
  throw new GeneralError(`No moz.build found in any parent of ${childRelDir} up to the engine root; ` +
@@ -90,7 +91,7 @@ export async function ensureParentDirsWiring(engineDir, childRelDir, dryRun) {
90
91
  await writeText(parentManifestPath, scaffold);
91
92
  actions.push({ manifest: parentManifestRel, change: `created with DIRS += ["${childName}"]` });
92
93
  childName = parentRel.split('/').at(-1) ?? parentRel;
93
- parentRel = dirname(parentRel).replace(/\\/g, '/');
94
+ parentRel = normalizePathSlashes(dirname(parentRel));
94
95
  }
95
96
  }
96
97
  /**
@@ -122,7 +123,7 @@ export async function scaffoldModuleMozBuild(engineDir, moduleDir, fileName, dry
122
123
  * entry already exists).
123
124
  */
124
125
  export async function ensureXpcshellManifestWiring(engineDir, manifestRelPath, dryRun) {
125
- let dirRel = dirname(manifestRelPath).replace(/\\/g, '/');
126
+ let dirRel = normalizePathSlashes(dirname(manifestRelPath));
126
127
  for (;;) {
127
128
  if (dirRel === '.' || dirRel === '' || dirRel === '/') {
128
129
  throw new GeneralError(`No moz.build found in any parent of ${manifestRelPath} up to the engine root; ` +
@@ -140,6 +141,6 @@ export async function ensureXpcshellManifestWiring(engineDir, manifestRelPath, d
140
141
  await writeText(mozBuildPath, updated);
141
142
  return [{ manifest: mozBuildRel, change: `XPCSHELL_TESTS_MANIFESTS += ["${relEntry}"]` }];
142
143
  }
143
- dirRel = dirname(dirRel).replace(/\\/g, '/');
144
+ dirRel = normalizePathSlashes(dirname(dirRel));
144
145
  }
145
146
  }
@@ -5,6 +5,7 @@
5
5
  import { basename, join } from 'node:path';
6
6
  import { GeneralError } from '../errors/base.js';
7
7
  import { pathExists, readText, writeText } from '../utils/fs.js';
8
+ import { normalizePathSlashes } from '../utils/paths.js';
8
9
  import { insertJarMnEntry } from './moz-manifest-helpers.js';
9
10
  /**
10
11
  * Tokenizer-based implementation for shared CSS registration.
@@ -65,7 +66,7 @@ export function buildEntry(name, sourceColumn) {
65
66
  const minColumn = target.length + MIN_SOURCE_GAP;
66
67
  const column = sourceColumn !== undefined && sourceColumn >= minColumn ? sourceColumn : minColumn;
67
68
  const padding = ' '.repeat(column - target.length);
68
- return `${target}${padding}(../shared/${name}.css)`.replace(/\\/g, '/');
69
+ return normalizePathSlashes(`${target}${padding}(../shared/${name}.css)`);
69
70
  }
70
71
  /**
71
72
  * Registers a CSS file in browser/themes/shared/jar.inc.mn.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Conservative secret redaction for the run-log FILE tee.
3
+ *
4
+ * A run log is a complete copy of mach's output, and mach echoes its
5
+ * environment in places (configure summaries, `mach env`, harness
6
+ * diagnostics that print the command line). The terminal already showed
7
+ * every byte to the operator who ran it; the file, by contrast, is retained
8
+ * twenty deep and gets attached to bug reports. So the file gets a narrow
9
+ * masking pass and the terminal does not.
10
+ *
11
+ * Deliberately minimal and pattern-based — see {@link redactRunLogText} for
12
+ * the exact contract. It is a seatbelt against the common accidental leak,
13
+ * not a guarantee that a log is free of secrets.
14
+ */
15
+ /**
16
+ * Masks the obvious secret shapes in run-log text, line by line.
17
+ *
18
+ * Redacted:
19
+ * - the value of an env-style assignment `KEY=value` whose KEY matches
20
+ * `/(TOKEN|SECRET|PASSWORD|PASSWD|API_KEY|AUTH)/i` — the value up to the
21
+ * next whitespace, or a whole quoted string — becomes `KEY=<redacted>`;
22
+ * the `--flag=value` and `-DNAME=value` spellings of the same shape are
23
+ * covered (`--password=x` → `--password=<redacted>`);
24
+ * - the credential of an `Authorization:` header (`Bearer`, `Basic` and
25
+ * `Token` schemes keep the scheme word; anything else masks the whole
26
+ * value).
27
+ *
28
+ * NOT redacted: secrets that appear without one of those key names or
29
+ * headers (a bare token in a URL query string, a space-separated
30
+ * `--password foo` CLI argument, JSON `"token": "..."` fields, cookies).
31
+ * Line structure and every other byte pass through unchanged; `\n`, `\r\n`
32
+ * and lone `\r` all delimit lines, so carriage-return-repainted progress
33
+ * output is masked per repaint.
34
+ *
35
+ * @param text - One or more complete lines
36
+ * @returns The same text with masked values
37
+ */
38
+ export declare function redactRunLogText(text: string): string;
@@ -0,0 +1,69 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Conservative secret redaction for the run-log FILE tee.
4
+ *
5
+ * A run log is a complete copy of mach's output, and mach echoes its
6
+ * environment in places (configure summaries, `mach env`, harness
7
+ * diagnostics that print the command line). The terminal already showed
8
+ * every byte to the operator who ran it; the file, by contrast, is retained
9
+ * twenty deep and gets attached to bug reports. So the file gets a narrow
10
+ * masking pass and the terminal does not.
11
+ *
12
+ * Deliberately minimal and pattern-based — see {@link redactRunLogText} for
13
+ * the exact contract. It is a seatbelt against the common accidental leak,
14
+ * not a guarantee that a log is free of secrets.
15
+ */
16
+ /** Env-style keys whose assigned value is masked. */
17
+ const SECRET_KEY_PATTERN = /(TOKEN|SECRET|PASSWORD|PASSWD|API_KEY|AUTH)/i;
18
+ /**
19
+ * `KEY=value` where KEY is an identifier and the value runs to the next
20
+ * whitespace, or is a single/double-quoted string. The leading group keeps
21
+ * the assignment anchored at a token boundary so `x.y=z` is not split
22
+ * mid-token. A hyphen IS a boundary: mach and configure echo their argv, so
23
+ * `--token=…`, `--password=…` and `-DAUTH_TOKEN=…` are exactly the forms a
24
+ * retained log would otherwise carry verbatim (the lead keeps the dash; the
25
+ * key is what follows it). Empty values are left alone (nothing to hide).
26
+ */
27
+ const ENV_ASSIGNMENT_PATTERN = /(^|[\s"'([{,;-])([A-Za-z_][A-Za-z0-9_]*)=("(?:[^"\\]|\\.)*"|'[^']*'|[^\s"',;)\]}]+)/g;
28
+ /** `Authorization: <scheme> <credential>` (scheme optional) in HTTP dumps. */
29
+ const AUTHORIZATION_HEADER_PATTERN = /(authorization\s*:\s*)(?:(bearer|basic|token)\s+)?(\S+)/gi;
30
+ /** Marker written in place of a masked value. */
31
+ const REDACTED = '<redacted>';
32
+ function redactLine(line) {
33
+ return line
34
+ .replace(ENV_ASSIGNMENT_PATTERN, (whole, lead, key) => SECRET_KEY_PATTERN.test(key) ? `${lead}${key}=${REDACTED}` : whole)
35
+ .replace(AUTHORIZATION_HEADER_PATTERN, (_whole, lead, scheme) => scheme === undefined ? `${lead}${REDACTED}` : `${lead}${scheme} ${REDACTED}`);
36
+ }
37
+ /**
38
+ * Masks the obvious secret shapes in run-log text, line by line.
39
+ *
40
+ * Redacted:
41
+ * - the value of an env-style assignment `KEY=value` whose KEY matches
42
+ * `/(TOKEN|SECRET|PASSWORD|PASSWD|API_KEY|AUTH)/i` — the value up to the
43
+ * next whitespace, or a whole quoted string — becomes `KEY=<redacted>`;
44
+ * the `--flag=value` and `-DNAME=value` spellings of the same shape are
45
+ * covered (`--password=x` → `--password=<redacted>`);
46
+ * - the credential of an `Authorization:` header (`Bearer`, `Basic` and
47
+ * `Token` schemes keep the scheme word; anything else masks the whole
48
+ * value).
49
+ *
50
+ * NOT redacted: secrets that appear without one of those key names or
51
+ * headers (a bare token in a URL query string, a space-separated
52
+ * `--password foo` CLI argument, JSON `"token": "..."` fields, cookies).
53
+ * Line structure and every other byte pass through unchanged; `\n`, `\r\n`
54
+ * and lone `\r` all delimit lines, so carriage-return-repainted progress
55
+ * output is masked per repaint.
56
+ *
57
+ * @param text - One or more complete lines
58
+ * @returns The same text with masked values
59
+ */
60
+ export function redactRunLogText(text) {
61
+ if (text.length === 0)
62
+ return text;
63
+ // Split with a capturing group so the terminators survive the round trip;
64
+ // odd indices are the terminators themselves.
65
+ return text
66
+ .split(/(\r\n|\n|\r)/)
67
+ .map((part, index) => (index % 2 === 0 ? redactLine(part) : part))
68
+ .join('');
69
+ }