@hominis/fireforge 0.44.4 → 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 (378) hide show
  1. package/CHANGELOG.md +120 -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-source-pin.d.ts +29 -0
  18. package/dist/src/commands/doctor-source-pin.js +81 -0
  19. package/dist/src/commands/doctor.d.ts +2 -0
  20. package/dist/src/commands/doctor.js +9 -8
  21. package/dist/src/commands/download.js +35 -24
  22. package/dist/src/commands/export-all.js +21 -4
  23. package/dist/src/commands/export-flow.js +15 -60
  24. package/dist/src/commands/export-placement-conflicts.js +3 -3
  25. package/dist/src/commands/export-placement-gate.js +2 -8
  26. package/dist/src/commands/export-placement-policy.js +3 -5
  27. package/dist/src/commands/export-shared.d.ts +42 -23
  28. package/dist/src/commands/export-shared.js +4 -19
  29. package/dist/src/commands/export.js +10 -1
  30. package/dist/src/commands/furnace/authoring-config.d.ts +13 -0
  31. package/dist/src/commands/furnace/authoring-config.js +19 -0
  32. package/dist/src/commands/furnace/chrome-doc-remove.js +1 -1
  33. package/dist/src/commands/furnace/create-browser-test.js +3 -8
  34. package/dist/src/commands/furnace/create-dry-run.js +2 -5
  35. package/dist/src/commands/furnace/create-templates.d.ts +17 -1
  36. package/dist/src/commands/furnace/create-templates.js +2 -1
  37. package/dist/src/commands/furnace/create.js +25 -19
  38. package/dist/src/commands/furnace/deploy.js +11 -2
  39. package/dist/src/commands/furnace/init.js +2 -1
  40. package/dist/src/commands/furnace/override.js +28 -19
  41. package/dist/src/commands/furnace/refresh.js +9 -1
  42. package/dist/src/commands/furnace/remove-mutations.d.ts +6 -7
  43. package/dist/src/commands/furnace/remove-mutations.js +0 -1
  44. package/dist/src/commands/furnace/remove.js +2 -6
  45. package/dist/src/commands/furnace/rename-browser-test.js +1 -7
  46. package/dist/src/commands/furnace/rename.js +29 -29
  47. package/dist/src/commands/furnace/status.js +12 -9
  48. package/dist/src/commands/furnace/test-file-name.d.ts +28 -0
  49. package/dist/src/commands/furnace/test-file-name.js +39 -0
  50. package/dist/src/commands/furnace/validation-output.d.ts +21 -9
  51. package/dist/src/commands/furnace/validation-output.js +3 -8
  52. package/dist/src/commands/import.js +2 -9
  53. package/dist/src/commands/lint-per-patch.d.ts +2 -2
  54. package/dist/src/commands/lint-per-patch.js +19 -4
  55. package/dist/src/commands/lint-per-run-checkjs.d.ts +2 -2
  56. package/dist/src/commands/lint.d.ts +2 -1
  57. package/dist/src/commands/lint.js +1 -1
  58. package/dist/src/commands/manifest.js +1 -1
  59. package/dist/src/commands/package.js +2 -4
  60. package/dist/src/commands/patch/compact.js +14 -15
  61. package/dist/src/commands/patch/delete.js +15 -25
  62. package/dist/src/commands/patch/entry-projection.d.ts +13 -0
  63. package/dist/src/commands/patch/entry-projection.js +24 -0
  64. package/dist/src/commands/patch/lint-ignore.d.ts +0 -8
  65. package/dist/src/commands/patch/lint-ignore.js +19 -29
  66. package/dist/src/commands/patch/move-files-create.js +24 -10
  67. package/dist/src/commands/patch/move-files-into.js +38 -37
  68. package/dist/src/commands/patch/move-files.js +4 -13
  69. package/dist/src/commands/patch/rename.js +27 -41
  70. package/dist/src/commands/patch/reorder.js +25 -38
  71. package/dist/src/commands/patch/split-plan.d.ts +4 -5
  72. package/dist/src/commands/patch/split-plan.js +4 -11
  73. package/dist/src/commands/patch/split.js +45 -43
  74. package/dist/src/commands/patch/staged-dependency.js +16 -22
  75. package/dist/src/commands/patch/tier.js +15 -25
  76. package/dist/src/commands/patch-rename-map.d.ts +19 -0
  77. package/dist/src/commands/patch-rename-map.js +36 -0
  78. package/dist/src/commands/pending-resolution.d.ts +15 -0
  79. package/dist/src/commands/pending-resolution.js +25 -0
  80. package/dist/src/commands/re-export-adjacent.d.ts +32 -2
  81. package/dist/src/commands/re-export-adjacent.js +51 -10
  82. package/dist/src/commands/re-export-bulk-scan.js +6 -10
  83. package/dist/src/commands/re-export-drift.d.ts +8 -0
  84. package/dist/src/commands/re-export-drift.js +65 -4
  85. package/dist/src/commands/re-export-files.d.ts +2 -3
  86. package/dist/src/commands/re-export-files.js +38 -32
  87. package/dist/src/commands/re-export-lint.d.ts +3 -4
  88. package/dist/src/commands/re-export-lint.js +9 -2
  89. package/dist/src/commands/re-export-options.js +19 -5
  90. package/dist/src/commands/re-export-register.js +5 -1
  91. package/dist/src/commands/re-export-scan.js +8 -8
  92. package/dist/src/commands/re-export-single.d.ts +22 -3
  93. package/dist/src/commands/re-export-single.js +19 -11
  94. package/dist/src/commands/re-export.js +44 -27
  95. package/dist/src/commands/rebase/abort.js +3 -8
  96. package/dist/src/commands/rebase/continue.js +12 -12
  97. package/dist/src/commands/rebase/index.js +2 -2
  98. package/dist/src/commands/rebase/patch-loop.d.ts +2 -2
  99. package/dist/src/commands/rebase/patch-loop.js +2 -7
  100. package/dist/src/commands/resolve.js +18 -10
  101. package/dist/src/commands/run.d.ts +0 -13
  102. package/dist/src/commands/run.js +12 -25
  103. package/dist/src/commands/setup-support.d.ts +16 -4
  104. package/dist/src/commands/setup-support.js +16 -16
  105. package/dist/src/commands/setup.js +3 -9
  106. package/dist/src/commands/source.js +8 -1
  107. package/dist/src/commands/status-check.js +2 -2
  108. package/dist/src/commands/status-output.js +2 -2
  109. package/dist/src/commands/status.js +3 -4
  110. package/dist/src/commands/test-build-phase.d.ts +2 -2
  111. package/dist/src/commands/test-build-phase.js +33 -8
  112. package/dist/src/commands/test-diagnose.js +14 -5
  113. package/dist/src/commands/test-modes.d.ts +2 -2
  114. package/dist/src/commands/test-modes.js +0 -1
  115. package/dist/src/commands/test-register.js +6 -3
  116. package/dist/src/commands/test-run.js +29 -21
  117. package/dist/src/commands/test-stale-gate.js +8 -5
  118. package/dist/src/commands/test-verdict.d.ts +16 -2
  119. package/dist/src/commands/test-verdict.js +18 -3
  120. package/dist/src/commands/test.js +51 -7
  121. package/dist/src/commands/token-list.d.ts +26 -0
  122. package/dist/src/commands/token-list.js +153 -0
  123. package/dist/src/commands/token.js +17 -1
  124. package/dist/src/commands/tree.js +7 -2
  125. package/dist/src/commands/typecheck.js +9 -9
  126. package/dist/src/commands/wire.js +4 -3
  127. package/dist/src/core/branding.d.ts +0 -7
  128. package/dist/src/core/branding.js +3 -2
  129. package/dist/src/core/browser-wire.js +6 -3
  130. package/dist/src/core/build-audit-platform.d.ts +29 -0
  131. package/dist/src/core/build-audit-platform.js +140 -0
  132. package/dist/src/core/build-audit-registration.js +3 -2
  133. package/dist/src/core/build-audit-skip.d.ts +57 -0
  134. package/dist/src/core/build-audit-skip.js +112 -0
  135. package/dist/src/core/build-audit.d.ts +15 -2
  136. package/dist/src/core/build-audit.js +154 -12
  137. package/dist/src/core/build-baseline.d.ts +45 -28
  138. package/dist/src/core/build-baseline.js +6 -29
  139. package/dist/src/core/build-jar-escalation.d.ts +64 -0
  140. package/dist/src/core/build-jar-escalation.js +128 -0
  141. package/dist/src/core/build-overwrite-guard.js +1 -1
  142. package/dist/src/core/build-prepare.d.ts +5 -2
  143. package/dist/src/core/build-prepare.js +34 -6
  144. package/dist/src/core/config-paths.d.ts +2 -6
  145. package/dist/src/core/config-paths.js +4 -2
  146. package/dist/src/core/config-state.d.ts +0 -6
  147. package/dist/src/core/config-state.js +1 -19
  148. package/dist/src/core/config-validate-build-audit.d.ts +8 -0
  149. package/dist/src/core/config-validate-build-audit.js +80 -0
  150. package/dist/src/core/config-validate-file-size.d.ts +12 -0
  151. package/dist/src/core/config-validate-file-size.js +65 -0
  152. package/dist/src/core/config-validate-patch-policy.d.ts +2 -2
  153. package/dist/src/core/config-validate-patch-policy.js +3 -3
  154. package/dist/src/core/config-validate-test-toolchains.d.ts +2 -2
  155. package/dist/src/core/config-validate-test-toolchains.js +4 -5
  156. package/dist/src/core/config-validate.js +20 -3
  157. package/dist/src/core/config.d.ts +2 -2
  158. package/dist/src/core/config.js +11 -15
  159. package/dist/src/core/coverage-extend.js +4 -4
  160. package/dist/src/core/discard-baseline.js +7 -7
  161. package/dist/src/core/engine-precondition.d.ts +1 -9
  162. package/dist/src/core/engine-precondition.js +3 -8
  163. package/dist/src/core/engine-session-lock.js +13 -17
  164. package/dist/src/core/export-moved-code-shape.js +2 -2
  165. package/dist/src/core/file-lock-owner.d.ts +103 -0
  166. package/dist/src/core/file-lock-owner.js +259 -0
  167. package/dist/src/core/file-lock.d.ts +2 -7
  168. package/dist/src/core/file-lock.js +40 -91
  169. package/dist/src/core/firefox-cache.d.ts +13 -1
  170. package/dist/src/core/firefox-cache.js +59 -34
  171. package/dist/src/core/firefox-download.d.ts +15 -0
  172. package/dist/src/core/firefox-download.js +37 -21
  173. package/dist/src/core/firefox-extract.d.ts +6 -5
  174. package/dist/src/core/firefox-extract.js +36 -18
  175. package/dist/src/core/firefox.d.ts +30 -32
  176. package/dist/src/core/firefox.js +5 -36
  177. package/dist/src/core/furnace-apply-dry-run.d.ts +1 -6
  178. package/dist/src/core/furnace-apply-dry-run.js +1 -7
  179. package/dist/src/core/furnace-apply-helpers.d.ts +0 -11
  180. package/dist/src/core/furnace-apply-helpers.js +8 -19
  181. package/dist/src/core/furnace-apply-overwrite-warn.js +4 -2
  182. package/dist/src/core/furnace-apply.d.ts +1 -1
  183. package/dist/src/core/furnace-apply.js +30 -10
  184. package/dist/src/core/furnace-config.d.ts +1 -1
  185. package/dist/src/core/furnace-config.js +23 -36
  186. package/dist/src/core/furnace-constants.js +3 -2
  187. package/dist/src/core/furnace-dir-entry.d.ts +20 -0
  188. package/dist/src/core/furnace-dir-entry.js +21 -0
  189. package/dist/src/core/furnace-operation.d.ts +2 -0
  190. package/dist/src/core/furnace-operation.js +3 -1
  191. package/dist/src/core/furnace-refresh.d.ts +18 -6
  192. package/dist/src/core/furnace-refresh.js +3 -6
  193. package/dist/src/core/furnace-registration-ast.d.ts +0 -1
  194. package/dist/src/core/furnace-registration-ast.js +0 -3
  195. package/dist/src/core/furnace-registration.d.ts +2 -1
  196. package/dist/src/core/furnace-registration.js +22 -29
  197. package/dist/src/core/furnace-scanner.js +3 -6
  198. package/dist/src/core/furnace-state-persist.d.ts +2 -2
  199. package/dist/src/core/furnace-validate-registration.js +5 -7
  200. package/dist/src/core/git-base.d.ts +3 -1
  201. package/dist/src/core/git-base.js +25 -3
  202. package/dist/src/core/git-diff-new-file.d.ts +40 -0
  203. package/dist/src/core/git-diff-new-file.js +119 -0
  204. package/dist/src/core/git-diff.d.ts +9 -9
  205. package/dist/src/core/git-diff.js +88 -157
  206. package/dist/src/core/git-file-ops.d.ts +0 -7
  207. package/dist/src/core/git-file-ops.js +4 -28
  208. package/dist/src/core/git-readonly-index.d.ts +0 -2
  209. package/dist/src/core/git-readonly-index.js +0 -4
  210. package/dist/src/core/git-status.d.ts +7 -0
  211. package/dist/src/core/git-status.js +21 -9
  212. package/dist/src/core/git.d.ts +0 -6
  213. package/dist/src/core/git.js +14 -43
  214. package/dist/src/core/harness-orphans.d.ts +73 -0
  215. package/dist/src/core/harness-orphans.js +187 -0
  216. package/dist/src/core/history-log.d.ts +19 -0
  217. package/dist/src/core/history-log.js +29 -0
  218. package/dist/src/core/mach-error-hints.d.ts +0 -5
  219. package/dist/src/core/mach-error-hints.js +1 -1
  220. package/dist/src/core/mach-known-noise-filter.d.ts +25 -3
  221. package/dist/src/core/mach-known-noise-filter.js +31 -10
  222. package/dist/src/core/mach-mozconfig.js +2 -1
  223. package/dist/src/core/mach-python.d.ts +2 -0
  224. package/dist/src/core/mach-python.js +12 -7
  225. package/dist/src/core/mach-resource-shim.d.ts +13 -1
  226. package/dist/src/core/mach-resource-shim.js +112 -9
  227. package/dist/src/core/mach.d.ts +46 -53
  228. package/dist/src/core/mach.js +31 -83
  229. package/dist/src/core/marionette-port.d.ts +0 -17
  230. package/dist/src/core/marionette-port.js +12 -58
  231. package/dist/src/core/marionette-preflight.js +9 -27
  232. package/dist/src/core/mochitest-server-port.js +3 -3
  233. package/dist/src/core/moz-manifest-rules.js +37 -33
  234. package/dist/src/core/patch-apply-fuzz.js +4 -4
  235. package/dist/src/core/patch-apply.d.ts +4 -5
  236. package/dist/src/core/patch-apply.js +55 -32
  237. package/dist/src/core/patch-export-coverage.d.ts +0 -9
  238. package/dist/src/core/patch-export-coverage.js +1 -30
  239. package/dist/src/core/patch-export-update.d.ts +18 -1
  240. package/dist/src/core/patch-export-update.js +4 -3
  241. package/dist/src/core/patch-export.d.ts +11 -21
  242. package/dist/src/core/patch-export.js +44 -66
  243. package/dist/src/core/patch-files.d.ts +0 -4
  244. package/dist/src/core/patch-files.js +0 -11
  245. package/dist/src/core/patch-lint-binary.d.ts +6 -12
  246. package/dist/src/core/patch-lint-cache.d.ts +17 -1
  247. package/dist/src/core/patch-lint-cache.js +3 -6
  248. package/dist/src/core/patch-lint-checkjs.d.ts +33 -16
  249. package/dist/src/core/patch-lint-checkjs.js +43 -25
  250. package/dist/src/core/patch-lint-creators.d.ts +2 -6
  251. package/dist/src/core/patch-lint-cross.d.ts +16 -8
  252. package/dist/src/core/patch-lint-cross.js +49 -92
  253. package/dist/src/core/patch-lint-css.js +10 -6
  254. package/dist/src/core/patch-lint-file-size.d.ts +52 -0
  255. package/dist/src/core/patch-lint-file-size.js +53 -0
  256. package/dist/src/core/patch-lint-forward-registration.d.ts +58 -0
  257. package/dist/src/core/patch-lint-forward-registration.js +210 -0
  258. package/dist/src/core/patch-lint-jsdoc.js +36 -36
  259. package/dist/src/core/patch-lint-module-registration.d.ts +2 -8
  260. package/dist/src/core/patch-lint-prettier.d.ts +36 -0
  261. package/dist/src/core/patch-lint-prettier.js +126 -0
  262. package/dist/src/core/patch-lint-queue-types.d.ts +60 -0
  263. package/dist/src/core/patch-lint-queue-types.js +12 -0
  264. package/dist/src/core/patch-lint-staged-registration.d.ts +61 -0
  265. package/dist/src/core/patch-lint-staged-registration.js +90 -0
  266. package/dist/src/core/patch-lint-unmanaged-companion.d.ts +106 -0
  267. package/dist/src/core/patch-lint-unmanaged-companion.js +214 -0
  268. package/dist/src/core/patch-lint.d.ts +14 -9
  269. package/dist/src/core/patch-lint.js +27 -27
  270. package/dist/src/core/patch-lock.js +3 -3
  271. package/dist/src/core/patch-manifest-io.js +8 -7
  272. package/dist/src/core/patch-parse.d.ts +19 -8
  273. package/dist/src/core/patch-parse.js +25 -0
  274. package/dist/src/core/patch-policy.js +15 -27
  275. package/dist/src/core/patch-source-metadata.d.ts +1 -3
  276. package/dist/src/core/patch-source-metadata.js +0 -4
  277. package/dist/src/core/patch-transform.d.ts +21 -8
  278. package/dist/src/core/patch-transform.js +38 -10
  279. package/dist/src/core/process-cpu-time.js +1 -1
  280. package/dist/src/core/rebase-session.d.ts +2 -8
  281. package/dist/src/core/rebase-session.js +5 -17
  282. package/dist/src/core/register-browser-content.js +3 -2
  283. package/dist/src/core/register-browser-test.js +2 -1
  284. package/dist/src/core/register-module.js +2 -1
  285. package/dist/src/core/register-scaffold.js +5 -4
  286. package/dist/src/core/register-shared-css.js +2 -1
  287. package/dist/src/core/run-log-redact.d.ts +38 -0
  288. package/dist/src/core/run-log-redact.js +69 -0
  289. package/dist/src/core/run-log.d.ts +13 -2
  290. package/dist/src/core/run-log.js +67 -18
  291. package/dist/src/core/signal-critical.js +2 -4
  292. package/dist/src/core/smoke-patterns.d.ts +0 -1
  293. package/dist/src/core/smoke-patterns.js +18 -11
  294. package/dist/src/core/state-file.d.ts +12 -1
  295. package/dist/src/core/state-file.js +14 -1
  296. package/dist/src/core/status-classify.js +2 -1
  297. package/dist/src/core/test-changed-prefs.js +26 -11
  298. package/dist/src/core/test-harness-crash.js +61 -48
  299. package/dist/src/core/test-harness-verdict-notes.d.ts +55 -0
  300. package/dist/src/core/test-harness-verdict-notes.js +48 -0
  301. package/dist/src/core/test-stale-check.d.ts +23 -8
  302. package/dist/src/core/test-stale-check.js +77 -4
  303. package/dist/src/core/test-stall-triage.js +37 -19
  304. package/dist/src/core/test-xpcshell-retry.d.ts +26 -7
  305. package/dist/src/core/test-xpcshell-retry.js +14 -11
  306. package/dist/src/core/token-category.d.ts +33 -0
  307. package/dist/src/core/token-category.js +56 -48
  308. package/dist/src/core/token-dark-mode.js +2 -14
  309. package/dist/src/core/token-inventory.d.ts +56 -0
  310. package/dist/src/core/token-inventory.js +109 -0
  311. package/dist/src/core/token-manager.d.ts +1 -1
  312. package/dist/src/core/token-manager.js +1 -1
  313. package/dist/src/core/token-variant.js +21 -22
  314. package/dist/src/core/toolchain-preflight.js +1 -1
  315. package/dist/src/core/tree-cow.d.ts +0 -2
  316. package/dist/src/core/tree-cow.js +2 -2
  317. package/dist/src/core/tree-store.js +4 -5
  318. package/dist/src/core/typecheck.js +2 -2
  319. package/dist/src/core/wire-dom-fragment.js +6 -17
  320. package/dist/src/core/wire-utils.js +5 -17
  321. package/dist/src/core/xpcshell-appdir.js +2 -4
  322. package/dist/src/errors/base.d.ts +53 -1
  323. package/dist/src/errors/base.js +79 -11
  324. package/dist/src/errors/build.d.ts +28 -0
  325. package/dist/src/errors/build.js +51 -10
  326. package/dist/src/errors/config.d.ts +0 -14
  327. package/dist/src/errors/config.js +13 -34
  328. package/dist/src/errors/download.d.ts +13 -0
  329. package/dist/src/errors/download.js +53 -24
  330. package/dist/src/errors/furnace.js +8 -9
  331. package/dist/src/errors/git.d.ts +8 -8
  332. package/dist/src/errors/git.js +20 -30
  333. package/dist/src/errors/patch.js +8 -10
  334. package/dist/src/errors/rebase.js +7 -7
  335. package/dist/src/types/commands/options.d.ts +96 -123
  336. package/dist/src/types/commands/patches.d.ts +17 -6
  337. package/dist/src/types/config.d.ts +75 -2
  338. package/dist/src/types/index.d.ts +0 -1
  339. package/dist/src/utils/assert.d.ts +3 -15
  340. package/dist/src/utils/assert.js +1 -18
  341. package/dist/src/utils/build-info.d.ts +1 -1
  342. package/dist/src/utils/child-env.d.ts +2 -1
  343. package/dist/src/utils/elapsed.js +10 -1
  344. package/dist/src/utils/fs.d.ts +4 -2
  345. package/dist/src/utils/fs.js +11 -14
  346. package/dist/src/utils/hash.d.ts +12 -0
  347. package/dist/src/utils/hash.js +16 -0
  348. package/dist/src/utils/line-dispatch.d.ts +15 -0
  349. package/dist/src/utils/line-dispatch.js +48 -0
  350. package/dist/src/utils/options.d.ts +1 -7
  351. package/dist/src/utils/options.js +6 -7
  352. package/dist/src/utils/package-root.js +3 -12
  353. package/dist/src/utils/platform.d.ts +0 -4
  354. package/dist/src/utils/process-group.d.ts +2 -1
  355. package/dist/src/utils/process-group.js +20 -19
  356. package/dist/src/utils/process.d.ts +40 -26
  357. package/dist/src/utils/process.js +115 -166
  358. package/dist/src/utils/ps-duration.js +3 -3
  359. package/dist/src/utils/sleep.d.ts +26 -0
  360. package/dist/src/utils/sleep.js +31 -0
  361. package/dist/src/utils/validation.d.ts +2 -38
  362. package/dist/src/utils/validation.js +7 -16
  363. package/docs/configuration.md +136 -1
  364. package/docs/exit-codes.md +34 -20
  365. package/docs/lifecycle-invariants.md +18 -12
  366. package/docs/machine-output.md +31 -8
  367. package/docs/patch-workflow.md +2 -1
  368. package/docs/run-logs.md +44 -4
  369. package/docs/testing.md +70 -11
  370. package/package.json +8 -7
  371. package/dist/src/commands/rebase.d.ts +0 -4
  372. package/dist/src/commands/rebase.js +0 -5
  373. package/dist/src/core/patch-artifact-normalize.d.ts +0 -21
  374. package/dist/src/core/patch-artifact-normalize.js +0 -24
  375. package/dist/src/core/patch-lint-reexports.d.ts +0 -7
  376. package/dist/src/core/patch-lint-reexports.js +0 -8
  377. package/dist/src/core/wire-targets.d.ts +0 -7
  378. package/dist/src/core/wire-targets.js +0 -8
@@ -18,6 +18,27 @@
18
18
  * binary new-file conflicts as unresolvable rather than auto-resolving.
19
19
  */
20
20
  export declare function extractNewFileContentFromDiff(diff: string, targetFile?: string): string;
21
+ /**
22
+ * Projects every TEXT new file a diff creates into a path → content map, for
23
+ * the cross-patch lint projections that build a synthetic patch-queue entry.
24
+ *
25
+ * Binary new files are SKIPPED rather than refused. The map feeds only the
26
+ * forward-import rule, which scans content for import statements — a binary
27
+ * blob authors none, so omitting it contributes exactly what including it
28
+ * would. Refusing instead (which is what
29
+ * {@link extractNewFileContentFromDiff} correctly does for a caller that
30
+ * named ONE file) made vendoring a new binary impossible: every projection
31
+ * site fed it every detected new file, so `export --order`, `re-export
32
+ * --scan --scan-file`, `patch move-files-into` and `patch split` all died on
33
+ * a file the export half had just written a valid `GIT binary patch` for.
34
+ * Nothing is lost by skipping: binary bodies are covered independently by the
35
+ * `binary-body-not-reconstructable` queue lint, and
36
+ * `buildPatchQueueContext` already tolerates them for real queue entries.
37
+ *
38
+ * One parse for the whole diff, rather than one per new file as the five
39
+ * call sites previously did.
40
+ */
41
+ export declare function buildNewFileTextProjection(diff: string): Map<string, string>;
21
42
  /**
22
43
  * Extracts the complete file content from a "new file" patch.
23
44
  * When targetFile is provided, only extracts content for that file
@@ -27,14 +48,6 @@ export declare function extractNewFileContentFromDiff(diff: string, targetFile?:
27
48
  * @returns The file content that the patch would create
28
49
  */
29
50
  export declare function extractNewFileContent(patchPath: string, targetFile?: string): Promise<string>;
30
- /**
31
- * Applies a patch's changes to content.
32
- * @param content - Original content (null for new files)
33
- * @param patchPath - Path to the patch file
34
- * @param targetFile - The file path within the patch
35
- * @returns Modified content
36
- */
37
- export declare function applyPatchToContent(content: string | null, patchPath: string, targetFile: string): Promise<string>;
38
51
  /**
39
52
  * Applies an already-read patch body to content. Batched callers that hold
40
53
  * many (patch, file) pairs use this to read each patch body once instead of
@@ -27,6 +27,15 @@ export function extractNewFileContentFromDiff(diff, targetFile) {
27
27
  if (binary) {
28
28
  throw new PatchError(`Cannot extract text content from binary patch section for ${binary.targetPath}`, binary.targetPath);
29
29
  }
30
+ return contentFromSections(sections);
31
+ }
32
+ /**
33
+ * Walks the added (`+`) lines of already-parsed sections into file content.
34
+ * Shared by {@link extractNewFileContentFromDiff} and
35
+ * {@link buildNewFileTextProjection} so the two cannot drift on the empty-file
36
+ * and no-trailing-newline cases below.
37
+ */
38
+ function contentFromSections(sections) {
30
39
  // An empty new file is a legitimate git diff: `new file mode` with no
31
40
  // hunks at all. It must extract as '' — the historical line-walker
32
41
  // returned '\n' for it, creating a one-byte file that failed checksum
@@ -51,6 +60,35 @@ export function extractNewFileContentFromDiff(diff, targetFile) {
51
60
  const result = contentLines.join('\n');
52
61
  return hasNoNewlineMarker ? result : result + '\n';
53
62
  }
63
+ /**
64
+ * Projects every TEXT new file a diff creates into a path → content map, for
65
+ * the cross-patch lint projections that build a synthetic patch-queue entry.
66
+ *
67
+ * Binary new files are SKIPPED rather than refused. The map feeds only the
68
+ * forward-import rule, which scans content for import statements — a binary
69
+ * blob authors none, so omitting it contributes exactly what including it
70
+ * would. Refusing instead (which is what
71
+ * {@link extractNewFileContentFromDiff} correctly does for a caller that
72
+ * named ONE file) made vendoring a new binary impossible: every projection
73
+ * site fed it every detected new file, so `export --order`, `re-export
74
+ * --scan --scan-file`, `patch move-files-into` and `patch split` all died on
75
+ * a file the export half had just written a valid `GIT binary patch` for.
76
+ * Nothing is lost by skipping: binary bodies are covered independently by the
77
+ * `binary-body-not-reconstructable` queue lint, and
78
+ * `buildPatchQueueContext` already tolerates them for real queue entries.
79
+ *
80
+ * One parse for the whole diff, rather than one per new file as the five
81
+ * call sites previously did.
82
+ */
83
+ export function buildNewFileTextProjection(diff) {
84
+ const newFiles = new Map();
85
+ for (const section of parseDiffSections(diff)) {
86
+ if (!section.isNewFile || section.isBinary)
87
+ continue;
88
+ newFiles.set(section.targetPath, contentFromSections([section]));
89
+ }
90
+ return newFiles;
91
+ }
54
92
  /**
55
93
  * Extracts the complete file content from a "new file" patch.
56
94
  * When targetFile is provided, only extracts content for that file
@@ -63,16 +101,6 @@ export async function extractNewFileContent(patchPath, targetFile) {
63
101
  const content = await readText(patchPath);
64
102
  return extractNewFileContentFromDiff(content, targetFile);
65
103
  }
66
- /**
67
- * Applies a patch's changes to content.
68
- * @param content - Original content (null for new files)
69
- * @param patchPath - Path to the patch file
70
- * @param targetFile - The file path within the patch
71
- * @returns Modified content
72
- */
73
- export async function applyPatchToContent(content, patchPath, targetFile) {
74
- return applyPatchTextToContent(content, await readText(patchPath), targetFile);
75
- }
76
104
  /**
77
105
  * Applies an already-read patch body to content. Batched callers that hold
78
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
+ }
@@ -22,7 +22,11 @@
22
22
  export interface RunLog {
23
23
  /** Absolute path to the log file. */
24
24
  path: string;
25
- /** Appends raw output. Silently drops on any stream error. */
25
+ /**
26
+ * Appends output. Complete lines pass through {@link redactRunLogText}
27
+ * before hitting disk; a trailing partial line is held until its newline
28
+ * arrives (or until `close`). Silently drops on any stream error.
29
+ */
26
30
  write: (chunk: string) => void;
27
31
  /** Flushes and closes. Resolves even when the stream already failed. */
28
32
  close: () => Promise<void>;
@@ -45,7 +49,14 @@ export declare function setActiveRunLog(log: RunLog | undefined): void;
45
49
  export declare function getActiveRunLogPath(): string | undefined;
46
50
  /** Appends to the active run log. A no-op when none is open. */
47
51
  export declare function writeToActiveRunLog(chunk: string): void;
48
- /** Closes and clears the active run log. Resolves even when none is open. */
52
+ /**
53
+ * Closes and clears the active run log. Resolves even when none is open.
54
+ *
55
+ * Also called from the entry point's SIGINT/SIGTERM path: the held partial
56
+ * line and anything queued in the stream would otherwise be lost across
57
+ * `process.exit`, and a killed run's last line is usually the one that
58
+ * says what it was doing.
59
+ */
49
60
  export declare function closeActiveRunLog(): Promise<void>;
50
61
  /**
51
62
  * A writable that forwards to `base` AND to the active run log.
@@ -23,8 +23,11 @@ import { createWriteStream } from 'node:fs';
23
23
  import { mkdir, readdir, unlink } from 'node:fs/promises';
24
24
  import { join } from 'node:path';
25
25
  import { Writable } from 'node:stream';
26
+ import { toError } from '../utils/errors.js';
26
27
  import { verbose } from '../utils/logger.js';
27
28
  import { FIREFORGE_DIR } from './config-paths.js';
29
+ import { redactRunLogText } from './run-log-redact.js';
30
+ import { fileSafeTimestamp } from './state-file.js';
28
31
  /** Directory under `.fireforge/` holding run logs. */
29
32
  const LOGS_DIRNAME = 'logs';
30
33
  /**
@@ -33,20 +36,34 @@ const LOGS_DIRNAME = 'logs';
33
36
  * most often needs to read together.
34
37
  */
35
38
  const RETAINED_LOGS_PER_COMMAND = 20;
39
+ /**
40
+ * Cap on the partial line held back between writes. A child emitting one
41
+ * enormous unterminated line (a minified dump on stderr) would otherwise
42
+ * grow the buffer without bound — and, because each write rebuilds
43
+ * `pending + chunk`, quadratically — with nothing reaching disk until
44
+ * close. Past the cap the tail is redacted and written as-is; a `KEY=value`
45
+ * split exactly at the cap boundary is the accepted cost. Same figure as
46
+ * `line-dispatch.ts` uses for the stream matchers.
47
+ */
48
+ const MAX_PENDING_LINE_SIZE = 1024 * 1024;
49
+ /**
50
+ * Index just past the last line terminator in `text`, or -1 when there is
51
+ * none. `\n` and lone `\r` both terminate — progress-bar output (mach,
52
+ * cargo) repaints a line with carriage returns and may never send a
53
+ * newline, which would otherwise pin the whole run in `pending`. A single
54
+ * trailing `\r` is NOT a terminator yet: it may be the first half of a
55
+ * `\r\n` split across chunks.
56
+ */
57
+ function lastLineEnd(text) {
58
+ const lastNewline = text.lastIndexOf('\n');
59
+ const lastReturn = text.lastIndexOf('\r');
60
+ const cut = lastReturn > lastNewline && lastReturn < text.length - 1 ? lastReturn : lastNewline;
61
+ return cut === -1 ? -1 : cut + 1;
62
+ }
36
63
  /** `.fireforge/logs` under `projectRoot`. */
37
64
  export function getRunLogDir(projectRoot) {
38
65
  return join(projectRoot, FIREFORGE_DIR, LOGS_DIRNAME);
39
66
  }
40
- /**
41
- * A filesystem-safe, sortable timestamp: `2026-08-28T14-32-05-123Z`.
42
- *
43
- * Colons are not portable in filenames (Windows rejects them outright), so
44
- * the ISO form is punctuated with dashes only. Lexical order still equals
45
- * chronological order, which is what the pruning below relies on.
46
- */
47
- function logTimestamp(now) {
48
- return now.toISOString().replace(/[:.]/g, '-');
49
- }
50
67
  /**
51
68
  * Makes room for one new log by deleting the oldest, so that at most
52
69
  * {@link RETAINED_LOGS_PER_COMMAND} logs for `command` exist once the new
@@ -76,7 +93,7 @@ async function pruneRunLogs(dir, command) {
76
93
  }));
77
94
  }
78
95
  catch (error) {
79
- verbose(`Run-log pruning skipped: ${String(error)}`);
96
+ verbose(`Run-log pruning skipped: ${toError(error).message}`);
80
97
  }
81
98
  }
82
99
  /**
@@ -91,14 +108,14 @@ async function pruneRunLogs(dir, command) {
91
108
  export async function openRunLog(projectRoot, command, now = new Date()) {
92
109
  const dir = getRunLogDir(projectRoot);
93
110
  let stream;
94
- const path = join(dir, `${command}-${logTimestamp(now)}.log`);
111
+ const path = join(dir, `${command}-${fileSafeTimestamp(now)}.log`);
95
112
  try {
96
113
  await mkdir(dir, { recursive: true });
97
114
  await pruneRunLogs(dir, command);
98
115
  stream = createWriteStream(path, { flags: 'a' });
99
116
  }
100
117
  catch (error) {
101
- verbose(`Run log unavailable (${path}): ${String(error)}`);
118
+ verbose(`Run log unavailable (${path}): ${toError(error).message}`);
102
119
  return undefined;
103
120
  }
104
121
  // A stream error after open (disk full, a removed directory) must not
@@ -109,19 +126,44 @@ export async function openRunLog(projectRoot, command, now = new Date()) {
109
126
  broken = true;
110
127
  verbose(`Run log write failed (${path}): ${error.message}`);
111
128
  });
129
+ // Redaction is per LINE, and chunks arrive at arbitrary byte boundaries,
130
+ // so a `KEY=value` split across two chunks would slip past a per-chunk
131
+ // pass. Hold the trailing partial line until its terminator arrives (or
132
+ // it outgrows MAX_PENDING_LINE_SIZE, or the log is closed).
133
+ let pending = '';
134
+ const emit = (text) => {
135
+ if (broken || text.length === 0)
136
+ return;
137
+ try {
138
+ stream.write(redactRunLogText(text));
139
+ }
140
+ catch {
141
+ broken = true;
142
+ }
143
+ };
112
144
  return {
113
145
  path,
114
146
  write: (chunk) => {
115
147
  if (broken)
116
148
  return;
117
- try {
118
- stream.write(chunk);
149
+ const buffered = pending + chunk;
150
+ const lineEnd = lastLineEnd(buffered);
151
+ if (lineEnd === -1) {
152
+ pending = buffered;
119
153
  }
120
- catch {
121
- broken = true;
154
+ else {
155
+ pending = buffered.slice(lineEnd);
156
+ emit(buffered.slice(0, lineEnd));
157
+ }
158
+ if (pending.length > MAX_PENDING_LINE_SIZE) {
159
+ emit(pending);
160
+ pending = '';
122
161
  }
123
162
  },
124
163
  close: async () => {
164
+ const tail = pending;
165
+ pending = '';
166
+ emit(tail);
125
167
  await new Promise((resolve) => {
126
168
  stream.end(() => {
127
169
  resolve();
@@ -155,7 +197,14 @@ export function getActiveRunLogPath() {
155
197
  export function writeToActiveRunLog(chunk) {
156
198
  activeRunLog?.write(chunk);
157
199
  }
158
- /** Closes and clears the active run log. Resolves even when none is open. */
200
+ /**
201
+ * Closes and clears the active run log. Resolves even when none is open.
202
+ *
203
+ * Also called from the entry point's SIGINT/SIGTERM path: the held partial
204
+ * line and anything queued in the stream would otherwise be lost across
205
+ * `process.exit`, and a killed run's last line is usually the one that
206
+ * says what it was doing.
207
+ */
159
208
  export async function closeActiveRunLog() {
160
209
  const log = activeRunLog;
161
210
  activeRunLog = undefined;
@@ -18,6 +18,7 @@
18
18
  * itself. The bin entry point is responsible for awaiting
19
19
  * `waitForActiveCriticalSections` before terminating.
20
20
  */
21
+ import { sleep } from '../utils/sleep.js';
21
22
  const activeSections = new Set();
22
23
  /**
23
24
  * Runs `fn` inside a signal-deferred critical section. The CLI entry point's
@@ -57,8 +58,5 @@ export async function waitForActiveCriticalSections(timeoutMs) {
57
58
  if (activeSections.size === 0)
58
59
  return;
59
60
  const snapshot = [...activeSections].map((s) => s.promise);
60
- await Promise.race([
61
- Promise.allSettled(snapshot).then(() => undefined),
62
- new Promise((resolve) => setTimeout(resolve, timeoutMs)),
63
- ]);
61
+ await Promise.race([Promise.allSettled(snapshot).then(() => undefined), sleep(timeoutMs)]);
64
62
  }
@@ -10,7 +10,6 @@
10
10
  * warning (e.g. `"pending JavaScript error cleanup"`) do not start with
11
11
  * the prefix and should not trip the scanner.
12
12
  */
13
- export declare const SMOKE_ERROR_PATTERNS: readonly RegExp[];
14
13
  /**
15
14
  * Returns `true` when `line` matches any pattern in
16
15
  * {@link SMOKE_ERROR_PATTERNS}. Does not consult the allowlist — that step