onveloz 0.0.0-beta.18 → 0.0.0-beta.19

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 +896 -104
  2. package/package.json +2 -2
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, execSync } from "node:child_process";
3
+ import { exec, execFile, execSync } from "node:child_process";
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
5
5
  import { dirname, join, relative, resolve } from "node:path";
6
6
  import { z as z$1 } from "zod";
@@ -174,8 +174,9 @@ function getDatabaseSizeOptions(engine) {
174
174
  }
175
175
  /** @deprecated Use getDatabaseSizeOptions(engine) instead. */
176
176
  const DATABASE_SIZE_OPTIONS = getDatabaseSizeOptions("postgresql");
177
- function resolveDatabaseSize(cpu, memory) {
178
- for (const [key, tier] of Object.entries(DATABASE_SIZES)) if (tier.cpu === cpu && tier.memory === memory) return key;
177
+ function resolveDatabaseSize(cpu, memory, engine) {
178
+ const sizes = engine ? DATABASE_SIZES_BY_ENGINE[engine] : DATABASE_SIZES;
179
+ for (const [key, tier] of Object.entries(sizes)) if (tier.cpu === cpu && tier.memory === memory) return key;
179
180
  return null;
180
181
  }
181
182
 
@@ -278,7 +279,9 @@ const ServiceConfigSchema = z$1.object({
278
279
  runtime: RuntimeConfigSchema.optional(),
279
280
  env: z$1.record(z$1.string().regex(/^[A-Z][A-Z0-9_]*$/), EnvVarDefinitionSchema).optional(),
280
281
  volumes: z$1.array(VolumeConfigSchema).optional(),
281
- resources: ResourcesSchema.optional()
282
+ resources: ResourcesSchema.optional(),
283
+ ignore: z$1.string().optional(),
284
+ watch: z$1.array(z$1.string()).optional()
282
285
  });
283
286
  const ProjectConfigSchema = z$1.object({
284
287
  id: z$1.string(),
@@ -603,6 +606,34 @@ async function withSpinner(opts) {
603
606
  function isInteractive() {
604
607
  return process.stdout.isTTY === true && process.stdin.isTTY === true;
605
608
  }
609
+ /**
610
+ * Gate for destructive actions in non-interactive (MCP) mode.
611
+ *
612
+ * Forces AI agents to:
613
+ * 1. Explain the action to the user first
614
+ * 2. Ask the user for explicit approval
615
+ * 3. Copy the user's approval text into --userConfirmation
616
+ *
617
+ * In interactive (TTY) mode this is a no-op — the normal promptConfirm
618
+ * flow handles confirmation.
619
+ */
620
+ function requireMcpConfirmation(userConfirmation, action) {
621
+ if (isInteractive()) return;
622
+ if (!userConfirmation) throw new Error(`Ação destrutiva: ${action}. ANTES de executar esta ação, você DEVE:
623
+ 1. Explicar ao usuário exatamente o que esta ação faz e quais são as consequências
624
+ 2. Aguardar o usuário confirmar explicitamente com uma mensagem de aprovação
625
+ 3. Copiar a resposta de confirmação do usuário no parâmetro --userConfirmation
626
+
627
+ NÃO execute esta ação sem aprovação explícita do usuário. O parâmetro --yes NÃO substitui --userConfirmation.`);
628
+ }
629
+ /**
630
+ * Whether the current non-interactive confirmation came from --userConfirmation
631
+ * (valid) vs only --yes (not sufficient in MCP mode).
632
+ */
633
+ function hasValidNonInteractiveConfirmation(userConfirmation, yes) {
634
+ if (isMcpMode()) return !!userConfirmation;
635
+ return yes || !!userConfirmation;
636
+ }
606
637
  async function prompt(question) {
607
638
  if (!isInteractive()) return "";
608
639
  const rl = createInterface({
@@ -655,7 +686,7 @@ async function promptMultiSelect(question, options) {
655
686
 
656
687
  //#endregion
657
688
  //#region src/commands/login.ts
658
- function openBrowser(url) {
689
+ function openBrowser$1(url) {
659
690
  const os = platform();
660
691
  exec(os === "darwin" ? `open "${url}"` : os === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`, () => {});
661
692
  }
@@ -683,7 +714,7 @@ async function performLogin(apiUrl) {
683
714
  console.log(chalk.dim(" Se o navegador não abrir, acesse manualmente:"));
684
715
  console.log(chalk.dim(` ${verificationUrl}`));
685
716
  console.log();
686
- openBrowser(verificationUrl);
717
+ openBrowser$1(verificationUrl);
687
718
  const pollSpinner = spinner("Aguardando autorização no navegador...");
688
719
  const token = await pollForToken(authClient, data.device_code, data.interval || 5);
689
720
  if (!token) {
@@ -1117,19 +1148,26 @@ envGroup.command("set", {
1117
1148
  }
1118
1149
  spin.stop();
1119
1150
  for (const w of allWarnings) warn(w);
1120
- if (pares.length === 1) {
1121
- const key = pares[0].slice(0, pares[0].indexOf("="));
1122
- success(`Variável ${chalk.bold(key)} definida com sucesso.`);
1123
- } else success(`${pares.length} variáveis definidas com sucesso.`);
1151
+ const keys = pares.map((p) => p.slice(0, p.indexOf("=")));
1152
+ if (pares.length === 1) success(`Variável ${chalk.bold(keys[0])} definida com sucesso.`);
1153
+ else success(`${pares.length} variáveis definidas com sucesso.`);
1124
1154
  info("Faça um novo deploy para aplicar as alterações.");
1155
+ return {
1156
+ count: pares.length,
1157
+ keys
1158
+ };
1125
1159
  }
1126
1160
  });
1127
1161
  envGroup.command("delete", {
1128
1162
  description: "Remover variável de ambiente",
1129
1163
  middleware: [requireAuth],
1130
1164
  args: z.object({ chave: z.string().describe("Chave da variável de ambiente") }),
1131
- options: z.object({ service: z.string().optional().describe("Serviço alvo (chave ou nome)") }),
1165
+ options: z.object({
1166
+ service: z.string().optional().describe("Serviço alvo (chave ou nome)"),
1167
+ userConfirmation: z.string().optional()
1168
+ }),
1132
1169
  async run(c) {
1170
+ requireMcpConfirmation(c.options.userConfirmation, `remover variável de ambiente "${c.args.chave}"`);
1133
1171
  const serviceId = await resolveServiceId(c.options.service);
1134
1172
  const client = await getClient();
1135
1173
  await withSpinner({
@@ -1141,6 +1179,10 @@ envGroup.command("delete", {
1141
1179
  });
1142
1180
  success(`Variável ${chalk.bold(c.args.chave)} removida com sucesso.`);
1143
1181
  info("Faça um novo deploy para aplicar as alterações.");
1182
+ return {
1183
+ deleted: true,
1184
+ key: c.args.chave
1185
+ };
1144
1186
  }
1145
1187
  });
1146
1188
  envGroup.command("import", {
@@ -1149,9 +1191,11 @@ envGroup.command("import", {
1149
1191
  args: z.object({ arquivo: z.string().optional().describe("Caminho do arquivo .env") }),
1150
1192
  options: z.object({
1151
1193
  replace: z.boolean().default(false).describe("Substituir todas as variáveis existentes"),
1152
- service: z.string().optional().describe("Serviço alvo (chave ou nome)")
1194
+ service: z.string().optional().describe("Serviço alvo (chave ou nome)"),
1195
+ userConfirmation: z.string().optional()
1153
1196
  }),
1154
1197
  async run(c) {
1198
+ if (c.options.replace) requireMcpConfirmation(c.options.userConfirmation, "substituir TODAS as variáveis de ambiente existentes");
1155
1199
  const serviceId = await resolveServiceId(c.options.service);
1156
1200
  const client = await getClient();
1157
1201
  let envContent = "";
@@ -1220,6 +1264,11 @@ envGroup.command("import", {
1220
1264
  if (result.warnings) for (const w of result.warnings) warn(w);
1221
1265
  success(`${varsCount} variável(is) importada(s) com sucesso!`);
1222
1266
  info("Faça um novo deploy para aplicar as alterações.");
1267
+ return {
1268
+ imported: true,
1269
+ count: varsCount,
1270
+ keys: Object.keys(envVars)
1271
+ };
1223
1272
  }
1224
1273
  });
1225
1274
  envGroup.command("export", {
@@ -1337,6 +1386,11 @@ domainsGroup.command("add", {
1337
1386
  console.log(chalk.white(` ${result.dnsInstruction}`));
1338
1387
  console.log();
1339
1388
  info(`Após configurar o DNS, execute: ${chalk.bold(`veloz domains verify ${result.id}`)}`);
1389
+ return {
1390
+ id: result.id,
1391
+ domain: c.args.dominio,
1392
+ dnsInstruction: result.dnsInstruction
1393
+ };
1340
1394
  }
1341
1395
  });
1342
1396
  domainsGroup.command("verify", {
@@ -1354,19 +1408,30 @@ domainsGroup.command("verify", {
1354
1408
  console.log(chalk.yellow(`\n⚠ Certificado TLS de ${chalk.bold(result.domain.domain)} ainda sendo provisionado.`));
1355
1409
  info("Verifique se o CNAME foi configurado para cname.onveloz.com.");
1356
1410
  }
1411
+ return {
1412
+ ready: result.ready,
1413
+ domain: result.domain.domain,
1414
+ tlsStatus: result.domain.tlsStatus
1415
+ };
1357
1416
  }
1358
1417
  });
1359
1418
  domainsGroup.command("delete", {
1360
1419
  description: "Remover domínio",
1361
1420
  middleware: [requireAuth],
1362
1421
  args: z.object({ domainId: z.string().describe("ID do domínio") }),
1422
+ options: z.object({ userConfirmation: z.string().optional() }),
1363
1423
  async run(c) {
1424
+ requireMcpConfirmation(c.options.userConfirmation, `remover domínio "${c.args.domainId}"`);
1364
1425
  const client = await getClient();
1365
1426
  await withSpinner({
1366
1427
  text: "Removendo domínio...",
1367
1428
  fn: () => client.domains.delete({ domainId: c.args.domainId })
1368
1429
  });
1369
1430
  success("Domínio removido com sucesso.");
1431
+ return {
1432
+ deleted: true,
1433
+ domainId: c.args.domainId
1434
+ };
1370
1435
  }
1371
1436
  });
1372
1437
 
@@ -1517,6 +1582,12 @@ volumesGroup.command("create", {
1517
1582
  });
1518
1583
  success("Serviço reiniciando com os volumes atualizados.");
1519
1584
  } else await promptAndSyncDeployment(service.id);
1585
+ return {
1586
+ id: volume.id,
1587
+ name: volume.name,
1588
+ mountPath: volume.mountPath,
1589
+ sizeGb: volume.sizeGb
1590
+ };
1520
1591
  }
1521
1592
  });
1522
1593
  volumesGroup.command("expand", {
@@ -1545,6 +1616,11 @@ volumesGroup.command("expand", {
1545
1616
  sizeGb
1546
1617
  } : entry));
1547
1618
  success(`Volume ${chalk.bold(volume.name)} expandido para ${sizeGb} GB.`);
1619
+ return {
1620
+ id: volume.id,
1621
+ name: volume.name,
1622
+ sizeGb
1623
+ };
1548
1624
  }
1549
1625
  });
1550
1626
  volumesGroup.command("delete", {
@@ -1554,13 +1630,15 @@ volumesGroup.command("delete", {
1554
1630
  options: z.object({
1555
1631
  service: z.string().optional().describe("Serviço alvo (chave ou nome)"),
1556
1632
  restart: z.boolean().default(false).describe("Reiniciar o serviço imediatamente após remover"),
1557
- yes: z.boolean().default(false).describe("Pular confirmação")
1633
+ yes: z.boolean().default(false).describe("Pular confirmação"),
1634
+ userConfirmation: z.string().optional()
1558
1635
  }),
1559
1636
  async run(c) {
1560
1637
  const { key, service } = await resolveService(c.options.service);
1561
1638
  const client = await getClient();
1562
1639
  const volume = await resolveRemoteVolume(service.id, c.args.volume);
1563
- if (!(isInteractive() ? await promptConfirm(`Remover volume ${chalk.bold(volume.name)} (${volume.mountPath}) do serviço? Os dados serão mantidos por 30 dias.`, false) : c.options.yes === true)) {
1640
+ requireMcpConfirmation(c.options.userConfirmation, `remover volume "${volume.name}" do serviço`);
1641
+ if (!(isInteractive() ? await promptConfirm(`Remover volume ${chalk.bold(volume.name)} (${volume.mountPath}) do serviço? Os dados serão mantidos por 30 dias.`, false) : hasValidNonInteractiveConfirmation(c.options.userConfirmation, c.options.yes))) {
1564
1642
  info("Operação cancelada.");
1565
1643
  return;
1566
1644
  }
@@ -1578,6 +1656,10 @@ volumesGroup.command("delete", {
1578
1656
  });
1579
1657
  success("Serviço reiniciando sem o volume.");
1580
1658
  } else await promptAndSyncDeployment(service.id);
1659
+ return {
1660
+ deleted: true,
1661
+ name: volume.name
1662
+ };
1581
1663
  }
1582
1664
  });
1583
1665
  volumesGroup.command("sync", {
@@ -1592,6 +1674,10 @@ volumesGroup.command("sync", {
1592
1674
  fn: () => client.volumes.syncDeployment({ serviceId: service.id })
1593
1675
  });
1594
1676
  success("Serviço reiniciando com os volumes atualizados.");
1677
+ return {
1678
+ synced: true,
1679
+ serviceId: service.id
1680
+ };
1595
1681
  }
1596
1682
  });
1597
1683
 
@@ -1721,6 +1807,7 @@ configGroup.command("set", {
1721
1807
  }
1722
1808
  console.log();
1723
1809
  info("Execute 'veloz deploy' para aplicar as mudanças.");
1810
+ return { updated: updates };
1724
1811
  }
1725
1812
  });
1726
1813
  configGroup.command("edit", {
@@ -1768,6 +1855,7 @@ configGroup.command("edit", {
1768
1855
  });
1769
1856
  success("Configurações atualizadas com sucesso!");
1770
1857
  info("Execute 'veloz deploy' para aplicar as mudanças.");
1858
+ return { updated: updates };
1771
1859
  }
1772
1860
  });
1773
1861
  configGroup.command("reset", {
@@ -1778,9 +1866,11 @@ configGroup.command("reset", {
1778
1866
  start: z.boolean().default(false).describe("Resetar comando de start"),
1779
1867
  preStart: z.boolean().default(false).describe("Resetar comando de pre-start"),
1780
1868
  all: z.boolean().default(false).describe("Resetar todas as configurações opcionais"),
1781
- service: z.string().optional().describe("Serviço alvo (chave ou nome)")
1869
+ service: z.string().optional().describe("Serviço alvo (chave ou nome)"),
1870
+ userConfirmation: z.string().optional()
1782
1871
  }),
1783
1872
  async run(c) {
1873
+ requireMcpConfirmation(c.options.userConfirmation, "resetar configurações para os padrões");
1784
1874
  const serviceId = await resolveServiceId(c.options.service);
1785
1875
  const client = await getClient();
1786
1876
  const updates = {};
@@ -1807,6 +1897,7 @@ configGroup.command("reset", {
1807
1897
  });
1808
1898
  success("Configurações resetadas para os padrões!");
1809
1899
  info("Execute 'veloz deploy' para aplicar as mudanças.");
1900
+ return { reset: Object.keys(updates) };
1810
1901
  }
1811
1902
  });
1812
1903
 
@@ -2101,6 +2192,12 @@ dbGroup.command("create", {
2101
2192
  });
2102
2193
  info(`Adicionado ao ${getConfigFileName()}.`);
2103
2194
  }
2195
+ return {
2196
+ id: db.id,
2197
+ name: db.name,
2198
+ engine: validEngine,
2199
+ engineVersion: version ?? null
2200
+ };
2104
2201
  }
2105
2202
  });
2106
2203
  dbGroup.command("credentials", {
@@ -2137,10 +2234,14 @@ dbGroup.command("delete", {
2137
2234
  description: "Excluir um banco de dados",
2138
2235
  middleware: [requireAuth],
2139
2236
  args: z.object({ name: z.string().describe("Nome ou ID do banco de dados") }),
2140
- options: z.object({ yes: z.boolean().default(false).describe("Pular confirmação") }),
2237
+ options: z.object({
2238
+ yes: z.boolean().default(false).describe("Pular confirmação"),
2239
+ userConfirmation: z.string().optional()
2240
+ }),
2141
2241
  async run(c) {
2142
2242
  const db = await resolveDatabaseByName(getProjectId$1(), c.args.name);
2143
- if (!c.options.yes) {
2243
+ requireMcpConfirmation(c.options.userConfirmation, `excluir banco de dados "${db.name}"`);
2244
+ if (!c.options.yes && !c.options.userConfirmation) {
2144
2245
  if (!isInteractive()) throw new Error("Use --yes para confirmar a exclusão em modo não-interativo.");
2145
2246
  warn(`Você está prestes a excluir o banco de dados "${db.name}".`);
2146
2247
  console.log(chalk.red(" Esta ação é irreversível!\n"));
@@ -2174,20 +2275,31 @@ dbGroup.command("delete", {
2174
2275
  info(`Removido do ${getConfigFileName()}.`);
2175
2276
  }
2176
2277
  }
2278
+ return {
2279
+ deleted: true,
2280
+ name: db.name,
2281
+ dependentServices: result.dependentServices.map((d) => d.name)
2282
+ };
2177
2283
  }
2178
2284
  });
2179
2285
  dbGroup.command("restart", {
2180
2286
  description: "Reiniciar um banco de dados",
2181
2287
  middleware: [requireAuth],
2182
2288
  args: z.object({ name: z.string().describe("Nome ou ID do banco de dados") }),
2289
+ options: z.object({ userConfirmation: z.string().optional() }),
2183
2290
  async run(c) {
2184
2291
  const db = await resolveDatabaseByName(getProjectId$1(), c.args.name);
2292
+ requireMcpConfirmation(c.options.userConfirmation, `reiniciar banco de dados "${db.name}"`);
2185
2293
  const client = await getClient();
2186
2294
  await withSpinner({
2187
2295
  text: "Reiniciando banco de dados...",
2188
2296
  fn: () => client.databases.restart({ serviceId: db.id })
2189
2297
  });
2190
2298
  success(`Banco de dados ${chalk.bold(db.name)} reiniciado com sucesso.`);
2299
+ return {
2300
+ restarted: true,
2301
+ name: db.name
2302
+ };
2191
2303
  }
2192
2304
  });
2193
2305
  dbGroup.command("update", {
@@ -2312,7 +2424,10 @@ dbGroup.command("update", {
2312
2424
  }
2313
2425
  if (!updated) {
2314
2426
  info("Nenhuma alteração realizada.");
2315
- return;
2427
+ return {
2428
+ updated: false,
2429
+ changes: {}
2430
+ };
2316
2431
  }
2317
2432
  if (loadConfig()) {
2318
2433
  patchConfig((raw) => {
@@ -2335,6 +2450,15 @@ dbGroup.command("update", {
2335
2450
  });
2336
2451
  info(`Atualizado no ${getConfigFileName()}.`);
2337
2452
  }
2453
+ return {
2454
+ updated: true,
2455
+ changes: {
2456
+ ...size ? { size } : {},
2457
+ ...storage ? { storage } : {},
2458
+ ...poolerEnabled !== void 0 ? { poolerEnabled } : {},
2459
+ ...poolMode ? { poolMode } : {}
2460
+ }
2461
+ };
2338
2462
  }
2339
2463
  });
2340
2464
  dbGroup.command("query", {
@@ -2358,13 +2482,17 @@ dbGroup.command("query", {
2358
2482
  query
2359
2483
  });
2360
2484
  let hasOutput = false;
2485
+ const outputLines = [];
2486
+ const errorLines = [];
2361
2487
  for await (const event of stream) switch (event.type) {
2362
2488
  case "output":
2363
2489
  hasOutput = true;
2364
2490
  console.log(event.content);
2491
+ outputLines.push(event.content);
2365
2492
  break;
2366
2493
  case "error":
2367
2494
  console.log(chalk.red(event.content));
2495
+ errorLines.push(event.content);
2368
2496
  break;
2369
2497
  case "status":
2370
2498
  if (event.content === "Consulta finalizada.") {
@@ -2373,6 +2501,8 @@ dbGroup.command("query", {
2373
2501
  }
2374
2502
  break;
2375
2503
  }
2504
+ if (errorLines.length > 0) return { error: errorLines.join("\n") };
2505
+ return { output: outputLines.length > 0 ? outputLines.join("\n") : "Consulta executada sem retorno." };
2376
2506
  }
2377
2507
  });
2378
2508
  const ENGINE_DEFAULT_PORTS = {
@@ -2521,9 +2651,11 @@ templateGroup.command("deploy", {
2521
2651
  args: z.object({ slug: z.string().describe("Slug do template (ex: n8n, metabase)") }),
2522
2652
  options: z.object({
2523
2653
  yes: z.boolean().default(false).describe("Pular confirmação"),
2524
- name: z.string().optional().describe("Nome do projeto (padrão: nome do template)")
2654
+ name: z.string().optional().describe("Nome do projeto (padrão: nome do template)"),
2655
+ userConfirmation: z.string().optional()
2525
2656
  }),
2526
2657
  async run(c) {
2658
+ requireMcpConfirmation(c.options.userConfirmation, `fazer deploy do template "${c.args.slug}"`);
2527
2659
  const client = await getClient();
2528
2660
  const template = await withSpinner({
2529
2661
  text: "Carregando template...",
@@ -2542,7 +2674,7 @@ templateGroup.command("deploy", {
2542
2674
  }
2543
2675
  console.log(chalk.dim(` Um novo projeto será criado para este template${c.options.name ? `: ${c.options.name}` : ""}.`));
2544
2676
  console.log();
2545
- if (!c.options.yes) {
2677
+ if (!c.options.yes && !c.options.userConfirmation) {
2546
2678
  if (!await promptConfirm(`Deseja fazer deploy do template '${template.displayName}'?`, false)) {
2547
2679
  info("Operação cancelada.");
2548
2680
  return;
@@ -2565,6 +2697,14 @@ templateGroup.command("deploy", {
2565
2697
  console.log();
2566
2698
  }
2567
2699
  info("O deploy está sendo processado em background. Acompanhe pelo dashboard ou execute " + chalk.bold("veloz services list") + " no diretório do projeto.");
2700
+ return {
2701
+ projectName: result.projectName,
2702
+ projectSlug: result.projectSlug,
2703
+ services: result.services.map((svc) => ({
2704
+ name: svc.name,
2705
+ type: svc.type
2706
+ }))
2707
+ };
2568
2708
  }
2569
2709
  });
2570
2710
 
@@ -2609,7 +2749,10 @@ servicesGroup.command("delete", {
2609
2749
  description: "Deletar um serviço",
2610
2750
  middleware: [requireAuth],
2611
2751
  args: z.object({ service: z.string().optional().describe("Nome ou ID do serviço") }),
2612
- options: z.object({ yes: z.boolean().default(false).describe("Pular confirmação") }),
2752
+ options: z.object({
2753
+ yes: z.boolean().default(false).describe("Pular confirmação"),
2754
+ userConfirmation: z.string().optional()
2755
+ }),
2613
2756
  async run(c) {
2614
2757
  const projectId = getProjectId();
2615
2758
  const client = await getClient();
@@ -2641,7 +2784,8 @@ servicesGroup.command("delete", {
2641
2784
  warn("Nenhum serviço selecionado.");
2642
2785
  return;
2643
2786
  }
2644
- if (!(isInteractive() ? await promptConfirm(`Deletar serviço ${chalk.bold(serviceToDelete.name)} (${serviceToDelete.type})? Esta ação é irreversível.`, false) : c.options.yes === true)) {
2787
+ requireMcpConfirmation(c.options.userConfirmation, `deletar serviço "${serviceToDelete.name}"`);
2788
+ if (!(isInteractive() ? await promptConfirm(`Deletar serviço ${chalk.bold(serviceToDelete.name)} (${serviceToDelete.type})? Esta ação é irreversível.`, false) : hasValidNonInteractiveConfirmation(c.options.userConfirmation, c.options.yes))) {
2645
2789
  info("Operação cancelada.");
2646
2790
  return;
2647
2791
  }
@@ -2650,6 +2794,729 @@ servicesGroup.command("delete", {
2650
2794
  fn: () => client.services.delete({ serviceId: serviceToDelete.id })
2651
2795
  });
2652
2796
  success(`Serviço "${serviceToDelete.name}" deletado com sucesso!`);
2797
+ return {
2798
+ deleted: true,
2799
+ name: serviceToDelete.name,
2800
+ type: serviceToDelete.type
2801
+ };
2802
+ }
2803
+ });
2804
+
2805
+ //#endregion
2806
+ //#region src/lib/sparkline.ts
2807
+ /**
2808
+ * Unicode sparkline renderer for terminal metrics display.
2809
+ * Uses 8-level block characters: ▁▂▃▄▅▆▇█
2810
+ */
2811
+ const BLOCKS = [
2812
+ "▁",
2813
+ "▂",
2814
+ "▃",
2815
+ "▄",
2816
+ "▅",
2817
+ "▆",
2818
+ "▇",
2819
+ "█"
2820
+ ];
2821
+ /**
2822
+ * Render a series of numeric values as a Unicode sparkline string.
2823
+ * Empty or all-zero arrays return a flat line.
2824
+ */
2825
+ function sparkline(values) {
2826
+ if (values.length === 0) return "";
2827
+ let min = values[0];
2828
+ let max = values[0];
2829
+ for (const v of values) {
2830
+ if (v < min) min = v;
2831
+ if (v > max) max = v;
2832
+ }
2833
+ const range = max - min;
2834
+ if (range === 0) return BLOCKS[0].repeat(values.length);
2835
+ return values.map((v) => {
2836
+ const normalized = (v - min) / range;
2837
+ return BLOCKS[Math.min(Math.floor(normalized * BLOCKS.length), BLOCKS.length - 1)];
2838
+ }).join("");
2839
+ }
2840
+ /**
2841
+ * Format a number to a compact human-readable string.
2842
+ * Examples: 1234 → "1.2k", 0.0035 → "3.5ms"
2843
+ */
2844
+ function formatCompact(value, unit) {
2845
+ if (unit === "ms") {
2846
+ if (value < 1) return `${(value * 1e3).toFixed(0)}µs`;
2847
+ if (value < 1e3) return `${value.toFixed(0)}ms`;
2848
+ return `${(value / 1e3).toFixed(1)}s`;
2849
+ }
2850
+ if (unit === "s") {
2851
+ if (value < .001) return `${(value * 1e6).toFixed(0)}µs`;
2852
+ if (value < 1) return `${(value * 1e3).toFixed(0)}ms`;
2853
+ return `${value.toFixed(2)}s`;
2854
+ }
2855
+ if (unit === "bytes") {
2856
+ if (value < 1024) return `${value.toFixed(0)}B`;
2857
+ if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)}KB`;
2858
+ if (value < 1024 * 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(1)}MB`;
2859
+ return `${(value / (1024 * 1024 * 1024)).toFixed(1)}GB`;
2860
+ }
2861
+ if (value === 0) return "0";
2862
+ if (Math.abs(value) < .01) return value.toExponential(1);
2863
+ if (Math.abs(value) < 1) return value.toFixed(2);
2864
+ if (Math.abs(value) < 1e3) return value.toFixed(1);
2865
+ if (Math.abs(value) < 1e6) return `${(value / 1e3).toFixed(1)}k`;
2866
+ return `${(value / 1e6).toFixed(1)}M`;
2867
+ }
2868
+
2869
+ //#endregion
2870
+ //#region src/commands/metrics.ts
2871
+ function statusColor(code) {
2872
+ if (code.startsWith("2")) return chalk.green;
2873
+ if (code.startsWith("3")) return chalk.cyan;
2874
+ if (code.startsWith("4")) return chalk.yellow;
2875
+ if (code.startsWith("5")) return chalk.red;
2876
+ return chalk.dim;
2877
+ }
2878
+ function rateColor(errorRate) {
2879
+ if (errorRate === 0) return chalk.green;
2880
+ if (errorRate < 1) return chalk.yellow;
2881
+ return chalk.red;
2882
+ }
2883
+ function seriesValues(points) {
2884
+ return points.map((p) => p.value);
2885
+ }
2886
+ function labeledValues(series) {
2887
+ return series.data.map((p) => p.value);
2888
+ }
2889
+ function latestValue(values) {
2890
+ return values[values.length - 1] ?? 0;
2891
+ }
2892
+ function printSparkline(label, values, suffix, width) {
2893
+ const latest = latestValue(values);
2894
+ console.log(` ${label.padEnd(width)} ${sparkline(values)} ${chalk.bold(`${formatCompact(latest)}${suffix}`)}`);
2895
+ }
2896
+ const metricsGroup = Cli.create("metrics", { description: "Visualizar métricas dos serviços" });
2897
+ metricsGroup.command("show", {
2898
+ description: "Exibir métricas atuais do serviço",
2899
+ middleware: [requireAuth],
2900
+ options: z.object({ service: z.string().optional().describe("Filtrar por serviço") }),
2901
+ output: z.object({
2902
+ requestRate: z.number(),
2903
+ errorRate: z.number(),
2904
+ latencyP95: z.number(),
2905
+ statusCodes: z.record(z.string(), z.number()),
2906
+ grafanaDashboardUrl: z.string().nullable()
2907
+ }),
2908
+ async run(c) {
2909
+ const serviceId = await resolveServiceId(c.options.service);
2910
+ const client = await getClient();
2911
+ const metrics = await withSpinner({
2912
+ text: "Carregando métricas...",
2913
+ fn: () => client.metrics.getServiceMetrics({ serviceId })
2914
+ });
2915
+ if (process.stdout.isTTY) {
2916
+ console.log();
2917
+ console.log(chalk.bold(" Métricas do Serviço"));
2918
+ console.log();
2919
+ console.log(` Requisições/min ${chalk.bold(formatCompact(metrics.requestRate))}`);
2920
+ console.log(` Erros/min ${rateColor(metrics.errorRate)(formatCompact(metrics.errorRate))}`);
2921
+ console.log(` Latência P95 ${chalk.bold(formatCompact(metrics.latencyP95, "s"))}`);
2922
+ const codes = Object.entries(metrics.statusCodes);
2923
+ if (codes.length > 0) {
2924
+ console.log();
2925
+ console.log(chalk.dim(" Status HTTP:"));
2926
+ for (const [code, count] of codes.sort(([a], [b]) => a.localeCompare(b))) console.log(` ${statusColor(code)(code)} ${formatCompact(count)}`);
2927
+ }
2928
+ if (metrics.grafanaDashboardUrl) {
2929
+ console.log();
2930
+ info(`Dashboard: ${chalk.underline(metrics.grafanaDashboardUrl)}`);
2931
+ }
2932
+ console.log();
2933
+ }
2934
+ return metrics;
2935
+ }
2936
+ });
2937
+ metricsGroup.command("range", {
2938
+ description: "Exibir métricas em intervalo de tempo com sparklines",
2939
+ middleware: [requireAuth],
2940
+ options: z.object({
2941
+ service: z.string().optional().describe("Filtrar por serviço"),
2942
+ range: z.enum([
2943
+ "1h",
2944
+ "6h",
2945
+ "24h",
2946
+ "7d"
2947
+ ]).default("1h").describe("Intervalo de tempo (1h, 6h, 24h, 7d)")
2948
+ }),
2949
+ alias: { range: "r" },
2950
+ async run(c) {
2951
+ const serviceId = await resolveServiceId(c.options.service);
2952
+ const client = await getClient();
2953
+ const data = await withSpinner({
2954
+ text: `Carregando métricas (${c.options.range})...`,
2955
+ fn: () => client.metrics.getServiceMetricsRange({
2956
+ serviceId,
2957
+ range: c.options.range
2958
+ })
2959
+ });
2960
+ if (process.stdout.isTTY) {
2961
+ const W = 20;
2962
+ const rangeLabel = chalk.dim(`(últimas ${c.options.range})`);
2963
+ console.log();
2964
+ console.log(chalk.bold(` Métricas ${rangeLabel}`));
2965
+ console.log();
2966
+ if (data.requestRate.length > 0) {
2967
+ const v = seriesValues(data.requestRate);
2968
+ console.log(` ${"Req/min".padEnd(W)} ${sparkline(v)} ${chalk.bold(formatCompact(latestValue(v)))}`);
2969
+ }
2970
+ if (data.latencyP95.length > 0) {
2971
+ const v = seriesValues(data.latencyP95);
2972
+ console.log(` ${"Latência P95".padEnd(W)} ${sparkline(v)} ${chalk.bold(formatCompact(latestValue(v), "s"))}`);
2973
+ }
2974
+ if (data.latencyP50.length > 0) {
2975
+ const v = seriesValues(data.latencyP50);
2976
+ console.log(` ${"Latência P50".padEnd(W)} ${sparkline(v)} ${chalk.bold(formatCompact(latestValue(v), "s"))}`);
2977
+ }
2978
+ for (const series of data.cpuUsage) printSparkline(`CPU (${series.label})`, labeledValues(series), "m", W);
2979
+ for (const series of data.memoryUsage) printSparkline(`Memória (${series.label})`, labeledValues(series), "Mi", W);
2980
+ if (data.networkIn.length > 0) printSparkline("Rede ↓", seriesValues(data.networkIn), "KB/s", W);
2981
+ if (data.networkOut.length > 0) printSparkline("Rede ↑", seriesValues(data.networkOut), "KB/s", W);
2982
+ console.log();
2983
+ }
2984
+ return data;
2985
+ }
2986
+ });
2987
+ const METRICS_HELP_SECTIONS = [
2988
+ {
2989
+ title: "Métricas disponíveis",
2990
+ content: ` metrics show Snapshot instantâneo do serviço
2991
+ metrics range Séries temporais com sparklines no terminal`
2992
+ },
2993
+ {
2994
+ title: "metrics show — campos retornados",
2995
+ content: ` requestRate Requisições por minuto (req/min)
2996
+ errorRate Erros 5xx por minuto
2997
+ latencyP95 Latência percentil 95 (segundos)
2998
+ statusCodes Mapa de código HTTP → contagem (ex: {"200": 1500, "404": 12})
2999
+ grafanaDashboardUrl URL do dashboard Grafana (pode ser null)`
3000
+ },
3001
+ {
3002
+ title: "metrics range — séries temporais retornadas",
3003
+ content: ` Tráfego (Traefik):
3004
+ requestRate [{timestamp, value}] — req/min ao longo do tempo
3005
+ latencyP50 [{timestamp, value}] — latência mediana
3006
+ latencyP95 [{timestamp, value}] — latência P95
3007
+ latencyP99 [{timestamp, value}] — latência P99
3008
+ statusCodes [{label, data}] — série por código HTTP
3009
+
3010
+ Recursos (por instância):
3011
+ cpuUsage [{label, data}] — millicores por instância
3012
+ memoryUsage [{label, data}] — MiB por instância
3013
+ networkIn [{timestamp, value}] — KB/s entrada
3014
+ networkOut [{timestamp, value}] — KB/s saída`
3015
+ },
3016
+ {
3017
+ title: "Intervalos de tempo (--range)",
3018
+ content: ` 1h Última hora (padrão)
3019
+ 6h Últimas 6 horas
3020
+ 24h Últimas 24 horas
3021
+ 7d Últimos 7 dias`
3022
+ },
3023
+ {
3024
+ title: "Tipos de dados",
3025
+ content: ` TimeSeriesPoint { timestamp: number, value: number }
3026
+ LabeledTimeSeries { label: string, data: TimeSeriesPoint[] }
3027
+ label identifica a instância (ex: "web-abc12")`
3028
+ },
3029
+ {
3030
+ title: "Exemplos de uso",
3031
+ content: ` veloz metrics show Métricas atuais (serviço padrão)
3032
+ veloz metrics show --service api Métricas da API
3033
+ veloz metrics range Sparklines última hora
3034
+ veloz metrics range --range 24h Sparklines últimas 24h
3035
+ veloz metrics range --range 7d --service web Sparklines 7 dias do frontend`
3036
+ },
3037
+ {
3038
+ title: "Workflow para agentes IA",
3039
+ content: ` 1. veloz metrics show → verificar saúde geral
3040
+ 2. Se errorRate > 0:
3041
+ veloz logs search "error" → investigar causa dos erros
3042
+ 3. veloz metrics range --range 1h → verificar tendência recente
3043
+ 4. Se latencyP95 alto:
3044
+ veloz metrics range --range 24h → comparar com padrão do dia`
3045
+ }
3046
+ ];
3047
+ metricsGroup.command("help", {
3048
+ description: "Referência de métricas disponíveis e seus campos",
3049
+ output: z.object({ sections: z.array(z.object({
3050
+ title: z.string(),
3051
+ content: z.string()
3052
+ })) }),
3053
+ async run() {
3054
+ if (process.stdout.isTTY) {
3055
+ console.log();
3056
+ console.log(chalk.bold(" Referência de Métricas"));
3057
+ console.log(chalk.dim(" Dados disponíveis via metrics show e metrics range"));
3058
+ console.log();
3059
+ for (const section of METRICS_HELP_SECTIONS) {
3060
+ console.log(chalk.bold.underline(` ${section.title}`));
3061
+ console.log();
3062
+ console.log(section.content);
3063
+ console.log();
3064
+ }
3065
+ console.log(chalk.dim(" Uso: veloz metrics show [--service NOME] | veloz metrics range [--range 1h|6h|24h|7d]"));
3066
+ console.log();
3067
+ }
3068
+ return { sections: METRICS_HELP_SECTIONS };
3069
+ }
3070
+ });
3071
+
3072
+ //#endregion
3073
+ //#region src/commands/logs.ts
3074
+ function formatTime(timestamp) {
3075
+ return chalk.dim(new Date(timestamp).toLocaleTimeString("pt-BR"));
3076
+ }
3077
+ async function streamFollow(services, maxNameLen, tailLines) {
3078
+ const client = await getClient();
3079
+ const showTags = services.length > 1;
3080
+ const collected = [];
3081
+ const streams = services.map(async ({ service, index }) => {
3082
+ const tag = showTags ? `${getServiceTag(service.name, maxNameLen, index)} ` : "";
3083
+ if (!service.id) {
3084
+ console.error(`${tag}${chalk.red("ID do serviço ausente para")} ${service.name}`);
3085
+ return;
3086
+ }
3087
+ try {
3088
+ const stream = await client.logs.streamRuntime({
3089
+ serviceId: service.id,
3090
+ tailLines: Math.ceil(tailLines / services.length)
3091
+ });
3092
+ for await (const entry of stream) {
3093
+ const line = `${tag}${formatTime(entry.timestamp)} ${entry.message}`;
3094
+ console.log(line);
3095
+ collected.push(`${tag}${entry.timestamp} ${entry.message}`);
3096
+ }
3097
+ } catch (err) {
3098
+ console.error(`${tag}${chalk.red("Erro ao conectar ao stream de logs:")} ${err instanceof Error ? err.message : String(err)}`);
3099
+ }
3100
+ });
3101
+ await Promise.allSettled(streams);
3102
+ return collected;
3103
+ }
3104
+ async function fetchRecent(services, maxNameLen, tailLines) {
3105
+ const client = await getClient();
3106
+ const showTags = services.length > 1;
3107
+ const allEntries = (await Promise.allSettled(services.map(async ({ service, index }) => {
3108
+ if (!service.id) return [];
3109
+ return (await client.logs.getRecent({
3110
+ serviceId: service.id,
3111
+ tailLines
3112
+ })).map((e) => ({
3113
+ ...e,
3114
+ serviceIndex: index,
3115
+ serviceName: service.name
3116
+ }));
3117
+ }))).filter((r) => r.status === "fulfilled").flatMap((r) => r.value).sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
3118
+ if (allEntries.length === 0) {
3119
+ info("Nenhum log encontrado.");
3120
+ return [];
3121
+ }
3122
+ console.log();
3123
+ for (const entry of allEntries) {
3124
+ const tag = showTags ? `${getServiceTag(entry.serviceName, maxNameLen, entry.serviceIndex)} ` : "";
3125
+ console.log(`${tag}${formatTime(entry.timestamp)} ${entry.message}`);
3126
+ }
3127
+ console.log();
3128
+ info(`Mostrando ${allEntries.length} linha(s). Use ${chalk.bold("--follow")} para acompanhar em tempo real.`);
3129
+ return allEntries.map((entry) => {
3130
+ return `${showTags ? `[${entry.serviceName}] ` : ""}${entry.timestamp} ${entry.message}`;
3131
+ });
3132
+ }
3133
+ const logsGroup = Cli.create("logs", { description: "Visualizar logs dos serviços" });
3134
+ logsGroup.command("show", {
3135
+ description: "Visualizar logs recentes ou acompanhar em tempo real",
3136
+ middleware: [requireAuth],
3137
+ options: z.object({
3138
+ follow: z.boolean().optional().describe("Acompanhar logs em tempo real"),
3139
+ tail: z.number().default(50).describe("Número de linhas recentes"),
3140
+ service: z.string().optional().describe("Filtrar por serviço")
3141
+ }),
3142
+ alias: {
3143
+ follow: "f",
3144
+ tail: "n"
3145
+ },
3146
+ async run(c) {
3147
+ const spin = spinner("Carregando logs...");
3148
+ const { services, maxNameLen } = resolveAllServices(c.options.service);
3149
+ const tailLines = c.options.tail;
3150
+ if (c.options.follow) {
3151
+ spin.text = services.length > 1 ? `Conectando a ${services.length} serviço(s)...` : "Conectando ao streaming de logs...";
3152
+ spin.stop();
3153
+ info("Streaming de logs ativo. Pressione Ctrl+C para sair.\n");
3154
+ const lines$1 = await streamFollow(services, maxNameLen, tailLines);
3155
+ return lines$1.length > 0 ? lines$1.join("\n") : "Nenhum log encontrado.";
3156
+ }
3157
+ spin.stop();
3158
+ const lines = await fetchRecent(services, maxNameLen, tailLines);
3159
+ return lines.length > 0 ? lines.join("\n") : "Nenhum log encontrado.";
3160
+ }
3161
+ });
3162
+ logsGroup.command("search", {
3163
+ description: "Pesquisar logs com consultas LogsQL",
3164
+ middleware: [requireAuth],
3165
+ args: z.object({ query: z.string().default("").describe("Consulta LogsQL (ex: error, ~\"regex\", field:value)") }),
3166
+ options: z.object({
3167
+ service: z.string().optional().describe("Filtrar por serviço"),
3168
+ start: z.string().optional().describe("Data/hora início (ISO 8601)"),
3169
+ end: z.string().optional().describe("Data/hora fim (ISO 8601)"),
3170
+ limit: z.number().default(100).describe("Número máximo de resultados"),
3171
+ deployment: z.string().optional().describe("Filtrar por ID do deploy")
3172
+ }),
3173
+ alias: { limit: "l" },
3174
+ output: z.array(z.object({
3175
+ timestamp: z.string(),
3176
+ pod: z.string(),
3177
+ message: z.string()
3178
+ })),
3179
+ async run(c) {
3180
+ const serviceId = await resolveServiceId(c.options.service);
3181
+ const entries = await (await getClient()).logs.search({
3182
+ serviceId,
3183
+ query: c.args.query,
3184
+ start: c.options.start,
3185
+ end: c.options.end,
3186
+ limit: c.options.limit,
3187
+ deploymentId: c.options.deployment
3188
+ });
3189
+ if (process.stdout.isTTY) if (entries.length === 0) info("Nenhum log encontrado para essa consulta.");
3190
+ else {
3191
+ console.log();
3192
+ for (const entry of entries) console.log(`${formatTime(entry.timestamp)} ${entry.message}`);
3193
+ console.log();
3194
+ info(`${entries.length} resultado(s) encontrado(s).`);
3195
+ }
3196
+ return entries;
3197
+ }
3198
+ });
3199
+ const QUERY_HELP_SECTIONS = [
3200
+ {
3201
+ title: "Filtros básicos",
3202
+ content: ` palavra Busca por palavra em qualquer campo
3203
+ "frase exata" Busca por frase exata
3204
+ exact("texto") Match exato (sem tokenização)
3205
+ prefix("iní") Busca por prefixo
3206
+ ~"regex" Expressão regular (RE2 syntax)`
3207
+ },
3208
+ {
3209
+ title: "Filtros por campo",
3210
+ content: ` campo:valor Match exato no campo
3211
+ campo:~"regex" Regex no campo
3212
+ campo:* Campo existe (não vazio)`
3213
+ },
3214
+ {
3215
+ title: "Filtros por tempo",
3216
+ content: ` _time:5m Últimos 5 minutos
3217
+ _time:1h Última hora
3218
+ _time:24h Últimas 24 horas
3219
+ _time:[início, fim] Intervalo ISO 8601`
3220
+ },
3221
+ {
3222
+ title: "Operadores lógicos",
3223
+ content: ` termo1 termo2 AND implícito (espaço)
3224
+ termo1 OR termo2 OR explícito
3225
+ NOT termo Negação
3226
+ -termo Negação (atalho)
3227
+ !termo Negação (atalho)`
3228
+ },
3229
+ {
3230
+ title: "Pipes",
3231
+ content: ` | stats count() by (campo) Agregar por campo
3232
+ | sort by (campo) desc Ordenar resultados
3233
+ | uniq by (campo) Valores únicos
3234
+ | top N by (campo) Top N por campo
3235
+ | fields campo1, campo2 Selecionar campos
3236
+ | limit N Limitar resultados
3237
+ | extract "padrão" Extrair campos com regex
3238
+ | unpack_json Expandir JSON embarcado
3239
+ | math expr Calcular expressões
3240
+ | format "template" Formatar saída
3241
+ | filter condição Filtrar pós-processamento
3242
+ | replace ("de", "para") Substituir texto
3243
+ | replace_regexp ("re", "sub") Substituir com regex`
3244
+ },
3245
+ {
3246
+ title: "Funções de agregação (stats)",
3247
+ content: ` count() Contagem
3248
+ sum(campo) Soma
3249
+ avg(campo) Média
3250
+ min(campo) Mínimo
3251
+ max(campo) Máximo
3252
+ median(campo) Mediana
3253
+ quantile(N, campo) Percentil
3254
+ count_uniq(campo) Valores únicos
3255
+ uniq_values(campo) Lista valores únicos
3256
+ rate(campo) Taxa por segundo`
3257
+ },
3258
+ {
3259
+ title: "Exemplos práticos",
3260
+ content: ` error Logs com "error"
3261
+ "connection refused" Frase exata
3262
+ error NOT timeout Erros exceto timeouts
3263
+ ~"status=[45]\\d{2}" Status 4xx/5xx via regex
3264
+ _time:1h error | stats count() by (level) Contagem de erros por nível na última hora
3265
+ _time:24h | top 10 by (message) Top 10 mensagens do dia
3266
+ error | sort by (_time) desc | limit 50 Últimos 50 erros`
3267
+ }
3268
+ ];
3269
+ logsGroup.command("query-help", {
3270
+ description: "Referência de sintaxe LogsQL para pesquisa de logs",
3271
+ output: z.object({ sections: z.array(z.object({
3272
+ title: z.string(),
3273
+ content: z.string()
3274
+ })) }),
3275
+ async run() {
3276
+ if (process.stdout.isTTY) {
3277
+ console.log();
3278
+ console.log(chalk.bold(" Referência LogsQL"));
3279
+ console.log(chalk.dim(" Sintaxe de consulta para pesquisa de logs"));
3280
+ console.log();
3281
+ for (const section of QUERY_HELP_SECTIONS) {
3282
+ console.log(chalk.bold.underline(` ${section.title}`));
3283
+ console.log();
3284
+ console.log(section.content);
3285
+ console.log();
3286
+ }
3287
+ console.log(chalk.dim(" Uso: veloz logs search <consulta> [--start ISO] [--end ISO] [--limit N]"));
3288
+ console.log();
3289
+ }
3290
+ return { sections: QUERY_HELP_SECTIONS };
3291
+ }
3292
+ });
3293
+
3294
+ //#endregion
3295
+ //#region src/commands/github.ts
3296
+ function openBrowser(url) {
3297
+ const os = platform();
3298
+ execFile(os === "darwin" ? "open" : os === "win32" ? "start" : "xdg-open", os === "win32" ? ["", url] : [url], (error) => {
3299
+ if (error) warn(`Não foi possível abrir o navegador: ${error.message}`);
3300
+ });
3301
+ }
3302
+ const githubGroup = Cli.create("github", { description: "Gerenciar integração com GitHub" });
3303
+ githubGroup.command("setup", {
3304
+ description: "Configurar GitHub App para deploy automático via webhook",
3305
+ middleware: [requireAuth],
3306
+ options: z.object({ project: z.string().optional().describe("ID do projeto a conectar") }),
3307
+ alias: { project: "p" },
3308
+ output: z.object({
3309
+ connected: z.boolean(),
3310
+ installationId: z.string().nullable(),
3311
+ projectId: z.string().nullable()
3312
+ }),
3313
+ async run(c) {
3314
+ const client = await getClient();
3315
+ const remote = getGitRemote();
3316
+ if (!remote) {
3317
+ warn("Não foi possível detectar o repositório GitHub. Certifique-se de estar em um repositório git com remote 'origin' apontando para o GitHub.");
3318
+ return {
3319
+ connected: false,
3320
+ installationId: null,
3321
+ projectId: null
3322
+ };
3323
+ }
3324
+ info(`Repositório detectado: ${chalk.bold(`${remote.owner}/${remote.repo}`)}`);
3325
+ let projectId = c.options.project;
3326
+ if (!projectId) {
3327
+ const config = loadConfig();
3328
+ if (config?.project?.id) {
3329
+ projectId = config.project.id;
3330
+ info(`Projeto encontrado no veloz.json: ${chalk.bold(config.project.name)}`);
3331
+ }
3332
+ }
3333
+ if (!projectId) {
3334
+ const existingProject = await client.projects.findByRepo({
3335
+ githubRepoOwner: remote.owner,
3336
+ githubRepoName: remote.repo
3337
+ });
3338
+ if (existingProject) {
3339
+ projectId = existingProject.id;
3340
+ info(`Projeto encontrado: ${chalk.bold(existingProject.name)}`);
3341
+ }
3342
+ }
3343
+ if (!projectId) {
3344
+ const projects = await client.projects.list();
3345
+ if (projects.length === 0) {
3346
+ warn("Nenhum projeto encontrado. Crie um projeto primeiro com 'veloz deploy'.");
3347
+ return {
3348
+ connected: false,
3349
+ installationId: null,
3350
+ projectId: null
3351
+ };
3352
+ }
3353
+ projectId = await promptSelect("Selecione o projeto para conectar:", projects.map((p) => ({
3354
+ label: `${p.name} (${p.slug})`,
3355
+ value: p.id
3356
+ })));
3357
+ }
3358
+ const s = spinner("Verificando instalação do GitHub App...");
3359
+ const installation = await client.github.getInstallation({ owner: remote.owner });
3360
+ s.stop();
3361
+ if (installation.error === "TOKEN_EXPIRED") {
3362
+ warn("Token do GitHub expirado. Faça login novamente no dashboard para reconectar sua conta GitHub.");
3363
+ return {
3364
+ connected: false,
3365
+ installationId: null,
3366
+ projectId
3367
+ };
3368
+ }
3369
+ if (installation.error === "RATE_LIMITED") {
3370
+ warn("Limite de requisições do GitHub atingido. Aguarde alguns minutos e tente novamente.");
3371
+ return {
3372
+ connected: false,
3373
+ installationId: null,
3374
+ projectId
3375
+ };
3376
+ }
3377
+ if (installation.installed && installation.installationId) {
3378
+ info(`GitHub App já instalado para ${chalk.bold(remote.owner)}.`);
3379
+ await client.projects.connectRepo({
3380
+ projectId,
3381
+ githubRepoOwner: remote.owner,
3382
+ githubRepoName: remote.repo,
3383
+ githubInstallationId: installation.installationId
3384
+ });
3385
+ success(`Repositório ${remote.owner}/${remote.repo} conectado! Pushes para o branch configurado vão iniciar deploys automaticamente.`);
3386
+ return {
3387
+ connected: true,
3388
+ installationId: installation.installationId,
3389
+ projectId
3390
+ };
3391
+ }
3392
+ if (!installation.installUrl) {
3393
+ warn("GitHub App não configurado no servidor. Contate o administrador da plataforma para configurar o GITHUB_APP_SLUG.");
3394
+ return {
3395
+ connected: false,
3396
+ installationId: null,
3397
+ projectId
3398
+ };
3399
+ }
3400
+ console.log();
3401
+ info(`O GitHub App precisa ser instalado na conta/organização ${chalk.bold(remote.owner)}.`);
3402
+ console.log();
3403
+ if (!await promptConfirm("Abrir o navegador para instalar o GitHub App?")) {
3404
+ console.log();
3405
+ console.log(chalk.dim(` Instale manualmente em: ${installation.installUrl}`));
3406
+ console.log(chalk.dim(" Depois execute 'veloz github setup' novamente."));
3407
+ return {
3408
+ connected: false,
3409
+ installationId: null,
3410
+ projectId
3411
+ };
3412
+ }
3413
+ openBrowser(installation.installUrl);
3414
+ console.log();
3415
+ info("Aguardando instalação do GitHub App...");
3416
+ console.log(chalk.dim(" Instale o app no navegador e volte aqui."));
3417
+ console.log();
3418
+ const pollSpinner = spinner("Aguardando instalação...");
3419
+ const maxAttempts = 60;
3420
+ const pollInterval = 5e3;
3421
+ let installationId = null;
3422
+ for (let i = 0; i < maxAttempts; i++) {
3423
+ await new Promise((r) => {
3424
+ setTimeout(r, pollInterval);
3425
+ });
3426
+ const check = await client.github.getInstallation({ owner: remote.owner });
3427
+ if (check.installed && check.installationId) {
3428
+ installationId = check.installationId;
3429
+ break;
3430
+ }
3431
+ if (check.error === "TOKEN_EXPIRED") {
3432
+ pollSpinner.fail("Token do GitHub expirou durante a espera.");
3433
+ return {
3434
+ connected: false,
3435
+ installationId: null,
3436
+ projectId
3437
+ };
3438
+ }
3439
+ }
3440
+ pollSpinner.stop();
3441
+ if (!installationId) {
3442
+ warn("Tempo esgotado aguardando instalação. Instale o GitHub App e execute 'veloz github setup' novamente.");
3443
+ return {
3444
+ connected: false,
3445
+ installationId: null,
3446
+ projectId
3447
+ };
3448
+ }
3449
+ await client.projects.connectRepo({
3450
+ projectId,
3451
+ githubRepoOwner: remote.owner,
3452
+ githubRepoName: remote.repo,
3453
+ githubInstallationId: installationId
3454
+ });
3455
+ success(`GitHub App instalado e repositório ${remote.owner}/${remote.repo} conectado! Pushes para o branch configurado vão iniciar deploys automaticamente.`);
3456
+ return {
3457
+ connected: true,
3458
+ installationId,
3459
+ projectId
3460
+ };
3461
+ }
3462
+ });
3463
+ githubGroup.command("status", {
3464
+ description: "Verificar status da integração GitHub do projeto",
3465
+ middleware: [requireAuth],
3466
+ output: z.object({
3467
+ connected: z.boolean(),
3468
+ owner: z.string().nullable(),
3469
+ repo: z.string().nullable(),
3470
+ installationId: z.string().nullable()
3471
+ }),
3472
+ async run() {
3473
+ const config = loadConfig();
3474
+ if (!config?.project?.id) {
3475
+ warn("Nenhum projeto encontrado. Execute 'veloz link' ou 'veloz deploy' primeiro.");
3476
+ return {
3477
+ connected: false,
3478
+ owner: null,
3479
+ repo: null,
3480
+ installationId: null
3481
+ };
3482
+ }
3483
+ const project = (await (await getClient()).projects.list()).find((p) => p.id === config.project.id);
3484
+ if (!project) {
3485
+ warn("Projeto não encontrado na plataforma.");
3486
+ return {
3487
+ connected: false,
3488
+ owner: null,
3489
+ repo: null,
3490
+ installationId: null
3491
+ };
3492
+ }
3493
+ if (!project.githubRepoOwner || !project.githubRepoName) {
3494
+ info("Repositório GitHub não conectado.");
3495
+ console.log(chalk.dim(" Execute 'veloz github setup' para conectar."));
3496
+ return {
3497
+ connected: false,
3498
+ owner: null,
3499
+ repo: null,
3500
+ installationId: null
3501
+ };
3502
+ }
3503
+ const hasInstallation = !!project.githubInstallationId;
3504
+ console.log();
3505
+ info(`Repositório: ${chalk.bold(`${project.githubRepoOwner}/${project.githubRepoName}`)}`);
3506
+ if (hasInstallation) {
3507
+ console.log(chalk.green(" GitHub App: instalado"));
3508
+ console.log(chalk.dim(` Installation ID: ${project.githubInstallationId}`));
3509
+ console.log(chalk.green(" Deploys automáticos: ativados"));
3510
+ } else {
3511
+ console.log(chalk.yellow(" GitHub App: não instalado"));
3512
+ console.log(chalk.dim(" Execute 'veloz github setup' para ativar deploys automáticos."));
3513
+ }
3514
+ return {
3515
+ connected: true,
3516
+ owner: project.githubRepoOwner,
3517
+ repo: project.githubRepoName,
3518
+ installationId: project.githubInstallationId
3519
+ };
2653
3520
  }
2654
3521
  });
2655
3522
 
@@ -3702,7 +4569,7 @@ const LOGO_LINES = [
3702
4569
  ];
3703
4570
  const BRAND_COLOR = "#FF4D00";
3704
4571
  function getVersion() {
3705
- return "0.0.0-beta.18";
4572
+ return "0.0.0-beta.19";
3706
4573
  }
3707
4574
  function printBanner(subtitle) {
3708
4575
  const version = getVersion();
@@ -4672,7 +5539,7 @@ async function autoUpdate() {
4672
5539
  if (process.env.VELOZ_MCP === "true") return;
4673
5540
  const pm = detectPackageManager();
4674
5541
  if (!pm) return;
4675
- const currentVersion = "0.0.0-beta.18";
5542
+ const currentVersion = "0.0.0-beta.19";
4676
5543
  const latestVersion = await fetchLatestVersion();
4677
5544
  if (!latestVersion || latestVersion === currentVersion) return;
4678
5545
  const installCmd = getInstallCommand(pm, latestVersion);
@@ -4799,7 +5666,7 @@ async function provisionDatabases(config, opts) {
4799
5666
  async function updateDatabaseResources(client, key, dbConfig, existing, serviceId, isLive) {
4800
5667
  const desiredSize = dbConfig.size;
4801
5668
  if (!desiredSize) return;
4802
- if ((existing.size ?? resolveDatabaseSize(existing.cpuLimit, existing.memoryLimit)) === desiredSize) return;
5669
+ if ((existing.size ?? resolveDatabaseSize(existing.cpuLimit, existing.memoryLimit, existing.engine)) === desiredSize) return;
4803
5670
  if (!isLive) {
4804
5671
  warn(`Banco de dados "${key}" não está LIVE — não é possível alterar recursos agora.`);
4805
5672
  return;
@@ -5987,83 +6854,6 @@ async function cliDeployFlow(opts) {
5987
6854
  await triggerDeploy(serviceId);
5988
6855
  }
5989
6856
 
5990
- //#endregion
5991
- //#region src/commands/logs.ts
5992
- function formatTime(timestamp) {
5993
- return chalk.dim(new Date(timestamp).toLocaleTimeString("pt-BR"));
5994
- }
5995
- async function streamFollow(services, maxNameLen, tailLines) {
5996
- const client = await getClient();
5997
- const showTags = services.length > 1;
5998
- const streams = services.map(async ({ service, index }) => {
5999
- const tag = showTags ? `${getServiceTag(service.name, maxNameLen, index)} ` : "";
6000
- try {
6001
- const stream = await client.logs.streamRuntime({
6002
- serviceId: service.id,
6003
- tailLines: Math.ceil(tailLines / services.length)
6004
- });
6005
- for await (const entry of stream) console.log(`${tag}${formatTime(entry.timestamp)} ${entry.message}`);
6006
- } catch {
6007
- console.log(`${tag}${chalk.red("Erro ao conectar ao stream de logs")}`);
6008
- }
6009
- });
6010
- await Promise.allSettled(streams);
6011
- }
6012
- async function fetchRecent(services, maxNameLen, tailLines) {
6013
- const client = await getClient();
6014
- const showTags = services.length > 1;
6015
- const allEntries = (await Promise.allSettled(services.map(async ({ service, index }) => {
6016
- return (await client.logs.getRecent({
6017
- serviceId: service.id,
6018
- tailLines
6019
- })).map((e) => ({
6020
- ...e,
6021
- serviceIndex: index,
6022
- serviceName: service.name
6023
- }));
6024
- }))).filter((r) => r.status === "fulfilled").flatMap((r) => r.value).sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
6025
- if (allEntries.length === 0) {
6026
- info("Nenhum log encontrado.");
6027
- return;
6028
- }
6029
- console.log();
6030
- for (const entry of allEntries) {
6031
- const tag = showTags ? `${getServiceTag(entry.serviceName, maxNameLen, entry.serviceIndex)} ` : "";
6032
- console.log(`${tag}${formatTime(entry.timestamp)} ${entry.message}`);
6033
- }
6034
- console.log();
6035
- info(`Mostrando ${allEntries.length} linha(s). Use ${chalk.bold("--follow")} para acompanhar em tempo real.`);
6036
- }
6037
- function registerLogs(cli$1) {
6038
- cli$1.command("logs", {
6039
- description: "Visualizar logs dos serviços",
6040
- middleware: [requireAuth],
6041
- options: z.object({
6042
- follow: z.boolean().optional().describe("Acompanhar logs em tempo real"),
6043
- tail: z.number().default(50).describe("Número de linhas recentes"),
6044
- service: z.string().optional().describe("Filtrar por serviço")
6045
- }),
6046
- alias: {
6047
- follow: "f",
6048
- tail: "n"
6049
- },
6050
- async run(c) {
6051
- const spin = spinner("Carregando logs...");
6052
- const { services, maxNameLen } = resolveAllServices(c.options.service);
6053
- const tailLines = c.options.tail;
6054
- if (c.options.follow) {
6055
- spin.text = services.length > 1 ? `Conectando a ${services.length} serviço(s)...` : "Conectando ao streaming de logs...";
6056
- spin.stop();
6057
- info("Streaming de logs ativo. Pressione Ctrl+C para sair.\n");
6058
- await streamFollow(services, maxNameLen, tailLines);
6059
- } else {
6060
- spin.stop();
6061
- await fetchRecent(services, maxNameLen, tailLines);
6062
- }
6063
- }
6064
- });
6065
- }
6066
-
6067
6857
  //#endregion
6068
6858
  //#region src/commands/use.ts
6069
6859
  function registerUse(cli$1) {
@@ -6326,7 +7116,7 @@ async function pruneRemovedEntries(config, existingConfig, services, databases)
6326
7116
  //#region src/index.ts
6327
7117
  if (process.argv.includes("--mcp")) process.env.VELOZ_MCP = "true";
6328
7118
  const cli = Cli.create("veloz", {
6329
- version: "0.0.0-beta.18",
7119
+ version: "0.0.0-beta.19",
6330
7120
  description: "CLI da plataforma Veloz — deploy rápido para o Brasil",
6331
7121
  env: z.object({ VELOZ_ENV: z.string().optional().describe("Ambiente alvo (ex: preview, staging)") })
6332
7122
  });
@@ -6390,10 +7180,12 @@ cli.command(apikeyGroup);
6390
7180
  cli.command(dbGroup);
6391
7181
  cli.command(templateGroup);
6392
7182
  cli.command(servicesGroup);
7183
+ cli.command(metricsGroup);
7184
+ cli.command(logsGroup);
7185
+ cli.command(githubGroup);
6393
7186
  registerLogin(cli);
6394
7187
  registerLink(cli);
6395
7188
  registerDeploy(cli);
6396
- registerLogs(cli);
6397
7189
  registerUse(cli);
6398
7190
  registerWhoami(cli);
6399
7191
  registerPull(cli);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onveloz",
3
- "version": "0.0.0-beta.18",
3
+ "version": "0.0.0-beta.19",
4
4
  "description": "CLI da plataforma Veloz — deploy rápido para o Brasil",
5
5
  "keywords": [
6
6
  "brasil",
@@ -46,7 +46,7 @@
46
46
  "@veloz/config": "workspace:*",
47
47
  "bumpp": "^10.4.1",
48
48
  "tsdown": "^0.16.5",
49
- "tsx": "^4.19.2",
49
+ "tsx": "catalog:",
50
50
  "typescript": "catalog:",
51
51
  "vitest": "catalog:"
52
52
  },