@hominis/fireforge 0.35.0 → 0.37.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 +42 -0
- package/README.md +34 -9
- package/dist/bin/fireforge.js +21 -6
- package/dist/src/cli.js +22 -5
- package/dist/src/commands/bootstrap-checks.js +0 -1
- package/dist/src/commands/bootstrap.d.ts +2 -1
- package/dist/src/commands/bootstrap.js +16 -7
- package/dist/src/commands/build.js +5 -4
- package/dist/src/commands/config.js +30 -10
- package/dist/src/commands/discard.js +18 -3
- package/dist/src/commands/doctor/post-rebase-audit.js +0 -1
- package/dist/src/commands/doctor-check-core.js +0 -1
- package/dist/src/commands/doctor-external-toolchains.d.ts +3 -0
- package/dist/src/commands/doctor-external-toolchains.js +54 -0
- package/dist/src/commands/doctor-furnace-jar.js +0 -1
- package/dist/src/commands/doctor-furnace-manifest-sync.js +0 -1
- package/dist/src/commands/doctor-furnace.js +2 -2
- package/dist/src/commands/doctor-orphaned-harness.d.ts +52 -0
- package/dist/src/commands/doctor-orphaned-harness.js +132 -0
- package/dist/src/commands/doctor-working-tree.js +0 -1
- package/dist/src/commands/doctor.js +6 -1
- package/dist/src/commands/download.js +28 -13
- package/dist/src/commands/export-all.js +5 -3
- package/dist/src/commands/export-flow.js +12 -7
- package/dist/src/commands/export-placement-gate.js +0 -1
- package/dist/src/commands/export-placement-policy.js +0 -1
- package/dist/src/commands/export-shared.d.ts +5 -1
- package/dist/src/commands/export-shared.js +18 -6
- package/dist/src/commands/export.js +22 -10
- package/dist/src/commands/furnace/apply.js +22 -6
- package/dist/src/commands/furnace/chrome-doc-remove.js +0 -1
- package/dist/src/commands/furnace/chrome-doc-templates.js +0 -1
- package/dist/src/commands/furnace/chrome-doc-tests.js +0 -1
- package/dist/src/commands/furnace/chrome-doc.js +0 -1
- package/dist/src/commands/furnace/create-browser-test.js +0 -1
- package/dist/src/commands/furnace/create-dry-run.js +0 -1
- package/dist/src/commands/furnace/create-features.js +0 -1
- package/dist/src/commands/furnace/create-mochikit.js +0 -1
- package/dist/src/commands/furnace/create-readback.js +0 -1
- package/dist/src/commands/furnace/create-templates.js +0 -1
- package/dist/src/commands/furnace/create-validation.js +0 -1
- package/dist/src/commands/furnace/create-xpcshell.js +0 -1
- package/dist/src/commands/furnace/create.js +0 -1
- package/dist/src/commands/furnace/deploy.js +7 -83
- package/dist/src/commands/furnace/diff.js +0 -1
- package/dist/src/commands/furnace/index.js +59 -25
- package/dist/src/commands/furnace/init.js +0 -1
- package/dist/src/commands/furnace/list.js +0 -1
- package/dist/src/commands/furnace/override.js +0 -1
- package/dist/src/commands/furnace/preview.js +3 -3
- package/dist/src/commands/furnace/refresh.js +0 -1
- package/dist/src/commands/furnace/remove-state.d.ts +15 -0
- package/dist/src/commands/furnace/remove-state.js +47 -1
- package/dist/src/commands/furnace/remove.js +3 -9
- package/dist/src/commands/furnace/rename-browser-test.js +1 -5
- package/dist/src/commands/furnace/rename-helpers.d.ts +6 -0
- package/dist/src/commands/furnace/rename-helpers.js +30 -1
- package/dist/src/commands/furnace/rename-xpcshell.js +7 -6
- package/dist/src/commands/furnace/rename.js +15 -36
- package/dist/src/commands/furnace/scan.js +0 -1
- package/dist/src/commands/furnace/status.js +0 -1
- package/dist/src/commands/furnace/sync.js +76 -4
- package/dist/src/commands/furnace/validate.js +0 -1
- package/dist/src/commands/furnace/validation-output.js +0 -1
- package/dist/src/commands/import.js +35 -9
- package/dist/src/commands/lint-per-patch.js +19 -7
- package/dist/src/commands/lint.js +13 -3
- package/dist/src/commands/manifest.js +1 -2
- package/dist/src/commands/package.js +2 -1
- package/dist/src/commands/patch/compact.js +1 -1
- package/dist/src/commands/patch/delete.js +1 -1
- package/dist/src/commands/patch/index.js +1 -1
- package/dist/src/commands/patch/lint-ignore.js +21 -6
- package/dist/src/commands/patch/move-files.js +1 -1
- package/dist/src/commands/patch/patch-context.js +0 -1
- package/dist/src/commands/patch/rename.js +4 -4
- package/dist/src/commands/patch/reorder.js +3 -4
- package/dist/src/commands/patch/split-plan.js +2 -1
- package/dist/src/commands/patch/split.js +4 -4
- package/dist/src/commands/patch/staged-dependency.d.ts +1 -1
- package/dist/src/commands/patch/staged-dependency.js +114 -52
- package/dist/src/commands/patch/tier.js +18 -5
- package/dist/src/commands/re-export-bulk-scan.js +0 -1
- package/dist/src/commands/re-export-files.js +5 -1
- package/dist/src/commands/re-export-options.js +0 -1
- package/dist/src/commands/re-export-scan.js +0 -1
- package/dist/src/commands/re-export.js +14 -4
- package/dist/src/commands/rebase/abort.js +2 -9
- package/dist/src/commands/rebase/confirm.js +0 -1
- package/dist/src/commands/rebase/conflict-summary.js +0 -1
- package/dist/src/commands/rebase/continue.js +0 -1
- package/dist/src/commands/rebase/index.d.ts +1 -1
- package/dist/src/commands/rebase/index.js +10 -15
- package/dist/src/commands/rebase/patch-loop.js +38 -7
- package/dist/src/commands/rebase/summary.js +2 -3
- package/dist/src/commands/rebase.js +0 -1
- package/dist/src/commands/register.js +1 -1
- package/dist/src/commands/reset.js +16 -9
- package/dist/src/commands/resolve.js +2 -2
- package/dist/src/commands/run.js +4 -4
- package/dist/src/commands/setup-support.js +0 -1
- package/dist/src/commands/setup.js +1 -2
- package/dist/src/commands/source.js +1 -2
- package/dist/src/commands/status-output.js +1 -12
- package/dist/src/commands/status.js +147 -133
- package/dist/src/commands/test-appdir.js +0 -1
- package/dist/src/commands/test-diagnose.js +38 -3
- package/dist/src/commands/test-modes.d.ts +15 -0
- package/dist/src/commands/test-modes.js +61 -0
- package/dist/src/commands/test-register.d.ts +4 -0
- package/dist/src/commands/test-register.js +64 -0
- package/dist/src/commands/test-run.d.ts +23 -7
- package/dist/src/commands/test-run.js +15 -14
- package/dist/src/commands/test.d.ts +0 -4
- package/dist/src/commands/test.js +147 -96
- package/dist/src/commands/token-coverage.js +0 -1
- package/dist/src/commands/token.js +1 -2
- package/dist/src/commands/typecheck.js +1 -1
- package/dist/src/commands/verify.js +3 -9
- package/dist/src/commands/watch.js +5 -1
- package/dist/src/commands/wire.js +1 -1
- package/dist/src/core/ast-utils.js +0 -1
- package/dist/src/core/brand-validation.js +0 -1
- package/dist/src/core/branding.js +0 -1
- package/dist/src/core/browser-wire.js +0 -1
- package/dist/src/core/build-audit-platform.js +0 -1
- package/dist/src/core/build-audit-registration.js +0 -1
- package/dist/src/core/build-audit-resolve.js +0 -1
- package/dist/src/core/build-audit-transforms.js +0 -1
- package/dist/src/core/build-audit.js +0 -1
- package/dist/src/core/build-baseline-types.d.ts +24 -0
- package/dist/src/core/build-baseline-types.js +0 -1
- package/dist/src/core/build-baseline.d.ts +5 -2
- package/dist/src/core/build-baseline.js +5 -2
- package/dist/src/core/build-prepare.js +2 -2
- package/dist/src/core/config-mutate.js +0 -1
- package/dist/src/core/config-paths.d.ts +2 -2
- package/dist/src/core/config-paths.js +6 -1
- package/dist/src/core/config-state.js +0 -1
- package/dist/src/core/config-validate-patch-policy.js +0 -1
- package/dist/src/core/config-validate-test-toolchains.d.ts +8 -0
- package/dist/src/core/config-validate-test-toolchains.js +105 -0
- package/dist/src/core/config-validate.js +3 -1
- package/dist/src/core/config.js +0 -1
- package/dist/src/core/destructive.js +0 -1
- package/dist/src/core/diff-hunks.js +13 -7
- package/dist/src/core/engine-changes.js +0 -1
- package/dist/src/core/engine-session-lock.d.ts +12 -0
- package/dist/src/core/engine-session-lock.js +45 -0
- package/dist/src/core/file-lock.d.ts +8 -0
- package/dist/src/core/file-lock.js +157 -48
- package/dist/src/core/firefox-archive.d.ts +4 -0
- package/dist/src/core/firefox-archive.js +6 -1
- package/dist/src/core/firefox-cache.js +60 -4
- package/dist/src/core/firefox-download.js +0 -1
- package/dist/src/core/firefox-extract.js +75 -16
- package/dist/src/core/firefox-ignorefile.js +0 -1
- package/dist/src/core/firefox.d.ts +12 -0
- package/dist/src/core/firefox.js +64 -2
- package/dist/src/core/furnace-apply-dry-run.js +0 -1
- package/dist/src/core/furnace-apply-ftl.d.ts +7 -3
- package/dist/src/core/furnace-apply-ftl.js +10 -5
- package/dist/src/core/furnace-apply-helpers.js +0 -1
- package/dist/src/core/furnace-apply-output.js +2 -2
- package/dist/src/core/furnace-apply.d.ts +7 -1
- package/dist/src/core/furnace-apply.js +47 -13
- package/dist/src/core/furnace-checksum-utils.js +0 -1
- package/dist/src/core/furnace-config-array-utils.js +0 -1
- package/dist/src/core/furnace-config-custom.js +10 -1
- package/dist/src/core/furnace-config-order.js +0 -1
- package/dist/src/core/furnace-config-tokens.js +0 -1
- package/dist/src/core/furnace-config-validate.js +0 -1
- package/dist/src/core/furnace-config.d.ts +11 -0
- package/dist/src/core/furnace-config.js +37 -1
- package/dist/src/core/furnace-constants.js +0 -1
- package/dist/src/core/furnace-css-fragments.js +0 -1
- package/dist/src/core/furnace-graph-utils.js +0 -1
- package/dist/src/core/furnace-jsconfig.d.ts +2 -3
- package/dist/src/core/furnace-jsconfig.js +25 -18
- package/dist/src/core/furnace-marker.js +0 -1
- package/dist/src/core/furnace-operation.js +0 -1
- package/dist/src/core/furnace-refresh.js +0 -1
- package/dist/src/core/furnace-registration-ast.d.ts +14 -0
- package/dist/src/core/furnace-registration-ast.js +1 -2
- package/dist/src/core/furnace-registration-remove.js +0 -1
- package/dist/src/core/furnace-registration-validate.js +1 -5
- package/dist/src/core/furnace-registration.js +0 -1
- package/dist/src/core/furnace-rollback.js +0 -1
- package/dist/src/core/furnace-scanner.js +0 -1
- package/dist/src/core/furnace-stale-export.d.ts +59 -0
- package/dist/src/core/furnace-stale-export.js +123 -0
- package/dist/src/core/furnace-staleness.js +0 -1
- package/dist/src/core/furnace-state-persist.d.ts +72 -0
- package/dist/src/core/furnace-state-persist.js +118 -0
- package/dist/src/core/furnace-step-errors.d.ts +23 -0
- package/dist/src/core/furnace-step-errors.js +25 -0
- package/dist/src/core/furnace-stories.js +0 -1
- package/dist/src/core/furnace-validate-accessibility.js +0 -1
- package/dist/src/core/furnace-validate-checks.js +0 -1
- package/dist/src/core/furnace-validate-compatibility.js +9 -3
- package/dist/src/core/furnace-validate-helpers.js +0 -1
- package/dist/src/core/furnace-validate-registration.js +7 -7
- package/dist/src/core/furnace-validate-structure.d.ts +8 -0
- package/dist/src/core/furnace-validate-structure.js +14 -4
- package/dist/src/core/furnace-validate.js +0 -1
- package/dist/src/core/furnace-version-drift.d.ts +6 -3
- package/dist/src/core/furnace-version-drift.js +6 -4
- package/dist/src/core/git-base.js +0 -1
- package/dist/src/core/git-diff.js +0 -1
- package/dist/src/core/git-file-ops.js +0 -1
- package/dist/src/core/git-status.d.ts +13 -0
- package/dist/src/core/git-status.js +32 -2
- package/dist/src/core/git.d.ts +10 -1
- package/dist/src/core/git.js +63 -6
- package/dist/src/core/license-headers.js +0 -1
- package/dist/src/core/lint-cache.js +0 -1
- package/dist/src/core/lint-projection.js +0 -1
- package/dist/src/core/mach-build-artifacts.js +0 -1
- package/dist/src/core/mach-error-hints.js +0 -1
- package/dist/src/core/mach-known-noise-filter.d.ts +60 -0
- package/dist/src/core/mach-known-noise-filter.js +193 -0
- package/dist/src/core/mach-mozconfig.js +0 -1
- package/dist/src/core/mach-python.js +0 -1
- package/dist/src/core/mach-resource-shim.js +0 -1
- package/dist/src/core/mach.d.ts +8 -0
- package/dist/src/core/mach.js +36 -4
- package/dist/src/core/manifest-helpers.js +0 -1
- package/dist/src/core/manifest-register.js +0 -1
- package/dist/src/core/manifest-rules.js +0 -1
- package/dist/src/core/manifest-tokenizers.js +0 -1
- package/dist/src/core/marionette-port.d.ts +8 -0
- package/dist/src/core/marionette-port.js +34 -3
- package/dist/src/core/marionette-preflight.js +0 -1
- package/dist/src/core/markdown-table.js +0 -1
- package/dist/src/core/ownership-table.js +2 -9
- package/dist/src/core/parser-fallback.js +0 -1
- package/dist/src/core/patch-apply-fuzz.d.ts +27 -9
- package/dist/src/core/patch-apply-fuzz.js +71 -34
- package/dist/src/core/patch-apply.d.ts +30 -0
- package/dist/src/core/patch-apply.js +101 -18
- package/dist/src/core/patch-artifact-normalize.d.ts +18 -5
- package/dist/src/core/patch-artifact-normalize.js +19 -15
- package/dist/src/core/patch-export-coverage.js +0 -1
- package/dist/src/core/patch-export-metadata.js +0 -1
- package/dist/src/core/patch-export-update.js +2 -3
- package/dist/src/core/patch-export.d.ts +2 -6
- package/dist/src/core/patch-export.js +24 -48
- package/dist/src/core/patch-files.js +0 -1
- package/dist/src/core/patch-identifier-suggest.js +0 -1
- package/dist/src/core/patch-lint-checkjs.js +0 -1
- package/dist/src/core/patch-lint-chrome-jsdoc.js +0 -1
- package/dist/src/core/patch-lint-cross.js +80 -3
- package/dist/src/core/patch-lint-css.js +0 -1
- package/dist/src/core/patch-lint-diff-tag.js +0 -1
- package/dist/src/core/patch-lint-diff.d.ts +2 -1
- package/dist/src/core/patch-lint-diff.js +17 -32
- package/dist/src/core/patch-lint-imports.js +0 -1
- package/dist/src/core/patch-lint-jsdoc.js +0 -1
- package/dist/src/core/patch-lint-observer.js +0 -1
- package/dist/src/core/patch-lint-ownership.js +0 -1
- package/dist/src/core/patch-lint-reexports.js +0 -1
- package/dist/src/core/patch-lint.d.ts +37 -2
- package/dist/src/core/patch-lint.js +47 -34
- package/dist/src/core/patch-lock.js +0 -1
- package/dist/src/core/patch-manifest-consistency.js +9 -1
- package/dist/src/core/patch-manifest-io.d.ts +19 -2
- package/dist/src/core/patch-manifest-io.js +57 -21
- package/dist/src/core/patch-manifest-query.js +18 -10
- package/dist/src/core/patch-manifest-resolve.js +0 -1
- package/dist/src/core/patch-manifest-validate.js +24 -1
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-manifest.js +1 -2
- package/dist/src/core/patch-parse.d.ts +59 -0
- package/dist/src/core/patch-parse.js +209 -92
- package/dist/src/core/patch-policy.js +0 -1
- package/dist/src/core/patch-registration-refs.js +16 -26
- package/dist/src/core/patch-source-metadata.js +0 -1
- package/dist/src/core/patch-transform.d.ts +4 -0
- package/dist/src/core/patch-transform.js +30 -38
- package/dist/src/core/rebase-session.js +0 -1
- package/dist/src/core/register-browser-content.js +0 -1
- package/dist/src/core/register-module.js +0 -1
- package/dist/src/core/register-result.js +0 -1
- package/dist/src/core/register-scaffold.js +0 -1
- package/dist/src/core/register-shared-css.js +0 -1
- package/dist/src/core/register-test-manifest.js +0 -1
- package/dist/src/core/register-xpcshell-test.js +0 -1
- package/dist/src/core/shared-ftl.js +0 -1
- package/dist/src/core/signal-critical.js +0 -1
- package/dist/src/core/smoke-patterns.js +0 -1
- package/dist/src/core/state-file.js +0 -1
- package/dist/src/core/status-classify.d.ts +16 -0
- package/dist/src/core/status-classify.js +24 -16
- package/dist/src/core/test-harness-crash.d.ts +80 -7
- package/dist/src/core/test-harness-crash.js +251 -15
- package/dist/src/core/test-harness-output.js +0 -1
- package/dist/src/core/test-path-scope.d.ts +21 -13
- package/dist/src/core/test-path-scope.js +43 -28
- package/dist/src/core/test-stale-check.d.ts +29 -1
- package/dist/src/core/test-stale-check.js +59 -1
- package/dist/src/core/test-stale-symlink.js +0 -1
- package/dist/src/core/test-xpcshell-retry.js +0 -1
- package/dist/src/core/token-coverage.js +0 -1
- package/dist/src/core/token-dark-mode.js +0 -1
- package/dist/src/core/token-docs.js +0 -1
- package/dist/src/core/token-manager.js +0 -1
- package/dist/src/core/token-scaffold.js +0 -1
- package/dist/src/core/token-variant.js +0 -1
- package/dist/src/core/toolchain-preflight.d.ts +31 -10
- package/dist/src/core/toolchain-preflight.js +97 -36
- package/dist/src/core/typecheck-shim.js +13 -1
- package/dist/src/core/typecheck.js +0 -1
- package/dist/src/core/wire-destroy.js +0 -1
- package/dist/src/core/wire-dom-fragment.js +0 -1
- package/dist/src/core/wire-init.js +0 -1
- package/dist/src/core/wire-subscript.js +0 -1
- package/dist/src/core/wire-targets.js +0 -1
- package/dist/src/core/wire-utils.js +0 -1
- package/dist/src/core/xpcshell-appdir.js +1 -4
- package/dist/src/errors/base.d.ts +29 -0
- package/dist/src/errors/base.js +35 -1
- package/dist/src/errors/build.js +0 -1
- package/dist/src/errors/codes.d.ts +6 -0
- package/dist/src/errors/codes.js +6 -1
- package/dist/src/errors/config.js +0 -1
- package/dist/src/errors/download.js +0 -1
- package/dist/src/errors/furnace.d.ts +11 -1
- package/dist/src/errors/furnace.js +6 -5
- package/dist/src/errors/git.js +0 -1
- package/dist/src/errors/patch.d.ts +17 -0
- package/dist/src/errors/patch.js +29 -1
- package/dist/src/errors/rebase.js +0 -1
- package/dist/src/errors/run.js +1 -1
- package/dist/src/index.js +0 -1
- package/dist/src/types/cli.js +0 -1
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/index.js +0 -1
- package/dist/src/types/commands/options.d.ts +40 -9
- package/dist/src/types/commands/options.js +0 -1
- package/dist/src/types/commands/patches.d.ts +37 -0
- package/dist/src/types/commands/patches.js +0 -1
- package/dist/src/types/commands/project.d.ts +11 -2
- package/dist/src/types/commands/project.js +0 -1
- package/dist/src/types/config.d.ts +32 -0
- package/dist/src/types/config.js +0 -1
- package/dist/src/types/furnace.d.ts +23 -0
- package/dist/src/types/furnace.js +0 -1
- package/dist/src/types/index.js +0 -1
- package/dist/src/types/typecheck.js +0 -1
- package/dist/src/utils/elapsed.js +0 -1
- package/dist/src/utils/errors.js +0 -1
- package/dist/src/utils/fs.js +37 -5
- package/dist/src/utils/logger.d.ts +7 -0
- package/dist/src/utils/logger.js +85 -3
- package/dist/src/utils/options.d.ts +15 -0
- package/dist/src/utils/options.js +25 -1
- package/dist/src/utils/package-root.js +11 -4
- package/dist/src/utils/parse.js +0 -1
- package/dist/src/utils/paths.js +0 -1
- package/dist/src/utils/platform.js +0 -1
- package/dist/src/utils/process-group.d.ts +33 -0
- package/dist/src/utils/process-group.js +161 -0
- package/dist/src/utils/process.d.ts +48 -4
- package/dist/src/utils/process.js +258 -59
- package/dist/src/utils/regex.d.ts +13 -3
- package/dist/src/utils/regex.js +53 -4
- package/dist/src/utils/validation.d.ts +0 -12
- package/dist/src/utils/validation.js +13 -26
- package/package.json +9 -8
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor check for PRE-EXISTING orphaned test-harness workers (0.37.0
|
|
3
|
+
* item 9b).
|
|
4
|
+
*
|
|
5
|
+
* Field incident: a mach invocation that died at harness startup stranded a
|
|
6
|
+
* Python 3.14 `multiprocessing` spawn worker plus its resource tracker; the
|
|
7
|
+
* worker reparented to launchd (PPID 1) and busy-spun at 100% CPU for ~26
|
|
8
|
+
* days (~415 CPU-hours) undetected. The dispatch-side fix is the
|
|
9
|
+
* process-group reaping in `utils/process.ts`; this check covers orphans
|
|
10
|
+
* that predate it (or leaked from non-FireForge invocations), using the
|
|
11
|
+
* detection recipe that found the incident process: PPID 1 + large
|
|
12
|
+
* accumulated CPU TIME + a `multiprocessing.spawn`/`resource_tracker`
|
|
13
|
+
* command line.
|
|
14
|
+
*
|
|
15
|
+
* Report-only by design: FireForge never kills pre-existing processes it
|
|
16
|
+
* did not spawn — the check names each candidate and suggests the kill.
|
|
17
|
+
*
|
|
18
|
+
* Canary integration was deliberately skipped: the friction log asked for
|
|
19
|
+
* doctor AND/OR `test --canary`, and adding a system-wide process scan to
|
|
20
|
+
* the canary hot path buys no extra coverage over the doctor check.
|
|
21
|
+
*/
|
|
22
|
+
import type { DoctorCheckDefinition } from './doctor-check-core.js';
|
|
23
|
+
/** One process matching the orphaned-harness-worker shape. */
|
|
24
|
+
export interface OrphanedHarnessWorker {
|
|
25
|
+
pid: number;
|
|
26
|
+
ppid: number;
|
|
27
|
+
/** Raw TIME column as `ps` printed it. */
|
|
28
|
+
cpuTime: string;
|
|
29
|
+
/** Parsed accumulated CPU seconds. */
|
|
30
|
+
cpuSeconds: number;
|
|
31
|
+
command: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parses a `ps` TIME value in either dialect:
|
|
35
|
+
* - linux `[dd-]hh:mm:ss` (e.g. `26-03:14:12`, `03:14:12`)
|
|
36
|
+
* - darwin/BSD `mm:ss.cc` with cumulative minutes (e.g. `38412:07.55`)
|
|
37
|
+
* Returns NaN for unrecognized shapes.
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseCpuTime(time: string): number;
|
|
40
|
+
/**
|
|
41
|
+
* Scans `ps -axo pid=,ppid=,time=,command=` output for orphaned harness
|
|
42
|
+
* workers: PPID 1 (reparented to init/launchd), a multiprocessing
|
|
43
|
+
* worker/tracker command line, and at least `minCpuSeconds` of accumulated
|
|
44
|
+
* CPU time. Pure — fixture-testable without spawning anything.
|
|
45
|
+
*/
|
|
46
|
+
export declare function findOrphanedHarnessWorkers(psOutput: string, minCpuSeconds?: number): OrphanedHarnessWorker[];
|
|
47
|
+
/**
|
|
48
|
+
* Doctor check reporting orphaned harness workers. Windows has no `ps`;
|
|
49
|
+
* the check is skipped there (best-effort platform gap, matching the
|
|
50
|
+
* process-group reaper's POSIX-only sweep).
|
|
51
|
+
*/
|
|
52
|
+
export declare const ORPHANED_HARNESS_DOCTOR_CHECK: DoctorCheckDefinition;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Doctor check for PRE-EXISTING orphaned test-harness workers (0.37.0
|
|
4
|
+
* item 9b).
|
|
5
|
+
*
|
|
6
|
+
* Field incident: a mach invocation that died at harness startup stranded a
|
|
7
|
+
* Python 3.14 `multiprocessing` spawn worker plus its resource tracker; the
|
|
8
|
+
* worker reparented to launchd (PPID 1) and busy-spun at 100% CPU for ~26
|
|
9
|
+
* days (~415 CPU-hours) undetected. The dispatch-side fix is the
|
|
10
|
+
* process-group reaping in `utils/process.ts`; this check covers orphans
|
|
11
|
+
* that predate it (or leaked from non-FireForge invocations), using the
|
|
12
|
+
* detection recipe that found the incident process: PPID 1 + large
|
|
13
|
+
* accumulated CPU TIME + a `multiprocessing.spawn`/`resource_tracker`
|
|
14
|
+
* command line.
|
|
15
|
+
*
|
|
16
|
+
* Report-only by design: FireForge never kills pre-existing processes it
|
|
17
|
+
* did not spawn — the check names each candidate and suggests the kill.
|
|
18
|
+
*
|
|
19
|
+
* Canary integration was deliberately skipped: the friction log asked for
|
|
20
|
+
* doctor AND/OR `test --canary`, and adding a system-wide process scan to
|
|
21
|
+
* the canary hot path buys no extra coverage over the doctor check.
|
|
22
|
+
*/
|
|
23
|
+
import { exec } from '../utils/process.js';
|
|
24
|
+
import { ok, warning } from './doctor-check-core.js';
|
|
25
|
+
/**
|
|
26
|
+
* Command-line shapes of Python multiprocessing helper processes: the
|
|
27
|
+
* spawn/forkserver worker bootstrap (`from multiprocessing.spawn import
|
|
28
|
+
* spawn_main; spawn_main(...)`) and the resource tracker.
|
|
29
|
+
*/
|
|
30
|
+
const HARNESS_WORKER_COMMAND_PATTERN = /multiprocessing\.(?:spawn|forkserver)\b|multiprocessing\.resource_tracker|from multiprocessing\.\w+ import/;
|
|
31
|
+
/** Minimum accumulated CPU time before a match is reported (10 minutes). */
|
|
32
|
+
const DEFAULT_MIN_CPU_SECONDS = 600;
|
|
33
|
+
/**
|
|
34
|
+
* Parses a `ps` TIME value in either dialect:
|
|
35
|
+
* - linux `[dd-]hh:mm:ss` (e.g. `26-03:14:12`, `03:14:12`)
|
|
36
|
+
* - darwin/BSD `mm:ss.cc` with cumulative minutes (e.g. `38412:07.55`)
|
|
37
|
+
* Returns NaN for unrecognized shapes.
|
|
38
|
+
*/
|
|
39
|
+
export function parseCpuTime(time) {
|
|
40
|
+
const trimmed = time.trim();
|
|
41
|
+
let days = 0;
|
|
42
|
+
let rest = trimmed;
|
|
43
|
+
const dayMatch = /^(\d+)-(.*)$/.exec(trimmed);
|
|
44
|
+
if (dayMatch) {
|
|
45
|
+
days = Number(dayMatch[1]);
|
|
46
|
+
rest = dayMatch[2] ?? '';
|
|
47
|
+
}
|
|
48
|
+
const parts = rest.split(':');
|
|
49
|
+
if (parts.length === 3) {
|
|
50
|
+
// hh:mm:ss (linux; seconds may not carry fractions but tolerate them)
|
|
51
|
+
const [h, m, s] = parts.map(Number);
|
|
52
|
+
if ([h, m, s].some((n) => Number.isNaN(n)))
|
|
53
|
+
return NaN;
|
|
54
|
+
return days * 86400 + (h ?? 0) * 3600 + (m ?? 0) * 60 + (s ?? 0);
|
|
55
|
+
}
|
|
56
|
+
if (parts.length === 2) {
|
|
57
|
+
// mm:ss.cc (darwin — the minutes field accumulates without wrapping)
|
|
58
|
+
const [m, s] = parts.map(Number);
|
|
59
|
+
if ([m, s].some((n) => Number.isNaN(n)))
|
|
60
|
+
return NaN;
|
|
61
|
+
return days * 86400 + (m ?? 0) * 60 + (s ?? 0);
|
|
62
|
+
}
|
|
63
|
+
return NaN;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Scans `ps -axo pid=,ppid=,time=,command=` output for orphaned harness
|
|
67
|
+
* workers: PPID 1 (reparented to init/launchd), a multiprocessing
|
|
68
|
+
* worker/tracker command line, and at least `minCpuSeconds` of accumulated
|
|
69
|
+
* CPU time. Pure — fixture-testable without spawning anything.
|
|
70
|
+
*/
|
|
71
|
+
export function findOrphanedHarnessWorkers(psOutput, minCpuSeconds = DEFAULT_MIN_CPU_SECONDS) {
|
|
72
|
+
const workers = [];
|
|
73
|
+
for (const line of psOutput.split('\n')) {
|
|
74
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.+)$/.exec(line);
|
|
75
|
+
if (!match)
|
|
76
|
+
continue;
|
|
77
|
+
const pid = Number(match[1]);
|
|
78
|
+
const ppid = Number(match[2]);
|
|
79
|
+
const cpuTime = match[3] ?? '';
|
|
80
|
+
const command = (match[4] ?? '').trim();
|
|
81
|
+
if (ppid !== 1)
|
|
82
|
+
continue;
|
|
83
|
+
if (!HARNESS_WORKER_COMMAND_PATTERN.test(command))
|
|
84
|
+
continue;
|
|
85
|
+
const cpuSeconds = parseCpuTime(cpuTime);
|
|
86
|
+
if (Number.isNaN(cpuSeconds) || cpuSeconds < minCpuSeconds)
|
|
87
|
+
continue;
|
|
88
|
+
workers.push({ pid, ppid, cpuTime, cpuSeconds, command });
|
|
89
|
+
}
|
|
90
|
+
return workers;
|
|
91
|
+
}
|
|
92
|
+
/** Thin exec wrapper for the process listing (tests mock `exec` instead). */
|
|
93
|
+
async function listSystemProcesses() {
|
|
94
|
+
const result = await exec('ps', ['-axo', 'pid=,ppid=,time=,command='], { timeout: 10000 });
|
|
95
|
+
if (result.exitCode !== 0) {
|
|
96
|
+
throw new Error(`ps exited ${String(result.exitCode)}`);
|
|
97
|
+
}
|
|
98
|
+
return result.stdout;
|
|
99
|
+
}
|
|
100
|
+
const CHECK_NAME = 'Orphaned harness workers';
|
|
101
|
+
async function runOrphanedHarnessCheck() {
|
|
102
|
+
let psOutput;
|
|
103
|
+
try {
|
|
104
|
+
psOutput = await listSystemProcesses();
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
return warning(CHECK_NAME, `Could not scan system processes (${error instanceof Error ? error.message : String(error)}); skipping the orphaned-worker check.`);
|
|
108
|
+
}
|
|
109
|
+
const orphans = findOrphanedHarnessWorkers(psOutput);
|
|
110
|
+
if (orphans.length === 0) {
|
|
111
|
+
return ok(CHECK_NAME);
|
|
112
|
+
}
|
|
113
|
+
const rows = orphans
|
|
114
|
+
.map((w) => `PID ${String(w.pid)} (CPU time ${w.cpuTime}): ${w.command.slice(0, 160)}`)
|
|
115
|
+
.join('; ');
|
|
116
|
+
const pids = orphans.map((w) => String(w.pid)).join(' ');
|
|
117
|
+
return warning(CHECK_NAME, `Found ${String(orphans.length)} orphaned Python multiprocessing worker(s) — PPID 1 with ` +
|
|
118
|
+
`high accumulated CPU time, the shape a test harness that died at startup leaves behind ` +
|
|
119
|
+
`(field incident: one such worker busy-spun for ~26 days). ${rows}`, `These look like workers orphaned by a crashed test harness. Verify each command line, ` +
|
|
120
|
+
`then terminate with: kill ${pids} (or kill -9 if a process survives). ` +
|
|
121
|
+
'FireForge never kills pre-existing processes automatically.');
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Doctor check reporting orphaned harness workers. Windows has no `ps`;
|
|
125
|
+
* the check is skipped there (best-effort platform gap, matching the
|
|
126
|
+
* process-group reaper's POSIX-only sweep).
|
|
127
|
+
*/
|
|
128
|
+
export const ORPHANED_HARNESS_DOCTOR_CHECK = {
|
|
129
|
+
name: CHECK_NAME,
|
|
130
|
+
skipIf: () => process.platform === 'win32',
|
|
131
|
+
run: runOrphanedHarnessCheck,
|
|
132
|
+
};
|
|
@@ -94,4 +94,3 @@ export async function inspectEngineWorkingTree(ctx) {
|
|
|
94
94
|
: '';
|
|
95
95
|
return warning('Engine working tree', `Engine working tree has ${counts.unmanaged} unmanaged change${counts.unmanaged === 1 ? '' : 's'}.${managedTail}`, 'Use "fireforge status --ownership" to separate patch-backed from unmanaged files, then export, discard, or reset only the unmanaged set.');
|
|
96
96
|
}
|
|
97
|
-
//# sourceMappingURL=doctor-working-tree.js.map
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { Command } from 'commander';
|
|
1
3
|
import { configExists, getProjectPaths, loadConfig, loadState, updateState, } from '../core/config.js';
|
|
2
4
|
import { furnaceConfigExists as checkFurnaceConfigExists } from '../core/furnace-config.js';
|
|
3
5
|
import { getCurrentBranch, getHead, isGitRepository, isMissingHeadError } from '../core/git.js';
|
|
@@ -12,7 +14,9 @@ import { error, info, intro, outro, success, warn } from '../utils/logger.js';
|
|
|
12
14
|
import { findExecutable } from '../utils/process.js';
|
|
13
15
|
import { POST_REBASE_AUDIT_CHECK } from './doctor/post-rebase-audit.js';
|
|
14
16
|
import { failure, ok, warning } from './doctor-check-core.js';
|
|
17
|
+
import { EXTERNAL_TOOLCHAIN_DOCTOR_CHECK } from './doctor-external-toolchains.js';
|
|
15
18
|
import { FURNACE_DOCTOR_CHECKS } from './doctor-furnace.js';
|
|
19
|
+
import { ORPHANED_HARNESS_DOCTOR_CHECK } from './doctor-orphaned-harness.js';
|
|
16
20
|
import { inspectEngineWorkingTree } from './doctor-working-tree.js';
|
|
17
21
|
import { collectPatchQueueHealth } from './verify.js';
|
|
18
22
|
/**
|
|
@@ -173,6 +177,7 @@ const DOCTOR_CHECKS = [
|
|
|
173
177
|
},
|
|
174
178
|
fix: 'Check fireforge.json for syntax errors or missing fields',
|
|
175
179
|
},
|
|
180
|
+
EXTERNAL_TOOLCHAIN_DOCTOR_CHECK,
|
|
176
181
|
{
|
|
177
182
|
name: 'Engine directory exists',
|
|
178
183
|
run: (ctx) => {
|
|
@@ -260,6 +265,7 @@ const DOCTOR_CHECKS = [
|
|
|
260
265
|
return warning('Watchman available', 'watchman is not installed or not on PATH. "fireforge watch" requires it.', 'Install watchman (brew install watchman / dnf install watchman / https://facebook.github.io/watchman/), then re-run doctor.');
|
|
261
266
|
},
|
|
262
267
|
},
|
|
268
|
+
ORPHANED_HARNESS_DOCTOR_CHECK,
|
|
263
269
|
{
|
|
264
270
|
name: 'Patches directory exists',
|
|
265
271
|
run: async (ctx) => {
|
|
@@ -476,4 +482,3 @@ export function registerDoctor(program, { getProjectRoot, withErrorHandling }) {
|
|
|
476
482
|
}
|
|
477
483
|
}));
|
|
478
484
|
}
|
|
479
|
-
//# sourceMappingURL=doctor.js.map
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { rename } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { Command } from 'commander';
|
|
5
6
|
import { getProjectPaths, loadConfig, loadState, updateState } from '../core/config.js';
|
|
6
7
|
import { withFileLock } from '../core/file-lock.js';
|
|
7
|
-
import { downloadFirefoxSource, formatBytes } from '../core/firefox.js';
|
|
8
|
-
import {
|
|
8
|
+
import { downloadFirefoxSource, formatBytes, sweepOrphanedEngineWorkDirs, } from '../core/firefox.js';
|
|
9
|
+
import { clearAppliedFurnaceState } from '../core/furnace-config.js';
|
|
9
10
|
import { getHead, initRepository, isGitRepository, isMissingHeadError, resumeRepository, } from '../core/git.js';
|
|
10
11
|
import { restoreTrackedPath } from '../core/git-file-ops.js';
|
|
11
12
|
import { getDirtyFiles } from '../core/git-status.js';
|
|
@@ -116,15 +117,8 @@ function closeRestoreSpinner(restoreSpinner, result) {
|
|
|
116
117
|
}
|
|
117
118
|
async function clearStaleFurnaceApplyState(projectRoot) {
|
|
118
119
|
// --force installs a new baseCommit, which invalidates every applied
|
|
119
|
-
// checksum in furnace-state.json.
|
|
120
|
-
|
|
121
|
-
// should survive an engine refresh.
|
|
122
|
-
const furnacePaths = getFurnacePaths(projectRoot);
|
|
123
|
-
if (await pathExists(furnacePaths.furnaceState)) {
|
|
124
|
-
await updateFurnaceState(projectRoot, (current) => ({
|
|
125
|
-
...(current.pendingRepair ? { pendingRepair: current.pendingRepair } : {}),
|
|
126
|
-
}));
|
|
127
|
-
}
|
|
120
|
+
// checksum in furnace-state.json.
|
|
121
|
+
await clearAppliedFurnaceState(projectRoot);
|
|
128
122
|
}
|
|
129
123
|
async function activateReplacementEngine(args) {
|
|
130
124
|
const { engineDir, replacementDir, backupDir } = args;
|
|
@@ -285,7 +279,29 @@ export async function downloadCommand(projectRoot, options) {
|
|
|
285
279
|
const previousVersion = (await loadState(projectRoot)).downloadedVersion;
|
|
286
280
|
info(`Firefox version: ${version}`);
|
|
287
281
|
await checkDiskSpace(projectRoot, 5 * 1024 * 1024 * 1024, warn);
|
|
282
|
+
// A legitimate holder of this lock runs for 10+ minutes (download +
|
|
283
|
+
// extract + git indexing). The previous default 30 s timeout failed the
|
|
284
|
+
// second invocation with "remove the lock directory if it is stale" advice
|
|
285
|
+
// while the lock was actively held — dangerous guidance mid-extraction.
|
|
286
|
+
// Wait generously instead: a dead holder is reaped within seconds by the
|
|
287
|
+
// PID-based stale probe, so a long timeout only ever waits on real work.
|
|
288
|
+
const downloadLockOptions = {
|
|
289
|
+
timeoutMs: 30 * 60_000,
|
|
290
|
+
onTimeoutMessage: 'Timed out waiting for another FireForge download to finish. ' +
|
|
291
|
+
'If no other `fireforge download` is running, remove .fireforge/download.fireforge.lock and retry.',
|
|
292
|
+
onStaleLockMessage: (ageMs) => `Removing download lock left behind by a crashed run (${Math.round(ageMs / 1000)}s old).`,
|
|
293
|
+
};
|
|
288
294
|
await withFileLock(join(paths.fireforgeDir, 'download.fireforge.lock'), async () => {
|
|
295
|
+
// Reclaim multi-GB partial trees left behind by interrupted runs.
|
|
296
|
+
// Safe under the download lock: any `.tmp-*`/`.replacement-*` dir
|
|
297
|
+
// present now is orphaned, since live ones only exist while a
|
|
298
|
+
// download holds this lock. `.backup-*` dirs are NOT swept — a
|
|
299
|
+
// backup can hold the previous engine after a failed forced
|
|
300
|
+
// replacement and is the operator's recovery copy.
|
|
301
|
+
const sweptDirs = await sweepOrphanedEngineWorkDirs(paths.engine);
|
|
302
|
+
if (sweptDirs.length > 0) {
|
|
303
|
+
info(`Removed ${sweptDirs.length} orphaned working director${sweptDirs.length === 1 ? 'y' : 'ies'} from interrupted downloads.`);
|
|
304
|
+
}
|
|
289
305
|
let installEngineDir = paths.engine;
|
|
290
306
|
let replacementEngineDir;
|
|
291
307
|
let backupEngineDir;
|
|
@@ -413,7 +429,7 @@ export async function downloadCommand(projectRoot, options) {
|
|
|
413
429
|
replacementActivated,
|
|
414
430
|
...(backupEngineDir !== undefined ? { backupEngineDir } : {}),
|
|
415
431
|
});
|
|
416
|
-
});
|
|
432
|
+
}, downloadLockOptions);
|
|
417
433
|
}
|
|
418
434
|
/** Registers the download command on the CLI program. */
|
|
419
435
|
export function registerDownload(program, { getProjectRoot, withErrorHandling }) {
|
|
@@ -425,4 +441,3 @@ export function registerDownload(program, { getProjectRoot, withErrorHandling })
|
|
|
425
441
|
await downloadCommand(getProjectRoot(), pickDefined(options));
|
|
426
442
|
}));
|
|
427
443
|
}
|
|
428
|
-
//# sourceMappingURL=download.js.map
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { Command } from 'commander';
|
|
1
3
|
import { isBrandingManagedPath } from '../core/branding.js';
|
|
2
4
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
3
5
|
import { collectFurnaceManagedPrefixes, furnaceConfigExists, loadFurnaceConfig, } from '../core/furnace-config.js';
|
|
@@ -228,8 +230,9 @@ export async function exportAllCommand(projectRoot, options = {}) {
|
|
|
228
230
|
await checkDanglingFurnaceRegistrations(projectRoot, diff, furnaceExcluded);
|
|
229
231
|
// Check for non-interactive mode
|
|
230
232
|
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
231
|
-
// Auto-fix missing license headers on new files (interactive only
|
|
232
|
-
|
|
233
|
+
// Auto-fix missing license headers on new files (interactive only;
|
|
234
|
+
// report-only under --dry-run so the preview never mutates engine/)
|
|
235
|
+
const headersAdded = await autoFixLicenseHeaders(paths.engine, diff, config, isInteractive, isDryRun);
|
|
233
236
|
if (headersAdded) {
|
|
234
237
|
diff = await getAllDiff(paths.engine);
|
|
235
238
|
}
|
|
@@ -335,4 +338,3 @@ export function registerExportAll(program, { getProjectRoot, withErrorHandling }
|
|
|
335
338
|
});
|
|
336
339
|
}));
|
|
337
340
|
}
|
|
338
|
-
//# sourceMappingURL=export-all.js.map
|
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
* testable without dragging the whole command harness along for the ride.
|
|
9
9
|
*/
|
|
10
10
|
import { join } from 'node:path';
|
|
11
|
-
import {
|
|
11
|
+
import {} from '../core/destructive.js';
|
|
12
|
+
import { normalizePatchArtifact } from '../core/patch-artifact-normalize.js';
|
|
13
|
+
import { findAllPatchesForFilesWithDetails, patchNameSlug, planExport, } from '../core/patch-export.js';
|
|
12
14
|
import { buildModifiedFileAdditionsFromDiff, buildPatchQueueContext, detectNewFilesInDiff, lintPatchQueue, } from '../core/patch-lint.js';
|
|
13
15
|
import { withPatchDirectoryLock } from '../core/patch-lock.js';
|
|
14
|
-
import { addPatchToManifest, loadPatchesManifest, renumberPatchesInManifest, resolvePatchIdentifier, savePatchesManifest, } from '../core/patch-manifest.js';
|
|
16
|
+
import { addPatchToManifest, loadPatchesManifest, loadPatchesManifestForWrite, renumberPatchesInManifest, resolvePatchIdentifier, savePatchesManifest, } from '../core/patch-manifest.js';
|
|
15
17
|
import { applyRenameMapToManifest, buildProjectedManifest, enforcePatchPolicy, } from '../core/patch-policy.js';
|
|
16
18
|
import { extractNewFileContentFromDiff } from '../core/patch-transform.js';
|
|
17
19
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
@@ -22,7 +24,7 @@ import { assertPlacementPreservesReservedRanges } from './export-placement-polic
|
|
|
22
24
|
import { findPartialOwnershipOverlap } from './export-shared.js';
|
|
23
25
|
function buildFilenameForPlacement(category, name, order, width) {
|
|
24
26
|
const padded = String(order).padStart(Math.max(3, width), '0');
|
|
25
|
-
return `${padded}-${category}-${
|
|
27
|
+
return `${padded}-${category}-${patchNameSlug(name, category)}.patch`;
|
|
26
28
|
}
|
|
27
29
|
function prefixWidthForPatches(manifestPatches, requestedOrder) {
|
|
28
30
|
return manifestPatches.reduce((width, patch) => {
|
|
@@ -113,7 +115,9 @@ export function computeExactPlacementPlan(manifestPatches, newPatchCategory, new
|
|
|
113
115
|
* Resolves a placement plan from CLI flags against the current manifest.
|
|
114
116
|
*/
|
|
115
117
|
export async function resolvePlacementPlan(patchesDir, options, category, name) {
|
|
116
|
-
|
|
118
|
+
// ForWrite: placement planning feeds a manifest rewrite; a corrupt
|
|
119
|
+
// manifest read as empty would allocate colliding orders.
|
|
120
|
+
const manifest = await loadPatchesManifestForWrite(patchesDir);
|
|
117
121
|
const existingPatches = manifest?.patches ?? [];
|
|
118
122
|
let targetOrder;
|
|
119
123
|
if (options.order !== undefined) {
|
|
@@ -232,7 +236,7 @@ export async function commitPlacementExport(input) {
|
|
|
232
236
|
're-run the export with an explicit file list that leaves those files with their owner — ' +
|
|
233
237
|
'do NOT bypass with --force-unsafe. Pass --force-unsafe only for a reviewed placement conflict.', '--force-unsafe');
|
|
234
238
|
}
|
|
235
|
-
const originalManifest = await
|
|
239
|
+
const originalManifest = await loadPatchesManifestForWrite(input.patchesDir);
|
|
236
240
|
if (originalManifest !== null) {
|
|
237
241
|
assertPlacementPreservesReservedRanges(currentPlan, originalManifest.patches, input.config, input.category);
|
|
238
242
|
}
|
|
@@ -269,7 +273,9 @@ export async function commitPlacementExport(input) {
|
|
|
269
273
|
await renumberPatchesInManifest(input.patchesDir, currentPlan.renameMap);
|
|
270
274
|
renumberApplied = true;
|
|
271
275
|
}
|
|
272
|
-
|
|
276
|
+
// Normalize identically to commitExportedPatch — the two export
|
|
277
|
+
// paths must produce one artifact contract for the same diff.
|
|
278
|
+
await writeText(patchPath, normalizePatchArtifact(input.diff));
|
|
273
279
|
await addPatchToManifest(input.patchesDir, {
|
|
274
280
|
...input.metadata,
|
|
275
281
|
filename: currentPlan.newFilename,
|
|
@@ -414,4 +420,3 @@ export async function renderDryRunPreview(input) {
|
|
|
414
420
|
info('[dry-run] No cross-patch ownership overlap detected.');
|
|
415
421
|
}
|
|
416
422
|
}
|
|
417
|
-
//# sourceMappingURL=export-flow.js.map
|
|
@@ -56,9 +56,13 @@ export declare function confirmSupersedePatches(patchesDir: string, filesAffecte
|
|
|
56
56
|
* @param diffContent - Current unified diff
|
|
57
57
|
* @param config - Project configuration
|
|
58
58
|
* @param isInteractive - Whether interactive prompts are available
|
|
59
|
+
* @param dryRun - When true, only REPORT missing headers, never prompt or
|
|
60
|
+
* write. Dry-run must stay read-only: before this flag existed, an
|
|
61
|
+
* interactive `export --dry-run` prompted (default Yes) and wrote license
|
|
62
|
+
* headers into engine/ files, then closed with "no changes made".
|
|
59
63
|
* @returns true if files were modified on disk (caller must regenerate diff)
|
|
60
64
|
*/
|
|
61
|
-
export declare function autoFixLicenseHeaders(engineDir: string, diffContent: string, config: FireForgeConfig, isInteractive: boolean): Promise<boolean>;
|
|
65
|
+
export declare function autoFixLicenseHeaders(engineDir: string, diffContent: string, config: FireForgeConfig, isInteractive: boolean, dryRun?: boolean): Promise<boolean>;
|
|
62
66
|
/**
|
|
63
67
|
* Maps every file in `filesAffected` to the existing patches that already
|
|
64
68
|
* claim ownership of it, excluding the caller's own patch (when `newFilename`
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { confirm, select, text } from '@clack/prompts';
|
|
4
|
-
import { addLicenseHeaderToFile
|
|
4
|
+
import { addLicenseHeaderToFile } from '../core/license-headers.js';
|
|
5
5
|
import { findAllPatchesForFiles } from '../core/patch-export.js';
|
|
6
|
-
import { commentStyleForFile, detectNewFilesInDiff, lintExportedPatch, resolvePatchSizeTier, } from '../core/patch-lint.js';
|
|
6
|
+
import { commentStyleForFile, detectNewFilesInDiff, isAcceptableNewFileHeader, lintExportedPatch, resolvePatchSizeTier, } from '../core/patch-lint.js';
|
|
7
7
|
import { resolvePatchOwnedSysMjs } from '../core/patch-lint-ownership.js';
|
|
8
8
|
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
9
9
|
import { getPatchPolicyCategories, isCategoryAllowedByConfig } from '../core/patch-policy.js';
|
|
@@ -206,9 +206,13 @@ export async function confirmSupersedePatches(patchesDir, filesAffected, superse
|
|
|
206
206
|
* @param diffContent - Current unified diff
|
|
207
207
|
* @param config - Project configuration
|
|
208
208
|
* @param isInteractive - Whether interactive prompts are available
|
|
209
|
+
* @param dryRun - When true, only REPORT missing headers, never prompt or
|
|
210
|
+
* write. Dry-run must stay read-only: before this flag existed, an
|
|
211
|
+
* interactive `export --dry-run` prompted (default Yes) and wrote license
|
|
212
|
+
* headers into engine/ files, then closed with "no changes made".
|
|
209
213
|
* @returns true if files were modified on disk (caller must regenerate diff)
|
|
210
214
|
*/
|
|
211
|
-
export async function autoFixLicenseHeaders(engineDir, diffContent, config, isInteractive) {
|
|
215
|
+
export async function autoFixLicenseHeaders(engineDir, diffContent, config, isInteractive, dryRun = false) {
|
|
212
216
|
const license = config.license ?? 'MPL-2.0';
|
|
213
217
|
const newFiles = detectNewFilesInDiff(diffContent);
|
|
214
218
|
if (newFiles.size === 0)
|
|
@@ -222,13 +226,22 @@ export async function autoFixLicenseHeaders(engineDir, diffContent, config, isIn
|
|
|
222
226
|
if (!(await pathExists(filePath)))
|
|
223
227
|
continue;
|
|
224
228
|
const content = await readText(filePath);
|
|
225
|
-
|
|
226
|
-
|
|
229
|
+
// Same acceptance policy as the missing-license-header rule: offering
|
|
230
|
+
// to "fix" a file the lint already accepts (e.g. a verbatim upstream
|
|
231
|
+
// MPL block header on a derived JS/CSS file) would stack a second
|
|
232
|
+
// header on top of a legitimate one.
|
|
233
|
+
if (!isAcceptableNewFileHeader(file, content, style, license)) {
|
|
227
234
|
filesToFix.push(file);
|
|
228
235
|
}
|
|
229
236
|
}
|
|
230
237
|
if (filesToFix.length === 0)
|
|
231
238
|
return false;
|
|
239
|
+
if (dryRun) {
|
|
240
|
+
const fileList = filesToFix.map((f) => ` - ${f}`).join('\n');
|
|
241
|
+
info(`[dry-run] ${filesToFix.length} new file(s) missing the ${license} license header ` +
|
|
242
|
+
`(a real export would offer to add them):\n${fileList}`);
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
232
245
|
if (!isInteractive)
|
|
233
246
|
return false;
|
|
234
247
|
const fileList = filesToFix.map((f) => ` - ${f}`).join('\n');
|
|
@@ -360,4 +373,3 @@ export async function runSupersedeAndOverlapGates(args) {
|
|
|
360
373
|
s,
|
|
361
374
|
});
|
|
362
375
|
}
|
|
363
|
-
//# sourceMappingURL=export-shared.js.map
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { stat } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { Option } from 'commander';
|
|
4
|
+
import { Command, Option } from 'commander';
|
|
5
5
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
6
6
|
import { appendHistory } from '../core/destructive.js';
|
|
7
|
+
import { withEngineSessionLock } from '../core/engine-session-lock.js';
|
|
7
8
|
import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
|
|
9
|
+
import { enforceFreshFurnaceSources } from '../core/furnace-stale-export.js';
|
|
8
10
|
import { getStatusWithCodes, isGitRepository } from '../core/git.js';
|
|
9
11
|
import { generateBinaryFilePatch, generateFullFilePatch } from '../core/git-diff.js';
|
|
10
12
|
import { isBinaryFile } from '../core/git-file-ops.js';
|
|
@@ -18,7 +20,7 @@ import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
|
18
20
|
import { toError } from '../utils/errors.js';
|
|
19
21
|
import { ensureDir, pathExists } from '../utils/fs.js';
|
|
20
22
|
import { info, intro, outro, spinner, verbose, warn } from '../utils/logger.js';
|
|
21
|
-
import { pickDefined } from '../utils/options.js';
|
|
23
|
+
import { commanderArgParser, pickDefined } from '../utils/options.js';
|
|
22
24
|
import { stripEnginePrefix } from '../utils/paths.js';
|
|
23
25
|
import { parsePositiveIntegerFlag } from '../utils/validation.js';
|
|
24
26
|
import { commitPlacementExport, renderDryRunPreview } from './export-flow.js';
|
|
@@ -200,6 +202,11 @@ async function prepareExport(projectRoot, files, options) {
|
|
|
200
202
|
throw new GeneralError(`Paths "${pathList}" have no changes to export.\n\n` +
|
|
201
203
|
'Run "fireforge status" to see modified files.');
|
|
202
204
|
}
|
|
205
|
+
// Stale-furnace-source gate (0.37.0 item 4): the export captures deployed
|
|
206
|
+
// engine copies; refuse (or warn under --allow-stale-furnace) when a
|
|
207
|
+
// covered component's source changed since the last furnace apply. Runs
|
|
208
|
+
// after --exclude-furnace so excluded furnace files never trigger it.
|
|
209
|
+
await enforceFreshFurnaceSources(projectRoot, allFiles, options.allowStaleFurnace === true, 'export');
|
|
203
210
|
let diff = await generatePatchDiff(paths.engine, allFiles);
|
|
204
211
|
if (!diff.trim()) {
|
|
205
212
|
if (options.skipLint) {
|
|
@@ -209,12 +216,16 @@ async function prepareExport(projectRoot, files, options) {
|
|
|
209
216
|
}
|
|
210
217
|
throw new GeneralError('The specified paths have no diff content to export.');
|
|
211
218
|
}
|
|
212
|
-
// Ensure patches directory exists
|
|
213
|
-
|
|
219
|
+
// Ensure patches directory exists. Skip during a dry-run so the command
|
|
220
|
+
// is purely read-only (matching export-all's dry-run contract).
|
|
221
|
+
if (!options.dryRun) {
|
|
222
|
+
await ensureDir(paths.patches);
|
|
223
|
+
}
|
|
214
224
|
const config = await loadConfig(projectRoot);
|
|
215
225
|
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
216
|
-
// Auto-fix missing license headers on new files (interactive only
|
|
217
|
-
|
|
226
|
+
// Auto-fix missing license headers on new files (interactive only;
|
|
227
|
+
// report-only under --dry-run so the preview never mutates engine/)
|
|
228
|
+
const headersAdded = await autoFixLicenseHeaders(paths.engine, diff, config, isInteractive, options.dryRun ?? false);
|
|
218
229
|
if (headersAdded) {
|
|
219
230
|
diff = await generatePatchDiff(paths.engine, allFiles);
|
|
220
231
|
}
|
|
@@ -398,23 +409,24 @@ export function registerExport(program, { getProjectRoot, withErrorHandling }) {
|
|
|
398
409
|
.option('--supersede', 'Allow superseding multiple existing patches')
|
|
399
410
|
.option('--skip-lint', 'Skip patch lint checks (downgrade errors to warnings)')
|
|
400
411
|
.option('--dry-run', 'Print the export plan (including supersede preview) without writing')
|
|
401
|
-
.addOption(new Option('--order <N>', 'Place the new patch at this exact unused order without renumbering existing patches').argParser((v) => parsePositiveIntegerFlag('--order', v)))
|
|
412
|
+
.addOption(new Option('--order <N>', 'Place the new patch at this exact unused order without renumbering existing patches').argParser(commanderArgParser((v) => parsePositiveIntegerFlag('--order', v))))
|
|
402
413
|
.option('--before <anchor>', 'Place the new patch immediately before <anchor>')
|
|
403
414
|
.option('--after <anchor>', 'Place the new patch immediately after <anchor>')
|
|
404
415
|
.option('-y, --yes', 'Skip confirmation for placement renumbers (required for non-TTY)')
|
|
405
416
|
.option('--force-unsafe', 'Bypass cross-patch lint refusal on projected placement')
|
|
406
417
|
.option('--exclude-furnace', 'Exclude furnace-managed file paths from the export')
|
|
418
|
+
.option('--allow-stale-furnace', 'Export the deployed engine copy even when the components/ source changed since the last furnace apply')
|
|
407
419
|
.option('--allow-overlap', 'Acknowledge cross-patch ownership overlap (default mode only; the resulting queue fails verify)')
|
|
408
420
|
.addOption(new Option('--tier <tier>', 'Force a tier override on the new patch (only "branding" recognised)').choices(['branding']))
|
|
409
421
|
.option('--lint-ignore <check-id>', 'Suppress a lint check on this patch (writes to PatchMetadata.lintIgnore; repeatable)', (value, prev) => [...prev, value], [])
|
|
410
422
|
.action(withErrorHandling(async (paths, options) => {
|
|
411
423
|
const { category, tier, lintIgnore, ...rest } = options;
|
|
412
|
-
|
|
424
|
+
const projectRoot = getProjectRoot();
|
|
425
|
+
await withEngineSessionLock(projectRoot, 'export', () => exportCommand(projectRoot, paths, {
|
|
413
426
|
...pickDefined(rest),
|
|
414
427
|
...(category !== undefined ? { category } : {}),
|
|
415
428
|
...(tier !== undefined ? { tier: tier } : {}),
|
|
416
429
|
...(lintIgnore !== undefined && lintIgnore.length > 0 ? { lintIgnore } : {}),
|
|
417
|
-
});
|
|
430
|
+
}));
|
|
418
431
|
}));
|
|
419
432
|
}
|
|
420
|
-
//# sourceMappingURL=export.js.map
|
|
@@ -6,6 +6,8 @@ import { logApplyResult } from '../../core/furnace-apply-output.js';
|
|
|
6
6
|
import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, } from '../../core/furnace-config.js';
|
|
7
7
|
import { isComponentSourceFile } from '../../core/furnace-constants.js';
|
|
8
8
|
import { runFurnaceMutation } from '../../core/furnace-operation.js';
|
|
9
|
+
import { getPersistableAppliedEntry, persistSingleComponentState, shouldPersistSingleComponentState, } from '../../core/furnace-state-persist.js';
|
|
10
|
+
import { countEntriesWithBlockingStepErrors } from '../../core/furnace-step-errors.js';
|
|
9
11
|
import { findOverrideBaseVersionDrift, formatOverrideBaseVersionDriftError, formatOverrideBaseVersionDriftWarning, } from '../../core/furnace-version-drift.js';
|
|
10
12
|
import { FurnaceError } from '../../errors/furnace.js';
|
|
11
13
|
import { pathExists } from '../../utils/fs.js';
|
|
@@ -282,17 +284,32 @@ export async function furnaceApplyCommand(projectRoot, name, options = {}) {
|
|
|
282
284
|
throw new FurnaceError(formatOverrideBaseVersionDriftError(driftEntries));
|
|
283
285
|
}
|
|
284
286
|
const applySpinner = dryRun ? undefined : spinner('Applying components to engine...');
|
|
285
|
-
const result = await runFurnaceMutation(projectRoot, 'apply-rollback', (ctx) =>
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
const result = await runFurnaceMutation(projectRoot, 'apply-rollback', async (ctx) => {
|
|
288
|
+
// `persistState: false` for a NAMED apply is load-bearing: the batch
|
|
289
|
+
// persist path replaces `appliedChecksums` wholesale with only this
|
|
290
|
+
// run's entries, and the batch loops filter to the named component —
|
|
291
|
+
// so routing a named apply through it persisted a state file
|
|
292
|
+
// containing ONLY that component, wiping every other component's
|
|
293
|
+
// checksums. Orphan detection and deleted-file undeploy both key on
|
|
294
|
+
// that state, so the wiped components' stale engine files became
|
|
295
|
+
// invisible to apply AND to `furnace validate`. Named apply merges
|
|
296
|
+
// per-component state below, exactly like `furnace deploy <name>`.
|
|
297
|
+
const applyResult = await applyAllComponents(projectRoot, dryRun, {
|
|
298
|
+
operationContext: ctx,
|
|
299
|
+
...(name !== undefined ? { componentName: name, persistState: false } : {}),
|
|
300
|
+
});
|
|
301
|
+
if (name !== undefined && shouldPersistSingleComponentState(applyResult, dryRun)) {
|
|
302
|
+
await persistSingleComponentState(projectRoot, getPersistableAppliedEntry('Apply', name, applyResult.applied[0]), getFurnacePaths(projectRoot));
|
|
303
|
+
}
|
|
304
|
+
return applyResult;
|
|
305
|
+
}, { dryRun });
|
|
289
306
|
if (applySpinner) {
|
|
290
307
|
applySpinner.stop('Components applied');
|
|
291
308
|
}
|
|
292
309
|
logApplyResult(result, dryRun);
|
|
293
310
|
const appliedWithStepErrorsCount = dryRun
|
|
294
311
|
? 0
|
|
295
|
-
: result.applied
|
|
312
|
+
: countEntriesWithBlockingStepErrors(result.applied);
|
|
296
313
|
const totalApplyFailures = result.errors.length + appliedWithStepErrorsCount;
|
|
297
314
|
if (totalApplyFailures > 0) {
|
|
298
315
|
throw new FurnaceError(`${totalApplyFailures} component${totalApplyFailures === 1 ? '' : 's'} failed to apply cleanly`);
|
|
@@ -310,4 +327,3 @@ export async function furnaceApplyCommand(projectRoot, name, options = {}) {
|
|
|
310
327
|
await runWatchLoop(projectRoot);
|
|
311
328
|
}
|
|
312
329
|
}
|
|
313
|
-
//# sourceMappingURL=apply.js.map
|