@hominis/fireforge 0.44.0 → 0.44.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 +29 -2
- package/README.md +2 -11
- package/dist/build-info.json +4 -4
- package/dist/src/commands/doctor-check-core.d.ts +9 -0
- package/dist/src/commands/doctor-patch-manifest.d.ts +8 -0
- package/dist/src/commands/doctor-patch-manifest.js +155 -0
- package/dist/src/commands/doctor.d.ts +1 -1
- package/dist/src/commands/doctor.js +48 -62
- package/dist/src/commands/furnace/diff.js +6 -1
- package/dist/src/commands/furnace/remove-mutations.js +5 -1
- package/dist/src/commands/import.js +6 -2
- package/dist/src/core/build-audit-resolve.js +12 -3
- package/dist/src/core/config-mutate.js +20 -19
- package/dist/src/core/furnace-apply-ftl.js +4 -1
- package/dist/src/core/furnace-apply-helpers.js +11 -5
- package/dist/src/core/git-base.d.ts +14 -0
- package/dist/src/core/git-base.js +17 -0
- package/dist/src/core/git.js +3 -1
- package/dist/src/core/patch-lint-checkjs.js +25 -4
- package/dist/src/core/patch-manifest-consistency.d.ts +56 -6
- package/dist/src/core/patch-manifest-consistency.js +96 -23
- package/dist/src/core/patch-manifest-files-affected.d.ts +43 -0
- package/dist/src/core/patch-manifest-files-affected.js +77 -0
- package/dist/src/core/patch-manifest.d.ts +2 -1
- package/dist/src/core/patch-manifest.js +2 -1
- package/dist/src/core/token-category.js +61 -7
- package/dist/src/core/token-dark-mode.js +20 -2
- package/dist/src/core/token-docs.js +1 -1
- package/dist/src/core/token-variant.js +24 -2
- package/dist/src/core/tree-store.js +5 -1
- package/dist/src/core/typecheck.js +7 -1
- package/dist/src/types/commands/options.d.ts +23 -0
- package/package.json +2 -2
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Narrow repair for `filesAffected` — the one manifest field a `.patch` body
|
|
4
|
+
* can actually yield.
|
|
5
|
+
*
|
|
6
|
+
* A `files-affected-mismatch` is pure derived-value drift: the manifest's list
|
|
7
|
+
* disagrees with the diff it describes, and the diff is right by definition.
|
|
8
|
+
* Rebuilding the whole manifest to fix it is a much larger operation than the
|
|
9
|
+
* failure calls for, so this repair recomputes one field on the drifted rows
|
|
10
|
+
* and leaves every other row byte-identical.
|
|
11
|
+
*/
|
|
12
|
+
import { GeneralError } from '../errors/base.js';
|
|
13
|
+
import { discoverPatches, getAllTargetFilesFromPatch } from './patch-files.js';
|
|
14
|
+
import { withPatchDirectoryLock } from './patch-lock.js';
|
|
15
|
+
import { normalizeAffectedFiles } from './patch-manifest-consistency.js';
|
|
16
|
+
import { loadPatchesManifestForWrite, mutatePatchRowsInManifest, PATCHES_MANIFEST, } from './patch-manifest-io.js';
|
|
17
|
+
/**
|
|
18
|
+
* Recomputes `filesAffected` from the patch body for the named patches.
|
|
19
|
+
*
|
|
20
|
+
* @param patchesDir - Path to the patches directory
|
|
21
|
+
* @param filenames - Patch filenames to repair
|
|
22
|
+
* @param options - Lock options plus `dryRun`
|
|
23
|
+
* @returns The corrections applied (or projected) and the filenames skipped
|
|
24
|
+
*/
|
|
25
|
+
export async function repairPatchesFilesAffected(patchesDir, filenames, options = {}) {
|
|
26
|
+
// Same lock contract as the full rebuild (invariant 2,
|
|
27
|
+
// docs/lifecycle-invariants.md): a manifest write serializes on the
|
|
28
|
+
// patch-directory lock, and a dry run reads under it so its projection
|
|
29
|
+
// cannot be computed against a queue being rewritten underneath.
|
|
30
|
+
return withPatchDirectoryLock(patchesDir, () => repairPatchesFilesAffectedUnderLock(patchesDir, filenames, options), options);
|
|
31
|
+
}
|
|
32
|
+
async function repairPatchesFilesAffectedUnderLock(patchesDir, filenames, options) {
|
|
33
|
+
// ForWrite: a corrupt manifest must abort here rather than be treated as
|
|
34
|
+
// empty. This repair exists precisely so that a one-field drift never
|
|
35
|
+
// reaches a whole-manifest rewrite, and silently writing over unparseable
|
|
36
|
+
// metadata would be a larger version of what it avoids.
|
|
37
|
+
const manifest = await loadPatchesManifestForWrite(patchesDir);
|
|
38
|
+
if (!manifest) {
|
|
39
|
+
throw new GeneralError(`Cannot repair filesAffected: ${PATCHES_MANIFEST} does not exist in ${patchesDir}.`);
|
|
40
|
+
}
|
|
41
|
+
const patchPathByFilename = new Map((await discoverPatches(patchesDir)).map((patch) => [patch.filename, patch.path]));
|
|
42
|
+
const rowByFilename = new Map(manifest.patches.map((entry) => [entry.filename, entry]));
|
|
43
|
+
const repairs = [];
|
|
44
|
+
const skippedFilenames = [];
|
|
45
|
+
for (const filename of new Set(filenames)) {
|
|
46
|
+
const patchPath = patchPathByFilename.get(filename);
|
|
47
|
+
const row = rowByFilename.get(filename);
|
|
48
|
+
if (patchPath === undefined || !row) {
|
|
49
|
+
// A missing patch file or a missing row is a different consistency
|
|
50
|
+
// issue with a different remedy; this repair reports it rather than
|
|
51
|
+
// inventing a row or deleting one.
|
|
52
|
+
skippedFilenames.push(filename);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const before = normalizeAffectedFiles(row.filesAffected);
|
|
56
|
+
const after = normalizeAffectedFiles(await getAllTargetFilesFromPatch(patchPath));
|
|
57
|
+
if (sameFiles(before, after))
|
|
58
|
+
continue;
|
|
59
|
+
repairs.push({ filename, before, after });
|
|
60
|
+
}
|
|
61
|
+
skippedFilenames.sort((left, right) => left.localeCompare(right));
|
|
62
|
+
if (repairs.length === 0 || options.dryRun === true) {
|
|
63
|
+
return { repairs, skippedFilenames, written: false };
|
|
64
|
+
}
|
|
65
|
+
const correctedByFilename = new Map(repairs.map((repair) => [repair.filename, repair.after]));
|
|
66
|
+
// Row-scoped mutation, so every untouched entry keeps its exact JSON shape —
|
|
67
|
+
// no re-sorted `filesAffected` lists and no reader-only fallback fields
|
|
68
|
+
// serialized into rows this repair has no business touching.
|
|
69
|
+
await mutatePatchRowsInManifest(patchesDir, [...correctedByFilename.keys()], (existing) => {
|
|
70
|
+
const corrected = correctedByFilename.get(existing.filename);
|
|
71
|
+
return corrected ? { set: { filesAffected: corrected } } : null;
|
|
72
|
+
});
|
|
73
|
+
return { repairs, skippedFilenames, written: true };
|
|
74
|
+
}
|
|
75
|
+
function sameFiles(left, right) {
|
|
76
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
77
|
+
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Callers should continue to import from this module; the internal split
|
|
5
5
|
* is an implementation detail.
|
|
6
6
|
*/
|
|
7
|
-
export { rebuildPatchesManifest, validatePatchesManifestConsistency, } from './patch-manifest-consistency.js';
|
|
7
|
+
export { type PatchManifestConsistencyIssue, rebuildPatchesManifest, recommendManifestRepair, validatePatchesManifestConsistency, } from './patch-manifest-consistency.js';
|
|
8
|
+
export { type FilesAffectedRepair, repairPatchesFilesAffected, } from './patch-manifest-files-affected.js';
|
|
8
9
|
export { addPatchToManifest, loadPatchesManifest, loadPatchesManifestForWrite, mutatePatchRowsInManifest, PATCHES_MANIFEST, type PatchRenameEntry, removePatchFileAndManifest, renumberPatchesInManifest, rewriteStagedDependencyOwners, savePatchesManifest, } from './patch-manifest-io.js';
|
|
9
10
|
export { checkVersionCompatibility, findPatchesAffectingFile, getClaimedFiles, stampPatchVersions, validatePatchIntegrity, } from './patch-manifest-query.js';
|
|
10
11
|
export { resolvePatchIdentifier } from './patch-manifest-resolve.js';
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* Callers should continue to import from this module; the internal split
|
|
6
6
|
* is an implementation detail.
|
|
7
7
|
*/
|
|
8
|
-
export { rebuildPatchesManifest, validatePatchesManifestConsistency, } from './patch-manifest-consistency.js';
|
|
8
|
+
export { rebuildPatchesManifest, recommendManifestRepair, validatePatchesManifestConsistency, } from './patch-manifest-consistency.js';
|
|
9
|
+
export { repairPatchesFilesAffected, } from './patch-manifest-files-affected.js';
|
|
9
10
|
export { addPatchToManifest, loadPatchesManifest, loadPatchesManifestForWrite, mutatePatchRowsInManifest, PATCHES_MANIFEST, removePatchFileAndManifest, renumberPatchesInManifest, rewriteStagedDependencyOwners, savePatchesManifest, } from './patch-manifest-io.js';
|
|
10
11
|
export { checkVersionCompatibility, findPatchesAffectingFile, getClaimedFiles, stampPatchVersions, validatePatchIntegrity, } from './patch-manifest-query.js';
|
|
11
12
|
export { resolvePatchIdentifier } from './patch-manifest-resolve.js';
|
|
@@ -19,6 +19,63 @@ function singleLineBannerPattern(category) {
|
|
|
19
19
|
function multiLineBlockNameMatches(blockLine, category) {
|
|
20
20
|
return blockLine.replace(/^\s*\*\s*/, '').trim() === category;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Body of the first `/* … *\/` comment on `line`, or `null` when the line
|
|
24
|
+
* carries no closed block comment.
|
|
25
|
+
*
|
|
26
|
+
* Deliberately index arithmetic rather than a regex: every regex spelling of
|
|
27
|
+
* "banner comment" this module used to carry was super-linear on a line that
|
|
28
|
+
* repeats the opening shape (CodeQL `js/polynomial-redos`), and `tokens.css`
|
|
29
|
+
* is a file FireForge reads out of a consumer's engine tree, so a
|
|
30
|
+
* pathological line needs no attacker to arrive.
|
|
31
|
+
*/
|
|
32
|
+
function blockCommentBody(line) {
|
|
33
|
+
const open = line.indexOf('/*');
|
|
34
|
+
if (open === -1)
|
|
35
|
+
return null;
|
|
36
|
+
const close = line.indexOf('*/', open + 2);
|
|
37
|
+
if (close === -1)
|
|
38
|
+
return null;
|
|
39
|
+
return line.slice(open + 2, close);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* True when `line` carries a single-line banner comment — a closed block
|
|
43
|
+
* comment whose body both opens and closes with `=`. Recognises the decorative
|
|
44
|
+
* all-`=` rule as well as a named `= Foo =` banner, which is what the section
|
|
45
|
+
* scan wants: either shape ends the preceding section.
|
|
46
|
+
*/
|
|
47
|
+
function isSingleLineBannerLine(line) {
|
|
48
|
+
const body = blockCommentBody(line)?.trim();
|
|
49
|
+
return body !== undefined && body.length >= 2 && body.startsWith('=') && body.endsWith('=');
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Name declared by a single-line banner (`/* = Foo = *\/` → `Foo`), or `null`
|
|
53
|
+
* when the line is not a named banner. The `=` runs on both sides are
|
|
54
|
+
* required; a decorative all-`=` rule carries no name and yields `null`.
|
|
55
|
+
*/
|
|
56
|
+
function singleLineBannerName(line) {
|
|
57
|
+
const body = blockCommentBody(line);
|
|
58
|
+
if (body === null)
|
|
59
|
+
return null;
|
|
60
|
+
let start = 0;
|
|
61
|
+
let end = body.length;
|
|
62
|
+
while (start < end && /\s/.test(body[start] ?? ''))
|
|
63
|
+
start++;
|
|
64
|
+
const leadingEqStart = start;
|
|
65
|
+
while (start < end && body[start] === '=')
|
|
66
|
+
start++;
|
|
67
|
+
if (start === leadingEqStart)
|
|
68
|
+
return null;
|
|
69
|
+
while (end > start && /\s/.test(body[end - 1] ?? ''))
|
|
70
|
+
end--;
|
|
71
|
+
const trailingEqEnd = end;
|
|
72
|
+
while (end > start && body[end - 1] === '=')
|
|
73
|
+
end--;
|
|
74
|
+
if (end === trailingEqEnd)
|
|
75
|
+
return null;
|
|
76
|
+
const name = body.slice(start, end).trim();
|
|
77
|
+
return name.length > 0 ? name : null;
|
|
78
|
+
}
|
|
22
79
|
/**
|
|
23
80
|
* True when `lines` contain a category header (single-line or multi-line
|
|
24
81
|
* banner shape) whose name EQUALS `category`. Shared by the pre-add
|
|
@@ -60,14 +117,11 @@ export function categoryHeaderExists(lines, category) {
|
|
|
60
117
|
*/
|
|
61
118
|
function discoverCategoryHeaders(lines) {
|
|
62
119
|
const categories = new Set();
|
|
63
|
-
const singleLinePattern = /\/\*\s*=+\s*(.+?)\s*=+\s*\*\//;
|
|
64
120
|
for (let i = 0; i < lines.length; i++) {
|
|
65
121
|
const line = lines[i] ?? '';
|
|
66
|
-
const
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
if (extracted.length > 0)
|
|
70
|
-
categories.add(extracted);
|
|
122
|
+
const extracted = singleLineBannerName(line);
|
|
123
|
+
if (extracted !== null) {
|
|
124
|
+
categories.add(extracted);
|
|
71
125
|
continue;
|
|
72
126
|
}
|
|
73
127
|
if (/^\s*\/\*\s*=+/.test(line) && !/\*\//.test(line)) {
|
|
@@ -181,7 +235,7 @@ export function findCategorySection(lines, category, tokensCssPath) {
|
|
|
181
235
|
let sectionEnd = lines.length;
|
|
182
236
|
for (let i = scanStart; i < lines.length; i++) {
|
|
183
237
|
const line = lines[i] ?? '';
|
|
184
|
-
if (
|
|
238
|
+
if (isSingleLineBannerLine(line) ||
|
|
185
239
|
(/^\s*\/\*\s*=+/.test(line) && !/\*\//.test(line)) ||
|
|
186
240
|
/^\s*\}/.test(line)) {
|
|
187
241
|
sectionEnd = i;
|
|
@@ -12,6 +12,24 @@
|
|
|
12
12
|
* fallback helper returns the outer `@media` block's close so the caller can
|
|
13
13
|
* materialise a fresh `:root` wrapper rather than dropping the dark value.
|
|
14
14
|
*/
|
|
15
|
+
/**
|
|
16
|
+
* True when `line` carries a `:root` selector whose opening brace is on the
|
|
17
|
+
* same line, with no intervening brace of either kind.
|
|
18
|
+
*
|
|
19
|
+
* Index arithmetic rather than `/:root[^{}]*\{/`: that pattern restarts at
|
|
20
|
+
* every `:root` in the line, which is quadratic on a line repeating the
|
|
21
|
+
* selector (CodeQL `js/polynomial-redos`). `tokens.css` comes from a
|
|
22
|
+
* consumer's engine tree, so such a line needs no attacker to arrive.
|
|
23
|
+
*/
|
|
24
|
+
function rootOpensBraceOnLine(line) {
|
|
25
|
+
const brace = line.indexOf('{');
|
|
26
|
+
if (brace === -1)
|
|
27
|
+
return false;
|
|
28
|
+
const root = line.lastIndexOf(':root', brace - ':root'.length);
|
|
29
|
+
if (root === -1 || root + ':root'.length > brace)
|
|
30
|
+
return false;
|
|
31
|
+
return !line.slice(root + ':root'.length, brace).includes('}');
|
|
32
|
+
}
|
|
15
33
|
/**
|
|
16
34
|
* Strips the content of `/* ... *\/` block comments from an array of
|
|
17
35
|
* CSS source lines while preserving each line's length. Indexed scans
|
|
@@ -85,9 +103,9 @@ export function findDarkRootInsertionIndex(lines) {
|
|
|
85
103
|
let rootOpenLine = -1;
|
|
86
104
|
for (let i = darkMediaLine; i < stripped.length; i++) {
|
|
87
105
|
const line = stripped[i] ?? '';
|
|
88
|
-
if (/(
|
|
106
|
+
if (/(?:^|[\s,{]):root\b/.test(line)) {
|
|
89
107
|
// Brace on the same line?
|
|
90
|
-
if (
|
|
108
|
+
if (rootOpensBraceOnLine(line)) {
|
|
91
109
|
rootOpenLine = i;
|
|
92
110
|
break;
|
|
93
111
|
}
|
|
@@ -44,7 +44,7 @@ export async function addTokenToDocs(engineDir, options, annotation) {
|
|
|
44
44
|
let unmappedAdded = false;
|
|
45
45
|
let countUpdated = false;
|
|
46
46
|
const isLiteral = !options.value.startsWith('var(');
|
|
47
|
-
const mapsTo = isLiteral ? '—' : options.value.replace(
|
|
47
|
+
const mapsTo = isLiteral ? '—' : options.value.replace(/^var\(([^)]+)\)/, '$1');
|
|
48
48
|
const tokenCell = `\`${options.tokenName}\``;
|
|
49
49
|
const valueCell = `\`${options.value}\``;
|
|
50
50
|
// --- Main token table: Category | Token | Value | Maps to | Mode ---
|
|
@@ -52,6 +52,28 @@ export function validateVariantSelector(raw) {
|
|
|
52
52
|
function canonicalSelector(selector) {
|
|
53
53
|
return selector.replace(/\s+/g, '').replace(/["']/g, '');
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* The `:root[…]` selector fragment on `line`, spanning to the LAST `]` that
|
|
57
|
+
* precedes the block's opening brace, or `null` when the line carries none.
|
|
58
|
+
*
|
|
59
|
+
* Index arithmetic rather than `/:root\[[^{]*\]/`: `[^{]` also matches `]`,
|
|
60
|
+
* so that pattern backtracks quadratically on a line repeating `:root[`
|
|
61
|
+
* (CodeQL `js/polynomial-redos`). Taking the last `]` — not the first —
|
|
62
|
+
* preserves the greedy regex's reach, so a multi-attribute selector still
|
|
63
|
+
* compares as the whole fragment and does not collide with a single-attribute
|
|
64
|
+
* variant that happens to be its prefix.
|
|
65
|
+
*/
|
|
66
|
+
function rootAttributeSelector(line) {
|
|
67
|
+
const start = line.indexOf(':root[');
|
|
68
|
+
if (start === -1)
|
|
69
|
+
return null;
|
|
70
|
+
const brace = line.indexOf('{', start);
|
|
71
|
+
const limit = brace === -1 ? line.length : brace;
|
|
72
|
+
const close = line.lastIndexOf(']', limit - 1);
|
|
73
|
+
if (close <= start + ':root['.length - 1)
|
|
74
|
+
return null;
|
|
75
|
+
return line.slice(start, close + 1);
|
|
76
|
+
}
|
|
55
77
|
/**
|
|
56
78
|
* Finds the top-level `:root<variant>` block whose attribute selector
|
|
57
79
|
* matches `variant` (compared canonically). Returns the opening-brace and
|
|
@@ -66,8 +88,8 @@ function findVariantBlock(lines, variant) {
|
|
|
66
88
|
const want = canonicalSelector(`:root${variant}`);
|
|
67
89
|
for (let i = 0; i < stripped.length; i++) {
|
|
68
90
|
const line = stripped[i] ?? '';
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
91
|
+
const selector = rootAttributeSelector(line);
|
|
92
|
+
if (selector === null || canonicalSelector(selector) !== want)
|
|
71
93
|
continue;
|
|
72
94
|
let openLine = /\{/.test(line) ? i : -1;
|
|
73
95
|
if (openLine === -1) {
|
|
@@ -443,7 +443,11 @@ async function inspectTreeLock(lockPath) {
|
|
|
443
443
|
*/
|
|
444
444
|
export async function removeTree(primaryRoot, name, options = {}) {
|
|
445
445
|
assertValidTreeName(name);
|
|
446
|
-
|
|
446
|
+
// Both sides resolved: the containment prefix has to be in the same form as
|
|
447
|
+
// the path it guards. `getTreesDir` is a `join`, so on Windows it yields a
|
|
448
|
+
// drive-less path while `resolve` below adds the drive letter — the prefix
|
|
449
|
+
// test then failed for EVERY tree and refused each removal as an escape.
|
|
450
|
+
const treesDir = resolve(getTreesDir(primaryRoot));
|
|
447
451
|
const treeRoot = resolve(treesDir, name);
|
|
448
452
|
if (!treeRoot.startsWith(treesDir + sep) || basename(treeRoot) !== name) {
|
|
449
453
|
throw new GeneralError(`Refusing to remove "${name}": resolved path escapes the trees directory.`);
|
|
@@ -22,6 +22,7 @@ import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
|
22
22
|
import { toError } from '../utils/errors.js';
|
|
23
23
|
import { pathExists } from '../utils/fs.js';
|
|
24
24
|
import { verbose } from '../utils/logger.js';
|
|
25
|
+
import { normalizePathSlashes } from '../utils/paths.js';
|
|
25
26
|
import { composeShimSource, SHIM_FILENAME, SUPPRESSED_DIAGNOSTIC_CODES, UNDEFINED_IDENTIFIER_CODES, UNDEFINED_IDENTIFIER_HINT, } from './typecheck-shim.js';
|
|
26
27
|
/**
|
|
27
28
|
* Sentinel string surfaced as a `TypecheckIssue.message` when the
|
|
@@ -239,7 +240,12 @@ async function runTypecheckForProject(ts, projectRoot, projectPath, shimSource,
|
|
|
239
240
|
// file — and never write it to disk. The CompilerHost below serves
|
|
240
241
|
// it from `shimSource` for `fileExists`/`readFile`/`getSourceFile`.
|
|
241
242
|
const projectDir = dirname(absConfig);
|
|
242
|
-
|
|
243
|
+
// Forward slashes deliberately: TypeScript normalizes every `fileName` it
|
|
244
|
+
// hands a CompilerHost, on every platform. A shim path carrying Windows
|
|
245
|
+
// separators never matches those callbacks, so the shim is not served (its
|
|
246
|
+
// globals report as undefined identifiers) and its own diagnostics are not
|
|
247
|
+
// filtered out below — the pass reports failures that do not exist.
|
|
248
|
+
const shimPath = normalizePathSlashes(resolve(projectDir, `.fireforge-${SHIM_FILENAME}`));
|
|
243
249
|
const rootFiles = [...parsed.fileNames, shimPath];
|
|
244
250
|
const defaultHost = ts.createCompilerHost(options);
|
|
245
251
|
const host = {
|
|
@@ -987,7 +987,30 @@ export interface DoctorOptions {
|
|
|
987
987
|
* the patch manifest under that lock.
|
|
988
988
|
*/
|
|
989
989
|
waitLock?: number | boolean;
|
|
990
|
+
/**
|
|
991
|
+
* Rebuild patches.json from the on-disk patch files, preserving every
|
|
992
|
+
* field the patch bodies cannot express. Refused when patches.json exists
|
|
993
|
+
* but cannot be parsed, unless `allowMetadataLoss` is also set.
|
|
994
|
+
*/
|
|
990
995
|
repairPatchesManifest?: boolean;
|
|
996
|
+
/**
|
|
997
|
+
* Repair only the `filesAffected` lists that disagree with their patch
|
|
998
|
+
* body. Narrower than `repairPatchesManifest`: it rewrites one field on the
|
|
999
|
+
* drifted rows and leaves every other row byte-identical, and it needs no
|
|
1000
|
+
* project config, so it works on a project whose fireforge.json is broken.
|
|
1001
|
+
*/
|
|
1002
|
+
repairFilesAffected?: boolean;
|
|
1003
|
+
/**
|
|
1004
|
+
* Accept the whole-manifest metadata reinvention that rebuilding an
|
|
1005
|
+
* unparseable patches.json implies. Only meaningful with
|
|
1006
|
+
* `repairPatchesManifest`.
|
|
1007
|
+
*/
|
|
1008
|
+
allowMetadataLoss?: boolean;
|
|
1009
|
+
/**
|
|
1010
|
+
* Project what a requested repair would change and write nothing. Refused
|
|
1011
|
+
* without a repair flag rather than accepted as a silent no-op.
|
|
1012
|
+
*/
|
|
1013
|
+
dryRun?: boolean;
|
|
991
1014
|
/**
|
|
992
1015
|
* Clear a stale `pendingResolution` marker, but only after the same
|
|
993
1016
|
* read-only queue health checks used by `fireforge verify` report no
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hominis/fireforge",
|
|
3
|
-
"version": "0.44.
|
|
3
|
+
"version": "0.44.1",
|
|
4
4
|
"description": "FireForge — a build tool for customizing Firefox",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"vitest": "^4.0.18"
|
|
84
84
|
},
|
|
85
85
|
"engines": {
|
|
86
|
-
"node": ">=22.22.
|
|
86
|
+
"node": ">=22.22.2"
|
|
87
87
|
},
|
|
88
88
|
"packageManager": "npm@11.12.1",
|
|
89
89
|
"license": "EUPL-1.2",
|