@hominis/fireforge 0.10.1 → 0.11.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 +93 -1
- package/README.md +125 -238
- package/dist/bin/fireforge.js +26 -0
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +131 -52
- package/dist/src/commands/bootstrap.js +6 -2
- package/dist/src/commands/build.js +4 -2
- package/dist/src/commands/discard.js +16 -4
- package/dist/src/commands/doctor-furnace.d.ts +8 -0
- package/dist/src/commands/doctor-furnace.js +422 -0
- package/dist/src/commands/doctor.d.ts +115 -0
- package/dist/src/commands/doctor.js +327 -258
- package/dist/src/commands/download.js +16 -1
- package/dist/src/commands/export-all.js +15 -0
- package/dist/src/commands/export-flow.d.ts +91 -0
- package/dist/src/commands/export-flow.js +344 -0
- package/dist/src/commands/export.js +151 -5
- package/dist/src/commands/furnace/apply.d.ts +3 -2
- package/dist/src/commands/furnace/apply.js +169 -36
- package/dist/src/commands/furnace/create.js +162 -52
- package/dist/src/commands/furnace/deploy.js +156 -144
- package/dist/src/commands/furnace/diff.d.ts +8 -4
- package/dist/src/commands/furnace/diff.js +142 -73
- package/dist/src/commands/furnace/index.d.ts +6 -2
- package/dist/src/commands/furnace/index.js +76 -25
- package/dist/src/commands/furnace/init.d.ts +11 -0
- package/dist/src/commands/furnace/init.js +76 -0
- package/dist/src/commands/furnace/list.d.ts +4 -1
- package/dist/src/commands/furnace/list.js +35 -3
- package/dist/src/commands/furnace/override.d.ts +8 -0
- package/dist/src/commands/furnace/override.js +216 -26
- package/dist/src/commands/furnace/preview.js +184 -30
- package/dist/src/commands/furnace/refresh.d.ts +10 -0
- package/dist/src/commands/furnace/refresh.js +268 -0
- package/dist/src/commands/furnace/remove.js +285 -89
- package/dist/src/commands/furnace/rename.d.ts +5 -0
- package/dist/src/commands/furnace/rename.js +308 -0
- package/dist/src/commands/furnace/scan.d.ts +4 -1
- package/dist/src/commands/furnace/scan.js +72 -11
- package/dist/src/commands/furnace/status.js +85 -20
- package/dist/src/commands/furnace/sync.d.ts +12 -0
- package/dist/src/commands/furnace/sync.js +77 -0
- package/dist/src/commands/furnace/validate.d.ts +4 -1
- package/dist/src/commands/furnace/validate.js +99 -3
- package/dist/src/commands/furnace/validation-output.d.ts +24 -1
- package/dist/src/commands/furnace/validation-output.js +93 -1
- package/dist/src/commands/import.js +37 -4
- package/dist/src/commands/lint.js +11 -2
- package/dist/src/commands/manifest.d.ts +39 -0
- package/dist/src/commands/manifest.js +59 -0
- package/dist/src/commands/patch/delete.d.ts +28 -0
- package/dist/src/commands/patch/delete.js +209 -0
- package/dist/src/commands/patch/index.d.ts +17 -0
- package/dist/src/commands/patch/index.js +25 -0
- package/dist/src/commands/patch/reorder.d.ts +30 -0
- package/dist/src/commands/patch/reorder.js +377 -0
- package/dist/src/commands/re-export-files.d.ts +17 -0
- package/dist/src/commands/re-export-files.js +177 -0
- package/dist/src/commands/re-export.js +44 -0
- package/dist/src/commands/rebase/abort.d.ts +1 -1
- package/dist/src/commands/rebase/abort.js +12 -3
- package/dist/src/commands/rebase/confirm.d.ts +3 -3
- package/dist/src/commands/rebase/confirm.js +4 -4
- package/dist/src/commands/rebase/index.js +13 -4
- package/dist/src/commands/reset.js +20 -4
- package/dist/src/commands/run.js +46 -1
- package/dist/src/commands/setup-support.js +5 -5
- package/dist/src/commands/status.js +97 -6
- package/dist/src/commands/test.js +5 -37
- package/dist/src/commands/verify.d.ts +31 -0
- package/dist/src/commands/verify.js +126 -0
- package/dist/src/core/build-prepare.js +40 -16
- package/dist/src/core/destructive.d.ts +96 -0
- package/dist/src/core/destructive.js +137 -0
- package/dist/src/core/diff-hunks.d.ts +73 -0
- package/dist/src/core/diff-hunks.js +268 -0
- package/dist/src/core/firefox.d.ts +1 -1
- package/dist/src/core/firefox.js +1 -1
- package/dist/src/core/furnace-apply-helpers.d.ts +89 -6
- package/dist/src/core/furnace-apply-helpers.js +302 -57
- package/dist/src/core/furnace-apply-output.d.ts +16 -0
- package/dist/src/core/furnace-apply-output.js +57 -0
- package/dist/src/core/furnace-apply.d.ts +21 -3
- package/dist/src/core/furnace-apply.js +260 -29
- package/dist/src/core/furnace-checksum-utils.d.ts +4 -0
- package/dist/src/core/furnace-checksum-utils.js +24 -0
- package/dist/src/core/furnace-config.d.ts +28 -1
- package/dist/src/core/furnace-config.js +180 -17
- package/dist/src/core/furnace-constants.d.ts +22 -0
- package/dist/src/core/furnace-constants.js +36 -0
- package/dist/src/core/furnace-graph-utils.d.ts +11 -0
- package/dist/src/core/furnace-graph-utils.js +94 -0
- package/dist/src/core/furnace-operation.d.ts +108 -0
- package/dist/src/core/furnace-operation.js +220 -0
- package/dist/src/core/furnace-refresh.d.ts +20 -0
- package/dist/src/core/furnace-refresh.js +118 -0
- package/dist/src/core/furnace-registration-ast.d.ts +5 -0
- package/dist/src/core/furnace-registration-ast.js +134 -4
- package/dist/src/core/furnace-registration-remove.d.ts +25 -3
- package/dist/src/core/furnace-registration-remove.js +196 -62
- package/dist/src/core/furnace-registration-validate.d.ts +13 -1
- package/dist/src/core/furnace-registration-validate.js +15 -3
- package/dist/src/core/furnace-registration.d.ts +27 -4
- package/dist/src/core/furnace-registration.js +93 -11
- package/dist/src/core/furnace-rollback.d.ts +11 -0
- package/dist/src/core/furnace-rollback.js +78 -7
- package/dist/src/core/furnace-scanner.d.ts +8 -2
- package/dist/src/core/furnace-scanner.js +152 -55
- package/dist/src/core/furnace-stories.js +7 -5
- package/dist/src/core/furnace-validate-accessibility.js +7 -1
- package/dist/src/core/furnace-validate-compatibility.d.ts +1 -1
- package/dist/src/core/furnace-validate-compatibility.js +85 -1
- package/dist/src/core/furnace-validate-helpers.d.ts +4 -0
- package/dist/src/core/furnace-validate-helpers.js +31 -0
- package/dist/src/core/furnace-validate-registration.d.ts +17 -2
- package/dist/src/core/furnace-validate-registration.js +73 -3
- package/dist/src/core/furnace-validate-structure.d.ts +10 -2
- package/dist/src/core/furnace-validate-structure.js +45 -3
- package/dist/src/core/furnace-validate.d.ts +10 -1
- package/dist/src/core/furnace-validate.js +80 -6
- package/dist/src/core/furnace-version-drift.d.ts +55 -0
- package/dist/src/core/furnace-version-drift.js +101 -0
- package/dist/src/core/git-file-ops.d.ts +8 -0
- package/dist/src/core/git-file-ops.js +19 -6
- package/dist/src/core/lint-projection.d.ts +25 -0
- package/dist/src/core/lint-projection.js +44 -0
- package/dist/src/core/mach.d.ts +4 -2
- package/dist/src/core/mach.js +17 -2
- package/dist/src/core/markdown-table.d.ts +104 -0
- package/dist/src/core/markdown-table.js +266 -0
- package/dist/src/core/ownership-table.d.ts +53 -0
- package/dist/src/core/ownership-table.js +144 -0
- package/dist/src/core/patch-apply.d.ts +17 -3
- package/dist/src/core/patch-apply.js +86 -8
- package/dist/src/core/patch-export.d.ts +119 -5
- package/dist/src/core/patch-export.js +183 -25
- package/dist/src/core/patch-lint-cross.d.ts +195 -0
- package/dist/src/core/patch-lint-cross.js +428 -0
- package/dist/src/core/patch-lint-diff.d.ts +33 -0
- package/dist/src/core/patch-lint-diff.js +84 -0
- package/dist/src/core/patch-lint.d.ts +2 -4
- package/dist/src/core/patch-lint.js +12 -50
- package/dist/src/core/patch-lock.js +2 -1
- package/dist/src/core/patch-manifest-io.d.ts +102 -1
- package/dist/src/core/patch-manifest-io.js +270 -2
- package/dist/src/core/patch-manifest-query.d.ts +1 -1
- package/dist/src/core/patch-manifest-query.js +1 -1
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-manifest.js +1 -1
- package/dist/src/core/patch-transform.d.ts +12 -0
- package/dist/src/core/patch-transform.js +21 -7
- package/dist/src/core/token-manager.js +67 -69
- package/dist/src/core/wire-destroy.js +6 -3
- package/dist/src/core/wire-init.js +10 -4
- package/dist/src/core/wire-subscript.js +9 -3
- package/dist/src/core/wire-utils.d.ts +52 -5
- package/dist/src/core/wire-utils.js +69 -6
- package/dist/src/errors/base.d.ts +20 -0
- package/dist/src/errors/base.js +24 -0
- package/dist/src/errors/furnace.js +7 -1
- package/dist/src/errors/rebase.js +6 -1
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/options.d.ts +125 -4
- package/dist/src/types/commands/patches.d.ts +11 -1
- package/dist/src/types/config.d.ts +1 -1
- package/dist/src/types/furnace.d.ts +55 -1
- package/dist/src/utils/fs.d.ts +12 -0
- package/dist/src/utils/fs.js +30 -1
- package/dist/src/utils/package-root.d.ts +5 -0
- package/dist/src/utils/package-root.js +12 -0
- package/dist/src/utils/process.js +9 -4
- package/dist/src/utils/validation.d.ts +20 -2
- package/dist/src/utils/validation.js +26 -3
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { configExists, getProjectPaths, loadConfig, loadState } from '../core/config.js';
|
|
2
|
+
import { furnaceConfigExists as checkFurnaceConfigExists } from '../core/furnace-config.js';
|
|
2
3
|
import { getCurrentBranch, getHead, isGitRepository, isMissingHeadError } from '../core/git.js';
|
|
3
4
|
import { ensureGit } from '../core/git-base.js';
|
|
4
5
|
import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/git-status.js';
|
|
@@ -9,169 +10,338 @@ import { ExitCode } from '../errors/codes.js';
|
|
|
9
10
|
import { toError } from '../utils/errors.js';
|
|
10
11
|
import { pathExists } from '../utils/fs.js';
|
|
11
12
|
import { error, info, intro, outro, success, warn } from '../utils/logger.js';
|
|
13
|
+
import { FURNACE_DOCTOR_CHECKS } from './doctor-furnace.js';
|
|
12
14
|
/**
|
|
13
|
-
*
|
|
15
|
+
* Builds a DoctorCheck object representing a successful "OK" check.
|
|
16
|
+
* Exported for sibling check modules that declare `DoctorCheckDefinition`
|
|
17
|
+
* entries out-of-file (e.g. `doctor-furnace.ts`).
|
|
14
18
|
*/
|
|
15
|
-
|
|
19
|
+
export function ok(name) {
|
|
20
|
+
return { name, passed: true, severity: 'ok', message: 'OK' };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Builds a DoctorCheck object representing a warning result.
|
|
24
|
+
* Exported for sibling check modules — see {@link ok}.
|
|
25
|
+
*/
|
|
26
|
+
export function warning(name, message, fix) {
|
|
27
|
+
return {
|
|
28
|
+
name,
|
|
29
|
+
passed: true,
|
|
30
|
+
severity: 'warning',
|
|
31
|
+
warning: true,
|
|
32
|
+
message,
|
|
33
|
+
...(fix ? { fix } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Builds a DoctorCheck object representing a failure result.
|
|
38
|
+
* Exported for sibling check modules — see {@link ok}.
|
|
39
|
+
*/
|
|
40
|
+
export function failure(name, message, fix) {
|
|
41
|
+
return { name, passed: false, severity: 'error', message, ...(fix ? { fix } : {}) };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Runs a single check definition, converting thrown errors into
|
|
45
|
+
* DoctorCheck failure rows. Always returns an array so the caller can
|
|
46
|
+
* flatten results uniformly.
|
|
47
|
+
*/
|
|
48
|
+
async function executeCheck(definition, ctx) {
|
|
49
|
+
if (definition.skipIf?.(ctx)) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
16
52
|
try {
|
|
17
|
-
await
|
|
18
|
-
return
|
|
53
|
+
const result = await definition.run(ctx);
|
|
54
|
+
return Array.isArray(result) ? result : [result];
|
|
19
55
|
}
|
|
20
|
-
catch (
|
|
21
|
-
|
|
22
|
-
const result = { name, passed: false, severity: 'error', message };
|
|
23
|
-
if (fix !== undefined) {
|
|
24
|
-
result.fix = fix;
|
|
25
|
-
}
|
|
26
|
-
return result;
|
|
56
|
+
catch (err) {
|
|
57
|
+
return [failure(definition.name, toError(err).message, definition.fix)];
|
|
27
58
|
}
|
|
28
59
|
}
|
|
29
60
|
function summarizeWorkingTreeChangeCount(changeCount) {
|
|
30
61
|
return `Engine working tree has ${changeCount} local change${changeCount === 1 ? '' : 's'}. Some FireForge commands assume a clean baseline and may behave differently until these are exported, discarded, or committed.`;
|
|
31
62
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Runs the subset of engine checks that depend on a healthy git repository
|
|
65
|
+
* and HEAD. This group shares mutable state (currentHead, canValidateBranch),
|
|
66
|
+
* so it lives as a single definition returning multiple rows.
|
|
67
|
+
*/
|
|
68
|
+
async function runEngineGitChecks(ctx) {
|
|
69
|
+
const { paths, state } = ctx;
|
|
70
|
+
const rows = [];
|
|
71
|
+
let currentHead;
|
|
72
|
+
let canValidateBranch = true;
|
|
73
|
+
if (state.baseCommit) {
|
|
74
|
+
try {
|
|
75
|
+
currentHead = await getHead(paths.engine);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
if (!isMissingHeadError(err)) {
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
canValidateBranch = false;
|
|
82
|
+
rows.push(failure('Engine state consistency', 'Engine repository has no baseline commit yet. A previous "fireforge download" likely stopped after git init but before the initial Firefox commit was created.', 'Re-run "fireforge download --force" to recreate the baseline repository cleanly.'));
|
|
83
|
+
}
|
|
84
|
+
if (canValidateBranch && currentHead !== state.baseCommit) {
|
|
85
|
+
rows.push(failure('Engine state consistency', 'HEAD differs from baseCommit. FireForge expects the engine repository to remain at the downloaded baseline commit; branch switches or commits inside engine/ can break import, resolve, and patch regeneration workflows.', 'Reset engine/ to the baseline commit or re-run "fireforge download --force".'));
|
|
86
|
+
}
|
|
87
|
+
else if (canValidateBranch) {
|
|
88
|
+
rows.push(ok('Engine state consistency'));
|
|
89
|
+
}
|
|
36
90
|
}
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
currentHead = await getHead(paths.engine);
|
|
91
|
+
const rawStatus = await getWorkingTreeStatus(paths.engine);
|
|
92
|
+
const workingTreeStatus = await expandUntrackedDirectoryEntries(paths.engine, rawStatus);
|
|
93
|
+
if (workingTreeStatus.length > 0) {
|
|
94
|
+
rows.push(warning('Engine working tree', summarizeWorkingTreeChangeCount(workingTreeStatus.length), 'Use "fireforge status" to review changes, then export, discard, or reset them as appropriate.'));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
rows.push(ok('Engine working tree'));
|
|
98
|
+
}
|
|
99
|
+
let branch;
|
|
100
|
+
if (canValidateBranch) {
|
|
101
|
+
try {
|
|
102
|
+
branch = await getCurrentBranch(paths.engine);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
if (!isMissingHeadError(err)) {
|
|
106
|
+
throw err;
|
|
54
107
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
108
|
+
canValidateBranch = false;
|
|
109
|
+
rows.push(failure('Engine branch', 'Engine repository has no baseline commit yet. A previous "fireforge download" likely stopped before git created the initial Firefox commit.', 'Re-run "fireforge download --force" to recreate the baseline repository cleanly.'));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!canValidateBranch &&
|
|
113
|
+
branch === undefined &&
|
|
114
|
+
currentHead === undefined &&
|
|
115
|
+
!state.baseCommit) {
|
|
116
|
+
// Unborn repository with no recorded baseline — the earlier failure row
|
|
117
|
+
// explains recovery; avoid adding a second near-identical row.
|
|
118
|
+
}
|
|
119
|
+
else if (!canValidateBranch) {
|
|
120
|
+
rows.push(warning('Engine branch', 'Skipped branch validation because the baseline commit is missing.', 'Finish recreating the engine baseline with "fireforge download --force".'));
|
|
121
|
+
}
|
|
122
|
+
else if (branch === 'firefox') {
|
|
123
|
+
rows.push(ok('Engine branch'));
|
|
124
|
+
}
|
|
125
|
+
else if (branch === 'HEAD' && state.baseCommit && currentHead === state.baseCommit) {
|
|
126
|
+
rows.push(warning('Engine branch', 'Engine is detached at the recorded base commit. This is acceptable for disposable worktrees and audit clones.', 'If this is your primary workspace, checkout the "firefox" branch to match FireForge defaults.'));
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
rows.push(failure('Engine branch', `Engine is on branch "${branch}", but expected "firefox".`));
|
|
130
|
+
}
|
|
131
|
+
return rows;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Validates that every check's `dependsOn` entries appear earlier in the
|
|
135
|
+
* registry. Called once at module load time so a broken reorder surfaces
|
|
136
|
+
* immediately as a thrown error rather than producing a subtle
|
|
137
|
+
* context-population bug at runtime.
|
|
138
|
+
*/
|
|
139
|
+
function validateCheckDependencies(checks) {
|
|
140
|
+
const seen = new Set();
|
|
141
|
+
for (const check of checks) {
|
|
142
|
+
if (check.dependsOn) {
|
|
143
|
+
for (const dep of check.dependsOn) {
|
|
144
|
+
if (!seen.has(dep)) {
|
|
145
|
+
throw new Error(`Doctor check "${check.name}" declares dependsOn "${dep}", ` +
|
|
146
|
+
`but "${dep}" does not appear earlier in the registry. ` +
|
|
147
|
+
'Fix the ordering in DOCTOR_CHECKS.');
|
|
58
148
|
}
|
|
59
|
-
canValidateBranch = false;
|
|
60
|
-
checks.push({
|
|
61
|
-
name: 'Engine state consistency',
|
|
62
|
-
passed: false,
|
|
63
|
-
severity: 'error',
|
|
64
|
-
message: 'Engine repository has no baseline commit yet. A previous "fireforge download" likely stopped after git init but before the initial Firefox commit was created.',
|
|
65
|
-
fix: 'Re-run "fireforge download --force" to recreate the baseline repository cleanly.',
|
|
66
|
-
});
|
|
67
149
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
150
|
+
}
|
|
151
|
+
seen.add(check.name);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The declarative doctor check registry. The order of entries here is the
|
|
156
|
+
* order checks appear in the report. Adding a new check is a one-entry
|
|
157
|
+
* edit; each check only contains its own inspection logic.
|
|
158
|
+
*
|
|
159
|
+
* ## Ordering dependency chain
|
|
160
|
+
*
|
|
161
|
+
* Later checks may read state populated by earlier ones via the shared
|
|
162
|
+
* {@link DoctorCheckContext}. Dependencies are declared via the
|
|
163
|
+
* `dependsOn` field and enforced by {@link validateCheckDependencies}
|
|
164
|
+
* at module load time.
|
|
165
|
+
*
|
|
166
|
+
* {@link DOCTOR_CHECK_ORDER} is exported so tests can pin the sequence.
|
|
167
|
+
*/
|
|
168
|
+
const DOCTOR_CHECKS = [
|
|
169
|
+
{
|
|
170
|
+
name: 'Git installed',
|
|
171
|
+
run: async () => {
|
|
172
|
+
await ensureGit();
|
|
173
|
+
return ok('Git installed');
|
|
174
|
+
},
|
|
175
|
+
fix: 'Install git from https://git-scm.com/',
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: 'Python supported by mach',
|
|
179
|
+
run: async (ctx) => {
|
|
180
|
+
await ensurePython(ctx.paths.engine);
|
|
181
|
+
return ok('Python supported by mach');
|
|
182
|
+
},
|
|
183
|
+
fix: 'Install a Python version supported by engine/mach, then re-run "fireforge doctor".',
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: 'fireforge.json exists',
|
|
187
|
+
run: async (ctx) => {
|
|
188
|
+
if (!(await configExists(ctx.projectRoot))) {
|
|
189
|
+
throw new Error('fireforge.json not found');
|
|
190
|
+
}
|
|
191
|
+
return ok('fireforge.json exists');
|
|
192
|
+
},
|
|
193
|
+
fix: 'Run "fireforge setup" to create a project',
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: 'fireforge.json is valid',
|
|
197
|
+
run: async (ctx) => {
|
|
198
|
+
ctx.config = await loadConfig(ctx.projectRoot);
|
|
199
|
+
return ok('fireforge.json is valid');
|
|
200
|
+
},
|
|
201
|
+
fix: 'Check fireforge.json for syntax errors or missing fields',
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: 'Engine directory exists',
|
|
205
|
+
run: (ctx) => {
|
|
206
|
+
if (!ctx.engineExists) {
|
|
207
|
+
throw new Error('engine/ directory not found');
|
|
76
208
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
209
|
+
return ok('Engine directory exists');
|
|
210
|
+
},
|
|
211
|
+
fix: 'Run "fireforge download" to download Firefox source',
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'Pending Resolution',
|
|
215
|
+
skipIf: (ctx) => !ctx.state.pendingResolution,
|
|
216
|
+
run: (ctx) => {
|
|
217
|
+
const patchFilename = ctx.state.pendingResolution?.patchFilename ?? 'unknown';
|
|
218
|
+
return failure('Pending Resolution', `You are currently resolving a conflict for patch ${patchFilename}.`, 'Build and Export commands may behave unexpectedly until "fireforge resolve" is completed.');
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'Engine is git repository',
|
|
223
|
+
skipIf: (ctx) => !ctx.engineExists,
|
|
224
|
+
run: async (ctx) => {
|
|
225
|
+
const isRepo = await isGitRepository(ctx.paths.engine);
|
|
226
|
+
if (!isRepo) {
|
|
227
|
+
return failure('Engine is git repository', 'engine/ is not a git repository', 'Run "fireforge download --force" to reinitialize');
|
|
84
228
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
229
|
+
// Git-dependent follow-up checks share mutable currentHead/branch
|
|
230
|
+
// state, so they live in a helper that returns all rows at once.
|
|
231
|
+
return [ok('Engine is git repository'), ...(await runEngineGitChecks(ctx))];
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: 'mach available',
|
|
236
|
+
skipIf: (ctx) => !ctx.engineExists,
|
|
237
|
+
run: async (ctx) => {
|
|
238
|
+
await ensureMach(ctx.paths.engine);
|
|
239
|
+
return ok('mach available');
|
|
240
|
+
},
|
|
241
|
+
fix: 'Firefox source may be corrupted. Re-download with "fireforge download --force"',
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: 'Patches directory exists',
|
|
245
|
+
run: async (ctx) => {
|
|
246
|
+
const patchesExist = await pathExists(ctx.paths.patches);
|
|
247
|
+
return {
|
|
248
|
+
name: 'Patches directory exists',
|
|
91
249
|
passed: true,
|
|
92
|
-
severity: '
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
250
|
+
severity: 'ok',
|
|
251
|
+
message: patchesExist ? 'OK' : 'No patches/ directory (optional)',
|
|
252
|
+
};
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: 'Patches found',
|
|
257
|
+
run: async (ctx) => {
|
|
258
|
+
if (!(await pathExists(ctx.paths.patches))) {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
const patchCount = await countPatches(ctx.paths.patches);
|
|
262
|
+
return {
|
|
263
|
+
name: 'Patches found',
|
|
101
264
|
passed: true,
|
|
102
265
|
severity: 'ok',
|
|
103
|
-
message: '
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
266
|
+
message: `${patchCount} patch${patchCount === 1 ? '' : 'es'} found`,
|
|
267
|
+
};
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: 'Patch manifest consistency',
|
|
272
|
+
dependsOn: ['fireforge.json is valid'],
|
|
273
|
+
run: async (ctx) => {
|
|
274
|
+
if (!(await pathExists(ctx.paths.patches))) {
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
const manifestConsistencyIssues = await validatePatchesManifestConsistency(ctx.paths.patches);
|
|
278
|
+
if (manifestConsistencyIssues.length === 0) {
|
|
279
|
+
return ok('Patch manifest consistency');
|
|
280
|
+
}
|
|
281
|
+
if (!ctx.options.repairPatchesManifest) {
|
|
282
|
+
return failure('Patch manifest consistency', manifestConsistencyIssues.map((issue) => issue.message).join(' '), 'Run "fireforge doctor --repair-patches-manifest" to rebuild patches.json from patch files.');
|
|
283
|
+
}
|
|
284
|
+
// Repair stamps sourceEsrVersion into every recovered entry. If the
|
|
285
|
+
// earlier "fireforge.json is valid" check failed, ctx.config is
|
|
286
|
+
// undefined and we must refuse rather than fabricate a fallback —
|
|
287
|
+
// persisting 'unknown' into manifest metadata is hard to reverse
|
|
288
|
+
// and would mislead every later command that reads it.
|
|
289
|
+
if (!ctx.config) {
|
|
290
|
+
return failure('Patch manifest consistency', 'Cannot repair patches.json: fireforge.json could not be loaded, so the Firefox version to stamp into recovered manifest entries is unknown.', 'Fix the fireforge.json errors reported above and re-run "fireforge doctor --repair-patches-manifest".');
|
|
291
|
+
}
|
|
108
292
|
try {
|
|
109
|
-
|
|
293
|
+
const repaired = await rebuildPatchesManifest(ctx.paths.patches, ctx.config.firefox.version);
|
|
294
|
+
return warning('Patch manifest consistency', `Rebuilt patches.json from ${repaired.patches.length} patch${repaired.patches.length === 1 ? '' : 'es'}. Review recovered metadata before release.`);
|
|
110
295
|
}
|
|
111
|
-
catch (
|
|
112
|
-
|
|
113
|
-
throw error;
|
|
114
|
-
}
|
|
115
|
-
canValidateBranch = false;
|
|
116
|
-
checks.push({
|
|
117
|
-
name: 'Engine branch',
|
|
118
|
-
passed: false,
|
|
119
|
-
severity: 'error',
|
|
120
|
-
message: 'Engine repository has no baseline commit yet. A previous "fireforge download" likely stopped before git created the initial Firefox commit.',
|
|
121
|
-
fix: 'Re-run "fireforge download --force" to recreate the baseline repository cleanly.',
|
|
122
|
-
});
|
|
296
|
+
catch (err) {
|
|
297
|
+
return failure('Patch manifest consistency', toError(err).message, 'Repair failed. Fix the underlying patch metadata issue and retry the doctor command.');
|
|
123
298
|
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
checks.push(await runCheck('mach available', async () => {
|
|
171
|
-
await ensureMach(paths.engine);
|
|
172
|
-
}, 'Firefox source may be corrupted. Re-download with "fireforge download --force"'));
|
|
173
|
-
return checks;
|
|
174
|
-
}
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: 'Patch integrity',
|
|
303
|
+
skipIf: (ctx) => !ctx.engineExists,
|
|
304
|
+
run: async (ctx) => {
|
|
305
|
+
if (!(await pathExists(ctx.paths.patches))) {
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
308
|
+
const issues = await validatePatchIntegrity(ctx.paths.patches, ctx.paths.engine);
|
|
309
|
+
if (issues.length === 0) {
|
|
310
|
+
return ok('Patch integrity');
|
|
311
|
+
}
|
|
312
|
+
const fileList = issues.map((issue) => issue.targetFile).filter(Boolean);
|
|
313
|
+
throw new Error(`${issues.length} patch(es) are modification patches for non-existent files: ${fileList.join(', ')}`);
|
|
314
|
+
},
|
|
315
|
+
fix: 'Re-export affected files with "fireforge export <paths...>" to create full-file patches',
|
|
316
|
+
},
|
|
317
|
+
// Furnace checks live in a sibling module so this file stays under the
|
|
318
|
+
// max-lines threshold. Splicing them in as an array preserves the
|
|
319
|
+
// declarative registry contract — each entry remains a single
|
|
320
|
+
// `DoctorCheckDefinition` with its own skipIf/run/fix, and the order
|
|
321
|
+
// here is the order they appear in the report.
|
|
322
|
+
...FURNACE_DOCTOR_CHECKS,
|
|
323
|
+
{
|
|
324
|
+
name: 'Configs directory exists',
|
|
325
|
+
run: async (ctx) => {
|
|
326
|
+
if (!(await pathExists(ctx.paths.configs))) {
|
|
327
|
+
throw new Error('configs/ directory not found');
|
|
328
|
+
}
|
|
329
|
+
return ok('Configs directory exists');
|
|
330
|
+
},
|
|
331
|
+
fix: 'Run "fireforge setup" to create configs',
|
|
332
|
+
},
|
|
333
|
+
];
|
|
334
|
+
// Validate dependency ordering at module load time so broken reorders
|
|
335
|
+
// fail immediately instead of producing subtle runtime bugs.
|
|
336
|
+
validateCheckDependencies(DOCTOR_CHECKS);
|
|
337
|
+
/**
|
|
338
|
+
* Ordered list of the doctor check names, exported for tests. Pinning
|
|
339
|
+
* the order here is intentional: any reorder that breaks the
|
|
340
|
+
* context-population dependency chain (see {@link DOCTOR_CHECKS}) must
|
|
341
|
+
* also update this list, which gives us a single place to notice and
|
|
342
|
+
* think through the consequences.
|
|
343
|
+
*/
|
|
344
|
+
export const DOCTOR_CHECK_ORDER = DOCTOR_CHECKS.map((check) => check.name);
|
|
175
345
|
function reportDoctorResults(checks) {
|
|
176
346
|
info('');
|
|
177
347
|
let passedCount = 0;
|
|
@@ -211,132 +381,30 @@ function reportDoctorResults(checks) {
|
|
|
211
381
|
}
|
|
212
382
|
return ExitCode.SUCCESS;
|
|
213
383
|
}
|
|
214
|
-
async function collectProjectChecks(paths, engineExists, firefoxVersion, options) {
|
|
215
|
-
const checks = [];
|
|
216
|
-
const patchesExist = await pathExists(paths.patches);
|
|
217
|
-
checks.push({
|
|
218
|
-
name: 'Patches directory exists',
|
|
219
|
-
passed: true,
|
|
220
|
-
severity: 'ok',
|
|
221
|
-
message: patchesExist ? 'OK' : 'No patches/ directory (optional)',
|
|
222
|
-
});
|
|
223
|
-
if (patchesExist) {
|
|
224
|
-
const patchCount = await countPatches(paths.patches);
|
|
225
|
-
checks.push({
|
|
226
|
-
name: 'Patches found',
|
|
227
|
-
passed: true,
|
|
228
|
-
severity: 'ok',
|
|
229
|
-
message: `${patchCount} patch${patchCount === 1 ? '' : 'es'} found`,
|
|
230
|
-
});
|
|
231
|
-
const manifestConsistencyIssues = await validatePatchesManifestConsistency(paths.patches);
|
|
232
|
-
if (manifestConsistencyIssues.length > 0) {
|
|
233
|
-
if (options.repairPatchesManifest) {
|
|
234
|
-
try {
|
|
235
|
-
const repairedManifest = await rebuildPatchesManifest(paths.patches, firefoxVersion ?? 'unknown');
|
|
236
|
-
checks.push({
|
|
237
|
-
name: 'Patch manifest consistency',
|
|
238
|
-
passed: true,
|
|
239
|
-
severity: 'warning',
|
|
240
|
-
warning: true,
|
|
241
|
-
message: `Rebuilt patches.json from ${repairedManifest.patches.length} patch` +
|
|
242
|
-
`${repairedManifest.patches.length === 1 ? '' : 'es'}. Review recovered metadata before release.`,
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
catch (error) {
|
|
246
|
-
checks.push({
|
|
247
|
-
name: 'Patch manifest consistency',
|
|
248
|
-
passed: false,
|
|
249
|
-
severity: 'error',
|
|
250
|
-
message: toError(error).message,
|
|
251
|
-
fix: 'Repair failed. Fix the underlying patch metadata issue and retry the doctor command.',
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
else {
|
|
256
|
-
checks.push({
|
|
257
|
-
name: 'Patch manifest consistency',
|
|
258
|
-
passed: false,
|
|
259
|
-
severity: 'error',
|
|
260
|
-
message: manifestConsistencyIssues.map((issue) => issue.message).join(' '),
|
|
261
|
-
fix: 'Run "fireforge doctor --repair-patches-manifest" to rebuild patches.json from patch files.',
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
else {
|
|
266
|
-
checks.push({
|
|
267
|
-
name: 'Patch manifest consistency',
|
|
268
|
-
passed: true,
|
|
269
|
-
severity: 'ok',
|
|
270
|
-
message: 'OK',
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
if (engineExists) {
|
|
274
|
-
checks.push(await runCheck('Patch integrity', async () => {
|
|
275
|
-
const issues = await validatePatchIntegrity(paths.patches, paths.engine);
|
|
276
|
-
if (issues.length > 0) {
|
|
277
|
-
const fileList = issues.map((issue) => issue.targetFile).filter(Boolean);
|
|
278
|
-
throw new Error(`${issues.length} patch(es) are modification patches for non-existent files: ${fileList.join(', ')}`);
|
|
279
|
-
}
|
|
280
|
-
}, 'Re-export affected files with "fireforge export <paths...>" to create full-file patches'));
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
const configsExist = await pathExists(paths.configs);
|
|
284
|
-
checks.push(await runCheck('Configs directory exists', () => {
|
|
285
|
-
if (!configsExist) {
|
|
286
|
-
throw new Error('configs/ directory not found');
|
|
287
|
-
}
|
|
288
|
-
}, 'Run "fireforge setup" to create configs'));
|
|
289
|
-
return checks;
|
|
290
|
-
}
|
|
291
384
|
/**
|
|
292
385
|
* Runs the doctor command to diagnose issues.
|
|
293
386
|
* @param projectRoot - Root directory of the project
|
|
294
387
|
*/
|
|
295
388
|
export async function doctorCommand(projectRoot, options = {}) {
|
|
296
389
|
intro('FireForge Doctor');
|
|
297
|
-
const checks = [];
|
|
298
390
|
const paths = getProjectPaths(projectRoot);
|
|
299
391
|
const state = await loadState(projectRoot);
|
|
300
|
-
let config;
|
|
301
|
-
// Check 1: Git installed
|
|
302
|
-
checks.push(await runCheck('Git installed', async () => {
|
|
303
|
-
await ensureGit();
|
|
304
|
-
}, 'Install git from https://git-scm.com/'));
|
|
305
|
-
// Check 2: Python supported by mach
|
|
306
|
-
checks.push(await runCheck('Python supported by mach', async () => {
|
|
307
|
-
await ensurePython(paths.engine);
|
|
308
|
-
}, 'Install a Python version supported by engine/mach, then re-run "fireforge doctor".'));
|
|
309
|
-
// Check 3: fireforge.json exists
|
|
310
|
-
checks.push(await runCheck('fireforge.json exists', async () => {
|
|
311
|
-
if (!(await configExists(projectRoot))) {
|
|
312
|
-
throw new Error('fireforge.json not found');
|
|
313
|
-
}
|
|
314
|
-
}, 'Run "fireforge setup" to create a project'));
|
|
315
|
-
// Check 4: fireforge.json is valid
|
|
316
|
-
checks.push(await runCheck('fireforge.json is valid', async () => {
|
|
317
|
-
config = await loadConfig(projectRoot);
|
|
318
|
-
}, 'Check fireforge.json for syntax errors or missing fields'));
|
|
319
|
-
// Check 5: Engine directory exists
|
|
320
392
|
const engineExists = await pathExists(paths.engine);
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
393
|
+
const furnaceConfigExistsFlag = await checkFurnaceConfigExists(projectRoot);
|
|
394
|
+
const ctx = {
|
|
395
|
+
projectRoot,
|
|
396
|
+
paths,
|
|
397
|
+
state,
|
|
398
|
+
options,
|
|
399
|
+
engineExists,
|
|
400
|
+
config: undefined,
|
|
401
|
+
furnaceConfigExists: furnaceConfigExistsFlag,
|
|
402
|
+
furnaceConfig: undefined,
|
|
403
|
+
};
|
|
404
|
+
const checks = [];
|
|
405
|
+
for (const definition of DOCTOR_CHECKS) {
|
|
406
|
+
checks.push(...(await executeCheck(definition, ctx)));
|
|
335
407
|
}
|
|
336
|
-
// Engine checks (git repo, state consistency, working tree, branch, mach)
|
|
337
|
-
checks.push(...(await collectEngineChecks(paths, state, engineExists)));
|
|
338
|
-
checks.push(...(await collectProjectChecks(paths, engineExists, config?.firefox.version, options)));
|
|
339
|
-
// Display results and return
|
|
340
408
|
const exitCode = reportDoctorResults(checks);
|
|
341
409
|
return { checks, exitCode };
|
|
342
410
|
}
|
|
@@ -346,6 +414,7 @@ export function registerDoctor(program, { getProjectRoot, withErrorHandling }) {
|
|
|
346
414
|
.command('doctor')
|
|
347
415
|
.description('Diagnose project issues')
|
|
348
416
|
.option('--repair-patches-manifest', 'Rebuild patches/patches.json from the current patch files before reporting results')
|
|
417
|
+
.option('--repair-furnace', 'Reconcile furnace state: clear stale furnace-state.json entries, re-run furnace apply to fix engine drift, and clear the pending-repair marker set by a failed preview teardown')
|
|
349
418
|
.action(withErrorHandling(async (options) => {
|
|
350
419
|
const result = await doctorCommand(getProjectRoot(), options);
|
|
351
420
|
if (result.exitCode !== 0) {
|