@xtrable-ltd/nanoesis 0.1.8 → 0.1.10
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/dist/adapter-azure-blob.d.ts +7 -1
- package/dist/adapter-azure-blob.js +2 -2
- package/dist/{chunk-J6VYOB47.js → chunk-UZQ7UP2B.js} +1678 -1298
- package/dist/editor-api.d.ts +39 -2
- package/dist/editor-api.js +393 -4
- package/dist/index.d.ts +371 -165
- package/dist/index.js +21 -1
- package/editor/assets/MigrationsPane-BAHPPSXP.css +1 -0
- package/editor/assets/MigrationsPane-_FGonx4-.js +4 -0
- package/editor/assets/{TemplatesPane-B4_sg2u5.css → TemplatesPane-CiLiMCc8.css} +1 -1
- package/editor/assets/{TemplatesPane-4IAoeX4-.js → TemplatesPane-Z6Bn69Hb.js} +204 -202
- package/editor/assets/{cssMode-BhSmGQp_.js → cssMode-dkQrIPWx.js} +1 -1
- package/editor/assets/{freemarker2-Z1jVSRUs.js → freemarker2-DEqcFFWa.js} +1 -1
- package/editor/assets/{handlebars-C3kew8-P.js → handlebars-C6ojANWr.js} +1 -1
- package/editor/assets/{html-RVg2mWQY.js → html-BmiAmVUD.js} +1 -1
- package/editor/assets/{htmlMode-ljHXud-Y.js → htmlMode-BBmUqToI.js} +1 -1
- package/editor/assets/{index-R39CtpUa.css → index-DEz8GUII.css} +1 -1
- package/editor/assets/index-LtCzUHAw.js +138 -0
- package/editor/assets/{javascript-CAr3NHzi.js → javascript-Cxm2TfJy.js} +1 -1
- package/editor/assets/{jsonMode-Cq4fxtNe.js → jsonMode-CW5012Hx.js} +1 -1
- package/editor/assets/{liquid-DeSAzZOT.js → liquid-DrS7ilHv.js} +1 -1
- package/editor/assets/{mdx-C4Ynnq4H.js → mdx-CwdSU5o1.js} +1 -1
- package/editor/assets/{python-rTSPH-tA.js → python-CALCR0yC.js} +1 -1
- package/editor/assets/{razor-B-0Do2A9.js → razor-SVCo2LoM.js} +1 -1
- package/editor/assets/{tsMode-m4eOcwoz.js → tsMode-CzXfTR_Q.js} +1 -1
- package/editor/assets/{typescript-CdvinFng.js → typescript-CP0Ovrv7.js} +1 -1
- package/editor/assets/{xml-DuAYX2gv.js → xml-B2yqloTa.js} +1 -1
- package/editor/assets/{yaml-BWplD8Hf.js → yaml-DTLJhzgY.js} +1 -1
- package/editor/index.html +2 -2
- package/package.json +1 -1
- package/editor/assets/index-CLoI_HF2.js +0 -134
package/dist/editor-api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WorkingStore, IdentityProvider, PublishResult, ReconcileResult, AuthEndpoints, UserAdminEndpoints, AuthorOption, DiagnosticRegistry, UserSummary, AuthorDirectory,
|
|
1
|
+
import { WorkingStore, IdentityProvider, PublishResult, ReconcileResult, AuthEndpoints, UserAdminEndpoints, AuthorOption, DiagnosticRegistry, UserSummary, AuthorDirectory, Repair, DiagnosticCheck } from '@nanoesis/engine';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Editor white-labelling storage (DESIGN §11, Phase B). This is the *editor app's*
|
|
@@ -285,5 +285,42 @@ declare const recreateHomeTemplateRepair: Repair;
|
|
|
285
285
|
* user picks what to publish; we only point out that nothing is.
|
|
286
286
|
*/
|
|
287
287
|
declare const noPublishedContentDiagnostic: DiagnosticCheck;
|
|
288
|
+
/**
|
|
289
|
+
* Walk every published content item; for any pinned to a snapshot, check whether the
|
|
290
|
+
* snapshot file exists and parses (PROPOSAL §16.1, §16.2). Emits one finding per
|
|
291
|
+
* affected item — the missing-snapshot case (publish-blocking error) and the
|
|
292
|
+
* corrupt-snapshot case (publish-blocking error, distinct headline so the user knows
|
|
293
|
+
* "the file is there but the bytes are bad" vs "the file is gone"). Both repair via
|
|
294
|
+
* `templates.rebind-item-to-current`.
|
|
295
|
+
*/
|
|
296
|
+
declare const templateSnapshotIntegrityDiagnostic: DiagnosticCheck;
|
|
297
|
+
/**
|
|
298
|
+
* Rebind an item's `template` field from `<name>@v<N>` to `<name>` (current). Used for
|
|
299
|
+
* the missing/corrupt snapshot repair: the item's pin is lost (the snapshot was the
|
|
300
|
+
* intent), but the item itself stays, and shows up in the pending-migrations list so
|
|
301
|
+
* its fields can be reconciled with current. Idempotent: a write of the same JSON is
|
|
302
|
+
* a no-op semantically.
|
|
303
|
+
*/
|
|
304
|
+
declare const rebindItemToCurrentRepair: Repair;
|
|
305
|
+
/**
|
|
306
|
+
* Snapshot exists but its current sibling is missing (PROPOSAL §16.4). Someone deleted
|
|
307
|
+
* `templates/article.html` and left `templates/article@v1.html` behind. The Health page
|
|
308
|
+
* surfaces this with the `copy-snapshot-to-current` repair — promotes the snapshot to
|
|
309
|
+
* be the current.
|
|
310
|
+
*/
|
|
311
|
+
declare const templateSuffixConflictDiagnostic: DiagnosticCheck;
|
|
312
|
+
/**
|
|
313
|
+
* Copy a snapshot's bytes to the current path (PROPOSAL §16.4 repair). The snapshot
|
|
314
|
+
* itself remains in place; the user can retire it manually later. Idempotent: if the
|
|
315
|
+
* current already exists, the repair is a no-op (so a stale UI re-firing the repair
|
|
316
|
+
* never silently overwrites real work).
|
|
317
|
+
*/
|
|
318
|
+
declare const copySnapshotToCurrentRepair: Repair;
|
|
319
|
+
/**
|
|
320
|
+
* Count items whose current-template binding has orphan fields (PROPOSAL §10). Read-only
|
|
321
|
+
* summary — points the user at the Migrations workspace (Phase 2). No auto-repair, since
|
|
322
|
+
* resolution is per-item (drop / rename / keep) and needs human choice.
|
|
323
|
+
*/
|
|
324
|
+
declare const pendingMigrationsDiagnostic: DiagnosticCheck;
|
|
288
325
|
|
|
289
|
-
export { type ApiDeps, type ApiRequest, type ApiResponse, type BrandingLogo, type BrandingLogoMeta, type BrandingState, type BrandingStore, FileBrandingStore, InMemoryBrandingStore, MCP_RESOURCES, MCP_TOOLS, type McpCallOptions, type McpResourceDef, type McpToolDef, type McpToolResult, SCAFFOLD_FILES, authorDirectory, authorOptions, buildDefaultDiagnostics, callMcpTool, handleApi, homeTemplateMissingDiagnostic, noPublishedContentDiagnostic, readMcpResource, recreateHomeTemplateRepair };
|
|
326
|
+
export { type ApiDeps, type ApiRequest, type ApiResponse, type BrandingLogo, type BrandingLogoMeta, type BrandingState, type BrandingStore, FileBrandingStore, InMemoryBrandingStore, MCP_RESOURCES, MCP_TOOLS, type McpCallOptions, type McpResourceDef, type McpToolDef, type McpToolResult, SCAFFOLD_FILES, authorDirectory, authorOptions, buildDefaultDiagnostics, callMcpTool, copySnapshotToCurrentRepair, handleApi, homeTemplateMissingDiagnostic, noPublishedContentDiagnostic, pendingMigrationsDiagnostic, readMcpResource, rebindItemToCurrentRepair, recreateHomeTemplateRepair, templateSnapshotIntegrityDiagnostic, templateSuffixConflictDiagnostic };
|
package/dist/editor-api.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
|
+
analyzeTemplate,
|
|
3
|
+
applyMigration,
|
|
4
|
+
baseTemplateName,
|
|
2
5
|
buildAuthoringReference,
|
|
3
6
|
canEdit,
|
|
4
7
|
contentTypeFor,
|
|
5
8
|
createDiagnosticRegistry,
|
|
6
9
|
deriveFields,
|
|
7
10
|
hasRole,
|
|
11
|
+
isVersionedTemplateName,
|
|
8
12
|
loadComponents,
|
|
9
13
|
loadTemplate,
|
|
14
|
+
pendingMigrations,
|
|
10
15
|
renderReferenceMarkdown,
|
|
11
16
|
validateSite,
|
|
12
17
|
workingStoreRoundTripDiagnostic
|
|
13
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-UZQ7UP2B.js";
|
|
14
19
|
|
|
15
20
|
// ../editor-api/src/scaffold.ts
|
|
16
21
|
var HOME_HTML = `<!doctype html>
|
|
@@ -254,6 +259,128 @@ async function handleDiagnosticsRoute(deps, req, caller) {
|
|
|
254
259
|
return json(400, { ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
255
260
|
}
|
|
256
261
|
}
|
|
262
|
+
async function handleMigrationsRoute(deps, req, caller) {
|
|
263
|
+
if (req.path === "/api/migrations") {
|
|
264
|
+
if (req.method !== "GET") return json(405, { ok: false, error: "method not allowed" });
|
|
265
|
+
const result = await pendingMigrations(deps.store);
|
|
266
|
+
return json(200, {
|
|
267
|
+
items: result.items,
|
|
268
|
+
byTemplate: Object.fromEntries(result.byTemplate)
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
if (req.path === "/api/migrations/preview") {
|
|
272
|
+
if (req.method !== "GET") return json(405, { ok: false, error: "method not allowed" });
|
|
273
|
+
const itemPath2 = req.query.get("path") ?? "";
|
|
274
|
+
if (itemPath2 === "") return json(400, { ok: false, error: "path query param required" });
|
|
275
|
+
return previewMigration(deps, itemPath2);
|
|
276
|
+
}
|
|
277
|
+
if (req.method !== "POST") return json(405, { ok: false, error: "method not allowed" });
|
|
278
|
+
void caller;
|
|
279
|
+
const body = await parseJsonBody(req);
|
|
280
|
+
if (!isObject(body) || typeof body["path"] !== "string" || !isObject(body["resolution"])) {
|
|
281
|
+
return json(400, { ok: false, error: "JSON body { path, resolution } required" });
|
|
282
|
+
}
|
|
283
|
+
const itemPath = body["path"];
|
|
284
|
+
const denied = denyWrite(caller, itemPath);
|
|
285
|
+
if (denied) return denied;
|
|
286
|
+
try {
|
|
287
|
+
await applyMigration(deps.store, itemPath, normalizeResolution(body["resolution"]));
|
|
288
|
+
const refreshed = await pendingMigrations(deps.store);
|
|
289
|
+
return json(200, {
|
|
290
|
+
ok: true,
|
|
291
|
+
items: refreshed.items,
|
|
292
|
+
byTemplate: Object.fromEntries(refreshed.byTemplate)
|
|
293
|
+
});
|
|
294
|
+
} catch (error) {
|
|
295
|
+
return json(400, { ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async function previewMigration(deps, itemPath) {
|
|
299
|
+
let itemJson;
|
|
300
|
+
try {
|
|
301
|
+
itemJson = JSON.parse(await deps.store.readText(itemPath));
|
|
302
|
+
} catch {
|
|
303
|
+
return json(404, { ok: false, error: "item not found" });
|
|
304
|
+
}
|
|
305
|
+
const tplName = typeof itemJson["template"] === "string" ? itemJson["template"] : "";
|
|
306
|
+
if (tplName === "") return json(400, { ok: false, error: "item has no template binding" });
|
|
307
|
+
const baseTpl = tplName.replace(/@v\d+$/, "");
|
|
308
|
+
const components = await loadComponents(deps.store).catch(() => /* @__PURE__ */ new Map());
|
|
309
|
+
const currentHtml = await safeReadTemplate(deps.store, baseTpl);
|
|
310
|
+
const currentFields = currentHtml === null ? [] : deriveFields(currentHtml, components).map((f) => f.name);
|
|
311
|
+
let leftTpl = null;
|
|
312
|
+
let leftHtml = null;
|
|
313
|
+
if (tplName !== baseTpl) {
|
|
314
|
+
leftTpl = tplName;
|
|
315
|
+
leftHtml = await safeReadTemplate(deps.store, tplName);
|
|
316
|
+
} else {
|
|
317
|
+
const { bestFitSnapshot: pickBestFit } = await import("./index.js");
|
|
318
|
+
const itemFields2 = Object.keys(
|
|
319
|
+
typeof itemJson["fields"] === "object" && itemJson["fields"] !== null ? itemJson["fields"] : {}
|
|
320
|
+
);
|
|
321
|
+
const best = await pickBestFit(deps.store, baseTpl, itemFields2);
|
|
322
|
+
if (best !== null) {
|
|
323
|
+
leftTpl = best;
|
|
324
|
+
leftHtml = await safeReadTemplate(deps.store, best);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const measureTpl = tplName === baseTpl ? baseTpl : tplName;
|
|
328
|
+
const measureHtml = tplName === baseTpl ? currentHtml : leftHtml;
|
|
329
|
+
const measureFields = measureHtml === null ? [] : deriveFields(measureHtml, components).map((f) => f.name);
|
|
330
|
+
const itemFields = typeof itemJson["fields"] === "object" && itemJson["fields"] !== null ? itemJson["fields"] : {};
|
|
331
|
+
const orphans = Object.keys(itemFields).filter((name) => !measureFields.includes(name)).map((name) => ({ name, value: itemFields[name] }));
|
|
332
|
+
void measureTpl;
|
|
333
|
+
return json(200, {
|
|
334
|
+
left: leftHtml === null ? null : { template: leftTpl, html: leftHtml },
|
|
335
|
+
right: { template: baseTpl, html: currentHtml ?? "" },
|
|
336
|
+
orphans,
|
|
337
|
+
missing: [],
|
|
338
|
+
currentTemplateFields: currentFields
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
async function safeReadTemplate(store, name) {
|
|
342
|
+
try {
|
|
343
|
+
return await store.readText(`templates/${name}.html`);
|
|
344
|
+
} catch {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function normalizeResolution(raw) {
|
|
349
|
+
const drop = Array.isArray(raw["drop"]) ? raw["drop"].filter((v) => typeof v === "string") : [];
|
|
350
|
+
const keep = Array.isArray(raw["keep"]) ? raw["keep"].filter((v) => typeof v === "string") : [];
|
|
351
|
+
const rename2 = {};
|
|
352
|
+
if (isObject(raw["rename"])) {
|
|
353
|
+
for (const [from, to] of Object.entries(raw["rename"])) {
|
|
354
|
+
if (typeof to === "string") rename2[from] = to;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const fill = {};
|
|
358
|
+
if (isObject(raw["fill"])) {
|
|
359
|
+
for (const [name, value] of Object.entries(raw["fill"])) {
|
|
360
|
+
if (typeof value === "string") fill[name] = value;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return { drop, rename: rename2, keep, fill };
|
|
364
|
+
}
|
|
365
|
+
async function handleStampRoute(deps, req, caller) {
|
|
366
|
+
if (req.method !== "POST") return json(405, { ok: false, error: "method not allowed" });
|
|
367
|
+
if (!hasRole(caller, "developer")) {
|
|
368
|
+
return json(403, { ok: false, error: "developer role required" });
|
|
369
|
+
}
|
|
370
|
+
const body = await parseJsonBody(req);
|
|
371
|
+
if (!isObject(body) || typeof body["name"] !== "string" || body["kind"] !== "template" && body["kind"] !== "component") {
|
|
372
|
+
return json(400, {
|
|
373
|
+
ok: false,
|
|
374
|
+
error: 'JSON body { name, kind: "template" | "component" } required'
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
const record = await deps.store.stamp(body["name"], body["kind"]);
|
|
379
|
+
return json(200, record);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
return json(400, { ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
257
384
|
async function handleBrandingRoute(deps, req) {
|
|
258
385
|
if (req.path !== "/api/branding" && req.path !== "/api/branding/logo") return void 0;
|
|
259
386
|
const store = deps.branding;
|
|
@@ -322,6 +449,12 @@ async function dispatchApi(deps, req) {
|
|
|
322
449
|
if (req.path === "/api/admin/diagnostics" || req.path === "/api/admin/diagnostics/repair") {
|
|
323
450
|
return handleDiagnosticsRoute(deps, req, principal);
|
|
324
451
|
}
|
|
452
|
+
if (req.path === "/api/migrations" || req.path === "/api/migrations/preview" || req.path === "/api/migrations/apply") {
|
|
453
|
+
return handleMigrationsRoute(deps, req, principal);
|
|
454
|
+
}
|
|
455
|
+
if (req.path === "/api/templates/stamp") {
|
|
456
|
+
return handleStampRoute(deps, req, principal);
|
|
457
|
+
}
|
|
325
458
|
const get = (key) => req.query.get(key) ?? "";
|
|
326
459
|
switch (req.path) {
|
|
327
460
|
case "/api/list": {
|
|
@@ -362,8 +495,12 @@ async function dispatchApi(deps, req) {
|
|
|
362
495
|
const denied = denyWrite(principal, get("path"));
|
|
363
496
|
if (denied) return denied;
|
|
364
497
|
try {
|
|
365
|
-
await deps.store.write(get("path"), await req.body());
|
|
366
|
-
return json(200, {
|
|
498
|
+
const result = await deps.store.write(get("path"), await req.body());
|
|
499
|
+
return json(200, {
|
|
500
|
+
ok: true,
|
|
501
|
+
...result.stamped !== void 0 && { stamped: result.stamped },
|
|
502
|
+
...result.stampIncomplete === true && { stampIncomplete: true }
|
|
503
|
+
});
|
|
367
504
|
} catch (error) {
|
|
368
505
|
return json(500, { ok: false, error: String(error) });
|
|
369
506
|
}
|
|
@@ -610,8 +747,59 @@ var TOOL_SPECS = [
|
|
|
610
747
|
description: "Compile and publish the whole site. Runs the validation gate first; if anything would break, it reports the problems and writes nothing.",
|
|
611
748
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
612
749
|
toRequest: (_args, token) => request("POST", "/api/publish", {}, void 0, token)
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
name: "list_pending_migrations",
|
|
753
|
+
description: "List content items whose JSON fields don't match their bound template's schema (orphan fields or missing required fields). Use this after a destructive template edit (you'll see a `stamped` record on write_file) to see what needs migrating, or any time to check site-wide drift.",
|
|
754
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
755
|
+
toRequest: (_args, token) => request("GET", "/api/migrations", {}, void 0, token)
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
name: "preview_migration",
|
|
759
|
+
description: 'For one content item, return the current template HTML, the best-fit snapshot HTML (the "before"), the list of orphan fields with their values, and the names of fields the current template defines. Use this to plan how to migrate (drop, rename, or keep each orphan).',
|
|
760
|
+
inputSchema: {
|
|
761
|
+
type: "object",
|
|
762
|
+
properties: { path: pathArg },
|
|
763
|
+
required: ["path"],
|
|
764
|
+
additionalProperties: false
|
|
765
|
+
},
|
|
766
|
+
toRequest: (args, token) => request("GET", "/api/migrations/preview", { path: str(args, "path") }, void 0, token)
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
name: "apply_migration",
|
|
770
|
+
description: "Resolve a content item's schema drift in one go. `resolution` is a JSON object: `drop` (array of orphan field names to delete), `rename` (object mapping orphan field name \u2192 current template field name to copy the value into), `keep` (array of orphan field names to leave untouched), and `fill` (object mapping current template field name \u2192 value, for missing required fields). Returns the refreshed pending list.",
|
|
771
|
+
inputSchema: {
|
|
772
|
+
type: "object",
|
|
773
|
+
properties: {
|
|
774
|
+
path: pathArg,
|
|
775
|
+
resolution: {
|
|
776
|
+
type: "string",
|
|
777
|
+
description: 'A JSON object encoded as a string: { "drop": string[], "rename": { from: to }, "keep": string[], "fill": { name: value } }.'
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
required: ["path", "resolution"],
|
|
781
|
+
additionalProperties: false
|
|
782
|
+
},
|
|
783
|
+
toRequest: (args, token) => request(
|
|
784
|
+
"POST",
|
|
785
|
+
"/api/migrations/apply",
|
|
786
|
+
{},
|
|
787
|
+
JSON.stringify({
|
|
788
|
+
path: str(args, "path"),
|
|
789
|
+
resolution: parseResolutionArg(str(args, "resolution"))
|
|
790
|
+
}),
|
|
791
|
+
token
|
|
792
|
+
)
|
|
613
793
|
}
|
|
614
794
|
];
|
|
795
|
+
function parseResolutionArg(raw) {
|
|
796
|
+
if (raw === "") return {};
|
|
797
|
+
try {
|
|
798
|
+
return JSON.parse(raw);
|
|
799
|
+
} catch {
|
|
800
|
+
return {};
|
|
801
|
+
}
|
|
802
|
+
}
|
|
615
803
|
var MCP_TOOLS = TOOL_SPECS.map(
|
|
616
804
|
({ name, description, inputSchema }) => ({ name, description, inputSchema })
|
|
617
805
|
);
|
|
@@ -646,7 +834,12 @@ function buildDefaultDiagnostics() {
|
|
|
646
834
|
registry.add(workingStoreRoundTripDiagnostic);
|
|
647
835
|
registry.add(homeTemplateMissingDiagnostic);
|
|
648
836
|
registry.add(noPublishedContentDiagnostic);
|
|
837
|
+
registry.add(templateSnapshotIntegrityDiagnostic);
|
|
838
|
+
registry.add(templateSuffixConflictDiagnostic);
|
|
839
|
+
registry.add(pendingMigrationsDiagnostic);
|
|
649
840
|
registry.addRepair(recreateHomeTemplateRepair);
|
|
841
|
+
registry.addRepair(rebindItemToCurrentRepair);
|
|
842
|
+
registry.addRepair(copySnapshotToCurrentRepair);
|
|
650
843
|
return registry;
|
|
651
844
|
}
|
|
652
845
|
var HOME_PATH = "templates/home.html";
|
|
@@ -690,6 +883,197 @@ var noPublishedContentDiagnostic = {
|
|
|
690
883
|
];
|
|
691
884
|
}
|
|
692
885
|
};
|
|
886
|
+
var templateSnapshotIntegrityDiagnostic = {
|
|
887
|
+
id: "templates.snapshot-integrity",
|
|
888
|
+
async run({ store }) {
|
|
889
|
+
const findings = [];
|
|
890
|
+
const components = await loadComponents(store).catch(() => /* @__PURE__ */ new Map());
|
|
891
|
+
const items = await collectContentItems(store);
|
|
892
|
+
const snapshotCache = /* @__PURE__ */ new Map();
|
|
893
|
+
for (const item of items) {
|
|
894
|
+
if (!isVersionedTemplateName(item.template)) continue;
|
|
895
|
+
const kind = "template";
|
|
896
|
+
const path = `${kind === "template" ? "templates" : "components"}/${item.template}.html`;
|
|
897
|
+
let state = snapshotCache.get(path);
|
|
898
|
+
if (state === void 0) {
|
|
899
|
+
state = await classifySnapshot(store, path, components);
|
|
900
|
+
snapshotCache.set(path, state);
|
|
901
|
+
}
|
|
902
|
+
if (state === "ok") continue;
|
|
903
|
+
const isMissing = state === "missing";
|
|
904
|
+
findings.push({
|
|
905
|
+
id: `templates.${isMissing ? "missing" : "corrupt"}-snapshot:${item.path}`,
|
|
906
|
+
source: "templates",
|
|
907
|
+
severity: "error",
|
|
908
|
+
title: isMissing ? `Pinned snapshot missing: ${path}` : `Pinned snapshot will not parse: ${path}`,
|
|
909
|
+
detail: isMissing ? `${item.path} pins to ${item.template}, but ${path} does not exist. Restore the snapshot from git/backup, or rebind this item to the current template (its fields will need migration after).` : `${item.path} pins to ${item.template}, but ${path} cannot be parsed as a template. Restore valid bytes from git/backup, or rebind this item to the current template.`,
|
|
910
|
+
repair: {
|
|
911
|
+
id: "templates.rebind-item-to-current",
|
|
912
|
+
label: "Rebind item to current template",
|
|
913
|
+
args: { itemPath: item.path }
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
return findings;
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
var rebindItemToCurrentRepair = {
|
|
921
|
+
id: "templates.rebind-item-to-current",
|
|
922
|
+
async run({ store }, args) {
|
|
923
|
+
const itemPath = args["itemPath"];
|
|
924
|
+
if (typeof itemPath !== "string" || itemPath === "") {
|
|
925
|
+
throw new Error("rebind-item-to-current: missing itemPath arg");
|
|
926
|
+
}
|
|
927
|
+
const text = await store.readText(itemPath);
|
|
928
|
+
const data = JSON.parse(text);
|
|
929
|
+
if (typeof data !== "object" || data === null) {
|
|
930
|
+
throw new Error(`rebind-item-to-current: ${itemPath} is not a JSON object`);
|
|
931
|
+
}
|
|
932
|
+
const obj = data;
|
|
933
|
+
if (typeof obj.template !== "string" || !isVersionedTemplateName(obj.template)) {
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
obj.template = baseTemplateName(obj.template);
|
|
937
|
+
const next = `${JSON.stringify(obj, null, 2)}
|
|
938
|
+
`;
|
|
939
|
+
await store.write(itemPath, new TextEncoder().encode(next));
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
var templateSuffixConflictDiagnostic = {
|
|
943
|
+
id: "templates.suffix-conflict",
|
|
944
|
+
async run({ store }) {
|
|
945
|
+
const findings = [];
|
|
946
|
+
for (const dir of ["templates", "components"]) {
|
|
947
|
+
let entries;
|
|
948
|
+
try {
|
|
949
|
+
entries = await store.list(dir);
|
|
950
|
+
} catch {
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
const seenBases = /* @__PURE__ */ new Set();
|
|
954
|
+
const snapshots = [];
|
|
955
|
+
for (const entry of entries) {
|
|
956
|
+
if (entry.kind !== "file" || !entry.name.endsWith(".html")) continue;
|
|
957
|
+
const stem = entry.name.slice(0, -".html".length);
|
|
958
|
+
if (isVersionedTemplateName(stem)) {
|
|
959
|
+
snapshots.push({ base: baseTemplateName(stem), snapshotPath: `${dir}/${entry.name}` });
|
|
960
|
+
} else {
|
|
961
|
+
seenBases.add(stem);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
for (const snap of snapshots) {
|
|
965
|
+
if (seenBases.has(snap.base)) continue;
|
|
966
|
+
const currentPath = `${dir}/${snap.base}.html`;
|
|
967
|
+
findings.push({
|
|
968
|
+
id: `templates.suffix-conflict:${snap.snapshotPath}`,
|
|
969
|
+
source: "templates",
|
|
970
|
+
severity: "warning",
|
|
971
|
+
title: `Snapshot has no current sibling: ${snap.snapshotPath}`,
|
|
972
|
+
detail: `${snap.snapshotPath} exists but ${currentPath} does not. Promote the snapshot to current with "Copy snapshot to current", or rename the snapshot off the @v<N> suffix if it was named this way by mistake.`,
|
|
973
|
+
repair: {
|
|
974
|
+
id: "templates.copy-snapshot-to-current",
|
|
975
|
+
label: "Copy snapshot to current",
|
|
976
|
+
args: { snapshotPath: snap.snapshotPath, currentPath }
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return findings;
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
var copySnapshotToCurrentRepair = {
|
|
985
|
+
id: "templates.copy-snapshot-to-current",
|
|
986
|
+
async run({ store }, args) {
|
|
987
|
+
const snapshotPath = args["snapshotPath"];
|
|
988
|
+
const currentPath = args["currentPath"];
|
|
989
|
+
if (typeof snapshotPath !== "string" || typeof currentPath !== "string") {
|
|
990
|
+
throw new Error("copy-snapshot-to-current: missing snapshotPath or currentPath args");
|
|
991
|
+
}
|
|
992
|
+
if (await store.exists(currentPath)) return;
|
|
993
|
+
const bytes = await store.readBytes(snapshotPath);
|
|
994
|
+
await store.write(currentPath, bytes);
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
var pendingMigrationsDiagnostic = {
|
|
998
|
+
id: "content.pending-migrations",
|
|
999
|
+
async run({ store }) {
|
|
1000
|
+
const components = await loadComponents(store).catch(() => /* @__PURE__ */ new Map());
|
|
1001
|
+
const items = await collectContentItems(store);
|
|
1002
|
+
const templateFieldCache = /* @__PURE__ */ new Map();
|
|
1003
|
+
let pendingCount = 0;
|
|
1004
|
+
for (const item of items) {
|
|
1005
|
+
if (isVersionedTemplateName(item.template)) continue;
|
|
1006
|
+
let expected = templateFieldCache.get(item.template);
|
|
1007
|
+
if (expected === void 0) {
|
|
1008
|
+
const path = `templates/${item.template}.html`;
|
|
1009
|
+
try {
|
|
1010
|
+
const html = await store.readText(path);
|
|
1011
|
+
expected = new Set(deriveFields(html, components).map((f) => f.name));
|
|
1012
|
+
} catch {
|
|
1013
|
+
expected = /* @__PURE__ */ new Set();
|
|
1014
|
+
}
|
|
1015
|
+
templateFieldCache.set(item.template, expected);
|
|
1016
|
+
}
|
|
1017
|
+
const orphans = Object.keys(item.fields).filter((k) => !expected.has(k));
|
|
1018
|
+
if (orphans.length > 0) pendingCount += 1;
|
|
1019
|
+
}
|
|
1020
|
+
if (pendingCount === 0) return [];
|
|
1021
|
+
return [
|
|
1022
|
+
{
|
|
1023
|
+
id: "content.pending-migrations",
|
|
1024
|
+
source: "content",
|
|
1025
|
+
severity: "warning",
|
|
1026
|
+
title: `${pendingCount} item${pendingCount === 1 ? "" : "s"} pending migration`,
|
|
1027
|
+
detail: `${pendingCount} content item${pendingCount === 1 ? "" : "s"} ${pendingCount === 1 ? "has" : "have"} fields not rendered by ${pendingCount === 1 ? "its" : "their"} current template. The Migrations workspace (shipping in Phase 2 of versioning) will resolve these; for now, edit the item JSON to drop or rename the orphan fields. The site continues to publish \u2014 drift is a soft warning, not a publish blocker.`
|
|
1028
|
+
}
|
|
1029
|
+
];
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
async function collectContentItems(store, dir = "content") {
|
|
1033
|
+
const out = [];
|
|
1034
|
+
let entries;
|
|
1035
|
+
try {
|
|
1036
|
+
entries = await store.list(dir);
|
|
1037
|
+
} catch {
|
|
1038
|
+
return out;
|
|
1039
|
+
}
|
|
1040
|
+
for (const entry of entries) {
|
|
1041
|
+
const path = dir === "" ? entry.name : `${dir}/${entry.name}`;
|
|
1042
|
+
if (entry.kind === "dir") {
|
|
1043
|
+
out.push(...await collectContentItems(store, path));
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
if (!entry.name.endsWith(".json") || entry.name.startsWith("_")) continue;
|
|
1047
|
+
let data;
|
|
1048
|
+
try {
|
|
1049
|
+
data = JSON.parse(await store.readText(path));
|
|
1050
|
+
} catch {
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
if (typeof data !== "object" || data === null) continue;
|
|
1054
|
+
const obj = data;
|
|
1055
|
+
const template = obj["template"];
|
|
1056
|
+
if (typeof template !== "string") continue;
|
|
1057
|
+
const fields = obj["fields"];
|
|
1058
|
+
out.push({
|
|
1059
|
+
path,
|
|
1060
|
+
template,
|
|
1061
|
+
fields: typeof fields === "object" && fields !== null ? fields : {}
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
return out;
|
|
1065
|
+
}
|
|
1066
|
+
async function classifySnapshot(store, path, components) {
|
|
1067
|
+
if (!await store.exists(path)) return "missing";
|
|
1068
|
+
try {
|
|
1069
|
+
const html = await store.readText(path);
|
|
1070
|
+
if (html.trim() === "") return "corrupt";
|
|
1071
|
+
analyzeTemplate(html, components);
|
|
1072
|
+
return "ok";
|
|
1073
|
+
} catch {
|
|
1074
|
+
return "corrupt";
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
693
1077
|
async function anyPublishedContentItem(store, dir = "content") {
|
|
694
1078
|
let entries;
|
|
695
1079
|
try {
|
|
@@ -822,9 +1206,14 @@ export {
|
|
|
822
1206
|
authorOptions,
|
|
823
1207
|
buildDefaultDiagnostics,
|
|
824
1208
|
callMcpTool,
|
|
1209
|
+
copySnapshotToCurrentRepair,
|
|
825
1210
|
handleApi,
|
|
826
1211
|
homeTemplateMissingDiagnostic,
|
|
827
1212
|
noPublishedContentDiagnostic,
|
|
1213
|
+
pendingMigrationsDiagnostic,
|
|
828
1214
|
readMcpResource,
|
|
829
|
-
|
|
1215
|
+
rebindItemToCurrentRepair,
|
|
1216
|
+
recreateHomeTemplateRepair,
|
|
1217
|
+
templateSnapshotIntegrityDiagnostic,
|
|
1218
|
+
templateSuffixConflictDiagnostic
|
|
830
1219
|
};
|