@hominis/fireforge 0.13.2 → 0.15.1
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 +85 -0
- package/README.md +20 -1
- 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-templates.d.ts +26 -0
- package/dist/src/commands/furnace/create-templates.js +86 -0
- package/dist/src/commands/furnace/create.js +77 -103
- package/dist/src/commands/furnace/deploy.js +20 -5
- 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/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 +33 -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-paths.d.ts +2 -2
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate.js +47 -1
- package/dist/src/core/furnace-apply-ftl.d.ts +33 -0
- package/dist/src/core/furnace-apply-ftl.js +102 -0
- package/dist/src/core/furnace-apply-helpers.d.ts +10 -1
- package/dist/src/core/furnace-apply-helpers.js +16 -12
- package/dist/src/core/furnace-apply.js +7 -4
- package/dist/src/core/furnace-config-tokens.d.ts +11 -0
- package/dist/src/core/furnace-config-tokens.js +28 -0
- package/dist/src/core/furnace-config.d.ts +6 -0
- package/dist/src/core/furnace-config.js +8 -1
- package/dist/src/core/furnace-constants.d.ts +20 -0
- package/dist/src/core/furnace-constants.js +32 -0
- package/dist/src/core/furnace-registration-ast.d.ts +13 -1
- package/dist/src/core/furnace-registration-ast.js +58 -25
- package/dist/src/core/furnace-registration.d.ts +28 -1
- package/dist/src/core/furnace-registration.js +98 -1
- package/dist/src/core/furnace-staleness.d.ts +17 -0
- package/dist/src/core/furnace-staleness.js +58 -0
- package/dist/src/core/furnace-validate-accessibility.js +8 -2
- package/dist/src/core/furnace-validate-helpers.d.ts +8 -0
- package/dist/src/core/furnace-validate-helpers.js +81 -0
- package/dist/src/core/furnace-validate-registration.d.ts +8 -2
- package/dist/src/core/furnace-validate-registration.js +34 -9
- package/dist/src/core/furnace-validate.js +2 -2
- package/dist/src/core/marionette-preflight.d.ts +39 -0
- package/dist/src/core/marionette-preflight.js +210 -0
- 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/options.d.ts +6 -0
- package/dist/src/types/config.d.ts +7 -0
- package/dist/src/types/furnace.d.ts +8 -0
- package/dist/src/utils/process.d.ts +15 -2
- package/dist/src/utils/process.js +73 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,90 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.0
|
|
4
|
+
|
|
5
|
+
### Furnace registration
|
|
6
|
+
|
|
7
|
+
- `furnace apply` idempotency check is marker-comment-tolerant. Previously the single-line substring match (`content.includes('["tag",')`) missed multi-line entries, and the standalone-line regex anchored on `\s*$`, which did not allow trailing `// <marker>:` comments an operator may have appended to a previously-written entry. A duplicate tag was then inserted on every re-apply, and the second `setElementCreationCallback` invocation threw `NotSupportedError: Operation is not supported` at every window-load. The idempotency check now matches on tag-name column 0 (both single- and multi-line array shapes) and tolerates trailing `//` comments on the line.
|
|
8
|
+
- New optional fireforge.json field `markerComment` (e.g. `"HOMINIS"`) is appended as a ` // HOMINIS:` suffix to every line FireForge writes into `customElements.js`. Keeps fork modifications discoverable and re-applies idempotent without hand-tagging after each apply. The field is threaded through `applyCustomComponent` and `furnace deploy`, not just `furnace create`.
|
|
9
|
+
- `addCustomElementRegistration` and its regex fallback both accept the new marker as an optional parameter; the AST idempotency check and the regex-fallback idempotency check share a single helper (`isTagAlreadyRegistered`).
|
|
10
|
+
|
|
11
|
+
### Furnace `--localized`
|
|
12
|
+
|
|
13
|
+
- `furnace create --localized` now emits the Mozilla-idiomatic `MozLitElement` l10n pattern: a module-level `window.MozXULElement?.insertFTLIfNeeded("<chrome-uri>")` call and `this.ownerDocument.l10n?.connectRoot(this.shadowRoot)` / `disconnectRoot` in `connectedCallback` / `disconnectedCallback`. Previously the template called `this.insertFTLIfNeeded(...)` directly on a `MozLitElement` instance, which throws `TypeError: this.insertFTLIfNeeded is not a function` at every connect because that method lives on `MozXULElement`, not `MozLitElement`. The `--localized` path was silently non-functional.
|
|
14
|
+
- `furnace apply` now registers the scaffolded `.ftl` in the locale jar.mn (default `toolkit/locales/jar.mn`) so the chrome URI `insertFTLIfNeeded` expects actually resolves at runtime. Previously only the `.ftl` file itself was copied into the FTL tree, with no chrome registration. `furnace remove` and the workspace-delete codepath (`undeployCustomFiles`) drop the jar.mn entry symmetrically.
|
|
15
|
+
- The locale jar.mn write degrades gracefully — a missing target (non-standard fork tree) surfaces a structured step error rather than aborting apply, so a well-formed `.mjs`/`.css` is never blocked by a broken locale path.
|
|
16
|
+
- FTL chrome URIs are now derived from `furnace.json.ftlBasePath` via a pair of helpers (`resolveFtlChromeSubPath`, `resolveFtlLocaleJarMnPath`) so forks that customise the FTL tree get matching `insertFTLIfNeeded` and jar.mn output.
|
|
17
|
+
|
|
18
|
+
### Furnace validate
|
|
19
|
+
|
|
20
|
+
- `missing-token-link` now reads `tokenHostDocuments` from furnace.json and scans every configured chrome document for the tokens CSS link. Warning fires only when NONE of them link the tokens CSS; the warning enumerates the documents it actually checked. Previously the check was hardcoded to `browser/base/content/browser.xhtml`, which false-positived on forks that mount components in a different chrome document (e.g. `hominis.xhtml`). Defaults to `["browser/base/content/browser.xhtml"]` when omitted — behaviour is unchanged for projects that never set the field.
|
|
21
|
+
- `no-keyboard-handler` no longer warns when `@click` sits on a native interactive element (`<button>`, `<a href>`, `<input>`, `<select>`, `<textarea>`, `<summary>`, `<details>`, or the Firefox `moz-button`/`moz-toggle`/`moz-checkbox`/`moz-radio`/`moz-menulist` widgets). Those elements dispatch `click` on Enter and Space via the platform, so a duplicate `@keydown`/`@keypress` handler would double-fire. The rule still fires for synthetic interactive markup (e.g. `<div @click>`) and for bare `<a>` without an `href` attribute, which are the real keyboard-a11y hazards.
|
|
22
|
+
|
|
23
|
+
### Run / test
|
|
24
|
+
|
|
25
|
+
- `fireforge run`, `fireforge watch`, `fireforge build`, and every other `mach` invocation launched with inherited stdio now forward parent `SIGINT`/`SIGTERM` to the child as `SIGTERM` and wait ~1.5 s before escalating to `SIGKILL`. A second Ctrl-C during the grace window escalates immediately (matches the usual "hit Ctrl-C twice to force-quit" UX). Previously the parent could exit before Gecko's `AsyncShutdown` / `profileBeforeChange` blockers finished flushing in-memory state, losing the last few seconds of edits. The grace window is configurable via a new `shutdownGraceMs` option on `execInherit` / `execInheritCapture`.
|
|
26
|
+
- New `fireforge test --doctor` runs a short marionette handshake preflight before (optionally) invoking `mach test`. Spawns the built browser headless, opens a TCP socket to `127.0.0.1:2828`, waits for the handshake bytes, and reports PASS/FAIL with the tail of stderr on FAIL. When `--doctor` is supplied with no test paths, it exits after the preflight — a sub-minute way to tell "marionette wedged" apart from "test failed to discover" when `mach test` hangs for the full 360 s marionette timeout. When supplied with test paths, a FAIL preflight short-circuits before `mach test` runs.
|
|
27
|
+
|
|
28
|
+
### Internal
|
|
29
|
+
|
|
30
|
+
- Extracted `furnace-apply-ftl.ts`, `furnace-config-tokens.ts`, and `create-templates.ts` to keep apply / config / scaffolding files under the per-file LOC budget after the new features landed. `parseStringArray` is now exported from `furnace-config.ts` for cross-module reuse.
|
|
31
|
+
- New `src/core/marionette-preflight.ts` owns the `--doctor` probe and its teardown semantics.
|
|
32
|
+
- Test mocks for `furnace-registration.js` now cover the new `addLocaleFtlJarMnEntry` / `removeLocaleFtlJarMnEntry` exports; `config.js` mocks in apply-batch tests now cover `loadConfig` because the apply path reads `markerComment` from fireforge.json.
|
|
33
|
+
|
|
34
|
+
## 0.14.0
|
|
35
|
+
|
|
36
|
+
### Concurrency and atomicity
|
|
37
|
+
|
|
38
|
+
- 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.
|
|
39
|
+
- 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.
|
|
40
|
+
- `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.
|
|
41
|
+
|
|
42
|
+
### Rebase
|
|
43
|
+
|
|
44
|
+
- 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.
|
|
45
|
+
- `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.
|
|
46
|
+
- `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.
|
|
47
|
+
|
|
48
|
+
### Download
|
|
49
|
+
|
|
50
|
+
- `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.
|
|
51
|
+
- `cleanPatchTouchedFiles` runs before stamping `state.downloadedVersion`, preserving the invariant that the stamped version matches a clean engine.
|
|
52
|
+
- 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.
|
|
53
|
+
|
|
54
|
+
### Import
|
|
55
|
+
|
|
56
|
+
- 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.
|
|
57
|
+
- 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.
|
|
58
|
+
|
|
59
|
+
### Furnace
|
|
60
|
+
|
|
61
|
+
- `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".
|
|
62
|
+
- `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.
|
|
63
|
+
- `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.
|
|
64
|
+
- `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.
|
|
65
|
+
- `furnace deploy` asserts `applied[0].name` matches the requested component before persisting state; state-mismatch errors recommend `fireforge doctor --repair-furnace`.
|
|
66
|
+
- `furnace validate --fix` reports the actual delta from re-validation instead of inflating the count on no-op fixes.
|
|
67
|
+
- `furnace list -v` tolerates missing or unreadable component directories, rendering `unavailable` instead of terminating the listing.
|
|
68
|
+
- `furnace diff` surfaces `--reset-base` recovery in the primary error rather than a secondary catch block.
|
|
69
|
+
- `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.
|
|
70
|
+
|
|
71
|
+
### Other commands
|
|
72
|
+
|
|
73
|
+
- `setup` rejects project names whose sanitised slug is empty (emoji-only, pure punctuation, `---`). `validateConfig` similarly rejects empty `name`, `vendor`, `appId`, `binaryName`.
|
|
74
|
+
- `config --force` no longer bypasses structural validation for known keys in `SUPPORTED_CONFIG_PATHS`; the flag is only an escape hatch for unknown keys.
|
|
75
|
+
- `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`.
|
|
76
|
+
- `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.
|
|
77
|
+
- `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.
|
|
78
|
+
- `export` empty-diff error distinguishes the `--skip-lint` case; `export-shared` always announces when `--skip-lint` is active.
|
|
79
|
+
- `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.
|
|
80
|
+
- `token add --mode` uses Commander's `.choices()` so invalid modes fail with the built-in message and `--help` lists the valid options.
|
|
81
|
+
- `run` whitelisted exit codes (0, 130 SIGINT, 143 SIGTERM) are documented inline; SIGKILL (137) and other abnormal signal codes surface as build errors.
|
|
82
|
+
- `discard` wraps error causes via `toError` so thrown strings or numbers propagate as real Errors with stack traces.
|
|
83
|
+
|
|
84
|
+
### Internal
|
|
85
|
+
|
|
86
|
+
- 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.
|
|
87
|
+
|
|
3
88
|
## 0.13.0
|
|
4
89
|
|
|
5
90
|
### Setup
|
package/README.md
CHANGED
|
@@ -368,10 +368,29 @@ fireforge token --name "--my-color" --value "light-dark(#fff, #000)"
|
|
|
368
368
|
"patchLint": {
|
|
369
369
|
"checkJs": true,
|
|
370
370
|
"rawColorAllowlist": ["hominis-tokens.css"]
|
|
371
|
-
}
|
|
371
|
+
},
|
|
372
|
+
"markerComment": "MYBROWSER"
|
|
372
373
|
}
|
|
373
374
|
```
|
|
374
375
|
|
|
376
|
+
**`markerComment`** (optional). Appended as a ` // <marker>:` suffix to every line FireForge writes into upstream Firefox source files (starting with `customElements.js`). Keeps fork modifications discoverable and makes re-apply idempotent without hand-tagging entries after each `furnace apply`. Reject list: empty strings, leading/trailing whitespace, newlines, `*/` (would close an enclosing block comment), control characters.
|
|
377
|
+
|
|
378
|
+
**`furnace.json.tokenHostDocuments`** (optional). List of chrome XHTML documents the `missing-token-link` validator scans for the tokens CSS link. Forks with a second chrome host (e.g. `hominis.xhtml` alongside `browser.xhtml`) should list every document that may own the link — the rule fires only when NONE of them link the tokens CSS. Defaults to `["browser/base/content/browser.xhtml"]` when omitted.
|
|
379
|
+
|
|
380
|
+
### `furnace create --localized` for `MozLitElement`
|
|
381
|
+
|
|
382
|
+
`fireforge furnace create <tag> --localized` scaffolds a Fluent-ready component. The generated `.mjs` uses the Mozilla-idiomatic `MozLitElement` pattern: a module-level `window.MozXULElement?.insertFTLIfNeeded("<chrome-uri>")` plus `this.ownerDocument.l10n?.connectRoot(this.shadowRoot)` / `disconnectRoot` in `connectedCallback` / `disconnectedCallback`. The chrome URI derives from `furnace.json.ftlBasePath` (default `toolkit/locales/en-US/toolkit/global` → `toolkit/global/<tag>.ftl`). `furnace apply` registers the `.ftl` in the matching locale jar.mn (default `toolkit/locales/jar.mn`) so the chrome URI resolves at runtime. If the locale jar.mn is missing in your fork (non-standard tree), apply surfaces a structured step error instead of aborting — the `.mjs`/`.css` still ship.
|
|
383
|
+
|
|
384
|
+
### `fireforge test --doctor`
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
# Sub-minute marionette handshake probe; bails out of mach test on FAIL
|
|
388
|
+
fireforge test --doctor
|
|
389
|
+
fireforge test --doctor browser/base/content/test/foo/browser_bar.js
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Spawns the built browser headless, waits for a marionette handshake on `127.0.0.1:2828`, and reports PASS/FAIL with the tail of the browser's stderr on FAIL. Distinguishes "marionette wedged" (socket silent) from "mach test discovery failed" — both otherwise surface as a silent 360-second hang followed by `Passed: 0, Failed: 0`. Useful as a prefix on routine `fireforge test` invocations when marionette has been flaky.
|
|
393
|
+
|
|
375
394
|
## Roadmap
|
|
376
395
|
|
|
377
396
|
Planned but not yet implemented:
|
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
|