@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/index.js CHANGED
@@ -342,7 +342,7 @@ var init_crdt = __esm(() => {
342
342
  },
343
343
  documents: {
344
344
  table: "documents",
345
- cols: new Set(["title", "body", "database_id", "parent_id", "created_hlc", "__deleted"])
345
+ cols: new Set(["title", "body", "database_id", "parent_id", "created_hlc", "order_key", "__deleted"])
346
346
  },
347
347
  doc_blocks: {
348
348
  table: "doc_blocks",
@@ -398,6 +398,24 @@ function inferContentType(path) {
398
398
  const ext = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
399
399
  return MIME[ext] ?? "application/octet-stream";
400
400
  }
401
+ function normalizeSiteName(raw) {
402
+ const slug = raw.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
403
+ if (!slug)
404
+ throw new Error(`invalid site name: ${JSON.stringify(raw)}`);
405
+ return slug;
406
+ }
407
+ function normalizeSitePath(raw) {
408
+ const segs = [];
409
+ for (const seg of raw.split("/")) {
410
+ if (seg === "" || seg === ".")
411
+ continue;
412
+ if (seg === "..")
413
+ segs.pop();
414
+ else
415
+ segs.push(seg);
416
+ }
417
+ return segs.join("/");
418
+ }
401
419
  function isTextType(contentType) {
402
420
  const ct = contentType.toLowerCase();
403
421
  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");
@@ -408,10 +426,11 @@ function toBytes(data) {
408
426
  return data instanceof Uint8Array ? data : new Uint8Array(data);
409
427
  }
410
428
  function createSite(db, opts) {
411
- if (getSiteByName(db, opts.name))
412
- throw new Error(`site name already exists: ${opts.name}`);
413
- const id = newId("site", opts.name);
414
- const first = emit(db, "sites", id, "name", opts.name);
429
+ const name = normalizeSiteName(opts.name);
430
+ if (getSiteByName(db, name))
431
+ throw new Error(`site name already exists: ${name}`);
432
+ const id = newId("site", name);
433
+ const first = emit(db, "sites", id, "name", name);
415
434
  emit(db, "sites", id, "created_hlc", first.hlc);
416
435
  if (opts.title !== undefined)
417
436
  emit(db, "sites", id, "title", opts.title);
@@ -421,7 +440,13 @@ function getSite(db, id) {
421
440
  return db.query("SELECT id, name, title, created_hlc FROM sites WHERE id = ? AND __deleted = 0").get(id);
422
441
  }
423
442
  function getSiteByName(db, name) {
424
- 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);
443
+ let slug;
444
+ try {
445
+ slug = normalizeSiteName(name);
446
+ } catch {
447
+ return null;
448
+ }
449
+ 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);
425
450
  }
426
451
  function listSites(db) {
427
452
  return db.query("SELECT id, name, title, created_hlc FROM sites WHERE __deleted = 0 ORDER BY created_hlc").all();
@@ -433,6 +458,20 @@ function resolveSite(db, ref) {
433
458
  throw new Error(`no such site: ${ref}`);
434
459
  return site;
435
460
  }
461
+ function updateSite(db, id, opts) {
462
+ if (!getSite(db, id))
463
+ throw new Error(`no such site: ${id}`);
464
+ if (opts.name !== undefined) {
465
+ const name = normalizeSiteName(opts.name);
466
+ const dup = getSiteByName(db, name);
467
+ if (dup && dup.id !== id)
468
+ throw new Error(`site name already exists: ${name}`);
469
+ emit(db, "sites", id, "name", name);
470
+ }
471
+ if (opts.title !== undefined)
472
+ emit(db, "sites", id, "title", opts.title);
473
+ return getSite(db, id);
474
+ }
436
475
  function deleteSite(db, id) {
437
476
  if (!getSite(db, id))
438
477
  return false;
@@ -447,7 +486,10 @@ function fileIdFor(db, siteId, path) {
447
486
  return row?.id ?? null;
448
487
  }
449
488
  async function putFile(db, siteId, path, opts) {
450
- const contentType = opts.contentType ?? inferContentType(path);
489
+ const cleanPath = normalizeSitePath(path);
490
+ if (!cleanPath)
491
+ throw new Error(`invalid file path: ${JSON.stringify(path)}`);
492
+ const contentType = opts.contentType ?? inferContentType(cleanPath);
451
493
  const bytes = toBytes(opts.data);
452
494
  let encoding;
453
495
  let content;
@@ -461,11 +503,11 @@ async function putFile(db, siteId, path, opts) {
461
503
  encoding = "blob";
462
504
  content = (await putBlob(bytes)).hash;
463
505
  }
464
- const existing = fileIdFor(db, siteId, path);
465
- const id = existing ?? newId("sf", path);
506
+ const existing = fileIdFor(db, siteId, cleanPath);
507
+ const id = existing ?? newId("sf", cleanPath);
466
508
  if (!existing) {
467
509
  const first = emit(db, "site_files", id, "site_id", siteId);
468
- emit(db, "site_files", id, "path", path);
510
+ emit(db, "site_files", id, "path", cleanPath);
469
511
  emit(db, "site_files", id, "created_hlc", first.hlc);
470
512
  }
471
513
  emit(db, "site_files", id, "content_type", contentType);
@@ -478,7 +520,7 @@ function listFiles(db, siteId) {
478
520
  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);
479
521
  }
480
522
  function deleteFile(db, siteId, path) {
481
- const id = fileIdFor(db, siteId, path);
523
+ const id = fileIdFor(db, siteId, normalizeSitePath(path));
482
524
  if (!id)
483
525
  return false;
484
526
  const live = db.query("SELECT __deleted AS d FROM site_files WHERE id = ?").get(id);
@@ -488,7 +530,10 @@ function deleteFile(db, siteId, path) {
488
530
  return true;
489
531
  }
490
532
  async function getFileForServe(db, siteId, path) {
491
- const p = path === "" || path.endsWith("/") ? `${path}index.html` : path;
533
+ const withIndex = path === "" || path.endsWith("/") ? `${path}index.html` : path;
534
+ const p = normalizeSitePath(withIndex);
535
+ if (!p)
536
+ return null;
492
537
  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);
493
538
  if (!row)
494
539
  return null;
@@ -684,7 +729,7 @@ var HTML = `<!doctype html>
684
729
  .sb-section-head .add { opacity:0; padding:3px; border-radius:5px; color:var(--muted); transition:opacity .12s; }
685
730
  .sb-section:hover .add { opacity:1; }
686
731
  .sb-section-head .add:hover { background:var(--hover-2); color:var(--fg); }
687
- .navitem { display:flex; align-items:center; gap:3px; width:100%; text-align:left; padding:5px 6px; border-radius:var(--radius-sm);
732
+ .navitem { display:flex; align-items:center; gap:3px; width:100%; text-align:left; padding:5px 6px; margin-bottom:2px; border-radius:var(--radius-sm);
688
733
  color:var(--fg-soft); font-size:13.5px; position:relative; white-space:nowrap; transition:background .1s; }
689
734
  .navitem:hover { background:var(--hover-2); }
690
735
  .navitem.active { background:var(--bg); color:var(--fg); font-weight:600; box-shadow:var(--shadow-sm); }
@@ -793,6 +838,9 @@ var HTML = `<!doctype html>
793
838
  body.desktop-mac .sb-head .iconbtn { -webkit-app-region:no-drag; }
794
839
  body.desktop-mac .topbar { -webkit-app-region:drag; }
795
840
  body.desktop-mac .topbar .iconbtn, body.desktop-mac .topbar .btn { -webkit-app-region:no-drag; }
841
+ /* peek drawer header overlaps the topbar drag region \u2192 clear its buttons so they stay clickable */
842
+ body.desktop-mac .peek-head { -webkit-app-region:drag; }
843
+ body.desktop-mac .peek-head .iconbtn, body.desktop-mac .peek-head .btn { -webkit-app-region:no-drag; }
796
844
  /* sidebar collapsed \u2192 traffic lights sit over the main header; clear the hamburger */
797
845
  body.desktop-mac .sidebar.collapsed ~ .main .topbar { padding-left:80px; }
798
846
 
@@ -1027,6 +1075,83 @@ var HTML = `<!doctype html>
1027
1075
  .gridfoot { display:flex; gap:18px; color:var(--muted); font-size:12px; padding:8px 2px; }
1028
1076
  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; }
1029
1077
 
1078
+ /* ---- sites (\u7AD9\u70B9\u7BA1\u7406) ---- */
1079
+ .sites-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(264px, 1fr)); gap:14px; margin-top:6px; }
1080
+ .site-card { display:flex; flex-direction:column; gap:13px; padding:16px 16px 14px; background:var(--bg); border:1px solid var(--line);
1081
+ border-radius:var(--radius); box-shadow:var(--shadow-sm); cursor:pointer; transition:box-shadow .16s, border-color .16s, transform .16s; }
1082
+ .site-card:hover { box-shadow:var(--shadow-md); border-color:var(--line-strong); transform:translateY(-1px); }
1083
+ .site-card-head { display:flex; align-items:center; gap:11px; min-width:0; }
1084
+ .site-card .si { width:40px; height:40px; border-radius:11px; background:var(--accent-soft); color:var(--accent); display:grid; place-items:center; flex:none; }
1085
+ .site-card .si svg { width:20px; height:20px; }
1086
+ .site-card-id { min-width:0; }
1087
+ .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; }
1088
+ .site-card .ttl { font-size:12px; color:var(--fg-soft); margin-top:1px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1089
+ .site-card .ttl.muted { color:var(--muted); font-style:italic; }
1090
+ .site-addr { display:flex; align-items:center; gap:6px; width:100%; text-align:left; background:var(--surface); border:1px solid var(--line);
1091
+ 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; }
1092
+ .site-addr svg { flex:none; }
1093
+ .site-addr span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1094
+ .site-addr:hover { border-color:var(--accent); color:var(--accent); }
1095
+ .site-card-meta { display:flex; align-items:center; gap:7px; font-size:12px; color:var(--muted); }
1096
+ .site-card-meta b { color:var(--fg-soft); font-weight:600; font-variant-numeric:tabular-nums; }
1097
+ .site-card-meta .dot { color:var(--line-strong); }
1098
+ .site-card-foot { display:flex; align-items:center; gap:6px; padding-top:12px; margin-top:1px; border-top:1px solid var(--line); }
1099
+ .site-card-foot .btn-secondary { padding:6px 12px; justify-content:center; }
1100
+
1101
+ .site-empty { text-align:center; color:var(--muted); border:1px dashed var(--line-strong); border-radius:var(--radius); padding:48px 24px; margin-top:8px; }
1102
+ .site-empty .ei { font-size:30px; margin-bottom:10px; }
1103
+ .site-empty .et { font-size:14px; color:var(--fg-soft); margin-bottom:4px; font-weight:600; }
1104
+ .site-empty .ed { font-size:12.5px; margin-bottom:16px; }
1105
+
1106
+ /* site peek: access bar + file manager */
1107
+ .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; }
1108
+ .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; }
1109
+ .acc-link button { width:28px; height:28px; display:grid; place-items:center; border-radius:6px; color:var(--muted); flex:none; }
1110
+ .acc-link button:hover { background:var(--hover-2); color:var(--fg); }
1111
+ .acc-link button.accent { color:var(--accent); }
1112
+ .site-meta { display:flex; gap:18px; color:var(--muted); font-size:12px; margin-bottom:20px; }
1113
+ .site-meta b { color:var(--fg); font-weight:600; }
1114
+ .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; }
1115
+ .filerow { display:flex; align-items:center; gap:10px; padding:9px 8px; border-radius:var(--radius-sm); cursor:pointer; border-bottom:1px solid var(--line); }
1116
+ .filerow:last-child { border-bottom:0; }
1117
+ .filerow:hover { background:var(--hover); }
1118
+ .filerow .fi { color:var(--muted); display:grid; place-items:center; flex:none; }
1119
+ .filerow .fpath { flex:1; font-family:var(--mono); font-size:12.5px; color:var(--fg); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1120
+ .filerow .facts { display:flex; gap:1px; opacity:0; flex:none; }
1121
+ .filerow:hover .facts { opacity:1; }
1122
+ .filerow .facts button { width:27px; height:27px; display:grid; place-items:center; border-radius:6px; color:var(--muted); }
1123
+ .filerow .facts button:hover { background:var(--hover-2); color:var(--fg); }
1124
+ .filerow .facts button.del:hover { color:var(--danger); }
1125
+ .enc-badge { display:inline-flex; align-items:center; font-family:var(--mono); font-size:10.5px; font-weight:500; letter-spacing:.02em;
1126
+ padding:2px 6px; border-radius:5px; background:var(--surface-2); color:var(--fg-soft); border:1px solid var(--line); }
1127
+ .enc-badge.blob { color:var(--accent); background:var(--accent-soft); border-color:transparent; }
1128
+
1129
+ /* file preview (inside modal) */
1130
+ .preview-box { font-family:var(--mono); font-size:12.5px; background:var(--surface-2); border:1px solid var(--line); border-radius:var(--radius);
1131
+ padding:14px 16px; max-height:52vh; overflow:auto; white-space:pre-wrap; line-height:1.6; color:var(--fg); }
1132
+ .preview-bin { text-align:center; color:var(--muted); border:1px dashed var(--line-strong); border-radius:var(--radius); padding:40px 20px; }
1133
+ .preview-bin .pi { font-size:28px; margin-bottom:8px; }
1134
+ .preview-bin .pt { color:var(--fg-soft); font-size:13px; }
1135
+
1136
+ /* in-app site preview (browser-framed iframe) */
1137
+ .spv-scrim { position:fixed; inset:0; background:rgba(15,15,25,.5); z-index:85; display:grid; place-items:center; padding:28px;
1138
+ opacity:0; pointer-events:none; transition:opacity .16s; animation:spv-in .16s forwards; }
1139
+ @keyframes spv-in { to { opacity:1; pointer-events:auto; } }
1140
+ .spv-scrim.open { opacity:1; pointer-events:auto; }
1141
+ .spv { width:min(1100px, 92vw); height:88vh; background:var(--bg); border-radius:14px; box-shadow:var(--shadow-lg);
1142
+ display:flex; flex-direction:column; overflow:hidden; }
1143
+ .spv-chrome { display:flex; align-items:center; gap:10px; padding:9px 12px; border-bottom:1px solid var(--line); background:var(--surface); }
1144
+ .spv-dots { display:flex; gap:6px; flex:none; }
1145
+ .spv-dots i { width:11px; height:11px; border-radius:50%; background:var(--line-strong); }
1146
+ .spv-url { flex:1; display:flex; align-items:center; gap:6px; min-width:0; background:var(--bg); border:1px solid var(--line);
1147
+ border-radius:7px; padding:5px 10px; color:var(--fg-soft); font-family:var(--mono); font-size:12px; }
1148
+ .spv-url svg { color:var(--muted); flex:none; }
1149
+ .spv-url span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1150
+ .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; }
1151
+ .spv-btn:hover { background:var(--hover-2); color:var(--fg); }
1152
+ .spv-btn.icon { padding:6px; }
1153
+ .spv-frame { flex:1; width:100%; border:0; background:#fff; }
1154
+
1030
1155
  .selbar { position:fixed; bottom:24px; left:50%; transform:translateX(-50%); z-index:65; display:flex; align-items:center; gap:4px;
1031
1156
  background:var(--fg); color:var(--bg); padding:7px 8px 7px 14px; border-radius:11px; box-shadow:var(--shadow-lg); }
1032
1157
  .selbar .cnt { font-size:13px; font-weight:600; margin-right:6px; }
@@ -1478,6 +1603,7 @@ CREATE TABLE IF NOT EXISTS documents (
1478
1603
  database_id TEXT,
1479
1604
  parent_id TEXT,
1480
1605
  created_hlc TEXT,
1606
+ order_key TEXT, -- fractional index among siblings (same parent_id); ties broken by created_hlc,id
1481
1607
  __deleted INTEGER NOT NULL DEFAULT 0
1482
1608
  );
1483
1609
 
@@ -2120,103 +2246,6 @@ function deleteRecord(db, id) {
2120
2246
  return true;
2121
2247
  }
2122
2248
 
2123
- // src/core/db.ts
2124
- function ensureDirs() {
2125
- mkdirSync(metahubHome(), { recursive: true });
2126
- mkdirSync(cacheDir(), { recursive: true });
2127
- }
2128
- function runSchema(db) {
2129
- db.exec(CORE_SCHEMA);
2130
- try {
2131
- db.exec(FTS_SCHEMA);
2132
- } catch {}
2133
- }
2134
- function ftsAvailable(db) {
2135
- const row = db.query("SELECT 1 AS ok FROM sqlite_master WHERE name = 'search_fts'").get();
2136
- return row != null;
2137
- }
2138
- function hasColumn(db, table, column) {
2139
- const cols = db.query(`PRAGMA table_info(${table})`).all();
2140
- return cols.some((c) => c.name === column);
2141
- }
2142
- function tableExists2(db, table) {
2143
- return db.query("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) != null;
2144
- }
2145
- function migrateRecords(db) {
2146
- if (!hasColumn(db, "records", "data"))
2147
- db.exec("ALTER TABLE records ADD COLUMN data TEXT NOT NULL DEFAULT '{}'");
2148
- if (!hasColumn(db, "records", "order_key"))
2149
- db.exec("ALTER TABLE records ADD COLUMN order_key TEXT");
2150
- if (!tableExists2(db, "record_values"))
2151
- return;
2152
- const tx = db.transaction(() => {
2153
- db.exec(`
2154
- UPDATE records SET data = coalesce((
2155
- SELECT json_group_object(rv.property_id, json(rv.value))
2156
- FROM record_values rv WHERE rv.record_id = records.id
2157
- ), '{}')
2158
- WHERE id IN (SELECT DISTINCT record_id FROM record_values);
2159
- `);
2160
- db.exec("DROP TABLE record_values");
2161
- backfillRecordOrderKeys(db);
2162
- });
2163
- tx();
2164
- backfillRecordOrderKeys(db);
2165
- }
2166
- function migratePeers(db) {
2167
- const add = [
2168
- ["token", "TEXT"],
2169
- ["label", "TEXT"],
2170
- ["node_id", "TEXT"],
2171
- ["enabled", "INTEGER NOT NULL DEFAULT 1"],
2172
- ["last_sync_at", "INTEGER"],
2173
- ["last_status", "TEXT"],
2174
- ["last_error", "TEXT"]
2175
- ];
2176
- for (const [col, decl] of add) {
2177
- if (!hasColumn(db, "peers", col))
2178
- db.exec(`ALTER TABLE peers ADD COLUMN ${col} ${decl}`);
2179
- }
2180
- }
2181
- function migrateDocBlocks(db) {
2182
- if (!hasColumn(db, "doc_blocks", "blank_after"))
2183
- db.exec("ALTER TABLE doc_blocks ADD COLUMN blank_after INTEGER NOT NULL DEFAULT 0");
2184
- }
2185
- function openMetahub() {
2186
- ensureDirs();
2187
- const db = new Database(dbPath(), { create: true });
2188
- db.exec("PRAGMA journal_mode = WAL;");
2189
- runSchema(db);
2190
- migrateRecords(db);
2191
- migratePeers(db);
2192
- migrateDocBlocks(db);
2193
- return db;
2194
- }
2195
-
2196
- // src/core/index.ts
2197
- init_ids();
2198
- init_crdt();
2199
- init_node();
2200
-
2201
- // src/core/context.ts
2202
- var KEY = "current_db";
2203
- function getCurrentDatabase(db) {
2204
- const row = db.query("SELECT value FROM meta WHERE key = ?").get(KEY);
2205
- if (!row)
2206
- return null;
2207
- const found = getDatabase(db, row.value);
2208
- if (!found) {
2209
- clearCurrentDatabase(db);
2210
- return null;
2211
- }
2212
- return found;
2213
- }
2214
- function setCurrentDatabase(db, id) {
2215
- db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(KEY, id);
2216
- }
2217
- function clearCurrentDatabase(db) {
2218
- db.query("DELETE FROM meta WHERE key = ?").run(KEY);
2219
- }
2220
2249
  // src/core/documents.ts
2221
2250
  init_ids();
2222
2251
  init_crdt();
@@ -2288,6 +2317,7 @@ function createDocument(db, opts) {
2288
2317
  emit(db, "documents", id, "database_id", opts.database_id);
2289
2318
  if (opts.parent_id !== undefined)
2290
2319
  emit(db, "documents", id, "parent_id", opts.parent_id);
2320
+ placeInSiblings(db, id, opts.parent_id ?? null);
2291
2321
  if (opts.body !== undefined) {
2292
2322
  const blocks = parseDocBlocks(opts.body);
2293
2323
  if (blocks.length)
@@ -2296,15 +2326,111 @@ function createDocument(db, opts) {
2296
2326
  return getDocument(db, id);
2297
2327
  }
2298
2328
  function getDocument(db, id) {
2299
- return db.query("SELECT id, title, body, database_id, parent_id, created_hlc FROM documents WHERE id = ? AND __deleted = 0").get(id);
2329
+ return db.query("SELECT id, title, body, database_id, parent_id, created_hlc, order_key FROM documents WHERE id = ? AND __deleted = 0").get(id);
2300
2330
  }
2331
+ var ORDER_BY = "ORDER BY order_key IS NULL, order_key, created_hlc, id";
2301
2332
  function listDocuments(db, opts = {}) {
2302
- const cols = "SELECT id, title, database_id, parent_id, created_hlc FROM documents WHERE __deleted = 0";
2333
+ const cols = "SELECT id, title, database_id, parent_id, created_hlc, order_key FROM documents WHERE __deleted = 0";
2303
2334
  if (opts.parent_id !== undefined)
2304
- return db.query(`${cols} AND parent_id = ? ORDER BY created_hlc`).all(opts.parent_id);
2335
+ return db.query(`${cols} AND parent_id = ? ${ORDER_BY}`).all(opts.parent_id);
2305
2336
  if (opts.database_id)
2306
- return db.query(`${cols} AND database_id = ? ORDER BY created_hlc`).all(opts.database_id);
2307
- return db.query(`${cols} ORDER BY created_hlc`).all();
2337
+ return db.query(`${cols} AND database_id = ? ${ORDER_BY}`).all(opts.database_id);
2338
+ return db.query(`${cols} ${ORDER_BY}`).all();
2339
+ }
2340
+ function siblingWhere(parentId) {
2341
+ return parentId === null ? { clause: "parent_id IS NULL", args: [] } : { clause: "parent_id = ?", args: [parentId] };
2342
+ }
2343
+ function canEmitOrderKeys2(db) {
2344
+ return tableExists2(db, "meta") && tableExists2(db, "crdt_changes");
2345
+ }
2346
+ function tableExists2(db, table) {
2347
+ return db.query("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) != null;
2348
+ }
2349
+ function lastSiblingOrderKey(db, parentId, excludeId) {
2350
+ const w = siblingWhere(parentId);
2351
+ const exclude = excludeId ? " AND id <> ?" : "";
2352
+ const row = db.query(`SELECT order_key FROM documents WHERE ${w.clause}${exclude}
2353
+ AND __deleted = 0 AND order_key IS NOT NULL
2354
+ ORDER BY order_key DESC, id DESC LIMIT 1`).get(...w.args, ...excludeId ? [excludeId] : []);
2355
+ return row?.order_key ?? null;
2356
+ }
2357
+ function orderedSiblings(db, parentId) {
2358
+ const w = siblingWhere(parentId);
2359
+ return db.query(`SELECT id, order_key FROM documents WHERE ${w.clause} AND __deleted = 0 ${ORDER_BY}`).all(...w.args);
2360
+ }
2361
+ function rebalanceSiblings(db, parentId) {
2362
+ const rows = orderedSiblings(db, parentId);
2363
+ const keys = keysBetween(null, null, rows.length);
2364
+ rows.forEach((row, i) => {
2365
+ if (row.order_key !== keys[i])
2366
+ emit(db, "documents", row.id, "order_key", keys[i]);
2367
+ });
2368
+ }
2369
+ function backfillSiblings(db, parentId) {
2370
+ const emitChange = canEmitOrderKeys2(db);
2371
+ const w = siblingWhere(parentId);
2372
+ const missing = db.query(`SELECT id FROM documents WHERE ${w.clause} AND __deleted = 0 AND order_key IS NULL
2373
+ ORDER BY created_hlc, id`).all(...w.args);
2374
+ if (!missing.length)
2375
+ return;
2376
+ const keys = keysBetween(lastSiblingOrderKey(db, parentId), null, missing.length);
2377
+ missing.forEach((row, i) => {
2378
+ if (emitChange)
2379
+ emit(db, "documents", row.id, "order_key", keys[i]);
2380
+ else
2381
+ db.query("UPDATE documents SET order_key = ? WHERE id = ?").run(keys[i], row.id);
2382
+ });
2383
+ }
2384
+ function backfillDocumentOrderKeys(db) {
2385
+ const parents = db.query("SELECT DISTINCT parent_id FROM documents WHERE __deleted = 0 AND order_key IS NULL").all();
2386
+ for (const p of parents)
2387
+ backfillSiblings(db, p.parent_id);
2388
+ }
2389
+ function placeInSiblings(db, id, parentId, anchor) {
2390
+ let cur = parentId;
2391
+ while (cur) {
2392
+ if (cur === id)
2393
+ throw new Error(`cannot set parent_id: would create a cycle (${id})`);
2394
+ cur = getDocument(db, cur)?.parent_id ?? null;
2395
+ }
2396
+ const current = getDocument(db, id);
2397
+ if (current && (current.parent_id ?? null) !== parentId)
2398
+ emit(db, "documents", id, "parent_id", parentId);
2399
+ let key;
2400
+ if (!anchor) {
2401
+ key = keyBetween(lastSiblingOrderKey(db, parentId, id), null);
2402
+ } else {
2403
+ backfillSiblings(db, parentId);
2404
+ const neighbors = () => {
2405
+ const rows = orderedSiblings(db, parentId).filter((r) => r.id !== id);
2406
+ const to = rows.findIndex((r) => r.id === anchor.targetId);
2407
+ if (to < 0)
2408
+ throw new Error(`no such target document: ${anchor.targetId}`);
2409
+ return anchor.where === "before" ? [rows[to - 1]?.order_key ?? null, rows[to].order_key] : [rows[to].order_key, rows[to + 1]?.order_key ?? null];
2410
+ };
2411
+ let [left, right] = neighbors();
2412
+ if (left !== null && right !== null && left >= right) {
2413
+ rebalanceSiblings(db, parentId);
2414
+ [left, right] = neighbors();
2415
+ }
2416
+ key = keyBetween(left, right);
2417
+ }
2418
+ emit(db, "documents", id, "order_key", key);
2419
+ }
2420
+ function moveDocument(db, id, targetId, where) {
2421
+ const src = getDocument(db, id);
2422
+ if (!src)
2423
+ throw new Error(`no such document: ${id}`);
2424
+ if (id === targetId)
2425
+ return src;
2426
+ const target = getDocument(db, targetId);
2427
+ if (!target)
2428
+ throw new Error(`no such target document: ${targetId}`);
2429
+ if (where === "into")
2430
+ placeInSiblings(db, id, targetId);
2431
+ else
2432
+ placeInSiblings(db, id, target.parent_id, { targetId, where });
2433
+ return getDocument(db, id);
2308
2434
  }
2309
2435
  function updateDocument(db, id, fields) {
2310
2436
  if (!getDocument(db, id))
@@ -2314,13 +2440,9 @@ function updateDocument(db, id, fields) {
2314
2440
  if (fields.database_id !== undefined)
2315
2441
  emit(db, "documents", id, "database_id", fields.database_id);
2316
2442
  if (fields.parent_id !== undefined) {
2317
- let cur = fields.parent_id;
2318
- while (cur) {
2319
- if (cur === id)
2320
- throw new Error(`cannot set parent_id: would create a cycle (${id})`);
2321
- cur = getDocument(db, cur)?.parent_id ?? null;
2322
- }
2323
- emit(db, "documents", id, "parent_id", fields.parent_id);
2443
+ const next = fields.parent_id ?? null;
2444
+ if ((getDocument(db, id).parent_id ?? null) !== next)
2445
+ placeInSiblings(db, id, next);
2324
2446
  }
2325
2447
  if (fields.body !== undefined) {
2326
2448
  ensureBlocks(db, id);
@@ -2408,6 +2530,110 @@ function countOccurrences(haystack, needle) {
2408
2530
  }
2409
2531
  return n;
2410
2532
  }
2533
+
2534
+ // src/core/db.ts
2535
+ function ensureDirs() {
2536
+ mkdirSync(metahubHome(), { recursive: true });
2537
+ mkdirSync(cacheDir(), { recursive: true });
2538
+ }
2539
+ function runSchema(db) {
2540
+ db.exec(CORE_SCHEMA);
2541
+ try {
2542
+ db.exec(FTS_SCHEMA);
2543
+ } catch {}
2544
+ }
2545
+ function ftsAvailable(db) {
2546
+ const row = db.query("SELECT 1 AS ok FROM sqlite_master WHERE name = 'search_fts'").get();
2547
+ return row != null;
2548
+ }
2549
+ function hasColumn(db, table, column) {
2550
+ const cols = db.query(`PRAGMA table_info(${table})`).all();
2551
+ return cols.some((c) => c.name === column);
2552
+ }
2553
+ function tableExists3(db, table) {
2554
+ return db.query("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) != null;
2555
+ }
2556
+ function migrateRecords(db) {
2557
+ if (!hasColumn(db, "records", "data"))
2558
+ db.exec("ALTER TABLE records ADD COLUMN data TEXT NOT NULL DEFAULT '{}'");
2559
+ if (!hasColumn(db, "records", "order_key"))
2560
+ db.exec("ALTER TABLE records ADD COLUMN order_key TEXT");
2561
+ if (!tableExists3(db, "record_values"))
2562
+ return;
2563
+ const tx = db.transaction(() => {
2564
+ db.exec(`
2565
+ UPDATE records SET data = coalesce((
2566
+ SELECT json_group_object(rv.property_id, json(rv.value))
2567
+ FROM record_values rv WHERE rv.record_id = records.id
2568
+ ), '{}')
2569
+ WHERE id IN (SELECT DISTINCT record_id FROM record_values);
2570
+ `);
2571
+ db.exec("DROP TABLE record_values");
2572
+ backfillRecordOrderKeys(db);
2573
+ });
2574
+ tx();
2575
+ backfillRecordOrderKeys(db);
2576
+ }
2577
+ function migratePeers(db) {
2578
+ const add = [
2579
+ ["token", "TEXT"],
2580
+ ["label", "TEXT"],
2581
+ ["node_id", "TEXT"],
2582
+ ["enabled", "INTEGER NOT NULL DEFAULT 1"],
2583
+ ["last_sync_at", "INTEGER"],
2584
+ ["last_status", "TEXT"],
2585
+ ["last_error", "TEXT"]
2586
+ ];
2587
+ for (const [col, decl] of add) {
2588
+ if (!hasColumn(db, "peers", col))
2589
+ db.exec(`ALTER TABLE peers ADD COLUMN ${col} ${decl}`);
2590
+ }
2591
+ }
2592
+ function migrateDocBlocks(db) {
2593
+ if (!hasColumn(db, "doc_blocks", "blank_after"))
2594
+ db.exec("ALTER TABLE doc_blocks ADD COLUMN blank_after INTEGER NOT NULL DEFAULT 0");
2595
+ }
2596
+ function migrateDocuments(db) {
2597
+ if (!hasColumn(db, "documents", "order_key"))
2598
+ db.exec("ALTER TABLE documents ADD COLUMN order_key TEXT");
2599
+ backfillDocumentOrderKeys(db);
2600
+ }
2601
+ function openMetahub() {
2602
+ ensureDirs();
2603
+ const db = new Database(dbPath(), { create: true });
2604
+ db.exec("PRAGMA journal_mode = WAL;");
2605
+ runSchema(db);
2606
+ migrateRecords(db);
2607
+ migratePeers(db);
2608
+ migrateDocBlocks(db);
2609
+ migrateDocuments(db);
2610
+ return db;
2611
+ }
2612
+
2613
+ // src/core/index.ts
2614
+ init_ids();
2615
+ init_crdt();
2616
+ init_node();
2617
+
2618
+ // src/core/context.ts
2619
+ var KEY = "current_db";
2620
+ function getCurrentDatabase(db) {
2621
+ const row = db.query("SELECT value FROM meta WHERE key = ?").get(KEY);
2622
+ if (!row)
2623
+ return null;
2624
+ const found = getDatabase(db, row.value);
2625
+ if (!found) {
2626
+ clearCurrentDatabase(db);
2627
+ return null;
2628
+ }
2629
+ return found;
2630
+ }
2631
+ function setCurrentDatabase(db, id) {
2632
+ db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(KEY, id);
2633
+ }
2634
+ function clearCurrentDatabase(db) {
2635
+ db.query("DELETE FROM meta WHERE key = ?").run(KEY);
2636
+ }
2411
2637
  // src/core/search.ts
2412
2638
  init_crdt();
2413
2639
  var TEXT_TYPES = "('text','url','select','multi_select','date')";
@@ -17028,7 +17254,7 @@ function getServerConfig(db) {
17028
17254
  // package.json
17029
17255
  var package_default = {
17030
17256
  name: "@tensorix/metahub",
17031
- version: "0.1.8",
17257
+ version: "0.1.9",
17032
17258
  description: "Metahub core library and CLI",
17033
17259
  license: "AGPL-3.0-only",
17034
17260
  type: "module",
@@ -17097,7 +17323,7 @@ var PropertySchema = exports_external.object({
17097
17323
  var RecordSchema = exports_external.object({
17098
17324
  id: exports_external.string(),
17099
17325
  database_id: exports_external.string(),
17100
- values: exports_external.record(exports_external.string(), exports_external.any())
17326
+ 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.')
17101
17327
  });
17102
17328
  var MoveRecordReq = exports_external.object({
17103
17329
  target: exports_external.string(),
@@ -17108,7 +17334,12 @@ var DocumentSummarySchema = exports_external.object({
17108
17334
  title: exports_external.string(),
17109
17335
  database_id: exports_external.string().nullable(),
17110
17336
  parent_id: exports_external.string().nullable(),
17111
- created_hlc: exports_external.string()
17337
+ created_hlc: exports_external.string(),
17338
+ order_key: exports_external.string().nullable()
17339
+ });
17340
+ var MoveDocumentReq = exports_external.object({
17341
+ target: exports_external.string(),
17342
+ where: exports_external.enum(["before", "after", "into"])
17112
17343
  });
17113
17344
  var DocumentSchema = DocumentSummarySchema.extend({ body: exports_external.string().nullable() });
17114
17345
  var SearchHitSchema = exports_external.object({
@@ -17137,7 +17368,7 @@ var UpdatePropertyReq = exports_external.object({
17137
17368
  position: exports_external.number().optional()
17138
17369
  });
17139
17370
  var SetWidthReq = exports_external.object({ width: exports_external.number() });
17140
- var RecordValuesReq = exports_external.record(exports_external.string(), exports_external.any());
17371
+ var RecordValuesReq = exports_external.record(exports_external.string(), exports_external.any()).describe("Flat { column: value } cell patch (keyed by property name).");
17141
17372
  var CreateDocumentReq = exports_external.object({
17142
17373
  title: exports_external.string(),
17143
17374
  body: exports_external.string().optional(),
@@ -17364,6 +17595,17 @@ var webuiRoutes = [
17364
17595
  return updateDocument(db, need(req, "id"), body);
17365
17596
  })
17366
17597
  },
17598
+ {
17599
+ method: "PATCH",
17600
+ path: "/api/document/move",
17601
+ summary: "Move a document before/after another, or into it as a child. Query: ?id=<id>",
17602
+ request: MoveDocumentReq,
17603
+ response: DocumentSchema,
17604
+ handler: handle(async (req, { db }) => {
17605
+ const body = await req.json();
17606
+ return moveDocument(db, need(req, "id"), body.target, body.where);
17607
+ })
17608
+ },
17367
17609
  {
17368
17610
  method: "DELETE",
17369
17611
  path: "/api/document",
@@ -17391,6 +17633,7 @@ var SiteSchema = exports_external.object({
17391
17633
  title: exports_external.string().nullable(),
17392
17634
  created_hlc: exports_external.string()
17393
17635
  });
17636
+ var SiteWithCountSchema = SiteSchema.extend({ file_count: exports_external.number() });
17394
17637
  var SiteFileSchema = exports_external.object({
17395
17638
  id: exports_external.string(),
17396
17639
  site_id: exports_external.string(),
@@ -17398,6 +17641,8 @@ var SiteFileSchema = exports_external.object({
17398
17641
  content_type: exports_external.string(),
17399
17642
  encoding: exports_external.string()
17400
17643
  });
17644
+ var CreateSiteBody = exports_external.object({ name: exports_external.string(), title: exports_external.string().optional() });
17645
+ var UpdateSiteBody = exports_external.object({ name: exports_external.string().optional(), title: exports_external.string().optional() });
17401
17646
  function need2(req, key) {
17402
17647
  const v = new URL(req.url).searchParams.get(key);
17403
17648
  if (!v)
@@ -17417,9 +17662,9 @@ var sitesRoutes = [
17417
17662
  {
17418
17663
  method: "GET",
17419
17664
  path: "/api/sites",
17420
- summary: "List published sites",
17421
- response: exports_external.array(SiteSchema),
17422
- handler: handle2((_req, { db }) => listSites(db))
17665
+ summary: "List published sites (with file counts)",
17666
+ response: exports_external.array(SiteWithCountSchema),
17667
+ handler: handle2((_req, { db }) => listSites(db).map((s) => ({ ...s, file_count: listFiles(db, s.id).length })))
17423
17668
  },
17424
17669
  {
17425
17670
  method: "GET",
@@ -17427,6 +17672,60 @@ var sitesRoutes = [
17427
17672
  summary: "List a site's files (manifest). Query: ?site=<id|name>",
17428
17673
  response: exports_external.array(SiteFileSchema),
17429
17674
  handler: handle2((req, { db }) => listFiles(db, resolveSite(db, need2(req, "site")).id))
17675
+ },
17676
+ {
17677
+ method: "POST",
17678
+ path: "/api/sites",
17679
+ summary: "Create a site",
17680
+ request: CreateSiteBody,
17681
+ response: SiteSchema,
17682
+ handler: handle2(async (req, { db }) => {
17683
+ const body = CreateSiteBody.parse(await req.json());
17684
+ return createSite(db, body);
17685
+ })
17686
+ },
17687
+ {
17688
+ method: "PATCH",
17689
+ path: "/api/site",
17690
+ summary: "Rename a site or change its title. Query: ?id=<id>",
17691
+ request: UpdateSiteBody,
17692
+ response: SiteSchema,
17693
+ handler: handle2(async (req, { db }) => {
17694
+ const body = UpdateSiteBody.parse(await req.json());
17695
+ return updateSite(db, need2(req, "id"), body);
17696
+ })
17697
+ },
17698
+ {
17699
+ method: "DELETE",
17700
+ path: "/api/site",
17701
+ summary: "Delete a site and its files. Query: ?id=<id>",
17702
+ response: exports_external.object({ ok: exports_external.boolean() }),
17703
+ handler: handle2((req, { db }) => ({ ok: deleteSite(db, need2(req, "id")) }))
17704
+ },
17705
+ {
17706
+ method: "POST",
17707
+ path: "/api/site/file",
17708
+ summary: "Upload/replace a file. Query: ?site=<id|name>&path=<path>; body is raw bytes",
17709
+ response: SiteFileSchema,
17710
+ handler: handle2(async (req, { db }) => {
17711
+ const site = resolveSite(db, need2(req, "site"));
17712
+ const ct = req.headers.get("content-type");
17713
+ const file2 = await putFile(db, site.id, need2(req, "path"), {
17714
+ data: await req.arrayBuffer(),
17715
+ contentType: ct && ct !== "application/octet-stream" ? ct : undefined
17716
+ });
17717
+ const { content: _content, ...summary } = file2;
17718
+ return summary;
17719
+ })
17720
+ },
17721
+ {
17722
+ method: "DELETE",
17723
+ path: "/api/site/file",
17724
+ summary: "Delete a file. Query: ?site=<id|name>&path=<path>",
17725
+ response: exports_external.object({ ok: exports_external.boolean() }),
17726
+ handler: handle2((req, { db }) => ({
17727
+ ok: deleteFile(db, resolveSite(db, need2(req, "site")).id, need2(req, "path"))
17728
+ }))
17430
17729
  }
17431
17730
  ];
17432
17731
 
@@ -18021,6 +18320,7 @@ function startServer(opts = {}) {
18021
18320
  };
18022
18321
  }
18023
18322
  export {
18323
+ updateSite,
18024
18324
  updateRecord,
18025
18325
  updateProperty,
18026
18326
  updateDocument,
@@ -18048,11 +18348,15 @@ export {
18048
18348
  parseDocBlocks,
18049
18349
  openMetahub,
18050
18350
  observeHlc,
18351
+ normalizeSitePath,
18352
+ normalizeSiteName,
18051
18353
  nextHlc,
18052
18354
  newId,
18053
18355
  moveRecord,
18356
+ moveDocument,
18054
18357
  migrateRecords,
18055
18358
  migratePeers,
18359
+ migrateDocuments,
18056
18360
  migrateDocBlocks,
18057
18361
  metahubHome,
18058
18362
  maybeAutoIndex,
@@ -18103,6 +18407,7 @@ export {
18103
18407
  cacheDir,
18104
18408
  blobPath,
18105
18409
  backfillRecordOrderKeys,
18410
+ backfillDocumentOrderKeys,
18106
18411
  applyChange,
18107
18412
  appendDocument,
18108
18413
  addProperty,
@@ -18120,4 +18425,4 @@ export {
18120
18425
  ChangeSchema
18121
18426
  };
18122
18427
 
18123
- //# debugId=D4C8ECDF76A598A964756E2164756E21
18428
+ //# debugId=F1DD5DA39D0D2ABD64756E2164756E21