onveloz 0.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.mjs +2949 -0
  2. package/package.json +52 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,2949 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "@commander-js/extra-typings";
3
+ import chalk from "chalk";
4
+ import { exec, execSync } from "node:child_process";
5
+ import { homedir, platform, tmpdir } from "node:os";
6
+ import { createHash } from "node:crypto";
7
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
8
+ import { join, relative, resolve } from "node:path";
9
+ import * as readline from "node:readline";
10
+ import { createInterface } from "node:readline";
11
+ import ora from "ora";
12
+ import { createAuthClient } from "better-auth/client";
13
+ import { deviceAuthorizationClient } from "better-auth/client/plugins";
14
+ import { createORPCClient } from "@orpc/client";
15
+ import { RPCLink } from "@orpc/client/fetch";
16
+ import { z } from "zod";
17
+ import { mkdtemp, readdir, rm, stat } from "node:fs/promises";
18
+ import ignore from "ignore";
19
+ import tar from "tar";
20
+
21
+ //#region src/lib/config.ts
22
+ const CONFIG_DIR = join(homedir(), ".veloz");
23
+ const ENV_API_URL = process.env.VELOZ_API_URL;
24
+ const DEFAULT_API_URL = ENV_API_URL ?? "https://platform.onveloz.com";
25
+ function getConfigFile() {
26
+ if (!ENV_API_URL) return join(CONFIG_DIR, "config.json");
27
+ return join(CONFIG_DIR, `config.${createHash("md5").update(ENV_API_URL).digest("hex").slice(0, 8)}.json`);
28
+ }
29
+ const CONFIG_FILE$1 = getConfigFile();
30
+ function ensureConfigDir() {
31
+ if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
32
+ }
33
+ function loadConfig$1() {
34
+ if (!existsSync(CONFIG_FILE$1)) return {
35
+ apiKey: "",
36
+ apiUrl: DEFAULT_API_URL
37
+ };
38
+ try {
39
+ const raw = readFileSync(CONFIG_FILE$1, "utf-8");
40
+ const parsed = JSON.parse(raw);
41
+ return {
42
+ apiKey: parsed.apiKey ?? "",
43
+ apiUrl: ENV_API_URL ?? parsed.apiUrl ?? DEFAULT_API_URL
44
+ };
45
+ } catch {
46
+ return {
47
+ apiKey: "",
48
+ apiUrl: DEFAULT_API_URL
49
+ };
50
+ }
51
+ }
52
+ function saveConfig$1(config) {
53
+ ensureConfigDir();
54
+ const existing = loadConfig$1();
55
+ const merged = {
56
+ apiKey: config.apiKey ?? existing.apiKey,
57
+ apiUrl: config.apiUrl ?? existing.apiUrl
58
+ };
59
+ writeFileSync(CONFIG_FILE$1, JSON.stringify(merged, null, 2), "utf-8");
60
+ }
61
+ function deleteConfig() {
62
+ if (existsSync(CONFIG_FILE$1)) unlinkSync(CONFIG_FILE$1);
63
+ }
64
+ function isAuthenticated() {
65
+ return loadConfig$1().apiKey.length > 0;
66
+ }
67
+ let envApiUrlLogged = false;
68
+ async function requireAuth() {
69
+ let config = loadConfig$1();
70
+ if (!config.apiKey) {
71
+ await performLogin(config.apiUrl);
72
+ config = loadConfig$1();
73
+ }
74
+ if (ENV_API_URL && !envApiUrlLogged) {
75
+ envApiUrlLogged = true;
76
+ console.log(`\n🔧 Usando API: ${ENV_API_URL}\n`);
77
+ }
78
+ return config;
79
+ }
80
+
81
+ //#endregion
82
+ //#region src/lib/utils.ts
83
+ function isRateLimitError(error) {
84
+ if (!(error instanceof Error)) return null;
85
+ const orpcError = error;
86
+ if (orpcError.code === "TOO_MANY_REQUESTS" && orpcError.data?.retryAfterMs) return { retryAfterMs: orpcError.data.retryAfterMs };
87
+ return null;
88
+ }
89
+ function handleError(error) {
90
+ if (error instanceof Error) {
91
+ const orpcError = error;
92
+ if (orpcError.code) {
93
+ const message = orpcError.data?.message ?? orpcError.message ?? {
94
+ NOT_FOUND: "Recurso não encontrado.",
95
+ FORBIDDEN: "Sem permissão para acessar este recurso.",
96
+ CONFLICT: "Conflito — recurso já existe.",
97
+ BAD_REQUEST: "Requisição inválida.",
98
+ UNAUTHORIZED: "Não autorizado. Execute `veloz login` para autenticar.",
99
+ INTERNAL_SERVER_ERROR: "Erro interno do servidor. Tente novamente.",
100
+ TOO_MANY_REQUESTS: "Tente novamente em alguns segundos."
101
+ }[orpcError.code] ?? "Erro desconhecido.";
102
+ console.error(chalk.red(`\n✗ Erro: ${message}`));
103
+ process.exit(1);
104
+ }
105
+ if (error.message.includes("fetch") || error.message.includes("ECONNREFUSED")) {
106
+ console.error(chalk.red("\n✗ Erro de conexão: Não foi possível conectar ao servidor Veloz."));
107
+ console.error(chalk.yellow(" Verifique se o servidor está rodando e a URL está correta."));
108
+ process.exit(1);
109
+ }
110
+ console.error(chalk.red(`\n✗ Erro: ${error.message}`));
111
+ process.exit(1);
112
+ }
113
+ console.error(chalk.red("\n✗ Erro inesperado."));
114
+ process.exit(1);
115
+ }
116
+ function success(message) {
117
+ console.log(chalk.green(`\n✓ ${message}`));
118
+ }
119
+ function info(message) {
120
+ console.log(chalk.cyan(`ℹ ${message}`));
121
+ }
122
+ function warn(message) {
123
+ console.log(chalk.yellow(`⚠ ${message}`));
124
+ }
125
+ function spinner(text) {
126
+ return ora({
127
+ text,
128
+ color: "cyan"
129
+ }).start();
130
+ }
131
+ function printTable(headers, rows) {
132
+ const colWidths = headers.map((h, i) => {
133
+ const maxRow = rows.reduce((max, row) => Math.max(max, (row[i] ?? "").length), 0);
134
+ return Math.max(h.length, maxRow);
135
+ });
136
+ const headerLine = headers.map((h, i) => chalk.bold(h.padEnd(colWidths[i]))).join(" ");
137
+ const separator = colWidths.map((w) => "─".repeat(w)).join("──");
138
+ console.log(`\n${headerLine}`);
139
+ console.log(chalk.dim(separator));
140
+ for (const row of rows) {
141
+ const line = row.map((cell, i) => cell.padEnd(colWidths[i])).join(" ");
142
+ console.log(line);
143
+ }
144
+ console.log();
145
+ }
146
+ async function prompt(question) {
147
+ const rl = createInterface({
148
+ input: process.stdin,
149
+ output: process.stdout
150
+ });
151
+ return new Promise((resolve$1) => {
152
+ rl.question(chalk.cyan(`${question} `), (answer) => {
153
+ rl.close();
154
+ resolve$1(answer.trim());
155
+ });
156
+ });
157
+ }
158
+ async function promptConfirm(question, defaultYes = true) {
159
+ const answer = await prompt(`${question} ${defaultYes ? "(S/n)" : "(s/N)"}`);
160
+ if (answer === "") return defaultYes;
161
+ return answer.toLowerCase().startsWith("s") || answer.toLowerCase().startsWith("y");
162
+ }
163
+ async function promptSelect(question, options) {
164
+ console.log(chalk.cyan(`\n${question}\n`));
165
+ for (let i = 0; i < options.length; i++) console.log(chalk.white(` ${chalk.bold(`${i + 1})`)} ${options[i].label}`));
166
+ const answer = await prompt("\nEscolha (número):");
167
+ const index = parseInt(answer, 10) - 1;
168
+ if (isNaN(index) || index < 0 || index >= options.length) {
169
+ console.error(chalk.red("Opção inválida."));
170
+ process.exit(1);
171
+ }
172
+ return options[index].value;
173
+ }
174
+ async function promptMultiSelect(question, options) {
175
+ console.log(chalk.cyan(`\n${question}\n`));
176
+ for (let i = 0; i < options.length; i++) console.log(chalk.white(` ${chalk.bold(`${i + 1})`)} ${options[i].label}`));
177
+ console.log(chalk.dim(`\n * = todos`));
178
+ const answer = await prompt("\nEscolha (números separados por vírgula, ou *):");
179
+ if (answer.trim() === "*") return options.map((o) => o.value);
180
+ const indices = answer.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((i) => !isNaN(i) && i >= 0 && i < options.length);
181
+ if (indices.length === 0) {
182
+ console.error(chalk.red("Nenhuma opção válida selecionada."));
183
+ process.exit(1);
184
+ }
185
+ return indices.map((i) => options[i].value);
186
+ }
187
+
188
+ //#endregion
189
+ //#region src/commands/login.ts
190
+ function openBrowser(url) {
191
+ const os = platform();
192
+ exec(os === "darwin" ? `open "${url}"` : os === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`, () => {});
193
+ }
194
+ const CLIENT_ID = "veloz-cli";
195
+ async function performLogin(apiUrl) {
196
+ const authClient = createAuthClient({
197
+ baseURL: apiUrl,
198
+ plugins: [deviceAuthorizationClient()]
199
+ });
200
+ const s = spinner("Iniciando autenticação...");
201
+ try {
202
+ const { data, error } = await authClient.device.code({ client_id: CLIENT_ID });
203
+ if (error || !data) {
204
+ s.fail("Erro ao iniciar autenticação.");
205
+ process.exit(1);
206
+ }
207
+ s.stop();
208
+ const verificationUrl = `${apiUrl.includes("localhost") ? "http://localhost:3001" : "https://app.onveloz.com"}${data.verification_uri_complete ? new URL(data.verification_uri_complete).pathname + new URL(data.verification_uri_complete).search : `/cli/auth?code=${data.user_code}`}`;
209
+ console.log();
210
+ info("Abrindo navegador para autenticação...");
211
+ console.log();
212
+ console.log(chalk.white(` Código de verificação: ${chalk.bold.cyan(data.user_code)}`));
213
+ console.log();
214
+ console.log(chalk.dim(" Se o navegador não abrir, acesse manualmente:"));
215
+ console.log(chalk.dim(` ${verificationUrl}`));
216
+ console.log();
217
+ openBrowser(verificationUrl);
218
+ const pollSpinner = spinner("Aguardando autorização no navegador...");
219
+ const token = await pollForToken(authClient, data.device_code, data.interval || 5);
220
+ if (!token) {
221
+ pollSpinner.fail("Tempo esgotado. Execute `veloz login` para tentar novamente.");
222
+ process.exit(1);
223
+ }
224
+ pollSpinner.stop();
225
+ saveConfig$1({
226
+ apiKey: token,
227
+ apiUrl
228
+ });
229
+ success("Autenticado com sucesso!");
230
+ } catch (err) {
231
+ s.stop();
232
+ if (err instanceof Error && (err.message.includes("fetch") || err.message.includes("ECONNREFUSED"))) {
233
+ console.error(chalk.red("\n✗ Erro de conexão: Não foi possível conectar ao servidor Veloz."));
234
+ console.error(chalk.yellow(" Verifique se o servidor está rodando e a URL está correta."));
235
+ process.exit(1);
236
+ }
237
+ console.error(chalk.red(`\n✗ Erro: ${err instanceof Error ? err.message : "Erro inesperado."}`));
238
+ process.exit(1);
239
+ }
240
+ }
241
+ const loginCommand = new Command("login").description("Autenticar na plataforma Veloz").option("--api-url <url>", "URL da API Veloz").option("--api-key <key>", "Chave de API (para CI/automação)").action(async (opts) => {
242
+ if (isAuthenticated()) {
243
+ warn("Você já está autenticado.");
244
+ if ((await prompt("Deseja fazer login novamente? (s/N)")).toLowerCase() !== "s") process.exit(0);
245
+ }
246
+ const config = loadConfig$1();
247
+ const apiUrl = opts.apiUrl ?? config.apiUrl;
248
+ if (opts.apiKey) {
249
+ saveConfig$1({
250
+ apiKey: opts.apiKey,
251
+ apiUrl
252
+ });
253
+ success("Autenticado com sucesso!");
254
+ info("Configuração salva em ~/.veloz/config.json");
255
+ return;
256
+ }
257
+ await performLogin(apiUrl);
258
+ });
259
+ async function pollForToken(authClient, deviceCode, interval) {
260
+ let pollingInterval = interval;
261
+ const maxAttempts = Math.ceil(300 / pollingInterval);
262
+ for (let i = 0; i < maxAttempts; i++) {
263
+ await new Promise((r) => setTimeout(r, pollingInterval * 1e3));
264
+ try {
265
+ const { data, error } = await authClient.device.token({
266
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
267
+ device_code: deviceCode,
268
+ client_id: CLIENT_ID
269
+ });
270
+ if (data?.access_token) return data.access_token;
271
+ if (error?.error === "slow_down") pollingInterval += 5;
272
+ if (error?.error === "expired_token") return null;
273
+ } catch {
274
+ continue;
275
+ }
276
+ }
277
+ return null;
278
+ }
279
+ const logoutCommand = new Command("logout").description("Encerrar sessão na plataforma Veloz").action(() => {
280
+ if (!isAuthenticated()) {
281
+ warn("Você não está autenticado.");
282
+ return;
283
+ }
284
+ deleteConfig();
285
+ success("Sessão encerrada com sucesso.");
286
+ });
287
+
288
+ //#endregion
289
+ //#region ../../packages/api/src/client.ts
290
+ function createClient(baseUrl, headers) {
291
+ return createORPCClient(new RPCLink({
292
+ url: `${baseUrl}/rpc`,
293
+ headers: headers ?? (() => ({})),
294
+ fetch: (request, init) => {
295
+ return globalThis.fetch(request, {
296
+ ...init,
297
+ credentials: "include"
298
+ });
299
+ }
300
+ }));
301
+ }
302
+
303
+ //#endregion
304
+ //#region src/lib/client.ts
305
+ async function getClient() {
306
+ const config = await requireAuth();
307
+ return createClient(config.apiUrl, () => ({ Authorization: `Bearer ${config.apiKey}` }));
308
+ }
309
+
310
+ //#endregion
311
+ //#region src/commands/projects.ts
312
+ const projectsCommand = new Command("projects").alias("projetos").description("Gerenciar projetos");
313
+ projectsCommand.command("list").alias("listar").description("Listar todos os projetos").action(async () => {
314
+ const spin = spinner("Carregando projetos...");
315
+ try {
316
+ const projects = await (await getClient()).projects.list();
317
+ spin.stop();
318
+ if (projects.length === 0) {
319
+ info("Nenhum projeto encontrado. Crie um pelo dashboard.");
320
+ return;
321
+ }
322
+ printTable([
323
+ "ID",
324
+ "Nome",
325
+ "Slug",
326
+ "Repo GitHub",
327
+ "Criado em"
328
+ ], projects.map((p) => [
329
+ p.id.slice(0, 8),
330
+ p.name,
331
+ p.slug,
332
+ p.githubRepoOwner && p.githubRepoName ? `${p.githubRepoOwner}/${p.githubRepoName}` : "—",
333
+ new Date(p.createdAt).toLocaleDateString("pt-BR")
334
+ ]));
335
+ } catch (error) {
336
+ spin.stop();
337
+ handleError(error);
338
+ }
339
+ });
340
+
341
+ //#endregion
342
+ //#region ../../packages/config/veloz-config.ts
343
+ const ServiceTypeSchema = z.enum(["web", "static"]);
344
+ const PackageManagerSchema = z.enum([
345
+ "npm",
346
+ "yarn",
347
+ "pnpm",
348
+ "bun",
349
+ "auto"
350
+ ]);
351
+ const BuildConfigSchema = z.object({
352
+ command: z.string().nullable().optional(),
353
+ nodeVersion: z.string().regex(/^[0-9]+(\.x)?$/).default("20").optional(),
354
+ packageManager: PackageManagerSchema.default("auto").optional(),
355
+ installCommand: z.string().nullable().optional(),
356
+ outputDir: z.string().nullable().optional()
357
+ });
358
+ const RuntimeConfigSchema = z.object({
359
+ command: z.string().nullable().optional(),
360
+ port: z.number().min(1).max(65535).default(3e3).optional(),
361
+ healthCheck: z.object({
362
+ path: z.string().default("/").optional(),
363
+ interval: z.number().default(30).optional(),
364
+ timeout: z.number().default(10).optional()
365
+ }).optional()
366
+ });
367
+ const ResourcesSchema = z.object({
368
+ instances: z.number().min(1).max(10).default(1).optional(),
369
+ cpu: z.string().regex(/^[0-9]+(\.[0-9]+)?|[0-9]+m$/).default("500m").optional(),
370
+ memory: z.string().regex(/^[0-9]+(Mi|Gi)$/).default("512Mi").optional(),
371
+ autoscale: z.object({
372
+ enabled: z.boolean().default(false).optional(),
373
+ minInstances: z.number().min(1).default(1).optional(),
374
+ maxInstances: z.number().min(1).max(20).default(3).optional(),
375
+ targetCPU: z.number().min(10).max(90).default(70).optional()
376
+ }).optional()
377
+ });
378
+ const EnvVarDefinitionSchema = z.object({
379
+ description: z.string().optional(),
380
+ required: z.boolean().default(false).optional(),
381
+ example: z.string().optional()
382
+ });
383
+ const ServiceConfigSchema = z.object({
384
+ id: z.string(),
385
+ name: z.string(),
386
+ type: ServiceTypeSchema.default("web"),
387
+ root: z.string().default("."),
388
+ branch: z.string().optional(),
389
+ build: BuildConfigSchema.optional(),
390
+ runtime: RuntimeConfigSchema.optional(),
391
+ env: z.record(z.string().regex(/^[A-Z][A-Z0-9_]*$/), EnvVarDefinitionSchema).optional(),
392
+ resources: ResourcesSchema.optional()
393
+ });
394
+ const ProjectConfigSchema = z.object({
395
+ id: z.string(),
396
+ name: z.string(),
397
+ slug: z.string().regex(/^[a-z0-9-]+$/).optional()
398
+ });
399
+ const ServiceDefaultsSchema = z.object({
400
+ type: ServiceTypeSchema.optional(),
401
+ branch: z.string().optional(),
402
+ build: BuildConfigSchema.optional(),
403
+ runtime: RuntimeConfigSchema.optional(),
404
+ resources: ResourcesSchema.optional()
405
+ });
406
+ const VelozConfigSchema = z.object({
407
+ $schema: z.string().optional(),
408
+ version: z.literal("1.0"),
409
+ project: ProjectConfigSchema,
410
+ services: z.record(z.string(), ServiceConfigSchema),
411
+ defaults: ServiceDefaultsSchema.optional(),
412
+ created: z.string().datetime().optional(),
413
+ updated: z.string().datetime().optional()
414
+ });
415
+ function mergeServiceWithDefaults(service, defaults) {
416
+ if (!defaults) return service;
417
+ return {
418
+ ...service,
419
+ type: service.type ?? defaults.type ?? "web",
420
+ branch: service.branch ?? defaults.branch,
421
+ build: {
422
+ ...defaults.build,
423
+ ...service.build
424
+ },
425
+ runtime: {
426
+ ...defaults.runtime,
427
+ ...service.runtime
428
+ },
429
+ resources: {
430
+ ...defaults.resources,
431
+ ...service.resources
432
+ }
433
+ };
434
+ }
435
+ function parseVelozConfig(data) {
436
+ return VelozConfigSchema.parse(data);
437
+ }
438
+
439
+ //#endregion
440
+ //#region src/lib/link.ts
441
+ const CONFIG_FILE = "veloz.json";
442
+ const LOCAL_CONFIG_FILE = "veloz.local.json";
443
+ /**
444
+ * Find the root directory (git root or monorepo root)
445
+ */
446
+ function findProjectRoot(startPath = process.cwd()) {
447
+ let currentPath = resolve(startPath);
448
+ const root = resolve("/");
449
+ while (currentPath !== root) {
450
+ if (existsSync(join(currentPath, ".git"))) return currentPath;
451
+ if (existsSync(join(currentPath, "pnpm-workspace.yaml")) || existsSync(join(currentPath, "lerna.json")) || existsSync(join(currentPath, "rush.json")) || existsSync(join(currentPath, "nx.json"))) return currentPath;
452
+ const pkgJsonPath = join(currentPath, "package.json");
453
+ if (existsSync(pkgJsonPath)) try {
454
+ if (JSON.parse(readFileSync(pkgJsonPath, "utf-8")).workspaces) return currentPath;
455
+ } catch {}
456
+ const parentPath = resolve(currentPath, "..");
457
+ if (parentPath === currentPath) break;
458
+ currentPath = parentPath;
459
+ }
460
+ return process.cwd();
461
+ }
462
+ /**
463
+ * Get the config file name (veloz.local.json when VELOZ_API_URL is set, veloz.json otherwise)
464
+ */
465
+ function getConfigFileName() {
466
+ return process.env.VELOZ_API_URL ? LOCAL_CONFIG_FILE : CONFIG_FILE;
467
+ }
468
+ /**
469
+ * Get the path to veloz.json (or veloz.local.json when using custom API URL)
470
+ */
471
+ function getConfigPath() {
472
+ return join(findProjectRoot(), getConfigFileName());
473
+ }
474
+ /**
475
+ * Load the veloz.json config from project root
476
+ */
477
+ function loadConfig() {
478
+ const path = getConfigPath();
479
+ if (!existsSync(path)) return null;
480
+ try {
481
+ const raw = readFileSync(path, "utf-8");
482
+ return parseVelozConfig(JSON.parse(raw));
483
+ } catch (error) {
484
+ console.error(`Error loading ${getConfigFileName()}:`, error);
485
+ return null;
486
+ }
487
+ }
488
+ /**
489
+ * Save the veloz.json config to project root
490
+ */
491
+ function saveConfig(config) {
492
+ const path = getConfigPath();
493
+ const configWithSchema = {
494
+ $schema: "https://veloz.app/schemas/veloz-config.schema.json",
495
+ ...config
496
+ };
497
+ writeFileSync(path, JSON.stringify(configWithSchema, null, 2), "utf-8");
498
+ }
499
+ /**
500
+ * Require config to exist, throw if not found
501
+ */
502
+ function requireConfig() {
503
+ const config = loadConfig();
504
+ if (!config) throw new Error(`No ${getConfigFileName()} found in project root. Run 'veloz init' or 'veloz deploy' to set up your project.`);
505
+ return config;
506
+ }
507
+ /**
508
+ * Check if current directory is a git repository
509
+ */
510
+ function isGitRepo() {
511
+ try {
512
+ execSync("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
513
+ return true;
514
+ } catch {
515
+ return false;
516
+ }
517
+ }
518
+ /**
519
+ * Get git remote origin info
520
+ */
521
+ function getGitRemote() {
522
+ try {
523
+ const url = execSync("git remote get-url origin", { stdio: "pipe" }).toString().trim();
524
+ const httpsMatch = url.match(/github\.com\/([^/]+)\/([^/.]+)/);
525
+ if (httpsMatch) return {
526
+ owner: httpsMatch[1],
527
+ repo: httpsMatch[2]
528
+ };
529
+ const sshMatch = url.match(/github\.com:([^/]+)\/([^/.]+)/);
530
+ if (sshMatch) return {
531
+ owner: sshMatch[1],
532
+ repo: sshMatch[2]
533
+ };
534
+ return null;
535
+ } catch {
536
+ return null;
537
+ }
538
+ }
539
+ /**
540
+ * Get current git branch
541
+ */
542
+ function getGitBranch() {
543
+ try {
544
+ return execSync("git rev-parse --abbrev-ref HEAD", { stdio: "pipe" }).toString().trim();
545
+ } catch {
546
+ return "main";
547
+ }
548
+ }
549
+
550
+ //#endregion
551
+ //#region src/commands/link.ts
552
+ const linkCommand = new Command("link").description("Verificar vínculo do projeto com Veloz").action(async () => {
553
+ const config = loadConfig();
554
+ if (!config) {
555
+ warn("Nenhum projeto configurado. Execute 'veloz deploy' para configurar seu projeto.");
556
+ return;
557
+ }
558
+ console.log(chalk.bold("\n🔗 Projeto Vinculado\n"));
559
+ console.log(` ${chalk.bold("Projeto:")} ${chalk.cyan(config.project.name)}`);
560
+ console.log(` ${chalk.bold("ID:")} ${chalk.dim(config.project.id)}`);
561
+ const services = Object.entries(config.services);
562
+ if (services.length > 0) {
563
+ console.log(`\n ${chalk.bold("Serviços:")}`);
564
+ services.forEach(([key, service]) => {
565
+ const type = service.type === "web" ? "Serviço Web" : "Site Estático";
566
+ console.log(` • ${chalk.cyan(service.name)} (${key}) - ${type}`);
567
+ });
568
+ }
569
+ console.log();
570
+ info("Use 'veloz deploy' para fazer deploy ou atualizar serviços.");
571
+ });
572
+
573
+ //#endregion
574
+ //#region ../../packages/api/src/lib/framework-detector.ts
575
+ const FRAMEWORK_RULES = [
576
+ {
577
+ name: "nextjs",
578
+ label: "Next.js",
579
+ type: "WEB",
580
+ match: (deps) => "next" in deps,
581
+ buildCommand: "npm run build",
582
+ startCommand: "npm start",
583
+ outputDir: ".next",
584
+ port: 3e3
585
+ },
586
+ {
587
+ name: "hono",
588
+ label: "Hono",
589
+ type: "WEB",
590
+ match: (deps) => "hono" in deps,
591
+ buildCommand: "npm run build",
592
+ startCommand: "npm start",
593
+ outputDir: "dist",
594
+ port: 3e3
595
+ },
596
+ {
597
+ name: "nuxt",
598
+ label: "Nuxt",
599
+ type: "WEB",
600
+ match: (deps) => "nuxt" in deps,
601
+ buildCommand: "npm run build",
602
+ startCommand: "npm start",
603
+ outputDir: ".nuxt",
604
+ port: 3e3
605
+ },
606
+ {
607
+ name: "remix",
608
+ label: "Remix",
609
+ type: "WEB",
610
+ match: (deps) => "@remix-run/node" in deps,
611
+ buildCommand: "npm run build",
612
+ startCommand: "npm start",
613
+ outputDir: "build",
614
+ port: 3e3
615
+ },
616
+ {
617
+ name: "sveltekit",
618
+ label: "SvelteKit",
619
+ type: "WEB",
620
+ match: (deps) => "@sveltejs/kit" in deps,
621
+ buildCommand: "npm run build",
622
+ startCommand: "npm start",
623
+ outputDir: ".svelte-kit",
624
+ port: 3e3
625
+ },
626
+ {
627
+ name: "astro",
628
+ label: "Astro",
629
+ type: "STATIC",
630
+ match: (deps) => "astro" in deps,
631
+ buildCommand: "npm run build",
632
+ startCommand: null,
633
+ outputDir: "dist",
634
+ port: 3e3
635
+ },
636
+ {
637
+ name: "gatsby",
638
+ label: "Gatsby",
639
+ type: "STATIC",
640
+ match: (deps) => "gatsby" in deps,
641
+ buildCommand: "npm run build",
642
+ startCommand: null,
643
+ outputDir: "public",
644
+ port: 3e3
645
+ },
646
+ {
647
+ name: "create-react-app",
648
+ label: "Create React App",
649
+ type: "STATIC",
650
+ match: (deps) => "react-scripts" in deps,
651
+ buildCommand: "npm run build",
652
+ startCommand: null,
653
+ outputDir: "build",
654
+ port: 3e3
655
+ },
656
+ {
657
+ name: "vite-react",
658
+ label: "Vite + React",
659
+ type: "STATIC",
660
+ match: (deps) => "vite" in deps && "react" in deps,
661
+ buildCommand: "npm run build",
662
+ startCommand: null,
663
+ outputDir: "dist",
664
+ port: 3e3
665
+ },
666
+ {
667
+ name: "vite-vue",
668
+ label: "Vite + Vue",
669
+ type: "STATIC",
670
+ match: (deps) => "vite" in deps && "vue" in deps,
671
+ buildCommand: "npm run build",
672
+ startCommand: null,
673
+ outputDir: "dist",
674
+ port: 3e3
675
+ },
676
+ {
677
+ name: "vite",
678
+ label: "Vite",
679
+ type: "STATIC",
680
+ match: (deps) => "vite" in deps,
681
+ buildCommand: "npm run build",
682
+ startCommand: null,
683
+ outputDir: "dist",
684
+ port: 3e3
685
+ },
686
+ {
687
+ name: "angular",
688
+ label: "Angular",
689
+ type: "STATIC",
690
+ match: (deps) => "@angular/core" in deps,
691
+ buildCommand: "npm run build",
692
+ startCommand: null,
693
+ outputDir: "dist",
694
+ port: 4200
695
+ },
696
+ {
697
+ name: "express",
698
+ label: "Express",
699
+ type: "WEB",
700
+ match: (deps) => "express" in deps,
701
+ buildCommand: null,
702
+ startCommand: "node index.js",
703
+ outputDir: null,
704
+ port: 3e3
705
+ },
706
+ {
707
+ name: "fastify",
708
+ label: "Fastify",
709
+ type: "WEB",
710
+ match: (deps) => "fastify" in deps,
711
+ buildCommand: null,
712
+ startCommand: "node index.js",
713
+ outputDir: null,
714
+ port: 3e3
715
+ },
716
+ {
717
+ name: "nestjs",
718
+ label: "NestJS",
719
+ type: "WEB",
720
+ match: (deps) => "@nestjs/core" in deps,
721
+ buildCommand: "npm run build",
722
+ startCommand: "npm run start:prod",
723
+ outputDir: "dist",
724
+ port: 3e3
725
+ },
726
+ {
727
+ name: "hono",
728
+ label: "Hono",
729
+ type: "WEB",
730
+ match: (deps) => "hono" in deps || "@hono/node-server" in deps,
731
+ buildCommand: null,
732
+ startCommand: "node index.js",
733
+ outputDir: null,
734
+ port: 3e3
735
+ }
736
+ ];
737
+ function detectPackageManager$1(files) {
738
+ if ("bun.lockb" in files || "bun.lock" in files) return "bun";
739
+ if ("pnpm-lock.yaml" in files || "pnpm-workspace.yaml" in files) return "pnpm";
740
+ if ("yarn.lock" in files) return "yarn";
741
+ return "npm";
742
+ }
743
+ function runCmd(pm, script) {
744
+ switch (pm) {
745
+ case "bun": return `bun run ${script}`;
746
+ case "yarn": return `yarn ${script}`;
747
+ case "pnpm": return `pnpm run ${script}`;
748
+ default: return `npm run ${script}`;
749
+ }
750
+ }
751
+ function startCmd(pm) {
752
+ switch (pm) {
753
+ case "bun": return "bun run start";
754
+ case "yarn": return "yarn start";
755
+ case "pnpm": return "pnpm start";
756
+ default: return "npm start";
757
+ }
758
+ }
759
+ function safeParse(json) {
760
+ if (!json) return null;
761
+ try {
762
+ return JSON.parse(json);
763
+ } catch {
764
+ return null;
765
+ }
766
+ }
767
+ function getAllDeps(pkg) {
768
+ const deps = pkg.dependencies ?? {};
769
+ const devDeps = pkg.devDependencies ?? {};
770
+ return {
771
+ ...deps,
772
+ ...devDeps
773
+ };
774
+ }
775
+ function detectFramework(packageJsonStr, pm = "npm") {
776
+ const pkg = safeParse(packageJsonStr);
777
+ if (!pkg) return null;
778
+ const allDeps = getAllDeps(pkg);
779
+ const scripts = pkg.scripts ?? {};
780
+ for (const rule of FRAMEWORK_RULES) if (rule.match(allDeps)) {
781
+ const ruleStart = rule.startCommand === "node index.js" ? "node index.js" : rule.startCommand ? startCmd(pm) : null;
782
+ let buildScript = "build";
783
+ let startScript = "start";
784
+ const scriptKeys = Object.keys(scripts);
785
+ const buildVariants = scriptKeys.filter((k) => k.startsWith("build:") || k.startsWith("build-"));
786
+ const startVariants = scriptKeys.filter((k) => k.startsWith("start:") || k.startsWith("start-"));
787
+ if (buildVariants.length > 0 && !scripts.build) buildScript = buildVariants[0];
788
+ if (startVariants.length > 0 && !scripts.start) startScript = startVariants[0];
789
+ return {
790
+ name: rule.name,
791
+ label: rule.label,
792
+ type: rule.type,
793
+ buildCommand: scripts[buildScript] ? runCmd(pm, buildScript) : rule.buildCommand ? runCmd(pm, "build") : null,
794
+ startCommand: scripts[startScript] ? runCmd(pm, startScript) : ruleStart,
795
+ outputDir: rule.outputDir,
796
+ port: rule.port
797
+ };
798
+ }
799
+ if (scripts.build || scripts.start) {
800
+ let outputDir = null;
801
+ if (scripts.build) {
802
+ if (scripts.build.includes("tsc")) outputDir = "dist";
803
+ else if (scripts.build.includes("tsup")) outputDir = "dist";
804
+ else if (scripts.build.includes("esbuild")) outputDir = "dist";
805
+ else if (scripts.build.includes("webpack")) outputDir = "dist";
806
+ else if (scripts.build.includes("rollup")) outputDir = "dist";
807
+ else if (scripts.build.includes("parcel")) outputDir = "dist";
808
+ }
809
+ return {
810
+ name: "node",
811
+ label: "Node.js",
812
+ type: scripts.start ? "WEB" : "STATIC",
813
+ buildCommand: scripts.build ? runCmd(pm, "build") : null,
814
+ startCommand: scripts.start ? startCmd(pm) : null,
815
+ outputDir,
816
+ port: 3e3
817
+ };
818
+ }
819
+ return null;
820
+ }
821
+ function detectEnvVars(files) {
822
+ const envContent = files[".env.example"] ?? files[".env.sample"] ?? files[".env.local.example"] ?? files[".env"];
823
+ if (!envContent) return [];
824
+ const vars = [];
825
+ for (const line of envContent.split("\n")) {
826
+ const trimmed = line.trim();
827
+ if (!trimmed || trimmed.startsWith("#")) continue;
828
+ const eqIdx = trimmed.indexOf("=");
829
+ if (eqIdx === -1) continue;
830
+ const key = trimmed.slice(0, eqIdx).trim();
831
+ const value = trimmed.slice(eqIdx + 1).trim();
832
+ if (key) vars.push({
833
+ key,
834
+ value
835
+ });
836
+ }
837
+ return vars;
838
+ }
839
+ function resolveWorkspacePatterns(patterns, availableFiles) {
840
+ const dirs = /* @__PURE__ */ new Set();
841
+ for (const pattern of patterns) {
842
+ const hasGlob = /\/?\*\*?$/.test(pattern);
843
+ const base = pattern.replace(/\/?\*\*?$/, "");
844
+ if (hasGlob) {
845
+ for (const filePath of availableFiles) if (filePath.startsWith(base + "/") && filePath.endsWith("/package.json")) {
846
+ const parts = filePath.slice(base.length + 1).split("/");
847
+ if (parts.length === 2 && parts[1] === "package.json") dirs.add(base + "/" + parts[0]);
848
+ }
849
+ } else {
850
+ const pkgPath = base + "/package.json";
851
+ if (availableFiles.includes(pkgPath)) dirs.add(base);
852
+ }
853
+ }
854
+ return [...dirs];
855
+ }
856
+ function detectMonorepo(files, pm = "npm") {
857
+ const rootPkg = safeParse(files["package.json"]);
858
+ const availableFiles = Object.keys(files);
859
+ let workspacePatterns = [];
860
+ if (rootPkg?.workspaces) {
861
+ const ws = rootPkg.workspaces;
862
+ if (Array.isArray(ws)) workspacePatterns = ws;
863
+ else if (typeof ws === "object" && ws !== null && "packages" in ws && Array.isArray(ws.packages)) workspacePatterns = ws.packages;
864
+ }
865
+ if (workspacePatterns.length === 0 && files["pnpm-workspace.yaml"]) {
866
+ const lines = files["pnpm-workspace.yaml"].split("\n");
867
+ let inPackages = false;
868
+ for (const line of lines) {
869
+ if (line.match(/^packages\s*:/)) {
870
+ inPackages = true;
871
+ continue;
872
+ }
873
+ if (inPackages) {
874
+ const match = line.match(/^\s+-\s+['"]?([^'"]+)['"]?\s*$/);
875
+ if (match) workspacePatterns.push(match[1]);
876
+ else if (line.match(/^\S/)) break;
877
+ }
878
+ }
879
+ }
880
+ if (workspacePatterns.length === 0) return {
881
+ isMonorepo: false,
882
+ apps: []
883
+ };
884
+ const dirs = resolveWorkspacePatterns(workspacePatterns, availableFiles);
885
+ const rootScripts = rootPkg?.scripts || {};
886
+ const apps = [];
887
+ for (const dir of dirs) {
888
+ const pkgContent = files[`${dir}/package.json`];
889
+ const pkg = safeParse(pkgContent);
890
+ const name = pkg?.name ?? dir.split("/").pop() ?? dir;
891
+ let framework = detectFramework(pkgContent, pm);
892
+ if (framework && rootScripts) {
893
+ const serviceName = dir.split("/").pop() ?? name;
894
+ const buildKey = [
895
+ `build:${serviceName}`,
896
+ `build-${serviceName}`,
897
+ `${serviceName}:build`
898
+ ].find((k) => rootScripts[k]);
899
+ if (buildKey) framework.buildCommand = `${pm} run ${buildKey}`;
900
+ const startKey = [
901
+ `start:${serviceName}`,
902
+ `start-${serviceName}`,
903
+ `${serviceName}:start`,
904
+ `dev:${serviceName}`,
905
+ `serve:${serviceName}`
906
+ ].find((k) => rootScripts[k]);
907
+ if (startKey) framework.startCommand = `${pm} run ${startKey}`;
908
+ if (framework.name === "nextjs" && !startKey) framework.startCommand = `cd ${dir} && ${pm} run start`;
909
+ else if ([
910
+ "hono",
911
+ "express",
912
+ "fastify",
913
+ "nestjs"
914
+ ].includes(framework.name)) {
915
+ if (!startKey && framework.buildCommand) {
916
+ const scripts = pkg?.scripts || {};
917
+ if (scripts.build && scripts.build.includes("tsc")) framework.startCommand = `node ${dir}/dist/index.js`;
918
+ else if (scripts.build && scripts.build.includes("tsup")) framework.startCommand = `node ${dir}/dist/index.js`;
919
+ else if (scripts.build && scripts.build.includes("esbuild")) framework.startCommand = `node ${dir}/dist/index.js`;
920
+ }
921
+ }
922
+ }
923
+ apps.push({
924
+ name,
925
+ path: dir,
926
+ framework
927
+ });
928
+ }
929
+ return {
930
+ isMonorepo: apps.length > 0,
931
+ apps
932
+ };
933
+ }
934
+ function analyzeRepo(files) {
935
+ const pm = detectPackageManager$1(files);
936
+ const framework = detectFramework(files["package.json"], pm);
937
+ const envVars = detectEnvVars(files);
938
+ const { isMonorepo, apps } = detectMonorepo(files, pm);
939
+ return {
940
+ framework,
941
+ packageManager: pm,
942
+ envVars,
943
+ isMonorepo,
944
+ monorepoApps: apps
945
+ };
946
+ }
947
+
948
+ //#endregion
949
+ //#region src/lib/upload.ts
950
+ const DEFAULT_IGNORE = [
951
+ ".git",
952
+ "node_modules",
953
+ ".env",
954
+ ".env.local",
955
+ ".env.*.local",
956
+ "dist",
957
+ "build",
958
+ ".next",
959
+ ".nuxt",
960
+ ".output",
961
+ "*.log",
962
+ ".DS_Store",
963
+ "Thumbs.db",
964
+ ".idea",
965
+ ".vscode",
966
+ ".claude",
967
+ ".cursor",
968
+ ".sisyphus",
969
+ ".playwright-mcp",
970
+ "*.swp",
971
+ "*.swo"
972
+ ];
973
+ async function getIgnorePatterns(directory) {
974
+ const patterns = [...DEFAULT_IGNORE];
975
+ try {
976
+ const gitignoreContent = readFileSync(join(directory, ".gitignore"), "utf-8");
977
+ patterns.push(...gitignoreContent.split("\n").filter((line) => line.trim() && !line.startsWith("#")));
978
+ } catch {}
979
+ try {
980
+ const velozignoreContent = readFileSync(join(directory, ".velozignore"), "utf-8");
981
+ patterns.push(...velozignoreContent.split("\n").filter((line) => line.trim() && !line.startsWith("#")));
982
+ } catch {}
983
+ return patterns;
984
+ }
985
+ async function getFilesToUpload(directory) {
986
+ const ignorePatterns = await getIgnorePatterns(directory);
987
+ const ig = ignore().add(ignorePatterns);
988
+ const files = [];
989
+ async function walk(dir) {
990
+ const entries = await readdir(dir, { withFileTypes: true });
991
+ for (const entry of entries) {
992
+ const fullPath = join(dir, entry.name);
993
+ const relativePath = relative(directory, fullPath);
994
+ if (ig.ignores(relativePath)) continue;
995
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
996
+ if (entry.isDirectory()) await walk(fullPath);
997
+ else if (entry.isFile()) files.push(fullPath);
998
+ }
999
+ }
1000
+ await walk(directory);
1001
+ return files;
1002
+ }
1003
+ async function createTarball(directory, extraFiles) {
1004
+ const createdFiles = [];
1005
+ if (extraFiles) for (const file of extraFiles) {
1006
+ const filePath = join(directory, file.name);
1007
+ if (!existsSync(filePath)) {
1008
+ writeFileSync(filePath, file.content);
1009
+ createdFiles.push(filePath);
1010
+ }
1011
+ }
1012
+ try {
1013
+ const files = await getFilesToUpload(directory);
1014
+ if (files.length === 0) throw new Error("No files to upload");
1015
+ const tarPath = join(await mkdtemp(join(tmpdir(), "veloz-upload-")), "source.tar.gz");
1016
+ const relativePaths = files.map((f) => relative(directory, f));
1017
+ for (const f of createdFiles) {
1018
+ const rel = relative(directory, f);
1019
+ if (!relativePaths.includes(rel)) relativePaths.push(rel);
1020
+ }
1021
+ await tar.create({
1022
+ gzip: true,
1023
+ file: tarPath,
1024
+ cwd: directory
1025
+ }, relativePaths);
1026
+ return {
1027
+ path: tarPath,
1028
+ size: statSync(tarPath).size
1029
+ };
1030
+ } finally {
1031
+ for (const f of createdFiles) try {
1032
+ unlinkSync(f);
1033
+ } catch {}
1034
+ }
1035
+ }
1036
+ async function uploadSource(apiUrl, deploymentId, directory, token, extraFiles) {
1037
+ const { path: tarPath } = await createTarball(directory, extraFiles);
1038
+ try {
1039
+ const urlResponse = await fetch(`${apiUrl}/api/deployments/${deploymentId}/upload-url`, {
1040
+ method: "POST",
1041
+ headers: {
1042
+ Authorization: `Bearer ${token}`,
1043
+ "Content-Type": "application/json"
1044
+ }
1045
+ });
1046
+ if (!urlResponse.ok) {
1047
+ const err = await urlResponse.json().catch(() => ({ error: "Unknown error" }));
1048
+ throw new Error(`Failed to get upload URL: ${err.error || urlResponse.statusText}`);
1049
+ }
1050
+ const { uploadUrl, objectKey } = await urlResponse.json();
1051
+ const fileBuffer = readFileSync(tarPath);
1052
+ const putResponse = await fetch(uploadUrl, {
1053
+ method: "PUT",
1054
+ headers: { "Content-Type": "application/gzip" },
1055
+ body: fileBuffer
1056
+ });
1057
+ if (!putResponse.ok) throw new Error(`S3 upload failed: ${putResponse.status} ${putResponse.statusText}`);
1058
+ const buildResponse = await fetch(`${apiUrl}/api/deployments/${deploymentId}/build`, {
1059
+ method: "POST",
1060
+ headers: {
1061
+ Authorization: `Bearer ${token}`,
1062
+ "Content-Type": "application/json"
1063
+ },
1064
+ body: JSON.stringify({ objectKey })
1065
+ });
1066
+ if (!buildResponse.ok) {
1067
+ const err = await buildResponse.json().catch(() => ({ error: "Unknown error" }));
1068
+ throw new Error(`Failed to trigger build: ${err.error || buildResponse.statusText}`);
1069
+ }
1070
+ } finally {
1071
+ await rm(tarPath, {
1072
+ force: true,
1073
+ recursive: true
1074
+ }).catch(() => {});
1075
+ }
1076
+ }
1077
+ async function calculateDirectorySize(directory) {
1078
+ const files = await getFilesToUpload(directory);
1079
+ let totalSize = 0;
1080
+ for (const file of files) {
1081
+ const stats = await stat(file);
1082
+ totalSize += stats.size;
1083
+ }
1084
+ return totalSize;
1085
+ }
1086
+
1087
+ //#endregion
1088
+ //#region src/lib/deploy-stream.ts
1089
+ const statusLabels$1 = {
1090
+ QUEUED: "Na fila",
1091
+ BUILDING: "Construindo",
1092
+ BUILD_FAILED: "Falha na construção",
1093
+ DEPLOYING: "Implantando",
1094
+ LIVE: "Ativo",
1095
+ FAILED: "Falhou",
1096
+ CANCELLED: "Cancelado"
1097
+ };
1098
+ const TERMINAL_STATUSES$1 = new Set([
1099
+ "LIVE",
1100
+ "BUILD_FAILED",
1101
+ "FAILED",
1102
+ "CANCELLED"
1103
+ ]);
1104
+ async function streamDeploymentLogs(client, deploymentId, _serviceId, serviceName) {
1105
+ const isVerbose = process.env.VELOZ_VERBOSE === "true";
1106
+ const logHeader = serviceName ? `📦 Build logs para ${chalk.bold(serviceName)}:` : "📦 Build logs:";
1107
+ console.log(chalk.cyan(`\n${logHeader}`));
1108
+ console.log(chalk.dim("─".repeat(60)));
1109
+ if (isVerbose) console.log(chalk.dim(`[verbose] Conectando ao stream de logs para deployment ${deploymentId}...`));
1110
+ let finalStatus = "";
1111
+ let logsReceived = false;
1112
+ try {
1113
+ const stream = await client.logs.streamBuildLogs({ deploymentId });
1114
+ for await (const event of stream) {
1115
+ logsReceived = true;
1116
+ if (event.type === "status") {
1117
+ const label = statusLabels$1[event.content] ?? event.content;
1118
+ const icon = event.content === "LIVE" ? chalk.green("●") : event.content === "BUILD_FAILED" || event.content === "FAILED" ? chalk.red("●") : chalk.yellow("●");
1119
+ process.stdout.write(`\n${icon} ${chalk.bold(label)}\n`);
1120
+ finalStatus = event.content;
1121
+ if (isVerbose) console.log(chalk.dim(`[verbose] Status mudou para: ${event.content}`));
1122
+ } else if (event.type === "log") process.stdout.write(event.content);
1123
+ }
1124
+ if (!logsReceived && isVerbose) console.log(chalk.yellow("[verbose] Nenhum log recebido do stream. Buscando status do deployment..."));
1125
+ } catch (error) {
1126
+ if (isVerbose) console.log(chalk.red(`[verbose] Erro no stream: ${error.message}`));
1127
+ try {
1128
+ const d = await client.deployments.get({ deploymentId });
1129
+ finalStatus = d.status;
1130
+ if (isVerbose) console.log(chalk.dim(`[verbose] Status do deployment: ${d.status}`));
1131
+ try {
1132
+ const logs = await client.logs.getBuildLogs({ deploymentId });
1133
+ if (logs.buildLogs) {
1134
+ console.log(chalk.dim("\n── Logs salvos ──\n"));
1135
+ process.stdout.write(logs.buildLogs);
1136
+ } else if (isVerbose) console.log(chalk.yellow("[verbose] Nenhum log salvo encontrado para este deployment"));
1137
+ } catch {
1138
+ if (isVerbose) console.log(chalk.yellow("[verbose] Não foi possível buscar logs de build"));
1139
+ }
1140
+ } catch (fetchError) {
1141
+ if (isVerbose) console.log(chalk.red(`[verbose] Erro ao buscar deployment: ${fetchError.message}`));
1142
+ }
1143
+ }
1144
+ console.log(chalk.dim("\n" + "─".repeat(60)));
1145
+ if (finalStatus === "LIVE") success(serviceName ? `Deploy de ${chalk.bold(serviceName)} concluído! Serviço está ativo.` : "Deploy concluído! Serviço está ativo.");
1146
+ else if (TERMINAL_STATUSES$1.has(finalStatus)) {
1147
+ const errorMsg = serviceName ? `✗ Deploy de ${chalk.bold(serviceName)} finalizou com status: ${statusLabels$1[finalStatus] ?? finalStatus}` : `✗ Deploy finalizou com status: ${statusLabels$1[finalStatus] ?? finalStatus}`;
1148
+ console.error(chalk.red(errorMsg));
1149
+ if (!serviceName) process.exit(1);
1150
+ }
1151
+ }
1152
+
1153
+ //#endregion
1154
+ //#region src/lib/deploy-parallel.ts
1155
+ async function withRetry$1(fn, maxRetries = 3) {
1156
+ for (let attempt = 0; attempt <= maxRetries; attempt++) try {
1157
+ return await fn();
1158
+ } catch (error) {
1159
+ if (attempt < maxRetries) {
1160
+ const delay = Math.min(1e3 * Math.pow(2, attempt), 1e4);
1161
+ await new Promise((resolve$1) => setTimeout(resolve$1, delay));
1162
+ continue;
1163
+ }
1164
+ throw error;
1165
+ }
1166
+ throw new Error("Max retries exceeded");
1167
+ }
1168
+ const statusLabels = {
1169
+ QUEUED: "Na fila",
1170
+ BUILDING: "Construindo",
1171
+ BUILD_FAILED: "Falha na construção",
1172
+ DEPLOYING: "Implantando",
1173
+ LIVE: "Ativo",
1174
+ FAILED: "Falhou",
1175
+ CANCELLED: "Cancelado"
1176
+ };
1177
+ const statusIcons = {
1178
+ QUEUED: chalk.gray("○"),
1179
+ BUILDING: chalk.yellow("●"),
1180
+ DEPLOYING: chalk.blue("●"),
1181
+ LIVE: chalk.green("●"),
1182
+ BUILD_FAILED: chalk.red("●"),
1183
+ FAILED: chalk.red("●"),
1184
+ CANCELLED: chalk.gray("●")
1185
+ };
1186
+ const TERMINAL_STATUSES = new Set([
1187
+ "LIVE",
1188
+ "BUILD_FAILED",
1189
+ "FAILED",
1190
+ "CANCELLED"
1191
+ ]);
1192
+ function renderProgress(progressMap, prevLineCount) {
1193
+ for (let i = 0; i < prevLineCount; i++) process.stdout.write("\x1B[1A\x1B[2K");
1194
+ let lineCount = 0;
1195
+ for (const [, progress] of progressMap) {
1196
+ const icon = statusIcons[progress.status] || chalk.gray("○");
1197
+ const label = statusLabels[progress.status] || progress.status;
1198
+ process.stdout.write(`${icon} ${chalk.bold(progress.serviceName)}: ${label}\n`);
1199
+ lineCount++;
1200
+ if (progress.status === "BUILDING" || progress.status === "BUILD_FAILED") {
1201
+ if (progress.logLines.length > 0) {
1202
+ const tail = progress.logLines.slice(-3);
1203
+ for (const line of tail) {
1204
+ const truncated = line.substring(0, 80) + (line.length > 80 ? "..." : "");
1205
+ process.stdout.write(` ${chalk.dim(truncated)}\n`);
1206
+ lineCount++;
1207
+ }
1208
+ } else if (progress.status === "BUILDING") {
1209
+ process.stdout.write(` ${chalk.dim("⏳ Aguardando logs do build...")}\n`);
1210
+ lineCount++;
1211
+ }
1212
+ } else if (progress.status === "QUEUED") {
1213
+ process.stdout.write(` ${chalk.dim("📋 Na fila para construção...")}\n`);
1214
+ lineCount++;
1215
+ }
1216
+ }
1217
+ return lineCount;
1218
+ }
1219
+ async function deployServicesInParallel(client, services) {
1220
+ console.log(chalk.cyan("\n🚀 Iniciando deploy paralelo de múltiplos serviços...\n"));
1221
+ const progressMap = /* @__PURE__ */ new Map();
1222
+ const projectRoot = process.cwd();
1223
+ const sizeInBytes = await calculateDirectorySize(projectRoot);
1224
+ const sizeMB = Math.round(sizeInBytes / (1024 * 1024) * 10) / 10;
1225
+ const deploymentPromises = services.map(async (service) => {
1226
+ try {
1227
+ const deployment = await withRetry$1(() => client.deployments.create({ serviceId: service.serviceId }));
1228
+ const authConfig = await requireAuth();
1229
+ await withRetry$1(() => uploadSource(authConfig.apiUrl, deployment.id, projectRoot, authConfig.apiKey, service.extraFiles));
1230
+ progressMap.set(service.serviceId, {
1231
+ serviceName: service.serviceName,
1232
+ deploymentId: deployment.id,
1233
+ status: "QUEUED",
1234
+ logLines: [],
1235
+ completed: false,
1236
+ success: false
1237
+ });
1238
+ console.log(`${chalk.green("✓")} ${chalk.bold(service.serviceName)}: Upload concluído (${sizeMB} MB)`);
1239
+ return {
1240
+ service,
1241
+ deploymentId: deployment.id
1242
+ };
1243
+ } catch (error) {
1244
+ console.log(`${chalk.red("✗")} ${chalk.bold(service.serviceName)}: Falha ao iniciar deploy`);
1245
+ progressMap.set(service.serviceId, {
1246
+ serviceName: service.serviceName,
1247
+ deploymentId: "",
1248
+ status: "FAILED",
1249
+ logLines: [],
1250
+ completed: true,
1251
+ success: false
1252
+ });
1253
+ throw error;
1254
+ }
1255
+ });
1256
+ const activeDeployments = (await Promise.allSettled(deploymentPromises)).filter((d) => d.status === "fulfilled").map((d) => d.value);
1257
+ if (activeDeployments.length === 0) {
1258
+ console.error(chalk.red("\n✗ Todos os deploys falharam ao iniciar."));
1259
+ return;
1260
+ }
1261
+ console.log(chalk.cyan("\n📦 Monitorando progresso dos deploys:\n"));
1262
+ console.log(chalk.dim("─".repeat(60)) + "\n");
1263
+ let lineCount = 0;
1264
+ lineCount = renderProgress(progressMap, lineCount);
1265
+ const isVerbose = process.env.VELOZ_VERBOSE === "true";
1266
+ const streamPromises = activeDeployments.map(async ({ service, deploymentId }) => {
1267
+ try {
1268
+ await new Promise((resolve$1) => setTimeout(resolve$1, 1e3));
1269
+ if (isVerbose) console.log(chalk.dim(`\n[verbose] Conectando ao stream para ${service.serviceName} (${deploymentId})...`));
1270
+ const stream = await client.logs.streamBuildLogs({ deploymentId });
1271
+ for await (const event of stream) {
1272
+ const progress = progressMap.get(service.serviceId);
1273
+ if (!progress) continue;
1274
+ if (event.type === "status") {
1275
+ progress.status = event.content;
1276
+ if (TERMINAL_STATUSES.has(event.content)) {
1277
+ progress.completed = true;
1278
+ progress.success = event.content === "LIVE";
1279
+ }
1280
+ if (isVerbose) console.log(chalk.dim(`\n[verbose] ${service.serviceName}: status mudou para ${event.content}`));
1281
+ } else if (event.type === "log") {
1282
+ const newLines = event.content.split("\n").filter((l) => l.trim());
1283
+ progress.logLines.push(...newLines);
1284
+ }
1285
+ lineCount = renderProgress(progressMap, lineCount);
1286
+ }
1287
+ } catch (error) {
1288
+ const progress = progressMap.get(service.serviceId);
1289
+ if (progress && !progress.completed) {
1290
+ if (isVerbose) console.error(`\n${chalk.red("[verbose]")} Error streaming logs for ${service.serviceName}:`, error.message);
1291
+ else console.error(`\n${chalk.red("✗")} Error streaming logs for ${service.serviceName}`);
1292
+ progress.status = "FAILED";
1293
+ progress.completed = true;
1294
+ lineCount = renderProgress(progressMap, lineCount);
1295
+ }
1296
+ }
1297
+ });
1298
+ await Promise.all(streamPromises);
1299
+ renderProgress(progressMap, lineCount);
1300
+ console.log(chalk.dim("\n" + "─".repeat(60)));
1301
+ const successful = Array.from(progressMap.values()).filter((p) => p.success);
1302
+ const failed = Array.from(progressMap.values()).filter((p) => !p.success);
1303
+ if (successful.length > 0) {
1304
+ console.log(chalk.green(`\n✅ ${successful.length} serviço(s) implantado(s) com sucesso:\n`));
1305
+ for (const progress of successful) console.log(` ${chalk.green("✓")} ${chalk.bold(progress.serviceName)}`);
1306
+ }
1307
+ if (failed.length > 0) {
1308
+ console.log(chalk.red(`\n✗ ${failed.length} serviço(s) falhou(aram):\n`));
1309
+ for (const progress of failed) {
1310
+ console.log(` ${chalk.red("✗")} ${chalk.bold(progress.serviceName)}`);
1311
+ if (progress.logLines.length > 0) {
1312
+ console.log(chalk.dim("─".repeat(40)));
1313
+ const tail = progress.logLines.slice(-50);
1314
+ for (const line of tail) console.log(` ${chalk.dim(line)}`);
1315
+ console.log(chalk.dim("─".repeat(40)));
1316
+ }
1317
+ }
1318
+ }
1319
+ if (successful.length > 0) info("\nUse 'veloz logs -f' para acompanhar os logs de execução.");
1320
+ }
1321
+
1322
+ //#endregion
1323
+ //#region src/lib/dockerfile-generator.ts
1324
+ function pmSetupInstructions(pm) {
1325
+ switch (pm) {
1326
+ case "pnpm": return "RUN corepack enable && corepack prepare pnpm@latest --activate";
1327
+ case "yarn": return "RUN corepack enable";
1328
+ 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";
1329
+ case "npm": return "";
1330
+ }
1331
+ }
1332
+ function installCommand(pm) {
1333
+ switch (pm) {
1334
+ case "bun": return "bun install --frozen-lockfile";
1335
+ case "pnpm": return "pnpm install --frozen-lockfile";
1336
+ case "yarn": return "yarn install --frozen-lockfile";
1337
+ case "npm": return "npm ci";
1338
+ }
1339
+ }
1340
+ function lockfileNames(pm) {
1341
+ switch (pm) {
1342
+ case "bun": return ["bun.lockb", "bun.lock"];
1343
+ case "pnpm": return ["pnpm-lock.yaml"];
1344
+ case "yarn": return ["yarn.lock"];
1345
+ case "npm": return ["package-lock.json"];
1346
+ }
1347
+ }
1348
+ function generateWebDockerfile(opts) {
1349
+ const { nodeVersion, pm, buildCommand, startCommand, rootDirectory, port = 3e3 } = opts;
1350
+ const setup = pmSetupInstructions(pm);
1351
+ const lockfiles = lockfileNames(pm);
1352
+ const hasRoot = !!(rootDirectory && rootDirectory !== "/");
1353
+ const cleanRoot = hasRoot ? rootDirectory.replace(/^\//, "") : "";
1354
+ const depsCopy = hasRoot ? [
1355
+ `# Root-level dependency files`,
1356
+ `COPY package.json ${lockfiles.join(" ")} ./`,
1357
+ `# Service-level dependency files`,
1358
+ `COPY ${cleanRoot}/package.json ./${cleanRoot}/`
1359
+ ] : [`COPY package.json ${lockfiles.join(" ")} ./`];
1360
+ const workdirPrefix = hasRoot ? `cd ${cleanRoot} && ` : "";
1361
+ const defaultStart = pm === "bun" ? "bun start" : `${pm} run start`;
1362
+ const finalStart = startCommand || defaultStart;
1363
+ return `# ── Stage 1: Install & Build ────────────────────────────
1364
+ FROM node:${nodeVersion}-alpine AS builder
1365
+
1366
+ WORKDIR /app
1367
+ ${setup ? "\n" + setup + "\n" : ""}
1368
+ # Install dependencies (cached layer)
1369
+ ${depsCopy.join("\n")}
1370
+ RUN ${installCommand(pm)}
1371
+
1372
+ # Copy full source
1373
+ COPY . .
1374
+
1375
+ # Build with env vars from BuildKit secret mount
1376
+ RUN --mount=type=secret,id=build-env \\
1377
+ set -a && \\
1378
+ if [ -f /run/secrets/build-env ]; then . /run/secrets/build-env; fi && \\
1379
+ set +a && \\
1380
+ ${workdirPrefix}${buildCommand}
1381
+
1382
+ # ── Stage 2: Production runner ─────────────────────────
1383
+ FROM node:${nodeVersion}-alpine
1384
+
1385
+ WORKDIR /app
1386
+
1387
+ # Copy built app (node_modules + build output)
1388
+ COPY --from=builder /app .
1389
+
1390
+ ENV NODE_ENV=production
1391
+ ENV PORT=${port}
1392
+ EXPOSE ${port}
1393
+
1394
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \\
1395
+ CMD wget --quiet --tries=1 --spider http://localhost:${port}/ || exit 1
1396
+
1397
+ CMD ${workdirPrefix ? `["sh", "-c", "${workdirPrefix}${finalStart}"]` : JSON.stringify(finalStart.split(" "))}
1398
+ `;
1399
+ }
1400
+ function generateStaticDockerfile(opts) {
1401
+ const { nodeVersion, pm, buildCommand, outputDir, rootDirectory } = opts;
1402
+ const setup = pmSetupInstructions(pm);
1403
+ const lockfiles = lockfileNames(pm);
1404
+ const hasRoot = !!(rootDirectory && rootDirectory !== "/");
1405
+ const cleanRoot = hasRoot ? rootDirectory.replace(/^\//, "") : "";
1406
+ const servicePrefix = hasRoot ? cleanRoot + "/" : "";
1407
+ const depsCopy = hasRoot ? [`COPY package.json ${lockfiles.join(" ")} ./`, `COPY ${cleanRoot}/package.json ./${cleanRoot}/`] : [`COPY package.json ${lockfiles.join(" ")} ./`];
1408
+ const workdirPrefix = hasRoot ? `cd ${cleanRoot} && ` : "";
1409
+ const outputDetection = outputDir ? `ENV OUTPUT_DIR="${outputDir}"` : [
1410
+ `# Auto-detect output directory`,
1411
+ `RUN if [ -d "${servicePrefix}dist" ]; then echo "${servicePrefix}dist" > /tmp/output-dir; \\`,
1412
+ ` elif [ -d "${servicePrefix}build" ]; then echo "${servicePrefix}build" > /tmp/output-dir; \\`,
1413
+ ` elif [ -d "${servicePrefix}out" ]; then echo "${servicePrefix}out" > /tmp/output-dir; \\`,
1414
+ ` elif [ -d "${servicePrefix}.next/out" ]; then echo "${servicePrefix}.next/out" > /tmp/output-dir; \\`,
1415
+ ` elif [ -d "${servicePrefix}public" ]; then echo "${servicePrefix}public" > /tmp/output-dir; \\`,
1416
+ ` else echo "${servicePrefix}dist" > /tmp/output-dir; fi`
1417
+ ].join("\n");
1418
+ const outputDirRef = outputDir ? servicePrefix + outputDir : "$(cat /tmp/output-dir)";
1419
+ return `# ── Stage 1: Build ──────────────────────────────────────
1420
+ FROM node:${nodeVersion}-alpine AS builder
1421
+
1422
+ WORKDIR /app
1423
+ ${setup ? "\n" + setup + "\n" : ""}
1424
+ # Install dependencies (cached layer)
1425
+ ${depsCopy.join("\n")}
1426
+ RUN ${installCommand(pm)}
1427
+
1428
+ # Copy full source
1429
+ COPY . .
1430
+
1431
+ # Build with env vars from BuildKit secret mount
1432
+ RUN --mount=type=secret,id=build-env \\
1433
+ set -a && \\
1434
+ if [ -f /run/secrets/build-env ]; then . /run/secrets/build-env; fi && \\
1435
+ set +a && \\
1436
+ ${workdirPrefix}${buildCommand}
1437
+
1438
+ # Detect output directory
1439
+ ${outputDetection}
1440
+
1441
+ # Process _headers and _redirects into nginx config
1442
+ COPY nginx.conf /tmp/nginx-base.conf
1443
+ COPY generate-nginx-config.cjs /tmp/generate-nginx-config.cjs
1444
+ RUN node /tmp/generate-nginx-config.cjs \\
1445
+ --base /tmp/nginx-base.conf \\
1446
+ --output-dir /app/${outputDirRef} \\
1447
+ --out /tmp/nginx-final.conf
1448
+
1449
+ # Copy build output to a known location
1450
+ RUN cp -r /app/${outputDirRef} /output
1451
+
1452
+ # ── Stage 2: Serve ─────────────────────────────────────
1453
+ FROM nginx:alpine
1454
+
1455
+ # Remove default nginx site
1456
+ RUN rm -f /etc/nginx/conf.d/default.conf
1457
+
1458
+ # Copy processed nginx config and static files
1459
+ COPY --from=builder /tmp/nginx-final.conf /etc/nginx/conf.d/default.conf
1460
+ COPY --from=builder /output /usr/share/nginx/html
1461
+
1462
+ EXPOSE 80
1463
+
1464
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
1465
+ CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1
1466
+
1467
+ CMD ["nginx", "-g", "daemon off;"]
1468
+ `;
1469
+ }
1470
+ function generateDockerfile(opts) {
1471
+ if (opts.serviceType === "STATIC") return generateStaticDockerfile(opts);
1472
+ return generateWebDockerfile(opts);
1473
+ }
1474
+
1475
+ //#endregion
1476
+ //#region src/lib/templates.ts
1477
+ /**
1478
+ * Embedded templates for STATIC site builds.
1479
+ * These are written to the project directory temporarily before creating the tar,
1480
+ * then cleaned up immediately after.
1481
+ */
1482
+ const NGINX_CONF = `server {
1483
+ listen 80;
1484
+ server_name _;
1485
+ root /usr/share/nginx/html;
1486
+ index index.html;
1487
+
1488
+ # SPA routing - fallback to index.html for client-side routing
1489
+ location / {
1490
+ try_files \\$uri \\$uri/ /index.html;
1491
+ }
1492
+
1493
+ # Gzip compression
1494
+ gzip on;
1495
+ gzip_vary on;
1496
+ gzip_min_length 256;
1497
+ gzip_types
1498
+ text/plain
1499
+ text/css
1500
+ text/xml
1501
+ text/javascript
1502
+ application/json
1503
+ application/javascript
1504
+ application/xml+rss
1505
+ application/rss+xml
1506
+ application/atom+xml
1507
+ image/svg+xml
1508
+ text/x-component
1509
+ text/x-cross-domain-policy;
1510
+
1511
+ # Cache hashed assets (fingerprinted files)
1512
+ location ~* \\\\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|otf|webp|avif)\\$ {
1513
+ expires 1y;
1514
+ add_header Cache-Control "public, immutable";
1515
+ access_log off;
1516
+ }
1517
+
1518
+ # No cache for HTML files
1519
+ location ~* \\\\.html\\$ {
1520
+ expires -1;
1521
+ add_header Cache-Control "no-cache, no-store, must-revalidate";
1522
+ add_header Pragma "no-cache";
1523
+ }
1524
+
1525
+ # Security headers
1526
+ add_header X-Frame-Options "SAMEORIGIN" always;
1527
+ add_header X-Content-Type-Options "nosniff" always;
1528
+ add_header X-XSS-Protection "1; mode=block" always;
1529
+
1530
+ # Custom headers placeholder (will be replaced during build)
1531
+ # CUSTOM_HEADERS_PLACEHOLDER
1532
+
1533
+ # Custom redirects placeholder (will be replaced during build)
1534
+ # CUSTOM_REDIRECTS_PLACEHOLDER
1535
+ }`;
1536
+ const GENERATE_NGINX_CONFIG_CJS = `#!/usr/bin/env node
1537
+ "use strict";
1538
+
1539
+ /**
1540
+ * Generates a final nginx.conf by processing Netlify-style _headers and _redirects
1541
+ * files from the static site build output. Runs inside the Docker build stage.
1542
+ *
1543
+ * Usage: node generate-nginx-config.cjs --base <nginx.conf> --output-dir <dir> --out <dest>
1544
+ */
1545
+
1546
+ const fs = require("fs");
1547
+ const path = require("path");
1548
+
1549
+ // ── Parse CLI args ─────────────────────────────────────
1550
+
1551
+ const args = process.argv.slice(2);
1552
+ let baseConfig = "";
1553
+ let outputDir = "";
1554
+ let outFile = "";
1555
+
1556
+ for (let i = 0; i < args.length; i++) {
1557
+ if (args[i] === "--base" && args[i + 1]) baseConfig = args[++i];
1558
+ if (args[i] === "--output-dir" && args[i + 1]) outputDir = args[++i];
1559
+ if (args[i] === "--out" && args[i + 1]) outFile = args[++i];
1560
+ }
1561
+
1562
+ if (!baseConfig || !outputDir || !outFile) {
1563
+ console.error(
1564
+ "Usage: node generate-nginx-config.cjs --base <nginx.conf> --output-dir <dir> --out <dest>"
1565
+ );
1566
+ process.exit(1);
1567
+ }
1568
+
1569
+ let nginxConfig = fs.readFileSync(baseConfig, "utf-8");
1570
+
1571
+ // ── Parse _headers (Netlify-style) ─────────────────────
1572
+ //
1573
+ // Format:
1574
+ // /api/*
1575
+ // Access-Control-Allow-Origin: *
1576
+ // /*
1577
+ // X-Frame-Options: DENY
1578
+
1579
+ const headersPath = path.join(outputDir, "_headers");
1580
+ if (fs.existsSync(headersPath)) {
1581
+ const content = fs.readFileSync(headersPath, "utf-8");
1582
+ let headerDirectives = "";
1583
+ let currentPath = "";
1584
+ let inLocationBlock = false;
1585
+
1586
+ for (const line of content.split("\\n")) {
1587
+ const trimmed = line.trim();
1588
+ if (!trimmed || trimmed.startsWith("#")) continue;
1589
+
1590
+ // Path pattern (starts without whitespace)
1591
+ if (!line.startsWith(" ") && !line.startsWith("\\t")) {
1592
+ // Close previous location block if open
1593
+ if (inLocationBlock) {
1594
+ headerDirectives += " }\\n";
1595
+ inLocationBlock = false;
1596
+ }
1597
+ currentPath = trimmed;
1598
+ // Global headers (/*) go at server level, others get location blocks
1599
+ if (currentPath !== "/*") {
1600
+ headerDirectives += \\\`\\n location \\\${currentPath} {\\n\\\`;
1601
+ inLocationBlock = true;
1602
+ }
1603
+ } else {
1604
+ // Header line (indented)
1605
+ const colonIdx = trimmed.indexOf(":");
1606
+ if (colonIdx === -1) continue;
1607
+ const key = trimmed.slice(0, colonIdx).trim();
1608
+ const value = trimmed.slice(colonIdx + 1).trim();
1609
+ if (key && value) {
1610
+ if (currentPath === "/*") {
1611
+ headerDirectives += \\\` add_header \\\${key} "\\\${value}" always;\\n\\\`;
1612
+ } else {
1613
+ headerDirectives += \\\` add_header \\\${key} "\\\${value}" always;\\n\\\`;
1614
+ }
1615
+ }
1616
+ }
1617
+ }
1618
+
1619
+ // Close final location block
1620
+ if (inLocationBlock) {
1621
+ headerDirectives += " }\\n";
1622
+ }
1623
+
1624
+ nginxConfig = nginxConfig.replace(
1625
+ "# CUSTOM_HEADERS_PLACEHOLDER",
1626
+ headerDirectives
1627
+ );
1628
+ console.log("Processed _headers file");
1629
+ }
1630
+
1631
+ // ── Parse _redirects (Netlify-style) ───────────────────
1632
+ //
1633
+ // Format:
1634
+ // /old-path /new-path 301
1635
+ // /api/* https://api.example.com/:splat 200
1636
+
1637
+ const redirectsPath = path.join(outputDir, "_redirects");
1638
+ if (fs.existsSync(redirectsPath)) {
1639
+ const content = fs.readFileSync(redirectsPath, "utf-8");
1640
+ let redirectDirectives = "";
1641
+
1642
+ for (const line of content.split("\\n")) {
1643
+ const trimmed = line.trim();
1644
+ if (!trimmed || trimmed.startsWith("#")) continue;
1645
+
1646
+ const parts = trimmed.split(/\\s+/);
1647
+ if (parts.length < 2) continue;
1648
+
1649
+ const from = parts[0];
1650
+ const to = parts[1];
1651
+ const code = parts[2] || "301";
1652
+
1653
+ if (!from || !to) continue;
1654
+
1655
+ if (!from.includes("*") && !to.includes(":splat")) {
1656
+ // Simple redirect
1657
+ const nginxFlag = code === "301" ? "permanent" : "redirect";
1658
+ redirectDirectives += \\\` rewrite ^\\\${from}\\\\$ \\\${to} \\\${nginxFlag};\\n\\\`;
1659
+ } else {
1660
+ // Wildcard redirect
1661
+ const fromPattern = from.replace(/\\*/g, "(.*)");
1662
+ const toPattern = to.replace(/:splat/g, "\\$1");
1663
+ const nginxFlag = code === "301" ? "permanent" : "redirect";
1664
+ redirectDirectives += \\\` rewrite ^\\\${fromPattern}\\\\$ \\\${toPattern} \\\${nginxFlag};\\n\\\`;
1665
+ }
1666
+ }
1667
+
1668
+ nginxConfig = nginxConfig.replace(
1669
+ "# CUSTOM_REDIRECTS_PLACEHOLDER",
1670
+ redirectDirectives
1671
+ );
1672
+ console.log("Processed _redirects file");
1673
+ }
1674
+
1675
+ // ── Write final config ─────────────────────────────────
1676
+
1677
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
1678
+ fs.writeFileSync(outFile, nginxConfig);
1679
+ console.log(\\\`Generated nginx config at \\\${outFile}\\\`);`;
1680
+
1681
+ //#endregion
1682
+ //#region src/commands/deploy.ts
1683
+ async function withRetry(fn, maxRetries = 3) {
1684
+ for (let attempt = 0; attempt <= maxRetries; attempt++) try {
1685
+ return await fn();
1686
+ } catch (error) {
1687
+ const rateLimit = isRateLimitError(error);
1688
+ if (rateLimit && attempt < maxRetries) {
1689
+ const waitMs = Math.min(rateLimit.retryAfterMs, 3e4);
1690
+ await new Promise((r) => setTimeout(r, waitMs));
1691
+ continue;
1692
+ }
1693
+ throw error;
1694
+ }
1695
+ throw new Error("Max retries exceeded");
1696
+ }
1697
+ function detectPackageManager() {
1698
+ if (existsSync(resolve(process.cwd(), "bun.lockb")) || existsSync(resolve(process.cwd(), "bun.lock"))) return "bun";
1699
+ if (existsSync(resolve(process.cwd(), "pnpm-lock.yaml"))) return "pnpm";
1700
+ if (existsSync(resolve(process.cwd(), "yarn.lock"))) return "yarn";
1701
+ return "npm";
1702
+ }
1703
+ function detectNodeVersion() {
1704
+ try {
1705
+ const pkgPath = resolve(process.cwd(), "package.json");
1706
+ if (existsSync(pkgPath)) {
1707
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1708
+ if (pkg.engines?.node) {
1709
+ const match = pkg.engines.node.match(/(\d+)/);
1710
+ if (match) return match[1];
1711
+ }
1712
+ }
1713
+ } catch {}
1714
+ return "20";
1715
+ }
1716
+ function prepareExtraFiles(detection, serviceConfig) {
1717
+ if (existsSync(resolve(process.cwd(), "Dockerfile"))) return [];
1718
+ const rootDir = serviceConfig?.rootDirectory || ".";
1719
+ const serviceDockerfilePath = resolve(process.cwd(), rootDir, "Dockerfile");
1720
+ if (rootDir !== "." && existsSync(serviceDockerfilePath)) return [{
1721
+ name: "Dockerfile",
1722
+ content: readFileSync(serviceDockerfilePath, "utf-8")
1723
+ }];
1724
+ const fw = detection.framework;
1725
+ const pm = detectPackageManager();
1726
+ const nodeVersion = detectNodeVersion();
1727
+ const type = serviceConfig?.type?.toUpperCase() ?? fw?.type ?? "WEB";
1728
+ const files = [{
1729
+ name: "Dockerfile",
1730
+ content: generateDockerfile({
1731
+ serviceType: type,
1732
+ nodeVersion,
1733
+ pm,
1734
+ buildCommand: serviceConfig?.buildCommand ?? fw?.buildCommand ?? `${pm} run build`,
1735
+ startCommand: serviceConfig?.startCommand ?? fw?.startCommand ?? void 0,
1736
+ outputDir: serviceConfig?.outputDir ?? fw?.outputDir ?? void 0,
1737
+ rootDirectory: serviceConfig?.rootDirectory,
1738
+ port: serviceConfig?.port ?? fw?.port ?? 3e3
1739
+ })
1740
+ }];
1741
+ if (type === "STATIC") files.push({
1742
+ name: "nginx.conf",
1743
+ content: NGINX_CONF
1744
+ }, {
1745
+ name: "generate-nginx-config.cjs",
1746
+ content: GENERATE_NGINX_CONFIG_CJS
1747
+ });
1748
+ return files;
1749
+ }
1750
+ function computeExtraFilesForServices(services) {
1751
+ const velozConfig = loadConfig();
1752
+ return services.map((svc) => {
1753
+ let serviceConf;
1754
+ if (velozConfig) {
1755
+ for (const [, conf] of Object.entries(velozConfig.services)) if (conf.id === svc.serviceId) {
1756
+ const merged = mergeServiceWithDefaults(conf, velozConfig.defaults);
1757
+ serviceConf = {
1758
+ type: merged.type,
1759
+ buildCommand: merged.build?.command ?? void 0,
1760
+ startCommand: merged.runtime?.command ?? void 0,
1761
+ port: merged.runtime?.port ?? void 0,
1762
+ rootDirectory: merged.root,
1763
+ outputDir: merged.build?.outputDir ?? void 0
1764
+ };
1765
+ break;
1766
+ }
1767
+ }
1768
+ const detection = detectLocalRepo();
1769
+ return {
1770
+ ...svc,
1771
+ extraFiles: prepareExtraFiles(detection, serviceConf)
1772
+ };
1773
+ });
1774
+ }
1775
+ async function triggerDeploy(serviceId, serviceName) {
1776
+ const spinUpload = spinner(serviceName ? `Fazendo upload ${chalk.bold(serviceName)}...` : "Fazendo upload do código...");
1777
+ try {
1778
+ const sizeInBytes = await calculateDirectorySize(process.cwd());
1779
+ const sizeMB = Math.round(sizeInBytes / (1024 * 1024) * 10) / 10;
1780
+ if (sizeMB > 5) spinUpload.text = `Fazendo upload (${sizeMB} MB)...`;
1781
+ const client = await getClient();
1782
+ const authConfig = await requireAuth();
1783
+ const velozConfig = loadConfig();
1784
+ let serviceConf;
1785
+ if (velozConfig) {
1786
+ for (const [, svc] of Object.entries(velozConfig.services)) if (svc.id === serviceId) {
1787
+ const merged = mergeServiceWithDefaults(svc, velozConfig.defaults);
1788
+ serviceConf = {
1789
+ type: merged.type,
1790
+ buildCommand: merged.build?.command ?? void 0,
1791
+ startCommand: merged.runtime?.command ?? void 0,
1792
+ port: merged.runtime?.port ?? void 0,
1793
+ rootDirectory: merged.root,
1794
+ outputDir: merged.build?.outputDir ?? void 0
1795
+ };
1796
+ break;
1797
+ }
1798
+ }
1799
+ const extraFiles = prepareExtraFiles(detectLocalRepo(), serviceConf);
1800
+ spinUpload.text = "Iniciando deploy...";
1801
+ const deployment = await withRetry(() => client.deployments.create({ serviceId }));
1802
+ spinUpload.text = "Fazendo upload do código...";
1803
+ await withRetry(() => uploadSource(authConfig.apiUrl, deployment.id, process.cwd(), authConfig.apiKey, extraFiles));
1804
+ spinUpload.stop();
1805
+ success("Deploy iniciado com sucesso!");
1806
+ await streamDeploymentLogs(client, deployment.id, serviceId, serviceName);
1807
+ } catch (error) {
1808
+ spinUpload.stop();
1809
+ handleError(error);
1810
+ }
1811
+ }
1812
+ async function findServicesFromConfig() {
1813
+ const config = loadConfig();
1814
+ if (!config) return [];
1815
+ const services = [];
1816
+ for (const [key, serviceConfig] of Object.entries(config.services)) services.push({
1817
+ path: resolve(process.cwd(), serviceConfig.root),
1818
+ serviceId: serviceConfig.id,
1819
+ projectId: config.project.id,
1820
+ serviceName: serviceConfig.name,
1821
+ projectName: config.project.name,
1822
+ key
1823
+ });
1824
+ return services;
1825
+ }
1826
+ function readLocalFile(path) {
1827
+ const fullPath = resolve(process.cwd(), path);
1828
+ if (!existsSync(fullPath)) return null;
1829
+ try {
1830
+ return readFileSync(fullPath, "utf-8");
1831
+ } catch {
1832
+ return null;
1833
+ }
1834
+ }
1835
+ function printSummary(settings) {
1836
+ console.log();
1837
+ console.log(chalk.dim("─".repeat(40)));
1838
+ console.log(` ${chalk.bold("Nome:")} ${settings.name}`);
1839
+ console.log(` ${chalk.bold("Tipo:")} ${settings.type === "WEB" ? "Serviço Web" : "Site Estático"}`);
1840
+ console.log(` ${chalk.bold("Branch:")} ${settings.branch}`);
1841
+ if (settings.framework) console.log(` ${chalk.bold("Framework:")} ${settings.framework}`);
1842
+ if (settings.packageManager) console.log(` ${chalk.bold("Package Mgr:")} ${settings.packageManager}`);
1843
+ if (settings.buildCommand) console.log(` ${chalk.bold("Build:")} ${settings.buildCommand}`);
1844
+ if (settings.startCommand) console.log(` ${chalk.bold("Start:")} ${settings.startCommand}`);
1845
+ if (settings.outputDir) console.log(` ${chalk.bold("Output:")} ${settings.outputDir}`);
1846
+ if (settings.port) console.log(` ${chalk.bold("Porta:")} ${settings.port}`);
1847
+ console.log(chalk.dim("─".repeat(40)));
1848
+ console.log();
1849
+ }
1850
+ function detectLocalRepo(basePath = ".") {
1851
+ const files = {};
1852
+ const pkgJson = readLocalFile(join(basePath, "package.json"));
1853
+ if (pkgJson) files["package.json"] = pkgJson;
1854
+ const envExample = readLocalFile(join(basePath, ".env.example"));
1855
+ if (envExample) files[".env.example"] = envExample;
1856
+ else {
1857
+ const envSample = readLocalFile(join(basePath, ".env.sample"));
1858
+ if (envSample) files[".env.sample"] = envSample;
1859
+ else {
1860
+ const envFile = readLocalFile(join(basePath, ".env"));
1861
+ if (envFile) files[".env"] = envFile;
1862
+ }
1863
+ }
1864
+ const pnpmWs = readLocalFile(join(basePath, "pnpm-workspace.yaml"));
1865
+ if (pnpmWs) files["pnpm-workspace.yaml"] = pnpmWs;
1866
+ for (const lf of [
1867
+ "pnpm-lock.yaml",
1868
+ "yarn.lock",
1869
+ "bun.lock",
1870
+ "bun.lockb",
1871
+ "package-lock.json"
1872
+ ]) if (existsSync(resolve(process.cwd(), basePath, lf))) files[lf] = "";
1873
+ const workspacePatterns = [];
1874
+ if (pkgJson) try {
1875
+ const pkg = JSON.parse(pkgJson);
1876
+ if (pkg.workspaces) {
1877
+ const ws = pkg.workspaces;
1878
+ if (Array.isArray(ws)) workspacePatterns.push(...ws);
1879
+ else if (typeof ws === "object" && ws !== null && "packages" in ws && Array.isArray(ws.packages)) workspacePatterns.push(...ws.packages);
1880
+ }
1881
+ } catch {}
1882
+ if (pnpmWs) {
1883
+ const lines = pnpmWs.split("\n");
1884
+ let inPackages = false;
1885
+ for (const line of lines) {
1886
+ if (line.match(/^packages\s*:/)) {
1887
+ inPackages = true;
1888
+ continue;
1889
+ }
1890
+ if (inPackages) {
1891
+ const m = line.match(/^\s+-\s+['"]?([^'"]+)['"]?\s*$/);
1892
+ if (m) workspacePatterns.push(m[1]);
1893
+ else if (line.match(/^\S/)) break;
1894
+ }
1895
+ }
1896
+ }
1897
+ for (const pattern of workspacePatterns) {
1898
+ const hasGlob = /\/?\*\*?$/.test(pattern);
1899
+ const base = pattern.replace(/\/?\*\*?$/, "");
1900
+ if (hasGlob) {
1901
+ const dirPath = resolve(process.cwd(), base);
1902
+ if (!existsSync(dirPath)) continue;
1903
+ try {
1904
+ const entries = readdirSync(dirPath, { withFileTypes: true });
1905
+ for (const entry of entries) if (entry.isDirectory()) {
1906
+ const nestedPkg = readLocalFile(join(basePath, base, entry.name, "package.json"));
1907
+ if (nestedPkg) files[`${base}/${entry.name}/package.json`] = nestedPkg;
1908
+ }
1909
+ } catch {}
1910
+ } else {
1911
+ const nestedPkg = readLocalFile(join(basePath, base, "package.json"));
1912
+ if (nestedPkg) files[`${base}/package.json`] = nestedPkg;
1913
+ }
1914
+ }
1915
+ return analyzeRepo(files);
1916
+ }
1917
+ async function promptEnvVars(client, serviceId, detectedVars) {
1918
+ if (detectedVars.length === 0) return;
1919
+ console.log(chalk.cyan(`\n📝 ${detectedVars.length} variável(is) de ambiente detectada(s):\n`));
1920
+ for (const v of detectedVars) console.log(` • ${v.key}`);
1921
+ console.log();
1922
+ if (!await promptConfirm("Deseja preencher as variáveis agora?", false)) return;
1923
+ const vars = {};
1924
+ for (const v of detectedVars) {
1925
+ const finalVal = await prompt(` ${chalk.bold(v.key)}:${v.value ? chalk.dim(` (${v.value})`) : ""}`) || v.value;
1926
+ if (finalVal) vars[v.key] = finalVal;
1927
+ }
1928
+ const filled = Object.keys(vars).length;
1929
+ if (filled > 0) {
1930
+ const spinVars = spinner("Definindo variáveis de ambiente...");
1931
+ await withRetry(() => client.envVars.setBulk({
1932
+ serviceId,
1933
+ vars
1934
+ }));
1935
+ spinVars.stop();
1936
+ success(`${filled} variável(is) definida(s).`);
1937
+ }
1938
+ }
1939
+ async function createServiceFlow(client, projectId, projectName, repoName) {
1940
+ const branch = getGitBranch();
1941
+ const spinDetect = spinner("Detectando framework...");
1942
+ const detection = detectLocalRepo();
1943
+ spinDetect.stop();
1944
+ const pm = detection.packageManager;
1945
+ if (detection.isMonorepo && detection.monorepoApps.length > 0) {
1946
+ info(`Monorepo detectado (${pm})`);
1947
+ const selectedPaths = await promptMultiSelect("Quais apps deseja fazer o deploy?", detection.monorepoApps.map((app) => ({
1948
+ label: `${app.name}${app.framework ? ` (${app.framework.label})` : ""} — ${app.path}`,
1949
+ value: app.path
1950
+ })));
1951
+ const selectedApps = detection.monorepoApps.filter((a) => selectedPaths.includes(a.path));
1952
+ for (const app of selectedApps) {
1953
+ const fw$1 = app.framework;
1954
+ console.log(chalk.cyan(`\n── ${app.name} ──`));
1955
+ printSummary({
1956
+ name: app.name,
1957
+ type: fw$1?.type ?? "WEB",
1958
+ rootDir: app.path,
1959
+ branch,
1960
+ framework: fw$1?.label ?? null,
1961
+ packageManager: pm,
1962
+ buildCommand: fw$1?.buildCommand ?? null,
1963
+ startCommand: fw$1?.startCommand ?? null,
1964
+ outputDir: fw$1?.outputDir ?? null,
1965
+ port: fw$1?.port ?? 3e3
1966
+ });
1967
+ }
1968
+ if (!await promptConfirm("Confirmar e fazer deploy?")) for (const app of selectedApps) {
1969
+ const fw$1 = app.framework;
1970
+ console.log(chalk.cyan(`\n── Editar: ${app.name} ──`));
1971
+ const newBuild = await prompt(` Build command: ${chalk.dim(`(${fw$1?.buildCommand ?? "—"})`)}`);
1972
+ if (newBuild && fw$1) fw$1.buildCommand = newBuild;
1973
+ const newStart = await prompt(` Start command: ${chalk.dim(`(${fw$1?.startCommand ?? "—"})`)}`);
1974
+ if (newStart && fw$1) fw$1.startCommand = newStart;
1975
+ const newPort = await prompt(` Port: ${chalk.dim(`(${fw$1?.port ?? 3e3})`)}`);
1976
+ if (newPort && fw$1) fw$1.port = parseInt(newPort, 10) || fw$1.port;
1977
+ }
1978
+ const config = {
1979
+ version: "1.0",
1980
+ project: {
1981
+ id: projectId,
1982
+ name: projectName
1983
+ },
1984
+ services: {},
1985
+ created: (/* @__PURE__ */ new Date()).toISOString()
1986
+ };
1987
+ const createdServices = [];
1988
+ for (const app of selectedApps) {
1989
+ const fw$1 = app.framework;
1990
+ console.log(chalk.cyan(`\n── Criando serviço: ${app.name} ──\n`));
1991
+ const spinService$1 = spinner(`Criando serviço ${chalk.bold(app.name)}...`);
1992
+ const service$1 = await withRetry(() => client.services.create({
1993
+ projectId,
1994
+ name: app.name,
1995
+ type: fw$1?.type ?? "WEB",
1996
+ branch,
1997
+ rootDirectory: app.path,
1998
+ buildCommand: fw$1?.buildCommand ?? void 0,
1999
+ startCommand: fw$1?.startCommand ?? void 0,
2000
+ port: fw$1?.port ?? 3e3
2001
+ }));
2002
+ spinService$1.stop();
2003
+ success(`Serviço criado: ${chalk.bold(service$1.name)}`);
2004
+ createdServices.push({
2005
+ service: service$1,
2006
+ app
2007
+ });
2008
+ config.services[app.path] = {
2009
+ id: service$1.id,
2010
+ name: service$1.name,
2011
+ type: (fw$1?.type ?? "web").toLowerCase(),
2012
+ root: app.path,
2013
+ branch,
2014
+ build: fw$1?.buildCommand || fw$1?.outputDir ? {
2015
+ command: fw$1?.buildCommand ?? void 0,
2016
+ outputDir: fw$1?.outputDir ?? void 0
2017
+ } : void 0,
2018
+ runtime: {
2019
+ command: fw$1?.startCommand ?? void 0,
2020
+ port: fw$1?.port ?? 3e3
2021
+ }
2022
+ };
2023
+ }
2024
+ saveConfig(config);
2025
+ info(`Arquivo ${getConfigFileName()} criado na raiz do projeto.`);
2026
+ for (const { service: service$1, app } of createdServices) {
2027
+ console.log(chalk.cyan(`\n── Configurando variáveis: ${app.name} ──\n`));
2028
+ const serviceDetection = detectLocalRepo(app.path);
2029
+ await promptEnvVars(client, service$1.id, serviceDetection.envVars);
2030
+ }
2031
+ await deployServicesInParallel(client, createdServices.map(({ service: service$1, app }) => ({
2032
+ serviceId: service$1.id,
2033
+ serviceName: app.name,
2034
+ path: resolve(process.cwd(), app.path),
2035
+ extraFiles: prepareExtraFiles(detectLocalRepo(app.path), {
2036
+ type: app.framework?.type,
2037
+ buildCommand: app.framework?.buildCommand ?? void 0,
2038
+ startCommand: app.framework?.startCommand ?? void 0,
2039
+ port: app.framework?.port ?? void 0,
2040
+ rootDirectory: app.path,
2041
+ outputDir: app.framework?.outputDir ?? void 0
2042
+ })
2043
+ })));
2044
+ return createdServices[createdServices.length - 1]?.service.id || "";
2045
+ }
2046
+ const fw = detection.framework;
2047
+ const settings = {
2048
+ name: repoName,
2049
+ type: fw?.type ?? "WEB",
2050
+ rootDir: ".",
2051
+ branch,
2052
+ framework: fw?.label ?? null,
2053
+ packageManager: pm,
2054
+ buildCommand: fw?.buildCommand ?? null,
2055
+ startCommand: fw?.startCommand ?? null,
2056
+ outputDir: fw?.outputDir ?? null,
2057
+ port: fw?.port ?? 3e3
2058
+ };
2059
+ if (fw) info(`Framework detectado: ${chalk.bold(fw.label)}`);
2060
+ printSummary(settings);
2061
+ if (!await promptConfirm("Confirmar e fazer deploy?")) {
2062
+ const newName = await prompt(`Nome do serviço: ${chalk.dim(`(${settings.name})`)}`);
2063
+ if (newName) settings.name = newName;
2064
+ settings.type = await promptSelect("Tipo de serviço:", [{
2065
+ label: "Serviço Web",
2066
+ value: "WEB"
2067
+ }, {
2068
+ label: "Site Estático",
2069
+ value: "STATIC"
2070
+ }]);
2071
+ const newBuild = await prompt(`Build command: ${chalk.dim(`(${settings.buildCommand ?? "—"})`)}`);
2072
+ if (newBuild) settings.buildCommand = newBuild;
2073
+ const newStart = await prompt(`Start command: ${chalk.dim(`(${settings.startCommand ?? "—"})`)}`);
2074
+ if (newStart) settings.startCommand = newStart;
2075
+ const newPort = await prompt(`Port: ${chalk.dim(`(${settings.port})`)}`);
2076
+ if (newPort) settings.port = parseInt(newPort, 10) || settings.port;
2077
+ }
2078
+ const spinService = spinner("Criando serviço...");
2079
+ const service = await withRetry(() => client.services.create({
2080
+ projectId,
2081
+ name: settings.name,
2082
+ type: settings.type,
2083
+ branch,
2084
+ rootDirectory: settings.rootDir,
2085
+ buildCommand: settings.buildCommand ?? void 0,
2086
+ startCommand: settings.startCommand ?? void 0,
2087
+ port: settings.port
2088
+ }));
2089
+ spinService.stop();
2090
+ success(`Serviço criado: ${chalk.bold(service.name)}`);
2091
+ await promptEnvVars(client, service.id, detection.envVars);
2092
+ saveConfig({
2093
+ version: "1.0",
2094
+ project: {
2095
+ id: projectId,
2096
+ name: projectName
2097
+ },
2098
+ services: { main: {
2099
+ id: service.id,
2100
+ name: service.name,
2101
+ type: settings.type.toLowerCase(),
2102
+ root: settings.rootDir,
2103
+ branch,
2104
+ build: settings.buildCommand || settings.outputDir ? {
2105
+ command: settings.buildCommand ?? void 0,
2106
+ outputDir: settings.outputDir ?? void 0
2107
+ } : void 0,
2108
+ runtime: {
2109
+ command: settings.startCommand ?? void 0,
2110
+ port: settings.port
2111
+ }
2112
+ } },
2113
+ created: (/* @__PURE__ */ new Date()).toISOString()
2114
+ });
2115
+ info(`Arquivo ${getConfigFileName()} criado na raiz do projeto.`);
2116
+ return service.id;
2117
+ }
2118
+ const deployCommand = new Command("deploy").description("Fazer deploy do serviço (auto-detecta projeto pelo git)").option("-a, --all", "Deploy todos os serviços do monorepo").option("--service <service>", "Deploy de um serviço específico (chave ou nome)").option("-v, --verbose", "Mostrar logs detalhados do servidor").action(async (options) => {
2119
+ if (options.verbose) process.env.VELOZ_VERBOSE = "true";
2120
+ try {
2121
+ await requireAuth();
2122
+ const configuredServices = await findServicesFromConfig();
2123
+ if (configuredServices.length > 0) {
2124
+ if (options.service) {
2125
+ const found = configuredServices.find((s) => s.key === options.service || s.serviceName.toLowerCase() === options.service.toLowerCase() || s.serviceId === options.service);
2126
+ if (!found) {
2127
+ const available = configuredServices.map((s) => ` • ${s.key} (${s.serviceName})`).join("\n");
2128
+ console.error(chalk.red(`\n✗ Serviço '${options.service}' não encontrado.\n\nServiços disponíveis:\n${available}`));
2129
+ process.exit(1);
2130
+ }
2131
+ await triggerDeploy(found.serviceId, found.serviceName);
2132
+ return;
2133
+ }
2134
+ if (options.all || configuredServices.length === 1) {
2135
+ if (configuredServices.length > 1) {
2136
+ console.log(chalk.cyan(`\n🚀 Fazendo deploy de ${configuredServices.length} serviço(s):\n`));
2137
+ for (const service of configuredServices) {
2138
+ const relPath = relative(process.cwd(), service.path) || ".";
2139
+ console.log(` • ${chalk.bold(service.serviceName)} ${chalk.dim(`(${relPath})`)}`);
2140
+ }
2141
+ console.log();
2142
+ if (!await promptConfirm("Confirmar deploy de todos os serviços?")) {
2143
+ info("Deploy cancelado.");
2144
+ return;
2145
+ }
2146
+ }
2147
+ if (configuredServices.length === 1) await triggerDeploy(configuredServices[0].serviceId, configuredServices[0].serviceName);
2148
+ else {
2149
+ const servicesWithExtraFiles = computeExtraFilesForServices(configuredServices);
2150
+ await deployServicesInParallel(await getClient(), servicesWithExtraFiles);
2151
+ }
2152
+ return;
2153
+ } else {
2154
+ console.log(chalk.bold("\n📦 Serviços disponíveis:\n"));
2155
+ const selectedServiceIds = await promptMultiSelect("Quais serviços deseja fazer deploy?", configuredServices.map((s) => {
2156
+ const relPath = relative(process.cwd(), s.path) || ".";
2157
+ return {
2158
+ label: `${s.serviceName} ${chalk.dim(`(${relPath})`)}`,
2159
+ value: s.serviceId
2160
+ };
2161
+ }));
2162
+ const selectedServices = configuredServices.filter((s) => selectedServiceIds.includes(s.serviceId));
2163
+ if (selectedServices.length === 0) {
2164
+ info("Nenhum serviço selecionado.");
2165
+ return;
2166
+ }
2167
+ if (selectedServices.length === 1) await triggerDeploy(selectedServices[0].serviceId, selectedServices[0].serviceName);
2168
+ else {
2169
+ const servicesWithExtraFiles = computeExtraFilesForServices(selectedServices);
2170
+ await deployServicesInParallel(await getClient(), servicesWithExtraFiles);
2171
+ }
2172
+ return;
2173
+ }
2174
+ }
2175
+ if (!isGitRepo()) {
2176
+ console.error(chalk.red("\n✗ Este diretório não é um repositório git. Inicialize com `git init` e adicione um remote."));
2177
+ process.exit(1);
2178
+ }
2179
+ info("Detectando repositório git...");
2180
+ const remote = getGitRemote();
2181
+ if (!remote) {
2182
+ console.error(chalk.red("\n✗ Nenhum remote 'origin' encontrado. Adicione um remote git."));
2183
+ process.exit(1);
2184
+ }
2185
+ console.log(chalk.white(` ${chalk.bold("Repositório:")} ${remote.owner}/${remote.repo}`));
2186
+ const client = await getClient();
2187
+ const spin = spinner("Buscando projeto...");
2188
+ const project = await withRetry(() => client.projects.findByRepo({
2189
+ githubRepoOwner: remote.owner,
2190
+ githubRepoName: remote.repo
2191
+ }));
2192
+ spin.stop();
2193
+ if (project && project.services.length > 0) {
2194
+ info(`Projeto encontrado: ${chalk.bold(project.name)}`);
2195
+ let serviceId;
2196
+ let serviceName;
2197
+ if (project.services.length === 1) {
2198
+ const svc = project.services[0];
2199
+ serviceId = svc.id;
2200
+ serviceName = svc.name;
2201
+ } else {
2202
+ serviceId = await promptSelect("Selecione o serviço:", project.services.map((s) => ({
2203
+ label: `${s.name} (${s.type} — branch: ${s.branch})`,
2204
+ value: s.id
2205
+ })));
2206
+ serviceName = project.services.find((s) => s.id === serviceId)?.name ?? "";
2207
+ }
2208
+ const selectedService = project.services.find((s) => s.id === serviceId);
2209
+ saveConfig({
2210
+ version: "1.0",
2211
+ project: {
2212
+ id: project.id,
2213
+ name: project.name
2214
+ },
2215
+ services: { main: {
2216
+ id: serviceId,
2217
+ name: serviceName,
2218
+ type: selectedService?.type?.toLowerCase() ?? "web",
2219
+ root: ".",
2220
+ branch: selectedService?.branch
2221
+ } },
2222
+ created: (/* @__PURE__ */ new Date()).toISOString()
2223
+ });
2224
+ info(`Arquivo ${getConfigFileName()} criado na raiz do projeto.`);
2225
+ await triggerDeploy(serviceId);
2226
+ return;
2227
+ }
2228
+ if (project && project.services.length === 0) {
2229
+ info(`Projeto encontrado: ${chalk.bold(project.name)}`);
2230
+ info("Nenhum serviço configurado. Vamos criar um.");
2231
+ await triggerDeploy(await createServiceFlow(client, project.id, project.name, remote.repo));
2232
+ return;
2233
+ }
2234
+ info("Projeto não encontrado. Vamos criar um novo.");
2235
+ const projectName = await prompt(`Nome do projeto: ${chalk.dim(`(${remote.repo})`)}`) || remote.repo;
2236
+ const spinProject = spinner("Criando projeto...");
2237
+ const newProject = await withRetry(() => client.projects.create({
2238
+ name: projectName,
2239
+ githubRepoOwner: remote.owner,
2240
+ githubRepoName: remote.repo
2241
+ }));
2242
+ spinProject.stop();
2243
+ success(`Projeto criado: ${chalk.bold(newProject.name)}`);
2244
+ await triggerDeploy(await createServiceFlow(client, newProject.id, newProject.name, remote.repo));
2245
+ } catch (error) {
2246
+ handleError(error);
2247
+ }
2248
+ });
2249
+
2250
+ //#endregion
2251
+ //#region src/lib/service-resolver.ts
2252
+ const STATE_FILE = join(homedir(), ".veloz", "state.json");
2253
+ function loadState() {
2254
+ if (!existsSync(STATE_FILE)) return { projectDefaults: {} };
2255
+ try {
2256
+ return JSON.parse(readFileSync(STATE_FILE, "utf-8"));
2257
+ } catch {
2258
+ return { projectDefaults: {} };
2259
+ }
2260
+ }
2261
+ function saveState(state) {
2262
+ const dir = join(homedir(), ".veloz");
2263
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
2264
+ writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), "utf-8");
2265
+ }
2266
+ function getDefaultServiceKey() {
2267
+ const config = loadConfig();
2268
+ if (!config) return null;
2269
+ return loadState().projectDefaults[config.project.id]?.currentService ?? null;
2270
+ }
2271
+ function setDefaultServiceKey(serviceKey) {
2272
+ const config = requireConfig();
2273
+ const state = loadState();
2274
+ state.projectDefaults[config.project.id] = { currentService: serviceKey };
2275
+ saveState(state);
2276
+ }
2277
+ const TAG_COLORS = [
2278
+ chalk.cyan,
2279
+ chalk.magenta,
2280
+ chalk.yellow,
2281
+ chalk.green,
2282
+ chalk.blue,
2283
+ chalk.red
2284
+ ];
2285
+ function getServiceTag(name, maxLen, colorIndex) {
2286
+ const color = TAG_COLORS[colorIndex % TAG_COLORS.length];
2287
+ return color(`[${name.padEnd(maxLen)}]`);
2288
+ }
2289
+ function getServiceHeader(name, colorIndex) {
2290
+ const color = TAG_COLORS[colorIndex % TAG_COLORS.length];
2291
+ return color(`── ${name} ──`);
2292
+ }
2293
+ function findByFlag(entries, flag) {
2294
+ return entries.find(([key, svc]) => key === flag || svc.name.toLowerCase() === flag.toLowerCase() || svc.id === flag);
2295
+ }
2296
+ function throwNotFound(flag, entries) {
2297
+ const available = entries.map(([key, svc]) => ` • ${key} (${svc.name})`).join("\n");
2298
+ throw new Error(`Serviço '${flag}' não encontrado.\n\nServiços disponíveis:\n${available}`);
2299
+ }
2300
+ /**
2301
+ * Resolution order:
2302
+ * 1. Explicit `--service` flag → match by key, name, or id
2303
+ * 2. Single service in config → auto-select (zero friction)
2304
+ * 3. Default from `veloz use` → remembered choice
2305
+ * 4. Interactive prompt → last resort
2306
+ */
2307
+ async function resolveService(serviceFlag) {
2308
+ const config = requireConfig();
2309
+ const entries = Object.entries(config.services);
2310
+ if (entries.length === 0) throw new Error("Nenhum serviço encontrado na configuração. Execute 'veloz deploy' para criar um.");
2311
+ if (serviceFlag) {
2312
+ const found = findByFlag(entries, serviceFlag);
2313
+ if (!found) throwNotFound(serviceFlag, entries);
2314
+ const [key, service] = found;
2315
+ return {
2316
+ key,
2317
+ service: mergeServiceWithDefaults(service, config.defaults),
2318
+ config
2319
+ };
2320
+ }
2321
+ if (entries.length === 1) {
2322
+ const [key, service] = entries[0];
2323
+ return {
2324
+ key,
2325
+ service: mergeServiceWithDefaults(service, config.defaults),
2326
+ config
2327
+ };
2328
+ }
2329
+ const defaultKey = getDefaultServiceKey();
2330
+ if (defaultKey && config.services[defaultKey]) {
2331
+ const service = config.services[defaultKey];
2332
+ return {
2333
+ key: defaultKey,
2334
+ service: mergeServiceWithDefaults(service, config.defaults),
2335
+ config
2336
+ };
2337
+ }
2338
+ const selectedKey = await promptSelect("Qual serviço?", entries.map(([key, svc]) => ({
2339
+ label: `${svc.name} (${key})`,
2340
+ value: key
2341
+ })));
2342
+ return {
2343
+ key: selectedKey,
2344
+ service: mergeServiceWithDefaults(config.services[selectedKey], config.defaults),
2345
+ config
2346
+ };
2347
+ }
2348
+ async function resolveServiceId(serviceFlag) {
2349
+ const { service } = await resolveService(serviceFlag);
2350
+ return service.id;
2351
+ }
2352
+ function resolveAllServices(serviceFlag) {
2353
+ const config = requireConfig();
2354
+ const entries = Object.entries(config.services);
2355
+ if (entries.length === 0) throw new Error("Nenhum serviço encontrado na configuração. Execute 'veloz deploy' para criar um.");
2356
+ if (serviceFlag) {
2357
+ const found = findByFlag(entries, serviceFlag);
2358
+ if (!found) throwNotFound(serviceFlag, entries);
2359
+ const [key, raw] = found;
2360
+ const service = mergeServiceWithDefaults(raw, config.defaults);
2361
+ return {
2362
+ services: [{
2363
+ key,
2364
+ service,
2365
+ index: 0
2366
+ }],
2367
+ config,
2368
+ maxNameLen: service.name.length
2369
+ };
2370
+ }
2371
+ const services = entries.map(([key, raw], index) => ({
2372
+ key,
2373
+ service: mergeServiceWithDefaults(raw, config.defaults),
2374
+ index
2375
+ }));
2376
+ return {
2377
+ services,
2378
+ config,
2379
+ maxNameLen: Math.max(...services.map((s) => s.service.name.length))
2380
+ };
2381
+ }
2382
+
2383
+ //#endregion
2384
+ //#region src/commands/logs.ts
2385
+ function formatTime(timestamp) {
2386
+ return chalk.dim(new Date(timestamp).toLocaleTimeString("pt-BR"));
2387
+ }
2388
+ async function streamFollow(client, services, maxNameLen, tailLines) {
2389
+ const showTags = services.length > 1;
2390
+ const streams = services.map(async ({ service, index }) => {
2391
+ const tag = showTags ? `${getServiceTag(service.name, maxNameLen, index)} ` : "";
2392
+ try {
2393
+ const stream = await client.logs.streamRuntime({
2394
+ serviceId: service.id,
2395
+ tailLines: Math.ceil(tailLines / services.length)
2396
+ });
2397
+ for await (const entry of stream) console.log(`${tag}${formatTime(entry.timestamp)} ${entry.message}`);
2398
+ } catch {
2399
+ console.log(`${tag}${chalk.red("Erro ao conectar ao stream de logs")}`);
2400
+ }
2401
+ });
2402
+ await Promise.allSettled(streams);
2403
+ }
2404
+ async function fetchRecent(client, services, maxNameLen, tailLines) {
2405
+ const showTags = services.length > 1;
2406
+ const allEntries = (await Promise.allSettled(services.map(async ({ service, index }) => {
2407
+ return (await client.logs.getRecent({
2408
+ serviceId: service.id,
2409
+ tailLines
2410
+ })).map((e) => ({
2411
+ ...e,
2412
+ serviceIndex: index,
2413
+ serviceName: service.name
2414
+ }));
2415
+ }))).filter((r) => r.status === "fulfilled").flatMap((r) => r.value).sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
2416
+ if (allEntries.length === 0) {
2417
+ info("Nenhum log encontrado.");
2418
+ return;
2419
+ }
2420
+ console.log();
2421
+ for (const entry of allEntries) {
2422
+ const tag = showTags ? `${getServiceTag(entry.serviceName, maxNameLen, entry.serviceIndex)} ` : "";
2423
+ console.log(`${tag}${formatTime(entry.timestamp)} ${entry.message}`);
2424
+ }
2425
+ console.log();
2426
+ info(`Mostrando ${allEntries.length} linha(s). Use ${chalk.bold("--follow")} para acompanhar em tempo real.`);
2427
+ }
2428
+ const logsCommand = new Command("logs").description("Visualizar logs dos serviços").option("-f, --follow", "Acompanhar logs em tempo real").option("-n, --tail <linhas>", "Número de linhas recentes", "50").option("--service <service>", "Filtrar por serviço (chave ou nome)").action(async (opts) => {
2429
+ const spin = spinner("Carregando logs...");
2430
+ try {
2431
+ const { services, maxNameLen } = resolveAllServices(opts.service);
2432
+ const client = await getClient();
2433
+ const tailLines = parseInt(opts.tail, 10) || 50;
2434
+ if (opts.follow) {
2435
+ spin.text = services.length > 1 ? `Conectando a ${services.length} serviço(s)...` : "Conectando ao streaming de logs...";
2436
+ spin.stop();
2437
+ info("Streaming de logs ativo. Pressione Ctrl+C para sair.\n");
2438
+ await streamFollow(client, services, maxNameLen, tailLines);
2439
+ } else {
2440
+ spin.stop();
2441
+ await fetchRecent(client, services, maxNameLen, tailLines);
2442
+ }
2443
+ } catch (error) {
2444
+ spin.stop();
2445
+ handleError(error);
2446
+ }
2447
+ });
2448
+
2449
+ //#endregion
2450
+ //#region src/commands/env.ts
2451
+ const envCommand = new Command("env").description("Gerenciar variáveis de ambiente");
2452
+ envCommand.command("list").alias("listar").description("Listar variáveis de ambiente").option("--service <service>", "Filtrar por serviço (chave ou nome)").action(async (opts) => {
2453
+ const spin = spinner("Carregando variáveis de ambiente...");
2454
+ try {
2455
+ const { services } = resolveAllServices(opts.service);
2456
+ const client = await getClient();
2457
+ const showHeaders = services.length > 1;
2458
+ let totalVars = 0;
2459
+ for (const { service, index } of services) {
2460
+ const envVars = await client.envVars.list({ serviceId: service.id });
2461
+ totalVars += envVars.length;
2462
+ if (showHeaders) console.log(`\n${getServiceHeader(service.name, index)}`);
2463
+ if (envVars.length === 0) {
2464
+ info("Nenhuma variável de ambiente configurada.");
2465
+ continue;
2466
+ }
2467
+ printTable([
2468
+ "Chave",
2469
+ "Valor (mascarado)",
2470
+ "Atualizado em"
2471
+ ], envVars.map((v) => [
2472
+ chalk.bold(v.key),
2473
+ chalk.dim(v.maskedValue),
2474
+ new Date(v.updatedAt).toLocaleDateString("pt-BR")
2475
+ ]));
2476
+ }
2477
+ spin.stop();
2478
+ if (totalVars === 0 && !showHeaders) info("Nenhuma variável de ambiente configurada.");
2479
+ } catch (error) {
2480
+ spin.stop();
2481
+ handleError(error);
2482
+ }
2483
+ });
2484
+ envCommand.command("set <pares...>").description("Definir variável de ambiente (formato: CHAVE=VALOR)").option("--service <service>", "Serviço alvo (chave ou nome)").action(async (pares, opts) => {
2485
+ const spin = spinner("Salvando variáveis de ambiente...");
2486
+ try {
2487
+ const serviceId = await resolveServiceId(opts.service);
2488
+ const client = await getClient();
2489
+ for (const par of pares) {
2490
+ const eqIndex = par.indexOf("=");
2491
+ if (eqIndex === -1) {
2492
+ spin.stop();
2493
+ console.error(chalk.red(`\n✗ Formato inválido: "${par}". Use CHAVE=VALOR.`));
2494
+ process.exit(1);
2495
+ }
2496
+ const key = par.slice(0, eqIndex);
2497
+ const value = par.slice(eqIndex + 1);
2498
+ if (!key) {
2499
+ spin.stop();
2500
+ console.error(chalk.red("\n✗ Chave não pode estar vazia."));
2501
+ process.exit(1);
2502
+ }
2503
+ await client.envVars.set({
2504
+ serviceId,
2505
+ key,
2506
+ value
2507
+ });
2508
+ }
2509
+ spin.stop();
2510
+ if (pares.length === 1) {
2511
+ const key = pares[0].slice(0, pares[0].indexOf("="));
2512
+ success(`Variável ${chalk.bold(key)} definida com sucesso.`);
2513
+ } else success(`${pares.length} variáveis definidas com sucesso.`);
2514
+ info("Faça um novo deploy para aplicar as alterações.");
2515
+ } catch (error) {
2516
+ spin.stop();
2517
+ handleError(error);
2518
+ }
2519
+ });
2520
+ envCommand.command("delete <chave>").alias("deletar").description("Remover variável de ambiente").option("--service <service>", "Serviço alvo (chave ou nome)").action(async (chave, opts) => {
2521
+ const spin = spinner("Removendo variável de ambiente...");
2522
+ try {
2523
+ const serviceId = await resolveServiceId(opts.service);
2524
+ await (await getClient()).envVars.delete({
2525
+ serviceId,
2526
+ key: chave
2527
+ });
2528
+ spin.stop();
2529
+ success(`Variável ${chalk.bold(chave)} removida com sucesso.`);
2530
+ info("Faça um novo deploy para aplicar as alterações.");
2531
+ } catch (error) {
2532
+ spin.stop();
2533
+ handleError(error);
2534
+ }
2535
+ });
2536
+ envCommand.command("import [arquivo]").description("Importar variáveis de ambiente de um arquivo .env ou colar diretamente").option("-r, --replace", "Substituir todas as variáveis existentes").option("--service <service>", "Serviço alvo (chave ou nome)").action(async (arquivo, options) => {
2537
+ try {
2538
+ const serviceId = await resolveServiceId(options.service);
2539
+ const client = await getClient();
2540
+ let envContent = "";
2541
+ if (arquivo) {
2542
+ const filePath = resolve(process.cwd(), arquivo);
2543
+ if (!existsSync(filePath)) {
2544
+ console.error(chalk.red(`\n✗ Arquivo não encontrado: ${arquivo}`));
2545
+ process.exit(1);
2546
+ }
2547
+ envContent = readFileSync(filePath, "utf-8");
2548
+ info(`Importando de ${chalk.bold(arquivo)}...`);
2549
+ } else {
2550
+ console.log(chalk.cyan("\n📋 Modo de colagem interativo"));
2551
+ console.log(chalk.dim("Cole seu conteúdo .env abaixo. Pressione Ctrl+D (ou Ctrl+Z no Windows) quando terminar:\n"));
2552
+ const rl = readline.createInterface({
2553
+ input: process.stdin,
2554
+ output: process.stdout
2555
+ });
2556
+ const lines$1 = [];
2557
+ await new Promise((resolve$1) => {
2558
+ rl.on("line", (line) => {
2559
+ lines$1.push(line);
2560
+ });
2561
+ rl.on("close", () => {
2562
+ resolve$1();
2563
+ });
2564
+ });
2565
+ envContent = lines$1.join("\n");
2566
+ }
2567
+ const envVars = {};
2568
+ const lines = envContent.split("\n");
2569
+ for (const line of lines) {
2570
+ const trimmed = line.trim();
2571
+ if (!trimmed || trimmed.startsWith("#")) continue;
2572
+ const eqIndex = trimmed.indexOf("=");
2573
+ if (eqIndex === -1) continue;
2574
+ let key = trimmed.slice(0, eqIndex).trim();
2575
+ let value = trimmed.slice(eqIndex + 1).trim();
2576
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
2577
+ value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\\\/g, "\\");
2578
+ envVars[key] = value;
2579
+ }
2580
+ const varsCount = Object.keys(envVars).length;
2581
+ if (varsCount === 0) {
2582
+ console.error(chalk.yellow("⚠ Nenhuma variável válida encontrada."));
2583
+ return;
2584
+ }
2585
+ console.log(chalk.bold("\n📝 Variáveis a serem importadas:\n"));
2586
+ for (const [key, value] of Object.entries(envVars)) {
2587
+ const maskedValue = value.length > 20 ? value.slice(0, 20) + "..." : value;
2588
+ console.log(` ${chalk.green(key)}=${chalk.dim(maskedValue)}`);
2589
+ }
2590
+ console.log();
2591
+ if (options.replace) console.log(chalk.yellow("⚠ Modo de substituição: TODAS as variáveis existentes serão removidas!\n"));
2592
+ if (!await promptConfirm(`Importar ${varsCount} variável(is) de ambiente?`)) {
2593
+ info("Importação cancelada.");
2594
+ return;
2595
+ }
2596
+ const spin = spinner("Importando variáveis de ambiente...");
2597
+ try {
2598
+ if (options.replace) {
2599
+ const existingVars = await client.envVars.list({ serviceId });
2600
+ for (const envVar of existingVars) await client.envVars.delete({
2601
+ serviceId,
2602
+ key: envVar.key
2603
+ });
2604
+ }
2605
+ await client.envVars.setBulk({
2606
+ serviceId,
2607
+ vars: envVars
2608
+ });
2609
+ spin.stop();
2610
+ success(`${varsCount} variável(is) importada(s) com sucesso!`);
2611
+ info("Faça um novo deploy para aplicar as alterações.");
2612
+ } catch (error) {
2613
+ spin.stop();
2614
+ throw error;
2615
+ }
2616
+ } catch (error) {
2617
+ handleError(error);
2618
+ }
2619
+ });
2620
+ envCommand.command("export [arquivo]").description("Exportar variáveis de ambiente para um arquivo .env").option("--service <service>", "Serviço alvo (chave ou nome)").action(async (arquivo, opts) => {
2621
+ const spin = spinner("Carregando variáveis de ambiente...");
2622
+ try {
2623
+ const serviceId = await resolveServiceId(opts.service);
2624
+ const envVars = await (await getClient()).envVars.list({ serviceId });
2625
+ spin.stop();
2626
+ if (envVars.length === 0) {
2627
+ info("Nenhuma variável de ambiente para exportar.");
2628
+ return;
2629
+ }
2630
+ const envContent = envVars.map((v) => `${v.key}=${v.maskedValue}`).join("\n");
2631
+ if (arquivo) {
2632
+ writeFileSync(resolve(process.cwd(), arquivo), envContent + "\n", "utf-8");
2633
+ success(`Variáveis exportadas para ${chalk.bold(arquivo)}`);
2634
+ console.log(chalk.dim("Nota: Valores estão mascarados por segurança."));
2635
+ } else {
2636
+ console.log(chalk.bold("\n# Variáveis de Ambiente (valores mascarados)\n"));
2637
+ console.log(envContent);
2638
+ console.log();
2639
+ }
2640
+ } catch (error) {
2641
+ spin.stop();
2642
+ handleError(error);
2643
+ }
2644
+ });
2645
+
2646
+ //#endregion
2647
+ //#region src/commands/domains.ts
2648
+ const tlsStatusLabels = {
2649
+ PENDING: "Pendente",
2650
+ PROVISIONING: "Provisionando",
2651
+ ACTIVE: "Ativo",
2652
+ FAILED: "Falhou"
2653
+ };
2654
+ const domainsCommand = new Command("domains").alias("dominios").description("Gerenciar domínios personalizados");
2655
+ domainsCommand.command("list").alias("listar").description("Listar domínios dos serviços").option("--service <service>", "Filtrar por serviço (chave ou nome)").action(async (opts) => {
2656
+ const spin = spinner("Carregando domínios...");
2657
+ try {
2658
+ const { services } = resolveAllServices(opts.service);
2659
+ const client = await getClient();
2660
+ const showHeaders = services.length > 1;
2661
+ let totalDomains = 0;
2662
+ for (const { service, index } of services) {
2663
+ const domains = await client.domains.list({ serviceId: service.id });
2664
+ totalDomains += domains.length;
2665
+ if (showHeaders) console.log(`\n${getServiceHeader(service.name, index)}`);
2666
+ if (domains.length === 0) {
2667
+ info("Nenhum domínio configurado.");
2668
+ continue;
2669
+ }
2670
+ printTable([
2671
+ "ID",
2672
+ "Domínio",
2673
+ "Verificado",
2674
+ "TLS",
2675
+ "Tipo"
2676
+ ], domains.map((d) => [
2677
+ d.id.slice(0, 8),
2678
+ chalk.bold(d.domain),
2679
+ d.verified ? chalk.green("✓ Sim") : chalk.yellow("✗ Não"),
2680
+ tlsStatusLabels[d.tlsStatus] ?? d.tlsStatus,
2681
+ d.isAutoGenerated ? "Auto" : "Personalizado"
2682
+ ]));
2683
+ }
2684
+ spin.stop();
2685
+ if (totalDomains === 0 && !showHeaders) info("Nenhum domínio configurado.");
2686
+ } catch (error) {
2687
+ spin.stop();
2688
+ handleError(error);
2689
+ }
2690
+ });
2691
+ domainsCommand.command("add <dominio>").alias("adicionar").description("Adicionar domínio personalizado").option("--service <service>", "Serviço alvo (chave ou nome)").action(async (dominio, opts) => {
2692
+ const spin = spinner("Adicionando domínio...");
2693
+ try {
2694
+ const serviceId = await resolveServiceId(opts.service);
2695
+ const result = await (await getClient()).domains.add({
2696
+ serviceId,
2697
+ domain: dominio
2698
+ });
2699
+ spin.stop();
2700
+ success(`Domínio ${chalk.bold(dominio)} adicionado com sucesso!`);
2701
+ console.log();
2702
+ console.log(chalk.yellow.bold(" Instruções de DNS:"));
2703
+ console.log(chalk.white(` ${result.dnsInstruction}`));
2704
+ console.log();
2705
+ info(`Após configurar o DNS, execute: ${chalk.bold(`veloz domains verify ${result.id.slice(0, 8)}`)}`);
2706
+ } catch (error) {
2707
+ spin.stop();
2708
+ handleError(error);
2709
+ }
2710
+ });
2711
+ domainsCommand.command("verify <domainId>").alias("verificar").description("Verificar configuração DNS de um domínio").action(async (domainId) => {
2712
+ const spin = spinner("Verificando DNS...");
2713
+ try {
2714
+ const result = await (await getClient()).domains.verify({ domainId });
2715
+ spin.stop();
2716
+ if (result.verified) {
2717
+ success(`Domínio ${chalk.bold(result.domain.domain)} verificado com sucesso!`);
2718
+ info("O certificado TLS será provisionado automaticamente.");
2719
+ } else {
2720
+ console.log(chalk.yellow(`\n⚠ Domínio ${chalk.bold(result.domain.domain)} ainda não verificado.`));
2721
+ info("Verifique se o CNAME foi propagado e tente novamente.");
2722
+ }
2723
+ } catch (error) {
2724
+ spin.stop();
2725
+ handleError(error);
2726
+ }
2727
+ });
2728
+ domainsCommand.command("delete <domainId>").alias("deletar").description("Remover domínio").action(async (domainId) => {
2729
+ const spin = spinner("Removendo domínio...");
2730
+ try {
2731
+ await (await getClient()).domains.delete({ domainId });
2732
+ spin.stop();
2733
+ success("Domínio removido com sucesso.");
2734
+ } catch (error) {
2735
+ spin.stop();
2736
+ handleError(error);
2737
+ }
2738
+ });
2739
+
2740
+ //#endregion
2741
+ //#region src/commands/config.ts
2742
+ function formatValue(value) {
2743
+ if (value === null || value === void 0 || value === "") return chalk.dim("—");
2744
+ return chalk.cyan(String(value));
2745
+ }
2746
+ function printServiceConfig(service) {
2747
+ console.log(` ${chalk.bold("Nome:")} ${formatValue(service.name)}`);
2748
+ console.log(` ${chalk.bold("Tipo:")} ${formatValue(service.type === "WEB" ? "Serviço Web" : "Site Estático")}`);
2749
+ console.log(` ${chalk.bold("Branch:")} ${formatValue(service.branch)}`);
2750
+ console.log(` ${chalk.bold("Root Dir:")} ${formatValue(service.rootDirectory || "/")}`);
2751
+ console.log(` ${chalk.bold("Build Command:")} ${formatValue(service.buildCommand)}`);
2752
+ console.log(` ${chalk.bold("Start Command:")} ${formatValue(service.startCommand)}`);
2753
+ console.log(` ${chalk.bold("Porta:")} ${formatValue(service.port)}`);
2754
+ console.log(` ${chalk.bold("Instâncias:")} ${formatValue(service.instanceCount)}`);
2755
+ console.log(` ${chalk.bold("CPU Limit:")} ${formatValue(service.cpuLimit)}`);
2756
+ console.log(` ${chalk.bold("Memory Limit:")} ${formatValue(service.memoryLimit)}`);
2757
+ }
2758
+ const configCommand = new Command("config").description("Gerenciar configurações do serviço");
2759
+ configCommand.command("show").description("Mostrar configurações atuais dos serviços").option("--service <service>", "Filtrar por serviço (chave ou nome)").action(async (opts) => {
2760
+ try {
2761
+ const { services } = resolveAllServices(opts.service);
2762
+ const client = await getClient();
2763
+ const showHeaders = services.length > 1;
2764
+ const spin = spinner("Buscando configurações...");
2765
+ for (const { service: svcConfig, index } of services) {
2766
+ const service = await client.services.get({ serviceId: svcConfig.id });
2767
+ if (showHeaders) console.log(`\n${getServiceHeader(svcConfig.name, index)}\n`);
2768
+ else console.log(chalk.bold("\n📋 Configurações do Serviço\n"));
2769
+ printServiceConfig(service);
2770
+ console.log();
2771
+ }
2772
+ spin.stop();
2773
+ } catch (error) {
2774
+ handleError(error);
2775
+ }
2776
+ });
2777
+ configCommand.command("set").description("Atualizar configurações do serviço").option("-n, --name <name>", "Nome do serviço").option("-b, --build <command>", "Comando de build").option("-s, --start <command>", "Comando de start").option("-p, --port <port>", "Porta do serviço").option("-r, --root <dir>", "Diretório raiz").option("-i, --instances <count>", "Número de instâncias").option("--cpu <limit>", "Limite de CPU (ex: 500m, 1)").option("--memory <limit>", "Limite de memória (ex: 512Mi, 1Gi)").option("--branch <branch>", "Branch do Git").option("--service <service>", "Serviço alvo (chave ou nome)").action(async (options) => {
2778
+ try {
2779
+ const serviceId = await resolveServiceId(options.service);
2780
+ const client = await getClient();
2781
+ const updates = {};
2782
+ if (options.name) updates.name = options.name;
2783
+ if (options.build !== void 0) updates.buildCommand = options.build === "none" ? null : options.build;
2784
+ if (options.start !== void 0) updates.startCommand = options.start === "none" ? null : options.start;
2785
+ if (options.port) updates.port = parseInt(options.port, 10);
2786
+ if (options.root !== void 0) updates.rootDirectory = options.root === "/" ? null : options.root;
2787
+ if (options.instances) updates.instanceCount = parseInt(options.instances, 10);
2788
+ if (options.cpu) updates.cpuLimit = options.cpu;
2789
+ if (options.memory) updates.memoryLimit = options.memory;
2790
+ if (options.branch) updates.branch = options.branch;
2791
+ if (Object.keys(updates).length === 0) {
2792
+ console.error(chalk.yellow("⚠ Nenhuma configuração fornecida para atualizar."));
2793
+ console.log(chalk.dim("\nUse 'veloz config set --help' para ver as opções disponíveis."));
2794
+ return;
2795
+ }
2796
+ const spin = spinner("Atualizando configurações...");
2797
+ await client.services.update({
2798
+ serviceId,
2799
+ ...updates
2800
+ });
2801
+ spin.stop();
2802
+ success("Configurações atualizadas com sucesso!");
2803
+ console.log(chalk.dim("\nValores atualizados:"));
2804
+ for (const [key, value] of Object.entries(updates)) {
2805
+ const displayKey = key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()).trim();
2806
+ console.log(` ${chalk.bold(displayKey)}: ${formatValue(value)}`);
2807
+ }
2808
+ console.log();
2809
+ info("Execute 'veloz deploy' para aplicar as mudanças.");
2810
+ } catch (error) {
2811
+ handleError(error);
2812
+ }
2813
+ });
2814
+ configCommand.command("edit").description("Editar configurações interativamente").option("--service <service>", "Serviço alvo (chave ou nome)").action(async (opts) => {
2815
+ try {
2816
+ const serviceId = await resolveServiceId(opts.service);
2817
+ const client = await getClient();
2818
+ const spin = spinner("Buscando configurações atuais...");
2819
+ const service = await client.services.get({ serviceId });
2820
+ spin.stop();
2821
+ console.log(chalk.bold("\n📝 Editar Configurações\n"));
2822
+ console.log(chalk.dim("Pressione Enter para manter o valor atual, digite 'none' para remover\n"));
2823
+ const updates = {};
2824
+ const name = await prompt(`Nome do serviço ${chalk.dim(`(${service.name})`)}: `);
2825
+ if (name) updates.name = name;
2826
+ const buildCmd = await prompt(`Build command ${chalk.dim(`(${service.buildCommand || "—"})`)}: `);
2827
+ if (buildCmd) updates.buildCommand = buildCmd === "none" ? null : buildCmd;
2828
+ const startCmd$1 = await prompt(`Start command ${chalk.dim(`(${service.startCommand || "—"})`)}: `);
2829
+ if (startCmd$1) updates.startCommand = startCmd$1 === "none" ? null : startCmd$1;
2830
+ const port = await prompt(`Porta ${chalk.dim(`(${service.port})`)}: `);
2831
+ if (port) updates.port = parseInt(port, 10);
2832
+ const rootDir = await prompt(`Diretório raiz ${chalk.dim(`(${service.rootDirectory || "/"})`)}: `);
2833
+ if (rootDir) updates.rootDirectory = rootDir === "/" ? null : rootDir;
2834
+ const instances = await prompt(`Número de instâncias ${chalk.dim(`(${service.instanceCount})`)}: `);
2835
+ if (instances) updates.instanceCount = parseInt(instances, 10);
2836
+ const cpu = await prompt(`Limite de CPU ${chalk.dim(`(${service.cpuLimit})`)}: `);
2837
+ if (cpu) updates.cpuLimit = cpu;
2838
+ const memory = await prompt(`Limite de memória ${chalk.dim(`(${service.memoryLimit})`)}: `);
2839
+ if (memory) updates.memoryLimit = memory;
2840
+ if (Object.keys(updates).length === 0) {
2841
+ info("Nenhuma alteração realizada.");
2842
+ return;
2843
+ }
2844
+ const updateSpin = spinner("Salvando configurações...");
2845
+ await client.services.update({
2846
+ serviceId,
2847
+ ...updates
2848
+ });
2849
+ updateSpin.stop();
2850
+ success("Configurações atualizadas com sucesso!");
2851
+ info("Execute 'veloz deploy' para aplicar as mudanças.");
2852
+ } catch (error) {
2853
+ handleError(error);
2854
+ }
2855
+ });
2856
+ configCommand.command("reset").description("Resetar configurações para os padrões").option("--build", "Resetar comando de build").option("--start", "Resetar comando de start").option("--all", "Resetar todas as configurações opcionais").option("--service <service>", "Serviço alvo (chave ou nome)").action(async (options) => {
2857
+ try {
2858
+ const serviceId = await resolveServiceId(options.service);
2859
+ const client = await getClient();
2860
+ const updates = {};
2861
+ if (options.all) {
2862
+ updates.buildCommand = null;
2863
+ updates.startCommand = null;
2864
+ updates.rootDirectory = null;
2865
+ } else {
2866
+ if (options.build) updates.buildCommand = null;
2867
+ if (options.start) updates.startCommand = null;
2868
+ }
2869
+ if (Object.keys(updates).length === 0) {
2870
+ console.error(chalk.yellow("⚠ Especifique o que resetar: --build, --start, ou --all"));
2871
+ return;
2872
+ }
2873
+ const spin = spinner("Resetando configurações...");
2874
+ await client.services.update({
2875
+ serviceId,
2876
+ ...updates
2877
+ });
2878
+ spin.stop();
2879
+ success("Configurações resetadas para os padrões!");
2880
+ info("Execute 'veloz deploy' para aplicar as mudanças.");
2881
+ } catch (error) {
2882
+ handleError(error);
2883
+ }
2884
+ });
2885
+
2886
+ //#endregion
2887
+ //#region src/commands/use.ts
2888
+ const useCommand = new Command("use").description("Selecionar qual serviço usar como padrão").argument("[serviço]", "Nome ou chave do serviço").action(async (servicoArg) => {
2889
+ try {
2890
+ const config = loadConfig();
2891
+ if (!config) {
2892
+ console.error(chalk.red("\n✗ Nenhum projeto configurado. Execute 'veloz deploy' primeiro."));
2893
+ process.exit(1);
2894
+ }
2895
+ const services = Object.entries(config.services);
2896
+ if (services.length === 0) {
2897
+ console.error(chalk.red("\n✗ Nenhum serviço encontrado na configuração."));
2898
+ process.exit(1);
2899
+ }
2900
+ if (services.length === 1) {
2901
+ info("Apenas um serviço configurado — já é o padrão.");
2902
+ return;
2903
+ }
2904
+ const currentDefault = getDefaultServiceKey();
2905
+ let selectedKey;
2906
+ if (servicoArg) {
2907
+ const found = services.find(([key, service]) => key === servicoArg || service.name.toLowerCase() === servicoArg.toLowerCase());
2908
+ if (!found) {
2909
+ console.error(chalk.red(`\n✗ Serviço '${servicoArg}' não encontrado.`));
2910
+ console.log(chalk.dim("\nServiços disponíveis:"));
2911
+ services.forEach(([key, service], i) => {
2912
+ const marker = key === currentDefault ? chalk.cyan(" ← padrão") : "";
2913
+ console.log(` ${getServiceHeader(service.name, i)} ${chalk.dim(key)}${marker}`);
2914
+ });
2915
+ process.exit(1);
2916
+ }
2917
+ selectedKey = found[0];
2918
+ } else selectedKey = await promptSelect("Qual serviço usar como padrão?", services.map(([key, service]) => ({
2919
+ label: `${service.name} (${key})${key === currentDefault ? chalk.cyan(" ← atual") : ""}`,
2920
+ value: key
2921
+ })));
2922
+ setDefaultServiceKey(selectedKey);
2923
+ const selectedService = config.services[selectedKey];
2924
+ success(`Serviço padrão: ${chalk.bold(selectedService.name)} (${selectedKey})`);
2925
+ console.log(chalk.dim(`\nComandos como ${chalk.white("logs")}, ${chalk.white("env")}, ${chalk.white("config")} vão usar este serviço automaticamente.`));
2926
+ console.log(chalk.dim(`Use ${chalk.white("--service <nome>")} para sobrescrever pontualmente.`));
2927
+ } catch (error) {
2928
+ handleError(error);
2929
+ }
2930
+ });
2931
+
2932
+ //#endregion
2933
+ //#region src/index.ts
2934
+ const version = process.env.npm_package_version;
2935
+ const program = new Command().name("veloz").description("CLI da plataforma Veloz — deploy rápido para o Brasil").version(version ?? "0.0.0-beta.1");
2936
+ program.addCommand(loginCommand);
2937
+ program.addCommand(logoutCommand);
2938
+ program.addCommand(projectsCommand);
2939
+ program.addCommand(linkCommand);
2940
+ program.addCommand(deployCommand);
2941
+ program.addCommand(logsCommand);
2942
+ program.addCommand(envCommand);
2943
+ program.addCommand(domainsCommand);
2944
+ program.addCommand(configCommand);
2945
+ program.addCommand(useCommand);
2946
+ program.parse();
2947
+
2948
+ //#endregion
2949
+ export { };