@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/bin/wod.cjs CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env -S bun --no-env-file
2
2
  'use strict';
3
3
 
4
4
  var wodWikiLang = require('@bitcobblers/wod-wiki-lang');
@@ -10,6 +10,7 @@ var wodWikiWql = require('@bitcobblers/wod-wiki-wql');
10
10
  var crypto$1 = require('crypto');
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) {
@@ -805,6 +1052,132 @@ var init_compiler = __esm({
805
1052
  };
806
1053
  }
807
1054
  });
1055
+
1056
+ // src/cli/server.ts
1057
+ var server_exports = {};
1058
+ __export(server_exports, {
1059
+ runCliServer: () => runCliServer
1060
+ });
1061
+ function bun() {
1062
+ const b = globalThis.Bun;
1063
+ if (!b) {
1064
+ throw new Error("wod server/playground requires the Bun runtime \u2014 start with: bun wod server");
1065
+ }
1066
+ return b;
1067
+ }
1068
+ function moduleDir() {
1069
+ const p = nodePath();
1070
+ const meta = ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('wod.cjs', document.baseURI).href)) });
1071
+ if (typeof meta.dir === "string") return p.resolve(meta.dir);
1072
+ if (typeof meta.url === "string" && meta.url.startsWith("file:")) {
1073
+ return p.resolve(p.dirname(decodeURIComponent(new URL(".", meta.url).pathname)));
1074
+ }
1075
+ throw new Error("wod server/playground: cannot determine the installed engine module directory");
1076
+ }
1077
+ function engineRoot() {
1078
+ const p = nodePath();
1079
+ const fs = nodeFs();
1080
+ for (let dir = moduleDir(); ; dir = p.dirname(dir)) {
1081
+ try {
1082
+ if (JSON.parse(fs.readFileSync(p.resolve(dir, "package.json"), "utf8")).name === "@bitcobblers/wod-wiki-engine") return dir;
1083
+ } catch {
1084
+ }
1085
+ const parent = p.dirname(dir);
1086
+ if (parent === dir) break;
1087
+ }
1088
+ throw new Error("wod server/playground: cannot locate the installed @bitcobblers/wod-wiki-engine package");
1089
+ }
1090
+ function resolveDist(mode, options) {
1091
+ const p = nodePath();
1092
+ const explicit = options.dist?.trim() || options.env.PLAYGROUND_DIST?.trim() || void 0;
1093
+ if (explicit) {
1094
+ const dist2 = p.resolve(explicit);
1095
+ if (!nodeFs().existsSync(p.resolve(dist2, "index.html"))) {
1096
+ throw new Error(
1097
+ `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)`
1098
+ );
1099
+ }
1100
+ return dist2;
1101
+ }
1102
+ const dist = p.resolve(engineRoot(), "dist", "web", mode === "playground" ? "playground" : "server");
1103
+ if (!nodeFs().existsSync(p.resolve(dist, "index.html"))) {
1104
+ throw new Error(
1105
+ `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.`
1106
+ );
1107
+ }
1108
+ return dist;
1109
+ }
1110
+ function resolveBind(mode, options) {
1111
+ const host = options.host?.trim() || options.env.HOST?.trim() || "127.0.0.1";
1112
+ const rawPort = options.port?.trim() || options.env.PORT?.trim() || "3000";
1113
+ const port = Number(rawPort);
1114
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
1115
+ throw new Error(`wod ${mode}: invalid port ${JSON.stringify(rawPort)} (need integer 1..65535)`);
1116
+ }
1117
+ return { host, port };
1118
+ }
1119
+ async function runCliServer(mode, options, writeOut) {
1120
+ const runtime = bun();
1121
+ const { host, port } = resolveBind(mode, options);
1122
+ let db;
1123
+ let dbLabel = "no database";
1124
+ if (mode === "server") {
1125
+ const database = options.database;
1126
+ if (!database) {
1127
+ throw new Error("wod server: no database configuration resolved \u2014 this is a runner bug (expected resolveDatabaseConfig output)");
1128
+ }
1129
+ try {
1130
+ db = await openCliDb(database, { initialize: false });
1131
+ } catch (e) {
1132
+ const message = e instanceof Error ? e.message : String(e);
1133
+ throw new Error(`wod server: database not ready \u2014 run \`wod init\` first. ${message}`, { cause: e });
1134
+ }
1135
+ dbLabel = db.path;
1136
+ }
1137
+ let server;
1138
+ try {
1139
+ const dist = resolveDist(mode, options);
1140
+ const handlerModule = "@bitcobblers/wod-wiki-engine/server";
1141
+ const { createHandler } = await import(
1142
+ /* @vite-ignore */
1143
+ handlerModule
1144
+ );
1145
+ const handler = createHandler(db?.db ?? null, { playgroundDist: dist });
1146
+ server = runtime.serve({ port, hostname: host, fetch: handler });
1147
+ if (mode === "server") {
1148
+ writeOut(`wod server: http://${host}:${port} \u2014 ${dbLabel} \xB7 dist ${dist}`);
1149
+ } else {
1150
+ writeOut(`wod playground: http://${host}:${port} \u2014 in-browser storage (no database) \xB7 dist ${dist}`);
1151
+ }
1152
+ } catch (e) {
1153
+ if (db) await db.close();
1154
+ throw e;
1155
+ }
1156
+ let closing = false;
1157
+ const shutdown = async () => {
1158
+ if (closing) process.exit(0);
1159
+ closing = true;
1160
+ try {
1161
+ await server.stop();
1162
+ } finally {
1163
+ if (db) await db.close();
1164
+ process.exit(0);
1165
+ }
1166
+ };
1167
+ process.on("SIGINT", () => {
1168
+ void shutdown();
1169
+ });
1170
+ process.on("SIGTERM", () => {
1171
+ void shutdown();
1172
+ });
1173
+ await new Promise(() => {
1174
+ });
1175
+ }
1176
+ var init_server = __esm({
1177
+ "src/cli/server.ts"() {
1178
+ init_db();
1179
+ }
1180
+ });
808
1181
  var activePacks = /* @__PURE__ */ new Map();
809
1182
  function registerLanguagePack(pack) {
810
1183
  const packId = pack.identity?.id ?? pack.identity?.name ?? "anonymous-pack";
@@ -1175,7 +1548,7 @@ function loadQueryData(options) {
1175
1548
  } else if (obj.facts || obj.results || obj.notes || obj.blocks || obj.efforts || obj.tags || obj.logs) {
1176
1549
  const corpus = payload;
1177
1550
  if (corpus.facts) data.facts = corpus.facts;
1178
- if (corpus.results) data.results = corpus.results;
1551
+ if (Array.isArray(corpus.results)) data.results = corpus.results;
1179
1552
  if (corpus.notes) data.notes = corpus.notes;
1180
1553
  if (corpus.blocks) data.blocks = corpus.blocks;
1181
1554
  if (corpus.efforts) data.efforts = corpus.efforts;
@@ -1302,193 +1675,256 @@ async function runQueryCli(wqlString, options = {}) {
1302
1675
  });
1303
1676
  return createIRFile("query-result", result, { source: options.sourceLabel ?? "cli:wod query" });
1304
1677
  }
1305
- var nodePath = () => nodeRequire()("node:path");
1306
- var nodeFs = () => nodeRequire()("node:fs");
1307
- var nodeCrypto = () => nodeRequire()("node:crypto");
1308
- function nodeRequire() {
1678
+
1679
+ // src/cli/runner.ts
1680
+ init_db();
1681
+
1682
+ // src/cli/config.ts
1683
+ function nodeRequire2() {
1309
1684
  const g = globalThis;
1310
1685
  if (typeof g.process?.versions?.node === "undefined" && typeof g.Bun === "undefined") {
1311
- throw new Error("SQLite operations are only supported in Node / Bun environments");
1686
+ throw new Error("database configuration is only supported in Node / Bun environments");
1312
1687
  }
1313
1688
  const req = g.require ?? (typeof __require === "function" ? __require : void 0);
1314
- if (!req) throw new Error("SQLite operations require a CJS require shim (missing tsup banner?)");
1689
+ if (!req) throw new Error("database configuration requires a CJS require shim (missing tsup banner?)");
1315
1690
  return req;
1316
1691
  }
1317
- var sqlModulePromise;
1318
- function loadSql() {
1319
- sqlModulePromise ??= import(
1320
- /* @vite-ignore */
1321
- '@bitcobblers/wod-wiki-storage/sql'
1322
- );
1323
- return sqlModulePromise;
1324
- }
1325
- function resolveDbPath(dbPath) {
1326
- const chosen = dbPath ?? process.env.WOD_WIKI_DB ?? "./wodwiki.db";
1327
- return nodePath().resolve(process.cwd(), chosen);
1328
- }
1329
- async function openCliDb(dbPath) {
1330
- const [{ createClient }, sql] = await Promise.all([
1331
- import(
1332
- /* @vite-ignore */
1333
- '@libsql/client'
1334
- ),
1335
- loadSql()
1336
- ]);
1337
- const resolved = resolveDbPath(dbPath);
1338
- nodeFs().mkdirSync(nodePath().dirname(resolved), { recursive: true });
1339
- const db = sql.libsqlDatabase(createClient({ url: `file:${resolved}` }));
1692
+ var nodePath2 = () => nodeRequire2()("node:path");
1693
+ var nodeFs2 = () => nodeRequire2()("node:fs");
1694
+ function parseEnvFile(content) {
1695
+ return nodeRequire2()("node:util").parseEnv(content);
1696
+ }
1697
+ function loadCliEnvironment(envFile) {
1698
+ const fs = nodeFs2();
1699
+ const path = nodePath2();
1700
+ let file;
1701
+ if (envFile !== void 0) {
1702
+ file = path.resolve(process.cwd(), envFile);
1703
+ if (!fs.existsSync(file)) throw new Error(`[wod] --env-file: file not found: ${envFile}`);
1704
+ } else {
1705
+ let dir = process.cwd();
1706
+ for (; ; ) {
1707
+ const candidate = path.join(dir, ".env");
1708
+ if (fs.existsSync(candidate)) {
1709
+ file = candidate;
1710
+ break;
1711
+ }
1712
+ const parent = path.dirname(dir);
1713
+ if (parent === dir) break;
1714
+ dir = parent;
1715
+ }
1716
+ }
1717
+ return file !== void 0 ? parseEnvFile(fs.readFileSync(file, "utf-8")) : {};
1718
+ }
1719
+ var SQLITE_URL_SCHEMES = /^(libsql|https?|wss?|file):\/\//;
1720
+ var PG_URL_SCHEMES = /^postgres(ql)?:\/\//;
1721
+ var TURSO_URL_SCHEMES = /^(libsql|https?|wss?):\/\//;
1722
+ var PG_FLAG_KEYS = [
1723
+ "postgresHost",
1724
+ "postgresPort",
1725
+ "postgresUser",
1726
+ "postgresPassword",
1727
+ "postgresDatabase",
1728
+ "postgresSsl"
1729
+ ];
1730
+ var PG_ENV_KEYS = [
1731
+ "POSTGRES_HOST",
1732
+ "POSTGRES_PORT",
1733
+ "POSTGRES_USER",
1734
+ "POSTGRES_PASSWORD",
1735
+ "POSTGRES_DATABASE",
1736
+ "POSTGRES_SSL"
1737
+ ];
1738
+ var isBackend = (v) => v === "sqlite" || v === "postgres" || v === "turso";
1739
+ function setEnv(v) {
1740
+ return v !== void 0 && v.trim() !== "" ? v : void 0;
1741
+ }
1742
+ function fail(message) {
1743
+ throw new Error(`[wod] ${message}`);
1744
+ }
1745
+ function parsePort(v, source) {
1746
+ if (!/^\d+$/.test(v)) fail(`${source} must be an integer 1..65535, got ${JSON.stringify(v)}`);
1747
+ const port = Number(v);
1748
+ if (port < 1 || port > 65535) fail(`${source} must be an integer 1..65535, got ${v}`);
1749
+ return port;
1750
+ }
1751
+ function sslMode(v, source) {
1752
+ if (v === void 0 || v === "") return "prefer";
1753
+ if (typeof v === "boolean") return v ? "require" : "disable";
1754
+ const s = v.trim().toLowerCase();
1755
+ if (["true", "1", "yes", "require"].includes(s)) return "require";
1756
+ if (["false", "0", "no", "disable", "disabled"].includes(s)) return "disable";
1757
+ if (s === "prefer" || s === "verify-ca" || s === "verify-full") return s;
1758
+ fail(`${source} must be disable|prefer|require|verify-ca|verify-full, got ${JSON.stringify(v)}`);
1759
+ }
1760
+ function safeUrlLabel(raw) {
1340
1761
  try {
1341
- await sql.initializeStore(db);
1342
- await sql.initializeDomain(db);
1343
- return { db, storage: new sql.SqlStorage(db), path: resolved, close: () => db.close() };
1344
- } catch (error) {
1345
- await db.close();
1346
- throw error;
1762
+ const u = new URL(raw);
1763
+ return `${u.protocol}//${u.host}${u.pathname}`;
1764
+ } catch {
1765
+ return "<unparsable-url>";
1347
1766
  }
1348
1767
  }
1349
- function formatV5Digest(digest) {
1350
- const bytes = digest.slice(0, 16);
1351
- bytes[6] = bytes[6] & 15 | 80;
1352
- bytes[8] = bytes[8] & 63 | 128;
1353
- const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0"));
1354
- 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("")}`;
1355
- }
1356
- function uuid5(namespace, name) {
1357
- return formatV5Digest(nodeCrypto().createHash("sha1").update(namespace).update(name).digest());
1358
- }
1359
- var CLI_UUID_NAMESPACE = "c0ffee00-8b1a-4e2c-9d3f-5a6b7c8d9e0f";
1360
- function stableNoteId(sourceLabel, content) {
1361
- if (sourceLabel && sourceLabel !== "-") {
1362
- return uuid5(CLI_UUID_NAMESPACE, `wod-cli:${nodePath().resolve(process.cwd(), sourceLabel)}`);
1768
+ function postgresFromUrl(url, source) {
1769
+ if (!PG_URL_SCHEMES.test(url)) {
1770
+ fail(`${source} must start with postgres:// or postgresql://`);
1363
1771
  }
1364
- return uuid5(CLI_UUID_NAMESPACE, `wod-cli:stdin:${content.length}:${content.slice(0, 256)}`);
1365
- }
1366
- function randomId() {
1367
- return nodeCrypto().randomUUID();
1368
- }
1369
- async function allRows(sql, db, store) {
1370
- return sql.listRows(db, sql.storeDefinition(store));
1371
- }
1372
- async function rowsByIndex(sql, db, store, index, only) {
1373
- return sql.listRows(db, sql.storeDefinition(store), { only }, void 0, index);
1374
- }
1375
- function sqliteEventStore(db) {
1376
- const all = async () => await allRows(await loadSql(), db, "events");
1772
+ let u;
1773
+ try {
1774
+ u = new URL(url);
1775
+ } catch {
1776
+ fail(`${source} is not a parseable URL`);
1777
+ }
1778
+ const database = decodeURIComponent(u.pathname.replace(/^\/+|\/+$/g, ""));
1779
+ if (database === "") fail(`${source} has no database name`);
1377
1780
  return {
1378
- getEventsByTimeRange: async (start, end) => (await all()).filter((r) => r.timestamp >= start && r.timestamp <= end),
1379
- getEventsByResult: async (resultId) => (await all()).filter((r) => r.resultId === resultId),
1380
- getEventsForNote: async (noteId) => (await all()).filter((r) => r.noteId === noteId),
1381
- getEventsByContent: async (blockContentId5) => (await all()).filter((r) => r.blockContentId === blockContentId5),
1382
- scanAll: all,
1383
- appendEvents: async (appended) => {
1384
- const sql = await loadSql();
1385
- await db.transaction(async (tx) => {
1386
- const eventsDef = sql.storeDefinition("events");
1387
- for (const row of appended) await sql.putRow(tx, eventsDef, row);
1388
- });
1389
- },
1390
- finalizeSummaries: async (resultId, finals) => {
1391
- const sql = await loadSql();
1392
- await db.transaction(async (tx) => {
1393
- const eventsDef = sql.storeDefinition("events");
1394
- const existing = (await sql.listRows(tx, eventsDef)).filter(
1395
- (r) => r.resultId === resultId && r.grain === "summary" && r.origin !== "user"
1396
- );
1397
- for (const row of existing) await sql.deleteRows(tx, eventsDef, row.id);
1398
- for (const row of finals) await sql.putRow(tx, eventsDef, row);
1399
- });
1400
- },
1401
- deleteEvents: async (ids) => {
1402
- const sql = await loadSql();
1403
- await db.transaction(async (tx) => {
1404
- const eventsDef = sql.storeDefinition("events");
1405
- for (const id of ids) await sql.deleteRows(tx, eventsDef, id);
1406
- });
1407
- }
1781
+ kind: "postgres",
1782
+ label: safeUrlLabel(url),
1783
+ url,
1784
+ host: u.hostname,
1785
+ port: u.port !== "" ? parsePort(u.port, `${source} port`) : 5432,
1786
+ database,
1787
+ user: u.username !== "" ? decodeURIComponent(u.username) : void 0,
1788
+ password: u.password !== "" ? decodeURIComponent(u.password) : void 0,
1789
+ ssl: sslMode(u.searchParams.get("sslmode") ?? u.searchParams.get("ssl") ?? void 0, `${source} ssl parameter`)
1408
1790
  };
1409
1791
  }
1410
- function sqliteNoteStore(db) {
1411
- return {
1412
- getAllNotes: async () => await allRows(await loadSql(), db, "notes"),
1413
- getNotesByIds: async (noteIds) => {
1414
- const sql = await loadSql();
1415
- const rows = await sql.getRows(db, sql.storeDefinition("notes"), [...noteIds]);
1416
- return rows.filter((row) => row !== void 0);
1417
- },
1418
- getNoteIdsForTag: async (label) => {
1419
- const sql = await loadSql();
1420
- const tags = (await allRows(sql, db, "tags")).filter((t) => t.label === label);
1421
- const ids = {};
1422
- for (const tag of tags) {
1423
- for (const row of await rowsByIndex(sql, db, "note_tags", "by-tag", tag.id)) {
1424
- ids[row.noteId] = true;
1425
- }
1426
- }
1427
- return new Set(Object.keys(ids));
1428
- },
1429
- getNoteTagLabels: async (noteId) => {
1430
- const sql = await loadSql();
1431
- const tagRows = await allRows(sql, db, "tags");
1432
- const labelById = {};
1433
- for (const t of tagRows) labelById[t.id] = t.label;
1434
- const links = await rowsByIndex(sql, db, "note_tags", "by-note", noteId);
1435
- return links.map((l) => labelById[l.tagId]).filter((l) => typeof l === "string");
1436
- },
1437
- queryDomain: async (request) => {
1438
- const result = await (await loadSql()).handleDomainQuery(db, request);
1439
- return result;
1440
- }
1441
- };
1442
- }
1443
- function sqliteBlockStore(db) {
1444
- return {
1445
- getAllBlocks: async () => await allRows(await loadSql(), db, "block_index"),
1446
- queryDomain: async (request) => {
1447
- const result = await (await loadSql()).handleDomainQuery(db, { ...request, plan: "blocks" });
1448
- return result;
1792
+ function resolvePostgres(options, e, databaseUrl) {
1793
+ const flagProps = PG_FLAG_KEYS.some((k) => options[k] !== void 0);
1794
+ const envProps = PG_ENV_KEYS.some((k) => setEnv(e[k]) !== void 0);
1795
+ if (flagProps || envProps && databaseUrl === void 0) {
1796
+ const host = options.postgresHost ?? setEnv(e.POSTGRES_HOST) ?? "127.0.0.1";
1797
+ const port = parsePort(options.postgresPort ?? setEnv(e.POSTGRES_PORT) ?? "5432", "--postgres-port/POSTGRES_PORT");
1798
+ const database = options.postgresDatabase ?? setEnv(e.POSTGRES_DATABASE);
1799
+ if (database === void 0) {
1800
+ fail("--postgres-database (or POSTGRES_DATABASE) is required for per-property PostgreSQL configuration");
1449
1801
  }
1450
- };
1451
- }
1452
- function sqliteEffortStore(db) {
1453
- return {
1454
- getAllEfforts: async () => {
1455
- const stored = await allRows(await loadSql(), db, "efforts");
1456
- const bySlug = {};
1457
- for (const effort of stored) bySlug[effort.slug] = effort;
1458
- for (const effort of wodWikiLang.bundledEfforts) {
1459
- bySlug[effort.slug] ??= effort;
1460
- }
1461
- return Object.values(bySlug);
1802
+ const user = options.postgresUser ?? setEnv(e.POSTGRES_USER);
1803
+ const password = options.postgresPassword ?? setEnv(e.POSTGRES_PASSWORD);
1804
+ const ssl = sslMode(options.postgresSsl ?? setEnv(e.POSTGRES_SSL), "--postgres-ssl/POSTGRES_SSL");
1805
+ try {
1806
+ const u = new URL(`postgresql://${host}`);
1807
+ u.port = String(port);
1808
+ u.pathname = `/${database}`;
1809
+ if (user !== void 0) u.username = user;
1810
+ if (password !== void 0) u.password = password;
1811
+ u.searchParams.set("sslmode", ssl);
1812
+ return {
1813
+ kind: "postgres",
1814
+ label: `postgresql://${u.host}${u.pathname}`,
1815
+ url: u.toString(),
1816
+ host,
1817
+ port,
1818
+ database,
1819
+ user,
1820
+ password,
1821
+ ssl
1822
+ };
1823
+ } catch {
1824
+ fail("invalid PostgreSQL host/database in per-property configuration");
1462
1825
  }
1463
- };
1826
+ }
1827
+ if (databaseUrl !== void 0) return postgresFromUrl(databaseUrl, "DB_DRIVER=postgres DATABASE_URL");
1828
+ fail("DB_DRIVER=postgres requires DATABASE_URL starting with postgres:// or postgresql:// \u2014 or per-property --postgres-host/--postgres-database flags");
1464
1829
  }
1465
- function sqliteIndexStore(db, store) {
1830
+ function resolveTurso(options, e, databaseUrl) {
1831
+ const url = options.tursoUrl ?? setEnv(e.TURSO_DATABASE_URL) ?? databaseUrl;
1832
+ if (url === void 0) {
1833
+ fail("DB_DRIVER=turso requires --turso-url / TURSO_DATABASE_URL or DATABASE_URL (libsql:// or https://)");
1834
+ }
1835
+ if (PG_URL_SCHEMES.test(url)) fail("DB_DRIVER=turso cannot use a postgres:// URL");
1836
+ if (!TURSO_URL_SCHEMES.test(url)) fail("DB_DRIVER=turso requires a libsql://, https://, http:// or wss:// URL");
1466
1837
  return {
1467
- getAllFromIndex: async (index, key) => rowsByIndex(await loadSql(), db, store, index, key)
1838
+ kind: "turso",
1839
+ label: safeUrlLabel(url),
1840
+ url,
1841
+ authToken: options.tursoAuthToken ?? setEnv(e.TURSO_AUTH_TOKEN) ?? setEnv(e.LIBSQL_AUTH_TOKEN)
1468
1842
  };
1469
1843
  }
1470
- async function getNoteBundle(db, noteId) {
1471
- const sql = await loadSql();
1472
- const note = await sql.getRow(db, sql.storeDefinition("notes"), noteId);
1473
- return { note };
1474
- }
1475
- async function getSessionBundle(db, resultId) {
1476
- const sql = await loadSql();
1477
- const session = await sql.getRow(db, sql.storeDefinition("sessions"), resultId);
1478
- if (!session) return void 0;
1479
- const events = await sql.listRows(db, sql.storeDefinition("events"), { only: resultId }, void 0, "by-result-grain");
1480
- return { session, events };
1481
- }
1482
- async function getEffortBySlug(db, slug) {
1483
- const sql = await loadSql();
1484
- const efforts = await allRows(sql, db, "efforts");
1485
- return efforts.find((e) => e.slug === slug);
1844
+ function resolveSqlite(options, e, databaseUrl) {
1845
+ if (options.db !== void 0) {
1846
+ const path2 = nodePath2().resolve(process.cwd(), options.db);
1847
+ return { kind: "sqlite", label: path2, path: path2 };
1848
+ }
1849
+ if (databaseUrl !== void 0) {
1850
+ if (PG_URL_SCHEMES.test(databaseUrl)) fail("DB_DRIVER=sqlite cannot use a postgres:// DATABASE_URL");
1851
+ if (databaseUrl.startsWith("file:")) {
1852
+ const path2 = nodePath2().resolve(process.cwd(), decodeURIComponent(databaseUrl.slice("file:".length).split("?")[0]));
1853
+ return { kind: "sqlite", label: path2, path: path2 };
1854
+ }
1855
+ if (!SQLITE_URL_SCHEMES.test(databaseUrl)) {
1856
+ fail("DB_DRIVER=sqlite requires a libsql://, https://, http://, wss:// or file:// DATABASE_URL");
1857
+ }
1858
+ return { kind: "turso", label: safeUrlLabel(databaseUrl), url: databaseUrl, authToken: setEnv(e.LIBSQL_AUTH_TOKEN) };
1859
+ }
1860
+ const path = nodePath2().resolve(process.cwd(), setEnv(e.WOD_WIKI_DB) ?? setEnv(e.SQLITE_PATH) ?? "./wodwiki.db");
1861
+ return { kind: "sqlite", label: path, path };
1862
+ }
1863
+ function envBackend(e) {
1864
+ const kinds = /* @__PURE__ */ new Set();
1865
+ if (setEnv(e.WOD_WIKI_DB) !== void 0 || setEnv(e.SQLITE_PATH) !== void 0) kinds.add("sqlite");
1866
+ const dbUrl = setEnv(e.DATABASE_URL);
1867
+ if (PG_ENV_KEYS.some((k) => setEnv(e[k]) !== void 0) || dbUrl !== void 0 && PG_URL_SCHEMES.test(dbUrl)) {
1868
+ kinds.add("postgres");
1869
+ } else if (dbUrl !== void 0 && SQLITE_URL_SCHEMES.test(dbUrl)) {
1870
+ kinds.add("sqlite");
1871
+ }
1872
+ if (setEnv(e.TURSO_DATABASE_URL) !== void 0) kinds.add("turso");
1873
+ if (kinds.size > 1) {
1874
+ fail(`ambiguous database configuration: ${[...kinds].join(" + ")} env vars are both set \u2014 set DB_DRIVER (or pass explicit flags) to choose`);
1875
+ }
1876
+ return kinds.values().next().value ?? "sqlite";
1486
1877
  }
1487
- async function listTagsByType(db, type) {
1488
- const sql = await loadSql();
1489
- const tags = await allRows(sql, db, "tags");
1490
- return tags.filter((t) => t.type === type);
1878
+ function resolveDatabaseConfig(options = {}, env) {
1879
+ const e = env ?? { ...loadCliEnvironment(options.envFile), ...process.env };
1880
+ const driverFlag = options.dbDriver;
1881
+ if (driverFlag !== void 0 && !isBackend(driverFlag)) {
1882
+ fail(`--db-driver must be sqlite|postgres|turso, got ${JSON.stringify(driverFlag)}`);
1883
+ }
1884
+ const pinned = [];
1885
+ if (options.db !== void 0) pinned.push(["--db", "sqlite"]);
1886
+ if (driverFlag !== void 0) pinned.push([`--db-driver ${driverFlag}`, driverFlag]);
1887
+ if (options.tursoUrl !== void 0) pinned.push(["--turso-url", "turso"]);
1888
+ if (options.tursoAuthToken !== void 0) pinned.push(["--turso-auth-token", "turso"]);
1889
+ if (PG_FLAG_KEYS.some((k) => options[k] !== void 0)) pinned.push(["--postgres-*", "postgres"]);
1890
+ const kinds = new Set(pinned.map(([, kind]) => kind));
1891
+ if (kinds.size > 1) {
1892
+ fail(`conflicting database selectors: ${pinned.map(([flag, kind]) => `${flag} (${kind})`).join(", ")}`);
1893
+ }
1894
+ if (options.databaseUrl !== void 0) {
1895
+ if (options.db !== void 0) fail("conflicting database selectors: --db and --database-url are both set");
1896
+ if (options.tursoUrl !== void 0) fail("conflicting database selectors: --turso-url and --database-url are both set");
1897
+ if (PG_FLAG_KEYS.some((k) => options[k] !== void 0)) {
1898
+ fail("conflicting database selectors: --database-url and --postgres-* flags are both set");
1899
+ }
1900
+ }
1901
+ let backend;
1902
+ if (pinned.length > 0) {
1903
+ backend = pinned[0][1];
1904
+ } else {
1905
+ const envDriver = setEnv(e.DB_DRIVER);
1906
+ if (envDriver !== void 0) {
1907
+ if (!isBackend(envDriver)) {
1908
+ fail(`Unsupported DB_DRIVER: ${envDriver} (expected sqlite|turso|postgres)`);
1909
+ }
1910
+ backend = envDriver;
1911
+ } else {
1912
+ backend = envBackend(e);
1913
+ }
1914
+ }
1915
+ const databaseUrl = options.databaseUrl ?? setEnv(e.DATABASE_URL);
1916
+ switch (backend) {
1917
+ case "postgres":
1918
+ return resolvePostgres(options, e, databaseUrl);
1919
+ case "turso":
1920
+ return resolveTurso(options, e, databaseUrl);
1921
+ case "sqlite":
1922
+ return resolveSqlite(options, e, databaseUrl);
1923
+ }
1491
1924
  }
1925
+
1926
+ // src/cli/persist.ts
1927
+ init_db();
1492
1928
  var registeredWidgets = wodWikiCore.KNOWN_SECTION_WIDGETS;
1493
1929
  var saveStores = ["notes", "block_revisions", "block_index", "block_efforts", "tags", "note_tags", "sessions"];
1494
1930
  function extractEffortSlugs(statements) {
@@ -1758,6 +2194,9 @@ async function persistTypedTags(db, labels, type) {
1758
2194
  return added;
1759
2195
  });
1760
2196
  }
2197
+
2198
+ // src/cli/seed.ts
2199
+ init_db();
1761
2200
  var NOTE_ID_FRONTMATTER_KEY = "wod-note-id";
1762
2201
  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;
1763
2202
  function extractNoteIdentity(markdown) {
@@ -2880,16 +3319,53 @@ ${source}
2880
3319
  ${fence}
2881
3320
  `;
2882
3321
  }
3322
+ var VALUE_FLAGS = [
3323
+ ["--db", "db"],
3324
+ ["--db-driver", "dbDriver"],
3325
+ ["--database-url", "databaseUrl"],
3326
+ ["--postgres-host", "postgresHost"],
3327
+ ["--postgres-port", "postgresPort"],
3328
+ ["--postgres-user", "postgresUser"],
3329
+ ["--postgres-password", "postgresPassword"],
3330
+ ["--postgres-database", "postgresDatabase"],
3331
+ ["--postgres-ssl", "postgresSsl"],
3332
+ ["--turso-url", "tursoUrl"],
3333
+ ["--turso-auth-token", "tursoAuthToken"],
3334
+ ["--env-file", "envFile"],
3335
+ ["--host", "host"],
3336
+ ["--port", "port"],
3337
+ ["--dist", "dist"]
3338
+ ];
3339
+ function readValueFlag(args, i, flag) {
3340
+ const inline = args[i].startsWith(`${flag}=`);
3341
+ if (inline) {
3342
+ const value2 = args[i].slice(flag.length + 1);
3343
+ if (!value2) throw new Error(`Missing value for ${flag}`);
3344
+ return { value: value2, next: i + 1 };
3345
+ }
3346
+ const value = args[i + 1];
3347
+ if (value === void 0 || value === "" || value.startsWith("--")) throw new Error(`Missing value for ${flag}`);
3348
+ return { value, next: i + 2 };
3349
+ }
2883
3350
  function parseCliArgs(args) {
2884
3351
  const result = {
2885
3352
  format: "json",
2886
3353
  packs: [],
2887
3354
  sources: []
2888
3355
  };
3356
+ const valueArgs = result;
2889
3357
  let i = 0;
2890
3358
  while (i < args.length) {
2891
3359
  const arg = args[i];
2892
3360
  if (arg === "--at" || arg.startsWith("--at=")) throw new Error("Unknown option --at; whole Note exports have no as-of version");
3361
+ const valueFlag = VALUE_FLAGS.find(([flag]) => arg === flag || arg.startsWith(`${flag}=`));
3362
+ if (valueFlag) {
3363
+ const [flag, key] = valueFlag;
3364
+ const { value, next } = readValueFlag(args, i, flag);
3365
+ valueArgs[key] = value;
3366
+ i = next;
3367
+ continue;
3368
+ }
2893
3369
  if (arg === "--help" || arg === "-h") {
2894
3370
  result.help = true;
2895
3371
  i++;
@@ -2936,12 +3412,6 @@ function parseCliArgs(args) {
2936
3412
  } else if (arg === "--unit") {
2937
3413
  result.unit = args[++i];
2938
3414
  i++;
2939
- } else if (arg === "--db") {
2940
- result.db = args[++i];
2941
- i++;
2942
- } else if (arg.startsWith("--db=")) {
2943
- result.db = arg.slice("--db=".length);
2944
- i++;
2945
3415
  } else if (arg === "--debug") {
2946
3416
  result.debug = "";
2947
3417
  i++;
@@ -2977,7 +3447,7 @@ function parseCliArgs(args) {
2977
3447
  result.dir = arg.slice("--dir=".length);
2978
3448
  i++;
2979
3449
  } else if (!result.subcommand) {
2980
- if (arg === "parse" || arg === "run" || arg === "query" || arg === "download" || arg === "import" || arg === "build" || arg === "seed" || arg === "reset" || arg === "help" || arg === "version") {
3450
+ 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") {
2981
3451
  result.subcommand = arg;
2982
3452
  } else {
2983
3453
  result.target = arg;
@@ -2993,14 +3463,19 @@ function parseCliArgs(args) {
2993
3463
  i++;
2994
3464
  }
2995
3465
  }
3466
+ for (const [flag, value] of [["--port", result.port], ["--postgres-port", result.postgresPort]]) {
3467
+ if (value !== void 0 && (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 65535)) {
3468
+ throw new Error(`Invalid value for ${flag}: "${value}" is not a TCP port (1-65535)`);
3469
+ }
3470
+ }
2996
3471
  return result;
2997
3472
  }
2998
3473
  function readBunStdinText() {
2999
3474
  const g = globalThis;
3000
3475
  if (g && typeof g === "object" && "Bun" in g) {
3001
- const bun = g.Bun;
3002
- if (bun && typeof bun === "object" && "stdin" in bun) {
3003
- const stdin = bun.stdin;
3476
+ const bun2 = g.Bun;
3477
+ if (bun2 && typeof bun2 === "object" && "stdin" in bun2) {
3478
+ const stdin = bun2.stdin;
3004
3479
  if (stdin && typeof stdin === "object" && "text" in stdin) {
3005
3480
  const text = stdin.text;
3006
3481
  if (typeof text === "function") return text;
@@ -3038,6 +3513,11 @@ USAGE:
3038
3513
  wod build [dir] [options]
3039
3514
  wod seed [options]
3040
3515
  wod reset [options]
3516
+ wod init Initialize the configured database (idempotent)
3517
+ wod server [options]
3518
+ Serve the app (HTTP API + packaged frontend; alias: serve)
3519
+ wod playground [options]
3520
+ Serve the local playground (browser IndexedDB build)
3041
3521
 
3042
3522
  SUBCOMMANDS:
3043
3523
  parse <file | -> Parse Whiteboard Script, create a note in the database
@@ -3091,9 +3571,47 @@ INPUT SOURCING (for 'query'; without these, the database is queried):
3091
3571
  --stdin-log Read kind: 'execution-log' JSON payload from stdin
3092
3572
  --stdin-facts Read raw kind: 'fact-set' JSON payload from stdin
3093
3573
 
3094
- DATABASE (parse / run / query / download / import / seed / reset):
3095
- --db <path> SQLite database file (default: $WOD_WIKI_DB or
3096
- ./wodwiki.db). Created on demand by parse/run/seed.
3574
+ DATABASE (init / parse / run / query / download / import / seed / reset / server):
3575
+ Pick one backend; explicit flags win over the process environment, the
3576
+ process environment wins over .env files. Run 'wod init' once first: it
3577
+ creates the SQLite file + schema, creates the PostgreSQL database when
3578
+ your role is authorized to (otherwise use an existing/provisioned one),
3579
+ or installs the schema in your provisioned Turso database \u2014 idempotently,
3580
+ preserving existing notes. 'wod server' refuses an uninitialized database.
3581
+ --db <path> Local SQLite database file (default: $WOD_WIKI_DB
3582
+ or ./wodwiki.db). Passing --db always selects SQLite,
3583
+ even when the environment names a remote backend.
3584
+ Created on demand by parse/run/seed.
3585
+ --db-driver <name> Backend driver: sqlite (default), postgres, or
3586
+ turso ($DB_DRIVER)
3587
+ --database-url <url> Database connection URL ($DATABASE_URL); accepts
3588
+ PostgreSQL, local SQLite, and remote libSQL URLs.
3589
+ --postgres-host <host> PostgreSQL host ($POSTGRES_HOST)
3590
+ --postgres-port <port> PostgreSQL port ($POSTGRES_PORT)
3591
+ --postgres-user <user> PostgreSQL user ($POSTGRES_USER)
3592
+ --postgres-password <pw> PostgreSQL password ($POSTGRES_PASSWORD)
3593
+ --postgres-database <db> PostgreSQL database name ($POSTGRES_DATABASE)
3594
+ --postgres-ssl <mode> PostgreSQL SSL mode, e.g. disable/prefer/require
3595
+ ($POSTGRES_SSL)
3596
+ --turso-url <url> Turso/libSQL database URL (libsql://\u2026). The
3597
+ hosted database must already exist (provisioned on
3598
+ Turso); only its schema is installed.
3599
+ --turso-auth-token <token> Turso authentication token (never printed)
3600
+ --env-file <path> Load variables from this file; a missing file is
3601
+ an error. Without it: ./.env when present, otherwise
3602
+ the workspace-root .env. Credentials are never logged.
3603
+
3604
+ HOSTING (for 'server' / 'playground'; requires the Bun runtime):
3605
+ server HTTP API over the configured database plus the frontend
3606
+ packaged inside the engine module. Requires an
3607
+ initialized database. ('serve' is an accepted alias.)
3608
+ playground Serves the playground app backed by browser IndexedDB;
3609
+ the configured database is not touched.
3610
+ --host <host> Bind address (default 127.0.0.1, $HOST)
3611
+ --port <port> TCP port (default 3000, $PORT)
3612
+ --dist <dir> Serve this prebuilt frontend directory instead of the
3613
+ packaged one ($PLAYGROUND_DIST); must already contain
3614
+ index.html.
3097
3615
 
3098
3616
  SEED SOURCING (for 'seed'):
3099
3617
  --source <dir> Extra compiled seed directory (manifest.json + chunks);
@@ -3113,6 +3631,15 @@ OPTIONS:
3113
3631
  -h, --help Show help message
3114
3632
  -v, --version Show version number
3115
3633
 
3634
+ ENVIRONMENT (flags > process env > .env):
3635
+ WOD_WIKI_DB, SQLITE_PATH SQLite file path
3636
+ DB_DRIVER sqlite | postgres | turso
3637
+ DATABASE_URL PostgreSQL connection URL
3638
+ POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER,
3639
+ POSTGRES_PASSWORD, POSTGRES_DATABASE, POSTGRES_SSL
3640
+ TURSO_DATABASE_URL, TURSO_AUTH_TOKEN, LIBSQL_AUTH_TOKEN
3641
+ HOST, PORT, PLAYGROUND_DIST server / playground defaults
3642
+
3116
3643
  EXIT CODES:
3117
3644
  0 - OK / Success
3118
3645
  1 - General / Runtime error
@@ -3132,6 +3659,10 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3132
3659
  const writeOut = io.stdout ?? ((t) => process.stdout.write(t + "\n"));
3133
3660
  const writeErr = io.stderr ?? ((t) => process.stderr.write(t + "\n"));
3134
3661
  const getStdin = io.readStdinFn ?? readStdin;
3662
+ let envLoaded;
3663
+ const cliEnv = () => envLoaded ??= { ...loadCliEnvironment(parsed.envFile), ...process.env };
3664
+ let dbResolved;
3665
+ const cliDb = () => dbResolved ??= resolveDatabaseConfig(parsed, cliEnv());
3135
3666
  let parsed;
3136
3667
  try {
3137
3668
  parsed = parseCliArgs(argv);
@@ -3198,7 +3729,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3198
3729
  const ir = runParse(source, { sport: parsed.sport, sourceLabel: `cli:wod parse ${parsed.target}` });
3199
3730
  const identity = extractNoteIdentity(source);
3200
3731
  const tags = extractTypedTags(source);
3201
- const db = await openCliDb(parsed.db);
3732
+ const db = await openCliDb(cliDb());
3202
3733
  try {
3203
3734
  const persisted = await persistNote(db, {
3204
3735
  source: noteSource,
@@ -3237,7 +3768,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3237
3768
  source = fs.readFileSync(parsed.target, "utf-8");
3238
3769
  }
3239
3770
  const identity = extractNoteIdentity(source);
3240
- const db = await openCliDb(parsed.db);
3771
+ const db = await openCliDb(cliDb());
3241
3772
  let ir;
3242
3773
  try {
3243
3774
  const started = await startRun(db, {
@@ -3288,14 +3819,14 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3288
3819
  }
3289
3820
  let ir;
3290
3821
  if (useDb) {
3291
- const dbPath = resolveDbPath(parsed.db);
3292
- if (!nodeFs().existsSync(dbPath)) {
3822
+ const config = cliDb();
3823
+ if (config.kind === "sqlite" && !nodeFs().existsSync(config.path)) {
3293
3824
  writeErr(
3294
- `[wod query] No input dataset and no database at ${dbPath}. Specify --corpus <file>, --stdin-log, --stdin-facts, or run \`wod seed\` first.`
3825
+ `[wod query] No input dataset and no database at ${config.path}. Specify --corpus <file>, --stdin-log, --stdin-facts, or run \`wod seed\` first.`
3295
3826
  );
3296
3827
  return 1;
3297
3828
  }
3298
- const db = await openCliDb(parsed.db);
3829
+ const db = await openCliDb(config, config.kind === "sqlite" ? void 0 : { initialize: false });
3299
3830
  try {
3300
3831
  ir = await runQueryCli(parsed.target, {
3301
3832
  preferredUnit: parsed.unit,
@@ -3341,7 +3872,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3341
3872
  return 1;
3342
3873
  }
3343
3874
  const targetId = id;
3344
- const db = await openCliDb(parsed.db);
3875
+ const db = await openCliDb(cliDb());
3345
3876
  try {
3346
3877
  if (kind === "note") {
3347
3878
  const bundle = await getNoteBundle(db.db, targetId);
@@ -3418,7 +3949,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3418
3949
  const source = fs.readFileSync(file, "utf-8");
3419
3950
  const identity = extractNoteIdentity(source);
3420
3951
  const tags = extractTypedTags(source);
3421
- const db2 = await openCliDb(parsed.db);
3952
+ const db2 = await openCliDb(cliDb());
3422
3953
  try {
3423
3954
  const persisted = await persistNote(db2, {
3424
3955
  source,
@@ -3441,7 +3972,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3441
3972
  writeErr(`[wod import] Unparseable effort file (need frontmatter id/slug/label + numeric baseAttributes.met): ${file}`);
3442
3973
  return 1;
3443
3974
  }
3444
- const db2 = await openCliDb(parsed.db);
3975
+ const db2 = await openCliDb(cliDb());
3445
3976
  try {
3446
3977
  await persistEffort(db2, { ...effort, registrySource: "user" });
3447
3978
  writeOut(`[wod import] effort ${effort.slug} \u2014 ${effort.label} \u2192 ${db2.path}`);
@@ -3456,7 +3987,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3456
3987
  writeErr(`[wod import] No equipment labels found in ${file}`);
3457
3988
  return 1;
3458
3989
  }
3459
- const db2 = await openCliDb(parsed.db);
3990
+ const db2 = await openCliDb(cliDb());
3460
3991
  try {
3461
3992
  const added = await persistTypedTags(db2, labels, "equipment");
3462
3993
  writeOut(`[wod import] equipment \u2014 ${labels.length} labels (${added} new) \u2192 ${db2.path}`);
@@ -3466,7 +3997,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3466
3997
  return 0;
3467
3998
  }
3468
3999
  const { session, events } = csvToSessionRows(fs.readFileSync(file, "utf-8"));
3469
- const db = await openCliDb(parsed.db);
4000
+ const db = await openCliDb(cliDb());
3470
4001
  try {
3471
4002
  const result = await persistSessionBundle(db, session, events);
3472
4003
  writeOut(
@@ -3513,7 +4044,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3513
4044
  );
3514
4045
  return 1;
3515
4046
  }
3516
- const db = await openCliDb(parsed.db);
4047
+ const db = await openCliDb(cliDb());
3517
4048
  try {
3518
4049
  for (const dir of dirs) {
3519
4050
  const summary = await importSeedDir(db, dir, "");
@@ -3527,7 +4058,14 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3527
4058
  return 0;
3528
4059
  }
3529
4060
  if (parsed.subcommand === "reset") {
3530
- const target = resolveDbPath(parsed.db);
4061
+ const config = cliDb();
4062
+ if (config.kind !== "sqlite") {
4063
+ writeErr(
4064
+ `[wod reset] refusing to reset the configured ${config.kind} database \u2014 reset only removes a local SQLite file. Clear remote databases through their provider.`
4065
+ );
4066
+ return 1;
4067
+ }
4068
+ const target = config.path;
3531
4069
  if (nodeFs().existsSync(target)) {
3532
4070
  nodeFs().unlinkSync(target);
3533
4071
  writeOut(`[wod reset] removed ${target}`);
@@ -3536,6 +4074,32 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3536
4074
  }
3537
4075
  return 0;
3538
4076
  }
4077
+ if (parsed.subcommand === "init") {
4078
+ const db = await openCliDb(cliDb(), { create: true });
4079
+ try {
4080
+ writeOut(`[wod init] database ready at ${db.path}`);
4081
+ } finally {
4082
+ await db.close();
4083
+ }
4084
+ return 0;
4085
+ }
4086
+ if (parsed.subcommand === "server" || parsed.subcommand === "serve" || parsed.subcommand === "playground") {
4087
+ if (!("Bun" in globalThis)) {
4088
+ writeErr(
4089
+ `[wod ${parsed.subcommand}] Error: hosting requires the Bun runtime (https://bun.sh) \u2014 run \`bun wod server\` / \`bun wod playground\`.`
4090
+ );
4091
+ return 1;
4092
+ }
4093
+ const mode = parsed.subcommand === "playground" ? "playground" : "server";
4094
+ const { runCliServer: runCliServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
4095
+ const options = { env: cliEnv() };
4096
+ if (mode === "server") options.database = cliDb();
4097
+ if (parsed.host !== void 0) options.host = parsed.host;
4098
+ if (parsed.port !== void 0) options.port = parsed.port;
4099
+ if (parsed.dist !== void 0) options.dist = parsed.dist;
4100
+ await runCliServer2(mode, options, writeOut);
4101
+ return 0;
4102
+ }
3539
4103
  writeErr(`[wod] Unknown subcommand "${parsed.subcommand}"`);
3540
4104
  return 1;
3541
4105
  } catch (error) {