@xtrable-ltd/nanoesis 0.1.1 → 0.1.4
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-G2UEZTYC.js → chunk-S23S4ZAK.js} +50 -2
- package/dist/editor-api.d.ts +38 -4
- package/dist/editor-api.js +165 -3
- package/dist/index.d.ts +86 -34
- package/dist/index.js +5 -1
- package/editor/assets/{TemplatesPane-5qsDAK_B.js → TemplatesPane-Cjwu8SOY.js} +7 -7
- package/editor/assets/{cssMode-CGp4MIjR.js → cssMode-CIqtqlOX.js} +1 -1
- package/editor/assets/{freemarker2-CJkwxmPv.js → freemarker2-03aLbgAQ.js} +1 -1
- package/editor/assets/{handlebars-CKb5i2nM.js → handlebars-CsCT0Kz-.js} +1 -1
- package/editor/assets/{html-DyMbQx0w.js → html-BMDaxhoV.js} +1 -1
- package/editor/assets/{htmlMode-DVPeqtn-.js → htmlMode-BmX-GJzB.js} +1 -1
- package/editor/assets/{index-CbuWEnUB.css → index-DSooOnUR.css} +1 -1
- package/editor/assets/index-xCwd0bS3.js +138 -0
- package/editor/assets/{javascript-Bp1Qh9wR.js → javascript-y8MJYJ58.js} +1 -1
- package/editor/assets/{jsonMode-FLEeVtx7.js → jsonMode-DPpxOVap.js} +1 -1
- package/editor/assets/{liquid-Bh8c534t.js → liquid-CGoDffSW.js} +1 -1
- package/editor/assets/{mdx-BUbo8M9l.js → mdx-CMvr2LP2.js} +1 -1
- package/editor/assets/{python-CuJlk8g3.js → python-Dg93CFNM.js} +1 -1
- package/editor/assets/{razor-CuQT_1Ku.js → razor-D8HipAex.js} +1 -1
- package/editor/assets/{tsMode-CT2HUNtN.js → tsMode-Drl3yYU0.js} +1 -1
- package/editor/assets/{typescript-CtMx97cn.js → typescript-Dcqumk7N.js} +1 -1
- package/editor/assets/{xml-CyfpINj_.js → xml-zoMMlERG.js} +1 -1
- package/editor/assets/{yaml-BBWmgfMA.js → yaml-BMaJnJHN.js} +1 -1
- package/editor/index.html +2 -2
- package/package.json +1 -1
- package/editor/assets/index-DJmSgobK.js +0 -129
|
@@ -182,6 +182,16 @@ async function saveIndex(store, prev, nextKeys) {
|
|
|
182
182
|
await store.put(INDEX_KEY, serialize(next));
|
|
183
183
|
return next;
|
|
184
184
|
}
|
|
185
|
+
async function reconcileIndex(store, actualKeys) {
|
|
186
|
+
const prev = await loadIndex(store);
|
|
187
|
+
const keys = [...new Set(actualKeys.filter((key) => !key.startsWith(RESERVED_PREFIX)))].sort();
|
|
188
|
+
const prevKeys = new Set(prev.keys);
|
|
189
|
+
const nextKeys = new Set(keys);
|
|
190
|
+
const added = keys.filter((key) => !prevKeys.has(key));
|
|
191
|
+
const removed = prev.keys.filter((key) => !nextKeys.has(key));
|
|
192
|
+
if (added.length === 0 && removed.length === 0) return { index: prev, added, removed };
|
|
193
|
+
return { index: await saveIndex(store, prev, keys), added, removed };
|
|
194
|
+
}
|
|
185
195
|
function freezeIndex(version, keys) {
|
|
186
196
|
const sorted = [...new Set(keys)].sort();
|
|
187
197
|
return { version, keys: sorted, checksum: checksumOf(version, sorted) };
|
|
@@ -312,6 +322,19 @@ var IndexedStore = class {
|
|
|
312
322
|
this.index = await saveIndex(this.store, index, next);
|
|
313
323
|
return { ok: true };
|
|
314
324
|
}
|
|
325
|
+
/**
|
|
326
|
+
* Rebuild this store's index from the *actual* keys the underlying store holds (DESIGN
|
|
327
|
+
* §11d), recovering files that arrived by a path that bypassed the index. A
|
|
328
|
+
* {@link BlobStore} cannot enumerate itself, so the caller supplies the real key set
|
|
329
|
+
* from an adapter that can (e.g. `BlobContainer.list`). Unlike calling
|
|
330
|
+
* {@link reconcileIndex} on a fresh store, this also refreshes the cached in-memory
|
|
331
|
+
* index, so this live instance sees the recovered files immediately.
|
|
332
|
+
*/
|
|
333
|
+
async reconcile(actualKeys) {
|
|
334
|
+
const result = await reconcileIndex(this.store, actualKeys);
|
|
335
|
+
this.index = result.index;
|
|
336
|
+
return result;
|
|
337
|
+
}
|
|
315
338
|
};
|
|
316
339
|
function guarded(key) {
|
|
317
340
|
if (key === "" || key.startsWith(RESERVED_PREFIX)) {
|
|
@@ -2407,8 +2430,9 @@ var noopPurgeService = {
|
|
|
2407
2430
|
var DEFAULT_WRITE_CONCURRENCY = 8;
|
|
2408
2431
|
async function publishSite(source, sink, options = {}) {
|
|
2409
2432
|
const validation = await validateSite(source);
|
|
2433
|
+
const summary = summarizeTree(await loadContentTree(source));
|
|
2410
2434
|
if (!validation.ok) {
|
|
2411
|
-
return { ok: false, validation, written: [] };
|
|
2435
|
+
return { ok: false, validation, written: [], summary };
|
|
2412
2436
|
}
|
|
2413
2437
|
const {
|
|
2414
2438
|
purge = noopPurgeService,
|
|
@@ -2431,7 +2455,29 @@ async function publishSite(source, sink, options = {}) {
|
|
|
2431
2455
|
([path, contents]) => sink.write(path, contents)
|
|
2432
2456
|
);
|
|
2433
2457
|
await purge.purgeAll();
|
|
2434
|
-
return { ok: true, validation, written: [...byPath.keys()].sort() };
|
|
2458
|
+
return { ok: true, validation, written: [...byPath.keys()].sort(), summary };
|
|
2459
|
+
}
|
|
2460
|
+
function summarizeTree(root) {
|
|
2461
|
+
let published = 0;
|
|
2462
|
+
let drafts = 0;
|
|
2463
|
+
const walk = (node) => {
|
|
2464
|
+
for (const child of node.children) {
|
|
2465
|
+
if (child.kind === "dir") {
|
|
2466
|
+
walk(child);
|
|
2467
|
+
} else if (child.item.isPublished) {
|
|
2468
|
+
published += 1;
|
|
2469
|
+
} else {
|
|
2470
|
+
drafts += 1;
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
};
|
|
2474
|
+
walk(root);
|
|
2475
|
+
if (published > 0) return { published, drafts };
|
|
2476
|
+
return {
|
|
2477
|
+
published,
|
|
2478
|
+
drafts,
|
|
2479
|
+
firstPublishWarning: drafts > 0 ? `Your site has no published pages \u2014 ${drafts} draft${drafts === 1 ? "" : "s"} were skipped. Toggle Publish on at least one page to go live.` : "Your site has no published pages. Create a page and toggle Publish on to go live."
|
|
2480
|
+
};
|
|
2435
2481
|
}
|
|
2436
2482
|
async function collectPublic(source, publicDir) {
|
|
2437
2483
|
if (!await source.exists(publicDir)) return [];
|
|
@@ -2485,9 +2531,11 @@ export {
|
|
|
2485
2531
|
parseContentItem,
|
|
2486
2532
|
parseSortFile,
|
|
2487
2533
|
InMemoryBlobStore,
|
|
2534
|
+
RESERVED_PREFIX,
|
|
2488
2535
|
emptyIndex,
|
|
2489
2536
|
loadIndex,
|
|
2490
2537
|
saveIndex,
|
|
2538
|
+
reconcileIndex,
|
|
2491
2539
|
IndexedStore,
|
|
2492
2540
|
InMemoryContentSource,
|
|
2493
2541
|
parseRedirects,
|
package/dist/editor-api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WorkingStore, IdentityProvider, PublishResult, AuthEndpoints, UserAdminEndpoints, AuthorOption, UserSummary, AuthorDirectory } from '@nanoesis/engine';
|
|
1
|
+
import { WorkingStore, IdentityProvider, PublishResult, ReconcileResult, AuthEndpoints, UserAdminEndpoints, AuthorOption, UserSummary, AuthorDirectory } from '@nanoesis/engine';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Editor white-labelling storage (DESIGN §11, Phase B). This is the *editor app's*
|
|
@@ -100,6 +100,15 @@ interface ApiDeps {
|
|
|
100
100
|
readonly identity: IdentityProvider;
|
|
101
101
|
/** Run the publish pipeline (host binds source/sink/purge). */
|
|
102
102
|
readonly publish: () => Promise<PublishResult>;
|
|
103
|
+
/**
|
|
104
|
+
* Optional index-reconcile (DESIGN §11d): rebuild the working store's content index
|
|
105
|
+
* from the keys actually present, recovering files that arrived by a path that
|
|
106
|
+
* bypassed the index (the silent-bootstrap failure). When present, the router mounts
|
|
107
|
+
* the admin-gated `POST /api/reconcile`. The host binds it because enumerating the
|
|
108
|
+
* real key set needs the adapter's listing (`BlobContainer.list`), which the engine's
|
|
109
|
+
* `BlobStore` deliberately lacks. Omitted on stores that cannot enumerate.
|
|
110
|
+
*/
|
|
111
|
+
readonly reconcile?: () => Promise<ReconcileResult>;
|
|
103
112
|
/**
|
|
104
113
|
* Optional credential-management surface (DESIGN §11). When present, the router
|
|
105
114
|
* mounts `/api/login`, `/api/refresh`, `/api/logout`, and `/api/auth/state`,
|
|
@@ -131,8 +140,12 @@ interface ApiDeps {
|
|
|
131
140
|
*/
|
|
132
141
|
readonly authors?: () => Promise<readonly AuthorOption[]>;
|
|
133
142
|
}
|
|
134
|
-
/**
|
|
135
|
-
*
|
|
143
|
+
/**
|
|
144
|
+
* Dispatch one API request. The editor is private, so every route requires an editing
|
|
145
|
+
* role (author/developer/admin); writes are further partitioned by path (DESIGN §11).
|
|
146
|
+
* Every response then passes through {@link withDefaultHeaders} so that no `/api/*`
|
|
147
|
+
* response is silently cached (DESIGN §13).
|
|
148
|
+
*/
|
|
136
149
|
declare function handleApi(deps: ApiDeps, req: ApiRequest): Promise<ApiResponse>;
|
|
137
150
|
|
|
138
151
|
/** The byline picker's options, display name + stable handle, sorted by display name. */
|
|
@@ -195,4 +208,25 @@ declare function readMcpResource(uri: string): {
|
|
|
195
208
|
readonly mimeType: string;
|
|
196
209
|
} | undefined;
|
|
197
210
|
|
|
198
|
-
|
|
211
|
+
/**
|
|
212
|
+
* The first-run starter site (DESIGN §14): a minimal, self-contained set of files that
|
|
213
|
+
* turns an empty working store into a real, publishable site. It is the *blessed* way to
|
|
214
|
+
* get initial content in — written through the editor API (so the content index stays
|
|
215
|
+
* authoritative by construction, never the bypassing path that silently breaks a store,
|
|
216
|
+
* §11d) and bundled here as **pure data**, so the engine's purity is untouched and the
|
|
217
|
+
* scaffold works identically for every host and the MCP server with no per-host wiring.
|
|
218
|
+
*
|
|
219
|
+
* Deliberately minimal (the owner's call, distinct from the fuller `examples/starter`):
|
|
220
|
+
* one home page, a page template, the document shell, and a stylesheet. It is gate-clean
|
|
221
|
+
* by construction — no collection loops (nothing to point at yet), no custom components,
|
|
222
|
+
* no required fields left unset — so a fresh adopter can publish it immediately, and the
|
|
223
|
+
* home page is a **draft** (`isPublished: false`) so going live is a deliberate act.
|
|
224
|
+
*/
|
|
225
|
+
/**
|
|
226
|
+
* The starter files, keyed by working-store path. Written through the store's `write`
|
|
227
|
+
* (so the index records each), in this order — templates and styles before the content
|
|
228
|
+
* that depends on them, though the store has no cross-file ordering requirement.
|
|
229
|
+
*/
|
|
230
|
+
declare const SCAFFOLD_FILES: Readonly<Record<string, string>>;
|
|
231
|
+
|
|
232
|
+
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, callMcpTool, handleApi, readMcpResource };
|
package/dist/editor-api.js
CHANGED
|
@@ -8,7 +8,111 @@ import {
|
|
|
8
8
|
loadTemplate,
|
|
9
9
|
renderReferenceMarkdown,
|
|
10
10
|
validateSite
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-S23S4ZAK.js";
|
|
12
|
+
|
|
13
|
+
// ../editor-api/src/scaffold.ts
|
|
14
|
+
var DOCUMENT_HTML = `<!doctype html>
|
|
15
|
+
<html lang="en">
|
|
16
|
+
<head>
|
|
17
|
+
<meta charset="utf-8" />
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
19
|
+
<title>{title}</title>
|
|
20
|
+
<meta name="description" content="{meta_description}" />
|
|
21
|
+
<link rel="stylesheet" href="/styles.css" />
|
|
22
|
+
</head>
|
|
23
|
+
<body>
|
|
24
|
+
<header class="site-header">
|
|
25
|
+
<a class="brand" href="/">Your site</a>
|
|
26
|
+
</header>
|
|
27
|
+
<main>
|
|
28
|
+
<slot></slot>
|
|
29
|
+
</main>
|
|
30
|
+
<footer class="site-footer">
|
|
31
|
+
<p>Built with nanoesis \u2014 static HTML, no runtime.</p>
|
|
32
|
+
</footer>
|
|
33
|
+
</body>
|
|
34
|
+
</html>
|
|
35
|
+
`;
|
|
36
|
+
var HOME_HTML = `<section class="hero">
|
|
37
|
+
<h1>{title}</h1>
|
|
38
|
+
<p class="lead">{lead}</p>
|
|
39
|
+
</section>
|
|
40
|
+
<div class="prose" data-type="richtext">{body}</div>
|
|
41
|
+
`;
|
|
42
|
+
var HOME_JSON = `${JSON.stringify(
|
|
43
|
+
{
|
|
44
|
+
template: "home",
|
|
45
|
+
title: "Welcome",
|
|
46
|
+
isPublished: false,
|
|
47
|
+
fields: {
|
|
48
|
+
lead: "Edit this page in the nanoesis editor, then publish when you are ready.",
|
|
49
|
+
body: "<p>This is your new site. It compiles to plain static HTML \u2014 there is no server and no client framework, and nothing of the editor survives into production.</p>"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
null,
|
|
53
|
+
2
|
|
54
|
+
)}
|
|
55
|
+
`;
|
|
56
|
+
var STYLES_CSS = `:root {
|
|
57
|
+
--ink: #1a1a2e;
|
|
58
|
+
--muted: #555;
|
|
59
|
+
--accent: #4f46e5;
|
|
60
|
+
--max: 42rem;
|
|
61
|
+
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
|
62
|
+
line-height: 1.6;
|
|
63
|
+
color: var(--ink);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
body {
|
|
67
|
+
margin: 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.site-header,
|
|
71
|
+
main,
|
|
72
|
+
.site-footer {
|
|
73
|
+
max-width: var(--max);
|
|
74
|
+
margin-inline: auto;
|
|
75
|
+
padding: 1.5rem 1.25rem;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.site-header .brand {
|
|
79
|
+
font-weight: 700;
|
|
80
|
+
text-decoration: none;
|
|
81
|
+
color: var(--ink);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.hero h1 {
|
|
85
|
+
font-size: 2.25rem;
|
|
86
|
+
letter-spacing: -0.01em;
|
|
87
|
+
margin: 0 0 0.5rem;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.lead {
|
|
91
|
+
font-size: 1.2rem;
|
|
92
|
+
color: var(--muted);
|
|
93
|
+
margin: 0 0 2rem;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.prose {
|
|
97
|
+
font-size: 1.05rem;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
a {
|
|
101
|
+
color: var(--accent);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.site-footer {
|
|
105
|
+
color: var(--muted);
|
|
106
|
+
font-size: 0.9rem;
|
|
107
|
+
border-top: 1px solid #eee;
|
|
108
|
+
}
|
|
109
|
+
`;
|
|
110
|
+
var SCAFFOLD_FILES = {
|
|
111
|
+
"templates/document.html": DOCUMENT_HTML,
|
|
112
|
+
"templates/home.html": HOME_HTML,
|
|
113
|
+
"public/styles.css": STYLES_CSS,
|
|
114
|
+
"content/index.json": HOME_JSON
|
|
115
|
+
};
|
|
12
116
|
|
|
13
117
|
// ../editor-api/src/api.ts
|
|
14
118
|
var MAX_LOGO_BYTES = 1024 * 1024;
|
|
@@ -32,6 +136,14 @@ function json(status, data) {
|
|
|
32
136
|
function methodNotAllowed() {
|
|
33
137
|
return json(405, { ok: false, error: "POST only" });
|
|
34
138
|
}
|
|
139
|
+
function withDefaultHeaders(response) {
|
|
140
|
+
const headers = { ...response.headers ?? {} };
|
|
141
|
+
if (headers["cache-control"] === void 0) {
|
|
142
|
+
headers["cache-control"] = "no-store";
|
|
143
|
+
if (headers["vary"] === void 0) headers["vary"] = "Authorization";
|
|
144
|
+
}
|
|
145
|
+
return { ...response, headers };
|
|
146
|
+
}
|
|
35
147
|
var BEARER_PREFIX = /^Bearer\s+/i;
|
|
36
148
|
function bearerToken(getHeader) {
|
|
37
149
|
const raw = getHeader("authorization") ?? getHeader("Authorization");
|
|
@@ -200,6 +312,9 @@ async function handleBrandingRoute(deps, req) {
|
|
|
200
312
|
return json(405, { ok: false, error: "method not allowed" });
|
|
201
313
|
}
|
|
202
314
|
async function handleApi(deps, req) {
|
|
315
|
+
return withDefaultHeaders(await dispatchApi(deps, req));
|
|
316
|
+
}
|
|
317
|
+
async function dispatchApi(deps, req) {
|
|
203
318
|
const authResponse = await handleAuthRoute(deps, req);
|
|
204
319
|
if (authResponse !== void 0) return authResponse;
|
|
205
320
|
const brandingResponse = await handleBrandingRoute(deps, req);
|
|
@@ -280,8 +395,54 @@ async function handleApi(deps, req) {
|
|
|
280
395
|
case "/api/publish": {
|
|
281
396
|
if (req.method !== "POST") return methodNotAllowed();
|
|
282
397
|
const result = await deps.publish();
|
|
283
|
-
if (result.ok)
|
|
284
|
-
|
|
398
|
+
if (result.ok) {
|
|
399
|
+
return json(200, {
|
|
400
|
+
ok: true,
|
|
401
|
+
written: result.written.length,
|
|
402
|
+
...result.summary !== void 0 && { summary: result.summary }
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
return json(422, {
|
|
406
|
+
ok: false,
|
|
407
|
+
errors: result.validation.errors.map((e) => e.message),
|
|
408
|
+
...result.summary !== void 0 && { summary: result.summary }
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
case "/api/scaffold": {
|
|
412
|
+
if (req.method !== "POST") return methodNotAllowed();
|
|
413
|
+
if (!hasRole(principal, "admin")) {
|
|
414
|
+
return json(403, { ok: false, error: "admin role required" });
|
|
415
|
+
}
|
|
416
|
+
if ((await deps.store.list("content")).length > 0) {
|
|
417
|
+
return json(409, { ok: false, error: "site already has content" });
|
|
418
|
+
}
|
|
419
|
+
const created = [];
|
|
420
|
+
const skipped = [];
|
|
421
|
+
for (const [path, content] of Object.entries(SCAFFOLD_FILES)) {
|
|
422
|
+
if (await deps.store.exists(path)) {
|
|
423
|
+
skipped.push(path);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
await deps.store.write(path, new TextEncoder().encode(content));
|
|
427
|
+
created.push(path);
|
|
428
|
+
}
|
|
429
|
+
return json(200, { ok: true, created, skipped });
|
|
430
|
+
}
|
|
431
|
+
case "/api/reconcile": {
|
|
432
|
+
if (req.method !== "POST") return methodNotAllowed();
|
|
433
|
+
if (deps.reconcile === void 0) {
|
|
434
|
+
return json(404, { ok: false, error: "reconcile is not available on this host" });
|
|
435
|
+
}
|
|
436
|
+
if (!hasRole(principal, "admin")) {
|
|
437
|
+
return json(403, { ok: false, error: "admin role required" });
|
|
438
|
+
}
|
|
439
|
+
const result = await deps.reconcile();
|
|
440
|
+
return json(200, {
|
|
441
|
+
ok: true,
|
|
442
|
+
added: result.added.length,
|
|
443
|
+
removed: result.removed.length,
|
|
444
|
+
total: result.index.keys.length
|
|
445
|
+
});
|
|
285
446
|
}
|
|
286
447
|
case "/api/me/password": {
|
|
287
448
|
if (req.method !== "POST") return methodNotAllowed();
|
|
@@ -584,6 +745,7 @@ export {
|
|
|
584
745
|
InMemoryBrandingStore,
|
|
585
746
|
MCP_RESOURCES,
|
|
586
747
|
MCP_TOOLS,
|
|
748
|
+
SCAFFOLD_FILES,
|
|
587
749
|
authorDirectory,
|
|
588
750
|
authorOptions,
|
|
589
751
|
callMcpTool,
|
package/dist/index.d.ts
CHANGED
|
@@ -203,6 +203,62 @@ interface WorkingStore extends ContentSource {
|
|
|
203
203
|
rename(from: string, to: string): Promise<RenameResult>;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
/**
|
|
207
|
+
* The content index (DESIGN §11d): the sorted set of every key in the working store,
|
|
208
|
+
* the directory listing a flat blob store cannot give cheaply, lifted into one object
|
|
209
|
+
* we own. Enumeration (the editor tree, compile) reads this, so the adopter's store
|
|
210
|
+
* stays get/put/delete with no `list`. Persisted under a reserved key with a fixed-size
|
|
211
|
+
* backup ring for integrity.
|
|
212
|
+
*/
|
|
213
|
+
interface ContentIndex {
|
|
214
|
+
/** Monotonic, bumped on every save; orders the backup ring during recovery. */
|
|
215
|
+
readonly version: number;
|
|
216
|
+
/** Every key that exists, sorted and de-duplicated (deterministic, CLAUDE §2). */
|
|
217
|
+
readonly keys: readonly string[];
|
|
218
|
+
/** Checksum over version + keys, so a corrupt stored index is caught on read. */
|
|
219
|
+
readonly checksum: string;
|
|
220
|
+
}
|
|
221
|
+
/** Reserved key prefix for the index and its backups; excluded from enumeration. */
|
|
222
|
+
declare const RESERVED_PREFIX = ".nanoesis/";
|
|
223
|
+
/** The empty index of a fresh store: version 0, no keys. */
|
|
224
|
+
declare function emptyIndex(): ContentIndex;
|
|
225
|
+
/**
|
|
226
|
+
* Load the index, recovering through the backup ring if the live copy is missing or
|
|
227
|
+
* corrupt (DESIGN §11d): the highest valid version among the ring slots wins. Returns
|
|
228
|
+
* the empty index when nothing valid is found, a fresh store, or the accepted residual
|
|
229
|
+
* case where the live index and every backup are lost (the files still exist by key,
|
|
230
|
+
* but cannot be enumerated until something rewrites the index).
|
|
231
|
+
*/
|
|
232
|
+
declare function loadIndex(store: BlobStore): Promise<ContentIndex>;
|
|
233
|
+
/**
|
|
234
|
+
* Write a new index whose keys are `nextKeys`, backing up the one it replaces first
|
|
235
|
+
* (DESIGN §11d). The previous index goes into its ring slot (`version % ring`, oldest
|
|
236
|
+
* overwritten), then the new index is written to the live key, a constant two writes
|
|
237
|
+
* per save regardless of ring size. Returns the new index (the caller's next `prev`).
|
|
238
|
+
*/
|
|
239
|
+
declare function saveIndex(store: BlobStore, prev: ContentIndex, nextKeys: readonly string[]): Promise<ContentIndex>;
|
|
240
|
+
/** What a {@link reconcileIndex} did, for a maintenance UI/CLI and the startup signal. */
|
|
241
|
+
interface ReconcileResult {
|
|
242
|
+
/** The index after reconciling (the prior one unchanged when nothing differed). */
|
|
243
|
+
readonly index: ContentIndex;
|
|
244
|
+
/** Keys now in the index that the prior index lacked (orphaned blobs, recovered). */
|
|
245
|
+
readonly added: readonly string[];
|
|
246
|
+
/** Keys the prior index held that no longer exist in the store (stale entries dropped). */
|
|
247
|
+
readonly removed: readonly string[];
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Rebuild the index from the store's *actual* keys (DESIGN §11d): the safety net for a
|
|
251
|
+
* store that got files by a path that bypassed the index, where the files are present
|
|
252
|
+
* by key but unenumerable. The engine cannot list a {@link BlobStore} (none is required
|
|
253
|
+
* to, §11c), so the caller supplies the real key set, an adapter that *can* enumerate
|
|
254
|
+
* (e.g. `BlobContainer.list`) provides it; the rebuild stays pure index logic here.
|
|
255
|
+
*
|
|
256
|
+
* The reserved namespace is never indexed (the index is not its own content). When the
|
|
257
|
+
* resulting key set matches the existing index this is a no-op, no write, no version
|
|
258
|
+
* bump, so reconciling a healthy store is cheap and idempotent.
|
|
259
|
+
*/
|
|
260
|
+
declare function reconcileIndex(store: BlobStore, actualKeys: readonly string[]): Promise<ReconcileResult>;
|
|
261
|
+
|
|
206
262
|
/**
|
|
207
263
|
* A {@link ContentSource} backed by the content index (DESIGN §11d) over a
|
|
208
264
|
* {@link BlobStore}: enumeration (`list`/`exists`) is synthesised from the index's key
|
|
@@ -251,41 +307,17 @@ declare class IndexedStore implements WorkingStore {
|
|
|
251
307
|
* still throws.)
|
|
252
308
|
*/
|
|
253
309
|
rename(from: string, to: string): Promise<RenameResult>;
|
|
310
|
+
/**
|
|
311
|
+
* Rebuild this store's index from the *actual* keys the underlying store holds (DESIGN
|
|
312
|
+
* §11d), recovering files that arrived by a path that bypassed the index. A
|
|
313
|
+
* {@link BlobStore} cannot enumerate itself, so the caller supplies the real key set
|
|
314
|
+
* from an adapter that can (e.g. `BlobContainer.list`). Unlike calling
|
|
315
|
+
* {@link reconcileIndex} on a fresh store, this also refreshes the cached in-memory
|
|
316
|
+
* index, so this live instance sees the recovered files immediately.
|
|
317
|
+
*/
|
|
318
|
+
reconcile(actualKeys: readonly string[]): Promise<ReconcileResult>;
|
|
254
319
|
}
|
|
255
320
|
|
|
256
|
-
/**
|
|
257
|
-
* The content index (DESIGN §11d): the sorted set of every key in the working store,
|
|
258
|
-
* the directory listing a flat blob store cannot give cheaply, lifted into one object
|
|
259
|
-
* we own. Enumeration (the editor tree, compile) reads this, so the adopter's store
|
|
260
|
-
* stays get/put/delete with no `list`. Persisted under a reserved key with a fixed-size
|
|
261
|
-
* backup ring for integrity.
|
|
262
|
-
*/
|
|
263
|
-
interface ContentIndex {
|
|
264
|
-
/** Monotonic, bumped on every save; orders the backup ring during recovery. */
|
|
265
|
-
readonly version: number;
|
|
266
|
-
/** Every key that exists, sorted and de-duplicated (deterministic, CLAUDE §2). */
|
|
267
|
-
readonly keys: readonly string[];
|
|
268
|
-
/** Checksum over version + keys, so a corrupt stored index is caught on read. */
|
|
269
|
-
readonly checksum: string;
|
|
270
|
-
}
|
|
271
|
-
/** The empty index of a fresh store: version 0, no keys. */
|
|
272
|
-
declare function emptyIndex(): ContentIndex;
|
|
273
|
-
/**
|
|
274
|
-
* Load the index, recovering through the backup ring if the live copy is missing or
|
|
275
|
-
* corrupt (DESIGN §11d): the highest valid version among the ring slots wins. Returns
|
|
276
|
-
* the empty index when nothing valid is found, a fresh store, or the accepted residual
|
|
277
|
-
* case where the live index and every backup are lost (the files still exist by key,
|
|
278
|
-
* but cannot be enumerated until something rewrites the index).
|
|
279
|
-
*/
|
|
280
|
-
declare function loadIndex(store: BlobStore): Promise<ContentIndex>;
|
|
281
|
-
/**
|
|
282
|
-
* Write a new index whose keys are `nextKeys`, backing up the one it replaces first
|
|
283
|
-
* (DESIGN §11d). The previous index goes into its ring slot (`version % ring`, oldest
|
|
284
|
-
* overwritten), then the new index is written to the live key, a constant two writes
|
|
285
|
-
* per save regardless of ring size. Returns the new index (the caller's next `prev`).
|
|
286
|
-
*/
|
|
287
|
-
declare function saveIndex(store: BlobStore, prev: ContentIndex, nextKeys: readonly string[]): Promise<ContentIndex>;
|
|
288
|
-
|
|
289
321
|
/**
|
|
290
322
|
* The authors byline (DESIGN §6.11). An `authors` field stores an ordered list of
|
|
291
323
|
* author refs; the compiler renders them as one grammatical line. Everything here
|
|
@@ -962,6 +994,24 @@ interface PublishOptions extends CompileSiteOptions {
|
|
|
962
994
|
/** Max sink writes in flight at once (default {@link DEFAULT_WRITE_CONCURRENCY}). */
|
|
963
995
|
readonly writeConcurrency?: number;
|
|
964
996
|
}
|
|
997
|
+
/**
|
|
998
|
+
* What the publish actually did to *content*, as opposed to artifacts. Counts come from a
|
|
999
|
+
* walk of the content tree (the same definition the compiler uses, `item.isPublished`).
|
|
1000
|
+
* Surfaced so a host/UI can explain a publish whose written-file count looks surprising,
|
|
1001
|
+
* the silent first-publish footgun: a published site with no published pages.
|
|
1002
|
+
*/
|
|
1003
|
+
interface PublishSummary {
|
|
1004
|
+
/** Content items with `isPublished: true` — the pages the publish included. */
|
|
1005
|
+
readonly published: number;
|
|
1006
|
+
/** Content items with `isPublished` false/absent — the pages the publish skipped. */
|
|
1007
|
+
readonly drafts: number;
|
|
1008
|
+
/**
|
|
1009
|
+
* A friendly one-liner when `published === 0`, so a UI can warn an operator who just
|
|
1010
|
+
* scaffolded a draft home and hit Publish that nothing went live. Absent when at least
|
|
1011
|
+
* one page is published.
|
|
1012
|
+
*/
|
|
1013
|
+
readonly firstPublishWarning?: string;
|
|
1014
|
+
}
|
|
965
1015
|
interface PublishResult {
|
|
966
1016
|
/** True when the validation gate passed and the site was published. */
|
|
967
1017
|
readonly ok: boolean;
|
|
@@ -969,6 +1019,8 @@ interface PublishResult {
|
|
|
969
1019
|
readonly validation: ValidationResult;
|
|
970
1020
|
/** Output paths written to the sink, sorted; empty when validation blocked the publish. */
|
|
971
1021
|
readonly written: readonly string[];
|
|
1022
|
+
/** Per-content counts (the human-readable side of `written`); present when the tree loaded. */
|
|
1023
|
+
readonly summary?: PublishSummary;
|
|
972
1024
|
}
|
|
973
1025
|
/**
|
|
974
1026
|
* Run the Phase-1 publish pipeline (DESIGN §11) through ports: **validate → compile →
|
|
@@ -1235,4 +1287,4 @@ declare function escapeJsonStringContent(value: string): string;
|
|
|
1235
1287
|
*/
|
|
1236
1288
|
declare function sanitizeUrl(url: string): string;
|
|
1237
1289
|
|
|
1238
|
-
export { type Artifact, type ArtifactSink, type AuthEndpoints, type AuthResult, type AuthorDirectory, type AuthorEntry, type AuthorOption, type AuthorRef, type AuthoringReference, type BlobStore, type BoundItem, type ChangePasswordRequest, type ChangePasswordSuccess, type CollectionConfig, type CollectionQuery, type CompileInput, type CompilePageOptions, type CompileSiteOptions, type CompiledPage, type ComponentMap, type ContentIndex, type ContentItem, ContentParseError, type ContentSource, type CreateTokenSuccess, type CreateUserRequest, DEFAULT_DIRS, DOCUMENT_SHELL, type DerivedField, type Diagnostic, type DirEntry, type DirNode, type EncodeRequest, type EncodedImage, type EncodedVariant, type EntryKind, FIELD_TYPES, type FieldPrimitive, type FieldRecord, type FieldType, type FieldTypeDef, type FieldValue, type IdentityProvider, type ImageEncoder, type ImageFormat, type ImageInfo, InMemoryArtifactSink, InMemoryBlobStore, InMemoryContentSource, IndexedStore, type ItemNode, type LengthConstraints, type LoginRequest, type LoginSuccess, type MediaResolver, type PageEntry, type PreBuildHook, type Principal, type PublishOptions, type PublishResult, type PurgeService, type RedirectRule, type ReferenceContext, type ReferenceEntry, type ReferenceSection, type RefreshSuccess, type RenameResult, type ResetPasswordRequest, type ResolveContext, type Role, type RssOptions, type Scope, type Severity, type SiteConfig, type SortFile, type TemplateAnalysis, type TokenContext, type TokenRef, type TreeNode, type UpdateUserRequest, type UserAdminEndpoints, type UserSummary, type ValidationResult, type ValueKind, type WorkingStore, analyzeTemplate, buildAuthoringReference, buildContentIndex, buildPictureMarkup, buildRedirects, buildResolveContext, buildRss, buildSitemap, canEdit, compilePage, compileSite, compileTemplate, contentHash, contentTypeFor, deriveFields, emptyIndex, escapeHtmlAttribute, escapeHtmlText, escapeJsonStringContent, findTokens, hasRole, humanize, inferControl, isFieldType, joinAuthors, loadComponentScripts, loadComponentStyles, loadComponents, loadContentTree, loadDocumentShell, loadIndex, loadRedirects, loadSiteConfig, loadTemplate, noopPurgeService, outputPathForItem, parseContentItem, parseRedirects, parseSortFile, processImage, publishSite, renderAuthors, renderReferenceMarkdown, sanitizeUrl, saveIndex, slugify, textContent, toAuthorRefs, urlForItem, validateSite, valueKindOf, wholeValueToken };
|
|
1290
|
+
export { type Artifact, type ArtifactSink, type AuthEndpoints, type AuthResult, type AuthorDirectory, type AuthorEntry, type AuthorOption, type AuthorRef, type AuthoringReference, type BlobStore, type BoundItem, type ChangePasswordRequest, type ChangePasswordSuccess, type CollectionConfig, type CollectionQuery, type CompileInput, type CompilePageOptions, type CompileSiteOptions, type CompiledPage, type ComponentMap, type ContentIndex, type ContentItem, ContentParseError, type ContentSource, type CreateTokenSuccess, type CreateUserRequest, DEFAULT_DIRS, DOCUMENT_SHELL, type DerivedField, type Diagnostic, type DirEntry, type DirNode, type EncodeRequest, type EncodedImage, type EncodedVariant, type EntryKind, FIELD_TYPES, type FieldPrimitive, type FieldRecord, type FieldType, type FieldTypeDef, type FieldValue, type IdentityProvider, type ImageEncoder, type ImageFormat, type ImageInfo, InMemoryArtifactSink, InMemoryBlobStore, InMemoryContentSource, IndexedStore, type ItemNode, type LengthConstraints, type LoginRequest, type LoginSuccess, type MediaResolver, type PageEntry, type PreBuildHook, type Principal, type PublishOptions, type PublishResult, type PublishSummary, type PurgeService, RESERVED_PREFIX, type ReconcileResult, type RedirectRule, type ReferenceContext, type ReferenceEntry, type ReferenceSection, type RefreshSuccess, type RenameResult, type ResetPasswordRequest, type ResolveContext, type Role, type RssOptions, type Scope, type Severity, type SiteConfig, type SortFile, type TemplateAnalysis, type TokenContext, type TokenRef, type TreeNode, type UpdateUserRequest, type UserAdminEndpoints, type UserSummary, type ValidationResult, type ValueKind, type WorkingStore, analyzeTemplate, buildAuthoringReference, buildContentIndex, buildPictureMarkup, buildRedirects, buildResolveContext, buildRss, buildSitemap, canEdit, compilePage, compileSite, compileTemplate, contentHash, contentTypeFor, deriveFields, emptyIndex, escapeHtmlAttribute, escapeHtmlText, escapeJsonStringContent, findTokens, hasRole, humanize, inferControl, isFieldType, joinAuthors, loadComponentScripts, loadComponentStyles, loadComponents, loadContentTree, loadDocumentShell, loadIndex, loadRedirects, loadSiteConfig, loadTemplate, noopPurgeService, outputPathForItem, parseContentItem, parseRedirects, parseSortFile, processImage, publishSite, reconcileIndex, renderAuthors, renderReferenceMarkdown, sanitizeUrl, saveIndex, slugify, textContent, toAuthorRefs, urlForItem, validateSite, valueKindOf, wholeValueToken };
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
InMemoryBlobStore,
|
|
8
8
|
InMemoryContentSource,
|
|
9
9
|
IndexedStore,
|
|
10
|
+
RESERVED_PREFIX,
|
|
10
11
|
analyzeTemplate,
|
|
11
12
|
buildAuthoringReference,
|
|
12
13
|
buildContentIndex,
|
|
@@ -48,6 +49,7 @@ import {
|
|
|
48
49
|
parseSortFile,
|
|
49
50
|
processImage,
|
|
50
51
|
publishSite,
|
|
52
|
+
reconcileIndex,
|
|
51
53
|
renderAuthors,
|
|
52
54
|
renderReferenceMarkdown,
|
|
53
55
|
sanitizeUrl,
|
|
@@ -59,7 +61,7 @@ import {
|
|
|
59
61
|
validateSite,
|
|
60
62
|
valueKindOf,
|
|
61
63
|
wholeValueToken
|
|
62
|
-
} from "./chunk-
|
|
64
|
+
} from "./chunk-S23S4ZAK.js";
|
|
63
65
|
export {
|
|
64
66
|
ContentParseError,
|
|
65
67
|
DEFAULT_DIRS,
|
|
@@ -69,6 +71,7 @@ export {
|
|
|
69
71
|
InMemoryBlobStore,
|
|
70
72
|
InMemoryContentSource,
|
|
71
73
|
IndexedStore,
|
|
74
|
+
RESERVED_PREFIX,
|
|
72
75
|
analyzeTemplate,
|
|
73
76
|
buildAuthoringReference,
|
|
74
77
|
buildContentIndex,
|
|
@@ -110,6 +113,7 @@ export {
|
|
|
110
113
|
parseSortFile,
|
|
111
114
|
processImage,
|
|
112
115
|
publishSite,
|
|
116
|
+
reconcileIndex,
|
|
113
117
|
renderAuthors,
|
|
114
118
|
renderReferenceMarkdown,
|
|
115
119
|
sanitizeUrl,
|