@korajs/cli 0.1.0

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 (38) hide show
  1. package/dist/bin.cjs +2170 -0
  2. package/dist/bin.cjs.map +1 -0
  3. package/dist/bin.js +1640 -0
  4. package/dist/bin.js.map +1 -0
  5. package/dist/chunk-N36PFOSA.js +103 -0
  6. package/dist/chunk-N36PFOSA.js.map +1 -0
  7. package/dist/chunk-REOTYAM6.js +370 -0
  8. package/dist/chunk-REOTYAM6.js.map +1 -0
  9. package/dist/chunk-ZVB4HAB3.js +88 -0
  10. package/dist/chunk-ZVB4HAB3.js.map +1 -0
  11. package/dist/create.cjs +313 -0
  12. package/dist/create.cjs.map +1 -0
  13. package/dist/create.js +10 -0
  14. package/dist/create.js.map +1 -0
  15. package/dist/index.cjs +218 -0
  16. package/dist/index.cjs.map +1 -0
  17. package/dist/index.d.cts +72 -0
  18. package/dist/index.d.ts +72 -0
  19. package/dist/index.js +26 -0
  20. package/dist/index.js.map +1 -0
  21. package/package.json +48 -0
  22. package/templates/react-basic/index.html.hbs +12 -0
  23. package/templates/react-basic/kora.config.ts +13 -0
  24. package/templates/react-basic/package.json.hbs +26 -0
  25. package/templates/react-basic/src/App.tsx +48 -0
  26. package/templates/react-basic/src/main.tsx +16 -0
  27. package/templates/react-basic/src/schema.ts +15 -0
  28. package/templates/react-basic/tsconfig.json +14 -0
  29. package/templates/react-basic/vite.config.ts +6 -0
  30. package/templates/react-sync/index.html.hbs +12 -0
  31. package/templates/react-sync/kora.config.ts +17 -0
  32. package/templates/react-sync/package.json.hbs +28 -0
  33. package/templates/react-sync/server.ts +11 -0
  34. package/templates/react-sync/src/App.tsx +50 -0
  35. package/templates/react-sync/src/main.tsx +24 -0
  36. package/templates/react-sync/src/schema.ts +15 -0
  37. package/templates/react-sync/tsconfig.json +14 -0
  38. package/templates/react-sync/vite.config.ts +6 -0
package/dist/bin.js ADDED
@@ -0,0 +1,1640 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ createCommand,
4
+ createLogger,
5
+ findProjectRoot,
6
+ findSchemaFile,
7
+ promptConfirm,
8
+ resolveProjectBinary
9
+ } from "./chunk-REOTYAM6.js";
10
+ import {
11
+ generateTypes
12
+ } from "./chunk-N36PFOSA.js";
13
+ import {
14
+ DevServerError,
15
+ InvalidProjectError,
16
+ SchemaNotFoundError
17
+ } from "./chunk-ZVB4HAB3.js";
18
+
19
+ // src/bin.ts
20
+ import { defineCommand as defineCommand4, runMain } from "citty";
21
+
22
+ // src/commands/dev/dev-command.ts
23
+ import { access as access2 } from "fs/promises";
24
+ import { join as join2 } from "path";
25
+ import { resolve } from "path";
26
+ import { defineCommand } from "citty";
27
+
28
+ // src/commands/dev/kora-config.ts
29
+ import { spawn } from "child_process";
30
+ import { access } from "fs/promises";
31
+ import { extname, join } from "path";
32
+ import { pathToFileURL } from "url";
33
+ var CONFIG_CANDIDATES = [
34
+ "kora.config.ts",
35
+ "kora.config.mts",
36
+ "kora.config.cts",
37
+ "kora.config.js",
38
+ "kora.config.mjs",
39
+ "kora.config.cjs"
40
+ ];
41
+ async function loadKoraConfig(projectRoot) {
42
+ const configPath = await findKoraConfigFile(projectRoot);
43
+ if (!configPath) return null;
44
+ const ext = extname(configPath);
45
+ if (ext === ".ts" || ext === ".mts" || ext === ".cts") {
46
+ const loaded2 = await loadTypeScriptConfig(configPath, projectRoot);
47
+ return toConfigObject(loaded2);
48
+ }
49
+ const loaded = await import(pathToFileURL(configPath).href);
50
+ return toConfigObject(loaded);
51
+ }
52
+ async function findKoraConfigFile(projectRoot) {
53
+ for (const file of CONFIG_CANDIDATES) {
54
+ const candidate = join(projectRoot, file);
55
+ try {
56
+ await access(candidate);
57
+ return candidate;
58
+ } catch {
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+ async function loadTypeScriptConfig(configPath, projectRoot) {
64
+ const tsxBinary = await resolveProjectBinary(projectRoot, "tsx");
65
+ if (!tsxBinary) {
66
+ throw new Error(
67
+ `Found TypeScript config at ${configPath}, but "tsx" is not installed in this project. Install tsx or use kora.config.js.`
68
+ );
69
+ }
70
+ const script = [
71
+ "import { pathToFileURL } from 'node:url'",
72
+ "const configPath = process.argv[process.argv.length - 1]",
73
+ "const mod = await import(pathToFileURL(configPath).href)",
74
+ "const value = mod.default ?? mod",
75
+ "process.stdout.write(JSON.stringify(value))"
76
+ ].join(";");
77
+ const output = await runCommand(tsxBinary, ["--eval", script, configPath], projectRoot);
78
+ try {
79
+ return JSON.parse(output);
80
+ } catch {
81
+ throw new Error(`Failed to parse ${configPath} output as JSON.`);
82
+ }
83
+ }
84
+ async function runCommand(command, args, cwd) {
85
+ return await new Promise((resolve4, reject) => {
86
+ const child = spawn(command, args, {
87
+ cwd,
88
+ stdio: ["ignore", "pipe", "pipe"],
89
+ env: process.env
90
+ });
91
+ let stdout = "";
92
+ let stderr = "";
93
+ child.stdout?.on("data", (chunk) => {
94
+ stdout += chunk.toString("utf-8");
95
+ });
96
+ child.stderr?.on("data", (chunk) => {
97
+ stderr += chunk.toString("utf-8");
98
+ });
99
+ child.on("error", (error) => {
100
+ reject(error);
101
+ });
102
+ child.on("exit", (code) => {
103
+ if (code === 0) {
104
+ resolve4(stdout.trim());
105
+ return;
106
+ }
107
+ reject(new Error(`Failed to load kora config (exit ${code ?? "unknown"}): ${stderr.trim()}`));
108
+ });
109
+ });
110
+ }
111
+ function toConfigObject(mod) {
112
+ if (typeof mod !== "object" || mod === null) {
113
+ throw new Error("kora config must export an object.");
114
+ }
115
+ const value = mod.default ?? mod;
116
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
117
+ throw new Error("kora config must export an object.");
118
+ }
119
+ return value;
120
+ }
121
+
122
+ // src/commands/dev/process-manager.ts
123
+ import { spawn as spawn2 } from "child_process";
124
+ var ProcessManager = class {
125
+ processes = /* @__PURE__ */ new Map();
126
+ spawn(config) {
127
+ const options = {
128
+ cwd: config.cwd,
129
+ env: { ...process.env, ...config.env },
130
+ stdio: ["ignore", "pipe", "pipe"]
131
+ };
132
+ const child = spawn2(config.command, config.args, options);
133
+ let resolveExit;
134
+ const runningProcess = {
135
+ child,
136
+ exitPromise: new Promise((resolve4) => {
137
+ resolveExit = resolve4;
138
+ }),
139
+ stdoutBuffer: "",
140
+ stderrBuffer: ""
141
+ };
142
+ this.processes.set(config.label, runningProcess);
143
+ child.stdout?.on("data", (chunk) => {
144
+ runningProcess.stdoutBuffer = this.writeChunk(
145
+ config.label,
146
+ runningProcess.stdoutBuffer,
147
+ chunk,
148
+ false
149
+ );
150
+ });
151
+ child.stderr?.on("data", (chunk) => {
152
+ runningProcess.stderrBuffer = this.writeChunk(
153
+ config.label,
154
+ runningProcess.stderrBuffer,
155
+ chunk,
156
+ true
157
+ );
158
+ });
159
+ child.on("exit", (code, signal) => {
160
+ this.flushBuffer(config.label, runningProcess.stdoutBuffer, false);
161
+ this.flushBuffer(config.label, runningProcess.stderrBuffer, true);
162
+ this.processes.delete(config.label);
163
+ config.onExit?.(code, signal);
164
+ resolveExit?.();
165
+ });
166
+ }
167
+ hasRunning() {
168
+ return this.processes.size > 0;
169
+ }
170
+ async shutdownAll() {
171
+ const running = Array.from(this.processes.values());
172
+ if (running.length === 0) return;
173
+ for (const processEntry of running) {
174
+ processEntry.child.kill("SIGTERM");
175
+ }
176
+ await Promise.race([Promise.all(running.map((entry) => entry.exitPromise)), delay(5e3)]);
177
+ const remaining = Array.from(this.processes.values());
178
+ if (remaining.length === 0) return;
179
+ for (const processEntry of remaining) {
180
+ processEntry.child.kill("SIGKILL");
181
+ }
182
+ await Promise.all(remaining.map((entry) => entry.exitPromise));
183
+ }
184
+ writeChunk(label, buffer, chunk, isError) {
185
+ const combined = `${buffer}${chunk.toString("utf-8")}`;
186
+ const lines = combined.split(/\r?\n/);
187
+ const remaining = lines.pop() ?? "";
188
+ for (const line of lines) {
189
+ this.writeLine(label, line, isError);
190
+ }
191
+ return remaining;
192
+ }
193
+ flushBuffer(label, buffer, isError) {
194
+ if (!buffer) return;
195
+ this.writeLine(label, buffer, isError);
196
+ }
197
+ writeLine(label, line, isError) {
198
+ const stream = isError ? process.stderr : process.stdout;
199
+ stream.write(`[${label}] ${line}
200
+ `);
201
+ }
202
+ };
203
+ function delay(ms) {
204
+ return new Promise((resolve4) => {
205
+ setTimeout(resolve4, ms);
206
+ });
207
+ }
208
+
209
+ // src/commands/dev/schema-watcher.ts
210
+ import { spawn as spawn3 } from "child_process";
211
+ import { watch } from "fs";
212
+ var SchemaWatcher = class {
213
+ constructor(config) {
214
+ this.config = config;
215
+ this.debounceMs = config.debounceMs ?? 300;
216
+ }
217
+ config;
218
+ debounceMs;
219
+ watcher = null;
220
+ debounceTimer = null;
221
+ start() {
222
+ if (this.watcher) return;
223
+ this.watcher = watch(this.config.schemaPath, () => {
224
+ this.scheduleRegeneration();
225
+ });
226
+ this.watcher.on("error", (error) => {
227
+ this.config.onError?.(toError(error));
228
+ });
229
+ }
230
+ stop() {
231
+ if (this.debounceTimer) {
232
+ clearTimeout(this.debounceTimer);
233
+ this.debounceTimer = null;
234
+ }
235
+ this.watcher?.close();
236
+ this.watcher = null;
237
+ }
238
+ async regenerate() {
239
+ const koraBinary = await resolveProjectBinary(this.config.projectRoot, "kora");
240
+ if (!koraBinary) {
241
+ throw new Error('Could not find project binary "kora" in node_modules/.bin.');
242
+ }
243
+ const tsxBinary = await resolveProjectBinary(this.config.projectRoot, "tsx");
244
+ if (!tsxBinary) {
245
+ process.stderr.write('[kora] Could not find "tsx" binary. Falling back to node.\n');
246
+ }
247
+ const command = tsxBinary ?? process.execPath;
248
+ const args = [koraBinary, "generate", "types", "--schema", this.config.schemaPath];
249
+ await spawnCommand(command, args, this.config.projectRoot);
250
+ this.config.onRegenerate?.();
251
+ }
252
+ scheduleRegeneration() {
253
+ if (this.debounceTimer) {
254
+ clearTimeout(this.debounceTimer);
255
+ }
256
+ this.debounceTimer = setTimeout(() => {
257
+ this.debounceTimer = null;
258
+ void this.regenerate().catch((error) => {
259
+ this.config.onError?.(toError(error));
260
+ });
261
+ }, this.debounceMs);
262
+ }
263
+ };
264
+ async function spawnCommand(command, args, cwd) {
265
+ await new Promise((resolve4, reject) => {
266
+ const child = spawn3(command, args, {
267
+ cwd,
268
+ stdio: ["ignore", "pipe", "pipe"],
269
+ env: process.env
270
+ });
271
+ child.stdout?.on("data", (chunk) => {
272
+ writePrefixedLines(chunk, false);
273
+ });
274
+ child.stderr?.on("data", (chunk) => {
275
+ writePrefixedLines(chunk, true);
276
+ });
277
+ child.on("error", (error) => {
278
+ reject(error);
279
+ });
280
+ child.on("exit", (code) => {
281
+ if (code === 0) {
282
+ resolve4();
283
+ return;
284
+ }
285
+ reject(new Error(`Type generation exited with code ${code ?? "unknown"}.`));
286
+ });
287
+ });
288
+ }
289
+ function writePrefixedLines(chunk, isError) {
290
+ const text = chunk.toString("utf-8");
291
+ const lines = text.split(/\r?\n/).filter((line) => line.length > 0);
292
+ const stream = isError ? process.stderr : process.stdout;
293
+ for (const line of lines) {
294
+ stream.write(`[kora] ${line}
295
+ `);
296
+ }
297
+ }
298
+ function toError(error) {
299
+ if (error instanceof Error) return error;
300
+ return new Error(String(error));
301
+ }
302
+
303
+ // src/commands/dev/dev-command.ts
304
+ var devCommand = defineCommand({
305
+ meta: {
306
+ name: "dev",
307
+ description: "Start the Kora development environment"
308
+ },
309
+ args: {
310
+ port: {
311
+ type: "string",
312
+ description: "Vite dev server port"
313
+ },
314
+ "sync-port": {
315
+ type: "string",
316
+ description: "Kora sync server port"
317
+ },
318
+ "no-sync": {
319
+ type: "boolean",
320
+ description: "Disable sync server startup",
321
+ default: false
322
+ },
323
+ "no-watch": {
324
+ type: "boolean",
325
+ description: "Disable schema file watching",
326
+ default: false
327
+ }
328
+ },
329
+ async run({ args }) {
330
+ const logger = createLogger();
331
+ const projectRoot = await findProjectRoot();
332
+ if (!projectRoot) {
333
+ throw new InvalidProjectError(process.cwd());
334
+ }
335
+ const config = await loadKoraConfig(projectRoot);
336
+ const vitePort = typeof args.port === "string" ? args.port : String(config?.dev?.port ?? 5173);
337
+ const syncPortFromConfig = typeof config?.dev?.sync === "object" && typeof config.dev.sync.port === "number" ? config.dev.sync.port : 3001;
338
+ const syncPort = typeof args["sync-port"] === "string" ? args["sync-port"] : String(syncPortFromConfig);
339
+ const configSyncEnabled = config?.dev?.sync === void 0 || config.dev.sync === true || typeof config.dev.sync === "object" && config.dev.sync.enabled !== false;
340
+ const configWatchEnabled = config?.dev?.watch === void 0 || config.dev.watch === true || typeof config.dev.watch === "object" && config.dev.watch.enabled !== false;
341
+ const watchDebounceMs = typeof config?.dev?.watch === "object" && typeof config.dev.watch.debounceMs === "number" ? config.dev.watch.debounceMs : 300;
342
+ const viteBinary = await resolveProjectBinary(projectRoot, "vite");
343
+ if (!viteBinary) {
344
+ throw new DevServerError("vite", join2(projectRoot, "node_modules", ".bin", "vite"));
345
+ }
346
+ const syncServerFile = await findSyncServerFile(projectRoot);
347
+ let managedSyncStore = normalizeManagedSyncStore(config, projectRoot);
348
+ const postgresEnvRequested = isPostgresEnvRequested(config);
349
+ const syncAllowed = args["no-sync"] !== true && configSyncEnabled;
350
+ let shouldStartSync = syncAllowed && (syncServerFile !== null || managedSyncStore !== null);
351
+ let syncBinary = null;
352
+ if (shouldStartSync && syncServerFile !== null) {
353
+ syncBinary = await resolveProjectBinary(projectRoot, "tsx");
354
+ if (!syncBinary) {
355
+ logger.warn('Sync server detected, but local "tsx" binary was not found. Skipping sync.');
356
+ }
357
+ }
358
+ if (shouldStartSync && syncServerFile === null && managedSyncStore) {
359
+ const hasServerPackage = await fileExists(
360
+ join2(projectRoot, "node_modules", "@kora", "server", "package.json")
361
+ );
362
+ if (!hasServerPackage) {
363
+ logger.warn(
364
+ "Managed sync is configured, but @korajs/server is not installed. Install it or add server.ts."
365
+ );
366
+ managedSyncStore = null;
367
+ shouldStartSync = syncAllowed && (syncServerFile !== null || managedSyncStore !== null);
368
+ }
369
+ }
370
+ if (syncAllowed && syncServerFile === null && managedSyncStore === null && postgresEnvRequested) {
371
+ logger.warn(
372
+ "Managed postgres sync requested but no connection string found. Set dev.sync.store.connectionString or DATABASE_URL."
373
+ );
374
+ }
375
+ let configuredSchemaPath = null;
376
+ if (typeof config?.schema === "string") {
377
+ const candidate = resolve(projectRoot, config.schema);
378
+ if (await fileExists(candidate)) {
379
+ configuredSchemaPath = candidate;
380
+ } else {
381
+ logger.warn(`Configured schema file not found: ${candidate}. Falling back to auto-detection.`);
382
+ }
383
+ }
384
+ const schemaPath = configuredSchemaPath ?? await findSchemaFile(projectRoot);
385
+ const watchEnabled = args["no-watch"] !== true && configWatchEnabled && schemaPath !== null;
386
+ const processManager = new ProcessManager();
387
+ let schemaWatcher = null;
388
+ let shuttingDown = false;
389
+ let resolveFinished;
390
+ const finished = new Promise((resolve4) => {
391
+ resolveFinished = resolve4;
392
+ });
393
+ const onManagedProcessExit = () => {
394
+ if (!processManager.hasRunning() && !shuttingDown) {
395
+ resolveFinished?.();
396
+ }
397
+ };
398
+ const shutdown = async () => {
399
+ if (shuttingDown) return;
400
+ shuttingDown = true;
401
+ schemaWatcher?.stop();
402
+ await processManager.shutdownAll();
403
+ resolveFinished?.();
404
+ };
405
+ const onSigInt = () => {
406
+ void shutdown();
407
+ };
408
+ const onSigTerm = () => {
409
+ void shutdown();
410
+ };
411
+ process.on("SIGINT", onSigInt);
412
+ process.on("SIGTERM", onSigTerm);
413
+ logger.banner();
414
+ logger.info("Starting development environment:");
415
+ logger.blank();
416
+ logger.step(` Vite dev server on port ${vitePort}`);
417
+ if (shouldStartSync && syncBinary && syncServerFile) {
418
+ logger.step(` Sync server on port ${syncPort}`);
419
+ } else if (shouldStartSync && syncServerFile === null && managedSyncStore !== null) {
420
+ logger.step(` Managed sync server on port ${syncPort} (${managedSyncStore.type})`);
421
+ } else if (syncAllowed && syncServerFile === null) {
422
+ logger.step(" Sync server configured but no server.ts/server.js or managed store found");
423
+ } else if (!syncAllowed) {
424
+ logger.step(" Sync server disabled via --no-sync");
425
+ }
426
+ if (watchEnabled && schemaPath) {
427
+ logger.step(` Schema watcher enabled (${schemaPath})`);
428
+ } else if (args["no-watch"] === true) {
429
+ logger.step(" Schema watcher disabled via --no-watch");
430
+ } else {
431
+ logger.step(" Schema watcher disabled (schema.ts not found)");
432
+ }
433
+ logger.blank();
434
+ processManager.spawn({
435
+ label: "vite",
436
+ command: viteBinary,
437
+ args: ["--port", String(vitePort)],
438
+ cwd: projectRoot,
439
+ onExit: onManagedProcessExit
440
+ });
441
+ if (shouldStartSync && syncBinary && syncServerFile) {
442
+ processManager.spawn({
443
+ label: "sync",
444
+ command: syncBinary,
445
+ args: [syncServerFile],
446
+ cwd: projectRoot,
447
+ env: {
448
+ PORT: String(syncPort),
449
+ KORA_SYNC_PORT: String(syncPort)
450
+ },
451
+ onExit: onManagedProcessExit
452
+ });
453
+ }
454
+ if (shouldStartSync && syncServerFile === null && managedSyncStore !== null) {
455
+ processManager.spawn({
456
+ label: "sync",
457
+ command: process.execPath,
458
+ args: ["--input-type=module", "--eval", MANAGED_SYNC_BOOTSTRAP_SCRIPT],
459
+ cwd: projectRoot,
460
+ env: {
461
+ KORA_DEV_SYNC_CONFIG: JSON.stringify({
462
+ port: Number(syncPort),
463
+ store: managedSyncStore
464
+ })
465
+ },
466
+ onExit: onManagedProcessExit
467
+ });
468
+ }
469
+ if (watchEnabled && schemaPath) {
470
+ schemaWatcher = new SchemaWatcher({
471
+ schemaPath,
472
+ projectRoot,
473
+ debounceMs: watchDebounceMs,
474
+ onRegenerate: () => {
475
+ logger.success("Regenerated types from schema changes");
476
+ },
477
+ onError: (error) => {
478
+ logger.error(`Schema watcher error: ${error.message}`);
479
+ }
480
+ });
481
+ schemaWatcher.start();
482
+ }
483
+ await finished;
484
+ process.off("SIGINT", onSigInt);
485
+ process.off("SIGTERM", onSigTerm);
486
+ }
487
+ });
488
+ async function findSyncServerFile(projectRoot) {
489
+ const candidates = [join2(projectRoot, "server.ts"), join2(projectRoot, "server.js")];
490
+ for (const candidate of candidates) {
491
+ try {
492
+ await access2(candidate);
493
+ return candidate;
494
+ } catch {
495
+ }
496
+ }
497
+ return null;
498
+ }
499
+ async function fileExists(path) {
500
+ try {
501
+ await access2(path);
502
+ return true;
503
+ } catch {
504
+ return false;
505
+ }
506
+ }
507
+ function normalizeManagedSyncStore(config, projectRoot) {
508
+ const sync = config?.dev?.sync;
509
+ if (typeof sync !== "object" || sync === null) return null;
510
+ const store = sync.store;
511
+ if (store === void 0) return { type: "memory" };
512
+ if (store === "memory") return { type: "memory" };
513
+ if (store === "sqlite") return { type: "sqlite", filename: join2(projectRoot, "kora-sync.db") };
514
+ if (store === "postgres") {
515
+ const connectionString = process.env.DATABASE_URL;
516
+ if (!connectionString) return null;
517
+ return { type: "postgres", connectionString };
518
+ }
519
+ if (typeof store === "object" && store !== null) {
520
+ if (store.type === "memory") return { type: "memory" };
521
+ if (store.type === "sqlite") {
522
+ const filename = typeof store.filename === "string" && store.filename.length > 0 ? resolve(projectRoot, store.filename) : join2(projectRoot, "kora-sync.db");
523
+ return { type: "sqlite", filename };
524
+ }
525
+ if (store.type === "postgres" && typeof store.connectionString === "string") {
526
+ return { type: "postgres", connectionString: store.connectionString };
527
+ }
528
+ }
529
+ return null;
530
+ }
531
+ function isPostgresEnvRequested(config) {
532
+ const sync = config?.dev?.sync;
533
+ if (typeof sync !== "object" || sync === null) return false;
534
+ if (sync.store === "postgres") return true;
535
+ if (typeof sync.store === "object" && sync.store !== null && sync.store.type === "postgres") return true;
536
+ return false;
537
+ }
538
+ var MANAGED_SYNC_BOOTSTRAP_SCRIPT = `
539
+ const config = JSON.parse(process.env.KORA_DEV_SYNC_CONFIG ?? '{}');
540
+ const {
541
+ createKoraServer,
542
+ MemoryServerStore,
543
+ createSqliteServerStore,
544
+ createPostgresServerStore,
545
+ } = await import('@korajs/server');
546
+ const storeConfig = config.store ?? { type: 'memory' };
547
+ let store;
548
+ if (storeConfig.type === 'memory') {
549
+ store = new MemoryServerStore();
550
+ } else if (storeConfig.type === 'sqlite') {
551
+ const filename = typeof storeConfig.filename === 'string' && storeConfig.filename.length > 0
552
+ ? storeConfig.filename
553
+ : './kora-sync.db';
554
+ store = createSqliteServerStore({ filename });
555
+ } else if (storeConfig.type === 'postgres') {
556
+ if (typeof storeConfig.connectionString !== 'string' || storeConfig.connectionString.length === 0) {
557
+ throw new Error('Managed postgres sync requires a connectionString');
558
+ }
559
+ store = await createPostgresServerStore({
560
+ connectionString: storeConfig.connectionString,
561
+ });
562
+ } else {
563
+ throw new Error('Unsupported managed sync store type: ' + String(storeConfig.type));
564
+ }
565
+ const server = createKoraServer({ store, port: Number(config.port ?? 3001) });
566
+ const shutdown = async () => {
567
+ try {
568
+ await server.stop();
569
+ } catch {
570
+ }
571
+ process.exit(0);
572
+ };
573
+ process.on('SIGINT', () => {
574
+ void shutdown();
575
+ });
576
+ process.on('SIGTERM', () => {
577
+ void shutdown();
578
+ });
579
+ await server.start();
580
+ process.stdout.write('Managed sync server running on ws://localhost:' + String(config.port ?? 3001) + '\\n');
581
+ await new Promise(() => {});
582
+ `;
583
+
584
+ // src/commands/generate/generate-command.ts
585
+ import { mkdir, writeFile } from "fs/promises";
586
+ import { dirname, resolve as resolve2 } from "path";
587
+ import { defineCommand as defineCommand2 } from "citty";
588
+ var generateCommand = defineCommand2({
589
+ meta: {
590
+ name: "generate",
591
+ description: "Generate code from your Kora schema"
592
+ },
593
+ subCommands: {
594
+ types: defineCommand2({
595
+ meta: {
596
+ name: "types",
597
+ description: "Generate TypeScript types from your schema"
598
+ },
599
+ args: {
600
+ schema: {
601
+ type: "string",
602
+ description: "Path to schema file"
603
+ },
604
+ output: {
605
+ type: "string",
606
+ description: "Output file path",
607
+ default: "kora/generated/types.ts"
608
+ }
609
+ },
610
+ async run({ args }) {
611
+ const logger = createLogger();
612
+ const projectRoot = await findProjectRoot();
613
+ if (!projectRoot) {
614
+ throw new InvalidProjectError(process.cwd());
615
+ }
616
+ let schemaPath;
617
+ if (args.schema && typeof args.schema === "string") {
618
+ schemaPath = resolve2(args.schema);
619
+ } else {
620
+ const found = await findSchemaFile(projectRoot);
621
+ if (!found) {
622
+ throw new SchemaNotFoundError([
623
+ "src/schema.ts",
624
+ "schema.ts",
625
+ "src/schema.js",
626
+ "schema.js"
627
+ ]);
628
+ }
629
+ schemaPath = found;
630
+ }
631
+ logger.step(`Reading schema from ${schemaPath}...`);
632
+ const schemaModule = await import(schemaPath);
633
+ const schema = extractSchema(schemaModule);
634
+ if (!schema) {
635
+ logger.error("Schema file must export a SchemaDefinition as the default export.");
636
+ process.exitCode = 1;
637
+ return;
638
+ }
639
+ const output = generateTypes(schema);
640
+ const outputFile = typeof args.output === "string" ? args.output : "kora/generated/types.ts";
641
+ const outputPath = resolve2(projectRoot, outputFile);
642
+ await mkdir(dirname(outputPath), { recursive: true });
643
+ await writeFile(outputPath, output, "utf-8");
644
+ logger.success(`Generated types at ${outputPath}`);
645
+ }
646
+ })
647
+ }
648
+ });
649
+ function extractSchema(mod) {
650
+ if (typeof mod !== "object" || mod === null) return null;
651
+ const record = mod;
652
+ const candidate = record.default ?? record;
653
+ if (isSchemaDefinition(candidate)) return candidate;
654
+ return null;
655
+ }
656
+ function isSchemaDefinition(value) {
657
+ if (typeof value !== "object" || value === null) return false;
658
+ const obj = value;
659
+ return typeof obj.version === "number" && typeof obj.collections === "object" && obj.collections !== null;
660
+ }
661
+
662
+ // src/commands/migrate/migrate-command.ts
663
+ import { mkdir as mkdir2, readFile, readdir, writeFile as writeFile2 } from "fs/promises";
664
+ import { dirname as dirname2, join as join3, resolve as resolve3 } from "path";
665
+ import { defineCommand as defineCommand3 } from "citty";
666
+
667
+ // src/commands/migrate/migration-generator.ts
668
+ import { generateSQL } from "@korajs/core";
669
+
670
+ // src/commands/migrate/schema-differ.ts
671
+ function diffSchemas(previous, current) {
672
+ const changes = [];
673
+ const previousCollections = new Set(Object.keys(previous.collections));
674
+ const currentCollections = new Set(Object.keys(current.collections));
675
+ for (const collection of currentCollections) {
676
+ if (!previousCollections.has(collection)) {
677
+ changes.push({ type: "collection-added", collection });
678
+ }
679
+ }
680
+ for (const collection of previousCollections) {
681
+ if (!currentCollections.has(collection)) {
682
+ changes.push({ type: "collection-removed", collection });
683
+ }
684
+ }
685
+ for (const collection of currentCollections) {
686
+ if (!previousCollections.has(collection)) continue;
687
+ const previousDef = previous.collections[collection];
688
+ const currentDef = current.collections[collection];
689
+ if (!previousDef || !currentDef) continue;
690
+ const previousFields = previousDef.fields;
691
+ const currentFields = currentDef.fields;
692
+ for (const [fieldName, currentField] of Object.entries(currentFields)) {
693
+ const previousField = previousFields[fieldName];
694
+ if (!previousField) {
695
+ changes.push({
696
+ type: "field-added",
697
+ collection,
698
+ field: fieldName,
699
+ descriptor: currentField
700
+ });
701
+ continue;
702
+ }
703
+ if (!fieldDescriptorsEqual(previousField, currentField)) {
704
+ changes.push({
705
+ type: "field-changed",
706
+ collection,
707
+ field: fieldName,
708
+ before: previousField,
709
+ after: currentField
710
+ });
711
+ }
712
+ }
713
+ for (const [fieldName, previousField] of Object.entries(previousFields)) {
714
+ if (!(fieldName in currentFields)) {
715
+ changes.push({
716
+ type: "field-removed",
717
+ collection,
718
+ field: fieldName,
719
+ descriptor: previousField
720
+ });
721
+ }
722
+ }
723
+ const previousIndexes = new Set(previousDef.indexes);
724
+ const currentIndexes = new Set(currentDef.indexes);
725
+ for (const index of currentIndexes) {
726
+ if (!previousIndexes.has(index)) {
727
+ changes.push({ type: "index-added", collection, index });
728
+ }
729
+ }
730
+ for (const index of previousIndexes) {
731
+ if (!currentIndexes.has(index)) {
732
+ changes.push({ type: "index-removed", collection, index });
733
+ }
734
+ }
735
+ }
736
+ changes.sort(compareChanges);
737
+ return {
738
+ fromVersion: previous.version,
739
+ toVersion: current.version,
740
+ changes,
741
+ hasChanges: changes.length > 0,
742
+ hasBreakingChanges: changes.some(isBreakingChange)
743
+ };
744
+ }
745
+ function getChangedCollections(diff) {
746
+ const collections = /* @__PURE__ */ new Set();
747
+ for (const change of diff.changes) {
748
+ collections.add(change.collection);
749
+ }
750
+ return [...collections].sort();
751
+ }
752
+ function isBreakingChange(change) {
753
+ if (change.type === "collection-removed" || change.type === "field-removed") return true;
754
+ if (change.type === "field-changed") {
755
+ if (change.before.kind !== change.after.kind) return true;
756
+ if (change.before.itemKind !== change.after.itemKind) return true;
757
+ if (serializeEnum(change.before.enumValues) !== serializeEnum(change.after.enumValues)) return true;
758
+ if (change.before.required !== change.after.required && change.after.required) return true;
759
+ return false;
760
+ }
761
+ if (change.type === "field-added") {
762
+ const descriptor = change.descriptor;
763
+ return descriptor.required && descriptor.defaultValue === void 0 && !descriptor.auto;
764
+ }
765
+ return false;
766
+ }
767
+ function fieldDescriptorsEqual(left, right) {
768
+ return left.kind === right.kind && left.required === right.required && left.defaultValue === right.defaultValue && left.auto === right.auto && left.itemKind === right.itemKind && serializeEnum(left.enumValues) === serializeEnum(right.enumValues);
769
+ }
770
+ function serializeEnum(values) {
771
+ if (!values) return "";
772
+ return values.join("|");
773
+ }
774
+ function compareChanges(left, right) {
775
+ if (left.collection < right.collection) return -1;
776
+ if (left.collection > right.collection) return 1;
777
+ if (left.type < right.type) return -1;
778
+ if (left.type > right.type) return 1;
779
+ const leftKey = "field" in left ? left.field : "index" in left ? left.index : "";
780
+ const rightKey = "field" in right ? right.field : "index" in right ? right.index : "";
781
+ if (leftKey < rightKey) return -1;
782
+ if (leftKey > rightKey) return 1;
783
+ return 0;
784
+ }
785
+
786
+ // src/commands/migrate/migration-generator.ts
787
+ function generateMigration(previous, current, diff) {
788
+ const up = [];
789
+ const down = [];
790
+ for (const change of diff.changes) {
791
+ if (change.type === "collection-added") {
792
+ const collectionDef = current.collections[change.collection];
793
+ if (!collectionDef) continue;
794
+ up.push(...generateSQL(change.collection, collectionDef));
795
+ down.push(...dropCollectionStatements(change.collection));
796
+ }
797
+ if (change.type === "collection-removed") {
798
+ const collectionDef = previous.collections[change.collection];
799
+ up.push(...dropCollectionStatements(change.collection));
800
+ if (collectionDef) {
801
+ down.push(...generateSQL(change.collection, collectionDef));
802
+ }
803
+ }
804
+ }
805
+ const changedCollections = getChangedCollections(diff).filter(
806
+ (collection) => collection in previous.collections && collection in current.collections && diff.changes.some(
807
+ (change) => change.collection === collection && (change.type === "field-added" || change.type === "field-removed" || change.type === "field-changed" || change.type === "index-added" || change.type === "index-removed")
808
+ )
809
+ );
810
+ for (const collection of changedCollections) {
811
+ const previousDef = previous.collections[collection];
812
+ const currentDef = current.collections[collection];
813
+ if (!previousDef || !currentDef) continue;
814
+ validateRebuildSafety(collection, previousDef, currentDef);
815
+ up.push(...generateRebuildStatements(collection, previousDef, currentDef));
816
+ down.push(...generateRebuildStatements(collection, currentDef, previousDef));
817
+ }
818
+ down.reverse();
819
+ return {
820
+ up,
821
+ down,
822
+ summary: diff.changes.map(formatChange),
823
+ containsBreakingChanges: diff.hasBreakingChanges
824
+ };
825
+ }
826
+ function generateRebuildStatements(collection, from, to) {
827
+ const table = quoteIdentifier(collection);
828
+ const tempTable = quoteIdentifier(`_kora_mig_${collection}_new`);
829
+ const targetColumns = [
830
+ "id TEXT PRIMARY KEY NOT NULL",
831
+ ...Object.entries(to.fields).map(([field, descriptor]) => columnDefinition(field, descriptor)),
832
+ "_created_at INTEGER NOT NULL",
833
+ "_updated_at INTEGER NOT NULL",
834
+ "_deleted INTEGER NOT NULL DEFAULT 0"
835
+ ];
836
+ const statements = [];
837
+ statements.push(`CREATE TABLE ${tempTable} (
838
+ ${targetColumns.join(",\n ")}
839
+ )`);
840
+ const toFields = Object.keys(to.fields);
841
+ const columns = ["id", ...toFields, "_created_at", "_updated_at", "_deleted"];
842
+ const selectExpressions = columns.map(
843
+ (column) => projectionForColumn(column, from.fields, to.fields[column] ?? null)
844
+ );
845
+ statements.push(
846
+ `INSERT INTO ${tempTable} (${columns.map(quoteIdentifier).join(", ")}) SELECT ${selectExpressions.join(", ")} FROM ${table}`
847
+ );
848
+ statements.push(`DROP TABLE ${table}`);
849
+ statements.push(`ALTER TABLE ${tempTable} RENAME TO ${table}`);
850
+ for (const indexField of to.indexes) {
851
+ statements.push(
852
+ `CREATE INDEX IF NOT EXISTS idx_${collection}_${indexField} ON ${table} (${quoteIdentifier(indexField)})`
853
+ );
854
+ }
855
+ return statements;
856
+ }
857
+ function validateRebuildSafety(collection, from, to) {
858
+ for (const [fieldName, descriptor] of Object.entries(to.fields)) {
859
+ if (fieldName in from.fields) continue;
860
+ if (descriptor.required && descriptor.defaultValue === void 0 && !descriptor.auto) {
861
+ throw new Error(
862
+ `Cannot auto-migrate collection "${collection}": added required field "${fieldName}" has no default value.`
863
+ );
864
+ }
865
+ }
866
+ for (const [fieldName, targetDescriptor] of Object.entries(to.fields)) {
867
+ const sourceDescriptor = from.fields[fieldName];
868
+ if (!sourceDescriptor) continue;
869
+ if (canTransformField(sourceDescriptor, targetDescriptor)) continue;
870
+ if (targetDescriptor.required && targetDescriptor.defaultValue === void 0 && !targetDescriptor.auto) {
871
+ throw new Error(
872
+ `Cannot auto-migrate collection "${collection}": changed required field "${fieldName}" from ${sourceDescriptor.kind} to ${targetDescriptor.kind} without a safe transform/default.`
873
+ );
874
+ }
875
+ }
876
+ }
877
+ function projectionForColumn(column, fromFields, targetDescriptor) {
878
+ if (column === "id" || column === "_created_at" || column === "_updated_at" || column === "_deleted") {
879
+ return quoteIdentifier(column);
880
+ }
881
+ const sourceDescriptor = fromFields[column];
882
+ if (sourceDescriptor && targetDescriptor) {
883
+ return projectionForFieldTransform(column, sourceDescriptor, targetDescriptor);
884
+ }
885
+ if (sourceDescriptor) {
886
+ return quoteIdentifier(column);
887
+ }
888
+ if (!targetDescriptor) {
889
+ return "NULL";
890
+ }
891
+ if (targetDescriptor.auto && targetDescriptor.kind === "timestamp") {
892
+ return "CAST(strftime('%s','now') AS INTEGER) * 1000";
893
+ }
894
+ if (targetDescriptor.defaultValue !== void 0) {
895
+ return sqlLiteral(targetDescriptor.defaultValue);
896
+ }
897
+ return "NULL";
898
+ }
899
+ function projectionForFieldTransform(column, source, target) {
900
+ const sourceColumn = quoteIdentifier(column);
901
+ if (source.kind === target.kind && source.itemKind === target.itemKind) {
902
+ if (target.kind === "enum" && target.enumValues && target.enumValues.length > 0) {
903
+ const allowed = target.enumValues.map((value) => sqlLiteral(value)).join(", ");
904
+ const fallback = target.defaultValue !== void 0 ? sqlLiteral(target.defaultValue) : sourceColumn;
905
+ return `CASE WHEN ${sourceColumn} IN (${allowed}) THEN ${sourceColumn} ELSE ${fallback} END`;
906
+ }
907
+ return sourceColumn;
908
+ }
909
+ if (target.kind === "string") {
910
+ return `CAST(${sourceColumn} AS TEXT)`;
911
+ }
912
+ if (target.kind === "number" || target.kind === "timestamp") {
913
+ if (source.kind === "string" || source.kind === "enum" || source.kind === "number" || source.kind === "timestamp" || source.kind === "boolean") {
914
+ const castType = target.kind === "number" ? "REAL" : "INTEGER";
915
+ return `CASE WHEN ${sourceColumn} IS NULL THEN NULL ELSE CAST(${sourceColumn} AS ${castType}) END`;
916
+ }
917
+ }
918
+ if (target.kind === "boolean") {
919
+ if (source.kind === "number" || source.kind === "timestamp" || source.kind === "boolean") {
920
+ return `CASE WHEN ${sourceColumn} IS NULL THEN NULL WHEN CAST(${sourceColumn} AS REAL) = 0 THEN 0 ELSE 1 END`;
921
+ }
922
+ if (source.kind === "string" || source.kind === "enum") {
923
+ return `CASE WHEN ${sourceColumn} IS NULL THEN NULL WHEN LOWER(TRIM(CAST(${sourceColumn} AS TEXT))) IN ('1','true','t','yes','y','on') THEN 1 WHEN LOWER(TRIM(CAST(${sourceColumn} AS TEXT))) IN ('0','false','f','no','n','off') THEN 0 ELSE ${projectionFallback(target)} END`;
924
+ }
925
+ }
926
+ if (target.kind === "enum" && target.enumValues && target.enumValues.length > 0) {
927
+ if (source.kind === "string" || source.kind === "enum") {
928
+ const allowed = target.enumValues.map((value) => sqlLiteral(value)).join(", ");
929
+ return `CASE WHEN ${sourceColumn} IN (${allowed}) THEN ${sourceColumn} ELSE ${projectionFallback(target)} END`;
930
+ }
931
+ }
932
+ if (target.kind === "array" && source.kind === "array" && source.itemKind === target.itemKind) {
933
+ return sourceColumn;
934
+ }
935
+ if (target.auto && target.kind === "timestamp") {
936
+ return "CAST(strftime('%s','now') AS INTEGER) * 1000";
937
+ }
938
+ return projectionFallback(target);
939
+ }
940
+ function canTransformField(source, target) {
941
+ if (source.kind === target.kind && source.itemKind === target.itemKind) {
942
+ return true;
943
+ }
944
+ if (target.kind === "string") {
945
+ return true;
946
+ }
947
+ if (target.kind === "number" || target.kind === "timestamp") {
948
+ return source.kind === "string" || source.kind === "enum" || source.kind === "number" || source.kind === "timestamp" || source.kind === "boolean";
949
+ }
950
+ if (target.kind === "boolean") {
951
+ return source.kind === "number" || source.kind === "timestamp" || source.kind === "boolean" || source.kind === "string" || source.kind === "enum";
952
+ }
953
+ if (target.kind === "enum") {
954
+ return source.kind === "string" || source.kind === "enum";
955
+ }
956
+ if (target.kind === "array") {
957
+ return source.kind === "array" && source.itemKind === target.itemKind;
958
+ }
959
+ if (target.kind === "richtext") {
960
+ return source.kind === "richtext";
961
+ }
962
+ return false;
963
+ }
964
+ function projectionFallback(target) {
965
+ if (target.auto && target.kind === "timestamp") {
966
+ return "CAST(strftime('%s','now') AS INTEGER) * 1000";
967
+ }
968
+ if (target.defaultValue !== void 0) {
969
+ return sqlLiteral(target.defaultValue);
970
+ }
971
+ return "NULL";
972
+ }
973
+ function dropCollectionStatements(collection) {
974
+ const table = quoteIdentifier(collection);
975
+ const opsTable = quoteIdentifier(`_kora_ops_${collection}`);
976
+ return [`DROP TABLE IF EXISTS ${table}`, `DROP TABLE IF EXISTS ${opsTable}`];
977
+ }
978
+ function columnDefinition(fieldName, descriptor) {
979
+ const sqlType = mapFieldType(descriptor);
980
+ const parts = [quoteIdentifier(fieldName), sqlType];
981
+ if (descriptor.required && descriptor.defaultValue === void 0 && !descriptor.auto) {
982
+ parts.push("NOT NULL");
983
+ }
984
+ if (descriptor.defaultValue !== void 0) {
985
+ parts.push(`DEFAULT ${sqlLiteral(descriptor.defaultValue)}`);
986
+ }
987
+ if (descriptor.kind === "enum" && descriptor.enumValues) {
988
+ const values = descriptor.enumValues.map((value) => sqlLiteral(value)).join(", ");
989
+ parts.push(`CHECK (${quoteIdentifier(fieldName)} IN (${values}))`);
990
+ }
991
+ return parts.join(" ");
992
+ }
993
+ function mapFieldType(descriptor) {
994
+ switch (descriptor.kind) {
995
+ case "string":
996
+ return "TEXT";
997
+ case "number":
998
+ return "REAL";
999
+ case "boolean":
1000
+ return "INTEGER";
1001
+ case "enum":
1002
+ return "TEXT";
1003
+ case "timestamp":
1004
+ return "INTEGER";
1005
+ case "array":
1006
+ return "TEXT";
1007
+ case "richtext":
1008
+ return "BLOB";
1009
+ }
1010
+ }
1011
+ function sqlLiteral(value) {
1012
+ if (value === null) return "NULL";
1013
+ if (typeof value === "number") return String(value);
1014
+ if (typeof value === "boolean") return value ? "1" : "0";
1015
+ if (typeof value === "string") return `'${value.replaceAll("'", "''")}'`;
1016
+ return `'${JSON.stringify(value).replaceAll("'", "''")}'`;
1017
+ }
1018
+ function quoteIdentifier(identifier) {
1019
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(identifier)) {
1020
+ throw new Error(`Invalid SQL identifier: ${identifier}`);
1021
+ }
1022
+ return identifier;
1023
+ }
1024
+ function formatChange(change) {
1025
+ switch (change.type) {
1026
+ case "collection-added":
1027
+ return `+ collection ${change.collection}`;
1028
+ case "collection-removed":
1029
+ return `- collection ${change.collection}`;
1030
+ case "field-added":
1031
+ return `+ ${change.collection}.${change.field}`;
1032
+ case "field-removed":
1033
+ return `- ${change.collection}.${change.field}`;
1034
+ case "field-changed":
1035
+ return `~ ${change.collection}.${change.field}`;
1036
+ case "index-added":
1037
+ return `+ index ${change.collection}.${change.index}`;
1038
+ case "index-removed":
1039
+ return `- index ${change.collection}.${change.index}`;
1040
+ }
1041
+ }
1042
+
1043
+ // src/commands/migrate/migration-runner.ts
1044
+ var MigrationApplyError = class extends Error {
1045
+ constructor(message, backend, report) {
1046
+ super(message);
1047
+ this.backend = backend;
1048
+ this.report = report;
1049
+ this.name = "MigrationApplyError";
1050
+ }
1051
+ backend;
1052
+ report;
1053
+ };
1054
+ async function runMigration(options) {
1055
+ const report = { backends: [] };
1056
+ const migrationId = options.migrationId ?? `migration-${Date.now()}`;
1057
+ const fromVersion = options.fromVersion ?? 0;
1058
+ const toVersion = options.toVersion ?? 0;
1059
+ if (options.sqlitePath) {
1060
+ try {
1061
+ const sqliteReport = await runSqliteMigration(
1062
+ options.sqlitePath,
1063
+ options.upStatements,
1064
+ migrationId,
1065
+ fromVersion,
1066
+ toVersion,
1067
+ options.projectRoot,
1068
+ options.sqliteDriver
1069
+ );
1070
+ report.backends.push(sqliteReport);
1071
+ } catch (error) {
1072
+ throw new MigrationApplyError(error.message, "sqlite", report);
1073
+ }
1074
+ }
1075
+ if (options.postgresConnectionString) {
1076
+ try {
1077
+ const postgresReport = await runPostgresMigration(
1078
+ options.postgresConnectionString,
1079
+ options.upStatements,
1080
+ migrationId,
1081
+ fromVersion,
1082
+ toVersion,
1083
+ options.postgresClientFactory
1084
+ );
1085
+ report.backends.push(postgresReport);
1086
+ } catch (error) {
1087
+ throw new MigrationApplyError(error.message, "postgres", report);
1088
+ }
1089
+ }
1090
+ return report;
1091
+ }
1092
+ async function runSqliteMigration(path, statements, migrationId, fromVersion, toVersion, projectRoot, driverOverride) {
1093
+ const driver = driverOverride ?? await loadSqliteDriver(projectRoot);
1094
+ const db = driver.open(path);
1095
+ let statementsApplied = 0;
1096
+ try {
1097
+ db.exec("BEGIN");
1098
+ db.exec(
1099
+ "CREATE TABLE IF NOT EXISTS _kora_migrations (id TEXT PRIMARY KEY NOT NULL, from_version INTEGER NOT NULL, to_version INTEGER NOT NULL, applied_at INTEGER NOT NULL)"
1100
+ );
1101
+ const alreadyApplied = typeof db.isMigrationApplied === "function" ? db.isMigrationApplied(migrationId) : false;
1102
+ if (alreadyApplied) {
1103
+ db.exec("COMMIT");
1104
+ return {
1105
+ backend: "sqlite",
1106
+ statementsApplied: 0,
1107
+ historyRecorded: true,
1108
+ skipped: true
1109
+ };
1110
+ }
1111
+ for (const statement of statements) {
1112
+ db.exec(statement);
1113
+ statementsApplied++;
1114
+ }
1115
+ db.exec(
1116
+ `INSERT OR REPLACE INTO _kora_migrations (id, from_version, to_version, applied_at) VALUES (${sqlLiteral2(migrationId)}, ${fromVersion}, ${toVersion}, ${Date.now()})`
1117
+ );
1118
+ db.exec("COMMIT");
1119
+ return {
1120
+ backend: "sqlite",
1121
+ statementsApplied,
1122
+ historyRecorded: true,
1123
+ skipped: false
1124
+ };
1125
+ } catch (error) {
1126
+ try {
1127
+ db.exec("ROLLBACK");
1128
+ } catch {
1129
+ }
1130
+ throw error;
1131
+ } finally {
1132
+ if (typeof db.close === "function") {
1133
+ db.close();
1134
+ }
1135
+ }
1136
+ }
1137
+ async function loadSqliteDriver(projectRoot) {
1138
+ try {
1139
+ const { createRequire } = await import("module");
1140
+ const requireFrom = createRequire(
1141
+ projectRoot ? `${projectRoot}/package.json` : import.meta.url
1142
+ );
1143
+ const Database = requireFrom("better-sqlite3");
1144
+ return {
1145
+ open(path) {
1146
+ const db = new Database(path);
1147
+ return {
1148
+ exec(sql) {
1149
+ db.exec(sql);
1150
+ },
1151
+ isMigrationApplied(id) {
1152
+ const row = db.prepare("SELECT COUNT(*) AS count FROM _kora_migrations WHERE id = ?").get(id);
1153
+ return (row?.count ?? 0) > 0;
1154
+ },
1155
+ close() {
1156
+ db.close();
1157
+ }
1158
+ };
1159
+ }
1160
+ };
1161
+ } catch {
1162
+ throw new Error(
1163
+ 'SQLite migration apply requires the "better-sqlite3" package in the target project dependencies.'
1164
+ );
1165
+ }
1166
+ }
1167
+ async function runPostgresMigration(connectionString, statements, migrationId, fromVersion, toVersion, clientFactoryOverride) {
1168
+ const sql = typeof clientFactoryOverride === "function" ? clientFactoryOverride(connectionString) : (await loadPostgresModule()).default(connectionString);
1169
+ let statementsApplied = 0;
1170
+ try {
1171
+ await sql.unsafe("BEGIN");
1172
+ await sql.unsafe(
1173
+ "CREATE TABLE IF NOT EXISTS _kora_migrations (id TEXT PRIMARY KEY, from_version INTEGER NOT NULL, to_version INTEGER NOT NULL, applied_at BIGINT NOT NULL)"
1174
+ );
1175
+ const existing = await sql.unsafe(
1176
+ `SELECT COUNT(*)::int AS count FROM _kora_migrations WHERE id = ${sqlLiteral2(migrationId)}`
1177
+ );
1178
+ if ((existing[0]?.count ?? 0) > 0) {
1179
+ await sql.unsafe("COMMIT");
1180
+ return {
1181
+ backend: "postgres",
1182
+ statementsApplied: 0,
1183
+ historyRecorded: true,
1184
+ skipped: true
1185
+ };
1186
+ }
1187
+ for (const statement of statements) {
1188
+ await sql.unsafe(statement);
1189
+ statementsApplied++;
1190
+ }
1191
+ await sql.unsafe(
1192
+ `INSERT INTO _kora_migrations (id, from_version, to_version, applied_at) VALUES (${sqlLiteral2(migrationId)}, ${fromVersion}, ${toVersion}, ${Date.now()}) ON CONFLICT (id) DO UPDATE SET from_version = EXCLUDED.from_version, to_version = EXCLUDED.to_version, applied_at = EXCLUDED.applied_at`
1193
+ );
1194
+ await sql.unsafe("COMMIT");
1195
+ return {
1196
+ backend: "postgres",
1197
+ statementsApplied,
1198
+ historyRecorded: true,
1199
+ skipped: false
1200
+ };
1201
+ } catch (error) {
1202
+ try {
1203
+ await sql.unsafe("ROLLBACK");
1204
+ } catch {
1205
+ }
1206
+ throw error;
1207
+ } finally {
1208
+ if (typeof sql.end === "function") {
1209
+ await sql.end();
1210
+ }
1211
+ }
1212
+ }
1213
+ function sqlLiteral2(value) {
1214
+ return `'${value.replaceAll("'", "''")}'`;
1215
+ }
1216
+ async function loadPostgresModule() {
1217
+ try {
1218
+ const dynamicImport = new Function("specifier", "return import(specifier)");
1219
+ const mod = await dynamicImport("postgres");
1220
+ if (typeof mod === "object" && mod !== null && "default" in mod) {
1221
+ return mod;
1222
+ }
1223
+ throw new Error("Invalid postgres module");
1224
+ } catch {
1225
+ throw new Error(
1226
+ 'PostgreSQL migration apply requires the "postgres" package in the target project dependencies.'
1227
+ );
1228
+ }
1229
+ }
1230
+
1231
+ // src/commands/migrate/schema-loader.ts
1232
+ import { spawn as spawn4 } from "child_process";
1233
+ import { extname as extname2 } from "path";
1234
+ import { pathToFileURL as pathToFileURL2 } from "url";
1235
+ async function loadSchemaDefinition(schemaPath, projectRoot) {
1236
+ const ext = extname2(schemaPath);
1237
+ const moduleValue = ext === ".ts" || ext === ".mts" || ext === ".cts" ? await loadTypeScriptModule(schemaPath, projectRoot) : await import(`${pathToFileURL2(schemaPath).href}?t=${Date.now()}-${Math.random()}`);
1238
+ return extractSchema2(moduleValue);
1239
+ }
1240
+ function extractSchema2(value) {
1241
+ if (typeof value !== "object" || value === null) {
1242
+ throw new Error("Schema module must export an object.");
1243
+ }
1244
+ const moduleRecord = value;
1245
+ const candidate = moduleRecord.default ?? moduleRecord;
1246
+ if (!isSchemaDefinition2(candidate)) {
1247
+ throw new Error("Schema module must export a valid SchemaDefinition as default export.");
1248
+ }
1249
+ return candidate;
1250
+ }
1251
+ function isSchemaDefinition2(value) {
1252
+ if (typeof value !== "object" || value === null) return false;
1253
+ const object = value;
1254
+ return typeof object.version === "number" && typeof object.collections === "object" && object.collections !== null && typeof object.relations === "object" && object.relations !== null;
1255
+ }
1256
+ async function loadTypeScriptModule(schemaPath, projectRoot) {
1257
+ const tsxBinary = await resolveProjectBinary(projectRoot, "tsx");
1258
+ if (!tsxBinary) {
1259
+ throw new Error(
1260
+ `Schema file is TypeScript (${schemaPath}) but local "tsx" was not found. Install tsx in the project.`
1261
+ );
1262
+ }
1263
+ const script = [
1264
+ "import { pathToFileURL } from 'node:url'",
1265
+ "const modulePath = process.argv[process.argv.length - 1]",
1266
+ "const mod = await import(pathToFileURL(modulePath).href)",
1267
+ "const value = mod.default ?? mod",
1268
+ "process.stdout.write(JSON.stringify(value))"
1269
+ ].join(";");
1270
+ const output = await runCommand2(tsxBinary, ["--eval", script, schemaPath], projectRoot);
1271
+ try {
1272
+ return JSON.parse(output);
1273
+ } catch {
1274
+ throw new Error(`Failed to parse schema module output for ${schemaPath}`);
1275
+ }
1276
+ }
1277
+ async function runCommand2(command, args, cwd) {
1278
+ return await new Promise((resolve4, reject) => {
1279
+ const child = spawn4(command, args, {
1280
+ cwd,
1281
+ stdio: ["ignore", "pipe", "pipe"],
1282
+ env: process.env
1283
+ });
1284
+ let stdout = "";
1285
+ let stderr = "";
1286
+ child.stdout?.on("data", (chunk) => {
1287
+ stdout += chunk.toString("utf-8");
1288
+ });
1289
+ child.stderr?.on("data", (chunk) => {
1290
+ stderr += chunk.toString("utf-8");
1291
+ });
1292
+ child.on("error", (error) => {
1293
+ reject(error);
1294
+ });
1295
+ child.on("exit", (code) => {
1296
+ if (code === 0) {
1297
+ resolve4(stdout.trim());
1298
+ return;
1299
+ }
1300
+ reject(
1301
+ new Error(`Failed to load TypeScript schema (exit ${code ?? "unknown"}): ${stderr.trim()}`)
1302
+ );
1303
+ });
1304
+ });
1305
+ }
1306
+
1307
+ // src/commands/migrate/migrate-command.ts
1308
+ var SNAPSHOT_PATH = "kora/schema.snapshot.json";
1309
+ var MIGRATIONS_DIR = "kora/migrations";
1310
+ var migrateCommand = defineCommand3({
1311
+ meta: {
1312
+ name: "migrate",
1313
+ description: "Detect schema changes and generate/apply migrations"
1314
+ },
1315
+ args: {
1316
+ apply: {
1317
+ type: "boolean",
1318
+ description: "Apply migration to configured database backends",
1319
+ default: false
1320
+ },
1321
+ schema: {
1322
+ type: "string",
1323
+ description: "Path to schema file"
1324
+ },
1325
+ db: {
1326
+ type: "string",
1327
+ description: "SQLite database path for --apply (overrides config)"
1328
+ },
1329
+ "output-dir": {
1330
+ type: "string",
1331
+ description: "Migration output directory",
1332
+ default: MIGRATIONS_DIR
1333
+ },
1334
+ "dry-run": {
1335
+ type: "boolean",
1336
+ description: "Preview migration changes without writing files",
1337
+ default: false
1338
+ },
1339
+ force: {
1340
+ type: "boolean",
1341
+ description: "Skip breaking-change confirmation prompts",
1342
+ default: false
1343
+ }
1344
+ },
1345
+ async run({ args }) {
1346
+ const logger = createLogger();
1347
+ const projectRoot = await findProjectRoot();
1348
+ if (!projectRoot) {
1349
+ throw new InvalidProjectError(process.cwd());
1350
+ }
1351
+ const config = await loadKoraConfig(projectRoot);
1352
+ const resolvedSchemaPath = typeof args.schema === "string" ? resolve3(projectRoot, args.schema) : typeof config?.schema === "string" ? resolve3(projectRoot, config.schema) : await findSchemaFile(projectRoot);
1353
+ if (!resolvedSchemaPath) {
1354
+ throw new SchemaNotFoundError(["src/schema.ts", "schema.ts", "src/schema.js", "schema.js"]);
1355
+ }
1356
+ const currentSchema = await loadSchemaDefinition(resolvedSchemaPath, projectRoot);
1357
+ const snapshotFile = join3(projectRoot, SNAPSHOT_PATH);
1358
+ const previousSchema = await readSchemaSnapshot(snapshotFile);
1359
+ if (!previousSchema) {
1360
+ if (args["dry-run"] === true) {
1361
+ logger.info("No schema snapshot found. Dry run: baseline snapshot would be created.");
1362
+ return;
1363
+ }
1364
+ await writeSchemaSnapshot(snapshotFile, currentSchema);
1365
+ logger.success(`Initialized schema snapshot at ${snapshotFile}`);
1366
+ logger.step("Run `kora migrate` again after schema changes to generate migrations.");
1367
+ return;
1368
+ }
1369
+ const diff = diffSchemas(previousSchema, currentSchema);
1370
+ const outputDir = typeof args["output-dir"] === "string" ? resolve3(projectRoot, args["output-dir"]) : resolve3(projectRoot, MIGRATIONS_DIR);
1371
+ if (!diff.hasChanges) {
1372
+ logger.success("No schema changes detected.");
1373
+ if (args.apply === true) {
1374
+ const sqlitePath = resolveSqliteApplyPath(args.db, projectRoot, config);
1375
+ const postgresConnectionString = resolvePostgresConnectionString(config);
1376
+ const pending = await listMigrationManifests(outputDir);
1377
+ if (pending.length === 0) {
1378
+ logger.step("No migration files found to apply.");
1379
+ return;
1380
+ }
1381
+ for (const manifest of pending) {
1382
+ const report = await runMigration({
1383
+ upStatements: manifest.up,
1384
+ migrationId: manifest.id,
1385
+ fromVersion: manifest.fromVersion,
1386
+ toVersion: manifest.toVersion,
1387
+ sqlitePath,
1388
+ postgresConnectionString,
1389
+ projectRoot
1390
+ });
1391
+ for (const backend of report.backends) {
1392
+ logger.step(
1393
+ ` ${manifest.id} -> ${backend.backend}: applied=${backend.statementsApplied}, skipped=${backend.skipped}`
1394
+ );
1395
+ }
1396
+ }
1397
+ }
1398
+ return;
1399
+ }
1400
+ const generated = generateMigration(previousSchema, currentSchema, diff);
1401
+ logger.banner();
1402
+ logger.info(`Detected schema change: v${diff.fromVersion} \u2192 v${diff.toVersion}`);
1403
+ logger.blank();
1404
+ logger.info("Changes:");
1405
+ for (const line of generated.summary) {
1406
+ logger.step(` ${line}`);
1407
+ }
1408
+ if (diff.hasBreakingChanges && args["dry-run"] !== true) {
1409
+ logger.blank();
1410
+ logger.warn("Breaking schema changes detected.");
1411
+ const shouldContinue = await confirmBreakingChanges(args.force === true);
1412
+ if (!shouldContinue) {
1413
+ logger.warn("Migration generation aborted.");
1414
+ return;
1415
+ }
1416
+ }
1417
+ if (args["dry-run"] === true) {
1418
+ logger.blank();
1419
+ logger.warn("Dry run enabled: no files written, no migrations applied.");
1420
+ return;
1421
+ }
1422
+ await mkdir2(outputDir, { recursive: true });
1423
+ const migrationPath = await writeMigrationFile(outputDir, diff.fromVersion, diff.toVersion, generated);
1424
+ await writeSchemaSnapshot(snapshotFile, currentSchema);
1425
+ logger.blank();
1426
+ logger.success(`Generated migration: ${migrationPath}`);
1427
+ if (args.apply === true) {
1428
+ const sqlitePath = resolveSqliteApplyPath(args.db, projectRoot, config);
1429
+ const postgresConnectionString = resolvePostgresConnectionString(config);
1430
+ const pending = await listMigrationManifests(outputDir);
1431
+ for (const manifest of pending) {
1432
+ const report = await runMigration({
1433
+ upStatements: manifest.up,
1434
+ migrationId: manifest.id,
1435
+ fromVersion: manifest.fromVersion,
1436
+ toVersion: manifest.toVersion,
1437
+ sqlitePath,
1438
+ postgresConnectionString,
1439
+ projectRoot
1440
+ });
1441
+ for (const backend of report.backends) {
1442
+ logger.step(
1443
+ ` ${manifest.id} -> ${backend.backend}: applied=${backend.statementsApplied}, skipped=${backend.skipped}, history=${backend.historyRecorded}`
1444
+ );
1445
+ }
1446
+ }
1447
+ logger.success("Applied pending migrations successfully.");
1448
+ }
1449
+ }
1450
+ });
1451
+ async function readSchemaSnapshot(path) {
1452
+ try {
1453
+ const content = await readFile(path, "utf-8");
1454
+ return JSON.parse(content);
1455
+ } catch {
1456
+ return null;
1457
+ }
1458
+ }
1459
+ async function confirmBreakingChanges(force) {
1460
+ if (force) {
1461
+ return true;
1462
+ }
1463
+ if (!isInteractiveTerminal()) {
1464
+ throw new Error(
1465
+ "Breaking schema changes require confirmation. Re-run with --force to continue or --dry-run to preview."
1466
+ );
1467
+ }
1468
+ return await promptConfirm("Continue and generate a breaking migration?", false);
1469
+ }
1470
+ function isInteractiveTerminal() {
1471
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
1472
+ }
1473
+ async function writeSchemaSnapshot(path, schema) {
1474
+ await mkdir2(dirname2(path), { recursive: true });
1475
+ await writeFile2(path, `${JSON.stringify(schema, null, 2)}
1476
+ `, "utf-8");
1477
+ }
1478
+ async function writeMigrationFile(outputDir, fromVersion, toVersion, generated) {
1479
+ const existing = await readdir(outputDir).catch(() => []);
1480
+ const sequence = existing.filter((file) => /^\d{3}-/.test(file)).length + 1;
1481
+ const filename = `${String(sequence).padStart(3, "0")}-v${fromVersion}-to-v${toVersion}.ts`;
1482
+ const path = join3(outputDir, filename);
1483
+ const migrationId = filename.replace(/\.ts$/, "");
1484
+ const fileContent = [
1485
+ `export const up = ${JSON.stringify(generated.up, null, 2)} as const`,
1486
+ "",
1487
+ `export const down = ${JSON.stringify(generated.down, null, 2)} as const`,
1488
+ "",
1489
+ `export const summary = ${JSON.stringify(generated.summary, null, 2)} as const`,
1490
+ "",
1491
+ `export const containsBreakingChanges = ${generated.containsBreakingChanges}`,
1492
+ ""
1493
+ ].join("\n");
1494
+ await writeFile2(path, fileContent, "utf-8");
1495
+ await writeMigrationManifest(join3(outputDir, `${migrationId}.json`), {
1496
+ id: migrationId,
1497
+ fromVersion,
1498
+ toVersion,
1499
+ up: generated.up,
1500
+ down: generated.down,
1501
+ summary: generated.summary,
1502
+ containsBreakingChanges: generated.containsBreakingChanges
1503
+ });
1504
+ return path;
1505
+ }
1506
+ async function writeMigrationManifest(path, manifest) {
1507
+ await writeFile2(path, `${JSON.stringify(manifest, null, 2)}
1508
+ `, "utf-8");
1509
+ }
1510
+ async function listMigrationManifests(outputDir) {
1511
+ const files = await readdir(outputDir).catch(() => []);
1512
+ const migrationFiles = files.filter((file) => /^\d{3}-.*\.ts$/.test(file)).sort((left, right) => left.localeCompare(right));
1513
+ const manifests = [];
1514
+ for (const file of migrationFiles) {
1515
+ const id = file.replace(/\.ts$/, "");
1516
+ const manifestPath = join3(outputDir, `${id}.json`);
1517
+ const jsonManifest = await readMigrationManifest(manifestPath);
1518
+ if (jsonManifest) {
1519
+ manifests.push({ ...jsonManifest, id });
1520
+ continue;
1521
+ }
1522
+ const migrationPath = join3(outputDir, file);
1523
+ const sourceManifest = await readMigrationManifestFromSource(migrationPath, id);
1524
+ manifests.push(sourceManifest);
1525
+ }
1526
+ return manifests;
1527
+ }
1528
+ async function readMigrationManifest(path) {
1529
+ try {
1530
+ const content = await readFile(path, "utf-8");
1531
+ return JSON.parse(content);
1532
+ } catch (error) {
1533
+ const code = error.code;
1534
+ if (code === "ENOENT") {
1535
+ return null;
1536
+ }
1537
+ throw error;
1538
+ }
1539
+ }
1540
+ async function readMigrationManifestFromSource(path, id) {
1541
+ const content = await readFile(path, "utf-8");
1542
+ const versions = parseVersionsFromMigrationId(id);
1543
+ return {
1544
+ id,
1545
+ fromVersion: versions.fromVersion,
1546
+ toVersion: versions.toVersion,
1547
+ up: parseStringArrayExport(content, "up"),
1548
+ down: parseStringArrayExport(content, "down"),
1549
+ summary: parseStringArrayExport(content, "summary"),
1550
+ containsBreakingChanges: parseBooleanExport(content, "containsBreakingChanges")
1551
+ };
1552
+ }
1553
+ function parseVersionsFromMigrationId(id) {
1554
+ const match = id.match(/-v(\d+)-to-v(\d+)$/);
1555
+ if (!match) {
1556
+ throw new Error(`Migration id "${id}" does not include a vX-to-vY version suffix.`);
1557
+ }
1558
+ return {
1559
+ fromVersion: Number.parseInt(match[1], 10),
1560
+ toVersion: Number.parseInt(match[2], 10)
1561
+ };
1562
+ }
1563
+ function parseStringArrayExport(source, exportName) {
1564
+ const expression = parseExportExpression(source, exportName);
1565
+ const parsed = JSON.parse(expression);
1566
+ if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
1567
+ throw new Error(`Migration export "${exportName}" must be a string array.`);
1568
+ }
1569
+ return parsed;
1570
+ }
1571
+ function parseBooleanExport(source, exportName) {
1572
+ const expression = parseLiteralExport(source, exportName);
1573
+ if (expression === "true") return true;
1574
+ if (expression === "false") return false;
1575
+ throw new Error(`Migration export "${exportName}" must be a boolean literal.`);
1576
+ }
1577
+ function parseExportExpression(source, exportName) {
1578
+ const escapedName = exportName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1579
+ const regex = new RegExp(`export const ${escapedName} = ([\\s\\S]*?) as const`);
1580
+ const match = source.match(regex);
1581
+ if (!match || !match[1]) {
1582
+ throw new Error(`Failed to read migration export "${exportName}".`);
1583
+ }
1584
+ return match[1].trim();
1585
+ }
1586
+ function parseLiteralExport(source, exportName) {
1587
+ const escapedName = exportName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1588
+ const regex = new RegExp(`export const ${escapedName} = ([^
1589
+ \r]+)`);
1590
+ const match = source.match(regex);
1591
+ if (!match || !match[1]) {
1592
+ throw new Error(`Failed to read migration export "${exportName}".`);
1593
+ }
1594
+ return match[1].trim();
1595
+ }
1596
+ function resolveSqliteApplyPath(dbArg, projectRoot, config) {
1597
+ if (typeof dbArg === "string") {
1598
+ return resolve3(projectRoot, dbArg);
1599
+ }
1600
+ const sync = config?.dev?.sync;
1601
+ if (typeof sync === "object" && sync !== null) {
1602
+ if (sync.store === "sqlite") {
1603
+ return join3(projectRoot, "kora-sync.db");
1604
+ }
1605
+ if (typeof sync.store === "object" && sync.store !== null && sync.store.type === "sqlite") {
1606
+ if (typeof sync.store.filename === "string" && sync.store.filename.length > 0) {
1607
+ return resolve3(projectRoot, sync.store.filename);
1608
+ }
1609
+ return join3(projectRoot, "kora-sync.db");
1610
+ }
1611
+ }
1612
+ return void 0;
1613
+ }
1614
+ function resolvePostgresConnectionString(config) {
1615
+ const sync = config?.dev?.sync;
1616
+ if (typeof sync !== "object" || sync === null) return void 0;
1617
+ if (sync.store === "postgres") {
1618
+ return process.env.DATABASE_URL;
1619
+ }
1620
+ if (typeof sync.store === "object" && sync.store !== null && sync.store.type === "postgres") {
1621
+ return sync.store.connectionString;
1622
+ }
1623
+ return void 0;
1624
+ }
1625
+
1626
+ // src/bin.ts
1627
+ var main = defineCommand4({
1628
+ meta: {
1629
+ name: "kora",
1630
+ description: "Kora.js \u2014 Offline-first application framework"
1631
+ },
1632
+ subCommands: {
1633
+ create: createCommand,
1634
+ dev: devCommand,
1635
+ generate: generateCommand,
1636
+ migrate: migrateCommand
1637
+ }
1638
+ });
1639
+ runMain(main);
1640
+ //# sourceMappingURL=bin.js.map