onveloz 0.0.0-beta.1 → 0.0.0-beta.10

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 +2386 -1447
  2. package/package.json +16 -14
package/dist/index.mjs CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "@commander-js/extra-typings";
3
- import chalk from "chalk";
3
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
4
4
  import { exec, execSync } from "node:child_process";
5
+ import { dirname, join, relative, resolve } from "node:path";
6
+ import { z } from "zod";
7
+ import chalk from "chalk";
5
8
  import { homedir, platform, tmpdir } from "node:os";
6
9
  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
10
  import * as readline from "node:readline";
10
11
  import { createInterface } from "node:readline";
11
12
  import ora from "ora";
@@ -13,11 +14,315 @@ import { createAuthClient } from "better-auth/client";
13
14
  import { deviceAuthorizationClient } from "better-auth/client/plugins";
14
15
  import { createORPCClient } from "@orpc/client";
15
16
  import { RPCLink } from "@orpc/client/fetch";
16
- import { z } from "zod";
17
- import { mkdtemp, readdir, rm, stat } from "node:fs/promises";
17
+ import { link, mkdir, mkdtemp, readdir, rm, stat } from "node:fs/promises";
18
18
  import ignore from "ignore";
19
19
  import tar from "tar";
20
20
 
21
+ //#region src/lib/output.ts
22
+ let _outputMode = null;
23
+ function getOutputMode() {
24
+ if (_outputMode) return _outputMode;
25
+ return detectOutputMode();
26
+ }
27
+ function detectOutputMode() {
28
+ if (process.env.VELOZ_OUTPUT === "json") return "json";
29
+ if (process.env.VELOZ_OUTPUT === "github-actions") return "github-actions";
30
+ if (process.env.VELOZ_OUTPUT === "plain") return "plain";
31
+ if (process.env.GITHUB_ACTIONS === "true") return "github-actions";
32
+ if (process.env.CI === "true") return "plain";
33
+ if (!process.stdout.isTTY || process.env.TERM === "dumb") return "plain";
34
+ return "fancy";
35
+ }
36
+ function setOutputMode(mode) {
37
+ _outputMode = mode;
38
+ }
39
+ function isInteractive() {
40
+ return getOutputMode() === "fancy" && process.stdin.isTTY === true;
41
+ }
42
+ function isMachineReadable() {
43
+ return getOutputMode() === "json";
44
+ }
45
+ /**
46
+ * Emit structured JSON data to stdout.
47
+ * In JSON mode: outputs raw JSON object.
48
+ * In other modes: no-op (callers should use regular output helpers).
49
+ */
50
+ function emitData(data) {
51
+ if (isMachineReadable()) process.stdout.write(JSON.stringify(data) + "\n");
52
+ }
53
+ /**
54
+ * Open a collapsible group (GitHub Actions) or section header.
55
+ */
56
+ function startGroup(name) {
57
+ if (getOutputMode() === "github-actions") process.stdout.write(`::group::${name}\n`);
58
+ }
59
+ /**
60
+ * Close a collapsible group (GitHub Actions).
61
+ */
62
+ function endGroup() {
63
+ if (getOutputMode() === "github-actions") process.stdout.write("::endgroup::\n");
64
+ }
65
+
66
+ //#endregion
67
+ //#region ../../packages/config/veloz-config.ts
68
+ const ServiceTypeSchema = z.enum(["web", "static"]);
69
+ const PackageManagerSchema = z.enum([
70
+ "npm",
71
+ "yarn",
72
+ "pnpm",
73
+ "bun",
74
+ "auto"
75
+ ]);
76
+ const BuildConfigSchema = z.object({
77
+ command: z.string().nullable().optional(),
78
+ nodeVersion: z.string().regex(/^[0-9]+(\.[0-9]+){0,2}(\.x)?$/).default("20").optional(),
79
+ nixpkgsArchive: z.string().regex(/^[a-f0-9]{40}$/).optional(),
80
+ packageManager: PackageManagerSchema.default("auto").optional(),
81
+ installCommand: z.string().nullable().optional(),
82
+ outputDir: z.string().nullable().optional(),
83
+ aptPackages: z.array(z.string().regex(/^[a-z0-9][a-z0-9.+\-]+$/, "Nome de pacote inválido")).optional()
84
+ });
85
+ const RuntimeConfigSchema = z.object({
86
+ command: z.string().nullable().optional(),
87
+ port: z.number().min(1).max(65535).default(3e3).optional(),
88
+ healthCheck: z.object({
89
+ path: z.string().default("/").optional(),
90
+ interval: z.number().default(30).optional(),
91
+ timeout: z.number().default(10).optional()
92
+ }).optional()
93
+ });
94
+ const ResourcesSchema = z.object({
95
+ instances: z.number().min(1).max(10).default(1).optional(),
96
+ cpu: z.string().regex(/^[0-9]+(\.[0-9]+)?|[0-9]+m$/).default("500m").optional(),
97
+ memory: z.string().regex(/^[0-9]+(Mi|Gi)$/).default("512Mi").optional(),
98
+ autoscale: z.object({
99
+ enabled: z.boolean().default(false).optional(),
100
+ minInstances: z.number().min(1).default(1).optional(),
101
+ maxInstances: z.number().min(1).max(20).default(3).optional(),
102
+ targetCPU: z.number().min(10).max(90).default(70).optional()
103
+ }).optional()
104
+ });
105
+ const EnvVarDefinitionSchema = z.object({
106
+ description: z.string().optional(),
107
+ required: z.boolean().default(false).optional(),
108
+ example: z.string().optional()
109
+ });
110
+ const ServiceConfigSchema = z.object({
111
+ id: z.string(),
112
+ name: z.string(),
113
+ type: ServiceTypeSchema.default("web"),
114
+ root: z.string().default("."),
115
+ branch: z.string().optional(),
116
+ build: BuildConfigSchema.optional(),
117
+ runtime: RuntimeConfigSchema.optional(),
118
+ env: z.record(z.string().regex(/^[A-Z][A-Z0-9_]*$/), EnvVarDefinitionSchema).optional(),
119
+ resources: ResourcesSchema.optional()
120
+ });
121
+ const ProjectConfigSchema = z.object({
122
+ id: z.string(),
123
+ name: z.string(),
124
+ slug: z.string().regex(/^[a-z0-9-]+$/).optional()
125
+ });
126
+ const ServiceDefaultsSchema = z.object({
127
+ type: ServiceTypeSchema.optional(),
128
+ branch: z.string().optional(),
129
+ build: BuildConfigSchema.optional(),
130
+ runtime: RuntimeConfigSchema.optional(),
131
+ resources: ResourcesSchema.optional()
132
+ });
133
+ const EnvironmentServiceOverrideSchema = z.object({
134
+ id: z.string(),
135
+ name: z.string().optional()
136
+ });
137
+ const EnvironmentOverrideSchema = z.object({
138
+ project: ProjectConfigSchema,
139
+ services: z.record(z.string(), EnvironmentServiceOverrideSchema)
140
+ });
141
+ const VelozConfigSchema = z.object({
142
+ $schema: z.string().optional(),
143
+ version: z.literal("1.0"),
144
+ project: ProjectConfigSchema,
145
+ services: z.record(z.string(), ServiceConfigSchema),
146
+ defaults: ServiceDefaultsSchema.optional(),
147
+ environments: z.record(z.string(), EnvironmentOverrideSchema).optional(),
148
+ created: z.string().datetime().optional(),
149
+ updated: z.string().datetime().optional()
150
+ });
151
+ function mergeServiceWithDefaults(service, defaults) {
152
+ if (!defaults) return service;
153
+ return {
154
+ ...service,
155
+ type: service.type ?? defaults.type ?? "web",
156
+ branch: service.branch ?? defaults.branch,
157
+ build: {
158
+ ...defaults.build,
159
+ ...service.build
160
+ },
161
+ runtime: {
162
+ ...defaults.runtime,
163
+ ...service.runtime
164
+ },
165
+ resources: {
166
+ ...defaults.resources,
167
+ ...service.resources
168
+ }
169
+ };
170
+ }
171
+ function parseVelozConfig(data) {
172
+ return VelozConfigSchema.parse(data);
173
+ }
174
+ function resolveConfigForEnv(config, env) {
175
+ const envOverride = config.environments?.[env];
176
+ if (!envOverride) return null;
177
+ const resolvedServices = {};
178
+ for (const [key, service] of Object.entries(config.services)) {
179
+ const override = envOverride.services[key];
180
+ if (override) resolvedServices[key] = {
181
+ ...service,
182
+ id: override.id,
183
+ name: override.name ?? service.name
184
+ };
185
+ }
186
+ return {
187
+ ...config,
188
+ project: envOverride.project,
189
+ services: resolvedServices,
190
+ environments: void 0
191
+ };
192
+ }
193
+
194
+ //#endregion
195
+ //#region src/lib/link.ts
196
+ const CONFIG_FILE$1 = "veloz.json";
197
+ const LOCAL_CONFIG_FILE = "veloz.local.json";
198
+ let _activeEnv;
199
+ function setActiveEnv(env) {
200
+ _activeEnv = env;
201
+ }
202
+ function getActiveEnv() {
203
+ return _activeEnv ?? process.env.VELOZ_ENV;
204
+ }
205
+ /**
206
+ * Find the root directory (git root or monorepo root)
207
+ */
208
+ function findProjectRoot(startPath = process.cwd()) {
209
+ let currentPath = resolve(startPath);
210
+ const root = resolve("/");
211
+ while (currentPath !== root) {
212
+ if (existsSync(join(currentPath, ".git"))) return currentPath;
213
+ if (existsSync(join(currentPath, "pnpm-workspace.yaml")) || existsSync(join(currentPath, "lerna.json")) || existsSync(join(currentPath, "rush.json")) || existsSync(join(currentPath, "nx.json"))) return currentPath;
214
+ const pkgJsonPath = join(currentPath, "package.json");
215
+ if (existsSync(pkgJsonPath)) try {
216
+ if (JSON.parse(readFileSync(pkgJsonPath, "utf-8")).workspaces) return currentPath;
217
+ } catch {}
218
+ const parentPath = resolve(currentPath, "..");
219
+ if (parentPath === currentPath) break;
220
+ currentPath = parentPath;
221
+ }
222
+ return process.cwd();
223
+ }
224
+ /**
225
+ * Get the config file name (veloz.local.json when VELOZ_API_URL is set, veloz.json otherwise)
226
+ */
227
+ function getConfigFileName() {
228
+ return process.env.VELOZ_API_URL ? LOCAL_CONFIG_FILE : CONFIG_FILE$1;
229
+ }
230
+ /**
231
+ * Get the path to veloz.json (or veloz.local.json when using custom API URL)
232
+ */
233
+ function getConfigPath() {
234
+ return join(findProjectRoot(), getConfigFileName());
235
+ }
236
+ /**
237
+ * Load the veloz.json config from project root (without environment resolution)
238
+ */
239
+ function loadRawConfig() {
240
+ const path = getConfigPath();
241
+ if (!existsSync(path)) return null;
242
+ try {
243
+ const raw = readFileSync(path, "utf-8");
244
+ return parseVelozConfig(JSON.parse(raw));
245
+ } catch (error) {
246
+ console.error(`Error loading ${getConfigFileName()}:`, error);
247
+ return null;
248
+ }
249
+ }
250
+ /**
251
+ * Load the veloz.json config, resolving environment overrides if --env is active.
252
+ * Returns the env-resolved config when an env is active and configured,
253
+ * or the base config when no env is active.
254
+ * Returns null if the config file doesn't exist or the active env isn't configured.
255
+ */
256
+ function loadConfig() {
257
+ const config = loadRawConfig();
258
+ if (!config) return null;
259
+ const env = getActiveEnv();
260
+ if (env) return resolveConfigForEnv(config, env);
261
+ return config;
262
+ }
263
+ /**
264
+ * Save the veloz.json config to project root
265
+ */
266
+ function saveConfig(config) {
267
+ const path = getConfigPath();
268
+ const configWithSchema = {
269
+ $schema: "https://onveloz.com/schemas/veloz-config.schema.json",
270
+ ...config
271
+ };
272
+ writeFileSync(path, JSON.stringify(configWithSchema, null, 2), "utf-8");
273
+ }
274
+ /**
275
+ * Require config to exist, throw if not found
276
+ */
277
+ function requireConfig() {
278
+ const config = loadConfig();
279
+ if (!config) throw new Error(`No ${getConfigFileName()} found in project root. Run 'veloz init' or 'veloz deploy' to set up your project.`);
280
+ return config;
281
+ }
282
+ /**
283
+ * Check if current directory is a git repository
284
+ */
285
+ function isGitRepo() {
286
+ try {
287
+ execSync("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
288
+ return true;
289
+ } catch {
290
+ return false;
291
+ }
292
+ }
293
+ /**
294
+ * Get git remote origin info
295
+ */
296
+ function getGitRemote() {
297
+ try {
298
+ const url = execSync("git remote get-url origin", { stdio: "pipe" }).toString().trim();
299
+ const httpsMatch = url.match(/github\.com\/([^/]+)\/([^/.]+)/);
300
+ if (httpsMatch) return {
301
+ owner: httpsMatch[1],
302
+ repo: httpsMatch[2]
303
+ };
304
+ const sshMatch = url.match(/github\.com:([^/]+)\/([^/.]+)/);
305
+ if (sshMatch) return {
306
+ owner: sshMatch[1],
307
+ repo: sshMatch[2]
308
+ };
309
+ return null;
310
+ } catch {
311
+ return null;
312
+ }
313
+ }
314
+ /**
315
+ * Get current git branch
316
+ */
317
+ function getGitBranch() {
318
+ try {
319
+ return execSync("git rev-parse --abbrev-ref HEAD", { stdio: "pipe" }).toString().trim();
320
+ } catch {
321
+ return "main";
322
+ }
323
+ }
324
+
325
+ //#endregion
21
326
  //#region src/lib/config.ts
22
327
  const CONFIG_DIR = join(homedir(), ".veloz");
23
328
  const ENV_API_URL = process.env.VELOZ_API_URL;
@@ -26,21 +331,22 @@ function getConfigFile() {
26
331
  if (!ENV_API_URL) return join(CONFIG_DIR, "config.json");
27
332
  return join(CONFIG_DIR, `config.${createHash("md5").update(ENV_API_URL).digest("hex").slice(0, 8)}.json`);
28
333
  }
29
- const CONFIG_FILE$1 = getConfigFile();
334
+ const CONFIG_FILE = getConfigFile();
30
335
  function ensureConfigDir() {
31
336
  if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
32
337
  }
33
338
  function loadConfig$1() {
34
- if (!existsSync(CONFIG_FILE$1)) return {
339
+ if (!existsSync(CONFIG_FILE)) return {
35
340
  apiKey: "",
36
341
  apiUrl: DEFAULT_API_URL
37
342
  };
38
343
  try {
39
- const raw = readFileSync(CONFIG_FILE$1, "utf-8");
344
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
40
345
  const parsed = JSON.parse(raw);
41
346
  return {
42
347
  apiKey: parsed.apiKey ?? "",
43
- apiUrl: ENV_API_URL ?? parsed.apiUrl ?? DEFAULT_API_URL
348
+ apiUrl: ENV_API_URL ?? parsed.apiUrl ?? DEFAULT_API_URL,
349
+ organizationId: parsed.organizationId
44
350
  };
45
351
  } catch {
46
352
  return {
@@ -54,20 +360,28 @@ function saveConfig$1(config) {
54
360
  const existing = loadConfig$1();
55
361
  const merged = {
56
362
  apiKey: config.apiKey ?? existing.apiKey,
57
- apiUrl: config.apiUrl ?? existing.apiUrl
363
+ apiUrl: config.apiUrl ?? existing.apiUrl,
364
+ organizationId: config.organizationId ?? existing.organizationId
58
365
  };
59
- writeFileSync(CONFIG_FILE$1, JSON.stringify(merged, null, 2), "utf-8");
366
+ writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), "utf-8");
60
367
  }
61
368
  function deleteConfig() {
62
- if (existsSync(CONFIG_FILE$1)) unlinkSync(CONFIG_FILE$1);
369
+ if (existsSync(CONFIG_FILE)) unlinkSync(CONFIG_FILE);
63
370
  }
64
371
  function isAuthenticated() {
65
372
  return loadConfig$1().apiKey.length > 0;
66
373
  }
67
374
  let envApiUrlLogged = false;
68
- async function requireAuth() {
375
+ async function requireAuth(options) {
376
+ const envApiKey = process.env.VELOZ_API_KEY;
377
+ if (envApiKey) return {
378
+ apiKey: envApiKey,
379
+ apiUrl: ENV_API_URL ?? DEFAULT_API_URL
380
+ };
381
+ if ("VELOZ_API_KEY" in process.env) throw new Error("VELOZ_API_KEY está definida mas vazia. Verifique o valor do secret.");
69
382
  let config = loadConfig$1();
70
383
  if (!config.apiKey) {
384
+ if (options?.nonInteractive) throw new Error("Nenhuma autenticação encontrada. Defina VELOZ_API_KEY ou execute `veloz login`.");
71
385
  await performLogin(config.apiUrl);
72
386
  config = loadConfig$1();
73
387
  }
@@ -87,10 +401,28 @@ function isRateLimitError(error) {
87
401
  return null;
88
402
  }
89
403
  function handleError(error) {
404
+ const mode = getOutputMode();
90
405
  if (error instanceof Error) {
91
406
  const orpcError = error;
92
407
  if (orpcError.code) {
93
- const message = orpcError.data?.message ?? orpcError.message ?? {
408
+ const orpcData = orpcError.data;
409
+ if (orpcData?.code === "ACCESS_NOT_APPROVED") {
410
+ const gateMsg = "Sua conta ainda não foi aprovada para usar a Veloz.";
411
+ const gateHint = "Acesse app.onveloz.com para solicitar acesso.";
412
+ if (mode === "json") emitData({
413
+ type: "error",
414
+ code: "ACCESS_NOT_APPROVED",
415
+ message: gateMsg,
416
+ hint: gateHint
417
+ });
418
+ else if (mode === "github-actions") process.stdout.write(`::error::${gateMsg} ${gateHint}\n`);
419
+ else {
420
+ console.error(chalk.red(`\n✗ ${gateMsg}`));
421
+ console.error(chalk.yellow(` ${gateHint}`));
422
+ }
423
+ process.exit(1);
424
+ }
425
+ const message = orpcData?.message ?? orpcError.message ?? {
94
426
  NOT_FOUND: "Recurso não encontrado.",
95
427
  FORBIDDEN: "Sem permissão para acessar este recurso.",
96
428
  CONFLICT: "Conflito — recurso já existe.",
@@ -99,51 +431,189 @@ function handleError(error) {
99
431
  INTERNAL_SERVER_ERROR: "Erro interno do servidor. Tente novamente.",
100
432
  TOO_MANY_REQUESTS: "Tente novamente em alguns segundos."
101
433
  }[orpcError.code] ?? "Erro desconhecido.";
102
- console.error(chalk.red(`\n✗ Erro: ${message}`));
434
+ if (mode === "json") emitData({
435
+ type: "error",
436
+ code: orpcError.code,
437
+ message
438
+ });
439
+ else if (mode === "github-actions") process.stdout.write(`::error::${message}\n`);
440
+ else console.error(chalk.red(`\n✗ Erro: ${message}`));
103
441
  process.exit(1);
104
442
  }
105
443
  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."));
444
+ const message = "Não foi possível conectar ao servidor Veloz.";
445
+ const hint = "Verifique se o servidor está rodando e a URL está correta.";
446
+ if (mode === "json") emitData({
447
+ type: "error",
448
+ code: "CONNECTION_ERROR",
449
+ message,
450
+ hint
451
+ });
452
+ else if (mode === "github-actions") process.stdout.write(`::error::${message} ${hint}\n`);
453
+ else {
454
+ console.error(chalk.red(`\n✗ Erro de conexão: ${message}`));
455
+ console.error(chalk.yellow(` ${hint}`));
456
+ }
108
457
  process.exit(1);
109
458
  }
110
- console.error(chalk.red(`\n✗ Erro: ${error.message}`));
459
+ if (mode === "json") emitData({
460
+ type: "error",
461
+ message: error.message
462
+ });
463
+ else if (mode === "github-actions") process.stdout.write(`::error::${error.message}\n`);
464
+ else console.error(chalk.red(`\n✗ Erro: ${error.message}`));
111
465
  process.exit(1);
112
466
  }
113
- console.error(chalk.red("\n✗ Erro inesperado."));
467
+ if (mode === "json") emitData({
468
+ type: "error",
469
+ message: "Erro inesperado."
470
+ });
471
+ else if (mode === "github-actions") process.stdout.write("::error::Erro inesperado.\n");
472
+ else console.error(chalk.red("\n✗ Erro inesperado."));
114
473
  process.exit(1);
115
474
  }
116
475
  function success(message) {
117
- console.log(chalk.green(`\n✓ ${message}`));
476
+ const mode = getOutputMode();
477
+ if (mode === "json") emitData({
478
+ type: "success",
479
+ message
480
+ });
481
+ else if (mode === "github-actions") process.stdout.write(`✓ ${message}\n`);
482
+ else console.log(chalk.green(`\n✓ ${message}`));
118
483
  }
119
484
  function info(message) {
120
- console.log(chalk.cyan(`ℹ ${message}`));
485
+ const mode = getOutputMode();
486
+ if (mode === "json") emitData({
487
+ type: "info",
488
+ message
489
+ });
490
+ else if (mode === "github-actions") process.stdout.write(`${message}\n`);
491
+ else console.log(chalk.cyan(`ℹ ${message}`));
492
+ }
493
+ function warn$1(message) {
494
+ const mode = getOutputMode();
495
+ if (mode === "json") emitData({
496
+ type: "warning",
497
+ message
498
+ });
499
+ else if (mode === "github-actions") process.stdout.write(`::warning::${message}\n`);
500
+ else console.log(chalk.yellow(`⚠ ${message}`));
121
501
  }
122
- function warn(message) {
123
- console.log(chalk.yellow(`⚠ ${message}`));
502
+ /**
503
+ * Emit structured data in JSON mode, or run the display callback otherwise.
504
+ *
505
+ * Usage:
506
+ * output({ type: "user", name, email }, () => {
507
+ * console.log(` Nome: ${name}`);
508
+ * console.log(` Email: ${email}`);
509
+ * });
510
+ */
511
+ function output(data, display) {
512
+ if (isMachineReadable()) emitData(data);
513
+ else display();
124
514
  }
125
515
  function spinner(text) {
516
+ const mode = getOutputMode();
517
+ if (mode === "json" || mode === "github-actions" || mode === "plain") {
518
+ const noopSpinner = ora({
519
+ text,
520
+ isEnabled: false
521
+ });
522
+ const originalStop = noopSpinner.stop.bind(noopSpinner);
523
+ const originalFail = noopSpinner.fail.bind(noopSpinner);
524
+ noopSpinner.start = function(newText) {
525
+ if (newText) this.text = newText;
526
+ if (mode === "github-actions") process.stdout.write(`${this.text}\n`);
527
+ return this;
528
+ };
529
+ noopSpinner.stop = function() {
530
+ return originalStop();
531
+ };
532
+ noopSpinner.fail = function(newText) {
533
+ const msg = newText ?? this.text;
534
+ if (mode === "json") emitData({
535
+ type: "error",
536
+ message: msg
537
+ });
538
+ else if (mode === "github-actions") process.stdout.write(`::error::${msg}\n`);
539
+ else console.error(msg);
540
+ return originalFail(newText);
541
+ };
542
+ return noopSpinner;
543
+ }
126
544
  return ora({
127
545
  text,
128
546
  color: "cyan"
129
547
  }).start();
130
548
  }
131
- function printTable(headers, rows) {
549
+ /**
550
+ * Print a table. Pass `rawData` for structured JSON output — commands
551
+ * build display rows with chalk as before, and raw objects for machines.
552
+ *
553
+ * Usage:
554
+ * printTable(
555
+ * ["Nome", "Email"],
556
+ * users.map(u => [chalk.bold(u.name), u.email]),
557
+ * users.map(u => ({ name: u.name, email: u.email })),
558
+ * );
559
+ */
560
+ function printTable(headers, rows, rawData) {
561
+ const mode = getOutputMode();
562
+ if (mode === "json") {
563
+ if (rawData) emitData({
564
+ type: "table",
565
+ data: rawData
566
+ });
567
+ else emitData({
568
+ type: "table",
569
+ data: rows.map((row) => {
570
+ const obj = {};
571
+ headers.forEach((h, i) => {
572
+ obj[h] = stripAnsi(row[i] ?? "");
573
+ });
574
+ return obj;
575
+ })
576
+ });
577
+ return;
578
+ }
579
+ if (mode === "github-actions" || mode === "plain") {
580
+ const cleanHeaders = headers.map(stripAnsi);
581
+ const cleanRows = rows.map((row) => row.map(stripAnsi));
582
+ const colWidths$1 = cleanHeaders.map((h, i) => {
583
+ const maxRow = cleanRows.reduce((max, row) => Math.max(max, (row[i] ?? "").length), 0);
584
+ return Math.max(h.length, maxRow);
585
+ });
586
+ const headerLine$1 = cleanHeaders.map((h, i) => h.padEnd(colWidths$1[i])).join(" ");
587
+ const separator$1 = colWidths$1.map((w) => "-".repeat(w)).join("--");
588
+ process.stdout.write(`\n${headerLine$1}\n`);
589
+ process.stdout.write(`${separator$1}\n`);
590
+ for (const row of cleanRows) {
591
+ const line = row.map((cell, i) => cell.padEnd(colWidths$1[i])).join(" ");
592
+ process.stdout.write(`${line}\n`);
593
+ }
594
+ process.stdout.write("\n");
595
+ return;
596
+ }
132
597
  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);
598
+ const maxRow = rows.reduce((max, row) => Math.max(max, stripAnsi(row[i] ?? "").length), 0);
599
+ return Math.max(stripAnsi(h).length, maxRow);
135
600
  });
136
601
  const headerLine = headers.map((h, i) => chalk.bold(h.padEnd(colWidths[i]))).join(" ");
137
602
  const separator = colWidths.map((w) => "─".repeat(w)).join("──");
138
603
  console.log(`\n${headerLine}`);
139
604
  console.log(chalk.dim(separator));
140
605
  for (const row of rows) {
141
- const line = row.map((cell, i) => cell.padEnd(colWidths[i])).join(" ");
606
+ const line = row.map((cell, i) => {
607
+ const visible = stripAnsi(cell);
608
+ const padding = colWidths[i] - visible.length;
609
+ return cell + " ".repeat(Math.max(0, padding));
610
+ }).join(" ");
142
611
  console.log(line);
143
612
  }
144
613
  console.log();
145
614
  }
146
615
  async function prompt(question) {
616
+ if (!isInteractive()) return "";
147
617
  const rl = createInterface({
148
618
  input: process.stdin,
149
619
  output: process.stdout
@@ -156,11 +626,17 @@ async function prompt(question) {
156
626
  });
157
627
  }
158
628
  async function promptConfirm(question, defaultYes = true) {
629
+ if (!isInteractive()) return defaultYes;
159
630
  const answer = await prompt(`${question} ${defaultYes ? "(S/n)" : "(s/N)"}`);
160
631
  if (answer === "") return defaultYes;
161
632
  return answer.toLowerCase().startsWith("s") || answer.toLowerCase().startsWith("y");
162
633
  }
163
634
  async function promptSelect(question, options) {
635
+ if (!isInteractive()) {
636
+ if (options.length > 0) return options[0].value;
637
+ console.error(chalk.red("Nenhuma opção disponível no modo não-interativo."));
638
+ process.exit(1);
639
+ }
164
640
  console.log(chalk.cyan(`\n${question}\n`));
165
641
  for (let i = 0; i < options.length; i++) console.log(chalk.white(` ${chalk.bold(`${i + 1})`)} ${options[i].label}`));
166
642
  const answer = await prompt("\nEscolha (número):");
@@ -172,6 +648,7 @@ async function promptSelect(question, options) {
172
648
  return options[index].value;
173
649
  }
174
650
  async function promptMultiSelect(question, options) {
651
+ if (!isInteractive()) return options.map((o) => o.value);
175
652
  console.log(chalk.cyan(`\n${question}\n`));
176
653
  for (let i = 0; i < options.length; i++) console.log(chalk.white(` ${chalk.bold(`${i + 1})`)} ${options[i].label}`));
177
654
  console.log(chalk.dim(`\n * = todos`));
@@ -184,6 +661,10 @@ async function promptMultiSelect(question, options) {
184
661
  }
185
662
  return indices.map((i) => options[i].value);
186
663
  }
664
+ const ANSI_REGEX = /\x1B\[[0-9;]*m/g;
665
+ function stripAnsi(str) {
666
+ return str.replace(ANSI_REGEX, "");
667
+ }
187
668
 
188
669
  //#endregion
189
670
  //#region src/commands/login.ts
@@ -206,15 +687,21 @@ async function performLogin(apiUrl) {
206
687
  }
207
688
  s.stop();
208
689
  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);
690
+ output({
691
+ type: "auth_code",
692
+ userCode: data.user_code,
693
+ verificationUrl
694
+ }, () => {
695
+ console.log();
696
+ info("Abrindo navegador para autenticação...");
697
+ console.log();
698
+ console.log(chalk.white(` Código de verificação: ${chalk.bold.cyan(data.user_code)}`));
699
+ console.log();
700
+ console.log(chalk.dim(" Se o navegador não abrir, acesse manualmente:"));
701
+ console.log(chalk.dim(` ${verificationUrl}`));
702
+ console.log();
703
+ openBrowser(verificationUrl);
704
+ });
218
705
  const pollSpinner = spinner("Aguardando autorização no navegador...");
219
706
  const token = await pollForToken(authClient, data.device_code, data.interval || 5);
220
707
  if (!token) {
@@ -229,18 +716,17 @@ async function performLogin(apiUrl) {
229
716
  success("Autenticado com sucesso!");
230
717
  } catch (err) {
231
718
  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);
719
+ if (err instanceof Error) handleLoginError(err);
720
+ throw err;
239
721
  }
240
722
  }
723
+ function handleLoginError(err) {
724
+ if (err.message.includes("fetch") || err.message.includes("ECONNREFUSED")) throw err;
725
+ throw err;
726
+ }
241
727
  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
728
  if (isAuthenticated()) {
243
- warn("Você já está autenticado.");
729
+ warn$1("Você já está autenticado.");
244
730
  if ((await prompt("Deseja fazer login novamente? (s/N)")).toLowerCase() !== "s") process.exit(0);
245
731
  }
246
732
  const config = loadConfig$1();
@@ -278,7 +764,7 @@ async function pollForToken(authClient, deviceCode, interval) {
278
764
  }
279
765
  const logoutCommand = new Command("logout").description("Encerrar sessão na plataforma Veloz").action(() => {
280
766
  if (!isAuthenticated()) {
281
- warn("Você não está autenticado.");
767
+ warn$1("Você não está autenticado.");
282
768
  return;
283
769
  }
284
770
  deleteConfig();
@@ -288,660 +774,329 @@ const logoutCommand = new Command("logout").description("Encerrar sessão na pla
288
774
  //#endregion
289
775
  //#region ../../packages/api/src/client.ts
290
776
  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
- }
777
+ return createORPCClient(new RPCLink({
778
+ url: `${baseUrl}/rpc`,
779
+ headers: headers ?? (() => ({})),
780
+ fetch: (request, init) => {
781
+ return globalThis.fetch(request, {
782
+ ...init,
783
+ credentials: "include"
784
+ });
785
+ }
786
+ }));
538
787
  }
539
- /**
540
- * Get current git branch
541
- */
542
- function getGitBranch() {
788
+
789
+ //#endregion
790
+ //#region src/lib/client.ts
791
+ async function getClient() {
792
+ const authConfig = await requireAuth();
793
+ const projectConfig = loadConfig();
794
+ return createClient(authConfig.apiUrl, () => {
795
+ const headers = { Authorization: `Bearer ${authConfig.apiKey}` };
796
+ if (projectConfig?.project?.id) headers["X-Project-Id"] = projectConfig.project.id;
797
+ if (!projectConfig?.project?.id && authConfig.organizationId) headers["X-Organization-Id"] = authConfig.organizationId;
798
+ return headers;
799
+ });
800
+ }
801
+
802
+ //#endregion
803
+ //#region src/commands/projects.ts
804
+ const projectsCommand = new Command("projects").alias("projetos").description("Gerenciar projetos");
805
+ projectsCommand.command("list").alias("listar").description("Listar todos os projetos").action(async () => {
806
+ const spin = spinner("Carregando projetos...");
543
807
  try {
544
- return execSync("git rev-parse --abbrev-ref HEAD", { stdio: "pipe" }).toString().trim();
545
- } catch {
546
- return "main";
808
+ const projects = await (await getClient()).projects.list();
809
+ spin.stop();
810
+ if (projects.length === 0) {
811
+ info("Nenhum projeto encontrado. Crie um pelo dashboard.");
812
+ return;
813
+ }
814
+ printTable([
815
+ "ID",
816
+ "Nome",
817
+ "Slug",
818
+ "Repo GitHub",
819
+ "Criado em"
820
+ ], projects.map((p) => [
821
+ p.id,
822
+ p.name,
823
+ p.slug,
824
+ p.githubRepoOwner && p.githubRepoName ? `${p.githubRepoOwner}/${p.githubRepoName}` : "—",
825
+ new Date(p.createdAt).toLocaleDateString("pt-BR")
826
+ ]), projects.map((p) => ({
827
+ id: p.id,
828
+ name: p.name,
829
+ slug: p.slug,
830
+ githubRepo: p.githubRepoOwner && p.githubRepoName ? `${p.githubRepoOwner}/${p.githubRepoName}` : null,
831
+ createdAt: p.createdAt
832
+ })));
833
+ } catch (error) {
834
+ spin.stop();
835
+ handleError(error);
547
836
  }
548
- }
837
+ });
549
838
 
550
839
  //#endregion
551
840
  //#region src/commands/link.ts
552
841
  const linkCommand = new Command("link").description("Verificar vínculo do projeto com Veloz").action(async () => {
553
842
  const config = loadConfig();
554
843
  if (!config) {
555
- warn("Nenhum projeto configurado. Execute 'veloz deploy' para configurar seu projeto.");
844
+ warn$1("Nenhum projeto configurado. Execute 'veloz deploy' para configurar seu projeto.");
556
845
  return;
557
846
  }
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
847
  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.");
848
+ output({
849
+ type: "link",
850
+ project: {
851
+ id: config.project.id,
852
+ name: config.project.name
853
+ },
854
+ services: services.map(([key, service]) => ({
855
+ key,
856
+ id: service.id,
857
+ name: service.name,
858
+ type: service.type
859
+ }))
860
+ }, () => {
861
+ console.log(chalk.bold("\n🔗 Projeto Vinculado\n"));
862
+ console.log(` ${chalk.bold("Projeto:")} ${chalk.cyan(config.project.name)}`);
863
+ console.log(` ${chalk.bold("ID:")} ${chalk.dim(config.project.id)}`);
864
+ if (services.length > 0) {
865
+ console.log(`\n ${chalk.bold("Serviços:")}`);
866
+ services.forEach(([key, service]) => {
867
+ const type = service.type === "web" ? "Serviço Web" : "Site Estático";
868
+ console.log(` • ${chalk.cyan(service.name)} (${key}) - ${type}`);
869
+ });
870
+ }
871
+ console.log();
872
+ info("Use 'veloz deploy' para fazer deploy ou atualizar serviços.");
873
+ });
571
874
  });
572
875
 
573
876
  //#endregion
574
877
  //#region ../../packages/api/src/lib/framework-detector.ts
575
- const FRAMEWORK_RULES = [
576
- {
878
+ function safeParsePkg(content) {
879
+ try {
880
+ return JSON.parse(content);
881
+ } catch {
882
+ return null;
883
+ }
884
+ }
885
+ function pmRun(pm, script) {
886
+ if (pm === "yarn") return `yarn ${script}`;
887
+ return `${pm} run ${script}`;
888
+ }
889
+ function detectFramework(pkgJsonStr, pm) {
890
+ const pkg = safeParsePkg(pkgJsonStr);
891
+ if (!pkg) return null;
892
+ const allDeps = {
893
+ ...pkg.dependencies,
894
+ ...pkg.devDependencies
895
+ };
896
+ const hasReact = !!allDeps["react"];
897
+ if (allDeps["next"]) return {
577
898
  name: "nextjs",
578
899
  label: "Next.js",
579
900
  type: "WEB",
580
- match: (deps) => "next" in deps,
581
- buildCommand: "npm run build",
582
- startCommand: "npm start",
901
+ buildCommand: pmRun(pm, "build"),
902
+ startCommand: pmRun(pm, "start"),
583
903
  outputDir: ".next",
584
904
  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
- {
905
+ };
906
+ if (allDeps["nuxt"] || allDeps["nuxt3"]) return {
597
907
  name: "nuxt",
598
908
  label: "Nuxt",
599
909
  type: "WEB",
600
- match: (deps) => "nuxt" in deps,
601
- buildCommand: "npm run build",
602
- startCommand: "npm start",
603
- outputDir: ".nuxt",
910
+ buildCommand: pmRun(pm, "build"),
911
+ startCommand: pmRun(pm, "start"),
912
+ outputDir: ".output",
604
913
  port: 3e3
605
- },
606
- {
914
+ };
915
+ if (allDeps["@remix-run/node"] || allDeps["@remix-run/react"]) return {
607
916
  name: "remix",
608
917
  label: "Remix",
609
918
  type: "WEB",
610
- match: (deps) => "@remix-run/node" in deps,
611
- buildCommand: "npm run build",
612
- startCommand: "npm start",
919
+ buildCommand: pmRun(pm, "build"),
920
+ startCommand: pmRun(pm, "start"),
613
921
  outputDir: "build",
614
922
  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
- {
923
+ };
924
+ if (allDeps["astro"]) return {
627
925
  name: "astro",
628
926
  label: "Astro",
629
927
  type: "STATIC",
630
- match: (deps) => "astro" in deps,
631
- buildCommand: "npm run build",
928
+ buildCommand: pmRun(pm, "build"),
632
929
  startCommand: null,
633
930
  outputDir: "dist",
634
931
  port: 3e3
635
- },
636
- {
932
+ };
933
+ if (allDeps["@sveltejs/kit"]) return {
934
+ name: "sveltekit",
935
+ label: "SvelteKit",
936
+ type: "WEB",
937
+ buildCommand: pmRun(pm, "build"),
938
+ startCommand: pmRun(pm, "preview"),
939
+ outputDir: "build",
940
+ port: 3e3
941
+ };
942
+ if (allDeps["gatsby"]) return {
637
943
  name: "gatsby",
638
944
  label: "Gatsby",
639
945
  type: "STATIC",
640
- match: (deps) => "gatsby" in deps,
641
- buildCommand: "npm run build",
946
+ buildCommand: pmRun(pm, "build"),
642
947
  startCommand: null,
643
948
  outputDir: "public",
644
949
  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
- {
950
+ };
951
+ if (allDeps["@angular/core"]) return {
687
952
  name: "angular",
688
953
  label: "Angular",
689
- type: "STATIC",
690
- match: (deps) => "@angular/core" in deps,
691
- buildCommand: "npm run build",
692
- startCommand: null,
954
+ type: "WEB",
955
+ buildCommand: pmRun(pm, "build"),
956
+ startCommand: pmRun(pm, "start"),
693
957
  outputDir: "dist",
694
958
  port: 4200
695
- },
696
- {
959
+ };
960
+ if (allDeps["hono"]) return {
961
+ name: "hono",
962
+ label: "Hono",
963
+ type: "WEB",
964
+ buildCommand: pmRun(pm, "build"),
965
+ startCommand: pmRun(pm, "start"),
966
+ outputDir: null,
967
+ port: 3e3
968
+ };
969
+ if (allDeps["express"]) return {
697
970
  name: "express",
698
971
  label: "Express",
699
972
  type: "WEB",
700
- match: (deps) => "express" in deps,
701
- buildCommand: null,
702
- startCommand: "node index.js",
973
+ buildCommand: pmRun(pm, "build"),
974
+ startCommand: pmRun(pm, "start"),
703
975
  outputDir: null,
704
976
  port: 3e3
705
- },
706
- {
977
+ };
978
+ if (allDeps["fastify"]) return {
707
979
  name: "fastify",
708
980
  label: "Fastify",
709
981
  type: "WEB",
710
- match: (deps) => "fastify" in deps,
711
- buildCommand: null,
712
- startCommand: "node index.js",
982
+ buildCommand: pmRun(pm, "build"),
983
+ startCommand: pmRun(pm, "start"),
713
984
  outputDir: null,
714
985
  port: 3e3
715
- },
716
- {
986
+ };
987
+ if (allDeps["@nestjs/core"]) return {
717
988
  name: "nestjs",
718
989
  label: "NestJS",
719
990
  type: "WEB",
720
- match: (deps) => "@nestjs/core" in deps,
721
- buildCommand: "npm run build",
722
- startCommand: "npm run start:prod",
991
+ buildCommand: pmRun(pm, "build"),
992
+ startCommand: pmRun(pm, "start:prod"),
723
993
  outputDir: "dist",
724
994
  port: 3e3
725
- },
726
- {
727
- name: "hono",
728
- label: "Hono",
995
+ };
996
+ if (allDeps["vite"]) return {
997
+ name: hasReact ? "vite-react" : "vite",
998
+ label: hasReact ? "Vite + React" : "Vite",
999
+ type: "STATIC",
1000
+ buildCommand: pmRun(pm, "build"),
1001
+ startCommand: null,
1002
+ outputDir: "dist",
1003
+ port: 3e3
1004
+ };
1005
+ if (allDeps["react-scripts"]) return {
1006
+ name: "cra",
1007
+ label: "Create React App",
1008
+ type: "STATIC",
1009
+ buildCommand: pmRun(pm, "build"),
1010
+ startCommand: null,
1011
+ outputDir: "build",
1012
+ port: 3e3
1013
+ };
1014
+ if (pkg.scripts?.build || pkg.scripts?.start) return {
1015
+ name: "node",
1016
+ label: "Node.js",
729
1017
  type: "WEB",
730
- match: (deps) => "hono" in deps || "@hono/node-server" in deps,
731
- buildCommand: null,
732
- startCommand: "node index.js",
733
- outputDir: null,
1018
+ buildCommand: pkg.scripts?.build ? pmRun(pm, "build") : "",
1019
+ startCommand: pkg.scripts?.start ? pmRun(pm, "start") : "node index.js",
1020
+ outputDir: "dist",
734
1021
  port: 3e3
735
- }
736
- ];
1022
+ };
1023
+ return null;
1024
+ }
737
1025
  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";
1026
+ if ("pnpm-lock.yaml" in files) return "pnpm";
740
1027
  if ("yarn.lock" in files) return "yarn";
1028
+ if ("bun.lock" in files || "bun.lockb" in files) return "bun";
741
1029
  return "npm";
742
1030
  }
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 [];
1031
+ function extractEnvVars(files) {
824
1032
  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]);
1033
+ const seen = /* @__PURE__ */ new Set();
1034
+ for (const key of [
1035
+ ".env.example",
1036
+ ".env.sample",
1037
+ ".env"
1038
+ ]) {
1039
+ const content = files[key];
1040
+ if (!content) continue;
1041
+ for (const line of content.split("\n")) {
1042
+ const trimmed = line.trim();
1043
+ if (!trimmed || trimmed.startsWith("#")) continue;
1044
+ const eqIdx = trimmed.indexOf("=");
1045
+ if (eqIdx > 0) {
1046
+ const envKey = trimmed.slice(0, eqIdx).trim();
1047
+ if (!seen.has(envKey)) {
1048
+ seen.add(envKey);
1049
+ vars.push({
1050
+ key: envKey,
1051
+ value: trimmed.slice(eqIdx + 1)
1052
+ });
1053
+ }
848
1054
  }
849
- } else {
850
- const pkgPath = base + "/package.json";
851
- if (availableFiles.includes(pkgPath)) dirs.add(base);
852
1055
  }
853
1056
  }
854
- return [...dirs];
1057
+ return vars;
855
1058
  }
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
- }
1059
+ function analyzeRepo(files) {
1060
+ const packageManager = detectPackageManager$1(files);
1061
+ const envVars = extractEnvVars(files);
1062
+ const rootPkgContent = files["package.json"];
1063
+ const rootPkg = rootPkgContent ? safeParsePkg(rootPkgContent) : null;
1064
+ const isMonorepo = "pnpm-workspace.yaml" in files || !!rootPkg?.workspaces;
1065
+ const framework = rootPkgContent ? detectFramework(rootPkgContent, packageManager) : null;
1066
+ const monorepoApps = [];
1067
+ if (isMonorepo) for (const [filePath, content] of Object.entries(files)) {
1068
+ if (filePath === "package.json") continue;
1069
+ if (!filePath.endsWith("/package.json")) continue;
1070
+ const nested = safeParsePkg(content);
1071
+ if (!nested) continue;
1072
+ const appPath = filePath.replace("/package.json", "");
1073
+ const appName = nested.name || appPath.split("/").pop() || appPath;
1074
+ let appFramework = detectFramework(content, packageManager);
1075
+ if (appFramework && rootPkg) {
1076
+ if (rootPkg.scripts?.[`build:${appName}`]) appFramework = {
1077
+ ...appFramework,
1078
+ buildCommand: pmRun(packageManager, `build:${appName}`)
1079
+ };
1080
+ if (nested.scripts?.start?.startsWith("node ")) {
1081
+ const nodePath = nested.scripts.start.slice(5).trim();
1082
+ appFramework = {
1083
+ ...appFramework,
1084
+ startCommand: `node ${appPath}/${nodePath}`
1085
+ };
921
1086
  }
922
1087
  }
923
- apps.push({
924
- name,
925
- path: dir,
926
- framework
1088
+ monorepoApps.push({
1089
+ name: appName,
1090
+ path: appPath,
1091
+ framework: appFramework
927
1092
  });
928
1093
  }
929
1094
  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 {
1095
+ packageManager,
940
1096
  framework,
941
- packageManager: pm,
942
1097
  envVars,
943
1098
  isMonorepo,
944
- monorepoApps: apps
1099
+ monorepoApps
945
1100
  };
946
1101
  }
947
1102
 
@@ -1001,170 +1156,108 @@ async function getFilesToUpload(directory) {
1001
1156
  return files;
1002
1157
  }
1003
1158
  async function createTarball(directory, extraFiles) {
1004
- const createdFiles = [];
1159
+ const tempDir = await mkdtemp(join(tmpdir(), "veloz-upload-"));
1160
+ const tarPath = join(tempDir, "source.tar.gz");
1161
+ const injectedFiles = [];
1005
1162
  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
- }
1163
+ if (existsSync(join(directory, file.name))) continue;
1164
+ injectedFiles.push(file);
1011
1165
  }
1012
1166
  try {
1013
1167
  const files = await getFilesToUpload(directory);
1014
1168
  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
1169
  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({
1170
+ if (injectedFiles.length === 0) await tar.create({
1022
1171
  gzip: true,
1023
1172
  file: tarPath,
1024
1173
  cwd: directory
1025
1174
  }, relativePaths);
1175
+ else {
1176
+ const stagingDir = join(tempDir, "staging");
1177
+ await mkdir(stagingDir, { recursive: true });
1178
+ for (const rel of relativePaths) {
1179
+ const src = join(directory, rel);
1180
+ const dest = join(stagingDir, rel);
1181
+ await mkdir(dirname(dest), { recursive: true });
1182
+ await link(src, dest);
1183
+ }
1184
+ for (const file of injectedFiles) {
1185
+ writeFileSync(join(stagingDir, file.name), file.content);
1186
+ if (!relativePaths.includes(file.name)) relativePaths.push(file.name);
1187
+ }
1188
+ await tar.create({
1189
+ gzip: true,
1190
+ file: tarPath,
1191
+ cwd: stagingDir
1192
+ }, relativePaths);
1193
+ }
1026
1194
  return {
1027
1195
  path: tarPath,
1028
- size: statSync(tarPath).size
1196
+ size: statSync(tarPath).size,
1197
+ tempDir
1029
1198
  };
1030
- } finally {
1031
- for (const f of createdFiles) try {
1032
- unlinkSync(f);
1033
- } catch {}
1199
+ } catch (err) {
1200
+ await rm(tempDir, {
1201
+ recursive: true,
1202
+ force: true
1203
+ }).catch(() => {});
1204
+ throw err;
1034
1205
  }
1035
1206
  }
1036
- async function uploadSource(apiUrl, deploymentId, directory, token, extraFiles) {
1037
- const { path: tarPath } = await createTarball(directory, extraFiles);
1207
+ async function uploadSource(deploymentId, directory, extraFiles) {
1208
+ const { path: tarPath, tempDir } = await createTarball(directory, extraFiles);
1209
+ const client = await getClient();
1038
1210
  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();
1211
+ const { uploadUrl, objectKey } = await client.deployments.getUploadUrl({ deploymentId });
1051
1212
  const fileBuffer = readFileSync(tarPath);
1052
1213
  const putResponse = await fetch(uploadUrl, {
1053
1214
  method: "PUT",
1054
1215
  headers: { "Content-Type": "application/gzip" },
1055
1216
  body: fileBuffer
1056
1217
  });
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 })
1218
+ if (!putResponse.ok) throw new Error(`Upload falhou: ${putResponse.status}`);
1219
+ await client.deployments.startBuild({
1220
+ deploymentId,
1221
+ objectKey
1065
1222
  });
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
1223
  } finally {
1071
- await rm(tarPath, {
1072
- force: true,
1073
- recursive: true
1224
+ await rm(tempDir, {
1225
+ recursive: true,
1226
+ force: true
1074
1227
  }).catch(() => {});
1075
1228
  }
1076
1229
  }
1077
1230
  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);
1231
+ const files = await getFilesToUpload(directory);
1232
+ let totalSize = 0;
1233
+ for (const file of files) {
1234
+ const stats = await stat(file);
1235
+ totalSize += stats.size;
1150
1236
  }
1237
+ return totalSize;
1151
1238
  }
1152
1239
 
1153
1240
  //#endregion
1154
- //#region src/lib/deploy-parallel.ts
1155
- async function withRetry$1(fn, maxRetries = 3) {
1241
+ //#region src/lib/retry.ts
1242
+ async function withRetry(fn, maxRetries = 3) {
1156
1243
  for (let attempt = 0; attempt <= maxRetries; attempt++) try {
1157
1244
  return await fn();
1158
1245
  } catch (error) {
1159
- if (attempt < maxRetries) {
1246
+ if (attempt >= maxRetries) throw error;
1247
+ const rateLimit = isRateLimitError(error);
1248
+ if (rateLimit) {
1249
+ const waitMs = Math.min(rateLimit.retryAfterMs, 3e4);
1250
+ await new Promise((r) => setTimeout(r, waitMs));
1251
+ } else {
1160
1252
  const delay = Math.min(1e3 * Math.pow(2, attempt), 1e4);
1161
- await new Promise((resolve$1) => setTimeout(resolve$1, delay));
1162
- continue;
1253
+ await new Promise((r) => setTimeout(r, delay));
1163
1254
  }
1164
- throw error;
1165
1255
  }
1166
1256
  throw new Error("Max retries exceeded");
1167
1257
  }
1258
+
1259
+ //#endregion
1260
+ //#region src/lib/deploy-constants.ts
1168
1261
  const statusLabels = {
1169
1262
  QUEUED: "Na fila",
1170
1263
  BUILDING: "Construindo",
@@ -1174,21 +1267,90 @@ const statusLabels = {
1174
1267
  FAILED: "Falhou",
1175
1268
  CANCELLED: "Cancelado"
1176
1269
  };
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
- };
1270
+ function makeStatusIcons() {
1271
+ const mode = getOutputMode();
1272
+ if (mode === "json" || mode === "github-actions" || mode === "plain") return {
1273
+ QUEUED: "",
1274
+ BUILDING: "●",
1275
+ DEPLOYING: "●",
1276
+ LIVE: "●",
1277
+ BUILD_FAILED: "●",
1278
+ FAILED: "●",
1279
+ CANCELLED: "●"
1280
+ };
1281
+ return {
1282
+ QUEUED: chalk.gray("○"),
1283
+ BUILDING: chalk.yellow("●"),
1284
+ DEPLOYING: chalk.blue("●"),
1285
+ LIVE: chalk.green("●"),
1286
+ BUILD_FAILED: chalk.red("●"),
1287
+ FAILED: chalk.red("●"),
1288
+ CANCELLED: chalk.gray("●")
1289
+ };
1290
+ }
1291
+ const statusIcons = new Proxy({}, { get(_target, prop) {
1292
+ return makeStatusIcons()[prop] ?? chalk.yellow("●");
1293
+ } });
1186
1294
  const TERMINAL_STATUSES = new Set([
1187
1295
  "LIVE",
1188
1296
  "BUILD_FAILED",
1189
1297
  "FAILED",
1190
1298
  "CANCELLED"
1191
1299
  ]);
1300
+
1301
+ //#endregion
1302
+ //#region src/lib/deploy-cancel.ts
1303
+ const activeDeploymentIds = /* @__PURE__ */ new Set();
1304
+ let sigintHandlerRegistered = false;
1305
+ function trackDeployment(deploymentId) {
1306
+ activeDeploymentIds.add(deploymentId);
1307
+ }
1308
+ function untrackDeployment(deploymentId) {
1309
+ activeDeploymentIds.delete(deploymentId);
1310
+ }
1311
+ function setupSigintHandler() {
1312
+ if (sigintHandlerRegistered) return;
1313
+ sigintHandlerRegistered = true;
1314
+ process.on("SIGINT", async () => {
1315
+ if (activeDeploymentIds.size === 0) process.exit(130);
1316
+ const mode = getOutputMode();
1317
+ if (mode === "json") process.stdout.write(JSON.stringify({ type: "deploy_cancelled" }) + "\n");
1318
+ else console.log(chalk.yellow("\n\nCancelando deploy(s)..."));
1319
+ try {
1320
+ const client = await getClient();
1321
+ const cancelPromises = Array.from(activeDeploymentIds).map((deploymentId) => client.deployments.cancel({ deploymentId }).catch(() => {}));
1322
+ await Promise.all(cancelPromises);
1323
+ if (mode !== "json") console.log(chalk.yellow("Deploy cancelado."));
1324
+ } catch {}
1325
+ process.exit(130);
1326
+ });
1327
+ }
1328
+
1329
+ //#endregion
1330
+ //#region src/lib/deploy-parallel.ts
1331
+ async function fetchDeployUrls$1(client, serviceId) {
1332
+ try {
1333
+ return (await client.domains.list({ serviceId })).map((d) => `https://${d.domain}`);
1334
+ } catch {
1335
+ return [];
1336
+ }
1337
+ }
1338
+ function getFailureHints$1(status) {
1339
+ switch (status) {
1340
+ case "BUILD_FAILED": return [
1341
+ "Verifique os logs de build acima para erros de compilação",
1342
+ "Teste o build localmente: rode o comando de build do seu projeto",
1343
+ "Use 'veloz config show' para verificar as configurações"
1344
+ ];
1345
+ case "DEPLOY_FAILED": return [
1346
+ "O build passou mas o serviço falhou ao iniciar",
1347
+ "Verifique se a porta configurada está correta: 'veloz config show'",
1348
+ "Veja os logs de runtime: 'veloz logs -f'"
1349
+ ];
1350
+ case "CANCELLED": return ["Deploy cancelado. Execute 'veloz deploy' para tentar novamente."];
1351
+ default: return ["Execute 'veloz logs -f' para mais detalhes."];
1352
+ }
1353
+ }
1192
1354
  function renderProgress(progressMap, prevLineCount) {
1193
1355
  for (let i = 0; i < prevLineCount; i++) process.stdout.write("\x1B[1A\x1B[2K");
1194
1356
  let lineCount = 0;
@@ -1198,35 +1360,47 @@ function renderProgress(progressMap, prevLineCount) {
1198
1360
  process.stdout.write(`${icon} ${chalk.bold(progress.serviceName)}: ${label}\n`);
1199
1361
  lineCount++;
1200
1362
  if (progress.status === "BUILDING" || progress.status === "BUILD_FAILED") {
1201
- if (progress.logLines.length > 0) {
1202
- const tail = progress.logLines.slice(-3);
1363
+ const nonEmptyLines = progress.logLines.filter((l) => l.trim());
1364
+ if (nonEmptyLines.length > 0) {
1365
+ const tail = nonEmptyLines.slice(-3);
1203
1366
  for (const line of tail) {
1204
1367
  const truncated = line.substring(0, 80) + (line.length > 80 ? "..." : "");
1205
1368
  process.stdout.write(` ${chalk.dim(truncated)}\n`);
1206
1369
  lineCount++;
1207
1370
  }
1208
1371
  } else if (progress.status === "BUILDING") {
1209
- process.stdout.write(` ${chalk.dim("Aguardando logs do build...")}\n`);
1372
+ process.stdout.write(` ${chalk.dim("Aguardando logs do build...")}\n`);
1210
1373
  lineCount++;
1211
1374
  }
1212
1375
  } else if (progress.status === "QUEUED") {
1213
- process.stdout.write(` ${chalk.dim("📋 Na fila para construção...")}\n`);
1376
+ process.stdout.write(` ${chalk.dim("Na fila para construção...")}\n`);
1214
1377
  lineCount++;
1215
1378
  }
1216
1379
  }
1217
1380
  return lineCount;
1218
1381
  }
1219
- async function deployServicesInParallel(client, services) {
1220
- console.log(chalk.cyan("\n🚀 Iniciando deploy paralelo de múltiplos serviços...\n"));
1382
+ async function deployServicesInParallel(services) {
1383
+ const client = await getClient();
1384
+ const mode = getOutputMode();
1385
+ if (mode === "json") emitData({
1386
+ type: "parallel_deploy_start",
1387
+ services: services.map((s) => ({
1388
+ serviceId: s.serviceId,
1389
+ serviceName: s.serviceName
1390
+ }))
1391
+ });
1392
+ else if (mode === "github-actions") process.stdout.write(`Iniciando deploy de ${services.length} serviço(s)\n`);
1393
+ else console.log(chalk.cyan(`\nIniciando deploy de ${services.length} serviço(s)...\n`));
1394
+ setupSigintHandler();
1221
1395
  const progressMap = /* @__PURE__ */ new Map();
1222
1396
  const projectRoot = process.cwd();
1223
1397
  const sizeInBytes = await calculateDirectorySize(projectRoot);
1224
1398
  const sizeMB = Math.round(sizeInBytes / (1024 * 1024) * 10) / 10;
1225
1399
  const deploymentPromises = services.map(async (service) => {
1226
1400
  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));
1401
+ const deployment = await withRetry(() => client.deployments.create({ serviceId: service.serviceId }));
1402
+ await withRetry(() => uploadSource(deployment.id, projectRoot, service.extraFiles));
1403
+ trackDeployment(deployment.id);
1230
1404
  progressMap.set(service.serviceId, {
1231
1405
  serviceName: service.serviceName,
1232
1406
  deploymentId: deployment.id,
@@ -1235,13 +1409,27 @@ async function deployServicesInParallel(client, services) {
1235
1409
  completed: false,
1236
1410
  success: false
1237
1411
  });
1238
- console.log(`${chalk.green("")} ${chalk.bold(service.serviceName)}: Upload concluído (${sizeMB} MB)`);
1412
+ if (mode === "json") emitData({
1413
+ type: "upload_complete",
1414
+ serviceId: service.serviceId,
1415
+ serviceName: service.serviceName,
1416
+ deploymentId: deployment.id,
1417
+ sizeMB
1418
+ });
1419
+ else if (mode === "github-actions") process.stdout.write(`✓ ${service.serviceName}: Upload concluído (${sizeMB} MB)\n`);
1420
+ else console.log(`${chalk.green("✓")} ${chalk.bold(service.serviceName)}: Upload concluído ${chalk.dim(`(${sizeMB} MB)`)}`);
1239
1421
  return {
1240
1422
  service,
1241
1423
  deploymentId: deployment.id
1242
1424
  };
1243
1425
  } catch (error) {
1244
- console.log(`${chalk.red("")} ${chalk.bold(service.serviceName)}: Falha ao iniciar deploy`);
1426
+ if (mode === "json") emitData({
1427
+ type: "upload_failed",
1428
+ serviceId: service.serviceId,
1429
+ serviceName: service.serviceName
1430
+ });
1431
+ else if (mode === "github-actions") process.stdout.write(`::error::${service.serviceName}: Falha ao iniciar deploy\n`);
1432
+ else console.log(`${chalk.red("✗")} ${chalk.bold(service.serviceName)}: Falha ao iniciar deploy`);
1245
1433
  progressMap.set(service.serviceId, {
1246
1434
  serviceName: service.serviceName,
1247
1435
  deploymentId: "",
@@ -1255,18 +1443,28 @@ async function deployServicesInParallel(client, services) {
1255
1443
  });
1256
1444
  const activeDeployments = (await Promise.allSettled(deploymentPromises)).filter((d) => d.status === "fulfilled").map((d) => d.value);
1257
1445
  if (activeDeployments.length === 0) {
1258
- console.error(chalk.red("\n✗ Todos os deploys falharam ao iniciar."));
1259
- return;
1446
+ if (mode === "json") emitData({
1447
+ type: "error",
1448
+ message: "Todos os deploys falharam ao iniciar."
1449
+ });
1450
+ else if (mode === "github-actions") process.stdout.write("::error::Todos os deploys falharam ao iniciar.\n");
1451
+ else console.error(chalk.red("\n✗ Todos os deploys falharam ao iniciar."));
1452
+ process.exit(1);
1453
+ }
1454
+ if (mode === "json") emitData({
1455
+ type: "monitoring_deploys",
1456
+ count: activeDeployments.length
1457
+ });
1458
+ else if (mode === "github-actions") process.stdout.write(`Monitorando ${activeDeployments.length} deploy(s)\n`);
1459
+ else {
1460
+ console.log(chalk.cyan(`\nMonitorando progresso dos deploys:\n`));
1461
+ console.log(chalk.dim("─".repeat(50)) + "\n");
1260
1462
  }
1261
- console.log(chalk.cyan("\n📦 Monitorando progresso dos deploys:\n"));
1262
- console.log(chalk.dim("─".repeat(60)) + "\n");
1263
1463
  let lineCount = 0;
1264
- lineCount = renderProgress(progressMap, lineCount);
1265
- const isVerbose = process.env.VELOZ_VERBOSE === "true";
1464
+ if (mode === "fancy") lineCount = renderProgress(progressMap, lineCount);
1266
1465
  const streamPromises = activeDeployments.map(async ({ service, deploymentId }) => {
1267
1466
  try {
1268
1467
  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
1468
  const stream = await client.logs.streamBuildLogs({ deploymentId });
1271
1469
  for await (const event of stream) {
1272
1470
  const progress = progressMap.get(service.serviceId);
@@ -1277,443 +1475,865 @@ async function deployServicesInParallel(client, services) {
1277
1475
  progress.completed = true;
1278
1476
  progress.success = event.content === "LIVE";
1279
1477
  }
1280
- if (isVerbose) console.log(chalk.dim(`\n[verbose] ${service.serviceName}: status mudou para ${event.content}`));
1478
+ if (mode === "json") emitData({
1479
+ type: "deploy_status",
1480
+ serviceId: service.serviceId,
1481
+ serviceName: service.serviceName,
1482
+ deploymentId,
1483
+ status: event.content
1484
+ });
1485
+ else if (mode === "plain") {
1486
+ const label = statusLabels[event.content] || event.content;
1487
+ process.stdout.write(`[${service.serviceName}] ${label}\n`);
1488
+ }
1281
1489
  } else if (event.type === "log") {
1282
1490
  const newLines = event.content.split("\n").filter((l) => l.trim());
1283
1491
  progress.logLines.push(...newLines);
1492
+ if (mode === "json") emitData({
1493
+ type: "build_log",
1494
+ serviceId: service.serviceId,
1495
+ serviceName: service.serviceName,
1496
+ deploymentId,
1497
+ lines: newLines
1498
+ });
1499
+ else if (mode === "github-actions" || mode === "plain") for (const line of newLines) process.stdout.write(`[${service.serviceName}] ${line}\n`);
1284
1500
  }
1285
- lineCount = renderProgress(progressMap, lineCount);
1501
+ if (mode === "fancy") lineCount = renderProgress(progressMap, lineCount);
1286
1502
  }
1287
1503
  } catch (error) {
1288
1504
  const progress = progressMap.get(service.serviceId);
1289
1505
  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}`);
1506
+ if (mode === "json") emitData({
1507
+ type: "deploy_stream_error",
1508
+ serviceId: service.serviceId,
1509
+ serviceName: service.serviceName,
1510
+ error: error instanceof Error ? error.message : "Erro desconhecido"
1511
+ });
1512
+ else if (mode === "github-actions") process.stdout.write(`::error::Erro no streaming de logs para ${service.serviceName}\n`);
1513
+ else if (mode !== "fancy") console.error(`${chalk.red("✗")} Erro no streaming de logs para ${service.serviceName}`);
1292
1514
  progress.status = "FAILED";
1293
1515
  progress.completed = true;
1294
- lineCount = renderProgress(progressMap, lineCount);
1516
+ if (mode === "fancy") lineCount = renderProgress(progressMap, lineCount);
1295
1517
  }
1518
+ } finally {
1519
+ untrackDeployment(deploymentId);
1296
1520
  }
1297
1521
  });
1298
1522
  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)));
1523
+ if (mode === "fancy") renderProgress(progressMap, lineCount);
1524
+ const successfulEntries = Array.from(progressMap.entries()).filter(([, p]) => p.success);
1525
+ const failedEntries = Array.from(progressMap.entries()).filter(([, p]) => !p.success);
1526
+ const urlMap = /* @__PURE__ */ new Map();
1527
+ await Promise.all(successfulEntries.map(async ([serviceId]) => {
1528
+ const urls = await fetchDeployUrls$1(client, serviceId);
1529
+ if (urls.length > 0) urlMap.set(serviceId, urls);
1530
+ }));
1531
+ if (mode === "json") emitData({
1532
+ type: "parallel_deploy_complete",
1533
+ successful: successfulEntries.map(([serviceId, p]) => ({
1534
+ serviceName: p.serviceName,
1535
+ deploymentId: p.deploymentId,
1536
+ urls: urlMap.get(serviceId) ?? []
1537
+ })),
1538
+ failed: failedEntries.map(([, p]) => ({
1539
+ serviceName: p.serviceName,
1540
+ deploymentId: p.deploymentId,
1541
+ status: p.status,
1542
+ hints: getFailureHints$1(p.status)
1543
+ }))
1544
+ });
1545
+ else {
1546
+ if (mode === "fancy") console.log(chalk.dim("\n" + "─".repeat(50)));
1547
+ if (successfulEntries.length > 0) if (mode === "github-actions") {
1548
+ process.stdout.write(`\n✓ ${successfulEntries.length} serviço(s) implantado(s) com sucesso\n`);
1549
+ for (const [serviceId, progress] of successfulEntries) {
1550
+ process.stdout.write(` ✓ ${progress.serviceName}\n`);
1551
+ for (const url of urlMap.get(serviceId) ?? []) process.stdout.write(` ${url}\n`);
1552
+ }
1553
+ } else if (mode === "plain") {
1554
+ process.stdout.write(`\n${successfulEntries.length} serviço(s) implantado(s) com sucesso:\n`);
1555
+ for (const [serviceId, progress] of successfulEntries) {
1556
+ process.stdout.write(` ✓ ${progress.serviceName}\n`);
1557
+ for (const url of urlMap.get(serviceId) ?? []) process.stdout.write(` ${url}\n`);
1558
+ }
1559
+ } else {
1560
+ console.log(chalk.green(`\n✓ ${successfulEntries.length} serviço(s) implantado(s) com sucesso:\n`));
1561
+ for (const [serviceId, progress] of successfulEntries) {
1562
+ console.log(` ${chalk.green("✓")} ${chalk.bold(progress.serviceName)}`);
1563
+ for (const url of urlMap.get(serviceId) ?? []) console.log(` ${chalk.cyan(url)}`);
1564
+ }
1565
+ }
1566
+ if (failedEntries.length > 0) if (mode === "github-actions") {
1567
+ process.stdout.write(`\n✗ ${failedEntries.length} serviço(s) falhou(aram)\n`);
1568
+ for (const [, progress] of failedEntries) {
1569
+ process.stdout.write(`::error::${progress.serviceName} falhou (${progress.status})\n`);
1570
+ const hints = getFailureHints$1(progress.status);
1571
+ for (const hint of hints) process.stdout.write(` ${hint}\n`);
1572
+ }
1573
+ } else if (mode === "plain") {
1574
+ process.stdout.write(`\n${failedEntries.length} serviço(s) falhou(aram):\n`);
1575
+ for (const [, progress] of failedEntries) {
1576
+ process.stdout.write(`\n ✗ ${progress.serviceName} (${progress.status})\n`);
1577
+ const hints = getFailureHints$1(progress.status);
1578
+ for (const hint of hints) process.stdout.write(` → ${hint}\n`);
1579
+ }
1580
+ } else {
1581
+ console.log(chalk.red(`\n✗ ${failedEntries.length} serviço(s) falhou(aram):\n`));
1582
+ for (const [, progress] of failedEntries) {
1583
+ console.log(` ${chalk.red("✗")} ${chalk.bold(progress.serviceName)} ${chalk.dim(`(${progress.status})`)}`);
1584
+ if (progress.logLines.length > 0) {
1585
+ console.log(chalk.red(` ${"─".repeat(50)}`));
1586
+ console.log(chalk.red.bold(" Logs de build:"));
1587
+ console.log(chalk.red(` ${"─".repeat(50)}`));
1588
+ for (const line of progress.logLines) if (line.trim()) console.log(` ${chalk.dim(line)}`);
1589
+ console.log(chalk.red(` ${"─".repeat(50)}`));
1590
+ }
1591
+ const hints = getFailureHints$1(progress.status);
1592
+ for (const hint of hints) console.log(chalk.yellow(` → ${hint}`));
1316
1593
  }
1317
1594
  }
1595
+ if (successfulEntries.length > 0) info("\nUse 'veloz logs -f' para acompanhar os logs de execução.");
1318
1596
  }
1319
- if (successful.length > 0) info("\nUse 'veloz logs -f' para acompanhar os logs de execução.");
1597
+ if (failedEntries.length > 0) process.exit(1);
1320
1598
  }
1321
1599
 
1322
1600
  //#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 "";
1601
+ //#region src/lib/deploy-stream.ts
1602
+ async function fetchDeployUrls(client, serviceId) {
1603
+ try {
1604
+ return (await client.domains.list({ serviceId })).map((d) => `https://${d.domain}`);
1605
+ } catch {
1606
+ return [];
1607
+ }
1608
+ }
1609
+ function getFailureHints(status) {
1610
+ switch (status) {
1611
+ case "BUILD_FAILED": return [
1612
+ "Verifique os logs de build acima para erros de compilação",
1613
+ "Teste o build localmente: rode o comando de build do seu projeto",
1614
+ "Use 'veloz config show' para verificar as configurações"
1615
+ ];
1616
+ case "DEPLOY_FAILED": return [
1617
+ "O build passou mas o serviço falhou ao iniciar",
1618
+ "Verifique se a porta configurada está correta: 'veloz config show'",
1619
+ "Veja os logs de runtime: 'veloz logs -f'"
1620
+ ];
1621
+ case "CANCELLED": return ["Deploy cancelado. Execute 'veloz deploy' para tentar novamente."];
1622
+ default: return ["Execute 'veloz logs -f' para mais detalhes."];
1623
+ }
1624
+ }
1625
+ async function streamDeploymentLogs(deploymentId, serviceId, serviceName) {
1626
+ const client = await getClient();
1627
+ const isVerbose = process.env.VELOZ_VERBOSE === "true";
1628
+ const mode = getOutputMode();
1629
+ const allLogLines = [];
1630
+ let buildSpinner = null;
1631
+ if (mode === "json") emitData({
1632
+ type: "deploy_stream_start",
1633
+ deploymentId,
1634
+ serviceName: serviceName ?? null
1635
+ });
1636
+ else if (mode === "github-actions") startGroup(serviceName ? `Build: ${serviceName}` : "Build");
1637
+ else if (mode === "fancy" && !isVerbose) buildSpinner = ora({
1638
+ text: "Aguardando início do build...",
1639
+ color: "cyan"
1640
+ }).start();
1641
+ else {
1642
+ const header = serviceName ? `Build: ${chalk.bold(serviceName)}` : "Build";
1643
+ console.log(chalk.cyan(`\n${header}`));
1644
+ console.log(chalk.dim("─".repeat(50)));
1330
1645
  }
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";
1646
+ let finalStatus = "";
1647
+ try {
1648
+ const stream = await client.logs.streamBuildLogs({ deploymentId });
1649
+ for await (const event of stream) if (event.type === "status") {
1650
+ const label = statusLabels[event.content] ?? event.content;
1651
+ finalStatus = event.content;
1652
+ if (mode === "json") emitData({
1653
+ type: "deploy_status",
1654
+ deploymentId,
1655
+ status: event.content,
1656
+ label
1657
+ });
1658
+ else if (mode === "fancy" && !isVerbose) {
1659
+ if (buildSpinner) {
1660
+ if (event.content === "BUILDING") buildSpinner.text = "Construindo...";
1661
+ else if (event.content === "DEPLOYING") {
1662
+ buildSpinner.succeed("Build concluído");
1663
+ buildSpinner = ora({
1664
+ text: "Publicando...",
1665
+ color: "cyan"
1666
+ }).start();
1667
+ } else if (event.content === "LIVE") {
1668
+ buildSpinner.succeed("Publicado");
1669
+ buildSpinner = null;
1670
+ } else if (TERMINAL_STATUSES.has(event.content) && event.content !== "LIVE") {
1671
+ buildSpinner.fail(label);
1672
+ buildSpinner = null;
1673
+ }
1674
+ }
1675
+ } else {
1676
+ const icon = statusIcons[event.content] ?? chalk.yellow("●");
1677
+ process.stdout.write(`\n${icon} ${chalk.bold(label)}\n`);
1678
+ }
1679
+ } else if (event.type === "log") {
1680
+ const lines = event.content.split("\n");
1681
+ allLogLines.push(...lines);
1682
+ if (mode === "json") emitData({
1683
+ type: "build_log",
1684
+ deploymentId,
1685
+ content: event.content
1686
+ });
1687
+ else if (mode === "fancy" && !isVerbose) for (const line of lines) {
1688
+ const trimmed = line.trim();
1689
+ if (trimmed) {
1690
+ const display = trimmed.length > 60 ? trimmed.substring(0, 57) + "..." : trimmed;
1691
+ if (buildSpinner) buildSpinner.text = display;
1692
+ }
1693
+ }
1694
+ else for (const line of lines) if (line.trim()) process.stdout.write(` ${line}\n`);
1695
+ }
1696
+ } catch {
1697
+ if (buildSpinner) {
1698
+ buildSpinner.stop();
1699
+ buildSpinner = null;
1700
+ }
1701
+ try {
1702
+ finalStatus = (await client.deployments.get({ deploymentId })).status;
1703
+ try {
1704
+ const logs = await client.logs.getBuildLogs({ deploymentId });
1705
+ if (logs.buildLogs) {
1706
+ allLogLines.push(...logs.buildLogs.split("\n"));
1707
+ if (mode === "json") emitData({
1708
+ type: "build_log",
1709
+ deploymentId,
1710
+ content: logs.buildLogs
1711
+ });
1712
+ }
1713
+ } catch {}
1714
+ } catch {}
1715
+ }
1716
+ if (mode === "github-actions") endGroup();
1717
+ if (finalStatus === "LIVE") {
1718
+ if (buildSpinner) {
1719
+ buildSpinner.stop();
1720
+ buildSpinner = null;
1721
+ }
1722
+ success(serviceName ? `Deploy de ${chalk.bold(serviceName)} concluído! Serviço ativo.` : "Deploy concluído! Serviço ativo.");
1723
+ const urls = await fetchDeployUrls(client, serviceId);
1724
+ if (mode === "json") emitData({
1725
+ type: "deploy_complete",
1726
+ deploymentId,
1727
+ status: "LIVE",
1728
+ serviceName: serviceName ?? null,
1729
+ urls
1730
+ });
1731
+ else if (urls.length > 0) for (const url of urls) info(`${chalk.bold(url)}`);
1732
+ } else if (TERMINAL_STATUSES.has(finalStatus)) {
1733
+ if (buildSpinner) {
1734
+ buildSpinner.stop();
1735
+ buildSpinner = null;
1736
+ }
1737
+ const label = statusLabels[finalStatus] ?? finalStatus;
1738
+ const hints = getFailureHints(finalStatus);
1739
+ if (mode === "fancy" && allLogLines.length > 0) {
1740
+ console.log();
1741
+ console.log(chalk.red(` ${"─".repeat(50)}`));
1742
+ console.log(chalk.red.bold(" Logs de build:"));
1743
+ console.log(chalk.red(` ${"─".repeat(50)}`));
1744
+ for (const line of allLogLines) if (line.trim()) console.log(chalk.dim(` ${line}`));
1745
+ console.log(chalk.red(` ${"─".repeat(50)}`));
1746
+ }
1747
+ if (mode === "json") emitData({
1748
+ type: "deploy_complete",
1749
+ deploymentId,
1750
+ status: finalStatus,
1751
+ serviceName: serviceName ?? null,
1752
+ hints
1753
+ });
1754
+ else if (mode === "github-actions") {
1755
+ const msg = serviceName ? `Deploy de ${serviceName} finalizou com status: ${label}` : `Deploy finalizou com status: ${label}`;
1756
+ process.stdout.write(`::error::${msg}\n`);
1757
+ for (const hint of hints) process.stdout.write(` ${hint}\n`);
1758
+ } else {
1759
+ const errorMsg = serviceName ? `Deploy de ${chalk.bold(serviceName)} finalizou: ${label}` : `Deploy finalizou: ${label}`;
1760
+ console.error(chalk.red(`\n✗ ${errorMsg}`));
1761
+ for (const hint of hints) console.error(chalk.yellow(` → ${hint}`));
1762
+ }
1763
+ process.exit(1);
1338
1764
  }
1339
1765
  }
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
1766
 
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
1767
+ //#endregion
1768
+ //#region src/lib/brand.ts
1769
+ const LOGO_LINES = [
1770
+ "╦ ╦╔═╗╦ ╔═╗╔═╗",
1771
+ "╚╗╔╝║╣ ║ ║ ║ ╔╝",
1772
+ " ╚╝ ╚═╝╩═╝╚═╝╚═╝"
1773
+ ];
1774
+ const BRAND_COLOR = "#FF4D00";
1775
+ function getVersion() {
1776
+ return "0.0.0-beta.10";
1777
+ }
1778
+ function printBanner(subtitle) {
1779
+ const mode = getOutputMode();
1780
+ const version = getVersion();
1781
+ const env = getActiveEnv();
1782
+ const envTag = env ? ` [${env}]` : "";
1783
+ if (mode === "json") return;
1784
+ if (mode === "github-actions") {
1785
+ process.stdout.write(`Veloz ${subtitle ?? ""} v${version}${envTag}\n`);
1786
+ return;
1787
+ }
1788
+ if (mode === "plain") {
1789
+ process.stdout.write(`\nVeloz ${subtitle ?? ""} v${version}${envTag}\n\n`);
1790
+ return;
1791
+ }
1792
+ const sub = subtitle ?? "";
1793
+ const envBadge = env ? ` ${chalk.bgYellow.black(` ${env} `)}` : "";
1794
+ console.log();
1795
+ console.log(` ${chalk.hex(BRAND_COLOR).bold(LOGO_LINES[0])}`);
1796
+ console.log(` ${chalk.hex(BRAND_COLOR).bold(LOGO_LINES[1])} ${chalk.bold(sub)}${envBadge}`);
1797
+ console.log(` ${chalk.hex(BRAND_COLOR).bold(LOGO_LINES[2])} ${chalk.dim(`v${version}`)}`);
1798
+ console.log();
1799
+ console.log(` ${chalk.dim("─".repeat(40))}`);
1800
+ console.log();
1801
+ }
1463
1802
 
1464
- HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
1465
- CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1
1803
+ //#endregion
1804
+ //#region src/lib/deploy-config.ts
1805
+ function resolveServiceConf(velozConfig, serviceId) {
1806
+ if (!velozConfig) return void 0;
1807
+ for (const [, conf] of Object.entries(velozConfig.services)) if (conf.id === serviceId) {
1808
+ const merged = mergeServiceWithDefaults(conf, velozConfig.defaults);
1809
+ return {
1810
+ type: merged.type,
1811
+ buildCommand: merged.build?.command ?? void 0,
1812
+ startCommand: merged.runtime?.command ?? void 0,
1813
+ port: merged.runtime?.port ?? void 0,
1814
+ rootDirectory: merged.root,
1815
+ outputDir: merged.build?.outputDir ?? void 0,
1816
+ instanceCount: merged.resources?.instances ?? void 0,
1817
+ cpuLimit: merged.resources?.cpu ?? void 0,
1818
+ memoryLimit: merged.resources?.memory ?? void 0,
1819
+ healthCheckPath: merged.runtime?.healthCheck?.path ?? null,
1820
+ aptPackages: merged.build?.aptPackages ?? void 0,
1821
+ nodeVersion: merged.build?.nodeVersion ?? void 0,
1822
+ nixpkgsArchive: merged.build?.nixpkgsArchive ?? void 0,
1823
+ packageManager: merged.build?.packageManager ?? void 0,
1824
+ installCommand: merged.build?.installCommand ?? void 0
1825
+ };
1826
+ }
1827
+ }
1828
+ async function syncServiceConfig(client, serviceId, conf) {
1829
+ await withRetry(() => client.services.update({
1830
+ serviceId,
1831
+ type: conf.type?.toUpperCase(),
1832
+ port: conf.port,
1833
+ instanceCount: conf.instanceCount,
1834
+ cpuLimit: conf.cpuLimit,
1835
+ memoryLimit: conf.memoryLimit,
1836
+ buildCommand: conf.buildCommand,
1837
+ startCommand: conf.startCommand,
1838
+ rootDirectory: conf.rootDirectory,
1839
+ healthCheckPath: conf.healthCheckPath,
1840
+ aptPackages: conf.aptPackages,
1841
+ nodeVersion: conf.nodeVersion,
1842
+ nixpkgsArchive: conf.nixpkgsArchive,
1843
+ packageManager: conf.packageManager,
1844
+ installCommand: conf.installCommand
1845
+ }));
1846
+ }
1466
1847
 
1467
- CMD ["nginx", "-g", "daemon off;"]
1468
- `;
1848
+ //#endregion
1849
+ //#region src/lib/deploy-checks.ts
1850
+ /**
1851
+ * Platform-specific presets that won't work on Veloz (generic K8s).
1852
+ * Maps preset name to the platform it targets.
1853
+ */
1854
+ const INCOMPATIBLE_PRESETS = {
1855
+ vercel: "Vercel",
1856
+ "cloudflare-pages": "Cloudflare Pages",
1857
+ "cloudflare-workers": "Cloudflare Workers",
1858
+ "cloudflare-module": "Cloudflare Workers",
1859
+ netlify: "Netlify",
1860
+ "netlify-edge": "Netlify Edge",
1861
+ "aws-lambda": "AWS Lambda",
1862
+ firebase: "Firebase",
1863
+ "deno-deploy": "Deno Deploy",
1864
+ "render-com": "Render",
1865
+ "flight-control": "Flightcontrol",
1866
+ stormkit: "Stormkit",
1867
+ edgio: "Edgio",
1868
+ lagon: "Lagon"
1869
+ };
1870
+ /**
1871
+ * Recommended preset based on package manager / runtime.
1872
+ */
1873
+ function recommendedPreset(basePath) {
1874
+ const pkgPath = resolve(basePath, "package.json");
1875
+ if (!existsSync(pkgPath)) return "node-server";
1876
+ try {
1877
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1878
+ if ("bun" in {
1879
+ ...pkg.dependencies,
1880
+ ...pkg.devDependencies
1881
+ } || existsSync(resolve(basePath, "bun.lockb")) || existsSync(resolve(basePath, "bun.lock"))) return "bun";
1882
+ } catch {}
1883
+ return "node-server";
1884
+ }
1885
+ /**
1886
+ * Check for Nitro preset misconfigurations in vite/nuxt config files.
1887
+ */
1888
+ function checkNitroPreset(basePath) {
1889
+ for (const file of [
1890
+ "vite.config.ts",
1891
+ "vite.config.js",
1892
+ "vite.config.mjs",
1893
+ "nuxt.config.ts",
1894
+ "nuxt.config.js"
1895
+ ]) {
1896
+ const filePath = resolve(basePath, file);
1897
+ if (!existsSync(filePath)) continue;
1898
+ let content;
1899
+ try {
1900
+ content = readFileSync(filePath, "utf-8");
1901
+ } catch {
1902
+ continue;
1903
+ }
1904
+ const presetMatch = content.match(/preset\s*:\s*["']([^"']+)["']/);
1905
+ if (!presetMatch) continue;
1906
+ const preset = presetMatch[1];
1907
+ const platform$1 = INCOMPATIBLE_PRESETS[preset];
1908
+ if (!platform$1) continue;
1909
+ const recommended = recommendedPreset(basePath);
1910
+ return {
1911
+ message: `${file} usa preset "${preset}" (${platform$1}) — incompativel com Veloz`,
1912
+ hint: `Altere para preset: "${recommended}" em ${file}`
1913
+ };
1914
+ }
1915
+ return null;
1916
+ }
1917
+ /**
1918
+ * Check for Dockerfile COPY instructions that reference both bun.lockb and bun.lock.
1919
+ * Only one usually exists — the COPY will fail if both are listed but one is missing.
1920
+ */
1921
+ function checkDockerfileLockFiles(basePath) {
1922
+ const dockerfilePath = resolve(basePath, "Dockerfile");
1923
+ if (!existsSync(dockerfilePath)) return null;
1924
+ let content;
1925
+ try {
1926
+ content = readFileSync(dockerfilePath, "utf-8");
1927
+ } catch {
1928
+ return null;
1929
+ }
1930
+ const copyLines = content.split("\n").filter((l) => /^COPY\s/.test(l.trim()));
1931
+ for (const line of copyLines) if (line.includes("bun.lockb") && line.includes("bun.lock") && !line.includes("bun.lock*")) {
1932
+ if (!(existsSync(resolve(basePath, "bun.lockb")) && existsSync(resolve(basePath, "bun.lock")))) return {
1933
+ message: "Dockerfile lista bun.lockb e bun.lock mas apenas um existe",
1934
+ hint: "Use \"COPY package.json bun.lock* ./\" para copiar o que existir"
1935
+ };
1936
+ }
1937
+ return null;
1938
+ }
1939
+ /**
1940
+ * Next.js needs `output: "standalone"` for Docker/K8s deploys.
1941
+ * Without it, the build produces a node_modules-dependent output
1942
+ * that's huge and doesn't run well in containers.
1943
+ */
1944
+ function checkNextStandalone(basePath) {
1945
+ const pkgPath = resolve(basePath, "package.json");
1946
+ if (!existsSync(pkgPath)) return null;
1947
+ try {
1948
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1949
+ if (!("next" in {
1950
+ ...pkg.dependencies,
1951
+ ...pkg.devDependencies
1952
+ })) return null;
1953
+ } catch {
1954
+ return null;
1955
+ }
1956
+ if (existsSync(resolve(basePath, "Dockerfile"))) return null;
1957
+ for (const file of [
1958
+ "next.config.ts",
1959
+ "next.config.js",
1960
+ "next.config.mjs"
1961
+ ]) {
1962
+ const filePath = resolve(basePath, file);
1963
+ if (!existsSync(filePath)) continue;
1964
+ try {
1965
+ const content = readFileSync(filePath, "utf-8");
1966
+ if (content.includes("\"standalone\"") || content.includes("'standalone'")) return null;
1967
+ return {
1968
+ message: `${file} nao tem output: "standalone"`,
1969
+ hint: "Adicione output: \"standalone\" no next.config para deploy na Veloz"
1970
+ };
1971
+ } catch {
1972
+ continue;
1973
+ }
1974
+ }
1975
+ return null;
1976
+ }
1977
+ /**
1978
+ * Prisma needs `prisma generate` in the build step.
1979
+ * Without it, the Prisma client won't be generated and the app will crash.
1980
+ */
1981
+ function checkPrismaGenerate(basePath) {
1982
+ const pkgPath = resolve(basePath, "package.json");
1983
+ if (!existsSync(pkgPath)) return null;
1984
+ try {
1985
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1986
+ const allDeps = {
1987
+ ...pkg.dependencies,
1988
+ ...pkg.devDependencies
1989
+ };
1990
+ if (!("prisma" in allDeps) && !("@prisma/client" in allDeps)) return null;
1991
+ const scripts = pkg.scripts || {};
1992
+ const buildScript = scripts.build || "";
1993
+ const postinstall = scripts.postinstall || "";
1994
+ if (buildScript.includes("prisma generate") || postinstall.includes("prisma generate") || buildScript.includes("prisma db push")) return null;
1995
+ return {
1996
+ message: "Prisma detectado mas prisma generate nao esta no build/postinstall",
1997
+ hint: "Adicione \"prisma generate\" ao script postinstall ou build no package.json"
1998
+ };
1999
+ } catch {
2000
+ return null;
2001
+ }
2002
+ }
2003
+ /**
2004
+ * Detect if the app hardcodes a port that doesn't match the configured port.
2005
+ * Common issue: app listens on 8080 but service port is 3000.
2006
+ */
2007
+ function checkPortMismatch(basePath) {
2008
+ const pkgPath = resolve(basePath, "package.json");
2009
+ if (!existsSync(pkgPath)) return null;
2010
+ try {
2011
+ const portMatch = ((JSON.parse(readFileSync(pkgPath, "utf-8")).scripts || {}).start || "").match(/(?:--port|-p)\s+(\d+)/);
2012
+ if (portMatch) {
2013
+ const hardcodedPort = parseInt(portMatch[1], 10);
2014
+ if (hardcodedPort !== 3e3) return {
2015
+ message: `Script start usa porta ${hardcodedPort} — certifique-se de que a porta do servico esta configurada corretamente`,
2016
+ hint: `Configure a porta do servico para ${hardcodedPort} no dashboard ou veloz.json`
2017
+ };
2018
+ }
2019
+ } catch {}
2020
+ return null;
2021
+ }
2022
+ /**
2023
+ * Check for .env files that might be accidentally uploaded.
2024
+ */
2025
+ function checkEnvFileCommitted(basePath) {
2026
+ if (!existsSync(resolve(basePath, ".env"))) return null;
2027
+ const gitignorePath = resolve(basePath, ".gitignore");
2028
+ if (existsSync(gitignorePath)) try {
2029
+ const lines = readFileSync(gitignorePath, "utf-8").split("\n").map((l) => l.trim());
2030
+ if (lines.includes(".env") || lines.includes(".env*") || lines.includes("*.env")) return null;
2031
+ } catch {}
2032
+ return {
2033
+ message: "Arquivo .env encontrado e nao esta no .gitignore",
2034
+ hint: "Adicione .env ao .gitignore — use variaveis de ambiente no dashboard ou CLI"
2035
+ };
2036
+ }
2037
+ /**
2038
+ * Node.js project without a start command — nixpacks won't know how to run it.
2039
+ * Checks for: scripts.start, main field, or common entry files.
2040
+ */
2041
+ function checkMissingStartCommand(basePath) {
2042
+ const pkgPath = resolve(basePath, "package.json");
2043
+ if (!existsSync(pkgPath)) return null;
2044
+ if (existsSync(resolve(basePath, "Dockerfile"))) return null;
2045
+ try {
2046
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
2047
+ if (pkg.scripts?.start) return null;
2048
+ if (pkg.main) return null;
2049
+ const allDeps = {
2050
+ ...pkg.dependencies,
2051
+ ...pkg.devDependencies
2052
+ };
2053
+ if ([
2054
+ "next",
2055
+ "nuxt",
2056
+ "nuxt3",
2057
+ "@sveltejs/kit",
2058
+ "remix",
2059
+ "astro",
2060
+ "@angular/core",
2061
+ "gatsby"
2062
+ ].some((f) => f in allDeps)) return null;
2063
+ if ([
2064
+ "index.js",
2065
+ "index.mjs",
2066
+ "index.ts",
2067
+ "server.js",
2068
+ "server.ts",
2069
+ "app.js",
2070
+ "app.ts",
2071
+ "src/index.js",
2072
+ "src/index.ts",
2073
+ "src/server.js",
2074
+ "src/server.ts"
2075
+ ].some((f) => existsSync(resolve(basePath, f)))) return null;
2076
+ return {
2077
+ message: "Nenhum script start encontrado no package.json",
2078
+ hint: "Adicione \"start\" em scripts (ex: \"node dist/index.js\") ou um campo \"main\" no package.json"
2079
+ };
2080
+ } catch {
2081
+ return null;
2082
+ }
2083
+ }
2084
+ /**
2085
+ * packageManager field in package.json doesn't match the lockfile present.
2086
+ * e.g., packageManager: "pnpm@9.0.0" but only package-lock.json exists.
2087
+ */
2088
+ function checkPackageManagerMismatch(basePath) {
2089
+ const pkgPath = resolve(basePath, "package.json");
2090
+ if (!existsSync(pkgPath)) return null;
2091
+ try {
2092
+ const pmField = JSON.parse(readFileSync(pkgPath, "utf-8")).packageManager;
2093
+ if (!pmField) return null;
2094
+ const declaredPm = pmField.split("@")[0];
2095
+ const lockfileMap = {
2096
+ npm: ["package-lock.json"],
2097
+ yarn: ["yarn.lock"],
2098
+ pnpm: ["pnpm-lock.yaml"],
2099
+ bun: ["bun.lockb", "bun.lock"]
2100
+ };
2101
+ const expectedLockfiles = lockfileMap[declaredPm];
2102
+ if (!expectedLockfiles) return null;
2103
+ if (expectedLockfiles.some((f) => existsSync(resolve(basePath, f)))) return null;
2104
+ const otherPms = Object.entries(lockfileMap).filter(([pm]) => pm !== declaredPm);
2105
+ for (const [pm, files] of otherPms) if (files.some((f) => existsSync(resolve(basePath, f)))) return {
2106
+ message: `packageManager "${pmField}" no package.json mas lockfile de ${pm} encontrado`,
2107
+ hint: `Remova o campo packageManager ou gere o lockfile correto com ${declaredPm} install`
2108
+ };
2109
+ } catch {}
2110
+ return null;
2111
+ }
2112
+ /**
2113
+ * Native modules that need system packages to build.
2114
+ * The server auto-detects and injects apt packages for known modules (sharp, canvas,
2115
+ * puppeteer, playwright-chromium). This check warns about bcrypt which has a pure-JS
2116
+ * alternative, and modules not in the auto-detect list.
2117
+ */
2118
+ function checkNativeModules(basePath) {
2119
+ const pkgPath = resolve(basePath, "package.json");
2120
+ if (!existsSync(pkgPath)) return null;
2121
+ if (existsSync(resolve(basePath, "Dockerfile"))) return null;
2122
+ try {
2123
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
2124
+ if ("bcrypt" in {
2125
+ ...pkg.dependencies,
2126
+ ...pkg.devDependencies
2127
+ }) return {
2128
+ message: "bcrypt compila codigo nativo — pode falhar em alguns ambientes",
2129
+ hint: "Considere usar bcryptjs (pure JS) para evitar falhas de build"
2130
+ };
2131
+ } catch {}
2132
+ return null;
2133
+ }
2134
+ /**
2135
+ * nixpacks.toml phases that override defaults instead of extending them.
2136
+ * Missing "..." in [phases.X.cmds] replaces all default commands.
2137
+ */
2138
+ function checkNixpacksTomlSpread(basePath) {
2139
+ const tomlPath = resolve(basePath, "nixpacks.toml");
2140
+ if (!existsSync(tomlPath)) return null;
2141
+ try {
2142
+ const content = readFileSync(tomlPath, "utf-8");
2143
+ if (!content.match(/\[phases\.\w+\]/g)) return null;
2144
+ const hasCmds = /cmds\s*=\s*\[/.test(content);
2145
+ const hasSpread = content.includes("\"...\"");
2146
+ if (hasCmds && !hasSpread) return {
2147
+ message: "nixpacks.toml define cmds sem \"...\" — isso substitui os comandos padrao",
2148
+ hint: "Adicione \"...\" no array cmds para manter os comandos padrao: cmds = [\"...\", \"seu-comando\"]"
2149
+ };
2150
+ } catch {}
2151
+ return null;
2152
+ }
2153
+ /**
2154
+ * Django project without gunicorn — the dev server isn't suitable for production.
2155
+ */
2156
+ function checkDjangoGunicorn(basePath) {
2157
+ if (existsSync(resolve(basePath, "Dockerfile"))) return null;
2158
+ const requirementsFiles = [
2159
+ "requirements.txt",
2160
+ "requirements/production.txt",
2161
+ "requirements/prod.txt"
2162
+ ];
2163
+ let hasDjango = false;
2164
+ let hasGunicorn = false;
2165
+ for (const file of requirementsFiles) {
2166
+ const filePath = resolve(basePath, file);
2167
+ if (!existsSync(filePath)) continue;
2168
+ try {
2169
+ const content = readFileSync(filePath, "utf-8").toLowerCase();
2170
+ if (content.includes("django")) hasDjango = true;
2171
+ if (content.includes("gunicorn") || content.includes("uvicorn")) hasGunicorn = true;
2172
+ } catch {
2173
+ continue;
2174
+ }
2175
+ }
2176
+ for (const file of ["pyproject.toml", "Pipfile"]) {
2177
+ const filePath = resolve(basePath, file);
2178
+ if (!existsSync(filePath)) continue;
2179
+ try {
2180
+ const content = readFileSync(filePath, "utf-8").toLowerCase();
2181
+ if (content.includes("django")) hasDjango = true;
2182
+ if (content.includes("gunicorn") || content.includes("uvicorn")) hasGunicorn = true;
2183
+ } catch {
2184
+ continue;
2185
+ }
2186
+ }
2187
+ if (hasDjango && !hasGunicorn) return {
2188
+ message: "Django detectado sem gunicorn/uvicorn — o servidor de dev nao deve ser usado em producao",
2189
+ hint: "Adicione gunicorn ao requirements.txt e configure o start command: \"gunicorn myproject.wsgi\""
2190
+ };
2191
+ return null;
1469
2192
  }
1470
- function generateDockerfile(opts) {
1471
- if (opts.serviceType === "STATIC") return generateStaticDockerfile(opts);
1472
- return generateWebDockerfile(opts);
2193
+ /**
2194
+ * SvelteKit needs adapter-node for container deploys.
2195
+ * Default adapter-auto or adapter-vercel/netlify won't work.
2196
+ */
2197
+ function checkSvelteKitAdapter(basePath) {
2198
+ const pkgPath = resolve(basePath, "package.json");
2199
+ if (!existsSync(pkgPath)) return null;
2200
+ try {
2201
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
2202
+ const allDeps = {
2203
+ ...pkg.dependencies,
2204
+ ...pkg.devDependencies
2205
+ };
2206
+ if (!("@sveltejs/kit" in allDeps)) return null;
2207
+ const hasNodeAdapter = "@sveltejs/adapter-node" in allDeps;
2208
+ const hasBunAdapter = "svelte-adapter-bun" in allDeps;
2209
+ if (hasNodeAdapter || hasBunAdapter) return null;
2210
+ const installed = [
2211
+ "@sveltejs/adapter-vercel",
2212
+ "@sveltejs/adapter-netlify",
2213
+ "@sveltejs/adapter-cloudflare",
2214
+ "@sveltejs/adapter-cloudflare-workers"
2215
+ ].find((a) => a in allDeps);
2216
+ if (installed) return {
2217
+ message: `SvelteKit usa ${installed} — incompativel com Veloz`,
2218
+ hint: "Instale @sveltejs/adapter-node e configure em svelte.config.js"
2219
+ };
2220
+ if ("@sveltejs/adapter-auto" in allDeps) return {
2221
+ message: "SvelteKit usa adapter-auto — pode nao funcionar no deploy",
2222
+ hint: "Instale @sveltejs/adapter-node para deploys na Veloz"
2223
+ };
2224
+ } catch {}
2225
+ return null;
1473
2226
  }
1474
-
1475
- //#endregion
1476
- //#region src/lib/templates.ts
1477
2227
  /**
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.
2228
+ * Run all pre-deploy checks and return warnings.
2229
+ * Does not block just warns the user.
1481
2230
  */
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");
2231
+ function runPreDeployChecks(basePath = ".") {
2232
+ const warnings = [];
2233
+ const fullPath = resolve(process.cwd(), basePath);
2234
+ const checks = [
2235
+ checkNitroPreset,
2236
+ checkDockerfileLockFiles,
2237
+ checkNextStandalone,
2238
+ checkPrismaGenerate,
2239
+ checkPortMismatch,
2240
+ checkEnvFileCommitted,
2241
+ checkSvelteKitAdapter,
2242
+ checkMissingStartCommand,
2243
+ checkPackageManagerMismatch,
2244
+ checkNativeModules,
2245
+ checkNixpacksTomlSpread,
2246
+ checkDjangoGunicorn
2247
+ ];
2248
+ for (const check of checks) {
2249
+ const result = check(fullPath);
2250
+ if (result) warnings.push(result);
2251
+ }
2252
+ return warnings;
1629
2253
  }
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");
2254
+ /**
2255
+ * Print deploy warnings to the console.
2256
+ * Returns true if any warnings were found.
2257
+ */
2258
+ function printDeployWarnings(warnings) {
2259
+ if (warnings.length === 0) return false;
2260
+ const mode = getOutputMode();
2261
+ if (mode === "json") {
2262
+ emitData({
2263
+ type: "deploy_warnings",
2264
+ warnings: warnings.map((w) => ({
2265
+ message: w.message,
2266
+ hint: w.hint
2267
+ }))
2268
+ });
2269
+ return true;
2270
+ }
2271
+ console.log();
2272
+ for (const w of warnings) if (mode === "github-actions") process.stdout.write(`::warning::${w.message} — ${w.hint}\n`);
2273
+ else {
2274
+ console.log(chalk.yellow(` AVISO: ${w.message}`));
2275
+ console.log(chalk.dim(` ${w.hint}`));
2276
+ }
2277
+ console.log();
2278
+ return true;
1673
2279
  }
1674
2280
 
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
2281
  //#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
- }
2282
+ //#region src/lib/auto-update.ts
2283
+ const PACKAGE_NAME = "onveloz";
1697
2284
  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";
2285
+ try {
2286
+ const binPath = realpathSync(process.argv[1]);
2287
+ if (binPath.includes("/hostcloud/") || binPath.includes("/onveloz/src/")) return null;
2288
+ if (binPath.includes("/.pnpm/") || binPath.includes("/pnpm/")) return "pnpm";
2289
+ if (binPath.includes("/.bun/")) return "bun";
2290
+ if (binPath.includes("/yarn/")) return "yarn";
2291
+ } catch {}
1701
2292
  return "npm";
1702
2293
  }
1703
- function detectNodeVersion() {
2294
+ function getInstallCommand(pm, version) {
2295
+ switch (pm) {
2296
+ case "pnpm": return `pnpm install -g ${PACKAGE_NAME}@${version}`;
2297
+ case "yarn": return `yarn global add ${PACKAGE_NAME}@${version}`;
2298
+ case "bun": return `bun install -g ${PACKAGE_NAME}@${version}`;
2299
+ case "npm": return `npm install -g ${PACKAGE_NAME}@${version}`;
2300
+ }
2301
+ }
2302
+ async function fetchLatestVersion() {
1704
2303
  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";
2304
+ const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`);
2305
+ if (!res.ok) return null;
2306
+ return (await res.json()).version ?? null;
2307
+ } catch {
2308
+ return null;
2309
+ }
2310
+ }
2311
+ async function autoUpdate() {
2312
+ const pm = detectPackageManager();
2313
+ if (!pm) return;
2314
+ const currentVersion = "0.0.0-beta.10";
2315
+ const latestVersion = await fetchLatestVersion();
2316
+ if (!latestVersion || latestVersion === currentVersion) return;
2317
+ const installCmd = getInstallCommand(pm, latestVersion);
2318
+ const spin = spinner(`Atualizando CLI ${chalk.dim(currentVersion)} → ${chalk.bold(latestVersion)}...`);
2319
+ try {
2320
+ execSync(installCmd, { stdio: "ignore" });
2321
+ spin.stop();
2322
+ console.log(chalk.green(`\n✓ CLI atualizada: ${chalk.bold(latestVersion)}\n`));
2323
+ } catch {
2324
+ spin.stop();
2325
+ console.log(chalk.yellow(`\n⚠ Não foi possível atualizar automaticamente. Execute manualmente:\n ${installCmd}\n`));
2326
+ }
1715
2327
  }
1716
- function prepareExtraFiles(detection, serviceConfig) {
2328
+
2329
+ //#endregion
2330
+ //#region src/commands/deploy.ts
2331
+ /**
2332
+ * If a Dockerfile exists in a subdirectory (rootDirectory), copy it to tar root
2333
+ * so BuildKit can find it. If no Dockerfile exists anywhere, return nothing —
2334
+ * the server will generate one with nixpacks.
2335
+ */
2336
+ function prepareExtraFiles(_detection, serviceConfig) {
1717
2337
  if (existsSync(resolve(process.cwd(), "Dockerfile"))) return [];
1718
2338
  const rootDir = serviceConfig?.rootDirectory || ".";
1719
2339
  const serviceDockerfilePath = resolve(process.cwd(), rootDir, "Dockerfile");
@@ -1721,89 +2341,71 @@ function prepareExtraFiles(detection, serviceConfig) {
1721
2341
  name: "Dockerfile",
1722
2342
  content: readFileSync(serviceDockerfilePath, "utf-8")
1723
2343
  }];
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;
2344
+ return [];
1749
2345
  }
1750
- function computeExtraFilesForServices(services) {
2346
+ async function computeExtraFilesForServices(services) {
1751
2347
  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 {
2348
+ const client = await getClient();
2349
+ const results = [];
2350
+ const allWarnings = [];
2351
+ for (const svc of services) {
2352
+ const warnings = runPreDeployChecks(resolveServiceConf(velozConfig, svc.serviceId)?.rootDirectory || ".");
2353
+ if (warnings.length > 0) allWarnings.push({
2354
+ service: svc.serviceName,
2355
+ warnings
2356
+ });
2357
+ }
2358
+ if (allWarnings.length > 0) for (const { service, warnings } of allWarnings) output({
2359
+ type: "service_warnings",
2360
+ service,
2361
+ warnings: warnings.map((w) => ({
2362
+ message: w.message,
2363
+ hint: w.hint
2364
+ }))
2365
+ }, () => {
2366
+ console.log(chalk.yellow(`\n ${chalk.bold(service)}:`));
2367
+ printDeployWarnings(warnings);
2368
+ });
2369
+ for (const svc of services) {
2370
+ const serviceConf = resolveServiceConf(velozConfig, svc.serviceId);
2371
+ if (serviceConf) await syncServiceConfig(client, svc.serviceId, serviceConf);
2372
+ const detection = detectLocalRepo(serviceConf?.rootDirectory || ".");
2373
+ results.push({
1770
2374
  ...svc,
1771
2375
  extraFiles: prepareExtraFiles(detection, serviceConf)
1772
- };
1773
- });
2376
+ });
2377
+ }
2378
+ return results;
1774
2379
  }
1775
- async function triggerDeploy(serviceId, serviceName) {
2380
+ async function triggerDeploy(serviceId, serviceName, preDetection) {
1776
2381
  const spinUpload = spinner(serviceName ? `Fazendo upload ${chalk.bold(serviceName)}...` : "Fazendo upload do código...");
1777
2382
  try {
1778
2383
  const sizeInBytes = await calculateDirectorySize(process.cwd());
1779
2384
  const sizeMB = Math.round(sizeInBytes / (1024 * 1024) * 10) / 10;
1780
2385
  if (sizeMB > 5) spinUpload.text = `Fazendo upload (${sizeMB} MB)...`;
1781
2386
  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
- }
2387
+ const serviceConf = resolveServiceConf(loadConfig(), serviceId);
2388
+ if (serviceConf) await syncServiceConfig(client, serviceId, serviceConf);
2389
+ const extraFiles = prepareExtraFiles(preDetection ?? detectLocalRepo(), serviceConf);
2390
+ const warnings = runPreDeployChecks(serviceConf?.rootDirectory || ".");
2391
+ if (warnings.length > 0) {
2392
+ spinUpload.stop();
2393
+ printDeployWarnings(warnings);
2394
+ spinUpload.start();
1798
2395
  }
1799
- const extraFiles = prepareExtraFiles(detectLocalRepo(), serviceConf);
1800
2396
  spinUpload.text = "Iniciando deploy...";
1801
2397
  const deployment = await withRetry(() => client.deployments.create({ serviceId }));
1802
2398
  spinUpload.text = "Fazendo upload do código...";
1803
- await withRetry(() => uploadSource(authConfig.apiUrl, deployment.id, process.cwd(), authConfig.apiKey, extraFiles));
2399
+ await withRetry(() => uploadSource(deployment.id, process.cwd(), extraFiles));
1804
2400
  spinUpload.stop();
1805
- success("Deploy iniciado com sucesso!");
1806
- await streamDeploymentLogs(client, deployment.id, serviceId, serviceName);
2401
+ success(serviceName ? `Upload de ${chalk.bold(serviceName)} concluído` : "Upload concluído");
2402
+ setupSigintHandler();
2403
+ trackDeployment(deployment.id);
2404
+ try {
2405
+ await streamDeploymentLogs(deployment.id, serviceId, serviceName);
2406
+ } finally {
2407
+ untrackDeployment(deployment.id);
2408
+ }
1807
2409
  } catch (error) {
1808
2410
  spinUpload.stop();
1809
2411
  handleError(error);
@@ -1833,19 +2435,26 @@ function readLocalFile(path) {
1833
2435
  }
1834
2436
  }
1835
2437
  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();
2438
+ const { type: serviceType, ...rest } = settings;
2439
+ output({
2440
+ type: "service_summary",
2441
+ serviceType,
2442
+ ...rest
2443
+ }, () => {
2444
+ console.log();
2445
+ console.log(chalk.dim("".repeat(40)));
2446
+ console.log(` ${chalk.bold("Nome:")} ${settings.name}`);
2447
+ console.log(` ${chalk.bold("Tipo:")} ${settings.type === "WEB" ? "Serviço Web" : "Site Estático"}`);
2448
+ console.log(` ${chalk.bold("Branch:")} ${settings.branch}`);
2449
+ if (settings.framework) console.log(` ${chalk.bold("Framework:")} ${settings.framework}`);
2450
+ if (settings.packageManager) console.log(` ${chalk.bold("Package Mgr:")} ${settings.packageManager}`);
2451
+ if (settings.buildCommand) console.log(` ${chalk.bold("Build:")} ${settings.buildCommand}`);
2452
+ if (settings.startCommand) console.log(` ${chalk.bold("Start:")} ${settings.startCommand}`);
2453
+ if (settings.outputDir) console.log(` ${chalk.bold("Output:")} ${settings.outputDir}`);
2454
+ if (settings.port) console.log(` ${chalk.bold("Porta:")} ${settings.port}`);
2455
+ console.log(chalk.dim("─".repeat(40)));
2456
+ console.log();
2457
+ });
1849
2458
  }
1850
2459
  function detectLocalRepo(basePath = ".") {
1851
2460
  const files = {};
@@ -1914,20 +2523,21 @@ function detectLocalRepo(basePath = ".") {
1914
2523
  }
1915
2524
  return analyzeRepo(files);
1916
2525
  }
1917
- async function promptEnvVars(client, serviceId, detectedVars) {
2526
+ async function promptEnvVars(serviceId, detectedVars) {
1918
2527
  if (detectedVars.length === 0) return;
1919
2528
  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}`);
2529
+ for (const v of detectedVars) console.log(` • ${v}`);
1921
2530
  console.log();
1922
2531
  if (!await promptConfirm("Deseja preencher as variáveis agora?", false)) return;
1923
2532
  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;
2533
+ for (const key of detectedVars) {
2534
+ const val = await prompt(` ${chalk.bold(key)}:`);
2535
+ if (val) vars[key] = val;
1927
2536
  }
1928
2537
  const filled = Object.keys(vars).length;
1929
2538
  if (filled > 0) {
1930
2539
  const spinVars = spinner("Definindo variáveis de ambiente...");
2540
+ const client = await getClient();
1931
2541
  await withRetry(() => client.envVars.setBulk({
1932
2542
  serviceId,
1933
2543
  vars
@@ -1936,7 +2546,8 @@ async function promptEnvVars(client, serviceId, detectedVars) {
1936
2546
  success(`${filled} variável(is) definida(s).`);
1937
2547
  }
1938
2548
  }
1939
- async function createServiceFlow(client, projectId, projectName, repoName) {
2549
+ async function createServiceFlow(projectId, projectName, repoName, opts = {}) {
2550
+ const client = await getClient();
1940
2551
  const branch = getGitBranch();
1941
2552
  const spinDetect = spinner("Detectando framework...");
1942
2553
  const detection = detectLocalRepo();
@@ -1944,36 +2555,61 @@ async function createServiceFlow(client, projectId, projectName, repoName) {
1944
2555
  const pm = detection.packageManager;
1945
2556
  if (detection.isMonorepo && detection.monorepoApps.length > 0) {
1946
2557
  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));
2558
+ const allApps = detection.monorepoApps.map((a) => ({
2559
+ name: a.name,
2560
+ root: a.path,
2561
+ framework: a.framework?.name ?? null,
2562
+ buildCommand: a.framework?.buildCommand ?? null,
2563
+ startCommand: a.framework?.startCommand ?? null,
2564
+ port: a.framework?.port ?? 3e3
2565
+ }));
2566
+ let selectedApps;
2567
+ if (opts.yes) if (opts.app) {
2568
+ selectedApps = allApps.filter((a) => a.root === opts.app);
2569
+ if (selectedApps.length === 0) {
2570
+ output({
2571
+ type: "error",
2572
+ message: `App '${opts.app}' não encontrado.`,
2573
+ available: allApps.map((a) => a.root)
2574
+ }, () => {
2575
+ const available = allApps.map((a) => ` • ${a.root}`).join("\n");
2576
+ console.error(chalk.red(`\n✗ App '${opts.app}' não encontrado.\n\nApps disponíveis:\n${available}`));
2577
+ });
2578
+ process.exit(1);
2579
+ }
2580
+ } else selectedApps = allApps;
2581
+ else {
2582
+ const selectedPaths = await promptMultiSelect("Quais apps deseja fazer o deploy?", allApps.map((app) => ({
2583
+ label: `${app.name}${app.framework ? ` (${app.framework})` : ""} — ${app.root}`,
2584
+ value: app.root
2585
+ })));
2586
+ selectedApps = allApps.filter((a) => selectedPaths.includes(a.root));
2587
+ }
1952
2588
  for (const app of selectedApps) {
1953
- const fw$1 = app.framework;
1954
2589
  console.log(chalk.cyan(`\n── ${app.name} ──`));
1955
2590
  printSummary({
1956
2591
  name: app.name,
1957
- type: fw$1?.type ?? "WEB",
1958
- rootDir: app.path,
2592
+ type: "WEB",
2593
+ rootDir: app.root,
1959
2594
  branch,
1960
- framework: fw$1?.label ?? null,
2595
+ framework: app.framework,
1961
2596
  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
2597
+ buildCommand: app.buildCommand,
2598
+ startCommand: app.startCommand,
2599
+ outputDir: null,
2600
+ port: app.port ?? 3e3
1966
2601
  });
1967
2602
  }
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;
2603
+ if (!opts.yes) {
2604
+ if (!await promptConfirm("Confirmar e fazer deploy?")) for (const app of selectedApps) {
2605
+ console.log(chalk.cyan(`\n── Editar: ${app.name} ──`));
2606
+ const newBuild = await prompt(` Build command: ${chalk.dim(`(${app.buildCommand ?? "—"})`)}`);
2607
+ if (newBuild) app.buildCommand = newBuild;
2608
+ const newStart = await prompt(` Start command: ${chalk.dim(`(${app.startCommand ?? "—"})`)}`);
2609
+ if (newStart) app.startCommand = newStart;
2610
+ const newPort = await prompt(` Port: ${chalk.dim(`(${app.port ?? 3e3})`)}`);
2611
+ if (newPort) app.port = parseInt(newPort, 10) || app.port;
2612
+ }
1977
2613
  }
1978
2614
  const config = {
1979
2615
  version: "1.0",
@@ -1986,18 +2622,17 @@ async function createServiceFlow(client, projectId, projectName, repoName) {
1986
2622
  };
1987
2623
  const createdServices = [];
1988
2624
  for (const app of selectedApps) {
1989
- const fw$1 = app.framework;
1990
2625
  console.log(chalk.cyan(`\n── Criando serviço: ${app.name} ──\n`));
1991
2626
  const spinService$1 = spinner(`Criando serviço ${chalk.bold(app.name)}...`);
1992
2627
  const service$1 = await withRetry(() => client.services.create({
1993
2628
  projectId,
1994
2629
  name: app.name,
1995
- type: fw$1?.type ?? "WEB",
2630
+ type: "WEB",
1996
2631
  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
2632
+ rootDirectory: app.root,
2633
+ buildCommand: app.buildCommand ?? void 0,
2634
+ startCommand: app.startCommand ?? void 0,
2635
+ port: app.port ?? 3e3
2001
2636
  }));
2002
2637
  spinService$1.stop();
2003
2638
  success(`Serviço criado: ${chalk.bold(service$1.name)}`);
@@ -2005,40 +2640,36 @@ async function createServiceFlow(client, projectId, projectName, repoName) {
2005
2640
  service: service$1,
2006
2641
  app
2007
2642
  });
2008
- config.services[app.path] = {
2643
+ config.services[app.root] = {
2009
2644
  id: service$1.id,
2010
2645
  name: service$1.name,
2011
- type: (fw$1?.type ?? "web").toLowerCase(),
2012
- root: app.path,
2646
+ type: "web",
2647
+ root: app.root,
2013
2648
  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,
2649
+ build: app.buildCommand ? { command: app.buildCommand ?? void 0 } : void 0,
2018
2650
  runtime: {
2019
- command: fw$1?.startCommand ?? void 0,
2020
- port: fw$1?.port ?? 3e3
2651
+ command: app.startCommand ?? void 0,
2652
+ port: app.port ?? 3e3
2021
2653
  }
2022
2654
  };
2023
2655
  }
2024
2656
  saveConfig(config);
2025
2657
  info(`Arquivo ${getConfigFileName()} criado na raiz do projeto.`);
2026
- for (const { service: service$1, app } of createdServices) {
2658
+ if (!opts.yes) for (const { service: service$1, app } of createdServices) {
2027
2659
  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);
2660
+ const serviceDetection = detectLocalRepo(app.root);
2661
+ await promptEnvVars(service$1.id, serviceDetection.envVars.map((v) => v.key));
2030
2662
  }
2031
- await deployServicesInParallel(client, createdServices.map(({ service: service$1, app }) => ({
2663
+ await deployServicesInParallel(createdServices.map(({ service: service$1, app }) => ({
2032
2664
  serviceId: service$1.id,
2033
2665
  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
2666
+ path: resolve(process.cwd(), app.root),
2667
+ extraFiles: prepareExtraFiles(detectLocalRepo(app.root), {
2668
+ type: "WEB",
2669
+ buildCommand: app.buildCommand ?? void 0,
2670
+ startCommand: app.startCommand ?? void 0,
2671
+ port: app.port ?? void 0,
2672
+ rootDirectory: app.root
2042
2673
  })
2043
2674
  })));
2044
2675
  return createdServices[createdServices.length - 1]?.service.id || "";
@@ -2049,31 +2680,33 @@ async function createServiceFlow(client, projectId, projectName, repoName) {
2049
2680
  type: fw?.type ?? "WEB",
2050
2681
  rootDir: ".",
2051
2682
  branch,
2052
- framework: fw?.label ?? null,
2683
+ framework: fw?.name ?? null,
2053
2684
  packageManager: pm,
2054
2685
  buildCommand: fw?.buildCommand ?? null,
2055
2686
  startCommand: fw?.startCommand ?? null,
2056
2687
  outputDir: fw?.outputDir ?? null,
2057
2688
  port: fw?.port ?? 3e3
2058
2689
  };
2059
- if (fw) info(`Framework detectado: ${chalk.bold(fw.label)}`);
2690
+ if (fw) info(`Framework detectado: ${chalk.bold(fw.name)}`);
2060
2691
  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;
2692
+ if (!opts.yes) {
2693
+ if (!await promptConfirm("Confirmar e fazer deploy?")) {
2694
+ const newName = await prompt(`Nome do serviço: ${chalk.dim(`(${settings.name})`)}`);
2695
+ if (newName) settings.name = newName;
2696
+ settings.type = await promptSelect("Tipo de serviço:", [{
2697
+ label: "Serviço Web",
2698
+ value: "WEB"
2699
+ }, {
2700
+ label: "Site Estático",
2701
+ value: "STATIC"
2702
+ }]);
2703
+ const newBuild = await prompt(`Build command: ${chalk.dim(`(${settings.buildCommand ?? "—"})`)}`);
2704
+ if (newBuild) settings.buildCommand = newBuild;
2705
+ const newStart = await prompt(`Start command: ${chalk.dim(`(${settings.startCommand ?? "—"})`)}`);
2706
+ if (newStart) settings.startCommand = newStart;
2707
+ const newPort = await prompt(`Port: ${chalk.dim(`(${settings.port})`)}`);
2708
+ if (newPort) settings.port = parseInt(newPort, 10) || settings.port;
2709
+ }
2077
2710
  }
2078
2711
  const spinService = spinner("Criando serviço...");
2079
2712
  const service = await withRetry(() => client.services.create({
@@ -2088,7 +2721,7 @@ async function createServiceFlow(client, projectId, projectName, repoName) {
2088
2721
  }));
2089
2722
  spinService.stop();
2090
2723
  success(`Serviço criado: ${chalk.bold(service.name)}`);
2091
- await promptEnvVars(client, service.id, detection.envVars);
2724
+ if (!opts.yes) await promptEnvVars(service.id, detection.envVars.map((v) => v.key));
2092
2725
  saveConfig({
2093
2726
  version: "1.0",
2094
2727
  project: {
@@ -2115,24 +2748,129 @@ async function createServiceFlow(client, projectId, projectName, repoName) {
2115
2748
  info(`Arquivo ${getConfigFileName()} criado na raiz do projeto.`);
2116
2749
  return service.id;
2117
2750
  }
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) => {
2751
+ /**
2752
+ * Create a new environment by mirroring the base config's services
2753
+ * into a new project on the platform. Saves the environment mapping to veloz.json.
2754
+ */
2755
+ async function createEnvironmentFlow(rawConfig, envName, opts) {
2756
+ const client = await getClient();
2757
+ const projectName = `${rawConfig.project.name}-${envName}`;
2758
+ info(`Criando ambiente "${envName}" — projeto: ${chalk.bold(projectName)}`);
2759
+ if (!opts.yes) {
2760
+ if (!await promptConfirm(`Criar ambiente "${envName}" com ${Object.keys(rawConfig.services).length} serviço(s)?`)) {
2761
+ info("Criação de ambiente cancelada.");
2762
+ process.exit(0);
2763
+ }
2764
+ }
2765
+ const remote = getGitRemote();
2766
+ const spinProject = spinner(`Criando projeto ${chalk.bold(projectName)}...`);
2767
+ const newProject = await withRetry(() => client.projects.create({
2768
+ name: projectName,
2769
+ githubRepoOwner: remote?.owner,
2770
+ githubRepoName: remote?.repo
2771
+ }));
2772
+ spinProject.stop();
2773
+ success(`Projeto criado: ${chalk.bold(newProject.name)}`);
2774
+ const envServices = {};
2775
+ for (const [key, serviceConfig] of Object.entries(rawConfig.services)) {
2776
+ const serviceName = `${serviceConfig.name}-${envName}`;
2777
+ const spinService = spinner(`Criando serviço ${chalk.bold(serviceName)}...`);
2778
+ const service = await withRetry(() => client.services.create({
2779
+ projectId: newProject.id,
2780
+ name: serviceName,
2781
+ type: serviceConfig.type?.toUpperCase() ?? "WEB",
2782
+ branch: serviceConfig.branch ?? "main",
2783
+ rootDirectory: serviceConfig.root,
2784
+ buildCommand: serviceConfig.build?.command ?? void 0,
2785
+ startCommand: serviceConfig.runtime?.command ?? void 0,
2786
+ port: serviceConfig.runtime?.port ?? 3e3
2787
+ }));
2788
+ spinService.stop();
2789
+ success(`Serviço criado: ${chalk.bold(service.name)}`);
2790
+ envServices[key] = {
2791
+ id: service.id,
2792
+ name: serviceName
2793
+ };
2794
+ }
2795
+ const envOverride = {
2796
+ project: {
2797
+ id: newProject.id,
2798
+ name: newProject.name
2799
+ },
2800
+ services: envServices
2801
+ };
2802
+ saveConfig({
2803
+ ...rawConfig,
2804
+ environments: {
2805
+ ...rawConfig.environments,
2806
+ [envName]: envOverride
2807
+ },
2808
+ updated: (/* @__PURE__ */ new Date()).toISOString()
2809
+ });
2810
+ success(`Ambiente "${envName}" salvo em ${getConfigFileName()}`);
2811
+ setActiveEnv(envName);
2812
+ return loadConfig();
2813
+ }
2814
+ function isMultipleOrgsError(error) {
2815
+ if (!(error instanceof Error)) return false;
2816
+ const e = error;
2817
+ return e.data?.code === "MULTIPLE_ORGS" && Array.isArray(e.data?.organizations);
2818
+ }
2819
+ async function resolveOrgIfNeeded(nonInteractive) {
2820
+ if ((await requireAuth({ nonInteractive })).organizationId) return;
2821
+ if (loadConfig()?.project?.id) return;
2822
+ const client = await getClient();
2823
+ try {
2824
+ await client.projects.list();
2825
+ } catch (error) {
2826
+ if (!isMultipleOrgsError(error)) throw error;
2827
+ const orgs = error.data.organizations;
2828
+ info(`Você tem acesso a ${orgs.length} organizações.`);
2829
+ saveConfig$1({ organizationId: await promptSelect("Selecione a organização para este deploy:", orgs.map((org) => ({
2830
+ label: `${org.name} ${chalk.dim(`(${org.slug})`)}`,
2831
+ value: org.id
2832
+ }))) });
2833
+ success("Organização salva.");
2834
+ }
2835
+ }
2836
+ 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("--app <path>", "App do monorepo para deploy (ex: apps/web)").option("-y, --yes", "Auto-confirmar tudo (modo não-interativo)").option("-v, --verbose", "Mostrar logs detalhados do servidor").action(async (options) => {
2119
2837
  if (options.verbose) process.env.VELOZ_VERBOSE = "true";
2120
2838
  try {
2121
- await requireAuth();
2839
+ printBanner("Deploy");
2840
+ await autoUpdate();
2841
+ await requireAuth({ nonInteractive: options.yes });
2842
+ await resolveOrgIfNeeded(options.yes);
2843
+ const activeEnv = getActiveEnv();
2844
+ if (activeEnv) {
2845
+ const rawConfig = loadRawConfig();
2846
+ if (rawConfig && !rawConfig.environments?.[activeEnv]) {
2847
+ info(`Ambiente "${activeEnv}" não encontrado. Configurando...`);
2848
+ await createEnvironmentFlow(rawConfig, activeEnv, { yes: options.yes });
2849
+ }
2850
+ }
2122
2851
  const configuredServices = await findServicesFromConfig();
2123
2852
  if (configuredServices.length > 0) {
2124
2853
  if (options.service) {
2125
2854
  const found = configuredServices.find((s) => s.key === options.service || s.serviceName.toLowerCase() === options.service.toLowerCase() || s.serviceId === options.service);
2126
2855
  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}`));
2856
+ output({
2857
+ type: "error",
2858
+ message: `Serviço '${options.service}' não encontrado.`,
2859
+ available: configuredServices.map((s) => ({
2860
+ key: s.key,
2861
+ name: s.serviceName
2862
+ }))
2863
+ }, () => {
2864
+ const available = configuredServices.map((s) => ` • ${s.key} (${s.serviceName})`).join("\n");
2865
+ console.error(chalk.red(`\n✗ Serviço '${options.service}' não encontrado.\n\nServiços disponíveis:\n${available}`));
2866
+ });
2129
2867
  process.exit(1);
2130
2868
  }
2131
2869
  await triggerDeploy(found.serviceId, found.serviceName);
2132
2870
  return;
2133
2871
  }
2134
- if (options.all || configuredServices.length === 1) {
2135
- if (configuredServices.length > 1) {
2872
+ if (options.all || options.yes || configuredServices.length === 1) {
2873
+ if (configuredServices.length > 1 && !options.yes) {
2136
2874
  console.log(chalk.cyan(`\n🚀 Fazendo deploy de ${configuredServices.length} serviço(s):\n`));
2137
2875
  for (const service of configuredServices) {
2138
2876
  const relPath = relative(process.cwd(), service.path) || ".";
@@ -2145,10 +2883,7 @@ const deployCommand = new Command("deploy").description("Fazer deploy do serviç
2145
2883
  }
2146
2884
  }
2147
2885
  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
- }
2886
+ else await deployServicesInParallel(await computeExtraFilesForServices(configuredServices));
2152
2887
  return;
2153
2888
  } else {
2154
2889
  console.log(chalk.bold("\n📦 Serviços disponíveis:\n"));
@@ -2165,24 +2900,21 @@ const deployCommand = new Command("deploy").description("Fazer deploy do serviç
2165
2900
  return;
2166
2901
  }
2167
2902
  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
- }
2903
+ else await deployServicesInParallel(await computeExtraFilesForServices(selectedServices));
2172
2904
  return;
2173
2905
  }
2174
2906
  }
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
- }
2907
+ if (!isGitRepo()) handleError(/* @__PURE__ */ new Error("Este diretório não é um repositório git. Inicialize com `git init` e adicione um remote."));
2179
2908
  info("Detectando repositório git...");
2180
2909
  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}`));
2910
+ if (!remote) handleError(/* @__PURE__ */ new Error("Nenhum remote 'origin' encontrado. Adicione um remote git."));
2911
+ output({
2912
+ type: "git_repo",
2913
+ owner: remote.owner,
2914
+ repo: remote.repo
2915
+ }, () => {
2916
+ console.log(chalk.white(` ${chalk.bold("Repositório:")} ${remote.owner}/${remote.repo}`));
2917
+ });
2186
2918
  const client = await getClient();
2187
2919
  const spin = spinner("Buscando projeto...");
2188
2920
  const project = await withRetry(() => client.projects.findByRepo({
@@ -2198,6 +2930,10 @@ const deployCommand = new Command("deploy").description("Fazer deploy do serviç
2198
2930
  const svc = project.services[0];
2199
2931
  serviceId = svc.id;
2200
2932
  serviceName = svc.name;
2933
+ } else if (options.yes) {
2934
+ const svc = project.services[0];
2935
+ serviceId = svc.id;
2936
+ serviceName = svc.name;
2201
2937
  } else {
2202
2938
  serviceId = await promptSelect("Selecione o serviço:", project.services.map((s) => ({
2203
2939
  label: `${s.name} (${s.type} — branch: ${s.branch})`,
@@ -2206,6 +2942,8 @@ const deployCommand = new Command("deploy").description("Fazer deploy do serviç
2206
2942
  serviceName = project.services.find((s) => s.id === serviceId)?.name ?? "";
2207
2943
  }
2208
2944
  const selectedService = project.services.find((s) => s.id === serviceId);
2945
+ const detection = detectLocalRepo();
2946
+ const detectedType = selectedService?.type?.toLowerCase() ?? "web";
2209
2947
  saveConfig({
2210
2948
  version: "1.0",
2211
2949
  project: {
@@ -2215,24 +2953,27 @@ const deployCommand = new Command("deploy").description("Fazer deploy do serviç
2215
2953
  services: { main: {
2216
2954
  id: serviceId,
2217
2955
  name: serviceName,
2218
- type: selectedService?.type?.toLowerCase() ?? "web",
2956
+ type: detectedType,
2219
2957
  root: ".",
2220
2958
  branch: selectedService?.branch
2221
2959
  } },
2222
2960
  created: (/* @__PURE__ */ new Date()).toISOString()
2223
2961
  });
2224
2962
  info(`Arquivo ${getConfigFileName()} criado na raiz do projeto.`);
2225
- await triggerDeploy(serviceId);
2963
+ await triggerDeploy(serviceId, void 0, detection);
2226
2964
  return;
2227
2965
  }
2228
2966
  if (project && project.services.length === 0) {
2229
2967
  info(`Projeto encontrado: ${chalk.bold(project.name)}`);
2230
2968
  info("Nenhum serviço configurado. Vamos criar um.");
2231
- await triggerDeploy(await createServiceFlow(client, project.id, project.name, remote.repo));
2969
+ await triggerDeploy(await createServiceFlow(project.id, project.name, remote.repo, {
2970
+ yes: options.yes,
2971
+ app: options.app
2972
+ }));
2232
2973
  return;
2233
2974
  }
2234
2975
  info("Projeto não encontrado. Vamos criar um novo.");
2235
- const projectName = await prompt(`Nome do projeto: ${chalk.dim(`(${remote.repo})`)}`) || remote.repo;
2976
+ const projectName = options.yes ? remote.repo : await prompt(`Nome do projeto: ${chalk.dim(`(${remote.repo})`)}`) || remote.repo;
2236
2977
  const spinProject = spinner("Criando projeto...");
2237
2978
  const newProject = await withRetry(() => client.projects.create({
2238
2979
  name: projectName,
@@ -2241,7 +2982,10 @@ const deployCommand = new Command("deploy").description("Fazer deploy do serviç
2241
2982
  }));
2242
2983
  spinProject.stop();
2243
2984
  success(`Projeto criado: ${chalk.bold(newProject.name)}`);
2244
- await triggerDeploy(await createServiceFlow(client, newProject.id, newProject.name, remote.repo));
2985
+ await triggerDeploy(await createServiceFlow(newProject.id, newProject.name, remote.repo, {
2986
+ yes: options.yes,
2987
+ app: options.app
2988
+ }));
2245
2989
  } catch (error) {
2246
2990
  handleError(error);
2247
2991
  }
@@ -2283,10 +3027,14 @@ const TAG_COLORS = [
2283
3027
  chalk.red
2284
3028
  ];
2285
3029
  function getServiceTag(name, maxLen, colorIndex) {
3030
+ const mode = getOutputMode();
3031
+ if (mode === "json" || mode === "github-actions" || mode === "plain") return `[${name.padEnd(maxLen)}]`;
2286
3032
  const color = TAG_COLORS[colorIndex % TAG_COLORS.length];
2287
3033
  return color(`[${name.padEnd(maxLen)}]`);
2288
3034
  }
2289
3035
  function getServiceHeader(name, colorIndex) {
3036
+ const mode = getOutputMode();
3037
+ if (mode === "json" || mode === "github-actions" || mode === "plain") return `── ${name} ──`;
2290
3038
  const color = TAG_COLORS[colorIndex % TAG_COLORS.length];
2291
3039
  return color(`── ${name} ──`);
2292
3040
  }
@@ -2385,7 +3133,8 @@ function resolveAllServices(serviceFlag) {
2385
3133
  function formatTime(timestamp) {
2386
3134
  return chalk.dim(new Date(timestamp).toLocaleTimeString("pt-BR"));
2387
3135
  }
2388
- async function streamFollow(client, services, maxNameLen, tailLines) {
3136
+ async function streamFollow(services, maxNameLen, tailLines) {
3137
+ const client = await getClient();
2389
3138
  const showTags = services.length > 1;
2390
3139
  const streams = services.map(async ({ service, index }) => {
2391
3140
  const tag = showTags ? `${getServiceTag(service.name, maxNameLen, index)} ` : "";
@@ -2394,14 +3143,22 @@ async function streamFollow(client, services, maxNameLen, tailLines) {
2394
3143
  serviceId: service.id,
2395
3144
  tailLines: Math.ceil(tailLines / services.length)
2396
3145
  });
2397
- for await (const entry of stream) console.log(`${tag}${formatTime(entry.timestamp)} ${entry.message}`);
3146
+ for await (const entry of stream) output({
3147
+ type: "log_entry",
3148
+ service: service.name,
3149
+ timestamp: entry.timestamp,
3150
+ message: entry.message
3151
+ }, () => {
3152
+ console.log(`${tag}${formatTime(entry.timestamp)} ${entry.message}`);
3153
+ });
2398
3154
  } catch {
2399
3155
  console.log(`${tag}${chalk.red("Erro ao conectar ao stream de logs")}`);
2400
3156
  }
2401
3157
  });
2402
3158
  await Promise.allSettled(streams);
2403
3159
  }
2404
- async function fetchRecent(client, services, maxNameLen, tailLines) {
3160
+ async function fetchRecent(services, maxNameLen, tailLines) {
3161
+ const client = await getClient();
2405
3162
  const showTags = services.length > 1;
2406
3163
  const allEntries = (await Promise.allSettled(services.map(async ({ service, index }) => {
2407
3164
  return (await client.logs.getRecent({
@@ -2417,28 +3174,36 @@ async function fetchRecent(client, services, maxNameLen, tailLines) {
2417
3174
  info("Nenhum log encontrado.");
2418
3175
  return;
2419
3176
  }
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.`);
3177
+ output({
3178
+ type: "logs",
3179
+ entries: allEntries.map((e) => ({
3180
+ service: e.serviceName,
3181
+ timestamp: e.timestamp,
3182
+ message: e.message
3183
+ }))
3184
+ }, () => {
3185
+ console.log();
3186
+ for (const entry of allEntries) {
3187
+ const tag = showTags ? `${getServiceTag(entry.serviceName, maxNameLen, entry.serviceIndex)} ` : "";
3188
+ console.log(`${tag}${formatTime(entry.timestamp)} ${entry.message}`);
3189
+ }
3190
+ console.log();
3191
+ info(`Mostrando ${allEntries.length} linha(s). Use ${chalk.bold("--follow")} para acompanhar em tempo real.`);
3192
+ });
2427
3193
  }
2428
3194
  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
3195
  const spin = spinner("Carregando logs...");
2430
3196
  try {
2431
3197
  const { services, maxNameLen } = resolveAllServices(opts.service);
2432
- const client = await getClient();
2433
3198
  const tailLines = parseInt(opts.tail, 10) || 50;
2434
3199
  if (opts.follow) {
2435
3200
  spin.text = services.length > 1 ? `Conectando a ${services.length} serviço(s)...` : "Conectando ao streaming de logs...";
2436
3201
  spin.stop();
2437
3202
  info("Streaming de logs ativo. Pressione Ctrl+C para sair.\n");
2438
- await streamFollow(client, services, maxNameLen, tailLines);
3203
+ await streamFollow(services, maxNameLen, tailLines);
2439
3204
  } else {
2440
3205
  spin.stop();
2441
- await fetchRecent(client, services, maxNameLen, tailLines);
3206
+ await fetchRecent(services, maxNameLen, tailLines);
2442
3207
  }
2443
3208
  } catch (error) {
2444
3209
  spin.stop();
@@ -2472,7 +3237,11 @@ envCommand.command("list").alias("listar").description("Listar variáveis de amb
2472
3237
  chalk.bold(v.key),
2473
3238
  chalk.dim(v.maskedValue),
2474
3239
  new Date(v.updatedAt).toLocaleDateString("pt-BR")
2475
- ]));
3240
+ ]), envVars.map((v) => ({
3241
+ key: v.key,
3242
+ maskedValue: v.maskedValue,
3243
+ updatedAt: v.updatedAt
3244
+ })));
2476
3245
  }
2477
3246
  spin.stop();
2478
3247
  if (totalVars === 0 && !showHeaders) info("Nenhuma variável de ambiente configurada.");
@@ -2490,15 +3259,13 @@ envCommand.command("set <pares...>").description("Definir variável de ambiente
2490
3259
  const eqIndex = par.indexOf("=");
2491
3260
  if (eqIndex === -1) {
2492
3261
  spin.stop();
2493
- console.error(chalk.red(`\n✗ Formato inválido: "${par}". Use CHAVE=VALOR.`));
2494
- process.exit(1);
3262
+ handleError(/* @__PURE__ */ new Error(`Formato inválido: "${par}". Use CHAVE=VALOR.`));
2495
3263
  }
2496
3264
  const key = par.slice(0, eqIndex);
2497
3265
  const value = par.slice(eqIndex + 1);
2498
3266
  if (!key) {
2499
3267
  spin.stop();
2500
- console.error(chalk.red("\n✗ Chave não pode estar vazia."));
2501
- process.exit(1);
3268
+ handleError(/* @__PURE__ */ new Error("Chave não pode estar vazia."));
2502
3269
  }
2503
3270
  await client.envVars.set({
2504
3271
  serviceId,
@@ -2540,38 +3307,32 @@ envCommand.command("import [arquivo]").description("Importar variáveis de ambie
2540
3307
  let envContent = "";
2541
3308
  if (arquivo) {
2542
3309
  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
- }
3310
+ if (!existsSync(filePath)) handleError(/* @__PURE__ */ new Error(`Arquivo não encontrado: ${arquivo}`));
2547
3311
  envContent = readFileSync(filePath, "utf-8");
2548
3312
  info(`Importando de ${chalk.bold(arquivo)}...`);
2549
3313
  } 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
3314
  const rl = readline.createInterface({
2553
3315
  input: process.stdin,
2554
- output: process.stdout
3316
+ output: isInteractive() ? process.stdout : void 0
2555
3317
  });
2556
- const lines$1 = [];
3318
+ if (isInteractive()) {
3319
+ console.log(chalk.cyan("\n📋 Modo de colagem interativo"));
3320
+ console.log(chalk.dim("Cole seu conteúdo .env abaixo. Pressione Ctrl+D (ou Ctrl+Z no Windows) quando terminar:\n"));
3321
+ }
3322
+ const lines = [];
2557
3323
  await new Promise((resolve$1) => {
2558
- rl.on("line", (line) => {
2559
- lines$1.push(line);
2560
- });
2561
- rl.on("close", () => {
2562
- resolve$1();
2563
- });
3324
+ rl.on("line", (line) => lines.push(line));
3325
+ rl.on("close", () => resolve$1());
2564
3326
  });
2565
- envContent = lines$1.join("\n");
3327
+ envContent = lines.join("\n");
2566
3328
  }
2567
3329
  const envVars = {};
2568
- const lines = envContent.split("\n");
2569
- for (const line of lines) {
3330
+ for (const line of envContent.split("\n")) {
2570
3331
  const trimmed = line.trim();
2571
3332
  if (!trimmed || trimmed.startsWith("#")) continue;
2572
3333
  const eqIndex = trimmed.indexOf("=");
2573
3334
  if (eqIndex === -1) continue;
2574
- let key = trimmed.slice(0, eqIndex).trim();
3335
+ const key = trimmed.slice(0, eqIndex).trim();
2575
3336
  let value = trimmed.slice(eqIndex + 1).trim();
2576
3337
  if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
2577
3338
  value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\\\/g, "\\");
@@ -2579,7 +3340,7 @@ envCommand.command("import [arquivo]").description("Importar variáveis de ambie
2579
3340
  }
2580
3341
  const varsCount = Object.keys(envVars).length;
2581
3342
  if (varsCount === 0) {
2582
- console.error(chalk.yellow("Nenhuma variável válida encontrada."));
3343
+ warn("Nenhuma variável válida encontrada.");
2583
3344
  return;
2584
3345
  }
2585
3346
  console.log(chalk.bold("\n📝 Variáveis a serem importadas:\n"));
@@ -2628,20 +3389,31 @@ envCommand.command("export [arquivo]").description("Exportar variáveis de ambie
2628
3389
  return;
2629
3390
  }
2630
3391
  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
- }
3392
+ output({
3393
+ type: "env_export",
3394
+ vars: envVars.map((v) => ({
3395
+ key: v.key,
3396
+ maskedValue: v.maskedValue
3397
+ }))
3398
+ }, () => {
3399
+ if (arquivo) {
3400
+ writeFileSync(resolve(process.cwd(), arquivo), envContent + "\n", "utf-8");
3401
+ success(`Variáveis exportadas para ${chalk.bold(arquivo)}`);
3402
+ console.log(chalk.dim("Nota: Valores estão mascarados por segurança."));
3403
+ } else {
3404
+ console.log(chalk.bold("\n# Variáveis de Ambiente (valores mascarados)\n"));
3405
+ console.log(envContent);
3406
+ console.log();
3407
+ }
3408
+ });
2640
3409
  } catch (error) {
2641
3410
  spin.stop();
2642
3411
  handleError(error);
2643
3412
  }
2644
3413
  });
3414
+ function warn(message) {
3415
+ console.log(chalk.yellow(`⚠ ${message}`));
3416
+ }
2645
3417
 
2646
3418
  //#endregion
2647
3419
  //#region src/commands/domains.ts
@@ -2674,12 +3446,18 @@ domainsCommand.command("list").alias("listar").description("Listar domínios dos
2674
3446
  "TLS",
2675
3447
  "Tipo"
2676
3448
  ], domains.map((d) => [
2677
- d.id.slice(0, 8),
3449
+ d.id,
2678
3450
  chalk.bold(d.domain),
2679
3451
  d.verified ? chalk.green("✓ Sim") : chalk.yellow("✗ Não"),
2680
3452
  tlsStatusLabels[d.tlsStatus] ?? d.tlsStatus,
2681
3453
  d.isAutoGenerated ? "Auto" : "Personalizado"
2682
- ]));
3454
+ ]), domains.map((d) => ({
3455
+ id: d.id,
3456
+ domain: d.domain,
3457
+ verified: d.verified,
3458
+ tlsStatus: d.tlsStatus,
3459
+ isAutoGenerated: d.isAutoGenerated
3460
+ })));
2683
3461
  }
2684
3462
  spin.stop();
2685
3463
  if (totalDomains === 0 && !showHeaders) info("Nenhum domínio configurado.");
@@ -2697,12 +3475,19 @@ domainsCommand.command("add <dominio>").alias("adicionar").description("Adiciona
2697
3475
  domain: dominio
2698
3476
  });
2699
3477
  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)}`)}`);
3478
+ output({
3479
+ type: "domain_added",
3480
+ id: result.id,
3481
+ domain: dominio,
3482
+ dnsInstruction: result.dnsInstruction
3483
+ }, () => {
3484
+ success(`Domínio ${chalk.bold(dominio)} adicionado com sucesso!`);
3485
+ console.log();
3486
+ console.log(chalk.yellow.bold(" Instruções de DNS:"));
3487
+ console.log(chalk.white(` ${result.dnsInstruction}`));
3488
+ console.log();
3489
+ info(`Após configurar o DNS, execute: ${chalk.bold(`veloz domains verify ${result.id}`)}`);
3490
+ });
2706
3491
  } catch (error) {
2707
3492
  spin.stop();
2708
3493
  handleError(error);
@@ -2713,13 +3498,19 @@ domainsCommand.command("verify <domainId>").alias("verificar").description("Veri
2713
3498
  try {
2714
3499
  const result = await (await getClient()).domains.verify({ domainId });
2715
3500
  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
- }
3501
+ output({
3502
+ type: "domain_verified",
3503
+ domain: result.domain.domain,
3504
+ verified: result.verified
3505
+ }, () => {
3506
+ if (result.verified) {
3507
+ success(`Domínio ${chalk.bold(result.domain.domain)} verificado com sucesso!`);
3508
+ info("O certificado TLS será provisionado automaticamente.");
3509
+ } else {
3510
+ console.log(chalk.yellow(`\n⚠ Domínio ${chalk.bold(result.domain.domain)} ainda não verificado.`));
3511
+ info("Verifique se o CNAME foi propagado e tente novamente.");
3512
+ }
3513
+ });
2723
3514
  } catch (error) {
2724
3515
  spin.stop();
2725
3516
  handleError(error);
@@ -2762,14 +3553,22 @@ configCommand.command("show").description("Mostrar configurações atuais dos se
2762
3553
  const client = await getClient();
2763
3554
  const showHeaders = services.length > 1;
2764
3555
  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
- }
3556
+ const configs = [];
3557
+ for (const { service: svcConfig } of services) configs.push(await client.services.get({ serviceId: svcConfig.id }));
2772
3558
  spin.stop();
3559
+ output({
3560
+ type: "service_configs",
3561
+ services: configs
3562
+ }, () => {
3563
+ for (let i = 0; i < services.length; i++) {
3564
+ const { service: svcConfig, index } = services[i];
3565
+ const service = configs[i];
3566
+ if (showHeaders) console.log(`\n${getServiceHeader(svcConfig.name, index)}\n`);
3567
+ else console.log(chalk.bold("\n📋 Configurações do Serviço\n"));
3568
+ printServiceConfig(service);
3569
+ console.log();
3570
+ }
3571
+ });
2773
3572
  } catch (error) {
2774
3573
  handleError(error);
2775
3574
  }
@@ -2800,13 +3599,18 @@ configCommand.command("set").description("Atualizar configurações do serviço"
2800
3599
  });
2801
3600
  spin.stop();
2802
3601
  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.");
3602
+ output({
3603
+ type: "config_updated",
3604
+ updates
3605
+ }, () => {
3606
+ console.log(chalk.dim("\nValores atualizados:"));
3607
+ for (const [key, value] of Object.entries(updates)) {
3608
+ const displayKey = key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()).trim();
3609
+ console.log(` ${chalk.bold(displayKey)}: ${formatValue(value)}`);
3610
+ }
3611
+ console.log();
3612
+ info("Execute 'veloz deploy' para aplicar as mudanças.");
3613
+ });
2810
3614
  } catch (error) {
2811
3615
  handleError(error);
2812
3616
  }
@@ -2825,8 +3629,8 @@ configCommand.command("edit").description("Editar configurações interativament
2825
3629
  if (name) updates.name = name;
2826
3630
  const buildCmd = await prompt(`Build command ${chalk.dim(`(${service.buildCommand || "—"})`)}: `);
2827
3631
  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;
3632
+ const startCmd = await prompt(`Start command ${chalk.dim(`(${service.startCommand || "—"})`)}: `);
3633
+ if (startCmd) updates.startCommand = startCmd === "none" ? null : startCmd;
2830
3634
  const port = await prompt(`Porta ${chalk.dim(`(${service.port})`)}: `);
2831
3635
  if (port) updates.port = parseInt(port, 10);
2832
3636
  const rootDir = await prompt(`Diretório raiz ${chalk.dim(`(${service.rootDirectory || "/"})`)}: `);
@@ -2888,15 +3692,9 @@ configCommand.command("reset").description("Resetar configurações para os padr
2888
3692
  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
3693
  try {
2890
3694
  const config = loadConfig();
2891
- if (!config) {
2892
- console.error(chalk.red("\n✗ Nenhum projeto configurado. Execute 'veloz deploy' primeiro."));
2893
- process.exit(1);
2894
- }
3695
+ if (!config) handleError(/* @__PURE__ */ new Error("Nenhum projeto configurado. Execute 'veloz deploy' primeiro."));
2895
3696
  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
- }
3697
+ if (services.length === 0) handleError(/* @__PURE__ */ new Error("Nenhum serviço encontrado na configuração."));
2900
3698
  if (services.length === 1) {
2901
3699
  info("Apenas um serviço configurado — já é o padrão.");
2902
3700
  return;
@@ -2906,13 +3704,8 @@ const useCommand = new Command("use").description("Selecionar qual serviço usar
2906
3704
  if (servicoArg) {
2907
3705
  const found = services.find(([key, service]) => key === servicoArg || service.name.toLowerCase() === servicoArg.toLowerCase());
2908
3706
  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);
3707
+ const available = services.map(([key, svc]) => ` • ${key} (${svc.name})`).join("\n");
3708
+ handleError(/* @__PURE__ */ new Error(`Serviço '${servicoArg}' não encontrado.\n\nServiços disponíveis:\n${available}`));
2916
3709
  }
2917
3710
  selectedKey = found[0];
2918
3711
  } else selectedKey = await promptSelect("Qual serviço usar como padrão?", services.map(([key, service]) => ({
@@ -2921,9 +3714,138 @@ const useCommand = new Command("use").description("Selecionar qual serviço usar
2921
3714
  })));
2922
3715
  setDefaultServiceKey(selectedKey);
2923
3716
  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.`));
3717
+ output({
3718
+ type: "default_service",
3719
+ key: selectedKey,
3720
+ name: selectedService.name
3721
+ }, () => {
3722
+ success(`Serviço padrão: ${chalk.bold(selectedService.name)} (${selectedKey})`);
3723
+ console.log(chalk.dim(`\nComandos como ${chalk.white("logs")}, ${chalk.white("env")}, ${chalk.white("config")} vão usar este serviço automaticamente.`));
3724
+ console.log(chalk.dim(`Use ${chalk.white("--service <nome>")} para sobrescrever pontualmente.`));
3725
+ });
3726
+ } catch (error) {
3727
+ handleError(error);
3728
+ }
3729
+ });
3730
+
3731
+ //#endregion
3732
+ //#region src/commands/apikey.ts
3733
+ async function authFetch(path, options = {}) {
3734
+ const config = await requireAuth();
3735
+ const url = `${config.apiUrl}/api/auth${path}`;
3736
+ const res = await fetch(url, {
3737
+ ...options,
3738
+ headers: {
3739
+ "Content-Type": "application/json",
3740
+ Authorization: `Bearer ${config.apiKey}`,
3741
+ ...options.headers
3742
+ }
3743
+ });
3744
+ if (!res.ok) {
3745
+ const body = await res.text().catch(() => "");
3746
+ throw new Error(`Erro ${res.status}: ${body || res.statusText}`);
3747
+ }
3748
+ return res.json();
3749
+ }
3750
+ const apikeyCommand = new Command("apikey").description("Gerenciar chaves de API");
3751
+ apikeyCommand.command("create").description("Criar uma nova chave de API").option("--name <name>", "Nome da chave", "cli").option("--no-expire", "Chave sem expiração").action(async (opts) => {
3752
+ try {
3753
+ const s = spinner("Criando chave de API...");
3754
+ const body = {
3755
+ name: opts.name,
3756
+ prefix: "veloz"
3757
+ };
3758
+ if (opts.expire === false) body.expiresIn = null;
3759
+ const data = await authFetch("/api-key/create", {
3760
+ method: "POST",
3761
+ body: JSON.stringify(body)
3762
+ });
3763
+ s.stop();
3764
+ output({
3765
+ type: "apikey_created",
3766
+ key: data.key,
3767
+ name: data.name,
3768
+ expiresAt: data.expiresAt
3769
+ }, () => {
3770
+ success("Chave de API criada!");
3771
+ console.log();
3772
+ console.log(chalk.bold(" Chave:"), chalk.green(data.key));
3773
+ console.log(chalk.bold(" Nome:"), data.name);
3774
+ if (data.expiresAt) console.log(chalk.bold(" Expira:"), new Date(data.expiresAt).toLocaleDateString("pt-BR"));
3775
+ else console.log(chalk.bold(" Expira:"), "Nunca");
3776
+ console.log();
3777
+ console.log(chalk.yellow(" Guarde esta chave — ela não será exibida novamente."));
3778
+ console.log();
3779
+ console.log(chalk.dim(" Uso em CI:"));
3780
+ console.log(chalk.dim(` VELOZ_API_KEY=${data.key} veloz deploy -y --service web`));
3781
+ console.log();
3782
+ console.log(chalk.dim(" GitHub Actions:"));
3783
+ console.log(chalk.dim(` gh secret set VELOZ_API_KEY --body "${data.key}"`));
3784
+ console.log();
3785
+ });
3786
+ } catch (error) {
3787
+ handleError(error);
3788
+ }
3789
+ });
3790
+ apikeyCommand.command("list").alias("listar").description("Listar chaves de API").action(async () => {
3791
+ try {
3792
+ const s = spinner("Buscando chaves...");
3793
+ const data = await authFetch("/api-key/list", { method: "GET" });
3794
+ s.stop();
3795
+ const keys = Array.isArray(data) ? data : data.apiKeys ?? [];
3796
+ if (!keys || keys.length === 0) {
3797
+ info("Nenhuma chave de API encontrada.");
3798
+ return;
3799
+ }
3800
+ printTable([
3801
+ "Nome",
3802
+ "ID",
3803
+ "Criado",
3804
+ "Expira"
3805
+ ], keys.map((k) => [
3806
+ k.name ?? "-",
3807
+ k.id,
3808
+ new Date(k.createdAt).toLocaleDateString("pt-BR"),
3809
+ k.expiresAt ? new Date(k.expiresAt).toLocaleDateString("pt-BR") : "Nunca"
3810
+ ]), keys.map((k) => ({
3811
+ name: k.name,
3812
+ id: k.id,
3813
+ createdAt: k.createdAt,
3814
+ expiresAt: k.expiresAt
3815
+ })));
3816
+ } catch (error) {
3817
+ handleError(error);
3818
+ }
3819
+ });
3820
+ apikeyCommand.command("delete <keyId>").alias("deletar").description("Deletar uma chave de API").action(async (keyId) => {
3821
+ try {
3822
+ const s = spinner("Deletando chave...");
3823
+ await authFetch("/api-key/delete", {
3824
+ method: "POST",
3825
+ body: JSON.stringify({ keyId })
3826
+ });
3827
+ s.stop();
3828
+ success("Chave deletada.");
3829
+ } catch (error) {
3830
+ handleError(error);
3831
+ }
3832
+ });
3833
+
3834
+ //#endregion
3835
+ //#region src/commands/whoami.ts
3836
+ const whoamiCommand = new Command("whoami").description("Mostrar usuário autenticado").action(async () => {
3837
+ try {
3838
+ const user = await (await getClient()).me();
3839
+ output({
3840
+ type: "user",
3841
+ name: user.name,
3842
+ email: user.email
3843
+ }, () => {
3844
+ console.log();
3845
+ console.log(` ${chalk.bold("Nome:")} ${user.name}`);
3846
+ console.log(` ${chalk.bold("Email:")} ${user.email}`);
3847
+ console.log();
3848
+ });
2927
3849
  } catch (error) {
2928
3850
  handleError(error);
2929
3851
  }
@@ -2931,8 +3853,23 @@ const useCommand = new Command("use").description("Selecionar qual serviço usar
2931
3853
 
2932
3854
  //#endregion
2933
3855
  //#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");
3856
+ const program = new Command().name("veloz").description("CLI da plataforma Veloz — deploy rápido para o Brasil").version("0.0.0-beta.10").option("--output <format>", "Formato de saída: fancy, json, github-actions, plain").option("--env <environment>", "Ambiente alvo (ex: preview, staging)").hook("preAction", (thisCommand) => {
3857
+ const opts = thisCommand.opts();
3858
+ if (opts.output) {
3859
+ const valid = [
3860
+ "fancy",
3861
+ "json",
3862
+ "github-actions",
3863
+ "plain"
3864
+ ];
3865
+ if (!valid.includes(opts.output)) {
3866
+ console.error(`Formato de saída inválido: "${opts.output}". Use: ${valid.join(", ")}`);
3867
+ process.exit(1);
3868
+ }
3869
+ setOutputMode(opts.output);
3870
+ }
3871
+ if (opts.env) setActiveEnv(opts.env);
3872
+ });
2936
3873
  program.addCommand(loginCommand);
2937
3874
  program.addCommand(logoutCommand);
2938
3875
  program.addCommand(projectsCommand);
@@ -2943,6 +3880,8 @@ program.addCommand(envCommand);
2943
3880
  program.addCommand(domainsCommand);
2944
3881
  program.addCommand(configCommand);
2945
3882
  program.addCommand(useCommand);
3883
+ program.addCommand(apikeyCommand);
3884
+ program.addCommand(whoamiCommand);
2946
3885
  program.parse();
2947
3886
 
2948
3887
  //#endregion