@korajs/cli 0.3.4 → 0.5.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 (104) hide show
  1. package/dist/bin.cjs +1839 -829
  2. package/dist/bin.cjs.map +1 -1
  3. package/dist/bin.js +1107 -146
  4. package/dist/bin.js.map +1 -1
  5. package/dist/{chunk-HNVCNPER.js → chunk-3WNFM3QB.js} +107 -97
  6. package/dist/chunk-3WNFM3QB.js.map +1 -0
  7. package/dist/{chunk-ULTO5NFA.js → chunk-PSRM56B7.js} +2 -2
  8. package/dist/chunk-PSRM56B7.js.map +1 -0
  9. package/dist/{chunk-MIMLCXSX.js → chunk-SOTZIWIF.js} +267 -228
  10. package/dist/chunk-SOTZIWIF.js.map +1 -0
  11. package/dist/create.cjs +58 -48
  12. package/dist/create.cjs.map +1 -1
  13. package/dist/create.js +2 -2
  14. package/dist/index.cjs +322 -273
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +31 -29
  17. package/dist/index.d.ts +31 -29
  18. package/dist/index.js +2 -2
  19. package/package.json +9 -3
  20. package/templates/react-basic/README.md.hbs +10 -1
  21. package/templates/react-basic/src/App.tsx +112 -126
  22. package/templates/react-basic/src/index.css +165 -159
  23. package/templates/react-basic/src/kora-worker.ts +0 -1
  24. package/templates/react-basic/src/main.tsx +12 -12
  25. package/templates/react-basic/src/modules/todos/todo.mutations.ts +25 -0
  26. package/templates/react-basic/src/modules/todos/todo.queries.ts +5 -0
  27. package/templates/react-basic/src/modules/todos/todo.schema.ts +10 -0
  28. package/templates/react-basic/src/modules/todos/useTodos.ts +31 -0
  29. package/templates/react-basic/src/schema.ts +6 -12
  30. package/templates/react-basic/src/vite-env.d.ts +17 -0
  31. package/templates/react-basic/{tsconfig.json → tsconfig.json.hbs} +1 -2
  32. package/templates/react-basic/vite.config.ts +57 -50
  33. package/templates/react-sync/.env.example +19 -0
  34. package/templates/react-sync/README.md.hbs +43 -1
  35. package/templates/react-sync/package.json.hbs +3 -0
  36. package/templates/react-sync/server.ts +90 -26
  37. package/templates/react-sync/src/App.tsx +132 -130
  38. package/templates/react-sync/src/auth.ts +25 -0
  39. package/templates/react-sync/src/index.css +212 -165
  40. package/templates/react-sync/src/kora-worker.ts +0 -1
  41. package/templates/react-sync/src/main.tsx +34 -18
  42. package/templates/react-sync/src/modules/todos/todo.mutations.ts +25 -0
  43. package/templates/react-sync/src/modules/todos/todo.queries.ts +5 -0
  44. package/templates/react-sync/src/modules/todos/todo.schema.ts +10 -0
  45. package/templates/react-sync/src/modules/todos/useTodos.ts +31 -0
  46. package/templates/react-sync/src/schema.ts +6 -12
  47. package/templates/react-sync/src/vite-env.d.ts +17 -0
  48. package/templates/react-sync/{tsconfig.json → tsconfig.json.hbs} +1 -2
  49. package/templates/react-sync/vite.config.ts +67 -60
  50. package/templates/react-tailwind/README.md.hbs +10 -1
  51. package/templates/react-tailwind/src/App.tsx +186 -197
  52. package/templates/react-tailwind/src/index.css +3 -3
  53. package/templates/react-tailwind/src/kora-worker.ts +0 -1
  54. package/templates/react-tailwind/src/main.tsx +19 -12
  55. package/templates/react-tailwind/src/modules/todos/todo.mutations.ts +25 -0
  56. package/templates/react-tailwind/src/modules/todos/todo.queries.ts +5 -0
  57. package/templates/react-tailwind/src/modules/todos/todo.schema.ts +10 -0
  58. package/templates/react-tailwind/src/modules/todos/useTodos.ts +31 -0
  59. package/templates/react-tailwind/src/schema.ts +6 -12
  60. package/templates/react-tailwind/src/vite-env.d.ts +17 -0
  61. package/templates/react-tailwind/{tsconfig.json → tsconfig.json.hbs} +1 -2
  62. package/templates/react-tailwind/vite.config.ts +57 -50
  63. package/templates/react-tailwind-sync/.env.example +19 -0
  64. package/templates/react-tailwind-sync/README.md.hbs +43 -1
  65. package/templates/react-tailwind-sync/package.json.hbs +3 -0
  66. package/templates/react-tailwind-sync/server.ts +90 -26
  67. package/templates/react-tailwind-sync/src/App.tsx +246 -222
  68. package/templates/react-tailwind-sync/src/auth.ts +25 -0
  69. package/templates/react-tailwind-sync/src/index.css +3 -3
  70. package/templates/react-tailwind-sync/src/kora-worker.ts +0 -1
  71. package/templates/react-tailwind-sync/src/main.tsx +48 -18
  72. package/templates/react-tailwind-sync/src/modules/todos/todo.mutations.ts +25 -0
  73. package/templates/react-tailwind-sync/src/modules/todos/todo.queries.ts +5 -0
  74. package/templates/react-tailwind-sync/src/modules/todos/todo.schema.ts +10 -0
  75. package/templates/react-tailwind-sync/src/modules/todos/useTodos.ts +31 -0
  76. package/templates/react-tailwind-sync/src/schema.ts +6 -12
  77. package/templates/react-tailwind-sync/src/vite-env.d.ts +17 -0
  78. package/templates/react-tailwind-sync/{tsconfig.json → tsconfig.json.hbs} +1 -2
  79. package/templates/react-tailwind-sync/vite.config.ts +67 -60
  80. package/templates/tauri-react/.env.example +20 -0
  81. package/templates/tauri-react/README.md.hbs +36 -1
  82. package/templates/tauri-react/dev.ts +11 -11
  83. package/templates/tauri-react/package.json.hbs +2 -0
  84. package/templates/tauri-react/server.ts +86 -12
  85. package/templates/tauri-react/src/App.tsx +769 -544
  86. package/templates/tauri-react/src/AppShell.tsx +125 -83
  87. package/templates/tauri-react/src/SetupScreen.tsx +257 -208
  88. package/templates/tauri-react/src/auth.ts +45 -0
  89. package/templates/tauri-react/src/main.tsx +3 -3
  90. package/templates/tauri-react/src/modules/todos/todo.mutations.ts +25 -0
  91. package/templates/tauri-react/src/modules/todos/todo.queries.ts +5 -0
  92. package/templates/tauri-react/src/modules/todos/todo.schema.ts +10 -0
  93. package/templates/tauri-react/src/modules/todos/useTodos.ts +31 -0
  94. package/templates/tauri-react/src/schema.ts +6 -12
  95. package/templates/tauri-react/src/sync-config.ts +47 -47
  96. package/templates/tauri-react/src/updater.ts +15 -15
  97. package/templates/tauri-react/src/vite-env.d.ts +17 -0
  98. package/templates/tauri-react/src-tauri/tauri.conf.json +2 -2
  99. package/templates/tauri-react/tsconfig.json.hbs +14 -0
  100. package/templates/tauri-react/vite.config.ts +9 -9
  101. package/dist/chunk-HNVCNPER.js.map +0 -1
  102. package/dist/chunk-MIMLCXSX.js.map +0 -1
  103. package/dist/chunk-ULTO5NFA.js.map +0 -1
  104. package/templates/tauri-react/tsconfig.json +0 -15
package/dist/bin.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createCommand
4
- } from "./chunk-ULTO5NFA.js";
4
+ } from "./chunk-PSRM56B7.js";
5
5
  import {
6
6
  deployCommand,
7
7
  generateTypes
8
- } from "./chunk-MIMLCXSX.js";
8
+ } from "./chunk-SOTZIWIF.js";
9
9
  import {
10
10
  DevServerError,
11
11
  InvalidProjectError,
@@ -16,21 +16,187 @@ import {
16
16
  hasTsxInstalled,
17
17
  promptConfirm,
18
18
  resolveProjectBinaryEntryPoint
19
- } from "./chunk-HNVCNPER.js";
19
+ } from "./chunk-3WNFM3QB.js";
20
20
 
21
21
  // src/bin.ts
22
- import { defineCommand as defineCommand4, runMain } from "citty";
22
+ import { defineCommand as defineCommand9, runMain } from "citty";
23
23
 
24
- // src/commands/dev/dev-command.ts
25
- import { access as access2 } from "fs/promises";
26
- import { join as join3 } from "path";
27
- import { resolve } from "path";
24
+ // src/commands/backup/backup-command.ts
25
+ import { readFile, writeFile } from "fs/promises";
26
+ import "path";
28
27
  import { defineCommand } from "citty";
28
+ var DEFAULT_SYNC_PORT = 3001;
29
+ var backupCommand = defineCommand({
30
+ meta: {
31
+ name: "backup",
32
+ description: "Backup and restore Kora sync server data"
33
+ },
34
+ subCommands: {
35
+ create: defineCommand({
36
+ meta: {
37
+ name: "create",
38
+ description: "Create a backup of the sync server"
39
+ },
40
+ args: {
41
+ url: {
42
+ type: "string",
43
+ description: "Sync server URL (default: http://localhost:3001)",
44
+ default: `http://localhost:${DEFAULT_SYNC_PORT}`
45
+ },
46
+ out: {
47
+ type: "string",
48
+ description: "Output file path (default: kora-backup-<timestamp>.kora)"
49
+ },
50
+ token: {
51
+ type: "string",
52
+ description: "Backup token (defaults to KORA_BACKUP_TOKEN or KORA_ADMIN_TOKEN)"
53
+ }
54
+ },
55
+ async run({ args }) {
56
+ const logger = createLogger();
57
+ const url = typeof args.url === "string" ? args.url : `http://localhost:${DEFAULT_SYNC_PORT}`;
58
+ const outFile = typeof args.out === "string" ? args.out : `kora-backup-${Date.now()}.kora`;
59
+ const token = typeof args.token === "string" ? args.token : process.env.KORA_BACKUP_TOKEN ?? process.env.KORA_ADMIN_TOKEN;
60
+ logger.banner();
61
+ logger.info(`Exporting backup from ${url}...`);
62
+ try {
63
+ const backupUrl = `${url.replace(/\/$/, "")}/__kora/backup/export`;
64
+ const response = await fetch(backupUrl, {
65
+ method: "POST",
66
+ headers: token ? { Authorization: `Bearer ${token}` } : void 0
67
+ });
68
+ if (!response.ok) {
69
+ const error = await response.json().catch(() => ({ message: response.statusText }));
70
+ throw new Error(
71
+ `Backup failed: ${error.message ?? response.statusText}`
72
+ );
73
+ }
74
+ const buffer = await response.arrayBuffer();
75
+ await writeFile(outFile, new Uint8Array(buffer));
76
+ const size = (buffer.byteLength / 1024).toFixed(1);
77
+ logger.success(`Backup saved to ${outFile} (${size} KB)`);
78
+ } catch (error) {
79
+ logger.error("Backup failed");
80
+ if (error instanceof Error) logger.error(error.message);
81
+ logger.blank();
82
+ logger.step("Make sure the Kora sync server is running.");
83
+ process.exit(1);
84
+ }
85
+ }
86
+ }),
87
+ restore: defineCommand({
88
+ meta: {
89
+ name: "restore",
90
+ description: "Restore a backup to the sync server"
91
+ },
92
+ args: {
93
+ file: {
94
+ type: "string",
95
+ description: "Backup file path",
96
+ required: true
97
+ },
98
+ url: {
99
+ type: "string",
100
+ description: "Sync server URL (default: http://localhost:3001)",
101
+ default: `http://localhost:${DEFAULT_SYNC_PORT}`
102
+ },
103
+ merge: {
104
+ type: "boolean",
105
+ description: "Merge with existing data instead of replacing",
106
+ default: false
107
+ },
108
+ token: {
109
+ type: "string",
110
+ description: "Backup token (defaults to KORA_BACKUP_TOKEN or KORA_ADMIN_TOKEN)"
111
+ }
112
+ },
113
+ async run({ args }) {
114
+ const logger = createLogger();
115
+ const filePath = typeof args.file === "string" ? args.file : "";
116
+ const url = typeof args.url === "string" ? args.url : `http://localhost:${DEFAULT_SYNC_PORT}`;
117
+ const merge = args.merge === true;
118
+ const token = typeof args.token === "string" ? args.token : process.env.KORA_BACKUP_TOKEN ?? process.env.KORA_ADMIN_TOKEN;
119
+ logger.banner();
120
+ logger.info(`Restoring backup from ${filePath} to ${url}...`);
121
+ try {
122
+ const data = await readFile(filePath);
123
+ const restoreUrl = `${url.replace(/\/$/, "")}/__kora/backup/import?merge=${merge}`;
124
+ const response = await fetch(restoreUrl, {
125
+ method: "POST",
126
+ headers: token ? { Authorization: `Bearer ${token}` } : void 0,
127
+ body: data
128
+ });
129
+ if (!response.ok) {
130
+ const error = await response.json().catch(() => ({ message: response.statusText }));
131
+ throw new Error(
132
+ `Restore failed: ${error.message ?? response.statusText}`
133
+ );
134
+ }
135
+ const result = await response.json();
136
+ if (result.success) {
137
+ logger.success(
138
+ `Restored ${result.operationsRestored} operations${result.duration ? ` in ${result.duration}ms` : ""}`
139
+ );
140
+ } else {
141
+ logger.error("Restore completed with errors");
142
+ }
143
+ } catch (error) {
144
+ logger.error("Restore failed");
145
+ if (error instanceof Error) logger.error(error.message);
146
+ process.exit(1);
147
+ }
148
+ }
149
+ }),
150
+ info: defineCommand({
151
+ meta: {
152
+ name: "info",
153
+ description: "Show backup file information"
154
+ },
155
+ args: {
156
+ file: {
157
+ type: "string",
158
+ description: "Backup file path",
159
+ required: true
160
+ }
161
+ },
162
+ async run({ args }) {
163
+ const logger = createLogger();
164
+ const filePath = typeof args.file === "string" ? args.file : "";
165
+ try {
166
+ const data = await readFile(filePath);
167
+ const { readBackupManifest } = await import("@korajs/store");
168
+ const manifest = readBackupManifest(
169
+ new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
170
+ );
171
+ logger.banner();
172
+ logger.info(`Backup: ${filePath}`);
173
+ logger.blank();
174
+ logger.step(`Created: ${new Date(manifest.createdAt).toISOString()}`);
175
+ logger.step(`Node ID: ${manifest.nodeId}`);
176
+ logger.step(`Schema: v${manifest.schemaVersion}`);
177
+ logger.step(`Operations: ${manifest.operationCount.toLocaleString()}`);
178
+ logger.step(`Collections: ${manifest.collections.join(", ") || "(none)"}`);
179
+ logger.step(`Checksum: ${manifest.checksum.slice(0, 16)}...`);
180
+ logger.blank();
181
+ logger.step(`Format version: ${manifest.version}`);
182
+ } catch (error) {
183
+ logger.error("Failed to read backup file");
184
+ if (error instanceof Error) logger.error(error.message);
185
+ process.exit(1);
186
+ }
187
+ }
188
+ })
189
+ }
190
+ });
191
+
192
+ // src/commands/compact/compact-command.ts
193
+ import { resolve } from "path";
194
+ import { defineCommand as defineCommand2 } from "citty";
29
195
 
30
196
  // src/commands/dev/kora-config.ts
31
197
  import { spawn } from "child_process";
32
198
  import { access } from "fs/promises";
33
- import { extname, join } from "path";
199
+ import { extname, join as join2 } from "path";
34
200
  import { pathToFileURL } from "url";
35
201
  var CONFIG_CANDIDATES = [
36
202
  "kora.config.ts",
@@ -53,7 +219,7 @@ async function loadKoraConfig(projectRoot) {
53
219
  }
54
220
  async function findKoraConfigFile(projectRoot) {
55
221
  for (const file of CONFIG_CANDIDATES) {
56
- const candidate = join(projectRoot, file);
222
+ const candidate = join2(projectRoot, file);
57
223
  try {
58
224
  await access(candidate);
59
225
  return candidate;
@@ -81,7 +247,7 @@ async function loadTypeScriptConfig(configPath, projectRoot) {
81
247
  }
82
248
  }
83
249
  async function runCommand(command, args, cwd) {
84
- return await new Promise((resolve4, reject) => {
250
+ return await new Promise((resolve6, reject) => {
85
251
  const child = spawn(command, args, {
86
252
  cwd,
87
253
  stdio: ["ignore", "pipe", "pipe"],
@@ -100,7 +266,7 @@ async function runCommand(command, args, cwd) {
100
266
  });
101
267
  child.on("exit", (code) => {
102
268
  if (code === 0) {
103
- resolve4(stdout.trim());
269
+ resolve6(stdout.trim());
104
270
  return;
105
271
  }
106
272
  reject(new Error(`Failed to load kora config (exit ${code ?? "unknown"}): ${stderr.trim()}`));
@@ -118,6 +284,156 @@ function toConfigObject(mod) {
118
284
  return value;
119
285
  }
120
286
 
287
+ // src/commands/migrate/schema-loader.ts
288
+ import { spawn as spawn2 } from "child_process";
289
+ import { extname as extname2 } from "path";
290
+ import { pathToFileURL as pathToFileURL2 } from "url";
291
+ async function loadSchemaDefinition(schemaPath, projectRoot) {
292
+ const ext = extname2(schemaPath);
293
+ const moduleValue = ext === ".ts" || ext === ".mts" || ext === ".cts" ? await loadTypeScriptModule(schemaPath, projectRoot) : await import(`${pathToFileURL2(schemaPath).href}?t=${Date.now()}-${Math.random()}`);
294
+ return extractSchema(moduleValue);
295
+ }
296
+ function extractSchema(value) {
297
+ if (typeof value !== "object" || value === null) {
298
+ throw new Error("Schema module must export an object.");
299
+ }
300
+ const moduleRecord = value;
301
+ const candidate = moduleRecord.default ?? moduleRecord;
302
+ if (!isSchemaDefinition(candidate)) {
303
+ throw new Error("Schema module must export a valid SchemaDefinition as default export.");
304
+ }
305
+ return candidate;
306
+ }
307
+ function isSchemaDefinition(value) {
308
+ if (typeof value !== "object" || value === null) return false;
309
+ const object = value;
310
+ return typeof object.version === "number" && typeof object.collections === "object" && object.collections !== null && typeof object.relations === "object" && object.relations !== null;
311
+ }
312
+ async function loadTypeScriptModule(schemaPath, projectRoot) {
313
+ if (!await hasTsxInstalled(projectRoot)) {
314
+ throw new Error(
315
+ `Schema file is TypeScript (${schemaPath}) but local "tsx" was not found. Install tsx in the project.`
316
+ );
317
+ }
318
+ const script = "const modulePath = process.argv[process.argv.length - 1];import('node:url').then(u => import(u.pathToFileURL(modulePath).href)).then(mod => { const v = mod.default ?? mod; process.stdout.write(JSON.stringify(v)) }).catch(e => { process.stderr.write(String(e)); process.exit(1) })";
319
+ const output = await runCommand2(
320
+ process.execPath,
321
+ ["--import", "tsx", "--eval", script, schemaPath],
322
+ projectRoot
323
+ );
324
+ try {
325
+ return JSON.parse(output);
326
+ } catch {
327
+ throw new Error(`Failed to parse schema module output for ${schemaPath}`);
328
+ }
329
+ }
330
+ async function runCommand2(command, args, cwd) {
331
+ return await new Promise((resolve6, reject) => {
332
+ const child = spawn2(command, args, {
333
+ cwd,
334
+ stdio: ["ignore", "pipe", "pipe"],
335
+ env: process.env
336
+ });
337
+ let stdout = "";
338
+ let stderr = "";
339
+ child.stdout?.on("data", (chunk) => {
340
+ stdout += chunk.toString("utf-8");
341
+ });
342
+ child.stderr?.on("data", (chunk) => {
343
+ stderr += chunk.toString("utf-8");
344
+ });
345
+ child.on("error", (error) => {
346
+ reject(error);
347
+ });
348
+ child.on("exit", (code) => {
349
+ if (code === 0) {
350
+ resolve6(stdout.trim());
351
+ return;
352
+ }
353
+ reject(
354
+ new Error(`Failed to load TypeScript schema (exit ${code ?? "unknown"}): ${stderr.trim()}`)
355
+ );
356
+ });
357
+ });
358
+ }
359
+
360
+ // src/commands/compact/compact-command.ts
361
+ var compactCommand = defineCommand2({
362
+ meta: {
363
+ name: "compact",
364
+ description: "Compact the local Kora operation log after server ack"
365
+ },
366
+ args: {
367
+ db: {
368
+ type: "string",
369
+ description: "Path to the local SQLite database (required)"
370
+ },
371
+ schema: {
372
+ type: "string",
373
+ description: "Path to schema file"
374
+ },
375
+ strategy: {
376
+ type: "string",
377
+ description: "Compaction strategy: after-ack | after-days | never",
378
+ default: "after-ack"
379
+ },
380
+ days: {
381
+ type: "string",
382
+ description: "Age threshold when strategy is after-days",
383
+ default: "30"
384
+ }
385
+ },
386
+ async run({ args }) {
387
+ const logger = createLogger();
388
+ const projectRoot = await findProjectRoot();
389
+ if (!projectRoot) {
390
+ throw new InvalidProjectError(process.cwd());
391
+ }
392
+ const dbPath = typeof args.db === "string" ? resolve(projectRoot, args.db) : void 0;
393
+ if (!dbPath) {
394
+ throw new Error("Missing --db <path> to the local SQLite database file.");
395
+ }
396
+ const config = await loadKoraConfig(projectRoot);
397
+ const schemaPath = typeof args.schema === "string" ? resolve(projectRoot, args.schema) : typeof config?.schema === "string" ? resolve(projectRoot, config.schema) : void 0;
398
+ if (!schemaPath) {
399
+ throw new Error("Could not resolve schema path. Pass --schema <path>.");
400
+ }
401
+ const schema = await loadSchemaDefinition(schemaPath, projectRoot);
402
+ const { BetterSqlite3Adapter } = await import("@korajs/store/better-sqlite3");
403
+ const { Store } = await import("@korajs/store");
404
+ const strategyName = typeof args.strategy === "string" ? args.strategy : "after-ack";
405
+ const days = Number.parseInt(typeof args.days === "string" ? args.days : "30", 10);
406
+ const store = new Store({
407
+ schema,
408
+ adapter: new BetterSqlite3Adapter(dbPath)
409
+ });
410
+ await store.open();
411
+ let result;
412
+ if (strategyName === "never") {
413
+ result = await store.compact({ mode: "never" });
414
+ } else if (strategyName === "after-days") {
415
+ result = await store.compact({ mode: "after-days", days });
416
+ } else if (strategyName === "after-ack") {
417
+ result = await store.compact({ mode: "after-ack" });
418
+ } else {
419
+ await store.close();
420
+ throw new Error(`Unknown strategy "${strategyName}". Use after-ack, after-days, or never.`);
421
+ }
422
+ await store.close();
423
+ logger.banner();
424
+ logger.success(`Compacted ${result.deletedCount} operation log entries.`);
425
+ for (const [nodeId, seq] of result.watermark) {
426
+ logger.step(` ${nodeId}: seq <= ${seq}`);
427
+ }
428
+ }
429
+ });
430
+
431
+ // src/commands/dev/dev-command.ts
432
+ import { access as access2 } from "fs/promises";
433
+ import { join as join4 } from "path";
434
+ import { resolve as resolve2 } from "path";
435
+ import { defineCommand as defineCommand3 } from "citty";
436
+
121
437
  // src/commands/dev/process-manager.ts
122
438
  import { spawn as spawnChild } from "child_process";
123
439
  var ProcessManager = class {
@@ -132,8 +448,8 @@ var ProcessManager = class {
132
448
  let resolveExit;
133
449
  const runningProcess = {
134
450
  child,
135
- exitPromise: new Promise((resolve4) => {
136
- resolveExit = resolve4;
451
+ exitPromise: new Promise((resolve6) => {
452
+ resolveExit = resolve6;
137
453
  }),
138
454
  stdoutBuffer: "",
139
455
  stderrBuffer: ""
@@ -200,15 +516,15 @@ var ProcessManager = class {
200
516
  }
201
517
  };
202
518
  function delay(ms) {
203
- return new Promise((resolve4) => {
204
- setTimeout(resolve4, ms);
519
+ return new Promise((resolve6) => {
520
+ setTimeout(resolve6, ms);
205
521
  });
206
522
  }
207
523
 
208
524
  // src/commands/dev/schema-watcher.ts
209
- import { spawn as spawn2 } from "child_process";
525
+ import { spawn as spawn3 } from "child_process";
210
526
  import { watch } from "fs";
211
- import { join as join2 } from "path";
527
+ import { join as join3 } from "path";
212
528
  var SchemaWatcher = class {
213
529
  constructor(config) {
214
530
  this.config = config;
@@ -236,7 +552,14 @@ var SchemaWatcher = class {
236
552
  this.watcher = null;
237
553
  }
238
554
  async regenerate() {
239
- const koraBinJs = join2(this.config.projectRoot, "node_modules", "@korajs", "cli", "dist", "bin.js");
555
+ const koraBinJs = join3(
556
+ this.config.projectRoot,
557
+ "node_modules",
558
+ "@korajs",
559
+ "cli",
560
+ "dist",
561
+ "bin.js"
562
+ );
240
563
  const hasTsx = await hasTsxInstalled(this.config.projectRoot);
241
564
  const command = process.execPath;
242
565
  const args = hasTsx ? ["--import", "tsx", koraBinJs, "generate", "types", "--schema", this.config.schemaPath] : [koraBinJs, "generate", "types", "--schema", this.config.schemaPath];
@@ -256,8 +579,8 @@ var SchemaWatcher = class {
256
579
  }
257
580
  };
258
581
  async function spawnCommand(command, args, cwd) {
259
- await new Promise((resolve4, reject) => {
260
- const child = spawn2(command, args, {
582
+ await new Promise((resolve6, reject) => {
583
+ const child = spawn3(command, args, {
261
584
  cwd,
262
585
  stdio: ["ignore", "pipe", "pipe"],
263
586
  env: process.env
@@ -273,7 +596,7 @@ async function spawnCommand(command, args, cwd) {
273
596
  });
274
597
  child.on("exit", (code) => {
275
598
  if (code === 0) {
276
- resolve4();
599
+ resolve6();
277
600
  return;
278
601
  }
279
602
  reject(new Error(`Type generation exited with code ${code ?? "unknown"}.`));
@@ -295,7 +618,7 @@ function toError(error) {
295
618
  }
296
619
 
297
620
  // src/commands/dev/dev-command.ts
298
- var devCommand = defineCommand({
621
+ var devCommand = defineCommand3({
299
622
  meta: {
300
623
  name: "dev",
301
624
  description: "Start the Kora development environment"
@@ -335,7 +658,7 @@ var devCommand = defineCommand({
335
658
  const watchDebounceMs = typeof config?.dev?.watch === "object" && typeof config.dev.watch.debounceMs === "number" ? config.dev.watch.debounceMs : 300;
336
659
  const viteEntryPoint = await resolveProjectBinaryEntryPoint(projectRoot, "vite", "vite");
337
660
  if (!viteEntryPoint) {
338
- throw new DevServerError("vite", join3(projectRoot, "node_modules", ".bin", "vite"));
661
+ throw new DevServerError("vite", join4(projectRoot, "node_modules", ".bin", "vite"));
339
662
  }
340
663
  const syncServerFile = await findSyncServerFile(projectRoot);
341
664
  let managedSyncStore = normalizeManagedSyncStore(config, projectRoot);
@@ -351,7 +674,7 @@ var devCommand = defineCommand({
351
674
  }
352
675
  if (shouldStartSync && syncServerFile === null && managedSyncStore) {
353
676
  const hasServerPackage = await fileExists(
354
- join3(projectRoot, "node_modules", "@korajs", "server", "package.json")
677
+ join4(projectRoot, "node_modules", "@korajs", "server", "package.json")
355
678
  );
356
679
  if (!hasServerPackage) {
357
680
  logger.warn(
@@ -368,11 +691,13 @@ var devCommand = defineCommand({
368
691
  }
369
692
  let configuredSchemaPath = null;
370
693
  if (typeof config?.schema === "string") {
371
- const candidate = resolve(projectRoot, config.schema);
694
+ const candidate = resolve2(projectRoot, config.schema);
372
695
  if (await fileExists(candidate)) {
373
696
  configuredSchemaPath = candidate;
374
697
  } else {
375
- logger.warn(`Configured schema file not found: ${candidate}. Falling back to auto-detection.`);
698
+ logger.warn(
699
+ `Configured schema file not found: ${candidate}. Falling back to auto-detection.`
700
+ );
376
701
  }
377
702
  }
378
703
  const schemaPath = configuredSchemaPath ?? await findSchemaFile(projectRoot);
@@ -381,8 +706,8 @@ var devCommand = defineCommand({
381
706
  let schemaWatcher = null;
382
707
  let shuttingDown = false;
383
708
  let resolveFinished;
384
- const finished = new Promise((resolve4) => {
385
- resolveFinished = resolve4;
709
+ const finished = new Promise((resolve6) => {
710
+ resolveFinished = resolve6;
386
711
  });
387
712
  const onManagedProcessExit = () => {
388
713
  if (!processManager.hasRunning() && !shuttingDown) {
@@ -425,11 +750,15 @@ var devCommand = defineCommand({
425
750
  logger.step(" Schema watcher disabled (schema.ts not found)");
426
751
  }
427
752
  logger.blank();
753
+ logger.step(" Kora DevTools overlay: Ctrl+Shift+K (Cmd+Shift+K on macOS) when devtools: true");
428
754
  processManager.spawn({
429
755
  label: "vite",
430
756
  command: process.execPath,
431
757
  args: [viteEntryPoint, "--port", String(vitePort)],
432
758
  cwd: projectRoot,
759
+ env: {
760
+ KORA_DEV: "1"
761
+ },
433
762
  onExit: onManagedProcessExit
434
763
  });
435
764
  if (shouldStartSync && hasTsx && syncServerFile) {
@@ -480,7 +809,7 @@ var devCommand = defineCommand({
480
809
  }
481
810
  });
482
811
  async function findSyncServerFile(projectRoot) {
483
- const candidates = [join3(projectRoot, "server.ts"), join3(projectRoot, "server.js")];
812
+ const candidates = [join4(projectRoot, "server.ts"), join4(projectRoot, "server.js")];
484
813
  for (const candidate of candidates) {
485
814
  try {
486
815
  await access2(candidate);
@@ -504,7 +833,7 @@ function normalizeManagedSyncStore(config, projectRoot) {
504
833
  const store = sync.store;
505
834
  if (store === void 0) return { type: "memory" };
506
835
  if (store === "memory") return { type: "memory" };
507
- if (store === "sqlite") return { type: "sqlite", filename: join3(projectRoot, "kora-sync.db") };
836
+ if (store === "sqlite") return { type: "sqlite", filename: join4(projectRoot, "kora-sync.db") };
508
837
  if (store === "postgres") {
509
838
  const connectionString = process.env.DATABASE_URL;
510
839
  if (!connectionString) return null;
@@ -513,7 +842,7 @@ function normalizeManagedSyncStore(config, projectRoot) {
513
842
  if (typeof store === "object" && store !== null) {
514
843
  if (store.type === "memory") return { type: "memory" };
515
844
  if (store.type === "sqlite") {
516
- const filename = typeof store.filename === "string" && store.filename.length > 0 ? resolve(projectRoot, store.filename) : join3(projectRoot, "kora-sync.db");
845
+ const filename = typeof store.filename === "string" && store.filename.length > 0 ? resolve2(projectRoot, store.filename) : join4(projectRoot, "kora-sync.db");
517
846
  return { type: "sqlite", filename };
518
847
  }
519
848
  if (store.type === "postgres" && typeof store.connectionString === "string") {
@@ -526,7 +855,8 @@ function isPostgresEnvRequested(config) {
526
855
  const sync = config?.dev?.sync;
527
856
  if (typeof sync !== "object" || sync === null) return false;
528
857
  if (sync.store === "postgres") return true;
529
- if (typeof sync.store === "object" && sync.store !== null && sync.store.type === "postgres") return true;
858
+ if (typeof sync.store === "object" && sync.store !== null && sync.store.type === "postgres")
859
+ return true;
530
860
  return false;
531
861
  }
532
862
  var MANAGED_SYNC_BOOTSTRAP_SCRIPT = `
@@ -575,17 +905,387 @@ process.stdout.write('Managed sync server running on ws://localhost:' + String(c
575
905
  await new Promise(() => {});
576
906
  `;
577
907
 
908
+ // src/commands/doctor/doctor-command.ts
909
+ import { defineCommand as defineCommand4 } from "citty";
910
+
911
+ // src/commands/doctor/doctor-checks.ts
912
+ import { access as access3, readFile as readFile2 } from "fs/promises";
913
+ import { join as join5, resolve as resolve3 } from "path";
914
+ var STATUS_ENDPOINT = "/__kora/status";
915
+ async function runDoctorChecks(options) {
916
+ const { projectRoot } = options;
917
+ const results = [];
918
+ results.push({
919
+ id: "project-root",
920
+ title: "Project root",
921
+ status: "pass",
922
+ message: projectRoot
923
+ });
924
+ const config = await loadKoraConfig(projectRoot);
925
+ if (!config) {
926
+ results.push({
927
+ id: "kora-config",
928
+ title: "kora.config",
929
+ status: "warn",
930
+ message: "No kora.config.* file found.",
931
+ fix: "Add kora.config.ts with schema path and dev settings (see create-kora-app templates)."
932
+ });
933
+ } else {
934
+ results.push({
935
+ id: "kora-config",
936
+ title: "kora.config",
937
+ status: "pass",
938
+ message: "Configuration file loaded."
939
+ });
940
+ }
941
+ const schemaPath = await resolveSchemaPath(projectRoot, config?.schema);
942
+ if (!schemaPath) {
943
+ results.push({
944
+ id: "schema",
945
+ title: "Schema",
946
+ status: "fail",
947
+ message: "Could not find schema.ts in src/ or project root.",
948
+ fix: "Add src/schema.ts or set schema in kora.config.ts."
949
+ });
950
+ } else {
951
+ try {
952
+ const schema = await loadSchemaDefinition(schemaPath, projectRoot);
953
+ results.push({
954
+ id: "schema",
955
+ title: "Schema",
956
+ status: "pass",
957
+ message: `Loaded schema v${schema.version} (${Object.keys(schema.collections).length} collection(s)).`
958
+ });
959
+ const workerCheck = await checkWorkerFile(projectRoot);
960
+ results.push(workerCheck);
961
+ const depsCheck = await checkDependencies(projectRoot);
962
+ results.push(depsCheck);
963
+ results.push(opfsBrowserNote());
964
+ if (!options.skipNetwork) {
965
+ const syncUrl = options.syncHttpUrl ?? await resolveSyncHttpUrl(projectRoot, config);
966
+ const syncChecks = await checkSyncServer(syncUrl, schema);
967
+ results.push(...syncChecks);
968
+ } else {
969
+ results.push({
970
+ id: "sync-server",
971
+ title: "Sync server",
972
+ status: "skip",
973
+ message: "Network checks skipped."
974
+ });
975
+ }
976
+ } catch (error) {
977
+ const message = error instanceof Error ? error.message : String(error);
978
+ results.push({
979
+ id: "schema",
980
+ title: "Schema",
981
+ status: "fail",
982
+ message: `Failed to load schema: ${message}`,
983
+ fix: "Ensure the schema exports defineSchema() as default and TypeScript can be evaluated (install tsx)."
984
+ });
985
+ }
986
+ }
987
+ return results;
988
+ }
989
+ async function resolveSchemaPath(projectRoot, configSchema) {
990
+ if (typeof configSchema === "string") {
991
+ const resolved = resolve3(projectRoot, configSchema);
992
+ try {
993
+ await access3(resolved);
994
+ return resolved;
995
+ } catch {
996
+ }
997
+ }
998
+ return findSchemaFile(projectRoot);
999
+ }
1000
+ async function checkWorkerFile(projectRoot) {
1001
+ const candidates = [
1002
+ join5(projectRoot, "src", "kora-worker.ts"),
1003
+ join5(projectRoot, "src", "kora-worker.js")
1004
+ ];
1005
+ for (const path of candidates) {
1006
+ try {
1007
+ await access3(path);
1008
+ return {
1009
+ id: "worker",
1010
+ title: "SQLite WASM worker",
1011
+ status: "pass",
1012
+ message: `Found ${path.replace(projectRoot, ".")}.`
1013
+ };
1014
+ } catch {
1015
+ }
1016
+ }
1017
+ return {
1018
+ id: "worker",
1019
+ title: "SQLite WASM worker",
1020
+ status: "warn",
1021
+ message: "No src/kora-worker.ts found.",
1022
+ fix: "Add kora-worker.ts and pass store.workerUrl from Vite (?worker&url) in createApp()."
1023
+ };
1024
+ }
1025
+ async function checkDependencies(projectRoot) {
1026
+ try {
1027
+ const raw = await readFile2(join5(projectRoot, "package.json"), "utf-8");
1028
+ const pkg = JSON.parse(raw);
1029
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1030
+ const hasKora = Boolean(deps.korajs || deps.kora);
1031
+ const hasStore = Boolean(deps["@korajs/store"]);
1032
+ if (hasKora || hasStore) {
1033
+ return {
1034
+ id: "dependencies",
1035
+ title: "Dependencies",
1036
+ status: "pass",
1037
+ message: hasKora ? "korajs is listed in package.json." : "@korajs/store is listed in package.json."
1038
+ };
1039
+ }
1040
+ return {
1041
+ id: "dependencies",
1042
+ title: "Dependencies",
1043
+ status: "fail",
1044
+ message: "No korajs or @korajs/store dependency found.",
1045
+ fix: "Run pnpm add korajs @korajs/react (or your package manager equivalent)."
1046
+ };
1047
+ } catch {
1048
+ return {
1049
+ id: "dependencies",
1050
+ title: "Dependencies",
1051
+ status: "fail",
1052
+ message: "Could not read package.json.",
1053
+ fix: "Run this command from a Kora app directory with a valid package.json."
1054
+ };
1055
+ }
1056
+ }
1057
+ function opfsBrowserNote() {
1058
+ return {
1059
+ id: "opfs",
1060
+ title: "OPFS persistence",
1061
+ status: "skip",
1062
+ message: "OPFS availability is checked in the browser at runtime. Open the app and confirm no IndexedDB fallback warning in the console.",
1063
+ fix: "Use a Chromium-based browser with storage access; avoid private mode if persistence fails."
1064
+ };
1065
+ }
1066
+ async function resolveSyncHttpUrl(projectRoot, config) {
1067
+ const envUrl = await readEnvSyncUrl(projectRoot);
1068
+ if (envUrl) {
1069
+ return toSyncStatusHttpBase(envUrl);
1070
+ }
1071
+ const syncPort = typeof config?.dev?.sync === "object" && typeof config.dev.sync.port === "number" ? config.dev.sync.port : 3001;
1072
+ return `http://localhost:${syncPort}`;
1073
+ }
1074
+ async function readEnvSyncUrl(projectRoot) {
1075
+ for (const file of [".env", ".env.local"]) {
1076
+ const path = join5(projectRoot, file);
1077
+ try {
1078
+ const content = await readFile2(path, "utf-8");
1079
+ for (const line of content.split("\n")) {
1080
+ const trimmed = line.trim();
1081
+ if (!trimmed || trimmed.startsWith("#")) continue;
1082
+ const match = trimmed.match(/^VITE_SYNC_URL=(.+)$/);
1083
+ if (match?.[1]) {
1084
+ return match[1].trim().replace(/^["']|["']$/g, "");
1085
+ }
1086
+ }
1087
+ } catch {
1088
+ }
1089
+ }
1090
+ return null;
1091
+ }
1092
+ function toSyncStatusHttpBase(url) {
1093
+ try {
1094
+ const parsed = new URL(url);
1095
+ if (parsed.protocol === "ws:") {
1096
+ parsed.protocol = "http:";
1097
+ } else if (parsed.protocol === "wss:") {
1098
+ parsed.protocol = "https:";
1099
+ }
1100
+ parsed.pathname = "";
1101
+ parsed.search = "";
1102
+ parsed.hash = "";
1103
+ return parsed.toString().replace(/\/$/, "");
1104
+ } catch {
1105
+ return "http://localhost:3001";
1106
+ }
1107
+ }
1108
+ async function checkSyncServer(httpBase, schema) {
1109
+ const statusUrl = `${httpBase.replace(/\/$/, "")}${STATUS_ENDPOINT}`;
1110
+ try {
1111
+ const response = await fetch(statusUrl, {
1112
+ signal: AbortSignal.timeout(5e3)
1113
+ });
1114
+ if (!response.ok) {
1115
+ return [
1116
+ {
1117
+ id: "sync-server",
1118
+ title: "Sync server",
1119
+ status: "fail",
1120
+ message: `GET ${statusUrl} returned ${response.status}.`,
1121
+ fix: "Start the sync server with `kora dev` or `pnpm run dev` in a sync template."
1122
+ }
1123
+ ];
1124
+ }
1125
+ const body = await response.json();
1126
+ const results = [
1127
+ {
1128
+ id: "sync-server",
1129
+ title: "Sync server",
1130
+ status: "pass",
1131
+ message: `Reachable at ${statusUrl} (server v${body.version ?? "unknown"}).`
1132
+ }
1133
+ ];
1134
+ if (typeof body.schemaVersion === "number" && body.schemaVersion !== schema.version) {
1135
+ results.push({
1136
+ id: "schema-version",
1137
+ title: "Schema version",
1138
+ status: "warn",
1139
+ message: `Client schema v${schema.version} differs from server schema v${body.schemaVersion}.`,
1140
+ fix: "Run migrations on the server and client, or align schema.version before syncing."
1141
+ });
1142
+ } else {
1143
+ results.push({
1144
+ id: "schema-version",
1145
+ title: "Schema version",
1146
+ status: "pass",
1147
+ message: `Client and server both use schema v${schema.version}.`
1148
+ });
1149
+ }
1150
+ return results;
1151
+ } catch (error) {
1152
+ const detail = error instanceof Error ? error.message : String(error);
1153
+ return [
1154
+ {
1155
+ id: "sync-server",
1156
+ title: "Sync server",
1157
+ status: "warn",
1158
+ message: `Could not reach ${statusUrl}: ${detail}`,
1159
+ fix: "Start the sync server, or pass --skip-network for local-only checks."
1160
+ }
1161
+ ];
1162
+ }
1163
+ }
1164
+ function doctorHasFailures(results) {
1165
+ return results.some((r) => r.status === "fail");
1166
+ }
1167
+
1168
+ // src/commands/doctor/doctor-command.ts
1169
+ var doctorCommand = defineCommand4({
1170
+ meta: {
1171
+ name: "doctor",
1172
+ description: "Check Kora project setup (schema, worker, sync server, versions)"
1173
+ },
1174
+ args: {
1175
+ url: {
1176
+ type: "string",
1177
+ description: "Sync server HTTP base URL for status probe (default: from .env or localhost:3001)"
1178
+ },
1179
+ "skip-network": {
1180
+ type: "boolean",
1181
+ description: "Skip sync server and schema version network checks",
1182
+ default: false
1183
+ }
1184
+ },
1185
+ async run({ args }) {
1186
+ const logger = createLogger();
1187
+ const projectRoot = await findProjectRoot();
1188
+ if (!projectRoot) {
1189
+ throw new InvalidProjectError(process.cwd());
1190
+ }
1191
+ const syncHttpUrl = typeof args.url === "string" ? args.url : void 0;
1192
+ const skipNetwork = args["skip-network"] === true;
1193
+ logger.banner();
1194
+ logger.info(`Checking project at ${projectRoot}`);
1195
+ logger.blank();
1196
+ const results = await runDoctorChecks({
1197
+ projectRoot,
1198
+ syncHttpUrl,
1199
+ skipNetwork
1200
+ });
1201
+ for (const check of results) {
1202
+ const prefix = check.status === "pass" ? "success" : check.status === "warn" ? "warn" : check.status === "fail" ? "error" : "step";
1203
+ logger[prefix](`${check.title}: ${check.message}`);
1204
+ if (check.fix && check.status !== "pass" && check.status !== "skip") {
1205
+ logger.step(` Fix: ${check.fix}`);
1206
+ }
1207
+ }
1208
+ logger.blank();
1209
+ if (doctorHasFailures(results)) {
1210
+ logger.error("Doctor found blocking issues.");
1211
+ process.exitCode = 1;
1212
+ return;
1213
+ }
1214
+ const warnings = results.filter((r) => r.status === "warn").length;
1215
+ if (warnings > 0) {
1216
+ logger.warn(`Doctor finished with ${warnings} warning(s).`);
1217
+ } else {
1218
+ logger.success("All checks passed.");
1219
+ }
1220
+ }
1221
+ });
1222
+
578
1223
  // src/commands/generate/generate-command.ts
579
- import { mkdir, writeFile } from "fs/promises";
580
- import { dirname, resolve as resolve2 } from "path";
581
- import { defineCommand as defineCommand2 } from "citty";
582
- var generateCommand = defineCommand2({
1224
+ import { mkdir, writeFile as writeFile2 } from "fs/promises";
1225
+ import { dirname, resolve as resolve4 } from "path";
1226
+ import { defineCommand as defineCommand5 } from "citty";
1227
+
1228
+ // src/commands/generate/hook-generator.ts
1229
+ function generateCollectionHooks(schema) {
1230
+ const files = /* @__PURE__ */ new Map();
1231
+ for (const collectionName of Object.keys(schema.collections)) {
1232
+ const pascal = toPascalCase(collectionName);
1233
+ const hookName = `use${pascal}`;
1234
+ const content = [
1235
+ "// Auto-generated by `kora generate hooks` \u2014 safe to edit",
1236
+ `// Schema v${String(schema.version)}`,
1237
+ "",
1238
+ "import { useCollection, useMutation, useQuery } from '@korajs/react'",
1239
+ `import type { ${pascal}InsertInput, ${pascal}Record, ${pascal}UpdateInput } from '../types'`,
1240
+ "",
1241
+ "/**",
1242
+ ` * Collection hook for \`${collectionName}\`.`,
1243
+ " * Add useQuery(...) calls with your QueryBuilder factories.",
1244
+ " */",
1245
+ `export function ${hookName}() {`,
1246
+ ` const ${collectionName} = useCollection('${collectionName}')`,
1247
+ "",
1248
+ ` const insert = useMutation((data: ${pascal}InsertInput) => ${collectionName}.insert(data))`,
1249
+ ` const update = useMutation((id: string, data: ${pascal}UpdateInput) =>`,
1250
+ ` ${collectionName}.update(id, data),`,
1251
+ " )",
1252
+ ` const remove = useMutation((id: string) => ${collectionName}.delete(id))`,
1253
+ "",
1254
+ " return {",
1255
+ ` ${collectionName},`,
1256
+ " insert,",
1257
+ " update,",
1258
+ " remove,",
1259
+ " }",
1260
+ "}",
1261
+ ""
1262
+ ].join("\n");
1263
+ files.set(`${collectionName}.ts`, content);
1264
+ }
1265
+ const indexLines = [
1266
+ "// Auto-generated by `kora generate hooks`",
1267
+ "",
1268
+ ...Object.keys(schema.collections).map((name) => {
1269
+ const pascal = toPascalCase(name);
1270
+ return `export { use${pascal} } from './${name}'`;
1271
+ }),
1272
+ ""
1273
+ ];
1274
+ files.set("index.ts", indexLines.join("\n"));
1275
+ return files;
1276
+ }
1277
+ function toPascalCase(value) {
1278
+ return value.split(/[-_]/).filter((part) => part.length > 0).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
1279
+ }
1280
+
1281
+ // src/commands/generate/generate-command.ts
1282
+ var generateCommand = defineCommand5({
583
1283
  meta: {
584
1284
  name: "generate",
585
1285
  description: "Generate code from your Kora schema"
586
1286
  },
587
1287
  subCommands: {
588
- types: defineCommand2({
1288
+ types: defineCommand5({
589
1289
  meta: {
590
1290
  name: "types",
591
1291
  description: "Generate TypeScript types from your schema"
@@ -609,7 +1309,7 @@ var generateCommand = defineCommand2({
609
1309
  }
610
1310
  let schemaPath;
611
1311
  if (args.schema && typeof args.schema === "string") {
612
- schemaPath = resolve2(args.schema);
1312
+ schemaPath = resolve4(args.schema);
613
1313
  } else {
614
1314
  const found = await findSchemaFile(projectRoot);
615
1315
  if (!found) {
@@ -624,7 +1324,7 @@ var generateCommand = defineCommand2({
624
1324
  }
625
1325
  logger.step(`Reading schema from ${schemaPath}...`);
626
1326
  const schemaModule = await import(schemaPath);
627
- const schema = extractSchema(schemaModule);
1327
+ const schema = extractSchema2(schemaModule);
628
1328
  if (!schema) {
629
1329
  logger.error("Schema file must export a SchemaDefinition as the default export.");
630
1330
  process.exitCode = 1;
@@ -632,31 +1332,193 @@ var generateCommand = defineCommand2({
632
1332
  }
633
1333
  const output = generateTypes(schema);
634
1334
  const outputFile = typeof args.output === "string" ? args.output : "kora/generated/types.ts";
635
- const outputPath = resolve2(projectRoot, outputFile);
1335
+ const outputPath = resolve4(projectRoot, outputFile);
636
1336
  await mkdir(dirname(outputPath), { recursive: true });
637
- await writeFile(outputPath, output, "utf-8");
1337
+ await writeFile2(outputPath, output, "utf-8");
638
1338
  logger.success(`Generated types at ${outputPath}`);
639
1339
  }
1340
+ }),
1341
+ hooks: defineCommand5({
1342
+ meta: {
1343
+ name: "hooks",
1344
+ description: "Generate per-collection React hook stubs"
1345
+ },
1346
+ args: {
1347
+ schema: {
1348
+ type: "string",
1349
+ description: "Path to schema file"
1350
+ },
1351
+ output: {
1352
+ type: "string",
1353
+ description: "Output directory for hooks",
1354
+ default: "kora/generated/hooks"
1355
+ },
1356
+ types: {
1357
+ type: "string",
1358
+ description: "Relative import path to generated types from hook files",
1359
+ default: "../types"
1360
+ }
1361
+ },
1362
+ async run({ args }) {
1363
+ const logger = createLogger();
1364
+ const projectRoot = await findProjectRoot();
1365
+ if (!projectRoot) {
1366
+ throw new InvalidProjectError(process.cwd());
1367
+ }
1368
+ let schemaPath;
1369
+ if (args.schema && typeof args.schema === "string") {
1370
+ schemaPath = resolve4(projectRoot, args.schema);
1371
+ } else {
1372
+ const found = await findSchemaFile(projectRoot);
1373
+ if (!found) {
1374
+ throw new SchemaNotFoundError([
1375
+ "src/schema.ts",
1376
+ "schema.ts",
1377
+ "src/schema.js",
1378
+ "schema.js"
1379
+ ]);
1380
+ }
1381
+ schemaPath = found;
1382
+ }
1383
+ const schema = await loadSchemaDefinition(schemaPath, projectRoot);
1384
+ const outputDir = typeof args.output === "string" ? resolve4(projectRoot, args.output) : resolve4(projectRoot, "kora/generated/hooks");
1385
+ const files = generateCollectionHooks(schema);
1386
+ for (const [name, content] of files) {
1387
+ const adjusted = typeof args.types === "string" && args.types !== "../types" ? content.replace("from '../types'", `from '${args.types}'`) : content;
1388
+ const filePath = resolve4(outputDir, name);
1389
+ await mkdir(dirname(filePath), { recursive: true });
1390
+ await writeFile2(filePath, adjusted, "utf-8");
1391
+ }
1392
+ logger.success(`Generated ${String(files.size)} hook file(s) at ${outputDir}`);
1393
+ logger.step("Run `kora generate types` first if ../types is missing.");
1394
+ }
640
1395
  })
641
1396
  }
642
1397
  });
643
- function extractSchema(mod) {
1398
+ function extractSchema2(mod) {
644
1399
  if (typeof mod !== "object" || mod === null) return null;
645
1400
  const record = mod;
646
1401
  const candidate = record.default ?? record;
647
- if (isSchemaDefinition(candidate)) return candidate;
1402
+ if (isSchemaDefinition2(candidate)) return candidate;
648
1403
  return null;
649
1404
  }
650
- function isSchemaDefinition(value) {
1405
+ function isSchemaDefinition2(value) {
651
1406
  if (typeof value !== "object" || value === null) return false;
652
1407
  const obj = value;
653
1408
  return typeof obj.version === "number" && typeof obj.collections === "object" && obj.collections !== null;
654
1409
  }
655
1410
 
1411
+ // src/commands/logs/logs-command.ts
1412
+ import { defineCommand as defineCommand6 } from "citty";
1413
+ var DEFAULT_SYNC_PORT2 = 3001;
1414
+ var EVENTS_ENDPOINT = "/__kora/events";
1415
+ var logsCommand = defineCommand6({
1416
+ meta: {
1417
+ name: "logs",
1418
+ description: "Stream real-time events from a Kora sync server"
1419
+ },
1420
+ args: {
1421
+ url: {
1422
+ type: "string",
1423
+ description: "Sync server URL (default: http://localhost:3001)",
1424
+ default: `http://localhost:${DEFAULT_SYNC_PORT2}`
1425
+ },
1426
+ follow: {
1427
+ type: "boolean",
1428
+ description: "Follow log output (like tail -f)",
1429
+ default: true,
1430
+ alias: "f"
1431
+ },
1432
+ level: {
1433
+ type: "string",
1434
+ description: "Filter by log level (info, warn, error)"
1435
+ },
1436
+ token: {
1437
+ type: "string",
1438
+ description: "Admin token (defaults to KORA_ADMIN_TOKEN)"
1439
+ }
1440
+ },
1441
+ async run({ args }) {
1442
+ const logger = createLogger();
1443
+ const url = typeof args.url === "string" ? args.url : `http://localhost:${DEFAULT_SYNC_PORT2}`;
1444
+ const follow = args.follow !== false;
1445
+ const levelFilter = typeof args.level === "string" ? args.level : null;
1446
+ const token = typeof args.token === "string" ? args.token : process.env.KORA_ADMIN_TOKEN ?? void 0;
1447
+ const eventsUrl = `${url.replace(/\/$/, "")}${EVENTS_ENDPOINT}`;
1448
+ logger.banner();
1449
+ logger.info(`Connecting to ${eventsUrl}...`);
1450
+ logger.blank();
1451
+ try {
1452
+ const response = await fetch(eventsUrl, {
1453
+ headers: token ? { Authorization: `Bearer ${token}` } : void 0
1454
+ });
1455
+ if (!response.ok) {
1456
+ throw new Error(
1457
+ `Failed to connect to ${eventsUrl}: ${response.status} ${response.statusText}`
1458
+ );
1459
+ }
1460
+ const reader = response.body?.getReader();
1461
+ if (!reader) {
1462
+ throw new Error("Response body is not readable");
1463
+ }
1464
+ const decoder = new TextDecoder();
1465
+ let buffer = "";
1466
+ const processLine = (line) => {
1467
+ if (line.startsWith("event: ")) {
1468
+ return;
1469
+ }
1470
+ if (line.startsWith("data: ")) {
1471
+ const data = line.slice(6);
1472
+ try {
1473
+ const parsed = JSON.parse(data);
1474
+ const level = String(parsed.level ?? "info");
1475
+ if (levelFilter && level !== levelFilter) return;
1476
+ const timestamp = parsed.timestamp ? new Date(parsed.timestamp).toISOString() : "";
1477
+ const event = String(parsed.event ?? "unknown");
1478
+ const nodeId = parsed.nodeId ? ` [${parsed.nodeId}]` : "";
1479
+ const session = parsed.sessionId ? ` <${parsed.sessionId}>` : "";
1480
+ const count = parsed.count ? ` (${parsed.count})` : "";
1481
+ const error = parsed.error ? ` \u2014 ${parsed.error}` : "";
1482
+ const prefix = level === "error" ? "\u2717" : level === "warn" ? "\u26A0" : "\u25CF";
1483
+ const color = level === "error" ? "\x1B[31m" : level === "warn" ? "\x1B[33m" : "\x1B[36m";
1484
+ console.log(
1485
+ `${color}${prefix}${"\x1B[0m"} ${timestamp.slice(11, 23)} ${event}${nodeId}${session}${count}${error}`
1486
+ );
1487
+ } catch {
1488
+ }
1489
+ }
1490
+ };
1491
+ const read = async () => {
1492
+ while (true) {
1493
+ const { done, value } = await reader.read();
1494
+ if (done) break;
1495
+ buffer += decoder.decode(value, { stream: true });
1496
+ const lines = buffer.split("\n");
1497
+ buffer = lines.pop() ?? "";
1498
+ for (const line of lines) {
1499
+ processLine(line.trim());
1500
+ }
1501
+ if (!follow) break;
1502
+ }
1503
+ };
1504
+ await read();
1505
+ } catch (error) {
1506
+ logger.error(`Failed to connect to ${eventsUrl}`);
1507
+ if (error instanceof Error) {
1508
+ logger.error(error.message);
1509
+ }
1510
+ logger.blank();
1511
+ logger.step("Make sure the Kora sync server is running.");
1512
+ logger.step("Start it with: kora dev");
1513
+ process.exit(1);
1514
+ }
1515
+ }
1516
+ });
1517
+
656
1518
  // src/commands/migrate/migrate-command.ts
657
- import { mkdir as mkdir2, readFile, readdir, writeFile as writeFile2 } from "fs/promises";
658
- import { dirname as dirname2, join as join4, resolve as resolve3 } from "path";
659
- import { defineCommand as defineCommand3 } from "citty";
1519
+ import { mkdir as mkdir2, readFile as readFile3, readdir, writeFile as writeFile3 } from "fs/promises";
1520
+ import { dirname as dirname2, join as join6, resolve as resolve5 } from "path";
1521
+ import { defineCommand as defineCommand7 } from "citty";
660
1522
 
661
1523
  // src/commands/migrate/migration-generator.ts
662
1524
  import { generateSQL } from "@korajs/core";
@@ -748,7 +1610,8 @@ function isBreakingChange(change) {
748
1610
  if (change.type === "field-changed") {
749
1611
  if (change.before.kind !== change.after.kind) return true;
750
1612
  if (change.before.itemKind !== change.after.itemKind) return true;
751
- if (serializeEnum(change.before.enumValues) !== serializeEnum(change.after.enumValues)) return true;
1613
+ if (serializeEnum(change.before.enumValues) !== serializeEnum(change.after.enumValues))
1614
+ return true;
752
1615
  if (change.before.required !== change.after.required && change.after.required) return true;
753
1616
  return false;
754
1617
  }
@@ -1131,9 +1994,7 @@ async function runSqliteMigration(path, statements, migrationId, fromVersion, to
1131
1994
  async function loadSqliteDriver(projectRoot) {
1132
1995
  try {
1133
1996
  const { createRequire } = await import("module");
1134
- const requireFrom = createRequire(
1135
- projectRoot ? `${projectRoot}/package.json` : import.meta.url
1136
- );
1997
+ const requireFrom = createRequire(projectRoot ? `${projectRoot}/package.json` : import.meta.url);
1137
1998
  const Database = requireFrom("better-sqlite3");
1138
1999
  return {
1139
2000
  open(path) {
@@ -1222,83 +2083,34 @@ async function loadPostgresModule() {
1222
2083
  }
1223
2084
  }
1224
2085
 
1225
- // src/commands/migrate/schema-loader.ts
1226
- import { spawn as spawn3 } from "child_process";
1227
- import { extname as extname2 } from "path";
1228
- import { pathToFileURL as pathToFileURL2 } from "url";
1229
- async function loadSchemaDefinition(schemaPath, projectRoot) {
1230
- const ext = extname2(schemaPath);
1231
- const moduleValue = ext === ".ts" || ext === ".mts" || ext === ".cts" ? await loadTypeScriptModule(schemaPath, projectRoot) : await import(`${pathToFileURL2(schemaPath).href}?t=${Date.now()}-${Math.random()}`);
1232
- return extractSchema2(moduleValue);
1233
- }
1234
- function extractSchema2(value) {
1235
- if (typeof value !== "object" || value === null) {
1236
- throw new Error("Schema module must export an object.");
1237
- }
1238
- const moduleRecord = value;
1239
- const candidate = moduleRecord.default ?? moduleRecord;
1240
- if (!isSchemaDefinition2(candidate)) {
1241
- throw new Error("Schema module must export a valid SchemaDefinition as default export.");
1242
- }
1243
- return candidate;
1244
- }
1245
- function isSchemaDefinition2(value) {
1246
- if (typeof value !== "object" || value === null) return false;
1247
- const object = value;
1248
- return typeof object.version === "number" && typeof object.collections === "object" && object.collections !== null && typeof object.relations === "object" && object.relations !== null;
1249
- }
1250
- async function loadTypeScriptModule(schemaPath, projectRoot) {
1251
- if (!await hasTsxInstalled(projectRoot)) {
1252
- throw new Error(
1253
- `Schema file is TypeScript (${schemaPath}) but local "tsx" was not found. Install tsx in the project.`
1254
- );
1255
- }
1256
- const script = "const modulePath = process.argv[process.argv.length - 1];import('node:url').then(u => import(u.pathToFileURL(modulePath).href)).then(mod => { const v = mod.default ?? mod; process.stdout.write(JSON.stringify(v)) }).catch(e => { process.stderr.write(String(e)); process.exit(1) })";
1257
- const output = await runCommand2(
1258
- process.execPath,
1259
- ["--import", "tsx", "--eval", script, schemaPath],
1260
- projectRoot
1261
- );
1262
- try {
1263
- return JSON.parse(output);
1264
- } catch {
1265
- throw new Error(`Failed to parse schema module output for ${schemaPath}`);
1266
- }
1267
- }
1268
- async function runCommand2(command, args, cwd) {
1269
- return await new Promise((resolve4, reject) => {
1270
- const child = spawn3(command, args, {
1271
- cwd,
1272
- stdio: ["ignore", "pipe", "pipe"],
1273
- env: process.env
1274
- });
1275
- let stdout = "";
1276
- let stderr = "";
1277
- child.stdout?.on("data", (chunk) => {
1278
- stdout += chunk.toString("utf-8");
1279
- });
1280
- child.stderr?.on("data", (chunk) => {
1281
- stderr += chunk.toString("utf-8");
1282
- });
1283
- child.on("error", (error) => {
1284
- reject(error);
1285
- });
1286
- child.on("exit", (code) => {
1287
- if (code === 0) {
1288
- resolve4(stdout.trim());
1289
- return;
1290
- }
1291
- reject(
1292
- new Error(`Failed to load TypeScript schema (exit ${code ?? "unknown"}): ${stderr.trim()}`)
1293
- );
1294
- });
1295
- });
2086
+ // src/commands/migrate/operation-transform-generator.ts
2087
+ function generateOperationTransformModule(fromVersion, toVersion) {
2088
+ return [
2089
+ "import type { Operation, OperationTransform } from '@korajs/core'",
2090
+ "",
2091
+ "/**",
2092
+ ` * Operation transforms for schema v${fromVersion} \u2192 v${toVersion}.`,
2093
+ " * Implement `transform` to rewrite or drop legacy operations during sync.",
2094
+ " * @see https://korajs.dev/docs/migrations",
2095
+ " */",
2096
+ "export const operationTransforms: OperationTransform[] = [",
2097
+ " {",
2098
+ ` fromVersion: ${fromVersion},`,
2099
+ ` toVersion: ${toVersion},`,
2100
+ " transform(operation: Operation): Operation | null {",
2101
+ " // Return null to drop unmigratable operations.",
2102
+ " return operation",
2103
+ " },",
2104
+ " },",
2105
+ "]",
2106
+ ""
2107
+ ].join("\n");
1296
2108
  }
1297
2109
 
1298
2110
  // src/commands/migrate/migrate-command.ts
1299
2111
  var SNAPSHOT_PATH = "kora/schema.snapshot.json";
1300
2112
  var MIGRATIONS_DIR = "kora/migrations";
1301
- var migrateCommand = defineCommand3({
2113
+ var migrateCommand = defineCommand7({
1302
2114
  meta: {
1303
2115
  name: "migrate",
1304
2116
  description: "Detect schema changes and generate/apply migrations"
@@ -1340,12 +2152,12 @@ var migrateCommand = defineCommand3({
1340
2152
  throw new InvalidProjectError(process.cwd());
1341
2153
  }
1342
2154
  const config = await loadKoraConfig(projectRoot);
1343
- const resolvedSchemaPath = typeof args.schema === "string" ? resolve3(projectRoot, args.schema) : typeof config?.schema === "string" ? resolve3(projectRoot, config.schema) : await findSchemaFile(projectRoot);
2155
+ const resolvedSchemaPath = typeof args.schema === "string" ? resolve5(projectRoot, args.schema) : typeof config?.schema === "string" ? resolve5(projectRoot, config.schema) : await findSchemaFile(projectRoot);
1344
2156
  if (!resolvedSchemaPath) {
1345
2157
  throw new SchemaNotFoundError(["src/schema.ts", "schema.ts", "src/schema.js", "schema.js"]);
1346
2158
  }
1347
2159
  const currentSchema = await loadSchemaDefinition(resolvedSchemaPath, projectRoot);
1348
- const snapshotFile = join4(projectRoot, SNAPSHOT_PATH);
2160
+ const snapshotFile = join6(projectRoot, SNAPSHOT_PATH);
1349
2161
  const previousSchema = await readSchemaSnapshot(snapshotFile);
1350
2162
  if (!previousSchema) {
1351
2163
  if (args["dry-run"] === true) {
@@ -1358,7 +2170,7 @@ var migrateCommand = defineCommand3({
1358
2170
  return;
1359
2171
  }
1360
2172
  const diff = diffSchemas(previousSchema, currentSchema);
1361
- const outputDir = typeof args["output-dir"] === "string" ? resolve3(projectRoot, args["output-dir"]) : resolve3(projectRoot, MIGRATIONS_DIR);
2173
+ const outputDir = typeof args["output-dir"] === "string" ? resolve5(projectRoot, args["output-dir"]) : resolve5(projectRoot, MIGRATIONS_DIR);
1362
2174
  if (!diff.hasChanges) {
1363
2175
  logger.success("No schema changes detected.");
1364
2176
  if (args.apply === true) {
@@ -1411,7 +2223,12 @@ var migrateCommand = defineCommand3({
1411
2223
  return;
1412
2224
  }
1413
2225
  await mkdir2(outputDir, { recursive: true });
1414
- const migrationPath = await writeMigrationFile(outputDir, diff.fromVersion, diff.toVersion, generated);
2226
+ const migrationPath = await writeMigrationFile(
2227
+ outputDir,
2228
+ diff.fromVersion,
2229
+ diff.toVersion,
2230
+ generated
2231
+ );
1415
2232
  await writeSchemaSnapshot(snapshotFile, currentSchema);
1416
2233
  logger.blank();
1417
2234
  logger.success(`Generated migration: ${migrationPath}`);
@@ -1441,7 +2258,7 @@ var migrateCommand = defineCommand3({
1441
2258
  });
1442
2259
  async function readSchemaSnapshot(path) {
1443
2260
  try {
1444
- const content = await readFile(path, "utf-8");
2261
+ const content = await readFile3(path, "utf-8");
1445
2262
  return JSON.parse(content);
1446
2263
  } catch {
1447
2264
  return null;
@@ -1463,14 +2280,14 @@ function isInteractiveTerminal() {
1463
2280
  }
1464
2281
  async function writeSchemaSnapshot(path, schema) {
1465
2282
  await mkdir2(dirname2(path), { recursive: true });
1466
- await writeFile2(path, `${JSON.stringify(schema, null, 2)}
2283
+ await writeFile3(path, `${JSON.stringify(schema, null, 2)}
1467
2284
  `, "utf-8");
1468
2285
  }
1469
2286
  async function writeMigrationFile(outputDir, fromVersion, toVersion, generated) {
1470
2287
  const existing = await readdir(outputDir).catch(() => []);
1471
2288
  const sequence = existing.filter((file) => /^\d{3}-/.test(file)).length + 1;
1472
2289
  const filename = `${String(sequence).padStart(3, "0")}-v${fromVersion}-to-v${toVersion}.ts`;
1473
- const path = join4(outputDir, filename);
2290
+ const path = join6(outputDir, filename);
1474
2291
  const migrationId = filename.replace(/\.ts$/, "");
1475
2292
  const fileContent = [
1476
2293
  `export const up = ${JSON.stringify(generated.up, null, 2)} as const`,
@@ -1482,8 +2299,10 @@ async function writeMigrationFile(outputDir, fromVersion, toVersion, generated)
1482
2299
  `export const containsBreakingChanges = ${generated.containsBreakingChanges}`,
1483
2300
  ""
1484
2301
  ].join("\n");
1485
- await writeFile2(path, fileContent, "utf-8");
1486
- await writeMigrationManifest(join4(outputDir, `${migrationId}.json`), {
2302
+ await writeFile3(path, fileContent, "utf-8");
2303
+ const transformsPath = join6(outputDir, `${migrationId}.transforms.ts`);
2304
+ await writeFile3(transformsPath, generateOperationTransformModule(fromVersion, toVersion), "utf-8");
2305
+ await writeMigrationManifest(join6(outputDir, `${migrationId}.json`), {
1487
2306
  id: migrationId,
1488
2307
  fromVersion,
1489
2308
  toVersion,
@@ -1495,7 +2314,7 @@ async function writeMigrationFile(outputDir, fromVersion, toVersion, generated)
1495
2314
  return path;
1496
2315
  }
1497
2316
  async function writeMigrationManifest(path, manifest) {
1498
- await writeFile2(path, `${JSON.stringify(manifest, null, 2)}
2317
+ await writeFile3(path, `${JSON.stringify(manifest, null, 2)}
1499
2318
  `, "utf-8");
1500
2319
  }
1501
2320
  async function listMigrationManifests(outputDir) {
@@ -1504,13 +2323,13 @@ async function listMigrationManifests(outputDir) {
1504
2323
  const manifests = [];
1505
2324
  for (const file of migrationFiles) {
1506
2325
  const id = file.replace(/\.ts$/, "");
1507
- const manifestPath = join4(outputDir, `${id}.json`);
2326
+ const manifestPath = join6(outputDir, `${id}.json`);
1508
2327
  const jsonManifest = await readMigrationManifest(manifestPath);
1509
2328
  if (jsonManifest) {
1510
2329
  manifests.push({ ...jsonManifest, id });
1511
2330
  continue;
1512
2331
  }
1513
- const migrationPath = join4(outputDir, file);
2332
+ const migrationPath = join6(outputDir, file);
1514
2333
  const sourceManifest = await readMigrationManifestFromSource(migrationPath, id);
1515
2334
  manifests.push(sourceManifest);
1516
2335
  }
@@ -1518,7 +2337,7 @@ async function listMigrationManifests(outputDir) {
1518
2337
  }
1519
2338
  async function readMigrationManifest(path) {
1520
2339
  try {
1521
- const content = await readFile(path, "utf-8");
2340
+ const content = await readFile3(path, "utf-8");
1522
2341
  return JSON.parse(content);
1523
2342
  } catch (error) {
1524
2343
  const code = error.code;
@@ -1529,7 +2348,7 @@ async function readMigrationManifest(path) {
1529
2348
  }
1530
2349
  }
1531
2350
  async function readMigrationManifestFromSource(path, id) {
1532
- const content = await readFile(path, "utf-8");
2351
+ const content = await readFile3(path, "utf-8");
1533
2352
  const versions = parseVersionsFromMigrationId(id);
1534
2353
  return {
1535
2354
  id,
@@ -1546,9 +2365,14 @@ function parseVersionsFromMigrationId(id) {
1546
2365
  if (!match) {
1547
2366
  throw new Error(`Migration id "${id}" does not include a vX-to-vY version suffix.`);
1548
2367
  }
2368
+ const fromVersionText = match[1];
2369
+ const toVersionText = match[2];
2370
+ if (fromVersionText === void 0 || toVersionText === void 0) {
2371
+ throw new Error(`Migration id "${id}" does not include a vX-to-vY version suffix.`);
2372
+ }
1549
2373
  return {
1550
- fromVersion: Number.parseInt(match[1], 10),
1551
- toVersion: Number.parseInt(match[2], 10)
2374
+ fromVersion: Number.parseInt(fromVersionText, 10),
2375
+ toVersion: Number.parseInt(toVersionText, 10)
1552
2376
  };
1553
2377
  }
1554
2378
  function parseStringArrayExport(source, exportName) {
@@ -1586,18 +2410,18 @@ function parseLiteralExport(source, exportName) {
1586
2410
  }
1587
2411
  function resolveSqliteApplyPath(dbArg, projectRoot, config) {
1588
2412
  if (typeof dbArg === "string") {
1589
- return resolve3(projectRoot, dbArg);
2413
+ return resolve5(projectRoot, dbArg);
1590
2414
  }
1591
2415
  const sync = config?.dev?.sync;
1592
2416
  if (typeof sync === "object" && sync !== null) {
1593
2417
  if (sync.store === "sqlite") {
1594
- return join4(projectRoot, "kora-sync.db");
2418
+ return join6(projectRoot, "kora-sync.db");
1595
2419
  }
1596
2420
  if (typeof sync.store === "object" && sync.store !== null && sync.store.type === "sqlite") {
1597
2421
  if (typeof sync.store.filename === "string" && sync.store.filename.length > 0) {
1598
- return resolve3(projectRoot, sync.store.filename);
2422
+ return resolve5(projectRoot, sync.store.filename);
1599
2423
  }
1600
- return join4(projectRoot, "kora-sync.db");
2424
+ return join6(projectRoot, "kora-sync.db");
1601
2425
  }
1602
2426
  }
1603
2427
  return void 0;
@@ -1614,18 +2438,155 @@ function resolvePostgresConnectionString(config) {
1614
2438
  return void 0;
1615
2439
  }
1616
2440
 
2441
+ // src/commands/status/status-command.ts
2442
+ import { defineCommand as defineCommand8 } from "citty";
2443
+ var DEFAULT_SYNC_PORT3 = 3001;
2444
+ var STATUS_ENDPOINT2 = "/__kora/status";
2445
+ async function fetchServerStatus(url, token) {
2446
+ const statusUrl = `${url.replace(/\/$/, "")}${STATUS_ENDPOINT2}`;
2447
+ const response = await fetch(statusUrl, {
2448
+ headers: token ? { Authorization: `Bearer ${token}` } : void 0
2449
+ });
2450
+ if (!response.ok) {
2451
+ throw new Error(
2452
+ `Failed to fetch status from ${statusUrl}: ${response.status} ${response.statusText}`
2453
+ );
2454
+ }
2455
+ return await response.json();
2456
+ }
2457
+ function formatUptime(ms) {
2458
+ const seconds = Math.floor(ms / 1e3);
2459
+ const minutes = Math.floor(seconds / 60);
2460
+ const hours = Math.floor(minutes / 60);
2461
+ const days = Math.floor(hours / 24);
2462
+ const parts = [];
2463
+ if (days > 0) parts.push(`${days}d`);
2464
+ if (hours % 24 > 0) parts.push(`${hours % 24}h`);
2465
+ if (minutes % 60 > 0) parts.push(`${minutes % 60}m`);
2466
+ parts.push(`${seconds % 60}s`);
2467
+ return parts.join(" ");
2468
+ }
2469
+ var statusCommand = defineCommand8({
2470
+ meta: {
2471
+ name: "status",
2472
+ description: "Show Kora sync server status"
2473
+ },
2474
+ args: {
2475
+ url: {
2476
+ type: "string",
2477
+ description: "Sync server URL (default: http://localhost:3001)",
2478
+ default: `http://localhost:${DEFAULT_SYNC_PORT3}`
2479
+ },
2480
+ watch: {
2481
+ type: "boolean",
2482
+ description: "Live-updating status (like htop)",
2483
+ default: false,
2484
+ alias: "w"
2485
+ },
2486
+ token: {
2487
+ type: "string",
2488
+ description: "Admin token (defaults to KORA_ADMIN_TOKEN)"
2489
+ }
2490
+ },
2491
+ async run({ args }) {
2492
+ const logger = createLogger();
2493
+ const url = typeof args.url === "string" ? args.url : `http://localhost:${DEFAULT_SYNC_PORT3}`;
2494
+ const watch2 = args.watch === true;
2495
+ const token = typeof args.token === "string" ? args.token : process.env.KORA_ADMIN_TOKEN ?? void 0;
2496
+ try {
2497
+ if (watch2) {
2498
+ console.clear();
2499
+ logger.banner();
2500
+ logger.info(`Connecting to ${url}...`);
2501
+ const interval = setInterval(async () => {
2502
+ try {
2503
+ const status = await fetchServerStatus(url, token);
2504
+ printStatus(status, url, logger);
2505
+ } catch {
2506
+ }
2507
+ }, 2e3);
2508
+ try {
2509
+ const status = await fetchServerStatus(url, token);
2510
+ printStatus(status, url, logger);
2511
+ } catch {
2512
+ }
2513
+ await new Promise(() => {
2514
+ process.on("SIGINT", () => {
2515
+ clearInterval(interval);
2516
+ process.exit(0);
2517
+ });
2518
+ process.on("SIGTERM", () => {
2519
+ clearInterval(interval);
2520
+ process.exit(0);
2521
+ });
2522
+ });
2523
+ } else {
2524
+ const status = await fetchServerStatus(url, token);
2525
+ printStatus(status, url, logger);
2526
+ }
2527
+ } catch (error) {
2528
+ logger.error(`Failed to connect to ${url}`);
2529
+ if (error instanceof Error) {
2530
+ logger.error(error.message);
2531
+ }
2532
+ logger.blank();
2533
+ logger.step("Make sure the Kora sync server is running.");
2534
+ logger.step("Start it with: kora dev");
2535
+ process.exit(1);
2536
+ }
2537
+ }
2538
+ });
2539
+ function printStatus(status, url, logger) {
2540
+ console.clear();
2541
+ logger.banner();
2542
+ logger.info(`Kora Sync Server \u2014 ${url}`);
2543
+ logger.blank();
2544
+ if (!status.running) {
2545
+ logger.error("Server is not running");
2546
+ return;
2547
+ }
2548
+ logger.step(`Status: Running (uptime: ${formatUptime(status.uptime)}) \u2014 v${status.version}`);
2549
+ logger.step(`Schema: v${status.schemaVersion}`);
2550
+ logger.blank();
2551
+ logger.step(`Connections: ${status.connectedClients} connected`);
2552
+ logger.step(` Peak: ${status.peakConnections}`);
2553
+ logger.step(` Total: ${status.connectionsTotal}`);
2554
+ if (status.connectedNodeIds.length > 0) {
2555
+ logger.step(" Active nodes:");
2556
+ for (const nodeId of status.connectedNodeIds) {
2557
+ logger.step(` \u2022 ${nodeId}`);
2558
+ }
2559
+ }
2560
+ logger.blank();
2561
+ logger.step("Operations:");
2562
+ logger.step(` Received: ${status.operationsReceived.toLocaleString()}`);
2563
+ logger.step(` Sent: ${status.operationsSent.toLocaleString()}`);
2564
+ logger.step(` Total: ${status.totalOperations.toLocaleString()}`);
2565
+ logger.blank();
2566
+ if (status.errorCount > 0) {
2567
+ logger.warn(`Errors: ${status.errorCount}`);
2568
+ } else {
2569
+ logger.step("Errors: 0");
2570
+ }
2571
+ }
2572
+
1617
2573
  // src/bin.ts
1618
- var main = defineCommand4({
2574
+ var main = defineCommand9({
1619
2575
  meta: {
1620
2576
  name: "kora",
1621
2577
  description: "Kora.js \u2014 Offline-first application framework"
1622
2578
  },
1623
2579
  subCommands: {
2580
+ backup: backupCommand,
2581
+ compact: compactCommand,
1624
2582
  create: createCommand,
1625
2583
  dev: devCommand,
2584
+ doctor: doctorCommand,
1626
2585
  deploy: deployCommand,
1627
2586
  generate: generateCommand,
1628
- migrate: migrateCommand
2587
+ logs: logsCommand,
2588
+ migrate: migrateCommand,
2589
+ status: statusCommand
1629
2590
  }
1630
2591
  });
1631
2592
  runMain(main);