scribe-cms 0.0.4 → 0.0.5

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.
@@ -13,6 +13,7 @@ var dotenv = require('dotenv');
13
13
  var nodeServer = require('@hono/node-server');
14
14
  var hono = require('hono');
15
15
  var prompts = require('@inquirer/prompts');
16
+ var url = require('url');
16
17
 
17
18
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
18
19
 
@@ -232,14 +233,14 @@ var SLUG_PLACEHOLDER = "{slug}";
232
233
  function isRoutableType(type) {
233
234
  return typeof type.path === "string" && type.path.length > 0;
234
235
  }
235
- function assertValidPathTemplate(path11, typeId) {
236
- if (!path11.startsWith("/")) {
237
- throw new Error(`Content type "${typeId}": path must start with / (got "${path11}")`);
236
+ function assertValidPathTemplate(path13, typeId) {
237
+ if (!path13.startsWith("/")) {
238
+ throw new Error(`Content type "${typeId}": path must start with / (got "${path13}")`);
238
239
  }
239
- const count = (path11.match(/\{slug\}/g) ?? []).length;
240
+ const count = (path13.match(/\{slug\}/g) ?? []).length;
240
241
  if (count !== 1) {
241
242
  throw new Error(
242
- `Content type "${typeId}": path must contain exactly one {slug} (got "${path11}")`
243
+ `Content type "${typeId}": path must contain exactly one {slug} (got "${path13}")`
243
244
  );
244
245
  }
245
246
  }
@@ -291,6 +292,7 @@ function resolveConfig(input, baseDir) {
291
292
  const projectRoot = path3__default.default.resolve(baseDir ?? process.cwd(), raw.rootDir);
292
293
  const contentRoot = path3__default.default.resolve(projectRoot, raw.contentDir ?? "content");
293
294
  const storePath = path3__default.default.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
295
+ const assetsPath = raw.assetsDir ? path3__default.default.resolve(projectRoot, raw.assetsDir) : void 0;
294
296
  const seenIds = /* @__PURE__ */ new Set();
295
297
  const types = raw.types.map((type) => {
296
298
  if (seenIds.has(type.id)) {
@@ -311,6 +313,7 @@ function resolveConfig(input, baseDir) {
311
313
  const config = {
312
314
  rootDir: contentRoot,
313
315
  storePath,
316
+ assetsPath,
314
317
  locales: [...raw.locales],
315
318
  defaultLocale,
316
319
  localePresets: raw.localePresets,
@@ -367,10 +370,10 @@ function introspectSchema(schema, prefix = []) {
367
370
  }
368
371
  function extractByPaths(data, paths) {
369
372
  const out = {};
370
- for (const path11 of paths) {
371
- const value = getAtPath(data, path11);
373
+ for (const path13 of paths) {
374
+ const value = getAtPath(data, path13);
372
375
  if (value !== void 0) {
373
- setAtPath(out, path11.filter((p) => p !== "*"), value);
376
+ setAtPath(out, path13.filter((p) => p !== "*"), value);
374
377
  }
375
378
  }
376
379
  return out;
@@ -388,13 +391,13 @@ function mergeStructuralOntoLocale(localeData, enData, schema) {
388
391
  const translatable = pickTranslatable(localeData, schema);
389
392
  return deepMerge(structural, translatable);
390
393
  }
391
- function getAtPath(obj, path11) {
394
+ function getAtPath(obj, path13) {
392
395
  let current = obj;
393
- for (const segment of path11) {
396
+ for (const segment of path13) {
394
397
  if (segment === "*") {
395
398
  if (!Array.isArray(current)) return void 0;
396
399
  return current.map(
397
- (item) => typeof item === "object" && item !== null ? getAtPath(item, path11.slice(path11.indexOf("*") + 1)) : void 0
400
+ (item) => typeof item === "object" && item !== null ? getAtPath(item, path13.slice(path13.indexOf("*") + 1)) : void 0
398
401
  );
399
402
  }
400
403
  if (typeof current !== "object" || current === null || Array.isArray(current)) return void 0;
@@ -402,13 +405,13 @@ function getAtPath(obj, path11) {
402
405
  }
403
406
  return current;
404
407
  }
405
- function setAtPath(obj, path11, value) {
406
- if (path11.length === 0) return;
407
- if (path11.length === 1) {
408
- obj[path11[0]] = value;
408
+ function setAtPath(obj, path13, value) {
409
+ if (path13.length === 0) return;
410
+ if (path13.length === 1) {
411
+ obj[path13[0]] = value;
409
412
  return;
410
413
  }
411
- const [head, ...rest] = path11;
414
+ const [head, ...rest] = path13;
412
415
  if (!(head in obj) || typeof obj[head] !== "object" || obj[head] === null) {
413
416
  obj[head] = {};
414
417
  }
@@ -1480,43 +1483,6 @@ function loadConfigSync(options = {}) {
1480
1483
 
1481
1484
  // src/validate/validate-project.ts
1482
1485
  init_cjs_shims();
1483
-
1484
- // src/hash/page-hash.ts
1485
- init_cjs_shims();
1486
- function sha256(input) {
1487
- return crypto.createHash("sha256").update(input, "utf8").digest("hex");
1488
- }
1489
- function computePageEnHash(translatableFrontmatter, body) {
1490
- const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
1491
- return sha256(payload);
1492
- }
1493
- function computeBodyHash(body) {
1494
- return sha256(body);
1495
- }
1496
-
1497
- // src/history/record-revision.ts
1498
- init_cjs_shims();
1499
- init_sqlite();
1500
- function recordRevision(config, input) {
1501
- const db = openStore(config, "readwrite");
1502
- const id = appendRevision(db, {
1503
- contentType: input.contentType,
1504
- enSlug: input.enSlug,
1505
- locale: input.locale,
1506
- revisionKind: input.revisionKind,
1507
- enHash: input.enHash,
1508
- bodyHash: computeBodyHash(input.body),
1509
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1510
- model: input.model,
1511
- bodyPreview: input.body.slice(0, 200),
1512
- frontmatterJson: input.frontmatter ? JSON.stringify(input.frontmatter) : null,
1513
- body: input.body
1514
- });
1515
- db.close();
1516
- return id;
1517
- }
1518
-
1519
- // src/validate/validate-project.ts
1520
1486
  init_sqlite();
1521
1487
 
1522
1488
  // src/validate/validate-relations.ts
@@ -1601,6 +1567,75 @@ function validateRelations(project) {
1601
1567
  return issues;
1602
1568
  }
1603
1569
 
1570
+ // src/validate/validate-assets.ts
1571
+ init_cjs_shims();
1572
+ var IMAGE_EXT = String.raw`(?:png|jpe?g|webp|gif|svg)`;
1573
+ var IMAGE_WEB_PATH = String.raw`\/[\w\-./]+\.${IMAGE_EXT}`;
1574
+ var MARKDOWN_IMAGE_RE = new RegExp(
1575
+ String.raw`!\[[^\]]*\]\((${IMAGE_WEB_PATH})\)`,
1576
+ "gi"
1577
+ );
1578
+ var HTML_SRC_RE = new RegExp(String.raw`src=["'](${IMAGE_WEB_PATH})["']`, "gi");
1579
+ function isImageWebPath(value) {
1580
+ return new RegExp(String.raw`^${IMAGE_WEB_PATH}$`, "i").test(value);
1581
+ }
1582
+ function isSiteAssetPath(webPath) {
1583
+ const segment = webPath.split("/")[1];
1584
+ return Boolean(segment && !segment.includes("."));
1585
+ }
1586
+ function addImagePath(webPath, out) {
1587
+ if (isSiteAssetPath(webPath)) out.add(webPath);
1588
+ }
1589
+ function collectBodyImagePaths(body, out) {
1590
+ for (const re of [MARKDOWN_IMAGE_RE, HTML_SRC_RE]) {
1591
+ for (const match of body.matchAll(re)) {
1592
+ addImagePath(match[1], out);
1593
+ }
1594
+ }
1595
+ }
1596
+ function collectFrontmatterImagePaths(value, out) {
1597
+ if (typeof value === "string") {
1598
+ if (isImageWebPath(value)) addImagePath(value, out);
1599
+ return;
1600
+ }
1601
+ if (Array.isArray(value)) {
1602
+ for (const item of value) collectFrontmatterImagePaths(item, out);
1603
+ return;
1604
+ }
1605
+ if (value && typeof value === "object") {
1606
+ for (const nested of Object.values(value)) {
1607
+ collectFrontmatterImagePaths(nested, out);
1608
+ }
1609
+ }
1610
+ }
1611
+ function collectImagePaths(frontmatter, body) {
1612
+ const paths = /* @__PURE__ */ new Set();
1613
+ collectFrontmatterImagePaths(frontmatter, paths);
1614
+ collectBodyImagePaths(body, paths);
1615
+ return [...paths].sort();
1616
+ }
1617
+ function assetFilePath(assetsPath, webPath) {
1618
+ return path3__default.default.join(assetsPath, webPath.replace(/^\//, ""));
1619
+ }
1620
+ function validateDocumentAssets(config, input) {
1621
+ const assetsPath = config.assetsPath;
1622
+ if (!assetsPath) return [];
1623
+ const issues = [];
1624
+ for (const webPath of collectImagePaths(input.frontmatter, input.body)) {
1625
+ const filePath = assetFilePath(assetsPath, webPath);
1626
+ if (fs2__default.default.existsSync(filePath)) continue;
1627
+ issues.push({
1628
+ level: "warning",
1629
+ contentType: input.contentType,
1630
+ enSlug: input.enSlug,
1631
+ locale: input.locale,
1632
+ field: "asset",
1633
+ message: `Missing image asset ${webPath} (expected ${filePath})`
1634
+ });
1635
+ }
1636
+ return issues;
1637
+ }
1638
+
1604
1639
  // src/validate/validate-slug-suffix.ts
1605
1640
  init_cjs_shims();
1606
1641
  function validateTranslationSlugSuffixes(config, db) {
@@ -1663,8 +1698,6 @@ function validateProject(config) {
1663
1698
  });
1664
1699
  continue;
1665
1700
  }
1666
- const payload = getTranslatablePayload(enDoc, type);
1667
- const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
1668
1701
  const crossIssues = type.crossValidate?.(enDoc.frontmatter, {
1669
1702
  locale: config.defaultLocale,
1670
1703
  defaultLocale: config.defaultLocale,
@@ -1682,29 +1715,18 @@ function validateProject(config) {
1682
1715
  message: issue.message
1683
1716
  });
1684
1717
  }
1718
+ for (const issue of validateDocumentAssets(config, {
1719
+ contentType: type.id,
1720
+ enSlug,
1721
+ frontmatter: enDoc.frontmatter,
1722
+ body: enDoc.content
1723
+ })) {
1724
+ issues.push(issue);
1725
+ }
1685
1726
  for (const locale of config.locales) {
1686
1727
  if (locale === config.defaultLocale) continue;
1687
1728
  const row = getTranslation(db, type.id, enSlug, locale);
1688
1729
  if (!row) continue;
1689
- if (row.en_hash !== currentEnHash) {
1690
- issues.push({
1691
- level: "warning",
1692
- contentType: type.id,
1693
- enSlug,
1694
- locale,
1695
- field: "en_hash",
1696
- message: "Translation is stale (en_hash mismatch)"
1697
- });
1698
- recordRevision(config, {
1699
- contentType: type.id,
1700
- enSlug,
1701
- locale,
1702
- revisionKind: "en_edit_detected",
1703
- enHash: currentEnHash,
1704
- body: row.body,
1705
- frontmatter: JSON.parse(row.frontmatter_json)
1706
- });
1707
- }
1708
1730
  const localeFm = JSON.parse(row.frontmatter_json);
1709
1731
  for (const issue of validateLocaleBuiltinFields(localeFm)) {
1710
1732
  issues.push({
@@ -1716,6 +1738,15 @@ function validateProject(config) {
1716
1738
  message: issue.message
1717
1739
  });
1718
1740
  }
1741
+ for (const issue of validateDocumentAssets(config, {
1742
+ contentType: type.id,
1743
+ enSlug,
1744
+ locale,
1745
+ frontmatter: localeFm,
1746
+ body: row.body
1747
+ })) {
1748
+ issues.push(issue);
1749
+ }
1719
1750
  }
1720
1751
  }
1721
1752
  try {
@@ -1786,6 +1817,21 @@ function validateProject(config) {
1786
1817
 
1787
1818
  // src/translate/worklist.ts
1788
1819
  init_cjs_shims();
1820
+
1821
+ // src/hash/page-hash.ts
1822
+ init_cjs_shims();
1823
+ function sha256(input) {
1824
+ return crypto.createHash("sha256").update(input, "utf8").digest("hex");
1825
+ }
1826
+ function computePageEnHash(translatableFrontmatter, body) {
1827
+ const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
1828
+ return sha256(payload);
1829
+ }
1830
+ function computeBodyHash(body) {
1831
+ return sha256(body);
1832
+ }
1833
+
1834
+ // src/translate/worklist.ts
1789
1835
  init_sqlite();
1790
1836
  function listEnSlugs3(rootDir, contentDir) {
1791
1837
  const dir = path3__default.default.join(rootDir, contentDir);
@@ -1847,6 +1893,30 @@ function resolveLocalesFromPreset(config, preset, explicitLocales) {
1847
1893
 
1848
1894
  // src/translate/page-translator.ts
1849
1895
  init_cjs_shims();
1896
+
1897
+ // src/history/record-revision.ts
1898
+ init_cjs_shims();
1899
+ init_sqlite();
1900
+ function recordRevision(config, input) {
1901
+ const db = openStore(config, "readwrite");
1902
+ const id = appendRevision(db, {
1903
+ contentType: input.contentType,
1904
+ enSlug: input.enSlug,
1905
+ locale: input.locale,
1906
+ revisionKind: input.revisionKind,
1907
+ enHash: input.enHash,
1908
+ bodyHash: computeBodyHash(input.body),
1909
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1910
+ model: input.model,
1911
+ bodyPreview: input.body.slice(0, 200),
1912
+ frontmatterJson: input.frontmatter ? JSON.stringify(input.frontmatter) : null,
1913
+ body: input.body
1914
+ });
1915
+ db.close();
1916
+ return id;
1917
+ }
1918
+
1919
+ // src/translate/page-translator.ts
1850
1920
  init_sqlite();
1851
1921
 
1852
1922
  // src/translate/gemini-client.ts
@@ -3178,6 +3248,24 @@ function createTranslateProgressReporter(options = {}) {
3178
3248
  };
3179
3249
  }
3180
3250
 
3251
+ // src/version.ts
3252
+ init_cjs_shims();
3253
+ function readScribeVersion() {
3254
+ let dir = path3__default.default.dirname(url.fileURLToPath(importMetaUrl));
3255
+ for (; ; ) {
3256
+ const candidate = path3__default.default.join(dir, "package.json");
3257
+ if (fs2__default.default.existsSync(candidate)) {
3258
+ const pkg = JSON.parse(fs2__default.default.readFileSync(candidate, "utf8"));
3259
+ if (pkg.name === "scribe-cms" && pkg.version) return pkg.version;
3260
+ }
3261
+ const parent = path3__default.default.dirname(dir);
3262
+ if (parent === dir) break;
3263
+ dir = parent;
3264
+ }
3265
+ return "unknown";
3266
+ }
3267
+ var SCRIBE_VERSION = readScribeVersion();
3268
+
3181
3269
  // cli/index.ts
3182
3270
  function parseArgs(argv) {
3183
3271
  const options = { cwd: process.cwd() };
@@ -3280,7 +3368,13 @@ function loadProject(options) {
3280
3368
  return createProject(config);
3281
3369
  }
3282
3370
  async function main() {
3283
- const { command, options, rest } = parseArgs(process.argv.slice(2));
3371
+ const argv = process.argv.slice(2);
3372
+ const first = argv[0];
3373
+ if (first === "--version" || first === "-V" || first === "version") {
3374
+ console.log(SCRIBE_VERSION);
3375
+ return;
3376
+ }
3377
+ const { command, options, rest } = parseArgs(argv);
3284
3378
  loadEnvFromCwd(options.cwd);
3285
3379
  if (command === "help" || command === "--help") {
3286
3380
  console.log(`Usage: scribe <command>
@@ -3292,6 +3386,7 @@ Commands:
3292
3386
  translate Translate stale/missing locale pages
3293
3387
  history <type> <slug> Show revision timeline
3294
3388
  studio Start read-only local studio
3389
+ version Print scribe-cms version
3295
3390
 
3296
3391
  Export-static flags:
3297
3392
  --out <dir> Output directory (default: public)
@@ -3313,6 +3408,10 @@ Translate flags:
3313
3408
  `);
3314
3409
  return;
3315
3410
  }
3411
+ if (command === "version") {
3412
+ console.log(SCRIBE_VERSION);
3413
+ return;
3414
+ }
3316
3415
  const project = loadProject(options);
3317
3416
  const config = project.config;
3318
3417
  switch (command) {