@hominis/fireforge 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +93 -1
- package/README.md +125 -238
- package/dist/bin/fireforge.js +26 -0
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +131 -52
- package/dist/src/commands/bootstrap.js +6 -2
- package/dist/src/commands/build.js +4 -2
- package/dist/src/commands/discard.js +16 -4
- package/dist/src/commands/doctor-furnace.d.ts +8 -0
- package/dist/src/commands/doctor-furnace.js +422 -0
- package/dist/src/commands/doctor.d.ts +115 -0
- package/dist/src/commands/doctor.js +327 -258
- package/dist/src/commands/download.js +16 -1
- package/dist/src/commands/export-all.js +15 -0
- package/dist/src/commands/export-flow.d.ts +91 -0
- package/dist/src/commands/export-flow.js +344 -0
- package/dist/src/commands/export.js +151 -5
- package/dist/src/commands/furnace/apply.d.ts +3 -2
- package/dist/src/commands/furnace/apply.js +169 -36
- package/dist/src/commands/furnace/create.js +162 -52
- package/dist/src/commands/furnace/deploy.js +156 -144
- package/dist/src/commands/furnace/diff.d.ts +8 -4
- package/dist/src/commands/furnace/diff.js +142 -73
- package/dist/src/commands/furnace/index.d.ts +6 -2
- package/dist/src/commands/furnace/index.js +76 -25
- package/dist/src/commands/furnace/init.d.ts +11 -0
- package/dist/src/commands/furnace/init.js +76 -0
- package/dist/src/commands/furnace/list.d.ts +4 -1
- package/dist/src/commands/furnace/list.js +35 -3
- package/dist/src/commands/furnace/override.d.ts +8 -0
- package/dist/src/commands/furnace/override.js +216 -26
- package/dist/src/commands/furnace/preview.js +184 -30
- package/dist/src/commands/furnace/refresh.d.ts +10 -0
- package/dist/src/commands/furnace/refresh.js +268 -0
- package/dist/src/commands/furnace/remove.js +285 -89
- package/dist/src/commands/furnace/rename.d.ts +5 -0
- package/dist/src/commands/furnace/rename.js +308 -0
- package/dist/src/commands/furnace/scan.d.ts +4 -1
- package/dist/src/commands/furnace/scan.js +72 -11
- package/dist/src/commands/furnace/status.js +85 -20
- package/dist/src/commands/furnace/sync.d.ts +12 -0
- package/dist/src/commands/furnace/sync.js +77 -0
- package/dist/src/commands/furnace/validate.d.ts +4 -1
- package/dist/src/commands/furnace/validate.js +99 -3
- package/dist/src/commands/furnace/validation-output.d.ts +24 -1
- package/dist/src/commands/furnace/validation-output.js +93 -1
- package/dist/src/commands/import.js +37 -4
- package/dist/src/commands/lint.js +11 -2
- package/dist/src/commands/manifest.d.ts +39 -0
- package/dist/src/commands/manifest.js +59 -0
- package/dist/src/commands/patch/delete.d.ts +28 -0
- package/dist/src/commands/patch/delete.js +209 -0
- package/dist/src/commands/patch/index.d.ts +17 -0
- package/dist/src/commands/patch/index.js +25 -0
- package/dist/src/commands/patch/reorder.d.ts +30 -0
- package/dist/src/commands/patch/reorder.js +377 -0
- package/dist/src/commands/re-export-files.d.ts +17 -0
- package/dist/src/commands/re-export-files.js +177 -0
- package/dist/src/commands/re-export.js +44 -0
- package/dist/src/commands/rebase/abort.d.ts +1 -1
- package/dist/src/commands/rebase/abort.js +12 -3
- package/dist/src/commands/rebase/confirm.d.ts +3 -3
- package/dist/src/commands/rebase/confirm.js +4 -4
- package/dist/src/commands/rebase/index.js +13 -4
- package/dist/src/commands/reset.js +20 -4
- package/dist/src/commands/run.js +46 -1
- package/dist/src/commands/setup-support.js +5 -5
- package/dist/src/commands/status.js +97 -6
- package/dist/src/commands/test.js +5 -37
- package/dist/src/commands/verify.d.ts +31 -0
- package/dist/src/commands/verify.js +126 -0
- package/dist/src/core/build-prepare.js +40 -16
- package/dist/src/core/destructive.d.ts +96 -0
- package/dist/src/core/destructive.js +137 -0
- package/dist/src/core/diff-hunks.d.ts +73 -0
- package/dist/src/core/diff-hunks.js +268 -0
- package/dist/src/core/firefox.d.ts +1 -1
- package/dist/src/core/firefox.js +1 -1
- package/dist/src/core/furnace-apply-helpers.d.ts +89 -6
- package/dist/src/core/furnace-apply-helpers.js +302 -57
- package/dist/src/core/furnace-apply-output.d.ts +16 -0
- package/dist/src/core/furnace-apply-output.js +57 -0
- package/dist/src/core/furnace-apply.d.ts +21 -3
- package/dist/src/core/furnace-apply.js +260 -29
- package/dist/src/core/furnace-checksum-utils.d.ts +4 -0
- package/dist/src/core/furnace-checksum-utils.js +24 -0
- package/dist/src/core/furnace-config.d.ts +28 -1
- package/dist/src/core/furnace-config.js +180 -17
- package/dist/src/core/furnace-constants.d.ts +22 -0
- package/dist/src/core/furnace-constants.js +36 -0
- package/dist/src/core/furnace-graph-utils.d.ts +11 -0
- package/dist/src/core/furnace-graph-utils.js +94 -0
- package/dist/src/core/furnace-operation.d.ts +108 -0
- package/dist/src/core/furnace-operation.js +220 -0
- package/dist/src/core/furnace-refresh.d.ts +20 -0
- package/dist/src/core/furnace-refresh.js +118 -0
- package/dist/src/core/furnace-registration-ast.d.ts +5 -0
- package/dist/src/core/furnace-registration-ast.js +134 -4
- package/dist/src/core/furnace-registration-remove.d.ts +25 -3
- package/dist/src/core/furnace-registration-remove.js +196 -62
- package/dist/src/core/furnace-registration-validate.d.ts +13 -1
- package/dist/src/core/furnace-registration-validate.js +15 -3
- package/dist/src/core/furnace-registration.d.ts +27 -4
- package/dist/src/core/furnace-registration.js +93 -11
- package/dist/src/core/furnace-rollback.d.ts +11 -0
- package/dist/src/core/furnace-rollback.js +78 -7
- package/dist/src/core/furnace-scanner.d.ts +8 -2
- package/dist/src/core/furnace-scanner.js +152 -55
- package/dist/src/core/furnace-stories.js +7 -5
- package/dist/src/core/furnace-validate-accessibility.js +7 -1
- package/dist/src/core/furnace-validate-compatibility.d.ts +1 -1
- package/dist/src/core/furnace-validate-compatibility.js +85 -1
- package/dist/src/core/furnace-validate-helpers.d.ts +4 -0
- package/dist/src/core/furnace-validate-helpers.js +31 -0
- package/dist/src/core/furnace-validate-registration.d.ts +17 -2
- package/dist/src/core/furnace-validate-registration.js +73 -3
- package/dist/src/core/furnace-validate-structure.d.ts +10 -2
- package/dist/src/core/furnace-validate-structure.js +45 -3
- package/dist/src/core/furnace-validate.d.ts +10 -1
- package/dist/src/core/furnace-validate.js +80 -6
- package/dist/src/core/furnace-version-drift.d.ts +55 -0
- package/dist/src/core/furnace-version-drift.js +101 -0
- package/dist/src/core/git-file-ops.d.ts +8 -0
- package/dist/src/core/git-file-ops.js +19 -6
- package/dist/src/core/lint-projection.d.ts +25 -0
- package/dist/src/core/lint-projection.js +44 -0
- package/dist/src/core/mach.d.ts +4 -2
- package/dist/src/core/mach.js +17 -2
- package/dist/src/core/markdown-table.d.ts +104 -0
- package/dist/src/core/markdown-table.js +266 -0
- package/dist/src/core/ownership-table.d.ts +53 -0
- package/dist/src/core/ownership-table.js +144 -0
- package/dist/src/core/patch-apply.d.ts +17 -3
- package/dist/src/core/patch-apply.js +86 -8
- package/dist/src/core/patch-export.d.ts +119 -5
- package/dist/src/core/patch-export.js +183 -25
- package/dist/src/core/patch-lint-cross.d.ts +195 -0
- package/dist/src/core/patch-lint-cross.js +428 -0
- package/dist/src/core/patch-lint-diff.d.ts +33 -0
- package/dist/src/core/patch-lint-diff.js +84 -0
- package/dist/src/core/patch-lint.d.ts +2 -4
- package/dist/src/core/patch-lint.js +12 -50
- package/dist/src/core/patch-lock.js +2 -1
- package/dist/src/core/patch-manifest-io.d.ts +102 -1
- package/dist/src/core/patch-manifest-io.js +270 -2
- package/dist/src/core/patch-manifest-query.d.ts +1 -1
- package/dist/src/core/patch-manifest-query.js +1 -1
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-manifest.js +1 -1
- package/dist/src/core/patch-transform.d.ts +12 -0
- package/dist/src/core/patch-transform.js +21 -7
- package/dist/src/core/token-manager.js +67 -69
- package/dist/src/core/wire-destroy.js +6 -3
- package/dist/src/core/wire-init.js +10 -4
- package/dist/src/core/wire-subscript.js +9 -3
- package/dist/src/core/wire-utils.d.ts +52 -5
- package/dist/src/core/wire-utils.js +69 -6
- package/dist/src/errors/base.d.ts +20 -0
- package/dist/src/errors/base.js +24 -0
- package/dist/src/errors/furnace.js +7 -1
- package/dist/src/errors/rebase.js +6 -1
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/options.d.ts +125 -4
- package/dist/src/types/commands/patches.d.ts +11 -1
- package/dist/src/types/config.d.ts +1 -1
- package/dist/src/types/furnace.d.ts +55 -1
- package/dist/src/utils/fs.d.ts +12 -0
- package/dist/src/utils/fs.js +30 -1
- package/dist/src/utils/package-root.d.ts +5 -0
- package/dist/src/utils/package-root.js +12 -0
- package/dist/src/utils/process.js +9 -4
- package/dist/src/utils/validation.d.ts +20 -2
- package/dist/src/utils/validation.js +26 -3
- package/package.json +1 -1
|
@@ -6,7 +6,10 @@ import { pathExists, readJson, writeJson } from '../utils/fs.js';
|
|
|
6
6
|
import { warn } from '../utils/logger.js';
|
|
7
7
|
import { isArray, isBoolean, isObject, isString } from '../utils/validation.js';
|
|
8
8
|
import { FIREFORGE_DIR } from './config.js';
|
|
9
|
+
import { resolveFtlDir } from './furnace-constants.js';
|
|
10
|
+
import { detectComposesCycles, validateComposesReferences } from './furnace-graph-utils.js';
|
|
9
11
|
import { quarantineStateFile, withStateFileLock } from './state-file.js';
|
|
12
|
+
export { detectComposesCycles };
|
|
10
13
|
/** Name of the furnace configuration file */
|
|
11
14
|
export const FURNACE_CONFIG_FILENAME = 'furnace.json';
|
|
12
15
|
/** Name of the furnace state file */
|
|
@@ -81,6 +84,7 @@ function parseOverrideConfig(data, name) {
|
|
|
81
84
|
description: data['description'],
|
|
82
85
|
basePath: data['basePath'],
|
|
83
86
|
baseVersion: data['baseVersion'],
|
|
87
|
+
...(isString(data['baseCommit']) ? { baseCommit: data['baseCommit'] } : {}),
|
|
84
88
|
};
|
|
85
89
|
}
|
|
86
90
|
/**
|
|
@@ -117,6 +121,37 @@ function parseCustomConfig(data, name) {
|
|
|
117
121
|
: {}),
|
|
118
122
|
};
|
|
119
123
|
}
|
|
124
|
+
/** The current (and only) config schema version. */
|
|
125
|
+
const CURRENT_CONFIG_VERSION = 1;
|
|
126
|
+
/**
|
|
127
|
+
* Migrates a furnace config from an older schema version to the current one.
|
|
128
|
+
* Returns the data unchanged if it is already at the current version.
|
|
129
|
+
*
|
|
130
|
+
* When a future version 2 is introduced, add a `case 1:` that transforms
|
|
131
|
+
* v1 data into v2 shape and falls through to validation. The pattern is:
|
|
132
|
+
*
|
|
133
|
+
* ```
|
|
134
|
+
* case 1:
|
|
135
|
+
* data = migrateV1ToV2(data);
|
|
136
|
+
* // fallthrough
|
|
137
|
+
* case 2:
|
|
138
|
+
* break;
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export function migrateFurnaceConfig(data) {
|
|
142
|
+
const version = data['version'];
|
|
143
|
+
if (typeof version !== 'number' || !Number.isInteger(version) || version < 1) {
|
|
144
|
+
throw new FurnaceError(`Furnace config: "version" must be a positive integer (got ${JSON.stringify(version)}). ` +
|
|
145
|
+
`Current schema version is ${CURRENT_CONFIG_VERSION}.`);
|
|
146
|
+
}
|
|
147
|
+
if (version > CURRENT_CONFIG_VERSION) {
|
|
148
|
+
throw new FurnaceError(`Furnace config: version ${version} is newer than what this version of FireForge supports (${CURRENT_CONFIG_VERSION}). ` +
|
|
149
|
+
'Upgrade FireForge to read this config.');
|
|
150
|
+
}
|
|
151
|
+
// Today only version 1 exists, so no migration is needed. When future
|
|
152
|
+
// versions are added, migration steps will be chained here.
|
|
153
|
+
return data;
|
|
154
|
+
}
|
|
120
155
|
/**
|
|
121
156
|
* Validates a raw config object and returns a typed FurnaceConfig.
|
|
122
157
|
* @param data - Raw data to validate
|
|
@@ -127,27 +162,39 @@ export function validateFurnaceConfig(data) {
|
|
|
127
162
|
if (!isObject(data)) {
|
|
128
163
|
throw new FurnaceError('Furnace config must be an object');
|
|
129
164
|
}
|
|
130
|
-
|
|
131
|
-
|
|
165
|
+
// Run migration before validation so older configs are transparently upgraded.
|
|
166
|
+
const migrated = migrateFurnaceConfig(data);
|
|
167
|
+
if (migrated['version'] !== CURRENT_CONFIG_VERSION) {
|
|
168
|
+
throw new FurnaceError(`Furnace config: "version" must be ${CURRENT_CONFIG_VERSION} after migration`);
|
|
132
169
|
}
|
|
133
|
-
if (!isString(
|
|
170
|
+
if (!isString(migrated['componentPrefix'])) {
|
|
134
171
|
throw new FurnaceError('Furnace config: "componentPrefix" must be a string');
|
|
135
172
|
}
|
|
136
173
|
// Validate optional tokenPrefix
|
|
137
|
-
if (
|
|
174
|
+
if (migrated['tokenPrefix'] !== undefined && !isString(migrated['tokenPrefix'])) {
|
|
138
175
|
throw new FurnaceError('Furnace config: "tokenPrefix" must be a string if provided');
|
|
139
176
|
}
|
|
140
177
|
// Validate optional tokenAllowlist
|
|
141
|
-
if (
|
|
142
|
-
parseStringArray(
|
|
178
|
+
if (migrated['tokenAllowlist'] !== undefined) {
|
|
179
|
+
parseStringArray(migrated['tokenAllowlist'], 'tokenAllowlist');
|
|
180
|
+
}
|
|
181
|
+
const stock = parseStringArray(migrated['stock'], 'stock');
|
|
182
|
+
const stockSet = new Set();
|
|
183
|
+
for (const name of stock) {
|
|
184
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
185
|
+
throw new FurnaceError(`Furnace config: stock entry "${name}" must match /^[a-z][a-z0-9-]*$/ (lowercase, no path separators)`);
|
|
186
|
+
}
|
|
187
|
+
if (stockSet.has(name)) {
|
|
188
|
+
throw new FurnaceError(`Furnace config: duplicate stock entry "${name}"`);
|
|
189
|
+
}
|
|
190
|
+
stockSet.add(name);
|
|
143
191
|
}
|
|
144
|
-
const stock = parseStringArray(data['stock'], 'stock');
|
|
145
192
|
// Validate overrides
|
|
146
|
-
if (!isObject(
|
|
193
|
+
if (!isObject(migrated['overrides'])) {
|
|
147
194
|
throw new FurnaceError('Furnace config: "overrides" must be an object');
|
|
148
195
|
}
|
|
149
196
|
const overrides = {};
|
|
150
|
-
for (const [name, value] of Object.entries(
|
|
197
|
+
for (const [name, value] of Object.entries(migrated['overrides'])) {
|
|
151
198
|
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
152
199
|
throw new FurnaceError(`Furnace config: override name "${name}" must match /^[a-z][a-z0-9-]*$/ (lowercase, no path separators)`);
|
|
153
200
|
}
|
|
@@ -157,11 +204,11 @@ export function validateFurnaceConfig(data) {
|
|
|
157
204
|
overrides[name] = parseOverrideConfig(value, name);
|
|
158
205
|
}
|
|
159
206
|
// Validate custom
|
|
160
|
-
if (!isObject(
|
|
207
|
+
if (!isObject(migrated['custom'])) {
|
|
161
208
|
throw new FurnaceError('Furnace config: "custom" must be an object');
|
|
162
209
|
}
|
|
163
210
|
const custom = {};
|
|
164
|
-
for (const [name, value] of Object.entries(
|
|
211
|
+
for (const [name, value] of Object.entries(migrated['custom'])) {
|
|
165
212
|
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
166
213
|
throw new FurnaceError(`Furnace config: custom name "${name}" must match /^[a-z][a-z0-9-]*$/ (lowercase, no path separators)`);
|
|
167
214
|
}
|
|
@@ -170,18 +217,42 @@ export function validateFurnaceConfig(data) {
|
|
|
170
217
|
}
|
|
171
218
|
custom[name] = parseCustomConfig(value, name);
|
|
172
219
|
}
|
|
220
|
+
// Detect circular composes references among custom components.
|
|
221
|
+
detectComposesCycles(custom);
|
|
222
|
+
// Validate that every composes reference points to a known component.
|
|
223
|
+
validateComposesReferences(stock, overrides, custom);
|
|
173
224
|
const config = {
|
|
174
|
-
version:
|
|
175
|
-
componentPrefix:
|
|
225
|
+
version: CURRENT_CONFIG_VERSION,
|
|
226
|
+
componentPrefix: migrated['componentPrefix'],
|
|
176
227
|
stock,
|
|
177
228
|
overrides,
|
|
178
229
|
custom,
|
|
179
230
|
};
|
|
180
|
-
if (
|
|
181
|
-
config.tokenPrefix =
|
|
231
|
+
if (migrated['tokenPrefix'] !== undefined) {
|
|
232
|
+
config.tokenPrefix = migrated['tokenPrefix'];
|
|
182
233
|
}
|
|
183
|
-
if (
|
|
184
|
-
config.tokenAllowlist = parseStringArray(
|
|
234
|
+
if (migrated['tokenAllowlist'] !== undefined) {
|
|
235
|
+
config.tokenAllowlist = parseStringArray(migrated['tokenAllowlist'], 'tokenAllowlist');
|
|
236
|
+
}
|
|
237
|
+
// Validate optional ftlBasePath
|
|
238
|
+
if (migrated['ftlBasePath'] !== undefined) {
|
|
239
|
+
if (!isString(migrated['ftlBasePath'])) {
|
|
240
|
+
throw new FurnaceError('Furnace config: "ftlBasePath" must be a string if provided');
|
|
241
|
+
}
|
|
242
|
+
if (migrated['ftlBasePath'].includes('..')) {
|
|
243
|
+
throw new FurnaceError('Furnace config: "ftlBasePath" must not contain ".." (path traversal)');
|
|
244
|
+
}
|
|
245
|
+
config.ftlBasePath = migrated['ftlBasePath'];
|
|
246
|
+
}
|
|
247
|
+
// Validate optional scanPaths
|
|
248
|
+
if (migrated['scanPaths'] !== undefined) {
|
|
249
|
+
const paths = parseStringArray(migrated['scanPaths'], 'scanPaths');
|
|
250
|
+
for (const p of paths) {
|
|
251
|
+
if (p.includes('..')) {
|
|
252
|
+
throw new FurnaceError('Furnace config: "scanPaths" entries must not contain ".." (path traversal)');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
config.scanPaths = paths;
|
|
185
256
|
}
|
|
186
257
|
return config;
|
|
187
258
|
}
|
|
@@ -197,6 +268,39 @@ export function validateFurnaceState(data) {
|
|
|
197
268
|
}
|
|
198
269
|
return result.state;
|
|
199
270
|
}
|
|
271
|
+
const PENDING_REPAIR_OPERATIONS = [
|
|
272
|
+
'preview-teardown',
|
|
273
|
+
'apply-rollback',
|
|
274
|
+
'deploy-rollback',
|
|
275
|
+
'remove-rollback',
|
|
276
|
+
'create-rollback',
|
|
277
|
+
'override-rollback',
|
|
278
|
+
'scan-rollback',
|
|
279
|
+
'rename-rollback',
|
|
280
|
+
'refresh-rollback',
|
|
281
|
+
];
|
|
282
|
+
function parsePendingRepair(data) {
|
|
283
|
+
if (!isObject(data)) {
|
|
284
|
+
return { error: 'field "pendingRepair" must be an object' };
|
|
285
|
+
}
|
|
286
|
+
if (!isString(data['operation']) ||
|
|
287
|
+
!PENDING_REPAIR_OPERATIONS.includes(data['operation'])) {
|
|
288
|
+
return {
|
|
289
|
+
error: `pendingRepair.operation must be one of: ${PENDING_REPAIR_OPERATIONS.join(', ')}`,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
if (!isString(data['timestamp'])) {
|
|
293
|
+
return { error: 'pendingRepair.timestamp must be a string' };
|
|
294
|
+
}
|
|
295
|
+
if (!isString(data['reason'])) {
|
|
296
|
+
return { error: 'pendingRepair.reason must be a string' };
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
operation: data['operation'],
|
|
300
|
+
timestamp: data['timestamp'],
|
|
301
|
+
reason: data['reason'],
|
|
302
|
+
};
|
|
303
|
+
}
|
|
200
304
|
function sanitizeFurnaceState(data) {
|
|
201
305
|
if (!isObject(data)) {
|
|
202
306
|
return {
|
|
@@ -238,6 +342,33 @@ function sanitizeFurnaceState(data) {
|
|
|
238
342
|
}
|
|
239
343
|
}
|
|
240
344
|
}
|
|
345
|
+
if (data['engineChecksums'] !== undefined) {
|
|
346
|
+
if (!isObject(data['engineChecksums'])) {
|
|
347
|
+
issues.push('field "engineChecksums" must be an object of string checksum values');
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
const engineChecksums = {};
|
|
351
|
+
for (const [filePath, checksum] of Object.entries(data['engineChecksums'])) {
|
|
352
|
+
if (isString(checksum)) {
|
|
353
|
+
engineChecksums[filePath] = checksum;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (Object.keys(engineChecksums).length > 0) {
|
|
357
|
+
state.engineChecksums = engineChecksums;
|
|
358
|
+
recoveredFields.push('engineChecksums');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (data['pendingRepair'] !== undefined) {
|
|
363
|
+
const parsed = parsePendingRepair(data['pendingRepair']);
|
|
364
|
+
if ('error' in parsed) {
|
|
365
|
+
issues.push(parsed.error);
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
state.pendingRepair = parsed;
|
|
369
|
+
recoveredFields.push('pendingRepair');
|
|
370
|
+
}
|
|
371
|
+
}
|
|
241
372
|
return { state, issues, recoveredFields };
|
|
242
373
|
}
|
|
243
374
|
async function recoverInvalidFurnaceState(statePath, result, alreadyLocked = false) {
|
|
@@ -369,4 +500,36 @@ export async function updateFurnaceState(root, updates) {
|
|
|
369
500
|
await writeJson(paths.furnaceState, validateFurnaceState(nextState));
|
|
370
501
|
});
|
|
371
502
|
}
|
|
503
|
+
/**
|
|
504
|
+
* Collects engine-relative path prefixes that are managed by the Furnace
|
|
505
|
+
* component system (overrides, custom components, and their Fluent l10n
|
|
506
|
+
* files). Used by `status` and `export-all` to classify engine changes
|
|
507
|
+
* as Furnace-managed rather than unmanaged drift.
|
|
508
|
+
*
|
|
509
|
+
* Returns an empty set when no furnace config exists (opt-in subsystem).
|
|
510
|
+
* Prefixes always end with `/` so callers can use `startsWith()`.
|
|
511
|
+
*/
|
|
512
|
+
export async function collectFurnaceManagedPrefixes(root) {
|
|
513
|
+
if (!(await furnaceConfigExists(root)))
|
|
514
|
+
return new Set();
|
|
515
|
+
const config = await loadFurnaceConfig(root);
|
|
516
|
+
const ftlDir = resolveFtlDir(config.ftlBasePath);
|
|
517
|
+
const prefixes = new Set();
|
|
518
|
+
for (const [, overrideCfg] of Object.entries(config.overrides)) {
|
|
519
|
+
const base = overrideCfg.basePath.endsWith('/')
|
|
520
|
+
? overrideCfg.basePath
|
|
521
|
+
: overrideCfg.basePath + '/';
|
|
522
|
+
prefixes.add(base);
|
|
523
|
+
}
|
|
524
|
+
for (const [, customCfg] of Object.entries(config.custom)) {
|
|
525
|
+
const target = customCfg.targetPath.endsWith('/')
|
|
526
|
+
? customCfg.targetPath
|
|
527
|
+
: customCfg.targetPath + '/';
|
|
528
|
+
prefixes.add(target);
|
|
529
|
+
if (customCfg.localized) {
|
|
530
|
+
prefixes.add(ftlDir.endsWith('/') ? ftlDir : ftlDir + '/');
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return prefixes;
|
|
534
|
+
}
|
|
372
535
|
//# sourceMappingURL=furnace-config.js.map
|
|
@@ -2,3 +2,25 @@
|
|
|
2
2
|
export declare const CUSTOM_ELEMENTS_JS = "toolkit/content/customElements.js";
|
|
3
3
|
/** Path to jar.mn within the engine source tree (toolkit global) */
|
|
4
4
|
export declare const JAR_MN = "toolkit/content/jar.mn";
|
|
5
|
+
/** Default Fluent localization directory for toolkit global components, relative to engine root */
|
|
6
|
+
export declare const FTL_DIR = "toolkit/locales/en-US/toolkit/global";
|
|
7
|
+
/** File extensions that constitute a Furnace component's source files. */
|
|
8
|
+
export declare const COMPONENT_FILE_EXTENSIONS: readonly [".mjs", ".css", ".ftl"];
|
|
9
|
+
/** Returns true when `fileName` has one of the standard component file extensions. */
|
|
10
|
+
export declare function isComponentSourceFile(fileName: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Resolves the FTL base path, preferring the user-configured value from
|
|
13
|
+
* `furnace.json` over the built-in default.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveFtlDir(configuredPath?: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Converts a kebab-case tag name to PascalCase class name.
|
|
18
|
+
* e.g. "moz-sidebar-panel" → "MozSidebarPanel"
|
|
19
|
+
*/
|
|
20
|
+
export declare function tagNameToClassName(tagName: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Strips a known component prefix from a tag name to produce a concise
|
|
23
|
+
* display name. Falls back to the full tag name when the prefix doesn't
|
|
24
|
+
* match, so callers never receive an empty string.
|
|
25
|
+
*/
|
|
26
|
+
export declare function stripComponentPrefix(tagName: string, componentPrefix: string): string;
|
|
@@ -3,4 +3,40 @@
|
|
|
3
3
|
export const CUSTOM_ELEMENTS_JS = 'toolkit/content/customElements.js';
|
|
4
4
|
/** Path to jar.mn within the engine source tree (toolkit global) */
|
|
5
5
|
export const JAR_MN = 'toolkit/content/jar.mn';
|
|
6
|
+
/** Default Fluent localization directory for toolkit global components, relative to engine root */
|
|
7
|
+
export const FTL_DIR = 'toolkit/locales/en-US/toolkit/global';
|
|
8
|
+
/** File extensions that constitute a Furnace component's source files. */
|
|
9
|
+
export const COMPONENT_FILE_EXTENSIONS = ['.mjs', '.css', '.ftl'];
|
|
10
|
+
/** Returns true when `fileName` has one of the standard component file extensions. */
|
|
11
|
+
export function isComponentSourceFile(fileName) {
|
|
12
|
+
return COMPONENT_FILE_EXTENSIONS.some((ext) => fileName.endsWith(ext));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolves the FTL base path, preferring the user-configured value from
|
|
16
|
+
* `furnace.json` over the built-in default.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveFtlDir(configuredPath) {
|
|
19
|
+
return configuredPath ?? FTL_DIR;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Converts a kebab-case tag name to PascalCase class name.
|
|
23
|
+
* e.g. "moz-sidebar-panel" → "MozSidebarPanel"
|
|
24
|
+
*/
|
|
25
|
+
export function tagNameToClassName(tagName) {
|
|
26
|
+
return tagName
|
|
27
|
+
.split('-')
|
|
28
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
29
|
+
.join('');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Strips a known component prefix from a tag name to produce a concise
|
|
33
|
+
* display name. Falls back to the full tag name when the prefix doesn't
|
|
34
|
+
* match, so callers never receive an empty string.
|
|
35
|
+
*/
|
|
36
|
+
export function stripComponentPrefix(tagName, componentPrefix) {
|
|
37
|
+
if (componentPrefix && tagName.startsWith(componentPrefix)) {
|
|
38
|
+
return tagName.slice(componentPrefix.length);
|
|
39
|
+
}
|
|
40
|
+
return tagName;
|
|
41
|
+
}
|
|
6
42
|
//# sourceMappingURL=furnace-constants.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CustomComponentConfig, FurnaceConfig } from '../types/furnace.js';
|
|
2
|
+
/** Throws a {@link FurnaceError} if the composes graph among custom components contains a cycle. */
|
|
3
|
+
export declare function detectComposesCycles(custom: FurnaceConfig['custom']): void;
|
|
4
|
+
/** Validates that every `composes` reference in custom components points to a known component. */
|
|
5
|
+
export declare function validateComposesReferences(stock: string[], overrides: FurnaceConfig['overrides'], custom: FurnaceConfig['custom']): void;
|
|
6
|
+
/**
|
|
7
|
+
* Returns custom component names in topological order so that components
|
|
8
|
+
* depended upon via `composes` are applied before those that compose them.
|
|
9
|
+
* Falls back to insertion order when there are no composes edges.
|
|
10
|
+
*/
|
|
11
|
+
export declare function topologicalSortCustom(custom: Record<string, CustomComponentConfig>): string[];
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { FurnaceError } from '../errors/furnace.js';
|
|
3
|
+
/** Throws a {@link FurnaceError} if the composes graph among custom components contains a cycle. */
|
|
4
|
+
export function detectComposesCycles(custom) {
|
|
5
|
+
const visited = new Set();
|
|
6
|
+
const stack = new Set();
|
|
7
|
+
function visit(name, path) {
|
|
8
|
+
if (stack.has(name)) {
|
|
9
|
+
const cycle = [...path.slice(path.indexOf(name)), name];
|
|
10
|
+
throw new FurnaceError(`Furnace config: circular composes dependency detected: ${cycle.join(' → ')}`);
|
|
11
|
+
}
|
|
12
|
+
if (visited.has(name))
|
|
13
|
+
return;
|
|
14
|
+
stack.add(name);
|
|
15
|
+
path.push(name);
|
|
16
|
+
const deps = custom[name]?.composes;
|
|
17
|
+
if (deps) {
|
|
18
|
+
for (const dep of deps) {
|
|
19
|
+
if (dep in custom) {
|
|
20
|
+
visit(dep, path);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
path.pop();
|
|
25
|
+
stack.delete(name);
|
|
26
|
+
visited.add(name);
|
|
27
|
+
}
|
|
28
|
+
for (const name of Object.keys(custom)) {
|
|
29
|
+
visit(name, []);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Validates that every `composes` reference in custom components points to a known component. */
|
|
33
|
+
export function validateComposesReferences(stock, overrides, custom) {
|
|
34
|
+
const known = new Set([...stock, ...Object.keys(overrides), ...Object.keys(custom)]);
|
|
35
|
+
for (const [name, config] of Object.entries(custom)) {
|
|
36
|
+
if (!config.composes)
|
|
37
|
+
continue;
|
|
38
|
+
for (const ref of config.composes) {
|
|
39
|
+
if (!known.has(ref)) {
|
|
40
|
+
throw new FurnaceError(`Furnace config: custom "${name}" composes unknown component "${ref}". ` +
|
|
41
|
+
'The referenced component must be registered as stock, override, or custom.');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Returns custom component names in topological order so that components
|
|
48
|
+
* depended upon via `composes` are applied before those that compose them.
|
|
49
|
+
* Falls back to insertion order when there are no composes edges.
|
|
50
|
+
*/
|
|
51
|
+
export function topologicalSortCustom(custom) {
|
|
52
|
+
const names = Object.keys(custom);
|
|
53
|
+
const inDegree = new Map();
|
|
54
|
+
const dependents = new Map();
|
|
55
|
+
for (const name of names) {
|
|
56
|
+
inDegree.set(name, 0);
|
|
57
|
+
dependents.set(name, []);
|
|
58
|
+
}
|
|
59
|
+
for (const [name, config] of Object.entries(custom)) {
|
|
60
|
+
if (!config.composes)
|
|
61
|
+
continue;
|
|
62
|
+
for (const dep of config.composes) {
|
|
63
|
+
if (dep in custom) {
|
|
64
|
+
inDegree.set(name, (inDegree.get(name) ?? 0) + 1);
|
|
65
|
+
dependents.get(dep)?.push(name);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const queue = names.filter((n) => (inDegree.get(n) ?? 0) === 0);
|
|
70
|
+
const sorted = [];
|
|
71
|
+
while (queue.length > 0) {
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guarded by while condition
|
|
73
|
+
const current = queue.shift();
|
|
74
|
+
sorted.push(current);
|
|
75
|
+
for (const dep of dependents.get(current) ?? []) {
|
|
76
|
+
const newDegree = (inDegree.get(dep) ?? 1) - 1;
|
|
77
|
+
inDegree.set(dep, newDegree);
|
|
78
|
+
if (newDegree === 0) {
|
|
79
|
+
queue.push(dep);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// If a cycle was somehow missed by config validation, fall back to insertion order
|
|
84
|
+
// for any nodes not reached.
|
|
85
|
+
if (sorted.length < names.length) {
|
|
86
|
+
for (const name of names) {
|
|
87
|
+
if (!sorted.includes(name)) {
|
|
88
|
+
sorted.push(name);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return sorted;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=furnace-graph-utils.js.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { FurnacePendingRepairOperation } from '../types/furnace.js';
|
|
2
|
+
import { type RollbackJournal } from './furnace-rollback.js';
|
|
3
|
+
/**
|
|
4
|
+
* The signal names the lifecycle wrapper knows how to react to. Spelled out
|
|
5
|
+
* as a literal union (rather than `NodeJS.Signals`) so the public type
|
|
6
|
+
* surface does not depend on the NodeJS global namespace — consumers of
|
|
7
|
+
* `@hominis/fireforge` may compile against tsconfigs that omit `@types/node`.
|
|
8
|
+
*/
|
|
9
|
+
export type FurnaceShutdownSignal = 'SIGINT' | 'SIGTERM';
|
|
10
|
+
/**
|
|
11
|
+
* Context handed to a furnace mutation body so it can register the in-flight
|
|
12
|
+
* rollback journal with the lifecycle wrapper. The wrapper uses the registered
|
|
13
|
+
* journal to perform rollback when the process receives SIGINT/SIGTERM mid-run.
|
|
14
|
+
*/
|
|
15
|
+
export interface FurnaceOperationContext {
|
|
16
|
+
/**
|
|
17
|
+
* Registers the rollback journal for the current operation. Must be called
|
|
18
|
+
* once the body has constructed its journal so the signal handler can find
|
|
19
|
+
* it. Calling more than once replaces the prior reference (this is fine for
|
|
20
|
+
* commands that build the journal lazily).
|
|
21
|
+
*/
|
|
22
|
+
registerJournal(journal: RollbackJournal): void;
|
|
23
|
+
/**
|
|
24
|
+
* Registers an extra cleanup callback to run during signal-driven teardown
|
|
25
|
+
* in addition to the journal restore. Used by `furnace preview` to make
|
|
26
|
+
* sure `cleanStories` runs even when the user hits Ctrl+C mid-run. The
|
|
27
|
+
* callback should be best-effort and idempotent: cleanup errors are
|
|
28
|
+
* collected, not re-thrown.
|
|
29
|
+
*/
|
|
30
|
+
registerCleanup(cleanup: () => Promise<void>): void;
|
|
31
|
+
}
|
|
32
|
+
/** Options for `runFurnaceMutation`. */
|
|
33
|
+
export interface RunFurnaceMutationOptions {
|
|
34
|
+
/**
|
|
35
|
+
* If true, skip lock acquisition and signal-handler installation entirely.
|
|
36
|
+
* Used by dry-run paths where no engine mutation occurs.
|
|
37
|
+
*
|
|
38
|
+
* Note: a dry-run can overlap with a real mutation because it does not
|
|
39
|
+
* acquire the lock. This is safe because dry-runs only read; however, a
|
|
40
|
+
* dry-run that starts before a real mutation and finishes after it may
|
|
41
|
+
* observe partially-written engine state. Accept this trade-off so that
|
|
42
|
+
* concurrent dry-runs never block each other or a real apply.
|
|
43
|
+
*/
|
|
44
|
+
dryRun?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Override the default 30s lock timeout. The watch-mode caller may want a
|
|
47
|
+
* shorter window so an interactive rebuild fails fast instead of stalling.
|
|
48
|
+
*/
|
|
49
|
+
lockTimeoutMs?: number;
|
|
50
|
+
/**
|
|
51
|
+
* If true, skip the pendingRepair pre-flight check. Used by `doctor
|
|
52
|
+
* --repair-furnace` which must be able to mutate the engine even when a
|
|
53
|
+
* pendingRepair marker is set.
|
|
54
|
+
*/
|
|
55
|
+
skipPendingRepairCheck?: boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Returns true while a signal-driven rollback is in progress. The bin entry
|
|
59
|
+
* point uses this as a re-entrancy guard so a user mashing Ctrl+C cannot
|
|
60
|
+
* trigger a second rollback that races the first. Exposed for the bin shim
|
|
61
|
+
* (and the test suite); production callers should not need it.
|
|
62
|
+
*/
|
|
63
|
+
export declare function isSignalRollbackInFlight(): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Rolls back every in-flight furnace operation and writes a pendingRepair
|
|
66
|
+
* marker for each. Each cleanup callback and journal restore is bounded by a
|
|
67
|
+
* timeout so a stuck I/O operation cannot hang the process indefinitely.
|
|
68
|
+
*/
|
|
69
|
+
export declare function rollbackActiveOperationsForSignal(signal: FurnaceShutdownSignal): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Resolves the path of the lock directory used to serialize furnace mutations
|
|
72
|
+
* for a given project root. Exposed for tests; production callers should not
|
|
73
|
+
* touch this directly.
|
|
74
|
+
*/
|
|
75
|
+
export declare function getFurnaceLockPath(root: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* Runs a furnace-mutating body under the apply-wide lock and registers it
|
|
78
|
+
* with the process-wide SIGINT/SIGTERM rollback pathway. The lock prevents
|
|
79
|
+
* two `furnace apply`/`deploy`/`create`/etc. runs from racing on the engine
|
|
80
|
+
* working copy; the CLI entrypoint's global signal handlers consult this
|
|
81
|
+
* registry and invoke rollback (writing a `pendingRepair` marker when needed)
|
|
82
|
+
* if the user hits Ctrl+C mid-run.
|
|
83
|
+
*
|
|
84
|
+
* Dry-run callers should pass `options.dryRun = true` so the wrapper skips
|
|
85
|
+
* the lock entirely (concurrent dry-runs are safe and shouldn't block each
|
|
86
|
+
* other).
|
|
87
|
+
*
|
|
88
|
+
* The body receives a {@link FurnaceOperationContext}; it must call
|
|
89
|
+
* `ctx.registerJournal(journal)` once it has constructed its rollback journal.
|
|
90
|
+
* Bodies that don't manage a journal directly (e.g. apply, which delegates to
|
|
91
|
+
* `applyAllComponents`) can pass an internal callback through.
|
|
92
|
+
*/
|
|
93
|
+
export declare function runFurnaceMutation<T>(root: string, kind: FurnacePendingRepairOperation, body: (ctx: FurnaceOperationContext) => Promise<T>, options?: RunFurnaceMutationOptions): Promise<T>;
|
|
94
|
+
/**
|
|
95
|
+
* Persists an `apply-rollback` (or other operation-kind) `pendingRepair`
|
|
96
|
+
* marker on behalf of a caller that detected a rollback failure outside the
|
|
97
|
+
* signal-handler path (e.g. apply's own catch-around-restore). Exposed so
|
|
98
|
+
* `furnace-apply.ts` can write the marker without taking on a dependency on
|
|
99
|
+
* the lifecycle wrapper's internals.
|
|
100
|
+
*/
|
|
101
|
+
export declare function recordFurnaceRollbackFailure(root: string, operation: FurnacePendingRepairOperation, reason: string): Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* Test-only helper: tears down the module-scoped state. Vitest workers may
|
|
104
|
+
* reuse the module across tests, so the test suite must call this between
|
|
105
|
+
* cases that exercise the signal pathway. Not exported from the package
|
|
106
|
+
* entry point.
|
|
107
|
+
*/
|
|
108
|
+
export declare function __resetFurnaceOperationStateForTests(): void;
|