onveloz 0.0.0-beta.31 → 0.0.0-beta.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.mjs +2691 -628
  2. package/package.json +1 -1
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, execSync, spawn } from "node:child_process";
4
4
  import * as fs from "node:fs";
5
5
  import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
6
6
  import * as path from "node:path";
@@ -682,6 +682,14 @@ function mergeServiceWithDefaults(service$2, defaults) {
682
682
  function parseVelozConfig(data) {
683
683
  return VelozConfigSchema.parse(data);
684
684
  }
685
+ function validateVelozConfig(data) {
686
+ const result$2 = VelozConfigSchema.safeParse(data);
687
+ if (!result$2.success) return {
688
+ success: false,
689
+ error: result$2.error.issues.map((e) => `${String(e.path.join("."))}: ${e.message}`).join("; ")
690
+ };
691
+ return { success: true };
692
+ }
685
693
  function resolveConfigForEnv(config, env) {
686
694
  const envOverride = config.environments?.[env];
687
695
  if (!envOverride) return null;
@@ -1191,9 +1199,11 @@ function ensureConfigDir() {
1191
1199
  if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
1192
1200
  }
1193
1201
  function loadConfig() {
1202
+ const envOrgId = process.env.VELOZ_ORG_ID || void 0;
1194
1203
  if (!existsSync(CONFIG_FILE)) return {
1195
1204
  apiKey: "",
1196
- apiUrl: DEFAULT_API_URL
1205
+ apiUrl: DEFAULT_API_URL,
1206
+ organizationId: envOrgId
1197
1207
  };
1198
1208
  try {
1199
1209
  const raw = readFileSync(CONFIG_FILE, "utf-8");
@@ -1201,12 +1211,13 @@ function loadConfig() {
1201
1211
  return {
1202
1212
  apiKey: parsed.apiKey ?? "",
1203
1213
  apiUrl: ENV_API_URL ?? parsed.apiUrl ?? DEFAULT_API_URL,
1204
- organizationId: parsed.organizationId
1214
+ organizationId: envOrgId ?? parsed.organizationId
1205
1215
  };
1206
1216
  } catch {
1207
1217
  return {
1208
1218
  apiKey: "",
1209
- apiUrl: DEFAULT_API_URL
1219
+ apiUrl: DEFAULT_API_URL,
1220
+ organizationId: envOrgId
1210
1221
  };
1211
1222
  }
1212
1223
  }
@@ -1249,23 +1260,58 @@ async function requireAuth(options) {
1249
1260
 
1250
1261
  //#endregion
1251
1262
  //#region src/lib/client.ts
1263
+ const CLI_VERSION = "0.0.0-beta.32";
1264
+ const USER_AGENT = `veloz-cli/${CLI_VERSION}`;
1265
+ /**
1266
+ * Client source — "cli" by default; the init wizard sets this to "cli-wizard"
1267
+ * so server can flag deployments/audit records as coming from `veloz init`.
1268
+ */
1269
+ let clientSource = "cli";
1270
+ function setClientSource(source) {
1271
+ clientSource = source;
1272
+ }
1273
+ /**
1274
+ * Single source of truth for headers the CLI sends to the Veloz server.
1275
+ *
1276
+ * Every ORPC request (via getClient) and every raw fetch against the Veloz
1277
+ * backend (Better Auth endpoints, presigned URL negotiations) must route
1278
+ * through this builder. Any new header — workspace scope, project scope,
1279
+ * tenant hints, user-agent — is added here and nowhere else.
1280
+ */
1281
+ function buildVelozHeaders(authConfig) {
1282
+ const headers = {
1283
+ Authorization: `Bearer ${authConfig.apiKey}`,
1284
+ "User-Agent": USER_AGENT,
1285
+ "X-Veloz-Cli-Version": CLI_VERSION,
1286
+ "X-Veloz-Client-Source": clientSource
1287
+ };
1288
+ const projectConfig = loadConfig$1();
1289
+ if (authConfig.organizationId) headers["X-Organization-Id"] = authConfig.organizationId;
1290
+ if (projectConfig?.project?.id) headers["X-Project-Id"] = projectConfig.project.id;
1291
+ return headers;
1292
+ }
1293
+ /**
1294
+ * Build headers from the current on-disk config. Use when you have a raw fetch
1295
+ * and you know auth has already been enforced by an upstream middleware.
1296
+ */
1297
+ async function getAuthHeaders() {
1298
+ return buildVelozHeaders(await requireAuth());
1299
+ }
1252
1300
  async function getClient() {
1253
1301
  const authConfig = await requireAuth();
1254
- const projectConfig = loadConfig$1();
1255
- return createClient(authConfig.apiUrl, () => {
1256
- const headers = {
1257
- Authorization: `Bearer ${authConfig.apiKey}`,
1258
- "User-Agent": "veloz-cli"
1259
- };
1260
- if (authConfig.organizationId) headers["X-Organization-Id"] = authConfig.organizationId;
1261
- if (projectConfig?.project?.id) headers["X-Project-Id"] = projectConfig.project.id;
1262
- return headers;
1263
- });
1302
+ return createClient(authConfig.apiUrl, () => buildVelozHeaders(authConfig));
1264
1303
  }
1265
1304
 
1266
1305
  //#endregion
1267
1306
  //#region src/lib/middleware.ts
1307
+ function isDryRun() {
1308
+ return process.argv.includes("--dry-run");
1309
+ }
1268
1310
  const requireAuth$1 = middleware(async (c, next) => {
1311
+ if (isDryRun()) {
1312
+ await next();
1313
+ return;
1314
+ }
1269
1315
  const config = loadConfig();
1270
1316
  if (!(process.env.VELOZ_API_KEY || config?.apiKey)) {
1271
1317
  if (!process.stdout.isTTY) return c.error({
@@ -1315,6 +1361,123 @@ projectsGroup.command("list", {
1315
1361
  }
1316
1362
  });
1317
1363
 
1364
+ //#endregion
1365
+ //#region src/commands/orgs.ts
1366
+ const orgsGroup = Cli.create("orgs", { description: "Gerenciar workspaces (organizações) do usuário" });
1367
+ orgsGroup.command("list", {
1368
+ description: "Listar workspaces que você pertence",
1369
+ middleware: [requireAuth$1],
1370
+ output: z.object({ items: z.array(z.object({
1371
+ id: z.string(),
1372
+ name: z.string(),
1373
+ slug: z.string(),
1374
+ role: z.string(),
1375
+ isActive: z.boolean()
1376
+ })) }),
1377
+ async run() {
1378
+ const client = await getClient();
1379
+ const orgs = await withSpinner({
1380
+ text: "Carregando workspaces...",
1381
+ fn: () => client.organizations.list()
1382
+ });
1383
+ if (orgs.length === 0) {
1384
+ info("Nenhum workspace encontrado. Crie um em app.onveloz.com/onboarding.");
1385
+ return { items: [] };
1386
+ }
1387
+ return { items: orgs };
1388
+ }
1389
+ });
1390
+ orgsGroup.command("use", {
1391
+ description: "Selecionar qual workspace usar como padrão",
1392
+ args: z.object({ organizacao: z.string().optional().describe("Nome, slug ou ID do workspace") }),
1393
+ middleware: [requireAuth$1],
1394
+ output: z.object({
1395
+ id: z.string(),
1396
+ name: z.string(),
1397
+ slug: z.string(),
1398
+ role: z.string()
1399
+ }),
1400
+ async run(c) {
1401
+ const client = await getClient();
1402
+ const orgs = await withSpinner({
1403
+ text: "Carregando workspaces...",
1404
+ fn: () => client.organizations.list()
1405
+ });
1406
+ if (orgs.length === 0) throw new Error("Nenhum workspace encontrado. Crie um em app.onveloz.com/onboarding.");
1407
+ const current = loadConfig().organizationId;
1408
+ let selectedId;
1409
+ if (c.args.organizacao) {
1410
+ const query = c.args.organizacao.toLowerCase();
1411
+ const found = orgs.find((o) => o.id === c.args.organizacao || o.slug.toLowerCase() === query || o.name.toLowerCase() === query);
1412
+ if (!found) {
1413
+ const available = orgs.map((o) => ` • ${o.name} (${o.slug})`).join("\n");
1414
+ throw new Error(`Workspace '${c.args.organizacao}' não encontrado.\n\nWorkspaces disponíveis:\n${available}`);
1415
+ }
1416
+ selectedId = found.id;
1417
+ } else selectedId = await promptSelect("Qual workspace usar como padrão?", orgs.map((o) => ({
1418
+ label: `${o.name} ${chalk.dim(`(${o.slug})`)}${o.id === current ? chalk.cyan(" ← atual") : ""}`,
1419
+ value: o.id
1420
+ })));
1421
+ const selected = orgs.find((o) => o.id === selectedId);
1422
+ await client.organizations.setActive({ organizationId: selected.id });
1423
+ saveConfig({ organizationId: selected.id });
1424
+ success(`Workspace padrão: ${chalk.bold(selected.name)} (${selected.slug})`);
1425
+ return c.ok({
1426
+ id: selected.id,
1427
+ name: selected.name,
1428
+ slug: selected.slug,
1429
+ role: selected.role
1430
+ }, { cta: { commands: [{
1431
+ command: "projects list",
1432
+ description: "Listar projetos do workspace"
1433
+ }, {
1434
+ command: "whoami",
1435
+ description: "Ver usuário e workspace ativo"
1436
+ }] } });
1437
+ }
1438
+ });
1439
+
1440
+ //#endregion
1441
+ //#region src/lib/org-resolver.ts
1442
+ /**
1443
+ * Ensure an active organization is selected for the current CLI session.
1444
+ *
1445
+ * Short-circuits when:
1446
+ * - config already has organizationId
1447
+ * - a project is linked in the current directory (org inferred server-side from X-Project-Id)
1448
+ *
1449
+ * Otherwise, fetches the user's orgs and prompts when interactive. Non-interactive
1450
+ * multi-org calls throw with a CTA pointing at `veloz orgs use`.
1451
+ */
1452
+ async function resolveOrgIfNeeded(options) {
1453
+ if (loadConfig().organizationId) return;
1454
+ if (loadConfig$1()?.project?.id) return;
1455
+ const client = await getClient();
1456
+ const orgs = await withSpinner({
1457
+ text: "Carregando workspaces...",
1458
+ fn: () => client.organizations.list()
1459
+ });
1460
+ if (orgs.length === 0) return;
1461
+ if (orgs.length === 1) {
1462
+ const org = orgs[0];
1463
+ saveConfig({ organizationId: org.id });
1464
+ info(`Workspace: ${chalk.bold(org.name)}`);
1465
+ return;
1466
+ }
1467
+ if (options?.nonInteractive || !isInteractive()) {
1468
+ const available = orgs.map((o) => ` • ${o.name} (${o.slug})`).join("\n");
1469
+ throw new Error(`Múltiplos workspaces encontrados. Selecione um com \`veloz orgs use <slug>\`.\n\nWorkspaces disponíveis:\n${available}`);
1470
+ }
1471
+ info(`Você tem acesso a ${orgs.length} workspaces.`);
1472
+ const selectedId = await promptSelect("Selecione o workspace padrão:", orgs.map((o) => ({
1473
+ label: `${o.name} ${chalk.dim(`(${o.slug})`)}`,
1474
+ value: o.id
1475
+ })));
1476
+ saveConfig({ organizationId: selectedId });
1477
+ const selected = orgs.find((o) => o.id === selectedId);
1478
+ info(`Workspace: ${chalk.bold(selected?.name ?? selectedId)}`);
1479
+ }
1480
+
1318
1481
  //#endregion
1319
1482
  //#region src/lib/project-resolver.ts
1320
1483
  const CACHE_FILE = join(homedir(), ".veloz", "directory-cache.json");
@@ -1354,39 +1517,6 @@ function getGlobalProjectFlag() {
1354
1517
  const idx = process.argv.indexOf("--project");
1355
1518
  if (idx !== -1 && idx + 1 < process.argv.length) return process.argv[idx + 1];
1356
1519
  }
1357
- function isMultipleOrgsError$1(error) {
1358
- if (!(error instanceof Error)) return false;
1359
- const e = error;
1360
- return e.data?.code === "MULTIPLE_ORGS" && Array.isArray(e.data?.organizations);
1361
- }
1362
- /**
1363
- * Fetch projects, handling MULTIPLE_ORGS by prompting for org selection first.
1364
- */
1365
- async function fetchProjectsWithOrgResolution(client) {
1366
- try {
1367
- return await client.projects.list();
1368
- } catch (error) {
1369
- if (!isMultipleOrgsError$1(error)) throw error;
1370
- const orgs = error.data.organizations;
1371
- if (orgs.length === 1) {
1372
- const org = orgs[0];
1373
- saveConfig({ organizationId: org.id });
1374
- info(`Workspace: ${org.name}`);
1375
- return client.projects.list();
1376
- }
1377
- if (!isInteractive()) {
1378
- const available = orgs.map((o) => ` • ${o.name} (${o.slug})`).join("\n");
1379
- throw new Error(`Múltiplos workspaces encontrados. Execute 'veloz login' para selecionar um.\n\nWorkspaces:\n${available}`);
1380
- }
1381
- const selectedOrgId = await promptSelect("Qual workspace?", orgs.map((o) => ({
1382
- label: `${o.name} (${o.slug})`,
1383
- value: o.id
1384
- })));
1385
- saveConfig({ organizationId: selectedOrgId });
1386
- info(`Workspace: ${orgs.find((o) => o.id === selectedOrgId)?.name ?? selectedOrgId}`);
1387
- return client.projects.list();
1388
- }
1389
- }
1390
1520
  /**
1391
1521
  * Resolve a project ID when no veloz.json is present.
1392
1522
  *
@@ -1400,10 +1530,11 @@ async function fetchProjectsWithOrgResolution(client) {
1400
1530
  async function resolveProjectFromAPI(explicitProject) {
1401
1531
  const projectFlag = explicitProject ?? getGlobalProjectFlag();
1402
1532
  if (projectFlag) {
1533
+ await resolveOrgIfNeeded();
1403
1534
  const client$1 = await getClient();
1404
1535
  const projects$1 = await withSpinner({
1405
1536
  text: "Carregando projetos...",
1406
- fn: () => fetchProjectsWithOrgResolution(client$1)
1537
+ fn: () => client$1.projects.list()
1407
1538
  });
1408
1539
  const match$3 = projects$1.find((p) => p.id === projectFlag || p.slug === projectFlag || p.name === projectFlag);
1409
1540
  if (!match$3) {
@@ -1418,10 +1549,11 @@ async function resolveProjectFromAPI(explicitProject) {
1418
1549
  }
1419
1550
  const cached$2 = getCachedProjectId();
1420
1551
  if (cached$2) return cached$2;
1552
+ await resolveOrgIfNeeded();
1421
1553
  const client = await getClient();
1422
1554
  const projects = await withSpinner({
1423
1555
  text: "Carregando projetos...",
1424
- fn: () => fetchProjectsWithOrgResolution(client)
1556
+ fn: () => client.projects.list()
1425
1557
  });
1426
1558
  if (projects.length === 0) throw new Error("Nenhum projeto encontrado. Execute 'veloz deploy' para criar seu primeiro projeto.");
1427
1559
  if (projects.length === 1) {
@@ -1752,7 +1884,7 @@ async function resolveProjectId(projectFlag) {
1752
1884
 
1753
1885
  //#endregion
1754
1886
  //#region src/commands/env.ts
1755
- const envGroup = Cli.create("env", { description: "Gerenciar variáveis de ambiente" });
1887
+ 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}})." });
1756
1888
  envGroup.command("list", {
1757
1889
  description: "Listar variáveis de ambiente",
1758
1890
  middleware: [requireAuth$1],
@@ -1787,10 +1919,15 @@ envGroup.command("list", {
1787
1919
  });
1788
1920
  continue;
1789
1921
  }
1922
+ let anyInterpolated = false;
1790
1923
  for (const v of envVars) {
1791
1924
  const displayValue = v.value ?? v.maskedValue;
1792
- console.log(` ${chalk.bold(v.key)} ${chalk.dim(displayValue)} ${new Date(v.updatedAt).toLocaleDateString("pt-BR")}`);
1925
+ const hasRef = typeof v.value === "string" && /\$\{[A-Za-z_][A-Za-z0-9_]*\}/.test(v.value);
1926
+ if (hasRef) anyInterpolated = true;
1927
+ const suffix = hasRef ? chalk.cyan(" ·interpolada") : "";
1928
+ console.log(` ${chalk.bold(v.key)} ${chalk.dim(displayValue)}${suffix} ${new Date(v.updatedAt).toLocaleDateString("pt-BR")}`);
1793
1929
  }
1930
+ if (anyInterpolated) console.log(chalk.dim(" Valores com ${VAR} são resolvidos no deploy contra as outras vars do serviço."));
1794
1931
  result$2.push({
1795
1932
  serviceName: service$2.name,
1796
1933
  vars: envVars.map((v) => ({
@@ -1806,7 +1943,7 @@ envGroup.command("list", {
1806
1943
  }
1807
1944
  });
1808
1945
  envGroup.command("set", {
1809
- description: "Definir variável de ambiente (formato: CHAVE=VALOR)",
1946
+ description: "Definir variável de ambiente (formato: CHAVE=VALOR). O valor pode referenciar outras variáveis do mesmo serviço com ${OUTRA_VAR} — a substituição acontece no deploy. Use $${LITERAL} para preservar um ${...} literal. Ex.: DATABASE_URL='${POSTGRES_POOLER_URL}?sslmode=require'.",
1810
1947
  middleware: [requireAuth$1],
1811
1948
  args: z.object({ pares: z.string().describe("Pares CHAVE=VALOR separados por espaço") }),
1812
1949
  options: z.object({
@@ -1889,7 +2026,7 @@ envGroup.command("delete", {
1889
2026
  }
1890
2027
  });
1891
2028
  envGroup.command("import", {
1892
- description: "Importar variáveis de ambiente de um arquivo .env ou colar diretamente",
2029
+ 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.",
1893
2030
  middleware: [requireAuth$1],
1894
2031
  args: z.object({ arquivo: z.string().optional().describe("Caminho do arquivo .env") }),
1895
2032
  options: z.object({
@@ -2722,7 +2859,7 @@ async function authFetch(path$1, options = {}) {
2722
2859
  ...options,
2723
2860
  headers: {
2724
2861
  "Content-Type": "application/json",
2725
- Authorization: `Bearer ${config.apiKey}`,
2862
+ ...buildVelozHeaders(config),
2726
2863
  ...options.headers
2727
2864
  }
2728
2865
  });
@@ -5136,7 +5273,7 @@ const TERMINAL_STATUSES = new Set([
5136
5273
 
5137
5274
  //#endregion
5138
5275
  //#region src/lib/deploy-stream.ts
5139
- const DASHBOARD_BASE = process.env.VELOZ_WEB_URL || "https://app.onveloz.com";
5276
+ const DASHBOARD_BASE$1 = process.env.VELOZ_WEB_URL || "https://app.onveloz.com";
5140
5277
  /**
5141
5278
  * Fetch deployment details from the API and enrich a DeployStreamResult
5142
5279
  * with metadata useful for agents (failReason, commit info, duration, dashboard link).
@@ -5151,7 +5288,11 @@ async function enrichResult(client, result$2) {
5151
5288
  result$2.commitSha = d.commitSha ?? null;
5152
5289
  result$2.branch = d.branch ?? null;
5153
5290
  if (d.startedAt && d.finishedAt) result$2.durationSeconds = Math.round((new Date(d.finishedAt).getTime() - new Date(d.startedAt).getTime()) / 1e3);
5154
- if (projectId) result$2.dashboardUrl = `${DASHBOARD_BASE}/projetos/${projectId}/servicos/${d.serviceId}/deploys/${d.id}`;
5291
+ if (projectId) {
5292
+ result$2.dashboardUrl = `${DASHBOARD_BASE$1}/projetos/${projectId}/servicos/${d.serviceId}/deploys/${d.id}`;
5293
+ const project = await client.projects.get({ projectId }).catch(() => null);
5294
+ if (project) result$2.organizationId = project.organizationId ?? null;
5295
+ }
5155
5296
  } catch {}
5156
5297
  return result$2;
5157
5298
  }
@@ -24080,7 +24221,7 @@ const LOGO_LINES$1 = [
24080
24221
  ];
24081
24222
  const BRAND_COLOR$1 = "#FF4D00";
24082
24223
  function getVersion() {
24083
- return "0.0.0-beta.31";
24224
+ return "0.0.0-beta.32";
24084
24225
  }
24085
24226
  function printBanner(subtitle) {
24086
24227
  const version$2 = getVersion();
@@ -24704,6 +24845,16 @@ function createDeployOutput() {
24704
24845
 
24705
24846
  //#endregion
24706
24847
  //#region src/lib/deploy-config.ts
24848
+ /**
24849
+ * Resolve the full ServiceConfig snapshot for a service id — the SSOT that
24850
+ * gets stored on the Deployment row. Preserves all veloz.json fields
24851
+ * (build.method, dockerfile, context, runtime.healthCheck, autoscale, env,
24852
+ * watch, ignore, …) that the flat `DeploymentServiceConfigInput` drops.
24853
+ */
24854
+ function resolveServiceConfigSnapshot(velozConfig, serviceId) {
24855
+ if (!velozConfig) return void 0;
24856
+ for (const [, conf] of Object.entries(velozConfig.services)) if (conf.id === serviceId) return mergeServiceWithDefaults(conf, velozConfig.defaults);
24857
+ }
24707
24858
  function resolveServiceConf(velozConfig, serviceId) {
24708
24859
  if (!velozConfig) return void 0;
24709
24860
  for (const [, conf] of Object.entries(velozConfig.services)) if (conf.id === serviceId) {
@@ -25176,7 +25327,7 @@ async function fetchLatestVersion() {
25176
25327
  }
25177
25328
  }
25178
25329
  function getCurrentVersion() {
25179
- return "0.0.0-beta.31";
25330
+ return "0.0.0-beta.32";
25180
25331
  }
25181
25332
  /**
25182
25333
  * Install a specific CLI version. Returns true on success.
@@ -25234,6 +25385,7 @@ async function provisionDatabases(config, opts) {
25234
25385
  const entries = Object.entries(databases);
25235
25386
  if (entries.length === 0) return config;
25236
25387
  const projectId = config.project.id;
25388
+ if (!projectId) throw new Error("Projeto ainda não foi vinculado. Execute 'veloz deploy' primeiro.");
25237
25389
  const client = await getClient();
25238
25390
  const serverDatabases = await withSpinner({
25239
25391
  text: "Verificando bancos de dados...",
@@ -25465,11 +25617,13 @@ function prepareServicesForDeploy(services) {
25465
25617
  }
25466
25618
  return services.map((svc) => {
25467
25619
  const serviceConf = resolveServiceConf(velozConfig, svc.serviceId);
25620
+ const configSnapshot = resolveServiceConfigSnapshot(velozConfig, svc.serviceId);
25468
25621
  return {
25469
25622
  serviceId: svc.serviceId,
25470
25623
  serviceName: svc.serviceName,
25471
25624
  projectId: svc.projectId,
25472
- serviceConfig: serviceConf
25625
+ serviceConfig: serviceConf,
25626
+ config: configSnapshot
25473
25627
  };
25474
25628
  });
25475
25629
  }
@@ -25479,6 +25633,7 @@ async function triggerDeploy(serviceId, serviceName) {
25479
25633
  const client = await getClient();
25480
25634
  const velozConfig = loadConfig$1();
25481
25635
  const serviceConf = resolveServiceConf(velozConfig, serviceId);
25636
+ const configSnapshot = resolveServiceConfigSnapshot(velozConfig, serviceId);
25482
25637
  const warnings = runPreDeployChecks(serviceConf?.rootDirectory || ".");
25483
25638
  if (warnings.length > 0) printDeployWarnings(warnings);
25484
25639
  const spinUpload = spinner(serviceName ? `Fazendo upload ${chalk.bold(serviceName)}...` : "Fazendo upload do código...");
@@ -25486,7 +25641,8 @@ async function triggerDeploy(serviceId, serviceName) {
25486
25641
  spinUpload.text = "Iniciando deploy...";
25487
25642
  const deployment = await withRetry(() => client.deployments.create({
25488
25643
  serviceId,
25489
- serviceConfig: serviceConf
25644
+ serviceConfig: serviceConf,
25645
+ config: configSnapshot
25490
25646
  }));
25491
25647
  spinUpload.text = "Fazendo upload do código...";
25492
25648
  await withRetry(() => uploadSource(deployment.id, process.cwd()));
@@ -26292,36 +26448,6 @@ async function addServiceFlow(existingConfig, opts) {
26292
26448
  await provisionDatabases(updatedConfig, { yes: opts.yes ?? false });
26293
26449
  await triggerDeploy(service$2.id, service$2.name);
26294
26450
  }
26295
- function isMultipleOrgsError(error) {
26296
- if (!(error instanceof Error)) return false;
26297
- const e = error;
26298
- return e.data?.code === "MULTIPLE_ORGS" && Array.isArray(e.data?.organizations);
26299
- }
26300
- async function resolveOrgIfNeeded(nonInteractive) {
26301
- if ((await requireAuth({ nonInteractive })).organizationId) return;
26302
- if (loadConfig$1()?.project?.id) return;
26303
- const client = await getClient();
26304
- try {
26305
- await client.projects.list();
26306
- } catch (error) {
26307
- if (!isMultipleOrgsError(error)) throw error;
26308
- const orgs = error.data.organizations;
26309
- if (orgs.length === 1) {
26310
- const org = orgs[0];
26311
- saveConfig({ organizationId: org.id });
26312
- info(`Workspace: ${chalk.bold(org.name)}`);
26313
- return;
26314
- }
26315
- info(`Você tem acesso a ${orgs.length} workspaces.`);
26316
- const selectedOrgId = await promptSelect("Selecione o workspace para este deploy:", orgs.map((org) => ({
26317
- label: `${org.name} ${chalk.dim(`(${org.slug})`)}`,
26318
- value: org.id
26319
- })));
26320
- const selectedOrg = orgs.find((o) => o.id === selectedOrgId);
26321
- saveConfig({ organizationId: selectedOrgId });
26322
- info(`Workspace: ${chalk.bold(selectedOrg?.name ?? selectedOrgId)}`);
26323
- }
26324
- }
26325
26451
  function registerDeploy(cli$1) {
26326
26452
  cli$1.command("deploy", {
26327
26453
  description: "Fazer deploy do serviço. Returns streamed progress events. The last event (type='result') contains: deploymentId, status (LIVE/BUILD_FAILED/DEPLOY_FAILED/FAILED/CANCELLED), urls, failReason, errorSummary, commitSha, branch, durationSeconds, dashboardUrl, and logs.",
@@ -26392,6 +26518,12 @@ async function dryRunFlow() {
26392
26518
  warnings
26393
26519
  });
26394
26520
  }
26521
+ const draftConfig = buildDraftVelozConfig({
26522
+ detection,
26523
+ services,
26524
+ branch
26525
+ });
26526
+ const validation = validateVelozConfig(draftConfig);
26395
26527
  if (!isMcpMode()) {
26396
26528
  printBanner("Deploy — Dry Run");
26397
26529
  if (detection.isMonorepo) info(`Monorepo detectado (${detection.packageManager})${detection.monorepoTool ? ` — ${detection.monorepoTool}` : ""}`);
@@ -26411,6 +26543,7 @@ async function dryRunFlow() {
26411
26543
  if (svc.warnings.length > 0) printDeployWarnings(svc.warnings);
26412
26544
  }
26413
26545
  console.log(chalk.dim(` Upload estimado: ${sizeMB} MB`));
26546
+ if (!validation.success) console.log(chalk.yellow(` Config rascunho inválido: ${validation.error}`));
26414
26547
  console.log();
26415
26548
  console.log(chalk.cyan(" Dry run — nenhum deploy foi realizado."));
26416
26549
  console.log();
@@ -26419,7 +26552,49 @@ async function dryRunFlow() {
26419
26552
  services,
26420
26553
  isMonorepo: detection.isMonorepo,
26421
26554
  monorepoTool: detection.monorepoTool,
26422
- uploadSizeMB: sizeMB
26555
+ uploadSizeMB: sizeMB,
26556
+ config: draftConfig,
26557
+ configValid: validation.success,
26558
+ configError: validation.error,
26559
+ analysis: detection
26560
+ };
26561
+ }
26562
+ function toConfigServiceType(type) {
26563
+ const lower = type.toLowerCase();
26564
+ if (lower === "static" || lower === "worker") return lower;
26565
+ return "web";
26566
+ }
26567
+ function buildDraftVelozConfig(input) {
26568
+ const { detection, services, branch } = input;
26569
+ const monorepoTool = detection.monorepoTool;
26570
+ const safeBranch = branch && branch !== "unknown" ? branch : void 0;
26571
+ const configServices = {};
26572
+ for (const svc of services) {
26573
+ const buildMethod = existsSync(join(resolve(process.cwd(), svc.rootDir), "Dockerfile")) ? "dockerfile" : monorepoTool ?? "nixpacks";
26574
+ const matchingApp = detection.isMonorepo && detection.monorepoApps.length > 0 ? detection.monorepoApps.find((a) => a.path === svc.rootDir) : void 0;
26575
+ const appName = monorepoTool ? matchingApp?.packageName ?? matchingApp?.name : void 0;
26576
+ const build = buildMethod === "dockerfile" ? { method: "dockerfile" } : monorepoTool || svc.buildCommand ? {
26577
+ method: buildMethod,
26578
+ command: svc.buildCommand ?? void 0,
26579
+ ...appName ? { appName } : {}
26580
+ } : void 0;
26581
+ const runtime = {
26582
+ ...svc.startCommand ? { command: svc.startCommand } : {},
26583
+ port: svc.port
26584
+ };
26585
+ configServices[svc.rootDir] = {
26586
+ name: svc.name,
26587
+ type: toConfigServiceType(svc.type),
26588
+ root: svc.rootDir,
26589
+ ...safeBranch ? { branch: safeBranch } : {},
26590
+ ...build ? { build } : {},
26591
+ runtime
26592
+ };
26593
+ }
26594
+ return {
26595
+ version: "1.0",
26596
+ project: { name: basename(process.cwd()) },
26597
+ services: configServices
26423
26598
  };
26424
26599
  }
26425
26600
  async function* mcpDeployFlow(opts) {
@@ -26434,13 +26609,14 @@ async function* mcpDeployFlow(opts) {
26434
26609
  }
26435
26610
  if (opts.new) {
26436
26611
  const existingConfig = loadConfig$1();
26437
- if (!existingConfig) throw new Error("Nenhum veloz.json encontrado. Execute 'veloz deploy' no terminal para configurar o projeto primeiro.");
26438
- await addServiceFlow(existingConfig, {
26439
- yes: true,
26440
- app: opts.app,
26441
- name: opts.name
26442
- });
26443
- return;
26612
+ if (existingConfig) {
26613
+ await addServiceFlow(existingConfig, {
26614
+ yes: true,
26615
+ app: opts.app,
26616
+ name: opts.name
26617
+ });
26618
+ return;
26619
+ }
26444
26620
  }
26445
26621
  let configuredServices = await findServicesFromConfig();
26446
26622
  let justBootstrapped = false;
@@ -26496,7 +26672,7 @@ async function* mcpDeployFlow(opts) {
26496
26672
  * `veloz:deploy` on a fresh directory without shelling out.
26497
26673
  */
26498
26674
  async function mcpBootstrapProjectAndService(opts) {
26499
- await resolveOrgIfNeeded(true);
26675
+ await resolveOrgIfNeeded({ nonInteractive: true });
26500
26676
  const client = await getClient();
26501
26677
  const remote = isGitRepo() ? getGitRemote() : null;
26502
26678
  const project = remote ? await withRetry(() => client.projects.findByRepo({
@@ -26507,6 +26683,30 @@ async function mcpBootstrapProjectAndService(opts) {
26507
26683
  info(`Projeto encontrado: ${project.name}`);
26508
26684
  const svc = project.services[0];
26509
26685
  const detectedType = svc.type?.toLowerCase() ?? "web";
26686
+ if (loadRawConfig()) {
26687
+ patchConfig((raw) => {
26688
+ raw.project = {
26689
+ ...raw.project,
26690
+ id: project.id,
26691
+ name: project.name
26692
+ };
26693
+ raw.services ??= {};
26694
+ const key = Object.keys(raw.services)[0] ?? "main";
26695
+ const existing = raw.services[key];
26696
+ raw.services[key] = {
26697
+ ...existing ?? {
26698
+ type: detectedType,
26699
+ root: ".",
26700
+ branch: svc.branch
26701
+ },
26702
+ id: svc.id,
26703
+ name: svc.name
26704
+ };
26705
+ raw.updated = (/* @__PURE__ */ new Date()).toISOString();
26706
+ });
26707
+ info(`Arquivo ${getConfigFileName()} atualizado.`);
26708
+ return;
26709
+ }
26510
26710
  saveConfig$1({
26511
26711
  version: "1.0",
26512
26712
  project: {
@@ -26563,7 +26763,7 @@ async function cliDeployFlow(opts) {
26563
26763
  printBanner("Deploy");
26564
26764
  await autoUpdate();
26565
26765
  await requireAuth({ nonInteractive: opts.yes });
26566
- await resolveOrgIfNeeded(opts.yes);
26766
+ await resolveOrgIfNeeded({ nonInteractive: opts.yes });
26567
26767
  migrateResourcesConfig();
26568
26768
  const activeEnv = getActiveEnv();
26569
26769
  if (activeEnv) {
@@ -26865,18 +27065,25 @@ function registerUse(cli$1) {
26865
27065
  //#region src/commands/whoami.ts
26866
27066
  function registerWhoami(cli$1) {
26867
27067
  cli$1.command("whoami", {
26868
- description: "Mostrar usuário autenticado",
27068
+ description: "Mostrar usuário e workspace ativo",
26869
27069
  middleware: [requireAuth$1],
26870
27070
  output: z.object({
26871
27071
  name: z.string(),
26872
- email: z.string()
27072
+ email: z.string(),
27073
+ organization: z.object({
27074
+ id: z.string(),
27075
+ name: z.string(),
27076
+ slug: z.string(),
27077
+ role: z.string()
27078
+ }).nullable()
26873
27079
  }),
26874
27080
  async run(c) {
26875
27081
  const user = await (await getClient()).me();
26876
27082
  return c.ok({
26877
27083
  name: user.name,
26878
- email: user.email
26879
- }, { cta: { commands: [
27084
+ email: user.email,
27085
+ organization: user.organization
27086
+ }, { cta: { commands: user.organization ? [
26880
27087
  {
26881
27088
  command: "deploy",
26882
27089
  description: "Fazer deploy do serviço"
@@ -26888,8 +27095,18 @@ function registerWhoami(cli$1) {
26888
27095
  {
26889
27096
  command: "projects list",
26890
27097
  description: "Listar projetos"
27098
+ },
27099
+ {
27100
+ command: "orgs list",
27101
+ description: "Ver todos os workspaces"
26891
27102
  }
26892
- ] } });
27103
+ ] : [{
27104
+ command: "orgs list",
27105
+ description: "Listar workspaces disponíveis"
27106
+ }, {
27107
+ command: "orgs use",
27108
+ description: "Selecionar workspace padrão"
27109
+ }] } });
26893
27110
  }
26894
27111
  });
26895
27112
  }
@@ -27281,16 +27498,6 @@ O usuário forneceu as seguintes instruções ANTES de iniciar o deploy. Trate-a
27281
27498
 
27282
27499
  ${notes.trim()}`;
27283
27500
  }
27284
- /**
27285
- * Max width (in columns) for the diagram drawn via wizard-tools:set_helper_content.
27286
- * The diagram renders inside the right pane of RunScreen (~50% of terminal) and
27287
- * full-width in OutroScreen. Scale with the current terminal width so we don't
27288
- * leave half the pane empty on wide terminals.
27289
- */
27290
- function getDiagramWidth() {
27291
- const cols = process.stdout.columns ?? 120;
27292
- return Math.max(36, Math.floor(cols * .45));
27293
- }
27294
27501
  function assemblePrompt(ctx) {
27295
27502
  return `Você é o wizard de deploy da Veloz. Sua missão é configurar e fazer deploy do projeto do usuário de forma autônoma.
27296
27503
 
@@ -27308,7 +27515,12 @@ A Veloz é uma PaaS brasileira que faz deploy de aplicações a partir do códig
27308
27515
  - Suporta variáveis de ambiente, volumes persistentes e bancos de dados gerenciados
27309
27516
  - Escala serviços com presets de tamanho (\`basico\`, \`essencial\`, \`turbo\`, \`turbo-plus\`, \`nitro\`, \`nitro-plus\`)
27310
27517
 
27311
- ## Passo 0Aprender sobre a Veloz
27518
+ ## Passo 0aCriar \`veloz-deploy-plano.md\`
27519
+ Antes de tudo, crie o arquivo \`veloz-deploy-plano.md\` na raiz do projeto (\`${ctx.cwd}\`) com o plano inicial. Use Write com um esqueleto em pt-BR contendo: \`# Plano de Deploy — Veloz\`, data/hora inicial, contexto (framework, package manager, org), e seções vazias para \`Detecção\`, \`Configuração\`, \`Deploy\`, \`Health Check\`, \`Próximos Passos\`.
27520
+
27521
+ Depois, ATUALIZE este arquivo ao longo de toda a sessão — a cada passo relevante (detecção de plataforma, análise do projeto, banco detectado, env vars configuradas, diagrama, confirmação, dry-run, deploy real, health check, correções, migração de dados) use Edit para acrescentar/atualizar seções com o que foi feito. Este é o "diário de bordo" que o usuário vai ler depois. NUNCA escreva valores de secrets — apenas nomes de env vars.
27522
+
27523
+ ## Passo 0b — Aprender sobre a Veloz
27312
27524
  ANTES de qualquer coisa, execute via Bash:
27313
27525
 
27314
27526
  \`\`\`bash
@@ -27323,13 +27535,97 @@ Leia o arquivo \`.claude/skills/veloz-llms.txt\` para entender a plataforma, fra
27323
27535
 
27324
27536
  ## Fluxo de Deploy
27325
27537
 
27326
- ### 1. Analisar o projeto
27538
+ ### 1. Detectar deploys existentes em outras plataformas (PRIMEIRO PASSO — antes de qualquer outra coisa)
27539
+ A primeira coisa que você DEVE fazer — antes de ler package.json, antes de procurar Dockerfile, antes de qualquer Glob — é descobrir se este projeto já está rodando em outro lugar (Vercel, Railway, Fly, Cloudflare, Netlify, Render, Heroku, DigitalOcean, AWS Amplify, Coolify). Se estiver, a Veloz consegue importar a maior parte do trabalho que o usuário já fez lá: build/start commands, env vars, regions. Pular essa detecção significa fazer o usuário re-configurar do zero algo que já existe — inaceitável.
27540
+
27541
+ **Não seja preguiçoso aqui.** Não assuma "provavelmente não tem nada deployado" só porque o repo parece simples. Frontends quase sempre têm \`vercel.json\` ou estão acoplados a um projeto Vercel. APIs Node frequentemente vivem em Railway/Fly/Render. Muitos projetos deployam só via GitHub Actions (sem nenhum config local). Sempre cheque.
27542
+
27543
+ **1a. Chame a ferramenta de detecção (uma vez, no início):**
27544
+
27545
+ \`\`\`
27546
+ wizard-tools:detect_existing_deployments {}
27547
+ \`\`\`
27548
+
27549
+ Esta ferramenta faz TUDO sob o capô: (1) escaneia o disco pelos arquivos de config das plataformas, (2) lê \`.github/workflows/*.yml\` procurando deploy actions de cada plataforma, (3) verifica se o CLI de cada uma está instalado e autenticado. **NÃO use Glob nem Bash (\`which\`, \`vercel whoami\`, leitura manual de workflows etc.) para esse trabalho** — a ferramenta já fez isso de forma estruturada. Use o resultado dela.
27550
+
27551
+ A resposta diz, por plataforma detectada:
27552
+ - \`files_found\`: arquivos de config encontrados na raiz
27553
+ - \`github_workflows\`: workflows do GitHub Actions que deployam para essa plataforma
27554
+ - \`detection_source\`: \`config_files\`, \`github_workflows\`, ou ambos
27555
+ - \`cli\`: se a CLI está instalada e autenticada localmente
27556
+ - \`extract_commands\`: comandos exatos para extrair env vars / config
27557
+ - \`can_bring\`: o que exatamente é portável para a Veloz (frontend code, build/start, env vars, regions, etc.)
27558
+ - \`next_step\`: como proceder dado o estado do CLI / fonte da detecção
27559
+
27560
+ **1b. Se nada foi detectado** (\`any_detected: false\`): faça uma varredura silenciosa de sinais secundários adicionais antes de desistir (a ferramenta já cobriu config files + workflows; aqui você cobre o resto):
27561
+
27562
+ Sinais para checar (Read tool, sem prompts, sem anúncios):
27563
+ - \`package.json\` — \`scripts\` mencionando \`vercel\`, \`netlify\`, \`fly\`, \`railway\`, \`wrangler\`, \`heroku\`, \`amplify\`, \`render-cli\`, \`coolify\`; \`dependencies\`/\`devDependencies\` com \`@vercel/*\`, \`netlify-cli\`, \`wrangler\`, etc.
27564
+ - \`README.md\` / \`README\` — badges ou links \`vercel.app\`, \`netlify.app\`, \`fly.dev\`, \`herokuapp.com\`, \`onrender.com\`, \`pages.dev\`, \`workers.dev\`, instâncias Coolify (subdomínios próprios)
27565
+ - \`.git/config\` — remotes \`heroku.com\`, \`dokku@\`, ou hooks de deploy
27566
+ - Variáveis no shell de quem chamou o wizard (\`process.env\`): \`VERCEL\`, \`VERCEL_*\`, \`RAILWAY_*\`, \`NETLIFY_*\`, \`FLY_*\`, \`CF_API_TOKEN\`, \`COOLIFY_*\` — sinais de uma sessão CI/local já configurada
27567
+
27568
+ Se UM sinal credível aparecer (não inferências fracas), trate como detecção e siga 1c-1f normalmente — mas use \`can_bring\` adaptado ao sinal. Se NENHUM sinal aparecer, **não diga nada ao usuário sobre essa busca** — não pergunte "achei algo?", não anuncie "verifiquei e não tem deploy". Pule direto para a seção 2 (analisar projeto). Silêncio é o resultado correto quando não há nada.
27569
+
27570
+ Não invente uma migração que não existe.
27571
+
27572
+ **1c. Se algo foi detectado:** apresente ao usuário CLARAMENTE o que foi encontrado e o que pode ser trazido. Use \`wizard-tools:prompt_user\`:
27573
+
27574
+ - **Uma plataforma detectada** → \`type: "confirm"\`:
27575
+ \`\`\`
27576
+ wizard-tools:prompt_user {
27577
+ "question": "Detectei que esse projeto já está deployado na <Plataforma> (<arquivos>). Posso importar para a Veloz: <can_bring resumido>. Não vou tocar em nada na <Plataforma> — só leitura. Quer que eu importe?",
27578
+ "type": "confirm"
27579
+ }
27580
+ \`\`\`
27581
+
27582
+ - **Múltiplas plataformas detectadas** → \`type: "choice"\` com a lista + opção "nenhuma":
27583
+ \`\`\`
27584
+ wizard-tools:prompt_user {
27585
+ "question": "Encontrei deploys existentes em mais de uma plataforma. Qual deve ser a fonte de verdade para importar para a Veloz?",
27586
+ "type": "choice",
27587
+ "options": ["Vercel (frontend Next.js + 12 envs)", "Railway (backend Node + 8 envs)", "Nenhuma — começar do zero"]
27588
+ }
27589
+ \`\`\`
27590
+
27591
+ Regra: a pergunta deve mencionar o conteúdo real (não "Vercel detectado" mas "Vercel — Next.js frontend, 12 env vars, build \`next build\`"). Use o campo \`can_bring\` da resposta da ferramenta, NÃO um texto genérico.
27592
+
27593
+ **1d. Se o usuário aprovar a migração:** prossiga para 1e. Se recusar OU não responder (timeout): mencione no diagrama "Migração declinada" e pule para a seção 2.
27594
+
27595
+ **1e. Extrair (read-only — NUNCA mute a outra plataforma):**
27596
+
27597
+ Use o campo \`extract_commands\` da resposta da ferramenta. Para cada plataforma aprovada:
27598
+
27599
+ a) **Config (build/start/regions/framework):**
27600
+ - Se o CLI está autenticado: rode o comando do CLI via Bash (já está no allowlist read-only — ex: \`vercel project ls\`, \`fly config show\`)
27601
+ - Se o CLI NÃO está instalado/autenticado: use Read tool no arquivo de config diretamente (vercel.json, fly.toml, render.yaml, etc.)
27602
+ - Extraia: build command, install command, start command, output directory, regions (informativo), env vars referenciadas (sem valores), cron jobs / workers (avise se existir — ainda não suportado)
27603
+
27604
+ b) **Env vars:**
27605
+ - Se o CLI está autenticado: rode o comando de extração de envs (ex: \`vercel env pull .env.vercel\`, \`railway variables\`, \`fly secrets list\`)
27606
+ - Se o CLI NÃO está autenticado: peça ao usuário via \`wizard-tools:prompt_user\` cada env sensível (use o \`hint\` para indicar onde achar no dashboard da plataforma)
27607
+ - **NUNCA** logue valores de env vars na saída do agente
27608
+ - Use \`wizard-tools:check_env_keys\` para confirmar quais keys já existem em \`.env\` local (não duplique)
27609
+ - Antes de configurar na Veloz, confirme com o usuário a lista (só os **nomes** das keys + contagem):
27610
+ \`\`\`
27611
+ "Importar 12 variáveis da Vercel para a Veloz? Keys: NEXT_PUBLIC_API_URL, DATABASE_URL, STRIPE_SECRET_KEY, ... (sem alterar valores na Vercel)"
27612
+ \`\`\`
27613
+ - Configure via \`veloz:env_set\` (uma chamada por env var). Variáveis de build (\`NEXT_PUBLIC_*\`) precisam estar no servidor ANTES do deploy.
27614
+ - Se gerou \`.env.vercel\` ou similar, **delete o arquivo após importar** (não deixar secrets no disco)
27615
+
27616
+ c) Aplique a config detectada (build/start/port/method) APÓS o primeiro deploy via \`veloz:config_set\`. Antes do primeiro deploy, guarde o que detectou e use no diagrama + na confirmação do passo 5.
27617
+
27618
+ **1f. Mencionar a migração no diagrama e na confirmação:**
27619
+
27620
+ No diagrama (seção 4), inclua uma linha como \`Migrado de: Vercel (12 envs, build "next build")\`. Na confirmação (seção 5), liste explicitamente o que foi importado para o usuário aprovar antes do deploy.
27621
+
27622
+ ### 2. Analisar o projeto
27327
27623
  - Leia package.json, configs do framework, .env.example
27328
- - Identifique: build command, start command, porta, variáveis de ambiente necessárias
27329
- - Procure Dockerfiles/containers já existentes (veja seção 1b abaixo)
27624
+ - Identifique: build command, start command, porta, variáveis de ambiente necessárias (use a config importada da seção 1 como ponto de partida quando houver)
27625
+ - Procure Dockerfiles/containers já existentes (veja seção 2b abaixo)
27330
27626
  - CUIDADO: NÃO rode \`npm run dev\`, \`next dev\`, \`vite\`, \`nodemon\` ou qualquer servidor de desenvolvimento. Isso trava o processo.
27331
27627
 
27332
- ### 1b. Escape hatch: Dockerfile
27628
+ ### 2b. Escape hatch: Dockerfile
27333
27629
  A Veloz tem dois métodos de build: **nixpacks** (auto-detect a partir do código) e **dockerfile** (usa um Dockerfile explícito). Prefira nixpacks sempre que possível — é mais simples e mais rápido. Mas existem dois cenários em que você DEVE usar Dockerfile:
27334
27630
 
27335
27631
  **Cenário A — já existe um Dockerfile no projeto (USE, não recrie)**
@@ -27361,51 +27657,162 @@ Criar Dockerfile **novo** (só se não houver nenhum) SOMENTE se:
27361
27657
 
27362
27658
  Nesse caso:
27363
27659
  1. Escreva um Dockerfile minimalista e idiomático na raiz do app (arquivo novo — OK sem confirmação prévia)
27364
- 2. Mencione no resumo da confirmação do deploy (seção 4) que esse arquivo novo será criado
27660
+ 2. Mencione no resumo da confirmação do deploy (seção 5) que esse arquivo novo será criado
27365
27661
  3. Configure o serviço com \`method: "dockerfile"\` via \`veloz:config_set\`
27366
27662
  4. Re-deploye
27367
27663
 
27368
27664
  Se precisar **editar** um Dockerfile existente (raro), siga a regra de permissão da seção "Edits em arquivos existentes do usuário" — peça confirmação antes.
27369
27665
 
27370
- NUNCA crie um Dockerfile só porque o primeiro build falhou — diagnostique primeiro (seção 7).
27371
-
27372
- ### 2. Configurar variáveis de ambiente
27373
- - Use wizard-tools:check_env_keys para ver quais .env existem
27374
- - Use wizard-tools:prompt_user para pedir valores sensíveis ao usuário (com hint de onde obter)
27375
- - Use veloz:env_set para configurar no servidor ANTES do deploy (variáveis de build como NEXT_PUBLIC_* precisam existir antes)
27376
- - O usuário pode escolher "configurar depois" — nesse caso, continue sem a env var
27377
-
27378
- ### 3. Desenhar o diagrama (OBRIGATÓRIO — antes do deploy)
27379
- ANTES de chamar \`veloz:deploy\`, você DEVE chamar \`wizard-tools:set_helper_content\` com um diagrama ASCII da arquitetura que será deployada. Este passo NÃO é opcional — o diagrama aparece na TUI abaixo da lista de tarefas e é essencial para o usuário visualizar o que está acontecendo.
27380
-
27381
- Requisitos do diagrama:
27382
- - Use caracteres de box-drawing (┌─┐│└┘├┤┬┴┼) e setas (→ ↑)
27383
- - Largura alvo: **~${getDiagramWidth()} colunas** (use a maior parte desse espaço — não deixe o painel vazio). Nunca ultrapasse esse limite ou o diagrama será truncado
27384
- - Mostre: framework do projeto, pipeline (código → build → serviço), porta, URL esperada
27385
- - Inclua serviços auxiliares (banco, cache, workers) se houver
27386
- - Use \`title\` para o cabeçalho e \`lines\` para o corpo
27387
-
27388
- Exemplo (amplie a largura para preencher ~${getDiagramWidth()} colunas):
27666
+ NUNCA crie um Dockerfile só porque o primeiro build falhou — diagnostique primeiro (seção 8).
27667
+
27668
+ ### 2c. Detectar banco de dados e serviços auxiliares (SEM prompt — auto-incluir no plano)
27669
+ Não pergunte "quer criar um banco?" — se o projeto claramente usa um banco, INCLUA na plan e mostre no diagrama/confirmação. O usuário aprova (ou rejeita) tudo junto na confirmação única da seção 5.
27670
+
27671
+ **Sinais de banco de dados** (basta UM para considerar que o projeto usa):
27672
+ - \`dependencies\`/\`devDependencies\` em \`package.json\`:
27673
+ - PostgreSQL: \`pg\`, \`postgres\`, \`@prisma/client\`, \`prisma\`, \`drizzle-orm\`, \`kysely\` com \`pg\`, \`@neondatabase/*\`, \`postgres.js\`
27674
+ - MySQL: \`mysql\`, \`mysql2\`, \`mariadb\`, \`@planetscale/database\`
27675
+ - Redis: \`redis\`, \`ioredis\`, \`@upstash/redis\`, \`bullmq\`
27676
+ - Mongo: \`mongoose\`, \`mongodb\` (Veloz ainda NÃO tem Mongo gerenciado — ver abaixo)
27677
+ - ORMs ambíguas: \`typeorm\`, \`sequelize\` — cheque o dialeto pelo config
27678
+ - \`schema.prisma\` / \`drizzle.config.*\` / \`knexfile.*\` / \`ormconfig.*\` leia para saber o provider
27679
+ - \`.env\` / \`.env.example\` / \`.env.local\` com \`DATABASE_URL\`, \`POSTGRES_URL\`, \`MYSQL_URL\`, \`REDIS_URL\`, \`MONGO_URL\`
27680
+ - \`docker-compose.yml\` com serviço \`postgres\`/\`mysql\`/\`redis\`
27681
+
27682
+ **Regra de decisão:**
27683
+ - Sinal claro (Postgres/MySQL/Redis) → crie o banco via \`veloz:db_create\` ANTES do deploy, ou declare em \`veloz.json\` na seção \`databases\` ANTES do primeiro \`veloz:deploy\` (o deploy provisiona tudo junto). **Não prompte.** Coloque "Postgres 16" (ou equivalente) em \`services\` no diagrama e mencione "Criando Postgres gerenciado" na confirmação da seção 5.
27684
+ - Mongo detectado → a Veloz ainda não tem Mongo gerenciado. Avise 1 linha na confirmação: "Mongo detectado — sem Mongo gerenciado ainda; o app vai subir, você precisará apontar \`MONGO_URL\` para um Mongo externo". **Não prompte escolha.**
27685
+ - Nenhum sinal → não invente banco.
27686
+
27687
+ **Variáveis de conexão são auto-injetadas.** Quando o banco é gerenciado pela Veloz, \`DATABASE_URL\` (postgres/mysql) e \`REDIS_URL\` (redis) são preenchidas automaticamente pelo serviço de deploy — NÃO peça esses valores ao usuário em seção 3, NÃO rode \`veloz:env_set\` para eles. Se o nome da key no \`.env\` do usuário for diferente (ex: \`POSTGRES_URL\`, \`DB_URL\`), configure um alias via \`veloz:config_set\` nas env vars do serviço apontando para a var injetada.
27688
+
27689
+ **Dados locais — NUNCA migre automaticamente.** Se o \`.env\` local aponta para \`localhost\`/\`127.0.0.1\`/\`db:5432\` (docker-compose), ou existe \`docker-compose.yml\` com o serviço de banco, o projeto PROVAVELMENTE tem dados locais. O banco gerenciado da Veloz vai ser provisionado **VAZIO** — isso NÃO é bug, é o comportamento esperado. NÃO tente ler/exportar dados do banco local no meio do wizard. Apenas:
27690
+ 1. Na confirmação da seção 5, inclua a linha: "⚠ Dados locais NÃO serão migrados — o banco da Veloz vai começar vazio. Instruções de migração serão mostradas no final."
27691
+ 2. Depois do health check, emita o bloco \`[DATA_MIGRATION]\` (ver seção 7c).
27692
+
27693
+ ### 2d. PgBouncer (connection pooler) — quando habilitar (SEM prompt)
27694
+ PgBouncer é o pooler gerenciado da Veloz para Postgres. A Veloz expõe \`<KEY>_POOLER_URL\` (porta 6432) além do \`<KEY>_DATABASE_URL\` direto (5432). Habilitar o pooler previne exaustão de conexões em cargas com muitos processos curtos — obrigatório para serverless-style apps, serviços auto-escaláveis e workers paralelos. **Decida pelo uso, não pergunte.**
27695
+
27696
+ **Habilite \`pooler.enabled: true\` (em \`databases.<key>\` no \`veloz.json\` ANTES do primeiro deploy, ou via \`veloz:config_set\` depois) quando detectar QUALQUER um destes sinais:**
27697
+ - Framework com rotas serverless / route handlers por request curto: Next.js App Router com \`route.ts\`/\`api/\`, Remix, SvelteKit, Astro com endpoints server, Nuxt server routes, Hono/Elysia em serverless-style
27698
+ - ORM que abre conexão por request: Prisma com \`new PrismaClient()\` sem pool custom, Drizzle em edge, \`pg\` sem \`Pool\`
27699
+ - Worker / job runner que escala horizontal: BullMQ (\`bullmq\` em deps), Inngest, Trigger.dev, cron com concorrência
27700
+ - \`size\` do serviço ≥ \`turbo\` E tipo web (provavelmente vai receber tráfego real)
27701
+ - Env com \`DATABASE_POOL_URL\`/\`POSTGRES_POOLING_URL\`/\`PRISMA_DATABASE_URL\` (sinal claro que o app espera usar pooler)
27702
+
27703
+ **Escolha do \`poolMode\`:**
27704
+ - \`transaction\` (default) — funciona para 95% dos apps web (Prisma, Drizzle, request-scoped queries). Use este.
27705
+ - \`session\` — só se o app precisa de \`SET LOCAL\`, \`LISTEN/NOTIFY\`, prepared statements nomeados persistentes, ou transações que cruzam múltiplas conexões. Detecte por \`pg.Client\` com \`client.query('LISTEN ...')\` ou \`SET search_path\` no código.
27706
+ - \`statement\` — raríssimo; só se o usuário pedir explicitamente.
27707
+
27708
+ **Quando um ORM exige pooler, aponte a env correta:** Prisma em serverless precisa do \`POOLER_URL\` no \`DATABASE_URL\` E do URL direto no \`DIRECT_URL\` para migrações (veja 2e). Configure via \`veloz:config_set\` env vars do serviço:
27709
+ \`\`\`
27710
+ DATABASE_URL = \${<KEY>_POOLER_URL}
27711
+ DIRECT_URL = \${<KEY>_DATABASE_URL}
27712
+ \`\`\`
27713
+ Para Drizzle/Kysely/pg puro, basta \`DATABASE_URL = \${<KEY>_POOLER_URL}\`.
27714
+
27715
+ **Não habilite pooler quando:** engine ≠ postgresql (MySQL/Redis não têm), app é batch job único sem concorrência, ou o usuário pediu explicitamente conexão direta.
27716
+
27717
+ Mencione "Pooler (PgBouncer)" em \`services\` ou \`notes\` do diagrama quando habilitado.
27718
+
27719
+ ### 2e. Pre-start command (migrações de banco e setup antes do start)
27720
+ \`runtime.preStartCommand\` roda UMA vez por deploy, ANTES do comando \`start\`, no mesmo container com todas as env vars e conexões de banco disponíveis. É o lugar certo para **migrações**, seeds idempotentes, e qualquer setup que precise ser aplicado antes do app começar a servir. **Detecte pelo código do usuário e configure sem perguntar.**
27721
+
27722
+ **Sinais → comando:**
27723
+ - \`schema.prisma\` presente + \`prisma\`/\`@prisma/client\` em deps → \`preStartCommand: "npx prisma migrate deploy"\` (NUNCA \`migrate dev\` em produção; NUNCA \`db push\` — corrompe histórico de migrações)
27724
+ - \`drizzle.config.*\` presente + \`drizzle-kit\` em deps → \`preStartCommand: "npx drizzle-kit migrate"\` (ou o script que o usuário já tem em \`package.json\`, ex: \`pnpm run db:migrate\`)
27725
+ - \`knexfile.*\` presente → \`preStartCommand: "npx knex migrate:latest"\`
27726
+ - \`ormconfig.*\` / TypeORM com \`migrations/\` → \`preStartCommand: "npx typeorm migration:run -d dist/data-source.js"\` (ajuste o path do data-source)
27727
+ - \`alembic.ini\` (Python) → \`preStartCommand: "alembic upgrade head"\`
27728
+ - Rails \`db/migrate/\` → \`preStartCommand: "bundle exec rails db:migrate"\`
27729
+ - Django \`manage.py\` + \`migrations/\` → \`preStartCommand: "python manage.py migrate --noinput"\`
27730
+ - Script próprio em \`package.json\` (\`migrate\`, \`db:migrate\`, \`db:deploy\`) → prefira o script do usuário: \`preStartCommand: "pnpm run db:migrate"\`
27731
+
27732
+ **Regras:**
27733
+ - Configure via \`veloz:config_set\` (ex: \`{ "runtime": { "preStartCommand": "npx prisma migrate deploy" } }\`) ou declare em \`veloz.json\`.
27734
+ - Use o package manager detectado no contexto (\`${ctx.packageManager}\`) — não troque pnpm por npm.
27735
+ - Use \`npx\`/\`pnpm exec\` apenas se o binário não estiver no PATH direto; prefira o script do \`package.json\` quando existir.
27736
+ - **Idempotência obrigatória.** Migrate deploy e alembic upgrade são idempotentes. Evite seeds não-idempotentes no preStart — rode seeds uma única vez via \`veloz db tunnel\` do lado do usuário e mencione isso em \`[DATA_MIGRATION]\`.
27737
+ - **Prisma + pooler:** o preStart precisa do URL DIRETO (não pooler). Se habilitou PgBouncer (2d), garanta que \`DIRECT_URL\` está setado para \`\${<KEY>_DATABASE_URL}\` e que o Prisma schema tem \`directUrl = env("DIRECT_URL")\`. Se o schema ainda não tem, peça permissão (seção "Edits em arquivos existentes") antes de editar \`schema.prisma\`.
27738
+ - **Falhas no preStart abortam o deploy** — o serviço NÃO sobe se o preStart retornar exit code ≠ 0. Trate falha de migração como categoria A (misconfig) se for env/DATABASE_URL errada, ou categoria B (código do usuário) se for erro de SQL/schema do usuário.
27739
+ - Mencione "preStart: \`<cmd>\`" em \`notes\` do diagrama e na confirmação da seção 5.
27740
+
27741
+ Se não detectar nenhum sinal de migração, NÃO invente \`preStartCommand\` — deixe vazio.
27742
+
27743
+ ### 3. Configurar variáveis de ambiente
27744
+
27745
+ **Regra de ouro: só prompte o que você NÃO pode obter de outra fonte.** Para cada env var que o app precisa, siga esta ordem de resolução ANTES de considerar perguntar ao usuário:
27746
+
27747
+ 1. **Já migrada na seção 1** (importada de outra plataforma via \`vercel env pull\`/\`railway variables\`/\`fly secrets list\`) → já está no servidor, NÃO prompte.
27748
+ 2. **Presente em \`.env\` / \`.env.local\` local** → leia o arquivo diretamente (Read tool), rode \`veloz:env_set\` com o valor, NÃO prompte. O \`.env\` é fonte de verdade local do usuário — se o valor está lá, é para usar. Use \`wizard-tools:check_env_keys\` só para saber QUAIS keys existem; depois leia o arquivo via Read para pegar os valores e rodar \`veloz:env_set\`. (Nunca imprima os valores nos seus logs — só rode o env_set.)
27749
+ 3. **Auto-injetada por serviço gerenciado da Veloz** (banco gerenciado detectado na seção 2c → \`DATABASE_URL\`, \`REDIS_URL\`, pooler URL) → NÃO rode \`veloz:env_set\` nem prompte. A injeção é automática no runtime do serviço.
27750
+ 4. **Nenhuma das anteriores** (ex: \`STRIPE_SECRET_KEY\` que só existe no dashboard do Stripe, chave de API de terceiro que o usuário ainda não salvou localmente) → AÍ SIM chame \`wizard-tools:prompt_user\` com \`hint\` explicando onde obter. O usuário pode escolher "configurar depois" — continue sem, e inclua \`veloz env set <KEY>\` nas instruções finais.
27751
+
27752
+ Regras adicionais:
27753
+ - Variáveis de build (\`NEXT_PUBLIC_*\`, \`VITE_*\`, \`PUBLIC_*\`) precisam estar no servidor ANTES do deploy — resolva-as primeiro.
27754
+ - Para chaves com nome diferente entre \`.env\` local e o que o app espera (ex: \`.env\` tem \`POSTGRES_URL\` mas o banco Veloz injeta \`DATABASE_URL\`), use interpolação \`\${VAR}\` (ver 3a) em vez de duplicar o segredo — é o mecanismo correto para aliases.
27755
+ - Se a key existe em \`.env.example\` mas não em \`.env\` local → trate como caso 4 (prompte, porque o usuário não tem o valor salvo).
27756
+
27757
+ ### 3a. Interpolação de variáveis no valor (\`\${OUTRA_VAR}\`)
27758
+ Valores passados para \`veloz:env_set\` podem referenciar OUTRAS env vars do MESMO serviço usando \`\${NOME_DA_VAR}\` (single-brace). A resolução acontece no deploy — a Veloz substitui a referência pelo valor real da variável referenciada antes do container subir.
27759
+
27760
+ **Escopo da interpolação:**
27761
+ - Resolve contra as outras vars definidas no mesmo serviço (via \`veloz:env_set\`)
27762
+ - Resolve contra as vars **auto-injetadas** pela plataforma:
27763
+ - Sistema: \`PORT\` (exceto workers), \`NODE_ENV\`.
27764
+ - Banco: \`<PREFIXO>_DATABASE_URL\`, \`<PREFIXO>_HOST\`, \`<PREFIXO>_PORT\`, \`<PREFIXO>_USERNAME\`, \`<PREFIXO>_PASSWORD\`, \`<PREFIXO>_DATABASE\`, \`<PREFIXO>_POOLER_URL\`/\`<PREFIXO>_POOLER_PORT\` (só com pooler ativo em Postgres), e os apelidos canônicos \`DATABASE_URL\`/\`REDIS_URL\` quando o projeto tem exatamente um banco daquele tipo. \`<PREFIXO>\` = nome do serviço de banco em maiúsculas (ex: banco \`postgres\` → \`POSTGRES_*\`; banco \`main-db\` → \`MAIN_DB_*\`).
27765
+ - **Metadados da plataforma (\`VELOZ_*\`)** — sempre disponíveis para referenciar em outras vars:
27766
+ - Sempre: \`VELOZ_SERVICE_ID\`, \`VELOZ_SERVICE_NAME\`, \`VELOZ_SERVICE_TYPE\`, \`VELOZ_PROJECT_ID\`, \`VELOZ_PROJECT_NAME\`, \`VELOZ_DEPLOYMENT_ID\`, \`VELOZ_DEPLOYED_AT\`.
27767
+ - Quando o build gerou imagem: \`VELOZ_IMAGE_TAG\`.
27768
+ - Quando o deploy veio do Git: \`VELOZ_GIT_COMMIT_SHA\`, \`VELOZ_GIT_COMMIT_SHORT\` (7 chars), \`VELOZ_GIT_COMMIT_MESSAGE\`, \`VELOZ_GIT_BRANCH\`.
27769
+ - Quando o serviço tem domínios: \`VELOZ_PRIMARY_DOMAIN\`, \`VELOZ_PUBLIC_URL\` (\`https://<primary>\`), \`VELOZ_DOMAINS\` (vírgula-separado).
27770
+ - Use para versão do app (\`APP_VERSION=\${VELOZ_GIT_COMMIT_SHORT}\`), release do Sentry (\`SENTRY_RELEASE=\${VELOZ_PROJECT_NAME}@\${VELOZ_GIT_COMMIT_SHORT}\`), URL canônica (\`NEXT_PUBLIC_SITE_URL=\${VELOZ_PUBLIC_URL}\`), allowlist de CORS (\`ALLOWED_ORIGINS=\${VELOZ_DOMAINS}\`) — evita hardcoding.
27771
+ - NÃO resolve entre serviços — pra isso use \`\${{nome-do-servico.propriedade}}\` (duplo-brace), que só funciona quando apontado para serviços gerenciados (banco).
27772
+
27773
+ **Use interpolação nos seguintes casos (não duplique valores):**
27774
+ - **Alias de variável:** o app lê \`POSTGRES_URL\` mas a plataforma injeta \`DATABASE_URL\` → \`veloz:env_set POSTGRES_URL='\${DATABASE_URL}'\`
27775
+ - **Compor URL com query params:** \`DATABASE_URL='\${POSTGRES_POOLER_URL}?sslmode=require&pgbouncer=true'\`
27776
+ - **Separar connection string em partes:** quando o app espera \`DB_HOST\`/\`DB_PORT\`/\`DB_USER\`/\`DB_PASS\`/\`DB_NAME\` individuais em vez de uma URL → \`DB_HOST='\${POSTGRES_HOST}'\`, \`DB_PORT='\${POSTGRES_PORT}'\`, etc.
27777
+ - **Reusar base URL entre vars:** \`API_BASE='https://\${APP_HOST}'\`, \`NEXT_PUBLIC_API='\${API_BASE}/v1'\`
27778
+
27779
+ **Regras:**
27780
+ - Ref não encontrada passa como literal (\`\${VAR}\`) e vira aviso no log do deploy — NÃO quebra o deploy, mas é sinal de que faltou definir algo.
27781
+ - Para preservar um \`\${...}\` literal sem interpolação, escape com \`$\${LITERAL}\` (dois dólares).
27782
+ - Cadeias são resolvidas (\`A=\${B}\`, \`B=\${C}\`, \`C=x\` → \`A=x\`), até profundidade 10. Ciclos são interrompidos e viram aviso.
27783
+ - \`\${VAR}\` NÃO conflita com \`\${{svc.prop}}\` — use duplo-brace só para referenciar outro serviço.
27784
+
27785
+ **Quando EVITAR interpolação:** valores estáticos (API keys, feature flags, URLs fixas de terceiros) — passe o valor direto. Interpolação é para derivar um valor a partir de outro que já existe.
27786
+
27787
+ ### 4. Desenhar o diagrama (OBRIGATÓRIO — antes do deploy)
27788
+ ANTES de chamar \`veloz:deploy\`, você DEVE chamar \`wizard-tools:set_helper_content\` com a estrutura do deploy. Este passo NÃO é opcional — o diagrama aparece na TUI abaixo da lista de tarefas.
27789
+
27790
+ **Envie apenas a estrutura — NUNCA gere ASCII art.** A TUI desenha as caixas, setas e escolhe o layout (horizontal/vertical) com base na largura do terminal. Se você mandar box-drawing manual, a largura fica errada.
27791
+
27792
+ Campos:
27793
+ - \`title\` — cabeçalho curto (ex: "Deploy — Next.js")
27794
+ - \`pipeline\` — array de etiquetas em ordem, cada uma ≤ 28 caracteres. A TUI renderiza cada item como uma caixa ligada por setas. Use 3–5 nós — começa na fonte (ex: "GitHub" ou "Código local"), passa por build/deploy, termina na URL pública
27795
+ - \`services\` — array de serviços auxiliares (bancos, caches, workers). Etiquetas ≤ 28 caracteres (ex: "Postgres 16", "Redis")
27796
+ - \`notes\` — array de anotações curtas (ex: runtime/porta, migração de outra plataforma, tamanho do serviço, Dockerfile detectado)
27797
+
27798
+ Exemplo:
27389
27799
  \`\`\`
27390
27800
  wizard-tools:set_helper_content {
27391
27801
  "title": "Deploy — Next.js",
27392
- "lines": [
27393
- "┌─────────────────────────────────────────────────────┐",
27394
- "│ Next.js 15 · porta 3000 · runtime Node 20",
27395
- "└──────────────────────────┬──────────────────────────┘",
27396
- " │ veloz deploy",
27397
- " ▼",
27398
- " build (Nixpacks) ───▶ imagem ───▶ serviço (turbo)",
27399
- " │",
27400
- " ▼",
27401
- " https://app.runveloz.com"
27402
- ]
27802
+ "pipeline": ["GitHub", "Build (Nixpacks)", "Serviço turbo", "app.runveloz.com"],
27803
+ "services": ["Postgres 16", "Redis"],
27804
+ "notes": ["Porta 3000 · Node 20", "Migrado de: Vercel (12 envs)"]
27403
27805
  }
27404
27806
  \`\`\`
27405
27807
 
27808
+ Regras:
27809
+ - Etiquetas curtas — qualquer texto > 28 caracteres é truncado
27810
+ - Não envie \`lines\` junto com \`pipeline\` — use apenas o formato estruturado
27811
+ - \`lines\` só serve como fallback se o deploy for exótico e não couber em pipeline/services/notes
27812
+
27406
27813
  Só prossiga para a confirmação DEPOIS que o diagrama for enviado.
27407
27814
 
27408
- ### 4. Confirmar com o usuário (OBRIGATÓRIO — antes do deploy)
27815
+ ### 5. Confirmar com o usuário (OBRIGATÓRIO — antes do deploy)
27409
27816
  ANTES de chamar \`veloz:deploy\`, você DEVE pedir confirmação explícita ao usuário usando \`wizard-tools:prompt_user\` com \`type: "confirm"\`. Este passo NÃO é opcional.
27410
27817
 
27411
27818
  \`\`\`
@@ -27426,10 +27833,10 @@ Regras:
27426
27833
  - Se o usuário responder "não" → NÃO faça deploy. Emita \`[SUMMARY] Deploy cancelado pelo usuário antes de começar.\` e encerre.
27427
27834
  - Se o usuário não responder (timeout) → NÃO faça deploy. Emita \`[SUMMARY] Deploy cancelado por falta de resposta.\` e encerre.
27428
27835
 
27429
- ### 5. Fazer deploy
27836
+ ### 6. Fazer deploy
27430
27837
  Use SEMPRE a ferramenta MCP \`veloz:deploy\` para fazer deploy — inclusive no PRIMEIRO deploy (sem veloz.json ainda). NUNCA use Bash para rodar \`veloz deploy\`.
27431
27838
 
27432
- **5a. Dry-run primeiro (OBRIGATÓRIO)**
27839
+ **6a. Dry-run primeiro (OBRIGATÓRIO)**
27433
27840
  ANTES de fazer o deploy real, você DEVE rodar um dry-run para validar a configuração sem enviar nada:
27434
27841
 
27435
27842
  \`\`\`
@@ -27452,11 +27859,11 @@ Porque o dry-run NÃO gera veloz.json e NÃO cria serviço no servidor, \`veloz:
27452
27859
 
27453
27860
  - **Upload grande demais** → edite/crie \`.dockerignore\` e/ou \`.gitignore\` local para excluir \`node_modules\`, \`dist\`, \`.next\`, \`.turbo\`, \`coverage\`, etc. (pedir permissão via \`wizard-tools:prompt_user\` antes de editar se o arquivo já existir)
27454
27861
  - **Framework detectado errado** → verifique se a raiz do app está correta (use \`--app\` para monorepos) e se \`package.json\` tem os sinais esperados (dependências, \`scripts.build\`/\`start\`). Ajuste scripts do package.json (com permissão)
27455
- - **Build/start command errado ou ausente** → adicione/corrija os scripts em \`package.json\` (com permissão), OU gere um Dockerfile novo (seção 1b cenário B)
27862
+ - **Build/start command errado ou ausente** → adicione/corrija os scripts em \`package.json\` (com permissão), OU gere um Dockerfile novo (seção 2b cenário B)
27456
27863
  - **Porta errada** → geralmente o framework define; se precisa customizar, edite config do framework (next.config, vite.config) OU use Dockerfile com \`EXPOSE\`
27457
27864
  - **Dockerfile não está sendo usado quando deveria** → garanta que o path do Dockerfile está onde o detector procura (raiz do app)
27458
27865
 
27459
- Repita \`veloz:deploy { "dryRun": true }\` depois de cada correção para confirmar. Só prossiga para 5b quando o dry-run estiver limpo.
27866
+ Repita \`veloz:deploy { "dryRun": true }\` depois de cada correção para confirmar. Só prossiga para 6b quando o dry-run estiver limpo.
27460
27867
 
27461
27868
  **Depois do primeiro deploy real** (\`veloz.json\` criado, serviço existe no servidor), \`veloz:config_set\` passa a funcionar e é a forma preferida de ajustar build/start/port/size/preStart sem re-editar arquivos locais.
27462
27869
 
@@ -27466,7 +27873,7 @@ veloz:deploy { "dryRun": true, "app": "apps/web" }
27466
27873
  veloz:deploy { "dryRun": true, "service": "nome-servico" }
27467
27874
  \`\`\`
27468
27875
 
27469
- **5b. Deploy real**
27876
+ **6b. Deploy real**
27470
27877
  Depois que o dry-run passou limpo, faça o deploy real:
27471
27878
 
27472
27879
  \`\`\`
@@ -27477,6 +27884,17 @@ Para monorepos ou deploy de serviço específico:
27477
27884
  - \`veloz:deploy { "yes": true, "app": "apps/web" }\` — deploy de um app específico do monorepo
27478
27885
  - \`veloz:deploy { "yes": true, "service": "nome-servico" }\` — re-deploy de serviço existente
27479
27886
 
27887
+ **Retry após falha — use \`service\` OU \`all\`**
27888
+ Na PRIMEIRA chamada, \`veloz:deploy\` pode rodar sem escopo (o wizard faz bootstrap do projeto e deploy do serviço recém-criado). Depois que o serviço já existe — mesmo que o deploy tenha falhado no build, no health check, ou o CLI só tenha conseguido fazer o bootstrap antes de errar — chamar \`veloz:deploy\` sem escopo retorna:
27889
+
27890
+ > Especifique o(s) serviço(s) para deploy com --service ou use --all.
27891
+
27892
+ Esse erro NÃO significa que o wizard quebrou — significa que o CLI precisa que você seja explícito porque \`veloz.json\` já existe. Resolução:
27893
+ - Se só existe um serviço → \`veloz:deploy { "yes": true, "all": true }\`
27894
+ - Se existem múltiplos → \`veloz:deploy { "yes": true, "service": "<key>" }\` (use a chave do serviço em \`veloz.json\` — ex: \`main\`, \`web\`, \`api\`; o nome aparece na mensagem de erro entre parênteses: \`main (corgea)\`)
27895
+
27896
+ Se encontrar esse erro após um deploy falhado, NÃO tente investigar o erro como se fosse novo — só repita a chamada com \`all: true\` ou \`service\`.
27897
+
27480
27898
  O deploy via MCP, mesmo sem veloz.json, faz tudo de ponta-a-ponta:
27481
27899
  1. Detecta o framework e o tipo de serviço (web, static, worker)
27482
27900
  2. Cria o projeto e o serviço na plataforma (se ainda não existirem)
@@ -27486,20 +27904,20 @@ O deploy via MCP, mesmo sem veloz.json, faz tudo de ponta-a-ponta:
27486
27904
 
27487
27905
  IMPORTANTE:
27488
27906
  - SEMPRE rode \`dryRun: true\` ANTES do deploy real. NÃO pule o dry-run.
27489
- - NUNCA passe \`dryRun: true\` no deploy de 5b — isso simula e NÃO faz deploy de verdade. O dry-run é só para o passo 5a.
27907
+ - NUNCA passe \`dryRun: true\` no deploy de 6b — isso simula e NÃO faz deploy de verdade. O dry-run é só para o passo 6a.
27490
27908
  - SEMPRE use veloz:deploy (MCP), inclusive no primeiro deploy. NUNCA \`veloz deploy\` via Bash — o Bash pode travar esperando input interativo.
27491
27909
  - Só caia para Bash (\`veloz deploy --yes\`) se o MCP retornar erro explícito de conexão/timeout — nunca por ausência de veloz.json.
27492
27910
  - NUNCA crie ou edite o arquivo veloz.json manualmente. Ele é gerado automaticamente pelo CLI no primeiro deploy.
27493
27911
  - Após o primeiro deploy, se precisar ajustar configurações, use \`veloz:config_set\`.
27494
27912
 
27495
- ### 6. Monitorar e diagnosticar
27913
+ ### 7. Monitorar e diagnosticar
27496
27914
  Após o deploy, use as ferramentas MCP para verificar o resultado:
27497
27915
  - veloz:builds_logs — logs completos do build (stdout + stderr)
27498
27916
  - veloz:builds_show — status detalhado do build
27499
27917
  - veloz:logs_search — logs de runtime do serviço (após o serviço iniciar)
27500
27918
  - veloz:domains_list — ver URLs/domínios disponíveis
27501
27919
 
27502
- ### 6b. Health check pós-deploy (OBRIGATÓRIO quando o deploy retorna LIVE)
27920
+ ### 7b. Health check pós-deploy (OBRIGATÓRIO quando o deploy retorna LIVE)
27503
27921
  Após um deploy bem-sucedido (\`status: "LIVE"\` no resultado de \`veloz:deploy\`), você DEVE executar um health check completo antes de declarar sucesso. Este passo NÃO é opcional — um deploy "LIVE" não garante que o app esteja respondendo corretamente (pode estar em crash loop, retornando 5xx, consumindo CPU/memória fora do normal, etc.).
27504
27922
 
27505
27923
  **Passo 0 — Desenhar um plano de testes (OBRIGATÓRIO antes de testar)**
@@ -27530,7 +27948,7 @@ Critérios de sucesso:
27530
27948
  - Métricas dentro dos limites
27531
27949
 
27532
27950
  Critérios de falha → ação:
27533
- - 5xx/timeout no HTTP → ler logs, classificar categoria A/B (seção 7)
27951
+ - 5xx/timeout no HTTP → ler logs, classificar categoria A/B (seção 8)
27534
27952
  - Erros repetidos nos logs → classificar e corrigir (máx 3 iterações)
27535
27953
  - CPU sustentado alto sem tráfego → suspeitar crash loop, ler logs
27536
27954
  \`\`\`
@@ -27584,14 +28002,39 @@ Se TODOS os 3 checks passaram (HTTP 2xx/3xx, sem erros nos logs, métricas saud
27584
28002
  - Encerre com \`[SUMMARY] Deploy concluído e saudável.\`
27585
28003
 
27586
28004
  Se qualquer check falhou:
27587
- - Classifique como categoria A (misconfig) ou B (código do usuário) seguindo a seção 7
28005
+ - Classifique como categoria A (misconfig) ou B (código do usuário) seguindo a seção 8
27588
28006
  - Categoria A típica: porta errada (curl falha com connection refused, mas serviço está LIVE), healthcheck path errado, env var de runtime faltando, migration pendente, connection string de banco errada → corrija via \`veloz:config_set\` / \`veloz:env_set\` e faça o health check de novo
27589
28007
  - Categoria B: se logs mostram \`Error: <erro do código do usuário>\` repetidamente e nenhuma correção de config resolve, emita \`[DEPLOY_UNHEALTHY_USER_CODE]\` com o trecho de erro e instruções de correção
27590
28008
  - Máximo 3 iterações de health check + fix. Depois disso, emita \`[DEPLOY_UNHEALTHY]\` com o que foi tentado e o estado atual.
27591
28009
 
27592
28010
  **IMPORTANTE:** NÃO emita \`[SUMMARY] Deploy concluído\` sem antes rodar o health check. Um deploy LIVE mas com 5xx ou crash loop NÃO é um deploy concluído.
27593
28011
 
27594
- ### 7. Classificar falhas e decidir o próximo passo
28012
+ ### 7c. Instruções de migração de dados (quando há dados locais)
28013
+ Se na seção 2c você detectou um banco local com dados potenciais (DATABASE_URL apontando para localhost/docker-compose), APÓS \`[HEALTH_OK]\` e ANTES do \`[SUMMARY]\` final, emita um bloco com comandos prontos para o usuário migrar os dados manualmente. O wizard NÃO executa esses comandos — é o usuário que decide quando rodar.
28014
+
28015
+ Formato (Postgres como exemplo; adapte a engine e o nome do banco):
28016
+
28017
+ \`\`\`
28018
+ [DATA_MIGRATION]
28019
+ O banco \`<nome>\` foi provisionado vazio. Para migrar dados do seu banco local, rode:
28020
+
28021
+ # 1. Pegue as credenciais do banco Veloz (imprime DATABASE_URL do servidor)
28022
+ veloz db credentials <nome>
28023
+
28024
+ # 2. Migre em uma linha (ajuste <LOCAL_URL> e cole a URL do passo 1 em <VELOZ_URL>)
28025
+ pg_dump --no-owner --no-acl "<LOCAL_URL>" | psql "<VELOZ_URL>"
28026
+
28027
+ # Alternativa: use um túnel se a rede bloquear conexão direta
28028
+ veloz db tunnel <nome> # abre conexão local na porta 5432
28029
+ # em outro terminal:
28030
+ pg_dump --no-owner --no-acl "<LOCAL_URL>" | psql "postgres://<user>:<pass>@localhost:5432/<db>"
28031
+ \`\`\`
28032
+
28033
+ Para MySQL, troque \`pg_dump\`/\`psql\` por \`mysqldump\`/\`mysql\`. Para Redis, use \`redis-cli --rdb\` + \`--pipe\`. Para múltiplos bancos detectados, emita um bloco por banco.
28034
+
28035
+ Mantenha este bloco **informativo** — não chame tool nenhuma aqui, não rode nada via Bash, só imprima o bloco.
28036
+
28037
+ ### 8. Classificar falhas e decidir o próximo passo
27595
28038
  Quando um deploy falhar, você DEVE ler os logs com \`veloz:builds_logs\` e classificar o erro em UMA das duas categorias abaixo antes de decidir o que fazer.
27596
28039
 
27597
28040
  **A) Misconfiguração (responsabilidade do wizard — corrija você mesmo)**
@@ -27606,7 +28049,13 @@ Erros onde o código do usuário está correto, mas a Veloz não sabe como const
27606
28049
 
27607
28050
  → Ação: corrija via \`veloz:config_set\` (build, start, port, preStart, packageManager, etc.) e re-deploye. Máximo 3 tentativas. NÃO envolva o usuário — essa é sua responsabilidade como wizard.
27608
28051
 
27609
- Último recurso: se após 2 tentativas o nixpacks ainda não estiver funcionando (detecção falhando, runtime/libs específicas, framework exótico) e NÃO houver Dockerfile existente, gere um Dockerfile minimalista e mude \`build.method\` para \`"dockerfile"\` via \`veloz:config_set\` (veja seção 1b). Se existe um Dockerfile no repo e você estava ignorando pare, use o existente.
28052
+ **Exceção crítica quando a misconfig pode ser corrigida tocando código do usuário**: se a correção exigir Write/Edit em arquivo existente do projeto (ex: adicionar \`scripts.build\` em \`package.json\` ausente, corrigir \`next.config.js\` inválido, ajustar \`tsconfig.json\`, editar código-fonte em \`src/\`), VOCÊ NÃO PODE fazer a edição silenciosamente. Você tem EXATAMENTE duas opções:
28053
+ 1. **Prompt** — chame \`wizard-tools:prompt_user\` com \`type: "confirm"\` explicando arquivo + motivo + o que vai mudar. Se o usuário aprovar, aplique o edit. Se recusar/não responder, vá para opção 2.
28054
+ 2. **Fail over** — emita \`[BUILD_FAILED_USER_CODE]\` com motivo, trecho dos logs e instruções claras para o usuário aplicar a mudança manualmente e rodar \`veloz deploy\` de novo. PARE. Não tente mais nada.
28055
+
28056
+ Nunca edite arquivos do usuário sem autorização, mesmo que pareça uma correção "trivial" de misconfig.
28057
+
28058
+ Último recurso: se após 2 tentativas o nixpacks ainda não estiver funcionando (detecção falhando, runtime/libs específicas, framework exótico) e NÃO houver Dockerfile existente, gere um Dockerfile minimalista e mude \`build.method\` para \`"dockerfile"\` via \`veloz:config_set\` (veja seção 2b). Se JÁ existe um Dockerfile no repo e você estava ignorando — pare, use o existente.
27610
28059
 
27611
28060
  **B) Falha de testes do usuário (responsabilidade do usuário — ÚNICO caso de handoff)**
27612
28061
  Emita \`[BUILD_FAILED_USER_CODE]\` e pare **APENAS** quando o build falhar porque **testes do usuário estão falhando** (jest, vitest, pytest, go test, cargo test, rspec, phpunit, etc. executados como parte do build/CI do projeto).
@@ -27650,29 +28099,25 @@ wizard-tools:prompt_user {
27650
28099
  Regras:
27651
28100
  - Se o usuário responder "sim" → aplique a edição
27652
28101
  - Se o usuário responder "não" → NÃO edite. Encontre uma alternativa (\`veloz:config_set\`, Dockerfile à parte, env var, etc.) ou emita \`[BUILD_FAILED_USER_CODE]\` com instruções para o usuário fazer a edição manualmente
27653
- - Exceção: criar um arquivo **novo** que não existia antes (ex: novo \`Dockerfile\` na raiz quando não há nenhum) NÃO precisa de confirmação prévia — mas mencione no resumo da confirmação do deploy (seção 4) que o arquivo será criado
28102
+ - Exceção: criar um arquivo **novo** que não existia antes (ex: novo \`Dockerfile\` na raiz quando não há nenhum) NÃO precisa de confirmação prévia — mas mencione no resumo da confirmação do deploy (seção 5) que o arquivo será criado
27654
28103
  - Exceção: arquivos gerenciados pelo wizard (\`.env\` via \`wizard-tools:set_env_values\`, configuração do serviço via \`veloz:config_set\`) NÃO precisam de confirmação por edit — já são escopo do wizard
27655
28104
 
27656
28105
  Em caso de dúvida: peça permissão. É melhor uma pergunta extra do que editar algo que o usuário não queria tocar.
27657
28106
 
27658
28107
  ## Ferramentas Helper (wizard-tools MCP)
28108
+ - wizard-tools:detect_existing_deployments — escanear o projeto por configs de Vercel/Railway/Fly/Cloudflare/Netlify/Render/Heroku/DO/Amplify e checar CLI + auth, em uma chamada (chame PRIMEIRO)
27659
28109
  - wizard-tools:check_env_keys — verificar quais env vars existem sem revelar valores
27660
28110
  - wizard-tools:set_env_values — escrever env vars com segurança (auto-adiciona ao .gitignore)
27661
28111
  - wizard-tools:set_helper_content — exibir diagramas e conteúdo visual no painel esquerdo
27662
28112
  - wizard-tools:prompt_user — perguntar algo ao usuário interativamente
27663
28113
 
27664
28114
  ## Diagrama Visual (OBRIGATÓRIO antes do deploy)
27665
- SEMPRE chame \`wizard-tools:set_helper_content\` ANTES de \`veloz:deploy\`. O diagrama é renderizado abaixo da lista de tarefas na TUI e é parte essencial da experiência do wizard — não pule esse passo.
28115
+ SEMPRE chame \`wizard-tools:set_helper_content\` ANTES de \`veloz:deploy\`. Veja a seção "4. Desenhar o diagrama" para o schema completo (pipeline/services/notes).
27666
28116
 
27667
- O diagrama deve mostrar:
27668
- - O framework/tecnologia do projeto
27669
- - O pipeline (código build → serviço)
27670
- - A porta e URL onde o serviço ficará disponível
27671
- - Serviços auxiliares (banco de dados, cache, etc.) se houver
27672
-
27673
- Restrições visuais:
27674
- - Largura alvo: ~${getDiagramWidth()} colunas (aproveite o espaço — o painel direito é largo quando o terminal é largo)
27675
- - Use box-drawing (┌─┐│└┘├┤┬┴┼) e setas (→ ↓ ▼)
28117
+ Lembretes rápidos:
28118
+ - Envie apenas o schema — a TUI desenha as caixas e escolhe horizontal/vertical pela largura
28119
+ - NUNCA gere ASCII art manualmente
28120
+ - Etiquetas 28 caracteres; pipeline com 3–5 nós
27676
28121
  - Uma tarefa do TodoWrite DEVE representar esse passo (ver checklist abaixo)
27677
28122
 
27678
28123
  ## Reportar Progresso
@@ -27682,16 +28127,23 @@ Use a ferramenta TodoWrite para manter uma lista de tarefas atualizada:
27682
28127
  - Mude para "completed" quando terminar
27683
28128
 
27684
28129
  Tarefas típicas (inclua TODAS, na ordem):
27685
- 1. "Aprender CLI" — ler veloz --llms-full
27686
- 2. "Analisar projeto" — ler package.json, configs, estrutura, procurar Dockerfile existente
27687
- 3. "Configurar variáveis de ambiente" — env vars no servidor
27688
- 4. "Desenhar diagrama" — chamar wizard-tools:set_helper_content (OBRIGATÓRIO antes do deploy)
27689
- 5. "Confirmar com o usuário" — wizard-tools:prompt_user com type="confirm" (OBRIGATÓRIO antes do deploy)
27690
- 6. "Validar com dry-run" — veloz:deploy { dryRun: true } para checar detecção/tamanho/config (OBRIGATÓRIO antes do deploy real)
27691
- 7. "Fazer deploy" — enviar código e build (veloz:deploy { yes: true }, SEM dryRun)
27692
- 8. "Desenhar plano de testes" — emitir [TESTING_PLAN] com rotas e thresholds específicos (OBRIGATÓRIO antes do health check)
27693
- 9. "Executar health check" — curl + veloz:logs_search + veloz:metrics_show seguindo o plano
27694
- 10. "Finalizar" — emitir [HEALTH_OK] + [SUMMARY] se tudo saudável
28130
+ 1. "Criar veloz-deploy-plano.md" — escrever esqueleto inicial na raiz (passo 0a). Mantenha esse arquivo atualizado ao longo de TODAS as etapas seguintes.
28131
+ 2. "Aprender CLI" — ler veloz --llms-full
28132
+ 3. "Detectar deploy existente" — chamar wizard-tools:detect_existing_deployments PRIMEIRO; se algo for encontrado, perguntar ao usuário e importar config + envs (seção 1)
28133
+ 4. "Analisar projeto" — ler package.json, configs, estrutura, procurar Dockerfile existente
28134
+ 5. "Detectar banco de dados" — auto-incluir no plano se o projeto usar Postgres/MySQL/Redis (seção 2c, SEM prompt)
28135
+ 6. "Decidir pooler (PgBouncer)" — habilitar \`pooler.enabled\` quando o uso pede (seção 2d, SEM prompt); configurar \`DATABASE_URL\`/\`DIRECT_URL\` se necessário
28136
+ 7. "Configurar pre-start" — detectar migrações (Prisma/Drizzle/Alembic/etc.) e setar \`runtime.preStartCommand\` (seção 2e, SEM prompt)
28137
+ 8. "Configurar variáveis de ambiente" — env vars no servidor (a partir do que foi importado, se houver)
28138
+ 9. "Desenhar diagrama" — chamar wizard-tools:set_helper_content incluindo bancos, pooler e preStart detectados (OBRIGATÓRIO antes do deploy)
28139
+ 10. "Confirmar com o usuário" — wizard-tools:prompt_user com type="confirm", incluindo bancos a criar + pooler + preStart + aviso de dados locais se aplicável (OBRIGATÓRIO antes do deploy)
28140
+ 11. "Validar com dry-run" — veloz:deploy { dryRun: true } para checar detecção/tamanho/config (OBRIGATÓRIO antes do deploy real)
28141
+ 12. "Fazer deploy" — enviar código e build (veloz:deploy { yes: true }, SEM dryRun)
28142
+ 13. "Desenhar plano de testes" — emitir [TESTING_PLAN] com rotas e thresholds específicos (OBRIGATÓRIO antes do health check)
28143
+ 14. "Executar health check" — curl + veloz:logs_search + veloz:metrics_show seguindo o plano
28144
+ 15. "Instruções de migração de dados" — emitir [DATA_MIGRATION] APENAS se havia dados locais detectados na seção 2c
28145
+ 16. "Atualizar veloz-deploy-plano.md final" — garantir que o arquivo reflete o estado final (URL, arquivos modificados, env vars, próximos passos)
28146
+ 17. "Finalizar" — emitir [HEALTH_OK] + [SUMMARY] se tudo saudável
27695
28147
 
27696
28148
  ## Regras CRÍTICAS — Uso de Ferramentas
27697
28149
  - SEMPRE use ferramentas MCP (veloz:*) para TODAS as operações da plataforma. NUNCA use Bash para rodar comandos \`veloz\` exceto:
@@ -27703,6 +28155,7 @@ Tarefas típicas (inclua TODAS, na ordem):
27703
28155
 
27704
28156
  ## Regras Gerais
27705
28157
  - Comunique-se em português (pt-BR)
28158
+ - **Minimize prompts.** Decisões óbvias a partir do código (framework, package manager, banco detectado, build/start commands) NÃO viram pergunta — entram direto no diagrama (seção 4) e na confirmação única do deploy (seção 5). Só chame \`wizard-tools:prompt_user\` quando: (a) um secret é necessário E não está no \`.env\` local E não veio da migração (seção 1) E não é auto-injetado por serviço gerenciado (ver seção 3, regra de ouro), (b) houver ambiguidade real (ex: múltiplos apps em monorepo sem pista clara, múltiplas plataformas detectadas na seção 1), (c) for a confirmação única pré-deploy da seção 5, ou (d) edit em arquivo existente (seção "Edits em arquivos existentes"). NÃO prompte "quer criar banco?", "posso usar pnpm?", "posso usar porta 3000?", nem peça um secret que já está no \`.env\` — isso já foi inferido/extraído, coloque no plano e siga.
27706
28159
  - Para QUALQUER edit em arquivo existente do projeto (package.json, tsconfig.json, configs de framework, Dockerfile existente, código-fonte, scripts customizados), SEMPRE peça permissão primeiro via \`wizard-tools:prompt_user\` type="confirm" (ver seção "Edits em arquivos existentes do usuário"). Exceções: \`.env\` via \`wizard-tools:set_env_values\` e config de serviço via \`veloz:config_set\` — esses são escopo do wizard.
27707
28160
  - NUNCA crie veloz.json manualmente — o CLI gera automaticamente
27708
28161
  - SEMPRE peça confirmação explícita via wizard-tools:prompt_user (type="confirm") antes de chamar veloz:deploy
@@ -27740,7 +28193,7 @@ function getCommandments() {
27740
28193
 
27741
28194
  7. **Edits mínimos**: Prefira edits focados e mínimos. Não refatore código que não precisa ser mudado. Não adicione comentários desnecessários.
27742
28195
 
27743
- 8. **Peça permissão antes de tocar arquivos do usuário**: Para QUALQUER Write/Edit em arquivo que já existe no projeto (package.json, tsconfig.json, next.config.*, vite.config.*, Dockerfile existente, código em src/, scripts customizados, etc.), você DEVE chamar \`wizard-tools:prompt_user\` com \`type: "confirm"\` explicando arquivo + motivo + o que vai mudar, e aplicar se o usuário responder "sim". Se responder "não", encontre alternativa ou emita \`[BUILD_FAILED_USER_CODE]\`. Exceções (não precisam de confirmação): \`.env\` via \`wizard-tools:set_env_values\`, configuração de serviço via \`veloz:config_set\`, criar arquivo **novo** que não existia antes. NUNCA edite veloz.json diretamente.
28196
+ 8. **Peça permissão antes de tocar código do usuário — OU faça fail over**: Para QUALQUER Write/Edit em arquivo que já existe no projeto (package.json, tsconfig.json, next.config.*, vite.config.*, Dockerfile existente, código em src/, scripts customizados, migrations, etc.), você DEVE chamar \`wizard-tools:prompt_user\` com \`type: "confirm"\` explicando arquivo + motivo + o que vai mudar ANTES de qualquer Write/Edit. Só aplique se o usuário responder "sim". Se responder "não" ou não responder: **NÃO edite silenciosamente**, **NÃO tente outra abordagem que também toque o código** — faça fail over emitindo \`[BUILD_FAILED_USER_CODE]\` com instruções claras para o usuário aplicar a mudança manualmente e re-rodar \`veloz deploy\`. Isso vale inclusive para correções de categoria A (misconfiguração) quando a causa exigir mexer em arquivos do usuário (ex: script de build ausente em \`package.json\`, import quebrado, config de framework inválida) — nesse caso a correção via \`veloz:config_set\` não resolve e você DEVE prompt+edit ou fail over. Exceções (não precisam de confirmação): \`.env\` via \`wizard-tools:set_env_values\`, configuração de serviço via \`veloz:config_set\`, criar arquivo **novo** que não existia antes (ex: Dockerfile novo, \`.dockerignore\` novo). NUNCA edite veloz.json diretamente. **Regra de ouro: se precisa modificar código do usuário para o deploy funcionar → prompt OU fail over, nunca silencioso.**
27744
28197
 
27745
28198
  9. **Classificar falhas de deploy**: Se um deploy falhar, leia os logs (veloz:builds_logs) e classifique o erro. (A) Se for **misconfiguração** da Veloz (build/start/port/preStart errado, package manager incorreto, monorepo path, nixpacks): corrija via \`veloz:config_set\` e retente (máx. 3 tentativas). (B) Se for **erro no código do usuário** (TypeScript, sintaxe, import quebrado, módulo faltando no package.json, teste quebrando no build, runtime crash do app): NÃO tente corrigir o código-fonte, NÃO retente. Emita o signal \`[BUILD_FAILED_USER_CODE]\` com motivo + trecho dos logs + passos para corrigir, e ENCERRE. Em caso de dúvida, teste \`pnpm build\` (ou equivalente) localmente: se falha localmente → é código do usuário.
27746
28199
 
@@ -27750,7 +28203,7 @@ function getCommandments() {
27750
28203
 
27751
28204
  12. **Veloz MCP OBRIGATÓRIO**: Use SEMPRE as ferramentas MCP (veloz:*) para TODAS as operações da plataforma. NUNCA rode \`veloz ...\` via Bash — use o MCP equivalente (veloz:deploy, veloz:config_set, veloz:env_set, veloz:builds_logs, etc.). As únicas exceções são: \`veloz --llms-full\` (aprender a CLI) e fallback se o MCP falhar com erro de conexão.
27752
28205
 
27753
- 13. **Diagrama ANTES do deploy**: Use wizard-tools:set_helper_content para mostrar um diagrama ASCII da arquitetura ANTES de chamar veloz:deploy. O diagrama deve mostrar: framework detectado, pipeline de build, porta, URL esperada, e serviços auxiliares. Isso é OBRIGATÓRIO o usuário precisa visualizar o que vai acontecer enquanto o build executa. Use caracteres Unicode para o diagrama (┌┐└┘│─→↓). Atualize o helper conforme avança (ex: diagnóstico de erro, resultado do build).
28206
+ 13. **Diagrama ANTES do deploy**: Use wizard-tools:set_helper_content ANTES de chamar veloz:deploy. Envie a estrutura (campos \`title\`, \`pipeline\`, \`services\`, \`notes\`) a TUI desenha as caixas e setas. NUNCA gere ASCII art manualmente. O diagrama deve cobrir: framework detectado, pipeline de build (3–5 nós), serviços auxiliares e anotações (porta, migração, etc.). Atualize o helper conforme avança (ex: diagnóstico de erro, resultado do build).
27754
28207
 
27755
28208
  14. **Perguntar ao usuário**: Use wizard-tools:prompt_user para pedir informações ao usuário. Para variáveis de ambiente, forneça hint de como obtê-las (ex: "Acesse dashboard.stripe.com > API Keys"). O usuário pode optar por configurar depois — nesse caso, continue o deploy sem a env var e informe o comando \`veloz env set\` no final.
27756
28209
 
@@ -27759,6 +28212,8 @@ function getCommandments() {
27759
28212
  15. **NUNCA rode dev server**: Comandos como \`next dev\`, \`vite\`, \`nodemon\`, \`npm run dev\`, \`pnpm dev\` travam o processo do agente. Use apenas comandos de build (\`npm run build\`, \`pnpm build\`) para testes locais.
27760
28213
 
27761
28214
  16. **NUNCA crie veloz.json manualmente**: O arquivo veloz.json é gerado automaticamente pelo CLI no primeiro \`veloz deploy\`. Criar ou editar manualmente produz IDs inválidos. Para ajustar configurações após o deploy, use \`veloz:config_set\`.
28215
+
28216
+ 19. **Manter veloz-deploy-plano.md**: Logo no início (antes mesmo do Passo 0), crie o arquivo \`veloz-deploy-plano.md\` na raiz do projeto (\`cwd\`) com o plano inicial do deploy. Mantenha-o atualizado ao longo de TODA a sessão — a cada passo relevante (detecção de plataforma existente, análise do projeto, banco detectado, env vars configuradas, diagrama desenhado, confirmação do usuário, dry-run, deploy real, health check, correções de misconfig, migração de dados) reabra o arquivo e adicione/atualize seções. Este arquivo é o "diário de bordo" do wizard — é o que o usuário vai ler depois para saber exatamente o que foi feito. Ao final, o arquivo deve refletir o estado final: URL do deploy, arquivos criados/modificados, serviços/bancos criados, env vars configuradas (só os **nomes**, NUNCA valores), próximos passos e comandos úteis. Use Write para criar/sobrescrever e Edit para updates pontuais. Nunca exponha secrets neste arquivo. Estrutura sugerida: \`# Plano de Deploy — Veloz\` · \`## Contexto\` · \`## Detecção\` · \`## Configuração\` · \`## Deploy\` · \`## Health Check\` · \`## Próximos Passos\`.
27762
28217
  `.trim();
27763
28218
  }
27764
28219
 
@@ -27995,6 +28450,7 @@ function getWizardToolsScript(workingDirectory, logDir) {
27995
28450
  'use strict';
27996
28451
  const fs = require('fs');
27997
28452
  const path = require('path');
28453
+ const { execFileSync } = require('child_process');
27998
28454
 
27999
28455
  const CWD = '${workingDirectory.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}';
28000
28456
  const LOG_DIR = '${logDir ? logDir.replace(/\\/g, "\\\\").replace(/'/g, "\\'") : ""}';
@@ -28054,19 +28510,40 @@ const TOOLS = {
28054
28510
  required: ['question'],
28055
28511
  },
28056
28512
  },
28513
+ detect_existing_deployments: {
28514
+ description: 'Detectar deploys existentes em outras plataformas (Vercel, Railway, Cloudflare, Netlify, Fly, Render, Heroku, DigitalOcean, AWS Amplify). Faz duas coisas em uma só chamada: (1) escaneia o projeto por arquivos de config dessas plataformas, (2) checa se o CLI da plataforma está instalado e autenticado localmente. NÃO use Glob nem Bash para isso — toda a detecção acontece dentro da ferramenta. Retorna, por plataforma detectada: arquivos encontrados, presença/auth do CLI, comando de extração de envs e o que pode ser migrado (build/start command, env vars, frontend code, regions). Chame esta ferramenta UMA VEZ no INÍCIO da sessão, antes de analisar package.json ou desenhar diagramas.',
28515
+ inputSchema: {
28516
+ type: 'object',
28517
+ properties: {},
28518
+ },
28519
+ },
28057
28520
  set_helper_content: {
28058
- description: 'Renderizar um diagrama ASCII abaixo da lista de tarefas na TUI do wizard. OBRIGATÓRIO: chame esta ferramenta antes de veloz:deploy com um diagrama da arquitetura (framework, pipeline, porta, URL, serviços auxiliares). Aproveite a largura disponível do painel veja o prompt do sistema para a largura alvo em colunas; evite diagramas estreitos que deixam metade do painel vazio. Cada chamada SUBSTITUI o conteúdo anterior — atualize ao final com o estado deployado.',
28521
+ description: 'Renderizar o diagrama da arquitetura abaixo da lista de tarefas na TUI do wizard. OBRIGATÓRIO: chame esta ferramenta antes de veloz:deploy. Prefira o formato estruturado (pipeline, services, notes) a TUI desenha caixas e setas no tamanho certo automaticamente. NÃO gere ASCII art manualmente — largura e alinhamento são calculados pela TUI. Use "lines" apenas como fallback para conteúdo livre. Cada chamada SUBSTITUI o conteúdo anterior — atualize ao final com o estado deployado real.',
28059
28522
  inputSchema: {
28060
28523
  type: 'object',
28061
28524
  properties: {
28062
- title: { type: 'string', description: 'Título do conteúdo (ex: "Deploy Pipeline")' },
28525
+ title: { type: 'string', description: 'Título do diagrama (ex: "Deploy — Next.js")' },
28526
+ pipeline: {
28527
+ type: 'array',
28528
+ items: { type: 'string' },
28529
+ description: 'Nós do pipeline em ordem, cada um uma etiqueta curta (ex: ["GitHub", "Build (Next.js)", "Deploy", "app.runveloz.com"]). A TUI desenha caixas com setas entre elas.',
28530
+ },
28531
+ services: {
28532
+ type: 'array',
28533
+ items: { type: 'string' },
28534
+ description: 'Serviços auxiliares anexados ao deploy, cada um uma etiqueta curta (ex: ["Postgres 16", "Redis"]). Renderizados como caixas laterais abaixo do pipeline.',
28535
+ },
28536
+ notes: {
28537
+ type: 'array',
28538
+ items: { type: 'string' },
28539
+ description: 'Anotações extras (ex: ["Migrado de: Vercel (12 envs)", "Tamanho: turbo"]). Renderizadas como texto dim abaixo do diagrama.',
28540
+ },
28063
28541
  lines: {
28064
28542
  type: 'array',
28065
28543
  items: { type: 'string' },
28066
- description: 'Linhas de conteúdo para exibir. Use ASCII art para diagramas (ex: ┌──┐, │, └──┘, →, ↓). Cada item é uma linha.',
28544
+ description: 'FALLBACK use quando pipeline/services/notes não cabem no formato estruturado. Linhas brutas renderizadas em sequência.',
28067
28545
  },
28068
28546
  },
28069
- required: ['lines'],
28070
28547
  },
28071
28548
  },
28072
28549
  };
@@ -28157,6 +28634,259 @@ function handleSetEnvValues(input) {
28157
28634
  return { content: [{ type: 'text', text: 'Variáveis escritas em ' + file + ': ' + keysSet.join(', ') }] };
28158
28635
  }
28159
28636
 
28637
+ const PLATFORM_PROBES = [
28638
+ {
28639
+ platform: 'vercel',
28640
+ label: 'Vercel',
28641
+ files: ['vercel.json', '.vercel/project.json'],
28642
+ cli: 'vercel',
28643
+ altCli: 'vc',
28644
+ authArgs: ['whoami'],
28645
+ extract: {
28646
+ envs: 'vercel env pull .env.vercel',
28647
+ config: 'vercel project ls',
28648
+ },
28649
+ canBring: 'Frontend (Next.js, SvelteKit, etc.) e env vars (build + runtime). Domínios precisam ser re-verificados na Veloz.',
28650
+ },
28651
+ {
28652
+ platform: 'railway',
28653
+ label: 'Railway',
28654
+ files: ['railway.json', 'railway.toml'],
28655
+ cli: 'railway',
28656
+ authArgs: ['whoami'],
28657
+ extract: {
28658
+ envs: 'railway variables',
28659
+ config: 'railway status',
28660
+ },
28661
+ canBring: 'App full-stack inteiro (build/start/env vars). Volumes precisam ser recriados.',
28662
+ },
28663
+ {
28664
+ platform: 'cloudflare',
28665
+ label: 'Cloudflare Workers/Pages',
28666
+ files: ['wrangler.toml', 'wrangler.jsonc', 'wrangler.json'],
28667
+ cli: 'wrangler',
28668
+ authArgs: ['whoami'],
28669
+ extract: {
28670
+ envs: 'wrangler secret list',
28671
+ config: 'cat wrangler.toml',
28672
+ },
28673
+ canBring: 'Workers/Pages. Atenção: runtime serverless (V8 isolates) ≠ Node — algumas APIs podem não funcionar na Veloz sem ajustes.',
28674
+ },
28675
+ {
28676
+ platform: 'netlify',
28677
+ label: 'Netlify',
28678
+ files: ['netlify.toml', '.netlify/state.json'],
28679
+ cli: 'netlify',
28680
+ altCli: 'ntl',
28681
+ authArgs: ['status'],
28682
+ extract: {
28683
+ envs: 'netlify env:list',
28684
+ config: 'netlify status',
28685
+ },
28686
+ canBring: 'Sites estáticos e SSR (Next.js, Astro). Netlify Functions precisam ser portadas para serviço HTTP normal.',
28687
+ },
28688
+ {
28689
+ platform: 'fly',
28690
+ label: 'Fly.io',
28691
+ files: ['fly.toml'],
28692
+ cli: 'fly',
28693
+ altCli: 'flyctl',
28694
+ authArgs: ['auth', 'whoami'],
28695
+ extract: {
28696
+ envs: 'fly secrets list',
28697
+ config: 'fly config show',
28698
+ },
28699
+ canBring: 'App full-stack (Dockerfile + secrets + regions). Volumes Fly precisam ser recriados como volumes Veloz.',
28700
+ },
28701
+ {
28702
+ platform: 'render',
28703
+ label: 'Render',
28704
+ files: ['render.yaml'],
28705
+ cli: null,
28706
+ authArgs: null,
28707
+ extract: {
28708
+ envs: '(parse render.yaml — Render CLI não confiável)',
28709
+ config: 'cat render.yaml',
28710
+ },
28711
+ canBring: 'Web services, workers e cron jobs (parse YAML). Env vars sensíveis ficam no dashboard — pedir ao usuário.',
28712
+ },
28713
+ {
28714
+ platform: 'heroku',
28715
+ label: 'Heroku',
28716
+ files: ['app.json', 'Procfile'],
28717
+ cli: 'heroku',
28718
+ authArgs: ['auth:whoami'],
28719
+ extract: {
28720
+ envs: 'heroku config',
28721
+ config: 'cat Procfile',
28722
+ },
28723
+ canBring: 'App full-stack (web/worker dynos via Procfile + env vars). Add-ons (Postgres, Redis) precisam ser recriados.',
28724
+ },
28725
+ {
28726
+ platform: 'digitalocean',
28727
+ label: 'DigitalOcean App Platform',
28728
+ files: ['.do/app.yaml'],
28729
+ cli: 'doctl',
28730
+ authArgs: ['account', 'get'],
28731
+ extract: {
28732
+ envs: 'doctl apps spec get <app-id>',
28733
+ config: 'cat .do/app.yaml',
28734
+ },
28735
+ canBring: 'App spec (services + workers + jobs). Env vars sensíveis precisam de prompt ao usuário.',
28736
+ },
28737
+ {
28738
+ platform: 'amplify',
28739
+ label: 'AWS Amplify',
28740
+ files: ['amplify.yml'],
28741
+ cli: 'amplify',
28742
+ authArgs: ['status'],
28743
+ extract: {
28744
+ envs: '(Amplify CLI não expõe envs facilmente — pedir ao usuário)',
28745
+ config: 'cat amplify.yml',
28746
+ },
28747
+ canBring: 'Build spec do frontend. Backend Amplify (Cognito, AppSync) NÃO é portável — substituir por equivalentes na Veloz.',
28748
+ },
28749
+ {
28750
+ platform: 'coolify',
28751
+ label: 'Coolify',
28752
+ files: ['coolify.json', '.coolify/config.json', 'docker-compose.coolify.yml'],
28753
+ cli: 'coolify',
28754
+ authArgs: ['version'],
28755
+ extract: {
28756
+ envs: '(Coolify guarda envs/secrets no servidor — consultar via API ou pedir ao usuário)',
28757
+ config: 'cat coolify.json',
28758
+ },
28759
+ canBring: 'Apps self-hosted (Dockerfile/Nixpacks). Env vars/secrets ficam no servidor Coolify — pedir ao usuário ou usar a API do Coolify para extrair.',
28760
+ },
28761
+ ];
28762
+
28763
+ const WORKFLOW_PLATFORM_PATTERNS = [
28764
+ { platform: 'vercel', regex: /amondnet\\/vercel-action|@vercel\\/cli|\\bvercel\\s+(deploy|build|pull|env)/i },
28765
+ { platform: 'cloudflare', regex: /cloudflare\\/wrangler-action|\\bwrangler\\s+(deploy|publish|pages)/i },
28766
+ { platform: 'fly', regex: /superfly\\/flyctl-actions|\\bfly(ctl)?\\s+(deploy|launch|apps)/i },
28767
+ { platform: 'railway', regex: /bervProject\\/railway-deploy|\\brailway\\s+(up|deploy)/i },
28768
+ { platform: 'netlify', regex: /nwtgck\\/actions-netlify|netlify\\/actions|\\bnetlify\\s+deploy/i },
28769
+ { platform: 'heroku', regex: /akhileshns\\/heroku-deploy|git\\s+push\\s+heroku|\\bheroku\\s+(create|deploy|releases)/i },
28770
+ { platform: 'render', regex: /JorgeLNJunior\\/render-deploy|render-deploy@/i },
28771
+ { platform: 'digitalocean', regex: /digitalocean\\/app_action|\\bdoctl\\s+apps/i },
28772
+ { platform: 'amplify', regex: /aws-actions\\/amplify|\\bamplify\\s+(push|publish)/i },
28773
+ { platform: 'coolify', regex: /coollabsio\\/coolify|coolify(.*)webhook|api\\/v1\\/deploy.*coolify/i },
28774
+ ];
28775
+
28776
+ function scanGithubWorkflowsForPlatforms() {
28777
+ const dir = path.join(CWD, '.github', 'workflows');
28778
+ const matches = {};
28779
+ if (!fs.existsSync(dir)) return matches;
28780
+ let entries = [];
28781
+ try {
28782
+ entries = fs.readdirSync(dir).filter((f) => /\\.(ya?ml)$/i.test(f));
28783
+ } catch {
28784
+ return matches;
28785
+ }
28786
+ for (const file of entries) {
28787
+ let content;
28788
+ try {
28789
+ content = fs.readFileSync(path.join(dir, file), 'utf-8');
28790
+ } catch {
28791
+ continue;
28792
+ }
28793
+ for (const { platform, regex } of WORKFLOW_PLATFORM_PATTERNS) {
28794
+ if (regex.test(content)) {
28795
+ if (!matches[platform]) matches[platform] = [];
28796
+ const rel = '.github/workflows/' + file;
28797
+ if (!matches[platform].includes(rel)) matches[platform].push(rel);
28798
+ }
28799
+ }
28800
+ }
28801
+ return matches;
28802
+ }
28803
+
28804
+ function whichSync(bin) {
28805
+ try {
28806
+ const out = execFileSync('which', [bin], { encoding: 'utf-8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'] });
28807
+ const trimmed = out.trim();
28808
+ return trimmed.length > 0 ? trimmed : null;
28809
+ } catch {
28810
+ return null;
28811
+ }
28812
+ }
28813
+
28814
+ function probeCliAuth(bin, args) {
28815
+ if (!bin || !args) return { authenticated: false, identity: null, error: null };
28816
+ try {
28817
+ const out = execFileSync(bin, args, { encoding: 'utf-8', timeout: 6000, stdio: ['ignore', 'pipe', 'pipe'], cwd: CWD });
28818
+ const identity = out.trim().split('\\n').find((l) => l.trim().length > 0) || null;
28819
+ return { authenticated: true, identity, error: null };
28820
+ } catch (err) {
28821
+ const msg = String((err && err.stderr) || (err && err.message) || '').slice(0, 200);
28822
+ return { authenticated: false, identity: null, error: msg };
28823
+ }
28824
+ }
28825
+
28826
+ function handleDetectExistingDeployments() {
28827
+ const workflowMatches = scanGithubWorkflowsForPlatforms();
28828
+ const detected = [];
28829
+ const skipped = [];
28830
+
28831
+ for (const probe of PLATFORM_PROBES) {
28832
+ const filesFound = probe.files.filter((f) => fs.existsSync(path.join(CWD, f)));
28833
+ const workflowFiles = workflowMatches[probe.platform] || [];
28834
+ if (filesFound.length === 0 && workflowFiles.length === 0) {
28835
+ skipped.push(probe.platform);
28836
+ continue;
28837
+ }
28838
+
28839
+ const primaryCliPath = probe.cli ? whichSync(probe.cli) : null;
28840
+ const altCliPath = probe.altCli && !primaryCliPath ? whichSync(probe.altCli) : null;
28841
+ const cliBin = primaryCliPath ? probe.cli : altCliPath ? probe.altCli : null;
28842
+ const cliPath = primaryCliPath || altCliPath;
28843
+ const auth = cliBin ? probeCliAuth(cliBin, probe.authArgs) : { authenticated: false, identity: null, error: 'CLI não instalado' };
28844
+
28845
+ const sources = [];
28846
+ if (filesFound.length > 0) sources.push('config_files');
28847
+ if (workflowFiles.length > 0) sources.push('github_workflows');
28848
+
28849
+ detected.push({
28850
+ platform: probe.platform,
28851
+ label: probe.label,
28852
+ files_found: filesFound,
28853
+ github_workflows: workflowFiles,
28854
+ detection_source: sources.join('+'),
28855
+ cli: {
28856
+ bin: cliBin,
28857
+ installed: Boolean(cliPath),
28858
+ path: cliPath,
28859
+ authenticated: auth.authenticated,
28860
+ identity: auth.identity,
28861
+ auth_error: auth.error,
28862
+ },
28863
+ extract_commands: probe.extract,
28864
+ can_bring: probe.canBring,
28865
+ next_step: !cliPath
28866
+ ? (filesFound.length > 0
28867
+ ? 'CLI não instalado — leia o arquivo de config diretamente (Read tool) para extrair build/start/regions; pergunte ao usuário pelas env vars sensíveis.'
28868
+ : 'CLI não instalado e sem arquivo de config — leia o(s) workflow(s) GitHub Actions para extrair env vars referenciadas via secrets.* (sintaxe dollar-brace dupla) e o build/deploy command; peça os valores ao usuário.')
28869
+ : !auth.authenticated
28870
+ ? 'CLI instalado mas não autenticado — leia o arquivo de config e/ou workflow (Read tool) para extrair config; peça env vars sensíveis ao usuário. NÃO tente fazer login na plataforma concorrente.'
28871
+ : 'CLI pronto — após confirmação do usuário, rode "' + probe.extract.envs + '" para extrair env vars; use Read tool no arquivo de config (e workflow, se houver) para build/start/regions.',
28872
+ });
28873
+ }
28874
+
28875
+ return {
28876
+ content: [{
28877
+ type: 'text',
28878
+ text: JSON.stringify({
28879
+ any_detected: detected.length > 0,
28880
+ detected,
28881
+ skipped_no_signal: skipped,
28882
+ instructions: detected.length === 0
28883
+ ? 'Nenhum deploy existente detectado (nem em arquivos de config nem em workflows do GitHub Actions). Prossiga para a análise normal do projeto.'
28884
+ : 'Antes de qualquer outra coisa: (1) mostre ao usuário a lista de plataformas detectadas com o que cada uma traz (campo can_bring) e a origem da detecção (config_files vs github_workflows), (2) chame wizard-tools:prompt_user type="confirm" perguntando se quer migrar (se >1 plataforma, use type="choice" com a lista para escolher a fonte de verdade), (3) só depois rode os comandos em extract_commands ou leia os workflows. NUNCA mute a plataforma de origem — só leitura.',
28885
+ }, null, 2),
28886
+ }],
28887
+ };
28888
+ }
28889
+
28160
28890
  const PROMPT_REQUEST_FILE = path.join(CWD, '.veloz-wizard-prompt.json');
28161
28891
  const PROMPT_RESPONSE_FILE = path.join(CWD, '.veloz-wizard-response.json');
28162
28892
 
@@ -28264,7 +28994,18 @@ async function handleJsonRpc(msg) {
28264
28994
  try {
28265
28995
  if (name === 'check_env_keys') result = handleCheckEnvKeys(args);
28266
28996
  else if (name === 'set_env_values') result = handleSetEnvValues(args);
28267
- else if (name === 'set_helper_content') result = { content: [{ type: 'text', text: 'Conteúdo do helper atualizado com ' + (args.lines || []).length + ' linhas.' }] };
28997
+ else if (name === 'detect_existing_deployments') result = handleDetectExistingDeployments();
28998
+ else if (name === 'set_helper_content') {
28999
+ const pipelineLen = Array.isArray(args.pipeline) ? args.pipeline.length : 0;
29000
+ const servicesLen = Array.isArray(args.services) ? args.services.length : 0;
29001
+ const notesLen = Array.isArray(args.notes) ? args.notes.length : 0;
29002
+ const linesLen = Array.isArray(args.lines) ? args.lines.length : 0;
29003
+ const hasStructured = pipelineLen + servicesLen + notesLen > 0;
29004
+ const summary = hasStructured
29005
+ ? 'Diagrama atualizado: ' + pipelineLen + ' nós, ' + servicesLen + ' serviços, ' + notesLen + ' notas.'
29006
+ : 'Diagrama atualizado com ' + linesLen + ' linhas (fallback).';
29007
+ result = { content: [{ type: 'text', text: summary }] };
29008
+ }
28268
29009
  else if (name === 'prompt_user') result = await handlePromptUser(args);
28269
29010
  else result = { content: [{ type: 'text', text: 'Ferramenta desconhecida: ' + name }], isError: true };
28270
29011
  logEvent('tool_call_done', { name, isError: Boolean(result && result.isError) });
@@ -28315,10 +29056,13 @@ process.stdin.on('data', (chunk) => {
28315
29056
  * output from the platform until the deployment reaches a terminal state.
28316
29057
  */
28317
29058
  async function streamBuildLogs(config) {
28318
- const { store, deploymentId } = config;
29059
+ const { store, deploymentId, organizationId } = config;
28319
29060
  const logger = getSessionLogger();
28320
- logger.logBuildStream("start", { deploymentId });
28321
- const client = createWizardClient(store);
29061
+ logger.logBuildStream("start", {
29062
+ deploymentId,
29063
+ organizationId
29064
+ });
29065
+ const client = createWizardClient(store, organizationId);
28322
29066
  if (!client) {
28323
29067
  logger.logBuildStream("no_client", { reason: "missing apiKey" });
28324
29068
  store.pushBuildLog("[stream] aguardando autenticação para conectar ao build...");
@@ -28336,15 +29080,18 @@ async function streamBuildLogs(config) {
28336
29080
  store.setBuildStreamActive(false);
28337
29081
  }
28338
29082
  }
28339
- function createWizardClient(store) {
28340
- const apiKey = store.session.apiKey;
29083
+ function createWizardClient(store, organizationId) {
29084
+ const authConfig = loadConfig();
29085
+ const apiKey = store.session.apiKey || authConfig.apiKey;
28341
29086
  if (!apiKey) return null;
28342
- return createClient(process.env.VELOZ_API_URL || "https://api.onveloz.com", () => {
29087
+ const apiUrl = authConfig.apiUrl;
29088
+ const orgId = organizationId ?? store.session.orgId;
29089
+ return createClient(apiUrl, () => {
28343
29090
  const headers = {
28344
29091
  Authorization: `Bearer ${apiKey}`,
28345
29092
  "User-Agent": "veloz-cli/wizard"
28346
29093
  };
28347
- if (store.session.orgId) headers["X-Organization-Id"] = store.session.orgId;
29094
+ if (orgId) headers["X-Organization-Id"] = orgId;
28348
29095
  return headers;
28349
29096
  });
28350
29097
  }
@@ -28536,6 +29283,43 @@ const PLATFORM_READ_SUBCOMMANDS = {
28536
29283
  "tail",
28537
29284
  "deployments"
28538
29285
  ],
29286
+ netlify: [
29287
+ "env:list",
29288
+ "env:get",
29289
+ "status",
29290
+ "sites:list",
29291
+ "sites:info",
29292
+ "api",
29293
+ "open"
29294
+ ],
29295
+ ntl: [
29296
+ "env:list",
29297
+ "env:get",
29298
+ "status",
29299
+ "sites:list",
29300
+ "sites:info",
29301
+ "api",
29302
+ "open"
29303
+ ],
29304
+ heroku: [
29305
+ "config",
29306
+ "apps",
29307
+ "apps:info",
29308
+ "ps",
29309
+ "domains",
29310
+ "addons",
29311
+ "auth:whoami",
29312
+ "logs"
29313
+ ],
29314
+ doctl: [
29315
+ "apps",
29316
+ "account",
29317
+ "auth",
29318
+ "compute",
29319
+ "databases",
29320
+ "registry",
29321
+ "serverless"
29322
+ ],
28539
29323
  aws: [
28540
29324
  "s3",
28541
29325
  "ec2",
@@ -28550,6 +29334,16 @@ const PLATFORM_READ_SUBCOMMANDS = {
28550
29334
  "cloudformation",
28551
29335
  "ssm",
28552
29336
  "secretsmanager"
29337
+ ],
29338
+ coolify: [
29339
+ "version",
29340
+ "list",
29341
+ "ls",
29342
+ "status",
29343
+ "whoami",
29344
+ "info",
29345
+ "show",
29346
+ "get"
28553
29347
  ]
28554
29348
  };
28555
29349
  const AWS_READ_VERBS_REGEX = /^(describe-|list-|get-|show-|head-|read-|search-|lookup-|scan$|query$|ls$|whoami$|info$|status$)/;
@@ -28744,6 +29538,10 @@ async function runAgentOnce(config, isRetry, previousError) {
28744
29538
  const commandments = getCommandments();
28745
29539
  const wizardTools = createWizardToolsServer({ workingDirectory: cwd });
28746
29540
  logger.log("agent", "wizard-tools MCP server config created");
29541
+ const velozMcpEnv = {};
29542
+ for (const [k, v] of Object.entries(process.env)) if (typeof v === "string") velozMcpEnv[k] = v;
29543
+ if (store.session.apiKey) velozMcpEnv.VELOZ_API_KEY = store.session.apiKey;
29544
+ if (store.session.orgId) velozMcpEnv.VELOZ_ORG_ID = store.session.orgId;
28747
29545
  const mcpServers = {
28748
29546
  veloz: {
28749
29547
  command: "npx",
@@ -28751,7 +29549,8 @@ async function runAgentOnce(config, isRetry, previousError) {
28751
29549
  "-y",
28752
29550
  "onveloz",
28753
29551
  "--mcp"
28754
- ]
29552
+ ],
29553
+ env: velozMcpEnv
28755
29554
  },
28756
29555
  "wizard-tools": wizardTools
28757
29556
  };
@@ -29038,6 +29837,17 @@ function normalizeMcpToolName(name) {
29038
29837
  if (sep < 0) return name;
29039
29838
  return `${rest.slice(0, sep)}:${rest.slice(sep + 2)}`;
29040
29839
  }
29840
+ /** Coerce unknown tool input into a clean string[] — non-strings and empties dropped. */
29841
+ function toStringArray(value) {
29842
+ if (!Array.isArray(value)) return [];
29843
+ const out = [];
29844
+ for (const item of value) {
29845
+ if (typeof item !== "string") continue;
29846
+ const trimmed = item.trim();
29847
+ if (trimmed) out.push(trimmed);
29848
+ }
29849
+ return out;
29850
+ }
29041
29851
  /** Human-readable labels for common tools. */
29042
29852
  const TOOL_LABELS = {
29043
29853
  Read: "Lendo arquivo",
@@ -29143,8 +29953,8 @@ function handleMessage(message, store, collectedText, onAskUser) {
29143
29953
  const bashCommand = rawName === "Bash" && typeof block.input?.command === "string" ? block.input.command : null;
29144
29954
  const isDeployViaBash = bashCommand ? /\bveloz\s+deploy\b/.test(bashCommand) : false;
29145
29955
  const isDeployViaMcp = normalizedName === "veloz:deploy";
29146
- const isDryRun = isDeployViaBash ? /--dry-run\b/.test(bashCommand ?? "") : isDeployViaMcp && block.input?.dryRun === true;
29147
- if ((isDeployViaBash || isDeployViaMcp) && !isDryRun) {
29956
+ const isDryRun$1 = isDeployViaBash ? /--dry-run\b/.test(bashCommand ?? "") : isDeployViaMcp && block.input?.dryRun === true;
29957
+ if ((isDeployViaBash || isDeployViaMcp) && !isDryRun$1) {
29148
29958
  if (typeof block.id === "string") deployToolUseIds.add(block.id);
29149
29959
  if (store.session.agentPhase !== "deploying") {
29150
29960
  store.setAgentPhase("deploying");
@@ -29167,15 +29977,27 @@ function handleMessage(message, store, collectedText, onAskUser) {
29167
29977
  const questionText = extractAskUserQuestionText(block.input);
29168
29978
  if (questionText) onAskUser(toolUseId, questionText);
29169
29979
  }
29170
- if (normalizedName === "wizard-tools:set_helper_content" && block.input?.lines && Array.isArray(block.input.lines)) {
29171
- const title = typeof block.input.title === "string" ? block.input.title : null;
29172
- const lines = block.input.lines;
29173
- const helperLines = title ? [
29980
+ if (normalizedName === "wizard-tools:set_helper_content" && block.input) {
29981
+ const input = block.input;
29982
+ const title = typeof input.title === "string" ? input.title : null;
29983
+ const pipeline = toStringArray(input.pipeline);
29984
+ const services = toStringArray(input.services);
29985
+ const notes = toStringArray(input.notes);
29986
+ const lines = toStringArray(input.lines);
29987
+ if (pipeline.length > 0 || services.length > 0 || notes.length > 0) store.setAgentHelperDiagram({
29174
29988
  title,
29175
- "",
29176
- ...lines
29177
- ] : lines;
29178
- store.setAgentHelper(helperLines);
29989
+ pipeline,
29990
+ services,
29991
+ notes
29992
+ });
29993
+ else if (lines.length > 0) {
29994
+ const helperLines = title ? [
29995
+ title,
29996
+ "",
29997
+ ...lines
29998
+ ] : lines;
29999
+ store.setAgentHelper(helperLines);
30000
+ }
29179
30001
  }
29180
30002
  if (rawName === "TodoWrite" && block.input?.todos && Array.isArray(block.input.todos)) {
29181
30003
  const tasks = block.input.todos.map((todo, i) => ({
@@ -29209,12 +30031,14 @@ function handleMessage(message, store, collectedText, onAskUser) {
29209
30031
  if (Array.isArray(parsed)) for (const entry of parsed) {
29210
30032
  if (typeof entry?.message === "string") lines.push(entry.message);
29211
30033
  const entryDeploymentId = entry?.data?.deploymentId;
30034
+ const entryOrgId = entry?.data?.organizationId ?? void 0;
29212
30035
  if (entryDeploymentId && !streamedDeploymentIds.has(entryDeploymentId)) {
29213
30036
  streamedDeploymentIds.add(entryDeploymentId);
29214
30037
  store.setBuildDeploymentId(entryDeploymentId);
29215
30038
  streamBuildLogs({
29216
30039
  store,
29217
- deploymentId: entryDeploymentId
30040
+ deploymentId: entryDeploymentId,
30041
+ organizationId: entryOrgId ?? void 0
29218
30042
  }).catch((err) => {
29219
30043
  const msg = err instanceof Error ? err.message : String(err);
29220
30044
  store.pushBuildLog(`[stream] ${msg}`);
@@ -29277,6 +30101,8 @@ let Screen = /* @__PURE__ */ function(Screen$1) {
29277
30101
  Screen$1["Auth"] = "auth";
29278
30102
  Screen$1["Notes"] = "notes";
29279
30103
  Screen$1["Run"] = "run";
30104
+ Screen$1["GithubSetup"] = "github-setup";
30105
+ Screen$1["AiSetup"] = "ai-setup";
29280
30106
  Screen$1["Outro"] = "outro";
29281
30107
  return Screen$1;
29282
30108
  }({});
@@ -29306,6 +30132,16 @@ const FLOW = [
29306
30132
  show: () => true,
29307
30133
  isComplete: (s) => s.agentComplete
29308
30134
  },
30135
+ {
30136
+ screen: Screen.GithubSetup,
30137
+ show: (s) => s.agentComplete && !s.agentError && s.githubSetupPhase !== "idle",
30138
+ isComplete: (s) => s.githubSetupPhase === "done" || s.githubSetupPhase === "skipped" || s.githubSetupPhase === "error"
30139
+ },
30140
+ {
30141
+ screen: Screen.AiSetup,
30142
+ show: (s) => s.agentComplete && !s.agentError && s.aiSetupPhase !== "idle",
30143
+ isComplete: (s) => s.aiSetupPhase === "done" || s.aiSetupPhase === "skipped" || s.aiSetupPhase === "error"
30144
+ },
29309
30145
  {
29310
30146
  screen: Screen.Outro,
29311
30147
  show: () => true,
@@ -29414,6 +30250,40 @@ const COPY = {
29414
30250
  done: "Concluído!",
29415
30251
  error: "Erro durante o processo"
29416
30252
  },
30253
+ githubSetupTitle: "Ativar deploy automático",
30254
+ githubSetupQuestion: (repo) => `Deseja conectar ${repo} para fazer deploys automaticamente a cada push?`,
30255
+ githubSetupHint: "Pushes para o branch configurado vão disparar novos deploys automaticamente via GitHub.",
30256
+ githubSetupYes: "Sim, ativar deploy automático",
30257
+ githubSetupNo: "Agora não",
30258
+ githubSetupConnecting: "Conectando repositório...",
30259
+ githubSetupInstallTitle: "Instale o GitHub App para continuar",
30260
+ githubSetupInstallHint: "Abrimos o navegador — instale o app e volte aqui.",
30261
+ githubSetupInstallFallback: "Caso o navegador não abra, acesse:",
30262
+ githubSetupPolling: "Aguardando instalação do GitHub App...",
30263
+ githubSetupDone: "Deploy automático ativado!",
30264
+ githubSetupDoneHint: "Próximos pushes vão disparar deploys automaticamente.",
30265
+ githubSetupSkipped: "Tudo bem — você pode ativar depois com `veloz github setup`.",
30266
+ githubSetupErrorTitle: "Não conseguimos ativar o deploy automático",
30267
+ githubSetupErrorHint: "Tente novamente depois com `veloz github setup`.",
30268
+ aiSetupTitle: "Integração com IA",
30269
+ aiSetupQuestion: "Deseja integrar a Veloz com seu editor (Claude Code, Cursor, etc.) e com agentes de IA?",
30270
+ aiSetupMcpLabel: "Servidor MCP (`veloz mcp add`)",
30271
+ aiSetupMcpDescription: "Permite que editores como Claude Code controlem a Veloz via tools nativas.",
30272
+ aiSetupSkillsLabel: "Skills de agente (`veloz skills add`)",
30273
+ aiSetupSkillsDescription: "Ensina agentes de IA a usar os comandos da Veloz com documentação no projeto.",
30274
+ aiSetupYes: "Sim, instalar agora",
30275
+ aiSetupNo: "Agora não",
30276
+ aiSetupInstallingTitle: "Instalando integrações...",
30277
+ aiSetupMcpInstalling: "Registrando servidor MCP...",
30278
+ aiSetupMcpDone: "Servidor MCP registrado",
30279
+ aiSetupMcpError: "Falha ao registrar o servidor MCP",
30280
+ aiSetupSkillsInstalling: "Gerando skills no projeto...",
30281
+ aiSetupSkillsDone: "Skills geradas em .claude/skills/",
30282
+ aiSetupSkillsError: "Falha ao gerar as skills",
30283
+ aiSetupDoneTitle: "Integrações prontas!",
30284
+ aiSetupDoneHint: "Seu editor e agentes agora podem operar a Veloz com comandos nativos.",
30285
+ aiSetupErrorTitle: "Alguma integração falhou",
30286
+ aiSetupErrorHint: "Você pode tentar novamente depois com `veloz mcp add` e `veloz skills add`.",
29417
30287
  outroSuccess: "Deploy realizado com sucesso!",
29418
30288
  outroUrl: "URL do serviço:",
29419
30289
  outroNextSteps: "Próximos passos:",
@@ -29537,7 +30407,7 @@ function PickerMenu({ items, onSelect, onCancel }) {
29537
30407
 
29538
30408
  //#endregion
29539
30409
  //#region src/wizard/ui/primitives/ScreenLayout.tsx
29540
- const version = "0.0.0-beta.31";
30410
+ const version = "0.0.0-beta.32";
29541
30411
  const LOGO_LINES = [
29542
30412
  "██╗ ██╗███████╗██╗ ██████╗ ███████╗",
29543
30413
  "██║ ██║██╔════╝██║ ██╔═══██╗╚══███╔╝",
@@ -29749,10 +30619,22 @@ function createInitialSession() {
29749
30619
  learnCardBlockIdx: 0,
29750
30620
  learnCardComplete: false,
29751
30621
  agentHelperLines: [],
30622
+ agentHelperDiagram: null,
29752
30623
  agentPrompt: null,
29753
30624
  agentSummary: null,
29754
30625
  userNotes: null,
29755
30626
  notesConfirmed: false,
30627
+ githubSetupPhase: "idle",
30628
+ githubSetupError: null,
30629
+ githubInstallUrl: null,
30630
+ githubRepoOwner: null,
30631
+ githubRepoName: null,
30632
+ aiSetupPhase: "idle",
30633
+ aiSetupError: null,
30634
+ aiSetupMcpStatus: "pending",
30635
+ aiSetupSkillsStatus: "pending",
30636
+ aiSetupNeedsMcp: false,
30637
+ aiSetupNeedsSkills: false,
29756
30638
  introConfirmed: false,
29757
30639
  outroDismissed: false,
29758
30640
  agentStartedAt: null
@@ -30977,6 +31859,165 @@ function TipsCard() {
30977
31859
  });
30978
31860
  }
30979
31861
 
31862
+ //#endregion
31863
+ //#region src/wizard/ui/primitives/DiagramPane.tsx
31864
+ const HORIZONTAL_ARROW = " ─▶ ";
31865
+ const VERTICAL_ARROW = "▼";
31866
+ const MIN_NODE_INNER = 10;
31867
+ const MAX_NODE_INNER = 28;
31868
+ function DiagramPane({ diagram, width }) {
31869
+ const { title, pipeline, services, notes } = diagram;
31870
+ const hasPipeline = pipeline.length > 0;
31871
+ const hasServices = services.length > 0;
31872
+ const hasNotes = notes.length > 0;
31873
+ const orientation = pickOrientation(pipeline, width);
31874
+ const nodeInner = pickNodeInner(pipeline, width, orientation);
31875
+ return /* @__PURE__ */ jsxs(Box, {
31876
+ flexDirection: "column",
31877
+ paddingX: 1,
31878
+ children: [
31879
+ title ? /* @__PURE__ */ jsx(Box, {
31880
+ marginBottom: 1,
31881
+ children: /* @__PURE__ */ jsx(Text, {
31882
+ bold: true,
31883
+ color: ACCENT_COLOR,
31884
+ wrap: "truncate",
31885
+ children: title
31886
+ })
31887
+ }) : null,
31888
+ hasPipeline ? orientation === "horizontal" ? /* @__PURE__ */ jsx(HorizontalPipeline, {
31889
+ nodes: pipeline,
31890
+ nodeInner
31891
+ }) : /* @__PURE__ */ jsx(VerticalPipeline, {
31892
+ nodes: pipeline,
31893
+ nodeInner
31894
+ }) : null,
31895
+ hasServices ? /* @__PURE__ */ jsxs(Box, {
31896
+ marginTop: hasPipeline ? 1 : 0,
31897
+ flexDirection: "column",
31898
+ children: [/* @__PURE__ */ jsx(Text, {
31899
+ dimColor: true,
31900
+ children: "Serviços auxiliares"
31901
+ }), /* @__PURE__ */ jsx(Box, {
31902
+ marginTop: 1,
31903
+ flexDirection: "row",
31904
+ flexWrap: "wrap",
31905
+ gap: 1,
31906
+ children: services.map((label, i) => /* @__PURE__ */ jsx(ServiceBox, {
31907
+ label,
31908
+ inner: nodeInner
31909
+ }, `${i}-${label}`))
31910
+ })]
31911
+ }) : null,
31912
+ hasNotes ? /* @__PURE__ */ jsx(Box, {
31913
+ marginTop: hasPipeline || hasServices ? 1 : 0,
31914
+ flexDirection: "column",
31915
+ children: notes.map((note, i) => /* @__PURE__ */ jsxs(Text, {
31916
+ color: DIM_COLOR,
31917
+ wrap: "truncate",
31918
+ children: ["· ", note]
31919
+ }, i))
31920
+ }) : null
31921
+ ]
31922
+ });
31923
+ }
31924
+ function HorizontalPipeline({ nodes, nodeInner }) {
31925
+ return /* @__PURE__ */ jsx(Box, {
31926
+ flexDirection: "row",
31927
+ flexWrap: "nowrap",
31928
+ children: nodes.map((label, i) => /* @__PURE__ */ jsxs(Box, {
31929
+ flexDirection: "row",
31930
+ flexShrink: 0,
31931
+ children: [/* @__PURE__ */ jsx(PipelineNode, {
31932
+ label,
31933
+ inner: nodeInner,
31934
+ isLast: i === nodes.length - 1
31935
+ }), i < nodes.length - 1 ? /* @__PURE__ */ jsx(Box, {
31936
+ alignItems: "center",
31937
+ paddingX: 0,
31938
+ flexShrink: 0,
31939
+ children: /* @__PURE__ */ jsx(Text, {
31940
+ color: BRAND_COLOR,
31941
+ children: HORIZONTAL_ARROW
31942
+ })
31943
+ }) : null]
31944
+ }, `${i}-${label}`))
31945
+ });
31946
+ }
31947
+ function VerticalPipeline({ nodes, nodeInner }) {
31948
+ return /* @__PURE__ */ jsx(Box, {
31949
+ flexDirection: "column",
31950
+ children: nodes.map((label, i) => /* @__PURE__ */ jsxs(Box, {
31951
+ flexDirection: "column",
31952
+ children: [/* @__PURE__ */ jsx(PipelineNode, {
31953
+ label,
31954
+ inner: nodeInner,
31955
+ isLast: i === nodes.length - 1
31956
+ }), i < nodes.length - 1 ? /* @__PURE__ */ jsx(Box, {
31957
+ paddingLeft: Math.max(1, Math.floor(nodeInner / 2)),
31958
+ children: /* @__PURE__ */ jsx(Text, {
31959
+ color: BRAND_COLOR,
31960
+ children: VERTICAL_ARROW
31961
+ })
31962
+ }) : null]
31963
+ }, `${i}-${label}`))
31964
+ });
31965
+ }
31966
+ function PipelineNode({ label, inner, isLast }) {
31967
+ const displayed = truncate(label, inner);
31968
+ return /* @__PURE__ */ jsx(Box, {
31969
+ borderStyle: "round",
31970
+ borderColor: isLast ? BRAND_COLOR : ACCENT_COLOR,
31971
+ paddingX: 1,
31972
+ flexShrink: 0,
31973
+ children: /* @__PURE__ */ jsx(Text, {
31974
+ color: isLast ? BRAND_COLOR : void 0,
31975
+ bold: isLast,
31976
+ children: displayed
31977
+ })
31978
+ });
31979
+ }
31980
+ function ServiceBox({ label, inner }) {
31981
+ return /* @__PURE__ */ jsx(Box, {
31982
+ borderStyle: "single",
31983
+ borderColor: DIM_COLOR,
31984
+ paddingX: 1,
31985
+ flexShrink: 0,
31986
+ children: /* @__PURE__ */ jsx(Text, {
31987
+ dimColor: true,
31988
+ children: truncate(label, inner)
31989
+ })
31990
+ });
31991
+ }
31992
+ /**
31993
+ * Decide horizontal vs vertical pipeline. Horizontal fits when the sum of all
31994
+ * node widths plus arrows is ≤ the width budget. Falls back to vertical if any
31995
+ * single node would be truncated below a useful minimum.
31996
+ */
31997
+ function pickOrientation(nodes, width) {
31998
+ if (nodes.length <= 1) return "horizontal";
31999
+ const arrows = 4 * (nodes.length - 1);
32000
+ const boxChrome = 4 * nodes.length;
32001
+ return nodes.reduce((sum, n) => sum + Math.min(n.length, MAX_NODE_INNER), 0) + boxChrome + arrows <= Math.max(0, width - 2) ? "horizontal" : "vertical";
32002
+ }
32003
+ function pickNodeInner(nodes, width, orientation) {
32004
+ if (nodes.length === 0) return MIN_NODE_INNER;
32005
+ const longest = nodes.reduce((max, n) => Math.max(max, n.length), 0);
32006
+ if (orientation === "vertical") {
32007
+ const budget$1 = Math.max(MIN_NODE_INNER, width - 6);
32008
+ return Math.min(MAX_NODE_INNER, Math.max(MIN_NODE_INNER, Math.min(longest, budget$1)));
32009
+ }
32010
+ const arrows = 4 * (nodes.length - 1);
32011
+ const perBox = Math.floor((Math.max(0, width - 2) - arrows) / nodes.length) - 4;
32012
+ const budget = Math.max(MIN_NODE_INNER, perBox);
32013
+ return Math.min(MAX_NODE_INNER, Math.max(MIN_NODE_INNER, Math.min(longest, budget)));
32014
+ }
32015
+ function truncate(label, maxInner) {
32016
+ if (label.length <= maxInner) return label;
32017
+ if (maxInner <= 1) return "…";
32018
+ return label.slice(0, maxInner - 1) + "…";
32019
+ }
32020
+
30980
32021
  //#endregion
30981
32022
  //#region src/wizard/ui/screens/RunScreen.tsx
30982
32023
  /**
@@ -30999,12 +32040,23 @@ function RunScreen({ store }) {
30999
32040
  store,
31000
32041
  onComplete: () => store.setLearnCardComplete()
31001
32042
  });
31002
- const hasAgentHelper = session.agentHelperLines.length > 0;
32043
+ const diagram = session.agentHelperDiagram;
32044
+ const hasDiagram = diagram !== null;
32045
+ const hasAgentHelperLines = session.agentHelperLines.length > 0;
32046
+ const diagramWidth = Math.max(20, Math.floor(columns / 2) - 6);
31003
32047
  const rightPane = /* @__PURE__ */ jsxs(Box, {
31004
32048
  flexDirection: "column",
31005
32049
  flexGrow: 1,
31006
32050
  overflow: "hidden",
31007
- children: [/* @__PURE__ */ jsx(TasksPane, { store }), hasAgentHelper ? /* @__PURE__ */ jsx(Box, {
32051
+ children: [/* @__PURE__ */ jsx(TasksPane, { store }), hasDiagram ? /* @__PURE__ */ jsx(Box, {
32052
+ marginTop: 1,
32053
+ flexGrow: 1,
32054
+ overflow: "hidden",
32055
+ children: /* @__PURE__ */ jsx(DiagramPane, {
32056
+ diagram,
32057
+ width: diagramWidth
32058
+ })
32059
+ }) : hasAgentHelperLines ? /* @__PURE__ */ jsx(Box, {
31008
32060
  marginTop: 1,
31009
32061
  flexGrow: 1,
31010
32062
  overflow: "hidden",
@@ -31452,6 +32504,397 @@ function AgentHelperPane({ lines }) {
31452
32504
  });
31453
32505
  }
31454
32506
 
32507
+ //#endregion
32508
+ //#region src/wizard/ui/screens/GithubSetupScreen.tsx
32509
+ /**
32510
+ * GithubSetupScreen — Post-deploy step asking whether to enable automatic
32511
+ * deploys via the Veloz GitHub App. The runner in `index.ts` drives the
32512
+ * phases; this screen only renders the current state.
32513
+ */
32514
+ function GithubSetupScreen({ store }) {
32515
+ useSyncExternalStore(store.subscribe, store.getSnapshot);
32516
+ const { session } = store;
32517
+ const repo = session.githubRepoOwner && session.githubRepoName ? `${session.githubRepoOwner}/${session.githubRepoName}` : "seu repositório";
32518
+ if (session.githubSetupPhase === "prompt") return /* @__PURE__ */ jsx(PromptView$1, {
32519
+ store,
32520
+ repo
32521
+ });
32522
+ if (session.githubSetupPhase === "connecting") return /* @__PURE__ */ jsxs(ScreenLayout, { children: [/* @__PURE__ */ jsx(Text, {
32523
+ color: BRAND_COLOR,
32524
+ bold: true,
32525
+ children: COPY.githubSetupTitle
32526
+ }), /* @__PURE__ */ jsx(Box, {
32527
+ marginTop: 1,
32528
+ children: /* @__PURE__ */ jsx(Spinner, { label: COPY.githubSetupConnecting })
32529
+ })] });
32530
+ if (session.githubSetupPhase === "installing") return /* @__PURE__ */ jsxs(ScreenLayout, {
32531
+ footer: COPY.githubSetupInstallHint,
32532
+ children: [
32533
+ /* @__PURE__ */ jsx(Text, {
32534
+ color: BRAND_COLOR,
32535
+ bold: true,
32536
+ children: COPY.githubSetupInstallTitle
32537
+ }),
32538
+ /* @__PURE__ */ jsx(Box, {
32539
+ marginTop: 1,
32540
+ children: /* @__PURE__ */ jsx(Spinner, { label: COPY.githubSetupPolling })
32541
+ }),
32542
+ session.githubInstallUrl ? /* @__PURE__ */ jsxs(Box, {
32543
+ marginTop: 1,
32544
+ flexDirection: "column",
32545
+ children: [/* @__PURE__ */ jsx(Text, {
32546
+ dimColor: true,
32547
+ children: COPY.githubSetupInstallFallback
32548
+ }), /* @__PURE__ */ jsx(Text, {
32549
+ bold: true,
32550
+ children: session.githubInstallUrl
32551
+ })]
32552
+ }) : null
32553
+ ]
32554
+ });
32555
+ if (session.githubSetupPhase === "done") return /* @__PURE__ */ jsxs(ScreenLayout, { children: [/* @__PURE__ */ jsxs(Box, {
32556
+ gap: 1,
32557
+ children: [/* @__PURE__ */ jsx(Text, {
32558
+ color: SUCCESS_COLOR,
32559
+ children: Icons.completed
32560
+ }), /* @__PURE__ */ jsx(Text, {
32561
+ color: SUCCESS_COLOR,
32562
+ bold: true,
32563
+ children: COPY.githubSetupDone
32564
+ })]
32565
+ }), /* @__PURE__ */ jsx(Box, {
32566
+ marginTop: 1,
32567
+ children: /* @__PURE__ */ jsx(Text, {
32568
+ dimColor: true,
32569
+ children: COPY.githubSetupDoneHint
32570
+ })
32571
+ })] });
32572
+ if (session.githubSetupPhase === "error") return /* @__PURE__ */ jsxs(ScreenLayout, { children: [
32573
+ /* @__PURE__ */ jsxs(Box, {
32574
+ gap: 1,
32575
+ children: [/* @__PURE__ */ jsx(Text, {
32576
+ color: ERROR_COLOR,
32577
+ children: Icons.error
32578
+ }), /* @__PURE__ */ jsx(Text, {
32579
+ color: ERROR_COLOR,
32580
+ bold: true,
32581
+ children: COPY.githubSetupErrorTitle
32582
+ })]
32583
+ }),
32584
+ session.githubSetupError ? /* @__PURE__ */ jsx(Box, {
32585
+ marginTop: 1,
32586
+ marginLeft: 2,
32587
+ children: /* @__PURE__ */ jsx(Text, {
32588
+ color: ERROR_COLOR,
32589
+ wrap: "wrap",
32590
+ children: session.githubSetupError
32591
+ })
32592
+ }) : null,
32593
+ /* @__PURE__ */ jsx(Box, {
32594
+ marginTop: 1,
32595
+ children: /* @__PURE__ */ jsx(Text, {
32596
+ dimColor: true,
32597
+ children: COPY.githubSetupErrorHint
32598
+ })
32599
+ })
32600
+ ] });
32601
+ return /* @__PURE__ */ jsx(ScreenLayout, { children: /* @__PURE__ */ jsx(Text, {
32602
+ dimColor: true,
32603
+ children: COPY.githubSetupSkipped
32604
+ }) });
32605
+ }
32606
+ function PromptView$1({ store, repo }) {
32607
+ const [selected, setSelected] = React.useState(0);
32608
+ useGatedInput((_input, key) => {
32609
+ if (key.upArrow || key.downArrow) {
32610
+ setSelected((s) => s === 0 ? 1 : 0);
32611
+ return;
32612
+ }
32613
+ if (key.return) store.setGithubSetupPhase(selected === 0 ? "connecting" : "skipped");
32614
+ });
32615
+ const options = [COPY.githubSetupYes, COPY.githubSetupNo];
32616
+ return /* @__PURE__ */ jsxs(ScreenLayout, {
32617
+ footer: "↑↓ escolher · Enter confirmar",
32618
+ children: [
32619
+ /* @__PURE__ */ jsx(Text, {
32620
+ color: BRAND_COLOR,
32621
+ bold: true,
32622
+ children: COPY.githubSetupTitle
32623
+ }),
32624
+ /* @__PURE__ */ jsx(Box, {
32625
+ marginTop: 1,
32626
+ children: /* @__PURE__ */ jsx(Text, {
32627
+ wrap: "wrap",
32628
+ children: COPY.githubSetupQuestion(repo)
32629
+ })
32630
+ }),
32631
+ /* @__PURE__ */ jsx(Box, {
32632
+ marginTop: 1,
32633
+ children: /* @__PURE__ */ jsx(Text, {
32634
+ color: DIM_COLOR,
32635
+ wrap: "wrap",
32636
+ children: COPY.githubSetupHint
32637
+ })
32638
+ }),
32639
+ /* @__PURE__ */ jsx(Box, {
32640
+ marginTop: 1,
32641
+ flexDirection: "column",
32642
+ children: options.map((opt, i) => /* @__PURE__ */ jsxs(Box, {
32643
+ gap: 1,
32644
+ marginLeft: 2,
32645
+ children: [/* @__PURE__ */ jsx(Text, {
32646
+ color: i === selected ? BRAND_COLOR : DIM_COLOR,
32647
+ children: i === selected ? Icons.triangleRight : " "
32648
+ }), /* @__PURE__ */ jsx(Text, {
32649
+ color: i === selected ? BRAND_COLOR : void 0,
32650
+ bold: i === selected,
32651
+ children: opt
32652
+ })]
32653
+ }, i))
32654
+ })
32655
+ ]
32656
+ });
32657
+ }
32658
+
32659
+ //#endregion
32660
+ //#region src/wizard/ui/screens/AiSetupScreen.tsx
32661
+ /**
32662
+ * AiSetupScreen — Post-deploy step that offers to install the Veloz MCP
32663
+ * server and the agent skills. The runner in `index.ts` detects what's
32664
+ * missing, drives the phases, and spawns the installation commands; this
32665
+ * screen renders the current state.
32666
+ */
32667
+ function AiSetupScreen({ store }) {
32668
+ useSyncExternalStore(store.subscribe, store.getSnapshot);
32669
+ const { session } = store;
32670
+ if (session.aiSetupPhase === "prompt") return /* @__PURE__ */ jsx(PromptView, { store });
32671
+ if (session.aiSetupPhase === "installing") return /* @__PURE__ */ jsxs(ScreenLayout, { children: [/* @__PURE__ */ jsx(Text, {
32672
+ color: BRAND_COLOR,
32673
+ bold: true,
32674
+ children: COPY.aiSetupInstallingTitle
32675
+ }), /* @__PURE__ */ jsxs(Box, {
32676
+ marginTop: 1,
32677
+ flexDirection: "column",
32678
+ children: [session.aiSetupNeedsMcp ? /* @__PURE__ */ jsx(ToolRow, {
32679
+ status: session.aiSetupMcpStatus,
32680
+ installingLabel: COPY.aiSetupMcpInstalling,
32681
+ doneLabel: COPY.aiSetupMcpDone,
32682
+ errorLabel: COPY.aiSetupMcpError,
32683
+ pendingLabel: COPY.aiSetupMcpLabel
32684
+ }) : null, session.aiSetupNeedsSkills ? /* @__PURE__ */ jsx(ToolRow, {
32685
+ status: session.aiSetupSkillsStatus,
32686
+ installingLabel: COPY.aiSetupSkillsInstalling,
32687
+ doneLabel: COPY.aiSetupSkillsDone,
32688
+ errorLabel: COPY.aiSetupSkillsError,
32689
+ pendingLabel: COPY.aiSetupSkillsLabel
32690
+ }) : null]
32691
+ })] });
32692
+ if (session.aiSetupPhase === "done") return /* @__PURE__ */ jsxs(ScreenLayout, { children: [
32693
+ /* @__PURE__ */ jsxs(Box, {
32694
+ gap: 1,
32695
+ children: [/* @__PURE__ */ jsx(Text, {
32696
+ color: SUCCESS_COLOR,
32697
+ children: Icons.completed
32698
+ }), /* @__PURE__ */ jsx(Text, {
32699
+ color: SUCCESS_COLOR,
32700
+ bold: true,
32701
+ children: COPY.aiSetupDoneTitle
32702
+ })]
32703
+ }),
32704
+ /* @__PURE__ */ jsxs(Box, {
32705
+ marginTop: 1,
32706
+ flexDirection: "column",
32707
+ children: [session.aiSetupNeedsMcp ? /* @__PURE__ */ jsx(ToolRow, {
32708
+ status: session.aiSetupMcpStatus,
32709
+ installingLabel: COPY.aiSetupMcpInstalling,
32710
+ doneLabel: COPY.aiSetupMcpDone,
32711
+ errorLabel: COPY.aiSetupMcpError,
32712
+ pendingLabel: COPY.aiSetupMcpLabel
32713
+ }) : null, session.aiSetupNeedsSkills ? /* @__PURE__ */ jsx(ToolRow, {
32714
+ status: session.aiSetupSkillsStatus,
32715
+ installingLabel: COPY.aiSetupSkillsInstalling,
32716
+ doneLabel: COPY.aiSetupSkillsDone,
32717
+ errorLabel: COPY.aiSetupSkillsError,
32718
+ pendingLabel: COPY.aiSetupSkillsLabel
32719
+ }) : null]
32720
+ }),
32721
+ /* @__PURE__ */ jsx(Box, {
32722
+ marginTop: 1,
32723
+ children: /* @__PURE__ */ jsx(Text, {
32724
+ dimColor: true,
32725
+ children: COPY.aiSetupDoneHint
32726
+ })
32727
+ })
32728
+ ] });
32729
+ if (session.aiSetupPhase === "error") return /* @__PURE__ */ jsxs(ScreenLayout, { children: [
32730
+ /* @__PURE__ */ jsxs(Box, {
32731
+ gap: 1,
32732
+ children: [/* @__PURE__ */ jsx(Text, {
32733
+ color: ERROR_COLOR,
32734
+ children: Icons.error
32735
+ }), /* @__PURE__ */ jsx(Text, {
32736
+ color: ERROR_COLOR,
32737
+ bold: true,
32738
+ children: COPY.aiSetupErrorTitle
32739
+ })]
32740
+ }),
32741
+ session.aiSetupError ? /* @__PURE__ */ jsx(Box, {
32742
+ marginTop: 1,
32743
+ marginLeft: 2,
32744
+ children: /* @__PURE__ */ jsx(Text, {
32745
+ color: ERROR_COLOR,
32746
+ wrap: "wrap",
32747
+ children: session.aiSetupError
32748
+ })
32749
+ }) : null,
32750
+ /* @__PURE__ */ jsx(Box, {
32751
+ marginTop: 1,
32752
+ children: /* @__PURE__ */ jsx(Text, {
32753
+ dimColor: true,
32754
+ children: COPY.aiSetupErrorHint
32755
+ })
32756
+ })
32757
+ ] });
32758
+ return /* @__PURE__ */ jsx(ScreenLayout, { children: null });
32759
+ }
32760
+ function ToolRow({ status, installingLabel, doneLabel, errorLabel, pendingLabel }) {
32761
+ if (status === "installing") return /* @__PURE__ */ jsx(Box, {
32762
+ marginLeft: 2,
32763
+ children: /* @__PURE__ */ jsx(Spinner, { label: installingLabel })
32764
+ });
32765
+ if (status === "done") return /* @__PURE__ */ jsxs(Box, {
32766
+ gap: 1,
32767
+ marginLeft: 2,
32768
+ children: [/* @__PURE__ */ jsx(Text, {
32769
+ color: SUCCESS_COLOR,
32770
+ children: Icons.completed
32771
+ }), /* @__PURE__ */ jsx(Text, { children: doneLabel })]
32772
+ });
32773
+ if (status === "error") return /* @__PURE__ */ jsxs(Box, {
32774
+ gap: 1,
32775
+ marginLeft: 2,
32776
+ children: [/* @__PURE__ */ jsx(Text, {
32777
+ color: ERROR_COLOR,
32778
+ children: Icons.error
32779
+ }), /* @__PURE__ */ jsx(Text, {
32780
+ color: ERROR_COLOR,
32781
+ children: errorLabel
32782
+ })]
32783
+ });
32784
+ if (status === "skipped") return /* @__PURE__ */ jsxs(Box, {
32785
+ gap: 1,
32786
+ marginLeft: 2,
32787
+ children: [/* @__PURE__ */ jsx(Text, {
32788
+ color: DIM_COLOR,
32789
+ children: "-"
32790
+ }), /* @__PURE__ */ jsxs(Text, {
32791
+ dimColor: true,
32792
+ children: [pendingLabel, " (já instalado)"]
32793
+ })]
32794
+ });
32795
+ return /* @__PURE__ */ jsxs(Box, {
32796
+ gap: 1,
32797
+ marginLeft: 2,
32798
+ children: [/* @__PURE__ */ jsx(Text, {
32799
+ color: DIM_COLOR,
32800
+ children: Icons.pending
32801
+ }), /* @__PURE__ */ jsx(Text, {
32802
+ dimColor: true,
32803
+ children: pendingLabel
32804
+ })]
32805
+ });
32806
+ }
32807
+ function PromptView({ store }) {
32808
+ const [selected, setSelected] = React.useState(0);
32809
+ const { session } = store;
32810
+ useGatedInput((_input, key) => {
32811
+ if (key.upArrow || key.downArrow) {
32812
+ setSelected((s) => s === 0 ? 1 : 0);
32813
+ return;
32814
+ }
32815
+ if (key.return) store.setAiSetupPhase(selected === 0 ? "installing" : "skipped");
32816
+ });
32817
+ const options = [COPY.aiSetupYes, COPY.aiSetupNo];
32818
+ return /* @__PURE__ */ jsxs(ScreenLayout, {
32819
+ footer: "↑↓ escolher · Enter confirmar",
32820
+ children: [
32821
+ /* @__PURE__ */ jsx(Text, {
32822
+ color: BRAND_COLOR,
32823
+ bold: true,
32824
+ children: COPY.aiSetupTitle
32825
+ }),
32826
+ /* @__PURE__ */ jsx(Box, {
32827
+ marginTop: 1,
32828
+ children: /* @__PURE__ */ jsx(Text, {
32829
+ wrap: "wrap",
32830
+ children: COPY.aiSetupQuestion
32831
+ })
32832
+ }),
32833
+ /* @__PURE__ */ jsxs(Box, {
32834
+ marginTop: 1,
32835
+ flexDirection: "column",
32836
+ marginLeft: 2,
32837
+ children: [session.aiSetupNeedsMcp ? /* @__PURE__ */ jsxs(Box, {
32838
+ flexDirection: "column",
32839
+ marginBottom: 1,
32840
+ children: [/* @__PURE__ */ jsxs(Box, {
32841
+ gap: 1,
32842
+ children: [/* @__PURE__ */ jsx(Text, {
32843
+ color: BRAND_COLOR,
32844
+ children: Icons.bullet
32845
+ }), /* @__PURE__ */ jsx(Text, {
32846
+ bold: true,
32847
+ children: COPY.aiSetupMcpLabel
32848
+ })]
32849
+ }), /* @__PURE__ */ jsx(Box, {
32850
+ marginLeft: 2,
32851
+ children: /* @__PURE__ */ jsx(Text, {
32852
+ color: DIM_COLOR,
32853
+ wrap: "wrap",
32854
+ children: COPY.aiSetupMcpDescription
32855
+ })
32856
+ })]
32857
+ }) : null, session.aiSetupNeedsSkills ? /* @__PURE__ */ jsxs(Box, {
32858
+ flexDirection: "column",
32859
+ children: [/* @__PURE__ */ jsxs(Box, {
32860
+ gap: 1,
32861
+ children: [/* @__PURE__ */ jsx(Text, {
32862
+ color: BRAND_COLOR,
32863
+ children: Icons.bullet
32864
+ }), /* @__PURE__ */ jsx(Text, {
32865
+ bold: true,
32866
+ children: COPY.aiSetupSkillsLabel
32867
+ })]
32868
+ }), /* @__PURE__ */ jsx(Box, {
32869
+ marginLeft: 2,
32870
+ children: /* @__PURE__ */ jsx(Text, {
32871
+ color: DIM_COLOR,
32872
+ wrap: "wrap",
32873
+ children: COPY.aiSetupSkillsDescription
32874
+ })
32875
+ })]
32876
+ }) : null]
32877
+ }),
32878
+ /* @__PURE__ */ jsx(Box, {
32879
+ marginTop: 1,
32880
+ flexDirection: "column",
32881
+ children: options.map((opt, i) => /* @__PURE__ */ jsxs(Box, {
32882
+ gap: 1,
32883
+ marginLeft: 2,
32884
+ children: [/* @__PURE__ */ jsx(Text, {
32885
+ color: i === selected ? BRAND_COLOR : DIM_COLOR,
32886
+ children: i === selected ? Icons.triangleRight : " "
32887
+ }), /* @__PURE__ */ jsx(Text, {
32888
+ color: i === selected ? BRAND_COLOR : void 0,
32889
+ bold: i === selected,
32890
+ children: opt
32891
+ })]
32892
+ }, i))
32893
+ })
32894
+ ]
32895
+ });
32896
+ }
32897
+
31455
32898
  //#endregion
31456
32899
  //#region src/wizard/ui/primitives/MarkdownText.tsx
31457
32900
  /** Parse inline markdown (**bold** and `code`) into Ink <Text> elements. */
@@ -31517,203 +32960,327 @@ function MarkdownText({ children }) {
31517
32960
  //#endregion
31518
32961
  //#region src/wizard/ui/screens/OutroScreen.tsx
31519
32962
  /**
31520
- * OutroScreen — Final screen after agent completes or fails.
31521
- * Fullscreen with logo. Shows domain, agent summary, diagram on success.
31522
- * Shows error details + debugging context on failure.
32963
+ * OutroScreen — Final screen after the agent completes or fails.
32964
+ *
32965
+ * Scrollable content (↑/↓) with an action bar at the bottom:
32966
+ * [ Abrir projeto ] [ Abrir dashboard ] [ Fechar ]
32967
+ * Left/right to move between actions, Enter to confirm.
31523
32968
  */
32969
+ const DASHBOARD_BASE = process.env.VELOZ_WEB_URL || "https://app.onveloz.com";
31524
32970
  function OutroScreen({ store }) {
31525
32971
  useSyncExternalStore(store.subscribe, store.getSnapshot);
31526
32972
  const { session } = store;
31527
32973
  const isSuccess$4 = !session.agentError;
31528
- const [columns] = useStdoutDimensions();
32974
+ const [columns, rows] = useStdoutDimensions();
31529
32975
  const sepWidth = Math.min(columns - 8, 80);
31530
32976
  const aiSetup = useMemo(() => checkAiSetup(), []);
32977
+ const projectConfig = useMemo(() => loadConfig$1(), []);
32978
+ const dashboardUrl = useMemo(() => {
32979
+ const projectId = projectConfig?.project?.id;
32980
+ return projectId ? `${DASHBOARD_BASE}/projetos/${projectId}` : DASHBOARD_BASE;
32981
+ }, [projectConfig]);
32982
+ const actions = useMemo(() => {
32983
+ if (!isSuccess$4) return [{
32984
+ id: "close",
32985
+ label: "Fechar"
32986
+ }];
32987
+ const list = [];
32988
+ if (session.deployUrl) list.push({
32989
+ id: "open-project",
32990
+ label: "Abrir projeto"
32991
+ });
32992
+ list.push({
32993
+ id: "open-dashboard",
32994
+ label: "Abrir dashboard"
32995
+ });
32996
+ list.push({
32997
+ id: "close",
32998
+ label: "Fechar"
32999
+ });
33000
+ return list;
33001
+ }, [isSuccess$4, session.deployUrl]);
33002
+ const [actionIdx, setActionIdx] = useState(0);
33003
+ const [scrollOffset, setScrollOffset] = useState(0);
33004
+ const performExit = React.useCallback(async (action) => {
33005
+ try {
33006
+ if (action.id === "open-project" && session.deployUrl) await openBrowser(session.deployUrl);
33007
+ else if (action.id === "open-dashboard") await openBrowser(dashboardUrl);
33008
+ } catch {}
33009
+ store.dismissOutro();
33010
+ process.exit(isSuccess$4 ? 0 : 1);
33011
+ }, [
33012
+ dashboardUrl,
33013
+ isSuccess$4,
33014
+ session.deployUrl,
33015
+ store
33016
+ ]);
31531
33017
  useGatedInput((_input, key) => {
31532
- if (key.return || key.escape) {
33018
+ if (key.upArrow) {
33019
+ setScrollOffset((s) => Math.max(0, s - 1));
33020
+ return;
33021
+ }
33022
+ if (key.downArrow) {
33023
+ setScrollOffset((s) => s + 1);
33024
+ return;
33025
+ }
33026
+ if (key.leftArrow) {
33027
+ setActionIdx((i) => i > 0 ? i - 1 : actions.length - 1);
33028
+ return;
33029
+ }
33030
+ if (key.rightArrow) {
33031
+ setActionIdx((i) => i < actions.length - 1 ? i + 1 : 0);
33032
+ return;
33033
+ }
33034
+ if (key.return) {
33035
+ const action = actions[actionIdx] ?? actions[0];
33036
+ if (action) performExit(action);
33037
+ return;
33038
+ }
33039
+ if (key.escape) {
31533
33040
  store.dismissOutro();
31534
33041
  process.exit(isSuccess$4 ? 0 : 1);
31535
33042
  }
31536
33043
  });
31537
- if (isSuccess$4) return /* @__PURE__ */ jsxs(ScreenLayout, {
31538
- footer: "Pressione Enter para sair",
31539
- children: [
31540
- /* @__PURE__ */ jsxs(Box, {
31541
- gap: 1,
31542
- children: [/* @__PURE__ */ jsx(Text, {
31543
- color: SUCCESS_COLOR,
31544
- children: Icons.squareFilled
31545
- }), /* @__PURE__ */ jsx(Text, {
31546
- color: SUCCESS_COLOR,
31547
- bold: true,
31548
- children: COPY.outroSuccess
31549
- })]
31550
- }),
31551
- session.deployUrl ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Separator, { width: sepWidth }), /* @__PURE__ */ jsxs(Box, {
31552
- marginTop: 1,
31553
- flexDirection: "column",
31554
- children: [/* @__PURE__ */ jsx(Text, {
31555
- dimColor: true,
31556
- children: COPY.outroUrl
31557
- }), /* @__PURE__ */ jsxs(Text, {
31558
- color: BRAND_COLOR,
31559
- bold: true,
31560
- children: [" ", session.deployUrl]
31561
- })]
31562
- })] }) : null,
31563
- session.agentSummary ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Separator, { width: sepWidth }), /* @__PURE__ */ jsxs(Box, {
31564
- marginTop: 1,
31565
- flexDirection: "column",
31566
- children: [/* @__PURE__ */ jsx(Text, {
31567
- bold: true,
31568
- color: ACCENT_COLOR,
31569
- children: "Resumo do deploy:"
31570
- }), /* @__PURE__ */ jsx(Box, {
31571
- marginLeft: 2,
31572
- children: /* @__PURE__ */ jsx(MarkdownText, { children: session.agentSummary })
31573
- })]
31574
- })] }) : null,
31575
- session.agentHelperLines.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Separator, { width: sepWidth }), /* @__PURE__ */ jsx(Box, {
31576
- marginTop: 1,
33044
+ const body = isSuccess$4 ? /* @__PURE__ */ jsx(SuccessBody, {
33045
+ store,
33046
+ sepWidth,
33047
+ aiSetup
33048
+ }) : /* @__PURE__ */ jsx(ErrorBody, {
33049
+ store,
33050
+ sepWidth
33051
+ });
33052
+ return /* @__PURE__ */ jsxs(ScreenLayout, {
33053
+ footer: "↑↓ rolar · ←→ escolher ação · Enter confirmar · Esc fechar",
33054
+ children: [/* @__PURE__ */ jsx(Box, {
33055
+ flexDirection: "column",
33056
+ flexGrow: 1,
33057
+ flexShrink: 1,
33058
+ overflow: "hidden",
33059
+ height: Math.max(6, rows - 14),
33060
+ children: /* @__PURE__ */ jsx(Box, {
31577
33061
  flexDirection: "column",
31578
- children: session.agentHelperLines.map((line, i) => {
31579
- if (i === 0 && line.trim()) return /* @__PURE__ */ jsx(Text, {
31580
- bold: true,
31581
- color: ACCENT_COLOR,
31582
- wrap: "truncate",
31583
- children: line
31584
- }, i);
31585
- return /* @__PURE__ */ jsx(Text, {
31586
- color: DIM_COLOR,
31587
- wrap: "truncate",
31588
- children: line
31589
- }, i);
33062
+ marginTop: -scrollOffset,
33063
+ children: body
33064
+ })
33065
+ }), /* @__PURE__ */ jsx(ActionBar, {
33066
+ actions,
33067
+ selected: actionIdx
33068
+ })]
33069
+ });
33070
+ }
33071
+ function ActionBar({ actions, selected }) {
33072
+ return /* @__PURE__ */ jsx(Box, {
33073
+ marginTop: 1,
33074
+ gap: 2,
33075
+ children: actions.map((action, i) => {
33076
+ const active$1 = i === selected;
33077
+ return /* @__PURE__ */ jsx(Box, {
33078
+ borderStyle: "single",
33079
+ borderColor: active$1 ? BRAND_COLOR : DIM_COLOR,
33080
+ paddingX: 1,
33081
+ children: /* @__PURE__ */ jsx(Text, {
33082
+ color: active$1 ? BRAND_COLOR : void 0,
33083
+ bold: active$1,
33084
+ children: action.label
31590
33085
  })
31591
- })] }) : null,
31592
- /* @__PURE__ */ jsx(Separator, { width: sepWidth }),
31593
- /* @__PURE__ */ jsxs(Box, {
31594
- marginTop: 1,
31595
- flexDirection: "column",
31596
- children: [/* @__PURE__ */ jsx(Text, {
33086
+ }, action.id);
33087
+ })
33088
+ });
33089
+ }
33090
+ function SuccessBody({ store, sepWidth, aiSetup }) {
33091
+ const { session } = store;
33092
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
33093
+ /* @__PURE__ */ jsxs(Box, {
33094
+ gap: 1,
33095
+ children: [/* @__PURE__ */ jsx(Text, {
33096
+ color: SUCCESS_COLOR,
33097
+ children: Icons.squareFilled
33098
+ }), /* @__PURE__ */ jsx(Text, {
33099
+ color: SUCCESS_COLOR,
33100
+ bold: true,
33101
+ children: COPY.outroSuccess
33102
+ })]
33103
+ }),
33104
+ session.deployUrl ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Separator, { width: sepWidth }), /* @__PURE__ */ jsxs(Box, {
33105
+ marginTop: 1,
33106
+ flexDirection: "column",
33107
+ children: [/* @__PURE__ */ jsx(Text, {
33108
+ dimColor: true,
33109
+ children: COPY.outroUrl
33110
+ }), /* @__PURE__ */ jsxs(Text, {
33111
+ color: BRAND_COLOR,
33112
+ bold: true,
33113
+ children: [" ", session.deployUrl]
33114
+ })]
33115
+ })] }) : null,
33116
+ session.githubSetupPhase === "done" ? /* @__PURE__ */ jsxs(Box, {
33117
+ marginTop: 1,
33118
+ gap: 1,
33119
+ children: [/* @__PURE__ */ jsx(Text, {
33120
+ color: SUCCESS_COLOR,
33121
+ children: Icons.completed
33122
+ }), /* @__PURE__ */ jsx(Text, { children: "Deploy automático ativado via GitHub." })]
33123
+ }) : null,
33124
+ session.agentSummary ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Separator, { width: sepWidth }), /* @__PURE__ */ jsxs(Box, {
33125
+ marginTop: 1,
33126
+ flexDirection: "column",
33127
+ children: [/* @__PURE__ */ jsx(Text, {
33128
+ bold: true,
33129
+ color: ACCENT_COLOR,
33130
+ children: "Resumo do deploy:"
33131
+ }), /* @__PURE__ */ jsx(Box, {
33132
+ marginLeft: 2,
33133
+ children: /* @__PURE__ */ jsx(MarkdownText, { children: session.agentSummary })
33134
+ })]
33135
+ })] }) : null,
33136
+ session.agentHelperDiagram ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Separator, { width: sepWidth }), /* @__PURE__ */ jsx(Box, {
33137
+ marginTop: 1,
33138
+ flexDirection: "column",
33139
+ children: /* @__PURE__ */ jsx(DiagramPane, {
33140
+ diagram: session.agentHelperDiagram,
33141
+ width: sepWidth
33142
+ })
33143
+ })] }) : session.agentHelperLines.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Separator, { width: sepWidth }), /* @__PURE__ */ jsx(Box, {
33144
+ marginTop: 1,
33145
+ flexDirection: "column",
33146
+ children: session.agentHelperLines.map((line, i) => {
33147
+ if (i === 0 && line.trim()) return /* @__PURE__ */ jsx(Text, {
31597
33148
  bold: true,
31598
- children: COPY.outroNextSteps
31599
- }), COPY.outroNextStepsList.map((step, i) => /* @__PURE__ */ jsxs(Text, {
33149
+ color: ACCENT_COLOR,
33150
+ wrap: "truncate",
33151
+ children: line
33152
+ }, i);
33153
+ return /* @__PURE__ */ jsx(Text, {
31600
33154
  color: DIM_COLOR,
31601
- children: [
31602
- " ",
31603
- i + 1,
31604
- ". ",
31605
- step
31606
- ]
31607
- }, i))]
31608
- }),
31609
- !aiSetup.mcpInstalled || !aiSetup.skillsInstalled ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Separator, { width: sepWidth }), /* @__PURE__ */ jsxs(Box, {
31610
- marginTop: 1,
31611
- flexDirection: "column",
33155
+ wrap: "truncate",
33156
+ children: line
33157
+ }, i);
33158
+ })
33159
+ })] }) : null,
33160
+ /* @__PURE__ */ jsx(Separator, { width: sepWidth }),
33161
+ /* @__PURE__ */ jsxs(Box, {
33162
+ marginTop: 1,
33163
+ flexDirection: "column",
33164
+ children: [/* @__PURE__ */ jsx(Text, {
33165
+ bold: true,
33166
+ children: COPY.outroNextSteps
33167
+ }), COPY.outroNextStepsList.map((step, i) => /* @__PURE__ */ jsxs(Text, {
33168
+ color: DIM_COLOR,
31612
33169
  children: [
31613
- /* @__PURE__ */ jsx(Text, {
31614
- bold: true,
31615
- color: ACCENT_COLOR,
31616
- children: "Integração com IA:"
31617
- }),
31618
- !aiSetup.mcpInstalled ? /* @__PURE__ */ jsxs(Text, {
31619
- color: DIM_COLOR,
31620
- children: [
31621
- " ",
31622
- Icons.bullet,
31623
- " ",
31624
- COPY.outroAiSetupMcp
31625
- ]
31626
- }) : null,
31627
- !aiSetup.skillsInstalled ? /* @__PURE__ */ jsxs(Text, {
31628
- color: DIM_COLOR,
31629
- children: [
31630
- " ",
31631
- Icons.bullet,
31632
- " ",
31633
- COPY.outroAiSetupSkills
31634
- ]
31635
- }) : null
33170
+ " ",
33171
+ i + 1,
33172
+ ". ",
33173
+ step
31636
33174
  ]
31637
- })] }) : null
31638
- ]
31639
- });
31640
- const recentOutput = session.agentOutputLines.slice(-8);
31641
- return /* @__PURE__ */ jsxs(ScreenLayout, {
31642
- footer: "Pressione Enter para sair",
31643
- children: [
31644
- /* @__PURE__ */ jsxs(Box, {
31645
- gap: 1,
31646
- children: [/* @__PURE__ */ jsx(Text, {
31647
- color: ERROR_COLOR,
31648
- children: Icons.error
31649
- }), /* @__PURE__ */ jsx(Text, {
31650
- color: ERROR_COLOR,
31651
- bold: true,
31652
- children: COPY.outroError
31653
- })]
31654
- }),
31655
- session.agentError ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Separator, { width: sepWidth }), /* @__PURE__ */ jsxs(Box, {
31656
- marginTop: 1,
31657
- flexDirection: "column",
31658
- children: [/* @__PURE__ */ jsx(Text, {
31659
- bold: true,
31660
- color: ERROR_COLOR,
31661
- children: "Motivo:"
31662
- }), /* @__PURE__ */ jsx(Box, {
31663
- marginLeft: 2,
31664
- flexDirection: "column",
31665
- children: /* @__PURE__ */ jsx(Text, {
31666
- color: ERROR_COLOR,
31667
- wrap: "wrap",
31668
- children: session.agentError
31669
- })
31670
- })]
31671
- })] }) : null,
31672
- recentOutput.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Separator, { width: sepWidth }), /* @__PURE__ */ jsxs(Box, {
31673
- marginTop: 1,
31674
- flexDirection: "column",
31675
- children: [/* @__PURE__ */ jsx(Text, {
33175
+ }, i))]
33176
+ }),
33177
+ !aiSetup.mcpInstalled || !aiSetup.skillsInstalled ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Separator, { width: sepWidth }), /* @__PURE__ */ jsxs(Box, {
33178
+ marginTop: 1,
33179
+ flexDirection: "column",
33180
+ children: [
33181
+ /* @__PURE__ */ jsx(Text, {
31676
33182
  bold: true,
31677
33183
  color: ACCENT_COLOR,
31678
- children: "Últimas ações do agente:"
31679
- }), recentOutput.map((line, i) => /* @__PURE__ */ jsxs(Text, {
33184
+ children: "Integração com IA:"
33185
+ }),
33186
+ !aiSetup.mcpInstalled ? /* @__PURE__ */ jsxs(Text, {
31680
33187
  color: DIM_COLOR,
31681
- wrap: "wrap",
31682
- children: [" ", line]
31683
- }, i))]
31684
- })] }) : null,
31685
- session.retryCount > 0 ? /* @__PURE__ */ jsx(Box, {
31686
- marginTop: 1,
31687
- children: /* @__PURE__ */ jsxs(Text, {
31688
- dimColor: true,
31689
33188
  children: [
31690
- "O agente tentou ",
31691
- session.retryCount + 1,
31692
- " vez",
31693
- session.retryCount > 0 ? "es" : "",
31694
- " antes de falhar."
33189
+ " ",
33190
+ Icons.bullet,
33191
+ " ",
33192
+ COPY.outroAiSetupMcp
31695
33193
  ]
31696
- })
31697
- }) : null,
31698
- /* @__PURE__ */ jsx(Separator, { width: sepWidth }),
31699
- /* @__PURE__ */ jsxs(Box, {
31700
- marginTop: 1,
31701
- flexDirection: "column",
31702
- children: [/* @__PURE__ */ jsx(Text, {
31703
- bold: true,
31704
- children: "Próximos passos:"
31705
- }), COPY.outroErrorHints.map((hint, i) => /* @__PURE__ */ jsxs(Text, {
33194
+ }) : null,
33195
+ !aiSetup.skillsInstalled ? /* @__PURE__ */ jsxs(Text, {
31706
33196
  color: DIM_COLOR,
31707
33197
  children: [
31708
33198
  " ",
31709
33199
  Icons.bullet,
31710
33200
  " ",
31711
- hint
33201
+ COPY.outroAiSetupSkills
31712
33202
  ]
31713
- }, i))]
33203
+ }) : null
33204
+ ]
33205
+ })] }) : null
33206
+ ] });
33207
+ }
33208
+ function ErrorBody({ store, sepWidth }) {
33209
+ const { session } = store;
33210
+ const recentOutput = session.agentOutputLines.slice(-8);
33211
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
33212
+ /* @__PURE__ */ jsxs(Box, {
33213
+ gap: 1,
33214
+ children: [/* @__PURE__ */ jsx(Text, {
33215
+ color: ERROR_COLOR,
33216
+ children: Icons.error
33217
+ }), /* @__PURE__ */ jsx(Text, {
33218
+ color: ERROR_COLOR,
33219
+ bold: true,
33220
+ children: COPY.outroError
33221
+ })]
33222
+ }),
33223
+ session.agentError ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Separator, { width: sepWidth }), /* @__PURE__ */ jsxs(Box, {
33224
+ marginTop: 1,
33225
+ flexDirection: "column",
33226
+ children: [/* @__PURE__ */ jsx(Text, {
33227
+ bold: true,
33228
+ color: ERROR_COLOR,
33229
+ children: "Motivo:"
33230
+ }), /* @__PURE__ */ jsx(Box, {
33231
+ marginLeft: 2,
33232
+ flexDirection: "column",
33233
+ children: /* @__PURE__ */ jsx(Text, {
33234
+ color: ERROR_COLOR,
33235
+ wrap: "wrap",
33236
+ children: session.agentError
33237
+ })
33238
+ })]
33239
+ })] }) : null,
33240
+ recentOutput.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Separator, { width: sepWidth }), /* @__PURE__ */ jsxs(Box, {
33241
+ marginTop: 1,
33242
+ flexDirection: "column",
33243
+ children: [/* @__PURE__ */ jsx(Text, {
33244
+ bold: true,
33245
+ color: ACCENT_COLOR,
33246
+ children: "Últimas ações do agente:"
33247
+ }), recentOutput.map((line, i) => /* @__PURE__ */ jsxs(Text, {
33248
+ color: DIM_COLOR,
33249
+ wrap: "wrap",
33250
+ children: [" ", line]
33251
+ }, i))]
33252
+ })] }) : null,
33253
+ session.retryCount > 0 ? /* @__PURE__ */ jsx(Box, {
33254
+ marginTop: 1,
33255
+ children: /* @__PURE__ */ jsxs(Text, {
33256
+ dimColor: true,
33257
+ children: [
33258
+ "O agente tentou ",
33259
+ session.retryCount + 1,
33260
+ " vez",
33261
+ session.retryCount > 0 ? "es" : "",
33262
+ " antes de falhar."
33263
+ ]
31714
33264
  })
31715
- ]
31716
- });
33265
+ }) : null,
33266
+ /* @__PURE__ */ jsx(Separator, { width: sepWidth }),
33267
+ /* @__PURE__ */ jsxs(Box, {
33268
+ marginTop: 1,
33269
+ flexDirection: "column",
33270
+ children: [/* @__PURE__ */ jsx(Text, {
33271
+ bold: true,
33272
+ children: "Próximos passos:"
33273
+ }), COPY.outroErrorHints.map((hint, i) => /* @__PURE__ */ jsxs(Text, {
33274
+ color: DIM_COLOR,
33275
+ children: [
33276
+ " ",
33277
+ Icons.bullet,
33278
+ " ",
33279
+ hint
33280
+ ]
33281
+ }, i))]
33282
+ })
33283
+ ] });
31717
33284
  }
31718
33285
  function Separator({ width }) {
31719
33286
  return /* @__PURE__ */ jsx(Box, {
@@ -31760,25 +33327,58 @@ function ErrorOverlay({ message, onDismiss }) {
31760
33327
 
31761
33328
  //#endregion
31762
33329
  //#region src/wizard/ui/overlays/ConfirmExitOverlay.tsx
33330
+ /**
33331
+ * ConfirmExitOverlay — Alert-style confirmation for Ctrl+C.
33332
+ *
33333
+ * Rendered in the absolute-positioned overlay slot in App.tsx, just like
33334
+ * PromptOverlay: pulsing border, own `useInput` handler, screen remains
33335
+ * visible behind it while `InputFocusProvider` gates the base layer.
33336
+ */
33337
+ function useOverlayWidth$1() {
33338
+ const [columns] = useStdoutDimensions();
33339
+ return Math.max(30, Math.min(60, columns - 4));
33340
+ }
31763
33341
  function ConfirmExitOverlay({ onConfirm, onCancel }) {
31764
33342
  const [selected, setSelected] = useState(1);
31765
- useGatedInput((_input, key) => {
31766
- if (key.upArrow || key.downArrow) setSelected((s) => s === 0 ? 1 : 0);
31767
- if (key.return) if (selected === 0) onConfirm();
31768
- else onCancel();
33343
+ const [pulse, setPulse] = useState(true);
33344
+ const width = useOverlayWidth$1();
33345
+ useEffect(() => {
33346
+ const timer = setInterval(() => setPulse((p) => !p), 800);
33347
+ return () => clearInterval(timer);
33348
+ }, []);
33349
+ useInput((_input, key) => {
33350
+ if (key.upArrow || key.downArrow) {
33351
+ setSelected((s) => s === 0 ? 1 : 0);
33352
+ return;
33353
+ }
33354
+ if (key.return) {
33355
+ if (selected === 0) onConfirm();
33356
+ else onCancel();
33357
+ return;
33358
+ }
31769
33359
  if (key.escape) onCancel();
31770
33360
  });
33361
+ const borderColor = pulse ? ERROR_COLOR : BRAND_COLOR;
31771
33362
  const options = [COPY.confirmExitYes, COPY.confirmExitNo];
31772
33363
  return /* @__PURE__ */ jsxs(Box, {
31773
33364
  flexDirection: "column",
31774
- borderStyle: "single",
31775
- borderColor: ERROR_COLOR,
33365
+ borderStyle: "bold",
33366
+ borderColor,
33367
+ backgroundColor: "black",
31776
33368
  paddingX: 2,
31777
33369
  paddingY: 1,
33370
+ width,
31778
33371
  children: [
31779
- /* @__PURE__ */ jsx(Text, {
31780
- bold: true,
31781
- children: COPY.confirmExitTitle
33372
+ /* @__PURE__ */ jsxs(Box, {
33373
+ gap: 1,
33374
+ children: [/* @__PURE__ */ jsx(Text, {
33375
+ color: ERROR_COLOR,
33376
+ bold: true,
33377
+ children: Icons.diamond
33378
+ }), /* @__PURE__ */ jsx(Text, {
33379
+ bold: true,
33380
+ children: COPY.confirmExitTitle
33381
+ })]
31782
33382
  }),
31783
33383
  /* @__PURE__ */ jsx(Box, {
31784
33384
  marginTop: 1,
@@ -31791,12 +33391,19 @@ function ConfirmExitOverlay({ onConfirm, onCancel }) {
31791
33391
  gap: 1,
31792
33392
  children: [/* @__PURE__ */ jsx(Text, {
31793
33393
  color: i === selected ? BRAND_COLOR : DIM_COLOR,
31794
- children: i === selected ? "▶" : " "
33394
+ children: i === selected ? Icons.triangleRight : " "
31795
33395
  }), /* @__PURE__ */ jsx(Text, {
31796
33396
  bold: i === selected,
31797
33397
  children: opt
31798
33398
  })]
31799
33399
  }, i))
33400
+ }),
33401
+ /* @__PURE__ */ jsx(Box, {
33402
+ marginTop: 1,
33403
+ children: /* @__PURE__ */ jsx(Text, {
33404
+ dimColor: true,
33405
+ children: "↑↓ escolher · Enter confirmar · Esc cancelar"
33406
+ })
31800
33407
  })
31801
33408
  ]
31802
33409
  });
@@ -31840,14 +33447,8 @@ function PromptOverlay({ prompt: prompt$1, onSubmit, onSkip }) {
31840
33447
  }
31841
33448
  function TextPrompt({ prompt: prompt$1, pulse, onSubmit, onSkip }) {
31842
33449
  const [value, setValue] = useState("");
31843
- const [mode, setMode] = useState("ask");
31844
33450
  const width = useOverlayWidth();
31845
33451
  useInput((input, key) => {
31846
- if (mode === "ask") {
31847
- if (input === "1" || key.return) setMode("input");
31848
- if (input === "2" || key.escape) onSkip();
31849
- return;
31850
- }
31851
33452
  if (key.return && value.trim()) {
31852
33453
  onSubmit(value.trim());
31853
33454
  return;
@@ -31866,6 +33467,7 @@ function TextPrompt({ prompt: prompt$1, pulse, onSubmit, onSkip }) {
31866
33467
  flexDirection: "column",
31867
33468
  borderStyle: "bold",
31868
33469
  borderColor: pulse ? BRAND_COLOR : ACCENT_COLOR,
33470
+ backgroundColor: "black",
31869
33471
  paddingX: 2,
31870
33472
  paddingY: 1,
31871
33473
  width,
@@ -31912,17 +33514,7 @@ function TextPrompt({ prompt: prompt$1, pulse, onSubmit, onSkip }) {
31912
33514
  ]
31913
33515
  })
31914
33516
  }) : null,
31915
- mode === "ask" ? /* @__PURE__ */ jsxs(Box, {
31916
- marginTop: 1,
31917
- flexDirection: "column",
31918
- children: [/* @__PURE__ */ jsx(Text, {
31919
- color: SUCCESS_COLOR,
31920
- children: "[1] Inserir valor agora"
31921
- }), /* @__PURE__ */ jsx(Text, {
31922
- color: DIM_COLOR,
31923
- children: "[2] Configurar depois manualmente"
31924
- })]
31925
- }) : /* @__PURE__ */ jsxs(Box, {
33517
+ /* @__PURE__ */ jsxs(Box, {
31926
33518
  marginTop: 1,
31927
33519
  flexDirection: "column",
31928
33520
  children: [/* @__PURE__ */ jsxs(Box, {
@@ -31956,6 +33548,7 @@ function ConfirmPrompt({ prompt: prompt$1, pulse, onSubmit }) {
31956
33548
  flexDirection: "column",
31957
33549
  borderStyle: "bold",
31958
33550
  borderColor: pulse ? BRAND_COLOR : ACCENT_COLOR,
33551
+ backgroundColor: "black",
31959
33552
  paddingX: 2,
31960
33553
  paddingY: 1,
31961
33554
  width,
@@ -31995,17 +33588,40 @@ function ConfirmPrompt({ prompt: prompt$1, pulse, onSubmit }) {
31995
33588
  function ChoicePrompt({ prompt: prompt$1, pulse, onSubmit, onSkip }) {
31996
33589
  const options = prompt$1.options ?? [];
31997
33590
  const [selected, setSelected] = useState(0);
33591
+ const [typed, setTyped] = useState("");
31998
33592
  const width = useOverlayWidth();
31999
- useInput((_input, key) => {
32000
- if (key.upArrow) setSelected((s) => Math.max(0, s - 1));
32001
- if (key.downArrow) setSelected((s) => Math.min(options.length - 1, s + 1));
32002
- if (key.return) onSubmit(options[selected] ?? "");
32003
- if (key.escape) onSkip();
33593
+ useInput((input, key) => {
33594
+ if (key.upArrow) {
33595
+ setSelected((s) => Math.max(0, s - 1));
33596
+ return;
33597
+ }
33598
+ if (key.downArrow) {
33599
+ setSelected((s) => Math.min(options.length - 1, s + 1));
33600
+ return;
33601
+ }
33602
+ if (key.return) {
33603
+ const trimmed = typed.trim();
33604
+ if (trimmed) onSubmit(trimmed);
33605
+ else onSubmit(options[selected] ?? "");
33606
+ return;
33607
+ }
33608
+ if (key.escape) {
33609
+ onSkip();
33610
+ return;
33611
+ }
33612
+ if (key.backspace || key.delete) {
33613
+ setTyped((v) => v.slice(0, -1));
33614
+ return;
33615
+ }
33616
+ if (input && !key.ctrl && !key.meta) setTyped((v) => v + input);
32004
33617
  });
33618
+ const borderColor = pulse ? BRAND_COLOR : ACCENT_COLOR;
33619
+ const trimmedTyped = typed.trim();
32005
33620
  return /* @__PURE__ */ jsxs(Box, {
32006
33621
  flexDirection: "column",
32007
33622
  borderStyle: "bold",
32008
- borderColor: pulse ? BRAND_COLOR : ACCENT_COLOR,
33623
+ borderColor,
33624
+ backgroundColor: "black",
32009
33625
  paddingX: 2,
32010
33626
  paddingY: 1,
32011
33627
  width,
@@ -32032,22 +33648,43 @@ function ChoicePrompt({ prompt: prompt$1, pulse, onSubmit, onSkip }) {
32032
33648
  /* @__PURE__ */ jsx(Box, {
32033
33649
  marginTop: 1,
32034
33650
  flexDirection: "column",
32035
- children: options.map((opt, i) => /* @__PURE__ */ jsxs(Box, {
33651
+ children: options.map((opt, i) => {
33652
+ const active$1 = !trimmedTyped && i === selected;
33653
+ return /* @__PURE__ */ jsxs(Box, {
33654
+ gap: 1,
33655
+ children: [/* @__PURE__ */ jsx(Text, {
33656
+ color: active$1 ? BRAND_COLOR : DIM_COLOR,
33657
+ children: active$1 ? Icons.triangleRight : " "
33658
+ }), /* @__PURE__ */ jsx(Text, {
33659
+ bold: active$1,
33660
+ dimColor: Boolean(trimmedTyped),
33661
+ children: opt
33662
+ })]
33663
+ }, i);
33664
+ })
33665
+ }),
33666
+ /* @__PURE__ */ jsx(Box, {
33667
+ marginTop: 1,
33668
+ flexDirection: "column",
33669
+ children: /* @__PURE__ */ jsxs(Box, {
32036
33670
  gap: 1,
32037
33671
  children: [/* @__PURE__ */ jsx(Text, {
32038
- color: i === selected ? BRAND_COLOR : DIM_COLOR,
32039
- children: i === selected ? Icons.triangleRight : " "
33672
+ color: trimmedTyped ? BRAND_COLOR : DIM_COLOR,
33673
+ children: Icons.triangleRight
33674
+ }), /* @__PURE__ */ jsxs(Text, { children: [typed || /* @__PURE__ */ jsx(Text, {
33675
+ color: DIM_COLOR,
33676
+ children: "ou digite uma resposta livre"
32040
33677
  }), /* @__PURE__ */ jsx(Text, {
32041
- bold: i === selected,
32042
- children: opt
32043
- })]
32044
- }, i))
33678
+ color: DIM_COLOR,
33679
+ children: "▌"
33680
+ })] })]
33681
+ })
32045
33682
  }),
32046
33683
  /* @__PURE__ */ jsx(Box, {
32047
33684
  marginTop: 1,
32048
33685
  children: /* @__PURE__ */ jsx(Text, {
32049
33686
  dimColor: true,
32050
- children: "Esc para pular"
33687
+ children: "↑↓ escolher · Enter confirmar · Esc pular"
32051
33688
  })
32052
33689
  })
32053
33690
  ]
@@ -32056,9 +33693,9 @@ function ChoicePrompt({ prompt: prompt$1, pulse, onSubmit, onSkip }) {
32056
33693
 
32057
33694
  //#endregion
32058
33695
  //#region src/wizard/ui/App.tsx
32059
- function App({ store, router, cwd }) {
33696
+ function App({ store, router, cwd, onExit: onExit$2 }) {
32060
33697
  useSyncExternalStore(store.subscribe, store.getSnapshot);
32061
- const [, rows] = useStdoutDimensions();
33698
+ const [columns, rows] = useStdoutDimensions();
32062
33699
  const { session } = store;
32063
33700
  const writePromptResponse = useCallback((value, skipped) => {
32064
33701
  const responsePath = join(cwd, ".veloz-wizard-response.json");
@@ -32080,14 +33717,7 @@ function App({ store, router, cwd }) {
32080
33717
  ]);
32081
33718
  const overlay = router.activeOverlay;
32082
33719
  let baseLayer;
32083
- if (overlay === Overlay.ConfirmExit) baseLayer = /* @__PURE__ */ jsx(ConfirmExitOverlay, {
32084
- onConfirm: () => {
32085
- router.popOverlay();
32086
- process.exit(130);
32087
- },
32088
- onCancel: () => router.popOverlay()
32089
- });
32090
- else if (overlay === Overlay.Error) baseLayer = /* @__PURE__ */ jsx(ErrorOverlay, {
33720
+ if (overlay === Overlay.Error) baseLayer = /* @__PURE__ */ jsx(ErrorOverlay, {
32091
33721
  message: session.agentError ?? "Erro desconhecido",
32092
33722
  onDismiss: () => router.popOverlay()
32093
33723
  });
@@ -32104,17 +33734,34 @@ function App({ store, router, cwd }) {
32104
33734
  case Screen.Run:
32105
33735
  baseLayer = /* @__PURE__ */ jsx(RunScreen, { store });
32106
33736
  break;
33737
+ case Screen.GithubSetup:
33738
+ baseLayer = /* @__PURE__ */ jsx(GithubSetupScreen, { store });
33739
+ break;
33740
+ case Screen.AiSetup:
33741
+ baseLayer = /* @__PURE__ */ jsx(AiSetupScreen, { store });
33742
+ break;
32107
33743
  case Screen.Outro:
32108
33744
  baseLayer = /* @__PURE__ */ jsx(OutroScreen, { store });
32109
33745
  break;
32110
33746
  default: baseLayer = /* @__PURE__ */ jsx(IntroScreen, { store });
32111
33747
  }
32112
33748
  const prompt$1 = session.agentPrompt;
33749
+ const floatingOverlay = overlay === Overlay.ConfirmExit ? /* @__PURE__ */ jsx(ConfirmExitOverlay, {
33750
+ onConfirm: () => {
33751
+ onExit$2("user confirmed exit");
33752
+ },
33753
+ onCancel: () => router.popOverlay()
33754
+ }) : prompt$1 ? /* @__PURE__ */ jsx(PromptOverlay, {
33755
+ prompt: prompt$1,
33756
+ onSubmit: (value) => writePromptResponse(value, false),
33757
+ onSkip: () => writePromptResponse("", true)
33758
+ }) : null;
32113
33759
  return /* @__PURE__ */ jsxs(Box, {
32114
33760
  flexDirection: "column",
32115
33761
  height: rows,
33762
+ width: columns,
32116
33763
  children: [/* @__PURE__ */ jsx(InputFocusProvider, {
32117
- active: !prompt$1,
33764
+ active: !floatingOverlay,
32118
33765
  children: /* @__PURE__ */ jsx(Box, {
32119
33766
  flexDirection: "column",
32120
33767
  flexGrow: 1,
@@ -32123,15 +33770,13 @@ function App({ store, router, cwd }) {
32123
33770
  overflow: "hidden",
32124
33771
  children: baseLayer
32125
33772
  })
32126
- }), prompt$1 ? /* @__PURE__ */ jsx(Box, {
32127
- flexShrink: 0,
32128
- paddingX: 2,
32129
- paddingY: 1,
32130
- children: /* @__PURE__ */ jsx(PromptOverlay, {
32131
- prompt: prompt$1,
32132
- onSubmit: (value) => writePromptResponse(value, false),
32133
- onSkip: () => writePromptResponse("", true)
32134
- })
33773
+ }), floatingOverlay ? /* @__PURE__ */ jsx(Box, {
33774
+ position: "absolute",
33775
+ width: columns,
33776
+ height: rows,
33777
+ alignItems: "center",
33778
+ justifyContent: "center",
33779
+ children: floatingOverlay
32135
33780
  }) : null]
32136
33781
  });
32137
33782
  }
@@ -32362,6 +34007,7 @@ var WizardStore = class {
32362
34007
  /** Set agent helper content (replaces previous content). */
32363
34008
  setAgentHelper(lines) {
32364
34009
  this.$session.setKey("agentHelperLines", lines);
34010
+ this.$session.setKey("agentHelperDiagram", null);
32365
34011
  this.emitChange();
32366
34012
  }
32367
34013
  /** Append a line to agent helper content. */
@@ -32370,6 +34016,12 @@ var WizardStore = class {
32370
34016
  this.$session.setKey("agentHelperLines", lines);
32371
34017
  this.emitChange();
32372
34018
  }
34019
+ /** Set structured deploy diagram (replaces previous diagram and lines). */
34020
+ setAgentHelperDiagram(diagram) {
34021
+ this.$session.setKey("agentHelperDiagram", diagram);
34022
+ this.$session.setKey("agentHelperLines", []);
34023
+ this.emitChange();
34024
+ }
32373
34025
  /** Show a prompt overlay to the user. */
32374
34026
  setAgentPrompt(prompt$1) {
32375
34027
  getSessionLogger().logStoreEvent("setAgentPrompt", {
@@ -32412,46 +34064,60 @@ var WizardStore = class {
32412
34064
  this.$session.setKey("buildDeploymentId", id);
32413
34065
  this.emitChange();
32414
34066
  }
34067
+ setGithubSetupPhase(phase) {
34068
+ getSessionLogger().logStoreEvent("githubSetupPhase", {
34069
+ from: this.session.githubSetupPhase,
34070
+ to: phase
34071
+ });
34072
+ this.$session.setKey("githubSetupPhase", phase);
34073
+ this.emitChange();
34074
+ }
34075
+ setGithubRepoInfo(owner, repo) {
34076
+ this.$session.setKey("githubRepoOwner", owner);
34077
+ this.$session.setKey("githubRepoName", repo);
34078
+ this.emitChange();
34079
+ }
34080
+ setGithubInstallUrl(url) {
34081
+ this.$session.setKey("githubInstallUrl", url);
34082
+ this.emitChange();
34083
+ }
34084
+ failGithubSetup(error) {
34085
+ getSessionLogger().logStoreEvent("failGithubSetup", { error });
34086
+ this.$session.setKey("githubSetupError", error);
34087
+ this.$session.setKey("githubSetupPhase", "error");
34088
+ this.emitChange();
34089
+ }
34090
+ setAiSetupNeeds(needs) {
34091
+ this.$session.setKey("aiSetupNeedsMcp", needs.mcp);
34092
+ this.$session.setKey("aiSetupNeedsSkills", needs.skills);
34093
+ this.$session.setKey("aiSetupMcpStatus", needs.mcp ? "pending" : "skipped");
34094
+ this.$session.setKey("aiSetupSkillsStatus", needs.skills ? "pending" : "skipped");
34095
+ this.emitChange();
34096
+ }
34097
+ setAiSetupPhase(phase) {
34098
+ getSessionLogger().logStoreEvent("aiSetupPhase", {
34099
+ from: this.session.aiSetupPhase,
34100
+ to: phase
34101
+ });
34102
+ this.$session.setKey("aiSetupPhase", phase);
34103
+ this.emitChange();
34104
+ }
34105
+ setAiSetupMcpStatus(status) {
34106
+ this.$session.setKey("aiSetupMcpStatus", status);
34107
+ this.emitChange();
34108
+ }
34109
+ setAiSetupSkillsStatus(status) {
34110
+ this.$session.setKey("aiSetupSkillsStatus", status);
34111
+ this.emitChange();
34112
+ }
34113
+ failAiSetup(error) {
34114
+ getSessionLogger().logStoreEvent("failAiSetup", { error });
34115
+ this.$session.setKey("aiSetupError", error);
34116
+ this.$session.setKey("aiSetupPhase", "error");
34117
+ this.emitChange();
34118
+ }
32415
34119
  };
32416
34120
 
32417
- //#endregion
32418
- //#region src/wizard/start-tui.ts
32419
- /**
32420
- * Start the Ink TUI renderer for the wizard.
32421
- * Forces dark background, creates store + router, renders App.
32422
- */
32423
- function startTUI(cwd) {
32424
- if (process.stdout.isTTY) process.stdout.write("\x1B[48;2;0;0;0m\x1B[2J\x1B[H");
32425
- const store = new WizardStore();
32426
- const router = new WizardRouter();
32427
- const { unmount } = render(createElement(App, {
32428
- store,
32429
- router,
32430
- cwd: cwd ?? process.cwd()
32431
- }));
32432
- const cleanup = () => {
32433
- if (process.stdout.isTTY) process.stdout.write("\x1B[0m\x1B[2J\x1B[H");
32434
- };
32435
- process.on("exit", cleanup);
32436
- let sigintCount = 0;
32437
- process.on("SIGINT", () => {
32438
- sigintCount++;
32439
- const logger = getSessionLogger();
32440
- logger.log("signal", "SIGINT received", { count: sigintCount });
32441
- if (sigintCount >= 2) {
32442
- logger.finalize("abort", { error: "user double-SIGINT" });
32443
- process.exit(130);
32444
- }
32445
- router.pushOverlay(Overlay.ConfirmExit);
32446
- });
32447
- return {
32448
- store,
32449
- router,
32450
- unmount,
32451
- waitForIntro: () => store.getGate("intro", (s) => s.introConfirmed)
32452
- };
32453
- }
32454
-
32455
34121
  //#endregion
32456
34122
  //#region src/wizard/upload-telemetry.ts
32457
34123
  /**
@@ -32482,23 +34148,16 @@ function contentTypeFor(filename) {
32482
34148
  return CONTENT_TYPES[filename] ?? "application/octet-stream";
32483
34149
  }
32484
34150
  async function uploadInitTelemetry(sessionDir) {
32485
- if (process.env.VELOZ_NO_TELEMETRY === "1") {
32486
- process.stderr.write("[veloz init] telemetria desativada (VELOZ_NO_TELEMETRY=1)\n");
32487
- return;
32488
- }
34151
+ if (process.env.VELOZ_NO_TELEMETRY === "1") return;
32489
34152
  const metaPath = join(sessionDir, "meta.json");
32490
34153
  let meta;
32491
34154
  try {
32492
34155
  meta = JSON.parse(readFileSync(metaPath, "utf-8"));
32493
- } catch (err) {
32494
- process.stderr.write(`[veloz init] falha ao ler meta.json: ${err instanceof Error ? err.message : String(err)}\n`);
34156
+ } catch {
32495
34157
  return;
32496
34158
  }
32497
34159
  const authConfig = loadConfig();
32498
- if (!authConfig.apiKey) {
32499
- process.stderr.write("[veloz init] sem apiKey — telemetria não enviada\n");
32500
- return;
32501
- }
34160
+ if (!authConfig.apiKey) return;
32502
34161
  const files = readdirSync(sessionDir).filter((name) => {
32503
34162
  const full = join(sessionDir, name);
32504
34163
  try {
@@ -32535,11 +34194,10 @@ async function uploadInitTelemetry(sessionDir) {
32535
34194
  startedAt: meta.startedAt,
32536
34195
  files
32537
34196
  });
32538
- } catch (err) {
32539
- process.stderr.write(`[veloz init] telemetria (start) falhou: ${err instanceof Error ? err.message : String(err)}\n`);
34197
+ } catch {
32540
34198
  return;
32541
34199
  }
32542
- const failures = (await Promise.allSettled(files.map(async (filename) => {
34200
+ await Promise.allSettled(files.map(async (filename) => {
32543
34201
  const url = started.uploads[filename];
32544
34202
  if (!url) return;
32545
34203
  const body = readFileSync(join(sessionDir, filename));
@@ -32549,8 +34207,7 @@ async function uploadInitTelemetry(sessionDir) {
32549
34207
  body
32550
34208
  });
32551
34209
  if (!res.ok) throw new Error(`${filename}: HTTP ${res.status}`);
32552
- }))).filter((r) => r.status === "rejected");
32553
- if (failures.length > 0) for (const failure of failures) process.stderr.write(`[veloz init] upload de arquivo falhou: ${String(failure.reason?.message ?? failure.reason)}\n`);
34210
+ }));
32554
34211
  try {
32555
34212
  await client.initTelemetry.finalize({
32556
34213
  id: started.id,
@@ -32561,11 +34218,70 @@ async function uploadInitTelemetry(sessionDir) {
32561
34218
  durationMs: meta.durationMs,
32562
34219
  endedAt: meta.endedAt
32563
34220
  });
32564
- } catch (err) {
32565
- process.stderr.write(`[veloz init] telemetria (finalize) falhou: ${err instanceof Error ? err.message : String(err)}\n`);
32566
- return;
32567
- }
32568
- process.stderr.write(`[veloz init] telemetria enviada (sessão ${started.id})\n`);
34221
+ } catch {}
34222
+ }
34223
+ let telemetrySent = false;
34224
+ /**
34225
+ * Finalize the session logger and upload telemetry exactly once per process.
34226
+ *
34227
+ * Called on both normal exit and abort paths (double-SIGINT, ConfirmExit, headless
34228
+ * SIGINT). Bounded by `timeoutMs` so a hanging upload cannot block process exit.
34229
+ */
34230
+ async function finalizeAndUploadOnce(logger, outcome, info$1, timeoutMs = 5e3) {
34231
+ if (telemetrySent) return;
34232
+ telemetrySent = true;
34233
+ logger.finalize(outcome, info$1);
34234
+ await Promise.race([uploadInitTelemetry(logger.dir).catch(() => {}), new Promise((resolve$1) => {
34235
+ setTimeout(resolve$1, timeoutMs);
34236
+ })]);
34237
+ }
34238
+
34239
+ //#endregion
34240
+ //#region src/wizard/start-tui.ts
34241
+ /**
34242
+ * Start the Ink TUI renderer for the wizard.
34243
+ * Forces dark background, creates store + router, renders App.
34244
+ */
34245
+ function startTUI(cwd) {
34246
+ if (process.stdout.isTTY) process.stdout.write("\x1B[48;2;0;0;0m\x1B[2J\x1B[H");
34247
+ const store = new WizardStore();
34248
+ const router = new WizardRouter();
34249
+ const workingDir = cwd ?? process.cwd();
34250
+ let teardown = () => {};
34251
+ const exitWithTelemetry = async (reason) => {
34252
+ try {
34253
+ teardown();
34254
+ } catch {}
34255
+ await finalizeAndUploadOnce(getSessionLogger(), "abort", { error: reason });
34256
+ process.exit(130);
34257
+ };
34258
+ const { unmount } = render(createElement(App, {
34259
+ store,
34260
+ router,
34261
+ cwd: workingDir,
34262
+ onExit: exitWithTelemetry
34263
+ }));
34264
+ teardown = unmount;
34265
+ const cleanup = () => {
34266
+ if (process.stdout.isTTY) process.stdout.write("\x1B[0m\x1B[2J\x1B[H");
34267
+ };
34268
+ process.on("exit", cleanup);
34269
+ let sigintCount = 0;
34270
+ process.on("SIGINT", () => {
34271
+ sigintCount++;
34272
+ getSessionLogger().log("signal", "SIGINT received", { count: sigintCount });
34273
+ if (sigintCount >= 2) {
34274
+ exitWithTelemetry("user double-SIGINT");
34275
+ return;
34276
+ }
34277
+ router.pushOverlay(Overlay.ConfirmExit);
34278
+ });
34279
+ return {
34280
+ store,
34281
+ router,
34282
+ unmount,
34283
+ waitForIntro: () => store.getGate("intro", (s) => s.introConfirmed)
34284
+ };
32569
34285
  }
32570
34286
 
32571
34287
  //#endregion
@@ -32580,6 +34296,43 @@ async function uploadInitTelemetry(sessionDir) {
32580
34296
  * 5. Spawn Claude Agent → show RunScreen
32581
34297
  * 6. Show OutroScreen with URLs + next steps
32582
34298
  */
34299
+ /** Convert an unknown thrown value into a stable string for telemetry. */
34300
+ function describeError(err) {
34301
+ if (err instanceof Error) return {
34302
+ message: err.message,
34303
+ stack: err.stack
34304
+ };
34305
+ return { message: typeof err === "string" ? err : JSON.stringify(err) };
34306
+ }
34307
+ /** Compose the error string we persist on `meta.error`. */
34308
+ function formatErrorForMeta(prefix, err) {
34309
+ const { message, stack } = describeError(err);
34310
+ return stack ? `${prefix}: ${message}\n${stack}` : `${prefix}: ${message}`;
34311
+ }
34312
+ /**
34313
+ * Install process-level safety nets so a stray throw / rejection still flushes
34314
+ * telemetry before the CLI dies. Returns a disposer that removes the handlers
34315
+ * (so we don't pollute long-running parent processes in tests).
34316
+ */
34317
+ function installCrashHandlers(logger) {
34318
+ const handleCrash = (kind, err) => {
34319
+ const { message, stack } = describeError(err);
34320
+ logger.log("crash", `process ${kind}`, {
34321
+ message,
34322
+ stack
34323
+ });
34324
+ process.stderr.write(`[veloz init] ${kind}: ${message}\n`);
34325
+ finalizeAndUploadOnce(logger, "error", { error: formatErrorForMeta(kind, err) }).catch(() => {}).finally(() => process.exit(1));
34326
+ };
34327
+ const onUncaught = (err) => handleCrash("uncaughtException", err);
34328
+ const onUnhandled = (reason) => handleCrash("unhandledRejection", reason);
34329
+ process.on("uncaughtException", onUncaught);
34330
+ process.on("unhandledRejection", onUnhandled);
34331
+ return () => {
34332
+ process.off("uncaughtException", onUncaught);
34333
+ process.off("unhandledRejection", onUnhandled);
34334
+ };
34335
+ }
32583
34336
  /** Detect repo using the lazy local filesystem layer. */
32584
34337
  function detectLocalRepo(cwd) {
32585
34338
  const layer = makeLocalFs(cwd);
@@ -32616,6 +34369,7 @@ function registerInit(cli$1) {
32616
34369
  }),
32617
34370
  outputPolicy: "agent-only",
32618
34371
  async run(c) {
34372
+ setClientSource("cli-wizard");
32619
34373
  const cwd = process.cwd();
32620
34374
  const logger = initSessionLogger({ cwd });
32621
34375
  logger.log("init", "veloz init invoked", {
@@ -32626,105 +34380,375 @@ function registerInit(cli$1) {
32626
34380
  argv: process.argv.slice(2)
32627
34381
  });
32628
34382
  process.stderr.write(`[veloz init] debug logs: ${logger.dir}\n`);
32629
- const analysis = detectLocalRepo(cwd);
32630
- logger.log("detection", "repo analysis complete", {
32631
- framework: analysis.framework?.name ?? null,
32632
- frameworkLabel: analysis.framework?.label ?? null,
32633
- packageManager: analysis.packageManager,
32634
- isMonorepo: analysis.isMonorepo
32635
- });
32636
- const detectedFrameworkId = analysis.framework ? DETECTOR_TO_FRAMEWORK[analysis.framework.name] ?? "nodejs" : null;
32637
- const detectedLabel = analysis.framework?.label ?? null;
32638
- const forcedFramework = c.options.framework;
32639
- if (!process.stdout.isTTY || c.options.ci) {
32640
- logger.log("mode", "running headless (no TUI)");
32641
- return await runHeadless({
32642
- cwd,
32643
- frameworkId: forcedFramework ?? detectedFrameworkId ?? "nodejs",
32644
- frameworkLabel: forcedFramework ? FRAMEWORK_LABELS[forcedFramework] ?? forcedFramework : detectedLabel ?? "Node.js",
32645
- packageManager: analysis.packageManager ?? "npm"
34383
+ const removeCrashHandlers = installCrashHandlers(logger);
34384
+ try {
34385
+ return await runInitFlow(c, logger, cwd);
34386
+ } catch (err) {
34387
+ const { message, stack } = describeError(err);
34388
+ logger.log("error", "init flow failed", {
34389
+ message,
34390
+ stack
32646
34391
  });
34392
+ await finalizeAndUploadOnce(logger, "error", { error: formatErrorForMeta("init", err) });
34393
+ throw err;
34394
+ } finally {
34395
+ removeCrashHandlers();
32647
34396
  }
32648
- const tui = startTUI();
32649
- const { store } = tui;
32650
- logger.log("tui", "TUI started");
32651
- store.setDetection({
32652
- framework: detectedFrameworkId,
32653
- frameworkLabel: detectedLabel,
32654
- packageManager: analysis.packageManager,
32655
- gitRemote: detectGitRemote(cwd),
32656
- isMonorepo: analysis.isMonorepo
32657
- });
32658
- if (forcedFramework) {
32659
- store.selectFramework(forcedFramework, FRAMEWORK_LABELS[forcedFramework] ?? forcedFramework);
32660
- store.confirmIntro();
32661
- }
32662
- logger.log("flow", "awaiting intro confirmation");
32663
- await tui.waitForIntro();
32664
- logger.log("flow", "intro confirmed");
32665
- const selectedFramework = store.session.selectedFramework ?? detectedFrameworkId ?? "nodejs";
32666
- const selectedLabel = store.session.selectedFrameworkLabel ?? detectedLabel ?? "Node.js";
32667
- logger.updateMeta({
32668
- framework: selectedFramework,
32669
- frameworkLabel: selectedLabel,
32670
- packageManager: store.session.packageManager ?? "npm",
32671
- isMonorepo: store.session.isMonorepo,
32672
- gitRemote: store.session.gitRemote
32673
- });
32674
- logger.log("flow", "resolving auth + org");
32675
- await resolveAuthAndOrg(store);
32676
- logger.log("auth", "auth + org resolved", {
32677
- userName: store.session.userName,
32678
- orgId: store.session.orgId,
32679
- orgName: store.session.orgName
32680
- });
32681
- logger.updateMeta({
32682
- userName: store.session.userName,
32683
- orgName: store.session.orgName,
32684
- orgId: store.session.orgId
34397
+ }
34398
+ });
34399
+ }
34400
+ async function runInitFlow(c, logger, cwd) {
34401
+ const forcedFramework = c.options.framework;
34402
+ const analysis = detectLocalRepo(cwd);
34403
+ logger.log("detection", "repo analysis complete", {
34404
+ framework: analysis.framework?.name ?? null,
34405
+ frameworkLabel: analysis.framework?.label ?? null,
34406
+ packageManager: analysis.packageManager,
34407
+ isMonorepo: analysis.isMonorepo
34408
+ });
34409
+ logger.updateMeta({ analysis });
34410
+ const detectedFrameworkId = analysis.framework ? DETECTOR_TO_FRAMEWORK[analysis.framework.name] ?? "nodejs" : null;
34411
+ const detectedLabel = analysis.framework?.label ?? null;
34412
+ if (!process.stdout.isTTY || c.options.ci) {
34413
+ logger.log("mode", "running headless (no TUI)");
34414
+ return await runHeadless({
34415
+ cwd,
34416
+ frameworkId: forcedFramework ?? detectedFrameworkId ?? "nodejs",
34417
+ frameworkLabel: forcedFramework ? FRAMEWORK_LABELS[forcedFramework] ?? forcedFramework : detectedLabel ?? "Node.js",
34418
+ packageManager: analysis.packageManager ?? "npm"
34419
+ });
34420
+ }
34421
+ const tui = startTUI();
34422
+ const { store } = tui;
34423
+ logger.log("tui", "TUI started");
34424
+ store.setDetection({
34425
+ framework: detectedFrameworkId,
34426
+ frameworkLabel: detectedLabel,
34427
+ packageManager: analysis.packageManager,
34428
+ gitRemote: detectGitRemote(cwd),
34429
+ isMonorepo: analysis.isMonorepo
34430
+ });
34431
+ if (forcedFramework) {
34432
+ store.selectFramework(forcedFramework, FRAMEWORK_LABELS[forcedFramework] ?? forcedFramework);
34433
+ store.confirmIntro();
34434
+ }
34435
+ logger.log("flow", "awaiting intro confirmation");
34436
+ await tui.waitForIntro();
34437
+ logger.log("flow", "intro confirmed");
34438
+ const selectedFramework = store.session.selectedFramework ?? detectedFrameworkId ?? "nodejs";
34439
+ const selectedLabel = store.session.selectedFrameworkLabel ?? detectedLabel ?? "Node.js";
34440
+ logger.updateMeta({
34441
+ framework: selectedFramework,
34442
+ frameworkLabel: selectedLabel,
34443
+ packageManager: store.session.packageManager ?? "npm",
34444
+ isMonorepo: store.session.isMonorepo,
34445
+ gitRemote: store.session.gitRemote
34446
+ });
34447
+ logger.log("flow", "resolving auth + org");
34448
+ await resolveAuthAndOrg(store);
34449
+ logger.log("auth", "auth + org resolved", {
34450
+ userName: store.session.userName,
34451
+ orgId: store.session.orgId,
34452
+ orgName: store.session.orgName
34453
+ });
34454
+ logger.updateMeta({
34455
+ userName: store.session.userName,
34456
+ orgName: store.session.orgName,
34457
+ orgId: store.session.orgId
34458
+ });
34459
+ logger.log("flow", "awaiting user notes");
34460
+ await store.getGate("notes", (s) => s.notesConfirmed);
34461
+ logger.log("flow", "notes confirmed", {
34462
+ hasNotes: Boolean(store.session.userNotes),
34463
+ bytes: store.session.userNotes?.length ?? 0
34464
+ });
34465
+ logger.log("agent", "invoking runAgent");
34466
+ await runAgent({
34467
+ store,
34468
+ cwd,
34469
+ framework: selectedFramework,
34470
+ frameworkLabel: selectedLabel,
34471
+ userName: store.session.userName,
34472
+ orgName: store.session.orgName,
34473
+ packageManager: store.session.packageManager ?? "npm",
34474
+ userNotes: store.session.userNotes
34475
+ });
34476
+ logger.log("agent", "runAgent returned", {
34477
+ phase: store.session.agentPhase,
34478
+ error: store.session.agentError,
34479
+ deployUrl: store.session.deployUrl,
34480
+ retryCount: store.session.retryCount
34481
+ });
34482
+ if (!store.session.agentError) {
34483
+ logger.log("flow", "running github auto-deploy prompt flow");
34484
+ await runGithubSetupFlow(store, cwd);
34485
+ logger.log("flow", "github auto-deploy flow done", {
34486
+ phase: store.session.githubSetupPhase,
34487
+ error: store.session.githubSetupError
34488
+ });
34489
+ }
34490
+ if (!store.session.agentError) {
34491
+ logger.log("flow", "running ai tooling prompt flow");
34492
+ await runAiSetupFlow(store);
34493
+ logger.log("flow", "ai tooling flow done", {
34494
+ phase: store.session.aiSetupPhase,
34495
+ error: store.session.aiSetupError
34496
+ });
34497
+ }
34498
+ logger.log("flow", "awaiting outro dismissal");
34499
+ await store.getGate("outro", (s) => s.outroDismissed);
34500
+ logger.log("flow", "outro dismissed");
34501
+ tui.unmount();
34502
+ logger.log("tui", "TUI unmounted");
34503
+ const success$1 = !store.session.agentError;
34504
+ await finalizeAndUploadOnce(logger, success$1 ? "success" : "error", {
34505
+ error: store.session.agentError ?? void 0,
34506
+ deployUrl: store.session.deployUrl ?? void 0,
34507
+ retries: store.session.retryCount
34508
+ });
34509
+ return {
34510
+ success: success$1,
34511
+ url: store.session.deployUrl ?? void 0
34512
+ };
34513
+ }
34514
+ /**
34515
+ * Parse the first URL in `.git/config` into an owner/repo pair.
34516
+ * We read the file directly (instead of shelling out) so the flow works
34517
+ * regardless of cwd and without depending on `git` being on PATH.
34518
+ */
34519
+ function detectGithubRepo(cwd) {
34520
+ try {
34521
+ const gitConfigPath = join(cwd, ".git", "config");
34522
+ if (!existsSync(gitConfigPath)) return null;
34523
+ const match$3 = readFileSync(gitConfigPath, "utf-8").match(/url\s*=\s*(.+)/);
34524
+ if (!match$3?.[1]) return null;
34525
+ const url = match$3[1].trim();
34526
+ const httpsMatch = url.match(/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/);
34527
+ if (httpsMatch?.[1] && httpsMatch[2]) return {
34528
+ owner: httpsMatch[1],
34529
+ repo: httpsMatch[2]
34530
+ };
34531
+ const sshMatch = url.match(/github\.com:([^/]+)\/([^/.]+?)(?:\.git)?$/);
34532
+ if (sshMatch?.[1] && sshMatch[2]) return {
34533
+ owner: sshMatch[1],
34534
+ repo: sshMatch[2]
34535
+ };
34536
+ return null;
34537
+ } catch {
34538
+ return null;
34539
+ }
34540
+ }
34541
+ /**
34542
+ * Drive the post-deploy GitHub auto-deploy setup.
34543
+ *
34544
+ * 1. If there is no GitHub remote or no veloz.json project id, stay idle.
34545
+ * 2. Show the prompt screen (Yes/No). If the user declines → phase=skipped.
34546
+ * 3. On "yes", check if the project is already connected on the server — if so,
34547
+ * short-circuit to `done` without hitting the GitHub App APIs.
34548
+ * 4. Otherwise check the GitHub App installation status. If installed → connect
34549
+ * the repo and finish. If not → open the install URL, poll for install, then
34550
+ * connect the repo.
34551
+ */
34552
+ async function runGithubSetupFlow(store, cwd) {
34553
+ const logger = getSessionLogger();
34554
+ const remote = detectGithubRepo(cwd);
34555
+ if (!remote) {
34556
+ logger.log("github-setup", "no GitHub remote detected — skipping");
34557
+ return;
34558
+ }
34559
+ const projectId = loadConfig$1()?.project?.id;
34560
+ if (!projectId) {
34561
+ logger.log("github-setup", "no project id in veloz.json — skipping");
34562
+ return;
34563
+ }
34564
+ store.setGithubRepoInfo(remote.owner, remote.repo);
34565
+ store.setGithubSetupPhase("prompt");
34566
+ await store.getGate("github-setup-prompt", (s) => s.githubSetupPhase !== "prompt");
34567
+ if (store.session.githubSetupPhase === "skipped") {
34568
+ logger.log("github-setup", "user declined auto-deploy setup");
34569
+ return;
34570
+ }
34571
+ try {
34572
+ const client = await getClient();
34573
+ const existing = (await client.projects.list()).find((p) => p.id === projectId);
34574
+ if (existing?.githubRepoOwner === remote.owner && existing.githubRepoName === remote.repo && existing.githubInstallationId) {
34575
+ logger.log("github-setup", "project already connected — short-circuit");
34576
+ store.setGithubSetupPhase("done");
34577
+ return;
34578
+ }
34579
+ const installation = await client.github.getInstallation({ owner: remote.owner });
34580
+ if (installation.error === "TOKEN_EXPIRED") {
34581
+ store.failGithubSetup("Token do GitHub expirado. Reconecte sua conta no dashboard e tente novamente.");
34582
+ return;
34583
+ }
34584
+ if (installation.error === "RATE_LIMITED") {
34585
+ store.failGithubSetup("Limite de requisições do GitHub atingido. Tente de novo em alguns minutos.");
34586
+ return;
34587
+ }
34588
+ if (installation.installed && installation.installationId) {
34589
+ store.setGithubSetupPhase("connecting");
34590
+ await client.projects.connectRepo({
34591
+ projectId,
34592
+ githubRepoOwner: remote.owner,
34593
+ githubRepoName: remote.repo,
34594
+ githubInstallationId: installation.installationId
32685
34595
  });
32686
- logger.log("flow", "awaiting user notes");
32687
- await store.getGate("notes", (s) => s.notesConfirmed);
32688
- logger.log("flow", "notes confirmed", {
32689
- hasNotes: Boolean(store.session.userNotes),
32690
- bytes: store.session.userNotes?.length ?? 0
34596
+ store.setGithubSetupPhase("done");
34597
+ return;
34598
+ }
34599
+ if (!installation.installUrl) {
34600
+ store.failGithubSetup("GitHub App não está configurado no servidor. Contate o administrador da plataforma.");
34601
+ return;
34602
+ }
34603
+ store.setGithubInstallUrl(installation.installUrl);
34604
+ store.setGithubSetupPhase("installing");
34605
+ await openBrowser(installation.installUrl);
34606
+ const installationId = await pollGithubInstallation(store, remote.owner);
34607
+ if (!installationId) {
34608
+ store.failGithubSetup("Tempo esgotado aguardando a instalação do GitHub App.");
34609
+ return;
34610
+ }
34611
+ store.setGithubSetupPhase("connecting");
34612
+ await client.projects.connectRepo({
34613
+ projectId,
34614
+ githubRepoOwner: remote.owner,
34615
+ githubRepoName: remote.repo,
34616
+ githubInstallationId: installationId
34617
+ });
34618
+ store.setGithubSetupPhase("done");
34619
+ } catch (error) {
34620
+ const message = error instanceof Error ? error.message : String(error);
34621
+ logger.log("github-setup", "unexpected error", { message });
34622
+ store.failGithubSetup(message);
34623
+ }
34624
+ }
34625
+ /**
34626
+ * Offer to install the Veloz MCP server and/or the agent skills when missing.
34627
+ * When the user agrees, we re-invoke the current CLI binary (`process.execPath
34628
+ * <argv[1]>`) for each command so we work regardless of install location, and
34629
+ * capture stdio so the child's output doesn't corrupt the Ink frame.
34630
+ */
34631
+ async function runAiSetupFlow(store) {
34632
+ const logger = getSessionLogger();
34633
+ const status = checkAiSetup();
34634
+ const needsMcp = !status.mcpInstalled;
34635
+ const needsSkills = !status.skillsInstalled;
34636
+ if (!needsMcp && !needsSkills) {
34637
+ logger.log("ai-setup", "both MCP and skills already installed — skipping");
34638
+ return;
34639
+ }
34640
+ store.setAiSetupNeeds({
34641
+ mcp: needsMcp,
34642
+ skills: needsSkills
34643
+ });
34644
+ store.setAiSetupPhase("prompt");
34645
+ await store.getGate("ai-setup-prompt", (s) => s.aiSetupPhase !== "prompt");
34646
+ if (store.session.aiSetupPhase === "skipped") {
34647
+ logger.log("ai-setup", "user declined ai tooling setup");
34648
+ return;
34649
+ }
34650
+ let anyError = false;
34651
+ if (needsMcp) {
34652
+ store.setAiSetupMcpStatus("installing");
34653
+ const mcpResult = await runVelozSubcommand(["mcp", "add"]);
34654
+ if (mcpResult.ok) store.setAiSetupMcpStatus("done");
34655
+ else {
34656
+ store.setAiSetupMcpStatus("error");
34657
+ anyError = true;
34658
+ logger.log("ai-setup", "mcp add failed", { stderr: mcpResult.stderr });
34659
+ }
34660
+ }
34661
+ if (needsSkills) {
34662
+ store.setAiSetupSkillsStatus("installing");
34663
+ const skillsResult = await runVelozSubcommand(["skills", "add"]);
34664
+ if (skillsResult.ok) store.setAiSetupSkillsStatus("done");
34665
+ else {
34666
+ store.setAiSetupSkillsStatus("error");
34667
+ anyError = true;
34668
+ logger.log("ai-setup", "skills add failed", { stderr: skillsResult.stderr });
34669
+ }
34670
+ }
34671
+ if (anyError) store.failAiSetup("Alguma etapa da integração falhou. Veja os logs de debug para detalhes.");
34672
+ else store.setAiSetupPhase("done");
34673
+ }
34674
+ /**
34675
+ * Re-invoke the current CLI (`process.execPath <argv[1]>`) with the given args
34676
+ * while the TUI is running. Silences child stdout/stderr (captured and returned
34677
+ * on failure) so the child doesn't paint over the Ink frame. Times out after
34678
+ * two minutes to prevent a stuck interactive child from hanging the wizard.
34679
+ */
34680
+ function runVelozSubcommand(args$1) {
34681
+ return new Promise((resolve$1) => {
34682
+ const entry = process.argv[1];
34683
+ if (!entry) {
34684
+ resolve$1({
34685
+ ok: false,
34686
+ stderr: "Could not resolve CLI entry path."
32691
34687
  });
32692
- logger.log("agent", "invoking runAgent");
32693
- await runAgent({
32694
- store,
32695
- cwd,
32696
- framework: selectedFramework,
32697
- frameworkLabel: selectedLabel,
32698
- userName: store.session.userName,
32699
- orgName: store.session.orgName,
32700
- packageManager: store.session.packageManager ?? "npm",
32701
- userNotes: store.session.userNotes
34688
+ return;
34689
+ }
34690
+ const child = spawn(process.execPath, [entry, ...args$1], {
34691
+ stdio: [
34692
+ "ignore",
34693
+ "pipe",
34694
+ "pipe"
34695
+ ],
34696
+ env: process.env
34697
+ });
34698
+ let stderr = "";
34699
+ let settled = false;
34700
+ const settle = (result$2) => {
34701
+ if (settled) return;
34702
+ settled = true;
34703
+ clearTimeout(timeoutHandle);
34704
+ resolve$1(result$2);
34705
+ };
34706
+ const timeoutHandle = setTimeout(() => {
34707
+ child.kill("SIGTERM");
34708
+ settle({
34709
+ ok: false,
34710
+ stderr: "Tempo esgotado na instalação."
32702
34711
  });
32703
- logger.log("agent", "runAgent returned", {
32704
- phase: store.session.agentPhase,
32705
- error: store.session.agentError,
32706
- deployUrl: store.session.deployUrl,
32707
- retryCount: store.session.retryCount
34712
+ }, 12e4);
34713
+ child.stdout?.on("data", () => {});
34714
+ child.stderr?.on("data", (chunk) => {
34715
+ stderr += chunk.toString();
34716
+ });
34717
+ child.on("error", (err) => {
34718
+ settle({
34719
+ ok: false,
34720
+ stderr: err.message
32708
34721
  });
32709
- logger.log("flow", "awaiting outro dismissal");
32710
- await store.getGate("outro", (s) => s.outroDismissed);
32711
- logger.log("flow", "outro dismissed");
32712
- tui.unmount();
32713
- logger.log("tui", "TUI unmounted");
32714
- const success$1 = !store.session.agentError;
32715
- logger.finalize(success$1 ? "success" : "error", {
32716
- error: store.session.agentError ?? void 0,
32717
- deployUrl: store.session.deployUrl ?? void 0,
32718
- retries: store.session.retryCount
34722
+ });
34723
+ child.on("close", (code) => {
34724
+ settle({
34725
+ ok: code === 0,
34726
+ stderr
32719
34727
  });
32720
- await uploadInitTelemetry(logger.dir);
32721
- return {
32722
- success: success$1,
32723
- url: store.session.deployUrl ?? void 0
32724
- };
32725
- }
34728
+ });
32726
34729
  });
32727
34730
  }
34731
+ /**
34732
+ * Poll the server for a completed GitHub App installation on `owner`.
34733
+ * Returns the installation id when detected, or null on timeout / auth loss.
34734
+ */
34735
+ async function pollGithubInstallation(store, owner) {
34736
+ const client = await getClient();
34737
+ const maxAttempts = 60;
34738
+ const pollInterval = 5e3;
34739
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
34740
+ await new Promise((resolve$1) => {
34741
+ setTimeout(resolve$1, pollInterval);
34742
+ });
34743
+ const check = await client.github.getInstallation({ owner });
34744
+ if (check.installed && check.installationId) return check.installationId;
34745
+ if (check.error === "TOKEN_EXPIRED") {
34746
+ store.failGithubSetup("Token do GitHub expirou durante a espera.");
34747
+ return null;
34748
+ }
34749
+ }
34750
+ return null;
34751
+ }
32728
34752
  async function runHeadless(opts) {
32729
34753
  const { WizardStore: WizardStore$1 } = await import("./store-CFF2J0lW.mjs");
32730
34754
  const { runAgent: run } = await import("./agent-interface-DL5S8SEn.mjs");
@@ -32750,27 +34774,50 @@ async function runHeadless(opts) {
32750
34774
  userName: "CI",
32751
34775
  orgId: config.organizationId ?? ""
32752
34776
  });
32753
- logger.log("headless", "invoking runAgent");
32754
- await run({
32755
- store,
32756
- cwd: opts.cwd,
32757
- framework: opts.frameworkId,
32758
- frameworkLabel: opts.frameworkLabel,
32759
- userName: "CI",
32760
- orgName: "",
32761
- packageManager: opts.packageManager
32762
- });
32763
- const success$1 = !store.session.agentError;
32764
- logger.finalize(success$1 ? "success" : "error", {
32765
- error: store.session.agentError ?? void 0,
32766
- deployUrl: store.session.deployUrl ?? void 0,
32767
- retries: store.session.retryCount
32768
- });
32769
- await uploadInitTelemetry(logger.dir);
32770
- return {
32771
- success: success$1,
32772
- url: store.session.deployUrl ?? void 0
34777
+ const onSigint = () => {
34778
+ logger.log("signal", "SIGINT received (headless)");
34779
+ (async () => {
34780
+ await finalizeAndUploadOnce(logger, "abort", { error: "user SIGINT" });
34781
+ process.exit(130);
34782
+ })();
32773
34783
  };
34784
+ process.on("SIGINT", onSigint);
34785
+ try {
34786
+ logger.log("headless", "invoking runAgent");
34787
+ await run({
34788
+ store,
34789
+ cwd: opts.cwd,
34790
+ framework: opts.frameworkId,
34791
+ frameworkLabel: opts.frameworkLabel,
34792
+ userName: "CI",
34793
+ orgName: "",
34794
+ packageManager: opts.packageManager
34795
+ });
34796
+ const success$1 = !store.session.agentError;
34797
+ await finalizeAndUploadOnce(logger, success$1 ? "success" : "error", {
34798
+ error: store.session.agentError ?? void 0,
34799
+ deployUrl: store.session.deployUrl ?? void 0,
34800
+ retries: store.session.retryCount
34801
+ });
34802
+ return {
34803
+ success: success$1,
34804
+ url: store.session.deployUrl ?? void 0
34805
+ };
34806
+ } catch (err) {
34807
+ const { message, stack } = describeError(err);
34808
+ logger.log("headless", "runAgent threw", {
34809
+ message,
34810
+ stack
34811
+ });
34812
+ await finalizeAndUploadOnce(logger, "error", {
34813
+ error: formatErrorForMeta("headless", err),
34814
+ deployUrl: store.session.deployUrl ?? void 0,
34815
+ retries: store.session.retryCount
34816
+ });
34817
+ throw err;
34818
+ } finally {
34819
+ process.off("SIGINT", onSigint);
34820
+ }
32774
34821
  }
32775
34822
  /**
32776
34823
  * Perform device auth with TUI feedback — reuses shared helpers from login.ts.
@@ -32786,14 +34833,9 @@ async function performDeviceAuth(store, apiUrl) {
32786
34833
  store.setAuthPhase("polling");
32787
34834
  return pollForToken(authClient, data.device_code, data.interval || 5);
32788
34835
  }
32789
- /** Fetch the list of orgs the user belongs to via Better Auth. */
32790
- async function listOrganizations(apiUrl, apiKey) {
32791
- const res = await fetch(`${apiUrl}/api/auth/organization/list`, { headers: {
32792
- Authorization: `Bearer ${apiKey}`,
32793
- "User-Agent": "veloz-cli"
32794
- } });
32795
- if (!res.ok) throw new Error(`Falha ao listar workspaces (${res.status}).`);
32796
- return (await res.json()).map((o) => ({
34836
+ /** Fetch the list of orgs the user belongs to via ORPC. */
34837
+ async function listOrganizations() {
34838
+ return (await (await getClient()).organizations.list()).map((o) => ({
32797
34839
  id: o.id,
32798
34840
  name: o.name,
32799
34841
  slug: o.slug
@@ -32803,16 +34845,16 @@ async function listOrganizations(apiUrl, apiKey) {
32803
34845
  * Create a new organization via Better Auth. The slug is derived from the name
32804
34846
  * and suffixed with a random token when a collision occurs.
32805
34847
  */
32806
- async function createOrganization(apiUrl, apiKey, name) {
34848
+ async function createOrganization(apiUrl, name) {
32807
34849
  const baseSlug = name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "workspace";
34850
+ const headers = await getAuthHeaders();
32808
34851
  let slug = baseSlug;
32809
34852
  for (let attempt = 0; attempt < 3; attempt++) {
32810
34853
  const res = await fetch(`${apiUrl}/api/auth/organization/create`, {
32811
34854
  method: "POST",
32812
34855
  headers: {
32813
- Authorization: `Bearer ${apiKey}`,
32814
- "Content-Type": "application/json",
32815
- "User-Agent": "veloz-cli"
34856
+ ...headers,
34857
+ "Content-Type": "application/json"
32816
34858
  },
32817
34859
  body: JSON.stringify({
32818
34860
  name,
@@ -32868,16 +34910,16 @@ async function resolveAuthAndOrg(store) {
32868
34910
  }
32869
34911
  } else logger.log("auth", "already authenticated from config");
32870
34912
  logger.log("auth", "fetching organizations");
32871
- const orgs = await listOrganizations(config.apiUrl, config.apiKey);
34913
+ const orgs = await listOrganizations();
32872
34914
  logger.log("auth", "organizations loaded", { count: orgs.length });
32873
34915
  let chosen;
32874
34916
  if (orgs.length === 0) {
32875
34917
  store.setAuthPhase("org-create");
32876
- chosen = await runOrgCreationFlow(store, config.apiUrl, config.apiKey);
34918
+ chosen = await runOrgCreationFlow(store, config.apiUrl);
32877
34919
  } else {
32878
34920
  store.setAvailableOrgs(orgs);
32879
34921
  await store.getGate("org-select", (s) => s.selectedOrgId !== null || s.authPhase === "org-create");
32880
- if (store.session.authPhase === "org-create") chosen = await runOrgCreationFlow(store, config.apiUrl, config.apiKey);
34922
+ if (store.session.authPhase === "org-create") chosen = await runOrgCreationFlow(store, config.apiUrl);
32881
34923
  else {
32882
34924
  const selectedOrgId = store.session.selectedOrgId;
32883
34925
  const match$3 = orgs.find((o) => o.id === selectedOrgId);
@@ -32898,7 +34940,7 @@ async function resolveAuthAndOrg(store) {
32898
34940
  * Drive the org-create screen: wait for the user to submit a name, try to
32899
34941
  * create it, and retry on failure without leaving the wizard.
32900
34942
  */
32901
- async function runOrgCreationFlow(store, apiUrl, apiKey) {
34943
+ async function runOrgCreationFlow(store, apiUrl) {
32902
34944
  store.requestOrgCreation();
32903
34945
  for (;;) {
32904
34946
  await store.getGate(`org-create-${Date.now()}-${Math.random()}`, (s) => s.newOrgName !== null && s.authPhase === "org-creating");
@@ -32908,7 +34950,7 @@ async function runOrgCreationFlow(store, apiUrl, apiKey) {
32908
34950
  continue;
32909
34951
  }
32910
34952
  try {
32911
- return await createOrganization(apiUrl, apiKey, name);
34953
+ return await createOrganization(apiUrl, name);
32912
34954
  } catch (error) {
32913
34955
  const message = error instanceof Error ? error.message : String(error);
32914
34956
  store.failOrgCreation(message);
@@ -32920,7 +34962,7 @@ async function runOrgCreationFlow(store, apiUrl, apiKey) {
32920
34962
  //#region src/index.ts
32921
34963
  if (process.argv.includes("--mcp")) process.env.VELOZ_MCP = "true";
32922
34964
  const cli = Cli.create("veloz", {
32923
- version: "0.0.0-beta.31",
34965
+ version: "0.0.0-beta.32",
32924
34966
  description: "CLI da plataforma Veloz — deploy rápido para o Brasil",
32925
34967
  env: z.object({ VELOZ_ENV: z.string().optional().describe("Ambiente alvo (ex: preview, staging)") }),
32926
34968
  mcp: { command: "npx -y onveloz --mcp" }
@@ -32937,6 +34979,26 @@ cli.use(async (c, next) => {
32937
34979
  if (error instanceof Error) {
32938
34980
  const orpcError = error;
32939
34981
  const orpcData = orpcError.data;
34982
+ if (orpcData?.code === "MULTIPLE_ORGS") {
34983
+ const msg = "Você pertence a múltiplos workspaces. Selecione um para continuar.";
34984
+ if (process.env.GITHUB_ACTIONS === "true") process.stdout.write(`::error::${msg}\n`);
34985
+ else if (process.stdout.isTTY) {
34986
+ console.error(`\n✗ ${msg}`);
34987
+ console.error(" Execute `veloz orgs list` para ver e `veloz orgs use <slug>` para escolher.");
34988
+ }
34989
+ return c.error({
34990
+ code: "MULTIPLE_ORGS",
34991
+ message: msg,
34992
+ exitCode: 1,
34993
+ cta: { commands: [{
34994
+ command: "orgs list",
34995
+ description: "Listar workspaces disponíveis"
34996
+ }, {
34997
+ command: "orgs use",
34998
+ description: "Selecionar workspace padrão"
34999
+ }] }
35000
+ });
35001
+ }
32940
35002
  if (orpcData?.code === "NO_ORGANIZATION" || orpcData?.code === "ACCESS_NOT_APPROVED") {
32941
35003
  const msg = "Você precisa criar um workspace antes de continuar.";
32942
35004
  const hint = "Acesse app.onveloz.com/onboarding para criar seu workspace.";
@@ -32997,6 +35059,7 @@ cli.use(async (c, next) => {
32997
35059
  }
32998
35060
  });
32999
35061
  cli.command(projectsGroup);
35062
+ cli.command(orgsGroup);
33000
35063
  cli.command(envGroup);
33001
35064
  cli.command(domainsGroup);
33002
35065
  cli.command(volumesGroup);