@korajs/cli 0.2.0 → 0.3.1

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.
@@ -8,54 +8,16 @@ import {
8
8
  } from "./chunk-KTSRAPSE.js";
9
9
 
10
10
  // src/commands/deploy/adapters/adapter.ts
11
- var DEPLOY_PLATFORMS = ["fly", "railway", "render", "docker", "kora-cloud"];
11
+ var DEPLOY_PLATFORMS = ["fly", "railway", "aws-ecs", "aws-lightsail", "render", "docker", "kora-cloud"];
12
12
  function isDeployPlatform(value) {
13
13
  return DEPLOY_PLATFORMS.includes(value);
14
14
  }
15
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
16
  // src/commands/deploy/builder/client-builder.ts
55
17
  import { spawn } from "child_process";
56
18
  import { copyFile, readdir } from "fs/promises";
57
19
  import { existsSync } from "fs";
58
- import { join as join2, resolve } from "path";
20
+ import { join, resolve } from "path";
59
21
  async function buildClient(options) {
60
22
  const viteEntryPoint = await resolveProjectBinaryEntryPoint(options.projectRoot, "vite", "vite");
61
23
  if (!viteEntryPoint) {
@@ -76,12 +38,12 @@ async function buildClient(options) {
76
38
  return { outDir: options.outDir };
77
39
  }
78
40
  async function patchSqliteWasmAssets(projectRoot, outDir) {
79
- const assetsDir = join2(outDir, "assets");
41
+ const assetsDir = join(outDir, "assets");
80
42
  if (!existsSync(assetsDir)) return;
81
43
  const files = await readdir(assetsDir);
82
44
  const hashedWasm = files.find((f) => /^sqlite3-.+\.wasm$/.test(f));
83
45
  if (hashedWasm && !files.includes("sqlite3.wasm")) {
84
- await copyFile(join2(assetsDir, hashedWasm), join2(assetsDir, "sqlite3.wasm"));
46
+ await copyFile(join(assetsDir, hashedWasm), join(assetsDir, "sqlite3.wasm"));
85
47
  }
86
48
  if (!files.includes("sqlite3-opfs-async-proxy.js")) {
87
49
  const proxyFile = resolve(
@@ -94,7 +56,7 @@ async function patchSqliteWasmAssets(projectRoot, outDir) {
94
56
  "sqlite3-opfs-async-proxy.js"
95
57
  );
96
58
  if (existsSync(proxyFile)) {
97
- await copyFile(proxyFile, join2(assetsDir, "sqlite3-opfs-async-proxy.js"));
59
+ await copyFile(proxyFile, join(assetsDir, "sqlite3-opfs-async-proxy.js"));
98
60
  }
99
61
  }
100
62
  }
@@ -119,9 +81,9 @@ async function runProcess(command, args, cwd) {
119
81
  }
120
82
 
121
83
  // src/commands/deploy/builder/server-bundler.ts
122
- import { mkdir as mkdir2 } from "fs/promises";
84
+ import { mkdir } from "fs/promises";
123
85
  import { access } from "fs/promises";
124
- import { join as join3 } from "path";
86
+ import { join as join2 } from "path";
125
87
  import { build } from "esbuild";
126
88
  var DEFAULT_ENTRY_CANDIDATES = [
127
89
  "server.ts",
@@ -137,8 +99,8 @@ async function bundleServer(options) {
137
99
  `Could not find a server entry file in ${options.projectRoot}. Looked for: ${candidates.join(", ")}`
138
100
  );
139
101
  }
140
- await mkdir2(options.deployDirectory, { recursive: true });
141
- const outputFilePath = join3(options.deployDirectory, "server-bundled.js");
102
+ await mkdir(options.deployDirectory, { recursive: true });
103
+ const outputFilePath = join2(options.deployDirectory, "server-bundled.js");
142
104
  await build({
143
105
  entryPoints: [entryFilePath],
144
106
  outfile: outputFilePath,
@@ -160,7 +122,7 @@ async function bundleServer(options) {
160
122
  }
161
123
  async function resolveServerEntry(projectRoot, candidates) {
162
124
  for (const candidate of candidates) {
163
- const fullPath = join3(projectRoot, candidate);
125
+ const fullPath = join2(projectRoot, candidate);
164
126
  try {
165
127
  await access(fullPath);
166
128
  return fullPath;
@@ -170,6 +132,44 @@ async function resolveServerEntry(projectRoot, candidates) {
170
132
  return null;
171
133
  }
172
134
 
135
+ // src/commands/deploy/artifacts/fly-toml-generator.ts
136
+ import { mkdir as mkdir2, writeFile } from "fs/promises";
137
+ import { join as join3 } from "path";
138
+ var GENERATED_HEADER = "# Generated by kora deploy \u2014 do not edit manually";
139
+ function generateFlyToml(options) {
140
+ const internalPort = options.internalPort ?? 3e3;
141
+ return [
142
+ GENERATED_HEADER,
143
+ "",
144
+ `app = "${options.appName}"`,
145
+ `primary_region = "${options.region}"`,
146
+ "",
147
+ "[build]",
148
+ ` dockerfile = "Dockerfile"`,
149
+ "",
150
+ "[http_service]",
151
+ ` internal_port = ${String(internalPort)}`,
152
+ " force_https = true",
153
+ ' auto_stop_machines = "stop"',
154
+ " auto_start_machines = true",
155
+ " min_machines_running = 0",
156
+ "",
157
+ " [[http_service.checks]]",
158
+ ' grace_period = "10s"',
159
+ ' interval = "30s"',
160
+ ' method = "GET"',
161
+ ' timeout = "5s"',
162
+ ' path = "/"',
163
+ ""
164
+ ].join("\n");
165
+ }
166
+ async function writeFlyTomlArtifact(deployDirectory, options) {
167
+ await mkdir2(deployDirectory, { recursive: true });
168
+ const filePath = join3(deployDirectory, "fly.toml");
169
+ await writeFile(filePath, generateFlyToml(options), "utf-8");
170
+ return filePath;
171
+ }
172
+
173
173
  // src/commands/deploy/adapters/fly-adapter.ts
174
174
  import { spawn as spawn2 } from "child_process";
175
175
  import { join as join4 } from "path";
@@ -896,6 +896,745 @@ var StubDeployAdapter = class {
896
896
  }
897
897
  };
898
898
 
899
+ // src/commands/deploy/adapters/aws-ecs-adapter.ts
900
+ import { spawn as spawn4 } from "child_process";
901
+ import { join as join7 } from "path";
902
+ var AwsEcsAdapter = class {
903
+ name = "aws-ecs";
904
+ logger = createLogger();
905
+ runner;
906
+ currentContext;
907
+ constructor(options = {}) {
908
+ this.runner = options.runner ?? new NodeAwsCommandRunner();
909
+ this.currentContext = options.context ?? null;
910
+ }
911
+ setContext(context) {
912
+ this.currentContext = context;
913
+ }
914
+ async detect() {
915
+ const result = await this.runner.run("aws", ["--version"], process.cwd());
916
+ return result.exitCode === 0;
917
+ }
918
+ async install() {
919
+ const available = await this.detect();
920
+ if (!available) {
921
+ throw new Error(
922
+ "AWS CLI is required but not installed. Install from https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html"
923
+ );
924
+ }
925
+ }
926
+ async authenticate() {
927
+ const cwd = this.currentContext?.projectRoot ?? process.cwd();
928
+ const result = await this.runner.run("aws", ["sts", "get-caller-identity"], cwd);
929
+ if (result.exitCode !== 0) {
930
+ throw new Error(
931
+ "AWS CLI is not authenticated. Run `aws configure` or set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables."
932
+ );
933
+ }
934
+ }
935
+ async provision(config) {
936
+ this.currentContext = {
937
+ projectRoot: config.projectRoot,
938
+ appName: config.appName,
939
+ region: config.region ?? "us-east-1"
940
+ };
941
+ const region = config.region ?? "us-east-1";
942
+ const repoName = `kora/${config.appName}`;
943
+ const createRepo = await this.runner.run("aws", [
944
+ "ecr",
945
+ "create-repository",
946
+ "--repository-name",
947
+ repoName,
948
+ "--region",
949
+ region,
950
+ "--image-scanning-configuration",
951
+ "scanOnPush=true"
952
+ ], config.projectRoot);
953
+ if (createRepo.exitCode !== 0 && !createRepo.stderr.includes("RepositoryAlreadyExistsException")) {
954
+ throw new Error(`Failed to create ECR repository: ${createRepo.stderr}`);
955
+ }
956
+ const identity = await this.runner.run("aws", ["sts", "get-caller-identity", "--query", "Account", "--output", "text"], config.projectRoot);
957
+ const accountId = identity.stdout.trim();
958
+ await this.runner.run("aws", [
959
+ "ecs",
960
+ "create-cluster",
961
+ "--cluster-name",
962
+ config.appName,
963
+ "--region",
964
+ region
965
+ ], config.projectRoot);
966
+ await this.runner.run("aws", [
967
+ "logs",
968
+ "create-log-group",
969
+ "--log-group-name",
970
+ `/ecs/${config.appName}`,
971
+ "--region",
972
+ region
973
+ ], config.projectRoot);
974
+ return {
975
+ applicationId: `${accountId}.dkr.ecr.${region}.amazonaws.com/${repoName}`,
976
+ databaseId: null,
977
+ secretsSet: ["PORT"]
978
+ };
979
+ }
980
+ async build(config) {
981
+ this.currentContext = {
982
+ projectRoot: config.projectRoot,
983
+ appName: config.appName,
984
+ region: config.region
985
+ };
986
+ const deployDirectory = join7(config.projectRoot, ".kora", "deploy");
987
+ await bundleServer({
988
+ projectRoot: config.projectRoot,
989
+ deployDirectory
990
+ });
991
+ const client = await buildClient({
992
+ projectRoot: config.projectRoot,
993
+ outDir: join7(deployDirectory, "dist"),
994
+ mode: "production"
995
+ });
996
+ return {
997
+ clientDirectory: client.outDir,
998
+ serverBundlePath: join7(deployDirectory, "server-bundled.js"),
999
+ deployDirectory
1000
+ };
1001
+ }
1002
+ async deploy(artifacts) {
1003
+ const context = this.requireContext();
1004
+ const region = context.region ?? "us-east-1";
1005
+ const repoName = `kora/${context.appName}`;
1006
+ const loginPassword = await this.runner.run("aws", [
1007
+ "ecr",
1008
+ "get-login-password",
1009
+ "--region",
1010
+ region
1011
+ ], context.projectRoot);
1012
+ if (loginPassword.exitCode !== 0) {
1013
+ throw new Error(`ECR login failed: ${loginPassword.stderr}`);
1014
+ }
1015
+ const identity = await this.runner.run("aws", [
1016
+ "sts",
1017
+ "get-caller-identity",
1018
+ "--query",
1019
+ "Account",
1020
+ "--output",
1021
+ "text"
1022
+ ], context.projectRoot);
1023
+ const accountId = identity.stdout.trim();
1024
+ const ecrUri = `${accountId}.dkr.ecr.${region}.amazonaws.com`;
1025
+ const imageUri = `${ecrUri}/${repoName}:latest`;
1026
+ const dockerLogin = await this.runner.run("docker", [
1027
+ "login",
1028
+ "--username",
1029
+ "AWS",
1030
+ "--password-stdin",
1031
+ ecrUri
1032
+ ], artifacts.deployDirectory);
1033
+ this.logger.step("Building Docker image...");
1034
+ const dockerBuild = await this.runner.run("docker", [
1035
+ "build",
1036
+ "--platform",
1037
+ "linux/amd64",
1038
+ "-t",
1039
+ imageUri,
1040
+ "."
1041
+ ], artifacts.deployDirectory);
1042
+ if (dockerBuild.exitCode !== 0) {
1043
+ throw new Error(`Docker build failed: ${dockerBuild.stderr}`);
1044
+ }
1045
+ this.logger.step("Pushing image to ECR...");
1046
+ const dockerPush = await this.runner.run("docker", [
1047
+ "push",
1048
+ imageUri
1049
+ ], artifacts.deployDirectory);
1050
+ if (dockerPush.exitCode !== 0) {
1051
+ throw new Error(`Docker push failed: ${dockerPush.stderr}`);
1052
+ }
1053
+ const taskDef = JSON.stringify({
1054
+ family: context.appName,
1055
+ networkMode: "awsvpc",
1056
+ requiresCompatibilities: ["FARGATE"],
1057
+ cpu: "256",
1058
+ memory: "512",
1059
+ executionRoleArn: `arn:aws:iam::${accountId}:role/ecsTaskExecutionRole`,
1060
+ containerDefinitions: [{
1061
+ name: context.appName,
1062
+ image: imageUri,
1063
+ essential: true,
1064
+ portMappings: [{ containerPort: 3001, protocol: "tcp" }],
1065
+ logConfiguration: {
1066
+ logDriver: "awslogs",
1067
+ options: {
1068
+ "awslogs-group": `/ecs/${context.appName}`,
1069
+ "awslogs-region": region,
1070
+ "awslogs-stream-prefix": "ecs"
1071
+ }
1072
+ },
1073
+ healthCheck: {
1074
+ command: ["CMD-SHELL", "curl -f http://localhost:3001/health || exit 1"],
1075
+ interval: 30,
1076
+ timeout: 5,
1077
+ retries: 3,
1078
+ startPeriod: 60
1079
+ }
1080
+ }]
1081
+ });
1082
+ const registerTask = await this.runner.run("aws", [
1083
+ "ecs",
1084
+ "register-task-definition",
1085
+ "--cli-input-json",
1086
+ taskDef,
1087
+ "--region",
1088
+ region
1089
+ ], context.projectRoot);
1090
+ if (registerTask.exitCode !== 0) {
1091
+ throw new Error(`Task definition registration failed: ${registerTask.stderr}`);
1092
+ }
1093
+ const updateService = await this.runner.run("aws", [
1094
+ "ecs",
1095
+ "update-service",
1096
+ "--cluster",
1097
+ context.appName,
1098
+ "--service",
1099
+ context.appName,
1100
+ "--task-definition",
1101
+ context.appName,
1102
+ "--force-new-deployment",
1103
+ "--region",
1104
+ region
1105
+ ], context.projectRoot);
1106
+ const deploymentId = (/* @__PURE__ */ new Date()).toISOString();
1107
+ if (updateService.exitCode !== 0) {
1108
+ this.logger.step("Service not found, creating new service...");
1109
+ this.logger.step(
1110
+ `Task definition registered. Create the ECS service with:
1111
+ aws ecs create-service \\
1112
+ --cluster ${context.appName} \\
1113
+ --service-name ${context.appName} \\
1114
+ --task-definition ${context.appName} \\
1115
+ --desired-count 1 \\
1116
+ --launch-type FARGATE \\
1117
+ --network-configuration "awsvpcConfiguration={subnets=[<subnet-id>],securityGroups=[<sg-id>],assignPublicIp=ENABLED}" \\
1118
+ --region ${region}`
1119
+ );
1120
+ }
1121
+ return {
1122
+ deploymentId,
1123
+ liveUrl: `https://${context.appName}.${region}.amazonaws.com`,
1124
+ syncUrl: `wss://${context.appName}.${region}.amazonaws.com/kora-sync`
1125
+ };
1126
+ }
1127
+ async rollback(deploymentId) {
1128
+ const context = this.requireContext();
1129
+ const region = context.region ?? "us-east-1";
1130
+ const result = await this.runner.run("aws", [
1131
+ "ecs",
1132
+ "update-service",
1133
+ "--cluster",
1134
+ context.appName,
1135
+ "--service",
1136
+ context.appName,
1137
+ "--task-definition",
1138
+ `${context.appName}:${deploymentId}`,
1139
+ "--force-new-deployment",
1140
+ "--region",
1141
+ region
1142
+ ], context.projectRoot);
1143
+ if (result.exitCode !== 0) {
1144
+ throw new Error(`ECS rollback failed: ${result.stderr}`);
1145
+ }
1146
+ }
1147
+ async *logs(options) {
1148
+ const context = this.requireContext();
1149
+ const region = context.region ?? "us-east-1";
1150
+ const args = [
1151
+ "logs",
1152
+ "get-log-events",
1153
+ "--log-group-name",
1154
+ `/ecs/${context.appName}`,
1155
+ "--log-stream-name",
1156
+ "ecs/latest",
1157
+ "--region",
1158
+ region
1159
+ ];
1160
+ if (options.tail) {
1161
+ args.push("--limit", String(options.tail));
1162
+ }
1163
+ const result = await this.runner.run("aws", args, context.projectRoot);
1164
+ if (result.exitCode !== 0) {
1165
+ return;
1166
+ }
1167
+ try {
1168
+ const parsed = JSON.parse(result.stdout);
1169
+ for (const event of parsed.events ?? []) {
1170
+ yield {
1171
+ timestamp: new Date(event.timestamp).toISOString(),
1172
+ level: inferLogLevel3(event.message),
1173
+ message: event.message
1174
+ };
1175
+ }
1176
+ } catch {
1177
+ for (const line of result.stdout.split("\n").filter(Boolean)) {
1178
+ yield { timestamp: (/* @__PURE__ */ new Date()).toISOString(), level: "info", message: line };
1179
+ }
1180
+ }
1181
+ }
1182
+ async status() {
1183
+ const context = this.requireContext();
1184
+ const region = context.region ?? "us-east-1";
1185
+ const result = await this.runner.run("aws", [
1186
+ "ecs",
1187
+ "describe-services",
1188
+ "--cluster",
1189
+ context.appName,
1190
+ "--services",
1191
+ context.appName,
1192
+ "--region",
1193
+ region
1194
+ ], context.projectRoot);
1195
+ if (result.exitCode !== 0) {
1196
+ return { state: "failed", message: result.stderr };
1197
+ }
1198
+ try {
1199
+ const parsed = JSON.parse(result.stdout);
1200
+ const service = parsed.services?.[0];
1201
+ if (!service) {
1202
+ return { state: "unknown", message: "Service not found" };
1203
+ }
1204
+ if (service.status === "ACTIVE" && service.runningCount > 0) {
1205
+ return {
1206
+ state: "healthy",
1207
+ message: `Running ${service.runningCount}/${service.desiredCount} tasks`
1208
+ };
1209
+ }
1210
+ return {
1211
+ state: service.runningCount === 0 ? "pending" : "healthy",
1212
+ message: `Status: ${service.status}, running: ${service.runningCount}/${service.desiredCount}`
1213
+ };
1214
+ } catch {
1215
+ return { state: "unknown", message: "Could not parse service status" };
1216
+ }
1217
+ }
1218
+ requireContext() {
1219
+ if (!this.currentContext) {
1220
+ throw new Error("AWS ECS adapter context is not initialized. Run provision() first.");
1221
+ }
1222
+ return this.currentContext;
1223
+ }
1224
+ };
1225
+ var NodeAwsCommandRunner = class {
1226
+ async run(command, args, cwd) {
1227
+ return new Promise((resolve2) => {
1228
+ const child = spawn4(command, args, {
1229
+ cwd,
1230
+ env: process.env,
1231
+ stdio: ["ignore", "pipe", "pipe"]
1232
+ });
1233
+ let stdout = "";
1234
+ let stderr = "";
1235
+ child.stdout?.on("data", (chunk) => {
1236
+ stdout += chunk.toString("utf-8");
1237
+ });
1238
+ child.stderr?.on("data", (chunk) => {
1239
+ stderr += chunk.toString("utf-8");
1240
+ });
1241
+ child.on("error", (error) => {
1242
+ resolve2({ exitCode: 1, stdout, stderr: `${stderr}
1243
+ ${error.message}` });
1244
+ });
1245
+ child.on("exit", (code) => {
1246
+ resolve2({ exitCode: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() });
1247
+ });
1248
+ });
1249
+ }
1250
+ };
1251
+ function inferLogLevel3(line) {
1252
+ const normalized = line.toLowerCase();
1253
+ if (normalized.includes("error")) return "error";
1254
+ if (normalized.includes("warn")) return "warn";
1255
+ if (normalized.includes("debug")) return "debug";
1256
+ return "info";
1257
+ }
1258
+
1259
+ // src/commands/deploy/adapters/aws-lightsail-adapter.ts
1260
+ import { spawn as spawn5 } from "child_process";
1261
+ import { join as join8 } from "path";
1262
+ var AwsLightsailAdapter = class {
1263
+ name = "aws-lightsail";
1264
+ logger = createLogger();
1265
+ runner;
1266
+ currentContext;
1267
+ constructor(options = {}) {
1268
+ this.runner = options.runner ?? new NodeAwsLightsailCommandRunner();
1269
+ this.currentContext = options.context ?? null;
1270
+ }
1271
+ setContext(context) {
1272
+ this.currentContext = context;
1273
+ }
1274
+ async detect() {
1275
+ const result = await this.runner.run("aws", ["--version"], process.cwd());
1276
+ return result.exitCode === 0;
1277
+ }
1278
+ async install() {
1279
+ const available = await this.detect();
1280
+ if (!available) {
1281
+ throw new Error(
1282
+ "AWS CLI is required but not installed. Install from https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html"
1283
+ );
1284
+ }
1285
+ }
1286
+ async authenticate() {
1287
+ const cwd = this.currentContext?.projectRoot ?? process.cwd();
1288
+ const result = await this.runner.run("aws", ["sts", "get-caller-identity"], cwd);
1289
+ if (result.exitCode !== 0) {
1290
+ throw new Error(
1291
+ "AWS CLI is not authenticated. Run `aws configure` or set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables."
1292
+ );
1293
+ }
1294
+ }
1295
+ async provision(config) {
1296
+ this.currentContext = {
1297
+ projectRoot: config.projectRoot,
1298
+ appName: config.appName,
1299
+ region: config.region ?? "us-east-1"
1300
+ };
1301
+ const region = config.region ?? "us-east-1";
1302
+ const serviceName = sanitizeLightsailName(config.appName);
1303
+ const createService = await this.runner.run("aws", [
1304
+ "lightsail",
1305
+ "create-container-service",
1306
+ "--service-name",
1307
+ serviceName,
1308
+ "--power",
1309
+ "nano",
1310
+ "--scale",
1311
+ "1",
1312
+ "--region",
1313
+ region
1314
+ ], config.projectRoot);
1315
+ if (createService.exitCode !== 0 && !createService.stderr.includes("already exists")) {
1316
+ throw new Error(`Failed to create Lightsail container service: ${createService.stderr}`);
1317
+ }
1318
+ return {
1319
+ applicationId: serviceName,
1320
+ databaseId: null,
1321
+ secretsSet: ["PORT"]
1322
+ };
1323
+ }
1324
+ async build(config) {
1325
+ this.currentContext = {
1326
+ projectRoot: config.projectRoot,
1327
+ appName: config.appName,
1328
+ region: config.region
1329
+ };
1330
+ const deployDirectory = join8(config.projectRoot, ".kora", "deploy");
1331
+ await bundleServer({
1332
+ projectRoot: config.projectRoot,
1333
+ deployDirectory
1334
+ });
1335
+ const client = await buildClient({
1336
+ projectRoot: config.projectRoot,
1337
+ outDir: join8(deployDirectory, "dist"),
1338
+ mode: "production"
1339
+ });
1340
+ return {
1341
+ clientDirectory: client.outDir,
1342
+ serverBundlePath: join8(deployDirectory, "server-bundled.js"),
1343
+ deployDirectory
1344
+ };
1345
+ }
1346
+ async deploy(artifacts) {
1347
+ const context = this.requireContext();
1348
+ const region = context.region ?? "us-east-1";
1349
+ const serviceName = sanitizeLightsailName(context.appName);
1350
+ const imageTag = `${serviceName}:latest`;
1351
+ this.logger.step("Building Docker image...");
1352
+ const dockerBuild = await this.runner.run("docker", [
1353
+ "build",
1354
+ "--platform",
1355
+ "linux/amd64",
1356
+ "-t",
1357
+ imageTag,
1358
+ "."
1359
+ ], artifacts.deployDirectory);
1360
+ if (dockerBuild.exitCode !== 0) {
1361
+ throw new Error(`Docker build failed: ${dockerBuild.stderr}`);
1362
+ }
1363
+ this.logger.step("Pushing image to Lightsail...");
1364
+ const pushImage = await this.runner.run("aws", [
1365
+ "lightsail",
1366
+ "push-container-image",
1367
+ "--service-name",
1368
+ serviceName,
1369
+ "--label",
1370
+ "latest",
1371
+ "--image",
1372
+ imageTag,
1373
+ "--region",
1374
+ region
1375
+ ], artifacts.deployDirectory);
1376
+ if (pushImage.exitCode !== 0) {
1377
+ throw new Error(`Lightsail image push failed: ${pushImage.stderr}`);
1378
+ }
1379
+ const lightsailImage = parseLightsailImageRef(pushImage.stdout) ?? `:${serviceName}.latest.1`;
1380
+ this.logger.step("Creating Lightsail deployment...");
1381
+ const environment = { PORT: "3001" };
1382
+ for (const key of PASSTHROUGH_ENV_VARS) {
1383
+ const value = process.env[key];
1384
+ if (value) {
1385
+ environment[key] = value;
1386
+ }
1387
+ }
1388
+ const containers = JSON.stringify({
1389
+ [serviceName]: {
1390
+ image: lightsailImage,
1391
+ ports: { "3001": "HTTP" },
1392
+ environment
1393
+ }
1394
+ });
1395
+ const publicEndpoint = JSON.stringify({
1396
+ containerName: serviceName,
1397
+ containerPort: 3001,
1398
+ healthCheck: {
1399
+ path: "/health",
1400
+ intervalSeconds: 30,
1401
+ timeoutSeconds: 5,
1402
+ healthyThreshold: 2,
1403
+ unhealthyThreshold: 3
1404
+ }
1405
+ });
1406
+ const createDeploy = await this.runner.run("aws", [
1407
+ "lightsail",
1408
+ "create-container-service-deployment",
1409
+ "--service-name",
1410
+ serviceName,
1411
+ "--containers",
1412
+ containers,
1413
+ "--public-endpoint",
1414
+ publicEndpoint,
1415
+ "--region",
1416
+ region
1417
+ ], context.projectRoot);
1418
+ if (createDeploy.exitCode !== 0) {
1419
+ throw new Error(`Lightsail deployment failed: ${createDeploy.stderr}`);
1420
+ }
1421
+ const serviceInfo = await this.runner.run("aws", [
1422
+ "lightsail",
1423
+ "get-container-services",
1424
+ "--service-name",
1425
+ serviceName,
1426
+ "--region",
1427
+ region
1428
+ ], context.projectRoot);
1429
+ const rawUrl = parseLightsailUrl(serviceInfo.stdout) ?? `https://${serviceName}.${region}.cs.amazonlightsail.com`;
1430
+ const serviceUrl = rawUrl.replace(/\/+$/, "");
1431
+ const deploymentId = (/* @__PURE__ */ new Date()).toISOString();
1432
+ return {
1433
+ deploymentId,
1434
+ liveUrl: serviceUrl,
1435
+ syncUrl: `${serviceUrl.replace("https://", "wss://")}/kora-sync`
1436
+ };
1437
+ }
1438
+ async rollback(_deploymentId) {
1439
+ const context = this.requireContext();
1440
+ const region = context.region ?? "us-east-1";
1441
+ const serviceName = sanitizeLightsailName(context.appName);
1442
+ const deployments = await this.runner.run("aws", [
1443
+ "lightsail",
1444
+ "get-container-service-deployments",
1445
+ "--service-name",
1446
+ serviceName,
1447
+ "--region",
1448
+ region
1449
+ ], context.projectRoot);
1450
+ if (deployments.exitCode !== 0) {
1451
+ throw new Error(`Lightsail rollback failed: ${deployments.stderr}`);
1452
+ }
1453
+ const previousDeployment = parsePreviousDeployment(deployments.stdout, serviceName);
1454
+ if (!previousDeployment) {
1455
+ throw new Error("No previous deployment found to rollback to.");
1456
+ }
1457
+ const redeploy = await this.runner.run("aws", [
1458
+ "lightsail",
1459
+ "create-container-service-deployment",
1460
+ "--service-name",
1461
+ serviceName,
1462
+ "--containers",
1463
+ JSON.stringify(previousDeployment.containers),
1464
+ "--public-endpoint",
1465
+ JSON.stringify(previousDeployment.publicEndpoint),
1466
+ "--region",
1467
+ region
1468
+ ], context.projectRoot);
1469
+ if (redeploy.exitCode !== 0) {
1470
+ throw new Error(`Lightsail rollback deployment failed: ${redeploy.stderr}`);
1471
+ }
1472
+ }
1473
+ async *logs(options) {
1474
+ const context = this.requireContext();
1475
+ const region = context.region ?? "us-east-1";
1476
+ const serviceName = sanitizeLightsailName(context.appName);
1477
+ const args = [
1478
+ "lightsail",
1479
+ "get-container-log",
1480
+ "--service-name",
1481
+ serviceName,
1482
+ "--container-name",
1483
+ serviceName,
1484
+ "--region",
1485
+ region
1486
+ ];
1487
+ if (options.since) {
1488
+ args.push("--start-time", options.since);
1489
+ }
1490
+ const result = await this.runner.run("aws", args, context.projectRoot);
1491
+ if (result.exitCode !== 0) {
1492
+ return;
1493
+ }
1494
+ try {
1495
+ const parsed = JSON.parse(result.stdout);
1496
+ const events = parsed.logEvents ?? [];
1497
+ const limited = options.tail ? events.slice(-options.tail) : events;
1498
+ for (const event of limited) {
1499
+ yield {
1500
+ timestamp: event.createdAt,
1501
+ level: inferLogLevel4(event.message),
1502
+ message: event.message
1503
+ };
1504
+ }
1505
+ } catch {
1506
+ for (const line of result.stdout.split("\n").filter(Boolean)) {
1507
+ yield { timestamp: (/* @__PURE__ */ new Date()).toISOString(), level: "info", message: line };
1508
+ }
1509
+ }
1510
+ }
1511
+ async status() {
1512
+ const context = this.requireContext();
1513
+ const region = context.region ?? "us-east-1";
1514
+ const serviceName = sanitizeLightsailName(context.appName);
1515
+ const result = await this.runner.run("aws", [
1516
+ "lightsail",
1517
+ "get-container-services",
1518
+ "--service-name",
1519
+ serviceName,
1520
+ "--region",
1521
+ region
1522
+ ], context.projectRoot);
1523
+ if (result.exitCode !== 0) {
1524
+ return { state: "failed", message: result.stderr };
1525
+ }
1526
+ try {
1527
+ const parsed = JSON.parse(result.stdout);
1528
+ const service = parsed.containerServices?.[0];
1529
+ if (!service) {
1530
+ return { state: "unknown", message: "Container service not found" };
1531
+ }
1532
+ const deployState = service.currentDeployment?.state ?? "UNKNOWN";
1533
+ if (service.state === "RUNNING" && deployState === "ACTIVE") {
1534
+ return {
1535
+ state: "healthy",
1536
+ message: "Lightsail container service is running",
1537
+ liveUrl: service.url
1538
+ };
1539
+ }
1540
+ if (service.state === "DEPLOYING" || deployState === "ACTIVATING") {
1541
+ return {
1542
+ state: "pending",
1543
+ message: `Service: ${service.state}, Deployment: ${deployState}`
1544
+ };
1545
+ }
1546
+ return {
1547
+ state: service.state === "DISABLED" ? "failed" : "unknown",
1548
+ message: `Service: ${service.state}, Deployment: ${deployState}`
1549
+ };
1550
+ } catch {
1551
+ return { state: "unknown", message: "Could not parse service status" };
1552
+ }
1553
+ }
1554
+ requireContext() {
1555
+ if (!this.currentContext) {
1556
+ throw new Error("AWS Lightsail adapter context is not initialized. Run provision() first.");
1557
+ }
1558
+ return this.currentContext;
1559
+ }
1560
+ };
1561
+ var NodeAwsLightsailCommandRunner = class {
1562
+ async run(command, args, cwd) {
1563
+ return new Promise((resolve2) => {
1564
+ const child = spawn5(command, args, {
1565
+ cwd,
1566
+ env: process.env,
1567
+ stdio: ["ignore", "pipe", "pipe"]
1568
+ });
1569
+ let stdout = "";
1570
+ let stderr = "";
1571
+ child.stdout?.on("data", (chunk) => {
1572
+ stdout += chunk.toString("utf-8");
1573
+ });
1574
+ child.stderr?.on("data", (chunk) => {
1575
+ stderr += chunk.toString("utf-8");
1576
+ });
1577
+ child.on("error", (error) => {
1578
+ resolve2({ exitCode: 1, stdout, stderr: `${stderr}
1579
+ ${error.message}` });
1580
+ });
1581
+ child.on("exit", (code) => {
1582
+ resolve2({ exitCode: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() });
1583
+ });
1584
+ });
1585
+ }
1586
+ };
1587
+ var PASSTHROUGH_ENV_VARS = [
1588
+ "DATABASE_URL",
1589
+ "AUTH_SECRET",
1590
+ "PUBLIC_URL",
1591
+ "NODE_ENV"
1592
+ ];
1593
+ function sanitizeLightsailName(name) {
1594
+ return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "").slice(0, 255) || "kora-app";
1595
+ }
1596
+ function parseLightsailImageRef(output) {
1597
+ const match = output.match(/Refer to this image as\s+"?(:[^"\s]+)"?/i);
1598
+ if (match?.[1]) {
1599
+ return match[1];
1600
+ }
1601
+ const refMatch = output.match(/(:\S+\.\S+\.\d+)/);
1602
+ return refMatch?.[1] ?? null;
1603
+ }
1604
+ function parseLightsailUrl(rawJson) {
1605
+ try {
1606
+ const parsed = JSON.parse(rawJson);
1607
+ const url = parsed.containerServices?.[0]?.url;
1608
+ if (typeof url === "string" && url.length > 0) {
1609
+ return url.startsWith("https://") ? url : `https://${url}`;
1610
+ }
1611
+ return null;
1612
+ } catch {
1613
+ return null;
1614
+ }
1615
+ }
1616
+ function parsePreviousDeployment(rawJson, serviceName) {
1617
+ try {
1618
+ const parsed = JSON.parse(rawJson);
1619
+ const deployments = parsed.deployments ?? [];
1620
+ const previous = deployments.length > 1 ? deployments[1] : null;
1621
+ if (!previous) return null;
1622
+ return {
1623
+ containers: previous.containers,
1624
+ publicEndpoint: previous.publicEndpoint
1625
+ };
1626
+ } catch {
1627
+ return null;
1628
+ }
1629
+ }
1630
+ function inferLogLevel4(line) {
1631
+ const normalized = line.toLowerCase();
1632
+ if (normalized.includes("error")) return "error";
1633
+ if (normalized.includes("warn")) return "warn";
1634
+ if (normalized.includes("debug")) return "debug";
1635
+ return "info";
1636
+ }
1637
+
899
1638
  // src/commands/deploy/adapters/factory.ts
900
1639
  function createDeployAdapter(platform) {
901
1640
  switch (platform) {
@@ -903,6 +1642,10 @@ function createDeployAdapter(platform) {
903
1642
  return new FlyAdapter();
904
1643
  case "railway":
905
1644
  return new RailwayAdapter();
1645
+ case "aws-ecs":
1646
+ return new AwsEcsAdapter();
1647
+ case "aws-lightsail":
1648
+ return new AwsLightsailAdapter();
906
1649
  case "render":
907
1650
  case "docker":
908
1651
  case "kora-cloud":
@@ -916,7 +1659,7 @@ function createDeployAdapter(platform) {
916
1659
 
917
1660
  // src/commands/deploy/artifacts/dockerfile-generator.ts
918
1661
  import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
919
- import { join as join7 } from "path";
1662
+ import { join as join9 } from "path";
920
1663
  var GENERATED_HEADER3 = "# Generated by kora deploy \u2014 do not edit manually";
921
1664
  function generateDockerfile(options = {}) {
922
1665
  const nodeVersion = options.nodeVersion ?? "20-alpine";
@@ -980,31 +1723,31 @@ function generateDockerIgnore() {
980
1723
  }
981
1724
  async function writeDockerfileArtifact(deployDirectory, options = {}) {
982
1725
  await mkdir4(deployDirectory, { recursive: true });
983
- const dockerfilePath = join7(deployDirectory, "Dockerfile");
1726
+ const dockerfilePath = join9(deployDirectory, "Dockerfile");
984
1727
  await writeFile3(dockerfilePath, generateDockerfile(options), "utf-8");
985
1728
  if (options.nativeDependencies && Object.keys(options.nativeDependencies).length > 0) {
986
- const pkgJsonPath = join7(deployDirectory, "package.json");
1729
+ const pkgJsonPath = join9(deployDirectory, "package.json");
987
1730
  await writeFile3(pkgJsonPath, generateDeployPackageJson(options.nativeDependencies), "utf-8");
988
1731
  }
989
1732
  return dockerfilePath;
990
1733
  }
991
1734
  async function writeDockerIgnoreArtifact(deployDirectory) {
992
1735
  await mkdir4(deployDirectory, { recursive: true });
993
- const dockerIgnorePath = join7(deployDirectory, ".dockerignore");
1736
+ const dockerIgnorePath = join9(deployDirectory, ".dockerignore");
994
1737
  await writeFile3(dockerIgnorePath, generateDockerIgnore(), "utf-8");
995
1738
  return dockerIgnorePath;
996
1739
  }
997
1740
 
998
1741
  // src/commands/deploy/state/deploy-state.ts
999
1742
  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");
1743
+ import { join as join10 } from "path";
1744
+ var KORA_DEPLOY_DIRECTORY = join10(".kora", "deploy");
1002
1745
  var DEPLOY_STATE_FILENAME = "deploy.json";
1003
1746
  function resolveDeployDirectory(projectRoot) {
1004
- return join8(projectRoot, KORA_DEPLOY_DIRECTORY);
1747
+ return join10(projectRoot, KORA_DEPLOY_DIRECTORY);
1005
1748
  }
1006
1749
  function resolveDeployStatePath(projectRoot) {
1007
- return join8(resolveDeployDirectory(projectRoot), DEPLOY_STATE_FILENAME);
1750
+ return join10(resolveDeployDirectory(projectRoot), DEPLOY_STATE_FILENAME);
1008
1751
  }
1009
1752
  async function readDeployState(projectRoot) {
1010
1753
  const statePath = resolveDeployStatePath(projectRoot);
@@ -1290,7 +2033,8 @@ var deployCommand = defineCommand({
1290
2033
  await writeDockerfileArtifact(deployDirectory, {
1291
2034
  nativeDependencies: {
1292
2035
  "better-sqlite3": "^11.0.0",
1293
- "drizzle-orm": "^0.45.2"
2036
+ "drizzle-orm": "^0.45.2",
2037
+ "postgres": "^3.4.0"
1294
2038
  }
1295
2039
  });
1296
2040
  await writeDockerIgnoreArtifact(deployDirectory);
@@ -1357,6 +2101,14 @@ async function resolvePlatform(options) {
1357
2101
  label: "Railway",
1358
2102
  value: "railway"
1359
2103
  },
2104
+ {
2105
+ label: "AWS ECS Fargate (production multi-instance)",
2106
+ value: "aws-ecs"
2107
+ },
2108
+ {
2109
+ label: "AWS Lightsail Containers (simple, cost-effective)",
2110
+ value: "aws-lightsail"
2111
+ },
1360
2112
  {
1361
2113
  label: "Render",
1362
2114
  value: "render"
@@ -1521,10 +2273,10 @@ function toPascalCase(name) {
1521
2273
  export {
1522
2274
  DEPLOY_PLATFORMS,
1523
2275
  isDeployPlatform,
1524
- generateFlyToml,
1525
- writeFlyTomlArtifact,
1526
2276
  buildClient,
1527
2277
  bundleServer,
2278
+ generateFlyToml,
2279
+ writeFlyTomlArtifact,
1528
2280
  FlyAdapter,
1529
2281
  NodeFlyCommandRunner,
1530
2282
  generateRailwayJson,
@@ -1547,4 +2299,4 @@ export {
1547
2299
  deployCommand,
1548
2300
  generateTypes
1549
2301
  };
1550
- //# sourceMappingURL=chunk-E4JG7THU.js.map
2302
+ //# sourceMappingURL=chunk-E7OCVRYL.js.map