@xtrable-ltd/nanoesis 0.1.11 → 0.1.13
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.js +1 -1
- package/dist/{chunk-XO3CT6GL.js → chunk-7TB5ANIS.js} +58 -4
- package/dist/chunk-LXNOLO66.js +1224 -0
- package/dist/editor-api.js +21 -1198
- package/dist/index.d.ts +50 -12
- package/dist/index.js +1 -1
- package/dist/mcp.d.ts +48 -0
- package/dist/mcp.js +76 -0
- package/editor/assets/{MigrationsPane-BYGqWBAA.js → MigrationsPane-BnVflp7I.js} +1 -1
- package/editor/assets/{TemplatesPane-B5hn_v0Z.js → TemplatesPane-DPcXg8ZA.js} +12 -8
- package/editor/assets/{cssMode-BbIf5k6I.js → cssMode-CA8TzUhD.js} +1 -1
- package/editor/assets/{freemarker2-DoW0pSYV.js → freemarker2-BVvYxc4P.js} +1 -1
- package/editor/assets/{handlebars-DLlET-qc.js → handlebars-mDJMZKVv.js} +1 -1
- package/editor/assets/{html-4khbqrhe.js → html-D6Gw0A_W.js} +1 -1
- package/editor/assets/{htmlMode-DblHkZ-k.js → htmlMode-BD8QBoPu.js} +1 -1
- package/editor/assets/index-DAuQQi22.js +138 -0
- package/editor/assets/{javascript-CgPO2Hmj.js → javascript-CT1GQ3oq.js} +1 -1
- package/editor/assets/{jsonMode-BrWh2436.js → jsonMode-CBS8chp_.js} +1 -1
- package/editor/assets/{liquid-BsQJXwPT.js → liquid-BywhH4Kg.js} +1 -1
- package/editor/assets/{mdx-AO8t67gx.js → mdx-VnQOs-88.js} +1 -1
- package/editor/assets/{python-3w4sZj5c.js → python-vRZdM7SD.js} +1 -1
- package/editor/assets/{razor-BFsvo06w.js → razor--qLVVNOO.js} +1 -1
- package/editor/assets/{tsMode-QrC4ERjp.js → tsMode-CLwp9V4R.js} +1 -1
- package/editor/assets/{typescript-BXJ3QLad.js → typescript-DUO5mANL.js} +1 -1
- package/editor/assets/{xml-CxKYn1FP.js → xml-CcxvqYR1.js} +1 -1
- package/editor/assets/{yaml-BmWLvF7Q.js → yaml-BpCmDgZz.js} +1 -1
- package/editor/index.html +1 -1
- package/package.json +7 -1
- package/editor/assets/index-Do1drqEQ.js +0 -138
|
@@ -0,0 +1,1224 @@
|
|
|
1
|
+
import {
|
|
2
|
+
analyzeTemplate,
|
|
3
|
+
applyMigration,
|
|
4
|
+
baseTemplateName,
|
|
5
|
+
buildAuthoringReference,
|
|
6
|
+
canEdit,
|
|
7
|
+
contentTypeFor,
|
|
8
|
+
createDiagnosticRegistry,
|
|
9
|
+
deriveFields,
|
|
10
|
+
hasRole,
|
|
11
|
+
isVersionedTemplateName,
|
|
12
|
+
loadComponents,
|
|
13
|
+
loadTemplate,
|
|
14
|
+
pendingMigrations,
|
|
15
|
+
renderReferenceMarkdown,
|
|
16
|
+
validateSite,
|
|
17
|
+
workingStoreRoundTripDiagnostic
|
|
18
|
+
} from "./chunk-7TB5ANIS.js";
|
|
19
|
+
|
|
20
|
+
// ../editor-api/src/scaffold.ts
|
|
21
|
+
var HOME_HTML = `<!doctype html>
|
|
22
|
+
<html lang="en">
|
|
23
|
+
<head>
|
|
24
|
+
<meta charset="utf-8" />
|
|
25
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
26
|
+
<title>{title}</title>
|
|
27
|
+
<link rel="stylesheet" href="/styles.css" />
|
|
28
|
+
</head>
|
|
29
|
+
<body>
|
|
30
|
+
<main>
|
|
31
|
+
<h1>{title}</h1>
|
|
32
|
+
<div class="prose" data-type="richtext">{body}</div>
|
|
33
|
+
</main>
|
|
34
|
+
</body>
|
|
35
|
+
</html>
|
|
36
|
+
`;
|
|
37
|
+
var HOME_JSON = `${JSON.stringify(
|
|
38
|
+
{
|
|
39
|
+
template: "home",
|
|
40
|
+
title: "Welcome",
|
|
41
|
+
isPublished: true,
|
|
42
|
+
fields: {
|
|
43
|
+
body: "<p>This is your starter home page. Edit it in the nanoesis editor and click Publish to update your site.</p>"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
null,
|
|
47
|
+
2
|
|
48
|
+
)}
|
|
49
|
+
`;
|
|
50
|
+
var STYLES_CSS = `:root {
|
|
51
|
+
--ink: #1a1a2e;
|
|
52
|
+
--muted: #555;
|
|
53
|
+
--accent: #4f46e5;
|
|
54
|
+
--max: 42rem;
|
|
55
|
+
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
|
56
|
+
line-height: 1.6;
|
|
57
|
+
color: var(--ink);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
body {
|
|
61
|
+
margin: 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
main {
|
|
65
|
+
max-width: var(--max);
|
|
66
|
+
margin-inline: auto;
|
|
67
|
+
padding: 1.5rem 1.25rem;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
h1 {
|
|
71
|
+
font-size: 2.25rem;
|
|
72
|
+
letter-spacing: -0.01em;
|
|
73
|
+
margin: 0 0 1rem;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.prose {
|
|
77
|
+
font-size: 1.05rem;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
a {
|
|
81
|
+
color: var(--accent);
|
|
82
|
+
}
|
|
83
|
+
`;
|
|
84
|
+
var SCAFFOLD_FILES = {
|
|
85
|
+
"templates/home.html": HOME_HTML,
|
|
86
|
+
"public/styles.css": STYLES_CSS,
|
|
87
|
+
"content/index.json": HOME_JSON
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// ../editor-api/src/api.ts
|
|
91
|
+
var MAX_LOGO_BYTES = 1024 * 1024;
|
|
92
|
+
function requiredWriteRole(path) {
|
|
93
|
+
const top = path.split("/")[0];
|
|
94
|
+
if (top === "content") return "author";
|
|
95
|
+
if (top === "templates" || top === "components" || top === "public") return "developer";
|
|
96
|
+
return "admin";
|
|
97
|
+
}
|
|
98
|
+
function denyWrite(principal, path) {
|
|
99
|
+
if (hasRole(principal, requiredWriteRole(path))) return null;
|
|
100
|
+
return json(403, { ok: false, error: "your role cannot modify this path" });
|
|
101
|
+
}
|
|
102
|
+
function json(status, data) {
|
|
103
|
+
return {
|
|
104
|
+
status,
|
|
105
|
+
headers: { "content-type": "application/json" },
|
|
106
|
+
body: JSON.stringify(data)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function methodNotAllowed() {
|
|
110
|
+
return json(405, { ok: false, error: "POST only" });
|
|
111
|
+
}
|
|
112
|
+
function withDefaultHeaders(response) {
|
|
113
|
+
const headers = { ...response.headers ?? {} };
|
|
114
|
+
if (headers["cache-control"] === void 0) {
|
|
115
|
+
headers["cache-control"] = "no-store";
|
|
116
|
+
if (headers["vary"] === void 0) headers["vary"] = "Authorization";
|
|
117
|
+
}
|
|
118
|
+
return { ...response, headers };
|
|
119
|
+
}
|
|
120
|
+
var BEARER_PREFIX = /^Bearer\s+/i;
|
|
121
|
+
function bearerToken(getHeader) {
|
|
122
|
+
const raw = getHeader("authorization") ?? getHeader("Authorization");
|
|
123
|
+
if (raw === void 0) return void 0;
|
|
124
|
+
const match = BEARER_PREFIX.exec(raw);
|
|
125
|
+
return match ? raw.slice(match[0].length).trim() : void 0;
|
|
126
|
+
}
|
|
127
|
+
async function parseJsonBody(req) {
|
|
128
|
+
const bytes = await req.body();
|
|
129
|
+
if (bytes.byteLength === 0) return null;
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function isLoginRequest(value) {
|
|
137
|
+
return typeof value === "object" && value !== null && typeof value["username"] === "string" && typeof value["password"] === "string";
|
|
138
|
+
}
|
|
139
|
+
function isChangePasswordRequest(value) {
|
|
140
|
+
return typeof value === "object" && value !== null && typeof value["currentPassword"] === "string" && typeof value["newPassword"] === "string";
|
|
141
|
+
}
|
|
142
|
+
async function handleAuthRoute(deps, req) {
|
|
143
|
+
const endpoints = deps.authEndpoints;
|
|
144
|
+
if (endpoints === void 0) return void 0;
|
|
145
|
+
switch (req.path) {
|
|
146
|
+
case "/api/auth/state":
|
|
147
|
+
return json(200, await endpoints.state());
|
|
148
|
+
case "/api/login": {
|
|
149
|
+
if (req.method !== "POST") return methodNotAllowed();
|
|
150
|
+
const body = await parseJsonBody(req);
|
|
151
|
+
if (!isLoginRequest(body)) {
|
|
152
|
+
return json(400, { ok: false, error: "JSON body with username + password required" });
|
|
153
|
+
}
|
|
154
|
+
const result = await endpoints.login(body);
|
|
155
|
+
if (result.ok) {
|
|
156
|
+
return json(200, {
|
|
157
|
+
ok: true,
|
|
158
|
+
token: result.value.token,
|
|
159
|
+
principal: result.value.principal,
|
|
160
|
+
firstRunBootstrap: result.value.firstRunBootstrap
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return json(result.status, { ok: false, error: result.error });
|
|
164
|
+
}
|
|
165
|
+
case "/api/refresh": {
|
|
166
|
+
if (req.method !== "POST") return methodNotAllowed();
|
|
167
|
+
const token = bearerToken(req.getHeader);
|
|
168
|
+
if (token === void 0) return json(401, { ok: false, error: "no token" });
|
|
169
|
+
const result = await endpoints.refresh(token);
|
|
170
|
+
if (result.ok) {
|
|
171
|
+
return json(200, {
|
|
172
|
+
ok: true,
|
|
173
|
+
token: result.value.token,
|
|
174
|
+
principal: result.value.principal
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return json(result.status, { ok: false, error: result.error });
|
|
178
|
+
}
|
|
179
|
+
case "/api/logout": {
|
|
180
|
+
if (req.method !== "POST") return methodNotAllowed();
|
|
181
|
+
const token = bearerToken(req.getHeader);
|
|
182
|
+
await endpoints.logout(token ?? "");
|
|
183
|
+
return json(200, { ok: true });
|
|
184
|
+
}
|
|
185
|
+
default:
|
|
186
|
+
return void 0;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function isObject(value) {
|
|
190
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
191
|
+
}
|
|
192
|
+
function fromAuthResult(result) {
|
|
193
|
+
return result.ok ? json(200, result.value) : json(result.status, { ok: false, error: result.error });
|
|
194
|
+
}
|
|
195
|
+
async function handleUserAdminRoute(deps, req, caller) {
|
|
196
|
+
const userAdmin = deps.userAdmin;
|
|
197
|
+
if (userAdmin === void 0) {
|
|
198
|
+
return json(404, { ok: false, error: "user management is not available on this host" });
|
|
199
|
+
}
|
|
200
|
+
if (!hasRole(caller, "admin")) {
|
|
201
|
+
return json(403, { ok: false, error: "admin role required" });
|
|
202
|
+
}
|
|
203
|
+
const segments = req.path.slice("/api/users".length).split("/").filter((s) => s !== "");
|
|
204
|
+
if (segments.length === 0) {
|
|
205
|
+
if (req.method === "GET") return fromAuthResult(await userAdmin.listUsers());
|
|
206
|
+
if (req.method === "POST") {
|
|
207
|
+
const body = await parseJsonBody(req);
|
|
208
|
+
if (!isObject(body)) return json(400, { ok: false, error: "JSON body required" });
|
|
209
|
+
return fromAuthResult(await userAdmin.createUser(body));
|
|
210
|
+
}
|
|
211
|
+
return json(405, { ok: false, error: "method not allowed" });
|
|
212
|
+
}
|
|
213
|
+
const targetId = segments[0];
|
|
214
|
+
if (segments.length === 2 && segments[1] === "password") {
|
|
215
|
+
if (req.method !== "POST") return json(405, { ok: false, error: "method not allowed" });
|
|
216
|
+
const body = await parseJsonBody(req);
|
|
217
|
+
if (!isObject(body)) return json(400, { ok: false, error: "JSON body required" });
|
|
218
|
+
return fromAuthResult(
|
|
219
|
+
await userAdmin.resetPassword(targetId, body)
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
if (segments.length === 1) {
|
|
223
|
+
if (req.method === "PATCH") {
|
|
224
|
+
const body = await parseJsonBody(req);
|
|
225
|
+
if (!isObject(body)) return json(400, { ok: false, error: "JSON body required" });
|
|
226
|
+
return fromAuthResult(
|
|
227
|
+
await userAdmin.updateUser(caller.userId, targetId, body)
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (req.method === "DELETE") {
|
|
231
|
+
return fromAuthResult(await userAdmin.deleteUser(caller.userId, targetId));
|
|
232
|
+
}
|
|
233
|
+
return json(405, { ok: false, error: "method not allowed" });
|
|
234
|
+
}
|
|
235
|
+
return json(404, { ok: false, error: `Unknown API route: ${req.path}` });
|
|
236
|
+
}
|
|
237
|
+
async function handleDiagnosticsRoute(deps, req, caller) {
|
|
238
|
+
const registry = deps.diagnostics;
|
|
239
|
+
if (registry === void 0) {
|
|
240
|
+
return json(404, { ok: false, error: "diagnostics are not available on this host" });
|
|
241
|
+
}
|
|
242
|
+
if (!hasRole(caller, "admin")) {
|
|
243
|
+
return json(403, { ok: false, error: "admin role required" });
|
|
244
|
+
}
|
|
245
|
+
if (req.path === "/api/admin/diagnostics") {
|
|
246
|
+
if (req.method !== "GET") return json(405, { ok: false, error: "method not allowed" });
|
|
247
|
+
const findings = await registry.runAll({ store: deps.store });
|
|
248
|
+
return json(200, { findings });
|
|
249
|
+
}
|
|
250
|
+
if (req.method !== "POST") return json(405, { ok: false, error: "method not allowed" });
|
|
251
|
+
const body = await parseJsonBody(req);
|
|
252
|
+
if (!isObject(body) || typeof body["id"] !== "string") {
|
|
253
|
+
return json(400, { ok: false, error: "JSON body with repair id required" });
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
const findings = await registry.runRepair(body["id"], { store: deps.store });
|
|
257
|
+
return json(200, { findings });
|
|
258
|
+
} catch (error) {
|
|
259
|
+
return json(400, { ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
260
|
+
}
|
|
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
|
+
}
|
|
384
|
+
async function handleBrandingRoute(deps, req) {
|
|
385
|
+
if (req.path !== "/api/branding" && req.path !== "/api/branding/logo") return void 0;
|
|
386
|
+
const store = deps.branding;
|
|
387
|
+
if (req.method === "GET") {
|
|
388
|
+
if (req.path === "/api/branding") {
|
|
389
|
+
if (store === void 0) return json(200, { name: null, logoUrl: null });
|
|
390
|
+
const state = await store.get();
|
|
391
|
+
const logoUrl = state.logo === null ? null : `/api/branding/logo?v=${state.logo.updatedAt}`;
|
|
392
|
+
return json(200, { name: state.name, logoUrl });
|
|
393
|
+
}
|
|
394
|
+
const logo = store === void 0 ? null : await store.getLogo();
|
|
395
|
+
if (logo === null) return { status: 404, body: "" };
|
|
396
|
+
return {
|
|
397
|
+
status: 200,
|
|
398
|
+
headers: { "content-type": logo.contentType, "cache-control": "public, max-age=31536000" },
|
|
399
|
+
body: logo.bytes
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
if (store === void 0) {
|
|
403
|
+
return json(404, { ok: false, error: "branding is not available on this host" });
|
|
404
|
+
}
|
|
405
|
+
if (!hasRole(await deps.identity.authenticate(req.getHeader), "admin")) {
|
|
406
|
+
return json(403, { ok: false, error: "admin role required" });
|
|
407
|
+
}
|
|
408
|
+
if (req.path === "/api/branding") {
|
|
409
|
+
if (req.method !== "PUT") return json(405, { ok: false, error: "method not allowed" });
|
|
410
|
+
const body = await parseJsonBody(req);
|
|
411
|
+
if (!isObject(body)) return json(400, { ok: false, error: "JSON body required" });
|
|
412
|
+
const raw = body["name"];
|
|
413
|
+
const name = typeof raw === "string" && raw.trim() !== "" ? raw.trim() : null;
|
|
414
|
+
await store.setName(name);
|
|
415
|
+
return json(200, { ok: true });
|
|
416
|
+
}
|
|
417
|
+
if (req.method === "POST") {
|
|
418
|
+
const contentType = req.getHeader("content-type") ?? "";
|
|
419
|
+
if (!contentType.startsWith("image/")) {
|
|
420
|
+
return json(415, { ok: false, error: "logo must be an image" });
|
|
421
|
+
}
|
|
422
|
+
const bytes = await req.body();
|
|
423
|
+
if (bytes.byteLength === 0) return json(400, { ok: false, error: "empty body" });
|
|
424
|
+
if (bytes.byteLength > MAX_LOGO_BYTES) return json(413, { ok: false, error: "logo too large" });
|
|
425
|
+
await store.setLogo(bytes, contentType);
|
|
426
|
+
return json(200, { ok: true });
|
|
427
|
+
}
|
|
428
|
+
if (req.method === "DELETE") {
|
|
429
|
+
await store.clearLogo();
|
|
430
|
+
return json(200, { ok: true });
|
|
431
|
+
}
|
|
432
|
+
return json(405, { ok: false, error: "method not allowed" });
|
|
433
|
+
}
|
|
434
|
+
async function handleApi(deps, req) {
|
|
435
|
+
return withDefaultHeaders(await dispatchApi(deps, req));
|
|
436
|
+
}
|
|
437
|
+
async function dispatchApi(deps, req) {
|
|
438
|
+
const authResponse = await handleAuthRoute(deps, req);
|
|
439
|
+
if (authResponse !== void 0) return authResponse;
|
|
440
|
+
const brandingResponse = await handleBrandingRoute(deps, req);
|
|
441
|
+
if (brandingResponse !== void 0) return brandingResponse;
|
|
442
|
+
const principal = await deps.identity.authenticate(req.getHeader);
|
|
443
|
+
if (!canEdit(principal)) {
|
|
444
|
+
return json(401, { ok: false, error: "Unauthorized" });
|
|
445
|
+
}
|
|
446
|
+
if (req.path === "/api/users" || req.path.startsWith("/api/users/")) {
|
|
447
|
+
return handleUserAdminRoute(deps, req, principal);
|
|
448
|
+
}
|
|
449
|
+
if (req.path === "/api/admin/diagnostics" || req.path === "/api/admin/diagnostics/repair") {
|
|
450
|
+
return handleDiagnosticsRoute(deps, req, principal);
|
|
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
|
+
}
|
|
458
|
+
const get = (key) => req.query.get(key) ?? "";
|
|
459
|
+
switch (req.path) {
|
|
460
|
+
case "/api/list": {
|
|
461
|
+
try {
|
|
462
|
+
return json(200, await deps.store.list(get("dir")));
|
|
463
|
+
} catch {
|
|
464
|
+
return json(200, []);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
case "/api/read": {
|
|
468
|
+
const path = get("path");
|
|
469
|
+
try {
|
|
470
|
+
const bytes = await deps.store.readBytes(path);
|
|
471
|
+
return { status: 200, headers: { "content-type": contentTypeFor(path) }, body: bytes };
|
|
472
|
+
} catch {
|
|
473
|
+
return { status: 404, body: "" };
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
case "/api/exists":
|
|
477
|
+
return json(200, { exists: await deps.store.exists(get("path")) });
|
|
478
|
+
case "/api/validate":
|
|
479
|
+
return json(200, await validateSite(deps.store));
|
|
480
|
+
case "/api/authors":
|
|
481
|
+
return json(200, deps.authors ? await deps.authors() : []);
|
|
482
|
+
case "/api/template-fields": {
|
|
483
|
+
try {
|
|
484
|
+
const [template, components] = await Promise.all([
|
|
485
|
+
loadTemplate(deps.store, get("name")),
|
|
486
|
+
loadComponents(deps.store)
|
|
487
|
+
]);
|
|
488
|
+
return json(200, deriveFields(template, components));
|
|
489
|
+
} catch {
|
|
490
|
+
return { status: 404, body: "" };
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
case "/api/write": {
|
|
494
|
+
if (req.method !== "POST") return methodNotAllowed();
|
|
495
|
+
const denied = denyWrite(principal, get("path"));
|
|
496
|
+
if (denied) return denied;
|
|
497
|
+
try {
|
|
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
|
+
...result.schemaDelta !== void 0 && { schemaDelta: result.schemaDelta },
|
|
504
|
+
...result.parseDiagnostics !== void 0 && {
|
|
505
|
+
parseDiagnostics: result.parseDiagnostics
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
} catch (error) {
|
|
509
|
+
return json(500, { ok: false, error: String(error) });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
case "/api/delete": {
|
|
513
|
+
if (req.method !== "POST") return methodNotAllowed();
|
|
514
|
+
const denied = denyWrite(principal, get("path"));
|
|
515
|
+
if (denied) return denied;
|
|
516
|
+
try {
|
|
517
|
+
await deps.store.delete(get("path"));
|
|
518
|
+
return json(200, { ok: true });
|
|
519
|
+
} catch (error) {
|
|
520
|
+
return json(500, { ok: false, error: String(error) });
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
case "/api/rename": {
|
|
524
|
+
if (req.method !== "POST") return methodNotAllowed();
|
|
525
|
+
const denied = denyWrite(principal, get("from")) ?? denyWrite(principal, get("to"));
|
|
526
|
+
if (denied) return denied;
|
|
527
|
+
const result = await deps.store.rename(get("from"), get("to"));
|
|
528
|
+
if (result.ok) return json(200, { ok: true });
|
|
529
|
+
const status = result.reason === "exists" ? 409 : 404;
|
|
530
|
+
return json(status, { ok: false, error: result.reason });
|
|
531
|
+
}
|
|
532
|
+
case "/api/publish": {
|
|
533
|
+
if (req.method !== "POST") return methodNotAllowed();
|
|
534
|
+
const result = await deps.publish();
|
|
535
|
+
if (result.ok) {
|
|
536
|
+
return json(200, {
|
|
537
|
+
ok: true,
|
|
538
|
+
written: result.written.length,
|
|
539
|
+
...result.summary !== void 0 && { summary: result.summary }
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
return json(422, {
|
|
543
|
+
ok: false,
|
|
544
|
+
errors: result.validation.errors.map((e) => e.message),
|
|
545
|
+
...result.summary !== void 0 && { summary: result.summary }
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
case "/api/scaffold": {
|
|
549
|
+
if (req.method !== "POST") return methodNotAllowed();
|
|
550
|
+
if (!hasRole(principal, "admin")) {
|
|
551
|
+
return json(403, { ok: false, error: "admin role required" });
|
|
552
|
+
}
|
|
553
|
+
if ((await deps.store.list("content")).length > 0) {
|
|
554
|
+
return json(409, { ok: false, error: "site already has content" });
|
|
555
|
+
}
|
|
556
|
+
const created = [];
|
|
557
|
+
const skipped = [];
|
|
558
|
+
for (const [path, content] of Object.entries(SCAFFOLD_FILES)) {
|
|
559
|
+
if (await deps.store.exists(path)) {
|
|
560
|
+
skipped.push(path);
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
await deps.store.write(path, new TextEncoder().encode(content));
|
|
564
|
+
created.push(path);
|
|
565
|
+
}
|
|
566
|
+
return json(200, { ok: true, created, skipped });
|
|
567
|
+
}
|
|
568
|
+
case "/api/reconcile": {
|
|
569
|
+
if (req.method !== "POST") return methodNotAllowed();
|
|
570
|
+
if (deps.reconcile === void 0) {
|
|
571
|
+
return json(404, { ok: false, error: "reconcile is not available on this host" });
|
|
572
|
+
}
|
|
573
|
+
if (!hasRole(principal, "admin")) {
|
|
574
|
+
return json(403, { ok: false, error: "admin role required" });
|
|
575
|
+
}
|
|
576
|
+
const result = await deps.reconcile();
|
|
577
|
+
return json(200, {
|
|
578
|
+
ok: true,
|
|
579
|
+
added: result.added.length,
|
|
580
|
+
removed: result.removed.length,
|
|
581
|
+
total: result.index.keys.length
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
case "/api/me/password": {
|
|
585
|
+
if (req.method !== "POST") return methodNotAllowed();
|
|
586
|
+
if (deps.authEndpoints === void 0) {
|
|
587
|
+
return json(404, { ok: false, error: "no credential provider configured" });
|
|
588
|
+
}
|
|
589
|
+
const body = await parseJsonBody(req);
|
|
590
|
+
if (!isChangePasswordRequest(body)) {
|
|
591
|
+
return json(400, {
|
|
592
|
+
ok: false,
|
|
593
|
+
error: "JSON body with currentPassword + newPassword required"
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
const result = await deps.authEndpoints.changePassword(principal.userId, body);
|
|
597
|
+
if (result.ok) return json(200, { ok: true, token: result.value.token });
|
|
598
|
+
return json(result.status, { ok: false, error: result.error });
|
|
599
|
+
}
|
|
600
|
+
case "/api/me/token": {
|
|
601
|
+
const endpoints = deps.authEndpoints;
|
|
602
|
+
if (req.method === "POST") {
|
|
603
|
+
if (endpoints?.createToken === void 0) {
|
|
604
|
+
return json(404, { ok: false, error: "tokens are not available on this host" });
|
|
605
|
+
}
|
|
606
|
+
const result = await endpoints.createToken(principal.userId);
|
|
607
|
+
return result.ok ? json(200, { ok: true, token: result.value.token, expiresAt: result.value.expiresAt }) : json(result.status, { ok: false, error: result.error });
|
|
608
|
+
}
|
|
609
|
+
if (req.method === "DELETE") {
|
|
610
|
+
if (endpoints?.revokeTokens === void 0) {
|
|
611
|
+
return json(404, { ok: false, error: "token revocation is not available" });
|
|
612
|
+
}
|
|
613
|
+
const result = await endpoints.revokeTokens(principal.userId);
|
|
614
|
+
return result.ok ? json(200, { ok: true }) : json(result.status, { ok: false, error: result.error });
|
|
615
|
+
}
|
|
616
|
+
return json(405, { ok: false, error: "method not allowed" });
|
|
617
|
+
}
|
|
618
|
+
default:
|
|
619
|
+
return json(404, { ok: false, error: `Unknown API route: ${req.path}` });
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ../editor-api/src/authors.ts
|
|
624
|
+
function displayNameOf(user) {
|
|
625
|
+
const explicit = user.displayName?.trim();
|
|
626
|
+
return explicit !== void 0 && explicit !== "" ? explicit : user.username;
|
|
627
|
+
}
|
|
628
|
+
function authorOptions(users) {
|
|
629
|
+
return users.map((user) => ({ name: displayNameOf(user), user: user.username })).sort((a, b) => a.name.localeCompare(b.name));
|
|
630
|
+
}
|
|
631
|
+
function authorDirectory(users) {
|
|
632
|
+
const byUser = new Map(users.map((user) => [user.username, displayNameOf(user)]));
|
|
633
|
+
return (user) => {
|
|
634
|
+
const displayName = byUser.get(user);
|
|
635
|
+
return displayName === void 0 ? void 0 : { displayName };
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// ../editor-api/src/mcp.ts
|
|
640
|
+
function str(args, key) {
|
|
641
|
+
const value = args[key];
|
|
642
|
+
return typeof value === "string" ? value : "";
|
|
643
|
+
}
|
|
644
|
+
function request(method, path, query, bodyText, token) {
|
|
645
|
+
const body = bodyText === void 0 ? new Uint8Array() : new TextEncoder().encode(bodyText);
|
|
646
|
+
return {
|
|
647
|
+
method,
|
|
648
|
+
path,
|
|
649
|
+
query: new URLSearchParams(query),
|
|
650
|
+
getHeader: (name) => name.toLowerCase() === "authorization" && token !== void 0 ? `Bearer ${token}` : void 0,
|
|
651
|
+
body: async () => body
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
var pathArg = {
|
|
655
|
+
type: "string",
|
|
656
|
+
description: 'Site-relative path with forward slashes, e.g. "content/blog/post.json".'
|
|
657
|
+
};
|
|
658
|
+
var TOOL_SPECS = [
|
|
659
|
+
{
|
|
660
|
+
name: "list_dir",
|
|
661
|
+
description: "List the immediate children (files and folders) of a site directory. Use '' or omit for the site root; the top-level folders are content/ (pages), templates/, components/, and public/.",
|
|
662
|
+
inputSchema: {
|
|
663
|
+
type: "object",
|
|
664
|
+
properties: {
|
|
665
|
+
path: { type: "string", description: "Directory path, '' for the site root." }
|
|
666
|
+
},
|
|
667
|
+
additionalProperties: false
|
|
668
|
+
},
|
|
669
|
+
toRequest: (args, token) => request("GET", "/api/list", { dir: str(args, "path") }, void 0, token)
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
name: "read_file",
|
|
673
|
+
description: "Read a file's text contents (content JSON, a template/component's HTML, CSS, etc.). Reports an error if the file does not exist.",
|
|
674
|
+
inputSchema: {
|
|
675
|
+
type: "object",
|
|
676
|
+
properties: { path: pathArg },
|
|
677
|
+
required: ["path"],
|
|
678
|
+
additionalProperties: false
|
|
679
|
+
},
|
|
680
|
+
toRequest: (args, token) => request("GET", "/api/read", { path: str(args, "path") }, void 0, token)
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
name: "describe_template",
|
|
684
|
+
description: "List the fields a template defines (name, type, label, whether required, length limits), so you know what a page using it needs. Pass the template name without its path or extension, e.g. 'article' for templates/article.html.",
|
|
685
|
+
inputSchema: {
|
|
686
|
+
type: "object",
|
|
687
|
+
properties: {
|
|
688
|
+
name: {
|
|
689
|
+
type: "string",
|
|
690
|
+
description: 'Template name without path or extension, e.g. "article".'
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
required: ["name"],
|
|
694
|
+
additionalProperties: false
|
|
695
|
+
},
|
|
696
|
+
toRequest: (args, token) => request("GET", "/api/template-fields", { name: str(args, "name") }, void 0, token)
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
name: "write_file",
|
|
700
|
+
description: "Create or overwrite a text file. content/ requires the author role; templates/, components/, and public/ require the developer role. Read the nanoesis://reference resource first for the authoring syntax. For template/component paths the response includes `schemaDelta` (added/removed/typeChanged fields) \u2014 if `typeChanged` contains `destructive: true` or `removed` is non-empty, an auto-stamp has been written and authored content may need migration, so surface to the user before further edits. For content/*.json writes the response may include `parseDiagnostics.unknownTopLevelKeys` \u2014 top-level keys the parser dropped because they are not system keys; content values belong under `fields` (system keys are case-sensitive: `isPublished`, not `IsPublished`). See the reference's 'Authoring guardrails for LLMs' section for the common pitfalls (notably: `data-*` annotations bind to the element, not the token inside it \u2014 moving a token out of its annotated container silently strips them).",
|
|
701
|
+
inputSchema: {
|
|
702
|
+
type: "object",
|
|
703
|
+
properties: {
|
|
704
|
+
path: pathArg,
|
|
705
|
+
contents: { type: "string", description: "The full new text contents of the file." }
|
|
706
|
+
},
|
|
707
|
+
required: ["path", "contents"],
|
|
708
|
+
additionalProperties: false
|
|
709
|
+
},
|
|
710
|
+
toRequest: (args, token) => request("POST", "/api/write", { path: str(args, "path") }, str(args, "contents"), token)
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
name: "delete_path",
|
|
714
|
+
description: "Delete a file or a whole folder subtree. Same role rules as write_file. Deleting a path that does not exist is a no-op.",
|
|
715
|
+
inputSchema: {
|
|
716
|
+
type: "object",
|
|
717
|
+
properties: { path: pathArg },
|
|
718
|
+
required: ["path"],
|
|
719
|
+
additionalProperties: false
|
|
720
|
+
},
|
|
721
|
+
toRequest: (args, token) => request("POST", "/api/delete", { path: str(args, "path") }, void 0, token)
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
name: "rename_path",
|
|
725
|
+
description: "Move or rename a file or folder subtree. Refuses to overwrite an existing destination. Requires write rights at both the source and the destination. Renaming a template also requires updating every content item bound to the old name (the validation gate will catch unbound items on publish, but `list_pending_migrations` is the cheaper check).",
|
|
726
|
+
inputSchema: {
|
|
727
|
+
type: "object",
|
|
728
|
+
properties: {
|
|
729
|
+
from: { type: "string", description: "The current path." },
|
|
730
|
+
to: { type: "string", description: "The new path." }
|
|
731
|
+
},
|
|
732
|
+
required: ["from", "to"],
|
|
733
|
+
additionalProperties: false
|
|
734
|
+
},
|
|
735
|
+
toRequest: (args, token) => request(
|
|
736
|
+
"POST",
|
|
737
|
+
"/api/rename",
|
|
738
|
+
{ from: str(args, "from"), to: str(args, "to") },
|
|
739
|
+
void 0,
|
|
740
|
+
token
|
|
741
|
+
)
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
name: "validate",
|
|
745
|
+
description: "Run the validation gate over the whole site and return its diagnostics, without publishing. Use it to check your work: errors would block a publish, warnings (e.g. length constraints) only inform.",
|
|
746
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
747
|
+
toRequest: (_args, token) => request("GET", "/api/validate", {}, void 0, token)
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
name: "publish",
|
|
751
|
+
description: "Compile and publish the whole site. Runs the validation gate first; if anything would break, it reports the problems and writes nothing.",
|
|
752
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
753
|
+
toRequest: (_args, token) => request("POST", "/api/publish", {}, void 0, token)
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
name: "list_pending_migrations",
|
|
757
|
+
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.",
|
|
758
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
759
|
+
toRequest: (_args, token) => request("GET", "/api/migrations", {}, void 0, token)
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
name: "preview_migration",
|
|
763
|
+
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).',
|
|
764
|
+
inputSchema: {
|
|
765
|
+
type: "object",
|
|
766
|
+
properties: { path: pathArg },
|
|
767
|
+
required: ["path"],
|
|
768
|
+
additionalProperties: false
|
|
769
|
+
},
|
|
770
|
+
toRequest: (args, token) => request("GET", "/api/migrations/preview", { path: str(args, "path") }, void 0, token)
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
name: "apply_migration",
|
|
774
|
+
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.",
|
|
775
|
+
inputSchema: {
|
|
776
|
+
type: "object",
|
|
777
|
+
properties: {
|
|
778
|
+
path: pathArg,
|
|
779
|
+
resolution: {
|
|
780
|
+
type: "string",
|
|
781
|
+
description: 'A JSON object encoded as a string: { "drop": string[], "rename": { from: to }, "keep": string[], "fill": { name: value } }.'
|
|
782
|
+
}
|
|
783
|
+
},
|
|
784
|
+
required: ["path", "resolution"],
|
|
785
|
+
additionalProperties: false
|
|
786
|
+
},
|
|
787
|
+
toRequest: (args, token) => request(
|
|
788
|
+
"POST",
|
|
789
|
+
"/api/migrations/apply",
|
|
790
|
+
{},
|
|
791
|
+
JSON.stringify({
|
|
792
|
+
path: str(args, "path"),
|
|
793
|
+
resolution: parseResolutionArg(str(args, "resolution"))
|
|
794
|
+
}),
|
|
795
|
+
token
|
|
796
|
+
)
|
|
797
|
+
}
|
|
798
|
+
];
|
|
799
|
+
function parseResolutionArg(raw) {
|
|
800
|
+
if (raw === "") return {};
|
|
801
|
+
try {
|
|
802
|
+
return JSON.parse(raw);
|
|
803
|
+
} catch {
|
|
804
|
+
return {};
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
var MCP_TOOLS = TOOL_SPECS.map(
|
|
808
|
+
({ name, description, inputSchema }) => ({ name, description, inputSchema })
|
|
809
|
+
);
|
|
810
|
+
function toResult(response) {
|
|
811
|
+
const text = typeof response.body === "string" ? response.body : response.body === void 0 ? "" : new TextDecoder().decode(response.body);
|
|
812
|
+
return { text, isError: response.status >= 400 };
|
|
813
|
+
}
|
|
814
|
+
async function callMcpTool(deps, name, args = {}, options = {}) {
|
|
815
|
+
const spec = TOOL_SPECS.find((tool) => tool.name === name);
|
|
816
|
+
if (spec === void 0) return { text: `Unknown tool: ${name}`, isError: true };
|
|
817
|
+
return toResult(await handleApi(deps, spec.toRequest(args, options.token)));
|
|
818
|
+
}
|
|
819
|
+
var REFERENCE_URI = "nanoesis://reference";
|
|
820
|
+
var MCP_RESOURCES = [
|
|
821
|
+
{
|
|
822
|
+
uri: REFERENCE_URI,
|
|
823
|
+
name: "nanoesis authoring reference",
|
|
824
|
+
description: "The generated reference for nanoesis templates and content: tokens, field types, annotations, loops, components, and the document shell. Read this before writing templates or content.",
|
|
825
|
+
mimeType: "text/markdown"
|
|
826
|
+
}
|
|
827
|
+
];
|
|
828
|
+
function readMcpResource(uri) {
|
|
829
|
+
if (uri === REFERENCE_URI) {
|
|
830
|
+
return { text: renderReferenceMarkdown(buildAuthoringReference()), mimeType: "text/markdown" };
|
|
831
|
+
}
|
|
832
|
+
return void 0;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// ../editor-api/src/diagnose.ts
|
|
836
|
+
function buildDefaultDiagnostics() {
|
|
837
|
+
const registry = createDiagnosticRegistry();
|
|
838
|
+
registry.add(workingStoreRoundTripDiagnostic);
|
|
839
|
+
registry.add(homeTemplateMissingDiagnostic);
|
|
840
|
+
registry.add(noPublishedContentDiagnostic);
|
|
841
|
+
registry.add(templateSnapshotIntegrityDiagnostic);
|
|
842
|
+
registry.add(templateSuffixConflictDiagnostic);
|
|
843
|
+
registry.add(pendingMigrationsDiagnostic);
|
|
844
|
+
registry.addRepair(recreateHomeTemplateRepair);
|
|
845
|
+
registry.addRepair(rebindItemToCurrentRepair);
|
|
846
|
+
registry.addRepair(copySnapshotToCurrentRepair);
|
|
847
|
+
return registry;
|
|
848
|
+
}
|
|
849
|
+
var HOME_PATH = "templates/home.html";
|
|
850
|
+
var homeTemplateMissingDiagnostic = {
|
|
851
|
+
id: "content.home-template",
|
|
852
|
+
async run({ store }) {
|
|
853
|
+
if (await store.exists(HOME_PATH)) return [];
|
|
854
|
+
return [
|
|
855
|
+
{
|
|
856
|
+
id: "content.home-template.missing",
|
|
857
|
+
source: "content",
|
|
858
|
+
severity: "error",
|
|
859
|
+
title: "Home template is missing",
|
|
860
|
+
detail: `The locked path ${HOME_PATH} does not exist. Without it, the editor cannot render the home page and publishing will fail to compile the index. Recreate it from the bundled scaffold to restore the working state.`,
|
|
861
|
+
repair: { id: "content.home-template.recreate", label: "Recreate home template" }
|
|
862
|
+
}
|
|
863
|
+
];
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
var recreateHomeTemplateRepair = {
|
|
867
|
+
id: "content.home-template.recreate",
|
|
868
|
+
async run({ store }) {
|
|
869
|
+
if (await store.exists(HOME_PATH)) return;
|
|
870
|
+
const bytes = new TextEncoder().encode(SCAFFOLD_FILES[HOME_PATH]);
|
|
871
|
+
await store.write(HOME_PATH, bytes);
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
var noPublishedContentDiagnostic = {
|
|
875
|
+
id: "content.no-published-content",
|
|
876
|
+
async run({ store }) {
|
|
877
|
+
const published = await anyPublishedContentItem(store);
|
|
878
|
+
if (published) return [];
|
|
879
|
+
return [
|
|
880
|
+
{
|
|
881
|
+
id: "content.no-published-content",
|
|
882
|
+
source: "content",
|
|
883
|
+
severity: "warning",
|
|
884
|
+
title: "Nothing is published",
|
|
885
|
+
detail: "No content item has isPublished: true, so a publish would produce no pages. Open a content item and toggle Published, then publish again."
|
|
886
|
+
}
|
|
887
|
+
];
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
var templateSnapshotIntegrityDiagnostic = {
|
|
891
|
+
id: "templates.snapshot-integrity",
|
|
892
|
+
async run({ store }) {
|
|
893
|
+
const findings = [];
|
|
894
|
+
const components = await loadComponents(store).catch(() => /* @__PURE__ */ new Map());
|
|
895
|
+
const items = await collectContentItems(store);
|
|
896
|
+
const snapshotCache = /* @__PURE__ */ new Map();
|
|
897
|
+
for (const item of items) {
|
|
898
|
+
if (!isVersionedTemplateName(item.template)) continue;
|
|
899
|
+
const kind = "template";
|
|
900
|
+
const path = `${kind === "template" ? "templates" : "components"}/${item.template}.html`;
|
|
901
|
+
let state = snapshotCache.get(path);
|
|
902
|
+
if (state === void 0) {
|
|
903
|
+
state = await classifySnapshot(store, path, components);
|
|
904
|
+
snapshotCache.set(path, state);
|
|
905
|
+
}
|
|
906
|
+
if (state === "ok") continue;
|
|
907
|
+
const isMissing = state === "missing";
|
|
908
|
+
findings.push({
|
|
909
|
+
id: `templates.${isMissing ? "missing" : "corrupt"}-snapshot:${item.path}`,
|
|
910
|
+
source: "templates",
|
|
911
|
+
severity: "error",
|
|
912
|
+
title: isMissing ? `Pinned snapshot missing: ${path}` : `Pinned snapshot will not parse: ${path}`,
|
|
913
|
+
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.`,
|
|
914
|
+
repair: {
|
|
915
|
+
id: "templates.rebind-item-to-current",
|
|
916
|
+
label: "Rebind item to current template",
|
|
917
|
+
args: { itemPath: item.path }
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
return findings;
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
var rebindItemToCurrentRepair = {
|
|
925
|
+
id: "templates.rebind-item-to-current",
|
|
926
|
+
async run({ store }, args) {
|
|
927
|
+
const itemPath = args["itemPath"];
|
|
928
|
+
if (typeof itemPath !== "string" || itemPath === "") {
|
|
929
|
+
throw new Error("rebind-item-to-current: missing itemPath arg");
|
|
930
|
+
}
|
|
931
|
+
const text = await store.readText(itemPath);
|
|
932
|
+
const data = JSON.parse(text);
|
|
933
|
+
if (typeof data !== "object" || data === null) {
|
|
934
|
+
throw new Error(`rebind-item-to-current: ${itemPath} is not a JSON object`);
|
|
935
|
+
}
|
|
936
|
+
const obj = data;
|
|
937
|
+
if (typeof obj.template !== "string" || !isVersionedTemplateName(obj.template)) {
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
obj.template = baseTemplateName(obj.template);
|
|
941
|
+
const next = `${JSON.stringify(obj, null, 2)}
|
|
942
|
+
`;
|
|
943
|
+
await store.write(itemPath, new TextEncoder().encode(next));
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
var templateSuffixConflictDiagnostic = {
|
|
947
|
+
id: "templates.suffix-conflict",
|
|
948
|
+
async run({ store }) {
|
|
949
|
+
const findings = [];
|
|
950
|
+
for (const dir of ["templates", "components"]) {
|
|
951
|
+
let entries;
|
|
952
|
+
try {
|
|
953
|
+
entries = await store.list(dir);
|
|
954
|
+
} catch {
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
const seenBases = /* @__PURE__ */ new Set();
|
|
958
|
+
const snapshots = [];
|
|
959
|
+
for (const entry of entries) {
|
|
960
|
+
if (entry.kind !== "file" || !entry.name.endsWith(".html")) continue;
|
|
961
|
+
const stem = entry.name.slice(0, -".html".length);
|
|
962
|
+
if (isVersionedTemplateName(stem)) {
|
|
963
|
+
snapshots.push({ base: baseTemplateName(stem), snapshotPath: `${dir}/${entry.name}` });
|
|
964
|
+
} else {
|
|
965
|
+
seenBases.add(stem);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
for (const snap of snapshots) {
|
|
969
|
+
if (seenBases.has(snap.base)) continue;
|
|
970
|
+
const currentPath = `${dir}/${snap.base}.html`;
|
|
971
|
+
findings.push({
|
|
972
|
+
id: `templates.suffix-conflict:${snap.snapshotPath}`,
|
|
973
|
+
source: "templates",
|
|
974
|
+
severity: "warning",
|
|
975
|
+
title: `Snapshot has no current sibling: ${snap.snapshotPath}`,
|
|
976
|
+
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.`,
|
|
977
|
+
repair: {
|
|
978
|
+
id: "templates.copy-snapshot-to-current",
|
|
979
|
+
label: "Copy snapshot to current",
|
|
980
|
+
args: { snapshotPath: snap.snapshotPath, currentPath }
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return findings;
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
var copySnapshotToCurrentRepair = {
|
|
989
|
+
id: "templates.copy-snapshot-to-current",
|
|
990
|
+
async run({ store }, args) {
|
|
991
|
+
const snapshotPath = args["snapshotPath"];
|
|
992
|
+
const currentPath = args["currentPath"];
|
|
993
|
+
if (typeof snapshotPath !== "string" || typeof currentPath !== "string") {
|
|
994
|
+
throw new Error("copy-snapshot-to-current: missing snapshotPath or currentPath args");
|
|
995
|
+
}
|
|
996
|
+
if (await store.exists(currentPath)) return;
|
|
997
|
+
const bytes = await store.readBytes(snapshotPath);
|
|
998
|
+
await store.write(currentPath, bytes);
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
var pendingMigrationsDiagnostic = {
|
|
1002
|
+
id: "content.pending-migrations",
|
|
1003
|
+
async run({ store }) {
|
|
1004
|
+
const components = await loadComponents(store).catch(() => /* @__PURE__ */ new Map());
|
|
1005
|
+
const items = await collectContentItems(store);
|
|
1006
|
+
const templateFieldCache = /* @__PURE__ */ new Map();
|
|
1007
|
+
let pendingCount = 0;
|
|
1008
|
+
for (const item of items) {
|
|
1009
|
+
if (isVersionedTemplateName(item.template)) continue;
|
|
1010
|
+
let expected = templateFieldCache.get(item.template);
|
|
1011
|
+
if (expected === void 0) {
|
|
1012
|
+
const path = `templates/${item.template}.html`;
|
|
1013
|
+
try {
|
|
1014
|
+
const html = await store.readText(path);
|
|
1015
|
+
expected = new Set(deriveFields(html, components).map((f) => f.name));
|
|
1016
|
+
} catch {
|
|
1017
|
+
expected = /* @__PURE__ */ new Set();
|
|
1018
|
+
}
|
|
1019
|
+
templateFieldCache.set(item.template, expected);
|
|
1020
|
+
}
|
|
1021
|
+
const orphans = Object.keys(item.fields).filter((k) => !expected.has(k));
|
|
1022
|
+
if (orphans.length > 0) pendingCount += 1;
|
|
1023
|
+
}
|
|
1024
|
+
if (pendingCount === 0) return [];
|
|
1025
|
+
return [
|
|
1026
|
+
{
|
|
1027
|
+
id: "content.pending-migrations",
|
|
1028
|
+
source: "content",
|
|
1029
|
+
severity: "warning",
|
|
1030
|
+
title: `${pendingCount} item${pendingCount === 1 ? "" : "s"} pending migration`,
|
|
1031
|
+
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.`
|
|
1032
|
+
}
|
|
1033
|
+
];
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
async function collectContentItems(store, dir = "content") {
|
|
1037
|
+
const out = [];
|
|
1038
|
+
let entries;
|
|
1039
|
+
try {
|
|
1040
|
+
entries = await store.list(dir);
|
|
1041
|
+
} catch {
|
|
1042
|
+
return out;
|
|
1043
|
+
}
|
|
1044
|
+
for (const entry of entries) {
|
|
1045
|
+
const path = dir === "" ? entry.name : `${dir}/${entry.name}`;
|
|
1046
|
+
if (entry.kind === "dir") {
|
|
1047
|
+
out.push(...await collectContentItems(store, path));
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
if (!entry.name.endsWith(".json") || entry.name.startsWith("_")) continue;
|
|
1051
|
+
let data;
|
|
1052
|
+
try {
|
|
1053
|
+
data = JSON.parse(await store.readText(path));
|
|
1054
|
+
} catch {
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
if (typeof data !== "object" || data === null) continue;
|
|
1058
|
+
const obj = data;
|
|
1059
|
+
const template = obj["template"];
|
|
1060
|
+
if (typeof template !== "string") continue;
|
|
1061
|
+
const fields = obj["fields"];
|
|
1062
|
+
out.push({
|
|
1063
|
+
path,
|
|
1064
|
+
template,
|
|
1065
|
+
fields: typeof fields === "object" && fields !== null ? fields : {}
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
return out;
|
|
1069
|
+
}
|
|
1070
|
+
async function classifySnapshot(store, path, components) {
|
|
1071
|
+
if (!await store.exists(path)) return "missing";
|
|
1072
|
+
try {
|
|
1073
|
+
const html = await store.readText(path);
|
|
1074
|
+
if (html.trim() === "") return "corrupt";
|
|
1075
|
+
analyzeTemplate(html, components);
|
|
1076
|
+
return "ok";
|
|
1077
|
+
} catch {
|
|
1078
|
+
return "corrupt";
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
async function anyPublishedContentItem(store, dir = "content") {
|
|
1082
|
+
let entries;
|
|
1083
|
+
try {
|
|
1084
|
+
entries = await store.list(dir);
|
|
1085
|
+
} catch {
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
for (const entry of entries) {
|
|
1089
|
+
const path = dir === "" ? entry.name : `${dir}/${entry.name}`;
|
|
1090
|
+
if (entry.kind === "dir") {
|
|
1091
|
+
if (await anyPublishedContentItem(store, path)) return true;
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
if (!entry.name.endsWith(".json")) continue;
|
|
1095
|
+
try {
|
|
1096
|
+
const text = await store.readText(path);
|
|
1097
|
+
const data = JSON.parse(text);
|
|
1098
|
+
if (typeof data === "object" && data !== null && data.isPublished === true) {
|
|
1099
|
+
return true;
|
|
1100
|
+
}
|
|
1101
|
+
} catch {
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return false;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// ../editor-api/src/branding.ts
|
|
1108
|
+
import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
1109
|
+
import { dirname, join } from "path";
|
|
1110
|
+
var EMPTY = { name: null, logo: null };
|
|
1111
|
+
var InMemoryBrandingStore = class {
|
|
1112
|
+
constructor(now = Date.now) {
|
|
1113
|
+
this.now = now;
|
|
1114
|
+
}
|
|
1115
|
+
now;
|
|
1116
|
+
name = null;
|
|
1117
|
+
logo = null;
|
|
1118
|
+
logoUpdatedAt = 0;
|
|
1119
|
+
async get() {
|
|
1120
|
+
return {
|
|
1121
|
+
name: this.name,
|
|
1122
|
+
logo: this.logo === null ? null : { contentType: this.logo.contentType, updatedAt: this.logoUpdatedAt }
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
async getLogo() {
|
|
1126
|
+
return this.logo;
|
|
1127
|
+
}
|
|
1128
|
+
async setName(name) {
|
|
1129
|
+
this.name = name;
|
|
1130
|
+
}
|
|
1131
|
+
async setLogo(bytes, contentType) {
|
|
1132
|
+
this.logo = { bytes, contentType };
|
|
1133
|
+
this.logoUpdatedAt = this.now();
|
|
1134
|
+
}
|
|
1135
|
+
async clearLogo() {
|
|
1136
|
+
this.logo = null;
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
var FileBrandingStore = class {
|
|
1140
|
+
constructor(metaPath, now = Date.now) {
|
|
1141
|
+
this.metaPath = metaPath;
|
|
1142
|
+
this.now = now;
|
|
1143
|
+
this.logoPath = join(dirname(metaPath), "branding-logo");
|
|
1144
|
+
}
|
|
1145
|
+
metaPath;
|
|
1146
|
+
now;
|
|
1147
|
+
logoPath;
|
|
1148
|
+
async loadMeta() {
|
|
1149
|
+
try {
|
|
1150
|
+
const raw = await readFile(this.metaPath, "utf8");
|
|
1151
|
+
const text = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
|
|
1152
|
+
const parsed = JSON.parse(text);
|
|
1153
|
+
return {
|
|
1154
|
+
name: typeof parsed.name === "string" ? parsed.name : null,
|
|
1155
|
+
logo: parsed.logo && typeof parsed.logo.contentType === "string" ? {
|
|
1156
|
+
contentType: parsed.logo.contentType,
|
|
1157
|
+
updatedAt: Number(parsed.logo.updatedAt) || 0
|
|
1158
|
+
} : null
|
|
1159
|
+
};
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
if (error.code === "ENOENT") return EMPTY;
|
|
1162
|
+
throw error;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
async saveMeta(state) {
|
|
1166
|
+
await mkdir(dirname(this.metaPath), { recursive: true });
|
|
1167
|
+
const tmp = `${this.metaPath}.tmp`;
|
|
1168
|
+
await writeFile(tmp, JSON.stringify(state, null, 2), "utf8");
|
|
1169
|
+
await rename(tmp, this.metaPath);
|
|
1170
|
+
}
|
|
1171
|
+
async get() {
|
|
1172
|
+
return this.loadMeta();
|
|
1173
|
+
}
|
|
1174
|
+
async getLogo() {
|
|
1175
|
+
const meta = await this.loadMeta();
|
|
1176
|
+
if (meta.logo === null) return null;
|
|
1177
|
+
try {
|
|
1178
|
+
const bytes = await readFile(this.logoPath);
|
|
1179
|
+
return { bytes: new Uint8Array(bytes), contentType: meta.logo.contentType };
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
if (error.code === "ENOENT") return null;
|
|
1182
|
+
throw error;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
async setName(name) {
|
|
1186
|
+
const meta = await this.loadMeta();
|
|
1187
|
+
await this.saveMeta({ name, logo: meta.logo });
|
|
1188
|
+
}
|
|
1189
|
+
async setLogo(bytes, contentType) {
|
|
1190
|
+
const meta = await this.loadMeta();
|
|
1191
|
+
await mkdir(dirname(this.logoPath), { recursive: true });
|
|
1192
|
+
const tmp = `${this.logoPath}.tmp`;
|
|
1193
|
+
await writeFile(tmp, bytes);
|
|
1194
|
+
await rename(tmp, this.logoPath);
|
|
1195
|
+
await this.saveMeta({ name: meta.name, logo: { contentType, updatedAt: this.now() } });
|
|
1196
|
+
}
|
|
1197
|
+
async clearLogo() {
|
|
1198
|
+
const meta = await this.loadMeta();
|
|
1199
|
+
await rm(this.logoPath, { force: true });
|
|
1200
|
+
await this.saveMeta({ name: meta.name, logo: null });
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
export {
|
|
1205
|
+
SCAFFOLD_FILES,
|
|
1206
|
+
handleApi,
|
|
1207
|
+
authorOptions,
|
|
1208
|
+
authorDirectory,
|
|
1209
|
+
MCP_TOOLS,
|
|
1210
|
+
callMcpTool,
|
|
1211
|
+
MCP_RESOURCES,
|
|
1212
|
+
readMcpResource,
|
|
1213
|
+
buildDefaultDiagnostics,
|
|
1214
|
+
homeTemplateMissingDiagnostic,
|
|
1215
|
+
recreateHomeTemplateRepair,
|
|
1216
|
+
noPublishedContentDiagnostic,
|
|
1217
|
+
templateSnapshotIntegrityDiagnostic,
|
|
1218
|
+
rebindItemToCurrentRepair,
|
|
1219
|
+
templateSuffixConflictDiagnostic,
|
|
1220
|
+
copySnapshotToCurrentRepair,
|
|
1221
|
+
pendingMigrationsDiagnostic,
|
|
1222
|
+
InMemoryBrandingStore,
|
|
1223
|
+
FileBrandingStore
|
|
1224
|
+
};
|