@hominis/fireforge 0.39.0 → 0.41.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 +100 -0
- package/README.md +35 -5
- package/dist/bin/fireforge.js +25 -4
- package/dist/build-info.json +9 -0
- package/dist/src/cli.js +27 -3
- package/dist/src/commands/bootstrap.js +5 -1
- package/dist/src/commands/discard.js +2 -22
- package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
- package/dist/src/commands/doctor-check-core.d.ts +20 -0
- package/dist/src/commands/doctor-check-core.js +28 -0
- package/dist/src/commands/doctor-external-toolchains.js +2 -0
- package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
- package/dist/src/commands/doctor-furnace.js +13 -11
- package/dist/src/commands/doctor-orphaned-harness.js +2 -1
- package/dist/src/commands/doctor.js +2 -2
- package/dist/src/commands/export-all.js +9 -1
- package/dist/src/commands/export-flow.js +27 -9
- package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
- package/dist/src/commands/export-placement-conflicts.js +74 -0
- package/dist/src/commands/export-placement-gate.js +1 -1
- package/dist/src/commands/export-shared.d.ts +8 -1
- package/dist/src/commands/export-shared.js +30 -10
- package/dist/src/commands/export.js +2 -2
- package/dist/src/commands/furnace/apply.js +4 -3
- package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
- package/dist/src/commands/furnace/chrome-doc.js +13 -2
- package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
- package/dist/src/commands/furnace/create-browser-test.js +5 -0
- package/dist/src/commands/furnace/create.js +2 -2
- package/dist/src/commands/furnace/diff.js +2 -1
- package/dist/src/commands/furnace/init.js +4 -6
- package/dist/src/commands/furnace/override.js +2 -2
- package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
- package/dist/src/commands/furnace/remove-mutations.js +171 -0
- package/dist/src/commands/furnace/remove.js +21 -128
- package/dist/src/commands/furnace/rename-browser-test.js +54 -5
- package/dist/src/commands/furnace/scan.js +27 -36
- package/dist/src/commands/furnace/status.js +30 -28
- package/dist/src/commands/furnace/validate.js +25 -18
- package/dist/src/commands/import.js +6 -20
- package/dist/src/commands/lint-per-patch.js +73 -51
- package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
- package/dist/src/commands/lint-per-run-checkjs.js +102 -0
- package/dist/src/commands/lint-report.d.ts +21 -0
- package/dist/src/commands/lint-report.js +62 -0
- package/dist/src/commands/lint.js +26 -22
- package/dist/src/commands/manifest.js +3 -1
- package/dist/src/commands/package.js +9 -16
- package/dist/src/commands/patch/compact.js +5 -5
- package/dist/src/commands/patch/delete.js +5 -5
- package/dist/src/commands/patch/lint-ignore.js +3 -2
- package/dist/src/commands/patch/move-files-create.js +22 -4
- package/dist/src/commands/patch/move-files-into.js +14 -10
- package/dist/src/commands/patch/move-files.js +6 -5
- package/dist/src/commands/patch/rename.d.ts +2 -2
- package/dist/src/commands/patch/rename.js +152 -51
- package/dist/src/commands/patch/reorder.d.ts +9 -0
- package/dist/src/commands/patch/reorder.js +14 -10
- package/dist/src/commands/patch/split-plan.d.ts +13 -6
- package/dist/src/commands/patch/split-plan.js +22 -12
- package/dist/src/commands/patch/split.d.ts +1 -1
- package/dist/src/commands/patch/split.js +23 -9
- package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
- package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
- package/dist/src/commands/patch/staged-dependency.js +112 -14
- package/dist/src/commands/re-export-adjacent.d.ts +34 -0
- package/dist/src/commands/re-export-adjacent.js +105 -0
- package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
- package/dist/src/commands/re-export-bulk-scan.js +142 -2
- package/dist/src/commands/re-export-drift.d.ts +74 -0
- package/dist/src/commands/re-export-drift.js +234 -0
- package/dist/src/commands/re-export-files.js +5 -3
- package/dist/src/commands/re-export-lint.d.ts +97 -0
- package/dist/src/commands/re-export-lint.js +210 -0
- package/dist/src/commands/re-export-options.js +11 -0
- package/dist/src/commands/re-export-register.d.ts +18 -0
- package/dist/src/commands/re-export-register.js +61 -0
- package/dist/src/commands/re-export-scan.js +13 -7
- package/dist/src/commands/re-export-single.d.ts +20 -0
- package/dist/src/commands/re-export-single.js +261 -0
- package/dist/src/commands/re-export.d.ts +0 -4
- package/dist/src/commands/re-export.js +123 -289
- package/dist/src/commands/rebase/abort.js +13 -4
- package/dist/src/commands/rebase/continue.js +11 -4
- package/dist/src/commands/rebase/index.js +19 -29
- package/dist/src/commands/rebase/patch-loop.js +12 -0
- package/dist/src/commands/reset.js +4 -23
- package/dist/src/commands/resolve.js +2 -9
- package/dist/src/commands/run.js +29 -23
- package/dist/src/commands/source.js +3 -9
- package/dist/src/commands/status-check.d.ts +48 -0
- package/dist/src/commands/status-check.js +106 -0
- package/dist/src/commands/status-json.d.ts +31 -0
- package/dist/src/commands/status-json.js +76 -0
- package/dist/src/commands/status-output.d.ts +1 -0
- package/dist/src/commands/status-output.js +10 -1
- package/dist/src/commands/status-ownership.d.ts +52 -0
- package/dist/src/commands/status-ownership.js +57 -0
- package/dist/src/commands/status.js +190 -170
- package/dist/src/commands/test-build-phase.d.ts +19 -0
- package/dist/src/commands/test-build-phase.js +168 -0
- package/dist/src/commands/test-diagnose.js +30 -2
- package/dist/src/commands/test-modes.d.ts +9 -1
- package/dist/src/commands/test-modes.js +46 -13
- package/dist/src/commands/test-register.js +59 -27
- package/dist/src/commands/test-run.d.ts +29 -3
- package/dist/src/commands/test-run.js +59 -7
- package/dist/src/commands/test-verdict.d.ts +32 -0
- package/dist/src/commands/test-verdict.js +48 -0
- package/dist/src/commands/test.d.ts +9 -0
- package/dist/src/commands/test.js +100 -123
- package/dist/src/commands/token-coverage.js +4 -9
- package/dist/src/commands/token.js +4 -1
- package/dist/src/commands/tree.d.ts +19 -0
- package/dist/src/commands/tree.js +320 -0
- package/dist/src/commands/verify.js +2 -2
- package/dist/src/commands/watch.js +22 -21
- package/dist/src/commands/wire.js +1 -1
- package/dist/src/core/ast-utils.d.ts +11 -0
- package/dist/src/core/ast-utils.js +11 -0
- package/dist/src/core/build-audit-platform.js +6 -1
- package/dist/src/core/build-audit-registration.js +2 -0
- package/dist/src/core/build-audit-resolve.d.ts +13 -0
- package/dist/src/core/build-audit-resolve.js +34 -13
- package/dist/src/core/build-audit.js +4 -20
- package/dist/src/core/build-baseline-types.d.ts +29 -8
- package/dist/src/core/build-baseline-types.js +2 -1
- package/dist/src/core/build-baseline.d.ts +10 -2
- package/dist/src/core/build-baseline.js +39 -8
- package/dist/src/core/build-prepare.d.ts +4 -0
- package/dist/src/core/build-prepare.js +7 -1
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-test-toolchains.js +3 -2
- package/dist/src/core/config-validate.js +45 -30
- package/dist/src/core/config.js +16 -3
- package/dist/src/core/coverage-extend.d.ts +89 -0
- package/dist/src/core/coverage-extend.js +188 -0
- package/dist/src/core/destructive.js +3 -1
- package/dist/src/core/discard-baseline.js +18 -4
- package/dist/src/core/engine-precondition.d.ts +35 -0
- package/dist/src/core/engine-precondition.js +50 -0
- package/dist/src/core/engine-session-lock.d.ts +38 -1
- package/dist/src/core/engine-session-lock.js +78 -4
- package/dist/src/core/file-lock.d.ts +7 -0
- package/dist/src/core/file-lock.js +20 -30
- package/dist/src/core/firefox.js +5 -2
- package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
- package/dist/src/core/furnace-apply-helpers.js +12 -3
- package/dist/src/core/furnace-apply-output.js +9 -0
- package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
- package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
- package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
- package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
- package/dist/src/core/furnace-apply.d.ts +16 -4
- package/dist/src/core/furnace-apply.js +39 -43
- package/dist/src/core/furnace-config.js +2 -0
- package/dist/src/core/furnace-css-fragments.js +2 -0
- package/dist/src/core/furnace-jsconfig.js +14 -7
- package/dist/src/core/furnace-registration-ast.js +8 -8
- package/dist/src/core/furnace-registration-remove.js +6 -3
- package/dist/src/core/furnace-registration.js +9 -12
- package/dist/src/core/furnace-rollback.js +1 -1
- package/dist/src/core/furnace-scanner.js +2 -1
- package/dist/src/core/furnace-stale-export.js +2 -1
- package/dist/src/core/furnace-state-persist.d.ts +4 -6
- package/dist/src/core/furnace-validate-compatibility.js +4 -7
- package/dist/src/core/furnace-validate-helpers.js +33 -1
- package/dist/src/core/furnace-validate-registration.js +16 -0
- package/dist/src/core/furnace-validate.js +4 -2
- package/dist/src/core/git-diff.js +37 -2
- package/dist/src/core/lint-cache.d.ts +15 -5
- package/dist/src/core/lint-cache.js +49 -14
- package/dist/src/core/mach-build-artifacts.d.ts +63 -1
- package/dist/src/core/mach-build-artifacts.js +108 -2
- package/dist/src/core/mach-resource-shim.js +1 -0
- package/dist/src/core/marionette-port.d.ts +16 -0
- package/dist/src/core/marionette-port.js +93 -8
- package/dist/src/core/marionette-preflight.js +6 -5
- package/dist/src/core/patch-apply.d.ts +20 -13
- package/dist/src/core/patch-apply.js +32 -33
- package/dist/src/core/patch-export.d.ts +2 -2
- package/dist/src/core/patch-export.js +4 -6
- package/dist/src/core/patch-lint-checkjs.d.ts +42 -2
- package/dist/src/core/patch-lint-checkjs.js +132 -8
- package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
- package/dist/src/core/patch-lint-creators.d.ts +30 -0
- package/dist/src/core/patch-lint-creators.js +45 -0
- package/dist/src/core/patch-lint-cross.d.ts +1 -17
- package/dist/src/core/patch-lint-cross.js +16 -32
- package/dist/src/core/patch-lint-css.js +64 -16
- package/dist/src/core/patch-lint-diff.d.ts +8 -0
- package/dist/src/core/patch-lint-diff.js +31 -0
- package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
- package/dist/src/core/patch-lint-jsdoc.js +15 -5
- package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
- package/dist/src/core/patch-lint-module-registration.js +88 -0
- package/dist/src/core/patch-lint-mozbuild.js +99 -11
- package/dist/src/core/patch-lint-ownership.d.ts +21 -0
- package/dist/src/core/patch-lint-ownership.js +30 -0
- package/dist/src/core/patch-lint-reexports.d.ts +2 -1
- package/dist/src/core/patch-lint-reexports.js +1 -0
- package/dist/src/core/patch-lint.d.ts +31 -0
- package/dist/src/core/patch-lint.js +114 -8
- package/dist/src/core/patch-lock.d.ts +12 -1
- package/dist/src/core/patch-lock.js +40 -4
- package/dist/src/core/patch-manifest-consistency.js +15 -3
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-parse.d.ts +8 -0
- package/dist/src/core/patch-parse.js +10 -0
- package/dist/src/core/patch-policy.d.ts +7 -0
- package/dist/src/core/patch-policy.js +4 -1
- package/dist/src/core/patch-transform.d.ts +10 -0
- package/dist/src/core/patch-transform.js +13 -2
- package/dist/src/core/rebase-session.d.ts +44 -2
- package/dist/src/core/rebase-session.js +114 -17
- package/dist/src/core/register-browser-content.js +2 -1
- package/dist/src/core/register-shared-css.js +2 -1
- package/dist/src/core/smoke-patterns.d.ts +24 -16
- package/dist/src/core/smoke-patterns.js +26 -14
- package/dist/src/core/status-binary.d.ts +55 -0
- package/dist/src/core/status-binary.js +124 -0
- package/dist/src/core/status-classify.d.ts +22 -1
- package/dist/src/core/status-classify.js +112 -70
- package/dist/src/core/test-harness-crash.d.ts +32 -1
- package/dist/src/core/test-harness-crash.js +59 -4
- package/dist/src/core/test-path-scope.js +2 -0
- package/dist/src/core/test-stale-check.js +1 -20
- package/dist/src/core/token-category.d.ts +40 -0
- package/dist/src/core/token-category.js +264 -0
- package/dist/src/core/token-manager.d.ts +7 -0
- package/dist/src/core/token-manager.js +48 -196
- package/dist/src/core/toolchain-preflight.js +24 -26
- package/dist/src/core/tree-cow.d.ts +18 -0
- package/dist/src/core/tree-cow.js +71 -0
- package/dist/src/core/tree-guard.d.ts +35 -0
- package/dist/src/core/tree-guard.js +179 -0
- package/dist/src/core/tree-store.d.ts +129 -0
- package/dist/src/core/tree-store.js +452 -0
- package/dist/src/core/typecheck-shim.d.ts +14 -0
- package/dist/src/core/typecheck-shim.js +41 -2
- package/dist/src/core/typecheck.js +2 -1
- package/dist/src/core/wire-destroy.js +14 -3
- package/dist/src/core/wire-init.js +37 -40
- package/dist/src/core/wire-subscript.js +11 -3
- package/dist/src/core/wire-utils.js +4 -4
- package/dist/src/core/xpcshell-appdir.js +3 -0
- package/dist/src/core/xpcshell-profile-dir.d.ts +12 -0
- package/dist/src/core/xpcshell-profile-dir.js +45 -0
- package/dist/src/errors/base.d.ts +13 -0
- package/dist/src/errors/base.js +13 -0
- package/dist/src/errors/git.d.ts +8 -0
- package/dist/src/errors/git.js +17 -0
- package/dist/src/errors/rebase.d.ts +13 -0
- package/dist/src/errors/rebase.js +17 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/types/commands/options.d.ts +119 -3
- package/dist/src/types/commands/patches.d.ts +8 -0
- package/dist/src/types/config.d.ts +19 -0
- package/dist/src/types/furnace.d.ts +7 -0
- package/dist/src/utils/build-info.d.ts +31 -0
- package/dist/src/utils/build-info.js +98 -0
- package/dist/src/utils/concurrency.d.ts +8 -0
- package/dist/src/utils/concurrency.js +21 -0
- package/dist/src/utils/errors.d.ts +39 -0
- package/dist/src/utils/errors.js +55 -0
- package/dist/src/utils/fs.js +5 -12
- package/dist/src/utils/logger.d.ts +14 -2
- package/dist/src/utils/logger.js +43 -13
- package/dist/src/utils/options.d.ts +21 -4
- package/dist/src/utils/options.js +26 -6
- package/dist/src/utils/package-root.js +7 -4
- package/dist/src/utils/process-group.js +13 -2
- package/dist/src/utils/stdio-drain.d.ts +28 -0
- package/dist/src/utils/stdio-drain.js +54 -0
- package/dist/src/utils/validation.d.ts +33 -2
- package/dist/src/utils/validation.js +57 -2
- package/package.json +13 -13
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Source identity of the running build. */
|
|
2
|
+
export interface BuildIdentity {
|
|
3
|
+
shortCommit: string;
|
|
4
|
+
dirty: boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Content hash of the uncommitted diff, present only for a DIRTY
|
|
7
|
+
* installed build (FORGE L8). Two packs from the same HEAD with
|
|
8
|
+
* different uncommitted content share `shortCommit` and the `.dirty`
|
|
9
|
+
* marker, so without this they report byte-identical `--version`
|
|
10
|
+
* strings — which is what made a field report against
|
|
11
|
+
* `0.41.0+g<sha>.dirty` impossible to trace back to a specific pack.
|
|
12
|
+
* Absent in a git checkout: identity there comes from live git, and
|
|
13
|
+
* hashing the full diff on every `--version` call is not worth it.
|
|
14
|
+
*/
|
|
15
|
+
dirtyHash?: string;
|
|
16
|
+
}
|
|
17
|
+
/** @internal Reads the stamped dist/build-info.json; null when absent/invalid. */
|
|
18
|
+
export declare function readBuildInfoFile(packageRoot: string): BuildIdentity | null;
|
|
19
|
+
type GitRunner = (args: string[]) => string;
|
|
20
|
+
/** @internal Live git identity of the checkout; null when git is unusable. */
|
|
21
|
+
export declare function readGitBuildIdentity(runGit?: GitRunner): BuildIdentity | null;
|
|
22
|
+
/**
|
|
23
|
+
* @internal Formats `<version>+g<sha>[.dirty[.<hash>]]`; plain version on
|
|
24
|
+
* null identity. The trailing content hash appears only for a dirty
|
|
25
|
+
* INSTALLED build, where `dist/build-info.json` already carries it — every
|
|
26
|
+
* segment stays valid semver build metadata (dot-separated alphanumerics).
|
|
27
|
+
*/
|
|
28
|
+
export declare function formatVersionWithIdentity(version: string, identity: BuildIdentity | null): string;
|
|
29
|
+
/** The `--version` string: semver plus build identity when known. */
|
|
30
|
+
export declare function getCliVersion(): string;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Build identity for `--version` (FORGE K2/K3/L8). Three distinct builds
|
|
4
|
+
* shipped as plain "0.41.0" — consumers had to fingerprint tarballs by
|
|
5
|
+
* sha256 to know which build a claim was verified against. `--version`
|
|
6
|
+
* now reports `<semver>+g<short-sha>[.dirty[.<content-hash>]]`:
|
|
7
|
+
*
|
|
8
|
+
* - In a git checkout (dev runs via tsx), identity comes from live git —
|
|
9
|
+
* a stale `dist/build-info.json` from an old build must never win.
|
|
10
|
+
* - In an installed package, it comes from `dist/build-info.json`,
|
|
11
|
+
* stamped by scripts/generate-build-info.mjs at build/pack time.
|
|
12
|
+
* - When neither yields an identity, `--version` degrades to the plain
|
|
13
|
+
* semver. Reading identity must never throw.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately NOT wired into the per-patch lint cache key: identity
|
|
16
|
+
* would churn the cache on every commit at the same semver for no
|
|
17
|
+
* correctness gain (`getPackageVersion` stays the cache input).
|
|
18
|
+
*/
|
|
19
|
+
import { execFileSync } from 'node:child_process';
|
|
20
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { getPackageRoot, getPackageVersion } from './package-root.js';
|
|
23
|
+
import { isObject } from './validation.js';
|
|
24
|
+
/** @internal Reads the stamped dist/build-info.json; null when absent/invalid. */
|
|
25
|
+
export function readBuildInfoFile(packageRoot) {
|
|
26
|
+
try {
|
|
27
|
+
const raw = readFileSync(join(packageRoot, 'dist', 'build-info.json'), 'utf-8');
|
|
28
|
+
const data = JSON.parse(raw);
|
|
29
|
+
if (!isObject(data) || data['schemaVersion'] !== 1)
|
|
30
|
+
return null;
|
|
31
|
+
const shortCommit = data['shortCommit'];
|
|
32
|
+
const dirty = data['dirty'];
|
|
33
|
+
if (typeof shortCommit !== 'string' || shortCommit.length === 0)
|
|
34
|
+
return null;
|
|
35
|
+
const dirtyHash = data['dirtyHash'];
|
|
36
|
+
return {
|
|
37
|
+
shortCommit,
|
|
38
|
+
dirty: dirty === true,
|
|
39
|
+
...(typeof dirtyHash === 'string' && dirtyHash.length > 0 ? { dirtyHash } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const defaultGitRunner = (args) => execFileSync('git', args, {
|
|
47
|
+
cwd: getPackageRoot(),
|
|
48
|
+
encoding: 'utf-8',
|
|
49
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
50
|
+
});
|
|
51
|
+
/** @internal Live git identity of the checkout; null when git is unusable. */
|
|
52
|
+
export function readGitBuildIdentity(runGit = defaultGitRunner) {
|
|
53
|
+
try {
|
|
54
|
+
const commit = runGit(['rev-parse', 'HEAD']).trim();
|
|
55
|
+
if (!/^[0-9a-f]{7,40}$/.test(commit))
|
|
56
|
+
return null;
|
|
57
|
+
const status = runGit(['status', '--porcelain']);
|
|
58
|
+
return { shortCommit: commit.slice(0, 12), dirty: status.length > 0 };
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Resolves the running build's identity: live git in a checkout (covers
|
|
66
|
+
* worktrees, whose `.git` is a file), else the stamped build-info file,
|
|
67
|
+
* else null. Never throws.
|
|
68
|
+
*/
|
|
69
|
+
function getBuildIdentity() {
|
|
70
|
+
try {
|
|
71
|
+
const packageRoot = getPackageRoot();
|
|
72
|
+
if (existsSync(join(packageRoot, '.git'))) {
|
|
73
|
+
return readGitBuildIdentity();
|
|
74
|
+
}
|
|
75
|
+
return readBuildInfoFile(packageRoot);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* @internal Formats `<version>+g<sha>[.dirty[.<hash>]]`; plain version on
|
|
83
|
+
* null identity. The trailing content hash appears only for a dirty
|
|
84
|
+
* INSTALLED build, where `dist/build-info.json` already carries it — every
|
|
85
|
+
* segment stays valid semver build metadata (dot-separated alphanumerics).
|
|
86
|
+
*/
|
|
87
|
+
export function formatVersionWithIdentity(version, identity) {
|
|
88
|
+
if (identity === null)
|
|
89
|
+
return version;
|
|
90
|
+
if (!identity.dirty)
|
|
91
|
+
return `${version}+g${identity.shortCommit}`;
|
|
92
|
+
const contentHash = identity.dirtyHash === undefined ? '' : `.${identity.dirtyHash}`;
|
|
93
|
+
return `${version}+g${identity.shortCommit}.dirty${contentHash}`;
|
|
94
|
+
}
|
|
95
|
+
/** The `--version` string: semver plus build identity when known. */
|
|
96
|
+
export function getCliVersion() {
|
|
97
|
+
return formatVersionWithIdentity(getPackageVersion(), getBuildIdentity());
|
|
98
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded-concurrency helpers shared by commands that fan out per-file IO
|
|
3
|
+
* (status classification, import's unmanaged-dirty guard, re-export scans,
|
|
4
|
+
* the dry-run purity guard). Kept deliberately tiny: one order-preserving
|
|
5
|
+
* mapper, no queues, no cancellation.
|
|
6
|
+
*/
|
|
7
|
+
/** Maps items with at most `limit` in-flight promises. Preserves order. */
|
|
8
|
+
export declare function mapWithConcurrency<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded-concurrency helpers shared by commands that fan out per-file IO
|
|
3
|
+
* (status classification, import's unmanaged-dirty guard, re-export scans,
|
|
4
|
+
* the dry-run purity guard). Kept deliberately tiny: one order-preserving
|
|
5
|
+
* mapper, no queues, no cancellation.
|
|
6
|
+
*/
|
|
7
|
+
/** Maps items with at most `limit` in-flight promises. Preserves order. */
|
|
8
|
+
export async function mapWithConcurrency(items, limit, fn) {
|
|
9
|
+
const results = new Array(items.length);
|
|
10
|
+
let next = 0;
|
|
11
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
12
|
+
for (;;) {
|
|
13
|
+
const index = next++;
|
|
14
|
+
if (index >= items.length)
|
|
15
|
+
return;
|
|
16
|
+
results[index] = await fn(items[index], index);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
await Promise.all(workers);
|
|
20
|
+
return results;
|
|
21
|
+
}
|
|
@@ -1,2 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Throwable normalisation primitives.
|
|
3
|
+
*
|
|
4
|
+
* **This module must import nothing.** It sits at the very bottom of the
|
|
5
|
+
* dependency graph — 81 modules across `core/`, `commands/`, `utils/` and
|
|
6
|
+
* `bin/` reach it — so it is the one place a shared helper can live and be
|
|
7
|
+
* provably cycle-immune. Adding an import here makes that whole subtree
|
|
8
|
+
* reachable from every util, and any future `src/errors/*` module wanting a
|
|
9
|
+
* util back (`errors/git.ts` already carries a mirrored index-lock heuristic
|
|
10
|
+
* that would like `getNodeErrorCode`) then closes the loop and fails the
|
|
11
|
+
* `dpdm --exit-code circular:1` release gate. A helper that needs an import
|
|
12
|
+
* belongs in a sibling leaf module, not here.
|
|
13
|
+
*/
|
|
1
14
|
/** Normalizes unknown throwables into an Error instance. */
|
|
2
15
|
export declare function toError(error: unknown): Error;
|
|
16
|
+
/**
|
|
17
|
+
* Extracts a Node errno string (`ENOENT`, `EACCES`, `EPERM`, …) from an
|
|
18
|
+
* unknown throwable, or `undefined` when the value carries no string `code`.
|
|
19
|
+
*
|
|
20
|
+
* The check is deliberately structural rather than `error instanceof Error`:
|
|
21
|
+
* a plain object carrying `.code` — exactly the shape {@link toError} exists
|
|
22
|
+
* to normalise — reaches errno consumers through rejected promises and
|
|
23
|
+
* cross-realm throws. Nine hand-rolled copies of this predicate existed
|
|
24
|
+
* before 0.41.0, four of them (in `utils/fs.ts`) gated on `instanceof Error`
|
|
25
|
+
* and so misclassified that shape as "no code".
|
|
26
|
+
*/
|
|
27
|
+
export declare function getNodeErrorCode(error: unknown): string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Checks whether a process with the given PID is still running, using
|
|
30
|
+
* `kill(pid, 0)` — no signal is sent, only the existence check is performed.
|
|
31
|
+
*
|
|
32
|
+
* Only `ESRCH` ("no such process") means dead. **`EPERM` means ALIVE**: the
|
|
33
|
+
* process exists but is owned by another uid, which happens routinely with
|
|
34
|
+
* root-owned builds, `sudo`, shared CI runners and container UID mismatches.
|
|
35
|
+
* Two copies of this predicate (`tree-store.ts`, `doctor-furnace.ts`) read
|
|
36
|
+
* EPERM as dead before 0.41.0, and both gated a recursive delete — a live
|
|
37
|
+
* build's tree clone and a live furnace lock were removed out from under
|
|
38
|
+
* their owner. Any other errno is treated as "unknown, assume alive", which
|
|
39
|
+
* is the safe direction for every caller: it refuses rather than destroys.
|
|
40
|
+
*/
|
|
41
|
+
export declare function isProcessAlive(pid: number): boolean;
|
package/dist/src/utils/errors.js
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Throwable normalisation primitives.
|
|
4
|
+
*
|
|
5
|
+
* **This module must import nothing.** It sits at the very bottom of the
|
|
6
|
+
* dependency graph — 81 modules across `core/`, `commands/`, `utils/` and
|
|
7
|
+
* `bin/` reach it — so it is the one place a shared helper can live and be
|
|
8
|
+
* provably cycle-immune. Adding an import here makes that whole subtree
|
|
9
|
+
* reachable from every util, and any future `src/errors/*` module wanting a
|
|
10
|
+
* util back (`errors/git.ts` already carries a mirrored index-lock heuristic
|
|
11
|
+
* that would like `getNodeErrorCode`) then closes the loop and fails the
|
|
12
|
+
* `dpdm --exit-code circular:1` release gate. A helper that needs an import
|
|
13
|
+
* belongs in a sibling leaf module, not here.
|
|
14
|
+
*/
|
|
2
15
|
/** Normalizes unknown throwables into an Error instance. */
|
|
3
16
|
export function toError(error) {
|
|
4
17
|
if (error instanceof Error) {
|
|
@@ -12,3 +25,45 @@ export function toError(error) {
|
|
|
12
25
|
}
|
|
13
26
|
return new Error(typeof error === 'string' ? error : String(error), { cause: error });
|
|
14
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Extracts a Node errno string (`ENOENT`, `EACCES`, `EPERM`, …) from an
|
|
30
|
+
* unknown throwable, or `undefined` when the value carries no string `code`.
|
|
31
|
+
*
|
|
32
|
+
* The check is deliberately structural rather than `error instanceof Error`:
|
|
33
|
+
* a plain object carrying `.code` — exactly the shape {@link toError} exists
|
|
34
|
+
* to normalise — reaches errno consumers through rejected promises and
|
|
35
|
+
* cross-realm throws. Nine hand-rolled copies of this predicate existed
|
|
36
|
+
* before 0.41.0, four of them (in `utils/fs.ts`) gated on `instanceof Error`
|
|
37
|
+
* and so misclassified that shape as "no code".
|
|
38
|
+
*/
|
|
39
|
+
export function getNodeErrorCode(error) {
|
|
40
|
+
if (typeof error === 'object' &&
|
|
41
|
+
error !== null &&
|
|
42
|
+
'code' in error &&
|
|
43
|
+
typeof error.code === 'string') {
|
|
44
|
+
return error.code;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Checks whether a process with the given PID is still running, using
|
|
50
|
+
* `kill(pid, 0)` — no signal is sent, only the existence check is performed.
|
|
51
|
+
*
|
|
52
|
+
* Only `ESRCH` ("no such process") means dead. **`EPERM` means ALIVE**: the
|
|
53
|
+
* process exists but is owned by another uid, which happens routinely with
|
|
54
|
+
* root-owned builds, `sudo`, shared CI runners and container UID mismatches.
|
|
55
|
+
* Two copies of this predicate (`tree-store.ts`, `doctor-furnace.ts`) read
|
|
56
|
+
* EPERM as dead before 0.41.0, and both gated a recursive delete — a live
|
|
57
|
+
* build's tree clone and a live furnace lock were removed out from under
|
|
58
|
+
* their owner. Any other errno is treated as "unknown, assume alive", which
|
|
59
|
+
* is the safe direction for every caller: it refuses rather than destroys.
|
|
60
|
+
*/
|
|
61
|
+
export function isProcessAlive(pid) {
|
|
62
|
+
try {
|
|
63
|
+
process.kill(pid, 0);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
return getNodeErrorCode(error) !== 'ESRCH';
|
|
68
|
+
}
|
|
69
|
+
}
|
package/dist/src/utils/fs.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { access, chmod, copyFile as fsCopyFile, lstat, mkdir, open, readdir, readFile, rename, rm, stat, statfs, } from 'node:fs/promises';
|
|
4
4
|
import { basename, dirname, join } from 'node:path';
|
|
5
|
+
import { getNodeErrorCode } from './errors.js';
|
|
5
6
|
const RETRIABLE_REMOVE_ERRORS = new Set(['ENOTEMPTY', 'EBUSY', 'EPERM']);
|
|
6
7
|
const RETRIABLE_RENAME_ERRORS = new Set(['EPERM', 'EACCES', 'EBUSY']);
|
|
7
8
|
function sleep(ms) {
|
|
@@ -33,9 +34,7 @@ export async function pathExistsStrict(path) {
|
|
|
33
34
|
return true;
|
|
34
35
|
}
|
|
35
36
|
catch (error) {
|
|
36
|
-
const code = error
|
|
37
|
-
? error.code
|
|
38
|
-
: undefined;
|
|
37
|
+
const code = getNodeErrorCode(error);
|
|
39
38
|
if (code === 'ENOENT') {
|
|
40
39
|
return false;
|
|
41
40
|
}
|
|
@@ -68,9 +67,7 @@ export async function removeDir(path) {
|
|
|
68
67
|
return;
|
|
69
68
|
}
|
|
70
69
|
catch (error) {
|
|
71
|
-
const code = error
|
|
72
|
-
? error.code
|
|
73
|
-
: undefined;
|
|
70
|
+
const code = getNodeErrorCode(error);
|
|
74
71
|
if (!code || !RETRIABLE_REMOVE_ERRORS.has(code) || attempt === 4) {
|
|
75
72
|
throw error;
|
|
76
73
|
}
|
|
@@ -177,9 +174,7 @@ export async function writeFileAtomic(path, content) {
|
|
|
177
174
|
existingMode = (await stat(path)).mode;
|
|
178
175
|
}
|
|
179
176
|
catch (error) {
|
|
180
|
-
const code = error
|
|
181
|
-
? error.code
|
|
182
|
-
: undefined;
|
|
177
|
+
const code = getNodeErrorCode(error);
|
|
183
178
|
if (code !== 'ENOENT') {
|
|
184
179
|
throw error;
|
|
185
180
|
}
|
|
@@ -223,9 +218,7 @@ async function renameWithRetries(from, to) {
|
|
|
223
218
|
return;
|
|
224
219
|
}
|
|
225
220
|
catch (error) {
|
|
226
|
-
const code = error
|
|
227
|
-
? error.code
|
|
228
|
-
: undefined;
|
|
221
|
+
const code = getNodeErrorCode(error);
|
|
229
222
|
if (!code || !RETRIABLE_RENAME_ERRORS.has(code) || attempt === 4) {
|
|
230
223
|
throw error;
|
|
231
224
|
}
|
|
@@ -10,6 +10,8 @@ export declare function setVerbose(enabled: boolean): void;
|
|
|
10
10
|
export declare function setMachineOutputMode(enabled: boolean): void;
|
|
11
11
|
/** True when machine-output mode is active. */
|
|
12
12
|
export declare function isMachineOutputMode(): boolean;
|
|
13
|
+
/** Seals (or unseals) stdout after a machine-readable final line. */
|
|
14
|
+
export declare function setStdoutSealed(sealed: boolean): void;
|
|
13
15
|
/**
|
|
14
16
|
* Displays a verbose/debug message (only shown if verbose mode is enabled).
|
|
15
17
|
* @param message - Message to display
|
|
@@ -54,7 +56,17 @@ export declare function formatErrorText(message: string): string;
|
|
|
54
56
|
export declare function spinner(initialMessage: string): SpinnerHandle;
|
|
55
57
|
/** Emits a cancellation message. */
|
|
56
58
|
export declare function cancel(message: string): void;
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Checks whether a prompt result represents a user cancellation.
|
|
61
|
+
*
|
|
62
|
+
* Narrows to `symbol` — clack returns its cancel sentinel as a symbol and
|
|
63
|
+
* `p.isCancel` is itself a type predicate. This wrapper erased that narrowing
|
|
64
|
+
* by returning plain `boolean` until 0.41.0, which forced eleven `as string`
|
|
65
|
+
* / `as PatchCategory` / `as OverrideType` casts across six command modules
|
|
66
|
+
* on the *non*-cancelled branch, where the value is already known not to be
|
|
67
|
+
* the sentinel. Keep the predicate form: it is the single highest-leverage
|
|
68
|
+
* signature in the codebase for escape-hatch count.
|
|
69
|
+
*/
|
|
70
|
+
export declare function isCancel(value: unknown): value is symbol;
|
|
59
71
|
/** Displays a titled note block for follow-up details. */
|
|
60
72
|
export declare function note(message: string, title?: string): void;
|
package/dist/src/utils/logger.js
CHANGED
|
@@ -34,6 +34,26 @@ export function setMachineOutputMode(enabled) {
|
|
|
34
34
|
export function isMachineOutputMode() {
|
|
35
35
|
return machineOutputMode;
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Whether stdout is sealed behind a machine-readable final line.
|
|
39
|
+
*
|
|
40
|
+
* The `FIREFORGE-VERDICT:` contract promises that line as the run's LAST
|
|
41
|
+
* stdout write — but emit-then-throw failure paths rethrow into
|
|
42
|
+
* `withErrorHandling`, whose clack error/cancel rendering lands on stdout
|
|
43
|
+
* in non-machine mode, displacing the verdict from the final line. The
|
|
44
|
+
* verdict sink seals stdout the moment it writes; every later logger call
|
|
45
|
+
* routes to stderr through the same diagnostic channel machine mode uses.
|
|
46
|
+
* `withErrorHandling`'s finally clears the seal (alongside machine mode).
|
|
47
|
+
*/
|
|
48
|
+
let stdoutSealed = false;
|
|
49
|
+
/** Seals (or unseals) stdout after a machine-readable final line. */
|
|
50
|
+
export function setStdoutSealed(sealed) {
|
|
51
|
+
stdoutSealed = sealed;
|
|
52
|
+
}
|
|
53
|
+
/** True when human output must avoid stdout. */
|
|
54
|
+
function routeToStderr() {
|
|
55
|
+
return machineOutputMode || stdoutSealed;
|
|
56
|
+
}
|
|
37
57
|
/** Writes a plain diagnostic line to stderr (machine-mode side channel). */
|
|
38
58
|
function writeDiagnostic(prefix, message) {
|
|
39
59
|
process.stderr.write(`${prefix}${message}\n`);
|
|
@@ -52,7 +72,7 @@ function isVerbose() {
|
|
|
52
72
|
export function verbose(message) {
|
|
53
73
|
if (!isVerbose())
|
|
54
74
|
return;
|
|
55
|
-
if (
|
|
75
|
+
if (routeToStderr()) {
|
|
56
76
|
writeDiagnostic('[debug] ', message);
|
|
57
77
|
return;
|
|
58
78
|
}
|
|
@@ -63,19 +83,19 @@ function supportsInteractiveSpinner() {
|
|
|
63
83
|
}
|
|
64
84
|
/** Displays the top-level intro banner for a command. */
|
|
65
85
|
export function intro(message) {
|
|
66
|
-
if (
|
|
86
|
+
if (routeToStderr())
|
|
67
87
|
return;
|
|
68
88
|
p.intro(message);
|
|
69
89
|
}
|
|
70
90
|
/** Displays the closing outro banner for a command. */
|
|
71
91
|
export function outro(message) {
|
|
72
|
-
if (
|
|
92
|
+
if (routeToStderr())
|
|
73
93
|
return;
|
|
74
94
|
p.outro(message);
|
|
75
95
|
}
|
|
76
96
|
/** Logs an informational message. */
|
|
77
97
|
export function info(message) {
|
|
78
|
-
if (
|
|
98
|
+
if (routeToStderr()) {
|
|
79
99
|
writeDiagnostic('', message);
|
|
80
100
|
return;
|
|
81
101
|
}
|
|
@@ -83,7 +103,7 @@ export function info(message) {
|
|
|
83
103
|
}
|
|
84
104
|
/** Logs a success message. */
|
|
85
105
|
export function success(message) {
|
|
86
|
-
if (
|
|
106
|
+
if (routeToStderr()) {
|
|
87
107
|
writeDiagnostic('', message);
|
|
88
108
|
return;
|
|
89
109
|
}
|
|
@@ -91,7 +111,7 @@ export function success(message) {
|
|
|
91
111
|
}
|
|
92
112
|
/** Logs a warning message. */
|
|
93
113
|
export function warn(message) {
|
|
94
|
-
if (
|
|
114
|
+
if (routeToStderr()) {
|
|
95
115
|
writeDiagnostic('warning: ', message);
|
|
96
116
|
return;
|
|
97
117
|
}
|
|
@@ -99,7 +119,7 @@ export function warn(message) {
|
|
|
99
119
|
}
|
|
100
120
|
/** Logs an error message. */
|
|
101
121
|
export function error(message) {
|
|
102
|
-
if (
|
|
122
|
+
if (routeToStderr()) {
|
|
103
123
|
writeDiagnostic('error: ', message);
|
|
104
124
|
return;
|
|
105
125
|
}
|
|
@@ -107,7 +127,7 @@ export function error(message) {
|
|
|
107
127
|
}
|
|
108
128
|
/** Logs an in-progress step message. */
|
|
109
129
|
export function step(message) {
|
|
110
|
-
if (
|
|
130
|
+
if (routeToStderr()) {
|
|
111
131
|
writeDiagnostic('', message);
|
|
112
132
|
return;
|
|
113
133
|
}
|
|
@@ -115,7 +135,7 @@ export function step(message) {
|
|
|
115
135
|
}
|
|
116
136
|
/** Logs a plain message without a status prefix. */
|
|
117
137
|
export function message(message) {
|
|
118
|
-
if (
|
|
138
|
+
if (routeToStderr()) {
|
|
119
139
|
writeDiagnostic('', message);
|
|
120
140
|
return;
|
|
121
141
|
}
|
|
@@ -135,7 +155,7 @@ export function formatErrorText(message) {
|
|
|
135
155
|
* @returns Spinner handle with message(), stop(), and error() methods
|
|
136
156
|
*/
|
|
137
157
|
export function spinner(initialMessage) {
|
|
138
|
-
if (
|
|
158
|
+
if (routeToStderr()) {
|
|
139
159
|
// Spinner progress is diagnostics; keep stdout clean for the payload.
|
|
140
160
|
return {
|
|
141
161
|
message: (msg) => {
|
|
@@ -182,19 +202,29 @@ export function spinner(initialMessage) {
|
|
|
182
202
|
}
|
|
183
203
|
/** Emits a cancellation message. */
|
|
184
204
|
export function cancel(message) {
|
|
185
|
-
if (
|
|
205
|
+
if (routeToStderr()) {
|
|
186
206
|
writeDiagnostic('cancelled: ', message);
|
|
187
207
|
return;
|
|
188
208
|
}
|
|
189
209
|
p.cancel(message);
|
|
190
210
|
}
|
|
191
|
-
/**
|
|
211
|
+
/**
|
|
212
|
+
* Checks whether a prompt result represents a user cancellation.
|
|
213
|
+
*
|
|
214
|
+
* Narrows to `symbol` — clack returns its cancel sentinel as a symbol and
|
|
215
|
+
* `p.isCancel` is itself a type predicate. This wrapper erased that narrowing
|
|
216
|
+
* by returning plain `boolean` until 0.41.0, which forced eleven `as string`
|
|
217
|
+
* / `as PatchCategory` / `as OverrideType` casts across six command modules
|
|
218
|
+
* on the *non*-cancelled branch, where the value is already known not to be
|
|
219
|
+
* the sentinel. Keep the predicate form: it is the single highest-leverage
|
|
220
|
+
* signature in the codebase for escape-hatch count.
|
|
221
|
+
*/
|
|
192
222
|
export function isCancel(value) {
|
|
193
223
|
return p.isCancel(value);
|
|
194
224
|
}
|
|
195
225
|
/** Displays a titled note block for follow-up details. */
|
|
196
226
|
export function note(message, title) {
|
|
197
|
-
if (
|
|
227
|
+
if (routeToStderr()) {
|
|
198
228
|
writeDiagnostic('', title ? `${title}: ${message}` : message);
|
|
199
229
|
return;
|
|
200
230
|
}
|
|
@@ -22,6 +22,21 @@ import type { Command } from 'commander';
|
|
|
22
22
|
* helper (or throw commander's InvalidArgumentError directly).
|
|
23
23
|
*/
|
|
24
24
|
export declare function commanderArgParser<T>(parse: (raw: string) => T): (raw: string) => T;
|
|
25
|
+
/**
|
|
26
|
+
* Commander plumbing for a repeatable string option, as a spreadable
|
|
27
|
+
* `[accumulator, default]` pair:
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* .option('--mach-arg <arg>', 'Repeatable.', ...stringListOption())
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* Seven call sites open-coded this, each with its own `[] as string[]`
|
|
34
|
+
* default — commander types that slot as `unknown`, so every one carried the
|
|
35
|
+
* cast. Two of the seven also mutated the accumulator in place rather than
|
|
36
|
+
* returning a new array. Returning a fresh array per call additionally means
|
|
37
|
+
* the default cannot be shared between two `.option()` registrations.
|
|
38
|
+
*/
|
|
39
|
+
export declare function stringListOption(): [(value: string, previous: string[]) => string[], string[]];
|
|
25
40
|
/**
|
|
26
41
|
* Resolves the parsed `--wait-lock [seconds]` option value into a wait budget
|
|
27
42
|
* in seconds for `withEngineSessionLock`:
|
|
@@ -35,9 +50,11 @@ export declare function commanderArgParser<T>(parse: (raw: string) => T): (raw:
|
|
|
35
50
|
*/
|
|
36
51
|
export declare function resolveWaitLockSeconds(value: string | number | boolean | undefined): number | undefined;
|
|
37
52
|
/**
|
|
38
|
-
* Registers the shared `--wait-lock [seconds]` flag on
|
|
39
|
-
* command
|
|
40
|
-
*
|
|
41
|
-
*
|
|
53
|
+
* Registers the shared `--wait-lock [seconds]` flag on a lock-taking
|
|
54
|
+
* command (the engine session lock for engine-mutating commands, the patch
|
|
55
|
+
* directory lock for queue-mutating ones). The parsed option value is `true`
|
|
56
|
+
* for the bare flag or a validated integer; feed it through
|
|
57
|
+
* {@link resolveWaitLockSeconds} at the call site to obtain the
|
|
58
|
+
* `waitLockSeconds` for `withEngineSessionLock`/`withPatchDirectoryLock`.
|
|
42
59
|
*/
|
|
43
60
|
export declare function addWaitLockOption(command: Command): Command;
|
|
@@ -15,6 +15,7 @@ export function pickDefined(obj) {
|
|
|
15
15
|
}
|
|
16
16
|
import { InvalidArgumentError as CommanderInvalidArgumentError } from 'commander';
|
|
17
17
|
import { GeneralError } from '../errors/base.js';
|
|
18
|
+
import { toError } from './errors.js';
|
|
18
19
|
/**
|
|
19
20
|
* Wraps an option-argument parser so its failures surface through
|
|
20
21
|
* commander's own invalid-argument channel.
|
|
@@ -35,10 +36,27 @@ export function commanderArgParser(parse) {
|
|
|
35
36
|
return parse(raw);
|
|
36
37
|
}
|
|
37
38
|
catch (error) {
|
|
38
|
-
throw new CommanderInvalidArgumentError(error
|
|
39
|
+
throw new CommanderInvalidArgumentError(toError(error).message);
|
|
39
40
|
}
|
|
40
41
|
};
|
|
41
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Commander plumbing for a repeatable string option, as a spreadable
|
|
45
|
+
* `[accumulator, default]` pair:
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* .option('--mach-arg <arg>', 'Repeatable.', ...stringListOption())
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* Seven call sites open-coded this, each with its own `[] as string[]`
|
|
52
|
+
* default — commander types that slot as `unknown`, so every one carried the
|
|
53
|
+
* cast. Two of the seven also mutated the accumulator in place rather than
|
|
54
|
+
* returning a new array. Returning a fresh array per call additionally means
|
|
55
|
+
* the default cannot be shared between two `.option()` registrations.
|
|
56
|
+
*/
|
|
57
|
+
export function stringListOption() {
|
|
58
|
+
return [(value, previous) => [...previous, value], []];
|
|
59
|
+
}
|
|
42
60
|
/** Wait budget applied when `--wait-lock` is passed without a value. */
|
|
43
61
|
const DEFAULT_WAIT_LOCK_SECONDS = 60;
|
|
44
62
|
/**
|
|
@@ -69,11 +87,13 @@ export function resolveWaitLockSeconds(value) {
|
|
|
69
87
|
return n;
|
|
70
88
|
}
|
|
71
89
|
/**
|
|
72
|
-
* Registers the shared `--wait-lock [seconds]` flag on
|
|
73
|
-
* command
|
|
74
|
-
*
|
|
75
|
-
*
|
|
90
|
+
* Registers the shared `--wait-lock [seconds]` flag on a lock-taking
|
|
91
|
+
* command (the engine session lock for engine-mutating commands, the patch
|
|
92
|
+
* directory lock for queue-mutating ones). The parsed option value is `true`
|
|
93
|
+
* for the bare flag or a validated integer; feed it through
|
|
94
|
+
* {@link resolveWaitLockSeconds} at the call site to obtain the
|
|
95
|
+
* `waitLockSeconds` for `withEngineSessionLock`/`withPatchDirectoryLock`.
|
|
76
96
|
*/
|
|
77
97
|
export function addWaitLockOption(command) {
|
|
78
|
-
return command.option('--wait-lock [seconds]', 'Wait up to this many seconds (default 60) for another FireForge
|
|
98
|
+
return command.option('--wait-lock [seconds]', 'Wait up to this many seconds (default 60) for another FireForge command to release the contended lock, instead of failing on the default budget', commanderArgParser((raw) => resolveWaitLockSeconds(raw)));
|
|
79
99
|
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { getNodeErrorCode, toError } from './errors.js';
|
|
6
|
+
import { isObject } from './validation.js';
|
|
5
7
|
function validatePackageMetadata(data, filePath) {
|
|
6
8
|
if (typeof data !== 'object' || data === null) {
|
|
7
9
|
throw new Error(`Invalid package metadata in ${filePath}: expected an object`);
|
|
@@ -48,10 +50,11 @@ export function getPackageRoot() {
|
|
|
48
50
|
// to a wrong ancestor package (any parent exposing bin.fireforge) or
|
|
49
51
|
// end in the unhelpful generic "could not locate" error, hiding the
|
|
50
52
|
// actual syntax problem.
|
|
51
|
-
const code = error
|
|
53
|
+
const code = getNodeErrorCode(error);
|
|
52
54
|
if (code !== 'ENOENT' && code !== 'ENOTDIR') {
|
|
53
|
-
throw new Error(`Found ${packagePath} but could not parse it: ` +
|
|
54
|
-
|
|
55
|
+
throw new Error(`Found ${packagePath} but could not parse it: ` + toError(error).message, {
|
|
56
|
+
cause: error,
|
|
57
|
+
});
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
60
|
const parent = dirname(current);
|
|
@@ -63,7 +66,7 @@ export function getPackageRoot() {
|
|
|
63
66
|
}
|
|
64
67
|
/** @internal */
|
|
65
68
|
export function isFireForgePackageMetadata(pkg) {
|
|
66
|
-
if (
|
|
69
|
+
if (!isObject(pkg.bin)) {
|
|
67
70
|
return false;
|
|
68
71
|
}
|
|
69
72
|
const bin = pkg.bin;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* two modules do not import each other cyclically.
|
|
7
7
|
*/
|
|
8
8
|
import { spawn } from 'node:child_process';
|
|
9
|
+
import { getNodeErrorCode } from './errors.js';
|
|
9
10
|
import { verbose, warn } from './logger.js';
|
|
10
11
|
/**
|
|
11
12
|
* Sends `signal` to the child's whole tree: the process GROUP on POSIX
|
|
@@ -99,8 +100,14 @@ async function listGroupSurvivors(pgid) {
|
|
|
99
100
|
process.kill(-pgid, 0);
|
|
100
101
|
return [{ pid: -1, command: 'unknown (pgrep unavailable; group still has live members)' }];
|
|
101
102
|
}
|
|
102
|
-
catch {
|
|
103
|
-
|
|
103
|
+
catch (error) {
|
|
104
|
+
// Same errno semantics as `isProcessAlive` (which cannot be called here —
|
|
105
|
+
// it probes a single positive pid): only ESRCH proves the group is gone.
|
|
106
|
+
// EPERM means it exists under another uid, and an unrecognized failure is
|
|
107
|
+
// not evidence of absence — report survivors in both cases.
|
|
108
|
+
if (getNodeErrorCode(error) === 'ESRCH')
|
|
109
|
+
return [];
|
|
110
|
+
return [{ pid: -1, command: 'unknown (pgrep unavailable; group liveness probe denied)' }];
|
|
104
111
|
}
|
|
105
112
|
}
|
|
106
113
|
function describeSurvivors(list) {
|
|
@@ -133,6 +140,8 @@ export async function sweepProcessGroup(pgid, graceMs = SWEEP_GRACE_MS) {
|
|
|
133
140
|
process.kill(-pgid, 'SIGTERM');
|
|
134
141
|
}
|
|
135
142
|
catch {
|
|
143
|
+
// The group vanished between the listing and the signal — nothing left to
|
|
144
|
+
// escalate against, so report the survivors collected so far.
|
|
136
145
|
return { survivors };
|
|
137
146
|
}
|
|
138
147
|
await sweepDelay(graceMs);
|
|
@@ -142,6 +151,8 @@ export async function sweepProcessGroup(pgid, graceMs = SWEEP_GRACE_MS) {
|
|
|
142
151
|
process.kill(-pgid, 'SIGKILL');
|
|
143
152
|
}
|
|
144
153
|
catch {
|
|
154
|
+
// The group died during the grace period, so the SIGKILL escalation has
|
|
155
|
+
// nothing to signal.
|
|
145
156
|
return { survivors };
|
|
146
157
|
}
|
|
147
158
|
await sweepDelay(Math.min(200, graceMs));
|