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.
- package/dist/index.mjs +615 -594
- 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://
|
|
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
|
|
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
|
|
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-
|
|
1094
|
-
const statusLabels
|
|
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
|
|
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
|
|
1124
|
-
const icon = event.content
|
|
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
|
|
1153
|
-
const errorMsg = serviceName ? `✗ Deploy de ${chalk.bold(serviceName)} finalizou com status: ${statusLabels
|
|
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
|
-
|
|
1174
|
+
process.exit(1);
|
|
1156
1175
|
}
|
|
1157
1176
|
}
|
|
1158
1177
|
|
|
1159
1178
|
//#endregion
|
|
1160
|
-
//#region src/lib/
|
|
1161
|
-
async function withRetry
|
|
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
|
|
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((
|
|
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
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
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
|
|
1235
|
-
await withRetry
|
|
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
|
-
|
|
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/
|
|
1330
|
-
function
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
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
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
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/
|
|
1397
|
+
//#region src/lib/deploy-checks.ts
|
|
1531
1398
|
/**
|
|
1532
|
-
*
|
|
1533
|
-
*
|
|
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
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
const
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
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
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
const
|
|
1634
|
-
if (
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
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
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
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
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
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
|
-
|
|
1523
|
+
return null;
|
|
1750
1524
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
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
|
-
|
|
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
|
|
1760
|
-
if (
|
|
1761
|
-
const
|
|
1762
|
-
if (
|
|
1763
|
-
|
|
1764
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1811
|
-
if (
|
|
1812
|
-
|
|
1813
|
-
|
|
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
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
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
|
-
|
|
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
|
|
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
|