@hominis/fireforge 0.13.1 → 0.14.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 +65 -0
- package/README.md +12 -8
- package/dist/bin/fireforge.js +19 -5
- package/dist/src/commands/config.js +7 -1
- package/dist/src/commands/discard.js +6 -1
- package/dist/src/commands/doctor.d.ts +12 -0
- package/dist/src/commands/doctor.js +6 -1
- package/dist/src/commands/download.js +106 -7
- package/dist/src/commands/export-shared.js +7 -0
- package/dist/src/commands/export.js +5 -0
- package/dist/src/commands/furnace/apply.js +147 -47
- package/dist/src/commands/furnace/create.js +13 -2
- package/dist/src/commands/furnace/deploy.js +17 -2
- package/dist/src/commands/furnace/diff.js +3 -1
- package/dist/src/commands/furnace/init.js +25 -7
- package/dist/src/commands/furnace/list.js +15 -7
- package/dist/src/commands/furnace/override.js +47 -15
- package/dist/src/commands/furnace/remove.js +68 -20
- package/dist/src/commands/furnace/rename.js +31 -3
- package/dist/src/commands/furnace/scan.js +8 -0
- package/dist/src/commands/furnace/validate.js +70 -7
- package/dist/src/commands/import.js +65 -11
- package/dist/src/commands/patch/compact.d.ts +25 -0
- package/dist/src/commands/patch/compact.js +132 -0
- package/dist/src/commands/patch/index.d.ts +1 -0
- package/dist/src/commands/patch/index.js +4 -1
- package/dist/src/commands/patch/reorder.d.ts +5 -1
- package/dist/src/commands/patch/reorder.js +4 -2
- package/dist/src/commands/re-export.js +11 -4
- package/dist/src/commands/rebase/abort.js +26 -14
- package/dist/src/commands/rebase/confirm.d.ts +15 -2
- package/dist/src/commands/rebase/confirm.js +2 -2
- package/dist/src/commands/rebase/continue.js +39 -15
- package/dist/src/commands/rebase/index.js +2 -1
- package/dist/src/commands/rebase/patch-loop.js +90 -33
- package/dist/src/commands/register.js +13 -0
- package/dist/src/commands/resolve.js +31 -10
- package/dist/src/commands/run.js +9 -44
- package/dist/src/commands/setup-support.js +25 -7
- package/dist/src/commands/status.js +59 -8
- package/dist/src/commands/test.js +13 -7
- package/dist/src/commands/token.js +11 -1
- package/dist/src/commands/watch.js +51 -1
- package/dist/src/commands/wire.js +23 -0
- package/dist/src/core/config-validate.js +15 -1
- package/dist/src/core/furnace-registration.d.ts +1 -1
- package/dist/src/core/furnace-registration.js +2 -1
- package/dist/src/core/furnace-staleness.d.ts +17 -0
- package/dist/src/core/furnace-staleness.js +58 -0
- package/dist/src/core/license-headers.d.ts +15 -0
- package/dist/src/core/license-headers.js +28 -0
- package/dist/src/core/manifest-rules.js +24 -3
- package/dist/src/core/patch-lint.d.ts +11 -0
- package/dist/src/core/patch-lint.js +30 -3
- package/dist/src/core/signal-critical.d.ts +49 -0
- package/dist/src/core/signal-critical.js +80 -0
- package/dist/src/errors/download.d.ts +1 -1
- package/dist/src/errors/download.js +6 -3
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/options.d.ts +9 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.0
|
|
4
|
+
|
|
5
|
+
### Concurrency and atomicity
|
|
6
|
+
|
|
7
|
+
- Patch body and manifest writes in `re-export`, `rebase --continue`, and the post-apply re-export loop in `rebase` are now atomic via `updatePatchAndMetadata`, so a concurrent `resolve` / `re-export` / `patch compact` / `patch reorder` cannot leave body and metadata disagreeing.
|
|
8
|
+
- State writes in `import`, `resolve`, and `rebase` (abort, continue, patch loop) use transactional `updateState` so a concurrent command's unrelated keys are no longer clobbered.
|
|
9
|
+
- `rebase` apply + session persist is guarded by a new `runInSignalCriticalSection` primitive in `src/core/signal-critical.ts`; SIGINT / SIGTERM between apply and persist (5 s ceiling) no longer leaves an applied patch marked pending, so `--continue` does not re-apply it.
|
|
10
|
+
|
|
11
|
+
### Rebase
|
|
12
|
+
|
|
13
|
+
- Per-patch re-export failures after apply are collected instead of silently dropped. The session stays on disk and `sourceEsrVersion` is not stamped until every re-export succeeds, so `--continue` can retry after the root cause is fixed.
|
|
14
|
+
- `rebase --continue` retries the post-apply pipeline when the apply loop has already finished; the prior "session may be corrupt" rejection no longer blocks resumption.
|
|
15
|
+
- `rebase --abort` splits into four sequenced steps (git reset, furnace state clear, `pendingResolution` clear, session clear) so failures get correctly-labelled errors and the session stays on disk for retry.
|
|
16
|
+
|
|
17
|
+
### Download
|
|
18
|
+
|
|
19
|
+
- `download` restores patch-touched files to baseline after the initial commit (or a resumed partial init), so extraction artefacts and line-ending normalisation no longer force `fireforge import --force` on a clean install. Pre-existing uncommitted edits are preserved and warned about.
|
|
20
|
+
- `cleanPatchTouchedFiles` runs before stamping `state.downloadedVersion`, preserving the invariant that the stamped version matches a clean engine.
|
|
21
|
+
- Resume preserves the original error cause (timeout, permission denied, corrupted git object, disk full) instead of discarding it behind a generic `PartialEngineExistsError`. Unexpected errors during the partial-engine probe are also wrapped rather than re-thrown bare.
|
|
22
|
+
|
|
23
|
+
### Import
|
|
24
|
+
|
|
25
|
+
- Classification no longer swallows structural errors as "unmanaged dirty file". Only pure-IO errors (`ENOENT`, `EACCES`, `EPERM`, `EISDIR`, `EBUSY`) fall through; `PatchError`, manifest corruption, and patch-parse failures re-throw with the original diagnostic.
|
|
26
|
+
- Patch integrity issues prompt in interactive mode and error in non-interactive mode instead of warn-and-continue; `--force` still bypasses with an explicit warning.
|
|
27
|
+
|
|
28
|
+
### Furnace
|
|
29
|
+
|
|
30
|
+
- `furnace apply --watch` picks up newly-created component directories dynamically, remembers edits that arrive during an in-flight apply (a second cycle runs automatically), and classifies errors errno-aware (`EACCES`, `ENOSPC`, `EBUSY`, `ENOENT`, `ETIMEDOUT`) instead of collapsing into a generic "Apply failed".
|
|
31
|
+
- `furnace override` rejects collisions with `config.stock` and `config.custom` in both single and batch paths, and wraps snapshot + copy pairs in per-file error context so a mid-copy failure names the failing file.
|
|
32
|
+
- `furnace remove` requires a git engine for custom components (not just overrides) and hoists the precondition outside the lock and journal registration. A summary line surfaces when test-file cleanup fails partway.
|
|
33
|
+
- `furnace rename` does prefix-only filename replacement; the prior substring replacement mangled names when `oldName` appeared more than once. Content regexes now escape every metacharacter.
|
|
34
|
+
- `furnace deploy` asserts `applied[0].name` matches the requested component before persisting state; state-mismatch errors recommend `fireforge doctor --repair-furnace`.
|
|
35
|
+
- `furnace validate --fix` reports the actual delta from re-validation instead of inflating the count on no-op fixes.
|
|
36
|
+
- `furnace list -v` tolerates missing or unreadable component directories, rendering `unavailable` instead of terminating the listing.
|
|
37
|
+
- `furnace diff` surfaces `--reset-base` recovery in the primary error rather than a secondary catch block.
|
|
38
|
+
- `furnace init --ftl-base-path` traversal check uses path normalisation instead of substring match, rejecting absolute paths, null bytes, and `..` segments. Interactive detection checks both `stdin.isTTY` and `stdout.isTTY` to match every other interactive command.
|
|
39
|
+
|
|
40
|
+
### Other commands
|
|
41
|
+
|
|
42
|
+
- `setup` rejects project names whose sanitised slug is empty (emoji-only, pure punctuation, `---`). `validateConfig` similarly rejects empty `name`, `vendor`, `appId`, `binaryName`.
|
|
43
|
+
- `config --force` no longer bypasses structural validation for known keys in `SUPPORTED_CONFIG_PATHS`; the flag is only an escape hatch for unknown keys.
|
|
44
|
+
- `watch` probes `watchman --version` with a 5 s timeout before starting, and runs the furnace staleness check previously only in `run`. Both commands share a new `warnIfFurnaceStale` helper in `src/core/furnace-staleness.ts`.
|
|
45
|
+
- `test` path normalisation is case-insensitive, accepts `\` as well as `/`, and trims whitespace, so `Engine/foo/bar` on macOS / Windows no longer reaches `mach` with the prefix intact.
|
|
46
|
+
- `status` caps untracked-directory expansion at 5 000 files per directory (configurable via `FIREFORGE_MAX_UNTRACKED_FILES`) and renders a top-of-output banner when directories were truncated, so large outputs don't hide the warning in scrollback.
|
|
47
|
+
- `export` empty-diff error distinguishes the `--skip-lint` case; `export-shared` always announces when `--skip-lint` is active.
|
|
48
|
+
- `wire <name>` and `register --after <entry>` validate their inputs against strict regexes, rejecting path separators, parent-dir segments, control characters, and line terminators before any filesystem operation.
|
|
49
|
+
- `token add --mode` uses Commander's `.choices()` so invalid modes fail with the built-in message and `--help` lists the valid options.
|
|
50
|
+
- `run` whitelisted exit codes (0, 130 SIGINT, 143 SIGTERM) are documented inline; SIGKILL (137) and other abnormal signal codes surface as build errors.
|
|
51
|
+
- `discard` wraps error causes via `toError` so thrown strings or numbers propagate as real Errors with stack traces.
|
|
52
|
+
|
|
53
|
+
### Internal
|
|
54
|
+
|
|
55
|
+
- New unit tests for `validateCheckDependencies` in `src/commands/doctor.ts` assert the forward-only dependency invariant so a regression cannot slip in when reordering checks.
|
|
56
|
+
|
|
3
57
|
## 0.13.0
|
|
4
58
|
|
|
5
59
|
### Setup
|
|
@@ -12,6 +66,17 @@
|
|
|
12
66
|
- **`observer-topic-naming` no longer matches across newlines.** The regex that extracts topic strings from `notifyObservers`/`addObserver`/`removeObserver` calls now anchors to a single line, preventing false positives when the call spans multiple lines and an unrelated string literal appears later.
|
|
13
67
|
- **`raw-color-value` now supports a file allowlist and inline suppression.** New `patchLint.rawColorAllowlist` config array in `fireforge.json` exempts file paths (exact or basename match) from the raw-color check — intended for design token files that must contain raw color values. Individual declarations can also be suppressed with an inline `/* fireforge-ignore: raw-color-value */` comment.
|
|
14
68
|
- **`large-patch-lines` now uses tiered severity thresholds.** The old single >300-line warning is replaced with a three-tier system matching the `file-too-large` pattern. General patches: 800+ lines notice, 1500+ warning, 3000+ error. Test-only patches (all files match test patterns): 1500+ notice, 3000+ warning, 6000+ error. The previous threshold was too restrictive relative to file LOC limits — creating a single new file at the `file-too-large` notice tier (500 LOC) already exceeded it. Messages now include the applicable soft and hard limits.
|
|
69
|
+
- **`large-patch-lines` now ignores binary content.** Patches whose diff contains GIT binary patch hunks (PNG, ICO, ICNS, BMP, etc.) no longer count base85-encoded data toward the line limit. This removes the need for `--skip-lint` on branding asset patches that are predominantly binary.
|
|
70
|
+
- **`modified-file-missing-header` no longer false-positives on upstream files.** Modified upstream files (e.g. `BrowserGlue.sys.mjs`) that carry an MPL-2.0 header in `/* */` block-comment style were incorrectly flagged because the check only tried the comment style inferred from the file extension. The check now cascades through all comment styles and falls back to scanning leading lines for raw license identifier strings (MPL, Apache, MIT, GPL, SPDX).
|
|
71
|
+
|
|
72
|
+
### New commands
|
|
73
|
+
|
|
74
|
+
- **`fireforge patch compact`** — closes ordinal gaps in the patch queue in a single atomic operation. After deletes or splits, patch ordinals may have gaps (e.g. 1, 3, 7); `compact` renumbers them sequentially (1, 2, 3). Previously this required N sequential `patch reorder` calls. Supports `--dry-run` and `--yes`.
|
|
75
|
+
|
|
76
|
+
### Register improvements
|
|
77
|
+
|
|
78
|
+
- **`register` now supports `.xhtml` and `.css` files in `browser/base/content/`.** Previously only `.js` and `.mjs` files were accepted; XHTML and CSS files required manual `jar.mn` edits.
|
|
79
|
+
- **`register` now gives actionable advice for unregistrable file types.** Attempting to register a `.ftl` locale file explains that FTL files are auto-discovered via `jar.mn` glob patterns. Attempting to register an individual test file explains that it should be added to the corresponding `browser.toml` and suggests the correct `register` invocation for the test directory manifest.
|
|
15
80
|
|
|
16
81
|
### General Improvements
|
|
17
82
|
|
package/README.md
CHANGED
|
@@ -233,6 +233,7 @@ Then fix with the appropriate primitive:
|
|
|
233
233
|
| Two patches each creating the same file | `fireforge patch delete <duplicate>` or `fireforge re-export --files` |
|
|
234
234
|
| A patch imports from a module in a later patch | `fireforge patch reorder <later> --before <importer>` |
|
|
235
235
|
| Wrong patch ordering | `fireforge patch reorder <patch> --to <N>` |
|
|
236
|
+
| Ordinal gaps after deletes/splits | `fireforge patch compact` |
|
|
236
237
|
| A patch claims files that belong elsewhere | `fireforge re-export --files <subset> <patch>` |
|
|
237
238
|
| Manifest references a missing patch file | `fireforge doctor --repair-patches-manifest` |
|
|
238
239
|
| Unmanaged changes you want to discard | `fireforge discard <file>` or `fireforge reset` |
|
|
@@ -266,13 +267,13 @@ fireforge register browser/modules/mybrowser/MyStore.sys.mjs
|
|
|
266
267
|
<details>
|
|
267
268
|
<summary>Supported register patterns</summary>
|
|
268
269
|
|
|
269
|
-
| File pattern
|
|
270
|
-
|
|
|
271
|
-
| `browser/themes/shared/*.css`
|
|
272
|
-
| `browser/base/content/*.{js,mjs}`
|
|
273
|
-
| `browser/base/content/test/*/browser.toml`
|
|
274
|
-
| `browser/modules/mybrowser/*.sys.mjs`
|
|
275
|
-
| `toolkit/content/widgets/*/*.{mjs,css}`
|
|
270
|
+
| File pattern | Manifest | Entry format |
|
|
271
|
+
| ------------------------------------------- | ------------------------------------- | ----------------------------------- |
|
|
272
|
+
| `browser/themes/shared/*.css` | `browser/themes/shared/jar.inc.mn` | `skin/classic/browser/{name}.css` |
|
|
273
|
+
| `browser/base/content/*.{js,mjs,xhtml,css}` | `browser/base/jar.mn` | `content/browser/{file}` |
|
|
274
|
+
| `browser/base/content/test/*/browser.toml` | `browser/base/moz.build` | `"content/test/{dir}/browser.toml"` |
|
|
275
|
+
| `browser/modules/mybrowser/*.sys.mjs` | `browser/modules/mybrowser/moz.build` | `"{name}.sys.mjs"` |
|
|
276
|
+
| `toolkit/content/widgets/*/*.{mjs,css}` | `toolkit/content/jar.mn` | `content/global/elements/{file}` |
|
|
276
277
|
|
|
277
278
|
</details>
|
|
278
279
|
|
|
@@ -327,9 +328,12 @@ fireforge patch reorder 003-ui-sidebar-tweaks.patch --to 1
|
|
|
327
328
|
|
|
328
329
|
# Move a patch before or after another
|
|
329
330
|
fireforge patch reorder 003-ui-sidebar.patch --before 001-branding-logo.patch
|
|
331
|
+
|
|
332
|
+
# Close ordinal gaps after deletes or splits (e.g. 1, 3, 7 → 1, 2, 3)
|
|
333
|
+
fireforge patch compact
|
|
330
334
|
```
|
|
331
335
|
|
|
332
|
-
|
|
336
|
+
All subcommands support `--dry-run` and `--yes`.
|
|
333
337
|
|
|
334
338
|
### Additional workflow commands
|
|
335
339
|
|
package/dist/bin/fireforge.js
CHANGED
|
@@ -10,7 +10,15 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { installBrokenPipeHandler, main } from '../src/cli.js';
|
|
12
12
|
import { isSignalRollbackInFlight, rollbackActiveOperationsForSignal, } from '../src/core/furnace-operation.js';
|
|
13
|
+
import { waitForActiveCriticalSections } from '../src/core/signal-critical.js';
|
|
13
14
|
import { CommandError } from '../src/errors/base.js';
|
|
15
|
+
/**
|
|
16
|
+
* Upper bound (ms) the signal handler will wait for any in-flight critical
|
|
17
|
+
* section (e.g. rebase apply + session persist) to finish before calling
|
|
18
|
+
* process.exit. Keep short so a stuck I/O operation cannot indefinitely
|
|
19
|
+
* postpone the exit a user requested with Ctrl+C.
|
|
20
|
+
*/
|
|
21
|
+
const SIGNAL_CRITICAL_SECTION_TIMEOUT_MS = 5_000;
|
|
14
22
|
installBrokenPipeHandler();
|
|
15
23
|
process.on('unhandledRejection', (reason) => {
|
|
16
24
|
console.error('Fatal error (unhandled rejection):', reason instanceof Error ? reason.message : reason);
|
|
@@ -33,11 +41,17 @@ function installFurnaceSignalHandler(signal, exitCode) {
|
|
|
33
41
|
// rather than queueing another rollback that will race the first.
|
|
34
42
|
process.exit(exitCode);
|
|
35
43
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
// Run furnace rollback and signal-critical-section drain in parallel.
|
|
45
|
+
// Rebase-style operations register critical sections (apply + session
|
|
46
|
+
// persist) via `runInSignalCriticalSection`; awaiting them here ensures
|
|
47
|
+
// the CLI never exits with a patch applied to the engine but a stale
|
|
48
|
+
// session file that would mis-track progress on `--continue`.
|
|
49
|
+
void Promise.allSettled([
|
|
50
|
+
rollbackActiveOperationsForSignal(signal).catch((error) => {
|
|
51
|
+
console.error(`Furnace rollback after ${signal} failed:`, error instanceof Error ? error.message : error);
|
|
52
|
+
}),
|
|
53
|
+
waitForActiveCriticalSections(SIGNAL_CRITICAL_SECTION_TIMEOUT_MS),
|
|
54
|
+
]).finally(() => {
|
|
41
55
|
process.exit(exitCode);
|
|
42
56
|
});
|
|
43
57
|
});
|
|
@@ -105,8 +105,14 @@ export async function configCommand(projectRoot, key, value, options = {}) {
|
|
|
105
105
|
throw new InvalidArgumentError(`Unknown config key: "${key}". Known keys: ${SUPPORTED_CONFIG_PATHS.join(', ')}. Use --force to set anyway.`);
|
|
106
106
|
}
|
|
107
107
|
const parsedValue = parseValue(value, key);
|
|
108
|
+
const keyIsKnown = SUPPORTED_CONFIG_PATHS.includes(key);
|
|
108
109
|
try {
|
|
109
|
-
|
|
110
|
+
// `--force` is intended as an escape hatch for *unknown* keys; it
|
|
111
|
+
// should not also let the user write a structurally invalid value
|
|
112
|
+
// for a *known* key. Apply strict validation whenever the key is
|
|
113
|
+
// listed in SUPPORTED_CONFIG_PATHS, regardless of --force, and only
|
|
114
|
+
// skip validation for genuinely unknown key paths.
|
|
115
|
+
if (options.force && !keyIsKnown) {
|
|
110
116
|
const updatedConfig = mutateConfig(config, key, parsedValue, true);
|
|
111
117
|
await writeConfigDocument(projectRoot, updatedConfig);
|
|
112
118
|
}
|
|
@@ -7,6 +7,7 @@ import { discardStatusEntry } from '../core/git-file-ops.js';
|
|
|
7
7
|
import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/git-status.js';
|
|
8
8
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
9
9
|
import { GitError } from '../errors/git.js';
|
|
10
|
+
import { toError } from '../utils/errors.js';
|
|
10
11
|
import { pathExists } from '../utils/fs.js';
|
|
11
12
|
import { info, intro, isCancel, outro, spinner, warn } from '../utils/logger.js';
|
|
12
13
|
import { pickDefined } from '../utils/options.js';
|
|
@@ -79,7 +80,11 @@ export async function discardCommand(projectRoot, file, options = {}) {
|
|
|
79
80
|
}
|
|
80
81
|
throw new GitError(`Failed to discard ${file}`, statusEntry.isUntracked
|
|
81
82
|
? `rm ${statusEntry.file}`
|
|
82
|
-
: `restore --source HEAD --staged --worktree -- ${statusEntry.file}`,
|
|
83
|
+
: `restore --source HEAD --staged --worktree -- ${statusEntry.file}`,
|
|
84
|
+
// Always attach the cause via toError so thrown primitives (strings,
|
|
85
|
+
// numbers) produced by poorly-behaved utilities still propagate as
|
|
86
|
+
// an Error, preserving stack traces for verbose-mode triage.
|
|
87
|
+
toError(error));
|
|
83
88
|
}
|
|
84
89
|
}
|
|
85
90
|
/** Registers the discard command on the CLI program. */
|
|
@@ -109,6 +109,18 @@ export declare function warning(name: string, message: string, fix?: string): Do
|
|
|
109
109
|
* Exported for sibling check modules — see {@link ok}.
|
|
110
110
|
*/
|
|
111
111
|
export declare function failure(name: string, message: string, fix?: string): DoctorCheck;
|
|
112
|
+
/**
|
|
113
|
+
* Validates that every check's `dependsOn` entries appear earlier in the
|
|
114
|
+
* registry. Called once at module load time so a broken reorder surfaces
|
|
115
|
+
* immediately as a thrown error rather than producing a subtle
|
|
116
|
+
* context-population bug at runtime.
|
|
117
|
+
*
|
|
118
|
+
* Exported so tests can exercise the forward-only invariant against
|
|
119
|
+
* fixtures — the real DOCTOR_CHECKS list is also validated at import
|
|
120
|
+
* time, but a targeted unit test makes the contract explicit and
|
|
121
|
+
* prevents regressions if the validator is ever relaxed.
|
|
122
|
+
*/
|
|
123
|
+
export declare function validateCheckDependencies(checks: readonly DoctorCheckDefinition[]): void;
|
|
112
124
|
/**
|
|
113
125
|
* Ordered list of the doctor check names, exported for tests. Pinning
|
|
114
126
|
* the order here is intentional: any reorder that breaks the
|
|
@@ -135,8 +135,13 @@ async function runEngineGitChecks(ctx) {
|
|
|
135
135
|
* registry. Called once at module load time so a broken reorder surfaces
|
|
136
136
|
* immediately as a thrown error rather than producing a subtle
|
|
137
137
|
* context-population bug at runtime.
|
|
138
|
+
*
|
|
139
|
+
* Exported so tests can exercise the forward-only invariant against
|
|
140
|
+
* fixtures — the real DOCTOR_CHECKS list is also validated at import
|
|
141
|
+
* time, but a targeted unit test makes the contract explicit and
|
|
142
|
+
* prevents regressions if the validator is ever relaxed.
|
|
138
143
|
*/
|
|
139
|
-
function validateCheckDependencies(checks) {
|
|
144
|
+
export function validateCheckDependencies(checks) {
|
|
140
145
|
const seen = new Set();
|
|
141
146
|
for (const check of checks) {
|
|
142
147
|
if (check.dependsOn) {
|
|
@@ -4,10 +4,68 @@ import { getProjectPaths, loadConfig, updateState } from '../core/config.js';
|
|
|
4
4
|
import { downloadFirefoxSource, formatBytes } from '../core/firefox.js';
|
|
5
5
|
import { getFurnacePaths, updateFurnaceState } from '../core/furnace-config.js';
|
|
6
6
|
import { getHead, initRepository, isGitRepository, isMissingHeadError, resumeRepository, } from '../core/git.js';
|
|
7
|
+
import { restoreTrackedPath } from '../core/git-file-ops.js';
|
|
8
|
+
import { getDirtyFiles } from '../core/git-status.js';
|
|
9
|
+
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
7
10
|
import { EngineExistsError, PartialEngineExistsError } from '../errors/download.js';
|
|
11
|
+
import { toError } from '../utils/errors.js';
|
|
8
12
|
import { checkDiskSpace, ensureDir, pathExists, removeDir } from '../utils/fs.js';
|
|
9
|
-
import { info, intro, outro, spinner, step, warn } from '../utils/logger.js';
|
|
13
|
+
import { info, intro, outro, spinner, step, verbose, warn } from '../utils/logger.js';
|
|
10
14
|
import { pickDefined } from '../utils/options.js';
|
|
15
|
+
/**
|
|
16
|
+
* Collects the set of patch-touched files from the manifest.
|
|
17
|
+
* Returns an empty set when the patches directory or manifest is absent.
|
|
18
|
+
*/
|
|
19
|
+
async function getPatchTouchedFiles(patchesDir) {
|
|
20
|
+
if (!(await pathExists(patchesDir)))
|
|
21
|
+
return new Set();
|
|
22
|
+
const manifest = await loadPatchesManifest(patchesDir);
|
|
23
|
+
if (!manifest || manifest.patches.length === 0)
|
|
24
|
+
return new Set();
|
|
25
|
+
const files = new Set();
|
|
26
|
+
for (const patch of manifest.patches) {
|
|
27
|
+
for (const file of patch.filesAffected) {
|
|
28
|
+
files.add(file);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return files;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Restores patch-touched files to their committed (HEAD) state so that a
|
|
35
|
+
* subsequent `fireforge import` does not see spurious uncommitted changes.
|
|
36
|
+
*
|
|
37
|
+
* Files that were already dirty *before* the download started (tracked via
|
|
38
|
+
* `preExistingDirty`) are left untouched and warned about.
|
|
39
|
+
*/
|
|
40
|
+
async function cleanPatchTouchedFiles(engineDir, patchesDir, preExistingDirty) {
|
|
41
|
+
const patchFiles = await getPatchTouchedFiles(patchesDir);
|
|
42
|
+
if (patchFiles.size === 0)
|
|
43
|
+
return;
|
|
44
|
+
const dirtyFiles = await getDirtyFiles(engineDir, [...patchFiles]);
|
|
45
|
+
if (dirtyFiles.length === 0)
|
|
46
|
+
return;
|
|
47
|
+
const toClean = preExistingDirty
|
|
48
|
+
? dirtyFiles.filter((f) => !preExistingDirty.has(f))
|
|
49
|
+
: dirtyFiles;
|
|
50
|
+
const preserved = preExistingDirty ? dirtyFiles.filter((f) => preExistingDirty.has(f)) : [];
|
|
51
|
+
for (const file of toClean) {
|
|
52
|
+
try {
|
|
53
|
+
await restoreTrackedPath(engineDir, file);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
warn(`Could not restore patch-touched file: ${file}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (toClean.length > 0) {
|
|
60
|
+
info(`Restored ${toClean.length} patch-touched file(s) to baseline state.`);
|
|
61
|
+
}
|
|
62
|
+
if (preserved.length > 0) {
|
|
63
|
+
warn(`${preserved.length} patch-touched file(s) had pre-existing changes and were left as-is:`);
|
|
64
|
+
for (const file of preserved) {
|
|
65
|
+
warn(` ${file}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
11
69
|
/**
|
|
12
70
|
* Runs the download command.
|
|
13
71
|
* @param projectRoot - Root directory of the project
|
|
@@ -33,6 +91,12 @@ export async function downloadCommand(projectRoot, options) {
|
|
|
33
91
|
if (isMissingHeadError(error)) {
|
|
34
92
|
// Partial init detected — attempt to resume instead of requiring --force
|
|
35
93
|
info('Detected partially initialized engine. Attempting to resume...');
|
|
94
|
+
// Snapshot patch-touched files that are already dirty so we
|
|
95
|
+
// can preserve them after the resume commit.
|
|
96
|
+
const patchFiles = await getPatchTouchedFiles(paths.patches);
|
|
97
|
+
const preExistingDirty = patchFiles.size > 0
|
|
98
|
+
? new Set(await getDirtyFiles(paths.engine, [...patchFiles]))
|
|
99
|
+
: new Set();
|
|
36
100
|
const resumeSpinner = spinner('Resuming git repository initialization...');
|
|
37
101
|
try {
|
|
38
102
|
await resumeRepository(paths.engine, {
|
|
@@ -45,6 +109,14 @@ export async function downloadCommand(projectRoot, options) {
|
|
|
45
109
|
});
|
|
46
110
|
const baseCommit = await getHead(paths.engine);
|
|
47
111
|
resumeSpinner.stop('Git repository resumed successfully');
|
|
112
|
+
// Restore patch-touched files BEFORE stamping state. If this
|
|
113
|
+
// step fails (disk full, permission denied, git object issue),
|
|
114
|
+
// state.json keeps the previous downloadedVersion so the
|
|
115
|
+
// invariant "state.downloadedVersion matches a clean engine"
|
|
116
|
+
// holds. A retry of `fireforge download` then re-enters the
|
|
117
|
+
// resume path instead of declaring success against a dirty
|
|
118
|
+
// engine.
|
|
119
|
+
await cleanPatchTouchedFiles(paths.engine, paths.patches, preExistingDirty);
|
|
48
120
|
await updateState(projectRoot, {
|
|
49
121
|
downloadedVersion: version,
|
|
50
122
|
baseCommit,
|
|
@@ -53,14 +125,31 @@ export async function downloadCommand(projectRoot, options) {
|
|
|
53
125
|
return;
|
|
54
126
|
}
|
|
55
127
|
catch (error) {
|
|
56
|
-
void error;
|
|
57
128
|
resumeSpinner.error('Resume failed');
|
|
58
|
-
|
|
129
|
+
// Preserve the underlying cause so the user sees *why* the
|
|
130
|
+
// resume failed (timeout, permission denied, corrupted object,
|
|
131
|
+
// disk full, …) instead of only the generic "partial engine
|
|
132
|
+
// exists" story. Verbose mode prints the stack for deeper
|
|
133
|
+
// triage.
|
|
134
|
+
const cause = toError(error);
|
|
135
|
+
verbose(`Resume failure detail: ${cause.message}`);
|
|
136
|
+
if (cause.stack) {
|
|
137
|
+
verbose(cause.stack);
|
|
138
|
+
}
|
|
139
|
+
throw new PartialEngineExistsError(paths.engine, cause);
|
|
59
140
|
}
|
|
60
141
|
}
|
|
61
|
-
// Re-throw unexpected git errors (
|
|
62
|
-
//
|
|
63
|
-
|
|
142
|
+
// Re-throw unexpected git errors (corrupted objects, permission
|
|
143
|
+
// denied, …) wrapped in PartialEngineExistsError so the user sees
|
|
144
|
+
// both narratives: "we detected a partial engine and attempted
|
|
145
|
+
// resume" AND the underlying git failure. Without the wrap the
|
|
146
|
+
// raw git error loses the context that resume was in flight.
|
|
147
|
+
const cause = toError(error);
|
|
148
|
+
verbose(`Partial-engine probe failed with unexpected error: ${cause.message}`);
|
|
149
|
+
if (cause.stack) {
|
|
150
|
+
verbose(cause.stack);
|
|
151
|
+
}
|
|
152
|
+
throw new PartialEngineExistsError(paths.engine, cause);
|
|
64
153
|
}
|
|
65
154
|
}
|
|
66
155
|
throw new EngineExistsError(paths.engine);
|
|
@@ -122,7 +211,17 @@ export async function downloadCommand(projectRoot, options) {
|
|
|
122
211
|
warn('engine/ may now contain a partially initialized git repository. Re-run "fireforge download --force" to recreate the baseline cleanly.');
|
|
123
212
|
throw error;
|
|
124
213
|
}
|
|
125
|
-
//
|
|
214
|
+
// Restore any patch-touched files that ended up dirty after the initial
|
|
215
|
+
// commit (e.g. line-ending normalisation or extraction artefacts) so that
|
|
216
|
+
// a subsequent `fireforge import` works without --force.
|
|
217
|
+
//
|
|
218
|
+
// This runs BEFORE updateState so a restore failure keeps the previous
|
|
219
|
+
// downloadedVersion in state.json. The invariant we preserve is
|
|
220
|
+
// "state.downloadedVersion matches a clean engine": stamping the new
|
|
221
|
+
// version only after the restore succeeds means a failed clean-up will
|
|
222
|
+
// re-enter the resume path on the next `fireforge download` rather than
|
|
223
|
+
// reporting success against a dirty engine.
|
|
224
|
+
await cleanPatchTouchedFiles(paths.engine, paths.patches);
|
|
126
225
|
await updateState(projectRoot, {
|
|
127
226
|
downloadedVersion: version,
|
|
128
227
|
baseCommit,
|
|
@@ -47,6 +47,13 @@ export async function runPatchLint(engineDir, filesAffected, diffContent, config
|
|
|
47
47
|
}
|
|
48
48
|
info(`Lint: ${errors.length} error(s) downgraded to warnings (--skip-lint)`);
|
|
49
49
|
}
|
|
50
|
+
else if (skipLint) {
|
|
51
|
+
// Always announce that --skip-lint was honoured, even when there were
|
|
52
|
+
// no errors to downgrade, so the operator can confirm the flag took
|
|
53
|
+
// effect. Without this, a clean `--skip-lint` run emitted nothing
|
|
54
|
+
// about the flag and looked identical to an unflagged run.
|
|
55
|
+
info('Lint: 0 error(s); --skip-lint is active (no effect on this run).');
|
|
56
|
+
}
|
|
50
57
|
const warnCount = warnings.length + (skipLint ? errors.length : 0);
|
|
51
58
|
if (warnCount > 0) {
|
|
52
59
|
info(`Patch lint: ${warnCount} warning(s)`);
|
|
@@ -139,6 +139,11 @@ export async function exportCommand(projectRoot, files, options) {
|
|
|
139
139
|
}
|
|
140
140
|
let diff = await generatePatchDiff(paths.engine, allFiles);
|
|
141
141
|
if (!diff.trim()) {
|
|
142
|
+
if (options.skipLint) {
|
|
143
|
+
throw new GeneralError('The specified paths have no diff content to export. ' +
|
|
144
|
+
'(--skip-lint is set; lint checks were bypassed but there are still no content changes — ' +
|
|
145
|
+
'the paths may have only been lint-level differences that resolved, or the working tree is already clean.)');
|
|
146
|
+
}
|
|
142
147
|
throw new GeneralError('The specified paths have no diff content to export.');
|
|
143
148
|
}
|
|
144
149
|
// Ensure patches directory exists
|