onveloz 0.0.0-beta.26 → 0.0.0-beta.28

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.
@@ -27,6 +27,25 @@ function buildStageOrder(steps) {
27
27
  }
28
28
  return order;
29
29
  }
30
+ /** Matches the pre-start log section header written by the reconciler. */
31
+ const PRE_START_HEADER_RE = /^──\s*Logs do comando pre-start/;
32
+ /** Matches the pre-start unavailable message. */
33
+ const PRE_START_UNAVAILABLE_RE = /^──\s*Logs do comando pre-start indisponíveis/;
34
+ /** Matches the crash log section header written by the reconciler. */
35
+ const CRASH_LOG_HEADER_RE = /^──\s*Logs do container \(crash\)/;
36
+ /** Matches the crash logs unavailable message. */
37
+ const CRASH_LOG_UNAVAILABLE_RE = /^──\s*Logs do container indisponíveis/;
38
+ /** Lines that mark the beginning of the deploy/rollout phase. */
39
+ const DEPLOY_PHASE_MARKERS = [
40
+ /^Realizando deploy/,
41
+ /^Atualizando serviço/,
42
+ /^Criando serviço/,
43
+ /^Waiting for rollout/,
44
+ /^Updating deployment/,
45
+ /^Creating K8s deployment/
46
+ ];
47
+ /** Lines that indicate the pre-start command is starting. */
48
+ const PRE_START_MARKER_RE = /^Executando comando pre-start:/;
30
49
  const RUNTIME_LINE_PATTERNS = [
31
50
  /^[\u26A0\u{1F680}]/u,
32
51
  /^\{.*"(?:level|msg|pid)"/,
@@ -40,6 +59,14 @@ const RUNTIME_LINE_PATTERNS = [
40
59
  function isRuntimeLine(content) {
41
60
  return RUNTIME_LINE_PATTERNS.some((p) => p.test(content.trim()));
42
61
  }
62
+ function isPreStartLine(content) {
63
+ const trimmed = content.trim();
64
+ return PRE_START_MARKER_RE.test(trimmed) || PRE_START_HEADER_RE.test(trimmed) || PRE_START_UNAVAILABLE_RE.test(trimmed);
65
+ }
66
+ function isDeployPhaseLine(content) {
67
+ const trimmed = content.trim();
68
+ return DEPLOY_PHASE_MARKERS.some((p) => p.test(trimmed)) || CRASH_LOG_HEADER_RE.test(trimmed) || CRASH_LOG_UNAVAILABLE_RE.test(trimmed);
69
+ }
43
70
  function parseBuildSteps(rawText) {
44
71
  const rawLines = rawText.split("\n");
45
72
  let currentPhase = 0;
@@ -172,22 +199,61 @@ function parseBuildSteps(rawText) {
172
199
  generalCount++;
173
200
  const generalStep = allSteps[entry.origIdx];
174
201
  if (generalCount === totalGenerals && totalGenerals > 1) {
202
+ const finalizationLines = [];
203
+ const preStartLines = [];
175
204
  const deployLines = [];
176
205
  const healthLines = [];
177
- let hitRuntime = false;
206
+ let phase = "finalization";
207
+ let inCrashLogBlock = false;
178
208
  for (const line of generalStep.lines) {
179
- if (!hitRuntime && isRuntimeLine(line.content)) hitRuntime = true;
180
- if (hitRuntime) healthLines.push(line);
181
- else deployLines.push(line);
209
+ const content = line.content.trim();
210
+ if (CRASH_LOG_HEADER_RE.test(content) || PRE_START_HEADER_RE.test(content)) inCrashLogBlock = true;
211
+ if (phase === "finalization" && isPreStartLine(content)) phase = "prestart";
212
+ else if ((phase === "finalization" || phase === "prestart") && isDeployPhaseLine(content)) phase = "deploy";
213
+ else if ((phase === "finalization" || phase === "deploy") && !inCrashLogBlock && isRuntimeLine(content)) phase = "health";
214
+ switch (phase) {
215
+ case "finalization":
216
+ finalizationLines.push(line);
217
+ break;
218
+ case "prestart":
219
+ preStartLines.push(line);
220
+ break;
221
+ case "deploy":
222
+ deployLines.push(line);
223
+ break;
224
+ case "health":
225
+ healthLines.push(line);
226
+ break;
227
+ }
182
228
  }
183
- if (deployLines.length > 0) result.push({
229
+ const hasPreStartError = preStartLines.some((l) => /pre-start falhou|FAILED/i.test(l.content));
230
+ const hasDeployError = deployLines.some((l) => /DEPLOY FAILED/i.test(l.content));
231
+ if (finalizationLines.length > 0) result.push({
184
232
  stepNumber: null,
185
233
  title: "Finalização",
186
234
  duration: null,
187
235
  status: "done",
236
+ lines: finalizationLines,
237
+ phase: generalStep.phase,
238
+ startedAt: finalizationLines[0]?.timestamp ?? generalStep.startedAt
239
+ });
240
+ if (preStartLines.length > 0) result.push({
241
+ stepNumber: null,
242
+ title: "Pre-start",
243
+ duration: null,
244
+ status: hasPreStartError ? "error" : "done",
245
+ lines: preStartLines,
246
+ phase: generalStep.phase,
247
+ startedAt: preStartLines[0]?.timestamp ?? null
248
+ });
249
+ if (deployLines.length > 0) result.push({
250
+ stepNumber: null,
251
+ title: "Deploy",
252
+ duration: null,
253
+ status: hasDeployError ? "error" : "done",
188
254
  lines: deployLines,
189
255
  phase: generalStep.phase,
190
- startedAt: deployLines[0]?.timestamp ?? generalStep.startedAt
256
+ startedAt: deployLines[0]?.timestamp ?? null
191
257
  });
192
258
  if (healthLines.length > 0) result.push({
193
259
  stepNumber: null,
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { Cli, middleware, z } from "incur";
3
- import { exec, execFile, execSync } from "node:child_process";
3
+ import { exec, execSync } from "node:child_process";
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
5
5
  import { basename, dirname, join, relative, resolve } from "node:path";
6
6
  import { z as z$1 } from "zod";
@@ -9,11 +9,11 @@ import { RPCLink } from "@orpc/client/fetch";
9
9
  import { createHash } from "node:crypto";
10
10
  import { homedir, platform, tmpdir } from "node:os";
11
11
  import chalk from "chalk";
12
+ import { createAuthClient } from "better-auth/client";
13
+ import { deviceAuthorizationClient } from "better-auth/client/plugins";
12
14
  import * as readline from "node:readline";
13
15
  import { createInterface } from "node:readline";
14
16
  import ora from "ora";
15
- import { createAuthClient } from "better-auth/client";
16
- import { deviceAuthorizationClient } from "better-auth/client/plugins";
17
17
  import net from "node:net";
18
18
  import WebSocket from "ws";
19
19
  import { link, mkdir, mkdtemp, readdir, rm, stat } from "node:fs/promises";
@@ -237,6 +237,192 @@ function resolveServiceSizeFromResources(cpu, memory) {
237
237
  for (const [key, tier] of Object.entries(SERVICE_SIZES)) if (tier.cpu === cpu && tier.memory === memory) return key;
238
238
  return null;
239
239
  }
240
+ /** All known flag keys. Used for validation and UI rendering. */
241
+ const PROJECT_FLAG_DEFINITIONS = {
242
+ cloudflareSaas: {
243
+ label: "Cloudflare for SaaS",
244
+ description: "SSL de domínios customizados via Cloudflare (em vez de Let's Encrypt)"
245
+ },
246
+ cloudflareTunnel: {
247
+ label: "Cloudflare Tunnel",
248
+ description: "Roteamento de tráfego via túnel Cloudflare (em vez de IP direto). Domínios customizados apontam para proxy.onveloz.com"
249
+ },
250
+ buildkit: {
251
+ label: "BuildKit",
252
+ description: "Build de imagens via BuildKit no cluster (em vez de Depot externo). Mais controle e sem dependência de serviço externo."
253
+ }
254
+ };
255
+ const PROJECT_FLAG_KEYS = Object.keys(PROJECT_FLAG_DEFINITIONS);
256
+ /**
257
+ * Allowlist of user-configurable PostgreSQL parameters with validation bounds.
258
+ * PGTune-relevant params + commonly tuned settings.
259
+ */
260
+ const PG_CONFIGURABLE_PARAMS = {
261
+ shared_buffers: {
262
+ type: "integer",
263
+ unit: "MB",
264
+ min: 16,
265
+ max: 16384,
266
+ description: "Memória dedicada para cache de dados do PostgreSQL",
267
+ requiresRestart: true
268
+ },
269
+ effective_cache_size: {
270
+ type: "integer",
271
+ unit: "MB",
272
+ min: 64,
273
+ max: 65536,
274
+ description: "Estimativa de memória disponível para cache do sistema operacional",
275
+ requiresRestart: false
276
+ },
277
+ work_mem: {
278
+ type: "integer",
279
+ unit: "MB",
280
+ min: 1,
281
+ max: 2048,
282
+ description: "Memória por operação de ordenação/hash",
283
+ requiresRestart: false
284
+ },
285
+ maintenance_work_mem: {
286
+ type: "integer",
287
+ unit: "MB",
288
+ min: 16,
289
+ max: 2048,
290
+ description: "Memória para operações de manutenção (VACUUM, CREATE INDEX)",
291
+ requiresRestart: false
292
+ },
293
+ wal_buffers: {
294
+ type: "integer",
295
+ unit: "MB",
296
+ min: 1,
297
+ max: 64,
298
+ description: "Memória para buffers de WAL",
299
+ requiresRestart: true
300
+ },
301
+ min_wal_size: {
302
+ type: "integer",
303
+ unit: "MB",
304
+ min: 32,
305
+ max: 4096,
306
+ description: "Tamanho mínimo de WAL antes de reciclar",
307
+ requiresRestart: false
308
+ },
309
+ max_wal_size: {
310
+ type: "integer",
311
+ unit: "MB",
312
+ min: 64,
313
+ max: 16384,
314
+ description: "Tamanho máximo de WAL antes de forçar checkpoint",
315
+ requiresRestart: false
316
+ },
317
+ max_connections: {
318
+ type: "integer",
319
+ min: 20,
320
+ max: 1e3,
321
+ description: "Número máximo de conexões simultâneas",
322
+ requiresRestart: true
323
+ },
324
+ random_page_cost: {
325
+ type: "real",
326
+ min: .1,
327
+ max: 10,
328
+ description: "Custo estimado de leitura aleatória de página (SSD ≈ 1.1, HDD ≈ 4.0)",
329
+ requiresRestart: false
330
+ },
331
+ effective_io_concurrency: {
332
+ type: "integer",
333
+ min: 0,
334
+ max: 1e3,
335
+ description: "Número de operações de I/O simultâneas (SSD ≈ 200)",
336
+ requiresRestart: false
337
+ },
338
+ max_worker_processes: {
339
+ type: "integer",
340
+ min: 1,
341
+ max: 128,
342
+ description: "Número máximo de processos worker",
343
+ requiresRestart: true
344
+ },
345
+ max_parallel_workers: {
346
+ type: "integer",
347
+ min: 0,
348
+ max: 128,
349
+ description: "Número máximo de workers paralelos",
350
+ requiresRestart: false
351
+ },
352
+ max_parallel_workers_per_gather: {
353
+ type: "integer",
354
+ min: 0,
355
+ max: 64,
356
+ description: "Número máximo de workers por operação Gather",
357
+ requiresRestart: false
358
+ },
359
+ max_parallel_maintenance_workers: {
360
+ type: "integer",
361
+ min: 0,
362
+ max: 64,
363
+ description: "Número máximo de workers para manutenção paralela",
364
+ requiresRestart: false
365
+ },
366
+ checkpoint_completion_target: {
367
+ type: "real",
368
+ min: .1,
369
+ max: 1,
370
+ description: "Fração do intervalo de checkpoint para completar a escrita",
371
+ requiresRestart: false
372
+ },
373
+ autovacuum_max_workers: {
374
+ type: "integer",
375
+ min: 1,
376
+ max: 16,
377
+ description: "Número máximo de processos autovacuum",
378
+ requiresRestart: true
379
+ },
380
+ autovacuum_naptime: {
381
+ type: "integer",
382
+ unit: "s",
383
+ min: 1,
384
+ max: 86400,
385
+ description: "Intervalo entre execuções do autovacuum",
386
+ requiresRestart: false
387
+ },
388
+ log_min_duration_statement: {
389
+ type: "integer",
390
+ unit: "ms",
391
+ min: -1,
392
+ max: 6e5,
393
+ description: "Log de queries mais lentas que N ms (-1 desabilita)",
394
+ requiresRestart: false
395
+ },
396
+ log_statement: {
397
+ type: "enum",
398
+ enumValues: [
399
+ "none",
400
+ "ddl",
401
+ "mod",
402
+ "all"
403
+ ],
404
+ description: "Tipos de statements que são logados",
405
+ requiresRestart: false
406
+ },
407
+ statement_timeout: {
408
+ type: "integer",
409
+ unit: "ms",
410
+ min: 0,
411
+ max: 864e5,
412
+ description: "Timeout para execução de statements (0 desabilita)",
413
+ requiresRestart: false
414
+ },
415
+ idle_in_transaction_session_timeout: {
416
+ type: "integer",
417
+ unit: "ms",
418
+ min: 0,
419
+ max: 864e5,
420
+ description: "Timeout para sessões idle em transação (0 desabilita)",
421
+ requiresRestart: false
422
+ }
423
+ };
424
+ /** All user-configurable param keys. */
425
+ const PG_CONFIGURABLE_PARAM_KEYS = Object.keys(PG_CONFIGURABLE_PARAMS);
240
426
 
241
427
  //#endregion
242
428
  //#region ../../packages/config/veloz-config.ts
@@ -761,13 +947,17 @@ async function promptMultiSelect(question, options) {
761
947
  }
762
948
  return indices.map((i) => options[i].value);
763
949
  }
950
+ function openBrowser(url) {
951
+ return new Promise((resolve$1) => {
952
+ const os = platform();
953
+ exec(os === "darwin" ? `open "${url}"` : os === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`, (error) => {
954
+ resolve$1(!error);
955
+ });
956
+ });
957
+ }
764
958
 
765
959
  //#endregion
766
960
  //#region src/commands/login.ts
767
- function openBrowser$1(url) {
768
- const os = platform();
769
- exec(os === "darwin" ? `open "${url}"` : os === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`, () => {});
770
- }
771
961
  const CLIENT_ID = "veloz-cli";
772
962
  async function performLogin(apiUrl) {
773
963
  const config = loadConfig$1();
@@ -789,10 +979,15 @@ async function performLogin(apiUrl) {
789
979
  console.log();
790
980
  console.log(chalk.white(` Código de verificação: ${chalk.bold.cyan(data.user_code)}`));
791
981
  console.log();
792
- console.log(chalk.dim(" Se o navegador não abrir, acesse manualmente:"));
793
- console.log(chalk.dim(` ${verificationUrl}`));
982
+ if (!await openBrowser(verificationUrl)) {
983
+ info("Não foi possível abrir o navegador. Acesse manualmente:");
984
+ console.log();
985
+ console.log(` ${chalk.bold(verificationUrl)}`);
986
+ } else {
987
+ console.log(chalk.dim(" Se o navegador não abrir, acesse manualmente:"));
988
+ console.log(chalk.dim(` ${verificationUrl}`));
989
+ }
794
990
  console.log();
795
- openBrowser$1(verificationUrl);
796
991
  const pollSpinner = spinner("Aguardando autorização no navegador...");
797
992
  const token = await pollForToken(authClient, data.device_code, data.interval || 5);
798
993
  if (!token) {
@@ -1179,7 +1374,7 @@ envGroup.command("list", {
1179
1374
  serviceName: z.string(),
1180
1375
  vars: z.array(z.object({
1181
1376
  key: z.string(),
1182
- maskedValue: z.string(),
1377
+ value: z.string(),
1183
1378
  updatedAt: z.string()
1184
1379
  }))
1185
1380
  })),
@@ -1202,12 +1397,15 @@ envGroup.command("list", {
1202
1397
  });
1203
1398
  continue;
1204
1399
  }
1205
- for (const v of envVars) console.log(` ${chalk.bold(v.key)} ${chalk.dim(v.maskedValue)} ${new Date(v.updatedAt).toLocaleDateString("pt-BR")}`);
1400
+ for (const v of envVars) {
1401
+ const displayValue = v.value ?? v.maskedValue;
1402
+ console.log(` ${chalk.bold(v.key)} ${chalk.dim(displayValue)} ${new Date(v.updatedAt).toLocaleDateString("pt-BR")}`);
1403
+ }
1206
1404
  result.push({
1207
1405
  serviceName: service.name,
1208
1406
  vars: envVars.map((v) => ({
1209
1407
  key: v.key,
1210
- maskedValue: v.maskedValue,
1408
+ value: v.value ?? v.maskedValue,
1211
1409
  updatedAt: v.updatedAt instanceof Date ? v.updatedAt.toISOString() : String(v.updatedAt)
1212
1410
  }))
1213
1411
  });
@@ -1379,7 +1577,7 @@ envGroup.command("export", {
1379
1577
  options: z.object({ service: z.string().optional().describe("Serviço alvo (chave ou nome)") }),
1380
1578
  output: z.object({ vars: z.array(z.object({
1381
1579
  key: z.string(),
1382
- maskedValue: z.string()
1580
+ value: z.string()
1383
1581
  })) }),
1384
1582
  async run(c) {
1385
1583
  const serviceId = await resolveServiceId(c.options.service);
@@ -1392,19 +1590,21 @@ envGroup.command("export", {
1392
1590
  info("Nenhuma variável de ambiente para exportar.");
1393
1591
  return { vars: [] };
1394
1592
  }
1395
- const envContent = envVars.map((v) => `${v.key}=${v.maskedValue}`).join("\n");
1593
+ const isDecrypted = envVars.some((v) => v.value !== null && v.value !== void 0);
1594
+ const envContent = envVars.map((v) => `${v.key}=${v.value ?? v.maskedValue}`).join("\n");
1396
1595
  if (c.args.arquivo) {
1397
1596
  writeFileSync(resolve(process.cwd(), c.args.arquivo), envContent + "\n", "utf-8");
1398
1597
  success(`Variáveis exportadas para ${chalk.bold(c.args.arquivo)}`);
1399
- console.log(chalk.dim("Nota: Valores estão mascarados por segurança."));
1598
+ if (!isDecrypted) console.log(chalk.dim("Nota: Valores estão mascarados por segurança."));
1400
1599
  } else {
1401
- console.log(chalk.bold("\n# Variáveis de Ambiente (valores mascarados)\n"));
1600
+ if (isDecrypted) console.log(chalk.bold("\n# Variáveis de Ambiente\n"));
1601
+ else console.log(chalk.bold("\n# Variáveis de Ambiente (valores mascarados)\n"));
1402
1602
  console.log(envContent);
1403
1603
  console.log();
1404
1604
  }
1405
1605
  return { vars: envVars.map((v) => ({
1406
1606
  key: v.key,
1407
- maskedValue: v.maskedValue
1607
+ value: v.value ?? v.maskedValue
1408
1608
  })) };
1409
1609
  }
1410
1610
  });
@@ -1784,6 +1984,54 @@ volumesGroup.command("sync", {
1784
1984
 
1785
1985
  //#endregion
1786
1986
  //#region src/commands/config.ts
1987
+ /**
1988
+ * Sync server-side config updates back to veloz.json.
1989
+ * Maps server field names to veloz.json service config paths.
1990
+ */
1991
+ function syncConfigToVelozJson(serviceKey, updates) {
1992
+ try {
1993
+ if (!loadConfig()) return;
1994
+ patchConfig((raw) => {
1995
+ const svc = raw.services[serviceKey];
1996
+ if (!svc) return;
1997
+ for (const [field, value] of Object.entries(updates)) switch (field) {
1998
+ case "name":
1999
+ svc.name = value;
2000
+ break;
2001
+ case "buildCommand":
2002
+ if (!svc.build) svc.build = {};
2003
+ svc.build.command = value;
2004
+ break;
2005
+ case "startCommand":
2006
+ if (!svc.runtime) svc.runtime = {};
2007
+ svc.runtime.command = value;
2008
+ break;
2009
+ case "preStartCommand":
2010
+ if (!svc.runtime) svc.runtime = {};
2011
+ svc.runtime.preStartCommand = value;
2012
+ break;
2013
+ case "port":
2014
+ if (!svc.runtime) svc.runtime = {};
2015
+ svc.runtime.port = value;
2016
+ break;
2017
+ case "rootDirectory":
2018
+ svc.root = value ?? ".";
2019
+ break;
2020
+ case "instanceCount":
2021
+ if (!svc.resources) svc.resources = {};
2022
+ svc.resources.instances = value;
2023
+ break;
2024
+ case "size":
2025
+ if (!svc.resources) svc.resources = {};
2026
+ svc.resources.size = value;
2027
+ break;
2028
+ case "branch":
2029
+ svc.branch = value;
2030
+ break;
2031
+ }
2032
+ });
2033
+ } catch {}
2034
+ }
1787
2035
  function formatValue(value) {
1788
2036
  if (value === null || value === void 0 || value === "") return chalk.dim("—");
1789
2037
  return chalk.cyan(String(value));
@@ -1874,7 +2122,8 @@ configGroup.command("set", {
1874
2122
  instances: "i"
1875
2123
  },
1876
2124
  async run(c) {
1877
- const serviceId = await resolveServiceId(c.options.service);
2125
+ const { key: serviceKey, service } = await resolveService(c.options.service);
2126
+ const serviceId = service.id;
1878
2127
  const client = await getClient();
1879
2128
  const updates = {};
1880
2129
  if (c.options.name) updates.name = c.options.name;
@@ -1898,6 +2147,7 @@ configGroup.command("set", {
1898
2147
  ...updates
1899
2148
  })
1900
2149
  });
2150
+ syncConfigToVelozJson(serviceKey, updates);
1901
2151
  success("Configurações atualizadas com sucesso!");
1902
2152
  console.log(chalk.dim("\nValores atualizados:"));
1903
2153
  for (const [key, value] of Object.entries(updates)) {
@@ -1914,7 +2164,8 @@ configGroup.command("edit", {
1914
2164
  middleware: [requireAuth],
1915
2165
  options: z.object({ service: z.string().optional().describe("Serviço alvo (chave ou nome)") }),
1916
2166
  async run(c) {
1917
- const serviceId = await resolveServiceId(c.options.service);
2167
+ const { key: serviceKey, service: resolved } = await resolveService(c.options.service);
2168
+ const serviceId = resolved.id;
1918
2169
  const client = await getClient();
1919
2170
  const svc = await withSpinner({
1920
2171
  text: "Buscando configurações atuais...",
@@ -1955,6 +2206,7 @@ configGroup.command("edit", {
1955
2206
  ...updates
1956
2207
  })
1957
2208
  });
2209
+ syncConfigToVelozJson(serviceKey, updates);
1958
2210
  success("Configurações atualizadas com sucesso!");
1959
2211
  info("Execute 'veloz deploy' para aplicar as mudanças.");
1960
2212
  return { updated: updates };
@@ -1973,7 +2225,8 @@ configGroup.command("reset", {
1973
2225
  }),
1974
2226
  async run(c) {
1975
2227
  requireMcpConfirmation(c.options.userConfirmation, "resetar configurações para os padrões");
1976
- const serviceId = await resolveServiceId(c.options.service);
2228
+ const { key: serviceKey, service } = await resolveService(c.options.service);
2229
+ const serviceId = service.id;
1977
2230
  const client = await getClient();
1978
2231
  const updates = {};
1979
2232
  if (c.options.all) {
@@ -1997,6 +2250,7 @@ configGroup.command("reset", {
1997
2250
  ...updates
1998
2251
  })
1999
2252
  });
2253
+ syncConfigToVelozJson(serviceKey, updates);
2000
2254
  success("Configurações resetadas para os padrões!");
2001
2255
  info("Execute 'veloz deploy' para aplicar as mudanças.");
2002
2256
  return { reset: Object.keys(updates) };
@@ -2707,6 +2961,116 @@ dbGroup.command("tunnel", {
2707
2961
  });
2708
2962
  }
2709
2963
  });
2964
+ dbGroup.command("config", {
2965
+ description: "Ver ou alterar configuração do PostgreSQL",
2966
+ middleware: [requireAuth],
2967
+ args: z.object({ name: z.string().describe("Nome ou ID do banco de dados") }),
2968
+ options: z.object({
2969
+ set: z.string().optional().describe("Definir parâmetro (formato: chave=valor)"),
2970
+ reset: z.boolean().optional().describe("Resetar para configuração auto-tuned"),
2971
+ params: z.boolean().optional().describe("Listar parâmetros configuráveis"),
2972
+ userConfirmation: z.string().optional()
2973
+ }),
2974
+ output: z.object({
2975
+ params: z.array(z.object({
2976
+ name: z.string(),
2977
+ type: z.string(),
2978
+ unit: z.string().optional(),
2979
+ requiresRestart: z.boolean(),
2980
+ description: z.string()
2981
+ })).optional(),
2982
+ effective: z.record(z.string(), z.string()).optional(),
2983
+ userOverrides: z.record(z.string(), z.string()).optional(),
2984
+ updated: z.boolean().optional(),
2985
+ key: z.string().optional(),
2986
+ value: z.string().optional(),
2987
+ requiresRestart: z.boolean().optional(),
2988
+ reset: z.boolean().optional()
2989
+ }),
2990
+ async run(c) {
2991
+ const db = await resolveDatabaseByName(getProjectId(), c.args.name);
2992
+ const client = await getClient();
2993
+ if (c.options.params) {
2994
+ log(chalk.bold("\n Parâmetros configuráveis:\n"));
2995
+ log(` ${chalk.bold("Parâmetro".padEnd(40))}${chalk.bold("Tipo".padEnd(12))}${chalk.bold("Reinício?")}`);
2996
+ log(chalk.dim(" " + "─".repeat(65)));
2997
+ const params = Object.entries(PG_CONFIGURABLE_PARAMS).map(([key, def]) => {
2998
+ const typeLabel = def.unit ? `${def.type} (${def.unit})` : def.type;
2999
+ const restartLabel = def.requiresRestart ? chalk.yellow("Sim") : chalk.green("Não");
3000
+ log(` ${key.padEnd(40)}${typeLabel.padEnd(12)}${restartLabel}`);
3001
+ return {
3002
+ name: key,
3003
+ type: def.type,
3004
+ unit: def.unit,
3005
+ requiresRestart: def.requiresRestart,
3006
+ description: def.description
3007
+ };
3008
+ });
3009
+ log("");
3010
+ return { params };
3011
+ }
3012
+ if (db.engine !== "postgresql") throw new Error("Configuração personalizada só é suportada para PostgreSQL.");
3013
+ if (c.options.reset) {
3014
+ requireMcpConfirmation(c.options.userConfirmation, `resetar configuração do banco de dados "${db.name}" para auto-tuned`);
3015
+ if (!c.options.userConfirmation && isInteractive()) {
3016
+ if (!await promptConfirm(`Resetar toda a configuração personalizada de "${db.name}" para auto-tuned?`, false)) {
3017
+ info("Operação cancelada.");
3018
+ return {};
3019
+ }
3020
+ }
3021
+ const result = await withSpinner({
3022
+ text: "Resetando configuração...",
3023
+ fn: () => client.databases.resetConfig({ serviceId: db.id })
3024
+ });
3025
+ success("Configuração resetada para auto-tuned.");
3026
+ info("O banco de dados será reiniciado para aplicar as alterações.");
3027
+ return {
3028
+ reset: true,
3029
+ effective: result.effective
3030
+ };
3031
+ }
3032
+ if (c.options.set) {
3033
+ const eqIndex = c.options.set.indexOf("=");
3034
+ if (eqIndex === -1) throw new Error("Formato inválido. Use: --set chave=valor (ex: --set shared_buffers=512MB)");
3035
+ const key = c.options.set.slice(0, eqIndex).trim();
3036
+ const value = c.options.set.slice(eqIndex + 1).trim();
3037
+ const result = await withSpinner({
3038
+ text: `Atualizando ${key}...`,
3039
+ fn: () => client.databases.updateConfig({
3040
+ serviceId: db.id,
3041
+ config: { [key]: value }
3042
+ })
3043
+ });
3044
+ success(`${key} = ${value}`);
3045
+ if (result.requiresRestart) warn("Esta alteração requer reinício do banco de dados para ter efeito.");
3046
+ return {
3047
+ updated: true,
3048
+ key,
3049
+ value,
3050
+ requiresRestart: result.requiresRestart
3051
+ };
3052
+ }
3053
+ const config = await withSpinner({
3054
+ text: "Carregando configuração...",
3055
+ fn: () => client.databases.getConfig({ serviceId: db.id })
3056
+ });
3057
+ log(chalk.bold(`\n Configuração PostgreSQL — ${db.name}\n`));
3058
+ log(` ${chalk.bold("Parâmetro".padEnd(40))}${chalk.bold("Valor".padEnd(20))}${chalk.bold("Origem")}`);
3059
+ log(chalk.dim(" " + "─".repeat(70)));
3060
+ for (const [key, value] of Object.entries(config.effective)) {
3061
+ const isOverride = key in config.userOverrides;
3062
+ const originLabel = isOverride ? chalk.cyan("personalizado") : chalk.dim("auto-tuned");
3063
+ const valueLabel = isOverride ? chalk.cyan(value) : value;
3064
+ log(` ${key.padEnd(40)}${String(valueLabel).padEnd(20)}${originLabel}`);
3065
+ }
3066
+ if (Object.keys(config.userOverrides).length > 0) log(chalk.dim(`\n ${Object.keys(config.userOverrides).length} parâmetro(s) personalizado(s)`));
3067
+ log("");
3068
+ return {
3069
+ effective: config.effective,
3070
+ userOverrides: config.userOverrides
3071
+ };
3072
+ }
3073
+ });
2710
3074
 
2711
3075
  //#endregion
2712
3076
  //#region src/commands/template.ts
@@ -3985,12 +4349,6 @@ logsGroup.command("query-help", {
3985
4349
 
3986
4350
  //#endregion
3987
4351
  //#region src/commands/github.ts
3988
- function openBrowser(url) {
3989
- const os = platform();
3990
- execFile(os === "darwin" ? "open" : os === "win32" ? "start" : "xdg-open", os === "win32" ? ["", url] : [url], (error) => {
3991
- if (error) warn(`Não foi possível abrir o navegador: ${error.message}`);
3992
- });
3993
- }
3994
4352
  const githubGroup = Cli.create("github", { description: "Gerenciar integração com GitHub" });
3995
4353
  githubGroup.command("setup", {
3996
4354
  description: "Configurar GitHub App para deploy automático via webhook",
@@ -4102,10 +4460,18 @@ githubGroup.command("setup", {
4102
4460
  projectId
4103
4461
  };
4104
4462
  }
4105
- openBrowser(installation.installUrl);
4463
+ const opened = await openBrowser(installation.installUrl);
4106
4464
  console.log();
4107
- info("Aguardando instalação do GitHub App...");
4108
- console.log(chalk.dim(" Instale o app no navegador e volte aqui."));
4465
+ if (opened) {
4466
+ info("Aguardando instalação do GitHub App...");
4467
+ console.log(chalk.dim(" Instale o app no navegador e volte aqui."));
4468
+ } else {
4469
+ info("Não foi possível abrir o navegador. Abra o link abaixo manualmente:");
4470
+ console.log();
4471
+ console.log(` ${chalk.bold(installation.installUrl)}`);
4472
+ console.log();
4473
+ console.log(chalk.dim(" Instale o app e volte aqui."));
4474
+ }
4109
4475
  console.log();
4110
4476
  const pollSpinner = spinner("Aguardando instalação...");
4111
4477
  const maxAttempts = 60;
@@ -4258,6 +4624,25 @@ const TERMINAL_STATUSES = new Set([
4258
4624
 
4259
4625
  //#endregion
4260
4626
  //#region src/lib/deploy-stream.ts
4627
+ const DASHBOARD_BASE = process.env.VELOZ_WEB_URL || "https://app.onveloz.com";
4628
+ /**
4629
+ * Fetch deployment details from the API and enrich a DeployStreamResult
4630
+ * with metadata useful for agents (failReason, commit info, duration, dashboard link).
4631
+ */
4632
+ async function enrichResult(client, result) {
4633
+ try {
4634
+ const d = await client.deployments.get({ deploymentId: result.deploymentId });
4635
+ const service = await client.services.get({ serviceId: d.serviceId }).catch(() => null);
4636
+ const projectId = service ? service.projectId : null;
4637
+ result.failReason = d.failReason ?? null;
4638
+ result.errorSummary = d.errorSummary ?? null;
4639
+ result.commitSha = d.commitSha ?? null;
4640
+ result.branch = d.branch ?? null;
4641
+ if (d.startedAt && d.finishedAt) result.durationSeconds = Math.round((new Date(d.finishedAt).getTime() - new Date(d.startedAt).getTime()) / 1e3);
4642
+ if (projectId) result.dashboardUrl = `${DASHBOARD_BASE}/projetos/${projectId}/servicos/${d.serviceId}/deploys/${d.id}`;
4643
+ } catch {}
4644
+ return result;
4645
+ }
4261
4646
  async function fetchDeployUrls$1(client, serviceId) {
4262
4647
  try {
4263
4648
  return (await client.domains.list({ serviceId })).map((d) => `https://${d.domain}`);
@@ -4397,7 +4782,7 @@ async function streamDeploymentLogs(deploymentId, serviceId, serviceName) {
4397
4782
  const isTTY = !mcp && process.stdout.isTTY;
4398
4783
  const isGHA = !mcp && process.env.GITHUB_ACTIONS === "true";
4399
4784
  if (isTTY && !isVerbose) {
4400
- const { renderDeployTUI } = await import("./deploy-tui-DGRYRsf_.mjs");
4785
+ const { renderDeployTUI } = await import("./deploy-tui-D_pzq4Tu.mjs");
4401
4786
  return renderDeployTUI(deploymentId, serviceId, serviceName);
4402
4787
  }
4403
4788
  const allLogLines = [];
@@ -4470,13 +4855,13 @@ async function streamDeploymentLogs(deploymentId, serviceId, serviceName) {
4470
4855
  }
4471
4856
  if (!mcp) process.exit(1);
4472
4857
  }
4473
- return {
4858
+ return enrichResult(client, {
4474
4859
  deploymentId,
4475
4860
  status: finalStatus,
4476
4861
  logs: allLogLines.filter((l) => l.trim()),
4477
4862
  urls,
4478
4863
  serviceName
4479
- };
4864
+ });
4480
4865
  }
4481
4866
 
4482
4867
  //#endregion
@@ -5017,6 +5402,18 @@ function extractEnvVars(files) {
5017
5402
  }
5018
5403
  return vars;
5019
5404
  }
5405
+ /**
5406
+ * Extract the major Node.js version from a package.json string's `engines.node` field.
5407
+ * Handles ranges like `>=22`, `^20`, `~18.0`, `22.x`, `20`, `>=22.0.0`.
5408
+ * Returns the major version string (e.g. "22") or undefined if not parseable.
5409
+ */
5410
+ function detectNodeVersion(pkgJsonStr) {
5411
+ const pkg = safeParsePkg(pkgJsonStr);
5412
+ if (!pkg?.engines?.node) return void 0;
5413
+ const match = pkg.engines.node.trim().match(/(\d+)/);
5414
+ if (!match) return void 0;
5415
+ return match[1];
5416
+ }
5020
5417
  function analyzeRepo(files) {
5021
5418
  const packageManager = detectPackageManager$1(files);
5022
5419
  const envVars = extractEnvVars(files);
@@ -5034,6 +5431,7 @@ function analyzeRepo(files) {
5034
5431
  const appPath = filePath.replace("/package.json", "");
5035
5432
  const appName = nested.name || appPath.split("/").pop() || appPath;
5036
5433
  let appFramework = detectFramework(content, packageManager);
5434
+ if (!appFramework && !nested.scripts?.start) continue;
5037
5435
  if (appFramework && rootPkg) {
5038
5436
  if (rootPkg.scripts?.[`build:${appName}`]) appFramework = {
5039
5437
  ...appFramework,
@@ -5051,7 +5449,8 @@ function analyzeRepo(files) {
5051
5449
  name: appName,
5052
5450
  path: appPath,
5053
5451
  framework: appFramework,
5054
- usesNodeFs: sourceEntries.some(([srcPath, fileContent]) => srcPath.startsWith(`${appPath}/`) && usesNodeFs(fileContent))
5452
+ usesNodeFs: sourceEntries.some(([srcPath, fileContent]) => srcPath.startsWith(`${appPath}/`) && usesNodeFs(fileContent)),
5453
+ nodeVersion: detectNodeVersion(content)
5055
5454
  });
5056
5455
  }
5057
5456
  return {
@@ -5060,7 +5459,8 @@ function analyzeRepo(files) {
5060
5459
  envVars,
5061
5460
  isMonorepo,
5062
5461
  monorepoApps,
5063
- usesNodeFs: sourceEntries.some(([, content]) => usesNodeFs(content))
5462
+ usesNodeFs: sourceEntries.some(([, content]) => usesNodeFs(content)),
5463
+ nodeVersion: rootPkgContent ? detectNodeVersion(rootPkgContent) : void 0
5064
5464
  };
5065
5465
  }
5066
5466
 
@@ -5283,7 +5683,7 @@ const LOGO_LINES = [
5283
5683
  ];
5284
5684
  const BRAND_COLOR = "#FF4D00";
5285
5685
  function getVersion() {
5286
- return "0.0.0-beta.26";
5686
+ return "0.0.0-beta.28";
5287
5687
  }
5288
5688
  function printBanner(subtitle) {
5289
5689
  const version = getVersion();
@@ -6011,32 +6411,6 @@ function checkDockerfileLockFiles(basePath) {
6011
6411
  * and public/ and does not simplify the setup on this platform.
6012
6412
  */
6013
6413
  /**
6014
- * Prisma needs `prisma generate` in the build step.
6015
- * Without it, the Prisma client won't be generated and the app will crash.
6016
- */
6017
- function checkPrismaGenerate(basePath) {
6018
- const pkgPath = resolve(basePath, "package.json");
6019
- if (!existsSync(pkgPath)) return null;
6020
- try {
6021
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
6022
- const allDeps = {
6023
- ...pkg.dependencies,
6024
- ...pkg.devDependencies
6025
- };
6026
- if (!("prisma" in allDeps) && !("@prisma/client" in allDeps)) return null;
6027
- const scripts = pkg.scripts || {};
6028
- const buildScript = scripts.build || "";
6029
- const postinstall = scripts.postinstall || "";
6030
- if (buildScript.includes("prisma generate") || postinstall.includes("prisma generate") || buildScript.includes("prisma db push")) return null;
6031
- return {
6032
- message: "Prisma detectado mas prisma generate nao esta no build/postinstall",
6033
- hint: "Adicione \"prisma generate\" ao postinstall ou build → onveloz.com/docs/best-practices#prisma--prisma-generate-no-build"
6034
- };
6035
- } catch {
6036
- return null;
6037
- }
6038
- }
6039
- /**
6040
6414
  * Detect if the app hardcodes a port that doesn't match the configured port.
6041
6415
  * Common issue: app listens on 8080 but service port is 3000.
6042
6416
  */
@@ -6270,7 +6644,6 @@ function runPreDeployChecks(basePath = ".") {
6270
6644
  const checks = [
6271
6645
  checkNitroPreset,
6272
6646
  checkDockerfileLockFiles,
6273
- checkPrismaGenerate,
6274
6647
  checkPortMismatch,
6275
6648
  checkEnvFileCommitted,
6276
6649
  checkSvelteKitAdapter,
@@ -6333,24 +6706,50 @@ async function fetchLatestVersion() {
6333
6706
  return null;
6334
6707
  }
6335
6708
  }
6336
- async function autoUpdate() {
6337
- if (process.env.VELOZ_MCP === "true") return;
6338
- const pm = detectPackageManager();
6339
- if (!pm) return;
6340
- const currentVersion = "0.0.0-beta.26";
6341
- const latestVersion = await fetchLatestVersion();
6342
- if (!latestVersion || latestVersion === currentVersion) return;
6343
- const installCmd = getInstallCommand(pm, latestVersion);
6344
- const spin = spinner(`Atualizando CLI ${chalk.dim(currentVersion)} → ${chalk.bold(latestVersion)}...`);
6709
+ function getCurrentVersion() {
6710
+ return "0.0.0-beta.28";
6711
+ }
6712
+ /**
6713
+ * Install a specific CLI version. Returns true on success.
6714
+ */
6715
+ function installVersion(pm, version) {
6716
+ const installCmd = getInstallCommand(pm, version);
6717
+ const spin = spinner(`Atualizando CLI ${chalk.dim(getCurrentVersion())} → ${chalk.bold(version)}...`);
6345
6718
  try {
6346
6719
  execSync(installCmd, { stdio: "ignore" });
6347
6720
  spin.stop();
6348
- console.log(chalk.green(`\n✓ CLI atualizada: ${chalk.bold(latestVersion)}\n`));
6721
+ console.log(chalk.green(`\n✓ CLI atualizada: ${chalk.bold(version)}\n`));
6722
+ return true;
6349
6723
  } catch {
6350
6724
  spin.stop();
6351
6725
  console.log(chalk.yellow(`\n⚠ Não foi possível atualizar automaticamente. Execute manualmente:\n ${installCmd}\n`));
6726
+ return false;
6352
6727
  }
6353
6728
  }
6729
+ /**
6730
+ * Re-exec the CLI with the same args so the new binary runs.
6731
+ * Used after auto-update during `veloz deploy`.
6732
+ */
6733
+ function reExec() {
6734
+ execSync(`veloz ${process.argv.slice(2).join(" ")}`, {
6735
+ stdio: "inherit",
6736
+ env: {
6737
+ ...process.env,
6738
+ VELOZ_SKIP_UPDATE: "true"
6739
+ }
6740
+ });
6741
+ process.exit(0);
6742
+ }
6743
+ async function autoUpdate() {
6744
+ if (process.env.VELOZ_MCP === "true") return;
6745
+ if (process.env.VELOZ_SKIP_UPDATE === "true") return;
6746
+ const pm = detectPackageManager();
6747
+ if (!pm) return;
6748
+ const currentVersion = getCurrentVersion();
6749
+ const latestVersion = await fetchLatestVersion();
6750
+ if (!latestVersion || latestVersion === currentVersion) return;
6751
+ if (installVersion(pm, latestVersion)) reExec();
6752
+ }
6354
6753
 
6355
6754
  //#endregion
6356
6755
  //#region src/lib/deploy-databases.ts
@@ -6595,7 +6994,6 @@ function prepareServicesForDeploy(services) {
6595
6994
  }
6596
6995
  return services.map((svc) => {
6597
6996
  const serviceConf = resolveServiceConf(velozConfig, svc.serviceId);
6598
- warnIfEphemeralFsDetected(detectLocalRepo(serviceConf?.rootDirectory || "."), serviceConf, svc.serviceName);
6599
6997
  return {
6600
6998
  serviceId: svc.serviceId,
6601
6999
  serviceName: svc.serviceName,
@@ -6604,13 +7002,12 @@ function prepareServicesForDeploy(services) {
6604
7002
  };
6605
7003
  });
6606
7004
  }
6607
- async function triggerDeploy(serviceId, serviceName, preDetection) {
7005
+ async function triggerDeploy(serviceId, serviceName) {
6608
7006
  const sizeInBytes = await calculateDirectorySize(process.cwd());
6609
7007
  const sizeMB = Math.round(sizeInBytes / (1024 * 1024) * 10) / 10;
6610
7008
  const client = await getClient();
6611
7009
  const velozConfig = loadConfig();
6612
7010
  const serviceConf = resolveServiceConf(velozConfig, serviceId);
6613
- warnIfEphemeralFsDetected(preDetection ?? detectLocalRepo(), serviceConf, serviceName ?? void 0);
6614
7011
  const warnings = runPreDeployChecks(serviceConf?.rootDirectory || ".");
6615
7012
  if (warnings.length > 0) printDeployWarnings(warnings);
6616
7013
  const spinUpload = spinner(serviceName ? `Fazendo upload ${chalk.bold(serviceName)}...` : "Fazendo upload do código...");
@@ -6637,25 +7034,6 @@ async function triggerDeploy(serviceId, serviceName, preDetection) {
6637
7034
  untrackDeployment(deployment.id);
6638
7035
  }
6639
7036
  }
6640
- function warnIfEphemeralFsDetected(detection, serviceConf, serviceLabel) {
6641
- if (!detection.usesNodeFs || (serviceConf?.volumes?.length ?? 0) > 0) return;
6642
- warn(`Uso de fs/node:fs detectado${serviceLabel ? ` no serviço ${chalk.bold(serviceLabel)}` : ""}. O filesystem do container é efêmero; configure um volume em veloz.json ou use 'veloz volumes create'.`);
6643
- }
6644
- async function maybeConfigurePersistentVolume(serviceConfig, detection, opts, serviceLabel) {
6645
- if (!detection.usesNodeFs || (serviceConfig.volumes?.length ?? 0) > 0) return;
6646
- if (opts.yes) return;
6647
- if (!await promptConfirm(`Deseja adicionar um volume persistente para ${serviceLabel}?`, true)) return;
6648
- const name = await prompt(`Nome do volume ${chalk.dim("(data)")}:`) || "data";
6649
- const mountPath = await prompt(`Mount path ${chalk.dim("(/data)")}:`) || "/data";
6650
- const sizeInput = await prompt(`Tamanho em GB ${chalk.dim("(10)")}:`);
6651
- const parsedSize = Number.parseInt(sizeInput || "10", 10);
6652
- serviceConfig.volumes = [{
6653
- name,
6654
- mountPath,
6655
- sizeGb: Number.isInteger(parsedSize) && parsedSize >= 10 ? parsedSize : 10
6656
- }];
6657
- info(`Volume persistente configurado para ${serviceLabel}.`);
6658
- }
6659
7037
  async function findServicesFromConfig() {
6660
7038
  const config = loadConfig();
6661
7039
  if (!config) return [];
@@ -6907,7 +7285,8 @@ async function createServiceFlow(projectId, projectName, repoName, opts = {}) {
6907
7285
  buildCommand: a.framework?.buildCommand ?? null,
6908
7286
  startCommand: a.framework?.startCommand ?? null,
6909
7287
  port: a.framework?.port ?? 3e3,
6910
- usesNodeFs: a.usesNodeFs
7288
+ usesNodeFs: a.usesNodeFs,
7289
+ nodeVersion: a.nodeVersion
6911
7290
  }));
6912
7291
  let selectedApps;
6913
7292
  if (opts.yes) if (opts.app) {
@@ -6951,7 +7330,7 @@ async function createServiceFlow(projectId, projectName, repoName, opts = {}) {
6951
7330
  if (newPort) app.port = parseInt(newPort, 10) || app.port;
6952
7331
  }
6953
7332
  }
6954
- let config$1 = {
7333
+ let config = {
6955
7334
  version: "1.0",
6956
7335
  project: {
6957
7336
  id: projectId,
@@ -6974,7 +7353,8 @@ async function createServiceFlow(projectId, projectName, repoName, opts = {}) {
6974
7353
  buildCommand: app.buildCommand ?? void 0,
6975
7354
  startCommand: app.startCommand ?? void 0,
6976
7355
  port: app.port ?? 3e3,
6977
- framework: app.framework ?? void 0
7356
+ framework: app.framework ?? void 0,
7357
+ nodeVersion: app.nodeVersion
6978
7358
  }))
6979
7359
  });
6980
7360
  success(`Serviço criado: ${chalk.bold(service$1.name)}`);
@@ -6982,7 +7362,7 @@ async function createServiceFlow(projectId, projectName, repoName, opts = {}) {
6982
7362
  service: service$1,
6983
7363
  app
6984
7364
  });
6985
- config$1.services[app.root] = {
7365
+ config.services[app.root] = {
6986
7366
  id: service$1.id,
6987
7367
  name: service$1.name,
6988
7368
  type: "web",
@@ -6994,21 +7374,20 @@ async function createServiceFlow(projectId, projectName, repoName, opts = {}) {
6994
7374
  port: app.port ?? 3e3
6995
7375
  }
6996
7376
  };
6997
- await maybeConfigurePersistentVolume(config$1.services[app.root], detectLocalRepo(app.root), opts, app.name);
6998
7377
  }
6999
- saveConfig(config$1);
7378
+ saveConfig(config);
7000
7379
  info(`Arquivo ${getConfigFileName()} criado na raiz do projeto.`);
7001
7380
  if (!opts.yes) for (const { service: service$1, app } of createdServices) {
7002
7381
  console.log(chalk.cyan(`\n── Configurando variáveis: ${app.name} ──\n`));
7003
7382
  const serviceDetection = detectLocalRepo(app.root);
7004
- await promptEnvVars(service$1.id, serviceDetection.envVars.map((v) => v.key), config$1);
7383
+ await promptEnvVars(service$1.id, serviceDetection.envVars.map((v) => v.key), config);
7005
7384
  }
7006
- config$1 = await provisionDatabases(config$1, { yes: opts.yes ?? false });
7385
+ config = await provisionDatabases(config, { yes: opts.yes ?? false });
7007
7386
  await deployServices(createdServices.map(({ service: service$1, app }) => ({
7008
7387
  serviceId: service$1.id,
7009
7388
  serviceName: app.name,
7010
7389
  projectId,
7011
- serviceConfig: resolveServiceConf(config$1, service$1.id)
7390
+ serviceConfig: resolveServiceConf(config, service$1.id)
7012
7391
  })), {
7013
7392
  projectRoot: process.cwd(),
7014
7393
  output: createDeployOutput()
@@ -7067,12 +7446,13 @@ async function createServiceFlow(projectId, projectName, repoName, opts = {}) {
7067
7446
  buildCommand: settings.buildCommand ?? void 0,
7068
7447
  startCommand: settings.startCommand ?? void 0,
7069
7448
  port: settings.port,
7070
- framework: settings.framework ?? void 0
7449
+ framework: settings.framework ?? void 0,
7450
+ nodeVersion: detection.nodeVersion
7071
7451
  }))
7072
7452
  });
7073
7453
  success(`Serviço criado: ${chalk.bold(service.name)}`);
7074
7454
  if (!opts.yes) await promptEnvVars(service.id, detection.envVars.map((v) => v.key), null);
7075
- const config = {
7455
+ saveConfig({
7076
7456
  version: "1.0",
7077
7457
  project: {
7078
7458
  id: projectId,
@@ -7094,9 +7474,7 @@ async function createServiceFlow(projectId, projectName, repoName, opts = {}) {
7094
7474
  }
7095
7475
  } },
7096
7476
  created: (/* @__PURE__ */ new Date()).toISOString()
7097
- };
7098
- await maybeConfigurePersistentVolume(config.services.main, detection, opts, service.name);
7099
- saveConfig(config);
7477
+ });
7100
7478
  info(`Arquivo ${getConfigFileName()} criado na raiz do projeto.`);
7101
7479
  return service.id;
7102
7480
  }
@@ -7187,7 +7565,8 @@ async function addServiceFlow(existingConfig, opts) {
7187
7565
  buildCommand: a.framework?.buildCommand ?? null,
7188
7566
  startCommand: a.framework?.startCommand ?? null,
7189
7567
  port: a.framework?.port ?? 3e3,
7190
- usesNodeFs: a.usesNodeFs
7568
+ usesNodeFs: a.usesNodeFs,
7569
+ nodeVersion: a.nodeVersion
7191
7570
  }));
7192
7571
  const availableApps = allApps.filter((a) => !existingRoots.has(a.root));
7193
7572
  if (availableApps.length === 0) {
@@ -7261,7 +7640,8 @@ async function addServiceFlow(existingConfig, opts) {
7261
7640
  buildCommand: hasDockerfile ? void 0 : app.buildCommand ?? void 0,
7262
7641
  startCommand: hasDockerfile ? void 0 : app.startCommand ?? void 0,
7263
7642
  port: app.port ?? 3e3,
7264
- framework: hasDockerfile ? "docker" : app.framework ?? void 0
7643
+ framework: hasDockerfile ? "docker" : app.framework ?? void 0,
7644
+ nodeVersion: hasDockerfile ? void 0 : app.nodeVersion
7265
7645
  }))
7266
7646
  });
7267
7647
  success(`Serviço criado: ${chalk.bold(service$1.name)}`);
@@ -7286,7 +7666,6 @@ async function addServiceFlow(existingConfig, opts) {
7286
7666
  port: app.port ?? 3e3
7287
7667
  }
7288
7668
  };
7289
- await maybeConfigurePersistentVolume(updatedConfig$1.services[configKey], detectLocalRepo(app.root), opts, serviceName);
7290
7669
  }
7291
7670
  const newServices = {};
7292
7671
  for (const { app } of createdServices) {
@@ -7373,7 +7752,8 @@ async function addServiceFlow(existingConfig, opts) {
7373
7752
  buildCommand: settings.buildCommand ?? void 0,
7374
7753
  startCommand: settings.startCommand ?? void 0,
7375
7754
  port: settings.port,
7376
- framework: settings.framework ?? void 0
7755
+ framework: settings.framework ?? void 0,
7756
+ nodeVersion: detection.nodeVersion
7377
7757
  }))
7378
7758
  });
7379
7759
  success(`Serviço criado: ${chalk.bold(service.name)}`);
@@ -7401,7 +7781,6 @@ async function addServiceFlow(existingConfig, opts) {
7401
7781
  },
7402
7782
  updated: (/* @__PURE__ */ new Date()).toISOString()
7403
7783
  };
7404
- await maybeConfigurePersistentVolume(updatedConfig.services[serviceKey], detection, opts, service.name);
7405
7784
  const newServiceEntry = updatedConfig.services[serviceKey];
7406
7785
  patchConfig((raw) => {
7407
7786
  raw.services[serviceKey] = newServiceEntry;
@@ -7435,7 +7814,7 @@ async function resolveOrgIfNeeded(nonInteractive) {
7435
7814
  }
7436
7815
  function registerDeploy(cli$1) {
7437
7816
  cli$1.command("deploy", {
7438
- description: "Fazer deploy do serviço",
7817
+ description: "Fazer deploy do serviço. Returns streamed progress events. The last event (type='result') contains: deploymentId, status (LIVE/BUILD_FAILED/DEPLOY_FAILED/FAILED/CANCELLED), urls, failReason, errorSummary, commitSha, branch, durationSeconds, dashboardUrl, and logs.",
7439
7818
  middleware: [requireAuth],
7440
7819
  options: z.object({
7441
7820
  all: z.boolean().optional().describe("Deploy todos os serviços do monorepo"),
@@ -7642,7 +8021,6 @@ async function cliDeployFlow(opts) {
7642
8021
  serviceName = project.services.find((s) => s.id === serviceId$1)?.name ?? "";
7643
8022
  }
7644
8023
  const selectedService = project.services.find((s) => s.id === serviceId$1);
7645
- const detection = detectLocalRepo();
7646
8024
  const detectedType = selectedService?.type?.toLowerCase() ?? "web";
7647
8025
  saveConfig({
7648
8026
  version: "1.0",
@@ -7662,7 +8040,7 @@ async function cliDeployFlow(opts) {
7662
8040
  info(`Arquivo ${getConfigFileName()} criado na raiz do projeto.`);
7663
8041
  const freshConfig = loadConfig();
7664
8042
  if (freshConfig) await provisionDatabases(freshConfig, { yes: opts.yes ?? false });
7665
- await triggerDeploy(serviceId$1, void 0, detection);
8043
+ await triggerDeploy(serviceId$1);
7666
8044
  return;
7667
8045
  }
7668
8046
  if (project && project.services.length === 0) {
@@ -7955,13 +8333,64 @@ async function pruneRemovedEntries(config, existingConfig, services, databases)
7955
8333
  return config;
7956
8334
  }
7957
8335
 
8336
+ //#endregion
8337
+ //#region src/commands/update.ts
8338
+ function registerUpdate(cli$1) {
8339
+ cli$1.command("update", {
8340
+ description: "Atualizar a CLI para a versão mais recente",
8341
+ output: z.object({
8342
+ updated: z.boolean(),
8343
+ version: z.string().nullable()
8344
+ }),
8345
+ async run() {
8346
+ const pm = detectPackageManager();
8347
+ if (!pm) {
8348
+ warn("Instalação de desenvolvimento detectada. Use 'pnpm cli:install' para atualizar.");
8349
+ return {
8350
+ updated: false,
8351
+ version: null
8352
+ };
8353
+ }
8354
+ const currentVersion = getCurrentVersion();
8355
+ info(`Versão atual: ${chalk.bold(currentVersion)}`);
8356
+ const latestVersion = await fetchLatestVersion();
8357
+ if (!latestVersion) {
8358
+ warn("Não foi possível verificar a versão mais recente.");
8359
+ return {
8360
+ updated: false,
8361
+ version: null
8362
+ };
8363
+ }
8364
+ if (latestVersion === currentVersion) {
8365
+ info(`Você já está na versão mais recente ${chalk.bold(currentVersion)}.`);
8366
+ return {
8367
+ updated: false,
8368
+ version: currentVersion
8369
+ };
8370
+ }
8371
+ if (!installVersion(pm, latestVersion)) {
8372
+ console.log(chalk.dim(` Execute manualmente: ${getInstallCommand(pm, latestVersion)}`));
8373
+ return {
8374
+ updated: false,
8375
+ version: null
8376
+ };
8377
+ }
8378
+ return {
8379
+ updated: true,
8380
+ version: latestVersion
8381
+ };
8382
+ }
8383
+ });
8384
+ }
8385
+
7958
8386
  //#endregion
7959
8387
  //#region src/index.ts
7960
8388
  if (process.argv.includes("--mcp")) process.env.VELOZ_MCP = "true";
7961
8389
  const cli = Cli.create("veloz", {
7962
- version: "0.0.0-beta.26",
8390
+ version: "0.0.0-beta.28",
7963
8391
  description: "CLI da plataforma Veloz — deploy rápido para o Brasil",
7964
- env: z.object({ VELOZ_ENV: z.string().optional().describe("Ambiente alvo (ex: preview, staging)") })
8392
+ env: z.object({ VELOZ_ENV: z.string().optional().describe("Ambiente alvo (ex: preview, staging)") }),
8393
+ mcp: { command: "npx -y onveloz --mcp" }
7965
8394
  });
7966
8395
  cli.use(async (c, next) => {
7967
8396
  const env = c.env.VELOZ_ENV;
@@ -8033,6 +8462,7 @@ registerDeploy(cli);
8033
8462
  registerUse(cli);
8034
8463
  registerWhoami(cli);
8035
8464
  registerPull(cli);
8465
+ registerUpdate(cli);
8036
8466
  cli.serve();
8037
8467
 
8038
8468
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onveloz",
3
- "version": "0.0.0-beta.26",
3
+ "version": "0.0.0-beta.28",
4
4
  "description": "CLI da plataforma Veloz — deploy rápido para o Brasil",
5
5
  "keywords": [
6
6
  "brasil",