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.
- package/dist/index.mjs +896 -104
- 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
|
-
|
|
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
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
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({
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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": "
|
|
49
|
+
"tsx": "catalog:",
|
|
50
50
|
"typescript": "catalog:",
|
|
51
51
|
"vitest": "catalog:"
|
|
52
52
|
},
|