@korajs/cli 0.0.0-canary-20261008174129
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.
- package/LICENSE +21 -0
- package/README.md +44 -0
- package/dist/bin.cjs +9465 -0
- package/dist/bin.cjs.map +1 -0
- package/dist/bin.js +3150 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-5NI2FEQL.js +92 -0
- package/dist/chunk-5NI2FEQL.js.map +1 -0
- package/dist/chunk-6C4BHSRA.js +82 -0
- package/dist/chunk-6C4BHSRA.js.map +1 -0
- package/dist/chunk-7Y4J2GXP.js +796 -0
- package/dist/chunk-7Y4J2GXP.js.map +1 -0
- package/dist/chunk-BURFQVMV.js +719 -0
- package/dist/chunk-BURFQVMV.js.map +1 -0
- package/dist/chunk-TVIRMDER.js +2569 -0
- package/dist/chunk-TVIRMDER.js.map +1 -0
- package/dist/create.cjs +1368 -0
- package/dist/create.cjs.map +1 -0
- package/dist/create.js +11 -0
- package/dist/create.js.map +1 -0
- package/dist/index.cjs +3489 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +772 -0
- package/dist/index.d.ts +772 -0
- package/dist/index.js +129 -0
- package/dist/index.js.map +1 -0
- package/dist/lab-manager-KUDII6BT.js +280 -0
- package/dist/lab-manager-KUDII6BT.js.map +1 -0
- package/dist/schema-loader-GGHECMTM.js +9 -0
- package/dist/schema-loader-GGHECMTM.js.map +1 -0
- package/dist/spectator-manager-FTH2NFSE.js +145 -0
- package/dist/spectator-manager-FTH2NFSE.js.map +1 -0
- package/dist/studio-server-XLGZNGG5.js +1747 -0
- package/dist/studio-server-XLGZNGG5.js.map +1 -0
- package/dist/vite.cjs +351 -0
- package/dist/vite.cjs.map +1 -0
- package/dist/vite.d.cts +161 -0
- package/dist/vite.d.ts +161 -0
- package/dist/vite.js +321 -0
- package/dist/vite.js.map +1 -0
- package/package.json +79 -0
- package/templates/react-basic/.env.example +3 -0
- package/templates/react-basic/AGENTS.md +87 -0
- package/templates/react-basic/README.md.hbs +62 -0
- package/templates/react-basic/index.html.hbs +12 -0
- package/templates/react-basic/kora.config.ts +13 -0
- package/templates/react-basic/package.json.hbs +35 -0
- package/templates/react-basic/src/App.tsx +132 -0
- package/templates/react-basic/src/index.css +287 -0
- package/templates/react-basic/src/kora-worker.ts +9 -0
- package/templates/react-basic/src/main.tsx +24 -0
- package/templates/react-basic/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/react-basic/src/modules/todos/todo.queries.ts +5 -0
- package/templates/react-basic/src/modules/todos/todo.schema.ts +10 -0
- package/templates/react-basic/src/modules/todos/useTodos.ts +31 -0
- package/templates/react-basic/src/schema.ts +9 -0
- package/templates/react-basic/src/vite-env.d.ts +18 -0
- package/templates/react-basic/tsconfig.json.hbs +14 -0
- package/templates/react-basic/vite.config.ts +88 -0
- package/templates/react-sync/.env.example +25 -0
- package/templates/react-sync/AGENTS.md +87 -0
- package/templates/react-sync/README.md.hbs +106 -0
- package/templates/react-sync/index.html.hbs +12 -0
- package/templates/react-sync/kora.config.ts +17 -0
- package/templates/react-sync/package.json.hbs +44 -0
- package/templates/react-sync/server.ts +141 -0
- package/templates/react-sync/src/App.tsx +160 -0
- package/templates/react-sync/src/auth.ts +25 -0
- package/templates/react-sync/src/index.css +350 -0
- package/templates/react-sync/src/kora-worker.ts +9 -0
- package/templates/react-sync/src/main.tsx +52 -0
- package/templates/react-sync/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/react-sync/src/modules/todos/todo.queries.ts +5 -0
- package/templates/react-sync/src/modules/todos/todo.schema.ts +10 -0
- package/templates/react-sync/src/modules/todos/useTodos.ts +31 -0
- package/templates/react-sync/src/schema.ts +9 -0
- package/templates/react-sync/src/vite-env.d.ts +19 -0
- package/templates/react-sync/tsconfig.json.hbs +14 -0
- package/templates/react-sync/vite.config.ts +105 -0
- package/templates/react-tailwind/.env.example +3 -0
- package/templates/react-tailwind/AGENTS.md +87 -0
- package/templates/react-tailwind/README.md.hbs +62 -0
- package/templates/react-tailwind/index.html.hbs +12 -0
- package/templates/react-tailwind/kora.config.ts +13 -0
- package/templates/react-tailwind/package.json.hbs +38 -0
- package/templates/react-tailwind/src/App.tsx +201 -0
- package/templates/react-tailwind/src/index.css +7 -0
- package/templates/react-tailwind/src/kora-worker.ts +9 -0
- package/templates/react-tailwind/src/main.tsx +31 -0
- package/templates/react-tailwind/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/react-tailwind/src/modules/todos/todo.queries.ts +5 -0
- package/templates/react-tailwind/src/modules/todos/todo.schema.ts +10 -0
- package/templates/react-tailwind/src/modules/todos/useTodos.ts +31 -0
- package/templates/react-tailwind/src/schema.ts +9 -0
- package/templates/react-tailwind/src/vite-env.d.ts +18 -0
- package/templates/react-tailwind/tsconfig.json.hbs +14 -0
- package/templates/react-tailwind/vite.config.ts +95 -0
- package/templates/react-tailwind-sync/.env.example +25 -0
- package/templates/react-tailwind-sync/AGENTS.md +89 -0
- package/templates/react-tailwind-sync/README.md.hbs +106 -0
- package/templates/react-tailwind-sync/index.html.hbs +12 -0
- package/templates/react-tailwind-sync/kora.config.ts +17 -0
- package/templates/react-tailwind-sync/package.json.hbs +47 -0
- package/templates/react-tailwind-sync/server.ts +141 -0
- package/templates/react-tailwind-sync/src/App.tsx +293 -0
- package/templates/react-tailwind-sync/src/auth.ts +25 -0
- package/templates/react-tailwind-sync/src/index.css +7 -0
- package/templates/react-tailwind-sync/src/kora-worker.ts +9 -0
- package/templates/react-tailwind-sync/src/kora.ts +32 -0
- package/templates/react-tailwind-sync/src/main.tsx +44 -0
- package/templates/react-tailwind-sync/src/modules/todos/todo.mutations.ts +21 -0
- package/templates/react-tailwind-sync/src/modules/todos/todo.queries.ts +5 -0
- package/templates/react-tailwind-sync/src/modules/todos/todo.schema.ts +10 -0
- package/templates/react-tailwind-sync/src/modules/todos/useTodos.ts +31 -0
- package/templates/react-tailwind-sync/src/schema.ts +9 -0
- package/templates/react-tailwind-sync/src/vite-env.d.ts +19 -0
- package/templates/react-tailwind-sync/tsconfig.json.hbs +14 -0
- package/templates/react-tailwind-sync/vite.config.ts +112 -0
- package/templates/svelte-basic/.env.example +3 -0
- package/templates/svelte-basic/AGENTS.md +76 -0
- package/templates/svelte-basic/README.md.hbs +62 -0
- package/templates/svelte-basic/index.html.hbs +12 -0
- package/templates/svelte-basic/kora.config.ts +12 -0
- package/templates/svelte-basic/package.json.hbs +33 -0
- package/templates/svelte-basic/src/App.svelte +128 -0
- package/templates/svelte-basic/src/Root.svelte +14 -0
- package/templates/svelte-basic/src/index.css +287 -0
- package/templates/svelte-basic/src/kora-worker.ts +9 -0
- package/templates/svelte-basic/src/main.ts +16 -0
- package/templates/svelte-basic/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/svelte-basic/src/modules/todos/todo.queries.ts +5 -0
- package/templates/svelte-basic/src/modules/todos/todo.schema.ts +10 -0
- package/templates/svelte-basic/src/modules/todos/useTodos.ts +25 -0
- package/templates/svelte-basic/src/schema.ts +9 -0
- package/templates/svelte-basic/src/vite-env.d.ts +12 -0
- package/templates/svelte-basic/svelte.config.js +5 -0
- package/templates/svelte-basic/tsconfig.json.hbs +14 -0
- package/templates/svelte-basic/vite.config.ts +57 -0
- package/templates/svelte-sync/.env.example +25 -0
- package/templates/svelte-sync/AGENTS.md +76 -0
- package/templates/svelte-sync/README.md.hbs +106 -0
- package/templates/svelte-sync/index.html.hbs +12 -0
- package/templates/svelte-sync/kora.config.ts +17 -0
- package/templates/svelte-sync/package.json.hbs +43 -0
- package/templates/svelte-sync/server.ts +141 -0
- package/templates/svelte-sync/src/App.svelte +151 -0
- package/templates/svelte-sync/src/Root.svelte +22 -0
- package/templates/svelte-sync/src/auth.ts +25 -0
- package/templates/svelte-sync/src/index.css +339 -0
- package/templates/svelte-sync/src/kora-worker.ts +9 -0
- package/templates/svelte-sync/src/main.ts +32 -0
- package/templates/svelte-sync/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/svelte-sync/src/modules/todos/todo.queries.ts +5 -0
- package/templates/svelte-sync/src/modules/todos/todo.schema.ts +10 -0
- package/templates/svelte-sync/src/modules/todos/useTodos.ts +25 -0
- package/templates/svelte-sync/src/schema.ts +9 -0
- package/templates/svelte-sync/src/vite-env.d.ts +22 -0
- package/templates/svelte-sync/svelte.config.js +5 -0
- package/templates/svelte-sync/tsconfig.json.hbs +14 -0
- package/templates/svelte-sync/vite.config.ts +91 -0
- package/templates/svelte-tailwind/AGENTS.md +76 -0
- package/templates/svelte-tailwind/README.md.hbs +62 -0
- package/templates/svelte-tailwind/index.html.hbs +12 -0
- package/templates/svelte-tailwind/kora.config.ts +12 -0
- package/templates/svelte-tailwind/package.json.hbs +36 -0
- package/templates/svelte-tailwind/src/App.svelte +76 -0
- package/templates/svelte-tailwind/src/Root.svelte +14 -0
- package/templates/svelte-tailwind/src/index.css +7 -0
- package/templates/svelte-tailwind/src/kora-worker.ts +9 -0
- package/templates/svelte-tailwind/src/main.ts +16 -0
- package/templates/svelte-tailwind/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/svelte-tailwind/src/modules/todos/todo.queries.ts +5 -0
- package/templates/svelte-tailwind/src/modules/todos/todo.schema.ts +10 -0
- package/templates/svelte-tailwind/src/modules/todos/useTodos.ts +25 -0
- package/templates/svelte-tailwind/src/schema.ts +9 -0
- package/templates/svelte-tailwind/src/vite-env.d.ts +12 -0
- package/templates/svelte-tailwind/svelte.config.js +5 -0
- package/templates/svelte-tailwind/tsconfig.json.hbs +14 -0
- package/templates/svelte-tailwind/vite.config.ts +64 -0
- package/templates/svelte-tailwind-sync/.env.example +25 -0
- package/templates/svelte-tailwind-sync/AGENTS.md +76 -0
- package/templates/svelte-tailwind-sync/README.md.hbs +106 -0
- package/templates/svelte-tailwind-sync/index.html.hbs +12 -0
- package/templates/svelte-tailwind-sync/kora.config.ts +17 -0
- package/templates/svelte-tailwind-sync/package.json.hbs +46 -0
- package/templates/svelte-tailwind-sync/server.ts +141 -0
- package/templates/svelte-tailwind-sync/src/App.svelte +193 -0
- package/templates/svelte-tailwind-sync/src/Root.svelte +26 -0
- package/templates/svelte-tailwind-sync/src/auth.ts +25 -0
- package/templates/svelte-tailwind-sync/src/index.css +7 -0
- package/templates/svelte-tailwind-sync/src/kora-worker.ts +9 -0
- package/templates/svelte-tailwind-sync/src/main.ts +32 -0
- package/templates/svelte-tailwind-sync/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/svelte-tailwind-sync/src/modules/todos/todo.queries.ts +5 -0
- package/templates/svelte-tailwind-sync/src/modules/todos/todo.schema.ts +10 -0
- package/templates/svelte-tailwind-sync/src/modules/todos/useTodos.ts +25 -0
- package/templates/svelte-tailwind-sync/src/schema.ts +9 -0
- package/templates/svelte-tailwind-sync/src/vite-env.d.ts +22 -0
- package/templates/svelte-tailwind-sync/svelte.config.js +5 -0
- package/templates/svelte-tailwind-sync/tsconfig.json.hbs +14 -0
- package/templates/svelte-tailwind-sync/vite.config.ts +98 -0
- package/templates/tauri-react/.env.example +27 -0
- package/templates/tauri-react/.github/workflows/release-desktop.yml +107 -0
- package/templates/tauri-react/AGENTS.md +91 -0
- package/templates/tauri-react/README.md.hbs +183 -0
- package/templates/tauri-react/dev.ts +37 -0
- package/templates/tauri-react/index.html.hbs +16 -0
- package/templates/tauri-react/kora.config.ts +17 -0
- package/templates/tauri-react/package.json.hbs +46 -0
- package/templates/tauri-react/server.ts +148 -0
- package/templates/tauri-react/src/App.tsx +803 -0
- package/templates/tauri-react/src/AppShell.tsx +158 -0
- package/templates/tauri-react/src/SetupScreen.tsx +288 -0
- package/templates/tauri-react/src/auth.ts +45 -0
- package/templates/tauri-react/src/main.tsx +9 -0
- package/templates/tauri-react/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/tauri-react/src/modules/todos/todo.queries.ts +5 -0
- package/templates/tauri-react/src/modules/todos/todo.schema.ts +10 -0
- package/templates/tauri-react/src/modules/todos/useTodos.ts +31 -0
- package/templates/tauri-react/src/schema.ts +9 -0
- package/templates/tauri-react/src/sync-config.ts +103 -0
- package/templates/tauri-react/src/updater.ts +38 -0
- package/templates/tauri-react/src/vite-env.d.ts +19 -0
- package/templates/tauri-react/src-tauri/Cargo.toml +19 -0
- package/templates/tauri-react/src-tauri/build.rs +3 -0
- package/templates/tauri-react/src-tauri/capabilities/default.json +12 -0
- package/templates/tauri-react/src-tauri/icons/128x128.png +0 -0
- package/templates/tauri-react/src-tauri/icons/128x128@2x.png +0 -0
- package/templates/tauri-react/src-tauri/icons/32x32.png +0 -0
- package/templates/tauri-react/src-tauri/icons/icon.icns +0 -0
- package/templates/tauri-react/src-tauri/icons/icon.ico +0 -0
- package/templates/tauri-react/src-tauri/src/lib.rs +8 -0
- package/templates/tauri-react/src-tauri/src/main.rs +6 -0
- package/templates/tauri-react/src-tauri/tauri.conf.json +44 -0
- package/templates/tauri-react/tsconfig.json.hbs +14 -0
- package/templates/tauri-react/vite.config.ts +15 -0
- package/templates/vue-basic/AGENTS.md +84 -0
- package/templates/vue-basic/README.md.hbs +62 -0
- package/templates/vue-basic/index.html.hbs +12 -0
- package/templates/vue-basic/kora.config.ts +12 -0
- package/templates/vue-basic/package.json.hbs +32 -0
- package/templates/vue-basic/src/App.vue +117 -0
- package/templates/vue-basic/src/index.css +287 -0
- package/templates/vue-basic/src/kora-worker.ts +9 -0
- package/templates/vue-basic/src/main.ts +20 -0
- package/templates/vue-basic/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/vue-basic/src/modules/todos/todo.queries.ts +5 -0
- package/templates/vue-basic/src/modules/todos/todo.schema.ts +10 -0
- package/templates/vue-basic/src/modules/todos/useTodos.ts +32 -0
- package/templates/vue-basic/src/schema.ts +9 -0
- package/templates/vue-basic/src/vite-env.d.ts +17 -0
- package/templates/vue-basic/tsconfig.json.hbs +15 -0
- package/templates/vue-basic/vite.config.ts +74 -0
- package/templates/vue-sync/.env.example +25 -0
- package/templates/vue-sync/AGENTS.md +84 -0
- package/templates/vue-sync/README.md.hbs +106 -0
- package/templates/vue-sync/index.html.hbs +12 -0
- package/templates/vue-sync/kora.config.ts +17 -0
- package/templates/vue-sync/package.json.hbs +42 -0
- package/templates/vue-sync/server.ts +141 -0
- package/templates/vue-sync/src/App.vue +135 -0
- package/templates/vue-sync/src/auth.ts +25 -0
- package/templates/vue-sync/src/index.css +339 -0
- package/templates/vue-sync/src/kora-worker.ts +9 -0
- package/templates/vue-sync/src/main.ts +47 -0
- package/templates/vue-sync/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/vue-sync/src/modules/todos/todo.queries.ts +5 -0
- package/templates/vue-sync/src/modules/todos/todo.schema.ts +10 -0
- package/templates/vue-sync/src/modules/todos/useTodos.ts +32 -0
- package/templates/vue-sync/src/schema.ts +9 -0
- package/templates/vue-sync/src/vite-env.d.ts +27 -0
- package/templates/vue-sync/tsconfig.json.hbs +15 -0
- package/templates/vue-sync/vite.config.ts +91 -0
- package/templates/vue-tailwind/AGENTS.md +84 -0
- package/templates/vue-tailwind/README.md.hbs +62 -0
- package/templates/vue-tailwind/index.html.hbs +12 -0
- package/templates/vue-tailwind/kora.config.ts +12 -0
- package/templates/vue-tailwind/package.json.hbs +35 -0
- package/templates/vue-tailwind/src/App.vue +135 -0
- package/templates/vue-tailwind/src/index.css +7 -0
- package/templates/vue-tailwind/src/kora-worker.ts +9 -0
- package/templates/vue-tailwind/src/main.ts +31 -0
- package/templates/vue-tailwind/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/vue-tailwind/src/modules/todos/todo.queries.ts +5 -0
- package/templates/vue-tailwind/src/modules/todos/todo.schema.ts +10 -0
- package/templates/vue-tailwind/src/modules/todos/useTodos.ts +32 -0
- package/templates/vue-tailwind/src/schema.ts +9 -0
- package/templates/vue-tailwind/src/vite-env.d.ts +17 -0
- package/templates/vue-tailwind/tsconfig.json.hbs +15 -0
- package/templates/vue-tailwind/vite.config.ts +75 -0
- package/templates/vue-tailwind-sync/.env.example +25 -0
- package/templates/vue-tailwind-sync/AGENTS.md +84 -0
- package/templates/vue-tailwind-sync/README.md.hbs +106 -0
- package/templates/vue-tailwind-sync/index.html.hbs +12 -0
- package/templates/vue-tailwind-sync/kora.config.ts +17 -0
- package/templates/vue-tailwind-sync/package.json.hbs +45 -0
- package/templates/vue-tailwind-sync/server.ts +141 -0
- package/templates/vue-tailwind-sync/src/App.vue +245 -0
- package/templates/vue-tailwind-sync/src/auth.ts +25 -0
- package/templates/vue-tailwind-sync/src/index.css +7 -0
- package/templates/vue-tailwind-sync/src/kora-worker.ts +9 -0
- package/templates/vue-tailwind-sync/src/main.ts +56 -0
- package/templates/vue-tailwind-sync/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/vue-tailwind-sync/src/modules/todos/todo.queries.ts +5 -0
- package/templates/vue-tailwind-sync/src/modules/todos/todo.schema.ts +10 -0
- package/templates/vue-tailwind-sync/src/modules/todos/useTodos.ts +32 -0
- package/templates/vue-tailwind-sync/src/schema.ts +9 -0
- package/templates/vue-tailwind-sync/src/vite-env.d.ts +27 -0
- package/templates/vue-tailwind-sync/tsconfig.json.hbs +15 -0
- package/templates/vue-tailwind-sync/vite.config.ts +92 -0
package/dist/bin.js
ADDED
|
@@ -0,0 +1,3150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
agentsCommand,
|
|
4
|
+
deployCommand,
|
|
5
|
+
generateTypes
|
|
6
|
+
} from "./chunk-TVIRMDER.js";
|
|
7
|
+
import {
|
|
8
|
+
createCommand
|
|
9
|
+
} from "./chunk-BURFQVMV.js";
|
|
10
|
+
import {
|
|
11
|
+
DevServerError,
|
|
12
|
+
InvalidProjectError,
|
|
13
|
+
SchemaNotFoundError,
|
|
14
|
+
createLogger,
|
|
15
|
+
promptConfirm
|
|
16
|
+
} from "./chunk-7Y4J2GXP.js";
|
|
17
|
+
import {
|
|
18
|
+
loadSchemaDefinition
|
|
19
|
+
} from "./chunk-6C4BHSRA.js";
|
|
20
|
+
import {
|
|
21
|
+
findProjectRoot,
|
|
22
|
+
findSchemaFile,
|
|
23
|
+
hasTsxInstalled,
|
|
24
|
+
resolveProjectBinaryEntryPoint
|
|
25
|
+
} from "./chunk-5NI2FEQL.js";
|
|
26
|
+
|
|
27
|
+
// src/bin.ts
|
|
28
|
+
import { defineCommand as defineCommand10, runMain } from "citty";
|
|
29
|
+
|
|
30
|
+
// src/commands/backup/backup-command.ts
|
|
31
|
+
import { readFile, writeFile } from "fs/promises";
|
|
32
|
+
import "path";
|
|
33
|
+
import { defineCommand } from "citty";
|
|
34
|
+
var DEFAULT_SYNC_PORT = 3001;
|
|
35
|
+
var backupCommand = defineCommand({
|
|
36
|
+
meta: {
|
|
37
|
+
name: "backup",
|
|
38
|
+
description: "Backup and restore Kora sync server data"
|
|
39
|
+
},
|
|
40
|
+
subCommands: {
|
|
41
|
+
create: defineCommand({
|
|
42
|
+
meta: {
|
|
43
|
+
name: "create",
|
|
44
|
+
description: "Create a backup of the sync server"
|
|
45
|
+
},
|
|
46
|
+
args: {
|
|
47
|
+
url: {
|
|
48
|
+
type: "string",
|
|
49
|
+
description: "Sync server URL (default: http://localhost:3001)",
|
|
50
|
+
default: `http://localhost:${DEFAULT_SYNC_PORT}`
|
|
51
|
+
},
|
|
52
|
+
out: {
|
|
53
|
+
type: "string",
|
|
54
|
+
description: "Output file path (default: kora-backup-<timestamp>.kora)"
|
|
55
|
+
},
|
|
56
|
+
token: {
|
|
57
|
+
type: "string",
|
|
58
|
+
description: "Backup token (defaults to KORA_BACKUP_TOKEN or KORA_ADMIN_TOKEN)"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
async run({ args }) {
|
|
62
|
+
const logger = createLogger();
|
|
63
|
+
const url = typeof args.url === "string" ? args.url : `http://localhost:${DEFAULT_SYNC_PORT}`;
|
|
64
|
+
const outFile = typeof args.out === "string" ? args.out : `kora-backup-${Date.now()}.kora`;
|
|
65
|
+
const token = typeof args.token === "string" ? args.token : process.env.KORA_BACKUP_TOKEN ?? process.env.KORA_ADMIN_TOKEN;
|
|
66
|
+
logger.banner();
|
|
67
|
+
logger.info(`Exporting backup from ${url}...`);
|
|
68
|
+
try {
|
|
69
|
+
const backupUrl = `${url.replace(/\/$/, "")}/__kora/backup/export`;
|
|
70
|
+
const response = await fetch(backupUrl, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: token ? { Authorization: `Bearer ${token}` } : void 0
|
|
73
|
+
});
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
const error = await response.json().catch(() => ({ message: response.statusText }));
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Backup failed: ${error.message ?? response.statusText}`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const buffer = await response.arrayBuffer();
|
|
81
|
+
await writeFile(outFile, new Uint8Array(buffer));
|
|
82
|
+
const size = (buffer.byteLength / 1024).toFixed(1);
|
|
83
|
+
logger.success(`Backup saved to ${outFile} (${size} KB)`);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
logger.error("Backup failed");
|
|
86
|
+
if (error instanceof Error) logger.error(error.message);
|
|
87
|
+
logger.blank();
|
|
88
|
+
logger.step("Make sure the Kora sync server is running.");
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}),
|
|
93
|
+
restore: defineCommand({
|
|
94
|
+
meta: {
|
|
95
|
+
name: "restore",
|
|
96
|
+
description: "Restore a backup to the sync server"
|
|
97
|
+
},
|
|
98
|
+
args: {
|
|
99
|
+
file: {
|
|
100
|
+
type: "string",
|
|
101
|
+
description: "Backup file path",
|
|
102
|
+
required: true
|
|
103
|
+
},
|
|
104
|
+
url: {
|
|
105
|
+
type: "string",
|
|
106
|
+
description: "Sync server URL (default: http://localhost:3001)",
|
|
107
|
+
default: `http://localhost:${DEFAULT_SYNC_PORT}`
|
|
108
|
+
},
|
|
109
|
+
merge: {
|
|
110
|
+
type: "boolean",
|
|
111
|
+
description: "Merge with existing data instead of replacing",
|
|
112
|
+
default: false
|
|
113
|
+
},
|
|
114
|
+
token: {
|
|
115
|
+
type: "string",
|
|
116
|
+
description: "Backup token (defaults to KORA_BACKUP_TOKEN or KORA_ADMIN_TOKEN)"
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
async run({ args }) {
|
|
120
|
+
const logger = createLogger();
|
|
121
|
+
const filePath = typeof args.file === "string" ? args.file : "";
|
|
122
|
+
const url = typeof args.url === "string" ? args.url : `http://localhost:${DEFAULT_SYNC_PORT}`;
|
|
123
|
+
const merge = args.merge === true;
|
|
124
|
+
const token = typeof args.token === "string" ? args.token : process.env.KORA_BACKUP_TOKEN ?? process.env.KORA_ADMIN_TOKEN;
|
|
125
|
+
logger.banner();
|
|
126
|
+
logger.info(`Restoring backup from ${filePath} to ${url}...`);
|
|
127
|
+
try {
|
|
128
|
+
const data = await readFile(filePath);
|
|
129
|
+
const restoreUrl = `${url.replace(/\/$/, "")}/__kora/backup/import?merge=${merge}`;
|
|
130
|
+
const response = await fetch(restoreUrl, {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: token ? { Authorization: `Bearer ${token}` } : void 0,
|
|
133
|
+
body: data
|
|
134
|
+
});
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
const error = await response.json().catch(() => ({ message: response.statusText }));
|
|
137
|
+
throw new Error(
|
|
138
|
+
`Restore failed: ${error.message ?? response.statusText}`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const result = await response.json();
|
|
142
|
+
if (result.success) {
|
|
143
|
+
logger.success(
|
|
144
|
+
`Restored ${result.operationsRestored} operations${result.duration ? ` in ${result.duration}ms` : ""}`
|
|
145
|
+
);
|
|
146
|
+
} else {
|
|
147
|
+
logger.error("Restore completed with errors");
|
|
148
|
+
}
|
|
149
|
+
} catch (error) {
|
|
150
|
+
logger.error("Restore failed");
|
|
151
|
+
if (error instanceof Error) logger.error(error.message);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}),
|
|
156
|
+
info: defineCommand({
|
|
157
|
+
meta: {
|
|
158
|
+
name: "info",
|
|
159
|
+
description: "Show backup file information"
|
|
160
|
+
},
|
|
161
|
+
args: {
|
|
162
|
+
file: {
|
|
163
|
+
type: "string",
|
|
164
|
+
description: "Backup file path",
|
|
165
|
+
required: true
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
async run({ args }) {
|
|
169
|
+
const logger = createLogger();
|
|
170
|
+
const filePath = typeof args.file === "string" ? args.file : "";
|
|
171
|
+
try {
|
|
172
|
+
const data = await readFile(filePath);
|
|
173
|
+
const { readBackupManifest } = await import("@korajs/store");
|
|
174
|
+
const manifest = readBackupManifest(
|
|
175
|
+
new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
|
176
|
+
);
|
|
177
|
+
logger.banner();
|
|
178
|
+
logger.info(`Backup: ${filePath}`);
|
|
179
|
+
logger.blank();
|
|
180
|
+
logger.step(`Created: ${new Date(manifest.createdAt).toISOString()}`);
|
|
181
|
+
logger.step(`Node ID: ${manifest.nodeId}`);
|
|
182
|
+
logger.step(`Schema: v${manifest.schemaVersion}`);
|
|
183
|
+
logger.step(`Operations: ${manifest.operationCount.toLocaleString()}`);
|
|
184
|
+
logger.step(`Collections: ${manifest.collections.join(", ") || "(none)"}`);
|
|
185
|
+
logger.step(`Checksum: ${manifest.checksum.slice(0, 16)}...`);
|
|
186
|
+
logger.blank();
|
|
187
|
+
logger.step(`Format version: ${manifest.version}`);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
logger.error("Failed to read backup file");
|
|
190
|
+
if (error instanceof Error) logger.error(error.message);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// src/commands/compact/compact-command.ts
|
|
199
|
+
import { resolve } from "path";
|
|
200
|
+
import { defineCommand as defineCommand2 } from "citty";
|
|
201
|
+
|
|
202
|
+
// src/commands/dev/kora-config.ts
|
|
203
|
+
import { spawn } from "child_process";
|
|
204
|
+
import { access } from "fs/promises";
|
|
205
|
+
import { extname, join as join2 } from "path";
|
|
206
|
+
import { pathToFileURL } from "url";
|
|
207
|
+
var CONFIG_CANDIDATES = [
|
|
208
|
+
"kora.config.ts",
|
|
209
|
+
"kora.config.mts",
|
|
210
|
+
"kora.config.cts",
|
|
211
|
+
"kora.config.js",
|
|
212
|
+
"kora.config.mjs",
|
|
213
|
+
"kora.config.cjs"
|
|
214
|
+
];
|
|
215
|
+
async function loadKoraConfig(projectRoot) {
|
|
216
|
+
const configPath = await findKoraConfigFile(projectRoot);
|
|
217
|
+
if (!configPath) return null;
|
|
218
|
+
const ext = extname(configPath);
|
|
219
|
+
if (ext === ".ts" || ext === ".mts" || ext === ".cts") {
|
|
220
|
+
const loaded2 = await loadTypeScriptConfig(configPath, projectRoot);
|
|
221
|
+
return toConfigObject(loaded2);
|
|
222
|
+
}
|
|
223
|
+
const loaded = await import(pathToFileURL(configPath).href);
|
|
224
|
+
return toConfigObject(loaded);
|
|
225
|
+
}
|
|
226
|
+
async function findKoraConfigFile(projectRoot) {
|
|
227
|
+
for (const file of CONFIG_CANDIDATES) {
|
|
228
|
+
const candidate = join2(projectRoot, file);
|
|
229
|
+
try {
|
|
230
|
+
await access(candidate);
|
|
231
|
+
return candidate;
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
async function loadTypeScriptConfig(configPath, projectRoot) {
|
|
238
|
+
if (!await hasTsxInstalled(projectRoot)) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`Found TypeScript config at ${configPath}, but "tsx" is not installed in this project. Install tsx or use kora.config.js.`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
const script = "const configPath = process.argv[process.argv.length - 1];import('node:url').then(u => import(u.pathToFileURL(configPath).href)).then(mod => { const v = mod.default ?? mod; process.stdout.write(JSON.stringify(v)) }).catch(e => { process.stderr.write(String(e)); process.exit(1) })";
|
|
244
|
+
const output = await runCommand(
|
|
245
|
+
process.execPath,
|
|
246
|
+
["--import", "tsx", "--eval", script, configPath],
|
|
247
|
+
projectRoot
|
|
248
|
+
);
|
|
249
|
+
try {
|
|
250
|
+
return JSON.parse(output);
|
|
251
|
+
} catch {
|
|
252
|
+
throw new Error(`Failed to parse ${configPath} output as JSON.`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async function runCommand(command, args, cwd) {
|
|
256
|
+
return await new Promise((resolve7, reject) => {
|
|
257
|
+
const child = spawn(command, args, {
|
|
258
|
+
cwd,
|
|
259
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
260
|
+
env: process.env
|
|
261
|
+
});
|
|
262
|
+
let stdout = "";
|
|
263
|
+
let stderr = "";
|
|
264
|
+
child.stdout?.on("data", (chunk) => {
|
|
265
|
+
stdout += chunk.toString("utf-8");
|
|
266
|
+
});
|
|
267
|
+
child.stderr?.on("data", (chunk) => {
|
|
268
|
+
stderr += chunk.toString("utf-8");
|
|
269
|
+
});
|
|
270
|
+
child.on("error", (error) => {
|
|
271
|
+
reject(error);
|
|
272
|
+
});
|
|
273
|
+
child.on("exit", (code) => {
|
|
274
|
+
if (code === 0) {
|
|
275
|
+
resolve7(stdout.trim());
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
reject(new Error(`Failed to load kora config (exit ${code ?? "unknown"}): ${stderr.trim()}`));
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
function toConfigObject(mod) {
|
|
283
|
+
if (typeof mod !== "object" || mod === null) {
|
|
284
|
+
throw new Error("kora config must export an object.");
|
|
285
|
+
}
|
|
286
|
+
const value = mod.default ?? mod;
|
|
287
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
288
|
+
throw new Error("kora config must export an object.");
|
|
289
|
+
}
|
|
290
|
+
return value;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/commands/compact/compact-command.ts
|
|
294
|
+
var compactCommand = defineCommand2({
|
|
295
|
+
meta: {
|
|
296
|
+
name: "compact",
|
|
297
|
+
description: "Compact the local Kora operation log after server ack"
|
|
298
|
+
},
|
|
299
|
+
args: {
|
|
300
|
+
db: {
|
|
301
|
+
type: "string",
|
|
302
|
+
description: "Path to the local SQLite database (required)"
|
|
303
|
+
},
|
|
304
|
+
schema: {
|
|
305
|
+
type: "string",
|
|
306
|
+
description: "Path to schema file"
|
|
307
|
+
},
|
|
308
|
+
strategy: {
|
|
309
|
+
type: "string",
|
|
310
|
+
description: "Compaction strategy: after-ack | after-days | never",
|
|
311
|
+
default: "after-ack"
|
|
312
|
+
},
|
|
313
|
+
days: {
|
|
314
|
+
type: "string",
|
|
315
|
+
description: "Age threshold when strategy is after-days",
|
|
316
|
+
default: "30"
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
async run({ args }) {
|
|
320
|
+
const logger = createLogger();
|
|
321
|
+
const projectRoot = await findProjectRoot();
|
|
322
|
+
if (!projectRoot) {
|
|
323
|
+
throw new InvalidProjectError(process.cwd());
|
|
324
|
+
}
|
|
325
|
+
const dbPath = typeof args.db === "string" ? resolve(projectRoot, args.db) : void 0;
|
|
326
|
+
if (!dbPath) {
|
|
327
|
+
throw new Error("Missing --db <path> to the local SQLite database file.");
|
|
328
|
+
}
|
|
329
|
+
const config = await loadKoraConfig(projectRoot);
|
|
330
|
+
const schemaPath = typeof args.schema === "string" ? resolve(projectRoot, args.schema) : typeof config?.schema === "string" ? resolve(projectRoot, config.schema) : void 0;
|
|
331
|
+
if (!schemaPath) {
|
|
332
|
+
throw new Error("Could not resolve schema path. Pass --schema <path>.");
|
|
333
|
+
}
|
|
334
|
+
const schema = await loadSchemaDefinition(schemaPath, projectRoot);
|
|
335
|
+
const { BetterSqlite3Adapter } = await import("@korajs/store/better-sqlite3");
|
|
336
|
+
const { Store } = await import("@korajs/store");
|
|
337
|
+
const strategyName = typeof args.strategy === "string" ? args.strategy : "after-ack";
|
|
338
|
+
const days = Number.parseInt(typeof args.days === "string" ? args.days : "30", 10);
|
|
339
|
+
const store = new Store({
|
|
340
|
+
schema,
|
|
341
|
+
adapter: new BetterSqlite3Adapter(dbPath)
|
|
342
|
+
});
|
|
343
|
+
await store.open();
|
|
344
|
+
let result;
|
|
345
|
+
if (strategyName === "never") {
|
|
346
|
+
result = await store.compact({ mode: "never" });
|
|
347
|
+
} else if (strategyName === "after-days") {
|
|
348
|
+
result = await store.compact({ mode: "after-days", days });
|
|
349
|
+
} else if (strategyName === "after-ack") {
|
|
350
|
+
result = await store.compact({ mode: "after-ack" });
|
|
351
|
+
} else {
|
|
352
|
+
await store.close();
|
|
353
|
+
throw new Error(`Unknown strategy "${strategyName}". Use after-ack, after-days, or never.`);
|
|
354
|
+
}
|
|
355
|
+
await store.close();
|
|
356
|
+
logger.banner();
|
|
357
|
+
logger.success(`Compacted ${result.deletedCount} operation log entries.`);
|
|
358
|
+
for (const [nodeId, seq] of result.watermark) {
|
|
359
|
+
logger.step(` ${nodeId}: seq <= ${seq}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// src/commands/dev/dev-command.ts
|
|
365
|
+
import { existsSync, readFileSync } from "fs";
|
|
366
|
+
import { access as access2 } from "fs/promises";
|
|
367
|
+
import { join as join4 } from "path";
|
|
368
|
+
import { resolve as resolve2 } from "path";
|
|
369
|
+
import { defineCommand as defineCommand3 } from "citty";
|
|
370
|
+
|
|
371
|
+
// src/commands/dev/process-manager.ts
|
|
372
|
+
import { spawn as spawnChild } from "child_process";
|
|
373
|
+
var ProcessManager = class {
|
|
374
|
+
processes = /* @__PURE__ */ new Map();
|
|
375
|
+
spawn(config) {
|
|
376
|
+
this.processes.get(config.label)?.child.kill("SIGTERM");
|
|
377
|
+
const options = {
|
|
378
|
+
cwd: config.cwd,
|
|
379
|
+
env: { ...process.env, ...config.env },
|
|
380
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
381
|
+
};
|
|
382
|
+
const child = spawnChild(config.command, config.args, options);
|
|
383
|
+
let resolveExit;
|
|
384
|
+
const runningProcess = {
|
|
385
|
+
child,
|
|
386
|
+
exitPromise: new Promise((resolve7) => {
|
|
387
|
+
resolveExit = resolve7;
|
|
388
|
+
}),
|
|
389
|
+
stdoutBuffer: "",
|
|
390
|
+
stderrBuffer: ""
|
|
391
|
+
};
|
|
392
|
+
this.processes.set(config.label, runningProcess);
|
|
393
|
+
child.stdout?.on("data", (chunk) => {
|
|
394
|
+
runningProcess.stdoutBuffer = this.writeChunk(
|
|
395
|
+
config.label,
|
|
396
|
+
runningProcess.stdoutBuffer,
|
|
397
|
+
chunk,
|
|
398
|
+
false
|
|
399
|
+
);
|
|
400
|
+
});
|
|
401
|
+
child.stderr?.on("data", (chunk) => {
|
|
402
|
+
runningProcess.stderrBuffer = this.writeChunk(
|
|
403
|
+
config.label,
|
|
404
|
+
runningProcess.stderrBuffer,
|
|
405
|
+
chunk,
|
|
406
|
+
true
|
|
407
|
+
);
|
|
408
|
+
});
|
|
409
|
+
child.on("exit", (code, signal) => {
|
|
410
|
+
this.flushBuffer(config.label, runningProcess.stdoutBuffer, false);
|
|
411
|
+
this.flushBuffer(config.label, runningProcess.stderrBuffer, true);
|
|
412
|
+
this.processes.delete(config.label);
|
|
413
|
+
config.onExit?.(code, signal);
|
|
414
|
+
resolveExit?.();
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
hasRunning() {
|
|
418
|
+
return this.processes.size > 0;
|
|
419
|
+
}
|
|
420
|
+
async restart(config) {
|
|
421
|
+
await this.stop(config.label);
|
|
422
|
+
this.spawn(config);
|
|
423
|
+
}
|
|
424
|
+
async stop(label) {
|
|
425
|
+
const running = this.processes.get(label);
|
|
426
|
+
if (!running) return;
|
|
427
|
+
running.child.kill("SIGTERM");
|
|
428
|
+
await Promise.race([running.exitPromise, delay(5e3)]);
|
|
429
|
+
const remaining = this.processes.get(label);
|
|
430
|
+
if (!remaining) return;
|
|
431
|
+
remaining.child.kill("SIGKILL");
|
|
432
|
+
await remaining.exitPromise;
|
|
433
|
+
}
|
|
434
|
+
async shutdownAll() {
|
|
435
|
+
const running = Array.from(this.processes.values());
|
|
436
|
+
if (running.length === 0) return;
|
|
437
|
+
for (const processEntry of running) {
|
|
438
|
+
processEntry.child.kill("SIGTERM");
|
|
439
|
+
}
|
|
440
|
+
await Promise.race([Promise.all(running.map((entry) => entry.exitPromise)), delay(5e3)]);
|
|
441
|
+
const remaining = Array.from(this.processes.values());
|
|
442
|
+
if (remaining.length === 0) return;
|
|
443
|
+
for (const processEntry of remaining) {
|
|
444
|
+
processEntry.child.kill("SIGKILL");
|
|
445
|
+
}
|
|
446
|
+
await Promise.all(remaining.map((entry) => entry.exitPromise));
|
|
447
|
+
}
|
|
448
|
+
writeChunk(label, buffer, chunk, isError) {
|
|
449
|
+
const combined = `${buffer}${chunk.toString("utf-8")}`;
|
|
450
|
+
const lines = combined.split(/\r?\n/);
|
|
451
|
+
const remaining = lines.pop() ?? "";
|
|
452
|
+
for (const line of lines) {
|
|
453
|
+
this.writeLine(label, line, isError);
|
|
454
|
+
}
|
|
455
|
+
return remaining;
|
|
456
|
+
}
|
|
457
|
+
flushBuffer(label, buffer, isError) {
|
|
458
|
+
if (!buffer) return;
|
|
459
|
+
this.writeLine(label, buffer, isError);
|
|
460
|
+
}
|
|
461
|
+
writeLine(label, line, isError) {
|
|
462
|
+
const stream = isError ? process.stderr : process.stdout;
|
|
463
|
+
stream.write(`[${label}] ${line}
|
|
464
|
+
`);
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
function delay(ms) {
|
|
468
|
+
return new Promise((resolve7) => {
|
|
469
|
+
setTimeout(resolve7, ms);
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/commands/dev/schema-watcher.ts
|
|
474
|
+
import { spawn as spawn2 } from "child_process";
|
|
475
|
+
import { watch } from "fs";
|
|
476
|
+
import { join as join3 } from "path";
|
|
477
|
+
var SchemaWatcher = class {
|
|
478
|
+
constructor(config) {
|
|
479
|
+
this.config = config;
|
|
480
|
+
this.debounceMs = config.debounceMs ?? 300;
|
|
481
|
+
}
|
|
482
|
+
config;
|
|
483
|
+
debounceMs;
|
|
484
|
+
watcher = null;
|
|
485
|
+
debounceTimer = null;
|
|
486
|
+
schemaVersion = null;
|
|
487
|
+
start() {
|
|
488
|
+
if (this.watcher) return;
|
|
489
|
+
this.watcher = watch(this.config.schemaPath, () => {
|
|
490
|
+
this.scheduleRegeneration();
|
|
491
|
+
});
|
|
492
|
+
this.watcher.on("error", (error) => {
|
|
493
|
+
this.config.onError?.(toError(error));
|
|
494
|
+
});
|
|
495
|
+
void this.refreshSchemaVersion().catch((error) => {
|
|
496
|
+
this.config.onError?.(toError(error));
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
stop() {
|
|
500
|
+
if (this.debounceTimer) {
|
|
501
|
+
clearTimeout(this.debounceTimer);
|
|
502
|
+
this.debounceTimer = null;
|
|
503
|
+
}
|
|
504
|
+
this.watcher?.close();
|
|
505
|
+
this.watcher = null;
|
|
506
|
+
}
|
|
507
|
+
async regenerate() {
|
|
508
|
+
const koraBinJs = join3(
|
|
509
|
+
this.config.projectRoot,
|
|
510
|
+
"node_modules",
|
|
511
|
+
"@korajs",
|
|
512
|
+
"cli",
|
|
513
|
+
"dist",
|
|
514
|
+
"bin.js"
|
|
515
|
+
);
|
|
516
|
+
const hasTsx = await hasTsxInstalled(this.config.projectRoot);
|
|
517
|
+
const command = process.execPath;
|
|
518
|
+
const args = hasTsx ? ["--import", "tsx", koraBinJs, "generate", "types", "--schema", this.config.schemaPath] : [koraBinJs, "generate", "types", "--schema", this.config.schemaPath];
|
|
519
|
+
await spawnCommand(command, args, this.config.projectRoot);
|
|
520
|
+
this.config.onRegenerate?.();
|
|
521
|
+
await this.refreshSchemaVersion();
|
|
522
|
+
}
|
|
523
|
+
async refreshSchemaVersion() {
|
|
524
|
+
const schema = await loadSchemaDefinition(this.config.schemaPath, this.config.projectRoot);
|
|
525
|
+
const previous = this.schemaVersion;
|
|
526
|
+
this.schemaVersion = schema.version;
|
|
527
|
+
if (previous !== null && previous !== schema.version) {
|
|
528
|
+
this.config.onSchemaVersionChange?.({ previous, next: schema.version });
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
scheduleRegeneration() {
|
|
532
|
+
if (this.debounceTimer) {
|
|
533
|
+
clearTimeout(this.debounceTimer);
|
|
534
|
+
}
|
|
535
|
+
this.debounceTimer = setTimeout(() => {
|
|
536
|
+
this.debounceTimer = null;
|
|
537
|
+
void this.regenerate().catch((error) => {
|
|
538
|
+
this.config.onError?.(toError(error));
|
|
539
|
+
});
|
|
540
|
+
}, this.debounceMs);
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
async function spawnCommand(command, args, cwd) {
|
|
544
|
+
await new Promise((resolve7, reject) => {
|
|
545
|
+
const child = spawn2(command, args, {
|
|
546
|
+
cwd,
|
|
547
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
548
|
+
env: process.env
|
|
549
|
+
});
|
|
550
|
+
child.stdout?.on("data", (chunk) => {
|
|
551
|
+
writePrefixedLines(chunk, false);
|
|
552
|
+
});
|
|
553
|
+
child.stderr?.on("data", (chunk) => {
|
|
554
|
+
writePrefixedLines(chunk, true);
|
|
555
|
+
});
|
|
556
|
+
child.on("error", (error) => {
|
|
557
|
+
reject(error);
|
|
558
|
+
});
|
|
559
|
+
child.on("exit", (code) => {
|
|
560
|
+
if (code === 0) {
|
|
561
|
+
resolve7();
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
reject(new Error(`Type generation exited with code ${code ?? "unknown"}.`));
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
function writePrefixedLines(chunk, isError) {
|
|
569
|
+
const text = chunk.toString("utf-8");
|
|
570
|
+
const lines = text.split(/\r?\n/).filter((line) => line.length > 0);
|
|
571
|
+
const stream = isError ? process.stderr : process.stdout;
|
|
572
|
+
for (const line of lines) {
|
|
573
|
+
stream.write(`[kora] ${line}
|
|
574
|
+
`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function toError(error) {
|
|
578
|
+
if (error instanceof Error) return error;
|
|
579
|
+
return new Error(String(error));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/commands/dev/dev-command.ts
|
|
583
|
+
var devCommand = defineCommand3({
|
|
584
|
+
meta: {
|
|
585
|
+
name: "dev",
|
|
586
|
+
description: "Start the Kora development environment"
|
|
587
|
+
},
|
|
588
|
+
args: {
|
|
589
|
+
port: {
|
|
590
|
+
type: "string",
|
|
591
|
+
description: "Vite dev server port"
|
|
592
|
+
},
|
|
593
|
+
"sync-port": {
|
|
594
|
+
type: "string",
|
|
595
|
+
description: "Kora sync server port"
|
|
596
|
+
},
|
|
597
|
+
"no-sync": {
|
|
598
|
+
type: "boolean",
|
|
599
|
+
description: "Disable sync server startup",
|
|
600
|
+
default: false
|
|
601
|
+
},
|
|
602
|
+
"no-watch": {
|
|
603
|
+
type: "boolean",
|
|
604
|
+
description: "Disable schema file watching",
|
|
605
|
+
default: false
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
async run({ args }) {
|
|
609
|
+
const logger = createLogger();
|
|
610
|
+
const projectRoot = await findProjectRoot();
|
|
611
|
+
if (!projectRoot) {
|
|
612
|
+
throw new InvalidProjectError(process.cwd());
|
|
613
|
+
}
|
|
614
|
+
loadProjectEnv(projectRoot);
|
|
615
|
+
const config = await loadKoraConfig(projectRoot);
|
|
616
|
+
const vitePort = typeof args.port === "string" ? args.port : String(config?.dev?.port ?? 5173);
|
|
617
|
+
const syncPortFromConfig = typeof config?.dev?.sync === "object" && typeof config.dev.sync.port === "number" ? config.dev.sync.port : 3001;
|
|
618
|
+
const syncPort = typeof args["sync-port"] === "string" ? args["sync-port"] : String(syncPortFromConfig);
|
|
619
|
+
const configSyncEnabled = config?.dev?.sync === void 0 || config.dev.sync === true || typeof config.dev.sync === "object" && config.dev.sync.enabled !== false;
|
|
620
|
+
const configWatchEnabled = config?.dev?.watch === void 0 || config.dev.watch === true || typeof config.dev.watch === "object" && config.dev.watch.enabled !== false;
|
|
621
|
+
const watchDebounceMs = typeof config?.dev?.watch === "object" && typeof config.dev.watch.debounceMs === "number" ? config.dev.watch.debounceMs : 300;
|
|
622
|
+
const viteEntryPoint = await resolveProjectBinaryEntryPoint(projectRoot, "vite", "vite");
|
|
623
|
+
if (!viteEntryPoint) {
|
|
624
|
+
throw new DevServerError("vite", join4(projectRoot, "node_modules", ".bin", "vite"));
|
|
625
|
+
}
|
|
626
|
+
const syncServerFile = await findSyncServerFile(projectRoot);
|
|
627
|
+
let managedSyncStore = normalizeManagedSyncStore(config, projectRoot);
|
|
628
|
+
const postgresEnvRequested = isPostgresEnvRequested(config);
|
|
629
|
+
const syncAllowed = args["no-sync"] !== true && configSyncEnabled;
|
|
630
|
+
let shouldStartSync = syncAllowed && (syncServerFile !== null || managedSyncStore !== null);
|
|
631
|
+
let hasTsx = false;
|
|
632
|
+
if (shouldStartSync && syncServerFile !== null) {
|
|
633
|
+
hasTsx = await hasTsxInstalled(projectRoot);
|
|
634
|
+
if (!hasTsx) {
|
|
635
|
+
logger.warn('Sync server detected, but "tsx" is not installed. Skipping sync.');
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (shouldStartSync && syncServerFile === null && managedSyncStore) {
|
|
639
|
+
const hasServerPackage = await fileExists(
|
|
640
|
+
join4(projectRoot, "node_modules", "@korajs", "server", "package.json")
|
|
641
|
+
);
|
|
642
|
+
if (!hasServerPackage) {
|
|
643
|
+
logger.warn(
|
|
644
|
+
"Managed sync is configured, but @korajs/server is not installed. Install it or add server.ts."
|
|
645
|
+
);
|
|
646
|
+
managedSyncStore = null;
|
|
647
|
+
shouldStartSync = syncAllowed && (syncServerFile !== null || managedSyncStore !== null);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (syncAllowed && syncServerFile === null && managedSyncStore === null && postgresEnvRequested) {
|
|
651
|
+
logger.warn(
|
|
652
|
+
"Managed postgres sync requested but no connection string found. Set dev.sync.store.connectionString or DATABASE_URL."
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
let configuredSchemaPath = null;
|
|
656
|
+
if (typeof config?.schema === "string") {
|
|
657
|
+
const candidate = resolve2(projectRoot, config.schema);
|
|
658
|
+
if (await fileExists(candidate)) {
|
|
659
|
+
configuredSchemaPath = candidate;
|
|
660
|
+
} else {
|
|
661
|
+
logger.warn(
|
|
662
|
+
`Configured schema file not found: ${candidate}. Falling back to auto-detection.`
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const schemaPath = configuredSchemaPath ?? await findSchemaFile(projectRoot);
|
|
667
|
+
const watchEnabled = args["no-watch"] !== true && configWatchEnabled && schemaPath !== null;
|
|
668
|
+
const processManager = new ProcessManager();
|
|
669
|
+
let schemaWatcher = null;
|
|
670
|
+
let shuttingDown = false;
|
|
671
|
+
let resolveFinished;
|
|
672
|
+
const finished = new Promise((resolve7) => {
|
|
673
|
+
resolveFinished = resolve7;
|
|
674
|
+
});
|
|
675
|
+
const onManagedProcessExit = () => {
|
|
676
|
+
if (!processManager.hasRunning() && !shuttingDown) {
|
|
677
|
+
resolveFinished?.();
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
const shutdown = async () => {
|
|
681
|
+
if (shuttingDown) return;
|
|
682
|
+
shuttingDown = true;
|
|
683
|
+
schemaWatcher?.stop();
|
|
684
|
+
await processManager.shutdownAll();
|
|
685
|
+
resolveFinished?.();
|
|
686
|
+
};
|
|
687
|
+
const onSigInt = () => {
|
|
688
|
+
void shutdown();
|
|
689
|
+
};
|
|
690
|
+
const onSigTerm = () => {
|
|
691
|
+
void shutdown();
|
|
692
|
+
};
|
|
693
|
+
process.on("SIGINT", onSigInt);
|
|
694
|
+
process.on("SIGTERM", onSigTerm);
|
|
695
|
+
logger.banner();
|
|
696
|
+
logger.info("Starting development environment:");
|
|
697
|
+
logger.blank();
|
|
698
|
+
logger.step(` Vite dev server on port ${vitePort}`);
|
|
699
|
+
if (shouldStartSync && hasTsx && syncServerFile) {
|
|
700
|
+
logger.step(` Sync server on port ${syncPort}`);
|
|
701
|
+
} else if (shouldStartSync && syncServerFile === null && managedSyncStore !== null) {
|
|
702
|
+
logger.step(` Managed sync server on port ${syncPort} (${managedSyncStore.type})`);
|
|
703
|
+
} else if (syncAllowed && syncServerFile === null) {
|
|
704
|
+
logger.step(" Sync server configured but no server.ts/server.js or managed store found");
|
|
705
|
+
} else if (!syncAllowed) {
|
|
706
|
+
logger.step(" Sync server disabled via --no-sync");
|
|
707
|
+
}
|
|
708
|
+
if (watchEnabled && schemaPath) {
|
|
709
|
+
logger.step(` Schema watcher enabled (${schemaPath})`);
|
|
710
|
+
} else if (args["no-watch"] === true) {
|
|
711
|
+
logger.step(" Schema watcher disabled via --no-watch");
|
|
712
|
+
} else {
|
|
713
|
+
logger.step(" Schema watcher disabled (schema.ts not found)");
|
|
714
|
+
}
|
|
715
|
+
logger.blank();
|
|
716
|
+
logger.step(
|
|
717
|
+
" Kora DevTools overlay: Ctrl+Shift+K (Cmd+Shift+K on macOS) when devtools is enabled"
|
|
718
|
+
);
|
|
719
|
+
processManager.spawn({
|
|
720
|
+
label: "vite",
|
|
721
|
+
command: process.execPath,
|
|
722
|
+
args: [viteEntryPoint, "--port", String(vitePort)],
|
|
723
|
+
cwd: projectRoot,
|
|
724
|
+
env: {
|
|
725
|
+
KORA_DEV: "1"
|
|
726
|
+
},
|
|
727
|
+
onExit: onManagedProcessExit
|
|
728
|
+
});
|
|
729
|
+
const syncProcessConfig = shouldStartSync && hasTsx && syncServerFile ? {
|
|
730
|
+
label: "sync",
|
|
731
|
+
command: process.execPath,
|
|
732
|
+
args: ["--import", "tsx", syncServerFile],
|
|
733
|
+
cwd: projectRoot,
|
|
734
|
+
env: {
|
|
735
|
+
PORT: String(syncPort),
|
|
736
|
+
KORA_SYNC_PORT: String(syncPort)
|
|
737
|
+
},
|
|
738
|
+
onExit: onManagedProcessExit
|
|
739
|
+
} : shouldStartSync && syncServerFile === null && managedSyncStore !== null ? {
|
|
740
|
+
label: "sync",
|
|
741
|
+
command: process.execPath,
|
|
742
|
+
args: ["--input-type=module", "--eval", MANAGED_SYNC_BOOTSTRAP_SCRIPT],
|
|
743
|
+
cwd: projectRoot,
|
|
744
|
+
env: {
|
|
745
|
+
KORA_DEV_SYNC_CONFIG: JSON.stringify({
|
|
746
|
+
port: Number(syncPort),
|
|
747
|
+
store: managedSyncStore
|
|
748
|
+
})
|
|
749
|
+
},
|
|
750
|
+
onExit: onManagedProcessExit
|
|
751
|
+
} : null;
|
|
752
|
+
if (syncProcessConfig) {
|
|
753
|
+
processManager.spawn(syncProcessConfig);
|
|
754
|
+
}
|
|
755
|
+
if (watchEnabled && schemaPath) {
|
|
756
|
+
schemaWatcher = new SchemaWatcher({
|
|
757
|
+
schemaPath,
|
|
758
|
+
projectRoot,
|
|
759
|
+
debounceMs: watchDebounceMs,
|
|
760
|
+
onRegenerate: () => {
|
|
761
|
+
logger.success("Regenerated types from schema changes");
|
|
762
|
+
},
|
|
763
|
+
onSchemaVersionChange: ({ previous, next }) => {
|
|
764
|
+
if (!syncProcessConfig) {
|
|
765
|
+
logger.warn(
|
|
766
|
+
`Schema version changed from ${previous} to ${next}; restart your sync server before clients can sync.`
|
|
767
|
+
);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
logger.warn(`Schema version changed from ${previous} to ${next}; restarting sync server.`);
|
|
771
|
+
void processManager.restart(syncProcessConfig);
|
|
772
|
+
},
|
|
773
|
+
onError: (error) => {
|
|
774
|
+
logger.error(`Schema watcher error: ${error.message}`);
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
schemaWatcher.start();
|
|
778
|
+
}
|
|
779
|
+
await finished;
|
|
780
|
+
process.off("SIGINT", onSigInt);
|
|
781
|
+
process.off("SIGTERM", onSigTerm);
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
async function findSyncServerFile(projectRoot) {
|
|
785
|
+
const candidates = [join4(projectRoot, "server.ts"), join4(projectRoot, "server.js")];
|
|
786
|
+
for (const candidate of candidates) {
|
|
787
|
+
try {
|
|
788
|
+
await access2(candidate);
|
|
789
|
+
return candidate;
|
|
790
|
+
} catch {
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
async function fileExists(path) {
|
|
796
|
+
try {
|
|
797
|
+
await access2(path);
|
|
798
|
+
return true;
|
|
799
|
+
} catch {
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
function loadProjectEnv(projectRoot, mode = "development") {
|
|
804
|
+
const files = [".env", ".env.local", `.env.${mode}`, `.env.${mode}.local`];
|
|
805
|
+
const existingKeys = new Set(Object.keys(process.env));
|
|
806
|
+
for (const file of files) {
|
|
807
|
+
const path = join4(projectRoot, file);
|
|
808
|
+
if (!existsSync(path)) continue;
|
|
809
|
+
const parsed = parseEnvFile(readFileSync(path, "utf8"));
|
|
810
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
811
|
+
if (!existingKeys.has(key)) {
|
|
812
|
+
process.env[key] = value;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
function parseEnvFile(contents) {
|
|
818
|
+
const env = {};
|
|
819
|
+
for (const rawLine of contents.split(/\r?\n/)) {
|
|
820
|
+
const line = rawLine.trim();
|
|
821
|
+
if (!line || line.startsWith("#")) continue;
|
|
822
|
+
const normalized = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
|
|
823
|
+
const equalsIndex = normalized.indexOf("=");
|
|
824
|
+
if (equalsIndex <= 0) continue;
|
|
825
|
+
const key = normalized.slice(0, equalsIndex).trim();
|
|
826
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
|
|
827
|
+
env[key] = parseEnvValue(normalized.slice(equalsIndex + 1).trim());
|
|
828
|
+
}
|
|
829
|
+
return env;
|
|
830
|
+
}
|
|
831
|
+
function parseEnvValue(value) {
|
|
832
|
+
const quote = value[0];
|
|
833
|
+
if ((quote === '"' || quote === "'") && value[value.length - 1] === quote) {
|
|
834
|
+
const inner = value.slice(1, -1);
|
|
835
|
+
return quote === '"' ? inner.replace(/\\n/g, "\n").replace(/\\r/g, "\r") : inner;
|
|
836
|
+
}
|
|
837
|
+
const commentIndex = value.search(/\s#/);
|
|
838
|
+
return (commentIndex >= 0 ? value.slice(0, commentIndex) : value).trim();
|
|
839
|
+
}
|
|
840
|
+
function normalizeManagedSyncStore(config, projectRoot) {
|
|
841
|
+
const sync = config?.dev?.sync;
|
|
842
|
+
if (typeof sync !== "object" || sync === null) return null;
|
|
843
|
+
const store = sync.store;
|
|
844
|
+
if (store === void 0) return { type: "memory" };
|
|
845
|
+
if (store === "memory") return { type: "memory" };
|
|
846
|
+
if (store === "sqlite") return { type: "sqlite", filename: join4(projectRoot, "kora-sync.db") };
|
|
847
|
+
if (store === "postgres") {
|
|
848
|
+
const connectionString = process.env.DATABASE_URL;
|
|
849
|
+
if (!connectionString) return null;
|
|
850
|
+
return { type: "postgres", connectionString };
|
|
851
|
+
}
|
|
852
|
+
if (typeof store === "object" && store !== null) {
|
|
853
|
+
if (store.type === "memory") return { type: "memory" };
|
|
854
|
+
if (store.type === "sqlite") {
|
|
855
|
+
const filename = typeof store.filename === "string" && store.filename.length > 0 ? resolve2(projectRoot, store.filename) : join4(projectRoot, "kora-sync.db");
|
|
856
|
+
return { type: "sqlite", filename };
|
|
857
|
+
}
|
|
858
|
+
if (store.type === "postgres" && typeof store.connectionString === "string") {
|
|
859
|
+
return { type: "postgres", connectionString: store.connectionString };
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
function isPostgresEnvRequested(config) {
|
|
865
|
+
const sync = config?.dev?.sync;
|
|
866
|
+
if (typeof sync !== "object" || sync === null) return false;
|
|
867
|
+
if (sync.store === "postgres") return true;
|
|
868
|
+
if (typeof sync.store === "object" && sync.store !== null && sync.store.type === "postgres")
|
|
869
|
+
return true;
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
var MANAGED_SYNC_BOOTSTRAP_SCRIPT = `
|
|
873
|
+
const config = JSON.parse(process.env.KORA_DEV_SYNC_CONFIG ?? '{}');
|
|
874
|
+
const {
|
|
875
|
+
createKoraServer,
|
|
876
|
+
MemoryServerStore,
|
|
877
|
+
createSqliteServerStore,
|
|
878
|
+
createPostgresServerStore,
|
|
879
|
+
} = await import('@korajs/server');
|
|
880
|
+
const storeConfig = config.store ?? { type: 'memory' };
|
|
881
|
+
let store;
|
|
882
|
+
if (storeConfig.type === 'memory') {
|
|
883
|
+
store = new MemoryServerStore();
|
|
884
|
+
} else if (storeConfig.type === 'sqlite') {
|
|
885
|
+
const filename = typeof storeConfig.filename === 'string' && storeConfig.filename.length > 0
|
|
886
|
+
? storeConfig.filename
|
|
887
|
+
: './kora-sync.db';
|
|
888
|
+
store = createSqliteServerStore({ filename });
|
|
889
|
+
} else if (storeConfig.type === 'postgres') {
|
|
890
|
+
if (typeof storeConfig.connectionString !== 'string' || storeConfig.connectionString.length === 0) {
|
|
891
|
+
throw new Error('Managed postgres sync requires a connectionString');
|
|
892
|
+
}
|
|
893
|
+
store = await createPostgresServerStore({
|
|
894
|
+
connectionString: storeConfig.connectionString,
|
|
895
|
+
});
|
|
896
|
+
} else {
|
|
897
|
+
throw new Error('Unsupported managed sync store type: ' + String(storeConfig.type));
|
|
898
|
+
}
|
|
899
|
+
const server = createKoraServer({ store, port: Number(config.port ?? 3001) });
|
|
900
|
+
const shutdown = async () => {
|
|
901
|
+
try {
|
|
902
|
+
await server.stop();
|
|
903
|
+
} catch {
|
|
904
|
+
}
|
|
905
|
+
process.exit(0);
|
|
906
|
+
};
|
|
907
|
+
process.on('SIGINT', () => {
|
|
908
|
+
void shutdown();
|
|
909
|
+
});
|
|
910
|
+
process.on('SIGTERM', () => {
|
|
911
|
+
void shutdown();
|
|
912
|
+
});
|
|
913
|
+
await server.start();
|
|
914
|
+
process.stdout.write('Managed sync server running on ws://localhost:' + String(config.port ?? 3001) + '\\n');
|
|
915
|
+
await new Promise(() => {});
|
|
916
|
+
`;
|
|
917
|
+
|
|
918
|
+
// src/commands/doctor/doctor-command.ts
|
|
919
|
+
import { defineCommand as defineCommand4 } from "citty";
|
|
920
|
+
|
|
921
|
+
// src/commands/doctor/doctor-checks.ts
|
|
922
|
+
import { access as access3, readFile as readFile2 } from "fs/promises";
|
|
923
|
+
import { join as join5, resolve as resolve3 } from "path";
|
|
924
|
+
var STATUS_ENDPOINT = "/__kora/status";
|
|
925
|
+
async function runDoctorChecks(options) {
|
|
926
|
+
const { projectRoot } = options;
|
|
927
|
+
const results = [];
|
|
928
|
+
results.push({
|
|
929
|
+
id: "project-root",
|
|
930
|
+
title: "Project root",
|
|
931
|
+
status: "pass",
|
|
932
|
+
message: projectRoot
|
|
933
|
+
});
|
|
934
|
+
const config = await loadKoraConfig(projectRoot);
|
|
935
|
+
if (!config) {
|
|
936
|
+
results.push({
|
|
937
|
+
id: "kora-config",
|
|
938
|
+
title: "kora.config",
|
|
939
|
+
status: "warn",
|
|
940
|
+
message: "No kora.config.* file found.",
|
|
941
|
+
fix: "Add kora.config.ts with schema path and dev settings (see create-kora-app templates)."
|
|
942
|
+
});
|
|
943
|
+
} else {
|
|
944
|
+
results.push({
|
|
945
|
+
id: "kora-config",
|
|
946
|
+
title: "kora.config",
|
|
947
|
+
status: "pass",
|
|
948
|
+
message: "Configuration file loaded."
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
const schemaPath = await resolveSchemaPath(projectRoot, config?.schema);
|
|
952
|
+
if (!schemaPath) {
|
|
953
|
+
results.push({
|
|
954
|
+
id: "schema",
|
|
955
|
+
title: "Schema",
|
|
956
|
+
status: "fail",
|
|
957
|
+
message: "Could not find schema.ts in src/ or project root.",
|
|
958
|
+
fix: "Add src/schema.ts or set schema in kora.config.ts."
|
|
959
|
+
});
|
|
960
|
+
} else {
|
|
961
|
+
try {
|
|
962
|
+
const schema = await loadSchemaDefinition(schemaPath, projectRoot);
|
|
963
|
+
results.push({
|
|
964
|
+
id: "schema",
|
|
965
|
+
title: "Schema",
|
|
966
|
+
status: "pass",
|
|
967
|
+
message: `Loaded schema v${schema.version} (${Object.keys(schema.collections).length} collection(s)).`
|
|
968
|
+
});
|
|
969
|
+
const workerCheck = await checkWorkerFile(projectRoot);
|
|
970
|
+
results.push(workerCheck);
|
|
971
|
+
const depsCheck = await checkDependencies(projectRoot);
|
|
972
|
+
results.push(depsCheck);
|
|
973
|
+
results.push(opfsBrowserNote());
|
|
974
|
+
if (!options.skipNetwork) {
|
|
975
|
+
const syncUrl = options.syncHttpUrl ?? await resolveSyncHttpUrl(projectRoot, config);
|
|
976
|
+
const syncChecks = await checkSyncServer(syncUrl, schema);
|
|
977
|
+
results.push(...syncChecks);
|
|
978
|
+
} else {
|
|
979
|
+
results.push({
|
|
980
|
+
id: "sync-server",
|
|
981
|
+
title: "Sync server",
|
|
982
|
+
status: "skip",
|
|
983
|
+
message: "Network checks skipped."
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
} catch (error) {
|
|
987
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
988
|
+
results.push({
|
|
989
|
+
id: "schema",
|
|
990
|
+
title: "Schema",
|
|
991
|
+
status: "fail",
|
|
992
|
+
message: `Failed to load schema: ${message}`,
|
|
993
|
+
fix: "Ensure the schema exports defineSchema() as default and TypeScript can be evaluated (install tsx)."
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return results;
|
|
998
|
+
}
|
|
999
|
+
async function resolveSchemaPath(projectRoot, configSchema) {
|
|
1000
|
+
if (typeof configSchema === "string") {
|
|
1001
|
+
const resolved = resolve3(projectRoot, configSchema);
|
|
1002
|
+
try {
|
|
1003
|
+
await access3(resolved);
|
|
1004
|
+
return resolved;
|
|
1005
|
+
} catch {
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
return findSchemaFile(projectRoot);
|
|
1009
|
+
}
|
|
1010
|
+
async function checkWorkerFile(projectRoot) {
|
|
1011
|
+
const candidates = [
|
|
1012
|
+
join5(projectRoot, "src", "kora-worker.ts"),
|
|
1013
|
+
join5(projectRoot, "src", "kora-worker.js")
|
|
1014
|
+
];
|
|
1015
|
+
for (const path of candidates) {
|
|
1016
|
+
try {
|
|
1017
|
+
await access3(path);
|
|
1018
|
+
return {
|
|
1019
|
+
id: "worker",
|
|
1020
|
+
title: "SQLite WASM worker",
|
|
1021
|
+
status: "pass",
|
|
1022
|
+
message: `Found ${path.replace(projectRoot, ".")}.`
|
|
1023
|
+
};
|
|
1024
|
+
} catch {
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
return {
|
|
1028
|
+
id: "worker",
|
|
1029
|
+
title: "SQLite WASM worker",
|
|
1030
|
+
status: "warn",
|
|
1031
|
+
message: "No src/kora-worker.ts found.",
|
|
1032
|
+
fix: "Add kora-worker.ts and pass store.workerUrl from Vite (?worker&url) in createApp()."
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
async function checkDependencies(projectRoot) {
|
|
1036
|
+
try {
|
|
1037
|
+
const raw = await readFile2(join5(projectRoot, "package.json"), "utf-8");
|
|
1038
|
+
const pkg = JSON.parse(raw);
|
|
1039
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1040
|
+
const hasKora = Boolean(deps.korajs || deps.kora);
|
|
1041
|
+
const hasStore = Boolean(deps["@korajs/store"]);
|
|
1042
|
+
if (hasKora || hasStore) {
|
|
1043
|
+
return {
|
|
1044
|
+
id: "dependencies",
|
|
1045
|
+
title: "Dependencies",
|
|
1046
|
+
status: "pass",
|
|
1047
|
+
message: hasKora ? "korajs is listed in package.json." : "@korajs/store is listed in package.json."
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
return {
|
|
1051
|
+
id: "dependencies",
|
|
1052
|
+
title: "Dependencies",
|
|
1053
|
+
status: "fail",
|
|
1054
|
+
message: "No korajs or @korajs/store dependency found.",
|
|
1055
|
+
fix: "Run pnpm add korajs @korajs/react (or your package manager equivalent)."
|
|
1056
|
+
};
|
|
1057
|
+
} catch {
|
|
1058
|
+
return {
|
|
1059
|
+
id: "dependencies",
|
|
1060
|
+
title: "Dependencies",
|
|
1061
|
+
status: "fail",
|
|
1062
|
+
message: "Could not read package.json.",
|
|
1063
|
+
fix: "Run this command from a Kora app directory with a valid package.json."
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
function opfsBrowserNote() {
|
|
1068
|
+
return {
|
|
1069
|
+
id: "opfs",
|
|
1070
|
+
title: "OPFS persistence",
|
|
1071
|
+
status: "skip",
|
|
1072
|
+
message: "OPFS availability is checked in the browser at runtime. Open the app and confirm no IndexedDB fallback warning in the console.",
|
|
1073
|
+
fix: "Use a Chromium-based browser with storage access; avoid private mode if persistence fails."
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
async function resolveSyncHttpUrl(projectRoot, config) {
|
|
1077
|
+
const envUrl = await readEnvSyncUrl(projectRoot);
|
|
1078
|
+
if (envUrl) {
|
|
1079
|
+
return toSyncStatusHttpBase(envUrl);
|
|
1080
|
+
}
|
|
1081
|
+
const syncPort = typeof config?.dev?.sync === "object" && typeof config.dev.sync.port === "number" ? config.dev.sync.port : 3001;
|
|
1082
|
+
return `http://localhost:${syncPort}`;
|
|
1083
|
+
}
|
|
1084
|
+
async function readEnvSyncUrl(projectRoot) {
|
|
1085
|
+
for (const file of [".env", ".env.local"]) {
|
|
1086
|
+
const path = join5(projectRoot, file);
|
|
1087
|
+
try {
|
|
1088
|
+
const content = await readFile2(path, "utf-8");
|
|
1089
|
+
for (const line of content.split("\n")) {
|
|
1090
|
+
const trimmed = line.trim();
|
|
1091
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1092
|
+
const match = trimmed.match(/^VITE_SYNC_URL=(.+)$/);
|
|
1093
|
+
if (match?.[1]) {
|
|
1094
|
+
return match[1].trim().replace(/^["']|["']$/g, "");
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
} catch {
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
function toSyncStatusHttpBase(url) {
|
|
1103
|
+
try {
|
|
1104
|
+
const parsed = new URL(url);
|
|
1105
|
+
if (parsed.protocol === "ws:") {
|
|
1106
|
+
parsed.protocol = "http:";
|
|
1107
|
+
} else if (parsed.protocol === "wss:") {
|
|
1108
|
+
parsed.protocol = "https:";
|
|
1109
|
+
}
|
|
1110
|
+
parsed.pathname = "";
|
|
1111
|
+
parsed.search = "";
|
|
1112
|
+
parsed.hash = "";
|
|
1113
|
+
return parsed.toString().replace(/\/$/, "");
|
|
1114
|
+
} catch {
|
|
1115
|
+
return "http://localhost:3001";
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
async function checkSyncServer(httpBase, schema) {
|
|
1119
|
+
const statusUrl = `${httpBase.replace(/\/$/, "")}${STATUS_ENDPOINT}`;
|
|
1120
|
+
try {
|
|
1121
|
+
const response = await fetch(statusUrl, {
|
|
1122
|
+
signal: AbortSignal.timeout(5e3)
|
|
1123
|
+
});
|
|
1124
|
+
if (!response.ok) {
|
|
1125
|
+
return [
|
|
1126
|
+
{
|
|
1127
|
+
id: "sync-server",
|
|
1128
|
+
title: "Sync server",
|
|
1129
|
+
status: "fail",
|
|
1130
|
+
message: `GET ${statusUrl} returned ${response.status}.`,
|
|
1131
|
+
fix: "Start the sync server with `kora dev` or `pnpm run dev` in a sync template."
|
|
1132
|
+
}
|
|
1133
|
+
];
|
|
1134
|
+
}
|
|
1135
|
+
const body = await response.json();
|
|
1136
|
+
const results = [
|
|
1137
|
+
{
|
|
1138
|
+
id: "sync-server",
|
|
1139
|
+
title: "Sync server",
|
|
1140
|
+
status: "pass",
|
|
1141
|
+
message: `Reachable at ${statusUrl} (server v${body.version ?? "unknown"}).`
|
|
1142
|
+
}
|
|
1143
|
+
];
|
|
1144
|
+
if (typeof body.schemaVersion === "number" && body.schemaVersion !== schema.version) {
|
|
1145
|
+
results.push({
|
|
1146
|
+
id: "schema-version",
|
|
1147
|
+
title: "Schema version",
|
|
1148
|
+
status: "warn",
|
|
1149
|
+
message: `Client schema v${schema.version} differs from server schema v${body.schemaVersion}.`,
|
|
1150
|
+
fix: "Run migrations on the server and client, or align schema.version before syncing."
|
|
1151
|
+
});
|
|
1152
|
+
} else {
|
|
1153
|
+
results.push({
|
|
1154
|
+
id: "schema-version",
|
|
1155
|
+
title: "Schema version",
|
|
1156
|
+
status: "pass",
|
|
1157
|
+
message: `Client and server both use schema v${schema.version}.`
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
return results;
|
|
1161
|
+
} catch (error) {
|
|
1162
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1163
|
+
return [
|
|
1164
|
+
{
|
|
1165
|
+
id: "sync-server",
|
|
1166
|
+
title: "Sync server",
|
|
1167
|
+
status: "warn",
|
|
1168
|
+
message: `Could not reach ${statusUrl}: ${detail}`,
|
|
1169
|
+
fix: "Start the sync server, or pass --skip-network for local-only checks."
|
|
1170
|
+
}
|
|
1171
|
+
];
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
function doctorHasFailures(results) {
|
|
1175
|
+
return results.some((r) => r.status === "fail");
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// src/commands/doctor/doctor-command.ts
|
|
1179
|
+
var doctorCommand = defineCommand4({
|
|
1180
|
+
meta: {
|
|
1181
|
+
name: "doctor",
|
|
1182
|
+
description: "Check Kora project setup (schema, worker, sync server, versions)"
|
|
1183
|
+
},
|
|
1184
|
+
args: {
|
|
1185
|
+
url: {
|
|
1186
|
+
type: "string",
|
|
1187
|
+
description: "Sync server HTTP base URL for status probe (default: from .env or localhost:3001)"
|
|
1188
|
+
},
|
|
1189
|
+
"skip-network": {
|
|
1190
|
+
type: "boolean",
|
|
1191
|
+
description: "Skip sync server and schema version network checks",
|
|
1192
|
+
default: false
|
|
1193
|
+
}
|
|
1194
|
+
},
|
|
1195
|
+
async run({ args }) {
|
|
1196
|
+
const logger = createLogger();
|
|
1197
|
+
const projectRoot = await findProjectRoot();
|
|
1198
|
+
if (!projectRoot) {
|
|
1199
|
+
throw new InvalidProjectError(process.cwd());
|
|
1200
|
+
}
|
|
1201
|
+
const syncHttpUrl = typeof args.url === "string" ? args.url : void 0;
|
|
1202
|
+
const skipNetwork = args["skip-network"] === true;
|
|
1203
|
+
logger.banner();
|
|
1204
|
+
logger.info(`Checking project at ${projectRoot}`);
|
|
1205
|
+
logger.blank();
|
|
1206
|
+
const results = await runDoctorChecks({
|
|
1207
|
+
projectRoot,
|
|
1208
|
+
syncHttpUrl,
|
|
1209
|
+
skipNetwork
|
|
1210
|
+
});
|
|
1211
|
+
for (const check of results) {
|
|
1212
|
+
const prefix = check.status === "pass" ? "success" : check.status === "warn" ? "warn" : check.status === "fail" ? "error" : "step";
|
|
1213
|
+
logger[prefix](`${check.title}: ${check.message}`);
|
|
1214
|
+
if (check.fix && check.status !== "pass" && check.status !== "skip") {
|
|
1215
|
+
logger.step(` Fix: ${check.fix}`);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
logger.blank();
|
|
1219
|
+
if (doctorHasFailures(results)) {
|
|
1220
|
+
logger.error("Doctor found blocking issues.");
|
|
1221
|
+
process.exitCode = 1;
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
const warnings = results.filter((r) => r.status === "warn").length;
|
|
1225
|
+
if (warnings > 0) {
|
|
1226
|
+
logger.warn(`Doctor finished with ${warnings} warning(s).`);
|
|
1227
|
+
} else {
|
|
1228
|
+
logger.success("All checks passed.");
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
// src/commands/generate/generate-command.ts
|
|
1234
|
+
import { mkdir, writeFile as writeFile2 } from "fs/promises";
|
|
1235
|
+
import { dirname, resolve as resolve4 } from "path";
|
|
1236
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
1237
|
+
|
|
1238
|
+
// src/commands/generate/hook-generator.ts
|
|
1239
|
+
function generateCollectionHooks(schema) {
|
|
1240
|
+
const files = /* @__PURE__ */ new Map();
|
|
1241
|
+
for (const collectionName of Object.keys(schema.collections)) {
|
|
1242
|
+
const pascal = toPascalCase(collectionName);
|
|
1243
|
+
const hookName = `use${pascal}`;
|
|
1244
|
+
const content = [
|
|
1245
|
+
"// Auto-generated by `kora generate hooks` \u2014 safe to edit",
|
|
1246
|
+
`// Schema v${String(schema.version)}`,
|
|
1247
|
+
"",
|
|
1248
|
+
"import { useCollection, useMutation, useQuery } from '@korajs/react'",
|
|
1249
|
+
`import type { ${pascal}InsertInput, ${pascal}Record, ${pascal}UpdateInput } from '../types'`,
|
|
1250
|
+
"",
|
|
1251
|
+
"/**",
|
|
1252
|
+
` * Collection hook for \`${collectionName}\`.`,
|
|
1253
|
+
" * Add useQuery(...) calls with your QueryBuilder factories.",
|
|
1254
|
+
" */",
|
|
1255
|
+
`export function ${hookName}() {`,
|
|
1256
|
+
` const ${collectionName} = useCollection('${collectionName}')`,
|
|
1257
|
+
"",
|
|
1258
|
+
` const insert = useMutation((data: ${pascal}InsertInput) => ${collectionName}.insert(data))`,
|
|
1259
|
+
` const update = useMutation((id: string, data: ${pascal}UpdateInput) =>`,
|
|
1260
|
+
` ${collectionName}.update(id, data),`,
|
|
1261
|
+
" )",
|
|
1262
|
+
` const remove = useMutation((id: string) => ${collectionName}.delete(id))`,
|
|
1263
|
+
"",
|
|
1264
|
+
" return {",
|
|
1265
|
+
` ${collectionName},`,
|
|
1266
|
+
" insert,",
|
|
1267
|
+
" update,",
|
|
1268
|
+
" remove,",
|
|
1269
|
+
" }",
|
|
1270
|
+
"}",
|
|
1271
|
+
""
|
|
1272
|
+
].join("\n");
|
|
1273
|
+
files.set(`${collectionName}.ts`, content);
|
|
1274
|
+
}
|
|
1275
|
+
const indexLines = [
|
|
1276
|
+
"// Auto-generated by `kora generate hooks`",
|
|
1277
|
+
"",
|
|
1278
|
+
...Object.keys(schema.collections).map((name) => {
|
|
1279
|
+
const pascal = toPascalCase(name);
|
|
1280
|
+
return `export { use${pascal} } from './${name}'`;
|
|
1281
|
+
}),
|
|
1282
|
+
""
|
|
1283
|
+
];
|
|
1284
|
+
files.set("index.ts", indexLines.join("\n"));
|
|
1285
|
+
return files;
|
|
1286
|
+
}
|
|
1287
|
+
function toPascalCase(value) {
|
|
1288
|
+
return value.split(/[-_]/).filter((part) => part.length > 0).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// src/commands/generate/generate-command.ts
|
|
1292
|
+
var generateCommand = defineCommand5({
|
|
1293
|
+
meta: {
|
|
1294
|
+
name: "generate",
|
|
1295
|
+
description: "Generate code from your Kora schema"
|
|
1296
|
+
},
|
|
1297
|
+
subCommands: {
|
|
1298
|
+
types: defineCommand5({
|
|
1299
|
+
meta: {
|
|
1300
|
+
name: "types",
|
|
1301
|
+
description: "Generate TypeScript types from your schema"
|
|
1302
|
+
},
|
|
1303
|
+
args: {
|
|
1304
|
+
schema: {
|
|
1305
|
+
type: "string",
|
|
1306
|
+
description: "Path to schema file"
|
|
1307
|
+
},
|
|
1308
|
+
output: {
|
|
1309
|
+
type: "string",
|
|
1310
|
+
description: "Output file path",
|
|
1311
|
+
default: "kora/generated/types.ts"
|
|
1312
|
+
}
|
|
1313
|
+
},
|
|
1314
|
+
async run({ args }) {
|
|
1315
|
+
const logger = createLogger();
|
|
1316
|
+
const projectRoot = await findProjectRoot();
|
|
1317
|
+
if (!projectRoot) {
|
|
1318
|
+
throw new InvalidProjectError(process.cwd());
|
|
1319
|
+
}
|
|
1320
|
+
let schemaPath;
|
|
1321
|
+
if (args.schema && typeof args.schema === "string") {
|
|
1322
|
+
schemaPath = resolve4(args.schema);
|
|
1323
|
+
} else {
|
|
1324
|
+
const found = await findSchemaFile(projectRoot);
|
|
1325
|
+
if (!found) {
|
|
1326
|
+
throw new SchemaNotFoundError([
|
|
1327
|
+
"src/schema.ts",
|
|
1328
|
+
"schema.ts",
|
|
1329
|
+
"src/schema.js",
|
|
1330
|
+
"schema.js"
|
|
1331
|
+
]);
|
|
1332
|
+
}
|
|
1333
|
+
schemaPath = found;
|
|
1334
|
+
}
|
|
1335
|
+
logger.step(`Reading schema from ${schemaPath}...`);
|
|
1336
|
+
const schemaModule = await import(schemaPath);
|
|
1337
|
+
const schema = extractSchema(schemaModule);
|
|
1338
|
+
if (!schema) {
|
|
1339
|
+
logger.error("Schema file must export a SchemaDefinition as the default export.");
|
|
1340
|
+
process.exitCode = 1;
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
const output = generateTypes(schema);
|
|
1344
|
+
const outputFile = typeof args.output === "string" ? args.output : "kora/generated/types.ts";
|
|
1345
|
+
const outputPath = resolve4(projectRoot, outputFile);
|
|
1346
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
1347
|
+
await writeFile2(outputPath, output, "utf-8");
|
|
1348
|
+
logger.success(`Generated types at ${outputPath}`);
|
|
1349
|
+
}
|
|
1350
|
+
}),
|
|
1351
|
+
hooks: defineCommand5({
|
|
1352
|
+
meta: {
|
|
1353
|
+
name: "hooks",
|
|
1354
|
+
description: "Generate per-collection React hook stubs"
|
|
1355
|
+
},
|
|
1356
|
+
args: {
|
|
1357
|
+
schema: {
|
|
1358
|
+
type: "string",
|
|
1359
|
+
description: "Path to schema file"
|
|
1360
|
+
},
|
|
1361
|
+
output: {
|
|
1362
|
+
type: "string",
|
|
1363
|
+
description: "Output directory for hooks",
|
|
1364
|
+
default: "kora/generated/hooks"
|
|
1365
|
+
},
|
|
1366
|
+
types: {
|
|
1367
|
+
type: "string",
|
|
1368
|
+
description: "Relative import path to generated types from hook files",
|
|
1369
|
+
default: "../types"
|
|
1370
|
+
}
|
|
1371
|
+
},
|
|
1372
|
+
async run({ args }) {
|
|
1373
|
+
const logger = createLogger();
|
|
1374
|
+
const projectRoot = await findProjectRoot();
|
|
1375
|
+
if (!projectRoot) {
|
|
1376
|
+
throw new InvalidProjectError(process.cwd());
|
|
1377
|
+
}
|
|
1378
|
+
let schemaPath;
|
|
1379
|
+
if (args.schema && typeof args.schema === "string") {
|
|
1380
|
+
schemaPath = resolve4(projectRoot, args.schema);
|
|
1381
|
+
} else {
|
|
1382
|
+
const found = await findSchemaFile(projectRoot);
|
|
1383
|
+
if (!found) {
|
|
1384
|
+
throw new SchemaNotFoundError([
|
|
1385
|
+
"src/schema.ts",
|
|
1386
|
+
"schema.ts",
|
|
1387
|
+
"src/schema.js",
|
|
1388
|
+
"schema.js"
|
|
1389
|
+
]);
|
|
1390
|
+
}
|
|
1391
|
+
schemaPath = found;
|
|
1392
|
+
}
|
|
1393
|
+
const schema = await loadSchemaDefinition(schemaPath, projectRoot);
|
|
1394
|
+
const outputDir = typeof args.output === "string" ? resolve4(projectRoot, args.output) : resolve4(projectRoot, "kora/generated/hooks");
|
|
1395
|
+
const files = generateCollectionHooks(schema);
|
|
1396
|
+
for (const [name, content] of files) {
|
|
1397
|
+
const adjusted = typeof args.types === "string" && args.types !== "../types" ? content.replace("from '../types'", `from '${args.types}'`) : content;
|
|
1398
|
+
const filePath = resolve4(outputDir, name);
|
|
1399
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
1400
|
+
await writeFile2(filePath, adjusted, "utf-8");
|
|
1401
|
+
}
|
|
1402
|
+
logger.success(`Generated ${String(files.size)} hook file(s) at ${outputDir}`);
|
|
1403
|
+
logger.step("Run `kora generate types` first if ../types is missing.");
|
|
1404
|
+
}
|
|
1405
|
+
})
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
1408
|
+
function extractSchema(mod) {
|
|
1409
|
+
if (typeof mod !== "object" || mod === null) return null;
|
|
1410
|
+
const record = mod;
|
|
1411
|
+
const candidate = record.default ?? record;
|
|
1412
|
+
if (isSchemaDefinition(candidate)) return candidate;
|
|
1413
|
+
return null;
|
|
1414
|
+
}
|
|
1415
|
+
function isSchemaDefinition(value) {
|
|
1416
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1417
|
+
const obj = value;
|
|
1418
|
+
return typeof obj.version === "number" && typeof obj.collections === "object" && obj.collections !== null;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// src/commands/logs/logs-command.ts
|
|
1422
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
1423
|
+
var DEFAULT_SYNC_PORT2 = 3001;
|
|
1424
|
+
var EVENTS_ENDPOINT = "/__kora/events";
|
|
1425
|
+
var logsCommand = defineCommand6({
|
|
1426
|
+
meta: {
|
|
1427
|
+
name: "logs",
|
|
1428
|
+
description: "Stream real-time events from a Kora sync server"
|
|
1429
|
+
},
|
|
1430
|
+
args: {
|
|
1431
|
+
url: {
|
|
1432
|
+
type: "string",
|
|
1433
|
+
description: "Sync server URL (default: http://localhost:3001)",
|
|
1434
|
+
default: `http://localhost:${DEFAULT_SYNC_PORT2}`
|
|
1435
|
+
},
|
|
1436
|
+
follow: {
|
|
1437
|
+
type: "boolean",
|
|
1438
|
+
description: "Follow log output (like tail -f)",
|
|
1439
|
+
default: true,
|
|
1440
|
+
alias: "f"
|
|
1441
|
+
},
|
|
1442
|
+
level: {
|
|
1443
|
+
type: "string",
|
|
1444
|
+
description: "Filter by log level (info, warn, error)"
|
|
1445
|
+
},
|
|
1446
|
+
token: {
|
|
1447
|
+
type: "string",
|
|
1448
|
+
description: "Admin token (defaults to KORA_ADMIN_TOKEN)"
|
|
1449
|
+
}
|
|
1450
|
+
},
|
|
1451
|
+
async run({ args }) {
|
|
1452
|
+
const logger = createLogger();
|
|
1453
|
+
const url = typeof args.url === "string" ? args.url : `http://localhost:${DEFAULT_SYNC_PORT2}`;
|
|
1454
|
+
const follow = args.follow !== false;
|
|
1455
|
+
const levelFilter = typeof args.level === "string" ? args.level : null;
|
|
1456
|
+
const token = typeof args.token === "string" ? args.token : process.env.KORA_ADMIN_TOKEN ?? void 0;
|
|
1457
|
+
const eventsUrl = `${url.replace(/\/$/, "")}${EVENTS_ENDPOINT}`;
|
|
1458
|
+
logger.banner();
|
|
1459
|
+
logger.info(`Connecting to ${eventsUrl}...`);
|
|
1460
|
+
logger.blank();
|
|
1461
|
+
try {
|
|
1462
|
+
const response = await fetch(eventsUrl, {
|
|
1463
|
+
headers: token ? { Authorization: `Bearer ${token}` } : void 0
|
|
1464
|
+
});
|
|
1465
|
+
if (!response.ok) {
|
|
1466
|
+
throw new Error(
|
|
1467
|
+
`Failed to connect to ${eventsUrl}: ${response.status} ${response.statusText}`
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
const reader = response.body?.getReader();
|
|
1471
|
+
if (!reader) {
|
|
1472
|
+
throw new Error("Response body is not readable");
|
|
1473
|
+
}
|
|
1474
|
+
const decoder = new TextDecoder();
|
|
1475
|
+
let buffer = "";
|
|
1476
|
+
const processLine = (line) => {
|
|
1477
|
+
if (line.startsWith("event: ")) {
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
if (line.startsWith("data: ")) {
|
|
1481
|
+
const data = line.slice(6);
|
|
1482
|
+
try {
|
|
1483
|
+
const parsed = JSON.parse(data);
|
|
1484
|
+
const level = String(parsed.level ?? "info");
|
|
1485
|
+
if (levelFilter && level !== levelFilter) return;
|
|
1486
|
+
const timestamp = parsed.timestamp ? new Date(parsed.timestamp).toISOString() : "";
|
|
1487
|
+
const event = String(parsed.event ?? "unknown");
|
|
1488
|
+
const nodeId = parsed.nodeId ? ` [${parsed.nodeId}]` : "";
|
|
1489
|
+
const session = parsed.sessionId ? ` <${parsed.sessionId}>` : "";
|
|
1490
|
+
const count = parsed.count ? ` (${parsed.count})` : "";
|
|
1491
|
+
const error = parsed.error ? ` \u2014 ${parsed.error}` : "";
|
|
1492
|
+
const prefix = level === "error" ? "\u2717" : level === "warn" ? "\u26A0" : "\u25CF";
|
|
1493
|
+
const color = level === "error" ? "\x1B[31m" : level === "warn" ? "\x1B[33m" : "\x1B[36m";
|
|
1494
|
+
console.log(
|
|
1495
|
+
`${color}${prefix}${"\x1B[0m"} ${timestamp.slice(11, 23)} ${event}${nodeId}${session}${count}${error}`
|
|
1496
|
+
);
|
|
1497
|
+
} catch {
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
};
|
|
1501
|
+
const read = async () => {
|
|
1502
|
+
while (true) {
|
|
1503
|
+
const { done, value } = await reader.read();
|
|
1504
|
+
if (done) break;
|
|
1505
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1506
|
+
const lines = buffer.split("\n");
|
|
1507
|
+
buffer = lines.pop() ?? "";
|
|
1508
|
+
for (const line of lines) {
|
|
1509
|
+
processLine(line.trim());
|
|
1510
|
+
}
|
|
1511
|
+
if (!follow) break;
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1514
|
+
await read();
|
|
1515
|
+
} catch (error) {
|
|
1516
|
+
logger.error(`Failed to connect to ${eventsUrl}`);
|
|
1517
|
+
if (error instanceof Error) {
|
|
1518
|
+
logger.error(error.message);
|
|
1519
|
+
}
|
|
1520
|
+
logger.blank();
|
|
1521
|
+
logger.step("Make sure the Kora sync server is running.");
|
|
1522
|
+
logger.step("Start it with: kora dev");
|
|
1523
|
+
process.exit(1);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
// src/commands/migrate/migrate-command.ts
|
|
1529
|
+
import { mkdir as mkdir2, readFile as readFile3, readdir, writeFile as writeFile3 } from "fs/promises";
|
|
1530
|
+
import { dirname as dirname2, join as join6, resolve as resolve5 } from "path";
|
|
1531
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
1532
|
+
|
|
1533
|
+
// src/commands/migrate/migration-generator.ts
|
|
1534
|
+
import { generateSQL } from "@korajs/core";
|
|
1535
|
+
|
|
1536
|
+
// src/commands/migrate/schema-differ.ts
|
|
1537
|
+
function diffSchemas(previous, current) {
|
|
1538
|
+
const changes = [];
|
|
1539
|
+
const previousCollections = new Set(Object.keys(previous.collections));
|
|
1540
|
+
const currentCollections = new Set(Object.keys(current.collections));
|
|
1541
|
+
for (const collection of currentCollections) {
|
|
1542
|
+
if (!previousCollections.has(collection)) {
|
|
1543
|
+
changes.push({ type: "collection-added", collection });
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
for (const collection of previousCollections) {
|
|
1547
|
+
if (!currentCollections.has(collection)) {
|
|
1548
|
+
changes.push({ type: "collection-removed", collection });
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
for (const collection of currentCollections) {
|
|
1552
|
+
if (!previousCollections.has(collection)) continue;
|
|
1553
|
+
const previousDef = previous.collections[collection];
|
|
1554
|
+
const currentDef = current.collections[collection];
|
|
1555
|
+
if (!previousDef || !currentDef) continue;
|
|
1556
|
+
const previousFields = previousDef.fields;
|
|
1557
|
+
const currentFields = currentDef.fields;
|
|
1558
|
+
for (const [fieldName, currentField] of Object.entries(currentFields)) {
|
|
1559
|
+
const previousField = previousFields[fieldName];
|
|
1560
|
+
if (!previousField) {
|
|
1561
|
+
changes.push({
|
|
1562
|
+
type: "field-added",
|
|
1563
|
+
collection,
|
|
1564
|
+
field: fieldName,
|
|
1565
|
+
descriptor: currentField
|
|
1566
|
+
});
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
if (!fieldDescriptorsEqual(previousField, currentField)) {
|
|
1570
|
+
changes.push({
|
|
1571
|
+
type: "field-changed",
|
|
1572
|
+
collection,
|
|
1573
|
+
field: fieldName,
|
|
1574
|
+
before: previousField,
|
|
1575
|
+
after: currentField
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
for (const [fieldName, previousField] of Object.entries(previousFields)) {
|
|
1580
|
+
if (!(fieldName in currentFields)) {
|
|
1581
|
+
changes.push({
|
|
1582
|
+
type: "field-removed",
|
|
1583
|
+
collection,
|
|
1584
|
+
field: fieldName,
|
|
1585
|
+
descriptor: previousField
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
const previousIndexes = new Set(previousDef.indexes);
|
|
1590
|
+
const currentIndexes = new Set(currentDef.indexes);
|
|
1591
|
+
for (const index of currentIndexes) {
|
|
1592
|
+
if (!previousIndexes.has(index)) {
|
|
1593
|
+
changes.push({ type: "index-added", collection, index });
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
for (const index of previousIndexes) {
|
|
1597
|
+
if (!currentIndexes.has(index)) {
|
|
1598
|
+
changes.push({ type: "index-removed", collection, index });
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
changes.sort(compareChanges);
|
|
1603
|
+
return {
|
|
1604
|
+
fromVersion: previous.version,
|
|
1605
|
+
toVersion: current.version,
|
|
1606
|
+
changes,
|
|
1607
|
+
hasChanges: changes.length > 0,
|
|
1608
|
+
hasBreakingChanges: changes.some(isBreakingChange)
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
function getChangedCollections(diff) {
|
|
1612
|
+
const collections = /* @__PURE__ */ new Set();
|
|
1613
|
+
for (const change of diff.changes) {
|
|
1614
|
+
collections.add(change.collection);
|
|
1615
|
+
}
|
|
1616
|
+
return [...collections].sort();
|
|
1617
|
+
}
|
|
1618
|
+
function isBreakingChange(change) {
|
|
1619
|
+
if (change.type === "collection-removed" || change.type === "field-removed") return true;
|
|
1620
|
+
if (change.type === "field-changed") {
|
|
1621
|
+
if (change.before.kind !== change.after.kind) return true;
|
|
1622
|
+
if (change.before.itemKind !== change.after.itemKind) return true;
|
|
1623
|
+
const kept = new Set(change.after.enumValues ?? []);
|
|
1624
|
+
if ((change.before.enumValues ?? []).some((value) => !kept.has(value))) return true;
|
|
1625
|
+
if (change.before.required !== change.after.required && change.after.required) return true;
|
|
1626
|
+
return false;
|
|
1627
|
+
}
|
|
1628
|
+
if (change.type === "field-added") {
|
|
1629
|
+
const descriptor = change.descriptor;
|
|
1630
|
+
return descriptor.required && descriptor.defaultValue === void 0 && !descriptor.auto;
|
|
1631
|
+
}
|
|
1632
|
+
return false;
|
|
1633
|
+
}
|
|
1634
|
+
function fieldDescriptorsEqual(left, right) {
|
|
1635
|
+
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);
|
|
1636
|
+
}
|
|
1637
|
+
function serializeEnum(values) {
|
|
1638
|
+
if (!values) return "";
|
|
1639
|
+
return values.join("|");
|
|
1640
|
+
}
|
|
1641
|
+
function compareChanges(left, right) {
|
|
1642
|
+
if (left.collection < right.collection) return -1;
|
|
1643
|
+
if (left.collection > right.collection) return 1;
|
|
1644
|
+
if (left.type < right.type) return -1;
|
|
1645
|
+
if (left.type > right.type) return 1;
|
|
1646
|
+
const leftKey = "field" in left ? left.field : "index" in left ? left.index : "";
|
|
1647
|
+
const rightKey = "field" in right ? right.field : "index" in right ? right.index : "";
|
|
1648
|
+
if (leftKey < rightKey) return -1;
|
|
1649
|
+
if (leftKey > rightKey) return 1;
|
|
1650
|
+
return 0;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// src/commands/migrate/table-evolution-directive.ts
|
|
1654
|
+
import {
|
|
1655
|
+
collectionIndexName,
|
|
1656
|
+
isKoraInternalColumn,
|
|
1657
|
+
parseSqliteCheckConstraints,
|
|
1658
|
+
readSqliteTableCatalog,
|
|
1659
|
+
sqlDefaultLiteral,
|
|
1660
|
+
sqliteCheckConstraintDefinition,
|
|
1661
|
+
sqliteCheckReferencesColumn
|
|
1662
|
+
} from "@korajs/core/internal";
|
|
1663
|
+
var EVOLVE_TABLE_DIRECTIVE = "--kora:evolve-table";
|
|
1664
|
+
function evolveFieldSpec(descriptor) {
|
|
1665
|
+
return {
|
|
1666
|
+
kind: descriptor.kind,
|
|
1667
|
+
...descriptor.itemKind ? { itemKind: descriptor.itemKind } : {},
|
|
1668
|
+
...descriptor.enumValues ? { enumValues: [...descriptor.enumValues] } : {},
|
|
1669
|
+
...descriptor.defaultValue !== void 0 ? { default: descriptor.defaultValue } : {},
|
|
1670
|
+
...descriptor.auto ? { auto: true } : {}
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
function formatEvolveTableDirective(target) {
|
|
1674
|
+
return `${EVOLVE_TABLE_DIRECTIVE} ${JSON.stringify(target)}`;
|
|
1675
|
+
}
|
|
1676
|
+
function parseEvolveTableDirective(statement) {
|
|
1677
|
+
const trimmed = statement.trimStart();
|
|
1678
|
+
if (!trimmed.startsWith(EVOLVE_TABLE_DIRECTIVE)) return null;
|
|
1679
|
+
const payload = trimmed.slice(EVOLVE_TABLE_DIRECTIVE.length).trim();
|
|
1680
|
+
let parsed;
|
|
1681
|
+
try {
|
|
1682
|
+
parsed = JSON.parse(payload);
|
|
1683
|
+
} catch {
|
|
1684
|
+
parsed = null;
|
|
1685
|
+
}
|
|
1686
|
+
const target = parsed;
|
|
1687
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1688
|
+
const isStrings = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
1689
|
+
const isSpec = (value) => isRecord(value) && typeof value.kind === "string";
|
|
1690
|
+
if (!isRecord(target) || typeof target.table !== "string" || !isRecord(target.add) || !Object.values(target.add).every(isSpec) || !isStrings(target.drop) || !isRecord(target.change) || !Object.values(target.change).every(
|
|
1691
|
+
(change) => isRecord(change) && isSpec(change.from) && isSpec(change.to)
|
|
1692
|
+
) || !isStrings(target.addIndexes) || !isStrings(target.removeIndexes)) {
|
|
1693
|
+
throw new Error(
|
|
1694
|
+
`Malformed migration directive "${statement}": expected ${EVOLVE_TABLE_DIRECTIVE} {"table", "add", "drop", "change", "addIndexes", "removeIndexes"}. Regenerate the migration with \`kora migrate\`.`
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1697
|
+
return {
|
|
1698
|
+
table: target.table,
|
|
1699
|
+
add: target.add,
|
|
1700
|
+
drop: [...target.drop],
|
|
1701
|
+
change: target.change,
|
|
1702
|
+
addIndexes: [...target.addIndexes],
|
|
1703
|
+
removeIndexes: [...target.removeIndexes]
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
async function expandEvolveTableForSqlite(target, query, now) {
|
|
1707
|
+
const catalog = await readSqliteTableCatalog(query, target.table);
|
|
1708
|
+
if (!catalog) return [];
|
|
1709
|
+
const table = quoteIdentifier(target.table);
|
|
1710
|
+
const existing = new Map(
|
|
1711
|
+
catalog.columns.map((column) => [column.name, column.type.toUpperCase()])
|
|
1712
|
+
);
|
|
1713
|
+
const plan = planColumns(target, existing, sqliteType);
|
|
1714
|
+
const statements = [];
|
|
1715
|
+
const presentIndexes = await indexColumnsByName(query, target.table);
|
|
1716
|
+
const removedIndexNames = new Set(
|
|
1717
|
+
target.removeIndexes.flatMap((field) => koraIndexNames(target.table, field)).filter((name) => presentIndexes.has(name))
|
|
1718
|
+
);
|
|
1719
|
+
for (const name of removedIndexNames) {
|
|
1720
|
+
statements.push(`DROP INDEX IF EXISTS ${quoteIdentifier(name)}`);
|
|
1721
|
+
}
|
|
1722
|
+
if (plan.drop.length > 0 || plan.change.length > 0) {
|
|
1723
|
+
statements.push(
|
|
1724
|
+
...await sqliteRebuildStatements(catalog, plan, query, removedIndexNames, now)
|
|
1725
|
+
);
|
|
1726
|
+
} else {
|
|
1727
|
+
for (const [field, spec] of plan.add) {
|
|
1728
|
+
statements.push(
|
|
1729
|
+
`ALTER TABLE ${table} ADD COLUMN ${quoteIdentifier(field)} ${sqliteType(spec)}${defaultClause(spec)}`
|
|
1730
|
+
);
|
|
1731
|
+
if (spec.auto && spec.kind === "timestamp" && spec.default === void 0) {
|
|
1732
|
+
statements.push(`UPDATE ${table} SET ${quoteIdentifier(field)} = ${now}`);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
const indexed = await singleColumnIndexes(query, target.table, removedIndexNames);
|
|
1737
|
+
for (const field of target.addIndexes) {
|
|
1738
|
+
if (indexed.has(field)) continue;
|
|
1739
|
+
statements.push(
|
|
1740
|
+
`CREATE INDEX IF NOT EXISTS ${koraIndexNames(target.table, field)[0]} ON ${table} (${quoteIdentifier(field)})`
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
return statements;
|
|
1744
|
+
}
|
|
1745
|
+
async function expandEvolveTableForPostgres(target, query, now) {
|
|
1746
|
+
const columns = await query(
|
|
1747
|
+
`SELECT column_name::text AS name, upper(data_type::text) AS type FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = ${sqlText(target.table)}`
|
|
1748
|
+
);
|
|
1749
|
+
if (columns.length === 0) return [];
|
|
1750
|
+
const table = quoteIdentifier(target.table);
|
|
1751
|
+
const existing = new Map(columns.map((row) => [String(row.name), String(row.type)]));
|
|
1752
|
+
const plan = planColumns(target, existing, postgresType);
|
|
1753
|
+
const statements = [];
|
|
1754
|
+
const indexNames = new Set(
|
|
1755
|
+
(await query(
|
|
1756
|
+
`SELECT indexname::text AS name FROM pg_indexes WHERE schemaname = current_schema() AND tablename = ${sqlText(target.table)}`
|
|
1757
|
+
)).map((row) => String(row.name))
|
|
1758
|
+
);
|
|
1759
|
+
for (const field of target.removeIndexes) {
|
|
1760
|
+
const [serverName, clientName] = koraIndexNames(target.table, field);
|
|
1761
|
+
if (indexNames.has(serverName.toLowerCase())) {
|
|
1762
|
+
statements.push(`DROP INDEX IF EXISTS ${serverName}`);
|
|
1763
|
+
}
|
|
1764
|
+
if (indexNames.has(clientName)) {
|
|
1765
|
+
statements.push(`DROP INDEX IF EXISTS ${quoteIdentifier(clientName)}`);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
for (const field of plan.drop) {
|
|
1769
|
+
statements.push(`ALTER TABLE ${table} DROP COLUMN IF EXISTS ${quoteIdentifier(field)}`);
|
|
1770
|
+
}
|
|
1771
|
+
for (const [field, change] of plan.change) {
|
|
1772
|
+
const column = quoteIdentifier(field);
|
|
1773
|
+
const type = postgresType(change.to);
|
|
1774
|
+
statements.push(`ALTER TABLE ${table} ALTER COLUMN ${column} DROP DEFAULT`);
|
|
1775
|
+
statements.push(
|
|
1776
|
+
`ALTER TABLE ${table} ALTER COLUMN ${column} TYPE ${type} USING CAST((${postgresProjection(column, change.from, change.to, now)}) AS ${type})`
|
|
1777
|
+
);
|
|
1778
|
+
if (change.to.default !== void 0) {
|
|
1779
|
+
statements.push(
|
|
1780
|
+
`ALTER TABLE ${table} ALTER COLUMN ${column} SET DEFAULT ${sqlDefaultLiteral(change.to.default)}`
|
|
1781
|
+
);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
for (const [field, spec] of plan.add) {
|
|
1785
|
+
statements.push(
|
|
1786
|
+
`ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${quoteIdentifier(field)} ${postgresType(spec)}${defaultClause(spec)}`
|
|
1787
|
+
);
|
|
1788
|
+
if (spec.auto && spec.kind === "timestamp" && spec.default === void 0) {
|
|
1789
|
+
statements.push(`UPDATE ${table} SET ${quoteIdentifier(field)} = ${now}`);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
for (const field of target.addIndexes) {
|
|
1793
|
+
const name = koraIndexNames(target.table, field)[0];
|
|
1794
|
+
if (indexNames.has(name.toLowerCase())) continue;
|
|
1795
|
+
statements.push(`CREATE INDEX IF NOT EXISTS ${name} ON ${table} (${quoteIdentifier(field)})`);
|
|
1796
|
+
}
|
|
1797
|
+
return statements;
|
|
1798
|
+
}
|
|
1799
|
+
function planColumns(target, existing, typeOf) {
|
|
1800
|
+
const plan = { add: [], drop: [], change: [] };
|
|
1801
|
+
for (const [field, spec] of Object.entries(target.add)) {
|
|
1802
|
+
if (!existing.has(field)) plan.add.push([field, spec]);
|
|
1803
|
+
}
|
|
1804
|
+
for (const [field, change] of Object.entries(target.change)) {
|
|
1805
|
+
const current = existing.get(field);
|
|
1806
|
+
if (current === void 0) {
|
|
1807
|
+
plan.add.push([field, change.to]);
|
|
1808
|
+
continue;
|
|
1809
|
+
}
|
|
1810
|
+
const from = typeOf(change.from);
|
|
1811
|
+
const to = typeOf(change.to);
|
|
1812
|
+
if (from !== to && current === to) continue;
|
|
1813
|
+
plan.change.push([field, change]);
|
|
1814
|
+
}
|
|
1815
|
+
for (const field of target.drop) {
|
|
1816
|
+
if (existing.has(field) && !(field in target.add) && !(field in target.change)) {
|
|
1817
|
+
if (isKoraInternalColumn(field)) {
|
|
1818
|
+
throw new Error(
|
|
1819
|
+
`Migration directive refuses to drop Kora's own column "${field}" of "${target.table}".`
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1822
|
+
plan.drop.push(field);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
return plan;
|
|
1826
|
+
}
|
|
1827
|
+
async function sqliteRebuildStatements(catalog, plan, query, removedIndexNames, now) {
|
|
1828
|
+
const table = catalog.table;
|
|
1829
|
+
const temp = `_kora_evolve_${table}`;
|
|
1830
|
+
const dropped = new Set(plan.drop);
|
|
1831
|
+
const changed = new Map(plan.change);
|
|
1832
|
+
const ordered = [...catalog.columns].sort((a, b) => a.cid - b.cid).filter((column) => !dropped.has(column.name));
|
|
1833
|
+
const pkColumns = ordered.filter((c) => c.pk > 0).sort((a, b) => a.pk - b.pk);
|
|
1834
|
+
const fkGroups = /* @__PURE__ */ new Map();
|
|
1835
|
+
for (const fk of catalog.foreignKeys) {
|
|
1836
|
+
const group = fkGroups.get(fk.id) ?? [];
|
|
1837
|
+
group.push(fk);
|
|
1838
|
+
fkGroups.set(fk.id, group);
|
|
1839
|
+
}
|
|
1840
|
+
const columnReferences = /* @__PURE__ */ new Map();
|
|
1841
|
+
const tableConstraints = [];
|
|
1842
|
+
for (const group of fkGroups.values()) {
|
|
1843
|
+
group.sort((a, b) => a.seq - b.seq);
|
|
1844
|
+
const first = group[0];
|
|
1845
|
+
if (!first || group.some((fk) => dropped.has(fk.from))) continue;
|
|
1846
|
+
const actions = foreignKeyActions(first);
|
|
1847
|
+
const targetRef = group.every((fk) => fk.to !== null && fk.to !== "") ? `${quoteIdentifier(first.table)}(${group.map((fk) => quoteIdentifier(fk.to ?? "")).join(", ")})` : quoteIdentifier(first.table);
|
|
1848
|
+
if (group.length === 1) {
|
|
1849
|
+
columnReferences.set(first.from, `REFERENCES ${targetRef}${actions}`);
|
|
1850
|
+
} else {
|
|
1851
|
+
tableConstraints.push(
|
|
1852
|
+
`FOREIGN KEY (${group.map((fk) => quoteIdentifier(fk.from)).join(", ")}) REFERENCES ${targetRef}${actions}`
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
if (pkColumns.length > 1) {
|
|
1857
|
+
tableConstraints.unshift(
|
|
1858
|
+
`PRIMARY KEY (${pkColumns.map((c) => quoteIdentifier(c.name)).join(", ")})`
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
for (const unique of catalog.uniqueConstraints) {
|
|
1862
|
+
if (unique.some((column) => dropped.has(column))) continue;
|
|
1863
|
+
tableConstraints.push(`UNIQUE (${unique.map(quoteIdentifier).join(", ")})`);
|
|
1864
|
+
}
|
|
1865
|
+
for (const check of parseSqliteCheckConstraints(catalog.sql)) {
|
|
1866
|
+
if (sqliteCheckReferencesColumn(check, dropped)) continue;
|
|
1867
|
+
tableConstraints.push(sqliteCheckConstraintDefinition(check));
|
|
1868
|
+
}
|
|
1869
|
+
const definitions = [];
|
|
1870
|
+
const targetColumns = [];
|
|
1871
|
+
const selections = [];
|
|
1872
|
+
for (const column of ordered) {
|
|
1873
|
+
const change = changed.get(column.name);
|
|
1874
|
+
const parts = [quoteIdentifier(column.name)];
|
|
1875
|
+
if (change) {
|
|
1876
|
+
parts.push(sqliteType(change.to));
|
|
1877
|
+
} else if (column.type) {
|
|
1878
|
+
parts.push(column.type);
|
|
1879
|
+
}
|
|
1880
|
+
if (pkColumns.length === 1 && column.pk === 1) parts.push("PRIMARY KEY");
|
|
1881
|
+
if (column.notnull === 1) parts.push("NOT NULL");
|
|
1882
|
+
if (change) {
|
|
1883
|
+
if (change.to.default !== void 0) {
|
|
1884
|
+
parts.push(`DEFAULT ${sqlDefaultLiteral(change.to.default)}`);
|
|
1885
|
+
}
|
|
1886
|
+
} else if (column.dflt_value !== null) {
|
|
1887
|
+
parts.push(`DEFAULT (${column.dflt_value})`);
|
|
1888
|
+
}
|
|
1889
|
+
const reference = columnReferences.get(column.name);
|
|
1890
|
+
if (reference) parts.push(reference);
|
|
1891
|
+
definitions.push(parts.join(" "));
|
|
1892
|
+
targetColumns.push(quoteIdentifier(column.name));
|
|
1893
|
+
selections.push(
|
|
1894
|
+
change ? sqliteProjection(quoteIdentifier(column.name), change.from, change.to, now) : quoteIdentifier(column.name)
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1897
|
+
for (const [field, spec] of plan.add) {
|
|
1898
|
+
definitions.push(`${quoteIdentifier(field)} ${sqliteType(spec)}${defaultClause(spec)}`);
|
|
1899
|
+
targetColumns.push(quoteIdentifier(field));
|
|
1900
|
+
selections.push(fallbackLiteral(spec, now));
|
|
1901
|
+
}
|
|
1902
|
+
const indexColumns = await indexColumnsByName(query, table);
|
|
1903
|
+
const dependents = [];
|
|
1904
|
+
for (const row of await query(
|
|
1905
|
+
`SELECT type, name, sql FROM sqlite_master WHERE type IN ('index', 'trigger') AND tbl_name = ${sqlText(table)} AND sql IS NOT NULL ORDER BY type, name`
|
|
1906
|
+
)) {
|
|
1907
|
+
const name = String(row.name);
|
|
1908
|
+
if (row.type === "index") {
|
|
1909
|
+
if (removedIndexNames.has(name)) continue;
|
|
1910
|
+
if ((indexColumns.get(name) ?? []).some((column) => dropped.has(column))) continue;
|
|
1911
|
+
}
|
|
1912
|
+
dependents.push(String(row.sql));
|
|
1913
|
+
}
|
|
1914
|
+
return [
|
|
1915
|
+
`DROP TABLE IF EXISTS ${quoteIdentifier(temp)}`,
|
|
1916
|
+
`CREATE TABLE ${quoteIdentifier(temp)} (
|
|
1917
|
+
${[...definitions, ...tableConstraints].join(",\n ")}
|
|
1918
|
+
)`,
|
|
1919
|
+
`INSERT INTO ${quoteIdentifier(temp)} (${targetColumns.join(", ")}) SELECT ${selections.join(", ")} FROM ${quoteIdentifier(table)}`,
|
|
1920
|
+
`DROP TABLE ${quoteIdentifier(table)}`,
|
|
1921
|
+
`ALTER TABLE ${quoteIdentifier(temp)} RENAME TO ${quoteIdentifier(table)}`,
|
|
1922
|
+
...dependents
|
|
1923
|
+
];
|
|
1924
|
+
}
|
|
1925
|
+
async function indexColumnsByName(query, table) {
|
|
1926
|
+
const out = /* @__PURE__ */ new Map();
|
|
1927
|
+
for (const index of await query(`SELECT name FROM pragma_index_list(${sqlText(table)})`)) {
|
|
1928
|
+
const name = String(index.name);
|
|
1929
|
+
const info = await query(`SELECT name FROM pragma_index_info(${sqlText(name)}) ORDER BY seqno`);
|
|
1930
|
+
out.set(
|
|
1931
|
+
name,
|
|
1932
|
+
info.filter((row) => typeof row.name === "string").map((row) => String(row.name))
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
return out;
|
|
1936
|
+
}
|
|
1937
|
+
async function singleColumnIndexes(query, table, ignore) {
|
|
1938
|
+
const out = /* @__PURE__ */ new Set();
|
|
1939
|
+
for (const [name, columns] of await indexColumnsByName(query, table)) {
|
|
1940
|
+
if (ignore.has(name)) continue;
|
|
1941
|
+
const only = columns[0];
|
|
1942
|
+
if (columns.length === 1 && only !== void 0) out.add(only);
|
|
1943
|
+
}
|
|
1944
|
+
return out;
|
|
1945
|
+
}
|
|
1946
|
+
function koraIndexNames(table, field) {
|
|
1947
|
+
quoteIdentifier(table);
|
|
1948
|
+
quoteIdentifier(field);
|
|
1949
|
+
return [`idx_${table}_${field}`, collectionIndexName(table, field)];
|
|
1950
|
+
}
|
|
1951
|
+
function defaultClause(spec) {
|
|
1952
|
+
return spec.default !== void 0 ? ` DEFAULT ${sqlDefaultLiteral(spec.default)}` : "";
|
|
1953
|
+
}
|
|
1954
|
+
function sqliteType(spec) {
|
|
1955
|
+
switch (spec.kind) {
|
|
1956
|
+
case "number":
|
|
1957
|
+
return "REAL";
|
|
1958
|
+
case "boolean":
|
|
1959
|
+
case "timestamp":
|
|
1960
|
+
return "INTEGER";
|
|
1961
|
+
case "richtext":
|
|
1962
|
+
return "BLOB";
|
|
1963
|
+
default:
|
|
1964
|
+
return "TEXT";
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
function postgresType(spec) {
|
|
1968
|
+
switch (spec.kind) {
|
|
1969
|
+
case "number":
|
|
1970
|
+
return "DOUBLE PRECISION";
|
|
1971
|
+
case "boolean":
|
|
1972
|
+
return "INTEGER";
|
|
1973
|
+
case "timestamp":
|
|
1974
|
+
return "BIGINT";
|
|
1975
|
+
case "array":
|
|
1976
|
+
case "object":
|
|
1977
|
+
case "json":
|
|
1978
|
+
return "JSONB";
|
|
1979
|
+
case "richtext":
|
|
1980
|
+
return "BYTEA";
|
|
1981
|
+
default:
|
|
1982
|
+
return "TEXT";
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
function fallbackLiteral(target, now) {
|
|
1986
|
+
if (target.auto && target.kind === "timestamp") return String(now);
|
|
1987
|
+
if (target.default !== void 0) return sqlDefaultLiteral(target.default);
|
|
1988
|
+
return "NULL";
|
|
1989
|
+
}
|
|
1990
|
+
var NUMERIC_SOURCES = /* @__PURE__ */ new Set(["number", "timestamp", "boolean"]);
|
|
1991
|
+
var TEXT_SOURCES = /* @__PURE__ */ new Set(["string", "enum"]);
|
|
1992
|
+
var TRUE_WORDS = "('1','true','t','yes','y','on')";
|
|
1993
|
+
var FALSE_WORDS = "('0','false','f','no','n','off')";
|
|
1994
|
+
function sqliteProjection(column, source, target, now) {
|
|
1995
|
+
if (source.kind === target.kind && source.itemKind === target.itemKind) return column;
|
|
1996
|
+
const fallback = fallbackLiteral(target, now);
|
|
1997
|
+
if (target.kind === "string") return `CAST(${column} AS TEXT)`;
|
|
1998
|
+
if (target.kind === "number" || target.kind === "timestamp") {
|
|
1999
|
+
if (NUMERIC_SOURCES.has(source.kind) || TEXT_SOURCES.has(source.kind)) {
|
|
2000
|
+
const castType = target.kind === "number" ? "REAL" : "INTEGER";
|
|
2001
|
+
return `CASE WHEN ${column} IS NULL THEN NULL ELSE CAST(${column} AS ${castType}) END`;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
if (target.kind === "boolean") {
|
|
2005
|
+
if (NUMERIC_SOURCES.has(source.kind)) {
|
|
2006
|
+
return `CASE WHEN ${column} IS NULL THEN NULL WHEN CAST(${column} AS REAL) = 0 THEN 0 ELSE 1 END`;
|
|
2007
|
+
}
|
|
2008
|
+
if (TEXT_SOURCES.has(source.kind)) {
|
|
2009
|
+
const word = `LOWER(TRIM(CAST(${column} AS TEXT)))`;
|
|
2010
|
+
return `CASE WHEN ${column} IS NULL THEN NULL WHEN ${word} IN ${TRUE_WORDS} THEN 1 WHEN ${word} IN ${FALSE_WORDS} THEN 0 ELSE ${fallback} END`;
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
if (target.kind === "enum" && target.enumValues && target.enumValues.length > 0) {
|
|
2014
|
+
if (TEXT_SOURCES.has(source.kind)) {
|
|
2015
|
+
const allowed = target.enumValues.map((value) => sqlDefaultLiteral(value)).join(", ");
|
|
2016
|
+
return `CASE WHEN ${column} IN (${allowed}) THEN ${column} ELSE ${fallback} END`;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
return fallback;
|
|
2020
|
+
}
|
|
2021
|
+
function postgresProjection(column, source, target, now) {
|
|
2022
|
+
if (source.kind === target.kind && source.itemKind === target.itemKind) return column;
|
|
2023
|
+
const fallback = fallbackLiteral(target, now);
|
|
2024
|
+
const text = `(${column})::text`;
|
|
2025
|
+
if (target.kind === "string") return text;
|
|
2026
|
+
if (target.kind === "number" || target.kind === "timestamp") {
|
|
2027
|
+
const toTarget = (value) => target.kind === "number" ? value : `round(${value})::bigint`;
|
|
2028
|
+
if (NUMERIC_SOURCES.has(source.kind)) {
|
|
2029
|
+
return `CASE WHEN ${column} IS NULL THEN NULL ELSE ${toTarget(`(${column})::double precision`)} END`;
|
|
2030
|
+
}
|
|
2031
|
+
if (TEXT_SOURCES.has(source.kind)) {
|
|
2032
|
+
const trimmed = `btrim(${text})`;
|
|
2033
|
+
return `CASE WHEN ${column} IS NULL THEN NULL WHEN ${trimmed} ~ '^[-+]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][-+]?[0-9]+)?$' THEN ${toTarget(`(${trimmed})::double precision`)} ELSE ${fallback} END`;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
if (target.kind === "boolean") {
|
|
2037
|
+
if (NUMERIC_SOURCES.has(source.kind)) {
|
|
2038
|
+
return `CASE WHEN ${column} IS NULL THEN NULL WHEN (${column})::double precision = 0 THEN 0 ELSE 1 END`;
|
|
2039
|
+
}
|
|
2040
|
+
if (TEXT_SOURCES.has(source.kind)) {
|
|
2041
|
+
const word = `lower(btrim(${text}))`;
|
|
2042
|
+
return `CASE WHEN ${column} IS NULL THEN NULL WHEN ${word} IN ${TRUE_WORDS} THEN 1 WHEN ${word} IN ${FALSE_WORDS} THEN 0 ELSE ${fallback} END`;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
if (target.kind === "enum" && target.enumValues && target.enumValues.length > 0) {
|
|
2046
|
+
if (TEXT_SOURCES.has(source.kind)) {
|
|
2047
|
+
const allowed = target.enumValues.map((value) => sqlDefaultLiteral(value)).join(", ");
|
|
2048
|
+
return `CASE WHEN ${text} IN (${allowed}) THEN ${text} ELSE ${fallback} END`;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
return fallback;
|
|
2052
|
+
}
|
|
2053
|
+
function foreignKeyActions(fk) {
|
|
2054
|
+
let out = "";
|
|
2055
|
+
const onDelete = fk.on_delete?.toUpperCase();
|
|
2056
|
+
const onUpdate = fk.on_update?.toUpperCase();
|
|
2057
|
+
if (onDelete && onDelete !== "NO ACTION") out += ` ON DELETE ${onDelete}`;
|
|
2058
|
+
if (onUpdate && onUpdate !== "NO ACTION") out += ` ON UPDATE ${onUpdate}`;
|
|
2059
|
+
return out;
|
|
2060
|
+
}
|
|
2061
|
+
function sqlText(value) {
|
|
2062
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
2063
|
+
}
|
|
2064
|
+
function quoteIdentifier(identifier) {
|
|
2065
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(identifier)) {
|
|
2066
|
+
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
2067
|
+
}
|
|
2068
|
+
return `"${identifier}"`;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
// src/commands/migrate/value-domain-directive.ts
|
|
2072
|
+
import {
|
|
2073
|
+
planPostgresConstraintRelaxation,
|
|
2074
|
+
planSqliteConstraintRelaxation
|
|
2075
|
+
} from "@korajs/core/internal";
|
|
2076
|
+
var RELAX_VALUE_DOMAIN_DIRECTIVE = "--kora:relax-value-domain";
|
|
2077
|
+
function formatRelaxValueDomainDirective(target) {
|
|
2078
|
+
return `${RELAX_VALUE_DOMAIN_DIRECTIVE} ${JSON.stringify(target)}`;
|
|
2079
|
+
}
|
|
2080
|
+
function parseRelaxValueDomainDirective(statement) {
|
|
2081
|
+
const trimmed = statement.trimStart();
|
|
2082
|
+
if (!trimmed.startsWith(RELAX_VALUE_DOMAIN_DIRECTIVE)) return null;
|
|
2083
|
+
const payload = trimmed.slice(RELAX_VALUE_DOMAIN_DIRECTIVE.length).trim();
|
|
2084
|
+
let parsed;
|
|
2085
|
+
try {
|
|
2086
|
+
parsed = JSON.parse(payload);
|
|
2087
|
+
} catch {
|
|
2088
|
+
parsed = null;
|
|
2089
|
+
}
|
|
2090
|
+
if (typeof parsed !== "object" || parsed === null || typeof parsed.table !== "string" || !Array.isArray(parsed.fields) || !parsed.fields.every((field) => typeof field === "string") || parsed.enums !== void 0 && (!Array.isArray(parsed.enums) || !parsed.enums.every((field) => typeof field === "string"))) {
|
|
2091
|
+
throw new Error(
|
|
2092
|
+
`Malformed migration directive "${statement}": expected ${RELAX_VALUE_DOMAIN_DIRECTIVE} {"table": "...", "fields": ["..."]}. Regenerate the migration with \`kora migrate\`.`
|
|
2093
|
+
);
|
|
2094
|
+
}
|
|
2095
|
+
const target = parsed;
|
|
2096
|
+
return {
|
|
2097
|
+
table: target.table,
|
|
2098
|
+
fields: [...target.fields],
|
|
2099
|
+
...target.enums !== void 0 ? { enums: [...target.enums] } : {}
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
async function expandRelaxValueDomainForSqlite(target, query) {
|
|
2103
|
+
return planSqliteConstraintRelaxation(query, [target.table], {
|
|
2104
|
+
[target.table]: target.enums ?? target.fields
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
async function expandRelaxValueDomainForPostgres(target, query) {
|
|
2108
|
+
return planPostgresConstraintRelaxation(
|
|
2109
|
+
query,
|
|
2110
|
+
{ [target.table]: target.fields },
|
|
2111
|
+
target.enums !== void 0 ? { [target.table]: target.enums } : {}
|
|
2112
|
+
);
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
// src/commands/migrate/migration-generator.ts
|
|
2116
|
+
function generateMigration(previous, current, diff) {
|
|
2117
|
+
const up = [];
|
|
2118
|
+
const downGroups = [];
|
|
2119
|
+
for (const change of diff.changes) {
|
|
2120
|
+
if (change.type === "collection-added") {
|
|
2121
|
+
const collectionDef = current.collections[change.collection];
|
|
2122
|
+
if (!collectionDef) continue;
|
|
2123
|
+
up.push(...generateSQL(change.collection, collectionDef));
|
|
2124
|
+
downGroups.push(dropCollectionStatements(change.collection));
|
|
2125
|
+
}
|
|
2126
|
+
if (change.type === "collection-removed") {
|
|
2127
|
+
const collectionDef = previous.collections[change.collection];
|
|
2128
|
+
up.push(...dropCollectionStatements(change.collection));
|
|
2129
|
+
if (collectionDef) {
|
|
2130
|
+
downGroups.push(generateSQL(change.collection, collectionDef));
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
const changedCollections = getChangedCollections(diff).filter(
|
|
2135
|
+
(collection) => collection in previous.collections && collection in current.collections && diff.changes.some(
|
|
2136
|
+
(change) => change.collection === collection && (change.type === "field-added" || change.type === "field-removed" || change.type === "field-changed" || change.type === "index-added" || change.type === "index-removed")
|
|
2137
|
+
)
|
|
2138
|
+
);
|
|
2139
|
+
for (const collection of changedCollections) {
|
|
2140
|
+
const previousDef = previous.collections[collection];
|
|
2141
|
+
const currentDef = current.collections[collection];
|
|
2142
|
+
if (!previousDef || !currentDef) continue;
|
|
2143
|
+
const valueDomainOnly = diff.changes.every(
|
|
2144
|
+
(change) => change.collection !== collection || change.type === "field-changed" && isValueDomainChange(change.before, change.after)
|
|
2145
|
+
);
|
|
2146
|
+
if (valueDomainOnly) {
|
|
2147
|
+
const directive = relaxValueDomainDirective(collection, previousDef, currentDef);
|
|
2148
|
+
up.push(directive);
|
|
2149
|
+
downGroups.push([directive]);
|
|
2150
|
+
continue;
|
|
2151
|
+
}
|
|
2152
|
+
validateRebuildSafety(collection, previousDef, currentDef);
|
|
2153
|
+
const relax = relaxValueDomainDirective(collection, previousDef, currentDef);
|
|
2154
|
+
up.push(relax, formatEvolveTableDirective(evolveTarget(collection, previousDef, currentDef)));
|
|
2155
|
+
downGroups.push([
|
|
2156
|
+
relax,
|
|
2157
|
+
formatEvolveTableDirective(evolveTarget(collection, currentDef, previousDef))
|
|
2158
|
+
]);
|
|
2159
|
+
}
|
|
2160
|
+
return {
|
|
2161
|
+
up,
|
|
2162
|
+
down: downGroups.reverse().flat(),
|
|
2163
|
+
summary: diff.changes.map(formatChange),
|
|
2164
|
+
containsBreakingChanges: diff.hasBreakingChanges
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
function isValueDomainChange(before, after) {
|
|
2168
|
+
return before.kind === after.kind && before.itemKind === after.itemKind && before.auto === after.auto;
|
|
2169
|
+
}
|
|
2170
|
+
function evolveTarget(collection, from, to) {
|
|
2171
|
+
const target = {
|
|
2172
|
+
table: collection,
|
|
2173
|
+
add: {},
|
|
2174
|
+
drop: [],
|
|
2175
|
+
change: {},
|
|
2176
|
+
addIndexes: to.indexes.filter((field) => !from.indexes.includes(field)),
|
|
2177
|
+
removeIndexes: from.indexes.filter((field) => !to.indexes.includes(field))
|
|
2178
|
+
};
|
|
2179
|
+
for (const [field, descriptor] of Object.entries(to.fields)) {
|
|
2180
|
+
const before = from.fields[field];
|
|
2181
|
+
if (!before) {
|
|
2182
|
+
target.add[field] = evolveFieldSpec(descriptor);
|
|
2183
|
+
} else if (before.kind !== descriptor.kind || before.itemKind !== descriptor.itemKind) {
|
|
2184
|
+
target.change[field] = { from: evolveFieldSpec(before), to: evolveFieldSpec(descriptor) };
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
target.drop = Object.keys(from.fields).filter((field) => !(field in to.fields));
|
|
2188
|
+
return target;
|
|
2189
|
+
}
|
|
2190
|
+
function relaxValueDomainDirective(collection, from, to) {
|
|
2191
|
+
const fields = [.../* @__PURE__ */ new Set([...Object.keys(from.fields), ...Object.keys(to.fields)])].sort();
|
|
2192
|
+
const enums = fields.filter(
|
|
2193
|
+
(field) => from.fields[field]?.kind === "enum" || to.fields[field]?.kind === "enum"
|
|
2194
|
+
);
|
|
2195
|
+
return formatRelaxValueDomainDirective({ table: collection, fields, enums });
|
|
2196
|
+
}
|
|
2197
|
+
function validateRebuildSafety(collection, from, to) {
|
|
2198
|
+
for (const [fieldName, descriptor] of Object.entries(to.fields)) {
|
|
2199
|
+
if (fieldName in from.fields) continue;
|
|
2200
|
+
if (descriptor.required && descriptor.defaultValue === void 0 && !descriptor.auto) {
|
|
2201
|
+
throw new Error(
|
|
2202
|
+
`Cannot auto-migrate collection "${collection}": added required field "${fieldName}" has no default value.`
|
|
2203
|
+
);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
for (const [fieldName, targetDescriptor] of Object.entries(to.fields)) {
|
|
2207
|
+
const sourceDescriptor = from.fields[fieldName];
|
|
2208
|
+
if (!sourceDescriptor) continue;
|
|
2209
|
+
if (canTransformField(sourceDescriptor, targetDescriptor)) continue;
|
|
2210
|
+
if (targetDescriptor.required && targetDescriptor.defaultValue === void 0 && !targetDescriptor.auto) {
|
|
2211
|
+
throw new Error(
|
|
2212
|
+
`Cannot auto-migrate collection "${collection}": changed required field "${fieldName}" from ${sourceDescriptor.kind} to ${targetDescriptor.kind} without a safe transform/default.`
|
|
2213
|
+
);
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
function canTransformField(source, target) {
|
|
2218
|
+
if (source.kind === target.kind && source.itemKind === target.itemKind) {
|
|
2219
|
+
return true;
|
|
2220
|
+
}
|
|
2221
|
+
if (target.kind === "string") {
|
|
2222
|
+
return true;
|
|
2223
|
+
}
|
|
2224
|
+
if (target.kind === "number" || target.kind === "timestamp") {
|
|
2225
|
+
return source.kind === "string" || source.kind === "enum" || source.kind === "number" || source.kind === "timestamp" || source.kind === "boolean";
|
|
2226
|
+
}
|
|
2227
|
+
if (target.kind === "boolean") {
|
|
2228
|
+
return source.kind === "number" || source.kind === "timestamp" || source.kind === "boolean" || source.kind === "string" || source.kind === "enum";
|
|
2229
|
+
}
|
|
2230
|
+
if (target.kind === "enum") {
|
|
2231
|
+
return source.kind === "string" || source.kind === "enum";
|
|
2232
|
+
}
|
|
2233
|
+
if (target.kind === "array") {
|
|
2234
|
+
return source.kind === "array" && source.itemKind === target.itemKind;
|
|
2235
|
+
}
|
|
2236
|
+
if (target.kind === "richtext") {
|
|
2237
|
+
return source.kind === "richtext";
|
|
2238
|
+
}
|
|
2239
|
+
return false;
|
|
2240
|
+
}
|
|
2241
|
+
function dropCollectionStatements(collection) {
|
|
2242
|
+
const table = quoteIdentifier2(collection);
|
|
2243
|
+
const opsTable = quoteIdentifier2(`_kora_ops_${collection}`);
|
|
2244
|
+
return [`DROP TABLE IF EXISTS ${table}`, `DROP TABLE IF EXISTS ${opsTable}`];
|
|
2245
|
+
}
|
|
2246
|
+
function quoteIdentifier2(identifier) {
|
|
2247
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(identifier)) {
|
|
2248
|
+
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
2249
|
+
}
|
|
2250
|
+
return `"${identifier}"`;
|
|
2251
|
+
}
|
|
2252
|
+
function formatChange(change) {
|
|
2253
|
+
switch (change.type) {
|
|
2254
|
+
case "collection-added":
|
|
2255
|
+
return `+ collection ${change.collection}`;
|
|
2256
|
+
case "collection-removed":
|
|
2257
|
+
return `- collection ${change.collection}`;
|
|
2258
|
+
case "field-added":
|
|
2259
|
+
return `+ ${change.collection}.${change.field}`;
|
|
2260
|
+
case "field-removed":
|
|
2261
|
+
return `- ${change.collection}.${change.field}`;
|
|
2262
|
+
case "field-changed":
|
|
2263
|
+
return `~ ${change.collection}.${change.field}`;
|
|
2264
|
+
case "index-added":
|
|
2265
|
+
return `+ index ${change.collection}.${change.index}`;
|
|
2266
|
+
case "index-removed":
|
|
2267
|
+
return `- index ${change.collection}.${change.index}`;
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
// src/commands/migrate/migration-runner.ts
|
|
2272
|
+
var MigrationApplyError = class extends Error {
|
|
2273
|
+
constructor(message, backend, report) {
|
|
2274
|
+
super(message);
|
|
2275
|
+
this.backend = backend;
|
|
2276
|
+
this.report = report;
|
|
2277
|
+
this.name = "MigrationApplyError";
|
|
2278
|
+
}
|
|
2279
|
+
backend;
|
|
2280
|
+
report;
|
|
2281
|
+
};
|
|
2282
|
+
async function runMigration(options) {
|
|
2283
|
+
const report = { backends: [] };
|
|
2284
|
+
const migrationId = options.migrationId ?? `migration-${Date.now()}`;
|
|
2285
|
+
const fromVersion = options.fromVersion ?? 0;
|
|
2286
|
+
const toVersion = options.toVersion ?? 0;
|
|
2287
|
+
if (options.sqlitePath) {
|
|
2288
|
+
try {
|
|
2289
|
+
const sqliteReport = await runSqliteMigration(
|
|
2290
|
+
options.sqlitePath,
|
|
2291
|
+
options.upStatements,
|
|
2292
|
+
migrationId,
|
|
2293
|
+
fromVersion,
|
|
2294
|
+
toVersion,
|
|
2295
|
+
options.projectRoot,
|
|
2296
|
+
options.sqliteDriver
|
|
2297
|
+
);
|
|
2298
|
+
report.backends.push(sqliteReport);
|
|
2299
|
+
} catch (error) {
|
|
2300
|
+
throw new MigrationApplyError(error.message, "sqlite", report);
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
if (options.postgresConnectionString) {
|
|
2304
|
+
try {
|
|
2305
|
+
const postgresReport = await runPostgresMigration(
|
|
2306
|
+
options.postgresConnectionString,
|
|
2307
|
+
options.upStatements,
|
|
2308
|
+
migrationId,
|
|
2309
|
+
fromVersion,
|
|
2310
|
+
toVersion,
|
|
2311
|
+
options.postgresClientFactory
|
|
2312
|
+
);
|
|
2313
|
+
report.backends.push(postgresReport);
|
|
2314
|
+
} catch (error) {
|
|
2315
|
+
throw new MigrationApplyError(error.message, "postgres", report);
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
return report;
|
|
2319
|
+
}
|
|
2320
|
+
async function runSqliteMigration(path, statements, migrationId, fromVersion, toVersion, projectRoot, driverOverride) {
|
|
2321
|
+
const driver = driverOverride ?? await loadSqliteDriver(projectRoot);
|
|
2322
|
+
const db = driver.open(path);
|
|
2323
|
+
let statementsApplied = 0;
|
|
2324
|
+
const foreignKeysOn = Number(db.query?.("PRAGMA foreign_keys")[0]?.foreign_keys ?? 0) === 1;
|
|
2325
|
+
if (foreignKeysOn) db.exec("PRAGMA foreign_keys = OFF");
|
|
2326
|
+
try {
|
|
2327
|
+
db.exec("BEGIN");
|
|
2328
|
+
db.exec(
|
|
2329
|
+
"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)"
|
|
2330
|
+
);
|
|
2331
|
+
const alreadyApplied = typeof db.isMigrationApplied === "function" ? db.isMigrationApplied(migrationId) : false;
|
|
2332
|
+
if (alreadyApplied) {
|
|
2333
|
+
db.exec("COMMIT");
|
|
2334
|
+
return {
|
|
2335
|
+
backend: "sqlite",
|
|
2336
|
+
statementsApplied: 0,
|
|
2337
|
+
historyRecorded: true,
|
|
2338
|
+
skipped: true
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
const now = Date.now();
|
|
2342
|
+
const catalogQuery = (table) => {
|
|
2343
|
+
const query = db.query?.bind(db);
|
|
2344
|
+
if (!query) {
|
|
2345
|
+
throw new Error(
|
|
2346
|
+
`Migration ${migrationId}: the SQLite driver cannot read the catalog needed to migrate "${table}".`
|
|
2347
|
+
);
|
|
2348
|
+
}
|
|
2349
|
+
return async (text) => query(text);
|
|
2350
|
+
};
|
|
2351
|
+
for (const statement of statements) {
|
|
2352
|
+
const relax = parseRelaxValueDomainDirective(statement);
|
|
2353
|
+
const evolve = relax ? null : parseEvolveTableDirective(statement);
|
|
2354
|
+
if (relax) {
|
|
2355
|
+
const expanded = await expandRelaxValueDomainForSqlite(relax, catalogQuery(relax.table));
|
|
2356
|
+
for (const rebuild of expanded) db.exec(rebuild);
|
|
2357
|
+
} else if (evolve) {
|
|
2358
|
+
const expanded = await expandEvolveTableForSqlite(evolve, catalogQuery(evolve.table), now);
|
|
2359
|
+
for (const change of expanded) db.exec(change);
|
|
2360
|
+
} else {
|
|
2361
|
+
db.exec(statement);
|
|
2362
|
+
}
|
|
2363
|
+
statementsApplied++;
|
|
2364
|
+
}
|
|
2365
|
+
const violations = db.query?.("PRAGMA foreign_key_check") ?? [];
|
|
2366
|
+
if (violations.length > 0) {
|
|
2367
|
+
throw new Error(
|
|
2368
|
+
`Migration ${migrationId} left ${violations.length} foreign key violation(s) (first: ${JSON.stringify(violations[0])}); nothing was applied.`
|
|
2369
|
+
);
|
|
2370
|
+
}
|
|
2371
|
+
db.exec(
|
|
2372
|
+
`INSERT OR REPLACE INTO _kora_migrations (id, from_version, to_version, applied_at) VALUES (${sqlLiteral(migrationId)}, ${fromVersion}, ${toVersion}, ${Date.now()})`
|
|
2373
|
+
);
|
|
2374
|
+
db.exec("COMMIT");
|
|
2375
|
+
return {
|
|
2376
|
+
backend: "sqlite",
|
|
2377
|
+
statementsApplied,
|
|
2378
|
+
historyRecorded: true,
|
|
2379
|
+
skipped: false
|
|
2380
|
+
};
|
|
2381
|
+
} catch (error) {
|
|
2382
|
+
try {
|
|
2383
|
+
db.exec("ROLLBACK");
|
|
2384
|
+
} catch {
|
|
2385
|
+
}
|
|
2386
|
+
throw error;
|
|
2387
|
+
} finally {
|
|
2388
|
+
if (foreignKeysOn) {
|
|
2389
|
+
try {
|
|
2390
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
2391
|
+
} catch {
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
if (typeof db.close === "function") {
|
|
2395
|
+
db.close();
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
async function loadSqliteDriver(projectRoot) {
|
|
2400
|
+
try {
|
|
2401
|
+
const { createRequire } = await import("module");
|
|
2402
|
+
const requireFrom = createRequire(projectRoot ? `${projectRoot}/package.json` : import.meta.url);
|
|
2403
|
+
const Database = requireFrom("better-sqlite3");
|
|
2404
|
+
return {
|
|
2405
|
+
open(path) {
|
|
2406
|
+
const db = new Database(path);
|
|
2407
|
+
return {
|
|
2408
|
+
exec(sql) {
|
|
2409
|
+
db.exec(sql);
|
|
2410
|
+
},
|
|
2411
|
+
isMigrationApplied(id) {
|
|
2412
|
+
const row = db.prepare("SELECT COUNT(*) AS count FROM _kora_migrations WHERE id = ?").get(id);
|
|
2413
|
+
return (row?.count ?? 0) > 0;
|
|
2414
|
+
},
|
|
2415
|
+
query(sql) {
|
|
2416
|
+
return db.prepare(sql).all();
|
|
2417
|
+
},
|
|
2418
|
+
close() {
|
|
2419
|
+
db.close();
|
|
2420
|
+
}
|
|
2421
|
+
};
|
|
2422
|
+
}
|
|
2423
|
+
};
|
|
2424
|
+
} catch {
|
|
2425
|
+
throw new Error(
|
|
2426
|
+
'SQLite migration apply requires the "better-sqlite3" package in the target project dependencies.'
|
|
2427
|
+
);
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
async function runPostgresMigration(connectionString, statements, migrationId, fromVersion, toVersion, clientFactoryOverride) {
|
|
2431
|
+
const sql = typeof clientFactoryOverride === "function" ? clientFactoryOverride(connectionString) : (
|
|
2432
|
+
// One connection: BEGIN, the statements and COMMIT must share it (postgres.js
|
|
2433
|
+
// refuses a raw BEGIN on a pool, UNSAFE_TRANSACTION).
|
|
2434
|
+
(await loadPostgresModule()).default(connectionString, { max: 1 })
|
|
2435
|
+
);
|
|
2436
|
+
let statementsApplied = 0;
|
|
2437
|
+
try {
|
|
2438
|
+
await sql.unsafe("BEGIN");
|
|
2439
|
+
await sql.unsafe(
|
|
2440
|
+
"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)"
|
|
2441
|
+
);
|
|
2442
|
+
const existing = await sql.unsafe(
|
|
2443
|
+
`SELECT COUNT(*)::int AS count FROM _kora_migrations WHERE id = ${sqlLiteral(migrationId)}`
|
|
2444
|
+
);
|
|
2445
|
+
if ((existing[0]?.count ?? 0) > 0) {
|
|
2446
|
+
await sql.unsafe("COMMIT");
|
|
2447
|
+
return {
|
|
2448
|
+
backend: "postgres",
|
|
2449
|
+
statementsApplied: 0,
|
|
2450
|
+
historyRecorded: true,
|
|
2451
|
+
skipped: true
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
const now = Date.now();
|
|
2455
|
+
const query = async (text) => await sql.unsafe(text);
|
|
2456
|
+
for (const statement of statements) {
|
|
2457
|
+
const relax = parseRelaxValueDomainDirective(statement);
|
|
2458
|
+
const evolve = relax ? null : parseEvolveTableDirective(statement);
|
|
2459
|
+
if (relax) {
|
|
2460
|
+
const expanded = await expandRelaxValueDomainForPostgres(relax, query);
|
|
2461
|
+
for (const alter of expanded) await sql.unsafe(alter);
|
|
2462
|
+
} else if (evolve) {
|
|
2463
|
+
const expanded = await expandEvolveTableForPostgres(evolve, query, now);
|
|
2464
|
+
for (const alter of expanded) await sql.unsafe(alter);
|
|
2465
|
+
} else {
|
|
2466
|
+
assertNotLegacyRebuild(statement, migrationId);
|
|
2467
|
+
await sql.unsafe(statement);
|
|
2468
|
+
}
|
|
2469
|
+
statementsApplied++;
|
|
2470
|
+
}
|
|
2471
|
+
await sql.unsafe(
|
|
2472
|
+
`INSERT INTO _kora_migrations (id, from_version, to_version, applied_at) VALUES (${sqlLiteral(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`
|
|
2473
|
+
);
|
|
2474
|
+
await sql.unsafe("COMMIT");
|
|
2475
|
+
return {
|
|
2476
|
+
backend: "postgres",
|
|
2477
|
+
statementsApplied,
|
|
2478
|
+
historyRecorded: true,
|
|
2479
|
+
skipped: false
|
|
2480
|
+
};
|
|
2481
|
+
} catch (error) {
|
|
2482
|
+
try {
|
|
2483
|
+
await sql.unsafe("ROLLBACK");
|
|
2484
|
+
} catch {
|
|
2485
|
+
}
|
|
2486
|
+
throw error;
|
|
2487
|
+
} finally {
|
|
2488
|
+
if (typeof sql.end === "function") {
|
|
2489
|
+
await sql.end();
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
function assertNotLegacyRebuild(statement, migrationId) {
|
|
2494
|
+
if (/^\s*CREATE\s+TABLE\s+"?_kora_mig_[A-Za-z0-9_]+_new"?/i.test(statement)) {
|
|
2495
|
+
throw new Error(
|
|
2496
|
+
`Migration ${migrationId} was generated by an earlier \`kora migrate\` as a SQLite-only table rebuild, which cannot run on Postgres. Regenerate it: restore kora/schema.snapshot.json to the schema version it migrates from, delete the migration's files, and run \`kora migrate\` again.`
|
|
2497
|
+
);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
function sqlLiteral(value) {
|
|
2501
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
2502
|
+
}
|
|
2503
|
+
async function loadPostgresModule() {
|
|
2504
|
+
try {
|
|
2505
|
+
const dynamicImport = new Function("specifier", "return import(specifier)");
|
|
2506
|
+
const mod = await dynamicImport("postgres");
|
|
2507
|
+
if (typeof mod === "object" && mod !== null && "default" in mod) {
|
|
2508
|
+
return mod;
|
|
2509
|
+
}
|
|
2510
|
+
throw new Error("Invalid postgres module");
|
|
2511
|
+
} catch {
|
|
2512
|
+
throw new Error(
|
|
2513
|
+
'PostgreSQL migration apply requires the "postgres" package in the target project dependencies.'
|
|
2514
|
+
);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
// src/commands/migrate/operation-transform-generator.ts
|
|
2519
|
+
function generateOperationTransformModule(fromVersion, toVersion) {
|
|
2520
|
+
return [
|
|
2521
|
+
"import type { Operation, OperationTransform } from '@korajs/core'",
|
|
2522
|
+
"",
|
|
2523
|
+
"/**",
|
|
2524
|
+
` * Operation transforms for schema v${fromVersion} \u2192 v${toVersion}.`,
|
|
2525
|
+
" * Implement `transform` to rewrite or drop legacy operations during sync.",
|
|
2526
|
+
" * @see https://korajs.dev/docs/migrations",
|
|
2527
|
+
" */",
|
|
2528
|
+
"export const operationTransforms: OperationTransform[] = [",
|
|
2529
|
+
" {",
|
|
2530
|
+
` fromVersion: ${fromVersion},`,
|
|
2531
|
+
` toVersion: ${toVersion},`,
|
|
2532
|
+
" transform(operation: Operation): Operation | null {",
|
|
2533
|
+
" // Return null to drop unmigratable operations.",
|
|
2534
|
+
" return operation",
|
|
2535
|
+
" },",
|
|
2536
|
+
" },",
|
|
2537
|
+
"]",
|
|
2538
|
+
""
|
|
2539
|
+
].join("\n");
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
// src/commands/migrate/migrate-command.ts
|
|
2543
|
+
var SNAPSHOT_PATH = "kora/schema.snapshot.json";
|
|
2544
|
+
var MIGRATIONS_DIR = "kora/migrations";
|
|
2545
|
+
var migrateCommand = defineCommand7({
|
|
2546
|
+
meta: {
|
|
2547
|
+
name: "migrate",
|
|
2548
|
+
description: "Detect schema changes and generate/apply migrations"
|
|
2549
|
+
},
|
|
2550
|
+
args: {
|
|
2551
|
+
apply: {
|
|
2552
|
+
type: "boolean",
|
|
2553
|
+
description: "Apply migration to configured database backends",
|
|
2554
|
+
default: false
|
|
2555
|
+
},
|
|
2556
|
+
schema: {
|
|
2557
|
+
type: "string",
|
|
2558
|
+
description: "Path to schema file"
|
|
2559
|
+
},
|
|
2560
|
+
db: {
|
|
2561
|
+
type: "string",
|
|
2562
|
+
description: "SQLite database path for --apply (overrides config)"
|
|
2563
|
+
},
|
|
2564
|
+
"output-dir": {
|
|
2565
|
+
type: "string",
|
|
2566
|
+
description: "Migration output directory",
|
|
2567
|
+
default: MIGRATIONS_DIR
|
|
2568
|
+
},
|
|
2569
|
+
"dry-run": {
|
|
2570
|
+
type: "boolean",
|
|
2571
|
+
description: "Preview migration changes without writing files",
|
|
2572
|
+
default: false
|
|
2573
|
+
},
|
|
2574
|
+
force: {
|
|
2575
|
+
type: "boolean",
|
|
2576
|
+
description: "Skip breaking-change confirmation prompts",
|
|
2577
|
+
default: false
|
|
2578
|
+
}
|
|
2579
|
+
},
|
|
2580
|
+
async run({ args }) {
|
|
2581
|
+
const logger = createLogger();
|
|
2582
|
+
const projectRoot = await findProjectRoot();
|
|
2583
|
+
if (!projectRoot) {
|
|
2584
|
+
throw new InvalidProjectError(process.cwd());
|
|
2585
|
+
}
|
|
2586
|
+
const config = await loadKoraConfig(projectRoot);
|
|
2587
|
+
const resolvedSchemaPath = typeof args.schema === "string" ? resolve5(projectRoot, args.schema) : typeof config?.schema === "string" ? resolve5(projectRoot, config.schema) : await findSchemaFile(projectRoot);
|
|
2588
|
+
if (!resolvedSchemaPath) {
|
|
2589
|
+
throw new SchemaNotFoundError(["src/schema.ts", "schema.ts", "src/schema.js", "schema.js"]);
|
|
2590
|
+
}
|
|
2591
|
+
const currentSchema = await loadSchemaDefinition(resolvedSchemaPath, projectRoot);
|
|
2592
|
+
const snapshotFile = join6(projectRoot, SNAPSHOT_PATH);
|
|
2593
|
+
const previousSchema = await readSchemaSnapshot(snapshotFile);
|
|
2594
|
+
if (!previousSchema) {
|
|
2595
|
+
if (args["dry-run"] === true) {
|
|
2596
|
+
logger.info("No schema snapshot found. Dry run: baseline snapshot would be created.");
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
await writeSchemaSnapshot(snapshotFile, currentSchema);
|
|
2600
|
+
logger.success(`Initialized schema snapshot at ${snapshotFile}`);
|
|
2601
|
+
logger.step("Run `kora migrate` again after schema changes to generate migrations.");
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2604
|
+
const diff = diffSchemas(previousSchema, currentSchema);
|
|
2605
|
+
const outputDir = typeof args["output-dir"] === "string" ? resolve5(projectRoot, args["output-dir"]) : resolve5(projectRoot, MIGRATIONS_DIR);
|
|
2606
|
+
if (!diff.hasChanges) {
|
|
2607
|
+
logger.success("No schema changes detected.");
|
|
2608
|
+
if (args.apply === true) {
|
|
2609
|
+
const sqlitePath = resolveSqliteApplyPath(args.db, projectRoot, config);
|
|
2610
|
+
const postgresConnectionString = resolvePostgresConnectionString(config);
|
|
2611
|
+
const pending = await listMigrationManifests(outputDir);
|
|
2612
|
+
if (pending.length === 0) {
|
|
2613
|
+
logger.step("No migration files found to apply.");
|
|
2614
|
+
return;
|
|
2615
|
+
}
|
|
2616
|
+
for (const manifest of pending) {
|
|
2617
|
+
const report = await runMigration({
|
|
2618
|
+
upStatements: manifest.up,
|
|
2619
|
+
migrationId: manifest.id,
|
|
2620
|
+
fromVersion: manifest.fromVersion,
|
|
2621
|
+
toVersion: manifest.toVersion,
|
|
2622
|
+
sqlitePath,
|
|
2623
|
+
postgresConnectionString,
|
|
2624
|
+
projectRoot
|
|
2625
|
+
});
|
|
2626
|
+
for (const backend of report.backends) {
|
|
2627
|
+
logger.step(
|
|
2628
|
+
` ${manifest.id} -> ${backend.backend}: applied=${backend.statementsApplied}, skipped=${backend.skipped}`
|
|
2629
|
+
);
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
const generated = generateMigration(previousSchema, currentSchema, diff);
|
|
2636
|
+
logger.banner();
|
|
2637
|
+
logger.info(`Detected schema change: v${diff.fromVersion} \u2192 v${diff.toVersion}`);
|
|
2638
|
+
logger.blank();
|
|
2639
|
+
logger.info("Changes:");
|
|
2640
|
+
for (const line of generated.summary) {
|
|
2641
|
+
logger.step(` ${line}`);
|
|
2642
|
+
}
|
|
2643
|
+
if (diff.hasBreakingChanges && args["dry-run"] !== true) {
|
|
2644
|
+
logger.blank();
|
|
2645
|
+
logger.warn("Breaking schema changes detected.");
|
|
2646
|
+
const shouldContinue = await confirmBreakingChanges(args.force === true);
|
|
2647
|
+
if (!shouldContinue) {
|
|
2648
|
+
logger.warn("Migration generation aborted.");
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
if (args["dry-run"] === true) {
|
|
2653
|
+
logger.blank();
|
|
2654
|
+
logger.warn("Dry run enabled: no files written, no migrations applied.");
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2657
|
+
await mkdir2(outputDir, { recursive: true });
|
|
2658
|
+
const migrationPath = await writeMigrationFile(
|
|
2659
|
+
outputDir,
|
|
2660
|
+
diff.fromVersion,
|
|
2661
|
+
diff.toVersion,
|
|
2662
|
+
generated
|
|
2663
|
+
);
|
|
2664
|
+
await writeSchemaSnapshot(snapshotFile, currentSchema);
|
|
2665
|
+
logger.blank();
|
|
2666
|
+
logger.success(`Generated migration: ${migrationPath}`);
|
|
2667
|
+
if (args.apply === true) {
|
|
2668
|
+
const sqlitePath = resolveSqliteApplyPath(args.db, projectRoot, config);
|
|
2669
|
+
const postgresConnectionString = resolvePostgresConnectionString(config);
|
|
2670
|
+
const pending = await listMigrationManifests(outputDir);
|
|
2671
|
+
for (const manifest of pending) {
|
|
2672
|
+
const report = await runMigration({
|
|
2673
|
+
upStatements: manifest.up,
|
|
2674
|
+
migrationId: manifest.id,
|
|
2675
|
+
fromVersion: manifest.fromVersion,
|
|
2676
|
+
toVersion: manifest.toVersion,
|
|
2677
|
+
sqlitePath,
|
|
2678
|
+
postgresConnectionString,
|
|
2679
|
+
projectRoot
|
|
2680
|
+
});
|
|
2681
|
+
for (const backend of report.backends) {
|
|
2682
|
+
logger.step(
|
|
2683
|
+
` ${manifest.id} -> ${backend.backend}: applied=${backend.statementsApplied}, skipped=${backend.skipped}, history=${backend.historyRecorded}`
|
|
2684
|
+
);
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
logger.success("Applied pending migrations successfully.");
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
});
|
|
2691
|
+
async function readSchemaSnapshot(path) {
|
|
2692
|
+
try {
|
|
2693
|
+
const content = await readFile3(path, "utf-8");
|
|
2694
|
+
return JSON.parse(content);
|
|
2695
|
+
} catch {
|
|
2696
|
+
return null;
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
async function confirmBreakingChanges(force) {
|
|
2700
|
+
if (force) {
|
|
2701
|
+
return true;
|
|
2702
|
+
}
|
|
2703
|
+
if (!isInteractiveTerminal()) {
|
|
2704
|
+
throw new Error(
|
|
2705
|
+
"Breaking schema changes require confirmation. Re-run with --force to continue or --dry-run to preview."
|
|
2706
|
+
);
|
|
2707
|
+
}
|
|
2708
|
+
return await promptConfirm("Continue and generate a breaking migration?", false);
|
|
2709
|
+
}
|
|
2710
|
+
function isInteractiveTerminal() {
|
|
2711
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
2712
|
+
}
|
|
2713
|
+
async function writeSchemaSnapshot(path, schema) {
|
|
2714
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
2715
|
+
await writeFile3(path, `${JSON.stringify(schema, null, 2)}
|
|
2716
|
+
`, "utf-8");
|
|
2717
|
+
}
|
|
2718
|
+
async function writeMigrationFile(outputDir, fromVersion, toVersion, generated) {
|
|
2719
|
+
const existing = await readdir(outputDir).catch(() => []);
|
|
2720
|
+
const sequence = existing.filter((file) => /^\d{3}-/.test(file)).length + 1;
|
|
2721
|
+
const filename = `${String(sequence).padStart(3, "0")}-v${fromVersion}-to-v${toVersion}.ts`;
|
|
2722
|
+
const path = join6(outputDir, filename);
|
|
2723
|
+
const migrationId = filename.replace(/\.ts$/, "");
|
|
2724
|
+
const fileContent = [
|
|
2725
|
+
`export const up = ${JSON.stringify(generated.up, null, 2)} as const`,
|
|
2726
|
+
"",
|
|
2727
|
+
`export const down = ${JSON.stringify(generated.down, null, 2)} as const`,
|
|
2728
|
+
"",
|
|
2729
|
+
`export const summary = ${JSON.stringify(generated.summary, null, 2)} as const`,
|
|
2730
|
+
"",
|
|
2731
|
+
`export const containsBreakingChanges = ${generated.containsBreakingChanges}`,
|
|
2732
|
+
""
|
|
2733
|
+
].join("\n");
|
|
2734
|
+
await writeFile3(path, fileContent, "utf-8");
|
|
2735
|
+
const transformsPath = join6(outputDir, `${migrationId}.transforms.ts`);
|
|
2736
|
+
await writeFile3(transformsPath, generateOperationTransformModule(fromVersion, toVersion), "utf-8");
|
|
2737
|
+
await writeMigrationManifest(join6(outputDir, `${migrationId}.json`), {
|
|
2738
|
+
id: migrationId,
|
|
2739
|
+
fromVersion,
|
|
2740
|
+
toVersion,
|
|
2741
|
+
up: generated.up,
|
|
2742
|
+
down: generated.down,
|
|
2743
|
+
summary: generated.summary,
|
|
2744
|
+
containsBreakingChanges: generated.containsBreakingChanges
|
|
2745
|
+
});
|
|
2746
|
+
return path;
|
|
2747
|
+
}
|
|
2748
|
+
async function writeMigrationManifest(path, manifest) {
|
|
2749
|
+
await writeFile3(path, `${JSON.stringify(manifest, null, 2)}
|
|
2750
|
+
`, "utf-8");
|
|
2751
|
+
}
|
|
2752
|
+
async function listMigrationManifests(outputDir) {
|
|
2753
|
+
const files = await readdir(outputDir).catch(() => []);
|
|
2754
|
+
const migrationFiles = files.filter((file) => /^\d{3}-.*\.ts$/.test(file)).sort((left, right) => left.localeCompare(right));
|
|
2755
|
+
const manifests = [];
|
|
2756
|
+
for (const file of migrationFiles) {
|
|
2757
|
+
const id = file.replace(/\.ts$/, "");
|
|
2758
|
+
const manifestPath = join6(outputDir, `${id}.json`);
|
|
2759
|
+
const jsonManifest = await readMigrationManifest(manifestPath);
|
|
2760
|
+
if (jsonManifest) {
|
|
2761
|
+
manifests.push({ ...jsonManifest, id });
|
|
2762
|
+
continue;
|
|
2763
|
+
}
|
|
2764
|
+
const migrationPath = join6(outputDir, file);
|
|
2765
|
+
const sourceManifest = await readMigrationManifestFromSource(migrationPath, id);
|
|
2766
|
+
manifests.push(sourceManifest);
|
|
2767
|
+
}
|
|
2768
|
+
return manifests;
|
|
2769
|
+
}
|
|
2770
|
+
async function readMigrationManifest(path) {
|
|
2771
|
+
try {
|
|
2772
|
+
const content = await readFile3(path, "utf-8");
|
|
2773
|
+
return JSON.parse(content);
|
|
2774
|
+
} catch (error) {
|
|
2775
|
+
const code = error.code;
|
|
2776
|
+
if (code === "ENOENT") {
|
|
2777
|
+
return null;
|
|
2778
|
+
}
|
|
2779
|
+
throw error;
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
async function readMigrationManifestFromSource(path, id) {
|
|
2783
|
+
const content = await readFile3(path, "utf-8");
|
|
2784
|
+
const versions = parseVersionsFromMigrationId(id);
|
|
2785
|
+
return {
|
|
2786
|
+
id,
|
|
2787
|
+
fromVersion: versions.fromVersion,
|
|
2788
|
+
toVersion: versions.toVersion,
|
|
2789
|
+
up: parseStringArrayExport(content, "up"),
|
|
2790
|
+
down: parseStringArrayExport(content, "down"),
|
|
2791
|
+
summary: parseStringArrayExport(content, "summary"),
|
|
2792
|
+
containsBreakingChanges: parseBooleanExport(content, "containsBreakingChanges")
|
|
2793
|
+
};
|
|
2794
|
+
}
|
|
2795
|
+
function parseVersionsFromMigrationId(id) {
|
|
2796
|
+
const match = id.match(/-v(\d+)-to-v(\d+)$/);
|
|
2797
|
+
if (!match) {
|
|
2798
|
+
throw new Error(`Migration id "${id}" does not include a vX-to-vY version suffix.`);
|
|
2799
|
+
}
|
|
2800
|
+
const fromVersionText = match[1];
|
|
2801
|
+
const toVersionText = match[2];
|
|
2802
|
+
if (fromVersionText === void 0 || toVersionText === void 0) {
|
|
2803
|
+
throw new Error(`Migration id "${id}" does not include a vX-to-vY version suffix.`);
|
|
2804
|
+
}
|
|
2805
|
+
return {
|
|
2806
|
+
fromVersion: Number.parseInt(fromVersionText, 10),
|
|
2807
|
+
toVersion: Number.parseInt(toVersionText, 10)
|
|
2808
|
+
};
|
|
2809
|
+
}
|
|
2810
|
+
function parseStringArrayExport(source, exportName) {
|
|
2811
|
+
const expression = parseExportExpression(source, exportName);
|
|
2812
|
+
const parsed = JSON.parse(expression);
|
|
2813
|
+
if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
|
|
2814
|
+
throw new Error(`Migration export "${exportName}" must be a string array.`);
|
|
2815
|
+
}
|
|
2816
|
+
return parsed;
|
|
2817
|
+
}
|
|
2818
|
+
function parseBooleanExport(source, exportName) {
|
|
2819
|
+
const expression = parseLiteralExport(source, exportName);
|
|
2820
|
+
if (expression === "true") return true;
|
|
2821
|
+
if (expression === "false") return false;
|
|
2822
|
+
throw new Error(`Migration export "${exportName}" must be a boolean literal.`);
|
|
2823
|
+
}
|
|
2824
|
+
function parseExportExpression(source, exportName) {
|
|
2825
|
+
const escapedName = exportName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2826
|
+
const regex = new RegExp(`export const ${escapedName} = ([\\s\\S]*?) as const`);
|
|
2827
|
+
const match = source.match(regex);
|
|
2828
|
+
if (!match || !match[1]) {
|
|
2829
|
+
throw new Error(`Failed to read migration export "${exportName}".`);
|
|
2830
|
+
}
|
|
2831
|
+
return match[1].trim();
|
|
2832
|
+
}
|
|
2833
|
+
function parseLiteralExport(source, exportName) {
|
|
2834
|
+
const escapedName = exportName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2835
|
+
const regex = new RegExp(`export const ${escapedName} = ([^
|
|
2836
|
+
\r]+)`);
|
|
2837
|
+
const match = source.match(regex);
|
|
2838
|
+
if (!match || !match[1]) {
|
|
2839
|
+
throw new Error(`Failed to read migration export "${exportName}".`);
|
|
2840
|
+
}
|
|
2841
|
+
return match[1].trim();
|
|
2842
|
+
}
|
|
2843
|
+
function resolveSqliteApplyPath(dbArg, projectRoot, config) {
|
|
2844
|
+
if (typeof dbArg === "string") {
|
|
2845
|
+
return resolve5(projectRoot, dbArg);
|
|
2846
|
+
}
|
|
2847
|
+
const sync = config?.dev?.sync;
|
|
2848
|
+
if (typeof sync === "object" && sync !== null) {
|
|
2849
|
+
if (sync.store === "sqlite") {
|
|
2850
|
+
return join6(projectRoot, "kora-sync.db");
|
|
2851
|
+
}
|
|
2852
|
+
if (typeof sync.store === "object" && sync.store !== null && sync.store.type === "sqlite") {
|
|
2853
|
+
if (typeof sync.store.filename === "string" && sync.store.filename.length > 0) {
|
|
2854
|
+
return resolve5(projectRoot, sync.store.filename);
|
|
2855
|
+
}
|
|
2856
|
+
return join6(projectRoot, "kora-sync.db");
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
return void 0;
|
|
2860
|
+
}
|
|
2861
|
+
function resolvePostgresConnectionString(config) {
|
|
2862
|
+
const sync = config?.dev?.sync;
|
|
2863
|
+
if (typeof sync !== "object" || sync === null) return void 0;
|
|
2864
|
+
if (sync.store === "postgres") {
|
|
2865
|
+
return process.env.DATABASE_URL;
|
|
2866
|
+
}
|
|
2867
|
+
if (typeof sync.store === "object" && sync.store !== null && sync.store.type === "postgres") {
|
|
2868
|
+
return sync.store.connectionString;
|
|
2869
|
+
}
|
|
2870
|
+
return void 0;
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
// src/commands/status/status-command.ts
|
|
2874
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
2875
|
+
var DEFAULT_SYNC_PORT3 = 3001;
|
|
2876
|
+
var STATUS_ENDPOINT2 = "/__kora/status";
|
|
2877
|
+
async function fetchServerStatus(url, token) {
|
|
2878
|
+
const statusUrl = `${url.replace(/\/$/, "")}${STATUS_ENDPOINT2}`;
|
|
2879
|
+
const response = await fetch(statusUrl, {
|
|
2880
|
+
headers: token ? { Authorization: `Bearer ${token}` } : void 0
|
|
2881
|
+
});
|
|
2882
|
+
if (!response.ok) {
|
|
2883
|
+
throw new Error(
|
|
2884
|
+
`Failed to fetch status from ${statusUrl}: ${response.status} ${response.statusText}`
|
|
2885
|
+
);
|
|
2886
|
+
}
|
|
2887
|
+
return await response.json();
|
|
2888
|
+
}
|
|
2889
|
+
function formatUptime(ms) {
|
|
2890
|
+
const seconds = Math.floor(ms / 1e3);
|
|
2891
|
+
const minutes = Math.floor(seconds / 60);
|
|
2892
|
+
const hours = Math.floor(minutes / 60);
|
|
2893
|
+
const days = Math.floor(hours / 24);
|
|
2894
|
+
const parts = [];
|
|
2895
|
+
if (days > 0) parts.push(`${days}d`);
|
|
2896
|
+
if (hours % 24 > 0) parts.push(`${hours % 24}h`);
|
|
2897
|
+
if (minutes % 60 > 0) parts.push(`${minutes % 60}m`);
|
|
2898
|
+
parts.push(`${seconds % 60}s`);
|
|
2899
|
+
return parts.join(" ");
|
|
2900
|
+
}
|
|
2901
|
+
var statusCommand = defineCommand8({
|
|
2902
|
+
meta: {
|
|
2903
|
+
name: "status",
|
|
2904
|
+
description: "Show Kora sync server status"
|
|
2905
|
+
},
|
|
2906
|
+
args: {
|
|
2907
|
+
url: {
|
|
2908
|
+
type: "string",
|
|
2909
|
+
description: "Sync server URL (default: http://localhost:3001)",
|
|
2910
|
+
default: `http://localhost:${DEFAULT_SYNC_PORT3}`
|
|
2911
|
+
},
|
|
2912
|
+
watch: {
|
|
2913
|
+
type: "boolean",
|
|
2914
|
+
description: "Live-updating status (like htop)",
|
|
2915
|
+
default: false,
|
|
2916
|
+
alias: "w"
|
|
2917
|
+
},
|
|
2918
|
+
token: {
|
|
2919
|
+
type: "string",
|
|
2920
|
+
description: "Admin token (defaults to KORA_ADMIN_TOKEN)"
|
|
2921
|
+
}
|
|
2922
|
+
},
|
|
2923
|
+
async run({ args }) {
|
|
2924
|
+
const logger = createLogger();
|
|
2925
|
+
const url = typeof args.url === "string" ? args.url : `http://localhost:${DEFAULT_SYNC_PORT3}`;
|
|
2926
|
+
const watch2 = args.watch === true;
|
|
2927
|
+
const token = typeof args.token === "string" ? args.token : process.env.KORA_ADMIN_TOKEN ?? void 0;
|
|
2928
|
+
try {
|
|
2929
|
+
if (watch2) {
|
|
2930
|
+
console.clear();
|
|
2931
|
+
logger.banner();
|
|
2932
|
+
logger.info(`Connecting to ${url}...`);
|
|
2933
|
+
const interval = setInterval(async () => {
|
|
2934
|
+
try {
|
|
2935
|
+
const status = await fetchServerStatus(url, token);
|
|
2936
|
+
printStatus(status, url, logger);
|
|
2937
|
+
} catch {
|
|
2938
|
+
}
|
|
2939
|
+
}, 2e3);
|
|
2940
|
+
try {
|
|
2941
|
+
const status = await fetchServerStatus(url, token);
|
|
2942
|
+
printStatus(status, url, logger);
|
|
2943
|
+
} catch {
|
|
2944
|
+
}
|
|
2945
|
+
await new Promise(() => {
|
|
2946
|
+
process.on("SIGINT", () => {
|
|
2947
|
+
clearInterval(interval);
|
|
2948
|
+
process.exit(0);
|
|
2949
|
+
});
|
|
2950
|
+
process.on("SIGTERM", () => {
|
|
2951
|
+
clearInterval(interval);
|
|
2952
|
+
process.exit(0);
|
|
2953
|
+
});
|
|
2954
|
+
});
|
|
2955
|
+
} else {
|
|
2956
|
+
const status = await fetchServerStatus(url, token);
|
|
2957
|
+
printStatus(status, url, logger);
|
|
2958
|
+
}
|
|
2959
|
+
} catch (error) {
|
|
2960
|
+
logger.error(`Failed to connect to ${url}`);
|
|
2961
|
+
if (error instanceof Error) {
|
|
2962
|
+
logger.error(error.message);
|
|
2963
|
+
}
|
|
2964
|
+
logger.blank();
|
|
2965
|
+
logger.step("Make sure the Kora sync server is running.");
|
|
2966
|
+
logger.step("Start it with: kora dev");
|
|
2967
|
+
process.exit(1);
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
});
|
|
2971
|
+
function printStatus(status, url, logger) {
|
|
2972
|
+
console.clear();
|
|
2973
|
+
logger.banner();
|
|
2974
|
+
logger.info(`Kora Sync Server \u2014 ${url}`);
|
|
2975
|
+
logger.blank();
|
|
2976
|
+
if (!status.running) {
|
|
2977
|
+
logger.error("Server is not running");
|
|
2978
|
+
return;
|
|
2979
|
+
}
|
|
2980
|
+
logger.step(`Status: Running (uptime: ${formatUptime(status.uptime)}) \u2014 v${status.version}`);
|
|
2981
|
+
logger.step(`Schema: v${status.schemaVersion}`);
|
|
2982
|
+
logger.blank();
|
|
2983
|
+
logger.step(`Connections: ${status.connectedClients} connected`);
|
|
2984
|
+
logger.step(` Peak: ${status.peakConnections}`);
|
|
2985
|
+
logger.step(` Total: ${status.connectionsTotal}`);
|
|
2986
|
+
if (status.connectedNodeIds.length > 0) {
|
|
2987
|
+
logger.step(" Active nodes:");
|
|
2988
|
+
for (const nodeId of status.connectedNodeIds) {
|
|
2989
|
+
logger.step(` \u2022 ${nodeId}`);
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
logger.blank();
|
|
2993
|
+
logger.step("Operations:");
|
|
2994
|
+
logger.step(` Received: ${status.operationsReceived.toLocaleString()}`);
|
|
2995
|
+
logger.step(` Sent: ${status.operationsSent.toLocaleString()}`);
|
|
2996
|
+
logger.step(` Total: ${status.totalOperations.toLocaleString()}`);
|
|
2997
|
+
logger.blank();
|
|
2998
|
+
if (status.errorCount > 0) {
|
|
2999
|
+
logger.warn(`Errors: ${status.errorCount}`);
|
|
3000
|
+
} else {
|
|
3001
|
+
logger.step("Errors: 0");
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
// src/commands/studio/studio-command.ts
|
|
3006
|
+
import { existsSync as existsSync2 } from "fs";
|
|
3007
|
+
import { resolve as resolve6 } from "path";
|
|
3008
|
+
import { defineCommand as defineCommand9 } from "citty";
|
|
3009
|
+
var studioCommand = defineCommand9({
|
|
3010
|
+
meta: {
|
|
3011
|
+
name: "studio",
|
|
3012
|
+
description: "Kora Studio: visualize records, operations, and sync \u2014 or run the sync lab"
|
|
3013
|
+
},
|
|
3014
|
+
args: {
|
|
3015
|
+
db: {
|
|
3016
|
+
type: "string",
|
|
3017
|
+
description: "Path to a Kora SQLite database file (file mode, read-only)"
|
|
3018
|
+
},
|
|
3019
|
+
lab: {
|
|
3020
|
+
type: "boolean",
|
|
3021
|
+
description: "Start the interactive multi-device sync laboratory"
|
|
3022
|
+
},
|
|
3023
|
+
connect: {
|
|
3024
|
+
type: "string",
|
|
3025
|
+
description: "Spectator mode: WebSocket URL of a live Kora sync server (read-only replica)"
|
|
3026
|
+
},
|
|
3027
|
+
token: {
|
|
3028
|
+
type: "string",
|
|
3029
|
+
description: "Spectator mode: bearer token for the sync auth handshake"
|
|
3030
|
+
},
|
|
3031
|
+
devices: {
|
|
3032
|
+
type: "string",
|
|
3033
|
+
description: "Lab mode: number of initial devices (default 2)"
|
|
3034
|
+
},
|
|
3035
|
+
schema: {
|
|
3036
|
+
type: "string",
|
|
3037
|
+
description: "Lab mode: path to a schema file (defaults to a built-in demo schema)"
|
|
3038
|
+
},
|
|
3039
|
+
port: {
|
|
3040
|
+
type: "string",
|
|
3041
|
+
description: "Port for the Studio UI (default 4321, 0 for a random free port)"
|
|
3042
|
+
}
|
|
3043
|
+
},
|
|
3044
|
+
async run({ args }) {
|
|
3045
|
+
const logger = createLogger();
|
|
3046
|
+
const port = args.port !== void 0 ? Number.parseInt(args.port, 10) : 4321;
|
|
3047
|
+
if (Number.isNaN(port) || port < 0 || port > 65535) {
|
|
3048
|
+
throw new Error(`Invalid port "${args.port}"`);
|
|
3049
|
+
}
|
|
3050
|
+
const { startStudioServer } = await import("./studio-server-XLGZNGG5.js");
|
|
3051
|
+
if (typeof args.connect === "string") {
|
|
3052
|
+
if (typeof args.schema !== "string") {
|
|
3053
|
+
throw new Error(
|
|
3054
|
+
"Spectator mode needs your schema to materialize records: kora studio --connect wss://\u2026 --schema ./kora/schema.ts"
|
|
3055
|
+
);
|
|
3056
|
+
}
|
|
3057
|
+
const { loadSchemaDefinition: loadSchemaDefinition2 } = await import("./schema-loader-GGHECMTM.js");
|
|
3058
|
+
const schema = await loadSchemaDefinition2(resolve6(process.cwd(), args.schema), process.cwd());
|
|
3059
|
+
const { SpectatorManager } = await import("./spectator-manager-FTH2NFSE.js");
|
|
3060
|
+
const spectator = new SpectatorManager({
|
|
3061
|
+
url: args.connect,
|
|
3062
|
+
schema,
|
|
3063
|
+
...typeof args.token === "string" ? { token: args.token } : {}
|
|
3064
|
+
});
|
|
3065
|
+
await spectator.start();
|
|
3066
|
+
const server2 = await startStudioServer({ port, spectator });
|
|
3067
|
+
logger.banner();
|
|
3068
|
+
logger.success(`Kora Studio SPECTATOR running at ${server2.url}`);
|
|
3069
|
+
logger.step(`Watching ${args.connect} (read-only replica \u2014 nothing is ever pushed)`);
|
|
3070
|
+
logger.step("Press Ctrl+C to stop.");
|
|
3071
|
+
const shutdown2 = async () => {
|
|
3072
|
+
await server2.close();
|
|
3073
|
+
await spectator.close();
|
|
3074
|
+
process.exit(0);
|
|
3075
|
+
};
|
|
3076
|
+
process.on("SIGINT", () => void shutdown2());
|
|
3077
|
+
process.on("SIGTERM", () => void shutdown2());
|
|
3078
|
+
return;
|
|
3079
|
+
}
|
|
3080
|
+
if (args.lab) {
|
|
3081
|
+
const { LabManager, defaultLabSchema } = await import("./lab-manager-KUDII6BT.js");
|
|
3082
|
+
let schema = defaultLabSchema();
|
|
3083
|
+
if (typeof args.schema === "string") {
|
|
3084
|
+
const { loadSchemaDefinition: loadSchemaDefinition2 } = await import("./schema-loader-GGHECMTM.js");
|
|
3085
|
+
schema = await loadSchemaDefinition2(resolve6(process.cwd(), args.schema), process.cwd());
|
|
3086
|
+
}
|
|
3087
|
+
const initialDevices = args.devices !== void 0 ? Math.max(1, Number.parseInt(args.devices, 10) || 2) : 2;
|
|
3088
|
+
const lab = new LabManager(schema);
|
|
3089
|
+
await lab.start(initialDevices);
|
|
3090
|
+
const server2 = await startStudioServer({ port, lab });
|
|
3091
|
+
logger.banner();
|
|
3092
|
+
logger.success(`Kora Studio LAB running at ${server2.url}`);
|
|
3093
|
+
logger.step(`${initialDevices} device(s) started against an in-process sync server.`);
|
|
3094
|
+
logger.step("Everything in the lab is throwaway \u2014 experiment freely.");
|
|
3095
|
+
logger.step("Press Ctrl+C to stop.");
|
|
3096
|
+
const shutdown2 = async () => {
|
|
3097
|
+
await server2.close();
|
|
3098
|
+
await lab.close();
|
|
3099
|
+
process.exit(0);
|
|
3100
|
+
};
|
|
3101
|
+
process.on("SIGINT", () => void shutdown2());
|
|
3102
|
+
process.on("SIGTERM", () => void shutdown2());
|
|
3103
|
+
return;
|
|
3104
|
+
}
|
|
3105
|
+
if (typeof args.db !== "string") {
|
|
3106
|
+
throw new Error(
|
|
3107
|
+
"Pass --db <path> to inspect a database, --lab for the sync laboratory, or --connect <wss url> for live spectator mode."
|
|
3108
|
+
);
|
|
3109
|
+
}
|
|
3110
|
+
const dbPath = resolve6(process.cwd(), args.db);
|
|
3111
|
+
if (!existsSync2(dbPath)) {
|
|
3112
|
+
throw new Error(`Database file not found: ${dbPath}`);
|
|
3113
|
+
}
|
|
3114
|
+
const server = await startStudioServer({ port, dbPath });
|
|
3115
|
+
logger.banner();
|
|
3116
|
+
logger.success(`Kora Studio running at ${server.url}`);
|
|
3117
|
+
logger.step(`Database: ${dbPath} (read-only)`);
|
|
3118
|
+
logger.step("Press Ctrl+C to stop.");
|
|
3119
|
+
const shutdown = async () => {
|
|
3120
|
+
await server.close();
|
|
3121
|
+
process.exit(0);
|
|
3122
|
+
};
|
|
3123
|
+
process.on("SIGINT", () => void shutdown());
|
|
3124
|
+
process.on("SIGTERM", () => void shutdown());
|
|
3125
|
+
}
|
|
3126
|
+
});
|
|
3127
|
+
|
|
3128
|
+
// src/bin.ts
|
|
3129
|
+
var main = defineCommand10({
|
|
3130
|
+
meta: {
|
|
3131
|
+
name: "kora",
|
|
3132
|
+
description: "Kora.js, the offline-first application framework"
|
|
3133
|
+
},
|
|
3134
|
+
subCommands: {
|
|
3135
|
+
"agents-md": agentsCommand,
|
|
3136
|
+
backup: backupCommand,
|
|
3137
|
+
compact: compactCommand,
|
|
3138
|
+
create: createCommand,
|
|
3139
|
+
dev: devCommand,
|
|
3140
|
+
doctor: doctorCommand,
|
|
3141
|
+
deploy: deployCommand,
|
|
3142
|
+
generate: generateCommand,
|
|
3143
|
+
logs: logsCommand,
|
|
3144
|
+
migrate: migrateCommand,
|
|
3145
|
+
status: statusCommand,
|
|
3146
|
+
studio: studioCommand
|
|
3147
|
+
}
|
|
3148
|
+
});
|
|
3149
|
+
runMain(main);
|
|
3150
|
+
//# sourceMappingURL=bin.js.map
|