scribe-cms 0.0.1

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.
Files changed (96) hide show
  1. package/README.md +150 -0
  2. package/dist/cli/index.cjs +2303 -0
  3. package/dist/cli/index.cjs.map +1 -0
  4. package/dist/cli/index.d.ts +3 -0
  5. package/dist/cli/index.d.ts.map +1 -0
  6. package/dist/cli/index.js +2289 -0
  7. package/dist/cli/index.js.map +1 -0
  8. package/dist/index.cjs +2268 -0
  9. package/dist/index.cjs.map +1 -0
  10. package/dist/index.js +2238 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/runtime.cjs +1216 -0
  13. package/dist/runtime.cjs.map +1 -0
  14. package/dist/runtime.js +1207 -0
  15. package/dist/runtime.js.map +1 -0
  16. package/dist/src/config/load-config.d.ts +10 -0
  17. package/dist/src/config/load-config.d.ts.map +1 -0
  18. package/dist/src/config/load-env.d.ts +3 -0
  19. package/dist/src/config/load-env.d.ts.map +1 -0
  20. package/dist/src/config/resolve-config.d.ts +13 -0
  21. package/dist/src/config/resolve-config.d.ts.map +1 -0
  22. package/dist/src/core/alias-helpers.d.ts +7 -0
  23. package/dist/src/core/alias-helpers.d.ts.map +1 -0
  24. package/dist/src/core/builtin-fields.d.ts +38 -0
  25. package/dist/src/core/builtin-fields.d.ts.map +1 -0
  26. package/dist/src/core/field.d.ts +60 -0
  27. package/dist/src/core/field.d.ts.map +1 -0
  28. package/dist/src/core/introspect-schema.d.ts +20 -0
  29. package/dist/src/core/introspect-schema.d.ts.map +1 -0
  30. package/dist/src/core/slug-aliases.d.ts +27 -0
  31. package/dist/src/core/slug-aliases.d.ts.map +1 -0
  32. package/dist/src/core/types.d.ts +261 -0
  33. package/dist/src/core/types.d.ts.map +1 -0
  34. package/dist/src/create-project.d.ts +10 -0
  35. package/dist/src/create-project.d.ts.map +1 -0
  36. package/dist/src/create-scribe.d.ts +11 -0
  37. package/dist/src/create-scribe.d.ts.map +1 -0
  38. package/dist/src/hash/page-hash.d.ts +7 -0
  39. package/dist/src/hash/page-hash.d.ts.map +1 -0
  40. package/dist/src/history/record-revision.d.ts +12 -0
  41. package/dist/src/history/record-revision.d.ts.map +1 -0
  42. package/dist/src/i18n/build-url.d.ts +14 -0
  43. package/dist/src/i18n/build-url.d.ts.map +1 -0
  44. package/dist/src/i18n/resolve-document.d.ts +12 -0
  45. package/dist/src/i18n/resolve-document.d.ts.map +1 -0
  46. package/dist/src/i18n/translation-index.d.ts +7 -0
  47. package/dist/src/i18n/translation-index.d.ts.map +1 -0
  48. package/dist/src/index.d.ts +18 -0
  49. package/dist/src/index.d.ts.map +1 -0
  50. package/dist/src/loader/create-loader.d.ts +17 -0
  51. package/dist/src/loader/create-loader.d.ts.map +1 -0
  52. package/dist/src/loader/normalize-en.d.ts +3 -0
  53. package/dist/src/loader/normalize-en.d.ts.map +1 -0
  54. package/dist/src/loader/parse-mdx.d.ts +7 -0
  55. package/dist/src/loader/parse-mdx.d.ts.map +1 -0
  56. package/dist/src/redirects/build-redirects.d.ts +17 -0
  57. package/dist/src/redirects/build-redirects.d.ts.map +1 -0
  58. package/dist/src/redirects/translation-index.d.ts +4 -0
  59. package/dist/src/redirects/translation-index.d.ts.map +1 -0
  60. package/dist/src/redirects/types.d.ts +6 -0
  61. package/dist/src/redirects/types.d.ts.map +1 -0
  62. package/dist/src/runtime.d.ts +5 -0
  63. package/dist/src/runtime.d.ts.map +1 -0
  64. package/dist/src/sitemap/generate-sitemap.d.ts +5 -0
  65. package/dist/src/sitemap/generate-sitemap.d.ts.map +1 -0
  66. package/dist/src/sitemap/join-base-url.d.ts +3 -0
  67. package/dist/src/sitemap/join-base-url.d.ts.map +1 -0
  68. package/dist/src/sitemap/types.d.ts +39 -0
  69. package/dist/src/sitemap/types.d.ts.map +1 -0
  70. package/dist/src/storage/sqlite.d.ts +9 -0
  71. package/dist/src/storage/sqlite.d.ts.map +1 -0
  72. package/dist/src/storage/translations.d.ts +58 -0
  73. package/dist/src/storage/translations.d.ts.map +1 -0
  74. package/dist/src/translate/gemini-client.d.ts +15 -0
  75. package/dist/src/translate/gemini-client.d.ts.map +1 -0
  76. package/dist/src/translate/page-translator.d.ts +24 -0
  77. package/dist/src/translate/page-translator.d.ts.map +1 -0
  78. package/dist/src/translate/prompts/translation-prompt.d.ts +14 -0
  79. package/dist/src/translate/prompts/translation-prompt.d.ts.map +1 -0
  80. package/dist/src/translate/resolve-translate-config.d.ts +10 -0
  81. package/dist/src/translate/resolve-translate-config.d.ts.map +1 -0
  82. package/dist/src/translate/worklist.d.ts +19 -0
  83. package/dist/src/translate/worklist.d.ts.map +1 -0
  84. package/dist/src/validate/validate-project.d.ts +16 -0
  85. package/dist/src/validate/validate-project.d.ts.map +1 -0
  86. package/dist/src/validate/validate-relations.d.ts +19 -0
  87. package/dist/src/validate/validate-relations.d.ts.map +1 -0
  88. package/dist/src/validate/validate-slug-suffix.d.ts +12 -0
  89. package/dist/src/validate/validate-slug-suffix.d.ts.map +1 -0
  90. package/dist/studio/server.cjs +673 -0
  91. package/dist/studio/server.cjs.map +1 -0
  92. package/dist/studio/server.d.ts +7 -0
  93. package/dist/studio/server.d.ts.map +1 -0
  94. package/dist/studio/server.js +664 -0
  95. package/dist/studio/server.js.map +1 -0
  96. package/package.json +70 -0
@@ -0,0 +1,673 @@
1
+ 'use strict';
2
+
3
+ var nodeServer = require('@hono/node-server');
4
+ var hono = require('hono');
5
+ var crypto = require('crypto');
6
+ var fs2 = require('fs');
7
+ var path2 = require('path');
8
+ var matter = require('gray-matter');
9
+ var zod = require('zod');
10
+ var Database = require('better-sqlite3');
11
+
12
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
13
+
14
+ var fs2__default = /*#__PURE__*/_interopDefault(fs2);
15
+ var path2__default = /*#__PURE__*/_interopDefault(path2);
16
+ var matter__default = /*#__PURE__*/_interopDefault(matter);
17
+ var Database__default = /*#__PURE__*/_interopDefault(Database);
18
+
19
+ // studio/server.ts
20
+ function sha256(input) {
21
+ return crypto.createHash("sha256").update(input, "utf8").digest("hex");
22
+ }
23
+ function computePageEnHash(translatableFrontmatter, body) {
24
+ const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
25
+ return sha256(payload);
26
+ }
27
+
28
+ // src/i18n/build-url.ts
29
+ var SLUG_PLACEHOLDER = "{slug}";
30
+ function pathPrefix(template) {
31
+ const idx = template.indexOf(SLUG_PLACEHOLDER);
32
+ if (idx === -1) throw new Error(`Invalid path template (missing {slug}): ${template}`);
33
+ return template.slice(0, idx);
34
+ }
35
+ function pathSuffix(template) {
36
+ const idx = template.indexOf(SLUG_PLACEHOLDER);
37
+ if (idx === -1) throw new Error(`Invalid path template (missing {slug}): ${template}`);
38
+ return template.slice(idx + SLUG_PLACEHOLDER.length);
39
+ }
40
+ function resolvePath(template, slug, locale, defaultLocale) {
41
+ const relative = template.replace(SLUG_PLACEHOLDER, slug);
42
+ if (locale === defaultLocale) return relative;
43
+ return `/${locale}${relative}`;
44
+ }
45
+ function extractSlugFromResolvedPath(template, resolvedPath) {
46
+ const prefix = pathPrefix(template);
47
+ const suffix = pathSuffix(template);
48
+ if (!resolvedPath.startsWith(prefix)) return null;
49
+ if (suffix && !resolvedPath.endsWith(suffix)) return null;
50
+ const slugEnd = suffix ? resolvedPath.length - suffix.length : resolvedPath.length;
51
+ const slug = resolvedPath.slice(prefix.length, slugEnd);
52
+ return slug.length > 0 ? slug : null;
53
+ }
54
+
55
+ // src/core/builtin-fields.ts
56
+ var SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
57
+ var slugPatternSchema = zod.z.string().regex(SLUG_PATTERN, "slug must be lowercase-kebab-case");
58
+ var isoDateSchema = zod.z.string().regex(/^\d{4}-\d{2}-\d{2}(T[\d:.Z+-]*)?$/, "Use ISO date YYYY-MM-DD or full ISO 8601");
59
+ var canonicalPathSchema = zod.z.string().regex(/^\//, "canonicalPath must start with /");
60
+ function extractBuiltinEnFields(data, pathTemplate, enSlug, defaultLocale) {
61
+ const issues = [];
62
+ const rest = { ...data };
63
+ const aliasesResult = zod.z.array(slugPatternSchema).max(20).optional().default([]).safeParse(rest.aliases ?? []);
64
+ delete rest.aliases;
65
+ const redirectRaw = rest.redirect_to;
66
+ delete rest.redirect_to;
67
+ let redirectTo;
68
+ if (redirectRaw !== void 0 && redirectRaw !== null && redirectRaw !== "") {
69
+ if (typeof redirectRaw !== "string") {
70
+ issues.push({
71
+ field: "redirect_to",
72
+ message: "redirect_to must be a string path",
73
+ level: "error"
74
+ });
75
+ } else if (!pathTemplate) {
76
+ issues.push({
77
+ field: "redirect_to",
78
+ message: "redirect_to is not allowed on reference-only content types",
79
+ level: "error"
80
+ });
81
+ } else if (!extractSlugFromResolvedPath(pathTemplate, redirectRaw)) {
82
+ issues.push({
83
+ field: "redirect_to",
84
+ message: `redirect_to must match path template "${pathTemplate}" (prefix "${pathPrefix(pathTemplate)}"${pathSuffix(pathTemplate) ? `, suffix "${pathSuffix(pathTemplate)}"` : ""})`,
85
+ level: "error"
86
+ });
87
+ } else {
88
+ redirectTo = redirectRaw;
89
+ }
90
+ }
91
+ const aliases = aliasesResult.success ? aliasesResult.data : [];
92
+ if (!aliasesResult.success) {
93
+ for (const issue of aliasesResult.error.issues) {
94
+ issues.push({
95
+ field: `aliases.${issue.path.join(".")}`,
96
+ message: issue.message,
97
+ level: "error"
98
+ });
99
+ }
100
+ }
101
+ let publishedAt;
102
+ if (rest.publishedAt !== void 0 && rest.publishedAt !== null && rest.publishedAt !== "") {
103
+ const parsed = isoDateSchema.safeParse(rest.publishedAt);
104
+ if (parsed.success) {
105
+ publishedAt = parsed.data;
106
+ } else {
107
+ for (const issue of parsed.error.issues) {
108
+ issues.push({ field: "publishedAt", message: issue.message, level: "error" });
109
+ }
110
+ }
111
+ delete rest.publishedAt;
112
+ }
113
+ let updatedAt;
114
+ if (rest.updatedAt !== void 0 && rest.updatedAt !== null && rest.updatedAt !== "") {
115
+ const parsed = isoDateSchema.safeParse(rest.updatedAt);
116
+ if (parsed.success) {
117
+ updatedAt = parsed.data;
118
+ } else {
119
+ for (const issue of parsed.error.issues) {
120
+ issues.push({ field: "updatedAt", message: issue.message, level: "error" });
121
+ }
122
+ }
123
+ delete rest.updatedAt;
124
+ } else if (publishedAt) {
125
+ updatedAt = publishedAt;
126
+ }
127
+ let noindex = false;
128
+ if (rest.noindex !== void 0 && rest.noindex !== null && rest.noindex !== "") {
129
+ if (typeof rest.noindex === "boolean") {
130
+ noindex = rest.noindex;
131
+ } else {
132
+ issues.push({ field: "noindex", message: "noindex must be a boolean", level: "error" });
133
+ }
134
+ delete rest.noindex;
135
+ }
136
+ let canonicalPathOverride;
137
+ if (rest.canonicalPath !== void 0 && rest.canonicalPath !== null && rest.canonicalPath !== "") {
138
+ const parsed = canonicalPathSchema.safeParse(rest.canonicalPath);
139
+ if (parsed.success) {
140
+ canonicalPathOverride = parsed.data;
141
+ } else {
142
+ for (const issue of parsed.error.issues) {
143
+ issues.push({ field: "canonicalPath", message: issue.message, level: "error" });
144
+ }
145
+ }
146
+ delete rest.canonicalPath;
147
+ }
148
+ for (const internalKey of ["translationOf", "enSlug"]) {
149
+ if (rest[internalKey] !== void 0) {
150
+ issues.push({
151
+ field: internalKey,
152
+ message: `${internalKey} is managed internally by Scribe; remove from frontmatter`,
153
+ level: "warning"
154
+ });
155
+ delete rest[internalKey];
156
+ }
157
+ }
158
+ return {
159
+ builtin: {
160
+ aliases,
161
+ redirectTo,
162
+ publishedAt,
163
+ updatedAt,
164
+ noindex,
165
+ canonicalPathOverride
166
+ },
167
+ rest,
168
+ issues
169
+ };
170
+ }
171
+ function resolveCanonicalPathname(type, doc, defaultLocale) {
172
+ if (doc.canonicalPathOverride) {
173
+ if (doc.locale === defaultLocale) return doc.canonicalPathOverride;
174
+ return `/${doc.locale}${doc.canonicalPathOverride}`;
175
+ }
176
+ if (!type.path) return `/${doc.slug}`;
177
+ return resolvePath(type.path, doc.slug, doc.locale, defaultLocale);
178
+ }
179
+ function mergeBuiltinsIntoFrontmatter(frontmatter, doc, type, defaultLocale) {
180
+ const out = { ...frontmatter };
181
+ if (doc.publishedAt !== void 0) out.publishedAt = doc.publishedAt;
182
+ if (doc.updatedAt !== void 0) out.updatedAt = doc.updatedAt;
183
+ out.noindex = doc.noindex;
184
+ out.canonicalPath = resolveCanonicalPathname(type, doc, defaultLocale);
185
+ return out;
186
+ }
187
+ var FIELD_KIND = /* @__PURE__ */ Symbol.for("@genlook/scribe/fieldKind");
188
+ var RELATION_META = /* @__PURE__ */ Symbol.for("@genlook/scribe/relationMeta");
189
+ function getFieldKind(schema) {
190
+ const tagged = schema;
191
+ return tagged[FIELD_KIND] ?? "structural";
192
+ }
193
+ function getRelationTarget(schema) {
194
+ let current = schema;
195
+ for (let i = 0; i < 8; i++) {
196
+ const tagged = current;
197
+ if (tagged[RELATION_META]) {
198
+ return tagged[RELATION_META];
199
+ }
200
+ if (typeof tagged.unwrap === "function") {
201
+ current = tagged.unwrap();
202
+ continue;
203
+ }
204
+ if (typeof tagged.removeDefault === "function") {
205
+ current = tagged.removeDefault();
206
+ continue;
207
+ }
208
+ if (tagged._def?.innerType) {
209
+ current = tagged._def.innerType;
210
+ continue;
211
+ }
212
+ break;
213
+ }
214
+ return null;
215
+ }
216
+ function unwrapSchema(schema) {
217
+ let current = schema;
218
+ for (let i = 0; i < 8; i++) {
219
+ const anySchema = current;
220
+ if (typeof anySchema.unwrap === "function") {
221
+ current = anySchema.unwrap();
222
+ continue;
223
+ }
224
+ if (typeof anySchema.removeDefault === "function") {
225
+ current = anySchema.removeDefault();
226
+ continue;
227
+ }
228
+ if (anySchema._def?.innerType) {
229
+ current = anySchema._def.innerType;
230
+ continue;
231
+ }
232
+ break;
233
+ }
234
+ return current;
235
+ }
236
+
237
+ // src/core/introspect-schema.ts
238
+ function introspectSchema(schema, prefix = []) {
239
+ const relation = getRelationTarget(schema);
240
+ if (relation && prefix.length > 0) {
241
+ return [
242
+ {
243
+ path: prefix,
244
+ kind: "relation",
245
+ relationTarget: relation.typeId,
246
+ relationMultiple: relation.multiple,
247
+ relationOptional: relation.optional
248
+ }
249
+ ];
250
+ }
251
+ const base = unwrapSchema(schema);
252
+ if (base instanceof Object && "shape" in base) {
253
+ const shape = base.shape;
254
+ const fields = [];
255
+ for (const [key, child] of Object.entries(shape)) {
256
+ fields.push(...introspectSchema(child, [...prefix, key]));
257
+ }
258
+ return fields;
259
+ }
260
+ if (base instanceof Object && "element" in base) {
261
+ const element = base.element;
262
+ const elementBase = unwrapSchema(element);
263
+ if (elementBase instanceof Object && "shape" in elementBase) {
264
+ const shape = elementBase.shape;
265
+ const fields = [];
266
+ for (const [key, child] of Object.entries(shape)) {
267
+ fields.push(...introspectSchema(child, [...prefix, "*", key]));
268
+ }
269
+ return fields;
270
+ }
271
+ }
272
+ return [{ path: prefix, kind: getFieldKind(schema) }];
273
+ }
274
+ function extractByPaths(data, paths) {
275
+ const out = {};
276
+ for (const path4 of paths) {
277
+ const value = getAtPath(data, path4);
278
+ if (value !== void 0) {
279
+ setAtPath(out, path4.filter((p) => p !== "*"), value);
280
+ }
281
+ }
282
+ return out;
283
+ }
284
+ function pickTranslatable(data, schema) {
285
+ const translatablePaths = introspectSchema(schema).filter((f) => f.kind === "translatable").map((f) => f.path);
286
+ return extractByPaths(data, translatablePaths);
287
+ }
288
+ function getAtPath(obj, path4) {
289
+ let current = obj;
290
+ for (const segment of path4) {
291
+ if (segment === "*") {
292
+ if (!Array.isArray(current)) return void 0;
293
+ return current.map(
294
+ (item) => typeof item === "object" && item !== null ? getAtPath(item, path4.slice(path4.indexOf("*") + 1)) : void 0
295
+ );
296
+ }
297
+ if (typeof current !== "object" || current === null || Array.isArray(current)) return void 0;
298
+ current = current[segment];
299
+ }
300
+ return current;
301
+ }
302
+ function setAtPath(obj, path4, value) {
303
+ if (path4.length === 0) return;
304
+ if (path4.length === 1) {
305
+ obj[path4[0]] = value;
306
+ return;
307
+ }
308
+ const [head, ...rest] = path4;
309
+ if (!(head in obj) || typeof obj[head] !== "object" || obj[head] === null) {
310
+ obj[head] = {};
311
+ }
312
+ setAtPath(obj[head], rest, value);
313
+ }
314
+ var SCHEMA_VERSION = 2;
315
+ var MIGRATIONS = [
316
+ `CREATE TABLE IF NOT EXISTS meta (
317
+ key TEXT PRIMARY KEY,
318
+ value TEXT NOT NULL
319
+ )`,
320
+ `CREATE TABLE IF NOT EXISTS translations (
321
+ content_type TEXT NOT NULL,
322
+ en_slug TEXT NOT NULL,
323
+ locale TEXT NOT NULL,
324
+ slug TEXT NOT NULL,
325
+ frontmatter_json TEXT NOT NULL,
326
+ body TEXT NOT NULL,
327
+ en_hash TEXT NOT NULL,
328
+ translated_at TEXT NOT NULL,
329
+ model TEXT NOT NULL,
330
+ PRIMARY KEY (content_type, en_slug, locale)
331
+ )`,
332
+ `CREATE INDEX IF NOT EXISTS idx_translations_type_locale
333
+ ON translations(content_type, locale)`,
334
+ `CREATE TABLE IF NOT EXISTS revisions (
335
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
336
+ content_type TEXT NOT NULL,
337
+ en_slug TEXT NOT NULL,
338
+ locale TEXT,
339
+ revision_kind TEXT NOT NULL,
340
+ en_hash TEXT NOT NULL,
341
+ body_hash TEXT NOT NULL,
342
+ created_at TEXT NOT NULL,
343
+ model TEXT,
344
+ body_preview TEXT
345
+ )`,
346
+ `CREATE INDEX IF NOT EXISTS idx_revisions_lookup
347
+ ON revisions(content_type, en_slug, locale, created_at DESC)`,
348
+ `CREATE TABLE IF NOT EXISTS slug_aliases (
349
+ content_type TEXT NOT NULL,
350
+ canonical_en_slug TEXT NOT NULL,
351
+ alias_en_slug TEXT NOT NULL,
352
+ created_at TEXT NOT NULL,
353
+ PRIMARY KEY (content_type, alias_en_slug)
354
+ )`,
355
+ `CREATE INDEX IF NOT EXISTS idx_slug_aliases_canonical
356
+ ON slug_aliases(content_type, canonical_en_slug)`,
357
+ `CREATE TABLE IF NOT EXISTS alias_locale_slugs (
358
+ content_type TEXT NOT NULL,
359
+ alias_en_slug TEXT NOT NULL,
360
+ locale TEXT NOT NULL,
361
+ locale_slug TEXT NOT NULL,
362
+ captured_at TEXT NOT NULL,
363
+ PRIMARY KEY (content_type, alias_en_slug, locale)
364
+ )`
365
+ ];
366
+ function resolveStorePath(config) {
367
+ return config.storePath;
368
+ }
369
+ function openStore(config, mode = "readwrite") {
370
+ const storePath = resolveStorePath(config);
371
+ fs2__default.default.mkdirSync(path2__default.default.dirname(storePath), { recursive: true });
372
+ const db = new Database__default.default(storePath, { readonly: mode === "readonly" });
373
+ if (mode === "readwrite") {
374
+ migrate(db);
375
+ }
376
+ return db;
377
+ }
378
+ function migrate(db) {
379
+ for (const sql of MIGRATIONS) {
380
+ db.exec(sql);
381
+ }
382
+ db.prepare(
383
+ `INSERT INTO meta(key, value) VALUES('schema_version', ?)
384
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`
385
+ ).run(String(SCHEMA_VERSION));
386
+ }
387
+
388
+ // src/storage/translations.ts
389
+ function getTranslation(db, contentType, enSlug, locale) {
390
+ return db.prepare(
391
+ `SELECT * FROM translations WHERE content_type = ? AND en_slug = ? AND locale = ?`
392
+ ).get(contentType, enSlug, locale);
393
+ }
394
+ function listTranslationsForType(db, contentType) {
395
+ return db.prepare(`SELECT * FROM translations WHERE content_type = ? ORDER BY en_slug, locale`).all(contentType);
396
+ }
397
+ function listRevisions(db, contentType, enSlug, locale) {
398
+ return db.prepare(
399
+ `SELECT * FROM revisions
400
+ WHERE content_type = ? AND en_slug = ?
401
+ ORDER BY created_at DESC, id DESC`
402
+ ).all(contentType, enSlug);
403
+ }
404
+
405
+ // src/loader/normalize-en.ts
406
+ function normalizeEnFrontmatter(data) {
407
+ const out = { ...data };
408
+ if (!out.publishedAt && typeof out.date === "string") {
409
+ out.publishedAt = out.date;
410
+ }
411
+ if (!out.heroImage && typeof out.image === "string" && out.image.startsWith("/")) {
412
+ out.heroImage = out.image;
413
+ }
414
+ if (!out.updatedAt && typeof out.publishedAt === "string") {
415
+ out.updatedAt = out.publishedAt;
416
+ }
417
+ return out;
418
+ }
419
+ function isPublishableContentFile(name) {
420
+ if (!name.endsWith(".md") && !name.endsWith(".mdx")) return false;
421
+ const first = name.charAt(0);
422
+ return first === first.toLowerCase() && first !== "_";
423
+ }
424
+
425
+ // src/loader/create-loader.ts
426
+ function slugFromPath(filePath) {
427
+ return path2__default.default.basename(filePath).replace(/\.(md|mdx)$/, "");
428
+ }
429
+ function parseEnMdx(filePath, config, type) {
430
+ const raw = fs2__default.default.readFileSync(filePath, "utf8");
431
+ const parsed = matter__default.default(raw);
432
+ const slug = slugFromPath(filePath);
433
+ const normalized = normalizeEnFrontmatter(parsed.data);
434
+ const { builtin, rest, issues: builtinIssues } = extractBuiltinEnFields(
435
+ normalized,
436
+ type.path,
437
+ slug,
438
+ config.defaultLocale
439
+ );
440
+ const issues = builtinIssues.map((issue) => ({
441
+ field: issue.field,
442
+ message: issue.message,
443
+ level: issue.level
444
+ }));
445
+ const result = type.schema.safeParse(rest);
446
+ if (!result.success) {
447
+ return {
448
+ document: null,
449
+ issues: [
450
+ ...issues,
451
+ ...result.error.issues.map((issue) => ({
452
+ field: issue.path.join("."),
453
+ message: issue.message,
454
+ level: "error"
455
+ }))
456
+ ]
457
+ };
458
+ }
459
+ if (issues.some((i) => i.level === "error")) {
460
+ return { document: null, issues };
461
+ }
462
+ const frontmatter = mergeBuiltinsIntoFrontmatter(
463
+ result.data,
464
+ {
465
+ publishedAt: builtin.publishedAt,
466
+ updatedAt: builtin.updatedAt,
467
+ noindex: builtin.noindex,
468
+ canonicalPathOverride: builtin.canonicalPathOverride,
469
+ slug,
470
+ locale: config.defaultLocale
471
+ },
472
+ type,
473
+ config.defaultLocale
474
+ );
475
+ const crossIssues = type.crossValidate?.(result.data, {
476
+ locale: config.defaultLocale,
477
+ defaultLocale: config.defaultLocale,
478
+ slug,
479
+ enSlug: slug,
480
+ knownLocales: config.locales
481
+ }) ?? [];
482
+ issues.push(...crossIssues);
483
+ const document = {
484
+ slug,
485
+ enSlug: slug,
486
+ locale: config.defaultLocale,
487
+ aliases: builtin.aliases,
488
+ redirectTo: builtin.redirectTo,
489
+ publishedAt: builtin.publishedAt,
490
+ updatedAt: builtin.updatedAt,
491
+ noindex: builtin.noindex,
492
+ canonicalPathOverride: builtin.canonicalPathOverride,
493
+ frontmatter,
494
+ content: parsed.content,
495
+ filePath
496
+ };
497
+ return { document, issues };
498
+ }
499
+ function readEnDocument(config, type, enSlug) {
500
+ const contentDir = path2__default.default.join(
501
+ /* turbopackIgnore: true */
502
+ config.rootDir,
503
+ type.contentDir
504
+ );
505
+ for (const ext of [".mdx", ".md"]) {
506
+ const filePath = path2__default.default.join(contentDir, `${enSlug}${ext}`);
507
+ if (!fs2__default.default.existsSync(filePath)) continue;
508
+ const { document } = parseEnMdx(filePath, config, type);
509
+ return document;
510
+ }
511
+ return null;
512
+ }
513
+ function getTranslatablePayload(doc, type) {
514
+ return {
515
+ frontmatter: pickTranslatable(doc.frontmatter, type.schema),
516
+ body: doc.content
517
+ };
518
+ }
519
+ function listEnSlugs(rootDir, contentDir) {
520
+ const dir = path2__default.default.join(rootDir, contentDir);
521
+ if (!fs2__default.default.existsSync(dir)) return [];
522
+ return fs2__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
523
+ }
524
+ function buildWorklist(config, options = {}) {
525
+ const db = openStore(config, "readonly");
526
+ const items = [];
527
+ const locales = options.locales ?? config.locales.filter((locale) => locale !== config.defaultLocale);
528
+ for (const type of config.types) {
529
+ if (options.contentType && type.id !== options.contentType) continue;
530
+ const enSlugs = options.enSlug ? [options.enSlug] : listEnSlugs(config.rootDir, type.contentDir);
531
+ for (const enSlug of enSlugs) {
532
+ const enDoc = readEnDocument(config, type, enSlug);
533
+ if (!enDoc) continue;
534
+ const payload = getTranslatablePayload(enDoc, type);
535
+ const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
536
+ for (const locale of locales) {
537
+ if (locale === config.defaultLocale) continue;
538
+ const existing = getTranslation(db, type.id, enSlug, locale);
539
+ if (!existing) {
540
+ items.push({
541
+ contentType: type.id,
542
+ enSlug,
543
+ locale,
544
+ reason: "missing",
545
+ currentEnHash
546
+ });
547
+ continue;
548
+ }
549
+ if (existing.en_hash !== currentEnHash) {
550
+ items.push({
551
+ contentType: type.id,
552
+ enSlug,
553
+ locale,
554
+ reason: "stale",
555
+ currentEnHash,
556
+ storedEnHash: existing.en_hash
557
+ });
558
+ }
559
+ }
560
+ }
561
+ }
562
+ db.close();
563
+ return items;
564
+ }
565
+
566
+ // studio/server.ts
567
+ function renderLayout(title, body) {
568
+ return `<!doctype html>
569
+ <html lang="en">
570
+ <head>
571
+ <meta charset="utf-8" />
572
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
573
+ <title>${title} \u2014 Scribe Studio</title>
574
+ <style>
575
+ body { font-family: ui-sans-serif, system-ui, sans-serif; margin: 0; background: #0b0d10; color: #e8eaed; }
576
+ header { padding: 16px 24px; border-bottom: 1px solid #222; display: flex; gap: 16px; }
577
+ header a { color: #9ecbff; text-decoration: none; }
578
+ main { padding: 24px; }
579
+ table { border-collapse: collapse; width: 100%; }
580
+ th, td { border: 1px solid #333; padding: 8px 10px; text-align: left; vertical-align: top; }
581
+ th { background: #151922; }
582
+ .muted { color: #9aa0a6; }
583
+ .stale { color: #ffb4a2; }
584
+ .ok { color: #8ce99a; }
585
+ .card { background: #151922; border: 1px solid #333; border-radius: 8px; padding: 16px; margin-bottom: 16px; }
586
+ </style>
587
+ </head>
588
+ <body>
589
+ <header>
590
+ <strong>Scribe Studio</strong>
591
+ <a href="/">Browser</a>
592
+ <a href="/staleness">Staleness</a>
593
+ <a href="/history">History</a>
594
+ </header>
595
+ <main>${body}</main>
596
+ </body>
597
+ </html>`;
598
+ }
599
+ async function startStudio(project, options = {}) {
600
+ const app = new hono.Hono();
601
+ const config = project.config;
602
+ app.get("/", (c) => {
603
+ const cards = project.listTypes().map((type) => {
604
+ const localeRows = config.locales.map((locale) => {
605
+ const count = type.load().get(locale)?.bySlug.size ?? 0;
606
+ return `<li>${locale}: ${count}</li>`;
607
+ }).join("");
608
+ return `<div class="card"><h2>${type.config.label}</h2><ul>${localeRows}</ul></div>`;
609
+ });
610
+ return c.html(renderLayout("Browser", cards.join("")));
611
+ });
612
+ app.get("/staleness", (c) => {
613
+ const worklist = buildWorklist(config);
614
+ const rows = worklist.slice(0, 500).map(
615
+ (item) => `<tr><td>${item.contentType}</td><td>${item.enSlug}</td><td>${item.locale}</td><td class="stale">${item.reason}</td></tr>`
616
+ ).join("");
617
+ const html = `<h1>Translation staleness</h1><p class="muted">${worklist.length} stale/missing entries (showing up to 500)</p>
618
+ <table><thead><tr><th>Type</th><th>EN slug</th><th>Locale</th><th>Status</th></tr></thead><tbody>${rows}</tbody></table>`;
619
+ return c.html(renderLayout("Staleness", html));
620
+ });
621
+ app.get("/history", (c) => {
622
+ const typeId = c.req.query("type") ?? project.listTypes()[0]?.id;
623
+ const enSlug = c.req.query("slug");
624
+ if (!typeId) return c.html(renderLayout("History", "<p>No content types.</p>"));
625
+ const db = openStore(config, "readonly");
626
+ let body = `<h1>History</h1><form><label>Type <select name="type">${project.listTypes().map((t) => `<option value="${t.id}" ${t.id === typeId ? "selected" : ""}>${t.id}</option>`).join("")}</select></label> <label>EN slug <input name="slug" value="${enSlug ?? ""}" /></label> <button>View</button></form>`;
627
+ if (enSlug) {
628
+ const rows = listRevisions(db, typeId, enSlug).slice(0, 100).map(
629
+ (row) => `<tr><td>${row.created_at}</td><td>${row.revision_kind}</td><td>${row.locale ?? "en"}</td><td>${row.en_hash.slice(0, 8)}</td><td class="muted">${row.body_preview ?? ""}</td></tr>`
630
+ ).join("");
631
+ body += `<table><thead><tr><th>When</th><th>Kind</th><th>Locale</th><th>EN hash</th><th>Preview</th></tr></thead><tbody>${rows}</tbody></table>`;
632
+ } else {
633
+ const translations = listTranslationsForType(db, typeId).slice(0, 100);
634
+ body += `<ul>${translations.map((row) => `<li><a href="/history?type=${typeId}&slug=${row.en_slug}">${row.en_slug}</a> (${row.locale})</li>`).join("")}</ul>`;
635
+ }
636
+ db.close();
637
+ return c.html(renderLayout("History", body));
638
+ });
639
+ app.get("/api/staleness-matrix", (c) => {
640
+ const matrix = {};
641
+ for (const type of project.listTypes()) {
642
+ matrix[type.id] = {};
643
+ for (const locale of config.locales) {
644
+ if (locale === config.defaultLocale) continue;
645
+ const enSlugs = type.list().map((doc) => doc.slug);
646
+ let stale = 0;
647
+ for (const enSlug of enSlugs) {
648
+ const enDoc = readEnDocument(config, type.config, enSlug);
649
+ if (!enDoc) continue;
650
+ const payload = getTranslatablePayload(enDoc, type.config);
651
+ const hash = computePageEnHash(payload.frontmatter, payload.body);
652
+ const db = openStore(config, "readonly");
653
+ const row = db.prepare(
654
+ `SELECT en_hash FROM translations WHERE content_type = ? AND en_slug = ? AND locale = ?`
655
+ ).get(type.id, enSlug, locale);
656
+ db.close();
657
+ if (!row || row.en_hash !== hash) stale++;
658
+ }
659
+ matrix[type.id][locale] = stale;
660
+ }
661
+ }
662
+ return c.json(matrix);
663
+ });
664
+ const port = options.port ?? 3600;
665
+ const host = options.host ?? "127.0.0.1";
666
+ nodeServer.serve({ fetch: app.fetch, port, hostname: host }, () => {
667
+ console.log(`Scribe studio listening on http://${host}:${port}`);
668
+ });
669
+ }
670
+
671
+ exports.startStudio = startStudio;
672
+ //# sourceMappingURL=server.cjs.map
673
+ //# sourceMappingURL=server.cjs.map