@bitcobblers/wod-wiki-engine 0.35.2129 → 0.36.2130

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 (74) hide show
  1. package/dist/bin/wod.cjs +758 -194
  2. package/dist/bin/wod.cjs.map +1 -1
  3. package/dist/bin/wod.d.cts +1 -1
  4. package/dist/bin/wod.d.ts +1 -1
  5. package/dist/bin/wod.mjs +5 -3
  6. package/dist/bin/wod.mjs.map +1 -1
  7. package/dist/chunk-6L5ZDZJ7.mjs +15 -0
  8. package/dist/chunk-6L5ZDZJ7.mjs.map +1 -0
  9. package/dist/{chunk-Y6WO6MXS.mjs → chunk-BMHE7DDG.mjs} +3 -9
  10. package/dist/chunk-BMHE7DDG.mjs.map +1 -0
  11. package/dist/chunk-EUQWEX6Z.mjs +253 -0
  12. package/dist/chunk-EUQWEX6Z.mjs.map +1 -0
  13. package/dist/{chunk-N4KFWY6Z.mjs → chunk-UKBQ36RW.mjs} +377 -193
  14. package/dist/chunk-UKBQ36RW.mjs.map +1 -0
  15. package/dist/{compiler-SXUCF7AU.mjs → compiler-NODHKWJJ.mjs} +4 -3
  16. package/dist/compiler-NODHKWJJ.mjs.map +1 -0
  17. package/dist/index.cjs +763 -199
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +27 -8
  20. package/dist/index.d.ts +27 -8
  21. package/dist/index.mjs +10 -8
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/server/app.cjs +299 -0
  24. package/dist/server/app.cjs.map +1 -0
  25. package/dist/server/app.d.cts +24 -0
  26. package/dist/server/app.d.ts +24 -0
  27. package/dist/server/app.mjs +301 -0
  28. package/dist/server/app.mjs.map +1 -0
  29. package/dist/server-4P6AJAGT.mjs +128 -0
  30. package/dist/server-4P6AJAGT.mjs.map +1 -0
  31. package/dist/web/playground/ai-skills/parse.md +656 -0
  32. package/dist/web/playground/assets/main-Bhn9NHgQ.css +2 -0
  33. package/dist/web/playground/assets/main-CGSJY1vM.js +212 -0
  34. package/dist/web/playground/assets/rolldown-runtime-C0FnF6B9.js +1 -0
  35. package/dist/web/playground/assets/statementBuilderFlow-PTgOOzz5.js +2 -0
  36. package/dist/web/playground/assets/vendor-BlJqgFME.js +201 -0
  37. package/dist/web/playground/assets/vendor-codemirror-BtJgoBjf.js +40 -0
  38. package/dist/web/playground/fonts/Inter-Regular.woff2 +0 -0
  39. package/dist/web/playground/fonts/Inter-SemiBold.woff2 +0 -0
  40. package/dist/web/playground/images/hero/editing.png +0 -0
  41. package/dist/web/playground/images/hero/journal.png +0 -0
  42. package/dist/web/playground/images/hero/results.png +0 -0
  43. package/dist/web/playground/images/hero/timer.png +0 -0
  44. package/dist/web/playground/images/wod-wiki-logo-dark.png +0 -0
  45. package/dist/web/playground/images/wod-wiki-logo-light.png +0 -0
  46. package/dist/web/playground/index.html +60 -0
  47. package/dist/web/playground/seed/core/chunks/canvas.e6c77ad0.json +1 -0
  48. package/dist/web/playground/seed/core/chunks/template.e5e0e82d.json +1 -0
  49. package/dist/web/playground/seed/core/manifest.json +24 -0
  50. package/dist/web/playground/seed/core/report.html +43 -0
  51. package/dist/web/server/ai-skills/parse.md +656 -0
  52. package/dist/web/server/assets/main-Bhn9NHgQ.css +2 -0
  53. package/dist/web/server/assets/main-DhN2J5Ax.js +211 -0
  54. package/dist/web/server/assets/rolldown-runtime-C0FnF6B9.js +1 -0
  55. package/dist/web/server/assets/statementBuilderFlow-Dj0o2Wpk.js +2 -0
  56. package/dist/web/server/assets/vendor-BlJqgFME.js +201 -0
  57. package/dist/web/server/assets/vendor-codemirror-BtJgoBjf.js +40 -0
  58. package/dist/web/server/fonts/Inter-Regular.woff2 +0 -0
  59. package/dist/web/server/fonts/Inter-SemiBold.woff2 +0 -0
  60. package/dist/web/server/images/hero/editing.png +0 -0
  61. package/dist/web/server/images/hero/journal.png +0 -0
  62. package/dist/web/server/images/hero/results.png +0 -0
  63. package/dist/web/server/images/hero/timer.png +0 -0
  64. package/dist/web/server/images/wod-wiki-logo-dark.png +0 -0
  65. package/dist/web/server/images/wod-wiki-logo-light.png +0 -0
  66. package/dist/web/server/index.html +60 -0
  67. package/dist/web/server/seed/core/chunks/canvas.e6c77ad0.json +1 -0
  68. package/dist/web/server/seed/core/chunks/template.e5e0e82d.json +1 -0
  69. package/dist/web/server/seed/core/manifest.json +24 -0
  70. package/dist/web/server/seed/core/report.html +43 -0
  71. package/package.json +11 -5
  72. package/dist/chunk-N4KFWY6Z.mjs.map +0 -1
  73. package/dist/chunk-Y6WO6MXS.mjs.map +0 -1
  74. package/dist/compiler-SXUCF7AU.mjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -10,6 +10,7 @@ var crypto$1 = require('crypto');
10
10
  var react = require('@bitcobblers/wod-wiki-lang/react');
11
11
  var wodWikiStorage = require('@bitcobblers/wod-wiki-storage');
12
12
 
13
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
13
14
  var __defProp = Object.defineProperty;
14
15
  var __getOwnPropNames = Object.getOwnPropertyNames;
15
16
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
@@ -25,6 +26,252 @@ var __export = (target, all) => {
25
26
  for (var name in all)
26
27
  __defProp(target, name, { get: all[name], enumerable: true });
27
28
  };
29
+ function nodeRequire() {
30
+ const g = globalThis;
31
+ if (typeof g.process?.versions?.node === "undefined" && typeof g.Bun === "undefined") {
32
+ throw new Error("SQLite operations are only supported in Node / Bun environments");
33
+ }
34
+ const req = g.require ?? (typeof __require === "function" ? __require : void 0);
35
+ if (!req) throw new Error("SQLite operations require a CJS require shim (missing tsup banner?)");
36
+ return req;
37
+ }
38
+ function loadSql() {
39
+ sqlModulePromise ??= import(
40
+ /* @vite-ignore */
41
+ '@bitcobblers/wod-wiki-storage/sql'
42
+ );
43
+ return sqlModulePromise;
44
+ }
45
+ function resolveDbPath(dbPath) {
46
+ const chosen = dbPath ?? process.env.WOD_WIKI_DB ?? "./wodwiki.db";
47
+ return nodePath().resolve(process.cwd(), chosen);
48
+ }
49
+ async function openCliDb(target, opts) {
50
+ if (target === void 0 || typeof target === "string") {
51
+ return openSqliteDb(resolveDbPath(target), opts);
52
+ }
53
+ switch (target.kind) {
54
+ case "sqlite":
55
+ return openSqliteDb(target.path, opts);
56
+ case "turso":
57
+ return openRemoteDb(
58
+ { DB_DRIVER: "turso", DATABASE_URL: target.url, LIBSQL_AUTH_TOKEN: target.authToken },
59
+ target.label,
60
+ opts
61
+ );
62
+ case "postgres":
63
+ return openRemoteDb({ DB_DRIVER: "postgres", DATABASE_URL: target.url }, target.label, opts);
64
+ }
65
+ }
66
+ async function openSqliteDb(resolved, opts) {
67
+ const [{ createClient }, sql] = await Promise.all([
68
+ import(
69
+ /* @vite-ignore */
70
+ '@libsql/client'
71
+ ),
72
+ loadSql()
73
+ ]);
74
+ const initialize = opts?.initialize ?? true;
75
+ if (!initialize && !nodeFs().existsSync(resolved)) {
76
+ throw new Error(`[wod] no database at ${resolved} \u2014 run \`wod init\` to create and initialize it`);
77
+ }
78
+ if (initialize) nodeFs().mkdirSync(nodePath().dirname(resolved), { recursive: true });
79
+ const db = sql.libsqlDatabase(createClient({ url: `file:${resolved}` }));
80
+ try {
81
+ if (initialize) {
82
+ await sql.initializeStore(db);
83
+ await sql.initializeDomain(db);
84
+ } else {
85
+ await sql.verifyReadiness(db).catch((error) => {
86
+ const message = error instanceof Error ? error.message : String(error);
87
+ throw new Error(`${message} \u2014 run \`wod init\` to initialize it`, { cause: error });
88
+ });
89
+ }
90
+ return { db, storage: new sql.SqlStorage(db), path: resolved, close: () => db.close() };
91
+ } catch (error) {
92
+ await db.close();
93
+ throw error;
94
+ }
95
+ }
96
+ async function openRemoteDb(env, label, opts) {
97
+ const [{ openDatabase }, sql] = await Promise.all([
98
+ import(
99
+ /* @vite-ignore */
100
+ '@bitcobblers/wod-wiki-storage/sql/node'
101
+ ),
102
+ loadSql()
103
+ ]);
104
+ let db;
105
+ try {
106
+ db = await openDatabase(env, { create: opts?.create });
107
+ if (opts?.initialize === false) {
108
+ await sql.verifyReadiness(db);
109
+ } else {
110
+ await sql.initializeStore(db);
111
+ await sql.initializeDomain(db);
112
+ }
113
+ } catch (error) {
114
+ await db?.close().catch(() => {
115
+ });
116
+ const message = error instanceof Error ? error.message : String(error);
117
+ throw new Error(
118
+ `[wod] ${label}: ${message}${opts?.initialize === false ? " \u2014 run `wod init` to initialize it" : ""}`,
119
+ { cause: error }
120
+ );
121
+ }
122
+ return { db, storage: new sql.SqlStorage(db), path: label, close: () => db.close() };
123
+ }
124
+ function formatV5Digest(digest) {
125
+ const bytes = digest.slice(0, 16);
126
+ bytes[6] = bytes[6] & 15 | 80;
127
+ bytes[8] = bytes[8] & 63 | 128;
128
+ const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0"));
129
+ return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
130
+ }
131
+ function uuid5(namespace, name) {
132
+ return formatV5Digest(nodeCrypto().createHash("sha1").update(namespace).update(name).digest());
133
+ }
134
+ function stableNoteId(sourceLabel, content) {
135
+ if (sourceLabel && sourceLabel !== "-") {
136
+ return uuid5(CLI_UUID_NAMESPACE, `wod-cli:${nodePath().resolve(process.cwd(), sourceLabel)}`);
137
+ }
138
+ return uuid5(CLI_UUID_NAMESPACE, `wod-cli:stdin:${content.length}:${content.slice(0, 256)}`);
139
+ }
140
+ function randomId() {
141
+ return nodeCrypto().randomUUID();
142
+ }
143
+ async function allRows(sql, db, store) {
144
+ return sql.listRows(db, sql.storeDefinition(store));
145
+ }
146
+ async function rowsByIndex(sql, db, store, index, only) {
147
+ return sql.listRows(db, sql.storeDefinition(store), { only }, void 0, index);
148
+ }
149
+ function sqliteEventStore(db) {
150
+ const all = async () => await allRows(await loadSql(), db, "events");
151
+ return {
152
+ getEventsByTimeRange: async (start, end) => (await all()).filter((r) => r.timestamp >= start && r.timestamp <= end),
153
+ getEventsByResult: async (resultId) => (await all()).filter((r) => r.resultId === resultId),
154
+ getEventsForNote: async (noteId) => (await all()).filter((r) => r.noteId === noteId),
155
+ getEventsByContent: async (blockContentId5) => (await all()).filter((r) => r.blockContentId === blockContentId5),
156
+ scanAll: all,
157
+ appendEvents: async (appended) => {
158
+ const sql = await loadSql();
159
+ await db.transaction(async (tx) => {
160
+ const eventsDef = sql.storeDefinition("events");
161
+ for (const row of appended) await sql.putRow(tx, eventsDef, row);
162
+ });
163
+ },
164
+ finalizeSummaries: async (resultId, finals) => {
165
+ const sql = await loadSql();
166
+ await db.transaction(async (tx) => {
167
+ const eventsDef = sql.storeDefinition("events");
168
+ const existing = (await sql.listRows(tx, eventsDef)).filter(
169
+ (r) => r.resultId === resultId && r.grain === "summary" && r.origin !== "user"
170
+ );
171
+ for (const row of existing) await sql.deleteRows(tx, eventsDef, row.id);
172
+ for (const row of finals) await sql.putRow(tx, eventsDef, row);
173
+ });
174
+ },
175
+ deleteEvents: async (ids) => {
176
+ const sql = await loadSql();
177
+ await db.transaction(async (tx) => {
178
+ const eventsDef = sql.storeDefinition("events");
179
+ for (const id of ids) await sql.deleteRows(tx, eventsDef, id);
180
+ });
181
+ }
182
+ };
183
+ }
184
+ function sqliteNoteStore(db) {
185
+ return {
186
+ getAllNotes: async () => await allRows(await loadSql(), db, "notes"),
187
+ getNotesByIds: async (noteIds) => {
188
+ const sql = await loadSql();
189
+ const rows = await sql.getRows(db, sql.storeDefinition("notes"), [...noteIds]);
190
+ return rows.filter((row) => row !== void 0);
191
+ },
192
+ getNoteIdsForTag: async (label) => {
193
+ const sql = await loadSql();
194
+ const tags = (await allRows(sql, db, "tags")).filter((t) => t.label === label);
195
+ const ids = {};
196
+ for (const tag of tags) {
197
+ for (const row of await rowsByIndex(sql, db, "note_tags", "by-tag", tag.id)) {
198
+ ids[row.noteId] = true;
199
+ }
200
+ }
201
+ return new Set(Object.keys(ids));
202
+ },
203
+ getNoteTagLabels: async (noteId) => {
204
+ const sql = await loadSql();
205
+ const tagRows = await allRows(sql, db, "tags");
206
+ const labelById = {};
207
+ for (const t of tagRows) labelById[t.id] = t.label;
208
+ const links = await rowsByIndex(sql, db, "note_tags", "by-note", noteId);
209
+ return links.map((l) => labelById[l.tagId]).filter((l) => typeof l === "string");
210
+ },
211
+ queryDomain: async (request) => {
212
+ const result = await (await loadSql()).handleDomainQuery(db, request);
213
+ return result;
214
+ }
215
+ };
216
+ }
217
+ function sqliteBlockStore(db) {
218
+ return {
219
+ getAllBlocks: async () => await allRows(await loadSql(), db, "block_index"),
220
+ queryDomain: async (request) => {
221
+ const result = await (await loadSql()).handleDomainQuery(db, { ...request, plan: "blocks" });
222
+ return result;
223
+ }
224
+ };
225
+ }
226
+ function sqliteEffortStore(db) {
227
+ return {
228
+ getAllEfforts: async () => {
229
+ const stored = await allRows(await loadSql(), db, "efforts");
230
+ const bySlug = {};
231
+ for (const effort of stored) bySlug[effort.slug] = effort;
232
+ for (const effort of wodWikiLang.bundledEfforts) {
233
+ bySlug[effort.slug] ??= effort;
234
+ }
235
+ return Object.values(bySlug);
236
+ }
237
+ };
238
+ }
239
+ function sqliteIndexStore(db, store) {
240
+ return {
241
+ getAllFromIndex: async (index, key) => rowsByIndex(await loadSql(), db, store, index, key)
242
+ };
243
+ }
244
+ async function getNoteBundle(db, noteId) {
245
+ const sql = await loadSql();
246
+ const note = await sql.getRow(db, sql.storeDefinition("notes"), noteId);
247
+ return { note };
248
+ }
249
+ async function getSessionBundle(db, resultId) {
250
+ const sql = await loadSql();
251
+ const session = await sql.getRow(db, sql.storeDefinition("sessions"), resultId);
252
+ if (!session) return void 0;
253
+ const events = await sql.listRows(db, sql.storeDefinition("events"), { only: resultId }, void 0, "by-result-grain");
254
+ return { session, events };
255
+ }
256
+ async function getEffortBySlug(db, slug) {
257
+ const sql = await loadSql();
258
+ const efforts = await allRows(sql, db, "efforts");
259
+ return efforts.find((e) => e.slug === slug);
260
+ }
261
+ async function listTagsByType(db, type) {
262
+ const sql = await loadSql();
263
+ const tags = await allRows(sql, db, "tags");
264
+ return tags.filter((t) => t.type === type);
265
+ }
266
+ var nodePath, nodeFs, nodeCrypto, sqlModulePromise, CLI_UUID_NAMESPACE;
267
+ var init_db = __esm({
268
+ "src/cli/db.ts"() {
269
+ nodePath = () => nodeRequire()("node:path");
270
+ nodeFs = () => nodeRequire()("node:fs");
271
+ nodeCrypto = () => nodeRequire()("node:crypto");
272
+ CLI_UUID_NAMESPACE = "c0ffee00-8b1a-4e2c-9d3f-5a6b7c8d9e0f";
273
+ }
274
+ });
28
275
 
29
276
  // src/cli/seedbuild/frontmatter.ts
30
277
  function unquote(value) {
@@ -806,6 +1053,132 @@ var init_compiler = __esm({
806
1053
  };
807
1054
  }
808
1055
  });
1056
+
1057
+ // src/cli/server.ts
1058
+ var server_exports = {};
1059
+ __export(server_exports, {
1060
+ runCliServer: () => runCliServer
1061
+ });
1062
+ function bun() {
1063
+ const b = globalThis.Bun;
1064
+ if (!b) {
1065
+ throw new Error("wod server/playground requires the Bun runtime \u2014 start with: bun wod server");
1066
+ }
1067
+ return b;
1068
+ }
1069
+ function moduleDir() {
1070
+ const p = nodePath();
1071
+ const meta = ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)) });
1072
+ if (typeof meta.dir === "string") return p.resolve(meta.dir);
1073
+ if (typeof meta.url === "string" && meta.url.startsWith("file:")) {
1074
+ return p.resolve(p.dirname(decodeURIComponent(new URL(".", meta.url).pathname)));
1075
+ }
1076
+ throw new Error("wod server/playground: cannot determine the installed engine module directory");
1077
+ }
1078
+ function engineRoot() {
1079
+ const p = nodePath();
1080
+ const fs = nodeFs();
1081
+ for (let dir = moduleDir(); ; dir = p.dirname(dir)) {
1082
+ try {
1083
+ if (JSON.parse(fs.readFileSync(p.resolve(dir, "package.json"), "utf8")).name === "@bitcobblers/wod-wiki-engine") return dir;
1084
+ } catch {
1085
+ }
1086
+ const parent = p.dirname(dir);
1087
+ if (parent === dir) break;
1088
+ }
1089
+ throw new Error("wod server/playground: cannot locate the installed @bitcobblers/wod-wiki-engine package");
1090
+ }
1091
+ function resolveDist(mode, options) {
1092
+ const p = nodePath();
1093
+ const explicit = options.dist?.trim() || options.env.PLAYGROUND_DIST?.trim() || void 0;
1094
+ if (explicit) {
1095
+ const dist2 = p.resolve(explicit);
1096
+ if (!nodeFs().existsSync(p.resolve(dist2, "index.html"))) {
1097
+ throw new Error(
1098
+ `wod ${mode}: no built frontend (index.html) at ${dist2} \u2014 --dist/PLAYGROUND_DIST must point at an already-built directory (the CLI never builds frontends at runtime)`
1099
+ );
1100
+ }
1101
+ return dist2;
1102
+ }
1103
+ const dist = p.resolve(engineRoot(), "dist", "web", mode === "playground" ? "playground" : "server");
1104
+ if (!nodeFs().existsSync(p.resolve(dist, "index.html"))) {
1105
+ throw new Error(
1106
+ `wod ${mode}: packaged frontend missing \u2014 expected ${dist}/index.html. Reinstall @bitcobblers/wod-wiki-engine (bun add @bitcobblers/wod-wiki-engine), or rebuild the release assets from a checkout (bun run build:cli) and reinstall the package.`
1107
+ );
1108
+ }
1109
+ return dist;
1110
+ }
1111
+ function resolveBind(mode, options) {
1112
+ const host = options.host?.trim() || options.env.HOST?.trim() || "127.0.0.1";
1113
+ const rawPort = options.port?.trim() || options.env.PORT?.trim() || "3000";
1114
+ const port = Number(rawPort);
1115
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
1116
+ throw new Error(`wod ${mode}: invalid port ${JSON.stringify(rawPort)} (need integer 1..65535)`);
1117
+ }
1118
+ return { host, port };
1119
+ }
1120
+ async function runCliServer(mode, options, writeOut) {
1121
+ const runtime = bun();
1122
+ const { host, port } = resolveBind(mode, options);
1123
+ let db;
1124
+ let dbLabel = "no database";
1125
+ if (mode === "server") {
1126
+ const database = options.database;
1127
+ if (!database) {
1128
+ throw new Error("wod server: no database configuration resolved \u2014 this is a runner bug (expected resolveDatabaseConfig output)");
1129
+ }
1130
+ try {
1131
+ db = await openCliDb(database, { initialize: false });
1132
+ } catch (e) {
1133
+ const message = e instanceof Error ? e.message : String(e);
1134
+ throw new Error(`wod server: database not ready \u2014 run \`wod init\` first. ${message}`, { cause: e });
1135
+ }
1136
+ dbLabel = db.path;
1137
+ }
1138
+ let server;
1139
+ try {
1140
+ const dist = resolveDist(mode, options);
1141
+ const handlerModule = "@bitcobblers/wod-wiki-engine/server";
1142
+ const { createHandler } = await import(
1143
+ /* @vite-ignore */
1144
+ handlerModule
1145
+ );
1146
+ const handler = createHandler(db?.db ?? null, { playgroundDist: dist });
1147
+ server = runtime.serve({ port, hostname: host, fetch: handler });
1148
+ if (mode === "server") {
1149
+ writeOut(`wod server: http://${host}:${port} \u2014 ${dbLabel} \xB7 dist ${dist}`);
1150
+ } else {
1151
+ writeOut(`wod playground: http://${host}:${port} \u2014 in-browser storage (no database) \xB7 dist ${dist}`);
1152
+ }
1153
+ } catch (e) {
1154
+ if (db) await db.close();
1155
+ throw e;
1156
+ }
1157
+ let closing = false;
1158
+ const shutdown = async () => {
1159
+ if (closing) process.exit(0);
1160
+ closing = true;
1161
+ try {
1162
+ await server.stop();
1163
+ } finally {
1164
+ if (db) await db.close();
1165
+ process.exit(0);
1166
+ }
1167
+ };
1168
+ process.on("SIGINT", () => {
1169
+ void shutdown();
1170
+ });
1171
+ process.on("SIGTERM", () => {
1172
+ void shutdown();
1173
+ });
1174
+ await new Promise(() => {
1175
+ });
1176
+ }
1177
+ var init_server = __esm({
1178
+ "src/cli/server.ts"() {
1179
+ init_db();
1180
+ }
1181
+ });
809
1182
  var activePacks = /* @__PURE__ */ new Map();
810
1183
  function defineLanguagePack(pack) {
811
1184
  return pack;
@@ -1158,7 +1531,7 @@ function loadQueryData(options) {
1158
1531
  } else if (obj.facts || obj.results || obj.notes || obj.blocks || obj.efforts || obj.tags || obj.logs) {
1159
1532
  const corpus = payload;
1160
1533
  if (corpus.facts) data.facts = corpus.facts;
1161
- if (corpus.results) data.results = corpus.results;
1534
+ if (Array.isArray(corpus.results)) data.results = corpus.results;
1162
1535
  if (corpus.notes) data.notes = corpus.notes;
1163
1536
  if (corpus.blocks) data.blocks = corpus.blocks;
1164
1537
  if (corpus.efforts) data.efforts = corpus.efforts;
@@ -1333,193 +1706,256 @@ async function loadLanguagePack(moduleSpec) {
1333
1706
  throw new PackLoadError(moduleSpec, error);
1334
1707
  }
1335
1708
  }
1336
- var nodePath = () => nodeRequire()("node:path");
1337
- var nodeFs = () => nodeRequire()("node:fs");
1338
- var nodeCrypto = () => nodeRequire()("node:crypto");
1339
- function nodeRequire() {
1709
+
1710
+ // src/cli/runner.ts
1711
+ init_db();
1712
+
1713
+ // src/cli/config.ts
1714
+ function nodeRequire2() {
1340
1715
  const g = globalThis;
1341
1716
  if (typeof g.process?.versions?.node === "undefined" && typeof g.Bun === "undefined") {
1342
- throw new Error("SQLite operations are only supported in Node / Bun environments");
1717
+ throw new Error("database configuration is only supported in Node / Bun environments");
1343
1718
  }
1344
1719
  const req = g.require ?? (typeof __require === "function" ? __require : void 0);
1345
- if (!req) throw new Error("SQLite operations require a CJS require shim (missing tsup banner?)");
1720
+ if (!req) throw new Error("database configuration requires a CJS require shim (missing tsup banner?)");
1346
1721
  return req;
1347
1722
  }
1348
- var sqlModulePromise;
1349
- function loadSql() {
1350
- sqlModulePromise ??= import(
1351
- /* @vite-ignore */
1352
- '@bitcobblers/wod-wiki-storage/sql'
1353
- );
1354
- return sqlModulePromise;
1355
- }
1356
- function resolveDbPath(dbPath) {
1357
- const chosen = dbPath ?? process.env.WOD_WIKI_DB ?? "./wodwiki.db";
1358
- return nodePath().resolve(process.cwd(), chosen);
1359
- }
1360
- async function openCliDb(dbPath) {
1361
- const [{ createClient }, sql] = await Promise.all([
1362
- import(
1363
- /* @vite-ignore */
1364
- '@libsql/client'
1365
- ),
1366
- loadSql()
1367
- ]);
1368
- const resolved = resolveDbPath(dbPath);
1369
- nodeFs().mkdirSync(nodePath().dirname(resolved), { recursive: true });
1370
- const db = sql.libsqlDatabase(createClient({ url: `file:${resolved}` }));
1723
+ var nodePath2 = () => nodeRequire2()("node:path");
1724
+ var nodeFs2 = () => nodeRequire2()("node:fs");
1725
+ function parseEnvFile(content) {
1726
+ return nodeRequire2()("node:util").parseEnv(content);
1727
+ }
1728
+ function loadCliEnvironment(envFile) {
1729
+ const fs = nodeFs2();
1730
+ const path = nodePath2();
1731
+ let file;
1732
+ if (envFile !== void 0) {
1733
+ file = path.resolve(process.cwd(), envFile);
1734
+ if (!fs.existsSync(file)) throw new Error(`[wod] --env-file: file not found: ${envFile}`);
1735
+ } else {
1736
+ let dir = process.cwd();
1737
+ for (; ; ) {
1738
+ const candidate = path.join(dir, ".env");
1739
+ if (fs.existsSync(candidate)) {
1740
+ file = candidate;
1741
+ break;
1742
+ }
1743
+ const parent = path.dirname(dir);
1744
+ if (parent === dir) break;
1745
+ dir = parent;
1746
+ }
1747
+ }
1748
+ return file !== void 0 ? parseEnvFile(fs.readFileSync(file, "utf-8")) : {};
1749
+ }
1750
+ var SQLITE_URL_SCHEMES = /^(libsql|https?|wss?|file):\/\//;
1751
+ var PG_URL_SCHEMES = /^postgres(ql)?:\/\//;
1752
+ var TURSO_URL_SCHEMES = /^(libsql|https?|wss?):\/\//;
1753
+ var PG_FLAG_KEYS = [
1754
+ "postgresHost",
1755
+ "postgresPort",
1756
+ "postgresUser",
1757
+ "postgresPassword",
1758
+ "postgresDatabase",
1759
+ "postgresSsl"
1760
+ ];
1761
+ var PG_ENV_KEYS = [
1762
+ "POSTGRES_HOST",
1763
+ "POSTGRES_PORT",
1764
+ "POSTGRES_USER",
1765
+ "POSTGRES_PASSWORD",
1766
+ "POSTGRES_DATABASE",
1767
+ "POSTGRES_SSL"
1768
+ ];
1769
+ var isBackend = (v) => v === "sqlite" || v === "postgres" || v === "turso";
1770
+ function setEnv(v) {
1771
+ return v !== void 0 && v.trim() !== "" ? v : void 0;
1772
+ }
1773
+ function fail(message) {
1774
+ throw new Error(`[wod] ${message}`);
1775
+ }
1776
+ function parsePort(v, source) {
1777
+ if (!/^\d+$/.test(v)) fail(`${source} must be an integer 1..65535, got ${JSON.stringify(v)}`);
1778
+ const port = Number(v);
1779
+ if (port < 1 || port > 65535) fail(`${source} must be an integer 1..65535, got ${v}`);
1780
+ return port;
1781
+ }
1782
+ function sslMode(v, source) {
1783
+ if (v === void 0 || v === "") return "prefer";
1784
+ if (typeof v === "boolean") return v ? "require" : "disable";
1785
+ const s = v.trim().toLowerCase();
1786
+ if (["true", "1", "yes", "require"].includes(s)) return "require";
1787
+ if (["false", "0", "no", "disable", "disabled"].includes(s)) return "disable";
1788
+ if (s === "prefer" || s === "verify-ca" || s === "verify-full") return s;
1789
+ fail(`${source} must be disable|prefer|require|verify-ca|verify-full, got ${JSON.stringify(v)}`);
1790
+ }
1791
+ function safeUrlLabel(raw) {
1371
1792
  try {
1372
- await sql.initializeStore(db);
1373
- await sql.initializeDomain(db);
1374
- return { db, storage: new sql.SqlStorage(db), path: resolved, close: () => db.close() };
1375
- } catch (error) {
1376
- await db.close();
1377
- throw error;
1793
+ const u = new URL(raw);
1794
+ return `${u.protocol}//${u.host}${u.pathname}`;
1795
+ } catch {
1796
+ return "<unparsable-url>";
1378
1797
  }
1379
1798
  }
1380
- function formatV5Digest(digest) {
1381
- const bytes = digest.slice(0, 16);
1382
- bytes[6] = bytes[6] & 15 | 80;
1383
- bytes[8] = bytes[8] & 63 | 128;
1384
- const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0"));
1385
- return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
1386
- }
1387
- function uuid5(namespace, name) {
1388
- return formatV5Digest(nodeCrypto().createHash("sha1").update(namespace).update(name).digest());
1389
- }
1390
- var CLI_UUID_NAMESPACE = "c0ffee00-8b1a-4e2c-9d3f-5a6b7c8d9e0f";
1391
- function stableNoteId(sourceLabel, content) {
1392
- if (sourceLabel && sourceLabel !== "-") {
1393
- return uuid5(CLI_UUID_NAMESPACE, `wod-cli:${nodePath().resolve(process.cwd(), sourceLabel)}`);
1799
+ function postgresFromUrl(url, source) {
1800
+ if (!PG_URL_SCHEMES.test(url)) {
1801
+ fail(`${source} must start with postgres:// or postgresql://`);
1394
1802
  }
1395
- return uuid5(CLI_UUID_NAMESPACE, `wod-cli:stdin:${content.length}:${content.slice(0, 256)}`);
1396
- }
1397
- function randomId() {
1398
- return nodeCrypto().randomUUID();
1399
- }
1400
- async function allRows(sql, db, store) {
1401
- return sql.listRows(db, sql.storeDefinition(store));
1402
- }
1403
- async function rowsByIndex(sql, db, store, index, only) {
1404
- return sql.listRows(db, sql.storeDefinition(store), { only }, void 0, index);
1405
- }
1406
- function sqliteEventStore(db) {
1407
- const all = async () => await allRows(await loadSql(), db, "events");
1408
- return {
1409
- getEventsByTimeRange: async (start, end) => (await all()).filter((r) => r.timestamp >= start && r.timestamp <= end),
1410
- getEventsByResult: async (resultId) => (await all()).filter((r) => r.resultId === resultId),
1411
- getEventsForNote: async (noteId) => (await all()).filter((r) => r.noteId === noteId),
1412
- getEventsByContent: async (blockContentId5) => (await all()).filter((r) => r.blockContentId === blockContentId5),
1413
- scanAll: all,
1414
- appendEvents: async (appended) => {
1415
- const sql = await loadSql();
1416
- await db.transaction(async (tx) => {
1417
- const eventsDef = sql.storeDefinition("events");
1418
- for (const row of appended) await sql.putRow(tx, eventsDef, row);
1419
- });
1420
- },
1421
- finalizeSummaries: async (resultId, finals) => {
1422
- const sql = await loadSql();
1423
- await db.transaction(async (tx) => {
1424
- const eventsDef = sql.storeDefinition("events");
1425
- const existing = (await sql.listRows(tx, eventsDef)).filter(
1426
- (r) => r.resultId === resultId && r.grain === "summary" && r.origin !== "user"
1427
- );
1428
- for (const row of existing) await sql.deleteRows(tx, eventsDef, row.id);
1429
- for (const row of finals) await sql.putRow(tx, eventsDef, row);
1430
- });
1431
- },
1432
- deleteEvents: async (ids) => {
1433
- const sql = await loadSql();
1434
- await db.transaction(async (tx) => {
1435
- const eventsDef = sql.storeDefinition("events");
1436
- for (const id of ids) await sql.deleteRows(tx, eventsDef, id);
1437
- });
1438
- }
1439
- };
1440
- }
1441
- function sqliteNoteStore(db) {
1803
+ let u;
1804
+ try {
1805
+ u = new URL(url);
1806
+ } catch {
1807
+ fail(`${source} is not a parseable URL`);
1808
+ }
1809
+ const database = decodeURIComponent(u.pathname.replace(/^\/+|\/+$/g, ""));
1810
+ if (database === "") fail(`${source} has no database name`);
1442
1811
  return {
1443
- getAllNotes: async () => await allRows(await loadSql(), db, "notes"),
1444
- getNotesByIds: async (noteIds) => {
1445
- const sql = await loadSql();
1446
- const rows = await sql.getRows(db, sql.storeDefinition("notes"), [...noteIds]);
1447
- return rows.filter((row) => row !== void 0);
1448
- },
1449
- getNoteIdsForTag: async (label) => {
1450
- const sql = await loadSql();
1451
- const tags = (await allRows(sql, db, "tags")).filter((t) => t.label === label);
1452
- const ids = {};
1453
- for (const tag of tags) {
1454
- for (const row of await rowsByIndex(sql, db, "note_tags", "by-tag", tag.id)) {
1455
- ids[row.noteId] = true;
1456
- }
1457
- }
1458
- return new Set(Object.keys(ids));
1459
- },
1460
- getNoteTagLabels: async (noteId) => {
1461
- const sql = await loadSql();
1462
- const tagRows = await allRows(sql, db, "tags");
1463
- const labelById = {};
1464
- for (const t of tagRows) labelById[t.id] = t.label;
1465
- const links = await rowsByIndex(sql, db, "note_tags", "by-note", noteId);
1466
- return links.map((l) => labelById[l.tagId]).filter((l) => typeof l === "string");
1467
- },
1468
- queryDomain: async (request) => {
1469
- const result = await (await loadSql()).handleDomainQuery(db, request);
1470
- return result;
1471
- }
1812
+ kind: "postgres",
1813
+ label: safeUrlLabel(url),
1814
+ url,
1815
+ host: u.hostname,
1816
+ port: u.port !== "" ? parsePort(u.port, `${source} port`) : 5432,
1817
+ database,
1818
+ user: u.username !== "" ? decodeURIComponent(u.username) : void 0,
1819
+ password: u.password !== "" ? decodeURIComponent(u.password) : void 0,
1820
+ ssl: sslMode(u.searchParams.get("sslmode") ?? u.searchParams.get("ssl") ?? void 0, `${source} ssl parameter`)
1472
1821
  };
1473
1822
  }
1474
- function sqliteBlockStore(db) {
1475
- return {
1476
- getAllBlocks: async () => await allRows(await loadSql(), db, "block_index"),
1477
- queryDomain: async (request) => {
1478
- const result = await (await loadSql()).handleDomainQuery(db, { ...request, plan: "blocks" });
1479
- return result;
1823
+ function resolvePostgres(options, e, databaseUrl) {
1824
+ const flagProps = PG_FLAG_KEYS.some((k) => options[k] !== void 0);
1825
+ const envProps = PG_ENV_KEYS.some((k) => setEnv(e[k]) !== void 0);
1826
+ if (flagProps || envProps && databaseUrl === void 0) {
1827
+ const host = options.postgresHost ?? setEnv(e.POSTGRES_HOST) ?? "127.0.0.1";
1828
+ const port = parsePort(options.postgresPort ?? setEnv(e.POSTGRES_PORT) ?? "5432", "--postgres-port/POSTGRES_PORT");
1829
+ const database = options.postgresDatabase ?? setEnv(e.POSTGRES_DATABASE);
1830
+ if (database === void 0) {
1831
+ fail("--postgres-database (or POSTGRES_DATABASE) is required for per-property PostgreSQL configuration");
1480
1832
  }
1481
- };
1482
- }
1483
- function sqliteEffortStore(db) {
1484
- return {
1485
- getAllEfforts: async () => {
1486
- const stored = await allRows(await loadSql(), db, "efforts");
1487
- const bySlug = {};
1488
- for (const effort of stored) bySlug[effort.slug] = effort;
1489
- for (const effort of wodWikiLang.bundledEfforts) {
1490
- bySlug[effort.slug] ??= effort;
1491
- }
1492
- return Object.values(bySlug);
1833
+ const user = options.postgresUser ?? setEnv(e.POSTGRES_USER);
1834
+ const password = options.postgresPassword ?? setEnv(e.POSTGRES_PASSWORD);
1835
+ const ssl = sslMode(options.postgresSsl ?? setEnv(e.POSTGRES_SSL), "--postgres-ssl/POSTGRES_SSL");
1836
+ try {
1837
+ const u = new URL(`postgresql://${host}`);
1838
+ u.port = String(port);
1839
+ u.pathname = `/${database}`;
1840
+ if (user !== void 0) u.username = user;
1841
+ if (password !== void 0) u.password = password;
1842
+ u.searchParams.set("sslmode", ssl);
1843
+ return {
1844
+ kind: "postgres",
1845
+ label: `postgresql://${u.host}${u.pathname}`,
1846
+ url: u.toString(),
1847
+ host,
1848
+ port,
1849
+ database,
1850
+ user,
1851
+ password,
1852
+ ssl
1853
+ };
1854
+ } catch {
1855
+ fail("invalid PostgreSQL host/database in per-property configuration");
1493
1856
  }
1494
- };
1857
+ }
1858
+ if (databaseUrl !== void 0) return postgresFromUrl(databaseUrl, "DB_DRIVER=postgres DATABASE_URL");
1859
+ fail("DB_DRIVER=postgres requires DATABASE_URL starting with postgres:// or postgresql:// \u2014 or per-property --postgres-host/--postgres-database flags");
1495
1860
  }
1496
- function sqliteIndexStore(db, store) {
1861
+ function resolveTurso(options, e, databaseUrl) {
1862
+ const url = options.tursoUrl ?? setEnv(e.TURSO_DATABASE_URL) ?? databaseUrl;
1863
+ if (url === void 0) {
1864
+ fail("DB_DRIVER=turso requires --turso-url / TURSO_DATABASE_URL or DATABASE_URL (libsql:// or https://)");
1865
+ }
1866
+ if (PG_URL_SCHEMES.test(url)) fail("DB_DRIVER=turso cannot use a postgres:// URL");
1867
+ if (!TURSO_URL_SCHEMES.test(url)) fail("DB_DRIVER=turso requires a libsql://, https://, http:// or wss:// URL");
1497
1868
  return {
1498
- getAllFromIndex: async (index, key) => rowsByIndex(await loadSql(), db, store, index, key)
1869
+ kind: "turso",
1870
+ label: safeUrlLabel(url),
1871
+ url,
1872
+ authToken: options.tursoAuthToken ?? setEnv(e.TURSO_AUTH_TOKEN) ?? setEnv(e.LIBSQL_AUTH_TOKEN)
1499
1873
  };
1500
1874
  }
1501
- async function getNoteBundle(db, noteId) {
1502
- const sql = await loadSql();
1503
- const note = await sql.getRow(db, sql.storeDefinition("notes"), noteId);
1504
- return { note };
1505
- }
1506
- async function getSessionBundle(db, resultId) {
1507
- const sql = await loadSql();
1508
- const session = await sql.getRow(db, sql.storeDefinition("sessions"), resultId);
1509
- if (!session) return void 0;
1510
- const events = await sql.listRows(db, sql.storeDefinition("events"), { only: resultId }, void 0, "by-result-grain");
1511
- return { session, events };
1512
- }
1513
- async function getEffortBySlug(db, slug) {
1514
- const sql = await loadSql();
1515
- const efforts = await allRows(sql, db, "efforts");
1516
- return efforts.find((e) => e.slug === slug);
1875
+ function resolveSqlite(options, e, databaseUrl) {
1876
+ if (options.db !== void 0) {
1877
+ const path2 = nodePath2().resolve(process.cwd(), options.db);
1878
+ return { kind: "sqlite", label: path2, path: path2 };
1879
+ }
1880
+ if (databaseUrl !== void 0) {
1881
+ if (PG_URL_SCHEMES.test(databaseUrl)) fail("DB_DRIVER=sqlite cannot use a postgres:// DATABASE_URL");
1882
+ if (databaseUrl.startsWith("file:")) {
1883
+ const path2 = nodePath2().resolve(process.cwd(), decodeURIComponent(databaseUrl.slice("file:".length).split("?")[0]));
1884
+ return { kind: "sqlite", label: path2, path: path2 };
1885
+ }
1886
+ if (!SQLITE_URL_SCHEMES.test(databaseUrl)) {
1887
+ fail("DB_DRIVER=sqlite requires a libsql://, https://, http://, wss:// or file:// DATABASE_URL");
1888
+ }
1889
+ return { kind: "turso", label: safeUrlLabel(databaseUrl), url: databaseUrl, authToken: setEnv(e.LIBSQL_AUTH_TOKEN) };
1890
+ }
1891
+ const path = nodePath2().resolve(process.cwd(), setEnv(e.WOD_WIKI_DB) ?? setEnv(e.SQLITE_PATH) ?? "./wodwiki.db");
1892
+ return { kind: "sqlite", label: path, path };
1893
+ }
1894
+ function envBackend(e) {
1895
+ const kinds = /* @__PURE__ */ new Set();
1896
+ if (setEnv(e.WOD_WIKI_DB) !== void 0 || setEnv(e.SQLITE_PATH) !== void 0) kinds.add("sqlite");
1897
+ const dbUrl = setEnv(e.DATABASE_URL);
1898
+ if (PG_ENV_KEYS.some((k) => setEnv(e[k]) !== void 0) || dbUrl !== void 0 && PG_URL_SCHEMES.test(dbUrl)) {
1899
+ kinds.add("postgres");
1900
+ } else if (dbUrl !== void 0 && SQLITE_URL_SCHEMES.test(dbUrl)) {
1901
+ kinds.add("sqlite");
1902
+ }
1903
+ if (setEnv(e.TURSO_DATABASE_URL) !== void 0) kinds.add("turso");
1904
+ if (kinds.size > 1) {
1905
+ fail(`ambiguous database configuration: ${[...kinds].join(" + ")} env vars are both set \u2014 set DB_DRIVER (or pass explicit flags) to choose`);
1906
+ }
1907
+ return kinds.values().next().value ?? "sqlite";
1517
1908
  }
1518
- async function listTagsByType(db, type) {
1519
- const sql = await loadSql();
1520
- const tags = await allRows(sql, db, "tags");
1521
- return tags.filter((t) => t.type === type);
1909
+ function resolveDatabaseConfig(options = {}, env) {
1910
+ const e = env ?? { ...loadCliEnvironment(options.envFile), ...process.env };
1911
+ const driverFlag = options.dbDriver;
1912
+ if (driverFlag !== void 0 && !isBackend(driverFlag)) {
1913
+ fail(`--db-driver must be sqlite|postgres|turso, got ${JSON.stringify(driverFlag)}`);
1914
+ }
1915
+ const pinned = [];
1916
+ if (options.db !== void 0) pinned.push(["--db", "sqlite"]);
1917
+ if (driverFlag !== void 0) pinned.push([`--db-driver ${driverFlag}`, driverFlag]);
1918
+ if (options.tursoUrl !== void 0) pinned.push(["--turso-url", "turso"]);
1919
+ if (options.tursoAuthToken !== void 0) pinned.push(["--turso-auth-token", "turso"]);
1920
+ if (PG_FLAG_KEYS.some((k) => options[k] !== void 0)) pinned.push(["--postgres-*", "postgres"]);
1921
+ const kinds = new Set(pinned.map(([, kind]) => kind));
1922
+ if (kinds.size > 1) {
1923
+ fail(`conflicting database selectors: ${pinned.map(([flag, kind]) => `${flag} (${kind})`).join(", ")}`);
1924
+ }
1925
+ if (options.databaseUrl !== void 0) {
1926
+ if (options.db !== void 0) fail("conflicting database selectors: --db and --database-url are both set");
1927
+ if (options.tursoUrl !== void 0) fail("conflicting database selectors: --turso-url and --database-url are both set");
1928
+ if (PG_FLAG_KEYS.some((k) => options[k] !== void 0)) {
1929
+ fail("conflicting database selectors: --database-url and --postgres-* flags are both set");
1930
+ }
1931
+ }
1932
+ let backend;
1933
+ if (pinned.length > 0) {
1934
+ backend = pinned[0][1];
1935
+ } else {
1936
+ const envDriver = setEnv(e.DB_DRIVER);
1937
+ if (envDriver !== void 0) {
1938
+ if (!isBackend(envDriver)) {
1939
+ fail(`Unsupported DB_DRIVER: ${envDriver} (expected sqlite|turso|postgres)`);
1940
+ }
1941
+ backend = envDriver;
1942
+ } else {
1943
+ backend = envBackend(e);
1944
+ }
1945
+ }
1946
+ const databaseUrl = options.databaseUrl ?? setEnv(e.DATABASE_URL);
1947
+ switch (backend) {
1948
+ case "postgres":
1949
+ return resolvePostgres(options, e, databaseUrl);
1950
+ case "turso":
1951
+ return resolveTurso(options, e, databaseUrl);
1952
+ case "sqlite":
1953
+ return resolveSqlite(options, e, databaseUrl);
1954
+ }
1522
1955
  }
1956
+
1957
+ // src/cli/persist.ts
1958
+ init_db();
1523
1959
  var registeredWidgets = wodWikiCore.KNOWN_SECTION_WIDGETS;
1524
1960
  var saveStores = ["notes", "block_revisions", "block_index", "block_efforts", "tags", "note_tags", "sessions"];
1525
1961
  function extractEffortSlugs(statements) {
@@ -1789,6 +2225,9 @@ async function persistTypedTags(db, labels, type) {
1789
2225
  return added;
1790
2226
  });
1791
2227
  }
2228
+
2229
+ // src/cli/seed.ts
2230
+ init_db();
1792
2231
  var NOTE_ID_FRONTMATTER_KEY = "wod-note-id";
1793
2232
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
1794
2233
  function extractNoteIdentity(markdown) {
@@ -2911,16 +3350,53 @@ ${source}
2911
3350
  ${fence}
2912
3351
  `;
2913
3352
  }
3353
+ var VALUE_FLAGS = [
3354
+ ["--db", "db"],
3355
+ ["--db-driver", "dbDriver"],
3356
+ ["--database-url", "databaseUrl"],
3357
+ ["--postgres-host", "postgresHost"],
3358
+ ["--postgres-port", "postgresPort"],
3359
+ ["--postgres-user", "postgresUser"],
3360
+ ["--postgres-password", "postgresPassword"],
3361
+ ["--postgres-database", "postgresDatabase"],
3362
+ ["--postgres-ssl", "postgresSsl"],
3363
+ ["--turso-url", "tursoUrl"],
3364
+ ["--turso-auth-token", "tursoAuthToken"],
3365
+ ["--env-file", "envFile"],
3366
+ ["--host", "host"],
3367
+ ["--port", "port"],
3368
+ ["--dist", "dist"]
3369
+ ];
3370
+ function readValueFlag(args, i, flag) {
3371
+ const inline = args[i].startsWith(`${flag}=`);
3372
+ if (inline) {
3373
+ const value2 = args[i].slice(flag.length + 1);
3374
+ if (!value2) throw new Error(`Missing value for ${flag}`);
3375
+ return { value: value2, next: i + 1 };
3376
+ }
3377
+ const value = args[i + 1];
3378
+ if (value === void 0 || value === "" || value.startsWith("--")) throw new Error(`Missing value for ${flag}`);
3379
+ return { value, next: i + 2 };
3380
+ }
2914
3381
  function parseCliArgs(args) {
2915
3382
  const result = {
2916
3383
  format: "json",
2917
3384
  packs: [],
2918
3385
  sources: []
2919
3386
  };
3387
+ const valueArgs = result;
2920
3388
  let i = 0;
2921
3389
  while (i < args.length) {
2922
3390
  const arg = args[i];
2923
3391
  if (arg === "--at" || arg.startsWith("--at=")) throw new Error("Unknown option --at; whole Note exports have no as-of version");
3392
+ const valueFlag = VALUE_FLAGS.find(([flag]) => arg === flag || arg.startsWith(`${flag}=`));
3393
+ if (valueFlag) {
3394
+ const [flag, key] = valueFlag;
3395
+ const { value, next } = readValueFlag(args, i, flag);
3396
+ valueArgs[key] = value;
3397
+ i = next;
3398
+ continue;
3399
+ }
2924
3400
  if (arg === "--help" || arg === "-h") {
2925
3401
  result.help = true;
2926
3402
  i++;
@@ -2967,12 +3443,6 @@ function parseCliArgs(args) {
2967
3443
  } else if (arg === "--unit") {
2968
3444
  result.unit = args[++i];
2969
3445
  i++;
2970
- } else if (arg === "--db") {
2971
- result.db = args[++i];
2972
- i++;
2973
- } else if (arg.startsWith("--db=")) {
2974
- result.db = arg.slice("--db=".length);
2975
- i++;
2976
3446
  } else if (arg === "--debug") {
2977
3447
  result.debug = "";
2978
3448
  i++;
@@ -3008,7 +3478,7 @@ function parseCliArgs(args) {
3008
3478
  result.dir = arg.slice("--dir=".length);
3009
3479
  i++;
3010
3480
  } else if (!result.subcommand) {
3011
- if (arg === "parse" || arg === "run" || arg === "query" || arg === "download" || arg === "import" || arg === "build" || arg === "seed" || arg === "reset" || arg === "help" || arg === "version") {
3481
+ 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") {
3012
3482
  result.subcommand = arg;
3013
3483
  } else {
3014
3484
  result.target = arg;
@@ -3024,14 +3494,19 @@ function parseCliArgs(args) {
3024
3494
  i++;
3025
3495
  }
3026
3496
  }
3497
+ for (const [flag, value] of [["--port", result.port], ["--postgres-port", result.postgresPort]]) {
3498
+ if (value !== void 0 && (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 65535)) {
3499
+ throw new Error(`Invalid value for ${flag}: "${value}" is not a TCP port (1-65535)`);
3500
+ }
3501
+ }
3027
3502
  return result;
3028
3503
  }
3029
3504
  function readBunStdinText() {
3030
3505
  const g = globalThis;
3031
3506
  if (g && typeof g === "object" && "Bun" in g) {
3032
- const bun = g.Bun;
3033
- if (bun && typeof bun === "object" && "stdin" in bun) {
3034
- const stdin = bun.stdin;
3507
+ const bun2 = g.Bun;
3508
+ if (bun2 && typeof bun2 === "object" && "stdin" in bun2) {
3509
+ const stdin = bun2.stdin;
3035
3510
  if (stdin && typeof stdin === "object" && "text" in stdin) {
3036
3511
  const text = stdin.text;
3037
3512
  if (typeof text === "function") return text;
@@ -3069,6 +3544,11 @@ USAGE:
3069
3544
  wod build [dir] [options]
3070
3545
  wod seed [options]
3071
3546
  wod reset [options]
3547
+ wod init Initialize the configured database (idempotent)
3548
+ wod server [options]
3549
+ Serve the app (HTTP API + packaged frontend; alias: serve)
3550
+ wod playground [options]
3551
+ Serve the local playground (browser IndexedDB build)
3072
3552
 
3073
3553
  SUBCOMMANDS:
3074
3554
  parse <file | -> Parse Whiteboard Script, create a note in the database
@@ -3122,9 +3602,47 @@ INPUT SOURCING (for 'query'; without these, the database is queried):
3122
3602
  --stdin-log Read kind: 'execution-log' JSON payload from stdin
3123
3603
  --stdin-facts Read raw kind: 'fact-set' JSON payload from stdin
3124
3604
 
3125
- DATABASE (parse / run / query / download / import / seed / reset):
3126
- --db <path> SQLite database file (default: $WOD_WIKI_DB or
3127
- ./wodwiki.db). Created on demand by parse/run/seed.
3605
+ DATABASE (init / parse / run / query / download / import / seed / reset / server):
3606
+ Pick one backend; explicit flags win over the process environment, the
3607
+ process environment wins over .env files. Run 'wod init' once first: it
3608
+ creates the SQLite file + schema, creates the PostgreSQL database when
3609
+ your role is authorized to (otherwise use an existing/provisioned one),
3610
+ or installs the schema in your provisioned Turso database \u2014 idempotently,
3611
+ preserving existing notes. 'wod server' refuses an uninitialized database.
3612
+ --db <path> Local SQLite database file (default: $WOD_WIKI_DB
3613
+ or ./wodwiki.db). Passing --db always selects SQLite,
3614
+ even when the environment names a remote backend.
3615
+ Created on demand by parse/run/seed.
3616
+ --db-driver <name> Backend driver: sqlite (default), postgres, or
3617
+ turso ($DB_DRIVER)
3618
+ --database-url <url> Database connection URL ($DATABASE_URL); accepts
3619
+ PostgreSQL, local SQLite, and remote libSQL URLs.
3620
+ --postgres-host <host> PostgreSQL host ($POSTGRES_HOST)
3621
+ --postgres-port <port> PostgreSQL port ($POSTGRES_PORT)
3622
+ --postgres-user <user> PostgreSQL user ($POSTGRES_USER)
3623
+ --postgres-password <pw> PostgreSQL password ($POSTGRES_PASSWORD)
3624
+ --postgres-database <db> PostgreSQL database name ($POSTGRES_DATABASE)
3625
+ --postgres-ssl <mode> PostgreSQL SSL mode, e.g. disable/prefer/require
3626
+ ($POSTGRES_SSL)
3627
+ --turso-url <url> Turso/libSQL database URL (libsql://\u2026). The
3628
+ hosted database must already exist (provisioned on
3629
+ Turso); only its schema is installed.
3630
+ --turso-auth-token <token> Turso authentication token (never printed)
3631
+ --env-file <path> Load variables from this file; a missing file is
3632
+ an error. Without it: ./.env when present, otherwise
3633
+ the workspace-root .env. Credentials are never logged.
3634
+
3635
+ HOSTING (for 'server' / 'playground'; requires the Bun runtime):
3636
+ server HTTP API over the configured database plus the frontend
3637
+ packaged inside the engine module. Requires an
3638
+ initialized database. ('serve' is an accepted alias.)
3639
+ playground Serves the playground app backed by browser IndexedDB;
3640
+ the configured database is not touched.
3641
+ --host <host> Bind address (default 127.0.0.1, $HOST)
3642
+ --port <port> TCP port (default 3000, $PORT)
3643
+ --dist <dir> Serve this prebuilt frontend directory instead of the
3644
+ packaged one ($PLAYGROUND_DIST); must already contain
3645
+ index.html.
3128
3646
 
3129
3647
  SEED SOURCING (for 'seed'):
3130
3648
  --source <dir> Extra compiled seed directory (manifest.json + chunks);
@@ -3144,6 +3662,15 @@ OPTIONS:
3144
3662
  -h, --help Show help message
3145
3663
  -v, --version Show version number
3146
3664
 
3665
+ ENVIRONMENT (flags > process env > .env):
3666
+ WOD_WIKI_DB, SQLITE_PATH SQLite file path
3667
+ DB_DRIVER sqlite | postgres | turso
3668
+ DATABASE_URL PostgreSQL connection URL
3669
+ POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER,
3670
+ POSTGRES_PASSWORD, POSTGRES_DATABASE, POSTGRES_SSL
3671
+ TURSO_DATABASE_URL, TURSO_AUTH_TOKEN, LIBSQL_AUTH_TOKEN
3672
+ HOST, PORT, PLAYGROUND_DIST server / playground defaults
3673
+
3147
3674
  EXIT CODES:
3148
3675
  0 - OK / Success
3149
3676
  1 - General / Runtime error
@@ -3163,6 +3690,10 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3163
3690
  const writeOut = io.stdout ?? ((t) => process.stdout.write(t + "\n"));
3164
3691
  const writeErr = io.stderr ?? ((t) => process.stderr.write(t + "\n"));
3165
3692
  const getStdin = io.readStdinFn ?? readStdin;
3693
+ let envLoaded;
3694
+ const cliEnv = () => envLoaded ??= { ...loadCliEnvironment(parsed.envFile), ...process.env };
3695
+ let dbResolved;
3696
+ const cliDb = () => dbResolved ??= resolveDatabaseConfig(parsed, cliEnv());
3166
3697
  let parsed;
3167
3698
  try {
3168
3699
  parsed = parseCliArgs(argv);
@@ -3229,7 +3760,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3229
3760
  const ir = runParse(source, { sport: parsed.sport, sourceLabel: `cli:wod parse ${parsed.target}` });
3230
3761
  const identity = extractNoteIdentity(source);
3231
3762
  const tags = extractTypedTags(source);
3232
- const db = await openCliDb(parsed.db);
3763
+ const db = await openCliDb(cliDb());
3233
3764
  try {
3234
3765
  const persisted = await persistNote(db, {
3235
3766
  source: noteSource,
@@ -3268,7 +3799,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3268
3799
  source = fs.readFileSync(parsed.target, "utf-8");
3269
3800
  }
3270
3801
  const identity = extractNoteIdentity(source);
3271
- const db = await openCliDb(parsed.db);
3802
+ const db = await openCliDb(cliDb());
3272
3803
  let ir;
3273
3804
  try {
3274
3805
  const started = await startRun(db, {
@@ -3319,14 +3850,14 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3319
3850
  }
3320
3851
  let ir;
3321
3852
  if (useDb) {
3322
- const dbPath = resolveDbPath(parsed.db);
3323
- if (!nodeFs().existsSync(dbPath)) {
3853
+ const config = cliDb();
3854
+ if (config.kind === "sqlite" && !nodeFs().existsSync(config.path)) {
3324
3855
  writeErr(
3325
- `[wod query] No input dataset and no database at ${dbPath}. Specify --corpus <file>, --stdin-log, --stdin-facts, or run \`wod seed\` first.`
3856
+ `[wod query] No input dataset and no database at ${config.path}. Specify --corpus <file>, --stdin-log, --stdin-facts, or run \`wod seed\` first.`
3326
3857
  );
3327
3858
  return 1;
3328
3859
  }
3329
- const db = await openCliDb(parsed.db);
3860
+ const db = await openCliDb(config, config.kind === "sqlite" ? void 0 : { initialize: false });
3330
3861
  try {
3331
3862
  ir = await runQueryCli(parsed.target, {
3332
3863
  preferredUnit: parsed.unit,
@@ -3372,7 +3903,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3372
3903
  return 1;
3373
3904
  }
3374
3905
  const targetId = id;
3375
- const db = await openCliDb(parsed.db);
3906
+ const db = await openCliDb(cliDb());
3376
3907
  try {
3377
3908
  if (kind === "note") {
3378
3909
  const bundle = await getNoteBundle(db.db, targetId);
@@ -3449,7 +3980,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3449
3980
  const source = fs.readFileSync(file, "utf-8");
3450
3981
  const identity = extractNoteIdentity(source);
3451
3982
  const tags = extractTypedTags(source);
3452
- const db2 = await openCliDb(parsed.db);
3983
+ const db2 = await openCliDb(cliDb());
3453
3984
  try {
3454
3985
  const persisted = await persistNote(db2, {
3455
3986
  source,
@@ -3472,7 +4003,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3472
4003
  writeErr(`[wod import] Unparseable effort file (need frontmatter id/slug/label + numeric baseAttributes.met): ${file}`);
3473
4004
  return 1;
3474
4005
  }
3475
- const db2 = await openCliDb(parsed.db);
4006
+ const db2 = await openCliDb(cliDb());
3476
4007
  try {
3477
4008
  await persistEffort(db2, { ...effort, registrySource: "user" });
3478
4009
  writeOut(`[wod import] effort ${effort.slug} \u2014 ${effort.label} \u2192 ${db2.path}`);
@@ -3487,7 +4018,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3487
4018
  writeErr(`[wod import] No equipment labels found in ${file}`);
3488
4019
  return 1;
3489
4020
  }
3490
- const db2 = await openCliDb(parsed.db);
4021
+ const db2 = await openCliDb(cliDb());
3491
4022
  try {
3492
4023
  const added = await persistTypedTags(db2, labels, "equipment");
3493
4024
  writeOut(`[wod import] equipment \u2014 ${labels.length} labels (${added} new) \u2192 ${db2.path}`);
@@ -3497,7 +4028,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3497
4028
  return 0;
3498
4029
  }
3499
4030
  const { session, events } = csvToSessionRows(fs.readFileSync(file, "utf-8"));
3500
- const db = await openCliDb(parsed.db);
4031
+ const db = await openCliDb(cliDb());
3501
4032
  try {
3502
4033
  const result = await persistSessionBundle(db, session, events);
3503
4034
  writeOut(
@@ -3544,7 +4075,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3544
4075
  );
3545
4076
  return 1;
3546
4077
  }
3547
- const db = await openCliDb(parsed.db);
4078
+ const db = await openCliDb(cliDb());
3548
4079
  try {
3549
4080
  for (const dir of dirs) {
3550
4081
  const summary = await importSeedDir(db, dir, "");
@@ -3558,7 +4089,14 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3558
4089
  return 0;
3559
4090
  }
3560
4091
  if (parsed.subcommand === "reset") {
3561
- const target = resolveDbPath(parsed.db);
4092
+ const config = cliDb();
4093
+ if (config.kind !== "sqlite") {
4094
+ writeErr(
4095
+ `[wod reset] refusing to reset the configured ${config.kind} database \u2014 reset only removes a local SQLite file. Clear remote databases through their provider.`
4096
+ );
4097
+ return 1;
4098
+ }
4099
+ const target = config.path;
3562
4100
  if (nodeFs().existsSync(target)) {
3563
4101
  nodeFs().unlinkSync(target);
3564
4102
  writeOut(`[wod reset] removed ${target}`);
@@ -3567,6 +4105,32 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3567
4105
  }
3568
4106
  return 0;
3569
4107
  }
4108
+ if (parsed.subcommand === "init") {
4109
+ const db = await openCliDb(cliDb(), { create: true });
4110
+ try {
4111
+ writeOut(`[wod init] database ready at ${db.path}`);
4112
+ } finally {
4113
+ await db.close();
4114
+ }
4115
+ return 0;
4116
+ }
4117
+ if (parsed.subcommand === "server" || parsed.subcommand === "serve" || parsed.subcommand === "playground") {
4118
+ if (!("Bun" in globalThis)) {
4119
+ writeErr(
4120
+ `[wod ${parsed.subcommand}] Error: hosting requires the Bun runtime (https://bun.sh) \u2014 run \`bun wod server\` / \`bun wod playground\`.`
4121
+ );
4122
+ return 1;
4123
+ }
4124
+ const mode = parsed.subcommand === "playground" ? "playground" : "server";
4125
+ const { runCliServer: runCliServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
4126
+ const options = { env: cliEnv() };
4127
+ if (mode === "server") options.database = cliDb();
4128
+ if (parsed.host !== void 0) options.host = parsed.host;
4129
+ if (parsed.port !== void 0) options.port = parsed.port;
4130
+ if (parsed.dist !== void 0) options.dist = parsed.dist;
4131
+ await runCliServer2(mode, options, writeOut);
4132
+ return 0;
4133
+ }
3570
4134
  writeErr(`[wod] Unknown subcommand "${parsed.subcommand}"`);
3571
4135
  return 1;
3572
4136
  } catch (error) {
@@ -3584,13 +4148,13 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3584
4148
  }
3585
4149
 
3586
4150
  // src/version.ts
3587
- var VERSION2 = "0.35.2129";
3588
- var GIT_SHA = "a49fd2c";
3589
- var BUILD_TIME = "2026-10-14T20:59:51.571Z";
4151
+ var VERSION2 = "0.36.2130";
4152
+ var GIT_SHA = "aabe90c";
4153
+ var BUILD_TIME = "2026-10-14T23:19:24.665Z";
3590
4154
  var SEMVER = {
3591
- version: "0.35.2129",
3592
- gitSha: "a49fd2c",
3593
- buildTime: "2026-10-14T20:59:51.571Z"
4155
+ version: "0.36.2130",
4156
+ gitSha: "aabe90c",
4157
+ buildTime: "2026-10-14T23:19:24.665Z"
3594
4158
  };
3595
4159
 
3596
4160
  // src/index.ts