@hominis/fireforge 0.43.1 → 0.44.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.
- package/CHANGELOG.md +126 -0
- package/README.md +10 -1
- package/dist/bin/fireforge.d.ts +4 -9
- package/dist/bin/fireforge.js +42 -40
- package/dist/build-info.json +4 -4
- package/dist/src/cli.js +74 -8
- package/dist/src/commands/bootstrap.js +17 -20
- package/dist/src/commands/build.js +25 -32
- package/dist/src/commands/config.js +34 -41
- package/dist/src/commands/discard.js +8 -9
- package/dist/src/commands/doctor-check-core.d.ts +4 -18
- package/dist/src/commands/doctor-check-core.js +8 -35
- package/dist/src/commands/doctor-external-toolchains.js +2 -2
- package/dist/src/commands/doctor-furnace-config-sync.d.ts +18 -0
- package/dist/src/commands/{doctor-furnace-manifest-sync.js → doctor-furnace-config-sync.js} +27 -26
- package/dist/src/commands/doctor-furnace-jar.d.ts +7 -9
- package/dist/src/commands/doctor-furnace-jar.js +7 -9
- package/dist/src/commands/doctor-furnace.js +21 -24
- package/dist/src/commands/doctor-orphaned-harness.d.ts +16 -20
- package/dist/src/commands/doctor-orphaned-harness.js +18 -44
- package/dist/src/commands/doctor-post-rebase-audit.d.ts +2 -0
- package/dist/src/commands/{doctor/post-rebase-audit.js → doctor-post-rebase-audit.js} +3 -3
- package/dist/src/commands/doctor-working-tree.d.ts +10 -12
- package/dist/src/commands/doctor-working-tree.js +11 -18
- package/dist/src/commands/doctor.js +35 -53
- package/dist/src/commands/download.js +32 -48
- package/dist/src/commands/export-all.js +29 -33
- package/dist/src/commands/export-flow.js +6 -6
- package/dist/src/commands/export-placement-conflicts.js +10 -9
- package/dist/src/commands/export-placement-gate.js +5 -5
- package/dist/src/commands/export-shared.d.ts +30 -33
- package/dist/src/commands/export-shared.js +45 -43
- package/dist/src/commands/export.js +21 -27
- package/dist/src/commands/furnace/apply.js +14 -23
- package/dist/src/commands/furnace/chrome-doc-remove.js +14 -21
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +23 -23
- package/dist/src/commands/furnace/chrome-doc-templates.js +23 -23
- package/dist/src/commands/furnace/chrome-doc-tests.js +26 -29
- package/dist/src/commands/furnace/chrome-doc.d.ts +16 -17
- package/dist/src/commands/furnace/chrome-doc.js +31 -41
- package/dist/src/commands/furnace/create-browser-test.d.ts +4 -4
- package/dist/src/commands/furnace/create-browser-test.js +8 -8
- package/dist/src/commands/furnace/create-mochikit.d.ts +6 -7
- package/dist/src/commands/furnace/create-mochikit.js +8 -9
- package/dist/src/commands/furnace/create-readback.d.ts +12 -13
- package/dist/src/commands/furnace/create-readback.js +12 -13
- package/dist/src/commands/furnace/create-templates.d.ts +22 -28
- package/dist/src/commands/furnace/create-templates.js +35 -30
- package/dist/src/commands/furnace/create-xpcshell.js +2 -2
- package/dist/src/commands/furnace/create.js +33 -23
- package/dist/src/commands/furnace/deploy.js +16 -24
- package/dist/src/commands/furnace/diff.js +9 -9
- package/dist/src/commands/furnace/init.js +33 -36
- package/dist/src/commands/furnace/override.js +23 -25
- package/dist/src/commands/furnace/preview.js +47 -67
- package/dist/src/commands/furnace/refresh.js +12 -16
- package/dist/src/commands/furnace/remove-mutations.d.ts +3 -6
- package/dist/src/commands/furnace/remove-mutations.js +9 -16
- package/dist/src/commands/furnace/remove-state.js +1 -1
- package/dist/src/commands/furnace/remove.js +33 -38
- package/dist/src/commands/furnace/rename-xpcshell.d.ts +8 -11
- package/dist/src/commands/furnace/rename-xpcshell.js +8 -11
- package/dist/src/commands/furnace/rename.js +59 -69
- package/dist/src/commands/furnace/scan.js +19 -33
- package/dist/src/commands/furnace/status.js +12 -9
- package/dist/src/commands/furnace/sync.js +9 -16
- package/dist/src/commands/furnace/validate.js +20 -20
- package/dist/src/commands/import.js +42 -35
- package/dist/src/commands/lint-per-patch.js +15 -10
- package/dist/src/commands/lint-per-run-checkjs.d.ts +13 -13
- package/dist/src/commands/lint-per-run-checkjs.js +13 -13
- package/dist/src/commands/lint-report.js +6 -7
- package/dist/src/commands/lint.d.ts +15 -16
- package/dist/src/commands/lint.js +86 -102
- package/dist/src/commands/package.js +6 -11
- package/dist/src/commands/patch/compact.js +1 -1
- package/dist/src/commands/patch/delete.js +24 -33
- package/dist/src/commands/patch/index.d.ts +5 -6
- package/dist/src/commands/patch/index.js +10 -12
- package/dist/src/commands/patch/lint-ignore.js +14 -13
- package/dist/src/commands/patch/move-files-create.d.ts +6 -6
- package/dist/src/commands/patch/move-files-create.js +8 -8
- package/dist/src/commands/patch/move-files-into.d.ts +6 -6
- package/dist/src/commands/patch/move-files-into.js +8 -8
- package/dist/src/commands/patch/patch-context.d.ts +2 -3
- package/dist/src/commands/patch/patch-context.js +2 -3
- package/dist/src/commands/patch/rename.d.ts +10 -11
- package/dist/src/commands/patch/rename.js +11 -12
- package/dist/src/commands/patch/reorder.js +8 -9
- package/dist/src/commands/patch/split-plan.js +1 -1
- package/dist/src/commands/patch/split.d.ts +8 -10
- package/dist/src/commands/patch/split.js +9 -11
- package/dist/src/commands/patch/staged-dependency-validate.js +8 -8
- package/dist/src/commands/patch/staged-dependency.js +7 -3
- package/dist/src/commands/patch/tier.d.ts +9 -9
- package/dist/src/commands/patch/tier.js +18 -15
- package/dist/src/commands/re-export-adjacent.js +9 -8
- package/dist/src/commands/re-export-bulk-scan.d.ts +9 -9
- package/dist/src/commands/re-export-bulk-scan.js +22 -21
- package/dist/src/commands/re-export-drift.js +10 -10
- package/dist/src/commands/re-export-files.d.ts +6 -7
- package/dist/src/commands/re-export-files.js +34 -44
- package/dist/src/commands/re-export-lint.d.ts +10 -13
- package/dist/src/commands/re-export-lint.js +16 -14
- package/dist/src/commands/re-export-options.d.ts +6 -6
- package/dist/src/commands/re-export-options.js +6 -6
- package/dist/src/commands/re-export-register.d.ts +4 -10
- package/dist/src/commands/re-export-register.js +5 -11
- package/dist/src/commands/re-export-scan.js +1 -1
- package/dist/src/commands/re-export-single.d.ts +3 -3
- package/dist/src/commands/re-export-single.js +33 -25
- package/dist/src/commands/re-export.js +13 -21
- package/dist/src/commands/rebase/abort.js +3 -3
- package/dist/src/commands/rebase/confirm.js +2 -1
- package/dist/src/commands/rebase/continue.d.ts +1 -1
- package/dist/src/commands/rebase/continue.js +14 -9
- package/dist/src/commands/rebase/index.js +11 -11
- package/dist/src/commands/rebase/patch-loop.d.ts +7 -1
- package/dist/src/commands/rebase/patch-loop.js +60 -39
- package/dist/src/commands/rebase/summary.js +1 -1
- package/dist/src/commands/register.js +5 -7
- package/dist/src/commands/reset.js +2 -1
- package/dist/src/commands/resolve.d.ts +16 -13
- package/dist/src/commands/resolve.js +30 -33
- package/dist/src/commands/run.js +33 -35
- package/dist/src/commands/setup-support.js +15 -19
- package/dist/src/commands/setup.js +13 -9
- package/dist/src/commands/status-check.d.ts +6 -6
- package/dist/src/commands/status-json.d.ts +6 -6
- package/dist/src/commands/status-json.js +5 -0
- package/dist/src/commands/status-output.d.ts +5 -5
- package/dist/src/commands/status-output.js +6 -6
- package/dist/src/commands/status-ownership.d.ts +16 -14
- package/dist/src/commands/status-ownership.js +16 -14
- package/dist/src/commands/status.js +42 -34
- package/dist/src/commands/test-build-phase.js +26 -13
- package/dist/src/commands/test-diagnose.js +44 -44
- package/dist/src/commands/test-register.js +1 -0
- package/dist/src/commands/test-run.d.ts +14 -16
- package/dist/src/commands/test-run.js +21 -13
- package/dist/src/commands/test-stale-gate.d.ts +15 -16
- package/dist/src/commands/test-stale-gate.js +15 -16
- package/dist/src/commands/test-verdict.d.ts +20 -3
- package/dist/src/commands/test-verdict.js +30 -0
- package/dist/src/commands/test.js +83 -76
- package/dist/src/commands/token-coverage.js +3 -4
- package/dist/src/commands/token.js +16 -18
- package/dist/src/commands/tree.js +47 -31
- package/dist/src/commands/typecheck.d.ts +10 -12
- package/dist/src/commands/typecheck.js +24 -27
- package/dist/src/commands/verify.js +13 -15
- package/dist/src/commands/watch.js +21 -23
- package/dist/src/commands/wire.js +52 -62
- package/dist/src/core/ast-utils.d.ts +8 -7
- package/dist/src/core/ast-utils.js +8 -7
- package/dist/src/core/branding.d.ts +9 -14
- package/dist/src/core/branding.js +7 -11
- package/dist/src/core/browser-wire.d.ts +1 -1
- package/dist/src/core/browser-wire.js +14 -19
- package/dist/src/core/build-audit-platform.js +25 -33
- package/dist/src/core/build-audit-registration.js +17 -25
- package/dist/src/core/build-audit-resolve.d.ts +15 -17
- package/dist/src/core/build-audit-resolve.js +27 -33
- package/dist/src/core/build-audit-transforms.js +18 -19
- package/dist/src/core/build-audit.js +28 -42
- package/dist/src/core/build-baseline-types.d.ts +29 -35
- package/dist/src/core/build-baseline.d.ts +5 -4
- package/dist/src/core/build-baseline.js +14 -14
- package/dist/src/core/build-overwrite-guard.d.ts +7 -14
- package/dist/src/core/build-overwrite-guard.js +17 -27
- package/dist/src/core/build-prepare.d.ts +2 -2
- package/dist/src/core/build-prepare.js +8 -9
- package/dist/src/core/config-state.js +12 -4
- package/dist/src/core/config-validate.js +8 -5
- package/dist/src/core/config.d.ts +5 -8
- package/dist/src/core/config.js +7 -16
- package/dist/src/core/coverage-extend.d.ts +15 -13
- package/dist/src/core/coverage-extend.js +15 -13
- package/dist/src/core/destructive.d.ts +31 -9
- package/dist/src/core/destructive.js +29 -26
- package/dist/src/core/diff-hunks.js +10 -12
- package/dist/src/core/discard-baseline.d.ts +8 -8
- package/dist/src/core/discard-baseline.js +16 -15
- package/dist/src/core/display-state.d.ts +12 -14
- package/dist/src/core/display-state.js +12 -14
- package/dist/src/core/engine-changes.d.ts +2 -4
- package/dist/src/core/engine-changes.js +2 -4
- package/dist/src/core/engine-precondition.d.ts +28 -13
- package/dist/src/core/engine-precondition.js +25 -10
- package/dist/src/core/engine-session-lock.d.ts +47 -21
- package/dist/src/core/engine-session-lock.js +168 -33
- package/dist/src/core/export-moved-code-shape.d.ts +10 -10
- package/dist/src/core/export-moved-code-shape.js +10 -10
- package/dist/src/core/file-lock.d.ts +62 -5
- package/dist/src/core/file-lock.js +196 -59
- package/dist/src/core/firefox-cache.js +1 -3
- package/dist/src/core/firefox-extract.js +10 -14
- package/dist/src/core/firefox.d.ts +7 -5
- package/dist/src/core/firefox.js +4 -5
- package/dist/src/core/furnace-apply-dry-run.d.ts +2 -2
- package/dist/src/core/furnace-apply-dry-run.js +3 -2
- package/dist/src/core/furnace-apply-ftl.d.ts +7 -9
- package/dist/src/core/furnace-apply-ftl.js +12 -12
- package/dist/src/core/furnace-apply-helpers.d.ts +7 -7
- package/dist/src/core/furnace-apply-helpers.js +19 -16
- package/dist/src/core/furnace-apply-overwrite-warn.d.ts +8 -9
- package/dist/src/core/furnace-apply-overwrite-warn.js +8 -9
- package/dist/src/core/furnace-apply-undeploy-actions.d.ts +3 -4
- package/dist/src/core/furnace-apply-undeploy-actions.js +3 -4
- package/dist/src/core/furnace-apply.d.ts +5 -8
- package/dist/src/core/furnace-apply.js +19 -24
- package/dist/src/core/furnace-checksum-utils.d.ts +14 -0
- package/dist/src/core/furnace-checksum-utils.js +16 -0
- package/dist/src/core/furnace-config.d.ts +21 -30
- package/dist/src/core/furnace-config.js +32 -54
- package/dist/src/core/furnace-constants.d.ts +8 -0
- package/dist/src/core/furnace-constants.js +10 -2
- package/dist/src/core/furnace-css-fragments.d.ts +5 -5
- package/dist/src/core/furnace-css-fragments.js +5 -5
- package/dist/src/core/furnace-jsconfig.d.ts +11 -12
- package/dist/src/core/furnace-jsconfig.js +11 -12
- package/dist/src/core/furnace-marker.d.ts +7 -8
- package/dist/src/core/furnace-marker.js +7 -8
- package/dist/src/core/furnace-operation.d.ts +31 -0
- package/dist/src/core/furnace-operation.js +54 -17
- package/dist/src/core/furnace-precondition.d.ts +61 -0
- package/dist/src/core/furnace-precondition.js +67 -0
- package/dist/src/core/furnace-registration-remove.d.ts +5 -6
- package/dist/src/core/furnace-registration-remove.js +6 -7
- package/dist/src/core/furnace-registration-validate.d.ts +13 -0
- package/dist/src/core/furnace-registration-validate.js +20 -1
- package/dist/src/core/furnace-registration.d.ts +5 -6
- package/dist/src/core/furnace-registration.js +10 -12
- package/dist/src/core/furnace-rollback.js +7 -7
- package/dist/src/core/furnace-scanner.d.ts +1 -0
- package/dist/src/core/furnace-scanner.js +1 -2
- package/dist/src/core/furnace-stale-export.d.ts +10 -10
- package/dist/src/core/furnace-stale-export.js +10 -10
- package/dist/src/core/furnace-state-persist.d.ts +6 -8
- package/dist/src/core/furnace-state-persist.js +6 -8
- package/dist/src/core/furnace-validate-accessibility.d.ts +22 -0
- package/dist/src/core/furnace-validate-accessibility.js +310 -1
- package/dist/src/core/furnace-validate-compatibility.js +141 -1
- package/dist/src/core/furnace-validate-helpers.d.ts +5 -116
- package/dist/src/core/furnace-validate-helpers.js +0 -491
- package/dist/src/core/furnace-validate-registration.d.ts +7 -0
- package/dist/src/core/furnace-validate-registration.js +115 -41
- package/dist/src/core/furnace-validate.d.ts +16 -0
- package/dist/src/core/furnace-validate.js +188 -145
- package/dist/src/core/furnace-version-drift.d.ts +10 -11
- package/dist/src/core/furnace-version-drift.js +10 -11
- package/dist/src/core/git-base.d.ts +7 -9
- package/dist/src/core/git-base.js +7 -9
- package/dist/src/core/git-diff.d.ts +20 -9
- package/dist/src/core/git-diff.js +148 -63
- package/dist/src/core/git-file-ops.d.ts +19 -19
- package/dist/src/core/git-file-ops.js +19 -19
- package/dist/src/core/git-readonly-index.d.ts +53 -19
- package/dist/src/core/git-readonly-index.js +75 -31
- package/dist/src/core/git-status.d.ts +5 -6
- package/dist/src/core/git-status.js +5 -6
- package/dist/src/core/git.d.ts +9 -10
- package/dist/src/core/git.js +54 -70
- package/dist/src/core/license-headers.d.ts +17 -23
- package/dist/src/core/license-headers.js +17 -23
- package/dist/src/core/mach-build-artifacts.d.ts +9 -9
- package/dist/src/core/mach-build-artifacts.js +57 -19
- package/dist/src/core/mach-error-hints.js +42 -29
- package/dist/src/core/mach-known-noise-filter.d.ts +15 -16
- package/dist/src/core/mach-known-noise-filter.js +18 -20
- package/dist/src/core/mach-mozconfig.d.ts +14 -0
- package/dist/src/core/mach-mozconfig.js +30 -0
- package/dist/src/core/mach-resource-shim.d.ts +43 -58
- package/dist/src/core/mach-resource-shim.js +50 -67
- package/dist/src/core/mach.d.ts +37 -41
- package/dist/src/core/mach.js +50 -51
- package/dist/src/core/marionette-port.d.ts +36 -1
- package/dist/src/core/marionette-port.js +96 -31
- package/dist/src/core/marionette-preflight.d.ts +8 -12
- package/dist/src/core/marionette-preflight.js +86 -52
- package/dist/src/core/mochitest-server-port.d.ts +42 -0
- package/dist/src/core/mochitest-server-port.js +119 -0
- package/dist/src/core/module-resolution-preflight.d.ts +9 -11
- package/dist/src/core/module-resolution-preflight.js +9 -11
- package/dist/src/core/moz-manifest-helpers.d.ts +79 -0
- package/dist/src/core/moz-manifest-helpers.js +163 -0
- package/dist/src/core/{manifest-register.d.ts → moz-manifest-register.d.ts} +1 -1
- package/dist/src/core/{manifest-register.js → moz-manifest-register.js} +13 -28
- package/dist/src/core/{manifest-rules.d.ts → moz-manifest-rules.d.ts} +10 -1
- package/dist/src/core/{manifest-rules.js → moz-manifest-rules.js} +31 -24
- package/dist/src/core/{manifest-tokenizers.d.ts → moz-manifest-tokenizers.d.ts} +3 -5
- package/dist/src/core/{manifest-tokenizers.js → moz-manifest-tokenizers.js} +9 -12
- package/dist/src/core/ownership-table.d.ts +14 -16
- package/dist/src/core/ownership-table.js +14 -16
- package/dist/src/core/parser-fallback.d.ts +15 -1
- package/dist/src/core/parser-fallback.js +22 -3
- package/dist/src/core/patch-apply-fuzz.d.ts +10 -12
- package/dist/src/core/patch-apply-fuzz.js +15 -18
- package/dist/src/core/patch-apply.d.ts +13 -34
- package/dist/src/core/patch-apply.js +16 -76
- package/dist/src/core/patch-artifact-normalize.d.ts +13 -15
- package/dist/src/core/patch-artifact-normalize.js +13 -15
- package/dist/src/core/patch-export-metadata.d.ts +12 -2
- package/dist/src/core/patch-export-metadata.js +13 -4
- package/dist/src/core/patch-export-update.d.ts +13 -1
- package/dist/src/core/patch-export-update.js +25 -8
- package/dist/src/core/patch-export.d.ts +15 -8
- package/dist/src/core/patch-export.js +10 -11
- package/dist/src/core/patch-files.d.ts +5 -6
- package/dist/src/core/patch-files.js +5 -6
- package/dist/src/core/patch-identifier-suggest.d.ts +7 -12
- package/dist/src/core/patch-identifier-suggest.js +14 -19
- package/dist/src/core/patch-lint-binary.d.ts +51 -0
- package/dist/src/core/patch-lint-binary.js +39 -0
- package/dist/src/core/{lint-cache.d.ts → patch-lint-cache.d.ts} +5 -5
- package/dist/src/core/{lint-cache.js → patch-lint-cache.js} +21 -23
- package/dist/src/core/patch-lint-checkjs.d.ts +16 -16
- package/dist/src/core/patch-lint-checkjs.js +16 -16
- package/dist/src/core/patch-lint-chrome-jsdoc.d.ts +6 -9
- package/dist/src/core/patch-lint-chrome-jsdoc.js +10 -14
- package/dist/src/core/patch-lint-creators.d.ts +8 -8
- package/dist/src/core/patch-lint-creators.js +8 -8
- package/dist/src/core/patch-lint-cross.d.ts +17 -20
- package/dist/src/core/patch-lint-cross.js +16 -16
- package/dist/src/core/patch-lint-css.d.ts +3 -5
- package/dist/src/core/patch-lint-css.js +17 -22
- package/dist/src/core/patch-lint-diff-tag.d.ts +14 -16
- package/dist/src/core/patch-lint-diff-tag.js +14 -16
- package/dist/src/core/patch-lint-jsdoc.d.ts +7 -8
- package/dist/src/core/patch-lint-jsdoc.js +26 -19
- package/dist/src/core/patch-lint-mozbuild.js +20 -20
- package/dist/src/core/patch-lint-observer.d.ts +7 -7
- package/dist/src/core/patch-lint-observer.js +7 -7
- package/dist/src/core/patch-lint.d.ts +38 -29
- package/dist/src/core/patch-lint.js +89 -121
- package/dist/src/core/patch-lock.d.ts +1 -1
- package/dist/src/core/patch-lock.js +45 -9
- package/dist/src/core/patch-manifest-consistency.d.ts +2 -1
- package/dist/src/core/patch-manifest-consistency.js +25 -27
- package/dist/src/core/patch-manifest-io.js +14 -18
- package/dist/src/core/patch-manifest-resolve.d.ts +9 -12
- package/dist/src/core/patch-manifest-resolve.js +9 -12
- package/dist/src/core/patch-manifest-validate.d.ts +6 -1
- package/dist/src/core/patch-manifest-validate.js +25 -13
- package/dist/src/core/patch-parse.d.ts +24 -13
- package/dist/src/core/patch-parse.js +19 -14
- package/dist/src/core/patch-policy.d.ts +2 -2
- package/dist/src/core/patch-policy.js +2 -2
- package/dist/src/core/patch-registration-refs.d.ts +7 -8
- package/dist/src/core/patch-registration-refs.js +9 -9
- package/dist/src/core/process-cpu-time.d.ts +12 -0
- package/dist/src/core/process-cpu-time.js +52 -0
- package/dist/src/core/rebase-session.d.ts +38 -26
- package/dist/src/core/rebase-session.js +55 -28
- package/dist/src/core/register-browser-content.js +9 -74
- package/dist/src/core/{register-test-manifest.js → register-browser-test.js} +7 -42
- package/dist/src/core/register-module.js +10 -43
- package/dist/src/core/register-scaffold.d.ts +8 -8
- package/dist/src/core/register-scaffold.js +10 -10
- package/dist/src/core/register-shared-css.d.ts +9 -12
- package/dist/src/core/register-shared-css.js +17 -87
- package/dist/src/core/register-xpcshell-test.d.ts +6 -7
- package/dist/src/core/register-xpcshell-test.js +7 -8
- package/dist/src/core/shared-ftl.js +12 -4
- package/dist/src/core/state-file.d.ts +45 -0
- package/dist/src/core/state-file.js +34 -0
- package/dist/src/core/status-binary.d.ts +18 -11
- package/dist/src/core/status-binary.js +19 -12
- package/dist/src/core/status-classify.d.ts +30 -36
- package/dist/src/core/status-classify.js +21 -26
- package/dist/src/core/test-changed-prefs.d.ts +30 -0
- package/dist/src/core/test-changed-prefs.js +78 -0
- package/dist/src/core/test-harness-crash.d.ts +25 -63
- package/dist/src/core/test-harness-crash.js +121 -142
- package/dist/src/core/test-harness-signature.d.ts +18 -0
- package/dist/src/core/test-harness-signature.js +13 -0
- package/dist/src/core/test-path-scope.d.ts +12 -15
- package/dist/src/core/test-path-scope.js +15 -18
- package/dist/src/core/test-stale-check.d.ts +17 -17
- package/dist/src/core/test-stale-check.js +50 -43
- package/dist/src/core/test-stall-triage.d.ts +51 -0
- package/dist/src/core/test-stall-triage.js +126 -0
- package/dist/src/core/token-category.js +5 -7
- package/dist/src/core/token-coverage.js +9 -9
- package/dist/src/core/token-dark-mode.d.ts +9 -11
- package/dist/src/core/token-dark-mode.js +9 -11
- package/dist/src/core/token-docs.d.ts +2 -0
- package/dist/src/core/token-docs.js +2 -0
- package/dist/src/core/token-manager.d.ts +10 -2
- package/dist/src/core/token-manager.js +17 -8
- package/dist/src/core/token-scaffold.d.ts +7 -7
- package/dist/src/core/token-scaffold.js +7 -7
- package/dist/src/core/token-variant.d.ts +4 -4
- package/dist/src/core/token-variant.js +4 -4
- package/dist/src/core/toolchain-preflight.d.ts +20 -21
- package/dist/src/core/toolchain-preflight.js +28 -31
- package/dist/src/core/tree-guard.js +12 -2
- package/dist/src/core/tree-store.d.ts +41 -27
- package/dist/src/core/tree-store.js +63 -42
- package/dist/src/core/typecheck-shim.d.ts +23 -24
- package/dist/src/core/typecheck-shim.js +23 -24
- package/dist/src/core/typecheck.js +8 -8
- package/dist/src/core/wire-destroy.d.ts +3 -3
- package/dist/src/core/wire-destroy.js +7 -15
- package/dist/src/core/wire-dom-fragment.d.ts +9 -17
- package/dist/src/core/wire-dom-fragment.js +11 -44
- package/dist/src/core/wire-init.d.ts +2 -3
- package/dist/src/core/wire-init.js +8 -19
- package/dist/src/core/wire-subscript.d.ts +2 -3
- package/dist/src/core/wire-subscript.js +4 -13
- package/dist/src/core/wire-utils.d.ts +7 -8
- package/dist/src/core/wire-utils.js +7 -8
- package/dist/src/core/xpcshell-appdir.d.ts +38 -45
- package/dist/src/core/xpcshell-appdir.js +38 -45
- package/dist/src/errors/base.d.ts +37 -16
- package/dist/src/errors/base.js +37 -16
- package/dist/src/errors/build.d.ts +13 -1
- package/dist/src/errors/build.js +14 -2
- package/dist/src/errors/codes.d.ts +16 -0
- package/dist/src/errors/codes.js +16 -0
- package/dist/src/errors/furnace.d.ts +4 -4
- package/dist/src/errors/git.d.ts +6 -10
- package/dist/src/errors/git.js +6 -10
- package/dist/src/errors/patch.d.ts +7 -7
- package/dist/src/errors/patch.js +7 -7
- package/dist/src/errors/rebase.d.ts +4 -4
- package/dist/src/errors/rebase.js +4 -4
- package/dist/src/index.d.ts +7 -1
- package/dist/src/index.js +5 -0
- package/dist/src/types/commands/index.d.ts +2 -2
- package/dist/src/types/commands/options.d.ts +88 -70
- package/dist/src/types/commands/patches.d.ts +24 -34
- package/dist/src/types/commands/project.d.ts +5 -34
- package/dist/src/types/config.d.ts +25 -23
- package/dist/src/types/config.js +6 -1
- package/dist/src/types/furnace.d.ts +37 -13
- package/dist/src/types/furnace.js +19 -1
- package/dist/src/types/index.d.ts +3 -3
- package/dist/src/utils/build-info.d.ts +7 -8
- package/dist/src/utils/build-info.js +12 -11
- package/dist/src/utils/errors.d.ts +6 -8
- package/dist/src/utils/errors.js +6 -8
- package/dist/src/utils/logger.d.ts +27 -10
- package/dist/src/utils/logger.js +28 -16
- package/dist/src/utils/machine-output.d.ts +12 -0
- package/dist/src/utils/machine-output.js +33 -0
- package/dist/src/utils/options.d.ts +31 -14
- package/dist/src/utils/options.js +86 -30
- package/dist/src/utils/paths.d.ts +10 -8
- package/dist/src/utils/paths.js +10 -8
- package/dist/src/utils/platform.d.ts +0 -25
- package/dist/src/utils/platform.js +1 -42
- package/dist/src/utils/process-group.d.ts +9 -4
- package/dist/src/utils/process-group.js +19 -5
- package/dist/src/utils/process.d.ts +42 -7
- package/dist/src/utils/process.js +44 -17
- package/dist/src/utils/ps-duration.d.ts +31 -0
- package/dist/src/utils/ps-duration.js +70 -0
- package/dist/src/utils/regex.d.ts +9 -10
- package/dist/src/utils/regex.js +9 -10
- package/dist/src/utils/validation.d.ts +58 -28
- package/dist/src/utils/validation.js +60 -33
- package/package.json +2 -2
- package/dist/src/commands/doctor/post-rebase-audit.d.ts +0 -2
- package/dist/src/commands/doctor-furnace-manifest-sync.d.ts +0 -18
- package/dist/src/core/manifest-helpers.d.ts +0 -38
- package/dist/src/core/manifest-helpers.js +0 -120
- /package/dist/src/core/{lint-projection.d.ts → patch-lint-projection.d.ts} +0 -0
- /package/dist/src/core/{lint-projection.js → patch-lint-projection.js} +0 -0
- /package/dist/src/core/{register-test-manifest.d.ts → register-browser-test.d.ts} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,131 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.44.0
|
|
4
|
+
|
|
5
|
+
### Gates
|
|
6
|
+
|
|
7
|
+
- **`release:check` ran twice per release**, because `npm publish` re-triggers it through `prepublishOnly`. The explicit step is gone; `prepublishOnly` is the single gate. The redundant third `npm pack` (`pack:dry-run`, already run by `pack:verify`) was dropped from the gate.
|
|
8
|
+
- Branch-protection contexts are stable: the smoke job is `smoke (${{ matrix.os }})` rather than embedding the Node version — the same trap already fixed for `quality (ubuntu)` — and `setup-rulesets.sh` reconciles (GET by name, then PUT or POST) instead of POSTing unconditionally, which is why the live ruleset had drifted to requiring nothing.
|
|
9
|
+
|
|
10
|
+
### Multi-session contention
|
|
11
|
+
|
|
12
|
+
A downstream fork runs this checkout with up to five concurrent agent sessions, where a held lock is the normal state rather than an exceptional one. Four findings from that workload:
|
|
13
|
+
|
|
14
|
+
- **A `--wait-lock` budget expired one position from the head of the queue** — the worst possible outcome, because it pays the entire wait and gets nothing. `--wait-lock 300` died still-queued where `3600` was needed. The timeout is now a budget against a STALLED queue rather than against wall-clock: each time the observed queue position improves, the wait is granted a fresh budget, up to four times the requested one (ceiling one hour, matching the flag's own bound). A queue that stops moving still starves on exactly the budget you asked for, which is what the timeout is for. The refusal also names the position reached — "you were still 2 from the head of a queue of 3" — so raising the budget is an informed decision instead of a guess. Only `--wait-lock` runs extend; the no-flag ~1 s fail-fast path is untouched.
|
|
15
|
+
- **A wait budget could not be expressed once for a session.** `FIREFORGE_WAIT_LOCK=<seconds>` now supplies the budget for every lock-taking command that was not given an explicit `--wait-lock` (which always wins). An unattended loop threading the flag through every invocation was the shape most likely to be forgotten, and the omission only surfaced as a tail of refusals after the loop had already run. An unusable value is refused rather than silently falling back to fail-fast. Note that `test`, `export` and `re-export` never differed here: all three fail fast at ~1 s without the flag.
|
|
16
|
+
- **A killed run wrote no terminal line**, so a log tail could not distinguish "killed" from "still running" from "never started" — recovery meant checking `ps` and the lock file by hand, at exactly the moment (a mass kill under a usage limit, mid-drain) when the tree's state most needs to be readable. SIGTERM/SIGINT now emit `FIREFORGE-VERDICT: FAIL reason=killed signal=…` before the shutdown drain, so the line survives a drain that times out. It is a no-op unless a test run was actually in flight: a Ctrl+C during `status` must not claim a test run happened.
|
|
17
|
+
- **The signal sweep released only furnace locks**, so a SIGTERM'd `fireforge test` left `.fireforge/engine-session.lock` behind — reclaimed immediately by the next contender's PID-first staleness check, but reported as held-by-a-dead-process in between. `file-lock.ts` now tracks the locks this process holds and the signal path sweeps all of them through the existing ownership-checked `releaseLockIfOwned`; one mechanism rather than a fourth sweeper. The furnace sweep stays and runs after it, because it carries rollback bookkeeping the generic one does not.
|
|
18
|
+
|
|
19
|
+
- **FireForge's own binary-patch bookkeeping broke the one-writer invariant it enforces.** `generateBinaryFilePatch` has to write an index entry to diff an untracked binary, and it wrote it to the SHARED index — visible, for the width of a stage/diff/restore, to anything running `git status`. That is precisely what `fireforge test` does to fingerprint `engine/`, so a parallel gate lane exporting the branding PNG/icns/bmp set killed healthy suites with `FAIL reason=inconclusive` on branding paths flapping between `A` and `??`. **This is the cause the 2026-08-21 report could not find**: it looked at `engine/.git/index`, which the detector cannot see, when the flap was in the porcelain TEXT.
|
|
20
|
+
`binaryStagingLock` never covered it — it is a module-level promise chain, so it serializes within one process while `--parallel-verify` lanes are separate processes. The staging now writes a per-call PRIVATE index (`mintDisposableGitIndex`) and throws it away, so the primary `.git/index` never moves, nothing is restored because nothing shared changed, and two concurrent callers get two indexes rather than contending. The 0.43.0 private-index work closed this for read-only commands; this path is the deliberate writer it did not cover, and it could not have been fixed by leaning on that scope — `export` and `re-export` install none, and `withPrivateGitIndex` is fail-open by design, which on a write path means silently mutating the primary checkout.
|
|
21
|
+
The seed is load-bearing rather than tidy: a tracked, unmodified binary reaches the same branch (its `diff HEAD` is empty), and against an empty index it would report as a fresh add. The shared-index path survives as a fallback for when no private index can be minted — refusing there would fail an export over an optimisation. A real-git test polls `git status` throughout the call and asserts the transient entry is never observable; it fails on the old implementation, which is what makes it worth having.
|
|
22
|
+
|
|
23
|
+
### Exit codes
|
|
24
|
+
|
|
25
|
+
- **Red, never-ran and thrown-away were three facts sharing two exit codes.** `reason=inconclusive` — a suite whose verdict was DISCARDED because `engine/` moved under it — exited **1** beside real test failures' **5**, and a summary line reading `FAIL — exit 1` next to `FAIL — exit 5` invites treating opposites as the same kind of fact: one suite failed, the other's healthy result was thrown away. It now exits **14** (`InconclusiveVerdictError`). `LockContentionError` moves from 1 to **15**: a lock timeout means the run never started, so a script should re-queue it rather than report a failure of the work it asked for. That re-codes patch-directory, build and furnace lock timeouts too — deliberately, since the fact is the same whichever lock was contended. See `docs/exit-codes.md`.
|
|
26
|
+
|
|
27
|
+
### Diagnostics
|
|
28
|
+
|
|
29
|
+
- **A stale mochitest httpd on the harness server port stalled every run, and nothing named the port.** The mochitest harness serves its manifest from an xpcshell `server.js` on `127.0.0.1:8888`. A survivor of an interrupted run keeps that port, and a fresh browser then connects to the ZOMBIE server, which cannot serve the new manifest — so the run stalls between browser startup and `TEST_START` and dies on the 370 s no-output timeout with `Ran 0 checks`. Nothing in that signature mentions a port, so the failure survived fresh builds and a sandbox change and cost one downstream fork a multi-hour bisect of files the bisect then exonerated. `--kill-stale-marionette` never covered it: it clears the browser and the Marionette control port, and the httpd is neither. A preflight now probes the port for browser-chrome dispatches — reusing `probeMarionettePort`, which was already port-generic, rather than growing a second probe — and refuses with the holder named. A recognized harness `server.js` can be terminated with `--kill-stale-marionette`; an unrecognized listener is refused but never killed, and recognition requires objdir/xpcshell provenance as well as the filename, because `server.js` is one of the most common filenames there is and a developer's own service must not be offered up. Best-effort throughout: an unprobeable port reports free, so a host without `lsof` runs exactly as before. The cause joins the no-output-stall census with `lsof -nP -iTCP:8888` as its probe — the one entry there whose mechanism is root-caused rather than correlated.
|
|
30
|
+
- **The stale-browser refusal named a PID the operator must not kill, and offered the flag that would kill it.** The objdir-bundle preflight matches any process running this project's binary, which on a shared checkout includes a peer session's LIVE browser — and it reported only the PID, while holding the command line and never surfacing it. One downstream session met exactly this: five identical preflight failures on a browser belonging to another session, with the message suggesting `--kill-stale-marionette`, which would have killed a multi-hour evaluation run. The refusal now reports the evidence — elapsed time (from `ps etime=`) and the full command line — and offers the kill flag ONLY for a marionette-driven browser. A bare launch is reported as not-a-test-browser with the flag deliberately withheld and the reason stated, because FireForge cannot tell a dead run's orphan from someone's live window. Making the preflight self-clearing was considered and rejected for the same reason. The elapsed field is parsed by the same `parsePsDuration` the orphaned-harness doctor check uses for `time=`; the two columns share one grammar and a second copy is how they would drift. A listing without the column still parses, with the age reported as unknown and the field folded back into the command line — a probe must degrade, not drop the process.
|
|
31
|
+
- **`--wait-lock`'s progress line kept quoting a budget that was no longer in force**, so a wait that had been legitimately extended was indistinguishable from a timeout that does not fire — which is how a downstream report read it. The line now reports the budget currently in force, and an extension announces itself, naming both figures: the one you asked for and the one now in force, plus the position that bought it. A wait that silently outlives its own stated timeout looks broken whether or not it is.
|
|
32
|
+
- **`status --lock` could not tell a working holder from a wedged one.** It reported holder liveness via `kill(pid, 0)`, which answers only "does the process exist" — so a queue stalled behind a command that had consumed 0.3 s of CPU in eleven minutes looked exactly like one behind a healthy long build, and the downstream working practice was to run `ps aux | grep fireforge` and read near-zero CPU by hand. That fact is now read and reported, with a long hold at near-zero CPU flagged as the shape of a wedged command. Stated as an observation with its own caveat, never as a verdict: a holder blocked on I/O looks identical, and nothing keys a refusal, a kill or a lock steal off the number. An unmeasurable value is omitted rather than rendered as zero, which would invert the diagnosis it exists for.
|
|
33
|
+
- **The 2026-08-27 report's stated cause was not reproducible, and is recorded rather than "fixed".** It attributed a hung `furnace deploy` holding the engine lock to an interactive prompt with no TTY to answer it. `furnace deploy` contains no prompt: its "overwriting deployed … its engine content differs" text is an advisory warning pushed onto `result.warnings`, in a module whose contract is that the probe must never block an apply. Nor is there an unguarded prompt anywhere else — `confirmDestructive` refuses a non-TTY run rather than reading stdin, and every raw prompt site gates on `stdioIsInteractive()` first. The two real defects behind the observed symptom are the waiter fixes above. Naming what the code actually does is how the next occurrence gets attributed to something else quickly, instead of inheriting a guess as a fact.
|
|
34
|
+
- **`--expect`'s help text read as per-patch on a command that takes many patches.** It is a whitelist of files across the whole invocation — a patch can only absorb drift in files it owns anyway — but the wording sent a consumer looping 18 single-patch re-exports at ~33 s each, past a tool timeout, to do what one call does. The text now says the whitelist spans every patch in the invocation.
|
|
35
|
+
|
|
36
|
+
- **The inconclusive refusal said only THAT `engine/` changed**, leaving the operator to reconstruct the writer by hand — `find -newermt` against the run window, minus the objdir, minus git's own bookkeeping. Both generation tokens are in hand at the refusal site, so it now names what moved: a HEAD move by both SHAs, and working-tree entries that appeared or went away, capped at five with a `(+N more)` tail. The `-z` rename shape is handled explicitly (an `R`/`C` record carries a second path field; consuming one desynchronises the parse and turns a single rename into a report that the whole tree moved). Degrades to the old message on an unparseable token — a diagnostic must never turn a refusal into a crash. Worth recording that the reported cause was NOT reproducible in this code: the detector is `git rev-parse HEAD` plus `git status --porcelain=v1 -z`, which never sees `.git/**` and honours `.gitignore`, so `engine/.git/index` moving cannot produce that refusal. Naming the delta is how the next occurrence answers the question itself.
|
|
37
|
+
- **A `moz.build` sort violation arrived as a raw Python traceback.** `mozbuild.util.UnsortedError` is now translated: the rule stated is the one operators get wrong, and it is the OPPOSITE of the intuitive reading — mozbuild lowercases before comparing (`StrictOrderingOnAppendList.ensure_sorted`), so uppercase does NOT sort ahead of lowercase, and `HominisEditorFilePaths.sys.mjs` belongs before `HominisEditorFiles.sys.mjs`. The hint also points at `fireforge lint --per-patch` (`mozbuild-unsorted-list`), which already catches this before a build and is the cheaper place to catch it.
|
|
38
|
+
- **`unexpected=2` on the `lastColdStartupCheck` / `globalprivacycontrol` pair was indistinguishable from two real assertion failures.** Those preferences are time-driven — they move once a run is long enough to cross the cold-startup check interval — so they correlate with run LENGTH, not with the build or with the change under test; the same work at `--chunk 8` passes where a 40-row chunk is red. FireForge cannot forgive the check (it is the harness's) but a run whose EVERY unexpected result is that pair now says so, in the failure message and as a verdict-line note, and names halving the chunk as the first probe. A single real failure beside the noise disqualifies the whole set — the point is to describe a run, never to explain away a failure. Keyed on the pref names rather than on the harness's phrasing, which has no contract with us.
|
|
39
|
+
- **The packaging-coverage refusal never mentioned `--extend-coverage`.** The cumulative claim shipped in 0.43.0, but under concurrent sessions the operator meets it at the moment a peer's scoped build has replaced coverage they still hold — and the refusal pointed only at a full rebuild. It now offers the union, and states the anchor conditions (unchanged engine HEAD and `engine/mozconfig`) so the operator knows when it will refuse. Coverage still REPLACES by default, for the reason documented on `BuildBaseline.testPackagingCoverage`: a blind union whitewashes an earlier scope's edited fixtures while `obj-*/_tests/` still holds that scope's stale staging.
|
|
40
|
+
- **The no-output-stall census asserted a mechanism it had no evidence for, and its third cause carried no discriminating step.** The `chrome://`-image entry shipped in 0.43.0 stating that a broken image URI on the startup page "stalls first paint"; that mechanism was never established here — it was written on top of a single downstream report which explicitly did not root-cause it, and it is doubtful on its own terms, since a failed image load in Gecko fires `error` without gating the document load event. The entry now states the correlation it actually rests on (one consumer, reproduced and un-reproduced on one variable) and says the mechanism is open, so nobody inherits a guess as a fact. Separately, its remedy — run a known-good control — is the correct opening move for **all three** causes, so attaching it to one entry made reaching that entry informationally empty; the control step is hoisted above the list, and cause 3 gains a probe that actually separates it from the other two (`fireforge run --smoke-exit … --capture-console`, which starts the same browser outside the test harness). No detector is claimed: no log line is known to key on, and `mach-error-hints.ts` sets the bar at an established diagnosis rather than a plausible one. The census moved to `src/core/test-stall-triage.ts` — it is operator advice, not classification, and the rules for editing it are not the rules for editing a classifier.
|
|
41
|
+
|
|
42
|
+
### Build output resolution
|
|
43
|
+
|
|
44
|
+
- **A second `obj-*` directory made FireForge refuse every command, even when the mozconfig said which one to use.** The refusal is correct where the choice is genuinely ambiguous — building a dev test against a release objdir is exactly the silent corruption it prevents — but `MOZ_OBJDIR` was never read, so an operator whose active configuration had already answered the question was sent to rename a directory to satisfy a scan. An ambiguous glob now consults `engine/mozconfig` (`mk_add_options MOZ_OBJDIR=…` or a bare `export`, `@TOPSRCDIR@` and relative forms resolved to a path segment) and proceeds when the declaration resolves to exactly one candidate — into the existing `mozinfo.json` verification, which is the real safety net. Everything else still refuses, and a declaration that named something the scan cannot see is reported, because that is itself the diagnosis.
|
|
45
|
+
|
|
46
|
+
### Correctness
|
|
47
|
+
|
|
48
|
+
- **A test's own passing diagnostic could turn a green run red, and the verdict then blamed the diagnostic.** The failure-line classifier matched `/\bFAIL\b/` and a case-INSENSITIVE `/\b(?:Assertion failure|MOZ_ASSERT|ASSERTION)\b/` against raw output, so any line containing the ordinary English word "assertion" counted as a real failure. `realFailures.length === 0` is a required rung in both `hasGreenShapedSummary` and the known-teardown-noise pass, so one such line made that pass unreachable: a substantively green suite (`Ran 11572 checks … Unexpected results: 0 … OK`) whose `mach` exit was pushed to 1 by the recognized upstream mozsystemmonitor teardown reported `FAIL reason=test-failures checks=11572 unexpected=0` — and named the diagnostic as `First real test failure:`, pointing the reader at a line that was a symptom of the misclassification rather than its cause. A downstream fork lost hours to it, and the trap is silent and inverted: a note whose whole purpose is to explain a red run can, by its wording alone, manufacture one.
|
|
49
|
+
The bare-word arm hid a second defect nobody had noticed: `\bFAIL\b` matches `TEST-KNOWN-FAIL`, an EXPECTED failure, so a suite carrying a known-fail annotation was counted as carrying a real one. Both arms are now harness-shaped — `TEST-(?!KNOWN-FAIL)[A-Z-]*FAIL` and the three concrete Gecko shapes (`Assertion failure:`, `MOZ_ASSERT(`, `###!!! ASSERTION`), case-SENSITIVE. A belt covers what tightening cannot reach: when the summary says `Unexpected results: 0` and no matched evidence carries a `TEST-UNEXPECTED-*` marker, the verdict line says so, because `unexpected=0` printed beside `reason=test-failures` is precisely the tell that the classification rests on a word match. Deliberately a note and not a reclassification — a non-zero exit with no green-shaped summary is still a failing run, and inventing a `reason` would break the contract its consumers parse.
|
|
50
|
+
- **`fireforge test --build` could not arm `--refuse-unexported-drift`.** The flag existed only on `build`, so the one shape the multi-session hazard actually shows up in — a scripted `test --build` on a checkout with concurrent sessions — had no way to turn the warning into a refusal, which is what a downstream fork reached for immediately after a near-miss. `test` now accepts it and forwards it to the pre-test build. It is refused without `--build`/`--build-only` rather than accepted over a build that will not run; the release has enough silent no-op flags in it already.
|
|
51
|
+
- **A `--wait-lock` waiter outlived the budget it was given, even against a queue that never moved.** The 0.44.0 extension renews the budget on a strict improvement in queue position — but `bestAhead` started `undefined`, so the FIRST observation always satisfied that test and handed out a free full grant. `--wait-lock 300` therefore waited past 300 s on a completely stalled queue, which is what a downstream report described as "it does not reliably give up after N seconds". The first probe now SEEDS the best-seen position without extending, so only a genuine advance buys time and a stalled queue starves on exactly the budget asked for. A test asserts no extension is granted on a stalled queue; it fails on the old implementation.
|
|
52
|
+
|
|
53
|
+
- **`re-export` rewrote a binary patch into an un-appliable stub, and every gate stayed green.** `export` has always been binary-aware — it routes a binary file to `generateBinaryFilePatch` and emits a real `GIT binary patch` delta. The batched diff path `re-export` uses did the same for UNTRACKED binaries only; a binary already tracked in HEAD fell through to a plain `git diff HEAD`, which degrades to the informational `Binary files a/x and b/x differ`. That form records THAT the bytes changed and carries none of them, so a bulk `re-export --all` replaced a downstream fork's two MAR signing-certificate deltas with bodies that cannot recreate the files — on patches the run had no reason to touch.
|
|
54
|
+
What makes it dangerous rather than lossy is that the stub still carries a CORRECT `index <old>..<new>` line. Every identity-shaped check keys on that hash: worktree classification hashed the live file against it, matched, and reported `patch-backed`; foreign-drift detection compared old and new hashes, saw no change, and reported no drift; `verify` never read the body at all. So the queue held a patch that provably could not rebuild its own file while `verify`, `status --check` and `--fail-on binary-unsupported` all passed, and the loss would surface only at the next clean-room apply. `--binary` now covers `getFileDiff`, `getAllDiff`, `buildTrackedSections` and `getStagedDiffForFiles` — it expands only the binary section's index line to full hashes, so text patches are byte-for-byte unaffected, which is what made the fix safe to apply across every caller at once (`re-export`, `export --all`, `lint`, `resolve`, `rebase --continue`).
|
|
55
|
+
Two independent guards close it from the other side, because the export fix cannot help a body that is already degraded or was hand-written. `parseDiffSections` now distinguishes a payload-carrying `GIT binary patch` from the stub (`hasBinaryDelta`), a new queue lint (`binary-body-not-reconstructable`) fails closed on any non-deletion binary section without a payload — reaching `verify`, `lint` and re-export's projected-queue gate, so a run that would introduce one is refused before anything is written — and binary classification no longer trusts a stub's index hash, so such a file reports `binary-unsupported` instead of `patch-backed`. A real-git test asserts the delta round-trips and that `git apply` replays it; it fails on the old implementation.
|
|
56
|
+
- **`re-export` reported "Re-exported" for patches whose body never moved.** A bulk run refreshes every row in the manifest and wrote each one unconditionally, so the output gave no signal about which patches actually changed and `git status` was the only place an unexpected rewrite showed up — the review step that caught the binary loss above. A body byte-identical to what is on disk is no longer written (its mtime is left alone) and is reported as `Unchanged`. Metadata still moves either way, and the patch still counts as refreshed, so `--stamp` completeness is unaffected.
|
|
57
|
+
- **`fireforge test --doctor` orphaned a whole browser tree on Ctrl+C.** `marionette-preflight.ts` holds the repo's one legitimate raw `spawn` — it needs the live `ChildProcess` mid-run, returns while the browser is still running, and aborts on a TCP byte rather than a deadline, none of which the exec-layer wrappers can express — but it had skipped the shutdown contract every wrapper applies to a detached child. It now registers closure tracking, forwards the parent's SIGINT/SIGTERM to the group, and sweeps for survivors. `execStream` was the one wrapper missing `trackChildClosure` and gained it, so `waitForActiveChildShutdown` finally waits for `runMachStream`'s children too.
|
|
58
|
+
- **Every unexpected failure during a patch-queue mutation was reported as a patch-compatibility problem.** One blanket catch in `withPatchDirectoryLock` funnelled 15 call sites: an `EACCES` writing `patches.json`, a rename postcondition failure, or a plain `TypeError` from a FireForge bug all reached the operator as "Patch Error" followed by three fixed remedies about Firefox versions and `fireforge reset`. Worse, `cli.ts` prints a stack only for non-`FireForgeError` throwables, so the rewrap moved real bugs into the branch that discards their stack. The catch now narrows by ORIGIN — the body's own failure passes through with its class, stack and exit code intact — and a genuine lock failure carries its `cause`. `CommandError`, the one class in `src/errors/` that is not a `FireForgeError`, is no longer swallowed.
|
|
59
|
+
- **A declined confirmation and an interrupted one were indistinguishable.** Both returned `'cancelled'` and exited 0, and `CancellationError.code` was `GENERAL_ERROR` — a contradiction with `ExitCode.USER_CANCELLED`'s own documentation that survived because the CLI intercepts the class by name before reading `.code`. Esc/Ctrl+C now raises `CancellationError` and exits **130**; answering "no" returns normally and exits **0**, because the run did what it was asked. See `docs/exit-codes.md`.
|
|
60
|
+
- **The mach PYTHONPATH guard wrote to a fixed, shared temp path.** `$TMPDIR/fireforge-mach-resource-shim` — fixed name, default mode, no ownership check, never removed — was prepended to `PYTHONPATH` for every protected build and test dispatch. Inert on macOS, where `tmpdir()` is the per-user `0700` `$TMPDIR`; on Linux, where `/tmp` is shared, another local account can own `sitecustomize.py` and have it execute inside the victim's build. It is now minted per dispatch with `mkdtemp` and removed in a `finally` that covers retries.
|
|
61
|
+
- **`furnace validate` never checked that the engine existed.** It was the one command in the family that checked `furnace.json` alone, so validating against a missing engine produced downstream noise instead of the family's refusal.
|
|
62
|
+
- **`furnace create` could scaffold a syntactically broken `.mjs`.** A `"` or newline in the FTL path, or a `*/` in the description, went unescaped into the generated module. Both are now escaped at the sink — which also covers the `ftlBasePath` and derived-name paths that never passed through the flag validator.
|
|
63
|
+
- The signal-time furnace lock sweeper removed lock directories without checking ownership, so it could delete a lock another process had acquired in the window between our operation dying and the sweep running. It now uses `releaseLockIfOwned`.
|
|
64
|
+
- `killProcessTree`'s POSIX branch swallowed `ESRCH`/`EPERM` from the group kill without falling through to a direct `child.kill`, so a failed group kill left the child running.
|
|
65
|
+
|
|
66
|
+
### Performance
|
|
67
|
+
|
|
68
|
+
- **`getAllDiff` spawned one `git hash-object` per untracked file, uncapped.** Its sibling `getDiffForFilesAgainstHead` was reshaped into classify → one batched hash → render for exactly this reason; this path kept the per-file form, and it is the only path behind `tree create`, `tree list`, `tree remove --all`, `lint` and `export-all` — against a Firefox tree where "untracked" can mean a very large number of files. Now batched. Everything observable is deliberately unchanged (renames, `ls-files` ordering, tracked-block-first emission, the `'\n'` empty sentinel) because this output is SHA-256'd into every tree fingerprint and written verbatim into `.patch` files; a new real-git test pins those bytes, which nothing did before.
|
|
69
|
+
|
|
70
|
+
### Drift collapsed
|
|
71
|
+
|
|
72
|
+
- The engine-precondition refusal was written out 15 times across 13 files, and `engine-precondition.ts` exists _specifically_ because five commands had drifted copies of it. Four two-rung sites now use `assertEngineGitReady` (gaining the unborn-HEAD rung they were missing); the rest use a new one-rung `assertEngineExists`.
|
|
73
|
+
- The Furnace family repeated its own two-rung ladder at nine sites in three different subsets. `assertFurnaceReady` / `assertFurnaceEngineReady` replace them, with an injectable error factory because `FurnaceError`'s exit code 9 is load-bearing. Three en-GB "initialise" messages and one em-dash variant were aligned.
|
|
74
|
+
- The same 15-line rollback catch appeared verbatim in eight furnace command bodies, two of which cross-referenced the duplication in a comment ("Matches the seven sibling mutation sites"). `completeJournalRollback` owns the sequence now — shaped as a catch-body handler rather than a wrapper, because the eight bodies differ in early returns and converting them to callbacks would restructure control flow through the engine's rollback path for a cosmetic gain.
|
|
75
|
+
- Five tokenizer-primary register/wire modules kept a hand-written legacy scanner whose only trigger was the primary's own "could not find X". Deleted (~230 lines). Three tests that fed bare list bodies with no header had been passing only because the lax legacy scanner tolerated them; they now use realistic manifests. The three acorn-primary fallbacks in `wire-init`/`wire-destroy`/`wire-subscript` are load-bearing and stay.
|
|
76
|
+
- `bootstrap.ts` carried a second copy of `detectBootstrapIssues`'s six regexes, and the copies disagreed on the traceback+403 case. It calls the canonical scanner now.
|
|
77
|
+
- Two content-equality policies were split across five comparison sites, so on a CRLF checkout `furnace status` and `furnace validate` could report drift for files `apply` had just decided were identical. `normalizeForChecksum` moved to `furnace-checksum-utils.ts` and all five use it.
|
|
78
|
+
- The four manifest registrars each carried their own copy of the insert sequence. The two jar.mn ones (browser content, shared CSS) were byte-identical apart from a sort regex, a sort key and a refusal message; the two moz.build ones likewise. They route through `insertJarMnEntry` and `insertMozBuildListEntry` now — deliberately two family-specific helpers rather than one generic pair, because `after` and the section concept are meaningless on the moz.build side, and because `tokenizeMozBuildList` **mutates the line array in place** when it normalizes `FOO += []` into the multi-line form. That rewrite is what makes the caller's splice land inside the list body, so a wrapper tokenizing a copy would have silently reintroduced a fixed bug.
|
|
79
|
+
- `registerToolkitWidget` was the one jar.mn target still hand-scanning lines with its own section-bounds and sort-key regexes, so it never inherited fixes made to the shared tokenizer path. It uses the same helper as its three siblings now. The dead `findAlphabeticalPosition` (its last caller) went with it, and `findAlphabeticalTokenPosition` became module-private.
|
|
80
|
+
- `WIDGETS_DIR` moved out of `furnace-scanner.ts`; the seven-line `withParserFallback` rationale moved onto the shared predicate it explains (one of its three copies had already drifted); 18 open-coded `process.stdin.isTTY && process.stdout.isTTY` checks became `stdioIsInteractive()`.
|
|
81
|
+
|
|
82
|
+
### Structure
|
|
83
|
+
|
|
84
|
+
- `src/commands/doctor/` held a single file while six `doctor-*` siblings sat flat; it moved up to `doctor-post-rebase-audit.ts`, turning its lone `../doctor-check-core.js` reach-back into `./`.
|
|
85
|
+
- `furnace-validate-helpers.ts` was a 572-line spill file with three disjoint subjects and 18 of its 19 exports having exactly one consumer each. Each block moved to the validator that uses it; the module is now 20 lines holding the one export that genuinely had two consumers. **knip then revealed 16 of those exports as module-private** — they had only been exported to cross a boundary that no longer exists.
|
|
86
|
+
- Four ambiguous `manifest-*.ts` modules became `moz-manifest-*.ts` — "manifest" named both `patches.json` and Firefox build files — and `register-test-manifest.ts` became `register-browser-test.ts` so it reads as a `register-*` leaf. `lint-cache.ts` and `lint-projection.ts` joined the `patch-lint-*` family they belong to. The `register-*` family was deliberately left alone: `register-` names the `fireforge register` command those modules implement.
|
|
87
|
+
- The `Furnace manifest sync` doctor check does not look at a manifest — it compares component directories on disk against `furnace.json`. In a codebase where "manifest" already names `patches.json` and Firefox build files, that made the one referent visible to operators the most misleading one. Check, module and symbol are now `Furnace config sync`.
|
|
88
|
+
- Four modules reached for the same two drift predicates through two different paths — `furnace-apply-helpers.js` in two, the `furnace-apply.js` compat barrel in the others. They all use the defining module now, and the three names nothing else re-exported came off the barrel. The direction matters: `furnace-apply.ts` is 650 lines against a 500-line cap, and routing a consumer through it drags the whole apply graph into every suite that mocks it — which is exactly how the first attempt at this cleanup broke seven doctor tests, whose exhaustive `vi.mock` factory for the barrel listed one export. `mach.ts`'s re-export block now carries a comment saying it is a frozen compatibility list, not a barrel to grow.
|
|
89
|
+
- Dead exports deleted: `applyPatches`/`validatePatches` (superseded by `applyPatchesWithContinue`), six unused `platform.ts` helpers, `hasActiveRebaseSession` (every reference was a test using it as an assertion helper), `peekParserFallbackEvents`. knip could not see any of them, because tests count as consumers.
|
|
90
|
+
- Fifteen `await import()` calls became static imports. `patch-export.ts`'s `await import('./patch-manifest.js')` was the notable one: tracing the static graph showed no cycle, so the laziness bought nothing and hid a real edge from the `dpdm` gate — which still reports no cycles after the change.
|
|
91
|
+
|
|
92
|
+
### Types
|
|
93
|
+
|
|
94
|
+
- **`DoctorCheck` encoded one ternary result in three fields** — `passed` + optional `severity` + a legacy `warning` flag — with the precedence rules in a prose block and a reconciler function, which made states like `passed: true, severity: 'error'` representable. `severity` is now required and the only field; `ok()` gained an optional message, absorbing all five hand-written literals.
|
|
95
|
+
- **Three string unions accepted a stale allowlist with no compile error**, and `FirefoxProduct` carried three copies of its member list that nothing linked to the union — one of them on the `fireforge.json` read path, where adding a product would have been silently rejected from every project config. A shared `makeEnumGuard` derives each type from its list; ~12 non-narrowing casts went with it.
|
|
96
|
+
- **`src/index.ts` published 32 types unreachable from any exported value's signature**, including 15 `*Options` types for commands it does not export and two (`GlobalOptions`, `ProjectStatus`) with no reference anywhere in the repository. Removed. The surface gained what a consumer genuinely could not name: `ApplyAllComponentsResult` is what `applyAllComponents` actually returns.
|
|
97
|
+
- **The per-component apply family passed four interchangeable `string`s in three different orders.** `applyCustomComponent` opened `(engineDir, name, componentDir, …)`, `buildCustomDryRunActions` opened `(name, componentDir, engineDir, …)`, and the drift pair disagreed on slot 1 outright: `hasOverrideEngineDrift(engineDir, …)` against `hasCustomEngineDrift(root, …)`. Every permutation typechecked against every other, so a swap surfaced as a wrong-directory read at runtime. The eight helpers now take `Pick<ComponentApplyContext, …>` of exactly the members they read — a caller is never asked to invent a value it does not have. The two undeploy helpers keep their positional form: they take one path and one directory separated by two non-string parameters, which is not the confusable shape.
|
|
98
|
+
- `lintExportedPatch` took four optional positional parameters after `config`, and four of its five call sites passed `undefined` placeholders to reach the one they wanted (`lint.ts` passed two in a row). `patchQueueCtx`, `ignoreChecks` and `patchTier` moved into the existing `LintExportedPatchOptions`. This is ergonomics, not a defect fix — TypeScript's tuple-arity check already caught drift, and it caught every stale test assertion during the change.
|
|
99
|
+
- Six Commander handlers annotated their options with an inline anonymous type that duplicated a named interface exactly (`FurnaceCreateOptions`, `FurnaceChromeDocCreateOptions`, `FurnaceRefreshOptions`, `PatchReorderOptions`, `PatchStagedDependencyOptions`, `WireOptions`). They use the names now. `run.ts` uses `Omit<RunOptions, 'args'>` — `args` is the variadic positional, not a flag, so it never appears in that object. The handlers whose inline literal is genuinely `string` where the interface is a narrowed union keep theirs: narrowing is what those handlers exist to do.
|
|
100
|
+
- Twelve runtime flag checks raised `GeneralError` (exit 1) where 191 raise `InvalidArgumentError` (exit 8) — including three consecutive checks in one `status.ts` function that used both.
|
|
101
|
+
- **Nothing at the CLI boundary ever read an error's `cause`.** Nine classes declare one and 22 throw sites pass one, but a `FireForgeError` rendered as its `userMessage` and stopped, so the underlying git stderr or errno never reached the operator; `download.ts` had hand-rolled the rendering four times to make its own "--verbose for the underlying error" promise true. The boundary now walks the chain under `--verbose`.
|
|
102
|
+
|
|
103
|
+
### CLI
|
|
104
|
+
|
|
105
|
+
- **`--wait-lock` told commands they took no lock when they did.** `patch tier`, `patch lint-ignore` and `patch staged-dependency` mutate patch metadata under the patch-directory lock, but carried the accept-and-ignore registration whose help text read "accepted and ignored: this command takes no FireForge lock" — so a scripted `--wait-lock 300` was silently given the default budget. They honour it now.
|
|
106
|
+
- `import` and `setup` accept `-y, --yes`. On `setup` it is a plain alias of `--force`. On `import` it is **not**: `--force` waives both the drift prompt and the patch-integrity gate, and `--yes` waives only the prompt, so an unattended import keeps the integrity refusal armed.
|
|
107
|
+
- **`fireforge test --doctor` printed the same preflight line three times** on a terminal: a 2026-04-24 fix added an emission for non-TTY capture without removing the two it superseded. The raw writes are gated on non-TTY, which is where the capture requirement applies; the clack framing stays for terminals.
|
|
108
|
+
- **All seven** `--wait-lock` commands that take the patch-directory lock now honour it: `patch tier`, `patch lint-ignore`, `patch staged-dependency`, `export-all`, `resolve`, `doctor` and `rebase`. Each routes through a different lock entry point, so the budget is threaded through `updatePatchMetadata`, `mutatePatchMetadata`, `commitExportedPatch`, `updatePatchAndMetadata`, `rebuildPatchesManifest` and the rebase patch loop.
|
|
109
|
+
- `status --json` outside a project wrote a clack-styled block to **stdout** and exited 2, because the config error is raised before the command body engages machine mode. The boundary now engages it from the flag, so stdout carries exactly one JSON envelope and the human guidance goes to stderr — rule 3 of `docs/machine-output.md`, which was aspirational until now. A payload that already owns stdout (`status --json --fail-on`) suppresses the envelope, so the "exactly one document" half of the contract holds too.
|
|
110
|
+
- `tree list --json` gained the failure envelope `status --json` already emitted, so a scripted consumer no longer gets a parseable refusal from one and a bare non-zero exit from the other. The contract is written down in `docs/machine-output.md`.
|
|
111
|
+
|
|
112
|
+
### More correctness
|
|
113
|
+
|
|
114
|
+
- **`furnace create`'s interactive tag prompt died on an invalid name.** It passed `validateTagName` — which THROWS — to clack's `validate` callback, which expects a returned message and re-prompts on it. So a typo escaped clack's validation loop as a `FurnaceError` and killed the prompt instead of showing the rule inline. This is the concrete cost of `validate*` spanning six incompatible return contracts across 42 exports: the name did not say which one you were getting. The message-returning minority is now `describe*Problem`, `validateTagName` is implemented in terms of its new sibling so the two cannot disagree, and the prefix contract is written down in `utils/validation.ts`.
|
|
115
|
+
- **`register --after` was silently ignored for four of six file types.** The CLI accepted the flag, the adapter took it as `_after`, and the entry went in alphabetically with nothing said. Rules now declare `supportsAfter`, and passing `--after` to one that cannot honour it says so.
|
|
116
|
+
- `RebasePatchEntry` became a discriminated union on `status`, so a status flip can no longer strand the previous status's payload — which `rebase --continue` did, persisting `{ status: 'resolved', error, conflictingFiles }` to the session file. The on-disk validator is deliberately NOT tightened to match: the session file carries no schema version, so a stricter validator would refuse a 0.43.x file mid-rebase, where the only remedies discard the operator's conflict resolution. Entries are normalized on read instead, and two tests pin that an older session still loads.
|
|
117
|
+
- **A `patches.json` from a newer FireForge was reported as malformed.** `patches.json version must be 1` sent the operator looking for damage in a file that only needed a newer build to read it, and the manifest had no migration hook at all. It now refuses by name — naming the found and supported schema versions and telling the operator not to delete the file — while a version below 1 or non-integer keeps the plain malformed message, because that one really is malformed.
|
|
118
|
+
- The three JSDoc walkers in `patch-lint-jsdoc.ts` return their issues instead of pushing into an out-param, matching the ~30 other patch-lint entry points. The new `jsdoc/check-param-names` rule immediately caught the stale `@param issues` the change left behind — which is the point of enabling it.
|
|
119
|
+
|
|
120
|
+
### Test infrastructure
|
|
121
|
+
|
|
122
|
+
- **`createLoggerMock()` and `createFsMock()`** in `src/test-utils/module-mocks.ts` replace hand-rolled `vi.mock` factories at 110 sites. `utils/logger.js` was mocked in 118 files by 113 full-replacement factories across 55 distinct key-sets, 34 appearing exactly once; `utils/fs.js` had 89 more. Each enumerated a SUBSET of the real module, so adding one export turned it `undefined` everywhere the factory omitted it — which happened twice while writing this release, 106 files each time.
|
|
123
|
+
The factories are annotated with `typeof import(...)`, so `tsc --noEmit` — already in the gate — reports a missing export as an error in **one** file instead of a runtime failure in a hundred suites. Verified by adding a probe export to each real module and confirming the failure lands only in `module-mocks.ts`.
|
|
124
|
+
- **`test.test.ts` was 2,943 lines under one `describe`.** What made it unsplittable was its own 190-line `vi.mock` header: any split either duplicated the header into each new file or did not happen. The header moved to `test-command-mocks.ts` as one factory per mocked module, called from inside each file's own `vi.mock` callback — hoisting is unaffected and every suite still gets fresh `vi.fn()`s rather than shared call history. The file is now six, largest 952 lines, split on the seams the tests already formed: discovery and failure rewriting, staleness and packaging coverage, xpcshell/mochitest hints, Marionette forwarding, harness resilience, verdict contract.
|
|
125
|
+
The split exposed a real test-isolation bug: `keeps the Marionette preflight for xpcshell-only --doctor runs` had never set a preflight result, and passed only because an `it` roughly 1,000 lines earlier had left one on the shared mock with `mockResolvedValue`. It sets its own now. Coverage of `src/commands/test.ts` is byte-identical before and after the split (95.12 / 94.28 / 100 / 86.23), which is the check that the mock rewiring did not quietly stop exercising anything.
|
|
126
|
+
- Five rollback tests moved from asserting a module-level spy to asserting the persisted pending-repair marker. The spy stopped working when the sequence moved into `completeJournalRollback` (its call to the recorder is intra-module), and the outcome is the better assertion anyway.
|
|
127
|
+
- `git-readonly-index.test.ts` carried a latent racy-clean flake: it edited `const b = 2;` to `const b = 3;` — the same byte length, within the same second as the index entry — without invalidating the stat cache, so git could report the file unmodified. Its sibling test already guarded for this.
|
|
128
|
+
|
|
3
129
|
## 0.43.0
|
|
4
130
|
|
|
5
131
|
### Verdict correctness
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Inspired by [fern.js](https://github.com/ghostery/user-agent-desktop) and [Melon
|
|
|
16
16
|
- **Wiring and registration** Add chrome scripts, DOM fragments, modules, styles, tests and manifest entries through commands built by learning from existing Firefox conventions.
|
|
17
17
|
- **Furnace components** Create or override `MozLitElement` widgets easily to add new or adapt existing UI components to your needs.
|
|
18
18
|
- **Quality** `lint`, `typecheck`, `verify` and `doctor` catch common issues early.
|
|
19
|
-
- **Tests**
|
|
19
|
+
- **Tests** FireForge was built by taking apart and applying patches of all sorts to original Firefox source code across different versions and products, learning what works vs doesn't and creating some quite extensive tests based on that covering all manner of scenarios. Yes, we mock quite a bit, but when building a tool that modifies a separate code base, I think it's a solid compromise for the time being. Full end-to-end runs are currently run locally on my MacBook, as they require about 30 GB of disk and significant compute for multiple full builds. Full end-to-end via Actions will be added soonishlyTM but might need a different runner...
|
|
20
20
|
|
|
21
21
|
## Requirements
|
|
22
22
|
|
|
@@ -89,6 +89,15 @@ new one as a single transaction — including staged-dependency owner rewrites
|
|
|
89
89
|
`--dry-run` support. `patch rename` also supports `--category` and `--order`; every
|
|
90
90
|
queue-mutating patch command accepts `--wait-lock [seconds]`.
|
|
91
91
|
|
|
92
|
+
Under several concurrent sessions a held lock is the normal state, not an exceptional
|
|
93
|
+
one, so a wait budget is a property of the session rather than of the invocation: set
|
|
94
|
+
`FIREFORGE_WAIT_LOCK=<seconds>` and every lock-taking command that was not given an
|
|
95
|
+
explicit `--wait-lock` uses it. A wait whose queue position keeps improving is granted a
|
|
96
|
+
fresh budget on each advance (up to four times the requested one, capped at an hour), so
|
|
97
|
+
a long-but-moving queue no longer expires one position from the head; a queue that stops
|
|
98
|
+
moving still starves on the budget you asked for, and the refusal names the position you
|
|
99
|
+
reached.
|
|
100
|
+
|
|
92
101
|
`fireforge test <directory>` runs exactly that directory: FireForge enumerates the
|
|
93
102
|
directory's test files and passes the explicit file list to mach in one invocation, so
|
|
94
103
|
mach's prefix-based path matching cannot silently sweep in sibling directories sharing the
|
package/dist/bin/fireforge.d.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* FireForge CLI entry point.
|
|
3
|
+
* FireForge CLI entry point, and the only file that may call process.exit().
|
|
4
|
+
* Library code propagates errors via CommandError or FireForgeError instead.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* FireForgeError — never by terminating the process directly.
|
|
8
|
-
*
|
|
9
|
-
* The signal pipeline below composes several lifecycle modules; the
|
|
10
|
-
* invariants it upholds (and who owns what) are documented in
|
|
11
|
-
* docs/lifecycle-invariants.md, and the composed behavior is pinned by
|
|
12
|
-
* src/core/__tests__/signal-compound-mutation-scenario.test.ts.
|
|
6
|
+
* The signal pipeline below composes several lifecycle modules; see
|
|
7
|
+
* docs/lifecycle-invariants.md for who owns what.
|
|
13
8
|
*/
|
|
14
9
|
export {};
|
package/dist/bin/fireforge.js
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// SPDX-License-Identifier: EUPL-1.2
|
|
3
3
|
/**
|
|
4
|
-
* FireForge CLI entry point.
|
|
4
|
+
* FireForge CLI entry point, and the only file that may call process.exit().
|
|
5
|
+
* Library code propagates errors via CommandError or FireForgeError instead.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* FireForgeError — never by terminating the process directly.
|
|
9
|
-
*
|
|
10
|
-
* The signal pipeline below composes several lifecycle modules; the
|
|
11
|
-
* invariants it upholds (and who owns what) are documented in
|
|
12
|
-
* docs/lifecycle-invariants.md, and the composed behavior is pinned by
|
|
13
|
-
* src/core/__tests__/signal-compound-mutation-scenario.test.ts.
|
|
7
|
+
* The signal pipeline below composes several lifecycle modules; see
|
|
8
|
+
* docs/lifecycle-invariants.md for who owns what.
|
|
14
9
|
*/
|
|
15
10
|
import { installBrokenPipeHandler, main } from '../src/cli.js';
|
|
11
|
+
import { emitKilledVerdict } from '../src/commands/test-verdict.js';
|
|
12
|
+
import { forceReleaseHeldLocksForSignal } from '../src/core/file-lock.js';
|
|
16
13
|
import { forceReleaseFurnaceLocksForActiveOperations, isSignalRollbackInFlight, rollbackActiveOperationsForSignal, } from '../src/core/furnace-operation.js';
|
|
17
14
|
import { waitForActiveCriticalSections } from '../src/core/signal-critical.js';
|
|
18
15
|
import { CommandError } from '../src/errors/base.js';
|
|
@@ -30,18 +27,16 @@ const SIGNAL_CRITICAL_SECTION_TIMEOUT_MS = 5_000;
|
|
|
30
27
|
* under `run`/`test`, mach under `build`) to shut down after the signal was
|
|
31
28
|
* forwarded to them. Must exceed the largest child grace window
|
|
32
29
|
* (execSmokeRun's killGraceMs default of 10 s) so the SIGTERM → grace →
|
|
33
|
-
* SIGKILL escalation can
|
|
34
|
-
*
|
|
35
|
-
* immediately, so an impatient operator is never stuck waiting.
|
|
30
|
+
* SIGKILL escalation can complete; exiting earlier orphans hung Firefox
|
|
31
|
+
* trees. A second Ctrl+C escalates to SIGKILL immediately.
|
|
36
32
|
*/
|
|
37
33
|
const CHILD_SHUTDOWN_TIMEOUT_MS = 12_000;
|
|
38
34
|
/**
|
|
39
|
-
* Upper bound (ms) a delayed exit waits for stdout/stderr to drain.
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* EPIPE'd/closed pipe releases the wait immediately.
|
|
35
|
+
* Upper bound (ms) a delayed exit waits for stdout/stderr to drain. A pipe
|
|
36
|
+
* is ASYNC, so `process.exit()` discards anything queued past the 64 KiB
|
|
37
|
+
* kernel buffer — enough to truncate a JSON payload mid-object. Bounded so
|
|
38
|
+
* a stalled reader cannot wedge a failing process; an EPIPE'd/closed pipe
|
|
39
|
+
* releases the wait immediately.
|
|
45
40
|
*/
|
|
46
41
|
const STDIO_FLUSH_TIMEOUT_MS = 5_000;
|
|
47
42
|
/**
|
|
@@ -62,29 +57,36 @@ process.on('unhandledRejection', (reason) => {
|
|
|
62
57
|
}
|
|
63
58
|
exitAfterStdioFlush(1);
|
|
64
59
|
});
|
|
65
|
-
// SIGINT / SIGTERM
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
// furnace mutation is currently registered with the lifecycle wrapper, so
|
|
70
|
-
// patch-only commands behave exactly as before.
|
|
60
|
+
// SIGINT / SIGTERM run any in-flight furnace rollback before terminating.
|
|
61
|
+
// The library may not call process.exit itself, so the entry point owns both
|
|
62
|
+
// the rollback dispatch and the exit. A no-op when no furnace mutation is
|
|
63
|
+
// registered with the lifecycle wrapper.
|
|
71
64
|
function installFurnaceSignalHandler(signal, exitCode) {
|
|
72
65
|
process.on(signal, () => {
|
|
66
|
+
// First, before anything that can stall: a killed test run must leave a
|
|
67
|
+
// terminal line, or a log tail cannot tell "killed" from "still
|
|
68
|
+
// running" from "never started". The drain below is bounded but not
|
|
69
|
+
// instant, and the verdict must survive a drain that times out.
|
|
70
|
+
// No-op unless a test run was actually in flight.
|
|
71
|
+
try {
|
|
72
|
+
emitKilledVerdict(signal);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// A diagnostic must never keep the process from exiting.
|
|
76
|
+
}
|
|
73
77
|
if (isSignalRollbackInFlight()) {
|
|
74
78
|
// A second Ctrl+C while we're already rolling back is a noisy "I want
|
|
75
79
|
// out now" — let the second signal terminate the process forcefully
|
|
76
80
|
// rather than queueing another rollback that will race the first.
|
|
77
81
|
process.exit(exitCode);
|
|
78
82
|
}
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
// execInherit/execSmokeRun forward to Firefox/mach — exiting before it
|
|
87
|
-
// ran is what used to orphan hung child trees.
|
|
83
|
+
// Rollback, critical-section drain, and child shutdown run in parallel.
|
|
84
|
+
// Draining critical sections (rebase's apply + session persist, registered
|
|
85
|
+
// via `runInSignalCriticalSection`) keeps the CLI from exiting with a patch
|
|
86
|
+
// applied to the engine but a stale session file that mis-tracks progress
|
|
87
|
+
// on `--continue`. Waiting on child shutdown keeps the parent alive through
|
|
88
|
+
// the SIGTERM → grace → SIGKILL escalation execInherit/execSmokeRun forward
|
|
89
|
+
// to Firefox/mach; exiting first orphans those trees.
|
|
88
90
|
void Promise.allSettled([
|
|
89
91
|
rollbackActiveOperationsForSignal(signal).catch((error) => {
|
|
90
92
|
console.error(`Furnace rollback after ${signal} failed:`, error instanceof Error ? error.message : error);
|
|
@@ -92,13 +94,13 @@ function installFurnaceSignalHandler(signal, exitCode) {
|
|
|
92
94
|
waitForActiveCriticalSections(SIGNAL_CRITICAL_SECTION_TIMEOUT_MS),
|
|
93
95
|
waitForActiveChildShutdown(CHILD_SHUTDOWN_TIMEOUT_MS),
|
|
94
96
|
])
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
97
|
+
// `withFileLock`'s `finally { rm }` never runs across `process.exit`,
|
|
98
|
+
// so both sweeps exist for the same reason. The generic one covers
|
|
99
|
+
// every lock this process holds (engine session, patch directory,
|
|
100
|
+
// build); the furnace one stays because it carries rollback
|
|
101
|
+
// bookkeeping the generic sweep does not, and runs after it so the
|
|
102
|
+
// ownership checks see a settled tree.
|
|
103
|
+
.then(() => forceReleaseHeldLocksForSignal())
|
|
102
104
|
.then(() => forceReleaseFurnaceLocksForActiveOperations())
|
|
103
105
|
.finally(() => {
|
|
104
106
|
exitAfterStdioFlush(exitCode);
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.
|
|
4
|
-
"commit": "
|
|
5
|
-
"shortCommit": "
|
|
3
|
+
"version": "0.44.0",
|
|
4
|
+
"commit": "93892e54177df3f88391e3b571674fd1189be43d",
|
|
5
|
+
"shortCommit": "93892e54177d",
|
|
6
6
|
"dirty": false,
|
|
7
7
|
"dirtyHash": null,
|
|
8
|
-
"buildTime": "2026-08-
|
|
8
|
+
"buildTime": "2026-08-27T18:55:32.566Z"
|
|
9
9
|
}
|
package/dist/src/cli.js
CHANGED
|
@@ -9,7 +9,8 @@ import { ExitCode } from './errors/codes.js';
|
|
|
9
9
|
import { ConfigNotFoundError } from './errors/config.js';
|
|
10
10
|
import { getCliVersion } from './utils/build-info.js';
|
|
11
11
|
import { toError } from './utils/errors.js';
|
|
12
|
-
import { cancel, error as logError, setMachineOutputMode, setStdoutSealed, setVerbose, } from './utils/logger.js';
|
|
12
|
+
import { cancel, error as logError, isStdoutSealed, isVerbose, setMachineOutputMode, setStdoutSealed, setVerbose, } from './utils/logger.js';
|
|
13
|
+
import { emitMachineError } from './utils/machine-output.js';
|
|
13
14
|
import { ensureWaitLockOptionEverywhere } from './utils/options.js';
|
|
14
15
|
const brokenPipeInstalledKey = Symbol.for('fireforge.cli.brokenPipeHandlerInstalled');
|
|
15
16
|
const brokenPipeListenerKey = Symbol.for('fireforge.cli.brokenPipeHandlerListener');
|
|
@@ -31,6 +32,51 @@ function getBrokenPipeHandler(state) {
|
|
|
31
32
|
state[brokenPipeListenerKey] = handler;
|
|
32
33
|
return handler;
|
|
33
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Stable machine-readable tag for an error class, for the `--json` envelope.
|
|
37
|
+
*
|
|
38
|
+
* Derived from the class name rather than hand-mapped: a new error class gets
|
|
39
|
+
* a sensible tag automatically, and the mapping cannot drift out of sync with
|
|
40
|
+
* the taxonomy. `ConfigNotFoundError` becomes `config-not-found`.
|
|
41
|
+
*
|
|
42
|
+
* @param error - The error being rendered
|
|
43
|
+
* @returns A kebab-case tag with the `Error` suffix stripped
|
|
44
|
+
*/
|
|
45
|
+
function machineErrorCode(error) {
|
|
46
|
+
return error.name
|
|
47
|
+
.replace(/Error$/, '')
|
|
48
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
49
|
+
.toLowerCase();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Prints an error's `cause` chain under `--verbose`.
|
|
53
|
+
*
|
|
54
|
+
* Nine error classes declare a `cause` and twenty-two throw sites pass one;
|
|
55
|
+
* without this the underlying git stderr, errno or parse failure never
|
|
56
|
+
* reaches the operator, because a `FireForgeError` renders as its
|
|
57
|
+
* `userMessage` and stops.
|
|
58
|
+
*
|
|
59
|
+
* Goes to stderr via console.error so a `--json` payload on stdout stays
|
|
60
|
+
* intact, matching the InternalInvariantError branch.
|
|
61
|
+
*
|
|
62
|
+
* @param error - The error whose chain to walk
|
|
63
|
+
*/
|
|
64
|
+
function printCauseChain(error) {
|
|
65
|
+
if (!isVerbose())
|
|
66
|
+
return;
|
|
67
|
+
let current = error.cause;
|
|
68
|
+
let depth = 0;
|
|
69
|
+
// Bounded: a self-referential cause chain must not hang the boundary.
|
|
70
|
+
while (current !== undefined && current !== null && depth < 8) {
|
|
71
|
+
const normalized = toError(current);
|
|
72
|
+
console.error(`Caused by: ${normalized.name}: ${normalized.message}`);
|
|
73
|
+
if (normalized.stack) {
|
|
74
|
+
console.error(normalized.stack);
|
|
75
|
+
}
|
|
76
|
+
current = normalized.cause;
|
|
77
|
+
depth += 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
34
80
|
/**
|
|
35
81
|
* Installs a handler for broken-pipe (EPIPE) errors on stdout/stderr.
|
|
36
82
|
* This is a process-level concern: when output is piped to a process that
|
|
@@ -97,6 +143,17 @@ export function getProjectRoot() {
|
|
|
97
143
|
*/
|
|
98
144
|
export function withErrorHandling(handler) {
|
|
99
145
|
return async (...args) => {
|
|
146
|
+
// Rule 3 of docs/machine-output.md: machine mode engages BEFORE any
|
|
147
|
+
// output. Anything that throws on the way in — most visibly
|
|
148
|
+
// `getProjectRoot`'s ConfigNotFoundError — would otherwise render a
|
|
149
|
+
// clack-styled block to stdout and leave a `--json` consumer with
|
|
150
|
+
// un-parseable output. Reading the flag from argv is blunt but exact: if
|
|
151
|
+
// the invocation asked for machine output, stdout belongs to the payload
|
|
152
|
+
// from the first byte.
|
|
153
|
+
const wantsMachineOutput = process.argv.includes('--json') || process.argv.includes('--raw');
|
|
154
|
+
if (wantsMachineOutput) {
|
|
155
|
+
setMachineOutputMode(true);
|
|
156
|
+
}
|
|
100
157
|
try {
|
|
101
158
|
await handler(...args);
|
|
102
159
|
}
|
|
@@ -126,7 +183,17 @@ export function withErrorHandling(handler) {
|
|
|
126
183
|
throw new CommandError(error.code);
|
|
127
184
|
}
|
|
128
185
|
if (error instanceof FireForgeError) {
|
|
186
|
+
// In machine mode the payload contract owes the consumer a parseable
|
|
187
|
+
// refusal on stdout, not just a non-zero exit. `logError` has already
|
|
188
|
+
// been routed to stderr by the mode, so the two do not collide.
|
|
129
189
|
logError(error.userMessage);
|
|
190
|
+
printCauseChain(error);
|
|
191
|
+
// Not when a payload already owns stdout: `status --json --fail-on`
|
|
192
|
+
// writes its full document and THEN refuses, and appending an
|
|
193
|
+
// envelope would make that two JSON documents.
|
|
194
|
+
if (wantsMachineOutput && !isStdoutSealed()) {
|
|
195
|
+
emitMachineError(machineErrorCode(error), error.message, error.code);
|
|
196
|
+
}
|
|
130
197
|
throw new CommandError(error.code);
|
|
131
198
|
}
|
|
132
199
|
const normalizedError = toError(error);
|
|
@@ -141,8 +208,8 @@ export function withErrorHandling(handler) {
|
|
|
141
208
|
// ENGAGED while an error propagates (a mid-throw restore would route
|
|
142
209
|
// the styled error to stdout, corrupting the `--json`/`--raw` payload
|
|
143
210
|
// stream or displacing the FIREFORGE-VERDICT line); the reset happens
|
|
144
|
-
// here, after logError has picked its stream, so no state leaks into
|
|
145
|
-
//
|
|
211
|
+
// here, after logError has picked its stream, so no state leaks into a
|
|
212
|
+
// subsequent in-process invocation.
|
|
146
213
|
setMachineOutputMode(false);
|
|
147
214
|
setStdoutSealed(false);
|
|
148
215
|
}
|
|
@@ -295,11 +362,10 @@ export function createProgram() {
|
|
|
295
362
|
for (const entry of COMMAND_MANIFEST) {
|
|
296
363
|
entry.register(program, ctx);
|
|
297
364
|
}
|
|
298
|
-
// Uniform `--wait-lock`: scripted sequences blanket-append the flag, and
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
// registrar having to remember; commands that already declare the
|
|
365
|
+
// Uniform `--wait-lock`: scripted sequences blanket-append the flag, and a
|
|
366
|
+
// subcommand that rejects it with "unknown option" kills the sequence with
|
|
367
|
+
// a usage error instead of a lock message. Applied after registration so it
|
|
368
|
+
// covers every subcommand at every depth; commands that already declare the
|
|
303
369
|
// honoring flag keep it untouched.
|
|
304
370
|
ensureWaitLockOptionEverywhere(program);
|
|
305
371
|
return program;
|
|
@@ -1,34 +1,33 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { getProjectPaths } from '../core/config.js';
|
|
4
|
+
import { assertEngineExists } from '../core/engine-precondition.js';
|
|
4
5
|
import { ensureOriginRemote } from '../core/git.js';
|
|
5
6
|
import { bootstrapWithOutput } from '../core/mach.js';
|
|
6
7
|
import { GeneralError } from '../errors/base.js';
|
|
7
8
|
import { BootstrapError } from '../errors/build.js';
|
|
8
9
|
import { ExitCode } from '../errors/codes.js';
|
|
9
|
-
import { pathExists } from '../utils/fs.js';
|
|
10
10
|
import { error, info, intro, outro, warn } from '../utils/logger.js';
|
|
11
|
-
import { detectBootstrapIssues, runPostBootstrapChecks } from './bootstrap-checks.js';
|
|
11
|
+
import { detectBootstrapIssues, runPostBootstrapChecks, } from './bootstrap-checks.js';
|
|
12
12
|
import { reportDoctorResults } from './doctor.js';
|
|
13
13
|
import { resolveDoctorSeverity } from './doctor-check-core.js';
|
|
14
|
+
/** One sentence per detected issue, keyed by the scanner's own tags. */
|
|
15
|
+
const BOOTSTRAP_FAILURE_SENTENCES = {
|
|
16
|
+
'python-traceback': 'Bootstrap emitted a Python traceback.',
|
|
17
|
+
'sdk-fetch-403': 'Bootstrap hit an HTTP 403 while fetching dependencies.',
|
|
18
|
+
'missing-origin-remote': 'Bootstrap expected an "origin" git remote in the Firefox source checkout.',
|
|
19
|
+
};
|
|
14
20
|
/**
|
|
15
21
|
* Builds a human-readable failure message for hard failures (non-zero exit).
|
|
16
22
|
* Used only when mach bootstrap itself reports failure.
|
|
17
23
|
*/
|
|
18
24
|
function buildBootstrapFailureMessage(output) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
issues.push('Bootstrap hit an HTTP 403 while fetching dependencies.');
|
|
26
|
-
}
|
|
27
|
-
if (/no such remote ['"]origin['"]/i.test(normalized) ||
|
|
28
|
-
/remote ['"]origin['"] does not exist/i.test(normalized) ||
|
|
29
|
-
/missing git remote ['"]origin['"]/i.test(normalized)) {
|
|
30
|
-
issues.push('Bootstrap expected an "origin" git remote in the Firefox source checkout.');
|
|
31
|
-
}
|
|
25
|
+
// Delegates detection to the canonical scanner rather than keeping a
|
|
26
|
+
// second copy of its six regexes. The copies had already disagreed: this
|
|
27
|
+
// one reported a traceback AND a 403 separately, while
|
|
28
|
+
// `detectBootstrapIssues` collapses them, because a bootstrap traceback
|
|
29
|
+
// accompanying a 403 is just the stack trace from that HTTP error.
|
|
30
|
+
const issues = detectBootstrapIssues(output).map((issue) => BOOTSTRAP_FAILURE_SENTENCES[issue]);
|
|
32
31
|
if (issues.length === 0) {
|
|
33
32
|
return undefined;
|
|
34
33
|
}
|
|
@@ -44,9 +43,7 @@ export async function bootstrapCommand(projectRoot) {
|
|
|
44
43
|
intro('FireForge Bootstrap');
|
|
45
44
|
const paths = getProjectPaths(projectRoot);
|
|
46
45
|
// Check if engine exists
|
|
47
|
-
|
|
48
|
-
throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
|
|
49
|
-
}
|
|
46
|
+
await assertEngineExists(paths.engine);
|
|
50
47
|
// Ensure the engine repo has an "origin" remote so Firefox's bootstrap
|
|
51
48
|
// scripts don't emit noisy "No such remote" errors.
|
|
52
49
|
await ensureOriginRemote(paths.engine);
|
|
@@ -69,8 +66,8 @@ export async function bootstrapCommand(projectRoot) {
|
|
|
69
66
|
if (issues.length > 0) {
|
|
70
67
|
const checks = await runPostBootstrapChecks(issues);
|
|
71
68
|
// Shares one resolver with reportDoctorResults so the two consumers of
|
|
72
|
-
// this same array cannot drift apart
|
|
73
|
-
//
|
|
69
|
+
// this same array cannot drift apart on how a check's severity is
|
|
70
|
+
// derived.
|
|
74
71
|
const hasErrors = checks.some((c) => resolveDoctorSeverity(c) === 'error');
|
|
75
72
|
info('');
|
|
76
73
|
if (hasErrors) {
|