@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.
@@ -0,0 +1,1550 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ InvalidProjectError,
4
+ createLogger,
5
+ createPromptClient,
6
+ findProjectRoot,
7
+ resolveProjectBinaryEntryPoint
8
+ } from "./chunk-KTSRAPSE.js";
9
+
10
+ // src/commands/deploy/adapters/adapter.ts
11
+ var DEPLOY_PLATFORMS = ["fly", "railway", "render", "docker", "kora-cloud"];
12
+ function isDeployPlatform(value) {
13
+ return DEPLOY_PLATFORMS.includes(value);
14
+ }
15
+
16
+ // src/commands/deploy/artifacts/fly-toml-generator.ts
17
+ import { mkdir, writeFile } from "fs/promises";
18
+ import { join } from "path";
19
+ var GENERATED_HEADER = "# Generated by kora deploy \u2014 do not edit manually";
20
+ function generateFlyToml(options) {
21
+ const internalPort = options.internalPort ?? 3e3;
22
+ return [
23
+ GENERATED_HEADER,
24
+ "",
25
+ `app = "${options.appName}"`,
26
+ `primary_region = "${options.region}"`,
27
+ "",
28
+ "[build]",
29
+ ` dockerfile = "Dockerfile"`,
30
+ "",
31
+ "[http_service]",
32
+ ` internal_port = ${String(internalPort)}`,
33
+ " force_https = true",
34
+ ' auto_stop_machines = "stop"',
35
+ " auto_start_machines = true",
36
+ " min_machines_running = 0",
37
+ "",
38
+ " [[http_service.checks]]",
39
+ ' grace_period = "10s"',
40
+ ' interval = "30s"',
41
+ ' method = "GET"',
42
+ ' timeout = "5s"',
43
+ ' path = "/"',
44
+ ""
45
+ ].join("\n");
46
+ }
47
+ async function writeFlyTomlArtifact(deployDirectory, options) {
48
+ await mkdir(deployDirectory, { recursive: true });
49
+ const filePath = join(deployDirectory, "fly.toml");
50
+ await writeFile(filePath, generateFlyToml(options), "utf-8");
51
+ return filePath;
52
+ }
53
+
54
+ // src/commands/deploy/builder/client-builder.ts
55
+ import { spawn } from "child_process";
56
+ import { copyFile, readdir } from "fs/promises";
57
+ import { existsSync } from "fs";
58
+ import { join as join2, resolve } from "path";
59
+ async function buildClient(options) {
60
+ const viteEntryPoint = await resolveProjectBinaryEntryPoint(options.projectRoot, "vite", "vite");
61
+ if (!viteEntryPoint) {
62
+ throw new Error(
63
+ `Could not find local Vite binary in ${options.projectRoot}. Install dependencies before deploying.`
64
+ );
65
+ }
66
+ const args = [
67
+ viteEntryPoint,
68
+ "build",
69
+ "--outDir",
70
+ options.outDir,
71
+ "--mode",
72
+ options.mode ?? "production"
73
+ ];
74
+ await runProcess(process.execPath, args, options.projectRoot);
75
+ await patchSqliteWasmAssets(options.projectRoot, options.outDir);
76
+ return { outDir: options.outDir };
77
+ }
78
+ async function patchSqliteWasmAssets(projectRoot, outDir) {
79
+ const assetsDir = join2(outDir, "assets");
80
+ if (!existsSync(assetsDir)) return;
81
+ const files = await readdir(assetsDir);
82
+ const hashedWasm = files.find((f) => /^sqlite3-.+\.wasm$/.test(f));
83
+ if (hashedWasm && !files.includes("sqlite3.wasm")) {
84
+ await copyFile(join2(assetsDir, hashedWasm), join2(assetsDir, "sqlite3.wasm"));
85
+ }
86
+ if (!files.includes("sqlite3-opfs-async-proxy.js")) {
87
+ const proxyFile = resolve(
88
+ projectRoot,
89
+ "node_modules",
90
+ "@sqlite.org",
91
+ "sqlite-wasm",
92
+ "sqlite-wasm",
93
+ "jswasm",
94
+ "sqlite3-opfs-async-proxy.js"
95
+ );
96
+ if (existsSync(proxyFile)) {
97
+ await copyFile(proxyFile, join2(assetsDir, "sqlite3-opfs-async-proxy.js"));
98
+ }
99
+ }
100
+ }
101
+ async function runProcess(command, args, cwd) {
102
+ await new Promise((resolve2, reject) => {
103
+ const child = spawn(command, args, {
104
+ cwd,
105
+ stdio: "inherit",
106
+ env: process.env
107
+ });
108
+ child.on("error", (error) => {
109
+ reject(error);
110
+ });
111
+ child.on("exit", (code) => {
112
+ if (code === 0) {
113
+ resolve2();
114
+ return;
115
+ }
116
+ reject(new Error(`Client build failed with exit code ${String(code ?? "unknown")}.`));
117
+ });
118
+ });
119
+ }
120
+
121
+ // src/commands/deploy/builder/server-bundler.ts
122
+ import { mkdir as mkdir2 } from "fs/promises";
123
+ import { access } from "fs/promises";
124
+ import { join as join3 } from "path";
125
+ import { build } from "esbuild";
126
+ var DEFAULT_ENTRY_CANDIDATES = [
127
+ "server.ts",
128
+ "server.js",
129
+ "src/server.ts",
130
+ "src/server.js"
131
+ ];
132
+ async function bundleServer(options) {
133
+ const candidates = options.entryFileCandidates ?? DEFAULT_ENTRY_CANDIDATES;
134
+ const entryFilePath = await resolveServerEntry(options.projectRoot, candidates);
135
+ if (!entryFilePath) {
136
+ throw new Error(
137
+ `Could not find a server entry file in ${options.projectRoot}. Looked for: ${candidates.join(", ")}`
138
+ );
139
+ }
140
+ await mkdir2(options.deployDirectory, { recursive: true });
141
+ const outputFilePath = join3(options.deployDirectory, "server-bundled.js");
142
+ await build({
143
+ entryPoints: [entryFilePath],
144
+ outfile: outputFilePath,
145
+ bundle: true,
146
+ platform: "node",
147
+ format: "esm",
148
+ target: ["node20"],
149
+ sourcemap: false,
150
+ logLevel: "silent",
151
+ external: ["better-sqlite3"],
152
+ banner: {
153
+ js: "import { createRequire as __createRequire } from 'module'; const require = __createRequire(import.meta.url);"
154
+ }
155
+ });
156
+ return {
157
+ entryFilePath,
158
+ outputFilePath
159
+ };
160
+ }
161
+ async function resolveServerEntry(projectRoot, candidates) {
162
+ for (const candidate of candidates) {
163
+ const fullPath = join3(projectRoot, candidate);
164
+ try {
165
+ await access(fullPath);
166
+ return fullPath;
167
+ } catch {
168
+ }
169
+ }
170
+ return null;
171
+ }
172
+
173
+ // src/commands/deploy/adapters/fly-adapter.ts
174
+ import { spawn as spawn2 } from "child_process";
175
+ import { join as join4 } from "path";
176
+ var FlyAdapter = class {
177
+ name = "fly";
178
+ logger = createLogger();
179
+ runner;
180
+ commandCandidates;
181
+ flyCommand = null;
182
+ currentContext;
183
+ lastDeploymentId = null;
184
+ constructor(options = {}) {
185
+ this.runner = options.runner ?? new NodeFlyCommandRunner();
186
+ this.commandCandidates = options.commandCandidates ?? DEFAULT_FLY_COMMAND_CANDIDATES;
187
+ this.currentContext = options.context ?? null;
188
+ }
189
+ /**
190
+ * Seeds runtime context for non-provisioning commands.
191
+ */
192
+ setContext(context) {
193
+ this.currentContext = context;
194
+ }
195
+ /**
196
+ * Checks whether a Fly CLI executable can be resolved.
197
+ */
198
+ async detect() {
199
+ const command = await this.tryResolveFlyCommand(process.cwd());
200
+ return command !== null;
201
+ }
202
+ /**
203
+ * Ensures Fly CLI is available before deployment.
204
+ */
205
+ async install() {
206
+ const available = await this.detect();
207
+ if (!available) {
208
+ throw new Error(
209
+ "Fly CLI is required but not installed. Install from https://fly.io/docs/hands-on/install-flyctl/."
210
+ );
211
+ }
212
+ }
213
+ /**
214
+ * Performs Fly authentication check/login.
215
+ */
216
+ async authenticate() {
217
+ const projectRoot = this.currentContext?.projectRoot ?? process.cwd();
218
+ const status = await this.runFlyCommand(["auth", "whoami", "--json"], projectRoot, false);
219
+ if (status.exitCode === 0) return;
220
+ const login = await this.runFlyCommand(["auth", "login"], projectRoot, true);
221
+ if (login.exitCode !== 0) {
222
+ throw new Error(`Fly authentication failed: ${normalizeError(login.stderr, login.stdout)}`);
223
+ }
224
+ }
225
+ /**
226
+ * Provisions app and optionally region-bound resources in Fly.
227
+ */
228
+ async provision(config) {
229
+ this.currentContext = {
230
+ projectRoot: config.projectRoot,
231
+ appName: config.appName,
232
+ region: config.region ?? "iad"
233
+ };
234
+ const appCreateArgs = ["apps", "create", config.appName];
235
+ const createApp = await this.runFlyCommand(appCreateArgs, config.projectRoot, false);
236
+ if (createApp.exitCode !== 0 && !isAlreadyExistsResponse(createApp.stderr, createApp.stdout)) {
237
+ throw new Error(
238
+ `Fly app provisioning failed for "${config.appName}": ${normalizeError(createApp.stderr, createApp.stdout)}`
239
+ );
240
+ }
241
+ const secrets = [];
242
+ const portSecret = await this.runFlyCommand(
243
+ ["secrets", "set", "PORT=3000", "--app", config.appName],
244
+ config.projectRoot,
245
+ false
246
+ );
247
+ if (portSecret.exitCode === 0) {
248
+ secrets.push("PORT");
249
+ }
250
+ return {
251
+ applicationId: config.appName,
252
+ databaseId: null,
253
+ secretsSet: secrets
254
+ };
255
+ }
256
+ /**
257
+ * Builds local artifacts consumed by Fly deploy.
258
+ */
259
+ async build(config) {
260
+ this.currentContext = {
261
+ projectRoot: config.projectRoot,
262
+ appName: config.appName,
263
+ region: config.region
264
+ };
265
+ const deployDirectory = join4(config.projectRoot, ".kora", "deploy");
266
+ await writeFlyTomlArtifact(deployDirectory, {
267
+ appName: config.appName,
268
+ region: config.region ?? "iad"
269
+ });
270
+ await bundleServer({
271
+ projectRoot: config.projectRoot,
272
+ deployDirectory
273
+ });
274
+ const client = await buildClient({
275
+ projectRoot: config.projectRoot,
276
+ outDir: join4(deployDirectory, "dist"),
277
+ mode: "production"
278
+ });
279
+ return {
280
+ clientDirectory: client.outDir,
281
+ serverBundlePath: join4(deployDirectory, "server-bundled.js"),
282
+ deployDirectory
283
+ };
284
+ }
285
+ /**
286
+ * Deploys generated artifacts to Fly and returns live endpoints.
287
+ */
288
+ async deploy(artifacts) {
289
+ const context = this.requireContext();
290
+ const deploy = await this.runFlyCommand(
291
+ ["deploy", "--config", "fly.toml", "--app", context.appName],
292
+ artifacts.deployDirectory,
293
+ true
294
+ );
295
+ if (deploy.exitCode !== 0) {
296
+ throw new Error(`Fly deployment failed: ${normalizeError(deploy.stderr, deploy.stdout)}`);
297
+ }
298
+ const info = await this.runFlyCommand(
299
+ ["status", "--app", context.appName, "--json"],
300
+ context.projectRoot,
301
+ false
302
+ );
303
+ const hostname = parseFlyHostname(info.stdout) ?? `${context.appName}.fly.dev`;
304
+ const deploymentId = parseFlyDeploymentId(info.stdout) ?? (/* @__PURE__ */ new Date()).toISOString();
305
+ this.lastDeploymentId = deploymentId;
306
+ return {
307
+ deploymentId,
308
+ liveUrl: `https://${hostname}`,
309
+ syncUrl: `wss://${hostname}/kora-sync`
310
+ };
311
+ }
312
+ /**
313
+ * Rolls back to a deployment version.
314
+ */
315
+ async rollback(deploymentId) {
316
+ const context = this.requireContext();
317
+ const rollback = await this.runFlyCommand(
318
+ ["releases", "revert", deploymentId, "--app", context.appName],
319
+ context.projectRoot,
320
+ true
321
+ );
322
+ if (rollback.exitCode !== 0) {
323
+ throw new Error(`Fly rollback failed: ${normalizeError(rollback.stderr, rollback.stdout)}`);
324
+ }
325
+ }
326
+ /**
327
+ * Returns deployment logs as an async iterable.
328
+ */
329
+ async *logs(options) {
330
+ const context = this.requireContext();
331
+ const args = ["logs", "--app", context.appName, "--no-tail"];
332
+ if (typeof options.since === "string" && options.since.length > 0) {
333
+ args.push("--region", options.since);
334
+ }
335
+ const result = await this.runFlyCommand(args, context.projectRoot, false);
336
+ if (result.exitCode !== 0) {
337
+ throw new Error(`Fly logs failed: ${normalizeError(result.stderr, result.stdout)}`);
338
+ }
339
+ const lines = result.stdout.split(/\r?\n/).filter((line) => line.length > 0);
340
+ for (const line of lines) {
341
+ yield {
342
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
343
+ level: inferLogLevel(line),
344
+ message: line
345
+ };
346
+ }
347
+ }
348
+ /**
349
+ * Reads current deployment status from Fly.
350
+ */
351
+ async status() {
352
+ const context = this.requireContext();
353
+ const status = await this.runFlyCommand(
354
+ ["status", "--app", context.appName, "--json"],
355
+ context.projectRoot,
356
+ false
357
+ );
358
+ if (status.exitCode !== 0) {
359
+ return {
360
+ state: "failed",
361
+ message: normalizeError(status.stderr, status.stdout)
362
+ };
363
+ }
364
+ const hostname = parseFlyHostname(status.stdout);
365
+ return {
366
+ state: "healthy",
367
+ message: "Fly deployment is healthy.",
368
+ liveUrl: hostname ? `https://${hostname}` : void 0
369
+ };
370
+ }
371
+ async runFlyCommand(flyArgs, cwd, inheritOutput) {
372
+ const flyBinary = await this.resolveFlyCommand(cwd);
373
+ if (inheritOutput) {
374
+ this.logger.step(`fly ${flyArgs.join(" ")}`);
375
+ }
376
+ return await this.runner.run(flyBinary, flyArgs, cwd);
377
+ }
378
+ requireContext() {
379
+ if (!this.currentContext) {
380
+ throw new Error("Fly adapter context is not initialized. Run provision() first.");
381
+ }
382
+ return this.currentContext;
383
+ }
384
+ async resolveFlyCommand(cwd) {
385
+ if (this.flyCommand) {
386
+ return this.flyCommand;
387
+ }
388
+ const resolved = await this.tryResolveFlyCommand(cwd);
389
+ if (!resolved) {
390
+ throw new Error("Could not resolve a usable Fly CLI command (tried flyctl, fly).");
391
+ }
392
+ this.flyCommand = resolved;
393
+ return resolved;
394
+ }
395
+ async tryResolveFlyCommand(cwd) {
396
+ for (const command of this.commandCandidates) {
397
+ const versionCheck = await this.runner.run(command, ["version"], cwd);
398
+ if (versionCheck.exitCode === 0) {
399
+ return command;
400
+ }
401
+ }
402
+ return null;
403
+ }
404
+ };
405
+ var NodeFlyCommandRunner = class {
406
+ async run(command, args, cwd) {
407
+ return await new Promise((resolve2) => {
408
+ const child = spawn2(command, args, {
409
+ cwd,
410
+ env: process.env,
411
+ stdio: ["ignore", "pipe", "pipe"]
412
+ });
413
+ let stdout = "";
414
+ let stderr = "";
415
+ child.stdout?.on("data", (chunk) => {
416
+ stdout += chunk.toString("utf-8");
417
+ });
418
+ child.stderr?.on("data", (chunk) => {
419
+ stderr += chunk.toString("utf-8");
420
+ });
421
+ child.on("error", (error) => {
422
+ resolve2({
423
+ exitCode: 1,
424
+ stdout,
425
+ stderr: `${stderr}
426
+ ${error.message}`
427
+ });
428
+ });
429
+ child.on("exit", (code) => {
430
+ resolve2({
431
+ exitCode: code ?? 1,
432
+ stdout: stdout.trim(),
433
+ stderr: stderr.trim()
434
+ });
435
+ });
436
+ });
437
+ }
438
+ };
439
+ var DEFAULT_FLY_COMMAND_CANDIDATES = ["flyctl", "fly"];
440
+ function parseFlyHostname(rawJson) {
441
+ const parsed = parseJsonRecord(rawJson);
442
+ if (!parsed) return null;
443
+ const hostname = parsed.Hostname;
444
+ if (typeof hostname === "string" && hostname.length > 0) {
445
+ return hostname;
446
+ }
447
+ const hostnames = parsed.Hostnames;
448
+ if (Array.isArray(hostnames)) {
449
+ const first = hostnames.find((item) => typeof item === "string");
450
+ if (typeof first === "string" && first.length > 0) {
451
+ return first;
452
+ }
453
+ }
454
+ return null;
455
+ }
456
+ function parseFlyDeploymentId(rawJson) {
457
+ const parsed = parseJsonRecord(rawJson);
458
+ if (!parsed) return null;
459
+ const deploymentId = parsed.DeploymentID;
460
+ if (typeof deploymentId === "string" && deploymentId.length > 0) {
461
+ return deploymentId;
462
+ }
463
+ const latestDeployment = parsed.LatestDeployment;
464
+ if (typeof latestDeployment === "object" && latestDeployment !== null) {
465
+ const id = latestDeployment.ID;
466
+ if (typeof id === "string" && id.length > 0) {
467
+ return id;
468
+ }
469
+ }
470
+ return null;
471
+ }
472
+ function parseJsonRecord(value) {
473
+ try {
474
+ const parsed = JSON.parse(value);
475
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
476
+ return parsed;
477
+ }
478
+ return null;
479
+ } catch {
480
+ return null;
481
+ }
482
+ }
483
+ function inferLogLevel(line) {
484
+ const normalized = line.toLowerCase();
485
+ if (normalized.includes("error")) return "error";
486
+ if (normalized.includes("warn")) return "warn";
487
+ if (normalized.includes("debug")) return "debug";
488
+ return "info";
489
+ }
490
+ function normalizeError(stderr, stdout) {
491
+ if (stderr.length > 0) return stderr;
492
+ if (stdout.length > 0) return stdout;
493
+ return "unknown fly CLI error";
494
+ }
495
+ function isAlreadyExistsResponse(stderr, stdout) {
496
+ const text = `${stderr}
497
+ ${stdout}`.toLowerCase();
498
+ return text.includes("already exists") || text.includes("already been taken");
499
+ }
500
+
501
+ // src/commands/deploy/artifacts/railway-json-generator.ts
502
+ import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
503
+ import { join as join5 } from "path";
504
+ var GENERATED_HEADER2 = "Generated by kora deploy - do not edit manually.";
505
+ function generateRailwayJson(options) {
506
+ const file = {
507
+ $schema: "https://railway.app/railway.schema.json",
508
+ build: {
509
+ builder: "DOCKERFILE",
510
+ dockerfilePath: "Dockerfile"
511
+ },
512
+ deploy: {
513
+ startCommand: options.startCommand ?? "node ./server-bundled.js",
514
+ healthcheckPath: options.healthcheckPath ?? "/health",
515
+ restartPolicyType: "on_failure",
516
+ restartPolicyMaxRetries: 3
517
+ },
518
+ metadata: {
519
+ generatedBy: GENERATED_HEADER2,
520
+ appName: options.appName,
521
+ region: options.region
522
+ }
523
+ };
524
+ return `${JSON.stringify(file, null, 2)}
525
+ `;
526
+ }
527
+ async function writeRailwayJsonArtifact(deployDirectory, options) {
528
+ await mkdir3(deployDirectory, { recursive: true });
529
+ const filePath = join5(deployDirectory, "railway.json");
530
+ await writeFile2(filePath, generateRailwayJson(options), "utf-8");
531
+ return filePath;
532
+ }
533
+
534
+ // src/commands/deploy/adapters/railway-adapter.ts
535
+ import { spawn as spawn3 } from "child_process";
536
+ import { join as join6 } from "path";
537
+ var RailwayAdapter = class {
538
+ name = "railway";
539
+ logger = createLogger();
540
+ runner;
541
+ commandCandidates;
542
+ railwayCommand = null;
543
+ currentContext;
544
+ lastDeploymentId = null;
545
+ constructor(options = {}) {
546
+ this.runner = options.runner ?? new NodeRailwayCommandRunner();
547
+ this.commandCandidates = options.commandCandidates ?? DEFAULT_RAILWAY_COMMAND_CANDIDATES;
548
+ this.currentContext = options.context ?? null;
549
+ }
550
+ setContext(context) {
551
+ this.currentContext = context;
552
+ }
553
+ async detect() {
554
+ const command = await this.tryResolveRailwayCommand(process.cwd());
555
+ return command !== null;
556
+ }
557
+ async install() {
558
+ const available = await this.detect();
559
+ if (!available) {
560
+ throw new Error(
561
+ "Railway CLI is required but not installed. Install with `npm i -g @railway/cli` or see https://docs.railway.com/guides/cli."
562
+ );
563
+ }
564
+ }
565
+ async authenticate() {
566
+ const projectRoot = this.currentContext?.projectRoot ?? process.cwd();
567
+ const whoami = await this.runRailwayCommand(["whoami"], projectRoot, false);
568
+ if (whoami.exitCode === 0) return;
569
+ const login = await this.runRailwayCommand(["login"], projectRoot, true);
570
+ if (login.exitCode !== 0) {
571
+ throw new Error(
572
+ `Railway authentication failed: ${normalizeError2(login.stderr, login.stdout)}`
573
+ );
574
+ }
575
+ }
576
+ async provision(config) {
577
+ this.currentContext = {
578
+ projectRoot: config.projectRoot,
579
+ appName: config.appName,
580
+ region: config.region
581
+ };
582
+ const init = await this.runRailwayCommand(
583
+ ["init", "--name", config.appName, "--confirm"],
584
+ config.projectRoot,
585
+ false
586
+ );
587
+ if (init.exitCode !== 0 && !isAlreadyExistsResponse2(init.stderr, init.stdout)) {
588
+ throw new Error(
589
+ `Railway project provisioning failed for "${config.appName}": ${normalizeError2(init.stderr, init.stdout)}`
590
+ );
591
+ }
592
+ const link = await this.runRailwayCommand(
593
+ ["link", "--project", config.appName, "--environment", config.environment, "--yes"],
594
+ config.projectRoot,
595
+ false
596
+ );
597
+ if (link.exitCode !== 0 && !isAlreadyExistsResponse2(link.stderr, link.stdout)) {
598
+ throw new Error(`Railway link failed: ${normalizeError2(link.stderr, link.stdout)}`);
599
+ }
600
+ const vars = [];
601
+ const setPort = await this.runRailwayCommand(
602
+ ["variables", "set", "PORT=3000", "--yes"],
603
+ config.projectRoot,
604
+ false
605
+ );
606
+ if (setPort.exitCode === 0) {
607
+ vars.push("PORT");
608
+ }
609
+ return {
610
+ applicationId: config.appName,
611
+ databaseId: null,
612
+ secretsSet: vars
613
+ };
614
+ }
615
+ async build(config) {
616
+ this.currentContext = {
617
+ projectRoot: config.projectRoot,
618
+ appName: config.appName,
619
+ region: config.region
620
+ };
621
+ const deployDirectory = join6(config.projectRoot, ".kora", "deploy");
622
+ await writeRailwayJsonArtifact(deployDirectory, {
623
+ appName: config.appName,
624
+ environment: config.environment
625
+ });
626
+ await bundleServer({
627
+ projectRoot: config.projectRoot,
628
+ deployDirectory
629
+ });
630
+ const client = await buildClient({
631
+ projectRoot: config.projectRoot,
632
+ outDir: join6(deployDirectory, "dist"),
633
+ mode: "production"
634
+ });
635
+ return {
636
+ clientDirectory: client.outDir,
637
+ serverBundlePath: join6(deployDirectory, "server-bundled.js"),
638
+ deployDirectory
639
+ };
640
+ }
641
+ async deploy(artifacts) {
642
+ const context = this.requireContext();
643
+ const up = await this.runRailwayCommand(["up", "--yes"], artifacts.deployDirectory, true);
644
+ if (up.exitCode !== 0) {
645
+ throw new Error(`Railway deployment failed: ${normalizeError2(up.stderr, up.stdout)}`);
646
+ }
647
+ const status = await this.runRailwayCommand(["status", "--json"], context.projectRoot, false);
648
+ const deploymentId = parseRailwayDeploymentId(status.stdout) ?? (/* @__PURE__ */ new Date()).toISOString();
649
+ const liveUrl = parseRailwayUrl(status.stdout) ?? `https://${context.appName}.up.railway.app`;
650
+ this.lastDeploymentId = deploymentId;
651
+ return {
652
+ deploymentId,
653
+ liveUrl,
654
+ syncUrl: toSyncUrl(liveUrl)
655
+ };
656
+ }
657
+ async rollback(deploymentId) {
658
+ const context = this.requireContext();
659
+ const rollback = await this.runRailwayCommand(
660
+ ["redeploy", "--deployment", deploymentId, "--yes"],
661
+ context.projectRoot,
662
+ true
663
+ );
664
+ if (rollback.exitCode !== 0) {
665
+ throw new Error(
666
+ `Railway rollback failed: ${normalizeError2(rollback.stderr, rollback.stdout)}`
667
+ );
668
+ }
669
+ }
670
+ async *logs(options) {
671
+ const context = this.requireContext();
672
+ const args = ["logs"];
673
+ if (typeof options.tail === "number" && options.tail > 0) {
674
+ args.push("--lines", String(options.tail));
675
+ }
676
+ const result = await this.runRailwayCommand(args, context.projectRoot, false);
677
+ if (result.exitCode !== 0) {
678
+ throw new Error(`Railway logs failed: ${normalizeError2(result.stderr, result.stdout)}`);
679
+ }
680
+ const lines = result.stdout.split(/\r?\n/).filter((line) => line.length > 0);
681
+ for (const line of lines) {
682
+ yield {
683
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
684
+ level: inferLogLevel2(line),
685
+ message: line
686
+ };
687
+ }
688
+ }
689
+ async status() {
690
+ const context = this.requireContext();
691
+ const status = await this.runRailwayCommand(["status", "--json"], context.projectRoot, false);
692
+ if (status.exitCode !== 0) {
693
+ return {
694
+ state: "failed",
695
+ message: normalizeError2(status.stderr, status.stdout)
696
+ };
697
+ }
698
+ const liveUrl = parseRailwayUrl(status.stdout);
699
+ return {
700
+ state: "healthy",
701
+ message: "Railway deployment is healthy.",
702
+ liveUrl: liveUrl ?? void 0
703
+ };
704
+ }
705
+ async runRailwayCommand(railwayArgs, cwd, inheritOutput) {
706
+ const command = await this.resolveRailwayCommand(cwd);
707
+ if (inheritOutput) {
708
+ this.logger.step(`railway ${railwayArgs.join(" ")}`);
709
+ }
710
+ return await this.runner.run(command, railwayArgs, cwd);
711
+ }
712
+ requireContext() {
713
+ if (!this.currentContext) {
714
+ throw new Error("Railway adapter context is not initialized. Run provision() first.");
715
+ }
716
+ return this.currentContext;
717
+ }
718
+ async resolveRailwayCommand(cwd) {
719
+ if (this.railwayCommand) {
720
+ return this.railwayCommand;
721
+ }
722
+ const resolved = await this.tryResolveRailwayCommand(cwd);
723
+ if (!resolved) {
724
+ throw new Error("Could not resolve a usable Railway CLI command (tried railway).");
725
+ }
726
+ this.railwayCommand = resolved;
727
+ return resolved;
728
+ }
729
+ async tryResolveRailwayCommand(cwd) {
730
+ for (const command of this.commandCandidates) {
731
+ const version = await this.runner.run(command, ["--version"], cwd);
732
+ if (version.exitCode === 0) {
733
+ return command;
734
+ }
735
+ }
736
+ return null;
737
+ }
738
+ };
739
+ var NodeRailwayCommandRunner = class {
740
+ async run(command, args, cwd) {
741
+ return await new Promise((resolve2) => {
742
+ const child = spawn3(command, args, {
743
+ cwd,
744
+ env: process.env,
745
+ stdio: ["ignore", "pipe", "pipe"]
746
+ });
747
+ let stdout = "";
748
+ let stderr = "";
749
+ child.stdout?.on("data", (chunk) => {
750
+ stdout += chunk.toString("utf-8");
751
+ });
752
+ child.stderr?.on("data", (chunk) => {
753
+ stderr += chunk.toString("utf-8");
754
+ });
755
+ child.on("error", (error) => {
756
+ resolve2({
757
+ exitCode: 1,
758
+ stdout,
759
+ stderr: `${stderr}
760
+ ${error.message}`
761
+ });
762
+ });
763
+ child.on("exit", (code) => {
764
+ resolve2({
765
+ exitCode: code ?? 1,
766
+ stdout: stdout.trim(),
767
+ stderr: stderr.trim()
768
+ });
769
+ });
770
+ });
771
+ }
772
+ };
773
+ var DEFAULT_RAILWAY_COMMAND_CANDIDATES = ["railway"];
774
+ function parseRailwayUrl(rawJson) {
775
+ const record = parseJsonRecord2(rawJson);
776
+ if (!record) return null;
777
+ const url = record.url;
778
+ if (typeof url === "string" && url.length > 0) {
779
+ return ensureHttpsUrl(url);
780
+ }
781
+ const service = record.service;
782
+ if (typeof service === "object" && service !== null) {
783
+ const domain = service.domain;
784
+ if (typeof domain === "string" && domain.length > 0) {
785
+ return ensureHttpsUrl(domain);
786
+ }
787
+ }
788
+ return null;
789
+ }
790
+ function parseRailwayDeploymentId(rawJson) {
791
+ const record = parseJsonRecord2(rawJson);
792
+ if (!record) return null;
793
+ const deploymentId = record.deploymentId;
794
+ if (typeof deploymentId === "string" && deploymentId.length > 0) {
795
+ return deploymentId;
796
+ }
797
+ const deployment = record.deployment;
798
+ if (typeof deployment === "object" && deployment !== null) {
799
+ const id = deployment.id;
800
+ if (typeof id === "string" && id.length > 0) {
801
+ return id;
802
+ }
803
+ }
804
+ return null;
805
+ }
806
+ function parseJsonRecord2(value) {
807
+ try {
808
+ const parsed = JSON.parse(value);
809
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
810
+ return parsed;
811
+ }
812
+ return null;
813
+ } catch {
814
+ return null;
815
+ }
816
+ }
817
+ function toSyncUrl(liveUrl) {
818
+ try {
819
+ const url = new URL(liveUrl);
820
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
821
+ url.pathname = "/kora-sync";
822
+ return url.toString().replace(/\/$/, "");
823
+ } catch {
824
+ return null;
825
+ }
826
+ }
827
+ function ensureHttpsUrl(value) {
828
+ if (value.startsWith("http://") || value.startsWith("https://")) {
829
+ return value;
830
+ }
831
+ return `https://${value}`;
832
+ }
833
+ function inferLogLevel2(line) {
834
+ const normalized = line.toLowerCase();
835
+ if (normalized.includes("error")) return "error";
836
+ if (normalized.includes("warn")) return "warn";
837
+ if (normalized.includes("debug")) return "debug";
838
+ return "info";
839
+ }
840
+ function normalizeError2(stderr, stdout) {
841
+ if (stderr.length > 0) return stderr;
842
+ if (stdout.length > 0) return stdout;
843
+ return "unknown railway CLI error";
844
+ }
845
+ function isAlreadyExistsResponse2(stderr, stdout) {
846
+ const text = `${stderr}
847
+ ${stdout}`.toLowerCase();
848
+ return text.includes("already exists");
849
+ }
850
+
851
+ // src/commands/deploy/adapters/stub-adapter.ts
852
+ var StubDeployAdapter = class {
853
+ name;
854
+ contextLabel;
855
+ constructor(platform) {
856
+ this.name = platform;
857
+ this.contextLabel = `Deploy adapter "${platform}" is not implemented yet.`;
858
+ }
859
+ setContext(_context) {
860
+ }
861
+ async detect() {
862
+ return false;
863
+ }
864
+ async install() {
865
+ throw this.notImplementedError();
866
+ }
867
+ async authenticate() {
868
+ throw this.notImplementedError();
869
+ }
870
+ async provision(_config) {
871
+ throw this.notImplementedError();
872
+ }
873
+ async build(_config) {
874
+ throw this.notImplementedError();
875
+ }
876
+ async deploy(_artifacts) {
877
+ throw this.notImplementedError();
878
+ }
879
+ async rollback(_deploymentId) {
880
+ throw this.notImplementedError();
881
+ }
882
+ logs(_options) {
883
+ throw this.notImplementedError();
884
+ }
885
+ async status() {
886
+ return {
887
+ state: "unknown",
888
+ message: this.notImplementedMessage()
889
+ };
890
+ }
891
+ notImplementedError() {
892
+ return new Error(this.notImplementedMessage());
893
+ }
894
+ notImplementedMessage() {
895
+ return `${this.contextLabel} Start with --platform fly for now.`;
896
+ }
897
+ };
898
+
899
+ // src/commands/deploy/adapters/factory.ts
900
+ function createDeployAdapter(platform) {
901
+ switch (platform) {
902
+ case "fly":
903
+ return new FlyAdapter();
904
+ case "railway":
905
+ return new RailwayAdapter();
906
+ case "render":
907
+ case "docker":
908
+ case "kora-cloud":
909
+ return new StubDeployAdapter(platform);
910
+ default: {
911
+ const exhaustiveCheck = platform;
912
+ return exhaustiveCheck;
913
+ }
914
+ }
915
+ }
916
+
917
+ // src/commands/deploy/artifacts/dockerfile-generator.ts
918
+ import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
919
+ import { join as join7 } from "path";
920
+ var GENERATED_HEADER3 = "# Generated by kora deploy \u2014 do not edit manually";
921
+ function generateDockerfile(options = {}) {
922
+ const nodeVersion = options.nodeVersion ?? "20-alpine";
923
+ const port = options.port ?? 3e3;
924
+ const clientDirectory = options.clientDirectory ?? "dist";
925
+ const serverBundleFile = options.serverBundleFile ?? "server-bundled.js";
926
+ const hasNativeDeps = options.nativeDependencies && Object.keys(options.nativeDependencies).length > 0;
927
+ const lines = [
928
+ GENERATED_HEADER3,
929
+ "",
930
+ `FROM node:${nodeVersion} AS runtime`,
931
+ "WORKDIR /app",
932
+ "ENV NODE_ENV=production",
933
+ `ENV PORT=${String(port)}`,
934
+ ""
935
+ ];
936
+ if (hasNativeDeps) {
937
+ lines.push("# Install build tools for native modules");
938
+ lines.push("RUN apk add --no-cache python3 make g++");
939
+ lines.push("");
940
+ lines.push("COPY package.json ./package.json");
941
+ lines.push("RUN npm install --omit=dev");
942
+ lines.push("");
943
+ }
944
+ lines.push(`COPY ${clientDirectory} ./dist`);
945
+ lines.push(`COPY ${serverBundleFile} ./server-bundled.js`);
946
+ lines.push("");
947
+ lines.push(`EXPOSE ${String(port)}`);
948
+ lines.push('CMD ["node", "./server-bundled.js"]');
949
+ lines.push("");
950
+ return lines.join("\n");
951
+ }
952
+ function generateDeployPackageJson(nativeDependencies) {
953
+ const pkg = {
954
+ name: "kora-deploy",
955
+ version: "1.0.0",
956
+ private: true,
957
+ type: "module",
958
+ dependencies: nativeDependencies
959
+ };
960
+ return JSON.stringify(pkg, null, 2) + "\n";
961
+ }
962
+ function generateDockerIgnore() {
963
+ return [
964
+ GENERATED_HEADER3,
965
+ "",
966
+ "node_modules",
967
+ "npm-debug.log*",
968
+ "pnpm-debug.log*",
969
+ "yarn-error.log*",
970
+ ".env",
971
+ ".env.*",
972
+ ".git",
973
+ ".gitignore",
974
+ "coverage",
975
+ ".turbo",
976
+ "*.test.ts",
977
+ "*.spec.ts",
978
+ ""
979
+ ].join("\n");
980
+ }
981
+ async function writeDockerfileArtifact(deployDirectory, options = {}) {
982
+ await mkdir4(deployDirectory, { recursive: true });
983
+ const dockerfilePath = join7(deployDirectory, "Dockerfile");
984
+ await writeFile3(dockerfilePath, generateDockerfile(options), "utf-8");
985
+ if (options.nativeDependencies && Object.keys(options.nativeDependencies).length > 0) {
986
+ const pkgJsonPath = join7(deployDirectory, "package.json");
987
+ await writeFile3(pkgJsonPath, generateDeployPackageJson(options.nativeDependencies), "utf-8");
988
+ }
989
+ return dockerfilePath;
990
+ }
991
+ async function writeDockerIgnoreArtifact(deployDirectory) {
992
+ await mkdir4(deployDirectory, { recursive: true });
993
+ const dockerIgnorePath = join7(deployDirectory, ".dockerignore");
994
+ await writeFile3(dockerIgnorePath, generateDockerIgnore(), "utf-8");
995
+ return dockerIgnorePath;
996
+ }
997
+
998
+ // src/commands/deploy/state/deploy-state.ts
999
+ import { mkdir as mkdir5, readFile, rm, writeFile as writeFile4 } from "fs/promises";
1000
+ import { join as join8 } from "path";
1001
+ var KORA_DEPLOY_DIRECTORY = join8(".kora", "deploy");
1002
+ var DEPLOY_STATE_FILENAME = "deploy.json";
1003
+ function resolveDeployDirectory(projectRoot) {
1004
+ return join8(projectRoot, KORA_DEPLOY_DIRECTORY);
1005
+ }
1006
+ function resolveDeployStatePath(projectRoot) {
1007
+ return join8(resolveDeployDirectory(projectRoot), DEPLOY_STATE_FILENAME);
1008
+ }
1009
+ async function readDeployState(projectRoot) {
1010
+ const statePath = resolveDeployStatePath(projectRoot);
1011
+ try {
1012
+ const source = await readFile(statePath, "utf-8");
1013
+ const parsed = JSON.parse(source);
1014
+ return parseDeployState(parsed);
1015
+ } catch (error) {
1016
+ const code = error.code;
1017
+ if (code === "ENOENT") return null;
1018
+ throw error;
1019
+ }
1020
+ }
1021
+ async function writeDeployState(projectRoot, input, now = /* @__PURE__ */ new Date()) {
1022
+ const timestamp = now.toISOString();
1023
+ const state = {
1024
+ platform: input.platform,
1025
+ appName: input.appName,
1026
+ region: input.region,
1027
+ projectRoot: input.projectRoot,
1028
+ liveUrl: input.liveUrl ?? null,
1029
+ syncUrl: input.syncUrl ?? null,
1030
+ databaseId: input.databaseId ?? null,
1031
+ lastDeploymentId: input.lastDeploymentId ?? null,
1032
+ createdAt: timestamp,
1033
+ updatedAt: timestamp
1034
+ };
1035
+ await persistDeployState(projectRoot, state);
1036
+ return state;
1037
+ }
1038
+ async function updateDeployState(projectRoot, patch, now = /* @__PURE__ */ new Date()) {
1039
+ const existing = await readDeployState(projectRoot);
1040
+ if (!existing) {
1041
+ throw new Error("Cannot update deploy state because deploy.json does not exist yet.");
1042
+ }
1043
+ const nextState = {
1044
+ platform: patch.platform ?? existing.platform,
1045
+ appName: patch.appName ?? existing.appName,
1046
+ region: patch.region === void 0 ? existing.region : patch.region,
1047
+ projectRoot: patch.projectRoot ?? existing.projectRoot,
1048
+ liveUrl: patch.liveUrl === void 0 ? existing.liveUrl : patch.liveUrl,
1049
+ syncUrl: patch.syncUrl === void 0 ? existing.syncUrl : patch.syncUrl,
1050
+ databaseId: patch.databaseId === void 0 ? existing.databaseId : patch.databaseId,
1051
+ lastDeploymentId: patch.lastDeploymentId === void 0 ? existing.lastDeploymentId : patch.lastDeploymentId,
1052
+ createdAt: existing.createdAt,
1053
+ updatedAt: now.toISOString()
1054
+ };
1055
+ await persistDeployState(projectRoot, nextState);
1056
+ return nextState;
1057
+ }
1058
+ async function resetDeployState(projectRoot) {
1059
+ await rm(resolveDeployDirectory(projectRoot), { recursive: true, force: true });
1060
+ }
1061
+ async function persistDeployState(projectRoot, state) {
1062
+ const deployDir = resolveDeployDirectory(projectRoot);
1063
+ const statePath = resolveDeployStatePath(projectRoot);
1064
+ await mkdir5(deployDir, { recursive: true });
1065
+ await writeFile4(statePath, `${JSON.stringify(state, null, 2)}
1066
+ `, "utf-8");
1067
+ }
1068
+ function parseDeployState(value) {
1069
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1070
+ throw new Error("Invalid deploy state: expected an object.");
1071
+ }
1072
+ const record = value;
1073
+ const platform = readPlatform(record.platform);
1074
+ const appName = readString(record.appName, "appName");
1075
+ const region = readOptionalString(record.region, "region");
1076
+ const projectRoot = readString(record.projectRoot, "projectRoot");
1077
+ const liveUrl = readOptionalString(record.liveUrl, "liveUrl");
1078
+ const syncUrl = readOptionalString(record.syncUrl, "syncUrl");
1079
+ const databaseId = readOptionalString(record.databaseId, "databaseId");
1080
+ const lastDeploymentId = readOptionalString(record.lastDeploymentId, "lastDeploymentId");
1081
+ const createdAt = readString(record.createdAt, "createdAt");
1082
+ const updatedAt = readString(record.updatedAt, "updatedAt");
1083
+ return {
1084
+ platform,
1085
+ appName,
1086
+ region,
1087
+ projectRoot,
1088
+ liveUrl,
1089
+ syncUrl,
1090
+ databaseId,
1091
+ lastDeploymentId,
1092
+ createdAt,
1093
+ updatedAt
1094
+ };
1095
+ }
1096
+ function readPlatform(value) {
1097
+ if (typeof value !== "string" || !isDeployPlatform(value)) {
1098
+ throw new Error('Invalid deploy state: "platform" must be a supported platform.');
1099
+ }
1100
+ return value;
1101
+ }
1102
+ function readString(value, field) {
1103
+ if (typeof value !== "string" || value.length === 0) {
1104
+ throw new Error(`Invalid deploy state: "${field}" must be a non-empty string.`);
1105
+ }
1106
+ return value;
1107
+ }
1108
+ function readOptionalString(value, field) {
1109
+ if (value === null) return null;
1110
+ if (typeof value === "string") return value;
1111
+ throw new Error(`Invalid deploy state: "${field}" must be a string or null.`);
1112
+ }
1113
+
1114
+ // src/commands/deploy/deploy-command.ts
1115
+ import { basename } from "path";
1116
+ import { defineCommand } from "citty";
1117
+ var deployCommand = defineCommand({
1118
+ meta: {
1119
+ name: "deploy",
1120
+ description: "Deploy a Kora project to your selected platform"
1121
+ },
1122
+ args: {
1123
+ platform: {
1124
+ type: "string",
1125
+ description: `Deployment platform (${DEPLOY_PLATFORMS.join(", ")})`
1126
+ },
1127
+ app: {
1128
+ type: "string",
1129
+ description: "Application name used by the target platform"
1130
+ },
1131
+ region: {
1132
+ type: "string",
1133
+ description: "Preferred deployment region (for example: iad, lhr, syd)"
1134
+ },
1135
+ prod: {
1136
+ type: "boolean",
1137
+ description: "Deploy to production environment",
1138
+ default: false
1139
+ },
1140
+ confirm: {
1141
+ type: "boolean",
1142
+ description: "Non-interactive mode (fail fast on missing required data)",
1143
+ default: false
1144
+ },
1145
+ reset: {
1146
+ type: "boolean",
1147
+ description: "Delete .kora/deploy state and generated artifacts",
1148
+ default: false
1149
+ }
1150
+ },
1151
+ subCommands: {
1152
+ status: defineCommand({
1153
+ meta: {
1154
+ name: "status",
1155
+ description: "Show current deployment status"
1156
+ },
1157
+ async run() {
1158
+ const logger = createLogger();
1159
+ const projectRoot = await requireProjectRoot();
1160
+ const state = await readDeployState(projectRoot);
1161
+ if (!state) {
1162
+ logger.warn("No deployment state found. Run `kora deploy` first.");
1163
+ return;
1164
+ }
1165
+ logger.banner();
1166
+ logger.info(`Platform: ${state.platform}`);
1167
+ logger.step(`App: ${state.appName}`);
1168
+ logger.step(`Region: ${state.region ?? "n/a"}`);
1169
+ logger.step(`Last deployment: ${state.lastDeploymentId ?? "n/a"}`);
1170
+ const adapter = createDeployAdapter(state.platform);
1171
+ configureAdapterContext(adapter, {
1172
+ projectRoot,
1173
+ appName: state.appName,
1174
+ region: state.region
1175
+ });
1176
+ const adapterStatus = await adapter.status();
1177
+ logger.step(`Status: ${adapterStatus.state}`);
1178
+ logger.step(`Message: ${adapterStatus.message}`);
1179
+ logger.step(`Live URL: ${adapterStatus.liveUrl ?? state.liveUrl ?? "n/a"}`);
1180
+ logger.step(`Sync URL: ${state.syncUrl ?? "n/a"}`);
1181
+ }
1182
+ }),
1183
+ rollback: defineCommand({
1184
+ meta: {
1185
+ name: "rollback",
1186
+ description: "Rollback the current deployment"
1187
+ },
1188
+ args: {
1189
+ id: {
1190
+ type: "positional",
1191
+ description: "Optional deployment identifier",
1192
+ required: false
1193
+ }
1194
+ },
1195
+ async run({ args }) {
1196
+ const logger = createLogger();
1197
+ const projectRoot = await requireProjectRoot();
1198
+ const state = await readDeployState(projectRoot);
1199
+ if (!state) {
1200
+ logger.warn("No deployment state found. Run `kora deploy` first.");
1201
+ return;
1202
+ }
1203
+ const adapter = createDeployAdapter(state.platform);
1204
+ configureAdapterContext(adapter, {
1205
+ projectRoot,
1206
+ appName: state.appName,
1207
+ region: state.region
1208
+ });
1209
+ const deploymentId = typeof args.id === "string" && args.id.length > 0 ? args.id : state.lastDeploymentId ?? "latest";
1210
+ await adapter.rollback(deploymentId);
1211
+ logger.success(`Rolled back ${state.platform} deployment to ${deploymentId}.`);
1212
+ }
1213
+ }),
1214
+ logs: defineCommand({
1215
+ meta: {
1216
+ name: "logs",
1217
+ description: "Read deployment logs"
1218
+ },
1219
+ async run() {
1220
+ const logger = createLogger();
1221
+ const projectRoot = await requireProjectRoot();
1222
+ const state = await readDeployState(projectRoot);
1223
+ if (!state) {
1224
+ logger.warn("No deployment state found. Run `kora deploy` first.");
1225
+ return;
1226
+ }
1227
+ const adapter = createDeployAdapter(state.platform);
1228
+ configureAdapterContext(adapter, {
1229
+ projectRoot,
1230
+ appName: state.appName,
1231
+ region: state.region
1232
+ });
1233
+ const logLines = adapter.logs({ tail: 200 });
1234
+ let hasLines = false;
1235
+ for await (const line of logLines) {
1236
+ hasLines = true;
1237
+ logger.step(`[${line.level}] ${line.message}`);
1238
+ }
1239
+ if (!hasLines) {
1240
+ logger.warn(`No logs returned from ${state.platform}.`);
1241
+ }
1242
+ }
1243
+ })
1244
+ },
1245
+ async run({ args, rawArgs }) {
1246
+ const subCommandNames = ["status", "rollback", "logs"];
1247
+ if (rawArgs.some((arg) => subCommandNames.includes(arg))) {
1248
+ return;
1249
+ }
1250
+ const logger = createLogger();
1251
+ const prompts = createPromptClient();
1252
+ const projectRoot = await requireProjectRoot();
1253
+ if (args.reset === true) {
1254
+ await resetDeployState(projectRoot);
1255
+ logger.success("Reset .kora/deploy state.");
1256
+ return;
1257
+ }
1258
+ const existingState = await readDeployState(projectRoot);
1259
+ const confirmMode = args.confirm === true;
1260
+ const platform = await resolvePlatform({
1261
+ promptClient: prompts,
1262
+ platformArg: args.platform,
1263
+ storedPlatform: existingState?.platform,
1264
+ confirm: confirmMode
1265
+ });
1266
+ const appName = resolveAppName(args.app, existingState?.appName, projectRoot, confirmMode);
1267
+ const region = resolveRegion(args.region, existingState?.region, confirmMode);
1268
+ const deployDirectory = resolveDeployDirectory(projectRoot);
1269
+ const environment = args.prod === true ? "production" : "preview";
1270
+ const config = {
1271
+ projectRoot,
1272
+ appName,
1273
+ region,
1274
+ environment,
1275
+ confirm: confirmMode
1276
+ };
1277
+ const adapter = createDeployAdapter(platform);
1278
+ configureAdapterContext(adapter, {
1279
+ projectRoot,
1280
+ appName,
1281
+ region
1282
+ });
1283
+ logger.banner();
1284
+ logger.info(
1285
+ `Deploying to ${platform} (${appName}${region ? `, ${region}` : ""}, ${environment})`
1286
+ );
1287
+ if (confirmMode) {
1288
+ logger.step("Running in --confirm mode (non-interactive, fail-fast).");
1289
+ }
1290
+ await writeDockerfileArtifact(deployDirectory, {
1291
+ nativeDependencies: {
1292
+ "better-sqlite3": "^11.0.0",
1293
+ "drizzle-orm": "^0.45.2"
1294
+ }
1295
+ });
1296
+ await writeDockerIgnoreArtifact(deployDirectory);
1297
+ const detected = await adapter.detect();
1298
+ if (!detected) {
1299
+ await adapter.install();
1300
+ }
1301
+ await adapter.authenticate();
1302
+ const provisionResult = await adapter.provision(config);
1303
+ const artifacts = await adapter.build(config);
1304
+ const deployResult = await adapter.deploy(artifacts);
1305
+ if (existingState) {
1306
+ await updateDeployState(projectRoot, {
1307
+ platform,
1308
+ appName,
1309
+ region,
1310
+ projectRoot,
1311
+ liveUrl: deployResult.liveUrl,
1312
+ syncUrl: deployResult.syncUrl,
1313
+ databaseId: provisionResult.databaseId,
1314
+ lastDeploymentId: deployResult.deploymentId
1315
+ });
1316
+ } else {
1317
+ await writeDeployState(projectRoot, {
1318
+ platform,
1319
+ appName,
1320
+ region,
1321
+ projectRoot,
1322
+ liveUrl: deployResult.liveUrl,
1323
+ syncUrl: deployResult.syncUrl,
1324
+ databaseId: provisionResult.databaseId,
1325
+ lastDeploymentId: deployResult.deploymentId
1326
+ });
1327
+ }
1328
+ logger.success(`Deployment completed: ${deployResult.liveUrl}`);
1329
+ if (deployResult.syncUrl) {
1330
+ logger.step(`Sync endpoint: ${deployResult.syncUrl}`);
1331
+ }
1332
+ }
1333
+ });
1334
+ async function resolvePlatform(options) {
1335
+ if (typeof options.platformArg === "string") {
1336
+ if (!isDeployPlatform(options.platformArg)) {
1337
+ throw new Error(
1338
+ `Invalid --platform value "${options.platformArg}". Valid options: ${DEPLOY_PLATFORMS.join(", ")}`
1339
+ );
1340
+ }
1341
+ return options.platformArg;
1342
+ }
1343
+ if (options.storedPlatform) {
1344
+ return options.storedPlatform;
1345
+ }
1346
+ if (options.confirm || !isInteractiveTerminal()) {
1347
+ throw new Error(
1348
+ "Missing deploy platform in --confirm mode. Provide --platform or run an interactive deploy first."
1349
+ );
1350
+ }
1351
+ return await options.promptClient.select("Where do you want to deploy?", [
1352
+ {
1353
+ label: "Fly.io (recommended for sync apps)",
1354
+ value: "fly"
1355
+ },
1356
+ {
1357
+ label: "Railway",
1358
+ value: "railway"
1359
+ },
1360
+ {
1361
+ label: "Render",
1362
+ value: "render"
1363
+ },
1364
+ {
1365
+ label: "Docker (self-hosted)",
1366
+ value: "docker"
1367
+ },
1368
+ {
1369
+ label: "Kora Cloud (coming soon)",
1370
+ value: "kora-cloud"
1371
+ }
1372
+ ]);
1373
+ }
1374
+ function resolveAppName(argValue, storedValue, projectRoot, confirm) {
1375
+ if (typeof argValue === "string" && argValue.length > 0) {
1376
+ return sanitizeAppName(argValue);
1377
+ }
1378
+ if (storedValue && storedValue.length > 0) {
1379
+ return storedValue;
1380
+ }
1381
+ if (confirm) {
1382
+ throw new Error(
1383
+ "Missing app name in --confirm mode. Provide --app or run an interactive deploy first."
1384
+ );
1385
+ }
1386
+ return sanitizeAppName(basename(projectRoot));
1387
+ }
1388
+ function resolveRegion(argValue, storedValue, confirm) {
1389
+ if (typeof argValue === "string" && argValue.length > 0) return argValue;
1390
+ if (storedValue !== void 0) return storedValue;
1391
+ if (confirm) {
1392
+ throw new Error(
1393
+ "Missing region in --confirm mode. Provide --region or run an interactive deploy first."
1394
+ );
1395
+ }
1396
+ return "iad";
1397
+ }
1398
+ function sanitizeAppName(value) {
1399
+ const normalized = value.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
1400
+ if (normalized.length === 0) {
1401
+ return "kora-app";
1402
+ }
1403
+ return normalized;
1404
+ }
1405
+ async function requireProjectRoot() {
1406
+ const projectRoot = await findProjectRoot();
1407
+ if (!projectRoot) {
1408
+ throw new InvalidProjectError(process.cwd());
1409
+ }
1410
+ return projectRoot;
1411
+ }
1412
+ function isInteractiveTerminal() {
1413
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
1414
+ }
1415
+ function configureAdapterContext(adapter, context) {
1416
+ if (hasContextSetter(adapter)) {
1417
+ adapter.setContext(context);
1418
+ }
1419
+ }
1420
+ function hasContextSetter(adapter) {
1421
+ return typeof adapter.setContext === "function";
1422
+ }
1423
+
1424
+ // src/commands/generate/type-generator.ts
1425
+ function generateTypes(schema) {
1426
+ const lines = [
1427
+ "// Auto-generated by @korajs/cli \u2014 do not edit manually",
1428
+ `// Generated from schema version ${String(schema.version)}`,
1429
+ ""
1430
+ ];
1431
+ const collectionNames = Object.keys(schema.collections);
1432
+ if (collectionNames.length === 0) {
1433
+ return lines.join("\n");
1434
+ }
1435
+ for (const [name, collection] of Object.entries(schema.collections)) {
1436
+ const pascal = toPascalCase(name);
1437
+ const fields = collection.fields;
1438
+ lines.push(`export interface ${pascal}Record {`);
1439
+ lines.push(" readonly id: string");
1440
+ for (const [fieldName, descriptor] of Object.entries(fields)) {
1441
+ const tsType = fieldKindToTypeScript(descriptor);
1442
+ const optional = !descriptor.required && !descriptor.auto ? "?" : "";
1443
+ lines.push(` readonly ${fieldName}${optional}: ${tsType}`);
1444
+ }
1445
+ lines.push("}");
1446
+ lines.push("");
1447
+ lines.push(`export interface ${pascal}InsertInput {`);
1448
+ for (const [fieldName, descriptor] of Object.entries(fields)) {
1449
+ if (descriptor.auto) continue;
1450
+ const tsType = fieldKindToTypeScript(descriptor);
1451
+ const optional = !descriptor.required || descriptor.defaultValue !== void 0 ? "?" : "";
1452
+ lines.push(` ${fieldName}${optional}: ${tsType}`);
1453
+ }
1454
+ lines.push("}");
1455
+ lines.push("");
1456
+ lines.push(`export interface ${pascal}UpdateInput {`);
1457
+ for (const [fieldName, descriptor] of Object.entries(fields)) {
1458
+ if (descriptor.auto) continue;
1459
+ const tsType = fieldKindToTypeScript(descriptor);
1460
+ lines.push(` ${fieldName}?: ${tsType}`);
1461
+ }
1462
+ lines.push("}");
1463
+ lines.push("");
1464
+ }
1465
+ return lines.join("\n");
1466
+ }
1467
+ function fieldKindToTypeScript(descriptor) {
1468
+ switch (descriptor.kind) {
1469
+ case "string":
1470
+ return "string";
1471
+ case "number":
1472
+ return "number";
1473
+ case "boolean":
1474
+ return "boolean";
1475
+ case "timestamp":
1476
+ return "number";
1477
+ case "richtext":
1478
+ return "string";
1479
+ case "enum": {
1480
+ if (descriptor.enumValues && descriptor.enumValues.length > 0) {
1481
+ return descriptor.enumValues.map((v) => `'${v}'`).join(" | ");
1482
+ }
1483
+ return "string";
1484
+ }
1485
+ case "array": {
1486
+ const itemType = itemKindToTypeScript(descriptor.itemKind);
1487
+ return `Array<${itemType}>`;
1488
+ }
1489
+ default:
1490
+ return "unknown";
1491
+ }
1492
+ }
1493
+ function itemKindToTypeScript(kind) {
1494
+ switch (kind) {
1495
+ case "string":
1496
+ return "string";
1497
+ case "number":
1498
+ return "number";
1499
+ case "boolean":
1500
+ return "boolean";
1501
+ case "timestamp":
1502
+ return "number";
1503
+ case "richtext":
1504
+ return "string";
1505
+ case "enum":
1506
+ return "string";
1507
+ case "array":
1508
+ return "unknown[]";
1509
+ default:
1510
+ return "unknown";
1511
+ }
1512
+ }
1513
+ function toPascalCase(name) {
1514
+ return name.split(/[_-]/).map((part) => {
1515
+ if (part.length === 0) return "";
1516
+ const first = part[0];
1517
+ return first ? first.toUpperCase() + part.slice(1) : "";
1518
+ }).join("");
1519
+ }
1520
+
1521
+ export {
1522
+ DEPLOY_PLATFORMS,
1523
+ isDeployPlatform,
1524
+ generateFlyToml,
1525
+ writeFlyTomlArtifact,
1526
+ buildClient,
1527
+ bundleServer,
1528
+ FlyAdapter,
1529
+ NodeFlyCommandRunner,
1530
+ generateRailwayJson,
1531
+ writeRailwayJsonArtifact,
1532
+ RailwayAdapter,
1533
+ NodeRailwayCommandRunner,
1534
+ StubDeployAdapter,
1535
+ createDeployAdapter,
1536
+ generateDockerfile,
1537
+ generateDeployPackageJson,
1538
+ generateDockerIgnore,
1539
+ writeDockerfileArtifact,
1540
+ writeDockerIgnoreArtifact,
1541
+ resolveDeployDirectory,
1542
+ resolveDeployStatePath,
1543
+ readDeployState,
1544
+ writeDeployState,
1545
+ updateDeployState,
1546
+ resetDeployState,
1547
+ deployCommand,
1548
+ generateTypes
1549
+ };
1550
+ //# sourceMappingURL=chunk-E4JG7THU.js.map