fimo 2.3.2 → 2.4.0-staging.2
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/assets/skills/fimo-cli/SKILL.md +1 -1
- package/assets/skills/fimo-cli/references/deploy.md +8 -11
- package/assets/skills/fimo-cli/references/init.md +1 -1
- package/assets/skills/fimo-cli/references/integrations/firecrawl.md +1 -1
- package/assets/skills/fimo-cli/references/preview.md +12 -42
- package/assets/skills/fimo-code/references/frameworks/next/app-router.md +7 -21
- package/assets/skills/fimo-code/references/frameworks/next/seo.md +168 -0
- package/assets/skills/fimo-code/references/frameworks/next.md +1 -1
- package/assets/skills/fimo-migration/SKILL.md +29 -33
- package/assets/skills/fimo-migration/references/content.md +1 -1
- package/assets/skills/fimo-migration/references/frameworks/nextjs.md +7 -0
- package/assets/skills/fimo-migration/references/import-contract.md +4 -6
- package/assets/skills/fimo-migration/references/manifest.md +4 -3
- package/assets/skills/fimo-migration/references/questions-and-improvements.md +1 -1
- package/assets/skills/fimo-migration/references/workflow.md +7 -5
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +494 -981
- package/dist/runtime/seo/index.d.ts +1 -0
- package/dist/runtime/seo/resolver.js +10 -0
- package/dist/runtime/seo/server.d.ts +13 -0
- package/dist/runtime/seo/server.js +23 -0
- package/dist/runtime/shared/fimo-config.server.d.ts +2 -0
- package/dist/runtime/shared/fimo-config.server.js +18 -14
- package/package.json +12 -8
- package/release.json +2 -2
- package/templates/astro/package.json +2 -2
- package/templates/nextjs/AGENTS.md +1 -1
- package/templates/nextjs/package.json +3 -3
- package/templates/nextjs/src/app/layout.tsx +5 -3
- package/templates/nextjs/src/app/page.tsx +6 -0
- package/templates/react-router/package.json +4 -4
- package/templates/sveltekit/package.json +2 -2
- package/templates/vite/package.json +4 -4
- package/assets/skills/fimo-migration/scripts/build-canonical-manifest.mjs +0 -595
|
@@ -1,595 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import process from 'node:process';
|
|
5
|
-
|
|
6
|
-
const STATUS_ORDER = new Map([
|
|
7
|
-
['discovered', 0],
|
|
8
|
-
['planned', 1],
|
|
9
|
-
['generated', 2],
|
|
10
|
-
['imported', 3],
|
|
11
|
-
['verified', 4],
|
|
12
|
-
['ignored', 4],
|
|
13
|
-
['unsupported', 4],
|
|
14
|
-
['unresolved', 4],
|
|
15
|
-
]);
|
|
16
|
-
|
|
17
|
-
function main() {
|
|
18
|
-
const args = parseArgs(process.argv.slice(2));
|
|
19
|
-
const projectRoot = path.resolve(args.projectRoot ?? process.cwd());
|
|
20
|
-
const outputPath = path.resolve(projectRoot, args.output ?? '.fimo/migration/manifest.json');
|
|
21
|
-
|
|
22
|
-
const urlLedgerPath = path.join(projectRoot, '.fimo/migrations/current/manifest.json');
|
|
23
|
-
const sourceLedgerPath = path.join(projectRoot, '.fimo/source-migration/manifest.json');
|
|
24
|
-
const previousPath = outputPath;
|
|
25
|
-
|
|
26
|
-
const urlLedger = readJsonIfExists(urlLedgerPath);
|
|
27
|
-
const sourceLedger = readJsonIfExists(sourceLedgerPath);
|
|
28
|
-
const previous = args.fresh ? null : readJsonIfExists(previousPath);
|
|
29
|
-
|
|
30
|
-
if (!urlLedger && !sourceLedger) {
|
|
31
|
-
fail(
|
|
32
|
-
`No migration ledgers found in ${projectRoot}. Expected .fimo/migrations/current/manifest.json or .fimo/source-migration/manifest.json. ` +
|
|
33
|
-
'This script only merges existing draft ledgers; for a fresh source-backed migration write .fimo/migration/manifest.json directly following references/manifest.md.'
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const manifest = buildCanonicalManifest({
|
|
38
|
-
projectRoot,
|
|
39
|
-
urlLedger,
|
|
40
|
-
urlLedgerPath,
|
|
41
|
-
sourceLedger,
|
|
42
|
-
sourceLedgerPath,
|
|
43
|
-
previous,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
47
|
-
fs.writeFileSync(outputPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
|
|
48
|
-
|
|
49
|
-
console.log(`Canonical migration manifest written to ${path.relative(projectRoot, outputPath)}`);
|
|
50
|
-
console.log(
|
|
51
|
-
`Routes: ${manifest.routes.length}, content types: ${manifest.contentTypes.length}, media: ${manifest.media.length}, forms: ${manifest.forms.length}, unresolved: ${manifest.unresolved.length}`
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function parseArgs(argv) {
|
|
56
|
-
const args = {};
|
|
57
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
58
|
-
const arg = argv[i];
|
|
59
|
-
if (arg === '--help' || arg === '-h') {
|
|
60
|
-
console.log(
|
|
61
|
-
`Usage: node build-canonical-manifest.mjs [project-root] [--output .fimo/migration/manifest.json] [--fresh]`
|
|
62
|
-
);
|
|
63
|
-
process.exit(0);
|
|
64
|
-
}
|
|
65
|
-
if (arg === '--fresh') {
|
|
66
|
-
args.fresh = true;
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
if (arg === '--output') {
|
|
70
|
-
args.output = argv[++i];
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
if (arg.startsWith('--output=')) {
|
|
74
|
-
args.output = arg.slice('--output='.length);
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (!args.projectRoot) {
|
|
78
|
-
args.projectRoot = arg;
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
fail(`Unknown argument: ${arg}`);
|
|
82
|
-
}
|
|
83
|
-
return args;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function buildCanonicalManifest({ projectRoot, urlLedger, urlLedgerPath, sourceLedger, sourceLedgerPath, previous }) {
|
|
87
|
-
const source = buildSource({ urlLedger, urlLedgerPath, sourceLedger, sourceLedgerPath, projectRoot, previous });
|
|
88
|
-
const routes = applyGeneratedRouteArtifacts(
|
|
89
|
-
projectRoot,
|
|
90
|
-
mergeItems([...normalizeSourceRoutes(sourceLedger?.routes), ...normalizeUrlRoutes(urlLedger?.pages)])
|
|
91
|
-
);
|
|
92
|
-
const contentTypes = mergeItems([
|
|
93
|
-
...normalizeSourceContentTypes(sourceLedger?.contentTypes),
|
|
94
|
-
...normalizeUrlCollections(urlLedger?.collections),
|
|
95
|
-
]);
|
|
96
|
-
const entries = mergeItems([
|
|
97
|
-
...normalizeItems(sourceLedger?.entries, 'entry-batch'),
|
|
98
|
-
...normalizeUrlEntryBatches(projectRoot, urlLedger?.collections),
|
|
99
|
-
]);
|
|
100
|
-
const media = mergeItems([...normalizeItems(sourceLedger?.media, 'media'), ...normalizeUrlMedia(projectRoot)]);
|
|
101
|
-
const forms = mergeItems([...normalizeSourceForms(sourceLedger?.forms), ...normalizeUrlForms(urlLedger?.forms)]);
|
|
102
|
-
|
|
103
|
-
const manifest = {
|
|
104
|
-
version: 1,
|
|
105
|
-
source,
|
|
106
|
-
artifacts: buildArtifacts(sourceLedger, urlLedger),
|
|
107
|
-
routes,
|
|
108
|
-
contentTypes,
|
|
109
|
-
entries,
|
|
110
|
-
media,
|
|
111
|
-
forms,
|
|
112
|
-
redirects: normalizeItems(sourceLedger?.redirects, 'redirect'),
|
|
113
|
-
locales: normalizeItems(sourceLedger?.locales, 'locale'),
|
|
114
|
-
seo: normalizeSeo(urlLedger, projectRoot),
|
|
115
|
-
unsupported: normalizeItems(sourceLedger?.unsupported, 'unsupported'),
|
|
116
|
-
unresolved: mergeItems([
|
|
117
|
-
...normalizeUnresolved(sourceLedger?.unresolved),
|
|
118
|
-
...normalizeUrlUnresolved(urlLedger?.unresolved),
|
|
119
|
-
...deriveUnresolvedFromUrlLedger(urlLedger),
|
|
120
|
-
]),
|
|
121
|
-
qa: previous?.qa ?? { status: 'not-run', checks: [] },
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
return preservePreviousProgress(manifest, previous);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function buildSource({ urlLedger, urlLedgerPath, sourceLedger, sourceLedgerPath, projectRoot, previous }) {
|
|
128
|
-
const hasUrl = Boolean(urlLedger);
|
|
129
|
-
const hasSource = Boolean(sourceLedger);
|
|
130
|
-
const sourceLedgers = [];
|
|
131
|
-
if (hasUrl) sourceLedgers.push(toPosix(path.relative(projectRoot, urlLedgerPath)));
|
|
132
|
-
if (hasSource) sourceLedgers.push(toPosix(path.relative(projectRoot, sourceLedgerPath)));
|
|
133
|
-
|
|
134
|
-
const cli = sourceLedger?.source?.cli ?? urlLedger?.source?.cli ?? previous?.source?.cli;
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
mode: hasUrl && hasSource ? 'hybrid' : hasSource ? 'source-backed' : 'url-only',
|
|
138
|
-
platform: sourceLedger?.source?.platform ?? urlLedger?.source?.platform ?? 'unknown',
|
|
139
|
-
publicUrl: sourceLedger?.source?.publicUrl ?? urlLedger?.source?.url ?? null,
|
|
140
|
-
rootPath: sourceLedger?.source?.rootPath ?? null,
|
|
141
|
-
...(cli ? { cli } : {}),
|
|
142
|
-
sourceLedgers,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function buildArtifacts(sourceLedger, urlLedger) {
|
|
147
|
-
const artifacts = {
|
|
148
|
-
code: [],
|
|
149
|
-
cms: [],
|
|
150
|
-
media: [],
|
|
151
|
-
forms: [],
|
|
152
|
-
screenshots: [],
|
|
153
|
-
other: [],
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
for (const key of Object.keys(artifacts)) {
|
|
157
|
-
artifacts[key] = uniqueArray([...(sourceLedger?.artifacts?.[key] ?? [])]);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (urlLedger?.assets?.manifest) {
|
|
161
|
-
artifacts.media.push(urlLedger.assets.manifest);
|
|
162
|
-
}
|
|
163
|
-
for (const page of asArray(urlLedger?.pages)) {
|
|
164
|
-
if (page?.artifacts?.html) artifacts.code.push(page.artifacts.html);
|
|
165
|
-
if (page?.artifacts?.screenshots?.desktop) artifacts.screenshots.push(page.artifacts.screenshots.desktop);
|
|
166
|
-
if (page?.artifacts?.screenshots?.mobile) artifacts.screenshots.push(page.artifacts.screenshots.mobile);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
for (const key of Object.keys(artifacts)) {
|
|
170
|
-
artifacts[key] = uniqueArray(artifacts[key]);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return artifacts;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function normalizeSourceRoutes(routes) {
|
|
177
|
-
return asArray(routes).map((route, index) => ({
|
|
178
|
-
...baseItem(route, `route-${index + 1}`),
|
|
179
|
-
id: route.id ?? route.path ?? route.route ?? `route-${index + 1}`,
|
|
180
|
-
path: route.path ?? route.route ?? null,
|
|
181
|
-
kind: route.kind ?? 'page',
|
|
182
|
-
contentType: route.contentType ?? null,
|
|
183
|
-
status: normalizeStatus(route.status, 'planned'),
|
|
184
|
-
}));
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function normalizeUrlRoutes(pages) {
|
|
188
|
-
return asArray(pages).map((page) => ({
|
|
189
|
-
id: `route:${page.route}`,
|
|
190
|
-
status: 'discovered',
|
|
191
|
-
path: page.route,
|
|
192
|
-
kind: page.kind ?? 'page',
|
|
193
|
-
sourceUrls: compact([page.url]),
|
|
194
|
-
sourceFiles: compact([page.artifacts?.html]),
|
|
195
|
-
targetFiles: [],
|
|
196
|
-
confidence: numberOr(page.confidence, 0.7),
|
|
197
|
-
seo: page.seo ?? null,
|
|
198
|
-
notes: compact([page.title ? `Source title: ${page.title}` : null]),
|
|
199
|
-
}));
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function applyGeneratedRouteArtifacts(projectRoot, routes) {
|
|
203
|
-
const artifactIndex = readJsonIfExists(path.join(projectRoot, '.fimo/migrations/current/generated/artifacts.json'));
|
|
204
|
-
const pageFiles = asArray(artifactIndex?.pageFiles);
|
|
205
|
-
return routes.map((route, index) => {
|
|
206
|
-
const targetFiles = uniqueArray([...asArray(route.targetFiles), pageFiles[index]]);
|
|
207
|
-
const hasGeneratedFile = targetFiles.some((file) => fs.existsSync(path.join(projectRoot, file)));
|
|
208
|
-
if (!hasGeneratedFile) return route;
|
|
209
|
-
return {
|
|
210
|
-
...route,
|
|
211
|
-
status: mostAdvancedStatus(route.status, 'generated'),
|
|
212
|
-
targetFiles,
|
|
213
|
-
};
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function normalizeSourceContentTypes(contentTypes) {
|
|
218
|
-
return asArray(contentTypes).map((type, index) => ({
|
|
219
|
-
...baseItem(type, `content-type-${index + 1}`),
|
|
220
|
-
id: type.id ?? type.uid ?? type.name ?? `content-type-${index + 1}`,
|
|
221
|
-
uid: type.uid ?? type.name ?? null,
|
|
222
|
-
name: type.name ?? type.uid ?? null,
|
|
223
|
-
status: normalizeStatus(type.status, 'planned'),
|
|
224
|
-
sourceCount: numberOrNull(type.sourceCount),
|
|
225
|
-
plannedCount: numberOrNull(type.plannedCount),
|
|
226
|
-
importedCount: numberOrNull(type.importedCount),
|
|
227
|
-
fields: type.fields ?? {},
|
|
228
|
-
references: type.references ?? [],
|
|
229
|
-
}));
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function normalizeUrlCollections(collections) {
|
|
233
|
-
return asArray(collections).map((collection) => ({
|
|
234
|
-
id: `content-type:${collection.suggestedSchema}`,
|
|
235
|
-
uid: collection.suggestedSchema,
|
|
236
|
-
name: collection.name ?? collection.suggestedSchema,
|
|
237
|
-
status: 'planned',
|
|
238
|
-
sourceFiles: [],
|
|
239
|
-
sourceUrls: asArray(collection.sourceRoutes).map((route) => `route:${route}`),
|
|
240
|
-
targetFiles: [`src/schemas/${collection.suggestedSchema}.json`],
|
|
241
|
-
confidence: numberOr(collection.confidence, 0.7),
|
|
242
|
-
sourceCount: asArray(collection.sourceRoutes).length,
|
|
243
|
-
plannedCount: asArray(collection.sourceRoutes).length,
|
|
244
|
-
importedCount: 0,
|
|
245
|
-
fields: Object.fromEntries(asArray(collection.fields).map((field) => [field, { source: field }])),
|
|
246
|
-
references: [],
|
|
247
|
-
notes: ['Inferred from crawled route patterns. Review before import.'],
|
|
248
|
-
}));
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function normalizeUrlEntryBatches(projectRoot, collections) {
|
|
252
|
-
return asArray(collections).map((collection) => {
|
|
253
|
-
const targetFile = `.fimo/migrations/current/generated/entries/${collection.suggestedSchema}.json`;
|
|
254
|
-
const entries = readJsonIfExists(path.join(projectRoot, targetFile));
|
|
255
|
-
const generatedCount = Array.isArray(entries) ? entries.length : 0;
|
|
256
|
-
return {
|
|
257
|
-
id: `entries:${collection.suggestedSchema}`,
|
|
258
|
-
contentType: collection.suggestedSchema,
|
|
259
|
-
status: generatedCount > 0 ? 'generated' : 'planned',
|
|
260
|
-
sourceFiles: [],
|
|
261
|
-
sourceUrls: asArray(collection.sourceRoutes).map((route) => `route:${route}`),
|
|
262
|
-
targetFiles: [targetFile],
|
|
263
|
-
confidence: numberOr(collection.confidence, 0.7),
|
|
264
|
-
sourceCount: asArray(collection.sourceRoutes).length,
|
|
265
|
-
generatedCount,
|
|
266
|
-
importedCount: 0,
|
|
267
|
-
skipped: [],
|
|
268
|
-
notes: ['Seed entries inferred from crawled pages. Replace with first-party exports when available.'],
|
|
269
|
-
};
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function normalizeUrlMedia(projectRoot) {
|
|
274
|
-
const mediaMapPath = path.join(projectRoot, '.fimo/migrations/current/media/map.json');
|
|
275
|
-
const mediaMap = readJsonIfExists(mediaMapPath);
|
|
276
|
-
return asArray(mediaMap).map((asset, index) => {
|
|
277
|
-
const upload = unwrapData(asset.upload);
|
|
278
|
-
return {
|
|
279
|
-
id: `media:${asset.sourceUrl ?? asset.localPath ?? index + 1}`,
|
|
280
|
-
status: asset.status === 'downloaded' && upload?.id ? 'imported' : normalizeStatus(asset.status, 'discovered'),
|
|
281
|
-
sourceUrl: asset.sourceUrl ?? null,
|
|
282
|
-
sourcePath: asset.localPath ?? null,
|
|
283
|
-
targetAssetId: upload?.id ?? null,
|
|
284
|
-
targetUrl: upload?.url ?? null,
|
|
285
|
-
fileName: asset.fileName ?? null,
|
|
286
|
-
contentType: asset.contentType ?? null,
|
|
287
|
-
bytes: numberOrNull(asset.bytes),
|
|
288
|
-
alt: asset.alt ?? '',
|
|
289
|
-
referencedBy: [],
|
|
290
|
-
uploadError: asset.error ?? null,
|
|
291
|
-
sourceFiles: compact([asset.localPath]),
|
|
292
|
-
sourceUrls: compact([asset.sourceUrl]),
|
|
293
|
-
targetFiles: [],
|
|
294
|
-
confidence: 0.8,
|
|
295
|
-
notes: [],
|
|
296
|
-
};
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function normalizeSourceForms(forms) {
|
|
301
|
-
return asArray(forms).map((form, index) => ({
|
|
302
|
-
...baseItem(form, `form-${index + 1}`),
|
|
303
|
-
id: form.id ?? form.name ?? `form-${index + 1}`,
|
|
304
|
-
name: form.name ?? form.id ?? `form-${index + 1}`,
|
|
305
|
-
status: normalizeStatus(form.status, 'planned'),
|
|
306
|
-
classification: form.classification ?? 'fimo-native',
|
|
307
|
-
action: form.action ?? null,
|
|
308
|
-
method: form.method ?? null,
|
|
309
|
-
fields: form.fields ?? [],
|
|
310
|
-
unsupportedReason: form.unsupportedReason ?? null,
|
|
311
|
-
}));
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function normalizeUrlForms(forms) {
|
|
315
|
-
return asArray(forms).map((form) => ({
|
|
316
|
-
id: `form:${form.route}:${form.name}`,
|
|
317
|
-
name: form.name,
|
|
318
|
-
status: 'generated',
|
|
319
|
-
classification: form.replacement === 'fimo-form' ? 'fimo-native' : 'unsupported',
|
|
320
|
-
sourceFiles: [],
|
|
321
|
-
sourceUrls: compact([form.route ? `route:${form.route}` : null]),
|
|
322
|
-
targetFiles: [`src/forms/${form.name}.json`],
|
|
323
|
-
confidence: numberOr(form.confidence, 0.7),
|
|
324
|
-
action: form.action ?? null,
|
|
325
|
-
method: form.method ?? null,
|
|
326
|
-
fields: form.fields ?? [],
|
|
327
|
-
unsupportedReason: null,
|
|
328
|
-
notes: form.action ? ['Original form action requires review before live parity is claimed.'] : [],
|
|
329
|
-
}));
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function normalizeSeo(urlLedger, projectRoot) {
|
|
333
|
-
const configPath = resolveProjectConfigPath(projectRoot);
|
|
334
|
-
return asArray(urlLedger?.pages).map((page) => ({
|
|
335
|
-
id: `seo:${page.route}`,
|
|
336
|
-
status: 'discovered',
|
|
337
|
-
route: page.route,
|
|
338
|
-
sourceUrls: compact([page.url]),
|
|
339
|
-
sourceFiles: compact([page.artifacts?.html]),
|
|
340
|
-
targetFiles: [configPath],
|
|
341
|
-
confidence: numberOr(page.confidence, 0.7),
|
|
342
|
-
title: page.seo?.title ?? page.title ?? null,
|
|
343
|
-
description: page.seo?.description ?? null,
|
|
344
|
-
notes: [],
|
|
345
|
-
}));
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function resolveProjectConfigPath(projectRoot) {
|
|
349
|
-
if (fs.existsSync(path.join(projectRoot, '.fimo/project.json'))) {
|
|
350
|
-
return '.fimo/config.json';
|
|
351
|
-
}
|
|
352
|
-
if (fs.existsSync(path.join(projectRoot, '.fimo.settings.json'))) {
|
|
353
|
-
if (fs.existsSync(path.join(projectRoot, 'fimo-config.json'))) {
|
|
354
|
-
return 'fimo-config.json';
|
|
355
|
-
}
|
|
356
|
-
if (fs.existsSync(path.join(projectRoot, 'fimo.config.json'))) {
|
|
357
|
-
return 'fimo.config.json';
|
|
358
|
-
}
|
|
359
|
-
return 'fimo-config.json';
|
|
360
|
-
}
|
|
361
|
-
if (
|
|
362
|
-
fs.existsSync(path.join(projectRoot, '.fimo/config.json')) ||
|
|
363
|
-
fs.existsSync(path.join(projectRoot, '.fimo/agents'))
|
|
364
|
-
) {
|
|
365
|
-
return '.fimo/config.json';
|
|
366
|
-
}
|
|
367
|
-
if (fs.existsSync(path.join(projectRoot, 'fimo-config.json'))) {
|
|
368
|
-
return 'fimo-config.json';
|
|
369
|
-
}
|
|
370
|
-
if (fs.existsSync(path.join(projectRoot, 'fimo.config.json'))) {
|
|
371
|
-
return 'fimo.config.json';
|
|
372
|
-
}
|
|
373
|
-
return '.fimo/config.json';
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function normalizeUnresolved(items) {
|
|
377
|
-
return asArray(items).map((item, index) => {
|
|
378
|
-
if (typeof item === 'string') {
|
|
379
|
-
return unresolvedItem(`source-unresolved-${index + 1}`, 'general', item);
|
|
380
|
-
}
|
|
381
|
-
return {
|
|
382
|
-
...baseItem(item, `source-unresolved-${index + 1}`),
|
|
383
|
-
id: item.id ?? `source-unresolved-${index + 1}`,
|
|
384
|
-
area: item.area ?? 'general',
|
|
385
|
-
severity: item.severity ?? 'medium',
|
|
386
|
-
status: 'unresolved',
|
|
387
|
-
message: item.message ?? item.reason ?? 'Unresolved migration item.',
|
|
388
|
-
suggestedAction: item.suggestedAction ?? 'Review with the site owner.',
|
|
389
|
-
};
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function normalizeUrlUnresolved(items) {
|
|
394
|
-
return asArray(items).map((message, index) =>
|
|
395
|
-
unresolvedItem(`url-unresolved-${index + 1}`, inferArea(message), String(message))
|
|
396
|
-
);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function deriveUnresolvedFromUrlLedger(urlLedger) {
|
|
400
|
-
if (!urlLedger) return [];
|
|
401
|
-
const unresolved = [];
|
|
402
|
-
if (urlLedger.crawl?.failures?.length > 0) {
|
|
403
|
-
unresolved.push(
|
|
404
|
-
unresolvedItem(
|
|
405
|
-
'crawl-failures',
|
|
406
|
-
'routes',
|
|
407
|
-
`${urlLedger.crawl.failures.length} route(s) failed during crawl.`,
|
|
408
|
-
'Inspect crawl.failures and re-run with browser mode, credentials, or a larger route list.'
|
|
409
|
-
)
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
if (urlLedger.assets?.failed > 0) {
|
|
413
|
-
unresolved.push(
|
|
414
|
-
unresolvedItem(
|
|
415
|
-
'media-download-failures',
|
|
416
|
-
'media',
|
|
417
|
-
`${urlLedger.assets.failed} media asset(s) failed to download.`,
|
|
418
|
-
'Re-run media collection, provide local exports, or mark affected references unresolved.'
|
|
419
|
-
)
|
|
420
|
-
);
|
|
421
|
-
}
|
|
422
|
-
return unresolved;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function normalizeItems(items, prefix) {
|
|
426
|
-
return asArray(items).map((item, index) => ({
|
|
427
|
-
...baseItem(item, `${prefix}-${index + 1}`),
|
|
428
|
-
id: item?.id ?? item?.uid ?? item?.name ?? `${prefix}-${index + 1}`,
|
|
429
|
-
status: normalizeStatus(item?.status, 'discovered'),
|
|
430
|
-
}));
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
function baseItem(item, fallbackId) {
|
|
434
|
-
return {
|
|
435
|
-
id: item?.id ?? fallbackId,
|
|
436
|
-
status: normalizeStatus(item?.status, 'discovered'),
|
|
437
|
-
sourceFiles: asArray(item?.sourceFiles),
|
|
438
|
-
sourceUrls: asArray(item?.sourceUrls),
|
|
439
|
-
targetFiles: asArray(item?.targetFiles),
|
|
440
|
-
confidence: numberOr(item?.confidence, 0.7),
|
|
441
|
-
notes: asArray(item?.notes),
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function preservePreviousProgress(manifest, previous) {
|
|
446
|
-
if (!previous) return manifest;
|
|
447
|
-
const sections = [
|
|
448
|
-
'routes',
|
|
449
|
-
'contentTypes',
|
|
450
|
-
'entries',
|
|
451
|
-
'media',
|
|
452
|
-
'forms',
|
|
453
|
-
'redirects',
|
|
454
|
-
'locales',
|
|
455
|
-
'seo',
|
|
456
|
-
'unsupported',
|
|
457
|
-
'unresolved',
|
|
458
|
-
];
|
|
459
|
-
const next = { ...manifest };
|
|
460
|
-
for (const section of sections) {
|
|
461
|
-
next[section] = preserveSection(manifest[section], previous[section]);
|
|
462
|
-
}
|
|
463
|
-
return next;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
function preserveSection(items, previousItems) {
|
|
467
|
-
const previousById = new Map(asArray(previousItems).map((item) => [item.id, item]));
|
|
468
|
-
const seen = new Set();
|
|
469
|
-
const merged = asArray(items).map((item) => {
|
|
470
|
-
seen.add(item.id);
|
|
471
|
-
const previous = previousById.get(item.id);
|
|
472
|
-
if (!previous) return item;
|
|
473
|
-
return {
|
|
474
|
-
...previous,
|
|
475
|
-
...item,
|
|
476
|
-
status: mostAdvancedStatus(previous.status, item.status),
|
|
477
|
-
notes: uniqueArray([...asArray(previous.notes), ...asArray(item.notes)]),
|
|
478
|
-
};
|
|
479
|
-
});
|
|
480
|
-
for (const previous of previousById.values()) {
|
|
481
|
-
if (!seen.has(previous.id)) {
|
|
482
|
-
merged.push(previous);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
return merged;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
function mostAdvancedStatus(a, b) {
|
|
489
|
-
const aRank = STATUS_ORDER.get(a) ?? 0;
|
|
490
|
-
const bRank = STATUS_ORDER.get(b) ?? 0;
|
|
491
|
-
return aRank >= bRank ? a : b;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
function mergeItems(items) {
|
|
495
|
-
const byId = new Map();
|
|
496
|
-
for (const item of items.filter(Boolean)) {
|
|
497
|
-
if (!byId.has(item.id)) {
|
|
498
|
-
byId.set(item.id, item);
|
|
499
|
-
continue;
|
|
500
|
-
}
|
|
501
|
-
const previous = byId.get(item.id);
|
|
502
|
-
byId.set(item.id, {
|
|
503
|
-
...previous,
|
|
504
|
-
...item,
|
|
505
|
-
sourceFiles: uniqueArray([...asArray(previous.sourceFiles), ...asArray(item.sourceFiles)]),
|
|
506
|
-
sourceUrls: uniqueArray([...asArray(previous.sourceUrls), ...asArray(item.sourceUrls)]),
|
|
507
|
-
targetFiles: uniqueArray([...asArray(previous.targetFiles), ...asArray(item.targetFiles)]),
|
|
508
|
-
notes: uniqueArray([...asArray(previous.notes), ...asArray(item.notes)]),
|
|
509
|
-
status: mostAdvancedStatus(previous.status, item.status),
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
return [...byId.values()];
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
function unresolvedItem(
|
|
516
|
-
id,
|
|
517
|
-
area,
|
|
518
|
-
message,
|
|
519
|
-
suggestedAction = 'Review and decide whether to migrate, ignore, or replace this item.'
|
|
520
|
-
) {
|
|
521
|
-
return {
|
|
522
|
-
id,
|
|
523
|
-
area,
|
|
524
|
-
severity: 'medium',
|
|
525
|
-
status: 'unresolved',
|
|
526
|
-
message,
|
|
527
|
-
suggestedAction,
|
|
528
|
-
sourceFiles: [],
|
|
529
|
-
sourceUrls: [],
|
|
530
|
-
targetFiles: [],
|
|
531
|
-
confidence: 1,
|
|
532
|
-
notes: [],
|
|
533
|
-
};
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function inferArea(message) {
|
|
537
|
-
const text = String(message).toLowerCase();
|
|
538
|
-
if (text.includes('form')) return 'forms';
|
|
539
|
-
if (text.includes('media') || text.includes('image') || text.includes('asset')) return 'media';
|
|
540
|
-
if (text.includes('crawl') || text.includes('route') || text.includes('page')) return 'routes';
|
|
541
|
-
if (text.includes('cms') || text.includes('collection') || text.includes('entry')) return 'cms';
|
|
542
|
-
return 'general';
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
function readJsonIfExists(filePath) {
|
|
546
|
-
if (!fs.existsSync(filePath)) return null;
|
|
547
|
-
try {
|
|
548
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
549
|
-
} catch (error) {
|
|
550
|
-
fail(`Could not parse ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function unwrapData(value) {
|
|
555
|
-
if (!value || typeof value !== 'object') return null;
|
|
556
|
-
return value.data && typeof value.data === 'object' ? value.data : value;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
function normalizeStatus(status, fallback) {
|
|
560
|
-
if (!status || typeof status !== 'string') return fallback;
|
|
561
|
-
if (status === 'downloaded') return 'generated';
|
|
562
|
-
if (status === 'upload-failed' || status === 'failed') return 'unresolved';
|
|
563
|
-
return STATUS_ORDER.has(status) ? status : fallback;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function numberOr(value, fallback) {
|
|
567
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
function numberOrNull(value) {
|
|
571
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
function asArray(value) {
|
|
575
|
-
return Array.isArray(value) ? value : [];
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function compact(values) {
|
|
579
|
-
return values.filter((value) => value !== null && value !== undefined && value !== '');
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
function uniqueArray(values) {
|
|
583
|
-
return [...new Set(compact(values))];
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
function toPosix(value) {
|
|
587
|
-
return value.split(path.sep).join('/');
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
function fail(message) {
|
|
591
|
-
console.error(message);
|
|
592
|
-
process.exit(1);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
main();
|