@korajs/cli 0.1.15 → 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 +8 -2
- 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/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,20 +17,71 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
21
31
|
var index_exports = {};
|
|
22
32
|
__export(index_exports, {
|
|
33
|
+
ClackPromptClient: () => ClackPromptClient,
|
|
23
34
|
CliError: () => CliError,
|
|
35
|
+
DEPLOY_PLATFORMS: () => DEPLOY_PLATFORMS,
|
|
24
36
|
DevServerError: () => DevServerError,
|
|
37
|
+
FlyAdapter: () => FlyAdapter,
|
|
25
38
|
InvalidProjectError: () => InvalidProjectError,
|
|
39
|
+
NodeFlyCommandRunner: () => NodeFlyCommandRunner,
|
|
40
|
+
NodeRailwayCommandRunner: () => NodeRailwayCommandRunner,
|
|
26
41
|
PACKAGE_MANAGERS: () => PACKAGE_MANAGERS,
|
|
42
|
+
PreferenceStore: () => PreferenceStore,
|
|
27
43
|
ProjectExistsError: () => ProjectExistsError,
|
|
44
|
+
PromptCancelledError: () => PromptCancelledError,
|
|
45
|
+
RailwayAdapter: () => RailwayAdapter,
|
|
46
|
+
ReadlinePromptClient: () => ReadlinePromptClient,
|
|
28
47
|
SchemaNotFoundError: () => SchemaNotFoundError,
|
|
48
|
+
StubDeployAdapter: () => StubDeployAdapter,
|
|
29
49
|
TEMPLATES: () => TEMPLATES,
|
|
30
50
|
TEMPLATE_INFO: () => TEMPLATE_INFO,
|
|
31
|
-
|
|
51
|
+
applySyncProviderPreset: () => applySyncProviderPreset,
|
|
52
|
+
buildClient: () => buildClient,
|
|
53
|
+
bundleServer: () => bundleServer,
|
|
54
|
+
createDeployAdapter: () => createDeployAdapter,
|
|
55
|
+
createPromptClient: () => createPromptClient,
|
|
56
|
+
deployCommand: () => deployCommand,
|
|
57
|
+
determineTemplateFromSelections: () => determineTemplateFromSelections,
|
|
58
|
+
generateDeployPackageJson: () => generateDeployPackageJson,
|
|
59
|
+
generateDockerIgnore: () => generateDockerIgnore,
|
|
60
|
+
generateDockerfile: () => generateDockerfile,
|
|
61
|
+
generateFlyToml: () => generateFlyToml,
|
|
62
|
+
generateRailwayJson: () => generateRailwayJson,
|
|
63
|
+
generateTypes: () => generateTypes,
|
|
64
|
+
getCreatePreferencesOrDefault: () => getCreatePreferencesOrDefault,
|
|
65
|
+
getDefaultCreatePreferences: () => getDefaultCreatePreferences,
|
|
66
|
+
isAuthValue: () => isAuthValue,
|
|
67
|
+
isDatabaseProviderValue: () => isDatabaseProviderValue,
|
|
68
|
+
isDatabaseValue: () => isDatabaseValue,
|
|
69
|
+
isDeployPlatform: () => isDeployPlatform,
|
|
70
|
+
isFrameworkValue: () => isFrameworkValue,
|
|
71
|
+
readDeployState: () => readDeployState,
|
|
72
|
+
resetDeployState: () => resetDeployState,
|
|
73
|
+
resolveCreatePreferencesFlow: () => resolveCreatePreferencesFlow,
|
|
74
|
+
resolveDeployDirectory: () => resolveDeployDirectory,
|
|
75
|
+
resolveDeployStatePath: () => resolveDeployStatePath,
|
|
76
|
+
saveResolvedPreferences: () => saveResolvedPreferences,
|
|
77
|
+
shouldSavePreferences: () => shouldSavePreferences,
|
|
78
|
+
updateDeployState: () => updateDeployState,
|
|
79
|
+
validateProjectName: () => validateProjectName,
|
|
80
|
+
writeDeployState: () => writeDeployState,
|
|
81
|
+
writeDockerIgnoreArtifact: () => writeDockerIgnoreArtifact,
|
|
82
|
+
writeDockerfileArtifact: () => writeDockerfileArtifact,
|
|
83
|
+
writeFlyTomlArtifact: () => writeFlyTomlArtifact,
|
|
84
|
+
writeRailwayJsonArtifact: () => writeRailwayJsonArtifact
|
|
32
85
|
});
|
|
33
86
|
module.exports = __toCommonJS(index_exports);
|
|
34
87
|
|
|
@@ -218,16 +271,2064 @@ function toPascalCase(name) {
|
|
|
218
271
|
return first ? first.toUpperCase() + part.slice(1) : "";
|
|
219
272
|
}).join("");
|
|
220
273
|
}
|
|
274
|
+
|
|
275
|
+
// src/commands/deploy/deploy-command.ts
|
|
276
|
+
var import_node_path10 = require("path");
|
|
277
|
+
var import_citty = require("citty");
|
|
278
|
+
|
|
279
|
+
// src/prompts/prompt-client.ts
|
|
280
|
+
var import_prompts = require("@clack/prompts");
|
|
281
|
+
|
|
282
|
+
// src/utils/prompt.ts
|
|
283
|
+
var import_node_readline = require("readline");
|
|
284
|
+
function promptText(message, defaultValue, options) {
|
|
285
|
+
return new Promise((resolve3) => {
|
|
286
|
+
const rl = createReadline(options);
|
|
287
|
+
const suffix = defaultValue !== void 0 ? ` (${defaultValue})` : "";
|
|
288
|
+
rl.question(` ? ${message}${suffix}: `, (answer) => {
|
|
289
|
+
rl.close();
|
|
290
|
+
const trimmed = answer.trim();
|
|
291
|
+
resolve3(trimmed || defaultValue || "");
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
function promptSelect(message, choices, options) {
|
|
296
|
+
return new Promise((resolve3) => {
|
|
297
|
+
const rl = createReadline(options);
|
|
298
|
+
const out = options?.output ?? process.stdout;
|
|
299
|
+
out.write(` ? ${message}
|
|
300
|
+
`);
|
|
301
|
+
for (let i = 0; i < choices.length; i++) {
|
|
302
|
+
const choice = choices[i];
|
|
303
|
+
if (choice) {
|
|
304
|
+
out.write(` ${i + 1}) ${choice.label}
|
|
305
|
+
`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const ask = () => {
|
|
309
|
+
rl.question(" > ", (answer) => {
|
|
310
|
+
const index = Number.parseInt(answer.trim(), 10) - 1;
|
|
311
|
+
const selected = choices[index];
|
|
312
|
+
if (selected) {
|
|
313
|
+
rl.close();
|
|
314
|
+
resolve3(selected.value);
|
|
315
|
+
} else {
|
|
316
|
+
out.write(` Please enter a number between 1 and ${choices.length}
|
|
317
|
+
`);
|
|
318
|
+
ask();
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
};
|
|
322
|
+
ask();
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
function promptConfirm(message, defaultValue = false, options) {
|
|
326
|
+
return new Promise((resolve3) => {
|
|
327
|
+
const rl = createReadline(options);
|
|
328
|
+
const suffix = defaultValue ? "Y/n" : "y/N";
|
|
329
|
+
const ask = () => {
|
|
330
|
+
rl.question(` ? ${message} (${suffix}): `, (answer) => {
|
|
331
|
+
const normalized = answer.trim().toLowerCase();
|
|
332
|
+
if (normalized.length === 0) {
|
|
333
|
+
rl.close();
|
|
334
|
+
resolve3(defaultValue);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (normalized === "y" || normalized === "yes") {
|
|
338
|
+
rl.close();
|
|
339
|
+
resolve3(true);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (normalized === "n" || normalized === "no") {
|
|
343
|
+
rl.close();
|
|
344
|
+
resolve3(false);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
(options?.output ?? process.stdout).write(" Please answer with y or n\n");
|
|
348
|
+
ask();
|
|
349
|
+
});
|
|
350
|
+
};
|
|
351
|
+
ask();
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
function createReadline(options) {
|
|
355
|
+
return (0, import_node_readline.createInterface)({
|
|
356
|
+
input: options?.input ?? process.stdin,
|
|
357
|
+
output: options?.output ?? process.stdout
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// src/prompts/prompt-client.ts
|
|
362
|
+
var PromptCancelledError = class extends Error {
|
|
363
|
+
constructor(message = "Prompt cancelled by user") {
|
|
364
|
+
super(message);
|
|
365
|
+
this.name = "PromptCancelledError";
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
var ReadlinePromptClient = class {
|
|
369
|
+
async text(message, defaultValue) {
|
|
370
|
+
return promptText(message, defaultValue);
|
|
371
|
+
}
|
|
372
|
+
async select(message, options) {
|
|
373
|
+
return promptSelect(
|
|
374
|
+
message,
|
|
375
|
+
options.filter((option) => option.disabled !== true).map((option) => ({ label: option.label, value: option.value }))
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
async confirm(message, defaultValue = false) {
|
|
379
|
+
return promptConfirm(message, defaultValue);
|
|
380
|
+
}
|
|
381
|
+
intro(message) {
|
|
382
|
+
void message;
|
|
383
|
+
}
|
|
384
|
+
outro(message) {
|
|
385
|
+
void message;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
function createPromptClient() {
|
|
389
|
+
const canUseInteractiveClack = typeof process !== "undefined" && process.stdin.isTTY && process.stdout.isTTY;
|
|
390
|
+
if (canUseInteractiveClack) {
|
|
391
|
+
return new ClackPromptClient();
|
|
392
|
+
}
|
|
393
|
+
return new ReadlinePromptClient();
|
|
394
|
+
}
|
|
395
|
+
var ClackPromptClient = class {
|
|
396
|
+
async text(message, defaultValue) {
|
|
397
|
+
const result = await (0, import_prompts.text)({
|
|
398
|
+
message,
|
|
399
|
+
placeholder: defaultValue,
|
|
400
|
+
defaultValue
|
|
401
|
+
});
|
|
402
|
+
if ((0, import_prompts.isCancel)(result)) {
|
|
403
|
+
(0, import_prompts.cancel)("Operation cancelled.");
|
|
404
|
+
throw new PromptCancelledError();
|
|
405
|
+
}
|
|
406
|
+
const value = result.trim();
|
|
407
|
+
if (value.length > 0) return value;
|
|
408
|
+
return defaultValue ?? "";
|
|
409
|
+
}
|
|
410
|
+
async select(message, options) {
|
|
411
|
+
const mappedOptions = options.map((option) => ({
|
|
412
|
+
label: option.label,
|
|
413
|
+
value: option.value,
|
|
414
|
+
hint: option.hint,
|
|
415
|
+
disabled: option.disabled
|
|
416
|
+
}));
|
|
417
|
+
const result = await (0, import_prompts.select)({
|
|
418
|
+
message,
|
|
419
|
+
options: mappedOptions
|
|
420
|
+
});
|
|
421
|
+
if ((0, import_prompts.isCancel)(result)) {
|
|
422
|
+
(0, import_prompts.cancel)("Operation cancelled.");
|
|
423
|
+
throw new PromptCancelledError();
|
|
424
|
+
}
|
|
425
|
+
return result;
|
|
426
|
+
}
|
|
427
|
+
async confirm(message, defaultValue = false) {
|
|
428
|
+
const result = await (0, import_prompts.confirm)({
|
|
429
|
+
message,
|
|
430
|
+
initialValue: defaultValue
|
|
431
|
+
});
|
|
432
|
+
if ((0, import_prompts.isCancel)(result)) {
|
|
433
|
+
(0, import_prompts.cancel)("Operation cancelled.");
|
|
434
|
+
throw new PromptCancelledError();
|
|
435
|
+
}
|
|
436
|
+
return result;
|
|
437
|
+
}
|
|
438
|
+
intro(message) {
|
|
439
|
+
(0, import_prompts.intro)(message);
|
|
440
|
+
}
|
|
441
|
+
outro(message) {
|
|
442
|
+
(0, import_prompts.outro)(message);
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// src/utils/fs-helpers.ts
|
|
447
|
+
var import_promises = require("fs/promises");
|
|
448
|
+
var import_node_path = require("path");
|
|
449
|
+
async function findProjectRoot(startDir) {
|
|
450
|
+
let current = (0, import_node_path.resolve)(startDir ?? process.cwd());
|
|
451
|
+
for (; ; ) {
|
|
452
|
+
const pkgPath = (0, import_node_path.join)(current, "package.json");
|
|
453
|
+
try {
|
|
454
|
+
const content = await (0, import_promises.readFile)(pkgPath, "utf-8");
|
|
455
|
+
const pkg = JSON.parse(content);
|
|
456
|
+
if (isKoraProject(pkg)) {
|
|
457
|
+
return current;
|
|
458
|
+
}
|
|
459
|
+
} catch {
|
|
460
|
+
}
|
|
461
|
+
const parent = (0, import_node_path.dirname)(current);
|
|
462
|
+
if (parent === current) break;
|
|
463
|
+
current = parent;
|
|
464
|
+
}
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
async function resolveProjectBinaryEntryPoint(projectRoot, packageName, binaryName) {
|
|
468
|
+
const pkgJsonPath = (0, import_node_path.join)(projectRoot, "node_modules", packageName, "package.json");
|
|
469
|
+
try {
|
|
470
|
+
const content = await (0, import_promises.readFile)(pkgJsonPath, "utf-8");
|
|
471
|
+
const pkg = JSON.parse(content);
|
|
472
|
+
let binPath;
|
|
473
|
+
if (typeof pkg.bin === "string") {
|
|
474
|
+
binPath = pkg.bin;
|
|
475
|
+
} else if (typeof pkg.bin === "object" && pkg.bin !== null) {
|
|
476
|
+
binPath = pkg.bin[binaryName];
|
|
477
|
+
}
|
|
478
|
+
if (!binPath) return null;
|
|
479
|
+
const fullPath = (0, import_node_path.join)(projectRoot, "node_modules", packageName, binPath);
|
|
480
|
+
await (0, import_promises.access)(fullPath);
|
|
481
|
+
return fullPath;
|
|
482
|
+
} catch {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function isKoraProject(pkg) {
|
|
487
|
+
if (typeof pkg !== "object" || pkg === null) return false;
|
|
488
|
+
const record = pkg;
|
|
489
|
+
return hasKoraDep(record.dependencies) || hasKoraDep(record.devDependencies);
|
|
490
|
+
}
|
|
491
|
+
function hasKoraDep(deps) {
|
|
492
|
+
if (typeof deps !== "object" || deps === null) return false;
|
|
493
|
+
return Object.keys(deps).some((key) => key === "kora" || key.startsWith("@korajs/"));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/utils/logger.ts
|
|
497
|
+
var RESET = "\x1B[0m";
|
|
498
|
+
var BOLD = "\x1B[1m";
|
|
499
|
+
var DIM = "\x1B[2m";
|
|
500
|
+
var GREEN = "\x1B[32m";
|
|
501
|
+
var YELLOW = "\x1B[33m";
|
|
502
|
+
var RED = "\x1B[31m";
|
|
503
|
+
var CYAN = "\x1B[36m";
|
|
504
|
+
function createLogger(options) {
|
|
505
|
+
const colorDisabled = options?.noColor === true || process.env.NO_COLOR !== void 0 || !process.stdout.isTTY;
|
|
506
|
+
function color(code, text) {
|
|
507
|
+
return colorDisabled ? text : `${code}${text}${RESET}`;
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
info(message) {
|
|
511
|
+
console.log(color(CYAN, message));
|
|
512
|
+
},
|
|
513
|
+
success(message) {
|
|
514
|
+
console.log(color(GREEN, ` \u2713 ${message}`));
|
|
515
|
+
},
|
|
516
|
+
warn(message) {
|
|
517
|
+
console.warn(color(YELLOW, ` \u26A0 ${message}`));
|
|
518
|
+
},
|
|
519
|
+
error(message) {
|
|
520
|
+
console.error(color(RED, ` \u2717 ${message}`));
|
|
521
|
+
},
|
|
522
|
+
step(message) {
|
|
523
|
+
console.log(color(DIM, ` ${message}`));
|
|
524
|
+
},
|
|
525
|
+
blank() {
|
|
526
|
+
console.log();
|
|
527
|
+
},
|
|
528
|
+
banner() {
|
|
529
|
+
console.log();
|
|
530
|
+
console.log(
|
|
531
|
+
color(BOLD + CYAN, " Kora.js") + color(DIM, " \u2014 Offline-first application framework")
|
|
532
|
+
);
|
|
533
|
+
console.log();
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/commands/deploy/adapters/adapter.ts
|
|
539
|
+
var DEPLOY_PLATFORMS = ["fly", "railway", "render", "docker", "kora-cloud"];
|
|
540
|
+
function isDeployPlatform(value) {
|
|
541
|
+
return DEPLOY_PLATFORMS.includes(value);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/commands/deploy/adapters/fly-adapter.ts
|
|
545
|
+
var import_node_child_process2 = require("child_process");
|
|
546
|
+
var import_node_path5 = require("path");
|
|
547
|
+
|
|
548
|
+
// src/commands/deploy/artifacts/fly-toml-generator.ts
|
|
549
|
+
var import_promises2 = require("fs/promises");
|
|
550
|
+
var import_node_path2 = require("path");
|
|
551
|
+
var GENERATED_HEADER = "# Generated by kora deploy \u2014 do not edit manually";
|
|
552
|
+
function generateFlyToml(options) {
|
|
553
|
+
const internalPort = options.internalPort ?? 3e3;
|
|
554
|
+
return [
|
|
555
|
+
GENERATED_HEADER,
|
|
556
|
+
"",
|
|
557
|
+
`app = "${options.appName}"`,
|
|
558
|
+
`primary_region = "${options.region}"`,
|
|
559
|
+
"",
|
|
560
|
+
"[build]",
|
|
561
|
+
` dockerfile = "Dockerfile"`,
|
|
562
|
+
"",
|
|
563
|
+
"[http_service]",
|
|
564
|
+
` internal_port = ${String(internalPort)}`,
|
|
565
|
+
" force_https = true",
|
|
566
|
+
' auto_stop_machines = "stop"',
|
|
567
|
+
" auto_start_machines = true",
|
|
568
|
+
" min_machines_running = 0",
|
|
569
|
+
"",
|
|
570
|
+
" [[http_service.checks]]",
|
|
571
|
+
' grace_period = "10s"',
|
|
572
|
+
' interval = "30s"',
|
|
573
|
+
' method = "GET"',
|
|
574
|
+
' timeout = "5s"',
|
|
575
|
+
' path = "/"',
|
|
576
|
+
""
|
|
577
|
+
].join("\n");
|
|
578
|
+
}
|
|
579
|
+
async function writeFlyTomlArtifact(deployDirectory, options) {
|
|
580
|
+
await (0, import_promises2.mkdir)(deployDirectory, { recursive: true });
|
|
581
|
+
const filePath = (0, import_node_path2.join)(deployDirectory, "fly.toml");
|
|
582
|
+
await (0, import_promises2.writeFile)(filePath, generateFlyToml(options), "utf-8");
|
|
583
|
+
return filePath;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/commands/deploy/builder/client-builder.ts
|
|
587
|
+
var import_node_child_process = require("child_process");
|
|
588
|
+
var import_promises3 = require("fs/promises");
|
|
589
|
+
var import_node_fs = require("fs");
|
|
590
|
+
var import_node_path3 = require("path");
|
|
591
|
+
async function buildClient(options) {
|
|
592
|
+
const viteEntryPoint = await resolveProjectBinaryEntryPoint(options.projectRoot, "vite", "vite");
|
|
593
|
+
if (!viteEntryPoint) {
|
|
594
|
+
throw new Error(
|
|
595
|
+
`Could not find local Vite binary in ${options.projectRoot}. Install dependencies before deploying.`
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
const args = [
|
|
599
|
+
viteEntryPoint,
|
|
600
|
+
"build",
|
|
601
|
+
"--outDir",
|
|
602
|
+
options.outDir,
|
|
603
|
+
"--mode",
|
|
604
|
+
options.mode ?? "production"
|
|
605
|
+
];
|
|
606
|
+
await runProcess(process.execPath, args, options.projectRoot);
|
|
607
|
+
await patchSqliteWasmAssets(options.projectRoot, options.outDir);
|
|
608
|
+
return { outDir: options.outDir };
|
|
609
|
+
}
|
|
610
|
+
async function patchSqliteWasmAssets(projectRoot, outDir) {
|
|
611
|
+
const assetsDir = (0, import_node_path3.join)(outDir, "assets");
|
|
612
|
+
if (!(0, import_node_fs.existsSync)(assetsDir)) return;
|
|
613
|
+
const files = await (0, import_promises3.readdir)(assetsDir);
|
|
614
|
+
const hashedWasm = files.find((f) => /^sqlite3-.+\.wasm$/.test(f));
|
|
615
|
+
if (hashedWasm && !files.includes("sqlite3.wasm")) {
|
|
616
|
+
await (0, import_promises3.copyFile)((0, import_node_path3.join)(assetsDir, hashedWasm), (0, import_node_path3.join)(assetsDir, "sqlite3.wasm"));
|
|
617
|
+
}
|
|
618
|
+
if (!files.includes("sqlite3-opfs-async-proxy.js")) {
|
|
619
|
+
const proxyFile = (0, import_node_path3.resolve)(
|
|
620
|
+
projectRoot,
|
|
621
|
+
"node_modules",
|
|
622
|
+
"@sqlite.org",
|
|
623
|
+
"sqlite-wasm",
|
|
624
|
+
"sqlite-wasm",
|
|
625
|
+
"jswasm",
|
|
626
|
+
"sqlite3-opfs-async-proxy.js"
|
|
627
|
+
);
|
|
628
|
+
if ((0, import_node_fs.existsSync)(proxyFile)) {
|
|
629
|
+
await (0, import_promises3.copyFile)(proxyFile, (0, import_node_path3.join)(assetsDir, "sqlite3-opfs-async-proxy.js"));
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
async function runProcess(command, args, cwd) {
|
|
634
|
+
await new Promise((resolve3, reject) => {
|
|
635
|
+
const child = (0, import_node_child_process.spawn)(command, args, {
|
|
636
|
+
cwd,
|
|
637
|
+
stdio: "inherit",
|
|
638
|
+
env: process.env
|
|
639
|
+
});
|
|
640
|
+
child.on("error", (error) => {
|
|
641
|
+
reject(error);
|
|
642
|
+
});
|
|
643
|
+
child.on("exit", (code) => {
|
|
644
|
+
if (code === 0) {
|
|
645
|
+
resolve3();
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
reject(new Error(`Client build failed with exit code ${String(code ?? "unknown")}.`));
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/commands/deploy/builder/server-bundler.ts
|
|
654
|
+
var import_promises4 = require("fs/promises");
|
|
655
|
+
var import_promises5 = require("fs/promises");
|
|
656
|
+
var import_node_path4 = require("path");
|
|
657
|
+
var import_esbuild = require("esbuild");
|
|
658
|
+
var DEFAULT_ENTRY_CANDIDATES = [
|
|
659
|
+
"server.ts",
|
|
660
|
+
"server.js",
|
|
661
|
+
"src/server.ts",
|
|
662
|
+
"src/server.js"
|
|
663
|
+
];
|
|
664
|
+
async function bundleServer(options) {
|
|
665
|
+
const candidates = options.entryFileCandidates ?? DEFAULT_ENTRY_CANDIDATES;
|
|
666
|
+
const entryFilePath = await resolveServerEntry(options.projectRoot, candidates);
|
|
667
|
+
if (!entryFilePath) {
|
|
668
|
+
throw new Error(
|
|
669
|
+
`Could not find a server entry file in ${options.projectRoot}. Looked for: ${candidates.join(", ")}`
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
await (0, import_promises4.mkdir)(options.deployDirectory, { recursive: true });
|
|
673
|
+
const outputFilePath = (0, import_node_path4.join)(options.deployDirectory, "server-bundled.js");
|
|
674
|
+
await (0, import_esbuild.build)({
|
|
675
|
+
entryPoints: [entryFilePath],
|
|
676
|
+
outfile: outputFilePath,
|
|
677
|
+
bundle: true,
|
|
678
|
+
platform: "node",
|
|
679
|
+
format: "esm",
|
|
680
|
+
target: ["node20"],
|
|
681
|
+
sourcemap: false,
|
|
682
|
+
logLevel: "silent",
|
|
683
|
+
external: ["better-sqlite3"],
|
|
684
|
+
banner: {
|
|
685
|
+
js: "import { createRequire as __createRequire } from 'module'; const require = __createRequire(import.meta.url);"
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
return {
|
|
689
|
+
entryFilePath,
|
|
690
|
+
outputFilePath
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
async function resolveServerEntry(projectRoot, candidates) {
|
|
694
|
+
for (const candidate of candidates) {
|
|
695
|
+
const fullPath = (0, import_node_path4.join)(projectRoot, candidate);
|
|
696
|
+
try {
|
|
697
|
+
await (0, import_promises5.access)(fullPath);
|
|
698
|
+
return fullPath;
|
|
699
|
+
} catch {
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// src/commands/deploy/adapters/fly-adapter.ts
|
|
706
|
+
var FlyAdapter = class {
|
|
707
|
+
name = "fly";
|
|
708
|
+
logger = createLogger();
|
|
709
|
+
runner;
|
|
710
|
+
commandCandidates;
|
|
711
|
+
flyCommand = null;
|
|
712
|
+
currentContext;
|
|
713
|
+
lastDeploymentId = null;
|
|
714
|
+
constructor(options = {}) {
|
|
715
|
+
this.runner = options.runner ?? new NodeFlyCommandRunner();
|
|
716
|
+
this.commandCandidates = options.commandCandidates ?? DEFAULT_FLY_COMMAND_CANDIDATES;
|
|
717
|
+
this.currentContext = options.context ?? null;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Seeds runtime context for non-provisioning commands.
|
|
721
|
+
*/
|
|
722
|
+
setContext(context) {
|
|
723
|
+
this.currentContext = context;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Checks whether a Fly CLI executable can be resolved.
|
|
727
|
+
*/
|
|
728
|
+
async detect() {
|
|
729
|
+
const command = await this.tryResolveFlyCommand(process.cwd());
|
|
730
|
+
return command !== null;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Ensures Fly CLI is available before deployment.
|
|
734
|
+
*/
|
|
735
|
+
async install() {
|
|
736
|
+
const available = await this.detect();
|
|
737
|
+
if (!available) {
|
|
738
|
+
throw new Error(
|
|
739
|
+
"Fly CLI is required but not installed. Install from https://fly.io/docs/hands-on/install-flyctl/."
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Performs Fly authentication check/login.
|
|
745
|
+
*/
|
|
746
|
+
async authenticate() {
|
|
747
|
+
const projectRoot = this.currentContext?.projectRoot ?? process.cwd();
|
|
748
|
+
const status = await this.runFlyCommand(["auth", "whoami", "--json"], projectRoot, false);
|
|
749
|
+
if (status.exitCode === 0) return;
|
|
750
|
+
const login = await this.runFlyCommand(["auth", "login"], projectRoot, true);
|
|
751
|
+
if (login.exitCode !== 0) {
|
|
752
|
+
throw new Error(`Fly authentication failed: ${normalizeError(login.stderr, login.stdout)}`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Provisions app and optionally region-bound resources in Fly.
|
|
757
|
+
*/
|
|
758
|
+
async provision(config) {
|
|
759
|
+
this.currentContext = {
|
|
760
|
+
projectRoot: config.projectRoot,
|
|
761
|
+
appName: config.appName,
|
|
762
|
+
region: config.region ?? "iad"
|
|
763
|
+
};
|
|
764
|
+
const appCreateArgs = ["apps", "create", config.appName];
|
|
765
|
+
const createApp = await this.runFlyCommand(appCreateArgs, config.projectRoot, false);
|
|
766
|
+
if (createApp.exitCode !== 0 && !isAlreadyExistsResponse(createApp.stderr, createApp.stdout)) {
|
|
767
|
+
throw new Error(
|
|
768
|
+
`Fly app provisioning failed for "${config.appName}": ${normalizeError(createApp.stderr, createApp.stdout)}`
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
const secrets = [];
|
|
772
|
+
const portSecret = await this.runFlyCommand(
|
|
773
|
+
["secrets", "set", "PORT=3000", "--app", config.appName],
|
|
774
|
+
config.projectRoot,
|
|
775
|
+
false
|
|
776
|
+
);
|
|
777
|
+
if (portSecret.exitCode === 0) {
|
|
778
|
+
secrets.push("PORT");
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
applicationId: config.appName,
|
|
782
|
+
databaseId: null,
|
|
783
|
+
secretsSet: secrets
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Builds local artifacts consumed by Fly deploy.
|
|
788
|
+
*/
|
|
789
|
+
async build(config) {
|
|
790
|
+
this.currentContext = {
|
|
791
|
+
projectRoot: config.projectRoot,
|
|
792
|
+
appName: config.appName,
|
|
793
|
+
region: config.region
|
|
794
|
+
};
|
|
795
|
+
const deployDirectory = (0, import_node_path5.join)(config.projectRoot, ".kora", "deploy");
|
|
796
|
+
await writeFlyTomlArtifact(deployDirectory, {
|
|
797
|
+
appName: config.appName,
|
|
798
|
+
region: config.region ?? "iad"
|
|
799
|
+
});
|
|
800
|
+
await bundleServer({
|
|
801
|
+
projectRoot: config.projectRoot,
|
|
802
|
+
deployDirectory
|
|
803
|
+
});
|
|
804
|
+
const client = await buildClient({
|
|
805
|
+
projectRoot: config.projectRoot,
|
|
806
|
+
outDir: (0, import_node_path5.join)(deployDirectory, "dist"),
|
|
807
|
+
mode: "production"
|
|
808
|
+
});
|
|
809
|
+
return {
|
|
810
|
+
clientDirectory: client.outDir,
|
|
811
|
+
serverBundlePath: (0, import_node_path5.join)(deployDirectory, "server-bundled.js"),
|
|
812
|
+
deployDirectory
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Deploys generated artifacts to Fly and returns live endpoints.
|
|
817
|
+
*/
|
|
818
|
+
async deploy(artifacts) {
|
|
819
|
+
const context = this.requireContext();
|
|
820
|
+
const deploy = await this.runFlyCommand(
|
|
821
|
+
["deploy", "--config", "fly.toml", "--app", context.appName],
|
|
822
|
+
artifacts.deployDirectory,
|
|
823
|
+
true
|
|
824
|
+
);
|
|
825
|
+
if (deploy.exitCode !== 0) {
|
|
826
|
+
throw new Error(`Fly deployment failed: ${normalizeError(deploy.stderr, deploy.stdout)}`);
|
|
827
|
+
}
|
|
828
|
+
const info = await this.runFlyCommand(
|
|
829
|
+
["status", "--app", context.appName, "--json"],
|
|
830
|
+
context.projectRoot,
|
|
831
|
+
false
|
|
832
|
+
);
|
|
833
|
+
const hostname = parseFlyHostname(info.stdout) ?? `${context.appName}.fly.dev`;
|
|
834
|
+
const deploymentId = parseFlyDeploymentId(info.stdout) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
835
|
+
this.lastDeploymentId = deploymentId;
|
|
836
|
+
return {
|
|
837
|
+
deploymentId,
|
|
838
|
+
liveUrl: `https://${hostname}`,
|
|
839
|
+
syncUrl: `wss://${hostname}/kora-sync`
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Rolls back to a deployment version.
|
|
844
|
+
*/
|
|
845
|
+
async rollback(deploymentId) {
|
|
846
|
+
const context = this.requireContext();
|
|
847
|
+
const rollback = await this.runFlyCommand(
|
|
848
|
+
["releases", "revert", deploymentId, "--app", context.appName],
|
|
849
|
+
context.projectRoot,
|
|
850
|
+
true
|
|
851
|
+
);
|
|
852
|
+
if (rollback.exitCode !== 0) {
|
|
853
|
+
throw new Error(`Fly rollback failed: ${normalizeError(rollback.stderr, rollback.stdout)}`);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Returns deployment logs as an async iterable.
|
|
858
|
+
*/
|
|
859
|
+
async *logs(options) {
|
|
860
|
+
const context = this.requireContext();
|
|
861
|
+
const args = ["logs", "--app", context.appName, "--no-tail"];
|
|
862
|
+
if (typeof options.since === "string" && options.since.length > 0) {
|
|
863
|
+
args.push("--region", options.since);
|
|
864
|
+
}
|
|
865
|
+
const result = await this.runFlyCommand(args, context.projectRoot, false);
|
|
866
|
+
if (result.exitCode !== 0) {
|
|
867
|
+
throw new Error(`Fly logs failed: ${normalizeError(result.stderr, result.stdout)}`);
|
|
868
|
+
}
|
|
869
|
+
const lines = result.stdout.split(/\r?\n/).filter((line) => line.length > 0);
|
|
870
|
+
for (const line of lines) {
|
|
871
|
+
yield {
|
|
872
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
873
|
+
level: inferLogLevel(line),
|
|
874
|
+
message: line
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Reads current deployment status from Fly.
|
|
880
|
+
*/
|
|
881
|
+
async status() {
|
|
882
|
+
const context = this.requireContext();
|
|
883
|
+
const status = await this.runFlyCommand(
|
|
884
|
+
["status", "--app", context.appName, "--json"],
|
|
885
|
+
context.projectRoot,
|
|
886
|
+
false
|
|
887
|
+
);
|
|
888
|
+
if (status.exitCode !== 0) {
|
|
889
|
+
return {
|
|
890
|
+
state: "failed",
|
|
891
|
+
message: normalizeError(status.stderr, status.stdout)
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
const hostname = parseFlyHostname(status.stdout);
|
|
895
|
+
return {
|
|
896
|
+
state: "healthy",
|
|
897
|
+
message: "Fly deployment is healthy.",
|
|
898
|
+
liveUrl: hostname ? `https://${hostname}` : void 0
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
async runFlyCommand(flyArgs, cwd, inheritOutput) {
|
|
902
|
+
const flyBinary = await this.resolveFlyCommand(cwd);
|
|
903
|
+
if (inheritOutput) {
|
|
904
|
+
this.logger.step(`fly ${flyArgs.join(" ")}`);
|
|
905
|
+
}
|
|
906
|
+
return await this.runner.run(flyBinary, flyArgs, cwd);
|
|
907
|
+
}
|
|
908
|
+
requireContext() {
|
|
909
|
+
if (!this.currentContext) {
|
|
910
|
+
throw new Error("Fly adapter context is not initialized. Run provision() first.");
|
|
911
|
+
}
|
|
912
|
+
return this.currentContext;
|
|
913
|
+
}
|
|
914
|
+
async resolveFlyCommand(cwd) {
|
|
915
|
+
if (this.flyCommand) {
|
|
916
|
+
return this.flyCommand;
|
|
917
|
+
}
|
|
918
|
+
const resolved = await this.tryResolveFlyCommand(cwd);
|
|
919
|
+
if (!resolved) {
|
|
920
|
+
throw new Error("Could not resolve a usable Fly CLI command (tried flyctl, fly).");
|
|
921
|
+
}
|
|
922
|
+
this.flyCommand = resolved;
|
|
923
|
+
return resolved;
|
|
924
|
+
}
|
|
925
|
+
async tryResolveFlyCommand(cwd) {
|
|
926
|
+
for (const command of this.commandCandidates) {
|
|
927
|
+
const versionCheck = await this.runner.run(command, ["version"], cwd);
|
|
928
|
+
if (versionCheck.exitCode === 0) {
|
|
929
|
+
return command;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
var NodeFlyCommandRunner = class {
|
|
936
|
+
async run(command, args, cwd) {
|
|
937
|
+
return await new Promise((resolve3) => {
|
|
938
|
+
const child = (0, import_node_child_process2.spawn)(command, args, {
|
|
939
|
+
cwd,
|
|
940
|
+
env: process.env,
|
|
941
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
942
|
+
});
|
|
943
|
+
let stdout = "";
|
|
944
|
+
let stderr = "";
|
|
945
|
+
child.stdout?.on("data", (chunk) => {
|
|
946
|
+
stdout += chunk.toString("utf-8");
|
|
947
|
+
});
|
|
948
|
+
child.stderr?.on("data", (chunk) => {
|
|
949
|
+
stderr += chunk.toString("utf-8");
|
|
950
|
+
});
|
|
951
|
+
child.on("error", (error) => {
|
|
952
|
+
resolve3({
|
|
953
|
+
exitCode: 1,
|
|
954
|
+
stdout,
|
|
955
|
+
stderr: `${stderr}
|
|
956
|
+
${error.message}`
|
|
957
|
+
});
|
|
958
|
+
});
|
|
959
|
+
child.on("exit", (code) => {
|
|
960
|
+
resolve3({
|
|
961
|
+
exitCode: code ?? 1,
|
|
962
|
+
stdout: stdout.trim(),
|
|
963
|
+
stderr: stderr.trim()
|
|
964
|
+
});
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
var DEFAULT_FLY_COMMAND_CANDIDATES = ["flyctl", "fly"];
|
|
970
|
+
function parseFlyHostname(rawJson) {
|
|
971
|
+
const parsed = parseJsonRecord(rawJson);
|
|
972
|
+
if (!parsed) return null;
|
|
973
|
+
const hostname = parsed.Hostname;
|
|
974
|
+
if (typeof hostname === "string" && hostname.length > 0) {
|
|
975
|
+
return hostname;
|
|
976
|
+
}
|
|
977
|
+
const hostnames = parsed.Hostnames;
|
|
978
|
+
if (Array.isArray(hostnames)) {
|
|
979
|
+
const first = hostnames.find((item) => typeof item === "string");
|
|
980
|
+
if (typeof first === "string" && first.length > 0) {
|
|
981
|
+
return first;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return null;
|
|
985
|
+
}
|
|
986
|
+
function parseFlyDeploymentId(rawJson) {
|
|
987
|
+
const parsed = parseJsonRecord(rawJson);
|
|
988
|
+
if (!parsed) return null;
|
|
989
|
+
const deploymentId = parsed.DeploymentID;
|
|
990
|
+
if (typeof deploymentId === "string" && deploymentId.length > 0) {
|
|
991
|
+
return deploymentId;
|
|
992
|
+
}
|
|
993
|
+
const latestDeployment = parsed.LatestDeployment;
|
|
994
|
+
if (typeof latestDeployment === "object" && latestDeployment !== null) {
|
|
995
|
+
const id = latestDeployment.ID;
|
|
996
|
+
if (typeof id === "string" && id.length > 0) {
|
|
997
|
+
return id;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
function parseJsonRecord(value) {
|
|
1003
|
+
try {
|
|
1004
|
+
const parsed = JSON.parse(value);
|
|
1005
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
1006
|
+
return parsed;
|
|
1007
|
+
}
|
|
1008
|
+
return null;
|
|
1009
|
+
} catch {
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
function inferLogLevel(line) {
|
|
1014
|
+
const normalized = line.toLowerCase();
|
|
1015
|
+
if (normalized.includes("error")) return "error";
|
|
1016
|
+
if (normalized.includes("warn")) return "warn";
|
|
1017
|
+
if (normalized.includes("debug")) return "debug";
|
|
1018
|
+
return "info";
|
|
1019
|
+
}
|
|
1020
|
+
function normalizeError(stderr, stdout) {
|
|
1021
|
+
if (stderr.length > 0) return stderr;
|
|
1022
|
+
if (stdout.length > 0) return stdout;
|
|
1023
|
+
return "unknown fly CLI error";
|
|
1024
|
+
}
|
|
1025
|
+
function isAlreadyExistsResponse(stderr, stdout) {
|
|
1026
|
+
const text = `${stderr}
|
|
1027
|
+
${stdout}`.toLowerCase();
|
|
1028
|
+
return text.includes("already exists") || text.includes("already been taken");
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// src/commands/deploy/adapters/railway-adapter.ts
|
|
1032
|
+
var import_node_child_process3 = require("child_process");
|
|
1033
|
+
var import_node_path7 = require("path");
|
|
1034
|
+
|
|
1035
|
+
// src/commands/deploy/artifacts/railway-json-generator.ts
|
|
1036
|
+
var import_promises6 = require("fs/promises");
|
|
1037
|
+
var import_node_path6 = require("path");
|
|
1038
|
+
var GENERATED_HEADER2 = "Generated by kora deploy - do not edit manually.";
|
|
1039
|
+
function generateRailwayJson(options) {
|
|
1040
|
+
const file = {
|
|
1041
|
+
$schema: "https://railway.app/railway.schema.json",
|
|
1042
|
+
build: {
|
|
1043
|
+
builder: "DOCKERFILE",
|
|
1044
|
+
dockerfilePath: "Dockerfile"
|
|
1045
|
+
},
|
|
1046
|
+
deploy: {
|
|
1047
|
+
startCommand: options.startCommand ?? "node ./server-bundled.js",
|
|
1048
|
+
healthcheckPath: options.healthcheckPath ?? "/health",
|
|
1049
|
+
restartPolicyType: "on_failure",
|
|
1050
|
+
restartPolicyMaxRetries: 3
|
|
1051
|
+
},
|
|
1052
|
+
metadata: {
|
|
1053
|
+
generatedBy: GENERATED_HEADER2,
|
|
1054
|
+
appName: options.appName,
|
|
1055
|
+
region: options.region
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
return `${JSON.stringify(file, null, 2)}
|
|
1059
|
+
`;
|
|
1060
|
+
}
|
|
1061
|
+
async function writeRailwayJsonArtifact(deployDirectory, options) {
|
|
1062
|
+
await (0, import_promises6.mkdir)(deployDirectory, { recursive: true });
|
|
1063
|
+
const filePath = (0, import_node_path6.join)(deployDirectory, "railway.json");
|
|
1064
|
+
await (0, import_promises6.writeFile)(filePath, generateRailwayJson(options), "utf-8");
|
|
1065
|
+
return filePath;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// src/commands/deploy/adapters/railway-adapter.ts
|
|
1069
|
+
var RailwayAdapter = class {
|
|
1070
|
+
name = "railway";
|
|
1071
|
+
logger = createLogger();
|
|
1072
|
+
runner;
|
|
1073
|
+
commandCandidates;
|
|
1074
|
+
railwayCommand = null;
|
|
1075
|
+
currentContext;
|
|
1076
|
+
lastDeploymentId = null;
|
|
1077
|
+
constructor(options = {}) {
|
|
1078
|
+
this.runner = options.runner ?? new NodeRailwayCommandRunner();
|
|
1079
|
+
this.commandCandidates = options.commandCandidates ?? DEFAULT_RAILWAY_COMMAND_CANDIDATES;
|
|
1080
|
+
this.currentContext = options.context ?? null;
|
|
1081
|
+
}
|
|
1082
|
+
setContext(context) {
|
|
1083
|
+
this.currentContext = context;
|
|
1084
|
+
}
|
|
1085
|
+
async detect() {
|
|
1086
|
+
const command = await this.tryResolveRailwayCommand(process.cwd());
|
|
1087
|
+
return command !== null;
|
|
1088
|
+
}
|
|
1089
|
+
async install() {
|
|
1090
|
+
const available = await this.detect();
|
|
1091
|
+
if (!available) {
|
|
1092
|
+
throw new Error(
|
|
1093
|
+
"Railway CLI is required but not installed. Install with `npm i -g @railway/cli` or see https://docs.railway.com/guides/cli."
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
async authenticate() {
|
|
1098
|
+
const projectRoot = this.currentContext?.projectRoot ?? process.cwd();
|
|
1099
|
+
const whoami = await this.runRailwayCommand(["whoami"], projectRoot, false);
|
|
1100
|
+
if (whoami.exitCode === 0) return;
|
|
1101
|
+
const login = await this.runRailwayCommand(["login"], projectRoot, true);
|
|
1102
|
+
if (login.exitCode !== 0) {
|
|
1103
|
+
throw new Error(
|
|
1104
|
+
`Railway authentication failed: ${normalizeError2(login.stderr, login.stdout)}`
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
async provision(config) {
|
|
1109
|
+
this.currentContext = {
|
|
1110
|
+
projectRoot: config.projectRoot,
|
|
1111
|
+
appName: config.appName,
|
|
1112
|
+
region: config.region
|
|
1113
|
+
};
|
|
1114
|
+
const init = await this.runRailwayCommand(
|
|
1115
|
+
["init", "--name", config.appName, "--confirm"],
|
|
1116
|
+
config.projectRoot,
|
|
1117
|
+
false
|
|
1118
|
+
);
|
|
1119
|
+
if (init.exitCode !== 0 && !isAlreadyExistsResponse2(init.stderr, init.stdout)) {
|
|
1120
|
+
throw new Error(
|
|
1121
|
+
`Railway project provisioning failed for "${config.appName}": ${normalizeError2(init.stderr, init.stdout)}`
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
const link = await this.runRailwayCommand(
|
|
1125
|
+
["link", "--project", config.appName, "--environment", config.environment, "--yes"],
|
|
1126
|
+
config.projectRoot,
|
|
1127
|
+
false
|
|
1128
|
+
);
|
|
1129
|
+
if (link.exitCode !== 0 && !isAlreadyExistsResponse2(link.stderr, link.stdout)) {
|
|
1130
|
+
throw new Error(`Railway link failed: ${normalizeError2(link.stderr, link.stdout)}`);
|
|
1131
|
+
}
|
|
1132
|
+
const vars = [];
|
|
1133
|
+
const setPort = await this.runRailwayCommand(
|
|
1134
|
+
["variables", "set", "PORT=3000", "--yes"],
|
|
1135
|
+
config.projectRoot,
|
|
1136
|
+
false
|
|
1137
|
+
);
|
|
1138
|
+
if (setPort.exitCode === 0) {
|
|
1139
|
+
vars.push("PORT");
|
|
1140
|
+
}
|
|
1141
|
+
return {
|
|
1142
|
+
applicationId: config.appName,
|
|
1143
|
+
databaseId: null,
|
|
1144
|
+
secretsSet: vars
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
async build(config) {
|
|
1148
|
+
this.currentContext = {
|
|
1149
|
+
projectRoot: config.projectRoot,
|
|
1150
|
+
appName: config.appName,
|
|
1151
|
+
region: config.region
|
|
1152
|
+
};
|
|
1153
|
+
const deployDirectory = (0, import_node_path7.join)(config.projectRoot, ".kora", "deploy");
|
|
1154
|
+
await writeRailwayJsonArtifact(deployDirectory, {
|
|
1155
|
+
appName: config.appName,
|
|
1156
|
+
environment: config.environment
|
|
1157
|
+
});
|
|
1158
|
+
await bundleServer({
|
|
1159
|
+
projectRoot: config.projectRoot,
|
|
1160
|
+
deployDirectory
|
|
1161
|
+
});
|
|
1162
|
+
const client = await buildClient({
|
|
1163
|
+
projectRoot: config.projectRoot,
|
|
1164
|
+
outDir: (0, import_node_path7.join)(deployDirectory, "dist"),
|
|
1165
|
+
mode: "production"
|
|
1166
|
+
});
|
|
1167
|
+
return {
|
|
1168
|
+
clientDirectory: client.outDir,
|
|
1169
|
+
serverBundlePath: (0, import_node_path7.join)(deployDirectory, "server-bundled.js"),
|
|
1170
|
+
deployDirectory
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
async deploy(artifacts) {
|
|
1174
|
+
const context = this.requireContext();
|
|
1175
|
+
const up = await this.runRailwayCommand(["up", "--yes"], artifacts.deployDirectory, true);
|
|
1176
|
+
if (up.exitCode !== 0) {
|
|
1177
|
+
throw new Error(`Railway deployment failed: ${normalizeError2(up.stderr, up.stdout)}`);
|
|
1178
|
+
}
|
|
1179
|
+
const status = await this.runRailwayCommand(["status", "--json"], context.projectRoot, false);
|
|
1180
|
+
const deploymentId = parseRailwayDeploymentId(status.stdout) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1181
|
+
const liveUrl = parseRailwayUrl(status.stdout) ?? `https://${context.appName}.up.railway.app`;
|
|
1182
|
+
this.lastDeploymentId = deploymentId;
|
|
1183
|
+
return {
|
|
1184
|
+
deploymentId,
|
|
1185
|
+
liveUrl,
|
|
1186
|
+
syncUrl: toSyncUrl(liveUrl)
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
async rollback(deploymentId) {
|
|
1190
|
+
const context = this.requireContext();
|
|
1191
|
+
const rollback = await this.runRailwayCommand(
|
|
1192
|
+
["redeploy", "--deployment", deploymentId, "--yes"],
|
|
1193
|
+
context.projectRoot,
|
|
1194
|
+
true
|
|
1195
|
+
);
|
|
1196
|
+
if (rollback.exitCode !== 0) {
|
|
1197
|
+
throw new Error(
|
|
1198
|
+
`Railway rollback failed: ${normalizeError2(rollback.stderr, rollback.stdout)}`
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
async *logs(options) {
|
|
1203
|
+
const context = this.requireContext();
|
|
1204
|
+
const args = ["logs"];
|
|
1205
|
+
if (typeof options.tail === "number" && options.tail > 0) {
|
|
1206
|
+
args.push("--lines", String(options.tail));
|
|
1207
|
+
}
|
|
1208
|
+
const result = await this.runRailwayCommand(args, context.projectRoot, false);
|
|
1209
|
+
if (result.exitCode !== 0) {
|
|
1210
|
+
throw new Error(`Railway logs failed: ${normalizeError2(result.stderr, result.stdout)}`);
|
|
1211
|
+
}
|
|
1212
|
+
const lines = result.stdout.split(/\r?\n/).filter((line) => line.length > 0);
|
|
1213
|
+
for (const line of lines) {
|
|
1214
|
+
yield {
|
|
1215
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1216
|
+
level: inferLogLevel2(line),
|
|
1217
|
+
message: line
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
async status() {
|
|
1222
|
+
const context = this.requireContext();
|
|
1223
|
+
const status = await this.runRailwayCommand(["status", "--json"], context.projectRoot, false);
|
|
1224
|
+
if (status.exitCode !== 0) {
|
|
1225
|
+
return {
|
|
1226
|
+
state: "failed",
|
|
1227
|
+
message: normalizeError2(status.stderr, status.stdout)
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
const liveUrl = parseRailwayUrl(status.stdout);
|
|
1231
|
+
return {
|
|
1232
|
+
state: "healthy",
|
|
1233
|
+
message: "Railway deployment is healthy.",
|
|
1234
|
+
liveUrl: liveUrl ?? void 0
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
async runRailwayCommand(railwayArgs, cwd, inheritOutput) {
|
|
1238
|
+
const command = await this.resolveRailwayCommand(cwd);
|
|
1239
|
+
if (inheritOutput) {
|
|
1240
|
+
this.logger.step(`railway ${railwayArgs.join(" ")}`);
|
|
1241
|
+
}
|
|
1242
|
+
return await this.runner.run(command, railwayArgs, cwd);
|
|
1243
|
+
}
|
|
1244
|
+
requireContext() {
|
|
1245
|
+
if (!this.currentContext) {
|
|
1246
|
+
throw new Error("Railway adapter context is not initialized. Run provision() first.");
|
|
1247
|
+
}
|
|
1248
|
+
return this.currentContext;
|
|
1249
|
+
}
|
|
1250
|
+
async resolveRailwayCommand(cwd) {
|
|
1251
|
+
if (this.railwayCommand) {
|
|
1252
|
+
return this.railwayCommand;
|
|
1253
|
+
}
|
|
1254
|
+
const resolved = await this.tryResolveRailwayCommand(cwd);
|
|
1255
|
+
if (!resolved) {
|
|
1256
|
+
throw new Error("Could not resolve a usable Railway CLI command (tried railway).");
|
|
1257
|
+
}
|
|
1258
|
+
this.railwayCommand = resolved;
|
|
1259
|
+
return resolved;
|
|
1260
|
+
}
|
|
1261
|
+
async tryResolveRailwayCommand(cwd) {
|
|
1262
|
+
for (const command of this.commandCandidates) {
|
|
1263
|
+
const version = await this.runner.run(command, ["--version"], cwd);
|
|
1264
|
+
if (version.exitCode === 0) {
|
|
1265
|
+
return command;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return null;
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
var NodeRailwayCommandRunner = class {
|
|
1272
|
+
async run(command, args, cwd) {
|
|
1273
|
+
return await new Promise((resolve3) => {
|
|
1274
|
+
const child = (0, import_node_child_process3.spawn)(command, args, {
|
|
1275
|
+
cwd,
|
|
1276
|
+
env: process.env,
|
|
1277
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1278
|
+
});
|
|
1279
|
+
let stdout = "";
|
|
1280
|
+
let stderr = "";
|
|
1281
|
+
child.stdout?.on("data", (chunk) => {
|
|
1282
|
+
stdout += chunk.toString("utf-8");
|
|
1283
|
+
});
|
|
1284
|
+
child.stderr?.on("data", (chunk) => {
|
|
1285
|
+
stderr += chunk.toString("utf-8");
|
|
1286
|
+
});
|
|
1287
|
+
child.on("error", (error) => {
|
|
1288
|
+
resolve3({
|
|
1289
|
+
exitCode: 1,
|
|
1290
|
+
stdout,
|
|
1291
|
+
stderr: `${stderr}
|
|
1292
|
+
${error.message}`
|
|
1293
|
+
});
|
|
1294
|
+
});
|
|
1295
|
+
child.on("exit", (code) => {
|
|
1296
|
+
resolve3({
|
|
1297
|
+
exitCode: code ?? 1,
|
|
1298
|
+
stdout: stdout.trim(),
|
|
1299
|
+
stderr: stderr.trim()
|
|
1300
|
+
});
|
|
1301
|
+
});
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
var DEFAULT_RAILWAY_COMMAND_CANDIDATES = ["railway"];
|
|
1306
|
+
function parseRailwayUrl(rawJson) {
|
|
1307
|
+
const record = parseJsonRecord2(rawJson);
|
|
1308
|
+
if (!record) return null;
|
|
1309
|
+
const url = record.url;
|
|
1310
|
+
if (typeof url === "string" && url.length > 0) {
|
|
1311
|
+
return ensureHttpsUrl(url);
|
|
1312
|
+
}
|
|
1313
|
+
const service = record.service;
|
|
1314
|
+
if (typeof service === "object" && service !== null) {
|
|
1315
|
+
const domain = service.domain;
|
|
1316
|
+
if (typeof domain === "string" && domain.length > 0) {
|
|
1317
|
+
return ensureHttpsUrl(domain);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
return null;
|
|
1321
|
+
}
|
|
1322
|
+
function parseRailwayDeploymentId(rawJson) {
|
|
1323
|
+
const record = parseJsonRecord2(rawJson);
|
|
1324
|
+
if (!record) return null;
|
|
1325
|
+
const deploymentId = record.deploymentId;
|
|
1326
|
+
if (typeof deploymentId === "string" && deploymentId.length > 0) {
|
|
1327
|
+
return deploymentId;
|
|
1328
|
+
}
|
|
1329
|
+
const deployment = record.deployment;
|
|
1330
|
+
if (typeof deployment === "object" && deployment !== null) {
|
|
1331
|
+
const id = deployment.id;
|
|
1332
|
+
if (typeof id === "string" && id.length > 0) {
|
|
1333
|
+
return id;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
return null;
|
|
1337
|
+
}
|
|
1338
|
+
function parseJsonRecord2(value) {
|
|
1339
|
+
try {
|
|
1340
|
+
const parsed = JSON.parse(value);
|
|
1341
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
1342
|
+
return parsed;
|
|
1343
|
+
}
|
|
1344
|
+
return null;
|
|
1345
|
+
} catch {
|
|
1346
|
+
return null;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
function toSyncUrl(liveUrl) {
|
|
1350
|
+
try {
|
|
1351
|
+
const url = new URL(liveUrl);
|
|
1352
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
1353
|
+
url.pathname = "/kora-sync";
|
|
1354
|
+
return url.toString().replace(/\/$/, "");
|
|
1355
|
+
} catch {
|
|
1356
|
+
return null;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
function ensureHttpsUrl(value) {
|
|
1360
|
+
if (value.startsWith("http://") || value.startsWith("https://")) {
|
|
1361
|
+
return value;
|
|
1362
|
+
}
|
|
1363
|
+
return `https://${value}`;
|
|
1364
|
+
}
|
|
1365
|
+
function inferLogLevel2(line) {
|
|
1366
|
+
const normalized = line.toLowerCase();
|
|
1367
|
+
if (normalized.includes("error")) return "error";
|
|
1368
|
+
if (normalized.includes("warn")) return "warn";
|
|
1369
|
+
if (normalized.includes("debug")) return "debug";
|
|
1370
|
+
return "info";
|
|
1371
|
+
}
|
|
1372
|
+
function normalizeError2(stderr, stdout) {
|
|
1373
|
+
if (stderr.length > 0) return stderr;
|
|
1374
|
+
if (stdout.length > 0) return stdout;
|
|
1375
|
+
return "unknown railway CLI error";
|
|
1376
|
+
}
|
|
1377
|
+
function isAlreadyExistsResponse2(stderr, stdout) {
|
|
1378
|
+
const text = `${stderr}
|
|
1379
|
+
${stdout}`.toLowerCase();
|
|
1380
|
+
return text.includes("already exists");
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// src/commands/deploy/adapters/stub-adapter.ts
|
|
1384
|
+
var StubDeployAdapter = class {
|
|
1385
|
+
name;
|
|
1386
|
+
contextLabel;
|
|
1387
|
+
constructor(platform) {
|
|
1388
|
+
this.name = platform;
|
|
1389
|
+
this.contextLabel = `Deploy adapter "${platform}" is not implemented yet.`;
|
|
1390
|
+
}
|
|
1391
|
+
setContext(_context) {
|
|
1392
|
+
}
|
|
1393
|
+
async detect() {
|
|
1394
|
+
return false;
|
|
1395
|
+
}
|
|
1396
|
+
async install() {
|
|
1397
|
+
throw this.notImplementedError();
|
|
1398
|
+
}
|
|
1399
|
+
async authenticate() {
|
|
1400
|
+
throw this.notImplementedError();
|
|
1401
|
+
}
|
|
1402
|
+
async provision(_config) {
|
|
1403
|
+
throw this.notImplementedError();
|
|
1404
|
+
}
|
|
1405
|
+
async build(_config) {
|
|
1406
|
+
throw this.notImplementedError();
|
|
1407
|
+
}
|
|
1408
|
+
async deploy(_artifacts) {
|
|
1409
|
+
throw this.notImplementedError();
|
|
1410
|
+
}
|
|
1411
|
+
async rollback(_deploymentId) {
|
|
1412
|
+
throw this.notImplementedError();
|
|
1413
|
+
}
|
|
1414
|
+
logs(_options) {
|
|
1415
|
+
throw this.notImplementedError();
|
|
1416
|
+
}
|
|
1417
|
+
async status() {
|
|
1418
|
+
return {
|
|
1419
|
+
state: "unknown",
|
|
1420
|
+
message: this.notImplementedMessage()
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
notImplementedError() {
|
|
1424
|
+
return new Error(this.notImplementedMessage());
|
|
1425
|
+
}
|
|
1426
|
+
notImplementedMessage() {
|
|
1427
|
+
return `${this.contextLabel} Start with --platform fly for now.`;
|
|
1428
|
+
}
|
|
1429
|
+
};
|
|
1430
|
+
|
|
1431
|
+
// src/commands/deploy/adapters/factory.ts
|
|
1432
|
+
function createDeployAdapter(platform) {
|
|
1433
|
+
switch (platform) {
|
|
1434
|
+
case "fly":
|
|
1435
|
+
return new FlyAdapter();
|
|
1436
|
+
case "railway":
|
|
1437
|
+
return new RailwayAdapter();
|
|
1438
|
+
case "render":
|
|
1439
|
+
case "docker":
|
|
1440
|
+
case "kora-cloud":
|
|
1441
|
+
return new StubDeployAdapter(platform);
|
|
1442
|
+
default: {
|
|
1443
|
+
const exhaustiveCheck = platform;
|
|
1444
|
+
return exhaustiveCheck;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// src/commands/deploy/artifacts/dockerfile-generator.ts
|
|
1450
|
+
var import_promises7 = require("fs/promises");
|
|
1451
|
+
var import_node_path8 = require("path");
|
|
1452
|
+
var GENERATED_HEADER3 = "# Generated by kora deploy \u2014 do not edit manually";
|
|
1453
|
+
function generateDockerfile(options = {}) {
|
|
1454
|
+
const nodeVersion = options.nodeVersion ?? "20-alpine";
|
|
1455
|
+
const port = options.port ?? 3e3;
|
|
1456
|
+
const clientDirectory = options.clientDirectory ?? "dist";
|
|
1457
|
+
const serverBundleFile = options.serverBundleFile ?? "server-bundled.js";
|
|
1458
|
+
const hasNativeDeps = options.nativeDependencies && Object.keys(options.nativeDependencies).length > 0;
|
|
1459
|
+
const lines = [
|
|
1460
|
+
GENERATED_HEADER3,
|
|
1461
|
+
"",
|
|
1462
|
+
`FROM node:${nodeVersion} AS runtime`,
|
|
1463
|
+
"WORKDIR /app",
|
|
1464
|
+
"ENV NODE_ENV=production",
|
|
1465
|
+
`ENV PORT=${String(port)}`,
|
|
1466
|
+
""
|
|
1467
|
+
];
|
|
1468
|
+
if (hasNativeDeps) {
|
|
1469
|
+
lines.push("# Install build tools for native modules");
|
|
1470
|
+
lines.push("RUN apk add --no-cache python3 make g++");
|
|
1471
|
+
lines.push("");
|
|
1472
|
+
lines.push("COPY package.json ./package.json");
|
|
1473
|
+
lines.push("RUN npm install --omit=dev");
|
|
1474
|
+
lines.push("");
|
|
1475
|
+
}
|
|
1476
|
+
lines.push(`COPY ${clientDirectory} ./dist`);
|
|
1477
|
+
lines.push(`COPY ${serverBundleFile} ./server-bundled.js`);
|
|
1478
|
+
lines.push("");
|
|
1479
|
+
lines.push(`EXPOSE ${String(port)}`);
|
|
1480
|
+
lines.push('CMD ["node", "./server-bundled.js"]');
|
|
1481
|
+
lines.push("");
|
|
1482
|
+
return lines.join("\n");
|
|
1483
|
+
}
|
|
1484
|
+
function generateDeployPackageJson(nativeDependencies) {
|
|
1485
|
+
const pkg = {
|
|
1486
|
+
name: "kora-deploy",
|
|
1487
|
+
version: "1.0.0",
|
|
1488
|
+
private: true,
|
|
1489
|
+
type: "module",
|
|
1490
|
+
dependencies: nativeDependencies
|
|
1491
|
+
};
|
|
1492
|
+
return JSON.stringify(pkg, null, 2) + "\n";
|
|
1493
|
+
}
|
|
1494
|
+
function generateDockerIgnore() {
|
|
1495
|
+
return [
|
|
1496
|
+
GENERATED_HEADER3,
|
|
1497
|
+
"",
|
|
1498
|
+
"node_modules",
|
|
1499
|
+
"npm-debug.log*",
|
|
1500
|
+
"pnpm-debug.log*",
|
|
1501
|
+
"yarn-error.log*",
|
|
1502
|
+
".env",
|
|
1503
|
+
".env.*",
|
|
1504
|
+
".git",
|
|
1505
|
+
".gitignore",
|
|
1506
|
+
"coverage",
|
|
1507
|
+
".turbo",
|
|
1508
|
+
"*.test.ts",
|
|
1509
|
+
"*.spec.ts",
|
|
1510
|
+
""
|
|
1511
|
+
].join("\n");
|
|
1512
|
+
}
|
|
1513
|
+
async function writeDockerfileArtifact(deployDirectory, options = {}) {
|
|
1514
|
+
await (0, import_promises7.mkdir)(deployDirectory, { recursive: true });
|
|
1515
|
+
const dockerfilePath = (0, import_node_path8.join)(deployDirectory, "Dockerfile");
|
|
1516
|
+
await (0, import_promises7.writeFile)(dockerfilePath, generateDockerfile(options), "utf-8");
|
|
1517
|
+
if (options.nativeDependencies && Object.keys(options.nativeDependencies).length > 0) {
|
|
1518
|
+
const pkgJsonPath = (0, import_node_path8.join)(deployDirectory, "package.json");
|
|
1519
|
+
await (0, import_promises7.writeFile)(pkgJsonPath, generateDeployPackageJson(options.nativeDependencies), "utf-8");
|
|
1520
|
+
}
|
|
1521
|
+
return dockerfilePath;
|
|
1522
|
+
}
|
|
1523
|
+
async function writeDockerIgnoreArtifact(deployDirectory) {
|
|
1524
|
+
await (0, import_promises7.mkdir)(deployDirectory, { recursive: true });
|
|
1525
|
+
const dockerIgnorePath = (0, import_node_path8.join)(deployDirectory, ".dockerignore");
|
|
1526
|
+
await (0, import_promises7.writeFile)(dockerIgnorePath, generateDockerIgnore(), "utf-8");
|
|
1527
|
+
return dockerIgnorePath;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// src/commands/deploy/state/deploy-state.ts
|
|
1531
|
+
var import_promises8 = require("fs/promises");
|
|
1532
|
+
var import_node_path9 = require("path");
|
|
1533
|
+
var KORA_DEPLOY_DIRECTORY = (0, import_node_path9.join)(".kora", "deploy");
|
|
1534
|
+
var DEPLOY_STATE_FILENAME = "deploy.json";
|
|
1535
|
+
function resolveDeployDirectory(projectRoot) {
|
|
1536
|
+
return (0, import_node_path9.join)(projectRoot, KORA_DEPLOY_DIRECTORY);
|
|
1537
|
+
}
|
|
1538
|
+
function resolveDeployStatePath(projectRoot) {
|
|
1539
|
+
return (0, import_node_path9.join)(resolveDeployDirectory(projectRoot), DEPLOY_STATE_FILENAME);
|
|
1540
|
+
}
|
|
1541
|
+
async function readDeployState(projectRoot) {
|
|
1542
|
+
const statePath = resolveDeployStatePath(projectRoot);
|
|
1543
|
+
try {
|
|
1544
|
+
const source = await (0, import_promises8.readFile)(statePath, "utf-8");
|
|
1545
|
+
const parsed = JSON.parse(source);
|
|
1546
|
+
return parseDeployState(parsed);
|
|
1547
|
+
} catch (error) {
|
|
1548
|
+
const code = error.code;
|
|
1549
|
+
if (code === "ENOENT") return null;
|
|
1550
|
+
throw error;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
async function writeDeployState(projectRoot, input, now = /* @__PURE__ */ new Date()) {
|
|
1554
|
+
const timestamp = now.toISOString();
|
|
1555
|
+
const state = {
|
|
1556
|
+
platform: input.platform,
|
|
1557
|
+
appName: input.appName,
|
|
1558
|
+
region: input.region,
|
|
1559
|
+
projectRoot: input.projectRoot,
|
|
1560
|
+
liveUrl: input.liveUrl ?? null,
|
|
1561
|
+
syncUrl: input.syncUrl ?? null,
|
|
1562
|
+
databaseId: input.databaseId ?? null,
|
|
1563
|
+
lastDeploymentId: input.lastDeploymentId ?? null,
|
|
1564
|
+
createdAt: timestamp,
|
|
1565
|
+
updatedAt: timestamp
|
|
1566
|
+
};
|
|
1567
|
+
await persistDeployState(projectRoot, state);
|
|
1568
|
+
return state;
|
|
1569
|
+
}
|
|
1570
|
+
async function updateDeployState(projectRoot, patch, now = /* @__PURE__ */ new Date()) {
|
|
1571
|
+
const existing = await readDeployState(projectRoot);
|
|
1572
|
+
if (!existing) {
|
|
1573
|
+
throw new Error("Cannot update deploy state because deploy.json does not exist yet.");
|
|
1574
|
+
}
|
|
1575
|
+
const nextState = {
|
|
1576
|
+
platform: patch.platform ?? existing.platform,
|
|
1577
|
+
appName: patch.appName ?? existing.appName,
|
|
1578
|
+
region: patch.region === void 0 ? existing.region : patch.region,
|
|
1579
|
+
projectRoot: patch.projectRoot ?? existing.projectRoot,
|
|
1580
|
+
liveUrl: patch.liveUrl === void 0 ? existing.liveUrl : patch.liveUrl,
|
|
1581
|
+
syncUrl: patch.syncUrl === void 0 ? existing.syncUrl : patch.syncUrl,
|
|
1582
|
+
databaseId: patch.databaseId === void 0 ? existing.databaseId : patch.databaseId,
|
|
1583
|
+
lastDeploymentId: patch.lastDeploymentId === void 0 ? existing.lastDeploymentId : patch.lastDeploymentId,
|
|
1584
|
+
createdAt: existing.createdAt,
|
|
1585
|
+
updatedAt: now.toISOString()
|
|
1586
|
+
};
|
|
1587
|
+
await persistDeployState(projectRoot, nextState);
|
|
1588
|
+
return nextState;
|
|
1589
|
+
}
|
|
1590
|
+
async function resetDeployState(projectRoot) {
|
|
1591
|
+
await (0, import_promises8.rm)(resolveDeployDirectory(projectRoot), { recursive: true, force: true });
|
|
1592
|
+
}
|
|
1593
|
+
async function persistDeployState(projectRoot, state) {
|
|
1594
|
+
const deployDir = resolveDeployDirectory(projectRoot);
|
|
1595
|
+
const statePath = resolveDeployStatePath(projectRoot);
|
|
1596
|
+
await (0, import_promises8.mkdir)(deployDir, { recursive: true });
|
|
1597
|
+
await (0, import_promises8.writeFile)(statePath, `${JSON.stringify(state, null, 2)}
|
|
1598
|
+
`, "utf-8");
|
|
1599
|
+
}
|
|
1600
|
+
function parseDeployState(value) {
|
|
1601
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1602
|
+
throw new Error("Invalid deploy state: expected an object.");
|
|
1603
|
+
}
|
|
1604
|
+
const record = value;
|
|
1605
|
+
const platform = readPlatform(record.platform);
|
|
1606
|
+
const appName = readString(record.appName, "appName");
|
|
1607
|
+
const region = readOptionalString(record.region, "region");
|
|
1608
|
+
const projectRoot = readString(record.projectRoot, "projectRoot");
|
|
1609
|
+
const liveUrl = readOptionalString(record.liveUrl, "liveUrl");
|
|
1610
|
+
const syncUrl = readOptionalString(record.syncUrl, "syncUrl");
|
|
1611
|
+
const databaseId = readOptionalString(record.databaseId, "databaseId");
|
|
1612
|
+
const lastDeploymentId = readOptionalString(record.lastDeploymentId, "lastDeploymentId");
|
|
1613
|
+
const createdAt = readString(record.createdAt, "createdAt");
|
|
1614
|
+
const updatedAt = readString(record.updatedAt, "updatedAt");
|
|
1615
|
+
return {
|
|
1616
|
+
platform,
|
|
1617
|
+
appName,
|
|
1618
|
+
region,
|
|
1619
|
+
projectRoot,
|
|
1620
|
+
liveUrl,
|
|
1621
|
+
syncUrl,
|
|
1622
|
+
databaseId,
|
|
1623
|
+
lastDeploymentId,
|
|
1624
|
+
createdAt,
|
|
1625
|
+
updatedAt
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
function readPlatform(value) {
|
|
1629
|
+
if (typeof value !== "string" || !isDeployPlatform(value)) {
|
|
1630
|
+
throw new Error('Invalid deploy state: "platform" must be a supported platform.');
|
|
1631
|
+
}
|
|
1632
|
+
return value;
|
|
1633
|
+
}
|
|
1634
|
+
function readString(value, field) {
|
|
1635
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1636
|
+
throw new Error(`Invalid deploy state: "${field}" must be a non-empty string.`);
|
|
1637
|
+
}
|
|
1638
|
+
return value;
|
|
1639
|
+
}
|
|
1640
|
+
function readOptionalString(value, field) {
|
|
1641
|
+
if (value === null) return null;
|
|
1642
|
+
if (typeof value === "string") return value;
|
|
1643
|
+
throw new Error(`Invalid deploy state: "${field}" must be a string or null.`);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// src/commands/deploy/deploy-command.ts
|
|
1647
|
+
var deployCommand = (0, import_citty.defineCommand)({
|
|
1648
|
+
meta: {
|
|
1649
|
+
name: "deploy",
|
|
1650
|
+
description: "Deploy a Kora project to your selected platform"
|
|
1651
|
+
},
|
|
1652
|
+
args: {
|
|
1653
|
+
platform: {
|
|
1654
|
+
type: "string",
|
|
1655
|
+
description: `Deployment platform (${DEPLOY_PLATFORMS.join(", ")})`
|
|
1656
|
+
},
|
|
1657
|
+
app: {
|
|
1658
|
+
type: "string",
|
|
1659
|
+
description: "Application name used by the target platform"
|
|
1660
|
+
},
|
|
1661
|
+
region: {
|
|
1662
|
+
type: "string",
|
|
1663
|
+
description: "Preferred deployment region (for example: iad, lhr, syd)"
|
|
1664
|
+
},
|
|
1665
|
+
prod: {
|
|
1666
|
+
type: "boolean",
|
|
1667
|
+
description: "Deploy to production environment",
|
|
1668
|
+
default: false
|
|
1669
|
+
},
|
|
1670
|
+
confirm: {
|
|
1671
|
+
type: "boolean",
|
|
1672
|
+
description: "Non-interactive mode (fail fast on missing required data)",
|
|
1673
|
+
default: false
|
|
1674
|
+
},
|
|
1675
|
+
reset: {
|
|
1676
|
+
type: "boolean",
|
|
1677
|
+
description: "Delete .kora/deploy state and generated artifacts",
|
|
1678
|
+
default: false
|
|
1679
|
+
}
|
|
1680
|
+
},
|
|
1681
|
+
subCommands: {
|
|
1682
|
+
status: (0, import_citty.defineCommand)({
|
|
1683
|
+
meta: {
|
|
1684
|
+
name: "status",
|
|
1685
|
+
description: "Show current deployment status"
|
|
1686
|
+
},
|
|
1687
|
+
async run() {
|
|
1688
|
+
const logger = createLogger();
|
|
1689
|
+
const projectRoot = await requireProjectRoot();
|
|
1690
|
+
const state = await readDeployState(projectRoot);
|
|
1691
|
+
if (!state) {
|
|
1692
|
+
logger.warn("No deployment state found. Run `kora deploy` first.");
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
logger.banner();
|
|
1696
|
+
logger.info(`Platform: ${state.platform}`);
|
|
1697
|
+
logger.step(`App: ${state.appName}`);
|
|
1698
|
+
logger.step(`Region: ${state.region ?? "n/a"}`);
|
|
1699
|
+
logger.step(`Last deployment: ${state.lastDeploymentId ?? "n/a"}`);
|
|
1700
|
+
const adapter = createDeployAdapter(state.platform);
|
|
1701
|
+
configureAdapterContext(adapter, {
|
|
1702
|
+
projectRoot,
|
|
1703
|
+
appName: state.appName,
|
|
1704
|
+
region: state.region
|
|
1705
|
+
});
|
|
1706
|
+
const adapterStatus = await adapter.status();
|
|
1707
|
+
logger.step(`Status: ${adapterStatus.state}`);
|
|
1708
|
+
logger.step(`Message: ${adapterStatus.message}`);
|
|
1709
|
+
logger.step(`Live URL: ${adapterStatus.liveUrl ?? state.liveUrl ?? "n/a"}`);
|
|
1710
|
+
logger.step(`Sync URL: ${state.syncUrl ?? "n/a"}`);
|
|
1711
|
+
}
|
|
1712
|
+
}),
|
|
1713
|
+
rollback: (0, import_citty.defineCommand)({
|
|
1714
|
+
meta: {
|
|
1715
|
+
name: "rollback",
|
|
1716
|
+
description: "Rollback the current deployment"
|
|
1717
|
+
},
|
|
1718
|
+
args: {
|
|
1719
|
+
id: {
|
|
1720
|
+
type: "positional",
|
|
1721
|
+
description: "Optional deployment identifier",
|
|
1722
|
+
required: false
|
|
1723
|
+
}
|
|
1724
|
+
},
|
|
1725
|
+
async run({ args }) {
|
|
1726
|
+
const logger = createLogger();
|
|
1727
|
+
const projectRoot = await requireProjectRoot();
|
|
1728
|
+
const state = await readDeployState(projectRoot);
|
|
1729
|
+
if (!state) {
|
|
1730
|
+
logger.warn("No deployment state found. Run `kora deploy` first.");
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
const adapter = createDeployAdapter(state.platform);
|
|
1734
|
+
configureAdapterContext(adapter, {
|
|
1735
|
+
projectRoot,
|
|
1736
|
+
appName: state.appName,
|
|
1737
|
+
region: state.region
|
|
1738
|
+
});
|
|
1739
|
+
const deploymentId = typeof args.id === "string" && args.id.length > 0 ? args.id : state.lastDeploymentId ?? "latest";
|
|
1740
|
+
await adapter.rollback(deploymentId);
|
|
1741
|
+
logger.success(`Rolled back ${state.platform} deployment to ${deploymentId}.`);
|
|
1742
|
+
}
|
|
1743
|
+
}),
|
|
1744
|
+
logs: (0, import_citty.defineCommand)({
|
|
1745
|
+
meta: {
|
|
1746
|
+
name: "logs",
|
|
1747
|
+
description: "Read deployment logs"
|
|
1748
|
+
},
|
|
1749
|
+
async run() {
|
|
1750
|
+
const logger = createLogger();
|
|
1751
|
+
const projectRoot = await requireProjectRoot();
|
|
1752
|
+
const state = await readDeployState(projectRoot);
|
|
1753
|
+
if (!state) {
|
|
1754
|
+
logger.warn("No deployment state found. Run `kora deploy` first.");
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
const adapter = createDeployAdapter(state.platform);
|
|
1758
|
+
configureAdapterContext(adapter, {
|
|
1759
|
+
projectRoot,
|
|
1760
|
+
appName: state.appName,
|
|
1761
|
+
region: state.region
|
|
1762
|
+
});
|
|
1763
|
+
const logLines = adapter.logs({ tail: 200 });
|
|
1764
|
+
let hasLines = false;
|
|
1765
|
+
for await (const line of logLines) {
|
|
1766
|
+
hasLines = true;
|
|
1767
|
+
logger.step(`[${line.level}] ${line.message}`);
|
|
1768
|
+
}
|
|
1769
|
+
if (!hasLines) {
|
|
1770
|
+
logger.warn(`No logs returned from ${state.platform}.`);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
})
|
|
1774
|
+
},
|
|
1775
|
+
async run({ args, rawArgs }) {
|
|
1776
|
+
const subCommandNames = ["status", "rollback", "logs"];
|
|
1777
|
+
if (rawArgs.some((arg) => subCommandNames.includes(arg))) {
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
const logger = createLogger();
|
|
1781
|
+
const prompts = createPromptClient();
|
|
1782
|
+
const projectRoot = await requireProjectRoot();
|
|
1783
|
+
if (args.reset === true) {
|
|
1784
|
+
await resetDeployState(projectRoot);
|
|
1785
|
+
logger.success("Reset .kora/deploy state.");
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
const existingState = await readDeployState(projectRoot);
|
|
1789
|
+
const confirmMode = args.confirm === true;
|
|
1790
|
+
const platform = await resolvePlatform({
|
|
1791
|
+
promptClient: prompts,
|
|
1792
|
+
platformArg: args.platform,
|
|
1793
|
+
storedPlatform: existingState?.platform,
|
|
1794
|
+
confirm: confirmMode
|
|
1795
|
+
});
|
|
1796
|
+
const appName = resolveAppName(args.app, existingState?.appName, projectRoot, confirmMode);
|
|
1797
|
+
const region = resolveRegion(args.region, existingState?.region, confirmMode);
|
|
1798
|
+
const deployDirectory = resolveDeployDirectory(projectRoot);
|
|
1799
|
+
const environment = args.prod === true ? "production" : "preview";
|
|
1800
|
+
const config = {
|
|
1801
|
+
projectRoot,
|
|
1802
|
+
appName,
|
|
1803
|
+
region,
|
|
1804
|
+
environment,
|
|
1805
|
+
confirm: confirmMode
|
|
1806
|
+
};
|
|
1807
|
+
const adapter = createDeployAdapter(platform);
|
|
1808
|
+
configureAdapterContext(adapter, {
|
|
1809
|
+
projectRoot,
|
|
1810
|
+
appName,
|
|
1811
|
+
region
|
|
1812
|
+
});
|
|
1813
|
+
logger.banner();
|
|
1814
|
+
logger.info(
|
|
1815
|
+
`Deploying to ${platform} (${appName}${region ? `, ${region}` : ""}, ${environment})`
|
|
1816
|
+
);
|
|
1817
|
+
if (confirmMode) {
|
|
1818
|
+
logger.step("Running in --confirm mode (non-interactive, fail-fast).");
|
|
1819
|
+
}
|
|
1820
|
+
await writeDockerfileArtifact(deployDirectory, {
|
|
1821
|
+
nativeDependencies: {
|
|
1822
|
+
"better-sqlite3": "^11.0.0",
|
|
1823
|
+
"drizzle-orm": "^0.45.2"
|
|
1824
|
+
}
|
|
1825
|
+
});
|
|
1826
|
+
await writeDockerIgnoreArtifact(deployDirectory);
|
|
1827
|
+
const detected = await adapter.detect();
|
|
1828
|
+
if (!detected) {
|
|
1829
|
+
await adapter.install();
|
|
1830
|
+
}
|
|
1831
|
+
await adapter.authenticate();
|
|
1832
|
+
const provisionResult = await adapter.provision(config);
|
|
1833
|
+
const artifacts = await adapter.build(config);
|
|
1834
|
+
const deployResult = await adapter.deploy(artifacts);
|
|
1835
|
+
if (existingState) {
|
|
1836
|
+
await updateDeployState(projectRoot, {
|
|
1837
|
+
platform,
|
|
1838
|
+
appName,
|
|
1839
|
+
region,
|
|
1840
|
+
projectRoot,
|
|
1841
|
+
liveUrl: deployResult.liveUrl,
|
|
1842
|
+
syncUrl: deployResult.syncUrl,
|
|
1843
|
+
databaseId: provisionResult.databaseId,
|
|
1844
|
+
lastDeploymentId: deployResult.deploymentId
|
|
1845
|
+
});
|
|
1846
|
+
} else {
|
|
1847
|
+
await writeDeployState(projectRoot, {
|
|
1848
|
+
platform,
|
|
1849
|
+
appName,
|
|
1850
|
+
region,
|
|
1851
|
+
projectRoot,
|
|
1852
|
+
liveUrl: deployResult.liveUrl,
|
|
1853
|
+
syncUrl: deployResult.syncUrl,
|
|
1854
|
+
databaseId: provisionResult.databaseId,
|
|
1855
|
+
lastDeploymentId: deployResult.deploymentId
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
logger.success(`Deployment completed: ${deployResult.liveUrl}`);
|
|
1859
|
+
if (deployResult.syncUrl) {
|
|
1860
|
+
logger.step(`Sync endpoint: ${deployResult.syncUrl}`);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
});
|
|
1864
|
+
async function resolvePlatform(options) {
|
|
1865
|
+
if (typeof options.platformArg === "string") {
|
|
1866
|
+
if (!isDeployPlatform(options.platformArg)) {
|
|
1867
|
+
throw new Error(
|
|
1868
|
+
`Invalid --platform value "${options.platformArg}". Valid options: ${DEPLOY_PLATFORMS.join(", ")}`
|
|
1869
|
+
);
|
|
1870
|
+
}
|
|
1871
|
+
return options.platformArg;
|
|
1872
|
+
}
|
|
1873
|
+
if (options.storedPlatform) {
|
|
1874
|
+
return options.storedPlatform;
|
|
1875
|
+
}
|
|
1876
|
+
if (options.confirm || !isInteractiveTerminal()) {
|
|
1877
|
+
throw new Error(
|
|
1878
|
+
"Missing deploy platform in --confirm mode. Provide --platform or run an interactive deploy first."
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
return await options.promptClient.select("Where do you want to deploy?", [
|
|
1882
|
+
{
|
|
1883
|
+
label: "Fly.io (recommended for sync apps)",
|
|
1884
|
+
value: "fly"
|
|
1885
|
+
},
|
|
1886
|
+
{
|
|
1887
|
+
label: "Railway",
|
|
1888
|
+
value: "railway"
|
|
1889
|
+
},
|
|
1890
|
+
{
|
|
1891
|
+
label: "Render",
|
|
1892
|
+
value: "render"
|
|
1893
|
+
},
|
|
1894
|
+
{
|
|
1895
|
+
label: "Docker (self-hosted)",
|
|
1896
|
+
value: "docker"
|
|
1897
|
+
},
|
|
1898
|
+
{
|
|
1899
|
+
label: "Kora Cloud (coming soon)",
|
|
1900
|
+
value: "kora-cloud"
|
|
1901
|
+
}
|
|
1902
|
+
]);
|
|
1903
|
+
}
|
|
1904
|
+
function resolveAppName(argValue, storedValue, projectRoot, confirm) {
|
|
1905
|
+
if (typeof argValue === "string" && argValue.length > 0) {
|
|
1906
|
+
return sanitizeAppName(argValue);
|
|
1907
|
+
}
|
|
1908
|
+
if (storedValue && storedValue.length > 0) {
|
|
1909
|
+
return storedValue;
|
|
1910
|
+
}
|
|
1911
|
+
if (confirm) {
|
|
1912
|
+
throw new Error(
|
|
1913
|
+
"Missing app name in --confirm mode. Provide --app or run an interactive deploy first."
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
return sanitizeAppName((0, import_node_path10.basename)(projectRoot));
|
|
1917
|
+
}
|
|
1918
|
+
function resolveRegion(argValue, storedValue, confirm) {
|
|
1919
|
+
if (typeof argValue === "string" && argValue.length > 0) return argValue;
|
|
1920
|
+
if (storedValue !== void 0) return storedValue;
|
|
1921
|
+
if (confirm) {
|
|
1922
|
+
throw new Error(
|
|
1923
|
+
"Missing region in --confirm mode. Provide --region or run an interactive deploy first."
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
return "iad";
|
|
1927
|
+
}
|
|
1928
|
+
function sanitizeAppName(value) {
|
|
1929
|
+
const normalized = value.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
|
|
1930
|
+
if (normalized.length === 0) {
|
|
1931
|
+
return "kora-app";
|
|
1932
|
+
}
|
|
1933
|
+
return normalized;
|
|
1934
|
+
}
|
|
1935
|
+
async function requireProjectRoot() {
|
|
1936
|
+
const projectRoot = await findProjectRoot();
|
|
1937
|
+
if (!projectRoot) {
|
|
1938
|
+
throw new InvalidProjectError(process.cwd());
|
|
1939
|
+
}
|
|
1940
|
+
return projectRoot;
|
|
1941
|
+
}
|
|
1942
|
+
function isInteractiveTerminal() {
|
|
1943
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
1944
|
+
}
|
|
1945
|
+
function configureAdapterContext(adapter, context) {
|
|
1946
|
+
if (hasContextSetter(adapter)) {
|
|
1947
|
+
adapter.setContext(context);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
function hasContextSetter(adapter) {
|
|
1951
|
+
return typeof adapter.setContext === "function";
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
// src/commands/create/project-name.ts
|
|
1955
|
+
var import_validate_npm_package_name = __toESM(require("validate-npm-package-name"), 1);
|
|
1956
|
+
function validateProjectName(name) {
|
|
1957
|
+
const trimmedName = name.trim();
|
|
1958
|
+
if (trimmedName.length === 0) {
|
|
1959
|
+
return {
|
|
1960
|
+
valid: false,
|
|
1961
|
+
issues: ["Project name cannot be empty."]
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
const validation = (0, import_validate_npm_package_name.default)(trimmedName);
|
|
1965
|
+
const issues = [...validation.errors ?? [], ...validation.warnings ?? []];
|
|
1966
|
+
if (!validation.validForNewPackages && issues.length === 0) {
|
|
1967
|
+
return {
|
|
1968
|
+
valid: false,
|
|
1969
|
+
issues: ["Project name is not a valid npm package name."]
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
return {
|
|
1973
|
+
valid: validation.validForNewPackages,
|
|
1974
|
+
issues
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// src/commands/create/options.ts
|
|
1979
|
+
function determineTemplateFromSelections(input) {
|
|
1980
|
+
const shouldSync = input.sync && input.db !== "none";
|
|
1981
|
+
if (input.tailwind && shouldSync) return "react-tailwind-sync";
|
|
1982
|
+
if (input.tailwind && !shouldSync) return "react-tailwind";
|
|
1983
|
+
if (!input.tailwind && shouldSync) return "react-sync";
|
|
1984
|
+
return "react-basic";
|
|
1985
|
+
}
|
|
1986
|
+
function isFrameworkValue(value) {
|
|
1987
|
+
return value === "react" || value === "vue" || value === "svelte" || value === "solid";
|
|
1988
|
+
}
|
|
1989
|
+
function isAuthValue(value) {
|
|
1990
|
+
return value === "none" || value === "email-password" || value === "oauth";
|
|
1991
|
+
}
|
|
1992
|
+
function isDatabaseValue(value) {
|
|
1993
|
+
return value === "none" || value === "sqlite" || value === "postgres";
|
|
1994
|
+
}
|
|
1995
|
+
function isDatabaseProviderValue(value) {
|
|
1996
|
+
return value === "none" || value === "local" || value === "supabase" || value === "neon" || value === "railway" || value === "vercel-postgres" || value === "custom";
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
// src/prompts/preferences.ts
|
|
2000
|
+
var import_conf = __toESM(require("conf"), 1);
|
|
2001
|
+
var DEFAULT_PREFERENCES = {
|
|
2002
|
+
framework: "react",
|
|
2003
|
+
tailwind: true,
|
|
2004
|
+
sync: true,
|
|
2005
|
+
db: "sqlite",
|
|
2006
|
+
dbProvider: "none",
|
|
2007
|
+
auth: "none",
|
|
2008
|
+
packageManager: "pnpm"
|
|
2009
|
+
};
|
|
2010
|
+
var PREFERENCES_KEY = "create.defaults";
|
|
2011
|
+
var PreferenceStore = class {
|
|
2012
|
+
store;
|
|
2013
|
+
constructor() {
|
|
2014
|
+
this.store = new import_conf.default({
|
|
2015
|
+
projectName: "korajs-cli"
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
getCreatePreferences() {
|
|
2019
|
+
return this.store.get(PREFERENCES_KEY) ?? null;
|
|
2020
|
+
}
|
|
2021
|
+
saveCreatePreferences(preferences) {
|
|
2022
|
+
this.store.set(PREFERENCES_KEY, preferences);
|
|
2023
|
+
}
|
|
2024
|
+
clearCreatePreferences() {
|
|
2025
|
+
this.store.delete(PREFERENCES_KEY);
|
|
2026
|
+
}
|
|
2027
|
+
};
|
|
2028
|
+
function getCreatePreferencesOrDefault(store) {
|
|
2029
|
+
return store.getCreatePreferences() ?? DEFAULT_PREFERENCES;
|
|
2030
|
+
}
|
|
2031
|
+
function getDefaultCreatePreferences() {
|
|
2032
|
+
return { ...DEFAULT_PREFERENCES };
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
// src/commands/create/preferences-flow.ts
|
|
2036
|
+
async function resolveCreatePreferencesFlow(params) {
|
|
2037
|
+
const { flags, prompts, store } = params;
|
|
2038
|
+
const stored = store.getCreatePreferences();
|
|
2039
|
+
const base = getCreatePreferencesOrDefault(store);
|
|
2040
|
+
const hasExplicitFlags = hasExplicitPreferenceFlags(flags);
|
|
2041
|
+
const canOfferStored = !flags.useDefaults && !hasExplicitFlags && stored !== null && promptSupportsRichOptions();
|
|
2042
|
+
let effective = { ...base };
|
|
2043
|
+
let usedStoredPreferences = false;
|
|
2044
|
+
if (flags.useDefaults) {
|
|
2045
|
+
effective = getDefaultCreatePreferences();
|
|
2046
|
+
} else if (canOfferStored && stored !== null) {
|
|
2047
|
+
const reuseStored = await prompts.select("Welcome back! Choose setup mode:", [
|
|
2048
|
+
{ label: formatStoredPreferenceLabel(stored), value: "reuse" },
|
|
2049
|
+
{ label: "Customize", value: "customize" }
|
|
2050
|
+
]);
|
|
2051
|
+
if (reuseStored === "reuse") {
|
|
2052
|
+
effective = { ...stored };
|
|
2053
|
+
usedStoredPreferences = true;
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
if (flags.framework !== void 0) {
|
|
2057
|
+
if (!isFrameworkValue(flags.framework)) {
|
|
2058
|
+
throw new Error(
|
|
2059
|
+
`Invalid --framework value "${flags.framework}". Expected one of: react, vue, svelte, solid.`
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
effective.framework = flags.framework;
|
|
2063
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
2064
|
+
effective.framework = await prompts.select("UI framework:", [
|
|
2065
|
+
{ label: "React", value: "react" },
|
|
2066
|
+
{ label: "Vue (coming soon)", value: "vue", disabled: true },
|
|
2067
|
+
{ label: "Svelte (coming soon)", value: "svelte", disabled: true },
|
|
2068
|
+
{ label: "Solid (coming soon)", value: "solid", disabled: true }
|
|
2069
|
+
]);
|
|
2070
|
+
}
|
|
2071
|
+
if (flags.auth !== void 0) {
|
|
2072
|
+
if (!isAuthValue(flags.auth)) {
|
|
2073
|
+
throw new Error(
|
|
2074
|
+
`Invalid --auth value "${flags.auth}". Expected one of: none, email-password, oauth.`
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
effective.auth = flags.auth;
|
|
2078
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
2079
|
+
effective.auth = await prompts.select("Authentication:", [
|
|
2080
|
+
{ label: "None", value: "none" },
|
|
2081
|
+
{ label: "Email + Password (coming soon)", value: "email-password", disabled: true },
|
|
2082
|
+
{ label: "OAuth (coming soon)", value: "oauth", disabled: true }
|
|
2083
|
+
]);
|
|
2084
|
+
}
|
|
2085
|
+
if (flags.tailwind !== void 0) {
|
|
2086
|
+
effective.tailwind = flags.tailwind;
|
|
2087
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
2088
|
+
effective.tailwind = await prompts.confirm("Use Tailwind CSS?", true);
|
|
2089
|
+
}
|
|
2090
|
+
if (flags.sync !== void 0) {
|
|
2091
|
+
effective.sync = flags.sync;
|
|
2092
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
2093
|
+
effective.sync = await prompts.confirm("Enable multi-device sync?", true);
|
|
2094
|
+
}
|
|
2095
|
+
if (flags.db !== void 0) {
|
|
2096
|
+
if (!isDatabaseValue(flags.db)) {
|
|
2097
|
+
throw new Error(`Invalid --db value "${flags.db}". Expected one of: none, sqlite, postgres.`);
|
|
2098
|
+
}
|
|
2099
|
+
effective.db = flags.db;
|
|
2100
|
+
} else if (!effective.sync) {
|
|
2101
|
+
effective.db = "none";
|
|
2102
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
2103
|
+
effective.db = await prompts.select("Server-side database:", [
|
|
2104
|
+
{ label: "SQLite (zero-config)", value: "sqlite" },
|
|
2105
|
+
{ label: "PostgreSQL (production-scale)", value: "postgres" }
|
|
2106
|
+
]);
|
|
2107
|
+
}
|
|
2108
|
+
if (effective.db !== "postgres") {
|
|
2109
|
+
effective.dbProvider = "none";
|
|
2110
|
+
} else if (flags.dbProvider !== void 0) {
|
|
2111
|
+
if (!isDatabaseProviderValue(flags.dbProvider)) {
|
|
2112
|
+
throw new Error(
|
|
2113
|
+
`Invalid --db-provider value "${flags.dbProvider}". Expected one of: none, local, supabase, neon, railway, vercel-postgres, custom.`
|
|
2114
|
+
);
|
|
2115
|
+
}
|
|
2116
|
+
effective.dbProvider = flags.dbProvider;
|
|
2117
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
2118
|
+
effective.dbProvider = await prompts.select("Database provider:", [
|
|
2119
|
+
{ label: "Local Postgres", value: "local" },
|
|
2120
|
+
{ label: "Supabase", value: "supabase" },
|
|
2121
|
+
{ label: "Neon", value: "neon" },
|
|
2122
|
+
{ label: "Railway", value: "railway" },
|
|
2123
|
+
{ label: "Vercel Postgres", value: "vercel-postgres" },
|
|
2124
|
+
{ label: "Custom connection string", value: "custom" }
|
|
2125
|
+
]);
|
|
2126
|
+
}
|
|
2127
|
+
const template = determineTemplateFromSelections({
|
|
2128
|
+
tailwind: effective.tailwind,
|
|
2129
|
+
sync: effective.sync,
|
|
2130
|
+
db: effective.db
|
|
2131
|
+
});
|
|
2132
|
+
return {
|
|
2133
|
+
framework: effective.framework,
|
|
2134
|
+
auth: effective.auth,
|
|
2135
|
+
db: effective.db,
|
|
2136
|
+
dbProvider: effective.dbProvider,
|
|
2137
|
+
tailwind: effective.tailwind,
|
|
2138
|
+
sync: effective.sync,
|
|
2139
|
+
template,
|
|
2140
|
+
usedStoredPreferences
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
function shouldSavePreferences(flags) {
|
|
2144
|
+
return !flags.useDefaults;
|
|
2145
|
+
}
|
|
2146
|
+
function saveResolvedPreferences(store, resolution) {
|
|
2147
|
+
store.saveCreatePreferences({
|
|
2148
|
+
framework: resolution.framework,
|
|
2149
|
+
tailwind: resolution.tailwind,
|
|
2150
|
+
sync: resolution.sync,
|
|
2151
|
+
db: resolution.db,
|
|
2152
|
+
dbProvider: resolution.dbProvider,
|
|
2153
|
+
auth: resolution.auth,
|
|
2154
|
+
packageManager: resolution.packageManager
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
function hasExplicitPreferenceFlags(flags) {
|
|
2158
|
+
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;
|
|
2159
|
+
}
|
|
2160
|
+
function promptSupportsRichOptions() {
|
|
2161
|
+
return typeof process !== "undefined" && process.stdin.isTTY && process.stdout.isTTY;
|
|
2162
|
+
}
|
|
2163
|
+
function formatStoredPreferenceLabel(preferences) {
|
|
2164
|
+
const syncLabel = preferences.sync ? `sync/${preferences.db}` : "local-only";
|
|
2165
|
+
const styleLabel = preferences.tailwind ? "tailwind" : "css";
|
|
2166
|
+
return `Use previous settings (${preferences.framework} + ${styleLabel} + ${syncLabel} + ${preferences.packageManager})`;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
// src/commands/create/sync-provider-preset.ts
|
|
2170
|
+
var import_promises9 = require("fs/promises");
|
|
2171
|
+
var import_node_path11 = require("path");
|
|
2172
|
+
async function applySyncProviderPreset(options) {
|
|
2173
|
+
if (!isSyncTemplate(options.template)) {
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
if (options.db !== "postgres") {
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
const providerName = getProviderDisplayName(options.dbProvider);
|
|
2180
|
+
const providerConnectionString = getProviderConnectionStringExample(options.dbProvider);
|
|
2181
|
+
const serverPath = (0, import_node_path11.join)(options.targetDir, "server.ts");
|
|
2182
|
+
const envPath = (0, import_node_path11.join)(options.targetDir, ".env.example");
|
|
2183
|
+
const readmePath = (0, import_node_path11.join)(options.targetDir, "README.md");
|
|
2184
|
+
const serverTemplate = [
|
|
2185
|
+
"import { createPostgresServerStore, createProductionServer } from '@korajs/server'",
|
|
2186
|
+
"",
|
|
2187
|
+
`// PostgreSQL provider preset: ${providerName}`,
|
|
2188
|
+
"// Ensure DATABASE_URL is set in your environment.",
|
|
2189
|
+
"",
|
|
2190
|
+
"async function start(): Promise<void> {",
|
|
2191
|
+
" const connectionString = process.env.DATABASE_URL || ''",
|
|
2192
|
+
" if (connectionString.length === 0) {",
|
|
2193
|
+
" throw new Error('DATABASE_URL is required for PostgreSQL sync server store.')",
|
|
2194
|
+
" }",
|
|
2195
|
+
"",
|
|
2196
|
+
" const store = await createPostgresServerStore({ connectionString })",
|
|
2197
|
+
" const server = createProductionServer({",
|
|
2198
|
+
" store,",
|
|
2199
|
+
" port: Number(process.env.PORT) || 3001,",
|
|
2200
|
+
" staticDir: './dist',",
|
|
2201
|
+
" syncPath: '/kora-sync',",
|
|
2202
|
+
" })",
|
|
2203
|
+
"",
|
|
2204
|
+
" const url = await server.start()",
|
|
2205
|
+
" console.log(`Kora app running at ${url}`)",
|
|
2206
|
+
"}",
|
|
2207
|
+
"",
|
|
2208
|
+
"void start()",
|
|
2209
|
+
""
|
|
2210
|
+
].join("\n");
|
|
2211
|
+
const envTemplate = [
|
|
2212
|
+
"# Kora Sync Server",
|
|
2213
|
+
"# WebSocket URL for the sync server (used by the client)",
|
|
2214
|
+
"VITE_SYNC_URL=ws://localhost:3001",
|
|
2215
|
+
"",
|
|
2216
|
+
"# Sync server port",
|
|
2217
|
+
"PORT=3001",
|
|
2218
|
+
"",
|
|
2219
|
+
`# PostgreSQL connection string (${providerName})`,
|
|
2220
|
+
`# Example: ${providerConnectionString}`,
|
|
2221
|
+
"DATABASE_URL=",
|
|
2222
|
+
""
|
|
2223
|
+
].join("\n");
|
|
2224
|
+
const existingReadme = await (0, import_promises9.readFile)(readmePath, "utf-8");
|
|
2225
|
+
const trimmedReadme = existingReadme.trimEnd();
|
|
2226
|
+
const readmeSuffix = [
|
|
2227
|
+
"",
|
|
2228
|
+
"## PostgreSQL Provider Preset",
|
|
2229
|
+
"",
|
|
2230
|
+
`Selected DB provider: ${options.dbProvider}`,
|
|
2231
|
+
"",
|
|
2232
|
+
"This scaffold uses `createPostgresServerStore` in `server.ts` and reads `DATABASE_URL` from the environment. See `.env.example` for provider-specific examples.",
|
|
2233
|
+
""
|
|
2234
|
+
].join("\n");
|
|
2235
|
+
const readmeTemplate = `${trimmedReadme}${readmeSuffix}`;
|
|
2236
|
+
await (0, import_promises9.writeFile)(serverPath, serverTemplate, "utf-8");
|
|
2237
|
+
await (0, import_promises9.writeFile)(envPath, envTemplate, "utf-8");
|
|
2238
|
+
await (0, import_promises9.writeFile)(readmePath, readmeTemplate, "utf-8");
|
|
2239
|
+
}
|
|
2240
|
+
function isSyncTemplate(template) {
|
|
2241
|
+
return template === "react-sync" || template === "react-tailwind-sync";
|
|
2242
|
+
}
|
|
2243
|
+
function getProviderDisplayName(provider) {
|
|
2244
|
+
switch (provider) {
|
|
2245
|
+
case "supabase":
|
|
2246
|
+
return "Supabase";
|
|
2247
|
+
case "neon":
|
|
2248
|
+
return "Neon";
|
|
2249
|
+
case "railway":
|
|
2250
|
+
return "Railway";
|
|
2251
|
+
case "vercel-postgres":
|
|
2252
|
+
return "Vercel Postgres";
|
|
2253
|
+
case "custom":
|
|
2254
|
+
return "Custom";
|
|
2255
|
+
case "local":
|
|
2256
|
+
return "Local Postgres";
|
|
2257
|
+
case "none":
|
|
2258
|
+
return "PostgreSQL";
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
function getProviderConnectionStringExample(provider) {
|
|
2262
|
+
switch (provider) {
|
|
2263
|
+
case "supabase":
|
|
2264
|
+
return "postgresql://postgres:<password>@db.<project-ref>.supabase.co:5432/postgres?sslmode=require";
|
|
2265
|
+
case "neon":
|
|
2266
|
+
return "postgresql://<user>:<password>@<branch>.<project>.neon.tech/neondb?sslmode=require";
|
|
2267
|
+
case "railway":
|
|
2268
|
+
return "postgresql://postgres:<password>@<host>.railway.app:<port>/railway?sslmode=require";
|
|
2269
|
+
case "vercel-postgres":
|
|
2270
|
+
return "postgresql://<user>:<password>@<host>.pooler.vercel-storage.com:5432/verceldb?sslmode=require";
|
|
2271
|
+
case "custom":
|
|
2272
|
+
return "postgresql://<user>:<password>@<host>:5432/<database>";
|
|
2273
|
+
case "local":
|
|
2274
|
+
return "postgresql://postgres:postgres@localhost:5432/kora";
|
|
2275
|
+
case "none":
|
|
2276
|
+
return "postgresql://postgres:postgres@localhost:5432/kora";
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
221
2279
|
// Annotate the CommonJS export names for ESM import in node:
|
|
222
2280
|
0 && (module.exports = {
|
|
2281
|
+
ClackPromptClient,
|
|
223
2282
|
CliError,
|
|
2283
|
+
DEPLOY_PLATFORMS,
|
|
224
2284
|
DevServerError,
|
|
2285
|
+
FlyAdapter,
|
|
225
2286
|
InvalidProjectError,
|
|
2287
|
+
NodeFlyCommandRunner,
|
|
2288
|
+
NodeRailwayCommandRunner,
|
|
226
2289
|
PACKAGE_MANAGERS,
|
|
2290
|
+
PreferenceStore,
|
|
227
2291
|
ProjectExistsError,
|
|
2292
|
+
PromptCancelledError,
|
|
2293
|
+
RailwayAdapter,
|
|
2294
|
+
ReadlinePromptClient,
|
|
228
2295
|
SchemaNotFoundError,
|
|
2296
|
+
StubDeployAdapter,
|
|
229
2297
|
TEMPLATES,
|
|
230
2298
|
TEMPLATE_INFO,
|
|
231
|
-
|
|
2299
|
+
applySyncProviderPreset,
|
|
2300
|
+
buildClient,
|
|
2301
|
+
bundleServer,
|
|
2302
|
+
createDeployAdapter,
|
|
2303
|
+
createPromptClient,
|
|
2304
|
+
deployCommand,
|
|
2305
|
+
determineTemplateFromSelections,
|
|
2306
|
+
generateDeployPackageJson,
|
|
2307
|
+
generateDockerIgnore,
|
|
2308
|
+
generateDockerfile,
|
|
2309
|
+
generateFlyToml,
|
|
2310
|
+
generateRailwayJson,
|
|
2311
|
+
generateTypes,
|
|
2312
|
+
getCreatePreferencesOrDefault,
|
|
2313
|
+
getDefaultCreatePreferences,
|
|
2314
|
+
isAuthValue,
|
|
2315
|
+
isDatabaseProviderValue,
|
|
2316
|
+
isDatabaseValue,
|
|
2317
|
+
isDeployPlatform,
|
|
2318
|
+
isFrameworkValue,
|
|
2319
|
+
readDeployState,
|
|
2320
|
+
resetDeployState,
|
|
2321
|
+
resolveCreatePreferencesFlow,
|
|
2322
|
+
resolveDeployDirectory,
|
|
2323
|
+
resolveDeployStatePath,
|
|
2324
|
+
saveResolvedPreferences,
|
|
2325
|
+
shouldSavePreferences,
|
|
2326
|
+
updateDeployState,
|
|
2327
|
+
validateProjectName,
|
|
2328
|
+
writeDeployState,
|
|
2329
|
+
writeDockerIgnoreArtifact,
|
|
2330
|
+
writeDockerfileArtifact,
|
|
2331
|
+
writeFlyTomlArtifact,
|
|
2332
|
+
writeRailwayJsonArtifact
|
|
232
2333
|
});
|
|
233
2334
|
//# sourceMappingURL=index.cjs.map
|