@hominis/fireforge 0.34.2 → 0.36.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 +48 -0
- package/README.md +43 -5
- package/dist/bin/fireforge.d.ts +4 -0
- package/dist/bin/fireforge.js +25 -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 +17 -3
- package/dist/src/commands/config.js +30 -10
- package/dist/src/commands/discard.js +24 -5
- 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-working-tree.js +0 -1
- package/dist/src/commands/doctor.js +4 -1
- package/dist/src/commands/download.js +51 -15
- 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 +15 -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.js +1 -1
- 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 +1 -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 +6 -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 +18 -6
- 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 +18 -2
- 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 +95 -56
- 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.js +0 -1
- package/dist/src/core/build-baseline.js +0 -1
- package/dist/src/core/build-prepare.js +21 -7
- 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 +67 -8
- package/dist/src/core/firefox-download.js +0 -1
- package/dist/src/core/firefox-extract.d.ts +21 -1
- package/dist/src/core/firefox-extract.js +178 -9
- 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 +0 -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-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 +0 -1
- 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 +11 -2
- 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 +65 -8
- package/dist/src/core/license-headers.d.ts +16 -0
- package/dist/src/core/license-headers.js +19 -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 +27 -1
- 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.js +0 -1
- 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 +152 -39
- 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 +0 -1
- 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 +48 -24
- 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 +16 -0
- package/dist/src/core/patch-manifest-io.js +37 -12
- package/dist/src/core/patch-manifest-query.d.ts +6 -0
- package/dist/src/core/patch-manifest-query.js +27 -12
- package/dist/src/core/patch-manifest-resolve.js +0 -1
- package/dist/src/core/patch-manifest-validate.js +0 -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 +64 -7
- package/dist/src/core/test-harness-crash.js +196 -15
- package/dist/src/core/test-harness-output.js +0 -1
- package/dist/src/core/test-path-scope.d.ts +62 -0
- package/dist/src/core/test-path-scope.js +148 -0
- package/dist/src/core/test-stale-check.js +0 -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 +85 -0
- package/dist/src/core/toolchain-preflight.js +256 -0
- package/dist/src/core/typecheck-shim.d.ts +4 -1
- package/dist/src/core/typecheck-shim.js +61 -18
- 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 +2 -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.js +0 -1
- package/dist/src/types/commands/options.d.ts +20 -5
- package/dist/src/types/commands/options.js +0 -1
- package/dist/src/types/commands/patches.d.ts +8 -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 +10 -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.d.ts +33 -4
- package/dist/src/utils/process.js +183 -28
- 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 +8 -7
|
@@ -48,6 +48,14 @@ export declare function probeMarionettePort(port?: number): Promise<MarionettePo
|
|
|
48
48
|
export declare function assertMarionettePortAvailable(port?: number, options?: {
|
|
49
49
|
binaryName?: string;
|
|
50
50
|
}): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Ensures the Marionette port is usable, optionally terminating a stale
|
|
53
|
+
* Firefox-family browser holder first. Unrelated listeners still fail.
|
|
54
|
+
*/
|
|
55
|
+
export declare function ensureMarionettePortAvailable(port?: number, options?: {
|
|
56
|
+
binaryName?: string;
|
|
57
|
+
killStaleBrowser?: boolean;
|
|
58
|
+
}): Promise<void>;
|
|
51
59
|
/**
|
|
52
60
|
* Extracts a `--marionette-port=N` (or `--marionette-port N`) argument from
|
|
53
61
|
* a list of forwarded mach args, if present. Used so an operator passing the
|
|
@@ -56,7 +56,7 @@ const BROWSER_BASENAMES = new Set([
|
|
|
56
56
|
* so a fork that ships under a custom name (e.g.
|
|
57
57
|
* `mybrowser-nightly`) is still recognised as a browser.
|
|
58
58
|
*/
|
|
59
|
-
function
|
|
59
|
+
function isBrowserMarionettePortHolder(holder, binaryName) {
|
|
60
60
|
if (/\s-marionette(?:\s|$)/.test(holder.commandLine)) {
|
|
61
61
|
return true;
|
|
62
62
|
}
|
|
@@ -195,7 +195,7 @@ export async function assertMarionettePortAvailable(port = DEFAULT_MARIONETTE_PO
|
|
|
195
195
|
if (!probe.inUse || !probe.holder)
|
|
196
196
|
return;
|
|
197
197
|
const holder = probe.holder;
|
|
198
|
-
if (
|
|
198
|
+
if (isBrowserMarionettePortHolder(holder, options.binaryName)) {
|
|
199
199
|
const killHint = process.platform === 'win32'
|
|
200
200
|
? `Stop-Process -Id ${holder.pid} -Force`
|
|
201
201
|
: `kill ${holder.pid} # or "kill -9 ${holder.pid}" if it doesn't exit`;
|
|
@@ -212,6 +212,38 @@ export async function assertMarionettePortAvailable(port = DEFAULT_MARIONETTE_PO
|
|
|
212
212
|
throw new GeneralError(`Marionette port ${port} is already in use by ${holder.command} (PID ${holder.pid}). ` +
|
|
213
213
|
`This is not a FireForge-launched browser; stop the holder process or free the port before rerunning.`);
|
|
214
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Ensures the Marionette port is usable, optionally terminating a stale
|
|
217
|
+
* Firefox-family browser holder first. Unrelated listeners still fail.
|
|
218
|
+
*/
|
|
219
|
+
export async function ensureMarionettePortAvailable(port = DEFAULT_MARIONETTE_PORT, options = {}) {
|
|
220
|
+
const probe = await probeMarionettePort(port);
|
|
221
|
+
if (!probe.inUse || !probe.holder)
|
|
222
|
+
return;
|
|
223
|
+
const holder = probe.holder;
|
|
224
|
+
const isBrowser = isBrowserMarionettePortHolder(holder, options.binaryName);
|
|
225
|
+
if (!options.killStaleBrowser || !isBrowser) {
|
|
226
|
+
await assertMarionettePortAvailable(port, options);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
if (process.platform === 'win32') {
|
|
231
|
+
await exec('powershell.exe', [
|
|
232
|
+
'-NoProfile',
|
|
233
|
+
'-NonInteractive',
|
|
234
|
+
'-Command',
|
|
235
|
+
`Stop-Process -Id ${holder.pid} -Force`,
|
|
236
|
+
]);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
process.kill(holder.pid, 'SIGTERM');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
throw new GeneralError(`Marionette port ${port} is held by stale browser ${holder.command} (PID ${holder.pid}), ` +
|
|
244
|
+
`but FireForge could not terminate it: ${error instanceof Error ? error.message : String(error)}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
215
247
|
/**
|
|
216
248
|
* Extracts a `--marionette-port=N` (or `--marionette-port N`) argument from
|
|
217
249
|
* a list of forwarded mach args, if present. Used so an operator passing the
|
|
@@ -350,4 +382,3 @@ export function isMarionetteFlavor(testPaths, machArgs) {
|
|
|
350
382
|
}
|
|
351
383
|
return false;
|
|
352
384
|
}
|
|
353
|
-
//# sourceMappingURL=marionette-port.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { info } from '../utils/logger.js';
|
|
2
|
+
import { buildPatchClaims } from './status-classify.js';
|
|
2
3
|
/**
|
|
3
4
|
* Builds the flat ownership table from the manifest, worktree status, and
|
|
4
5
|
* the duplicate-new-file-creation map produced by cross-patch lint.
|
|
@@ -29,14 +30,7 @@ import { info } from '../utils/logger.js';
|
|
|
29
30
|
* paths with a `.length > 1` owner list become duplicate-create conflicts
|
|
30
31
|
*/
|
|
31
32
|
export function buildOwnershipTable(manifestPatches, worktreeFiles, newFileCreatorsByPath, classifications = new Map()) {
|
|
32
|
-
const ownersByPath =
|
|
33
|
-
for (const patch of manifestPatches) {
|
|
34
|
-
for (const file of patch.filesAffected) {
|
|
35
|
-
const existing = ownersByPath.get(file) ?? [];
|
|
36
|
-
existing.push(patch.filename);
|
|
37
|
-
ownersByPath.set(file, existing);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
33
|
+
const ownersByPath = buildPatchClaims(manifestPatches);
|
|
40
34
|
// Merge duplicate-new-file-creation findings. The structured helper
|
|
41
35
|
// returns all new-file paths, so we filter to the ones with more
|
|
42
36
|
// than one creator. Paths are added to the table even when no patch
|
|
@@ -165,4 +159,3 @@ export function renderOwnershipTable(rows) {
|
|
|
165
159
|
info(`| ${pad(row.path, pathWidth)} | ${pad(ownerCell, ownerWidth)} | ${pad(stateCell, stateWidth)} |`);
|
|
166
160
|
}
|
|
167
161
|
}
|
|
168
|
-
//# sourceMappingURL=ownership-table.js.map
|
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Fuzz-like patch application with escalating context reduction.
|
|
3
3
|
*
|
|
4
|
-
* Tries an exact `git apply` first, then retries with
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Tries an exact `git apply` first, then retries with git's real
|
|
5
|
+
* drift-tolerance knob: `-C<n>` (reduced context matching). `git apply`
|
|
6
|
+
* has NO `--fuzz` option — that flag belongs to GNU `patch(1)`. The
|
|
7
|
+
* original implementation passed `--fuzz=N`, which git rejects with a
|
|
8
|
+
* usage error (exit 129) that the escalation loop swallowed, so every
|
|
9
|
+
* "fuzzy" attempt silently failed and patches that would have applied
|
|
10
|
+
* with context drift were reported as hard conflicts (2026-07-05 review,
|
|
11
|
+
* finding H1; verified against git 2.50). If all context-reduction levels
|
|
12
|
+
* fail, falls through to `git apply --reject` so the user gets `.rej`
|
|
13
|
+
* files for manual resolution.
|
|
14
|
+
*
|
|
15
|
+
* Semantics note: GNU fuzz *fuzzily matches* context; `-C<n>` *ignores*
|
|
16
|
+
* outer context lines instead. Step k maps to `-C(3-k)` (git's default
|
|
17
|
+
* context width is 3), so the final step `-C0` matches on line positions
|
|
18
|
+
* and -/+ content alone — the rough analogue of GNU `--fuzz=3` on a
|
|
19
|
+
* 3-context patch.
|
|
7
20
|
*/
|
|
8
21
|
export interface FuzzyApplyResult {
|
|
9
22
|
/** Whether the patch was applied successfully. */
|
|
10
23
|
success: boolean;
|
|
11
|
-
/**
|
|
24
|
+
/** Context-reduction steps taken (0 = exact match, k = applied with `-C(3-k)`). */
|
|
12
25
|
fuzzFactor: number;
|
|
13
26
|
/** Error description when `success` is false. */
|
|
14
27
|
error?: string;
|
|
@@ -16,14 +29,19 @@ export interface FuzzyApplyResult {
|
|
|
16
29
|
rejectFiles?: string[];
|
|
17
30
|
}
|
|
18
31
|
/**
|
|
19
|
-
* Attempts to apply a patch with escalating
|
|
32
|
+
* Attempts to apply a patch with escalating context reduction.
|
|
20
33
|
*
|
|
21
|
-
* 1. `git apply --check`
|
|
22
|
-
* 2. `git apply
|
|
34
|
+
* 1. `git apply --check` exact, then with `-C2`, `-C1`, `-C0`
|
|
35
|
+
* 2. `git apply` (same flags) at the first passing level
|
|
23
36
|
* 3. Fall through to `git apply --reject` if nothing succeeds
|
|
24
37
|
*
|
|
25
38
|
* @param patchPath - Absolute path to the `.patch` file
|
|
26
39
|
* @param engineDir - Working directory (engine/)
|
|
27
|
-
* @param maxFuzz - Maximum
|
|
40
|
+
* @param maxFuzz - Maximum context-reduction steps to try (default 3,
|
|
41
|
+
* 0 = exact only). Values above 3 are capped: git cannot reduce below
|
|
42
|
+
* `-C0`.
|
|
43
|
+
* @throws InvalidArgumentError when `maxFuzz` is not a non-negative integer —
|
|
44
|
+
* a NaN or negative value would silently skip every apply attempt
|
|
45
|
+
* (including the exact-match one) and fall straight through to `--reject`
|
|
28
46
|
*/
|
|
29
47
|
export declare function applyPatchWithFuzz(patchPath: string, engineDir: string, maxFuzz?: number): Promise<FuzzyApplyResult>;
|
|
@@ -1,70 +1,107 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Fuzz-like patch application with escalating context reduction.
|
|
4
4
|
*
|
|
5
|
-
* Tries an exact `git apply` first, then retries with
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Tries an exact `git apply` first, then retries with git's real
|
|
6
|
+
* drift-tolerance knob: `-C<n>` (reduced context matching). `git apply`
|
|
7
|
+
* has NO `--fuzz` option — that flag belongs to GNU `patch(1)`. The
|
|
8
|
+
* original implementation passed `--fuzz=N`, which git rejects with a
|
|
9
|
+
* usage error (exit 129) that the escalation loop swallowed, so every
|
|
10
|
+
* "fuzzy" attempt silently failed and patches that would have applied
|
|
11
|
+
* with context drift were reported as hard conflicts (2026-07-05 review,
|
|
12
|
+
* finding H1; verified against git 2.50). If all context-reduction levels
|
|
13
|
+
* fail, falls through to `git apply --reject` so the user gets `.rej`
|
|
14
|
+
* files for manual resolution.
|
|
15
|
+
*
|
|
16
|
+
* Semantics note: GNU fuzz *fuzzily matches* context; `-C<n>` *ignores*
|
|
17
|
+
* outer context lines instead. Step k maps to `-C(3-k)` (git's default
|
|
18
|
+
* context width is 3), so the final step `-C0` matches on line positions
|
|
19
|
+
* and -/+ content alone — the rough analogue of GNU `--fuzz=3` on a
|
|
20
|
+
* 3-context patch.
|
|
8
21
|
*/
|
|
22
|
+
import { InvalidArgumentError } from '../errors/base.js';
|
|
9
23
|
import { verbose } from '../utils/logger.js';
|
|
10
24
|
import { exec } from '../utils/process.js';
|
|
11
25
|
import { ensureGit } from './git-base.js';
|
|
26
|
+
/** Git's default diff context width — the ceiling for reduction steps. */
|
|
27
|
+
const GIT_DEFAULT_CONTEXT = 3;
|
|
12
28
|
// ── Implementation ──
|
|
13
29
|
/**
|
|
14
|
-
* Attempts to apply a patch with escalating
|
|
30
|
+
* Attempts to apply a patch with escalating context reduction.
|
|
15
31
|
*
|
|
16
|
-
* 1. `git apply --check`
|
|
17
|
-
* 2. `git apply
|
|
32
|
+
* 1. `git apply --check` exact, then with `-C2`, `-C1`, `-C0`
|
|
33
|
+
* 2. `git apply` (same flags) at the first passing level
|
|
18
34
|
* 3. Fall through to `git apply --reject` if nothing succeeds
|
|
19
35
|
*
|
|
20
36
|
* @param patchPath - Absolute path to the `.patch` file
|
|
21
37
|
* @param engineDir - Working directory (engine/)
|
|
22
|
-
* @param maxFuzz - Maximum
|
|
38
|
+
* @param maxFuzz - Maximum context-reduction steps to try (default 3,
|
|
39
|
+
* 0 = exact only). Values above 3 are capped: git cannot reduce below
|
|
40
|
+
* `-C0`.
|
|
41
|
+
* @throws InvalidArgumentError when `maxFuzz` is not a non-negative integer —
|
|
42
|
+
* a NaN or negative value would silently skip every apply attempt
|
|
43
|
+
* (including the exact-match one) and fall straight through to `--reject`
|
|
23
44
|
*/
|
|
24
45
|
export async function applyPatchWithFuzz(patchPath, engineDir, maxFuzz = 3) {
|
|
46
|
+
if (!Number.isInteger(maxFuzz) || maxFuzz < 0) {
|
|
47
|
+
throw new InvalidArgumentError(`maxFuzz must be a non-negative integer, got ${String(maxFuzz)}.`, 'maxFuzz');
|
|
48
|
+
}
|
|
25
49
|
await ensureGit();
|
|
26
|
-
// Try exact match first, then escalate
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
50
|
+
// Try exact match first, then escalate context reduction.
|
|
51
|
+
const maxSteps = Math.min(maxFuzz, GIT_DEFAULT_CONTEXT);
|
|
52
|
+
for (let step = 0; step <= maxSteps; step++) {
|
|
53
|
+
const contextArgs = step > 0 ? [`-C${String(GIT_DEFAULT_CONTEXT - step)}`] : [];
|
|
54
|
+
const check = await exec('git', ['apply', '--check', ...contextArgs, '--', patchPath], {
|
|
30
55
|
cwd: engineDir,
|
|
31
56
|
});
|
|
32
57
|
if (check.exitCode === 0) {
|
|
33
58
|
// --check passed: apply for real
|
|
34
|
-
const apply = await exec('git', ['apply', ...
|
|
59
|
+
const apply = await exec('git', ['apply', ...contextArgs, '--', patchPath], {
|
|
35
60
|
cwd: engineDir,
|
|
36
61
|
});
|
|
37
62
|
if (apply.exitCode === 0) {
|
|
38
|
-
if (
|
|
39
|
-
verbose(`Patch applied with
|
|
63
|
+
if (step > 0) {
|
|
64
|
+
verbose(`Patch applied with reduced context (-C${String(GIT_DEFAULT_CONTEXT - step)}): ${patchPath}`);
|
|
40
65
|
}
|
|
41
|
-
return { success: true, fuzzFactor:
|
|
66
|
+
return { success: true, fuzzFactor: step };
|
|
42
67
|
}
|
|
43
|
-
// Unlikely: --check passed but apply failed; fall through to next
|
|
44
|
-
verbose(`git apply
|
|
68
|
+
// Unlikely: --check passed but apply failed; fall through to next step
|
|
69
|
+
verbose(`git apply -C${String(GIT_DEFAULT_CONTEXT - step)} --check passed but apply failed: ${apply.stderr.trim()}`);
|
|
45
70
|
}
|
|
46
71
|
}
|
|
47
|
-
// All
|
|
72
|
+
// All context-reduction levels failed → generate .rej files for manual resolution
|
|
48
73
|
const rejectResult = await exec('git', ['apply', '--reject', '--', patchPath], {
|
|
49
74
|
cwd: engineDir,
|
|
50
75
|
});
|
|
51
|
-
const errorMessage = rejectResult.stderr.trim() || 'All
|
|
52
|
-
// Extract .rej file paths from stderr
|
|
53
|
-
const rejectFiles = [];
|
|
54
|
-
for (const line of rejectResult.stderr.split('\n')) {
|
|
55
|
-
const match = line.match(/Applying patch .* with (\d+) reject/);
|
|
56
|
-
if (match)
|
|
57
|
-
continue;
|
|
58
|
-
const rejMatch = line.match(/Rejected hunk.*to (.+\.rej)/);
|
|
59
|
-
if (rejMatch?.[1]) {
|
|
60
|
-
rejectFiles.push(rejMatch[1]);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
76
|
+
const errorMessage = rejectResult.stderr.trim() || 'All context-reduction levels failed';
|
|
63
77
|
return {
|
|
64
78
|
success: false,
|
|
65
|
-
fuzzFactor:
|
|
79
|
+
fuzzFactor: maxSteps,
|
|
66
80
|
error: errorMessage,
|
|
67
|
-
rejectFiles,
|
|
81
|
+
rejectFiles: extractRejectFiles(rejectResult.stderr),
|
|
68
82
|
};
|
|
69
83
|
}
|
|
70
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Extracts `.rej` file paths from `git apply --reject` stderr.
|
|
86
|
+
*
|
|
87
|
+
* Git's actual output shape (verified against git 2.50):
|
|
88
|
+
*
|
|
89
|
+
* Applying patch f.txt with 1 reject...
|
|
90
|
+
* Rejected hunk #1.
|
|
91
|
+
*
|
|
92
|
+
* The reject file is `<file>.rej` next to the target. The previous regex
|
|
93
|
+
* (`Rejected hunk.*to (.+\.rej)`) targeted GNU `patch(1)`'s "saving
|
|
94
|
+
* rejects to file X.rej" phrasing, which git never prints — so
|
|
95
|
+
* `rejectFiles` was always empty and the conflict-summary ".rej files
|
|
96
|
+
* created" hint never fired.
|
|
97
|
+
*/
|
|
98
|
+
function extractRejectFiles(stderr) {
|
|
99
|
+
const rejectFiles = [];
|
|
100
|
+
for (const line of stderr.split('\n')) {
|
|
101
|
+
const match = /^Applying patch (.+) with \d+ rejects?\.\.\./.exec(line);
|
|
102
|
+
if (match?.[1]) {
|
|
103
|
+
rejectFiles.push(`${match[1]}.rej`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return rejectFiles;
|
|
107
|
+
}
|
|
@@ -40,6 +40,28 @@ export interface ApplyPatchesOptions {
|
|
|
40
40
|
*/
|
|
41
41
|
untilFilename?: string | undefined;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Decides whether a patch filename matches an `--until` identifier.
|
|
45
|
+
*
|
|
46
|
+
* The identifier is one of three shapes and the three shapes must stay
|
|
47
|
+
* disjoint so the operator can reason about which patch they picked:
|
|
48
|
+
*
|
|
49
|
+
* 1. **Exact filename** — `005-foo.patch`. Matches only that filename.
|
|
50
|
+
* 2. **Filename without extension** — `005-foo`. Matches `005-foo.patch`.
|
|
51
|
+
* 3. **Bare numeric ordinal** — `5` or `005`. Matches the patch whose
|
|
52
|
+
* order prefix parses to the same integer (so `5` and `005` both
|
|
53
|
+
* match `005-foo.patch`, and `05` matches too because parseInt
|
|
54
|
+
* normalizes leading zeros).
|
|
55
|
+
*
|
|
56
|
+
* A purely-numeric identifier is treated **only** as an ordinal: it does
|
|
57
|
+
* not also match a filename that happens to literally equal the digits.
|
|
58
|
+
* That would require a patch literally named `5` or `005` — which would
|
|
59
|
+
* collide with the filename prefix anyway — and we prefer the explicit
|
|
60
|
+
* ordinal interpretation. The earlier form's `patchFilename === needle`
|
|
61
|
+
* short-circuit was kept behind the numeric gate so the match stays
|
|
62
|
+
* single-meaning per identifier.
|
|
63
|
+
*/
|
|
64
|
+
export declare function matchesUntilFilename(patchFilename: string, needle: string): boolean;
|
|
43
65
|
/**
|
|
44
66
|
* Enhanced patch application with continue mode.
|
|
45
67
|
* When continueOnFailure is false, rolls back all previously applied patches
|
|
@@ -58,3 +80,11 @@ export declare function applyPatchesWithContinue(patchesDir: string, engineDir:
|
|
|
58
80
|
* @returns Content after all patches applied, or null if file doesn't exist
|
|
59
81
|
*/
|
|
60
82
|
export declare function computePatchedContent(patchesDir: string, engineDir: string, filePath: string): Promise<string | null>;
|
|
83
|
+
/**
|
|
84
|
+
* Builds a batched variant of {@link computePatchedContent} that loads the
|
|
85
|
+
* manifest and discovers patch files ONCE for many lookups. The per-call
|
|
86
|
+
* function re-runs `loadPatchesManifest` + `discoverPatches` for every
|
|
87
|
+
* file — O(dirtyFiles × patches) redundant IO when classifying a broad
|
|
88
|
+
* engine edit session during `import`.
|
|
89
|
+
*/
|
|
90
|
+
export declare function createPatchedContentComputer(patchesDir: string, engineDir: string): Promise<(filePath: string) => Promise<string | null>>;
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Patch orchestration — coordinates patch discovery, application, and validation.
|
|
4
4
|
* Pure parsing, content transformation, and lock management are in separate modules.
|
|
5
5
|
*/
|
|
6
|
-
import { lstat } from 'node:fs/promises';
|
|
7
|
-
import { join, resolve } from 'node:path';
|
|
6
|
+
import { lstat, readlink, realpath } from 'node:fs/promises';
|
|
7
|
+
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
8
8
|
import { PatchError } from '../errors/patch.js';
|
|
9
9
|
import { toError } from '../utils/errors.js';
|
|
10
10
|
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
@@ -14,7 +14,7 @@ import { exec } from '../utils/process.js';
|
|
|
14
14
|
import { applyPatchIdempotent, reversePatch } from './git.js';
|
|
15
15
|
import { getFileContentFromHead } from './git-file-ops.js';
|
|
16
16
|
import { discoverPatches } from './patch-files.js';
|
|
17
|
-
import { findPatchesAffectingFile } from './patch-manifest.js';
|
|
17
|
+
import { findPatchesAffectingFile, loadPatchesManifest } from './patch-manifest.js';
|
|
18
18
|
import { extractAffectedFiles, extractConflictingFiles, isNewFileInPatch } from './patch-parse.js';
|
|
19
19
|
import { applyPatchToContent, extractNewFileContent } from './patch-transform.js';
|
|
20
20
|
// Re-export from split modules so existing import sites continue working
|
|
@@ -27,16 +27,23 @@ export { applyPatchToContent, extractNewFileContent } from './patch-transform.js
|
|
|
27
27
|
* Applies a single patch.
|
|
28
28
|
* @param patch - Patch info
|
|
29
29
|
* @param engineDir - Path to the engine directory
|
|
30
|
+
* @param protectedFiles - Files touched by patches already applied in the
|
|
31
|
+
* current run; the idempotent-recovery reset must not wipe them (see
|
|
32
|
+
* applyPatchIdempotent). Overlapping queues (`--allow-overlap`) share
|
|
33
|
+
* files between patches, and a later patch's recovery used to reset the
|
|
34
|
+
* shared file to HEAD — silently discarding the earlier patch's changes.
|
|
30
35
|
* @returns Patch result
|
|
31
36
|
*/
|
|
32
|
-
async function applySinglePatch(patch, engineDir) {
|
|
37
|
+
async function applySinglePatch(patch, engineDir, protectedFiles) {
|
|
33
38
|
let patchContent = '';
|
|
34
39
|
let affectedFiles = [];
|
|
35
40
|
try {
|
|
36
41
|
patchContent = await readText(patch.path);
|
|
37
42
|
affectedFiles = extractAffectedFiles(patchContent);
|
|
38
43
|
await validatePatchTargets(patch, affectedFiles, engineDir);
|
|
39
|
-
await applyPatchIdempotent(patch.path, engineDir
|
|
44
|
+
await applyPatchIdempotent(patch.path, engineDir, {
|
|
45
|
+
...(protectedFiles ? { protectedFiles } : {}),
|
|
46
|
+
});
|
|
40
47
|
return { patch, success: true };
|
|
41
48
|
}
|
|
42
49
|
catch (error) {
|
|
@@ -48,21 +55,39 @@ async function applySinglePatch(patch, engineDir) {
|
|
|
48
55
|
let resolvedNewFiles = false;
|
|
49
56
|
// Save original content for files we might overwrite, so we can restore on failure
|
|
50
57
|
const savedContents = new Map();
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
try {
|
|
59
|
+
for (const file of affectedFiles) {
|
|
60
|
+
if (isNewFileInPatch(patchContent, file)) {
|
|
61
|
+
const targetPath = join(engineDir, file);
|
|
62
|
+
if (await pathExists(targetPath)) {
|
|
63
|
+
savedContents.set(file, await readText(targetPath));
|
|
64
|
+
const content = await extractNewFileContent(patch.path, file);
|
|
65
|
+
await writeText(targetPath, content);
|
|
66
|
+
resolvedNewFiles = true;
|
|
67
|
+
}
|
|
59
68
|
}
|
|
60
69
|
}
|
|
61
70
|
}
|
|
71
|
+
catch (extractError) {
|
|
72
|
+
// extractNewFileContent threw for a LATER target (e.g. a binary
|
|
73
|
+
// section) after earlier targets were already overwritten — restore
|
|
74
|
+
// them before reporting failure, instead of letting the exception
|
|
75
|
+
// skip the restore loop entirely and crash the whole import.
|
|
76
|
+
for (const [file, originalContent] of savedContents) {
|
|
77
|
+
await writeText(join(engineDir, file), originalContent);
|
|
78
|
+
}
|
|
79
|
+
return { patch, success: false, error: toError(extractError).message };
|
|
80
|
+
}
|
|
62
81
|
if (resolvedNewFiles) {
|
|
63
82
|
try {
|
|
64
|
-
await applyPatchIdempotent(patch.path, engineDir
|
|
65
|
-
|
|
83
|
+
await applyPatchIdempotent(patch.path, engineDir, {
|
|
84
|
+
...(protectedFiles ? { protectedFiles } : {}),
|
|
85
|
+
});
|
|
86
|
+
// Keep the originals: rollbackPatches needs them if a LATER patch
|
|
87
|
+
// fails, since reverse-applying this new-file patch deletes the
|
|
88
|
+
// target and would otherwise permanently discard the pre-existing
|
|
89
|
+
// content (recoverable from git only if it was tracked at HEAD).
|
|
90
|
+
return { patch, success: true, autoResolved: true, autoResolvedOriginals: savedContents };
|
|
66
91
|
}
|
|
67
92
|
catch (retryError) {
|
|
68
93
|
verbose(`Auto-resolved new-file retry failed for ${patch.filename}: ${toError(retryError).message}`);
|
|
@@ -76,7 +101,10 @@ async function applySinglePatch(patch, engineDir) {
|
|
|
76
101
|
let errorMessage = applyError.message;
|
|
77
102
|
try {
|
|
78
103
|
// Use --reject to apply what we can and create .rej files for what we can't
|
|
79
|
-
await applyPatchIdempotent(patch.path, engineDir, {
|
|
104
|
+
await applyPatchIdempotent(patch.path, engineDir, {
|
|
105
|
+
reject: true,
|
|
106
|
+
...(protectedFiles ? { protectedFiles } : {}),
|
|
107
|
+
});
|
|
80
108
|
// If this somehow succeeds with --reject but failed without, it still shouldn't
|
|
81
109
|
// happen because applyPatch first runs --check which would fail.
|
|
82
110
|
// But if it did succeed, we should still return failure because manual fix is needed
|
|
@@ -105,6 +133,19 @@ async function rollbackPatches(results, engineDir) {
|
|
|
105
133
|
catch (rollbackError) {
|
|
106
134
|
verbose(`Failed to roll back ${result.patch.filename}: ${toError(rollbackError).message}`);
|
|
107
135
|
}
|
|
136
|
+
// Reversing an auto-resolved new-file patch deletes the target; put
|
|
137
|
+
// back the pre-existing content the auto-resolve overwrote.
|
|
138
|
+
if (result.autoResolvedOriginals) {
|
|
139
|
+
for (const [file, originalContent] of result.autoResolvedOriginals) {
|
|
140
|
+
try {
|
|
141
|
+
await writeText(join(engineDir, file), originalContent);
|
|
142
|
+
verbose(`Restored pre-existing content of ${file} after rollback`);
|
|
143
|
+
}
|
|
144
|
+
catch (restoreError) {
|
|
145
|
+
verbose(`Could not restore ${file}: ${toError(restoreError).message}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
108
149
|
}
|
|
109
150
|
}
|
|
110
151
|
/**
|
|
@@ -117,9 +158,15 @@ async function rollbackPatches(results, engineDir) {
|
|
|
117
158
|
export async function applyPatches(patchesDir, engineDir) {
|
|
118
159
|
const patches = await discoverPatches(patchesDir);
|
|
119
160
|
const results = [];
|
|
161
|
+
const appliedFiles = new Set();
|
|
120
162
|
for (const patch of patches) {
|
|
121
|
-
const result = await applySinglePatch(patch, engineDir);
|
|
163
|
+
const result = await applySinglePatch(patch, engineDir, appliedFiles);
|
|
122
164
|
results.push(result);
|
|
165
|
+
if (result.success) {
|
|
166
|
+
for (const file of extractAffectedFiles(await readText(patch.path))) {
|
|
167
|
+
appliedFiles.add(file);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
123
170
|
// Stop on first failure and roll back all previously applied patches
|
|
124
171
|
if (!result.success) {
|
|
125
172
|
const succeeded = results.filter((r) => r.success);
|
|
@@ -159,33 +206,63 @@ export async function validatePatches(patchesDir, engineDir) {
|
|
|
159
206
|
return { valid: errors.length === 0, errors };
|
|
160
207
|
}
|
|
161
208
|
async function validatePatchTargets(patch, affectedFiles, engineDir) {
|
|
209
|
+
// realpath the engine root once so containment is checked against the
|
|
210
|
+
// physical tree (the root itself may sit behind a symlink, e.g. /tmp on
|
|
211
|
+
// macOS). An unresolvable engine dir skips the symlink checks — a missing
|
|
212
|
+
// engine surfaces through `git apply --check` with a better message.
|
|
213
|
+
const engineRoot = engineDir ? await realpath(engineDir).catch(() => null) : null;
|
|
162
214
|
for (const file of affectedFiles) {
|
|
163
215
|
if (!isContainedRelativePath(file)) {
|
|
164
216
|
throw new PatchError(`Patch targets a path outside engine/: ${file}`, patch.filename);
|
|
165
217
|
}
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const realPath = resolve(engineDir, file);
|
|
175
|
-
const resolvedEngine = resolve(engineDir);
|
|
176
|
-
if (!realPath.startsWith(resolvedEngine + '/') && realPath !== resolvedEngine) {
|
|
177
|
-
throw new PatchError(`Patch targets a symlink that resolves outside engine/: ${file}`, patch.filename);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
catch (error) {
|
|
182
|
-
// File doesn't exist yet (new file) or stat fails — skip check
|
|
183
|
-
if (error instanceof PatchError)
|
|
184
|
-
throw error;
|
|
218
|
+
// Verify that a write to the target would physically land inside the
|
|
219
|
+
// engine tree. A crafted patch could otherwise write through a symlink
|
|
220
|
+
// (the target itself, a dangling link, or a symlinked parent directory)
|
|
221
|
+
// to an arbitrary location.
|
|
222
|
+
if (engineDir && engineRoot) {
|
|
223
|
+
const destination = await resolvePatchWriteDestination(join(engineDir, file));
|
|
224
|
+
if (destination !== engineRoot && !destination.startsWith(engineRoot + sep)) {
|
|
225
|
+
throw new PatchError(`Patch targets a path that resolves outside engine/ (symlink escape): ${file}`, patch.filename);
|
|
185
226
|
}
|
|
186
227
|
}
|
|
187
228
|
}
|
|
188
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Resolves where a write to `targetPath` would physically land, following
|
|
232
|
+
* symlinks on every path component that already exists.
|
|
233
|
+
*
|
|
234
|
+
* - Existing target: `realpath` resolves it fully.
|
|
235
|
+
* - Dangling symlink: `realpath` rejects it, but a write through the link
|
|
236
|
+
* would still be created at the link target, so the target is resolved
|
|
237
|
+
* against the link's (real) parent directory. The link target is taken
|
|
238
|
+
* textually — a loop or nested dangling link fails at apply time anyway.
|
|
239
|
+
* - Not-yet-existing file: resolved against the nearest existing ancestor's
|
|
240
|
+
* real path. Components below that ancestor do not exist, so they cannot
|
|
241
|
+
* be symlinks and are appended verbatim.
|
|
242
|
+
*/
|
|
243
|
+
async function resolvePatchWriteDestination(targetPath) {
|
|
244
|
+
try {
|
|
245
|
+
return await realpath(targetPath);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// Fall through: the path (or its symlink target) does not exist.
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const stats = await lstat(targetPath);
|
|
252
|
+
if (stats.isSymbolicLink()) {
|
|
253
|
+
const linkTarget = await readlink(targetPath);
|
|
254
|
+
return resolve(await resolvePatchWriteDestination(dirname(targetPath)), linkTarget);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// The path component itself does not exist.
|
|
259
|
+
}
|
|
260
|
+
const parent = dirname(targetPath);
|
|
261
|
+
if (parent === targetPath) {
|
|
262
|
+
return targetPath;
|
|
263
|
+
}
|
|
264
|
+
return join(await resolvePatchWriteDestination(parent), basename(targetPath));
|
|
265
|
+
}
|
|
189
266
|
/**
|
|
190
267
|
* Decides whether a patch filename matches an `--until` identifier.
|
|
191
268
|
*
|
|
@@ -207,7 +284,7 @@ async function validatePatchTargets(patch, affectedFiles, engineDir) {
|
|
|
207
284
|
* short-circuit was kept behind the numeric gate so the match stays
|
|
208
285
|
* single-meaning per identifier.
|
|
209
286
|
*/
|
|
210
|
-
function matchesUntilFilename(patchFilename, needle) {
|
|
287
|
+
export function matchesUntilFilename(patchFilename, needle) {
|
|
211
288
|
const isNumeric = /^\d+$/.test(needle);
|
|
212
289
|
if (isNumeric) {
|
|
213
290
|
const order = parseInt(needle, 10);
|
|
@@ -268,6 +345,7 @@ export async function applyPatchesWithContinue(patchesDir, engineDir, optionsOrC
|
|
|
268
345
|
const succeeded = [];
|
|
269
346
|
const failed = [];
|
|
270
347
|
const skipped = [];
|
|
348
|
+
const appliedFiles = new Set();
|
|
271
349
|
for (let i = 0; i < patches.length; i++) {
|
|
272
350
|
const patch = patches[i];
|
|
273
351
|
if (!patch)
|
|
@@ -279,9 +357,12 @@ export async function applyPatchesWithContinue(patchesDir, engineDir, optionsOrC
|
|
|
279
357
|
skipped.push(patch);
|
|
280
358
|
continue;
|
|
281
359
|
}
|
|
282
|
-
const result = await applySinglePatch(patch, engineDir);
|
|
360
|
+
const result = await applySinglePatch(patch, engineDir, appliedFiles);
|
|
283
361
|
if (result.success) {
|
|
284
362
|
succeeded.push(result);
|
|
363
|
+
for (const file of extractAffectedFiles(await readText(patch.path))) {
|
|
364
|
+
appliedFiles.add(file);
|
|
365
|
+
}
|
|
285
366
|
}
|
|
286
367
|
else {
|
|
287
368
|
// Try to extract conflicting files from error message
|
|
@@ -332,4 +413,36 @@ export async function computePatchedContent(patchesDir, engineDir, filePath) {
|
|
|
332
413
|
}
|
|
333
414
|
return content;
|
|
334
415
|
}
|
|
335
|
-
|
|
416
|
+
/**
|
|
417
|
+
* Builds a batched variant of {@link computePatchedContent} that loads the
|
|
418
|
+
* manifest and discovers patch files ONCE for many lookups. The per-call
|
|
419
|
+
* function re-runs `loadPatchesManifest` + `discoverPatches` for every
|
|
420
|
+
* file — O(dirtyFiles × patches) redundant IO when classifying a broad
|
|
421
|
+
* engine edit session during `import`.
|
|
422
|
+
*/
|
|
423
|
+
export async function createPatchedContentComputer(patchesDir, engineDir) {
|
|
424
|
+
const manifest = await loadPatchesManifest(patchesDir);
|
|
425
|
+
const patches = await discoverPatches(patchesDir);
|
|
426
|
+
const patchByFilename = new Map(patches.map((p) => [p.filename, p]));
|
|
427
|
+
const affectingByFile = new Map();
|
|
428
|
+
for (const metadata of manifest?.patches ?? []) {
|
|
429
|
+
const patch = patchByFilename.get(metadata.filename);
|
|
430
|
+
if (!patch)
|
|
431
|
+
continue;
|
|
432
|
+
for (const file of metadata.filesAffected) {
|
|
433
|
+
const list = affectingByFile.get(file) ?? [];
|
|
434
|
+
list.push(patch);
|
|
435
|
+
affectingByFile.set(file, list);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
for (const list of affectingByFile.values()) {
|
|
439
|
+
list.sort((a, b) => a.order - b.order);
|
|
440
|
+
}
|
|
441
|
+
return async (filePath) => {
|
|
442
|
+
let content = await getFileContentFromHead(engineDir, filePath);
|
|
443
|
+
for (const patch of affectingByFile.get(filePath) ?? []) {
|
|
444
|
+
content = await applyPatchToContent(content, patch.path, filePath);
|
|
445
|
+
}
|
|
446
|
+
return content;
|
|
447
|
+
};
|
|
448
|
+
}
|