@bitcobblers/wod-wiki-tools 0.36.2134 → 0.36.2136

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 (59) hide show
  1. package/dist/bin/wod.cjs +274 -69
  2. package/dist/bin/wod.cjs.map +1 -1
  3. package/dist/bin/wod.mjs +5 -14
  4. package/dist/bin/wod.mjs.map +1 -1
  5. package/dist/{chunk-B3QJYJVI.mjs → chunk-6QNX4FED.mjs} +217 -52
  6. package/dist/chunk-6QNX4FED.mjs.map +1 -0
  7. package/dist/{chunk-EUQWEX6Z.mjs → chunk-CYKNTEGY.mjs} +3 -3
  8. package/dist/chunk-CYKNTEGY.mjs.map +1 -0
  9. package/dist/{compiler-KT6P3AC6.mjs → chunk-FZKXOYMJ.mjs} +4 -5
  10. package/dist/chunk-FZKXOYMJ.mjs.map +1 -0
  11. package/dist/{chunk-32FASQP5.mjs → chunk-MA6BBTWC.mjs} +54 -12
  12. package/dist/chunk-MA6BBTWC.mjs.map +1 -0
  13. package/dist/{chunk-K3GZZFAC.mjs → chunk-V23POHG2.mjs} +10 -9
  14. package/dist/chunk-V23POHG2.mjs.map +1 -0
  15. package/dist/compiler-CTPLIZJK.mjs +9 -0
  16. package/dist/compiler-CTPLIZJK.mjs.map +1 -0
  17. package/dist/index.cjs +274 -58
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +3 -1
  20. package/dist/index.d.ts +3 -1
  21. package/dist/index.mjs +5 -4
  22. package/dist/server/app.cjs +7 -6
  23. package/dist/server/app.cjs.map +1 -1
  24. package/dist/server/app.mjs +1 -1
  25. package/dist/server-JXECWS4A.mjs +11 -0
  26. package/dist/{server-3TJZDKQC.mjs.map → server-JXECWS4A.mjs.map} +1 -1
  27. package/dist/web/playground/assets/main-B-bffxwE.css +2 -0
  28. package/dist/web/playground/assets/main-iLSeEl2E.js +188 -0
  29. package/dist/web/playground/assets/{statementBuilderFlow-DXlHS86r.js → statementBuilderFlow-ugwi0fu0.js} +2 -2
  30. package/dist/web/playground/assets/{vendor-codemirror-BtJgoBjf.js → vendor-codemirror-C41SFn-r.js} +2 -2
  31. package/dist/web/playground/assets/vendor-g-NJOjvC.js +198 -0
  32. package/dist/web/playground/index.html +4 -4
  33. package/dist/web/playground/seed/core/chunks/canvas.20048125.json +1 -0
  34. package/dist/web/playground/seed/core/manifest.json +7 -7
  35. package/dist/web/playground/seed/core/report.html +11 -11
  36. package/dist/web/server/assets/main-B-bffxwE.css +2 -0
  37. package/dist/web/server/assets/main-CRyLxlPN.js +187 -0
  38. package/dist/web/server/assets/{statementBuilderFlow-CXugzTXB.js → statementBuilderFlow-40WaYjdq.js} +2 -2
  39. package/dist/web/server/assets/{vendor-codemirror-BtJgoBjf.js → vendor-codemirror-C41SFn-r.js} +2 -2
  40. package/dist/web/server/assets/vendor-g-NJOjvC.js +198 -0
  41. package/dist/web/server/index.html +4 -4
  42. package/dist/web/server/seed/core/chunks/canvas.20048125.json +1 -0
  43. package/dist/web/server/seed/core/manifest.json +7 -7
  44. package/dist/web/server/seed/core/report.html +9 -9
  45. package/package.json +6 -8
  46. package/dist/chunk-32FASQP5.mjs.map +0 -1
  47. package/dist/chunk-B3QJYJVI.mjs.map +0 -1
  48. package/dist/chunk-EUQWEX6Z.mjs.map +0 -1
  49. package/dist/chunk-K3GZZFAC.mjs.map +0 -1
  50. package/dist/compiler-KT6P3AC6.mjs.map +0 -1
  51. package/dist/server-3TJZDKQC.mjs +0 -11
  52. package/dist/web/playground/assets/main-Bhn9NHgQ.css +0 -2
  53. package/dist/web/playground/assets/main-Bj_mxNMk.js +0 -212
  54. package/dist/web/playground/assets/vendor-BlJqgFME.js +0 -201
  55. package/dist/web/playground/seed/core/chunks/canvas.054cd7db.json +0 -1
  56. package/dist/web/server/assets/main-Bhn9NHgQ.css +0 -2
  57. package/dist/web/server/assets/main-CfAVkURA.js +0 -211
  58. package/dist/web/server/assets/vendor-BlJqgFME.js +0 -201
  59. package/dist/web/server/seed/core/chunks/canvas.054cd7db.json +0 -1
package/dist/index.cjs CHANGED
@@ -8,11 +8,17 @@ var path = require('path');
8
8
  var wodWikiWql = require('@bitcobblers/wod-wiki-wql');
9
9
  var crypto = require('crypto');
10
10
  var promises = require('fs/promises');
11
+ var stream = require('stream');
11
12
  var sql = require('@bitcobblers/wod-wiki-storage/sql');
12
13
  var wodWikiStorage = require('@bitcobblers/wod-wiki-storage');
14
+ var http = require('http');
13
15
  var wodWikiEngine = require('@bitcobblers/wod-wiki-engine');
14
16
 
15
17
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
18
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
19
+
20
+ var http__default = /*#__PURE__*/_interopDefault(http);
21
+
16
22
  var __defProp = Object.defineProperty;
17
23
  var __getOwnPropNames = Object.getOwnPropertyNames;
18
24
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
@@ -1065,10 +1071,10 @@ var init_errors = __esm({
1065
1071
  };
1066
1072
  }
1067
1073
  });
1068
- function bunFile(path) {
1069
- const bun2 = globalThis;
1070
- if (!bun2.Bun) throw new Error("the wod-wiki server handler requires the Bun runtime (https://bun.sh)");
1071
- return bun2.Bun.file(path);
1074
+ function streamFile(path) {
1075
+ const bun = globalThis.Bun;
1076
+ if (bun?.file) return bun.file(path);
1077
+ return stream.Readable.toWeb(fs.createReadStream(path));
1072
1078
  }
1073
1079
  function jsonParam(url, name) {
1074
1080
  const raw = qget(url, name);
@@ -1157,14 +1163,14 @@ async function realpathSafe(path) {
1157
1163
  async function serveFile(real, rel) {
1158
1164
  const cache = rel.includes("seed/chunks/") ? "public, max-age=31536000, immutable" : "no-cache";
1159
1165
  const ext = rel.slice(rel.lastIndexOf(".") + 1).toLowerCase();
1160
- return new Response(bunFile(real), {
1166
+ return new Response(streamFile(real), {
1161
1167
  headers: { "content-type": MIME[ext] ?? "application/octet-stream", "cache-control": cache }
1162
1168
  });
1163
1169
  }
1164
1170
  async function serveSpaShell(distRoot) {
1165
1171
  const index = await realpathSafe(path.join(distRoot, "index.html"));
1166
1172
  if (index && (index === distRoot || index.startsWith(distRoot + path.sep)) && await promises.stat(index).then((s) => s.isFile(), () => false)) {
1167
- return new Response(bunFile(index), {
1173
+ return new Response(streamFile(index), {
1168
1174
  headers: { "content-type": MIME.html, "cache-control": "no-cache" }
1169
1175
  });
1170
1176
  }
@@ -1351,12 +1357,55 @@ var server_exports = {};
1351
1357
  __export(server_exports, {
1352
1358
  runCliServer: () => runCliServer
1353
1359
  });
1354
- function bun() {
1360
+ function withResolvers() {
1361
+ let resolve2;
1362
+ let reject;
1363
+ const promise = new Promise((res, rej) => {
1364
+ resolve2 = res;
1365
+ reject = rej;
1366
+ });
1367
+ return { promise, resolve: resolve2, reject };
1368
+ }
1369
+ function startHttpServer(handler, host, port) {
1355
1370
  const b = globalThis.Bun;
1356
- if (!b) {
1357
- throw new Error("wod server/playground requires the Bun runtime (https://bun.sh)");
1371
+ if (b?.serve) {
1372
+ const s = b.serve({ port, hostname: host, fetch: handler });
1373
+ return Promise.resolve({ stop: () => s.stop() });
1358
1374
  }
1359
- return b;
1375
+ const { promise, resolve: resolve2, reject } = withResolvers();
1376
+ const server = http__default.default.createServer(async (req, res) => {
1377
+ try {
1378
+ const url = new URL(req.url || "/", `http://${req.headers.host || `${host}:${port}`}`);
1379
+ const hasBody = req.method !== "GET" && req.method !== "HEAD";
1380
+ const webReq = new Request(url, {
1381
+ method: req.method,
1382
+ headers: req.headers,
1383
+ body: hasBody ? stream.Readable.toWeb(req) : null,
1384
+ ...hasBody ? { duplex: "half" } : {}
1385
+ });
1386
+ const webRes = await handler(webReq);
1387
+ res.writeHead(webRes.status, Object.fromEntries(webRes.headers.entries()));
1388
+ if (webRes.body) {
1389
+ stream.Readable.fromWeb(webRes.body).pipe(res);
1390
+ } else {
1391
+ res.end();
1392
+ }
1393
+ } catch {
1394
+ res.statusCode = 500;
1395
+ res.end("internal server error");
1396
+ }
1397
+ });
1398
+ server.once("error", reject);
1399
+ server.listen(port, host, () => {
1400
+ resolve2({
1401
+ stop: () => {
1402
+ const close = withResolvers();
1403
+ server.close(() => close.resolve());
1404
+ return close.promise;
1405
+ }
1406
+ });
1407
+ });
1408
+ return promise;
1360
1409
  }
1361
1410
  function moduleDir() {
1362
1411
  const p = nodePath();
@@ -1410,7 +1459,6 @@ function resolveBind(mode, options) {
1410
1459
  return { host, port };
1411
1460
  }
1412
1461
  async function runCliServer(mode, options, writeOut) {
1413
- const runtime = bun();
1414
1462
  const { host, port } = resolveBind(mode, options);
1415
1463
  let db;
1416
1464
  let dbLabel = "no database";
@@ -1431,7 +1479,7 @@ async function runCliServer(mode, options, writeOut) {
1431
1479
  try {
1432
1480
  const dist = resolveDist(mode, options);
1433
1481
  const handler = createHandler(db?.db ?? null, { playgroundDist: dist });
1434
- server = runtime.serve({ port, hostname: host, fetch: handler });
1482
+ server = await startHttpServer(handler, host, port);
1435
1483
  if (mode === "server") {
1436
1484
  writeOut(`wod server: http://${host}:${port} \u2014 ${dbLabel} \xB7 dist ${dist}`);
1437
1485
  } else {
@@ -2129,18 +2177,24 @@ async function saveSource(db, options, startTime) {
2129
2177
  throw new Error(`Stale Note source revision for ${noteId}; expected ${expected}`);
2130
2178
  }
2131
2179
  let sourceRevision = existing?.rawContent === options.source ? existing.sourceRevision : (existing?.sourceRevision ?? 0) + 1;
2180
+ const history = await tx.readonly("block_revisions").getAllFromIndex("by-note", noteId);
2181
+ const latest = /* @__PURE__ */ new Map();
2182
+ for (const row of history) {
2183
+ if ((latest.get(row.blockId)?.version ?? 0) < row.version) latest.set(row.blockId, row);
2184
+ }
2185
+ let ordinal = 0;
2132
2186
  const blocks = wodWikiCore.reconcileNoteBlocks(options.source, {
2133
2187
  sourceRevision,
2134
2188
  blocks: options.blocks ?? (existing?.rawContent === options.source ? existing.blocks : void 0),
2135
2189
  registeredWidgets,
2136
- createId: randomId
2190
+ createId: () => {
2191
+ while (true) {
2192
+ const candidate = uuid5(CLI_UUID_NAMESPACE, `${noteId}:block:${ordinal++}`);
2193
+ if (!latest.has(candidate)) return candidate;
2194
+ }
2195
+ }
2137
2196
  });
2138
2197
  wodWikiCore.validateNoteBlocks(options.source, blocks, sourceRevision, { registeredWidgets });
2139
- const history = await tx.readonly("block_revisions").getAllFromIndex("by-note", noteId);
2140
- const latest = /* @__PURE__ */ new Map();
2141
- for (const row of history) {
2142
- if ((latest.get(row.blockId)?.version ?? 0) < row.version) latest.set(row.blockId, row);
2143
- }
2144
2198
  const revisions = [];
2145
2199
  for (const block of Object.values(blocks)) {
2146
2200
  const saved = latest.get(block.id);
@@ -2162,6 +2216,8 @@ async function saveSource(db, options, startTime) {
2162
2216
  sourceRevision++;
2163
2217
  for (const block of Object.values(blocks)) block.sourceRevision = sourceRevision;
2164
2218
  }
2219
+ const fm = wodWikiEngine.readFrontmatterMeta(options.source);
2220
+ const catalog = typeof fm["catalog"] === "string" && fm["catalog"].trim() || typeof fm["collection"] === "string" && fm["collection"].trim() || existing?.catalog;
2165
2221
  const note = {
2166
2222
  ...existing,
2167
2223
  id: noteId,
@@ -2171,7 +2227,8 @@ async function saveSource(db, options, startTime) {
2171
2227
  createdAt: existing?.createdAt ?? now,
2172
2228
  rawContent: options.source,
2173
2229
  sourceRevision,
2174
- blocks
2230
+ blocks,
2231
+ ...catalog ? { catalog } : {}
2175
2232
  };
2176
2233
  const priorRows = await tx.readonly("block_index").getAllFromIndex("by-note", noteId);
2177
2234
  const priorEfforts = await tx.readonly("block_efforts").getAllFromIndex("by-note", noteId);
@@ -2199,6 +2256,7 @@ async function saveSource(db, options, startTime) {
2199
2256
  noteTitle: note.title,
2200
2257
  createdAt: prior?.createdAt ?? now,
2201
2258
  blockContentId: fingerprint,
2259
+ ...catalog ? { catalog } : {},
2202
2260
  ...note.sourceId ? { sourceId: note.sourceId } : {}
2203
2261
  });
2204
2262
  if (block.kind === "wod") {
@@ -2416,6 +2474,136 @@ function defaultSeedDirs(cwd) {
2416
2474
  return ["core", "community"].map((id) => nodePath().resolve(base, id)).filter((dir) => nodeFs().existsSync(nodePath().resolve(dir, "manifest.json")));
2417
2475
  }
2418
2476
 
2477
+ // src/cli/publish.ts
2478
+ init_db();
2479
+ init_contract();
2480
+ init_compiler();
2481
+ init_report();
2482
+ async function publishCatalogSeed(db, options) {
2483
+ const t0 = Date.now();
2484
+ const fs = nodeFs();
2485
+ const path = nodePath();
2486
+ const allNotes = await db.storage.readonly("notes").getAll();
2487
+ if (allNotes.length === 0) {
2488
+ throw new Error(`[wod publish] No notes found in database ${db.path}`);
2489
+ }
2490
+ const notesByCatalog = /* @__PURE__ */ new Map();
2491
+ for (const note of allNotes) {
2492
+ const fm = wodWikiEngine.readFrontmatterMeta(note.rawContent);
2493
+ const cat = typeof fm["catalog"] === "string" && fm["catalog"].trim() || typeof fm["collection"] === "string" && fm["collection"].trim() || note.catalog || wodWikiWql.catalogOfItem(note);
2494
+ const key = cat || "__uncataloged__";
2495
+ const list = notesByCatalog.get(key) ?? [];
2496
+ list.push(note);
2497
+ notesByCatalog.set(key, list);
2498
+ }
2499
+ const targetCatalogs = /* @__PURE__ */ new Map();
2500
+ if (options.catalog) {
2501
+ const matching = notesByCatalog.get(options.catalog);
2502
+ if (matching) {
2503
+ targetCatalogs.set(options.catalog, matching);
2504
+ } else if (notesByCatalog.has("__uncataloged__") && notesByCatalog.size === 1) {
2505
+ targetCatalogs.set(options.catalog, notesByCatalog.get("__uncataloged__"));
2506
+ } else {
2507
+ const available = [...notesByCatalog.keys()].filter((k) => k !== "__uncataloged__");
2508
+ throw new Error(
2509
+ `[wod publish] Catalog "${options.catalog}" not found in ${db.path}.` + (available.length > 0 ? ` Available catalogs: ${available.join(", ")}` : "")
2510
+ );
2511
+ }
2512
+ } else {
2513
+ for (const [cat, notes] of notesByCatalog.entries()) {
2514
+ targetCatalogs.set(cat === "__uncataloged__" ? "default" : cat, notes);
2515
+ }
2516
+ }
2517
+ const allIndexRows = await db.storage.readonly("block_index").getAll();
2518
+ const allEffortRows = await db.storage.readonly("block_efforts").getAll();
2519
+ const indexByNote = /* @__PURE__ */ new Map();
2520
+ for (const row of allIndexRows) {
2521
+ const list = indexByNote.get(row.noteId) ?? [];
2522
+ list.push(row);
2523
+ indexByNote.set(row.noteId, list);
2524
+ }
2525
+ const effortsByNote = /* @__PURE__ */ new Map();
2526
+ for (const row of allEffortRows) {
2527
+ const list = effortsByNote.get(row.noteId) ?? [];
2528
+ list.push(row);
2529
+ effortsByNote.set(row.noteId, list);
2530
+ }
2531
+ const outDir = path.resolve(process.cwd(), options.outDir);
2532
+ const chunksDir = path.join(outDir, "chunks");
2533
+ fs.mkdirSync(chunksDir, { recursive: true });
2534
+ const manifestChunks = [];
2535
+ let totalNotes = 0;
2536
+ const publishedCatalogs = [];
2537
+ for (const [catalogName, notes] of targetCatalogs.entries()) {
2538
+ publishedCatalogs.push(catalogName);
2539
+ const chunkRows = [];
2540
+ for (const note of notes) {
2541
+ const stem = (note.title || note.id).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") || "note";
2542
+ const notePath = note.sourcePath ?? `markdown/collections/${catalogName}/${stem}.md`;
2543
+ const noteBlockIndex = indexByNote.get(note.id) ?? [];
2544
+ const noteBlockEfforts = effortsByNote.get(note.id) ?? [];
2545
+ const blockIndex = noteBlockIndex.length > 0 ? noteBlockIndex.map((b) => ({
2546
+ ...b,
2547
+ isStatic: true,
2548
+ sourceId: b.sourceId ?? `collection:${catalogName}/${stem}`,
2549
+ sourcePath: b.sourcePath ?? notePath,
2550
+ catalog: catalogName
2551
+ })) : compileSeedRow(notePath, note.rawContent, {
2552
+ routeId: `${catalogName}/${stem}`,
2553
+ sourceId: `collection:${catalogName}/${stem}`,
2554
+ createdAt: note.createdAt,
2555
+ noteTitle: note.title
2556
+ }).blockIndex;
2557
+ const blockEfforts = noteBlockEfforts.map((e) => ({
2558
+ ...e,
2559
+ isStatic: true,
2560
+ sourcePath: e.sourcePath ?? notePath
2561
+ }));
2562
+ chunkRows.push({
2563
+ path: notePath,
2564
+ content: note.rawContent,
2565
+ blocks: note.blocks,
2566
+ blockIndex,
2567
+ blockEfforts
2568
+ });
2569
+ }
2570
+ const chunkId = `collection.${catalogName}`;
2571
+ const json2 = JSON.stringify(chunkRows);
2572
+ const digest = nodeCrypto().createHash("sha256").update(json2).digest();
2573
+ const sha = [...digest].map((b) => b.toString(16).padStart(2, "0")).join("");
2574
+ const chunkFile = `chunks/${chunkId}.${sha.slice(0, 8)}.json`;
2575
+ fs.writeFileSync(path.join(outDir, chunkFile), json2, "utf-8");
2576
+ manifestChunks.push({
2577
+ id: chunkId,
2578
+ path: chunkFile,
2579
+ sha256: sha,
2580
+ bytes: Buffer.byteLength(json2, "utf8"),
2581
+ count: chunkRows.length,
2582
+ kind: "notes"
2583
+ });
2584
+ totalNotes += chunkRows.length;
2585
+ }
2586
+ const manifest = {
2587
+ schema: SEED_SCHEMA2,
2588
+ version: options.version ?? 1,
2589
+ versionLabel: `v${options.version ?? 1}`,
2590
+ builtAt: (/* @__PURE__ */ new Date()).toISOString(),
2591
+ chunks: manifestChunks
2592
+ };
2593
+ fs.writeFileSync(path.join(outDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", "utf-8");
2594
+ writeBuildReport(outDir, {
2595
+ manifest,
2596
+ totalMs: Date.now() - t0,
2597
+ passMs: { scan: 0, blockIndex: 0, parseGate: 0, blockEfforts: 0, write: Date.now() - t0 },
2598
+ fileCount: totalNotes});
2599
+ return {
2600
+ manifest,
2601
+ outDir,
2602
+ catalogs: publishedCatalogs,
2603
+ totalNotes
2604
+ };
2605
+ }
2606
+
2419
2607
  // src/cli/formatters.ts
2420
2608
  function escapeCsvField(field) {
2421
2609
  if (field === void 0 || field === null) return "";
@@ -2706,7 +2894,7 @@ ${escapeCsvField(data.parsed.metric)},${data.scalar},${escapeCsvField(data.unit
2706
2894
  }
2707
2895
 
2708
2896
  // src/version.ts
2709
- var VERSION = "0.36.2134";
2897
+ var VERSION = "0.36.2136";
2710
2898
 
2711
2899
  // src/cli/runner.ts
2712
2900
  function getFs2() {
@@ -2851,9 +3039,16 @@ function parseCliArgs(args) {
2851
3039
  i++;
2852
3040
  } else if (arg.startsWith("--dir=")) {
2853
3041
  result.dir = arg.slice("--dir=".length);
3042
+ } else if (arg === "--seed-version") {
3043
+ const v = Number(args[++i]);
3044
+ if (Number.isSafeInteger(v) && v >= 0) result.seedVersion = v;
3045
+ i++;
3046
+ } else if (arg.startsWith("--seed-version=")) {
3047
+ const v = Number(arg.slice("--seed-version=".length));
3048
+ if (Number.isSafeInteger(v) && v >= 0) result.seedVersion = v;
2854
3049
  i++;
2855
3050
  } else if (!result.subcommand) {
2856
- if (arg === "parse" || arg === "run" || arg === "query" || arg === "download" || arg === "import" || arg === "build" || arg === "seed" || arg === "reset" || arg === "init" || arg === "server" || arg === "serve" || arg === "playground" || arg === "help" || arg === "version") {
3051
+ if (arg === "parse" || arg === "run" || arg === "query" || arg === "download" || arg === "export" || arg === "import" || arg === "publish" || arg === "build" || arg === "seed" || arg === "reset" || arg === "init" || arg === "server" || arg === "serve" || arg === "playground" || arg === "help" || arg === "version") {
2857
3052
  result.subcommand = arg;
2858
3053
  } else {
2859
3054
  result.target = arg;
@@ -2879,9 +3074,9 @@ function parseCliArgs(args) {
2879
3074
  function readBunStdinText() {
2880
3075
  const g = globalThis;
2881
3076
  if (g && typeof g === "object" && "Bun" in g) {
2882
- const bun2 = g.Bun;
2883
- if (bun2 && typeof bun2 === "object" && "stdin" in bun2) {
2884
- const stdin = bun2.stdin;
3077
+ const bun = g.Bun;
3078
+ if (bun && typeof bun === "object" && "stdin" in bun) {
3079
+ const stdin = bun.stdin;
2885
3080
  if (stdin && typeof stdin === "object" && "text" in stdin) {
2886
3081
  const text = stdin.text;
2887
3082
  if (typeof text === "function") return text;
@@ -2914,8 +3109,10 @@ USAGE:
2914
3109
  wod parse <file | -> [options]
2915
3110
  wod run <file | -> [options]
2916
3111
  wod query '<wql-string>' [options]
2917
- wod download <note|session|effort|equipment> <id> [options]
2918
- wod import <note|session|effort|equipment> <file> [options]
3112
+ wod export <note|playground|session|effort|equipment> <id> [options]
3113
+ wod download <note|playground|session|effort|equipment> <id> [options]
3114
+ wod import <note|playground|session|effort|equipment> <file> [options]
3115
+ wod publish [catalog] [options]
2919
3116
  wod build [dir] [options]
2920
3117
  wod seed [options]
2921
3118
  wod reset [options]
@@ -2935,23 +3132,23 @@ SUBCOMMANDS:
2935
3132
  execution-log IR
2936
3133
  query '<wql>' Evaluate WQL against the database (or an explicit
2937
3134
  dataset) and emit QueryResult / RowsQueryResult IR
2938
- download note <id> Export the note's authored markdown to <id>.md.
3135
+ export note <id> Export the note's authored markdown to <id>.md.
3136
+ (alias: export playground, download note).
2939
3137
  The frontmatter identity stamp allows re-import.
2940
3138
  Frontmatter tags (general + typed equipment/quality/\u2026)
2941
3139
  are materialized into the tag graph on parse/import.
2942
- download session <id>
3140
+ export session <id>
2943
3141
  Write the session's generated metrics (event + summary
2944
3142
  rows, one CSV row per metric) to <id>.csv. Import back
2945
- with 'wod import session'.
2946
- download effort <slug>
3143
+ with 'wod import session'. (alias: download session).
3144
+ export effort <slug>
2947
3145
  Write the effort registry row as corpus-style markdown
2948
- (frontmatter id/slug/label/aliases/baseAttributes + body)
2949
3146
  to <slug>.md. Import back with 'wod import effort'.
2950
- download equipment
2951
- Write every equipment tag as a one-column CSV to
3147
+ export equipment Write every equipment tag as a one-column CSV to
2952
3148
  equipment.csv. Import back with 'wod import equipment'.
2953
- import note <file> Re-import exported note markdown (same as parse with
2954
- the stamped identity).
3149
+ download \u2026 Accepted alias for 'wod export'.
3150
+ import note <file> Re-import exported note markdown (alias: import playground;
3151
+ same as parse with the stamped identity).
2955
3152
  import session <file>
2956
3153
  Import a session + its events from a downloaded CSV.
2957
3154
  The exact saved block revision must already exist.
@@ -2959,6 +3156,10 @@ SUBCOMMANDS:
2959
3156
  Upsert an effort registry row from corpus-style markdown.
2960
3157
  import equipment <file>
2961
3158
  Create/upgrade equipment tags (CSV, one label per row).
3159
+ publish [catalog] Publish catalog notes from the database into a configured
3160
+ seed directory (manifest.json, chunks/, report.html)
3161
+ compatible with 'wod seed --source <dir>'. Override target
3162
+ with -o/--out/--output (defaults to ./dist/seed/<catalog>).
2962
3163
  build [dir] Compile a seed corpus: validate every .md under
2963
3164
  <dir>/markdown (parity gate + dashboard format gate) and
2964
3165
  write manifest.json + content-hashed chunks/ +
@@ -3007,9 +3208,9 @@ DATABASE (init / parse / run / query / download / import / seed / reset / server
3007
3208
  an error. Without it: ./.env when present, otherwise
3008
3209
  the workspace-root .env. Credentials are never logged.
3009
3210
 
3010
- HOSTING (for 'server' / 'playground'; requires the Bun runtime):
3211
+ HOSTING (for 'server' / 'playground'; supported on Node and Bun):
3011
3212
  server HTTP API over the configured database plus the frontend
3012
- packaged inside the engine module. Requires an
3213
+ packaged inside the tools module. Requires an
3013
3214
  initialized database. ('serve' is an accepted alias.)
3014
3215
  playground Serves the playground app backed by browser IndexedDB;
3015
3216
  the configured database is not touched.
@@ -3269,21 +3470,22 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3269
3470
  writeDebugFile("query", ir);
3270
3471
  return 0;
3271
3472
  }
3272
- if (parsed.subcommand === "download") {
3473
+ if (parsed.subcommand === "download" || parsed.subcommand === "export") {
3474
+ const verb = parsed.subcommand;
3273
3475
  const kind = parsed.target;
3274
3476
  const id = parsed.target2;
3275
- const DOWNLOAD_KINDS = ["note", "session", "effort", "equipment"];
3477
+ const DOWNLOAD_KINDS = ["note", "playground", "session", "effort", "equipment"];
3276
3478
  if (!id && kind !== "equipment" || !DOWNLOAD_KINDS.includes(kind ?? "")) {
3277
- writeErr("[wod download] Usage: wod download <note|session|effort|equipment> <id> [-o <path>]");
3479
+ writeErr(`[wod ${verb}] Usage: wod ${verb} <note|playground|session|effort|equipment> <id> [-o <path>]`);
3278
3480
  return 1;
3279
3481
  }
3280
3482
  const targetId = id;
3281
3483
  const db = await openCliDb(cliDb());
3282
3484
  try {
3283
- if (kind === "note") {
3485
+ if (kind === "note" || kind === "playground") {
3284
3486
  const bundle = await getNoteBundle(db.db, targetId);
3285
3487
  if (!bundle.note) {
3286
- writeErr(`[wod download] Note not found in ${db.path}: ${id}`);
3488
+ writeErr(`[wod ${verb}] Note not found in ${db.path}: ${id}`);
3287
3489
  return 1;
3288
3490
  }
3289
3491
  const markdown = wodWikiEngine.composeNoteMarkdown(bundle.note);
@@ -3293,11 +3495,11 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3293
3495
  } else {
3294
3496
  nodeFs().writeFileSync(target, markdown, "utf-8");
3295
3497
  }
3296
- if (parsed.output !== "-") writeOut(`[wod download] note ${id} \u2192 ${target}`);
3498
+ if (parsed.output !== "-") writeOut(`[wod ${verb}] ${kind} ${id} \u2192 ${target}`);
3297
3499
  } else if (kind === "session") {
3298
3500
  const bundle = await getSessionBundle(db.db, targetId);
3299
3501
  if (!bundle) {
3300
- writeErr(`[wod download] Session not found in ${db.path}: ${id}`);
3502
+ writeErr(`[wod ${verb}] Session not found in ${db.path}: ${id}`);
3301
3503
  return 1;
3302
3504
  }
3303
3505
  const csv = wodWikiEngine.sessionToCsv(bundle.session, bundle.events);
@@ -3307,11 +3509,11 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3307
3509
  } else {
3308
3510
  nodeFs().writeFileSync(target, csv, "utf-8");
3309
3511
  }
3310
- if (parsed.output !== "-") writeOut(`[wod download] session ${id} \u2192 ${target} (${bundle.events.length} events)`);
3512
+ if (parsed.output !== "-") writeOut(`[wod ${verb}] session ${id} \u2192 ${target} (${bundle.events.length} events)`);
3311
3513
  } else if (kind === "effort") {
3312
3514
  const effort = await getEffortBySlug(db.db, targetId);
3313
3515
  if (!effort) {
3314
- writeErr(`[wod download] Effort not found in ${db.path}: ${id}`);
3516
+ writeErr(`[wod ${verb}] Effort not found in ${db.path}: ${id}`);
3315
3517
  return 1;
3316
3518
  }
3317
3519
  const markdown = wodWikiEngine.effortToMarkdown(effort);
@@ -3321,7 +3523,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3321
3523
  } else {
3322
3524
  nodeFs().writeFileSync(target, markdown, "utf-8");
3323
3525
  }
3324
- writeOut(`[wod download] effort ${id} \u2192 ${target}`);
3526
+ writeOut(`[wod ${verb}] effort ${id} \u2192 ${target}`);
3325
3527
  } else {
3326
3528
  const labels = (await listTagsByType(db.db, "equipment")).map((t) => t.label).sort();
3327
3529
  const csv = wodWikiEngine.equipmentToCsv(labels);
@@ -3331,7 +3533,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3331
3533
  } else {
3332
3534
  nodeFs().writeFileSync(target, csv, "utf-8");
3333
3535
  }
3334
- writeOut(`[wod download] equipment \u2192 ${target} (${labels.length} labels)`);
3536
+ writeOut(`[wod ${verb}] equipment \u2192 ${target} (${labels.length} labels)`);
3335
3537
  }
3336
3538
  } finally {
3337
3539
  await db.close();
@@ -3341,9 +3543,9 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3341
3543
  if (parsed.subcommand === "import") {
3342
3544
  const kind = parsed.target;
3343
3545
  const file = parsed.target2;
3344
- const IMPORT_KINDS = ["note", "session", "effort", "equipment"];
3546
+ const IMPORT_KINDS = ["note", "playground", "session", "effort", "equipment"];
3345
3547
  if (!file || !IMPORT_KINDS.includes(kind ?? "")) {
3346
- writeErr("[wod import] Usage: wod import <note|session|effort|equipment> <file>");
3548
+ writeErr("[wod import] Usage: wod import <note|playground|session|effort|equipment> <file>");
3347
3549
  return 1;
3348
3550
  }
3349
3551
  const fs = nodeFs();
@@ -3351,7 +3553,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3351
3553
  writeErr(`[wod import] File not found: ${file}`);
3352
3554
  return 1;
3353
3555
  }
3354
- if (kind === "note") {
3556
+ if (kind === "note" || kind === "playground") {
3355
3557
  const source = fs.readFileSync(file, "utf-8");
3356
3558
  const identity = wodWikiEngine.extractNoteIdentity(source);
3357
3559
  const tags = wodWikiEngine.extractTypedTags(source);
@@ -3365,7 +3567,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3365
3567
  tags
3366
3568
  });
3367
3569
  writeOut(
3368
- `[wod import] note ${persisted.noteId}${persisted.created ? "" : " (updated)"} \u2014 ${persisted.blocks} blocks${persisted.tags > 0 ? `, ${persisted.tags} tags` : ""} \u2192 ${db2.path}`
3570
+ `[wod import] ${kind} ${persisted.noteId}${persisted.created ? "" : " (updated)"} \u2014 ${persisted.blocks} blocks${persisted.tags > 0 ? `, ${persisted.tags} tags` : ""} \u2192 ${db2.path}`
3369
3571
  );
3370
3572
  } finally {
3371
3573
  await db2.close();
@@ -3414,6 +3616,26 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3414
3616
  }
3415
3617
  return 0;
3416
3618
  }
3619
+ if (parsed.subcommand === "publish") {
3620
+ const isDir = (s) => Boolean(s && (s.startsWith(".") || s.startsWith("/") || s.includes("/") || s.includes("\\")));
3621
+ const catalog = isDir(parsed.target) ? void 0 : parsed.target;
3622
+ const targetDir = isDir(parsed.target) ? parsed.target : parsed.target2;
3623
+ const outDir = parsed.output ?? targetDir ?? (catalog ? `./dist/seed/${catalog}` : "./dist/seed");
3624
+ const db = await openCliDb(cliDb());
3625
+ try {
3626
+ const result = await publishCatalogSeed(db, {
3627
+ catalog,
3628
+ outDir,
3629
+ version: parsed.seedVersion
3630
+ });
3631
+ writeOut(
3632
+ `[wod publish] published ${result.catalogs.join(", ")} (${result.totalNotes} notes) \u2192 ${result.outDir} (manifest schema v${result.manifest.schema}, version ${result.manifest.version})`
3633
+ );
3634
+ } finally {
3635
+ await db.close();
3636
+ }
3637
+ return 0;
3638
+ }
3417
3639
  if (parsed.subcommand === "build") {
3418
3640
  const cwd = process.cwd();
3419
3641
  const root = nodePath().resolve(cwd, parsed.dir ?? parsed.target ?? ".");
@@ -3490,12 +3712,6 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3490
3712
  return 0;
3491
3713
  }
3492
3714
  if (parsed.subcommand === "server" || parsed.subcommand === "serve" || parsed.subcommand === "playground") {
3493
- if (!("Bun" in globalThis)) {
3494
- writeErr(
3495
- `[wod ${parsed.subcommand}] Error: hosting requires the Bun runtime (https://bun.sh). Install Bun to host: https://bun.sh`
3496
- );
3497
- return 1;
3498
- }
3499
3715
  const mode = parsed.subcommand === "playground" ? "playground" : "server";
3500
3716
  const { runCliServer: runCliServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
3501
3717
  const options = { env: cliEnv() };