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