onveloz 0.0.0-beta.4 → 0.0.0-beta.6

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.
Files changed (2) hide show
  1. package/dist/index.mjs +615 -594
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -307,44 +307,6 @@ function createClient(baseUrl, headers) {
307
307
  }));
308
308
  }
309
309
 
310
- //#endregion
311
- //#region src/lib/client.ts
312
- async function getClient() {
313
- const config = await requireAuth();
314
- return createClient(config.apiUrl, () => ({ Authorization: `Bearer ${config.apiKey}` }));
315
- }
316
-
317
- //#endregion
318
- //#region src/commands/projects.ts
319
- const projectsCommand = new Command("projects").alias("projetos").description("Gerenciar projetos");
320
- projectsCommand.command("list").alias("listar").description("Listar todos os projetos").action(async () => {
321
- const spin = spinner("Carregando projetos...");
322
- try {
323
- const projects = await (await getClient()).projects.list();
324
- spin.stop();
325
- if (projects.length === 0) {
326
- info("Nenhum projeto encontrado. Crie um pelo dashboard.");
327
- return;
328
- }
329
- printTable([
330
- "ID",
331
- "Nome",
332
- "Slug",
333
- "Repo GitHub",
334
- "Criado em"
335
- ], projects.map((p) => [
336
- p.id.slice(0, 8),
337
- p.name,
338
- p.slug,
339
- p.githubRepoOwner && p.githubRepoName ? `${p.githubRepoOwner}/${p.githubRepoName}` : "—",
340
- new Date(p.createdAt).toLocaleDateString("pt-BR")
341
- ]));
342
- } catch (error) {
343
- spin.stop();
344
- handleError(error);
345
- }
346
- });
347
-
348
310
  //#endregion
349
311
  //#region ../../packages/config/veloz-config.ts
350
312
  const ServiceTypeSchema = z.enum(["web", "static"]);
@@ -357,10 +319,12 @@ const PackageManagerSchema = z.enum([
357
319
  ]);
358
320
  const BuildConfigSchema = z.object({
359
321
  command: z.string().nullable().optional(),
360
- nodeVersion: z.string().regex(/^[0-9]+(\.x)?$/).default("20").optional(),
322
+ nodeVersion: z.string().regex(/^[0-9]+(\.[0-9]+){0,2}(\.x)?$/).default("20").optional(),
323
+ nixpkgsArchive: z.string().regex(/^[a-f0-9]{40}$/).optional(),
361
324
  packageManager: PackageManagerSchema.default("auto").optional(),
362
325
  installCommand: z.string().nullable().optional(),
363
- outputDir: z.string().nullable().optional()
326
+ outputDir: z.string().nullable().optional(),
327
+ aptPackages: z.array(z.string().regex(/^[a-z0-9][a-z0-9.+\-]+$/, "Nome de pacote inválido")).optional()
364
328
  });
365
329
  const RuntimeConfigSchema = z.object({
366
330
  command: z.string().nullable().optional(),
@@ -498,7 +462,7 @@ function loadConfig() {
498
462
  function saveConfig(config) {
499
463
  const path = getConfigPath();
500
464
  const configWithSchema = {
501
- $schema: "https://veloz.app/schemas/veloz-config.schema.json",
465
+ $schema: "https://onveloz.com/schemas/veloz-config.schema.json",
502
466
  ...config
503
467
  };
504
468
  writeFileSync(path, JSON.stringify(configWithSchema, null, 2), "utf-8");
@@ -554,6 +518,49 @@ function getGitBranch() {
554
518
  }
555
519
  }
556
520
 
521
+ //#endregion
522
+ //#region src/lib/client.ts
523
+ async function getClient() {
524
+ const authConfig = await requireAuth();
525
+ const projectConfig = loadConfig();
526
+ return createClient(authConfig.apiUrl, () => {
527
+ const headers = { Authorization: `Bearer ${authConfig.apiKey}` };
528
+ if (projectConfig?.project?.id) headers["X-Project-Id"] = projectConfig.project.id;
529
+ return headers;
530
+ });
531
+ }
532
+
533
+ //#endregion
534
+ //#region src/commands/projects.ts
535
+ const projectsCommand = new Command("projects").alias("projetos").description("Gerenciar projetos");
536
+ projectsCommand.command("list").alias("listar").description("Listar todos os projetos").action(async () => {
537
+ const spin = spinner("Carregando projetos...");
538
+ try {
539
+ const projects = await (await getClient()).projects.list();
540
+ spin.stop();
541
+ if (projects.length === 0) {
542
+ info("Nenhum projeto encontrado. Crie um pelo dashboard.");
543
+ return;
544
+ }
545
+ printTable([
546
+ "ID",
547
+ "Nome",
548
+ "Slug",
549
+ "Repo GitHub",
550
+ "Criado em"
551
+ ], projects.map((p) => [
552
+ p.id.slice(0, 8),
553
+ p.name,
554
+ p.slug,
555
+ p.githubRepoOwner && p.githubRepoName ? `${p.githubRepoOwner}/${p.githubRepoName}` : "—",
556
+ new Date(p.createdAt).toLocaleDateString("pt-BR")
557
+ ]));
558
+ } catch (error) {
559
+ spin.stop();
560
+ handleError(error);
561
+ }
562
+ });
563
+
557
564
  //#endregion
558
565
  //#region src/commands/link.ts
559
566
  const linkCommand = new Command("link").description("Verificar vínculo do projeto com Veloz").action(async () => {
@@ -741,7 +748,7 @@ const FRAMEWORK_RULES = [
741
748
  port: 3e3
742
749
  }
743
750
  ];
744
- function detectPackageManager$1(files) {
751
+ function detectPackageManager(files) {
745
752
  if ("bun.lockb" in files || "bun.lock" in files) return "bun";
746
753
  if ("pnpm-lock.yaml" in files || "pnpm-workspace.yaml" in files) return "pnpm";
747
754
  if ("yarn.lock" in files) return "yarn";
@@ -939,7 +946,7 @@ function detectMonorepo(files, pm = "npm") {
939
946
  };
940
947
  }
941
948
  function analyzeRepo(files) {
942
- const pm = detectPackageManager$1(files);
949
+ const pm = detectPackageManager(files);
943
950
  const framework = detectFramework(files["package.json"], pm);
944
951
  const envVars = detectEnvVars(files);
945
952
  const { isMonorepo, apps } = detectMonorepo(files, pm);
@@ -1090,8 +1097,8 @@ async function calculateDirectorySize(directory) {
1090
1097
  }
1091
1098
 
1092
1099
  //#endregion
1093
- //#region src/lib/deploy-stream.ts
1094
- const statusLabels$1 = {
1100
+ //#region src/lib/deploy-constants.ts
1101
+ const statusLabels = {
1095
1102
  QUEUED: "Na fila",
1096
1103
  BUILDING: "Construindo",
1097
1104
  BUILD_FAILED: "Falha na construção",
@@ -1100,12 +1107,24 @@ const statusLabels$1 = {
1100
1107
  FAILED: "Falhou",
1101
1108
  CANCELLED: "Cancelado"
1102
1109
  };
1103
- const TERMINAL_STATUSES$1 = new Set([
1110
+ const statusIcons = {
1111
+ QUEUED: chalk.gray("○"),
1112
+ BUILDING: chalk.yellow("●"),
1113
+ DEPLOYING: chalk.blue("●"),
1114
+ LIVE: chalk.green("●"),
1115
+ BUILD_FAILED: chalk.red("●"),
1116
+ FAILED: chalk.red("●"),
1117
+ CANCELLED: chalk.gray("●")
1118
+ };
1119
+ const TERMINAL_STATUSES = new Set([
1104
1120
  "LIVE",
1105
1121
  "BUILD_FAILED",
1106
1122
  "FAILED",
1107
1123
  "CANCELLED"
1108
1124
  ]);
1125
+
1126
+ //#endregion
1127
+ //#region src/lib/deploy-stream.ts
1109
1128
  async function streamDeploymentLogs(deploymentId, serviceName) {
1110
1129
  const client = await getClient();
1111
1130
  const isVerbose = process.env.VELOZ_VERBOSE === "true";
@@ -1120,8 +1139,8 @@ async function streamDeploymentLogs(deploymentId, serviceName) {
1120
1139
  for await (const event of stream) {
1121
1140
  logsReceived = true;
1122
1141
  if (event.type === "status") {
1123
- const label = statusLabels$1[event.content] ?? event.content;
1124
- const icon = event.content === "LIVE" ? chalk.green("●") : event.content === "BUILD_FAILED" || event.content === "FAILED" ? chalk.red("●") : chalk.yellow("●");
1142
+ const label = statusLabels[event.content] ?? event.content;
1143
+ const icon = statusIcons[event.content] ?? chalk.yellow("●");
1125
1144
  process.stdout.write(`\n${icon} ${chalk.bold(label)}\n`);
1126
1145
  finalStatus = event.content;
1127
1146
  if (isVerbose) console.log(chalk.dim(`[verbose] Status mudou para: ${event.content}`));
@@ -1149,52 +1168,60 @@ async function streamDeploymentLogs(deploymentId, serviceName) {
1149
1168
  }
1150
1169
  console.log(chalk.dim("\n" + "─".repeat(60)));
1151
1170
  if (finalStatus === "LIVE") success(serviceName ? `Deploy de ${chalk.bold(serviceName)} concluído! Serviço está ativo.` : "Deploy concluído! Serviço está ativo.");
1152
- else if (TERMINAL_STATUSES$1.has(finalStatus)) {
1153
- const errorMsg = serviceName ? `✗ Deploy de ${chalk.bold(serviceName)} finalizou com status: ${statusLabels$1[finalStatus] ?? finalStatus}` : `✗ Deploy finalizou com status: ${statusLabels$1[finalStatus] ?? finalStatus}`;
1171
+ else if (TERMINAL_STATUSES.has(finalStatus)) {
1172
+ const errorMsg = serviceName ? `✗ Deploy de ${chalk.bold(serviceName)} finalizou com status: ${statusLabels[finalStatus] ?? finalStatus}` : `✗ Deploy finalizou com status: ${statusLabels[finalStatus] ?? finalStatus}`;
1154
1173
  console.error(chalk.red(errorMsg));
1155
- if (!serviceName) process.exit(1);
1174
+ process.exit(1);
1156
1175
  }
1157
1176
  }
1158
1177
 
1159
1178
  //#endregion
1160
- //#region src/lib/deploy-parallel.ts
1161
- async function withRetry$1(fn, maxRetries = 3) {
1179
+ //#region src/lib/retry.ts
1180
+ async function withRetry(fn, maxRetries = 3) {
1162
1181
  for (let attempt = 0; attempt <= maxRetries; attempt++) try {
1163
1182
  return await fn();
1164
1183
  } catch (error) {
1165
- if (attempt < maxRetries) {
1184
+ if (attempt >= maxRetries) throw error;
1185
+ const rateLimit = isRateLimitError(error);
1186
+ if (rateLimit) {
1187
+ const waitMs = Math.min(rateLimit.retryAfterMs, 3e4);
1188
+ await new Promise((r) => setTimeout(r, waitMs));
1189
+ } else {
1166
1190
  const delay = Math.min(1e3 * Math.pow(2, attempt), 1e4);
1167
- await new Promise((resolve$1) => setTimeout(resolve$1, delay));
1168
- continue;
1191
+ await new Promise((r) => setTimeout(r, delay));
1169
1192
  }
1170
- throw error;
1171
1193
  }
1172
1194
  throw new Error("Max retries exceeded");
1173
1195
  }
1174
- const statusLabels = {
1175
- QUEUED: "Na fila",
1176
- BUILDING: "Construindo",
1177
- BUILD_FAILED: "Falha na construção",
1178
- DEPLOYING: "Implantando",
1179
- LIVE: "Ativo",
1180
- FAILED: "Falhou",
1181
- CANCELLED: "Cancelado"
1182
- };
1183
- const statusIcons = {
1184
- QUEUED: chalk.gray("○"),
1185
- BUILDING: chalk.yellow("●"),
1186
- DEPLOYING: chalk.blue("●"),
1187
- LIVE: chalk.green("●"),
1188
- BUILD_FAILED: chalk.red(""),
1189
- FAILED: chalk.red("●"),
1190
- CANCELLED: chalk.gray("")
1191
- };
1192
- const TERMINAL_STATUSES = new Set([
1193
- "LIVE",
1194
- "BUILD_FAILED",
1195
- "FAILED",
1196
- "CANCELLED"
1197
- ]);
1196
+
1197
+ //#endregion
1198
+ //#region src/lib/deploy-cancel.ts
1199
+ const activeDeploymentIds = /* @__PURE__ */ new Set();
1200
+ let sigintHandlerRegistered = false;
1201
+ function trackDeployment(deploymentId) {
1202
+ activeDeploymentIds.add(deploymentId);
1203
+ }
1204
+ function untrackDeployment(deploymentId) {
1205
+ activeDeploymentIds.delete(deploymentId);
1206
+ }
1207
+ function setupSigintHandler() {
1208
+ if (sigintHandlerRegistered) return;
1209
+ sigintHandlerRegistered = true;
1210
+ process.on("SIGINT", async () => {
1211
+ if (activeDeploymentIds.size === 0) process.exit(130);
1212
+ console.log(chalk.yellow("\n\nCancelando deploy(s)..."));
1213
+ try {
1214
+ const client = await getClient();
1215
+ const cancelPromises = Array.from(activeDeploymentIds).map((deploymentId) => client.deployments.cancel({ deploymentId }).catch(() => {}));
1216
+ await Promise.all(cancelPromises);
1217
+ console.log(chalk.yellow("Deploy cancelado."));
1218
+ } catch {}
1219
+ process.exit(130);
1220
+ });
1221
+ }
1222
+
1223
+ //#endregion
1224
+ //#region src/lib/deploy-parallel.ts
1198
1225
  function renderProgress(progressMap, prevLineCount) {
1199
1226
  for (let i = 0; i < prevLineCount; i++) process.stdout.write("\x1B[1A\x1B[2K");
1200
1227
  let lineCount = 0;
@@ -1225,14 +1252,16 @@ function renderProgress(progressMap, prevLineCount) {
1225
1252
  async function deployServicesInParallel(services) {
1226
1253
  const client = await getClient();
1227
1254
  console.log(chalk.cyan("\n🚀 Iniciando deploy paralelo de múltiplos serviços...\n"));
1255
+ setupSigintHandler();
1228
1256
  const progressMap = /* @__PURE__ */ new Map();
1229
1257
  const projectRoot = process.cwd();
1230
1258
  const sizeInBytes = await calculateDirectorySize(projectRoot);
1231
1259
  const sizeMB = Math.round(sizeInBytes / (1024 * 1024) * 10) / 10;
1232
1260
  const deploymentPromises = services.map(async (service) => {
1233
1261
  try {
1234
- const deployment = await withRetry$1(() => client.deployments.create({ serviceId: service.serviceId }));
1235
- await withRetry$1(() => uploadSource(deployment.id, projectRoot, service.extraFiles));
1262
+ const deployment = await withRetry(() => client.deployments.create({ serviceId: service.serviceId }));
1263
+ await withRetry(() => uploadSource(deployment.id, projectRoot, service.extraFiles));
1264
+ trackDeployment(deployment.id);
1236
1265
  progressMap.set(service.serviceId, {
1237
1266
  serviceName: service.serviceName,
1238
1267
  deploymentId: deployment.id,
@@ -1262,7 +1291,7 @@ async function deployServicesInParallel(services) {
1262
1291
  const activeDeployments = (await Promise.allSettled(deploymentPromises)).filter((d) => d.status === "fulfilled").map((d) => d.value);
1263
1292
  if (activeDeployments.length === 0) {
1264
1293
  console.error(chalk.red("\n✗ Todos os deploys falharam ao iniciar."));
1265
- return;
1294
+ process.exit(1);
1266
1295
  }
1267
1296
  console.log(chalk.cyan("\n📦 Monitorando progresso dos deploys:\n"));
1268
1297
  console.log(chalk.dim("─".repeat(60)) + "\n");
@@ -1299,6 +1328,8 @@ async function deployServicesInParallel(services) {
1299
1328
  progress.completed = true;
1300
1329
  lineCount = renderProgress(progressMap, lineCount);
1301
1330
  }
1331
+ } finally {
1332
+ untrackDeployment(deploymentId);
1302
1333
  }
1303
1334
  });
1304
1335
  await Promise.all(streamPromises);
@@ -1323,451 +1354,474 @@ async function deployServicesInParallel(services) {
1323
1354
  }
1324
1355
  }
1325
1356
  if (successful.length > 0) info("\nUse 'veloz logs -f' para acompanhar os logs de execução.");
1357
+ if (failed.length > 0) process.exit(1);
1326
1358
  }
1327
1359
 
1328
1360
  //#endregion
1329
- //#region src/lib/dockerfile-generator.ts
1330
- function pmSetupInstructions(pm) {
1331
- switch (pm) {
1332
- case "pnpm": return "RUN corepack enable && corepack prepare pnpm@latest --activate";
1333
- case "yarn": return "RUN corepack enable";
1334
- case "bun": return "RUN apk add --no-cache bash curl && curl -fsSL https://bun.sh/install | bash && ln -s /root/.bun/bin/bun /usr/local/bin/bun";
1335
- case "npm": return "";
1336
- }
1337
- }
1338
- function installCommand(pm) {
1339
- switch (pm) {
1340
- case "bun": return "bun install --frozen-lockfile";
1341
- case "pnpm": return "pnpm install --frozen-lockfile";
1342
- case "yarn": return "yarn install --frozen-lockfile";
1343
- case "npm": return "npm ci";
1361
+ //#region src/lib/deploy-config.ts
1362
+ function resolveServiceConf(velozConfig, serviceId) {
1363
+ if (!velozConfig) return void 0;
1364
+ for (const [, conf] of Object.entries(velozConfig.services)) if (conf.id === serviceId) {
1365
+ const merged = mergeServiceWithDefaults(conf, velozConfig.defaults);
1366
+ return {
1367
+ type: merged.type,
1368
+ buildCommand: merged.build?.command ?? void 0,
1369
+ startCommand: merged.runtime?.command ?? void 0,
1370
+ port: merged.runtime?.port ?? void 0,
1371
+ rootDirectory: merged.root,
1372
+ outputDir: merged.build?.outputDir ?? void 0,
1373
+ instanceCount: merged.resources?.instances ?? void 0,
1374
+ cpuLimit: merged.resources?.cpu ?? void 0,
1375
+ memoryLimit: merged.resources?.memory ?? void 0,
1376
+ healthCheckPath: merged.runtime?.healthCheck?.path ?? null,
1377
+ aptPackages: merged.build?.aptPackages ?? void 0
1378
+ };
1344
1379
  }
1345
1380
  }
1346
- function lockfileNames(pm) {
1347
- switch (pm) {
1348
- case "bun": return ["bun.lockb", "bun.lock"];
1349
- case "pnpm": return ["pnpm-lock.yaml"];
1350
- case "yarn": return ["yarn.lock"];
1351
- case "npm": return ["package-lock.json"];
1352
- }
1353
- }
1354
- function generateWebDockerfile(opts) {
1355
- const { nodeVersion, pm, buildCommand, startCommand, rootDirectory, port = 3e3 } = opts;
1356
- const setup = pmSetupInstructions(pm);
1357
- const lockfiles = lockfileNames(pm);
1358
- const hasRoot = !!(rootDirectory && rootDirectory !== "/");
1359
- const cleanRoot = hasRoot ? rootDirectory.replace(/^\//, "") : "";
1360
- if (pm === "pnpm" && hasRoot) {
1361
- const workdirPrefix$1 = `cd ${cleanRoot} && `;
1362
- const defaultStart$1 = `${pm} run start`;
1363
- const finalStart$1 = startCommand || defaultStart$1;
1364
- return `# ── Stage 1: Install & Build ────────────────────────────
1365
- FROM node:${nodeVersion}-alpine AS builder
1366
-
1367
- WORKDIR /app
1368
-
1369
- ${setup}
1370
-
1371
- # Copy full monorepo source
1372
- COPY . .
1373
-
1374
- # Install all dependencies
1375
- RUN ${installCommand(pm)}
1376
-
1377
- # Build with env vars from BuildKit secret mount
1378
- RUN --mount=type=secret,id=build-env \\
1379
- set -a && \\
1380
- if [ -f /run/secrets/build-env ]; then . /run/secrets/build-env; fi && \\
1381
- set +a && \\
1382
- ${workdirPrefix$1}${buildCommand}
1383
-
1384
- # ── Stage 2: Production runner ─────────────────────────
1385
- FROM node:${nodeVersion}-alpine
1386
-
1387
- WORKDIR /app
1388
-
1389
- ${setup}
1390
-
1391
- # Copy built app (node_modules + build output)
1392
- COPY --from=builder /app .
1393
-
1394
- ENV NODE_ENV=production
1395
- ENV PORT=${port}
1396
- EXPOSE ${port}
1397
-
1398
- HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \\
1399
- CMD wget --quiet --tries=1 --spider http://localhost:${port}/ || exit 1
1400
-
1401
- CMD ["sh", "-c", "${workdirPrefix$1}${finalStart$1}"]
1402
- `;
1403
- }
1404
- const depsCopy = hasRoot ? [`COPY package.json ${lockfiles.join(" ")} ./`, `COPY ${cleanRoot}/package.json ./${cleanRoot}/`] : [`COPY package.json ${lockfiles.join(" ")} ./`];
1405
- const workdirPrefix = hasRoot ? `cd ${cleanRoot} && ` : "";
1406
- const defaultStart = pm === "bun" ? "bun start" : `${pm} run start`;
1407
- const finalStart = startCommand || defaultStart;
1408
- return `# ── Stage 1: Install & Build ────────────────────────────
1409
- FROM node:${nodeVersion}-alpine AS builder
1410
-
1411
- WORKDIR /app
1412
- ${setup ? "\n" + setup + "\n" : ""}
1413
- # Install dependencies (cached layer)
1414
- ${depsCopy.join("\n")}
1415
- RUN ${installCommand(pm)}
1416
-
1417
- # Copy full source
1418
- COPY . .
1419
-
1420
- # Build with env vars from BuildKit secret mount
1421
- RUN --mount=type=secret,id=build-env \\
1422
- set -a && \\
1423
- if [ -f /run/secrets/build-env ]; then . /run/secrets/build-env; fi && \\
1424
- set +a && \\
1425
- ${workdirPrefix}${buildCommand}
1426
-
1427
- # ── Stage 2: Production runner ─────────────────────────
1428
- FROM node:${nodeVersion}-alpine
1429
-
1430
- WORKDIR /app
1431
-
1432
- # Copy built app (node_modules + build output)
1433
- COPY --from=builder /app .
1434
-
1435
- ENV NODE_ENV=production
1436
- ENV PORT=${port}
1437
- EXPOSE ${port}
1438
-
1439
- HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \\
1440
- CMD wget --quiet --tries=1 --spider http://localhost:${port}/ || exit 1
1441
-
1442
- CMD ${workdirPrefix ? `["sh", "-c", "${workdirPrefix}${finalStart}"]` : JSON.stringify(finalStart.split(" "))}
1443
- `;
1444
- }
1445
- function generateStaticDockerfile(opts) {
1446
- const { nodeVersion, pm, buildCommand, outputDir, rootDirectory } = opts;
1447
- const setup = pmSetupInstructions(pm);
1448
- const lockfiles = lockfileNames(pm);
1449
- const hasRoot = !!(rootDirectory && rootDirectory !== "/");
1450
- const cleanRoot = hasRoot ? rootDirectory.replace(/^\//, "") : "";
1451
- const servicePrefix = hasRoot ? cleanRoot + "/" : "";
1452
- const isPnpmMonorepo = pm === "pnpm" && hasRoot;
1453
- const workdirPrefix = hasRoot ? `cd ${cleanRoot} && ` : "";
1454
- const outputDetection = outputDir ? `ENV OUTPUT_DIR="${outputDir}"` : [
1455
- `# Auto-detect output directory`,
1456
- `RUN if [ -d "${servicePrefix}dist" ]; then echo "${servicePrefix}dist" > /tmp/output-dir; \\`,
1457
- ` elif [ -d "${servicePrefix}build" ]; then echo "${servicePrefix}build" > /tmp/output-dir; \\`,
1458
- ` elif [ -d "${servicePrefix}out" ]; then echo "${servicePrefix}out" > /tmp/output-dir; \\`,
1459
- ` elif [ -d "${servicePrefix}.next/out" ]; then echo "${servicePrefix}.next/out" > /tmp/output-dir; \\`,
1460
- ` elif [ -d "${servicePrefix}public" ]; then echo "${servicePrefix}public" > /tmp/output-dir; \\`,
1461
- ` else echo "${servicePrefix}dist" > /tmp/output-dir; fi`
1462
- ].join("\n");
1463
- const outputDirRef = outputDir ? servicePrefix + outputDir : "$(cat /tmp/output-dir)";
1464
- const installSection = isPnpmMonorepo ? [
1465
- `# Copy full monorepo source`,
1466
- `COPY . .`,
1467
- ``,
1468
- `# Install all dependencies`,
1469
- `RUN ${installCommand(pm)}`
1470
- ].join("\n") : [
1471
- `# Install dependencies (cached layer)`,
1472
- ...hasRoot ? [`COPY package.json ${lockfiles.join(" ")} ./`, `COPY ${cleanRoot}/package.json ./${cleanRoot}/`] : [`COPY package.json ${lockfiles.join(" ")} ./`],
1473
- `RUN ${installCommand(pm)}`,
1474
- ``,
1475
- `# Copy full source`,
1476
- `COPY . .`
1477
- ].join("\n");
1478
- return `# ── Stage 1: Build ──────────────────────────────────────
1479
- FROM node:${nodeVersion}-alpine AS builder
1480
-
1481
- WORKDIR /app
1482
- ${setup ? "\n" + setup + "\n" : ""}
1483
- ${installSection}
1484
-
1485
- # Build with env vars from BuildKit secret mount
1486
- RUN --mount=type=secret,id=build-env \\
1487
- set -a && \\
1488
- if [ -f /run/secrets/build-env ]; then . /run/secrets/build-env; fi && \\
1489
- set +a && \\
1490
- ${workdirPrefix}${buildCommand}
1491
-
1492
- # Detect output directory
1493
- ${outputDetection}
1494
-
1495
- # Process _headers and _redirects into nginx config
1496
- COPY nginx.conf /tmp/nginx-base.conf
1497
- COPY generate-nginx-config.cjs /tmp/generate-nginx-config.cjs
1498
- RUN node /tmp/generate-nginx-config.cjs \\
1499
- --base /tmp/nginx-base.conf \\
1500
- --output-dir /app/${outputDirRef} \\
1501
- --out /tmp/nginx-final.conf
1502
-
1503
- # Copy build output to a known location
1504
- RUN cp -r /app/${outputDirRef} /output
1505
-
1506
- # ── Stage 2: Serve ─────────────────────────────────────
1507
- FROM nginx:alpine
1508
-
1509
- # Remove default nginx site
1510
- RUN rm -f /etc/nginx/conf.d/default.conf
1511
-
1512
- # Copy processed nginx config and static files
1513
- COPY --from=builder /tmp/nginx-final.conf /etc/nginx/conf.d/default.conf
1514
- COPY --from=builder /output /usr/share/nginx/html
1515
-
1516
- EXPOSE 80
1517
-
1518
- HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
1519
- CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1
1520
-
1521
- CMD ["nginx", "-g", "daemon off;"]
1522
- `;
1523
- }
1524
- function generateDockerfile(opts) {
1525
- if (opts.serviceType === "STATIC") return generateStaticDockerfile(opts);
1526
- return generateWebDockerfile(opts);
1381
+ async function syncServiceConfig(client, serviceId, conf) {
1382
+ await withRetry(() => client.services.update({
1383
+ serviceId,
1384
+ port: conf.port,
1385
+ instanceCount: conf.instanceCount,
1386
+ cpuLimit: conf.cpuLimit,
1387
+ memoryLimit: conf.memoryLimit,
1388
+ buildCommand: conf.buildCommand,
1389
+ startCommand: conf.startCommand,
1390
+ rootDirectory: conf.rootDirectory,
1391
+ healthCheckPath: conf.healthCheckPath,
1392
+ aptPackages: conf.aptPackages
1393
+ }));
1527
1394
  }
1528
1395
 
1529
1396
  //#endregion
1530
- //#region src/lib/templates.ts
1397
+ //#region src/lib/deploy-checks.ts
1531
1398
  /**
1532
- * Embedded templates for STATIC site builds.
1533
- * These are written to the project directory temporarily before creating the tar,
1534
- * then cleaned up immediately after.
1399
+ * Platform-specific presets that won't work on Veloz (generic K8s).
1400
+ * Maps preset name to the platform it targets.
1535
1401
  */
1536
- const NGINX_CONF = `server {
1537
- listen 80;
1538
- server_name _;
1539
- root /usr/share/nginx/html;
1540
- index index.html;
1541
-
1542
- # SPA routing - fallback to index.html for client-side routing
1543
- location / {
1544
- try_files \\$uri \\$uri/ /index.html;
1545
- }
1546
-
1547
- # Gzip compression
1548
- gzip on;
1549
- gzip_vary on;
1550
- gzip_min_length 256;
1551
- gzip_types
1552
- text/plain
1553
- text/css
1554
- text/xml
1555
- text/javascript
1556
- application/json
1557
- application/javascript
1558
- application/xml+rss
1559
- application/rss+xml
1560
- application/atom+xml
1561
- image/svg+xml
1562
- text/x-component
1563
- text/x-cross-domain-policy;
1564
-
1565
- # Cache hashed assets (fingerprinted files)
1566
- location ~* \\\\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|otf|webp|avif)\\$ {
1567
- expires 1y;
1568
- add_header Cache-Control "public, immutable";
1569
- access_log off;
1570
- }
1571
-
1572
- # No cache for HTML files
1573
- location ~* \\\\.html\\$ {
1574
- expires -1;
1575
- add_header Cache-Control "no-cache, no-store, must-revalidate";
1576
- add_header Pragma "no-cache";
1577
- }
1578
-
1579
- # Security headers
1580
- add_header X-Frame-Options "SAMEORIGIN" always;
1581
- add_header X-Content-Type-Options "nosniff" always;
1582
- add_header X-XSS-Protection "1; mode=block" always;
1583
-
1584
- # Custom headers placeholder (will be replaced during build)
1585
- # CUSTOM_HEADERS_PLACEHOLDER
1586
-
1587
- # Custom redirects placeholder (will be replaced during build)
1588
- # CUSTOM_REDIRECTS_PLACEHOLDER
1589
- }`;
1590
- const GENERATE_NGINX_CONFIG_CJS = `#!/usr/bin/env node
1591
- "use strict";
1592
-
1402
+ const INCOMPATIBLE_PRESETS = {
1403
+ vercel: "Vercel",
1404
+ "cloudflare-pages": "Cloudflare Pages",
1405
+ "cloudflare-workers": "Cloudflare Workers",
1406
+ "cloudflare-module": "Cloudflare Workers",
1407
+ netlify: "Netlify",
1408
+ "netlify-edge": "Netlify Edge",
1409
+ "aws-lambda": "AWS Lambda",
1410
+ "firebase": "Firebase",
1411
+ "deno-deploy": "Deno Deploy",
1412
+ "render-com": "Render",
1413
+ "flight-control": "Flightcontrol",
1414
+ "stormkit": "Stormkit",
1415
+ "edgio": "Edgio",
1416
+ "lagon": "Lagon"
1417
+ };
1593
1418
  /**
1594
- * Generates a final nginx.conf by processing Netlify-style _headers and _redirects
1595
- * files from the static site build output. Runs inside the Docker build stage.
1596
- *
1597
- * Usage: node generate-nginx-config.cjs --base <nginx.conf> --output-dir <dir> --out <dest>
1598
- */
1599
-
1600
- const fs = require("fs");
1601
- const path = require("path");
1602
-
1603
- // ── Parse CLI args ─────────────────────────────────────
1604
-
1605
- const args = process.argv.slice(2);
1606
- let baseConfig = "";
1607
- let outputDir = "";
1608
- let outFile = "";
1609
-
1610
- for (let i = 0; i < args.length; i++) {
1611
- if (args[i] === "--base" && args[i + 1]) baseConfig = args[++i];
1612
- if (args[i] === "--output-dir" && args[i + 1]) outputDir = args[++i];
1613
- if (args[i] === "--out" && args[i + 1]) outFile = args[++i];
1614
- }
1615
-
1616
- if (!baseConfig || !outputDir || !outFile) {
1617
- console.error(
1618
- "Usage: node generate-nginx-config.cjs --base <nginx.conf> --output-dir <dir> --out <dest>"
1619
- );
1620
- process.exit(1);
1419
+ * Recommended preset based on package manager / runtime.
1420
+ */
1421
+ function recommendedPreset(basePath) {
1422
+ const pkgPath = resolve(basePath, "package.json");
1423
+ if (!existsSync(pkgPath)) return "node-server";
1424
+ try {
1425
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1426
+ if ("bun" in {
1427
+ ...pkg.dependencies,
1428
+ ...pkg.devDependencies
1429
+ } || existsSync(resolve(basePath, "bun.lockb")) || existsSync(resolve(basePath, "bun.lock"))) return "bun";
1430
+ } catch {}
1431
+ return "node-server";
1621
1432
  }
1622
-
1623
- let nginxConfig = fs.readFileSync(baseConfig, "utf-8");
1624
-
1625
- // ── Parse _headers (Netlify-style) ─────────────────────
1626
- //
1627
- // Format:
1628
- // /api/*
1629
- // Access-Control-Allow-Origin: *
1630
- // /*
1631
- // X-Frame-Options: DENY
1632
-
1633
- const headersPath = path.join(outputDir, "_headers");
1634
- if (fs.existsSync(headersPath)) {
1635
- const content = fs.readFileSync(headersPath, "utf-8");
1636
- let headerDirectives = "";
1637
- let currentPath = "";
1638
- let inLocationBlock = false;
1639
-
1640
- for (const line of content.split("\\n")) {
1641
- const trimmed = line.trim();
1642
- if (!trimmed || trimmed.startsWith("#")) continue;
1643
-
1644
- // Path pattern (starts without whitespace)
1645
- if (!line.startsWith(" ") && !line.startsWith("\\t")) {
1646
- // Close previous location block if open
1647
- if (inLocationBlock) {
1648
- headerDirectives += " }\\n";
1649
- inLocationBlock = false;
1650
- }
1651
- currentPath = trimmed;
1652
- // Global headers (/*) go at server level, others get location blocks
1653
- if (currentPath !== "/*") {
1654
- headerDirectives += \\\`\\n location \\\${currentPath} {\\n\\\`;
1655
- inLocationBlock = true;
1656
- }
1657
- } else {
1658
- // Header line (indented)
1659
- const colonIdx = trimmed.indexOf(":");
1660
- if (colonIdx === -1) continue;
1661
- const key = trimmed.slice(0, colonIdx).trim();
1662
- const value = trimmed.slice(colonIdx + 1).trim();
1663
- if (key && value) {
1664
- if (currentPath === "/*") {
1665
- headerDirectives += \\\` add_header \\\${key} "\\\${value}" always;\\n\\\`;
1666
- } else {
1667
- headerDirectives += \\\` add_header \\\${key} "\\\${value}" always;\\n\\\`;
1668
- }
1669
- }
1670
- }
1671
- }
1672
-
1673
- // Close final location block
1674
- if (inLocationBlock) {
1675
- headerDirectives += " }\\n";
1676
- }
1677
-
1678
- nginxConfig = nginxConfig.replace(
1679
- "# CUSTOM_HEADERS_PLACEHOLDER",
1680
- headerDirectives
1681
- );
1682
- console.log("Processed _headers file");
1433
+ /**
1434
+ * Check for Nitro preset misconfigurations in vite/nuxt config files.
1435
+ */
1436
+ function checkNitroPreset(basePath) {
1437
+ for (const file of [
1438
+ "vite.config.ts",
1439
+ "vite.config.js",
1440
+ "vite.config.mjs",
1441
+ "nuxt.config.ts",
1442
+ "nuxt.config.js"
1443
+ ]) {
1444
+ const filePath = resolve(basePath, file);
1445
+ if (!existsSync(filePath)) continue;
1446
+ let content;
1447
+ try {
1448
+ content = readFileSync(filePath, "utf-8");
1449
+ } catch {
1450
+ continue;
1451
+ }
1452
+ const presetMatch = content.match(/preset\s*:\s*["']([^"']+)["']/);
1453
+ if (!presetMatch) continue;
1454
+ const preset = presetMatch[1];
1455
+ const platform$1 = INCOMPATIBLE_PRESETS[preset];
1456
+ if (!platform$1) continue;
1457
+ const recommended = recommendedPreset(basePath);
1458
+ return {
1459
+ message: `${file} usa preset "${preset}" (${platform$1}) — incompativel com Veloz`,
1460
+ hint: `Altere para preset: "${recommended}" em ${file}`
1461
+ };
1462
+ }
1463
+ return null;
1683
1464
  }
1684
-
1685
- // ── Parse _redirects (Netlify-style) ───────────────────
1686
- //
1687
- // Format:
1688
- // /old-path /new-path 301
1689
- // /api/* https://api.example.com/:splat 200
1690
-
1691
- const redirectsPath = path.join(outputDir, "_redirects");
1692
- if (fs.existsSync(redirectsPath)) {
1693
- const content = fs.readFileSync(redirectsPath, "utf-8");
1694
- let redirectDirectives = "";
1695
-
1696
- for (const line of content.split("\\n")) {
1697
- const trimmed = line.trim();
1698
- if (!trimmed || trimmed.startsWith("#")) continue;
1699
-
1700
- const parts = trimmed.split(/\\s+/);
1701
- if (parts.length < 2) continue;
1702
-
1703
- const from = parts[0];
1704
- const to = parts[1];
1705
- const code = parts[2] || "301";
1706
-
1707
- if (!from || !to) continue;
1708
-
1709
- if (!from.includes("*") && !to.includes(":splat")) {
1710
- // Simple redirect
1711
- const nginxFlag = code === "301" ? "permanent" : "redirect";
1712
- redirectDirectives += \\\` rewrite ^\\\${from}\\\\$ \\\${to} \\\${nginxFlag};\\n\\\`;
1713
- } else {
1714
- // Wildcard redirect
1715
- const fromPattern = from.replace(/\\*/g, "(.*)");
1716
- const toPattern = to.replace(/:splat/g, "\\$1");
1717
- const nginxFlag = code === "301" ? "permanent" : "redirect";
1718
- redirectDirectives += \\\` rewrite ^\\\${fromPattern}\\\\$ \\\${toPattern} \\\${nginxFlag};\\n\\\`;
1719
- }
1720
- }
1721
-
1722
- nginxConfig = nginxConfig.replace(
1723
- "# CUSTOM_REDIRECTS_PLACEHOLDER",
1724
- redirectDirectives
1725
- );
1726
- console.log("Processed _redirects file");
1465
+ /**
1466
+ * Check for Dockerfile COPY instructions that reference both bun.lockb and bun.lock.
1467
+ * Only one usually exists — the COPY will fail if both are listed but one is missing.
1468
+ */
1469
+ function checkDockerfileLockFiles(basePath) {
1470
+ const dockerfilePath = resolve(basePath, "Dockerfile");
1471
+ if (!existsSync(dockerfilePath)) return null;
1472
+ let content;
1473
+ try {
1474
+ content = readFileSync(dockerfilePath, "utf-8");
1475
+ } catch {
1476
+ return null;
1477
+ }
1478
+ const copyLines = content.split("\n").filter((l) => /^COPY\s/.test(l.trim()));
1479
+ for (const line of copyLines) if (line.includes("bun.lockb") && line.includes("bun.lock") && !line.includes("bun.lock*")) {
1480
+ if (!(existsSync(resolve(basePath, "bun.lockb")) && existsSync(resolve(basePath, "bun.lock")))) return {
1481
+ message: "Dockerfile lista bun.lockb e bun.lock mas apenas um existe",
1482
+ hint: "Use \"COPY package.json bun.lock* ./\" para copiar o que existir"
1483
+ };
1484
+ }
1485
+ return null;
1727
1486
  }
1728
-
1729
- // ── Write final config ─────────────────────────────────
1730
-
1731
- fs.mkdirSync(path.dirname(outFile), { recursive: true });
1732
- fs.writeFileSync(outFile, nginxConfig);
1733
- console.log(\\\`Generated nginx config at \\\${outFile}\\\`);`;
1734
-
1735
- //#endregion
1736
- //#region src/commands/deploy.ts
1737
- async function withRetry(fn, maxRetries = 3) {
1738
- for (let attempt = 0; attempt <= maxRetries; attempt++) try {
1739
- return await fn();
1740
- } catch (error) {
1741
- const rateLimit = isRateLimitError(error);
1742
- if (rateLimit && attempt < maxRetries) {
1743
- const waitMs = Math.min(rateLimit.retryAfterMs, 3e4);
1744
- await new Promise((r) => setTimeout(r, waitMs));
1487
+ /**
1488
+ * Next.js needs `output: "standalone"` for Docker/K8s deploys.
1489
+ * Without it, the build produces a node_modules-dependent output
1490
+ * that's huge and doesn't run well in containers.
1491
+ */
1492
+ function checkNextStandalone(basePath) {
1493
+ const pkgPath = resolve(basePath, "package.json");
1494
+ if (!existsSync(pkgPath)) return null;
1495
+ try {
1496
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1497
+ if (!("next" in {
1498
+ ...pkg.dependencies,
1499
+ ...pkg.devDependencies
1500
+ })) return null;
1501
+ } catch {
1502
+ return null;
1503
+ }
1504
+ if (existsSync(resolve(basePath, "Dockerfile"))) return null;
1505
+ for (const file of [
1506
+ "next.config.ts",
1507
+ "next.config.js",
1508
+ "next.config.mjs"
1509
+ ]) {
1510
+ const filePath = resolve(basePath, file);
1511
+ if (!existsSync(filePath)) continue;
1512
+ try {
1513
+ const content = readFileSync(filePath, "utf-8");
1514
+ if (content.includes("\"standalone\"") || content.includes("'standalone'")) return null;
1515
+ return {
1516
+ message: `${file} nao tem output: "standalone"`,
1517
+ hint: "Adicione output: \"standalone\" no next.config para deploys em container"
1518
+ };
1519
+ } catch {
1745
1520
  continue;
1746
1521
  }
1747
- throw error;
1748
1522
  }
1749
- throw new Error("Max retries exceeded");
1523
+ return null;
1750
1524
  }
1751
- function detectPackageManager() {
1752
- if (existsSync(resolve(process.cwd(), "bun.lockb")) || existsSync(resolve(process.cwd(), "bun.lock"))) return "bun";
1753
- if (existsSync(resolve(process.cwd(), "pnpm-lock.yaml"))) return "pnpm";
1754
- if (existsSync(resolve(process.cwd(), "yarn.lock"))) return "yarn";
1755
- return "npm";
1525
+ /**
1526
+ * Prisma needs `prisma generate` in the build step.
1527
+ * Without it, the Prisma client won't be generated and the app will crash.
1528
+ */
1529
+ function checkPrismaGenerate(basePath) {
1530
+ const pkgPath = resolve(basePath, "package.json");
1531
+ if (!existsSync(pkgPath)) return null;
1532
+ try {
1533
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1534
+ const allDeps = {
1535
+ ...pkg.dependencies,
1536
+ ...pkg.devDependencies
1537
+ };
1538
+ if (!("prisma" in allDeps) && !("@prisma/client" in allDeps)) return null;
1539
+ const scripts = pkg.scripts || {};
1540
+ const buildScript = scripts.build || "";
1541
+ const postinstall = scripts.postinstall || "";
1542
+ if (buildScript.includes("prisma generate") || postinstall.includes("prisma generate") || buildScript.includes("prisma db push")) return null;
1543
+ return {
1544
+ message: "Prisma detectado mas prisma generate nao esta no build/postinstall",
1545
+ hint: "Adicione \"prisma generate\" ao script postinstall ou build no package.json"
1546
+ };
1547
+ } catch {
1548
+ return null;
1549
+ }
1756
1550
  }
1757
- function detectNodeVersion() {
1551
+ /**
1552
+ * Detect if the app hardcodes a port that doesn't match the configured port.
1553
+ * Common issue: app listens on 8080 but service port is 3000.
1554
+ */
1555
+ function checkPortMismatch(basePath) {
1556
+ const pkgPath = resolve(basePath, "package.json");
1557
+ if (!existsSync(pkgPath)) return null;
1758
1558
  try {
1759
- const pkgPath = resolve(process.cwd(), "package.json");
1760
- if (existsSync(pkgPath)) {
1761
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1762
- if (pkg.engines?.node) {
1763
- const match = pkg.engines.node.match(/(\d+)/);
1764
- if (match) return match[1];
1765
- }
1559
+ const portMatch = ((JSON.parse(readFileSync(pkgPath, "utf-8")).scripts || {}).start || "").match(/(?:--port|-p)\s+(\d+)/);
1560
+ if (portMatch) {
1561
+ const hardcodedPort = parseInt(portMatch[1], 10);
1562
+ if (hardcodedPort !== 3e3) return {
1563
+ message: `Script start usa porta ${hardcodedPort} — certifique-se de que a porta do servico esta configurada corretamente`,
1564
+ hint: `Configure a porta do servico para ${hardcodedPort} no dashboard ou veloz.json`
1565
+ };
1766
1566
  }
1767
1567
  } catch {}
1768
- return "20";
1568
+ return null;
1569
+ }
1570
+ /**
1571
+ * Check for .env files that might be accidentally uploaded.
1572
+ */
1573
+ function checkEnvFileCommitted(basePath) {
1574
+ if (!existsSync(resolve(basePath, ".env"))) return null;
1575
+ const gitignorePath = resolve(basePath, ".gitignore");
1576
+ if (existsSync(gitignorePath)) try {
1577
+ const lines = readFileSync(gitignorePath, "utf-8").split("\n").map((l) => l.trim());
1578
+ if (lines.includes(".env") || lines.includes(".env*") || lines.includes("*.env")) return null;
1579
+ } catch {}
1580
+ return {
1581
+ message: "Arquivo .env encontrado e nao esta no .gitignore",
1582
+ hint: "Adicione .env ao .gitignore — use variaveis de ambiente no dashboard ou CLI"
1583
+ };
1769
1584
  }
1770
- function prepareExtraFiles(detection, serviceConfig) {
1585
+ /**
1586
+ * Node.js project without a start command — nixpacks won't know how to run it.
1587
+ * Checks for: scripts.start, main field, or common entry files.
1588
+ */
1589
+ function checkMissingStartCommand(basePath) {
1590
+ const pkgPath = resolve(basePath, "package.json");
1591
+ if (!existsSync(pkgPath)) return null;
1592
+ if (existsSync(resolve(basePath, "Dockerfile"))) return null;
1593
+ try {
1594
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1595
+ if (pkg.scripts?.start) return null;
1596
+ if (pkg.main) return null;
1597
+ const allDeps = {
1598
+ ...pkg.dependencies,
1599
+ ...pkg.devDependencies
1600
+ };
1601
+ if ([
1602
+ "next",
1603
+ "nuxt",
1604
+ "nuxt3",
1605
+ "@sveltejs/kit",
1606
+ "remix",
1607
+ "astro",
1608
+ "@angular/core",
1609
+ "gatsby"
1610
+ ].some((f) => f in allDeps)) return null;
1611
+ if ([
1612
+ "index.js",
1613
+ "index.mjs",
1614
+ "index.ts",
1615
+ "server.js",
1616
+ "server.ts",
1617
+ "app.js",
1618
+ "app.ts",
1619
+ "src/index.js",
1620
+ "src/index.ts",
1621
+ "src/server.js",
1622
+ "src/server.ts"
1623
+ ].some((f) => existsSync(resolve(basePath, f)))) return null;
1624
+ return {
1625
+ message: "Nenhum script start encontrado no package.json",
1626
+ hint: "Adicione \"start\" em scripts (ex: \"node dist/index.js\") ou um campo \"main\" no package.json"
1627
+ };
1628
+ } catch {
1629
+ return null;
1630
+ }
1631
+ }
1632
+ /**
1633
+ * packageManager field in package.json doesn't match the lockfile present.
1634
+ * e.g., packageManager: "pnpm@9.0.0" but only package-lock.json exists.
1635
+ */
1636
+ function checkPackageManagerMismatch(basePath) {
1637
+ const pkgPath = resolve(basePath, "package.json");
1638
+ if (!existsSync(pkgPath)) return null;
1639
+ try {
1640
+ const pmField = JSON.parse(readFileSync(pkgPath, "utf-8")).packageManager;
1641
+ if (!pmField) return null;
1642
+ const declaredPm = pmField.split("@")[0];
1643
+ const lockfileMap = {
1644
+ npm: ["package-lock.json"],
1645
+ yarn: ["yarn.lock"],
1646
+ pnpm: ["pnpm-lock.yaml"],
1647
+ bun: ["bun.lockb", "bun.lock"]
1648
+ };
1649
+ const expectedLockfiles = lockfileMap[declaredPm];
1650
+ if (!expectedLockfiles) return null;
1651
+ if (expectedLockfiles.some((f) => existsSync(resolve(basePath, f)))) return null;
1652
+ const otherPms = Object.entries(lockfileMap).filter(([pm]) => pm !== declaredPm);
1653
+ for (const [pm, files] of otherPms) if (files.some((f) => existsSync(resolve(basePath, f)))) return {
1654
+ message: `packageManager "${pmField}" no package.json mas lockfile de ${pm} encontrado`,
1655
+ hint: `Remova o campo packageManager ou gere o lockfile correto com ${declaredPm} install`
1656
+ };
1657
+ } catch {}
1658
+ return null;
1659
+ }
1660
+ /**
1661
+ * Native modules that need system packages to build.
1662
+ * The server auto-detects and injects apt packages for known modules (sharp, canvas,
1663
+ * puppeteer, playwright-chromium). This check warns about bcrypt which has a pure-JS
1664
+ * alternative, and modules not in the auto-detect list.
1665
+ */
1666
+ function checkNativeModules(basePath) {
1667
+ const pkgPath = resolve(basePath, "package.json");
1668
+ if (!existsSync(pkgPath)) return null;
1669
+ if (existsSync(resolve(basePath, "Dockerfile"))) return null;
1670
+ try {
1671
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1672
+ if ("bcrypt" in {
1673
+ ...pkg.dependencies,
1674
+ ...pkg.devDependencies
1675
+ }) return {
1676
+ message: "bcrypt compila codigo nativo — pode falhar em alguns ambientes",
1677
+ hint: "Considere usar bcryptjs (pure JS) para evitar falhas de build"
1678
+ };
1679
+ } catch {}
1680
+ return null;
1681
+ }
1682
+ /**
1683
+ * nixpacks.toml phases that override defaults instead of extending them.
1684
+ * Missing "..." in [phases.X.cmds] replaces all default commands.
1685
+ */
1686
+ function checkNixpacksTomlSpread(basePath) {
1687
+ const tomlPath = resolve(basePath, "nixpacks.toml");
1688
+ if (!existsSync(tomlPath)) return null;
1689
+ try {
1690
+ const content = readFileSync(tomlPath, "utf-8");
1691
+ if (!content.match(/\[phases\.\w+\]/g)) return null;
1692
+ const hasCmds = /cmds\s*=\s*\[/.test(content);
1693
+ const hasSpread = content.includes("\"...\"");
1694
+ if (hasCmds && !hasSpread) return {
1695
+ message: "nixpacks.toml define cmds sem \"...\" — isso substitui os comandos padrao",
1696
+ hint: "Adicione \"...\" no array cmds para manter os comandos padrao: cmds = [\"...\", \"seu-comando\"]"
1697
+ };
1698
+ } catch {}
1699
+ return null;
1700
+ }
1701
+ /**
1702
+ * Django project without gunicorn — the dev server isn't suitable for production.
1703
+ */
1704
+ function checkDjangoGunicorn(basePath) {
1705
+ if (existsSync(resolve(basePath, "Dockerfile"))) return null;
1706
+ const requirementsFiles = [
1707
+ "requirements.txt",
1708
+ "requirements/production.txt",
1709
+ "requirements/prod.txt"
1710
+ ];
1711
+ let hasDjango = false;
1712
+ let hasGunicorn = false;
1713
+ for (const file of requirementsFiles) {
1714
+ const filePath = resolve(basePath, file);
1715
+ if (!existsSync(filePath)) continue;
1716
+ try {
1717
+ const content = readFileSync(filePath, "utf-8").toLowerCase();
1718
+ if (content.includes("django")) hasDjango = true;
1719
+ if (content.includes("gunicorn") || content.includes("uvicorn")) hasGunicorn = true;
1720
+ } catch {
1721
+ continue;
1722
+ }
1723
+ }
1724
+ for (const file of ["pyproject.toml", "Pipfile"]) {
1725
+ const filePath = resolve(basePath, file);
1726
+ if (!existsSync(filePath)) continue;
1727
+ try {
1728
+ const content = readFileSync(filePath, "utf-8").toLowerCase();
1729
+ if (content.includes("django")) hasDjango = true;
1730
+ if (content.includes("gunicorn") || content.includes("uvicorn")) hasGunicorn = true;
1731
+ } catch {
1732
+ continue;
1733
+ }
1734
+ }
1735
+ if (hasDjango && !hasGunicorn) return {
1736
+ message: "Django detectado sem gunicorn/uvicorn — o servidor de dev nao deve ser usado em producao",
1737
+ hint: "Adicione gunicorn ao requirements.txt e configure o start command: \"gunicorn myproject.wsgi\""
1738
+ };
1739
+ return null;
1740
+ }
1741
+ /**
1742
+ * SvelteKit needs adapter-node for container deploys.
1743
+ * Default adapter-auto or adapter-vercel/netlify won't work.
1744
+ */
1745
+ function checkSvelteKitAdapter(basePath) {
1746
+ const pkgPath = resolve(basePath, "package.json");
1747
+ if (!existsSync(pkgPath)) return null;
1748
+ try {
1749
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1750
+ const allDeps = {
1751
+ ...pkg.dependencies,
1752
+ ...pkg.devDependencies
1753
+ };
1754
+ if (!("@sveltejs/kit" in allDeps)) return null;
1755
+ const hasNodeAdapter = "@sveltejs/adapter-node" in allDeps;
1756
+ const hasBunAdapter = "svelte-adapter-bun" in allDeps;
1757
+ if (hasNodeAdapter || hasBunAdapter) return null;
1758
+ const installed = [
1759
+ "@sveltejs/adapter-vercel",
1760
+ "@sveltejs/adapter-netlify",
1761
+ "@sveltejs/adapter-cloudflare",
1762
+ "@sveltejs/adapter-cloudflare-workers"
1763
+ ].find((a) => a in allDeps);
1764
+ if (installed) return {
1765
+ message: `SvelteKit usa ${installed} — incompativel com Veloz`,
1766
+ hint: "Instale @sveltejs/adapter-node e configure em svelte.config.js"
1767
+ };
1768
+ if ("@sveltejs/adapter-auto" in allDeps) return {
1769
+ message: "SvelteKit usa adapter-auto — pode nao funcionar em container",
1770
+ hint: "Instale @sveltejs/adapter-node para deploys em container"
1771
+ };
1772
+ } catch {}
1773
+ return null;
1774
+ }
1775
+ /**
1776
+ * Run all pre-deploy checks and return warnings.
1777
+ * Does not block — just warns the user.
1778
+ */
1779
+ function runPreDeployChecks(basePath = ".") {
1780
+ const warnings = [];
1781
+ const fullPath = resolve(process.cwd(), basePath);
1782
+ const checks = [
1783
+ checkNitroPreset,
1784
+ checkDockerfileLockFiles,
1785
+ checkNextStandalone,
1786
+ checkPrismaGenerate,
1787
+ checkPortMismatch,
1788
+ checkEnvFileCommitted,
1789
+ checkSvelteKitAdapter,
1790
+ checkMissingStartCommand,
1791
+ checkPackageManagerMismatch,
1792
+ checkNativeModules,
1793
+ checkNixpacksTomlSpread,
1794
+ checkDjangoGunicorn
1795
+ ];
1796
+ for (const check of checks) {
1797
+ const result = check(fullPath);
1798
+ if (result) warnings.push(result);
1799
+ }
1800
+ return warnings;
1801
+ }
1802
+ /**
1803
+ * Print deploy warnings to the console.
1804
+ * Returns true if any warnings were found.
1805
+ */
1806
+ function printDeployWarnings(warnings) {
1807
+ if (warnings.length === 0) return false;
1808
+ console.log();
1809
+ for (const w of warnings) {
1810
+ console.log(chalk.yellow(` AVISO: ${w.message}`));
1811
+ console.log(chalk.dim(` ${w.hint}`));
1812
+ }
1813
+ console.log();
1814
+ return true;
1815
+ }
1816
+
1817
+ //#endregion
1818
+ //#region src/commands/deploy.ts
1819
+ /**
1820
+ * If a Dockerfile exists in a subdirectory (rootDirectory), copy it to tar root
1821
+ * so BuildKit can find it. If no Dockerfile exists anywhere, return nothing —
1822
+ * the server will generate one with nixpacks.
1823
+ */
1824
+ function prepareExtraFiles(_detection, serviceConfig) {
1771
1825
  if (existsSync(resolve(process.cwd(), "Dockerfile"))) return [];
1772
1826
  const rootDir = serviceConfig?.rootDirectory || ".";
1773
1827
  const serviceDockerfilePath = resolve(process.cwd(), rootDir, "Dockerfile");
@@ -1775,68 +1829,34 @@ function prepareExtraFiles(detection, serviceConfig) {
1775
1829
  name: "Dockerfile",
1776
1830
  content: readFileSync(serviceDockerfilePath, "utf-8")
1777
1831
  }];
1778
- const fw = detection.framework;
1779
- const pm = detectPackageManager();
1780
- const nodeVersion = detectNodeVersion();
1781
- const type = fw?.type ?? serviceConfig?.type?.toUpperCase() ?? "WEB";
1782
- const files = [{
1783
- name: "Dockerfile",
1784
- content: generateDockerfile({
1785
- serviceType: type,
1786
- nodeVersion,
1787
- pm,
1788
- buildCommand: serviceConfig?.buildCommand ?? fw?.buildCommand ?? `${pm} run build`,
1789
- startCommand: serviceConfig?.startCommand ?? fw?.startCommand ?? void 0,
1790
- outputDir: serviceConfig?.outputDir ?? fw?.outputDir ?? void 0,
1791
- rootDirectory: serviceConfig?.rootDirectory,
1792
- port: serviceConfig?.port ?? fw?.port ?? 3e3
1793
- })
1794
- }];
1795
- if (type === "STATIC") files.push({
1796
- name: "nginx.conf",
1797
- content: NGINX_CONF
1798
- }, {
1799
- name: "generate-nginx-config.cjs",
1800
- content: GENERATE_NGINX_CONFIG_CJS
1801
- });
1802
- return files;
1832
+ return [];
1803
1833
  }
1804
1834
  async function computeExtraFilesForServices(services) {
1805
1835
  const velozConfig = loadConfig();
1806
1836
  const client = await getClient();
1807
- const detection = detectLocalRepo();
1808
1837
  const results = [];
1838
+ const allWarnings = [];
1809
1839
  for (const svc of services) {
1810
- let serviceConf;
1811
- if (velozConfig) {
1812
- for (const [, conf] of Object.entries(velozConfig.services)) if (conf.id === svc.serviceId) {
1813
- const merged = mergeServiceWithDefaults(conf, velozConfig.defaults);
1814
- serviceConf = {
1815
- type: merged.type,
1816
- buildCommand: merged.build?.command ?? void 0,
1817
- startCommand: merged.runtime?.command ?? void 0,
1818
- port: merged.runtime?.port ?? void 0,
1819
- rootDirectory: merged.root,
1820
- outputDir: merged.build?.outputDir ?? void 0,
1821
- instanceCount: merged.resources?.instances ?? void 0,
1822
- cpuLimit: merged.resources?.cpu ?? void 0,
1823
- memoryLimit: merged.resources?.memory ?? void 0,
1824
- healthCheckPath: merged.runtime?.healthCheck?.path ?? null
1825
- };
1826
- break;
1827
- }
1828
- }
1829
- if (serviceConf) await client.services.update({
1830
- serviceId: svc.serviceId,
1831
- port: serviceConf.port,
1832
- instanceCount: serviceConf.instanceCount,
1833
- cpuLimit: serviceConf.cpuLimit,
1834
- memoryLimit: serviceConf.memoryLimit,
1835
- buildCommand: serviceConf.buildCommand,
1836
- startCommand: serviceConf.startCommand,
1837
- rootDirectory: serviceConf.rootDirectory,
1838
- healthCheckPath: serviceConf.healthCheckPath
1840
+ const warnings = runPreDeployChecks(resolveServiceConf(velozConfig, svc.serviceId)?.rootDirectory || ".");
1841
+ if (warnings.length > 0) allWarnings.push({
1842
+ service: svc.serviceName,
1843
+ warnings
1839
1844
  });
1845
+ }
1846
+ if (allWarnings.length > 0) {
1847
+ for (const { service, warnings } of allWarnings) {
1848
+ console.log(chalk.yellow(`\n ${chalk.bold(service)}:`));
1849
+ printDeployWarnings(warnings);
1850
+ }
1851
+ if (!await promptConfirm("Continuar mesmo assim?", false)) {
1852
+ info("Deploy cancelado.");
1853
+ process.exit(0);
1854
+ }
1855
+ }
1856
+ for (const svc of services) {
1857
+ const serviceConf = resolveServiceConf(velozConfig, svc.serviceId);
1858
+ if (serviceConf) await syncServiceConfig(client, svc.serviceId, serviceConf);
1859
+ const detection = detectLocalRepo(serviceConf?.rootDirectory || ".");
1840
1860
  results.push({
1841
1861
  ...svc,
1842
1862
  extraFiles: prepareExtraFiles(detection, serviceConf)
@@ -1851,45 +1871,32 @@ async function triggerDeploy(serviceId, serviceName, preDetection) {
1851
1871
  const sizeMB = Math.round(sizeInBytes / (1024 * 1024) * 10) / 10;
1852
1872
  if (sizeMB > 5) spinUpload.text = `Fazendo upload (${sizeMB} MB)...`;
1853
1873
  const client = await getClient();
1854
- const velozConfig = loadConfig();
1855
- let serviceConf;
1856
- if (velozConfig) {
1857
- for (const [, svc] of Object.entries(velozConfig.services)) if (svc.id === serviceId) {
1858
- const merged = mergeServiceWithDefaults(svc, velozConfig.defaults);
1859
- serviceConf = {
1860
- type: merged.type,
1861
- buildCommand: merged.build?.command ?? void 0,
1862
- startCommand: merged.runtime?.command ?? void 0,
1863
- port: merged.runtime?.port ?? void 0,
1864
- rootDirectory: merged.root,
1865
- outputDir: merged.build?.outputDir ?? void 0,
1866
- instanceCount: merged.resources?.instances ?? void 0,
1867
- cpuLimit: merged.resources?.cpu ?? void 0,
1868
- memoryLimit: merged.resources?.memory ?? void 0,
1869
- healthCheckPath: merged.runtime?.healthCheck?.path ?? null
1870
- };
1871
- break;
1874
+ const serviceConf = resolveServiceConf(loadConfig(), serviceId);
1875
+ if (serviceConf) await syncServiceConfig(client, serviceId, serviceConf);
1876
+ const extraFiles = prepareExtraFiles(preDetection ?? detectLocalRepo(), serviceConf);
1877
+ const warnings = runPreDeployChecks(serviceConf?.rootDirectory || ".");
1878
+ if (warnings.length > 0) {
1879
+ spinUpload.stop();
1880
+ printDeployWarnings(warnings);
1881
+ if (!await promptConfirm("Continuar mesmo assim?", false)) {
1882
+ info("Deploy cancelado.");
1883
+ return;
1872
1884
  }
1885
+ spinUpload.start();
1873
1886
  }
1874
- if (serviceConf) await withRetry(() => client.services.update({
1875
- serviceId,
1876
- port: serviceConf.port,
1877
- instanceCount: serviceConf.instanceCount,
1878
- cpuLimit: serviceConf.cpuLimit,
1879
- memoryLimit: serviceConf.memoryLimit,
1880
- buildCommand: serviceConf.buildCommand,
1881
- startCommand: serviceConf.startCommand,
1882
- rootDirectory: serviceConf.rootDirectory,
1883
- healthCheckPath: serviceConf.healthCheckPath
1884
- }));
1885
- const extraFiles = prepareExtraFiles(preDetection ?? detectLocalRepo(), serviceConf);
1886
1887
  spinUpload.text = "Iniciando deploy...";
1887
1888
  const deployment = await withRetry(() => client.deployments.create({ serviceId }));
1888
1889
  spinUpload.text = "Fazendo upload do código...";
1889
1890
  await withRetry(() => uploadSource(deployment.id, process.cwd(), extraFiles));
1890
1891
  spinUpload.stop();
1891
1892
  success("Deploy iniciado com sucesso!");
1892
- await streamDeploymentLogs(deployment.id, serviceName);
1893
+ setupSigintHandler();
1894
+ trackDeployment(deployment.id);
1895
+ try {
1896
+ await streamDeploymentLogs(deployment.id, serviceName);
1897
+ } finally {
1898
+ untrackDeployment(deployment.id);
1899
+ }
1893
1900
  } catch (error) {
1894
1901
  spinUpload.stop();
1895
1902
  handleError(error);
@@ -3130,10 +3137,23 @@ apikeyCommand.command("delete <keyId>").alias("deletar").description("Deletar um
3130
3137
  }
3131
3138
  });
3132
3139
 
3140
+ //#endregion
3141
+ //#region src/commands/whoami.ts
3142
+ const whoamiCommand = new Command("whoami").description("Mostrar usuário autenticado").action(async () => {
3143
+ try {
3144
+ const user = await (await getClient()).me();
3145
+ console.log();
3146
+ console.log(` ${chalk.bold("Nome:")} ${user.name}`);
3147
+ console.log(` ${chalk.bold("Email:")} ${user.email}`);
3148
+ console.log();
3149
+ } catch (error) {
3150
+ handleError(error);
3151
+ }
3152
+ });
3153
+
3133
3154
  //#endregion
3134
3155
  //#region src/index.ts
3135
- const version = process.env.npm_package_version;
3136
- const program = new Command().name("veloz").description("CLI da plataforma Veloz — deploy rápido para o Brasil").version(version ?? "0.0.0-beta.1");
3156
+ const program = new Command().name("veloz").description("CLI da plataforma Veloz — deploy rápido para o Brasil").version("0.0.0-beta.6");
3137
3157
  program.addCommand(loginCommand);
3138
3158
  program.addCommand(logoutCommand);
3139
3159
  program.addCommand(projectsCommand);
@@ -3145,6 +3165,7 @@ program.addCommand(domainsCommand);
3145
3165
  program.addCommand(configCommand);
3146
3166
  program.addCommand(useCommand);
3147
3167
  program.addCommand(apikeyCommand);
3168
+ program.addCommand(whoamiCommand);
3148
3169
  program.parse();
3149
3170
 
3150
3171
  //#endregion