@korajs/cli 0.0.0-canary-20261008174129
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +44 -0
- package/dist/bin.cjs +9465 -0
- package/dist/bin.cjs.map +1 -0
- package/dist/bin.js +3150 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-5NI2FEQL.js +92 -0
- package/dist/chunk-5NI2FEQL.js.map +1 -0
- package/dist/chunk-6C4BHSRA.js +82 -0
- package/dist/chunk-6C4BHSRA.js.map +1 -0
- package/dist/chunk-7Y4J2GXP.js +796 -0
- package/dist/chunk-7Y4J2GXP.js.map +1 -0
- package/dist/chunk-BURFQVMV.js +719 -0
- package/dist/chunk-BURFQVMV.js.map +1 -0
- package/dist/chunk-TVIRMDER.js +2569 -0
- package/dist/chunk-TVIRMDER.js.map +1 -0
- package/dist/create.cjs +1368 -0
- package/dist/create.cjs.map +1 -0
- package/dist/create.js +11 -0
- package/dist/create.js.map +1 -0
- package/dist/index.cjs +3489 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +772 -0
- package/dist/index.d.ts +772 -0
- package/dist/index.js +129 -0
- package/dist/index.js.map +1 -0
- package/dist/lab-manager-KUDII6BT.js +280 -0
- package/dist/lab-manager-KUDII6BT.js.map +1 -0
- package/dist/schema-loader-GGHECMTM.js +9 -0
- package/dist/schema-loader-GGHECMTM.js.map +1 -0
- package/dist/spectator-manager-FTH2NFSE.js +145 -0
- package/dist/spectator-manager-FTH2NFSE.js.map +1 -0
- package/dist/studio-server-XLGZNGG5.js +1747 -0
- package/dist/studio-server-XLGZNGG5.js.map +1 -0
- package/dist/vite.cjs +351 -0
- package/dist/vite.cjs.map +1 -0
- package/dist/vite.d.cts +161 -0
- package/dist/vite.d.ts +161 -0
- package/dist/vite.js +321 -0
- package/dist/vite.js.map +1 -0
- package/package.json +79 -0
- package/templates/react-basic/.env.example +3 -0
- package/templates/react-basic/AGENTS.md +87 -0
- package/templates/react-basic/README.md.hbs +62 -0
- package/templates/react-basic/index.html.hbs +12 -0
- package/templates/react-basic/kora.config.ts +13 -0
- package/templates/react-basic/package.json.hbs +35 -0
- package/templates/react-basic/src/App.tsx +132 -0
- package/templates/react-basic/src/index.css +287 -0
- package/templates/react-basic/src/kora-worker.ts +9 -0
- package/templates/react-basic/src/main.tsx +24 -0
- package/templates/react-basic/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/react-basic/src/modules/todos/todo.queries.ts +5 -0
- package/templates/react-basic/src/modules/todos/todo.schema.ts +10 -0
- package/templates/react-basic/src/modules/todos/useTodos.ts +31 -0
- package/templates/react-basic/src/schema.ts +9 -0
- package/templates/react-basic/src/vite-env.d.ts +18 -0
- package/templates/react-basic/tsconfig.json.hbs +14 -0
- package/templates/react-basic/vite.config.ts +88 -0
- package/templates/react-sync/.env.example +25 -0
- package/templates/react-sync/AGENTS.md +87 -0
- package/templates/react-sync/README.md.hbs +106 -0
- package/templates/react-sync/index.html.hbs +12 -0
- package/templates/react-sync/kora.config.ts +17 -0
- package/templates/react-sync/package.json.hbs +44 -0
- package/templates/react-sync/server.ts +141 -0
- package/templates/react-sync/src/App.tsx +160 -0
- package/templates/react-sync/src/auth.ts +25 -0
- package/templates/react-sync/src/index.css +350 -0
- package/templates/react-sync/src/kora-worker.ts +9 -0
- package/templates/react-sync/src/main.tsx +52 -0
- package/templates/react-sync/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/react-sync/src/modules/todos/todo.queries.ts +5 -0
- package/templates/react-sync/src/modules/todos/todo.schema.ts +10 -0
- package/templates/react-sync/src/modules/todos/useTodos.ts +31 -0
- package/templates/react-sync/src/schema.ts +9 -0
- package/templates/react-sync/src/vite-env.d.ts +19 -0
- package/templates/react-sync/tsconfig.json.hbs +14 -0
- package/templates/react-sync/vite.config.ts +105 -0
- package/templates/react-tailwind/.env.example +3 -0
- package/templates/react-tailwind/AGENTS.md +87 -0
- package/templates/react-tailwind/README.md.hbs +62 -0
- package/templates/react-tailwind/index.html.hbs +12 -0
- package/templates/react-tailwind/kora.config.ts +13 -0
- package/templates/react-tailwind/package.json.hbs +38 -0
- package/templates/react-tailwind/src/App.tsx +201 -0
- package/templates/react-tailwind/src/index.css +7 -0
- package/templates/react-tailwind/src/kora-worker.ts +9 -0
- package/templates/react-tailwind/src/main.tsx +31 -0
- package/templates/react-tailwind/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/react-tailwind/src/modules/todos/todo.queries.ts +5 -0
- package/templates/react-tailwind/src/modules/todos/todo.schema.ts +10 -0
- package/templates/react-tailwind/src/modules/todos/useTodos.ts +31 -0
- package/templates/react-tailwind/src/schema.ts +9 -0
- package/templates/react-tailwind/src/vite-env.d.ts +18 -0
- package/templates/react-tailwind/tsconfig.json.hbs +14 -0
- package/templates/react-tailwind/vite.config.ts +95 -0
- package/templates/react-tailwind-sync/.env.example +25 -0
- package/templates/react-tailwind-sync/AGENTS.md +89 -0
- package/templates/react-tailwind-sync/README.md.hbs +106 -0
- package/templates/react-tailwind-sync/index.html.hbs +12 -0
- package/templates/react-tailwind-sync/kora.config.ts +17 -0
- package/templates/react-tailwind-sync/package.json.hbs +47 -0
- package/templates/react-tailwind-sync/server.ts +141 -0
- package/templates/react-tailwind-sync/src/App.tsx +293 -0
- package/templates/react-tailwind-sync/src/auth.ts +25 -0
- package/templates/react-tailwind-sync/src/index.css +7 -0
- package/templates/react-tailwind-sync/src/kora-worker.ts +9 -0
- package/templates/react-tailwind-sync/src/kora.ts +32 -0
- package/templates/react-tailwind-sync/src/main.tsx +44 -0
- package/templates/react-tailwind-sync/src/modules/todos/todo.mutations.ts +21 -0
- package/templates/react-tailwind-sync/src/modules/todos/todo.queries.ts +5 -0
- package/templates/react-tailwind-sync/src/modules/todos/todo.schema.ts +10 -0
- package/templates/react-tailwind-sync/src/modules/todos/useTodos.ts +31 -0
- package/templates/react-tailwind-sync/src/schema.ts +9 -0
- package/templates/react-tailwind-sync/src/vite-env.d.ts +19 -0
- package/templates/react-tailwind-sync/tsconfig.json.hbs +14 -0
- package/templates/react-tailwind-sync/vite.config.ts +112 -0
- package/templates/svelte-basic/.env.example +3 -0
- package/templates/svelte-basic/AGENTS.md +76 -0
- package/templates/svelte-basic/README.md.hbs +62 -0
- package/templates/svelte-basic/index.html.hbs +12 -0
- package/templates/svelte-basic/kora.config.ts +12 -0
- package/templates/svelte-basic/package.json.hbs +33 -0
- package/templates/svelte-basic/src/App.svelte +128 -0
- package/templates/svelte-basic/src/Root.svelte +14 -0
- package/templates/svelte-basic/src/index.css +287 -0
- package/templates/svelte-basic/src/kora-worker.ts +9 -0
- package/templates/svelte-basic/src/main.ts +16 -0
- package/templates/svelte-basic/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/svelte-basic/src/modules/todos/todo.queries.ts +5 -0
- package/templates/svelte-basic/src/modules/todos/todo.schema.ts +10 -0
- package/templates/svelte-basic/src/modules/todos/useTodos.ts +25 -0
- package/templates/svelte-basic/src/schema.ts +9 -0
- package/templates/svelte-basic/src/vite-env.d.ts +12 -0
- package/templates/svelte-basic/svelte.config.js +5 -0
- package/templates/svelte-basic/tsconfig.json.hbs +14 -0
- package/templates/svelte-basic/vite.config.ts +57 -0
- package/templates/svelte-sync/.env.example +25 -0
- package/templates/svelte-sync/AGENTS.md +76 -0
- package/templates/svelte-sync/README.md.hbs +106 -0
- package/templates/svelte-sync/index.html.hbs +12 -0
- package/templates/svelte-sync/kora.config.ts +17 -0
- package/templates/svelte-sync/package.json.hbs +43 -0
- package/templates/svelte-sync/server.ts +141 -0
- package/templates/svelte-sync/src/App.svelte +151 -0
- package/templates/svelte-sync/src/Root.svelte +22 -0
- package/templates/svelte-sync/src/auth.ts +25 -0
- package/templates/svelte-sync/src/index.css +339 -0
- package/templates/svelte-sync/src/kora-worker.ts +9 -0
- package/templates/svelte-sync/src/main.ts +32 -0
- package/templates/svelte-sync/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/svelte-sync/src/modules/todos/todo.queries.ts +5 -0
- package/templates/svelte-sync/src/modules/todos/todo.schema.ts +10 -0
- package/templates/svelte-sync/src/modules/todos/useTodos.ts +25 -0
- package/templates/svelte-sync/src/schema.ts +9 -0
- package/templates/svelte-sync/src/vite-env.d.ts +22 -0
- package/templates/svelte-sync/svelte.config.js +5 -0
- package/templates/svelte-sync/tsconfig.json.hbs +14 -0
- package/templates/svelte-sync/vite.config.ts +91 -0
- package/templates/svelte-tailwind/AGENTS.md +76 -0
- package/templates/svelte-tailwind/README.md.hbs +62 -0
- package/templates/svelte-tailwind/index.html.hbs +12 -0
- package/templates/svelte-tailwind/kora.config.ts +12 -0
- package/templates/svelte-tailwind/package.json.hbs +36 -0
- package/templates/svelte-tailwind/src/App.svelte +76 -0
- package/templates/svelte-tailwind/src/Root.svelte +14 -0
- package/templates/svelte-tailwind/src/index.css +7 -0
- package/templates/svelte-tailwind/src/kora-worker.ts +9 -0
- package/templates/svelte-tailwind/src/main.ts +16 -0
- package/templates/svelte-tailwind/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/svelte-tailwind/src/modules/todos/todo.queries.ts +5 -0
- package/templates/svelte-tailwind/src/modules/todos/todo.schema.ts +10 -0
- package/templates/svelte-tailwind/src/modules/todos/useTodos.ts +25 -0
- package/templates/svelte-tailwind/src/schema.ts +9 -0
- package/templates/svelte-tailwind/src/vite-env.d.ts +12 -0
- package/templates/svelte-tailwind/svelte.config.js +5 -0
- package/templates/svelte-tailwind/tsconfig.json.hbs +14 -0
- package/templates/svelte-tailwind/vite.config.ts +64 -0
- package/templates/svelte-tailwind-sync/.env.example +25 -0
- package/templates/svelte-tailwind-sync/AGENTS.md +76 -0
- package/templates/svelte-tailwind-sync/README.md.hbs +106 -0
- package/templates/svelte-tailwind-sync/index.html.hbs +12 -0
- package/templates/svelte-tailwind-sync/kora.config.ts +17 -0
- package/templates/svelte-tailwind-sync/package.json.hbs +46 -0
- package/templates/svelte-tailwind-sync/server.ts +141 -0
- package/templates/svelte-tailwind-sync/src/App.svelte +193 -0
- package/templates/svelte-tailwind-sync/src/Root.svelte +26 -0
- package/templates/svelte-tailwind-sync/src/auth.ts +25 -0
- package/templates/svelte-tailwind-sync/src/index.css +7 -0
- package/templates/svelte-tailwind-sync/src/kora-worker.ts +9 -0
- package/templates/svelte-tailwind-sync/src/main.ts +32 -0
- package/templates/svelte-tailwind-sync/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/svelte-tailwind-sync/src/modules/todos/todo.queries.ts +5 -0
- package/templates/svelte-tailwind-sync/src/modules/todos/todo.schema.ts +10 -0
- package/templates/svelte-tailwind-sync/src/modules/todos/useTodos.ts +25 -0
- package/templates/svelte-tailwind-sync/src/schema.ts +9 -0
- package/templates/svelte-tailwind-sync/src/vite-env.d.ts +22 -0
- package/templates/svelte-tailwind-sync/svelte.config.js +5 -0
- package/templates/svelte-tailwind-sync/tsconfig.json.hbs +14 -0
- package/templates/svelte-tailwind-sync/vite.config.ts +98 -0
- package/templates/tauri-react/.env.example +27 -0
- package/templates/tauri-react/.github/workflows/release-desktop.yml +107 -0
- package/templates/tauri-react/AGENTS.md +91 -0
- package/templates/tauri-react/README.md.hbs +183 -0
- package/templates/tauri-react/dev.ts +37 -0
- package/templates/tauri-react/index.html.hbs +16 -0
- package/templates/tauri-react/kora.config.ts +17 -0
- package/templates/tauri-react/package.json.hbs +46 -0
- package/templates/tauri-react/server.ts +148 -0
- package/templates/tauri-react/src/App.tsx +803 -0
- package/templates/tauri-react/src/AppShell.tsx +158 -0
- package/templates/tauri-react/src/SetupScreen.tsx +288 -0
- package/templates/tauri-react/src/auth.ts +45 -0
- package/templates/tauri-react/src/main.tsx +9 -0
- package/templates/tauri-react/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/tauri-react/src/modules/todos/todo.queries.ts +5 -0
- package/templates/tauri-react/src/modules/todos/todo.schema.ts +10 -0
- package/templates/tauri-react/src/modules/todos/useTodos.ts +31 -0
- package/templates/tauri-react/src/schema.ts +9 -0
- package/templates/tauri-react/src/sync-config.ts +103 -0
- package/templates/tauri-react/src/updater.ts +38 -0
- package/templates/tauri-react/src/vite-env.d.ts +19 -0
- package/templates/tauri-react/src-tauri/Cargo.toml +19 -0
- package/templates/tauri-react/src-tauri/build.rs +3 -0
- package/templates/tauri-react/src-tauri/capabilities/default.json +12 -0
- package/templates/tauri-react/src-tauri/icons/128x128.png +0 -0
- package/templates/tauri-react/src-tauri/icons/128x128@2x.png +0 -0
- package/templates/tauri-react/src-tauri/icons/32x32.png +0 -0
- package/templates/tauri-react/src-tauri/icons/icon.icns +0 -0
- package/templates/tauri-react/src-tauri/icons/icon.ico +0 -0
- package/templates/tauri-react/src-tauri/src/lib.rs +8 -0
- package/templates/tauri-react/src-tauri/src/main.rs +6 -0
- package/templates/tauri-react/src-tauri/tauri.conf.json +44 -0
- package/templates/tauri-react/tsconfig.json.hbs +14 -0
- package/templates/tauri-react/vite.config.ts +15 -0
- package/templates/vue-basic/AGENTS.md +84 -0
- package/templates/vue-basic/README.md.hbs +62 -0
- package/templates/vue-basic/index.html.hbs +12 -0
- package/templates/vue-basic/kora.config.ts +12 -0
- package/templates/vue-basic/package.json.hbs +32 -0
- package/templates/vue-basic/src/App.vue +117 -0
- package/templates/vue-basic/src/index.css +287 -0
- package/templates/vue-basic/src/kora-worker.ts +9 -0
- package/templates/vue-basic/src/main.ts +20 -0
- package/templates/vue-basic/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/vue-basic/src/modules/todos/todo.queries.ts +5 -0
- package/templates/vue-basic/src/modules/todos/todo.schema.ts +10 -0
- package/templates/vue-basic/src/modules/todos/useTodos.ts +32 -0
- package/templates/vue-basic/src/schema.ts +9 -0
- package/templates/vue-basic/src/vite-env.d.ts +17 -0
- package/templates/vue-basic/tsconfig.json.hbs +15 -0
- package/templates/vue-basic/vite.config.ts +74 -0
- package/templates/vue-sync/.env.example +25 -0
- package/templates/vue-sync/AGENTS.md +84 -0
- package/templates/vue-sync/README.md.hbs +106 -0
- package/templates/vue-sync/index.html.hbs +12 -0
- package/templates/vue-sync/kora.config.ts +17 -0
- package/templates/vue-sync/package.json.hbs +42 -0
- package/templates/vue-sync/server.ts +141 -0
- package/templates/vue-sync/src/App.vue +135 -0
- package/templates/vue-sync/src/auth.ts +25 -0
- package/templates/vue-sync/src/index.css +339 -0
- package/templates/vue-sync/src/kora-worker.ts +9 -0
- package/templates/vue-sync/src/main.ts +47 -0
- package/templates/vue-sync/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/vue-sync/src/modules/todos/todo.queries.ts +5 -0
- package/templates/vue-sync/src/modules/todos/todo.schema.ts +10 -0
- package/templates/vue-sync/src/modules/todos/useTodos.ts +32 -0
- package/templates/vue-sync/src/schema.ts +9 -0
- package/templates/vue-sync/src/vite-env.d.ts +27 -0
- package/templates/vue-sync/tsconfig.json.hbs +15 -0
- package/templates/vue-sync/vite.config.ts +91 -0
- package/templates/vue-tailwind/AGENTS.md +84 -0
- package/templates/vue-tailwind/README.md.hbs +62 -0
- package/templates/vue-tailwind/index.html.hbs +12 -0
- package/templates/vue-tailwind/kora.config.ts +12 -0
- package/templates/vue-tailwind/package.json.hbs +35 -0
- package/templates/vue-tailwind/src/App.vue +135 -0
- package/templates/vue-tailwind/src/index.css +7 -0
- package/templates/vue-tailwind/src/kora-worker.ts +9 -0
- package/templates/vue-tailwind/src/main.ts +31 -0
- package/templates/vue-tailwind/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/vue-tailwind/src/modules/todos/todo.queries.ts +5 -0
- package/templates/vue-tailwind/src/modules/todos/todo.schema.ts +10 -0
- package/templates/vue-tailwind/src/modules/todos/useTodos.ts +32 -0
- package/templates/vue-tailwind/src/schema.ts +9 -0
- package/templates/vue-tailwind/src/vite-env.d.ts +17 -0
- package/templates/vue-tailwind/tsconfig.json.hbs +15 -0
- package/templates/vue-tailwind/vite.config.ts +75 -0
- package/templates/vue-tailwind-sync/.env.example +25 -0
- package/templates/vue-tailwind-sync/AGENTS.md +84 -0
- package/templates/vue-tailwind-sync/README.md.hbs +106 -0
- package/templates/vue-tailwind-sync/index.html.hbs +12 -0
- package/templates/vue-tailwind-sync/kora.config.ts +17 -0
- package/templates/vue-tailwind-sync/package.json.hbs +45 -0
- package/templates/vue-tailwind-sync/server.ts +141 -0
- package/templates/vue-tailwind-sync/src/App.vue +245 -0
- package/templates/vue-tailwind-sync/src/auth.ts +25 -0
- package/templates/vue-tailwind-sync/src/index.css +7 -0
- package/templates/vue-tailwind-sync/src/kora-worker.ts +9 -0
- package/templates/vue-tailwind-sync/src/main.ts +56 -0
- package/templates/vue-tailwind-sync/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/vue-tailwind-sync/src/modules/todos/todo.queries.ts +5 -0
- package/templates/vue-tailwind-sync/src/modules/todos/todo.schema.ts +10 -0
- package/templates/vue-tailwind-sync/src/modules/todos/useTodos.ts +32 -0
- package/templates/vue-tailwind-sync/src/schema.ts +9 -0
- package/templates/vue-tailwind-sync/src/vite-env.d.ts +27 -0
- package/templates/vue-tailwind-sync/tsconfig.json.hbs +15 -0
- package/templates/vue-tailwind-sync/vite.config.ts +92 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3489 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
AGENTS_FRAMEWORKS: () => AGENTS_FRAMEWORKS,
|
|
34
|
+
ClackPromptClient: () => ClackPromptClient,
|
|
35
|
+
CliError: () => CliError,
|
|
36
|
+
DEPLOY_PLATFORMS: () => DEPLOY_PLATFORMS,
|
|
37
|
+
DeployPlatformUnavailableError: () => DeployPlatformUnavailableError,
|
|
38
|
+
DevServerError: () => DevServerError,
|
|
39
|
+
FlyAdapter: () => FlyAdapter,
|
|
40
|
+
IMPLEMENTED_DEPLOY_PLATFORMS: () => IMPLEMENTED_DEPLOY_PLATFORMS,
|
|
41
|
+
InvalidProjectError: () => InvalidProjectError,
|
|
42
|
+
NodeFlyCommandRunner: () => NodeFlyCommandRunner,
|
|
43
|
+
NodeRailwayCommandRunner: () => NodeRailwayCommandRunner,
|
|
44
|
+
PACKAGE_MANAGERS: () => PACKAGE_MANAGERS,
|
|
45
|
+
PreferenceStore: () => PreferenceStore,
|
|
46
|
+
ProjectExistsError: () => ProjectExistsError,
|
|
47
|
+
PromptCancelledError: () => PromptCancelledError,
|
|
48
|
+
RailwayAdapter: () => RailwayAdapter,
|
|
49
|
+
ReadlinePromptClient: () => ReadlinePromptClient,
|
|
50
|
+
SchemaNotFoundError: () => SchemaNotFoundError,
|
|
51
|
+
StubDeployAdapter: () => StubDeployAdapter,
|
|
52
|
+
TEMPLATES: () => TEMPLATES,
|
|
53
|
+
TEMPLATE_INFO: () => TEMPLATE_INFO,
|
|
54
|
+
agentsCommand: () => agentsCommand,
|
|
55
|
+
applySyncProviderPreset: () => applySyncProviderPreset,
|
|
56
|
+
buildClient: () => buildClient,
|
|
57
|
+
bundleServer: () => bundleServer,
|
|
58
|
+
createDeployAdapter: () => createDeployAdapter,
|
|
59
|
+
createPromptClient: () => createPromptClient,
|
|
60
|
+
deployCommand: () => deployCommand,
|
|
61
|
+
detectFrameworkFromManifest: () => detectFrameworkFromManifest,
|
|
62
|
+
determineTemplateFromSelections: () => determineTemplateFromSelections,
|
|
63
|
+
generateAgentsMd: () => generateAgentsMd,
|
|
64
|
+
generateDeployPackageJson: () => generateDeployPackageJson,
|
|
65
|
+
generateDockerIgnore: () => generateDockerIgnore,
|
|
66
|
+
generateDockerfile: () => generateDockerfile,
|
|
67
|
+
generateFlyToml: () => generateFlyToml,
|
|
68
|
+
generateRailwayJson: () => generateRailwayJson,
|
|
69
|
+
generateTypes: () => generateTypes,
|
|
70
|
+
getCreatePreferencesOrDefault: () => getCreatePreferencesOrDefault,
|
|
71
|
+
getDefaultCreatePreferences: () => getDefaultCreatePreferences,
|
|
72
|
+
isAgentsFramework: () => isAgentsFramework,
|
|
73
|
+
isAuthValue: () => isAuthValue,
|
|
74
|
+
isDatabaseProviderValue: () => isDatabaseProviderValue,
|
|
75
|
+
isDatabaseValue: () => isDatabaseValue,
|
|
76
|
+
isDeployPlatform: () => isDeployPlatform,
|
|
77
|
+
isFrameworkValue: () => isFrameworkValue,
|
|
78
|
+
isImplementedDeployPlatform: () => isImplementedDeployPlatform,
|
|
79
|
+
readDeployState: () => readDeployState,
|
|
80
|
+
resetDeployState: () => resetDeployState,
|
|
81
|
+
resolveCreatePreferencesFlow: () => resolveCreatePreferencesFlow,
|
|
82
|
+
resolveDeployDirectory: () => resolveDeployDirectory,
|
|
83
|
+
resolveDeployStatePath: () => resolveDeployStatePath,
|
|
84
|
+
saveResolvedPreferences: () => saveResolvedPreferences,
|
|
85
|
+
shouldSavePreferences: () => shouldSavePreferences,
|
|
86
|
+
updateDeployState: () => updateDeployState,
|
|
87
|
+
validateProjectName: () => validateProjectName,
|
|
88
|
+
writeDeployState: () => writeDeployState,
|
|
89
|
+
writeDockerIgnoreArtifact: () => writeDockerIgnoreArtifact,
|
|
90
|
+
writeDockerfileArtifact: () => writeDockerfileArtifact,
|
|
91
|
+
writeFlyTomlArtifact: () => writeFlyTomlArtifact,
|
|
92
|
+
writeRailwayJsonArtifact: () => writeRailwayJsonArtifact
|
|
93
|
+
});
|
|
94
|
+
module.exports = __toCommonJS(src_exports);
|
|
95
|
+
|
|
96
|
+
// src/types.ts
|
|
97
|
+
var PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"];
|
|
98
|
+
var TEMPLATES = [
|
|
99
|
+
"react-tailwind-sync",
|
|
100
|
+
"react-tailwind",
|
|
101
|
+
"react-sync",
|
|
102
|
+
"react-basic",
|
|
103
|
+
"vue-sync",
|
|
104
|
+
"vue-basic",
|
|
105
|
+
"vue-tailwind-sync",
|
|
106
|
+
"vue-tailwind",
|
|
107
|
+
"svelte-sync",
|
|
108
|
+
"svelte-basic",
|
|
109
|
+
"svelte-tailwind-sync",
|
|
110
|
+
"svelte-tailwind",
|
|
111
|
+
"tauri-react"
|
|
112
|
+
];
|
|
113
|
+
var TEMPLATE_INFO = [
|
|
114
|
+
{
|
|
115
|
+
name: "react-tailwind-sync",
|
|
116
|
+
label: "React + Tailwind (with sync)",
|
|
117
|
+
description: "Polished dark-themed app with Tailwind CSS and sync server (Recommended)"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "react-tailwind",
|
|
121
|
+
label: "React + Tailwind (local-only)",
|
|
122
|
+
description: "Polished dark-themed app with Tailwind CSS \u2014 no sync server"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "react-sync",
|
|
126
|
+
label: "React + CSS (with sync)",
|
|
127
|
+
description: "Clean CSS app with sync server included"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "react-basic",
|
|
131
|
+
label: "React + CSS (local-only)",
|
|
132
|
+
description: "Clean CSS app \u2014 no sync server"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "vue-sync",
|
|
136
|
+
label: "Vue 3 + CSS (with sync)",
|
|
137
|
+
description: "Vue composables with sync server included"
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: "vue-basic",
|
|
141
|
+
label: "Vue 3 + CSS (local-only)",
|
|
142
|
+
description: "Vue composables \u2014 no sync server"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "vue-tailwind-sync",
|
|
146
|
+
label: "Vue 3 + Tailwind (with sync)",
|
|
147
|
+
description: "Polished Vue app with Tailwind CSS and sync server"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "vue-tailwind",
|
|
151
|
+
label: "Vue 3 + Tailwind (local-only)",
|
|
152
|
+
description: "Polished Vue app with Tailwind CSS \u2014 no sync server"
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "svelte-sync",
|
|
156
|
+
label: "Svelte 5 + CSS (with sync)",
|
|
157
|
+
description: "Svelte stores with sync server included"
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "svelte-basic",
|
|
161
|
+
label: "Svelte 5 + CSS (local-only)",
|
|
162
|
+
description: "Svelte stores \u2014 no sync server"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: "svelte-tailwind-sync",
|
|
166
|
+
label: "Svelte 5 + Tailwind (with sync)",
|
|
167
|
+
description: "Polished Svelte app with Tailwind CSS and sync server"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: "svelte-tailwind",
|
|
171
|
+
label: "Svelte 5 + Tailwind (local-only)",
|
|
172
|
+
description: "Polished Svelte app with Tailwind CSS \u2014 no sync server"
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: "tauri-react",
|
|
176
|
+
label: "Tauri Desktop (native SQLite)",
|
|
177
|
+
description: "Desktop app with native SQLite \u2014 no WASM, includes sync server"
|
|
178
|
+
}
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
// src/errors.ts
|
|
182
|
+
var import_core = require("@korajs/core");
|
|
183
|
+
var CliError = class extends import_core.KoraError {
|
|
184
|
+
constructor(message, context) {
|
|
185
|
+
super(message, "CLI_ERROR", context);
|
|
186
|
+
this.name = "CliError";
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
var DeployPlatformUnavailableError = class extends import_core.KoraError {
|
|
190
|
+
constructor(platform, available) {
|
|
191
|
+
super(
|
|
192
|
+
`Deploying to "${platform}" is coming soon and not available yet. Choose one of: ${available.join(", ")}. To self-host, run the production server (\`pnpm build && pnpm start\`) in any Node 20+ environment.`,
|
|
193
|
+
"DEPLOY_PLATFORM_UNAVAILABLE",
|
|
194
|
+
{ platform, available }
|
|
195
|
+
);
|
|
196
|
+
this.platform = platform;
|
|
197
|
+
this.available = available;
|
|
198
|
+
this.name = "DeployPlatformUnavailableError";
|
|
199
|
+
}
|
|
200
|
+
platform;
|
|
201
|
+
available;
|
|
202
|
+
};
|
|
203
|
+
var ProjectExistsError = class extends import_core.KoraError {
|
|
204
|
+
constructor(directory) {
|
|
205
|
+
super(
|
|
206
|
+
`Directory "${directory}" already exists. Choose a different name or remove the existing directory.`,
|
|
207
|
+
"PROJECT_EXISTS",
|
|
208
|
+
{ directory }
|
|
209
|
+
);
|
|
210
|
+
this.directory = directory;
|
|
211
|
+
this.name = "ProjectExistsError";
|
|
212
|
+
}
|
|
213
|
+
directory;
|
|
214
|
+
};
|
|
215
|
+
var SchemaNotFoundError = class extends import_core.KoraError {
|
|
216
|
+
constructor(searchedPaths) {
|
|
217
|
+
super(
|
|
218
|
+
`Could not find a schema file. Searched: ${searchedPaths.join(", ")}. Create a schema file using defineSchema() from @korajs/core.`,
|
|
219
|
+
"SCHEMA_NOT_FOUND",
|
|
220
|
+
{ searchedPaths }
|
|
221
|
+
);
|
|
222
|
+
this.searchedPaths = searchedPaths;
|
|
223
|
+
this.name = "SchemaNotFoundError";
|
|
224
|
+
}
|
|
225
|
+
searchedPaths;
|
|
226
|
+
};
|
|
227
|
+
var InvalidProjectError = class extends import_core.KoraError {
|
|
228
|
+
constructor(directory) {
|
|
229
|
+
super(
|
|
230
|
+
`"${directory}" is not a valid Kora project. No package.json with a kora dependency found. Run this command from inside a Kora project.`,
|
|
231
|
+
"INVALID_PROJECT",
|
|
232
|
+
{ directory }
|
|
233
|
+
);
|
|
234
|
+
this.directory = directory;
|
|
235
|
+
this.name = "InvalidProjectError";
|
|
236
|
+
}
|
|
237
|
+
directory;
|
|
238
|
+
};
|
|
239
|
+
var DevServerError = class extends import_core.KoraError {
|
|
240
|
+
constructor(binary, searchPath) {
|
|
241
|
+
super(
|
|
242
|
+
`Could not find required binary "${binary}" at ${searchPath}. Install project dependencies and try again.`,
|
|
243
|
+
"DEV_SERVER_ERROR",
|
|
244
|
+
{ binary, searchPath }
|
|
245
|
+
);
|
|
246
|
+
this.binary = binary;
|
|
247
|
+
this.searchPath = searchPath;
|
|
248
|
+
this.name = "DevServerError";
|
|
249
|
+
}
|
|
250
|
+
binary;
|
|
251
|
+
searchPath;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// src/commands/agents/agents-command.ts
|
|
255
|
+
var import_promises2 = require("fs/promises");
|
|
256
|
+
var import_node_path2 = require("path");
|
|
257
|
+
var import_citty = require("citty");
|
|
258
|
+
|
|
259
|
+
// src/utils/fs-helpers.ts
|
|
260
|
+
var import_promises = require("fs/promises");
|
|
261
|
+
var import_node_path = require("path");
|
|
262
|
+
async function findProjectRoot(startDir) {
|
|
263
|
+
let current = (0, import_node_path.resolve)(startDir ?? process.cwd());
|
|
264
|
+
for (; ; ) {
|
|
265
|
+
const pkgPath = (0, import_node_path.join)(current, "package.json");
|
|
266
|
+
try {
|
|
267
|
+
const content = await (0, import_promises.readFile)(pkgPath, "utf-8");
|
|
268
|
+
const pkg = JSON.parse(content);
|
|
269
|
+
if (isKoraProject(pkg)) {
|
|
270
|
+
return current;
|
|
271
|
+
}
|
|
272
|
+
} catch {
|
|
273
|
+
}
|
|
274
|
+
const parent = (0, import_node_path.dirname)(current);
|
|
275
|
+
if (parent === current) break;
|
|
276
|
+
current = parent;
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
async function resolveProjectBinaryEntryPoint(projectRoot, packageName, binaryName) {
|
|
281
|
+
const pkgJsonPath = (0, import_node_path.join)(projectRoot, "node_modules", packageName, "package.json");
|
|
282
|
+
try {
|
|
283
|
+
const content = await (0, import_promises.readFile)(pkgJsonPath, "utf-8");
|
|
284
|
+
const pkg = JSON.parse(content);
|
|
285
|
+
let binPath;
|
|
286
|
+
if (typeof pkg.bin === "string") {
|
|
287
|
+
binPath = pkg.bin;
|
|
288
|
+
} else if (typeof pkg.bin === "object" && pkg.bin !== null) {
|
|
289
|
+
binPath = pkg.bin[binaryName];
|
|
290
|
+
}
|
|
291
|
+
if (!binPath) return null;
|
|
292
|
+
const fullPath = (0, import_node_path.join)(projectRoot, "node_modules", packageName, binPath);
|
|
293
|
+
await (0, import_promises.access)(fullPath);
|
|
294
|
+
return fullPath;
|
|
295
|
+
} catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function isKoraProject(pkg) {
|
|
300
|
+
if (typeof pkg !== "object" || pkg === null) return false;
|
|
301
|
+
const record = pkg;
|
|
302
|
+
return hasKoraDep(record.dependencies) || hasKoraDep(record.devDependencies);
|
|
303
|
+
}
|
|
304
|
+
function hasKoraDep(deps) {
|
|
305
|
+
if (typeof deps !== "object" || deps === null) return false;
|
|
306
|
+
return Object.keys(deps).some((key) => key === "kora" || key.startsWith("@korajs/"));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/utils/logger.ts
|
|
310
|
+
var RESET = "\x1B[0m";
|
|
311
|
+
var BOLD = "\x1B[1m";
|
|
312
|
+
var DIM = "\x1B[2m";
|
|
313
|
+
var GREEN = "\x1B[32m";
|
|
314
|
+
var YELLOW = "\x1B[33m";
|
|
315
|
+
var RED = "\x1B[31m";
|
|
316
|
+
var CYAN = "\x1B[36m";
|
|
317
|
+
function createLogger(options) {
|
|
318
|
+
const colorDisabled = options?.noColor === true || process.env.NO_COLOR !== void 0 || !process.stdout.isTTY;
|
|
319
|
+
function color(code, text) {
|
|
320
|
+
return colorDisabled ? text : `${code}${text}${RESET}`;
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
info(message) {
|
|
324
|
+
console.log(color(CYAN, message));
|
|
325
|
+
},
|
|
326
|
+
success(message) {
|
|
327
|
+
console.log(color(GREEN, ` \u2713 ${message}`));
|
|
328
|
+
},
|
|
329
|
+
warn(message) {
|
|
330
|
+
console.warn(color(YELLOW, ` \u26A0 ${message}`));
|
|
331
|
+
},
|
|
332
|
+
error(message) {
|
|
333
|
+
console.error(color(RED, ` \u2717 ${message}`));
|
|
334
|
+
},
|
|
335
|
+
step(message) {
|
|
336
|
+
console.log(color(DIM, ` ${message}`));
|
|
337
|
+
},
|
|
338
|
+
blank() {
|
|
339
|
+
console.log();
|
|
340
|
+
},
|
|
341
|
+
banner() {
|
|
342
|
+
console.log();
|
|
343
|
+
console.log(
|
|
344
|
+
color(BOLD + CYAN, " Kora.js") + color(DIM, ", the offline-first application framework")
|
|
345
|
+
);
|
|
346
|
+
console.log();
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/commands/agents/agents-md-content.ts
|
|
352
|
+
var AGENTS_FRAMEWORKS = ["react", "vue", "svelte"];
|
|
353
|
+
function isAgentsFramework(value) {
|
|
354
|
+
return AGENTS_FRAMEWORKS.includes(value);
|
|
355
|
+
}
|
|
356
|
+
function detectFrameworkFromManifest(manifest) {
|
|
357
|
+
if (typeof manifest !== "object" || manifest === null) return null;
|
|
358
|
+
const record = manifest;
|
|
359
|
+
const deps = collectDependencyNames(record.dependencies, record.devDependencies);
|
|
360
|
+
if (deps.has("@korajs/react")) return "react";
|
|
361
|
+
if (deps.has("@korajs/vue")) return "vue";
|
|
362
|
+
if (deps.has("@korajs/svelte")) return "svelte";
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
function collectDependencyNames(...groups) {
|
|
366
|
+
const names = /* @__PURE__ */ new Set();
|
|
367
|
+
for (const group of groups) {
|
|
368
|
+
if (typeof group !== "object" || group === null) continue;
|
|
369
|
+
for (const name of Object.keys(group)) {
|
|
370
|
+
names.add(name);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return names;
|
|
374
|
+
}
|
|
375
|
+
var FRAMEWORK_BINDINGS = {
|
|
376
|
+
react: `## React bindings
|
|
377
|
+
|
|
378
|
+
\`\`\`tsx
|
|
379
|
+
import { KoraProvider, useCollection, useMutation, useQuery, useSyncStatus } from '@korajs/react'
|
|
380
|
+
|
|
381
|
+
// Wrap the tree once near your app entry: <KoraProvider app={app}><App /></KoraProvider>
|
|
382
|
+
|
|
383
|
+
const todos = useCollection('todos')
|
|
384
|
+
const rows = useQuery(todos.where({ completed: false }).orderBy('createdAt'))
|
|
385
|
+
const addTodo = useMutation((data) => todos.insert(data))
|
|
386
|
+
addTodo.mutate({ title: 'x' }) // fire-and-forget; errors land in addTodo.error
|
|
387
|
+
await addTodo.mutateAsync({ title: 'x' }) // resolves with the result; throws on failure
|
|
388
|
+
const status = useSyncStatus() // status.status, status.pendingOperations
|
|
389
|
+
\`\`\`
|
|
390
|
+
|
|
391
|
+
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".`,
|
|
392
|
+
vue: `## Vue bindings
|
|
393
|
+
|
|
394
|
+
\`\`\`ts
|
|
395
|
+
import { useCollection, useMutation, useQuery, useSyncStatus } from '@korajs/vue'
|
|
396
|
+
|
|
397
|
+
const todos = useCollection('todos')
|
|
398
|
+
const rows = useQuery(todos.where({ completed: false }).orderBy('createdAt')) // readonly reactive ref
|
|
399
|
+
const addTodo = useMutation((data) => todos.insert(data))
|
|
400
|
+
addTodo.mutate({ title: 'x' }) // fire-and-forget; errors land in addTodo.error
|
|
401
|
+
const status = useSyncStatus() // status.status, status.pendingOperations
|
|
402
|
+
\`\`\`
|
|
403
|
+
|
|
404
|
+
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\`).`,
|
|
405
|
+
svelte: `## Svelte bindings
|
|
406
|
+
|
|
407
|
+
Kora's Svelte bindings from \`@korajs/svelte\` expose \`useCollection\`, \`useQuery\`, \`useMutation\`, and \`useSyncStatus\`, wired through the provider set up in your app entry.
|
|
408
|
+
|
|
409
|
+
\`\`\`ts
|
|
410
|
+
import { useCollection, useMutation, useQuery, useSyncStatus } from '@korajs/svelte'
|
|
411
|
+
|
|
412
|
+
const todos = useCollection('todos')
|
|
413
|
+
const rows = useQuery(todos.where({ completed: false }).orderBy('createdAt')) // reactive store
|
|
414
|
+
const addTodo = useMutation((data) => todos.insert(data))
|
|
415
|
+
addTodo.mutate({ title: 'x' }) // fire-and-forget; errors land in addTodo.error
|
|
416
|
+
const status = useSyncStatus() // readable store; read $status.status in markup
|
|
417
|
+
\`\`\`
|
|
418
|
+
|
|
419
|
+
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.`
|
|
420
|
+
};
|
|
421
|
+
function generateAgentsMd(framework) {
|
|
422
|
+
return `# AGENTS.md
|
|
423
|
+
|
|
424
|
+
Guidance for AI coding agents working in this project. Humans: this is useful for you too.
|
|
425
|
+
|
|
426
|
+
## What this project is
|
|
427
|
+
|
|
428
|
+
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.
|
|
429
|
+
|
|
430
|
+
## Golden rules
|
|
431
|
+
|
|
432
|
+
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.
|
|
433
|
+
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.
|
|
434
|
+
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.
|
|
435
|
+
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.
|
|
436
|
+
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.
|
|
437
|
+
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.
|
|
438
|
+
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.
|
|
439
|
+
|
|
440
|
+
## Data API cheat sheet
|
|
441
|
+
|
|
442
|
+
\`\`\`ts
|
|
443
|
+
await app.ready
|
|
444
|
+
const rec = await app.todos.insert({ title: 'x' }) // defaults and .auto() fields applied
|
|
445
|
+
await app.todos.update(rec.id, { completed: true }) // partial update, changed fields only
|
|
446
|
+
await app.todos.delete(rec.id)
|
|
447
|
+
const one = await app.todos.findById(rec.id)
|
|
448
|
+
const unsubscribe = app.todos
|
|
449
|
+
.where({ completed: false })
|
|
450
|
+
.orderBy('createdAt', 'desc')
|
|
451
|
+
.subscribe((rows) => {/* fires immediately, then on every change */})
|
|
452
|
+
\`\`\`
|
|
453
|
+
|
|
454
|
+
Schema example (\`src/schema.ts\`):
|
|
455
|
+
|
|
456
|
+
\`\`\`ts
|
|
457
|
+
import { defineSchema, t } from 'korajs'
|
|
458
|
+
|
|
459
|
+
export default defineSchema({
|
|
460
|
+
version: 1,
|
|
461
|
+
collections: {
|
|
462
|
+
todos: {
|
|
463
|
+
fields: {
|
|
464
|
+
title: t.string(),
|
|
465
|
+
completed: t.boolean().default(false),
|
|
466
|
+
tags: t.array(t.string()).default([]),
|
|
467
|
+
priority: t.enum(['low', 'medium', 'high']).default('medium'),
|
|
468
|
+
createdAt: t.timestamp().auto(), // set automatically; never pass it on insert
|
|
469
|
+
},
|
|
470
|
+
indexes: ['completed', 'createdAt'],
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
})
|
|
474
|
+
\`\`\`
|
|
475
|
+
|
|
476
|
+
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.
|
|
477
|
+
|
|
478
|
+
## Conflict handling
|
|
479
|
+
|
|
480
|
+
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.
|
|
481
|
+
|
|
482
|
+
## Sync and auth
|
|
483
|
+
|
|
484
|
+
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.
|
|
485
|
+
|
|
486
|
+
${FRAMEWORK_BINDINGS[framework]}
|
|
487
|
+
|
|
488
|
+
## Commands
|
|
489
|
+
|
|
490
|
+
- \`npx kora dev\` runs the dev environment (app server, local sync server when configured, schema watcher).
|
|
491
|
+
- \`npx kora doctor\` diagnoses a broken setup.
|
|
492
|
+
- \`npx kora migrate\` generates a migration from a schema change (\`--apply\` runs it against a SQLite or Postgres database).
|
|
493
|
+
- In a running app, the DevTools overlay (Ctrl+Shift+K, Cmd+Shift+K on macOS) inspects operations, merges, and sync status.
|
|
494
|
+
`;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/commands/agents/agents-command.ts
|
|
498
|
+
async function fileExists(path) {
|
|
499
|
+
try {
|
|
500
|
+
await (0, import_promises2.access)(path);
|
|
501
|
+
return true;
|
|
502
|
+
} catch {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
async function readManifest(root) {
|
|
507
|
+
try {
|
|
508
|
+
return JSON.parse(await (0, import_promises2.readFile)((0, import_node_path2.join)(root, "package.json"), "utf-8"));
|
|
509
|
+
} catch {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
function resolveFramework(override, manifest) {
|
|
514
|
+
if (override !== void 0 && isAgentsFramework(override)) return override;
|
|
515
|
+
return detectFrameworkFromManifest(manifest) ?? "react";
|
|
516
|
+
}
|
|
517
|
+
var agentsCommand = (0, import_citty.defineCommand)({
|
|
518
|
+
meta: {
|
|
519
|
+
name: "agents-md",
|
|
520
|
+
description: "Write an AGENTS.md with Kora rules for AI coding agents into this project"
|
|
521
|
+
},
|
|
522
|
+
args: {
|
|
523
|
+
force: {
|
|
524
|
+
type: "boolean",
|
|
525
|
+
description: "Overwrite an existing AGENTS.md",
|
|
526
|
+
default: false
|
|
527
|
+
},
|
|
528
|
+
framework: {
|
|
529
|
+
type: "string",
|
|
530
|
+
description: "Override framework detection (react, vue, or svelte)"
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
async run({ args }) {
|
|
534
|
+
const logger = createLogger();
|
|
535
|
+
const projectRoot = await findProjectRoot() ?? process.cwd();
|
|
536
|
+
const manifest = await readManifest(projectRoot);
|
|
537
|
+
const override = typeof args.framework === "string" ? args.framework : void 0;
|
|
538
|
+
if (override !== void 0 && !isAgentsFramework(override)) {
|
|
539
|
+
logger.error(`Unknown framework "${override}". Use one of: react, vue, svelte.`);
|
|
540
|
+
process.exitCode = 1;
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const framework = resolveFramework(override, manifest);
|
|
544
|
+
const target = (0, import_node_path2.join)(projectRoot, "AGENTS.md");
|
|
545
|
+
const force = args.force === true;
|
|
546
|
+
if (await fileExists(target) && !force) {
|
|
547
|
+
logger.warn("AGENTS.md already exists. Re-run with --force to overwrite it.");
|
|
548
|
+
process.exitCode = 1;
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
await (0, import_promises2.writeFile)(target, generateAgentsMd(framework), "utf-8");
|
|
552
|
+
logger.banner();
|
|
553
|
+
logger.success(`Wrote AGENTS.md (${framework} bindings) to ${target}`);
|
|
554
|
+
logger.step("Commit it so coding agents pick up Kora conventions automatically.");
|
|
555
|
+
logger.blank();
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// src/commands/generate/type-generator.ts
|
|
560
|
+
function generateTypes(schema) {
|
|
561
|
+
const lines = [
|
|
562
|
+
"// Auto-generated by @korajs/cli \u2014 do not edit manually",
|
|
563
|
+
`// Generated from schema version ${String(schema.version)}`,
|
|
564
|
+
""
|
|
565
|
+
];
|
|
566
|
+
const collectionNames = Object.keys(schema.collections);
|
|
567
|
+
if (collectionNames.length === 0) {
|
|
568
|
+
return lines.join("\n");
|
|
569
|
+
}
|
|
570
|
+
for (const [name, collection] of Object.entries(schema.collections)) {
|
|
571
|
+
const pascal = toPascalCase(name);
|
|
572
|
+
const fields = collection.fields;
|
|
573
|
+
lines.push(`export interface ${pascal}Record {`);
|
|
574
|
+
lines.push(" readonly id: string");
|
|
575
|
+
for (const [fieldName, descriptor] of Object.entries(fields)) {
|
|
576
|
+
const tsType = fieldKindToTypeScript(descriptor);
|
|
577
|
+
const optional = !descriptor.required && !descriptor.auto ? "?" : "";
|
|
578
|
+
lines.push(` readonly ${fieldName}${optional}: ${tsType}`);
|
|
579
|
+
}
|
|
580
|
+
lines.push("}");
|
|
581
|
+
lines.push("");
|
|
582
|
+
lines.push(`export interface ${pascal}InsertInput {`);
|
|
583
|
+
for (const [fieldName, descriptor] of Object.entries(fields)) {
|
|
584
|
+
if (descriptor.auto) continue;
|
|
585
|
+
const tsType = fieldKindToTypeScript(descriptor);
|
|
586
|
+
const optional = !descriptor.required || descriptor.defaultValue !== void 0 ? "?" : "";
|
|
587
|
+
lines.push(` ${fieldName}${optional}: ${tsType}`);
|
|
588
|
+
}
|
|
589
|
+
lines.push("}");
|
|
590
|
+
lines.push("");
|
|
591
|
+
lines.push(`export interface ${pascal}UpdateInput {`);
|
|
592
|
+
for (const [fieldName, descriptor] of Object.entries(fields)) {
|
|
593
|
+
if (descriptor.auto) continue;
|
|
594
|
+
const tsType = fieldKindToTypeScript(descriptor);
|
|
595
|
+
lines.push(` ${fieldName}?: ${tsType}`);
|
|
596
|
+
}
|
|
597
|
+
lines.push("}");
|
|
598
|
+
lines.push("");
|
|
599
|
+
}
|
|
600
|
+
return lines.join("\n");
|
|
601
|
+
}
|
|
602
|
+
function fieldKindToTypeScript(descriptor) {
|
|
603
|
+
switch (descriptor.kind) {
|
|
604
|
+
case "string":
|
|
605
|
+
return "string";
|
|
606
|
+
case "number":
|
|
607
|
+
return "number";
|
|
608
|
+
case "boolean":
|
|
609
|
+
return "boolean";
|
|
610
|
+
case "timestamp":
|
|
611
|
+
return "number";
|
|
612
|
+
case "richtext":
|
|
613
|
+
return "string";
|
|
614
|
+
case "enum": {
|
|
615
|
+
if (descriptor.enumValues && descriptor.enumValues.length > 0) {
|
|
616
|
+
return descriptor.enumValues.map((v) => `'${v}'`).join(" | ");
|
|
617
|
+
}
|
|
618
|
+
return "string";
|
|
619
|
+
}
|
|
620
|
+
case "array": {
|
|
621
|
+
const itemType = itemKindToTypeScript(descriptor.itemKind);
|
|
622
|
+
return `Array<${itemType}>`;
|
|
623
|
+
}
|
|
624
|
+
default:
|
|
625
|
+
return "unknown";
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function itemKindToTypeScript(kind) {
|
|
629
|
+
switch (kind) {
|
|
630
|
+
case "string":
|
|
631
|
+
return "string";
|
|
632
|
+
case "number":
|
|
633
|
+
return "number";
|
|
634
|
+
case "boolean":
|
|
635
|
+
return "boolean";
|
|
636
|
+
case "timestamp":
|
|
637
|
+
return "number";
|
|
638
|
+
case "richtext":
|
|
639
|
+
return "string";
|
|
640
|
+
case "enum":
|
|
641
|
+
return "string";
|
|
642
|
+
case "array":
|
|
643
|
+
return "unknown[]";
|
|
644
|
+
default:
|
|
645
|
+
return "unknown";
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
function toPascalCase(name) {
|
|
649
|
+
return name.split(/[_-]/).map((part) => {
|
|
650
|
+
if (part.length === 0) return "";
|
|
651
|
+
const first = part[0];
|
|
652
|
+
return first ? first.toUpperCase() + part.slice(1) : "";
|
|
653
|
+
}).join("");
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// src/commands/deploy/deploy-command.ts
|
|
657
|
+
var import_node_fs2 = require("fs");
|
|
658
|
+
var import_node_path13 = require("path");
|
|
659
|
+
var import_citty2 = require("citty");
|
|
660
|
+
|
|
661
|
+
// src/prompts/prompt-client.ts
|
|
662
|
+
var import_prompts = require("@clack/prompts");
|
|
663
|
+
|
|
664
|
+
// src/utils/prompt.ts
|
|
665
|
+
var import_node_readline = require("readline");
|
|
666
|
+
function promptText(message, defaultValue, options) {
|
|
667
|
+
return new Promise((resolve3) => {
|
|
668
|
+
const rl = createReadline(options);
|
|
669
|
+
const suffix = defaultValue !== void 0 ? ` (${defaultValue})` : "";
|
|
670
|
+
rl.question(` ? ${message}${suffix}: `, (answer) => {
|
|
671
|
+
rl.close();
|
|
672
|
+
const trimmed = answer.trim();
|
|
673
|
+
resolve3(trimmed || defaultValue || "");
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
function promptSelect(message, choices, options) {
|
|
678
|
+
return new Promise((resolve3) => {
|
|
679
|
+
const rl = createReadline(options);
|
|
680
|
+
const out = options?.output ?? process.stdout;
|
|
681
|
+
out.write(` ? ${message}
|
|
682
|
+
`);
|
|
683
|
+
for (let i = 0; i < choices.length; i++) {
|
|
684
|
+
const choice = choices[i];
|
|
685
|
+
if (choice) {
|
|
686
|
+
out.write(` ${i + 1}) ${choice.label}
|
|
687
|
+
`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
const ask = () => {
|
|
691
|
+
rl.question(" > ", (answer) => {
|
|
692
|
+
const index = Number.parseInt(answer.trim(), 10) - 1;
|
|
693
|
+
const selected = choices[index];
|
|
694
|
+
if (selected) {
|
|
695
|
+
rl.close();
|
|
696
|
+
resolve3(selected.value);
|
|
697
|
+
} else {
|
|
698
|
+
out.write(` Please enter a number between 1 and ${choices.length}
|
|
699
|
+
`);
|
|
700
|
+
ask();
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
};
|
|
704
|
+
ask();
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
function promptConfirm(message, defaultValue = false, options) {
|
|
708
|
+
return new Promise((resolve3) => {
|
|
709
|
+
const rl = createReadline(options);
|
|
710
|
+
const suffix = defaultValue ? "Y/n" : "y/N";
|
|
711
|
+
const ask = () => {
|
|
712
|
+
rl.question(` ? ${message} (${suffix}): `, (answer) => {
|
|
713
|
+
const normalized = answer.trim().toLowerCase();
|
|
714
|
+
if (normalized.length === 0) {
|
|
715
|
+
rl.close();
|
|
716
|
+
resolve3(defaultValue);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (normalized === "y" || normalized === "yes") {
|
|
720
|
+
rl.close();
|
|
721
|
+
resolve3(true);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (normalized === "n" || normalized === "no") {
|
|
725
|
+
rl.close();
|
|
726
|
+
resolve3(false);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
;
|
|
730
|
+
(options?.output ?? process.stdout).write(" Please answer with y or n\n");
|
|
731
|
+
ask();
|
|
732
|
+
});
|
|
733
|
+
};
|
|
734
|
+
ask();
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
function createReadline(options) {
|
|
738
|
+
return (0, import_node_readline.createInterface)({
|
|
739
|
+
input: options?.input ?? process.stdin,
|
|
740
|
+
output: options?.output ?? process.stdout
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/prompts/prompt-client.ts
|
|
745
|
+
var PromptCancelledError = class extends Error {
|
|
746
|
+
constructor(message = "Prompt cancelled by user") {
|
|
747
|
+
super(message);
|
|
748
|
+
this.name = "PromptCancelledError";
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
var ReadlinePromptClient = class {
|
|
752
|
+
async text(message, defaultValue) {
|
|
753
|
+
return promptText(message, defaultValue);
|
|
754
|
+
}
|
|
755
|
+
async select(message, options) {
|
|
756
|
+
return promptSelect(
|
|
757
|
+
message,
|
|
758
|
+
options.filter((option) => option.disabled !== true).map((option) => ({ label: option.label, value: option.value }))
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
async confirm(message, defaultValue = false) {
|
|
762
|
+
return promptConfirm(message, defaultValue);
|
|
763
|
+
}
|
|
764
|
+
intro(message) {
|
|
765
|
+
void message;
|
|
766
|
+
}
|
|
767
|
+
outro(message) {
|
|
768
|
+
void message;
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
function createPromptClient() {
|
|
772
|
+
const canUseInteractiveClack = typeof process !== "undefined" && process.stdin.isTTY && process.stdout.isTTY;
|
|
773
|
+
if (canUseInteractiveClack) {
|
|
774
|
+
return new ClackPromptClient();
|
|
775
|
+
}
|
|
776
|
+
return new ReadlinePromptClient();
|
|
777
|
+
}
|
|
778
|
+
var ClackPromptClient = class {
|
|
779
|
+
async text(message, defaultValue) {
|
|
780
|
+
const result = await (0, import_prompts.text)({
|
|
781
|
+
message,
|
|
782
|
+
placeholder: defaultValue,
|
|
783
|
+
defaultValue
|
|
784
|
+
});
|
|
785
|
+
if ((0, import_prompts.isCancel)(result)) {
|
|
786
|
+
(0, import_prompts.cancel)("Operation cancelled.");
|
|
787
|
+
throw new PromptCancelledError();
|
|
788
|
+
}
|
|
789
|
+
const value = result.trim();
|
|
790
|
+
if (value.length > 0) return value;
|
|
791
|
+
return defaultValue ?? "";
|
|
792
|
+
}
|
|
793
|
+
async select(message, options) {
|
|
794
|
+
const mappedOptions = options.map((option) => ({
|
|
795
|
+
label: option.label,
|
|
796
|
+
value: option.value,
|
|
797
|
+
hint: option.hint,
|
|
798
|
+
disabled: option.disabled
|
|
799
|
+
}));
|
|
800
|
+
const result = await (0, import_prompts.select)({
|
|
801
|
+
message,
|
|
802
|
+
options: mappedOptions
|
|
803
|
+
});
|
|
804
|
+
if ((0, import_prompts.isCancel)(result)) {
|
|
805
|
+
(0, import_prompts.cancel)("Operation cancelled.");
|
|
806
|
+
throw new PromptCancelledError();
|
|
807
|
+
}
|
|
808
|
+
return result;
|
|
809
|
+
}
|
|
810
|
+
async confirm(message, defaultValue = false) {
|
|
811
|
+
const result = await (0, import_prompts.confirm)({
|
|
812
|
+
message,
|
|
813
|
+
initialValue: defaultValue
|
|
814
|
+
});
|
|
815
|
+
if ((0, import_prompts.isCancel)(result)) {
|
|
816
|
+
(0, import_prompts.cancel)("Operation cancelled.");
|
|
817
|
+
throw new PromptCancelledError();
|
|
818
|
+
}
|
|
819
|
+
return result;
|
|
820
|
+
}
|
|
821
|
+
intro(message) {
|
|
822
|
+
(0, import_prompts.intro)(message);
|
|
823
|
+
}
|
|
824
|
+
outro(message) {
|
|
825
|
+
(0, import_prompts.outro)(message);
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
// src/commands/deploy/adapters/adapter.ts
|
|
830
|
+
var DEPLOY_PLATFORMS = [
|
|
831
|
+
"fly",
|
|
832
|
+
"railway",
|
|
833
|
+
"aws-ecs",
|
|
834
|
+
"aws-lightsail",
|
|
835
|
+
"render",
|
|
836
|
+
"docker",
|
|
837
|
+
"kora-cloud"
|
|
838
|
+
];
|
|
839
|
+
var IMPLEMENTED_DEPLOY_PLATFORMS = ["fly", "railway", "aws-ecs", "aws-lightsail"];
|
|
840
|
+
function isImplementedDeployPlatform(platform) {
|
|
841
|
+
return IMPLEMENTED_DEPLOY_PLATFORMS.includes(platform);
|
|
842
|
+
}
|
|
843
|
+
function isDeployPlatform(value) {
|
|
844
|
+
return DEPLOY_PLATFORMS.includes(value);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// src/commands/deploy/adapters/aws-ecs-adapter.ts
|
|
848
|
+
var import_node_child_process2 = require("child_process");
|
|
849
|
+
var import_node_path5 = require("path");
|
|
850
|
+
|
|
851
|
+
// src/commands/deploy/builder/client-builder.ts
|
|
852
|
+
var import_node_child_process = require("child_process");
|
|
853
|
+
var import_node_fs = require("fs");
|
|
854
|
+
var import_promises3 = require("fs/promises");
|
|
855
|
+
var import_node_path3 = require("path");
|
|
856
|
+
async function buildClient(options) {
|
|
857
|
+
const viteEntryPoint = await resolveProjectBinaryEntryPoint(options.projectRoot, "vite", "vite");
|
|
858
|
+
if (!viteEntryPoint) {
|
|
859
|
+
throw new Error(
|
|
860
|
+
`Could not find local Vite binary in ${options.projectRoot}. Install dependencies before deploying.`
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
const args = [
|
|
864
|
+
viteEntryPoint,
|
|
865
|
+
"build",
|
|
866
|
+
"--outDir",
|
|
867
|
+
options.outDir,
|
|
868
|
+
"--mode",
|
|
869
|
+
options.mode ?? "production"
|
|
870
|
+
];
|
|
871
|
+
await runProcess(process.execPath, args, options.projectRoot);
|
|
872
|
+
await patchSqliteWasmAssets(options.projectRoot, options.outDir);
|
|
873
|
+
return { outDir: options.outDir };
|
|
874
|
+
}
|
|
875
|
+
async function patchSqliteWasmAssets(projectRoot, outDir) {
|
|
876
|
+
const assetsDir = (0, import_node_path3.join)(outDir, "assets");
|
|
877
|
+
if (!(0, import_node_fs.existsSync)(assetsDir)) return;
|
|
878
|
+
const files = await (0, import_promises3.readdir)(assetsDir);
|
|
879
|
+
const hashedWasm = files.find((f) => /^sqlite3-.+\.wasm$/.test(f));
|
|
880
|
+
if (hashedWasm && !files.includes("sqlite3.wasm")) {
|
|
881
|
+
await (0, import_promises3.copyFile)((0, import_node_path3.join)(assetsDir, hashedWasm), (0, import_node_path3.join)(assetsDir, "sqlite3.wasm"));
|
|
882
|
+
}
|
|
883
|
+
if (!files.includes("sqlite3-opfs-async-proxy.js")) {
|
|
884
|
+
const proxyFile = (0, import_node_path3.resolve)(
|
|
885
|
+
projectRoot,
|
|
886
|
+
"node_modules",
|
|
887
|
+
"@sqlite.org",
|
|
888
|
+
"sqlite-wasm",
|
|
889
|
+
"sqlite-wasm",
|
|
890
|
+
"jswasm",
|
|
891
|
+
"sqlite3-opfs-async-proxy.js"
|
|
892
|
+
);
|
|
893
|
+
if ((0, import_node_fs.existsSync)(proxyFile)) {
|
|
894
|
+
await (0, import_promises3.copyFile)(proxyFile, (0, import_node_path3.join)(assetsDir, "sqlite3-opfs-async-proxy.js"));
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
async function runProcess(command, args, cwd) {
|
|
899
|
+
await new Promise((resolve3, reject) => {
|
|
900
|
+
const child = (0, import_node_child_process.spawn)(command, args, {
|
|
901
|
+
cwd,
|
|
902
|
+
stdio: "inherit",
|
|
903
|
+
env: process.env
|
|
904
|
+
});
|
|
905
|
+
child.on("error", (error) => {
|
|
906
|
+
reject(error);
|
|
907
|
+
});
|
|
908
|
+
child.on("exit", (code) => {
|
|
909
|
+
if (code === 0) {
|
|
910
|
+
resolve3();
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
reject(new Error(`Client build failed with exit code ${String(code ?? "unknown")}.`));
|
|
914
|
+
});
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/commands/deploy/builder/server-bundler.ts
|
|
919
|
+
var import_promises4 = require("fs/promises");
|
|
920
|
+
var import_promises5 = require("fs/promises");
|
|
921
|
+
var import_node_path4 = require("path");
|
|
922
|
+
var import_esbuild = require("esbuild");
|
|
923
|
+
var DEFAULT_ENTRY_CANDIDATES = [
|
|
924
|
+
"server.ts",
|
|
925
|
+
"server.js",
|
|
926
|
+
"src/server.ts",
|
|
927
|
+
"src/server.js"
|
|
928
|
+
];
|
|
929
|
+
async function bundleServer(options) {
|
|
930
|
+
const candidates = options.entryFileCandidates ?? DEFAULT_ENTRY_CANDIDATES;
|
|
931
|
+
const entryFilePath = await resolveServerEntry(options.projectRoot, candidates);
|
|
932
|
+
if (!entryFilePath) {
|
|
933
|
+
throw new Error(
|
|
934
|
+
`Could not find a server entry file in ${options.projectRoot}. Looked for: ${candidates.join(", ")}`
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
await (0, import_promises4.mkdir)(options.deployDirectory, { recursive: true });
|
|
938
|
+
const outputFilePath = (0, import_node_path4.join)(options.deployDirectory, "server-bundled.js");
|
|
939
|
+
await (0, import_esbuild.build)({
|
|
940
|
+
entryPoints: [entryFilePath],
|
|
941
|
+
outfile: outputFilePath,
|
|
942
|
+
bundle: true,
|
|
943
|
+
platform: "node",
|
|
944
|
+
format: "esm",
|
|
945
|
+
target: ["node20"],
|
|
946
|
+
sourcemap: false,
|
|
947
|
+
logLevel: "silent",
|
|
948
|
+
external: ["better-sqlite3"],
|
|
949
|
+
banner: {
|
|
950
|
+
js: "import { createRequire as __createRequire } from 'module'; const require = __createRequire(import.meta.url);"
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
return {
|
|
954
|
+
entryFilePath,
|
|
955
|
+
outputFilePath
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
async function resolveServerEntry(projectRoot, candidates) {
|
|
959
|
+
for (const candidate of candidates) {
|
|
960
|
+
const fullPath = (0, import_node_path4.join)(projectRoot, candidate);
|
|
961
|
+
try {
|
|
962
|
+
await (0, import_promises5.access)(fullPath);
|
|
963
|
+
return fullPath;
|
|
964
|
+
} catch {
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// src/commands/deploy/adapters/aws-ecs-adapter.ts
|
|
971
|
+
var AwsEcsAdapter = class {
|
|
972
|
+
name = "aws-ecs";
|
|
973
|
+
logger = createLogger();
|
|
974
|
+
runner;
|
|
975
|
+
currentContext;
|
|
976
|
+
constructor(options = {}) {
|
|
977
|
+
this.runner = options.runner ?? new NodeAwsCommandRunner();
|
|
978
|
+
this.currentContext = options.context ?? null;
|
|
979
|
+
}
|
|
980
|
+
setContext(context) {
|
|
981
|
+
this.currentContext = context;
|
|
982
|
+
}
|
|
983
|
+
async detect() {
|
|
984
|
+
const result = await this.runner.run("aws", ["--version"], process.cwd());
|
|
985
|
+
return result.exitCode === 0;
|
|
986
|
+
}
|
|
987
|
+
async install() {
|
|
988
|
+
const available = await this.detect();
|
|
989
|
+
if (!available) {
|
|
990
|
+
throw new Error(
|
|
991
|
+
"AWS CLI is required but not installed. Install from https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html"
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
async authenticate() {
|
|
996
|
+
const cwd = this.currentContext?.projectRoot ?? process.cwd();
|
|
997
|
+
const result = await this.runner.run("aws", ["sts", "get-caller-identity"], cwd);
|
|
998
|
+
if (result.exitCode !== 0) {
|
|
999
|
+
throw new Error(
|
|
1000
|
+
"AWS CLI is not authenticated. Run `aws configure` or set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables."
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
async provision(config) {
|
|
1005
|
+
this.currentContext = {
|
|
1006
|
+
projectRoot: config.projectRoot,
|
|
1007
|
+
appName: config.appName,
|
|
1008
|
+
region: config.region ?? "us-east-1"
|
|
1009
|
+
};
|
|
1010
|
+
const region = config.region ?? "us-east-1";
|
|
1011
|
+
const repoName = `kora/${config.appName}`;
|
|
1012
|
+
const createRepo = await this.runner.run(
|
|
1013
|
+
"aws",
|
|
1014
|
+
[
|
|
1015
|
+
"ecr",
|
|
1016
|
+
"create-repository",
|
|
1017
|
+
"--repository-name",
|
|
1018
|
+
repoName,
|
|
1019
|
+
"--region",
|
|
1020
|
+
region,
|
|
1021
|
+
"--image-scanning-configuration",
|
|
1022
|
+
"scanOnPush=true"
|
|
1023
|
+
],
|
|
1024
|
+
config.projectRoot
|
|
1025
|
+
);
|
|
1026
|
+
if (createRepo.exitCode !== 0 && !createRepo.stderr.includes("RepositoryAlreadyExistsException")) {
|
|
1027
|
+
throw new Error(`Failed to create ECR repository: ${createRepo.stderr}`);
|
|
1028
|
+
}
|
|
1029
|
+
const identity = await this.runner.run(
|
|
1030
|
+
"aws",
|
|
1031
|
+
["sts", "get-caller-identity", "--query", "Account", "--output", "text"],
|
|
1032
|
+
config.projectRoot
|
|
1033
|
+
);
|
|
1034
|
+
const accountId = identity.stdout.trim();
|
|
1035
|
+
await this.runner.run(
|
|
1036
|
+
"aws",
|
|
1037
|
+
["ecs", "create-cluster", "--cluster-name", config.appName, "--region", region],
|
|
1038
|
+
config.projectRoot
|
|
1039
|
+
);
|
|
1040
|
+
await this.runner.run(
|
|
1041
|
+
"aws",
|
|
1042
|
+
[
|
|
1043
|
+
"logs",
|
|
1044
|
+
"create-log-group",
|
|
1045
|
+
"--log-group-name",
|
|
1046
|
+
`/ecs/${config.appName}`,
|
|
1047
|
+
"--region",
|
|
1048
|
+
region
|
|
1049
|
+
],
|
|
1050
|
+
config.projectRoot
|
|
1051
|
+
);
|
|
1052
|
+
return {
|
|
1053
|
+
applicationId: `${accountId}.dkr.ecr.${region}.amazonaws.com/${repoName}`,
|
|
1054
|
+
databaseId: null,
|
|
1055
|
+
secretsSet: ["PORT"]
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
async build(config) {
|
|
1059
|
+
this.currentContext = {
|
|
1060
|
+
projectRoot: config.projectRoot,
|
|
1061
|
+
appName: config.appName,
|
|
1062
|
+
region: config.region
|
|
1063
|
+
};
|
|
1064
|
+
const deployDirectory = (0, import_node_path5.join)(config.projectRoot, ".kora", "deploy");
|
|
1065
|
+
await bundleServer({
|
|
1066
|
+
projectRoot: config.projectRoot,
|
|
1067
|
+
deployDirectory
|
|
1068
|
+
});
|
|
1069
|
+
const client = config.deployTarget === "sync-server" ? null : await buildClient({
|
|
1070
|
+
projectRoot: config.projectRoot,
|
|
1071
|
+
outDir: (0, import_node_path5.join)(deployDirectory, "dist"),
|
|
1072
|
+
mode: "production"
|
|
1073
|
+
});
|
|
1074
|
+
return {
|
|
1075
|
+
clientDirectory: client?.outDir ?? null,
|
|
1076
|
+
serverBundlePath: (0, import_node_path5.join)(deployDirectory, "server-bundled.js"),
|
|
1077
|
+
deployDirectory
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
async deploy(artifacts) {
|
|
1081
|
+
const context = this.requireContext();
|
|
1082
|
+
const region = context.region ?? "us-east-1";
|
|
1083
|
+
const repoName = `kora/${context.appName}`;
|
|
1084
|
+
const loginPassword = await this.runner.run(
|
|
1085
|
+
"aws",
|
|
1086
|
+
["ecr", "get-login-password", "--region", region],
|
|
1087
|
+
context.projectRoot
|
|
1088
|
+
);
|
|
1089
|
+
if (loginPassword.exitCode !== 0) {
|
|
1090
|
+
throw new Error(`ECR login failed: ${loginPassword.stderr}`);
|
|
1091
|
+
}
|
|
1092
|
+
const identity = await this.runner.run(
|
|
1093
|
+
"aws",
|
|
1094
|
+
["sts", "get-caller-identity", "--query", "Account", "--output", "text"],
|
|
1095
|
+
context.projectRoot
|
|
1096
|
+
);
|
|
1097
|
+
const accountId = identity.stdout.trim();
|
|
1098
|
+
const ecrUri = `${accountId}.dkr.ecr.${region}.amazonaws.com`;
|
|
1099
|
+
const imageUri = `${ecrUri}/${repoName}:latest`;
|
|
1100
|
+
const dockerLogin = await this.runner.run(
|
|
1101
|
+
"docker",
|
|
1102
|
+
["login", "--username", "AWS", "--password-stdin", ecrUri],
|
|
1103
|
+
artifacts.deployDirectory
|
|
1104
|
+
);
|
|
1105
|
+
this.logger.step("Building Docker image...");
|
|
1106
|
+
const dockerBuild = await this.runner.run(
|
|
1107
|
+
"docker",
|
|
1108
|
+
["build", "--platform", "linux/amd64", "-t", imageUri, "."],
|
|
1109
|
+
artifacts.deployDirectory
|
|
1110
|
+
);
|
|
1111
|
+
if (dockerBuild.exitCode !== 0) {
|
|
1112
|
+
throw new Error(`Docker build failed: ${dockerBuild.stderr}`);
|
|
1113
|
+
}
|
|
1114
|
+
this.logger.step("Pushing image to ECR...");
|
|
1115
|
+
const dockerPush = await this.runner.run(
|
|
1116
|
+
"docker",
|
|
1117
|
+
["push", imageUri],
|
|
1118
|
+
artifacts.deployDirectory
|
|
1119
|
+
);
|
|
1120
|
+
if (dockerPush.exitCode !== 0) {
|
|
1121
|
+
throw new Error(`Docker push failed: ${dockerPush.stderr}`);
|
|
1122
|
+
}
|
|
1123
|
+
const taskDef = JSON.stringify({
|
|
1124
|
+
family: context.appName,
|
|
1125
|
+
networkMode: "awsvpc",
|
|
1126
|
+
requiresCompatibilities: ["FARGATE"],
|
|
1127
|
+
cpu: "256",
|
|
1128
|
+
memory: "512",
|
|
1129
|
+
executionRoleArn: `arn:aws:iam::${accountId}:role/ecsTaskExecutionRole`,
|
|
1130
|
+
containerDefinitions: [
|
|
1131
|
+
{
|
|
1132
|
+
name: context.appName,
|
|
1133
|
+
image: imageUri,
|
|
1134
|
+
essential: true,
|
|
1135
|
+
portMappings: [{ containerPort: 3001, protocol: "tcp" }],
|
|
1136
|
+
logConfiguration: {
|
|
1137
|
+
logDriver: "awslogs",
|
|
1138
|
+
options: {
|
|
1139
|
+
"awslogs-group": `/ecs/${context.appName}`,
|
|
1140
|
+
"awslogs-region": region,
|
|
1141
|
+
"awslogs-stream-prefix": "ecs"
|
|
1142
|
+
}
|
|
1143
|
+
},
|
|
1144
|
+
healthCheck: {
|
|
1145
|
+
command: ["CMD-SHELL", "curl -f http://localhost:3001/health || exit 1"],
|
|
1146
|
+
interval: 30,
|
|
1147
|
+
timeout: 5,
|
|
1148
|
+
retries: 3,
|
|
1149
|
+
startPeriod: 60
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
]
|
|
1153
|
+
});
|
|
1154
|
+
const registerTask = await this.runner.run(
|
|
1155
|
+
"aws",
|
|
1156
|
+
["ecs", "register-task-definition", "--cli-input-json", taskDef, "--region", region],
|
|
1157
|
+
context.projectRoot
|
|
1158
|
+
);
|
|
1159
|
+
if (registerTask.exitCode !== 0) {
|
|
1160
|
+
throw new Error(`Task definition registration failed: ${registerTask.stderr}`);
|
|
1161
|
+
}
|
|
1162
|
+
const updateService = await this.runner.run(
|
|
1163
|
+
"aws",
|
|
1164
|
+
[
|
|
1165
|
+
"ecs",
|
|
1166
|
+
"update-service",
|
|
1167
|
+
"--cluster",
|
|
1168
|
+
context.appName,
|
|
1169
|
+
"--service",
|
|
1170
|
+
context.appName,
|
|
1171
|
+
"--task-definition",
|
|
1172
|
+
context.appName,
|
|
1173
|
+
"--force-new-deployment",
|
|
1174
|
+
"--region",
|
|
1175
|
+
region
|
|
1176
|
+
],
|
|
1177
|
+
context.projectRoot
|
|
1178
|
+
);
|
|
1179
|
+
const deploymentId = (/* @__PURE__ */ new Date()).toISOString();
|
|
1180
|
+
if (updateService.exitCode !== 0) {
|
|
1181
|
+
this.logger.step("Service not found, creating new service...");
|
|
1182
|
+
this.logger.step(
|
|
1183
|
+
`Task definition registered. Create the ECS service with:
|
|
1184
|
+
aws ecs create-service \\
|
|
1185
|
+
--cluster ${context.appName} \\
|
|
1186
|
+
--service-name ${context.appName} \\
|
|
1187
|
+
--task-definition ${context.appName} \\
|
|
1188
|
+
--desired-count 1 \\
|
|
1189
|
+
--launch-type FARGATE \\
|
|
1190
|
+
--network-configuration "awsvpcConfiguration={subnets=[<subnet-id>],securityGroups=[<sg-id>],assignPublicIp=ENABLED}" \\
|
|
1191
|
+
--region ${region}`
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
return {
|
|
1195
|
+
deploymentId,
|
|
1196
|
+
liveUrl: `https://${context.appName}.${region}.amazonaws.com`,
|
|
1197
|
+
syncUrl: `wss://${context.appName}.${region}.amazonaws.com/kora-sync`
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
async rollback(deploymentId) {
|
|
1201
|
+
const context = this.requireContext();
|
|
1202
|
+
const region = context.region ?? "us-east-1";
|
|
1203
|
+
const result = await this.runner.run(
|
|
1204
|
+
"aws",
|
|
1205
|
+
[
|
|
1206
|
+
"ecs",
|
|
1207
|
+
"update-service",
|
|
1208
|
+
"--cluster",
|
|
1209
|
+
context.appName,
|
|
1210
|
+
"--service",
|
|
1211
|
+
context.appName,
|
|
1212
|
+
"--task-definition",
|
|
1213
|
+
`${context.appName}:${deploymentId}`,
|
|
1214
|
+
"--force-new-deployment",
|
|
1215
|
+
"--region",
|
|
1216
|
+
region
|
|
1217
|
+
],
|
|
1218
|
+
context.projectRoot
|
|
1219
|
+
);
|
|
1220
|
+
if (result.exitCode !== 0) {
|
|
1221
|
+
throw new Error(`ECS rollback failed: ${result.stderr}`);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
async *logs(options) {
|
|
1225
|
+
const context = this.requireContext();
|
|
1226
|
+
const region = context.region ?? "us-east-1";
|
|
1227
|
+
const args = [
|
|
1228
|
+
"logs",
|
|
1229
|
+
"get-log-events",
|
|
1230
|
+
"--log-group-name",
|
|
1231
|
+
`/ecs/${context.appName}`,
|
|
1232
|
+
"--log-stream-name",
|
|
1233
|
+
"ecs/latest",
|
|
1234
|
+
"--region",
|
|
1235
|
+
region
|
|
1236
|
+
];
|
|
1237
|
+
if (options.tail) {
|
|
1238
|
+
args.push("--limit", String(options.tail));
|
|
1239
|
+
}
|
|
1240
|
+
const result = await this.runner.run("aws", args, context.projectRoot);
|
|
1241
|
+
if (result.exitCode !== 0) {
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
try {
|
|
1245
|
+
const parsed = JSON.parse(result.stdout);
|
|
1246
|
+
for (const event of parsed.events ?? []) {
|
|
1247
|
+
yield {
|
|
1248
|
+
timestamp: new Date(event.timestamp).toISOString(),
|
|
1249
|
+
level: inferLogLevel(event.message),
|
|
1250
|
+
message: event.message
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
} catch {
|
|
1254
|
+
for (const line of result.stdout.split("\n").filter(Boolean)) {
|
|
1255
|
+
yield { timestamp: (/* @__PURE__ */ new Date()).toISOString(), level: "info", message: line };
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
async status() {
|
|
1260
|
+
const context = this.requireContext();
|
|
1261
|
+
const region = context.region ?? "us-east-1";
|
|
1262
|
+
const result = await this.runner.run(
|
|
1263
|
+
"aws",
|
|
1264
|
+
[
|
|
1265
|
+
"ecs",
|
|
1266
|
+
"describe-services",
|
|
1267
|
+
"--cluster",
|
|
1268
|
+
context.appName,
|
|
1269
|
+
"--services",
|
|
1270
|
+
context.appName,
|
|
1271
|
+
"--region",
|
|
1272
|
+
region
|
|
1273
|
+
],
|
|
1274
|
+
context.projectRoot
|
|
1275
|
+
);
|
|
1276
|
+
if (result.exitCode !== 0) {
|
|
1277
|
+
return { state: "failed", message: result.stderr };
|
|
1278
|
+
}
|
|
1279
|
+
try {
|
|
1280
|
+
const parsed = JSON.parse(result.stdout);
|
|
1281
|
+
const service = parsed.services?.[0];
|
|
1282
|
+
if (!service) {
|
|
1283
|
+
return { state: "unknown", message: "Service not found" };
|
|
1284
|
+
}
|
|
1285
|
+
if (service.status === "ACTIVE" && service.runningCount > 0) {
|
|
1286
|
+
return {
|
|
1287
|
+
state: "healthy",
|
|
1288
|
+
message: `Running ${service.runningCount}/${service.desiredCount} tasks`
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
return {
|
|
1292
|
+
state: service.runningCount === 0 ? "pending" : "healthy",
|
|
1293
|
+
message: `Status: ${service.status}, running: ${service.runningCount}/${service.desiredCount}`
|
|
1294
|
+
};
|
|
1295
|
+
} catch {
|
|
1296
|
+
return { state: "unknown", message: "Could not parse service status" };
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
requireContext() {
|
|
1300
|
+
if (!this.currentContext) {
|
|
1301
|
+
throw new Error("AWS ECS adapter context is not initialized. Run provision() first.");
|
|
1302
|
+
}
|
|
1303
|
+
return this.currentContext;
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
var NodeAwsCommandRunner = class {
|
|
1307
|
+
async run(command, args, cwd) {
|
|
1308
|
+
return new Promise((resolve3) => {
|
|
1309
|
+
const child = (0, import_node_child_process2.spawn)(command, args, {
|
|
1310
|
+
cwd,
|
|
1311
|
+
env: process.env,
|
|
1312
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1313
|
+
});
|
|
1314
|
+
let stdout = "";
|
|
1315
|
+
let stderr = "";
|
|
1316
|
+
child.stdout?.on("data", (chunk) => {
|
|
1317
|
+
stdout += chunk.toString("utf-8");
|
|
1318
|
+
});
|
|
1319
|
+
child.stderr?.on("data", (chunk) => {
|
|
1320
|
+
stderr += chunk.toString("utf-8");
|
|
1321
|
+
});
|
|
1322
|
+
child.on("error", (error) => {
|
|
1323
|
+
resolve3({ exitCode: 1, stdout, stderr: `${stderr}
|
|
1324
|
+
${error.message}` });
|
|
1325
|
+
});
|
|
1326
|
+
child.on("exit", (code) => {
|
|
1327
|
+
resolve3({ exitCode: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() });
|
|
1328
|
+
});
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
function inferLogLevel(line) {
|
|
1333
|
+
const normalized = line.toLowerCase();
|
|
1334
|
+
if (normalized.includes("error")) return "error";
|
|
1335
|
+
if (normalized.includes("warn")) return "warn";
|
|
1336
|
+
if (normalized.includes("debug")) return "debug";
|
|
1337
|
+
return "info";
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// src/commands/deploy/adapters/aws-lightsail-adapter.ts
|
|
1341
|
+
var import_node_child_process3 = require("child_process");
|
|
1342
|
+
var import_node_path6 = require("path");
|
|
1343
|
+
var AwsLightsailAdapter = class {
|
|
1344
|
+
name = "aws-lightsail";
|
|
1345
|
+
logger = createLogger();
|
|
1346
|
+
runner;
|
|
1347
|
+
currentContext;
|
|
1348
|
+
constructor(options = {}) {
|
|
1349
|
+
this.runner = options.runner ?? new NodeAwsLightsailCommandRunner();
|
|
1350
|
+
this.currentContext = options.context ?? null;
|
|
1351
|
+
}
|
|
1352
|
+
setContext(context) {
|
|
1353
|
+
this.currentContext = context;
|
|
1354
|
+
}
|
|
1355
|
+
async detect() {
|
|
1356
|
+
const result = await this.runner.run("aws", ["--version"], process.cwd());
|
|
1357
|
+
return result.exitCode === 0;
|
|
1358
|
+
}
|
|
1359
|
+
async install() {
|
|
1360
|
+
const available = await this.detect();
|
|
1361
|
+
if (!available) {
|
|
1362
|
+
throw new Error(
|
|
1363
|
+
"AWS CLI is required but not installed. Install from https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html"
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
async authenticate() {
|
|
1368
|
+
const cwd = this.currentContext?.projectRoot ?? process.cwd();
|
|
1369
|
+
const result = await this.runner.run("aws", ["sts", "get-caller-identity"], cwd);
|
|
1370
|
+
if (result.exitCode !== 0) {
|
|
1371
|
+
throw new Error(
|
|
1372
|
+
"AWS CLI is not authenticated. Run `aws configure` or set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables."
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
async provision(config) {
|
|
1377
|
+
this.currentContext = {
|
|
1378
|
+
projectRoot: config.projectRoot,
|
|
1379
|
+
appName: config.appName,
|
|
1380
|
+
region: config.region ?? "us-east-1"
|
|
1381
|
+
};
|
|
1382
|
+
const region = config.region ?? "us-east-1";
|
|
1383
|
+
const serviceName = sanitizeLightsailName(config.appName);
|
|
1384
|
+
const createService = await this.runner.run(
|
|
1385
|
+
"aws",
|
|
1386
|
+
[
|
|
1387
|
+
"lightsail",
|
|
1388
|
+
"create-container-service",
|
|
1389
|
+
"--service-name",
|
|
1390
|
+
serviceName,
|
|
1391
|
+
"--power",
|
|
1392
|
+
"nano",
|
|
1393
|
+
"--scale",
|
|
1394
|
+
"1",
|
|
1395
|
+
"--region",
|
|
1396
|
+
region
|
|
1397
|
+
],
|
|
1398
|
+
config.projectRoot
|
|
1399
|
+
);
|
|
1400
|
+
if (createService.exitCode !== 0 && !createService.stderr.includes("already exists")) {
|
|
1401
|
+
throw new Error(`Failed to create Lightsail container service: ${createService.stderr}`);
|
|
1402
|
+
}
|
|
1403
|
+
return {
|
|
1404
|
+
applicationId: serviceName,
|
|
1405
|
+
databaseId: null,
|
|
1406
|
+
secretsSet: ["PORT"]
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
async build(config) {
|
|
1410
|
+
this.currentContext = {
|
|
1411
|
+
projectRoot: config.projectRoot,
|
|
1412
|
+
appName: config.appName,
|
|
1413
|
+
region: config.region
|
|
1414
|
+
};
|
|
1415
|
+
const deployDirectory = (0, import_node_path6.join)(config.projectRoot, ".kora", "deploy");
|
|
1416
|
+
await bundleServer({
|
|
1417
|
+
projectRoot: config.projectRoot,
|
|
1418
|
+
deployDirectory
|
|
1419
|
+
});
|
|
1420
|
+
const client = config.deployTarget === "sync-server" ? null : await buildClient({
|
|
1421
|
+
projectRoot: config.projectRoot,
|
|
1422
|
+
outDir: (0, import_node_path6.join)(deployDirectory, "dist"),
|
|
1423
|
+
mode: "production"
|
|
1424
|
+
});
|
|
1425
|
+
return {
|
|
1426
|
+
clientDirectory: client?.outDir ?? null,
|
|
1427
|
+
serverBundlePath: (0, import_node_path6.join)(deployDirectory, "server-bundled.js"),
|
|
1428
|
+
deployDirectory
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
async deploy(artifacts) {
|
|
1432
|
+
const context = this.requireContext();
|
|
1433
|
+
const region = context.region ?? "us-east-1";
|
|
1434
|
+
const serviceName = sanitizeLightsailName(context.appName);
|
|
1435
|
+
const imageTag = `${serviceName}:latest`;
|
|
1436
|
+
this.logger.step("Building Docker image...");
|
|
1437
|
+
const dockerBuild = await this.runner.run(
|
|
1438
|
+
"docker",
|
|
1439
|
+
["build", "--platform", "linux/amd64", "-t", imageTag, "."],
|
|
1440
|
+
artifacts.deployDirectory
|
|
1441
|
+
);
|
|
1442
|
+
if (dockerBuild.exitCode !== 0) {
|
|
1443
|
+
throw new Error(`Docker build failed: ${dockerBuild.stderr}`);
|
|
1444
|
+
}
|
|
1445
|
+
this.logger.step("Pushing image to Lightsail...");
|
|
1446
|
+
const pushImage = await this.runner.run(
|
|
1447
|
+
"aws",
|
|
1448
|
+
[
|
|
1449
|
+
"lightsail",
|
|
1450
|
+
"push-container-image",
|
|
1451
|
+
"--service-name",
|
|
1452
|
+
serviceName,
|
|
1453
|
+
"--label",
|
|
1454
|
+
"latest",
|
|
1455
|
+
"--image",
|
|
1456
|
+
imageTag,
|
|
1457
|
+
"--region",
|
|
1458
|
+
region
|
|
1459
|
+
],
|
|
1460
|
+
artifacts.deployDirectory
|
|
1461
|
+
);
|
|
1462
|
+
if (pushImage.exitCode !== 0) {
|
|
1463
|
+
throw new Error(`Lightsail image push failed: ${pushImage.stderr}`);
|
|
1464
|
+
}
|
|
1465
|
+
const lightsailImage = parseLightsailImageRef(pushImage.stdout) ?? `:${serviceName}.latest.1`;
|
|
1466
|
+
this.logger.step("Creating Lightsail deployment...");
|
|
1467
|
+
const environment = { PORT: "3001" };
|
|
1468
|
+
for (const key of PASSTHROUGH_ENV_VARS) {
|
|
1469
|
+
const value = process.env[key];
|
|
1470
|
+
if (value) {
|
|
1471
|
+
environment[key] = value;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
const containers = JSON.stringify({
|
|
1475
|
+
[serviceName]: {
|
|
1476
|
+
image: lightsailImage,
|
|
1477
|
+
ports: { "3001": "HTTP" },
|
|
1478
|
+
environment
|
|
1479
|
+
}
|
|
1480
|
+
});
|
|
1481
|
+
const publicEndpoint = JSON.stringify({
|
|
1482
|
+
containerName: serviceName,
|
|
1483
|
+
containerPort: 3001,
|
|
1484
|
+
healthCheck: {
|
|
1485
|
+
path: "/health",
|
|
1486
|
+
intervalSeconds: 30,
|
|
1487
|
+
timeoutSeconds: 5,
|
|
1488
|
+
healthyThreshold: 2,
|
|
1489
|
+
unhealthyThreshold: 3
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1492
|
+
const createDeploy = await this.runner.run(
|
|
1493
|
+
"aws",
|
|
1494
|
+
[
|
|
1495
|
+
"lightsail",
|
|
1496
|
+
"create-container-service-deployment",
|
|
1497
|
+
"--service-name",
|
|
1498
|
+
serviceName,
|
|
1499
|
+
"--containers",
|
|
1500
|
+
containers,
|
|
1501
|
+
"--public-endpoint",
|
|
1502
|
+
publicEndpoint,
|
|
1503
|
+
"--region",
|
|
1504
|
+
region
|
|
1505
|
+
],
|
|
1506
|
+
context.projectRoot
|
|
1507
|
+
);
|
|
1508
|
+
if (createDeploy.exitCode !== 0) {
|
|
1509
|
+
throw new Error(`Lightsail deployment failed: ${createDeploy.stderr}`);
|
|
1510
|
+
}
|
|
1511
|
+
const serviceInfo = await this.runner.run(
|
|
1512
|
+
"aws",
|
|
1513
|
+
["lightsail", "get-container-services", "--service-name", serviceName, "--region", region],
|
|
1514
|
+
context.projectRoot
|
|
1515
|
+
);
|
|
1516
|
+
const rawUrl = parseLightsailUrl(serviceInfo.stdout) ?? `https://${serviceName}.${region}.cs.amazonlightsail.com`;
|
|
1517
|
+
const serviceUrl = rawUrl.replace(/\/+$/, "");
|
|
1518
|
+
const deploymentId = (/* @__PURE__ */ new Date()).toISOString();
|
|
1519
|
+
return {
|
|
1520
|
+
deploymentId,
|
|
1521
|
+
liveUrl: serviceUrl,
|
|
1522
|
+
syncUrl: `${serviceUrl.replace("https://", "wss://")}/kora-sync`
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
async rollback(_deploymentId) {
|
|
1526
|
+
const context = this.requireContext();
|
|
1527
|
+
const region = context.region ?? "us-east-1";
|
|
1528
|
+
const serviceName = sanitizeLightsailName(context.appName);
|
|
1529
|
+
const deployments = await this.runner.run(
|
|
1530
|
+
"aws",
|
|
1531
|
+
[
|
|
1532
|
+
"lightsail",
|
|
1533
|
+
"get-container-service-deployments",
|
|
1534
|
+
"--service-name",
|
|
1535
|
+
serviceName,
|
|
1536
|
+
"--region",
|
|
1537
|
+
region
|
|
1538
|
+
],
|
|
1539
|
+
context.projectRoot
|
|
1540
|
+
);
|
|
1541
|
+
if (deployments.exitCode !== 0) {
|
|
1542
|
+
throw new Error(`Lightsail rollback failed: ${deployments.stderr}`);
|
|
1543
|
+
}
|
|
1544
|
+
const previousDeployment = parsePreviousDeployment(deployments.stdout, serviceName);
|
|
1545
|
+
if (!previousDeployment) {
|
|
1546
|
+
throw new Error("No previous deployment found to rollback to.");
|
|
1547
|
+
}
|
|
1548
|
+
const redeploy = await this.runner.run(
|
|
1549
|
+
"aws",
|
|
1550
|
+
[
|
|
1551
|
+
"lightsail",
|
|
1552
|
+
"create-container-service-deployment",
|
|
1553
|
+
"--service-name",
|
|
1554
|
+
serviceName,
|
|
1555
|
+
"--containers",
|
|
1556
|
+
JSON.stringify(previousDeployment.containers),
|
|
1557
|
+
"--public-endpoint",
|
|
1558
|
+
JSON.stringify(previousDeployment.publicEndpoint),
|
|
1559
|
+
"--region",
|
|
1560
|
+
region
|
|
1561
|
+
],
|
|
1562
|
+
context.projectRoot
|
|
1563
|
+
);
|
|
1564
|
+
if (redeploy.exitCode !== 0) {
|
|
1565
|
+
throw new Error(`Lightsail rollback deployment failed: ${redeploy.stderr}`);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
async *logs(options) {
|
|
1569
|
+
const context = this.requireContext();
|
|
1570
|
+
const region = context.region ?? "us-east-1";
|
|
1571
|
+
const serviceName = sanitizeLightsailName(context.appName);
|
|
1572
|
+
const args = [
|
|
1573
|
+
"lightsail",
|
|
1574
|
+
"get-container-log",
|
|
1575
|
+
"--service-name",
|
|
1576
|
+
serviceName,
|
|
1577
|
+
"--container-name",
|
|
1578
|
+
serviceName,
|
|
1579
|
+
"--region",
|
|
1580
|
+
region
|
|
1581
|
+
];
|
|
1582
|
+
if (options.since) {
|
|
1583
|
+
args.push("--start-time", options.since);
|
|
1584
|
+
}
|
|
1585
|
+
const result = await this.runner.run("aws", args, context.projectRoot);
|
|
1586
|
+
if (result.exitCode !== 0) {
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
try {
|
|
1590
|
+
const parsed = JSON.parse(result.stdout);
|
|
1591
|
+
const events = parsed.logEvents ?? [];
|
|
1592
|
+
const limited = options.tail ? events.slice(-options.tail) : events;
|
|
1593
|
+
for (const event of limited) {
|
|
1594
|
+
yield {
|
|
1595
|
+
timestamp: event.createdAt,
|
|
1596
|
+
level: inferLogLevel2(event.message),
|
|
1597
|
+
message: event.message
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
} catch {
|
|
1601
|
+
for (const line of result.stdout.split("\n").filter(Boolean)) {
|
|
1602
|
+
yield { timestamp: (/* @__PURE__ */ new Date()).toISOString(), level: "info", message: line };
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
async status() {
|
|
1607
|
+
const context = this.requireContext();
|
|
1608
|
+
const region = context.region ?? "us-east-1";
|
|
1609
|
+
const serviceName = sanitizeLightsailName(context.appName);
|
|
1610
|
+
const result = await this.runner.run(
|
|
1611
|
+
"aws",
|
|
1612
|
+
["lightsail", "get-container-services", "--service-name", serviceName, "--region", region],
|
|
1613
|
+
context.projectRoot
|
|
1614
|
+
);
|
|
1615
|
+
if (result.exitCode !== 0) {
|
|
1616
|
+
return { state: "failed", message: result.stderr };
|
|
1617
|
+
}
|
|
1618
|
+
try {
|
|
1619
|
+
const parsed = JSON.parse(result.stdout);
|
|
1620
|
+
const service = parsed.containerServices?.[0];
|
|
1621
|
+
if (!service) {
|
|
1622
|
+
return { state: "unknown", message: "Container service not found" };
|
|
1623
|
+
}
|
|
1624
|
+
const deployState = service.currentDeployment?.state ?? "UNKNOWN";
|
|
1625
|
+
if (service.state === "RUNNING" && deployState === "ACTIVE") {
|
|
1626
|
+
return {
|
|
1627
|
+
state: "healthy",
|
|
1628
|
+
message: "Lightsail container service is running",
|
|
1629
|
+
liveUrl: service.url
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
if (service.state === "DEPLOYING" || deployState === "ACTIVATING") {
|
|
1633
|
+
return {
|
|
1634
|
+
state: "pending",
|
|
1635
|
+
message: `Service: ${service.state}, Deployment: ${deployState}`
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
return {
|
|
1639
|
+
state: service.state === "DISABLED" ? "failed" : "unknown",
|
|
1640
|
+
message: `Service: ${service.state}, Deployment: ${deployState}`
|
|
1641
|
+
};
|
|
1642
|
+
} catch {
|
|
1643
|
+
return { state: "unknown", message: "Could not parse service status" };
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
requireContext() {
|
|
1647
|
+
if (!this.currentContext) {
|
|
1648
|
+
throw new Error("AWS Lightsail adapter context is not initialized. Run provision() first.");
|
|
1649
|
+
}
|
|
1650
|
+
return this.currentContext;
|
|
1651
|
+
}
|
|
1652
|
+
};
|
|
1653
|
+
var NodeAwsLightsailCommandRunner = class {
|
|
1654
|
+
async run(command, args, cwd) {
|
|
1655
|
+
return new Promise((resolve3) => {
|
|
1656
|
+
const child = (0, import_node_child_process3.spawn)(command, args, {
|
|
1657
|
+
cwd,
|
|
1658
|
+
env: process.env,
|
|
1659
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1660
|
+
});
|
|
1661
|
+
let stdout = "";
|
|
1662
|
+
let stderr = "";
|
|
1663
|
+
child.stdout?.on("data", (chunk) => {
|
|
1664
|
+
stdout += chunk.toString("utf-8");
|
|
1665
|
+
});
|
|
1666
|
+
child.stderr?.on("data", (chunk) => {
|
|
1667
|
+
stderr += chunk.toString("utf-8");
|
|
1668
|
+
});
|
|
1669
|
+
child.on("error", (error) => {
|
|
1670
|
+
resolve3({ exitCode: 1, stdout, stderr: `${stderr}
|
|
1671
|
+
${error.message}` });
|
|
1672
|
+
});
|
|
1673
|
+
child.on("exit", (code) => {
|
|
1674
|
+
resolve3({ exitCode: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() });
|
|
1675
|
+
});
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
var PASSTHROUGH_ENV_VARS = ["DATABASE_URL", "AUTH_SECRET", "PUBLIC_URL", "NODE_ENV"];
|
|
1680
|
+
function sanitizeLightsailName(name) {
|
|
1681
|
+
return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "").slice(0, 255) || "kora-app";
|
|
1682
|
+
}
|
|
1683
|
+
function parseLightsailImageRef(output) {
|
|
1684
|
+
const match = output.match(/Refer to this image as\s+"?(:[^"\s]+)"?/i);
|
|
1685
|
+
if (match?.[1]) {
|
|
1686
|
+
return match[1];
|
|
1687
|
+
}
|
|
1688
|
+
const refMatch = output.match(/(:\S+\.\S+\.\d+)/);
|
|
1689
|
+
return refMatch?.[1] ?? null;
|
|
1690
|
+
}
|
|
1691
|
+
function parseLightsailUrl(rawJson) {
|
|
1692
|
+
try {
|
|
1693
|
+
const parsed = JSON.parse(rawJson);
|
|
1694
|
+
const url = parsed.containerServices?.[0]?.url;
|
|
1695
|
+
if (typeof url === "string" && url.length > 0) {
|
|
1696
|
+
return url.startsWith("https://") ? url : `https://${url}`;
|
|
1697
|
+
}
|
|
1698
|
+
return null;
|
|
1699
|
+
} catch {
|
|
1700
|
+
return null;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
function parsePreviousDeployment(rawJson, serviceName) {
|
|
1704
|
+
try {
|
|
1705
|
+
const parsed = JSON.parse(rawJson);
|
|
1706
|
+
const deployments = parsed.deployments ?? [];
|
|
1707
|
+
const previous = deployments.length > 1 ? deployments[1] : null;
|
|
1708
|
+
if (!previous) return null;
|
|
1709
|
+
return {
|
|
1710
|
+
containers: previous.containers,
|
|
1711
|
+
publicEndpoint: previous.publicEndpoint
|
|
1712
|
+
};
|
|
1713
|
+
} catch {
|
|
1714
|
+
return null;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
function inferLogLevel2(line) {
|
|
1718
|
+
const normalized = line.toLowerCase();
|
|
1719
|
+
if (normalized.includes("error")) return "error";
|
|
1720
|
+
if (normalized.includes("warn")) return "warn";
|
|
1721
|
+
if (normalized.includes("debug")) return "debug";
|
|
1722
|
+
return "info";
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// src/commands/deploy/adapters/fly-adapter.ts
|
|
1726
|
+
var import_node_child_process4 = require("child_process");
|
|
1727
|
+
var import_node_path8 = require("path");
|
|
1728
|
+
|
|
1729
|
+
// src/commands/deploy/artifacts/fly-toml-generator.ts
|
|
1730
|
+
var import_promises6 = require("fs/promises");
|
|
1731
|
+
var import_node_path7 = require("path");
|
|
1732
|
+
var GENERATED_HEADER = "# Generated by kora deploy \u2014 do not edit manually";
|
|
1733
|
+
function generateFlyToml(options) {
|
|
1734
|
+
const internalPort = options.internalPort ?? 3e3;
|
|
1735
|
+
return [
|
|
1736
|
+
GENERATED_HEADER,
|
|
1737
|
+
"",
|
|
1738
|
+
`app = "${options.appName}"`,
|
|
1739
|
+
`primary_region = "${options.region}"`,
|
|
1740
|
+
"",
|
|
1741
|
+
"[build]",
|
|
1742
|
+
` dockerfile = "Dockerfile"`,
|
|
1743
|
+
"",
|
|
1744
|
+
"[http_service]",
|
|
1745
|
+
` internal_port = ${String(internalPort)}`,
|
|
1746
|
+
" force_https = true",
|
|
1747
|
+
' auto_stop_machines = "stop"',
|
|
1748
|
+
" auto_start_machines = true",
|
|
1749
|
+
" min_machines_running = 0",
|
|
1750
|
+
"",
|
|
1751
|
+
" [[http_service.checks]]",
|
|
1752
|
+
' grace_period = "10s"',
|
|
1753
|
+
' interval = "30s"',
|
|
1754
|
+
' method = "GET"',
|
|
1755
|
+
' timeout = "5s"',
|
|
1756
|
+
' path = "/health"',
|
|
1757
|
+
""
|
|
1758
|
+
].join("\n");
|
|
1759
|
+
}
|
|
1760
|
+
async function writeFlyTomlArtifact(deployDirectory, options) {
|
|
1761
|
+
await (0, import_promises6.mkdir)(deployDirectory, { recursive: true });
|
|
1762
|
+
const filePath = (0, import_node_path7.join)(deployDirectory, "fly.toml");
|
|
1763
|
+
await (0, import_promises6.writeFile)(filePath, generateFlyToml(options), "utf-8");
|
|
1764
|
+
return filePath;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// src/commands/deploy/adapters/fly-adapter.ts
|
|
1768
|
+
var FlyAdapter = class {
|
|
1769
|
+
name = "fly";
|
|
1770
|
+
logger = createLogger();
|
|
1771
|
+
runner;
|
|
1772
|
+
commandCandidates;
|
|
1773
|
+
flyCommand = null;
|
|
1774
|
+
currentContext;
|
|
1775
|
+
lastDeploymentId = null;
|
|
1776
|
+
constructor(options = {}) {
|
|
1777
|
+
this.runner = options.runner ?? new NodeFlyCommandRunner();
|
|
1778
|
+
this.commandCandidates = options.commandCandidates ?? DEFAULT_FLY_COMMAND_CANDIDATES;
|
|
1779
|
+
this.currentContext = options.context ?? null;
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* Seeds runtime context for non-provisioning commands.
|
|
1783
|
+
*/
|
|
1784
|
+
setContext(context) {
|
|
1785
|
+
this.currentContext = context;
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Checks whether a Fly CLI executable can be resolved.
|
|
1789
|
+
*/
|
|
1790
|
+
async detect() {
|
|
1791
|
+
const command = await this.tryResolveFlyCommand(process.cwd());
|
|
1792
|
+
return command !== null;
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Ensures Fly CLI is available before deployment.
|
|
1796
|
+
*/
|
|
1797
|
+
async install() {
|
|
1798
|
+
const available = await this.detect();
|
|
1799
|
+
if (!available) {
|
|
1800
|
+
throw new Error(
|
|
1801
|
+
"Fly CLI is required but not installed. Install from https://fly.io/docs/hands-on/install-flyctl/."
|
|
1802
|
+
);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
/**
|
|
1806
|
+
* Performs Fly authentication check/login.
|
|
1807
|
+
*/
|
|
1808
|
+
async authenticate() {
|
|
1809
|
+
const projectRoot = this.currentContext?.projectRoot ?? process.cwd();
|
|
1810
|
+
const status = await this.runFlyCommand(["auth", "whoami", "--json"], projectRoot, false);
|
|
1811
|
+
if (status.exitCode === 0) return;
|
|
1812
|
+
const login = await this.runFlyCommand(["auth", "login"], projectRoot, true);
|
|
1813
|
+
if (login.exitCode !== 0) {
|
|
1814
|
+
throw new Error(`Fly authentication failed: ${normalizeError(login.stderr, login.stdout)}`);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Provisions app and optionally region-bound resources in Fly.
|
|
1819
|
+
*/
|
|
1820
|
+
async provision(config) {
|
|
1821
|
+
this.currentContext = {
|
|
1822
|
+
projectRoot: config.projectRoot,
|
|
1823
|
+
appName: config.appName,
|
|
1824
|
+
region: config.region ?? "iad"
|
|
1825
|
+
};
|
|
1826
|
+
const appCreateArgs = ["apps", "create", config.appName];
|
|
1827
|
+
const createApp = await this.runFlyCommand(appCreateArgs, config.projectRoot, false);
|
|
1828
|
+
if (createApp.exitCode !== 0 && !isAlreadyExistsResponse(createApp.stderr, createApp.stdout)) {
|
|
1829
|
+
throw new Error(
|
|
1830
|
+
`Fly app provisioning failed for "${config.appName}": ${normalizeError(createApp.stderr, createApp.stdout)}`
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1833
|
+
const secrets = [];
|
|
1834
|
+
const portSecret = await this.runFlyCommand(
|
|
1835
|
+
["secrets", "set", "PORT=3000", "--app", config.appName],
|
|
1836
|
+
config.projectRoot,
|
|
1837
|
+
false
|
|
1838
|
+
);
|
|
1839
|
+
if (portSecret.exitCode === 0) {
|
|
1840
|
+
secrets.push("PORT");
|
|
1841
|
+
}
|
|
1842
|
+
return {
|
|
1843
|
+
applicationId: config.appName,
|
|
1844
|
+
databaseId: null,
|
|
1845
|
+
secretsSet: secrets
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
/**
|
|
1849
|
+
* Builds local artifacts consumed by Fly deploy.
|
|
1850
|
+
*/
|
|
1851
|
+
async build(config) {
|
|
1852
|
+
this.currentContext = {
|
|
1853
|
+
projectRoot: config.projectRoot,
|
|
1854
|
+
appName: config.appName,
|
|
1855
|
+
region: config.region
|
|
1856
|
+
};
|
|
1857
|
+
const deployDirectory = (0, import_node_path8.join)(config.projectRoot, ".kora", "deploy");
|
|
1858
|
+
await writeFlyTomlArtifact(deployDirectory, {
|
|
1859
|
+
appName: config.appName,
|
|
1860
|
+
region: config.region ?? "iad"
|
|
1861
|
+
});
|
|
1862
|
+
await bundleServer({
|
|
1863
|
+
projectRoot: config.projectRoot,
|
|
1864
|
+
deployDirectory
|
|
1865
|
+
});
|
|
1866
|
+
const client = config.deployTarget === "sync-server" ? null : await buildClient({
|
|
1867
|
+
projectRoot: config.projectRoot,
|
|
1868
|
+
outDir: (0, import_node_path8.join)(deployDirectory, "dist"),
|
|
1869
|
+
mode: "production"
|
|
1870
|
+
});
|
|
1871
|
+
return {
|
|
1872
|
+
clientDirectory: client?.outDir ?? null,
|
|
1873
|
+
serverBundlePath: (0, import_node_path8.join)(deployDirectory, "server-bundled.js"),
|
|
1874
|
+
deployDirectory
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* Deploys generated artifacts to Fly and returns live endpoints.
|
|
1879
|
+
*/
|
|
1880
|
+
async deploy(artifacts) {
|
|
1881
|
+
const context = this.requireContext();
|
|
1882
|
+
const deploy = await this.runFlyCommand(
|
|
1883
|
+
["deploy", "--config", "fly.toml", "--app", context.appName],
|
|
1884
|
+
artifacts.deployDirectory,
|
|
1885
|
+
true
|
|
1886
|
+
);
|
|
1887
|
+
if (deploy.exitCode !== 0) {
|
|
1888
|
+
throw new Error(`Fly deployment failed: ${normalizeError(deploy.stderr, deploy.stdout)}`);
|
|
1889
|
+
}
|
|
1890
|
+
const info = await this.runFlyCommand(
|
|
1891
|
+
["status", "--app", context.appName, "--json"],
|
|
1892
|
+
context.projectRoot,
|
|
1893
|
+
false
|
|
1894
|
+
);
|
|
1895
|
+
const hostname = parseFlyHostname(info.stdout) ?? `${context.appName}.fly.dev`;
|
|
1896
|
+
const deploymentId = parseFlyDeploymentId(info.stdout) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1897
|
+
this.lastDeploymentId = deploymentId;
|
|
1898
|
+
return {
|
|
1899
|
+
deploymentId,
|
|
1900
|
+
liveUrl: `https://${hostname}`,
|
|
1901
|
+
syncUrl: `wss://${hostname}/kora-sync`
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* Rolls back to a deployment version.
|
|
1906
|
+
*/
|
|
1907
|
+
async rollback(deploymentId) {
|
|
1908
|
+
const context = this.requireContext();
|
|
1909
|
+
const rollback = await this.runFlyCommand(
|
|
1910
|
+
["releases", "revert", deploymentId, "--app", context.appName],
|
|
1911
|
+
context.projectRoot,
|
|
1912
|
+
true
|
|
1913
|
+
);
|
|
1914
|
+
if (rollback.exitCode !== 0) {
|
|
1915
|
+
throw new Error(`Fly rollback failed: ${normalizeError(rollback.stderr, rollback.stdout)}`);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
/**
|
|
1919
|
+
* Returns deployment logs as an async iterable.
|
|
1920
|
+
*/
|
|
1921
|
+
async *logs(options) {
|
|
1922
|
+
const context = this.requireContext();
|
|
1923
|
+
const args = ["logs", "--app", context.appName, "--no-tail"];
|
|
1924
|
+
if (typeof options.since === "string" && options.since.length > 0) {
|
|
1925
|
+
args.push("--region", options.since);
|
|
1926
|
+
}
|
|
1927
|
+
const result = await this.runFlyCommand(args, context.projectRoot, false);
|
|
1928
|
+
if (result.exitCode !== 0) {
|
|
1929
|
+
throw new Error(`Fly logs failed: ${normalizeError(result.stderr, result.stdout)}`);
|
|
1930
|
+
}
|
|
1931
|
+
const lines = result.stdout.split(/\r?\n/).filter((line) => line.length > 0);
|
|
1932
|
+
for (const line of lines) {
|
|
1933
|
+
yield {
|
|
1934
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1935
|
+
level: inferLogLevel3(line),
|
|
1936
|
+
message: line
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
/**
|
|
1941
|
+
* Reads current deployment status from Fly.
|
|
1942
|
+
*/
|
|
1943
|
+
async status() {
|
|
1944
|
+
const context = this.requireContext();
|
|
1945
|
+
const status = await this.runFlyCommand(
|
|
1946
|
+
["status", "--app", context.appName, "--json"],
|
|
1947
|
+
context.projectRoot,
|
|
1948
|
+
false
|
|
1949
|
+
);
|
|
1950
|
+
if (status.exitCode !== 0) {
|
|
1951
|
+
return {
|
|
1952
|
+
state: "failed",
|
|
1953
|
+
message: normalizeError(status.stderr, status.stdout)
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
const hostname = parseFlyHostname(status.stdout);
|
|
1957
|
+
return {
|
|
1958
|
+
state: "healthy",
|
|
1959
|
+
message: "Fly deployment is healthy.",
|
|
1960
|
+
liveUrl: hostname ? `https://${hostname}` : void 0
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
async runFlyCommand(flyArgs, cwd, inheritOutput) {
|
|
1964
|
+
const flyBinary = await this.resolveFlyCommand(cwd);
|
|
1965
|
+
if (inheritOutput) {
|
|
1966
|
+
this.logger.step(`fly ${flyArgs.join(" ")}`);
|
|
1967
|
+
}
|
|
1968
|
+
return await this.runner.run(flyBinary, flyArgs, cwd);
|
|
1969
|
+
}
|
|
1970
|
+
requireContext() {
|
|
1971
|
+
if (!this.currentContext) {
|
|
1972
|
+
throw new Error("Fly adapter context is not initialized. Run provision() first.");
|
|
1973
|
+
}
|
|
1974
|
+
return this.currentContext;
|
|
1975
|
+
}
|
|
1976
|
+
async resolveFlyCommand(cwd) {
|
|
1977
|
+
if (this.flyCommand) {
|
|
1978
|
+
return this.flyCommand;
|
|
1979
|
+
}
|
|
1980
|
+
const resolved = await this.tryResolveFlyCommand(cwd);
|
|
1981
|
+
if (!resolved) {
|
|
1982
|
+
throw new Error("Could not resolve a usable Fly CLI command (tried flyctl, fly).");
|
|
1983
|
+
}
|
|
1984
|
+
this.flyCommand = resolved;
|
|
1985
|
+
return resolved;
|
|
1986
|
+
}
|
|
1987
|
+
async tryResolveFlyCommand(cwd) {
|
|
1988
|
+
for (const command of this.commandCandidates) {
|
|
1989
|
+
const versionCheck = await this.runner.run(command, ["version"], cwd);
|
|
1990
|
+
if (versionCheck.exitCode === 0) {
|
|
1991
|
+
return command;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
return null;
|
|
1995
|
+
}
|
|
1996
|
+
};
|
|
1997
|
+
var NodeFlyCommandRunner = class {
|
|
1998
|
+
async run(command, args, cwd) {
|
|
1999
|
+
return await new Promise((resolve3) => {
|
|
2000
|
+
const child = (0, import_node_child_process4.spawn)(command, args, {
|
|
2001
|
+
cwd,
|
|
2002
|
+
env: process.env,
|
|
2003
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2004
|
+
});
|
|
2005
|
+
let stdout = "";
|
|
2006
|
+
let stderr = "";
|
|
2007
|
+
child.stdout?.on("data", (chunk) => {
|
|
2008
|
+
stdout += chunk.toString("utf-8");
|
|
2009
|
+
});
|
|
2010
|
+
child.stderr?.on("data", (chunk) => {
|
|
2011
|
+
stderr += chunk.toString("utf-8");
|
|
2012
|
+
});
|
|
2013
|
+
child.on("error", (error) => {
|
|
2014
|
+
resolve3({
|
|
2015
|
+
exitCode: 1,
|
|
2016
|
+
stdout,
|
|
2017
|
+
stderr: `${stderr}
|
|
2018
|
+
${error.message}`
|
|
2019
|
+
});
|
|
2020
|
+
});
|
|
2021
|
+
child.on("exit", (code) => {
|
|
2022
|
+
resolve3({
|
|
2023
|
+
exitCode: code ?? 1,
|
|
2024
|
+
stdout: stdout.trim(),
|
|
2025
|
+
stderr: stderr.trim()
|
|
2026
|
+
});
|
|
2027
|
+
});
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
};
|
|
2031
|
+
var DEFAULT_FLY_COMMAND_CANDIDATES = ["flyctl", "fly"];
|
|
2032
|
+
function parseFlyHostname(rawJson) {
|
|
2033
|
+
const parsed = parseJsonRecord(rawJson);
|
|
2034
|
+
if (!parsed) return null;
|
|
2035
|
+
const hostname = parsed.Hostname;
|
|
2036
|
+
if (typeof hostname === "string" && hostname.length > 0) {
|
|
2037
|
+
return hostname;
|
|
2038
|
+
}
|
|
2039
|
+
const hostnames = parsed.Hostnames;
|
|
2040
|
+
if (Array.isArray(hostnames)) {
|
|
2041
|
+
const first = hostnames.find((item) => typeof item === "string");
|
|
2042
|
+
if (typeof first === "string" && first.length > 0) {
|
|
2043
|
+
return first;
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
return null;
|
|
2047
|
+
}
|
|
2048
|
+
function parseFlyDeploymentId(rawJson) {
|
|
2049
|
+
const parsed = parseJsonRecord(rawJson);
|
|
2050
|
+
if (!parsed) return null;
|
|
2051
|
+
const deploymentId = parsed.DeploymentID;
|
|
2052
|
+
if (typeof deploymentId === "string" && deploymentId.length > 0) {
|
|
2053
|
+
return deploymentId;
|
|
2054
|
+
}
|
|
2055
|
+
const latestDeployment = parsed.LatestDeployment;
|
|
2056
|
+
if (typeof latestDeployment === "object" && latestDeployment !== null) {
|
|
2057
|
+
const id = latestDeployment.ID;
|
|
2058
|
+
if (typeof id === "string" && id.length > 0) {
|
|
2059
|
+
return id;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
return null;
|
|
2063
|
+
}
|
|
2064
|
+
function parseJsonRecord(value) {
|
|
2065
|
+
try {
|
|
2066
|
+
const parsed = JSON.parse(value);
|
|
2067
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
2068
|
+
return parsed;
|
|
2069
|
+
}
|
|
2070
|
+
return null;
|
|
2071
|
+
} catch {
|
|
2072
|
+
return null;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
function inferLogLevel3(line) {
|
|
2076
|
+
const normalized = line.toLowerCase();
|
|
2077
|
+
if (normalized.includes("error")) return "error";
|
|
2078
|
+
if (normalized.includes("warn")) return "warn";
|
|
2079
|
+
if (normalized.includes("debug")) return "debug";
|
|
2080
|
+
return "info";
|
|
2081
|
+
}
|
|
2082
|
+
function normalizeError(stderr, stdout) {
|
|
2083
|
+
if (stderr.length > 0) return stderr;
|
|
2084
|
+
if (stdout.length > 0) return stdout;
|
|
2085
|
+
return "unknown fly CLI error";
|
|
2086
|
+
}
|
|
2087
|
+
function isAlreadyExistsResponse(stderr, stdout) {
|
|
2088
|
+
const text = `${stderr}
|
|
2089
|
+
${stdout}`.toLowerCase();
|
|
2090
|
+
return text.includes("already exists") || text.includes("already been taken");
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
// src/commands/deploy/adapters/railway-adapter.ts
|
|
2094
|
+
var import_node_child_process5 = require("child_process");
|
|
2095
|
+
var import_node_path10 = require("path");
|
|
2096
|
+
|
|
2097
|
+
// src/commands/deploy/artifacts/railway-json-generator.ts
|
|
2098
|
+
var import_promises7 = require("fs/promises");
|
|
2099
|
+
var import_node_path9 = require("path");
|
|
2100
|
+
var GENERATED_HEADER2 = "Generated by kora deploy - do not edit manually.";
|
|
2101
|
+
function generateRailwayJson(options) {
|
|
2102
|
+
const file = {
|
|
2103
|
+
$schema: "https://railway.app/railway.schema.json",
|
|
2104
|
+
build: {
|
|
2105
|
+
builder: "DOCKERFILE",
|
|
2106
|
+
dockerfilePath: "Dockerfile"
|
|
2107
|
+
},
|
|
2108
|
+
deploy: {
|
|
2109
|
+
startCommand: options.startCommand ?? "node ./server-bundled.js",
|
|
2110
|
+
healthcheckPath: options.healthcheckPath ?? "/health",
|
|
2111
|
+
restartPolicyType: "on_failure",
|
|
2112
|
+
restartPolicyMaxRetries: 3
|
|
2113
|
+
},
|
|
2114
|
+
metadata: {
|
|
2115
|
+
generatedBy: GENERATED_HEADER2,
|
|
2116
|
+
appName: options.appName,
|
|
2117
|
+
region: options.region
|
|
2118
|
+
}
|
|
2119
|
+
};
|
|
2120
|
+
return `${JSON.stringify(file, null, 2)}
|
|
2121
|
+
`;
|
|
2122
|
+
}
|
|
2123
|
+
async function writeRailwayJsonArtifact(deployDirectory, options) {
|
|
2124
|
+
await (0, import_promises7.mkdir)(deployDirectory, { recursive: true });
|
|
2125
|
+
const filePath = (0, import_node_path9.join)(deployDirectory, "railway.json");
|
|
2126
|
+
await (0, import_promises7.writeFile)(filePath, generateRailwayJson(options), "utf-8");
|
|
2127
|
+
return filePath;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
// src/commands/deploy/adapters/railway-adapter.ts
|
|
2131
|
+
var RailwayAdapter = class {
|
|
2132
|
+
name = "railway";
|
|
2133
|
+
logger = createLogger();
|
|
2134
|
+
runner;
|
|
2135
|
+
commandCandidates;
|
|
2136
|
+
railwayCommand = null;
|
|
2137
|
+
currentContext;
|
|
2138
|
+
lastDeploymentId = null;
|
|
2139
|
+
constructor(options = {}) {
|
|
2140
|
+
this.runner = options.runner ?? new NodeRailwayCommandRunner();
|
|
2141
|
+
this.commandCandidates = options.commandCandidates ?? DEFAULT_RAILWAY_COMMAND_CANDIDATES;
|
|
2142
|
+
this.currentContext = options.context ?? null;
|
|
2143
|
+
}
|
|
2144
|
+
setContext(context) {
|
|
2145
|
+
this.currentContext = context;
|
|
2146
|
+
}
|
|
2147
|
+
async detect() {
|
|
2148
|
+
const command = await this.tryResolveRailwayCommand(process.cwd());
|
|
2149
|
+
return command !== null;
|
|
2150
|
+
}
|
|
2151
|
+
async install() {
|
|
2152
|
+
const available = await this.detect();
|
|
2153
|
+
if (!available) {
|
|
2154
|
+
throw new Error(
|
|
2155
|
+
"Railway CLI is required but not installed. Install with `npm i -g @railway/cli` or see https://docs.railway.com/guides/cli."
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
async authenticate() {
|
|
2160
|
+
const projectRoot = this.currentContext?.projectRoot ?? process.cwd();
|
|
2161
|
+
const whoami = await this.runRailwayCommand(["whoami"], projectRoot, false);
|
|
2162
|
+
if (whoami.exitCode === 0) return;
|
|
2163
|
+
const login = await this.runRailwayCommand(["login"], projectRoot, true);
|
|
2164
|
+
if (login.exitCode !== 0) {
|
|
2165
|
+
throw new Error(
|
|
2166
|
+
`Railway authentication failed: ${normalizeError2(login.stderr, login.stdout)}`
|
|
2167
|
+
);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
async provision(config) {
|
|
2171
|
+
this.currentContext = {
|
|
2172
|
+
projectRoot: config.projectRoot,
|
|
2173
|
+
appName: config.appName,
|
|
2174
|
+
region: config.region
|
|
2175
|
+
};
|
|
2176
|
+
const init = await this.runRailwayCommand(
|
|
2177
|
+
["init", "--name", config.appName, "--confirm"],
|
|
2178
|
+
config.projectRoot,
|
|
2179
|
+
false
|
|
2180
|
+
);
|
|
2181
|
+
if (init.exitCode !== 0 && !isAlreadyExistsResponse2(init.stderr, init.stdout)) {
|
|
2182
|
+
throw new Error(
|
|
2183
|
+
`Railway project provisioning failed for "${config.appName}": ${normalizeError2(init.stderr, init.stdout)}`
|
|
2184
|
+
);
|
|
2185
|
+
}
|
|
2186
|
+
const link = await this.runRailwayCommand(
|
|
2187
|
+
["link", "--project", config.appName, "--environment", config.environment, "--yes"],
|
|
2188
|
+
config.projectRoot,
|
|
2189
|
+
false
|
|
2190
|
+
);
|
|
2191
|
+
if (link.exitCode !== 0 && !isAlreadyExistsResponse2(link.stderr, link.stdout)) {
|
|
2192
|
+
throw new Error(`Railway link failed: ${normalizeError2(link.stderr, link.stdout)}`);
|
|
2193
|
+
}
|
|
2194
|
+
const vars = [];
|
|
2195
|
+
const setPort = await this.runRailwayCommand(
|
|
2196
|
+
["variables", "set", "PORT=3000", "--yes"],
|
|
2197
|
+
config.projectRoot,
|
|
2198
|
+
false
|
|
2199
|
+
);
|
|
2200
|
+
if (setPort.exitCode === 0) {
|
|
2201
|
+
vars.push("PORT");
|
|
2202
|
+
}
|
|
2203
|
+
return {
|
|
2204
|
+
applicationId: config.appName,
|
|
2205
|
+
databaseId: null,
|
|
2206
|
+
secretsSet: vars
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
async build(config) {
|
|
2210
|
+
this.currentContext = {
|
|
2211
|
+
projectRoot: config.projectRoot,
|
|
2212
|
+
appName: config.appName,
|
|
2213
|
+
region: config.region
|
|
2214
|
+
};
|
|
2215
|
+
const deployDirectory = (0, import_node_path10.join)(config.projectRoot, ".kora", "deploy");
|
|
2216
|
+
await writeRailwayJsonArtifact(deployDirectory, {
|
|
2217
|
+
appName: config.appName,
|
|
2218
|
+
environment: config.environment
|
|
2219
|
+
});
|
|
2220
|
+
await bundleServer({
|
|
2221
|
+
projectRoot: config.projectRoot,
|
|
2222
|
+
deployDirectory
|
|
2223
|
+
});
|
|
2224
|
+
const client = config.deployTarget === "sync-server" ? null : await buildClient({
|
|
2225
|
+
projectRoot: config.projectRoot,
|
|
2226
|
+
outDir: (0, import_node_path10.join)(deployDirectory, "dist"),
|
|
2227
|
+
mode: "production"
|
|
2228
|
+
});
|
|
2229
|
+
return {
|
|
2230
|
+
clientDirectory: client?.outDir ?? null,
|
|
2231
|
+
serverBundlePath: (0, import_node_path10.join)(deployDirectory, "server-bundled.js"),
|
|
2232
|
+
deployDirectory
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
async deploy(artifacts) {
|
|
2236
|
+
const context = this.requireContext();
|
|
2237
|
+
const up = await this.runRailwayCommand(["up", "--yes"], artifacts.deployDirectory, true);
|
|
2238
|
+
if (up.exitCode !== 0) {
|
|
2239
|
+
throw new Error(`Railway deployment failed: ${normalizeError2(up.stderr, up.stdout)}`);
|
|
2240
|
+
}
|
|
2241
|
+
const status = await this.runRailwayCommand(["status", "--json"], context.projectRoot, false);
|
|
2242
|
+
const deploymentId = parseRailwayDeploymentId(status.stdout) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2243
|
+
const liveUrl = parseRailwayUrl(status.stdout) ?? `https://${context.appName}.up.railway.app`;
|
|
2244
|
+
this.lastDeploymentId = deploymentId;
|
|
2245
|
+
return {
|
|
2246
|
+
deploymentId,
|
|
2247
|
+
liveUrl,
|
|
2248
|
+
syncUrl: toSyncUrl(liveUrl)
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
async rollback(deploymentId) {
|
|
2252
|
+
const context = this.requireContext();
|
|
2253
|
+
const rollback = await this.runRailwayCommand(
|
|
2254
|
+
["redeploy", "--deployment", deploymentId, "--yes"],
|
|
2255
|
+
context.projectRoot,
|
|
2256
|
+
true
|
|
2257
|
+
);
|
|
2258
|
+
if (rollback.exitCode !== 0) {
|
|
2259
|
+
throw new Error(
|
|
2260
|
+
`Railway rollback failed: ${normalizeError2(rollback.stderr, rollback.stdout)}`
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
async *logs(options) {
|
|
2265
|
+
const context = this.requireContext();
|
|
2266
|
+
const args = ["logs"];
|
|
2267
|
+
if (typeof options.tail === "number" && options.tail > 0) {
|
|
2268
|
+
args.push("--lines", String(options.tail));
|
|
2269
|
+
}
|
|
2270
|
+
const result = await this.runRailwayCommand(args, context.projectRoot, false);
|
|
2271
|
+
if (result.exitCode !== 0) {
|
|
2272
|
+
throw new Error(`Railway logs failed: ${normalizeError2(result.stderr, result.stdout)}`);
|
|
2273
|
+
}
|
|
2274
|
+
const lines = result.stdout.split(/\r?\n/).filter((line) => line.length > 0);
|
|
2275
|
+
for (const line of lines) {
|
|
2276
|
+
yield {
|
|
2277
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2278
|
+
level: inferLogLevel4(line),
|
|
2279
|
+
message: line
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
async status() {
|
|
2284
|
+
const context = this.requireContext();
|
|
2285
|
+
const status = await this.runRailwayCommand(["status", "--json"], context.projectRoot, false);
|
|
2286
|
+
if (status.exitCode !== 0) {
|
|
2287
|
+
return {
|
|
2288
|
+
state: "failed",
|
|
2289
|
+
message: normalizeError2(status.stderr, status.stdout)
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
const liveUrl = parseRailwayUrl(status.stdout);
|
|
2293
|
+
return {
|
|
2294
|
+
state: "healthy",
|
|
2295
|
+
message: "Railway deployment is healthy.",
|
|
2296
|
+
liveUrl: liveUrl ?? void 0
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
2299
|
+
async runRailwayCommand(railwayArgs, cwd, inheritOutput) {
|
|
2300
|
+
const command = await this.resolveRailwayCommand(cwd);
|
|
2301
|
+
if (inheritOutput) {
|
|
2302
|
+
this.logger.step(`railway ${railwayArgs.join(" ")}`);
|
|
2303
|
+
}
|
|
2304
|
+
return await this.runner.run(command, railwayArgs, cwd);
|
|
2305
|
+
}
|
|
2306
|
+
requireContext() {
|
|
2307
|
+
if (!this.currentContext) {
|
|
2308
|
+
throw new Error("Railway adapter context is not initialized. Run provision() first.");
|
|
2309
|
+
}
|
|
2310
|
+
return this.currentContext;
|
|
2311
|
+
}
|
|
2312
|
+
async resolveRailwayCommand(cwd) {
|
|
2313
|
+
if (this.railwayCommand) {
|
|
2314
|
+
return this.railwayCommand;
|
|
2315
|
+
}
|
|
2316
|
+
const resolved = await this.tryResolveRailwayCommand(cwd);
|
|
2317
|
+
if (!resolved) {
|
|
2318
|
+
throw new Error("Could not resolve a usable Railway CLI command (tried railway).");
|
|
2319
|
+
}
|
|
2320
|
+
this.railwayCommand = resolved;
|
|
2321
|
+
return resolved;
|
|
2322
|
+
}
|
|
2323
|
+
async tryResolveRailwayCommand(cwd) {
|
|
2324
|
+
for (const command of this.commandCandidates) {
|
|
2325
|
+
const version = await this.runner.run(command, ["--version"], cwd);
|
|
2326
|
+
if (version.exitCode === 0) {
|
|
2327
|
+
return command;
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
return null;
|
|
2331
|
+
}
|
|
2332
|
+
};
|
|
2333
|
+
var NodeRailwayCommandRunner = class {
|
|
2334
|
+
async run(command, args, cwd) {
|
|
2335
|
+
return await new Promise((resolve3) => {
|
|
2336
|
+
const child = (0, import_node_child_process5.spawn)(command, args, {
|
|
2337
|
+
cwd,
|
|
2338
|
+
env: process.env,
|
|
2339
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2340
|
+
});
|
|
2341
|
+
let stdout = "";
|
|
2342
|
+
let stderr = "";
|
|
2343
|
+
child.stdout?.on("data", (chunk) => {
|
|
2344
|
+
stdout += chunk.toString("utf-8");
|
|
2345
|
+
});
|
|
2346
|
+
child.stderr?.on("data", (chunk) => {
|
|
2347
|
+
stderr += chunk.toString("utf-8");
|
|
2348
|
+
});
|
|
2349
|
+
child.on("error", (error) => {
|
|
2350
|
+
resolve3({
|
|
2351
|
+
exitCode: 1,
|
|
2352
|
+
stdout,
|
|
2353
|
+
stderr: `${stderr}
|
|
2354
|
+
${error.message}`
|
|
2355
|
+
});
|
|
2356
|
+
});
|
|
2357
|
+
child.on("exit", (code) => {
|
|
2358
|
+
resolve3({
|
|
2359
|
+
exitCode: code ?? 1,
|
|
2360
|
+
stdout: stdout.trim(),
|
|
2361
|
+
stderr: stderr.trim()
|
|
2362
|
+
});
|
|
2363
|
+
});
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
};
|
|
2367
|
+
var DEFAULT_RAILWAY_COMMAND_CANDIDATES = ["railway"];
|
|
2368
|
+
function parseRailwayUrl(rawJson) {
|
|
2369
|
+
const record = parseJsonRecord2(rawJson);
|
|
2370
|
+
if (!record) return null;
|
|
2371
|
+
const url = record.url;
|
|
2372
|
+
if (typeof url === "string" && url.length > 0) {
|
|
2373
|
+
return ensureHttpsUrl(url);
|
|
2374
|
+
}
|
|
2375
|
+
const service = record.service;
|
|
2376
|
+
if (typeof service === "object" && service !== null) {
|
|
2377
|
+
const domain = service.domain;
|
|
2378
|
+
if (typeof domain === "string" && domain.length > 0) {
|
|
2379
|
+
return ensureHttpsUrl(domain);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
return null;
|
|
2383
|
+
}
|
|
2384
|
+
function parseRailwayDeploymentId(rawJson) {
|
|
2385
|
+
const record = parseJsonRecord2(rawJson);
|
|
2386
|
+
if (!record) return null;
|
|
2387
|
+
const deploymentId = record.deploymentId;
|
|
2388
|
+
if (typeof deploymentId === "string" && deploymentId.length > 0) {
|
|
2389
|
+
return deploymentId;
|
|
2390
|
+
}
|
|
2391
|
+
const deployment = record.deployment;
|
|
2392
|
+
if (typeof deployment === "object" && deployment !== null) {
|
|
2393
|
+
const id = deployment.id;
|
|
2394
|
+
if (typeof id === "string" && id.length > 0) {
|
|
2395
|
+
return id;
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
return null;
|
|
2399
|
+
}
|
|
2400
|
+
function parseJsonRecord2(value) {
|
|
2401
|
+
try {
|
|
2402
|
+
const parsed = JSON.parse(value);
|
|
2403
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
2404
|
+
return parsed;
|
|
2405
|
+
}
|
|
2406
|
+
return null;
|
|
2407
|
+
} catch {
|
|
2408
|
+
return null;
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
function toSyncUrl(liveUrl) {
|
|
2412
|
+
try {
|
|
2413
|
+
const url = new URL(liveUrl);
|
|
2414
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
2415
|
+
url.pathname = "/kora-sync";
|
|
2416
|
+
return url.toString().replace(/\/$/, "");
|
|
2417
|
+
} catch {
|
|
2418
|
+
return null;
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
function ensureHttpsUrl(value) {
|
|
2422
|
+
if (value.startsWith("http://") || value.startsWith("https://")) {
|
|
2423
|
+
return value;
|
|
2424
|
+
}
|
|
2425
|
+
return `https://${value}`;
|
|
2426
|
+
}
|
|
2427
|
+
function inferLogLevel4(line) {
|
|
2428
|
+
const normalized = line.toLowerCase();
|
|
2429
|
+
if (normalized.includes("error")) return "error";
|
|
2430
|
+
if (normalized.includes("warn")) return "warn";
|
|
2431
|
+
if (normalized.includes("debug")) return "debug";
|
|
2432
|
+
return "info";
|
|
2433
|
+
}
|
|
2434
|
+
function normalizeError2(stderr, stdout) {
|
|
2435
|
+
if (stderr.length > 0) return stderr;
|
|
2436
|
+
if (stdout.length > 0) return stdout;
|
|
2437
|
+
return "unknown railway CLI error";
|
|
2438
|
+
}
|
|
2439
|
+
function isAlreadyExistsResponse2(stderr, stdout) {
|
|
2440
|
+
const text = `${stderr}
|
|
2441
|
+
${stdout}`.toLowerCase();
|
|
2442
|
+
return text.includes("already exists");
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
// src/commands/deploy/adapters/stub-adapter.ts
|
|
2446
|
+
var StubDeployAdapter = class {
|
|
2447
|
+
name;
|
|
2448
|
+
contextLabel;
|
|
2449
|
+
constructor(platform) {
|
|
2450
|
+
this.name = platform;
|
|
2451
|
+
this.contextLabel = `Deploy adapter "${platform}" is not implemented yet.`;
|
|
2452
|
+
}
|
|
2453
|
+
setContext(_context) {
|
|
2454
|
+
}
|
|
2455
|
+
async detect() {
|
|
2456
|
+
return false;
|
|
2457
|
+
}
|
|
2458
|
+
async install() {
|
|
2459
|
+
throw this.notImplementedError();
|
|
2460
|
+
}
|
|
2461
|
+
async authenticate() {
|
|
2462
|
+
throw this.notImplementedError();
|
|
2463
|
+
}
|
|
2464
|
+
async provision(_config) {
|
|
2465
|
+
throw this.notImplementedError();
|
|
2466
|
+
}
|
|
2467
|
+
async build(_config) {
|
|
2468
|
+
throw this.notImplementedError();
|
|
2469
|
+
}
|
|
2470
|
+
async deploy(_artifacts) {
|
|
2471
|
+
throw this.notImplementedError();
|
|
2472
|
+
}
|
|
2473
|
+
async rollback(_deploymentId) {
|
|
2474
|
+
throw this.notImplementedError();
|
|
2475
|
+
}
|
|
2476
|
+
logs(_options) {
|
|
2477
|
+
throw this.notImplementedError();
|
|
2478
|
+
}
|
|
2479
|
+
async status() {
|
|
2480
|
+
return {
|
|
2481
|
+
state: "unknown",
|
|
2482
|
+
message: this.notImplementedMessage()
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
notImplementedError() {
|
|
2486
|
+
return new Error(this.notImplementedMessage());
|
|
2487
|
+
}
|
|
2488
|
+
notImplementedMessage() {
|
|
2489
|
+
return `${this.contextLabel} Start with --platform fly for now.`;
|
|
2490
|
+
}
|
|
2491
|
+
};
|
|
2492
|
+
|
|
2493
|
+
// src/commands/deploy/adapters/factory.ts
|
|
2494
|
+
function createDeployAdapter(platform) {
|
|
2495
|
+
switch (platform) {
|
|
2496
|
+
case "fly":
|
|
2497
|
+
return new FlyAdapter();
|
|
2498
|
+
case "railway":
|
|
2499
|
+
return new RailwayAdapter();
|
|
2500
|
+
case "aws-ecs":
|
|
2501
|
+
return new AwsEcsAdapter();
|
|
2502
|
+
case "aws-lightsail":
|
|
2503
|
+
return new AwsLightsailAdapter();
|
|
2504
|
+
case "render":
|
|
2505
|
+
case "docker":
|
|
2506
|
+
case "kora-cloud":
|
|
2507
|
+
return new StubDeployAdapter(platform);
|
|
2508
|
+
default: {
|
|
2509
|
+
const exhaustiveCheck = platform;
|
|
2510
|
+
return exhaustiveCheck;
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
// src/commands/deploy/artifacts/dockerfile-generator.ts
|
|
2516
|
+
var import_promises8 = require("fs/promises");
|
|
2517
|
+
var import_node_path11 = require("path");
|
|
2518
|
+
var GENERATED_HEADER3 = "# Generated by kora deploy \u2014 do not edit manually";
|
|
2519
|
+
function generateDockerfile(options = {}) {
|
|
2520
|
+
const nodeVersion = options.nodeVersion ?? "20-alpine";
|
|
2521
|
+
const port = options.port ?? 3e3;
|
|
2522
|
+
const clientDirectory = options.clientDirectory === void 0 ? "dist" : options.clientDirectory;
|
|
2523
|
+
const serverBundleFile = options.serverBundleFile ?? "server-bundled.js";
|
|
2524
|
+
const hasNativeDeps = options.nativeDependencies && Object.keys(options.nativeDependencies).length > 0;
|
|
2525
|
+
const lines = [
|
|
2526
|
+
GENERATED_HEADER3,
|
|
2527
|
+
"",
|
|
2528
|
+
`FROM node:${nodeVersion} AS runtime`,
|
|
2529
|
+
"WORKDIR /app",
|
|
2530
|
+
"ENV NODE_ENV=production",
|
|
2531
|
+
`ENV PORT=${String(port)}`,
|
|
2532
|
+
""
|
|
2533
|
+
];
|
|
2534
|
+
if (hasNativeDeps) {
|
|
2535
|
+
lines.push("# Install build tools for native modules");
|
|
2536
|
+
lines.push("RUN apk add --no-cache python3 make g++");
|
|
2537
|
+
lines.push("");
|
|
2538
|
+
lines.push("COPY package.json ./package.json");
|
|
2539
|
+
lines.push("RUN npm install --omit=dev");
|
|
2540
|
+
lines.push("");
|
|
2541
|
+
}
|
|
2542
|
+
if (clientDirectory !== null) {
|
|
2543
|
+
lines.push(`COPY ${clientDirectory} ./dist`);
|
|
2544
|
+
}
|
|
2545
|
+
lines.push(`COPY ${serverBundleFile} ./server-bundled.js`);
|
|
2546
|
+
lines.push("");
|
|
2547
|
+
lines.push(`EXPOSE ${String(port)}`);
|
|
2548
|
+
lines.push('CMD ["node", "./server-bundled.js"]');
|
|
2549
|
+
lines.push("");
|
|
2550
|
+
return lines.join("\n");
|
|
2551
|
+
}
|
|
2552
|
+
function generateDeployPackageJson(nativeDependencies) {
|
|
2553
|
+
const pkg = {
|
|
2554
|
+
name: "kora-deploy",
|
|
2555
|
+
version: "1.0.0",
|
|
2556
|
+
private: true,
|
|
2557
|
+
type: "module",
|
|
2558
|
+
dependencies: nativeDependencies
|
|
2559
|
+
};
|
|
2560
|
+
return `${JSON.stringify(pkg, null, 2)}
|
|
2561
|
+
`;
|
|
2562
|
+
}
|
|
2563
|
+
function generateDockerIgnore() {
|
|
2564
|
+
return [
|
|
2565
|
+
GENERATED_HEADER3,
|
|
2566
|
+
"",
|
|
2567
|
+
"node_modules",
|
|
2568
|
+
"npm-debug.log*",
|
|
2569
|
+
"pnpm-debug.log*",
|
|
2570
|
+
"yarn-error.log*",
|
|
2571
|
+
".env",
|
|
2572
|
+
".env.*",
|
|
2573
|
+
".git",
|
|
2574
|
+
".gitignore",
|
|
2575
|
+
"coverage",
|
|
2576
|
+
".turbo",
|
|
2577
|
+
"*.test.ts",
|
|
2578
|
+
"*.spec.ts",
|
|
2579
|
+
""
|
|
2580
|
+
].join("\n");
|
|
2581
|
+
}
|
|
2582
|
+
async function writeDockerfileArtifact(deployDirectory, options = {}) {
|
|
2583
|
+
await (0, import_promises8.mkdir)(deployDirectory, { recursive: true });
|
|
2584
|
+
const dockerfilePath = (0, import_node_path11.join)(deployDirectory, "Dockerfile");
|
|
2585
|
+
await (0, import_promises8.writeFile)(dockerfilePath, generateDockerfile(options), "utf-8");
|
|
2586
|
+
if (options.nativeDependencies && Object.keys(options.nativeDependencies).length > 0) {
|
|
2587
|
+
const pkgJsonPath = (0, import_node_path11.join)(deployDirectory, "package.json");
|
|
2588
|
+
await (0, import_promises8.writeFile)(pkgJsonPath, generateDeployPackageJson(options.nativeDependencies), "utf-8");
|
|
2589
|
+
}
|
|
2590
|
+
return dockerfilePath;
|
|
2591
|
+
}
|
|
2592
|
+
async function writeDockerIgnoreArtifact(deployDirectory) {
|
|
2593
|
+
await (0, import_promises8.mkdir)(deployDirectory, { recursive: true });
|
|
2594
|
+
const dockerIgnorePath = (0, import_node_path11.join)(deployDirectory, ".dockerignore");
|
|
2595
|
+
await (0, import_promises8.writeFile)(dockerIgnorePath, generateDockerIgnore(), "utf-8");
|
|
2596
|
+
return dockerIgnorePath;
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
// src/commands/deploy/state/deploy-state.ts
|
|
2600
|
+
var import_promises9 = require("fs/promises");
|
|
2601
|
+
var import_node_path12 = require("path");
|
|
2602
|
+
var KORA_DEPLOY_DIRECTORY = (0, import_node_path12.join)(".kora", "deploy");
|
|
2603
|
+
var DEPLOY_STATE_FILENAME = "deploy.json";
|
|
2604
|
+
function resolveDeployDirectory(projectRoot) {
|
|
2605
|
+
return (0, import_node_path12.join)(projectRoot, KORA_DEPLOY_DIRECTORY);
|
|
2606
|
+
}
|
|
2607
|
+
function resolveDeployStatePath(projectRoot) {
|
|
2608
|
+
return (0, import_node_path12.join)(resolveDeployDirectory(projectRoot), DEPLOY_STATE_FILENAME);
|
|
2609
|
+
}
|
|
2610
|
+
async function readDeployState(projectRoot) {
|
|
2611
|
+
const statePath = resolveDeployStatePath(projectRoot);
|
|
2612
|
+
try {
|
|
2613
|
+
const source = await (0, import_promises9.readFile)(statePath, "utf-8");
|
|
2614
|
+
const parsed = JSON.parse(source);
|
|
2615
|
+
return parseDeployState(parsed);
|
|
2616
|
+
} catch (error) {
|
|
2617
|
+
const code = error.code;
|
|
2618
|
+
if (code === "ENOENT") return null;
|
|
2619
|
+
throw error;
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
async function writeDeployState(projectRoot, input, now = /* @__PURE__ */ new Date()) {
|
|
2623
|
+
const timestamp = now.toISOString();
|
|
2624
|
+
const state = {
|
|
2625
|
+
platform: input.platform,
|
|
2626
|
+
appName: input.appName,
|
|
2627
|
+
region: input.region,
|
|
2628
|
+
projectRoot: input.projectRoot,
|
|
2629
|
+
liveUrl: input.liveUrl ?? null,
|
|
2630
|
+
syncUrl: input.syncUrl ?? null,
|
|
2631
|
+
databaseId: input.databaseId ?? null,
|
|
2632
|
+
lastDeploymentId: input.lastDeploymentId ?? null,
|
|
2633
|
+
createdAt: timestamp,
|
|
2634
|
+
updatedAt: timestamp
|
|
2635
|
+
};
|
|
2636
|
+
await persistDeployState(projectRoot, state);
|
|
2637
|
+
return state;
|
|
2638
|
+
}
|
|
2639
|
+
async function updateDeployState(projectRoot, patch, now = /* @__PURE__ */ new Date()) {
|
|
2640
|
+
const existing = await readDeployState(projectRoot);
|
|
2641
|
+
if (!existing) {
|
|
2642
|
+
throw new Error("Cannot update deploy state because deploy.json does not exist yet.");
|
|
2643
|
+
}
|
|
2644
|
+
const nextState = {
|
|
2645
|
+
platform: patch.platform ?? existing.platform,
|
|
2646
|
+
appName: patch.appName ?? existing.appName,
|
|
2647
|
+
region: patch.region === void 0 ? existing.region : patch.region,
|
|
2648
|
+
projectRoot: patch.projectRoot ?? existing.projectRoot,
|
|
2649
|
+
liveUrl: patch.liveUrl === void 0 ? existing.liveUrl : patch.liveUrl,
|
|
2650
|
+
syncUrl: patch.syncUrl === void 0 ? existing.syncUrl : patch.syncUrl,
|
|
2651
|
+
databaseId: patch.databaseId === void 0 ? existing.databaseId : patch.databaseId,
|
|
2652
|
+
lastDeploymentId: patch.lastDeploymentId === void 0 ? existing.lastDeploymentId : patch.lastDeploymentId,
|
|
2653
|
+
createdAt: existing.createdAt,
|
|
2654
|
+
updatedAt: now.toISOString()
|
|
2655
|
+
};
|
|
2656
|
+
await persistDeployState(projectRoot, nextState);
|
|
2657
|
+
return nextState;
|
|
2658
|
+
}
|
|
2659
|
+
async function resetDeployState(projectRoot) {
|
|
2660
|
+
await (0, import_promises9.rm)(resolveDeployDirectory(projectRoot), { recursive: true, force: true });
|
|
2661
|
+
}
|
|
2662
|
+
async function persistDeployState(projectRoot, state) {
|
|
2663
|
+
const deployDir = resolveDeployDirectory(projectRoot);
|
|
2664
|
+
const statePath = resolveDeployStatePath(projectRoot);
|
|
2665
|
+
await (0, import_promises9.mkdir)(deployDir, { recursive: true });
|
|
2666
|
+
await (0, import_promises9.writeFile)(statePath, `${JSON.stringify(state, null, 2)}
|
|
2667
|
+
`, "utf-8");
|
|
2668
|
+
}
|
|
2669
|
+
function parseDeployState(value) {
|
|
2670
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2671
|
+
throw new Error("Invalid deploy state: expected an object.");
|
|
2672
|
+
}
|
|
2673
|
+
const record = value;
|
|
2674
|
+
const platform = readPlatform(record.platform);
|
|
2675
|
+
const appName = readString(record.appName, "appName");
|
|
2676
|
+
const region = readOptionalString(record.region, "region");
|
|
2677
|
+
const projectRoot = readString(record.projectRoot, "projectRoot");
|
|
2678
|
+
const liveUrl = readOptionalString(record.liveUrl, "liveUrl");
|
|
2679
|
+
const syncUrl = readOptionalString(record.syncUrl, "syncUrl");
|
|
2680
|
+
const databaseId = readOptionalString(record.databaseId, "databaseId");
|
|
2681
|
+
const lastDeploymentId = readOptionalString(record.lastDeploymentId, "lastDeploymentId");
|
|
2682
|
+
const createdAt = readString(record.createdAt, "createdAt");
|
|
2683
|
+
const updatedAt = readString(record.updatedAt, "updatedAt");
|
|
2684
|
+
return {
|
|
2685
|
+
platform,
|
|
2686
|
+
appName,
|
|
2687
|
+
region,
|
|
2688
|
+
projectRoot,
|
|
2689
|
+
liveUrl,
|
|
2690
|
+
syncUrl,
|
|
2691
|
+
databaseId,
|
|
2692
|
+
lastDeploymentId,
|
|
2693
|
+
createdAt,
|
|
2694
|
+
updatedAt
|
|
2695
|
+
};
|
|
2696
|
+
}
|
|
2697
|
+
function readPlatform(value) {
|
|
2698
|
+
if (typeof value !== "string" || !isDeployPlatform(value)) {
|
|
2699
|
+
throw new Error('Invalid deploy state: "platform" must be a supported platform.');
|
|
2700
|
+
}
|
|
2701
|
+
return value;
|
|
2702
|
+
}
|
|
2703
|
+
function readString(value, field) {
|
|
2704
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
2705
|
+
throw new Error(`Invalid deploy state: "${field}" must be a non-empty string.`);
|
|
2706
|
+
}
|
|
2707
|
+
return value;
|
|
2708
|
+
}
|
|
2709
|
+
function readOptionalString(value, field) {
|
|
2710
|
+
if (value === null) return null;
|
|
2711
|
+
if (typeof value === "string") return value;
|
|
2712
|
+
throw new Error(`Invalid deploy state: "${field}" must be a string or null.`);
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
// src/commands/deploy/deploy-command.ts
|
|
2716
|
+
var deployCommand = (0, import_citty2.defineCommand)({
|
|
2717
|
+
meta: {
|
|
2718
|
+
name: "deploy",
|
|
2719
|
+
description: "Deploy a Kora project to your selected platform"
|
|
2720
|
+
},
|
|
2721
|
+
args: {
|
|
2722
|
+
platform: {
|
|
2723
|
+
type: "string",
|
|
2724
|
+
description: `Deployment platform (${DEPLOY_PLATFORMS.join(", ")})`
|
|
2725
|
+
},
|
|
2726
|
+
app: {
|
|
2727
|
+
type: "string",
|
|
2728
|
+
description: "Application name used by the target platform"
|
|
2729
|
+
},
|
|
2730
|
+
region: {
|
|
2731
|
+
type: "string",
|
|
2732
|
+
description: "Preferred deployment region (for example: iad, lhr, syd)"
|
|
2733
|
+
},
|
|
2734
|
+
prod: {
|
|
2735
|
+
type: "boolean",
|
|
2736
|
+
description: "Deploy to production environment",
|
|
2737
|
+
default: false
|
|
2738
|
+
},
|
|
2739
|
+
confirm: {
|
|
2740
|
+
type: "boolean",
|
|
2741
|
+
description: "Non-interactive mode (fail fast on missing required data)",
|
|
2742
|
+
default: false
|
|
2743
|
+
},
|
|
2744
|
+
reset: {
|
|
2745
|
+
type: "boolean",
|
|
2746
|
+
description: "Delete .kora/deploy state and generated artifacts",
|
|
2747
|
+
default: false
|
|
2748
|
+
}
|
|
2749
|
+
},
|
|
2750
|
+
subCommands: {
|
|
2751
|
+
status: (0, import_citty2.defineCommand)({
|
|
2752
|
+
meta: {
|
|
2753
|
+
name: "status",
|
|
2754
|
+
description: "Show current deployment status"
|
|
2755
|
+
},
|
|
2756
|
+
async run() {
|
|
2757
|
+
const logger = createLogger();
|
|
2758
|
+
const projectRoot = await requireProjectRoot();
|
|
2759
|
+
const state = await readDeployState(projectRoot);
|
|
2760
|
+
if (!state) {
|
|
2761
|
+
logger.warn("No deployment state found. Run `kora deploy` first.");
|
|
2762
|
+
return;
|
|
2763
|
+
}
|
|
2764
|
+
logger.banner();
|
|
2765
|
+
logger.info(`Platform: ${state.platform}`);
|
|
2766
|
+
logger.step(`App: ${state.appName}`);
|
|
2767
|
+
logger.step(`Region: ${state.region ?? "n/a"}`);
|
|
2768
|
+
logger.step(`Last deployment: ${state.lastDeploymentId ?? "n/a"}`);
|
|
2769
|
+
const adapter = createDeployAdapter(state.platform);
|
|
2770
|
+
configureAdapterContext(adapter, {
|
|
2771
|
+
projectRoot,
|
|
2772
|
+
appName: state.appName,
|
|
2773
|
+
region: state.region
|
|
2774
|
+
});
|
|
2775
|
+
const adapterStatus = await adapter.status();
|
|
2776
|
+
logger.step(`Status: ${adapterStatus.state}`);
|
|
2777
|
+
logger.step(`Message: ${adapterStatus.message}`);
|
|
2778
|
+
logger.step(`Live URL: ${adapterStatus.liveUrl ?? state.liveUrl ?? "n/a"}`);
|
|
2779
|
+
logger.step(`Sync URL: ${state.syncUrl ?? "n/a"}`);
|
|
2780
|
+
}
|
|
2781
|
+
}),
|
|
2782
|
+
rollback: (0, import_citty2.defineCommand)({
|
|
2783
|
+
meta: {
|
|
2784
|
+
name: "rollback",
|
|
2785
|
+
description: "Rollback the current deployment"
|
|
2786
|
+
},
|
|
2787
|
+
args: {
|
|
2788
|
+
id: {
|
|
2789
|
+
type: "positional",
|
|
2790
|
+
description: "Optional deployment identifier",
|
|
2791
|
+
required: false
|
|
2792
|
+
}
|
|
2793
|
+
},
|
|
2794
|
+
async run({ args }) {
|
|
2795
|
+
const logger = createLogger();
|
|
2796
|
+
const projectRoot = await requireProjectRoot();
|
|
2797
|
+
const state = await readDeployState(projectRoot);
|
|
2798
|
+
if (!state) {
|
|
2799
|
+
logger.warn("No deployment state found. Run `kora deploy` first.");
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
const adapter = createDeployAdapter(state.platform);
|
|
2803
|
+
configureAdapterContext(adapter, {
|
|
2804
|
+
projectRoot,
|
|
2805
|
+
appName: state.appName,
|
|
2806
|
+
region: state.region
|
|
2807
|
+
});
|
|
2808
|
+
const deploymentId = typeof args.id === "string" && args.id.length > 0 ? args.id : state.lastDeploymentId ?? "latest";
|
|
2809
|
+
await adapter.rollback(deploymentId);
|
|
2810
|
+
logger.success(`Rolled back ${state.platform} deployment to ${deploymentId}.`);
|
|
2811
|
+
}
|
|
2812
|
+
}),
|
|
2813
|
+
logs: (0, import_citty2.defineCommand)({
|
|
2814
|
+
meta: {
|
|
2815
|
+
name: "logs",
|
|
2816
|
+
description: "Read deployment logs"
|
|
2817
|
+
},
|
|
2818
|
+
async run() {
|
|
2819
|
+
const logger = createLogger();
|
|
2820
|
+
const projectRoot = await requireProjectRoot();
|
|
2821
|
+
const state = await readDeployState(projectRoot);
|
|
2822
|
+
if (!state) {
|
|
2823
|
+
logger.warn("No deployment state found. Run `kora deploy` first.");
|
|
2824
|
+
return;
|
|
2825
|
+
}
|
|
2826
|
+
const adapter = createDeployAdapter(state.platform);
|
|
2827
|
+
configureAdapterContext(adapter, {
|
|
2828
|
+
projectRoot,
|
|
2829
|
+
appName: state.appName,
|
|
2830
|
+
region: state.region
|
|
2831
|
+
});
|
|
2832
|
+
const logLines = adapter.logs({ tail: 200 });
|
|
2833
|
+
let hasLines = false;
|
|
2834
|
+
for await (const line of logLines) {
|
|
2835
|
+
hasLines = true;
|
|
2836
|
+
logger.step(`[${line.level}] ${line.message}`);
|
|
2837
|
+
}
|
|
2838
|
+
if (!hasLines) {
|
|
2839
|
+
logger.warn(`No logs returned from ${state.platform}.`);
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
})
|
|
2843
|
+
},
|
|
2844
|
+
async run({ args, rawArgs }) {
|
|
2845
|
+
const subCommandNames = ["status", "rollback", "logs"];
|
|
2846
|
+
if (rawArgs.some((arg) => subCommandNames.includes(arg))) {
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2849
|
+
const logger = createLogger();
|
|
2850
|
+
const prompts = createPromptClient();
|
|
2851
|
+
const projectRoot = await requireProjectRoot();
|
|
2852
|
+
if (args.reset === true) {
|
|
2853
|
+
await resetDeployState(projectRoot);
|
|
2854
|
+
logger.success("Reset .kora/deploy state.");
|
|
2855
|
+
return;
|
|
2856
|
+
}
|
|
2857
|
+
const existingState = await readDeployState(projectRoot);
|
|
2858
|
+
const confirmMode = args.confirm === true;
|
|
2859
|
+
const platform = await resolvePlatform({
|
|
2860
|
+
promptClient: prompts,
|
|
2861
|
+
platformArg: args.platform,
|
|
2862
|
+
storedPlatform: existingState?.platform,
|
|
2863
|
+
confirm: confirmMode
|
|
2864
|
+
});
|
|
2865
|
+
if (!isImplementedDeployPlatform(platform)) {
|
|
2866
|
+
throw new DeployPlatformUnavailableError(platform, IMPLEMENTED_DEPLOY_PLATFORMS);
|
|
2867
|
+
}
|
|
2868
|
+
const appName = resolveAppName(args.app, existingState?.appName, projectRoot, confirmMode);
|
|
2869
|
+
const region = resolveRegion(args.region, existingState?.region, confirmMode);
|
|
2870
|
+
const deployDirectory = resolveDeployDirectory(projectRoot);
|
|
2871
|
+
const environment = args.prod === true ? "production" : "preview";
|
|
2872
|
+
const deployTarget = detectDeployTarget(projectRoot);
|
|
2873
|
+
const config = {
|
|
2874
|
+
projectRoot,
|
|
2875
|
+
appName,
|
|
2876
|
+
region,
|
|
2877
|
+
environment,
|
|
2878
|
+
confirm: confirmMode,
|
|
2879
|
+
deployTarget
|
|
2880
|
+
};
|
|
2881
|
+
const adapter = createDeployAdapter(platform);
|
|
2882
|
+
configureAdapterContext(adapter, {
|
|
2883
|
+
projectRoot,
|
|
2884
|
+
appName,
|
|
2885
|
+
region
|
|
2886
|
+
});
|
|
2887
|
+
logger.banner();
|
|
2888
|
+
logger.info(
|
|
2889
|
+
`Deploying to ${platform} (${appName}${region ? `, ${region}` : ""}, ${environment})`
|
|
2890
|
+
);
|
|
2891
|
+
if (deployTarget === "sync-server") {
|
|
2892
|
+
logger.step("Detected Tauri project. Deploying the sync server only.");
|
|
2893
|
+
}
|
|
2894
|
+
if (confirmMode) {
|
|
2895
|
+
logger.step("Running in --confirm mode (non-interactive, fail-fast).");
|
|
2896
|
+
}
|
|
2897
|
+
await writeDockerfileArtifact(deployDirectory, {
|
|
2898
|
+
clientDirectory: deployTarget === "sync-server" ? null : void 0,
|
|
2899
|
+
nativeDependencies: {
|
|
2900
|
+
"better-sqlite3": "^11.0.0",
|
|
2901
|
+
"drizzle-orm": "^0.45.2",
|
|
2902
|
+
postgres: "^3.4.0"
|
|
2903
|
+
}
|
|
2904
|
+
});
|
|
2905
|
+
await writeDockerIgnoreArtifact(deployDirectory);
|
|
2906
|
+
const detected = await adapter.detect();
|
|
2907
|
+
if (!detected) {
|
|
2908
|
+
await adapter.install();
|
|
2909
|
+
}
|
|
2910
|
+
await adapter.authenticate();
|
|
2911
|
+
const provisionResult = await adapter.provision(config);
|
|
2912
|
+
const artifacts = await adapter.build(config);
|
|
2913
|
+
const deployResult = await adapter.deploy(artifacts);
|
|
2914
|
+
if (existingState) {
|
|
2915
|
+
await updateDeployState(projectRoot, {
|
|
2916
|
+
platform,
|
|
2917
|
+
appName,
|
|
2918
|
+
region,
|
|
2919
|
+
projectRoot,
|
|
2920
|
+
liveUrl: deployResult.liveUrl,
|
|
2921
|
+
syncUrl: deployResult.syncUrl,
|
|
2922
|
+
databaseId: provisionResult.databaseId,
|
|
2923
|
+
lastDeploymentId: deployResult.deploymentId
|
|
2924
|
+
});
|
|
2925
|
+
} else {
|
|
2926
|
+
await writeDeployState(projectRoot, {
|
|
2927
|
+
platform,
|
|
2928
|
+
appName,
|
|
2929
|
+
region,
|
|
2930
|
+
projectRoot,
|
|
2931
|
+
liveUrl: deployResult.liveUrl,
|
|
2932
|
+
syncUrl: deployResult.syncUrl,
|
|
2933
|
+
databaseId: provisionResult.databaseId,
|
|
2934
|
+
lastDeploymentId: deployResult.deploymentId
|
|
2935
|
+
});
|
|
2936
|
+
}
|
|
2937
|
+
logger.success(`Deployment completed: ${deployResult.liveUrl}`);
|
|
2938
|
+
if (deployResult.syncUrl) {
|
|
2939
|
+
logger.step(`Sync endpoint: ${deployResult.syncUrl}`);
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
});
|
|
2943
|
+
async function resolvePlatform(options) {
|
|
2944
|
+
if (typeof options.platformArg === "string") {
|
|
2945
|
+
if (!isDeployPlatform(options.platformArg)) {
|
|
2946
|
+
throw new Error(
|
|
2947
|
+
`Invalid --platform value "${options.platformArg}". Valid options: ${DEPLOY_PLATFORMS.join(", ")}`
|
|
2948
|
+
);
|
|
2949
|
+
}
|
|
2950
|
+
return options.platformArg;
|
|
2951
|
+
}
|
|
2952
|
+
if (options.storedPlatform) {
|
|
2953
|
+
return options.storedPlatform;
|
|
2954
|
+
}
|
|
2955
|
+
if (options.confirm || !isInteractiveTerminal()) {
|
|
2956
|
+
throw new Error(
|
|
2957
|
+
"Missing deploy platform in --confirm mode. Provide --platform or run an interactive deploy first."
|
|
2958
|
+
);
|
|
2959
|
+
}
|
|
2960
|
+
return await options.promptClient.select("Where do you want to deploy?", [
|
|
2961
|
+
{
|
|
2962
|
+
label: "Fly.io (recommended for sync apps)",
|
|
2963
|
+
value: "fly"
|
|
2964
|
+
},
|
|
2965
|
+
{
|
|
2966
|
+
label: "Railway",
|
|
2967
|
+
value: "railway"
|
|
2968
|
+
},
|
|
2969
|
+
{
|
|
2970
|
+
label: "AWS ECS Fargate (production multi-instance)",
|
|
2971
|
+
value: "aws-ecs"
|
|
2972
|
+
},
|
|
2973
|
+
{
|
|
2974
|
+
label: "AWS Lightsail Containers (simple, cost-effective)",
|
|
2975
|
+
value: "aws-lightsail"
|
|
2976
|
+
},
|
|
2977
|
+
{
|
|
2978
|
+
label: "Render (coming soon)",
|
|
2979
|
+
value: "render"
|
|
2980
|
+
},
|
|
2981
|
+
{
|
|
2982
|
+
label: "Docker (self-hosted, coming soon)",
|
|
2983
|
+
value: "docker"
|
|
2984
|
+
},
|
|
2985
|
+
{
|
|
2986
|
+
label: "Kora Cloud (coming soon)",
|
|
2987
|
+
value: "kora-cloud"
|
|
2988
|
+
}
|
|
2989
|
+
]);
|
|
2990
|
+
}
|
|
2991
|
+
function resolveAppName(argValue, storedValue, projectRoot, confirm) {
|
|
2992
|
+
if (typeof argValue === "string" && argValue.length > 0) {
|
|
2993
|
+
return sanitizeAppName(argValue);
|
|
2994
|
+
}
|
|
2995
|
+
if (storedValue && storedValue.length > 0) {
|
|
2996
|
+
return storedValue;
|
|
2997
|
+
}
|
|
2998
|
+
if (confirm) {
|
|
2999
|
+
throw new Error(
|
|
3000
|
+
"Missing app name in --confirm mode. Provide --app or run an interactive deploy first."
|
|
3001
|
+
);
|
|
3002
|
+
}
|
|
3003
|
+
return sanitizeAppName((0, import_node_path13.basename)(projectRoot));
|
|
3004
|
+
}
|
|
3005
|
+
function resolveRegion(argValue, storedValue, confirm) {
|
|
3006
|
+
if (typeof argValue === "string" && argValue.length > 0) return argValue;
|
|
3007
|
+
if (storedValue !== void 0) return storedValue;
|
|
3008
|
+
if (confirm) {
|
|
3009
|
+
throw new Error(
|
|
3010
|
+
"Missing region in --confirm mode. Provide --region or run an interactive deploy first."
|
|
3011
|
+
);
|
|
3012
|
+
}
|
|
3013
|
+
return "iad";
|
|
3014
|
+
}
|
|
3015
|
+
function sanitizeAppName(value) {
|
|
3016
|
+
const normalized = value.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
|
|
3017
|
+
if (normalized.length === 0) {
|
|
3018
|
+
return "kora-app";
|
|
3019
|
+
}
|
|
3020
|
+
return normalized;
|
|
3021
|
+
}
|
|
3022
|
+
async function requireProjectRoot() {
|
|
3023
|
+
const projectRoot = await findProjectRoot();
|
|
3024
|
+
if (!projectRoot) {
|
|
3025
|
+
throw new InvalidProjectError(process.cwd());
|
|
3026
|
+
}
|
|
3027
|
+
return projectRoot;
|
|
3028
|
+
}
|
|
3029
|
+
function isInteractiveTerminal() {
|
|
3030
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
3031
|
+
}
|
|
3032
|
+
function configureAdapterContext(adapter, context) {
|
|
3033
|
+
if (hasContextSetter(adapter)) {
|
|
3034
|
+
adapter.setContext(context);
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
function detectDeployTarget(projectRoot) {
|
|
3038
|
+
return (0, import_node_fs2.existsSync)((0, import_node_path13.join)(projectRoot, "src-tauri")) ? "sync-server" : "full-stack";
|
|
3039
|
+
}
|
|
3040
|
+
function hasContextSetter(adapter) {
|
|
3041
|
+
return typeof adapter.setContext === "function";
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
// src/commands/create/project-name.ts
|
|
3045
|
+
var import_validate_npm_package_name = __toESM(require("validate-npm-package-name"), 1);
|
|
3046
|
+
function validateProjectName(name) {
|
|
3047
|
+
const trimmedName = name.trim();
|
|
3048
|
+
if (trimmedName.length === 0) {
|
|
3049
|
+
return {
|
|
3050
|
+
valid: false,
|
|
3051
|
+
issues: ["Project name cannot be empty."]
|
|
3052
|
+
};
|
|
3053
|
+
}
|
|
3054
|
+
const validation = (0, import_validate_npm_package_name.default)(trimmedName);
|
|
3055
|
+
const issues = [...validation.errors ?? [], ...validation.warnings ?? []];
|
|
3056
|
+
if (!validation.validForNewPackages && issues.length === 0) {
|
|
3057
|
+
return {
|
|
3058
|
+
valid: false,
|
|
3059
|
+
issues: ["Project name is not a valid npm package name."]
|
|
3060
|
+
};
|
|
3061
|
+
}
|
|
3062
|
+
return {
|
|
3063
|
+
valid: validation.validForNewPackages,
|
|
3064
|
+
issues
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
// src/commands/create/options.ts
|
|
3069
|
+
function determineTemplateFromSelections(input) {
|
|
3070
|
+
if (input.platform === "desktop-tauri") return "tauri-react";
|
|
3071
|
+
const shouldSync = input.sync && input.db !== "none";
|
|
3072
|
+
if (input.framework === "vue") {
|
|
3073
|
+
if (input.tailwind && shouldSync) return "vue-tailwind-sync";
|
|
3074
|
+
if (input.tailwind && !shouldSync) return "vue-tailwind";
|
|
3075
|
+
return shouldSync ? "vue-sync" : "vue-basic";
|
|
3076
|
+
}
|
|
3077
|
+
if (input.framework === "svelte") {
|
|
3078
|
+
if (input.tailwind && shouldSync) return "svelte-tailwind-sync";
|
|
3079
|
+
if (input.tailwind && !shouldSync) return "svelte-tailwind";
|
|
3080
|
+
return shouldSync ? "svelte-sync" : "svelte-basic";
|
|
3081
|
+
}
|
|
3082
|
+
if (input.framework === "solid") {
|
|
3083
|
+
throw new Error("Solid templates are not available yet. Use react, vue, or svelte.");
|
|
3084
|
+
}
|
|
3085
|
+
if (input.tailwind && shouldSync) return "react-tailwind-sync";
|
|
3086
|
+
if (input.tailwind && !shouldSync) return "react-tailwind";
|
|
3087
|
+
if (!input.tailwind && shouldSync) return "react-sync";
|
|
3088
|
+
return "react-basic";
|
|
3089
|
+
}
|
|
3090
|
+
function isPlatformValue(value) {
|
|
3091
|
+
return value === "web" || value === "desktop-tauri";
|
|
3092
|
+
}
|
|
3093
|
+
function isFrameworkValue(value) {
|
|
3094
|
+
return value === "react" || value === "vue" || value === "svelte" || value === "solid";
|
|
3095
|
+
}
|
|
3096
|
+
function isAuthValue(value) {
|
|
3097
|
+
return value === "none" || value === "email-password" || value === "oauth";
|
|
3098
|
+
}
|
|
3099
|
+
function isDatabaseValue(value) {
|
|
3100
|
+
return value === "none" || value === "sqlite" || value === "postgres";
|
|
3101
|
+
}
|
|
3102
|
+
function isDatabaseProviderValue(value) {
|
|
3103
|
+
return value === "none" || value === "local" || value === "supabase" || value === "neon" || value === "railway" || value === "vercel-postgres" || value === "custom";
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
// src/prompts/preferences.ts
|
|
3107
|
+
var import_conf = __toESM(require("conf"), 1);
|
|
3108
|
+
var DEFAULT_PREFERENCES = {
|
|
3109
|
+
platform: "web",
|
|
3110
|
+
framework: "react",
|
|
3111
|
+
tailwind: true,
|
|
3112
|
+
sync: true,
|
|
3113
|
+
db: "sqlite",
|
|
3114
|
+
dbProvider: "none",
|
|
3115
|
+
auth: "none",
|
|
3116
|
+
packageManager: "pnpm"
|
|
3117
|
+
};
|
|
3118
|
+
var PREFERENCES_KEY = "create.defaults";
|
|
3119
|
+
var PreferenceStore = class {
|
|
3120
|
+
store;
|
|
3121
|
+
constructor() {
|
|
3122
|
+
this.store = new import_conf.default({
|
|
3123
|
+
projectName: "korajs-cli"
|
|
3124
|
+
});
|
|
3125
|
+
}
|
|
3126
|
+
getCreatePreferences() {
|
|
3127
|
+
return this.store.get(PREFERENCES_KEY) ?? null;
|
|
3128
|
+
}
|
|
3129
|
+
saveCreatePreferences(preferences) {
|
|
3130
|
+
this.store.set(PREFERENCES_KEY, preferences);
|
|
3131
|
+
}
|
|
3132
|
+
clearCreatePreferences() {
|
|
3133
|
+
this.store.delete(PREFERENCES_KEY);
|
|
3134
|
+
}
|
|
3135
|
+
};
|
|
3136
|
+
function getCreatePreferencesOrDefault(store) {
|
|
3137
|
+
return store.getCreatePreferences() ?? DEFAULT_PREFERENCES;
|
|
3138
|
+
}
|
|
3139
|
+
function getDefaultCreatePreferences() {
|
|
3140
|
+
return { ...DEFAULT_PREFERENCES };
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
// src/commands/create/preferences-flow.ts
|
|
3144
|
+
async function resolveCreatePreferencesFlow(params) {
|
|
3145
|
+
const { flags, prompts, store } = params;
|
|
3146
|
+
const stored = store.getCreatePreferences();
|
|
3147
|
+
const base = getCreatePreferencesOrDefault(store);
|
|
3148
|
+
const hasExplicitFlags = hasExplicitPreferenceFlags(flags);
|
|
3149
|
+
const canOfferStored = !flags.useDefaults && !hasExplicitFlags && stored !== null && promptSupportsRichOptions();
|
|
3150
|
+
let effective = { ...base };
|
|
3151
|
+
let usedStoredPreferences = false;
|
|
3152
|
+
if (flags.useDefaults) {
|
|
3153
|
+
effective = getDefaultCreatePreferences();
|
|
3154
|
+
} else if (canOfferStored && stored !== null) {
|
|
3155
|
+
const reuseStored = await prompts.select("Welcome back! Choose setup mode:", [
|
|
3156
|
+
{ label: formatStoredPreferenceLabel(stored), value: "reuse" },
|
|
3157
|
+
{ label: "Customize", value: "customize" }
|
|
3158
|
+
]);
|
|
3159
|
+
if (reuseStored === "reuse") {
|
|
3160
|
+
effective = { ...stored };
|
|
3161
|
+
usedStoredPreferences = true;
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
if (flags.platform !== void 0) {
|
|
3165
|
+
if (!isPlatformValue(flags.platform)) {
|
|
3166
|
+
throw new Error(
|
|
3167
|
+
`Invalid --platform value "${flags.platform}". Expected one of: web, desktop-tauri.`
|
|
3168
|
+
);
|
|
3169
|
+
}
|
|
3170
|
+
effective.platform = flags.platform;
|
|
3171
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
3172
|
+
effective.platform = await prompts.select("Platform:", [
|
|
3173
|
+
{ label: "Web (browser)", value: "web" },
|
|
3174
|
+
{ label: "Desktop (Tauri \u2014 native SQLite)", value: "desktop-tauri" }
|
|
3175
|
+
]);
|
|
3176
|
+
}
|
|
3177
|
+
const isTauri = effective.platform === "desktop-tauri";
|
|
3178
|
+
if (isTauri) {
|
|
3179
|
+
effective.framework = "react";
|
|
3180
|
+
effective.tailwind = false;
|
|
3181
|
+
effective.auth = "none";
|
|
3182
|
+
if (flags.sync !== void 0) {
|
|
3183
|
+
effective.sync = flags.sync;
|
|
3184
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
3185
|
+
effective.sync = await prompts.confirm("Enable multi-device sync?", true);
|
|
3186
|
+
}
|
|
3187
|
+
if (flags.db !== void 0) {
|
|
3188
|
+
if (!isDatabaseValue(flags.db)) {
|
|
3189
|
+
throw new Error(
|
|
3190
|
+
`Invalid --db value "${flags.db}". Expected one of: none, sqlite, postgres.`
|
|
3191
|
+
);
|
|
3192
|
+
}
|
|
3193
|
+
effective.db = flags.db;
|
|
3194
|
+
} else if (!effective.sync) {
|
|
3195
|
+
effective.db = "none";
|
|
3196
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
3197
|
+
effective.db = await prompts.select("Sync server database:", [
|
|
3198
|
+
{ label: "SQLite (zero-config; local, LAN, small teams)", value: "sqlite" },
|
|
3199
|
+
{ label: "PostgreSQL (production-scale)", value: "postgres" }
|
|
3200
|
+
]);
|
|
3201
|
+
}
|
|
3202
|
+
if (effective.db === "none") {
|
|
3203
|
+
effective.sync = false;
|
|
3204
|
+
}
|
|
3205
|
+
if (effective.db !== "postgres") {
|
|
3206
|
+
effective.dbProvider = "none";
|
|
3207
|
+
} else if (flags.dbProvider !== void 0) {
|
|
3208
|
+
if (!isDatabaseProviderValue(flags.dbProvider)) {
|
|
3209
|
+
throw new Error(
|
|
3210
|
+
`Invalid --db-provider value "${flags.dbProvider}". Expected one of: none, local, supabase, neon, railway, vercel-postgres, custom.`
|
|
3211
|
+
);
|
|
3212
|
+
}
|
|
3213
|
+
effective.dbProvider = flags.dbProvider;
|
|
3214
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
3215
|
+
effective.dbProvider = await prompts.select("Database provider:", [
|
|
3216
|
+
{ label: "Local Postgres", value: "local" },
|
|
3217
|
+
{ label: "Supabase", value: "supabase" },
|
|
3218
|
+
{ label: "Neon", value: "neon" },
|
|
3219
|
+
{ label: "Railway", value: "railway" },
|
|
3220
|
+
{ label: "Vercel Postgres", value: "vercel-postgres" },
|
|
3221
|
+
{ label: "Custom connection string", value: "custom" }
|
|
3222
|
+
]);
|
|
3223
|
+
}
|
|
3224
|
+
} else {
|
|
3225
|
+
if (flags.framework !== void 0) {
|
|
3226
|
+
if (!isFrameworkValue(flags.framework)) {
|
|
3227
|
+
throw new Error(
|
|
3228
|
+
`Invalid --framework value "${flags.framework}". Expected one of: react, vue, svelte, solid.`
|
|
3229
|
+
);
|
|
3230
|
+
}
|
|
3231
|
+
effective.framework = flags.framework;
|
|
3232
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
3233
|
+
effective.framework = await prompts.select("UI framework:", [
|
|
3234
|
+
{ label: "React", value: "react" },
|
|
3235
|
+
{ label: "Vue 3", value: "vue" },
|
|
3236
|
+
{ label: "Svelte 5", value: "svelte" },
|
|
3237
|
+
{ label: "Solid (coming soon)", value: "solid", disabled: true }
|
|
3238
|
+
]);
|
|
3239
|
+
}
|
|
3240
|
+
if (flags.auth !== void 0) {
|
|
3241
|
+
if (!isAuthValue(flags.auth)) {
|
|
3242
|
+
throw new Error(
|
|
3243
|
+
`Invalid --auth value "${flags.auth}". Expected one of: none, email-password, oauth.`
|
|
3244
|
+
);
|
|
3245
|
+
}
|
|
3246
|
+
effective.auth = flags.auth;
|
|
3247
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
3248
|
+
effective.auth = await prompts.select("Authentication:", [
|
|
3249
|
+
{ label: "None", value: "none" },
|
|
3250
|
+
{ label: "Email + Password (coming soon)", value: "email-password", disabled: true },
|
|
3251
|
+
{ label: "OAuth (coming soon)", value: "oauth", disabled: true }
|
|
3252
|
+
]);
|
|
3253
|
+
}
|
|
3254
|
+
if (flags.tailwind !== void 0) {
|
|
3255
|
+
effective.tailwind = flags.tailwind;
|
|
3256
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
3257
|
+
effective.tailwind = await prompts.confirm("Use Tailwind CSS?", true);
|
|
3258
|
+
}
|
|
3259
|
+
if (flags.sync !== void 0) {
|
|
3260
|
+
effective.sync = flags.sync;
|
|
3261
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
3262
|
+
effective.sync = await prompts.confirm("Enable multi-device sync?", true);
|
|
3263
|
+
}
|
|
3264
|
+
if (flags.db !== void 0) {
|
|
3265
|
+
if (!isDatabaseValue(flags.db)) {
|
|
3266
|
+
throw new Error(
|
|
3267
|
+
`Invalid --db value "${flags.db}". Expected one of: none, sqlite, postgres.`
|
|
3268
|
+
);
|
|
3269
|
+
}
|
|
3270
|
+
effective.db = flags.db;
|
|
3271
|
+
} else if (!effective.sync) {
|
|
3272
|
+
effective.db = "none";
|
|
3273
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
3274
|
+
effective.db = await prompts.select("Server-side database:", [
|
|
3275
|
+
{ label: "SQLite (zero-config)", value: "sqlite" },
|
|
3276
|
+
{ label: "PostgreSQL (production-scale)", value: "postgres" }
|
|
3277
|
+
]);
|
|
3278
|
+
}
|
|
3279
|
+
if (effective.db !== "postgres") {
|
|
3280
|
+
effective.dbProvider = "none";
|
|
3281
|
+
} else if (flags.dbProvider !== void 0) {
|
|
3282
|
+
if (!isDatabaseProviderValue(flags.dbProvider)) {
|
|
3283
|
+
throw new Error(
|
|
3284
|
+
`Invalid --db-provider value "${flags.dbProvider}". Expected one of: none, local, supabase, neon, railway, vercel-postgres, custom.`
|
|
3285
|
+
);
|
|
3286
|
+
}
|
|
3287
|
+
effective.dbProvider = flags.dbProvider;
|
|
3288
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
3289
|
+
effective.dbProvider = await prompts.select("Database provider:", [
|
|
3290
|
+
{ label: "Local Postgres", value: "local" },
|
|
3291
|
+
{ label: "Supabase", value: "supabase" },
|
|
3292
|
+
{ label: "Neon", value: "neon" },
|
|
3293
|
+
{ label: "Railway", value: "railway" },
|
|
3294
|
+
{ label: "Vercel Postgres", value: "vercel-postgres" },
|
|
3295
|
+
{ label: "Custom connection string", value: "custom" }
|
|
3296
|
+
]);
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
const template = determineTemplateFromSelections({
|
|
3300
|
+
platform: effective.platform,
|
|
3301
|
+
framework: effective.framework,
|
|
3302
|
+
tailwind: effective.tailwind,
|
|
3303
|
+
sync: effective.sync,
|
|
3304
|
+
db: effective.db
|
|
3305
|
+
});
|
|
3306
|
+
return {
|
|
3307
|
+
platform: effective.platform,
|
|
3308
|
+
framework: effective.framework,
|
|
3309
|
+
auth: effective.auth,
|
|
3310
|
+
db: effective.db,
|
|
3311
|
+
dbProvider: effective.dbProvider,
|
|
3312
|
+
tailwind: effective.tailwind,
|
|
3313
|
+
sync: effective.sync,
|
|
3314
|
+
template,
|
|
3315
|
+
usedStoredPreferences
|
|
3316
|
+
};
|
|
3317
|
+
}
|
|
3318
|
+
function shouldSavePreferences(flags) {
|
|
3319
|
+
return !flags.useDefaults;
|
|
3320
|
+
}
|
|
3321
|
+
function saveResolvedPreferences(store, resolution) {
|
|
3322
|
+
store.saveCreatePreferences({
|
|
3323
|
+
platform: resolution.platform,
|
|
3324
|
+
framework: resolution.framework,
|
|
3325
|
+
tailwind: resolution.tailwind,
|
|
3326
|
+
sync: resolution.sync,
|
|
3327
|
+
db: resolution.db,
|
|
3328
|
+
dbProvider: resolution.dbProvider,
|
|
3329
|
+
auth: resolution.auth,
|
|
3330
|
+
packageManager: resolution.packageManager
|
|
3331
|
+
});
|
|
3332
|
+
}
|
|
3333
|
+
function hasExplicitPreferenceFlags(flags) {
|
|
3334
|
+
return flags.platform !== void 0 || flags.framework !== void 0 || flags.auth !== void 0 || flags.db !== void 0 || flags.dbProvider !== void 0 || flags.tailwind !== void 0 || flags.sync !== void 0;
|
|
3335
|
+
}
|
|
3336
|
+
function promptSupportsRichOptions() {
|
|
3337
|
+
return typeof process !== "undefined" && process.stdin.isTTY && process.stdout.isTTY;
|
|
3338
|
+
}
|
|
3339
|
+
function formatStoredPreferenceLabel(preferences) {
|
|
3340
|
+
if (preferences.platform === "desktop-tauri") {
|
|
3341
|
+
return `Use previous settings (tauri-desktop + ${preferences.packageManager})`;
|
|
3342
|
+
}
|
|
3343
|
+
const syncLabel = preferences.sync ? `sync/${preferences.db}` : "local-only";
|
|
3344
|
+
const styleLabel = preferences.tailwind ? "tailwind" : "css";
|
|
3345
|
+
return `Use previous settings (${preferences.framework} + ${styleLabel} + ${syncLabel} + ${preferences.packageManager})`;
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
// src/commands/create/sync-provider-preset.ts
|
|
3349
|
+
var import_promises10 = require("fs/promises");
|
|
3350
|
+
var import_node_path14 = require("path");
|
|
3351
|
+
async function applySyncProviderPreset(options) {
|
|
3352
|
+
if (!isSyncTemplate(options.template)) {
|
|
3353
|
+
return;
|
|
3354
|
+
}
|
|
3355
|
+
if (options.db !== "postgres") {
|
|
3356
|
+
return;
|
|
3357
|
+
}
|
|
3358
|
+
const providerName = getProviderDisplayName(options.dbProvider);
|
|
3359
|
+
const providerConnectionString = getProviderConnectionStringExample(options.dbProvider);
|
|
3360
|
+
const envPath = (0, import_node_path14.join)(options.targetDir, ".env.example");
|
|
3361
|
+
const readmePath = (0, import_node_path14.join)(options.targetDir, "README.md");
|
|
3362
|
+
const existingReadme = await (0, import_promises10.readFile)(readmePath, "utf-8");
|
|
3363
|
+
const existingEnv = await (0, import_promises10.readFile)(envPath, "utf-8");
|
|
3364
|
+
const trimmedReadme = existingReadme.trimEnd();
|
|
3365
|
+
const trimmedEnv = existingEnv.trimEnd();
|
|
3366
|
+
const envSuffix = [
|
|
3367
|
+
"",
|
|
3368
|
+
`# PostgreSQL provider preset: ${providerName}`,
|
|
3369
|
+
`# Example: ${providerConnectionString}`
|
|
3370
|
+
].join("\n");
|
|
3371
|
+
const readmeSuffix = [
|
|
3372
|
+
"",
|
|
3373
|
+
"## PostgreSQL Provider Preset",
|
|
3374
|
+
"",
|
|
3375
|
+
`Selected DB provider: ${options.dbProvider}`,
|
|
3376
|
+
"",
|
|
3377
|
+
"This scaffold keeps one sync server entrypoint. When `DATABASE_URL` is set, `server.ts` uses PostgreSQL. When it is empty, the same server uses SQLite at `KORA_SERVER_DB`.",
|
|
3378
|
+
"",
|
|
3379
|
+
"The generated server uses `createPostgresServerStore` automatically when `DATABASE_URL` is present.",
|
|
3380
|
+
""
|
|
3381
|
+
].join("\n");
|
|
3382
|
+
const readmeTemplate = `${trimmedReadme}${readmeSuffix}`;
|
|
3383
|
+
await (0, import_promises10.writeFile)(envPath, `${trimmedEnv}${envSuffix}
|
|
3384
|
+
`, "utf-8");
|
|
3385
|
+
await (0, import_promises10.writeFile)(readmePath, readmeTemplate, "utf-8");
|
|
3386
|
+
}
|
|
3387
|
+
function isSyncTemplate(template) {
|
|
3388
|
+
return template === "react-sync" || template === "react-tailwind-sync" || template === "vue-sync" || template === "vue-tailwind-sync" || template === "svelte-sync" || template === "svelte-tailwind-sync" || template === "tauri-react";
|
|
3389
|
+
}
|
|
3390
|
+
function getProviderDisplayName(provider) {
|
|
3391
|
+
switch (provider) {
|
|
3392
|
+
case "supabase":
|
|
3393
|
+
return "Supabase";
|
|
3394
|
+
case "neon":
|
|
3395
|
+
return "Neon";
|
|
3396
|
+
case "railway":
|
|
3397
|
+
return "Railway";
|
|
3398
|
+
case "vercel-postgres":
|
|
3399
|
+
return "Vercel Postgres";
|
|
3400
|
+
case "custom":
|
|
3401
|
+
return "Custom";
|
|
3402
|
+
case "local":
|
|
3403
|
+
return "Local Postgres";
|
|
3404
|
+
case "none":
|
|
3405
|
+
return "PostgreSQL";
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
function getProviderConnectionStringExample(provider) {
|
|
3409
|
+
switch (provider) {
|
|
3410
|
+
case "supabase":
|
|
3411
|
+
return "postgresql://postgres:<password>@db.<project-ref>.supabase.co:5432/postgres?sslmode=require";
|
|
3412
|
+
case "neon":
|
|
3413
|
+
return "postgresql://<user>:<password>@<branch>.<project>.neon.tech/neondb?sslmode=require";
|
|
3414
|
+
case "railway":
|
|
3415
|
+
return "postgresql://postgres:<password>@<host>.railway.app:<port>/railway?sslmode=require";
|
|
3416
|
+
case "vercel-postgres":
|
|
3417
|
+
return "postgresql://<user>:<password>@<host>.pooler.vercel-storage.com:5432/verceldb?sslmode=require";
|
|
3418
|
+
case "custom":
|
|
3419
|
+
return "postgresql://<user>:<password>@<host>:5432/<database>";
|
|
3420
|
+
case "local":
|
|
3421
|
+
return "postgresql://postgres:postgres@localhost:5432/kora";
|
|
3422
|
+
case "none":
|
|
3423
|
+
return "postgresql://postgres:postgres@localhost:5432/kora";
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3427
|
+
0 && (module.exports = {
|
|
3428
|
+
AGENTS_FRAMEWORKS,
|
|
3429
|
+
ClackPromptClient,
|
|
3430
|
+
CliError,
|
|
3431
|
+
DEPLOY_PLATFORMS,
|
|
3432
|
+
DeployPlatformUnavailableError,
|
|
3433
|
+
DevServerError,
|
|
3434
|
+
FlyAdapter,
|
|
3435
|
+
IMPLEMENTED_DEPLOY_PLATFORMS,
|
|
3436
|
+
InvalidProjectError,
|
|
3437
|
+
NodeFlyCommandRunner,
|
|
3438
|
+
NodeRailwayCommandRunner,
|
|
3439
|
+
PACKAGE_MANAGERS,
|
|
3440
|
+
PreferenceStore,
|
|
3441
|
+
ProjectExistsError,
|
|
3442
|
+
PromptCancelledError,
|
|
3443
|
+
RailwayAdapter,
|
|
3444
|
+
ReadlinePromptClient,
|
|
3445
|
+
SchemaNotFoundError,
|
|
3446
|
+
StubDeployAdapter,
|
|
3447
|
+
TEMPLATES,
|
|
3448
|
+
TEMPLATE_INFO,
|
|
3449
|
+
agentsCommand,
|
|
3450
|
+
applySyncProviderPreset,
|
|
3451
|
+
buildClient,
|
|
3452
|
+
bundleServer,
|
|
3453
|
+
createDeployAdapter,
|
|
3454
|
+
createPromptClient,
|
|
3455
|
+
deployCommand,
|
|
3456
|
+
detectFrameworkFromManifest,
|
|
3457
|
+
determineTemplateFromSelections,
|
|
3458
|
+
generateAgentsMd,
|
|
3459
|
+
generateDeployPackageJson,
|
|
3460
|
+
generateDockerIgnore,
|
|
3461
|
+
generateDockerfile,
|
|
3462
|
+
generateFlyToml,
|
|
3463
|
+
generateRailwayJson,
|
|
3464
|
+
generateTypes,
|
|
3465
|
+
getCreatePreferencesOrDefault,
|
|
3466
|
+
getDefaultCreatePreferences,
|
|
3467
|
+
isAgentsFramework,
|
|
3468
|
+
isAuthValue,
|
|
3469
|
+
isDatabaseProviderValue,
|
|
3470
|
+
isDatabaseValue,
|
|
3471
|
+
isDeployPlatform,
|
|
3472
|
+
isFrameworkValue,
|
|
3473
|
+
isImplementedDeployPlatform,
|
|
3474
|
+
readDeployState,
|
|
3475
|
+
resetDeployState,
|
|
3476
|
+
resolveCreatePreferencesFlow,
|
|
3477
|
+
resolveDeployDirectory,
|
|
3478
|
+
resolveDeployStatePath,
|
|
3479
|
+
saveResolvedPreferences,
|
|
3480
|
+
shouldSavePreferences,
|
|
3481
|
+
updateDeployState,
|
|
3482
|
+
validateProjectName,
|
|
3483
|
+
writeDeployState,
|
|
3484
|
+
writeDockerIgnoreArtifact,
|
|
3485
|
+
writeDockerfileArtifact,
|
|
3486
|
+
writeFlyTomlArtifact,
|
|
3487
|
+
writeRailwayJsonArtifact
|
|
3488
|
+
});
|
|
3489
|
+
//# sourceMappingURL=index.cjs.map
|