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.
package/dist/cli/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import path4 from 'path';
3
- import 'url';
3
+ import { fileURLToPath } from 'url';
4
4
  import fs2 from 'fs';
5
5
  import Database from 'better-sqlite3';
6
6
  import { z } from 'zod';
@@ -218,14 +218,14 @@ var SLUG_PLACEHOLDER = "{slug}";
218
218
  function isRoutableType(type) {
219
219
  return typeof type.path === "string" && type.path.length > 0;
220
220
  }
221
- function assertValidPathTemplate(path12, typeId) {
222
- if (!path12.startsWith("/")) {
223
- throw new Error(`Content type "${typeId}": path must start with / (got "${path12}")`);
221
+ function assertValidPathTemplate(path14, typeId) {
222
+ if (!path14.startsWith("/")) {
223
+ throw new Error(`Content type "${typeId}": path must start with / (got "${path14}")`);
224
224
  }
225
- const count = (path12.match(/\{slug\}/g) ?? []).length;
225
+ const count = (path14.match(/\{slug\}/g) ?? []).length;
226
226
  if (count !== 1) {
227
227
  throw new Error(
228
- `Content type "${typeId}": path must contain exactly one {slug} (got "${path12}")`
228
+ `Content type "${typeId}": path must contain exactly one {slug} (got "${path14}")`
229
229
  );
230
230
  }
231
231
  }
@@ -277,6 +277,7 @@ function resolveConfig(input, baseDir) {
277
277
  const projectRoot = path4.resolve(baseDir ?? process.cwd(), raw.rootDir);
278
278
  const contentRoot = path4.resolve(projectRoot, raw.contentDir ?? "content");
279
279
  const storePath = path4.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
280
+ const assetsPath = raw.assetsDir ? path4.resolve(projectRoot, raw.assetsDir) : void 0;
280
281
  const seenIds = /* @__PURE__ */ new Set();
281
282
  const types = raw.types.map((type) => {
282
283
  if (seenIds.has(type.id)) {
@@ -297,6 +298,7 @@ function resolveConfig(input, baseDir) {
297
298
  const config = {
298
299
  rootDir: contentRoot,
299
300
  storePath,
301
+ assetsPath,
300
302
  locales: [...raw.locales],
301
303
  defaultLocale,
302
304
  localePresets: raw.localePresets,
@@ -353,10 +355,10 @@ function introspectSchema(schema, prefix = []) {
353
355
  }
354
356
  function extractByPaths(data, paths) {
355
357
  const out = {};
356
- for (const path12 of paths) {
357
- const value = getAtPath(data, path12);
358
+ for (const path14 of paths) {
359
+ const value = getAtPath(data, path14);
358
360
  if (value !== void 0) {
359
- setAtPath(out, path12.filter((p) => p !== "*"), value);
361
+ setAtPath(out, path14.filter((p) => p !== "*"), value);
360
362
  }
361
363
  }
362
364
  return out;
@@ -374,13 +376,13 @@ function mergeStructuralOntoLocale(localeData, enData, schema) {
374
376
  const translatable = pickTranslatable(localeData, schema);
375
377
  return deepMerge(structural, translatable);
376
378
  }
377
- function getAtPath(obj, path12) {
379
+ function getAtPath(obj, path14) {
378
380
  let current = obj;
379
- for (const segment of path12) {
381
+ for (const segment of path14) {
380
382
  if (segment === "*") {
381
383
  if (!Array.isArray(current)) return void 0;
382
384
  return current.map(
383
- (item) => typeof item === "object" && item !== null ? getAtPath(item, path12.slice(path12.indexOf("*") + 1)) : void 0
385
+ (item) => typeof item === "object" && item !== null ? getAtPath(item, path14.slice(path14.indexOf("*") + 1)) : void 0
384
386
  );
385
387
  }
386
388
  if (typeof current !== "object" || current === null || Array.isArray(current)) return void 0;
@@ -388,13 +390,13 @@ function getAtPath(obj, path12) {
388
390
  }
389
391
  return current;
390
392
  }
391
- function setAtPath(obj, path12, value) {
392
- if (path12.length === 0) return;
393
- if (path12.length === 1) {
394
- obj[path12[0]] = value;
393
+ function setAtPath(obj, path14, value) {
394
+ if (path14.length === 0) return;
395
+ if (path14.length === 1) {
396
+ obj[path14[0]] = value;
395
397
  return;
396
398
  }
397
- const [head, ...rest] = path12;
399
+ const [head, ...rest] = path14;
398
400
  if (!(head in obj) || typeof obj[head] !== "object" || obj[head] === null) {
399
401
  obj[head] = {};
400
402
  }
@@ -1466,43 +1468,6 @@ function loadConfigSync(options = {}) {
1466
1468
 
1467
1469
  // src/validate/validate-project.ts
1468
1470
  init_esm_shims();
1469
-
1470
- // src/hash/page-hash.ts
1471
- init_esm_shims();
1472
- function sha256(input) {
1473
- return createHash("sha256").update(input, "utf8").digest("hex");
1474
- }
1475
- function computePageEnHash(translatableFrontmatter, body) {
1476
- const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
1477
- return sha256(payload);
1478
- }
1479
- function computeBodyHash(body) {
1480
- return sha256(body);
1481
- }
1482
-
1483
- // src/history/record-revision.ts
1484
- init_esm_shims();
1485
- init_sqlite();
1486
- function recordRevision(config, input) {
1487
- const db = openStore(config, "readwrite");
1488
- const id = appendRevision(db, {
1489
- contentType: input.contentType,
1490
- enSlug: input.enSlug,
1491
- locale: input.locale,
1492
- revisionKind: input.revisionKind,
1493
- enHash: input.enHash,
1494
- bodyHash: computeBodyHash(input.body),
1495
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1496
- model: input.model,
1497
- bodyPreview: input.body.slice(0, 200),
1498
- frontmatterJson: input.frontmatter ? JSON.stringify(input.frontmatter) : null,
1499
- body: input.body
1500
- });
1501
- db.close();
1502
- return id;
1503
- }
1504
-
1505
- // src/validate/validate-project.ts
1506
1471
  init_sqlite();
1507
1472
 
1508
1473
  // src/validate/validate-relations.ts
@@ -1587,6 +1552,75 @@ function validateRelations(project) {
1587
1552
  return issues;
1588
1553
  }
1589
1554
 
1555
+ // src/validate/validate-assets.ts
1556
+ init_esm_shims();
1557
+ var IMAGE_EXT = String.raw`(?:png|jpe?g|webp|gif|svg)`;
1558
+ var IMAGE_WEB_PATH = String.raw`\/[\w\-./]+\.${IMAGE_EXT}`;
1559
+ var MARKDOWN_IMAGE_RE = new RegExp(
1560
+ String.raw`!\[[^\]]*\]\((${IMAGE_WEB_PATH})\)`,
1561
+ "gi"
1562
+ );
1563
+ var HTML_SRC_RE = new RegExp(String.raw`src=["'](${IMAGE_WEB_PATH})["']`, "gi");
1564
+ function isImageWebPath(value) {
1565
+ return new RegExp(String.raw`^${IMAGE_WEB_PATH}$`, "i").test(value);
1566
+ }
1567
+ function isSiteAssetPath(webPath) {
1568
+ const segment = webPath.split("/")[1];
1569
+ return Boolean(segment && !segment.includes("."));
1570
+ }
1571
+ function addImagePath(webPath, out) {
1572
+ if (isSiteAssetPath(webPath)) out.add(webPath);
1573
+ }
1574
+ function collectBodyImagePaths(body, out) {
1575
+ for (const re of [MARKDOWN_IMAGE_RE, HTML_SRC_RE]) {
1576
+ for (const match of body.matchAll(re)) {
1577
+ addImagePath(match[1], out);
1578
+ }
1579
+ }
1580
+ }
1581
+ function collectFrontmatterImagePaths(value, out) {
1582
+ if (typeof value === "string") {
1583
+ if (isImageWebPath(value)) addImagePath(value, out);
1584
+ return;
1585
+ }
1586
+ if (Array.isArray(value)) {
1587
+ for (const item of value) collectFrontmatterImagePaths(item, out);
1588
+ return;
1589
+ }
1590
+ if (value && typeof value === "object") {
1591
+ for (const nested of Object.values(value)) {
1592
+ collectFrontmatterImagePaths(nested, out);
1593
+ }
1594
+ }
1595
+ }
1596
+ function collectImagePaths(frontmatter, body) {
1597
+ const paths = /* @__PURE__ */ new Set();
1598
+ collectFrontmatterImagePaths(frontmatter, paths);
1599
+ collectBodyImagePaths(body, paths);
1600
+ return [...paths].sort();
1601
+ }
1602
+ function assetFilePath(assetsPath, webPath) {
1603
+ return path4.join(assetsPath, webPath.replace(/^\//, ""));
1604
+ }
1605
+ function validateDocumentAssets(config, input) {
1606
+ const assetsPath = config.assetsPath;
1607
+ if (!assetsPath) return [];
1608
+ const issues = [];
1609
+ for (const webPath of collectImagePaths(input.frontmatter, input.body)) {
1610
+ const filePath = assetFilePath(assetsPath, webPath);
1611
+ if (fs2.existsSync(filePath)) continue;
1612
+ issues.push({
1613
+ level: "warning",
1614
+ contentType: input.contentType,
1615
+ enSlug: input.enSlug,
1616
+ locale: input.locale,
1617
+ field: "asset",
1618
+ message: `Missing image asset ${webPath} (expected ${filePath})`
1619
+ });
1620
+ }
1621
+ return issues;
1622
+ }
1623
+
1590
1624
  // src/validate/validate-slug-suffix.ts
1591
1625
  init_esm_shims();
1592
1626
  function validateTranslationSlugSuffixes(config, db) {
@@ -1649,8 +1683,6 @@ function validateProject(config) {
1649
1683
  });
1650
1684
  continue;
1651
1685
  }
1652
- const payload = getTranslatablePayload(enDoc, type);
1653
- const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
1654
1686
  const crossIssues = type.crossValidate?.(enDoc.frontmatter, {
1655
1687
  locale: config.defaultLocale,
1656
1688
  defaultLocale: config.defaultLocale,
@@ -1668,29 +1700,18 @@ function validateProject(config) {
1668
1700
  message: issue.message
1669
1701
  });
1670
1702
  }
1703
+ for (const issue of validateDocumentAssets(config, {
1704
+ contentType: type.id,
1705
+ enSlug,
1706
+ frontmatter: enDoc.frontmatter,
1707
+ body: enDoc.content
1708
+ })) {
1709
+ issues.push(issue);
1710
+ }
1671
1711
  for (const locale of config.locales) {
1672
1712
  if (locale === config.defaultLocale) continue;
1673
1713
  const row = getTranslation(db, type.id, enSlug, locale);
1674
1714
  if (!row) continue;
1675
- if (row.en_hash !== currentEnHash) {
1676
- issues.push({
1677
- level: "warning",
1678
- contentType: type.id,
1679
- enSlug,
1680
- locale,
1681
- field: "en_hash",
1682
- message: "Translation is stale (en_hash mismatch)"
1683
- });
1684
- recordRevision(config, {
1685
- contentType: type.id,
1686
- enSlug,
1687
- locale,
1688
- revisionKind: "en_edit_detected",
1689
- enHash: currentEnHash,
1690
- body: row.body,
1691
- frontmatter: JSON.parse(row.frontmatter_json)
1692
- });
1693
- }
1694
1715
  const localeFm = JSON.parse(row.frontmatter_json);
1695
1716
  for (const issue of validateLocaleBuiltinFields(localeFm)) {
1696
1717
  issues.push({
@@ -1702,6 +1723,15 @@ function validateProject(config) {
1702
1723
  message: issue.message
1703
1724
  });
1704
1725
  }
1726
+ for (const issue of validateDocumentAssets(config, {
1727
+ contentType: type.id,
1728
+ enSlug,
1729
+ locale,
1730
+ frontmatter: localeFm,
1731
+ body: row.body
1732
+ })) {
1733
+ issues.push(issue);
1734
+ }
1705
1735
  }
1706
1736
  }
1707
1737
  try {
@@ -1772,6 +1802,21 @@ function validateProject(config) {
1772
1802
 
1773
1803
  // src/translate/worklist.ts
1774
1804
  init_esm_shims();
1805
+
1806
+ // src/hash/page-hash.ts
1807
+ init_esm_shims();
1808
+ function sha256(input) {
1809
+ return createHash("sha256").update(input, "utf8").digest("hex");
1810
+ }
1811
+ function computePageEnHash(translatableFrontmatter, body) {
1812
+ const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
1813
+ return sha256(payload);
1814
+ }
1815
+ function computeBodyHash(body) {
1816
+ return sha256(body);
1817
+ }
1818
+
1819
+ // src/translate/worklist.ts
1775
1820
  init_sqlite();
1776
1821
  function listEnSlugs3(rootDir, contentDir) {
1777
1822
  const dir = path4.join(rootDir, contentDir);
@@ -1833,6 +1878,30 @@ function resolveLocalesFromPreset(config, preset, explicitLocales) {
1833
1878
 
1834
1879
  // src/translate/page-translator.ts
1835
1880
  init_esm_shims();
1881
+
1882
+ // src/history/record-revision.ts
1883
+ init_esm_shims();
1884
+ init_sqlite();
1885
+ function recordRevision(config, input) {
1886
+ const db = openStore(config, "readwrite");
1887
+ const id = appendRevision(db, {
1888
+ contentType: input.contentType,
1889
+ enSlug: input.enSlug,
1890
+ locale: input.locale,
1891
+ revisionKind: input.revisionKind,
1892
+ enHash: input.enHash,
1893
+ bodyHash: computeBodyHash(input.body),
1894
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1895
+ model: input.model,
1896
+ bodyPreview: input.body.slice(0, 200),
1897
+ frontmatterJson: input.frontmatter ? JSON.stringify(input.frontmatter) : null,
1898
+ body: input.body
1899
+ });
1900
+ db.close();
1901
+ return id;
1902
+ }
1903
+
1904
+ // src/translate/page-translator.ts
1836
1905
  init_sqlite();
1837
1906
 
1838
1907
  // src/translate/gemini-client.ts
@@ -3164,6 +3233,24 @@ function createTranslateProgressReporter(options = {}) {
3164
3233
  };
3165
3234
  }
3166
3235
 
3236
+ // src/version.ts
3237
+ init_esm_shims();
3238
+ function readScribeVersion() {
3239
+ let dir = path4.dirname(fileURLToPath(import.meta.url));
3240
+ for (; ; ) {
3241
+ const candidate = path4.join(dir, "package.json");
3242
+ if (fs2.existsSync(candidate)) {
3243
+ const pkg = JSON.parse(fs2.readFileSync(candidate, "utf8"));
3244
+ if (pkg.name === "scribe-cms" && pkg.version) return pkg.version;
3245
+ }
3246
+ const parent = path4.dirname(dir);
3247
+ if (parent === dir) break;
3248
+ dir = parent;
3249
+ }
3250
+ return "unknown";
3251
+ }
3252
+ var SCRIBE_VERSION = readScribeVersion();
3253
+
3167
3254
  // cli/index.ts
3168
3255
  function parseArgs(argv) {
3169
3256
  const options = { cwd: process.cwd() };
@@ -3266,7 +3353,13 @@ function loadProject(options) {
3266
3353
  return createProject(config);
3267
3354
  }
3268
3355
  async function main() {
3269
- const { command, options, rest } = parseArgs(process.argv.slice(2));
3356
+ const argv = process.argv.slice(2);
3357
+ const first = argv[0];
3358
+ if (first === "--version" || first === "-V" || first === "version") {
3359
+ console.log(SCRIBE_VERSION);
3360
+ return;
3361
+ }
3362
+ const { command, options, rest } = parseArgs(argv);
3270
3363
  loadEnvFromCwd(options.cwd);
3271
3364
  if (command === "help" || command === "--help") {
3272
3365
  console.log(`Usage: scribe <command>
@@ -3278,6 +3371,7 @@ Commands:
3278
3371
  translate Translate stale/missing locale pages
3279
3372
  history <type> <slug> Show revision timeline
3280
3373
  studio Start read-only local studio
3374
+ version Print scribe-cms version
3281
3375
 
3282
3376
  Export-static flags:
3283
3377
  --out <dir> Output directory (default: public)
@@ -3299,6 +3393,10 @@ Translate flags:
3299
3393
  `);
3300
3394
  return;
3301
3395
  }
3396
+ if (command === "version") {
3397
+ console.log(SCRIBE_VERSION);
3398
+ return;
3399
+ }
3302
3400
  const project = loadProject(options);
3303
3401
  const config = project.config;
3304
3402
  switch (command) {