@hominis/fireforge 0.18.1 → 0.18.3
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/README.md +54 -33
- package/dist/src/commands/discard.js +93 -1
- package/dist/src/commands/doctor.js +17 -4
- package/dist/src/commands/download.js +21 -0
- package/dist/src/commands/export-all.js +35 -6
- package/dist/src/commands/export-flow.d.ts +4 -0
- package/dist/src/commands/export-flow.js +8 -0
- package/dist/src/commands/export.js +26 -2
- package/dist/src/commands/furnace/remove.js +68 -0
- package/dist/src/commands/import.js +9 -1
- package/dist/src/commands/lint.js +56 -10
- package/dist/src/commands/patch/index.d.ts +5 -3
- package/dist/src/commands/patch/index.js +10 -4
- package/dist/src/commands/patch/lint-ignore.d.ts +39 -0
- package/dist/src/commands/patch/lint-ignore.js +200 -0
- package/dist/src/commands/patch/tier.d.ts +34 -0
- package/dist/src/commands/patch/tier.js +134 -0
- package/dist/src/commands/re-export-files.js +88 -45
- package/dist/src/commands/re-export.js +49 -6
- package/dist/src/commands/status.js +27 -0
- package/dist/src/commands/test.js +20 -1
- package/dist/src/commands/token.js +1 -1
- package/dist/src/core/furnace-config.js +19 -0
- package/dist/src/core/git-diff.js +34 -2
- package/dist/src/core/license-headers.d.ts +8 -0
- package/dist/src/core/license-headers.js +15 -1
- package/dist/src/core/manifest-rules.js +9 -1
- package/dist/src/core/patch-export.d.ts +77 -2
- package/dist/src/core/patch-export.js +82 -3
- package/dist/src/core/patch-lint.js +86 -29
- package/dist/src/core/register-shared-css.js +8 -2
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/options.d.ts +67 -0
- package/dist/src/types/commands/patches.d.ts +6 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -143,6 +143,9 @@ fireforge re-export --all --scan
|
|
|
143
143
|
# Preview what an export would do without writing
|
|
144
144
|
fireforge export browser/base/content/browser.js --dry-run
|
|
145
145
|
|
|
146
|
+
# Same preview surface for the aggregate path
|
|
147
|
+
fireforge export-all --name "all-changes" --category ui --dry-run
|
|
148
|
+
|
|
146
149
|
# Insert a new patch at a specific position
|
|
147
150
|
fireforge export browser/base/content/browser.js --order 3 --name "inserted" --category ui
|
|
148
151
|
fireforge export browser/base/content/browser.js --before 005-ui-sidebar.patch --name "prelim"
|
|
@@ -158,7 +161,7 @@ fireforge re-export --files browser/base/content/browser.js 002-ui-toolbar
|
|
|
158
161
|
fireforge re-export --all --scan --stamp
|
|
159
162
|
```
|
|
160
163
|
|
|
161
|
-
`export` refuses when the new patch's `filesAffected` would overlap with files already claimed by another non-superseded patch. Repartitioning ownership is a deliberate operation: the message points at `fireforge re-export --files <paths> <patch>` as the safe primitive. Pass `--allow-overlap` to acknowledge the conflict and proceed anyway — the resulting queue will fail `fireforge verify` immediately, so this is an intentional escape hatch, not a default.
|
|
164
|
+
`export` refuses when the new patch's `filesAffected` would overlap with files already claimed by another non-superseded patch. Repartitioning ownership is a deliberate operation: the message points at `fireforge re-export --files <paths> <patch>` as the safe primitive. Pass `--allow-overlap` to acknowledge the conflict and proceed anyway — the resulting queue will fail `fireforge verify` immediately, so this is an intentional escape hatch, not a default. The flag covers cross-patch _modification_ overlap, where two patches both edit the same file. It does NOT bypass the new-file creation guard: two patches creating the same path on `/dev/null` cannot coexist in any apply order, so that case stays a hard refusal regardless of `--allow-overlap`.
|
|
162
165
|
|
|
163
166
|
`re-export --scan` also prompts before broadening a patch with more than a handful of newly discovered files or with files spanning multiple directories. The gate keeps the common refresh case frictionless (small, same-directory additions) while catching the failure mode where `--scan` silently pulls an adjacent feature into the wrong patch. Non-interactive mode requires `--yes` to acknowledge a broad expansion; dry-run previews never require confirmation.
|
|
164
167
|
|
|
@@ -210,7 +213,11 @@ This re-exports the fixed patch and clears the conflict state. The command is de
|
|
|
210
213
|
|
|
211
214
|
The optional `lintIgnore` field lists lint check IDs to suppress for that patch specifically. Useful for the class of patch that is advisory-noisy by nature — a cohesive branding bundle, a localised-resource pack, an auto-generated manifest — where `--skip-lint` is too blunt and a per-line marker cannot exist (the `.patch` body is regenerated on every export). Threaded through `export`, `re-export`, `re-export --files`, and `lint --per-patch`. Unknown check IDs are a no-op.
|
|
212
215
|
|
|
213
|
-
|
|
216
|
+
Settable through the CLI in two places. `fireforge export --lint-ignore <check-id>` (repeatable) writes the field on creation; `fireforge re-export <name> --lint-ignore <check-id>` (repeatable, append/union semantics, de-duplicated) adds entries to an existing patch on the next refresh. For metadata-only edits that should **not** regenerate the `.patch` body — including the inverse `--remove` and `--clear` modes that re-export's append-only flag cannot express — use `fireforge patch lint-ignore <name> --add <id> | --remove <id> | --clear`.
|
|
217
|
+
|
|
218
|
+
The optional `tier` field (only `"branding"` recognised) forces the branding threshold tier for the `large-patch-files` and `large-patch-lines` rules regardless of what `filesAffected` looks like. The automatic branding-tier detection already fires when every file is under `browser/branding/` plus a narrow allowlist of branding-registration siblings (`browser/moz.configure`, `browser/confvars.sh`) — covering the canonical Firefox fork shape. Declare `tier: "branding"` only when the patch legitimately also touches a non-allowlisted sibling the auto-detector cannot reach (a fork-specific theme override under `browser/themes/<name>/`, a vendor-specific icon resource, etc.). Precedence is `test > branding > general`: a patch of all-tests always gets the more permissive test-tier thresholds even if it declares `tier: "branding"`. Unknown tier values are rejected at load time rather than silently stripped, so a typo surfaces as a loader error. Prefer `tier` over `lintIgnore: ["large-patch-lines"]` when the patch is legitimately branding-shaped — `tier` keeps the rule running at the correct thresholds (so the warning still surfaces if the patch crosses them); `lintIgnore` drops the rule entirely.
|
|
219
|
+
|
|
220
|
+
Settable via `fireforge export --tier branding` on creation, `fireforge re-export <name> --tier branding` on refresh, and `fireforge patch tier <name> --tier branding | --clear` for a metadata-only edit that does not rewrite the `.patch` body. The CLI rejects values other than `branding` up-front (matching the validator's strictness), and `re-export --tier` / `--lint-ignore` refuse `--all` because mass tier/ignore edits across a heterogeneous queue are virtually always footguns.
|
|
214
221
|
|
|
215
222
|
If the manifest drifts after an interrupted export or manual edits, `fireforge import` will stop rather then silently applying a stale stack. Use `fireforge doctor --repair-patches-manifest` to rebuild it from disk. Because the rebuild is deterministic, the result will always be consistent with what is actually on the filesystem.
|
|
216
223
|
|
|
@@ -223,24 +230,24 @@ If the manifest drifts after an interrupted export or manual edits, `fireforge i
|
|
|
223
230
|
|
|
224
231
|
By default, a standalone `fireforge lint` (no arguments) lints the **aggregate** `git diff HEAD` — i.e. every applied patch summed — with tool-managed branding paths (`browser/branding/<binaryName>/`) excluded. A fresh-setup workspace carries a large generated branding diff that operators did not author directly, and letting it through tripped the patch-size and license-header rules on content that matches the `branding` bucket in `fireforge status`. When the exclusion fires the command prints a one-line note naming the excluded count so the filter is visible. On a repo where `fireforge import` or `fireforge rebase` has just applied the full queue, the patch-size rules (`large-patch-lines`, `large-patch-files`) fire against the sum, which reads as "my queue is broken" when it is really an artefact of aggregation. Use `fireforge lint --per-patch` to rescope the diff to each patch's own `filesAffected`, honouring the patch's own `lintIgnore`. Cross-patch rules (`duplicate-new-file-creation`, `forward-import`) still run once over the whole queue either way. Pass explicit file paths to narrow the scope further — explicit-path mode does lint branding files (the operator's explicit request wins over the branding exclusion); the three modes (aggregate, file-scoped, per-patch) are mutually exclusive.
|
|
225
232
|
|
|
226
|
-
| Check | Scope
|
|
227
|
-
| ------------------------------ |
|
|
228
|
-
| `missing-license-header` | New files (JS/CSS/FTL)
|
|
229
|
-
| `relative-import` | JS/MJS files
|
|
230
|
-
| `token-prefix-violation` | CSS files (with furnace)
|
|
231
|
-
| `raw-color-value` | Introduced CSS color values (allowlist via `patchLint.rawColorAllowlist`)
|
|
232
|
-
| `duplicate-new-file-creation` | Same path created by multiple patches
|
|
233
|
-
| `forward-import` | Patch imports from a later-patch file
|
|
234
|
-
| `missing-jsdoc` | Exports in patch-owned `.sys.mjs`
|
|
235
|
-
| `jsdoc-param-mismatch` | Exports in patch-owned `.sys.mjs`
|
|
236
|
-
| `jsdoc-missing-returns` | Exports in patch-owned `.sys.mjs`
|
|
237
|
-
| `checkjs-type-error` | Patch-owned `.sys.mjs` (opt-in)
|
|
238
|
-
| `missing-modification-comment` | Modified upstream JS/MJS
|
|
239
|
-
| `modified-file-missing-header` | Modified upstream files (JS/CSS/FTL)
|
|
240
|
-
| `file-too-large` | New files (tiered: 500/750/900 general, 1200/1400/1600 test)
|
|
241
|
-
| `observer-topic-naming` | Observer topics with binaryName
|
|
242
|
-
| `large-patch-files` | Patches affecting >5
|
|
243
|
-
| `large-patch-lines` | Patch line count (tiered: 800/1500/3000 general, 1500/3000/6000 test,
|
|
233
|
+
| Check | Scope | Severity |
|
|
234
|
+
| ------------------------------ | ------------------------------------------------------------------------------------------------ | ------------------------ |
|
|
235
|
+
| `missing-license-header` | New files (JS/CSS/FTL) | error |
|
|
236
|
+
| `relative-import` | JS/MJS files | error |
|
|
237
|
+
| `token-prefix-violation` | CSS files (with furnace) | error |
|
|
238
|
+
| `raw-color-value` | Introduced CSS color values (allowlist via `patchLint.rawColorAllowlist`) | error |
|
|
239
|
+
| `duplicate-new-file-creation` | Same path created by multiple patches | error |
|
|
240
|
+
| `forward-import` | Patch imports from a later-patch file | error |
|
|
241
|
+
| `missing-jsdoc` | Exports in patch-owned `.sys.mjs` | error |
|
|
242
|
+
| `jsdoc-param-mismatch` | Exports in patch-owned `.sys.mjs` | error |
|
|
243
|
+
| `jsdoc-missing-returns` | Exports in patch-owned `.sys.mjs` | error |
|
|
244
|
+
| `checkjs-type-error` | Patch-owned `.sys.mjs` (opt-in) | error |
|
|
245
|
+
| `missing-modification-comment` | Modified upstream JS/MJS | warning |
|
|
246
|
+
| `modified-file-missing-header` | Modified upstream files (JS/CSS/FTL) | warning |
|
|
247
|
+
| `file-too-large` | New files (tiered: 500/750/900 general, 1200/1400/1600 test) | notice / warning / error |
|
|
248
|
+
| `observer-topic-naming` | Observer topics with binaryName | warning |
|
|
249
|
+
| `large-patch-files` | Patches affecting many files (tiered: >5 general, >5 test, >60 branding) | warning |
|
|
250
|
+
| `large-patch-lines` | Patch line count (tiered: 800/1500/3000 general, 1500/3000/6000 test, 8000/18000/30000 branding) | notice / warning / error |
|
|
244
251
|
|
|
245
252
|
**JSDoc validation** uses AST-based analysis (Acorn) to validate exported APIs in patch-owned `.sys.mjs` files. A file is "patch-owned" if it was newly created by the current diff or by an existing patch in the queue. Functions must document every `@param` (names must match) and include `@returns` when the function returns a value. Exported constants and classes require a JSDoc block.
|
|
246
253
|
|
|
@@ -263,16 +270,16 @@ fireforge status --json # machine-readable classified output
|
|
|
263
270
|
|
|
264
271
|
Then fix with the appropriate primitive:
|
|
265
272
|
|
|
266
|
-
| Problem | Fix
|
|
267
|
-
| ---------------------------------------------- |
|
|
268
|
-
| Two patches each creating the same file | `fireforge patch delete <duplicate>` or `fireforge re-export --files`
|
|
269
|
-
| A patch imports from a module in a later patch | `fireforge patch reorder <later> --before <importer>`
|
|
270
|
-
| Wrong patch ordering | `fireforge patch reorder <patch> --to <N>`
|
|
271
|
-
| Ordinal gaps after deletes/splits | `fireforge patch compact`
|
|
272
|
-
| A patch claims files that belong elsewhere | `fireforge re-export --files <subset> <patch>`
|
|
273
|
-
| Manifest references a missing patch file | `fireforge doctor --repair-patches-manifest`
|
|
274
|
-
| Dangling widget / locale registration in patch | Re-run `fireforge export` without `--exclude-furnace` to capture the source files, or revert furnace changes
|
|
275
|
-
| Unmanaged changes you want to discard | `fireforge discard <file>` or `fireforge reset`
|
|
273
|
+
| Problem | Fix |
|
|
274
|
+
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
275
|
+
| Two patches each creating the same file | `fireforge patch delete <duplicate>` or `fireforge re-export --files` |
|
|
276
|
+
| A patch imports from a module in a later patch | `fireforge patch reorder <later> --before <importer>` |
|
|
277
|
+
| Wrong patch ordering | `fireforge patch reorder <patch> --to <N>` |
|
|
278
|
+
| Ordinal gaps after deletes/splits | `fireforge patch compact` |
|
|
279
|
+
| A patch claims files that belong elsewhere | `fireforge re-export --files <subset> <patch>` |
|
|
280
|
+
| Manifest references a missing patch file | `fireforge doctor --repair-patches-manifest` |
|
|
281
|
+
| Dangling widget / locale registration in patch | Re-run `fireforge export` without `--exclude-furnace` to capture the source files, or revert furnace changes |
|
|
282
|
+
| Unmanaged changes you want to discard | `fireforge discard <file>` (also accepts a directory path to discard everything beneath it) or `fireforge reset` |
|
|
276
283
|
|
|
277
284
|
Every destructive command defaults to an interactive confirmation with a change summary. `--dry-run` previews without writing; `--yes` skips the prompt for CI; `--force-unsafe` bypasses structural refusals when you have context the linter cannot see. Do not hand-edit `patches.json` as the file is owned by FireForge — `doctor --repair-patches-manifest` reconstructs missing metadata, and `fireforge re-export <filename> --description "<text>"` overwrites recovered entries with operator-supplied metadata through the tool. `fireforge verify` cross-checks every registration hunk in each patch body against the files the queue and engine supply, so a patch that registers a widget / locale without carrying its source surfaces as a `dangling-registration` error rather than slipping through as "Verify clean"; `fireforge export-all --exclude-furnace` refuses up-front when it would produce that shape.
|
|
278
285
|
|
|
@@ -436,11 +443,21 @@ fireforge patch reorder 003-ui-sidebar.patch --before 001-branding-logo.patch
|
|
|
436
443
|
|
|
437
444
|
# Close ordinal gaps after deletes or splits (e.g. 1, 3, 7 → 1, 2, 3)
|
|
438
445
|
fireforge patch compact
|
|
446
|
+
|
|
447
|
+
# Set or clear the threshold-tier override on a single patch
|
|
448
|
+
# (no .patch body rewrite — manifest entry only)
|
|
449
|
+
fireforge patch tier 001-branding-assets.patch --tier branding
|
|
450
|
+
fireforge patch tier 001-branding-assets.patch --clear
|
|
451
|
+
|
|
452
|
+
# Edit the lintIgnore list on a single patch (one mode per invocation)
|
|
453
|
+
fireforge patch lint-ignore 001-branding-assets.patch --add large-patch-lines --add large-patch-files
|
|
454
|
+
fireforge patch lint-ignore 001-branding-assets.patch --remove large-patch-lines
|
|
455
|
+
fireforge patch lint-ignore 001-branding-assets.patch --clear
|
|
439
456
|
```
|
|
440
457
|
|
|
441
|
-
`
|
|
458
|
+
All `patch <verb>` subcommands accept three identifier forms for their target: the ordinal number (`fireforge patch reorder 3 --to 1`), the full filename (`003-ui-sidebar-tweaks.patch`), or the manifest `name` handle the patch was exported with (`ui-sidebar-tweaks`). Anchors passed to `--before` / `--after` accept the same three forms. All subcommands support `--dry-run` and `--yes`.
|
|
442
459
|
|
|
443
|
-
|
|
460
|
+
`patch tier` and `patch lint-ignore` are metadata-only edits: they update `patches/patches.json` under the patch directory lock and never rewrite the `.patch` body. Reach for them when an operator-visible advisory (e.g. `large-patch-files` firing on a 56-file fresh-fork branding bundle) needs the threshold-tier override or a per-patch lint suppression but the patch body is already correct — running `re-export` for that case wastes an engine read + diff regeneration. `patch lint-ignore` modes (`--add` / `--remove` / `--clear`) are mutually exclusive; `patch tier` rejects `--tier` and `--clear` together. The history log records each invocation under `operation: "patch-tier"` / `"patch-lint-ignore"` for audit.
|
|
444
461
|
|
|
445
462
|
### Additional workflow commands
|
|
446
463
|
|
|
@@ -452,7 +469,11 @@ fireforge package
|
|
|
452
469
|
fireforge watch
|
|
453
470
|
|
|
454
471
|
# Add a CSS design token (requires `fireforge furnace init` first; see the Furnace/Tokens section below)
|
|
455
|
-
|
|
472
|
+
# The `--` separator is required because the token name itself starts with `--`,
|
|
473
|
+
# which Commander would otherwise read as an option flag. Bare names without `--`
|
|
474
|
+
# are accepted directly and get the configured `tokenPrefix` prepended.
|
|
475
|
+
fireforge token add --category 'Colors — General' --mode static -- --my-color 'light-dark(#fff, #000)'
|
|
476
|
+
fireforge token add --category 'Colors — General' --mode static my-color '#fff' # bare-name form
|
|
456
477
|
```
|
|
457
478
|
|
|
458
479
|
Tokens live in the Furnace-managed tokens CSS file (`engine/browser/themes/shared/<binaryName>-tokens.css`), scaffolded by `fireforge furnace init` alongside `furnace.json`. The scaffold seeds a default set of categories (`Colors — General`, `Colors — Canvas`, `Colors — Experiment`, `Spacing`); add a category by hand as a `/* = My Category = */` comment inside the `:root { … }` block if you need another. `fireforge furnace init` also registers the tokens CSS path in `patchLint.rawColorAllowlist` so raw color literals inside it are not flagged by `fireforge lint`, and derives `tokenPrefix: --<binaryName>-` from `fireforge.json`'s `binaryName` so `fireforge token coverage` has a prefix to key off on the very first run. Projects that prefer a different prefix can override it in `furnace.json` after init.
|
|
@@ -11,6 +11,82 @@ import { toError } from '../utils/errors.js';
|
|
|
11
11
|
import { pathExists } from '../utils/fs.js';
|
|
12
12
|
import { info, intro, isCancel, outro, spinner, warn } from '../utils/logger.js';
|
|
13
13
|
import { pickDefined } from '../utils/options.js';
|
|
14
|
+
/**
|
|
15
|
+
* Discards every status entry whose path lives under `dirPath`. Used by
|
|
16
|
+
* `discardCommand` as a directory-recursion fallback when the operator
|
|
17
|
+
* passed a directory path that contains modified or untracked entries
|
|
18
|
+
* but is not itself a status entry.
|
|
19
|
+
*
|
|
20
|
+
* Mirrors the single-file path's confirmation, dry-run, and Furnace-aware
|
|
21
|
+
* warning behaviour so the contract stays consistent. Each per-entry
|
|
22
|
+
* discard runs sequentially under its own try/catch so a failure on one
|
|
23
|
+
* file is reported but does not block the remaining files in the batch.
|
|
24
|
+
*/
|
|
25
|
+
async function discardDirectoryEntries(projectRoot, engineDir, dirPath, entries, options) {
|
|
26
|
+
if (!options.yes && !options.dryRun) {
|
|
27
|
+
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
28
|
+
if (!isInteractive) {
|
|
29
|
+
throw new InvalidArgumentError('Interactive confirmation not available. Use --yes flag to discard without confirmation.', 'Use: fireforge discard <directory> --yes');
|
|
30
|
+
}
|
|
31
|
+
const confirmed = await confirm({
|
|
32
|
+
message: `Discard changes to ${entries.length} file${entries.length === 1 ? '' : 's'} under ${dirPath}/?`,
|
|
33
|
+
initialValue: false,
|
|
34
|
+
});
|
|
35
|
+
if (isCancel(confirmed) || !confirmed) {
|
|
36
|
+
outro('Discard cancelled');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (options.dryRun) {
|
|
41
|
+
info(`Would discard changes to ${entries.length} file(s) under ${dirPath}/:`);
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
const target = entry.originalPath && entry.originalPath !== entry.file
|
|
44
|
+
? `${entry.originalPath} -> ${entry.file}`
|
|
45
|
+
: entry.file;
|
|
46
|
+
info(` ${target}`);
|
|
47
|
+
}
|
|
48
|
+
outro('Dry run complete — no changes made');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const s = spinner(`Discarding ${entries.length} file(s) under ${dirPath}/...`);
|
|
52
|
+
let succeeded = 0;
|
|
53
|
+
const failures = [];
|
|
54
|
+
try {
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
try {
|
|
57
|
+
await discardStatusEntry(engineDir, entry);
|
|
58
|
+
succeeded += 1;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
failures.push(`${entry.file}: ${toError(error).message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
s.stop(`Discarded ${succeeded} of ${entries.length} file(s) under ${dirPath}/${failures.length > 0 ? ` (${failures.length} failed)` : ''}`);
|
|
65
|
+
for (const failure of failures) {
|
|
66
|
+
warn(` ${failure}`);
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const furnacePrefixes = await collectFurnaceManagedPrefixes(projectRoot);
|
|
70
|
+
const dirIsFurnace = [...furnacePrefixes].some((prefix) => `${dirPath}/`.startsWith(prefix) || prefix.startsWith(`${dirPath}/`));
|
|
71
|
+
if (dirIsFurnace) {
|
|
72
|
+
warn('These paths are managed by Furnace. Run "fireforge furnace apply" to redeploy components if needed.');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Furnace config may not exist — skip silently
|
|
77
|
+
}
|
|
78
|
+
if (failures.length > 0) {
|
|
79
|
+
throw new GeneralError(`Failed to discard ${failures.length} file(s) under ${dirPath}/. See warnings above.`);
|
|
80
|
+
}
|
|
81
|
+
outro(`${succeeded} file(s) restored to original state`);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (!(error instanceof GeneralError)) {
|
|
85
|
+
s.error('Discard failed');
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
14
90
|
/**
|
|
15
91
|
* Runs the discard command to revert changes to a specific file.
|
|
16
92
|
* @param projectRoot - Root directory of the project
|
|
@@ -31,7 +107,23 @@ export async function discardCommand(projectRoot, file, options = {}) {
|
|
|
31
107
|
// Check if the file has changes
|
|
32
108
|
const statusEntries = await expandUntrackedDirectoryEntries(paths.engine, await getWorkingTreeStatus(paths.engine));
|
|
33
109
|
const statusEntry = statusEntries.find((entry) => entry.file === file || entry.originalPath === file);
|
|
110
|
+
// Directory recursion fallback: when the explicit path does not match a
|
|
111
|
+
// single status entry but DOES correspond to one or more entries below
|
|
112
|
+
// it, treat the input as a directory and discard everything inside.
|
|
113
|
+
// 2026-04-25 eval Finding 20: `discard browser/components/storybook/
|
|
114
|
+
// stories/furnace --yes` failed with "no changes to discard" even
|
|
115
|
+
// though `status --unmanaged` listed 23 files under that directory —
|
|
116
|
+
// operators were forced to discard each file individually or fall
|
|
117
|
+
// back to non-FireForge cleanup commands. Match against the
|
|
118
|
+
// directory-with-trailing-slash form so a path like `foo/bar` doesn't
|
|
119
|
+
// accidentally match `foo/bar2/file`.
|
|
34
120
|
if (!statusEntry) {
|
|
121
|
+
const dirPrefix = file.endsWith('/') ? file : `${file}/`;
|
|
122
|
+
const dirEntries = statusEntries.filter((entry) => entry.file.startsWith(dirPrefix) || entry.originalPath?.startsWith(dirPrefix));
|
|
123
|
+
if (dirEntries.length > 0) {
|
|
124
|
+
await discardDirectoryEntries(projectRoot, paths.engine, file, dirEntries, options);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
35
127
|
throw new GeneralError(`File "${file}" has no changes to discard.`);
|
|
36
128
|
}
|
|
37
129
|
if (!options.yes && !options.dryRun) {
|
|
@@ -91,7 +183,7 @@ export async function discardCommand(projectRoot, file, options = {}) {
|
|
|
91
183
|
export function registerDiscard(program, { getProjectRoot, withErrorHandling }) {
|
|
92
184
|
program
|
|
93
185
|
.command('discard <file>')
|
|
94
|
-
.description('Discard changes to a specific file (deletes untracked files)')
|
|
186
|
+
.description('Discard changes to a specific file (deletes untracked files). Pass a directory path to discard every modified or untracked file beneath it; the operation walks the status output and reverts each match individually.')
|
|
95
187
|
.option('--dry-run', 'Show what would be discarded without doing it')
|
|
96
188
|
.option('-y, --yes', 'Skip confirmation prompt')
|
|
97
189
|
.action(withErrorHandling(async (file, options) => {
|
|
@@ -9,7 +9,7 @@ import { ExitCode } from '../errors/codes.js';
|
|
|
9
9
|
import { toError } from '../utils/errors.js';
|
|
10
10
|
import { pathExists } from '../utils/fs.js';
|
|
11
11
|
import { error, info, intro, outro, success, warn } from '../utils/logger.js';
|
|
12
|
-
import {
|
|
12
|
+
import { findExecutable } from '../utils/process.js';
|
|
13
13
|
import { FURNACE_DOCTOR_CHECKS } from './doctor-furnace.js';
|
|
14
14
|
import { inspectEngineWorkingTree } from './doctor-working-tree.js';
|
|
15
15
|
/**
|
|
@@ -255,9 +255,22 @@ const DOCTOR_CHECKS = [
|
|
|
255
255
|
// failure site.
|
|
256
256
|
name: 'Watchman available',
|
|
257
257
|
run: async () => {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
258
|
+
// Resolve the absolute path so the OK row names what doctor actually
|
|
259
|
+
// found. The 2026-04-25 eval flagged a confusing case where the
|
|
260
|
+
// operator's interactive shell returned no result for `which
|
|
261
|
+
// watchman` but doctor still printed "OK" — the cause was a
|
|
262
|
+
// PATH-export discrepancy between the shell and the spawned
|
|
263
|
+
// subprocess, and surfacing the resolved path makes the discrepancy
|
|
264
|
+
// visible without users having to re-run with a verbose flag.
|
|
265
|
+
const path = await findExecutable('watchman');
|
|
266
|
+
if (path) {
|
|
267
|
+
return {
|
|
268
|
+
name: 'Watchman available',
|
|
269
|
+
passed: true,
|
|
270
|
+
severity: 'ok',
|
|
271
|
+
message: `OK (${path})`,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
261
274
|
return warning('Watchman available', 'watchman is not installed or not on PATH. "fireforge watch" requires it.', 'Install watchman (brew install watchman / dnf install watchman / https://facebook.github.io/watchman/), then re-run doctor.');
|
|
262
275
|
},
|
|
263
276
|
},
|
|
@@ -69,6 +69,25 @@ async function cleanPatchTouchedFiles(engineDir, patchesDir, preExistingDirty) {
|
|
|
69
69
|
}
|
|
70
70
|
return { hadQueue: true, restored: toClean.length, preserved: preserved.length };
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Prints a one-line nudge pointing at `fireforge import` when the project
|
|
74
|
+
* carries a non-empty patch queue but the just-downloaded engine has not
|
|
75
|
+
* yet had any patches applied. The post-download spinner closes with
|
|
76
|
+
* "Patch-touched files already match baseline" because a fresh tree IS at
|
|
77
|
+
* baseline, but the 2026-04-25 eval saw operators read that as "patches
|
|
78
|
+
* are restored" and skip the import step. The note is suppressed when
|
|
79
|
+
* patches/ is missing or the manifest is empty so unconfigured projects
|
|
80
|
+
* stay quiet.
|
|
81
|
+
*/
|
|
82
|
+
async function noteUnappliedPatches(patchesDir) {
|
|
83
|
+
if (!(await pathExists(patchesDir)))
|
|
84
|
+
return;
|
|
85
|
+
const manifest = await loadPatchesManifest(patchesDir);
|
|
86
|
+
if (!manifest || manifest.patches.length === 0)
|
|
87
|
+
return;
|
|
88
|
+
const n = manifest.patches.length;
|
|
89
|
+
info(`Note: ${n} patch${n === 1 ? '' : 'es'} in patches/ have not been applied to this fresh engine. Run "fireforge import" to apply them.`);
|
|
90
|
+
}
|
|
72
91
|
/**
|
|
73
92
|
* Stops `restoreSpinner` with a message that reflects what actually
|
|
74
93
|
* happened. Three branches: empty queue → explicit no-op; queue present but
|
|
@@ -151,6 +170,7 @@ export async function downloadCommand(projectRoot, options) {
|
|
|
151
170
|
downloadedVersion: version,
|
|
152
171
|
baseCommit,
|
|
153
172
|
});
|
|
173
|
+
await noteUnappliedPatches(paths.patches);
|
|
154
174
|
outro(`Firefox ${version} is ready! (resumed from partial init)`);
|
|
155
175
|
return;
|
|
156
176
|
}
|
|
@@ -299,6 +319,7 @@ export async function downloadCommand(projectRoot, options) {
|
|
|
299
319
|
downloadedVersion: version,
|
|
300
320
|
baseCommit,
|
|
301
321
|
});
|
|
322
|
+
await noteUnappliedPatches(paths.patches);
|
|
302
323
|
outro(`Firefox ${version} is ready!`);
|
|
303
324
|
}
|
|
304
325
|
/** Registers the download command on the CLI program. */
|
|
@@ -15,6 +15,7 @@ import { ensureDir, pathExists } from '../utils/fs.js';
|
|
|
15
15
|
import { info, intro, outro, spinner } from '../utils/logger.js';
|
|
16
16
|
import { pickDefined } from '../utils/options.js';
|
|
17
17
|
import { PATCH_CATEGORIES } from '../utils/validation.js';
|
|
18
|
+
import { renderDryRunPreview } from './export-flow.js';
|
|
18
19
|
import { autoFixLicenseHeaders, confirmSupersedePatches, guardOwnershipOverlap, promptExportPatchMetadata, runPatchLint, } from './export-shared.js';
|
|
19
20
|
async function checkBrandingManagedFiles(paths, config) {
|
|
20
21
|
const changedFiles = await getWorkingTreeStatus(paths.engine);
|
|
@@ -158,7 +159,8 @@ async function checkDuplicateNewFileCreations(paths, diff) {
|
|
|
158
159
|
.join('\n');
|
|
159
160
|
throw new GeneralError('Export-all refuses to capture new-file creations that are already claimed by existing patches.\n\n' +
|
|
160
161
|
`Conflicting creations:\n${conflictList}\n\n` +
|
|
161
|
-
'Only one patch may create a given path
|
|
162
|
+
'Only one patch may create a given path — two creation hunks on /dev/null cannot coexist in any apply order, so this case is structurally unrecoverable rather than verify-failing. The --allow-overlap escape hatch covers cross-patch MODIFICATION overlap (which yields a queue that fails verify but still applies); it deliberately does NOT cover this case. ' +
|
|
163
|
+
'Run "fireforge export <path> [...]" with an explicit file list that omits the already-claimed path(s), or resolve the conflict via "fireforge patch delete" / "fireforge re-export --files" before retrying export-all.');
|
|
162
164
|
}
|
|
163
165
|
/**
|
|
164
166
|
* Runs the export-all command to export all changes as a patch.
|
|
@@ -166,7 +168,8 @@ async function checkDuplicateNewFileCreations(paths, diff) {
|
|
|
166
168
|
* @param options - Export options
|
|
167
169
|
*/
|
|
168
170
|
export async function exportAllCommand(projectRoot, options = {}) {
|
|
169
|
-
|
|
171
|
+
const isDryRun = options.dryRun === true;
|
|
172
|
+
intro(isDryRun ? 'FireForge Export All (dry run)' : 'FireForge Export All');
|
|
170
173
|
const paths = getProjectPaths(projectRoot);
|
|
171
174
|
// Check if engine exists
|
|
172
175
|
if (!(await pathExists(paths.engine))) {
|
|
@@ -236,13 +239,38 @@ export async function exportAllCommand(projectRoot, options = {}) {
|
|
|
236
239
|
if (!metadata)
|
|
237
240
|
return;
|
|
238
241
|
const { patchName, selectedCategory, description } = metadata;
|
|
239
|
-
// Ensure patches directory exists
|
|
240
|
-
|
|
241
|
-
|
|
242
|
+
// Ensure patches directory exists. Skip during a dry-run so the command
|
|
243
|
+
// is purely read-only — `--dry-run` callers should be safe to invoke
|
|
244
|
+
// against a project that has never exported a patch without leaving the
|
|
245
|
+
// empty `patches/` directory behind.
|
|
246
|
+
if (!isDryRun) {
|
|
247
|
+
await ensureDir(paths.patches);
|
|
248
|
+
}
|
|
249
|
+
const s = spinner(isDryRun ? 'Planning export-all...' : 'Exporting all changes...');
|
|
242
250
|
try {
|
|
243
251
|
// Extract affected files from diff
|
|
244
252
|
const filesAffected = extractAffectedFiles(diff);
|
|
245
253
|
await runPatchLint(paths.engine, filesAffected, diff, config, options.skipLint);
|
|
254
|
+
// Dry-run: enumerate filename, metadata, and supersede coverage without
|
|
255
|
+
// writing. Mirrors `fireforge export --dry-run` so the same preview
|
|
256
|
+
// surface is available for both targeted and aggregate exports. Runs
|
|
257
|
+
// AFTER lint so the operator sees the same lint output they would on
|
|
258
|
+
// a real run; runs BEFORE the supersede confirmation prompt because
|
|
259
|
+
// confirming a dry-run is meaningless.
|
|
260
|
+
if (isDryRun) {
|
|
261
|
+
s.stop('Plan ready');
|
|
262
|
+
await renderDryRunPreview({
|
|
263
|
+
patchesDir: paths.patches,
|
|
264
|
+
category: selectedCategory,
|
|
265
|
+
name: patchName,
|
|
266
|
+
description,
|
|
267
|
+
filesAffected,
|
|
268
|
+
sourceEsrVersion: config.firefox.version,
|
|
269
|
+
explicitSupersede: options.supersede === true,
|
|
270
|
+
});
|
|
271
|
+
outro('Dry run complete — no changes made');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
246
274
|
// Check how many existing patches would be superseded
|
|
247
275
|
const shouldProceed = await confirmSupersedePatches(paths.patches, filesAffected, options.supersede, isInteractive, s);
|
|
248
276
|
if (!shouldProceed)
|
|
@@ -299,7 +327,8 @@ export function registerExportAll(program, { getProjectRoot, withErrorHandling }
|
|
|
299
327
|
.option('--supersede', 'Allow superseding multiple existing patches')
|
|
300
328
|
.option('--skip-lint', 'Skip patch lint checks (downgrade errors to warnings)')
|
|
301
329
|
.option('--exclude-furnace', 'Export the non-Furnace subset of the aggregate diff instead of refusing when Furnace-managed files are modified. Furnace-managed files are still deployed by "fireforge furnace apply"; this flag only changes whether export-all aborts or filters in their presence.')
|
|
302
|
-
.option('--allow-overlap', 'Acknowledge cross-patch ownership overlap with non-superseded patches (the resulting queue fails verify)')
|
|
330
|
+
.option('--allow-overlap', 'Acknowledge cross-patch ownership overlap with non-superseded patches (the resulting queue fails verify). Does not bypass the new-file creation guard — two patches creating the same path is structurally unrecoverable, so that case still refuses regardless of this flag.')
|
|
331
|
+
.option('--dry-run', 'Print the export-all plan (filename, metadata, files affected, supersede preview) without writing anything to patches/. Lint still runs so the operator sees the same lint output a real run would produce.')
|
|
303
332
|
.action(withErrorHandling(async (options) => {
|
|
304
333
|
const { category, ...rest } = options;
|
|
305
334
|
await exportAllCommand(getProjectRoot(), {
|
|
@@ -82,6 +82,10 @@ export interface DryRunPreviewInput {
|
|
|
82
82
|
filesAffected: string[];
|
|
83
83
|
sourceEsrVersion: string;
|
|
84
84
|
explicitSupersede: boolean;
|
|
85
|
+
/** Optional `PatchMetadata.tier` opt-in carried from the CLI. */
|
|
86
|
+
tier?: 'branding';
|
|
87
|
+
/** Optional `PatchMetadata.lintIgnore` carried from the CLI. */
|
|
88
|
+
lintIgnore?: string[];
|
|
85
89
|
}
|
|
86
90
|
/**
|
|
87
91
|
* Renders the plain (non-placement) dry-run preview: calls planExport,
|
|
@@ -314,12 +314,20 @@ export async function renderDryRunPreview(input) {
|
|
|
314
314
|
description: input.description,
|
|
315
315
|
filesAffected: input.filesAffected,
|
|
316
316
|
sourceEsrVersion: input.sourceEsrVersion,
|
|
317
|
+
...(input.tier !== undefined ? { tier: input.tier } : {}),
|
|
318
|
+
...(input.lintIgnore !== undefined ? { lintIgnore: input.lintIgnore } : {}),
|
|
317
319
|
});
|
|
318
320
|
info(`\n[dry-run] Would write: patches/${plan.patchFilename}`);
|
|
319
321
|
info(` category: ${plan.metadata.category}`);
|
|
320
322
|
info(` order: ${plan.metadata.order}`);
|
|
321
323
|
info(` description: ${plan.metadata.description || '(none)'}`);
|
|
322
324
|
info(` filesAffected (${plan.metadata.filesAffected.length}): ${plan.metadata.filesAffected.join(', ')}`);
|
|
325
|
+
if (plan.metadata.tier !== undefined) {
|
|
326
|
+
info(` tier: ${plan.metadata.tier}`);
|
|
327
|
+
}
|
|
328
|
+
if (plan.metadata.lintIgnore !== undefined && plan.metadata.lintIgnore.length > 0) {
|
|
329
|
+
info(` lintIgnore: ${plan.metadata.lintIgnore.join(', ')}`);
|
|
330
|
+
}
|
|
323
331
|
if (supersedeDetails.length > 0) {
|
|
324
332
|
info(`\n[dry-run] Would supersede ${supersedeDetails.length} existing patch(es):`);
|
|
325
333
|
for (const detail of supersedeDetails) {
|
|
@@ -172,7 +172,15 @@ export async function exportCommand(projectRoot, files, options) {
|
|
|
172
172
|
try {
|
|
173
173
|
// Extract affected files from diff
|
|
174
174
|
const filesAffected = extractAffectedFiles(diff);
|
|
175
|
-
|
|
175
|
+
// Apply the just-set --tier and --lint-ignore on the lint pass so the
|
|
176
|
+
// operator's intent takes effect on this invocation, not only on the
|
|
177
|
+
// next one. Without this, a fresh export with `--tier branding` would
|
|
178
|
+
// still hit general thresholds because the lint runs before the
|
|
179
|
+
// metadata is committed.
|
|
180
|
+
const exportIgnoreChecks = options.lintIgnore && options.lintIgnore.length > 0
|
|
181
|
+
? new Set(options.lintIgnore)
|
|
182
|
+
: undefined;
|
|
183
|
+
await runPatchLint(paths.engine, filesAffected, diff, config, options.skipLint, undefined, exportIgnoreChecks, options.tier);
|
|
176
184
|
// Resolve placement (if any flag was given). Placement is mutually
|
|
177
185
|
// exclusive with supersede — the semantics overlap confusingly.
|
|
178
186
|
let placementPlan = null;
|
|
@@ -225,6 +233,10 @@ export async function exportCommand(projectRoot, files, options) {
|
|
|
225
233
|
filesAffected,
|
|
226
234
|
sourceEsrVersion: config.firefox.version,
|
|
227
235
|
explicitSupersede: options.supersede === true,
|
|
236
|
+
...(options.tier !== undefined ? { tier: options.tier } : {}),
|
|
237
|
+
...(options.lintIgnore !== undefined && options.lintIgnore.length > 0
|
|
238
|
+
? { lintIgnore: options.lintIgnore }
|
|
239
|
+
: {}),
|
|
228
240
|
});
|
|
229
241
|
outro('Dry run complete — no changes made');
|
|
230
242
|
return;
|
|
@@ -243,6 +255,10 @@ export async function exportCommand(projectRoot, files, options) {
|
|
|
243
255
|
createdAt: new Date().toISOString(),
|
|
244
256
|
sourceEsrVersion: config.firefox.version,
|
|
245
257
|
filesAffected,
|
|
258
|
+
...(options.tier !== undefined ? { tier: options.tier } : {}),
|
|
259
|
+
...(options.lintIgnore !== undefined && options.lintIgnore.length > 0
|
|
260
|
+
? { lintIgnore: options.lintIgnore }
|
|
261
|
+
: {}),
|
|
246
262
|
};
|
|
247
263
|
const committedPlan = await commitPlacementExport({
|
|
248
264
|
patchesDir: paths.patches,
|
|
@@ -314,6 +330,10 @@ export async function exportCommand(projectRoot, files, options) {
|
|
|
314
330
|
diff,
|
|
315
331
|
filesAffected,
|
|
316
332
|
sourceEsrVersion: config.firefox.version,
|
|
333
|
+
...(options.tier !== undefined ? { tier: options.tier } : {}),
|
|
334
|
+
...(options.lintIgnore !== undefined && options.lintIgnore.length > 0
|
|
335
|
+
? { lintIgnore: options.lintIgnore }
|
|
336
|
+
: {}),
|
|
317
337
|
});
|
|
318
338
|
for (const oldPatch of superseded) {
|
|
319
339
|
info(`Superseded: ${oldPatch.filename}`);
|
|
@@ -348,11 +368,15 @@ export function registerExport(program, { getProjectRoot, withErrorHandling }) {
|
|
|
348
368
|
.option('--force-unsafe', 'Bypass cross-patch lint refusal on projected placement')
|
|
349
369
|
.option('--exclude-furnace', 'Exclude furnace-managed file paths from the export')
|
|
350
370
|
.option('--allow-overlap', 'Acknowledge cross-patch ownership overlap (default mode only; the resulting queue fails verify)')
|
|
371
|
+
.addOption(new Option('--tier <tier>', 'Force a tier override on the new patch (only "branding" recognised)').choices(['branding']))
|
|
372
|
+
.option('--lint-ignore <check-id>', 'Suppress a lint check on this patch (writes to PatchMetadata.lintIgnore; repeatable)', (value, prev) => [...prev, value], [])
|
|
351
373
|
.action(withErrorHandling(async (paths, options) => {
|
|
352
|
-
const { category, ...rest } = options;
|
|
374
|
+
const { category, tier, lintIgnore, ...rest } = options;
|
|
353
375
|
await exportCommand(getProjectRoot(), paths, {
|
|
354
376
|
...pickDefined(rest),
|
|
355
377
|
...(category !== undefined ? { category: category } : {}),
|
|
378
|
+
...(tier !== undefined ? { tier: tier } : {}),
|
|
379
|
+
...(lintIgnore !== undefined && lintIgnore.length > 0 ? { lintIgnore } : {}),
|
|
356
380
|
});
|
|
357
381
|
}));
|
|
358
382
|
}
|
|
@@ -212,6 +212,64 @@ async function cleanupCustomTestFiles(name, projectRoot, journal) {
|
|
|
212
212
|
}
|
|
213
213
|
return { partialFailures };
|
|
214
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Removes the MochiKit test scaffold a `furnace create --with-tests
|
|
217
|
+
* --test-style mochikit` produced for the component (matches the rename
|
|
218
|
+
* counterpart in `rename.ts`). The test file is `test_<name>.html` under
|
|
219
|
+
* `engine/toolkit/content/tests/widgets/` and the registration is the
|
|
220
|
+
* `["test_<name>.html"]` entry in the same directory's `chrome.toml`.
|
|
221
|
+
*
|
|
222
|
+
* 2026-04-25 eval Finding 13: the prior cleanup only handled the
|
|
223
|
+
* browser-chrome mochitest layout under `browser/base/content/test/
|
|
224
|
+
* <binary>/`, which left mochikit-style scaffolds and their toml entries
|
|
225
|
+
* orphaned after `furnace remove`. The post-rename name passed in here
|
|
226
|
+
* is the canonical one written to disk by deploy/rename, so the file
|
|
227
|
+
* basenames match without needing to re-derive from the old name.
|
|
228
|
+
*
|
|
229
|
+
* Best-effort: each step warns on failure rather than throwing so the
|
|
230
|
+
* rest of the remove transaction proceeds. The journal still snapshots
|
|
231
|
+
* touched files so the outer rollback can restore them on a later
|
|
232
|
+
* failure in the same operation.
|
|
233
|
+
*/
|
|
234
|
+
async function cleanupCustomMochikitTestFiles(name, projectRoot, journal) {
|
|
235
|
+
const partialFailures = [];
|
|
236
|
+
const paths = getProjectPaths(projectRoot);
|
|
237
|
+
const widgetsTestDir = join(paths.engine, 'toolkit/content/tests/widgets');
|
|
238
|
+
if (!(await pathExists(widgetsTestDir))) {
|
|
239
|
+
return { partialFailures };
|
|
240
|
+
}
|
|
241
|
+
const testFileName = `test_${name}.html`;
|
|
242
|
+
const testFilePath = join(widgetsTestDir, testFileName);
|
|
243
|
+
try {
|
|
244
|
+
if (await pathExists(testFilePath)) {
|
|
245
|
+
await snapshotFile(journal, testFilePath);
|
|
246
|
+
await unlink(testFilePath);
|
|
247
|
+
info(`Deleted mochikit test file: toolkit/content/tests/widgets/${testFileName}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
const msg = `Could not delete mochikit test file ${testFileName} — ${toError(error).message}. Remove it manually if needed.`;
|
|
252
|
+
warn(msg);
|
|
253
|
+
partialFailures.push(msg);
|
|
254
|
+
}
|
|
255
|
+
const chromeTomlPath = join(widgetsTestDir, 'chrome.toml');
|
|
256
|
+
try {
|
|
257
|
+
if (await pathExists(chromeTomlPath)) {
|
|
258
|
+
const toml = await readText(chromeTomlPath);
|
|
259
|
+
const headerLine = `["${testFileName}"]`;
|
|
260
|
+
if (toml.includes(headerLine)) {
|
|
261
|
+
await snapshotFile(journal, chromeTomlPath);
|
|
262
|
+
await writeText(chromeTomlPath, removeTomlSection(toml, testFileName));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
const msg = `Could not update widgets chrome.toml — ${toError(error).message}. Remove the test entry manually if needed.`;
|
|
268
|
+
warn(msg);
|
|
269
|
+
partialFailures.push(msg);
|
|
270
|
+
}
|
|
271
|
+
return { partialFailures };
|
|
272
|
+
}
|
|
215
273
|
/**
|
|
216
274
|
* Removes generated xpcshell test scaffolds associated with a custom
|
|
217
275
|
* component. 2026-04-24 eval Finding 5: `furnace remove` handled
|
|
@@ -433,6 +491,16 @@ export async function furnaceRemoveCommand(projectRoot, name, options = {}) {
|
|
|
433
491
|
// versions.
|
|
434
492
|
const xpcshellResult = await cleanupCustomXpcshellTestFiles(name, projectRoot, journal);
|
|
435
493
|
testCleanupFailures.push(...xpcshellResult.partialFailures);
|
|
494
|
+
// 2026-04-25 eval Finding 13: mochikit-style scaffolds
|
|
495
|
+
// (`--test-style mochikit`) live under
|
|
496
|
+
// `engine/toolkit/content/tests/widgets/` with `chrome.toml`
|
|
497
|
+
// entries — neither the browser-chrome path nor the xpcshell
|
|
498
|
+
// path touches them. Without this pass, a `furnace create
|
|
499
|
+
// --with-tests --test-style mochikit` followed by `furnace
|
|
500
|
+
// remove` left the test file and its toml entry referencing a
|
|
501
|
+
// component that no longer exists.
|
|
502
|
+
const mochikitResult = await cleanupCustomMochikitTestFiles(name, projectRoot, journal);
|
|
503
|
+
testCleanupFailures.push(...mochikitResult.partialFailures);
|
|
436
504
|
}
|
|
437
505
|
// Remove entry from furnace.json
|
|
438
506
|
if (type === 'stock') {
|