@tensorix/metahub 0.1.10 → 0.1.11

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.
Files changed (75) hide show
  1. package/README.md +19 -6
  2. package/README.zh-CN.md +7 -2
  3. package/dist/cli.js +1601 -595
  4. package/dist/cli.js.map +43 -38
  5. package/dist/core/compact.d.ts +42 -0
  6. package/dist/core/compact.d.ts.map +1 -0
  7. package/dist/core/config.d.ts +3 -3
  8. package/dist/core/config.d.ts.map +1 -1
  9. package/dist/core/context.d.ts +4 -4
  10. package/dist/core/context.d.ts.map +1 -1
  11. package/dist/core/crdt.d.ts +30 -7
  12. package/dist/core/crdt.d.ts.map +1 -1
  13. package/dist/core/databases.d.ts +22 -8
  14. package/dist/core/databases.d.ts.map +1 -1
  15. package/dist/core/db.d.ts +1 -27
  16. package/dist/core/db.d.ts.map +1 -1
  17. package/dist/core/documents.d.ts +28 -15
  18. package/dist/core/documents.d.ts.map +1 -1
  19. package/dist/core/driver.d.ts +25 -0
  20. package/dist/core/driver.d.ts.map +1 -0
  21. package/dist/core/history.d.ts +99 -11
  22. package/dist/core/history.d.ts.map +1 -1
  23. package/dist/core/hlc.d.ts +3 -3
  24. package/dist/core/hlc.d.ts.map +1 -1
  25. package/dist/core/index.d.ts +1 -0
  26. package/dist/core/index.d.ts.map +1 -1
  27. package/dist/core/indexing.d.ts +4 -4
  28. package/dist/core/indexing.d.ts.map +1 -1
  29. package/dist/core/integrity.d.ts +1 -1
  30. package/dist/core/integrity.d.ts.map +1 -1
  31. package/dist/core/node.d.ts +2 -2
  32. package/dist/core/node.d.ts.map +1 -1
  33. package/dist/core/properties.d.ts +10 -9
  34. package/dist/core/properties.d.ts.map +1 -1
  35. package/dist/core/records.d.ts +13 -8
  36. package/dist/core/records.d.ts.map +1 -1
  37. package/dist/core/resolve.d.ts +4 -4
  38. package/dist/core/resolve.d.ts.map +1 -1
  39. package/dist/core/schema-init.d.ts +37 -0
  40. package/dist/core/schema-init.d.ts.map +1 -0
  41. package/dist/core/schema.d.ts +1 -1
  42. package/dist/core/schema.d.ts.map +1 -1
  43. package/dist/core/search.d.ts +3 -3
  44. package/dist/core/search.d.ts.map +1 -1
  45. package/dist/core/sites-core.d.ts +60 -0
  46. package/dist/core/sites-core.d.ts.map +1 -0
  47. package/dist/core/sites.d.ts +11 -52
  48. package/dist/core/sites.d.ts.map +1 -1
  49. package/dist/core/sqlcols.d.ts +4 -0
  50. package/dist/core/sqlcols.d.ts.map +1 -0
  51. package/dist/core/sync/auth.d.ts +5 -3
  52. package/dist/core/sync/auth.d.ts.map +1 -1
  53. package/dist/core/sync/client.d.ts +11 -3
  54. package/dist/core/sync/client.d.ts.map +1 -1
  55. package/dist/core/sync/peers.d.ts +10 -10
  56. package/dist/core/sync/peers.d.ts.map +1 -1
  57. package/dist/core/sync/protocol.d.ts +5 -0
  58. package/dist/core/sync/protocol.d.ts.map +1 -1
  59. package/dist/core/sync/routes.d.ts.map +1 -1
  60. package/dist/core/sync/server.d.ts +10 -0
  61. package/dist/core/sync/server.d.ts.map +1 -1
  62. package/dist/db-worker.js +268 -0
  63. package/dist/index.js +1061 -340
  64. package/dist/index.js.map +31 -28
  65. package/dist/metahub-sdk.js +1 -0
  66. package/dist/mh-runtime.js +1 -0
  67. package/dist/sqlite3.wasm +0 -0
  68. package/dist/sw.js +1 -0
  69. package/dist/webui/server/assets.d.ts +8 -1
  70. package/dist/webui/server/assets.d.ts.map +1 -1
  71. package/dist/webui/server/icons.d.ts +4 -0
  72. package/dist/webui/server/icons.d.ts.map +1 -0
  73. package/dist/webui/server/routes.d.ts.map +1 -1
  74. package/dist/webui.js +57 -57
  75. package/package.json +3 -1
package/dist/cli.js CHANGED
@@ -1010,6 +1010,19 @@ function lcs(a2, b2) {
1010
1010
  }
1011
1011
 
1012
1012
  // src/core/crdt.ts
1013
+ function withChangeGroup(label, fn) {
1014
+ if (currentTxn !== null)
1015
+ return fn();
1016
+ currentTxn = (label ? label + ":" : "") + randomSuffix(8);
1017
+ try {
1018
+ return fn();
1019
+ } finally {
1020
+ currentTxn = null;
1021
+ }
1022
+ }
1023
+ function grouped(fn, label = null) {
1024
+ return (...args) => withChangeGroup(label, () => fn(...args));
1025
+ }
1013
1026
  function encodeScalar(val) {
1014
1027
  if (val === null || val === undefined)
1015
1028
  return null;
@@ -1063,7 +1076,7 @@ function materialize(db, dataset, rowId, col, valueJson) {
1063
1076
  }
1064
1077
  }
1065
1078
  function applyChange(db, c3) {
1066
- db.query("INSERT OR IGNORE INTO crdt_changes (hlc, node_id, dataset, row_id, col, value) VALUES (?, ?, ?, ?, ?, ?)").run(c3.hlc, c3.node_id, c3.dataset, c3.row_id, c3.col, c3.value);
1079
+ db.query("INSERT OR IGNORE INTO crdt_changes (hlc, node_id, dataset, row_id, col, value, txn) VALUES (?, ?, ?, ?, ?, ?, ?)").run(c3.hlc, c3.node_id, c3.dataset, c3.row_id, c3.col, c3.value, c3.txn ?? null);
1067
1080
  const cur = db.query("SELECT MAX(hlc) AS h FROM crdt_changes WHERE dataset = ? AND row_id = ? AND col = ?").get(c3.dataset, c3.row_id, c3.col);
1068
1081
  if (cur.h !== c3.hlc)
1069
1082
  return false;
@@ -1078,7 +1091,8 @@ function emit(db, dataset, rowId, col, value) {
1078
1091
  dataset,
1079
1092
  row_id: rowId,
1080
1093
  col,
1081
- value: value === undefined ? null : JSON.stringify(value)
1094
+ value: value === undefined ? null : JSON.stringify(value),
1095
+ txn: currentTxn
1082
1096
  };
1083
1097
  applyChange(db, change);
1084
1098
  return change;
@@ -1096,15 +1110,27 @@ function ingest(db, changes) {
1096
1110
  tx(changes);
1097
1111
  return applied;
1098
1112
  }
1099
- function changesAfterSeq(db, seq) {
1100
- const rows = db.query("SELECT rowid AS seq, hlc, node_id, dataset, row_id, col, value FROM crdt_changes WHERE rowid > ? ORDER BY rowid").all(seq);
1101
- const last = rows[rows.length - 1];
1102
- const cursor = last ? last.seq : seq;
1113
+ function changesAfterSeq(db, seq, opts = {}) {
1114
+ const exclude = opts.excludeDatasets ?? [];
1115
+ const where = exclude.length ? `rowid > ? AND dataset NOT IN (${exclude.map(() => "?").join(", ")})` : "rowid > ?";
1116
+ const limitSql = opts.limit != null && opts.limit > 0 ? ` LIMIT ${Math.floor(opts.limit)}` : "";
1117
+ const rows = db.query(`SELECT rowid AS seq, ${CHANGE_SELECT} FROM crdt_changes WHERE ${where} ORDER BY rowid${limitSql}`).all(seq, ...exclude);
1118
+ const exhausted = opts.limit == null || opts.limit <= 0 || rows.length < opts.limit;
1119
+ let cursor;
1120
+ if (exhausted) {
1121
+ const top = db.query("SELECT MAX(rowid) AS m FROM crdt_changes").get();
1122
+ cursor = Math.max(seq, top.m ?? seq);
1123
+ } else {
1124
+ cursor = rows[rows.length - 1].seq;
1125
+ }
1103
1126
  return { changes: rows.map(({ seq: _seq, ...c3 }) => c3), cursor };
1104
1127
  }
1105
- var DOMAIN, RECORD_META;
1128
+ var CHANGE_COLS, CHANGE_SELECT, currentTxn = null, DOMAIN, RECORD_META;
1106
1129
  var init_crdt = __esm(() => {
1107
1130
  init_node();
1131
+ init_ids();
1132
+ CHANGE_COLS = ["hlc", "node_id", "dataset", "row_id", "col", "value", "txn"];
1133
+ CHANGE_SELECT = CHANGE_COLS.join(", ");
1108
1134
  DOMAIN = {
1109
1135
  databases: {
1110
1136
  table: "databases",
@@ -1183,7 +1209,7 @@ var init_cache = __esm(() => {
1183
1209
  init_paths();
1184
1210
  });
1185
1211
 
1186
- // src/core/sites.ts
1212
+ // src/core/sites-core.ts
1187
1213
  function inferContentType(path) {
1188
1214
  const ext = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
1189
1215
  return MIME[ext] ?? "application/octet-stream";
@@ -1206,28 +1232,8 @@ function normalizeSitePath(raw) {
1206
1232
  }
1207
1233
  return segs.join("/");
1208
1234
  }
1209
- function isTextType(contentType) {
1210
- const ct = contentType.toLowerCase();
1211
- 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");
1212
- }
1213
- function toBytes(data) {
1214
- if (typeof data === "string")
1215
- return new TextEncoder().encode(data);
1216
- return data instanceof Uint8Array ? data : new Uint8Array(data);
1217
- }
1218
- function createSite(db, opts) {
1219
- const name = normalizeSiteName(opts.name);
1220
- if (getSiteByName(db, name))
1221
- throw new MhError("conflict", `site name already exists: ${name}`);
1222
- const id = newId("site", name);
1223
- const first = emit(db, "sites", id, "name", name);
1224
- emit(db, "sites", id, "created_hlc", first.hlc);
1225
- if (opts.title !== undefined)
1226
- emit(db, "sites", id, "title", opts.title);
1227
- return getSite(db, id);
1228
- }
1229
1235
  function getSite(db, id) {
1230
- return db.query("SELECT id, name, title, created_hlc FROM sites WHERE id = ? AND __deleted = 0").get(id);
1236
+ return db.query(`SELECT ${SITE_SELECT} FROM sites WHERE id = ? AND __deleted = 0`).get(id);
1231
1237
  }
1232
1238
  function getSiteByName(db, name) {
1233
1239
  let slug;
@@ -1236,10 +1242,10 @@ function getSiteByName(db, name) {
1236
1242
  } catch {
1237
1243
  return null;
1238
1244
  }
1239
- 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);
1245
+ return db.query(`SELECT ${SITE_SELECT} FROM sites WHERE name = ? AND __deleted = 0 ORDER BY created_hlc DESC LIMIT 1`).get(slug);
1240
1246
  }
1241
1247
  function listSites(db) {
1242
- return db.query("SELECT id, name, title, created_hlc FROM sites WHERE __deleted = 0 ORDER BY created_hlc").all();
1248
+ return db.query(`SELECT ${SITE_SELECT} FROM sites WHERE __deleted = 0 ORDER BY created_hlc`).all();
1243
1249
  }
1244
1250
  function resolveSite(db, ref) {
1245
1251
  const byId = ref.startsWith("site_") ? getSite(db, ref) : null;
@@ -1248,28 +1254,67 @@ function resolveSite(db, ref) {
1248
1254
  throw new MhError("not_found", `no such site: ${ref}`);
1249
1255
  return site;
1250
1256
  }
1251
- function updateSite(db, id, opts) {
1252
- if (!getSite(db, id))
1253
- throw new MhError("not_found", `no such site: ${id}`);
1254
- if (opts.name !== undefined) {
1255
- const name = normalizeSiteName(opts.name);
1256
- const dup = getSiteByName(db, name);
1257
- if (dup && dup.id !== id)
1258
- throw new MhError("conflict", `site name already exists: ${name}`);
1259
- emit(db, "sites", id, "name", name);
1260
- }
1261
- if (opts.title !== undefined)
1262
- emit(db, "sites", id, "title", opts.title);
1263
- return getSite(db, id);
1257
+ function getFileRow(db, siteId, path) {
1258
+ const withIndex = path === "" || path.endsWith("/") ? `${path}index.html` : path;
1259
+ const p = normalizeSitePath(withIndex);
1260
+ if (!p)
1261
+ return null;
1262
+ return 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);
1264
1263
  }
1265
- function deleteSite(db, id) {
1266
- if (!getSite(db, id))
1267
- return false;
1268
- const files = db.query("SELECT id FROM site_files WHERE site_id = ? AND __deleted = 0").all(id);
1269
- for (const f3 of files)
1270
- emit(db, "site_files", f3.id, "__deleted", 1);
1271
- emit(db, "sites", id, "__deleted", 1);
1272
- return true;
1264
+ function listFiles(db, siteId) {
1265
+ 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);
1266
+ }
1267
+ var SITE_COLS, SITE_SELECT, SITE_FILE_COLS, SITE_FILE_SELECT, MIME;
1268
+ var init_sites_core = __esm(() => {
1269
+ init_errors();
1270
+ SITE_COLS = ["id", "name", "title", "created_hlc"];
1271
+ SITE_SELECT = SITE_COLS.join(", ");
1272
+ SITE_FILE_COLS = [
1273
+ "id",
1274
+ "site_id",
1275
+ "path",
1276
+ "content_type",
1277
+ "encoding",
1278
+ "content"
1279
+ ];
1280
+ SITE_FILE_SELECT = SITE_FILE_COLS.join(", ");
1281
+ MIME = {
1282
+ html: "text/html; charset=utf-8",
1283
+ htm: "text/html; charset=utf-8",
1284
+ css: "text/css; charset=utf-8",
1285
+ js: "text/javascript; charset=utf-8",
1286
+ mjs: "text/javascript; charset=utf-8",
1287
+ json: "application/json; charset=utf-8",
1288
+ svg: "image/svg+xml",
1289
+ txt: "text/plain; charset=utf-8",
1290
+ md: "text/markdown; charset=utf-8",
1291
+ xml: "application/xml; charset=utf-8",
1292
+ webmanifest: "application/manifest+json",
1293
+ ico: "image/x-icon",
1294
+ png: "image/png",
1295
+ jpg: "image/jpeg",
1296
+ jpeg: "image/jpeg",
1297
+ gif: "image/gif",
1298
+ webp: "image/webp",
1299
+ avif: "image/avif",
1300
+ woff: "font/woff",
1301
+ woff2: "font/woff2",
1302
+ ttf: "font/ttf",
1303
+ otf: "font/otf",
1304
+ pdf: "application/pdf",
1305
+ wasm: "application/wasm"
1306
+ };
1307
+ });
1308
+
1309
+ // src/core/sites.ts
1310
+ function isTextType(contentType) {
1311
+ const ct = contentType.toLowerCase();
1312
+ 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");
1313
+ }
1314
+ function toBytes(data) {
1315
+ if (typeof data === "string")
1316
+ return new TextEncoder().encode(data);
1317
+ return data instanceof Uint8Array ? data : new Uint8Array(data);
1273
1318
  }
1274
1319
  function fileIdFor(db, siteId, path) {
1275
1320
  const row = db.query("SELECT id FROM site_files WHERE site_id = ? AND path = ? ORDER BY created_hlc LIMIT 1").get(siteId, path);
@@ -1295,36 +1340,21 @@ async function putFile(db, siteId, path, opts) {
1295
1340
  }
1296
1341
  const existing = fileIdFor(db, siteId, cleanPath);
1297
1342
  const id = existing ?? newId("sf", cleanPath);
1298
- if (!existing) {
1299
- const first = emit(db, "site_files", id, "site_id", siteId);
1300
- emit(db, "site_files", id, "path", cleanPath);
1301
- emit(db, "site_files", id, "created_hlc", first.hlc);
1302
- }
1303
- emit(db, "site_files", id, "content_type", contentType);
1304
- emit(db, "site_files", id, "encoding", encoding);
1305
- emit(db, "site_files", id, "content", content);
1306
- emit(db, "site_files", id, "__deleted", 0);
1307
- return db.query("SELECT id, site_id, path, content_type, encoding, content FROM site_files WHERE id = ?").get(id);
1308
- }
1309
- function listFiles(db, siteId) {
1310
- 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);
1311
- }
1312
- function deleteFile(db, siteId, path) {
1313
- const id = fileIdFor(db, siteId, normalizeSitePath(path));
1314
- if (!id)
1315
- return false;
1316
- const live = db.query("SELECT __deleted AS d FROM site_files WHERE id = ?").get(id);
1317
- if (!live || live.d)
1318
- return false;
1319
- emit(db, "site_files", id, "__deleted", 1);
1320
- return true;
1343
+ withChangeGroup(null, () => {
1344
+ if (!existing) {
1345
+ const first = emit(db, "site_files", id, "site_id", siteId);
1346
+ emit(db, "site_files", id, "path", cleanPath);
1347
+ emit(db, "site_files", id, "created_hlc", first.hlc);
1348
+ }
1349
+ emit(db, "site_files", id, "content_type", contentType);
1350
+ emit(db, "site_files", id, "encoding", encoding);
1351
+ emit(db, "site_files", id, "content", content);
1352
+ emit(db, "site_files", id, "__deleted", 0);
1353
+ });
1354
+ return db.query(`SELECT ${SITE_FILE_SELECT} FROM site_files WHERE id = ?`).get(id);
1321
1355
  }
1322
1356
  async function getFileForServe(db, siteId, path) {
1323
- const withIndex = path === "" || path.endsWith("/") ? `${path}index.html` : path;
1324
- const p = normalizeSitePath(withIndex);
1325
- if (!p)
1326
- return null;
1327
- 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);
1357
+ const row = getFileRow(db, siteId, path);
1328
1358
  if (!row)
1329
1359
  return null;
1330
1360
  const content = row.content ?? "";
@@ -1339,39 +1369,59 @@ async function getFileForServe(db, siteId, path) {
1339
1369
  return null;
1340
1370
  return { contentType: row.content_type, bytes };
1341
1371
  }
1342
- var INLINE_LIMIT, MIME;
1372
+ var INLINE_LIMIT, createSite, updateSite, deleteSite, deleteFile;
1343
1373
  var init_sites = __esm(() => {
1344
1374
  init_ids();
1345
1375
  init_crdt();
1346
1376
  init_cache();
1347
1377
  init_errors();
1378
+ init_sites_core();
1379
+ init_sites_core();
1348
1380
  INLINE_LIMIT = 256 * 1024;
1349
- MIME = {
1350
- html: "text/html; charset=utf-8",
1351
- htm: "text/html; charset=utf-8",
1352
- css: "text/css; charset=utf-8",
1353
- js: "text/javascript; charset=utf-8",
1354
- mjs: "text/javascript; charset=utf-8",
1355
- json: "application/json; charset=utf-8",
1356
- svg: "image/svg+xml",
1357
- txt: "text/plain; charset=utf-8",
1358
- md: "text/markdown; charset=utf-8",
1359
- xml: "application/xml; charset=utf-8",
1360
- webmanifest: "application/manifest+json",
1361
- ico: "image/x-icon",
1362
- png: "image/png",
1363
- jpg: "image/jpeg",
1364
- jpeg: "image/jpeg",
1365
- gif: "image/gif",
1366
- webp: "image/webp",
1367
- avif: "image/avif",
1368
- woff: "font/woff",
1369
- woff2: "font/woff2",
1370
- ttf: "font/ttf",
1371
- otf: "font/otf",
1372
- pdf: "application/pdf",
1373
- wasm: "application/wasm"
1374
- };
1381
+ createSite = grouped(function createSite2(db, opts) {
1382
+ const name = normalizeSiteName(opts.name);
1383
+ if (getSiteByName(db, name))
1384
+ throw new MhError("conflict", `site name already exists: ${name}`);
1385
+ const id = newId("site", name);
1386
+ const first = emit(db, "sites", id, "name", name);
1387
+ emit(db, "sites", id, "created_hlc", first.hlc);
1388
+ if (opts.title !== undefined)
1389
+ emit(db, "sites", id, "title", opts.title);
1390
+ return getSite(db, id);
1391
+ });
1392
+ updateSite = grouped(function updateSite2(db, id, opts) {
1393
+ if (!getSite(db, id))
1394
+ throw new MhError("not_found", `no such site: ${id}`);
1395
+ if (opts.name !== undefined) {
1396
+ const name = normalizeSiteName(opts.name);
1397
+ const dup = getSiteByName(db, name);
1398
+ if (dup && dup.id !== id)
1399
+ throw new MhError("conflict", `site name already exists: ${name}`);
1400
+ emit(db, "sites", id, "name", name);
1401
+ }
1402
+ if (opts.title !== undefined)
1403
+ emit(db, "sites", id, "title", opts.title);
1404
+ return getSite(db, id);
1405
+ });
1406
+ deleteSite = grouped(function deleteSite2(db, id) {
1407
+ if (!getSite(db, id))
1408
+ return false;
1409
+ const files = db.query("SELECT id FROM site_files WHERE site_id = ? AND __deleted = 0").all(id);
1410
+ for (const f3 of files)
1411
+ emit(db, "site_files", f3.id, "__deleted", 1);
1412
+ emit(db, "sites", id, "__deleted", 1);
1413
+ return true;
1414
+ });
1415
+ deleteFile = grouped(function deleteFile2(db, siteId, path) {
1416
+ const id = fileIdFor(db, siteId, normalizeSitePath(path));
1417
+ if (!id)
1418
+ return false;
1419
+ const live = db.query("SELECT __deleted AS d FROM site_files WHERE id = ?").get(id);
1420
+ if (!live || live.d)
1421
+ return false;
1422
+ emit(db, "site_files", id, "__deleted", 1);
1423
+ return true;
1424
+ });
1375
1425
  });
1376
1426
 
1377
1427
  // node_modules/sisteransi/src/index.js
@@ -2863,7 +2913,7 @@ async function runMain(cmd, opts = {}) {
2863
2913
  // package.json
2864
2914
  var package_default = {
2865
2915
  name: "@tensorix/metahub",
2866
- version: "0.1.10",
2916
+ version: "0.1.11",
2867
2917
  description: "Metahub core library and CLI",
2868
2918
  license: "AGPL-3.0-only",
2869
2919
  type: "module",
@@ -2897,6 +2947,7 @@ var package_default = {
2897
2947
  },
2898
2948
  dependencies: {
2899
2949
  "@clack/prompts": "^1.5.0",
2950
+ "@sqlite.org/sqlite-wasm": "^3.53.0-build1",
2900
2951
  citty: "^0.1.6",
2901
2952
  "highlight.js": "^11.11.1",
2902
2953
  preact: "^10.29.2",
@@ -2905,6 +2956,7 @@ var package_default = {
2905
2956
  zod: "^4.4.3"
2906
2957
  },
2907
2958
  devDependencies: {
2959
+ "@happy-dom/global-registrator": "^20.10.2",
2908
2960
  "@types/bun": "latest",
2909
2961
  "@types/turndown": "^5.0.6",
2910
2962
  typescript: "^5"
@@ -2924,6 +2976,8 @@ CREATE TABLE IF NOT EXISTS meta (
2924
2976
  );
2925
2977
 
2926
2978
  -- CRDT oplog: one row per field assignment. Source of truth for sync.
2979
+ -- txn groups the changes of one logical mutation for history rendering
2980
+ -- (nullable: legacy rows and changes from pre-txn peers have none).
2927
2981
  CREATE TABLE IF NOT EXISTS crdt_changes (
2928
2982
  hlc TEXT NOT NULL,
2929
2983
  node_id TEXT NOT NULL,
@@ -2931,9 +2985,14 @@ CREATE TABLE IF NOT EXISTS crdt_changes (
2931
2985
  row_id TEXT NOT NULL,
2932
2986
  col TEXT NOT NULL,
2933
2987
  value TEXT,
2988
+ txn TEXT,
2934
2989
  PRIMARY KEY (dataset, row_id, col, hlc)
2935
2990
  );
2936
2991
  CREATE INDEX IF NOT EXISTS idx_changes_hlc ON crdt_changes(hlc);
2992
+ -- Serves history's "every block ever attached to this doc" lookup. Partial so
2993
+ -- it never indexes the large values other datasets carry (e.g. site file bodies).
2994
+ CREATE INDEX IF NOT EXISTS idx_changes_docref ON crdt_changes(value)
2995
+ WHERE dataset = 'doc_blocks' AND col = 'doc_id';
2937
2996
 
2938
2997
  -- Per-peer replication cursors (SQLite rowids: monotonic in insertion order,
2939
2998
  -- so no change is ever skipped regardless of HLC/clock skew). Outbound side of
@@ -3073,47 +3132,6 @@ init_crdt();
3073
3132
  init_ids();
3074
3133
  init_crdt();
3075
3134
  init_errors();
3076
- function createDatabase(db, opts) {
3077
- const id = newId("db", opts.name);
3078
- const first = emit(db, "databases", id, "name", opts.name);
3079
- emit(db, "databases", id, "created_hlc", first.hlc);
3080
- if (opts.icon !== undefined)
3081
- emit(db, "databases", id, "icon", opts.icon);
3082
- return getDatabase(db, id);
3083
- }
3084
- function updateDatabase(db, id, fields) {
3085
- if (!getDatabase(db, id))
3086
- throw new MhError("not_found", `no such database: ${id}`);
3087
- if (fields.name !== undefined)
3088
- emit(db, "databases", id, "name", fields.name);
3089
- if (fields.icon !== undefined)
3090
- emit(db, "databases", id, "icon", fields.icon);
3091
- return getDatabase(db, id);
3092
- }
3093
- function getDatabase(db, id) {
3094
- return db.query("SELECT id, name, icon, created_hlc FROM databases WHERE id = ? AND __deleted = 0").get(id);
3095
- }
3096
- function listDatabases(db) {
3097
- return db.query("SELECT id, name, icon, created_hlc FROM databases WHERE __deleted = 0 ORDER BY created_hlc").all();
3098
- }
3099
- function deleteDatabase(db, id) {
3100
- if (!getDatabase(db, id))
3101
- return false;
3102
- emit(db, "databases", id, "__deleted", 1);
3103
- const tombstone = (table) => {
3104
- for (const r3 of liveChildren(db, table, "database_id", id))
3105
- emit(db, table, r3, "__deleted", 1);
3106
- };
3107
- tombstone("properties");
3108
- tombstone("records");
3109
- for (const d2 of liveChildren(db, "documents", "database_id", id))
3110
- emit(db, "documents", d2, "database_id", null);
3111
- db.query("DELETE FROM meta WHERE key = 'current_db' AND value = ?").run(id);
3112
- return true;
3113
- }
3114
- function liveChildren(db, table, col, parentId) {
3115
- return db.query(`SELECT id FROM ${table} WHERE ${col} = ? AND __deleted = 0`).all(parentId).map((r3) => r3.id);
3116
- }
3117
3135
 
3118
3136
  // src/core/properties.ts
3119
3137
  init_ids();
@@ -3161,6 +3179,8 @@ var PROP_TYPES = new Set([
3161
3179
  "relation",
3162
3180
  "url"
3163
3181
  ]);
3182
+ var PROPERTY_COLS = ["id", "database_id", "name", "type", "config", "position"];
3183
+ var PROPERTY_SELECT = PROPERTY_COLS.join(", ");
3164
3184
  function validateConfig(type, config) {
3165
3185
  if (type === "select" || type === "multi_select") {
3166
3186
  const opts = config?.options;
@@ -3176,7 +3196,7 @@ function nextPosition(db, databaseId) {
3176
3196
  const row = db.query("SELECT MAX(position) AS m FROM properties WHERE database_id = ? AND __deleted = 0").get(databaseId);
3177
3197
  return (row.m ?? 0) + 1;
3178
3198
  }
3179
- function addProperty(db, databaseId, opts) {
3199
+ var addProperty = grouped(function addProperty2(db, databaseId, opts) {
3180
3200
  if (!getDatabase(db, databaseId))
3181
3201
  throw new MhError("not_found", `no such database: ${databaseId}`);
3182
3202
  if (!PROP_TYPES.has(opts.type))
@@ -3192,21 +3212,21 @@ function addProperty(db, databaseId, opts) {
3192
3212
  if (opts.type === "relation" || opts.indexed === true)
3193
3213
  ensurePropIndex(db, databaseId, id);
3194
3214
  return getProperty(db, id);
3195
- }
3215
+ });
3196
3216
  function getProperty(db, id) {
3197
- const row = db.query("SELECT id, database_id, name, type, config, position FROM properties WHERE id = ? AND __deleted = 0").get(id);
3217
+ const row = db.query(`SELECT ${PROPERTY_SELECT} FROM properties WHERE id = ? AND __deleted = 0`).get(id);
3198
3218
  if (!row)
3199
3219
  return null;
3200
3220
  return { ...row, config: row.config ? JSON.parse(row.config) : null };
3201
3221
  }
3202
3222
  function listProperties(db, databaseId) {
3203
- const rows = db.query("SELECT id, database_id, name, type, config, position FROM properties WHERE database_id = ? AND __deleted = 0 ORDER BY position").all(databaseId);
3223
+ const rows = db.query(`SELECT ${PROPERTY_SELECT} FROM properties WHERE database_id = ? AND __deleted = 0 ORDER BY position`).all(databaseId);
3204
3224
  return rows.map((r3) => ({
3205
3225
  ...r3,
3206
3226
  config: r3.config ? JSON.parse(r3.config) : null
3207
3227
  }));
3208
3228
  }
3209
- function updateProperty(db, id, fields) {
3229
+ var updateProperty = grouped(function updateProperty2(db, id, fields) {
3210
3230
  const cur = getProperty(db, id);
3211
3231
  if (!cur)
3212
3232
  throw new MhError("not_found", `no such property: ${id}`);
@@ -3232,7 +3252,7 @@ function updateProperty(db, id, fields) {
3232
3252
  emit(db, "records", r3.id, id, null);
3233
3253
  }
3234
3254
  return getProperty(db, id);
3235
- }
3255
+ });
3236
3256
  function setPropertyWidth(db, id, width) {
3237
3257
  const cur = getProperty(db, id);
3238
3258
  if (!cur)
@@ -3242,7 +3262,7 @@ function setPropertyWidth(db, id, width) {
3242
3262
  const w2 = Math.max(80, Math.min(2000, Math.round(width)));
3243
3263
  return updateProperty(db, id, { config: { ...cur.config ?? {}, width: w2 } });
3244
3264
  }
3245
- function removeProperty(db, id) {
3265
+ var removeProperty = grouped(function removeProperty2(db, id) {
3246
3266
  const prop = getProperty(db, id);
3247
3267
  if (!prop)
3248
3268
  return false;
@@ -3251,6 +3271,76 @@ function removeProperty(db, id) {
3251
3271
  for (const r3 of rows)
3252
3272
  emit(db, "records", r3.id, id, undefined);
3253
3273
  return true;
3274
+ });
3275
+
3276
+ // src/core/databases.ts
3277
+ var DATABASE_COLS = ["id", "name", "icon", "created_hlc"];
3278
+ var DATABASE_SELECT = DATABASE_COLS.join(", ");
3279
+ var createDatabase = grouped(function createDatabase2(db, opts) {
3280
+ const id = newId("db", opts.name);
3281
+ const first = emit(db, "databases", id, "name", opts.name);
3282
+ emit(db, "databases", id, "created_hlc", first.hlc);
3283
+ if (opts.icon !== undefined)
3284
+ emit(db, "databases", id, "icon", opts.icon);
3285
+ return getDatabase(db, id);
3286
+ });
3287
+ var updateDatabase = grouped(function updateDatabase2(db, id, fields) {
3288
+ if (!getDatabase(db, id))
3289
+ throw new MhError("not_found", `no such database: ${id}`);
3290
+ if (fields.name !== undefined)
3291
+ emit(db, "databases", id, "name", fields.name);
3292
+ if (fields.icon !== undefined)
3293
+ emit(db, "databases", id, "icon", fields.icon);
3294
+ return getDatabase(db, id);
3295
+ });
3296
+ var duplicateDatabase = grouped(function duplicateDatabase2(db, id, opts = {}) {
3297
+ const src2 = getDatabase(db, id);
3298
+ if (!src2)
3299
+ throw new MhError("not_found", `no such database: ${id}`);
3300
+ const dup = createDatabase(db, {
3301
+ name: opts.name ?? src2.name,
3302
+ icon: opts.icon ?? src2.icon ?? undefined
3303
+ });
3304
+ const propIdMap = new Map;
3305
+ for (const p of listProperties(db, id)) {
3306
+ const config = p.config?.database === id ? { ...p.config, database: dup.id } : p.config ?? undefined;
3307
+ const np = addProperty(db, dup.id, { name: p.name, type: p.type, config, position: p.position });
3308
+ propIdMap.set(p.id, np.id);
3309
+ }
3310
+ for (const r3 of listRecords(db, id)) {
3311
+ const data = {};
3312
+ for (const [pid, v2] of Object.entries(r3.cells)) {
3313
+ const nid = propIdMap.get(pid);
3314
+ if (nid)
3315
+ data[nid] = v2;
3316
+ }
3317
+ createRecord(db, dup.id, data);
3318
+ }
3319
+ return getDatabase(db, dup.id);
3320
+ });
3321
+ function getDatabase(db, id) {
3322
+ return db.query(`SELECT ${DATABASE_SELECT} FROM databases WHERE id = ? AND __deleted = 0`).get(id);
3323
+ }
3324
+ function listDatabases(db) {
3325
+ return db.query(`SELECT ${DATABASE_SELECT} FROM databases WHERE __deleted = 0 ORDER BY created_hlc`).all();
3326
+ }
3327
+ var deleteDatabase = grouped(function deleteDatabase2(db, id) {
3328
+ if (!getDatabase(db, id))
3329
+ return false;
3330
+ emit(db, "databases", id, "__deleted", 1);
3331
+ const tombstone = (table) => {
3332
+ for (const r3 of liveChildren(db, table, "database_id", id))
3333
+ emit(db, table, r3, "__deleted", 1);
3334
+ };
3335
+ tombstone("properties");
3336
+ tombstone("records");
3337
+ for (const d2 of liveChildren(db, "documents", "database_id", id))
3338
+ emit(db, "documents", d2, "database_id", null);
3339
+ db.query("DELETE FROM meta WHERE key = 'current_db' AND value = ?").run(id);
3340
+ return true;
3341
+ });
3342
+ function liveChildren(db, table, col, parentId) {
3343
+ return db.query(`SELECT id FROM ${table} WHERE ${col} = ? AND __deleted = 0`).all(parentId).map((r3) => r3.id);
3254
3344
  }
3255
3345
 
3256
3346
  // src/core/resolve.ts
@@ -3468,12 +3558,30 @@ function coerce(db, prop, value) {
3468
3558
  }
3469
3559
  }
3470
3560
  }
3561
+ function propsByName(props) {
3562
+ const byName = new Map;
3563
+ for (const p of props) {
3564
+ const k2 = p.name.toLowerCase();
3565
+ const list = byName.get(k2);
3566
+ if (list)
3567
+ list.push(p);
3568
+ else
3569
+ byName.set(k2, [p]);
3570
+ }
3571
+ return byName;
3572
+ }
3471
3573
  function resolveData(props, data) {
3472
3574
  const byId = new Map(props.map((p) => [p.id, p]));
3473
- const byName = new Map(props.map((p) => [p.name.toLowerCase(), p]));
3575
+ const byName = propsByName(props);
3474
3576
  const out = [];
3475
3577
  for (const [key, value] of Object.entries(data)) {
3476
- const prop = byId.get(key) ?? byName.get(key.toLowerCase());
3578
+ let prop = byId.get(key);
3579
+ if (!prop) {
3580
+ const matches = byName.get(key.toLowerCase()) ?? [];
3581
+ if (matches.length > 1)
3582
+ throw new MhError("ambiguous", `property name "${key}" matches ${matches.length} properties; use a property id`);
3583
+ prop = matches[0];
3584
+ }
3477
3585
  if (!prop)
3478
3586
  throw new MhError("not_found", `unknown property: ${key}`);
3479
3587
  out.push({ prop, value });
@@ -3484,12 +3592,15 @@ function rowToRecord(row, props) {
3484
3592
  const raw = JSON.parse(row.data || "{}");
3485
3593
  const byId = new Map(props.map((p) => [p.id, p]));
3486
3594
  const values = {};
3595
+ const cells = {};
3487
3596
  for (const [propId, v2] of Object.entries(raw)) {
3488
3597
  const p = byId.get(propId);
3489
- if (p)
3490
- values[p.name] = v2;
3598
+ if (!p)
3599
+ continue;
3600
+ values[p.name] = v2;
3601
+ cells[p.id] = v2;
3491
3602
  }
3492
- return { id: row.id, database_id: row.database_id, values };
3603
+ return { id: row.id, database_id: row.database_id, values, cells };
3493
3604
  }
3494
3605
  function deriveTitle(props, resolved, fallbackBase) {
3495
3606
  const text = resolved.find((r3) => r3.prop.type === "text" && typeof r3.value === "string" && r3.value);
@@ -3526,7 +3637,7 @@ function rebalanceRecordOrderKeys(db, databaseId) {
3526
3637
  emit(db, "records", row.id, "order_key", keys[i2]);
3527
3638
  });
3528
3639
  }
3529
- function backfillRecordOrderKeys(db, databaseId) {
3640
+ var backfillRecordOrderKeys = grouped(function backfillRecordOrderKeys2(db, databaseId) {
3530
3641
  const emitChange = canEmitOrderKeys(db);
3531
3642
  const dbRows = databaseId ? [{ database_id: databaseId }] : db.query(`SELECT DISTINCT database_id FROM records
3532
3643
  WHERE database_id IS NOT NULL AND __deleted = 0 AND order_key IS NULL`).all();
@@ -3540,8 +3651,8 @@ function backfillRecordOrderKeys(db, databaseId) {
3540
3651
  const keys = keysBetween(start, null, missing.length);
3541
3652
  missing.forEach((row, i2) => writeRecordOrderKey(db, row.id, keys[i2], emitChange));
3542
3653
  }
3543
- }
3544
- function createRecord(db, databaseId, data) {
3654
+ });
3655
+ var createRecord = grouped(function createRecord2(db, databaseId, data) {
3545
3656
  const dbRow = getDatabase(db, databaseId);
3546
3657
  if (!dbRow)
3547
3658
  throw new MhError("not_found", `no such database: ${databaseId}`);
@@ -3555,7 +3666,7 @@ function createRecord(db, databaseId, data) {
3555
3666
  for (const { prop, value } of resolved)
3556
3667
  emit(db, "records", id, prop.id, coerce(db, prop, value));
3557
3668
  return getRecord(db, id);
3558
- }
3669
+ });
3559
3670
  function getRecord(db, id) {
3560
3671
  const row = db.query("SELECT id, database_id, data FROM records WHERE id = ? AND __deleted = 0").get(id);
3561
3672
  if (!row)
@@ -3582,7 +3693,13 @@ function listRecords(db, databaseId, opts = {}) {
3582
3693
  key = key.slice(1);
3583
3694
  }
3584
3695
  if (key !== "created" && key !== "created_hlc") {
3585
- const p = props.find((p2) => p2.name.toLowerCase() === key.toLowerCase() || p2.id === key);
3696
+ let p = props.find((p2) => p2.id === key);
3697
+ if (!p) {
3698
+ const matches = props.filter((q2) => q2.name.toLowerCase() === key.toLowerCase());
3699
+ if (matches.length > 1)
3700
+ throw new MhError("ambiguous", `sort field "${key}" matches ${matches.length} properties; use a property id`);
3701
+ p = matches[0];
3702
+ }
3586
3703
  if (!p)
3587
3704
  throw new MhError("not_found", `unknown sort field: ${key}`);
3588
3705
  sortProp = p;
@@ -3608,13 +3725,13 @@ function listRecords(db, databaseId, opts = {}) {
3608
3725
  }
3609
3726
  let rows = db.query(sql).all(...args).map((r3) => rowToRecord(r3, props));
3610
3727
  if (jsFilters.length) {
3611
- rows = rows.filter((r3) => jsFilters.every((f3) => JSON.stringify(r3.values[f3.prop.name] ?? null) === JSON.stringify(f3.value)));
3728
+ rows = rows.filter((r3) => jsFilters.every((f3) => JSON.stringify(r3.cells[f3.prop.id] ?? null) === JSON.stringify(f3.value)));
3612
3729
  if (opts.limit != null)
3613
3730
  rows = rows.slice(0, opts.limit);
3614
3731
  }
3615
3732
  return rows;
3616
3733
  }
3617
- function moveRecord(db, id, targetId, where) {
3734
+ var moveRecord = grouped(function moveRecord2(db, id, targetId, where) {
3618
3735
  if (where !== "before" && where !== "after")
3619
3736
  throw new MhError("invalid_input", `unknown move position: ${where}`);
3620
3737
  const src2 = db.query("SELECT id, database_id FROM records WHERE id = ? AND __deleted = 0").get(id);
@@ -3643,8 +3760,8 @@ function moveRecord(db, id, targetId, where) {
3643
3760
  }
3644
3761
  emit(db, "records", id, "order_key", keyBetween(left, right));
3645
3762
  return getRecord(db, id);
3646
- }
3647
- function updateRecord(db, id, data) {
3763
+ });
3764
+ var updateRecord = grouped(function updateRecord2(db, id, data) {
3648
3765
  const rec = db.query("SELECT id, database_id FROM records WHERE id = ? AND __deleted = 0").get(id);
3649
3766
  if (!rec)
3650
3767
  throw new MhError("not_found", `no such record: ${id}`);
@@ -3652,19 +3769,38 @@ function updateRecord(db, id, data) {
3652
3769
  for (const { prop, value } of resolved)
3653
3770
  emit(db, "records", id, prop.id, coerce(db, prop, value));
3654
3771
  return getRecord(db, id);
3655
- }
3656
- function deleteRecord(db, id) {
3772
+ });
3773
+ var deleteRecord = grouped(function deleteRecord2(db, id) {
3657
3774
  const rec = db.query("SELECT id FROM records WHERE id = ? AND __deleted = 0").get(id);
3658
3775
  if (!rec)
3659
3776
  return false;
3660
3777
  emit(db, "records", id, "__deleted", 1);
3661
3778
  return true;
3662
- }
3779
+ });
3663
3780
 
3664
3781
  // src/core/documents.ts
3665
3782
  init_ids();
3666
3783
  init_crdt();
3667
3784
  init_errors();
3785
+ var DOCUMENT_COLS = [
3786
+ "id",
3787
+ "title",
3788
+ "body",
3789
+ "database_id",
3790
+ "parent_id",
3791
+ "created_hlc",
3792
+ "order_key"
3793
+ ];
3794
+ var DOCUMENT_SELECT = DOCUMENT_COLS.join(", ");
3795
+ var DOC_SUMMARY_COLS = [
3796
+ "id",
3797
+ "title",
3798
+ "database_id",
3799
+ "parent_id",
3800
+ "created_hlc",
3801
+ "order_key"
3802
+ ];
3803
+ var DOC_SUMMARY_SELECT = DOC_SUMMARY_COLS.join(", ");
3668
3804
  function liveBlocks(db, docId) {
3669
3805
  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);
3670
3806
  }
@@ -3725,7 +3861,7 @@ function documentVersion(db, id) {
3725
3861
  OR (dataset = 'doc_blocks' AND row_id IN (SELECT id FROM doc_blocks WHERE doc_id = ?))`).get(id, id);
3726
3862
  return row.h ?? "";
3727
3863
  }
3728
- function createDocument(db, opts) {
3864
+ var createDocument = grouped(function createDocument2(db, opts) {
3729
3865
  const id = newId("doc", opts.title);
3730
3866
  const first = emit(db, "documents", id, "title", opts.title);
3731
3867
  emit(db, "documents", id, "created_hlc", first.hlc);
@@ -3740,13 +3876,13 @@ function createDocument(db, opts) {
3740
3876
  insertBlocks(db, id, blocks, null, null);
3741
3877
  }
3742
3878
  return getDocument(db, id);
3743
- }
3879
+ });
3744
3880
  function getDocument(db, id) {
3745
- return db.query("SELECT id, title, body, database_id, parent_id, created_hlc, order_key FROM documents WHERE id = ? AND __deleted = 0").get(id);
3881
+ return db.query(`SELECT ${DOCUMENT_SELECT} FROM documents WHERE id = ? AND __deleted = 0`).get(id);
3746
3882
  }
3747
3883
  var ORDER_BY = "ORDER BY order_key IS NULL, order_key, created_hlc, id";
3748
3884
  function listDocuments(db, opts = {}) {
3749
- const cols = "SELECT id, title, database_id, parent_id, created_hlc, order_key FROM documents WHERE __deleted = 0";
3885
+ const cols = `SELECT ${DOC_SUMMARY_SELECT} FROM documents WHERE __deleted = 0`;
3750
3886
  if (opts.parent_id !== undefined)
3751
3887
  return db.query(`${cols} AND parent_id = ? ${ORDER_BY}`).all(opts.parent_id);
3752
3888
  if (opts.database_id)
@@ -3797,11 +3933,11 @@ function backfillSiblings(db, parentId) {
3797
3933
  db.query("UPDATE documents SET order_key = ? WHERE id = ?").run(keys[i2], row.id);
3798
3934
  });
3799
3935
  }
3800
- function backfillDocumentOrderKeys(db) {
3936
+ var backfillDocumentOrderKeys = grouped(function backfillDocumentOrderKeys2(db) {
3801
3937
  const parents = db.query("SELECT DISTINCT parent_id FROM documents WHERE __deleted = 0 AND order_key IS NULL").all();
3802
3938
  for (const p of parents)
3803
3939
  backfillSiblings(db, p.parent_id);
3804
- }
3940
+ });
3805
3941
  function placeInSiblings(db, id, parentId, anchor) {
3806
3942
  let cur = parentId;
3807
3943
  while (cur) {
@@ -3833,7 +3969,7 @@ function placeInSiblings(db, id, parentId, anchor) {
3833
3969
  }
3834
3970
  emit(db, "documents", id, "order_key", key);
3835
3971
  }
3836
- function moveDocument(db, id, targetId, where) {
3972
+ var moveDocument = grouped(function moveDocument2(db, id, targetId, where) {
3837
3973
  const src2 = getDocument(db, id);
3838
3974
  if (!src2)
3839
3975
  throw new MhError("not_found", `no such document: ${id}`);
@@ -3847,8 +3983,26 @@ function moveDocument(db, id, targetId, where) {
3847
3983
  else
3848
3984
  placeInSiblings(db, id, target.parent_id, { targetId, where });
3849
3985
  return getDocument(db, id);
3850
- }
3851
- function updateDocument(db, id, fields, opts = {}) {
3986
+ });
3987
+ var duplicateDocument = grouped(function duplicateDocument2(db, id, opts = {}) {
3988
+ const src2 = getDocument(db, id);
3989
+ if (!src2)
3990
+ throw new MhError("not_found", `no such document: ${id}`);
3991
+ ensureBlocks(db, id);
3992
+ const blocks = liveBlocks(db, id);
3993
+ const parentId = opts.parentId !== undefined ? opts.parentId : src2.parent_id;
3994
+ const dup = createDocument(db, {
3995
+ title: opts.title ?? src2.title,
3996
+ database_id: src2.database_id ?? undefined,
3997
+ parent_id: parentId ?? undefined
3998
+ });
3999
+ if (blocks.length)
4000
+ insertBlocks(db, dup.id, blocks.map((b2) => ({ text: b2.text, blankAfter: b2.blank_after })), null, null);
4001
+ if (parentId === src2.parent_id)
4002
+ moveDocument(db, dup.id, src2.id, "after");
4003
+ return getDocument(db, dup.id);
4004
+ });
4005
+ var updateDocument = grouped(function updateDocument2(db, id, fields, opts = {}) {
3852
4006
  if (!getDocument(db, id))
3853
4007
  throw new MhError("not_found", `no such document: ${id}`);
3854
4008
  if (opts.ifMatch !== undefined && documentVersion(db, id) !== opts.ifMatch)
@@ -3867,8 +4021,8 @@ function updateDocument(db, id, fields, opts = {}) {
3867
4021
  reconcileBody(db, id, fields.body);
3868
4022
  }
3869
4023
  return getDocument(db, id);
3870
- }
3871
- function editDocument(db, id, opts) {
4024
+ });
4025
+ var editDocument = grouped(function editDocument2(db, id, opts) {
3872
4026
  if (!getDocument(db, id))
3873
4027
  throw new MhError("not_found", `no such document: ${id}`);
3874
4028
  if (opts.ifMatch !== undefined && documentVersion(db, id) !== opts.ifMatch)
@@ -3899,8 +4053,8 @@ function editDocument(db, id, opts) {
3899
4053
  const newBody = opts.replaceAll ? body.replaceAll(opts.old, opts.new) : body.replace(opts.old, opts.new);
3900
4054
  reconcileBody(db, id, newBody);
3901
4055
  return { id, changed: newBody !== body, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
3902
- }
3903
- function appendDocument(db, id, body) {
4056
+ });
4057
+ var appendDocument = grouped(function appendDocument2(db, id, body) {
3904
4058
  if (!getDocument(db, id))
3905
4059
  throw new MhError("not_found", `no such document: ${id}`);
3906
4060
  ensureBlocks(db, id);
@@ -3911,8 +4065,8 @@ function appendDocument(db, id, body) {
3911
4065
  insertBlocks(db, id, next, last, null);
3912
4066
  }
3913
4067
  return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
3914
- }
3915
- function prependDocument(db, id, body) {
4068
+ });
4069
+ var prependDocument = grouped(function prependDocument2(db, id, body) {
3916
4070
  if (!getDocument(db, id))
3917
4071
  throw new MhError("not_found", `no such document: ${id}`);
3918
4072
  ensureBlocks(db, id);
@@ -3923,8 +4077,8 @@ function prependDocument(db, id, body) {
3923
4077
  insertBlocks(db, id, next, null, first);
3924
4078
  }
3925
4079
  return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
3926
- }
3927
- function deleteDocument(db, id) {
4080
+ });
4081
+ var deleteDocument = grouped(function deleteDocument2(db, id) {
3928
4082
  if (!getDocument(db, id))
3929
4083
  return false;
3930
4084
  emit(db, "documents", id, "__deleted", 1);
@@ -3933,7 +4087,7 @@ function deleteDocument(db, id) {
3933
4087
  for (const c3 of liveChildIds(db, "documents", "parent_id", id))
3934
4088
  emit(db, "documents", c3, "parent_id", null);
3935
4089
  return true;
3936
- }
4090
+ });
3937
4091
  function liveChildIds(db, table, col, parentId) {
3938
4092
  return db.query(`SELECT id FROM ${table} WHERE ${col} = ? AND __deleted = 0`).all(parentId).map((r3) => r3.id);
3939
4093
  }
@@ -3949,11 +4103,7 @@ function countOccurrences(haystack, needle) {
3949
4103
  return n2;
3950
4104
  }
3951
4105
 
3952
- // src/core/db.ts
3953
- function ensureDirs() {
3954
- mkdirSync(metahubHome(), { recursive: true });
3955
- mkdirSync(cacheDir(), { recursive: true });
3956
- }
4106
+ // src/core/schema-init.ts
3957
4107
  function runSchema(db) {
3958
4108
  db.exec(CORE_SCHEMA);
3959
4109
  try {
@@ -4016,15 +4166,29 @@ function migrateDocuments(db) {
4016
4166
  db.exec("ALTER TABLE documents ADD COLUMN order_key TEXT");
4017
4167
  backfillDocumentOrderKeys(db);
4018
4168
  }
4019
- function openMetahub() {
4020
- ensureDirs();
4021
- const db = new Database(dbPath(), { create: true });
4022
- db.exec("PRAGMA journal_mode = WAL;");
4169
+ function migrateOplog(db) {
4170
+ if (!hasColumn(db, "crdt_changes", "txn"))
4171
+ db.exec("ALTER TABLE crdt_changes ADD COLUMN txn TEXT");
4172
+ }
4173
+ function initSchema(db) {
4023
4174
  runSchema(db);
4175
+ migrateOplog(db);
4024
4176
  migrateRecords(db);
4025
4177
  migratePeers(db);
4026
4178
  migrateDocBlocks(db);
4027
4179
  migrateDocuments(db);
4180
+ }
4181
+
4182
+ // src/core/db.ts
4183
+ function ensureDirs() {
4184
+ mkdirSync(metahubHome(), { recursive: true });
4185
+ mkdirSync(cacheDir(), { recursive: true });
4186
+ }
4187
+ function openMetahub() {
4188
+ ensureDirs();
4189
+ const db = new Database(dbPath(), { create: true });
4190
+ db.exec("PRAGMA journal_mode = WAL;");
4191
+ initSchema(db);
4028
4192
  return db;
4029
4193
  }
4030
4194
 
@@ -4051,6 +4215,9 @@ function print(data, pretty) {
4051
4215
  console.log(JSON.stringify(data, null, 2));
4052
4216
  }
4053
4217
  }
4218
+ function warn(message) {
4219
+ console.error(`warning: ${message}`);
4220
+ }
4054
4221
  var EXIT_CODES = {
4055
4222
  invalid_input: 2,
4056
4223
  not_found: 3,
@@ -4110,236 +4277,6 @@ var init_default = defineCommand({
4110
4277
  }
4111
4278
  });
4112
4279
 
4113
- // src/cli/commands/db.ts
4114
- var create = defineCommand({
4115
- meta: { name: "create", description: "Create a database" },
4116
- args: {
4117
- name: { type: "positional", required: true, description: "Database name" },
4118
- icon: { type: "string", description: "Optional icon" }
4119
- },
4120
- run: guard((args) => {
4121
- const row = createDatabase(openMetahub(), { name: args.name, icon: args.icon });
4122
- print(row, () => `${row.id} ${row.name}`);
4123
- })
4124
- });
4125
- var list = defineCommand({
4126
- meta: { name: "list", description: "List databases" },
4127
- run: guard(() => {
4128
- const rows = listDatabases(openMetahub());
4129
- print(rows, () => table(rows));
4130
- })
4131
- });
4132
- var get = defineCommand({
4133
- meta: { name: "get", description: "Show one database" },
4134
- args: { id: { type: "positional", required: true, description: "Database ref (id/prefix/name)" } },
4135
- run: guard((args) => {
4136
- const db = openMetahub();
4137
- print(getDatabase(db, resolveRef(db, args.id, { kind: "db" })));
4138
- })
4139
- });
4140
- var del = defineCommand({
4141
- meta: { name: "delete", description: "Delete a database" },
4142
- args: { id: { type: "positional", required: true, description: "Database ref (id/prefix/name)" } },
4143
- run: guard((args) => {
4144
- const db = openMetahub();
4145
- const id = resolveRef(db, args.id, { kind: "db" });
4146
- deleteDatabase(db, id);
4147
- print({ ok: true, deleted: id });
4148
- })
4149
- });
4150
- var db_default = defineCommand({
4151
- meta: { name: "db", description: "Manage databases (notion-like tables)" },
4152
- subCommands: { create, list, get, delete: del }
4153
- });
4154
-
4155
- // src/core/context.ts
4156
- var KEY = "current_db";
4157
- function getCurrentDatabase(db) {
4158
- const row = db.query("SELECT value FROM meta WHERE key = ?").get(KEY);
4159
- if (!row)
4160
- return null;
4161
- const found = getDatabase(db, row.value);
4162
- if (!found) {
4163
- clearCurrentDatabase(db);
4164
- return null;
4165
- }
4166
- return found;
4167
- }
4168
- function setCurrentDatabase(db, id) {
4169
- db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(KEY, id);
4170
- }
4171
- function clearCurrentDatabase(db) {
4172
- db.query("DELETE FROM meta WHERE key = ?").run(KEY);
4173
- }
4174
-
4175
- // src/cli/commands/use.ts
4176
- var use_default = defineCommand({
4177
- meta: {
4178
- name: "use",
4179
- description: "Set/show the current database (record, doc, prop default to it)"
4180
- },
4181
- args: {
4182
- database: {
4183
- type: "positional",
4184
- required: false,
4185
- description: "Database ref (id/prefix/name); omit to show current"
4186
- },
4187
- clear: { type: "boolean", description: "Clear the current database" }
4188
- },
4189
- run: guard((args) => {
4190
- const db = openMetahub();
4191
- if (args.clear) {
4192
- clearCurrentDatabase(db);
4193
- print({ ok: true, current: null }, () => "\u2713 current database cleared");
4194
- return;
4195
- }
4196
- if (args.database == null) {
4197
- const cur = getCurrentDatabase(db);
4198
- print({ current: cur }, () => cur ? `${cur.id} ${cur.name}` : "(no current database)");
4199
- return;
4200
- }
4201
- const id = resolveRef(db, args.database, { kind: "db" });
4202
- setCurrentDatabase(db, id);
4203
- const row = getDatabase(db, id);
4204
- print({ ok: true, current: row }, () => `\u2713 current database \u2192 ${row.name} (${row.id})`);
4205
- })
4206
- });
4207
-
4208
- // src/cli/commands/get.ts
4209
- var get_default = defineCommand({
4210
- meta: { name: "get", description: "Look up any entity by ref (id/prefix/name), type auto-detected" },
4211
- args: { ref: { type: "positional", required: true, description: "Ref (id/prefix/name)" } },
4212
- run: guard((args) => {
4213
- const db = openMetahub();
4214
- const { kind, id } = resolveEntity(db, args.ref);
4215
- switch (kind) {
4216
- case "db": {
4217
- const row = getDatabase(db, id);
4218
- return print(row, () => `${row.id} ${row.name}`);
4219
- }
4220
- case "prop": {
4221
- const row = getProperty(db, id);
4222
- return print(row, () => table([{ id: row.id, name: row.name, type: row.type }]));
4223
- }
4224
- case "rec":
4225
- return print(getRecord(db, id));
4226
- case "doc": {
4227
- const row = getDocument(db, id);
4228
- return print(row, () => row.body ?? "");
4229
- }
4230
- }
4231
- })
4232
- });
4233
-
4234
- // src/cli/input.ts
4235
- async function resolveValue2(raw) {
4236
- if (raw == null)
4237
- return;
4238
- if (raw === "@-")
4239
- return await Bun.stdin.text();
4240
- if (raw.startsWith("@"))
4241
- return await Bun.file(raw.slice(1)).text();
4242
- return raw;
4243
- }
4244
- async function resolveJson(raw) {
4245
- const text = await resolveValue2(raw);
4246
- if (text == null)
4247
- return;
4248
- return JSON.parse(text);
4249
- }
4250
-
4251
- // src/cli/refs.ts
4252
- function resolveDb(db, ref) {
4253
- if (ref != null)
4254
- return resolveRef(db, ref, { kind: "db" });
4255
- const cur = getCurrentDatabase(db);
4256
- if (!cur)
4257
- fail("no current database; pass <db> or run `mh use <db>`");
4258
- return cur.id;
4259
- }
4260
-
4261
- // src/cli/commands/prop.ts
4262
- function buildConfig(args, base) {
4263
- const config = { ...base };
4264
- if (typeof args.options === "string")
4265
- config.options = args.options.split(",").map((s2) => s2.trim()).filter(Boolean);
4266
- if (typeof args.target === "string")
4267
- config.database = args.target;
4268
- return Object.keys(config).length ? config : undefined;
4269
- }
4270
- var add = defineCommand({
4271
- meta: { name: "add", description: "Add a property (column) to a database" },
4272
- args: {
4273
- name: { type: "positional", required: true, description: "Property name" },
4274
- db: { type: "string", description: "Database ref (default: current)" },
4275
- type: { type: "string", required: true, description: "text|number|checkbox|select|multi_select|date|relation|url" },
4276
- options: { type: "string", description: "Comma list for select/multi_select" },
4277
- target: { type: "string", description: "Target database ref for relation" },
4278
- config: { type: "string", description: "Raw config JSON (@file/@- ok)" },
4279
- position: { type: "string", description: "Sort position" }
4280
- },
4281
- run: guard(async (args) => {
4282
- const db = openMetahub();
4283
- const databaseId = resolveDb(db, args.db);
4284
- const fromJson = await resolveJson(args.config);
4285
- const target = args.target != null ? resolveRef(db, args.target, { kind: "db" }) : undefined;
4286
- const config = buildConfig({ ...args, target }, fromJson);
4287
- const row = addProperty(db, databaseId, {
4288
- name: args.name,
4289
- type: args.type,
4290
- config,
4291
- position: args.position != null ? Number(args.position) : undefined
4292
- });
4293
- print(row, () => `${row.id} ${row.name} ${row.type}`);
4294
- })
4295
- });
4296
- var list2 = defineCommand({
4297
- meta: { name: "list", description: "List properties of a database" },
4298
- args: { database: { type: "positional", required: false, description: "Database ref (default: current)" } },
4299
- run: guard((args) => {
4300
- const db = openMetahub();
4301
- const rows = listProperties(db, resolveDb(db, args.database));
4302
- print(rows, () => table(rows.map((r3) => ({ id: r3.id, name: r3.name, type: r3.type, position: r3.position }))));
4303
- })
4304
- });
4305
- var update = defineCommand({
4306
- meta: { name: "update", description: "Update a property" },
4307
- args: {
4308
- id: { type: "positional", required: true, description: "Property ref (id/prefix/name)" },
4309
- name: { type: "string" },
4310
- options: { type: "string", description: "Comma list for select/multi_select" },
4311
- target: { type: "string", description: "Target database ref for relation" },
4312
- config: { type: "string", description: "Raw config JSON (@file/@- ok)" },
4313
- position: { type: "string" }
4314
- },
4315
- run: guard(async (args) => {
4316
- const db = openMetahub();
4317
- const fromJson = await resolveJson(args.config);
4318
- const target = args.target != null ? resolveRef(db, args.target, { kind: "db" }) : undefined;
4319
- const config = buildConfig({ ...args, target }, fromJson);
4320
- const row = updateProperty(db, resolveRef(db, args.id, { kind: "prop" }), {
4321
- name: args.name,
4322
- config,
4323
- position: args.position != null ? Number(args.position) : undefined
4324
- });
4325
- print(row);
4326
- })
4327
- });
4328
- var remove = defineCommand({
4329
- meta: { name: "remove", description: "Remove a property" },
4330
- args: { id: { type: "positional", required: true, description: "Property ref (id/prefix/name)" } },
4331
- run: guard((args) => {
4332
- const db = openMetahub();
4333
- const id = resolveRef(db, args.id, { kind: "prop" });
4334
- removeProperty(db, id);
4335
- print({ ok: true, removed: id });
4336
- })
4337
- });
4338
- var prop_default = defineCommand({
4339
- meta: { name: "prop", description: "Manage database properties (columns)" },
4340
- subCommands: { add, list: list2, update, remove }
4341
- });
4342
-
4343
4280
  // src/core/history.ts
4344
4281
  init_crdt();
4345
4282
  init_errors();
@@ -4350,12 +4287,20 @@ function hlcMillis(hlc) {
4350
4287
  function hlcIso(hlc) {
4351
4288
  return new Date(hlcMillis(hlc)).toISOString();
4352
4289
  }
4290
+ function revisionKind(txn) {
4291
+ if (txn?.startsWith("repair:"))
4292
+ return "repair";
4293
+ if (txn?.startsWith("revert:"))
4294
+ return "revert";
4295
+ return "user";
4296
+ }
4353
4297
  function clusterRevisions(changes) {
4354
4298
  const groups = [];
4355
4299
  let cur = [];
4356
4300
  for (const c3 of changes) {
4357
4301
  const prev = cur[cur.length - 1];
4358
- if (prev && (prev.node_id !== c3.node_id || hlcMillis(c3.hlc) - hlcMillis(prev.hlc) > REVISION_GAP_MS)) {
4302
+ const boundary = prev && (prev.txn !== null && c3.txn !== null ? prev.txn !== c3.txn : prev.node_id !== c3.node_id || hlcMillis(c3.hlc) - hlcMillis(prev.hlc) > REVISION_GAP_MS);
4303
+ if (boundary) {
4359
4304
  groups.push(cur);
4360
4305
  cur = [];
4361
4306
  }
@@ -4380,7 +4325,7 @@ function registersAt(changes, at) {
4380
4325
  return out;
4381
4326
  }
4382
4327
  function rowChanges(db, dataset, rowId) {
4383
- return db.query("SELECT hlc, node_id, dataset, row_id, col, value FROM crdt_changes WHERE dataset = ? AND row_id = ? ORDER BY hlc").all(dataset, rowId);
4328
+ return db.query(`SELECT ${CHANGE_SELECT} FROM crdt_changes WHERE dataset = ? AND row_id = ? ORDER BY hlc`).all(dataset, rowId);
4384
4329
  }
4385
4330
  function everBlockIds(db, docId) {
4386
4331
  return db.query("SELECT DISTINCT row_id FROM crdt_changes WHERE dataset = 'doc_blocks' AND col = 'doc_id' AND value = ?").all(JSON.stringify(docId)).map((r3) => r3.row_id);
@@ -4389,7 +4334,7 @@ function docChanges(db, docId) {
4389
4334
  const blocks = everBlockIds(db, docId);
4390
4335
  const placeholders = blocks.map(() => "?").join(",");
4391
4336
  const blockClause = blocks.length ? ` OR (dataset = 'doc_blocks' AND row_id IN (${placeholders}))` : "";
4392
- return db.query(`SELECT hlc, node_id, dataset, row_id, col, value FROM crdt_changes
4337
+ return db.query(`SELECT ${CHANGE_SELECT} FROM crdt_changes
4393
4338
  WHERE (dataset = 'documents' AND row_id = ?)${blockClause}
4394
4339
  ORDER BY hlc`).all(docId, ...blocks);
4395
4340
  }
@@ -4422,6 +4367,7 @@ function listDocumentRevisions(db, id) {
4422
4367
  version: last.hlc,
4423
4368
  at: hlcIso(last.hlc),
4424
4369
  node_id: last.node_id,
4370
+ kind: revisionKind(last.txn),
4425
4371
  changes: group.length,
4426
4372
  created,
4427
4373
  deleted,
@@ -4488,15 +4434,20 @@ function documentAtVersion(db, id, at) {
4488
4434
  version: changes[changes.length - 1].hlc
4489
4435
  };
4490
4436
  }
4491
- function revertDocument(db, id, to, opts = {}) {
4492
- const cur = getDocument(db, id);
4493
- if (!cur)
4437
+ var revertDocument = grouped(function revertDocument2(db, id, to, opts = {}) {
4438
+ let cur = getDocument(db, id);
4439
+ if (!cur && !db.query("SELECT 1 AS x FROM documents WHERE id = ?").get(id))
4494
4440
  throw new MhError("not_found", `no such document: ${id}`);
4495
4441
  if (opts.ifMatch !== undefined && documentVersion(db, id) !== opts.ifMatch)
4496
4442
  throw new MhError("stale", `stale: document changed since ${opts.ifMatch}; re-read first`);
4497
4443
  const past = documentAtVersion(db, id, to);
4498
4444
  if (past.deleted)
4499
4445
  throw new MhError("invalid_input", "target version is a deleted state; use doc delete instead");
4446
+ const undeleted = !cur;
4447
+ if (!cur) {
4448
+ emit(db, "documents", id, "__deleted", 0);
4449
+ cur = getDocument(db, id);
4450
+ }
4500
4451
  const fields = {};
4501
4452
  if (past.title !== cur.title)
4502
4453
  fields.title = past.title;
@@ -4506,43 +4457,98 @@ function revertDocument(db, id, to, opts = {}) {
4506
4457
  updateDocument(db, id, fields);
4507
4458
  return {
4508
4459
  id,
4509
- changed: Object.keys(fields).length > 0,
4460
+ changed: Object.keys(fields).length > 0 || undeleted,
4510
4461
  restored: past.version,
4511
- version: documentVersion(db, id)
4462
+ version: documentVersion(db, id),
4463
+ undeleted
4512
4464
  };
4513
- }
4465
+ }, "revert");
4514
4466
  var RECORD_META2 = new Set(["database_id", "created_hlc", "order_key", "__deleted"]);
4467
+ function recordRevisionOf(group) {
4468
+ const fields = new Set;
4469
+ let created = false;
4470
+ let deleted = false;
4471
+ let moved = false;
4472
+ for (const c3 of group) {
4473
+ if (c3.col === "created_hlc")
4474
+ created = true;
4475
+ else if (c3.col === "__deleted")
4476
+ deleted = flagSet(c3.value);
4477
+ else if (c3.col === "order_key")
4478
+ moved = true;
4479
+ else if (!RECORD_META2.has(c3.col))
4480
+ fields.add(c3.col);
4481
+ }
4482
+ const last = group[group.length - 1];
4483
+ return {
4484
+ version: last.hlc,
4485
+ at: hlcIso(last.hlc),
4486
+ node_id: last.node_id,
4487
+ kind: revisionKind(last.txn),
4488
+ changes: group.length,
4489
+ created,
4490
+ deleted,
4491
+ fields: [...fields],
4492
+ moved
4493
+ };
4494
+ }
4515
4495
  function listRecordRevisions(db, id) {
4516
4496
  const changes = rowChanges(db, "records", id);
4517
4497
  if (!changes.length)
4518
4498
  throw new MhError("not_found", `no such record: ${id}`);
4519
- return clusterRevisions(changes).map((group) => {
4520
- const fields = new Set;
4521
- let created = false;
4522
- let deleted = false;
4523
- let moved = false;
4524
- for (const c3 of group) {
4525
- if (c3.col === "created_hlc")
4526
- created = true;
4527
- else if (c3.col === "__deleted")
4528
- deleted = flagSet(c3.value);
4529
- else if (c3.col === "order_key")
4530
- moved = true;
4531
- else if (!RECORD_META2.has(c3.col))
4532
- fields.add(c3.col);
4499
+ return clusterRevisions(changes).map(recordRevisionOf).reverse();
4500
+ }
4501
+ function listDatabaseActivity(db, databaseId, opts = {}) {
4502
+ if (!db.query("SELECT 1 AS x FROM databases WHERE id = ?").get(databaseId))
4503
+ throw new MhError("not_found", `no such database: ${databaseId}`);
4504
+ const titleProp = db.query("SELECT id FROM properties WHERE database_id = ? AND type = 'text' AND __deleted = 0 ORDER BY position LIMIT 1").get(databaseId)?.id ?? null;
4505
+ const changes = db.query(`SELECT ${CHANGE_SELECT} FROM crdt_changes
4506
+ WHERE dataset = 'records' AND row_id IN (SELECT id FROM records WHERE database_id = ?)
4507
+ ORDER BY row_id, hlc`).all(databaseId);
4508
+ const out = [];
4509
+ let start = 0;
4510
+ for (let i2 = 1;i2 <= changes.length; i2++) {
4511
+ if (i2 !== changes.length && changes[i2].row_id === changes[start].row_id)
4512
+ continue;
4513
+ const rowId = changes[start].row_id;
4514
+ const state = new Map;
4515
+ for (const g3 of clusterRevisions(changes.slice(start, i2))) {
4516
+ const touched = new Set;
4517
+ for (const c3 of g3)
4518
+ if (!RECORD_META2.has(c3.col))
4519
+ touched.add(c3.col);
4520
+ const before = new Map;
4521
+ for (const col of touched)
4522
+ if (state.has(col))
4523
+ before.set(col, state.get(col));
4524
+ for (const c3 of g3) {
4525
+ if (RECORD_META2.has(c3.col))
4526
+ continue;
4527
+ if (c3.value === null)
4528
+ state.delete(c3.col);
4529
+ else
4530
+ state.set(c3.col, JSON.parse(c3.value));
4531
+ }
4532
+ const diffs = [];
4533
+ for (const col of touched) {
4534
+ const fc = { prop: col };
4535
+ if (before.has(col))
4536
+ fc.before = before.get(col);
4537
+ if (state.has(col))
4538
+ fc.after = state.get(col);
4539
+ diffs.push(fc);
4540
+ }
4541
+ out.push({
4542
+ ...recordRevisionOf(g3),
4543
+ record_id: rowId,
4544
+ record_title: titleProp && state.has(titleProp) ? String(state.get(titleProp) ?? "") : null,
4545
+ diffs
4546
+ });
4533
4547
  }
4534
- const last = group[group.length - 1];
4535
- return {
4536
- version: last.hlc,
4537
- at: hlcIso(last.hlc),
4538
- node_id: last.node_id,
4539
- changes: group.length,
4540
- created,
4541
- deleted,
4542
- fields: [...fields],
4543
- moved
4544
- };
4545
- }).reverse();
4548
+ start = i2;
4549
+ }
4550
+ out.sort((a2, b2) => a2.version < b2.version ? 1 : -1);
4551
+ return out.slice(0, opts.limit ?? 100);
4546
4552
  }
4547
4553
  function recordAtVersion(db, id, at) {
4548
4554
  if (!at)
@@ -4568,7 +4574,7 @@ function recordAtVersion(db, id, at) {
4568
4574
  version: changes[changes.length - 1].hlc
4569
4575
  };
4570
4576
  }
4571
- function revertRecord(db, id, to) {
4577
+ var revertRecord = grouped(function revertRecord2(db, id, to) {
4572
4578
  const cur = db.query("SELECT database_id, data, __deleted FROM records WHERE id = ?").get(id);
4573
4579
  if (!cur)
4574
4580
  throw new MhError("not_found", `no such record: ${id}`);
@@ -4593,7 +4599,127 @@ function revertRecord(db, id, to) {
4593
4599
  if (undeleted)
4594
4600
  emit(db, "records", id, "__deleted", 0);
4595
4601
  return { id, changed: fields.length > 0 || undeleted, fields, undeleted, restored: past.version };
4602
+ }, "revert");
4603
+ var PROP_DEF_COLS = ["name", "type", "config", "position"];
4604
+ function listPropertyRevisions(db, id) {
4605
+ const changes = rowChanges(db, "properties", id);
4606
+ if (!changes.length)
4607
+ throw new MhError("not_found", `no such property: ${id}`);
4608
+ const clearedByTxn = new Map(db.query("SELECT txn, COUNT(*) AS n FROM crdt_changes WHERE dataset = 'records' AND col = ? AND txn IS NOT NULL GROUP BY txn").all(id).map((r3) => [r3.txn, r3.n]));
4609
+ return clusterRevisions(changes).map((group) => {
4610
+ const fields = new Set;
4611
+ let created = false;
4612
+ let deleted = false;
4613
+ for (const c3 of group) {
4614
+ if (c3.col === "database_id")
4615
+ created = true;
4616
+ else if (c3.col === "__deleted")
4617
+ deleted = flagSet(c3.value);
4618
+ else if (PROP_DEF_COLS.includes(c3.col))
4619
+ fields.add(c3.col);
4620
+ }
4621
+ const last = group[group.length - 1];
4622
+ return {
4623
+ version: last.hlc,
4624
+ at: hlcIso(last.hlc),
4625
+ node_id: last.node_id,
4626
+ kind: revisionKind(last.txn),
4627
+ changes: group.length,
4628
+ created,
4629
+ deleted,
4630
+ fields: [...fields],
4631
+ cells_cleared: last.txn ? clearedByTxn.get(last.txn) ?? 0 : 0
4632
+ };
4633
+ }).reverse();
4596
4634
  }
4635
+ function propertyAtVersion(db, id, at) {
4636
+ if (!at)
4637
+ throw new MhError("invalid_input", "missing version cutoff");
4638
+ const changes = rowChanges(db, "properties", id).filter((c3) => c3.hlc <= at);
4639
+ if (!changes.length)
4640
+ throw new MhError("not_found", `no version of property ${id} at or before ${at}`);
4641
+ const regs = registersAt(changes, at);
4642
+ const reg = (col) => {
4643
+ const r3 = regs.get(col);
4644
+ return r3?.value == null ? undefined : JSON.parse(r3.value);
4645
+ };
4646
+ return {
4647
+ id,
4648
+ database_id: reg("database_id") ?? null,
4649
+ name: reg("name") ?? "",
4650
+ type: reg("type") ?? "text",
4651
+ config: reg("config") ?? null,
4652
+ position: reg("position") ?? null,
4653
+ deleted: regs.get("__deleted") ? flagSet(regs.get("__deleted").value) : false,
4654
+ version: changes[changes.length - 1].hlc
4655
+ };
4656
+ }
4657
+ var revertProperty = grouped(function revertProperty2(db, id, to) {
4658
+ const cur = db.query("SELECT database_id, name, type, config, position, __deleted FROM properties WHERE id = ?").get(id);
4659
+ if (!cur)
4660
+ throw new MhError("not_found", `no such property: ${id}`);
4661
+ const past = propertyAtVersion(db, id, to);
4662
+ if (past.deleted)
4663
+ throw new MhError("invalid_input", "target version is a deleted state; use prop remove instead");
4664
+ const curDef = {
4665
+ name: cur.name,
4666
+ type: cur.type,
4667
+ config: cur.config ? JSON.parse(cur.config) : null,
4668
+ position: cur.position
4669
+ };
4670
+ const pastDef = {
4671
+ name: past.name,
4672
+ type: past.type,
4673
+ config: past.config,
4674
+ position: past.position
4675
+ };
4676
+ const fields = [];
4677
+ for (const col of PROP_DEF_COLS) {
4678
+ if (JSON.stringify(pastDef[col] ?? null) !== JSON.stringify(curDef[col] ?? null)) {
4679
+ emit(db, "properties", id, col, pastDef[col]);
4680
+ fields.push(col);
4681
+ }
4682
+ }
4683
+ const undeleted = cur.__deleted !== 0;
4684
+ if (undeleted)
4685
+ emit(db, "properties", id, "__deleted", 0);
4686
+ const schemaTxns = new Set(db.query("SELECT DISTINCT txn FROM crdt_changes WHERE dataset = 'properties' AND row_id = ? AND hlc > ? AND txn IS NOT NULL").all(id, to).map((r3) => r3.txn));
4687
+ const cellChanges = db.query("SELECT row_id, hlc, value, txn FROM crdt_changes WHERE dataset = 'records' AND col = ? ORDER BY row_id, hlc").all(id);
4688
+ const isCascade = (txn) => txn !== null && (schemaTxns.has(txn) || txn.startsWith("repair:"));
4689
+ let restored = 0;
4690
+ let skipped = 0;
4691
+ let i2 = 0;
4692
+ while (i2 < cellChanges.length) {
4693
+ const rowId = cellChanges[i2].row_id;
4694
+ let winner = cellChanges[i2];
4695
+ let lastUser = null;
4696
+ for (;i2 < cellChanges.length && cellChanges[i2].row_id === rowId; i2++) {
4697
+ winner = cellChanges[i2];
4698
+ if (!isCascade(winner.txn))
4699
+ lastUser = winner;
4700
+ }
4701
+ if (winner.hlc <= to)
4702
+ continue;
4703
+ if (!isCascade(winner.txn)) {
4704
+ skipped++;
4705
+ continue;
4706
+ }
4707
+ const target = lastUser ? lastUser.value : null;
4708
+ if (target === winner.value)
4709
+ continue;
4710
+ emit(db, "records", rowId, id, target === null ? undefined : JSON.parse(target));
4711
+ restored++;
4712
+ }
4713
+ return {
4714
+ id,
4715
+ changed: fields.length > 0 || undeleted || restored > 0,
4716
+ fields,
4717
+ restored_cells: restored,
4718
+ skipped_cells: skipped,
4719
+ undeleted,
4720
+ restored: past.version
4721
+ };
4722
+ }, "revert");
4597
4723
  function recordFieldHistory(db, id, propId) {
4598
4724
  const rows = db.query("SELECT hlc, node_id, value FROM crdt_changes WHERE dataset = 'records' AND row_id = ? AND col = ? ORDER BY hlc DESC").all(id, propId);
4599
4725
  return rows.map((r3) => ({
@@ -4605,6 +4731,356 @@ function recordFieldHistory(db, id, propId) {
4605
4731
  }));
4606
4732
  }
4607
4733
 
4734
+ // src/core/context.ts
4735
+ var KEY = "current_db";
4736
+ function getCurrentDatabase(db) {
4737
+ const row = db.query("SELECT value FROM meta WHERE key = ?").get(KEY);
4738
+ if (!row)
4739
+ return null;
4740
+ const found = getDatabase(db, row.value);
4741
+ if (!found) {
4742
+ clearCurrentDatabase(db);
4743
+ return null;
4744
+ }
4745
+ return found;
4746
+ }
4747
+ function setCurrentDatabase(db, id) {
4748
+ db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(KEY, id);
4749
+ }
4750
+ function clearCurrentDatabase(db) {
4751
+ db.query("DELETE FROM meta WHERE key = ?").run(KEY);
4752
+ }
4753
+
4754
+ // src/cli/refs.ts
4755
+ function resolveDb(db, ref) {
4756
+ if (ref != null)
4757
+ return resolveRef(db, ref, { kind: "db" });
4758
+ const cur = getCurrentDatabase(db);
4759
+ if (!cur)
4760
+ fail("no current database; pass <db> or run `mh use <db>`");
4761
+ return cur.id;
4762
+ }
4763
+
4764
+ // src/cli/commands/db.ts
4765
+ var create = defineCommand({
4766
+ meta: { name: "create", description: "Create a database" },
4767
+ args: {
4768
+ name: { type: "positional", required: true, description: "Database name" },
4769
+ icon: { type: "string", description: "Optional icon" }
4770
+ },
4771
+ run: guard((args) => {
4772
+ const row = createDatabase(openMetahub(), { name: args.name, icon: args.icon });
4773
+ print(row, () => `${row.id} ${row.name}`);
4774
+ })
4775
+ });
4776
+ var list = defineCommand({
4777
+ meta: { name: "list", description: "List databases" },
4778
+ run: guard(() => {
4779
+ const rows = listDatabases(openMetahub());
4780
+ print(rows, () => table(rows));
4781
+ })
4782
+ });
4783
+ var get = defineCommand({
4784
+ meta: { name: "get", description: "Show one database" },
4785
+ args: { id: { type: "positional", required: true, description: "Database ref (id/prefix/name)" } },
4786
+ run: guard((args) => {
4787
+ const db = openMetahub();
4788
+ print(getDatabase(db, resolveRef(db, args.id, { kind: "db" })));
4789
+ })
4790
+ });
4791
+ var duplicate = defineCommand({
4792
+ meta: { name: "duplicate", description: "Copy a database whole \u2014 schema and all records" },
4793
+ args: {
4794
+ id: { type: "positional", required: true, description: "Database ref (id/prefix/name)" },
4795
+ name: { type: "string", description: "Name for the copy (defaults to the source name)" }
4796
+ },
4797
+ run: guard((args) => {
4798
+ const db = openMetahub();
4799
+ const row = duplicateDatabase(db, resolveRef(db, args.id, { kind: "db" }), { name: args.name });
4800
+ print(row, () => `${row.id} ${row.name}`);
4801
+ })
4802
+ });
4803
+ function fmtCell(v2) {
4804
+ const s2 = v2 === undefined || v2 === null || v2 === "" ? "(empty)" : typeof v2 === "string" ? v2.replaceAll(`
4805
+ `, "\u23CE") : JSON.stringify(v2);
4806
+ return s2.length > 40 ? s2.slice(0, 39) + "\u2026" : s2;
4807
+ }
4808
+ function activitySummary(e2, names) {
4809
+ const parts = [];
4810
+ if (e2.kind !== "user")
4811
+ parts.push(`[${e2.kind}]`);
4812
+ if (e2.deleted)
4813
+ parts.push("deleted");
4814
+ if (e2.created) {
4815
+ const vals = e2.diffs.filter((d2) => ("after" in d2)).map((d2) => `${names.get(d2.prop) ?? d2.prop}: ${fmtCell(d2.after)}`);
4816
+ parts.push("created" + (vals.length ? ` (${vals.join(", ")})` : ""));
4817
+ } else if (e2.diffs.length) {
4818
+ parts.push(e2.diffs.map((d2) => `${names.get(d2.prop) ?? d2.prop}: ${fmtCell(d2.before)} \u2192 ${fmtCell(d2.after)}`).join("; "));
4819
+ }
4820
+ if (e2.moved && !parts.length)
4821
+ parts.push("moved");
4822
+ return parts.join("; ") || "metadata";
4823
+ }
4824
+ var activity = defineCommand({
4825
+ meta: {
4826
+ name: "activity",
4827
+ description: "Recent changes across ALL records of a database, newest first"
4828
+ },
4829
+ args: {
4830
+ id: { type: "positional", required: false, description: "Database ref (default: current)" },
4831
+ limit: { type: "string", description: "Max entries (default 100)" }
4832
+ },
4833
+ run: guard((args) => {
4834
+ const db = openMetahub();
4835
+ const databaseId = resolveDb(db, args.id);
4836
+ const rows = listDatabaseActivity(db, databaseId, {
4837
+ limit: args.limit != null ? Number(args.limit) : undefined
4838
+ });
4839
+ const names = new Map(listProperties(db, databaseId).map((p) => [p.id, p.name]));
4840
+ print(rows, () => table(rows.map((e2) => ({
4841
+ version: e2.version,
4842
+ at: e2.at,
4843
+ node: e2.node_id,
4844
+ record: e2.record_title || e2.record_id,
4845
+ summary: activitySummary(e2, names)
4846
+ }))));
4847
+ })
4848
+ });
4849
+ var del = defineCommand({
4850
+ meta: { name: "delete", description: "Delete a database" },
4851
+ args: { id: { type: "positional", required: true, description: "Database ref (id/prefix/name)" } },
4852
+ run: guard((args) => {
4853
+ const db = openMetahub();
4854
+ const id = resolveRef(db, args.id, { kind: "db" });
4855
+ deleteDatabase(db, id);
4856
+ print({ ok: true, deleted: id });
4857
+ })
4858
+ });
4859
+ var db_default = defineCommand({
4860
+ meta: { name: "db", description: "Manage databases (notion-like tables)" },
4861
+ subCommands: { create, list, get, duplicate, activity, delete: del }
4862
+ });
4863
+
4864
+ // src/cli/commands/use.ts
4865
+ var use_default = defineCommand({
4866
+ meta: {
4867
+ name: "use",
4868
+ description: "Set/show the current database (record, doc, prop default to it)"
4869
+ },
4870
+ args: {
4871
+ database: {
4872
+ type: "positional",
4873
+ required: false,
4874
+ description: "Database ref (id/prefix/name); omit to show current"
4875
+ },
4876
+ clear: { type: "boolean", description: "Clear the current database" }
4877
+ },
4878
+ run: guard((args) => {
4879
+ const db = openMetahub();
4880
+ if (args.clear) {
4881
+ clearCurrentDatabase(db);
4882
+ print({ ok: true, current: null }, () => "\u2713 current database cleared");
4883
+ return;
4884
+ }
4885
+ if (args.database == null) {
4886
+ const cur = getCurrentDatabase(db);
4887
+ print({ current: cur }, () => cur ? `${cur.id} ${cur.name}` : "(no current database)");
4888
+ return;
4889
+ }
4890
+ const id = resolveRef(db, args.database, { kind: "db" });
4891
+ setCurrentDatabase(db, id);
4892
+ const row = getDatabase(db, id);
4893
+ print({ ok: true, current: row }, () => `\u2713 current database \u2192 ${row.name} (${row.id})`);
4894
+ })
4895
+ });
4896
+
4897
+ // src/cli/commands/get.ts
4898
+ var get_default = defineCommand({
4899
+ meta: { name: "get", description: "Look up any entity by ref (id/prefix/name), type auto-detected" },
4900
+ args: { ref: { type: "positional", required: true, description: "Ref (id/prefix/name)" } },
4901
+ run: guard((args) => {
4902
+ const db = openMetahub();
4903
+ const { kind, id } = resolveEntity(db, args.ref);
4904
+ switch (kind) {
4905
+ case "db": {
4906
+ const row = getDatabase(db, id);
4907
+ return print(row, () => `${row.id} ${row.name}`);
4908
+ }
4909
+ case "prop": {
4910
+ const row = getProperty(db, id);
4911
+ return print(row, () => table([{ id: row.id, name: row.name, type: row.type }]));
4912
+ }
4913
+ case "rec":
4914
+ return print(getRecord(db, id));
4915
+ case "doc": {
4916
+ const row = getDocument(db, id);
4917
+ return print(row, () => row.body ?? "");
4918
+ }
4919
+ }
4920
+ })
4921
+ });
4922
+
4923
+ // src/cli/input.ts
4924
+ async function resolveValue2(raw) {
4925
+ if (raw == null)
4926
+ return;
4927
+ if (raw === "@-")
4928
+ return await Bun.stdin.text();
4929
+ if (raw.startsWith("@"))
4930
+ return await Bun.file(raw.slice(1)).text();
4931
+ return raw;
4932
+ }
4933
+ async function resolveJson(raw) {
4934
+ const text = await resolveValue2(raw);
4935
+ if (text == null)
4936
+ return;
4937
+ return JSON.parse(text);
4938
+ }
4939
+
4940
+ // src/cli/commands/prop.ts
4941
+ init_errors();
4942
+ init_ids();
4943
+ function warnIfDuplicateName(db, databaseId, name) {
4944
+ const dups = listProperties(db, databaseId).filter((p) => p.name.toLowerCase() === name.toLowerCase());
4945
+ if (dups.length > 1)
4946
+ warn(`property name "${name}" now matches ${dups.length} properties in this database; ` + `name-keyed record reads/writes will fail as ambiguous \u2014 use property ids (${dups.map((p) => p.id).join(", ")})`);
4947
+ }
4948
+ function propIdMaybeDeleted(db, ref) {
4949
+ try {
4950
+ return resolveRef(db, ref, { kind: "prop" });
4951
+ } catch (e2) {
4952
+ if (errorCode(e2) === "not_found" && idKind(ref) === "prop")
4953
+ return ref;
4954
+ throw e2;
4955
+ }
4956
+ }
4957
+ function buildConfig(args, base) {
4958
+ const config = { ...base };
4959
+ if (typeof args.options === "string")
4960
+ config.options = args.options.split(",").map((s2) => s2.trim()).filter(Boolean);
4961
+ if (typeof args.target === "string")
4962
+ config.database = args.target;
4963
+ return Object.keys(config).length ? config : undefined;
4964
+ }
4965
+ var add = defineCommand({
4966
+ meta: { name: "add", description: "Add a property (column) to a database" },
4967
+ args: {
4968
+ name: { type: "positional", required: true, description: "Property name" },
4969
+ db: { type: "string", description: "Database ref (default: current)" },
4970
+ type: { type: "string", required: true, description: "text|number|checkbox|select|multi_select|date|relation|url" },
4971
+ options: { type: "string", description: "Comma list for select/multi_select" },
4972
+ target: { type: "string", description: "Target database ref for relation" },
4973
+ config: { type: "string", description: "Raw config JSON (@file/@- ok)" },
4974
+ position: { type: "string", description: "Sort position" }
4975
+ },
4976
+ run: guard(async (args) => {
4977
+ const db = openMetahub();
4978
+ const databaseId = resolveDb(db, args.db);
4979
+ const fromJson = await resolveJson(args.config);
4980
+ const target = args.target != null ? resolveRef(db, args.target, { kind: "db" }) : undefined;
4981
+ const config = buildConfig({ ...args, target }, fromJson);
4982
+ const row = addProperty(db, databaseId, {
4983
+ name: args.name,
4984
+ type: args.type,
4985
+ config,
4986
+ position: args.position != null ? Number(args.position) : undefined
4987
+ });
4988
+ warnIfDuplicateName(db, databaseId, row.name);
4989
+ print(row, () => `${row.id} ${row.name} ${row.type}`);
4990
+ })
4991
+ });
4992
+ var list2 = defineCommand({
4993
+ meta: { name: "list", description: "List properties of a database" },
4994
+ args: { database: { type: "positional", required: false, description: "Database ref (default: current)" } },
4995
+ run: guard((args) => {
4996
+ const db = openMetahub();
4997
+ const rows = listProperties(db, resolveDb(db, args.database));
4998
+ print(rows, () => table(rows.map((r3) => ({ id: r3.id, name: r3.name, type: r3.type, position: r3.position }))));
4999
+ })
5000
+ });
5001
+ var update = defineCommand({
5002
+ meta: { name: "update", description: "Update a property" },
5003
+ args: {
5004
+ id: { type: "positional", required: true, description: "Property ref (id/prefix/name)" },
5005
+ name: { type: "string" },
5006
+ options: { type: "string", description: "Comma list for select/multi_select" },
5007
+ target: { type: "string", description: "Target database ref for relation" },
5008
+ config: { type: "string", description: "Raw config JSON (@file/@- ok)" },
5009
+ position: { type: "string" }
5010
+ },
5011
+ run: guard(async (args) => {
5012
+ const db = openMetahub();
5013
+ const fromJson = await resolveJson(args.config);
5014
+ const target = args.target != null ? resolveRef(db, args.target, { kind: "db" }) : undefined;
5015
+ const config = buildConfig({ ...args, target }, fromJson);
5016
+ const row = updateProperty(db, resolveRef(db, args.id, { kind: "prop" }), {
5017
+ name: args.name,
5018
+ config,
5019
+ position: args.position != null ? Number(args.position) : undefined
5020
+ });
5021
+ if (args.name != null)
5022
+ warnIfDuplicateName(db, row.database_id, row.name);
5023
+ print(row);
5024
+ })
5025
+ });
5026
+ var remove = defineCommand({
5027
+ meta: { name: "remove", description: "Remove a property" },
5028
+ args: { id: { type: "positional", required: true, description: "Property ref (id/prefix/name)" } },
5029
+ run: guard((args) => {
5030
+ const db = openMetahub();
5031
+ const id = resolveRef(db, args.id, { kind: "prop" });
5032
+ removeProperty(db, id);
5033
+ print({ ok: true, removed: id });
5034
+ })
5035
+ });
5036
+ function propRevisionSummary(r3) {
5037
+ const parts = [];
5038
+ if (r3.kind !== "user")
5039
+ parts.push(`[${r3.kind}]`);
5040
+ if (r3.created)
5041
+ parts.push("created");
5042
+ if (r3.deleted)
5043
+ parts.push("removed");
5044
+ if (r3.fields.length)
5045
+ parts.push(r3.fields.join(", "));
5046
+ if (r3.cells_cleared)
5047
+ parts.push(`-${r3.cells_cleared} cell(s)`);
5048
+ return parts.join("; ") || "metadata";
5049
+ }
5050
+ var history = defineCommand({
5051
+ meta: { name: "history", description: "List a property's definition revisions (newest first)" },
5052
+ args: { id: { type: "positional", required: true, description: "Property ref (id/prefix/name)" } },
5053
+ run: guard((args) => {
5054
+ const db = openMetahub();
5055
+ const rows = listPropertyRevisions(db, propIdMaybeDeleted(db, args.id));
5056
+ print(rows, () => table(rows.map((r3) => ({
5057
+ version: r3.version,
5058
+ at: r3.at,
5059
+ node: r3.node_id,
5060
+ summary: propRevisionSummary(r3)
5061
+ }))));
5062
+ })
5063
+ });
5064
+ var revert = defineCommand({
5065
+ meta: {
5066
+ name: "revert",
5067
+ description: "Restore a property's definition AND the cells its type-change/removal cleared; user edits made since are kept"
5068
+ },
5069
+ args: {
5070
+ id: { type: "positional", required: true, description: "Property ref (id/prefix/name)" },
5071
+ to: { type: "string", required: true, description: "Version token from `prop history`" }
5072
+ },
5073
+ run: guard((args) => {
5074
+ const db = openMetahub();
5075
+ const r3 = revertProperty(db, propIdMaybeDeleted(db, args.id), args.to);
5076
+ print(r3, () => r3.changed ? `reverted ${r3.id} to ${r3.restored}` + (r3.fields.length ? ` (${r3.fields.join(", ")})` : "") + (r3.restored_cells ? ` +${r3.restored_cells} cell(s)` : "") + (r3.skipped_cells ? ` (${r3.skipped_cells} kept user edits)` : "") + (r3.undeleted ? " [undeleted]" : "") : `no change: ${r3.id} already matches ${r3.restored}`);
5077
+ })
5078
+ });
5079
+ var prop_default = defineCommand({
5080
+ meta: { name: "prop", description: "Manage database properties (columns)" },
5081
+ subCommands: { add, list: list2, update, history, revert, remove }
5082
+ });
5083
+
4608
5084
  // src/cli/commands/record.ts
4609
5085
  init_errors();
4610
5086
  init_ids();
@@ -4687,6 +5163,8 @@ var update2 = defineCommand({
4687
5163
  });
4688
5164
  function recordRevisionSummary(r3, names) {
4689
5165
  const parts = [];
5166
+ if (r3.kind !== "user")
5167
+ parts.push(`[${r3.kind}]`);
4690
5168
  if (r3.created)
4691
5169
  parts.push("created");
4692
5170
  if (r3.deleted)
@@ -4697,7 +5175,7 @@ function recordRevisionSummary(r3, names) {
4697
5175
  parts.push("moved");
4698
5176
  return parts.join("; ") || "metadata";
4699
5177
  }
4700
- var history = defineCommand({
5178
+ var history2 = defineCommand({
4701
5179
  meta: { name: "history", description: "List a record's revisions (newest first)" },
4702
5180
  args: {
4703
5181
  id: { type: "positional", required: true, description: "Record ref (id/prefix)" },
@@ -4729,7 +5207,7 @@ var history = defineCommand({
4729
5207
  }))));
4730
5208
  })
4731
5209
  });
4732
- var revert = defineCommand({
5210
+ var revert2 = defineCommand({
4733
5211
  meta: {
4734
5212
  name: "revert",
4735
5213
  description: "Restore a record's cells to a past version (recorded as a new revision)"
@@ -4758,13 +5236,24 @@ var del2 = defineCommand({
4758
5236
  });
4759
5237
  var record_default = defineCommand({
4760
5238
  meta: { name: "record", description: "Manage records (rows)" },
4761
- subCommands: { create: create2, list: list3, get: get2, update: update2, history, revert, delete: del2 }
5239
+ subCommands: { create: create2, list: list3, get: get2, update: update2, history: history2, revert: revert2, delete: del2 }
4762
5240
  });
4763
5241
 
4764
5242
  // src/cli/commands/doc.ts
5243
+ init_errors();
5244
+ init_ids();
4765
5245
  function docId(db, ref) {
4766
5246
  return resolveRef(db, ref, { kind: "doc" });
4767
5247
  }
5248
+ function docIdMaybeDeleted(db, ref) {
5249
+ try {
5250
+ return docId(db, ref);
5251
+ } catch (e2) {
5252
+ if (errorCode(e2) === "not_found" && idKind(ref) === "doc")
5253
+ return ref;
5254
+ throw e2;
5255
+ }
5256
+ }
4768
5257
  function docTree(rows) {
4769
5258
  if (rows.length === 0)
4770
5259
  return "(empty)";
@@ -4831,13 +5320,12 @@ var get3 = defineCommand({
4831
5320
  },
4832
5321
  run: guard((args) => {
4833
5322
  const db = openMetahub();
4834
- const id = docId(db, args.id);
4835
5323
  if (args.at !== undefined) {
4836
- const past = documentAtVersion(db, id, args.at);
5324
+ const past = documentAtVersion(db, docIdMaybeDeleted(db, args.id), args.at);
4837
5325
  print(past, () => past.body);
4838
5326
  return;
4839
5327
  }
4840
- const row = getDocument(db, id);
5328
+ const row = getDocument(db, docId(db, args.id));
4841
5329
  print(row, () => row.body ?? "");
4842
5330
  })
4843
5331
  });
@@ -4935,6 +5423,8 @@ var prepend = defineCommand({
4935
5423
  });
4936
5424
  function docRevisionSummary(r3) {
4937
5425
  const parts = [];
5426
+ if (r3.kind !== "user")
5427
+ parts.push(`[${r3.kind}]`);
4938
5428
  if (r3.created)
4939
5429
  parts.push("created");
4940
5430
  if (r3.deleted)
@@ -4947,12 +5437,12 @@ function docRevisionSummary(r3) {
4947
5437
  parts.push(`-${r3.blocks_deleted} block(s)`);
4948
5438
  return parts.join(", ") || "metadata";
4949
5439
  }
4950
- var history2 = defineCommand({
5440
+ var history3 = defineCommand({
4951
5441
  meta: { name: "history", description: "List a document's revisions (newest first)" },
4952
5442
  args: { id: { type: "positional", required: true, description: "Document ref (id/prefix/title)" } },
4953
5443
  run: guard((args) => {
4954
5444
  const db = openMetahub();
4955
- const rows = listDocumentRevisions(db, docId(db, args.id));
5445
+ const rows = listDocumentRevisions(db, docIdMaybeDeleted(db, args.id));
4956
5446
  print(rows, () => table(rows.map((r3) => ({
4957
5447
  version: r3.version,
4958
5448
  at: r3.at,
@@ -4961,7 +5451,7 @@ var history2 = defineCommand({
4961
5451
  }))));
4962
5452
  })
4963
5453
  });
4964
- var revert2 = defineCommand({
5454
+ var revert3 = defineCommand({
4965
5455
  meta: {
4966
5456
  name: "revert",
4967
5457
  description: "Restore title/body to a past version (recorded as a new revision)"
@@ -4973,8 +5463,22 @@ var revert2 = defineCommand({
4973
5463
  },
4974
5464
  run: guard((args) => {
4975
5465
  const db = openMetahub();
4976
- const r3 = revertDocument(db, docId(db, args.id), args.to, { ifMatch: args["if-match"] });
4977
- print(r3, () => r3.changed ? `reverted ${r3.id} to ${r3.restored}` : `no change: ${r3.id} already matches ${r3.restored}`);
5466
+ const r3 = revertDocument(db, docIdMaybeDeleted(db, args.id), args.to, {
5467
+ ifMatch: args["if-match"]
5468
+ });
5469
+ print(r3, () => r3.changed ? `reverted ${r3.id} to ${r3.restored}` + (r3.undeleted ? " [undeleted]" : "") : `no change: ${r3.id} already matches ${r3.restored}`);
5470
+ })
5471
+ });
5472
+ var duplicate2 = defineCommand({
5473
+ meta: { name: "duplicate", description: "Copy a document (title + all blocks) next to the source" },
5474
+ args: {
5475
+ id: { type: "positional", required: true, description: "Document ref (id/prefix/title)" },
5476
+ title: { type: "string", description: "Title for the copy (defaults to the source title)" }
5477
+ },
5478
+ run: guard((args) => {
5479
+ const db = openMetahub();
5480
+ const row = duplicateDocument(db, docId(db, args.id), { title: args.title });
5481
+ print({ id: row.id, title: row.title }, () => `${row.id} ${row.title}`);
4978
5482
  })
4979
5483
  });
4980
5484
  var del3 = defineCommand({
@@ -4989,7 +5493,7 @@ var del3 = defineCommand({
4989
5493
  });
4990
5494
  var doc_default = defineCommand({
4991
5495
  meta: { name: "doc", description: "Manage markdown documents" },
4992
- subCommands: { create: create3, list: list4, get: get3, read, update: update3, edit, append, prepend, history: history2, revert: revert2, delete: del3 }
5496
+ subCommands: { create: create3, list: list4, get: get3, read, update: update3, edit, append, prepend, history: history3, revert: revert3, duplicate: duplicate2, delete: del3 }
4993
5497
  });
4994
5498
 
4995
5499
  // src/cli/editor.ts
@@ -19896,12 +20400,15 @@ var ChangeSchema = exports_external.object({
19896
20400
  dataset: exports_external.string().describe("CRDT dataset, e.g. databases / records / documents"),
19897
20401
  row_id: exports_external.string().describe("Row identifier within the dataset"),
19898
20402
  col: exports_external.string().describe("Column / property identifier"),
19899
- value: exports_external.string().nullable().describe("JSON-encoded field value, or null")
20403
+ value: exports_external.string().nullable().describe("JSON-encoded field value, or null"),
20404
+ txn: exports_external.string().nullable().optional().describe("Change-group id (one logical mutation)")
19900
20405
  });
19901
20406
  var SyncRequestSchema = exports_external.object({
19902
20407
  node_id: exports_external.string().describe("Calling client's node id"),
19903
20408
  since: exports_external.number().describe("Server cursor the client last pulled"),
19904
- changes: exports_external.array(ChangeSchema).describe("Client's changes to push")
20409
+ changes: exports_external.array(ChangeSchema).describe("Client's changes to push"),
20410
+ limit: exports_external.number().optional().describe("Max changes to return (pagination for hydration)"),
20411
+ exclude_datasets: exports_external.array(exports_external.string()).optional().describe("Datasets to omit from the pull (partial replica)")
19905
20412
  });
19906
20413
  var SyncResponseSchema = exports_external.object({
19907
20414
  node_id: exports_external.string().describe("Server's node id"),
@@ -19936,10 +20443,11 @@ function getPeer(db, url2) {
19936
20443
  function setPeer(db, url2, c3) {
19937
20444
  db.query("INSERT INTO peers (url, pull_cursor, push_cursor) VALUES (?, ?, ?) ON CONFLICT(url) DO UPDATE SET pull_cursor = excluded.pull_cursor, push_cursor = excluded.push_cursor").run(url2, c3.pull_cursor, c3.push_cursor);
19938
20445
  }
19939
- async function syncWithPeer(db, url2, token) {
20446
+ async function syncWithPeer(db, url2, opts = {}) {
20447
+ const o3 = typeof opts === "string" ? { token: opts } : opts;
19940
20448
  const node = getNodeId(db);
19941
20449
  const peer = getPeer(db, url2);
19942
- const bearer = token ?? peer.token ?? undefined;
20450
+ const bearer = o3.token ?? peer.token ?? undefined;
19943
20451
  const toPush = changesAfterSeq(db, peer.push_cursor);
19944
20452
  const headers = { "content-type": "application/json" };
19945
20453
  if (bearer)
@@ -19947,7 +20455,13 @@ async function syncWithPeer(db, url2, token) {
19947
20455
  const res = await fetch(new URL(SYNC_PATH, url2), {
19948
20456
  method: "POST",
19949
20457
  headers,
19950
- body: JSON.stringify({ node_id: node, since: peer.pull_cursor, changes: toPush.changes })
20458
+ body: JSON.stringify({
20459
+ node_id: node,
20460
+ since: peer.pull_cursor,
20461
+ changes: toPush.changes,
20462
+ ...o3.pullLimit != null ? { limit: o3.pullLimit } : {},
20463
+ ...o3.excludeDatasets?.length ? { exclude_datasets: o3.excludeDatasets } : {}
20464
+ })
19951
20465
  });
19952
20466
  if (!res.ok)
19953
20467
  throw new MhError(res.status === 401 || res.status === 403 ? "auth" : "network", `sync failed: ${res.status} ${await res.text()}`);
@@ -20455,7 +20969,7 @@ function repairPass(db) {
20455
20969
  const applied = Object.values(fixed).reduce((a2, b2) => a2 + b2, 0);
20456
20970
  return { applied, fixed };
20457
20971
  }
20458
- function repairHub(db) {
20972
+ var repairHub = grouped(function repairHub2(db) {
20459
20973
  const fixed = {};
20460
20974
  let applied = 0;
20461
20975
  for (let i2 = 0;i2 < 20; i2++) {
@@ -20467,7 +20981,7 @@ function repairHub(db) {
20467
20981
  fixed[cat] = (fixed[cat] ?? 0) + n2;
20468
20982
  }
20469
20983
  return { applied, fixed, remaining: validateHub(db) };
20470
- }
20984
+ }, "repair");
20471
20985
 
20472
20986
  // src/core/snapshot.ts
20473
20987
  init_paths();
@@ -20643,6 +21157,113 @@ var restore_default = defineCommand({
20643
21157
  })
20644
21158
  });
20645
21159
 
21160
+ // src/core/compact.ts
21161
+ init_paths();
21162
+ init_errors();
21163
+ import { existsSync as existsSync2, readdirSync as readdirSync2, statSync, unlinkSync } from "fs";
21164
+ import { join as join6 } from "path";
21165
+ function dbBytes(db) {
21166
+ const pc = db.query("PRAGMA page_count").get();
21167
+ const ps = db.query("PRAGMA page_size").get();
21168
+ return pc.page_count * ps.page_size;
21169
+ }
21170
+ var COMPACTABLE = `
21171
+ hlc <= ?1
21172
+ AND rowid <> (SELECT MAX(rowid) FROM crdt_changes)
21173
+ AND EXISTS (
21174
+ SELECT 1 FROM crdt_changes k
21175
+ WHERE k.dataset = crdt_changes.dataset
21176
+ AND k.row_id = crdt_changes.row_id
21177
+ AND k.col = crdt_changes.col
21178
+ AND k.hlc > crdt_changes.hlc
21179
+ AND k.hlc <= ?1
21180
+ )`;
21181
+ function compactOplog(db, opts) {
21182
+ if (!Number.isFinite(opts.keepDays) || opts.keepDays < 0)
21183
+ throw new MhError("invalid_input", "keepDays must be a non-negative number");
21184
+ const now = opts.now ?? Date.now();
21185
+ const cutoff = formatHlc({
21186
+ millis: Math.max(0, now - opts.keepDays * 86400000),
21187
+ counter: 65535,
21188
+ node: "~"
21189
+ });
21190
+ const bytesBefore = dbBytes(db);
21191
+ const dryRun = opts.dryRun === true;
21192
+ let deleted;
21193
+ if (dryRun) {
21194
+ const row = db.query(`SELECT COUNT(*) AS n FROM crdt_changes WHERE ${COMPACTABLE}`).get(cutoff);
21195
+ deleted = row.n;
21196
+ } else {
21197
+ deleted = db.query(`DELETE FROM crdt_changes WHERE ${COMPACTABLE}`).run(cutoff).changes;
21198
+ }
21199
+ const kept = db.query("SELECT COUNT(*) AS n FROM crdt_changes").get().n - (dryRun ? deleted : 0);
21200
+ const blobs = gcBlobs(db, { dryRun });
21201
+ let bytesAfter = bytesBefore;
21202
+ if (!dryRun && opts.vacuum !== false) {
21203
+ try {
21204
+ db.exec("VACUUM");
21205
+ } catch {}
21206
+ bytesAfter = dbBytes(db);
21207
+ }
21208
+ return {
21209
+ cutoff,
21210
+ deleted_changes: deleted,
21211
+ kept_changes: kept,
21212
+ blobs_deleted: blobs.deleted,
21213
+ blob_bytes_freed: blobs.bytes,
21214
+ db_bytes_before: bytesBefore,
21215
+ db_bytes_after: bytesAfter,
21216
+ dry_run: dryRun
21217
+ };
21218
+ }
21219
+ function gcBlobs(db, opts = {}) {
21220
+ const dir = cacheDir();
21221
+ if (!existsSync2(dir))
21222
+ return { deleted: 0, bytes: 0 };
21223
+ const referenced = new Set;
21224
+ const collect = (rows) => {
21225
+ for (const r3 of rows) {
21226
+ if (r3.v == null)
21227
+ continue;
21228
+ try {
21229
+ const parsed = JSON.parse(r3.v);
21230
+ if (typeof parsed === "string")
21231
+ referenced.add(parsed);
21232
+ } catch {
21233
+ referenced.add(r3.v);
21234
+ }
21235
+ }
21236
+ };
21237
+ collect(db.query("SELECT DISTINCT value AS v FROM crdt_changes WHERE dataset = 'site_files' AND col = 'content'").all());
21238
+ collect(db.query("SELECT DISTINCT content AS v FROM site_files").all());
21239
+ let deleted = 0;
21240
+ let bytes = 0;
21241
+ for (const name of readdirSync2(dir)) {
21242
+ if (!/^[0-9a-f]{64}$/.test(name))
21243
+ continue;
21244
+ if (referenced.has(name))
21245
+ continue;
21246
+ const path = join6(dir, name);
21247
+ try {
21248
+ bytes += statSync(path).size;
21249
+ if (!opts.dryRun)
21250
+ unlinkSync(path);
21251
+ deleted++;
21252
+ } catch {}
21253
+ }
21254
+ return { deleted, bytes };
21255
+ }
21256
+ function compactEstimate(db, keepDays, now = Date.now()) {
21257
+ const cutoff = formatHlc({
21258
+ millis: Math.max(0, now - keepDays * 86400000),
21259
+ counter: 65535,
21260
+ node: "~"
21261
+ });
21262
+ const total = db.query("SELECT COUNT(*) AS n FROM crdt_changes").get().n;
21263
+ const compactable = db.query(`SELECT COUNT(*) AS n FROM crdt_changes WHERE ${COMPACTABLE}`).get(cutoff).n;
21264
+ return { total_changes: total, compactable_changes: compactable, db_bytes: dbBytes(db) };
21265
+ }
21266
+
20646
21267
  // src/cli/commands/doctor.ts
20647
21268
  var doctor_default = defineCommand({
20648
21269
  meta: {
@@ -20650,10 +21271,15 @@ var doctor_default = defineCommand({
20650
21271
  description: "Scan the hub for logical-integrity issues (read-only; run `mh repair` to fix)"
20651
21272
  },
20652
21273
  run: () => {
20653
- const report = validateHub(openMetahub());
20654
- print(report, () => {
21274
+ const db = openMetahub();
21275
+ const report = validateHub(db);
21276
+ const storage = compactEstimate(db, 90);
21277
+ print({ ...report, storage }, () => {
21278
+ const mb = (storage.db_bytes / 1024 / 1024).toFixed(1);
21279
+ const storageLine = `storage: ${storage.total_changes} oplog change(s), ${mb} MB on disk` + (storage.compactable_changes ? `; ${storage.compactable_changes} prunable with \`mh compact\` (90d window)` : "");
20655
21280
  if (report.ok)
20656
- return "No integrity issues found.";
21281
+ return `No integrity issues found.
21282
+ ${storageLine}`;
20657
21283
  const head = `${report.total} issue(s): ${Object.entries(report.counts).map(([k2, v2]) => `${k2}=${v2}`).join(", ")}`;
20658
21284
  const rows = report.issues.map((i2) => ({
20659
21285
  category: i2.category,
@@ -20664,7 +21290,9 @@ var doctor_default = defineCommand({
20664
21290
  }));
20665
21291
  return `${head}
20666
21292
 
20667
- ${table(rows)}`;
21293
+ ${table(rows)}
21294
+
21295
+ ${storageLine}`;
20668
21296
  });
20669
21297
  }
20670
21298
  });
@@ -20711,6 +21339,48 @@ ${left.length} report-only issue(s) need manual attention:
20711
21339
  }
20712
21340
  });
20713
21341
 
21342
+ // src/cli/commands/compact.ts
21343
+ function fmtBytes(n2) {
21344
+ if (n2 < 1024)
21345
+ return `${n2} B`;
21346
+ if (n2 < 1024 * 1024)
21347
+ return `${(n2 / 1024).toFixed(1)} KB`;
21348
+ return `${(n2 / 1024 / 1024).toFixed(1)} MB`;
21349
+ }
21350
+ var compact_default = defineCommand({
21351
+ meta: {
21352
+ name: "compact",
21353
+ description: "Prune oplog history older than the retention window and reclaim disk. " + "The current data is untouched; doc/record/prop history older than the window " + "collapses to a single baseline version (revert beyond it is no longer possible). " + "Local-only: peers keep their own history until they compact themselves."
21354
+ },
21355
+ args: {
21356
+ keep: {
21357
+ type: "string",
21358
+ description: "Days of full history to keep (default 90; 0 = head state only)"
21359
+ },
21360
+ "dry-run": { type: "boolean", description: "Report what would be removed, change nothing" },
21361
+ "no-vacuum": { type: "boolean", description: "Skip VACUUM (faster; space is reused, not returned)" }
21362
+ },
21363
+ run: guard((args) => {
21364
+ const db = openMetahub();
21365
+ const r3 = compactOplog(db, {
21366
+ keepDays: args.keep != null ? Number(args.keep) : 90,
21367
+ dryRun: args["dry-run"],
21368
+ vacuum: !args["no-vacuum"]
21369
+ });
21370
+ print(r3, () => {
21371
+ const lines = [
21372
+ `oplog: ${r3.deleted_changes} change(s) removed, ${r3.kept_changes} kept (cutoff ${r3.cutoff})`,
21373
+ `blobs: ${r3.blobs_deleted} file(s), ${fmtBytes(r3.blob_bytes_freed)}`,
21374
+ `db: ${fmtBytes(r3.db_bytes_before)} -> ${fmtBytes(r3.db_bytes_after)}`
21375
+ ];
21376
+ if (r3.dry_run)
21377
+ lines.push("(dry run \u2014 nothing deleted)");
21378
+ return lines.join(`
21379
+ `);
21380
+ });
21381
+ })
21382
+ });
21383
+
20714
21384
  // src/cli/commands/config.ts
20715
21385
  init_node();
20716
21386
 
@@ -22817,7 +23487,10 @@ var syncRoutes = [
22817
23487
  async handler(req, { db, node }) {
22818
23488
  const body = await req.json();
22819
23489
  ingest(db, body.changes ?? []);
22820
- const batch = changesAfterSeq(db, body.since ?? 0);
23490
+ const batch = changesAfterSeq(db, body.since ?? 0, {
23491
+ limit: body.limit,
23492
+ excludeDatasets: body.exclude_datasets
23493
+ });
22821
23494
  return Response.json({ node_id: node, changes: batch.changes, cursor: batch.cursor });
22822
23495
  }
22823
23496
  },
@@ -22982,50 +23655,14 @@ function unlockPage() {
22982
23655
  </script>
22983
23656
  </body></html>`;
22984
23657
  }
22985
- var SHIM = `<script>(function(){
22986
- var KEY = "mh_token";
22987
- function save(t){
22988
- try { localStorage.setItem(KEY, t); } catch (e) {}
22989
- document.cookie = KEY + "=" + encodeURIComponent(t) + "; path=/; SameSite=Strict; Max-Age=31536000";
22990
- }
22991
- var orig = window.fetch.bind(window);
22992
- window.fetch = function(input, init){
22993
- init = init || {};
22994
- var t = null; try { t = localStorage.getItem(KEY); } catch (e) {}
22995
- var url;
22996
- try { url = new URL((typeof input === "string" ? input : input.url), location.href); } catch (e) { url = null; }
22997
- var same = url && url.origin === location.origin;
22998
- if (same && t) {
22999
- var h = new Headers(init.headers || (typeof input !== "string" && input.headers) || {});
23000
- if (!h.has("authorization")) h.set("authorization", "Bearer " + t);
23001
- init.headers = h;
23002
- }
23003
- return orig(input, init).then(function(res){
23004
- if (res.status !== 401 || !same || !t || init.__mhRetried) return res;
23005
- return orig("${RENEW_PATH}", { headers: { authorization: "Bearer " + t } }).then(function(r){
23006
- if (!r.ok) return res;
23007
- return r.json().then(function(d){
23008
- if (!d || !d.token) return res;
23009
- save(d.token);
23010
- var h2 = new Headers(init.headers || {});
23011
- h2.set("authorization", "Bearer " + d.token);
23012
- init.headers = h2;
23013
- init.__mhRetried = true;
23014
- return orig(input, init);
23015
- });
23016
- }).catch(function(){ return res; });
23017
- });
23018
- };
23019
- })();</script>`;
23658
+ var RUNTIME_TAG = `<script src="/mh-runtime.js"></script>`;
23020
23659
  function injectShim(html) {
23021
23660
  const i2 = html.toLowerCase().indexOf("<head>");
23022
23661
  if (i2 >= 0)
23023
- return html.slice(0, i2 + 6) + SHIM + html.slice(i2 + 6);
23024
- return SHIM + html;
23662
+ return html.slice(0, i2 + 6) + RUNTIME_TAG + html.slice(i2 + 6);
23663
+ return RUNTIME_TAG + html;
23025
23664
  }
23026
- async function withShim(res, cfg) {
23027
- if (!authActive(cfg))
23028
- return res;
23665
+ async function withShim(res, _cfg) {
23029
23666
  const ct3 = res.headers.get("content-type") ?? "";
23030
23667
  if (!ct3.includes("text/html"))
23031
23668
  return res;
@@ -23078,15 +23715,16 @@ function startServer(opts = {}) {
23078
23715
  server = Bun.serve({
23079
23716
  port,
23080
23717
  hostname: host,
23718
+ tls: opts.tls ? { cert: Bun.file(opts.tls.certPath), key: Bun.file(opts.tls.keyPath) } : undefined,
23081
23719
  async fetch(req) {
23082
23720
  const url2 = new URL(req.url);
23083
23721
  if (url2.pathname === SYNC_PATH) {
23084
23722
  if (!acceptsSyncToken(req, url2, auth, db))
23085
23723
  return unauthorized();
23086
23724
  } else {
23087
- const exempt = url2.pathname === HEALTH_PATH || url2.pathname === RENEW_PATH || url2.pathname === PAIR_PATH;
23725
+ const exempt = url2.pathname === HEALTH_PATH || url2.pathname === RENEW_PATH || url2.pathname === PAIR_PATH || url2.pathname === "/manifest.webmanifest" || url2.pathname === "/sw.js" || url2.pathname.startsWith("/icons/");
23088
23726
  if (!exempt && !hasValidToken(req, url2, auth)) {
23089
- return wantsHtml(req) ? new Response(unlockPage(), { headers: HTML_HEADERS }) : unauthorized();
23727
+ return wantsHtml(req) ? new Response(unlockPage(), { headers: { ...HTML_HEADERS, "x-mh-unlock": "1" } }) : unauthorized();
23090
23728
  }
23091
23729
  }
23092
23730
  if (url2.pathname === RENEW_PATH) {
@@ -23143,6 +23781,7 @@ function startServer(opts = {}) {
23143
23781
  token: active?.token ?? null,
23144
23782
  exp: active?.exp ?? null,
23145
23783
  authMode,
23784
+ tls: Boolean(opts.tls),
23146
23785
  autoSync: Boolean(autoSync && syncIntervalMs > 0),
23147
23786
  syncIntervalMs,
23148
23787
  stop() {
@@ -23158,8 +23797,8 @@ init_errors();
23158
23797
 
23159
23798
  // src/webui/server/assets.ts
23160
23799
  import { fileURLToPath } from "url";
23161
- import { readdirSync as readdirSync2, statSync } from "fs";
23162
- import { join as join6 } from "path";
23800
+ import { readdirSync as readdirSync3, statSync as statSync2 } from "fs";
23801
+ import { join as join7, dirname } from "path";
23163
23802
 
23164
23803
  // src/webui/styles.css
23165
23804
  var styles_default = ` :root {
@@ -23244,7 +23883,26 @@ var styles_default = ` :root {
23244
23883
  /* sidebar */
23245
23884
  .sidebar { width:268px; flex:none; background:var(--sidebar); border-right:1px solid var(--line);
23246
23885
  display:flex; flex-direction:column; position:relative; transition:margin-left .22s cubic-bezier(.4,0,.2,1); }
23247
- .sb-resizer { position:absolute; top:0; right:-3px; width:6px; height:100%; cursor:col-resize; z-index:5; }
23886
+ /* right:-4px (not -3): the 7px handle then straddles the 1px border line
23887
+ symmetrically (3.5px each side of the border's center), so the centered
23888
+ ::before grip line lands exactly on the border instead of 1px left of it. */
23889
+ .sb-resizer { position:absolute; top:0; right:-4px; width:7px; height:100%; cursor:col-resize; z-index:5; }
23890
+ /* Unified resize-handle visual (sidebar + datasheet/doc-table column resizers):
23891
+ the handle stays an invisible hit area; a centered 3px round-capped line
23892
+ fades/expands in on hover and solidifies with a soft halo while dragging.
23893
+ --rz-guide-h (written by startColumnResize) stretches the dragging line to
23894
+ the full table height so the new column edge is visible across all rows. */
23895
+ .sb-resizer::before, .col-resizer::before, .doc-col-resizer::before {
23896
+ content:""; position:absolute; left:50%; top:0; height:100%; width:3px;
23897
+ transform:translateX(-50%) scaleX(.4); border-radius:999px;
23898
+ background:var(--accent); opacity:0; pointer-events:none;
23899
+ transition:opacity .15s cubic-bezier(.4,0,.2,1), transform .15s cubic-bezier(.4,0,.2,1), box-shadow .15s; }
23900
+ .sb-resizer:hover::before, .col-resizer:hover::before, .doc-col-resizer:hover::before {
23901
+ opacity:.6; transform:translateX(-50%) scaleX(1); }
23902
+ .sb-resizer.dragging::before, .col-resizer.dragging::before, .doc-col-resizer.dragging::before {
23903
+ opacity:1; transform:translateX(-50%) scaleX(1);
23904
+ box-shadow:0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
23905
+ height:var(--rz-guide-h, 100%); }
23248
23906
  .sb-head { display:flex; align-items:center; gap:8px; padding:14px 14px 10px; }
23249
23907
  .brand { display:flex; align-items:center; gap:8px; flex:1; font-weight:700; font-size:15px; letter-spacing:-.01em; }
23250
23908
  .brand .mark { width:24px; height:24px; border-radius:7px;
@@ -23376,18 +24034,19 @@ var styles_default = ` :root {
23376
24034
  .btn-primary:hover { filter:brightness(1.07); }
23377
24035
  .btn-danger { background:var(--danger); color:#fff; } .btn-danger:hover { filter:brightness(1.05); }
23378
24036
 
23379
- /* desktop app (frameless): drop the sidebar logo; on macOS reserve a draggable
23380
- strip for the inset traffic lights so they never overlap our controls */
23381
- body.desktop .brand { display:none; }
24037
+ /* macOS desktop (hiddenInset frameless): drop the sidebar logo and reserve a
24038
+ draggable strip so the inset traffic lights never overlap our controls.
24039
+ Windows/Linux keep the native frame, so the logo stays. */
24040
+ body.desktop-mac .brand { display:none; }
23382
24041
  /* logo gone \u2192 keep the collapse button on the right (where .brand's flex used to push it) */
23383
- body.desktop .sb-head .iconbtn { margin-left:auto; }
24042
+ body.desktop-mac .sb-head .iconbtn { margin-left:auto; }
23384
24043
  body.desktop-mac .sb-head { -webkit-app-region:drag; }
23385
24044
  body.desktop-mac .sb-head .iconbtn { -webkit-app-region:no-drag; }
23386
24045
  body.desktop-mac .topbar { -webkit-app-region:drag; }
23387
24046
  body.desktop-mac .topbar .iconbtn, body.desktop-mac .topbar .btn { -webkit-app-region:no-drag; }
23388
24047
  /* peek drawer header overlaps the topbar drag region \u2192 clear its buttons so they stay clickable */
23389
24048
  body.desktop-mac .peek-head { -webkit-app-region:drag; }
23390
- body.desktop-mac .peek-head .iconbtn, body.desktop-mac .peek-head .btn { -webkit-app-region:no-drag; }
24049
+ body.desktop-mac .peek-head .iconbtn, body.desktop-mac .peek-head .btn, body.desktop-mac .peek-head .hist-toggle { -webkit-app-region:no-drag; }
23391
24050
  /* sidebar collapsed \u2192 traffic lights sit over the main header; clear the hamburger */
23392
24051
  body.desktop-mac .sidebar.collapsed ~ .main .topbar { padding-left:80px; }
23393
24052
 
@@ -23552,8 +24211,7 @@ var styles_default = ` :root {
23552
24211
  color:var(--muted); border-radius:4px; opacity:0; transition:opacity .12s,background .12s; }
23553
24212
  .doc-th:hover .doc-col-menu { opacity:1; }
23554
24213
  .doc-col-menu:hover { background:var(--hover-2); color:var(--fg); }
23555
- .doc-col-resizer { position:absolute; top:0; right:-3px; width:7px; height:100%; cursor:col-resize; z-index:3; touch-action:none; }
23556
- .doc-col-resizer:hover, .doc-col-resizer.dragging { background:var(--accent); opacity:.35; }
24214
+ .doc-col-resizer { position:absolute; top:0; right:-4px; width:7px; height:100%; cursor:col-resize; z-index:3; touch-action:none; }
23557
24215
  /* per-row delete: small handle floating at the first cell on row hover */
23558
24216
  .doc-row-del { position:absolute; top:3px; left:2px; width:18px; height:18px; display:grid; place-items:center;
23559
24217
  color:var(--muted); background:var(--bg); border:1px solid var(--line); border-radius:5px; box-shadow:var(--shadow-sm);
@@ -23598,15 +24256,15 @@ var styles_default = ` :root {
23598
24256
  .colhead:hover { background:var(--hover); }
23599
24257
  .colhead .ti { color:var(--muted); } .colhead .ti svg { width:14px; height:14px; }
23600
24258
  .colhead .nm { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
23601
- .col-resizer { position:absolute; top:0; right:-3px; width:7px; height:100%; cursor:col-resize; z-index:2; touch-action:none; }
23602
- .col-resizer:hover, .col-resizer.dragging { background:var(--accent); opacity:.35; }
24259
+ .col-resizer { position:absolute; top:0; right:-4px; width:7px; height:100%; cursor:col-resize; z-index:2; touch-action:none; }
23603
24260
  .addcol { min-width:46px; } .addcol .colhead { justify-content:flex-start; color:var(--muted); }
23604
24261
  /* trailing filler column: empty canvas to the right of the data, not a real cell */
23605
24262
  table.grid td.filler { border-right:0; border-bottom:0; background:transparent; }
23606
24263
  body.col-resizing { user-select:none; cursor:col-resize; }
23607
24264
  table.grid .selcell { width:38px; min-width:38px; max-width:38px; text-align:center; }
23608
24265
  table.grid .gripcol, table.grid .rowgrip { width:26px; min-width:26px; max-width:26px; text-align:center; }
23609
- td.cell-td { vertical-align:top; }
24266
+ /* relative: anchors the .celledit overlay editor to the cell */
24267
+ td.cell-td { position:relative; vertical-align:top; }
23610
24268
  .cell { padding:8px 11px; min-height:37px; cursor:text; word-break:break-word; display:flex; flex-wrap:wrap; gap:3px; align-items:center; }
23611
24269
  /* hover/active tint goes on the <td> (always full row height) not the inner
23612
24270
  .cell (only as tall as its content), so a taller sibling row never leaves a
@@ -23658,6 +24316,14 @@ var styles_default = ` :root {
23658
24316
  .addrow:hover { background:var(--hover); color:var(--fg-soft); }
23659
24317
  .gridfoot { display:flex; gap:18px; color:var(--muted); font-size:12px; padding:8px 2px; }
23660
24318
  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; }
24319
+ /* Overlay cell editor: floats over the td; the display content underneath
24320
+ stays in flow so the row height never changes while editing. Padding on the
24321
+ input matches .cell (8px 11px) so the text doesn't shift on entering edit. */
24322
+ .celledit { position:absolute; top:0; left:0; width:100%; min-height:100%; z-index:10;
24323
+ display:flex; align-items:stretch; background:var(--bg); border-radius:var(--radius-sm);
24324
+ box-shadow:0 0 0 2px var(--accent), var(--shadow-md); }
24325
+ .celledit input.inlineedit { width:100%; min-width:0; border:0; outline:0; border-radius:inherit;
24326
+ padding:8px 11px; background:transparent; }
23661
24327
 
23662
24328
  /* ---- sites (\u7AD9\u70B9\u7BA1\u7406) ---- */
23663
24329
  .sites-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(264px, 1fr)); gap:14px; margin-top:6px; }
@@ -23761,6 +24427,53 @@ var styles_default = ` :root {
23761
24427
  .proprow .v:hover { background:var(--hover); }
23762
24428
  .peek-divider { height:1px; background:var(--line); margin:22px 0 18px; }
23763
24429
 
24430
+ /* version history (doc drawer + record peek view) */
24431
+ .hist-peek { width:760px; }
24432
+ .hist-title { display:flex; align-items:center; gap:6px; font-weight:600; font-size:var(--fs-ui); }
24433
+ .hist-toggle { display:flex; align-items:center; gap:5px; font-size:var(--fs-sm); color:var(--muted); cursor:pointer; user-select:none; white-space:nowrap; }
24434
+ .hist-toggle input { accent-color:var(--accent); }
24435
+ .hist-split { display:flex; flex:1; min-height:0; }
24436
+ .hist-list { width:252px; flex:none; overflow:auto; border-right:1px solid var(--line); padding:8px; }
24437
+ .hist-item { padding:8px 10px; border-radius:var(--radius-sm); cursor:pointer; }
24438
+ .hist-item:hover { background:var(--hover); }
24439
+ .hist-item.sel { background:var(--accent-soft); }
24440
+ .hist-item.static { cursor:default; border-bottom:1px solid var(--line); border-radius:0; padding:10px 4px; }
24441
+ .hist-item.static:hover { background:none; }
24442
+ .hist-item .row1 { display:flex; align-items:center; gap:7px; }
24443
+ .hist-item .row1 .when { font-weight:600; font-size:var(--fs-ui); }
24444
+ .hist-item .row2 { display:flex; gap:8px; margin-top:2px; font-size:var(--fs-sm); color:var(--muted); min-width:0; }
24445
+ .hist-item .row2 .what { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
24446
+ .hist-kind { font-size:var(--fs-xs); padding:1px 6px; border-radius:99px; background:var(--surface-2); color:var(--muted); }
24447
+ .hist-kind.revert { background:var(--accent-soft); color:var(--accent); }
24448
+ .hist-kind.repair { background:var(--danger-soft); color:var(--danger); }
24449
+ .hist-now { font-size:var(--fs-xs); padding:1px 6px; border-radius:99px; background:var(--accent); color:var(--accent-fg); }
24450
+ .hist-preview { flex:1; overflow:auto; padding:20px 24px; }
24451
+ .hist-doc-title { font-size:21px; font-weight:700; letter-spacing:-.02em; margin-bottom:14px; }
24452
+ .hist-block { white-space:pre-wrap; font-size:var(--fs-body); line-height:1.65; padding:2px 6px; border-radius:4px; margin:0 -6px 12px; }
24453
+ /* line-level diff rows (git-like): pale full-line tint, darker tint on the
24454
+ intra-line changed segment; strikethrough only on the deleted segment. */
24455
+ .hist-line { white-space:pre-wrap; font-size:var(--fs-body); line-height:1.6; padding:1px 6px; margin:0 -6px; border-radius:3px; }
24456
+ .hist-line.add { background:color-mix(in srgb, var(--success) 13%, transparent); }
24457
+ .hist-line.del { background:var(--danger-soft); }
24458
+ .hist-line.add .seg { background:color-mix(in srgb, var(--success) 32%, transparent); border-radius:2px; }
24459
+ .hist-line.del .seg { background:color-mix(in srgb, var(--danger) 28%, transparent); border-radius:2px; text-decoration:line-through; }
24460
+ .hist-foot { display:flex; justify-content:flex-end; padding:12px 16px; border-top:1px solid var(--line); }
24461
+ .pad { padding:12px; }
24462
+ .hist-rec-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:10px; }
24463
+ .hist-feed { padding:12px 24px; }
24464
+ .hist-recname { font-weight:600; font-size:var(--fs-sm); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
24465
+ .hist-status { font-size:var(--fs-xs); color:var(--muted); flex:none; }
24466
+ .hist-field .old, .hist-field .new { max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
24467
+ .hist-more { border:0; background:none; color:var(--muted); font-size:var(--fs-sm); cursor:pointer; padding:2px 0; text-align:left; }
24468
+ .hist-more:hover { color:var(--fg); }
24469
+ .hist-fields { padding:8px 4px 4px; display:flex; flex-direction:column; gap:6px; }
24470
+ .hist-field { display:flex; gap:8px; font-size:var(--fs-sm); align-items:baseline; flex-wrap:wrap; }
24471
+ .hist-field .fname { color:var(--muted); width:120px; flex:none; }
24472
+ .hist-field .old { text-decoration:line-through; color:var(--muted); }
24473
+ .hist-field .arr { color:var(--muted); }
24474
+ .hist-field .new { font-weight:500; }
24475
+ .hist-restore { margin-top:8px; align-self:flex-start; }
24476
+
23764
24477
  /* menu / popover */
23765
24478
  .menu-layer { position:fixed; inset:0; z-index:90; }
23766
24479
  .pop { position:fixed; background:var(--bg); border:1px solid var(--line); border-radius:var(--radius); box-shadow:var(--shadow-lg);
@@ -23889,12 +24602,51 @@ var styles_default = ` :root {
23889
24602
  .kg-particle { opacity:0 !important; }
23890
24603
  }
23891
24604
  .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; }
24605
+ .offline-bar { margin:12px 24px 0; padding:8px 14px; background:var(--surface-2); color:var(--fg-soft); border-radius:var(--radius-sm); font-size:12.5px; }
23892
24606
  .search-hit { margin:0 0 8px; padding:10px 14px; background:var(--surface); border-radius:var(--radius-sm); font-size:13px; cursor:pointer; }
23893
24607
  .search-hit:hover { background:var(--surface-2); }
23894
24608
  .search-hit mark { background:transparent; color:var(--accent); font-weight:600; }
23895
24609
  .view-placeholder { padding:40px; text-align:center; border:1px dashed var(--line-strong); border-radius:var(--radius); color:var(--muted); margin-top:8px; }
23896
24610
  .ico.flip { transform:scaleX(-1); }
23897
24611
 
24612
+ /* ---- find in document (Ctrl-F) ---- */
24613
+ /* CSS Custom Highlight overlays \u2014 no DOM mutation, safe over contentEditable. */
24614
+ ::highlight(mh-find-all) { background:color-mix(in srgb, var(--accent) 26%, transparent); border-radius:2px; }
24615
+ ::highlight(mh-find-current) { background:var(--accent); color:var(--accent-fg); }
24616
+ .find-bar {
24617
+ position:fixed; top:58px; right:18px; z-index:40; /* below the ~50px topbar */
24618
+ display:flex; align-items:center; gap:4px;
24619
+ padding:5px 6px; background:var(--bg); border:1px solid var(--line-strong);
24620
+ border-radius:var(--radius); box-shadow:var(--shadow-lg);
24621
+ }
24622
+ .find-bar .find-ico { color:var(--muted); margin:0 2px; flex:none; }
24623
+ .find-bar input {
24624
+ width:200px; max-width:42vw; border:0; outline:0; background:transparent;
24625
+ color:var(--fg); font-size:13px; padding:3px 2px;
24626
+ }
24627
+ .find-bar input::placeholder { color:var(--muted); }
24628
+ .find-count { font-size:12px; color:var(--muted); min-width:48px; text-align:right; padding:0 2px; font-variant-numeric:tabular-nums; }
24629
+ .find-opt {
24630
+ border:1px solid var(--line-strong); background:transparent; color:var(--fg-soft);
24631
+ border-radius:var(--radius-sm); font-size:12px; line-height:1; padding:4px 6px; cursor:pointer;
24632
+ transition:background .12s, color .12s, border-color .12s;
24633
+ }
24634
+ .find-opt:hover { background:var(--surface-2); }
24635
+ .find-opt.on { background:var(--accent-soft); color:var(--accent); border-color:color-mix(in srgb, var(--accent) 45%, transparent); }
24636
+ .find-nav {
24637
+ display:grid; place-items:center; width:26px; height:26px; flex:none;
24638
+ border:0; background:transparent; color:var(--fg-soft); border-radius:var(--radius-sm); cursor:pointer;
24639
+ transition:background .12s, color .12s;
24640
+ }
24641
+ .find-nav:hover:not(:disabled) { background:var(--surface-2); color:var(--fg); }
24642
+ .find-nav:disabled { opacity:.4; cursor:default; }
24643
+ .find-nav .find-prev { transform:rotate(180deg); }
24644
+ .find-close { color:var(--muted); }
24645
+ @media (max-width:560px) {
24646
+ .find-bar { left:12px; right:12px; }
24647
+ .find-bar input { width:auto; flex:1; max-width:none; }
24648
+ }
24649
+
23898
24650
  /* ---- board (kanban) view ---- */
23899
24651
  .board-wrap { margin-top:4px; }
23900
24652
  .board-bar { display:flex; align-items:center; gap:10px; padding:2px 0 12px; }
@@ -23970,9 +24722,16 @@ var styles_default = ` :root {
23970
24722
  display:flex; align-items:center; gap:2px; padding:0 4px; cursor:grab; touch-action:none; box-shadow:var(--shadow-sm); overflow:hidden; }
23971
24723
  .tl-item .tl-label { flex:1; font-size:12px; font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; pointer-events:none; }
23972
24724
  .tl-row:hover .tl-item { box-shadow:0 0 0 1px var(--accent), var(--shadow-sm); }
23973
- .tl-item .tl-h { width:8px; align-self:stretch; flex:none; cursor:col-resize; border-radius:6px; opacity:0; transition:opacity .1s; }
23974
- .tl-item:hover .tl-h { opacity:1; background:rgba(255,255,255,.35); }
23975
- .tl-item .tl-h:hover { background:rgba(255,255,255,.6); }
24725
+ /* Edge handles: a small centered round-capped grip inside the colored bar.
24726
+ --accent-fg (not white) keeps contrast right in both themes \u2014 dark grip on
24727
+ the light-purple dark-mode bar, white grip on the blue light-mode bar. */
24728
+ .tl-item .tl-h { position:relative; width:9px; align-self:stretch; flex:none; cursor:col-resize; opacity:0; transition:opacity .15s; touch-action:none; }
24729
+ .tl-item .tl-h::before { content:""; position:absolute; left:50%; top:50%; width:3px; height:10px;
24730
+ transform:translate(-50%,-50%); border-radius:999px;
24731
+ background:color-mix(in srgb, var(--accent-fg) 70%, transparent);
24732
+ transition:height .15s cubic-bezier(.4,0,.2,1), background .15s; }
24733
+ .tl-item:hover .tl-h, .tl-item .tl-h.dragging { opacity:1; }
24734
+ .tl-item .tl-h:hover::before, .tl-item .tl-h.dragging::before { height:14px; background:var(--accent-fg); }
23976
24735
  .tl-milestone { position:absolute; top:9px; width:18px; height:18px; transform:rotate(45deg); background:var(--accent); border-radius:3px; cursor:grab; touch-action:none; box-shadow:var(--shadow-sm); margin-left:-9px; }
23977
24736
  .tl-milestone:hover { box-shadow:0 0 0 2px var(--accent-soft), var(--shadow-sm); }
23978
24737
  .tl-track-empty { position:absolute; top:0; right:0; height:100%; cursor:copy; }
@@ -24066,8 +24825,13 @@ var styles_default = ` :root {
24066
24825
  /* Anchor/TOC jumps: clear the mobile topbar (49px min + notch inset);
24067
24826
  the desktop 60px estimate leaves blocks half-hidden behind it. */
24068
24827
  .block { scroll-margin-top:calc(49px + env(safe-area-inset-top) + 8px); }
24828
+ /* Find bar clears the taller mobile topbar (49px min + notch inset). */
24829
+ .find-bar { top:calc(49px + env(safe-area-inset-top) + 8px); }
24069
24830
  .db { padding:16px 14px 90px; }
24070
24831
  .peek { width:100%; }
24832
+ /* history drawer: the side-by-side list/preview doesn't fit a phone \u2014 stack. */
24833
+ .hist-split { flex-direction:column; }
24834
+ .hist-list { width:auto; max-height:38%; border-right:0; border-bottom:1px solid var(--line); }
24071
24835
 
24072
24836
  /* Safe areas (viewport-fit=cover): keep chrome clear of the notch / home bar. */
24073
24837
  .sb-head { padding-top:calc(14px + env(safe-area-inset-top)); }
@@ -24117,7 +24881,7 @@ var styles_default = ` :root {
24117
24881
  table.doc-table .doc-row-del { opacity:1; }
24118
24882
  .selcell input { opacity:1; }
24119
24883
  .rowgrip { color:var(--muted); }
24120
- .tl-item .tl-h { opacity:1; background:rgba(255,255,255,.35); }
24884
+ .tl-item .tl-h { opacity:1; }
24121
24885
  /* Neutralize the stickiest tap-residue tints (a tap "hovers" until the
24122
24886
  next tap elsewhere); transient menus/buttons are left alone. */
24123
24887
  .navitem:hover { background:none; }
@@ -24133,15 +24897,20 @@ var styles_default = ` :root {
24133
24897
  macOS vibrancy blur show through; a translucent panel keeps text readable.
24134
24898
  The top bar is the window drag region; its buttons opt back out. */
24135
24899
  body.quicknote { background:transparent; }
24900
+ /* html carries an opaque bg for the iOS 26 Safari toolbar tint; the vibrancy
24901
+ window needs the root transparent again or the blur is walled off. */
24902
+ html:has(body.quicknote) { background:transparent; }
24136
24903
  /* The main app lays #app out as a flex row (sidebar + main); the quick-note
24137
24904
  route has a single child, so override to a full-bleed block or the panel
24138
24905
  shrinks to its content width and the window's vibrancy shows through. */
24139
24906
  body.quicknote #app { display:block; width:100vw; height:100vh; height:100dvh; }
24140
24907
  .qn { display:flex; flex-direction:column; width:100%; height:100vh; height:100dvh; overflow:hidden;
24141
24908
  background:color-mix(in srgb, var(--bg) 20%, transparent); }
24142
- .qn-bar { display:flex; align-items:center; gap:6px; padding:4px 10px 4px 76px;
24909
+ .qn-bar { display:flex; align-items:center; gap:6px; padding:4px 10px;
24143
24910
  user-select:none; -webkit-app-region:drag;
24144
24911
  background:color-mix(in srgb, var(--surface) 30%, transparent); }
24912
+ /* macOS only: clear the inset traffic lights; Win/Linux frameless has none */
24913
+ body.desktop-mac .qn-bar { padding-left:76px; }
24145
24914
  .qn-brand { flex:1; min-width:0; font-size:12px; font-weight:600; color:var(--fg-soft);
24146
24915
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
24147
24916
  .qn-actions { display:flex; gap:2px; -webkit-app-region:no-drag; }
@@ -24151,6 +24920,10 @@ var styles_default = ` :root {
24151
24920
  .qn-body .doc { max-width:none; margin:0; padding:14px 22px 30vh; }
24152
24921
  .qn-body .doc-title { font-size:24px; margin-bottom:2px; }
24153
24922
  .qn-body .doc-meta { display:none; }
24923
+ /* The narrow-width gutter hide is now gated on (pointer:coarse), which this
24924
+ desktop window never matches \u2014 re-hide explicitly: quick notes offer no
24925
+ block insert/drag handles (Enter / slash commands cover insertion). */
24926
+ body.quicknote .block .gutter { display:none; }
24154
24927
  /* A native checkbox paints an opaque white box when unchecked, which breaks
24155
24928
  the window's vibrancy blur. Draw a custom box so the unchecked background
24156
24929
  stays transparent; checked falls back to the accent fill + checkmark. */
@@ -24184,6 +24957,11 @@ var styles_default = ` :root {
24184
24957
  .qn-shortcut.capturing { outline:2px solid var(--accent); color:var(--accent); }
24185
24958
  `;
24186
24959
 
24960
+ // src/webui/server/icons.ts
24961
+ var ICON_192 = "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";
24962
+ var ICON_512 = "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";
24963
+ var ICON_180 = "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";
24964
+
24187
24965
  // src/webui/server/assets.ts
24188
24966
  var RUNNING_FROM_SOURCE = import.meta.url.includes("/src/webui/");
24189
24967
  var HTML = `<!doctype html>
@@ -24211,6 +24989,8 @@ var HTML = `<!doctype html>
24211
24989
  alone, so the dark palette exists exactly once \u2014 see styles.css. Dark also
24212
24990
  pre-tints the status bar (hex mirrors --bg) so the first frame isn't white. -->
24213
24991
  <script>try{var t=localStorage.getItem('mh-theme'),d=t==='dark'||(t!=='light'&&matchMedia('(prefers-color-scheme: dark)').matches);document.documentElement.dataset.resolved=d?'dark':'light';if(d)document.getElementById('theme-color-meta').content='#1a1a1c'}catch(e){}</script>
24992
+ <link rel="manifest" href="/manifest.webmanifest">
24993
+ <link rel="apple-touch-icon" href="/icons/icon-180.png">
24214
24994
  <link rel="stylesheet" href="/webui.css">
24215
24995
  </head>
24216
24996
  <body>
@@ -24218,53 +24998,104 @@ var HTML = `<!doctype html>
24218
24998
  <script type="module" src="/webui.js"></script>
24219
24999
  </body>
24220
25000
  </html>`;
24221
- var injectedJs = null;
24222
- var cachedJs = null;
24223
- var cachedJsMtime = 0;
25001
+ var injected = {
25002
+ js: null,
25003
+ sw: null,
25004
+ dbWorker: null,
25005
+ runtime: null,
25006
+ sdk: null
25007
+ };
25008
+ var injectedWasm = null;
24224
25009
  function newestSourceMtime(dir) {
24225
25010
  let newest = 0;
24226
- for (const e3 of readdirSync2(dir, { withFileTypes: true })) {
25011
+ for (const e3 of readdirSync3(dir, { withFileTypes: true })) {
24227
25012
  if (e3.name === "server" || e3.name.startsWith("."))
24228
25013
  continue;
24229
- const p2 = join6(dir, e3.name);
25014
+ const p2 = join7(dir, e3.name);
24230
25015
  if (e3.isDirectory())
24231
25016
  newest = Math.max(newest, newestSourceMtime(p2));
24232
25017
  else if (/\.tsx?$/.test(e3.name))
24233
- newest = Math.max(newest, statSync(p2).mtimeMs);
25018
+ newest = Math.max(newest, statSync2(p2).mtimeMs);
24234
25019
  }
24235
25020
  return newest;
24236
25021
  }
24237
- async function getJs() {
24238
- if (injectedJs != null)
24239
- return injectedJs;
24240
- if (RUNNING_FROM_SOURCE) {
24241
- const srcDir = fileURLToPath(new URL("..", import.meta.url));
24242
- const newest = newestSourceMtime(srcDir);
24243
- if (cachedJs == null || newest > cachedJsMtime) {
24244
- const entry = fileURLToPath(new URL("../app.tsx", import.meta.url));
24245
- const res = await Bun.build({ entrypoints: [entry], target: "browser" });
24246
- if (!res.success)
24247
- throw new AggregateError(res.logs, "webui build failed");
24248
- cachedJs = await res.outputs[0].text();
24249
- cachedJsMtime = newest;
24250
- }
24251
- return cachedJs;
24252
- }
24253
- if (cachedJs == null) {
24254
- const prebuilt = Bun.file(fileURLToPath(new URL("./webui.js", import.meta.url)));
24255
- if (!await prebuilt.exists()) {
24256
- throw new Error("webui bundle missing: dist/webui.js was not built \u2014 run `bun run build`");
24257
- }
24258
- cachedJs = await prebuilt.text();
24259
- }
24260
- return cachedJs;
24261
- }
25022
+ function bundleGetter(opts) {
25023
+ let cached2 = null;
25024
+ let cachedMtime = 0;
25025
+ return async () => {
25026
+ const fromBinary = injected[opts.key];
25027
+ if (fromBinary != null)
25028
+ return fromBinary;
25029
+ if (RUNNING_FROM_SOURCE) {
25030
+ const srcDir = fileURLToPath(new URL("..", import.meta.url));
25031
+ let newest = newestSourceMtime(srcDir);
25032
+ for (const extra of opts.extraDirs ?? []) {
25033
+ newest = Math.max(newest, newestSourceMtime(fileURLToPath(new URL(extra, import.meta.url))));
25034
+ }
25035
+ if (cached2 == null || newest > cachedMtime) {
25036
+ const entry = fileURLToPath(new URL(opts.entry, import.meta.url));
25037
+ const res = await Bun.build({ entrypoints: [entry], target: "browser" });
25038
+ if (!res.success)
25039
+ throw new AggregateError(res.logs, `${opts.dist} build failed`);
25040
+ cached2 = await res.outputs[0].text();
25041
+ cachedMtime = newest;
25042
+ }
25043
+ return cached2;
25044
+ }
25045
+ if (cached2 == null) {
25046
+ const prebuilt = Bun.file(fileURLToPath(new URL(`./${opts.dist}`, import.meta.url)));
25047
+ if (!await prebuilt.exists()) {
25048
+ throw new Error(`${opts.dist} missing: run \`bun run build\``);
25049
+ }
25050
+ cached2 = await prebuilt.text();
25051
+ }
25052
+ return cached2;
25053
+ };
25054
+ }
25055
+ var getJs = bundleGetter({ key: "js", entry: "../app.tsx", dist: "webui.js" });
25056
+ var getSwRaw = bundleGetter({ key: "sw", entry: "../sw.ts", dist: "sw.js" });
25057
+ var getDbWorker = bundleGetter({ key: "dbWorker", entry: "../data/db-worker.ts", dist: "db-worker.js" });
25058
+ var getRuntime = bundleGetter({ key: "runtime", entry: "../runtime.ts", dist: "mh-runtime.js" });
25059
+ var getSdk = bundleGetter({ key: "sdk", entry: "../../sdk/client.ts", dist: "metahub-sdk.js", extraDirs: ["../../sdk"] });
24262
25060
  async function getCss() {
24263
25061
  if (RUNNING_FROM_SOURCE) {
24264
25062
  return Bun.file(fileURLToPath(new URL("../styles.css", import.meta.url))).text();
24265
25063
  }
24266
25064
  return styles_default;
24267
25065
  }
25066
+ async function getSw() {
25067
+ const [raw, js, css] = await Promise.all([getSwRaw(), getJs(), getCss()]);
25068
+ const version2 = new Bun.CryptoHasher("sha256").update(js).update(css).digest("hex").slice(0, 16);
25069
+ return raw.replaceAll("__MH_SW_VERSION__", version2);
25070
+ }
25071
+ async function getWasm() {
25072
+ if (injectedWasm != null)
25073
+ return injectedWasm;
25074
+ const path = RUNNING_FROM_SOURCE ? join7(dirname(Bun.resolveSync("@sqlite.org/sqlite-wasm", fileURLToPath(new URL(".", import.meta.url)))), "sqlite3.wasm") : fileURLToPath(new URL("./sqlite3.wasm", import.meta.url));
25075
+ const f4 = Bun.file(path);
25076
+ if (!await f4.exists()) {
25077
+ throw new Error(`sqlite3.wasm missing at ${path} \u2014 run \`bun install\` / \`bun run build\``);
25078
+ }
25079
+ return f4.arrayBuffer();
25080
+ }
25081
+ var MANIFEST = JSON.stringify({
25082
+ name: "Metahub",
25083
+ short_name: "Metahub",
25084
+ start_url: "/",
25085
+ scope: "/",
25086
+ display: "standalone",
25087
+ background_color: "#ffffff",
25088
+ theme_color: "#ffffff",
25089
+ icons: [
25090
+ { src: "/icons/icon-192.png", sizes: "192x192", type: "image/png" },
25091
+ { src: "/icons/icon-512.png", sizes: "512x512", type: "image/png" }
25092
+ ]
25093
+ });
25094
+ var ICONS = {
25095
+ "/icons/icon-192.png": ICON_192,
25096
+ "/icons/icon-512.png": ICON_512,
25097
+ "/icons/icon-180.png": ICON_180
25098
+ };
24268
25099
  async function warmWebui() {
24269
25100
  try {
24270
25101
  await getJs();
@@ -24283,11 +25114,37 @@ async function serveWebui(req) {
24283
25114
  return asset(HTML, "text/html; charset=utf-8");
24284
25115
  if (pathname === "/webui.css")
24285
25116
  return asset(await getCss(), "text/css; charset=utf-8");
24286
- if (pathname === "/webui.js") {
25117
+ if (pathname === "/manifest.webmanifest")
25118
+ return asset(MANIFEST, "application/manifest+json");
25119
+ const icon = ICONS[pathname];
25120
+ if (icon) {
25121
+ return new Response(Buffer.from(icon, "base64"), {
25122
+ headers: { "content-type": "image/png", "cache-control": "public, max-age=86400" }
25123
+ });
25124
+ }
25125
+ if (pathname === "/sqlite3.wasm") {
25126
+ try {
25127
+ return new Response(await getWasm(), {
25128
+ headers: { "content-type": "application/wasm", "cache-control": "public, max-age=86400" }
25129
+ });
25130
+ } catch (e3) {
25131
+ console.error("[webui] failed to serve /sqlite3.wasm \u2014", e3);
25132
+ return new Response(`sqlite3.wasm unavailable: ${e3}`, { status: 500 });
25133
+ }
25134
+ }
25135
+ const script = {
25136
+ "/webui.js": getJs,
25137
+ "/sw.js": getSw,
25138
+ "/db-worker.js": getDbWorker,
25139
+ "/mh-runtime.js": getRuntime,
25140
+ "/metahub-sdk.js": getSdk
25141
+ };
25142
+ const getter = script[pathname];
25143
+ if (getter) {
24287
25144
  try {
24288
- return asset(await getJs(), "text/javascript; charset=utf-8");
25145
+ return asset(await getter(), "text/javascript; charset=utf-8");
24289
25146
  } catch (e3) {
24290
- console.error("[webui] failed to serve /webui.js \u2014", e3);
25147
+ console.error(`[webui] failed to serve ${pathname} \u2014`, e3);
24291
25148
  return new Response(`webui build failed: ${e3}`, { status: 500 });
24292
25149
  }
24293
25150
  }
@@ -24296,6 +25153,7 @@ async function serveWebui(req) {
24296
25153
 
24297
25154
  // src/webui/server/routes.ts
24298
25155
  init_errors();
25156
+ init_node();
24299
25157
  var DatabaseSchema = exports_external.object({
24300
25158
  id: exports_external.string(),
24301
25159
  name: exports_external.string(),
@@ -24313,7 +25171,8 @@ var PropertySchema = exports_external.object({
24313
25171
  var RecordSchema = exports_external.object({
24314
25172
  id: exports_external.string(),
24315
25173
  database_id: exports_external.string(),
24316
- 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.')
25174
+ 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. Lossy when two properties share a name \u2014 prefer `cells` then.'),
25175
+ cells: exports_external.record(exports_external.string(), exports_external.any()).describe("Field values keyed by property id \u2014 lossless under duplicate property names. Writes accept property ids as keys too.")
24317
25176
  });
24318
25177
  var MoveRecordReq = exports_external.object({
24319
25178
  target: exports_external.string(),
@@ -24348,6 +25207,10 @@ var UpdateDatabaseReq = exports_external.object({
24348
25207
  name: exports_external.string().optional(),
24349
25208
  icon: exports_external.string().nullable().optional()
24350
25209
  });
25210
+ var DuplicateDatabaseReq = exports_external.object({
25211
+ name: exports_external.string().optional().describe("Name for the copy (defaults to the source name)"),
25212
+ icon: exports_external.string().optional()
25213
+ });
24351
25214
  var CreatePropertyReq = exports_external.object({
24352
25215
  db: exports_external.string(),
24353
25216
  name: exports_external.string(),
@@ -24374,10 +25237,15 @@ var UpdateDocumentReq = exports_external.object({
24374
25237
  parent_id: exports_external.string().nullable().optional(),
24375
25238
  if_match: exports_external.string().optional()
24376
25239
  });
25240
+ var DuplicateDocumentReq = exports_external.object({
25241
+ title: exports_external.string().optional().describe("Title for the copy (defaults to the source title)"),
25242
+ parent_id: exports_external.string().nullable().optional()
25243
+ });
24377
25244
  var RevisionBase = exports_external.object({
24378
25245
  version: exports_external.string(),
24379
25246
  at: exports_external.string().describe("Wall-clock time of the revision (ISO 8601)"),
24380
25247
  node_id: exports_external.string(),
25248
+ kind: exports_external.enum(["user", "repair", "revert"]).describe("Source of the revision"),
24381
25249
  changes: exports_external.number(),
24382
25250
  created: exports_external.boolean(),
24383
25251
  deleted: exports_external.boolean()
@@ -24391,6 +25259,15 @@ var RecordRevisionSchema = RevisionBase.extend({
24391
25259
  fields: exports_external.array(exports_external.string()).describe("Property ids of the cells written"),
24392
25260
  moved: exports_external.boolean()
24393
25261
  });
25262
+ var DatabaseActivityEntrySchema = RecordRevisionSchema.extend({
25263
+ record_id: exports_external.string(),
25264
+ record_title: exports_external.string().nullable().describe("Title-property value as of this revision (deleted records keep their last title)"),
25265
+ diffs: exports_external.array(exports_external.object({
25266
+ prop: exports_external.string(),
25267
+ before: exports_external.any().optional(),
25268
+ after: exports_external.any().optional()
25269
+ })).describe("Value-level cell changes; a missing side means the cell did not exist")
25270
+ });
24394
25271
  var DocumentVersionStateSchema = exports_external.object({
24395
25272
  id: exports_external.string(),
24396
25273
  title: exports_external.string(),
@@ -24409,7 +25286,8 @@ var RevertDocResultSchema = exports_external.object({
24409
25286
  id: exports_external.string(),
24410
25287
  changed: exports_external.boolean(),
24411
25288
  restored: exports_external.string(),
24412
- version: exports_external.string()
25289
+ version: exports_external.string(),
25290
+ undeleted: exports_external.boolean()
24413
25291
  });
24414
25292
  var RevertRecordResultSchema = exports_external.object({
24415
25293
  id: exports_external.string(),
@@ -24418,6 +25296,41 @@ var RevertRecordResultSchema = exports_external.object({
24418
25296
  undeleted: exports_external.boolean(),
24419
25297
  restored: exports_external.string()
24420
25298
  });
25299
+ var PropertyRevisionSchema = RevisionBase.extend({
25300
+ fields: exports_external.array(exports_external.string()).describe("Definition columns touched (name/type/config/position)"),
25301
+ cells_cleared: exports_external.number()
25302
+ });
25303
+ var RevertPropertyReq = exports_external.object({
25304
+ to: exports_external.string().describe("Version token from /api/property/history")
25305
+ });
25306
+ var RevertPropertyResultSchema = exports_external.object({
25307
+ id: exports_external.string(),
25308
+ changed: exports_external.boolean(),
25309
+ fields: exports_external.array(exports_external.string()),
25310
+ restored_cells: exports_external.number(),
25311
+ skipped_cells: exports_external.number(),
25312
+ undeleted: exports_external.boolean(),
25313
+ restored: exports_external.string()
25314
+ });
25315
+ var RecordVersionStateSchema = exports_external.object({
25316
+ id: exports_external.string(),
25317
+ database_id: exports_external.string().nullable(),
25318
+ deleted: exports_external.boolean(),
25319
+ data: exports_external.record(exports_external.string(), exports_external.any()).describe("Cells at that version, keyed by property id"),
25320
+ version: exports_external.string()
25321
+ });
25322
+ var FieldHistoryEntrySchema = exports_external.object({
25323
+ version: exports_external.string(),
25324
+ at: exports_external.string(),
25325
+ node_id: exports_external.string(),
25326
+ value: exports_external.any().optional(),
25327
+ cleared: exports_external.boolean()
25328
+ });
25329
+ var NodeInfoSchema = exports_external.object({
25330
+ node_id: exports_external.string(),
25331
+ label: exports_external.string().nullable().describe("Peer label from pairing; null when unnamed"),
25332
+ self: exports_external.boolean().describe("True for the node serving this request")
25333
+ });
24421
25334
  function need3(req, key) {
24422
25335
  const v3 = new URL(req.url).searchParams.get(key);
24423
25336
  if (!v3)
@@ -24477,6 +25390,17 @@ var webuiRoutes = [
24477
25390
  return updateDatabase(db, need3(req, "id"), body);
24478
25391
  })
24479
25392
  },
25393
+ {
25394
+ method: "POST",
25395
+ path: "/api/database/duplicate",
25396
+ summary: "Copy a database whole \u2014 schema and all records. Query: ?id=<id>",
25397
+ request: DuplicateDatabaseReq,
25398
+ response: DatabaseSchema,
25399
+ handler: handle3(async (req, { db }) => {
25400
+ const body = await req.json().catch(() => ({}));
25401
+ return duplicateDatabase(db, need3(req, "id"), body);
25402
+ })
25403
+ },
24480
25404
  {
24481
25405
  method: "DELETE",
24482
25406
  path: "/api/database",
@@ -24484,6 +25408,18 @@ var webuiRoutes = [
24484
25408
  response: OkSchema2,
24485
25409
  handler: handle3((req, { db }) => ({ ok: deleteDatabase(db, need3(req, "id")) }))
24486
25410
  },
25411
+ {
25412
+ method: "GET",
25413
+ path: "/api/database/activity",
25414
+ summary: "Recent changes across all records of a database, newest first. Query: ?db=<id>&limit=<n>",
25415
+ response: exports_external.array(DatabaseActivityEntrySchema),
25416
+ handler: handle3((req, { db }) => {
25417
+ const limit = opt(req, "limit");
25418
+ return listDatabaseActivity(db, need3(req, "db"), {
25419
+ limit: limit ? Number(limit) : undefined
25420
+ });
25421
+ })
25422
+ },
24487
25423
  {
24488
25424
  method: "GET",
24489
25425
  path: "/api/properties",
@@ -24531,6 +25467,24 @@ var webuiRoutes = [
24531
25467
  response: OkSchema2,
24532
25468
  handler: handle3((req, { db }) => ({ ok: removeProperty(db, need3(req, "id")) }))
24533
25469
  },
25470
+ {
25471
+ method: "GET",
25472
+ path: "/api/property/history",
25473
+ summary: "List a property's definition revisions, newest first. Query: ?id=<id>",
25474
+ response: exports_external.array(PropertyRevisionSchema),
25475
+ handler: handle3((req, { db }) => listPropertyRevisions(db, need3(req, "id")))
25476
+ },
25477
+ {
25478
+ method: "POST",
25479
+ path: "/api/property/revert",
25480
+ summary: "Schema rollback: restore a property's definition and the cells its type-change/removal cleared (user edits since are kept). Query: ?id=<id>",
25481
+ request: RevertPropertyReq,
25482
+ response: RevertPropertyResultSchema,
25483
+ handler: handle3(async (req, { db }) => {
25484
+ const body = await req.json();
25485
+ return revertProperty(db, need3(req, "id"), body.to);
25486
+ })
25487
+ },
24534
25488
  {
24535
25489
  method: "GET",
24536
25490
  path: "/api/records",
@@ -24597,6 +25551,20 @@ var webuiRoutes = [
24597
25551
  response: exports_external.array(RecordRevisionSchema),
24598
25552
  handler: handle3((req, { db }) => listRecordRevisions(db, need3(req, "id")))
24599
25553
  },
25554
+ {
25555
+ method: "GET",
25556
+ path: "/api/record/at",
25557
+ summary: "A record's cells as of a past version. Query: ?id=<id>&version=<token>",
25558
+ response: RecordVersionStateSchema,
25559
+ handler: handle3((req, { db }) => recordAtVersion(db, need3(req, "id"), need3(req, "version")))
25560
+ },
25561
+ {
25562
+ method: "GET",
25563
+ path: "/api/record/field-history",
25564
+ summary: "The write trail of one record cell, newest first. Query: ?id=<id>&prop=<propertyId>",
25565
+ response: exports_external.array(FieldHistoryEntrySchema),
25566
+ handler: handle3((req, { db }) => recordFieldHistory(db, need3(req, "id"), need3(req, "prop")))
25567
+ },
24600
25568
  {
24601
25569
  method: "POST",
24602
25570
  path: "/api/record/revert",
@@ -24659,6 +25627,21 @@ var webuiRoutes = [
24659
25627
  return { ...doc2, version: documentVersion(db, doc2.id) };
24660
25628
  })
24661
25629
  },
25630
+ {
25631
+ method: "POST",
25632
+ path: "/api/document/duplicate",
25633
+ summary: "Copy a document \u2014 title and every block \u2014 next to the source. Query: ?id=<id>",
25634
+ request: DuplicateDocumentReq,
25635
+ response: DocumentSchema,
25636
+ handler: handle3(async (req, { db }) => {
25637
+ const body = await req.json().catch(() => ({}));
25638
+ const doc2 = duplicateDocument(db, need3(req, "id"), {
25639
+ title: body.title,
25640
+ parentId: body.parent_id
25641
+ });
25642
+ return { ...doc2, version: documentVersion(db, doc2.id) };
25643
+ })
25644
+ },
24662
25645
  {
24663
25646
  method: "PATCH",
24664
25647
  path: "/api/document/move",
@@ -24702,6 +25685,20 @@ var webuiRoutes = [
24702
25685
  response: OkSchema2,
24703
25686
  handler: handle3((req, { db }) => ({ ok: deleteDocument(db, need3(req, "id")) }))
24704
25687
  },
25688
+ {
25689
+ method: "GET",
25690
+ path: "/api/nodes",
25691
+ summary: "Known nodes for display: this device plus paired peers with their labels",
25692
+ response: exports_external.array(NodeInfoSchema),
25693
+ handler: handle3((_req, { db }) => {
25694
+ const self = getNodeId(db);
25695
+ const peers = db.query("SELECT node_id, label FROM peers WHERE node_id IS NOT NULL AND node_id <> '' GROUP BY node_id").all();
25696
+ return [
25697
+ { node_id: self, label: null, self: true },
25698
+ ...peers.filter((p2) => p2.node_id !== self).map((p2) => ({ ...p2, self: false }))
25699
+ ];
25700
+ })
25701
+ },
24705
25702
  {
24706
25703
  method: "GET",
24707
25704
  path: "/api/search",
@@ -24884,12 +25881,12 @@ function classifyV6(ip) {
24884
25881
  function normFamily(family) {
24885
25882
  return family === "IPv6" || family === 6 ? "IPv6" : "IPv4";
24886
25883
  }
24887
- function formatUrl(ip, family, port) {
25884
+ function formatUrl(ip, family, port, scheme = "http") {
24888
25885
  if (family === "IPv6") {
24889
25886
  const bare = ip.replace(/%.*$/, "");
24890
- return `http://[${bare}]:${port}`;
25887
+ return `${scheme}://[${bare}]:${port}`;
24891
25888
  }
24892
- return `http://${ip}:${port}`;
25889
+ return `${scheme}://${ip}:${port}`;
24893
25890
  }
24894
25891
  function localAddresses(ifaces = os.networkInterfaces()) {
24895
25892
  const groups = Array.isArray(ifaces) ? { _: ifaces } : ifaces;
@@ -24906,11 +25903,11 @@ function localAddresses(ifaces = os.networkInterfaces()) {
24906
25903
  return out;
24907
25904
  }
24908
25905
  var SCOPE_ORDER = { loopback: 0, lan: 1, public: 2 };
24909
- function resolveEndpoints(host, port, ifaces = os.networkInterfaces()) {
25906
+ function resolveEndpoints(host, port, ifaces = os.networkInterfaces(), scheme = "http") {
24910
25907
  const localhost = {
24911
25908
  scope: "loopback",
24912
25909
  family: "IPv4",
24913
- url: `http://localhost:${port}`
25910
+ url: `${scheme}://localhost:${port}`
24914
25911
  };
24915
25912
  if (isLoopback2(host))
24916
25913
  return [localhost];
@@ -24925,7 +25922,7 @@ function resolveEndpoints(host, port, ifaces = os.networkInterfaces()) {
24925
25922
  if (seen.has(key))
24926
25923
  continue;
24927
25924
  seen.add(key);
24928
- reps.push({ scope: a2.scope, family: a2.family, url: formatUrl(a2.ip, a2.family, port) });
25925
+ reps.push({ scope: a2.scope, family: a2.family, url: formatUrl(a2.ip, a2.family, port, scheme) });
24929
25926
  }
24930
25927
  reps.sort((x4, y3) => SCOPE_ORDER[x4.scope] - SCOPE_ORDER[y3.scope] || x4.family.localeCompare(y3.family));
24931
25928
  return [localhost, ...reps];
@@ -25154,7 +26151,9 @@ var main = defineCommand({
25154
26151
  debug: { type: "boolean", description: "Disable auth on the server (with --server)" },
25155
26152
  token: { type: "string", description: "Server auth token; generated if omitted (with --server)" },
25156
26153
  "sync-interval": { type: "string", description: "Auto-sync interval, e.g. 30s/5m (with --server)" },
25157
- "no-auto-sync": { type: "boolean", description: "Disable the auto-sync timer (with --server)" }
26154
+ "no-auto-sync": { type: "boolean", description: "Disable the auto-sync timer (with --server)" },
26155
+ "tls-cert": { type: "string", description: "TLS certificate PEM path \u2014 serve https directly (with --server)" },
26156
+ "tls-key": { type: "string", description: "TLS private key PEM path (with --server)" }
25158
26157
  },
25159
26158
  subCommands: {
25160
26159
  init: init_default,
@@ -25175,6 +26174,7 @@ var main = defineCommand({
25175
26174
  restore: restore_default,
25176
26175
  doctor: doctor_default,
25177
26176
  repair: repair_default,
26177
+ compact: compact_default,
25178
26178
  config: config_default
25179
26179
  }
25180
26180
  });
@@ -25189,6 +26189,11 @@ var argv2 = process.argv.slice(2);
25189
26189
  if (argv2.includes("--server")) {
25190
26190
  const portFlag = flagValue(argv2, "port");
25191
26191
  const intervalFlag = flagValue(argv2, "sync-interval");
26192
+ const tlsCert = flagValue(argv2, "tls-cert");
26193
+ const tlsKey = flagValue(argv2, "tls-key");
26194
+ if (tlsCert == null !== (tlsKey == null)) {
26195
+ fail("--tls-cert and --tls-key must be provided together", 2);
26196
+ }
25192
26197
  let s2;
25193
26198
  try {
25194
26199
  s2 = startServer({
@@ -25198,13 +26203,14 @@ if (argv2.includes("--server")) {
25198
26203
  token: flagValue(argv2, "token") ?? process.env.METAHUB_TOKEN,
25199
26204
  syncIntervalMs: intervalFlag != null ? parseDuration(intervalFlag, 30000) : undefined,
25200
26205
  autoSync: argv2.includes("--no-auto-sync") ? false : undefined,
26206
+ tls: tlsCert != null && tlsKey != null ? { certPath: tlsCert, keyPath: tlsKey } : undefined,
25201
26207
  ui: { serveAssets: serveWebui, routes: webuiRoutes }
25202
26208
  });
25203
26209
  warmWebui();
25204
26210
  } catch (e3) {
25205
26211
  fail(e3 instanceof Error ? e3.message : String(e3), errorCode(e3) ?? 1);
25206
26212
  }
25207
- const endpoints = resolveEndpoints(s2.host, s2.port);
26213
+ const endpoints = resolveEndpoints(s2.host, s2.port, undefined, s2.tls ? "https" : "http");
25208
26214
  const webui = endpoints[0].url;
25209
26215
  const docs = `${webui}/docs`;
25210
26216
  print({
@@ -25237,4 +26243,4 @@ if (argv2.includes("--server")) {
25237
26243
  runMain(main, { showUsage: showUsage2 });
25238
26244
  }
25239
26245
 
25240
- //# debugId=494FBB064BE8950E64756E2164756E21
26246
+ //# debugId=0ECC27D7190891F564756E2164756E21