@tensorix/metahub 0.1.3 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -891,7 +891,7 @@ function observeHlc(db, node, remote, now = Date.now()) {
891
891
  }
892
892
 
893
893
  // src/core/blocks.ts
894
- function parseBlocks(md) {
894
+ function parseDocBlocks(md) {
895
895
  if (!md)
896
896
  return [];
897
897
  const lines = md.replace(/\r\n?/g, `
@@ -901,24 +901,24 @@ function parseBlocks(md) {
901
901
  let cur = [];
902
902
  let fenceChar = null;
903
903
  let fenceLen = 0;
904
- const flushPara = () => {
905
- while (cur.length && cur[cur.length - 1].trim() === "")
906
- cur.pop();
907
- while (cur.length && cur[0].trim() === "")
908
- cur.shift();
909
- if (cur.length)
910
- blocks.push(cur.join(`
911
- `));
912
- cur = [];
904
+ const closeBlock = () => {
905
+ if (cur.length) {
906
+ blocks.push({ text: cur.join(`
907
+ `), blankAfter: 0 });
908
+ cur = [];
909
+ }
910
+ };
911
+ const onBlank = () => {
912
+ closeBlock();
913
+ if (blocks.length)
914
+ blocks[blocks.length - 1].blankAfter++;
913
915
  };
914
916
  for (const raw of lines) {
915
917
  if (fenceChar) {
916
918
  cur.push(raw);
917
919
  const m2 = raw.match(/^\s*(`{3,}|~{3,})\s*$/);
918
920
  if (m2 && m2[1][0] === fenceChar && m2[1].length >= fenceLen) {
919
- blocks.push(cur.join(`
920
- `));
921
- cur = [];
921
+ closeBlock();
922
922
  fenceChar = null;
923
923
  fenceLen = 0;
924
924
  }
@@ -926,28 +926,34 @@ function parseBlocks(md) {
926
926
  }
927
927
  const open = raw.match(/^\s*(`{3,}|~{3,})/);
928
928
  if (open) {
929
- flushPara();
929
+ closeBlock();
930
930
  fenceChar = open[1][0];
931
931
  fenceLen = open[1].length;
932
932
  cur.push(raw);
933
933
  } else if (raw.trim() === "") {
934
- flushPara();
934
+ onBlank();
935
935
  } else {
936
936
  cur.push(raw);
937
937
  }
938
938
  }
939
- if (fenceChar) {
940
- if (cur.length)
941
- blocks.push(cur.join(`
942
- `));
943
- } else {
944
- flushPara();
945
- }
939
+ closeBlock();
940
+ for (let i2 = 0;i2 < blocks.length - 1; i2++)
941
+ blocks[i2].blankAfter = Math.max(0, blocks[i2].blankAfter - 1);
946
942
  return blocks;
947
943
  }
948
- function serializeBlocks(texts) {
949
- return texts.filter((t2) => !!t2 && t2.length > 0).join(`
950
-
944
+ function serializeDocBlocks(blocks) {
945
+ const live = blocks.filter((b2) => !!b2.text && b2.text.length > 0);
946
+ const out = [];
947
+ live.forEach((b2, i2) => {
948
+ out.push(...b2.text.split(`
949
+ `));
950
+ const extra = Math.max(0, b2.blankAfter ?? 0);
951
+ if (i2 < live.length - 1)
952
+ out.push("");
953
+ for (let k2 = 0;k2 < extra; k2++)
954
+ out.push("");
955
+ });
956
+ return out.join(`
951
957
  `);
952
958
  }
953
959
  function reconcile(oldTexts, newTexts) {
@@ -1004,9 +1010,9 @@ function isBlockManaged(db, docId) {
1004
1010
  return db.query("SELECT 1 AS x FROM doc_blocks WHERE doc_id = ? LIMIT 1").get(docId) != null;
1005
1011
  }
1006
1012
  function recomputeDocBody(db, docId) {
1007
- const rows = db.query("SELECT text FROM doc_blocks WHERE doc_id = ? AND __deleted = 0 ORDER BY order_key, id").all(docId);
1013
+ const rows = db.query("SELECT text, blank_after FROM doc_blocks WHERE doc_id = ? AND __deleted = 0 ORDER BY order_key, id").all(docId);
1008
1014
  ensureRow(db, "documents", docId);
1009
- db.query("UPDATE documents SET body = ? WHERE id = ?").run(serializeBlocks(rows.map((r3) => r3.text)), docId);
1015
+ db.query("UPDATE documents SET body = ? WHERE id = ?").run(serializeDocBlocks(rows.map((r3) => ({ text: r3.text, blankAfter: r3.blank_after ?? 0 }))), docId);
1010
1016
  }
1011
1017
  function materialize(db, dataset, rowId, col, valueJson) {
1012
1018
  if (dataset === "records") {
@@ -1096,7 +1102,7 @@ var init_crdt = __esm(() => {
1096
1102
  },
1097
1103
  doc_blocks: {
1098
1104
  table: "doc_blocks",
1099
- cols: new Set(["doc_id", "text", "order_key", "__deleted"])
1105
+ cols: new Set(["doc_id", "text", "order_key", "blank_after", "__deleted"])
1100
1106
  },
1101
1107
  sites: {
1102
1108
  table: "sites",
@@ -1349,9 +1355,14 @@ var require_src = __commonJS((exports, module) => {
1349
1355
  // src/core/sync/webui.ts
1350
1356
  var exports_webui = {};
1351
1357
  __export(exports_webui, {
1358
+ warmWebui: () => warmWebui,
1359
+ setWebuiBundle: () => setWebuiBundle,
1352
1360
  serveWebui: () => serveWebui
1353
1361
  });
1354
1362
  import { fileURLToPath } from "url";
1363
+ function setWebuiBundle(js) {
1364
+ cachedJs = js;
1365
+ }
1355
1366
  async function getJs() {
1356
1367
  if (cachedJs != null)
1357
1368
  return cachedJs;
@@ -1367,6 +1378,11 @@ async function getJs() {
1367
1378
  cachedJs = await res.outputs[0].text();
1368
1379
  return cachedJs;
1369
1380
  }
1381
+ async function warmWebui() {
1382
+ try {
1383
+ await getJs();
1384
+ } catch {}
1385
+ }
1370
1386
  async function serveWebui(req) {
1371
1387
  if (req.method !== "GET")
1372
1388
  return null;
@@ -1393,7 +1409,13 @@ var HTML = `<!doctype html>
1393
1409
  <title>Metahub</title>
1394
1410
  <link rel="preconnect" href="https://fonts.googleapis.com">
1395
1411
  <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">
1412
+ <!-- Load web fonts without blocking first paint: media="print" keeps the
1413
+ stylesheet non-render-blocking, then onload flips it to "all". The page
1414
+ paints instantly with the system-font fallbacks baked into --ui/--mono and
1415
+ swaps to the web fonts when they arrive (or stays on system fonts offline).
1416
+ This is what kept the desktop cold-start white for seconds on slow networks. -->
1417
+ <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">
1418
+ <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
1419
  <script>try{document.documentElement.dataset.theme=localStorage.getItem('mh-theme')||'system'}catch(e){}</script>
1398
1420
  <style>
1399
1421
  :root {
@@ -1407,6 +1429,7 @@ var HTML = `<!doctype html>
1407
1429
  --radius:8px; --radius-sm:6px;
1408
1430
  --ui:"Hanken Grotesk",-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
1409
1431
  --mono:"JetBrains Mono",ui-monospace,SFMono-Regular,monospace;
1432
+ --note-lh:1.6;
1410
1433
  --code-bg:#f8f8f6;
1411
1434
  --hl-cmt:#9a9aa2; --hl-key:#a626a4; --hl-str:#28823f; --hl-num:#b76a00;
1412
1435
  --hl-fn:#3f51d6; --hl-type:#0a7ea4; --hl-var:#c2410c;
@@ -1439,7 +1462,7 @@ var HTML = `<!doctype html>
1439
1462
  * { box-sizing: border-box; }
1440
1463
  html, body { margin: 0; height: 100%; }
1441
1464
  body { font-family: var(--ui); font-size: 14px; line-height: 1.55; color: var(--fg); background: var(--bg);
1442
- overflow: hidden; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
1465
+ overflow: hidden; text-rendering: auto; }
1443
1466
  button { font: inherit; color: inherit; cursor: pointer; background: none; border: 0; padding: 0; }
1444
1467
  input, textarea { font: inherit; color: inherit; }
1445
1468
  .muted { color: var(--muted); }
@@ -1495,6 +1518,8 @@ var HTML = `<!doctype html>
1495
1518
  .dragging { opacity:.35; }
1496
1519
  .sb-footer { flex:none; padding:6px 8px; border-top:1px solid var(--line); }
1497
1520
  .sb-footer .navitem { font-size:13.5px; }
1521
+ .nav-dot { flex:none; width:7px; height:7px; border-radius:50%; background:var(--danger);
1522
+ box-shadow:0 0 0 2px var(--surface); margin-right:2px; }
1498
1523
 
1499
1524
  /* settings page */
1500
1525
  .set-page { max-width:680px; margin:0 auto; padding:40px 28px 64px; }
@@ -1503,6 +1528,20 @@ var HTML = `<!doctype html>
1503
1528
  .set-section { margin-top:34px; }
1504
1529
  .set-section-head { font-size:12px; font-weight:600; text-transform:uppercase; letter-spacing:.07em; color:var(--muted); margin-bottom:4px; }
1505
1530
  .set-section-desc { color:var(--fg-soft); margin-bottom:16px; }
1531
+ .set-footer { margin-top:40px; padding-top:20px; border-top:1px solid var(--line); display:flex;
1532
+ flex-wrap:wrap; justify-content:center; align-items:center; gap:7px; color:var(--muted); font-size:12px; }
1533
+ .set-footer-sep { opacity:.45; }
1534
+ .ver-num { font-family:var(--mono); font-size:11px; color:var(--fg-soft); letter-spacing:-.01em; }
1535
+ .ver-up { display:inline-flex; align-items:center; gap:6px; }
1536
+ .ver-up.err .ver-msg { color:var(--danger); }
1537
+ .ver-act { background:none; border:0; padding:0; margin:0; font:inherit; font-size:12px; line-height:1;
1538
+ color:var(--muted); cursor:pointer; transition:color .12s; }
1539
+ .ver-act:hover { color:var(--accent); }
1540
+ .ver-act.accent { color:var(--accent); font-weight:600; }
1541
+ .ver-act[aria-disabled="true"] { opacity:.6; cursor:default; pointer-events:none; }
1542
+ .ver-dot { width:6px; height:6px; border-radius:50%; background:var(--accent); flex:none; }
1543
+ .ver-dot.pulse { animation:verpulse 1.8s ease-in-out infinite; }
1544
+ @keyframes verpulse { 0%,100% { opacity:1; } 50% { opacity:.4; } }
1506
1545
  .theme-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; }
1507
1546
  .theme-card { position:relative; display:flex; flex-direction:column; align-items:flex-start; gap:10px;
1508
1547
  text-align:left; padding:16px; border:1px solid var(--line-strong); border-radius:var(--radius);
@@ -1540,6 +1579,7 @@ var HTML = `<!doctype html>
1540
1579
  /* main */
1541
1580
  .main { flex:1; display:flex; flex-direction:column; overflow:hidden; min-width:0; }
1542
1581
  .topbar { display:flex; align-items:center; gap:6px; padding:10px 18px; border-bottom:1px solid var(--line); min-height:49px; }
1582
+ .topbar.bare { border-bottom:0; }
1543
1583
  .topbar .hamburger { display:none; }
1544
1584
  .topbar .hamburger.show-collapsed { display:grid; }
1545
1585
  .sidebar.collapsed { overflow:hidden; }
@@ -1594,8 +1634,10 @@ var HTML = `<!doctype html>
1594
1634
  /* The list item's host gutter controls the whole unit, so suppress the nested
1595
1635
  code block's own gutter (it would overlap the list marker on the same row). */
1596
1636
  .block.list-code-host + .block-wrap.nested .gutter { display:none; }
1597
- .editable { outline:none; flex:1; min-width:0; padding:3px 2px; line-height:1.6; }
1637
+ .editable { outline:none; flex:1; min-width:0; padding:3px 2px; line-height:var(--note-lh); }
1598
1638
  .editable:empty::before { content:attr(data-ph); color:var(--muted); pointer-events:none; }
1639
+ /* The generic "/" prompt only shows on the focused line; idle blank lines (paragraphs and list items, any depth) stay blank. */
1640
+ .editable[data-ph-hint="slash"]:empty:not(:focus)::before { content:""; }
1599
1641
  .b-h1 .editable { font-size:28px; font-weight:700; letter-spacing:-.02em; padding-top:12px; }
1600
1642
  .b-h2 .editable { font-size:22px; font-weight:650; letter-spacing:-.01em; padding-top:8px; }
1601
1643
  .b-h3 .editable { font-size:18px; font-weight:600; padding-top:4px; }
@@ -1647,7 +1689,8 @@ var HTML = `<!doctype html>
1647
1689
  .hljs-emphasis { font-style:italic; }
1648
1690
  .hljs-strong { font-weight:700; }
1649
1691
  .marker { width:24px; flex:none; text-align:center; color:var(--fg-soft); user-select:none; padding-top:4px; }
1650
- .b-todo .marker { padding-top:5px; }
1692
+ .b-todo .marker { box-sizing:content-box; padding-top:3px; height:calc(1em * var(--note-lh));
1693
+ display:flex; align-items:center; justify-content:center; }
1651
1694
  .b-todo .marker input { width:15px; height:15px; accent-color:var(--accent); cursor:pointer; }
1652
1695
  .b-divider { padding:10px 0; }
1653
1696
  .b-divider hr { border:0; border-top:1px solid var(--line-strong); margin:0; width:100%; }
@@ -1834,14 +1877,14 @@ var HTML = `<!doctype html>
1834
1877
  .optrow .x:hover { background:var(--hover-2); color:var(--danger); }
1835
1878
 
1836
1879
  /* modal */
1837
- .modal-scrim { position:fixed; inset:0; background:rgba(15,15,25,.4); z-index:100; display:grid; place-items:center;
1880
+ .modal-scrim { position:fixed; inset:0; background:rgba(15,15,25,.4); z-index:100; display:flex; align-items:center; justify-content:center;
1838
1881
  opacity:0; pointer-events:none; transition:opacity .15s; padding:20px; }
1839
1882
  .modal-scrim.open { opacity:1; pointer-events:auto; }
1840
- .modal { background:var(--bg); border-radius:16px; box-shadow:var(--shadow-lg); width:440px; max-width:100%; overflow:hidden; }
1883
+ .modal { background:var(--bg); border-radius:16px; box-shadow:var(--shadow-lg); width:440px; max-width:100%; max-height:100%; display:flex; flex-direction:column; overflow:hidden; }
1841
1884
  .modal-head { padding:20px 22px 0; }
1842
1885
  .modal-head h3 { margin:0; font-size:18px; font-weight:700; letter-spacing:-.01em; }
1843
1886
  .modal-head p { margin:6px 0 0; color:var(--muted); font-size:13.5px; }
1844
- .modal-body { padding:18px 22px; }
1887
+ .modal-body { padding:18px 22px; overflow-y:auto; }
1845
1888
  .modal-foot { display:flex; justify-content:flex-end; gap:8px; padding:14px 22px; background:var(--surface); border-top:1px solid var(--line); }
1846
1889
  .field-label { font-size:12px; font-weight:600; color:var(--fg-soft); margin:12px 0 5px; }
1847
1890
  .field-label:first-child { margin-top:0; }
@@ -1864,7 +1907,71 @@ var HTML = `<!doctype html>
1864
1907
  @keyframes slidein { from { opacity:0; transform:translateX(-12px); } }
1865
1908
 
1866
1909
  /* misc */
1867
- .empty { display:grid; place-items:center; height:100%; color:var(--muted); text-align:center; padding:40px; }
1910
+ .empty { display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%;
1911
+ color:var(--muted); text-align:center; padding:40px; }
1912
+
1913
+ /* ---- empty state: \u77E5\u8BC6\u751F\u957F\u63D2\u753B ---- */
1914
+ .estate-art { width:240px; height:208px; overflow:visible; animation:idleFloat 5s ease-in-out 1.6s infinite; }
1915
+ .estate-art .stroke-a { fill:none; stroke:var(--accent); stroke-width:2.4; stroke-linecap:round; stroke-linejoin:round; }
1916
+ .estate-art .stroke { fill:none; stroke:var(--line-strong); stroke-width:2.4; stroke-linecap:round; stroke-linejoin:round; }
1917
+ .estate-art .fill-a { fill:var(--accent); }
1918
+ .estate-art .fill-soft { fill:var(--accent-soft); }
1919
+ .estate-art .fill-surface { fill:var(--bg); }
1920
+ .estate-art .ground { fill:var(--accent); }
1921
+ .estate-title { margin:28px 0 0; font-size:18px; font-weight:600; letter-spacing:.2px; color:var(--fg);
1922
+ opacity:0; animation:fadeUp .5s ease forwards 1.0s; }
1923
+ .estate-hint { margin:9px 0 0; font-size:13.5px; line-height:1.6; color:var(--muted); max-width:300px;
1924
+ opacity:0; animation:fadeUp .5s ease forwards 1.15s; }
1925
+ .estate-link { margin:18px 0 0; font-size:14px; font-weight:600; color:var(--accent);
1926
+ display:inline-flex; align-items:center; gap:6px; padding:9px 8px; border-radius:var(--radius-sm);
1927
+ opacity:0; animation:fadeUp .5s ease forwards 1.3s; transition:opacity .15s; }
1928
+ .estate-link:hover { opacity:.7; }
1929
+ /* intro */
1930
+ .draw { stroke-dasharray:100; stroke-dashoffset:100; }
1931
+ @keyframes draw { to { stroke-dashoffset:0; } }
1932
+ @keyframes leafGrow { from { opacity:0; transform:scale(.2) rotate(-10deg); } to { opacity:1; transform:scale(1) rotate(0); } }
1933
+ @keyframes cardUp { from { opacity:0; transform:translateY(16px) scale(.96); } to { opacity:1; transform:translateY(0) scale(1); } }
1934
+ @keyframes groundIn { to { opacity:.1; } }
1935
+ @keyframes glowIn { to { opacity:.6; } }
1936
+ @keyframes fadeUp { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:translateY(0); } }
1937
+ /* idle loops \u2014 each 0% equals the settled rest state to avoid a jump */
1938
+ @keyframes idleFloat { 0%,100% { transform:translateY(0); } 50% { transform:translateY(-5px); } }
1939
+ @keyframes plantSway { 0%,100% { transform:rotate(0deg); } 25% { transform:rotate(-1.3deg); } 75% { transform:rotate(1.3deg); } }
1940
+ @keyframes leafSwayA { 0%,100% { transform:rotate(0deg); } 25% { transform:rotate(-2.6deg); } 75% { transform:rotate(2.6deg); } }
1941
+ @keyframes leafSwayB { 0%,100% { transform:rotate(0deg); } 25% { transform:rotate(2.6deg); } 75% { transform:rotate(-2.6deg); } }
1942
+ @keyframes groundBreathe { 0%,100% { transform:scale(1); opacity:.10; } 50% { transform:scale(1.07); opacity:.15; } }
1943
+ @keyframes glowPulse { 0%,100% { transform:scale(1); opacity:.6; } 50% { transform:scale(1.1); opacity:.85; } }
1944
+ @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); } }
1945
+ /* wiring */
1946
+ .kg-ground { transform-box:fill-box; transform-origin:center; opacity:0;
1947
+ animation:groundIn .5s ease forwards .1s, groundBreathe 6s ease-in-out 1.6s infinite; }
1948
+ .kg-glow { filter:blur(9px); transform-box:fill-box; transform-origin:center; opacity:0;
1949
+ animation:glowIn .6s ease forwards .1s, glowPulse 4.6s ease-in-out 1.6s infinite; }
1950
+ .kg-cardB { transform-box:fill-box; transform-origin:center; opacity:0; animation:cardUp .55s cubic-bezier(.2,.8,.2,1) forwards .2s; }
1951
+ .kg-cardA { transform-box:fill-box; transform-origin:center; opacity:0; animation:cardUp .55s cubic-bezier(.2,.8,.2,1) forwards .32s; }
1952
+ .kg-plant { transform-box:fill-box; transform-origin:50% 100%; animation:plantSway 6s ease-in-out 1.7s infinite; }
1953
+ .kg-stem { animation:draw .9s ease-out forwards .4s; }
1954
+ .kg-leaf { transform-box:fill-box; }
1955
+ .kg-grow { transform-box:fill-box; opacity:0; }
1956
+ .kg-leaf.l1 { transform-origin:90% 100%; animation:leafSwayA 4.6s ease-in-out 1.7s infinite; }
1957
+ .kg-leaf.l2 { transform-origin:0% 80%; animation:leafSwayB 5.2s ease-in-out 1.8s infinite; }
1958
+ .kg-leaf.l3 { transform-origin:0% 100%; animation:leafSwayA 5.0s ease-in-out 1.9s infinite; }
1959
+ .kg-leaf.l1 .kg-grow { transform-origin:90% 100%; animation:leafGrow .52s cubic-bezier(.18,.9,.24,1.22) forwards .85s; }
1960
+ .kg-leaf.l2 .kg-grow { transform-origin:0% 80%; animation:leafGrow .52s cubic-bezier(.18,.9,.24,1.22) forwards 1.0s; }
1961
+ .kg-leaf.l3 .kg-grow { transform-origin:0% 100%; animation:leafGrow .52s cubic-bezier(.18,.9,.24,1.22) forwards 1.15s; }
1962
+ .kg-particle { transform-box:fill-box; transform-origin:center; opacity:0; }
1963
+ .kg-particle.p1 { animation:particle 4.5s ease-in-out 1.6s infinite; }
1964
+ .kg-particle.p2 { animation:particle 5.2s ease-in-out 2.0s infinite; }
1965
+ .kg-particle.p3 { animation:particle 4.8s ease-in-out 2.6s infinite; }
1966
+ .kg-particle.p4 { animation:particle 5.6s ease-in-out 3.1s infinite; }
1967
+ .kg-particle.p5 { animation:particle 4.2s ease-in-out 2.3s infinite; }
1968
+ @media (prefers-reduced-motion: reduce) {
1969
+ .estate-art, .estate-art *, .estate-title, .estate-hint, .estate-link { animation:none !important; opacity:1 !important; transform:none !important; }
1970
+ .draw { stroke-dashoffset:0 !important; }
1971
+ .kg-ground { opacity:.1 !important; }
1972
+ .kg-glow { opacity:.6 !important; }
1973
+ .kg-particle { opacity:0 !important; }
1974
+ }
1868
1975
  .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
1976
  .view-placeholder { padding:40px; text-align:center; border:1px dashed var(--line-strong); border-radius:var(--radius); color:var(--muted); margin-top:8px; }
1870
1977
  .ico.flip { transform:scaleX(-1); }
@@ -1996,6 +2103,17 @@ var HTML = `<!doctype html>
1996
2103
  .qn-body .doc { max-width:none; margin:0; padding:14px 22px 30vh; }
1997
2104
  .qn-body .doc-title { font-size:24px; margin-bottom:2px; }
1998
2105
  .qn-body .doc-meta { display:none; }
2106
+ /* A native checkbox paints an opaque white box when unchecked, which breaks
2107
+ the window's vibrancy blur. Draw a custom box so the unchecked background
2108
+ stays transparent; checked falls back to the accent fill + checkmark. */
2109
+ body.quicknote .b-todo .marker input {
2110
+ appearance:none; -webkit-appearance:none; box-sizing:border-box;
2111
+ border:1.5px solid color-mix(in srgb, var(--fg-soft) 55%, transparent);
2112
+ border-radius:4px; background:transparent; }
2113
+ body.quicknote .b-todo .marker input:checked {
2114
+ border-color:var(--accent);
2115
+ background:var(--accent) center/10px no-repeat
2116
+ url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12l5 5L20 7'/%3E%3C/svg%3E"); }
1999
2117
  /* Compact menus inside the quick-note window only (dropdown + slash command
2000
2118
  share .pop/.item/.lico with the main app; scope under body.quicknote so the
2001
2119
  main app keeps its roomier density). */
@@ -3456,8 +3574,9 @@ async function runMain(cmd, opts = {}) {
3456
3574
  // package.json
3457
3575
  var package_default = {
3458
3576
  name: "@tensorix/metahub",
3459
- version: "0.1.3",
3577
+ version: "0.1.6",
3460
3578
  description: "Metahub core library and CLI",
3579
+ license: "AGPL-3.0-only",
3461
3580
  type: "module",
3462
3581
  main: "./dist/index.js",
3463
3582
  module: "./dist/index.js",
@@ -3475,14 +3594,17 @@ var package_default = {
3475
3594
  },
3476
3595
  files: [
3477
3596
  "dist",
3478
- "README.md"
3597
+ "README.md",
3598
+ "README.zh-CN.md",
3599
+ "LICENSE"
3479
3600
  ],
3480
3601
  scripts: {
3481
3602
  dev: "bun --hot src/cli/index.ts",
3482
3603
  build: "bun run scripts/build.ts",
3483
3604
  "build:binaries": "bun run scripts/compile-binaries.ts",
3484
3605
  prepublishOnly: "bun run build",
3485
- test: "bun test"
3606
+ test: "bun test",
3607
+ release: "bun run scripts/tag-release.ts"
3486
3608
  },
3487
3609
  dependencies: {
3488
3610
  "@clack/prompts": "^1.5.0",
@@ -3607,11 +3729,12 @@ CREATE TABLE IF NOT EXISTS documents (
3607
3729
  -- independent CRDT register so concurrent edits to different blocks merge.
3608
3730
  -- order_key is a fractional index (ties broken by id) giving display order.
3609
3731
  CREATE TABLE IF NOT EXISTS doc_blocks (
3610
- id TEXT PRIMARY KEY,
3611
- doc_id TEXT,
3612
- text TEXT,
3613
- order_key TEXT,
3614
- __deleted INTEGER NOT NULL DEFAULT 0
3732
+ id TEXT PRIMARY KEY,
3733
+ doc_id TEXT,
3734
+ text TEXT,
3735
+ order_key TEXT,
3736
+ blank_after INTEGER NOT NULL DEFAULT 0, -- extra blank lines kept after this block (user spacing)
3737
+ __deleted INTEGER NOT NULL DEFAULT 0
3615
3738
  );
3616
3739
  CREATE INDEX IF NOT EXISTS idx_doc_blocks_doc ON doc_blocks(doc_id);
3617
3740
 
@@ -3683,9 +3806,20 @@ function deleteDatabase(db, id) {
3683
3806
  if (!getDatabase(db, id))
3684
3807
  return false;
3685
3808
  emit(db, "databases", id, "__deleted", 1);
3809
+ const tombstone = (table) => {
3810
+ for (const r3 of liveChildren(db, table, "database_id", id))
3811
+ emit(db, table, r3, "__deleted", 1);
3812
+ };
3813
+ tombstone("properties");
3814
+ tombstone("records");
3815
+ for (const d2 of liveChildren(db, "documents", "database_id", id))
3816
+ emit(db, "documents", d2, "database_id", null);
3686
3817
  db.query("DELETE FROM meta WHERE key = 'current_db' AND value = ?").run(id);
3687
3818
  return true;
3688
3819
  }
3820
+ function liveChildren(db, table, col, parentId) {
3821
+ return db.query(`SELECT id FROM ${table} WHERE ${col} = ? AND __deleted = 0`).all(parentId).map((r3) => r3.id);
3822
+ }
3689
3823
 
3690
3824
  // src/core/properties.ts
3691
3825
  init_ids();
@@ -3814,9 +3948,13 @@ function setPropertyWidth(db, id, width) {
3814
3948
  return updateProperty(db, id, { config: { ...cur.config ?? {}, width: w2 } });
3815
3949
  }
3816
3950
  function removeProperty(db, id) {
3817
- if (!getProperty(db, id))
3951
+ const prop = getProperty(db, id);
3952
+ if (!prop)
3818
3953
  return false;
3819
3954
  emit(db, "properties", id, "__deleted", 1);
3955
+ const rows = db.query("SELECT id FROM records WHERE database_id = ? AND __deleted = 0 AND data ->> ? IS NOT NULL").all(prop.database_id, id);
3956
+ for (const r3 of rows)
3957
+ emit(db, "records", r3.id, id, undefined);
3820
3958
  return true;
3821
3959
  }
3822
3960
 
@@ -4284,6 +4422,10 @@ function migratePeers(db) {
4284
4422
  db.exec(`ALTER TABLE peers ADD COLUMN ${col} ${decl}`);
4285
4423
  }
4286
4424
  }
4425
+ function migrateDocBlocks(db) {
4426
+ if (!hasColumn(db, "doc_blocks", "blank_after"))
4427
+ db.exec("ALTER TABLE doc_blocks ADD COLUMN blank_after INTEGER NOT NULL DEFAULT 0");
4428
+ }
4287
4429
  function openMetahub() {
4288
4430
  ensureDirs();
4289
4431
  const db = new Database(dbPath(), { create: true });
@@ -4291,6 +4433,7 @@ function openMetahub() {
4291
4433
  runSchema(db);
4292
4434
  migrateRecords(db);
4293
4435
  migratePeers(db);
4436
+ migrateDocBlocks(db);
4294
4437
  return db;
4295
4438
  }
4296
4439
 
@@ -4462,7 +4605,7 @@ var use_default = defineCommand({
4462
4605
  init_ids();
4463
4606
  init_crdt();
4464
4607
  function liveBlocks(db, docId) {
4465
- return db.query("SELECT id, text, order_key FROM doc_blocks WHERE doc_id = ? AND __deleted = 0 ORDER BY order_key, id").all(docId);
4608
+ return db.query("SELECT id, text, order_key, blank_after FROM doc_blocks WHERE doc_id = ? AND __deleted = 0 ORDER BY order_key, id").all(docId);
4466
4609
  }
4467
4610
  function makeBlockId(text) {
4468
4611
  return newId("blk", text.split(`
@@ -4472,29 +4615,37 @@ function emitBlock(db, docId, fields) {
4472
4615
  const id = makeBlockId(fields.text);
4473
4616
  emit(db, "doc_blocks", id, "doc_id", docId);
4474
4617
  emit(db, "doc_blocks", id, "order_key", fields.order_key);
4618
+ if (fields.blankAfter)
4619
+ emit(db, "doc_blocks", id, "blank_after", fields.blankAfter);
4475
4620
  emit(db, "doc_blocks", id, "text", fields.text);
4476
4621
  }
4477
- function insertBlocks(db, docId, texts, after, before) {
4478
- const keys = keysBetween(after, before, texts.length);
4479
- texts.forEach((text, i2) => emitBlock(db, docId, { text, order_key: keys[i2] }));
4622
+ function insertBlocks(db, docId, blocks, after, before) {
4623
+ const keys = keysBetween(after, before, blocks.length);
4624
+ blocks.forEach((b2, i2) => emitBlock(db, docId, { text: b2.text, order_key: keys[i2], blankAfter: b2.blankAfter }));
4480
4625
  }
4481
4626
  function ensureBlocks(db, docId) {
4482
4627
  if (db.query("SELECT 1 AS x FROM doc_blocks WHERE doc_id = ? LIMIT 1").get(docId))
4483
4628
  return;
4484
4629
  const row = db.query("SELECT body FROM documents WHERE id = ?").get(docId);
4485
- const texts = parseBlocks(row?.body ?? "");
4486
- if (texts.length)
4487
- insertBlocks(db, docId, texts, null, null);
4630
+ const blocks = parseDocBlocks(row?.body ?? "");
4631
+ if (blocks.length)
4632
+ insertBlocks(db, docId, blocks, null, null);
4488
4633
  }
4489
4634
  function reconcileBody(db, docId, body) {
4490
4635
  const old = liveBlocks(db, docId);
4491
- const plan = reconcile(old.map((b2) => b2.text), parseBlocks(body));
4636
+ const next = parseDocBlocks(body);
4637
+ const plan = reconcile(old.map((b2) => b2.text), next.map((b2) => b2.text));
4492
4638
  for (const oi of plan.deleted)
4493
4639
  emit(db, "doc_blocks", old[oi].id, "__deleted", 1);
4494
4640
  const { items } = plan;
4495
4641
  let i2 = 0;
4496
4642
  while (i2 < items.length) {
4497
- if ("keep" in items[i2]) {
4643
+ const it = items[i2];
4644
+ if ("keep" in it) {
4645
+ const oldB = old[it.keep];
4646
+ const want = next[i2].blankAfter;
4647
+ if ((oldB.blank_after ?? 0) !== want)
4648
+ emit(db, "doc_blocks", oldB.id, "blank_after", want);
4498
4649
  i2++;
4499
4650
  continue;
4500
4651
  }
@@ -4503,8 +4654,7 @@ function reconcileBody(db, docId, body) {
4503
4654
  j++;
4504
4655
  const left = i2 > 0 ? old[items[i2 - 1].keep].order_key : null;
4505
4656
  const right = j < items.length ? old[items[j].keep].order_key : null;
4506
- const texts = items.slice(i2, j).map((it) => it.insert);
4507
- insertBlocks(db, docId, texts, left, right);
4657
+ insertBlocks(db, docId, next.slice(i2, j), left, right);
4508
4658
  i2 = j;
4509
4659
  }
4510
4660
  }
@@ -4523,9 +4673,9 @@ function createDocument(db, opts) {
4523
4673
  if (opts.parent_id !== undefined)
4524
4674
  emit(db, "documents", id, "parent_id", opts.parent_id);
4525
4675
  if (opts.body !== undefined) {
4526
- const texts = parseBlocks(opts.body);
4527
- if (texts.length)
4528
- insertBlocks(db, id, texts, null, null);
4676
+ const blocks = parseDocBlocks(opts.body);
4677
+ if (blocks.length)
4678
+ insertBlocks(db, id, blocks, null, null);
4529
4679
  }
4530
4680
  return getDocument(db, id);
4531
4681
  }
@@ -4571,7 +4721,7 @@ function editDocument(db, id, opts) {
4571
4721
  throw new Error("--old must not be empty");
4572
4722
  ensureBlocks(db, id);
4573
4723
  const blocks = liveBlocks(db, id);
4574
- const body = serializeBlocks(blocks.map((b2) => b2.text));
4724
+ const body = serializeDocBlocks(blocks.map((b2) => ({ text: b2.text, blankAfter: b2.blank_after })));
4575
4725
  const count = countOccurrences(body, opts.old);
4576
4726
  if (count === 0)
4577
4727
  throw new Error(`anchor not found: no match for --old`);
@@ -4598,32 +4748,39 @@ function appendDocument(db, id, body) {
4598
4748
  if (!getDocument(db, id))
4599
4749
  throw new Error(`no such document: ${id}`);
4600
4750
  ensureBlocks(db, id);
4601
- const texts = parseBlocks(body);
4602
- if (texts.length) {
4751
+ const next = parseDocBlocks(body);
4752
+ if (next.length) {
4603
4753
  const blocks = liveBlocks(db, id);
4604
4754
  const last = blocks.length ? blocks[blocks.length - 1].order_key : null;
4605
- insertBlocks(db, id, texts, last, null);
4755
+ insertBlocks(db, id, next, last, null);
4606
4756
  }
4607
- return { id, changed: texts.length > 0, replaced: texts.length, version: documentVersion(db, id) };
4757
+ return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
4608
4758
  }
4609
4759
  function prependDocument(db, id, body) {
4610
4760
  if (!getDocument(db, id))
4611
4761
  throw new Error(`no such document: ${id}`);
4612
4762
  ensureBlocks(db, id);
4613
- const texts = parseBlocks(body);
4614
- if (texts.length) {
4763
+ const next = parseDocBlocks(body);
4764
+ if (next.length) {
4615
4765
  const blocks = liveBlocks(db, id);
4616
4766
  const first = blocks.length ? blocks[0].order_key : null;
4617
- insertBlocks(db, id, texts, null, first);
4767
+ insertBlocks(db, id, next, null, first);
4618
4768
  }
4619
- return { id, changed: texts.length > 0, replaced: texts.length, version: documentVersion(db, id) };
4769
+ return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
4620
4770
  }
4621
4771
  function deleteDocument(db, id) {
4622
4772
  if (!getDocument(db, id))
4623
4773
  return false;
4624
4774
  emit(db, "documents", id, "__deleted", 1);
4775
+ for (const b2 of liveChildIds(db, "doc_blocks", "doc_id", id))
4776
+ emit(db, "doc_blocks", b2, "__deleted", 1);
4777
+ for (const c3 of liveChildIds(db, "documents", "parent_id", id))
4778
+ emit(db, "documents", c3, "parent_id", null);
4625
4779
  return true;
4626
4780
  }
4781
+ function liveChildIds(db, table2, col, parentId) {
4782
+ return db.query(`SELECT id FROM ${table2} WHERE ${col} = ? AND __deleted = 0`).all(parentId).map((r3) => r3.id);
4783
+ }
4627
4784
  function countOccurrences(haystack, needle) {
4628
4785
  if (needle === "")
4629
4786
  return 0;
@@ -5148,26 +5305,106 @@ var edit_default = defineCommand({
5148
5305
  });
5149
5306
 
5150
5307
  // src/core/search.ts
5308
+ init_crdt();
5151
5309
  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()) {
5310
+ var SEARCH_INDEX_VERSION = "1";
5311
+ function readMeta(db, key) {
5312
+ return db.query("SELECT value FROM meta WHERE key = ?").get(key)?.value ?? null;
5313
+ }
5314
+ function writeMeta(db, key, value) {
5315
+ db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
5316
+ }
5317
+ function reindexDocument(db, id) {
5318
+ db.query("DELETE FROM search_fts WHERE kind = 'document' AND id = ?").run(id);
5319
+ const d2 = db.query("SELECT id, title, body, database_id FROM documents WHERE id = ? AND __deleted = 0").get(id);
5320
+ if (d2)
5161
5321
  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
5322
+ }
5323
+ function recordBodyRows(db, where, ...args) {
5324
+ return db.query(`SELECT r.id AS id, r.database_id AS database_id, group_concat(je.value, ' ') AS body
5164
5325
  FROM records r, json_each(r.data) je
5165
5326
  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 ?? "");
5327
+ WHERE ${where} AND r.__deleted = 0 AND p.type IN ${TEXT_TYPES}
5328
+ GROUP BY r.id`).all(...args);
5329
+ }
5330
+ function insertRecordRow(db, r3) {
5331
+ db.query("INSERT INTO search_fts (kind, id, database_id, title, body) VALUES ('record', ?, ?, ?, ?)").run(r3.id, r3.database_id, r3.id, r3.body ?? "");
5332
+ }
5333
+ function reindexRecord(db, id) {
5334
+ db.query("DELETE FROM search_fts WHERE kind = 'record' AND id = ?").run(id);
5335
+ const r3 = recordBodyRows(db, "r.id = ?", id)[0];
5336
+ if (r3)
5337
+ insertRecordRow(db, r3);
5338
+ }
5339
+ function reindexDatabaseRecords(db, databaseId) {
5340
+ db.query("DELETE FROM search_fts WHERE kind = 'record' AND database_id = ?").run(databaseId);
5341
+ for (const r3 of recordBodyRows(db, "r.database_id = ?", databaseId))
5342
+ insertRecordRow(db, r3);
5343
+ }
5344
+ function fullRebuild(db) {
5345
+ db.query("DELETE FROM search_fts").run();
5346
+ for (const d2 of db.query("SELECT id, title, body, database_id FROM documents WHERE __deleted = 0").all())
5347
+ db.query("INSERT INTO search_fts (kind, id, database_id, title, body) VALUES ('document', ?, ?, ?, ?)").run(d2.id, d2.database_id, d2.title ?? "", d2.body ?? "");
5348
+ for (const r3 of recordBodyRows(db, "1 = 1"))
5349
+ insertRecordRow(db, r3);
5350
+ const max = db.query("SELECT MAX(rowid) AS m FROM crdt_changes").get().m ?? 0;
5351
+ writeMeta(db, "search_seq", String(max));
5352
+ writeMeta(db, "search_index_version", SEARCH_INDEX_VERSION);
5353
+ }
5354
+ function incrementalUpdate(db) {
5355
+ const cursor = Number(readMeta(db, "search_seq") ?? "0");
5356
+ const { changes, cursor: next } = changesAfterSeq(db, cursor);
5357
+ if (changes.length === 0)
5358
+ return;
5359
+ const docIds = new Set;
5360
+ const recordIds = new Set;
5361
+ const dbIds = new Set;
5362
+ for (const c3 of changes) {
5363
+ switch (c3.dataset) {
5364
+ case "documents":
5365
+ docIds.add(c3.row_id);
5366
+ break;
5367
+ case "doc_blocks": {
5368
+ const blk = db.query("SELECT doc_id FROM doc_blocks WHERE id = ?").get(c3.row_id);
5369
+ if (blk?.doc_id)
5370
+ docIds.add(blk.doc_id);
5371
+ break;
5372
+ }
5373
+ case "records":
5374
+ recordIds.add(c3.row_id);
5375
+ break;
5376
+ case "properties":
5377
+ if (c3.col === "type" || c3.col === "__deleted" || c3.col === "database_id") {
5378
+ const p = db.query("SELECT database_id FROM properties WHERE id = ?").get(c3.row_id);
5379
+ if (p?.database_id)
5380
+ dbIds.add(p.database_id);
5381
+ }
5382
+ break;
5383
+ }
5384
+ }
5385
+ for (const id of docIds)
5386
+ reindexDocument(db, id);
5387
+ for (const dbId of dbIds)
5388
+ reindexDatabaseRecords(db, dbId);
5389
+ for (const id of recordIds) {
5390
+ const r3 = db.query("SELECT database_id FROM records WHERE id = ?").get(id);
5391
+ if (r3?.database_id && dbIds.has(r3.database_id))
5392
+ continue;
5393
+ reindexRecord(db, id);
5169
5394
  }
5170
- db.query("INSERT INTO meta (key, value) VALUES ('search_hlc', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(max);
5395
+ writeMeta(db, "search_seq", String(next));
5396
+ }
5397
+ function ensureIndex(db) {
5398
+ if (!ftsAvailable(db))
5399
+ return false;
5400
+ const version = readMeta(db, "search_index_version");
5401
+ const seq = readMeta(db, "search_seq");
5402
+ db.transaction(() => {
5403
+ if (version !== SEARCH_INDEX_VERSION || seq === null)
5404
+ fullRebuild(db);
5405
+ else
5406
+ incrementalUpdate(db);
5407
+ })();
5171
5408
  return true;
5172
5409
  }
5173
5410
  function ftsSearch(db, query, limit) {
@@ -20166,11 +20403,262 @@ var sync_default = defineCommand({
20166
20403
  init_crdt();
20167
20404
  import { readdirSync, existsSync } from "fs";
20168
20405
  import { join as join5 } from "path";
20406
+
20407
+ // src/core/integrity.ts
20408
+ init_crdt();
20409
+ var REF_RULES = [
20410
+ { srcTable: "properties", fk: "database_id", tgtTable: "databases", fix: "tombstone-src" },
20411
+ { srcTable: "records", fk: "database_id", tgtTable: "databases", fix: "tombstone-src" },
20412
+ { srcTable: "documents", fk: "database_id", tgtTable: "databases", fix: "null-fk" },
20413
+ { srcTable: "documents", fk: "parent_id", tgtTable: "documents", fix: "null-fk" },
20414
+ { srcTable: "doc_blocks", fk: "doc_id", tgtTable: "documents", fix: "tombstone-src" },
20415
+ { srcTable: "site_files", fk: "site_id", tgtTable: "sites", fix: "tombstone-src" }
20416
+ ];
20417
+ function brokenRefRows(db, r3) {
20418
+ return db.query(`SELECT s.id AS id, s.${r3.fk} AS ref FROM ${r3.srcTable} s
20419
+ JOIN ${r3.tgtTable} t ON t.id = s.${r3.fk} AND t.__deleted = 1
20420
+ WHERE s.__deleted = 0 AND s.${r3.fk} IS NOT NULL
20421
+ ORDER BY s.id`).all();
20422
+ }
20423
+ function detectBrokenRefs(db) {
20424
+ const out = [];
20425
+ for (const r3 of REF_RULES) {
20426
+ for (const row of brokenRefRows(db, r3)) {
20427
+ out.push({
20428
+ category: "broken_ref",
20429
+ entity: r3.srcTable,
20430
+ id: row.id,
20431
+ detail: `${r3.fk} \u2192 deleted ${r3.tgtTable} ${row.ref} (${r3.fix === "tombstone-src" ? "tombstone" : `clear ${r3.fk}`})`,
20432
+ fixable: true
20433
+ });
20434
+ }
20435
+ }
20436
+ return out;
20437
+ }
20438
+ function repairBrokenRefs(db) {
20439
+ let n2 = 0;
20440
+ for (const r3 of REF_RULES) {
20441
+ for (const row of brokenRefRows(db, r3)) {
20442
+ if (r3.fix === "tombstone-src")
20443
+ emit(db, r3.srcTable, row.id, "__deleted", 1);
20444
+ else
20445
+ emit(db, r3.srcTable, row.id, r3.fk, null);
20446
+ n2++;
20447
+ }
20448
+ }
20449
+ return n2;
20450
+ }
20451
+ function orphanCellRows(db) {
20452
+ return db.query(`SELECT r.id AS rec, j.key AS prop
20453
+ FROM records r, json_each(coalesce(r.data, '{}')) j
20454
+ JOIN properties p ON p.id = j.key AND p.__deleted = 1
20455
+ WHERE r.__deleted = 0
20456
+ ORDER BY r.id, j.key`).all();
20457
+ }
20458
+ function detectOrphanCells(db) {
20459
+ return orphanCellRows(db).map((row) => ({
20460
+ category: "orphan_cell",
20461
+ entity: "records",
20462
+ id: row.rec,
20463
+ detail: `cell for deleted property ${row.prop}`,
20464
+ fixable: true
20465
+ }));
20466
+ }
20467
+ function repairOrphanCells(db) {
20468
+ const rows = orphanCellRows(db);
20469
+ for (const row of rows)
20470
+ emit(db, "records", row.rec, row.prop, undefined);
20471
+ return rows.length;
20472
+ }
20473
+ function dupPathLosers(db) {
20474
+ const rows = db.query(`SELECT id, site_id, path FROM site_files
20475
+ WHERE __deleted = 0 AND site_id IS NOT NULL AND path IS NOT NULL
20476
+ ORDER BY site_id, path, created_hlc, id`).all();
20477
+ const losers = [];
20478
+ let group = null;
20479
+ for (const r3 of rows) {
20480
+ const key = `${r3.site_id}\x00${r3.path}`;
20481
+ if (key === group)
20482
+ losers.push({ id: r3.id });
20483
+ else
20484
+ group = key;
20485
+ }
20486
+ return losers;
20487
+ }
20488
+ function detectDupPaths(db) {
20489
+ return dupPathLosers(db).map((row) => ({
20490
+ category: "dup_path",
20491
+ entity: "site_files",
20492
+ id: row.id,
20493
+ detail: "redundant duplicate (site_id, path); loser tombstoned",
20494
+ fixable: true
20495
+ }));
20496
+ }
20497
+ function repairDupPaths(db) {
20498
+ const losers = dupPathLosers(db);
20499
+ for (const row of losers)
20500
+ emit(db, "site_files", row.id, "__deleted", 1);
20501
+ return losers.length;
20502
+ }
20503
+ function detectParentCycles(db) {
20504
+ const rows = db.query(`SELECT d.id AS id, d.parent_id AS parent, d.created_hlc AS hlc
20505
+ FROM documents d
20506
+ WHERE d.__deleted = 0 AND d.parent_id IS NOT NULL
20507
+ AND EXISTS (SELECT 1 FROM documents p WHERE p.id = d.parent_id AND p.__deleted = 0)
20508
+ ORDER BY d.id`).all();
20509
+ const parent = new Map(rows.map((r3) => [r3.id, r3.parent]));
20510
+ const hlc = new Map(rows.map((r3) => [r3.id, r3.hlc]));
20511
+ const seen = new Set;
20512
+ const cycles = [];
20513
+ for (const start of parent.keys()) {
20514
+ if (seen.has(start))
20515
+ continue;
20516
+ const path = [];
20517
+ const pos = new Map;
20518
+ let cur = start;
20519
+ while (cur != null && parent.has(cur) && !seen.has(cur)) {
20520
+ if (pos.has(cur)) {
20521
+ cycles.push(path.slice(pos.get(cur)));
20522
+ break;
20523
+ }
20524
+ pos.set(cur, path.length);
20525
+ path.push(cur);
20526
+ cur = parent.get(cur);
20527
+ }
20528
+ for (const n2 of path)
20529
+ seen.add(n2);
20530
+ }
20531
+ return { cycles, hlc };
20532
+ }
20533
+ function geHlcId(hlc, a2, b2) {
20534
+ const ha = hlc.get(a2) ?? "";
20535
+ const hb = hlc.get(b2) ?? "";
20536
+ if (ha !== hb)
20537
+ return ha > hb;
20538
+ return a2 >= b2;
20539
+ }
20540
+ function detectCycleIssues(db) {
20541
+ const { cycles } = detectParentCycles(db);
20542
+ return cycles.map((cyc) => ({
20543
+ category: "parent_cycle",
20544
+ entity: "documents",
20545
+ id: [...cyc].sort()[0],
20546
+ detail: `parent cycle: ${[...cyc].sort().join(" \u2192 ")}`,
20547
+ fixable: true
20548
+ }));
20549
+ }
20550
+ function repairParentCycles(db) {
20551
+ const { cycles, hlc } = detectParentCycles(db);
20552
+ for (const cyc of cycles) {
20553
+ const brk = cyc.reduce((a2, b2) => geHlcId(hlc, a2, b2) ? a2 : b2);
20554
+ emit(db, "documents", brk, "parent_id", null);
20555
+ }
20556
+ return cycles.length;
20557
+ }
20558
+ function detectDupNames(db) {
20559
+ const out = [];
20560
+ const props = db.query(`SELECT database_id AS db, lower(name) AS n, count(*) AS c, group_concat(id) AS ids
20561
+ FROM properties
20562
+ WHERE __deleted = 0 AND name IS NOT NULL AND database_id IS NOT NULL
20563
+ GROUP BY database_id, lower(name) HAVING c > 1
20564
+ ORDER BY database_id, n`).all();
20565
+ for (const r3 of props)
20566
+ out.push({
20567
+ category: "dup_name",
20568
+ entity: "properties",
20569
+ id: r3.ids.split(",").sort()[0],
20570
+ detail: `${r3.c} properties named "${r3.n}" in database ${r3.db}: ${r3.ids}`,
20571
+ fixable: false
20572
+ });
20573
+ const dbs = db.query(`SELECT lower(name) AS n, count(*) AS c, group_concat(id) AS ids
20574
+ FROM databases WHERE __deleted = 0 AND name IS NOT NULL
20575
+ GROUP BY lower(name) HAVING c > 1 ORDER BY n`).all();
20576
+ for (const r3 of dbs)
20577
+ out.push({
20578
+ category: "dup_name",
20579
+ entity: "databases",
20580
+ id: r3.ids.split(",").sort()[0],
20581
+ detail: `${r3.c} databases named "${r3.n}": ${r3.ids}`,
20582
+ fixable: false
20583
+ });
20584
+ return out;
20585
+ }
20586
+ function configIssue(type, config2) {
20587
+ if (!PROP_TYPES.has(type))
20588
+ return `unknown property type: ${type}`;
20589
+ if (type === "select" || type === "multi_select") {
20590
+ const opts = config2?.options;
20591
+ if (!Array.isArray(opts) || opts.length === 0 || !opts.every((o3) => typeof o3 === "string"))
20592
+ return `${type} missing config.options: string[]`;
20593
+ }
20594
+ if (type === "relation" && typeof config2?.database !== "string")
20595
+ return "relation missing config.database";
20596
+ return null;
20597
+ }
20598
+ function detectBadConfig(db) {
20599
+ const rows = db.query("SELECT id, type, config FROM properties WHERE __deleted = 0 ORDER BY id").all();
20600
+ const out = [];
20601
+ for (const r3 of rows) {
20602
+ let config2 = null;
20603
+ try {
20604
+ config2 = r3.config ? JSON.parse(r3.config) : null;
20605
+ } catch {
20606
+ out.push({ category: "bad_config", entity: "properties", id: r3.id, detail: "config is not valid JSON", fixable: false });
20607
+ continue;
20608
+ }
20609
+ const problem = configIssue(r3.type, config2);
20610
+ if (problem)
20611
+ out.push({ category: "bad_config", entity: "properties", id: r3.id, detail: problem, fixable: false });
20612
+ }
20613
+ return out;
20614
+ }
20615
+ function validateHub(db) {
20616
+ const issues = [
20617
+ ...detectBrokenRefs(db),
20618
+ ...detectOrphanCells(db),
20619
+ ...detectDupPaths(db),
20620
+ ...detectCycleIssues(db),
20621
+ ...detectDupNames(db),
20622
+ ...detectBadConfig(db)
20623
+ ];
20624
+ const counts = {};
20625
+ for (const i2 of issues)
20626
+ counts[i2.category] = (counts[i2.category] ?? 0) + 1;
20627
+ return { ok: issues.length === 0, total: issues.length, counts, issues };
20628
+ }
20629
+ function repairPass(db) {
20630
+ const fixed = {};
20631
+ const bump = (cat, n2) => {
20632
+ if (n2)
20633
+ fixed[cat] = (fixed[cat] ?? 0) + n2;
20634
+ };
20635
+ bump("broken_ref", repairBrokenRefs(db));
20636
+ bump("orphan_cell", repairOrphanCells(db));
20637
+ bump("dup_path", repairDupPaths(db));
20638
+ bump("parent_cycle", repairParentCycles(db));
20639
+ const applied = Object.values(fixed).reduce((a2, b2) => a2 + b2, 0);
20640
+ return { applied, fixed };
20641
+ }
20642
+ function repairHub(db) {
20643
+ const fixed = {};
20644
+ let applied = 0;
20645
+ for (let i2 = 0;i2 < 20; i2++) {
20646
+ const pass = repairPass(db);
20647
+ if (pass.applied === 0)
20648
+ break;
20649
+ applied += pass.applied;
20650
+ for (const [cat, n2] of Object.entries(pass.fixed))
20651
+ fixed[cat] = (fixed[cat] ?? 0) + n2;
20652
+ }
20653
+ return { applied, fixed, remaining: validateHub(db) };
20654
+ }
20655
+
20656
+ // src/core/snapshot.ts
20169
20657
  init_paths();
20170
20658
  init_cache();
20171
20659
  var SNAPSHOT_FORMAT = "metahub-snapshot";
20172
20660
  var SNAPSHOT_VERSION = 1;
20173
- function readMeta(db, key) {
20661
+ function readMeta2(db, key) {
20174
20662
  const row = db.query("SELECT value FROM meta WHERE key = ?").get(key);
20175
20663
  return row?.value ?? null;
20176
20664
  }
@@ -20187,8 +20675,8 @@ async function readBlobs() {
20187
20675
  }
20188
20676
  async function createSnapshot(db) {
20189
20677
  const changes = changesAfterSeq(db, 0).changes;
20190
- const node_id = readMeta(db, "node_id");
20191
- const hlc = readMeta(db, "hlc");
20678
+ const node_id = readMeta2(db, "node_id");
20679
+ const hlc = readMeta2(db, "hlc");
20192
20680
  const peers = db.query("SELECT url, pull_cursor, push_cursor FROM peers").all();
20193
20681
  const blobs = await readBlobs();
20194
20682
  return {
@@ -20249,8 +20737,9 @@ async function restoreSnapshot(db, pkg, opts = {}) {
20249
20737
  if (!opts.reset) {
20250
20738
  const applied2 = ingest(db, pkg.changes);
20251
20739
  rebuildDeclaredIndexes(db);
20740
+ const repaired2 = repairHub(db).applied;
20252
20741
  const blobs2 = await writeBlobs(pkg.blobs);
20253
- return { mode: "merge", applied: applied2, blobs: blobs2 };
20742
+ return { mode: "merge", applied: applied2, blobs: blobs2, repaired: repaired2 };
20254
20743
  }
20255
20744
  if (!opts.force)
20256
20745
  throw new Error("refusing to replace local data without --force (a safety snapshot is saved first)");
@@ -20268,7 +20757,7 @@ async function restoreSnapshot(db, pkg, opts = {}) {
20268
20757
  "peers"
20269
20758
  ])
20270
20759
  db.query(`DELETE FROM ${t2}`).run();
20271
- db.query("DELETE FROM meta WHERE key = 'search_hlc'").run();
20760
+ db.query("DELETE FROM meta WHERE key IN ('search_seq', 'search_index_version')").run();
20272
20761
  if (ftsAvailable(db))
20273
20762
  db.query("DELETE FROM search_fts").run();
20274
20763
  const setMeta2 = db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value");
@@ -20282,8 +20771,9 @@ async function restoreSnapshot(db, pkg, opts = {}) {
20282
20771
  });
20283
20772
  tx();
20284
20773
  rebuildDeclaredIndexes(db);
20774
+ const repaired = repairHub(db).applied;
20285
20775
  const blobs = await writeBlobs(pkg.blobs);
20286
- return { mode: "reset", applied, blobs, safetyPath };
20776
+ return { mode: "reset", applied, blobs, repaired, safetyPath };
20287
20777
  }
20288
20778
 
20289
20779
  // src/cli/commands/snapshot.ts
@@ -20332,10 +20822,78 @@ var restore_default = defineCommand({
20332
20822
  reset: !!args.reset,
20333
20823
  force: !!args.force
20334
20824
  });
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`);
20825
+ 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
20826
  })
20337
20827
  });
20338
20828
 
20829
+ // src/cli/commands/doctor.ts
20830
+ var doctor_default = defineCommand({
20831
+ meta: {
20832
+ name: "doctor",
20833
+ description: "Scan the hub for logical-integrity issues (read-only; run `mh repair` to fix)"
20834
+ },
20835
+ run: () => {
20836
+ const report = validateHub(openMetahub());
20837
+ print(report, () => {
20838
+ if (report.ok)
20839
+ return "No integrity issues found.";
20840
+ const head = `${report.total} issue(s): ${Object.entries(report.counts).map(([k2, v2]) => `${k2}=${v2}`).join(", ")}`;
20841
+ const rows = report.issues.map((i2) => ({
20842
+ category: i2.category,
20843
+ entity: i2.entity,
20844
+ id: i2.id,
20845
+ fix: i2.fixable ? "auto" : "report",
20846
+ detail: i2.detail
20847
+ }));
20848
+ return `${head}
20849
+
20850
+ ${table(rows)}`;
20851
+ });
20852
+ }
20853
+ });
20854
+
20855
+ // src/cli/commands/repair.ts
20856
+ var repair_default = defineCommand({
20857
+ meta: {
20858
+ name: "repair",
20859
+ description: "Deterministically fix logical-integrity issues (idempotent; replicates over sync)"
20860
+ },
20861
+ args: {
20862
+ "dry-run": {
20863
+ type: "boolean",
20864
+ description: "Report what would be fixed without changing anything (same as `mh doctor`)"
20865
+ }
20866
+ },
20867
+ run: ({ args }) => {
20868
+ const db = openMetahub();
20869
+ if (args["dry-run"]) {
20870
+ const report = validateHub(db);
20871
+ const fixable = report.issues.filter((i2) => i2.fixable);
20872
+ 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.
20873
+
20874
+ ` + table(report.issues.map((i2) => ({
20875
+ category: i2.category,
20876
+ id: i2.id,
20877
+ fix: i2.fixable ? "auto" : "report",
20878
+ detail: i2.detail
20879
+ }))));
20880
+ return;
20881
+ }
20882
+ const result = repairHub(db);
20883
+ print(result, () => {
20884
+ const fixed = Object.entries(result.fixed).map(([k2, v2]) => `${k2}=${v2}`).join(", ");
20885
+ const head = result.applied === 0 ? "No fixable issues." : `Applied ${result.applied} repair(s): ${fixed}`;
20886
+ const left = result.remaining.issues;
20887
+ if (left.length === 0)
20888
+ return head;
20889
+ return `${head}
20890
+ ${left.length} report-only issue(s) need manual attention:
20891
+
20892
+ ` + table(left.map((i2) => ({ category: i2.category, id: i2.id, detail: i2.detail })));
20893
+ });
20894
+ }
20895
+ });
20896
+
20339
20897
  // src/cli/commands/config.ts
20340
20898
  init_node();
20341
20899
 
@@ -22251,7 +22809,15 @@ function handle(fn) {
22251
22809
  }
22252
22810
  };
22253
22811
  }
22812
+ var VersionSchema = exports_external.object({ version: exports_external.string() });
22254
22813
  var webuiRoutes = [
22814
+ {
22815
+ method: "GET",
22816
+ path: "/api/version",
22817
+ summary: "Version of the running core (sidecar). Used by the desktop app's settings footer.",
22818
+ response: VersionSchema,
22819
+ handler: () => Response.json({ version: package_default.version })
22820
+ },
22255
22821
  {
22256
22822
  method: "GET",
22257
22823
  path: "/api/databases",
@@ -22875,6 +23441,16 @@ function unauthorized() {
22875
23441
  }
22876
23442
 
22877
23443
  // src/core/sync/server.ts
23444
+ class PortInUseError extends Error {
23445
+ port;
23446
+ host;
23447
+ constructor(port, host) {
23448
+ 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>.`);
23449
+ this.port = port;
23450
+ this.host = host;
23451
+ this.name = "PortInUseError";
23452
+ }
23453
+ }
22878
23454
  function scalarHtml(specUrl) {
22879
23455
  return `<!doctype html><html><head>
22880
23456
  <meta charset="utf-8"><title>Metahub API</title>
@@ -22899,48 +23475,60 @@ function startServer(opts = {}) {
22899
23475
  ttlMs: DEFAULT_TTL_MS,
22900
23476
  graceMs: DEFAULT_GRACE_MS
22901
23477
  };
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();
23478
+ let server;
23479
+ try {
23480
+ server = Bun.serve({
23481
+ port,
23482
+ hostname: host,
23483
+ async fetch(req) {
23484
+ const url2 = new URL(req.url);
23485
+ if (url2.pathname === SYNC_PATH) {
23486
+ if (!acceptsSyncToken(req, url2, auth, db))
23487
+ return unauthorized();
23488
+ } else {
23489
+ const exempt = url2.pathname === HEALTH_PATH || url2.pathname === RENEW_PATH || url2.pathname === PAIR_PATH;
23490
+ if (!exempt && !hasValidToken(req, url2, auth)) {
23491
+ return wantsHtml(req) ? new Response(unlockPage(), { headers: HTML_HEADERS }) : unauthorized();
23492
+ }
22914
23493
  }
23494
+ if (url2.pathname === RENEW_PATH) {
23495
+ const r3 = renewToken(req, url2, auth);
23496
+ return r3 ? Response.json(r3) : unauthorized();
23497
+ }
23498
+ if (req.method === "GET" && url2.pathname === "/docs.json") {
23499
+ return Response.json(buildOpenApi(package_default.version));
23500
+ }
23501
+ if (req.method === "GET" && url2.pathname === "/docs") {
23502
+ return withShim(new Response(scalarHtml("/docs.json"), { headers: HTML_HEADERS }), auth);
23503
+ }
23504
+ if (req.method === "GET" && (url2.pathname === "/" || url2.pathname === "/webui.js")) {
23505
+ const { serveWebui: serveWebui2 } = await Promise.resolve().then(() => (init_webui(), exports_webui));
23506
+ const res = await serveWebui2(req);
23507
+ if (res)
23508
+ return withShim(res, auth);
23509
+ }
23510
+ if (req.method === "GET" && url2.pathname.startsWith("/sites/")) {
23511
+ const { serveSite: serveSite2 } = await Promise.resolve().then(() => (init_sites_serve(), exports_sites_serve));
23512
+ const res = await serveSite2(req, ctx);
23513
+ if (res)
23514
+ return withShim(res, auth);
23515
+ }
23516
+ const route = routes.find((r3) => r3.method === req.method && r3.path === url2.pathname);
23517
+ if (route)
23518
+ return route.handler(req, ctx);
23519
+ return new Response("not found", { status: 404 });
22915
23520
  }
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 });
23521
+ });
23522
+ } catch (e3) {
23523
+ const code = e3?.code;
23524
+ if (code === "EADDRINUSE") {
23525
+ throw new PortInUseError(port, host);
22942
23526
  }
22943
- });
23527
+ if (code === "EACCES") {
23528
+ throw new Error(`permission denied binding ${host}:${port} \u2014 ports below 1024 need elevated privileges; pick a higher port with --port`);
23529
+ }
23530
+ throw e3;
23531
+ }
22944
23532
  let timer = null;
22945
23533
  if (autoSync && syncIntervalMs > 0) {
22946
23534
  timer = setInterval(() => {
@@ -22949,12 +23537,17 @@ function startServer(opts = {}) {
22949
23537
  timer.unref?.();
22950
23538
  }
22951
23539
  const active = activeToken(auth);
23540
+ const authMode = opts.debug ? "disabled" : opts.token ? "static" : "managed";
22952
23541
  return {
22953
23542
  server,
22954
23543
  node,
22955
23544
  port: server.port ?? port,
23545
+ host,
22956
23546
  token: active?.token ?? null,
22957
23547
  exp: active?.exp ?? null,
23548
+ authMode,
23549
+ autoSync: Boolean(autoSync && syncIntervalMs > 0),
23550
+ syncIntervalMs,
22958
23551
  stop() {
22959
23552
  if (timer)
22960
23553
  clearInterval(timer);
@@ -22963,6 +23556,129 @@ function startServer(opts = {}) {
22963
23556
  };
22964
23557
  }
22965
23558
 
23559
+ // src/cli/banner.ts
23560
+ var ANSI = /\x1b\[[0-9;]*m/g;
23561
+ function supportsColor() {
23562
+ return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
23563
+ }
23564
+ function visibleWidth(s2) {
23565
+ return s2.replace(ANSI, "").length;
23566
+ }
23567
+ var CODES = {
23568
+ reset: "\x1B[0m",
23569
+ dim: "\x1B[2m",
23570
+ bold: "\x1B[1m",
23571
+ green: "\x1B[32m",
23572
+ cyan: "\x1B[36m",
23573
+ yellow: "\x1B[33m",
23574
+ magenta: "\x1B[35m",
23575
+ red: "\x1B[31m"
23576
+ };
23577
+ function makePalette(on) {
23578
+ const wrap = (code) => (s2) => on ? `${code}${s2}${CODES.reset}` : s2;
23579
+ return {
23580
+ reset: (s2) => s2,
23581
+ dim: wrap(CODES.dim),
23582
+ bold: wrap(CODES.bold),
23583
+ green: wrap(CODES.green),
23584
+ cyan: wrap(CODES.cyan),
23585
+ yellow: wrap(CODES.yellow),
23586
+ magenta: wrap(CODES.magenta),
23587
+ red: wrap(CODES.red)
23588
+ };
23589
+ }
23590
+ var MONTHS = [
23591
+ "Jan",
23592
+ "Feb",
23593
+ "Mar",
23594
+ "Apr",
23595
+ "May",
23596
+ "Jun",
23597
+ "Jul",
23598
+ "Aug",
23599
+ "Sep",
23600
+ "Oct",
23601
+ "Nov",
23602
+ "Dec"
23603
+ ];
23604
+ function humanizeExpiry(exp, now = Date.now()) {
23605
+ if (exp == null)
23606
+ return "";
23607
+ if (!Number.isFinite(exp))
23608
+ return "never expires";
23609
+ const ms = exp - now;
23610
+ if (ms <= 0)
23611
+ return "expired";
23612
+ const d2 = new Date(exp);
23613
+ const date5 = `${MONTHS[d2.getMonth()]} ${d2.getDate()}`;
23614
+ const mins = Math.round(ms / 60000);
23615
+ let rel;
23616
+ if (mins < 60)
23617
+ rel = `${mins}m`;
23618
+ else if (mins < 60 * 24)
23619
+ rel = `${Math.round(mins / 60)}h`;
23620
+ else
23621
+ rel = `${Math.round(mins / (60 * 24))} days`;
23622
+ return `expires in ${rel} \xB7 ${date5}`;
23623
+ }
23624
+ function formatSync(autoSync, ms) {
23625
+ if (!autoSync || ms <= 0)
23626
+ return "auto-sync off";
23627
+ const secs2 = Math.round(ms / 1000);
23628
+ const human = secs2 % 3600 === 0 ? `${secs2 / 3600}h` : secs2 % 60 === 0 ? `${secs2 / 60}m` : `${secs2}s`;
23629
+ return `every ${human}`;
23630
+ }
23631
+ function isLoopback(host) {
23632
+ return host === "127.0.0.1" || host === "::1" || host === "localhost" || host === "::";
23633
+ }
23634
+ function renderStartupBanner(info) {
23635
+ const on = info.color ?? supportsColor();
23636
+ const c3 = makePalette(on);
23637
+ const LABEL_W = 7;
23638
+ const label = (t2) => c3.dim(t2.padEnd(LABEL_W));
23639
+ const rows = [];
23640
+ const push = (plain, painted) => rows.push([plain, painted]);
23641
+ push(`\u25CF listening ${info.host}:${info.port}`, `${c3.green("\u25CF")} ${c3.dim("listening")} ${c3.bold(`${info.host}:${info.port}`)}`);
23642
+ if (!isLoopback(info.host)) {
23643
+ push("\u26A0 reachable on your network", c3.yellow("\u26A0 reachable on your network"));
23644
+ }
23645
+ push("", "");
23646
+ push(`${"WebUI".padEnd(LABEL_W)}${info.webuiUrl}`, `${label("WebUI")}${c3.cyan(info.webuiUrl)}`);
23647
+ push(`${"Docs".padEnd(LABEL_W)}${info.docsUrl}`, `${label("Docs")}${c3.cyan(info.docsUrl)}`);
23648
+ if (info.authMode === "disabled" || !info.token) {
23649
+ push(`${"Auth".padEnd(LABEL_W)}disabled (--debug)`, `${label("Auth")}${c3.red("disabled")} ${c3.dim("(--debug)")}`);
23650
+ } else {
23651
+ push(`${"Token".padEnd(LABEL_W)}${info.token}`, `${label("Token")}${c3.yellow(info.token)}`);
23652
+ const exp = humanizeExpiry(info.exp);
23653
+ if (exp)
23654
+ push(`${" ".repeat(LABEL_W)}${exp}`, `${" ".repeat(LABEL_W)}${c3.dim(exp)}`);
23655
+ }
23656
+ const sync = formatSync(info.autoSync, info.syncIntervalMs);
23657
+ const syncLabel = info.autoSync && info.syncIntervalMs > 0 ? `sync ${sync}` : sync;
23658
+ push(`${"Node".padEnd(LABEL_W)}${info.node} \xB7 ${syncLabel}`, `${label("Node")}${info.node} ${c3.dim("\xB7")} ${c3.dim(syncLabel)}`);
23659
+ const PAD = 2;
23660
+ const title = "metahub";
23661
+ const ver = `v${info.version}`;
23662
+ const footer = "Ctrl-C to stop";
23663
+ const contentW = Math.max(...rows.map(([plain]) => plain.length), title.length + ver.length + 6, footer.length + 4);
23664
+ const innerW = contentW + PAD * 2;
23665
+ const totalW = innerW + 2;
23666
+ const border = (lc, left, right, rc) => {
23667
+ const leftSeg = left ? `\u2500 ${left} ` : "\u2500";
23668
+ const rightSeg = right ? ` ${right} \u2500` : "\u2500";
23669
+ const dashes = totalW - 2 - visibleWidth(leftSeg) - visibleWidth(rightSeg);
23670
+ return `${lc}${leftSeg}${"\u2500".repeat(Math.max(0, dashes))}${rightSeg}${rc}`;
23671
+ };
23672
+ const top = border("\u256D", c3.bold(c3.magenta(title)), c3.dim(ver), "\u256E");
23673
+ const bottom = border("\u2570", c3.dim(footer), "", "\u256F");
23674
+ const body = rows.map(([plain, painted]) => {
23675
+ const fill = innerW - PAD * 2 - plain.length;
23676
+ return `\u2502${" ".repeat(PAD)}${painted}${" ".repeat(Math.max(0, fill) + PAD)}\u2502`;
23677
+ });
23678
+ return [top, ...body, bottom].join(`
23679
+ `);
23680
+ }
23681
+
22966
23682
  // src/cli/help.ts
22967
23683
  var ROOT_GUIDE = `metahub \u2014 local knowledge base for AI agents
22968
23684
  Notion-like typed databases (tables + rows) and markdown documents, stored in
@@ -23196,6 +23912,8 @@ var main = defineCommand({
23196
23912
  sync: sync_default,
23197
23913
  snapshot: snapshot_default,
23198
23914
  restore: restore_default,
23915
+ doctor: doctor_default,
23916
+ repair: repair_default,
23199
23917
  config: config_default
23200
23918
  }
23201
23919
  });
@@ -23210,26 +23928,48 @@ var argv2 = process.argv.slice(2);
23210
23928
  if (argv2.includes("--server")) {
23211
23929
  const portFlag = flagValue(argv2, "port");
23212
23930
  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()})` : "";
23931
+ let s2;
23932
+ try {
23933
+ s2 = startServer({
23934
+ port: portFlag != null ? Number(portFlag) : undefined,
23935
+ host: flagValue(argv2, "host"),
23936
+ debug: argv2.includes("--debug"),
23937
+ token: flagValue(argv2, "token") ?? process.env.METAHUB_TOKEN,
23938
+ syncIntervalMs: intervalFlag != null ? parseDuration(intervalFlag, 30000) : undefined,
23939
+ autoSync: argv2.includes("--no-auto-sync") ? false : undefined
23940
+ });
23941
+ } catch (e3) {
23942
+ fail(e3 instanceof Error ? e3.message : String(e3), e3 instanceof PortInUseError ? 98 : 1);
23943
+ }
23944
+ const webui = `http://localhost:${s2.port}`;
23945
+ const docs = `${webui}/docs`;
23222
23946
  print({
23223
23947
  server: "listening",
23224
23948
  port: s2.port,
23949
+ host: s2.host,
23225
23950
  nodeId: s2.node,
23226
23951
  docs: `/docs`,
23952
+ webui,
23953
+ authMode: s2.authMode,
23227
23954
  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)`));
23955
+ exp: s2.exp != null && Number.isFinite(s2.exp) ? s2.exp : null,
23956
+ autoSync: s2.autoSync,
23957
+ syncIntervalMs: s2.syncIntervalMs
23958
+ }, () => renderStartupBanner({
23959
+ version: package_default.version,
23960
+ host: s2.host,
23961
+ port: s2.port,
23962
+ webuiUrl: webui,
23963
+ docsUrl: docs,
23964
+ authMode: s2.authMode,
23965
+ token: s2.token,
23966
+ exp: s2.exp,
23967
+ node: s2.node,
23968
+ autoSync: s2.autoSync,
23969
+ syncIntervalMs: s2.syncIntervalMs
23970
+ }));
23231
23971
  } else {
23232
23972
  runMain(main, { showUsage: showUsage2 });
23233
23973
  }
23234
23974
 
23235
- //# debugId=10C9A2ACC0C4883B64756E2164756E21
23975
+ //# debugId=25AC050A4B6CCBA264756E2164756E21