@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
|
@@ -0,0 +1,2569 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DeployPlatformUnavailableError,
|
|
4
|
+
InvalidProjectError,
|
|
5
|
+
createLogger,
|
|
6
|
+
createPromptClient
|
|
7
|
+
} from "./chunk-7Y4J2GXP.js";
|
|
8
|
+
import {
|
|
9
|
+
findProjectRoot,
|
|
10
|
+
resolveProjectBinaryEntryPoint
|
|
11
|
+
} from "./chunk-5NI2FEQL.js";
|
|
12
|
+
|
|
13
|
+
// src/commands/agents/agents-md-content.ts
|
|
14
|
+
var AGENTS_FRAMEWORKS = ["react", "vue", "svelte"];
|
|
15
|
+
function isAgentsFramework(value) {
|
|
16
|
+
return AGENTS_FRAMEWORKS.includes(value);
|
|
17
|
+
}
|
|
18
|
+
function detectFrameworkFromManifest(manifest) {
|
|
19
|
+
if (typeof manifest !== "object" || manifest === null) return null;
|
|
20
|
+
const record = manifest;
|
|
21
|
+
const deps = collectDependencyNames(record.dependencies, record.devDependencies);
|
|
22
|
+
if (deps.has("@korajs/react")) return "react";
|
|
23
|
+
if (deps.has("@korajs/vue")) return "vue";
|
|
24
|
+
if (deps.has("@korajs/svelte")) return "svelte";
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
function collectDependencyNames(...groups) {
|
|
28
|
+
const names = /* @__PURE__ */ new Set();
|
|
29
|
+
for (const group of groups) {
|
|
30
|
+
if (typeof group !== "object" || group === null) continue;
|
|
31
|
+
for (const name of Object.keys(group)) {
|
|
32
|
+
names.add(name);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return names;
|
|
36
|
+
}
|
|
37
|
+
var FRAMEWORK_BINDINGS = {
|
|
38
|
+
react: `## React bindings
|
|
39
|
+
|
|
40
|
+
\`\`\`tsx
|
|
41
|
+
import { KoraProvider, useCollection, useMutation, useQuery, useSyncStatus } from '@korajs/react'
|
|
42
|
+
|
|
43
|
+
// Wrap the tree once near your app entry: <KoraProvider app={app}><App /></KoraProvider>
|
|
44
|
+
|
|
45
|
+
const todos = useCollection('todos')
|
|
46
|
+
const rows = useQuery(todos.where({ completed: false }).orderBy('createdAt'))
|
|
47
|
+
const addTodo = useMutation((data) => todos.insert(data))
|
|
48
|
+
addTodo.mutate({ title: 'x' }) // fire-and-forget; errors land in addTodo.error
|
|
49
|
+
await addTodo.mutateAsync({ title: 'x' }) // resolves with the result; throws on failure
|
|
50
|
+
const status = useSyncStatus() // status.status, status.pendingOperations
|
|
51
|
+
\`\`\`
|
|
52
|
+
|
|
53
|
+
Rules: always render \`mutation.error\` somewhere. Keep \`<StrictMode>\` in place; the hooks are StrictMode-safe. \`useQuery\` reads the local store: no network, no spinner. Its very first render returns \`[]\` before the local query has run; use \`useQueryState\` when you must tell "loading" from "no rows".`,
|
|
54
|
+
vue: `## Vue bindings
|
|
55
|
+
|
|
56
|
+
\`\`\`ts
|
|
57
|
+
import { useCollection, useMutation, useQuery, useSyncStatus } from '@korajs/vue'
|
|
58
|
+
|
|
59
|
+
const todos = useCollection('todos')
|
|
60
|
+
const rows = useQuery(todos.where({ completed: false }).orderBy('createdAt')) // readonly reactive ref
|
|
61
|
+
const addTodo = useMutation((data) => todos.insert(data))
|
|
62
|
+
addTodo.mutate({ title: 'x' }) // fire-and-forget; errors land in addTodo.error
|
|
63
|
+
const status = useSyncStatus() // status.status, status.pendingOperations
|
|
64
|
+
\`\`\`
|
|
65
|
+
|
|
66
|
+
Rules: surface the mutation's error state in the UI. \`useQuery\` reads the local store (no network, no spinner); the ref holds \`[]\` until the first local result. The Kora context comes from \`KoraProvider\`, wired in your app entry (for example \`src/main.ts\`).`,
|
|
67
|
+
svelte: `## Svelte bindings
|
|
68
|
+
|
|
69
|
+
Kora's Svelte bindings from \`@korajs/svelte\` expose \`useCollection\`, \`useQuery\`, \`useMutation\`, and \`useSyncStatus\`, wired through the provider set up in your app entry.
|
|
70
|
+
|
|
71
|
+
\`\`\`ts
|
|
72
|
+
import { useCollection, useMutation, useQuery, useSyncStatus } from '@korajs/svelte'
|
|
73
|
+
|
|
74
|
+
const todos = useCollection('todos')
|
|
75
|
+
const rows = useQuery(todos.where({ completed: false }).orderBy('createdAt')) // reactive store
|
|
76
|
+
const addTodo = useMutation((data) => todos.insert(data))
|
|
77
|
+
addTodo.mutate({ title: 'x' }) // fire-and-forget; errors land in addTodo.error
|
|
78
|
+
const status = useSyncStatus() // readable store; read $status.status in markup
|
|
79
|
+
\`\`\`
|
|
80
|
+
|
|
81
|
+
Rules: queries are reactive stores over the local database (no network, no spinner; \`[]\` until the first local result). Mutations expose an \`error\` state that must be surfaced in the UI.`
|
|
82
|
+
};
|
|
83
|
+
function generateAgentsMd(framework) {
|
|
84
|
+
return `# AGENTS.md
|
|
85
|
+
|
|
86
|
+
Guidance for AI coding agents working in this project. Humans: this is useful for you too.
|
|
87
|
+
|
|
88
|
+
## What this project is
|
|
89
|
+
|
|
90
|
+
This project uses **Kora.js**, an offline-first application framework. All application data lives in a local SQLite database (WASM + OPFS in the browser, native SQLite on desktop and server) and optionally syncs across devices through a Kora sync server. Offline is the normal state, not an error state. Full docs live at https://korajs.dev, with a machine-readable index at https://korajs.dev/llms.txt and the complete docs in one file at https://korajs.dev/llms-full.txt. Any documentation page is available as raw markdown by appending \`.md\` to its URL.
|
|
91
|
+
|
|
92
|
+
## Golden rules
|
|
93
|
+
|
|
94
|
+
1. **The schema is the source of truth.** Collections are defined with \`defineSchema\` and the \`t.*\` field builders (by convention in \`src/schema.ts\`). To add or change data shapes, edit the schema first; types flow from it automatically. Never hand-write types for records.
|
|
95
|
+
2. **Never fetch application data over HTTP.** Do not add REST or GraphQL calls for app data, and do not talk to the sync server directly. Read and write through Kora collections only; sync happens automatically in the background.
|
|
96
|
+
3. **Await readiness before direct collection access.** Outside the UI bindings, \`await app.ready\` before calling \`app.<collection>\` methods. The framework bindings handle this for you inside components.
|
|
97
|
+
4. **Offline must keep working.** Any feature you add must function with the network off. Never gate a read or write on connectivity. If you are checking \`navigator.onLine\` before a data operation, you are doing it wrong.
|
|
98
|
+
5. **Surface mutation errors.** Fire-and-forget \`mutate\` calls put errors into the mutation state. Always render the mutation's \`error\`, or handle the promise from the \`mutateAsync\` variant. Silent failure is the worst failure.
|
|
99
|
+
6. **Do not add a state library for server or app data.** Kora's reactive queries are the store. Do not reach for react-query, SWR, Redux, Zustand, or similar for data that lives in a collection. Local UI state (form inputs, toggles) can use your framework's normal state tools.
|
|
100
|
+
7. **Do not add network-style loading states for local reads.** \`useQuery\` reads the local database, which answers within a frame; its very first render returns an empty result. Use \`useQueryState\` only where "loading" and "no rows" must look different.
|
|
101
|
+
|
|
102
|
+
## Data API cheat sheet
|
|
103
|
+
|
|
104
|
+
\`\`\`ts
|
|
105
|
+
await app.ready
|
|
106
|
+
const rec = await app.todos.insert({ title: 'x' }) // defaults and .auto() fields applied
|
|
107
|
+
await app.todos.update(rec.id, { completed: true }) // partial update, changed fields only
|
|
108
|
+
await app.todos.delete(rec.id)
|
|
109
|
+
const one = await app.todos.findById(rec.id)
|
|
110
|
+
const unsubscribe = app.todos
|
|
111
|
+
.where({ completed: false })
|
|
112
|
+
.orderBy('createdAt', 'desc')
|
|
113
|
+
.subscribe((rows) => {/* fires immediately, then on every change */})
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
Schema example (\`src/schema.ts\`):
|
|
117
|
+
|
|
118
|
+
\`\`\`ts
|
|
119
|
+
import { defineSchema, t } from 'korajs'
|
|
120
|
+
|
|
121
|
+
export default defineSchema({
|
|
122
|
+
version: 1,
|
|
123
|
+
collections: {
|
|
124
|
+
todos: {
|
|
125
|
+
fields: {
|
|
126
|
+
title: t.string(),
|
|
127
|
+
completed: t.boolean().default(false),
|
|
128
|
+
tags: t.array(t.string()).default([]),
|
|
129
|
+
priority: t.enum(['low', 'medium', 'high']).default('medium'),
|
|
130
|
+
createdAt: t.timestamp().auto(), // set automatically; never pass it on insert
|
|
131
|
+
},
|
|
132
|
+
indexes: ['completed', 'createdAt'],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
\`\`\`
|
|
137
|
+
|
|
138
|
+
If you change a collection's shape, increment \`version\` and add a \`migrations\` entry for it (\`migrate().addField(...)\`): each device migrates its local database on its next open. \`npx kora migrate\` generates the matching migration for server databases and a transform stub for clients still on the older version.
|
|
139
|
+
|
|
140
|
+
## Conflict handling
|
|
141
|
+
|
|
142
|
+
Concurrent edits merge automatically: last-write-wins per field, arrays as multisets that keep every device's additions and removals, objects per top-level key, and character-level CRDT for \`t.richtext()\` fields. When a field needs domain-specific merging (counters, quantities), add a \`resolve\` function in the schema rather than writing sync logic by hand. Never write your own conflict or merge code.
|
|
143
|
+
|
|
144
|
+
## Sync and auth
|
|
145
|
+
|
|
146
|
+
Enable sync with one option on \`createApp\`: \`createApp({ schema, sync: { url, autoConnect: true } })\` (without \`autoConnect\`, call \`await app.sync?.connect()\` after \`app.ready\`). When the app is offline, writes queue locally and sync when connectivity returns. If the project uses \`@korajs/auth\`, pass its binding as \`createApp({ schema, sync: { url, authClient: createKoraAuthSync({ authClient, schema }) } })\`; the server grants what each user may sync. Local writes work without sign-in; sync requires the server to accept the connection.
|
|
147
|
+
|
|
148
|
+
${FRAMEWORK_BINDINGS[framework]}
|
|
149
|
+
|
|
150
|
+
## Commands
|
|
151
|
+
|
|
152
|
+
- \`npx kora dev\` runs the dev environment (app server, local sync server when configured, schema watcher).
|
|
153
|
+
- \`npx kora doctor\` diagnoses a broken setup.
|
|
154
|
+
- \`npx kora migrate\` generates a migration from a schema change (\`--apply\` runs it against a SQLite or Postgres database).
|
|
155
|
+
- In a running app, the DevTools overlay (Ctrl+Shift+K, Cmd+Shift+K on macOS) inspects operations, merges, and sync status.
|
|
156
|
+
`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/commands/agents/agents-command.ts
|
|
160
|
+
import { access, readFile, writeFile } from "fs/promises";
|
|
161
|
+
import { join } from "path";
|
|
162
|
+
import { defineCommand } from "citty";
|
|
163
|
+
async function fileExists(path) {
|
|
164
|
+
try {
|
|
165
|
+
await access(path);
|
|
166
|
+
return true;
|
|
167
|
+
} catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async function readManifest(root) {
|
|
172
|
+
try {
|
|
173
|
+
return JSON.parse(await readFile(join(root, "package.json"), "utf-8"));
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function resolveFramework(override, manifest) {
|
|
179
|
+
if (override !== void 0 && isAgentsFramework(override)) return override;
|
|
180
|
+
return detectFrameworkFromManifest(manifest) ?? "react";
|
|
181
|
+
}
|
|
182
|
+
var agentsCommand = defineCommand({
|
|
183
|
+
meta: {
|
|
184
|
+
name: "agents-md",
|
|
185
|
+
description: "Write an AGENTS.md with Kora rules for AI coding agents into this project"
|
|
186
|
+
},
|
|
187
|
+
args: {
|
|
188
|
+
force: {
|
|
189
|
+
type: "boolean",
|
|
190
|
+
description: "Overwrite an existing AGENTS.md",
|
|
191
|
+
default: false
|
|
192
|
+
},
|
|
193
|
+
framework: {
|
|
194
|
+
type: "string",
|
|
195
|
+
description: "Override framework detection (react, vue, or svelte)"
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
async run({ args }) {
|
|
199
|
+
const logger = createLogger();
|
|
200
|
+
const projectRoot = await findProjectRoot() ?? process.cwd();
|
|
201
|
+
const manifest = await readManifest(projectRoot);
|
|
202
|
+
const override = typeof args.framework === "string" ? args.framework : void 0;
|
|
203
|
+
if (override !== void 0 && !isAgentsFramework(override)) {
|
|
204
|
+
logger.error(`Unknown framework "${override}". Use one of: react, vue, svelte.`);
|
|
205
|
+
process.exitCode = 1;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const framework = resolveFramework(override, manifest);
|
|
209
|
+
const target = join(projectRoot, "AGENTS.md");
|
|
210
|
+
const force = args.force === true;
|
|
211
|
+
if (await fileExists(target) && !force) {
|
|
212
|
+
logger.warn("AGENTS.md already exists. Re-run with --force to overwrite it.");
|
|
213
|
+
process.exitCode = 1;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
await writeFile(target, generateAgentsMd(framework), "utf-8");
|
|
217
|
+
logger.banner();
|
|
218
|
+
logger.success(`Wrote AGENTS.md (${framework} bindings) to ${target}`);
|
|
219
|
+
logger.step("Commit it so coding agents pick up Kora conventions automatically.");
|
|
220
|
+
logger.blank();
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// src/commands/generate/type-generator.ts
|
|
225
|
+
function generateTypes(schema) {
|
|
226
|
+
const lines = [
|
|
227
|
+
"// Auto-generated by @korajs/cli \u2014 do not edit manually",
|
|
228
|
+
`// Generated from schema version ${String(schema.version)}`,
|
|
229
|
+
""
|
|
230
|
+
];
|
|
231
|
+
const collectionNames = Object.keys(schema.collections);
|
|
232
|
+
if (collectionNames.length === 0) {
|
|
233
|
+
return lines.join("\n");
|
|
234
|
+
}
|
|
235
|
+
for (const [name, collection] of Object.entries(schema.collections)) {
|
|
236
|
+
const pascal = toPascalCase(name);
|
|
237
|
+
const fields = collection.fields;
|
|
238
|
+
lines.push(`export interface ${pascal}Record {`);
|
|
239
|
+
lines.push(" readonly id: string");
|
|
240
|
+
for (const [fieldName, descriptor] of Object.entries(fields)) {
|
|
241
|
+
const tsType = fieldKindToTypeScript(descriptor);
|
|
242
|
+
const optional = !descriptor.required && !descriptor.auto ? "?" : "";
|
|
243
|
+
lines.push(` readonly ${fieldName}${optional}: ${tsType}`);
|
|
244
|
+
}
|
|
245
|
+
lines.push("}");
|
|
246
|
+
lines.push("");
|
|
247
|
+
lines.push(`export interface ${pascal}InsertInput {`);
|
|
248
|
+
for (const [fieldName, descriptor] of Object.entries(fields)) {
|
|
249
|
+
if (descriptor.auto) continue;
|
|
250
|
+
const tsType = fieldKindToTypeScript(descriptor);
|
|
251
|
+
const optional = !descriptor.required || descriptor.defaultValue !== void 0 ? "?" : "";
|
|
252
|
+
lines.push(` ${fieldName}${optional}: ${tsType}`);
|
|
253
|
+
}
|
|
254
|
+
lines.push("}");
|
|
255
|
+
lines.push("");
|
|
256
|
+
lines.push(`export interface ${pascal}UpdateInput {`);
|
|
257
|
+
for (const [fieldName, descriptor] of Object.entries(fields)) {
|
|
258
|
+
if (descriptor.auto) continue;
|
|
259
|
+
const tsType = fieldKindToTypeScript(descriptor);
|
|
260
|
+
lines.push(` ${fieldName}?: ${tsType}`);
|
|
261
|
+
}
|
|
262
|
+
lines.push("}");
|
|
263
|
+
lines.push("");
|
|
264
|
+
}
|
|
265
|
+
return lines.join("\n");
|
|
266
|
+
}
|
|
267
|
+
function fieldKindToTypeScript(descriptor) {
|
|
268
|
+
switch (descriptor.kind) {
|
|
269
|
+
case "string":
|
|
270
|
+
return "string";
|
|
271
|
+
case "number":
|
|
272
|
+
return "number";
|
|
273
|
+
case "boolean":
|
|
274
|
+
return "boolean";
|
|
275
|
+
case "timestamp":
|
|
276
|
+
return "number";
|
|
277
|
+
case "richtext":
|
|
278
|
+
return "string";
|
|
279
|
+
case "enum": {
|
|
280
|
+
if (descriptor.enumValues && descriptor.enumValues.length > 0) {
|
|
281
|
+
return descriptor.enumValues.map((v) => `'${v}'`).join(" | ");
|
|
282
|
+
}
|
|
283
|
+
return "string";
|
|
284
|
+
}
|
|
285
|
+
case "array": {
|
|
286
|
+
const itemType = itemKindToTypeScript(descriptor.itemKind);
|
|
287
|
+
return `Array<${itemType}>`;
|
|
288
|
+
}
|
|
289
|
+
default:
|
|
290
|
+
return "unknown";
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function itemKindToTypeScript(kind) {
|
|
294
|
+
switch (kind) {
|
|
295
|
+
case "string":
|
|
296
|
+
return "string";
|
|
297
|
+
case "number":
|
|
298
|
+
return "number";
|
|
299
|
+
case "boolean":
|
|
300
|
+
return "boolean";
|
|
301
|
+
case "timestamp":
|
|
302
|
+
return "number";
|
|
303
|
+
case "richtext":
|
|
304
|
+
return "string";
|
|
305
|
+
case "enum":
|
|
306
|
+
return "string";
|
|
307
|
+
case "array":
|
|
308
|
+
return "unknown[]";
|
|
309
|
+
default:
|
|
310
|
+
return "unknown";
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function toPascalCase(name) {
|
|
314
|
+
return name.split(/[_-]/).map((part) => {
|
|
315
|
+
if (part.length === 0) return "";
|
|
316
|
+
const first = part[0];
|
|
317
|
+
return first ? first.toUpperCase() + part.slice(1) : "";
|
|
318
|
+
}).join("");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/commands/deploy/adapters/adapter.ts
|
|
322
|
+
var DEPLOY_PLATFORMS = [
|
|
323
|
+
"fly",
|
|
324
|
+
"railway",
|
|
325
|
+
"aws-ecs",
|
|
326
|
+
"aws-lightsail",
|
|
327
|
+
"render",
|
|
328
|
+
"docker",
|
|
329
|
+
"kora-cloud"
|
|
330
|
+
];
|
|
331
|
+
var IMPLEMENTED_DEPLOY_PLATFORMS = ["fly", "railway", "aws-ecs", "aws-lightsail"];
|
|
332
|
+
function isImplementedDeployPlatform(platform) {
|
|
333
|
+
return IMPLEMENTED_DEPLOY_PLATFORMS.includes(platform);
|
|
334
|
+
}
|
|
335
|
+
function isDeployPlatform(value) {
|
|
336
|
+
return DEPLOY_PLATFORMS.includes(value);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/commands/deploy/builder/client-builder.ts
|
|
340
|
+
import { spawn } from "child_process";
|
|
341
|
+
import { existsSync } from "fs";
|
|
342
|
+
import { copyFile, readdir } from "fs/promises";
|
|
343
|
+
import { join as join2, resolve } from "path";
|
|
344
|
+
async function buildClient(options) {
|
|
345
|
+
const viteEntryPoint = await resolveProjectBinaryEntryPoint(options.projectRoot, "vite", "vite");
|
|
346
|
+
if (!viteEntryPoint) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
`Could not find local Vite binary in ${options.projectRoot}. Install dependencies before deploying.`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
const args = [
|
|
352
|
+
viteEntryPoint,
|
|
353
|
+
"build",
|
|
354
|
+
"--outDir",
|
|
355
|
+
options.outDir,
|
|
356
|
+
"--mode",
|
|
357
|
+
options.mode ?? "production"
|
|
358
|
+
];
|
|
359
|
+
await runProcess(process.execPath, args, options.projectRoot);
|
|
360
|
+
await patchSqliteWasmAssets(options.projectRoot, options.outDir);
|
|
361
|
+
return { outDir: options.outDir };
|
|
362
|
+
}
|
|
363
|
+
async function patchSqliteWasmAssets(projectRoot, outDir) {
|
|
364
|
+
const assetsDir = join2(outDir, "assets");
|
|
365
|
+
if (!existsSync(assetsDir)) return;
|
|
366
|
+
const files = await readdir(assetsDir);
|
|
367
|
+
const hashedWasm = files.find((f) => /^sqlite3-.+\.wasm$/.test(f));
|
|
368
|
+
if (hashedWasm && !files.includes("sqlite3.wasm")) {
|
|
369
|
+
await copyFile(join2(assetsDir, hashedWasm), join2(assetsDir, "sqlite3.wasm"));
|
|
370
|
+
}
|
|
371
|
+
if (!files.includes("sqlite3-opfs-async-proxy.js")) {
|
|
372
|
+
const proxyFile = resolve(
|
|
373
|
+
projectRoot,
|
|
374
|
+
"node_modules",
|
|
375
|
+
"@sqlite.org",
|
|
376
|
+
"sqlite-wasm",
|
|
377
|
+
"sqlite-wasm",
|
|
378
|
+
"jswasm",
|
|
379
|
+
"sqlite3-opfs-async-proxy.js"
|
|
380
|
+
);
|
|
381
|
+
if (existsSync(proxyFile)) {
|
|
382
|
+
await copyFile(proxyFile, join2(assetsDir, "sqlite3-opfs-async-proxy.js"));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
async function runProcess(command, args, cwd) {
|
|
387
|
+
await new Promise((resolve2, reject) => {
|
|
388
|
+
const child = spawn(command, args, {
|
|
389
|
+
cwd,
|
|
390
|
+
stdio: "inherit",
|
|
391
|
+
env: process.env
|
|
392
|
+
});
|
|
393
|
+
child.on("error", (error) => {
|
|
394
|
+
reject(error);
|
|
395
|
+
});
|
|
396
|
+
child.on("exit", (code) => {
|
|
397
|
+
if (code === 0) {
|
|
398
|
+
resolve2();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
reject(new Error(`Client build failed with exit code ${String(code ?? "unknown")}.`));
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/commands/deploy/builder/server-bundler.ts
|
|
407
|
+
import { mkdir } from "fs/promises";
|
|
408
|
+
import { access as access2 } from "fs/promises";
|
|
409
|
+
import { join as join3 } from "path";
|
|
410
|
+
import { build } from "esbuild";
|
|
411
|
+
var DEFAULT_ENTRY_CANDIDATES = [
|
|
412
|
+
"server.ts",
|
|
413
|
+
"server.js",
|
|
414
|
+
"src/server.ts",
|
|
415
|
+
"src/server.js"
|
|
416
|
+
];
|
|
417
|
+
async function bundleServer(options) {
|
|
418
|
+
const candidates = options.entryFileCandidates ?? DEFAULT_ENTRY_CANDIDATES;
|
|
419
|
+
const entryFilePath = await resolveServerEntry(options.projectRoot, candidates);
|
|
420
|
+
if (!entryFilePath) {
|
|
421
|
+
throw new Error(
|
|
422
|
+
`Could not find a server entry file in ${options.projectRoot}. Looked for: ${candidates.join(", ")}`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
await mkdir(options.deployDirectory, { recursive: true });
|
|
426
|
+
const outputFilePath = join3(options.deployDirectory, "server-bundled.js");
|
|
427
|
+
await build({
|
|
428
|
+
entryPoints: [entryFilePath],
|
|
429
|
+
outfile: outputFilePath,
|
|
430
|
+
bundle: true,
|
|
431
|
+
platform: "node",
|
|
432
|
+
format: "esm",
|
|
433
|
+
target: ["node20"],
|
|
434
|
+
sourcemap: false,
|
|
435
|
+
logLevel: "silent",
|
|
436
|
+
external: ["better-sqlite3"],
|
|
437
|
+
banner: {
|
|
438
|
+
js: "import { createRequire as __createRequire } from 'module'; const require = __createRequire(import.meta.url);"
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
return {
|
|
442
|
+
entryFilePath,
|
|
443
|
+
outputFilePath
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
async function resolveServerEntry(projectRoot, candidates) {
|
|
447
|
+
for (const candidate of candidates) {
|
|
448
|
+
const fullPath = join3(projectRoot, candidate);
|
|
449
|
+
try {
|
|
450
|
+
await access2(fullPath);
|
|
451
|
+
return fullPath;
|
|
452
|
+
} catch {
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/commands/deploy/artifacts/fly-toml-generator.ts
|
|
459
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
460
|
+
import { join as join4 } from "path";
|
|
461
|
+
var GENERATED_HEADER = "# Generated by kora deploy \u2014 do not edit manually";
|
|
462
|
+
function generateFlyToml(options) {
|
|
463
|
+
const internalPort = options.internalPort ?? 3e3;
|
|
464
|
+
return [
|
|
465
|
+
GENERATED_HEADER,
|
|
466
|
+
"",
|
|
467
|
+
`app = "${options.appName}"`,
|
|
468
|
+
`primary_region = "${options.region}"`,
|
|
469
|
+
"",
|
|
470
|
+
"[build]",
|
|
471
|
+
` dockerfile = "Dockerfile"`,
|
|
472
|
+
"",
|
|
473
|
+
"[http_service]",
|
|
474
|
+
` internal_port = ${String(internalPort)}`,
|
|
475
|
+
" force_https = true",
|
|
476
|
+
' auto_stop_machines = "stop"',
|
|
477
|
+
" auto_start_machines = true",
|
|
478
|
+
" min_machines_running = 0",
|
|
479
|
+
"",
|
|
480
|
+
" [[http_service.checks]]",
|
|
481
|
+
' grace_period = "10s"',
|
|
482
|
+
' interval = "30s"',
|
|
483
|
+
' method = "GET"',
|
|
484
|
+
' timeout = "5s"',
|
|
485
|
+
' path = "/health"',
|
|
486
|
+
""
|
|
487
|
+
].join("\n");
|
|
488
|
+
}
|
|
489
|
+
async function writeFlyTomlArtifact(deployDirectory, options) {
|
|
490
|
+
await mkdir2(deployDirectory, { recursive: true });
|
|
491
|
+
const filePath = join4(deployDirectory, "fly.toml");
|
|
492
|
+
await writeFile2(filePath, generateFlyToml(options), "utf-8");
|
|
493
|
+
return filePath;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/commands/deploy/adapters/fly-adapter.ts
|
|
497
|
+
import { spawn as spawn2 } from "child_process";
|
|
498
|
+
import { join as join5 } from "path";
|
|
499
|
+
var FlyAdapter = class {
|
|
500
|
+
name = "fly";
|
|
501
|
+
logger = createLogger();
|
|
502
|
+
runner;
|
|
503
|
+
commandCandidates;
|
|
504
|
+
flyCommand = null;
|
|
505
|
+
currentContext;
|
|
506
|
+
lastDeploymentId = null;
|
|
507
|
+
constructor(options = {}) {
|
|
508
|
+
this.runner = options.runner ?? new NodeFlyCommandRunner();
|
|
509
|
+
this.commandCandidates = options.commandCandidates ?? DEFAULT_FLY_COMMAND_CANDIDATES;
|
|
510
|
+
this.currentContext = options.context ?? null;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Seeds runtime context for non-provisioning commands.
|
|
514
|
+
*/
|
|
515
|
+
setContext(context) {
|
|
516
|
+
this.currentContext = context;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Checks whether a Fly CLI executable can be resolved.
|
|
520
|
+
*/
|
|
521
|
+
async detect() {
|
|
522
|
+
const command = await this.tryResolveFlyCommand(process.cwd());
|
|
523
|
+
return command !== null;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Ensures Fly CLI is available before deployment.
|
|
527
|
+
*/
|
|
528
|
+
async install() {
|
|
529
|
+
const available = await this.detect();
|
|
530
|
+
if (!available) {
|
|
531
|
+
throw new Error(
|
|
532
|
+
"Fly CLI is required but not installed. Install from https://fly.io/docs/hands-on/install-flyctl/."
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Performs Fly authentication check/login.
|
|
538
|
+
*/
|
|
539
|
+
async authenticate() {
|
|
540
|
+
const projectRoot = this.currentContext?.projectRoot ?? process.cwd();
|
|
541
|
+
const status = await this.runFlyCommand(["auth", "whoami", "--json"], projectRoot, false);
|
|
542
|
+
if (status.exitCode === 0) return;
|
|
543
|
+
const login = await this.runFlyCommand(["auth", "login"], projectRoot, true);
|
|
544
|
+
if (login.exitCode !== 0) {
|
|
545
|
+
throw new Error(`Fly authentication failed: ${normalizeError(login.stderr, login.stdout)}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Provisions app and optionally region-bound resources in Fly.
|
|
550
|
+
*/
|
|
551
|
+
async provision(config) {
|
|
552
|
+
this.currentContext = {
|
|
553
|
+
projectRoot: config.projectRoot,
|
|
554
|
+
appName: config.appName,
|
|
555
|
+
region: config.region ?? "iad"
|
|
556
|
+
};
|
|
557
|
+
const appCreateArgs = ["apps", "create", config.appName];
|
|
558
|
+
const createApp = await this.runFlyCommand(appCreateArgs, config.projectRoot, false);
|
|
559
|
+
if (createApp.exitCode !== 0 && !isAlreadyExistsResponse(createApp.stderr, createApp.stdout)) {
|
|
560
|
+
throw new Error(
|
|
561
|
+
`Fly app provisioning failed for "${config.appName}": ${normalizeError(createApp.stderr, createApp.stdout)}`
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
const secrets = [];
|
|
565
|
+
const portSecret = await this.runFlyCommand(
|
|
566
|
+
["secrets", "set", "PORT=3000", "--app", config.appName],
|
|
567
|
+
config.projectRoot,
|
|
568
|
+
false
|
|
569
|
+
);
|
|
570
|
+
if (portSecret.exitCode === 0) {
|
|
571
|
+
secrets.push("PORT");
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
applicationId: config.appName,
|
|
575
|
+
databaseId: null,
|
|
576
|
+
secretsSet: secrets
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Builds local artifacts consumed by Fly deploy.
|
|
581
|
+
*/
|
|
582
|
+
async build(config) {
|
|
583
|
+
this.currentContext = {
|
|
584
|
+
projectRoot: config.projectRoot,
|
|
585
|
+
appName: config.appName,
|
|
586
|
+
region: config.region
|
|
587
|
+
};
|
|
588
|
+
const deployDirectory = join5(config.projectRoot, ".kora", "deploy");
|
|
589
|
+
await writeFlyTomlArtifact(deployDirectory, {
|
|
590
|
+
appName: config.appName,
|
|
591
|
+
region: config.region ?? "iad"
|
|
592
|
+
});
|
|
593
|
+
await bundleServer({
|
|
594
|
+
projectRoot: config.projectRoot,
|
|
595
|
+
deployDirectory
|
|
596
|
+
});
|
|
597
|
+
const client = config.deployTarget === "sync-server" ? null : await buildClient({
|
|
598
|
+
projectRoot: config.projectRoot,
|
|
599
|
+
outDir: join5(deployDirectory, "dist"),
|
|
600
|
+
mode: "production"
|
|
601
|
+
});
|
|
602
|
+
return {
|
|
603
|
+
clientDirectory: client?.outDir ?? null,
|
|
604
|
+
serverBundlePath: join5(deployDirectory, "server-bundled.js"),
|
|
605
|
+
deployDirectory
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Deploys generated artifacts to Fly and returns live endpoints.
|
|
610
|
+
*/
|
|
611
|
+
async deploy(artifacts) {
|
|
612
|
+
const context = this.requireContext();
|
|
613
|
+
const deploy = await this.runFlyCommand(
|
|
614
|
+
["deploy", "--config", "fly.toml", "--app", context.appName],
|
|
615
|
+
artifacts.deployDirectory,
|
|
616
|
+
true
|
|
617
|
+
);
|
|
618
|
+
if (deploy.exitCode !== 0) {
|
|
619
|
+
throw new Error(`Fly deployment failed: ${normalizeError(deploy.stderr, deploy.stdout)}`);
|
|
620
|
+
}
|
|
621
|
+
const info = await this.runFlyCommand(
|
|
622
|
+
["status", "--app", context.appName, "--json"],
|
|
623
|
+
context.projectRoot,
|
|
624
|
+
false
|
|
625
|
+
);
|
|
626
|
+
const hostname = parseFlyHostname(info.stdout) ?? `${context.appName}.fly.dev`;
|
|
627
|
+
const deploymentId = parseFlyDeploymentId(info.stdout) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
628
|
+
this.lastDeploymentId = deploymentId;
|
|
629
|
+
return {
|
|
630
|
+
deploymentId,
|
|
631
|
+
liveUrl: `https://${hostname}`,
|
|
632
|
+
syncUrl: `wss://${hostname}/kora-sync`
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Rolls back to a deployment version.
|
|
637
|
+
*/
|
|
638
|
+
async rollback(deploymentId) {
|
|
639
|
+
const context = this.requireContext();
|
|
640
|
+
const rollback = await this.runFlyCommand(
|
|
641
|
+
["releases", "revert", deploymentId, "--app", context.appName],
|
|
642
|
+
context.projectRoot,
|
|
643
|
+
true
|
|
644
|
+
);
|
|
645
|
+
if (rollback.exitCode !== 0) {
|
|
646
|
+
throw new Error(`Fly rollback failed: ${normalizeError(rollback.stderr, rollback.stdout)}`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Returns deployment logs as an async iterable.
|
|
651
|
+
*/
|
|
652
|
+
async *logs(options) {
|
|
653
|
+
const context = this.requireContext();
|
|
654
|
+
const args = ["logs", "--app", context.appName, "--no-tail"];
|
|
655
|
+
if (typeof options.since === "string" && options.since.length > 0) {
|
|
656
|
+
args.push("--region", options.since);
|
|
657
|
+
}
|
|
658
|
+
const result = await this.runFlyCommand(args, context.projectRoot, false);
|
|
659
|
+
if (result.exitCode !== 0) {
|
|
660
|
+
throw new Error(`Fly logs failed: ${normalizeError(result.stderr, result.stdout)}`);
|
|
661
|
+
}
|
|
662
|
+
const lines = result.stdout.split(/\r?\n/).filter((line) => line.length > 0);
|
|
663
|
+
for (const line of lines) {
|
|
664
|
+
yield {
|
|
665
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
666
|
+
level: inferLogLevel(line),
|
|
667
|
+
message: line
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Reads current deployment status from Fly.
|
|
673
|
+
*/
|
|
674
|
+
async status() {
|
|
675
|
+
const context = this.requireContext();
|
|
676
|
+
const status = await this.runFlyCommand(
|
|
677
|
+
["status", "--app", context.appName, "--json"],
|
|
678
|
+
context.projectRoot,
|
|
679
|
+
false
|
|
680
|
+
);
|
|
681
|
+
if (status.exitCode !== 0) {
|
|
682
|
+
return {
|
|
683
|
+
state: "failed",
|
|
684
|
+
message: normalizeError(status.stderr, status.stdout)
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
const hostname = parseFlyHostname(status.stdout);
|
|
688
|
+
return {
|
|
689
|
+
state: "healthy",
|
|
690
|
+
message: "Fly deployment is healthy.",
|
|
691
|
+
liveUrl: hostname ? `https://${hostname}` : void 0
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
async runFlyCommand(flyArgs, cwd, inheritOutput) {
|
|
695
|
+
const flyBinary = await this.resolveFlyCommand(cwd);
|
|
696
|
+
if (inheritOutput) {
|
|
697
|
+
this.logger.step(`fly ${flyArgs.join(" ")}`);
|
|
698
|
+
}
|
|
699
|
+
return await this.runner.run(flyBinary, flyArgs, cwd);
|
|
700
|
+
}
|
|
701
|
+
requireContext() {
|
|
702
|
+
if (!this.currentContext) {
|
|
703
|
+
throw new Error("Fly adapter context is not initialized. Run provision() first.");
|
|
704
|
+
}
|
|
705
|
+
return this.currentContext;
|
|
706
|
+
}
|
|
707
|
+
async resolveFlyCommand(cwd) {
|
|
708
|
+
if (this.flyCommand) {
|
|
709
|
+
return this.flyCommand;
|
|
710
|
+
}
|
|
711
|
+
const resolved = await this.tryResolveFlyCommand(cwd);
|
|
712
|
+
if (!resolved) {
|
|
713
|
+
throw new Error("Could not resolve a usable Fly CLI command (tried flyctl, fly).");
|
|
714
|
+
}
|
|
715
|
+
this.flyCommand = resolved;
|
|
716
|
+
return resolved;
|
|
717
|
+
}
|
|
718
|
+
async tryResolveFlyCommand(cwd) {
|
|
719
|
+
for (const command of this.commandCandidates) {
|
|
720
|
+
const versionCheck = await this.runner.run(command, ["version"], cwd);
|
|
721
|
+
if (versionCheck.exitCode === 0) {
|
|
722
|
+
return command;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
var NodeFlyCommandRunner = class {
|
|
729
|
+
async run(command, args, cwd) {
|
|
730
|
+
return await new Promise((resolve2) => {
|
|
731
|
+
const child = spawn2(command, args, {
|
|
732
|
+
cwd,
|
|
733
|
+
env: process.env,
|
|
734
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
735
|
+
});
|
|
736
|
+
let stdout = "";
|
|
737
|
+
let stderr = "";
|
|
738
|
+
child.stdout?.on("data", (chunk) => {
|
|
739
|
+
stdout += chunk.toString("utf-8");
|
|
740
|
+
});
|
|
741
|
+
child.stderr?.on("data", (chunk) => {
|
|
742
|
+
stderr += chunk.toString("utf-8");
|
|
743
|
+
});
|
|
744
|
+
child.on("error", (error) => {
|
|
745
|
+
resolve2({
|
|
746
|
+
exitCode: 1,
|
|
747
|
+
stdout,
|
|
748
|
+
stderr: `${stderr}
|
|
749
|
+
${error.message}`
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
child.on("exit", (code) => {
|
|
753
|
+
resolve2({
|
|
754
|
+
exitCode: code ?? 1,
|
|
755
|
+
stdout: stdout.trim(),
|
|
756
|
+
stderr: stderr.trim()
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
var DEFAULT_FLY_COMMAND_CANDIDATES = ["flyctl", "fly"];
|
|
763
|
+
function parseFlyHostname(rawJson) {
|
|
764
|
+
const parsed = parseJsonRecord(rawJson);
|
|
765
|
+
if (!parsed) return null;
|
|
766
|
+
const hostname = parsed.Hostname;
|
|
767
|
+
if (typeof hostname === "string" && hostname.length > 0) {
|
|
768
|
+
return hostname;
|
|
769
|
+
}
|
|
770
|
+
const hostnames = parsed.Hostnames;
|
|
771
|
+
if (Array.isArray(hostnames)) {
|
|
772
|
+
const first = hostnames.find((item) => typeof item === "string");
|
|
773
|
+
if (typeof first === "string" && first.length > 0) {
|
|
774
|
+
return first;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
function parseFlyDeploymentId(rawJson) {
|
|
780
|
+
const parsed = parseJsonRecord(rawJson);
|
|
781
|
+
if (!parsed) return null;
|
|
782
|
+
const deploymentId = parsed.DeploymentID;
|
|
783
|
+
if (typeof deploymentId === "string" && deploymentId.length > 0) {
|
|
784
|
+
return deploymentId;
|
|
785
|
+
}
|
|
786
|
+
const latestDeployment = parsed.LatestDeployment;
|
|
787
|
+
if (typeof latestDeployment === "object" && latestDeployment !== null) {
|
|
788
|
+
const id = latestDeployment.ID;
|
|
789
|
+
if (typeof id === "string" && id.length > 0) {
|
|
790
|
+
return id;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
function parseJsonRecord(value) {
|
|
796
|
+
try {
|
|
797
|
+
const parsed = JSON.parse(value);
|
|
798
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
799
|
+
return parsed;
|
|
800
|
+
}
|
|
801
|
+
return null;
|
|
802
|
+
} catch {
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function inferLogLevel(line) {
|
|
807
|
+
const normalized = line.toLowerCase();
|
|
808
|
+
if (normalized.includes("error")) return "error";
|
|
809
|
+
if (normalized.includes("warn")) return "warn";
|
|
810
|
+
if (normalized.includes("debug")) return "debug";
|
|
811
|
+
return "info";
|
|
812
|
+
}
|
|
813
|
+
function normalizeError(stderr, stdout) {
|
|
814
|
+
if (stderr.length > 0) return stderr;
|
|
815
|
+
if (stdout.length > 0) return stdout;
|
|
816
|
+
return "unknown fly CLI error";
|
|
817
|
+
}
|
|
818
|
+
function isAlreadyExistsResponse(stderr, stdout) {
|
|
819
|
+
const text = `${stderr}
|
|
820
|
+
${stdout}`.toLowerCase();
|
|
821
|
+
return text.includes("already exists") || text.includes("already been taken");
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// src/commands/deploy/artifacts/railway-json-generator.ts
|
|
825
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
826
|
+
import { join as join6 } from "path";
|
|
827
|
+
var GENERATED_HEADER2 = "Generated by kora deploy - do not edit manually.";
|
|
828
|
+
function generateRailwayJson(options) {
|
|
829
|
+
const file = {
|
|
830
|
+
$schema: "https://railway.app/railway.schema.json",
|
|
831
|
+
build: {
|
|
832
|
+
builder: "DOCKERFILE",
|
|
833
|
+
dockerfilePath: "Dockerfile"
|
|
834
|
+
},
|
|
835
|
+
deploy: {
|
|
836
|
+
startCommand: options.startCommand ?? "node ./server-bundled.js",
|
|
837
|
+
healthcheckPath: options.healthcheckPath ?? "/health",
|
|
838
|
+
restartPolicyType: "on_failure",
|
|
839
|
+
restartPolicyMaxRetries: 3
|
|
840
|
+
},
|
|
841
|
+
metadata: {
|
|
842
|
+
generatedBy: GENERATED_HEADER2,
|
|
843
|
+
appName: options.appName,
|
|
844
|
+
region: options.region
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
return `${JSON.stringify(file, null, 2)}
|
|
848
|
+
`;
|
|
849
|
+
}
|
|
850
|
+
async function writeRailwayJsonArtifact(deployDirectory, options) {
|
|
851
|
+
await mkdir3(deployDirectory, { recursive: true });
|
|
852
|
+
const filePath = join6(deployDirectory, "railway.json");
|
|
853
|
+
await writeFile3(filePath, generateRailwayJson(options), "utf-8");
|
|
854
|
+
return filePath;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// src/commands/deploy/adapters/railway-adapter.ts
|
|
858
|
+
import { spawn as spawn3 } from "child_process";
|
|
859
|
+
import { join as join7 } from "path";
|
|
860
|
+
var RailwayAdapter = class {
|
|
861
|
+
name = "railway";
|
|
862
|
+
logger = createLogger();
|
|
863
|
+
runner;
|
|
864
|
+
commandCandidates;
|
|
865
|
+
railwayCommand = null;
|
|
866
|
+
currentContext;
|
|
867
|
+
lastDeploymentId = null;
|
|
868
|
+
constructor(options = {}) {
|
|
869
|
+
this.runner = options.runner ?? new NodeRailwayCommandRunner();
|
|
870
|
+
this.commandCandidates = options.commandCandidates ?? DEFAULT_RAILWAY_COMMAND_CANDIDATES;
|
|
871
|
+
this.currentContext = options.context ?? null;
|
|
872
|
+
}
|
|
873
|
+
setContext(context) {
|
|
874
|
+
this.currentContext = context;
|
|
875
|
+
}
|
|
876
|
+
async detect() {
|
|
877
|
+
const command = await this.tryResolveRailwayCommand(process.cwd());
|
|
878
|
+
return command !== null;
|
|
879
|
+
}
|
|
880
|
+
async install() {
|
|
881
|
+
const available = await this.detect();
|
|
882
|
+
if (!available) {
|
|
883
|
+
throw new Error(
|
|
884
|
+
"Railway CLI is required but not installed. Install with `npm i -g @railway/cli` or see https://docs.railway.com/guides/cli."
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
async authenticate() {
|
|
889
|
+
const projectRoot = this.currentContext?.projectRoot ?? process.cwd();
|
|
890
|
+
const whoami = await this.runRailwayCommand(["whoami"], projectRoot, false);
|
|
891
|
+
if (whoami.exitCode === 0) return;
|
|
892
|
+
const login = await this.runRailwayCommand(["login"], projectRoot, true);
|
|
893
|
+
if (login.exitCode !== 0) {
|
|
894
|
+
throw new Error(
|
|
895
|
+
`Railway authentication failed: ${normalizeError2(login.stderr, login.stdout)}`
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
async provision(config) {
|
|
900
|
+
this.currentContext = {
|
|
901
|
+
projectRoot: config.projectRoot,
|
|
902
|
+
appName: config.appName,
|
|
903
|
+
region: config.region
|
|
904
|
+
};
|
|
905
|
+
const init = await this.runRailwayCommand(
|
|
906
|
+
["init", "--name", config.appName, "--confirm"],
|
|
907
|
+
config.projectRoot,
|
|
908
|
+
false
|
|
909
|
+
);
|
|
910
|
+
if (init.exitCode !== 0 && !isAlreadyExistsResponse2(init.stderr, init.stdout)) {
|
|
911
|
+
throw new Error(
|
|
912
|
+
`Railway project provisioning failed for "${config.appName}": ${normalizeError2(init.stderr, init.stdout)}`
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
const link = await this.runRailwayCommand(
|
|
916
|
+
["link", "--project", config.appName, "--environment", config.environment, "--yes"],
|
|
917
|
+
config.projectRoot,
|
|
918
|
+
false
|
|
919
|
+
);
|
|
920
|
+
if (link.exitCode !== 0 && !isAlreadyExistsResponse2(link.stderr, link.stdout)) {
|
|
921
|
+
throw new Error(`Railway link failed: ${normalizeError2(link.stderr, link.stdout)}`);
|
|
922
|
+
}
|
|
923
|
+
const vars = [];
|
|
924
|
+
const setPort = await this.runRailwayCommand(
|
|
925
|
+
["variables", "set", "PORT=3000", "--yes"],
|
|
926
|
+
config.projectRoot,
|
|
927
|
+
false
|
|
928
|
+
);
|
|
929
|
+
if (setPort.exitCode === 0) {
|
|
930
|
+
vars.push("PORT");
|
|
931
|
+
}
|
|
932
|
+
return {
|
|
933
|
+
applicationId: config.appName,
|
|
934
|
+
databaseId: null,
|
|
935
|
+
secretsSet: vars
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
async build(config) {
|
|
939
|
+
this.currentContext = {
|
|
940
|
+
projectRoot: config.projectRoot,
|
|
941
|
+
appName: config.appName,
|
|
942
|
+
region: config.region
|
|
943
|
+
};
|
|
944
|
+
const deployDirectory = join7(config.projectRoot, ".kora", "deploy");
|
|
945
|
+
await writeRailwayJsonArtifact(deployDirectory, {
|
|
946
|
+
appName: config.appName,
|
|
947
|
+
environment: config.environment
|
|
948
|
+
});
|
|
949
|
+
await bundleServer({
|
|
950
|
+
projectRoot: config.projectRoot,
|
|
951
|
+
deployDirectory
|
|
952
|
+
});
|
|
953
|
+
const client = config.deployTarget === "sync-server" ? null : await buildClient({
|
|
954
|
+
projectRoot: config.projectRoot,
|
|
955
|
+
outDir: join7(deployDirectory, "dist"),
|
|
956
|
+
mode: "production"
|
|
957
|
+
});
|
|
958
|
+
return {
|
|
959
|
+
clientDirectory: client?.outDir ?? null,
|
|
960
|
+
serverBundlePath: join7(deployDirectory, "server-bundled.js"),
|
|
961
|
+
deployDirectory
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
async deploy(artifacts) {
|
|
965
|
+
const context = this.requireContext();
|
|
966
|
+
const up = await this.runRailwayCommand(["up", "--yes"], artifacts.deployDirectory, true);
|
|
967
|
+
if (up.exitCode !== 0) {
|
|
968
|
+
throw new Error(`Railway deployment failed: ${normalizeError2(up.stderr, up.stdout)}`);
|
|
969
|
+
}
|
|
970
|
+
const status = await this.runRailwayCommand(["status", "--json"], context.projectRoot, false);
|
|
971
|
+
const deploymentId = parseRailwayDeploymentId(status.stdout) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
972
|
+
const liveUrl = parseRailwayUrl(status.stdout) ?? `https://${context.appName}.up.railway.app`;
|
|
973
|
+
this.lastDeploymentId = deploymentId;
|
|
974
|
+
return {
|
|
975
|
+
deploymentId,
|
|
976
|
+
liveUrl,
|
|
977
|
+
syncUrl: toSyncUrl(liveUrl)
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
async rollback(deploymentId) {
|
|
981
|
+
const context = this.requireContext();
|
|
982
|
+
const rollback = await this.runRailwayCommand(
|
|
983
|
+
["redeploy", "--deployment", deploymentId, "--yes"],
|
|
984
|
+
context.projectRoot,
|
|
985
|
+
true
|
|
986
|
+
);
|
|
987
|
+
if (rollback.exitCode !== 0) {
|
|
988
|
+
throw new Error(
|
|
989
|
+
`Railway rollback failed: ${normalizeError2(rollback.stderr, rollback.stdout)}`
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
async *logs(options) {
|
|
994
|
+
const context = this.requireContext();
|
|
995
|
+
const args = ["logs"];
|
|
996
|
+
if (typeof options.tail === "number" && options.tail > 0) {
|
|
997
|
+
args.push("--lines", String(options.tail));
|
|
998
|
+
}
|
|
999
|
+
const result = await this.runRailwayCommand(args, context.projectRoot, false);
|
|
1000
|
+
if (result.exitCode !== 0) {
|
|
1001
|
+
throw new Error(`Railway logs failed: ${normalizeError2(result.stderr, result.stdout)}`);
|
|
1002
|
+
}
|
|
1003
|
+
const lines = result.stdout.split(/\r?\n/).filter((line) => line.length > 0);
|
|
1004
|
+
for (const line of lines) {
|
|
1005
|
+
yield {
|
|
1006
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1007
|
+
level: inferLogLevel2(line),
|
|
1008
|
+
message: line
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
async status() {
|
|
1013
|
+
const context = this.requireContext();
|
|
1014
|
+
const status = await this.runRailwayCommand(["status", "--json"], context.projectRoot, false);
|
|
1015
|
+
if (status.exitCode !== 0) {
|
|
1016
|
+
return {
|
|
1017
|
+
state: "failed",
|
|
1018
|
+
message: normalizeError2(status.stderr, status.stdout)
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
const liveUrl = parseRailwayUrl(status.stdout);
|
|
1022
|
+
return {
|
|
1023
|
+
state: "healthy",
|
|
1024
|
+
message: "Railway deployment is healthy.",
|
|
1025
|
+
liveUrl: liveUrl ?? void 0
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
async runRailwayCommand(railwayArgs, cwd, inheritOutput) {
|
|
1029
|
+
const command = await this.resolveRailwayCommand(cwd);
|
|
1030
|
+
if (inheritOutput) {
|
|
1031
|
+
this.logger.step(`railway ${railwayArgs.join(" ")}`);
|
|
1032
|
+
}
|
|
1033
|
+
return await this.runner.run(command, railwayArgs, cwd);
|
|
1034
|
+
}
|
|
1035
|
+
requireContext() {
|
|
1036
|
+
if (!this.currentContext) {
|
|
1037
|
+
throw new Error("Railway adapter context is not initialized. Run provision() first.");
|
|
1038
|
+
}
|
|
1039
|
+
return this.currentContext;
|
|
1040
|
+
}
|
|
1041
|
+
async resolveRailwayCommand(cwd) {
|
|
1042
|
+
if (this.railwayCommand) {
|
|
1043
|
+
return this.railwayCommand;
|
|
1044
|
+
}
|
|
1045
|
+
const resolved = await this.tryResolveRailwayCommand(cwd);
|
|
1046
|
+
if (!resolved) {
|
|
1047
|
+
throw new Error("Could not resolve a usable Railway CLI command (tried railway).");
|
|
1048
|
+
}
|
|
1049
|
+
this.railwayCommand = resolved;
|
|
1050
|
+
return resolved;
|
|
1051
|
+
}
|
|
1052
|
+
async tryResolveRailwayCommand(cwd) {
|
|
1053
|
+
for (const command of this.commandCandidates) {
|
|
1054
|
+
const version = await this.runner.run(command, ["--version"], cwd);
|
|
1055
|
+
if (version.exitCode === 0) {
|
|
1056
|
+
return command;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return null;
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
var NodeRailwayCommandRunner = class {
|
|
1063
|
+
async run(command, args, cwd) {
|
|
1064
|
+
return await new Promise((resolve2) => {
|
|
1065
|
+
const child = spawn3(command, args, {
|
|
1066
|
+
cwd,
|
|
1067
|
+
env: process.env,
|
|
1068
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1069
|
+
});
|
|
1070
|
+
let stdout = "";
|
|
1071
|
+
let stderr = "";
|
|
1072
|
+
child.stdout?.on("data", (chunk) => {
|
|
1073
|
+
stdout += chunk.toString("utf-8");
|
|
1074
|
+
});
|
|
1075
|
+
child.stderr?.on("data", (chunk) => {
|
|
1076
|
+
stderr += chunk.toString("utf-8");
|
|
1077
|
+
});
|
|
1078
|
+
child.on("error", (error) => {
|
|
1079
|
+
resolve2({
|
|
1080
|
+
exitCode: 1,
|
|
1081
|
+
stdout,
|
|
1082
|
+
stderr: `${stderr}
|
|
1083
|
+
${error.message}`
|
|
1084
|
+
});
|
|
1085
|
+
});
|
|
1086
|
+
child.on("exit", (code) => {
|
|
1087
|
+
resolve2({
|
|
1088
|
+
exitCode: code ?? 1,
|
|
1089
|
+
stdout: stdout.trim(),
|
|
1090
|
+
stderr: stderr.trim()
|
|
1091
|
+
});
|
|
1092
|
+
});
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
var DEFAULT_RAILWAY_COMMAND_CANDIDATES = ["railway"];
|
|
1097
|
+
function parseRailwayUrl(rawJson) {
|
|
1098
|
+
const record = parseJsonRecord2(rawJson);
|
|
1099
|
+
if (!record) return null;
|
|
1100
|
+
const url = record.url;
|
|
1101
|
+
if (typeof url === "string" && url.length > 0) {
|
|
1102
|
+
return ensureHttpsUrl(url);
|
|
1103
|
+
}
|
|
1104
|
+
const service = record.service;
|
|
1105
|
+
if (typeof service === "object" && service !== null) {
|
|
1106
|
+
const domain = service.domain;
|
|
1107
|
+
if (typeof domain === "string" && domain.length > 0) {
|
|
1108
|
+
return ensureHttpsUrl(domain);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return null;
|
|
1112
|
+
}
|
|
1113
|
+
function parseRailwayDeploymentId(rawJson) {
|
|
1114
|
+
const record = parseJsonRecord2(rawJson);
|
|
1115
|
+
if (!record) return null;
|
|
1116
|
+
const deploymentId = record.deploymentId;
|
|
1117
|
+
if (typeof deploymentId === "string" && deploymentId.length > 0) {
|
|
1118
|
+
return deploymentId;
|
|
1119
|
+
}
|
|
1120
|
+
const deployment = record.deployment;
|
|
1121
|
+
if (typeof deployment === "object" && deployment !== null) {
|
|
1122
|
+
const id = deployment.id;
|
|
1123
|
+
if (typeof id === "string" && id.length > 0) {
|
|
1124
|
+
return id;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return null;
|
|
1128
|
+
}
|
|
1129
|
+
function parseJsonRecord2(value) {
|
|
1130
|
+
try {
|
|
1131
|
+
const parsed = JSON.parse(value);
|
|
1132
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
1133
|
+
return parsed;
|
|
1134
|
+
}
|
|
1135
|
+
return null;
|
|
1136
|
+
} catch {
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
function toSyncUrl(liveUrl) {
|
|
1141
|
+
try {
|
|
1142
|
+
const url = new URL(liveUrl);
|
|
1143
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
1144
|
+
url.pathname = "/kora-sync";
|
|
1145
|
+
return url.toString().replace(/\/$/, "");
|
|
1146
|
+
} catch {
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function ensureHttpsUrl(value) {
|
|
1151
|
+
if (value.startsWith("http://") || value.startsWith("https://")) {
|
|
1152
|
+
return value;
|
|
1153
|
+
}
|
|
1154
|
+
return `https://${value}`;
|
|
1155
|
+
}
|
|
1156
|
+
function inferLogLevel2(line) {
|
|
1157
|
+
const normalized = line.toLowerCase();
|
|
1158
|
+
if (normalized.includes("error")) return "error";
|
|
1159
|
+
if (normalized.includes("warn")) return "warn";
|
|
1160
|
+
if (normalized.includes("debug")) return "debug";
|
|
1161
|
+
return "info";
|
|
1162
|
+
}
|
|
1163
|
+
function normalizeError2(stderr, stdout) {
|
|
1164
|
+
if (stderr.length > 0) return stderr;
|
|
1165
|
+
if (stdout.length > 0) return stdout;
|
|
1166
|
+
return "unknown railway CLI error";
|
|
1167
|
+
}
|
|
1168
|
+
function isAlreadyExistsResponse2(stderr, stdout) {
|
|
1169
|
+
const text = `${stderr}
|
|
1170
|
+
${stdout}`.toLowerCase();
|
|
1171
|
+
return text.includes("already exists");
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// src/commands/deploy/adapters/stub-adapter.ts
|
|
1175
|
+
var StubDeployAdapter = class {
|
|
1176
|
+
name;
|
|
1177
|
+
contextLabel;
|
|
1178
|
+
constructor(platform) {
|
|
1179
|
+
this.name = platform;
|
|
1180
|
+
this.contextLabel = `Deploy adapter "${platform}" is not implemented yet.`;
|
|
1181
|
+
}
|
|
1182
|
+
setContext(_context) {
|
|
1183
|
+
}
|
|
1184
|
+
async detect() {
|
|
1185
|
+
return false;
|
|
1186
|
+
}
|
|
1187
|
+
async install() {
|
|
1188
|
+
throw this.notImplementedError();
|
|
1189
|
+
}
|
|
1190
|
+
async authenticate() {
|
|
1191
|
+
throw this.notImplementedError();
|
|
1192
|
+
}
|
|
1193
|
+
async provision(_config) {
|
|
1194
|
+
throw this.notImplementedError();
|
|
1195
|
+
}
|
|
1196
|
+
async build(_config) {
|
|
1197
|
+
throw this.notImplementedError();
|
|
1198
|
+
}
|
|
1199
|
+
async deploy(_artifacts) {
|
|
1200
|
+
throw this.notImplementedError();
|
|
1201
|
+
}
|
|
1202
|
+
async rollback(_deploymentId) {
|
|
1203
|
+
throw this.notImplementedError();
|
|
1204
|
+
}
|
|
1205
|
+
logs(_options) {
|
|
1206
|
+
throw this.notImplementedError();
|
|
1207
|
+
}
|
|
1208
|
+
async status() {
|
|
1209
|
+
return {
|
|
1210
|
+
state: "unknown",
|
|
1211
|
+
message: this.notImplementedMessage()
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
notImplementedError() {
|
|
1215
|
+
return new Error(this.notImplementedMessage());
|
|
1216
|
+
}
|
|
1217
|
+
notImplementedMessage() {
|
|
1218
|
+
return `${this.contextLabel} Start with --platform fly for now.`;
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
// src/commands/deploy/adapters/aws-ecs-adapter.ts
|
|
1223
|
+
import { spawn as spawn4 } from "child_process";
|
|
1224
|
+
import { join as join8 } from "path";
|
|
1225
|
+
var AwsEcsAdapter = class {
|
|
1226
|
+
name = "aws-ecs";
|
|
1227
|
+
logger = createLogger();
|
|
1228
|
+
runner;
|
|
1229
|
+
currentContext;
|
|
1230
|
+
constructor(options = {}) {
|
|
1231
|
+
this.runner = options.runner ?? new NodeAwsCommandRunner();
|
|
1232
|
+
this.currentContext = options.context ?? null;
|
|
1233
|
+
}
|
|
1234
|
+
setContext(context) {
|
|
1235
|
+
this.currentContext = context;
|
|
1236
|
+
}
|
|
1237
|
+
async detect() {
|
|
1238
|
+
const result = await this.runner.run("aws", ["--version"], process.cwd());
|
|
1239
|
+
return result.exitCode === 0;
|
|
1240
|
+
}
|
|
1241
|
+
async install() {
|
|
1242
|
+
const available = await this.detect();
|
|
1243
|
+
if (!available) {
|
|
1244
|
+
throw new Error(
|
|
1245
|
+
"AWS CLI is required but not installed. Install from https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html"
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
async authenticate() {
|
|
1250
|
+
const cwd = this.currentContext?.projectRoot ?? process.cwd();
|
|
1251
|
+
const result = await this.runner.run("aws", ["sts", "get-caller-identity"], cwd);
|
|
1252
|
+
if (result.exitCode !== 0) {
|
|
1253
|
+
throw new Error(
|
|
1254
|
+
"AWS CLI is not authenticated. Run `aws configure` or set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables."
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
async provision(config) {
|
|
1259
|
+
this.currentContext = {
|
|
1260
|
+
projectRoot: config.projectRoot,
|
|
1261
|
+
appName: config.appName,
|
|
1262
|
+
region: config.region ?? "us-east-1"
|
|
1263
|
+
};
|
|
1264
|
+
const region = config.region ?? "us-east-1";
|
|
1265
|
+
const repoName = `kora/${config.appName}`;
|
|
1266
|
+
const createRepo = await this.runner.run(
|
|
1267
|
+
"aws",
|
|
1268
|
+
[
|
|
1269
|
+
"ecr",
|
|
1270
|
+
"create-repository",
|
|
1271
|
+
"--repository-name",
|
|
1272
|
+
repoName,
|
|
1273
|
+
"--region",
|
|
1274
|
+
region,
|
|
1275
|
+
"--image-scanning-configuration",
|
|
1276
|
+
"scanOnPush=true"
|
|
1277
|
+
],
|
|
1278
|
+
config.projectRoot
|
|
1279
|
+
);
|
|
1280
|
+
if (createRepo.exitCode !== 0 && !createRepo.stderr.includes("RepositoryAlreadyExistsException")) {
|
|
1281
|
+
throw new Error(`Failed to create ECR repository: ${createRepo.stderr}`);
|
|
1282
|
+
}
|
|
1283
|
+
const identity = await this.runner.run(
|
|
1284
|
+
"aws",
|
|
1285
|
+
["sts", "get-caller-identity", "--query", "Account", "--output", "text"],
|
|
1286
|
+
config.projectRoot
|
|
1287
|
+
);
|
|
1288
|
+
const accountId = identity.stdout.trim();
|
|
1289
|
+
await this.runner.run(
|
|
1290
|
+
"aws",
|
|
1291
|
+
["ecs", "create-cluster", "--cluster-name", config.appName, "--region", region],
|
|
1292
|
+
config.projectRoot
|
|
1293
|
+
);
|
|
1294
|
+
await this.runner.run(
|
|
1295
|
+
"aws",
|
|
1296
|
+
[
|
|
1297
|
+
"logs",
|
|
1298
|
+
"create-log-group",
|
|
1299
|
+
"--log-group-name",
|
|
1300
|
+
`/ecs/${config.appName}`,
|
|
1301
|
+
"--region",
|
|
1302
|
+
region
|
|
1303
|
+
],
|
|
1304
|
+
config.projectRoot
|
|
1305
|
+
);
|
|
1306
|
+
return {
|
|
1307
|
+
applicationId: `${accountId}.dkr.ecr.${region}.amazonaws.com/${repoName}`,
|
|
1308
|
+
databaseId: null,
|
|
1309
|
+
secretsSet: ["PORT"]
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
async build(config) {
|
|
1313
|
+
this.currentContext = {
|
|
1314
|
+
projectRoot: config.projectRoot,
|
|
1315
|
+
appName: config.appName,
|
|
1316
|
+
region: config.region
|
|
1317
|
+
};
|
|
1318
|
+
const deployDirectory = join8(config.projectRoot, ".kora", "deploy");
|
|
1319
|
+
await bundleServer({
|
|
1320
|
+
projectRoot: config.projectRoot,
|
|
1321
|
+
deployDirectory
|
|
1322
|
+
});
|
|
1323
|
+
const client = config.deployTarget === "sync-server" ? null : await buildClient({
|
|
1324
|
+
projectRoot: config.projectRoot,
|
|
1325
|
+
outDir: join8(deployDirectory, "dist"),
|
|
1326
|
+
mode: "production"
|
|
1327
|
+
});
|
|
1328
|
+
return {
|
|
1329
|
+
clientDirectory: client?.outDir ?? null,
|
|
1330
|
+
serverBundlePath: join8(deployDirectory, "server-bundled.js"),
|
|
1331
|
+
deployDirectory
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
async deploy(artifacts) {
|
|
1335
|
+
const context = this.requireContext();
|
|
1336
|
+
const region = context.region ?? "us-east-1";
|
|
1337
|
+
const repoName = `kora/${context.appName}`;
|
|
1338
|
+
const loginPassword = await this.runner.run(
|
|
1339
|
+
"aws",
|
|
1340
|
+
["ecr", "get-login-password", "--region", region],
|
|
1341
|
+
context.projectRoot
|
|
1342
|
+
);
|
|
1343
|
+
if (loginPassword.exitCode !== 0) {
|
|
1344
|
+
throw new Error(`ECR login failed: ${loginPassword.stderr}`);
|
|
1345
|
+
}
|
|
1346
|
+
const identity = await this.runner.run(
|
|
1347
|
+
"aws",
|
|
1348
|
+
["sts", "get-caller-identity", "--query", "Account", "--output", "text"],
|
|
1349
|
+
context.projectRoot
|
|
1350
|
+
);
|
|
1351
|
+
const accountId = identity.stdout.trim();
|
|
1352
|
+
const ecrUri = `${accountId}.dkr.ecr.${region}.amazonaws.com`;
|
|
1353
|
+
const imageUri = `${ecrUri}/${repoName}:latest`;
|
|
1354
|
+
const dockerLogin = await this.runner.run(
|
|
1355
|
+
"docker",
|
|
1356
|
+
["login", "--username", "AWS", "--password-stdin", ecrUri],
|
|
1357
|
+
artifacts.deployDirectory
|
|
1358
|
+
);
|
|
1359
|
+
this.logger.step("Building Docker image...");
|
|
1360
|
+
const dockerBuild = await this.runner.run(
|
|
1361
|
+
"docker",
|
|
1362
|
+
["build", "--platform", "linux/amd64", "-t", imageUri, "."],
|
|
1363
|
+
artifacts.deployDirectory
|
|
1364
|
+
);
|
|
1365
|
+
if (dockerBuild.exitCode !== 0) {
|
|
1366
|
+
throw new Error(`Docker build failed: ${dockerBuild.stderr}`);
|
|
1367
|
+
}
|
|
1368
|
+
this.logger.step("Pushing image to ECR...");
|
|
1369
|
+
const dockerPush = await this.runner.run(
|
|
1370
|
+
"docker",
|
|
1371
|
+
["push", imageUri],
|
|
1372
|
+
artifacts.deployDirectory
|
|
1373
|
+
);
|
|
1374
|
+
if (dockerPush.exitCode !== 0) {
|
|
1375
|
+
throw new Error(`Docker push failed: ${dockerPush.stderr}`);
|
|
1376
|
+
}
|
|
1377
|
+
const taskDef = JSON.stringify({
|
|
1378
|
+
family: context.appName,
|
|
1379
|
+
networkMode: "awsvpc",
|
|
1380
|
+
requiresCompatibilities: ["FARGATE"],
|
|
1381
|
+
cpu: "256",
|
|
1382
|
+
memory: "512",
|
|
1383
|
+
executionRoleArn: `arn:aws:iam::${accountId}:role/ecsTaskExecutionRole`,
|
|
1384
|
+
containerDefinitions: [
|
|
1385
|
+
{
|
|
1386
|
+
name: context.appName,
|
|
1387
|
+
image: imageUri,
|
|
1388
|
+
essential: true,
|
|
1389
|
+
portMappings: [{ containerPort: 3001, protocol: "tcp" }],
|
|
1390
|
+
logConfiguration: {
|
|
1391
|
+
logDriver: "awslogs",
|
|
1392
|
+
options: {
|
|
1393
|
+
"awslogs-group": `/ecs/${context.appName}`,
|
|
1394
|
+
"awslogs-region": region,
|
|
1395
|
+
"awslogs-stream-prefix": "ecs"
|
|
1396
|
+
}
|
|
1397
|
+
},
|
|
1398
|
+
healthCheck: {
|
|
1399
|
+
command: ["CMD-SHELL", "curl -f http://localhost:3001/health || exit 1"],
|
|
1400
|
+
interval: 30,
|
|
1401
|
+
timeout: 5,
|
|
1402
|
+
retries: 3,
|
|
1403
|
+
startPeriod: 60
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
]
|
|
1407
|
+
});
|
|
1408
|
+
const registerTask = await this.runner.run(
|
|
1409
|
+
"aws",
|
|
1410
|
+
["ecs", "register-task-definition", "--cli-input-json", taskDef, "--region", region],
|
|
1411
|
+
context.projectRoot
|
|
1412
|
+
);
|
|
1413
|
+
if (registerTask.exitCode !== 0) {
|
|
1414
|
+
throw new Error(`Task definition registration failed: ${registerTask.stderr}`);
|
|
1415
|
+
}
|
|
1416
|
+
const updateService = await this.runner.run(
|
|
1417
|
+
"aws",
|
|
1418
|
+
[
|
|
1419
|
+
"ecs",
|
|
1420
|
+
"update-service",
|
|
1421
|
+
"--cluster",
|
|
1422
|
+
context.appName,
|
|
1423
|
+
"--service",
|
|
1424
|
+
context.appName,
|
|
1425
|
+
"--task-definition",
|
|
1426
|
+
context.appName,
|
|
1427
|
+
"--force-new-deployment",
|
|
1428
|
+
"--region",
|
|
1429
|
+
region
|
|
1430
|
+
],
|
|
1431
|
+
context.projectRoot
|
|
1432
|
+
);
|
|
1433
|
+
const deploymentId = (/* @__PURE__ */ new Date()).toISOString();
|
|
1434
|
+
if (updateService.exitCode !== 0) {
|
|
1435
|
+
this.logger.step("Service not found, creating new service...");
|
|
1436
|
+
this.logger.step(
|
|
1437
|
+
`Task definition registered. Create the ECS service with:
|
|
1438
|
+
aws ecs create-service \\
|
|
1439
|
+
--cluster ${context.appName} \\
|
|
1440
|
+
--service-name ${context.appName} \\
|
|
1441
|
+
--task-definition ${context.appName} \\
|
|
1442
|
+
--desired-count 1 \\
|
|
1443
|
+
--launch-type FARGATE \\
|
|
1444
|
+
--network-configuration "awsvpcConfiguration={subnets=[<subnet-id>],securityGroups=[<sg-id>],assignPublicIp=ENABLED}" \\
|
|
1445
|
+
--region ${region}`
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
return {
|
|
1449
|
+
deploymentId,
|
|
1450
|
+
liveUrl: `https://${context.appName}.${region}.amazonaws.com`,
|
|
1451
|
+
syncUrl: `wss://${context.appName}.${region}.amazonaws.com/kora-sync`
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
async rollback(deploymentId) {
|
|
1455
|
+
const context = this.requireContext();
|
|
1456
|
+
const region = context.region ?? "us-east-1";
|
|
1457
|
+
const result = await this.runner.run(
|
|
1458
|
+
"aws",
|
|
1459
|
+
[
|
|
1460
|
+
"ecs",
|
|
1461
|
+
"update-service",
|
|
1462
|
+
"--cluster",
|
|
1463
|
+
context.appName,
|
|
1464
|
+
"--service",
|
|
1465
|
+
context.appName,
|
|
1466
|
+
"--task-definition",
|
|
1467
|
+
`${context.appName}:${deploymentId}`,
|
|
1468
|
+
"--force-new-deployment",
|
|
1469
|
+
"--region",
|
|
1470
|
+
region
|
|
1471
|
+
],
|
|
1472
|
+
context.projectRoot
|
|
1473
|
+
);
|
|
1474
|
+
if (result.exitCode !== 0) {
|
|
1475
|
+
throw new Error(`ECS rollback failed: ${result.stderr}`);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
async *logs(options) {
|
|
1479
|
+
const context = this.requireContext();
|
|
1480
|
+
const region = context.region ?? "us-east-1";
|
|
1481
|
+
const args = [
|
|
1482
|
+
"logs",
|
|
1483
|
+
"get-log-events",
|
|
1484
|
+
"--log-group-name",
|
|
1485
|
+
`/ecs/${context.appName}`,
|
|
1486
|
+
"--log-stream-name",
|
|
1487
|
+
"ecs/latest",
|
|
1488
|
+
"--region",
|
|
1489
|
+
region
|
|
1490
|
+
];
|
|
1491
|
+
if (options.tail) {
|
|
1492
|
+
args.push("--limit", String(options.tail));
|
|
1493
|
+
}
|
|
1494
|
+
const result = await this.runner.run("aws", args, context.projectRoot);
|
|
1495
|
+
if (result.exitCode !== 0) {
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
try {
|
|
1499
|
+
const parsed = JSON.parse(result.stdout);
|
|
1500
|
+
for (const event of parsed.events ?? []) {
|
|
1501
|
+
yield {
|
|
1502
|
+
timestamp: new Date(event.timestamp).toISOString(),
|
|
1503
|
+
level: inferLogLevel3(event.message),
|
|
1504
|
+
message: event.message
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
} catch {
|
|
1508
|
+
for (const line of result.stdout.split("\n").filter(Boolean)) {
|
|
1509
|
+
yield { timestamp: (/* @__PURE__ */ new Date()).toISOString(), level: "info", message: line };
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
async status() {
|
|
1514
|
+
const context = this.requireContext();
|
|
1515
|
+
const region = context.region ?? "us-east-1";
|
|
1516
|
+
const result = await this.runner.run(
|
|
1517
|
+
"aws",
|
|
1518
|
+
[
|
|
1519
|
+
"ecs",
|
|
1520
|
+
"describe-services",
|
|
1521
|
+
"--cluster",
|
|
1522
|
+
context.appName,
|
|
1523
|
+
"--services",
|
|
1524
|
+
context.appName,
|
|
1525
|
+
"--region",
|
|
1526
|
+
region
|
|
1527
|
+
],
|
|
1528
|
+
context.projectRoot
|
|
1529
|
+
);
|
|
1530
|
+
if (result.exitCode !== 0) {
|
|
1531
|
+
return { state: "failed", message: result.stderr };
|
|
1532
|
+
}
|
|
1533
|
+
try {
|
|
1534
|
+
const parsed = JSON.parse(result.stdout);
|
|
1535
|
+
const service = parsed.services?.[0];
|
|
1536
|
+
if (!service) {
|
|
1537
|
+
return { state: "unknown", message: "Service not found" };
|
|
1538
|
+
}
|
|
1539
|
+
if (service.status === "ACTIVE" && service.runningCount > 0) {
|
|
1540
|
+
return {
|
|
1541
|
+
state: "healthy",
|
|
1542
|
+
message: `Running ${service.runningCount}/${service.desiredCount} tasks`
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
return {
|
|
1546
|
+
state: service.runningCount === 0 ? "pending" : "healthy",
|
|
1547
|
+
message: `Status: ${service.status}, running: ${service.runningCount}/${service.desiredCount}`
|
|
1548
|
+
};
|
|
1549
|
+
} catch {
|
|
1550
|
+
return { state: "unknown", message: "Could not parse service status" };
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
requireContext() {
|
|
1554
|
+
if (!this.currentContext) {
|
|
1555
|
+
throw new Error("AWS ECS adapter context is not initialized. Run provision() first.");
|
|
1556
|
+
}
|
|
1557
|
+
return this.currentContext;
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
var NodeAwsCommandRunner = class {
|
|
1561
|
+
async run(command, args, cwd) {
|
|
1562
|
+
return new Promise((resolve2) => {
|
|
1563
|
+
const child = spawn4(command, args, {
|
|
1564
|
+
cwd,
|
|
1565
|
+
env: process.env,
|
|
1566
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1567
|
+
});
|
|
1568
|
+
let stdout = "";
|
|
1569
|
+
let stderr = "";
|
|
1570
|
+
child.stdout?.on("data", (chunk) => {
|
|
1571
|
+
stdout += chunk.toString("utf-8");
|
|
1572
|
+
});
|
|
1573
|
+
child.stderr?.on("data", (chunk) => {
|
|
1574
|
+
stderr += chunk.toString("utf-8");
|
|
1575
|
+
});
|
|
1576
|
+
child.on("error", (error) => {
|
|
1577
|
+
resolve2({ exitCode: 1, stdout, stderr: `${stderr}
|
|
1578
|
+
${error.message}` });
|
|
1579
|
+
});
|
|
1580
|
+
child.on("exit", (code) => {
|
|
1581
|
+
resolve2({ exitCode: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() });
|
|
1582
|
+
});
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
function inferLogLevel3(line) {
|
|
1587
|
+
const normalized = line.toLowerCase();
|
|
1588
|
+
if (normalized.includes("error")) return "error";
|
|
1589
|
+
if (normalized.includes("warn")) return "warn";
|
|
1590
|
+
if (normalized.includes("debug")) return "debug";
|
|
1591
|
+
return "info";
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// src/commands/deploy/adapters/aws-lightsail-adapter.ts
|
|
1595
|
+
import { spawn as spawn5 } from "child_process";
|
|
1596
|
+
import { join as join9 } from "path";
|
|
1597
|
+
var AwsLightsailAdapter = class {
|
|
1598
|
+
name = "aws-lightsail";
|
|
1599
|
+
logger = createLogger();
|
|
1600
|
+
runner;
|
|
1601
|
+
currentContext;
|
|
1602
|
+
constructor(options = {}) {
|
|
1603
|
+
this.runner = options.runner ?? new NodeAwsLightsailCommandRunner();
|
|
1604
|
+
this.currentContext = options.context ?? null;
|
|
1605
|
+
}
|
|
1606
|
+
setContext(context) {
|
|
1607
|
+
this.currentContext = context;
|
|
1608
|
+
}
|
|
1609
|
+
async detect() {
|
|
1610
|
+
const result = await this.runner.run("aws", ["--version"], process.cwd());
|
|
1611
|
+
return result.exitCode === 0;
|
|
1612
|
+
}
|
|
1613
|
+
async install() {
|
|
1614
|
+
const available = await this.detect();
|
|
1615
|
+
if (!available) {
|
|
1616
|
+
throw new Error(
|
|
1617
|
+
"AWS CLI is required but not installed. Install from https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html"
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
async authenticate() {
|
|
1622
|
+
const cwd = this.currentContext?.projectRoot ?? process.cwd();
|
|
1623
|
+
const result = await this.runner.run("aws", ["sts", "get-caller-identity"], cwd);
|
|
1624
|
+
if (result.exitCode !== 0) {
|
|
1625
|
+
throw new Error(
|
|
1626
|
+
"AWS CLI is not authenticated. Run `aws configure` or set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables."
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
async provision(config) {
|
|
1631
|
+
this.currentContext = {
|
|
1632
|
+
projectRoot: config.projectRoot,
|
|
1633
|
+
appName: config.appName,
|
|
1634
|
+
region: config.region ?? "us-east-1"
|
|
1635
|
+
};
|
|
1636
|
+
const region = config.region ?? "us-east-1";
|
|
1637
|
+
const serviceName = sanitizeLightsailName(config.appName);
|
|
1638
|
+
const createService = await this.runner.run(
|
|
1639
|
+
"aws",
|
|
1640
|
+
[
|
|
1641
|
+
"lightsail",
|
|
1642
|
+
"create-container-service",
|
|
1643
|
+
"--service-name",
|
|
1644
|
+
serviceName,
|
|
1645
|
+
"--power",
|
|
1646
|
+
"nano",
|
|
1647
|
+
"--scale",
|
|
1648
|
+
"1",
|
|
1649
|
+
"--region",
|
|
1650
|
+
region
|
|
1651
|
+
],
|
|
1652
|
+
config.projectRoot
|
|
1653
|
+
);
|
|
1654
|
+
if (createService.exitCode !== 0 && !createService.stderr.includes("already exists")) {
|
|
1655
|
+
throw new Error(`Failed to create Lightsail container service: ${createService.stderr}`);
|
|
1656
|
+
}
|
|
1657
|
+
return {
|
|
1658
|
+
applicationId: serviceName,
|
|
1659
|
+
databaseId: null,
|
|
1660
|
+
secretsSet: ["PORT"]
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
async build(config) {
|
|
1664
|
+
this.currentContext = {
|
|
1665
|
+
projectRoot: config.projectRoot,
|
|
1666
|
+
appName: config.appName,
|
|
1667
|
+
region: config.region
|
|
1668
|
+
};
|
|
1669
|
+
const deployDirectory = join9(config.projectRoot, ".kora", "deploy");
|
|
1670
|
+
await bundleServer({
|
|
1671
|
+
projectRoot: config.projectRoot,
|
|
1672
|
+
deployDirectory
|
|
1673
|
+
});
|
|
1674
|
+
const client = config.deployTarget === "sync-server" ? null : await buildClient({
|
|
1675
|
+
projectRoot: config.projectRoot,
|
|
1676
|
+
outDir: join9(deployDirectory, "dist"),
|
|
1677
|
+
mode: "production"
|
|
1678
|
+
});
|
|
1679
|
+
return {
|
|
1680
|
+
clientDirectory: client?.outDir ?? null,
|
|
1681
|
+
serverBundlePath: join9(deployDirectory, "server-bundled.js"),
|
|
1682
|
+
deployDirectory
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
async deploy(artifacts) {
|
|
1686
|
+
const context = this.requireContext();
|
|
1687
|
+
const region = context.region ?? "us-east-1";
|
|
1688
|
+
const serviceName = sanitizeLightsailName(context.appName);
|
|
1689
|
+
const imageTag = `${serviceName}:latest`;
|
|
1690
|
+
this.logger.step("Building Docker image...");
|
|
1691
|
+
const dockerBuild = await this.runner.run(
|
|
1692
|
+
"docker",
|
|
1693
|
+
["build", "--platform", "linux/amd64", "-t", imageTag, "."],
|
|
1694
|
+
artifacts.deployDirectory
|
|
1695
|
+
);
|
|
1696
|
+
if (dockerBuild.exitCode !== 0) {
|
|
1697
|
+
throw new Error(`Docker build failed: ${dockerBuild.stderr}`);
|
|
1698
|
+
}
|
|
1699
|
+
this.logger.step("Pushing image to Lightsail...");
|
|
1700
|
+
const pushImage = await this.runner.run(
|
|
1701
|
+
"aws",
|
|
1702
|
+
[
|
|
1703
|
+
"lightsail",
|
|
1704
|
+
"push-container-image",
|
|
1705
|
+
"--service-name",
|
|
1706
|
+
serviceName,
|
|
1707
|
+
"--label",
|
|
1708
|
+
"latest",
|
|
1709
|
+
"--image",
|
|
1710
|
+
imageTag,
|
|
1711
|
+
"--region",
|
|
1712
|
+
region
|
|
1713
|
+
],
|
|
1714
|
+
artifacts.deployDirectory
|
|
1715
|
+
);
|
|
1716
|
+
if (pushImage.exitCode !== 0) {
|
|
1717
|
+
throw new Error(`Lightsail image push failed: ${pushImage.stderr}`);
|
|
1718
|
+
}
|
|
1719
|
+
const lightsailImage = parseLightsailImageRef(pushImage.stdout) ?? `:${serviceName}.latest.1`;
|
|
1720
|
+
this.logger.step("Creating Lightsail deployment...");
|
|
1721
|
+
const environment = { PORT: "3001" };
|
|
1722
|
+
for (const key of PASSTHROUGH_ENV_VARS) {
|
|
1723
|
+
const value = process.env[key];
|
|
1724
|
+
if (value) {
|
|
1725
|
+
environment[key] = value;
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
const containers = JSON.stringify({
|
|
1729
|
+
[serviceName]: {
|
|
1730
|
+
image: lightsailImage,
|
|
1731
|
+
ports: { "3001": "HTTP" },
|
|
1732
|
+
environment
|
|
1733
|
+
}
|
|
1734
|
+
});
|
|
1735
|
+
const publicEndpoint = JSON.stringify({
|
|
1736
|
+
containerName: serviceName,
|
|
1737
|
+
containerPort: 3001,
|
|
1738
|
+
healthCheck: {
|
|
1739
|
+
path: "/health",
|
|
1740
|
+
intervalSeconds: 30,
|
|
1741
|
+
timeoutSeconds: 5,
|
|
1742
|
+
healthyThreshold: 2,
|
|
1743
|
+
unhealthyThreshold: 3
|
|
1744
|
+
}
|
|
1745
|
+
});
|
|
1746
|
+
const createDeploy = await this.runner.run(
|
|
1747
|
+
"aws",
|
|
1748
|
+
[
|
|
1749
|
+
"lightsail",
|
|
1750
|
+
"create-container-service-deployment",
|
|
1751
|
+
"--service-name",
|
|
1752
|
+
serviceName,
|
|
1753
|
+
"--containers",
|
|
1754
|
+
containers,
|
|
1755
|
+
"--public-endpoint",
|
|
1756
|
+
publicEndpoint,
|
|
1757
|
+
"--region",
|
|
1758
|
+
region
|
|
1759
|
+
],
|
|
1760
|
+
context.projectRoot
|
|
1761
|
+
);
|
|
1762
|
+
if (createDeploy.exitCode !== 0) {
|
|
1763
|
+
throw new Error(`Lightsail deployment failed: ${createDeploy.stderr}`);
|
|
1764
|
+
}
|
|
1765
|
+
const serviceInfo = await this.runner.run(
|
|
1766
|
+
"aws",
|
|
1767
|
+
["lightsail", "get-container-services", "--service-name", serviceName, "--region", region],
|
|
1768
|
+
context.projectRoot
|
|
1769
|
+
);
|
|
1770
|
+
const rawUrl = parseLightsailUrl(serviceInfo.stdout) ?? `https://${serviceName}.${region}.cs.amazonlightsail.com`;
|
|
1771
|
+
const serviceUrl = rawUrl.replace(/\/+$/, "");
|
|
1772
|
+
const deploymentId = (/* @__PURE__ */ new Date()).toISOString();
|
|
1773
|
+
return {
|
|
1774
|
+
deploymentId,
|
|
1775
|
+
liveUrl: serviceUrl,
|
|
1776
|
+
syncUrl: `${serviceUrl.replace("https://", "wss://")}/kora-sync`
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
async rollback(_deploymentId) {
|
|
1780
|
+
const context = this.requireContext();
|
|
1781
|
+
const region = context.region ?? "us-east-1";
|
|
1782
|
+
const serviceName = sanitizeLightsailName(context.appName);
|
|
1783
|
+
const deployments = await this.runner.run(
|
|
1784
|
+
"aws",
|
|
1785
|
+
[
|
|
1786
|
+
"lightsail",
|
|
1787
|
+
"get-container-service-deployments",
|
|
1788
|
+
"--service-name",
|
|
1789
|
+
serviceName,
|
|
1790
|
+
"--region",
|
|
1791
|
+
region
|
|
1792
|
+
],
|
|
1793
|
+
context.projectRoot
|
|
1794
|
+
);
|
|
1795
|
+
if (deployments.exitCode !== 0) {
|
|
1796
|
+
throw new Error(`Lightsail rollback failed: ${deployments.stderr}`);
|
|
1797
|
+
}
|
|
1798
|
+
const previousDeployment = parsePreviousDeployment(deployments.stdout, serviceName);
|
|
1799
|
+
if (!previousDeployment) {
|
|
1800
|
+
throw new Error("No previous deployment found to rollback to.");
|
|
1801
|
+
}
|
|
1802
|
+
const redeploy = await this.runner.run(
|
|
1803
|
+
"aws",
|
|
1804
|
+
[
|
|
1805
|
+
"lightsail",
|
|
1806
|
+
"create-container-service-deployment",
|
|
1807
|
+
"--service-name",
|
|
1808
|
+
serviceName,
|
|
1809
|
+
"--containers",
|
|
1810
|
+
JSON.stringify(previousDeployment.containers),
|
|
1811
|
+
"--public-endpoint",
|
|
1812
|
+
JSON.stringify(previousDeployment.publicEndpoint),
|
|
1813
|
+
"--region",
|
|
1814
|
+
region
|
|
1815
|
+
],
|
|
1816
|
+
context.projectRoot
|
|
1817
|
+
);
|
|
1818
|
+
if (redeploy.exitCode !== 0) {
|
|
1819
|
+
throw new Error(`Lightsail rollback deployment failed: ${redeploy.stderr}`);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
async *logs(options) {
|
|
1823
|
+
const context = this.requireContext();
|
|
1824
|
+
const region = context.region ?? "us-east-1";
|
|
1825
|
+
const serviceName = sanitizeLightsailName(context.appName);
|
|
1826
|
+
const args = [
|
|
1827
|
+
"lightsail",
|
|
1828
|
+
"get-container-log",
|
|
1829
|
+
"--service-name",
|
|
1830
|
+
serviceName,
|
|
1831
|
+
"--container-name",
|
|
1832
|
+
serviceName,
|
|
1833
|
+
"--region",
|
|
1834
|
+
region
|
|
1835
|
+
];
|
|
1836
|
+
if (options.since) {
|
|
1837
|
+
args.push("--start-time", options.since);
|
|
1838
|
+
}
|
|
1839
|
+
const result = await this.runner.run("aws", args, context.projectRoot);
|
|
1840
|
+
if (result.exitCode !== 0) {
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
try {
|
|
1844
|
+
const parsed = JSON.parse(result.stdout);
|
|
1845
|
+
const events = parsed.logEvents ?? [];
|
|
1846
|
+
const limited = options.tail ? events.slice(-options.tail) : events;
|
|
1847
|
+
for (const event of limited) {
|
|
1848
|
+
yield {
|
|
1849
|
+
timestamp: event.createdAt,
|
|
1850
|
+
level: inferLogLevel4(event.message),
|
|
1851
|
+
message: event.message
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
} catch {
|
|
1855
|
+
for (const line of result.stdout.split("\n").filter(Boolean)) {
|
|
1856
|
+
yield { timestamp: (/* @__PURE__ */ new Date()).toISOString(), level: "info", message: line };
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
async status() {
|
|
1861
|
+
const context = this.requireContext();
|
|
1862
|
+
const region = context.region ?? "us-east-1";
|
|
1863
|
+
const serviceName = sanitizeLightsailName(context.appName);
|
|
1864
|
+
const result = await this.runner.run(
|
|
1865
|
+
"aws",
|
|
1866
|
+
["lightsail", "get-container-services", "--service-name", serviceName, "--region", region],
|
|
1867
|
+
context.projectRoot
|
|
1868
|
+
);
|
|
1869
|
+
if (result.exitCode !== 0) {
|
|
1870
|
+
return { state: "failed", message: result.stderr };
|
|
1871
|
+
}
|
|
1872
|
+
try {
|
|
1873
|
+
const parsed = JSON.parse(result.stdout);
|
|
1874
|
+
const service = parsed.containerServices?.[0];
|
|
1875
|
+
if (!service) {
|
|
1876
|
+
return { state: "unknown", message: "Container service not found" };
|
|
1877
|
+
}
|
|
1878
|
+
const deployState = service.currentDeployment?.state ?? "UNKNOWN";
|
|
1879
|
+
if (service.state === "RUNNING" && deployState === "ACTIVE") {
|
|
1880
|
+
return {
|
|
1881
|
+
state: "healthy",
|
|
1882
|
+
message: "Lightsail container service is running",
|
|
1883
|
+
liveUrl: service.url
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
if (service.state === "DEPLOYING" || deployState === "ACTIVATING") {
|
|
1887
|
+
return {
|
|
1888
|
+
state: "pending",
|
|
1889
|
+
message: `Service: ${service.state}, Deployment: ${deployState}`
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1892
|
+
return {
|
|
1893
|
+
state: service.state === "DISABLED" ? "failed" : "unknown",
|
|
1894
|
+
message: `Service: ${service.state}, Deployment: ${deployState}`
|
|
1895
|
+
};
|
|
1896
|
+
} catch {
|
|
1897
|
+
return { state: "unknown", message: "Could not parse service status" };
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
requireContext() {
|
|
1901
|
+
if (!this.currentContext) {
|
|
1902
|
+
throw new Error("AWS Lightsail adapter context is not initialized. Run provision() first.");
|
|
1903
|
+
}
|
|
1904
|
+
return this.currentContext;
|
|
1905
|
+
}
|
|
1906
|
+
};
|
|
1907
|
+
var NodeAwsLightsailCommandRunner = class {
|
|
1908
|
+
async run(command, args, cwd) {
|
|
1909
|
+
return new Promise((resolve2) => {
|
|
1910
|
+
const child = spawn5(command, args, {
|
|
1911
|
+
cwd,
|
|
1912
|
+
env: process.env,
|
|
1913
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1914
|
+
});
|
|
1915
|
+
let stdout = "";
|
|
1916
|
+
let stderr = "";
|
|
1917
|
+
child.stdout?.on("data", (chunk) => {
|
|
1918
|
+
stdout += chunk.toString("utf-8");
|
|
1919
|
+
});
|
|
1920
|
+
child.stderr?.on("data", (chunk) => {
|
|
1921
|
+
stderr += chunk.toString("utf-8");
|
|
1922
|
+
});
|
|
1923
|
+
child.on("error", (error) => {
|
|
1924
|
+
resolve2({ exitCode: 1, stdout, stderr: `${stderr}
|
|
1925
|
+
${error.message}` });
|
|
1926
|
+
});
|
|
1927
|
+
child.on("exit", (code) => {
|
|
1928
|
+
resolve2({ exitCode: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() });
|
|
1929
|
+
});
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
};
|
|
1933
|
+
var PASSTHROUGH_ENV_VARS = ["DATABASE_URL", "AUTH_SECRET", "PUBLIC_URL", "NODE_ENV"];
|
|
1934
|
+
function sanitizeLightsailName(name) {
|
|
1935
|
+
return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "").slice(0, 255) || "kora-app";
|
|
1936
|
+
}
|
|
1937
|
+
function parseLightsailImageRef(output) {
|
|
1938
|
+
const match = output.match(/Refer to this image as\s+"?(:[^"\s]+)"?/i);
|
|
1939
|
+
if (match?.[1]) {
|
|
1940
|
+
return match[1];
|
|
1941
|
+
}
|
|
1942
|
+
const refMatch = output.match(/(:\S+\.\S+\.\d+)/);
|
|
1943
|
+
return refMatch?.[1] ?? null;
|
|
1944
|
+
}
|
|
1945
|
+
function parseLightsailUrl(rawJson) {
|
|
1946
|
+
try {
|
|
1947
|
+
const parsed = JSON.parse(rawJson);
|
|
1948
|
+
const url = parsed.containerServices?.[0]?.url;
|
|
1949
|
+
if (typeof url === "string" && url.length > 0) {
|
|
1950
|
+
return url.startsWith("https://") ? url : `https://${url}`;
|
|
1951
|
+
}
|
|
1952
|
+
return null;
|
|
1953
|
+
} catch {
|
|
1954
|
+
return null;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
function parsePreviousDeployment(rawJson, serviceName) {
|
|
1958
|
+
try {
|
|
1959
|
+
const parsed = JSON.parse(rawJson);
|
|
1960
|
+
const deployments = parsed.deployments ?? [];
|
|
1961
|
+
const previous = deployments.length > 1 ? deployments[1] : null;
|
|
1962
|
+
if (!previous) return null;
|
|
1963
|
+
return {
|
|
1964
|
+
containers: previous.containers,
|
|
1965
|
+
publicEndpoint: previous.publicEndpoint
|
|
1966
|
+
};
|
|
1967
|
+
} catch {
|
|
1968
|
+
return null;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
function inferLogLevel4(line) {
|
|
1972
|
+
const normalized = line.toLowerCase();
|
|
1973
|
+
if (normalized.includes("error")) return "error";
|
|
1974
|
+
if (normalized.includes("warn")) return "warn";
|
|
1975
|
+
if (normalized.includes("debug")) return "debug";
|
|
1976
|
+
return "info";
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
// src/commands/deploy/adapters/factory.ts
|
|
1980
|
+
function createDeployAdapter(platform) {
|
|
1981
|
+
switch (platform) {
|
|
1982
|
+
case "fly":
|
|
1983
|
+
return new FlyAdapter();
|
|
1984
|
+
case "railway":
|
|
1985
|
+
return new RailwayAdapter();
|
|
1986
|
+
case "aws-ecs":
|
|
1987
|
+
return new AwsEcsAdapter();
|
|
1988
|
+
case "aws-lightsail":
|
|
1989
|
+
return new AwsLightsailAdapter();
|
|
1990
|
+
case "render":
|
|
1991
|
+
case "docker":
|
|
1992
|
+
case "kora-cloud":
|
|
1993
|
+
return new StubDeployAdapter(platform);
|
|
1994
|
+
default: {
|
|
1995
|
+
const exhaustiveCheck = platform;
|
|
1996
|
+
return exhaustiveCheck;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
// src/commands/deploy/artifacts/dockerfile-generator.ts
|
|
2002
|
+
import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
|
|
2003
|
+
import { join as join10 } from "path";
|
|
2004
|
+
var GENERATED_HEADER3 = "# Generated by kora deploy \u2014 do not edit manually";
|
|
2005
|
+
function generateDockerfile(options = {}) {
|
|
2006
|
+
const nodeVersion = options.nodeVersion ?? "20-alpine";
|
|
2007
|
+
const port = options.port ?? 3e3;
|
|
2008
|
+
const clientDirectory = options.clientDirectory === void 0 ? "dist" : options.clientDirectory;
|
|
2009
|
+
const serverBundleFile = options.serverBundleFile ?? "server-bundled.js";
|
|
2010
|
+
const hasNativeDeps = options.nativeDependencies && Object.keys(options.nativeDependencies).length > 0;
|
|
2011
|
+
const lines = [
|
|
2012
|
+
GENERATED_HEADER3,
|
|
2013
|
+
"",
|
|
2014
|
+
`FROM node:${nodeVersion} AS runtime`,
|
|
2015
|
+
"WORKDIR /app",
|
|
2016
|
+
"ENV NODE_ENV=production",
|
|
2017
|
+
`ENV PORT=${String(port)}`,
|
|
2018
|
+
""
|
|
2019
|
+
];
|
|
2020
|
+
if (hasNativeDeps) {
|
|
2021
|
+
lines.push("# Install build tools for native modules");
|
|
2022
|
+
lines.push("RUN apk add --no-cache python3 make g++");
|
|
2023
|
+
lines.push("");
|
|
2024
|
+
lines.push("COPY package.json ./package.json");
|
|
2025
|
+
lines.push("RUN npm install --omit=dev");
|
|
2026
|
+
lines.push("");
|
|
2027
|
+
}
|
|
2028
|
+
if (clientDirectory !== null) {
|
|
2029
|
+
lines.push(`COPY ${clientDirectory} ./dist`);
|
|
2030
|
+
}
|
|
2031
|
+
lines.push(`COPY ${serverBundleFile} ./server-bundled.js`);
|
|
2032
|
+
lines.push("");
|
|
2033
|
+
lines.push(`EXPOSE ${String(port)}`);
|
|
2034
|
+
lines.push('CMD ["node", "./server-bundled.js"]');
|
|
2035
|
+
lines.push("");
|
|
2036
|
+
return lines.join("\n");
|
|
2037
|
+
}
|
|
2038
|
+
function generateDeployPackageJson(nativeDependencies) {
|
|
2039
|
+
const pkg = {
|
|
2040
|
+
name: "kora-deploy",
|
|
2041
|
+
version: "1.0.0",
|
|
2042
|
+
private: true,
|
|
2043
|
+
type: "module",
|
|
2044
|
+
dependencies: nativeDependencies
|
|
2045
|
+
};
|
|
2046
|
+
return `${JSON.stringify(pkg, null, 2)}
|
|
2047
|
+
`;
|
|
2048
|
+
}
|
|
2049
|
+
function generateDockerIgnore() {
|
|
2050
|
+
return [
|
|
2051
|
+
GENERATED_HEADER3,
|
|
2052
|
+
"",
|
|
2053
|
+
"node_modules",
|
|
2054
|
+
"npm-debug.log*",
|
|
2055
|
+
"pnpm-debug.log*",
|
|
2056
|
+
"yarn-error.log*",
|
|
2057
|
+
".env",
|
|
2058
|
+
".env.*",
|
|
2059
|
+
".git",
|
|
2060
|
+
".gitignore",
|
|
2061
|
+
"coverage",
|
|
2062
|
+
".turbo",
|
|
2063
|
+
"*.test.ts",
|
|
2064
|
+
"*.spec.ts",
|
|
2065
|
+
""
|
|
2066
|
+
].join("\n");
|
|
2067
|
+
}
|
|
2068
|
+
async function writeDockerfileArtifact(deployDirectory, options = {}) {
|
|
2069
|
+
await mkdir4(deployDirectory, { recursive: true });
|
|
2070
|
+
const dockerfilePath = join10(deployDirectory, "Dockerfile");
|
|
2071
|
+
await writeFile4(dockerfilePath, generateDockerfile(options), "utf-8");
|
|
2072
|
+
if (options.nativeDependencies && Object.keys(options.nativeDependencies).length > 0) {
|
|
2073
|
+
const pkgJsonPath = join10(deployDirectory, "package.json");
|
|
2074
|
+
await writeFile4(pkgJsonPath, generateDeployPackageJson(options.nativeDependencies), "utf-8");
|
|
2075
|
+
}
|
|
2076
|
+
return dockerfilePath;
|
|
2077
|
+
}
|
|
2078
|
+
async function writeDockerIgnoreArtifact(deployDirectory) {
|
|
2079
|
+
await mkdir4(deployDirectory, { recursive: true });
|
|
2080
|
+
const dockerIgnorePath = join10(deployDirectory, ".dockerignore");
|
|
2081
|
+
await writeFile4(dockerIgnorePath, generateDockerIgnore(), "utf-8");
|
|
2082
|
+
return dockerIgnorePath;
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
// src/commands/deploy/state/deploy-state.ts
|
|
2086
|
+
import { mkdir as mkdir5, readFile as readFile2, rm, writeFile as writeFile5 } from "fs/promises";
|
|
2087
|
+
import { join as join11 } from "path";
|
|
2088
|
+
var KORA_DEPLOY_DIRECTORY = join11(".kora", "deploy");
|
|
2089
|
+
var DEPLOY_STATE_FILENAME = "deploy.json";
|
|
2090
|
+
function resolveDeployDirectory(projectRoot) {
|
|
2091
|
+
return join11(projectRoot, KORA_DEPLOY_DIRECTORY);
|
|
2092
|
+
}
|
|
2093
|
+
function resolveDeployStatePath(projectRoot) {
|
|
2094
|
+
return join11(resolveDeployDirectory(projectRoot), DEPLOY_STATE_FILENAME);
|
|
2095
|
+
}
|
|
2096
|
+
async function readDeployState(projectRoot) {
|
|
2097
|
+
const statePath = resolveDeployStatePath(projectRoot);
|
|
2098
|
+
try {
|
|
2099
|
+
const source = await readFile2(statePath, "utf-8");
|
|
2100
|
+
const parsed = JSON.parse(source);
|
|
2101
|
+
return parseDeployState(parsed);
|
|
2102
|
+
} catch (error) {
|
|
2103
|
+
const code = error.code;
|
|
2104
|
+
if (code === "ENOENT") return null;
|
|
2105
|
+
throw error;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
async function writeDeployState(projectRoot, input, now = /* @__PURE__ */ new Date()) {
|
|
2109
|
+
const timestamp = now.toISOString();
|
|
2110
|
+
const state = {
|
|
2111
|
+
platform: input.platform,
|
|
2112
|
+
appName: input.appName,
|
|
2113
|
+
region: input.region,
|
|
2114
|
+
projectRoot: input.projectRoot,
|
|
2115
|
+
liveUrl: input.liveUrl ?? null,
|
|
2116
|
+
syncUrl: input.syncUrl ?? null,
|
|
2117
|
+
databaseId: input.databaseId ?? null,
|
|
2118
|
+
lastDeploymentId: input.lastDeploymentId ?? null,
|
|
2119
|
+
createdAt: timestamp,
|
|
2120
|
+
updatedAt: timestamp
|
|
2121
|
+
};
|
|
2122
|
+
await persistDeployState(projectRoot, state);
|
|
2123
|
+
return state;
|
|
2124
|
+
}
|
|
2125
|
+
async function updateDeployState(projectRoot, patch, now = /* @__PURE__ */ new Date()) {
|
|
2126
|
+
const existing = await readDeployState(projectRoot);
|
|
2127
|
+
if (!existing) {
|
|
2128
|
+
throw new Error("Cannot update deploy state because deploy.json does not exist yet.");
|
|
2129
|
+
}
|
|
2130
|
+
const nextState = {
|
|
2131
|
+
platform: patch.platform ?? existing.platform,
|
|
2132
|
+
appName: patch.appName ?? existing.appName,
|
|
2133
|
+
region: patch.region === void 0 ? existing.region : patch.region,
|
|
2134
|
+
projectRoot: patch.projectRoot ?? existing.projectRoot,
|
|
2135
|
+
liveUrl: patch.liveUrl === void 0 ? existing.liveUrl : patch.liveUrl,
|
|
2136
|
+
syncUrl: patch.syncUrl === void 0 ? existing.syncUrl : patch.syncUrl,
|
|
2137
|
+
databaseId: patch.databaseId === void 0 ? existing.databaseId : patch.databaseId,
|
|
2138
|
+
lastDeploymentId: patch.lastDeploymentId === void 0 ? existing.lastDeploymentId : patch.lastDeploymentId,
|
|
2139
|
+
createdAt: existing.createdAt,
|
|
2140
|
+
updatedAt: now.toISOString()
|
|
2141
|
+
};
|
|
2142
|
+
await persistDeployState(projectRoot, nextState);
|
|
2143
|
+
return nextState;
|
|
2144
|
+
}
|
|
2145
|
+
async function resetDeployState(projectRoot) {
|
|
2146
|
+
await rm(resolveDeployDirectory(projectRoot), { recursive: true, force: true });
|
|
2147
|
+
}
|
|
2148
|
+
async function persistDeployState(projectRoot, state) {
|
|
2149
|
+
const deployDir = resolveDeployDirectory(projectRoot);
|
|
2150
|
+
const statePath = resolveDeployStatePath(projectRoot);
|
|
2151
|
+
await mkdir5(deployDir, { recursive: true });
|
|
2152
|
+
await writeFile5(statePath, `${JSON.stringify(state, null, 2)}
|
|
2153
|
+
`, "utf-8");
|
|
2154
|
+
}
|
|
2155
|
+
function parseDeployState(value) {
|
|
2156
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2157
|
+
throw new Error("Invalid deploy state: expected an object.");
|
|
2158
|
+
}
|
|
2159
|
+
const record = value;
|
|
2160
|
+
const platform = readPlatform(record.platform);
|
|
2161
|
+
const appName = readString(record.appName, "appName");
|
|
2162
|
+
const region = readOptionalString(record.region, "region");
|
|
2163
|
+
const projectRoot = readString(record.projectRoot, "projectRoot");
|
|
2164
|
+
const liveUrl = readOptionalString(record.liveUrl, "liveUrl");
|
|
2165
|
+
const syncUrl = readOptionalString(record.syncUrl, "syncUrl");
|
|
2166
|
+
const databaseId = readOptionalString(record.databaseId, "databaseId");
|
|
2167
|
+
const lastDeploymentId = readOptionalString(record.lastDeploymentId, "lastDeploymentId");
|
|
2168
|
+
const createdAt = readString(record.createdAt, "createdAt");
|
|
2169
|
+
const updatedAt = readString(record.updatedAt, "updatedAt");
|
|
2170
|
+
return {
|
|
2171
|
+
platform,
|
|
2172
|
+
appName,
|
|
2173
|
+
region,
|
|
2174
|
+
projectRoot,
|
|
2175
|
+
liveUrl,
|
|
2176
|
+
syncUrl,
|
|
2177
|
+
databaseId,
|
|
2178
|
+
lastDeploymentId,
|
|
2179
|
+
createdAt,
|
|
2180
|
+
updatedAt
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
function readPlatform(value) {
|
|
2184
|
+
if (typeof value !== "string" || !isDeployPlatform(value)) {
|
|
2185
|
+
throw new Error('Invalid deploy state: "platform" must be a supported platform.');
|
|
2186
|
+
}
|
|
2187
|
+
return value;
|
|
2188
|
+
}
|
|
2189
|
+
function readString(value, field) {
|
|
2190
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
2191
|
+
throw new Error(`Invalid deploy state: "${field}" must be a non-empty string.`);
|
|
2192
|
+
}
|
|
2193
|
+
return value;
|
|
2194
|
+
}
|
|
2195
|
+
function readOptionalString(value, field) {
|
|
2196
|
+
if (value === null) return null;
|
|
2197
|
+
if (typeof value === "string") return value;
|
|
2198
|
+
throw new Error(`Invalid deploy state: "${field}" must be a string or null.`);
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
// src/commands/deploy/deploy-command.ts
|
|
2202
|
+
import { existsSync as existsSync2 } from "fs";
|
|
2203
|
+
import { basename, join as join12 } from "path";
|
|
2204
|
+
import { defineCommand as defineCommand2 } from "citty";
|
|
2205
|
+
var deployCommand = defineCommand2({
|
|
2206
|
+
meta: {
|
|
2207
|
+
name: "deploy",
|
|
2208
|
+
description: "Deploy a Kora project to your selected platform"
|
|
2209
|
+
},
|
|
2210
|
+
args: {
|
|
2211
|
+
platform: {
|
|
2212
|
+
type: "string",
|
|
2213
|
+
description: `Deployment platform (${DEPLOY_PLATFORMS.join(", ")})`
|
|
2214
|
+
},
|
|
2215
|
+
app: {
|
|
2216
|
+
type: "string",
|
|
2217
|
+
description: "Application name used by the target platform"
|
|
2218
|
+
},
|
|
2219
|
+
region: {
|
|
2220
|
+
type: "string",
|
|
2221
|
+
description: "Preferred deployment region (for example: iad, lhr, syd)"
|
|
2222
|
+
},
|
|
2223
|
+
prod: {
|
|
2224
|
+
type: "boolean",
|
|
2225
|
+
description: "Deploy to production environment",
|
|
2226
|
+
default: false
|
|
2227
|
+
},
|
|
2228
|
+
confirm: {
|
|
2229
|
+
type: "boolean",
|
|
2230
|
+
description: "Non-interactive mode (fail fast on missing required data)",
|
|
2231
|
+
default: false
|
|
2232
|
+
},
|
|
2233
|
+
reset: {
|
|
2234
|
+
type: "boolean",
|
|
2235
|
+
description: "Delete .kora/deploy state and generated artifacts",
|
|
2236
|
+
default: false
|
|
2237
|
+
}
|
|
2238
|
+
},
|
|
2239
|
+
subCommands: {
|
|
2240
|
+
status: defineCommand2({
|
|
2241
|
+
meta: {
|
|
2242
|
+
name: "status",
|
|
2243
|
+
description: "Show current deployment status"
|
|
2244
|
+
},
|
|
2245
|
+
async run() {
|
|
2246
|
+
const logger = createLogger();
|
|
2247
|
+
const projectRoot = await requireProjectRoot();
|
|
2248
|
+
const state = await readDeployState(projectRoot);
|
|
2249
|
+
if (!state) {
|
|
2250
|
+
logger.warn("No deployment state found. Run `kora deploy` first.");
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
logger.banner();
|
|
2254
|
+
logger.info(`Platform: ${state.platform}`);
|
|
2255
|
+
logger.step(`App: ${state.appName}`);
|
|
2256
|
+
logger.step(`Region: ${state.region ?? "n/a"}`);
|
|
2257
|
+
logger.step(`Last deployment: ${state.lastDeploymentId ?? "n/a"}`);
|
|
2258
|
+
const adapter = createDeployAdapter(state.platform);
|
|
2259
|
+
configureAdapterContext(adapter, {
|
|
2260
|
+
projectRoot,
|
|
2261
|
+
appName: state.appName,
|
|
2262
|
+
region: state.region
|
|
2263
|
+
});
|
|
2264
|
+
const adapterStatus = await adapter.status();
|
|
2265
|
+
logger.step(`Status: ${adapterStatus.state}`);
|
|
2266
|
+
logger.step(`Message: ${adapterStatus.message}`);
|
|
2267
|
+
logger.step(`Live URL: ${adapterStatus.liveUrl ?? state.liveUrl ?? "n/a"}`);
|
|
2268
|
+
logger.step(`Sync URL: ${state.syncUrl ?? "n/a"}`);
|
|
2269
|
+
}
|
|
2270
|
+
}),
|
|
2271
|
+
rollback: defineCommand2({
|
|
2272
|
+
meta: {
|
|
2273
|
+
name: "rollback",
|
|
2274
|
+
description: "Rollback the current deployment"
|
|
2275
|
+
},
|
|
2276
|
+
args: {
|
|
2277
|
+
id: {
|
|
2278
|
+
type: "positional",
|
|
2279
|
+
description: "Optional deployment identifier",
|
|
2280
|
+
required: false
|
|
2281
|
+
}
|
|
2282
|
+
},
|
|
2283
|
+
async run({ args }) {
|
|
2284
|
+
const logger = createLogger();
|
|
2285
|
+
const projectRoot = await requireProjectRoot();
|
|
2286
|
+
const state = await readDeployState(projectRoot);
|
|
2287
|
+
if (!state) {
|
|
2288
|
+
logger.warn("No deployment state found. Run `kora deploy` first.");
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2291
|
+
const adapter = createDeployAdapter(state.platform);
|
|
2292
|
+
configureAdapterContext(adapter, {
|
|
2293
|
+
projectRoot,
|
|
2294
|
+
appName: state.appName,
|
|
2295
|
+
region: state.region
|
|
2296
|
+
});
|
|
2297
|
+
const deploymentId = typeof args.id === "string" && args.id.length > 0 ? args.id : state.lastDeploymentId ?? "latest";
|
|
2298
|
+
await adapter.rollback(deploymentId);
|
|
2299
|
+
logger.success(`Rolled back ${state.platform} deployment to ${deploymentId}.`);
|
|
2300
|
+
}
|
|
2301
|
+
}),
|
|
2302
|
+
logs: defineCommand2({
|
|
2303
|
+
meta: {
|
|
2304
|
+
name: "logs",
|
|
2305
|
+
description: "Read deployment logs"
|
|
2306
|
+
},
|
|
2307
|
+
async run() {
|
|
2308
|
+
const logger = createLogger();
|
|
2309
|
+
const projectRoot = await requireProjectRoot();
|
|
2310
|
+
const state = await readDeployState(projectRoot);
|
|
2311
|
+
if (!state) {
|
|
2312
|
+
logger.warn("No deployment state found. Run `kora deploy` first.");
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
const adapter = createDeployAdapter(state.platform);
|
|
2316
|
+
configureAdapterContext(adapter, {
|
|
2317
|
+
projectRoot,
|
|
2318
|
+
appName: state.appName,
|
|
2319
|
+
region: state.region
|
|
2320
|
+
});
|
|
2321
|
+
const logLines = adapter.logs({ tail: 200 });
|
|
2322
|
+
let hasLines = false;
|
|
2323
|
+
for await (const line of logLines) {
|
|
2324
|
+
hasLines = true;
|
|
2325
|
+
logger.step(`[${line.level}] ${line.message}`);
|
|
2326
|
+
}
|
|
2327
|
+
if (!hasLines) {
|
|
2328
|
+
logger.warn(`No logs returned from ${state.platform}.`);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
})
|
|
2332
|
+
},
|
|
2333
|
+
async run({ args, rawArgs }) {
|
|
2334
|
+
const subCommandNames = ["status", "rollback", "logs"];
|
|
2335
|
+
if (rawArgs.some((arg) => subCommandNames.includes(arg))) {
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
const logger = createLogger();
|
|
2339
|
+
const prompts = createPromptClient();
|
|
2340
|
+
const projectRoot = await requireProjectRoot();
|
|
2341
|
+
if (args.reset === true) {
|
|
2342
|
+
await resetDeployState(projectRoot);
|
|
2343
|
+
logger.success("Reset .kora/deploy state.");
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
const existingState = await readDeployState(projectRoot);
|
|
2347
|
+
const confirmMode = args.confirm === true;
|
|
2348
|
+
const platform = await resolvePlatform({
|
|
2349
|
+
promptClient: prompts,
|
|
2350
|
+
platformArg: args.platform,
|
|
2351
|
+
storedPlatform: existingState?.platform,
|
|
2352
|
+
confirm: confirmMode
|
|
2353
|
+
});
|
|
2354
|
+
if (!isImplementedDeployPlatform(platform)) {
|
|
2355
|
+
throw new DeployPlatformUnavailableError(platform, IMPLEMENTED_DEPLOY_PLATFORMS);
|
|
2356
|
+
}
|
|
2357
|
+
const appName = resolveAppName(args.app, existingState?.appName, projectRoot, confirmMode);
|
|
2358
|
+
const region = resolveRegion(args.region, existingState?.region, confirmMode);
|
|
2359
|
+
const deployDirectory = resolveDeployDirectory(projectRoot);
|
|
2360
|
+
const environment = args.prod === true ? "production" : "preview";
|
|
2361
|
+
const deployTarget = detectDeployTarget(projectRoot);
|
|
2362
|
+
const config = {
|
|
2363
|
+
projectRoot,
|
|
2364
|
+
appName,
|
|
2365
|
+
region,
|
|
2366
|
+
environment,
|
|
2367
|
+
confirm: confirmMode,
|
|
2368
|
+
deployTarget
|
|
2369
|
+
};
|
|
2370
|
+
const adapter = createDeployAdapter(platform);
|
|
2371
|
+
configureAdapterContext(adapter, {
|
|
2372
|
+
projectRoot,
|
|
2373
|
+
appName,
|
|
2374
|
+
region
|
|
2375
|
+
});
|
|
2376
|
+
logger.banner();
|
|
2377
|
+
logger.info(
|
|
2378
|
+
`Deploying to ${platform} (${appName}${region ? `, ${region}` : ""}, ${environment})`
|
|
2379
|
+
);
|
|
2380
|
+
if (deployTarget === "sync-server") {
|
|
2381
|
+
logger.step("Detected Tauri project. Deploying the sync server only.");
|
|
2382
|
+
}
|
|
2383
|
+
if (confirmMode) {
|
|
2384
|
+
logger.step("Running in --confirm mode (non-interactive, fail-fast).");
|
|
2385
|
+
}
|
|
2386
|
+
await writeDockerfileArtifact(deployDirectory, {
|
|
2387
|
+
clientDirectory: deployTarget === "sync-server" ? null : void 0,
|
|
2388
|
+
nativeDependencies: {
|
|
2389
|
+
"better-sqlite3": "^11.0.0",
|
|
2390
|
+
"drizzle-orm": "^0.45.2",
|
|
2391
|
+
postgres: "^3.4.0"
|
|
2392
|
+
}
|
|
2393
|
+
});
|
|
2394
|
+
await writeDockerIgnoreArtifact(deployDirectory);
|
|
2395
|
+
const detected = await adapter.detect();
|
|
2396
|
+
if (!detected) {
|
|
2397
|
+
await adapter.install();
|
|
2398
|
+
}
|
|
2399
|
+
await adapter.authenticate();
|
|
2400
|
+
const provisionResult = await adapter.provision(config);
|
|
2401
|
+
const artifacts = await adapter.build(config);
|
|
2402
|
+
const deployResult = await adapter.deploy(artifacts);
|
|
2403
|
+
if (existingState) {
|
|
2404
|
+
await updateDeployState(projectRoot, {
|
|
2405
|
+
platform,
|
|
2406
|
+
appName,
|
|
2407
|
+
region,
|
|
2408
|
+
projectRoot,
|
|
2409
|
+
liveUrl: deployResult.liveUrl,
|
|
2410
|
+
syncUrl: deployResult.syncUrl,
|
|
2411
|
+
databaseId: provisionResult.databaseId,
|
|
2412
|
+
lastDeploymentId: deployResult.deploymentId
|
|
2413
|
+
});
|
|
2414
|
+
} else {
|
|
2415
|
+
await writeDeployState(projectRoot, {
|
|
2416
|
+
platform,
|
|
2417
|
+
appName,
|
|
2418
|
+
region,
|
|
2419
|
+
projectRoot,
|
|
2420
|
+
liveUrl: deployResult.liveUrl,
|
|
2421
|
+
syncUrl: deployResult.syncUrl,
|
|
2422
|
+
databaseId: provisionResult.databaseId,
|
|
2423
|
+
lastDeploymentId: deployResult.deploymentId
|
|
2424
|
+
});
|
|
2425
|
+
}
|
|
2426
|
+
logger.success(`Deployment completed: ${deployResult.liveUrl}`);
|
|
2427
|
+
if (deployResult.syncUrl) {
|
|
2428
|
+
logger.step(`Sync endpoint: ${deployResult.syncUrl}`);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
});
|
|
2432
|
+
async function resolvePlatform(options) {
|
|
2433
|
+
if (typeof options.platformArg === "string") {
|
|
2434
|
+
if (!isDeployPlatform(options.platformArg)) {
|
|
2435
|
+
throw new Error(
|
|
2436
|
+
`Invalid --platform value "${options.platformArg}". Valid options: ${DEPLOY_PLATFORMS.join(", ")}`
|
|
2437
|
+
);
|
|
2438
|
+
}
|
|
2439
|
+
return options.platformArg;
|
|
2440
|
+
}
|
|
2441
|
+
if (options.storedPlatform) {
|
|
2442
|
+
return options.storedPlatform;
|
|
2443
|
+
}
|
|
2444
|
+
if (options.confirm || !isInteractiveTerminal()) {
|
|
2445
|
+
throw new Error(
|
|
2446
|
+
"Missing deploy platform in --confirm mode. Provide --platform or run an interactive deploy first."
|
|
2447
|
+
);
|
|
2448
|
+
}
|
|
2449
|
+
return await options.promptClient.select("Where do you want to deploy?", [
|
|
2450
|
+
{
|
|
2451
|
+
label: "Fly.io (recommended for sync apps)",
|
|
2452
|
+
value: "fly"
|
|
2453
|
+
},
|
|
2454
|
+
{
|
|
2455
|
+
label: "Railway",
|
|
2456
|
+
value: "railway"
|
|
2457
|
+
},
|
|
2458
|
+
{
|
|
2459
|
+
label: "AWS ECS Fargate (production multi-instance)",
|
|
2460
|
+
value: "aws-ecs"
|
|
2461
|
+
},
|
|
2462
|
+
{
|
|
2463
|
+
label: "AWS Lightsail Containers (simple, cost-effective)",
|
|
2464
|
+
value: "aws-lightsail"
|
|
2465
|
+
},
|
|
2466
|
+
{
|
|
2467
|
+
label: "Render (coming soon)",
|
|
2468
|
+
value: "render"
|
|
2469
|
+
},
|
|
2470
|
+
{
|
|
2471
|
+
label: "Docker (self-hosted, coming soon)",
|
|
2472
|
+
value: "docker"
|
|
2473
|
+
},
|
|
2474
|
+
{
|
|
2475
|
+
label: "Kora Cloud (coming soon)",
|
|
2476
|
+
value: "kora-cloud"
|
|
2477
|
+
}
|
|
2478
|
+
]);
|
|
2479
|
+
}
|
|
2480
|
+
function resolveAppName(argValue, storedValue, projectRoot, confirm) {
|
|
2481
|
+
if (typeof argValue === "string" && argValue.length > 0) {
|
|
2482
|
+
return sanitizeAppName(argValue);
|
|
2483
|
+
}
|
|
2484
|
+
if (storedValue && storedValue.length > 0) {
|
|
2485
|
+
return storedValue;
|
|
2486
|
+
}
|
|
2487
|
+
if (confirm) {
|
|
2488
|
+
throw new Error(
|
|
2489
|
+
"Missing app name in --confirm mode. Provide --app or run an interactive deploy first."
|
|
2490
|
+
);
|
|
2491
|
+
}
|
|
2492
|
+
return sanitizeAppName(basename(projectRoot));
|
|
2493
|
+
}
|
|
2494
|
+
function resolveRegion(argValue, storedValue, confirm) {
|
|
2495
|
+
if (typeof argValue === "string" && argValue.length > 0) return argValue;
|
|
2496
|
+
if (storedValue !== void 0) return storedValue;
|
|
2497
|
+
if (confirm) {
|
|
2498
|
+
throw new Error(
|
|
2499
|
+
"Missing region in --confirm mode. Provide --region or run an interactive deploy first."
|
|
2500
|
+
);
|
|
2501
|
+
}
|
|
2502
|
+
return "iad";
|
|
2503
|
+
}
|
|
2504
|
+
function sanitizeAppName(value) {
|
|
2505
|
+
const normalized = value.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
|
|
2506
|
+
if (normalized.length === 0) {
|
|
2507
|
+
return "kora-app";
|
|
2508
|
+
}
|
|
2509
|
+
return normalized;
|
|
2510
|
+
}
|
|
2511
|
+
async function requireProjectRoot() {
|
|
2512
|
+
const projectRoot = await findProjectRoot();
|
|
2513
|
+
if (!projectRoot) {
|
|
2514
|
+
throw new InvalidProjectError(process.cwd());
|
|
2515
|
+
}
|
|
2516
|
+
return projectRoot;
|
|
2517
|
+
}
|
|
2518
|
+
function isInteractiveTerminal() {
|
|
2519
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
2520
|
+
}
|
|
2521
|
+
function configureAdapterContext(adapter, context) {
|
|
2522
|
+
if (hasContextSetter(adapter)) {
|
|
2523
|
+
adapter.setContext(context);
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
function detectDeployTarget(projectRoot) {
|
|
2527
|
+
return existsSync2(join12(projectRoot, "src-tauri")) ? "sync-server" : "full-stack";
|
|
2528
|
+
}
|
|
2529
|
+
function hasContextSetter(adapter) {
|
|
2530
|
+
return typeof adapter.setContext === "function";
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
export {
|
|
2534
|
+
AGENTS_FRAMEWORKS,
|
|
2535
|
+
isAgentsFramework,
|
|
2536
|
+
detectFrameworkFromManifest,
|
|
2537
|
+
generateAgentsMd,
|
|
2538
|
+
agentsCommand,
|
|
2539
|
+
generateTypes,
|
|
2540
|
+
DEPLOY_PLATFORMS,
|
|
2541
|
+
IMPLEMENTED_DEPLOY_PLATFORMS,
|
|
2542
|
+
isImplementedDeployPlatform,
|
|
2543
|
+
isDeployPlatform,
|
|
2544
|
+
buildClient,
|
|
2545
|
+
bundleServer,
|
|
2546
|
+
generateFlyToml,
|
|
2547
|
+
writeFlyTomlArtifact,
|
|
2548
|
+
FlyAdapter,
|
|
2549
|
+
NodeFlyCommandRunner,
|
|
2550
|
+
generateRailwayJson,
|
|
2551
|
+
writeRailwayJsonArtifact,
|
|
2552
|
+
RailwayAdapter,
|
|
2553
|
+
NodeRailwayCommandRunner,
|
|
2554
|
+
StubDeployAdapter,
|
|
2555
|
+
createDeployAdapter,
|
|
2556
|
+
generateDockerfile,
|
|
2557
|
+
generateDeployPackageJson,
|
|
2558
|
+
generateDockerIgnore,
|
|
2559
|
+
writeDockerfileArtifact,
|
|
2560
|
+
writeDockerIgnoreArtifact,
|
|
2561
|
+
resolveDeployDirectory,
|
|
2562
|
+
resolveDeployStatePath,
|
|
2563
|
+
readDeployState,
|
|
2564
|
+
writeDeployState,
|
|
2565
|
+
updateDeployState,
|
|
2566
|
+
resetDeployState,
|
|
2567
|
+
deployCommand
|
|
2568
|
+
};
|
|
2569
|
+
//# sourceMappingURL=chunk-TVIRMDER.js.map
|