@tensorix/metahub 0.1.8 → 0.1.9

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
@@ -1116,7 +1116,7 @@ var init_crdt = __esm(() => {
1116
1116
  },
1117
1117
  documents: {
1118
1118
  table: "documents",
1119
- cols: new Set(["title", "body", "database_id", "parent_id", "created_hlc", "__deleted"])
1119
+ cols: new Set(["title", "body", "database_id", "parent_id", "created_hlc", "order_key", "__deleted"])
1120
1120
  },
1121
1121
  doc_blocks: {
1122
1122
  table: "doc_blocks",
@@ -1172,6 +1172,24 @@ function inferContentType(path) {
1172
1172
  const ext = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
1173
1173
  return MIME[ext] ?? "application/octet-stream";
1174
1174
  }
1175
+ function normalizeSiteName(raw) {
1176
+ const slug = raw.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
1177
+ if (!slug)
1178
+ throw new Error(`invalid site name: ${JSON.stringify(raw)}`);
1179
+ return slug;
1180
+ }
1181
+ function normalizeSitePath(raw) {
1182
+ const segs = [];
1183
+ for (const seg of raw.split("/")) {
1184
+ if (seg === "" || seg === ".")
1185
+ continue;
1186
+ if (seg === "..")
1187
+ segs.pop();
1188
+ else
1189
+ segs.push(seg);
1190
+ }
1191
+ return segs.join("/");
1192
+ }
1175
1193
  function isTextType(contentType) {
1176
1194
  const ct = contentType.toLowerCase();
1177
1195
  return ct.startsWith("text/") || ct.startsWith("application/json") || ct.startsWith("application/manifest+json") || ct.startsWith("application/xml") || ct.startsWith("text/javascript") || ct.startsWith("application/javascript") || ct.startsWith("image/svg+xml");
@@ -1182,10 +1200,11 @@ function toBytes(data) {
1182
1200
  return data instanceof Uint8Array ? data : new Uint8Array(data);
1183
1201
  }
1184
1202
  function createSite(db, opts) {
1185
- if (getSiteByName(db, opts.name))
1186
- throw new Error(`site name already exists: ${opts.name}`);
1187
- const id = newId("site", opts.name);
1188
- const first = emit(db, "sites", id, "name", opts.name);
1203
+ const name = normalizeSiteName(opts.name);
1204
+ if (getSiteByName(db, name))
1205
+ throw new Error(`site name already exists: ${name}`);
1206
+ const id = newId("site", name);
1207
+ const first = emit(db, "sites", id, "name", name);
1189
1208
  emit(db, "sites", id, "created_hlc", first.hlc);
1190
1209
  if (opts.title !== undefined)
1191
1210
  emit(db, "sites", id, "title", opts.title);
@@ -1195,7 +1214,13 @@ function getSite(db, id) {
1195
1214
  return db.query("SELECT id, name, title, created_hlc FROM sites WHERE id = ? AND __deleted = 0").get(id);
1196
1215
  }
1197
1216
  function getSiteByName(db, name) {
1198
- return db.query("SELECT id, name, title, created_hlc FROM sites WHERE name = ? AND __deleted = 0 ORDER BY created_hlc DESC LIMIT 1").get(name);
1217
+ let slug;
1218
+ try {
1219
+ slug = normalizeSiteName(name);
1220
+ } catch {
1221
+ return null;
1222
+ }
1223
+ return db.query("SELECT id, name, title, created_hlc FROM sites WHERE name = ? AND __deleted = 0 ORDER BY created_hlc DESC LIMIT 1").get(slug);
1199
1224
  }
1200
1225
  function listSites(db) {
1201
1226
  return db.query("SELECT id, name, title, created_hlc FROM sites WHERE __deleted = 0 ORDER BY created_hlc").all();
@@ -1207,6 +1232,20 @@ function resolveSite(db, ref) {
1207
1232
  throw new Error(`no such site: ${ref}`);
1208
1233
  return site;
1209
1234
  }
1235
+ function updateSite(db, id, opts) {
1236
+ if (!getSite(db, id))
1237
+ throw new Error(`no such site: ${id}`);
1238
+ if (opts.name !== undefined) {
1239
+ const name = normalizeSiteName(opts.name);
1240
+ const dup = getSiteByName(db, name);
1241
+ if (dup && dup.id !== id)
1242
+ throw new Error(`site name already exists: ${name}`);
1243
+ emit(db, "sites", id, "name", name);
1244
+ }
1245
+ if (opts.title !== undefined)
1246
+ emit(db, "sites", id, "title", opts.title);
1247
+ return getSite(db, id);
1248
+ }
1210
1249
  function deleteSite(db, id) {
1211
1250
  if (!getSite(db, id))
1212
1251
  return false;
@@ -1221,7 +1260,10 @@ function fileIdFor(db, siteId, path) {
1221
1260
  return row?.id ?? null;
1222
1261
  }
1223
1262
  async function putFile(db, siteId, path, opts) {
1224
- const contentType = opts.contentType ?? inferContentType(path);
1263
+ const cleanPath = normalizeSitePath(path);
1264
+ if (!cleanPath)
1265
+ throw new Error(`invalid file path: ${JSON.stringify(path)}`);
1266
+ const contentType = opts.contentType ?? inferContentType(cleanPath);
1225
1267
  const bytes = toBytes(opts.data);
1226
1268
  let encoding;
1227
1269
  let content;
@@ -1235,11 +1277,11 @@ async function putFile(db, siteId, path, opts) {
1235
1277
  encoding = "blob";
1236
1278
  content = (await putBlob(bytes)).hash;
1237
1279
  }
1238
- const existing = fileIdFor(db, siteId, path);
1239
- const id = existing ?? newId("sf", path);
1280
+ const existing = fileIdFor(db, siteId, cleanPath);
1281
+ const id = existing ?? newId("sf", cleanPath);
1240
1282
  if (!existing) {
1241
1283
  const first = emit(db, "site_files", id, "site_id", siteId);
1242
- emit(db, "site_files", id, "path", path);
1284
+ emit(db, "site_files", id, "path", cleanPath);
1243
1285
  emit(db, "site_files", id, "created_hlc", first.hlc);
1244
1286
  }
1245
1287
  emit(db, "site_files", id, "content_type", contentType);
@@ -1252,7 +1294,7 @@ function listFiles(db, siteId) {
1252
1294
  return db.query("SELECT id, site_id, path, content_type, encoding FROM site_files WHERE site_id = ? AND __deleted = 0 ORDER BY path").all(siteId);
1253
1295
  }
1254
1296
  function deleteFile(db, siteId, path) {
1255
- const id = fileIdFor(db, siteId, path);
1297
+ const id = fileIdFor(db, siteId, normalizeSitePath(path));
1256
1298
  if (!id)
1257
1299
  return false;
1258
1300
  const live = db.query("SELECT __deleted AS d FROM site_files WHERE id = ?").get(id);
@@ -1262,7 +1304,10 @@ function deleteFile(db, siteId, path) {
1262
1304
  return true;
1263
1305
  }
1264
1306
  async function getFileForServe(db, siteId, path) {
1265
- const p = path === "" || path.endsWith("/") ? `${path}index.html` : path;
1307
+ const withIndex = path === "" || path.endsWith("/") ? `${path}index.html` : path;
1308
+ const p = normalizeSitePath(withIndex);
1309
+ if (!p)
1310
+ return null;
1266
1311
  const row = db.query("SELECT content_type, encoding, content FROM site_files WHERE site_id = ? AND path = ? AND __deleted = 0 ORDER BY created_hlc LIMIT 1").get(siteId, p);
1267
1312
  if (!row)
1268
1313
  return null;
@@ -1516,7 +1561,7 @@ var HTML = `<!doctype html>
1516
1561
  .sb-section-head .add { opacity:0; padding:3px; border-radius:5px; color:var(--muted); transition:opacity .12s; }
1517
1562
  .sb-section:hover .add { opacity:1; }
1518
1563
  .sb-section-head .add:hover { background:var(--hover-2); color:var(--fg); }
1519
- .navitem { display:flex; align-items:center; gap:3px; width:100%; text-align:left; padding:5px 6px; border-radius:var(--radius-sm);
1564
+ .navitem { display:flex; align-items:center; gap:3px; width:100%; text-align:left; padding:5px 6px; margin-bottom:2px; border-radius:var(--radius-sm);
1520
1565
  color:var(--fg-soft); font-size:13.5px; position:relative; white-space:nowrap; transition:background .1s; }
1521
1566
  .navitem:hover { background:var(--hover-2); }
1522
1567
  .navitem.active { background:var(--bg); color:var(--fg); font-weight:600; box-shadow:var(--shadow-sm); }
@@ -1625,6 +1670,9 @@ var HTML = `<!doctype html>
1625
1670
  body.desktop-mac .sb-head .iconbtn { -webkit-app-region:no-drag; }
1626
1671
  body.desktop-mac .topbar { -webkit-app-region:drag; }
1627
1672
  body.desktop-mac .topbar .iconbtn, body.desktop-mac .topbar .btn { -webkit-app-region:no-drag; }
1673
+ /* peek drawer header overlaps the topbar drag region \u2192 clear its buttons so they stay clickable */
1674
+ body.desktop-mac .peek-head { -webkit-app-region:drag; }
1675
+ body.desktop-mac .peek-head .iconbtn, body.desktop-mac .peek-head .btn { -webkit-app-region:no-drag; }
1628
1676
  /* sidebar collapsed \u2192 traffic lights sit over the main header; clear the hamburger */
1629
1677
  body.desktop-mac .sidebar.collapsed ~ .main .topbar { padding-left:80px; }
1630
1678
 
@@ -1859,6 +1907,83 @@ var HTML = `<!doctype html>
1859
1907
  .gridfoot { display:flex; gap:18px; color:var(--muted); font-size:12px; padding:8px 2px; }
1860
1908
  input.inlineedit { width:100%; border:0; outline:2px solid var(--accent); border-radius:4px; padding:7px 9px; background:var(--bg); color:var(--fg); font:inherit; }
1861
1909
 
1910
+ /* ---- sites (\u7AD9\u70B9\u7BA1\u7406) ---- */
1911
+ .sites-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(264px, 1fr)); gap:14px; margin-top:6px; }
1912
+ .site-card { display:flex; flex-direction:column; gap:13px; padding:16px 16px 14px; background:var(--bg); border:1px solid var(--line);
1913
+ border-radius:var(--radius); box-shadow:var(--shadow-sm); cursor:pointer; transition:box-shadow .16s, border-color .16s, transform .16s; }
1914
+ .site-card:hover { box-shadow:var(--shadow-md); border-color:var(--line-strong); transform:translateY(-1px); }
1915
+ .site-card-head { display:flex; align-items:center; gap:11px; min-width:0; }
1916
+ .site-card .si { width:40px; height:40px; border-radius:11px; background:var(--accent-soft); color:var(--accent); display:grid; place-items:center; flex:none; }
1917
+ .site-card .si svg { width:20px; height:20px; }
1918
+ .site-card-id { min-width:0; }
1919
+ .site-card .slug { font-family:var(--mono); font-weight:600; font-size:14.5px; color:var(--fg); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1920
+ .site-card .ttl { font-size:12px; color:var(--fg-soft); margin-top:1px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1921
+ .site-card .ttl.muted { color:var(--muted); font-style:italic; }
1922
+ .site-addr { display:flex; align-items:center; gap:6px; width:100%; text-align:left; background:var(--surface); border:1px solid var(--line);
1923
+ border-radius:var(--radius-sm); padding:6px 9px; color:var(--muted); font-family:var(--mono); font-size:11.5px; transition:border-color .12s, color .12s; }
1924
+ .site-addr svg { flex:none; }
1925
+ .site-addr span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1926
+ .site-addr:hover { border-color:var(--accent); color:var(--accent); }
1927
+ .site-card-meta { display:flex; align-items:center; gap:7px; font-size:12px; color:var(--muted); }
1928
+ .site-card-meta b { color:var(--fg-soft); font-weight:600; font-variant-numeric:tabular-nums; }
1929
+ .site-card-meta .dot { color:var(--line-strong); }
1930
+ .site-card-foot { display:flex; align-items:center; gap:6px; padding-top:12px; margin-top:1px; border-top:1px solid var(--line); }
1931
+ .site-card-foot .btn-secondary { padding:6px 12px; justify-content:center; }
1932
+
1933
+ .site-empty { text-align:center; color:var(--muted); border:1px dashed var(--line-strong); border-radius:var(--radius); padding:48px 24px; margin-top:8px; }
1934
+ .site-empty .ei { font-size:30px; margin-bottom:10px; }
1935
+ .site-empty .et { font-size:14px; color:var(--fg-soft); margin-bottom:4px; font-weight:600; }
1936
+ .site-empty .ed { font-size:12.5px; margin-bottom:16px; }
1937
+
1938
+ /* site peek: access bar + file manager */
1939
+ .acc-link { display:flex; align-items:center; gap:6px; background:var(--surface); border:1px solid var(--line); border-radius:var(--radius-sm); padding:7px 8px 7px 11px; margin-bottom:16px; }
1940
+ .acc-link .url { flex:1; font-family:var(--mono); font-size:12.5px; color:var(--fg-soft); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1941
+ .acc-link button { width:28px; height:28px; display:grid; place-items:center; border-radius:6px; color:var(--muted); flex:none; }
1942
+ .acc-link button:hover { background:var(--hover-2); color:var(--fg); }
1943
+ .acc-link button.accent { color:var(--accent); }
1944
+ .site-meta { display:flex; gap:18px; color:var(--muted); font-size:12px; margin-bottom:20px; }
1945
+ .site-meta b { color:var(--fg); font-weight:600; }
1946
+ .files-head { display:flex; align-items:center; justify-content:space-between; font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); margin:0 0 4px; }
1947
+ .filerow { display:flex; align-items:center; gap:10px; padding:9px 8px; border-radius:var(--radius-sm); cursor:pointer; border-bottom:1px solid var(--line); }
1948
+ .filerow:last-child { border-bottom:0; }
1949
+ .filerow:hover { background:var(--hover); }
1950
+ .filerow .fi { color:var(--muted); display:grid; place-items:center; flex:none; }
1951
+ .filerow .fpath { flex:1; font-family:var(--mono); font-size:12.5px; color:var(--fg); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1952
+ .filerow .facts { display:flex; gap:1px; opacity:0; flex:none; }
1953
+ .filerow:hover .facts { opacity:1; }
1954
+ .filerow .facts button { width:27px; height:27px; display:grid; place-items:center; border-radius:6px; color:var(--muted); }
1955
+ .filerow .facts button:hover { background:var(--hover-2); color:var(--fg); }
1956
+ .filerow .facts button.del:hover { color:var(--danger); }
1957
+ .enc-badge { display:inline-flex; align-items:center; font-family:var(--mono); font-size:10.5px; font-weight:500; letter-spacing:.02em;
1958
+ padding:2px 6px; border-radius:5px; background:var(--surface-2); color:var(--fg-soft); border:1px solid var(--line); }
1959
+ .enc-badge.blob { color:var(--accent); background:var(--accent-soft); border-color:transparent; }
1960
+
1961
+ /* file preview (inside modal) */
1962
+ .preview-box { font-family:var(--mono); font-size:12.5px; background:var(--surface-2); border:1px solid var(--line); border-radius:var(--radius);
1963
+ padding:14px 16px; max-height:52vh; overflow:auto; white-space:pre-wrap; line-height:1.6; color:var(--fg); }
1964
+ .preview-bin { text-align:center; color:var(--muted); border:1px dashed var(--line-strong); border-radius:var(--radius); padding:40px 20px; }
1965
+ .preview-bin .pi { font-size:28px; margin-bottom:8px; }
1966
+ .preview-bin .pt { color:var(--fg-soft); font-size:13px; }
1967
+
1968
+ /* in-app site preview (browser-framed iframe) */
1969
+ .spv-scrim { position:fixed; inset:0; background:rgba(15,15,25,.5); z-index:85; display:grid; place-items:center; padding:28px;
1970
+ opacity:0; pointer-events:none; transition:opacity .16s; animation:spv-in .16s forwards; }
1971
+ @keyframes spv-in { to { opacity:1; pointer-events:auto; } }
1972
+ .spv-scrim.open { opacity:1; pointer-events:auto; }
1973
+ .spv { width:min(1100px, 92vw); height:88vh; background:var(--bg); border-radius:14px; box-shadow:var(--shadow-lg);
1974
+ display:flex; flex-direction:column; overflow:hidden; }
1975
+ .spv-chrome { display:flex; align-items:center; gap:10px; padding:9px 12px; border-bottom:1px solid var(--line); background:var(--surface); }
1976
+ .spv-dots { display:flex; gap:6px; flex:none; }
1977
+ .spv-dots i { width:11px; height:11px; border-radius:50%; background:var(--line-strong); }
1978
+ .spv-url { flex:1; display:flex; align-items:center; gap:6px; min-width:0; background:var(--bg); border:1px solid var(--line);
1979
+ border-radius:7px; padding:5px 10px; color:var(--fg-soft); font-family:var(--mono); font-size:12px; }
1980
+ .spv-url svg { color:var(--muted); flex:none; }
1981
+ .spv-url span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1982
+ .spv-btn { display:inline-flex; align-items:center; gap:5px; flex:none; padding:6px 10px; border-radius:7px; color:var(--fg-soft); font-size:12.5px; font-weight:500; }
1983
+ .spv-btn:hover { background:var(--hover-2); color:var(--fg); }
1984
+ .spv-btn.icon { padding:6px; }
1985
+ .spv-frame { flex:1; width:100%; border:0; background:#fff; }
1986
+
1862
1987
  .selbar { position:fixed; bottom:24px; left:50%; transform:translateX(-50%); z-index:65; display:flex; align-items:center; gap:4px;
1863
1988
  background:var(--fg); color:var(--bg); padding:7px 8px 7px 14px; border-radius:11px; box-shadow:var(--shadow-lg); }
1864
1989
  .selbar .cnt { font-size:13px; font-weight:600; margin-right:6px; }
@@ -3605,7 +3730,7 @@ async function runMain(cmd, opts = {}) {
3605
3730
  // package.json
3606
3731
  var package_default = {
3607
3732
  name: "@tensorix/metahub",
3608
- version: "0.1.8",
3733
+ version: "0.1.9",
3609
3734
  description: "Metahub core library and CLI",
3610
3735
  license: "AGPL-3.0-only",
3611
3736
  type: "module",
@@ -3756,6 +3881,7 @@ CREATE TABLE IF NOT EXISTS documents (
3756
3881
  database_id TEXT,
3757
3882
  parent_id TEXT,
3758
3883
  created_hlc TEXT,
3884
+ order_key TEXT, -- fractional index among siblings (same parent_id); ties broken by created_hlc,id
3759
3885
  __deleted INTEGER NOT NULL DEFAULT 0
3760
3886
  );
3761
3887
 
@@ -4398,6 +4524,291 @@ function deleteRecord(db, id) {
4398
4524
  return true;
4399
4525
  }
4400
4526
 
4527
+ // src/core/documents.ts
4528
+ init_ids();
4529
+ init_crdt();
4530
+ function liveBlocks(db, docId) {
4531
+ 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);
4532
+ }
4533
+ function makeBlockId(text) {
4534
+ return newId("blk", text.split(`
4535
+ `, 1)[0] ?? "");
4536
+ }
4537
+ function emitBlock(db, docId, fields) {
4538
+ const id = makeBlockId(fields.text);
4539
+ emit(db, "doc_blocks", id, "doc_id", docId);
4540
+ emit(db, "doc_blocks", id, "order_key", fields.order_key);
4541
+ if (fields.blankAfter)
4542
+ emit(db, "doc_blocks", id, "blank_after", fields.blankAfter);
4543
+ emit(db, "doc_blocks", id, "text", fields.text);
4544
+ }
4545
+ function insertBlocks(db, docId, blocks, after, before) {
4546
+ const keys = keysBetween(after, before, blocks.length);
4547
+ blocks.forEach((b2, i2) => emitBlock(db, docId, { text: b2.text, order_key: keys[i2], blankAfter: b2.blankAfter }));
4548
+ }
4549
+ function ensureBlocks(db, docId) {
4550
+ if (db.query("SELECT 1 AS x FROM doc_blocks WHERE doc_id = ? LIMIT 1").get(docId))
4551
+ return;
4552
+ const row = db.query("SELECT body FROM documents WHERE id = ?").get(docId);
4553
+ const blocks = parseDocBlocks(row?.body ?? "");
4554
+ if (blocks.length)
4555
+ insertBlocks(db, docId, blocks, null, null);
4556
+ }
4557
+ function reconcileBody(db, docId, body) {
4558
+ const old = liveBlocks(db, docId);
4559
+ const next = parseDocBlocks(body);
4560
+ const plan = reconcile(old.map((b2) => b2.text), next.map((b2) => b2.text));
4561
+ for (const oi of plan.deleted)
4562
+ emit(db, "doc_blocks", old[oi].id, "__deleted", 1);
4563
+ const { items } = plan;
4564
+ let i2 = 0;
4565
+ while (i2 < items.length) {
4566
+ const it = items[i2];
4567
+ if ("keep" in it) {
4568
+ const oldB = old[it.keep];
4569
+ const want = next[i2].blankAfter;
4570
+ if ((oldB.blank_after ?? 0) !== want)
4571
+ emit(db, "doc_blocks", oldB.id, "blank_after", want);
4572
+ i2++;
4573
+ continue;
4574
+ }
4575
+ let j = i2;
4576
+ while (j < items.length && "insert" in items[j])
4577
+ j++;
4578
+ const left = i2 > 0 ? old[items[i2 - 1].keep].order_key : null;
4579
+ const right = j < items.length ? old[items[j].keep].order_key : null;
4580
+ insertBlocks(db, docId, next.slice(i2, j), left, right);
4581
+ i2 = j;
4582
+ }
4583
+ }
4584
+ function documentVersion(db, id) {
4585
+ const row = db.query(`SELECT MAX(hlc) AS h FROM crdt_changes
4586
+ WHERE (dataset = 'documents' AND row_id = ?)
4587
+ OR (dataset = 'doc_blocks' AND row_id IN (SELECT id FROM doc_blocks WHERE doc_id = ?))`).get(id, id);
4588
+ return row.h ?? "";
4589
+ }
4590
+ function createDocument(db, opts) {
4591
+ const id = newId("doc", opts.title);
4592
+ const first = emit(db, "documents", id, "title", opts.title);
4593
+ emit(db, "documents", id, "created_hlc", first.hlc);
4594
+ if (opts.database_id !== undefined)
4595
+ emit(db, "documents", id, "database_id", opts.database_id);
4596
+ if (opts.parent_id !== undefined)
4597
+ emit(db, "documents", id, "parent_id", opts.parent_id);
4598
+ placeInSiblings(db, id, opts.parent_id ?? null);
4599
+ if (opts.body !== undefined) {
4600
+ const blocks = parseDocBlocks(opts.body);
4601
+ if (blocks.length)
4602
+ insertBlocks(db, id, blocks, null, null);
4603
+ }
4604
+ return getDocument(db, id);
4605
+ }
4606
+ function getDocument(db, id) {
4607
+ return db.query("SELECT id, title, body, database_id, parent_id, created_hlc, order_key FROM documents WHERE id = ? AND __deleted = 0").get(id);
4608
+ }
4609
+ var ORDER_BY = "ORDER BY order_key IS NULL, order_key, created_hlc, id";
4610
+ function listDocuments(db, opts = {}) {
4611
+ const cols = "SELECT id, title, database_id, parent_id, created_hlc, order_key FROM documents WHERE __deleted = 0";
4612
+ if (opts.parent_id !== undefined)
4613
+ return db.query(`${cols} AND parent_id = ? ${ORDER_BY}`).all(opts.parent_id);
4614
+ if (opts.database_id)
4615
+ return db.query(`${cols} AND database_id = ? ${ORDER_BY}`).all(opts.database_id);
4616
+ return db.query(`${cols} ${ORDER_BY}`).all();
4617
+ }
4618
+ function siblingWhere(parentId) {
4619
+ return parentId === null ? { clause: "parent_id IS NULL", args: [] } : { clause: "parent_id = ?", args: [parentId] };
4620
+ }
4621
+ function canEmitOrderKeys2(db) {
4622
+ return tableExists2(db, "meta") && tableExists2(db, "crdt_changes");
4623
+ }
4624
+ function tableExists2(db, table) {
4625
+ return db.query("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) != null;
4626
+ }
4627
+ function lastSiblingOrderKey(db, parentId, excludeId) {
4628
+ const w2 = siblingWhere(parentId);
4629
+ const exclude = excludeId ? " AND id <> ?" : "";
4630
+ const row = db.query(`SELECT order_key FROM documents WHERE ${w2.clause}${exclude}
4631
+ AND __deleted = 0 AND order_key IS NOT NULL
4632
+ ORDER BY order_key DESC, id DESC LIMIT 1`).get(...w2.args, ...excludeId ? [excludeId] : []);
4633
+ return row?.order_key ?? null;
4634
+ }
4635
+ function orderedSiblings(db, parentId) {
4636
+ const w2 = siblingWhere(parentId);
4637
+ return db.query(`SELECT id, order_key FROM documents WHERE ${w2.clause} AND __deleted = 0 ${ORDER_BY}`).all(...w2.args);
4638
+ }
4639
+ function rebalanceSiblings(db, parentId) {
4640
+ const rows = orderedSiblings(db, parentId);
4641
+ const keys = keysBetween(null, null, rows.length);
4642
+ rows.forEach((row, i2) => {
4643
+ if (row.order_key !== keys[i2])
4644
+ emit(db, "documents", row.id, "order_key", keys[i2]);
4645
+ });
4646
+ }
4647
+ function backfillSiblings(db, parentId) {
4648
+ const emitChange = canEmitOrderKeys2(db);
4649
+ const w2 = siblingWhere(parentId);
4650
+ const missing = db.query(`SELECT id FROM documents WHERE ${w2.clause} AND __deleted = 0 AND order_key IS NULL
4651
+ ORDER BY created_hlc, id`).all(...w2.args);
4652
+ if (!missing.length)
4653
+ return;
4654
+ const keys = keysBetween(lastSiblingOrderKey(db, parentId), null, missing.length);
4655
+ missing.forEach((row, i2) => {
4656
+ if (emitChange)
4657
+ emit(db, "documents", row.id, "order_key", keys[i2]);
4658
+ else
4659
+ db.query("UPDATE documents SET order_key = ? WHERE id = ?").run(keys[i2], row.id);
4660
+ });
4661
+ }
4662
+ function backfillDocumentOrderKeys(db) {
4663
+ const parents = db.query("SELECT DISTINCT parent_id FROM documents WHERE __deleted = 0 AND order_key IS NULL").all();
4664
+ for (const p of parents)
4665
+ backfillSiblings(db, p.parent_id);
4666
+ }
4667
+ function placeInSiblings(db, id, parentId, anchor) {
4668
+ let cur = parentId;
4669
+ while (cur) {
4670
+ if (cur === id)
4671
+ throw new Error(`cannot set parent_id: would create a cycle (${id})`);
4672
+ cur = getDocument(db, cur)?.parent_id ?? null;
4673
+ }
4674
+ const current = getDocument(db, id);
4675
+ if (current && (current.parent_id ?? null) !== parentId)
4676
+ emit(db, "documents", id, "parent_id", parentId);
4677
+ let key;
4678
+ if (!anchor) {
4679
+ key = keyBetween(lastSiblingOrderKey(db, parentId, id), null);
4680
+ } else {
4681
+ backfillSiblings(db, parentId);
4682
+ const neighbors = () => {
4683
+ const rows = orderedSiblings(db, parentId).filter((r3) => r3.id !== id);
4684
+ const to = rows.findIndex((r3) => r3.id === anchor.targetId);
4685
+ if (to < 0)
4686
+ throw new Error(`no such target document: ${anchor.targetId}`);
4687
+ return anchor.where === "before" ? [rows[to - 1]?.order_key ?? null, rows[to].order_key] : [rows[to].order_key, rows[to + 1]?.order_key ?? null];
4688
+ };
4689
+ let [left, right] = neighbors();
4690
+ if (left !== null && right !== null && left >= right) {
4691
+ rebalanceSiblings(db, parentId);
4692
+ [left, right] = neighbors();
4693
+ }
4694
+ key = keyBetween(left, right);
4695
+ }
4696
+ emit(db, "documents", id, "order_key", key);
4697
+ }
4698
+ function moveDocument(db, id, targetId, where) {
4699
+ const src2 = getDocument(db, id);
4700
+ if (!src2)
4701
+ throw new Error(`no such document: ${id}`);
4702
+ if (id === targetId)
4703
+ return src2;
4704
+ const target = getDocument(db, targetId);
4705
+ if (!target)
4706
+ throw new Error(`no such target document: ${targetId}`);
4707
+ if (where === "into")
4708
+ placeInSiblings(db, id, targetId);
4709
+ else
4710
+ placeInSiblings(db, id, target.parent_id, { targetId, where });
4711
+ return getDocument(db, id);
4712
+ }
4713
+ function updateDocument(db, id, fields) {
4714
+ if (!getDocument(db, id))
4715
+ throw new Error(`no such document: ${id}`);
4716
+ if (fields.title !== undefined)
4717
+ emit(db, "documents", id, "title", fields.title);
4718
+ if (fields.database_id !== undefined)
4719
+ emit(db, "documents", id, "database_id", fields.database_id);
4720
+ if (fields.parent_id !== undefined) {
4721
+ const next = fields.parent_id ?? null;
4722
+ if ((getDocument(db, id).parent_id ?? null) !== next)
4723
+ placeInSiblings(db, id, next);
4724
+ }
4725
+ if (fields.body !== undefined) {
4726
+ ensureBlocks(db, id);
4727
+ reconcileBody(db, id, fields.body);
4728
+ }
4729
+ return getDocument(db, id);
4730
+ }
4731
+ function editDocument(db, id, opts) {
4732
+ if (!getDocument(db, id))
4733
+ throw new Error(`no such document: ${id}`);
4734
+ if (opts.ifMatch !== undefined && documentVersion(db, id) !== opts.ifMatch)
4735
+ throw new Error(`stale: document changed since ${opts.ifMatch}; re-read first`);
4736
+ if (opts.old === "")
4737
+ throw new Error("--old must not be empty");
4738
+ ensureBlocks(db, id);
4739
+ const blocks = liveBlocks(db, id);
4740
+ const body = serializeDocBlocks(blocks.map((b2) => ({ text: b2.text, blankAfter: b2.blank_after })));
4741
+ const count = countOccurrences(body, opts.old);
4742
+ if (count === 0)
4743
+ throw new Error(`anchor not found: no match for --old`);
4744
+ if (count > 1 && !opts.replaceAll)
4745
+ throw new Error(`${count} matches for --old; add surrounding context or use --replace-all`);
4746
+ if (!opts.old.includes(`
4747
+
4748
+ `)) {
4749
+ const affected = blocks.filter((b2) => b2.text.includes(opts.old));
4750
+ const next = affected.map((b2) => opts.replaceAll ? b2.text.replaceAll(opts.old, opts.new) : b2.text.replace(opts.old, opts.new));
4751
+ if (next.every((t2) => t2.length > 0 && !t2.includes(`
4752
+
4753
+ `))) {
4754
+ const targets = opts.replaceAll ? affected : affected.slice(0, 1);
4755
+ targets.forEach((b2, i2) => emit(db, "doc_blocks", b2.id, "text", next[i2]));
4756
+ return { id, changed: true, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
4757
+ }
4758
+ }
4759
+ const newBody = opts.replaceAll ? body.replaceAll(opts.old, opts.new) : body.replace(opts.old, opts.new);
4760
+ reconcileBody(db, id, newBody);
4761
+ return { id, changed: newBody !== body, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
4762
+ }
4763
+ function appendDocument(db, id, body) {
4764
+ if (!getDocument(db, id))
4765
+ throw new Error(`no such document: ${id}`);
4766
+ ensureBlocks(db, id);
4767
+ const next = parseDocBlocks(body);
4768
+ if (next.length) {
4769
+ const blocks = liveBlocks(db, id);
4770
+ const last = blocks.length ? blocks[blocks.length - 1].order_key : null;
4771
+ insertBlocks(db, id, next, last, null);
4772
+ }
4773
+ return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
4774
+ }
4775
+ function prependDocument(db, id, body) {
4776
+ if (!getDocument(db, id))
4777
+ throw new Error(`no such document: ${id}`);
4778
+ ensureBlocks(db, id);
4779
+ const next = parseDocBlocks(body);
4780
+ if (next.length) {
4781
+ const blocks = liveBlocks(db, id);
4782
+ const first = blocks.length ? blocks[0].order_key : null;
4783
+ insertBlocks(db, id, next, null, first);
4784
+ }
4785
+ return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
4786
+ }
4787
+ function deleteDocument(db, id) {
4788
+ if (!getDocument(db, id))
4789
+ return false;
4790
+ emit(db, "documents", id, "__deleted", 1);
4791
+ for (const b2 of liveChildIds(db, "doc_blocks", "doc_id", id))
4792
+ emit(db, "doc_blocks", b2, "__deleted", 1);
4793
+ for (const c3 of liveChildIds(db, "documents", "parent_id", id))
4794
+ emit(db, "documents", c3, "parent_id", null);
4795
+ return true;
4796
+ }
4797
+ function liveChildIds(db, table, col, parentId) {
4798
+ return db.query(`SELECT id FROM ${table} WHERE ${col} = ? AND __deleted = 0`).all(parentId).map((r3) => r3.id);
4799
+ }
4800
+ function countOccurrences(haystack, needle) {
4801
+ if (needle === "")
4802
+ return 0;
4803
+ let n2 = 0;
4804
+ let i2 = haystack.indexOf(needle);
4805
+ while (i2 !== -1) {
4806
+ n2++;
4807
+ i2 = haystack.indexOf(needle, i2 + needle.length);
4808
+ }
4809
+ return n2;
4810
+ }
4811
+
4401
4812
  // src/core/db.ts
4402
4813
  function ensureDirs() {
4403
4814
  mkdirSync(metahubHome(), { recursive: true });
@@ -4417,7 +4828,7 @@ function hasColumn(db, table, column) {
4417
4828
  const cols = db.query(`PRAGMA table_info(${table})`).all();
4418
4829
  return cols.some((c3) => c3.name === column);
4419
4830
  }
4420
- function tableExists2(db, table) {
4831
+ function tableExists3(db, table) {
4421
4832
  return db.query("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) != null;
4422
4833
  }
4423
4834
  function migrateRecords(db) {
@@ -4425,7 +4836,7 @@ function migrateRecords(db) {
4425
4836
  db.exec("ALTER TABLE records ADD COLUMN data TEXT NOT NULL DEFAULT '{}'");
4426
4837
  if (!hasColumn(db, "records", "order_key"))
4427
4838
  db.exec("ALTER TABLE records ADD COLUMN order_key TEXT");
4428
- if (!tableExists2(db, "record_values"))
4839
+ if (!tableExists3(db, "record_values"))
4429
4840
  return;
4430
4841
  const tx = db.transaction(() => {
4431
4842
  db.exec(`
@@ -4460,6 +4871,11 @@ function migrateDocBlocks(db) {
4460
4871
  if (!hasColumn(db, "doc_blocks", "blank_after"))
4461
4872
  db.exec("ALTER TABLE doc_blocks ADD COLUMN blank_after INTEGER NOT NULL DEFAULT 0");
4462
4873
  }
4874
+ function migrateDocuments(db) {
4875
+ if (!hasColumn(db, "documents", "order_key"))
4876
+ db.exec("ALTER TABLE documents ADD COLUMN order_key TEXT");
4877
+ backfillDocumentOrderKeys(db);
4878
+ }
4463
4879
  function openMetahub() {
4464
4880
  ensureDirs();
4465
4881
  const db = new Database(dbPath(), { create: true });
@@ -4468,6 +4884,7 @@ function openMetahub() {
4468
4884
  migrateRecords(db);
4469
4885
  migratePeers(db);
4470
4886
  migrateDocBlocks(db);
4887
+ migrateDocuments(db);
4471
4888
  return db;
4472
4889
  }
4473
4890
 
@@ -4635,198 +5052,6 @@ var use_default = defineCommand({
4635
5052
  })
4636
5053
  });
4637
5054
 
4638
- // src/core/documents.ts
4639
- init_ids();
4640
- init_crdt();
4641
- function liveBlocks(db, docId) {
4642
- 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);
4643
- }
4644
- function makeBlockId(text) {
4645
- return newId("blk", text.split(`
4646
- `, 1)[0] ?? "");
4647
- }
4648
- function emitBlock(db, docId, fields) {
4649
- const id = makeBlockId(fields.text);
4650
- emit(db, "doc_blocks", id, "doc_id", docId);
4651
- emit(db, "doc_blocks", id, "order_key", fields.order_key);
4652
- if (fields.blankAfter)
4653
- emit(db, "doc_blocks", id, "blank_after", fields.blankAfter);
4654
- emit(db, "doc_blocks", id, "text", fields.text);
4655
- }
4656
- function insertBlocks(db, docId, blocks, after, before) {
4657
- const keys = keysBetween(after, before, blocks.length);
4658
- blocks.forEach((b2, i2) => emitBlock(db, docId, { text: b2.text, order_key: keys[i2], blankAfter: b2.blankAfter }));
4659
- }
4660
- function ensureBlocks(db, docId) {
4661
- if (db.query("SELECT 1 AS x FROM doc_blocks WHERE doc_id = ? LIMIT 1").get(docId))
4662
- return;
4663
- const row = db.query("SELECT body FROM documents WHERE id = ?").get(docId);
4664
- const blocks = parseDocBlocks(row?.body ?? "");
4665
- if (blocks.length)
4666
- insertBlocks(db, docId, blocks, null, null);
4667
- }
4668
- function reconcileBody(db, docId, body) {
4669
- const old = liveBlocks(db, docId);
4670
- const next = parseDocBlocks(body);
4671
- const plan = reconcile(old.map((b2) => b2.text), next.map((b2) => b2.text));
4672
- for (const oi of plan.deleted)
4673
- emit(db, "doc_blocks", old[oi].id, "__deleted", 1);
4674
- const { items } = plan;
4675
- let i2 = 0;
4676
- while (i2 < items.length) {
4677
- const it = items[i2];
4678
- if ("keep" in it) {
4679
- const oldB = old[it.keep];
4680
- const want = next[i2].blankAfter;
4681
- if ((oldB.blank_after ?? 0) !== want)
4682
- emit(db, "doc_blocks", oldB.id, "blank_after", want);
4683
- i2++;
4684
- continue;
4685
- }
4686
- let j = i2;
4687
- while (j < items.length && "insert" in items[j])
4688
- j++;
4689
- const left = i2 > 0 ? old[items[i2 - 1].keep].order_key : null;
4690
- const right = j < items.length ? old[items[j].keep].order_key : null;
4691
- insertBlocks(db, docId, next.slice(i2, j), left, right);
4692
- i2 = j;
4693
- }
4694
- }
4695
- function documentVersion(db, id) {
4696
- const row = db.query(`SELECT MAX(hlc) AS h FROM crdt_changes
4697
- WHERE (dataset = 'documents' AND row_id = ?)
4698
- OR (dataset = 'doc_blocks' AND row_id IN (SELECT id FROM doc_blocks WHERE doc_id = ?))`).get(id, id);
4699
- return row.h ?? "";
4700
- }
4701
- function createDocument(db, opts) {
4702
- const id = newId("doc", opts.title);
4703
- const first = emit(db, "documents", id, "title", opts.title);
4704
- emit(db, "documents", id, "created_hlc", first.hlc);
4705
- if (opts.database_id !== undefined)
4706
- emit(db, "documents", id, "database_id", opts.database_id);
4707
- if (opts.parent_id !== undefined)
4708
- emit(db, "documents", id, "parent_id", opts.parent_id);
4709
- if (opts.body !== undefined) {
4710
- const blocks = parseDocBlocks(opts.body);
4711
- if (blocks.length)
4712
- insertBlocks(db, id, blocks, null, null);
4713
- }
4714
- return getDocument(db, id);
4715
- }
4716
- function getDocument(db, id) {
4717
- return db.query("SELECT id, title, body, database_id, parent_id, created_hlc FROM documents WHERE id = ? AND __deleted = 0").get(id);
4718
- }
4719
- function listDocuments(db, opts = {}) {
4720
- const cols = "SELECT id, title, database_id, parent_id, created_hlc FROM documents WHERE __deleted = 0";
4721
- if (opts.parent_id !== undefined)
4722
- return db.query(`${cols} AND parent_id = ? ORDER BY created_hlc`).all(opts.parent_id);
4723
- if (opts.database_id)
4724
- return db.query(`${cols} AND database_id = ? ORDER BY created_hlc`).all(opts.database_id);
4725
- return db.query(`${cols} ORDER BY created_hlc`).all();
4726
- }
4727
- function updateDocument(db, id, fields) {
4728
- if (!getDocument(db, id))
4729
- throw new Error(`no such document: ${id}`);
4730
- if (fields.title !== undefined)
4731
- emit(db, "documents", id, "title", fields.title);
4732
- if (fields.database_id !== undefined)
4733
- emit(db, "documents", id, "database_id", fields.database_id);
4734
- if (fields.parent_id !== undefined) {
4735
- let cur = fields.parent_id;
4736
- while (cur) {
4737
- if (cur === id)
4738
- throw new Error(`cannot set parent_id: would create a cycle (${id})`);
4739
- cur = getDocument(db, cur)?.parent_id ?? null;
4740
- }
4741
- emit(db, "documents", id, "parent_id", fields.parent_id);
4742
- }
4743
- if (fields.body !== undefined) {
4744
- ensureBlocks(db, id);
4745
- reconcileBody(db, id, fields.body);
4746
- }
4747
- return getDocument(db, id);
4748
- }
4749
- function editDocument(db, id, opts) {
4750
- if (!getDocument(db, id))
4751
- throw new Error(`no such document: ${id}`);
4752
- if (opts.ifMatch !== undefined && documentVersion(db, id) !== opts.ifMatch)
4753
- throw new Error(`stale: document changed since ${opts.ifMatch}; re-read first`);
4754
- if (opts.old === "")
4755
- throw new Error("--old must not be empty");
4756
- ensureBlocks(db, id);
4757
- const blocks = liveBlocks(db, id);
4758
- const body = serializeDocBlocks(blocks.map((b2) => ({ text: b2.text, blankAfter: b2.blank_after })));
4759
- const count = countOccurrences(body, opts.old);
4760
- if (count === 0)
4761
- throw new Error(`anchor not found: no match for --old`);
4762
- if (count > 1 && !opts.replaceAll)
4763
- throw new Error(`${count} matches for --old; add surrounding context or use --replace-all`);
4764
- if (!opts.old.includes(`
4765
-
4766
- `)) {
4767
- const affected = blocks.filter((b2) => b2.text.includes(opts.old));
4768
- const next = affected.map((b2) => opts.replaceAll ? b2.text.replaceAll(opts.old, opts.new) : b2.text.replace(opts.old, opts.new));
4769
- if (next.every((t2) => t2.length > 0 && !t2.includes(`
4770
-
4771
- `))) {
4772
- const targets = opts.replaceAll ? affected : affected.slice(0, 1);
4773
- targets.forEach((b2, i2) => emit(db, "doc_blocks", b2.id, "text", next[i2]));
4774
- return { id, changed: true, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
4775
- }
4776
- }
4777
- const newBody = opts.replaceAll ? body.replaceAll(opts.old, opts.new) : body.replace(opts.old, opts.new);
4778
- reconcileBody(db, id, newBody);
4779
- return { id, changed: newBody !== body, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
4780
- }
4781
- function appendDocument(db, id, body) {
4782
- if (!getDocument(db, id))
4783
- throw new Error(`no such document: ${id}`);
4784
- ensureBlocks(db, id);
4785
- const next = parseDocBlocks(body);
4786
- if (next.length) {
4787
- const blocks = liveBlocks(db, id);
4788
- const last = blocks.length ? blocks[blocks.length - 1].order_key : null;
4789
- insertBlocks(db, id, next, last, null);
4790
- }
4791
- return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
4792
- }
4793
- function prependDocument(db, id, body) {
4794
- if (!getDocument(db, id))
4795
- throw new Error(`no such document: ${id}`);
4796
- ensureBlocks(db, id);
4797
- const next = parseDocBlocks(body);
4798
- if (next.length) {
4799
- const blocks = liveBlocks(db, id);
4800
- const first = blocks.length ? blocks[0].order_key : null;
4801
- insertBlocks(db, id, next, null, first);
4802
- }
4803
- return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
4804
- }
4805
- function deleteDocument(db, id) {
4806
- if (!getDocument(db, id))
4807
- return false;
4808
- emit(db, "documents", id, "__deleted", 1);
4809
- for (const b2 of liveChildIds(db, "doc_blocks", "doc_id", id))
4810
- emit(db, "doc_blocks", b2, "__deleted", 1);
4811
- for (const c3 of liveChildIds(db, "documents", "parent_id", id))
4812
- emit(db, "documents", c3, "parent_id", null);
4813
- return true;
4814
- }
4815
- function liveChildIds(db, table2, col, parentId) {
4816
- return db.query(`SELECT id FROM ${table2} WHERE ${col} = ? AND __deleted = 0`).all(parentId).map((r3) => r3.id);
4817
- }
4818
- function countOccurrences(haystack, needle) {
4819
- if (needle === "")
4820
- return 0;
4821
- let n2 = 0;
4822
- let i2 = haystack.indexOf(needle);
4823
- while (i2 !== -1) {
4824
- n2++;
4825
- i2 = haystack.indexOf(needle, i2 + needle.length);
4826
- }
4827
- return n2;
4828
- }
4829
-
4830
5055
  // src/cli/commands/get.ts
4831
5056
  var get_default = defineCommand({
4832
5057
  meta: { name: "get", description: "Look up any entity by ref (id/prefix/name), type auto-detected" },
@@ -22770,7 +22995,7 @@ var PropertySchema = exports_external.object({
22770
22995
  var RecordSchema = exports_external.object({
22771
22996
  id: exports_external.string(),
22772
22997
  database_id: exports_external.string(),
22773
- values: exports_external.record(exports_external.string(), exports_external.any())
22998
+ values: exports_external.record(exports_external.string(), exports_external.any()).describe('Field values keyed by property name (e.g. {"Amount":35}). Reads nest cells here; writes take a flat {column: value} map.')
22774
22999
  });
22775
23000
  var MoveRecordReq = exports_external.object({
22776
23001
  target: exports_external.string(),
@@ -22781,7 +23006,12 @@ var DocumentSummarySchema = exports_external.object({
22781
23006
  title: exports_external.string(),
22782
23007
  database_id: exports_external.string().nullable(),
22783
23008
  parent_id: exports_external.string().nullable(),
22784
- created_hlc: exports_external.string()
23009
+ created_hlc: exports_external.string(),
23010
+ order_key: exports_external.string().nullable()
23011
+ });
23012
+ var MoveDocumentReq = exports_external.object({
23013
+ target: exports_external.string(),
23014
+ where: exports_external.enum(["before", "after", "into"])
22785
23015
  });
22786
23016
  var DocumentSchema = DocumentSummarySchema.extend({ body: exports_external.string().nullable() });
22787
23017
  var SearchHitSchema = exports_external.object({
@@ -22810,7 +23040,7 @@ var UpdatePropertyReq = exports_external.object({
22810
23040
  position: exports_external.number().optional()
22811
23041
  });
22812
23042
  var SetWidthReq = exports_external.object({ width: exports_external.number() });
22813
- var RecordValuesReq = exports_external.record(exports_external.string(), exports_external.any());
23043
+ var RecordValuesReq = exports_external.record(exports_external.string(), exports_external.any()).describe("Flat { column: value } cell patch (keyed by property name).");
22814
23044
  var CreateDocumentReq = exports_external.object({
22815
23045
  title: exports_external.string(),
22816
23046
  body: exports_external.string().optional(),
@@ -23037,6 +23267,17 @@ var webuiRoutes = [
23037
23267
  return updateDocument(db, need(req, "id"), body);
23038
23268
  })
23039
23269
  },
23270
+ {
23271
+ method: "PATCH",
23272
+ path: "/api/document/move",
23273
+ summary: "Move a document before/after another, or into it as a child. Query: ?id=<id>",
23274
+ request: MoveDocumentReq,
23275
+ response: DocumentSchema,
23276
+ handler: handle(async (req, { db }) => {
23277
+ const body = await req.json();
23278
+ return moveDocument(db, need(req, "id"), body.target, body.where);
23279
+ })
23280
+ },
23040
23281
  {
23041
23282
  method: "DELETE",
23042
23283
  path: "/api/document",
@@ -23064,6 +23305,7 @@ var SiteSchema = exports_external.object({
23064
23305
  title: exports_external.string().nullable(),
23065
23306
  created_hlc: exports_external.string()
23066
23307
  });
23308
+ var SiteWithCountSchema = SiteSchema.extend({ file_count: exports_external.number() });
23067
23309
  var SiteFileSchema = exports_external.object({
23068
23310
  id: exports_external.string(),
23069
23311
  site_id: exports_external.string(),
@@ -23071,6 +23313,8 @@ var SiteFileSchema = exports_external.object({
23071
23313
  content_type: exports_external.string(),
23072
23314
  encoding: exports_external.string()
23073
23315
  });
23316
+ var CreateSiteBody = exports_external.object({ name: exports_external.string(), title: exports_external.string().optional() });
23317
+ var UpdateSiteBody = exports_external.object({ name: exports_external.string().optional(), title: exports_external.string().optional() });
23074
23318
  function need2(req, key) {
23075
23319
  const v3 = new URL(req.url).searchParams.get(key);
23076
23320
  if (!v3)
@@ -23090,9 +23334,9 @@ var sitesRoutes = [
23090
23334
  {
23091
23335
  method: "GET",
23092
23336
  path: "/api/sites",
23093
- summary: "List published sites",
23094
- response: exports_external.array(SiteSchema),
23095
- handler: handle2((_req, { db }) => listSites(db))
23337
+ summary: "List published sites (with file counts)",
23338
+ response: exports_external.array(SiteWithCountSchema),
23339
+ handler: handle2((_req, { db }) => listSites(db).map((s2) => ({ ...s2, file_count: listFiles(db, s2.id).length })))
23096
23340
  },
23097
23341
  {
23098
23342
  method: "GET",
@@ -23100,6 +23344,60 @@ var sitesRoutes = [
23100
23344
  summary: "List a site's files (manifest). Query: ?site=<id|name>",
23101
23345
  response: exports_external.array(SiteFileSchema),
23102
23346
  handler: handle2((req, { db }) => listFiles(db, resolveSite(db, need2(req, "site")).id))
23347
+ },
23348
+ {
23349
+ method: "POST",
23350
+ path: "/api/sites",
23351
+ summary: "Create a site",
23352
+ request: CreateSiteBody,
23353
+ response: SiteSchema,
23354
+ handler: handle2(async (req, { db }) => {
23355
+ const body = CreateSiteBody.parse(await req.json());
23356
+ return createSite(db, body);
23357
+ })
23358
+ },
23359
+ {
23360
+ method: "PATCH",
23361
+ path: "/api/site",
23362
+ summary: "Rename a site or change its title. Query: ?id=<id>",
23363
+ request: UpdateSiteBody,
23364
+ response: SiteSchema,
23365
+ handler: handle2(async (req, { db }) => {
23366
+ const body = UpdateSiteBody.parse(await req.json());
23367
+ return updateSite(db, need2(req, "id"), body);
23368
+ })
23369
+ },
23370
+ {
23371
+ method: "DELETE",
23372
+ path: "/api/site",
23373
+ summary: "Delete a site and its files. Query: ?id=<id>",
23374
+ response: exports_external.object({ ok: exports_external.boolean() }),
23375
+ handler: handle2((req, { db }) => ({ ok: deleteSite(db, need2(req, "id")) }))
23376
+ },
23377
+ {
23378
+ method: "POST",
23379
+ path: "/api/site/file",
23380
+ summary: "Upload/replace a file. Query: ?site=<id|name>&path=<path>; body is raw bytes",
23381
+ response: SiteFileSchema,
23382
+ handler: handle2(async (req, { db }) => {
23383
+ const site = resolveSite(db, need2(req, "site"));
23384
+ const ct3 = req.headers.get("content-type");
23385
+ const file2 = await putFile(db, site.id, need2(req, "path"), {
23386
+ data: await req.arrayBuffer(),
23387
+ contentType: ct3 && ct3 !== "application/octet-stream" ? ct3 : undefined
23388
+ });
23389
+ const { content: _content, ...summary } = file2;
23390
+ return summary;
23391
+ })
23392
+ },
23393
+ {
23394
+ method: "DELETE",
23395
+ path: "/api/site/file",
23396
+ summary: "Delete a file. Query: ?site=<id|name>&path=<path>",
23397
+ response: exports_external.object({ ok: exports_external.boolean() }),
23398
+ handler: handle2((req, { db }) => ({
23399
+ ok: deleteFile(db, resolveSite(db, need2(req, "site")).id, need2(req, "path"))
23400
+ }))
23103
23401
  }
23104
23402
  ];
23105
23403
 
@@ -23726,7 +24024,9 @@ USAGE
23726
24024
  CONCEPTS
23727
24025
  database (db) A typed table. Owns properties and records.
23728
24026
  property (prop) A column with a type (text|number|...). Belongs to a db.
23729
- record (rec) A row: a { column: value } map inside a db.
24027
+ record (rec) A row of cells. Write a flat { column: value } map; reads
24028
+ wrap them as { id, database_id, values: { column: value } }
24029
+ \u2014 field values live under "values", keyed by property name.
23730
24030
  document (doc) A markdown doc, stored as ordered blocks (block-level CRDT).
23731
24031
  site (site) A named bucket of files (HTML/CSS/JS) served at /sites/<name>/.
23732
24032
  Every id is "<kind>_<slug>-<rand>", e.g. db_tasks-k3f9c1, rec_fix-login-7j02an.
@@ -23823,6 +24123,13 @@ AI WORKFLOW (edit a doc without clobbering concurrent changes)
23823
24123
  mh doc edit <ref> --old "old text" --new "new text" --if-match <version>
23824
24124
  mh doc append <ref> --body "a new paragraph"
23825
24125
 
24126
+ SITE DATA (read a table from a hosted page \u2014 field values nest under "values")
24127
+ // a page served at /sites/<name>/ reads this library same-origin:
24128
+ const rs = await fetch('/api/records?db=tasks').then(r => r.json());
24129
+ // rs.records: [{ id, database_id, values: { Title, Status } }]
24130
+ rs.records.forEach(r => render(r.values.Title, r.values.Status));
24131
+ // NOTE: writes take a flat { column: value } map; reads wrap cells in .values
24132
+
23826
24133
  Run 'mh <command> --help' for arguments and examples of a specific command.`;
23827
24134
  var EXAMPLES = {
23828
24135
  init: ["mh init", "METAHUB_HOME=/tmp/mh mh init # use an alternate home"],
@@ -24006,4 +24313,4 @@ if (argv2.includes("--server")) {
24006
24313
  runMain(main, { showUsage: showUsage2 });
24007
24314
  }
24008
24315
 
24009
- //# debugId=BD44ACF4937BFB2064756E2164756E21
24316
+ //# debugId=740ECE016A65928164756E2164756E21