@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/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
- <link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
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:grid; place-items:center; height:100%; color:var(--muted); text-align:center; padding:40px; }
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.3",
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
- if (!getProperty(db, id))
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
- function ensureIndex(db) {
5153
- if (!ftsAvailable(db))
5154
- return false;
5155
- const max = db.query("SELECT MAX(hlc) AS h FROM crdt_changes").get().h ?? "";
5156
- const indexed = db.query("SELECT value FROM meta WHERE key = 'search_hlc'").get()?.value ?? "";
5157
- if (max === indexed)
5158
- return true;
5159
- db.query("DELETE FROM search_fts").run();
5160
- for (const d2 of db.query("SELECT id, title, body, database_id FROM documents WHERE __deleted = 0").all()) {
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
- for (const r3 of db.query(`SELECT r.id AS id, r.database_id AS database_id, group_concat(je.value, ' ') AS body
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
- db.query("INSERT INTO search_fts (kind, id, database_id, title, body) VALUES ('record', ?, ?, ?, ?)").run(r3.id, r3.database_id, r3.id, r3.body ?? "");
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
- db.query("INSERT INTO meta (key, value) VALUES ('search_hlc', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(max);
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 readMeta(db, key) {
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 = readMeta(db, "node_id");
20191
- const hlc = readMeta(db, "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 = 'search_hlc'").run();
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
- const server = Bun.serve({
22903
- port,
22904
- hostname: host,
22905
- async fetch(req) {
22906
- const url2 = new URL(req.url);
22907
- if (url2.pathname === SYNC_PATH) {
22908
- if (!acceptsSyncToken(req, url2, auth, db))
22909
- return unauthorized();
22910
- } else {
22911
- const exempt = url2.pathname === HEALTH_PATH || url2.pathname === RENEW_PATH || url2.pathname === PAIR_PATH;
22912
- if (!exempt && !hasValidToken(req, url2, auth)) {
22913
- return wantsHtml(req) ? new Response(unlockPage(), { headers: HTML_HEADERS }) : unauthorized();
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
- if (url2.pathname === RENEW_PATH) {
22917
- const r3 = renewToken(req, url2, auth);
22918
- return r3 ? Response.json(r3) : unauthorized();
22919
- }
22920
- if (req.method === "GET" && url2.pathname === "/docs.json") {
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
- const s2 = startServer({
23214
- port: portFlag != null ? Number(portFlag) : undefined,
23215
- host: flagValue(argv2, "host"),
23216
- debug: argv2.includes("--debug"),
23217
- token: flagValue(argv2, "token") ?? process.env.METAHUB_TOKEN,
23218
- syncIntervalMs: intervalFlag != null ? parseDuration(intervalFlag, 30000) : undefined,
23219
- autoSync: argv2.includes("--no-auto-sync") ? false : undefined
23220
- });
23221
- const expNote = s2.token && s2.exp != null && Number.isFinite(s2.exp) ? ` (expires ${new Date(s2.exp).toISOString()})` : "";
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
- }, () => `metahub sync server on :${s2.port} (node ${s2.node}) \u2014 docs at http://localhost:${s2.port}/docs
23230
- ` + (s2.token ? `auth token: ${s2.token}${expNote}` : `auth: disabled (--debug)`));
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=10C9A2ACC0C4883B64756E2164756E21
23925
+ //# debugId=74D4BFCB8CE1AB2064756E2164756E21