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.cjs
CHANGED
|
@@ -71,12 +71,19 @@ function addColumnIfMissing(db, table, column, ddlType) {
|
|
|
71
71
|
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddlType}`);
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
+
function readSchemaVersion(db) {
|
|
75
|
+
const row = db.prepare(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
|
|
76
|
+
return row ? Number.parseInt(row.value, 10) || 0 : 0;
|
|
77
|
+
}
|
|
74
78
|
function migrate(db) {
|
|
79
|
+
const previousVersion = readSchemaVersion(db);
|
|
75
80
|
for (const sql of MIGRATIONS) {
|
|
76
81
|
db.exec(sql);
|
|
77
82
|
}
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
if (previousVersion < 4) {
|
|
84
|
+
db.exec(`DROP TABLE IF EXISTS revisions`);
|
|
85
|
+
}
|
|
86
|
+
addColumnIfMissing(db, "translations", "snapshot_id", "INTEGER");
|
|
80
87
|
db.prepare(
|
|
81
88
|
`INSERT INTO meta(key, value) VALUES('schema_version', ?)
|
|
82
89
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
@@ -89,7 +96,7 @@ var SCHEMA_VERSION, MIGRATIONS;
|
|
|
89
96
|
var init_sqlite = __esm({
|
|
90
97
|
"src/storage/sqlite.ts"() {
|
|
91
98
|
init_cjs_shims();
|
|
92
|
-
SCHEMA_VERSION =
|
|
99
|
+
SCHEMA_VERSION = 4;
|
|
93
100
|
MIGRATIONS = [
|
|
94
101
|
`CREATE TABLE IF NOT EXISTS meta (
|
|
95
102
|
key TEXT PRIMARY KEY,
|
|
@@ -109,20 +116,6 @@ var init_sqlite = __esm({
|
|
|
109
116
|
)`,
|
|
110
117
|
`CREATE INDEX IF NOT EXISTS idx_translations_type_locale
|
|
111
118
|
ON translations(content_type, locale)`,
|
|
112
|
-
`CREATE TABLE IF NOT EXISTS revisions (
|
|
113
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
114
|
-
content_type TEXT NOT NULL,
|
|
115
|
-
en_slug TEXT NOT NULL,
|
|
116
|
-
locale TEXT,
|
|
117
|
-
revision_kind TEXT NOT NULL,
|
|
118
|
-
en_hash TEXT NOT NULL,
|
|
119
|
-
body_hash TEXT NOT NULL,
|
|
120
|
-
created_at TEXT NOT NULL,
|
|
121
|
-
model TEXT,
|
|
122
|
-
body_preview TEXT
|
|
123
|
-
)`,
|
|
124
|
-
`CREATE INDEX IF NOT EXISTS idx_revisions_lookup
|
|
125
|
-
ON revisions(content_type, en_slug, locale, created_at DESC)`,
|
|
126
119
|
`CREATE TABLE IF NOT EXISTS slug_aliases (
|
|
127
120
|
content_type TEXT NOT NULL,
|
|
128
121
|
canonical_en_slug TEXT NOT NULL,
|
|
@@ -139,7 +132,19 @@ var init_sqlite = __esm({
|
|
|
139
132
|
locale_slug TEXT NOT NULL,
|
|
140
133
|
captured_at TEXT NOT NULL,
|
|
141
134
|
PRIMARY KEY (content_type, alias_en_slug, locale)
|
|
142
|
-
)
|
|
135
|
+
)`,
|
|
136
|
+
`CREATE TABLE IF NOT EXISTS en_snapshots (
|
|
137
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
138
|
+
content_type TEXT NOT NULL,
|
|
139
|
+
en_slug TEXT NOT NULL,
|
|
140
|
+
en_hash TEXT NOT NULL,
|
|
141
|
+
frontmatter_json TEXT NOT NULL,
|
|
142
|
+
body TEXT NOT NULL,
|
|
143
|
+
created_at TEXT NOT NULL,
|
|
144
|
+
UNIQUE (content_type, en_slug, en_hash)
|
|
145
|
+
)`,
|
|
146
|
+
`CREATE INDEX IF NOT EXISTS idx_en_snapshots_lookup
|
|
147
|
+
ON en_snapshots(content_type, en_slug, created_at DESC)`
|
|
143
148
|
];
|
|
144
149
|
}
|
|
145
150
|
});
|
|
@@ -613,18 +618,51 @@ init_sqlite();
|
|
|
613
618
|
|
|
614
619
|
// src/storage/translations.ts
|
|
615
620
|
init_cjs_shims();
|
|
621
|
+
function getOrCreateEnSnapshot(db, input) {
|
|
622
|
+
db.prepare(
|
|
623
|
+
`INSERT OR IGNORE INTO en_snapshots (
|
|
624
|
+
content_type, en_slug, en_hash, frontmatter_json, body, created_at
|
|
625
|
+
) VALUES (?, ?, ?, ?, ?, ?)`
|
|
626
|
+
).run(
|
|
627
|
+
input.contentType,
|
|
628
|
+
input.enSlug,
|
|
629
|
+
input.enHash,
|
|
630
|
+
JSON.stringify(input.frontmatter),
|
|
631
|
+
input.body,
|
|
632
|
+
input.createdAt
|
|
633
|
+
);
|
|
634
|
+
const row = db.prepare(
|
|
635
|
+
`SELECT id FROM en_snapshots
|
|
636
|
+
WHERE content_type = ? AND en_slug = ? AND en_hash = ?`
|
|
637
|
+
).get(input.contentType, input.enSlug, input.enHash);
|
|
638
|
+
return row.id;
|
|
639
|
+
}
|
|
640
|
+
function getEnSnapshot(db, snapshotId) {
|
|
641
|
+
return db.prepare(`SELECT * FROM en_snapshots WHERE id = ?`).get(snapshotId);
|
|
642
|
+
}
|
|
643
|
+
function listEnSnapshotsForEnSlug(db, contentType, enSlug) {
|
|
644
|
+
return db.prepare(
|
|
645
|
+
`SELECT s.*, GROUP_CONCAT(t.locale) AS locales
|
|
646
|
+
FROM en_snapshots s
|
|
647
|
+
LEFT JOIN translations t ON t.snapshot_id = s.id
|
|
648
|
+
WHERE s.content_type = ? AND s.en_slug = ?
|
|
649
|
+
GROUP BY s.id
|
|
650
|
+
ORDER BY s.created_at DESC, s.id DESC`
|
|
651
|
+
).all(contentType, enSlug);
|
|
652
|
+
}
|
|
616
653
|
function upsertTranslation(db, input) {
|
|
617
654
|
db.prepare(
|
|
618
655
|
`INSERT INTO translations (
|
|
619
|
-
content_type, en_slug, locale, slug, frontmatter_json, body, en_hash, translated_at, model
|
|
620
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
656
|
+
content_type, en_slug, locale, slug, frontmatter_json, body, en_hash, translated_at, model, snapshot_id
|
|
657
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
621
658
|
ON CONFLICT(content_type, en_slug, locale) DO UPDATE SET
|
|
622
659
|
slug = excluded.slug,
|
|
623
660
|
frontmatter_json = excluded.frontmatter_json,
|
|
624
661
|
body = excluded.body,
|
|
625
662
|
en_hash = excluded.en_hash,
|
|
626
663
|
translated_at = excluded.translated_at,
|
|
627
|
-
model = excluded.model
|
|
664
|
+
model = excluded.model,
|
|
665
|
+
snapshot_id = excluded.snapshot_id`
|
|
628
666
|
).run(
|
|
629
667
|
input.contentType,
|
|
630
668
|
input.enSlug,
|
|
@@ -634,7 +672,8 @@ function upsertTranslation(db, input) {
|
|
|
634
672
|
input.body,
|
|
635
673
|
input.enHash,
|
|
636
674
|
input.translatedAt,
|
|
637
|
-
input.model
|
|
675
|
+
input.model,
|
|
676
|
+
input.snapshotId
|
|
638
677
|
);
|
|
639
678
|
}
|
|
640
679
|
function getTranslation(db, contentType, enSlug, locale) {
|
|
@@ -656,41 +695,6 @@ function listTranslationsForLocale(db, contentType, locale) {
|
|
|
656
695
|
function bulkLoadTranslations(db) {
|
|
657
696
|
return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
|
|
658
697
|
}
|
|
659
|
-
function appendRevision(db, input) {
|
|
660
|
-
const result = db.prepare(
|
|
661
|
-
`INSERT INTO revisions (
|
|
662
|
-
content_type, en_slug, locale, revision_kind, en_hash, body_hash, created_at,
|
|
663
|
-
model, body_preview, frontmatter_json, body
|
|
664
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
665
|
-
).run(
|
|
666
|
-
input.contentType,
|
|
667
|
-
input.enSlug,
|
|
668
|
-
input.locale,
|
|
669
|
-
input.revisionKind,
|
|
670
|
-
input.enHash,
|
|
671
|
-
input.bodyHash,
|
|
672
|
-
input.createdAt,
|
|
673
|
-
input.model ?? null,
|
|
674
|
-
input.bodyPreview ?? null,
|
|
675
|
-
input.frontmatterJson ?? null,
|
|
676
|
-
input.body ?? null
|
|
677
|
-
);
|
|
678
|
-
return Number(result.lastInsertRowid);
|
|
679
|
-
}
|
|
680
|
-
function listRevisions(db, contentType, enSlug, locale) {
|
|
681
|
-
if (locale) {
|
|
682
|
-
return db.prepare(
|
|
683
|
-
`SELECT * FROM revisions
|
|
684
|
-
WHERE content_type = ? AND en_slug = ? AND locale = ?
|
|
685
|
-
ORDER BY created_at DESC, id DESC`
|
|
686
|
-
).all(contentType, enSlug, locale);
|
|
687
|
-
}
|
|
688
|
-
return db.prepare(
|
|
689
|
-
`SELECT * FROM revisions
|
|
690
|
-
WHERE content_type = ? AND en_slug = ?
|
|
691
|
-
ORDER BY created_at DESC, id DESC`
|
|
692
|
-
).all(contentType, enSlug);
|
|
693
|
-
}
|
|
694
698
|
|
|
695
699
|
// src/loader/normalize-en.ts
|
|
696
700
|
init_cjs_shims();
|
|
@@ -1827,9 +1831,6 @@ function computePageEnHash(translatableFrontmatter, body) {
|
|
|
1827
1831
|
const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
|
|
1828
1832
|
return sha256(payload);
|
|
1829
1833
|
}
|
|
1830
|
-
function computeBodyHash(body) {
|
|
1831
|
-
return sha256(body);
|
|
1832
|
-
}
|
|
1833
1834
|
|
|
1834
1835
|
// src/translate/worklist.ts
|
|
1835
1836
|
init_sqlite();
|
|
@@ -1894,25 +1895,20 @@ function resolveLocalesFromPreset(config, preset, explicitLocales) {
|
|
|
1894
1895
|
// src/translate/page-translator.ts
|
|
1895
1896
|
init_cjs_shims();
|
|
1896
1897
|
|
|
1897
|
-
// src/history/record-
|
|
1898
|
+
// src/history/record-snapshot.ts
|
|
1898
1899
|
init_cjs_shims();
|
|
1899
1900
|
init_sqlite();
|
|
1900
|
-
function
|
|
1901
|
-
const
|
|
1902
|
-
const id =
|
|
1901
|
+
function recordEnSnapshot(config, input, db) {
|
|
1902
|
+
const ownDb = db ?? openStore(config, "readwrite");
|
|
1903
|
+
const id = getOrCreateEnSnapshot(ownDb, {
|
|
1903
1904
|
contentType: input.contentType,
|
|
1904
1905
|
enSlug: input.enSlug,
|
|
1905
|
-
locale: input.locale,
|
|
1906
|
-
revisionKind: input.revisionKind,
|
|
1907
1906
|
enHash: input.enHash,
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
bodyPreview: input.body.slice(0, 200),
|
|
1912
|
-
frontmatterJson: input.frontmatter ? JSON.stringify(input.frontmatter) : null,
|
|
1913
|
-
body: input.body
|
|
1907
|
+
frontmatter: input.frontmatter,
|
|
1908
|
+
body: input.body,
|
|
1909
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1914
1910
|
});
|
|
1915
|
-
db.close();
|
|
1911
|
+
if (!db) ownDb.close();
|
|
1916
1912
|
return id;
|
|
1917
1913
|
}
|
|
1918
1914
|
|
|
@@ -2314,6 +2310,17 @@ async function translatePage(config, item, options = {}) {
|
|
|
2314
2310
|
throw new Error(`Translation validation failed: ${validated.error}`);
|
|
2315
2311
|
}
|
|
2316
2312
|
const writeDb = openStore(config, "readwrite");
|
|
2313
|
+
const snapshotId = recordEnSnapshot(
|
|
2314
|
+
config,
|
|
2315
|
+
{
|
|
2316
|
+
contentType: type.id,
|
|
2317
|
+
enSlug: item.enSlug,
|
|
2318
|
+
enHash: currentEnHash,
|
|
2319
|
+
frontmatter: payload.frontmatter,
|
|
2320
|
+
body: payload.body
|
|
2321
|
+
},
|
|
2322
|
+
writeDb
|
|
2323
|
+
);
|
|
2317
2324
|
upsertTranslation(writeDb, {
|
|
2318
2325
|
contentType: type.id,
|
|
2319
2326
|
enSlug: item.enSlug,
|
|
@@ -2323,19 +2330,10 @@ async function translatePage(config, item, options = {}) {
|
|
|
2323
2330
|
body: result.parsed.body,
|
|
2324
2331
|
enHash: currentEnHash,
|
|
2325
2332
|
translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2326
|
-
model: result.model
|
|
2333
|
+
model: result.model,
|
|
2334
|
+
snapshotId
|
|
2327
2335
|
});
|
|
2328
2336
|
writeDb.close();
|
|
2329
|
-
recordRevision(config, {
|
|
2330
|
-
contentType: type.id,
|
|
2331
|
-
enSlug: item.enSlug,
|
|
2332
|
-
locale: item.locale,
|
|
2333
|
-
revisionKind: "translation",
|
|
2334
|
-
enHash: currentEnHash,
|
|
2335
|
-
body: result.parsed.body,
|
|
2336
|
-
frontmatter: validated.frontmatter,
|
|
2337
|
-
model: result.model
|
|
2338
|
-
});
|
|
2339
2337
|
const estimatedCostUsd = estimateTranslationCostUsd(
|
|
2340
2338
|
normalizeGeminiDisplayName(result.model),
|
|
2341
2339
|
result.usage.inputTokens,
|
|
@@ -2574,38 +2572,31 @@ function renderFrontmatterTable(frontmatter, schema) {
|
|
|
2574
2572
|
<tbody>${rows || `<tr><td colspan="2" class="dim">\u2014</td></tr>`}</tbody>
|
|
2575
2573
|
</table>`;
|
|
2576
2574
|
}
|
|
2577
|
-
function
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
(row) => `<tr><td class="k">${escapeHtml(row.key)}</td><td class="v">${escapeHtml(row.value)}</td></tr>`
|
|
2587
|
-
).join("");
|
|
2588
|
-
return `<table class="kv"><tbody>${fmRows}</tbody></table>
|
|
2589
|
-
<pre class="code">${escapeHtml(revision.body)}</pre>`;
|
|
2590
|
-
}
|
|
2591
|
-
return `<p class="dim">${escapeHtml(revision.body_preview ?? "(no snapshot)")}</p>`;
|
|
2592
|
-
}
|
|
2593
|
-
function renderRevisionTimeline(revisions) {
|
|
2594
|
-
if (revisions.length === 0) {
|
|
2595
|
-
return `<p class="dim">No history.</p>`;
|
|
2596
|
-
}
|
|
2597
|
-
const items = revisions.map(
|
|
2598
|
-
(row) => `<tr>
|
|
2599
|
-
<td class="dim">${escapeHtml(row.created_at.slice(0, 19))}</td>
|
|
2600
|
-
<td>${escapeHtml(row.revision_kind)}</td>
|
|
2601
|
-
<td class="dim">${row.model ? escapeHtml(row.model) : "\u2014"}</td>
|
|
2602
|
-
<td class="dim mono">${escapeHtml(row.en_hash.slice(0, 8))}</td>
|
|
2603
|
-
<td><details><summary>view</summary>${renderRevisionSnapshot(row)}</details></td>
|
|
2604
|
-
</tr>`
|
|
2575
|
+
function renderEnSnapshotPreview(snapshot) {
|
|
2576
|
+
let frontmatter = {};
|
|
2577
|
+
try {
|
|
2578
|
+
frontmatter = JSON.parse(snapshot.frontmatter_json);
|
|
2579
|
+
} catch {
|
|
2580
|
+
frontmatter = {};
|
|
2581
|
+
}
|
|
2582
|
+
const fmRows = flattenFrontmatter(frontmatter).map(
|
|
2583
|
+
(row) => `<tr><td class="k">${escapeHtml(row.key)}</td><td class="v">${escapeHtml(row.value)}</td></tr>`
|
|
2605
2584
|
).join("");
|
|
2606
|
-
return `<table class="
|
|
2607
|
-
<
|
|
2608
|
-
|
|
2585
|
+
return `<table class="kv"><tbody>${fmRows}</tbody></table>
|
|
2586
|
+
<pre class="code">${escapeHtml(snapshot.body)}</pre>`;
|
|
2587
|
+
}
|
|
2588
|
+
function renderTranslationSnapshotPanel(snapshot, currentEnHash) {
|
|
2589
|
+
if (!snapshot) {
|
|
2590
|
+
return `<p class="dim">No EN snapshot linked to this translation.</p>`;
|
|
2591
|
+
}
|
|
2592
|
+
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>` : "";
|
|
2593
|
+
return `<dl class="meta">
|
|
2594
|
+
<dt>snapshot</dt><dd>#${snapshot.id}</dd>
|
|
2595
|
+
<dt>captured</dt><dd>${escapeHtml(snapshot.created_at.slice(0, 19))}</dd>
|
|
2596
|
+
<dt>en_hash</dt><dd class="mono">${escapeHtml(snapshot.en_hash.slice(0, 12))}</dd>
|
|
2597
|
+
</dl>
|
|
2598
|
+
${staleNote}
|
|
2599
|
+
<details><summary>EN source at translation time</summary>${renderEnSnapshotPreview(snapshot)}</details>`;
|
|
2609
2600
|
}
|
|
2610
2601
|
function renderLayout(title, body, project, options = {}) {
|
|
2611
2602
|
const typeLinks = project.listTypes().map((type) => {
|
|
@@ -2916,10 +2907,10 @@ async function startStudio(project, options = {}) {
|
|
|
2916
2907
|
<dt>slug</dt><dd>${escapeHtml(translation.slug)}</dd>
|
|
2917
2908
|
<dt>en_hash</dt><dd>${escapeHtml(currentEnHash?.slice(0, 12) ?? "\u2014")} / ${escapeHtml(storedEnHash?.slice(0, 12) ?? "\u2014")}</dd>
|
|
2918
2909
|
</dl>`;
|
|
2919
|
-
const
|
|
2910
|
+
const snapshot = translation.snapshot_id != null ? getEnSnapshot(db, translation.snapshot_id) : void 0;
|
|
2920
2911
|
historyPanel = `<div class="section">
|
|
2921
|
-
<div class="section-head">
|
|
2922
|
-
<div class="section-body">${
|
|
2912
|
+
<div class="section-head">EN snapshot</div>
|
|
2913
|
+
<div class="section-body">${renderTranslationSnapshotPanel(snapshot, currentEnHash)}</div>
|
|
2923
2914
|
</div>`;
|
|
2924
2915
|
} else {
|
|
2925
2916
|
contentPanel = `<p class="dim" style="padding:12px">No translation for ${escapeHtml(locale)}.</p>`;
|
|
@@ -3384,7 +3375,7 @@ Commands:
|
|
|
3384
3375
|
validate Validate EN files and sqlite consistency
|
|
3385
3376
|
export-static Write raw MDX files for static hosting
|
|
3386
3377
|
translate Translate stale/missing locale pages
|
|
3387
|
-
history <type> <slug> Show
|
|
3378
|
+
history <type> <slug> Show EN snapshot timeline
|
|
3388
3379
|
studio Start read-only local studio
|
|
3389
3380
|
version Print scribe-cms version
|
|
3390
3381
|
|
|
@@ -3490,11 +3481,13 @@ Translate flags:
|
|
|
3490
3481
|
}
|
|
3491
3482
|
const { openStore: openStore2 } = await Promise.resolve().then(() => (init_sqlite(), sqlite_exports));
|
|
3492
3483
|
const db = openStore2(config, "readonly");
|
|
3493
|
-
const rows =
|
|
3484
|
+
const rows = listEnSnapshotsForEnSlug(db, typeId, enSlug);
|
|
3494
3485
|
db.close();
|
|
3495
3486
|
for (const row of rows) {
|
|
3487
|
+
const locales = row.locales ?? "";
|
|
3488
|
+
if (locale && !locales.split(",").includes(locale)) continue;
|
|
3496
3489
|
console.log(
|
|
3497
|
-
`${row.created_at}
|
|
3490
|
+
`${row.created_at} snapshot=#${row.id} en_hash=${row.en_hash.slice(0, 8)} locales=${locales || "\u2014"}`
|
|
3498
3491
|
);
|
|
3499
3492
|
}
|
|
3500
3493
|
break;
|