onveloz 0.0.0-beta.39 → 0.0.0-beta.40
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 +609 -82
- package/package.json +5 -6
package/dist/index.mjs
CHANGED
|
@@ -34,7 +34,7 @@ const SERVICE_TYPES = [
|
|
|
34
34
|
"WORKER",
|
|
35
35
|
"DATABASE"
|
|
36
36
|
];
|
|
37
|
-
const BUILD_TIMEOUT_MS =
|
|
37
|
+
const BUILD_TIMEOUT_MS = 2700 * 1e3;
|
|
38
38
|
const DEPLOY_TIMEOUT_MS = 300 * 1e3;
|
|
39
39
|
const DATABASE_PROVISION_TIMEOUT_MS = 300 * 1e3;
|
|
40
40
|
const DATABASE_WAITING_ON_PROVIDER_AFTER_MS = 60 * 1e3;
|
|
@@ -307,6 +307,18 @@ const PROJECT_FLAG_DEFINITIONS = {
|
|
|
307
307
|
}
|
|
308
308
|
};
|
|
309
309
|
const PROJECT_FLAG_KEYS = Object.keys(PROJECT_FLAG_DEFINITIONS);
|
|
310
|
+
/** All known boolean org flags. Used for validation and UI rendering. */
|
|
311
|
+
const ORGANIZATION_FLAG_DEFINITIONS = {
|
|
312
|
+
browserApi: {
|
|
313
|
+
label: "Browser API",
|
|
314
|
+
description: "Concede a esta organização acesso à API de navegador (sessões de navegador gerenciadas). Sem a flag, o recurso fica restrito a administradores da plataforma."
|
|
315
|
+
},
|
|
316
|
+
managedAuth: {
|
|
317
|
+
label: "Autenticação Gerenciada",
|
|
318
|
+
description: "Concede a esta organização acesso à Autenticação Gerenciada (login hospedado pela Veloz para os apps do cliente). Sem a flag, o recurso fica restrito a administradores da plataforma."
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
const ORGANIZATION_FLAG_KEYS = Object.keys(ORGANIZATION_FLAG_DEFINITIONS);
|
|
310
322
|
/**
|
|
311
323
|
* Allowlist of user-configurable PostgreSQL parameters with validation bounds.
|
|
312
324
|
* PGTune-relevant params + commonly tuned settings.
|
|
@@ -642,7 +654,7 @@ const ServiceConfigSchema = z$1.object({
|
|
|
642
654
|
const ProjectConfigSchema = z$1.object({
|
|
643
655
|
id: z$1.string().optional(),
|
|
644
656
|
name: z$1.string().optional(),
|
|
645
|
-
slug: z$1.string().regex(/^[a-z0-9-]
|
|
657
|
+
slug: z$1.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).optional()
|
|
646
658
|
});
|
|
647
659
|
const ServiceDefaultsSchema = z$1.object({
|
|
648
660
|
type: ServiceTypeSchema.optional(),
|
|
@@ -886,6 +898,44 @@ function getGitBranch() {
|
|
|
886
898
|
}
|
|
887
899
|
}
|
|
888
900
|
|
|
901
|
+
//#endregion
|
|
902
|
+
//#region src/lib/mcp.ts
|
|
903
|
+
const MCP_SERVER_INSTRUCTIONS = [
|
|
904
|
+
"Use Veloz tools for platform operations and start with read-only discovery tools.",
|
|
905
|
+
"Before calling a tool annotated as destructive, obtain explicit confirmation for the exact action.",
|
|
906
|
+
"When a destructive tool accepts userConfirmation, pass the user's confirmation text in that field.",
|
|
907
|
+
"Deploy is non-blocking: after deploy, poll builds_show until the status is terminal, then use builds_logs when the build fails.",
|
|
908
|
+
"Treat credentials, API keys, and environment variables as secrets and never expose them unnecessarily."
|
|
909
|
+
].join(" ");
|
|
910
|
+
const MCP_READ_ONLY = { annotations: {
|
|
911
|
+
readOnlyHint: true,
|
|
912
|
+
destructiveHint: false,
|
|
913
|
+
openWorldHint: true
|
|
914
|
+
} };
|
|
915
|
+
const MCP_DESTRUCTIVE = {
|
|
916
|
+
annotations: {
|
|
917
|
+
readOnlyHint: false,
|
|
918
|
+
destructiveHint: true,
|
|
919
|
+
idempotentHint: false,
|
|
920
|
+
openWorldHint: true
|
|
921
|
+
},
|
|
922
|
+
instructions: "This tool can cause an irreversible or externally visible change. Call it only after the user explicitly confirms the exact action."
|
|
923
|
+
};
|
|
924
|
+
const MCP_CONFIRMATION_REQUIRED = {
|
|
925
|
+
...MCP_DESTRUCTIVE,
|
|
926
|
+
instructions: "This tool requires explicit confirmation for the exact action. Pass the user's confirmation text in userConfirmation."
|
|
927
|
+
};
|
|
928
|
+
const MCP_DEPLOY = {
|
|
929
|
+
description: "Start a Veloz deployment. In MCP mode this returns as soon as the build is queued and does not wait for completion.",
|
|
930
|
+
annotations: {
|
|
931
|
+
readOnlyHint: false,
|
|
932
|
+
destructiveHint: false,
|
|
933
|
+
idempotentHint: false,
|
|
934
|
+
openWorldHint: true
|
|
935
|
+
},
|
|
936
|
+
instructions: "Read deploymentId from the result, poll builds_show until status is LIVE, BUILD_FAILED, DEPLOY_FAILED, FAILED, or CANCELLED, and call builds_logs when the build fails."
|
|
937
|
+
};
|
|
938
|
+
|
|
889
939
|
//#endregion
|
|
890
940
|
//#region ../../packages/api/src/client.ts
|
|
891
941
|
function createClient(baseUrl, headers) {
|
|
@@ -907,6 +957,16 @@ function createClient(baseUrl, headers) {
|
|
|
907
957
|
function isMcpMode() {
|
|
908
958
|
return process.env.VELOZ_MCP === "true";
|
|
909
959
|
}
|
|
960
|
+
/**
|
|
961
|
+
* True when the CLI is being driven by an AI coding agent rather than a human.
|
|
962
|
+
* Covers Claude Code (`CLAUDECODE` / `CLAUDE_CODE_ENTRYPOINT`), generic agent
|
|
963
|
+
* runners (`AI_AGENT`), and MCP mode. Used to skip interactive / nested-AI
|
|
964
|
+
* flows like the `veloz init` wizard — an agent should drive `veloz deploy`
|
|
965
|
+
* directly instead of spawning a second agent inside the CLI.
|
|
966
|
+
*/
|
|
967
|
+
function isAiAgent() {
|
|
968
|
+
return isMcpMode() || Boolean(process.env.CLAUDECODE) || Boolean(process.env.CLAUDE_CODE_ENTRYPOINT) || Boolean(process.env.AI_AGENT);
|
|
969
|
+
}
|
|
910
970
|
/** Write a line to the safe output stream (stderr in MCP mode, stdout otherwise). */
|
|
911
971
|
function log(message) {
|
|
912
972
|
if (isMcpMode()) process.stderr.write(`${message}\n`);
|
|
@@ -1154,6 +1214,7 @@ async function performLogin(apiUrl) {
|
|
|
1154
1214
|
function registerLogin(cli$1) {
|
|
1155
1215
|
cli$1.command("login", {
|
|
1156
1216
|
description: "Autenticar na plataforma Veloz",
|
|
1217
|
+
mcp: false,
|
|
1157
1218
|
options: z.object({
|
|
1158
1219
|
apiUrl: z.string().optional().describe("URL da API Veloz"),
|
|
1159
1220
|
apiKey: z.string().optional().describe("Chave de API (para CI/automação)")
|
|
@@ -1185,6 +1246,7 @@ function registerLogin(cli$1) {
|
|
|
1185
1246
|
});
|
|
1186
1247
|
cli$1.command("logout", {
|
|
1187
1248
|
description: "Encerrar sessão na plataforma Veloz",
|
|
1249
|
+
mcp: false,
|
|
1188
1250
|
output: z.object({ loggedOut: z.boolean() }),
|
|
1189
1251
|
run() {
|
|
1190
1252
|
if (!isAuthenticated()) {
|
|
@@ -1273,7 +1335,7 @@ async function requireAuth(options) {
|
|
|
1273
1335
|
|
|
1274
1336
|
//#endregion
|
|
1275
1337
|
//#region src/lib/client.ts
|
|
1276
|
-
const CLI_VERSION = "0.0.0-beta.
|
|
1338
|
+
const CLI_VERSION = "0.0.0-beta.40";
|
|
1277
1339
|
const USER_AGENT = `veloz-cli/${CLI_VERSION}`;
|
|
1278
1340
|
/**
|
|
1279
1341
|
* Client source — "cli" by default; the init wizard sets this to "cli-wizard"
|
|
@@ -1353,6 +1415,7 @@ const requireAuth$1 = middleware(async (c, next) => {
|
|
|
1353
1415
|
const projectsGroup = Cli.create("projects", { description: "Gerenciar projetos" });
|
|
1354
1416
|
projectsGroup.command("list", {
|
|
1355
1417
|
description: "Listar todos os projetos",
|
|
1418
|
+
mcp: MCP_READ_ONLY,
|
|
1356
1419
|
middleware: [requireAuth$1],
|
|
1357
1420
|
output: z.object({ items: z.array(z.object({
|
|
1358
1421
|
id: z.string(),
|
|
@@ -1386,6 +1449,7 @@ projectsGroup.command("list", {
|
|
|
1386
1449
|
const orgsGroup = Cli.create("orgs", { description: "Gerenciar workspaces (organizações) do usuário" });
|
|
1387
1450
|
orgsGroup.command("list", {
|
|
1388
1451
|
description: "Listar workspaces que você pertence",
|
|
1452
|
+
mcp: MCP_READ_ONLY,
|
|
1389
1453
|
middleware: [requireAuth$1],
|
|
1390
1454
|
output: z.object({ items: z.array(z.object({
|
|
1391
1455
|
id: z.string(),
|
|
@@ -1933,6 +1997,7 @@ async function resolveProjectId(projectFlag) {
|
|
|
1933
1997
|
const envGroup = Cli.create("env", { description: "Gerenciar variáveis de ambiente. Valores suportam interpolação no deploy: ${OUTRA_VAR} referencia outra variável do mesmo serviço (inclui vars auto-injetadas como DATABASE_URL/REDIS_URL/PORT/NODE_ENV); $${LITERAL} preserva um ${...} literal; ${{servico.propriedade}} referencia outro serviço (ex: ${{postgres.url}})." });
|
|
1934
1998
|
envGroup.command("list", {
|
|
1935
1999
|
description: "Listar variáveis de ambiente",
|
|
2000
|
+
mcp: MCP_READ_ONLY,
|
|
1936
2001
|
middleware: [requireAuth$1],
|
|
1937
2002
|
options: z.object({
|
|
1938
2003
|
service: z.string().optional().describe("Filtrar por serviço (chave ou nome)"),
|
|
@@ -2040,6 +2105,7 @@ envGroup.command("set", {
|
|
|
2040
2105
|
});
|
|
2041
2106
|
envGroup.command("delete", {
|
|
2042
2107
|
description: "Remover variável de ambiente",
|
|
2108
|
+
mcp: MCP_CONFIRMATION_REQUIRED,
|
|
2043
2109
|
middleware: [requireAuth$1],
|
|
2044
2110
|
args: z.object({ chave: z.string().describe("Chave da variável de ambiente") }),
|
|
2045
2111
|
options: z.object({
|
|
@@ -2073,6 +2139,7 @@ envGroup.command("delete", {
|
|
|
2073
2139
|
});
|
|
2074
2140
|
envGroup.command("import", {
|
|
2075
2141
|
description: "Importar variáveis de ambiente de um arquivo .env ou colar diretamente. Valores com ${OUTRA_VAR} são salvos como referência e resolvidos no deploy (contra as outras vars do serviço, incluindo auto-injetadas). Use $${LITERAL} para preservar.",
|
|
2142
|
+
mcp: false,
|
|
2076
2143
|
middleware: [requireAuth$1],
|
|
2077
2144
|
args: z.object({ arquivo: z.string().optional().describe("Caminho do arquivo .env") }),
|
|
2078
2145
|
options: z.object({
|
|
@@ -2165,6 +2232,7 @@ envGroup.command("import", {
|
|
|
2165
2232
|
});
|
|
2166
2233
|
envGroup.command("export", {
|
|
2167
2234
|
description: "Exportar variáveis de ambiente para um arquivo .env",
|
|
2235
|
+
mcp: false,
|
|
2168
2236
|
middleware: [requireAuth$1],
|
|
2169
2237
|
args: z.object({ arquivo: z.string().optional().describe("Caminho do arquivo de saída") }),
|
|
2170
2238
|
options: z.object({
|
|
@@ -2235,6 +2303,7 @@ function printDnsRecords(records) {
|
|
|
2235
2303
|
const domainsGroup = Cli.create("domains", { description: "Gerenciar domínios personalizados" });
|
|
2236
2304
|
domainsGroup.command("list", {
|
|
2237
2305
|
description: "Listar domínios dos serviços",
|
|
2306
|
+
mcp: MCP_READ_ONLY,
|
|
2238
2307
|
middleware: [requireAuth$1],
|
|
2239
2308
|
options: z.object({
|
|
2240
2309
|
service: z.string().optional().describe("Filtrar por serviço (chave ou nome)"),
|
|
@@ -2362,12 +2431,13 @@ domainsGroup.command("verify", {
|
|
|
2362
2431
|
});
|
|
2363
2432
|
domainsGroup.command("search", {
|
|
2364
2433
|
description: "Buscar domínios disponíveis para registro",
|
|
2434
|
+
mcp: MCP_READ_ONLY,
|
|
2365
2435
|
middleware: [requireAuth$1],
|
|
2366
2436
|
args: z.object({ query: z.string().describe("Nome do domínio a buscar (ex: meusite)") }),
|
|
2367
|
-
output: z.array(z.object({
|
|
2437
|
+
output: z.object({ items: z.array(z.object({
|
|
2368
2438
|
domain: z.string(),
|
|
2369
2439
|
available: z.boolean()
|
|
2370
|
-
})),
|
|
2440
|
+
})) }),
|
|
2371
2441
|
async run(c) {
|
|
2372
2442
|
const client = await getClient();
|
|
2373
2443
|
const results = await withSpinner({
|
|
@@ -2376,17 +2446,18 @@ domainsGroup.command("search", {
|
|
|
2376
2446
|
});
|
|
2377
2447
|
if (results.length === 0) {
|
|
2378
2448
|
info("Nenhum resultado encontrado.");
|
|
2379
|
-
return [];
|
|
2449
|
+
return { items: [] };
|
|
2380
2450
|
}
|
|
2381
2451
|
for (const r of results) {
|
|
2382
2452
|
const status = r.available ? chalk.green("Disponível") : chalk.red("Indisponível");
|
|
2383
2453
|
console.log(` ${chalk.bold(r.domain.padEnd(30))} ${status}`);
|
|
2384
2454
|
}
|
|
2385
|
-
return results;
|
|
2455
|
+
return { items: results };
|
|
2386
2456
|
}
|
|
2387
2457
|
});
|
|
2388
2458
|
domainsGroup.command("buy", {
|
|
2389
2459
|
description: "Registrar um domínio",
|
|
2460
|
+
mcp: MCP_DESTRUCTIVE,
|
|
2390
2461
|
middleware: [requireAuth$1],
|
|
2391
2462
|
args: z.object({ dominio: z.string().describe("Domínio a registrar (ex: meusite.com)") }),
|
|
2392
2463
|
options: z.object({ service: z.string().optional().describe("Conectar ao serviço automaticamente") }),
|
|
@@ -2417,13 +2488,14 @@ domainsGroup.command("buy", {
|
|
|
2417
2488
|
});
|
|
2418
2489
|
domainsGroup.command("purchased", {
|
|
2419
2490
|
description: "Listar domínios registrados",
|
|
2491
|
+
mcp: MCP_READ_ONLY,
|
|
2420
2492
|
middleware: [requireAuth$1],
|
|
2421
|
-
output: z.array(z.object({
|
|
2493
|
+
output: z.object({ items: z.array(z.object({
|
|
2422
2494
|
id: z.string(),
|
|
2423
2495
|
domain: z.string(),
|
|
2424
2496
|
status: z.string(),
|
|
2425
2497
|
expiresAt: z.string().nullable()
|
|
2426
|
-
})),
|
|
2498
|
+
})) }),
|
|
2427
2499
|
async run() {
|
|
2428
2500
|
const client = await getClient();
|
|
2429
2501
|
const purchased = await withSpinner({
|
|
@@ -2432,9 +2504,9 @@ domainsGroup.command("purchased", {
|
|
|
2432
2504
|
});
|
|
2433
2505
|
if (purchased.length === 0) {
|
|
2434
2506
|
info("Nenhum domínio registrado.");
|
|
2435
|
-
return [];
|
|
2507
|
+
return { items: [] };
|
|
2436
2508
|
}
|
|
2437
|
-
const statusLabels$
|
|
2509
|
+
const statusLabels$2 = {
|
|
2438
2510
|
REGISTERING: chalk.yellow("Registrando"),
|
|
2439
2511
|
PROPAGATING: chalk.yellow("Propagando DNS"),
|
|
2440
2512
|
ACTIVE: chalk.green("Ativo"),
|
|
@@ -2442,20 +2514,21 @@ domainsGroup.command("purchased", {
|
|
|
2442
2514
|
EXPIRED: chalk.red("Expirado")
|
|
2443
2515
|
};
|
|
2444
2516
|
for (const d of purchased) {
|
|
2445
|
-
const status = statusLabels$
|
|
2517
|
+
const status = statusLabels$2[d.status] ?? d.status;
|
|
2446
2518
|
const expires = d.expiresAt ? new Date(d.expiresAt).toLocaleDateString("pt-BR") : "—";
|
|
2447
2519
|
console.log(` ${d.id} ${chalk.bold(d.domain.padEnd(30))} ${status} Expira: ${expires}`);
|
|
2448
2520
|
}
|
|
2449
|
-
return purchased.map((d) => ({
|
|
2521
|
+
return { items: purchased.map((d) => ({
|
|
2450
2522
|
id: d.id,
|
|
2451
2523
|
domain: d.domain,
|
|
2452
2524
|
status: d.status,
|
|
2453
2525
|
expiresAt: d.expiresAt ? new Date(d.expiresAt).toISOString() : null
|
|
2454
|
-
}));
|
|
2526
|
+
})) };
|
|
2455
2527
|
}
|
|
2456
2528
|
});
|
|
2457
2529
|
domainsGroup.command("delete", {
|
|
2458
2530
|
description: "Remover domínio",
|
|
2531
|
+
mcp: MCP_CONFIRMATION_REQUIRED,
|
|
2459
2532
|
middleware: [requireAuth$1],
|
|
2460
2533
|
args: z.object({ domainId: z.string().describe("ID do domínio") }),
|
|
2461
2534
|
options: z.object({ userConfirmation: z.string().optional() }),
|
|
@@ -2474,6 +2547,237 @@ domainsGroup.command("delete", {
|
|
|
2474
2547
|
}
|
|
2475
2548
|
});
|
|
2476
2549
|
|
|
2550
|
+
//#endregion
|
|
2551
|
+
//#region src/commands/email.ts
|
|
2552
|
+
const statusLabels$1 = {
|
|
2553
|
+
PENDING: chalk.yellow("Pendente"),
|
|
2554
|
+
VERIFYING: chalk.yellow("Verificando"),
|
|
2555
|
+
ACTIVE: chalk.green("Ativo"),
|
|
2556
|
+
FAILED: chalk.red("Falhou")
|
|
2557
|
+
};
|
|
2558
|
+
async function getEmailClient() {
|
|
2559
|
+
return (await getClient()).email;
|
|
2560
|
+
}
|
|
2561
|
+
function printDkimRecords(records) {
|
|
2562
|
+
if (records.length === 0) return;
|
|
2563
|
+
console.log();
|
|
2564
|
+
console.log(chalk.yellow.bold(" Registros DNS a publicar (DKIM):"));
|
|
2565
|
+
for (const r of records) console.log(` ${chalk.cyan(r.type.padEnd(5))} ${chalk.white(r.name)} → ${chalk.white(r.value)}`);
|
|
2566
|
+
console.log();
|
|
2567
|
+
console.log(chalk.dim(" Após publicar, rode `veloz email verify <id>` para confirmar a verificação."));
|
|
2568
|
+
console.log();
|
|
2569
|
+
}
|
|
2570
|
+
const emailGroup = Cli.create("email", {
|
|
2571
|
+
description: "Gerenciar envio de e-mail transacional",
|
|
2572
|
+
aliases: ["emails"]
|
|
2573
|
+
});
|
|
2574
|
+
emailGroup.command("list", {
|
|
2575
|
+
description: "Listar domínios remetentes",
|
|
2576
|
+
mcp: MCP_READ_ONLY,
|
|
2577
|
+
middleware: [requireAuth$1],
|
|
2578
|
+
output: z.object({ items: z.array(z.object({
|
|
2579
|
+
id: z.string(),
|
|
2580
|
+
domain: z.string(),
|
|
2581
|
+
status: z.string(),
|
|
2582
|
+
emailsSentThisPeriod: z.number()
|
|
2583
|
+
})) }),
|
|
2584
|
+
async run() {
|
|
2585
|
+
const spin = spinner("Carregando domínios de e-mail...");
|
|
2586
|
+
const domains = await (await getEmailClient()).listDomains();
|
|
2587
|
+
spin.stop();
|
|
2588
|
+
if (domains.length === 0) {
|
|
2589
|
+
info("Nenhum domínio remetente configurado. Use `veloz email add <domínio>`.");
|
|
2590
|
+
return { items: [] };
|
|
2591
|
+
}
|
|
2592
|
+
for (const d of domains) console.log(` ${d.id} ${chalk.bold(d.domain.padEnd(30))} ${statusLabels$1[d.status] ?? d.status} ${chalk.dim(`${d.emailsSentThisPeriod} enviados`)}`);
|
|
2593
|
+
return { items: domains.map((d) => ({
|
|
2594
|
+
id: d.id,
|
|
2595
|
+
domain: d.domain,
|
|
2596
|
+
status: d.status,
|
|
2597
|
+
emailsSentThisPeriod: d.emailsSentThisPeriod
|
|
2598
|
+
})) };
|
|
2599
|
+
}
|
|
2600
|
+
});
|
|
2601
|
+
emailGroup.command("add", {
|
|
2602
|
+
description: "Registrar um domínio remetente",
|
|
2603
|
+
middleware: [requireAuth$1],
|
|
2604
|
+
args: z.object({ dominio: z.string().describe("Domínio remetente (ex: mail.meusite.com)") }),
|
|
2605
|
+
async run(c) {
|
|
2606
|
+
const email = await getEmailClient();
|
|
2607
|
+
const result$2 = await withSpinner({
|
|
2608
|
+
text: `Registrando ${chalk.bold(c.args.dominio)}...`,
|
|
2609
|
+
fn: () => email.provisionDomain({ domain: c.args.dominio })
|
|
2610
|
+
});
|
|
2611
|
+
success(`Domínio ${chalk.bold(result$2.domain)} registrado!`);
|
|
2612
|
+
printDkimRecords(result$2.dkimRecords);
|
|
2613
|
+
return c.ok({
|
|
2614
|
+
id: result$2.id,
|
|
2615
|
+
domain: result$2.domain,
|
|
2616
|
+
status: result$2.status,
|
|
2617
|
+
dkimRecords: result$2.dkimRecords
|
|
2618
|
+
}, { cta: { commands: [{
|
|
2619
|
+
command: `email verify ${result$2.id}`,
|
|
2620
|
+
description: "Verificar configuração DKIM"
|
|
2621
|
+
}, {
|
|
2622
|
+
command: "email list",
|
|
2623
|
+
description: "Listar domínios remetentes"
|
|
2624
|
+
}] } });
|
|
2625
|
+
}
|
|
2626
|
+
});
|
|
2627
|
+
emailGroup.command("verify", {
|
|
2628
|
+
description: "Verificar a configuração DKIM de um domínio remetente",
|
|
2629
|
+
middleware: [requireAuth$1],
|
|
2630
|
+
args: z.object({ emailDomainId: z.string().describe("ID do domínio remetente") }),
|
|
2631
|
+
async run(c) {
|
|
2632
|
+
const email = await getEmailClient();
|
|
2633
|
+
const result$2 = await withSpinner({
|
|
2634
|
+
text: "Verificando DKIM...",
|
|
2635
|
+
fn: () => email.verifyDomain({ emailDomainId: c.args.emailDomainId })
|
|
2636
|
+
});
|
|
2637
|
+
if (result$2.ready) success(`Domínio ${chalk.bold(result$2.domain.domain)} verificado e pronto para enviar!`);
|
|
2638
|
+
else {
|
|
2639
|
+
console.log(chalk.yellow(`\n⚠ ${chalk.bold(result$2.domain.domain)} ainda não verificado (${result$2.domain.status}).`));
|
|
2640
|
+
if (result$2.domain.dkimRecords.length > 0) {
|
|
2641
|
+
info("Confirme que os registros DKIM abaixo estão publicados:");
|
|
2642
|
+
printDkimRecords(result$2.domain.dkimRecords);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
return {
|
|
2646
|
+
ready: result$2.ready,
|
|
2647
|
+
domain: result$2.domain.domain,
|
|
2648
|
+
status: result$2.domain.status
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
});
|
|
2652
|
+
emailGroup.command("remove", {
|
|
2653
|
+
description: "Remover um domínio remetente",
|
|
2654
|
+
mcp: MCP_CONFIRMATION_REQUIRED,
|
|
2655
|
+
middleware: [requireAuth$1],
|
|
2656
|
+
args: z.object({ emailDomainId: z.string().describe("ID do domínio remetente") }),
|
|
2657
|
+
options: z.object({ userConfirmation: z.string().optional() }),
|
|
2658
|
+
async run(c) {
|
|
2659
|
+
requireMcpConfirmation(c.options.userConfirmation, `remover domínio remetente "${c.args.emailDomainId}"`);
|
|
2660
|
+
const email = await getEmailClient();
|
|
2661
|
+
await withSpinner({
|
|
2662
|
+
text: "Removendo domínio...",
|
|
2663
|
+
fn: () => email.deleteDomain({ emailDomainId: c.args.emailDomainId })
|
|
2664
|
+
});
|
|
2665
|
+
success("Domínio remetente removido.");
|
|
2666
|
+
return {
|
|
2667
|
+
deleted: true,
|
|
2668
|
+
emailDomainId: c.args.emailDomainId
|
|
2669
|
+
};
|
|
2670
|
+
}
|
|
2671
|
+
});
|
|
2672
|
+
emailGroup.command("send", {
|
|
2673
|
+
description: "Enviar um e-mail transacional",
|
|
2674
|
+
mcp: MCP_DESTRUCTIVE,
|
|
2675
|
+
middleware: [requireAuth$1],
|
|
2676
|
+
options: z.object({
|
|
2677
|
+
from: z.string().describe("Endereço remetente (em um domínio verificado)"),
|
|
2678
|
+
to: z.string().describe("Destinatário"),
|
|
2679
|
+
subject: z.string().describe("Assunto"),
|
|
2680
|
+
text: z.string().optional().describe("Corpo em texto puro"),
|
|
2681
|
+
html: z.string().optional().describe("Corpo em HTML")
|
|
2682
|
+
}),
|
|
2683
|
+
output: z.object({ messageId: z.string() }),
|
|
2684
|
+
async run(c) {
|
|
2685
|
+
if (!c.options.text && !c.options.html) {
|
|
2686
|
+
info("Informe --text e/ou --html com o corpo do e-mail.");
|
|
2687
|
+
return { messageId: "" };
|
|
2688
|
+
}
|
|
2689
|
+
const email = await getEmailClient();
|
|
2690
|
+
const result$2 = await withSpinner({
|
|
2691
|
+
text: `Enviando e-mail para ${chalk.bold(c.options.to)}...`,
|
|
2692
|
+
fn: () => email.send({
|
|
2693
|
+
from: c.options.from,
|
|
2694
|
+
to: [c.options.to],
|
|
2695
|
+
subject: c.options.subject,
|
|
2696
|
+
text: c.options.text,
|
|
2697
|
+
html: c.options.html
|
|
2698
|
+
})
|
|
2699
|
+
});
|
|
2700
|
+
success(`E-mail enviado (id: ${chalk.dim(result$2.messageId)}).`);
|
|
2701
|
+
return result$2;
|
|
2702
|
+
}
|
|
2703
|
+
});
|
|
2704
|
+
emailGroup.command("keys", {
|
|
2705
|
+
description: "Listar chaves de API de e-mail (vmail_*)",
|
|
2706
|
+
mcp: MCP_READ_ONLY,
|
|
2707
|
+
middleware: [requireAuth$1],
|
|
2708
|
+
output: z.object({ items: z.array(z.object({
|
|
2709
|
+
id: z.string(),
|
|
2710
|
+
name: z.string().nullable(),
|
|
2711
|
+
prefix: z.string().nullable(),
|
|
2712
|
+
start: z.string().nullable()
|
|
2713
|
+
})) }),
|
|
2714
|
+
async run() {
|
|
2715
|
+
const email = await getEmailClient();
|
|
2716
|
+
const keys$1 = await withSpinner({
|
|
2717
|
+
text: "Carregando chaves...",
|
|
2718
|
+
fn: () => email.listApiKeys()
|
|
2719
|
+
});
|
|
2720
|
+
if (keys$1.length === 0) {
|
|
2721
|
+
info("Nenhuma chave de e-mail. Use `veloz email key-create <nome>`.");
|
|
2722
|
+
return { items: [] };
|
|
2723
|
+
}
|
|
2724
|
+
for (const k of keys$1) {
|
|
2725
|
+
const masked = k.start ? `${k.prefix ?? "vmail"}_${k.start}…` : k.prefix ?? "vmail";
|
|
2726
|
+
console.log(` ${k.id} ${chalk.bold(k.name ?? "—")} ${chalk.dim(masked)}`);
|
|
2727
|
+
}
|
|
2728
|
+
return { items: keys$1.map((k) => ({
|
|
2729
|
+
id: k.id,
|
|
2730
|
+
name: k.name,
|
|
2731
|
+
prefix: k.prefix,
|
|
2732
|
+
start: k.start
|
|
2733
|
+
})) };
|
|
2734
|
+
}
|
|
2735
|
+
});
|
|
2736
|
+
emailGroup.command("key-create", {
|
|
2737
|
+
description: "Criar uma chave de API de e-mail (vmail_*)",
|
|
2738
|
+
middleware: [requireAuth$1],
|
|
2739
|
+
args: z.object({ nome: z.string().describe("Nome da chave") }),
|
|
2740
|
+
output: z.object({
|
|
2741
|
+
id: z.string(),
|
|
2742
|
+
name: z.string(),
|
|
2743
|
+
key: z.string()
|
|
2744
|
+
}),
|
|
2745
|
+
async run(c) {
|
|
2746
|
+
const email = await getEmailClient();
|
|
2747
|
+
const created = await withSpinner({
|
|
2748
|
+
text: "Criando chave...",
|
|
2749
|
+
fn: () => email.createApiKey({ name: c.args.nome })
|
|
2750
|
+
});
|
|
2751
|
+
success("Chave criada. Copie agora — ela não será exibida novamente:");
|
|
2752
|
+
console.log(`\n ${chalk.bold(created.key)}\n`);
|
|
2753
|
+
return {
|
|
2754
|
+
id: created.id,
|
|
2755
|
+
name: created.name,
|
|
2756
|
+
key: created.key
|
|
2757
|
+
};
|
|
2758
|
+
}
|
|
2759
|
+
});
|
|
2760
|
+
emailGroup.command("key-revoke", {
|
|
2761
|
+
description: "Revogar uma chave de API de e-mail",
|
|
2762
|
+
mcp: MCP_CONFIRMATION_REQUIRED,
|
|
2763
|
+
middleware: [requireAuth$1],
|
|
2764
|
+
args: z.object({ keyId: z.string().describe("ID da chave") }),
|
|
2765
|
+
options: z.object({ userConfirmation: z.string().optional() }),
|
|
2766
|
+
async run(c) {
|
|
2767
|
+
requireMcpConfirmation(c.options.userConfirmation, `revogar chave "${c.args.keyId}"`);
|
|
2768
|
+
const email = await getEmailClient();
|
|
2769
|
+
await withSpinner({
|
|
2770
|
+
text: "Revogando chave...",
|
|
2771
|
+
fn: () => email.revokeApiKey({ keyId: c.args.keyId })
|
|
2772
|
+
});
|
|
2773
|
+
success("Chave revogada.");
|
|
2774
|
+
return {
|
|
2775
|
+
revoked: true,
|
|
2776
|
+
keyId: c.args.keyId
|
|
2777
|
+
};
|
|
2778
|
+
}
|
|
2779
|
+
});
|
|
2780
|
+
|
|
2477
2781
|
//#endregion
|
|
2478
2782
|
//#region src/lib/volume-config.ts
|
|
2479
2783
|
function updateServiceVolumesInConfig(serviceKey, updater) {
|
|
@@ -2528,6 +2832,7 @@ async function promptAndSyncDeployment(serviceId) {
|
|
|
2528
2832
|
const volumesGroup = Cli.create("volumes", { description: "Gerenciar volumes persistentes" });
|
|
2529
2833
|
volumesGroup.command("list", {
|
|
2530
2834
|
description: "Listar volumes dos serviços",
|
|
2835
|
+
mcp: MCP_READ_ONLY,
|
|
2531
2836
|
middleware: [requireAuth$1],
|
|
2532
2837
|
options: z.object({
|
|
2533
2838
|
service: z.string().optional().describe("Filtrar por serviço (chave ou nome)"),
|
|
@@ -2669,6 +2974,7 @@ volumesGroup.command("expand", {
|
|
|
2669
2974
|
});
|
|
2670
2975
|
volumesGroup.command("delete", {
|
|
2671
2976
|
description: "Remover um volume do serviço (dados mantidos por 30 dias)",
|
|
2977
|
+
mcp: MCP_CONFIRMATION_REQUIRED,
|
|
2672
2978
|
middleware: [requireAuth$1],
|
|
2673
2979
|
args: z.object({ volume: z.string().describe("Nome ou ID do volume") }),
|
|
2674
2980
|
options: z.object({
|
|
@@ -2801,6 +3107,7 @@ function printServiceConfig(service$2) {
|
|
|
2801
3107
|
const configGroup = Cli.create("config", { description: "Gerenciar configuração de serviços" });
|
|
2802
3108
|
configGroup.command("show", {
|
|
2803
3109
|
description: "Mostrar configurações atuais dos serviços",
|
|
3110
|
+
mcp: MCP_READ_ONLY,
|
|
2804
3111
|
middleware: [requireAuth$1],
|
|
2805
3112
|
options: z.object({
|
|
2806
3113
|
service: z.string().optional().describe("Filtrar por serviço (chave ou nome)"),
|
|
@@ -2917,6 +3224,7 @@ configGroup.command("set", {
|
|
|
2917
3224
|
});
|
|
2918
3225
|
configGroup.command("edit", {
|
|
2919
3226
|
description: "Editar configurações interativamente",
|
|
3227
|
+
mcp: false,
|
|
2920
3228
|
middleware: [requireAuth$1],
|
|
2921
3229
|
options: z.object({
|
|
2922
3230
|
service: z.string().optional().describe("Serviço alvo (chave ou nome)"),
|
|
@@ -2978,6 +3286,7 @@ configGroup.command("edit", {
|
|
|
2978
3286
|
});
|
|
2979
3287
|
configGroup.command("reset", {
|
|
2980
3288
|
description: "Resetar configurações para os padrões",
|
|
3289
|
+
mcp: MCP_CONFIRMATION_REQUIRED,
|
|
2981
3290
|
middleware: [requireAuth$1],
|
|
2982
3291
|
options: z.object({
|
|
2983
3292
|
build: z.boolean().default(false).describe("Resetar comando de build"),
|
|
@@ -3097,6 +3406,7 @@ apikeyGroup.command("create", {
|
|
|
3097
3406
|
});
|
|
3098
3407
|
apikeyGroup.command("list", {
|
|
3099
3408
|
description: "Listar chaves de API",
|
|
3409
|
+
mcp: MCP_READ_ONLY,
|
|
3100
3410
|
middleware: [requireAuth$1],
|
|
3101
3411
|
output: z.object({ items: z.array(z.object({
|
|
3102
3412
|
name: z.string(),
|
|
@@ -3124,6 +3434,7 @@ apikeyGroup.command("list", {
|
|
|
3124
3434
|
});
|
|
3125
3435
|
apikeyGroup.command("delete", {
|
|
3126
3436
|
description: "Deletar uma chave de API",
|
|
3437
|
+
mcp: MCP_DESTRUCTIVE,
|
|
3127
3438
|
middleware: [requireAuth$1],
|
|
3128
3439
|
args: z.object({ keyId: z.string().describe("ID da chave de API") }),
|
|
3129
3440
|
output: z.object({ deleted: z.boolean() }),
|
|
@@ -3201,6 +3512,7 @@ function parseDatabaseEngine(value) {
|
|
|
3201
3512
|
const dbGroup = Cli.create("db", { description: "Gerenciar bancos de dados" });
|
|
3202
3513
|
dbGroup.command("list", {
|
|
3203
3514
|
description: "Listar bancos de dados do projeto",
|
|
3515
|
+
mcp: MCP_READ_ONLY,
|
|
3204
3516
|
middleware: [requireAuth$1],
|
|
3205
3517
|
options: z.object({ project: z.string().optional().describe("Projeto (nome, slug ou ID)") }),
|
|
3206
3518
|
output: z.object({ items: z.array(z.object({
|
|
@@ -3342,6 +3654,7 @@ dbGroup.command("create", {
|
|
|
3342
3654
|
});
|
|
3343
3655
|
dbGroup.command("credentials", {
|
|
3344
3656
|
description: "Exibir credenciais de um banco de dados",
|
|
3657
|
+
mcp: MCP_READ_ONLY,
|
|
3345
3658
|
middleware: [requireAuth$1],
|
|
3346
3659
|
args: z.object({ name: z.string().describe("Nome ou ID do banco de dados") }),
|
|
3347
3660
|
options: z.object({ project: z.string().optional().describe("Projeto (nome, slug ou ID)") }),
|
|
@@ -3351,7 +3664,9 @@ dbGroup.command("credentials", {
|
|
|
3351
3664
|
username: z.string(),
|
|
3352
3665
|
password: z.string(),
|
|
3353
3666
|
database: z.string(),
|
|
3354
|
-
connectionUrl: z.string()
|
|
3667
|
+
connectionUrl: z.string(),
|
|
3668
|
+
poolerUrl: z.string().nullable(),
|
|
3669
|
+
poolerPort: z.number().nullable()
|
|
3355
3670
|
}),
|
|
3356
3671
|
async run(c) {
|
|
3357
3672
|
const db = await resolveDatabaseByName(await getProjectId(c.options.project), c.args.name);
|
|
@@ -3367,12 +3682,14 @@ dbGroup.command("credentials", {
|
|
|
3367
3682
|
console.log(` ${chalk.dim("Senha:")} ${creds.password}`);
|
|
3368
3683
|
console.log(` ${chalk.dim("Banco:")} ${creds.database}`);
|
|
3369
3684
|
console.log(` ${chalk.dim("URL:")} ${creds.connectionUrl}`);
|
|
3685
|
+
if (creds.poolerUrl) console.log(` ${chalk.dim("URL (pooler):")} ${creds.poolerUrl}`);
|
|
3370
3686
|
console.log();
|
|
3371
3687
|
return creds;
|
|
3372
3688
|
}
|
|
3373
3689
|
});
|
|
3374
3690
|
dbGroup.command("delete", {
|
|
3375
3691
|
description: "Excluir um banco de dados",
|
|
3692
|
+
mcp: MCP_CONFIRMATION_REQUIRED,
|
|
3376
3693
|
middleware: [requireAuth$1],
|
|
3377
3694
|
args: z.object({ name: z.string().describe("Nome ou ID do banco de dados") }),
|
|
3378
3695
|
options: z.object({
|
|
@@ -3426,6 +3743,7 @@ dbGroup.command("delete", {
|
|
|
3426
3743
|
});
|
|
3427
3744
|
dbGroup.command("restart", {
|
|
3428
3745
|
description: "Reiniciar um banco de dados",
|
|
3746
|
+
mcp: MCP_CONFIRMATION_REQUIRED,
|
|
3429
3747
|
middleware: [requireAuth$1],
|
|
3430
3748
|
args: z.object({ name: z.string().describe("Nome ou ID do banco de dados") }),
|
|
3431
3749
|
options: z.object({
|
|
@@ -3609,6 +3927,7 @@ dbGroup.command("update", {
|
|
|
3609
3927
|
});
|
|
3610
3928
|
dbGroup.command("query", {
|
|
3611
3929
|
description: "Executar uma consulta no banco de dados",
|
|
3930
|
+
mcp: MCP_DESTRUCTIVE,
|
|
3612
3931
|
middleware: [requireAuth$1],
|
|
3613
3932
|
args: z.object({ name: z.string().describe("Nome ou ID do banco de dados") }),
|
|
3614
3933
|
options: z.object({
|
|
@@ -3666,11 +3985,13 @@ const ENGINE_CONNECT_HINTS = {
|
|
|
3666
3985
|
};
|
|
3667
3986
|
dbGroup.command("tunnel", {
|
|
3668
3987
|
description: "Criar túnel local para um banco de dados",
|
|
3988
|
+
mcp: false,
|
|
3669
3989
|
middleware: [requireAuth$1],
|
|
3670
3990
|
args: z.object({ name: z.string().describe("Nome ou ID do banco de dados") }),
|
|
3671
3991
|
options: z.object({
|
|
3672
3992
|
project: z.string().optional().describe("Projeto (nome, slug ou ID)"),
|
|
3673
|
-
port: z.number().optional().describe("Porta local (padrão: porta do engine)")
|
|
3993
|
+
port: z.number().optional().describe("Porta local (padrão: porta do engine)"),
|
|
3994
|
+
pooler: z.boolean().optional().describe("Conectar via PgBouncer (pooling de transações) em vez de conexão direta")
|
|
3674
3995
|
}),
|
|
3675
3996
|
alias: { port: "p" },
|
|
3676
3997
|
async run(c) {
|
|
@@ -3688,7 +4009,10 @@ dbGroup.command("tunnel", {
|
|
|
3688
4009
|
const server = net.createServer(async (socket) => {
|
|
3689
4010
|
let session;
|
|
3690
4011
|
try {
|
|
3691
|
-
session = await client.databases.createTunnelSession({
|
|
4012
|
+
session = await client.databases.createTunnelSession({
|
|
4013
|
+
serviceId: db.id,
|
|
4014
|
+
pooler: c.options.pooler
|
|
4015
|
+
});
|
|
3692
4016
|
} catch (err) {
|
|
3693
4017
|
const message = err instanceof Error ? err.message : "Erro desconhecido";
|
|
3694
4018
|
console.log(chalk.red(` Falha ao criar sessão de túnel: ${message}`));
|
|
@@ -3732,7 +4056,7 @@ dbGroup.command("tunnel", {
|
|
|
3732
4056
|
});
|
|
3733
4057
|
server.listen(localPort, "127.0.0.1", () => {
|
|
3734
4058
|
console.log();
|
|
3735
|
-
success(`Túnel ${engineLabel} ativo para ${chalk.bold(db.name)}`);
|
|
4059
|
+
success(`Túnel ${engineLabel} ativo para ${chalk.bold(db.name)}${c.options.pooler ? chalk.dim(" (pooler)") : ""}`);
|
|
3736
4060
|
console.log();
|
|
3737
4061
|
console.log(` ${chalk.dim("Endereço local:")} ${chalk.bold(`127.0.0.1:${localPort}`)}`);
|
|
3738
4062
|
console.log(` ${chalk.dim("URL de conexão:")} ${chalk.cyan(localUrl)}`);
|
|
@@ -3759,6 +4083,7 @@ dbGroup.command("tunnel", {
|
|
|
3759
4083
|
});
|
|
3760
4084
|
dbGroup.command("config", {
|
|
3761
4085
|
description: "Ver ou alterar configuração do PostgreSQL",
|
|
4086
|
+
mcp: MCP_CONFIRMATION_REQUIRED,
|
|
3762
4087
|
middleware: [requireAuth$1],
|
|
3763
4088
|
args: z.object({ name: z.string().describe("Nome ou ID do banco de dados") }),
|
|
3764
4089
|
options: z.object({
|
|
@@ -3874,6 +4199,7 @@ dbGroup.command("config", {
|
|
|
3874
4199
|
const templateGroup = Cli.create("template", { description: "Gerenciar templates de deploy" });
|
|
3875
4200
|
templateGroup.command("list", {
|
|
3876
4201
|
description: "Listar templates disponíveis",
|
|
4202
|
+
mcp: MCP_READ_ONLY,
|
|
3877
4203
|
middleware: [requireAuth$1],
|
|
3878
4204
|
output: z.object({ items: z.array(z.object({
|
|
3879
4205
|
slug: z.string(),
|
|
@@ -3910,6 +4236,7 @@ templateGroup.command("list", {
|
|
|
3910
4236
|
});
|
|
3911
4237
|
templateGroup.command("deploy", {
|
|
3912
4238
|
description: "Fazer deploy de um template (cria um novo projeto automaticamente)",
|
|
4239
|
+
mcp: MCP_CONFIRMATION_REQUIRED,
|
|
3913
4240
|
middleware: [requireAuth$1],
|
|
3914
4241
|
args: z.object({ slug: z.string().describe("Slug do template (ex: n8n, metabase)") }),
|
|
3915
4242
|
options: z.object({
|
|
@@ -3971,6 +4298,36 @@ templateGroup.command("deploy", {
|
|
|
3971
4298
|
}
|
|
3972
4299
|
});
|
|
3973
4300
|
|
|
4301
|
+
//#endregion
|
|
4302
|
+
//#region src/lib/retry.ts
|
|
4303
|
+
async function withRetry(fn$2, maxRetries = 3) {
|
|
4304
|
+
let orgSwitched = false;
|
|
4305
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) try {
|
|
4306
|
+
return await fn$2();
|
|
4307
|
+
} catch (error) {
|
|
4308
|
+
if (!orgSwitched && isOrgMismatchError(error)) {
|
|
4309
|
+
if (applyOrgMismatchSwitch(error)) {
|
|
4310
|
+
orgSwitched = true;
|
|
4311
|
+
continue;
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
if (attempt >= maxRetries) throw error;
|
|
4315
|
+
const rateLimit = isRateLimitError(error);
|
|
4316
|
+
if (rateLimit) {
|
|
4317
|
+
const waitMs = Math.min(rateLimit.retryAfterMs, 3e4);
|
|
4318
|
+
await new Promise((r) => {
|
|
4319
|
+
setTimeout(r, waitMs);
|
|
4320
|
+
});
|
|
4321
|
+
} else {
|
|
4322
|
+
const delay$2 = Math.min(1e3 * Math.pow(2, attempt), 1e4);
|
|
4323
|
+
await new Promise((r) => {
|
|
4324
|
+
setTimeout(r, delay$2);
|
|
4325
|
+
});
|
|
4326
|
+
}
|
|
4327
|
+
}
|
|
4328
|
+
throw new Error("Max retries exceeded");
|
|
4329
|
+
}
|
|
4330
|
+
|
|
3974
4331
|
//#endregion
|
|
3975
4332
|
//#region src/commands/services.ts
|
|
3976
4333
|
const servicesGroup = Cli.create("services", {
|
|
@@ -3979,6 +4336,7 @@ const servicesGroup = Cli.create("services", {
|
|
|
3979
4336
|
});
|
|
3980
4337
|
servicesGroup.command("list", {
|
|
3981
4338
|
description: "Listar serviços do projeto",
|
|
4339
|
+
mcp: MCP_READ_ONLY,
|
|
3982
4340
|
middleware: [requireAuth$1],
|
|
3983
4341
|
options: z.object({ project: z.string().optional().describe("Projeto (nome, slug ou ID)") }),
|
|
3984
4342
|
output: z.object({ items: z.array(z.object({
|
|
@@ -3998,7 +4356,7 @@ servicesGroup.command("list", {
|
|
|
3998
4356
|
const services = await withSpinner({
|
|
3999
4357
|
text: "Carregando serviços...",
|
|
4000
4358
|
fn: async () => {
|
|
4001
|
-
return (await Promise.all([...projectIds].map((pid) => client.services.list({ projectId: pid })))).flat();
|
|
4359
|
+
return (await Promise.all([...projectIds].map((pid) => withRetry(() => client.services.list({ projectId: pid }))))).flat();
|
|
4002
4360
|
}
|
|
4003
4361
|
});
|
|
4004
4362
|
if (services.length === 0) {
|
|
@@ -4015,6 +4373,7 @@ servicesGroup.command("list", {
|
|
|
4015
4373
|
});
|
|
4016
4374
|
servicesGroup.command("delete", {
|
|
4017
4375
|
description: "Deletar um serviço",
|
|
4376
|
+
mcp: MCP_CONFIRMATION_REQUIRED,
|
|
4018
4377
|
middleware: [requireAuth$1],
|
|
4019
4378
|
args: z.object({ service: z.string().optional().describe("Nome ou ID do serviço") }),
|
|
4020
4379
|
options: z.object({
|
|
@@ -4033,7 +4392,7 @@ servicesGroup.command("delete", {
|
|
|
4033
4392
|
const services = await withSpinner({
|
|
4034
4393
|
text: "Carregando serviços...",
|
|
4035
4394
|
fn: async () => {
|
|
4036
|
-
return (await Promise.all([...projectIds].map((pid) => client.services.list({ projectId: pid })))).flat();
|
|
4395
|
+
return (await Promise.all([...projectIds].map((pid) => withRetry(() => client.services.list({ projectId: pid }))))).flat();
|
|
4037
4396
|
}
|
|
4038
4397
|
});
|
|
4039
4398
|
if (services.length === 0) {
|
|
@@ -4067,7 +4426,7 @@ servicesGroup.command("delete", {
|
|
|
4067
4426
|
}
|
|
4068
4427
|
await withSpinner({
|
|
4069
4428
|
text: `Deletando serviço "${serviceToDelete.name}"...`,
|
|
4070
|
-
fn: () => client.services.delete({ serviceId: serviceToDelete.id })
|
|
4429
|
+
fn: () => withRetry(() => client.services.delete({ serviceId: serviceToDelete.id }))
|
|
4071
4430
|
});
|
|
4072
4431
|
success(`Serviço "${serviceToDelete.name}" deletado com sucesso!`);
|
|
4073
4432
|
return c.ok({
|
|
@@ -4197,6 +4556,7 @@ function printInsightsHint() {
|
|
|
4197
4556
|
const metricsGroup = Cli.create("metrics", { description: "Visualizar métricas dos serviços" });
|
|
4198
4557
|
metricsGroup.command("show", {
|
|
4199
4558
|
description: "Exibir métricas atuais do serviço",
|
|
4559
|
+
mcp: MCP_READ_ONLY,
|
|
4200
4560
|
middleware: [requireAuth$1],
|
|
4201
4561
|
options: z.object({
|
|
4202
4562
|
service: z.string().optional().describe("Filtrar por serviço ou banco de dados"),
|
|
@@ -4311,6 +4671,7 @@ metricsGroup.command("show", {
|
|
|
4311
4671
|
});
|
|
4312
4672
|
metricsGroup.command("range", {
|
|
4313
4673
|
description: "Exibir métricas em intervalo de tempo com sparklines",
|
|
4674
|
+
mcp: MCP_READ_ONLY,
|
|
4314
4675
|
middleware: [requireAuth$1],
|
|
4315
4676
|
options: z.object({
|
|
4316
4677
|
service: z.string().optional().describe("Filtrar por serviço ou banco de dados"),
|
|
@@ -4388,6 +4749,7 @@ metricsGroup.command("range", {
|
|
|
4388
4749
|
});
|
|
4389
4750
|
metricsGroup.command("query", {
|
|
4390
4751
|
description: "Executar consulta MetricsQL personalizada",
|
|
4752
|
+
mcp: MCP_READ_ONLY,
|
|
4391
4753
|
middleware: [requireAuth$1],
|
|
4392
4754
|
args: z.object({ query: z.string().describe("Consulta MetricsQL (ex: up, rate(http_requests[5m]))") }),
|
|
4393
4755
|
options: z.object({
|
|
@@ -4468,6 +4830,7 @@ metricsGroup.command("query", {
|
|
|
4468
4830
|
});
|
|
4469
4831
|
metricsGroup.command("list", {
|
|
4470
4832
|
description: "Listar nomes de métricas disponíveis",
|
|
4833
|
+
mcp: MCP_READ_ONLY,
|
|
4471
4834
|
middleware: [requireAuth$1],
|
|
4472
4835
|
options: z.object({
|
|
4473
4836
|
match: z.string().optional().describe("Filtrar por seletor (ex: \"{job=\\\"node\\\"}\", \"traefik_.*\")"),
|
|
@@ -4501,6 +4864,7 @@ metricsGroup.command("list", {
|
|
|
4501
4864
|
});
|
|
4502
4865
|
metricsGroup.command("labels", {
|
|
4503
4866
|
description: "Listar labels disponíveis ou valores de um label específico",
|
|
4867
|
+
mcp: MCP_READ_ONLY,
|
|
4504
4868
|
middleware: [requireAuth$1],
|
|
4505
4869
|
options: z.object({
|
|
4506
4870
|
label: z.string().optional().describe("Nome do label para listar valores (ex: pod, namespace)"),
|
|
@@ -4540,6 +4904,7 @@ metricsGroup.command("labels", {
|
|
|
4540
4904
|
});
|
|
4541
4905
|
metricsGroup.command("series", {
|
|
4542
4906
|
description: "Encontrar séries de métricas por seletor",
|
|
4907
|
+
mcp: MCP_READ_ONLY,
|
|
4543
4908
|
middleware: [requireAuth$1],
|
|
4544
4909
|
args: z.object({ match: z.string().describe("Seletor de séries (ex: \"traefik_service_requests_total\", '{__name__=~\"pg_.*\"}')") }),
|
|
4545
4910
|
options: z.object({ limit: z.number().default(100).describe("Número máximo de séries") }),
|
|
@@ -4806,6 +5171,7 @@ const QUERY_HELP_SECTIONS$1 = [
|
|
|
4806
5171
|
];
|
|
4807
5172
|
metricsGroup.command("query-help", {
|
|
4808
5173
|
description: "Referência de MetricsQL para consultas de métricas",
|
|
5174
|
+
mcp: MCP_READ_ONLY,
|
|
4809
5175
|
outputPolicy: "agent-only",
|
|
4810
5176
|
output: z.object({ sections: z.array(z.object({
|
|
4811
5177
|
title: z.string(),
|
|
@@ -4832,6 +5198,7 @@ metricsGroup.command("query-help", {
|
|
|
4832
5198
|
});
|
|
4833
5199
|
metricsGroup.command("help", {
|
|
4834
5200
|
description: "Referência de métricas disponíveis e seus campos",
|
|
5201
|
+
mcp: MCP_READ_ONLY,
|
|
4835
5202
|
outputPolicy: "agent-only",
|
|
4836
5203
|
output: z.object({ sections: z.array(z.object({
|
|
4837
5204
|
title: z.string(),
|
|
@@ -4920,6 +5287,10 @@ async function fetchRecent(services, maxNameLen, tailLines) {
|
|
|
4920
5287
|
const logsGroup = Cli.create("logs", { description: "Visualizar logs dos serviços" });
|
|
4921
5288
|
logsGroup.command("show", {
|
|
4922
5289
|
description: "Visualizar logs recentes ou acompanhar em tempo real",
|
|
5290
|
+
mcp: {
|
|
5291
|
+
...MCP_READ_ONLY,
|
|
5292
|
+
instructions: "Never call this tool with follow=true in MCP mode because it opens a long-lived log stream. Use follow=false and poll again when fresh logs are needed."
|
|
5293
|
+
},
|
|
4923
5294
|
middleware: [requireAuth$1],
|
|
4924
5295
|
options: z.object({
|
|
4925
5296
|
follow: z.boolean().optional().describe("Acompanhar logs em tempo real"),
|
|
@@ -4932,6 +5303,10 @@ logsGroup.command("show", {
|
|
|
4932
5303
|
tail: "n"
|
|
4933
5304
|
},
|
|
4934
5305
|
async run(c) {
|
|
5306
|
+
if (c.options.follow && isMcpMode()) return c.error({
|
|
5307
|
+
code: "MCP_FOLLOW_UNSUPPORTED",
|
|
5308
|
+
message: "Long-lived log streaming is not supported over MCP. Use follow=false and poll again for fresh logs."
|
|
5309
|
+
});
|
|
4935
5310
|
const spin = spinner("Carregando logs...");
|
|
4936
5311
|
const { services, maxNameLen } = await resolveAllServices(c.options.service, c.options.project);
|
|
4937
5312
|
const tailLines = c.options.tail;
|
|
@@ -4951,6 +5326,7 @@ logsGroup.command("show", {
|
|
|
4951
5326
|
});
|
|
4952
5327
|
logsGroup.command("search", {
|
|
4953
5328
|
description: "Pesquisar logs com consultas LogsQL",
|
|
5329
|
+
mcp: MCP_READ_ONLY,
|
|
4954
5330
|
middleware: [requireAuth$1],
|
|
4955
5331
|
args: z.object({ query: z.string().default("").describe("Consulta LogsQL (ex: error, ~\"regex\", field:value)") }),
|
|
4956
5332
|
options: z.object({
|
|
@@ -5141,6 +5517,7 @@ const QUERY_HELP_SECTIONS = [
|
|
|
5141
5517
|
];
|
|
5142
5518
|
logsGroup.command("query-help", {
|
|
5143
5519
|
description: "Referência de sintaxe LogsQL para pesquisa de logs",
|
|
5520
|
+
mcp: MCP_READ_ONLY,
|
|
5144
5521
|
outputPolicy: "agent-only",
|
|
5145
5522
|
output: z.object({ sections: z.array(z.object({
|
|
5146
5523
|
title: z.string(),
|
|
@@ -5177,6 +5554,7 @@ logsGroup.command("query-help", {
|
|
|
5177
5554
|
const githubGroup = Cli.create("github", { description: "Gerenciar integração com GitHub" });
|
|
5178
5555
|
githubGroup.command("setup", {
|
|
5179
5556
|
description: "Configurar GitHub App para deploy automático via webhook",
|
|
5557
|
+
mcp: false,
|
|
5180
5558
|
middleware: [requireAuth$1],
|
|
5181
5559
|
options: z.object({ project: z.string().optional().describe("ID do projeto a conectar") }),
|
|
5182
5560
|
alias: { project: "p" },
|
|
@@ -5321,6 +5699,7 @@ githubGroup.command("setup", {
|
|
|
5321
5699
|
});
|
|
5322
5700
|
githubGroup.command("status", {
|
|
5323
5701
|
description: "Verificar status da integração GitHub do projeto",
|
|
5702
|
+
mcp: MCP_READ_ONLY,
|
|
5324
5703
|
middleware: [requireAuth$1],
|
|
5325
5704
|
output: z.object({
|
|
5326
5705
|
connected: z.boolean(),
|
|
@@ -5436,7 +5815,7 @@ const TERMINAL_STATUSES = new Set([
|
|
|
5436
5815
|
|
|
5437
5816
|
//#endregion
|
|
5438
5817
|
//#region src/lib/deploy-stream.ts
|
|
5439
|
-
const DASHBOARD_BASE$
|
|
5818
|
+
const DASHBOARD_BASE$2 = process.env.VELOZ_WEB_URL || "https://app.onveloz.com";
|
|
5440
5819
|
/**
|
|
5441
5820
|
* Fetch deployment details from the API and enrich a DeployStreamResult
|
|
5442
5821
|
* with metadata useful for agents (failReason, commit info, duration, dashboard link).
|
|
@@ -5452,7 +5831,7 @@ async function enrichResult(client, result$2) {
|
|
|
5452
5831
|
result$2.branch = d.branch ?? null;
|
|
5453
5832
|
if (d.startedAt && d.finishedAt) result$2.durationSeconds = Math.round((new Date(d.finishedAt).getTime() - new Date(d.startedAt).getTime()) / 1e3);
|
|
5454
5833
|
if (projectId) {
|
|
5455
|
-
result$2.dashboardUrl = `${DASHBOARD_BASE$
|
|
5834
|
+
result$2.dashboardUrl = `${DASHBOARD_BASE$2}/projetos/${projectId}/servicos/${d.serviceId}/deploys/${d.id}`;
|
|
5456
5835
|
const project = await client.projects.get({ projectId }).catch(() => null);
|
|
5457
5836
|
if (project) result$2.organizationId = project.organizationId ?? null;
|
|
5458
5837
|
}
|
|
@@ -5613,6 +5992,8 @@ async function streamDeploymentLogs(deploymentId, serviceId, serviceName) {
|
|
|
5613
5992
|
process.stdout.write(`${header}\n`);
|
|
5614
5993
|
}
|
|
5615
5994
|
let finalStatus = "";
|
|
5995
|
+
let finalErrorSummary = null;
|
|
5996
|
+
let finalFailReason = null;
|
|
5616
5997
|
try {
|
|
5617
5998
|
const stream = await client.logs.streamBuildLogs({ deploymentId });
|
|
5618
5999
|
for await (const event of stream) if (event.type === "status") {
|
|
@@ -5632,7 +6013,10 @@ async function streamDeploymentLogs(deploymentId, serviceId, serviceName) {
|
|
|
5632
6013
|
}
|
|
5633
6014
|
} catch {
|
|
5634
6015
|
try {
|
|
5635
|
-
|
|
6016
|
+
const d = await client.deployments.get({ deploymentId });
|
|
6017
|
+
finalStatus = d.status;
|
|
6018
|
+
finalErrorSummary = d.errorSummary ?? finalErrorSummary;
|
|
6019
|
+
finalFailReason = d.failReason ?? finalFailReason;
|
|
5636
6020
|
try {
|
|
5637
6021
|
const logs = await client.logs.getBuildLogs({ deploymentId });
|
|
5638
6022
|
if (logs.buildLogs) allLogLines.push(...logs.buildLogs.split("\n"));
|
|
@@ -5640,8 +6024,12 @@ async function streamDeploymentLogs(deploymentId, serviceId, serviceName) {
|
|
|
5640
6024
|
} catch {}
|
|
5641
6025
|
}
|
|
5642
6026
|
if (!TERMINAL_STATUSES.has(finalStatus)) try {
|
|
5643
|
-
|
|
6027
|
+
const d = await client.deployments.get({ deploymentId });
|
|
6028
|
+
finalStatus = d.status;
|
|
6029
|
+
finalErrorSummary = d.errorSummary ?? finalErrorSummary;
|
|
6030
|
+
finalFailReason = d.failReason ?? finalFailReason;
|
|
5644
6031
|
} catch {}
|
|
6032
|
+
const finalReason = finalErrorSummary ?? finalFailReason;
|
|
5645
6033
|
if (isGHA) endGroup();
|
|
5646
6034
|
const urls = finalStatus === "LIVE" ? await fetchDeployUrls$1(client, serviceId) : [];
|
|
5647
6035
|
if (finalStatus === "LIVE") {
|
|
@@ -5652,6 +6040,7 @@ async function streamDeploymentLogs(deploymentId, serviceId, serviceName) {
|
|
|
5652
6040
|
const hints = getFailureHints$1(finalStatus);
|
|
5653
6041
|
if (mcp) {
|
|
5654
6042
|
log(`✗ Deploy finalizou: ${label}`);
|
|
6043
|
+
if (finalReason) log(`Motivo: ${finalReason}`);
|
|
5655
6044
|
for (const line of allLogLines) if (line.trim()) log(`[build] ${line.trim()}`);
|
|
5656
6045
|
for (const hint of hints) log(` → ${hint}`);
|
|
5657
6046
|
} else if (isTTY && allLogLines.length > 0) {
|
|
@@ -5664,11 +6053,12 @@ async function streamDeploymentLogs(deploymentId, serviceId, serviceName) {
|
|
|
5664
6053
|
}
|
|
5665
6054
|
if (isGHA) {
|
|
5666
6055
|
const msg = serviceName ? `Deploy de ${serviceName} finalizou com status: ${label}` : `Deploy finalizou com status: ${label}`;
|
|
5667
|
-
process.stdout.write(`::error::${msg}\n`);
|
|
6056
|
+
process.stdout.write(`::error::${finalReason ? `${msg} — ${finalReason}` : msg}\n`);
|
|
5668
6057
|
for (const hint of hints) process.stdout.write(` ${hint}\n`);
|
|
5669
6058
|
} else if (!mcp) {
|
|
5670
6059
|
const errorMsg = serviceName ? `Deploy de ${chalk.bold(serviceName)} finalizou: ${label}` : `Deploy finalizou: ${label}`;
|
|
5671
6060
|
console.error(chalk.red(`\n✗ ${errorMsg}`));
|
|
6061
|
+
if (finalReason) console.error(chalk.red(` ${chalk.bold("Motivo:")} ${finalReason}`));
|
|
5672
6062
|
for (const hint of hints) console.error(chalk.yellow(` → ${hint}`));
|
|
5673
6063
|
console.error(chalk.yellow(` → Use 'veloz builds logs ${deploymentId}' para ver os logs completos`));
|
|
5674
6064
|
}
|
|
@@ -5729,6 +6119,7 @@ const buildsGroup = Cli.create("builds", {
|
|
|
5729
6119
|
});
|
|
5730
6120
|
buildsGroup.command("list", {
|
|
5731
6121
|
description: "Listar builds recentes de um serviço",
|
|
6122
|
+
mcp: MCP_READ_ONLY,
|
|
5732
6123
|
middleware: [requireAuth$1],
|
|
5733
6124
|
options: z.object({
|
|
5734
6125
|
service: z.string().optional().describe("Nome ou ID do serviço"),
|
|
@@ -5750,11 +6141,11 @@ buildsGroup.command("list", {
|
|
|
5750
6141
|
const client = await getClient();
|
|
5751
6142
|
const deployments = await withSpinner({
|
|
5752
6143
|
text: "Carregando builds...",
|
|
5753
|
-
fn: () => client.deployments.list({
|
|
6144
|
+
fn: () => withRetry(() => client.deployments.list({
|
|
5754
6145
|
serviceId,
|
|
5755
6146
|
limit: c.options.limit,
|
|
5756
6147
|
offset: 0
|
|
5757
|
-
})
|
|
6148
|
+
}))
|
|
5758
6149
|
});
|
|
5759
6150
|
if (deployments.length === 0) {
|
|
5760
6151
|
info("Nenhum build encontrado para este serviço.");
|
|
@@ -5790,6 +6181,7 @@ buildsGroup.command("list", {
|
|
|
5790
6181
|
});
|
|
5791
6182
|
buildsGroup.command("show", {
|
|
5792
6183
|
description: "Exibir detalhes de um build específico",
|
|
6184
|
+
mcp: MCP_READ_ONLY,
|
|
5793
6185
|
middleware: [requireAuth$1],
|
|
5794
6186
|
args: z.object({ id: z.string().describe("ID do build (completo ou parcial)") }),
|
|
5795
6187
|
options: z.object({
|
|
@@ -5817,17 +6209,17 @@ buildsGroup.command("show", {
|
|
|
5817
6209
|
try {
|
|
5818
6210
|
deployment = await withSpinner({
|
|
5819
6211
|
text: "Carregando build...",
|
|
5820
|
-
fn: () => client.deployments.get({ deploymentId: searchId })
|
|
6212
|
+
fn: () => withRetry(() => client.deployments.get({ deploymentId: searchId }))
|
|
5821
6213
|
});
|
|
5822
6214
|
} catch {
|
|
5823
6215
|
const serviceId = await resolveServiceId(c.options.service, c.options.project);
|
|
5824
6216
|
deployment = (await withSpinner({
|
|
5825
6217
|
text: "Buscando build...",
|
|
5826
|
-
fn: () => client.deployments.list({
|
|
6218
|
+
fn: () => withRetry(() => client.deployments.list({
|
|
5827
6219
|
serviceId,
|
|
5828
6220
|
limit: 50,
|
|
5829
6221
|
offset: 0
|
|
5830
|
-
})
|
|
6222
|
+
}))
|
|
5831
6223
|
})).find((d) => d.id.startsWith(searchId));
|
|
5832
6224
|
if (!deployment) throw new Error(`Build "${searchId}" não encontrado.`);
|
|
5833
6225
|
}
|
|
@@ -5875,6 +6267,7 @@ buildsGroup.command("show", {
|
|
|
5875
6267
|
});
|
|
5876
6268
|
buildsGroup.command("logs", {
|
|
5877
6269
|
description: "Exibir logs de build de um deploy",
|
|
6270
|
+
mcp: MCP_READ_ONLY,
|
|
5878
6271
|
middleware: [requireAuth$1],
|
|
5879
6272
|
args: z.object({ id: z.string().describe("ID do build (completo ou parcial)") }),
|
|
5880
6273
|
options: z.object({
|
|
@@ -5892,16 +6285,16 @@ buildsGroup.command("logs", {
|
|
|
5892
6285
|
const searchId = c.args.id;
|
|
5893
6286
|
let deploymentId = searchId;
|
|
5894
6287
|
try {
|
|
5895
|
-
deploymentId = (await client.deployments.get({ deploymentId: searchId })).id;
|
|
6288
|
+
deploymentId = (await withRetry(() => client.deployments.get({ deploymentId: searchId }))).id;
|
|
5896
6289
|
} catch {
|
|
5897
6290
|
const serviceId = await resolveServiceId(c.options.service, c.options.project);
|
|
5898
6291
|
const found = (await withSpinner({
|
|
5899
6292
|
text: "Buscando build...",
|
|
5900
|
-
fn: () => client.deployments.list({
|
|
6293
|
+
fn: () => withRetry(() => client.deployments.list({
|
|
5901
6294
|
serviceId,
|
|
5902
6295
|
limit: 50,
|
|
5903
6296
|
offset: 0
|
|
5904
|
-
})
|
|
6297
|
+
}))
|
|
5905
6298
|
})).find((d) => d.id.startsWith(searchId));
|
|
5906
6299
|
if (!found) {
|
|
5907
6300
|
warn(`Build "${searchId}" não encontrado.`);
|
|
@@ -5915,7 +6308,7 @@ buildsGroup.command("logs", {
|
|
|
5915
6308
|
}
|
|
5916
6309
|
const result$2 = await withSpinner({
|
|
5917
6310
|
text: "Carregando logs de build...",
|
|
5918
|
-
fn: () => client.logs.getBuildLogs({ deploymentId })
|
|
6311
|
+
fn: () => withRetry(() => client.logs.getBuildLogs({ deploymentId }))
|
|
5919
6312
|
});
|
|
5920
6313
|
if (!result$2.buildLogs) {
|
|
5921
6314
|
info("Nenhum log de build disponível para este deploy.");
|
|
@@ -5925,7 +6318,7 @@ buildsGroup.command("logs", {
|
|
|
5925
6318
|
logs: ""
|
|
5926
6319
|
};
|
|
5927
6320
|
}
|
|
5928
|
-
const deployment = await client.deployments.get({ deploymentId });
|
|
6321
|
+
const deployment = await withRetry(() => client.deployments.get({ deploymentId }));
|
|
5929
6322
|
if (process.stdout.isTTY) {
|
|
5930
6323
|
const icon = getStatusIcon(deployment.status);
|
|
5931
6324
|
const label = statusLabels[deployment.status] ?? deployment.status;
|
|
@@ -5986,6 +6379,7 @@ buildsGroup.command("logs", {
|
|
|
5986
6379
|
function registerLink(cli$1) {
|
|
5987
6380
|
cli$1.command("link", {
|
|
5988
6381
|
description: "Verificar vínculo do projeto com Veloz",
|
|
6382
|
+
mcp: MCP_READ_ONLY,
|
|
5989
6383
|
output: z.object({
|
|
5990
6384
|
project: z.object({
|
|
5991
6385
|
id: z.string(),
|
|
@@ -22743,6 +23137,25 @@ function resolveProductionStartCmd(scripts, pm) {
|
|
|
22743
23137
|
if (isDevScript(scripts.start)) return null;
|
|
22744
23138
|
return pmRun(pm, "start");
|
|
22745
23139
|
}
|
|
23140
|
+
/**
|
|
23141
|
+
* Resolve a production start command for a deployable app.
|
|
23142
|
+
*
|
|
23143
|
+
* Precedence: a valid (non-dev) `start` script → `<pm> run start`; otherwise the
|
|
23144
|
+
* framework's canonical default (`next start`, `node dist/main.js`, …). For a
|
|
23145
|
+
* monorepo app in a subdirectory the command is prefixed with `cd <appPath> &&`
|
|
23146
|
+
* so it runs from the app even though the builder's WORKDIR is the repo root —
|
|
23147
|
+
* what plain pnpm/yarn workspaces (no `NIXPACKS_<TOOL>_APP_NAME` targeting) need
|
|
23148
|
+
* to avoid "No start command could be found". Returns null for STATIC apps.
|
|
23149
|
+
*/
|
|
23150
|
+
function resolveAppStartCommand(pkg, pm, framework, appPath) {
|
|
23151
|
+
if (framework && framework.type === "STATIC") return null;
|
|
23152
|
+
const path$1 = appPath && appPath !== "." && appPath !== "/" ? appPath : null;
|
|
23153
|
+
const inApp = (cmd) => path$1 ? `cd ${path$1} && ${cmd}` : cmd;
|
|
23154
|
+
const scripts = pkg.scripts ?? {};
|
|
23155
|
+
if (scripts.start && !isDevScript(scripts.start)) return inApp(pmRun(pm, "start"));
|
|
23156
|
+
if (framework?.startCommand) return inApp(framework.startCommand);
|
|
23157
|
+
return null;
|
|
23158
|
+
}
|
|
22746
23159
|
/** Safely parse a JSON string as PkgJson. */
|
|
22747
23160
|
function safeParsePkg(content) {
|
|
22748
23161
|
try {
|
|
@@ -23744,13 +24157,9 @@ const detectMonorepo = (pm) => gen(function* () {
|
|
|
23744
24157
|
...appFramework,
|
|
23745
24158
|
buildCommand: pmRun(pm, `build:${appName}`)
|
|
23746
24159
|
};
|
|
23747
|
-
|
|
24160
|
+
appFramework = {
|
|
23748
24161
|
...appFramework,
|
|
23749
|
-
startCommand:
|
|
23750
|
-
};
|
|
23751
|
-
else if (appFramework.startCommand) appFramework = {
|
|
23752
|
-
...appFramework,
|
|
23753
|
-
startCommand: `cd ${appPath} && ${appFramework.startCommand}`
|
|
24162
|
+
startCommand: resolveAppStartCommand(nested, pm, appFramework, appPath)
|
|
23754
24163
|
};
|
|
23755
24164
|
}
|
|
23756
24165
|
let nodeVersion;
|
|
@@ -24387,7 +24796,7 @@ const LOGO_LINES$1 = [
|
|
|
24387
24796
|
];
|
|
24388
24797
|
const BRAND_COLOR$1 = "#FF4D00";
|
|
24389
24798
|
function getVersion() {
|
|
24390
|
-
return "0.0.0-beta.
|
|
24799
|
+
return "0.0.0-beta.40";
|
|
24391
24800
|
}
|
|
24392
24801
|
function printBanner(subtitle) {
|
|
24393
24802
|
const version$2 = getVersion();
|
|
@@ -24417,36 +24826,6 @@ function stripAnsi(str) {
|
|
|
24417
24826
|
return str.replace(/\u001B\[[0-9;]*m/g, "");
|
|
24418
24827
|
}
|
|
24419
24828
|
|
|
24420
|
-
//#endregion
|
|
24421
|
-
//#region src/lib/retry.ts
|
|
24422
|
-
async function withRetry(fn$2, maxRetries = 3) {
|
|
24423
|
-
let orgSwitched = false;
|
|
24424
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) try {
|
|
24425
|
-
return await fn$2();
|
|
24426
|
-
} catch (error) {
|
|
24427
|
-
if (!orgSwitched && isOrgMismatchError(error)) {
|
|
24428
|
-
if (applyOrgMismatchSwitch(error)) {
|
|
24429
|
-
orgSwitched = true;
|
|
24430
|
-
continue;
|
|
24431
|
-
}
|
|
24432
|
-
}
|
|
24433
|
-
if (attempt >= maxRetries) throw error;
|
|
24434
|
-
const rateLimit = isRateLimitError(error);
|
|
24435
|
-
if (rateLimit) {
|
|
24436
|
-
const waitMs = Math.min(rateLimit.retryAfterMs, 3e4);
|
|
24437
|
-
await new Promise((r) => {
|
|
24438
|
-
setTimeout(r, waitMs);
|
|
24439
|
-
});
|
|
24440
|
-
} else {
|
|
24441
|
-
const delay$2 = Math.min(1e3 * Math.pow(2, attempt), 1e4);
|
|
24442
|
-
await new Promise((r) => {
|
|
24443
|
-
setTimeout(r, delay$2);
|
|
24444
|
-
});
|
|
24445
|
-
}
|
|
24446
|
-
}
|
|
24447
|
-
throw new Error("Max retries exceeded");
|
|
24448
|
-
}
|
|
24449
|
-
|
|
24450
24829
|
//#endregion
|
|
24451
24830
|
//#region src/lib/deploy-core.ts
|
|
24452
24831
|
async function fetchDeployUrls(client, serviceId) {
|
|
@@ -24659,6 +25038,60 @@ async function deployMultipleServices(services, options) {
|
|
|
24659
25038
|
if (failed.length > 0 && !process.env.VELOZ_MCP) process.exit(1);
|
|
24660
25039
|
return results;
|
|
24661
25040
|
}
|
|
25041
|
+
const DASHBOARD_BASE$1 = process.env.VELOZ_WEB_URL || "https://app.onveloz.com";
|
|
25042
|
+
/**
|
|
25043
|
+
* Trigger deploys for 1-to-N services and return immediately, without waiting
|
|
25044
|
+
* for the build to finish. Uploads the source once per project, creates each
|
|
25045
|
+
* deployment, and starts its build, then reports the initial status.
|
|
25046
|
+
*
|
|
25047
|
+
* Used in MCP mode so an AI agent driving `veloz deploy` is not blocked for the
|
|
25048
|
+
* full build duration (builds can run for many minutes). The caller follows the
|
|
25049
|
+
* build to completion with `veloz builds logs <id>` / `veloz builds show <id>`.
|
|
25050
|
+
*/
|
|
25051
|
+
async function startDeploys(services, options) {
|
|
25052
|
+
const { projectRoot } = options;
|
|
25053
|
+
const client = await getClient();
|
|
25054
|
+
const sizeInBytes = await calculateDirectorySize(projectRoot);
|
|
25055
|
+
const sizeMB = Math.round(sizeInBytes / (1024 * 1024) * 10) / 10;
|
|
25056
|
+
const baseTarball = await createBaseTarball(projectRoot);
|
|
25057
|
+
const uniqueProjectIds = [...new Set(services.map((s) => s.projectId))];
|
|
25058
|
+
const objectKeyByProject = /* @__PURE__ */ new Map();
|
|
25059
|
+
try {
|
|
25060
|
+
await Promise.all(uniqueProjectIds.map(async (pid) => {
|
|
25061
|
+
const key = await withRetry(() => uploadSharedSource(client, pid, baseTarball.tarPath));
|
|
25062
|
+
objectKeyByProject.set(pid, key);
|
|
25063
|
+
}));
|
|
25064
|
+
} finally {
|
|
25065
|
+
await cleanupTarball(baseTarball);
|
|
25066
|
+
}
|
|
25067
|
+
info(`Upload concluído${sizeMB > 5 ? ` (${sizeMB} MB)` : ""}`);
|
|
25068
|
+
return await Promise.all(services.map(async (service$2) => {
|
|
25069
|
+
const deployment = await withRetry(() => client.deployments.create({
|
|
25070
|
+
serviceId: service$2.serviceId,
|
|
25071
|
+
serviceConfig: service$2.serviceConfig,
|
|
25072
|
+
config: service$2.config
|
|
25073
|
+
}));
|
|
25074
|
+
await withRetry(() => client.deployments.startBuild({
|
|
25075
|
+
deploymentId: deployment.id,
|
|
25076
|
+
objectKey: objectKeyByProject.get(service$2.projectId) ?? ""
|
|
25077
|
+
}));
|
|
25078
|
+
let status = "QUEUED";
|
|
25079
|
+
try {
|
|
25080
|
+
status = (await client.deployments.get({ deploymentId: deployment.id })).status;
|
|
25081
|
+
} catch {}
|
|
25082
|
+
const followCommand = `veloz builds show ${deployment.id}`;
|
|
25083
|
+
success(`Deploy iniciado para ${service$2.serviceName}. Acompanhe com: ${followCommand}`);
|
|
25084
|
+
return {
|
|
25085
|
+
deploymentId: deployment.id,
|
|
25086
|
+
status,
|
|
25087
|
+
logs: [],
|
|
25088
|
+
urls: [],
|
|
25089
|
+
serviceName: service$2.serviceName,
|
|
25090
|
+
dashboardUrl: `${DASHBOARD_BASE$1}/projetos/${service$2.projectId}/servicos/${service$2.serviceId}/deploys/${deployment.id}`,
|
|
25091
|
+
followCommand
|
|
25092
|
+
};
|
|
25093
|
+
}));
|
|
25094
|
+
}
|
|
24662
25095
|
/**
|
|
24663
25096
|
* Unified deploy function for 1-to-N services.
|
|
24664
25097
|
*
|
|
@@ -25511,7 +25944,7 @@ async function fetchLatestVersion() {
|
|
|
25511
25944
|
}
|
|
25512
25945
|
}
|
|
25513
25946
|
function getCurrentVersion() {
|
|
25514
|
-
return "0.0.0-beta.
|
|
25947
|
+
return "0.0.0-beta.40";
|
|
25515
25948
|
}
|
|
25516
25949
|
/**
|
|
25517
25950
|
* Install a specific CLI version. Returns true on success.
|
|
@@ -25785,6 +26218,40 @@ const SERVICE_TYPE_LABELS = {
|
|
|
25785
26218
|
WORKER: "Worker",
|
|
25786
26219
|
DATABASE: "Banco de Dados"
|
|
25787
26220
|
};
|
|
26221
|
+
/**
|
|
26222
|
+
* Detect-then-require a start command for long-lived process services (WEB/WORKER).
|
|
26223
|
+
*
|
|
26224
|
+
* A WEB/WORKER service with no resolvable start command dead-ends in a cryptic
|
|
26225
|
+
* "No start command could be found" deep in the build (or, worse, a service that
|
|
26226
|
+
* starts a dev server and crash-loops). We catch it before the upload: try local
|
|
26227
|
+
* framework detection and fill the command when found, otherwise fail fast with
|
|
26228
|
+
* an actionable message. STATIC/DATABASE services and Dockerfile builds (own CMD)
|
|
26229
|
+
* are exempt. Mutates `serviceConf.startCommand` in place when a command is found.
|
|
26230
|
+
*/
|
|
26231
|
+
function ensureStartCommand(serviceConf, configSnapshot, serviceName) {
|
|
26232
|
+
if (!serviceConf) return;
|
|
26233
|
+
const type = serviceConf.type;
|
|
26234
|
+
if (type !== "WEB" && type !== "WORKER") return;
|
|
26235
|
+
if (serviceConf.startCommand) return;
|
|
26236
|
+
if (configSnapshot?.build?.method === "dockerfile") return;
|
|
26237
|
+
const root = serviceConf.rootDirectory && serviceConf.rootDirectory !== "/" ? serviceConf.rootDirectory : ".";
|
|
26238
|
+
let analysis = null;
|
|
26239
|
+
try {
|
|
26240
|
+
analysis = detectLocalRepo$1(root);
|
|
26241
|
+
} catch {
|
|
26242
|
+
analysis = null;
|
|
26243
|
+
}
|
|
26244
|
+
const detected = analysis?.framework?.startCommand ?? null;
|
|
26245
|
+
if (detected) {
|
|
26246
|
+
serviceConf.startCommand = detected;
|
|
26247
|
+
info(`Comando de start detectado para ${chalk.bold(serviceName)}: ${chalk.dim(detected)}`);
|
|
26248
|
+
return;
|
|
26249
|
+
}
|
|
26250
|
+
const typeLabel = type === "WEB" ? "web" : "worker";
|
|
26251
|
+
const fw = analysis?.framework;
|
|
26252
|
+
if (fw && fw.type === "STATIC") throw new Error(`O serviço "${serviceName}" parece ser um site estático (${fw.name}), mas está configurado como "${typeLabel}". Defina "type": "static" no veloz.json para servi-lo como arquivos estáticos, ou defina "runtime.command" se ele roda um processo.`);
|
|
26253
|
+
throw new Error(`O serviço "${serviceName}" (${typeLabel}) não tem um comando de start e não foi possível detectar um automaticamente. Defina "runtime.command" no veloz.json (ex.: "node dist/index.js") e refaça o deploy.`);
|
|
26254
|
+
}
|
|
25788
26255
|
function prepareServicesForDeploy(services) {
|
|
25789
26256
|
const velozConfig = loadConfig$1();
|
|
25790
26257
|
const allWarnings = [];
|
|
@@ -25802,6 +26269,7 @@ function prepareServicesForDeploy(services) {
|
|
|
25802
26269
|
return services.map((svc) => {
|
|
25803
26270
|
const serviceConf = resolveServiceConf(velozConfig, svc.serviceId);
|
|
25804
26271
|
const configSnapshot = resolveServiceConfigSnapshot(velozConfig, svc.serviceId);
|
|
26272
|
+
ensureStartCommand(serviceConf, configSnapshot, svc.serviceName);
|
|
25805
26273
|
return {
|
|
25806
26274
|
serviceId: svc.serviceId,
|
|
25807
26275
|
serviceName: svc.serviceName,
|
|
@@ -25818,6 +26286,7 @@ async function triggerDeploy(serviceId, serviceName) {
|
|
|
25818
26286
|
const velozConfig = loadConfig$1();
|
|
25819
26287
|
const serviceConf = resolveServiceConf(velozConfig, serviceId);
|
|
25820
26288
|
const configSnapshot = resolveServiceConfigSnapshot(velozConfig, serviceId);
|
|
26289
|
+
ensureStartCommand(serviceConf, configSnapshot, serviceName ?? serviceId);
|
|
25821
26290
|
const warnings = runPreDeployChecks(serviceConf?.rootDirectory || ".");
|
|
25822
26291
|
if (warnings.length > 0) printDeployWarnings(warnings);
|
|
25823
26292
|
const spinUpload = spinner(serviceName ? `Fazendo upload ${chalk.bold(serviceName)}...` : "Fazendo upload do código...");
|
|
@@ -26635,7 +27104,8 @@ async function addServiceFlow(existingConfig, opts) {
|
|
|
26635
27104
|
}
|
|
26636
27105
|
function registerDeploy(cli$1) {
|
|
26637
27106
|
cli$1.command("deploy", {
|
|
26638
|
-
description: "Fazer deploy do serviço
|
|
27107
|
+
description: "Fazer deploy do serviço",
|
|
27108
|
+
mcp: MCP_DEPLOY,
|
|
26639
27109
|
middleware: [requireAuth$1],
|
|
26640
27110
|
options: z.object({
|
|
26641
27111
|
all: z.boolean().optional().describe("Deploy todos os serviços do monorepo"),
|
|
@@ -26846,7 +27316,20 @@ async function* headlessDeployFlow(opts) {
|
|
|
26846
27316
|
}
|
|
26847
27317
|
servicesToProcess = matched;
|
|
26848
27318
|
}
|
|
26849
|
-
const
|
|
27319
|
+
const servicesToDeploy = await prepareServicesForDeploy(servicesToProcess);
|
|
27320
|
+
if (!opts.wait) {
|
|
27321
|
+
const triggered = await startDeploys(servicesToDeploy, {
|
|
27322
|
+
projectRoot: process.cwd(),
|
|
27323
|
+
output: createDeployOutput()
|
|
27324
|
+
});
|
|
27325
|
+
for (const result$2 of triggered) yield {
|
|
27326
|
+
type: "result",
|
|
27327
|
+
message: `Deploy iniciado (${result$2.status}). O build está em andamento e pode levar alguns minutos. Acompanhe o status com "veloz builds show ${result$2.deploymentId}" (repita até o status ser terminal: LIVE, BUILD_FAILED, DEPLOY_FAILED, FAILED ou CANCELLED) e veja os logs com "veloz builds logs ${result$2.deploymentId}".`,
|
|
27328
|
+
data: result$2
|
|
27329
|
+
};
|
|
27330
|
+
return;
|
|
27331
|
+
}
|
|
27332
|
+
const results = await deployServices(servicesToDeploy, {
|
|
26850
27333
|
projectRoot: process.cwd(),
|
|
26851
27334
|
output: createDeployOutput()
|
|
26852
27335
|
});
|
|
@@ -27258,6 +27741,7 @@ function registerUse(cli$1) {
|
|
|
27258
27741
|
function registerWhoami(cli$1) {
|
|
27259
27742
|
cli$1.command("whoami", {
|
|
27260
27743
|
description: "Mostrar usuário e workspace ativo",
|
|
27744
|
+
mcp: MCP_READ_ONLY,
|
|
27261
27745
|
middleware: [requireAuth$1],
|
|
27262
27746
|
output: z.object({
|
|
27263
27747
|
name: z.string(),
|
|
@@ -27510,6 +27994,7 @@ async function pruneRemovedEntries(config, existingConfig, services, databases)
|
|
|
27510
27994
|
function registerUpdate(cli$1) {
|
|
27511
27995
|
cli$1.command("update", {
|
|
27512
27996
|
description: "Atualizar a CLI para a versão mais recente",
|
|
27997
|
+
mcp: false,
|
|
27513
27998
|
output: z.object({
|
|
27514
27999
|
updated: z.boolean(),
|
|
27515
28000
|
version: z.string().nullable()
|
|
@@ -30599,7 +31084,7 @@ function PickerMenu({ items, onSelect, onCancel }) {
|
|
|
30599
31084
|
|
|
30600
31085
|
//#endregion
|
|
30601
31086
|
//#region src/wizard/ui/primitives/ScreenLayout.tsx
|
|
30602
|
-
const version = "0.0.0-beta.
|
|
31087
|
+
const version = "0.0.0-beta.40";
|
|
30603
31088
|
const LOGO_LINES = [
|
|
30604
31089
|
"██╗ ██╗███████╗██╗ ██████╗ ███████╗",
|
|
30605
31090
|
"██║ ██║██╔════╝██║ ██╔═══██╗╚══███╔╝",
|
|
@@ -34602,6 +35087,7 @@ function detectGitRemote(cwd) {
|
|
|
34602
35087
|
function registerInit(cli$1) {
|
|
34603
35088
|
cli$1.command("init", {
|
|
34604
35089
|
description: "Wizard de deploy — configura e faz deploy do projeto automaticamente",
|
|
35090
|
+
mcp: false,
|
|
34605
35091
|
options: z.object({
|
|
34606
35092
|
framework: z.string().optional().describe("Forçar framework específico (ex: nextjs-app-router, express)"),
|
|
34607
35093
|
ci: z.boolean().default(false).describe("Modo CI — pula o wizard de IA e faz deploy direto via veloz deploy")
|
|
@@ -34612,7 +35098,8 @@ function registerInit(cli$1) {
|
|
|
34612
35098
|
},
|
|
34613
35099
|
output: z.object({
|
|
34614
35100
|
success: z.boolean(),
|
|
34615
|
-
url: z.string().optional()
|
|
35101
|
+
url: z.string().optional(),
|
|
35102
|
+
hint: z.string().optional()
|
|
34616
35103
|
}),
|
|
34617
35104
|
outputPolicy: "agent-only",
|
|
34618
35105
|
async run(c) {
|
|
@@ -34646,6 +35133,16 @@ function registerInit(cli$1) {
|
|
|
34646
35133
|
}
|
|
34647
35134
|
async function runInitFlow(c, logger, cwd) {
|
|
34648
35135
|
const forcedFramework = c.options.framework;
|
|
35136
|
+
if (isAiAgent() && !c.options.ci) {
|
|
35137
|
+
logger.log("mode", "AI agent detected — skipping wizard, guiding to veloz deploy");
|
|
35138
|
+
await ensureSkillForAgent(logger);
|
|
35139
|
+
info("Detectado agente de IA. O wizard de IA foi ignorado.");
|
|
35140
|
+
info("Para fazer deploy, execute: veloz deploy");
|
|
35141
|
+
return {
|
|
35142
|
+
success: true,
|
|
35143
|
+
hint: "Ambiente de agente de IA detectado. Execute `veloz deploy` para fazer deploy. A skill da Veloz foi instalada em .claude/skills/ com a referência completa de comandos."
|
|
35144
|
+
};
|
|
35145
|
+
}
|
|
34649
35146
|
const analysis = detectLocalRepo(cwd);
|
|
34650
35147
|
logger.log("detection", "repo analysis complete", {
|
|
34651
35148
|
framework: analysis.framework?.name ?? null,
|
|
@@ -35004,6 +35501,29 @@ async function pollGithubInstallation(_store, owner) {
|
|
|
35004
35501
|
return null;
|
|
35005
35502
|
}
|
|
35006
35503
|
/**
|
|
35504
|
+
* Install the Veloz agent skill into `.claude/skills/` for an AI agent that ran
|
|
35505
|
+
* `veloz init`, unless it is already present. Best-effort: a failure is surfaced
|
|
35506
|
+
* as a hint, never fatal — the agent can still run `veloz deploy`.
|
|
35507
|
+
*/
|
|
35508
|
+
async function ensureSkillForAgent(logger) {
|
|
35509
|
+
if (checkAiSetup().skillsInstalled) {
|
|
35510
|
+
logger.log("ai-setup", "skill already installed — skipping");
|
|
35511
|
+
return;
|
|
35512
|
+
}
|
|
35513
|
+
const result$2 = await runVelozSubcommand([
|
|
35514
|
+
"skills",
|
|
35515
|
+
"add",
|
|
35516
|
+
"--no-global"
|
|
35517
|
+
]);
|
|
35518
|
+
if (result$2.ok) {
|
|
35519
|
+
logger.log("ai-setup", "skills add succeeded");
|
|
35520
|
+
success("Skill da Veloz instalada em .claude/skills/");
|
|
35521
|
+
} else {
|
|
35522
|
+
logger.log("ai-setup", "skills add failed", { stderr: result$2.stderr });
|
|
35523
|
+
warn("Não foi possível instalar a skill. Execute manualmente: veloz skills add");
|
|
35524
|
+
}
|
|
35525
|
+
}
|
|
35526
|
+
/**
|
|
35007
35527
|
* In CI / non-TTY contexts (real CI runners, MCP tool calls, Claude Code's
|
|
35008
35528
|
* bash tool), `veloz init` delegates to the deterministic deploy flow rather
|
|
35009
35529
|
* than spawning a Claude Code agent. The deploy flow auto-bootstraps the
|
|
@@ -35032,7 +35552,8 @@ async function runHeadless(opts) {
|
|
|
35032
35552
|
let url;
|
|
35033
35553
|
for await (const event of headlessDeployFlow({
|
|
35034
35554
|
all: true,
|
|
35035
|
-
yes: true
|
|
35555
|
+
yes: true,
|
|
35556
|
+
wait: true
|
|
35036
35557
|
})) if (event.type === "result") {
|
|
35037
35558
|
const data = event.data;
|
|
35038
35559
|
if (data?.status === "LIVE") {
|
|
@@ -35229,13 +35750,18 @@ async function runOrgCreationFlow(store, apiUrl) {
|
|
|
35229
35750
|
//#region src/index.ts
|
|
35230
35751
|
if (process.argv.includes("--mcp")) process.env.VELOZ_MCP = "true";
|
|
35231
35752
|
const cli = Cli.create("veloz", {
|
|
35232
|
-
version: "0.0.0-beta.
|
|
35753
|
+
version: "0.0.0-beta.40",
|
|
35233
35754
|
description: "CLI da plataforma Veloz — deploy rápido para o Brasil",
|
|
35234
35755
|
env: z.object({ VELOZ_ENV: z.string().optional().describe("Ambiente alvo (ex: preview, staging)") }),
|
|
35235
|
-
|
|
35756
|
+
globals: z.object({ env: z.string().optional().describe("Ambiente alvo (ex: preview, staging)") }),
|
|
35757
|
+
globalAlias: { env: "e" },
|
|
35758
|
+
mcp: {
|
|
35759
|
+
command: "npx -y onveloz --mcp",
|
|
35760
|
+
instructions: MCP_SERVER_INSTRUCTIONS
|
|
35761
|
+
}
|
|
35236
35762
|
});
|
|
35237
35763
|
cli.use(async (c, next) => {
|
|
35238
|
-
const env = c.env.VELOZ_ENV;
|
|
35764
|
+
const env = c.globals.env ?? c.env.VELOZ_ENV;
|
|
35239
35765
|
if (env) setActiveEnv(env);
|
|
35240
35766
|
await next();
|
|
35241
35767
|
});
|
|
@@ -35329,6 +35855,7 @@ cli.command(projectsGroup);
|
|
|
35329
35855
|
cli.command(orgsGroup);
|
|
35330
35856
|
cli.command(envGroup);
|
|
35331
35857
|
cli.command(domainsGroup);
|
|
35858
|
+
cli.command(emailGroup);
|
|
35332
35859
|
cli.command(volumesGroup);
|
|
35333
35860
|
cli.command(configGroup);
|
|
35334
35861
|
cli.command(apikeyGroup);
|