@bitcobblers/wod-wiki-tools 0.36.2133 → 0.36.2135

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 (55) hide show
  1. package/dist/bin/wod.cjs +274 -58
  2. package/dist/bin/wod.cjs.map +1 -1
  3. package/dist/bin/wod.mjs +3 -2
  4. package/dist/bin/wod.mjs.map +1 -1
  5. package/dist/{chunk-EUQWEX6Z.mjs → chunk-CYKNTEGY.mjs} +3 -3
  6. package/dist/chunk-CYKNTEGY.mjs.map +1 -0
  7. package/dist/{compiler-KT6P3AC6.mjs → chunk-FZKXOYMJ.mjs} +4 -5
  8. package/dist/chunk-FZKXOYMJ.mjs.map +1 -0
  9. package/dist/{chunk-DZESCGCM.mjs → chunk-MA6BBTWC.mjs} +54 -12
  10. package/dist/chunk-MA6BBTWC.mjs.map +1 -0
  11. package/dist/{chunk-OZI2FYPF.mjs → chunk-OTJAPVT2.mjs} +217 -52
  12. package/dist/chunk-OTJAPVT2.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-Z2T7IGZX.mjs.map → server-JXECWS4A.mjs.map} +1 -1
  27. package/dist/web/playground/assets/main-Bod1taHc.js +188 -0
  28. package/dist/web/playground/assets/{statementBuilderFlow-gvzmxfiE.js → statementBuilderFlow-CCaLco1R.js} +2 -2
  29. package/dist/web/playground/assets/{vendor-codemirror-BtJgoBjf.js → vendor-codemirror-C41SFn-r.js} +2 -2
  30. package/dist/web/playground/assets/vendor-g-NJOjvC.js +198 -0
  31. package/dist/web/playground/index.html +3 -3
  32. package/dist/web/playground/seed/core/chunks/canvas.2d3e2e0e.json +1 -0
  33. package/dist/web/playground/seed/core/manifest.json +7 -7
  34. package/dist/web/playground/seed/core/report.html +11 -11
  35. package/dist/web/server/assets/main-DtiBHHer.js +187 -0
  36. package/dist/web/server/assets/{statementBuilderFlow-DT1tyXgc.js → statementBuilderFlow-MCfILbWE.js} +2 -2
  37. package/dist/web/server/assets/{vendor-codemirror-BtJgoBjf.js → vendor-codemirror-C41SFn-r.js} +2 -2
  38. package/dist/web/server/assets/vendor-g-NJOjvC.js +198 -0
  39. package/dist/web/server/index.html +3 -3
  40. package/dist/web/server/seed/core/chunks/canvas.2d3e2e0e.json +1 -0
  41. package/dist/web/server/seed/core/manifest.json +7 -7
  42. package/dist/web/server/seed/core/report.html +10 -10
  43. package/package.json +6 -8
  44. package/dist/chunk-DZESCGCM.mjs.map +0 -1
  45. package/dist/chunk-EUQWEX6Z.mjs.map +0 -1
  46. package/dist/chunk-K3GZZFAC.mjs.map +0 -1
  47. package/dist/chunk-OZI2FYPF.mjs.map +0 -1
  48. package/dist/compiler-KT6P3AC6.mjs.map +0 -1
  49. package/dist/server-Z2T7IGZX.mjs +0 -11
  50. package/dist/web/playground/assets/main-Bh_mthqc.js +0 -212
  51. package/dist/web/playground/assets/vendor-BlJqgFME.js +0 -201
  52. package/dist/web/playground/seed/core/chunks/canvas.d2a5e6e7.json +0 -1
  53. package/dist/web/server/assets/main-CPRFCrjV.js +0 -211
  54. package/dist/web/server/assets/vendor-BlJqgFME.js +0 -201
  55. package/dist/web/server/seed/core/chunks/canvas.d2a5e6e7.json +0 -1
package/dist/bin/wod.cjs CHANGED
@@ -9,11 +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');
15
+ var http = require('http');
14
16
  var wodWikiEngine = require('@bitcobblers/wod-wiki-engine');
15
17
 
16
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
+
17
23
  var __defProp = Object.defineProperty;
18
24
  var __getOwnPropNames = Object.getOwnPropertyNames;
19
25
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
@@ -1066,10 +1072,10 @@ var init_errors = __esm({
1066
1072
  };
1067
1073
  }
1068
1074
  });
1069
- function bunFile(path) {
1070
- const bun2 = globalThis;
1071
- if (!bun2.Bun) throw new Error("the wod-wiki server handler requires the Bun runtime (https://bun.sh)");
1072
- 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));
1073
1079
  }
1074
1080
  function jsonParam(url, name) {
1075
1081
  const raw = qget(url, name);
@@ -1158,14 +1164,14 @@ async function realpathSafe(path) {
1158
1164
  async function serveFile(real, rel) {
1159
1165
  const cache = rel.includes("seed/chunks/") ? "public, max-age=31536000, immutable" : "no-cache";
1160
1166
  const ext = rel.slice(rel.lastIndexOf(".") + 1).toLowerCase();
1161
- return new Response(bunFile(real), {
1167
+ return new Response(streamFile(real), {
1162
1168
  headers: { "content-type": MIME[ext] ?? "application/octet-stream", "cache-control": cache }
1163
1169
  });
1164
1170
  }
1165
1171
  async function serveSpaShell(distRoot) {
1166
1172
  const index = await realpathSafe(path.join(distRoot, "index.html"));
1167
1173
  if (index && (index === distRoot || index.startsWith(distRoot + path.sep)) && await promises.stat(index).then((s) => s.isFile(), () => false)) {
1168
- return new Response(bunFile(index), {
1174
+ return new Response(streamFile(index), {
1169
1175
  headers: { "content-type": MIME.html, "cache-control": "no-cache" }
1170
1176
  });
1171
1177
  }
@@ -1352,12 +1358,55 @@ var server_exports = {};
1352
1358
  __export(server_exports, {
1353
1359
  runCliServer: () => runCliServer
1354
1360
  });
1355
- 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) {
1356
1371
  const b = globalThis.Bun;
1357
- if (!b) {
1358
- throw new Error("wod server/playground requires the Bun runtime \u2014 start with: bun wod server");
1372
+ if (b?.serve) {
1373
+ const s = b.serve({ port, hostname: host, fetch: handler });
1374
+ return Promise.resolve({ stop: () => s.stop() });
1359
1375
  }
1360
- 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;
1361
1410
  }
1362
1411
  function moduleDir() {
1363
1412
  const p = nodePath();
@@ -1411,7 +1460,6 @@ function resolveBind(mode, options) {
1411
1460
  return { host, port };
1412
1461
  }
1413
1462
  async function runCliServer(mode, options, writeOut) {
1414
- const runtime = bun();
1415
1463
  const { host, port } = resolveBind(mode, options);
1416
1464
  let db;
1417
1465
  let dbLabel = "no database";
@@ -1432,7 +1480,7 @@ async function runCliServer(mode, options, writeOut) {
1432
1480
  try {
1433
1481
  const dist = resolveDist(mode, options);
1434
1482
  const handler = createHandler(db?.db ?? null, { playgroundDist: dist });
1435
- server = runtime.serve({ port, hostname: host, fetch: handler });
1483
+ server = await startHttpServer(handler, host, port);
1436
1484
  if (mode === "server") {
1437
1485
  writeOut(`wod server: http://${host}:${port} \u2014 ${dbLabel} \xB7 dist ${dist}`);
1438
1486
  } else {
@@ -2130,18 +2178,24 @@ async function saveSource(db, options, startTime) {
2130
2178
  throw new Error(`Stale Note source revision for ${noteId}; expected ${expected}`);
2131
2179
  }
2132
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;
2133
2187
  const blocks = wodWikiCore.reconcileNoteBlocks(options.source, {
2134
2188
  sourceRevision,
2135
2189
  blocks: options.blocks ?? (existing?.rawContent === options.source ? existing.blocks : void 0),
2136
2190
  registeredWidgets,
2137
- 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
+ }
2138
2197
  });
2139
2198
  wodWikiCore.validateNoteBlocks(options.source, blocks, sourceRevision, { registeredWidgets });
2140
- const history = await tx.readonly("block_revisions").getAllFromIndex("by-note", noteId);
2141
- const latest = /* @__PURE__ */ new Map();
2142
- for (const row of history) {
2143
- if ((latest.get(row.blockId)?.version ?? 0) < row.version) latest.set(row.blockId, row);
2144
- }
2145
2199
  const revisions = [];
2146
2200
  for (const block of Object.values(blocks)) {
2147
2201
  const saved = latest.get(block.id);
@@ -2163,6 +2217,8 @@ async function saveSource(db, options, startTime) {
2163
2217
  sourceRevision++;
2164
2218
  for (const block of Object.values(blocks)) block.sourceRevision = sourceRevision;
2165
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;
2166
2222
  const note = {
2167
2223
  ...existing,
2168
2224
  id: noteId,
@@ -2172,7 +2228,8 @@ async function saveSource(db, options, startTime) {
2172
2228
  createdAt: existing?.createdAt ?? now,
2173
2229
  rawContent: options.source,
2174
2230
  sourceRevision,
2175
- blocks
2231
+ blocks,
2232
+ ...catalog ? { catalog } : {}
2176
2233
  };
2177
2234
  const priorRows = await tx.readonly("block_index").getAllFromIndex("by-note", noteId);
2178
2235
  const priorEfforts = await tx.readonly("block_efforts").getAllFromIndex("by-note", noteId);
@@ -2200,6 +2257,7 @@ async function saveSource(db, options, startTime) {
2200
2257
  noteTitle: note.title,
2201
2258
  createdAt: prior?.createdAt ?? now,
2202
2259
  blockContentId: fingerprint,
2260
+ ...catalog ? { catalog } : {},
2203
2261
  ...note.sourceId ? { sourceId: note.sourceId } : {}
2204
2262
  });
2205
2263
  if (block.kind === "wod") {
@@ -2417,6 +2475,136 @@ function defaultSeedDirs(cwd) {
2417
2475
  return ["core", "community"].map((id) => nodePath().resolve(base, id)).filter((dir) => nodeFs().existsSync(nodePath().resolve(dir, "manifest.json")));
2418
2476
  }
2419
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
+
2420
2608
  // src/cli/formatters.ts
2421
2609
  function escapeCsvField(field) {
2422
2610
  if (field === void 0 || field === null) return "";
@@ -2707,7 +2895,7 @@ ${escapeCsvField(data.parsed.metric)},${data.scalar},${escapeCsvField(data.unit
2707
2895
  }
2708
2896
 
2709
2897
  // src/version.ts
2710
- var VERSION = "0.36.2133";
2898
+ var VERSION = "0.36.2135";
2711
2899
 
2712
2900
  // src/cli/runner.ts
2713
2901
  function getFs2() {
@@ -2852,9 +3040,16 @@ function parseCliArgs(args) {
2852
3040
  i++;
2853
3041
  } else if (arg.startsWith("--dir=")) {
2854
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;
2855
3050
  i++;
2856
3051
  } else if (!result.subcommand) {
2857
- 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") {
2858
3053
  result.subcommand = arg;
2859
3054
  } else {
2860
3055
  result.target = arg;
@@ -2880,9 +3075,9 @@ function parseCliArgs(args) {
2880
3075
  function readBunStdinText() {
2881
3076
  const g = globalThis;
2882
3077
  if (g && typeof g === "object" && "Bun" in g) {
2883
- const bun2 = g.Bun;
2884
- if (bun2 && typeof bun2 === "object" && "stdin" in bun2) {
2885
- const stdin = bun2.stdin;
3078
+ const bun = g.Bun;
3079
+ if (bun && typeof bun === "object" && "stdin" in bun) {
3080
+ const stdin = bun.stdin;
2886
3081
  if (stdin && typeof stdin === "object" && "text" in stdin) {
2887
3082
  const text = stdin.text;
2888
3083
  if (typeof text === "function") return text;
@@ -2915,8 +3110,10 @@ USAGE:
2915
3110
  wod parse <file | -> [options]
2916
3111
  wod run <file | -> [options]
2917
3112
  wod query '<wql-string>' [options]
2918
- wod download <note|session|effort|equipment> <id> [options]
2919
- 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]
2920
3117
  wod build [dir] [options]
2921
3118
  wod seed [options]
2922
3119
  wod reset [options]
@@ -2936,23 +3133,23 @@ SUBCOMMANDS:
2936
3133
  execution-log IR
2937
3134
  query '<wql>' Evaluate WQL against the database (or an explicit
2938
3135
  dataset) and emit QueryResult / RowsQueryResult IR
2939
- 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).
2940
3138
  The frontmatter identity stamp allows re-import.
2941
3139
  Frontmatter tags (general + typed equipment/quality/\u2026)
2942
3140
  are materialized into the tag graph on parse/import.
2943
- download session <id>
3141
+ export session <id>
2944
3142
  Write the session's generated metrics (event + summary
2945
3143
  rows, one CSV row per metric) to <id>.csv. Import back
2946
- with 'wod import session'.
2947
- download effort <slug>
3144
+ with 'wod import session'. (alias: download session).
3145
+ export effort <slug>
2948
3146
  Write the effort registry row as corpus-style markdown
2949
- (frontmatter id/slug/label/aliases/baseAttributes + body)
2950
3147
  to <slug>.md. Import back with 'wod import effort'.
2951
- download equipment
2952
- Write every equipment tag as a one-column CSV to
3148
+ export equipment Write every equipment tag as a one-column CSV to
2953
3149
  equipment.csv. Import back with 'wod import equipment'.
2954
- import note <file> Re-import exported note markdown (same as parse with
2955
- 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).
2956
3153
  import session <file>
2957
3154
  Import a session + its events from a downloaded CSV.
2958
3155
  The exact saved block revision must already exist.
@@ -2960,6 +3157,10 @@ SUBCOMMANDS:
2960
3157
  Upsert an effort registry row from corpus-style markdown.
2961
3158
  import equipment <file>
2962
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>).
2963
3164
  build [dir] Compile a seed corpus: validate every .md under
2964
3165
  <dir>/markdown (parity gate + dashboard format gate) and
2965
3166
  write manifest.json + content-hashed chunks/ +
@@ -3008,9 +3209,9 @@ DATABASE (init / parse / run / query / download / import / seed / reset / server
3008
3209
  an error. Without it: ./.env when present, otherwise
3009
3210
  the workspace-root .env. Credentials are never logged.
3010
3211
 
3011
- HOSTING (for 'server' / 'playground'; requires the Bun runtime):
3212
+ HOSTING (for 'server' / 'playground'; supported on Node and Bun):
3012
3213
  server HTTP API over the configured database plus the frontend
3013
- packaged inside the engine module. Requires an
3214
+ packaged inside the tools module. Requires an
3014
3215
  initialized database. ('serve' is an accepted alias.)
3015
3216
  playground Serves the playground app backed by browser IndexedDB;
3016
3217
  the configured database is not touched.
@@ -3270,21 +3471,22 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3270
3471
  writeDebugFile("query", ir);
3271
3472
  return 0;
3272
3473
  }
3273
- if (parsed.subcommand === "download") {
3474
+ if (parsed.subcommand === "download" || parsed.subcommand === "export") {
3475
+ const verb = parsed.subcommand;
3274
3476
  const kind = parsed.target;
3275
3477
  const id = parsed.target2;
3276
- const DOWNLOAD_KINDS = ["note", "session", "effort", "equipment"];
3478
+ const DOWNLOAD_KINDS = ["note", "playground", "session", "effort", "equipment"];
3277
3479
  if (!id && kind !== "equipment" || !DOWNLOAD_KINDS.includes(kind ?? "")) {
3278
- 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>]`);
3279
3481
  return 1;
3280
3482
  }
3281
3483
  const targetId = id;
3282
3484
  const db = await openCliDb(cliDb());
3283
3485
  try {
3284
- if (kind === "note") {
3486
+ if (kind === "note" || kind === "playground") {
3285
3487
  const bundle = await getNoteBundle(db.db, targetId);
3286
3488
  if (!bundle.note) {
3287
- writeErr(`[wod download] Note not found in ${db.path}: ${id}`);
3489
+ writeErr(`[wod ${verb}] Note not found in ${db.path}: ${id}`);
3288
3490
  return 1;
3289
3491
  }
3290
3492
  const markdown = wodWikiEngine.composeNoteMarkdown(bundle.note);
@@ -3294,11 +3496,11 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3294
3496
  } else {
3295
3497
  nodeFs().writeFileSync(target, markdown, "utf-8");
3296
3498
  }
3297
- if (parsed.output !== "-") writeOut(`[wod download] note ${id} \u2192 ${target}`);
3499
+ if (parsed.output !== "-") writeOut(`[wod ${verb}] ${kind} ${id} \u2192 ${target}`);
3298
3500
  } else if (kind === "session") {
3299
3501
  const bundle = await getSessionBundle(db.db, targetId);
3300
3502
  if (!bundle) {
3301
- writeErr(`[wod download] Session not found in ${db.path}: ${id}`);
3503
+ writeErr(`[wod ${verb}] Session not found in ${db.path}: ${id}`);
3302
3504
  return 1;
3303
3505
  }
3304
3506
  const csv = wodWikiEngine.sessionToCsv(bundle.session, bundle.events);
@@ -3308,11 +3510,11 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3308
3510
  } else {
3309
3511
  nodeFs().writeFileSync(target, csv, "utf-8");
3310
3512
  }
3311
- 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)`);
3312
3514
  } else if (kind === "effort") {
3313
3515
  const effort = await getEffortBySlug(db.db, targetId);
3314
3516
  if (!effort) {
3315
- writeErr(`[wod download] Effort not found in ${db.path}: ${id}`);
3517
+ writeErr(`[wod ${verb}] Effort not found in ${db.path}: ${id}`);
3316
3518
  return 1;
3317
3519
  }
3318
3520
  const markdown = wodWikiEngine.effortToMarkdown(effort);
@@ -3322,7 +3524,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3322
3524
  } else {
3323
3525
  nodeFs().writeFileSync(target, markdown, "utf-8");
3324
3526
  }
3325
- writeOut(`[wod download] effort ${id} \u2192 ${target}`);
3527
+ writeOut(`[wod ${verb}] effort ${id} \u2192 ${target}`);
3326
3528
  } else {
3327
3529
  const labels = (await listTagsByType(db.db, "equipment")).map((t) => t.label).sort();
3328
3530
  const csv = wodWikiEngine.equipmentToCsv(labels);
@@ -3332,7 +3534,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3332
3534
  } else {
3333
3535
  nodeFs().writeFileSync(target, csv, "utf-8");
3334
3536
  }
3335
- writeOut(`[wod download] equipment \u2192 ${target} (${labels.length} labels)`);
3537
+ writeOut(`[wod ${verb}] equipment \u2192 ${target} (${labels.length} labels)`);
3336
3538
  }
3337
3539
  } finally {
3338
3540
  await db.close();
@@ -3342,9 +3544,9 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3342
3544
  if (parsed.subcommand === "import") {
3343
3545
  const kind = parsed.target;
3344
3546
  const file = parsed.target2;
3345
- const IMPORT_KINDS = ["note", "session", "effort", "equipment"];
3547
+ const IMPORT_KINDS = ["note", "playground", "session", "effort", "equipment"];
3346
3548
  if (!file || !IMPORT_KINDS.includes(kind ?? "")) {
3347
- writeErr("[wod import] Usage: wod import <note|session|effort|equipment> <file>");
3549
+ writeErr("[wod import] Usage: wod import <note|playground|session|effort|equipment> <file>");
3348
3550
  return 1;
3349
3551
  }
3350
3552
  const fs = nodeFs();
@@ -3352,7 +3554,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3352
3554
  writeErr(`[wod import] File not found: ${file}`);
3353
3555
  return 1;
3354
3556
  }
3355
- if (kind === "note") {
3557
+ if (kind === "note" || kind === "playground") {
3356
3558
  const source = fs.readFileSync(file, "utf-8");
3357
3559
  const identity = wodWikiEngine.extractNoteIdentity(source);
3358
3560
  const tags = wodWikiEngine.extractTypedTags(source);
@@ -3366,7 +3568,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3366
3568
  tags
3367
3569
  });
3368
3570
  writeOut(
3369
- `[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}`
3370
3572
  );
3371
3573
  } finally {
3372
3574
  await db2.close();
@@ -3415,6 +3617,26 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3415
3617
  }
3416
3618
  return 0;
3417
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
+ }
3418
3640
  if (parsed.subcommand === "build") {
3419
3641
  const cwd = process.cwd();
3420
3642
  const root = nodePath().resolve(cwd, parsed.dir ?? parsed.target ?? ".");
@@ -3491,12 +3713,6 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3491
3713
  return 0;
3492
3714
  }
3493
3715
  if (parsed.subcommand === "server" || parsed.subcommand === "serve" || parsed.subcommand === "playground") {
3494
- if (!("Bun" in globalThis)) {
3495
- writeErr(
3496
- `[wod ${parsed.subcommand}] Error: hosting requires the Bun runtime (https://bun.sh) \u2014 run \`bun wod server\` / \`bun wod playground\`.`
3497
- );
3498
- return 1;
3499
- }
3500
3716
  const mode = parsed.subcommand === "playground" ? "playground" : "server";
3501
3717
  const { runCliServer: runCliServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
3502
3718
  const options = { env: cliEnv() };