onveloz 0.0.0-beta.31 → 0.0.0-beta.33

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 +2804 -652
  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.33";
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.33";
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.33";
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.33";
29541
30411
  const LOGO_LINES = [
29542
30412
  "██╗ ██╗███████╗██╗ ██████╗ ███████╗",
29543
30413
  "██║ ██║██╔════╝██║ ██╔═══██╗╚══███╔╝",
@@ -29726,6 +30596,7 @@ function createInitialSession() {
29726
30596
  selectedOrgId: null,
29727
30597
  newOrgName: null,
29728
30598
  orgCreateError: null,
30599
+ reauthNotice: null,
29729
30600
  detectedFramework: null,
29730
30601
  detectedFrameworkLabel: null,
29731
30602
  selectedFramework: null,
@@ -29749,10 +30620,22 @@ function createInitialSession() {
29749
30620
  learnCardBlockIdx: 0,
29750
30621
  learnCardComplete: false,
29751
30622
  agentHelperLines: [],
30623
+ agentHelperDiagram: null,
29752
30624
  agentPrompt: null,
29753
30625
  agentSummary: null,
29754
30626
  userNotes: null,
29755
30627
  notesConfirmed: false,
30628
+ githubSetupPhase: "idle",
30629
+ githubSetupError: null,
30630
+ githubInstallUrl: null,
30631
+ githubRepoOwner: null,
30632
+ githubRepoName: null,
30633
+ aiSetupPhase: "idle",
30634
+ aiSetupError: null,
30635
+ aiSetupMcpStatus: "pending",
30636
+ aiSetupSkillsStatus: "pending",
30637
+ aiSetupNeedsMcp: false,
30638
+ aiSetupNeedsSkills: false,
29756
30639
  introConfirmed: false,
29757
30640
  outroDismissed: false,
29758
30641
  agentStartedAt: null
@@ -29847,29 +30730,45 @@ function AuthScreen({ store }) {
29847
30730
  })]
29848
30731
  })] });
29849
30732
  }
29850
- if (session.authPhase === "browser" || session.authPhase === "polling") return /* @__PURE__ */ jsxs(ScreenLayout, { children: [/* @__PURE__ */ jsx(Spinner, { label: COPY.authPolling }), session.authDeviceCode ? /* @__PURE__ */ jsxs(Box, {
29851
- marginTop: 1,
29852
- flexDirection: "column",
29853
- gap: 1,
29854
- children: [/* @__PURE__ */ jsxs(Box, {
29855
- gap: 1,
29856
- children: [/* @__PURE__ */ jsx(Text, { children: COPY.authDeviceCode }), /* @__PURE__ */ jsx(Text, {
29857
- bold: true,
29858
- color: BRAND_COLOR,
29859
- children: session.authDeviceCode
29860
- })]
29861
- }), session.authVerificationUrl ? /* @__PURE__ */ jsxs(Box, {
30733
+ if (session.authPhase === "browser" || session.authPhase === "polling") return /* @__PURE__ */ jsxs(ScreenLayout, { children: [
30734
+ session.reauthNotice ? /* @__PURE__ */ jsx(Box, {
30735
+ marginBottom: 1,
30736
+ children: /* @__PURE__ */ jsxs(Text, {
30737
+ color: "yellow",
30738
+ children: ["⚠ ", session.reauthNotice]
30739
+ })
30740
+ }) : null,
30741
+ /* @__PURE__ */ jsx(Spinner, { label: COPY.authPolling }),
30742
+ session.authDeviceCode ? /* @__PURE__ */ jsxs(Box, {
30743
+ marginTop: 1,
29862
30744
  flexDirection: "column",
29863
- children: [/* @__PURE__ */ jsx(Text, {
29864
- dimColor: true,
29865
- children: COPY.authBrowserFallback
29866
- }), /* @__PURE__ */ jsx(Text, {
29867
- dimColor: true,
29868
- children: session.authVerificationUrl
29869
- })]
29870
- }) : null]
29871
- }) : null] });
29872
- return /* @__PURE__ */ jsx(ScreenLayout, { children: /* @__PURE__ */ jsx(Spinner, { label: COPY.authChecking }) });
30745
+ gap: 1,
30746
+ children: [/* @__PURE__ */ jsxs(Box, {
30747
+ gap: 1,
30748
+ children: [/* @__PURE__ */ jsx(Text, { children: COPY.authDeviceCode }), /* @__PURE__ */ jsx(Text, {
30749
+ bold: true,
30750
+ color: BRAND_COLOR,
30751
+ children: session.authDeviceCode
30752
+ })]
30753
+ }), session.authVerificationUrl ? /* @__PURE__ */ jsxs(Box, {
30754
+ flexDirection: "column",
30755
+ children: [/* @__PURE__ */ jsx(Text, {
30756
+ dimColor: true,
30757
+ children: COPY.authBrowserFallback
30758
+ }), /* @__PURE__ */ jsx(Text, {
30759
+ dimColor: true,
30760
+ children: session.authVerificationUrl
30761
+ })]
30762
+ }) : null]
30763
+ }) : null
30764
+ ] });
30765
+ return /* @__PURE__ */ jsxs(ScreenLayout, { children: [session.reauthNotice ? /* @__PURE__ */ jsx(Box, {
30766
+ marginBottom: 1,
30767
+ children: /* @__PURE__ */ jsxs(Text, {
30768
+ color: "yellow",
30769
+ children: ["⚠ ", session.reauthNotice]
30770
+ })
30771
+ }) : null, /* @__PURE__ */ jsx(Spinner, { label: COPY.authChecking })] });
29873
30772
  }
29874
30773
  function OrgCreateForm({ store }) {
29875
30774
  const [value, setValue] = useState("");
@@ -30977,6 +31876,165 @@ function TipsCard() {
30977
31876
  });
30978
31877
  }
30979
31878
 
31879
+ //#endregion
31880
+ //#region src/wizard/ui/primitives/DiagramPane.tsx
31881
+ const HORIZONTAL_ARROW = " ─▶ ";
31882
+ const VERTICAL_ARROW = "▼";
31883
+ const MIN_NODE_INNER = 10;
31884
+ const MAX_NODE_INNER = 28;
31885
+ function DiagramPane({ diagram, width }) {
31886
+ const { title, pipeline, services, notes } = diagram;
31887
+ const hasPipeline = pipeline.length > 0;
31888
+ const hasServices = services.length > 0;
31889
+ const hasNotes = notes.length > 0;
31890
+ const orientation = pickOrientation(pipeline, width);
31891
+ const nodeInner = pickNodeInner(pipeline, width, orientation);
31892
+ return /* @__PURE__ */ jsxs(Box, {
31893
+ flexDirection: "column",
31894
+ paddingX: 1,
31895
+ children: [
31896
+ title ? /* @__PURE__ */ jsx(Box, {
31897
+ marginBottom: 1,
31898
+ children: /* @__PURE__ */ jsx(Text, {
31899
+ bold: true,
31900
+ color: ACCENT_COLOR,
31901
+ wrap: "truncate",
31902
+ children: title
31903
+ })
31904
+ }) : null,
31905
+ hasPipeline ? orientation === "horizontal" ? /* @__PURE__ */ jsx(HorizontalPipeline, {
31906
+ nodes: pipeline,
31907
+ nodeInner
31908
+ }) : /* @__PURE__ */ jsx(VerticalPipeline, {
31909
+ nodes: pipeline,
31910
+ nodeInner
31911
+ }) : null,
31912
+ hasServices ? /* @__PURE__ */ jsxs(Box, {
31913
+ marginTop: hasPipeline ? 1 : 0,
31914
+ flexDirection: "column",
31915
+ children: [/* @__PURE__ */ jsx(Text, {
31916
+ dimColor: true,
31917
+ children: "Serviços auxiliares"
31918
+ }), /* @__PURE__ */ jsx(Box, {
31919
+ marginTop: 1,
31920
+ flexDirection: "row",
31921
+ flexWrap: "wrap",
31922
+ gap: 1,
31923
+ children: services.map((label, i) => /* @__PURE__ */ jsx(ServiceBox, {
31924
+ label,
31925
+ inner: nodeInner
31926
+ }, `${i}-${label}`))
31927
+ })]
31928
+ }) : null,
31929
+ hasNotes ? /* @__PURE__ */ jsx(Box, {
31930
+ marginTop: hasPipeline || hasServices ? 1 : 0,
31931
+ flexDirection: "column",
31932
+ children: notes.map((note, i) => /* @__PURE__ */ jsxs(Text, {
31933
+ color: DIM_COLOR,
31934
+ wrap: "truncate",
31935
+ children: ["· ", note]
31936
+ }, i))
31937
+ }) : null
31938
+ ]
31939
+ });
31940
+ }
31941
+ function HorizontalPipeline({ nodes, nodeInner }) {
31942
+ return /* @__PURE__ */ jsx(Box, {
31943
+ flexDirection: "row",
31944
+ flexWrap: "nowrap",
31945
+ children: nodes.map((label, i) => /* @__PURE__ */ jsxs(Box, {
31946
+ flexDirection: "row",
31947
+ flexShrink: 0,
31948
+ children: [/* @__PURE__ */ jsx(PipelineNode, {
31949
+ label,
31950
+ inner: nodeInner,
31951
+ isLast: i === nodes.length - 1
31952
+ }), i < nodes.length - 1 ? /* @__PURE__ */ jsx(Box, {
31953
+ alignItems: "center",
31954
+ paddingX: 0,
31955
+ flexShrink: 0,
31956
+ children: /* @__PURE__ */ jsx(Text, {
31957
+ color: BRAND_COLOR,
31958
+ children: HORIZONTAL_ARROW
31959
+ })
31960
+ }) : null]
31961
+ }, `${i}-${label}`))
31962
+ });
31963
+ }
31964
+ function VerticalPipeline({ nodes, nodeInner }) {
31965
+ return /* @__PURE__ */ jsx(Box, {
31966
+ flexDirection: "column",
31967
+ children: nodes.map((label, i) => /* @__PURE__ */ jsxs(Box, {
31968
+ flexDirection: "column",
31969
+ children: [/* @__PURE__ */ jsx(PipelineNode, {
31970
+ label,
31971
+ inner: nodeInner,
31972
+ isLast: i === nodes.length - 1
31973
+ }), i < nodes.length - 1 ? /* @__PURE__ */ jsx(Box, {
31974
+ paddingLeft: Math.max(1, Math.floor(nodeInner / 2)),
31975
+ children: /* @__PURE__ */ jsx(Text, {
31976
+ color: BRAND_COLOR,
31977
+ children: VERTICAL_ARROW
31978
+ })
31979
+ }) : null]
31980
+ }, `${i}-${label}`))
31981
+ });
31982
+ }
31983
+ function PipelineNode({ label, inner, isLast }) {
31984
+ const displayed = truncate(label, inner);
31985
+ return /* @__PURE__ */ jsx(Box, {
31986
+ borderStyle: "round",
31987
+ borderColor: isLast ? BRAND_COLOR : ACCENT_COLOR,
31988
+ paddingX: 1,
31989
+ flexShrink: 0,
31990
+ children: /* @__PURE__ */ jsx(Text, {
31991
+ color: isLast ? BRAND_COLOR : void 0,
31992
+ bold: isLast,
31993
+ children: displayed
31994
+ })
31995
+ });
31996
+ }
31997
+ function ServiceBox({ label, inner }) {
31998
+ return /* @__PURE__ */ jsx(Box, {
31999
+ borderStyle: "single",
32000
+ borderColor: DIM_COLOR,
32001
+ paddingX: 1,
32002
+ flexShrink: 0,
32003
+ children: /* @__PURE__ */ jsx(Text, {
32004
+ dimColor: true,
32005
+ children: truncate(label, inner)
32006
+ })
32007
+ });
32008
+ }
32009
+ /**
32010
+ * Decide horizontal vs vertical pipeline. Horizontal fits when the sum of all
32011
+ * node widths plus arrows is ≤ the width budget. Falls back to vertical if any
32012
+ * single node would be truncated below a useful minimum.
32013
+ */
32014
+ function pickOrientation(nodes, width) {
32015
+ if (nodes.length <= 1) return "horizontal";
32016
+ const arrows = 4 * (nodes.length - 1);
32017
+ const boxChrome = 4 * nodes.length;
32018
+ return nodes.reduce((sum, n) => sum + Math.min(n.length, MAX_NODE_INNER), 0) + boxChrome + arrows <= Math.max(0, width - 2) ? "horizontal" : "vertical";
32019
+ }
32020
+ function pickNodeInner(nodes, width, orientation) {
32021
+ if (nodes.length === 0) return MIN_NODE_INNER;
32022
+ const longest = nodes.reduce((max, n) => Math.max(max, n.length), 0);
32023
+ if (orientation === "vertical") {
32024
+ const budget$1 = Math.max(MIN_NODE_INNER, width - 6);
32025
+ return Math.min(MAX_NODE_INNER, Math.max(MIN_NODE_INNER, Math.min(longest, budget$1)));
32026
+ }
32027
+ const arrows = 4 * (nodes.length - 1);
32028
+ const perBox = Math.floor((Math.max(0, width - 2) - arrows) / nodes.length) - 4;
32029
+ const budget = Math.max(MIN_NODE_INNER, perBox);
32030
+ return Math.min(MAX_NODE_INNER, Math.max(MIN_NODE_INNER, Math.min(longest, budget)));
32031
+ }
32032
+ function truncate(label, maxInner) {
32033
+ if (label.length <= maxInner) return label;
32034
+ if (maxInner <= 1) return "…";
32035
+ return label.slice(0, maxInner - 1) + "…";
32036
+ }
32037
+
30980
32038
  //#endregion
30981
32039
  //#region src/wizard/ui/screens/RunScreen.tsx
30982
32040
  /**
@@ -30999,12 +32057,23 @@ function RunScreen({ store }) {
30999
32057
  store,
31000
32058
  onComplete: () => store.setLearnCardComplete()
31001
32059
  });
31002
- const hasAgentHelper = session.agentHelperLines.length > 0;
32060
+ const diagram = session.agentHelperDiagram;
32061
+ const hasDiagram = diagram !== null;
32062
+ const hasAgentHelperLines = session.agentHelperLines.length > 0;
32063
+ const diagramWidth = Math.max(20, Math.floor(columns / 2) - 6);
31003
32064
  const rightPane = /* @__PURE__ */ jsxs(Box, {
31004
32065
  flexDirection: "column",
31005
32066
  flexGrow: 1,
31006
32067
  overflow: "hidden",
31007
- children: [/* @__PURE__ */ jsx(TasksPane, { store }), hasAgentHelper ? /* @__PURE__ */ jsx(Box, {
32068
+ children: [/* @__PURE__ */ jsx(TasksPane, { store }), hasDiagram ? /* @__PURE__ */ jsx(Box, {
32069
+ marginTop: 1,
32070
+ flexGrow: 1,
32071
+ overflow: "hidden",
32072
+ children: /* @__PURE__ */ jsx(DiagramPane, {
32073
+ diagram,
32074
+ width: diagramWidth
32075
+ })
32076
+ }) : hasAgentHelperLines ? /* @__PURE__ */ jsx(Box, {
31008
32077
  marginTop: 1,
31009
32078
  flexGrow: 1,
31010
32079
  overflow: "hidden",
@@ -31452,6 +32521,397 @@ function AgentHelperPane({ lines }) {
31452
32521
  });
31453
32522
  }
31454
32523
 
32524
+ //#endregion
32525
+ //#region src/wizard/ui/screens/GithubSetupScreen.tsx
32526
+ /**
32527
+ * GithubSetupScreen — Post-deploy step asking whether to enable automatic
32528
+ * deploys via the Veloz GitHub App. The runner in `index.ts` drives the
32529
+ * phases; this screen only renders the current state.
32530
+ */
32531
+ function GithubSetupScreen({ store }) {
32532
+ useSyncExternalStore(store.subscribe, store.getSnapshot);
32533
+ const { session } = store;
32534
+ const repo = session.githubRepoOwner && session.githubRepoName ? `${session.githubRepoOwner}/${session.githubRepoName}` : "seu repositório";
32535
+ if (session.githubSetupPhase === "prompt") return /* @__PURE__ */ jsx(PromptView$1, {
32536
+ store,
32537
+ repo
32538
+ });
32539
+ if (session.githubSetupPhase === "connecting") return /* @__PURE__ */ jsxs(ScreenLayout, { children: [/* @__PURE__ */ jsx(Text, {
32540
+ color: BRAND_COLOR,
32541
+ bold: true,
32542
+ children: COPY.githubSetupTitle
32543
+ }), /* @__PURE__ */ jsx(Box, {
32544
+ marginTop: 1,
32545
+ children: /* @__PURE__ */ jsx(Spinner, { label: COPY.githubSetupConnecting })
32546
+ })] });
32547
+ if (session.githubSetupPhase === "installing") return /* @__PURE__ */ jsxs(ScreenLayout, {
32548
+ footer: COPY.githubSetupInstallHint,
32549
+ children: [
32550
+ /* @__PURE__ */ jsx(Text, {
32551
+ color: BRAND_COLOR,
32552
+ bold: true,
32553
+ children: COPY.githubSetupInstallTitle
32554
+ }),
32555
+ /* @__PURE__ */ jsx(Box, {
32556
+ marginTop: 1,
32557
+ children: /* @__PURE__ */ jsx(Spinner, { label: COPY.githubSetupPolling })
32558
+ }),
32559
+ session.githubInstallUrl ? /* @__PURE__ */ jsxs(Box, {
32560
+ marginTop: 1,
32561
+ flexDirection: "column",
32562
+ children: [/* @__PURE__ */ jsx(Text, {
32563
+ dimColor: true,
32564
+ children: COPY.githubSetupInstallFallback
32565
+ }), /* @__PURE__ */ jsx(Text, {
32566
+ bold: true,
32567
+ children: session.githubInstallUrl
32568
+ })]
32569
+ }) : null
32570
+ ]
32571
+ });
32572
+ if (session.githubSetupPhase === "done") return /* @__PURE__ */ jsxs(ScreenLayout, { children: [/* @__PURE__ */ jsxs(Box, {
32573
+ gap: 1,
32574
+ children: [/* @__PURE__ */ jsx(Text, {
32575
+ color: SUCCESS_COLOR,
32576
+ children: Icons.completed
32577
+ }), /* @__PURE__ */ jsx(Text, {
32578
+ color: SUCCESS_COLOR,
32579
+ bold: true,
32580
+ children: COPY.githubSetupDone
32581
+ })]
32582
+ }), /* @__PURE__ */ jsx(Box, {
32583
+ marginTop: 1,
32584
+ children: /* @__PURE__ */ jsx(Text, {
32585
+ dimColor: true,
32586
+ children: COPY.githubSetupDoneHint
32587
+ })
32588
+ })] });
32589
+ if (session.githubSetupPhase === "error") return /* @__PURE__ */ jsxs(ScreenLayout, { children: [
32590
+ /* @__PURE__ */ jsxs(Box, {
32591
+ gap: 1,
32592
+ children: [/* @__PURE__ */ jsx(Text, {
32593
+ color: ERROR_COLOR,
32594
+ children: Icons.error
32595
+ }), /* @__PURE__ */ jsx(Text, {
32596
+ color: ERROR_COLOR,
32597
+ bold: true,
32598
+ children: COPY.githubSetupErrorTitle
32599
+ })]
32600
+ }),
32601
+ session.githubSetupError ? /* @__PURE__ */ jsx(Box, {
32602
+ marginTop: 1,
32603
+ marginLeft: 2,
32604
+ children: /* @__PURE__ */ jsx(Text, {
32605
+ color: ERROR_COLOR,
32606
+ wrap: "wrap",
32607
+ children: session.githubSetupError
32608
+ })
32609
+ }) : null,
32610
+ /* @__PURE__ */ jsx(Box, {
32611
+ marginTop: 1,
32612
+ children: /* @__PURE__ */ jsx(Text, {
32613
+ dimColor: true,
32614
+ children: COPY.githubSetupErrorHint
32615
+ })
32616
+ })
32617
+ ] });
32618
+ return /* @__PURE__ */ jsx(ScreenLayout, { children: /* @__PURE__ */ jsx(Text, {
32619
+ dimColor: true,
32620
+ children: COPY.githubSetupSkipped
32621
+ }) });
32622
+ }
32623
+ function PromptView$1({ store, repo }) {
32624
+ const [selected, setSelected] = React.useState(0);
32625
+ useGatedInput((_input, key) => {
32626
+ if (key.upArrow || key.downArrow) {
32627
+ setSelected((s) => s === 0 ? 1 : 0);
32628
+ return;
32629
+ }
32630
+ if (key.return) store.setGithubSetupPhase(selected === 0 ? "connecting" : "skipped");
32631
+ });
32632
+ const options = [COPY.githubSetupYes, COPY.githubSetupNo];
32633
+ return /* @__PURE__ */ jsxs(ScreenLayout, {
32634
+ footer: "↑↓ escolher · Enter confirmar",
32635
+ children: [
32636
+ /* @__PURE__ */ jsx(Text, {
32637
+ color: BRAND_COLOR,
32638
+ bold: true,
32639
+ children: COPY.githubSetupTitle
32640
+ }),
32641
+ /* @__PURE__ */ jsx(Box, {
32642
+ marginTop: 1,
32643
+ children: /* @__PURE__ */ jsx(Text, {
32644
+ wrap: "wrap",
32645
+ children: COPY.githubSetupQuestion(repo)
32646
+ })
32647
+ }),
32648
+ /* @__PURE__ */ jsx(Box, {
32649
+ marginTop: 1,
32650
+ children: /* @__PURE__ */ jsx(Text, {
32651
+ color: DIM_COLOR,
32652
+ wrap: "wrap",
32653
+ children: COPY.githubSetupHint
32654
+ })
32655
+ }),
32656
+ /* @__PURE__ */ jsx(Box, {
32657
+ marginTop: 1,
32658
+ flexDirection: "column",
32659
+ children: options.map((opt, i) => /* @__PURE__ */ jsxs(Box, {
32660
+ gap: 1,
32661
+ marginLeft: 2,
32662
+ children: [/* @__PURE__ */ jsx(Text, {
32663
+ color: i === selected ? BRAND_COLOR : DIM_COLOR,
32664
+ children: i === selected ? Icons.triangleRight : " "
32665
+ }), /* @__PURE__ */ jsx(Text, {
32666
+ color: i === selected ? BRAND_COLOR : void 0,
32667
+ bold: i === selected,
32668
+ children: opt
32669
+ })]
32670
+ }, i))
32671
+ })
32672
+ ]
32673
+ });
32674
+ }
32675
+
32676
+ //#endregion
32677
+ //#region src/wizard/ui/screens/AiSetupScreen.tsx
32678
+ /**
32679
+ * AiSetupScreen — Post-deploy step that offers to install the Veloz MCP
32680
+ * server and the agent skills. The runner in `index.ts` detects what's
32681
+ * missing, drives the phases, and spawns the installation commands; this
32682
+ * screen renders the current state.
32683
+ */
32684
+ function AiSetupScreen({ store }) {
32685
+ useSyncExternalStore(store.subscribe, store.getSnapshot);
32686
+ const { session } = store;
32687
+ if (session.aiSetupPhase === "prompt") return /* @__PURE__ */ jsx(PromptView, { store });
32688
+ if (session.aiSetupPhase === "installing") return /* @__PURE__ */ jsxs(ScreenLayout, { children: [/* @__PURE__ */ jsx(Text, {
32689
+ color: BRAND_COLOR,
32690
+ bold: true,
32691
+ children: COPY.aiSetupInstallingTitle
32692
+ }), /* @__PURE__ */ jsxs(Box, {
32693
+ marginTop: 1,
32694
+ flexDirection: "column",
32695
+ children: [session.aiSetupNeedsMcp ? /* @__PURE__ */ jsx(ToolRow, {
32696
+ status: session.aiSetupMcpStatus,
32697
+ installingLabel: COPY.aiSetupMcpInstalling,
32698
+ doneLabel: COPY.aiSetupMcpDone,
32699
+ errorLabel: COPY.aiSetupMcpError,
32700
+ pendingLabel: COPY.aiSetupMcpLabel
32701
+ }) : null, session.aiSetupNeedsSkills ? /* @__PURE__ */ jsx(ToolRow, {
32702
+ status: session.aiSetupSkillsStatus,
32703
+ installingLabel: COPY.aiSetupSkillsInstalling,
32704
+ doneLabel: COPY.aiSetupSkillsDone,
32705
+ errorLabel: COPY.aiSetupSkillsError,
32706
+ pendingLabel: COPY.aiSetupSkillsLabel
32707
+ }) : null]
32708
+ })] });
32709
+ if (session.aiSetupPhase === "done") return /* @__PURE__ */ jsxs(ScreenLayout, { children: [
32710
+ /* @__PURE__ */ jsxs(Box, {
32711
+ gap: 1,
32712
+ children: [/* @__PURE__ */ jsx(Text, {
32713
+ color: SUCCESS_COLOR,
32714
+ children: Icons.completed
32715
+ }), /* @__PURE__ */ jsx(Text, {
32716
+ color: SUCCESS_COLOR,
32717
+ bold: true,
32718
+ children: COPY.aiSetupDoneTitle
32719
+ })]
32720
+ }),
32721
+ /* @__PURE__ */ jsxs(Box, {
32722
+ marginTop: 1,
32723
+ flexDirection: "column",
32724
+ children: [session.aiSetupNeedsMcp ? /* @__PURE__ */ jsx(ToolRow, {
32725
+ status: session.aiSetupMcpStatus,
32726
+ installingLabel: COPY.aiSetupMcpInstalling,
32727
+ doneLabel: COPY.aiSetupMcpDone,
32728
+ errorLabel: COPY.aiSetupMcpError,
32729
+ pendingLabel: COPY.aiSetupMcpLabel
32730
+ }) : null, session.aiSetupNeedsSkills ? /* @__PURE__ */ jsx(ToolRow, {
32731
+ status: session.aiSetupSkillsStatus,
32732
+ installingLabel: COPY.aiSetupSkillsInstalling,
32733
+ doneLabel: COPY.aiSetupSkillsDone,
32734
+ errorLabel: COPY.aiSetupSkillsError,
32735
+ pendingLabel: COPY.aiSetupSkillsLabel
32736
+ }) : null]
32737
+ }),
32738
+ /* @__PURE__ */ jsx(Box, {
32739
+ marginTop: 1,
32740
+ children: /* @__PURE__ */ jsx(Text, {
32741
+ dimColor: true,
32742
+ children: COPY.aiSetupDoneHint
32743
+ })
32744
+ })
32745
+ ] });
32746
+ if (session.aiSetupPhase === "error") return /* @__PURE__ */ jsxs(ScreenLayout, { children: [
32747
+ /* @__PURE__ */ jsxs(Box, {
32748
+ gap: 1,
32749
+ children: [/* @__PURE__ */ jsx(Text, {
32750
+ color: ERROR_COLOR,
32751
+ children: Icons.error
32752
+ }), /* @__PURE__ */ jsx(Text, {
32753
+ color: ERROR_COLOR,
32754
+ bold: true,
32755
+ children: COPY.aiSetupErrorTitle
32756
+ })]
32757
+ }),
32758
+ session.aiSetupError ? /* @__PURE__ */ jsx(Box, {
32759
+ marginTop: 1,
32760
+ marginLeft: 2,
32761
+ children: /* @__PURE__ */ jsx(Text, {
32762
+ color: ERROR_COLOR,
32763
+ wrap: "wrap",
32764
+ children: session.aiSetupError
32765
+ })
32766
+ }) : null,
32767
+ /* @__PURE__ */ jsx(Box, {
32768
+ marginTop: 1,
32769
+ children: /* @__PURE__ */ jsx(Text, {
32770
+ dimColor: true,
32771
+ children: COPY.aiSetupErrorHint
32772
+ })
32773
+ })
32774
+ ] });
32775
+ return /* @__PURE__ */ jsx(ScreenLayout, { children: null });
32776
+ }
32777
+ function ToolRow({ status, installingLabel, doneLabel, errorLabel, pendingLabel }) {
32778
+ if (status === "installing") return /* @__PURE__ */ jsx(Box, {
32779
+ marginLeft: 2,
32780
+ children: /* @__PURE__ */ jsx(Spinner, { label: installingLabel })
32781
+ });
32782
+ if (status === "done") return /* @__PURE__ */ jsxs(Box, {
32783
+ gap: 1,
32784
+ marginLeft: 2,
32785
+ children: [/* @__PURE__ */ jsx(Text, {
32786
+ color: SUCCESS_COLOR,
32787
+ children: Icons.completed
32788
+ }), /* @__PURE__ */ jsx(Text, { children: doneLabel })]
32789
+ });
32790
+ if (status === "error") return /* @__PURE__ */ jsxs(Box, {
32791
+ gap: 1,
32792
+ marginLeft: 2,
32793
+ children: [/* @__PURE__ */ jsx(Text, {
32794
+ color: ERROR_COLOR,
32795
+ children: Icons.error
32796
+ }), /* @__PURE__ */ jsx(Text, {
32797
+ color: ERROR_COLOR,
32798
+ children: errorLabel
32799
+ })]
32800
+ });
32801
+ if (status === "skipped") return /* @__PURE__ */ jsxs(Box, {
32802
+ gap: 1,
32803
+ marginLeft: 2,
32804
+ children: [/* @__PURE__ */ jsx(Text, {
32805
+ color: DIM_COLOR,
32806
+ children: "-"
32807
+ }), /* @__PURE__ */ jsxs(Text, {
32808
+ dimColor: true,
32809
+ children: [pendingLabel, " (já instalado)"]
32810
+ })]
32811
+ });
32812
+ return /* @__PURE__ */ jsxs(Box, {
32813
+ gap: 1,
32814
+ marginLeft: 2,
32815
+ children: [/* @__PURE__ */ jsx(Text, {
32816
+ color: DIM_COLOR,
32817
+ children: Icons.pending
32818
+ }), /* @__PURE__ */ jsx(Text, {
32819
+ dimColor: true,
32820
+ children: pendingLabel
32821
+ })]
32822
+ });
32823
+ }
32824
+ function PromptView({ store }) {
32825
+ const [selected, setSelected] = React.useState(0);
32826
+ const { session } = store;
32827
+ useGatedInput((_input, key) => {
32828
+ if (key.upArrow || key.downArrow) {
32829
+ setSelected((s) => s === 0 ? 1 : 0);
32830
+ return;
32831
+ }
32832
+ if (key.return) store.setAiSetupPhase(selected === 0 ? "installing" : "skipped");
32833
+ });
32834
+ const options = [COPY.aiSetupYes, COPY.aiSetupNo];
32835
+ return /* @__PURE__ */ jsxs(ScreenLayout, {
32836
+ footer: "↑↓ escolher · Enter confirmar",
32837
+ children: [
32838
+ /* @__PURE__ */ jsx(Text, {
32839
+ color: BRAND_COLOR,
32840
+ bold: true,
32841
+ children: COPY.aiSetupTitle
32842
+ }),
32843
+ /* @__PURE__ */ jsx(Box, {
32844
+ marginTop: 1,
32845
+ children: /* @__PURE__ */ jsx(Text, {
32846
+ wrap: "wrap",
32847
+ children: COPY.aiSetupQuestion
32848
+ })
32849
+ }),
32850
+ /* @__PURE__ */ jsxs(Box, {
32851
+ marginTop: 1,
32852
+ flexDirection: "column",
32853
+ marginLeft: 2,
32854
+ children: [session.aiSetupNeedsMcp ? /* @__PURE__ */ jsxs(Box, {
32855
+ flexDirection: "column",
32856
+ marginBottom: 1,
32857
+ children: [/* @__PURE__ */ jsxs(Box, {
32858
+ gap: 1,
32859
+ children: [/* @__PURE__ */ jsx(Text, {
32860
+ color: BRAND_COLOR,
32861
+ children: Icons.bullet
32862
+ }), /* @__PURE__ */ jsx(Text, {
32863
+ bold: true,
32864
+ children: COPY.aiSetupMcpLabel
32865
+ })]
32866
+ }), /* @__PURE__ */ jsx(Box, {
32867
+ marginLeft: 2,
32868
+ children: /* @__PURE__ */ jsx(Text, {
32869
+ color: DIM_COLOR,
32870
+ wrap: "wrap",
32871
+ children: COPY.aiSetupMcpDescription
32872
+ })
32873
+ })]
32874
+ }) : null, session.aiSetupNeedsSkills ? /* @__PURE__ */ jsxs(Box, {
32875
+ flexDirection: "column",
32876
+ children: [/* @__PURE__ */ jsxs(Box, {
32877
+ gap: 1,
32878
+ children: [/* @__PURE__ */ jsx(Text, {
32879
+ color: BRAND_COLOR,
32880
+ children: Icons.bullet
32881
+ }), /* @__PURE__ */ jsx(Text, {
32882
+ bold: true,
32883
+ children: COPY.aiSetupSkillsLabel
32884
+ })]
32885
+ }), /* @__PURE__ */ jsx(Box, {
32886
+ marginLeft: 2,
32887
+ children: /* @__PURE__ */ jsx(Text, {
32888
+ color: DIM_COLOR,
32889
+ wrap: "wrap",
32890
+ children: COPY.aiSetupSkillsDescription
32891
+ })
32892
+ })]
32893
+ }) : null]
32894
+ }),
32895
+ /* @__PURE__ */ jsx(Box, {
32896
+ marginTop: 1,
32897
+ flexDirection: "column",
32898
+ children: options.map((opt, i) => /* @__PURE__ */ jsxs(Box, {
32899
+ gap: 1,
32900
+ marginLeft: 2,
32901
+ children: [/* @__PURE__ */ jsx(Text, {
32902
+ color: i === selected ? BRAND_COLOR : DIM_COLOR,
32903
+ children: i === selected ? Icons.triangleRight : " "
32904
+ }), /* @__PURE__ */ jsx(Text, {
32905
+ color: i === selected ? BRAND_COLOR : void 0,
32906
+ bold: i === selected,
32907
+ children: opt
32908
+ })]
32909
+ }, i))
32910
+ })
32911
+ ]
32912
+ });
32913
+ }
32914
+
31455
32915
  //#endregion
31456
32916
  //#region src/wizard/ui/primitives/MarkdownText.tsx
31457
32917
  /** Parse inline markdown (**bold** and `code`) into Ink <Text> elements. */
@@ -31517,203 +32977,327 @@ function MarkdownText({ children }) {
31517
32977
  //#endregion
31518
32978
  //#region src/wizard/ui/screens/OutroScreen.tsx
31519
32979
  /**
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.
32980
+ * OutroScreen — Final screen after the agent completes or fails.
32981
+ *
32982
+ * Scrollable content (↑/↓) with an action bar at the bottom:
32983
+ * [ Abrir projeto ] [ Abrir dashboard ] [ Fechar ]
32984
+ * Left/right to move between actions, Enter to confirm.
31523
32985
  */
32986
+ const DASHBOARD_BASE = process.env.VELOZ_WEB_URL || "https://app.onveloz.com";
31524
32987
  function OutroScreen({ store }) {
31525
32988
  useSyncExternalStore(store.subscribe, store.getSnapshot);
31526
32989
  const { session } = store;
31527
32990
  const isSuccess$4 = !session.agentError;
31528
- const [columns] = useStdoutDimensions();
32991
+ const [columns, rows] = useStdoutDimensions();
31529
32992
  const sepWidth = Math.min(columns - 8, 80);
31530
32993
  const aiSetup = useMemo(() => checkAiSetup(), []);
32994
+ const projectConfig = useMemo(() => loadConfig$1(), []);
32995
+ const dashboardUrl = useMemo(() => {
32996
+ const projectId = projectConfig?.project?.id;
32997
+ return projectId ? `${DASHBOARD_BASE}/projetos/${projectId}` : DASHBOARD_BASE;
32998
+ }, [projectConfig]);
32999
+ const actions = useMemo(() => {
33000
+ if (!isSuccess$4) return [{
33001
+ id: "close",
33002
+ label: "Fechar"
33003
+ }];
33004
+ const list = [];
33005
+ if (session.deployUrl) list.push({
33006
+ id: "open-project",
33007
+ label: "Abrir projeto"
33008
+ });
33009
+ list.push({
33010
+ id: "open-dashboard",
33011
+ label: "Abrir dashboard"
33012
+ });
33013
+ list.push({
33014
+ id: "close",
33015
+ label: "Fechar"
33016
+ });
33017
+ return list;
33018
+ }, [isSuccess$4, session.deployUrl]);
33019
+ const [actionIdx, setActionIdx] = useState(0);
33020
+ const [scrollOffset, setScrollOffset] = useState(0);
33021
+ const performExit = React.useCallback(async (action) => {
33022
+ try {
33023
+ if (action.id === "open-project" && session.deployUrl) await openBrowser(session.deployUrl);
33024
+ else if (action.id === "open-dashboard") await openBrowser(dashboardUrl);
33025
+ } catch {}
33026
+ store.dismissOutro();
33027
+ process.exit(isSuccess$4 ? 0 : 1);
33028
+ }, [
33029
+ dashboardUrl,
33030
+ isSuccess$4,
33031
+ session.deployUrl,
33032
+ store
33033
+ ]);
31531
33034
  useGatedInput((_input, key) => {
31532
- if (key.return || key.escape) {
33035
+ if (key.upArrow) {
33036
+ setScrollOffset((s) => Math.max(0, s - 1));
33037
+ return;
33038
+ }
33039
+ if (key.downArrow) {
33040
+ setScrollOffset((s) => s + 1);
33041
+ return;
33042
+ }
33043
+ if (key.leftArrow) {
33044
+ setActionIdx((i) => i > 0 ? i - 1 : actions.length - 1);
33045
+ return;
33046
+ }
33047
+ if (key.rightArrow) {
33048
+ setActionIdx((i) => i < actions.length - 1 ? i + 1 : 0);
33049
+ return;
33050
+ }
33051
+ if (key.return) {
33052
+ const action = actions[actionIdx] ?? actions[0];
33053
+ if (action) performExit(action);
33054
+ return;
33055
+ }
33056
+ if (key.escape) {
31533
33057
  store.dismissOutro();
31534
33058
  process.exit(isSuccess$4 ? 0 : 1);
31535
33059
  }
31536
33060
  });
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,
33061
+ const body = isSuccess$4 ? /* @__PURE__ */ jsx(SuccessBody, {
33062
+ store,
33063
+ sepWidth,
33064
+ aiSetup
33065
+ }) : /* @__PURE__ */ jsx(ErrorBody, {
33066
+ store,
33067
+ sepWidth
33068
+ });
33069
+ return /* @__PURE__ */ jsxs(ScreenLayout, {
33070
+ footer: "↑↓ rolar · ←→ escolher ação · Enter confirmar · Esc fechar",
33071
+ children: [/* @__PURE__ */ jsx(Box, {
33072
+ flexDirection: "column",
33073
+ flexGrow: 1,
33074
+ flexShrink: 1,
33075
+ overflow: "hidden",
33076
+ height: Math.max(6, rows - 14),
33077
+ children: /* @__PURE__ */ jsx(Box, {
31577
33078
  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);
33079
+ marginTop: -scrollOffset,
33080
+ children: body
33081
+ })
33082
+ }), /* @__PURE__ */ jsx(ActionBar, {
33083
+ actions,
33084
+ selected: actionIdx
33085
+ })]
33086
+ });
33087
+ }
33088
+ function ActionBar({ actions, selected }) {
33089
+ return /* @__PURE__ */ jsx(Box, {
33090
+ marginTop: 1,
33091
+ gap: 2,
33092
+ children: actions.map((action, i) => {
33093
+ const active$1 = i === selected;
33094
+ return /* @__PURE__ */ jsx(Box, {
33095
+ borderStyle: "single",
33096
+ borderColor: active$1 ? BRAND_COLOR : DIM_COLOR,
33097
+ paddingX: 1,
33098
+ children: /* @__PURE__ */ jsx(Text, {
33099
+ color: active$1 ? BRAND_COLOR : void 0,
33100
+ bold: active$1,
33101
+ children: action.label
31590
33102
  })
31591
- })] }) : null,
31592
- /* @__PURE__ */ jsx(Separator, { width: sepWidth }),
31593
- /* @__PURE__ */ jsxs(Box, {
31594
- marginTop: 1,
31595
- flexDirection: "column",
31596
- children: [/* @__PURE__ */ jsx(Text, {
33103
+ }, action.id);
33104
+ })
33105
+ });
33106
+ }
33107
+ function SuccessBody({ store, sepWidth, aiSetup }) {
33108
+ const { session } = store;
33109
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
33110
+ /* @__PURE__ */ jsxs(Box, {
33111
+ gap: 1,
33112
+ children: [/* @__PURE__ */ jsx(Text, {
33113
+ color: SUCCESS_COLOR,
33114
+ children: Icons.squareFilled
33115
+ }), /* @__PURE__ */ jsx(Text, {
33116
+ color: SUCCESS_COLOR,
33117
+ bold: true,
33118
+ children: COPY.outroSuccess
33119
+ })]
33120
+ }),
33121
+ session.deployUrl ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Separator, { width: sepWidth }), /* @__PURE__ */ jsxs(Box, {
33122
+ marginTop: 1,
33123
+ flexDirection: "column",
33124
+ children: [/* @__PURE__ */ jsx(Text, {
33125
+ dimColor: true,
33126
+ children: COPY.outroUrl
33127
+ }), /* @__PURE__ */ jsxs(Text, {
33128
+ color: BRAND_COLOR,
33129
+ bold: true,
33130
+ children: [" ", session.deployUrl]
33131
+ })]
33132
+ })] }) : null,
33133
+ session.githubSetupPhase === "done" ? /* @__PURE__ */ jsxs(Box, {
33134
+ marginTop: 1,
33135
+ gap: 1,
33136
+ children: [/* @__PURE__ */ jsx(Text, {
33137
+ color: SUCCESS_COLOR,
33138
+ children: Icons.completed
33139
+ }), /* @__PURE__ */ jsx(Text, { children: "Deploy automático ativado via GitHub." })]
33140
+ }) : null,
33141
+ session.agentSummary ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Separator, { width: sepWidth }), /* @__PURE__ */ jsxs(Box, {
33142
+ marginTop: 1,
33143
+ flexDirection: "column",
33144
+ children: [/* @__PURE__ */ jsx(Text, {
33145
+ bold: true,
33146
+ color: ACCENT_COLOR,
33147
+ children: "Resumo do deploy:"
33148
+ }), /* @__PURE__ */ jsx(Box, {
33149
+ marginLeft: 2,
33150
+ children: /* @__PURE__ */ jsx(MarkdownText, { children: session.agentSummary })
33151
+ })]
33152
+ })] }) : null,
33153
+ session.agentHelperDiagram ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Separator, { width: sepWidth }), /* @__PURE__ */ jsx(Box, {
33154
+ marginTop: 1,
33155
+ flexDirection: "column",
33156
+ children: /* @__PURE__ */ jsx(DiagramPane, {
33157
+ diagram: session.agentHelperDiagram,
33158
+ width: sepWidth
33159
+ })
33160
+ })] }) : session.agentHelperLines.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Separator, { width: sepWidth }), /* @__PURE__ */ jsx(Box, {
33161
+ marginTop: 1,
33162
+ flexDirection: "column",
33163
+ children: session.agentHelperLines.map((line, i) => {
33164
+ if (i === 0 && line.trim()) return /* @__PURE__ */ jsx(Text, {
31597
33165
  bold: true,
31598
- children: COPY.outroNextSteps
31599
- }), COPY.outroNextStepsList.map((step, i) => /* @__PURE__ */ jsxs(Text, {
33166
+ color: ACCENT_COLOR,
33167
+ wrap: "truncate",
33168
+ children: line
33169
+ }, i);
33170
+ return /* @__PURE__ */ jsx(Text, {
31600
33171
  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",
33172
+ wrap: "truncate",
33173
+ children: line
33174
+ }, i);
33175
+ })
33176
+ })] }) : null,
33177
+ /* @__PURE__ */ jsx(Separator, { width: sepWidth }),
33178
+ /* @__PURE__ */ jsxs(Box, {
33179
+ marginTop: 1,
33180
+ flexDirection: "column",
33181
+ children: [/* @__PURE__ */ jsx(Text, {
33182
+ bold: true,
33183
+ children: COPY.outroNextSteps
33184
+ }), COPY.outroNextStepsList.map((step, i) => /* @__PURE__ */ jsxs(Text, {
33185
+ color: DIM_COLOR,
31612
33186
  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
33187
+ " ",
33188
+ i + 1,
33189
+ ". ",
33190
+ step
31636
33191
  ]
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, {
33192
+ }, i))]
33193
+ }),
33194
+ !aiSetup.mcpInstalled || !aiSetup.skillsInstalled ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Separator, { width: sepWidth }), /* @__PURE__ */ jsxs(Box, {
33195
+ marginTop: 1,
33196
+ flexDirection: "column",
33197
+ children: [
33198
+ /* @__PURE__ */ jsx(Text, {
31676
33199
  bold: true,
31677
33200
  color: ACCENT_COLOR,
31678
- children: "Últimas ações do agente:"
31679
- }), recentOutput.map((line, i) => /* @__PURE__ */ jsxs(Text, {
33201
+ children: "Integração com IA:"
33202
+ }),
33203
+ !aiSetup.mcpInstalled ? /* @__PURE__ */ jsxs(Text, {
31680
33204
  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
33205
  children: [
31690
- "O agente tentou ",
31691
- session.retryCount + 1,
31692
- " vez",
31693
- session.retryCount > 0 ? "es" : "",
31694
- " antes de falhar."
33206
+ " ",
33207
+ Icons.bullet,
33208
+ " ",
33209
+ COPY.outroAiSetupMcp
31695
33210
  ]
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, {
33211
+ }) : null,
33212
+ !aiSetup.skillsInstalled ? /* @__PURE__ */ jsxs(Text, {
31706
33213
  color: DIM_COLOR,
31707
33214
  children: [
31708
33215
  " ",
31709
33216
  Icons.bullet,
31710
33217
  " ",
31711
- hint
33218
+ COPY.outroAiSetupSkills
31712
33219
  ]
31713
- }, i))]
33220
+ }) : null
33221
+ ]
33222
+ })] }) : null
33223
+ ] });
33224
+ }
33225
+ function ErrorBody({ store, sepWidth }) {
33226
+ const { session } = store;
33227
+ const recentOutput = session.agentOutputLines.slice(-8);
33228
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
33229
+ /* @__PURE__ */ jsxs(Box, {
33230
+ gap: 1,
33231
+ children: [/* @__PURE__ */ jsx(Text, {
33232
+ color: ERROR_COLOR,
33233
+ children: Icons.error
33234
+ }), /* @__PURE__ */ jsx(Text, {
33235
+ color: ERROR_COLOR,
33236
+ bold: true,
33237
+ children: COPY.outroError
33238
+ })]
33239
+ }),
33240
+ session.agentError ? /* @__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: ERROR_COLOR,
33246
+ children: "Motivo:"
33247
+ }), /* @__PURE__ */ jsx(Box, {
33248
+ marginLeft: 2,
33249
+ flexDirection: "column",
33250
+ children: /* @__PURE__ */ jsx(Text, {
33251
+ color: ERROR_COLOR,
33252
+ wrap: "wrap",
33253
+ children: session.agentError
33254
+ })
33255
+ })]
33256
+ })] }) : null,
33257
+ recentOutput.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Separator, { width: sepWidth }), /* @__PURE__ */ jsxs(Box, {
33258
+ marginTop: 1,
33259
+ flexDirection: "column",
33260
+ children: [/* @__PURE__ */ jsx(Text, {
33261
+ bold: true,
33262
+ color: ACCENT_COLOR,
33263
+ children: "Últimas ações do agente:"
33264
+ }), recentOutput.map((line, i) => /* @__PURE__ */ jsxs(Text, {
33265
+ color: DIM_COLOR,
33266
+ wrap: "wrap",
33267
+ children: [" ", line]
33268
+ }, i))]
33269
+ })] }) : null,
33270
+ session.retryCount > 0 ? /* @__PURE__ */ jsx(Box, {
33271
+ marginTop: 1,
33272
+ children: /* @__PURE__ */ jsxs(Text, {
33273
+ dimColor: true,
33274
+ children: [
33275
+ "O agente tentou ",
33276
+ session.retryCount + 1,
33277
+ " vez",
33278
+ session.retryCount > 0 ? "es" : "",
33279
+ " antes de falhar."
33280
+ ]
31714
33281
  })
31715
- ]
31716
- });
33282
+ }) : null,
33283
+ /* @__PURE__ */ jsx(Separator, { width: sepWidth }),
33284
+ /* @__PURE__ */ jsxs(Box, {
33285
+ marginTop: 1,
33286
+ flexDirection: "column",
33287
+ children: [/* @__PURE__ */ jsx(Text, {
33288
+ bold: true,
33289
+ children: "Próximos passos:"
33290
+ }), COPY.outroErrorHints.map((hint, i) => /* @__PURE__ */ jsxs(Text, {
33291
+ color: DIM_COLOR,
33292
+ children: [
33293
+ " ",
33294
+ Icons.bullet,
33295
+ " ",
33296
+ hint
33297
+ ]
33298
+ }, i))]
33299
+ })
33300
+ ] });
31717
33301
  }
31718
33302
  function Separator({ width }) {
31719
33303
  return /* @__PURE__ */ jsx(Box, {
@@ -31760,25 +33344,58 @@ function ErrorOverlay({ message, onDismiss }) {
31760
33344
 
31761
33345
  //#endregion
31762
33346
  //#region src/wizard/ui/overlays/ConfirmExitOverlay.tsx
33347
+ /**
33348
+ * ConfirmExitOverlay — Alert-style confirmation for Ctrl+C.
33349
+ *
33350
+ * Rendered in the absolute-positioned overlay slot in App.tsx, just like
33351
+ * PromptOverlay: pulsing border, own `useInput` handler, screen remains
33352
+ * visible behind it while `InputFocusProvider` gates the base layer.
33353
+ */
33354
+ function useOverlayWidth$1() {
33355
+ const [columns] = useStdoutDimensions();
33356
+ return Math.max(30, Math.min(60, columns - 4));
33357
+ }
31763
33358
  function ConfirmExitOverlay({ onConfirm, onCancel }) {
31764
33359
  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();
33360
+ const [pulse, setPulse] = useState(true);
33361
+ const width = useOverlayWidth$1();
33362
+ useEffect(() => {
33363
+ const timer = setInterval(() => setPulse((p) => !p), 800);
33364
+ return () => clearInterval(timer);
33365
+ }, []);
33366
+ useInput((_input, key) => {
33367
+ if (key.upArrow || key.downArrow) {
33368
+ setSelected((s) => s === 0 ? 1 : 0);
33369
+ return;
33370
+ }
33371
+ if (key.return) {
33372
+ if (selected === 0) onConfirm();
33373
+ else onCancel();
33374
+ return;
33375
+ }
31769
33376
  if (key.escape) onCancel();
31770
33377
  });
33378
+ const borderColor = pulse ? ERROR_COLOR : BRAND_COLOR;
31771
33379
  const options = [COPY.confirmExitYes, COPY.confirmExitNo];
31772
33380
  return /* @__PURE__ */ jsxs(Box, {
31773
33381
  flexDirection: "column",
31774
- borderStyle: "single",
31775
- borderColor: ERROR_COLOR,
33382
+ borderStyle: "bold",
33383
+ borderColor,
33384
+ backgroundColor: "black",
31776
33385
  paddingX: 2,
31777
33386
  paddingY: 1,
33387
+ width,
31778
33388
  children: [
31779
- /* @__PURE__ */ jsx(Text, {
31780
- bold: true,
31781
- children: COPY.confirmExitTitle
33389
+ /* @__PURE__ */ jsxs(Box, {
33390
+ gap: 1,
33391
+ children: [/* @__PURE__ */ jsx(Text, {
33392
+ color: ERROR_COLOR,
33393
+ bold: true,
33394
+ children: Icons.diamond
33395
+ }), /* @__PURE__ */ jsx(Text, {
33396
+ bold: true,
33397
+ children: COPY.confirmExitTitle
33398
+ })]
31782
33399
  }),
31783
33400
  /* @__PURE__ */ jsx(Box, {
31784
33401
  marginTop: 1,
@@ -31791,12 +33408,19 @@ function ConfirmExitOverlay({ onConfirm, onCancel }) {
31791
33408
  gap: 1,
31792
33409
  children: [/* @__PURE__ */ jsx(Text, {
31793
33410
  color: i === selected ? BRAND_COLOR : DIM_COLOR,
31794
- children: i === selected ? "▶" : " "
33411
+ children: i === selected ? Icons.triangleRight : " "
31795
33412
  }), /* @__PURE__ */ jsx(Text, {
31796
33413
  bold: i === selected,
31797
33414
  children: opt
31798
33415
  })]
31799
33416
  }, i))
33417
+ }),
33418
+ /* @__PURE__ */ jsx(Box, {
33419
+ marginTop: 1,
33420
+ children: /* @__PURE__ */ jsx(Text, {
33421
+ dimColor: true,
33422
+ children: "↑↓ escolher · Enter confirmar · Esc cancelar"
33423
+ })
31800
33424
  })
31801
33425
  ]
31802
33426
  });
@@ -31840,14 +33464,8 @@ function PromptOverlay({ prompt: prompt$1, onSubmit, onSkip }) {
31840
33464
  }
31841
33465
  function TextPrompt({ prompt: prompt$1, pulse, onSubmit, onSkip }) {
31842
33466
  const [value, setValue] = useState("");
31843
- const [mode, setMode] = useState("ask");
31844
33467
  const width = useOverlayWidth();
31845
33468
  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
33469
  if (key.return && value.trim()) {
31852
33470
  onSubmit(value.trim());
31853
33471
  return;
@@ -31866,6 +33484,7 @@ function TextPrompt({ prompt: prompt$1, pulse, onSubmit, onSkip }) {
31866
33484
  flexDirection: "column",
31867
33485
  borderStyle: "bold",
31868
33486
  borderColor: pulse ? BRAND_COLOR : ACCENT_COLOR,
33487
+ backgroundColor: "black",
31869
33488
  paddingX: 2,
31870
33489
  paddingY: 1,
31871
33490
  width,
@@ -31912,17 +33531,7 @@ function TextPrompt({ prompt: prompt$1, pulse, onSubmit, onSkip }) {
31912
33531
  ]
31913
33532
  })
31914
33533
  }) : 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, {
33534
+ /* @__PURE__ */ jsxs(Box, {
31926
33535
  marginTop: 1,
31927
33536
  flexDirection: "column",
31928
33537
  children: [/* @__PURE__ */ jsxs(Box, {
@@ -31956,6 +33565,7 @@ function ConfirmPrompt({ prompt: prompt$1, pulse, onSubmit }) {
31956
33565
  flexDirection: "column",
31957
33566
  borderStyle: "bold",
31958
33567
  borderColor: pulse ? BRAND_COLOR : ACCENT_COLOR,
33568
+ backgroundColor: "black",
31959
33569
  paddingX: 2,
31960
33570
  paddingY: 1,
31961
33571
  width,
@@ -31995,17 +33605,40 @@ function ConfirmPrompt({ prompt: prompt$1, pulse, onSubmit }) {
31995
33605
  function ChoicePrompt({ prompt: prompt$1, pulse, onSubmit, onSkip }) {
31996
33606
  const options = prompt$1.options ?? [];
31997
33607
  const [selected, setSelected] = useState(0);
33608
+ const [typed, setTyped] = useState("");
31998
33609
  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();
33610
+ useInput((input, key) => {
33611
+ if (key.upArrow) {
33612
+ setSelected((s) => Math.max(0, s - 1));
33613
+ return;
33614
+ }
33615
+ if (key.downArrow) {
33616
+ setSelected((s) => Math.min(options.length - 1, s + 1));
33617
+ return;
33618
+ }
33619
+ if (key.return) {
33620
+ const trimmed = typed.trim();
33621
+ if (trimmed) onSubmit(trimmed);
33622
+ else onSubmit(options[selected] ?? "");
33623
+ return;
33624
+ }
33625
+ if (key.escape) {
33626
+ onSkip();
33627
+ return;
33628
+ }
33629
+ if (key.backspace || key.delete) {
33630
+ setTyped((v) => v.slice(0, -1));
33631
+ return;
33632
+ }
33633
+ if (input && !key.ctrl && !key.meta) setTyped((v) => v + input);
32004
33634
  });
33635
+ const borderColor = pulse ? BRAND_COLOR : ACCENT_COLOR;
33636
+ const trimmedTyped = typed.trim();
32005
33637
  return /* @__PURE__ */ jsxs(Box, {
32006
33638
  flexDirection: "column",
32007
33639
  borderStyle: "bold",
32008
- borderColor: pulse ? BRAND_COLOR : ACCENT_COLOR,
33640
+ borderColor,
33641
+ backgroundColor: "black",
32009
33642
  paddingX: 2,
32010
33643
  paddingY: 1,
32011
33644
  width,
@@ -32032,22 +33665,43 @@ function ChoicePrompt({ prompt: prompt$1, pulse, onSubmit, onSkip }) {
32032
33665
  /* @__PURE__ */ jsx(Box, {
32033
33666
  marginTop: 1,
32034
33667
  flexDirection: "column",
32035
- children: options.map((opt, i) => /* @__PURE__ */ jsxs(Box, {
33668
+ children: options.map((opt, i) => {
33669
+ const active$1 = !trimmedTyped && i === selected;
33670
+ return /* @__PURE__ */ jsxs(Box, {
33671
+ gap: 1,
33672
+ children: [/* @__PURE__ */ jsx(Text, {
33673
+ color: active$1 ? BRAND_COLOR : DIM_COLOR,
33674
+ children: active$1 ? Icons.triangleRight : " "
33675
+ }), /* @__PURE__ */ jsx(Text, {
33676
+ bold: active$1,
33677
+ dimColor: Boolean(trimmedTyped),
33678
+ children: opt
33679
+ })]
33680
+ }, i);
33681
+ })
33682
+ }),
33683
+ /* @__PURE__ */ jsx(Box, {
33684
+ marginTop: 1,
33685
+ flexDirection: "column",
33686
+ children: /* @__PURE__ */ jsxs(Box, {
32036
33687
  gap: 1,
32037
33688
  children: [/* @__PURE__ */ jsx(Text, {
32038
- color: i === selected ? BRAND_COLOR : DIM_COLOR,
32039
- children: i === selected ? Icons.triangleRight : " "
33689
+ color: trimmedTyped ? BRAND_COLOR : DIM_COLOR,
33690
+ children: Icons.triangleRight
33691
+ }), /* @__PURE__ */ jsxs(Text, { children: [typed || /* @__PURE__ */ jsx(Text, {
33692
+ color: DIM_COLOR,
33693
+ children: "ou digite uma resposta livre"
32040
33694
  }), /* @__PURE__ */ jsx(Text, {
32041
- bold: i === selected,
32042
- children: opt
32043
- })]
32044
- }, i))
33695
+ color: DIM_COLOR,
33696
+ children: "▌"
33697
+ })] })]
33698
+ })
32045
33699
  }),
32046
33700
  /* @__PURE__ */ jsx(Box, {
32047
33701
  marginTop: 1,
32048
33702
  children: /* @__PURE__ */ jsx(Text, {
32049
33703
  dimColor: true,
32050
- children: "Esc para pular"
33704
+ children: "↑↓ escolher · Enter confirmar · Esc pular"
32051
33705
  })
32052
33706
  })
32053
33707
  ]
@@ -32056,9 +33710,9 @@ function ChoicePrompt({ prompt: prompt$1, pulse, onSubmit, onSkip }) {
32056
33710
 
32057
33711
  //#endregion
32058
33712
  //#region src/wizard/ui/App.tsx
32059
- function App({ store, router, cwd }) {
33713
+ function App({ store, router, cwd, onExit: onExit$2 }) {
32060
33714
  useSyncExternalStore(store.subscribe, store.getSnapshot);
32061
- const [, rows] = useStdoutDimensions();
33715
+ const [columns, rows] = useStdoutDimensions();
32062
33716
  const { session } = store;
32063
33717
  const writePromptResponse = useCallback((value, skipped) => {
32064
33718
  const responsePath = join(cwd, ".veloz-wizard-response.json");
@@ -32080,14 +33734,7 @@ function App({ store, router, cwd }) {
32080
33734
  ]);
32081
33735
  const overlay = router.activeOverlay;
32082
33736
  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, {
33737
+ if (overlay === Overlay.Error) baseLayer = /* @__PURE__ */ jsx(ErrorOverlay, {
32091
33738
  message: session.agentError ?? "Erro desconhecido",
32092
33739
  onDismiss: () => router.popOverlay()
32093
33740
  });
@@ -32104,17 +33751,34 @@ function App({ store, router, cwd }) {
32104
33751
  case Screen.Run:
32105
33752
  baseLayer = /* @__PURE__ */ jsx(RunScreen, { store });
32106
33753
  break;
33754
+ case Screen.GithubSetup:
33755
+ baseLayer = /* @__PURE__ */ jsx(GithubSetupScreen, { store });
33756
+ break;
33757
+ case Screen.AiSetup:
33758
+ baseLayer = /* @__PURE__ */ jsx(AiSetupScreen, { store });
33759
+ break;
32107
33760
  case Screen.Outro:
32108
33761
  baseLayer = /* @__PURE__ */ jsx(OutroScreen, { store });
32109
33762
  break;
32110
33763
  default: baseLayer = /* @__PURE__ */ jsx(IntroScreen, { store });
32111
33764
  }
32112
33765
  const prompt$1 = session.agentPrompt;
33766
+ const floatingOverlay = overlay === Overlay.ConfirmExit ? /* @__PURE__ */ jsx(ConfirmExitOverlay, {
33767
+ onConfirm: () => {
33768
+ onExit$2("user confirmed exit");
33769
+ },
33770
+ onCancel: () => router.popOverlay()
33771
+ }) : prompt$1 ? /* @__PURE__ */ jsx(PromptOverlay, {
33772
+ prompt: prompt$1,
33773
+ onSubmit: (value) => writePromptResponse(value, false),
33774
+ onSkip: () => writePromptResponse("", true)
33775
+ }) : null;
32113
33776
  return /* @__PURE__ */ jsxs(Box, {
32114
33777
  flexDirection: "column",
32115
33778
  height: rows,
33779
+ width: columns,
32116
33780
  children: [/* @__PURE__ */ jsx(InputFocusProvider, {
32117
- active: !prompt$1,
33781
+ active: !floatingOverlay,
32118
33782
  children: /* @__PURE__ */ jsx(Box, {
32119
33783
  flexDirection: "column",
32120
33784
  flexGrow: 1,
@@ -32123,15 +33787,13 @@ function App({ store, router, cwd }) {
32123
33787
  overflow: "hidden",
32124
33788
  children: baseLayer
32125
33789
  })
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
- })
33790
+ }), floatingOverlay ? /* @__PURE__ */ jsx(Box, {
33791
+ position: "absolute",
33792
+ width: columns,
33793
+ height: rows,
33794
+ alignItems: "center",
33795
+ justifyContent: "center",
33796
+ children: floatingOverlay
32135
33797
  }) : null]
32136
33798
  });
32137
33799
  }
@@ -32239,6 +33901,15 @@ var WizardStore = class {
32239
33901
  this.$session.setKey("authPhase", "browser");
32240
33902
  this.emitChange();
32241
33903
  }
33904
+ /** Show a banner on AuthScreen explaining why we're re-running device auth. */
33905
+ setReauthNotice(message) {
33906
+ this.$session.setKey("reauthNotice", message);
33907
+ this.emitChange();
33908
+ }
33909
+ clearReauthNotice() {
33910
+ this.$session.setKey("reauthNotice", null);
33911
+ this.emitChange();
33912
+ }
32242
33913
  setAvailableOrgs(orgs) {
32243
33914
  this.$session.setKey("availableOrgs", orgs);
32244
33915
  this.$session.setKey("authPhase", "org-select");
@@ -32362,6 +34033,7 @@ var WizardStore = class {
32362
34033
  /** Set agent helper content (replaces previous content). */
32363
34034
  setAgentHelper(lines) {
32364
34035
  this.$session.setKey("agentHelperLines", lines);
34036
+ this.$session.setKey("agentHelperDiagram", null);
32365
34037
  this.emitChange();
32366
34038
  }
32367
34039
  /** Append a line to agent helper content. */
@@ -32370,6 +34042,12 @@ var WizardStore = class {
32370
34042
  this.$session.setKey("agentHelperLines", lines);
32371
34043
  this.emitChange();
32372
34044
  }
34045
+ /** Set structured deploy diagram (replaces previous diagram and lines). */
34046
+ setAgentHelperDiagram(diagram) {
34047
+ this.$session.setKey("agentHelperDiagram", diagram);
34048
+ this.$session.setKey("agentHelperLines", []);
34049
+ this.emitChange();
34050
+ }
32373
34051
  /** Show a prompt overlay to the user. */
32374
34052
  setAgentPrompt(prompt$1) {
32375
34053
  getSessionLogger().logStoreEvent("setAgentPrompt", {
@@ -32412,46 +34090,60 @@ var WizardStore = class {
32412
34090
  this.$session.setKey("buildDeploymentId", id);
32413
34091
  this.emitChange();
32414
34092
  }
34093
+ setGithubSetupPhase(phase) {
34094
+ getSessionLogger().logStoreEvent("githubSetupPhase", {
34095
+ from: this.session.githubSetupPhase,
34096
+ to: phase
34097
+ });
34098
+ this.$session.setKey("githubSetupPhase", phase);
34099
+ this.emitChange();
34100
+ }
34101
+ setGithubRepoInfo(owner, repo) {
34102
+ this.$session.setKey("githubRepoOwner", owner);
34103
+ this.$session.setKey("githubRepoName", repo);
34104
+ this.emitChange();
34105
+ }
34106
+ setGithubInstallUrl(url) {
34107
+ this.$session.setKey("githubInstallUrl", url);
34108
+ this.emitChange();
34109
+ }
34110
+ failGithubSetup(error) {
34111
+ getSessionLogger().logStoreEvent("failGithubSetup", { error });
34112
+ this.$session.setKey("githubSetupError", error);
34113
+ this.$session.setKey("githubSetupPhase", "error");
34114
+ this.emitChange();
34115
+ }
34116
+ setAiSetupNeeds(needs) {
34117
+ this.$session.setKey("aiSetupNeedsMcp", needs.mcp);
34118
+ this.$session.setKey("aiSetupNeedsSkills", needs.skills);
34119
+ this.$session.setKey("aiSetupMcpStatus", needs.mcp ? "pending" : "skipped");
34120
+ this.$session.setKey("aiSetupSkillsStatus", needs.skills ? "pending" : "skipped");
34121
+ this.emitChange();
34122
+ }
34123
+ setAiSetupPhase(phase) {
34124
+ getSessionLogger().logStoreEvent("aiSetupPhase", {
34125
+ from: this.session.aiSetupPhase,
34126
+ to: phase
34127
+ });
34128
+ this.$session.setKey("aiSetupPhase", phase);
34129
+ this.emitChange();
34130
+ }
34131
+ setAiSetupMcpStatus(status) {
34132
+ this.$session.setKey("aiSetupMcpStatus", status);
34133
+ this.emitChange();
34134
+ }
34135
+ setAiSetupSkillsStatus(status) {
34136
+ this.$session.setKey("aiSetupSkillsStatus", status);
34137
+ this.emitChange();
34138
+ }
34139
+ failAiSetup(error) {
34140
+ getSessionLogger().logStoreEvent("failAiSetup", { error });
34141
+ this.$session.setKey("aiSetupError", error);
34142
+ this.$session.setKey("aiSetupPhase", "error");
34143
+ this.emitChange();
34144
+ }
32415
34145
  };
32416
34146
 
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
34147
  //#endregion
32456
34148
  //#region src/wizard/upload-telemetry.ts
32457
34149
  /**
@@ -32482,23 +34174,15 @@ function contentTypeFor(filename) {
32482
34174
  return CONTENT_TYPES[filename] ?? "application/octet-stream";
32483
34175
  }
32484
34176
  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
- }
34177
+ if (process.env.VELOZ_NO_TELEMETRY === "1") return;
32489
34178
  const metaPath = join(sessionDir, "meta.json");
32490
34179
  let meta;
32491
34180
  try {
32492
34181
  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`);
34182
+ } catch {
32495
34183
  return;
32496
34184
  }
32497
34185
  const authConfig = loadConfig();
32498
- if (!authConfig.apiKey) {
32499
- process.stderr.write("[veloz init] sem apiKey — telemetria não enviada\n");
32500
- return;
32501
- }
32502
34186
  const files = readdirSync(sessionDir).filter((name) => {
32503
34187
  const full = join(sessionDir, name);
32504
34188
  try {
@@ -32511,9 +34195,10 @@ async function uploadInitTelemetry(sessionDir) {
32511
34195
  if (files.length === 0) return;
32512
34196
  const client = createClient(authConfig.apiUrl, () => {
32513
34197
  const headers = {
32514
- Authorization: `Bearer ${authConfig.apiKey}`,
32515
- "User-Agent": "veloz-cli/telemetry"
34198
+ "User-Agent": "veloz-cli/telemetry",
34199
+ "X-Veloz-Client-Source": "cli-wizard"
32516
34200
  };
34201
+ if (authConfig.apiKey) headers.Authorization = `Bearer ${authConfig.apiKey}`;
32517
34202
  if (authConfig.organizationId) headers["X-Organization-Id"] = authConfig.organizationId;
32518
34203
  return headers;
32519
34204
  });
@@ -32535,11 +34220,10 @@ async function uploadInitTelemetry(sessionDir) {
32535
34220
  startedAt: meta.startedAt,
32536
34221
  files
32537
34222
  });
32538
- } catch (err) {
32539
- process.stderr.write(`[veloz init] telemetria (start) falhou: ${err instanceof Error ? err.message : String(err)}\n`);
34223
+ } catch {
32540
34224
  return;
32541
34225
  }
32542
- const failures = (await Promise.allSettled(files.map(async (filename) => {
34226
+ await Promise.allSettled(files.map(async (filename) => {
32543
34227
  const url = started.uploads[filename];
32544
34228
  if (!url) return;
32545
34229
  const body = readFileSync(join(sessionDir, filename));
@@ -32549,8 +34233,7 @@ async function uploadInitTelemetry(sessionDir) {
32549
34233
  body
32550
34234
  });
32551
34235
  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`);
34236
+ }));
32554
34237
  try {
32555
34238
  await client.initTelemetry.finalize({
32556
34239
  id: started.id,
@@ -32561,11 +34244,70 @@ async function uploadInitTelemetry(sessionDir) {
32561
34244
  durationMs: meta.durationMs,
32562
34245
  endedAt: meta.endedAt
32563
34246
  });
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`);
34247
+ } catch {}
34248
+ }
34249
+ let telemetrySent = false;
34250
+ /**
34251
+ * Finalize the session logger and upload telemetry exactly once per process.
34252
+ *
34253
+ * Called on both normal exit and abort paths (double-SIGINT, ConfirmExit, headless
34254
+ * SIGINT). Bounded by `timeoutMs` so a hanging upload cannot block process exit.
34255
+ */
34256
+ async function finalizeAndUploadOnce(logger, outcome, info$1, timeoutMs = 5e3) {
34257
+ if (telemetrySent) return;
34258
+ telemetrySent = true;
34259
+ logger.finalize(outcome, info$1);
34260
+ await Promise.race([uploadInitTelemetry(logger.dir).catch(() => {}), new Promise((resolve$1) => {
34261
+ setTimeout(resolve$1, timeoutMs);
34262
+ })]);
34263
+ }
34264
+
34265
+ //#endregion
34266
+ //#region src/wizard/start-tui.ts
34267
+ /**
34268
+ * Start the Ink TUI renderer for the wizard.
34269
+ * Forces dark background, creates store + router, renders App.
34270
+ */
34271
+ function startTUI(cwd) {
34272
+ if (process.stdout.isTTY) process.stdout.write("\x1B[48;2;0;0;0m\x1B[2J\x1B[H");
34273
+ const store = new WizardStore();
34274
+ const router = new WizardRouter();
34275
+ const workingDir = cwd ?? process.cwd();
34276
+ let teardown = () => {};
34277
+ const exitWithTelemetry = async (reason) => {
34278
+ try {
34279
+ teardown();
34280
+ } catch {}
34281
+ await finalizeAndUploadOnce(getSessionLogger(), "abort", { error: reason });
34282
+ process.exit(130);
34283
+ };
34284
+ const { unmount } = render(createElement(App, {
34285
+ store,
34286
+ router,
34287
+ cwd: workingDir,
34288
+ onExit: exitWithTelemetry
34289
+ }));
34290
+ teardown = unmount;
34291
+ const cleanup = () => {
34292
+ if (process.stdout.isTTY) process.stdout.write("\x1B[0m");
34293
+ };
34294
+ process.on("exit", cleanup);
34295
+ let sigintCount = 0;
34296
+ process.on("SIGINT", () => {
34297
+ sigintCount++;
34298
+ getSessionLogger().log("signal", "SIGINT received", { count: sigintCount });
34299
+ if (sigintCount >= 2) {
34300
+ exitWithTelemetry("user double-SIGINT");
34301
+ return;
34302
+ }
34303
+ router.pushOverlay(Overlay.ConfirmExit);
34304
+ });
34305
+ return {
34306
+ store,
34307
+ router,
34308
+ unmount,
34309
+ waitForIntro: () => store.getGate("intro", (s) => s.introConfirmed)
34310
+ };
32569
34311
  }
32570
34312
 
32571
34313
  //#endregion
@@ -32580,6 +34322,57 @@ async function uploadInitTelemetry(sessionDir) {
32580
34322
  * 5. Spawn Claude Agent → show RunScreen
32581
34323
  * 6. Show OutroScreen with URLs + next steps
32582
34324
  */
34325
+ /** Convert an unknown thrown value into a stable string for telemetry. */
34326
+ function describeError(err) {
34327
+ if (err instanceof Error) return {
34328
+ message: err.message,
34329
+ stack: err.stack
34330
+ };
34331
+ return { message: typeof err === "string" ? err : JSON.stringify(err) };
34332
+ }
34333
+ /**
34334
+ * Detect an ORPC / HTTP 401 from a thrown value. Covers the three shapes we see
34335
+ * in practice: ORPCError instances (`.code === "UNAUTHORIZED"`), ORPC fetch
34336
+ * failures that preserve `.status === 401`, and raw Error messages containing
34337
+ * "Unauthorized".
34338
+ */
34339
+ function isUnauthorizedError(err) {
34340
+ if (!err || typeof err !== "object") return false;
34341
+ const e = err;
34342
+ if (e.code === "UNAUTHORIZED") return true;
34343
+ if (e.status === 401) return true;
34344
+ if (typeof e.message === "string" && /unauthorized/i.test(e.message)) return true;
34345
+ return false;
34346
+ }
34347
+ /** Compose the error string we persist on `meta.error`. */
34348
+ function formatErrorForMeta(prefix, err) {
34349
+ const { message, stack } = describeError(err);
34350
+ return stack ? `${prefix}: ${message}\n${stack}` : `${prefix}: ${message}`;
34351
+ }
34352
+ /**
34353
+ * Install process-level safety nets so a stray throw / rejection still flushes
34354
+ * telemetry before the CLI dies. Returns a disposer that removes the handlers
34355
+ * (so we don't pollute long-running parent processes in tests).
34356
+ */
34357
+ function installCrashHandlers(logger) {
34358
+ const handleCrash = (kind, err) => {
34359
+ const { message, stack } = describeError(err);
34360
+ logger.log("crash", `process ${kind}`, {
34361
+ message,
34362
+ stack
34363
+ });
34364
+ process.stderr.write(`[veloz init] ${kind}: ${message}\n`);
34365
+ finalizeAndUploadOnce(logger, "error", { error: formatErrorForMeta(kind, err) }).catch(() => {}).finally(() => process.exit(1));
34366
+ };
34367
+ const onUncaught = (err) => handleCrash("uncaughtException", err);
34368
+ const onUnhandled = (reason) => handleCrash("unhandledRejection", reason);
34369
+ process.on("uncaughtException", onUncaught);
34370
+ process.on("unhandledRejection", onUnhandled);
34371
+ return () => {
34372
+ process.off("uncaughtException", onUncaught);
34373
+ process.off("unhandledRejection", onUnhandled);
34374
+ };
34375
+ }
32583
34376
  /** Detect repo using the lazy local filesystem layer. */
32584
34377
  function detectLocalRepo(cwd) {
32585
34378
  const layer = makeLocalFs(cwd);
@@ -32616,6 +34409,7 @@ function registerInit(cli$1) {
32616
34409
  }),
32617
34410
  outputPolicy: "agent-only",
32618
34411
  async run(c) {
34412
+ setClientSource("cli-wizard");
32619
34413
  const cwd = process.cwd();
32620
34414
  const logger = initSessionLogger({ cwd });
32621
34415
  logger.log("init", "veloz init invoked", {
@@ -32626,105 +34420,398 @@ function registerInit(cli$1) {
32626
34420
  argv: process.argv.slice(2)
32627
34421
  });
32628
34422
  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"
34423
+ const removeCrashHandlers = installCrashHandlers(logger);
34424
+ try {
34425
+ return await runInitFlow(c, logger, cwd);
34426
+ } catch (err) {
34427
+ const { message, stack } = describeError(err);
34428
+ logger.log("error", "init flow failed", {
34429
+ message,
34430
+ stack
32646
34431
  });
34432
+ await finalizeAndUploadOnce(logger, "error", { error: formatErrorForMeta("init", err) });
34433
+ throw err;
34434
+ } finally {
34435
+ removeCrashHandlers();
32647
34436
  }
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
34437
+ }
34438
+ });
34439
+ }
34440
+ async function runInitFlow(c, logger, cwd) {
34441
+ const forcedFramework = c.options.framework;
34442
+ const analysis = detectLocalRepo(cwd);
34443
+ logger.log("detection", "repo analysis complete", {
34444
+ framework: analysis.framework?.name ?? null,
34445
+ frameworkLabel: analysis.framework?.label ?? null,
34446
+ packageManager: analysis.packageManager,
34447
+ isMonorepo: analysis.isMonorepo
34448
+ });
34449
+ logger.updateMeta({ analysis });
34450
+ const detectedFrameworkId = analysis.framework ? DETECTOR_TO_FRAMEWORK[analysis.framework.name] ?? "nodejs" : null;
34451
+ const detectedLabel = analysis.framework?.label ?? null;
34452
+ if (!process.stdout.isTTY || c.options.ci) {
34453
+ logger.log("mode", "running headless (no TUI)");
34454
+ return await runHeadless({
34455
+ cwd,
34456
+ frameworkId: forcedFramework ?? detectedFrameworkId ?? "nodejs",
34457
+ frameworkLabel: forcedFramework ? FRAMEWORK_LABELS[forcedFramework] ?? forcedFramework : detectedLabel ?? "Node.js",
34458
+ packageManager: analysis.packageManager ?? "npm"
34459
+ });
34460
+ }
34461
+ const tui = startTUI();
34462
+ const { store } = tui;
34463
+ logger.log("tui", "TUI started");
34464
+ try {
34465
+ return await runInteractiveFlow({
34466
+ tui,
34467
+ store,
34468
+ logger,
34469
+ cwd,
34470
+ analysis,
34471
+ detectedFrameworkId,
34472
+ detectedLabel,
34473
+ forcedFramework
34474
+ });
34475
+ } catch (err) {
34476
+ try {
34477
+ tui.unmount();
34478
+ } catch {}
34479
+ const { message } = describeError(err);
34480
+ process.stderr.write(`\n✗ Erro: ${message}\n`);
34481
+ process.stderr.write(` Logs de depuração: ${logger.dir}\n\n`);
34482
+ throw err;
34483
+ }
34484
+ }
34485
+ async function runInteractiveFlow(opts) {
34486
+ const { tui, store, logger, cwd, analysis, detectedFrameworkId, detectedLabel, forcedFramework } = opts;
34487
+ store.setDetection({
34488
+ framework: detectedFrameworkId,
34489
+ frameworkLabel: detectedLabel,
34490
+ packageManager: analysis.packageManager,
34491
+ gitRemote: detectGitRemote(cwd),
34492
+ isMonorepo: analysis.isMonorepo
34493
+ });
34494
+ if (forcedFramework) {
34495
+ store.selectFramework(forcedFramework, FRAMEWORK_LABELS[forcedFramework] ?? forcedFramework);
34496
+ store.confirmIntro();
34497
+ }
34498
+ logger.log("flow", "awaiting intro confirmation");
34499
+ await tui.waitForIntro();
34500
+ logger.log("flow", "intro confirmed");
34501
+ const selectedFramework = store.session.selectedFramework ?? detectedFrameworkId ?? "nodejs";
34502
+ const selectedLabel = store.session.selectedFrameworkLabel ?? detectedLabel ?? "Node.js";
34503
+ logger.updateMeta({
34504
+ framework: selectedFramework,
34505
+ frameworkLabel: selectedLabel,
34506
+ packageManager: store.session.packageManager ?? "npm",
34507
+ isMonorepo: store.session.isMonorepo,
34508
+ gitRemote: store.session.gitRemote
34509
+ });
34510
+ logger.log("flow", "resolving auth + org");
34511
+ await resolveAuthAndOrg(store);
34512
+ logger.log("auth", "auth + org resolved", {
34513
+ userName: store.session.userName,
34514
+ orgId: store.session.orgId,
34515
+ orgName: store.session.orgName
34516
+ });
34517
+ logger.updateMeta({
34518
+ userName: store.session.userName,
34519
+ orgName: store.session.orgName,
34520
+ orgId: store.session.orgId
34521
+ });
34522
+ logger.log("flow", "awaiting user notes");
34523
+ await store.getGate("notes", (s) => s.notesConfirmed);
34524
+ logger.log("flow", "notes confirmed", {
34525
+ hasNotes: Boolean(store.session.userNotes),
34526
+ bytes: store.session.userNotes?.length ?? 0
34527
+ });
34528
+ logger.log("agent", "invoking runAgent");
34529
+ await runAgent({
34530
+ store,
34531
+ cwd,
34532
+ framework: selectedFramework,
34533
+ frameworkLabel: selectedLabel,
34534
+ userName: store.session.userName,
34535
+ orgName: store.session.orgName,
34536
+ packageManager: store.session.packageManager ?? "npm",
34537
+ userNotes: store.session.userNotes
34538
+ });
34539
+ logger.log("agent", "runAgent returned", {
34540
+ phase: store.session.agentPhase,
34541
+ error: store.session.agentError,
34542
+ deployUrl: store.session.deployUrl,
34543
+ retryCount: store.session.retryCount
34544
+ });
34545
+ if (!store.session.agentError) {
34546
+ logger.log("flow", "running github auto-deploy prompt flow");
34547
+ await runGithubSetupFlow(store, cwd);
34548
+ logger.log("flow", "github auto-deploy flow done", {
34549
+ phase: store.session.githubSetupPhase,
34550
+ error: store.session.githubSetupError
34551
+ });
34552
+ }
34553
+ if (!store.session.agentError) {
34554
+ logger.log("flow", "running ai tooling prompt flow");
34555
+ await runAiSetupFlow(store);
34556
+ logger.log("flow", "ai tooling flow done", {
34557
+ phase: store.session.aiSetupPhase,
34558
+ error: store.session.aiSetupError
34559
+ });
34560
+ }
34561
+ logger.log("flow", "awaiting outro dismissal");
34562
+ await store.getGate("outro", (s) => s.outroDismissed);
34563
+ logger.log("flow", "outro dismissed");
34564
+ tui.unmount();
34565
+ logger.log("tui", "TUI unmounted");
34566
+ const success$1 = !store.session.agentError;
34567
+ await finalizeAndUploadOnce(logger, success$1 ? "success" : "error", {
34568
+ error: store.session.agentError ?? void 0,
34569
+ deployUrl: store.session.deployUrl ?? void 0,
34570
+ retries: store.session.retryCount
34571
+ });
34572
+ return {
34573
+ success: success$1,
34574
+ url: store.session.deployUrl ?? void 0
34575
+ };
34576
+ }
34577
+ /**
34578
+ * Parse the first URL in `.git/config` into an owner/repo pair.
34579
+ * We read the file directly (instead of shelling out) so the flow works
34580
+ * regardless of cwd and without depending on `git` being on PATH.
34581
+ */
34582
+ function detectGithubRepo(cwd) {
34583
+ try {
34584
+ const gitConfigPath = join(cwd, ".git", "config");
34585
+ if (!existsSync(gitConfigPath)) return null;
34586
+ const match$3 = readFileSync(gitConfigPath, "utf-8").match(/url\s*=\s*(.+)/);
34587
+ if (!match$3?.[1]) return null;
34588
+ const url = match$3[1].trim();
34589
+ const httpsMatch = url.match(/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/);
34590
+ if (httpsMatch?.[1] && httpsMatch[2]) return {
34591
+ owner: httpsMatch[1],
34592
+ repo: httpsMatch[2]
34593
+ };
34594
+ const sshMatch = url.match(/github\.com:([^/]+)\/([^/.]+?)(?:\.git)?$/);
34595
+ if (sshMatch?.[1] && sshMatch[2]) return {
34596
+ owner: sshMatch[1],
34597
+ repo: sshMatch[2]
34598
+ };
34599
+ return null;
34600
+ } catch {
34601
+ return null;
34602
+ }
34603
+ }
34604
+ /**
34605
+ * Drive the post-deploy GitHub auto-deploy setup.
34606
+ *
34607
+ * 1. If there is no GitHub remote or no veloz.json project id, stay idle.
34608
+ * 2. Show the prompt screen (Yes/No). If the user declines → phase=skipped.
34609
+ * 3. On "yes", check if the project is already connected on the server — if so,
34610
+ * short-circuit to `done` without hitting the GitHub App APIs.
34611
+ * 4. Otherwise check the GitHub App installation status. If installed → connect
34612
+ * the repo and finish. If not → open the install URL, poll for install, then
34613
+ * connect the repo.
34614
+ */
34615
+ async function runGithubSetupFlow(store, cwd) {
34616
+ const logger = getSessionLogger();
34617
+ const remote = detectGithubRepo(cwd);
34618
+ if (!remote) {
34619
+ logger.log("github-setup", "no GitHub remote detected — skipping");
34620
+ return;
34621
+ }
34622
+ const projectId = loadConfig$1()?.project?.id;
34623
+ if (!projectId) {
34624
+ logger.log("github-setup", "no project id in veloz.json — skipping");
34625
+ return;
34626
+ }
34627
+ store.setGithubRepoInfo(remote.owner, remote.repo);
34628
+ store.setGithubSetupPhase("prompt");
34629
+ await store.getGate("github-setup-prompt", (s) => s.githubSetupPhase !== "prompt");
34630
+ if (store.session.githubSetupPhase === "skipped") {
34631
+ logger.log("github-setup", "user declined auto-deploy setup");
34632
+ return;
34633
+ }
34634
+ try {
34635
+ const client = await getClient();
34636
+ const existing = (await client.projects.list()).find((p) => p.id === projectId);
34637
+ if (existing?.githubRepoOwner === remote.owner && existing.githubRepoName === remote.repo && existing.githubInstallationId) {
34638
+ logger.log("github-setup", "project already connected — short-circuit");
34639
+ store.setGithubSetupPhase("done");
34640
+ return;
34641
+ }
34642
+ const installation = await client.github.getInstallation({ owner: remote.owner });
34643
+ if (installation.error === "TOKEN_EXPIRED") {
34644
+ store.failGithubSetup("Token do GitHub expirado. Reconecte sua conta no dashboard e tente novamente.");
34645
+ return;
34646
+ }
34647
+ if (installation.error === "RATE_LIMITED") {
34648
+ store.failGithubSetup("Limite de requisições do GitHub atingido. Tente de novo em alguns minutos.");
34649
+ return;
34650
+ }
34651
+ if (installation.installed && installation.installationId) {
34652
+ store.setGithubSetupPhase("connecting");
34653
+ await client.projects.connectRepo({
34654
+ projectId,
34655
+ githubRepoOwner: remote.owner,
34656
+ githubRepoName: remote.repo,
34657
+ githubInstallationId: installation.installationId
32685
34658
  });
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
34659
+ store.setGithubSetupPhase("done");
34660
+ return;
34661
+ }
34662
+ if (!installation.installUrl) {
34663
+ store.failGithubSetup("GitHub App não está configurado no servidor. Contate o administrador da plataforma.");
34664
+ return;
34665
+ }
34666
+ store.setGithubInstallUrl(installation.installUrl);
34667
+ store.setGithubSetupPhase("installing");
34668
+ await openBrowser(installation.installUrl);
34669
+ const installationId = await pollGithubInstallation(store, remote.owner);
34670
+ if (!installationId) {
34671
+ store.failGithubSetup("Tempo esgotado aguardando a instalação do GitHub App.");
34672
+ return;
34673
+ }
34674
+ store.setGithubSetupPhase("connecting");
34675
+ await client.projects.connectRepo({
34676
+ projectId,
34677
+ githubRepoOwner: remote.owner,
34678
+ githubRepoName: remote.repo,
34679
+ githubInstallationId: installationId
34680
+ });
34681
+ store.setGithubSetupPhase("done");
34682
+ } catch (error) {
34683
+ const message = error instanceof Error ? error.message : String(error);
34684
+ logger.log("github-setup", "unexpected error", { message });
34685
+ store.failGithubSetup(message);
34686
+ }
34687
+ }
34688
+ /**
34689
+ * Offer to install the Veloz MCP server and/or the agent skills when missing.
34690
+ * When the user agrees, we re-invoke the current CLI binary (`process.execPath
34691
+ * <argv[1]>`) for each command so we work regardless of install location, and
34692
+ * capture stdio so the child's output doesn't corrupt the Ink frame.
34693
+ */
34694
+ async function runAiSetupFlow(store) {
34695
+ const logger = getSessionLogger();
34696
+ const status = checkAiSetup();
34697
+ const needsMcp = !status.mcpInstalled;
34698
+ const needsSkills = !status.skillsInstalled;
34699
+ if (!needsMcp && !needsSkills) {
34700
+ logger.log("ai-setup", "both MCP and skills already installed — skipping");
34701
+ return;
34702
+ }
34703
+ store.setAiSetupNeeds({
34704
+ mcp: needsMcp,
34705
+ skills: needsSkills
34706
+ });
34707
+ store.setAiSetupPhase("prompt");
34708
+ await store.getGate("ai-setup-prompt", (s) => s.aiSetupPhase !== "prompt");
34709
+ if (store.session.aiSetupPhase === "skipped") {
34710
+ logger.log("ai-setup", "user declined ai tooling setup");
34711
+ return;
34712
+ }
34713
+ let anyError = false;
34714
+ if (needsMcp) {
34715
+ store.setAiSetupMcpStatus("installing");
34716
+ const mcpResult = await runVelozSubcommand(["mcp", "add"]);
34717
+ if (mcpResult.ok) store.setAiSetupMcpStatus("done");
34718
+ else {
34719
+ store.setAiSetupMcpStatus("error");
34720
+ anyError = true;
34721
+ logger.log("ai-setup", "mcp add failed", { stderr: mcpResult.stderr });
34722
+ }
34723
+ }
34724
+ if (needsSkills) {
34725
+ store.setAiSetupSkillsStatus("installing");
34726
+ const skillsResult = await runVelozSubcommand(["skills", "add"]);
34727
+ if (skillsResult.ok) store.setAiSetupSkillsStatus("done");
34728
+ else {
34729
+ store.setAiSetupSkillsStatus("error");
34730
+ anyError = true;
34731
+ logger.log("ai-setup", "skills add failed", { stderr: skillsResult.stderr });
34732
+ }
34733
+ }
34734
+ if (anyError) store.failAiSetup("Alguma etapa da integração falhou. Veja os logs de debug para detalhes.");
34735
+ else store.setAiSetupPhase("done");
34736
+ }
34737
+ /**
34738
+ * Re-invoke the current CLI (`process.execPath <argv[1]>`) with the given args
34739
+ * while the TUI is running. Silences child stdout/stderr (captured and returned
34740
+ * on failure) so the child doesn't paint over the Ink frame. Times out after
34741
+ * two minutes to prevent a stuck interactive child from hanging the wizard.
34742
+ */
34743
+ function runVelozSubcommand(args$1) {
34744
+ return new Promise((resolve$1) => {
34745
+ const entry = process.argv[1];
34746
+ if (!entry) {
34747
+ resolve$1({
34748
+ ok: false,
34749
+ stderr: "Could not resolve CLI entry path."
32691
34750
  });
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
34751
+ return;
34752
+ }
34753
+ const child = spawn(process.execPath, [entry, ...args$1], {
34754
+ stdio: [
34755
+ "ignore",
34756
+ "pipe",
34757
+ "pipe"
34758
+ ],
34759
+ env: process.env
34760
+ });
34761
+ let stderr = "";
34762
+ let settled = false;
34763
+ const settle = (result$2) => {
34764
+ if (settled) return;
34765
+ settled = true;
34766
+ clearTimeout(timeoutHandle);
34767
+ resolve$1(result$2);
34768
+ };
34769
+ const timeoutHandle = setTimeout(() => {
34770
+ child.kill("SIGTERM");
34771
+ settle({
34772
+ ok: false,
34773
+ stderr: "Tempo esgotado na instalação."
32702
34774
  });
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
34775
+ }, 12e4);
34776
+ child.stdout?.on("data", () => {});
34777
+ child.stderr?.on("data", (chunk) => {
34778
+ stderr += chunk.toString();
34779
+ });
34780
+ child.on("error", (err) => {
34781
+ settle({
34782
+ ok: false,
34783
+ stderr: err.message
32708
34784
  });
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
34785
+ });
34786
+ child.on("close", (code) => {
34787
+ settle({
34788
+ ok: code === 0,
34789
+ stderr
32719
34790
  });
32720
- await uploadInitTelemetry(logger.dir);
32721
- return {
32722
- success: success$1,
32723
- url: store.session.deployUrl ?? void 0
32724
- };
32725
- }
34791
+ });
32726
34792
  });
32727
34793
  }
34794
+ /**
34795
+ * Poll the server for a completed GitHub App installation on `owner`.
34796
+ * Returns the installation id when detected, or null on timeout / auth loss.
34797
+ */
34798
+ async function pollGithubInstallation(store, owner) {
34799
+ const client = await getClient();
34800
+ const maxAttempts = 60;
34801
+ const pollInterval = 5e3;
34802
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
34803
+ await new Promise((resolve$1) => {
34804
+ setTimeout(resolve$1, pollInterval);
34805
+ });
34806
+ const check = await client.github.getInstallation({ owner });
34807
+ if (check.installed && check.installationId) return check.installationId;
34808
+ if (check.error === "TOKEN_EXPIRED") {
34809
+ store.failGithubSetup("Token do GitHub expirou durante a espera.");
34810
+ return null;
34811
+ }
34812
+ }
34813
+ return null;
34814
+ }
32728
34815
  async function runHeadless(opts) {
32729
34816
  const { WizardStore: WizardStore$1 } = await import("./store-CFF2J0lW.mjs");
32730
34817
  const { runAgent: run } = await import("./agent-interface-DL5S8SEn.mjs");
@@ -32750,27 +34837,50 @@ async function runHeadless(opts) {
32750
34837
  userName: "CI",
32751
34838
  orgId: config.organizationId ?? ""
32752
34839
  });
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
34840
+ const onSigint = () => {
34841
+ logger.log("signal", "SIGINT received (headless)");
34842
+ (async () => {
34843
+ await finalizeAndUploadOnce(logger, "abort", { error: "user SIGINT" });
34844
+ process.exit(130);
34845
+ })();
32773
34846
  };
34847
+ process.on("SIGINT", onSigint);
34848
+ try {
34849
+ logger.log("headless", "invoking runAgent");
34850
+ await run({
34851
+ store,
34852
+ cwd: opts.cwd,
34853
+ framework: opts.frameworkId,
34854
+ frameworkLabel: opts.frameworkLabel,
34855
+ userName: "CI",
34856
+ orgName: "",
34857
+ packageManager: opts.packageManager
34858
+ });
34859
+ const success$1 = !store.session.agentError;
34860
+ await finalizeAndUploadOnce(logger, success$1 ? "success" : "error", {
34861
+ error: store.session.agentError ?? void 0,
34862
+ deployUrl: store.session.deployUrl ?? void 0,
34863
+ retries: store.session.retryCount
34864
+ });
34865
+ return {
34866
+ success: success$1,
34867
+ url: store.session.deployUrl ?? void 0
34868
+ };
34869
+ } catch (err) {
34870
+ const { message, stack } = describeError(err);
34871
+ logger.log("headless", "runAgent threw", {
34872
+ message,
34873
+ stack
34874
+ });
34875
+ await finalizeAndUploadOnce(logger, "error", {
34876
+ error: formatErrorForMeta("headless", err),
34877
+ deployUrl: store.session.deployUrl ?? void 0,
34878
+ retries: store.session.retryCount
34879
+ });
34880
+ throw err;
34881
+ } finally {
34882
+ process.off("SIGINT", onSigint);
34883
+ }
32774
34884
  }
32775
34885
  /**
32776
34886
  * Perform device auth with TUI feedback — reuses shared helpers from login.ts.
@@ -32786,33 +34896,53 @@ async function performDeviceAuth(store, apiUrl) {
32786
34896
  store.setAuthPhase("polling");
32787
34897
  return pollForToken(authClient, data.device_code, data.interval || 5);
32788
34898
  }
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) => ({
34899
+ /** Fetch the list of orgs the user belongs to via ORPC. */
34900
+ async function listOrganizations() {
34901
+ return (await (await getClient()).organizations.list()).map((o) => ({
32797
34902
  id: o.id,
32798
34903
  name: o.name,
32799
34904
  slug: o.slug
32800
34905
  }));
32801
34906
  }
32802
34907
  /**
34908
+ * Fetch orgs, handling an expired local API key by transparently re-running
34909
+ * the device auth flow inside the TUI. The user sees: "Verificando autenticação…"
34910
+ * → "Sessão expirou, autenticando novamente…" → device-code screen →
34911
+ * org picker, without ever bailing back to the shell.
34912
+ */
34913
+ async function listOrgsOrReauth(store) {
34914
+ const logger = getSessionLogger();
34915
+ try {
34916
+ return await listOrganizations();
34917
+ } catch (err) {
34918
+ if (!isUnauthorizedError(err)) throw err;
34919
+ logger.log("auth", "stale api key detected (401) — forcing re-login", { message: describeError(err).message });
34920
+ store.setReauthNotice("Sua sessão expirou. Vamos autenticar novamente para continuar.");
34921
+ saveConfig({ apiKey: "" });
34922
+ const config = loadConfig();
34923
+ store.setAuthPhase("browser");
34924
+ const token = await performDeviceAuth(store, config.apiUrl);
34925
+ if (!token) throw new Error("Autenticação cancelada ou expirada.");
34926
+ logger.log("auth", "re-login succeeded — retrying org fetch");
34927
+ saveConfig({ apiKey: token });
34928
+ store.clearReauthNotice();
34929
+ return await listOrganizations();
34930
+ }
34931
+ }
34932
+ /**
32803
34933
  * Create a new organization via Better Auth. The slug is derived from the name
32804
34934
  * and suffixed with a random token when a collision occurs.
32805
34935
  */
32806
- async function createOrganization(apiUrl, apiKey, name) {
34936
+ async function createOrganization(apiUrl, name) {
32807
34937
  const baseSlug = name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "workspace";
34938
+ const headers = await getAuthHeaders();
32808
34939
  let slug = baseSlug;
32809
34940
  for (let attempt = 0; attempt < 3; attempt++) {
32810
34941
  const res = await fetch(`${apiUrl}/api/auth/organization/create`, {
32811
34942
  method: "POST",
32812
34943
  headers: {
32813
- Authorization: `Bearer ${apiKey}`,
32814
- "Content-Type": "application/json",
32815
- "User-Agent": "veloz-cli"
34944
+ ...headers,
34945
+ "Content-Type": "application/json"
32816
34946
  },
32817
34947
  body: JSON.stringify({
32818
34948
  name,
@@ -32868,16 +34998,17 @@ async function resolveAuthAndOrg(store) {
32868
34998
  }
32869
34999
  } else logger.log("auth", "already authenticated from config");
32870
35000
  logger.log("auth", "fetching organizations");
32871
- const orgs = await listOrganizations(config.apiUrl, config.apiKey);
35001
+ const orgs = await listOrgsOrReauth(store);
32872
35002
  logger.log("auth", "organizations loaded", { count: orgs.length });
35003
+ config = loadConfig();
32873
35004
  let chosen;
32874
35005
  if (orgs.length === 0) {
32875
35006
  store.setAuthPhase("org-create");
32876
- chosen = await runOrgCreationFlow(store, config.apiUrl, config.apiKey);
35007
+ chosen = await runOrgCreationFlow(store, config.apiUrl);
32877
35008
  } else {
32878
35009
  store.setAvailableOrgs(orgs);
32879
35010
  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);
35011
+ if (store.session.authPhase === "org-create") chosen = await runOrgCreationFlow(store, config.apiUrl);
32881
35012
  else {
32882
35013
  const selectedOrgId = store.session.selectedOrgId;
32883
35014
  const match$3 = orgs.find((o) => o.id === selectedOrgId);
@@ -32898,7 +35029,7 @@ async function resolveAuthAndOrg(store) {
32898
35029
  * Drive the org-create screen: wait for the user to submit a name, try to
32899
35030
  * create it, and retry on failure without leaving the wizard.
32900
35031
  */
32901
- async function runOrgCreationFlow(store, apiUrl, apiKey) {
35032
+ async function runOrgCreationFlow(store, apiUrl) {
32902
35033
  store.requestOrgCreation();
32903
35034
  for (;;) {
32904
35035
  await store.getGate(`org-create-${Date.now()}-${Math.random()}`, (s) => s.newOrgName !== null && s.authPhase === "org-creating");
@@ -32908,7 +35039,7 @@ async function runOrgCreationFlow(store, apiUrl, apiKey) {
32908
35039
  continue;
32909
35040
  }
32910
35041
  try {
32911
- return await createOrganization(apiUrl, apiKey, name);
35042
+ return await createOrganization(apiUrl, name);
32912
35043
  } catch (error) {
32913
35044
  const message = error instanceof Error ? error.message : String(error);
32914
35045
  store.failOrgCreation(message);
@@ -32920,7 +35051,7 @@ async function runOrgCreationFlow(store, apiUrl, apiKey) {
32920
35051
  //#region src/index.ts
32921
35052
  if (process.argv.includes("--mcp")) process.env.VELOZ_MCP = "true";
32922
35053
  const cli = Cli.create("veloz", {
32923
- version: "0.0.0-beta.31",
35054
+ version: "0.0.0-beta.33",
32924
35055
  description: "CLI da plataforma Veloz — deploy rápido para o Brasil",
32925
35056
  env: z.object({ VELOZ_ENV: z.string().optional().describe("Ambiente alvo (ex: preview, staging)") }),
32926
35057
  mcp: { command: "npx -y onveloz --mcp" }
@@ -32937,6 +35068,26 @@ cli.use(async (c, next) => {
32937
35068
  if (error instanceof Error) {
32938
35069
  const orpcError = error;
32939
35070
  const orpcData = orpcError.data;
35071
+ if (orpcData?.code === "MULTIPLE_ORGS") {
35072
+ const msg = "Você pertence a múltiplos workspaces. Selecione um para continuar.";
35073
+ if (process.env.GITHUB_ACTIONS === "true") process.stdout.write(`::error::${msg}\n`);
35074
+ else if (process.stdout.isTTY) {
35075
+ console.error(`\n✗ ${msg}`);
35076
+ console.error(" Execute `veloz orgs list` para ver e `veloz orgs use <slug>` para escolher.");
35077
+ }
35078
+ return c.error({
35079
+ code: "MULTIPLE_ORGS",
35080
+ message: msg,
35081
+ exitCode: 1,
35082
+ cta: { commands: [{
35083
+ command: "orgs list",
35084
+ description: "Listar workspaces disponíveis"
35085
+ }, {
35086
+ command: "orgs use",
35087
+ description: "Selecionar workspace padrão"
35088
+ }] }
35089
+ });
35090
+ }
32940
35091
  if (orpcData?.code === "NO_ORGANIZATION" || orpcData?.code === "ACCESS_NOT_APPROVED") {
32941
35092
  const msg = "Você precisa criar um workspace antes de continuar.";
32942
35093
  const hint = "Acesse app.onveloz.com/onboarding para criar seu workspace.";
@@ -32997,6 +35148,7 @@ cli.use(async (c, next) => {
32997
35148
  }
32998
35149
  });
32999
35150
  cli.command(projectsGroup);
35151
+ cli.command(orgsGroup);
33000
35152
  cli.command(envGroup);
33001
35153
  cli.command(domainsGroup);
33002
35154
  cli.command(volumesGroup);