@tensorix/metahub 0.1.3 → 0.1.4
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/README.md +20 -0
- package/dist/cli.js +769 -79
- package/dist/cli.js.map +17 -13
- package/dist/core/databases.d.ts.map +1 -1
- package/dist/core/documents.d.ts.map +1 -1
- package/dist/core/integrity.d.ts +32 -0
- package/dist/core/integrity.d.ts.map +1 -0
- package/dist/core/properties.d.ts.map +1 -1
- package/dist/core/search.d.ts +2 -0
- package/dist/core/search.d.ts.map +1 -1
- package/dist/core/snapshot.d.ts +2 -0
- package/dist/core/snapshot.d.ts.map +1 -1
- package/dist/core/sync/server.d.ts +14 -0
- package/dist/core/sync/server.d.ts.map +1 -1
- package/dist/core/sync/webui-routes.d.ts.map +1 -1
- package/dist/core/sync/webui.d.ts +11 -0
- package/dist/core/sync/webui.d.ts.map +1 -1
- package/dist/index.js +290 -60
- package/dist/index.js.map +9 -9
- package/dist/webui.js +33 -33
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -1349,9 +1349,14 @@ var require_src = __commonJS((exports, module) => {
|
|
|
1349
1349
|
// src/core/sync/webui.ts
|
|
1350
1350
|
var exports_webui = {};
|
|
1351
1351
|
__export(exports_webui, {
|
|
1352
|
+
warmWebui: () => warmWebui,
|
|
1353
|
+
setWebuiBundle: () => setWebuiBundle,
|
|
1352
1354
|
serveWebui: () => serveWebui
|
|
1353
1355
|
});
|
|
1354
1356
|
import { fileURLToPath } from "url";
|
|
1357
|
+
function setWebuiBundle(js) {
|
|
1358
|
+
cachedJs = js;
|
|
1359
|
+
}
|
|
1355
1360
|
async function getJs() {
|
|
1356
1361
|
if (cachedJs != null)
|
|
1357
1362
|
return cachedJs;
|
|
@@ -1367,6 +1372,11 @@ async function getJs() {
|
|
|
1367
1372
|
cachedJs = await res.outputs[0].text();
|
|
1368
1373
|
return cachedJs;
|
|
1369
1374
|
}
|
|
1375
|
+
async function warmWebui() {
|
|
1376
|
+
try {
|
|
1377
|
+
await getJs();
|
|
1378
|
+
} catch {}
|
|
1379
|
+
}
|
|
1370
1380
|
async function serveWebui(req) {
|
|
1371
1381
|
if (req.method !== "GET")
|
|
1372
1382
|
return null;
|
|
@@ -1393,7 +1403,13 @@ var HTML = `<!doctype html>
|
|
|
1393
1403
|
<title>Metahub</title>
|
|
1394
1404
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
1395
1405
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
1396
|
-
|
|
1406
|
+
<!-- Load web fonts without blocking first paint: media="print" keeps the
|
|
1407
|
+
stylesheet non-render-blocking, then onload flips it to "all". The page
|
|
1408
|
+
paints instantly with the system-font fallbacks baked into --ui/--mono and
|
|
1409
|
+
swaps to the web fonts when they arrive (or stays on system fonts offline).
|
|
1410
|
+
This is what kept the desktop cold-start white for seconds on slow networks. -->
|
|
1411
|
+
<link rel="stylesheet" media="print" onload="this.media='all'" href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">
|
|
1412
|
+
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"></noscript>
|
|
1397
1413
|
<script>try{document.documentElement.dataset.theme=localStorage.getItem('mh-theme')||'system'}catch(e){}</script>
|
|
1398
1414
|
<style>
|
|
1399
1415
|
:root {
|
|
@@ -1503,6 +1519,9 @@ var HTML = `<!doctype html>
|
|
|
1503
1519
|
.set-section { margin-top:34px; }
|
|
1504
1520
|
.set-section-head { font-size:12px; font-weight:600; text-transform:uppercase; letter-spacing:.07em; color:var(--muted); margin-bottom:4px; }
|
|
1505
1521
|
.set-section-desc { color:var(--fg-soft); margin-bottom:16px; }
|
|
1522
|
+
.set-footer { margin-top:40px; padding-top:20px; border-top:1px solid var(--line); display:flex;
|
|
1523
|
+
justify-content:center; gap:8px; color:var(--muted); font-size:12px; }
|
|
1524
|
+
.set-footer-sep { opacity:.5; }
|
|
1506
1525
|
.theme-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; }
|
|
1507
1526
|
.theme-card { position:relative; display:flex; flex-direction:column; align-items:flex-start; gap:10px;
|
|
1508
1527
|
text-align:left; padding:16px; border:1px solid var(--line-strong); border-radius:var(--radius);
|
|
@@ -1540,6 +1559,7 @@ var HTML = `<!doctype html>
|
|
|
1540
1559
|
/* main */
|
|
1541
1560
|
.main { flex:1; display:flex; flex-direction:column; overflow:hidden; min-width:0; }
|
|
1542
1561
|
.topbar { display:flex; align-items:center; gap:6px; padding:10px 18px; border-bottom:1px solid var(--line); min-height:49px; }
|
|
1562
|
+
.topbar.bare { border-bottom:0; }
|
|
1543
1563
|
.topbar .hamburger { display:none; }
|
|
1544
1564
|
.topbar .hamburger.show-collapsed { display:grid; }
|
|
1545
1565
|
.sidebar.collapsed { overflow:hidden; }
|
|
@@ -1864,7 +1884,71 @@ var HTML = `<!doctype html>
|
|
|
1864
1884
|
@keyframes slidein { from { opacity:0; transform:translateX(-12px); } }
|
|
1865
1885
|
|
|
1866
1886
|
/* misc */
|
|
1867
|
-
.empty { display:
|
|
1887
|
+
.empty { display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%;
|
|
1888
|
+
color:var(--muted); text-align:center; padding:40px; }
|
|
1889
|
+
|
|
1890
|
+
/* ---- empty state: \u77E5\u8BC6\u751F\u957F\u63D2\u753B ---- */
|
|
1891
|
+
.estate-art { width:240px; height:208px; overflow:visible; animation:idleFloat 5s ease-in-out 1.6s infinite; }
|
|
1892
|
+
.estate-art .stroke-a { fill:none; stroke:var(--accent); stroke-width:2.4; stroke-linecap:round; stroke-linejoin:round; }
|
|
1893
|
+
.estate-art .stroke { fill:none; stroke:var(--line-strong); stroke-width:2.4; stroke-linecap:round; stroke-linejoin:round; }
|
|
1894
|
+
.estate-art .fill-a { fill:var(--accent); }
|
|
1895
|
+
.estate-art .fill-soft { fill:var(--accent-soft); }
|
|
1896
|
+
.estate-art .fill-surface { fill:var(--bg); }
|
|
1897
|
+
.estate-art .ground { fill:var(--accent); }
|
|
1898
|
+
.estate-title { margin:28px 0 0; font-size:18px; font-weight:600; letter-spacing:.2px; color:var(--fg);
|
|
1899
|
+
opacity:0; animation:fadeUp .5s ease forwards 1.0s; }
|
|
1900
|
+
.estate-hint { margin:9px 0 0; font-size:13.5px; line-height:1.6; color:var(--muted); max-width:300px;
|
|
1901
|
+
opacity:0; animation:fadeUp .5s ease forwards 1.15s; }
|
|
1902
|
+
.estate-link { margin:18px 0 0; font-size:14px; font-weight:600; color:var(--accent);
|
|
1903
|
+
display:inline-flex; align-items:center; gap:6px; padding:9px 8px; border-radius:var(--radius-sm);
|
|
1904
|
+
opacity:0; animation:fadeUp .5s ease forwards 1.3s; transition:opacity .15s; }
|
|
1905
|
+
.estate-link:hover { opacity:.7; }
|
|
1906
|
+
/* intro */
|
|
1907
|
+
.draw { stroke-dasharray:100; stroke-dashoffset:100; }
|
|
1908
|
+
@keyframes draw { to { stroke-dashoffset:0; } }
|
|
1909
|
+
@keyframes leafGrow { from { opacity:0; transform:scale(.2) rotate(-10deg); } to { opacity:1; transform:scale(1) rotate(0); } }
|
|
1910
|
+
@keyframes cardUp { from { opacity:0; transform:translateY(16px) scale(.96); } to { opacity:1; transform:translateY(0) scale(1); } }
|
|
1911
|
+
@keyframes groundIn { to { opacity:.1; } }
|
|
1912
|
+
@keyframes glowIn { to { opacity:.6; } }
|
|
1913
|
+
@keyframes fadeUp { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:translateY(0); } }
|
|
1914
|
+
/* idle loops \u2014 each 0% equals the settled rest state to avoid a jump */
|
|
1915
|
+
@keyframes idleFloat { 0%,100% { transform:translateY(0); } 50% { transform:translateY(-5px); } }
|
|
1916
|
+
@keyframes plantSway { 0%,100% { transform:rotate(0deg); } 25% { transform:rotate(-1.3deg); } 75% { transform:rotate(1.3deg); } }
|
|
1917
|
+
@keyframes leafSwayA { 0%,100% { transform:rotate(0deg); } 25% { transform:rotate(-2.6deg); } 75% { transform:rotate(2.6deg); } }
|
|
1918
|
+
@keyframes leafSwayB { 0%,100% { transform:rotate(0deg); } 25% { transform:rotate(2.6deg); } 75% { transform:rotate(-2.6deg); } }
|
|
1919
|
+
@keyframes groundBreathe { 0%,100% { transform:scale(1); opacity:.10; } 50% { transform:scale(1.07); opacity:.15; } }
|
|
1920
|
+
@keyframes glowPulse { 0%,100% { transform:scale(1); opacity:.6; } 50% { transform:scale(1.1); opacity:.85; } }
|
|
1921
|
+
@keyframes particle { 0% { opacity:0; transform:translate(0,0) scale(.4); } 35% { opacity:.85; transform:translate(2px,-4px) scale(1); } 70% { opacity:0; transform:translate(4px,-9px) scale(.5); } 100% { opacity:0; transform:translate(0,0) scale(.4); } }
|
|
1922
|
+
/* wiring */
|
|
1923
|
+
.kg-ground { transform-box:fill-box; transform-origin:center; opacity:0;
|
|
1924
|
+
animation:groundIn .5s ease forwards .1s, groundBreathe 6s ease-in-out 1.6s infinite; }
|
|
1925
|
+
.kg-glow { filter:blur(9px); transform-box:fill-box; transform-origin:center; opacity:0;
|
|
1926
|
+
animation:glowIn .6s ease forwards .1s, glowPulse 4.6s ease-in-out 1.6s infinite; }
|
|
1927
|
+
.kg-cardB { transform-box:fill-box; transform-origin:center; opacity:0; animation:cardUp .55s cubic-bezier(.2,.8,.2,1) forwards .2s; }
|
|
1928
|
+
.kg-cardA { transform-box:fill-box; transform-origin:center; opacity:0; animation:cardUp .55s cubic-bezier(.2,.8,.2,1) forwards .32s; }
|
|
1929
|
+
.kg-plant { transform-box:fill-box; transform-origin:50% 100%; animation:plantSway 6s ease-in-out 1.7s infinite; }
|
|
1930
|
+
.kg-stem { animation:draw .9s ease-out forwards .4s; }
|
|
1931
|
+
.kg-leaf { transform-box:fill-box; }
|
|
1932
|
+
.kg-grow { transform-box:fill-box; opacity:0; }
|
|
1933
|
+
.kg-leaf.l1 { transform-origin:90% 100%; animation:leafSwayA 4.6s ease-in-out 1.7s infinite; }
|
|
1934
|
+
.kg-leaf.l2 { transform-origin:0% 80%; animation:leafSwayB 5.2s ease-in-out 1.8s infinite; }
|
|
1935
|
+
.kg-leaf.l3 { transform-origin:0% 100%; animation:leafSwayA 5.0s ease-in-out 1.9s infinite; }
|
|
1936
|
+
.kg-leaf.l1 .kg-grow { transform-origin:90% 100%; animation:leafGrow .52s cubic-bezier(.18,.9,.24,1.22) forwards .85s; }
|
|
1937
|
+
.kg-leaf.l2 .kg-grow { transform-origin:0% 80%; animation:leafGrow .52s cubic-bezier(.18,.9,.24,1.22) forwards 1.0s; }
|
|
1938
|
+
.kg-leaf.l3 .kg-grow { transform-origin:0% 100%; animation:leafGrow .52s cubic-bezier(.18,.9,.24,1.22) forwards 1.15s; }
|
|
1939
|
+
.kg-particle { transform-box:fill-box; transform-origin:center; opacity:0; }
|
|
1940
|
+
.kg-particle.p1 { animation:particle 4.5s ease-in-out 1.6s infinite; }
|
|
1941
|
+
.kg-particle.p2 { animation:particle 5.2s ease-in-out 2.0s infinite; }
|
|
1942
|
+
.kg-particle.p3 { animation:particle 4.8s ease-in-out 2.6s infinite; }
|
|
1943
|
+
.kg-particle.p4 { animation:particle 5.6s ease-in-out 3.1s infinite; }
|
|
1944
|
+
.kg-particle.p5 { animation:particle 4.2s ease-in-out 2.3s infinite; }
|
|
1945
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1946
|
+
.estate-art, .estate-art *, .estate-title, .estate-hint, .estate-link { animation:none !important; opacity:1 !important; transform:none !important; }
|
|
1947
|
+
.draw { stroke-dashoffset:0 !important; }
|
|
1948
|
+
.kg-ground { opacity:.1 !important; }
|
|
1949
|
+
.kg-glow { opacity:.6 !important; }
|
|
1950
|
+
.kg-particle { opacity:0 !important; }
|
|
1951
|
+
}
|
|
1868
1952
|
.error-bar { margin:12px 24px; padding:10px 14px; background:var(--danger-soft); color:var(--danger); border-radius:var(--radius-sm); font-size:13px; cursor:pointer; }
|
|
1869
1953
|
.view-placeholder { padding:40px; text-align:center; border:1px dashed var(--line-strong); border-radius:var(--radius); color:var(--muted); margin-top:8px; }
|
|
1870
1954
|
.ico.flip { transform:scaleX(-1); }
|
|
@@ -3456,7 +3540,7 @@ async function runMain(cmd, opts = {}) {
|
|
|
3456
3540
|
// package.json
|
|
3457
3541
|
var package_default = {
|
|
3458
3542
|
name: "@tensorix/metahub",
|
|
3459
|
-
version: "0.1.
|
|
3543
|
+
version: "0.1.4",
|
|
3460
3544
|
description: "Metahub core library and CLI",
|
|
3461
3545
|
type: "module",
|
|
3462
3546
|
main: "./dist/index.js",
|
|
@@ -3482,7 +3566,8 @@ var package_default = {
|
|
|
3482
3566
|
build: "bun run scripts/build.ts",
|
|
3483
3567
|
"build:binaries": "bun run scripts/compile-binaries.ts",
|
|
3484
3568
|
prepublishOnly: "bun run build",
|
|
3485
|
-
test: "bun test"
|
|
3569
|
+
test: "bun test",
|
|
3570
|
+
release: "bun run scripts/tag-release.ts"
|
|
3486
3571
|
},
|
|
3487
3572
|
dependencies: {
|
|
3488
3573
|
"@clack/prompts": "^1.5.0",
|
|
@@ -3683,9 +3768,20 @@ function deleteDatabase(db, id) {
|
|
|
3683
3768
|
if (!getDatabase(db, id))
|
|
3684
3769
|
return false;
|
|
3685
3770
|
emit(db, "databases", id, "__deleted", 1);
|
|
3771
|
+
const tombstone = (table) => {
|
|
3772
|
+
for (const r3 of liveChildren(db, table, "database_id", id))
|
|
3773
|
+
emit(db, table, r3, "__deleted", 1);
|
|
3774
|
+
};
|
|
3775
|
+
tombstone("properties");
|
|
3776
|
+
tombstone("records");
|
|
3777
|
+
for (const d2 of liveChildren(db, "documents", "database_id", id))
|
|
3778
|
+
emit(db, "documents", d2, "database_id", null);
|
|
3686
3779
|
db.query("DELETE FROM meta WHERE key = 'current_db' AND value = ?").run(id);
|
|
3687
3780
|
return true;
|
|
3688
3781
|
}
|
|
3782
|
+
function liveChildren(db, table, col, parentId) {
|
|
3783
|
+
return db.query(`SELECT id FROM ${table} WHERE ${col} = ? AND __deleted = 0`).all(parentId).map((r3) => r3.id);
|
|
3784
|
+
}
|
|
3689
3785
|
|
|
3690
3786
|
// src/core/properties.ts
|
|
3691
3787
|
init_ids();
|
|
@@ -3814,9 +3910,13 @@ function setPropertyWidth(db, id, width) {
|
|
|
3814
3910
|
return updateProperty(db, id, { config: { ...cur.config ?? {}, width: w2 } });
|
|
3815
3911
|
}
|
|
3816
3912
|
function removeProperty(db, id) {
|
|
3817
|
-
|
|
3913
|
+
const prop = getProperty(db, id);
|
|
3914
|
+
if (!prop)
|
|
3818
3915
|
return false;
|
|
3819
3916
|
emit(db, "properties", id, "__deleted", 1);
|
|
3917
|
+
const rows = db.query("SELECT id FROM records WHERE database_id = ? AND __deleted = 0 AND data ->> ? IS NOT NULL").all(prop.database_id, id);
|
|
3918
|
+
for (const r3 of rows)
|
|
3919
|
+
emit(db, "records", r3.id, id, undefined);
|
|
3820
3920
|
return true;
|
|
3821
3921
|
}
|
|
3822
3922
|
|
|
@@ -4622,8 +4722,15 @@ function deleteDocument(db, id) {
|
|
|
4622
4722
|
if (!getDocument(db, id))
|
|
4623
4723
|
return false;
|
|
4624
4724
|
emit(db, "documents", id, "__deleted", 1);
|
|
4725
|
+
for (const b2 of liveChildIds(db, "doc_blocks", "doc_id", id))
|
|
4726
|
+
emit(db, "doc_blocks", b2, "__deleted", 1);
|
|
4727
|
+
for (const c3 of liveChildIds(db, "documents", "parent_id", id))
|
|
4728
|
+
emit(db, "documents", c3, "parent_id", null);
|
|
4625
4729
|
return true;
|
|
4626
4730
|
}
|
|
4731
|
+
function liveChildIds(db, table2, col, parentId) {
|
|
4732
|
+
return db.query(`SELECT id FROM ${table2} WHERE ${col} = ? AND __deleted = 0`).all(parentId).map((r3) => r3.id);
|
|
4733
|
+
}
|
|
4627
4734
|
function countOccurrences(haystack, needle) {
|
|
4628
4735
|
if (needle === "")
|
|
4629
4736
|
return 0;
|
|
@@ -5148,26 +5255,106 @@ var edit_default = defineCommand({
|
|
|
5148
5255
|
});
|
|
5149
5256
|
|
|
5150
5257
|
// src/core/search.ts
|
|
5258
|
+
init_crdt();
|
|
5151
5259
|
var TEXT_TYPES = "('text','url','select','multi_select','date')";
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5260
|
+
var SEARCH_INDEX_VERSION = "1";
|
|
5261
|
+
function readMeta(db, key) {
|
|
5262
|
+
return db.query("SELECT value FROM meta WHERE key = ?").get(key)?.value ?? null;
|
|
5263
|
+
}
|
|
5264
|
+
function writeMeta(db, key, value) {
|
|
5265
|
+
db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
|
|
5266
|
+
}
|
|
5267
|
+
function reindexDocument(db, id) {
|
|
5268
|
+
db.query("DELETE FROM search_fts WHERE kind = 'document' AND id = ?").run(id);
|
|
5269
|
+
const d2 = db.query("SELECT id, title, body, database_id FROM documents WHERE id = ? AND __deleted = 0").get(id);
|
|
5270
|
+
if (d2)
|
|
5161
5271
|
db.query("INSERT INTO search_fts (kind, id, database_id, title, body) VALUES ('document', ?, ?, ?, ?)").run(d2.id, d2.database_id, d2.title ?? "", d2.body ?? "");
|
|
5162
|
-
|
|
5163
|
-
|
|
5272
|
+
}
|
|
5273
|
+
function recordBodyRows(db, where, ...args) {
|
|
5274
|
+
return db.query(`SELECT r.id AS id, r.database_id AS database_id, group_concat(je.value, ' ') AS body
|
|
5164
5275
|
FROM records r, json_each(r.data) je
|
|
5165
5276
|
JOIN properties p ON p.id = je.key AND p.__deleted = 0
|
|
5166
|
-
WHERE r.__deleted = 0 AND p.type IN ${TEXT_TYPES}
|
|
5167
|
-
GROUP BY r.id`).all()
|
|
5168
|
-
|
|
5277
|
+
WHERE ${where} AND r.__deleted = 0 AND p.type IN ${TEXT_TYPES}
|
|
5278
|
+
GROUP BY r.id`).all(...args);
|
|
5279
|
+
}
|
|
5280
|
+
function insertRecordRow(db, r3) {
|
|
5281
|
+
db.query("INSERT INTO search_fts (kind, id, database_id, title, body) VALUES ('record', ?, ?, ?, ?)").run(r3.id, r3.database_id, r3.id, r3.body ?? "");
|
|
5282
|
+
}
|
|
5283
|
+
function reindexRecord(db, id) {
|
|
5284
|
+
db.query("DELETE FROM search_fts WHERE kind = 'record' AND id = ?").run(id);
|
|
5285
|
+
const r3 = recordBodyRows(db, "r.id = ?", id)[0];
|
|
5286
|
+
if (r3)
|
|
5287
|
+
insertRecordRow(db, r3);
|
|
5288
|
+
}
|
|
5289
|
+
function reindexDatabaseRecords(db, databaseId) {
|
|
5290
|
+
db.query("DELETE FROM search_fts WHERE kind = 'record' AND database_id = ?").run(databaseId);
|
|
5291
|
+
for (const r3 of recordBodyRows(db, "r.database_id = ?", databaseId))
|
|
5292
|
+
insertRecordRow(db, r3);
|
|
5293
|
+
}
|
|
5294
|
+
function fullRebuild(db) {
|
|
5295
|
+
db.query("DELETE FROM search_fts").run();
|
|
5296
|
+
for (const d2 of db.query("SELECT id, title, body, database_id FROM documents WHERE __deleted = 0").all())
|
|
5297
|
+
db.query("INSERT INTO search_fts (kind, id, database_id, title, body) VALUES ('document', ?, ?, ?, ?)").run(d2.id, d2.database_id, d2.title ?? "", d2.body ?? "");
|
|
5298
|
+
for (const r3 of recordBodyRows(db, "1 = 1"))
|
|
5299
|
+
insertRecordRow(db, r3);
|
|
5300
|
+
const max = db.query("SELECT MAX(rowid) AS m FROM crdt_changes").get().m ?? 0;
|
|
5301
|
+
writeMeta(db, "search_seq", String(max));
|
|
5302
|
+
writeMeta(db, "search_index_version", SEARCH_INDEX_VERSION);
|
|
5303
|
+
}
|
|
5304
|
+
function incrementalUpdate(db) {
|
|
5305
|
+
const cursor = Number(readMeta(db, "search_seq") ?? "0");
|
|
5306
|
+
const { changes, cursor: next } = changesAfterSeq(db, cursor);
|
|
5307
|
+
if (changes.length === 0)
|
|
5308
|
+
return;
|
|
5309
|
+
const docIds = new Set;
|
|
5310
|
+
const recordIds = new Set;
|
|
5311
|
+
const dbIds = new Set;
|
|
5312
|
+
for (const c3 of changes) {
|
|
5313
|
+
switch (c3.dataset) {
|
|
5314
|
+
case "documents":
|
|
5315
|
+
docIds.add(c3.row_id);
|
|
5316
|
+
break;
|
|
5317
|
+
case "doc_blocks": {
|
|
5318
|
+
const blk = db.query("SELECT doc_id FROM doc_blocks WHERE id = ?").get(c3.row_id);
|
|
5319
|
+
if (blk?.doc_id)
|
|
5320
|
+
docIds.add(blk.doc_id);
|
|
5321
|
+
break;
|
|
5322
|
+
}
|
|
5323
|
+
case "records":
|
|
5324
|
+
recordIds.add(c3.row_id);
|
|
5325
|
+
break;
|
|
5326
|
+
case "properties":
|
|
5327
|
+
if (c3.col === "type" || c3.col === "__deleted" || c3.col === "database_id") {
|
|
5328
|
+
const p = db.query("SELECT database_id FROM properties WHERE id = ?").get(c3.row_id);
|
|
5329
|
+
if (p?.database_id)
|
|
5330
|
+
dbIds.add(p.database_id);
|
|
5331
|
+
}
|
|
5332
|
+
break;
|
|
5333
|
+
}
|
|
5169
5334
|
}
|
|
5170
|
-
|
|
5335
|
+
for (const id of docIds)
|
|
5336
|
+
reindexDocument(db, id);
|
|
5337
|
+
for (const dbId of dbIds)
|
|
5338
|
+
reindexDatabaseRecords(db, dbId);
|
|
5339
|
+
for (const id of recordIds) {
|
|
5340
|
+
const r3 = db.query("SELECT database_id FROM records WHERE id = ?").get(id);
|
|
5341
|
+
if (r3?.database_id && dbIds.has(r3.database_id))
|
|
5342
|
+
continue;
|
|
5343
|
+
reindexRecord(db, id);
|
|
5344
|
+
}
|
|
5345
|
+
writeMeta(db, "search_seq", String(next));
|
|
5346
|
+
}
|
|
5347
|
+
function ensureIndex(db) {
|
|
5348
|
+
if (!ftsAvailable(db))
|
|
5349
|
+
return false;
|
|
5350
|
+
const version = readMeta(db, "search_index_version");
|
|
5351
|
+
const seq = readMeta(db, "search_seq");
|
|
5352
|
+
db.transaction(() => {
|
|
5353
|
+
if (version !== SEARCH_INDEX_VERSION || seq === null)
|
|
5354
|
+
fullRebuild(db);
|
|
5355
|
+
else
|
|
5356
|
+
incrementalUpdate(db);
|
|
5357
|
+
})();
|
|
5171
5358
|
return true;
|
|
5172
5359
|
}
|
|
5173
5360
|
function ftsSearch(db, query, limit) {
|
|
@@ -20166,11 +20353,262 @@ var sync_default = defineCommand({
|
|
|
20166
20353
|
init_crdt();
|
|
20167
20354
|
import { readdirSync, existsSync } from "fs";
|
|
20168
20355
|
import { join as join5 } from "path";
|
|
20356
|
+
|
|
20357
|
+
// src/core/integrity.ts
|
|
20358
|
+
init_crdt();
|
|
20359
|
+
var REF_RULES = [
|
|
20360
|
+
{ srcTable: "properties", fk: "database_id", tgtTable: "databases", fix: "tombstone-src" },
|
|
20361
|
+
{ srcTable: "records", fk: "database_id", tgtTable: "databases", fix: "tombstone-src" },
|
|
20362
|
+
{ srcTable: "documents", fk: "database_id", tgtTable: "databases", fix: "null-fk" },
|
|
20363
|
+
{ srcTable: "documents", fk: "parent_id", tgtTable: "documents", fix: "null-fk" },
|
|
20364
|
+
{ srcTable: "doc_blocks", fk: "doc_id", tgtTable: "documents", fix: "tombstone-src" },
|
|
20365
|
+
{ srcTable: "site_files", fk: "site_id", tgtTable: "sites", fix: "tombstone-src" }
|
|
20366
|
+
];
|
|
20367
|
+
function brokenRefRows(db, r3) {
|
|
20368
|
+
return db.query(`SELECT s.id AS id, s.${r3.fk} AS ref FROM ${r3.srcTable} s
|
|
20369
|
+
JOIN ${r3.tgtTable} t ON t.id = s.${r3.fk} AND t.__deleted = 1
|
|
20370
|
+
WHERE s.__deleted = 0 AND s.${r3.fk} IS NOT NULL
|
|
20371
|
+
ORDER BY s.id`).all();
|
|
20372
|
+
}
|
|
20373
|
+
function detectBrokenRefs(db) {
|
|
20374
|
+
const out = [];
|
|
20375
|
+
for (const r3 of REF_RULES) {
|
|
20376
|
+
for (const row of brokenRefRows(db, r3)) {
|
|
20377
|
+
out.push({
|
|
20378
|
+
category: "broken_ref",
|
|
20379
|
+
entity: r3.srcTable,
|
|
20380
|
+
id: row.id,
|
|
20381
|
+
detail: `${r3.fk} \u2192 deleted ${r3.tgtTable} ${row.ref} (${r3.fix === "tombstone-src" ? "tombstone" : `clear ${r3.fk}`})`,
|
|
20382
|
+
fixable: true
|
|
20383
|
+
});
|
|
20384
|
+
}
|
|
20385
|
+
}
|
|
20386
|
+
return out;
|
|
20387
|
+
}
|
|
20388
|
+
function repairBrokenRefs(db) {
|
|
20389
|
+
let n2 = 0;
|
|
20390
|
+
for (const r3 of REF_RULES) {
|
|
20391
|
+
for (const row of brokenRefRows(db, r3)) {
|
|
20392
|
+
if (r3.fix === "tombstone-src")
|
|
20393
|
+
emit(db, r3.srcTable, row.id, "__deleted", 1);
|
|
20394
|
+
else
|
|
20395
|
+
emit(db, r3.srcTable, row.id, r3.fk, null);
|
|
20396
|
+
n2++;
|
|
20397
|
+
}
|
|
20398
|
+
}
|
|
20399
|
+
return n2;
|
|
20400
|
+
}
|
|
20401
|
+
function orphanCellRows(db) {
|
|
20402
|
+
return db.query(`SELECT r.id AS rec, j.key AS prop
|
|
20403
|
+
FROM records r, json_each(coalesce(r.data, '{}')) j
|
|
20404
|
+
JOIN properties p ON p.id = j.key AND p.__deleted = 1
|
|
20405
|
+
WHERE r.__deleted = 0
|
|
20406
|
+
ORDER BY r.id, j.key`).all();
|
|
20407
|
+
}
|
|
20408
|
+
function detectOrphanCells(db) {
|
|
20409
|
+
return orphanCellRows(db).map((row) => ({
|
|
20410
|
+
category: "orphan_cell",
|
|
20411
|
+
entity: "records",
|
|
20412
|
+
id: row.rec,
|
|
20413
|
+
detail: `cell for deleted property ${row.prop}`,
|
|
20414
|
+
fixable: true
|
|
20415
|
+
}));
|
|
20416
|
+
}
|
|
20417
|
+
function repairOrphanCells(db) {
|
|
20418
|
+
const rows = orphanCellRows(db);
|
|
20419
|
+
for (const row of rows)
|
|
20420
|
+
emit(db, "records", row.rec, row.prop, undefined);
|
|
20421
|
+
return rows.length;
|
|
20422
|
+
}
|
|
20423
|
+
function dupPathLosers(db) {
|
|
20424
|
+
const rows = db.query(`SELECT id, site_id, path FROM site_files
|
|
20425
|
+
WHERE __deleted = 0 AND site_id IS NOT NULL AND path IS NOT NULL
|
|
20426
|
+
ORDER BY site_id, path, created_hlc, id`).all();
|
|
20427
|
+
const losers = [];
|
|
20428
|
+
let group = null;
|
|
20429
|
+
for (const r3 of rows) {
|
|
20430
|
+
const key = `${r3.site_id}\x00${r3.path}`;
|
|
20431
|
+
if (key === group)
|
|
20432
|
+
losers.push({ id: r3.id });
|
|
20433
|
+
else
|
|
20434
|
+
group = key;
|
|
20435
|
+
}
|
|
20436
|
+
return losers;
|
|
20437
|
+
}
|
|
20438
|
+
function detectDupPaths(db) {
|
|
20439
|
+
return dupPathLosers(db).map((row) => ({
|
|
20440
|
+
category: "dup_path",
|
|
20441
|
+
entity: "site_files",
|
|
20442
|
+
id: row.id,
|
|
20443
|
+
detail: "redundant duplicate (site_id, path); loser tombstoned",
|
|
20444
|
+
fixable: true
|
|
20445
|
+
}));
|
|
20446
|
+
}
|
|
20447
|
+
function repairDupPaths(db) {
|
|
20448
|
+
const losers = dupPathLosers(db);
|
|
20449
|
+
for (const row of losers)
|
|
20450
|
+
emit(db, "site_files", row.id, "__deleted", 1);
|
|
20451
|
+
return losers.length;
|
|
20452
|
+
}
|
|
20453
|
+
function detectParentCycles(db) {
|
|
20454
|
+
const rows = db.query(`SELECT d.id AS id, d.parent_id AS parent, d.created_hlc AS hlc
|
|
20455
|
+
FROM documents d
|
|
20456
|
+
WHERE d.__deleted = 0 AND d.parent_id IS NOT NULL
|
|
20457
|
+
AND EXISTS (SELECT 1 FROM documents p WHERE p.id = d.parent_id AND p.__deleted = 0)
|
|
20458
|
+
ORDER BY d.id`).all();
|
|
20459
|
+
const parent = new Map(rows.map((r3) => [r3.id, r3.parent]));
|
|
20460
|
+
const hlc = new Map(rows.map((r3) => [r3.id, r3.hlc]));
|
|
20461
|
+
const seen = new Set;
|
|
20462
|
+
const cycles = [];
|
|
20463
|
+
for (const start of parent.keys()) {
|
|
20464
|
+
if (seen.has(start))
|
|
20465
|
+
continue;
|
|
20466
|
+
const path = [];
|
|
20467
|
+
const pos = new Map;
|
|
20468
|
+
let cur = start;
|
|
20469
|
+
while (cur != null && parent.has(cur) && !seen.has(cur)) {
|
|
20470
|
+
if (pos.has(cur)) {
|
|
20471
|
+
cycles.push(path.slice(pos.get(cur)));
|
|
20472
|
+
break;
|
|
20473
|
+
}
|
|
20474
|
+
pos.set(cur, path.length);
|
|
20475
|
+
path.push(cur);
|
|
20476
|
+
cur = parent.get(cur);
|
|
20477
|
+
}
|
|
20478
|
+
for (const n2 of path)
|
|
20479
|
+
seen.add(n2);
|
|
20480
|
+
}
|
|
20481
|
+
return { cycles, hlc };
|
|
20482
|
+
}
|
|
20483
|
+
function geHlcId(hlc, a2, b2) {
|
|
20484
|
+
const ha = hlc.get(a2) ?? "";
|
|
20485
|
+
const hb = hlc.get(b2) ?? "";
|
|
20486
|
+
if (ha !== hb)
|
|
20487
|
+
return ha > hb;
|
|
20488
|
+
return a2 >= b2;
|
|
20489
|
+
}
|
|
20490
|
+
function detectCycleIssues(db) {
|
|
20491
|
+
const { cycles } = detectParentCycles(db);
|
|
20492
|
+
return cycles.map((cyc) => ({
|
|
20493
|
+
category: "parent_cycle",
|
|
20494
|
+
entity: "documents",
|
|
20495
|
+
id: [...cyc].sort()[0],
|
|
20496
|
+
detail: `parent cycle: ${[...cyc].sort().join(" \u2192 ")}`,
|
|
20497
|
+
fixable: true
|
|
20498
|
+
}));
|
|
20499
|
+
}
|
|
20500
|
+
function repairParentCycles(db) {
|
|
20501
|
+
const { cycles, hlc } = detectParentCycles(db);
|
|
20502
|
+
for (const cyc of cycles) {
|
|
20503
|
+
const brk = cyc.reduce((a2, b2) => geHlcId(hlc, a2, b2) ? a2 : b2);
|
|
20504
|
+
emit(db, "documents", brk, "parent_id", null);
|
|
20505
|
+
}
|
|
20506
|
+
return cycles.length;
|
|
20507
|
+
}
|
|
20508
|
+
function detectDupNames(db) {
|
|
20509
|
+
const out = [];
|
|
20510
|
+
const props = db.query(`SELECT database_id AS db, lower(name) AS n, count(*) AS c, group_concat(id) AS ids
|
|
20511
|
+
FROM properties
|
|
20512
|
+
WHERE __deleted = 0 AND name IS NOT NULL AND database_id IS NOT NULL
|
|
20513
|
+
GROUP BY database_id, lower(name) HAVING c > 1
|
|
20514
|
+
ORDER BY database_id, n`).all();
|
|
20515
|
+
for (const r3 of props)
|
|
20516
|
+
out.push({
|
|
20517
|
+
category: "dup_name",
|
|
20518
|
+
entity: "properties",
|
|
20519
|
+
id: r3.ids.split(",").sort()[0],
|
|
20520
|
+
detail: `${r3.c} properties named "${r3.n}" in database ${r3.db}: ${r3.ids}`,
|
|
20521
|
+
fixable: false
|
|
20522
|
+
});
|
|
20523
|
+
const dbs = db.query(`SELECT lower(name) AS n, count(*) AS c, group_concat(id) AS ids
|
|
20524
|
+
FROM databases WHERE __deleted = 0 AND name IS NOT NULL
|
|
20525
|
+
GROUP BY lower(name) HAVING c > 1 ORDER BY n`).all();
|
|
20526
|
+
for (const r3 of dbs)
|
|
20527
|
+
out.push({
|
|
20528
|
+
category: "dup_name",
|
|
20529
|
+
entity: "databases",
|
|
20530
|
+
id: r3.ids.split(",").sort()[0],
|
|
20531
|
+
detail: `${r3.c} databases named "${r3.n}": ${r3.ids}`,
|
|
20532
|
+
fixable: false
|
|
20533
|
+
});
|
|
20534
|
+
return out;
|
|
20535
|
+
}
|
|
20536
|
+
function configIssue(type, config2) {
|
|
20537
|
+
if (!PROP_TYPES.has(type))
|
|
20538
|
+
return `unknown property type: ${type}`;
|
|
20539
|
+
if (type === "select" || type === "multi_select") {
|
|
20540
|
+
const opts = config2?.options;
|
|
20541
|
+
if (!Array.isArray(opts) || opts.length === 0 || !opts.every((o3) => typeof o3 === "string"))
|
|
20542
|
+
return `${type} missing config.options: string[]`;
|
|
20543
|
+
}
|
|
20544
|
+
if (type === "relation" && typeof config2?.database !== "string")
|
|
20545
|
+
return "relation missing config.database";
|
|
20546
|
+
return null;
|
|
20547
|
+
}
|
|
20548
|
+
function detectBadConfig(db) {
|
|
20549
|
+
const rows = db.query("SELECT id, type, config FROM properties WHERE __deleted = 0 ORDER BY id").all();
|
|
20550
|
+
const out = [];
|
|
20551
|
+
for (const r3 of rows) {
|
|
20552
|
+
let config2 = null;
|
|
20553
|
+
try {
|
|
20554
|
+
config2 = r3.config ? JSON.parse(r3.config) : null;
|
|
20555
|
+
} catch {
|
|
20556
|
+
out.push({ category: "bad_config", entity: "properties", id: r3.id, detail: "config is not valid JSON", fixable: false });
|
|
20557
|
+
continue;
|
|
20558
|
+
}
|
|
20559
|
+
const problem = configIssue(r3.type, config2);
|
|
20560
|
+
if (problem)
|
|
20561
|
+
out.push({ category: "bad_config", entity: "properties", id: r3.id, detail: problem, fixable: false });
|
|
20562
|
+
}
|
|
20563
|
+
return out;
|
|
20564
|
+
}
|
|
20565
|
+
function validateHub(db) {
|
|
20566
|
+
const issues = [
|
|
20567
|
+
...detectBrokenRefs(db),
|
|
20568
|
+
...detectOrphanCells(db),
|
|
20569
|
+
...detectDupPaths(db),
|
|
20570
|
+
...detectCycleIssues(db),
|
|
20571
|
+
...detectDupNames(db),
|
|
20572
|
+
...detectBadConfig(db)
|
|
20573
|
+
];
|
|
20574
|
+
const counts = {};
|
|
20575
|
+
for (const i2 of issues)
|
|
20576
|
+
counts[i2.category] = (counts[i2.category] ?? 0) + 1;
|
|
20577
|
+
return { ok: issues.length === 0, total: issues.length, counts, issues };
|
|
20578
|
+
}
|
|
20579
|
+
function repairPass(db) {
|
|
20580
|
+
const fixed = {};
|
|
20581
|
+
const bump = (cat, n2) => {
|
|
20582
|
+
if (n2)
|
|
20583
|
+
fixed[cat] = (fixed[cat] ?? 0) + n2;
|
|
20584
|
+
};
|
|
20585
|
+
bump("broken_ref", repairBrokenRefs(db));
|
|
20586
|
+
bump("orphan_cell", repairOrphanCells(db));
|
|
20587
|
+
bump("dup_path", repairDupPaths(db));
|
|
20588
|
+
bump("parent_cycle", repairParentCycles(db));
|
|
20589
|
+
const applied = Object.values(fixed).reduce((a2, b2) => a2 + b2, 0);
|
|
20590
|
+
return { applied, fixed };
|
|
20591
|
+
}
|
|
20592
|
+
function repairHub(db) {
|
|
20593
|
+
const fixed = {};
|
|
20594
|
+
let applied = 0;
|
|
20595
|
+
for (let i2 = 0;i2 < 20; i2++) {
|
|
20596
|
+
const pass = repairPass(db);
|
|
20597
|
+
if (pass.applied === 0)
|
|
20598
|
+
break;
|
|
20599
|
+
applied += pass.applied;
|
|
20600
|
+
for (const [cat, n2] of Object.entries(pass.fixed))
|
|
20601
|
+
fixed[cat] = (fixed[cat] ?? 0) + n2;
|
|
20602
|
+
}
|
|
20603
|
+
return { applied, fixed, remaining: validateHub(db) };
|
|
20604
|
+
}
|
|
20605
|
+
|
|
20606
|
+
// src/core/snapshot.ts
|
|
20169
20607
|
init_paths();
|
|
20170
20608
|
init_cache();
|
|
20171
20609
|
var SNAPSHOT_FORMAT = "metahub-snapshot";
|
|
20172
20610
|
var SNAPSHOT_VERSION = 1;
|
|
20173
|
-
function
|
|
20611
|
+
function readMeta2(db, key) {
|
|
20174
20612
|
const row = db.query("SELECT value FROM meta WHERE key = ?").get(key);
|
|
20175
20613
|
return row?.value ?? null;
|
|
20176
20614
|
}
|
|
@@ -20187,8 +20625,8 @@ async function readBlobs() {
|
|
|
20187
20625
|
}
|
|
20188
20626
|
async function createSnapshot(db) {
|
|
20189
20627
|
const changes = changesAfterSeq(db, 0).changes;
|
|
20190
|
-
const node_id =
|
|
20191
|
-
const hlc =
|
|
20628
|
+
const node_id = readMeta2(db, "node_id");
|
|
20629
|
+
const hlc = readMeta2(db, "hlc");
|
|
20192
20630
|
const peers = db.query("SELECT url, pull_cursor, push_cursor FROM peers").all();
|
|
20193
20631
|
const blobs = await readBlobs();
|
|
20194
20632
|
return {
|
|
@@ -20249,8 +20687,9 @@ async function restoreSnapshot(db, pkg, opts = {}) {
|
|
|
20249
20687
|
if (!opts.reset) {
|
|
20250
20688
|
const applied2 = ingest(db, pkg.changes);
|
|
20251
20689
|
rebuildDeclaredIndexes(db);
|
|
20690
|
+
const repaired2 = repairHub(db).applied;
|
|
20252
20691
|
const blobs2 = await writeBlobs(pkg.blobs);
|
|
20253
|
-
return { mode: "merge", applied: applied2, blobs: blobs2 };
|
|
20692
|
+
return { mode: "merge", applied: applied2, blobs: blobs2, repaired: repaired2 };
|
|
20254
20693
|
}
|
|
20255
20694
|
if (!opts.force)
|
|
20256
20695
|
throw new Error("refusing to replace local data without --force (a safety snapshot is saved first)");
|
|
@@ -20268,7 +20707,7 @@ async function restoreSnapshot(db, pkg, opts = {}) {
|
|
|
20268
20707
|
"peers"
|
|
20269
20708
|
])
|
|
20270
20709
|
db.query(`DELETE FROM ${t2}`).run();
|
|
20271
|
-
db.query("DELETE FROM meta WHERE key
|
|
20710
|
+
db.query("DELETE FROM meta WHERE key IN ('search_seq', 'search_index_version')").run();
|
|
20272
20711
|
if (ftsAvailable(db))
|
|
20273
20712
|
db.query("DELETE FROM search_fts").run();
|
|
20274
20713
|
const setMeta2 = db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value");
|
|
@@ -20282,8 +20721,9 @@ async function restoreSnapshot(db, pkg, opts = {}) {
|
|
|
20282
20721
|
});
|
|
20283
20722
|
tx();
|
|
20284
20723
|
rebuildDeclaredIndexes(db);
|
|
20724
|
+
const repaired = repairHub(db).applied;
|
|
20285
20725
|
const blobs = await writeBlobs(pkg.blobs);
|
|
20286
|
-
return { mode: "reset", applied, blobs, safetyPath };
|
|
20726
|
+
return { mode: "reset", applied, blobs, repaired, safetyPath };
|
|
20287
20727
|
}
|
|
20288
20728
|
|
|
20289
20729
|
// src/cli/commands/snapshot.ts
|
|
@@ -20332,10 +20772,78 @@ var restore_default = defineCommand({
|
|
|
20332
20772
|
reset: !!args.reset,
|
|
20333
20773
|
force: !!args.force
|
|
20334
20774
|
});
|
|
20335
|
-
print(result, () => result.mode === "reset" ? `Restored (reset) from ${args.file}: ${result.applied} changes, ${result.blobs} blobs. Safety snapshot: ${result.safetyPath}` : `Merged ${args.file}: ${result.applied} new changes, ${result.blobs} blobs`);
|
|
20775
|
+
print(result, () => result.mode === "reset" ? `Restored (reset) from ${args.file}: ${result.applied} changes, ${result.blobs} blobs, ${result.repaired} repairs. Safety snapshot: ${result.safetyPath}` : `Merged ${args.file}: ${result.applied} new changes, ${result.blobs} blobs, ${result.repaired} repairs`);
|
|
20336
20776
|
})
|
|
20337
20777
|
});
|
|
20338
20778
|
|
|
20779
|
+
// src/cli/commands/doctor.ts
|
|
20780
|
+
var doctor_default = defineCommand({
|
|
20781
|
+
meta: {
|
|
20782
|
+
name: "doctor",
|
|
20783
|
+
description: "Scan the hub for logical-integrity issues (read-only; run `mh repair` to fix)"
|
|
20784
|
+
},
|
|
20785
|
+
run: () => {
|
|
20786
|
+
const report = validateHub(openMetahub());
|
|
20787
|
+
print(report, () => {
|
|
20788
|
+
if (report.ok)
|
|
20789
|
+
return "No integrity issues found.";
|
|
20790
|
+
const head = `${report.total} issue(s): ${Object.entries(report.counts).map(([k2, v2]) => `${k2}=${v2}`).join(", ")}`;
|
|
20791
|
+
const rows = report.issues.map((i2) => ({
|
|
20792
|
+
category: i2.category,
|
|
20793
|
+
entity: i2.entity,
|
|
20794
|
+
id: i2.id,
|
|
20795
|
+
fix: i2.fixable ? "auto" : "report",
|
|
20796
|
+
detail: i2.detail
|
|
20797
|
+
}));
|
|
20798
|
+
return `${head}
|
|
20799
|
+
|
|
20800
|
+
${table(rows)}`;
|
|
20801
|
+
});
|
|
20802
|
+
}
|
|
20803
|
+
});
|
|
20804
|
+
|
|
20805
|
+
// src/cli/commands/repair.ts
|
|
20806
|
+
var repair_default = defineCommand({
|
|
20807
|
+
meta: {
|
|
20808
|
+
name: "repair",
|
|
20809
|
+
description: "Deterministically fix logical-integrity issues (idempotent; replicates over sync)"
|
|
20810
|
+
},
|
|
20811
|
+
args: {
|
|
20812
|
+
"dry-run": {
|
|
20813
|
+
type: "boolean",
|
|
20814
|
+
description: "Report what would be fixed without changing anything (same as `mh doctor`)"
|
|
20815
|
+
}
|
|
20816
|
+
},
|
|
20817
|
+
run: ({ args }) => {
|
|
20818
|
+
const db = openMetahub();
|
|
20819
|
+
if (args["dry-run"]) {
|
|
20820
|
+
const report = validateHub(db);
|
|
20821
|
+
const fixable = report.issues.filter((i2) => i2.fixable);
|
|
20822
|
+
print({ dryRun: true, ...report }, () => report.ok ? "No integrity issues found." : `Would fix ${fixable.length} of ${report.total} issue(s); ${report.total - fixable.length} report-only.
|
|
20823
|
+
|
|
20824
|
+
` + table(report.issues.map((i2) => ({
|
|
20825
|
+
category: i2.category,
|
|
20826
|
+
id: i2.id,
|
|
20827
|
+
fix: i2.fixable ? "auto" : "report",
|
|
20828
|
+
detail: i2.detail
|
|
20829
|
+
}))));
|
|
20830
|
+
return;
|
|
20831
|
+
}
|
|
20832
|
+
const result = repairHub(db);
|
|
20833
|
+
print(result, () => {
|
|
20834
|
+
const fixed = Object.entries(result.fixed).map(([k2, v2]) => `${k2}=${v2}`).join(", ");
|
|
20835
|
+
const head = result.applied === 0 ? "No fixable issues." : `Applied ${result.applied} repair(s): ${fixed}`;
|
|
20836
|
+
const left = result.remaining.issues;
|
|
20837
|
+
if (left.length === 0)
|
|
20838
|
+
return head;
|
|
20839
|
+
return `${head}
|
|
20840
|
+
${left.length} report-only issue(s) need manual attention:
|
|
20841
|
+
|
|
20842
|
+
` + table(left.map((i2) => ({ category: i2.category, id: i2.id, detail: i2.detail })));
|
|
20843
|
+
});
|
|
20844
|
+
}
|
|
20845
|
+
});
|
|
20846
|
+
|
|
20339
20847
|
// src/cli/commands/config.ts
|
|
20340
20848
|
init_node();
|
|
20341
20849
|
|
|
@@ -22251,7 +22759,15 @@ function handle(fn) {
|
|
|
22251
22759
|
}
|
|
22252
22760
|
};
|
|
22253
22761
|
}
|
|
22762
|
+
var VersionSchema = exports_external.object({ version: exports_external.string() });
|
|
22254
22763
|
var webuiRoutes = [
|
|
22764
|
+
{
|
|
22765
|
+
method: "GET",
|
|
22766
|
+
path: "/api/version",
|
|
22767
|
+
summary: "Version of the running core (sidecar). Used by the desktop app's settings footer.",
|
|
22768
|
+
response: VersionSchema,
|
|
22769
|
+
handler: () => Response.json({ version: package_default.version })
|
|
22770
|
+
},
|
|
22255
22771
|
{
|
|
22256
22772
|
method: "GET",
|
|
22257
22773
|
path: "/api/databases",
|
|
@@ -22875,6 +23391,16 @@ function unauthorized() {
|
|
|
22875
23391
|
}
|
|
22876
23392
|
|
|
22877
23393
|
// src/core/sync/server.ts
|
|
23394
|
+
class PortInUseError extends Error {
|
|
23395
|
+
port;
|
|
23396
|
+
host;
|
|
23397
|
+
constructor(port, host) {
|
|
23398
|
+
super(`port ${port} is already in use \u2014 another mh server may be running. ` + `Stop it, or start on a free port with --port <n>.`);
|
|
23399
|
+
this.port = port;
|
|
23400
|
+
this.host = host;
|
|
23401
|
+
this.name = "PortInUseError";
|
|
23402
|
+
}
|
|
23403
|
+
}
|
|
22878
23404
|
function scalarHtml(specUrl) {
|
|
22879
23405
|
return `<!doctype html><html><head>
|
|
22880
23406
|
<meta charset="utf-8"><title>Metahub API</title>
|
|
@@ -22899,48 +23425,60 @@ function startServer(opts = {}) {
|
|
|
22899
23425
|
ttlMs: DEFAULT_TTL_MS,
|
|
22900
23426
|
graceMs: DEFAULT_GRACE_MS
|
|
22901
23427
|
};
|
|
22902
|
-
|
|
22903
|
-
|
|
22904
|
-
|
|
22905
|
-
|
|
22906
|
-
|
|
22907
|
-
|
|
22908
|
-
|
|
22909
|
-
|
|
22910
|
-
|
|
22911
|
-
|
|
22912
|
-
|
|
22913
|
-
|
|
23428
|
+
let server;
|
|
23429
|
+
try {
|
|
23430
|
+
server = Bun.serve({
|
|
23431
|
+
port,
|
|
23432
|
+
hostname: host,
|
|
23433
|
+
async fetch(req) {
|
|
23434
|
+
const url2 = new URL(req.url);
|
|
23435
|
+
if (url2.pathname === SYNC_PATH) {
|
|
23436
|
+
if (!acceptsSyncToken(req, url2, auth, db))
|
|
23437
|
+
return unauthorized();
|
|
23438
|
+
} else {
|
|
23439
|
+
const exempt = url2.pathname === HEALTH_PATH || url2.pathname === RENEW_PATH || url2.pathname === PAIR_PATH;
|
|
23440
|
+
if (!exempt && !hasValidToken(req, url2, auth)) {
|
|
23441
|
+
return wantsHtml(req) ? new Response(unlockPage(), { headers: HTML_HEADERS }) : unauthorized();
|
|
23442
|
+
}
|
|
22914
23443
|
}
|
|
23444
|
+
if (url2.pathname === RENEW_PATH) {
|
|
23445
|
+
const r3 = renewToken(req, url2, auth);
|
|
23446
|
+
return r3 ? Response.json(r3) : unauthorized();
|
|
23447
|
+
}
|
|
23448
|
+
if (req.method === "GET" && url2.pathname === "/docs.json") {
|
|
23449
|
+
return Response.json(buildOpenApi(package_default.version));
|
|
23450
|
+
}
|
|
23451
|
+
if (req.method === "GET" && url2.pathname === "/docs") {
|
|
23452
|
+
return withShim(new Response(scalarHtml("/docs.json"), { headers: HTML_HEADERS }), auth);
|
|
23453
|
+
}
|
|
23454
|
+
if (req.method === "GET" && (url2.pathname === "/" || url2.pathname === "/webui.js")) {
|
|
23455
|
+
const { serveWebui: serveWebui2 } = await Promise.resolve().then(() => (init_webui(), exports_webui));
|
|
23456
|
+
const res = await serveWebui2(req);
|
|
23457
|
+
if (res)
|
|
23458
|
+
return withShim(res, auth);
|
|
23459
|
+
}
|
|
23460
|
+
if (req.method === "GET" && url2.pathname.startsWith("/sites/")) {
|
|
23461
|
+
const { serveSite: serveSite2 } = await Promise.resolve().then(() => (init_sites_serve(), exports_sites_serve));
|
|
23462
|
+
const res = await serveSite2(req, ctx);
|
|
23463
|
+
if (res)
|
|
23464
|
+
return withShim(res, auth);
|
|
23465
|
+
}
|
|
23466
|
+
const route = routes.find((r3) => r3.method === req.method && r3.path === url2.pathname);
|
|
23467
|
+
if (route)
|
|
23468
|
+
return route.handler(req, ctx);
|
|
23469
|
+
return new Response("not found", { status: 404 });
|
|
22915
23470
|
}
|
|
22916
|
-
|
|
22917
|
-
|
|
22918
|
-
|
|
22919
|
-
|
|
22920
|
-
|
|
22921
|
-
return Response.json(buildOpenApi(package_default.version));
|
|
22922
|
-
}
|
|
22923
|
-
if (req.method === "GET" && url2.pathname === "/docs") {
|
|
22924
|
-
return withShim(new Response(scalarHtml("/docs.json"), { headers: HTML_HEADERS }), auth);
|
|
22925
|
-
}
|
|
22926
|
-
if (req.method === "GET" && (url2.pathname === "/" || url2.pathname === "/webui.js")) {
|
|
22927
|
-
const { serveWebui: serveWebui2 } = await Promise.resolve().then(() => (init_webui(), exports_webui));
|
|
22928
|
-
const res = await serveWebui2(req);
|
|
22929
|
-
if (res)
|
|
22930
|
-
return withShim(res, auth);
|
|
22931
|
-
}
|
|
22932
|
-
if (req.method === "GET" && url2.pathname.startsWith("/sites/")) {
|
|
22933
|
-
const { serveSite: serveSite2 } = await Promise.resolve().then(() => (init_sites_serve(), exports_sites_serve));
|
|
22934
|
-
const res = await serveSite2(req, ctx);
|
|
22935
|
-
if (res)
|
|
22936
|
-
return withShim(res, auth);
|
|
22937
|
-
}
|
|
22938
|
-
const route = routes.find((r3) => r3.method === req.method && r3.path === url2.pathname);
|
|
22939
|
-
if (route)
|
|
22940
|
-
return route.handler(req, ctx);
|
|
22941
|
-
return new Response("not found", { status: 404 });
|
|
23471
|
+
});
|
|
23472
|
+
} catch (e3) {
|
|
23473
|
+
const code = e3?.code;
|
|
23474
|
+
if (code === "EADDRINUSE") {
|
|
23475
|
+
throw new PortInUseError(port, host);
|
|
22942
23476
|
}
|
|
22943
|
-
|
|
23477
|
+
if (code === "EACCES") {
|
|
23478
|
+
throw new Error(`permission denied binding ${host}:${port} \u2014 ports below 1024 need elevated privileges; pick a higher port with --port`);
|
|
23479
|
+
}
|
|
23480
|
+
throw e3;
|
|
23481
|
+
}
|
|
22944
23482
|
let timer = null;
|
|
22945
23483
|
if (autoSync && syncIntervalMs > 0) {
|
|
22946
23484
|
timer = setInterval(() => {
|
|
@@ -22949,12 +23487,17 @@ function startServer(opts = {}) {
|
|
|
22949
23487
|
timer.unref?.();
|
|
22950
23488
|
}
|
|
22951
23489
|
const active = activeToken(auth);
|
|
23490
|
+
const authMode = opts.debug ? "disabled" : opts.token ? "static" : "managed";
|
|
22952
23491
|
return {
|
|
22953
23492
|
server,
|
|
22954
23493
|
node,
|
|
22955
23494
|
port: server.port ?? port,
|
|
23495
|
+
host,
|
|
22956
23496
|
token: active?.token ?? null,
|
|
22957
23497
|
exp: active?.exp ?? null,
|
|
23498
|
+
authMode,
|
|
23499
|
+
autoSync: Boolean(autoSync && syncIntervalMs > 0),
|
|
23500
|
+
syncIntervalMs,
|
|
22958
23501
|
stop() {
|
|
22959
23502
|
if (timer)
|
|
22960
23503
|
clearInterval(timer);
|
|
@@ -22963,6 +23506,129 @@ function startServer(opts = {}) {
|
|
|
22963
23506
|
};
|
|
22964
23507
|
}
|
|
22965
23508
|
|
|
23509
|
+
// src/cli/banner.ts
|
|
23510
|
+
var ANSI = /\x1b\[[0-9;]*m/g;
|
|
23511
|
+
function supportsColor() {
|
|
23512
|
+
return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
23513
|
+
}
|
|
23514
|
+
function visibleWidth(s2) {
|
|
23515
|
+
return s2.replace(ANSI, "").length;
|
|
23516
|
+
}
|
|
23517
|
+
var CODES = {
|
|
23518
|
+
reset: "\x1B[0m",
|
|
23519
|
+
dim: "\x1B[2m",
|
|
23520
|
+
bold: "\x1B[1m",
|
|
23521
|
+
green: "\x1B[32m",
|
|
23522
|
+
cyan: "\x1B[36m",
|
|
23523
|
+
yellow: "\x1B[33m",
|
|
23524
|
+
magenta: "\x1B[35m",
|
|
23525
|
+
red: "\x1B[31m"
|
|
23526
|
+
};
|
|
23527
|
+
function makePalette(on) {
|
|
23528
|
+
const wrap = (code) => (s2) => on ? `${code}${s2}${CODES.reset}` : s2;
|
|
23529
|
+
return {
|
|
23530
|
+
reset: (s2) => s2,
|
|
23531
|
+
dim: wrap(CODES.dim),
|
|
23532
|
+
bold: wrap(CODES.bold),
|
|
23533
|
+
green: wrap(CODES.green),
|
|
23534
|
+
cyan: wrap(CODES.cyan),
|
|
23535
|
+
yellow: wrap(CODES.yellow),
|
|
23536
|
+
magenta: wrap(CODES.magenta),
|
|
23537
|
+
red: wrap(CODES.red)
|
|
23538
|
+
};
|
|
23539
|
+
}
|
|
23540
|
+
var MONTHS = [
|
|
23541
|
+
"Jan",
|
|
23542
|
+
"Feb",
|
|
23543
|
+
"Mar",
|
|
23544
|
+
"Apr",
|
|
23545
|
+
"May",
|
|
23546
|
+
"Jun",
|
|
23547
|
+
"Jul",
|
|
23548
|
+
"Aug",
|
|
23549
|
+
"Sep",
|
|
23550
|
+
"Oct",
|
|
23551
|
+
"Nov",
|
|
23552
|
+
"Dec"
|
|
23553
|
+
];
|
|
23554
|
+
function humanizeExpiry(exp, now = Date.now()) {
|
|
23555
|
+
if (exp == null)
|
|
23556
|
+
return "";
|
|
23557
|
+
if (!Number.isFinite(exp))
|
|
23558
|
+
return "never expires";
|
|
23559
|
+
const ms = exp - now;
|
|
23560
|
+
if (ms <= 0)
|
|
23561
|
+
return "expired";
|
|
23562
|
+
const d2 = new Date(exp);
|
|
23563
|
+
const date5 = `${MONTHS[d2.getMonth()]} ${d2.getDate()}`;
|
|
23564
|
+
const mins = Math.round(ms / 60000);
|
|
23565
|
+
let rel;
|
|
23566
|
+
if (mins < 60)
|
|
23567
|
+
rel = `${mins}m`;
|
|
23568
|
+
else if (mins < 60 * 24)
|
|
23569
|
+
rel = `${Math.round(mins / 60)}h`;
|
|
23570
|
+
else
|
|
23571
|
+
rel = `${Math.round(mins / (60 * 24))} days`;
|
|
23572
|
+
return `expires in ${rel} \xB7 ${date5}`;
|
|
23573
|
+
}
|
|
23574
|
+
function formatSync(autoSync, ms) {
|
|
23575
|
+
if (!autoSync || ms <= 0)
|
|
23576
|
+
return "auto-sync off";
|
|
23577
|
+
const secs2 = Math.round(ms / 1000);
|
|
23578
|
+
const human = secs2 % 3600 === 0 ? `${secs2 / 3600}h` : secs2 % 60 === 0 ? `${secs2 / 60}m` : `${secs2}s`;
|
|
23579
|
+
return `every ${human}`;
|
|
23580
|
+
}
|
|
23581
|
+
function isLoopback(host) {
|
|
23582
|
+
return host === "127.0.0.1" || host === "::1" || host === "localhost" || host === "::";
|
|
23583
|
+
}
|
|
23584
|
+
function renderStartupBanner(info) {
|
|
23585
|
+
const on = info.color ?? supportsColor();
|
|
23586
|
+
const c3 = makePalette(on);
|
|
23587
|
+
const LABEL_W = 7;
|
|
23588
|
+
const label = (t2) => c3.dim(t2.padEnd(LABEL_W));
|
|
23589
|
+
const rows = [];
|
|
23590
|
+
const push = (plain, painted) => rows.push([plain, painted]);
|
|
23591
|
+
push(`\u25CF listening ${info.host}:${info.port}`, `${c3.green("\u25CF")} ${c3.dim("listening")} ${c3.bold(`${info.host}:${info.port}`)}`);
|
|
23592
|
+
if (!isLoopback(info.host)) {
|
|
23593
|
+
push("\u26A0 reachable on your network", c3.yellow("\u26A0 reachable on your network"));
|
|
23594
|
+
}
|
|
23595
|
+
push("", "");
|
|
23596
|
+
push(`${"WebUI".padEnd(LABEL_W)}${info.webuiUrl}`, `${label("WebUI")}${c3.cyan(info.webuiUrl)}`);
|
|
23597
|
+
push(`${"Docs".padEnd(LABEL_W)}${info.docsUrl}`, `${label("Docs")}${c3.cyan(info.docsUrl)}`);
|
|
23598
|
+
if (info.authMode === "disabled" || !info.token) {
|
|
23599
|
+
push(`${"Auth".padEnd(LABEL_W)}disabled (--debug)`, `${label("Auth")}${c3.red("disabled")} ${c3.dim("(--debug)")}`);
|
|
23600
|
+
} else {
|
|
23601
|
+
push(`${"Token".padEnd(LABEL_W)}${info.token}`, `${label("Token")}${c3.yellow(info.token)}`);
|
|
23602
|
+
const exp = humanizeExpiry(info.exp);
|
|
23603
|
+
if (exp)
|
|
23604
|
+
push(`${" ".repeat(LABEL_W)}${exp}`, `${" ".repeat(LABEL_W)}${c3.dim(exp)}`);
|
|
23605
|
+
}
|
|
23606
|
+
const sync = formatSync(info.autoSync, info.syncIntervalMs);
|
|
23607
|
+
const syncLabel = info.autoSync && info.syncIntervalMs > 0 ? `sync ${sync}` : sync;
|
|
23608
|
+
push(`${"Node".padEnd(LABEL_W)}${info.node} \xB7 ${syncLabel}`, `${label("Node")}${info.node} ${c3.dim("\xB7")} ${c3.dim(syncLabel)}`);
|
|
23609
|
+
const PAD = 2;
|
|
23610
|
+
const title = "metahub";
|
|
23611
|
+
const ver = `v${info.version}`;
|
|
23612
|
+
const footer = "Ctrl-C to stop";
|
|
23613
|
+
const contentW = Math.max(...rows.map(([plain]) => plain.length), title.length + ver.length + 6, footer.length + 4);
|
|
23614
|
+
const innerW = contentW + PAD * 2;
|
|
23615
|
+
const totalW = innerW + 2;
|
|
23616
|
+
const border = (lc, left, right, rc) => {
|
|
23617
|
+
const leftSeg = left ? `\u2500 ${left} ` : "\u2500";
|
|
23618
|
+
const rightSeg = right ? ` ${right} \u2500` : "\u2500";
|
|
23619
|
+
const dashes = totalW - 2 - visibleWidth(leftSeg) - visibleWidth(rightSeg);
|
|
23620
|
+
return `${lc}${leftSeg}${"\u2500".repeat(Math.max(0, dashes))}${rightSeg}${rc}`;
|
|
23621
|
+
};
|
|
23622
|
+
const top = border("\u256D", c3.bold(c3.magenta(title)), c3.dim(ver), "\u256E");
|
|
23623
|
+
const bottom = border("\u2570", c3.dim(footer), "", "\u256F");
|
|
23624
|
+
const body = rows.map(([plain, painted]) => {
|
|
23625
|
+
const fill = innerW - PAD * 2 - plain.length;
|
|
23626
|
+
return `\u2502${" ".repeat(PAD)}${painted}${" ".repeat(Math.max(0, fill) + PAD)}\u2502`;
|
|
23627
|
+
});
|
|
23628
|
+
return [top, ...body, bottom].join(`
|
|
23629
|
+
`);
|
|
23630
|
+
}
|
|
23631
|
+
|
|
22966
23632
|
// src/cli/help.ts
|
|
22967
23633
|
var ROOT_GUIDE = `metahub \u2014 local knowledge base for AI agents
|
|
22968
23634
|
Notion-like typed databases (tables + rows) and markdown documents, stored in
|
|
@@ -23196,6 +23862,8 @@ var main = defineCommand({
|
|
|
23196
23862
|
sync: sync_default,
|
|
23197
23863
|
snapshot: snapshot_default,
|
|
23198
23864
|
restore: restore_default,
|
|
23865
|
+
doctor: doctor_default,
|
|
23866
|
+
repair: repair_default,
|
|
23199
23867
|
config: config_default
|
|
23200
23868
|
}
|
|
23201
23869
|
});
|
|
@@ -23210,26 +23878,48 @@ var argv2 = process.argv.slice(2);
|
|
|
23210
23878
|
if (argv2.includes("--server")) {
|
|
23211
23879
|
const portFlag = flagValue(argv2, "port");
|
|
23212
23880
|
const intervalFlag = flagValue(argv2, "sync-interval");
|
|
23213
|
-
|
|
23214
|
-
|
|
23215
|
-
|
|
23216
|
-
|
|
23217
|
-
|
|
23218
|
-
|
|
23219
|
-
|
|
23220
|
-
|
|
23221
|
-
|
|
23881
|
+
let s2;
|
|
23882
|
+
try {
|
|
23883
|
+
s2 = startServer({
|
|
23884
|
+
port: portFlag != null ? Number(portFlag) : undefined,
|
|
23885
|
+
host: flagValue(argv2, "host"),
|
|
23886
|
+
debug: argv2.includes("--debug"),
|
|
23887
|
+
token: flagValue(argv2, "token") ?? process.env.METAHUB_TOKEN,
|
|
23888
|
+
syncIntervalMs: intervalFlag != null ? parseDuration(intervalFlag, 30000) : undefined,
|
|
23889
|
+
autoSync: argv2.includes("--no-auto-sync") ? false : undefined
|
|
23890
|
+
});
|
|
23891
|
+
} catch (e3) {
|
|
23892
|
+
fail(e3 instanceof Error ? e3.message : String(e3), e3 instanceof PortInUseError ? 98 : 1);
|
|
23893
|
+
}
|
|
23894
|
+
const webui = `http://localhost:${s2.port}`;
|
|
23895
|
+
const docs = `${webui}/docs`;
|
|
23222
23896
|
print({
|
|
23223
23897
|
server: "listening",
|
|
23224
23898
|
port: s2.port,
|
|
23899
|
+
host: s2.host,
|
|
23225
23900
|
nodeId: s2.node,
|
|
23226
23901
|
docs: `/docs`,
|
|
23902
|
+
webui,
|
|
23903
|
+
authMode: s2.authMode,
|
|
23227
23904
|
token: s2.token,
|
|
23228
|
-
exp: s2.exp != null && Number.isFinite(s2.exp) ? s2.exp : null
|
|
23229
|
-
|
|
23230
|
-
|
|
23905
|
+
exp: s2.exp != null && Number.isFinite(s2.exp) ? s2.exp : null,
|
|
23906
|
+
autoSync: s2.autoSync,
|
|
23907
|
+
syncIntervalMs: s2.syncIntervalMs
|
|
23908
|
+
}, () => renderStartupBanner({
|
|
23909
|
+
version: package_default.version,
|
|
23910
|
+
host: s2.host,
|
|
23911
|
+
port: s2.port,
|
|
23912
|
+
webuiUrl: webui,
|
|
23913
|
+
docsUrl: docs,
|
|
23914
|
+
authMode: s2.authMode,
|
|
23915
|
+
token: s2.token,
|
|
23916
|
+
exp: s2.exp,
|
|
23917
|
+
node: s2.node,
|
|
23918
|
+
autoSync: s2.autoSync,
|
|
23919
|
+
syncIntervalMs: s2.syncIntervalMs
|
|
23920
|
+
}));
|
|
23231
23921
|
} else {
|
|
23232
23922
|
runMain(main, { showUsage: showUsage2 });
|
|
23233
23923
|
}
|
|
23234
23924
|
|
|
23235
|
-
//# debugId=
|
|
23925
|
+
//# debugId=74D4BFCB8CE1AB2064756E2164756E21
|