scribe-cms 0.0.5 → 0.0.7

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
@@ -11,11 +11,11 @@ import { GoogleGenAI } from '@google/genai';
11
11
  import dotenv from 'dotenv';
12
12
  import { serve } from '@hono/node-server';
13
13
  import { Hono } from 'hono';
14
+ import { CancelPromptError } from '@inquirer/core';
14
15
  import { select, checkbox } from '@inquirer/prompts';
15
16
 
16
17
  var __defProp = Object.defineProperty;
17
18
  var __getOwnPropNames = Object.getOwnPropertyNames;
18
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
19
19
  var __esm = (fn, res) => function __init() {
20
20
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
21
21
  };
@@ -23,7 +23,6 @@ var __export = (target, all) => {
23
23
  for (var name in all)
24
24
  __defProp(target, name, { get: all[name], enumerable: true });
25
25
  };
26
- var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
27
26
  var init_esm_shims = __esm({
28
27
  "../../node_modules/tsup/assets/esm_shims.js"() {
29
28
  }
@@ -56,12 +55,19 @@ function addColumnIfMissing(db, table, column, ddlType) {
56
55
  db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddlType}`);
57
56
  }
58
57
  }
58
+ function readSchemaVersion(db) {
59
+ const row = db.prepare(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
60
+ return row ? Number.parseInt(row.value, 10) || 0 : 0;
61
+ }
59
62
  function migrate(db) {
63
+ const previousVersion = readSchemaVersion(db);
60
64
  for (const sql of MIGRATIONS) {
61
65
  db.exec(sql);
62
66
  }
63
- addColumnIfMissing(db, "revisions", "frontmatter_json", "TEXT");
64
- addColumnIfMissing(db, "revisions", "body", "TEXT");
67
+ if (previousVersion < 4) {
68
+ db.exec(`DROP TABLE IF EXISTS revisions`);
69
+ }
70
+ addColumnIfMissing(db, "translations", "snapshot_id", "INTEGER");
65
71
  db.prepare(
66
72
  `INSERT INTO meta(key, value) VALUES('schema_version', ?)
67
73
  ON CONFLICT(key) DO UPDATE SET value = excluded.value`
@@ -74,7 +80,7 @@ var SCHEMA_VERSION, MIGRATIONS;
74
80
  var init_sqlite = __esm({
75
81
  "src/storage/sqlite.ts"() {
76
82
  init_esm_shims();
77
- SCHEMA_VERSION = 3;
83
+ SCHEMA_VERSION = 4;
78
84
  MIGRATIONS = [
79
85
  `CREATE TABLE IF NOT EXISTS meta (
80
86
  key TEXT PRIMARY KEY,
@@ -94,20 +100,6 @@ var init_sqlite = __esm({
94
100
  )`,
95
101
  `CREATE INDEX IF NOT EXISTS idx_translations_type_locale
96
102
  ON translations(content_type, locale)`,
97
- `CREATE TABLE IF NOT EXISTS revisions (
98
- id INTEGER PRIMARY KEY AUTOINCREMENT,
99
- content_type TEXT NOT NULL,
100
- en_slug TEXT NOT NULL,
101
- locale TEXT,
102
- revision_kind TEXT NOT NULL,
103
- en_hash TEXT NOT NULL,
104
- body_hash TEXT NOT NULL,
105
- created_at TEXT NOT NULL,
106
- model TEXT,
107
- body_preview TEXT
108
- )`,
109
- `CREATE INDEX IF NOT EXISTS idx_revisions_lookup
110
- ON revisions(content_type, en_slug, locale, created_at DESC)`,
111
103
  `CREATE TABLE IF NOT EXISTS slug_aliases (
112
104
  content_type TEXT NOT NULL,
113
105
  canonical_en_slug TEXT NOT NULL,
@@ -124,7 +116,19 @@ var init_sqlite = __esm({
124
116
  locale_slug TEXT NOT NULL,
125
117
  captured_at TEXT NOT NULL,
126
118
  PRIMARY KEY (content_type, alias_en_slug, locale)
127
- )`
119
+ )`,
120
+ `CREATE TABLE IF NOT EXISTS en_snapshots (
121
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
122
+ content_type TEXT NOT NULL,
123
+ en_slug TEXT NOT NULL,
124
+ en_hash TEXT NOT NULL,
125
+ frontmatter_json TEXT NOT NULL,
126
+ body TEXT NOT NULL,
127
+ created_at TEXT NOT NULL,
128
+ UNIQUE (content_type, en_slug, en_hash)
129
+ )`,
130
+ `CREATE INDEX IF NOT EXISTS idx_en_snapshots_lookup
131
+ ON en_snapshots(content_type, en_slug, created_at DESC)`
128
132
  ];
129
133
  }
130
134
  });
@@ -598,18 +602,51 @@ init_sqlite();
598
602
 
599
603
  // src/storage/translations.ts
600
604
  init_esm_shims();
605
+ function getOrCreateEnSnapshot(db, input) {
606
+ db.prepare(
607
+ `INSERT OR IGNORE INTO en_snapshots (
608
+ content_type, en_slug, en_hash, frontmatter_json, body, created_at
609
+ ) VALUES (?, ?, ?, ?, ?, ?)`
610
+ ).run(
611
+ input.contentType,
612
+ input.enSlug,
613
+ input.enHash,
614
+ JSON.stringify(input.frontmatter),
615
+ input.body,
616
+ input.createdAt
617
+ );
618
+ const row = db.prepare(
619
+ `SELECT id FROM en_snapshots
620
+ WHERE content_type = ? AND en_slug = ? AND en_hash = ?`
621
+ ).get(input.contentType, input.enSlug, input.enHash);
622
+ return row.id;
623
+ }
624
+ function getEnSnapshot(db, snapshotId) {
625
+ return db.prepare(`SELECT * FROM en_snapshots WHERE id = ?`).get(snapshotId);
626
+ }
627
+ function listEnSnapshotsForEnSlug(db, contentType, enSlug) {
628
+ return db.prepare(
629
+ `SELECT s.*, GROUP_CONCAT(t.locale) AS locales
630
+ FROM en_snapshots s
631
+ LEFT JOIN translations t ON t.snapshot_id = s.id
632
+ WHERE s.content_type = ? AND s.en_slug = ?
633
+ GROUP BY s.id
634
+ ORDER BY s.created_at DESC, s.id DESC`
635
+ ).all(contentType, enSlug);
636
+ }
601
637
  function upsertTranslation(db, input) {
602
638
  db.prepare(
603
639
  `INSERT INTO translations (
604
- content_type, en_slug, locale, slug, frontmatter_json, body, en_hash, translated_at, model
605
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
640
+ content_type, en_slug, locale, slug, frontmatter_json, body, en_hash, translated_at, model, snapshot_id
641
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
606
642
  ON CONFLICT(content_type, en_slug, locale) DO UPDATE SET
607
643
  slug = excluded.slug,
608
644
  frontmatter_json = excluded.frontmatter_json,
609
645
  body = excluded.body,
610
646
  en_hash = excluded.en_hash,
611
647
  translated_at = excluded.translated_at,
612
- model = excluded.model`
648
+ model = excluded.model,
649
+ snapshot_id = excluded.snapshot_id`
613
650
  ).run(
614
651
  input.contentType,
615
652
  input.enSlug,
@@ -619,7 +656,8 @@ function upsertTranslation(db, input) {
619
656
  input.body,
620
657
  input.enHash,
621
658
  input.translatedAt,
622
- input.model
659
+ input.model,
660
+ input.snapshotId
623
661
  );
624
662
  }
625
663
  function getTranslation(db, contentType, enSlug, locale) {
@@ -641,41 +679,6 @@ function listTranslationsForLocale(db, contentType, locale) {
641
679
  function bulkLoadTranslations(db) {
642
680
  return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
643
681
  }
644
- function appendRevision(db, input) {
645
- const result = db.prepare(
646
- `INSERT INTO revisions (
647
- content_type, en_slug, locale, revision_kind, en_hash, body_hash, created_at,
648
- model, body_preview, frontmatter_json, body
649
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
650
- ).run(
651
- input.contentType,
652
- input.enSlug,
653
- input.locale,
654
- input.revisionKind,
655
- input.enHash,
656
- input.bodyHash,
657
- input.createdAt,
658
- input.model ?? null,
659
- input.bodyPreview ?? null,
660
- input.frontmatterJson ?? null,
661
- input.body ?? null
662
- );
663
- return Number(result.lastInsertRowid);
664
- }
665
- function listRevisions(db, contentType, enSlug, locale) {
666
- if (locale) {
667
- return db.prepare(
668
- `SELECT * FROM revisions
669
- WHERE content_type = ? AND en_slug = ? AND locale = ?
670
- ORDER BY created_at DESC, id DESC`
671
- ).all(contentType, enSlug, locale);
672
- }
673
- return db.prepare(
674
- `SELECT * FROM revisions
675
- WHERE content_type = ? AND en_slug = ?
676
- ORDER BY created_at DESC, id DESC`
677
- ).all(contentType, enSlug);
678
- }
679
682
 
680
683
  // src/loader/normalize-en.ts
681
684
  init_esm_shims();
@@ -1623,22 +1626,45 @@ function validateDocumentAssets(config, input) {
1623
1626
 
1624
1627
  // src/validate/validate-slug-suffix.ts
1625
1628
  init_esm_shims();
1629
+
1630
+ // src/core/localized-slug.ts
1631
+ init_esm_shims();
1632
+ function findLocaleSuffixInSlug(slug, localeCodes) {
1633
+ const sorted = [...localeCodes].sort((a, b) => b.length - a.length);
1634
+ for (const code of sorted) {
1635
+ if (slug.endsWith(`-${code}`)) {
1636
+ return code;
1637
+ }
1638
+ }
1639
+ return void 0;
1640
+ }
1641
+ function stripLocaleSuffixFromSlug(slug, localeCodes) {
1642
+ const matchedCode = findLocaleSuffixInSlug(slug, localeCodes);
1643
+ if (!matchedCode) {
1644
+ return { slug, stripped: false };
1645
+ }
1646
+ return {
1647
+ slug: slug.slice(0, -(matchedCode.length + 1)),
1648
+ stripped: true,
1649
+ matchedCode
1650
+ };
1651
+ }
1652
+
1653
+ // src/validate/validate-slug-suffix.ts
1626
1654
  function validateTranslationSlugSuffixes(config, db) {
1627
1655
  const issues = [];
1628
1656
  const localeCodes = config.locales.filter((l) => l !== config.defaultLocale);
1629
1657
  for (const row of bulkLoadTranslations(db)) {
1630
1658
  if (row.locale === config.defaultLocale) continue;
1631
- for (const code of localeCodes) {
1632
- if (row.slug.endsWith(`-${code}`)) {
1633
- issues.push({
1634
- contentTypeId: row.content_type,
1635
- enSlug: row.en_slug,
1636
- locale: row.locale,
1637
- slug: row.slug,
1638
- message: `Translation slug "${row.slug}" ends with locale code "-${code}"`
1639
- });
1640
- break;
1641
- }
1659
+ const matchedCode = findLocaleSuffixInSlug(row.slug, localeCodes);
1660
+ if (matchedCode) {
1661
+ issues.push({
1662
+ contentTypeId: row.content_type,
1663
+ enSlug: row.en_slug,
1664
+ locale: row.locale,
1665
+ slug: row.slug,
1666
+ message: `Translation slug "${row.slug}" ends with locale code "-${matchedCode}"`
1667
+ });
1642
1668
  }
1643
1669
  }
1644
1670
  return issues;
@@ -1786,7 +1812,7 @@ function validateProject(config) {
1786
1812
  try {
1787
1813
  for (const issue of validateTranslationSlugSuffixes(config, dbForSuffix)) {
1788
1814
  issues.push({
1789
- level: "error",
1815
+ level: "warning",
1790
1816
  contentType: issue.contentTypeId,
1791
1817
  enSlug: issue.enSlug,
1792
1818
  locale: issue.locale,
@@ -1812,9 +1838,6 @@ function computePageEnHash(translatableFrontmatter, body) {
1812
1838
  const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
1813
1839
  return sha256(payload);
1814
1840
  }
1815
- function computeBodyHash(body) {
1816
- return sha256(body);
1817
- }
1818
1841
 
1819
1842
  // src/translate/worklist.ts
1820
1843
  init_sqlite();
@@ -1879,25 +1902,20 @@ function resolveLocalesFromPreset(config, preset, explicitLocales) {
1879
1902
  // src/translate/page-translator.ts
1880
1903
  init_esm_shims();
1881
1904
 
1882
- // src/history/record-revision.ts
1905
+ // src/history/record-snapshot.ts
1883
1906
  init_esm_shims();
1884
1907
  init_sqlite();
1885
- function recordRevision(config, input) {
1886
- const db = openStore(config, "readwrite");
1887
- const id = appendRevision(db, {
1908
+ function recordEnSnapshot(config, input, db) {
1909
+ const ownDb = db ?? openStore(config, "readwrite");
1910
+ const id = getOrCreateEnSnapshot(ownDb, {
1888
1911
  contentType: input.contentType,
1889
1912
  enSlug: input.enSlug,
1890
- locale: input.locale,
1891
- revisionKind: input.revisionKind,
1892
1913
  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
1914
+ frontmatter: input.frontmatter,
1915
+ body: input.body,
1916
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1899
1917
  });
1900
- db.close();
1918
+ if (!db) ownDb.close();
1901
1919
  return id;
1902
1920
  }
1903
1921
 
@@ -2045,6 +2063,9 @@ function buildPageTranslationPrompt(input) {
2045
2063
  ...input.contextLabel ? [`Document: ${input.contextLabel}`, ""] : [],
2046
2064
  "## Rules",
2047
2065
  ...input.resolved.rules.map((rule) => `- ${rule}`),
2066
+ ...input.slugStrategy === "localized" ? [
2067
+ `- The slug MUST be written in ${localeName}, derived from the ${localeName} title and its meaning \u2014 never the English slug. Transliterate non-Latin ${localeName} into ASCII Latin.`
2068
+ ] : [],
2048
2069
  "",
2049
2070
  "## Output format",
2050
2071
  "Return ONLY valid JSON with keys:",
@@ -2132,9 +2153,11 @@ init_esm_shims();
2132
2153
  function slugStrategyRules(slugStrategy, preserveTerms) {
2133
2154
  if (slugStrategy === "localized") {
2134
2155
  const rules = [
2135
- "Provide a localized URL slug in JSON field `slug`.",
2156
+ "Provide a URL slug in JSON field `slug` that is TRANSLATED into the target language.",
2157
+ "Base the slug on the meaning of the translated title \u2014 do NOT reuse the English slug words.",
2136
2158
  "Slug MUST be ASCII only: a-z, 0-9, hyphens. No uppercase, accents, underscores, or spaces.",
2137
- "For non-Latin locales, transliterate into Latin script."
2159
+ "For non-Latin languages, write the words in the target language and transliterate them into Latin script (e.g. Russian -> romanized Russian, not English).",
2160
+ "Do NOT append locale codes to the slug (e.g. -fr, -he, -zh-cn). Locale routing is handled by the URL prefix, not the slug."
2138
2161
  ];
2139
2162
  if (preserveTerms?.length) {
2140
2163
  rules.push(
@@ -2293,12 +2316,26 @@ async function translatePage(config, item, options = {}) {
2293
2316
  model,
2294
2317
  responseSchema: responseSchema ?? void 0
2295
2318
  });
2296
- const slug = type.slugStrategy === "localized" ? result.parsed.slug ?? existing?.slug ?? item.enSlug : item.enSlug;
2319
+ const rawSlug = type.slugStrategy === "localized" ? result.parsed.slug ?? existing?.slug ?? item.enSlug : item.enSlug;
2320
+ const localeCodes = config.locales.filter((l) => l !== config.defaultLocale);
2321
+ const { slug, stripped, matchedCode } = stripLocaleSuffixFromSlug(rawSlug, localeCodes);
2322
+ const slugAdjusted = stripped && matchedCode ? { from: rawSlug, to: slug, matchedCode } : void 0;
2297
2323
  const validated = validateTranslatedFrontmatter(enDoc, result.parsed.frontmatter, type.schema);
2298
2324
  if (!validated.ok) {
2299
2325
  throw new Error(`Translation validation failed: ${validated.error}`);
2300
2326
  }
2301
2327
  const writeDb = openStore(config, "readwrite");
2328
+ const snapshotId = recordEnSnapshot(
2329
+ config,
2330
+ {
2331
+ contentType: type.id,
2332
+ enSlug: item.enSlug,
2333
+ enHash: currentEnHash,
2334
+ frontmatter: payload.frontmatter,
2335
+ body: payload.body
2336
+ },
2337
+ writeDb
2338
+ );
2302
2339
  upsertTranslation(writeDb, {
2303
2340
  contentType: type.id,
2304
2341
  enSlug: item.enSlug,
@@ -2308,19 +2345,10 @@ async function translatePage(config, item, options = {}) {
2308
2345
  body: result.parsed.body,
2309
2346
  enHash: currentEnHash,
2310
2347
  translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2311
- model: result.model
2348
+ model: result.model,
2349
+ snapshotId
2312
2350
  });
2313
2351
  writeDb.close();
2314
- recordRevision(config, {
2315
- contentType: type.id,
2316
- enSlug: item.enSlug,
2317
- locale: item.locale,
2318
- revisionKind: "translation",
2319
- enHash: currentEnHash,
2320
- body: result.parsed.body,
2321
- frontmatter: validated.frontmatter,
2322
- model: result.model
2323
- });
2324
2352
  const estimatedCostUsd = estimateTranslationCostUsd(
2325
2353
  normalizeGeminiDisplayName(result.model),
2326
2354
  result.usage.inputTokens,
@@ -2332,7 +2360,8 @@ async function translatePage(config, item, options = {}) {
2332
2360
  model: result.model,
2333
2361
  usage: result.usage,
2334
2362
  estimatedCostUsd,
2335
- durationMs: Date.now() - startedAt
2363
+ durationMs: Date.now() - startedAt,
2364
+ slugAdjusted
2336
2365
  };
2337
2366
  } catch (error) {
2338
2367
  return {
@@ -2559,38 +2588,31 @@ function renderFrontmatterTable(frontmatter, schema) {
2559
2588
  <tbody>${rows || `<tr><td colspan="2" class="dim">\u2014</td></tr>`}</tbody>
2560
2589
  </table>`;
2561
2590
  }
2562
- function renderRevisionSnapshot(revision) {
2563
- if (revision.frontmatter_json && revision.body) {
2564
- let frontmatter = {};
2565
- try {
2566
- frontmatter = JSON.parse(revision.frontmatter_json);
2567
- } catch {
2568
- frontmatter = {};
2569
- }
2570
- const fmRows = flattenFrontmatter(frontmatter).map(
2571
- (row) => `<tr><td class="k">${escapeHtml(row.key)}</td><td class="v">${escapeHtml(row.value)}</td></tr>`
2572
- ).join("");
2573
- return `<table class="kv"><tbody>${fmRows}</tbody></table>
2574
- <pre class="code">${escapeHtml(revision.body)}</pre>`;
2575
- }
2576
- return `<p class="dim">${escapeHtml(revision.body_preview ?? "(no snapshot)")}</p>`;
2577
- }
2578
- function renderRevisionTimeline(revisions) {
2579
- if (revisions.length === 0) {
2580
- return `<p class="dim">No history.</p>`;
2581
- }
2582
- const items = revisions.map(
2583
- (row) => `<tr>
2584
- <td class="dim">${escapeHtml(row.created_at.slice(0, 19))}</td>
2585
- <td>${escapeHtml(row.revision_kind)}</td>
2586
- <td class="dim">${row.model ? escapeHtml(row.model) : "\u2014"}</td>
2587
- <td class="dim mono">${escapeHtml(row.en_hash.slice(0, 8))}</td>
2588
- <td><details><summary>view</summary>${renderRevisionSnapshot(row)}</details></td>
2589
- </tr>`
2591
+ function renderEnSnapshotPreview(snapshot) {
2592
+ let frontmatter = {};
2593
+ try {
2594
+ frontmatter = JSON.parse(snapshot.frontmatter_json);
2595
+ } catch {
2596
+ frontmatter = {};
2597
+ }
2598
+ const fmRows = flattenFrontmatter(frontmatter).map(
2599
+ (row) => `<tr><td class="k">${escapeHtml(row.key)}</td><td class="v">${escapeHtml(row.value)}</td></tr>`
2590
2600
  ).join("");
2591
- return `<table class="data"><thead><tr>
2592
- <th>When</th><th>Kind</th><th>Model</th><th>Hash</th><th></th>
2593
- </tr></thead><tbody>${items}</tbody></table>`;
2601
+ return `<table class="kv"><tbody>${fmRows}</tbody></table>
2602
+ <pre class="code">${escapeHtml(snapshot.body)}</pre>`;
2603
+ }
2604
+ function renderTranslationSnapshotPanel(snapshot, currentEnHash) {
2605
+ if (!snapshot) {
2606
+ return `<p class="dim">No EN snapshot linked to this translation.</p>`;
2607
+ }
2608
+ const staleNote = currentEnHash && currentEnHash !== snapshot.en_hash ? `<p class="dim">Current EN hash differs from snapshot (${escapeHtml(currentEnHash.slice(0, 12))} vs ${escapeHtml(snapshot.en_hash.slice(0, 12))}).</p>` : "";
2609
+ return `<dl class="meta">
2610
+ <dt>snapshot</dt><dd>#${snapshot.id}</dd>
2611
+ <dt>captured</dt><dd>${escapeHtml(snapshot.created_at.slice(0, 19))}</dd>
2612
+ <dt>en_hash</dt><dd class="mono">${escapeHtml(snapshot.en_hash.slice(0, 12))}</dd>
2613
+ </dl>
2614
+ ${staleNote}
2615
+ <details><summary>EN source at translation time</summary>${renderEnSnapshotPreview(snapshot)}</details>`;
2594
2616
  }
2595
2617
  function renderLayout(title, body, project, options = {}) {
2596
2618
  const typeLinks = project.listTypes().map((type) => {
@@ -2901,10 +2923,10 @@ async function startStudio(project, options = {}) {
2901
2923
  <dt>slug</dt><dd>${escapeHtml(translation.slug)}</dd>
2902
2924
  <dt>en_hash</dt><dd>${escapeHtml(currentEnHash?.slice(0, 12) ?? "\u2014")} / ${escapeHtml(storedEnHash?.slice(0, 12) ?? "\u2014")}</dd>
2903
2925
  </dl>`;
2904
- const revisions = listRevisions(db, typeId, enSlug, locale);
2926
+ const snapshot = translation.snapshot_id != null ? getEnSnapshot(db, translation.snapshot_id) : void 0;
2905
2927
  historyPanel = `<div class="section">
2906
- <div class="section-head">History</div>
2907
- <div class="section-body">${renderRevisionTimeline(revisions)}</div>
2928
+ <div class="section-head">EN snapshot</div>
2929
+ <div class="section-body">${renderTranslationSnapshotPanel(snapshot, currentEnHash)}</div>
2908
2930
  </div>`;
2909
2931
  } else {
2910
2932
  contentPanel = `<p class="dim" style="padding:12px">No translation for ${escapeHtml(locale)}.</p>`;
@@ -2965,16 +2987,6 @@ async function startStudio(project, options = {}) {
2965
2987
 
2966
2988
  // cli/prompt-translate.ts
2967
2989
  init_esm_shims();
2968
-
2969
- // ../../node_modules/@inquirer/core/dist/lib/errors.js
2970
- init_esm_shims();
2971
- var CancelPromptError = class extends Error {
2972
- constructor() {
2973
- super(...arguments);
2974
- __publicField(this, "name", "CancelPromptError");
2975
- __publicField(this, "message", "Prompt was canceled");
2976
- }
2977
- };
2978
2990
  function isInteractive() {
2979
2991
  return process.stdin.isTTY === true && process.stdout.isTTY === true;
2980
2992
  }
@@ -3086,8 +3098,17 @@ function statusForResult(result, dryRun) {
3086
3098
  if (dryRun) return yellow("would translate");
3087
3099
  return green("translated");
3088
3100
  }
3101
+ function slugAdjustedMessage(result) {
3102
+ if (!result.slugAdjusted) return void 0;
3103
+ const { from, to, matchedCode } = result.slugAdjusted;
3104
+ return yellow(
3105
+ `slug adjusted: "${from}" \u2192 "${to}" (stripped -${matchedCode})`
3106
+ );
3107
+ }
3089
3108
  function detailForResult(result) {
3090
3109
  const parts = [];
3110
+ const slugMsg = slugAdjustedMessage(result);
3111
+ if (slugMsg) parts.push(slugMsg);
3091
3112
  if (result.durationMs !== void 0) parts.push(`${(result.durationMs / 1e3).toFixed(1)}s`);
3092
3113
  if (result.usage) {
3093
3114
  parts.push(`${formatTokenCount(result.usage.inputTokens)} in`);
@@ -3121,6 +3142,10 @@ function createTranslateProgressReporter(options = {}) {
3121
3142
  const status = statusForResult(event.result, dryRun);
3122
3143
  const detail = detailForResult(event.result);
3123
3144
  console.log(`${label}: ${status}${detail ? ` (${detail.replace(/\x1b\[[0-9;]*m/g, "")})` : ""}`);
3145
+ const slugMsg = slugAdjustedMessage(event.result);
3146
+ if (slugMsg) {
3147
+ console.log(`[warning] ${labelForResult(event.result)} ${slugMsg.replace(/\x1b\[[0-9;]*m/g, "")}`);
3148
+ }
3124
3149
  if (event.result.failed && event.result.error) {
3125
3150
  console.error(event.result.error);
3126
3151
  }
@@ -3224,6 +3249,14 @@ function createTranslateProgressReporter(options = {}) {
3224
3249
  for (const result of event.results.filter((entry) => entry.failed)) {
3225
3250
  process.stdout.write("\x1B[2K" + red(`${labelForResult(result)}: ${result.error ?? "failed"}`) + "\n");
3226
3251
  }
3252
+ for (const result of event.results.filter((entry) => entry.slugAdjusted)) {
3253
+ const slugMsg = slugAdjustedMessage(result);
3254
+ if (slugMsg) {
3255
+ process.stdout.write(
3256
+ "\x1B[2K" + yellow(`[warning] ${labelForResult(result)} ${slugMsg.replace(/\x1b\[[0-9;]*m/g, "")}`) + "\n"
3257
+ );
3258
+ }
3259
+ }
3227
3260
  break;
3228
3261
  }
3229
3262
  },
@@ -3369,7 +3402,7 @@ Commands:
3369
3402
  validate Validate EN files and sqlite consistency
3370
3403
  export-static Write raw MDX files for static hosting
3371
3404
  translate Translate stale/missing locale pages
3372
- history <type> <slug> Show revision timeline
3405
+ history <type> <slug> Show EN snapshot timeline
3373
3406
  studio Start read-only local studio
3374
3407
  version Print scribe-cms version
3375
3408
 
@@ -3475,11 +3508,13 @@ Translate flags:
3475
3508
  }
3476
3509
  const { openStore: openStore2 } = await Promise.resolve().then(() => (init_sqlite(), sqlite_exports));
3477
3510
  const db = openStore2(config, "readonly");
3478
- const rows = listRevisions(db, typeId, enSlug, locale);
3511
+ const rows = listEnSnapshotsForEnSlug(db, typeId, enSlug);
3479
3512
  db.close();
3480
3513
  for (const row of rows) {
3514
+ const locales = row.locales ?? "";
3515
+ if (locale && !locales.split(",").includes(locale)) continue;
3481
3516
  console.log(
3482
- `${row.created_at} ${row.revision_kind} locale=${row.locale ?? "en"} en_hash=${row.en_hash.slice(0, 8)} body_hash=${row.body_hash.slice(0, 8)}`
3517
+ `${row.created_at} snapshot=#${row.id} en_hash=${row.en_hash.slice(0, 8)} locales=${locales || "\u2014"}`
3483
3518
  );
3484
3519
  }
3485
3520
  break;