scribe-cms 0.0.5 → 0.0.6
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.cjs +114 -121
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +114 -121
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +69 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +69 -70
- package/dist/index.js.map +1 -1
- package/dist/runtime.cjs +23 -18
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +23 -18
- package/dist/runtime.js.map +1 -1
- package/dist/src/history/record-snapshot.d.ts +11 -0
- package/dist/src/history/record-snapshot.d.ts.map +1 -0
- package/dist/src/storage/sqlite.d.ts.map +1 -1
- package/dist/src/storage/translations.d.ts +14 -21
- package/dist/src/storage/translations.d.ts.map +1 -1
- package/dist/src/translate/page-translator.d.ts.map +1 -1
- package/dist/studio/server.cjs +51 -64
- package/dist/studio/server.cjs.map +1 -1
- package/dist/studio/server.d.ts.map +1 -1
- package/dist/studio/server.js +51 -64
- package/dist/studio/server.js.map +1 -1
- package/package.json +1 -1
- package/dist/src/history/record-revision.d.ts +0 -14
- package/dist/src/history/record-revision.d.ts.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -56,12 +56,19 @@ function addColumnIfMissing(db, table, column, ddlType) {
|
|
|
56
56
|
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddlType}`);
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
function readSchemaVersion(db) {
|
|
60
|
+
const row = db.prepare(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
|
|
61
|
+
return row ? Number.parseInt(row.value, 10) || 0 : 0;
|
|
62
|
+
}
|
|
59
63
|
function migrate(db) {
|
|
64
|
+
const previousVersion = readSchemaVersion(db);
|
|
60
65
|
for (const sql of MIGRATIONS) {
|
|
61
66
|
db.exec(sql);
|
|
62
67
|
}
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
if (previousVersion < 4) {
|
|
69
|
+
db.exec(`DROP TABLE IF EXISTS revisions`);
|
|
70
|
+
}
|
|
71
|
+
addColumnIfMissing(db, "translations", "snapshot_id", "INTEGER");
|
|
65
72
|
db.prepare(
|
|
66
73
|
`INSERT INTO meta(key, value) VALUES('schema_version', ?)
|
|
67
74
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
@@ -74,7 +81,7 @@ var SCHEMA_VERSION, MIGRATIONS;
|
|
|
74
81
|
var init_sqlite = __esm({
|
|
75
82
|
"src/storage/sqlite.ts"() {
|
|
76
83
|
init_esm_shims();
|
|
77
|
-
SCHEMA_VERSION =
|
|
84
|
+
SCHEMA_VERSION = 4;
|
|
78
85
|
MIGRATIONS = [
|
|
79
86
|
`CREATE TABLE IF NOT EXISTS meta (
|
|
80
87
|
key TEXT PRIMARY KEY,
|
|
@@ -94,20 +101,6 @@ var init_sqlite = __esm({
|
|
|
94
101
|
)`,
|
|
95
102
|
`CREATE INDEX IF NOT EXISTS idx_translations_type_locale
|
|
96
103
|
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
104
|
`CREATE TABLE IF NOT EXISTS slug_aliases (
|
|
112
105
|
content_type TEXT NOT NULL,
|
|
113
106
|
canonical_en_slug TEXT NOT NULL,
|
|
@@ -124,7 +117,19 @@ var init_sqlite = __esm({
|
|
|
124
117
|
locale_slug TEXT NOT NULL,
|
|
125
118
|
captured_at TEXT NOT NULL,
|
|
126
119
|
PRIMARY KEY (content_type, alias_en_slug, locale)
|
|
127
|
-
)
|
|
120
|
+
)`,
|
|
121
|
+
`CREATE TABLE IF NOT EXISTS en_snapshots (
|
|
122
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
123
|
+
content_type TEXT NOT NULL,
|
|
124
|
+
en_slug TEXT NOT NULL,
|
|
125
|
+
en_hash TEXT NOT NULL,
|
|
126
|
+
frontmatter_json TEXT NOT NULL,
|
|
127
|
+
body TEXT NOT NULL,
|
|
128
|
+
created_at TEXT NOT NULL,
|
|
129
|
+
UNIQUE (content_type, en_slug, en_hash)
|
|
130
|
+
)`,
|
|
131
|
+
`CREATE INDEX IF NOT EXISTS idx_en_snapshots_lookup
|
|
132
|
+
ON en_snapshots(content_type, en_slug, created_at DESC)`
|
|
128
133
|
];
|
|
129
134
|
}
|
|
130
135
|
});
|
|
@@ -598,18 +603,51 @@ init_sqlite();
|
|
|
598
603
|
|
|
599
604
|
// src/storage/translations.ts
|
|
600
605
|
init_esm_shims();
|
|
606
|
+
function getOrCreateEnSnapshot(db, input) {
|
|
607
|
+
db.prepare(
|
|
608
|
+
`INSERT OR IGNORE INTO en_snapshots (
|
|
609
|
+
content_type, en_slug, en_hash, frontmatter_json, body, created_at
|
|
610
|
+
) VALUES (?, ?, ?, ?, ?, ?)`
|
|
611
|
+
).run(
|
|
612
|
+
input.contentType,
|
|
613
|
+
input.enSlug,
|
|
614
|
+
input.enHash,
|
|
615
|
+
JSON.stringify(input.frontmatter),
|
|
616
|
+
input.body,
|
|
617
|
+
input.createdAt
|
|
618
|
+
);
|
|
619
|
+
const row = db.prepare(
|
|
620
|
+
`SELECT id FROM en_snapshots
|
|
621
|
+
WHERE content_type = ? AND en_slug = ? AND en_hash = ?`
|
|
622
|
+
).get(input.contentType, input.enSlug, input.enHash);
|
|
623
|
+
return row.id;
|
|
624
|
+
}
|
|
625
|
+
function getEnSnapshot(db, snapshotId) {
|
|
626
|
+
return db.prepare(`SELECT * FROM en_snapshots WHERE id = ?`).get(snapshotId);
|
|
627
|
+
}
|
|
628
|
+
function listEnSnapshotsForEnSlug(db, contentType, enSlug) {
|
|
629
|
+
return db.prepare(
|
|
630
|
+
`SELECT s.*, GROUP_CONCAT(t.locale) AS locales
|
|
631
|
+
FROM en_snapshots s
|
|
632
|
+
LEFT JOIN translations t ON t.snapshot_id = s.id
|
|
633
|
+
WHERE s.content_type = ? AND s.en_slug = ?
|
|
634
|
+
GROUP BY s.id
|
|
635
|
+
ORDER BY s.created_at DESC, s.id DESC`
|
|
636
|
+
).all(contentType, enSlug);
|
|
637
|
+
}
|
|
601
638
|
function upsertTranslation(db, input) {
|
|
602
639
|
db.prepare(
|
|
603
640
|
`INSERT INTO translations (
|
|
604
|
-
content_type, en_slug, locale, slug, frontmatter_json, body, en_hash, translated_at, model
|
|
605
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
641
|
+
content_type, en_slug, locale, slug, frontmatter_json, body, en_hash, translated_at, model, snapshot_id
|
|
642
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
606
643
|
ON CONFLICT(content_type, en_slug, locale) DO UPDATE SET
|
|
607
644
|
slug = excluded.slug,
|
|
608
645
|
frontmatter_json = excluded.frontmatter_json,
|
|
609
646
|
body = excluded.body,
|
|
610
647
|
en_hash = excluded.en_hash,
|
|
611
648
|
translated_at = excluded.translated_at,
|
|
612
|
-
model = excluded.model
|
|
649
|
+
model = excluded.model,
|
|
650
|
+
snapshot_id = excluded.snapshot_id`
|
|
613
651
|
).run(
|
|
614
652
|
input.contentType,
|
|
615
653
|
input.enSlug,
|
|
@@ -619,7 +657,8 @@ function upsertTranslation(db, input) {
|
|
|
619
657
|
input.body,
|
|
620
658
|
input.enHash,
|
|
621
659
|
input.translatedAt,
|
|
622
|
-
input.model
|
|
660
|
+
input.model,
|
|
661
|
+
input.snapshotId
|
|
623
662
|
);
|
|
624
663
|
}
|
|
625
664
|
function getTranslation(db, contentType, enSlug, locale) {
|
|
@@ -641,41 +680,6 @@ function listTranslationsForLocale(db, contentType, locale) {
|
|
|
641
680
|
function bulkLoadTranslations(db) {
|
|
642
681
|
return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
|
|
643
682
|
}
|
|
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
683
|
|
|
680
684
|
// src/loader/normalize-en.ts
|
|
681
685
|
init_esm_shims();
|
|
@@ -1812,9 +1816,6 @@ function computePageEnHash(translatableFrontmatter, body) {
|
|
|
1812
1816
|
const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
|
|
1813
1817
|
return sha256(payload);
|
|
1814
1818
|
}
|
|
1815
|
-
function computeBodyHash(body) {
|
|
1816
|
-
return sha256(body);
|
|
1817
|
-
}
|
|
1818
1819
|
|
|
1819
1820
|
// src/translate/worklist.ts
|
|
1820
1821
|
init_sqlite();
|
|
@@ -1879,25 +1880,20 @@ function resolveLocalesFromPreset(config, preset, explicitLocales) {
|
|
|
1879
1880
|
// src/translate/page-translator.ts
|
|
1880
1881
|
init_esm_shims();
|
|
1881
1882
|
|
|
1882
|
-
// src/history/record-
|
|
1883
|
+
// src/history/record-snapshot.ts
|
|
1883
1884
|
init_esm_shims();
|
|
1884
1885
|
init_sqlite();
|
|
1885
|
-
function
|
|
1886
|
-
const
|
|
1887
|
-
const id =
|
|
1886
|
+
function recordEnSnapshot(config, input, db) {
|
|
1887
|
+
const ownDb = db ?? openStore(config, "readwrite");
|
|
1888
|
+
const id = getOrCreateEnSnapshot(ownDb, {
|
|
1888
1889
|
contentType: input.contentType,
|
|
1889
1890
|
enSlug: input.enSlug,
|
|
1890
|
-
locale: input.locale,
|
|
1891
|
-
revisionKind: input.revisionKind,
|
|
1892
1891
|
enHash: input.enHash,
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
bodyPreview: input.body.slice(0, 200),
|
|
1897
|
-
frontmatterJson: input.frontmatter ? JSON.stringify(input.frontmatter) : null,
|
|
1898
|
-
body: input.body
|
|
1892
|
+
frontmatter: input.frontmatter,
|
|
1893
|
+
body: input.body,
|
|
1894
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1899
1895
|
});
|
|
1900
|
-
db.close();
|
|
1896
|
+
if (!db) ownDb.close();
|
|
1901
1897
|
return id;
|
|
1902
1898
|
}
|
|
1903
1899
|
|
|
@@ -2299,6 +2295,17 @@ async function translatePage(config, item, options = {}) {
|
|
|
2299
2295
|
throw new Error(`Translation validation failed: ${validated.error}`);
|
|
2300
2296
|
}
|
|
2301
2297
|
const writeDb = openStore(config, "readwrite");
|
|
2298
|
+
const snapshotId = recordEnSnapshot(
|
|
2299
|
+
config,
|
|
2300
|
+
{
|
|
2301
|
+
contentType: type.id,
|
|
2302
|
+
enSlug: item.enSlug,
|
|
2303
|
+
enHash: currentEnHash,
|
|
2304
|
+
frontmatter: payload.frontmatter,
|
|
2305
|
+
body: payload.body
|
|
2306
|
+
},
|
|
2307
|
+
writeDb
|
|
2308
|
+
);
|
|
2302
2309
|
upsertTranslation(writeDb, {
|
|
2303
2310
|
contentType: type.id,
|
|
2304
2311
|
enSlug: item.enSlug,
|
|
@@ -2308,19 +2315,10 @@ async function translatePage(config, item, options = {}) {
|
|
|
2308
2315
|
body: result.parsed.body,
|
|
2309
2316
|
enHash: currentEnHash,
|
|
2310
2317
|
translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2311
|
-
model: result.model
|
|
2318
|
+
model: result.model,
|
|
2319
|
+
snapshotId
|
|
2312
2320
|
});
|
|
2313
2321
|
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
2322
|
const estimatedCostUsd = estimateTranslationCostUsd(
|
|
2325
2323
|
normalizeGeminiDisplayName(result.model),
|
|
2326
2324
|
result.usage.inputTokens,
|
|
@@ -2559,38 +2557,31 @@ function renderFrontmatterTable(frontmatter, schema) {
|
|
|
2559
2557
|
<tbody>${rows || `<tr><td colspan="2" class="dim">\u2014</td></tr>`}</tbody>
|
|
2560
2558
|
</table>`;
|
|
2561
2559
|
}
|
|
2562
|
-
function
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
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>`
|
|
2560
|
+
function renderEnSnapshotPreview(snapshot) {
|
|
2561
|
+
let frontmatter = {};
|
|
2562
|
+
try {
|
|
2563
|
+
frontmatter = JSON.parse(snapshot.frontmatter_json);
|
|
2564
|
+
} catch {
|
|
2565
|
+
frontmatter = {};
|
|
2566
|
+
}
|
|
2567
|
+
const fmRows = flattenFrontmatter(frontmatter).map(
|
|
2568
|
+
(row) => `<tr><td class="k">${escapeHtml(row.key)}</td><td class="v">${escapeHtml(row.value)}</td></tr>`
|
|
2590
2569
|
).join("");
|
|
2591
|
-
return `<table class="
|
|
2592
|
-
<
|
|
2593
|
-
|
|
2570
|
+
return `<table class="kv"><tbody>${fmRows}</tbody></table>
|
|
2571
|
+
<pre class="code">${escapeHtml(snapshot.body)}</pre>`;
|
|
2572
|
+
}
|
|
2573
|
+
function renderTranslationSnapshotPanel(snapshot, currentEnHash) {
|
|
2574
|
+
if (!snapshot) {
|
|
2575
|
+
return `<p class="dim">No EN snapshot linked to this translation.</p>`;
|
|
2576
|
+
}
|
|
2577
|
+
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>` : "";
|
|
2578
|
+
return `<dl class="meta">
|
|
2579
|
+
<dt>snapshot</dt><dd>#${snapshot.id}</dd>
|
|
2580
|
+
<dt>captured</dt><dd>${escapeHtml(snapshot.created_at.slice(0, 19))}</dd>
|
|
2581
|
+
<dt>en_hash</dt><dd class="mono">${escapeHtml(snapshot.en_hash.slice(0, 12))}</dd>
|
|
2582
|
+
</dl>
|
|
2583
|
+
${staleNote}
|
|
2584
|
+
<details><summary>EN source at translation time</summary>${renderEnSnapshotPreview(snapshot)}</details>`;
|
|
2594
2585
|
}
|
|
2595
2586
|
function renderLayout(title, body, project, options = {}) {
|
|
2596
2587
|
const typeLinks = project.listTypes().map((type) => {
|
|
@@ -2901,10 +2892,10 @@ async function startStudio(project, options = {}) {
|
|
|
2901
2892
|
<dt>slug</dt><dd>${escapeHtml(translation.slug)}</dd>
|
|
2902
2893
|
<dt>en_hash</dt><dd>${escapeHtml(currentEnHash?.slice(0, 12) ?? "\u2014")} / ${escapeHtml(storedEnHash?.slice(0, 12) ?? "\u2014")}</dd>
|
|
2903
2894
|
</dl>`;
|
|
2904
|
-
const
|
|
2895
|
+
const snapshot = translation.snapshot_id != null ? getEnSnapshot(db, translation.snapshot_id) : void 0;
|
|
2905
2896
|
historyPanel = `<div class="section">
|
|
2906
|
-
<div class="section-head">
|
|
2907
|
-
<div class="section-body">${
|
|
2897
|
+
<div class="section-head">EN snapshot</div>
|
|
2898
|
+
<div class="section-body">${renderTranslationSnapshotPanel(snapshot, currentEnHash)}</div>
|
|
2908
2899
|
</div>`;
|
|
2909
2900
|
} else {
|
|
2910
2901
|
contentPanel = `<p class="dim" style="padding:12px">No translation for ${escapeHtml(locale)}.</p>`;
|
|
@@ -3369,7 +3360,7 @@ Commands:
|
|
|
3369
3360
|
validate Validate EN files and sqlite consistency
|
|
3370
3361
|
export-static Write raw MDX files for static hosting
|
|
3371
3362
|
translate Translate stale/missing locale pages
|
|
3372
|
-
history <type> <slug> Show
|
|
3363
|
+
history <type> <slug> Show EN snapshot timeline
|
|
3373
3364
|
studio Start read-only local studio
|
|
3374
3365
|
version Print scribe-cms version
|
|
3375
3366
|
|
|
@@ -3475,11 +3466,13 @@ Translate flags:
|
|
|
3475
3466
|
}
|
|
3476
3467
|
const { openStore: openStore2 } = await Promise.resolve().then(() => (init_sqlite(), sqlite_exports));
|
|
3477
3468
|
const db = openStore2(config, "readonly");
|
|
3478
|
-
const rows =
|
|
3469
|
+
const rows = listEnSnapshotsForEnSlug(db, typeId, enSlug);
|
|
3479
3470
|
db.close();
|
|
3480
3471
|
for (const row of rows) {
|
|
3472
|
+
const locales = row.locales ?? "";
|
|
3473
|
+
if (locale && !locales.split(",").includes(locale)) continue;
|
|
3481
3474
|
console.log(
|
|
3482
|
-
`${row.created_at}
|
|
3475
|
+
`${row.created_at} snapshot=#${row.id} en_hash=${row.en_hash.slice(0, 8)} locales=${locales || "\u2014"}`
|
|
3483
3476
|
);
|
|
3484
3477
|
}
|
|
3485
3478
|
break;
|