@korajs/cli 0.1.14 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/dist/bin.cjs +2517 -332
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +12 -10
- package/dist/bin.js.map +1 -1
- package/dist/chunk-E4JG7THU.js +1550 -0
- package/dist/chunk-E4JG7THU.js.map +1 -0
- package/dist/chunk-KTSRAPSE.js +752 -0
- package/dist/chunk-KTSRAPSE.js.map +1 -0
- package/dist/chunk-ZGYRDYXS.js +609 -0
- package/dist/chunk-ZGYRDYXS.js.map +1 -0
- package/dist/create.cjs +974 -159
- package/dist/create.cjs.map +1 -1
- package/dist/create.js +2 -2
- package/dist/index.cjs +2103 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +617 -1
- package/dist/index.d.ts +617 -1
- package/dist/index.js +91 -5
- package/package.json +19 -13
- package/dist/chunk-N36PFOSA.js +0 -103
- package/dist/chunk-N36PFOSA.js.map +0 -1
- package/dist/chunk-SYOFLJLB.js +0 -437
- package/dist/chunk-SYOFLJLB.js.map +0 -1
- package/dist/chunk-VGOOQ56H.js +0 -103
- package/dist/chunk-VGOOQ56H.js.map +0 -1
package/dist/bin.cjs
CHANGED
|
@@ -23,12 +23,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
));
|
|
24
24
|
|
|
25
25
|
// src/bin.ts
|
|
26
|
-
var
|
|
26
|
+
var import_citty6 = require("citty");
|
|
27
27
|
|
|
28
28
|
// src/commands/create/create-command.ts
|
|
29
29
|
var import_node_child_process2 = require("child_process");
|
|
30
30
|
var import_node_fs2 = require("fs");
|
|
31
|
-
var
|
|
31
|
+
var import_node_path5 = require("path");
|
|
32
32
|
var import_node_url2 = require("url");
|
|
33
33
|
var import_citty = require("citty");
|
|
34
34
|
|
|
@@ -85,6 +85,209 @@ var DevServerError = class extends import_core.KoraError {
|
|
|
85
85
|
searchPath;
|
|
86
86
|
};
|
|
87
87
|
|
|
88
|
+
// src/prompts/preferences.ts
|
|
89
|
+
var import_conf = __toESM(require("conf"), 1);
|
|
90
|
+
var DEFAULT_PREFERENCES = {
|
|
91
|
+
framework: "react",
|
|
92
|
+
tailwind: true,
|
|
93
|
+
sync: true,
|
|
94
|
+
db: "sqlite",
|
|
95
|
+
dbProvider: "none",
|
|
96
|
+
auth: "none",
|
|
97
|
+
packageManager: "pnpm"
|
|
98
|
+
};
|
|
99
|
+
var PREFERENCES_KEY = "create.defaults";
|
|
100
|
+
var PreferenceStore = class {
|
|
101
|
+
store;
|
|
102
|
+
constructor() {
|
|
103
|
+
this.store = new import_conf.default({
|
|
104
|
+
projectName: "korajs-cli"
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
getCreatePreferences() {
|
|
108
|
+
return this.store.get(PREFERENCES_KEY) ?? null;
|
|
109
|
+
}
|
|
110
|
+
saveCreatePreferences(preferences) {
|
|
111
|
+
this.store.set(PREFERENCES_KEY, preferences);
|
|
112
|
+
}
|
|
113
|
+
clearCreatePreferences() {
|
|
114
|
+
this.store.delete(PREFERENCES_KEY);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
function getCreatePreferencesOrDefault(store) {
|
|
118
|
+
return store.getCreatePreferences() ?? DEFAULT_PREFERENCES;
|
|
119
|
+
}
|
|
120
|
+
function getDefaultCreatePreferences() {
|
|
121
|
+
return { ...DEFAULT_PREFERENCES };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/prompts/prompt-client.ts
|
|
125
|
+
var import_prompts = require("@clack/prompts");
|
|
126
|
+
|
|
127
|
+
// src/utils/prompt.ts
|
|
128
|
+
var import_node_readline = require("readline");
|
|
129
|
+
function promptText(message, defaultValue, options) {
|
|
130
|
+
return new Promise((resolve9) => {
|
|
131
|
+
const rl = createReadline(options);
|
|
132
|
+
const suffix = defaultValue !== void 0 ? ` (${defaultValue})` : "";
|
|
133
|
+
rl.question(` ? ${message}${suffix}: `, (answer) => {
|
|
134
|
+
rl.close();
|
|
135
|
+
const trimmed = answer.trim();
|
|
136
|
+
resolve9(trimmed || defaultValue || "");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function promptSelect(message, choices, options) {
|
|
141
|
+
return new Promise((resolve9) => {
|
|
142
|
+
const rl = createReadline(options);
|
|
143
|
+
const out = options?.output ?? process.stdout;
|
|
144
|
+
out.write(` ? ${message}
|
|
145
|
+
`);
|
|
146
|
+
for (let i = 0; i < choices.length; i++) {
|
|
147
|
+
const choice = choices[i];
|
|
148
|
+
if (choice) {
|
|
149
|
+
out.write(` ${i + 1}) ${choice.label}
|
|
150
|
+
`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const ask = () => {
|
|
154
|
+
rl.question(" > ", (answer) => {
|
|
155
|
+
const index = Number.parseInt(answer.trim(), 10) - 1;
|
|
156
|
+
const selected = choices[index];
|
|
157
|
+
if (selected) {
|
|
158
|
+
rl.close();
|
|
159
|
+
resolve9(selected.value);
|
|
160
|
+
} else {
|
|
161
|
+
out.write(` Please enter a number between 1 and ${choices.length}
|
|
162
|
+
`);
|
|
163
|
+
ask();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
ask();
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function promptConfirm(message, defaultValue = false, options) {
|
|
171
|
+
return new Promise((resolve9) => {
|
|
172
|
+
const rl = createReadline(options);
|
|
173
|
+
const suffix = defaultValue ? "Y/n" : "y/N";
|
|
174
|
+
const ask = () => {
|
|
175
|
+
rl.question(` ? ${message} (${suffix}): `, (answer) => {
|
|
176
|
+
const normalized = answer.trim().toLowerCase();
|
|
177
|
+
if (normalized.length === 0) {
|
|
178
|
+
rl.close();
|
|
179
|
+
resolve9(defaultValue);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (normalized === "y" || normalized === "yes") {
|
|
183
|
+
rl.close();
|
|
184
|
+
resolve9(true);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (normalized === "n" || normalized === "no") {
|
|
188
|
+
rl.close();
|
|
189
|
+
resolve9(false);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
(options?.output ?? process.stdout).write(" Please answer with y or n\n");
|
|
193
|
+
ask();
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
ask();
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
function createReadline(options) {
|
|
200
|
+
return (0, import_node_readline.createInterface)({
|
|
201
|
+
input: options?.input ?? process.stdin,
|
|
202
|
+
output: options?.output ?? process.stdout
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/prompts/prompt-client.ts
|
|
207
|
+
var PromptCancelledError = class extends Error {
|
|
208
|
+
constructor(message = "Prompt cancelled by user") {
|
|
209
|
+
super(message);
|
|
210
|
+
this.name = "PromptCancelledError";
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
var ReadlinePromptClient = class {
|
|
214
|
+
async text(message, defaultValue) {
|
|
215
|
+
return promptText(message, defaultValue);
|
|
216
|
+
}
|
|
217
|
+
async select(message, options) {
|
|
218
|
+
return promptSelect(
|
|
219
|
+
message,
|
|
220
|
+
options.filter((option) => option.disabled !== true).map((option) => ({ label: option.label, value: option.value }))
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
async confirm(message, defaultValue = false) {
|
|
224
|
+
return promptConfirm(message, defaultValue);
|
|
225
|
+
}
|
|
226
|
+
intro(message) {
|
|
227
|
+
void message;
|
|
228
|
+
}
|
|
229
|
+
outro(message) {
|
|
230
|
+
void message;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
function createPromptClient() {
|
|
234
|
+
const canUseInteractiveClack = typeof process !== "undefined" && process.stdin.isTTY && process.stdout.isTTY;
|
|
235
|
+
if (canUseInteractiveClack) {
|
|
236
|
+
return new ClackPromptClient();
|
|
237
|
+
}
|
|
238
|
+
return new ReadlinePromptClient();
|
|
239
|
+
}
|
|
240
|
+
var ClackPromptClient = class {
|
|
241
|
+
async text(message, defaultValue) {
|
|
242
|
+
const result = await (0, import_prompts.text)({
|
|
243
|
+
message,
|
|
244
|
+
placeholder: defaultValue,
|
|
245
|
+
defaultValue
|
|
246
|
+
});
|
|
247
|
+
if ((0, import_prompts.isCancel)(result)) {
|
|
248
|
+
(0, import_prompts.cancel)("Operation cancelled.");
|
|
249
|
+
throw new PromptCancelledError();
|
|
250
|
+
}
|
|
251
|
+
const value = result.trim();
|
|
252
|
+
if (value.length > 0) return value;
|
|
253
|
+
return defaultValue ?? "";
|
|
254
|
+
}
|
|
255
|
+
async select(message, options) {
|
|
256
|
+
const mappedOptions = options.map((option) => ({
|
|
257
|
+
label: option.label,
|
|
258
|
+
value: option.value,
|
|
259
|
+
hint: option.hint,
|
|
260
|
+
disabled: option.disabled
|
|
261
|
+
}));
|
|
262
|
+
const result = await (0, import_prompts.select)({
|
|
263
|
+
message,
|
|
264
|
+
options: mappedOptions
|
|
265
|
+
});
|
|
266
|
+
if ((0, import_prompts.isCancel)(result)) {
|
|
267
|
+
(0, import_prompts.cancel)("Operation cancelled.");
|
|
268
|
+
throw new PromptCancelledError();
|
|
269
|
+
}
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
async confirm(message, defaultValue = false) {
|
|
273
|
+
const result = await (0, import_prompts.confirm)({
|
|
274
|
+
message,
|
|
275
|
+
initialValue: defaultValue
|
|
276
|
+
});
|
|
277
|
+
if ((0, import_prompts.isCancel)(result)) {
|
|
278
|
+
(0, import_prompts.cancel)("Operation cancelled.");
|
|
279
|
+
throw new PromptCancelledError();
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
intro(message) {
|
|
284
|
+
(0, import_prompts.intro)(message);
|
|
285
|
+
}
|
|
286
|
+
outro(message) {
|
|
287
|
+
(0, import_prompts.outro)(message);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
88
291
|
// src/types.ts
|
|
89
292
|
var PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"];
|
|
90
293
|
var TEMPLATES = [
|
|
@@ -93,28 +296,6 @@ var TEMPLATES = [
|
|
|
93
296
|
"react-sync",
|
|
94
297
|
"react-basic"
|
|
95
298
|
];
|
|
96
|
-
var TEMPLATE_INFO = [
|
|
97
|
-
{
|
|
98
|
-
name: "react-tailwind-sync",
|
|
99
|
-
label: "React + Tailwind (with sync)",
|
|
100
|
-
description: "Polished dark-themed app with Tailwind CSS and sync server (Recommended)"
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
name: "react-tailwind",
|
|
104
|
-
label: "React + Tailwind (local-only)",
|
|
105
|
-
description: "Polished dark-themed app with Tailwind CSS \u2014 no sync server"
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
name: "react-sync",
|
|
109
|
-
label: "React + CSS (with sync)",
|
|
110
|
-
description: "Clean CSS app with sync server included"
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
name: "react-basic",
|
|
114
|
-
label: "React + CSS (local-only)",
|
|
115
|
-
description: "Clean CSS app \u2014 no sync server"
|
|
116
|
-
}
|
|
117
|
-
];
|
|
118
299
|
|
|
119
300
|
// src/utils/fs-helpers.ts
|
|
120
301
|
var import_promises = require("fs/promises");
|
|
@@ -258,288 +439,2291 @@ function getRunDevCommand(pm) {
|
|
|
258
439
|
return `${pm} dev`;
|
|
259
440
|
}
|
|
260
441
|
|
|
261
|
-
// src/
|
|
262
|
-
var
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
}
|
|
442
|
+
// src/commands/create/environment-detection.ts
|
|
443
|
+
var import_promises2 = require("fs/promises");
|
|
444
|
+
var import_node_path2 = require("path");
|
|
445
|
+
function detectEditorFromEnvironment(env = process.env) {
|
|
446
|
+
const termProgram = String(env.TERM_PROGRAM ?? "").toLowerCase();
|
|
447
|
+
const editorValue = `${String(env.EDITOR ?? "")} ${String(env.VISUAL ?? "")}`.toLowerCase();
|
|
448
|
+
if (termProgram.includes("cursor") || env.CURSOR_TRACE_ID !== void 0 || env.CURSOR_SESSION_ID !== void 0 || editorValue.includes("cursor")) {
|
|
449
|
+
return { editor: "cursor", source: "env" };
|
|
450
|
+
}
|
|
451
|
+
if (termProgram.includes("windsurf") || env.WINDSURF_SESSION_ID !== void 0 || editorValue.includes("windsurf")) {
|
|
452
|
+
return { editor: "windsurf", source: "env" };
|
|
453
|
+
}
|
|
454
|
+
if (termProgram.includes("vscode") || env.VSCODE_GIT_IPC_HANDLE !== void 0 || env.VSCODE_IPC_HOOK !== void 0 || env.VSCODE_PID !== void 0 || editorValue.includes("code")) {
|
|
455
|
+
return { editor: "vscode", source: "env" };
|
|
456
|
+
}
|
|
457
|
+
if (termProgram.includes("zed") || env.ZED_TERM !== void 0 || editorValue.includes("zed")) {
|
|
458
|
+
return { editor: "zed", source: "env" };
|
|
459
|
+
}
|
|
460
|
+
return { editor: "unknown", source: "none" };
|
|
273
461
|
}
|
|
274
|
-
function
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
462
|
+
async function detectGitContext(startDir) {
|
|
463
|
+
const root = await findNearestAncestorWithEntry(startDir, ".git");
|
|
464
|
+
return {
|
|
465
|
+
hasRepository: root !== null,
|
|
466
|
+
repositoryRoot: root
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
async function detectMonorepoContext(startDir) {
|
|
470
|
+
let current = (0, import_node_path2.resolve)(startDir);
|
|
471
|
+
for (; ; ) {
|
|
472
|
+
if (await fileExists((0, import_node_path2.join)(current, "pnpm-workspace.yaml"))) {
|
|
473
|
+
return { isMonorepo: true, root: current, kind: "pnpm-workspace" };
|
|
286
474
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
ask();
|
|
475
|
+
if (await fileExists((0, import_node_path2.join)(current, "turbo.json"))) {
|
|
476
|
+
return { isMonorepo: true, root: current, kind: "turborepo" };
|
|
477
|
+
}
|
|
478
|
+
const packageJsonPath = (0, import_node_path2.join)(current, "package.json");
|
|
479
|
+
if (await fileExists(packageJsonPath)) {
|
|
480
|
+
try {
|
|
481
|
+
const packageJsonRaw = await (0, import_promises2.readFile)(packageJsonPath, "utf-8");
|
|
482
|
+
const parsed = JSON.parse(packageJsonRaw);
|
|
483
|
+
if (Array.isArray(parsed.workspaces) || isNpmWorkspaceObject(parsed.workspaces)) {
|
|
484
|
+
return { isMonorepo: true, root: current, kind: "npm-workspaces" };
|
|
298
485
|
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
|
|
486
|
+
} catch {
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const parent = (0, import_node_path2.dirname)(current);
|
|
490
|
+
if (parent === current) {
|
|
491
|
+
return { isMonorepo: false, root: null, kind: "none" };
|
|
492
|
+
}
|
|
493
|
+
current = parent;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async function applyEditorWorkspacePreset(params) {
|
|
497
|
+
const { targetDir, editor } = params;
|
|
498
|
+
if (editor !== "vscode" && editor !== "cursor" && editor !== "windsurf") {
|
|
499
|
+
return { applied: false, filePath: null };
|
|
500
|
+
}
|
|
501
|
+
const vscodeDir = (0, import_node_path2.join)(targetDir, ".vscode");
|
|
502
|
+
const extensionsPath = (0, import_node_path2.join)(vscodeDir, "extensions.json");
|
|
503
|
+
await (0, import_promises2.mkdir)(vscodeDir, { recursive: true });
|
|
504
|
+
const recommendations = ["korajs.kora-devtools"];
|
|
505
|
+
const existing = await readJsonObject(extensionsPath);
|
|
506
|
+
const existingRecommendations = Array.isArray(existing?.recommendations) ? existing.recommendations.filter((item) => typeof item === "string") : [];
|
|
507
|
+
const mergedRecommendations = dedupeStrings([...existingRecommendations, ...recommendations]);
|
|
508
|
+
const next = {
|
|
509
|
+
recommendations: mergedRecommendations
|
|
510
|
+
};
|
|
511
|
+
await (0, import_promises2.writeFile)(extensionsPath, `${JSON.stringify(next, null, 2)}
|
|
512
|
+
`, "utf-8");
|
|
513
|
+
return { applied: true, filePath: extensionsPath };
|
|
514
|
+
}
|
|
515
|
+
function resolveMonorepoTargetDirectory(monorepoRoot, projectName) {
|
|
516
|
+
return (0, import_node_path2.join)(monorepoRoot, "packages", projectName);
|
|
517
|
+
}
|
|
518
|
+
function dedupeStrings(values) {
|
|
519
|
+
return Array.from(new Set(values));
|
|
520
|
+
}
|
|
521
|
+
async function readJsonObject(path) {
|
|
522
|
+
try {
|
|
523
|
+
const content = await (0, import_promises2.readFile)(path, "utf-8");
|
|
524
|
+
const parsed = JSON.parse(content);
|
|
525
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
526
|
+
return parsed;
|
|
527
|
+
}
|
|
528
|
+
return null;
|
|
529
|
+
} catch {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function isNpmWorkspaceObject(value) {
|
|
534
|
+
if (typeof value !== "object" || value === null) return false;
|
|
535
|
+
const record = value;
|
|
536
|
+
return Array.isArray(record.packages);
|
|
537
|
+
}
|
|
538
|
+
async function fileExists(path) {
|
|
539
|
+
try {
|
|
540
|
+
await (0, import_promises2.access)(path);
|
|
541
|
+
return true;
|
|
542
|
+
} catch {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
async function findNearestAncestorWithEntry(startDir, entryName) {
|
|
547
|
+
let current = (0, import_node_path2.resolve)(startDir);
|
|
548
|
+
for (; ; ) {
|
|
549
|
+
const candidate = (0, import_node_path2.join)(current, entryName);
|
|
550
|
+
try {
|
|
551
|
+
await (0, import_promises2.stat)(candidate);
|
|
552
|
+
return current;
|
|
553
|
+
} catch {
|
|
554
|
+
}
|
|
555
|
+
const parent = (0, import_node_path2.dirname)(current);
|
|
556
|
+
if (parent === current) {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
current = parent;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/commands/create/options.ts
|
|
564
|
+
function determineTemplateFromSelections(input) {
|
|
565
|
+
const shouldSync = input.sync && input.db !== "none";
|
|
566
|
+
if (input.tailwind && shouldSync) return "react-tailwind-sync";
|
|
567
|
+
if (input.tailwind && !shouldSync) return "react-tailwind";
|
|
568
|
+
if (!input.tailwind && shouldSync) return "react-sync";
|
|
569
|
+
return "react-basic";
|
|
570
|
+
}
|
|
571
|
+
function isFrameworkValue(value) {
|
|
572
|
+
return value === "react" || value === "vue" || value === "svelte" || value === "solid";
|
|
573
|
+
}
|
|
574
|
+
function isAuthValue(value) {
|
|
575
|
+
return value === "none" || value === "email-password" || value === "oauth";
|
|
576
|
+
}
|
|
577
|
+
function isDatabaseValue(value) {
|
|
578
|
+
return value === "none" || value === "sqlite" || value === "postgres";
|
|
579
|
+
}
|
|
580
|
+
function isDatabaseProviderValue(value) {
|
|
581
|
+
return value === "none" || value === "local" || value === "supabase" || value === "neon" || value === "railway" || value === "vercel-postgres" || value === "custom";
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/commands/create/preferences-flow.ts
|
|
585
|
+
async function resolveCreatePreferencesFlow(params) {
|
|
586
|
+
const { flags, prompts, store } = params;
|
|
587
|
+
const stored = store.getCreatePreferences();
|
|
588
|
+
const base = getCreatePreferencesOrDefault(store);
|
|
589
|
+
const hasExplicitFlags = hasExplicitPreferenceFlags(flags);
|
|
590
|
+
const canOfferStored = !flags.useDefaults && !hasExplicitFlags && stored !== null && promptSupportsRichOptions();
|
|
591
|
+
let effective = { ...base };
|
|
592
|
+
let usedStoredPreferences = false;
|
|
593
|
+
if (flags.useDefaults) {
|
|
594
|
+
effective = getDefaultCreatePreferences();
|
|
595
|
+
} else if (canOfferStored && stored !== null) {
|
|
596
|
+
const reuseStored = await prompts.select("Welcome back! Choose setup mode:", [
|
|
597
|
+
{ label: formatStoredPreferenceLabel(stored), value: "reuse" },
|
|
598
|
+
{ label: "Customize", value: "customize" }
|
|
599
|
+
]);
|
|
600
|
+
if (reuseStored === "reuse") {
|
|
601
|
+
effective = { ...stored };
|
|
602
|
+
usedStoredPreferences = true;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (flags.framework !== void 0) {
|
|
606
|
+
if (!isFrameworkValue(flags.framework)) {
|
|
607
|
+
throw new Error(
|
|
608
|
+
`Invalid --framework value "${flags.framework}". Expected one of: react, vue, svelte, solid.`
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
effective.framework = flags.framework;
|
|
612
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
613
|
+
effective.framework = await prompts.select("UI framework:", [
|
|
614
|
+
{ label: "React", value: "react" },
|
|
615
|
+
{ label: "Vue (coming soon)", value: "vue", disabled: true },
|
|
616
|
+
{ label: "Svelte (coming soon)", value: "svelte", disabled: true },
|
|
617
|
+
{ label: "Solid (coming soon)", value: "solid", disabled: true }
|
|
618
|
+
]);
|
|
619
|
+
}
|
|
620
|
+
if (flags.auth !== void 0) {
|
|
621
|
+
if (!isAuthValue(flags.auth)) {
|
|
622
|
+
throw new Error(
|
|
623
|
+
`Invalid --auth value "${flags.auth}". Expected one of: none, email-password, oauth.`
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
effective.auth = flags.auth;
|
|
627
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
628
|
+
effective.auth = await prompts.select("Authentication:", [
|
|
629
|
+
{ label: "None", value: "none" },
|
|
630
|
+
{ label: "Email + Password (coming soon)", value: "email-password", disabled: true },
|
|
631
|
+
{ label: "OAuth (coming soon)", value: "oauth", disabled: true }
|
|
632
|
+
]);
|
|
633
|
+
}
|
|
634
|
+
if (flags.tailwind !== void 0) {
|
|
635
|
+
effective.tailwind = flags.tailwind;
|
|
636
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
637
|
+
effective.tailwind = await prompts.confirm("Use Tailwind CSS?", true);
|
|
638
|
+
}
|
|
639
|
+
if (flags.sync !== void 0) {
|
|
640
|
+
effective.sync = flags.sync;
|
|
641
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
642
|
+
effective.sync = await prompts.confirm("Enable multi-device sync?", true);
|
|
643
|
+
}
|
|
644
|
+
if (flags.db !== void 0) {
|
|
645
|
+
if (!isDatabaseValue(flags.db)) {
|
|
646
|
+
throw new Error(`Invalid --db value "${flags.db}". Expected one of: none, sqlite, postgres.`);
|
|
647
|
+
}
|
|
648
|
+
effective.db = flags.db;
|
|
649
|
+
} else if (!effective.sync) {
|
|
650
|
+
effective.db = "none";
|
|
651
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
652
|
+
effective.db = await prompts.select("Server-side database:", [
|
|
653
|
+
{ label: "SQLite (zero-config)", value: "sqlite" },
|
|
654
|
+
{ label: "PostgreSQL (production-scale)", value: "postgres" }
|
|
655
|
+
]);
|
|
656
|
+
}
|
|
657
|
+
if (effective.db !== "postgres") {
|
|
658
|
+
effective.dbProvider = "none";
|
|
659
|
+
} else if (flags.dbProvider !== void 0) {
|
|
660
|
+
if (!isDatabaseProviderValue(flags.dbProvider)) {
|
|
661
|
+
throw new Error(
|
|
662
|
+
`Invalid --db-provider value "${flags.dbProvider}". Expected one of: none, local, supabase, neon, railway, vercel-postgres, custom.`
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
effective.dbProvider = flags.dbProvider;
|
|
666
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
667
|
+
effective.dbProvider = await prompts.select("Database provider:", [
|
|
668
|
+
{ label: "Local Postgres", value: "local" },
|
|
669
|
+
{ label: "Supabase", value: "supabase" },
|
|
670
|
+
{ label: "Neon", value: "neon" },
|
|
671
|
+
{ label: "Railway", value: "railway" },
|
|
672
|
+
{ label: "Vercel Postgres", value: "vercel-postgres" },
|
|
673
|
+
{ label: "Custom connection string", value: "custom" }
|
|
674
|
+
]);
|
|
675
|
+
}
|
|
676
|
+
const template = determineTemplateFromSelections({
|
|
677
|
+
tailwind: effective.tailwind,
|
|
678
|
+
sync: effective.sync,
|
|
679
|
+
db: effective.db
|
|
302
680
|
});
|
|
681
|
+
return {
|
|
682
|
+
framework: effective.framework,
|
|
683
|
+
auth: effective.auth,
|
|
684
|
+
db: effective.db,
|
|
685
|
+
dbProvider: effective.dbProvider,
|
|
686
|
+
tailwind: effective.tailwind,
|
|
687
|
+
sync: effective.sync,
|
|
688
|
+
template,
|
|
689
|
+
usedStoredPreferences
|
|
690
|
+
};
|
|
303
691
|
}
|
|
304
|
-
function
|
|
305
|
-
return
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
692
|
+
function shouldSavePreferences(flags) {
|
|
693
|
+
return !flags.useDefaults;
|
|
694
|
+
}
|
|
695
|
+
function saveResolvedPreferences(store, resolution) {
|
|
696
|
+
store.saveCreatePreferences({
|
|
697
|
+
framework: resolution.framework,
|
|
698
|
+
tailwind: resolution.tailwind,
|
|
699
|
+
sync: resolution.sync,
|
|
700
|
+
db: resolution.db,
|
|
701
|
+
dbProvider: resolution.dbProvider,
|
|
702
|
+
auth: resolution.auth,
|
|
703
|
+
packageManager: resolution.packageManager
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
function hasExplicitPreferenceFlags(flags) {
|
|
707
|
+
return flags.framework !== void 0 || flags.auth !== void 0 || flags.db !== void 0 || flags.dbProvider !== void 0 || flags.tailwind !== void 0 || flags.sync !== void 0;
|
|
708
|
+
}
|
|
709
|
+
function promptSupportsRichOptions() {
|
|
710
|
+
return typeof process !== "undefined" && process.stdin.isTTY && process.stdout.isTTY;
|
|
711
|
+
}
|
|
712
|
+
function formatStoredPreferenceLabel(preferences) {
|
|
713
|
+
const syncLabel = preferences.sync ? `sync/${preferences.db}` : "local-only";
|
|
714
|
+
const styleLabel = preferences.tailwind ? "tailwind" : "css";
|
|
715
|
+
return `Use previous settings (${preferences.framework} + ${styleLabel} + ${syncLabel} + ${preferences.packageManager})`;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// src/commands/create/project-name.ts
|
|
719
|
+
var import_validate_npm_package_name = __toESM(require("validate-npm-package-name"), 1);
|
|
720
|
+
function validateProjectName(name) {
|
|
721
|
+
const trimmedName = name.trim();
|
|
722
|
+
if (trimmedName.length === 0) {
|
|
723
|
+
return {
|
|
724
|
+
valid: false,
|
|
725
|
+
issues: ["Project name cannot be empty."]
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
const validation = (0, import_validate_npm_package_name.default)(trimmedName);
|
|
729
|
+
const issues = [...validation.errors ?? [], ...validation.warnings ?? []];
|
|
730
|
+
if (!validation.validForNewPackages && issues.length === 0) {
|
|
731
|
+
return {
|
|
732
|
+
valid: false,
|
|
733
|
+
issues: ["Project name is not a valid npm package name."]
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
return {
|
|
737
|
+
valid: validation.validForNewPackages,
|
|
738
|
+
issues
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/commands/create/sync-provider-preset.ts
|
|
743
|
+
var import_promises3 = require("fs/promises");
|
|
744
|
+
var import_node_path3 = require("path");
|
|
745
|
+
async function applySyncProviderPreset(options) {
|
|
746
|
+
if (!isSyncTemplate(options.template)) {
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (options.db !== "postgres") {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
const providerName = getProviderDisplayName(options.dbProvider);
|
|
753
|
+
const providerConnectionString = getProviderConnectionStringExample(options.dbProvider);
|
|
754
|
+
const serverPath = (0, import_node_path3.join)(options.targetDir, "server.ts");
|
|
755
|
+
const envPath = (0, import_node_path3.join)(options.targetDir, ".env.example");
|
|
756
|
+
const readmePath = (0, import_node_path3.join)(options.targetDir, "README.md");
|
|
757
|
+
const serverTemplate = [
|
|
758
|
+
"import { createPostgresServerStore, createProductionServer } from '@korajs/server'",
|
|
759
|
+
"",
|
|
760
|
+
`// PostgreSQL provider preset: ${providerName}`,
|
|
761
|
+
"// Ensure DATABASE_URL is set in your environment.",
|
|
762
|
+
"",
|
|
763
|
+
"async function start(): Promise<void> {",
|
|
764
|
+
" const connectionString = process.env.DATABASE_URL || ''",
|
|
765
|
+
" if (connectionString.length === 0) {",
|
|
766
|
+
" throw new Error('DATABASE_URL is required for PostgreSQL sync server store.')",
|
|
767
|
+
" }",
|
|
768
|
+
"",
|
|
769
|
+
" const store = await createPostgresServerStore({ connectionString })",
|
|
770
|
+
" const server = createProductionServer({",
|
|
771
|
+
" store,",
|
|
772
|
+
" port: Number(process.env.PORT) || 3001,",
|
|
773
|
+
" staticDir: './dist',",
|
|
774
|
+
" syncPath: '/kora-sync',",
|
|
775
|
+
" })",
|
|
776
|
+
"",
|
|
777
|
+
" const url = await server.start()",
|
|
778
|
+
" console.log(`Kora app running at ${url}`)",
|
|
779
|
+
"}",
|
|
780
|
+
"",
|
|
781
|
+
"void start()",
|
|
782
|
+
""
|
|
783
|
+
].join("\n");
|
|
784
|
+
const envTemplate = [
|
|
785
|
+
"# Kora Sync Server",
|
|
786
|
+
"# WebSocket URL for the sync server (used by the client)",
|
|
787
|
+
"VITE_SYNC_URL=ws://localhost:3001",
|
|
788
|
+
"",
|
|
789
|
+
"# Sync server port",
|
|
790
|
+
"PORT=3001",
|
|
791
|
+
"",
|
|
792
|
+
`# PostgreSQL connection string (${providerName})`,
|
|
793
|
+
`# Example: ${providerConnectionString}`,
|
|
794
|
+
"DATABASE_URL=",
|
|
795
|
+
""
|
|
796
|
+
].join("\n");
|
|
797
|
+
const existingReadme = await (0, import_promises3.readFile)(readmePath, "utf-8");
|
|
798
|
+
const trimmedReadme = existingReadme.trimEnd();
|
|
799
|
+
const readmeSuffix = [
|
|
800
|
+
"",
|
|
801
|
+
"## PostgreSQL Provider Preset",
|
|
802
|
+
"",
|
|
803
|
+
`Selected DB provider: ${options.dbProvider}`,
|
|
804
|
+
"",
|
|
805
|
+
"This scaffold uses `createPostgresServerStore` in `server.ts` and reads `DATABASE_URL` from the environment. See `.env.example` for provider-specific examples.",
|
|
806
|
+
""
|
|
807
|
+
].join("\n");
|
|
808
|
+
const readmeTemplate = `${trimmedReadme}${readmeSuffix}`;
|
|
809
|
+
await (0, import_promises3.writeFile)(serverPath, serverTemplate, "utf-8");
|
|
810
|
+
await (0, import_promises3.writeFile)(envPath, envTemplate, "utf-8");
|
|
811
|
+
await (0, import_promises3.writeFile)(readmePath, readmeTemplate, "utf-8");
|
|
812
|
+
}
|
|
813
|
+
function isSyncTemplate(template) {
|
|
814
|
+
return template === "react-sync" || template === "react-tailwind-sync";
|
|
815
|
+
}
|
|
816
|
+
function getProviderDisplayName(provider) {
|
|
817
|
+
switch (provider) {
|
|
818
|
+
case "supabase":
|
|
819
|
+
return "Supabase";
|
|
820
|
+
case "neon":
|
|
821
|
+
return "Neon";
|
|
822
|
+
case "railway":
|
|
823
|
+
return "Railway";
|
|
824
|
+
case "vercel-postgres":
|
|
825
|
+
return "Vercel Postgres";
|
|
826
|
+
case "custom":
|
|
827
|
+
return "Custom";
|
|
828
|
+
case "local":
|
|
829
|
+
return "Local Postgres";
|
|
830
|
+
case "none":
|
|
831
|
+
return "PostgreSQL";
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function getProviderConnectionStringExample(provider) {
|
|
835
|
+
switch (provider) {
|
|
836
|
+
case "supabase":
|
|
837
|
+
return "postgresql://postgres:<password>@db.<project-ref>.supabase.co:5432/postgres?sslmode=require";
|
|
838
|
+
case "neon":
|
|
839
|
+
return "postgresql://<user>:<password>@<branch>.<project>.neon.tech/neondb?sslmode=require";
|
|
840
|
+
case "railway":
|
|
841
|
+
return "postgresql://postgres:<password>@<host>.railway.app:<port>/railway?sslmode=require";
|
|
842
|
+
case "vercel-postgres":
|
|
843
|
+
return "postgresql://<user>:<password>@<host>.pooler.vercel-storage.com:5432/verceldb?sslmode=require";
|
|
844
|
+
case "custom":
|
|
845
|
+
return "postgresql://<user>:<password>@<host>:5432/<database>";
|
|
846
|
+
case "local":
|
|
847
|
+
return "postgresql://postgres:postgres@localhost:5432/kora";
|
|
848
|
+
case "none":
|
|
849
|
+
return "postgresql://postgres:postgres@localhost:5432/kora";
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// src/templates/composer.ts
|
|
854
|
+
var import_node_fs = require("fs");
|
|
855
|
+
var import_promises4 = require("fs/promises");
|
|
856
|
+
var import_node_path4 = require("path");
|
|
857
|
+
var import_node_url = require("url");
|
|
858
|
+
var import_meta = {};
|
|
859
|
+
function substituteVariables(template, context) {
|
|
860
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
|
|
861
|
+
const value = context[key];
|
|
862
|
+
return value !== void 0 ? value : `{{${key}}}`;
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
function getTemplatePath(templateName) {
|
|
866
|
+
let dir = (0, import_node_path4.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
867
|
+
for (let i = 0; i < 7; i++) {
|
|
868
|
+
const candidate = (0, import_node_path4.resolve)(dir, "templates", templateName);
|
|
869
|
+
if ((0, import_node_fs.existsSync)(candidate)) {
|
|
870
|
+
return candidate;
|
|
871
|
+
}
|
|
872
|
+
dir = (0, import_node_path4.dirname)(dir);
|
|
873
|
+
}
|
|
874
|
+
const currentDir = (0, import_node_path4.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
875
|
+
return (0, import_node_path4.resolve)(currentDir, "..", "..", "..", "templates", templateName);
|
|
876
|
+
}
|
|
877
|
+
function createCompatibilityLayerPlan(templateName) {
|
|
878
|
+
const baseLayer = { category: "base", name: "base", sourceTemplate: "react-basic" };
|
|
879
|
+
const uiLayer = { category: "ui", name: "react", sourceTemplate: null };
|
|
880
|
+
const authLayer = { category: "auth", name: "none", sourceTemplate: null };
|
|
881
|
+
switch (templateName) {
|
|
882
|
+
case "react-basic":
|
|
883
|
+
return {
|
|
884
|
+
compatibilityTarget: templateName,
|
|
885
|
+
layers: [
|
|
886
|
+
baseLayer,
|
|
887
|
+
uiLayer,
|
|
888
|
+
{ category: "style", name: "plain", sourceTemplate: null },
|
|
889
|
+
{ category: "sync", name: "disabled", sourceTemplate: null },
|
|
890
|
+
{ category: "db", name: "none", sourceTemplate: null },
|
|
891
|
+
authLayer
|
|
892
|
+
]
|
|
893
|
+
};
|
|
894
|
+
case "react-tailwind":
|
|
895
|
+
return {
|
|
896
|
+
compatibilityTarget: templateName,
|
|
897
|
+
layers: [
|
|
898
|
+
baseLayer,
|
|
899
|
+
uiLayer,
|
|
900
|
+
{ category: "style", name: "tailwind", sourceTemplate: "react-tailwind" },
|
|
901
|
+
{ category: "sync", name: "disabled", sourceTemplate: null },
|
|
902
|
+
{ category: "db", name: "none", sourceTemplate: null },
|
|
903
|
+
authLayer
|
|
904
|
+
]
|
|
905
|
+
};
|
|
906
|
+
case "react-sync":
|
|
907
|
+
return {
|
|
908
|
+
compatibilityTarget: templateName,
|
|
909
|
+
layers: [
|
|
910
|
+
baseLayer,
|
|
911
|
+
uiLayer,
|
|
912
|
+
{ category: "style", name: "plain", sourceTemplate: null },
|
|
913
|
+
{ category: "sync", name: "enabled", sourceTemplate: "react-sync" },
|
|
914
|
+
{ category: "db", name: "sqlite", sourceTemplate: null },
|
|
915
|
+
authLayer
|
|
916
|
+
]
|
|
917
|
+
};
|
|
918
|
+
case "react-tailwind-sync":
|
|
919
|
+
return {
|
|
920
|
+
compatibilityTarget: templateName,
|
|
921
|
+
layers: [
|
|
922
|
+
baseLayer,
|
|
923
|
+
uiLayer,
|
|
924
|
+
{ category: "style", name: "tailwind", sourceTemplate: "react-tailwind" },
|
|
925
|
+
{ category: "sync", name: "enabled", sourceTemplate: "react-tailwind-sync" },
|
|
926
|
+
{ category: "db", name: "sqlite", sourceTemplate: null },
|
|
927
|
+
authLayer
|
|
928
|
+
]
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
async function composeTemplateLayers(plan, targetDir, context) {
|
|
933
|
+
const vars = {
|
|
934
|
+
projectName: context.projectName,
|
|
935
|
+
packageManager: context.packageManager,
|
|
936
|
+
koraVersion: context.koraVersion,
|
|
937
|
+
dbProvider: context.dbProvider ?? "none"
|
|
938
|
+
};
|
|
939
|
+
for (const layer of plan.layers) {
|
|
940
|
+
if (layer.sourceTemplate === null) {
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
const sourceDir = getTemplatePath(layer.sourceTemplate);
|
|
944
|
+
await copyDirectory(sourceDir, targetDir, vars);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
async function copyDirectory(src, dest, vars) {
|
|
948
|
+
await (0, import_promises4.mkdir)(dest, { recursive: true });
|
|
949
|
+
const entries = await (0, import_promises4.readdir)(src);
|
|
950
|
+
for (const entry of entries) {
|
|
951
|
+
const srcPath = (0, import_node_path4.join)(src, entry);
|
|
952
|
+
const srcStat = await (0, import_promises4.stat)(srcPath);
|
|
953
|
+
if (srcStat.isDirectory()) {
|
|
954
|
+
await copyDirectory(srcPath, (0, import_node_path4.join)(dest, entry), vars);
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
if (entry.endsWith(".hbs")) {
|
|
958
|
+
const content = await (0, import_promises4.readFile)(srcPath, "utf-8");
|
|
959
|
+
const outputName = entry.slice(0, -4);
|
|
960
|
+
await (0, import_promises4.writeFile)((0, import_node_path4.join)(dest, outputName), substituteVariables(content, vars), "utf-8");
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
await (0, import_promises4.copyFile)(srcPath, (0, import_node_path4.join)(dest, entry));
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// src/commands/create/template-engine.ts
|
|
968
|
+
async function scaffoldTemplate(templateName, targetDir, context) {
|
|
969
|
+
const plan = createCompatibilityLayerPlan(templateName);
|
|
970
|
+
await composeTemplateLayers(plan, targetDir, context);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// src/commands/create/create-command.ts
|
|
974
|
+
var import_meta2 = {};
|
|
975
|
+
var createCommand = (0, import_citty.defineCommand)({
|
|
976
|
+
meta: {
|
|
977
|
+
name: "create",
|
|
978
|
+
description: "Create a new Kora application"
|
|
979
|
+
},
|
|
980
|
+
args: {
|
|
981
|
+
name: {
|
|
982
|
+
type: "positional",
|
|
983
|
+
description: "Project directory name",
|
|
984
|
+
required: false
|
|
985
|
+
},
|
|
986
|
+
template: {
|
|
987
|
+
type: "string",
|
|
988
|
+
description: "Project template (react-tailwind-sync, react-tailwind, react-sync, react-basic)"
|
|
989
|
+
},
|
|
990
|
+
pm: {
|
|
991
|
+
type: "string",
|
|
992
|
+
description: "Package manager (pnpm, npm, yarn, bun)"
|
|
993
|
+
},
|
|
994
|
+
framework: {
|
|
995
|
+
type: "string",
|
|
996
|
+
description: "UI framework (react, vue, svelte, solid)"
|
|
997
|
+
},
|
|
998
|
+
db: {
|
|
999
|
+
type: "string",
|
|
1000
|
+
description: "Database backend for sync templates (sqlite, postgres)"
|
|
1001
|
+
},
|
|
1002
|
+
"db-provider": {
|
|
1003
|
+
type: "string",
|
|
1004
|
+
description: "Database provider for postgres (local, supabase, neon, railway, vercel-postgres, custom)"
|
|
1005
|
+
},
|
|
1006
|
+
auth: {
|
|
1007
|
+
type: "string",
|
|
1008
|
+
description: "Authentication mode (none, email-password, oauth)"
|
|
1009
|
+
},
|
|
1010
|
+
"skip-install": {
|
|
1011
|
+
type: "boolean",
|
|
1012
|
+
description: "Skip installing dependencies",
|
|
1013
|
+
default: false
|
|
1014
|
+
},
|
|
1015
|
+
yes: {
|
|
1016
|
+
type: "boolean",
|
|
1017
|
+
alias: "y",
|
|
1018
|
+
description: "Accept all defaults (react-tailwind-sync + detected package manager)",
|
|
1019
|
+
default: false
|
|
1020
|
+
},
|
|
1021
|
+
tailwind: {
|
|
1022
|
+
type: "boolean",
|
|
1023
|
+
description: "Use Tailwind CSS (use --no-tailwind to skip)"
|
|
1024
|
+
},
|
|
1025
|
+
sync: {
|
|
1026
|
+
type: "boolean",
|
|
1027
|
+
description: "Include sync server (use --no-sync to skip)"
|
|
1028
|
+
}
|
|
1029
|
+
},
|
|
1030
|
+
async run({ args }) {
|
|
1031
|
+
const logger = createLogger();
|
|
1032
|
+
const prompts = createPromptClient();
|
|
1033
|
+
const preferenceStore = new PreferenceStore();
|
|
1034
|
+
logger.banner();
|
|
1035
|
+
try {
|
|
1036
|
+
const useDefaults = args.yes === true;
|
|
1037
|
+
if (!useDefaults) {
|
|
1038
|
+
prompts.intro("Kora.js \u2014 Offline-first application framework");
|
|
1039
|
+
}
|
|
1040
|
+
const projectName = args.name || (useDefaults ? "my-kora-app" : await prompts.text("Project name", "my-kora-app"));
|
|
1041
|
+
if (!projectName) {
|
|
1042
|
+
logger.error("Project name is required");
|
|
1043
|
+
process.exitCode = 1;
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
const nameValidation = validateProjectName(projectName);
|
|
1047
|
+
if (!nameValidation.valid) {
|
|
1048
|
+
logger.error("Invalid project name.");
|
|
1049
|
+
for (const issue of nameValidation.issues) {
|
|
1050
|
+
logger.step(`- ${issue}`);
|
|
1051
|
+
}
|
|
1052
|
+
if (!useDefaults) {
|
|
1053
|
+
prompts.outro("Project creation aborted.");
|
|
1054
|
+
}
|
|
1055
|
+
process.exitCode = 1;
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
const preferenceFlags = {
|
|
1059
|
+
framework: typeof args.framework === "string" ? args.framework : void 0,
|
|
1060
|
+
auth: typeof args.auth === "string" ? args.auth : void 0,
|
|
1061
|
+
db: typeof args.db === "string" ? args.db : void 0,
|
|
1062
|
+
dbProvider: typeof args["db-provider"] === "string" ? args["db-provider"] : void 0,
|
|
1063
|
+
tailwind: args.tailwind,
|
|
1064
|
+
sync: args.sync,
|
|
1065
|
+
useDefaults
|
|
1066
|
+
};
|
|
1067
|
+
const selection = await resolveCreatePreferencesFlow({
|
|
1068
|
+
flags: preferenceFlags,
|
|
1069
|
+
prompts,
|
|
1070
|
+
store: preferenceStore
|
|
1071
|
+
});
|
|
1072
|
+
if (selection.framework !== "react") {
|
|
1073
|
+
logger.error(`Framework "${selection.framework}" is not available yet. Use "react".`);
|
|
1074
|
+
if (!useDefaults) {
|
|
1075
|
+
prompts.outro("Project creation aborted.");
|
|
1076
|
+
}
|
|
1077
|
+
process.exitCode = 1;
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
if (selection.auth !== "none") {
|
|
1081
|
+
logger.error(`Auth mode "${selection.auth}" is not available yet. Use "none".`);
|
|
1082
|
+
if (!useDefaults) {
|
|
1083
|
+
prompts.outro("Project creation aborted.");
|
|
1084
|
+
}
|
|
1085
|
+
process.exitCode = 1;
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
const template = args.template && isValidTemplate(args.template) ? args.template : selection.template;
|
|
1089
|
+
let pm;
|
|
1090
|
+
if (args.pm && isValidPackageManager(args.pm)) {
|
|
1091
|
+
pm = args.pm;
|
|
1092
|
+
} else if (useDefaults) {
|
|
1093
|
+
pm = detectPackageManager();
|
|
1094
|
+
} else if (selection.usedStoredPreferences) {
|
|
1095
|
+
pm = preferenceStore.getCreatePreferences()?.packageManager ?? detectPackageManager();
|
|
1096
|
+
} else {
|
|
1097
|
+
const detected = detectPackageManager();
|
|
1098
|
+
pm = await prompts.select(
|
|
1099
|
+
"Package manager:",
|
|
1100
|
+
PACKAGE_MANAGERS.map((p) => ({
|
|
1101
|
+
label: p === detected ? `${p} (detected)` : p,
|
|
1102
|
+
value: p
|
|
1103
|
+
}))
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
const editorDetection = detectEditorFromEnvironment();
|
|
1107
|
+
const gitContext = await detectGitContext(process.cwd());
|
|
1108
|
+
const monorepoContext = await detectMonorepoContext(process.cwd());
|
|
1109
|
+
let targetDir = (0, import_node_path5.resolve)(process.cwd(), projectName);
|
|
1110
|
+
if (!useDefaults && monorepoContext.isMonorepo && monorepoContext.root !== null) {
|
|
1111
|
+
const useWorkspaceTarget = await prompts.confirm(
|
|
1112
|
+
`Detected ${formatMonorepoKind(monorepoContext.kind)} at ${monorepoContext.root}. Create app under packages/${projectName}?`,
|
|
1113
|
+
true
|
|
1114
|
+
);
|
|
1115
|
+
if (useWorkspaceTarget) {
|
|
1116
|
+
targetDir = resolveMonorepoTargetDirectory(monorepoContext.root, projectName);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
if (await directoryExists(targetDir)) {
|
|
1120
|
+
throw new ProjectExistsError(projectName);
|
|
1121
|
+
}
|
|
1122
|
+
const koraVersion = resolveKoraVersion();
|
|
1123
|
+
logger.step(`Creating ${projectName} with ${template} template...`);
|
|
1124
|
+
await scaffoldTemplate(template, targetDir, {
|
|
1125
|
+
projectName,
|
|
1126
|
+
packageManager: pm,
|
|
1127
|
+
koraVersion,
|
|
1128
|
+
dbProvider: selection.dbProvider
|
|
1129
|
+
});
|
|
1130
|
+
await applySyncProviderPreset({
|
|
1131
|
+
targetDir,
|
|
1132
|
+
template,
|
|
1133
|
+
db: selection.db,
|
|
1134
|
+
dbProvider: selection.dbProvider
|
|
1135
|
+
});
|
|
1136
|
+
if (!useDefaults && editorDetection.editor !== "unknown") {
|
|
1137
|
+
const shouldApplyEditorPreset = await prompts.confirm(
|
|
1138
|
+
`Detected ${formatEditor(editorDetection.editor)}. Add workspace recommendations for ${formatEditor(editorDetection.editor)}?`,
|
|
1139
|
+
true
|
|
1140
|
+
);
|
|
1141
|
+
if (shouldApplyEditorPreset) {
|
|
1142
|
+
const appliedPreset = await applyEditorWorkspacePreset({
|
|
1143
|
+
targetDir,
|
|
1144
|
+
editor: editorDetection.editor
|
|
1145
|
+
});
|
|
1146
|
+
if (appliedPreset.applied) {
|
|
1147
|
+
logger.step(
|
|
1148
|
+
`Added editor recommendations at ${relativeToTarget(targetDir, appliedPreset.filePath)}`
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
if (selection.db === "postgres" && isSyncTemplate2(template)) {
|
|
1154
|
+
logger.info(
|
|
1155
|
+
`Applied PostgreSQL sync preset (${formatDbProviderForLog(selection.dbProvider)}). Update DATABASE_URL in .env.example before running the sync server.`
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
if (gitContext.hasRepository) {
|
|
1159
|
+
logger.step(
|
|
1160
|
+
`Detected existing git repository at ${gitContext.repositoryRoot}. Skipping git initialization.`
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
logger.success("Project scaffolded");
|
|
1164
|
+
if (shouldSavePreferences(preferenceFlags)) {
|
|
1165
|
+
saveResolvedPreferences(preferenceStore, {
|
|
1166
|
+
framework: selection.framework,
|
|
1167
|
+
auth: selection.auth,
|
|
1168
|
+
db: selection.db,
|
|
1169
|
+
dbProvider: selection.dbProvider,
|
|
1170
|
+
tailwind: selection.tailwind,
|
|
1171
|
+
sync: selection.sync,
|
|
1172
|
+
packageManager: pm
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
if (!args["skip-install"]) {
|
|
1176
|
+
logger.step("Installing dependencies...");
|
|
1177
|
+
try {
|
|
1178
|
+
(0, import_node_child_process2.execSync)(getInstallCommand(pm), { cwd: targetDir, stdio: "inherit" });
|
|
1179
|
+
logger.success("Dependencies installed");
|
|
1180
|
+
} catch {
|
|
1181
|
+
logger.warn("Failed to install dependencies. Run install manually.");
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
logger.blank();
|
|
1185
|
+
logger.info("Done! Next steps:");
|
|
1186
|
+
logger.blank();
|
|
1187
|
+
logger.step(` cd ${targetDir}`);
|
|
1188
|
+
logger.step(` ${getRunDevCommand(pm)}`);
|
|
1189
|
+
logger.blank();
|
|
1190
|
+
if (!useDefaults) {
|
|
1191
|
+
prompts.outro("Project ready. Happy building with Kora!");
|
|
1192
|
+
}
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
if (error instanceof PromptCancelledError) {
|
|
1195
|
+
process.exitCode = 1;
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
if (error instanceof Error && error.message.startsWith("Invalid --")) {
|
|
1199
|
+
logger.error(error.message);
|
|
1200
|
+
if (!args.yes) {
|
|
1201
|
+
prompts.outro("Project creation aborted.");
|
|
1202
|
+
}
|
|
1203
|
+
process.exitCode = 1;
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
throw error;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
function isValidTemplate(value) {
|
|
1211
|
+
return TEMPLATES.includes(value);
|
|
1212
|
+
}
|
|
1213
|
+
function isValidPackageManager(value) {
|
|
1214
|
+
return PACKAGE_MANAGERS.includes(value);
|
|
1215
|
+
}
|
|
1216
|
+
function isSyncTemplate2(template) {
|
|
1217
|
+
return template === "react-sync" || template === "react-tailwind-sync";
|
|
1218
|
+
}
|
|
1219
|
+
function formatDbProviderForLog(dbProvider) {
|
|
1220
|
+
switch (dbProvider) {
|
|
1221
|
+
case "supabase":
|
|
1222
|
+
return "Supabase";
|
|
1223
|
+
case "neon":
|
|
1224
|
+
return "Neon";
|
|
1225
|
+
case "railway":
|
|
1226
|
+
return "Railway";
|
|
1227
|
+
case "vercel-postgres":
|
|
1228
|
+
return "Vercel Postgres";
|
|
1229
|
+
case "custom":
|
|
1230
|
+
return "Custom";
|
|
1231
|
+
case "local":
|
|
1232
|
+
return "Local Postgres";
|
|
1233
|
+
case "none":
|
|
1234
|
+
return "PostgreSQL";
|
|
1235
|
+
default:
|
|
1236
|
+
return dbProvider;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
function formatEditor(editor) {
|
|
1240
|
+
switch (editor) {
|
|
1241
|
+
case "vscode":
|
|
1242
|
+
return "VS Code";
|
|
1243
|
+
case "cursor":
|
|
1244
|
+
return "Cursor";
|
|
1245
|
+
case "windsurf":
|
|
1246
|
+
return "Windsurf";
|
|
1247
|
+
case "zed":
|
|
1248
|
+
return "Zed";
|
|
1249
|
+
default:
|
|
1250
|
+
return editor;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
function formatMonorepoKind(kind) {
|
|
1254
|
+
switch (kind) {
|
|
1255
|
+
case "pnpm-workspace":
|
|
1256
|
+
return "pnpm workspace";
|
|
1257
|
+
case "npm-workspaces":
|
|
1258
|
+
return "npm workspace";
|
|
1259
|
+
case "turborepo":
|
|
1260
|
+
return "Turborepo";
|
|
1261
|
+
default:
|
|
1262
|
+
return "monorepo";
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
function relativeToTarget(targetDir, filePath) {
|
|
1266
|
+
if (filePath === null) return ".";
|
|
1267
|
+
const normalizedTarget = targetDir.endsWith("/") ? targetDir : `${targetDir}/`;
|
|
1268
|
+
if (filePath.startsWith(normalizedTarget)) {
|
|
1269
|
+
return filePath.slice(normalizedTarget.length);
|
|
1270
|
+
}
|
|
1271
|
+
if (filePath === targetDir) {
|
|
1272
|
+
return ".";
|
|
1273
|
+
}
|
|
1274
|
+
return filePath;
|
|
1275
|
+
}
|
|
1276
|
+
function resolveKoraVersion() {
|
|
1277
|
+
try {
|
|
1278
|
+
let dir = (0, import_node_path5.dirname)((0, import_node_url2.fileURLToPath)(import_meta2.url));
|
|
1279
|
+
for (let i = 0; i < 5; i++) {
|
|
1280
|
+
const pkgPath = (0, import_node_path5.resolve)(dir, "package.json");
|
|
1281
|
+
if ((0, import_node_fs2.existsSync)(pkgPath)) {
|
|
1282
|
+
const pkg = JSON.parse((0, import_node_fs2.readFileSync)(pkgPath, "utf-8"));
|
|
1283
|
+
if (pkg.name === "@korajs/cli") {
|
|
1284
|
+
if (pkg.version === "0.0.0") return "latest";
|
|
1285
|
+
const parts = pkg.version.split(".");
|
|
1286
|
+
return `^${parts[0]}.${parts[1]}.0`;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
dir = (0, import_node_path5.dirname)(dir);
|
|
1290
|
+
}
|
|
1291
|
+
return "latest";
|
|
1292
|
+
} catch {
|
|
1293
|
+
return "latest";
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// src/commands/deploy/deploy-command.ts
|
|
1298
|
+
var import_node_path14 = require("path");
|
|
1299
|
+
var import_citty2 = require("citty");
|
|
1300
|
+
|
|
1301
|
+
// src/commands/deploy/adapters/adapter.ts
|
|
1302
|
+
var DEPLOY_PLATFORMS = ["fly", "railway", "render", "docker", "kora-cloud"];
|
|
1303
|
+
function isDeployPlatform(value) {
|
|
1304
|
+
return DEPLOY_PLATFORMS.includes(value);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// src/commands/deploy/adapters/fly-adapter.ts
|
|
1308
|
+
var import_node_child_process4 = require("child_process");
|
|
1309
|
+
var import_node_path9 = require("path");
|
|
1310
|
+
|
|
1311
|
+
// src/commands/deploy/artifacts/fly-toml-generator.ts
|
|
1312
|
+
var import_promises5 = require("fs/promises");
|
|
1313
|
+
var import_node_path6 = require("path");
|
|
1314
|
+
var GENERATED_HEADER = "# Generated by kora deploy \u2014 do not edit manually";
|
|
1315
|
+
function generateFlyToml(options) {
|
|
1316
|
+
const internalPort = options.internalPort ?? 3e3;
|
|
1317
|
+
return [
|
|
1318
|
+
GENERATED_HEADER,
|
|
1319
|
+
"",
|
|
1320
|
+
`app = "${options.appName}"`,
|
|
1321
|
+
`primary_region = "${options.region}"`,
|
|
1322
|
+
"",
|
|
1323
|
+
"[build]",
|
|
1324
|
+
` dockerfile = "Dockerfile"`,
|
|
1325
|
+
"",
|
|
1326
|
+
"[http_service]",
|
|
1327
|
+
` internal_port = ${String(internalPort)}`,
|
|
1328
|
+
" force_https = true",
|
|
1329
|
+
' auto_stop_machines = "stop"',
|
|
1330
|
+
" auto_start_machines = true",
|
|
1331
|
+
" min_machines_running = 0",
|
|
1332
|
+
"",
|
|
1333
|
+
" [[http_service.checks]]",
|
|
1334
|
+
' grace_period = "10s"',
|
|
1335
|
+
' interval = "30s"',
|
|
1336
|
+
' method = "GET"',
|
|
1337
|
+
' timeout = "5s"',
|
|
1338
|
+
' path = "/"',
|
|
1339
|
+
""
|
|
1340
|
+
].join("\n");
|
|
1341
|
+
}
|
|
1342
|
+
async function writeFlyTomlArtifact(deployDirectory, options) {
|
|
1343
|
+
await (0, import_promises5.mkdir)(deployDirectory, { recursive: true });
|
|
1344
|
+
const filePath = (0, import_node_path6.join)(deployDirectory, "fly.toml");
|
|
1345
|
+
await (0, import_promises5.writeFile)(filePath, generateFlyToml(options), "utf-8");
|
|
1346
|
+
return filePath;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/commands/deploy/builder/client-builder.ts
|
|
1350
|
+
var import_node_child_process3 = require("child_process");
|
|
1351
|
+
var import_promises6 = require("fs/promises");
|
|
1352
|
+
var import_node_fs3 = require("fs");
|
|
1353
|
+
var import_node_path7 = require("path");
|
|
1354
|
+
async function buildClient(options) {
|
|
1355
|
+
const viteEntryPoint = await resolveProjectBinaryEntryPoint(options.projectRoot, "vite", "vite");
|
|
1356
|
+
if (!viteEntryPoint) {
|
|
1357
|
+
throw new Error(
|
|
1358
|
+
`Could not find local Vite binary in ${options.projectRoot}. Install dependencies before deploying.`
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
const args = [
|
|
1362
|
+
viteEntryPoint,
|
|
1363
|
+
"build",
|
|
1364
|
+
"--outDir",
|
|
1365
|
+
options.outDir,
|
|
1366
|
+
"--mode",
|
|
1367
|
+
options.mode ?? "production"
|
|
1368
|
+
];
|
|
1369
|
+
await runProcess(process.execPath, args, options.projectRoot);
|
|
1370
|
+
await patchSqliteWasmAssets(options.projectRoot, options.outDir);
|
|
1371
|
+
return { outDir: options.outDir };
|
|
1372
|
+
}
|
|
1373
|
+
async function patchSqliteWasmAssets(projectRoot, outDir) {
|
|
1374
|
+
const assetsDir = (0, import_node_path7.join)(outDir, "assets");
|
|
1375
|
+
if (!(0, import_node_fs3.existsSync)(assetsDir)) return;
|
|
1376
|
+
const files = await (0, import_promises6.readdir)(assetsDir);
|
|
1377
|
+
const hashedWasm = files.find((f) => /^sqlite3-.+\.wasm$/.test(f));
|
|
1378
|
+
if (hashedWasm && !files.includes("sqlite3.wasm")) {
|
|
1379
|
+
await (0, import_promises6.copyFile)((0, import_node_path7.join)(assetsDir, hashedWasm), (0, import_node_path7.join)(assetsDir, "sqlite3.wasm"));
|
|
1380
|
+
}
|
|
1381
|
+
if (!files.includes("sqlite3-opfs-async-proxy.js")) {
|
|
1382
|
+
const proxyFile = (0, import_node_path7.resolve)(
|
|
1383
|
+
projectRoot,
|
|
1384
|
+
"node_modules",
|
|
1385
|
+
"@sqlite.org",
|
|
1386
|
+
"sqlite-wasm",
|
|
1387
|
+
"sqlite-wasm",
|
|
1388
|
+
"jswasm",
|
|
1389
|
+
"sqlite3-opfs-async-proxy.js"
|
|
1390
|
+
);
|
|
1391
|
+
if ((0, import_node_fs3.existsSync)(proxyFile)) {
|
|
1392
|
+
await (0, import_promises6.copyFile)(proxyFile, (0, import_node_path7.join)(assetsDir, "sqlite3-opfs-async-proxy.js"));
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
async function runProcess(command, args, cwd) {
|
|
1397
|
+
await new Promise((resolve9, reject) => {
|
|
1398
|
+
const child = (0, import_node_child_process3.spawn)(command, args, {
|
|
1399
|
+
cwd,
|
|
1400
|
+
stdio: "inherit",
|
|
1401
|
+
env: process.env
|
|
1402
|
+
});
|
|
1403
|
+
child.on("error", (error) => {
|
|
1404
|
+
reject(error);
|
|
1405
|
+
});
|
|
1406
|
+
child.on("exit", (code) => {
|
|
1407
|
+
if (code === 0) {
|
|
1408
|
+
resolve9();
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
reject(new Error(`Client build failed with exit code ${String(code ?? "unknown")}.`));
|
|
1412
|
+
});
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// src/commands/deploy/builder/server-bundler.ts
|
|
1417
|
+
var import_promises7 = require("fs/promises");
|
|
1418
|
+
var import_promises8 = require("fs/promises");
|
|
1419
|
+
var import_node_path8 = require("path");
|
|
1420
|
+
var import_esbuild = require("esbuild");
|
|
1421
|
+
var DEFAULT_ENTRY_CANDIDATES = [
|
|
1422
|
+
"server.ts",
|
|
1423
|
+
"server.js",
|
|
1424
|
+
"src/server.ts",
|
|
1425
|
+
"src/server.js"
|
|
1426
|
+
];
|
|
1427
|
+
async function bundleServer(options) {
|
|
1428
|
+
const candidates = options.entryFileCandidates ?? DEFAULT_ENTRY_CANDIDATES;
|
|
1429
|
+
const entryFilePath = await resolveServerEntry(options.projectRoot, candidates);
|
|
1430
|
+
if (!entryFilePath) {
|
|
1431
|
+
throw new Error(
|
|
1432
|
+
`Could not find a server entry file in ${options.projectRoot}. Looked for: ${candidates.join(", ")}`
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
await (0, import_promises7.mkdir)(options.deployDirectory, { recursive: true });
|
|
1436
|
+
const outputFilePath = (0, import_node_path8.join)(options.deployDirectory, "server-bundled.js");
|
|
1437
|
+
await (0, import_esbuild.build)({
|
|
1438
|
+
entryPoints: [entryFilePath],
|
|
1439
|
+
outfile: outputFilePath,
|
|
1440
|
+
bundle: true,
|
|
1441
|
+
platform: "node",
|
|
1442
|
+
format: "esm",
|
|
1443
|
+
target: ["node20"],
|
|
1444
|
+
sourcemap: false,
|
|
1445
|
+
logLevel: "silent",
|
|
1446
|
+
external: ["better-sqlite3"],
|
|
1447
|
+
banner: {
|
|
1448
|
+
js: "import { createRequire as __createRequire } from 'module'; const require = __createRequire(import.meta.url);"
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
return {
|
|
1452
|
+
entryFilePath,
|
|
1453
|
+
outputFilePath
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
async function resolveServerEntry(projectRoot, candidates) {
|
|
1457
|
+
for (const candidate of candidates) {
|
|
1458
|
+
const fullPath = (0, import_node_path8.join)(projectRoot, candidate);
|
|
1459
|
+
try {
|
|
1460
|
+
await (0, import_promises8.access)(fullPath);
|
|
1461
|
+
return fullPath;
|
|
1462
|
+
} catch {
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
return null;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// src/commands/deploy/adapters/fly-adapter.ts
|
|
1469
|
+
var FlyAdapter = class {
|
|
1470
|
+
name = "fly";
|
|
1471
|
+
logger = createLogger();
|
|
1472
|
+
runner;
|
|
1473
|
+
commandCandidates;
|
|
1474
|
+
flyCommand = null;
|
|
1475
|
+
currentContext;
|
|
1476
|
+
lastDeploymentId = null;
|
|
1477
|
+
constructor(options = {}) {
|
|
1478
|
+
this.runner = options.runner ?? new NodeFlyCommandRunner();
|
|
1479
|
+
this.commandCandidates = options.commandCandidates ?? DEFAULT_FLY_COMMAND_CANDIDATES;
|
|
1480
|
+
this.currentContext = options.context ?? null;
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Seeds runtime context for non-provisioning commands.
|
|
1484
|
+
*/
|
|
1485
|
+
setContext(context) {
|
|
1486
|
+
this.currentContext = context;
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Checks whether a Fly CLI executable can be resolved.
|
|
1490
|
+
*/
|
|
1491
|
+
async detect() {
|
|
1492
|
+
const command = await this.tryResolveFlyCommand(process.cwd());
|
|
1493
|
+
return command !== null;
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Ensures Fly CLI is available before deployment.
|
|
1497
|
+
*/
|
|
1498
|
+
async install() {
|
|
1499
|
+
const available = await this.detect();
|
|
1500
|
+
if (!available) {
|
|
1501
|
+
throw new Error(
|
|
1502
|
+
"Fly CLI is required but not installed. Install from https://fly.io/docs/hands-on/install-flyctl/."
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Performs Fly authentication check/login.
|
|
1508
|
+
*/
|
|
1509
|
+
async authenticate() {
|
|
1510
|
+
const projectRoot = this.currentContext?.projectRoot ?? process.cwd();
|
|
1511
|
+
const status = await this.runFlyCommand(["auth", "whoami", "--json"], projectRoot, false);
|
|
1512
|
+
if (status.exitCode === 0) return;
|
|
1513
|
+
const login = await this.runFlyCommand(["auth", "login"], projectRoot, true);
|
|
1514
|
+
if (login.exitCode !== 0) {
|
|
1515
|
+
throw new Error(`Fly authentication failed: ${normalizeError(login.stderr, login.stdout)}`);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Provisions app and optionally region-bound resources in Fly.
|
|
1520
|
+
*/
|
|
1521
|
+
async provision(config) {
|
|
1522
|
+
this.currentContext = {
|
|
1523
|
+
projectRoot: config.projectRoot,
|
|
1524
|
+
appName: config.appName,
|
|
1525
|
+
region: config.region ?? "iad"
|
|
1526
|
+
};
|
|
1527
|
+
const appCreateArgs = ["apps", "create", config.appName];
|
|
1528
|
+
const createApp = await this.runFlyCommand(appCreateArgs, config.projectRoot, false);
|
|
1529
|
+
if (createApp.exitCode !== 0 && !isAlreadyExistsResponse(createApp.stderr, createApp.stdout)) {
|
|
1530
|
+
throw new Error(
|
|
1531
|
+
`Fly app provisioning failed for "${config.appName}": ${normalizeError(createApp.stderr, createApp.stdout)}`
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
const secrets = [];
|
|
1535
|
+
const portSecret = await this.runFlyCommand(
|
|
1536
|
+
["secrets", "set", "PORT=3000", "--app", config.appName],
|
|
1537
|
+
config.projectRoot,
|
|
1538
|
+
false
|
|
1539
|
+
);
|
|
1540
|
+
if (portSecret.exitCode === 0) {
|
|
1541
|
+
secrets.push("PORT");
|
|
1542
|
+
}
|
|
1543
|
+
return {
|
|
1544
|
+
applicationId: config.appName,
|
|
1545
|
+
databaseId: null,
|
|
1546
|
+
secretsSet: secrets
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* Builds local artifacts consumed by Fly deploy.
|
|
1551
|
+
*/
|
|
1552
|
+
async build(config) {
|
|
1553
|
+
this.currentContext = {
|
|
1554
|
+
projectRoot: config.projectRoot,
|
|
1555
|
+
appName: config.appName,
|
|
1556
|
+
region: config.region
|
|
1557
|
+
};
|
|
1558
|
+
const deployDirectory = (0, import_node_path9.join)(config.projectRoot, ".kora", "deploy");
|
|
1559
|
+
await writeFlyTomlArtifact(deployDirectory, {
|
|
1560
|
+
appName: config.appName,
|
|
1561
|
+
region: config.region ?? "iad"
|
|
1562
|
+
});
|
|
1563
|
+
await bundleServer({
|
|
1564
|
+
projectRoot: config.projectRoot,
|
|
1565
|
+
deployDirectory
|
|
1566
|
+
});
|
|
1567
|
+
const client = await buildClient({
|
|
1568
|
+
projectRoot: config.projectRoot,
|
|
1569
|
+
outDir: (0, import_node_path9.join)(deployDirectory, "dist"),
|
|
1570
|
+
mode: "production"
|
|
1571
|
+
});
|
|
1572
|
+
return {
|
|
1573
|
+
clientDirectory: client.outDir,
|
|
1574
|
+
serverBundlePath: (0, import_node_path9.join)(deployDirectory, "server-bundled.js"),
|
|
1575
|
+
deployDirectory
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Deploys generated artifacts to Fly and returns live endpoints.
|
|
1580
|
+
*/
|
|
1581
|
+
async deploy(artifacts) {
|
|
1582
|
+
const context = this.requireContext();
|
|
1583
|
+
const deploy = await this.runFlyCommand(
|
|
1584
|
+
["deploy", "--config", "fly.toml", "--app", context.appName],
|
|
1585
|
+
artifacts.deployDirectory,
|
|
1586
|
+
true
|
|
1587
|
+
);
|
|
1588
|
+
if (deploy.exitCode !== 0) {
|
|
1589
|
+
throw new Error(`Fly deployment failed: ${normalizeError(deploy.stderr, deploy.stdout)}`);
|
|
1590
|
+
}
|
|
1591
|
+
const info = await this.runFlyCommand(
|
|
1592
|
+
["status", "--app", context.appName, "--json"],
|
|
1593
|
+
context.projectRoot,
|
|
1594
|
+
false
|
|
1595
|
+
);
|
|
1596
|
+
const hostname = parseFlyHostname(info.stdout) ?? `${context.appName}.fly.dev`;
|
|
1597
|
+
const deploymentId = parseFlyDeploymentId(info.stdout) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1598
|
+
this.lastDeploymentId = deploymentId;
|
|
1599
|
+
return {
|
|
1600
|
+
deploymentId,
|
|
1601
|
+
liveUrl: `https://${hostname}`,
|
|
1602
|
+
syncUrl: `wss://${hostname}/kora-sync`
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Rolls back to a deployment version.
|
|
1607
|
+
*/
|
|
1608
|
+
async rollback(deploymentId) {
|
|
1609
|
+
const context = this.requireContext();
|
|
1610
|
+
const rollback = await this.runFlyCommand(
|
|
1611
|
+
["releases", "revert", deploymentId, "--app", context.appName],
|
|
1612
|
+
context.projectRoot,
|
|
1613
|
+
true
|
|
1614
|
+
);
|
|
1615
|
+
if (rollback.exitCode !== 0) {
|
|
1616
|
+
throw new Error(`Fly rollback failed: ${normalizeError(rollback.stderr, rollback.stdout)}`);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Returns deployment logs as an async iterable.
|
|
1621
|
+
*/
|
|
1622
|
+
async *logs(options) {
|
|
1623
|
+
const context = this.requireContext();
|
|
1624
|
+
const args = ["logs", "--app", context.appName, "--no-tail"];
|
|
1625
|
+
if (typeof options.since === "string" && options.since.length > 0) {
|
|
1626
|
+
args.push("--region", options.since);
|
|
1627
|
+
}
|
|
1628
|
+
const result = await this.runFlyCommand(args, context.projectRoot, false);
|
|
1629
|
+
if (result.exitCode !== 0) {
|
|
1630
|
+
throw new Error(`Fly logs failed: ${normalizeError(result.stderr, result.stdout)}`);
|
|
1631
|
+
}
|
|
1632
|
+
const lines = result.stdout.split(/\r?\n/).filter((line) => line.length > 0);
|
|
1633
|
+
for (const line of lines) {
|
|
1634
|
+
yield {
|
|
1635
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1636
|
+
level: inferLogLevel(line),
|
|
1637
|
+
message: line
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Reads current deployment status from Fly.
|
|
1643
|
+
*/
|
|
1644
|
+
async status() {
|
|
1645
|
+
const context = this.requireContext();
|
|
1646
|
+
const status = await this.runFlyCommand(
|
|
1647
|
+
["status", "--app", context.appName, "--json"],
|
|
1648
|
+
context.projectRoot,
|
|
1649
|
+
false
|
|
1650
|
+
);
|
|
1651
|
+
if (status.exitCode !== 0) {
|
|
1652
|
+
return {
|
|
1653
|
+
state: "failed",
|
|
1654
|
+
message: normalizeError(status.stderr, status.stdout)
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
const hostname = parseFlyHostname(status.stdout);
|
|
1658
|
+
return {
|
|
1659
|
+
state: "healthy",
|
|
1660
|
+
message: "Fly deployment is healthy.",
|
|
1661
|
+
liveUrl: hostname ? `https://${hostname}` : void 0
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
async runFlyCommand(flyArgs, cwd, inheritOutput) {
|
|
1665
|
+
const flyBinary = await this.resolveFlyCommand(cwd);
|
|
1666
|
+
if (inheritOutput) {
|
|
1667
|
+
this.logger.step(`fly ${flyArgs.join(" ")}`);
|
|
1668
|
+
}
|
|
1669
|
+
return await this.runner.run(flyBinary, flyArgs, cwd);
|
|
1670
|
+
}
|
|
1671
|
+
requireContext() {
|
|
1672
|
+
if (!this.currentContext) {
|
|
1673
|
+
throw new Error("Fly adapter context is not initialized. Run provision() first.");
|
|
1674
|
+
}
|
|
1675
|
+
return this.currentContext;
|
|
1676
|
+
}
|
|
1677
|
+
async resolveFlyCommand(cwd) {
|
|
1678
|
+
if (this.flyCommand) {
|
|
1679
|
+
return this.flyCommand;
|
|
1680
|
+
}
|
|
1681
|
+
const resolved = await this.tryResolveFlyCommand(cwd);
|
|
1682
|
+
if (!resolved) {
|
|
1683
|
+
throw new Error("Could not resolve a usable Fly CLI command (tried flyctl, fly).");
|
|
1684
|
+
}
|
|
1685
|
+
this.flyCommand = resolved;
|
|
1686
|
+
return resolved;
|
|
1687
|
+
}
|
|
1688
|
+
async tryResolveFlyCommand(cwd) {
|
|
1689
|
+
for (const command of this.commandCandidates) {
|
|
1690
|
+
const versionCheck = await this.runner.run(command, ["version"], cwd);
|
|
1691
|
+
if (versionCheck.exitCode === 0) {
|
|
1692
|
+
return command;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
return null;
|
|
1696
|
+
}
|
|
1697
|
+
};
|
|
1698
|
+
var NodeFlyCommandRunner = class {
|
|
1699
|
+
async run(command, args, cwd) {
|
|
1700
|
+
return await new Promise((resolve9) => {
|
|
1701
|
+
const child = (0, import_node_child_process4.spawn)(command, args, {
|
|
1702
|
+
cwd,
|
|
1703
|
+
env: process.env,
|
|
1704
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1705
|
+
});
|
|
1706
|
+
let stdout = "";
|
|
1707
|
+
let stderr = "";
|
|
1708
|
+
child.stdout?.on("data", (chunk) => {
|
|
1709
|
+
stdout += chunk.toString("utf-8");
|
|
1710
|
+
});
|
|
1711
|
+
child.stderr?.on("data", (chunk) => {
|
|
1712
|
+
stderr += chunk.toString("utf-8");
|
|
1713
|
+
});
|
|
1714
|
+
child.on("error", (error) => {
|
|
1715
|
+
resolve9({
|
|
1716
|
+
exitCode: 1,
|
|
1717
|
+
stdout,
|
|
1718
|
+
stderr: `${stderr}
|
|
1719
|
+
${error.message}`
|
|
1720
|
+
});
|
|
1721
|
+
});
|
|
1722
|
+
child.on("exit", (code) => {
|
|
1723
|
+
resolve9({
|
|
1724
|
+
exitCode: code ?? 1,
|
|
1725
|
+
stdout: stdout.trim(),
|
|
1726
|
+
stderr: stderr.trim()
|
|
1727
|
+
});
|
|
1728
|
+
});
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
};
|
|
1732
|
+
var DEFAULT_FLY_COMMAND_CANDIDATES = ["flyctl", "fly"];
|
|
1733
|
+
function parseFlyHostname(rawJson) {
|
|
1734
|
+
const parsed = parseJsonRecord(rawJson);
|
|
1735
|
+
if (!parsed) return null;
|
|
1736
|
+
const hostname = parsed.Hostname;
|
|
1737
|
+
if (typeof hostname === "string" && hostname.length > 0) {
|
|
1738
|
+
return hostname;
|
|
1739
|
+
}
|
|
1740
|
+
const hostnames = parsed.Hostnames;
|
|
1741
|
+
if (Array.isArray(hostnames)) {
|
|
1742
|
+
const first = hostnames.find((item) => typeof item === "string");
|
|
1743
|
+
if (typeof first === "string" && first.length > 0) {
|
|
1744
|
+
return first;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
return null;
|
|
1748
|
+
}
|
|
1749
|
+
function parseFlyDeploymentId(rawJson) {
|
|
1750
|
+
const parsed = parseJsonRecord(rawJson);
|
|
1751
|
+
if (!parsed) return null;
|
|
1752
|
+
const deploymentId = parsed.DeploymentID;
|
|
1753
|
+
if (typeof deploymentId === "string" && deploymentId.length > 0) {
|
|
1754
|
+
return deploymentId;
|
|
1755
|
+
}
|
|
1756
|
+
const latestDeployment = parsed.LatestDeployment;
|
|
1757
|
+
if (typeof latestDeployment === "object" && latestDeployment !== null) {
|
|
1758
|
+
const id = latestDeployment.ID;
|
|
1759
|
+
if (typeof id === "string" && id.length > 0) {
|
|
1760
|
+
return id;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
return null;
|
|
1764
|
+
}
|
|
1765
|
+
function parseJsonRecord(value) {
|
|
1766
|
+
try {
|
|
1767
|
+
const parsed = JSON.parse(value);
|
|
1768
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
1769
|
+
return parsed;
|
|
1770
|
+
}
|
|
1771
|
+
return null;
|
|
1772
|
+
} catch {
|
|
1773
|
+
return null;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
function inferLogLevel(line) {
|
|
1777
|
+
const normalized = line.toLowerCase();
|
|
1778
|
+
if (normalized.includes("error")) return "error";
|
|
1779
|
+
if (normalized.includes("warn")) return "warn";
|
|
1780
|
+
if (normalized.includes("debug")) return "debug";
|
|
1781
|
+
return "info";
|
|
1782
|
+
}
|
|
1783
|
+
function normalizeError(stderr, stdout) {
|
|
1784
|
+
if (stderr.length > 0) return stderr;
|
|
1785
|
+
if (stdout.length > 0) return stdout;
|
|
1786
|
+
return "unknown fly CLI error";
|
|
1787
|
+
}
|
|
1788
|
+
function isAlreadyExistsResponse(stderr, stdout) {
|
|
1789
|
+
const text = `${stderr}
|
|
1790
|
+
${stdout}`.toLowerCase();
|
|
1791
|
+
return text.includes("already exists") || text.includes("already been taken");
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// src/commands/deploy/adapters/railway-adapter.ts
|
|
1795
|
+
var import_node_child_process5 = require("child_process");
|
|
1796
|
+
var import_node_path11 = require("path");
|
|
1797
|
+
|
|
1798
|
+
// src/commands/deploy/artifacts/railway-json-generator.ts
|
|
1799
|
+
var import_promises9 = require("fs/promises");
|
|
1800
|
+
var import_node_path10 = require("path");
|
|
1801
|
+
var GENERATED_HEADER2 = "Generated by kora deploy - do not edit manually.";
|
|
1802
|
+
function generateRailwayJson(options) {
|
|
1803
|
+
const file = {
|
|
1804
|
+
$schema: "https://railway.app/railway.schema.json",
|
|
1805
|
+
build: {
|
|
1806
|
+
builder: "DOCKERFILE",
|
|
1807
|
+
dockerfilePath: "Dockerfile"
|
|
1808
|
+
},
|
|
1809
|
+
deploy: {
|
|
1810
|
+
startCommand: options.startCommand ?? "node ./server-bundled.js",
|
|
1811
|
+
healthcheckPath: options.healthcheckPath ?? "/health",
|
|
1812
|
+
restartPolicyType: "on_failure",
|
|
1813
|
+
restartPolicyMaxRetries: 3
|
|
1814
|
+
},
|
|
1815
|
+
metadata: {
|
|
1816
|
+
generatedBy: GENERATED_HEADER2,
|
|
1817
|
+
appName: options.appName,
|
|
1818
|
+
region: options.region
|
|
1819
|
+
}
|
|
1820
|
+
};
|
|
1821
|
+
return `${JSON.stringify(file, null, 2)}
|
|
1822
|
+
`;
|
|
1823
|
+
}
|
|
1824
|
+
async function writeRailwayJsonArtifact(deployDirectory, options) {
|
|
1825
|
+
await (0, import_promises9.mkdir)(deployDirectory, { recursive: true });
|
|
1826
|
+
const filePath = (0, import_node_path10.join)(deployDirectory, "railway.json");
|
|
1827
|
+
await (0, import_promises9.writeFile)(filePath, generateRailwayJson(options), "utf-8");
|
|
1828
|
+
return filePath;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// src/commands/deploy/adapters/railway-adapter.ts
|
|
1832
|
+
var RailwayAdapter = class {
|
|
1833
|
+
name = "railway";
|
|
1834
|
+
logger = createLogger();
|
|
1835
|
+
runner;
|
|
1836
|
+
commandCandidates;
|
|
1837
|
+
railwayCommand = null;
|
|
1838
|
+
currentContext;
|
|
1839
|
+
lastDeploymentId = null;
|
|
1840
|
+
constructor(options = {}) {
|
|
1841
|
+
this.runner = options.runner ?? new NodeRailwayCommandRunner();
|
|
1842
|
+
this.commandCandidates = options.commandCandidates ?? DEFAULT_RAILWAY_COMMAND_CANDIDATES;
|
|
1843
|
+
this.currentContext = options.context ?? null;
|
|
1844
|
+
}
|
|
1845
|
+
setContext(context) {
|
|
1846
|
+
this.currentContext = context;
|
|
1847
|
+
}
|
|
1848
|
+
async detect() {
|
|
1849
|
+
const command = await this.tryResolveRailwayCommand(process.cwd());
|
|
1850
|
+
return command !== null;
|
|
1851
|
+
}
|
|
1852
|
+
async install() {
|
|
1853
|
+
const available = await this.detect();
|
|
1854
|
+
if (!available) {
|
|
1855
|
+
throw new Error(
|
|
1856
|
+
"Railway CLI is required but not installed. Install with `npm i -g @railway/cli` or see https://docs.railway.com/guides/cli."
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
async authenticate() {
|
|
1861
|
+
const projectRoot = this.currentContext?.projectRoot ?? process.cwd();
|
|
1862
|
+
const whoami = await this.runRailwayCommand(["whoami"], projectRoot, false);
|
|
1863
|
+
if (whoami.exitCode === 0) return;
|
|
1864
|
+
const login = await this.runRailwayCommand(["login"], projectRoot, true);
|
|
1865
|
+
if (login.exitCode !== 0) {
|
|
1866
|
+
throw new Error(
|
|
1867
|
+
`Railway authentication failed: ${normalizeError2(login.stderr, login.stdout)}`
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
async provision(config) {
|
|
1872
|
+
this.currentContext = {
|
|
1873
|
+
projectRoot: config.projectRoot,
|
|
1874
|
+
appName: config.appName,
|
|
1875
|
+
region: config.region
|
|
1876
|
+
};
|
|
1877
|
+
const init = await this.runRailwayCommand(
|
|
1878
|
+
["init", "--name", config.appName, "--confirm"],
|
|
1879
|
+
config.projectRoot,
|
|
1880
|
+
false
|
|
1881
|
+
);
|
|
1882
|
+
if (init.exitCode !== 0 && !isAlreadyExistsResponse2(init.stderr, init.stdout)) {
|
|
1883
|
+
throw new Error(
|
|
1884
|
+
`Railway project provisioning failed for "${config.appName}": ${normalizeError2(init.stderr, init.stdout)}`
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
const link = await this.runRailwayCommand(
|
|
1888
|
+
["link", "--project", config.appName, "--environment", config.environment, "--yes"],
|
|
1889
|
+
config.projectRoot,
|
|
1890
|
+
false
|
|
1891
|
+
);
|
|
1892
|
+
if (link.exitCode !== 0 && !isAlreadyExistsResponse2(link.stderr, link.stdout)) {
|
|
1893
|
+
throw new Error(`Railway link failed: ${normalizeError2(link.stderr, link.stdout)}`);
|
|
1894
|
+
}
|
|
1895
|
+
const vars = [];
|
|
1896
|
+
const setPort = await this.runRailwayCommand(
|
|
1897
|
+
["variables", "set", "PORT=3000", "--yes"],
|
|
1898
|
+
config.projectRoot,
|
|
1899
|
+
false
|
|
1900
|
+
);
|
|
1901
|
+
if (setPort.exitCode === 0) {
|
|
1902
|
+
vars.push("PORT");
|
|
1903
|
+
}
|
|
1904
|
+
return {
|
|
1905
|
+
applicationId: config.appName,
|
|
1906
|
+
databaseId: null,
|
|
1907
|
+
secretsSet: vars
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
async build(config) {
|
|
1911
|
+
this.currentContext = {
|
|
1912
|
+
projectRoot: config.projectRoot,
|
|
1913
|
+
appName: config.appName,
|
|
1914
|
+
region: config.region
|
|
1915
|
+
};
|
|
1916
|
+
const deployDirectory = (0, import_node_path11.join)(config.projectRoot, ".kora", "deploy");
|
|
1917
|
+
await writeRailwayJsonArtifact(deployDirectory, {
|
|
1918
|
+
appName: config.appName,
|
|
1919
|
+
environment: config.environment
|
|
1920
|
+
});
|
|
1921
|
+
await bundleServer({
|
|
1922
|
+
projectRoot: config.projectRoot,
|
|
1923
|
+
deployDirectory
|
|
1924
|
+
});
|
|
1925
|
+
const client = await buildClient({
|
|
1926
|
+
projectRoot: config.projectRoot,
|
|
1927
|
+
outDir: (0, import_node_path11.join)(deployDirectory, "dist"),
|
|
1928
|
+
mode: "production"
|
|
1929
|
+
});
|
|
1930
|
+
return {
|
|
1931
|
+
clientDirectory: client.outDir,
|
|
1932
|
+
serverBundlePath: (0, import_node_path11.join)(deployDirectory, "server-bundled.js"),
|
|
1933
|
+
deployDirectory
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
async deploy(artifacts) {
|
|
1937
|
+
const context = this.requireContext();
|
|
1938
|
+
const up = await this.runRailwayCommand(["up", "--yes"], artifacts.deployDirectory, true);
|
|
1939
|
+
if (up.exitCode !== 0) {
|
|
1940
|
+
throw new Error(`Railway deployment failed: ${normalizeError2(up.stderr, up.stdout)}`);
|
|
1941
|
+
}
|
|
1942
|
+
const status = await this.runRailwayCommand(["status", "--json"], context.projectRoot, false);
|
|
1943
|
+
const deploymentId = parseRailwayDeploymentId(status.stdout) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1944
|
+
const liveUrl = parseRailwayUrl(status.stdout) ?? `https://${context.appName}.up.railway.app`;
|
|
1945
|
+
this.lastDeploymentId = deploymentId;
|
|
1946
|
+
return {
|
|
1947
|
+
deploymentId,
|
|
1948
|
+
liveUrl,
|
|
1949
|
+
syncUrl: toSyncUrl(liveUrl)
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
async rollback(deploymentId) {
|
|
1953
|
+
const context = this.requireContext();
|
|
1954
|
+
const rollback = await this.runRailwayCommand(
|
|
1955
|
+
["redeploy", "--deployment", deploymentId, "--yes"],
|
|
1956
|
+
context.projectRoot,
|
|
1957
|
+
true
|
|
1958
|
+
);
|
|
1959
|
+
if (rollback.exitCode !== 0) {
|
|
1960
|
+
throw new Error(
|
|
1961
|
+
`Railway rollback failed: ${normalizeError2(rollback.stderr, rollback.stdout)}`
|
|
1962
|
+
);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
async *logs(options) {
|
|
1966
|
+
const context = this.requireContext();
|
|
1967
|
+
const args = ["logs"];
|
|
1968
|
+
if (typeof options.tail === "number" && options.tail > 0) {
|
|
1969
|
+
args.push("--lines", String(options.tail));
|
|
1970
|
+
}
|
|
1971
|
+
const result = await this.runRailwayCommand(args, context.projectRoot, false);
|
|
1972
|
+
if (result.exitCode !== 0) {
|
|
1973
|
+
throw new Error(`Railway logs failed: ${normalizeError2(result.stderr, result.stdout)}`);
|
|
1974
|
+
}
|
|
1975
|
+
const lines = result.stdout.split(/\r?\n/).filter((line) => line.length > 0);
|
|
1976
|
+
for (const line of lines) {
|
|
1977
|
+
yield {
|
|
1978
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1979
|
+
level: inferLogLevel2(line),
|
|
1980
|
+
message: line
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
async status() {
|
|
1985
|
+
const context = this.requireContext();
|
|
1986
|
+
const status = await this.runRailwayCommand(["status", "--json"], context.projectRoot, false);
|
|
1987
|
+
if (status.exitCode !== 0) {
|
|
1988
|
+
return {
|
|
1989
|
+
state: "failed",
|
|
1990
|
+
message: normalizeError2(status.stderr, status.stdout)
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
const liveUrl = parseRailwayUrl(status.stdout);
|
|
1994
|
+
return {
|
|
1995
|
+
state: "healthy",
|
|
1996
|
+
message: "Railway deployment is healthy.",
|
|
1997
|
+
liveUrl: liveUrl ?? void 0
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
async runRailwayCommand(railwayArgs, cwd, inheritOutput) {
|
|
2001
|
+
const command = await this.resolveRailwayCommand(cwd);
|
|
2002
|
+
if (inheritOutput) {
|
|
2003
|
+
this.logger.step(`railway ${railwayArgs.join(" ")}`);
|
|
2004
|
+
}
|
|
2005
|
+
return await this.runner.run(command, railwayArgs, cwd);
|
|
2006
|
+
}
|
|
2007
|
+
requireContext() {
|
|
2008
|
+
if (!this.currentContext) {
|
|
2009
|
+
throw new Error("Railway adapter context is not initialized. Run provision() first.");
|
|
2010
|
+
}
|
|
2011
|
+
return this.currentContext;
|
|
2012
|
+
}
|
|
2013
|
+
async resolveRailwayCommand(cwd) {
|
|
2014
|
+
if (this.railwayCommand) {
|
|
2015
|
+
return this.railwayCommand;
|
|
2016
|
+
}
|
|
2017
|
+
const resolved = await this.tryResolveRailwayCommand(cwd);
|
|
2018
|
+
if (!resolved) {
|
|
2019
|
+
throw new Error("Could not resolve a usable Railway CLI command (tried railway).");
|
|
2020
|
+
}
|
|
2021
|
+
this.railwayCommand = resolved;
|
|
2022
|
+
return resolved;
|
|
2023
|
+
}
|
|
2024
|
+
async tryResolveRailwayCommand(cwd) {
|
|
2025
|
+
for (const command of this.commandCandidates) {
|
|
2026
|
+
const version = await this.runner.run(command, ["--version"], cwd);
|
|
2027
|
+
if (version.exitCode === 0) {
|
|
2028
|
+
return command;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
return null;
|
|
2032
|
+
}
|
|
2033
|
+
};
|
|
2034
|
+
var NodeRailwayCommandRunner = class {
|
|
2035
|
+
async run(command, args, cwd) {
|
|
2036
|
+
return await new Promise((resolve9) => {
|
|
2037
|
+
const child = (0, import_node_child_process5.spawn)(command, args, {
|
|
2038
|
+
cwd,
|
|
2039
|
+
env: process.env,
|
|
2040
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2041
|
+
});
|
|
2042
|
+
let stdout = "";
|
|
2043
|
+
let stderr = "";
|
|
2044
|
+
child.stdout?.on("data", (chunk) => {
|
|
2045
|
+
stdout += chunk.toString("utf-8");
|
|
2046
|
+
});
|
|
2047
|
+
child.stderr?.on("data", (chunk) => {
|
|
2048
|
+
stderr += chunk.toString("utf-8");
|
|
2049
|
+
});
|
|
2050
|
+
child.on("error", (error) => {
|
|
2051
|
+
resolve9({
|
|
2052
|
+
exitCode: 1,
|
|
2053
|
+
stdout,
|
|
2054
|
+
stderr: `${stderr}
|
|
2055
|
+
${error.message}`
|
|
2056
|
+
});
|
|
2057
|
+
});
|
|
2058
|
+
child.on("exit", (code) => {
|
|
2059
|
+
resolve9({
|
|
2060
|
+
exitCode: code ?? 1,
|
|
2061
|
+
stdout: stdout.trim(),
|
|
2062
|
+
stderr: stderr.trim()
|
|
2063
|
+
});
|
|
2064
|
+
});
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
};
|
|
2068
|
+
var DEFAULT_RAILWAY_COMMAND_CANDIDATES = ["railway"];
|
|
2069
|
+
function parseRailwayUrl(rawJson) {
|
|
2070
|
+
const record = parseJsonRecord2(rawJson);
|
|
2071
|
+
if (!record) return null;
|
|
2072
|
+
const url = record.url;
|
|
2073
|
+
if (typeof url === "string" && url.length > 0) {
|
|
2074
|
+
return ensureHttpsUrl(url);
|
|
2075
|
+
}
|
|
2076
|
+
const service = record.service;
|
|
2077
|
+
if (typeof service === "object" && service !== null) {
|
|
2078
|
+
const domain = service.domain;
|
|
2079
|
+
if (typeof domain === "string" && domain.length > 0) {
|
|
2080
|
+
return ensureHttpsUrl(domain);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
return null;
|
|
2084
|
+
}
|
|
2085
|
+
function parseRailwayDeploymentId(rawJson) {
|
|
2086
|
+
const record = parseJsonRecord2(rawJson);
|
|
2087
|
+
if (!record) return null;
|
|
2088
|
+
const deploymentId = record.deploymentId;
|
|
2089
|
+
if (typeof deploymentId === "string" && deploymentId.length > 0) {
|
|
2090
|
+
return deploymentId;
|
|
2091
|
+
}
|
|
2092
|
+
const deployment = record.deployment;
|
|
2093
|
+
if (typeof deployment === "object" && deployment !== null) {
|
|
2094
|
+
const id = deployment.id;
|
|
2095
|
+
if (typeof id === "string" && id.length > 0) {
|
|
2096
|
+
return id;
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
return null;
|
|
2100
|
+
}
|
|
2101
|
+
function parseJsonRecord2(value) {
|
|
2102
|
+
try {
|
|
2103
|
+
const parsed = JSON.parse(value);
|
|
2104
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
2105
|
+
return parsed;
|
|
2106
|
+
}
|
|
2107
|
+
return null;
|
|
2108
|
+
} catch {
|
|
2109
|
+
return null;
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
function toSyncUrl(liveUrl) {
|
|
2113
|
+
try {
|
|
2114
|
+
const url = new URL(liveUrl);
|
|
2115
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
2116
|
+
url.pathname = "/kora-sync";
|
|
2117
|
+
return url.toString().replace(/\/$/, "");
|
|
2118
|
+
} catch {
|
|
2119
|
+
return null;
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
function ensureHttpsUrl(value) {
|
|
2123
|
+
if (value.startsWith("http://") || value.startsWith("https://")) {
|
|
2124
|
+
return value;
|
|
2125
|
+
}
|
|
2126
|
+
return `https://${value}`;
|
|
2127
|
+
}
|
|
2128
|
+
function inferLogLevel2(line) {
|
|
2129
|
+
const normalized = line.toLowerCase();
|
|
2130
|
+
if (normalized.includes("error")) return "error";
|
|
2131
|
+
if (normalized.includes("warn")) return "warn";
|
|
2132
|
+
if (normalized.includes("debug")) return "debug";
|
|
2133
|
+
return "info";
|
|
2134
|
+
}
|
|
2135
|
+
function normalizeError2(stderr, stdout) {
|
|
2136
|
+
if (stderr.length > 0) return stderr;
|
|
2137
|
+
if (stdout.length > 0) return stdout;
|
|
2138
|
+
return "unknown railway CLI error";
|
|
2139
|
+
}
|
|
2140
|
+
function isAlreadyExistsResponse2(stderr, stdout) {
|
|
2141
|
+
const text = `${stderr}
|
|
2142
|
+
${stdout}`.toLowerCase();
|
|
2143
|
+
return text.includes("already exists");
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
// src/commands/deploy/adapters/stub-adapter.ts
|
|
2147
|
+
var StubDeployAdapter = class {
|
|
2148
|
+
name;
|
|
2149
|
+
contextLabel;
|
|
2150
|
+
constructor(platform) {
|
|
2151
|
+
this.name = platform;
|
|
2152
|
+
this.contextLabel = `Deploy adapter "${platform}" is not implemented yet.`;
|
|
2153
|
+
}
|
|
2154
|
+
setContext(_context) {
|
|
2155
|
+
}
|
|
2156
|
+
async detect() {
|
|
2157
|
+
return false;
|
|
2158
|
+
}
|
|
2159
|
+
async install() {
|
|
2160
|
+
throw this.notImplementedError();
|
|
2161
|
+
}
|
|
2162
|
+
async authenticate() {
|
|
2163
|
+
throw this.notImplementedError();
|
|
2164
|
+
}
|
|
2165
|
+
async provision(_config) {
|
|
2166
|
+
throw this.notImplementedError();
|
|
2167
|
+
}
|
|
2168
|
+
async build(_config) {
|
|
2169
|
+
throw this.notImplementedError();
|
|
2170
|
+
}
|
|
2171
|
+
async deploy(_artifacts) {
|
|
2172
|
+
throw this.notImplementedError();
|
|
2173
|
+
}
|
|
2174
|
+
async rollback(_deploymentId) {
|
|
2175
|
+
throw this.notImplementedError();
|
|
2176
|
+
}
|
|
2177
|
+
logs(_options) {
|
|
2178
|
+
throw this.notImplementedError();
|
|
2179
|
+
}
|
|
2180
|
+
async status() {
|
|
2181
|
+
return {
|
|
2182
|
+
state: "unknown",
|
|
2183
|
+
message: this.notImplementedMessage()
|
|
329
2184
|
};
|
|
330
|
-
|
|
331
|
-
|
|
2185
|
+
}
|
|
2186
|
+
notImplementedError() {
|
|
2187
|
+
return new Error(this.notImplementedMessage());
|
|
2188
|
+
}
|
|
2189
|
+
notImplementedMessage() {
|
|
2190
|
+
return `${this.contextLabel} Start with --platform fly for now.`;
|
|
2191
|
+
}
|
|
2192
|
+
};
|
|
2193
|
+
|
|
2194
|
+
// src/commands/deploy/adapters/factory.ts
|
|
2195
|
+
function createDeployAdapter(platform) {
|
|
2196
|
+
switch (platform) {
|
|
2197
|
+
case "fly":
|
|
2198
|
+
return new FlyAdapter();
|
|
2199
|
+
case "railway":
|
|
2200
|
+
return new RailwayAdapter();
|
|
2201
|
+
case "render":
|
|
2202
|
+
case "docker":
|
|
2203
|
+
case "kora-cloud":
|
|
2204
|
+
return new StubDeployAdapter(platform);
|
|
2205
|
+
default: {
|
|
2206
|
+
const exhaustiveCheck = platform;
|
|
2207
|
+
return exhaustiveCheck;
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
332
2210
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
2211
|
+
|
|
2212
|
+
// src/commands/deploy/artifacts/dockerfile-generator.ts
|
|
2213
|
+
var import_promises10 = require("fs/promises");
|
|
2214
|
+
var import_node_path12 = require("path");
|
|
2215
|
+
var GENERATED_HEADER3 = "# Generated by kora deploy \u2014 do not edit manually";
|
|
2216
|
+
function generateDockerfile(options = {}) {
|
|
2217
|
+
const nodeVersion = options.nodeVersion ?? "20-alpine";
|
|
2218
|
+
const port = options.port ?? 3e3;
|
|
2219
|
+
const clientDirectory = options.clientDirectory ?? "dist";
|
|
2220
|
+
const serverBundleFile = options.serverBundleFile ?? "server-bundled.js";
|
|
2221
|
+
const hasNativeDeps = options.nativeDependencies && Object.keys(options.nativeDependencies).length > 0;
|
|
2222
|
+
const lines = [
|
|
2223
|
+
GENERATED_HEADER3,
|
|
2224
|
+
"",
|
|
2225
|
+
`FROM node:${nodeVersion} AS runtime`,
|
|
2226
|
+
"WORKDIR /app",
|
|
2227
|
+
"ENV NODE_ENV=production",
|
|
2228
|
+
`ENV PORT=${String(port)}`,
|
|
2229
|
+
""
|
|
2230
|
+
];
|
|
2231
|
+
if (hasNativeDeps) {
|
|
2232
|
+
lines.push("# Install build tools for native modules");
|
|
2233
|
+
lines.push("RUN apk add --no-cache python3 make g++");
|
|
2234
|
+
lines.push("");
|
|
2235
|
+
lines.push("COPY package.json ./package.json");
|
|
2236
|
+
lines.push("RUN npm install --omit=dev");
|
|
2237
|
+
lines.push("");
|
|
2238
|
+
}
|
|
2239
|
+
lines.push(`COPY ${clientDirectory} ./dist`);
|
|
2240
|
+
lines.push(`COPY ${serverBundleFile} ./server-bundled.js`);
|
|
2241
|
+
lines.push("");
|
|
2242
|
+
lines.push(`EXPOSE ${String(port)}`);
|
|
2243
|
+
lines.push('CMD ["node", "./server-bundled.js"]');
|
|
2244
|
+
lines.push("");
|
|
2245
|
+
return lines.join("\n");
|
|
2246
|
+
}
|
|
2247
|
+
function generateDeployPackageJson(nativeDependencies) {
|
|
2248
|
+
const pkg = {
|
|
2249
|
+
name: "kora-deploy",
|
|
2250
|
+
version: "1.0.0",
|
|
2251
|
+
private: true,
|
|
2252
|
+
type: "module",
|
|
2253
|
+
dependencies: nativeDependencies
|
|
2254
|
+
};
|
|
2255
|
+
return JSON.stringify(pkg, null, 2) + "\n";
|
|
2256
|
+
}
|
|
2257
|
+
function generateDockerIgnore() {
|
|
2258
|
+
return [
|
|
2259
|
+
GENERATED_HEADER3,
|
|
2260
|
+
"",
|
|
2261
|
+
"node_modules",
|
|
2262
|
+
"npm-debug.log*",
|
|
2263
|
+
"pnpm-debug.log*",
|
|
2264
|
+
"yarn-error.log*",
|
|
2265
|
+
".env",
|
|
2266
|
+
".env.*",
|
|
2267
|
+
".git",
|
|
2268
|
+
".gitignore",
|
|
2269
|
+
"coverage",
|
|
2270
|
+
".turbo",
|
|
2271
|
+
"*.test.ts",
|
|
2272
|
+
"*.spec.ts",
|
|
2273
|
+
""
|
|
2274
|
+
].join("\n");
|
|
2275
|
+
}
|
|
2276
|
+
async function writeDockerfileArtifact(deployDirectory, options = {}) {
|
|
2277
|
+
await (0, import_promises10.mkdir)(deployDirectory, { recursive: true });
|
|
2278
|
+
const dockerfilePath = (0, import_node_path12.join)(deployDirectory, "Dockerfile");
|
|
2279
|
+
await (0, import_promises10.writeFile)(dockerfilePath, generateDockerfile(options), "utf-8");
|
|
2280
|
+
if (options.nativeDependencies && Object.keys(options.nativeDependencies).length > 0) {
|
|
2281
|
+
const pkgJsonPath = (0, import_node_path12.join)(deployDirectory, "package.json");
|
|
2282
|
+
await (0, import_promises10.writeFile)(pkgJsonPath, generateDeployPackageJson(options.nativeDependencies), "utf-8");
|
|
2283
|
+
}
|
|
2284
|
+
return dockerfilePath;
|
|
2285
|
+
}
|
|
2286
|
+
async function writeDockerIgnoreArtifact(deployDirectory) {
|
|
2287
|
+
await (0, import_promises10.mkdir)(deployDirectory, { recursive: true });
|
|
2288
|
+
const dockerIgnorePath = (0, import_node_path12.join)(deployDirectory, ".dockerignore");
|
|
2289
|
+
await (0, import_promises10.writeFile)(dockerIgnorePath, generateDockerIgnore(), "utf-8");
|
|
2290
|
+
return dockerIgnorePath;
|
|
338
2291
|
}
|
|
339
2292
|
|
|
340
|
-
// src/commands/
|
|
341
|
-
var
|
|
342
|
-
var
|
|
343
|
-
var
|
|
344
|
-
var
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
|
|
348
|
-
const value = context[key];
|
|
349
|
-
return value !== void 0 ? value : `{{${key}}}`;
|
|
350
|
-
});
|
|
2293
|
+
// src/commands/deploy/state/deploy-state.ts
|
|
2294
|
+
var import_promises11 = require("fs/promises");
|
|
2295
|
+
var import_node_path13 = require("path");
|
|
2296
|
+
var KORA_DEPLOY_DIRECTORY = (0, import_node_path13.join)(".kora", "deploy");
|
|
2297
|
+
var DEPLOY_STATE_FILENAME = "deploy.json";
|
|
2298
|
+
function resolveDeployDirectory(projectRoot) {
|
|
2299
|
+
return (0, import_node_path13.join)(projectRoot, KORA_DEPLOY_DIRECTORY);
|
|
351
2300
|
}
|
|
352
|
-
function
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
2301
|
+
function resolveDeployStatePath(projectRoot) {
|
|
2302
|
+
return (0, import_node_path13.join)(resolveDeployDirectory(projectRoot), DEPLOY_STATE_FILENAME);
|
|
2303
|
+
}
|
|
2304
|
+
async function readDeployState(projectRoot) {
|
|
2305
|
+
const statePath = resolveDeployStatePath(projectRoot);
|
|
2306
|
+
try {
|
|
2307
|
+
const source = await (0, import_promises11.readFile)(statePath, "utf-8");
|
|
2308
|
+
const parsed = JSON.parse(source);
|
|
2309
|
+
return parseDeployState(parsed);
|
|
2310
|
+
} catch (error) {
|
|
2311
|
+
const code = error.code;
|
|
2312
|
+
if (code === "ENOENT") return null;
|
|
2313
|
+
throw error;
|
|
359
2314
|
}
|
|
360
|
-
const currentDir = (0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
361
|
-
return (0, import_node_path2.resolve)(currentDir, "..", "templates", templateName);
|
|
362
2315
|
}
|
|
363
|
-
async function
|
|
364
|
-
const
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
2316
|
+
async function writeDeployState(projectRoot, input, now = /* @__PURE__ */ new Date()) {
|
|
2317
|
+
const timestamp = now.toISOString();
|
|
2318
|
+
const state = {
|
|
2319
|
+
platform: input.platform,
|
|
2320
|
+
appName: input.appName,
|
|
2321
|
+
region: input.region,
|
|
2322
|
+
projectRoot: input.projectRoot,
|
|
2323
|
+
liveUrl: input.liveUrl ?? null,
|
|
2324
|
+
syncUrl: input.syncUrl ?? null,
|
|
2325
|
+
databaseId: input.databaseId ?? null,
|
|
2326
|
+
lastDeploymentId: input.lastDeploymentId ?? null,
|
|
2327
|
+
createdAt: timestamp,
|
|
2328
|
+
updatedAt: timestamp
|
|
369
2329
|
};
|
|
370
|
-
await
|
|
2330
|
+
await persistDeployState(projectRoot, state);
|
|
2331
|
+
return state;
|
|
371
2332
|
}
|
|
372
|
-
async function
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const srcPath = (0, import_node_path2.join)(src, entry);
|
|
377
|
-
const srcStat = await (0, import_promises2.stat)(srcPath);
|
|
378
|
-
if (srcStat.isDirectory()) {
|
|
379
|
-
await copyDirectory(srcPath, (0, import_node_path2.join)(dest, entry), vars);
|
|
380
|
-
} else if (entry.endsWith(".hbs")) {
|
|
381
|
-
const content = await (0, import_promises2.readFile)(srcPath, "utf-8");
|
|
382
|
-
const outputName = entry.slice(0, -4);
|
|
383
|
-
await (0, import_promises2.writeFile)((0, import_node_path2.join)(dest, outputName), substituteVariables(content, vars), "utf-8");
|
|
384
|
-
} else {
|
|
385
|
-
await (0, import_promises2.copyFile)(srcPath, (0, import_node_path2.join)(dest, entry));
|
|
386
|
-
}
|
|
2333
|
+
async function updateDeployState(projectRoot, patch, now = /* @__PURE__ */ new Date()) {
|
|
2334
|
+
const existing = await readDeployState(projectRoot);
|
|
2335
|
+
if (!existing) {
|
|
2336
|
+
throw new Error("Cannot update deploy state because deploy.json does not exist yet.");
|
|
387
2337
|
}
|
|
2338
|
+
const nextState = {
|
|
2339
|
+
platform: patch.platform ?? existing.platform,
|
|
2340
|
+
appName: patch.appName ?? existing.appName,
|
|
2341
|
+
region: patch.region === void 0 ? existing.region : patch.region,
|
|
2342
|
+
projectRoot: patch.projectRoot ?? existing.projectRoot,
|
|
2343
|
+
liveUrl: patch.liveUrl === void 0 ? existing.liveUrl : patch.liveUrl,
|
|
2344
|
+
syncUrl: patch.syncUrl === void 0 ? existing.syncUrl : patch.syncUrl,
|
|
2345
|
+
databaseId: patch.databaseId === void 0 ? existing.databaseId : patch.databaseId,
|
|
2346
|
+
lastDeploymentId: patch.lastDeploymentId === void 0 ? existing.lastDeploymentId : patch.lastDeploymentId,
|
|
2347
|
+
createdAt: existing.createdAt,
|
|
2348
|
+
updatedAt: now.toISOString()
|
|
2349
|
+
};
|
|
2350
|
+
await persistDeployState(projectRoot, nextState);
|
|
2351
|
+
return nextState;
|
|
2352
|
+
}
|
|
2353
|
+
async function resetDeployState(projectRoot) {
|
|
2354
|
+
await (0, import_promises11.rm)(resolveDeployDirectory(projectRoot), { recursive: true, force: true });
|
|
2355
|
+
}
|
|
2356
|
+
async function persistDeployState(projectRoot, state) {
|
|
2357
|
+
const deployDir = resolveDeployDirectory(projectRoot);
|
|
2358
|
+
const statePath = resolveDeployStatePath(projectRoot);
|
|
2359
|
+
await (0, import_promises11.mkdir)(deployDir, { recursive: true });
|
|
2360
|
+
await (0, import_promises11.writeFile)(statePath, `${JSON.stringify(state, null, 2)}
|
|
2361
|
+
`, "utf-8");
|
|
2362
|
+
}
|
|
2363
|
+
function parseDeployState(value) {
|
|
2364
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2365
|
+
throw new Error("Invalid deploy state: expected an object.");
|
|
2366
|
+
}
|
|
2367
|
+
const record = value;
|
|
2368
|
+
const platform = readPlatform(record.platform);
|
|
2369
|
+
const appName = readString(record.appName, "appName");
|
|
2370
|
+
const region = readOptionalString(record.region, "region");
|
|
2371
|
+
const projectRoot = readString(record.projectRoot, "projectRoot");
|
|
2372
|
+
const liveUrl = readOptionalString(record.liveUrl, "liveUrl");
|
|
2373
|
+
const syncUrl = readOptionalString(record.syncUrl, "syncUrl");
|
|
2374
|
+
const databaseId = readOptionalString(record.databaseId, "databaseId");
|
|
2375
|
+
const lastDeploymentId = readOptionalString(record.lastDeploymentId, "lastDeploymentId");
|
|
2376
|
+
const createdAt = readString(record.createdAt, "createdAt");
|
|
2377
|
+
const updatedAt = readString(record.updatedAt, "updatedAt");
|
|
2378
|
+
return {
|
|
2379
|
+
platform,
|
|
2380
|
+
appName,
|
|
2381
|
+
region,
|
|
2382
|
+
projectRoot,
|
|
2383
|
+
liveUrl,
|
|
2384
|
+
syncUrl,
|
|
2385
|
+
databaseId,
|
|
2386
|
+
lastDeploymentId,
|
|
2387
|
+
createdAt,
|
|
2388
|
+
updatedAt
|
|
2389
|
+
};
|
|
2390
|
+
}
|
|
2391
|
+
function readPlatform(value) {
|
|
2392
|
+
if (typeof value !== "string" || !isDeployPlatform(value)) {
|
|
2393
|
+
throw new Error('Invalid deploy state: "platform" must be a supported platform.');
|
|
2394
|
+
}
|
|
2395
|
+
return value;
|
|
2396
|
+
}
|
|
2397
|
+
function readString(value, field) {
|
|
2398
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
2399
|
+
throw new Error(`Invalid deploy state: "${field}" must be a non-empty string.`);
|
|
2400
|
+
}
|
|
2401
|
+
return value;
|
|
2402
|
+
}
|
|
2403
|
+
function readOptionalString(value, field) {
|
|
2404
|
+
if (value === null) return null;
|
|
2405
|
+
if (typeof value === "string") return value;
|
|
2406
|
+
throw new Error(`Invalid deploy state: "${field}" must be a string or null.`);
|
|
388
2407
|
}
|
|
389
2408
|
|
|
390
|
-
// src/commands/
|
|
391
|
-
var
|
|
392
|
-
var createCommand = (0, import_citty.defineCommand)({
|
|
2409
|
+
// src/commands/deploy/deploy-command.ts
|
|
2410
|
+
var deployCommand = (0, import_citty2.defineCommand)({
|
|
393
2411
|
meta: {
|
|
394
|
-
name: "
|
|
395
|
-
description: "
|
|
2412
|
+
name: "deploy",
|
|
2413
|
+
description: "Deploy a Kora project to your selected platform"
|
|
396
2414
|
},
|
|
397
2415
|
args: {
|
|
398
|
-
|
|
399
|
-
type: "
|
|
400
|
-
description:
|
|
401
|
-
required: false
|
|
2416
|
+
platform: {
|
|
2417
|
+
type: "string",
|
|
2418
|
+
description: `Deployment platform (${DEPLOY_PLATFORMS.join(", ")})`
|
|
402
2419
|
},
|
|
403
|
-
|
|
2420
|
+
app: {
|
|
404
2421
|
type: "string",
|
|
405
|
-
description: "
|
|
2422
|
+
description: "Application name used by the target platform"
|
|
406
2423
|
},
|
|
407
|
-
|
|
2424
|
+
region: {
|
|
408
2425
|
type: "string",
|
|
409
|
-
description: "
|
|
2426
|
+
description: "Preferred deployment region (for example: iad, lhr, syd)"
|
|
410
2427
|
},
|
|
411
|
-
|
|
2428
|
+
prod: {
|
|
412
2429
|
type: "boolean",
|
|
413
|
-
description: "
|
|
2430
|
+
description: "Deploy to production environment",
|
|
414
2431
|
default: false
|
|
415
2432
|
},
|
|
416
|
-
|
|
2433
|
+
confirm: {
|
|
417
2434
|
type: "boolean",
|
|
418
|
-
|
|
419
|
-
description: "Accept all defaults (react-tailwind-sync + detected package manager)",
|
|
2435
|
+
description: "Non-interactive mode (fail fast on missing required data)",
|
|
420
2436
|
default: false
|
|
421
2437
|
},
|
|
422
|
-
|
|
423
|
-
type: "boolean",
|
|
424
|
-
description: "Use Tailwind CSS (use --no-tailwind to skip)"
|
|
425
|
-
},
|
|
426
|
-
sync: {
|
|
2438
|
+
reset: {
|
|
427
2439
|
type: "boolean",
|
|
428
|
-
description: "
|
|
2440
|
+
description: "Delete .kora/deploy state and generated artifacts",
|
|
2441
|
+
default: false
|
|
429
2442
|
}
|
|
430
2443
|
},
|
|
431
|
-
|
|
2444
|
+
subCommands: {
|
|
2445
|
+
status: (0, import_citty2.defineCommand)({
|
|
2446
|
+
meta: {
|
|
2447
|
+
name: "status",
|
|
2448
|
+
description: "Show current deployment status"
|
|
2449
|
+
},
|
|
2450
|
+
async run() {
|
|
2451
|
+
const logger = createLogger();
|
|
2452
|
+
const projectRoot = await requireProjectRoot();
|
|
2453
|
+
const state = await readDeployState(projectRoot);
|
|
2454
|
+
if (!state) {
|
|
2455
|
+
logger.warn("No deployment state found. Run `kora deploy` first.");
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
logger.banner();
|
|
2459
|
+
logger.info(`Platform: ${state.platform}`);
|
|
2460
|
+
logger.step(`App: ${state.appName}`);
|
|
2461
|
+
logger.step(`Region: ${state.region ?? "n/a"}`);
|
|
2462
|
+
logger.step(`Last deployment: ${state.lastDeploymentId ?? "n/a"}`);
|
|
2463
|
+
const adapter = createDeployAdapter(state.platform);
|
|
2464
|
+
configureAdapterContext(adapter, {
|
|
2465
|
+
projectRoot,
|
|
2466
|
+
appName: state.appName,
|
|
2467
|
+
region: state.region
|
|
2468
|
+
});
|
|
2469
|
+
const adapterStatus = await adapter.status();
|
|
2470
|
+
logger.step(`Status: ${adapterStatus.state}`);
|
|
2471
|
+
logger.step(`Message: ${adapterStatus.message}`);
|
|
2472
|
+
logger.step(`Live URL: ${adapterStatus.liveUrl ?? state.liveUrl ?? "n/a"}`);
|
|
2473
|
+
logger.step(`Sync URL: ${state.syncUrl ?? "n/a"}`);
|
|
2474
|
+
}
|
|
2475
|
+
}),
|
|
2476
|
+
rollback: (0, import_citty2.defineCommand)({
|
|
2477
|
+
meta: {
|
|
2478
|
+
name: "rollback",
|
|
2479
|
+
description: "Rollback the current deployment"
|
|
2480
|
+
},
|
|
2481
|
+
args: {
|
|
2482
|
+
id: {
|
|
2483
|
+
type: "positional",
|
|
2484
|
+
description: "Optional deployment identifier",
|
|
2485
|
+
required: false
|
|
2486
|
+
}
|
|
2487
|
+
},
|
|
2488
|
+
async run({ args }) {
|
|
2489
|
+
const logger = createLogger();
|
|
2490
|
+
const projectRoot = await requireProjectRoot();
|
|
2491
|
+
const state = await readDeployState(projectRoot);
|
|
2492
|
+
if (!state) {
|
|
2493
|
+
logger.warn("No deployment state found. Run `kora deploy` first.");
|
|
2494
|
+
return;
|
|
2495
|
+
}
|
|
2496
|
+
const adapter = createDeployAdapter(state.platform);
|
|
2497
|
+
configureAdapterContext(adapter, {
|
|
2498
|
+
projectRoot,
|
|
2499
|
+
appName: state.appName,
|
|
2500
|
+
region: state.region
|
|
2501
|
+
});
|
|
2502
|
+
const deploymentId = typeof args.id === "string" && args.id.length > 0 ? args.id : state.lastDeploymentId ?? "latest";
|
|
2503
|
+
await adapter.rollback(deploymentId);
|
|
2504
|
+
logger.success(`Rolled back ${state.platform} deployment to ${deploymentId}.`);
|
|
2505
|
+
}
|
|
2506
|
+
}),
|
|
2507
|
+
logs: (0, import_citty2.defineCommand)({
|
|
2508
|
+
meta: {
|
|
2509
|
+
name: "logs",
|
|
2510
|
+
description: "Read deployment logs"
|
|
2511
|
+
},
|
|
2512
|
+
async run() {
|
|
2513
|
+
const logger = createLogger();
|
|
2514
|
+
const projectRoot = await requireProjectRoot();
|
|
2515
|
+
const state = await readDeployState(projectRoot);
|
|
2516
|
+
if (!state) {
|
|
2517
|
+
logger.warn("No deployment state found. Run `kora deploy` first.");
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
const adapter = createDeployAdapter(state.platform);
|
|
2521
|
+
configureAdapterContext(adapter, {
|
|
2522
|
+
projectRoot,
|
|
2523
|
+
appName: state.appName,
|
|
2524
|
+
region: state.region
|
|
2525
|
+
});
|
|
2526
|
+
const logLines = adapter.logs({ tail: 200 });
|
|
2527
|
+
let hasLines = false;
|
|
2528
|
+
for await (const line of logLines) {
|
|
2529
|
+
hasLines = true;
|
|
2530
|
+
logger.step(`[${line.level}] ${line.message}`);
|
|
2531
|
+
}
|
|
2532
|
+
if (!hasLines) {
|
|
2533
|
+
logger.warn(`No logs returned from ${state.platform}.`);
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
})
|
|
2537
|
+
},
|
|
2538
|
+
async run({ args, rawArgs }) {
|
|
2539
|
+
const subCommandNames = ["status", "rollback", "logs"];
|
|
2540
|
+
if (rawArgs.some((arg) => subCommandNames.includes(arg))) {
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
432
2543
|
const logger = createLogger();
|
|
433
|
-
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
logger.
|
|
438
|
-
process.exitCode = 1;
|
|
2544
|
+
const prompts = createPromptClient();
|
|
2545
|
+
const projectRoot = await requireProjectRoot();
|
|
2546
|
+
if (args.reset === true) {
|
|
2547
|
+
await resetDeployState(projectRoot);
|
|
2548
|
+
logger.success("Reset .kora/deploy state.");
|
|
439
2549
|
return;
|
|
440
2550
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
2551
|
+
const existingState = await readDeployState(projectRoot);
|
|
2552
|
+
const confirmMode = args.confirm === true;
|
|
2553
|
+
const platform = await resolvePlatform({
|
|
2554
|
+
promptClient: prompts,
|
|
2555
|
+
platformArg: args.platform,
|
|
2556
|
+
storedPlatform: existingState?.platform,
|
|
2557
|
+
confirm: confirmMode
|
|
2558
|
+
});
|
|
2559
|
+
const appName = resolveAppName(args.app, existingState?.appName, projectRoot, confirmMode);
|
|
2560
|
+
const region = resolveRegion(args.region, existingState?.region, confirmMode);
|
|
2561
|
+
const deployDirectory = resolveDeployDirectory(projectRoot);
|
|
2562
|
+
const environment = args.prod === true ? "production" : "preview";
|
|
2563
|
+
const config = {
|
|
2564
|
+
projectRoot,
|
|
2565
|
+
appName,
|
|
2566
|
+
region,
|
|
2567
|
+
environment,
|
|
2568
|
+
confirm: confirmMode
|
|
2569
|
+
};
|
|
2570
|
+
const adapter = createDeployAdapter(platform);
|
|
2571
|
+
configureAdapterContext(adapter, {
|
|
2572
|
+
projectRoot,
|
|
2573
|
+
appName,
|
|
2574
|
+
region
|
|
2575
|
+
});
|
|
2576
|
+
logger.banner();
|
|
2577
|
+
logger.info(
|
|
2578
|
+
`Deploying to ${platform} (${appName}${region ? `, ${region}` : ""}, ${environment})`
|
|
2579
|
+
);
|
|
2580
|
+
if (confirmMode) {
|
|
2581
|
+
logger.step("Running in --confirm mode (non-interactive, fail-fast).");
|
|
453
2582
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
label: p === detected ? `${p} (detected)` : p,
|
|
465
|
-
value: p
|
|
466
|
-
}))
|
|
467
|
-
);
|
|
2583
|
+
await writeDockerfileArtifact(deployDirectory, {
|
|
2584
|
+
nativeDependencies: {
|
|
2585
|
+
"better-sqlite3": "^11.0.0",
|
|
2586
|
+
"drizzle-orm": "^0.45.2"
|
|
2587
|
+
}
|
|
2588
|
+
});
|
|
2589
|
+
await writeDockerIgnoreArtifact(deployDirectory);
|
|
2590
|
+
const detected = await adapter.detect();
|
|
2591
|
+
if (!detected) {
|
|
2592
|
+
await adapter.install();
|
|
468
2593
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
2594
|
+
await adapter.authenticate();
|
|
2595
|
+
const provisionResult = await adapter.provision(config);
|
|
2596
|
+
const artifacts = await adapter.build(config);
|
|
2597
|
+
const deployResult = await adapter.deploy(artifacts);
|
|
2598
|
+
if (existingState) {
|
|
2599
|
+
await updateDeployState(projectRoot, {
|
|
2600
|
+
platform,
|
|
2601
|
+
appName,
|
|
2602
|
+
region,
|
|
2603
|
+
projectRoot,
|
|
2604
|
+
liveUrl: deployResult.liveUrl,
|
|
2605
|
+
syncUrl: deployResult.syncUrl,
|
|
2606
|
+
databaseId: provisionResult.databaseId,
|
|
2607
|
+
lastDeploymentId: deployResult.deploymentId
|
|
2608
|
+
});
|
|
2609
|
+
} else {
|
|
2610
|
+
await writeDeployState(projectRoot, {
|
|
2611
|
+
platform,
|
|
2612
|
+
appName,
|
|
2613
|
+
region,
|
|
2614
|
+
projectRoot,
|
|
2615
|
+
liveUrl: deployResult.liveUrl,
|
|
2616
|
+
syncUrl: deployResult.syncUrl,
|
|
2617
|
+
databaseId: provisionResult.databaseId,
|
|
2618
|
+
lastDeploymentId: deployResult.deploymentId
|
|
2619
|
+
});
|
|
472
2620
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
projectName,
|
|
477
|
-
packageManager: pm,
|
|
478
|
-
koraVersion
|
|
479
|
-
});
|
|
480
|
-
logger.success("Project scaffolded");
|
|
481
|
-
if (!args["skip-install"]) {
|
|
482
|
-
logger.step("Installing dependencies...");
|
|
483
|
-
try {
|
|
484
|
-
(0, import_node_child_process2.execSync)(getInstallCommand(pm), { cwd: targetDir, stdio: "inherit" });
|
|
485
|
-
logger.success("Dependencies installed");
|
|
486
|
-
} catch {
|
|
487
|
-
logger.warn("Failed to install dependencies. Run install manually.");
|
|
488
|
-
}
|
|
2621
|
+
logger.success(`Deployment completed: ${deployResult.liveUrl}`);
|
|
2622
|
+
if (deployResult.syncUrl) {
|
|
2623
|
+
logger.step(`Sync endpoint: ${deployResult.syncUrl}`);
|
|
489
2624
|
}
|
|
490
|
-
logger.blank();
|
|
491
|
-
logger.info("Done! Next steps:");
|
|
492
|
-
logger.blank();
|
|
493
|
-
logger.step(` cd ${projectName}`);
|
|
494
|
-
logger.step(` ${getRunDevCommand(pm)}`);
|
|
495
|
-
logger.blank();
|
|
496
2625
|
}
|
|
497
2626
|
});
|
|
498
|
-
function
|
|
499
|
-
|
|
2627
|
+
async function resolvePlatform(options) {
|
|
2628
|
+
if (typeof options.platformArg === "string") {
|
|
2629
|
+
if (!isDeployPlatform(options.platformArg)) {
|
|
2630
|
+
throw new Error(
|
|
2631
|
+
`Invalid --platform value "${options.platformArg}". Valid options: ${DEPLOY_PLATFORMS.join(", ")}`
|
|
2632
|
+
);
|
|
2633
|
+
}
|
|
2634
|
+
return options.platformArg;
|
|
2635
|
+
}
|
|
2636
|
+
if (options.storedPlatform) {
|
|
2637
|
+
return options.storedPlatform;
|
|
2638
|
+
}
|
|
2639
|
+
if (options.confirm || !isInteractiveTerminal()) {
|
|
2640
|
+
throw new Error(
|
|
2641
|
+
"Missing deploy platform in --confirm mode. Provide --platform or run an interactive deploy first."
|
|
2642
|
+
);
|
|
2643
|
+
}
|
|
2644
|
+
return await options.promptClient.select("Where do you want to deploy?", [
|
|
2645
|
+
{
|
|
2646
|
+
label: "Fly.io (recommended for sync apps)",
|
|
2647
|
+
value: "fly"
|
|
2648
|
+
},
|
|
2649
|
+
{
|
|
2650
|
+
label: "Railway",
|
|
2651
|
+
value: "railway"
|
|
2652
|
+
},
|
|
2653
|
+
{
|
|
2654
|
+
label: "Render",
|
|
2655
|
+
value: "render"
|
|
2656
|
+
},
|
|
2657
|
+
{
|
|
2658
|
+
label: "Docker (self-hosted)",
|
|
2659
|
+
value: "docker"
|
|
2660
|
+
},
|
|
2661
|
+
{
|
|
2662
|
+
label: "Kora Cloud (coming soon)",
|
|
2663
|
+
value: "kora-cloud"
|
|
2664
|
+
}
|
|
2665
|
+
]);
|
|
500
2666
|
}
|
|
501
|
-
function
|
|
502
|
-
|
|
2667
|
+
function resolveAppName(argValue, storedValue, projectRoot, confirm) {
|
|
2668
|
+
if (typeof argValue === "string" && argValue.length > 0) {
|
|
2669
|
+
return sanitizeAppName(argValue);
|
|
2670
|
+
}
|
|
2671
|
+
if (storedValue && storedValue.length > 0) {
|
|
2672
|
+
return storedValue;
|
|
2673
|
+
}
|
|
2674
|
+
if (confirm) {
|
|
2675
|
+
throw new Error(
|
|
2676
|
+
"Missing app name in --confirm mode. Provide --app or run an interactive deploy first."
|
|
2677
|
+
);
|
|
2678
|
+
}
|
|
2679
|
+
return sanitizeAppName((0, import_node_path14.basename)(projectRoot));
|
|
503
2680
|
}
|
|
504
|
-
function
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
if (
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
2681
|
+
function resolveRegion(argValue, storedValue, confirm) {
|
|
2682
|
+
if (typeof argValue === "string" && argValue.length > 0) return argValue;
|
|
2683
|
+
if (storedValue !== void 0) return storedValue;
|
|
2684
|
+
if (confirm) {
|
|
2685
|
+
throw new Error(
|
|
2686
|
+
"Missing region in --confirm mode. Provide --region or run an interactive deploy first."
|
|
2687
|
+
);
|
|
2688
|
+
}
|
|
2689
|
+
return "iad";
|
|
511
2690
|
}
|
|
512
|
-
function
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
2691
|
+
function sanitizeAppName(value) {
|
|
2692
|
+
const normalized = value.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
|
|
2693
|
+
if (normalized.length === 0) {
|
|
2694
|
+
return "kora-app";
|
|
2695
|
+
}
|
|
2696
|
+
return normalized;
|
|
2697
|
+
}
|
|
2698
|
+
async function requireProjectRoot() {
|
|
2699
|
+
const projectRoot = await findProjectRoot();
|
|
2700
|
+
if (!projectRoot) {
|
|
2701
|
+
throw new InvalidProjectError(process.cwd());
|
|
2702
|
+
}
|
|
2703
|
+
return projectRoot;
|
|
2704
|
+
}
|
|
2705
|
+
function isInteractiveTerminal() {
|
|
2706
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
2707
|
+
}
|
|
2708
|
+
function configureAdapterContext(adapter, context) {
|
|
2709
|
+
if (hasContextSetter(adapter)) {
|
|
2710
|
+
adapter.setContext(context);
|
|
530
2711
|
}
|
|
531
2712
|
}
|
|
2713
|
+
function hasContextSetter(adapter) {
|
|
2714
|
+
return typeof adapter.setContext === "function";
|
|
2715
|
+
}
|
|
532
2716
|
|
|
533
2717
|
// src/commands/dev/dev-command.ts
|
|
534
|
-
var
|
|
535
|
-
var
|
|
536
|
-
var
|
|
537
|
-
var
|
|
2718
|
+
var import_promises13 = require("fs/promises");
|
|
2719
|
+
var import_node_path17 = require("path");
|
|
2720
|
+
var import_node_path18 = require("path");
|
|
2721
|
+
var import_citty3 = require("citty");
|
|
538
2722
|
|
|
539
2723
|
// src/commands/dev/kora-config.ts
|
|
540
|
-
var
|
|
541
|
-
var
|
|
542
|
-
var
|
|
2724
|
+
var import_node_child_process6 = require("child_process");
|
|
2725
|
+
var import_promises12 = require("fs/promises");
|
|
2726
|
+
var import_node_path15 = require("path");
|
|
543
2727
|
var import_node_url3 = require("url");
|
|
544
2728
|
var CONFIG_CANDIDATES = [
|
|
545
2729
|
"kora.config.ts",
|
|
@@ -552,7 +2736,7 @@ var CONFIG_CANDIDATES = [
|
|
|
552
2736
|
async function loadKoraConfig(projectRoot) {
|
|
553
2737
|
const configPath = await findKoraConfigFile(projectRoot);
|
|
554
2738
|
if (!configPath) return null;
|
|
555
|
-
const ext = (0,
|
|
2739
|
+
const ext = (0, import_node_path15.extname)(configPath);
|
|
556
2740
|
if (ext === ".ts" || ext === ".mts" || ext === ".cts") {
|
|
557
2741
|
const loaded2 = await loadTypeScriptConfig(configPath, projectRoot);
|
|
558
2742
|
return toConfigObject(loaded2);
|
|
@@ -562,9 +2746,9 @@ async function loadKoraConfig(projectRoot) {
|
|
|
562
2746
|
}
|
|
563
2747
|
async function findKoraConfigFile(projectRoot) {
|
|
564
2748
|
for (const file of CONFIG_CANDIDATES) {
|
|
565
|
-
const candidate = (0,
|
|
2749
|
+
const candidate = (0, import_node_path15.join)(projectRoot, file);
|
|
566
2750
|
try {
|
|
567
|
-
await (0,
|
|
2751
|
+
await (0, import_promises12.access)(candidate);
|
|
568
2752
|
return candidate;
|
|
569
2753
|
} catch {
|
|
570
2754
|
}
|
|
@@ -590,8 +2774,8 @@ async function loadTypeScriptConfig(configPath, projectRoot) {
|
|
|
590
2774
|
}
|
|
591
2775
|
}
|
|
592
2776
|
async function runCommand(command, args, cwd) {
|
|
593
|
-
return await new Promise((
|
|
594
|
-
const child = (0,
|
|
2777
|
+
return await new Promise((resolve9, reject) => {
|
|
2778
|
+
const child = (0, import_node_child_process6.spawn)(command, args, {
|
|
595
2779
|
cwd,
|
|
596
2780
|
stdio: ["ignore", "pipe", "pipe"],
|
|
597
2781
|
env: process.env
|
|
@@ -609,7 +2793,7 @@ async function runCommand(command, args, cwd) {
|
|
|
609
2793
|
});
|
|
610
2794
|
child.on("exit", (code) => {
|
|
611
2795
|
if (code === 0) {
|
|
612
|
-
|
|
2796
|
+
resolve9(stdout.trim());
|
|
613
2797
|
return;
|
|
614
2798
|
}
|
|
615
2799
|
reject(new Error(`Failed to load kora config (exit ${code ?? "unknown"}): ${stderr.trim()}`));
|
|
@@ -628,7 +2812,7 @@ function toConfigObject(mod) {
|
|
|
628
2812
|
}
|
|
629
2813
|
|
|
630
2814
|
// src/commands/dev/process-manager.ts
|
|
631
|
-
var
|
|
2815
|
+
var import_node_child_process7 = require("child_process");
|
|
632
2816
|
var ProcessManager = class {
|
|
633
2817
|
processes = /* @__PURE__ */ new Map();
|
|
634
2818
|
spawn(config) {
|
|
@@ -637,12 +2821,12 @@ var ProcessManager = class {
|
|
|
637
2821
|
env: { ...process.env, ...config.env },
|
|
638
2822
|
stdio: ["ignore", "pipe", "pipe"]
|
|
639
2823
|
};
|
|
640
|
-
const child = (0,
|
|
2824
|
+
const child = (0, import_node_child_process7.spawn)(config.command, config.args, options);
|
|
641
2825
|
let resolveExit;
|
|
642
2826
|
const runningProcess = {
|
|
643
2827
|
child,
|
|
644
|
-
exitPromise: new Promise((
|
|
645
|
-
resolveExit =
|
|
2828
|
+
exitPromise: new Promise((resolve9) => {
|
|
2829
|
+
resolveExit = resolve9;
|
|
646
2830
|
}),
|
|
647
2831
|
stdoutBuffer: "",
|
|
648
2832
|
stderrBuffer: ""
|
|
@@ -709,15 +2893,15 @@ var ProcessManager = class {
|
|
|
709
2893
|
}
|
|
710
2894
|
};
|
|
711
2895
|
function delay(ms) {
|
|
712
|
-
return new Promise((
|
|
713
|
-
setTimeout(
|
|
2896
|
+
return new Promise((resolve9) => {
|
|
2897
|
+
setTimeout(resolve9, ms);
|
|
714
2898
|
});
|
|
715
2899
|
}
|
|
716
2900
|
|
|
717
2901
|
// src/commands/dev/schema-watcher.ts
|
|
718
|
-
var
|
|
719
|
-
var
|
|
720
|
-
var
|
|
2902
|
+
var import_node_child_process8 = require("child_process");
|
|
2903
|
+
var import_node_fs4 = require("fs");
|
|
2904
|
+
var import_node_path16 = require("path");
|
|
721
2905
|
var SchemaWatcher = class {
|
|
722
2906
|
constructor(config) {
|
|
723
2907
|
this.config = config;
|
|
@@ -729,7 +2913,7 @@ var SchemaWatcher = class {
|
|
|
729
2913
|
debounceTimer = null;
|
|
730
2914
|
start() {
|
|
731
2915
|
if (this.watcher) return;
|
|
732
|
-
this.watcher = (0,
|
|
2916
|
+
this.watcher = (0, import_node_fs4.watch)(this.config.schemaPath, () => {
|
|
733
2917
|
this.scheduleRegeneration();
|
|
734
2918
|
});
|
|
735
2919
|
this.watcher.on("error", (error) => {
|
|
@@ -745,7 +2929,7 @@ var SchemaWatcher = class {
|
|
|
745
2929
|
this.watcher = null;
|
|
746
2930
|
}
|
|
747
2931
|
async regenerate() {
|
|
748
|
-
const koraBinJs = (0,
|
|
2932
|
+
const koraBinJs = (0, import_node_path16.join)(this.config.projectRoot, "node_modules", "@korajs", "cli", "dist", "bin.js");
|
|
749
2933
|
const hasTsx = await hasTsxInstalled(this.config.projectRoot);
|
|
750
2934
|
const command = process.execPath;
|
|
751
2935
|
const args = hasTsx ? ["--import", "tsx", koraBinJs, "generate", "types", "--schema", this.config.schemaPath] : [koraBinJs, "generate", "types", "--schema", this.config.schemaPath];
|
|
@@ -765,8 +2949,8 @@ var SchemaWatcher = class {
|
|
|
765
2949
|
}
|
|
766
2950
|
};
|
|
767
2951
|
async function spawnCommand(command, args, cwd) {
|
|
768
|
-
await new Promise((
|
|
769
|
-
const child = (0,
|
|
2952
|
+
await new Promise((resolve9, reject) => {
|
|
2953
|
+
const child = (0, import_node_child_process8.spawn)(command, args, {
|
|
770
2954
|
cwd,
|
|
771
2955
|
stdio: ["ignore", "pipe", "pipe"],
|
|
772
2956
|
env: process.env
|
|
@@ -782,7 +2966,7 @@ async function spawnCommand(command, args, cwd) {
|
|
|
782
2966
|
});
|
|
783
2967
|
child.on("exit", (code) => {
|
|
784
2968
|
if (code === 0) {
|
|
785
|
-
|
|
2969
|
+
resolve9();
|
|
786
2970
|
return;
|
|
787
2971
|
}
|
|
788
2972
|
reject(new Error(`Type generation exited with code ${code ?? "unknown"}.`));
|
|
@@ -804,7 +2988,7 @@ function toError(error) {
|
|
|
804
2988
|
}
|
|
805
2989
|
|
|
806
2990
|
// src/commands/dev/dev-command.ts
|
|
807
|
-
var devCommand = (0,
|
|
2991
|
+
var devCommand = (0, import_citty3.defineCommand)({
|
|
808
2992
|
meta: {
|
|
809
2993
|
name: "dev",
|
|
810
2994
|
description: "Start the Kora development environment"
|
|
@@ -844,7 +3028,7 @@ var devCommand = (0, import_citty2.defineCommand)({
|
|
|
844
3028
|
const watchDebounceMs = typeof config?.dev?.watch === "object" && typeof config.dev.watch.debounceMs === "number" ? config.dev.watch.debounceMs : 300;
|
|
845
3029
|
const viteEntryPoint = await resolveProjectBinaryEntryPoint(projectRoot, "vite", "vite");
|
|
846
3030
|
if (!viteEntryPoint) {
|
|
847
|
-
throw new DevServerError("vite", (0,
|
|
3031
|
+
throw new DevServerError("vite", (0, import_node_path17.join)(projectRoot, "node_modules", ".bin", "vite"));
|
|
848
3032
|
}
|
|
849
3033
|
const syncServerFile = await findSyncServerFile(projectRoot);
|
|
850
3034
|
let managedSyncStore = normalizeManagedSyncStore(config, projectRoot);
|
|
@@ -859,8 +3043,8 @@ var devCommand = (0, import_citty2.defineCommand)({
|
|
|
859
3043
|
}
|
|
860
3044
|
}
|
|
861
3045
|
if (shouldStartSync && syncServerFile === null && managedSyncStore) {
|
|
862
|
-
const hasServerPackage = await
|
|
863
|
-
(0,
|
|
3046
|
+
const hasServerPackage = await fileExists2(
|
|
3047
|
+
(0, import_node_path17.join)(projectRoot, "node_modules", "@korajs", "server", "package.json")
|
|
864
3048
|
);
|
|
865
3049
|
if (!hasServerPackage) {
|
|
866
3050
|
logger.warn(
|
|
@@ -877,8 +3061,8 @@ var devCommand = (0, import_citty2.defineCommand)({
|
|
|
877
3061
|
}
|
|
878
3062
|
let configuredSchemaPath = null;
|
|
879
3063
|
if (typeof config?.schema === "string") {
|
|
880
|
-
const candidate = (0,
|
|
881
|
-
if (await
|
|
3064
|
+
const candidate = (0, import_node_path18.resolve)(projectRoot, config.schema);
|
|
3065
|
+
if (await fileExists2(candidate)) {
|
|
882
3066
|
configuredSchemaPath = candidate;
|
|
883
3067
|
} else {
|
|
884
3068
|
logger.warn(`Configured schema file not found: ${candidate}. Falling back to auto-detection.`);
|
|
@@ -890,8 +3074,8 @@ var devCommand = (0, import_citty2.defineCommand)({
|
|
|
890
3074
|
let schemaWatcher = null;
|
|
891
3075
|
let shuttingDown = false;
|
|
892
3076
|
let resolveFinished;
|
|
893
|
-
const finished = new Promise((
|
|
894
|
-
resolveFinished =
|
|
3077
|
+
const finished = new Promise((resolve9) => {
|
|
3078
|
+
resolveFinished = resolve9;
|
|
895
3079
|
});
|
|
896
3080
|
const onManagedProcessExit = () => {
|
|
897
3081
|
if (!processManager.hasRunning() && !shuttingDown) {
|
|
@@ -989,19 +3173,19 @@ var devCommand = (0, import_citty2.defineCommand)({
|
|
|
989
3173
|
}
|
|
990
3174
|
});
|
|
991
3175
|
async function findSyncServerFile(projectRoot) {
|
|
992
|
-
const candidates = [(0,
|
|
3176
|
+
const candidates = [(0, import_node_path17.join)(projectRoot, "server.ts"), (0, import_node_path17.join)(projectRoot, "server.js")];
|
|
993
3177
|
for (const candidate of candidates) {
|
|
994
3178
|
try {
|
|
995
|
-
await (0,
|
|
3179
|
+
await (0, import_promises13.access)(candidate);
|
|
996
3180
|
return candidate;
|
|
997
3181
|
} catch {
|
|
998
3182
|
}
|
|
999
3183
|
}
|
|
1000
3184
|
return null;
|
|
1001
3185
|
}
|
|
1002
|
-
async function
|
|
3186
|
+
async function fileExists2(path) {
|
|
1003
3187
|
try {
|
|
1004
|
-
await (0,
|
|
3188
|
+
await (0, import_promises13.access)(path);
|
|
1005
3189
|
return true;
|
|
1006
3190
|
} catch {
|
|
1007
3191
|
return false;
|
|
@@ -1013,7 +3197,7 @@ function normalizeManagedSyncStore(config, projectRoot) {
|
|
|
1013
3197
|
const store = sync.store;
|
|
1014
3198
|
if (store === void 0) return { type: "memory" };
|
|
1015
3199
|
if (store === "memory") return { type: "memory" };
|
|
1016
|
-
if (store === "sqlite") return { type: "sqlite", filename: (0,
|
|
3200
|
+
if (store === "sqlite") return { type: "sqlite", filename: (0, import_node_path17.join)(projectRoot, "kora-sync.db") };
|
|
1017
3201
|
if (store === "postgres") {
|
|
1018
3202
|
const connectionString = process.env.DATABASE_URL;
|
|
1019
3203
|
if (!connectionString) return null;
|
|
@@ -1022,7 +3206,7 @@ function normalizeManagedSyncStore(config, projectRoot) {
|
|
|
1022
3206
|
if (typeof store === "object" && store !== null) {
|
|
1023
3207
|
if (store.type === "memory") return { type: "memory" };
|
|
1024
3208
|
if (store.type === "sqlite") {
|
|
1025
|
-
const filename = typeof store.filename === "string" && store.filename.length > 0 ? (0,
|
|
3209
|
+
const filename = typeof store.filename === "string" && store.filename.length > 0 ? (0, import_node_path18.resolve)(projectRoot, store.filename) : (0, import_node_path17.join)(projectRoot, "kora-sync.db");
|
|
1026
3210
|
return { type: "sqlite", filename };
|
|
1027
3211
|
}
|
|
1028
3212
|
if (store.type === "postgres" && typeof store.connectionString === "string") {
|
|
@@ -1085,9 +3269,9 @@ await new Promise(() => {});
|
|
|
1085
3269
|
`;
|
|
1086
3270
|
|
|
1087
3271
|
// src/commands/generate/generate-command.ts
|
|
1088
|
-
var
|
|
1089
|
-
var
|
|
1090
|
-
var
|
|
3272
|
+
var import_promises14 = require("fs/promises");
|
|
3273
|
+
var import_node_path19 = require("path");
|
|
3274
|
+
var import_citty4 = require("citty");
|
|
1091
3275
|
|
|
1092
3276
|
// src/commands/generate/type-generator.ts
|
|
1093
3277
|
function generateTypes(schema) {
|
|
@@ -1187,13 +3371,13 @@ function toPascalCase(name) {
|
|
|
1187
3371
|
}
|
|
1188
3372
|
|
|
1189
3373
|
// src/commands/generate/generate-command.ts
|
|
1190
|
-
var generateCommand = (0,
|
|
3374
|
+
var generateCommand = (0, import_citty4.defineCommand)({
|
|
1191
3375
|
meta: {
|
|
1192
3376
|
name: "generate",
|
|
1193
3377
|
description: "Generate code from your Kora schema"
|
|
1194
3378
|
},
|
|
1195
3379
|
subCommands: {
|
|
1196
|
-
types: (0,
|
|
3380
|
+
types: (0, import_citty4.defineCommand)({
|
|
1197
3381
|
meta: {
|
|
1198
3382
|
name: "types",
|
|
1199
3383
|
description: "Generate TypeScript types from your schema"
|
|
@@ -1217,7 +3401,7 @@ var generateCommand = (0, import_citty3.defineCommand)({
|
|
|
1217
3401
|
}
|
|
1218
3402
|
let schemaPath;
|
|
1219
3403
|
if (args.schema && typeof args.schema === "string") {
|
|
1220
|
-
schemaPath = (0,
|
|
3404
|
+
schemaPath = (0, import_node_path19.resolve)(args.schema);
|
|
1221
3405
|
} else {
|
|
1222
3406
|
const found = await findSchemaFile(projectRoot);
|
|
1223
3407
|
if (!found) {
|
|
@@ -1240,9 +3424,9 @@ var generateCommand = (0, import_citty3.defineCommand)({
|
|
|
1240
3424
|
}
|
|
1241
3425
|
const output = generateTypes(schema);
|
|
1242
3426
|
const outputFile = typeof args.output === "string" ? args.output : "kora/generated/types.ts";
|
|
1243
|
-
const outputPath = (0,
|
|
1244
|
-
await (0,
|
|
1245
|
-
await (0,
|
|
3427
|
+
const outputPath = (0, import_node_path19.resolve)(projectRoot, outputFile);
|
|
3428
|
+
await (0, import_promises14.mkdir)((0, import_node_path19.dirname)(outputPath), { recursive: true });
|
|
3429
|
+
await (0, import_promises14.writeFile)(outputPath, output, "utf-8");
|
|
1246
3430
|
logger.success(`Generated types at ${outputPath}`);
|
|
1247
3431
|
}
|
|
1248
3432
|
})
|
|
@@ -1262,9 +3446,9 @@ function isSchemaDefinition(value) {
|
|
|
1262
3446
|
}
|
|
1263
3447
|
|
|
1264
3448
|
// src/commands/migrate/migrate-command.ts
|
|
1265
|
-
var
|
|
1266
|
-
var
|
|
1267
|
-
var
|
|
3449
|
+
var import_promises15 = require("fs/promises");
|
|
3450
|
+
var import_node_path21 = require("path");
|
|
3451
|
+
var import_citty5 = require("citty");
|
|
1268
3452
|
|
|
1269
3453
|
// src/commands/migrate/migration-generator.ts
|
|
1270
3454
|
var import_core2 = require("@korajs/core");
|
|
@@ -1832,11 +4016,11 @@ async function loadPostgresModule() {
|
|
|
1832
4016
|
}
|
|
1833
4017
|
|
|
1834
4018
|
// src/commands/migrate/schema-loader.ts
|
|
1835
|
-
var
|
|
1836
|
-
var
|
|
4019
|
+
var import_node_child_process9 = require("child_process");
|
|
4020
|
+
var import_node_path20 = require("path");
|
|
1837
4021
|
var import_node_url4 = require("url");
|
|
1838
4022
|
async function loadSchemaDefinition(schemaPath, projectRoot) {
|
|
1839
|
-
const ext = (0,
|
|
4023
|
+
const ext = (0, import_node_path20.extname)(schemaPath);
|
|
1840
4024
|
const moduleValue = ext === ".ts" || ext === ".mts" || ext === ".cts" ? await loadTypeScriptModule(schemaPath, projectRoot) : await import(`${(0, import_node_url4.pathToFileURL)(schemaPath).href}?t=${Date.now()}-${Math.random()}`);
|
|
1841
4025
|
return extractSchema2(moduleValue);
|
|
1842
4026
|
}
|
|
@@ -1875,8 +4059,8 @@ async function loadTypeScriptModule(schemaPath, projectRoot) {
|
|
|
1875
4059
|
}
|
|
1876
4060
|
}
|
|
1877
4061
|
async function runCommand2(command, args, cwd) {
|
|
1878
|
-
return await new Promise((
|
|
1879
|
-
const child = (0,
|
|
4062
|
+
return await new Promise((resolve9, reject) => {
|
|
4063
|
+
const child = (0, import_node_child_process9.spawn)(command, args, {
|
|
1880
4064
|
cwd,
|
|
1881
4065
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1882
4066
|
env: process.env
|
|
@@ -1894,7 +4078,7 @@ async function runCommand2(command, args, cwd) {
|
|
|
1894
4078
|
});
|
|
1895
4079
|
child.on("exit", (code) => {
|
|
1896
4080
|
if (code === 0) {
|
|
1897
|
-
|
|
4081
|
+
resolve9(stdout.trim());
|
|
1898
4082
|
return;
|
|
1899
4083
|
}
|
|
1900
4084
|
reject(
|
|
@@ -1907,7 +4091,7 @@ async function runCommand2(command, args, cwd) {
|
|
|
1907
4091
|
// src/commands/migrate/migrate-command.ts
|
|
1908
4092
|
var SNAPSHOT_PATH = "kora/schema.snapshot.json";
|
|
1909
4093
|
var MIGRATIONS_DIR = "kora/migrations";
|
|
1910
|
-
var migrateCommand = (0,
|
|
4094
|
+
var migrateCommand = (0, import_citty5.defineCommand)({
|
|
1911
4095
|
meta: {
|
|
1912
4096
|
name: "migrate",
|
|
1913
4097
|
description: "Detect schema changes and generate/apply migrations"
|
|
@@ -1949,12 +4133,12 @@ var migrateCommand = (0, import_citty4.defineCommand)({
|
|
|
1949
4133
|
throw new InvalidProjectError(process.cwd());
|
|
1950
4134
|
}
|
|
1951
4135
|
const config = await loadKoraConfig(projectRoot);
|
|
1952
|
-
const resolvedSchemaPath = typeof args.schema === "string" ? (0,
|
|
4136
|
+
const resolvedSchemaPath = typeof args.schema === "string" ? (0, import_node_path21.resolve)(projectRoot, args.schema) : typeof config?.schema === "string" ? (0, import_node_path21.resolve)(projectRoot, config.schema) : await findSchemaFile(projectRoot);
|
|
1953
4137
|
if (!resolvedSchemaPath) {
|
|
1954
4138
|
throw new SchemaNotFoundError(["src/schema.ts", "schema.ts", "src/schema.js", "schema.js"]);
|
|
1955
4139
|
}
|
|
1956
4140
|
const currentSchema = await loadSchemaDefinition(resolvedSchemaPath, projectRoot);
|
|
1957
|
-
const snapshotFile = (0,
|
|
4141
|
+
const snapshotFile = (0, import_node_path21.join)(projectRoot, SNAPSHOT_PATH);
|
|
1958
4142
|
const previousSchema = await readSchemaSnapshot(snapshotFile);
|
|
1959
4143
|
if (!previousSchema) {
|
|
1960
4144
|
if (args["dry-run"] === true) {
|
|
@@ -1967,7 +4151,7 @@ var migrateCommand = (0, import_citty4.defineCommand)({
|
|
|
1967
4151
|
return;
|
|
1968
4152
|
}
|
|
1969
4153
|
const diff = diffSchemas(previousSchema, currentSchema);
|
|
1970
|
-
const outputDir = typeof args["output-dir"] === "string" ? (0,
|
|
4154
|
+
const outputDir = typeof args["output-dir"] === "string" ? (0, import_node_path21.resolve)(projectRoot, args["output-dir"]) : (0, import_node_path21.resolve)(projectRoot, MIGRATIONS_DIR);
|
|
1971
4155
|
if (!diff.hasChanges) {
|
|
1972
4156
|
logger.success("No schema changes detected.");
|
|
1973
4157
|
if (args.apply === true) {
|
|
@@ -2019,7 +4203,7 @@ var migrateCommand = (0, import_citty4.defineCommand)({
|
|
|
2019
4203
|
logger.warn("Dry run enabled: no files written, no migrations applied.");
|
|
2020
4204
|
return;
|
|
2021
4205
|
}
|
|
2022
|
-
await (0,
|
|
4206
|
+
await (0, import_promises15.mkdir)(outputDir, { recursive: true });
|
|
2023
4207
|
const migrationPath = await writeMigrationFile(outputDir, diff.fromVersion, diff.toVersion, generated);
|
|
2024
4208
|
await writeSchemaSnapshot(snapshotFile, currentSchema);
|
|
2025
4209
|
logger.blank();
|
|
@@ -2050,7 +4234,7 @@ var migrateCommand = (0, import_citty4.defineCommand)({
|
|
|
2050
4234
|
});
|
|
2051
4235
|
async function readSchemaSnapshot(path) {
|
|
2052
4236
|
try {
|
|
2053
|
-
const content = await (0,
|
|
4237
|
+
const content = await (0, import_promises15.readFile)(path, "utf-8");
|
|
2054
4238
|
return JSON.parse(content);
|
|
2055
4239
|
} catch {
|
|
2056
4240
|
return null;
|
|
@@ -2060,26 +4244,26 @@ async function confirmBreakingChanges(force) {
|
|
|
2060
4244
|
if (force) {
|
|
2061
4245
|
return true;
|
|
2062
4246
|
}
|
|
2063
|
-
if (!
|
|
4247
|
+
if (!isInteractiveTerminal2()) {
|
|
2064
4248
|
throw new Error(
|
|
2065
4249
|
"Breaking schema changes require confirmation. Re-run with --force to continue or --dry-run to preview."
|
|
2066
4250
|
);
|
|
2067
4251
|
}
|
|
2068
4252
|
return await promptConfirm("Continue and generate a breaking migration?", false);
|
|
2069
4253
|
}
|
|
2070
|
-
function
|
|
4254
|
+
function isInteractiveTerminal2() {
|
|
2071
4255
|
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
2072
4256
|
}
|
|
2073
4257
|
async function writeSchemaSnapshot(path, schema) {
|
|
2074
|
-
await (0,
|
|
2075
|
-
await (0,
|
|
4258
|
+
await (0, import_promises15.mkdir)((0, import_node_path21.dirname)(path), { recursive: true });
|
|
4259
|
+
await (0, import_promises15.writeFile)(path, `${JSON.stringify(schema, null, 2)}
|
|
2076
4260
|
`, "utf-8");
|
|
2077
4261
|
}
|
|
2078
4262
|
async function writeMigrationFile(outputDir, fromVersion, toVersion, generated) {
|
|
2079
|
-
const existing = await (0,
|
|
4263
|
+
const existing = await (0, import_promises15.readdir)(outputDir).catch(() => []);
|
|
2080
4264
|
const sequence = existing.filter((file) => /^\d{3}-/.test(file)).length + 1;
|
|
2081
4265
|
const filename = `${String(sequence).padStart(3, "0")}-v${fromVersion}-to-v${toVersion}.ts`;
|
|
2082
|
-
const path = (0,
|
|
4266
|
+
const path = (0, import_node_path21.join)(outputDir, filename);
|
|
2083
4267
|
const migrationId = filename.replace(/\.ts$/, "");
|
|
2084
4268
|
const fileContent = [
|
|
2085
4269
|
`export const up = ${JSON.stringify(generated.up, null, 2)} as const`,
|
|
@@ -2091,8 +4275,8 @@ async function writeMigrationFile(outputDir, fromVersion, toVersion, generated)
|
|
|
2091
4275
|
`export const containsBreakingChanges = ${generated.containsBreakingChanges}`,
|
|
2092
4276
|
""
|
|
2093
4277
|
].join("\n");
|
|
2094
|
-
await (0,
|
|
2095
|
-
await writeMigrationManifest((0,
|
|
4278
|
+
await (0, import_promises15.writeFile)(path, fileContent, "utf-8");
|
|
4279
|
+
await writeMigrationManifest((0, import_node_path21.join)(outputDir, `${migrationId}.json`), {
|
|
2096
4280
|
id: migrationId,
|
|
2097
4281
|
fromVersion,
|
|
2098
4282
|
toVersion,
|
|
@@ -2104,22 +4288,22 @@ async function writeMigrationFile(outputDir, fromVersion, toVersion, generated)
|
|
|
2104
4288
|
return path;
|
|
2105
4289
|
}
|
|
2106
4290
|
async function writeMigrationManifest(path, manifest) {
|
|
2107
|
-
await (0,
|
|
4291
|
+
await (0, import_promises15.writeFile)(path, `${JSON.stringify(manifest, null, 2)}
|
|
2108
4292
|
`, "utf-8");
|
|
2109
4293
|
}
|
|
2110
4294
|
async function listMigrationManifests(outputDir) {
|
|
2111
|
-
const files = await (0,
|
|
4295
|
+
const files = await (0, import_promises15.readdir)(outputDir).catch(() => []);
|
|
2112
4296
|
const migrationFiles = files.filter((file) => /^\d{3}-.*\.ts$/.test(file)).sort((left, right) => left.localeCompare(right));
|
|
2113
4297
|
const manifests = [];
|
|
2114
4298
|
for (const file of migrationFiles) {
|
|
2115
4299
|
const id = file.replace(/\.ts$/, "");
|
|
2116
|
-
const manifestPath = (0,
|
|
4300
|
+
const manifestPath = (0, import_node_path21.join)(outputDir, `${id}.json`);
|
|
2117
4301
|
const jsonManifest = await readMigrationManifest(manifestPath);
|
|
2118
4302
|
if (jsonManifest) {
|
|
2119
4303
|
manifests.push({ ...jsonManifest, id });
|
|
2120
4304
|
continue;
|
|
2121
4305
|
}
|
|
2122
|
-
const migrationPath = (0,
|
|
4306
|
+
const migrationPath = (0, import_node_path21.join)(outputDir, file);
|
|
2123
4307
|
const sourceManifest = await readMigrationManifestFromSource(migrationPath, id);
|
|
2124
4308
|
manifests.push(sourceManifest);
|
|
2125
4309
|
}
|
|
@@ -2127,7 +4311,7 @@ async function listMigrationManifests(outputDir) {
|
|
|
2127
4311
|
}
|
|
2128
4312
|
async function readMigrationManifest(path) {
|
|
2129
4313
|
try {
|
|
2130
|
-
const content = await (0,
|
|
4314
|
+
const content = await (0, import_promises15.readFile)(path, "utf-8");
|
|
2131
4315
|
return JSON.parse(content);
|
|
2132
4316
|
} catch (error) {
|
|
2133
4317
|
const code = error.code;
|
|
@@ -2138,7 +4322,7 @@ async function readMigrationManifest(path) {
|
|
|
2138
4322
|
}
|
|
2139
4323
|
}
|
|
2140
4324
|
async function readMigrationManifestFromSource(path, id) {
|
|
2141
|
-
const content = await (0,
|
|
4325
|
+
const content = await (0, import_promises15.readFile)(path, "utf-8");
|
|
2142
4326
|
const versions = parseVersionsFromMigrationId(id);
|
|
2143
4327
|
return {
|
|
2144
4328
|
id,
|
|
@@ -2195,18 +4379,18 @@ function parseLiteralExport(source, exportName) {
|
|
|
2195
4379
|
}
|
|
2196
4380
|
function resolveSqliteApplyPath(dbArg, projectRoot, config) {
|
|
2197
4381
|
if (typeof dbArg === "string") {
|
|
2198
|
-
return (0,
|
|
4382
|
+
return (0, import_node_path21.resolve)(projectRoot, dbArg);
|
|
2199
4383
|
}
|
|
2200
4384
|
const sync = config?.dev?.sync;
|
|
2201
4385
|
if (typeof sync === "object" && sync !== null) {
|
|
2202
4386
|
if (sync.store === "sqlite") {
|
|
2203
|
-
return (0,
|
|
4387
|
+
return (0, import_node_path21.join)(projectRoot, "kora-sync.db");
|
|
2204
4388
|
}
|
|
2205
4389
|
if (typeof sync.store === "object" && sync.store !== null && sync.store.type === "sqlite") {
|
|
2206
4390
|
if (typeof sync.store.filename === "string" && sync.store.filename.length > 0) {
|
|
2207
|
-
return (0,
|
|
4391
|
+
return (0, import_node_path21.resolve)(projectRoot, sync.store.filename);
|
|
2208
4392
|
}
|
|
2209
|
-
return (0,
|
|
4393
|
+
return (0, import_node_path21.join)(projectRoot, "kora-sync.db");
|
|
2210
4394
|
}
|
|
2211
4395
|
}
|
|
2212
4396
|
return void 0;
|
|
@@ -2224,7 +4408,7 @@ function resolvePostgresConnectionString(config) {
|
|
|
2224
4408
|
}
|
|
2225
4409
|
|
|
2226
4410
|
// src/bin.ts
|
|
2227
|
-
var main = (0,
|
|
4411
|
+
var main = (0, import_citty6.defineCommand)({
|
|
2228
4412
|
meta: {
|
|
2229
4413
|
name: "kora",
|
|
2230
4414
|
description: "Kora.js \u2014 Offline-first application framework"
|
|
@@ -2232,9 +4416,10 @@ var main = (0, import_citty5.defineCommand)({
|
|
|
2232
4416
|
subCommands: {
|
|
2233
4417
|
create: createCommand,
|
|
2234
4418
|
dev: devCommand,
|
|
4419
|
+
deploy: deployCommand,
|
|
2235
4420
|
generate: generateCommand,
|
|
2236
4421
|
migrate: migrateCommand
|
|
2237
4422
|
}
|
|
2238
4423
|
});
|
|
2239
|
-
(0,
|
|
4424
|
+
(0, import_citty6.runMain)(main);
|
|
2240
4425
|
//# sourceMappingURL=bin.cjs.map
|