onveloz 0.0.0-beta.30 → 0.0.0-beta.31

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 +2259 -641
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -2,17 +2,17 @@
2
2
  import { Cli, middleware, z } from "incur";
3
3
  import { exec, execSync } from "node:child_process";
4
4
  import * as fs from "node:fs";
5
- import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
5
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
6
6
  import * as path from "node:path";
7
7
  import { basename, dirname, join, relative, resolve } from "node:path";
8
8
  import { z as z$1 } from "zod";
9
9
  import { createORPCClient } from "@orpc/client";
10
10
  import { RPCLink } from "@orpc/client/fetch";
11
- import { createHash } from "node:crypto";
11
+ import { createHash, randomBytes } from "node:crypto";
12
12
  import { homedir, platform, tmpdir } from "node:os";
13
13
  import chalk from "chalk";
14
14
  import { createAuthClient } from "better-auth/client";
15
- import { deviceAuthorizationClient, organizationClient } from "better-auth/client/plugins";
15
+ import { deviceAuthorizationClient } from "better-auth/client/plugins";
16
16
  import * as readline from "node:readline";
17
17
  import { createInterface } from "node:readline";
18
18
  import ora from "ora";
@@ -21,7 +21,8 @@ import WebSocket from "ws";
21
21
  import { link, mkdir, mkdtemp, readdir, rm, stat } from "node:fs/promises";
22
22
  import ignore from "ignore";
23
23
  import tar from "tar";
24
- import React, { createElement, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
24
+ import { promisify } from "node:util";
25
+ import React, { createContext, createElement, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
25
26
  import { Box, Text, render, useInput, useStdout } from "ink";
26
27
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
27
28
  import { map } from "nanostores";
@@ -1353,7 +1354,7 @@ function getGlobalProjectFlag() {
1353
1354
  const idx = process.argv.indexOf("--project");
1354
1355
  if (idx !== -1 && idx + 1 < process.argv.length) return process.argv[idx + 1];
1355
1356
  }
1356
- function isMultipleOrgsError$2(error) {
1357
+ function isMultipleOrgsError$1(error) {
1357
1358
  if (!(error instanceof Error)) return false;
1358
1359
  const e = error;
1359
1360
  return e.data?.code === "MULTIPLE_ORGS" && Array.isArray(e.data?.organizations);
@@ -1365,7 +1366,7 @@ async function fetchProjectsWithOrgResolution(client) {
1365
1366
  try {
1366
1367
  return await client.projects.list();
1367
1368
  } catch (error) {
1368
- if (!isMultipleOrgsError$2(error)) throw error;
1369
+ if (!isMultipleOrgsError$1(error)) throw error;
1369
1370
  const orgs = error.data.organizations;
1370
1371
  if (orgs.length === 1) {
1371
1372
  const org = orgs[0];
@@ -12386,16 +12387,16 @@ const isSuccess$1 = /* @__PURE__ */ matchEager$1({
12386
12387
  onSuccess: () => true
12387
12388
  });
12388
12389
  /** @internal */
12389
- const delay$1 = /* @__PURE__ */ dual(2, (self, duration) => andThen$1(sleep$3(duration), self));
12390
+ const delay$1 = /* @__PURE__ */ dual(2, (self, duration) => andThen$1(sleep$2(duration), self));
12390
12391
  /** @internal */
12391
- const timeoutOrElse$1 = /* @__PURE__ */ dual(2, (self, options) => raceFirst$1(self, flatMap$1(sleep$3(options.duration), options.orElse)));
12392
+ const timeoutOrElse$1 = /* @__PURE__ */ dual(2, (self, options) => raceFirst$1(self, flatMap$1(sleep$2(options.duration), options.orElse)));
12392
12393
  /** @internal */
12393
12394
  const timeout$1 = /* @__PURE__ */ dual(2, (self, duration) => timeoutOrElse$1(self, {
12394
12395
  duration,
12395
12396
  orElse: () => fail$2(new TimeoutError$1())
12396
12397
  }));
12397
12398
  /** @internal */
12398
- const timeoutOption$1 = /* @__PURE__ */ dual(2, (self, duration) => raceFirst$1(asSome$1(self), as$1(sleep$3(duration), none())));
12399
+ const timeoutOption$1 = /* @__PURE__ */ dual(2, (self, duration) => raceFirst$1(asSome$1(self), as$1(sleep$2(duration), none())));
12399
12400
  /** @internal */
12400
12401
  const timed$1 = (self) => clockWith$1((clock) => {
12401
12402
  const start = clock.currentTimeNanosUnsafe();
@@ -13212,7 +13213,7 @@ const processOrPerformanceNow = /* @__PURE__ */ function() {
13212
13213
  /** @internal */
13213
13214
  const clockWith$1 = (f) => withFiber$1((fiber$2) => f(fiber$2.getRef(ClockRef)));
13214
13215
  /** @internal */
13215
- const sleep$3 = (duration) => clockWith$1((clock) => clock.sleep(fromInputUnsafe(duration)));
13216
+ const sleep$2 = (duration) => clockWith$1((clock) => clock.sleep(fromInputUnsafe(duration)));
13216
13217
  /** @internal */
13217
13218
  const TimeoutErrorTypeId$1 = "~effect/Cause/TimeoutError";
13218
13219
  /** @internal */
@@ -15536,7 +15537,7 @@ const toStepWithMetadata = (schedule) => clockWith$1((clock) => map$3(toStep(sch
15536
15537
  const meta = metaFn(now, input);
15537
15538
  meta.output = output;
15538
15539
  meta.duration = duration;
15539
- return as$1(sleep$3(duration), meta);
15540
+ return as$1(sleep$2(duration), meta);
15540
15541
  });
15541
15542
  });
15542
15543
  }));
@@ -18892,7 +18893,7 @@ const delay = delay$1;
18892
18893
  * @since 2.0.0
18893
18894
  * @category Delays & Timeouts
18894
18895
  */
18895
- const sleep$2 = sleep$3;
18896
+ const sleep$1 = sleep$2;
18896
18897
  /**
18897
18898
  * Measures the runtime of an effect and returns the duration with its result.
18898
18899
  *
@@ -24079,7 +24080,7 @@ const LOGO_LINES$1 = [
24079
24080
  ];
24080
24081
  const BRAND_COLOR$1 = "#FF4D00";
24081
24082
  function getVersion() {
24082
- return "0.0.0-beta.30";
24083
+ return "0.0.0-beta.31";
24083
24084
  }
24084
24085
  function printBanner(subtitle) {
24085
24086
  const version$2 = getVersion();
@@ -25145,6 +25146,7 @@ function printDeployWarnings(warnings) {
25145
25146
 
25146
25147
  //#endregion
25147
25148
  //#region src/lib/auto-update.ts
25149
+ const execAsync = promisify(exec);
25148
25150
  const PACKAGE_NAME = "onveloz";
25149
25151
  function detectPackageManager() {
25150
25152
  try {
@@ -25174,22 +25176,24 @@ async function fetchLatestVersion() {
25174
25176
  }
25175
25177
  }
25176
25178
  function getCurrentVersion() {
25177
- return "0.0.0-beta.30";
25179
+ return "0.0.0-beta.31";
25178
25180
  }
25179
25181
  /**
25180
25182
  * Install a specific CLI version. Returns true on success.
25181
25183
  */
25182
- function installVersion(pm, version$2) {
25184
+ async function installVersion(pm, version$2) {
25183
25185
  const installCmd = getInstallCommand(pm, version$2);
25184
25186
  const spin = spinner(`Atualizando CLI ${chalk.dim(getCurrentVersion())} → ${chalk.bold(version$2)}...`);
25185
25187
  try {
25186
- execSync(installCmd, { stdio: "ignore" });
25188
+ await execAsync(installCmd, { timeout: 12e4 });
25187
25189
  spin.stop();
25188
25190
  console.log(chalk.green(`\n✓ CLI atualizada: ${chalk.bold(version$2)}\n`));
25189
25191
  return true;
25190
- } catch {
25192
+ } catch (error) {
25191
25193
  spin.stop();
25194
+ const stderr = error.stderr?.trim();
25192
25195
  console.log(chalk.yellow(`\n⚠ Não foi possível atualizar automaticamente. Execute manualmente:\n ${installCmd}\n`));
25196
+ if (stderr) console.log(chalk.dim(stderr.split("\n").slice(-5).join("\n")));
25193
25197
  return false;
25194
25198
  }
25195
25199
  }
@@ -25215,7 +25219,7 @@ async function autoUpdate() {
25215
25219
  const currentVersion = getCurrentVersion();
25216
25220
  const latestVersion = await fetchLatestVersion();
25217
25221
  if (!latestVersion || latestVersion === currentVersion) return;
25218
- if (installVersion(pm, latestVersion)) reExec();
25222
+ if (await installVersion(pm, latestVersion)) reExec();
25219
25223
  }
25220
25224
 
25221
25225
  //#endregion
@@ -26288,7 +26292,7 @@ async function addServiceFlow(existingConfig, opts) {
26288
26292
  await provisionDatabases(updatedConfig, { yes: opts.yes ?? false });
26289
26293
  await triggerDeploy(service$2.id, service$2.name);
26290
26294
  }
26291
- function isMultipleOrgsError$1(error) {
26295
+ function isMultipleOrgsError(error) {
26292
26296
  if (!(error instanceof Error)) return false;
26293
26297
  const e = error;
26294
26298
  return e.data?.code === "MULTIPLE_ORGS" && Array.isArray(e.data?.organizations);
@@ -26300,7 +26304,7 @@ async function resolveOrgIfNeeded(nonInteractive) {
26300
26304
  try {
26301
26305
  await client.projects.list();
26302
26306
  } catch (error) {
26303
- if (!isMultipleOrgsError$1(error)) throw error;
26307
+ if (!isMultipleOrgsError(error)) throw error;
26304
26308
  const orgs = error.data.organizations;
26305
26309
  if (orgs.length === 1) {
26306
26310
  const org = orgs[0];
@@ -26438,9 +26442,18 @@ async function* mcpDeployFlow(opts) {
26438
26442
  });
26439
26443
  return;
26440
26444
  }
26441
- const configuredServices = await findServicesFromConfig();
26442
- if (configuredServices.length === 0) throw new Error("Nenhum serviço configurado. Execute 'veloz deploy' no terminal para configurar o projeto primeiro.");
26443
- if (!opts.service && !opts.all) {
26445
+ let configuredServices = await findServicesFromConfig();
26446
+ let justBootstrapped = false;
26447
+ if (configuredServices.length === 0) {
26448
+ await mcpBootstrapProjectAndService({
26449
+ app: opts.app,
26450
+ name: opts.name
26451
+ });
26452
+ configuredServices = await findServicesFromConfig();
26453
+ if (configuredServices.length === 0) throw new Error("Falha ao configurar o projeto automaticamente. Verifique o output acima para detalhes.");
26454
+ justBootstrapped = true;
26455
+ }
26456
+ if (!opts.service && !opts.all && !justBootstrapped) {
26444
26457
  const available = configuredServices.map((s) => `${s.key} (${s.serviceName})`).join(", ");
26445
26458
  throw new Error(`Especifique o(s) serviço(s) para deploy com --service ou use --all.\n\nServiços disponíveis: ${available}`);
26446
26459
  }
@@ -26475,6 +26488,76 @@ async function* mcpDeployFlow(opts) {
26475
26488
  data: result$2
26476
26489
  };
26477
26490
  }
26491
+ /**
26492
+ * Bootstrap a project + service for the current cwd in MCP mode.
26493
+ *
26494
+ * Mirrors the first-deploy setup in `cliDeployFlow` but runs fully
26495
+ * non-interactively so agents (including the wizard) can call
26496
+ * `veloz:deploy` on a fresh directory without shelling out.
26497
+ */
26498
+ async function mcpBootstrapProjectAndService(opts) {
26499
+ await resolveOrgIfNeeded(true);
26500
+ const client = await getClient();
26501
+ const remote = isGitRepo() ? getGitRemote() : null;
26502
+ const project = remote ? await withRetry(() => client.projects.findByRepo({
26503
+ githubRepoOwner: remote.owner,
26504
+ githubRepoName: remote.repo
26505
+ })) : null;
26506
+ if (project && project.services.length > 0) {
26507
+ info(`Projeto encontrado: ${project.name}`);
26508
+ const svc = project.services[0];
26509
+ const detectedType = svc.type?.toLowerCase() ?? "web";
26510
+ saveConfig$1({
26511
+ version: "1.0",
26512
+ project: {
26513
+ id: project.id,
26514
+ name: project.name
26515
+ },
26516
+ services: { main: {
26517
+ id: svc.id,
26518
+ name: svc.name,
26519
+ type: detectedType,
26520
+ root: ".",
26521
+ branch: svc.branch
26522
+ } },
26523
+ created: (/* @__PURE__ */ new Date()).toISOString()
26524
+ });
26525
+ info(`Arquivo ${getConfigFileName()} criado.`);
26526
+ return;
26527
+ }
26528
+ if (project && project.services.length === 0) {
26529
+ info(`Projeto encontrado: ${project.name}. Criando serviço...`);
26530
+ await createServiceFlow(project.id, project.name, remote?.repo ?? project.name, {
26531
+ yes: true,
26532
+ app: opts.app
26533
+ });
26534
+ return;
26535
+ }
26536
+ const dirName = basename(process.cwd());
26537
+ const defaultName = opts.name ?? remote?.repo ?? dirName;
26538
+ const toSlug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
26539
+ let projectSlug = toSlug(defaultName);
26540
+ let slugCheck = await withRetry(() => client.projects.checkSlug({ slug: projectSlug }));
26541
+ if (!slugCheck.available) {
26542
+ projectSlug = toSlug(`${defaultName}-${Math.random().toString(36).slice(2, 6)}`);
26543
+ slugCheck = await withRetry(() => client.projects.checkSlug({ slug: projectSlug }));
26544
+ if (!slugCheck.available) throw new Error(`Não foi possível reservar um subdomínio para "${defaultName}". Tente novamente ou passe --name.`);
26545
+ }
26546
+ info(`Criando projeto "${defaultName}" (${projectSlug}.runveloz.com)...`);
26547
+ const newProject = await withRetry(() => client.projects.create({
26548
+ name: defaultName,
26549
+ slug: projectSlug,
26550
+ ...remote && {
26551
+ githubRepoOwner: remote.owner,
26552
+ githubRepoName: remote.repo
26553
+ }
26554
+ }));
26555
+ success(`Projeto criado: ${newProject.name}`);
26556
+ await createServiceFlow(newProject.id, newProject.name, remote?.repo ?? dirName, {
26557
+ yes: true,
26558
+ app: opts.app
26559
+ });
26560
+ }
26478
26561
  async function cliDeployFlow(opts) {
26479
26562
  if (opts.verbose) process.env.VELOZ_VERBOSE = "true";
26480
26563
  printBanner("Deploy");
@@ -27048,7 +27131,7 @@ function registerUpdate(cli$1) {
27048
27131
  version: currentVersion
27049
27132
  };
27050
27133
  }
27051
- if (!installVersion(pm, latestVersion)) {
27134
+ if (!await installVersion(pm, latestVersion)) {
27052
27135
  console.log(chalk.dim(` Execute manualmente: ${getInstallCommand(pm, latestVersion)}`));
27053
27136
  return {
27054
27137
  updated: false,
@@ -27189,6 +27272,25 @@ const FRAMEWORK_LABELS = Object.fromEntries(FRAMEWORK_LIST.map((f) => [f.value,
27189
27272
 
27190
27273
  //#endregion
27191
27274
  //#region src/wizard/agent/agent-prompt.ts
27275
+ function renderUserNotes(notes) {
27276
+ if (!notes || !notes.trim()) return "";
27277
+ return `
27278
+
27279
+ ## Notas do Usuário (instruções explícitas — prioridade máxima)
27280
+ O usuário forneceu as seguintes instruções ANTES de iniciar o deploy. Trate-as como requisitos obrigatórios e respeite-as em todas as decisões (escolha de apps, Dockerfile, flags, variáveis, etc.). Se as notas conflitarem com os defaults do wizard, as notas vencem.
27281
+
27282
+ ${notes.trim()}`;
27283
+ }
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
+ }
27192
27294
  function assemblePrompt(ctx) {
27193
27295
  return `Você é o wizard de deploy da Veloz. Sua missão é configurar e fazer deploy do projeto do usuário de forma autônoma.
27194
27296
 
@@ -27196,7 +27298,7 @@ function assemblePrompt(ctx) {
27196
27298
  - Diretório: ${ctx.cwd}
27197
27299
  - Framework detectado: ${ctx.frameworkLabel}
27198
27300
  - Package manager: ${ctx.packageManager}
27199
- - Já autenticado como: ${ctx.userName} (org: ${ctx.orgName})
27301
+ - Já autenticado como: ${ctx.userName} (org: ${ctx.orgName})${renderUserNotes(ctx.userNotes)}
27200
27302
 
27201
27303
  ## O que é a Veloz
27202
27304
  A Veloz é uma PaaS brasileira que faz deploy de aplicações a partir do código-fonte. A plataforma:
@@ -27224,54 +27326,334 @@ Leia o arquivo \`.claude/skills/veloz-llms.txt\` para entender a plataforma, fra
27224
27326
  ### 1. Analisar o projeto
27225
27327
  - Leia package.json, configs do framework, .env.example
27226
27328
  - Identifique: build command, start command, porta, variáveis de ambiente necessárias
27329
+ - Procure Dockerfiles/containers já existentes (veja seção 1b abaixo)
27227
27330
  - CUIDADO: NÃO rode \`npm run dev\`, \`next dev\`, \`vite\`, \`nodemon\` ou qualquer servidor de desenvolvimento. Isso trava o processo.
27228
27331
 
27332
+ ### 1b. Escape hatch: Dockerfile
27333
+ 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
+
27335
+ **Cenário A — já existe um Dockerfile no projeto (USE, não recrie)**
27336
+ ANTES de assumir nixpacks, procure por arquivos Docker já presentes:
27337
+
27338
+ \`\`\`
27339
+ Glob: Dockerfile
27340
+ Glob: **/Dockerfile
27341
+ Glob: **/Dockerfile.*
27342
+ Glob: docker-compose.yml
27343
+ \`\`\`
27344
+
27345
+ Se houver um Dockerfile no caminho do app (raiz do projeto, ou em \`apps/<app>/Dockerfile\` para monorepos), você DEVE reutilizá-lo:
27346
+ - NÃO sobrescreva, NÃO edite, NÃO gere um novo.
27347
+ - Configure o serviço para usar \`method: "dockerfile"\` via \`veloz:config_set\`:
27348
+ \`\`\`
27349
+ veloz:config_set { "build": { "method": "dockerfile", "dockerfile": "apps/api/Dockerfile", "context": "." } }
27350
+ \`\`\`
27351
+ - \`dockerfile\` é o caminho do Dockerfile relativo à raiz do projeto. \`context\` é o diretório de build (geralmente \`.\` para monorepos que precisam do workspace inteiro, ou o caminho do app para projetos simples).
27352
+ - Mencione no diagrama e na confirmação: "Usando Dockerfile existente em \`<caminho>\`".
27353
+
27354
+ Se houver múltiplos Dockerfiles (ex: \`Dockerfile\` + \`Dockerfile.dev\`), prefira o que NÃO tem sufixo \`dev\`/\`test\`. Em dúvida, pergunte ao usuário via \`wizard-tools:prompt_user\` com \`type: "choice"\`.
27355
+
27356
+ **Cenário B — nixpacks não dá conta do stack (fallback)**
27357
+ Criar Dockerfile **novo** (só se não houver nenhum) SOMENTE se:
27358
+ - O framework não é suportado pelo Nixpacks (ex: binários compilados customizados, runtime exótico, setup nativo complexo com bibliotecas de sistema específicas)
27359
+ - Já tentou corrigir a misconfiguração do nixpacks 2 vezes sem sucesso (via \`veloz:config_set\`)
27360
+ - O usuário pediu explicitamente nas notas
27361
+
27362
+ Nesse caso:
27363
+ 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
27365
+ 3. Configure o serviço com \`method: "dockerfile"\` via \`veloz:config_set\`
27366
+ 4. Re-deploye
27367
+
27368
+ 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
+
27370
+ NUNCA crie um Dockerfile só porque o primeiro build falhou — diagnostique primeiro (seção 7).
27371
+
27229
27372
  ### 2. Configurar variáveis de ambiente
27230
27373
  - Use wizard-tools:check_env_keys para ver quais .env existem
27231
27374
  - Use wizard-tools:prompt_user para pedir valores sensíveis ao usuário (com hint de onde obter)
27232
27375
  - Use veloz:env_set para configurar no servidor ANTES do deploy (variáveis de build como NEXT_PUBLIC_* precisam existir antes)
27233
27376
  - O usuário pode escolher "configurar depois" — nesse caso, continue sem a env var
27234
27377
 
27235
- ### 3. Fazer deploy
27236
- Use SEMPRE a ferramenta MCP \`veloz:deploy\` para fazer deploy. NUNCA use Bash para rodar \`veloz deploy\`.
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):
27389
+ \`\`\`
27390
+ wizard-tools:set_helper_content {
27391
+ "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
+ ]
27403
+ }
27404
+ \`\`\`
27405
+
27406
+ Só prossiga para a confirmação DEPOIS que o diagrama for enviado.
27407
+
27408
+ ### 4. Confirmar com o usuário (OBRIGATÓRIO — antes do deploy)
27409
+ 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
+
27411
+ \`\`\`
27412
+ wizard-tools:prompt_user {
27413
+ "question": "Pronto para fazer deploy de <framework> com build <build-cmd> e start <start-cmd>? Consulte o diagrama acima.",
27414
+ "type": "confirm"
27415
+ }
27416
+ \`\`\`
27417
+
27418
+ Inclua na pergunta um resumo claro do que vai ser deployado:
27419
+ - Framework e caminho do app (para monorepos)
27420
+ - Comando de build e start que serão usados
27421
+ - Se vai criar banco/volume/serviços auxiliares
27422
+ - Variáveis de ambiente que serão configuradas
27423
+
27424
+ Regras:
27425
+ - Se o usuário responder "sim" → prossiga para o deploy
27426
+ - 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
+ - Se o usuário não responder (timeout) → NÃO faça deploy. Emita \`[SUMMARY] Deploy cancelado por falta de resposta.\` e encerre.
27428
+
27429
+ ### 5. Fazer deploy
27430
+ 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
+
27432
+ **5a. Dry-run primeiro (OBRIGATÓRIO)**
27433
+ ANTES de fazer o deploy real, você DEVE rodar um dry-run para validar a configuração sem enviar nada:
27434
+
27435
+ \`\`\`
27436
+ veloz:deploy { "dryRun": true }
27437
+ \`\`\`
27438
+
27439
+ O dry-run é **local-only** — ele NÃO autentica, NÃO chama API, NÃO faz upload, NÃO cria serviço, NÃO gera veloz.json, NÃO consome quota. Ele apenas:
27440
+ - Detecta o framework e lista os serviços que seriam criados
27441
+ - Calcula o tamanho do tarball a ser enviado (MB)
27442
+ - Mostra build/start/porta/output-dir que seriam usados
27443
+ - Reporta warnings de pre-deploy (arquivos suspeitos, configs faltando, etc.)
27444
+
27445
+ Use o resultado do dry-run para:
27446
+ - Confirmar que o framework/app/serviço detectado está correto
27447
+ - Verificar se o tamanho do upload é razoável (>100MB é flag vermelha — provavelmente falta .dockerignore/.gitignore excluindo node_modules/dist/.next)
27448
+ - Pegar ajustes óbvios (build/start errado, porta errada, monorepo path errado) ANTES de gastar tempo num build real
27449
+
27450
+ **Como corrigir problemas encontrados no dry-run:**
27451
+ Porque o dry-run NÃO gera veloz.json e NÃO cria serviço no servidor, \`veloz:config_set\` NÃO funciona antes do primeiro deploy real (ele precisa de um serviço existente). As correções pré-primeiro-deploy só podem ser feitas via **arquivos locais do projeto**:
27452
+
27453
+ - **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
+ - **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)
27456
+ - **Porta errada** → geralmente o framework define; se precisa customizar, edite config do framework (next.config, vite.config) OU use Dockerfile com \`EXPOSE\`
27457
+ - **Dockerfile não está sendo usado quando deveria** → garanta que o path do Dockerfile está onde o detector procura (raiz do app)
27458
+
27459
+ Repita \`veloz:deploy { "dryRun": true }\` depois de cada correção para confirmar. Só prossiga para 5b quando o dry-run estiver limpo.
27460
+
27461
+ **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
+
27463
+ Para o dry-run, use as mesmas flags de escopo do deploy real (\`app\`, \`service\`, \`all\`):
27464
+ \`\`\`
27465
+ veloz:deploy { "dryRun": true, "app": "apps/web" }
27466
+ veloz:deploy { "dryRun": true, "service": "nome-servico" }
27467
+ \`\`\`
27468
+
27469
+ **5b. Deploy real**
27470
+ Depois que o dry-run passou limpo, faça o deploy real:
27237
27471
 
27238
27472
  \`\`\`
27239
27473
  veloz:deploy { "yes": true }
27240
27474
  \`\`\`
27241
27475
 
27242
- O deploy via MCP:
27476
+ Para monorepos ou deploy de serviço específico:
27477
+ - \`veloz:deploy { "yes": true, "app": "apps/web" }\` — deploy de um app específico do monorepo
27478
+ - \`veloz:deploy { "yes": true, "service": "nome-servico" }\` — re-deploy de serviço existente
27479
+
27480
+ O deploy via MCP, mesmo sem veloz.json, faz tudo de ponta-a-ponta:
27243
27481
  1. Detecta o framework e o tipo de serviço (web, static, worker)
27244
- 2. Cria o projeto e o serviço na plataforma
27482
+ 2. Cria o projeto e o serviço na plataforma (se ainda não existirem)
27245
27483
  3. Gera o arquivo veloz.json com a configuração correta
27246
27484
  4. Faz upload do código e inicia o build
27247
27485
  5. Retorna eventos de progresso e o resultado final
27248
27486
 
27249
- Para monorepos ou deploy de serviço específico:
27250
- - \`veloz:deploy { "yes": true, "app": "apps/web" }\` — deploy de um app específico do monorepo
27251
- - \`veloz:deploy { "yes": true, "service": "nome-servico" }\` — re-deploy de serviço existente
27252
-
27253
27487
  IMPORTANTE:
27254
- - SEMPRE use veloz:deploy (MCP), NUNCA \`veloz deploy\` via Bash. O Bash pode travar esperando input interativo.
27255
- - Se o MCP falhar com erro de conexão ou timeout, sim tente via Bash com \`veloz deploy --yes\` como fallback único.
27488
+ - 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 é para o passo 5a.
27490
+ - SEMPRE use veloz:deploy (MCP), inclusive no primeiro deploy. NUNCA \`veloz deploy\` via Bash — o Bash pode travar esperando input interativo.
27491
+ - 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.
27256
27492
  - NUNCA crie ou edite o arquivo veloz.json manualmente. Ele é gerado automaticamente pelo CLI no primeiro deploy.
27257
27493
  - Após o primeiro deploy, se precisar ajustar configurações, use \`veloz:config_set\`.
27258
27494
 
27259
- ### 4. Monitorar e diagnosticar
27495
+ ### 6. Monitorar e diagnosticar
27260
27496
  Após o deploy, use as ferramentas MCP para verificar o resultado:
27261
27497
  - veloz:builds_logs — logs completos do build (stdout + stderr)
27262
27498
  - veloz:builds_show — status detalhado do build
27263
27499
  - veloz:logs_search — logs de runtime do serviço (após o serviço iniciar)
27264
27500
  - veloz:domains_list — ver URLs/domínios disponíveis
27265
27501
 
27266
- ### 5. Auto-healing: quando algo falhar
27267
- Você é um engenheiro experiente. Quando um deploy falhar:
27502
+ ### 6b. Health check pós-deploy (OBRIGATÓRIO quando o deploy retorna LIVE)
27503
+ 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
+
27505
+ **Passo 0 — Desenhar um plano de testes (OBRIGATÓRIO antes de testar)**
27506
+ ANTES de rodar qualquer curl/logs/metrics, você DEVE escrever um plano de testes explícito e emitir para o usuário. O plano deve ser específico ao projeto que acabou de ser deployado (framework, rotas conhecidas, dependências), não um template genérico. Formato:
27507
+
27508
+ \`\`\`
27509
+ [TESTING_PLAN]
27510
+ Plano de health check para <nome-do-serviço> (<framework>):
27511
+
27512
+ HTTP checks:
27513
+ 1. GET <url-base>/ — esperar 2xx/3xx
27514
+ 2. GET <url-base>/<rota-de-health-conhecida-se-houver> — esperar 200
27515
+ 3. GET <url-base>/<rota-principal-da-app> — esperar 2xx
27516
+ (adicione rotas específicas que você identificou na análise do projeto, ex: /api/health, /_health, /ping)
27517
+
27518
+ Logs (janela: últimos 2min):
27519
+ - queries de erro: level:error, "Error:", "TypeError", "ECONNREFUSED", "panic", framework-specific
27520
+ - amostra geral para confirmar tráfego/startup
27521
+
27522
+ Métricas (janela: últimos 5min):
27523
+ - CPU: < 90% sustentado
27524
+ - Memória: < 80% do size preset (<preset atual>)
27525
+ - Instâncias reportando samples
27526
+
27527
+ Critérios de sucesso:
27528
+ - Todos HTTP checks em 2xx/3xx
27529
+ - Zero erros repetidos nos logs (warnings OK)
27530
+ - Métricas dentro dos limites
27531
+
27532
+ Critérios de falha → ação:
27533
+ - 5xx/timeout no HTTP → ler logs, classificar categoria A/B (seção 7)
27534
+ - Erros repetidos nos logs → classificar e corrigir (máx 3 iterações)
27535
+ - CPU sustentado alto sem tráfego → suspeitar crash loop, ler logs
27536
+ \`\`\`
27537
+
27538
+ Emita esse bloco COMPLETO, com as rotas/thresholds específicos, ANTES de rodar os checks. Depois, execute o plano na ordem listada abaixo.
27539
+
27540
+ **1) HTTP check via curl (Bash)**
27541
+ Use a URL retornada em \`urls\` (ou \`dashboardUrl\`) pelo \`veloz:deploy\`. Teste com curl — silencioso, só pegando status code, corpo e tempo:
27542
+
27543
+ \`\`\`bash
27544
+ curl -sS -o /tmp/veloz-health.txt -w "HTTP %{http_code} · %{time_total}s · %{size_download}B\\n" -m 15 "<url>"
27545
+ \`\`\`
27546
+
27547
+ Aguarde ~5 segundos antes do primeiro curl (tempo para o serviço terminar de subir). Se o status for:
27548
+ - **2xx** → OK, prossiga para o passo 2
27549
+ - **3xx** → siga o redirect com \`-L\` e re-teste; se continuar 3xx, OK
27550
+ - **4xx** (exceto 401/403 que podem ser esperados para rotas autenticadas) → tente \`/\` e \`/health\` / \`/api/health\` se disponível; reporte como aviso mas não falhe
27551
+ - **5xx ou timeout** → **falha**. Leia \`/tmp/veloz-health.txt\` para ver o corpo da resposta, vá direto para o passo 2 (logs) e diagnóstico
27552
+
27553
+ **2) Checar logs de runtime (veloz:logs_search)**
27554
+ Procure erros nos últimos 2 minutos:
27555
+
27556
+ \`\`\`
27557
+ veloz:logs_search { "query": "level:error OR level:fatal OR \\"Error:\\" OR \\"TypeError\\" OR \\"ECONNREFUSED\\" OR \\"panic\\"", "since": "2m", "limit": 50 }
27558
+ \`\`\`
27559
+
27560
+ Depois, pegue uma amostra geral para confirmar que o app está servindo requests:
27561
+
27562
+ \`\`\`
27563
+ veloz:logs_search { "query": "*", "since": "2m", "limit": 30 }
27564
+ \`\`\`
27565
+
27566
+ Se aparecerem erros repetidos (crash loop, exceções não tratadas, \`ECONNREFUSED\` em deps, migration falhando) → **falha**. Se só aparecem warnings ou os logs mostram o app servindo requests normalmente → OK.
27567
+
27568
+ **3) Checar métricas (veloz:metrics_show ou veloz:metrics_query)**
27569
+ Verifique CPU e memória nos últimos 5 minutos:
27570
+
27571
+ \`\`\`
27572
+ veloz:metrics_show { "since": "5m" }
27573
+ \`\`\`
27574
+
27575
+ Flags de alerta:
27576
+ - CPU sustentado > 90% sem tráfego real → provavelmente crash loop ou hot loop
27577
+ - Memória subindo rápido ou próxima do limite do size preset → memory leak ou size subdimensionado
27578
+ - Zero samples retornadas → serviço pode não estar reportando métricas (serviço não subiu)
27579
+
27580
+ **Classificando o resultado do health check:**
27268
27581
 
27269
- 1. **Leia os logs**: Use veloz:builds_logs para ver o erro do build, ou veloz:logs_search para erros de runtime
27270
- 2. **Diagnostique**: Identifique a causa raiz
27271
- 3. **Corrija**: Ajuste a configuração via \`veloz:config_set\` ele aceita qualquer propriedade do serviço (build, start, port, preStart, size, instances, etc.). Consulte o output de \`veloz --llms-full\` para ver todas as opções disponíveis.
27272
- 4. **Re-deploy**: Use veloz:deploy para tentar novamente (até 3 tentativas)
27582
+ Se TODOS os 3 checks passaram (HTTP 2xx/3xx, sem erros nos logs, métricas saudáveis):
27583
+ - Emita \`[HEALTH_OK]\` com um resumo curto em pt-BR: URL testada, status HTTP, amostra de logs, CPU/memória atuais
27584
+ - Encerre com \`[SUMMARY] Deploy concluído e saudável.\`
27273
27585
 
27274
- Se o build falhar, teste localmente com Bash para ver o erro completo antes de ajustar.
27586
+ Se qualquer check falhou:
27587
+ - Classifique como categoria A (misconfig) ou B (código do usuário) seguindo a seção 7
27588
+ - 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
+ - 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
+ - Máximo 3 iterações de health check + fix. Depois disso, emita \`[DEPLOY_UNHEALTHY]\` com o que foi tentado e o estado atual.
27591
+
27592
+ **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
+
27594
+ ### 7. Classificar falhas e decidir o próximo passo
27595
+ 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
+
27597
+ **A) Misconfiguração (responsabilidade do wizard — corrija você mesmo)**
27598
+ Erros onde o código do usuário está correto, mas a Veloz não sabe como construir/iniciar o app. Exemplos:
27599
+ - Comando de build errado ou faltando (ex: \`pnpm run build\` em um projeto sem esse script)
27600
+ - Comando de start errado (ex: porta errada, \`npm start\` sem \`start\` script)
27601
+ - Porta incorreta configurada no serviço
27602
+ - Package manager errado detectado (usando npm num repo pnpm)
27603
+ - Workspace/monorepo path incorreto
27604
+ - Falta de \`preStart\` para migrações de banco
27605
+ - Nixpacks não detectou o framework corretamente
27606
+
27607
+ → 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
+
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 JÁ existe um Dockerfile no repo e você estava ignorando — pare, use o existente.
27610
+
27611
+ **B) Falha de testes do usuário (responsabilidade do usuário — ÚNICO caso de handoff)**
27612
+ 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).
27613
+
27614
+ Para QUALQUER outro tipo de falha — incluindo TypeScript compile errors, erros de sintaxe, módulos faltando, runtime crashes, falhas em scripts \`postinstall\`/\`prebuild\`, variáveis de ambiente ausentes — você DEVE tratar como categoria A (tentar corrigir via \`veloz:config_set\`, ajustar build/start, adicionar env vars ausentes via \`wizard-tools:prompt_user\`, ou cair no Dockerfile como último recurso). NÃO emita \`[BUILD_FAILED_USER_CODE]\` para esses casos — o wizard existe pra resolvê-los.
27615
+
27616
+ → Ação (somente falha de testes): NÃO tente corrigir o código nem desabilitar os testes. Emita o signal abaixo e encerre:
27617
+
27618
+ \`\`\`
27619
+ [BUILD_FAILED_USER_CODE]
27620
+ Motivo: <explicação curta e clara do erro em pt-BR>
27621
+
27622
+ Detalhes:
27623
+ <trecho dos logs com o erro, arquivo:linha quando disponível>
27624
+
27625
+ Como corrigir:
27626
+ - <passo 1 acionável>
27627
+ - <passo 2 acionável>
27628
+ - <comando sugerido para reproduzir local, ex: pnpm build>
27629
+
27630
+ Após corrigir, execute \`veloz deploy\` novamente.
27631
+ \`\`\`
27632
+
27633
+ Depois de emitir \`[BUILD_FAILED_USER_CODE]\`, PARE. Não re-deploye, não tente editar o código-fonte do usuário, não chame mais ferramentas. Apenas encerre a sessão.
27634
+
27635
+ **Regra de ouro**: Se você não tem certeza em qual categoria o erro cai, teste localmente com \`pnpm build\` (ou equivalente) via Bash. Se o build local falha com testes → B (falha de testes). Caso contrário → A (misconfiguração, fixável pelo wizard).
27636
+
27637
+ ## Edits em arquivos existentes do usuário — SEMPRE peça permissão primeiro
27638
+
27639
+ Se em algum momento você precisar **criar, editar ou sobrescrever um arquivo que já existe no projeto do usuário** (package.json, next.config.*, vite.config.*, Dockerfile existente, .env, tsconfig.json, código-fonte em src/, scripts customizados, qualquer arquivo versionado), você DEVE pedir permissão explícita via \`wizard-tools:prompt_user\` com \`type: "confirm"\` ANTES de chamar Write/Edit.
27640
+
27641
+ Formato da pergunta:
27642
+
27643
+ \`\`\`
27644
+ wizard-tools:prompt_user {
27645
+ "question": "Preciso editar <caminho/do/arquivo> para <motivo em 1 linha>. Posso aplicar a mudança?\\n\\nO que vai mudar:\\n- <resumo curto da alteração>\\n- <antes → depois se útil>",
27646
+ "type": "confirm"
27647
+ }
27648
+ \`\`\`
27649
+
27650
+ Regras:
27651
+ - Se o usuário responder "sim" → aplique a edição
27652
+ - 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
27654
+ - 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
+
27656
+ 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.
27275
27657
 
27276
27658
  ## Ferramentas Helper (wizard-tools MCP)
27277
27659
  - wizard-tools:check_env_keys — verificar quais env vars existem sem revelar valores
@@ -27279,12 +27661,19 @@ Se o build falhar, teste localmente com Bash para ver o erro completo antes de a
27279
27661
  - wizard-tools:set_helper_content — exibir diagramas e conteúdo visual no painel esquerdo
27280
27662
  - wizard-tools:prompt_user — perguntar algo ao usuário interativamente
27281
27663
 
27282
- ## Diagrama Visual
27283
- ANTES de chamar o deploy, chame wizard-tools:set_helper_content com um diagrama ASCII mostrando:
27664
+ ## 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.
27666
+
27667
+ O diagrama deve mostrar:
27284
27668
  - O framework/tecnologia do projeto
27285
- - O pipeline (source → build → deploy)
27669
+ - O pipeline (código → build → serviço)
27286
27670
  - A porta e URL onde o serviço ficará disponível
27287
- - Serviços auxiliares (banco de dados, etc.) se houver
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 (→ ↓ ▼)
27676
+ - Uma tarefa do TodoWrite DEVE representar esse passo (ver checklist abaixo)
27288
27677
 
27289
27678
  ## Reportar Progresso
27290
27679
  Use a ferramenta TodoWrite para manter uma lista de tarefas atualizada:
@@ -27292,12 +27681,17 @@ Use a ferramenta TodoWrite para manter uma lista de tarefas atualizada:
27292
27681
  - Mude para "in_progress" quando começar cada etapa
27293
27682
  - Mude para "completed" quando terminar
27294
27683
 
27295
- Tarefas típicas:
27684
+ Tarefas típicas (inclua TODAS, na ordem):
27296
27685
  1. "Aprender CLI" — ler veloz --llms-full
27297
- 2. "Analisar projeto" — ler package.json, configs, estrutura
27686
+ 2. "Analisar projeto" — ler package.json, configs, estrutura, procurar Dockerfile existente
27298
27687
  3. "Configurar variáveis de ambiente" — env vars no servidor
27299
- 4. "Fazer deploy" — enviar código e build
27300
- 5. "Verificar resultado" — confirmar que está no ar
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
27301
27695
 
27302
27696
  ## Regras CRÍTICAS — Uso de Ferramentas
27303
27697
  - SEMPRE use ferramentas MCP (veloz:*) para TODAS as operações da plataforma. NUNCA use Bash para rodar comandos \`veloz\` exceto:
@@ -27309,10 +27703,11 @@ Tarefas típicas:
27309
27703
 
27310
27704
  ## Regras Gerais
27311
27705
  - Comunique-se em português (pt-BR)
27312
- - Não modifique código fonte do app a não ser que seja estritamente necessário para o deploy funcionar
27706
+ - 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.
27313
27707
  - NUNCA crie veloz.json manualmente — o CLI gera automaticamente
27708
+ - SEMPRE peça confirmação explícita via wizard-tools:prompt_user (type="confirm") antes de chamar veloz:deploy
27314
27709
  - Para env vars sensíveis, use wizard-tools:prompt_user — nunca invente valores
27315
- - Se algo falhar, DIAGNOSTIQUE E CORRIJA não desista facilmente
27710
+ - Classifique falhas de build: misconfiguração corrija e retente; erro de código do usuário → emita [BUILD_FAILED_USER_CODE] e PARE
27316
27711
  - NUNCA rode comandos de dev server (next dev, vite, nodemon, npm run dev) — isso trava o processo
27317
27712
  - NUNCA exponha secrets nos seus logs/mensagens
27318
27713
  - Não use terminologia de infraestrutura interna (Kubernetes, pods, containers) — use "serviço", "instância", "build"
@@ -27345,9 +27740,9 @@ function getCommandments() {
27345
27740
 
27346
27741
  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.
27347
27742
 
27348
- 8. **Não modifique código fonte**: Apenas modifique arquivos de configuração (.env, next.config.*, package.json scripts). Não altere componentes, rotas, ou lógica do app. Use \`veloz:config_set\` para ajustar configurações do serviço nunca edite veloz.json diretamente.
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 só 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.
27349
27744
 
27350
- 9. **Auto-healing obrigatório**: Se um deploy falhar, NÃO desista. Leia os logs (veloz:builds_logs), diagnostique, corrija, e tente novamente. Máximo de 3 tentativas.
27745
+ 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.
27351
27746
 
27352
27747
  10. **Build local primeiro**: Quando possível, teste o build localmente (via Bash: pnpm build / npm run build) ANTES de fazer deploy. Isso economiza tempo e créditos.
27353
27748
 
@@ -27357,7 +27752,9 @@ function getCommandments() {
27357
27752
 
27358
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).
27359
27754
 
27360
- 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. Use type="confirm" para confirmações antes do deploy.
27755
+ 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
+
27757
+ 18. **Confirmação obrigatória antes do deploy**: ANTES de chamar \`veloz:deploy\` (primeiro deploy ou re-deploy), você DEVE pedir confirmação explícita ao usuário com \`wizard-tools:prompt_user\` \`type="confirm"\`. A pergunta precisa resumir o que será deployado (framework, app path, build/start, banco/volumes, env vars). Se o usuário responder "não" ou não responder, NÃO faça deploy — emita \`[SUMMARY] Deploy cancelado pelo usuário.\` e encerre. Exceção única: re-deploy de auto-healing de **misconfiguração** (categoria A da regra 9) pode ser feito sem nova confirmação até o limite de 3 tentativas.
27361
27758
 
27362
27759
  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.
27363
27760
 
@@ -27365,16 +27762,227 @@ function getCommandments() {
27365
27762
  `.trim();
27366
27763
  }
27367
27764
 
27765
+ //#endregion
27766
+ //#region src/wizard/debug-logger.ts
27767
+ /**
27768
+ * Per-session debug logger for `veloz init`.
27769
+ *
27770
+ * Writes a folder under `~/.veloz/logs/init-<timestamp>-<id>/` with
27771
+ * structured files capturing every interesting event in the wizard run,
27772
+ * so failures can be diagnosed after the fact.
27773
+ *
27774
+ * Files:
27775
+ * session.log — human-readable timeline (append-only)
27776
+ * events.jsonl — structured events (category, timestamp, data)
27777
+ * sdk-messages.jsonl — raw SDK messages from the Agent SDK
27778
+ * tool-calls.jsonl — tool_use / tool_result pairs
27779
+ * store-events.jsonl — WizardStore state transitions
27780
+ * build-stream.jsonl — build log streaming events
27781
+ * mcp-tools.jsonl — wizard-tools MCP server invocations
27782
+ * prompts.md — initial + continuation prompts
27783
+ * meta.json — metadata (cwd, framework, user, outcome, timings)
27784
+ *
27785
+ * The logger never throws — disk failures are swallowed. MCP mode is
27786
+ * unaffected because writes only go to the session folder, never stdout.
27787
+ */
27788
+ var SessionLogger = class {
27789
+ dir;
27790
+ sessionId;
27791
+ startedAt;
27792
+ meta;
27793
+ constructor(config) {
27794
+ this.startedAt = Date.now();
27795
+ const now = new Date(this.startedAt);
27796
+ this.sessionId = `init-${now.toISOString().replace(/[:.]/g, "-")}-${randomBytes(3).toString("hex")}`;
27797
+ this.dir = join(config.root ?? join(homedir(), ".veloz", "logs"), this.sessionId);
27798
+ try {
27799
+ mkdirSync(this.dir, { recursive: true });
27800
+ } catch {}
27801
+ this.meta = {
27802
+ sessionId: this.sessionId,
27803
+ startedAt: now.toISOString(),
27804
+ cwd: config.cwd,
27805
+ node: process.version,
27806
+ platform: process.platform,
27807
+ pid: process.pid
27808
+ };
27809
+ this.writeMeta();
27810
+ this.log("session", "wizard session started", {
27811
+ cwd: config.cwd,
27812
+ pid: process.pid,
27813
+ node: process.version
27814
+ });
27815
+ }
27816
+ /** Append a line to session.log and a structured event to events.jsonl. */
27817
+ log(category, message, data) {
27818
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
27819
+ const text = data === void 0 ? message : `${message} ${safeStringify(data)}`;
27820
+ this.append("session.log", `[${ts}] [${category}] ${text}\n`);
27821
+ this.appendJsonl("events.jsonl", {
27822
+ ts,
27823
+ category,
27824
+ message,
27825
+ data
27826
+ });
27827
+ }
27828
+ logSdkMessage(message) {
27829
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
27830
+ this.appendJsonl("sdk-messages.jsonl", {
27831
+ ts,
27832
+ message
27833
+ });
27834
+ }
27835
+ logToolUse(name, input, toolUseId, extra) {
27836
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
27837
+ this.appendJsonl("tool-calls.jsonl", {
27838
+ ts,
27839
+ kind: "tool_use",
27840
+ name,
27841
+ toolUseId,
27842
+ input,
27843
+ ...extra ?? {}
27844
+ });
27845
+ this.append("session.log", `[${ts}] [tool_use] ${name}${toolUseId ? ` (${toolUseId})` : ""}\n`);
27846
+ }
27847
+ logToolResult(toolUseId, content, isError, extra) {
27848
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
27849
+ this.appendJsonl("tool-calls.jsonl", {
27850
+ ts,
27851
+ kind: "tool_result",
27852
+ toolUseId,
27853
+ isError,
27854
+ content,
27855
+ ...extra ?? {}
27856
+ });
27857
+ this.append("session.log", `[${ts}] [tool_result] ${toolUseId}${isError ? " ERROR" : ""}\n`);
27858
+ }
27859
+ logStoreEvent(event, data) {
27860
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
27861
+ this.appendJsonl("store-events.jsonl", {
27862
+ ts,
27863
+ event,
27864
+ data
27865
+ });
27866
+ }
27867
+ logBuildStream(event, data) {
27868
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
27869
+ this.appendJsonl("build-stream.jsonl", {
27870
+ ts,
27871
+ event,
27872
+ data
27873
+ });
27874
+ this.append("session.log", `[${ts}] [build-stream] ${event}\n`);
27875
+ }
27876
+ logPrompt(kind, body, context$2) {
27877
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
27878
+ const header = `\n\n---\n## [${ts}] ${kind} prompt\n`;
27879
+ const ctx = context$2 ? `\n<context>\n${safeStringify(context$2)}\n</context>\n\n` : "\n";
27880
+ this.append("prompts.md", `${header}${ctx}${body}\n`);
27881
+ this.appendJsonl("events.jsonl", {
27882
+ ts,
27883
+ category: "prompt",
27884
+ kind,
27885
+ bytes: body.length
27886
+ });
27887
+ }
27888
+ updateMeta(patch) {
27889
+ this.meta = {
27890
+ ...this.meta,
27891
+ ...patch
27892
+ };
27893
+ this.writeMeta();
27894
+ }
27895
+ finalize(outcome, info$1) {
27896
+ const endedAt = Date.now();
27897
+ this.updateMeta({
27898
+ outcome,
27899
+ endedAt: new Date(endedAt).toISOString(),
27900
+ durationMs: endedAt - this.startedAt,
27901
+ ...info$1 ?? {}
27902
+ });
27903
+ this.log("session", `wizard session finished: ${outcome}`, info$1);
27904
+ }
27905
+ writeMeta() {
27906
+ try {
27907
+ writeFileSync(join(this.dir, "meta.json"), JSON.stringify(this.meta, null, 2));
27908
+ } catch {}
27909
+ }
27910
+ append(file, text) {
27911
+ try {
27912
+ appendFileSync(join(this.dir, file), text);
27913
+ } catch {}
27914
+ }
27915
+ appendJsonl(file, obj) {
27916
+ try {
27917
+ appendFileSync(join(this.dir, file), safeStringify(obj) + "\n");
27918
+ } catch {}
27919
+ }
27920
+ };
27921
+ /**
27922
+ * Safely stringify any value, falling back to a diagnostic shape when the
27923
+ * value contains circular references or non-serializable content.
27924
+ */
27925
+ function safeStringify(value) {
27926
+ try {
27927
+ return JSON.stringify(value, replacer);
27928
+ } catch (err) {
27929
+ return JSON.stringify({
27930
+ __unserializable: true,
27931
+ reason: err instanceof Error ? err.message : String(err),
27932
+ preview: String(value).slice(0, 500)
27933
+ });
27934
+ }
27935
+ }
27936
+ function replacer(_key, value) {
27937
+ if (value instanceof Error) return {
27938
+ name: value.name,
27939
+ message: value.message,
27940
+ stack: value.stack
27941
+ };
27942
+ if (typeof value === "bigint") return value.toString();
27943
+ if (typeof value === "function") return `[Function ${value.name || "anonymous"}]`;
27944
+ return value;
27945
+ }
27946
+ let active = null;
27947
+ function initSessionLogger(config) {
27948
+ active = new SessionLogger(config);
27949
+ return active;
27950
+ }
27951
+ /** Returns the active logger, or a no-op logger if none was initialized. */
27952
+ function getSessionLogger() {
27953
+ return active ?? noopLogger;
27954
+ }
27955
+ function getSessionLogDir() {
27956
+ return active?.dir ?? null;
27957
+ }
27958
+ const noopLogger = {
27959
+ dir: "",
27960
+ sessionId: "",
27961
+ log: () => {},
27962
+ logSdkMessage: () => {},
27963
+ logToolUse: () => {},
27964
+ logToolResult: () => {},
27965
+ logStoreEvent: () => {},
27966
+ logBuildStream: () => {},
27967
+ logPrompt: () => {},
27968
+ updateMeta: () => {},
27969
+ finalize: () => {}
27970
+ };
27971
+
27368
27972
  //#endregion
27369
27973
  //#region src/wizard/agent/agent-tools.ts
27370
27974
  /**
27975
+ * Small in-process MCP server for sensitive wizard operations.
27976
+ * Only env var safety tools — everything else the agent does directly.
27977
+ */
27978
+ /**
27371
27979
  * Create the wizard-tools MCP server definition.
27372
27980
  * Returns a tools map that the agent SDK can consume as an in-process MCP server.
27373
27981
  */
27374
27982
  function createWizardToolsServer(config) {
27375
27983
  return {
27376
27984
  command: "node",
27377
- args: ["-e", getWizardToolsScript(config.workingDirectory)],
27985
+ args: ["-e", getWizardToolsScript(config.workingDirectory, getSessionLogDir())],
27378
27986
  env: {}
27379
27987
  };
27380
27988
  }
@@ -27382,13 +27990,26 @@ function createWizardToolsServer(config) {
27382
27990
  * Generate a self-contained Node.js script that implements the wizard-tools MCP server.
27383
27991
  * This runs as a child process communicating via stdio JSON-RPC.
27384
27992
  */
27385
- function getWizardToolsScript(workingDirectory) {
27993
+ function getWizardToolsScript(workingDirectory, logDir) {
27386
27994
  return `
27387
27995
  'use strict';
27388
27996
  const fs = require('fs');
27389
27997
  const path = require('path');
27390
27998
 
27391
27999
  const CWD = '${workingDirectory.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}';
28000
+ const LOG_DIR = '${logDir ? logDir.replace(/\\/g, "\\\\").replace(/'/g, "\\'") : ""}';
28001
+
28002
+ function logEvent(event, data) {
28003
+ if (!LOG_DIR) return;
28004
+ try {
28005
+ const line = JSON.stringify({ ts: new Date().toISOString(), event, data }) + '\\n';
28006
+ fs.appendFileSync(path.join(LOG_DIR, 'mcp-tools.jsonl'), line);
28007
+ } catch {}
28008
+ }
28009
+
28010
+ logEvent('server_start', { cwd: CWD, pid: process.pid });
28011
+ process.on('exit', (code) => logEvent('server_exit', { code }));
28012
+ process.on('uncaughtException', (err) => logEvent('uncaught_exception', { message: String(err && err.message), stack: String(err && err.stack) }));
27392
28013
 
27393
28014
  const TOOLS = {
27394
28015
  check_env_keys: {
@@ -27434,7 +28055,7 @@ const TOOLS = {
27434
28055
  },
27435
28056
  },
27436
28057
  set_helper_content: {
27437
- description: 'Exibir conteúdo customizado no painel esquerdo da TUI do wizard. Use para mostrar diagramas de deploy, explicações visuais, ou informações contextuais sobre o projeto. Cada chamada SUBSTITUI o conteúdo anterior. Use linhas vazias e caracteres ASCII para criar diagramas visuais.',
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.',
27438
28059
  inputSchema: {
27439
28060
  type: 'object',
27440
28061
  properties: {
@@ -27602,13 +28223,12 @@ async function handlePromptUser(input) {
27602
28223
  };
27603
28224
  }
27604
28225
 
27605
- // JSON-RPC over stdio with Content-Length framing
27606
- // The MCP protocol uses HTTP-style Content-Length headers:
27607
- // Content-Length: N\\r\\n\\r\\n{json}
28226
+ // MCP stdio transport: newline-delimited JSON (one JSON-RPC message per line).
28227
+ // Not LSP Content-Length framing — the Claude Agent SDK rejects that and marks
28228
+ // the server as "failed", which is exactly what broke the wizard diagram/prompt.
27608
28229
 
27609
28230
  function send(obj) {
27610
- const json = JSON.stringify(obj);
27611
- process.stdout.write('Content-Length: ' + Buffer.byteLength(json) + '\\r\\n\\r\\n' + json);
28231
+ process.stdout.write(JSON.stringify(obj) + '\\n');
27612
28232
  }
27613
28233
 
27614
28234
  async function handleJsonRpc(msg) {
@@ -27639,68 +28259,45 @@ async function handleJsonRpc(msg) {
27639
28259
  } else if (msg.method === 'tools/call') {
27640
28260
  const name = msg.params.name;
27641
28261
  const args = msg.params.arguments || {};
28262
+ logEvent('tool_call_start', { name, args });
27642
28263
  let result;
27643
- if (name === 'check_env_keys') result = handleCheckEnvKeys(args);
27644
- else if (name === 'set_env_values') result = handleSetEnvValues(args);
27645
- else if (name === 'set_helper_content') result = { content: [{ type: 'text', text: 'Conteúdo do helper atualizado com ' + (args.lines || []).length + ' linhas.' }] };
27646
- else if (name === 'prompt_user') result = await handlePromptUser(args);
27647
- else result = { content: [{ type: 'text', text: 'Ferramenta desconhecida: ' + name }], isError: true };
28264
+ try {
28265
+ if (name === 'check_env_keys') result = handleCheckEnvKeys(args);
28266
+ 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.' }] };
28268
+ else if (name === 'prompt_user') result = await handlePromptUser(args);
28269
+ else result = { content: [{ type: 'text', text: 'Ferramenta desconhecida: ' + name }], isError: true };
28270
+ logEvent('tool_call_done', { name, isError: Boolean(result && result.isError) });
28271
+ } catch (err) {
28272
+ logEvent('tool_call_error', { name, message: String(err && err.message), stack: String(err && err.stack) });
28273
+ result = { content: [{ type: 'text', text: 'Erro: ' + String(err && err.message) }], isError: true };
28274
+ }
27648
28275
  send({ jsonrpc: '2.0', id: msg.id, result });
27649
28276
  } else if (msg.id) {
27650
28277
  send({ jsonrpc: '2.0', id: msg.id, result: {} });
27651
28278
  }
27652
28279
  }
27653
28280
 
27654
- // Parse Content-Length framed messages from stdin
27655
- let rawBuffer = Buffer.alloc(0);
27656
- let expectedLength = -1;
27657
-
28281
+ // MCP stdio transport: read newline-delimited JSON-RPC messages from stdin.
28282
+ let stdinBuffer = '';
28283
+ process.stdin.setEncoding('utf-8');
27658
28284
  process.stdin.on('data', (chunk) => {
27659
- rawBuffer = Buffer.concat([rawBuffer, chunk]);
27660
- processBuffer();
27661
- });
27662
-
27663
- function processBuffer() {
27664
- while (rawBuffer.length > 0) {
27665
- if (expectedLength === -1) {
27666
- // Look for Content-Length header
27667
- const headerEnd = rawBuffer.indexOf('\\r\\n\\r\\n');
27668
- if (headerEnd === -1) return; // Need more data
27669
-
27670
- const header = rawBuffer.slice(0, headerEnd).toString('utf-8');
27671
- const match = header.match(/Content-Length:\\s*(\\d+)/i);
27672
- if (match) {
27673
- expectedLength = parseInt(match[1], 10);
27674
- rawBuffer = rawBuffer.slice(headerEnd + 4); // Skip \\r\\n\\r\\n
27675
- } else {
27676
- // No Content-Length header — try line-based JSON (fallback)
27677
- const lineEnd = rawBuffer.indexOf('\\n');
27678
- if (lineEnd === -1) return;
27679
- const line = rawBuffer.slice(0, lineEnd).toString('utf-8').trim();
27680
- rawBuffer = rawBuffer.slice(lineEnd + 1);
27681
- if (line) {
27682
- try {
27683
- const msg = JSON.parse(line);
27684
- handleJsonRpc(msg).catch(() => {});
27685
- } catch {}
27686
- }
27687
- continue;
27688
- }
27689
- }
27690
-
27691
- if (expectedLength >= 0 && rawBuffer.length >= expectedLength) {
27692
- const body = rawBuffer.slice(0, expectedLength).toString('utf-8');
27693
- rawBuffer = rawBuffer.slice(expectedLength);
27694
- expectedLength = -1;
27695
- try {
27696
- const msg = JSON.parse(body);
27697
- handleJsonRpc(msg).catch(() => {});
27698
- } catch {}
27699
- } else {
27700
- return; // Need more data
28285
+ stdinBuffer += chunk;
28286
+ let newlineIdx;
28287
+ while ((newlineIdx = stdinBuffer.indexOf('\\n')) !== -1) {
28288
+ const line = stdinBuffer.slice(0, newlineIdx).trim();
28289
+ stdinBuffer = stdinBuffer.slice(newlineIdx + 1);
28290
+ if (!line) continue;
28291
+ try {
28292
+ const msg = JSON.parse(line);
28293
+ handleJsonRpc(msg).catch((err) => {
28294
+ logEvent('handler_error', { message: String(err && err.message) });
28295
+ });
28296
+ } catch (err) {
28297
+ logEvent('parse_error', { message: String(err && err.message), line: line.slice(0, 200) });
27701
28298
  }
27702
28299
  }
27703
- }
28300
+ });
27704
28301
  `.trim();
27705
28302
  }
27706
28303
 
@@ -27709,147 +28306,335 @@ function processBuffer() {
27709
28306
  /**
27710
28307
  * Build log streamer for the wizard.
27711
28308
  *
27712
- * When the agent triggers `veloz:deploy`, this module polls the API
27713
- * to find the newly created deployment and streams its build logs
27714
- * back to the wizard store for display in the RunScreen.
27715
- */
27716
- /**
27717
- * Start streaming build logs for the most recent deployment.
27718
- *
27719
- * This runs in the background while the agent's `veloz:deploy` MCP call
27720
- * is also streaming logs (to stderr). The wizard picks up the same stream
27721
- * independently so it can render the build progress in the TUI.
27722
- */
27723
- async function startBuildLogStream(config) {
27724
- const { store, cwd } = config;
28309
+ * Triggered by `setBuildDeploymentId` the deploy tool_result already emits
28310
+ * the deployment ID, so we skip the veloz.json round-trip and open the ORPC
28311
+ * log stream directly.
28312
+ *
28313
+ * Runs alongside the tool_result capture in agent-interface.ts (which pushes
28314
+ * raw progress messages). This module provides the live stderr-style build
28315
+ * output from the platform until the deployment reaches a terminal state.
28316
+ */
28317
+ async function streamBuildLogs(config) {
28318
+ const { store, deploymentId } = config;
28319
+ const logger = getSessionLogger();
28320
+ logger.logBuildStream("start", { deploymentId });
28321
+ const client = createWizardClient(store);
28322
+ if (!client) {
28323
+ logger.logBuildStream("no_client", { reason: "missing apiKey" });
28324
+ store.pushBuildLog("[stream] aguardando autenticação para conectar ao build...");
28325
+ return;
28326
+ }
28327
+ store.setBuildStreamActive(true);
27725
28328
  try {
27726
- store.setBuildStreamActive(true);
27727
- const client = await createWizardClient(store, cwd);
27728
- if (!client) {
27729
- store.setBuildStreamActive(false);
27730
- return;
27731
- }
27732
- const serviceIds = getServiceIdsFromConfig(cwd);
27733
- if (serviceIds.length === 0) {
27734
- store.setBuildStreamActive(false);
27735
- return;
27736
- }
27737
- const deployment = await pollForNewDeployment(client, serviceIds);
27738
- if (!deployment) {
27739
- store.setBuildStreamActive(false);
27740
- return;
27741
- }
27742
- store.setBuildDeploymentId(deployment.id);
27743
- store.pushStatus("Build detectado — transmitindo logs...");
27744
- await streamLogs(client, deployment.id, store);
27745
- } catch {} finally {
28329
+ await openStream(client, deploymentId, store);
28330
+ logger.logBuildStream("stream_done", { deploymentId });
28331
+ } catch (err) {
28332
+ const msg = err instanceof Error ? err.message : String(err);
28333
+ logger.logBuildStream("error", { message: msg });
28334
+ store.pushBuildLog(`[stream] erro: ${msg}`);
28335
+ } finally {
27746
28336
  store.setBuildStreamActive(false);
27747
28337
  }
27748
28338
  }
27749
- function createWizardClient(store, cwd) {
28339
+ function createWizardClient(store) {
27750
28340
  const apiKey = store.session.apiKey;
27751
28341
  if (!apiKey) return null;
27752
- const apiUrl = process.env.VELOZ_API_URL || "https://api.onveloz.com";
27753
- let projectId = null;
27754
- const configPath = join(cwd, "veloz.json");
27755
- if (existsSync(configPath)) try {
27756
- projectId = JSON.parse(readFileSync(configPath, "utf-8"))?.project?.id ?? null;
27757
- } catch {}
27758
- return createClient(apiUrl, () => {
28342
+ return createClient(process.env.VELOZ_API_URL || "https://api.onveloz.com", () => {
27759
28343
  const headers = {
27760
28344
  Authorization: `Bearer ${apiKey}`,
27761
28345
  "User-Agent": "veloz-cli/wizard"
27762
28346
  };
27763
28347
  if (store.session.orgId) headers["X-Organization-Id"] = store.session.orgId;
27764
- if (projectId) headers["X-Project-Id"] = projectId;
27765
28348
  return headers;
27766
28349
  });
27767
28350
  }
27768
- function getServiceIdsFromConfig(cwd) {
27769
- const configPath = join(cwd, "veloz.json");
27770
- if (!existsSync(configPath)) return [];
28351
+ async function openStream(client, deploymentId, store) {
28352
+ const logger = getSessionLogger();
28353
+ logger.logBuildStream("open_stream", { deploymentId });
27771
28354
  try {
27772
- const raw = JSON.parse(readFileSync(configPath, "utf-8"));
27773
- const services = [];
27774
- if (raw?.service?.id) services.push(raw.service.id);
27775
- if (raw?.services && typeof raw.services === "object") for (const key of Object.keys(raw.services)) {
27776
- const svc = raw.services[key];
27777
- if (svc?.id) services.push(svc.id);
27778
- }
27779
- return services;
27780
- } catch {
27781
- return [];
27782
- }
27783
- }
27784
- async function pollForNewDeployment(client, serviceIds, maxWaitMs = 3e4) {
27785
- const startTime = Date.now();
27786
- const pollInterval = 2e3;
27787
- while (Date.now() - startTime < maxWaitMs) {
27788
- for (const serviceId of serviceIds) try {
27789
- const deployments = await client.deployments.list({
27790
- serviceId,
27791
- limit: 1,
27792
- offset: 0
27793
- });
27794
- if (deployments.length > 0) {
27795
- const d = deployments[0];
27796
- const status = d.status;
27797
- if (status === "QUEUED" || status === "BUILDING" || status === "DEPLOYING") return {
27798
- id: d.id,
27799
- serviceId,
27800
- status
27801
- };
28355
+ const stream = await client.logs.streamBuildLogs({ deploymentId });
28356
+ for await (const event of stream) {
28357
+ logger.logBuildStream("event", event);
28358
+ if (event.type === "status") {
28359
+ const label = statusLabels[event.content] ?? event.content;
28360
+ store.pushBuildLog(`[status] ${label}`);
28361
+ if (event.content === "LIVE") store.pushStatus("Deploy concluído — serviço ativo!");
28362
+ else if (event.content === "BUILD_FAILED" || event.content === "DEPLOY_FAILED") store.pushStatus(`Deploy falhou: ${label}`);
28363
+ if (TERMINAL_STATUSES.has(event.content)) return;
28364
+ } else if (event.type === "log") for (const line of event.content.split("\n")) {
28365
+ const display = formatLogLine(line);
28366
+ if (display) store.pushBuildLog(display);
27802
28367
  }
27803
- } catch {}
27804
- await sleep$1(pollInterval);
28368
+ }
28369
+ } catch (err) {
28370
+ const msg = err instanceof Error ? err.message : String(err);
28371
+ store.pushBuildLog(`[stream] desconectou: ${msg} — buscando logs salvos`);
28372
+ await fetchSavedLogs(client, deploymentId, store);
27805
28373
  }
27806
- return null;
27807
28374
  }
27808
- async function streamLogs(client, deploymentId, store) {
28375
+ /**
28376
+ * One-shot fetch of persisted build logs — used when the live stream breaks
28377
+ * (e.g. deployment finished before we attached).
28378
+ */
28379
+ async function fetchSavedLogs(client, deploymentId, store) {
27809
28380
  try {
27810
- const stream = await client.logs.streamBuildLogs({ deploymentId });
27811
- for await (const event of stream) if (event.type === "status") {
27812
- const label = statusLabels[event.content] ?? event.content;
27813
- store.pushBuildLog(`[status] ${label}`);
27814
- if (event.content === "LIVE") store.pushStatus("Deploy concluído — serviço ativo!");
27815
- else if (event.content === "BUILD_FAILED" || event.content === "DEPLOY_FAILED") store.pushStatus(`Deploy falhou: ${label}`);
27816
- } else if (event.type === "log") {
27817
- const lines = event.content.split("\n");
27818
- for (const line of lines) {
27819
- const trimmed = line.trim();
27820
- if (!trimmed) continue;
27821
- if (isHiddenMessage(trimmed)) continue;
27822
- const parsed = parseBuildLine(trimmed);
27823
- let displayLine = null;
27824
- switch (parsed.kind) {
27825
- case "step":
27826
- displayLine = `[${parsed.stage} ${parsed.step}/${parsed.total}] ${parsed.command}`;
27827
- break;
27828
- case "cached":
27829
- displayLine = `#${parsed.stepNum} CACHED`;
27830
- break;
27831
- case "done":
27832
- displayLine = `#${parsed.stepNum} done ${parsed.duration}`;
27833
- break;
27834
- case "output":
27835
- displayLine = cleanDisplayLine(parsed.text);
27836
- break;
27837
- case "platform":
27838
- displayLine = cleanDisplayLine(parsed.message);
27839
- break;
27840
- case "other":
27841
- displayLine = cleanDisplayLine(parsed.text);
27842
- break;
27843
- }
27844
- if (displayLine) store.pushBuildLog(displayLine);
27845
- }
28381
+ const logs = await client.logs.getBuildLogs({ deploymentId });
28382
+ if (!logs.buildLogs) return;
28383
+ for (const line of logs.buildLogs.split("\n")) {
28384
+ const display = formatLogLine(line);
28385
+ if (display) store.pushBuildLog(display);
27846
28386
  }
27847
- } catch {}
28387
+ } catch (err) {
28388
+ const msg = err instanceof Error ? err.message : String(err);
28389
+ store.pushBuildLog(`[stream] falha ao buscar logs salvos: ${msg}`);
28390
+ }
27848
28391
  }
27849
- function sleep$1(ms) {
27850
- return new Promise((resolve$1) => {
27851
- setTimeout(resolve$1, ms);
27852
- });
28392
+ function formatLogLine(line) {
28393
+ const trimmed = line.trim();
28394
+ if (!trimmed) return null;
28395
+ if (isHiddenMessage(trimmed)) return null;
28396
+ const parsed = parseBuildLine(trimmed);
28397
+ switch (parsed.kind) {
28398
+ case "step": return `[${parsed.stage} ${parsed.step}/${parsed.total}] ${parsed.command}`;
28399
+ case "cached": return `#${parsed.stepNum} CACHED`;
28400
+ case "done": return `#${parsed.stepNum} done ${parsed.duration}`;
28401
+ case "output": return cleanDisplayLine(parsed.text);
28402
+ case "platform": return cleanDisplayLine(parsed.message);
28403
+ case "other": return cleanDisplayLine(parsed.text);
28404
+ }
28405
+ }
28406
+
28407
+ //#endregion
28408
+ //#region src/wizard/agent/permissions.ts
28409
+ const PACKAGE_MANAGERS = [
28410
+ "npm",
28411
+ "pnpm",
28412
+ "yarn",
28413
+ "bun",
28414
+ "npx",
28415
+ "pnpx",
28416
+ "yarnpkg"
28417
+ ];
28418
+ const SAFE_SCRIPTS = [
28419
+ "install",
28420
+ "add",
28421
+ "ci",
28422
+ "build",
28423
+ "tsc",
28424
+ "typecheck",
28425
+ "type-check",
28426
+ "check-types",
28427
+ "types",
28428
+ "lint",
28429
+ "format",
28430
+ "test"
28431
+ ];
28432
+ const READ_ONLY_COMMANDS = [
28433
+ "ls",
28434
+ "pwd",
28435
+ "cat",
28436
+ "head",
28437
+ "tail",
28438
+ "echo",
28439
+ "which",
28440
+ "node",
28441
+ "env",
28442
+ "printenv",
28443
+ "find",
28444
+ "test",
28445
+ "stat"
28446
+ ];
28447
+ const GIT_READ_SUBCOMMANDS = [
28448
+ "status",
28449
+ "log",
28450
+ "diff",
28451
+ "show",
28452
+ "branch",
28453
+ "remote",
28454
+ "config",
28455
+ "rev-parse",
28456
+ "ls-files"
28457
+ ];
28458
+ /**
28459
+ * Read-only subcommand allowlists for third-party platform CLIs.
28460
+ * The wizard inspects existing deploys (Vercel, Railway, Render, Fly, Cloudflare, AWS)
28461
+ * to migrate config to Veloz — it must never mutate those platforms.
28462
+ */
28463
+ const PLATFORM_READ_SUBCOMMANDS = {
28464
+ vercel: [
28465
+ "env",
28466
+ "project",
28467
+ "projects",
28468
+ "domains",
28469
+ "teams",
28470
+ "whoami",
28471
+ "inspect",
28472
+ "list",
28473
+ "ls",
28474
+ "logs"
28475
+ ],
28476
+ vc: [
28477
+ "env",
28478
+ "project",
28479
+ "projects",
28480
+ "domains",
28481
+ "teams",
28482
+ "whoami",
28483
+ "inspect",
28484
+ "list",
28485
+ "ls",
28486
+ "logs"
28487
+ ],
28488
+ railway: [
28489
+ "status",
28490
+ "variables",
28491
+ "list",
28492
+ "whoami",
28493
+ "environment",
28494
+ "service",
28495
+ "logs"
28496
+ ],
28497
+ render: [
28498
+ "services",
28499
+ "env-groups",
28500
+ "list",
28501
+ "whoami",
28502
+ "logs"
28503
+ ],
28504
+ fly: [
28505
+ "status",
28506
+ "apps",
28507
+ "secrets",
28508
+ "config",
28509
+ "regions",
28510
+ "info",
28511
+ "logs",
28512
+ "list",
28513
+ "ips",
28514
+ "volumes"
28515
+ ],
28516
+ flyctl: [
28517
+ "status",
28518
+ "apps",
28519
+ "secrets",
28520
+ "config",
28521
+ "regions",
28522
+ "info",
28523
+ "logs",
28524
+ "list",
28525
+ "ips",
28526
+ "volumes"
28527
+ ],
28528
+ wrangler: [
28529
+ "whoami",
28530
+ "secret",
28531
+ "kv:namespace",
28532
+ "kv",
28533
+ "r2",
28534
+ "d1",
28535
+ "pages",
28536
+ "tail",
28537
+ "deployments"
28538
+ ],
28539
+ aws: [
28540
+ "s3",
28541
+ "ec2",
28542
+ "iam",
28543
+ "sts",
28544
+ "ecr",
28545
+ "ecs",
28546
+ "eks",
28547
+ "lambda",
28548
+ "rds",
28549
+ "route53",
28550
+ "cloudformation",
28551
+ "ssm",
28552
+ "secretsmanager"
28553
+ ]
28554
+ };
28555
+ const AWS_READ_VERBS_REGEX = /^(describe-|list-|get-|show-|head-|read-|search-|lookup-|scan$|query$|ls$|whoami$|info$|status$)/;
28556
+ const DANGEROUS_OPERATORS = /[;`$()]/;
28557
+ function matchesPackageManager(command) {
28558
+ const parts = command.split(/\s+/);
28559
+ if (parts.length === 0 || !PACKAGE_MANAGERS.includes(parts[0])) return false;
28560
+ let scriptIndex = 1;
28561
+ if (parts[scriptIndex] === "run" || parts[scriptIndex] === "exec") scriptIndex++;
28562
+ const scriptPart = parts.slice(scriptIndex).join(" ");
28563
+ return SAFE_SCRIPTS.some((s) => scriptPart.startsWith(s));
28564
+ }
28565
+ function matchesVeloz(command) {
28566
+ return /^veloz(\s|$)/.test(command);
28567
+ }
28568
+ function matchesGitRead(command) {
28569
+ const parts = command.split(/\s+/);
28570
+ if (parts[0] !== "git" || !parts[1]) return false;
28571
+ return GIT_READ_SUBCOMMANDS.includes(parts[1]);
28572
+ }
28573
+ function matchesReadOnly(command) {
28574
+ const first = command.split(/\s+/)[0] ?? "";
28575
+ return READ_ONLY_COMMANDS.includes(first);
28576
+ }
28577
+ /**
28578
+ * Match read-only subcommands on third-party platform CLIs.
28579
+ * e.g. `vercel env ls`, `fly secrets list`, `aws s3 ls`.
28580
+ */
28581
+ function matchesPlatformRead(command) {
28582
+ const parts = command.split(/\s+/);
28583
+ const cli$1 = parts[0] ?? "";
28584
+ const sub = parts[1] ?? "";
28585
+ const allowed = PLATFORM_READ_SUBCOMMANDS[cli$1];
28586
+ if (!allowed) return false;
28587
+ if (!sub || sub.startsWith("-")) return true;
28588
+ if (!allowed.includes(sub)) return false;
28589
+ if (cli$1 === "aws") {
28590
+ const verb = parts[2] ?? "";
28591
+ if (!verb) return true;
28592
+ return AWS_READ_VERBS_REGEX.test(verb);
28593
+ }
28594
+ return true;
28595
+ }
28596
+ function isAllowedBashCommand(command) {
28597
+ return matchesVeloz(command) || matchesGitRead(command) || matchesPackageManager(command) || matchesReadOnly(command) || matchesPlatformRead(command);
28598
+ }
28599
+ function wizardCanUseTool(toolName, input) {
28600
+ if (toolName.startsWith("veloz:") || toolName.startsWith("wizard-tools:") || toolName.startsWith("mcp__")) return {
28601
+ behavior: "allow",
28602
+ updatedInput: input
28603
+ };
28604
+ if (toolName !== "Bash") return {
28605
+ behavior: "allow",
28606
+ updatedInput: input
28607
+ };
28608
+ const command = (typeof input.command === "string" ? input.command : "").trim();
28609
+ if (DANGEROUS_OPERATORS.test(command)) return {
28610
+ behavior: "deny",
28611
+ message: "Operadores shell como ; ` $ ( ) não são permitidos."
28612
+ };
28613
+ const normalized = command.replace(/\s*\d*>&\d+\s*/g, " ").trim();
28614
+ const pipeMatch = normalized.match(/^(.+?)\s*\|\s*(tail|head)(\s+\S+)*\s*$/);
28615
+ if (pipeMatch) {
28616
+ const base = pipeMatch[1].trim();
28617
+ if (/[|&]/.test(base)) return {
28618
+ behavior: "deny",
28619
+ message: "Múltiplos pipes não são permitidos. Apenas | tail/head."
28620
+ };
28621
+ if (isAllowedBashCommand(base)) return {
28622
+ behavior: "allow",
28623
+ updatedInput: input
28624
+ };
28625
+ }
28626
+ if (/[|&]/.test(normalized)) return {
28627
+ behavior: "deny",
28628
+ message: "Pipes só são permitidos com tail/head para limitar saída."
28629
+ };
28630
+ if (isAllowedBashCommand(normalized)) return {
28631
+ behavior: "allow",
28632
+ updatedInput: input
28633
+ };
28634
+ return {
28635
+ behavior: "deny",
28636
+ message: "Comando não permitido. Use veloz, git (leitura), gerenciadores de pacote (install/build/typecheck/lint/test), ou comandos somente-leitura."
28637
+ };
27853
28638
  }
27854
28639
 
27855
28640
  //#endregion
@@ -27881,6 +28666,15 @@ async function getSDKModule() {
27881
28666
  */
27882
28667
  async function runAgent(config) {
27883
28668
  const { store, cwd } = config;
28669
+ const logger = getSessionLogger();
28670
+ logger.log("agent", "runAgent start", {
28671
+ framework: config.framework,
28672
+ frameworkLabel: config.frameworkLabel,
28673
+ packageManager: config.packageManager,
28674
+ userName: config.userName,
28675
+ orgName: config.orgName,
28676
+ hasNotes: Boolean(config.userNotes)
28677
+ });
27884
28678
  let attempt = 0;
27885
28679
  let lastError = null;
27886
28680
  const cleanupPromptFiles = () => {
@@ -27895,21 +28689,29 @@ async function runAgent(config) {
27895
28689
  while (attempt <= MAX_RETRIES) {
27896
28690
  const isRetry = attempt > 0;
27897
28691
  if (isRetry) {
28692
+ logger.log("agent", "retrying agent run", {
28693
+ attempt: attempt + 1,
28694
+ lastError
28695
+ });
27898
28696
  store.incrementRetry();
27899
28697
  store.setAgentPhase("healing");
27900
28698
  store.pushStatus(`Agente reiniciando — tentativa ${attempt + 1} de ${MAX_RETRIES + 1}...`);
27901
28699
  store.pushAgentOutput(`--- Reiniciando agente (tentativa ${attempt + 1}) ---`);
27902
28700
  await sleep(2e3);
27903
28701
  }
28702
+ logger.log("agent", `invoking runAgentOnce (attempt ${attempt + 1})`);
27904
28703
  const result$2 = await runAgentOnce(config, isRetry, lastError);
28704
+ logger.log("agent", "runAgentOnce returned", result$2);
27905
28705
  if (result$2.success) return;
27906
28706
  if (result$2.fatal) {
28707
+ logger.log("agent", "fatal error — not retrying", { error: result$2.error });
27907
28708
  store.failAgent(result$2.error ?? "Erro fatal");
27908
28709
  return;
27909
28710
  }
27910
28711
  lastError = result$2.error ?? "Processo do agente encerrou inesperadamente";
27911
28712
  attempt++;
27912
28713
  if (attempt > MAX_RETRIES) {
28714
+ logger.log("agent", "retries exhausted", { lastError });
27913
28715
  store.failAgent(`O agente não conseguiu completar após ${MAX_RETRIES + 1} tentativas. Último erro: ${lastError}. Tente novamente com \`veloz init\` ou faça o deploy manualmente com \`veloz deploy\`.`);
27914
28716
  return;
27915
28717
  }
@@ -27921,19 +28723,27 @@ async function runAgent(config) {
27921
28723
  */
27922
28724
  async function runAgentOnce(config, isRetry, previousError) {
27923
28725
  const { store, cwd } = config;
28726
+ const logger = getSessionLogger();
27924
28727
  resetBuildStreamState();
27925
28728
  const query = (await getSDKModule()).query;
28729
+ logger.log("agent", "SDK module loaded");
27926
28730
  const promptCtx = {
27927
28731
  cwd,
27928
28732
  framework: config.framework,
27929
28733
  frameworkLabel: config.frameworkLabel,
27930
28734
  userName: config.userName,
27931
28735
  orgName: config.orgName,
27932
- packageManager: config.packageManager
28736
+ packageManager: config.packageManager,
28737
+ userNotes: config.userNotes ?? null
27933
28738
  };
27934
28739
  const prompt$1 = isRetry ? assembleContinuationPrompt(promptCtx, previousError) : assemblePrompt(promptCtx);
28740
+ logger.logPrompt(isRetry ? "continuation" : "initial", prompt$1, {
28741
+ ...promptCtx,
28742
+ previousError: isRetry ? previousError : void 0
28743
+ });
27935
28744
  const commandments = getCommandments();
27936
28745
  const wizardTools = createWizardToolsServer({ workingDirectory: cwd });
28746
+ logger.log("agent", "wizard-tools MCP server config created");
27937
28747
  const mcpServers = {
27938
28748
  veloz: {
27939
28749
  command: "npx",
@@ -28016,6 +28826,8 @@ async function runAgentOnce(config, isRetry, previousError) {
28016
28826
  "TodoWrite",
28017
28827
  "TodoRead"
28018
28828
  ],
28829
+ disallowedTools: ["AskUserQuestion"],
28830
+ canUseTool: (toolName, input) => Promise.resolve(wizardCanUseTool(toolName, input)),
28019
28831
  systemPrompt: {
28020
28832
  type: "preset",
28021
28833
  preset: "claude_code",
@@ -28031,18 +28843,11 @@ async function runAgentOnce(config, isRetry, previousError) {
28031
28843
  timeout: 30
28032
28844
  }],
28033
28845
  PostToolUse: [{
28034
- matcher: /^veloz:deploy$/,
28846
+ matcher: /^(mcp__veloz__deploy|veloz:deploy)$/,
28035
28847
  hooks: [() => {
28036
28848
  if (store.session.agentPhase !== "deploying") {
28037
28849
  store.setAgentPhase("deploying");
28038
28850
  store.pushStatus("Deploy iniciado...");
28039
- if (!buildStreamStarted) {
28040
- buildStreamStarted = true;
28041
- startBuildLogStream({
28042
- store,
28043
- cwd
28044
- }).catch(() => {});
28045
- }
28046
28851
  }
28047
28852
  return {};
28048
28853
  }]
@@ -28051,7 +28856,8 @@ async function runAgentOnce(config, isRetry, previousError) {
28051
28856
  }
28052
28857
  });
28053
28858
  for await (const message of agentResponse) {
28054
- handleMessage(message, store, collectedText, cwd, (toolUseId, question) => {
28859
+ logger.logSdkMessage(message);
28860
+ handleMessage(message, store, collectedText, (toolUseId, question) => {
28055
28861
  handleAskUserQuestion(store, cwd, toolUseId, question).then((response) => {
28056
28862
  enqueueUserResponse({
28057
28863
  type: "user",
@@ -28076,33 +28882,73 @@ async function runAgentOnce(config, isRetry, previousError) {
28076
28882
  });
28077
28883
  if (message.type === "result") {
28078
28884
  if (message.subtype === "success" && !message.is_error) receivedSuccessResult = true;
28885
+ logger.log("agent", "SDK result received", {
28886
+ subtype: message.subtype,
28887
+ is_error: message.is_error,
28888
+ errors: message.errors
28889
+ });
28079
28890
  signalDone();
28080
28891
  }
28081
28892
  }
28893
+ const outputText = collectedText.join("\n");
28894
+ logger.log("agent", "agent stream ended", {
28895
+ outputBytes: outputText.length,
28896
+ receivedSuccessResult
28897
+ });
28898
+ const userCodeError = extractUserCodeError(outputText);
28899
+ if (userCodeError) {
28900
+ logger.log("agent", "user-code build failure detected", { error: userCodeError });
28901
+ return {
28902
+ success: false,
28903
+ fatal: true,
28904
+ error: userCodeError
28905
+ };
28906
+ }
28907
+ const cancellation = extractCancellation(outputText);
28908
+ if (cancellation) {
28909
+ logger.log("agent", "user cancelled deploy", { summary: cancellation });
28910
+ return {
28911
+ success: false,
28912
+ fatal: true,
28913
+ error: cancellation
28914
+ };
28915
+ }
28082
28916
  if (receivedSuccessResult) {
28917
+ logger.log("agent", "deploy reported success");
28083
28918
  store.pushStatus("Deploy concluído com sucesso!");
28084
28919
  store.completeAgent();
28085
28920
  return { success: true };
28086
28921
  }
28087
- const outputText = collectedText.join("\n");
28088
- if (outputText.includes("API Error: 401") || outputText.includes("API Error: 403")) return {
28089
- success: false,
28090
- fatal: true,
28091
- error: "Erro de autenticação. Faça login novamente com `veloz login`."
28092
- };
28093
- if (outputText.includes("API Error:")) return {
28094
- success: false,
28095
- error: "Erro de API durante o deploy."
28096
- };
28922
+ if (outputText.includes("API Error: 401") || outputText.includes("API Error: 403")) {
28923
+ logger.log("agent", "auth error detected in output");
28924
+ return {
28925
+ success: false,
28926
+ fatal: true,
28927
+ error: "Erro de autenticação. Faça login novamente com `veloz login`."
28928
+ };
28929
+ }
28930
+ if (outputText.includes("API Error:")) {
28931
+ logger.log("agent", "API error detected in output");
28932
+ return {
28933
+ success: false,
28934
+ error: "Erro de API durante o deploy."
28935
+ };
28936
+ }
28937
+ logger.log("agent", "no explicit success signal — treating as success");
28097
28938
  store.completeAgent();
28098
28939
  return { success: true };
28099
28940
  } catch (error) {
28100
28941
  signalDone();
28101
28942
  if (receivedSuccessResult) {
28943
+ logger.log("agent", "post-success error ignored", { error: error instanceof Error ? error.message : String(error) });
28102
28944
  store.completeAgent();
28103
28945
  return { success: true };
28104
28946
  }
28105
28947
  const errorMessage = error instanceof Error ? error.message : "Erro desconhecido";
28948
+ logger.log("agent", "agent threw", {
28949
+ message: errorMessage,
28950
+ stack: error instanceof Error ? error.stack : void 0
28951
+ });
28106
28952
  if (errorMessage.includes("ANTHROPIC_API_KEY") || errorMessage.includes("authentication")) return {
28107
28953
  success: false,
28108
28954
  fatal: true,
@@ -28117,6 +28963,25 @@ async function runAgentOnce(config, isRetry, previousError) {
28117
28963
  }
28118
28964
  }
28119
28965
  /**
28966
+ * Extract a user-code build failure report emitted by the agent.
28967
+ * The agent is instructed to emit `[BUILD_FAILED_USER_CODE]` followed by
28968
+ * a detailed explanation (motivo, trecho dos logs, passos para corrigir).
28969
+ * Returns the detailed body, or null if the signal wasn't emitted.
28970
+ */
28971
+ function extractUserCodeError(text) {
28972
+ const match$3 = text.match(/\[BUILD_FAILED_USER_CODE\]\s*([\s\S]+?)(?:\n\[(?:STATUS|SUMMARY)\]|$)/);
28973
+ if (!match$3?.[1]) return null;
28974
+ const body = match$3[1].trim();
28975
+ return body.length > 0 ? body : null;
28976
+ }
28977
+ /**
28978
+ * Extract a deploy cancellation summary emitted by the agent when the user
28979
+ * declined the pre-deploy confirmation prompt.
28980
+ */
28981
+ function extractCancellation(text) {
28982
+ return text.match(/\[SUMMARY\]\s*(Deploy cancelado[^\n]*)/i)?.[1]?.trim() ?? null;
28983
+ }
28984
+ /**
28120
28985
  * Build a continuation prompt for retries. Tells the agent to inspect
28121
28986
  * the current project state and continue from where the previous run left off.
28122
28987
  */
@@ -28148,13 +29013,31 @@ Antes de qualquer coisa, verifique o que já foi feito:
28148
29013
  - Framework: ${ctx.frameworkLabel}
28149
29014
  - Package manager: ${ctx.packageManager}
28150
29015
  - Autenticado como: ${ctx.userName} (org: ${ctx.orgName})
28151
-
29016
+ ${ctx.userNotes && ctx.userNotes.trim() ? `
29017
+ ## Notas do Usuário (prioridade máxima — respeitar sempre)
29018
+ ${ctx.userNotes.trim()}
29019
+ ` : ""}
28152
29020
  ## Regras
28153
29021
  - Use TodoWrite para reportar progresso
28154
29022
  - NUNCA desista — diagnostique e corrija qualquer problema
28155
29023
  - Comunique-se em português (pt-BR)
28156
29024
  - Se algo falhar, leia os logs e tente uma abordagem diferente`;
28157
29025
  }
29026
+ /**
29027
+ * Normalize MCP tool names to the `server:tool` shorthand used in prompts.
29028
+ *
29029
+ * The Claude Agent SDK emits MCP tool_use blocks with names like
29030
+ * `mcp__wizard-tools__set_helper_content`. Our interception logic (and the
29031
+ * PostToolUse hook matcher) is written against the `wizard-tools:set_helper_content`
29032
+ * shorthand used throughout the prompt, so we translate here.
29033
+ */
29034
+ function normalizeMcpToolName(name) {
29035
+ if (!name.startsWith("mcp__")) return name;
29036
+ const rest = name.slice(5);
29037
+ const sep = rest.indexOf("__");
29038
+ if (sep < 0) return name;
29039
+ return `${rest.slice(0, sep)}:${rest.slice(sep + 2)}`;
29040
+ }
28158
29041
  /** Human-readable labels for common tools. */
28159
29042
  const TOOL_LABELS = {
28160
29043
  Read: "Lendo arquivo",
@@ -28166,11 +29049,23 @@ const TOOL_LABELS = {
28166
29049
  TodoWrite: "Atualizando tarefas",
28167
29050
  TodoRead: "Lendo tarefas"
28168
29051
  };
28169
- /** Track whether build log streaming has already been started for this run. */
28170
- let buildStreamStarted = false;
29052
+ /** Tool-use IDs for in-flight deploy invocations used to forward tool_result output. */
29053
+ const deployToolUseIds = /* @__PURE__ */ new Set();
29054
+ /** Deployment IDs we've already opened a log stream for in this run. */
29055
+ const streamedDeploymentIds = /* @__PURE__ */ new Set();
28171
29056
  /** Reset build stream state for a new agent run. */
28172
29057
  function resetBuildStreamState() {
28173
- buildStreamStarted = false;
29058
+ deployToolUseIds.clear();
29059
+ streamedDeploymentIds.clear();
29060
+ }
29061
+ /**
29062
+ * Flatten a tool_result `content` field into plain text.
29063
+ * The SDK may return a string or an array of text blocks.
29064
+ */
29065
+ function extractToolResultText(content) {
29066
+ if (!content) return "";
29067
+ if (typeof content === "string") return content;
29068
+ return content.filter((c) => c.type === "text" && typeof c.text === "string").map((c) => c.text).join("\n");
28174
29069
  }
28175
29070
  /**
28176
29071
  * Handle AskUserQuestion by showing the TUI overlay and waiting for response.
@@ -28200,7 +29095,8 @@ async function handleAskUserQuestion(store, cwd, toolUseId, question) {
28200
29095
  store.clearAgentPrompt();
28201
29096
  return "O usuário não respondeu (timeout).";
28202
29097
  }
28203
- function handleMessage(message, store, collectedText, cwd, onAskUser) {
29098
+ function handleMessage(message, store, collectedText, onAskUser) {
29099
+ const logger = getSessionLogger();
28204
29100
  if (message.type === "assistant") {
28205
29101
  const content = message.message?.content;
28206
29102
  if (!Array.isArray(content)) return;
@@ -28231,31 +29127,34 @@ function handleMessage(message, store, collectedText, cwd, onAskUser) {
28231
29127
  if (lastLine) store.pushAgentLog(lastLine.trim());
28232
29128
  }
28233
29129
  if (block.type === "tool_use" && block.name) {
28234
- let toolDescription = `[tool] ${TOOL_LABELS[block.name] ?? block.name}`;
28235
- if (block.name === "Read" && block.input?.file_path) toolDescription += `: ${block.input.file_path}`;
28236
- else if (block.name === "Write" && block.input?.file_path) toolDescription += `: ${block.input.file_path}`;
28237
- else if (block.name === "Edit" && block.input?.file_path) toolDescription += `: ${block.input.file_path}`;
28238
- else if (block.name === "Bash" && block.input?.command) {
29130
+ const rawName = block.name;
29131
+ const normalizedName = normalizeMcpToolName(rawName);
29132
+ logger.logToolUse(normalizedName, block.input, block.id ?? null, { rawName });
29133
+ let toolDescription = `[tool] ${TOOL_LABELS[rawName] ?? rawName}`;
29134
+ if (rawName === "Read" && block.input?.file_path) toolDescription += `: ${block.input.file_path}`;
29135
+ else if (rawName === "Write" && block.input?.file_path) toolDescription += `: ${block.input.file_path}`;
29136
+ else if (rawName === "Edit" && block.input?.file_path) toolDescription += `: ${block.input.file_path}`;
29137
+ else if (rawName === "Bash" && block.input?.command) {
28239
29138
  const cmd = String(block.input.command);
28240
29139
  toolDescription += `: ${cmd.length > 60 ? cmd.slice(0, 60) + "..." : cmd}`;
28241
- } else if (block.name === "Glob" && block.input?.pattern) toolDescription += `: ${block.input.pattern}`;
28242
- else if (block.name === "Grep" && block.input?.pattern) toolDescription += `: ${block.input.pattern}`;
28243
- if (block.name.startsWith("veloz:") || block.name.startsWith("wizard-tools:")) toolDescription = `[tool] Veloz: ${block.name.split(":")[1]}`;
28244
- const isDeployViaBash = block.name === "Bash" && typeof block.input?.command === "string" && /\bveloz\s+deploy\b/.test(block.input.command);
28245
- const isDeployViaMcp = block.name === "veloz:deploy";
28246
- if ((isDeployViaBash || isDeployViaMcp) && !buildStreamStarted) {
28247
- buildStreamStarted = true;
28248
- store.setAgentPhase("deploying");
28249
- store.pushStatus("Deploy iniciado aguardando build...");
28250
- startBuildLogStream({
28251
- store,
28252
- cwd
28253
- }).catch(() => {});
29140
+ } else if (rawName === "Glob" && block.input?.pattern) toolDescription += `: ${block.input.pattern}`;
29141
+ else if (rawName === "Grep" && block.input?.pattern) toolDescription += `: ${block.input.pattern}`;
29142
+ if (normalizedName.startsWith("veloz:") || normalizedName.startsWith("wizard-tools:")) toolDescription = `[tool] Veloz: ${normalizedName.split(":")[1]}`;
29143
+ const bashCommand = rawName === "Bash" && typeof block.input?.command === "string" ? block.input.command : null;
29144
+ const isDeployViaBash = bashCommand ? /\bveloz\s+deploy\b/.test(bashCommand) : false;
29145
+ const isDeployViaMcp = normalizedName === "veloz:deploy";
29146
+ const isDryRun = isDeployViaBash ? /--dry-run\b/.test(bashCommand ?? "") : isDeployViaMcp && block.input?.dryRun === true;
29147
+ if ((isDeployViaBash || isDeployViaMcp) && !isDryRun) {
29148
+ if (typeof block.id === "string") deployToolUseIds.add(block.id);
29149
+ if (store.session.agentPhase !== "deploying") {
29150
+ store.setAgentPhase("deploying");
29151
+ store.pushStatus("Deploy iniciado — aguardando build...");
29152
+ }
28254
29153
  }
28255
29154
  store.pushAgentOutput(toolDescription);
28256
29155
  store.pushToolCall(toolDescription);
28257
29156
  store.pushAgentLog(toolDescription.replace("[tool] ", ""));
28258
- if (block.name === "wizard-tools:prompt_user" && block.input?.question) store.setAgentPrompt({
29157
+ if (normalizedName === "wizard-tools:prompt_user" && block.input?.question) store.setAgentPrompt({
28259
29158
  id: `prompt-${Date.now()}`,
28260
29159
  question: String(block.input.question),
28261
29160
  hint: typeof block.input.hint === "string" ? block.input.hint : void 0,
@@ -28263,8 +29162,12 @@ function handleMessage(message, store, collectedText, cwd, onAskUser) {
28263
29162
  options: Array.isArray(block.input.options) ? block.input.options : void 0,
28264
29163
  envKey: typeof block.input.env_key === "string" ? block.input.env_key : void 0
28265
29164
  });
28266
- if (block.name === "AskUserQuestion" && block.input?.question && onAskUser) onAskUser(typeof block.id === "string" ? block.id : `ask-${Date.now()}`, String(block.input.question));
28267
- if (block.name === "wizard-tools:set_helper_content" && block.input?.lines && Array.isArray(block.input.lines)) {
29165
+ if (rawName === "AskUserQuestion" && onAskUser) {
29166
+ const toolUseId = typeof block.id === "string" ? block.id : `ask-${Date.now()}`;
29167
+ const questionText = extractAskUserQuestionText(block.input);
29168
+ if (questionText) onAskUser(toolUseId, questionText);
29169
+ }
29170
+ if (normalizedName === "wizard-tools:set_helper_content" && block.input?.lines && Array.isArray(block.input.lines)) {
28268
29171
  const title = typeof block.input.title === "string" ? block.input.title : null;
28269
29172
  const lines = block.input.lines;
28270
29173
  const helperLines = title ? [
@@ -28274,7 +29177,7 @@ function handleMessage(message, store, collectedText, cwd, onAskUser) {
28274
29177
  ] : lines;
28275
29178
  store.setAgentHelper(helperLines);
28276
29179
  }
28277
- if (block.name === "TodoWrite" && block.input?.todos && Array.isArray(block.input.todos)) {
29180
+ if (rawName === "TodoWrite" && block.input?.todos && Array.isArray(block.input.todos)) {
28278
29181
  const tasks = block.input.todos.map((todo, i) => ({
28279
29182
  id: todo.id ?? `task-${i}`,
28280
29183
  label: todo.content ?? `Tarefa ${i + 1}`,
@@ -28291,6 +29194,50 @@ function handleMessage(message, store, collectedText, cwd, onAskUser) {
28291
29194
  }
28292
29195
  }
28293
29196
  }
29197
+ if (message.type === "user") {
29198
+ const content = message.message?.content;
29199
+ if (!Array.isArray(content)) return;
29200
+ for (const block of content) {
29201
+ if (block.type !== "tool_result" || !block.tool_use_id) continue;
29202
+ logger.logToolResult(block.tool_use_id, extractToolResultText(block.content), Boolean(block.is_error));
29203
+ if (!deployToolUseIds.has(block.tool_use_id)) continue;
29204
+ const text = extractToolResultText(block.content);
29205
+ if (!text) continue;
29206
+ let lines = [];
29207
+ try {
29208
+ const parsed = JSON.parse(text);
29209
+ if (Array.isArray(parsed)) for (const entry of parsed) {
29210
+ if (typeof entry?.message === "string") lines.push(entry.message);
29211
+ const entryDeploymentId = entry?.data?.deploymentId;
29212
+ if (entryDeploymentId && !streamedDeploymentIds.has(entryDeploymentId)) {
29213
+ streamedDeploymentIds.add(entryDeploymentId);
29214
+ store.setBuildDeploymentId(entryDeploymentId);
29215
+ streamBuildLogs({
29216
+ store,
29217
+ deploymentId: entryDeploymentId
29218
+ }).catch((err) => {
29219
+ const msg = err instanceof Error ? err.message : String(err);
29220
+ store.pushBuildLog(`[stream] ${msg}`);
29221
+ });
29222
+ }
29223
+ const url = entry?.data?.urls?.[0];
29224
+ if (url && !store.session.deployUrl) store.setDeployUrl(url);
29225
+ }
29226
+ else if (parsed && typeof parsed === "object" && "message" in parsed) {
29227
+ const msg = parsed.message;
29228
+ if (typeof msg === "string") lines = [msg];
29229
+ } else lines = text.split("\n");
29230
+ } catch {
29231
+ lines = text.split("\n");
29232
+ }
29233
+ for (const raw of lines) {
29234
+ const line = raw.trim();
29235
+ if (!line) continue;
29236
+ store.pushBuildLog(line);
29237
+ }
29238
+ deployToolUseIds.delete(block.tool_use_id);
29239
+ }
29240
+ }
28294
29241
  if (message.type === "result") {
28295
29242
  if (message.is_error && message.errors) for (const err of message.errors) {
28296
29243
  store.pushAgentLog(`Erro: ${err}`);
@@ -28299,6 +29246,17 @@ function handleMessage(message, store, collectedText, cwd, onAskUser) {
28299
29246
  }
28300
29247
  }
28301
29248
  }
29249
+ function extractAskUserQuestionText(input) {
29250
+ if (!input) return null;
29251
+ const questions = input.questions;
29252
+ if (Array.isArray(questions) && questions.length > 0) {
29253
+ const first = questions[0];
29254
+ if (first && typeof first.question === "string" && first.question.trim()) return first.question.trim();
29255
+ if (first && typeof first.header === "string" && first.header.trim()) return first.header.trim();
29256
+ }
29257
+ if (typeof input.question === "string" && input.question.trim()) return input.question.trim();
29258
+ return null;
29259
+ }
28302
29260
  function mapTodoStatus(status) {
28303
29261
  switch (status) {
28304
29262
  case "in_progress": return "in_progress";
@@ -28317,6 +29275,7 @@ function sleep(ms) {
28317
29275
  let Screen = /* @__PURE__ */ function(Screen$1) {
28318
29276
  Screen$1["Intro"] = "intro";
28319
29277
  Screen$1["Auth"] = "auth";
29278
+ Screen$1["Notes"] = "notes";
28320
29279
  Screen$1["Run"] = "run";
28321
29280
  Screen$1["Outro"] = "outro";
28322
29281
  return Screen$1;
@@ -28337,6 +29296,11 @@ const FLOW = [
28337
29296
  show: (s) => s.authPhase !== "done",
28338
29297
  isComplete: (s) => s.authPhase === "done"
28339
29298
  },
29299
+ {
29300
+ screen: Screen.Notes,
29301
+ show: () => true,
29302
+ isComplete: (s) => s.notesConfirmed
29303
+ },
28340
29304
  {
28341
29305
  screen: Screen.Run,
28342
29306
  show: () => true,
@@ -28419,9 +29383,27 @@ const COPY = {
28419
29383
  authTimeout: "Tempo esgotado. Tente novamente com `veloz login`.",
28420
29384
  authError: "Erro na autenticação.",
28421
29385
  authOrgSelect: "Selecione o workspace:",
29386
+ authOrgCreateOption: "Criar novo workspace",
29387
+ authOrgCreateTitle: "Criar novo workspace",
29388
+ authOrgCreateQuestion: "Qual o nome do novo workspace?",
29389
+ authOrgCreatePlaceholder: "Digite o nome e pressione Enter...",
29390
+ authOrgCreateHint: "Enter para criar",
29391
+ authOrgCreating: "Criando workspace...",
28422
29392
  authNoOrg: "Você ainda não tem um workspace.",
28423
29393
  authNoOrgHint: "Crie um workspace em app.onveloz.com para continuar.",
28424
29394
  authOrgResolved: "Workspace:",
29395
+ notesTitle: "Notas para o deploy",
29396
+ notesQuestion: "Algo específico que o agente deve saber antes de começar? Deploy de todos os apps, apenas um serviço, flags customizadas, etc.",
29397
+ notesExamplesLabel: "Exemplos:",
29398
+ notesExamples: [
29399
+ "Deploya só o apps/web",
29400
+ "Usa o Dockerfile que está em apps/api",
29401
+ "Ignora o build cache",
29402
+ "Não cria banco — já existe"
29403
+ ],
29404
+ notesPlaceholder: "Digite suas notas ou pressione Enter para pular...",
29405
+ notesHint: "Enter para continuar · Esc para pular",
29406
+ notesFooter: "Suas notas serão repassadas ao agente de deploy",
28425
29407
  runTitle: "Configurando seu projeto...",
28426
29408
  phaseLabels: {
28427
29409
  idle: "Aguardando...",
@@ -28490,11 +29472,31 @@ const Icons = {
28490
29472
  ]
28491
29473
  };
28492
29474
 
29475
+ //#endregion
29476
+ //#region src/wizard/ui/hooks/useGatedInput.ts
29477
+ /**
29478
+ * useGatedInput — Wrapper around Ink's useInput that respects an
29479
+ * InputFocusContext. When an overlay (like the agent prompt) owns
29480
+ * input, screens rendered behind it should not react to keystrokes.
29481
+ */
29482
+ const InputFocusContext = createContext({ active: true });
29483
+ function InputFocusProvider({ active: active$1, children }) {
29484
+ return React.createElement(InputFocusContext.Provider, { value: { active: active$1 } }, children);
29485
+ }
29486
+ function useGatedInput(handler, options) {
29487
+ const { active: active$1 } = useContext(InputFocusContext);
29488
+ const callerActive = options?.isActive ?? true;
29489
+ useInput(handler, {
29490
+ ...options,
29491
+ isActive: callerActive && active$1
29492
+ });
29493
+ }
29494
+
28493
29495
  //#endregion
28494
29496
  //#region src/wizard/ui/primitives/PickerMenu.tsx
28495
29497
  function PickerMenu({ items, onSelect, onCancel }) {
28496
29498
  const [cursor, setCursor] = useState(0);
28497
- useInput((_input, key) => {
29499
+ useGatedInput((_input, key) => {
28498
29500
  if (key.upArrow) setCursor((c) => c > 0 ? c - 1 : items.length - 1);
28499
29501
  if (key.downArrow) setCursor((c) => c < items.length - 1 ? c + 1 : 0);
28500
29502
  if (key.return) {
@@ -28533,34 +29535,9 @@ function PickerMenu({ items, onSelect, onCancel }) {
28533
29535
  });
28534
29536
  }
28535
29537
 
28536
- //#endregion
28537
- //#region src/wizard/ui/hooks/useStdoutDimensions.ts
28538
- /**
28539
- * useStdoutDimensions — Returns [columns, rows] and re-renders on terminal resize.
28540
- */
28541
- function useStdoutDimensions() {
28542
- const { stdout } = useStdout();
28543
- const [size, setSize] = useState(() => [stdout.columns || 80, stdout.rows || 24]);
28544
- useEffect(() => {
28545
- setSize([stdout.columns || 80, stdout.rows || 24]);
28546
- const stream = stdout;
28547
- if (typeof stream.on !== "function") return;
28548
- const onResize = () => {
28549
- const c = stdout.columns || 80;
28550
- const r = stdout.rows || 24;
28551
- if (c > 0 && r > 0) setSize([c, r]);
28552
- };
28553
- stream.on("resize", onResize);
28554
- return () => {
28555
- stream.off?.("resize", onResize);
28556
- };
28557
- }, [stdout]);
28558
- return size;
28559
- }
28560
-
28561
29538
  //#endregion
28562
29539
  //#region src/wizard/ui/primitives/ScreenLayout.tsx
28563
- const version = "0.0.0-beta.30";
29540
+ const version = "0.0.0-beta.31";
28564
29541
  const LOGO_LINES = [
28565
29542
  "██╗ ██╗███████╗██╗ ██████╗ ███████╗",
28566
29543
  "██║ ██║██╔════╝██║ ██╔═══██╗╚══███╔╝",
@@ -28570,10 +29547,11 @@ const LOGO_LINES = [
28570
29547
  " ╚═══╝ ╚══════╝╚══════╝ ╚═════╝ ╚══════╝"
28571
29548
  ];
28572
29549
  function ScreenLayout({ children, footer }) {
28573
- const [, rows] = useStdoutDimensions();
28574
29550
  return /* @__PURE__ */ jsxs(Box, {
28575
29551
  flexDirection: "column",
28576
- height: rows,
29552
+ flexGrow: 1,
29553
+ flexShrink: 1,
29554
+ minHeight: 0,
28577
29555
  paddingX: 2,
28578
29556
  paddingY: 1,
28579
29557
  children: [
@@ -28594,13 +29572,6 @@ function ScreenLayout({ children, footer }) {
28594
29572
  })] }),
28595
29573
  /* @__PURE__ */ jsx(Box, {
28596
29574
  marginTop: 1,
28597
- marginBottom: 1,
28598
- children: /* @__PURE__ */ jsx(Text, {
28599
- color: DIM_COLOR,
28600
- children: "─".repeat(50)
28601
- })
28602
- }),
28603
- /* @__PURE__ */ jsx(Box, {
28604
29575
  flexDirection: "column",
28605
29576
  flexGrow: 1,
28606
29577
  flexShrink: 1,
@@ -28737,6 +29708,57 @@ function Spinner({ label }) {
28737
29708
  }), label ? /* @__PURE__ */ jsxs(Text, { children: [" ", label] }) : null] });
28738
29709
  }
28739
29710
 
29711
+ //#endregion
29712
+ //#region src/wizard/session.ts
29713
+ /** Sentinel value used as a PickerMenu item to signal "create new workspace". */
29714
+ const CREATE_ORG_ACTION = "__create__";
29715
+ function createInitialSession() {
29716
+ return {
29717
+ isAuthenticated: false,
29718
+ userName: "",
29719
+ orgId: "",
29720
+ orgName: "",
29721
+ apiKey: "",
29722
+ authPhase: "checking",
29723
+ authDeviceCode: null,
29724
+ authVerificationUrl: null,
29725
+ availableOrgs: [],
29726
+ selectedOrgId: null,
29727
+ newOrgName: null,
29728
+ orgCreateError: null,
29729
+ detectedFramework: null,
29730
+ detectedFrameworkLabel: null,
29731
+ selectedFramework: null,
29732
+ selectedFrameworkLabel: null,
29733
+ packageManager: "npm",
29734
+ gitRemote: null,
29735
+ isMonorepo: false,
29736
+ agentPhase: "idle",
29737
+ agentTasks: [],
29738
+ agentLogLines: [],
29739
+ agentComplete: false,
29740
+ agentError: null,
29741
+ deployUrl: null,
29742
+ retryCount: 0,
29743
+ buildLogLines: [],
29744
+ buildStreamActive: false,
29745
+ buildDeploymentId: null,
29746
+ statusMessages: [],
29747
+ agentOutputLines: [],
29748
+ toolCallLog: [],
29749
+ learnCardBlockIdx: 0,
29750
+ learnCardComplete: false,
29751
+ agentHelperLines: [],
29752
+ agentPrompt: null,
29753
+ agentSummary: null,
29754
+ userNotes: null,
29755
+ notesConfirmed: false,
29756
+ introConfirmed: false,
29757
+ outroDismissed: false,
29758
+ agentStartedAt: null
29759
+ };
29760
+ }
29761
+
28740
29762
  //#endregion
28741
29763
  //#region src/wizard/ui/screens/AuthScreen.tsx
28742
29764
  function AuthScreen({ store }) {
@@ -28773,6 +29795,8 @@ function AuthScreen({ store }) {
28773
29795
  })
28774
29796
  ]
28775
29797
  }) : null] });
29798
+ if (session.authPhase === "org-creating") return /* @__PURE__ */ jsx(ScreenLayout, { children: /* @__PURE__ */ jsx(Spinner, { label: COPY.authOrgCreating }) });
29799
+ if (session.authPhase === "org-create") return /* @__PURE__ */ jsx(OrgCreateForm, { store });
28776
29800
  if (session.authPhase === "no-org") return /* @__PURE__ */ jsxs(ScreenLayout, { children: [/* @__PURE__ */ jsxs(Box, {
28777
29801
  gap: 1,
28778
29802
  children: [/* @__PURE__ */ jsx(Text, {
@@ -28792,10 +29816,13 @@ function AuthScreen({ store }) {
28792
29816
  })]
28793
29817
  })] });
28794
29818
  if (session.authPhase === "org-select") {
28795
- const items = session.availableOrgs.map((org) => ({
29819
+ const items = [...session.availableOrgs.map((org) => ({
28796
29820
  label: `${org.name} (${org.slug})`,
28797
29821
  value: org.id
28798
- }));
29822
+ })), {
29823
+ label: COPY.authOrgCreateOption,
29824
+ value: CREATE_ORG_ACTION
29825
+ }];
28799
29826
  return /* @__PURE__ */ jsxs(ScreenLayout, { children: [/* @__PURE__ */ jsxs(Box, {
28800
29827
  gap: 1,
28801
29828
  children: [/* @__PURE__ */ jsx(Text, {
@@ -28812,7 +29839,10 @@ function AuthScreen({ store }) {
28812
29839
  marginTop: 1,
28813
29840
  children: /* @__PURE__ */ jsx(PickerMenu, {
28814
29841
  items,
28815
- onSelect: (id) => store.selectOrg(id)
29842
+ onSelect: (id) => {
29843
+ if (id === CREATE_ORG_ACTION) store.requestOrgCreation();
29844
+ else store.selectOrg(id);
29845
+ }
28816
29846
  })
28817
29847
  })]
28818
29848
  })] });
@@ -28841,6 +29871,140 @@ function AuthScreen({ store }) {
28841
29871
  }) : null] });
28842
29872
  return /* @__PURE__ */ jsx(ScreenLayout, { children: /* @__PURE__ */ jsx(Spinner, { label: COPY.authChecking }) });
28843
29873
  }
29874
+ function OrgCreateForm({ store }) {
29875
+ const [value, setValue] = useState("");
29876
+ const session = store.session;
29877
+ useGatedInput((input, key) => {
29878
+ if (key.return) {
29879
+ if (value.trim().length > 0) store.submitNewOrgName(value.trim());
29880
+ return;
29881
+ }
29882
+ if (key.backspace || key.delete) {
29883
+ setValue((v) => v.slice(0, -1));
29884
+ return;
29885
+ }
29886
+ if (input && !key.ctrl && !key.meta) setValue((v) => v + input);
29887
+ });
29888
+ return /* @__PURE__ */ jsxs(ScreenLayout, { children: [
29889
+ /* @__PURE__ */ jsx(Text, {
29890
+ color: BRAND_COLOR,
29891
+ bold: true,
29892
+ children: COPY.authOrgCreateTitle
29893
+ }),
29894
+ /* @__PURE__ */ jsx(Box, {
29895
+ marginTop: 1,
29896
+ children: /* @__PURE__ */ jsx(Text, {
29897
+ wrap: "wrap",
29898
+ children: COPY.authOrgCreateQuestion
29899
+ })
29900
+ }),
29901
+ /* @__PURE__ */ jsx(Box, {
29902
+ marginTop: 1,
29903
+ children: /* @__PURE__ */ jsxs(Box, {
29904
+ gap: 1,
29905
+ children: [/* @__PURE__ */ jsx(Text, {
29906
+ color: BRAND_COLOR,
29907
+ children: Icons.triangleRight
29908
+ }), /* @__PURE__ */ jsxs(Text, { children: [value || /* @__PURE__ */ jsx(Text, {
29909
+ color: DIM_COLOR,
29910
+ children: COPY.authOrgCreatePlaceholder
29911
+ }), /* @__PURE__ */ jsx(Text, {
29912
+ color: DIM_COLOR,
29913
+ children: "▌"
29914
+ })] })]
29915
+ })
29916
+ }),
29917
+ session.orgCreateError ? /* @__PURE__ */ jsx(Box, {
29918
+ marginTop: 1,
29919
+ children: /* @__PURE__ */ jsx(Text, {
29920
+ color: ERROR_COLOR,
29921
+ children: session.orgCreateError
29922
+ })
29923
+ }) : null
29924
+ ] });
29925
+ }
29926
+
29927
+ //#endregion
29928
+ //#region src/wizard/ui/screens/NotesScreen.tsx
29929
+ function NotesScreen({ store }) {
29930
+ useSyncExternalStore(store.subscribe, store.getSnapshot);
29931
+ const [value, setValue] = useState("");
29932
+ useGatedInput((input, key) => {
29933
+ if (key.return) {
29934
+ store.confirmNotes(value);
29935
+ return;
29936
+ }
29937
+ if (key.escape) {
29938
+ store.confirmNotes(null);
29939
+ return;
29940
+ }
29941
+ if (key.backspace || key.delete) {
29942
+ setValue((v) => v.slice(0, -1));
29943
+ return;
29944
+ }
29945
+ if (input && !key.ctrl && !key.meta) setValue((v) => v + input);
29946
+ });
29947
+ return /* @__PURE__ */ jsxs(ScreenLayout, {
29948
+ footer: COPY.notesFooter,
29949
+ children: [
29950
+ /* @__PURE__ */ jsx(Text, {
29951
+ color: BRAND_COLOR,
29952
+ bold: true,
29953
+ children: COPY.notesTitle
29954
+ }),
29955
+ /* @__PURE__ */ jsx(Box, {
29956
+ marginTop: 1,
29957
+ children: /* @__PURE__ */ jsx(Text, {
29958
+ wrap: "wrap",
29959
+ children: COPY.notesQuestion
29960
+ })
29961
+ }),
29962
+ /* @__PURE__ */ jsxs(Box, {
29963
+ marginTop: 1,
29964
+ flexDirection: "column",
29965
+ children: [/* @__PURE__ */ jsx(Text, {
29966
+ color: DIM_COLOR,
29967
+ children: COPY.notesExamplesLabel
29968
+ }), COPY.notesExamples.map((ex, i) => /* @__PURE__ */ jsxs(Box, {
29969
+ gap: 1,
29970
+ marginLeft: 2,
29971
+ children: [/* @__PURE__ */ jsx(Text, {
29972
+ color: DIM_COLOR,
29973
+ children: Icons.bullet
29974
+ }), /* @__PURE__ */ jsx(Text, {
29975
+ color: DIM_COLOR,
29976
+ children: ex
29977
+ })]
29978
+ }, i))]
29979
+ }),
29980
+ /* @__PURE__ */ jsx(Box, {
29981
+ marginTop: 1,
29982
+ flexDirection: "column",
29983
+ children: /* @__PURE__ */ jsxs(Box, {
29984
+ gap: 1,
29985
+ children: [/* @__PURE__ */ jsx(Text, {
29986
+ color: BRAND_COLOR,
29987
+ children: Icons.triangleRight
29988
+ }), /* @__PURE__ */ jsxs(Text, { children: [value || /* @__PURE__ */ jsx(Text, {
29989
+ color: DIM_COLOR,
29990
+ children: COPY.notesPlaceholder
29991
+ }), /* @__PURE__ */ jsx(Text, {
29992
+ color: DIM_COLOR,
29993
+ children: "▌"
29994
+ })] })]
29995
+ })
29996
+ }),
29997
+ /* @__PURE__ */ jsx(Box, {
29998
+ marginTop: 1,
29999
+ children: /* @__PURE__ */ jsx(Text, {
30000
+ color: SUCCESS_COLOR,
30001
+ dimColor: true,
30002
+ children: COPY.notesHint
30003
+ })
30004
+ })
30005
+ ]
30006
+ });
30007
+ }
28844
30008
 
28845
30009
  //#endregion
28846
30010
  //#region src/wizard/ui/primitives/text-helpers.ts
@@ -28976,7 +30140,7 @@ const TEXT_REVEAL_MODE_DEFAULTS = {
28976
30140
  [TextRevealMode.WordByWord]: 240,
28977
30141
  [TextRevealMode.SentenceFade]: 2400
28978
30142
  };
28979
- function TextBlock({ text, active, completed, onComplete, mode, bullet, animationInterval, sentenceInterval = 1600, maxHeight, availableWidth }) {
30143
+ function TextBlock({ text, active: active$1, completed, onComplete, mode, bullet, animationInterval, sentenceInterval = 1600, maxHeight, availableWidth }) {
28980
30144
  const speed = animationInterval ?? TEXT_REVEAL_MODE_DEFAULTS[mode];
28981
30145
  const [animIdx, setAnimIdx] = useState(mode === TextRevealMode.SentenceFade ? 1 : 0);
28982
30146
  const resetRef = useRef(0);
@@ -28992,14 +30156,14 @@ function TextBlock({ text, active, completed, onComplete, mode, bullet, animatio
28992
30156
  const sentWordEnds = useMemo(() => sentenceEndWords(text), [text]);
28993
30157
  const isDone$2 = mode === TextRevealMode.Typewriter ? animIdx >= text.length : mode === TextRevealMode.WordByWord ? animIdx >= words.length : animIdx >= sentences.length;
28994
30158
  useEffect(() => {
28995
- if (isDone$2 && active) onComplete();
30159
+ if (isDone$2 && active$1) onComplete();
28996
30160
  }, [
28997
30161
  isDone$2,
28998
- active,
30162
+ active$1,
28999
30163
  onComplete
29000
30164
  ]);
29001
30165
  useEffect(() => {
29002
- if (!active || isDone$2) return;
30166
+ if (!active$1 || isDone$2) return;
29003
30167
  const token = resetRef.current;
29004
30168
  const isFirstTick = animIdx === 0;
29005
30169
  let delay$2 = isFirstTick ? 0 : speed;
@@ -29011,7 +30175,7 @@ function TextBlock({ text, active, completed, onComplete, mode, bullet, animatio
29011
30175
  }, delay$2);
29012
30176
  return () => clearTimeout(timer);
29013
30177
  }, [
29014
- active,
30178
+ active$1,
29015
30179
  mode,
29016
30180
  animIdx,
29017
30181
  isDone$2,
@@ -29053,22 +30217,22 @@ function TextBlock({ text, active, completed, onComplete, mode, bullet, animatio
29053
30217
  * LinesBlock — Reveals ReactNode lines one at a time.
29054
30218
  * Each line can contain colors, bold, ASCII art — any JSX.
29055
30219
  */
29056
- function LinesBlock({ lines, interval, active, completed, onComplete, maxHeight }) {
30220
+ function LinesBlock({ lines, interval, active: active$1, completed, onComplete, maxHeight }) {
29057
30221
  const [revealedCount, setRevealedCount] = useState(0);
29058
30222
  useEffect(() => {
29059
- if (!active || revealedCount >= lines.length) return;
30223
+ if (!active$1 || revealedCount >= lines.length) return;
29060
30224
  const timer = setTimeout(() => setRevealedCount((c) => c + 1), revealedCount === 0 ? 0 : interval);
29061
30225
  return () => clearTimeout(timer);
29062
30226
  }, [
29063
- active,
30227
+ active$1,
29064
30228
  revealedCount,
29065
30229
  lines.length,
29066
30230
  interval
29067
30231
  ]);
29068
30232
  useEffect(() => {
29069
- if (active && revealedCount >= lines.length) onComplete();
30233
+ if (active$1 && revealedCount >= lines.length) onComplete();
29070
30234
  }, [
29071
- active,
30235
+ active$1,
29072
30236
  revealedCount,
29073
30237
  lines.length,
29074
30238
  onComplete
@@ -29092,10 +30256,10 @@ function LinesBlock({ lines, interval, active, completed, onComplete, maxHeight
29092
30256
  /**
29093
30257
  * NodeBlock — Renders static JSX, fires onComplete immediately.
29094
30258
  */
29095
- function NodeBlock({ content, active, completed, onComplete }) {
30259
+ function NodeBlock({ content, active: active$1, completed, onComplete }) {
29096
30260
  useEffect(() => {
29097
- if (active) onComplete();
29098
- }, [active, onComplete]);
30261
+ if (active$1) onComplete();
30262
+ }, [active$1, onComplete]);
29099
30263
  if (completed) return /* @__PURE__ */ jsx(Text, {
29100
30264
  dimColor: true,
29101
30265
  children: content
@@ -29187,7 +30351,7 @@ function ContentSequencer({ blocks, mode, maxHeight, availableWidth, bullet, ani
29187
30351
  if (i < clearFloor) return null;
29188
30352
  if (isClearBlock(block) && i < activeIdx) return null;
29189
30353
  if (i < visibleStart || i > visibleEnd) return null;
29190
- const active = i === activeIdx;
30354
+ const active$1 = i === activeIdx;
29191
30355
  const completed = i < activeIdx || skipCurrent && i === activeIdx;
29192
30356
  if (completed && isObjectBlock(block)) {
29193
30357
  const isText = typeof block.content === "string";
@@ -29198,7 +30362,7 @@ function ContentSequencer({ blocks, mode, maxHeight, availableWidth, bullet, ani
29198
30362
  marginBottom: 1,
29199
30363
  children: /* @__PURE__ */ jsx(BlockRenderer, {
29200
30364
  block,
29201
- active,
30365
+ active: active$1,
29202
30366
  completed,
29203
30367
  onComplete: () => handleComplete(i),
29204
30368
  mode,
@@ -29213,19 +30377,19 @@ function ContentSequencer({ blocks, mode, maxHeight, availableWidth, bullet, ani
29213
30377
  })
29214
30378
  });
29215
30379
  }
29216
- function BlockRenderer({ block, active, completed, onComplete, mode, bullet, animationInterval, sentenceInterval, lineInterval, maxHeight, availableWidth }) {
30380
+ function BlockRenderer({ block, active: active$1, completed, onComplete, mode, bullet, animationInterval, sentenceInterval, lineInterval, maxHeight, availableWidth }) {
29217
30381
  const isClear = isClearBlock(block);
29218
30382
  useEffect(() => {
29219
- if (isClear && active) onComplete();
30383
+ if (isClear && active$1) onComplete();
29220
30384
  }, [
29221
30385
  isClear,
29222
- active,
30386
+ active$1,
29223
30387
  onComplete
29224
30388
  ]);
29225
30389
  if (isClear) return null;
29226
30390
  if (typeof block === "string") return /* @__PURE__ */ jsx(TextBlock, {
29227
30391
  text: block,
29228
- active,
30392
+ active: active$1,
29229
30393
  completed,
29230
30394
  onComplete,
29231
30395
  mode,
@@ -29238,14 +30402,14 @@ function BlockRenderer({ block, active, completed, onComplete, mode, bullet, ani
29238
30402
  if (isLinesBlock(block)) return /* @__PURE__ */ jsx(LinesBlock, {
29239
30403
  lines: block.lines,
29240
30404
  interval: block.interval ?? lineInterval,
29241
- active,
30405
+ active: active$1,
29242
30406
  completed,
29243
30407
  onComplete,
29244
30408
  maxHeight
29245
30409
  });
29246
30410
  if (typeof block.content === "string") return /* @__PURE__ */ jsx(TextBlock, {
29247
30411
  text: block.content,
29248
- active,
30412
+ active: active$1,
29249
30413
  completed,
29250
30414
  onComplete,
29251
30415
  mode: block.mode ?? mode,
@@ -29257,12 +30421,37 @@ function BlockRenderer({ block, active, completed, onComplete, mode, bullet, ani
29257
30421
  });
29258
30422
  return /* @__PURE__ */ jsx(NodeBlock, {
29259
30423
  content: block.content,
29260
- active,
30424
+ active: active$1,
29261
30425
  completed,
29262
30426
  onComplete
29263
30427
  });
29264
30428
  }
29265
30429
 
30430
+ //#endregion
30431
+ //#region src/wizard/ui/hooks/useStdoutDimensions.ts
30432
+ /**
30433
+ * useStdoutDimensions — Returns [columns, rows] and re-renders on terminal resize.
30434
+ */
30435
+ function useStdoutDimensions() {
30436
+ const { stdout } = useStdout();
30437
+ const [size, setSize] = useState(() => [stdout.columns || 80, stdout.rows || 24]);
30438
+ useEffect(() => {
30439
+ setSize([stdout.columns || 80, stdout.rows || 24]);
30440
+ const stream = stdout;
30441
+ if (typeof stream.on !== "function") return;
30442
+ const onResize = () => {
30443
+ const c = stdout.columns || 80;
30444
+ const r = stdout.rows || 24;
30445
+ if (c > 0 && r > 0) setSize([c, r]);
30446
+ };
30447
+ stream.on("resize", onResize);
30448
+ return () => {
30449
+ stream.off?.("resize", onResize);
30450
+ };
30451
+ }, [stdout]);
30452
+ return size;
30453
+ }
30454
+
29266
30455
  //#endregion
29267
30456
  //#region src/wizard/ui/components/LearnCard.tsx
29268
30457
  /**
@@ -29301,7 +30490,7 @@ const DEPLOY_PIPELINE = {
29301
30490
  /* @__PURE__ */ jsx(Text, { children: "veloz deploy" }),
29302
30491
  /* @__PURE__ */ jsx(Text, {
29303
30492
  color: DIM_COLOR,
29304
- children: " │"
30493
+ children: " │"
29305
30494
  })
29306
30495
  ] }, "deploy-cmd"),
29307
30496
  /* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsx(Text, {
@@ -29309,7 +30498,7 @@ const DEPLOY_PIPELINE = {
29309
30498
  children: " │ ↓"
29310
30499
  }), /* @__PURE__ */ jsx(Text, {
29311
30500
  color: DIM_COLOR,
29312
- children: " │"
30501
+ children: " │"
29313
30502
  })] }, "arrow-1"),
29314
30503
  /* @__PURE__ */ jsxs(Text, { children: [
29315
30504
  /* @__PURE__ */ jsx(Text, {
@@ -29337,7 +30526,7 @@ const DEPLOY_PIPELINE = {
29337
30526
  }),
29338
30527
  /* @__PURE__ */ jsx(Text, {
29339
30528
  color: DIM_COLOR,
29340
- children: " │"
30529
+ children: " │"
29341
30530
  })
29342
30531
  ] }, "upload-desc"),
29343
30532
  /* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsx(Text, {
@@ -29345,7 +30534,7 @@ const DEPLOY_PIPELINE = {
29345
30534
  children: " │ ↓"
29346
30535
  }), /* @__PURE__ */ jsx(Text, {
29347
30536
  color: DIM_COLOR,
29348
- children: " │"
30537
+ children: " │"
29349
30538
  })] }, "arrow-2"),
29350
30539
  /* @__PURE__ */ jsxs(Text, { children: [
29351
30540
  /* @__PURE__ */ jsx(Text, {
@@ -29373,7 +30562,7 @@ const DEPLOY_PIPELINE = {
29373
30562
  }),
29374
30563
  /* @__PURE__ */ jsx(Text, {
29375
30564
  color: DIM_COLOR,
29376
- children: " │"
30565
+ children: " │"
29377
30566
  })
29378
30567
  ] }, "build-deps"),
29379
30568
  /* @__PURE__ */ jsxs(Text, { children: [
@@ -29387,7 +30576,7 @@ const DEPLOY_PIPELINE = {
29387
30576
  }),
29388
30577
  /* @__PURE__ */ jsx(Text, {
29389
30578
  color: DIM_COLOR,
29390
- children: " │"
30579
+ children: " │"
29391
30580
  })
29392
30581
  ] }, "build-exec"),
29393
30582
  /* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsx(Text, {
@@ -29395,7 +30584,7 @@ const DEPLOY_PIPELINE = {
29395
30584
  children: " │ ↓"
29396
30585
  }), /* @__PURE__ */ jsx(Text, {
29397
30586
  color: DIM_COLOR,
29398
- children: " │"
30587
+ children: " │"
29399
30588
  })] }, "arrow-3"),
29400
30589
  /* @__PURE__ */ jsxs(Text, { children: [
29401
30590
  /* @__PURE__ */ jsx(Text, {
@@ -29423,7 +30612,7 @@ const DEPLOY_PIPELINE = {
29423
30612
  }),
29424
30613
  /* @__PURE__ */ jsx(Text, {
29425
30614
  color: DIM_COLOR,
29426
- children: " │"
30615
+ children: " │"
29427
30616
  })
29428
30617
  ] }, "url-label"),
29429
30618
  /* @__PURE__ */ jsx(Text, {
@@ -29806,73 +30995,159 @@ function RunScreen({ store }) {
29806
30995
  const topRows = Math.max(6, Math.floor(contentRows * .6));
29807
30996
  const bottomRows = Math.max(4, contentRows - topRows);
29808
30997
  const isNarrow = columns < 80;
29809
- const helperContent = session.agentHelperLines.length > 0 ? /* @__PURE__ */ jsx(AgentHelperPane, { lines: session.agentHelperLines }) : session.learnCardComplete ? /* @__PURE__ */ jsx(TipsCard, {}) : /* @__PURE__ */ jsx(LearnCard, {
30998
+ const helperContent = session.learnCardComplete ? /* @__PURE__ */ jsx(TipsCard, {}) : /* @__PURE__ */ jsx(LearnCard, {
29810
30999
  store,
29811
31000
  onComplete: () => store.setLearnCardComplete()
29812
31001
  });
29813
- const rightPane = /* @__PURE__ */ jsx(TasksPane, { store });
29814
- const showBuildLogs = session.buildLogLines.length > 0 && (session.agentPhase === "deploying" || session.buildStreamActive);
29815
- const visibleStatus = session.statusMessages.slice(-5);
29816
- return /* @__PURE__ */ jsxs(ScreenLayout, { children: [
29817
- /* @__PURE__ */ jsx(Box, {
31002
+ const hasAgentHelper = session.agentHelperLines.length > 0;
31003
+ const rightPane = /* @__PURE__ */ jsxs(Box, {
31004
+ flexDirection: "column",
31005
+ flexGrow: 1,
31006
+ overflow: "hidden",
31007
+ children: [/* @__PURE__ */ jsx(TasksPane, { store }), hasAgentHelper ? /* @__PURE__ */ jsx(Box, {
31008
+ marginTop: 1,
29818
31009
  flexGrow: 1,
29819
- flexShrink: 1,
29820
- height: topRows,
29821
- children: isNarrow ? rightPane : /* @__PURE__ */ jsxs(Box, {
29822
- flexDirection: "row",
29823
- flexGrow: 1,
29824
- gap: 2,
29825
- children: [/* @__PURE__ */ jsxs(Box, {
29826
- width: "50%",
29827
- flexDirection: "column",
29828
- overflow: "hidden",
29829
- children: [visibleStatus.length > 0 ? /* @__PURE__ */ jsx(Box, {
29830
- flexDirection: "column",
29831
- paddingX: 1,
29832
- marginBottom: 1,
29833
- children: visibleStatus.map((msg, i, arr) => {
29834
- const isCurrent = i === arr.length - 1;
29835
- return /* @__PURE__ */ jsxs(Text, {
29836
- color: DIM_COLOR,
29837
- dimColor: !isCurrent,
29838
- children: [
29839
- isCurrent ? Icons.diamond : Icons.bullet,
29840
- " ",
29841
- msg
29842
- ]
29843
- }, i);
29844
- })
29845
- }) : null, helperContent]
29846
- }), /* @__PURE__ */ jsx(Box, {
29847
- width: "50%",
31010
+ overflow: "hidden",
31011
+ children: /* @__PURE__ */ jsx(AgentHelperPane, { lines: session.agentHelperLines })
31012
+ }) : null]
31013
+ });
31014
+ const [activeTab, setActiveTab] = useState("agent");
31015
+ const [seenBuildCount, setSeenBuildCount] = useState(0);
31016
+ const unreadBuildLogs = activeTab !== "build" && session.buildLogLines.length > seenBuildCount;
31017
+ useEffect(() => {
31018
+ if (activeTab === "build") setSeenBuildCount(session.buildLogLines.length);
31019
+ }, [activeTab, session.buildLogLines.length]);
31020
+ useGatedInput((_input, key) => {
31021
+ if (key.tab) setActiveTab((prev) => prev === "agent" ? "build" : "agent");
31022
+ });
31023
+ const statusMessages = session.statusMessages;
31024
+ return /* @__PURE__ */ jsxs(ScreenLayout, { children: [/* @__PURE__ */ jsx(Box, {
31025
+ flexGrow: 1,
31026
+ flexShrink: 1,
31027
+ height: topRows,
31028
+ children: isNarrow ? /* @__PURE__ */ jsx(PaneBox, {
31029
+ title: "Tarefas",
31030
+ flexGrow: 1,
31031
+ children: rightPane
31032
+ }) : /* @__PURE__ */ jsxs(Box, {
31033
+ flexDirection: "row",
31034
+ flexGrow: 1,
31035
+ gap: 1,
31036
+ children: [/* @__PURE__ */ jsxs(PaneBox, {
31037
+ width: "50%",
31038
+ children: [/* @__PURE__ */ jsx(Box, {
29848
31039
  flexDirection: "column",
31040
+ flexGrow: 1,
31041
+ flexShrink: 1,
29849
31042
  overflow: "hidden",
29850
- children: rightPane
29851
- })]
29852
- })
29853
- }),
29854
- /* @__PURE__ */ jsx(Box, {
29855
- paddingX: 1,
29856
- children: /* @__PURE__ */ jsx(Text, {
29857
- color: DIM_COLOR,
29858
- children: "─".repeat(Math.min(columns - 8, 120))
31043
+ children: helperContent
31044
+ }), statusMessages.length > 0 ? /* @__PURE__ */ jsx(Box, {
31045
+ marginTop: 1,
31046
+ flexShrink: 0,
31047
+ children: /* @__PURE__ */ jsx(ScrollableStatus, {
31048
+ messages: statusMessages,
31049
+ maxLines: 5
31050
+ })
31051
+ }) : null]
31052
+ }), /* @__PURE__ */ jsx(PaneBox, {
31053
+ title: "Tarefas",
31054
+ width: "50%",
31055
+ children: rightPane
31056
+ })]
31057
+ })
31058
+ }), /* @__PURE__ */ jsx(Box, {
31059
+ marginTop: 1,
31060
+ flexGrow: 0,
31061
+ flexShrink: 0,
31062
+ height: bottomRows,
31063
+ children: /* @__PURE__ */ jsx(PaneBox, {
31064
+ flexGrow: 1,
31065
+ header: /* @__PURE__ */ jsx(TabBar, {
31066
+ active: activeTab,
31067
+ tabs: [{
31068
+ id: "agent",
31069
+ label: "Agente"
31070
+ }, {
31071
+ id: "build",
31072
+ label: "Build",
31073
+ badge: unreadBuildLogs
31074
+ }]
31075
+ }),
31076
+ children: activeTab === "build" ? /* @__PURE__ */ jsx(BuildLogPane, {
31077
+ buildLogLines: session.buildLogLines,
31078
+ maxLines: bottomRows - 3
31079
+ }) : /* @__PURE__ */ jsx(ScrollableLogPane, {
31080
+ lines: session.agentOutputLines,
31081
+ maxLines: bottomRows - 2,
31082
+ isComplete: session.agentComplete
29859
31083
  })
29860
- }),
29861
- showBuildLogs ? /* @__PURE__ */ jsx(Box, {
31084
+ })
31085
+ })] });
31086
+ }
31087
+ function PaneBox({ title, header, children, width, flexGrow }) {
31088
+ const headerNode = header ?? (title ? /* @__PURE__ */ jsx(Text, {
31089
+ bold: true,
31090
+ color: ACCENT_COLOR,
31091
+ children: title
31092
+ }) : null);
31093
+ return /* @__PURE__ */ jsxs(Box, {
31094
+ flexDirection: "column",
31095
+ width,
31096
+ height: "100%",
31097
+ flexGrow,
31098
+ borderStyle: "single",
31099
+ borderColor: DIM_COLOR,
31100
+ paddingX: 1,
31101
+ overflow: "hidden",
31102
+ children: [headerNode ? /* @__PURE__ */ jsx(Box, {
31103
+ marginBottom: 1,
31104
+ children: headerNode
31105
+ }) : null, /* @__PURE__ */ jsx(Box, {
29862
31106
  flexDirection: "column",
29863
- height: bottomRows,
29864
- paddingX: 1,
31107
+ flexGrow: 1,
29865
31108
  overflow: "hidden",
29866
- children: /* @__PURE__ */ jsx(BuildLogPane, {
29867
- buildLogLines: session.buildLogLines,
29868
- maxLines: bottomRows - 1
29869
- })
29870
- }) : /* @__PURE__ */ jsx(ScrollableLogPane, {
29871
- lines: session.agentOutputLines,
29872
- maxLines: bottomRows,
29873
- isComplete: session.agentComplete
31109
+ children
31110
+ })]
31111
+ });
31112
+ }
31113
+ function TabBar({ tabs, active: active$1 }) {
31114
+ const [pulse, setPulse] = useState(true);
31115
+ useEffect(() => {
31116
+ if (!tabs.some((t) => t.badge && t.id !== active$1)) {
31117
+ setPulse(true);
31118
+ return;
31119
+ }
31120
+ const id = setInterval(() => setPulse((p) => !p), 500);
31121
+ return () => clearInterval(id);
31122
+ }, [tabs, active$1]);
31123
+ return /* @__PURE__ */ jsx(Box, {
31124
+ flexDirection: "row",
31125
+ gap: 2,
31126
+ children: tabs.map((tab, i) => {
31127
+ const isActive = tab.id === active$1;
31128
+ const flashing = Boolean(tab.badge) && !isActive;
31129
+ return /* @__PURE__ */ jsxs(Box, {
31130
+ flexDirection: "row",
31131
+ children: [
31132
+ /* @__PURE__ */ jsxs(Text, {
31133
+ bold: true,
31134
+ color: isActive ? ACCENT_COLOR : flashing ? pulse ? BRAND_COLOR : ACCENT_COLOR : DIM_COLOR,
31135
+ dimColor: !isActive && !flashing,
31136
+ children: [isActive ? "● " : flashing ? pulse ? "◆ " : "◇ " : "○ ", tab.label]
31137
+ }),
31138
+ flashing ? /* @__PURE__ */ jsx(Text, {
31139
+ bold: true,
31140
+ color: pulse ? BRAND_COLOR : ACCENT_COLOR,
31141
+ children: pulse ? " NOVO" : " ●"
31142
+ }) : null,
31143
+ i === tabs.length - 1 ? /* @__PURE__ */ jsxs(Text, {
31144
+ dimColor: true,
31145
+ children: [" ", "Tab para alternar"]
31146
+ }) : null
31147
+ ]
31148
+ }, tab.id);
29874
31149
  })
29875
- ] });
31150
+ });
29876
31151
  }
29877
31152
  function TaskIcon({ status }) {
29878
31153
  switch (status) {
@@ -29912,57 +31187,94 @@ function TasksPane({ store }) {
29912
31187
  const tasks = session.agentTasks;
29913
31188
  const completedCount = tasks.filter((t) => t.status === "completed").length;
29914
31189
  const totalCount = tasks.length;
31190
+ const progressText = totalCount > 0 && !session.agentComplete ? completedCount < totalCount ? `${completedCount}/${totalCount}` : "…" : null;
29915
31191
  return /* @__PURE__ */ jsxs(Box, {
29916
31192
  flexDirection: "column",
29917
- paddingX: 1,
29918
- flexGrow: 1,
29919
- children: [
29920
- /* @__PURE__ */ jsxs(Box, {
29921
- gap: 1,
29922
- children: [!session.agentComplete ? /* @__PURE__ */ jsx(Spinner, { label: phaseLabel }) : /* @__PURE__ */ jsx(Text, {
31193
+ flexShrink: 0,
31194
+ children: [/* @__PURE__ */ jsxs(Box, {
31195
+ gap: 1,
31196
+ children: [
31197
+ !session.agentComplete ? /* @__PURE__ */ jsx(Spinner, { label: phaseLabel }) : /* @__PURE__ */ jsx(Text, {
29923
31198
  bold: true,
29924
31199
  color: session.agentError ? ERROR_COLOR : SUCCESS_COLOR,
29925
31200
  children: phaseLabel
29926
- }), /* @__PURE__ */ jsxs(Text, {
31201
+ }),
31202
+ /* @__PURE__ */ jsxs(Text, {
29927
31203
  dimColor: true,
29928
31204
  children: [
29929
31205
  "(",
29930
31206
  elapsed,
29931
31207
  ")"
29932
31208
  ]
29933
- })]
29934
- }),
29935
- /* @__PURE__ */ jsx(Box, { height: 1 }),
29936
- tasks.length > 0 ? /* @__PURE__ */ jsxs(Box, {
29937
- flexDirection: "column",
29938
- children: [tasks.map((task, i) => {
29939
- const label = task.status === "in_progress" && task.activeForm ? task.activeForm : task.label;
29940
- return /* @__PURE__ */ jsxs(Box, {
29941
- gap: 1,
29942
- children: [/* @__PURE__ */ jsx(TaskIcon, { status: task.status }), /* @__PURE__ */ jsx(Text, {
29943
- dimColor: task.status === "pending",
29944
- children: label
29945
- })]
29946
- }, task.id || i);
29947
- }), totalCount > 0 && !session.agentComplete ? /* @__PURE__ */ jsx(Box, {
29948
- marginTop: 1,
29949
- children: /* @__PURE__ */ jsx(Text, {
29950
- dimColor: true,
29951
- children: completedCount < totalCount ? `Progresso: ${completedCount}/${totalCount}` : "Finalizando..."
29952
- })
29953
- }) : null]
29954
- }) : !session.agentComplete ? /* @__PURE__ */ jsx(Text, {
29955
- dimColor: true,
29956
- children: "Analisando projeto..."
29957
- }) : null,
29958
- session.retryCount > 0 ? /* @__PURE__ */ jsx(Box, {
29959
- marginTop: 1,
29960
- children: /* @__PURE__ */ jsxs(Text, {
31209
+ }),
31210
+ progressText ? /* @__PURE__ */ jsxs(Text, {
31211
+ dimColor: true,
31212
+ children: ["· ", progressText]
31213
+ }) : null,
31214
+ session.retryCount > 0 ? /* @__PURE__ */ jsxs(Text, {
29961
31215
  color: BRAND_COLOR,
29962
- children: ["Tentativa ", session.retryCount + 1]
29963
- })
29964
- }) : null
29965
- ]
31216
+ children: ["· tentativa ", session.retryCount + 1]
31217
+ }) : null
31218
+ ]
31219
+ }), tasks.length > 0 ? /* @__PURE__ */ jsx(Box, {
31220
+ flexDirection: "column",
31221
+ marginTop: 1,
31222
+ children: tasks.map((task, i) => {
31223
+ const label = task.status === "in_progress" && task.activeForm ? task.activeForm : task.label;
31224
+ return /* @__PURE__ */ jsxs(Box, {
31225
+ gap: 1,
31226
+ children: [/* @__PURE__ */ jsx(TaskIcon, { status: task.status }), /* @__PURE__ */ jsx(Text, {
31227
+ dimColor: task.status === "pending",
31228
+ wrap: "truncate",
31229
+ children: label
31230
+ })]
31231
+ }, task.id || i);
31232
+ })
31233
+ }) : null]
31234
+ });
31235
+ }
31236
+ function ScrollableStatus({ messages, maxLines }) {
31237
+ const [scrollOffset, setScrollOffset] = useState(0);
31238
+ const [userScrolled, setUserScrolled] = useState(false);
31239
+ useEffect(() => {
31240
+ if (!userScrolled) setScrollOffset(0);
31241
+ }, [messages.length, userScrolled]);
31242
+ useGatedInput((_input, key) => {
31243
+ if (key.pageUp) {
31244
+ setUserScrolled(true);
31245
+ setScrollOffset((prev) => Math.min(prev + 3, Math.max(0, messages.length - maxLines)));
31246
+ }
31247
+ if (key.pageDown) setScrollOffset((prev) => {
31248
+ const next = Math.max(0, prev - 3);
31249
+ if (next === 0) setUserScrolled(false);
31250
+ return next;
31251
+ });
31252
+ });
31253
+ const endIdx = messages.length - scrollOffset;
31254
+ const startIdx = Math.max(0, endIdx - maxLines);
31255
+ const visible = messages.slice(startIdx, endIdx);
31256
+ const isAtBottom = scrollOffset === 0;
31257
+ return /* @__PURE__ */ jsxs(Box, {
31258
+ flexDirection: "column",
31259
+ children: [!isAtBottom ? /* @__PURE__ */ jsxs(Text, {
31260
+ dimColor: true,
31261
+ children: [
31262
+ "PgUp/PgDn · ",
31263
+ scrollOffset,
31264
+ " abaixo"
31265
+ ]
31266
+ }) : null, visible.map((msg, i) => {
31267
+ const isCurrent = i === visible.length - 1 && isAtBottom;
31268
+ return /* @__PURE__ */ jsxs(Text, {
31269
+ color: DIM_COLOR,
31270
+ dimColor: !isCurrent,
31271
+ children: [
31272
+ isCurrent ? Icons.diamond : Icons.bullet,
31273
+ " ",
31274
+ msg
31275
+ ]
31276
+ }, `${startIdx + i}`);
31277
+ })]
29966
31278
  });
29967
31279
  }
29968
31280
  function ScrollableLogPane({ lines, maxLines, isComplete }) {
@@ -29975,7 +31287,7 @@ function ScrollableLogPane({ lines, maxLines, isComplete }) {
29975
31287
  useEffect(() => {
29976
31288
  if (userScrolled && scrollOffset === 0) setUserScrolled(false);
29977
31289
  }, [scrollOffset, userScrolled]);
29978
- useInput((_input, key) => {
31290
+ useGatedInput((_input, key) => {
29979
31291
  if (key.upArrow) {
29980
31292
  setUserScrolled(true);
29981
31293
  setScrollOffset((prev) => Math.min(prev + 3, Math.max(0, lines.length - contentLines)));
@@ -29992,23 +31304,18 @@ function ScrollableLogPane({ lines, maxLines, isComplete }) {
29992
31304
  return /* @__PURE__ */ jsxs(Box, {
29993
31305
  flexDirection: "column",
29994
31306
  height: maxLines,
29995
- paddingX: 1,
29996
31307
  overflow: "hidden",
29997
- children: [/* @__PURE__ */ jsxs(Box, {
29998
- gap: 1,
29999
- children: [/* @__PURE__ */ jsx(Text, {
30000
- bold: true,
30001
- color: ACCENT_COLOR,
30002
- children: "Agente"
30003
- }), !(scrollOffset === 0) ? /* @__PURE__ */ jsxs(Text, {
31308
+ children: [!(scrollOffset === 0) ? /* @__PURE__ */ jsx(Box, {
31309
+ marginBottom: 1,
31310
+ children: /* @__PURE__ */ jsxs(Text, {
30004
31311
  dimColor: true,
30005
31312
  children: [
30006
31313
  "↑↓ rolar · ",
30007
31314
  scrollOffset,
30008
31315
  " linhas abaixo"
30009
31316
  ]
30010
- }) : null]
30011
- }), visibleLines.length > 0 ? visibleLines.map((line, i) => {
31317
+ })
31318
+ }) : null, visibleLines.length > 0 ? visibleLines.map((line, i) => {
30012
31319
  if (line.startsWith("[tool] ")) return /* @__PURE__ */ jsx(Text, {
30013
31320
  color: CYAN_COLOR,
30014
31321
  wrap: "wrap",
@@ -30026,12 +31333,37 @@ function ScrollableLogPane({ lines, maxLines, isComplete }) {
30026
31333
  });
30027
31334
  }
30028
31335
  function BuildLogPane({ buildLogLines, maxLines }) {
30029
- const recentLines = buildLogLines.slice(-maxLines);
30030
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Text, {
30031
- bold: true,
30032
- color: CYAN_COLOR,
30033
- children: "BUILD"
30034
- }), recentLines.map((line, i) => {
31336
+ const contentLines = Math.max(1, maxLines - 1);
31337
+ const [scrollOffset, setScrollOffset] = useState(0);
31338
+ const [userScrolled, setUserScrolled] = useState(false);
31339
+ useEffect(() => {
31340
+ if (!userScrolled) setScrollOffset(0);
31341
+ }, [buildLogLines.length, userScrolled]);
31342
+ useGatedInput((_input, key) => {
31343
+ if (key.upArrow) {
31344
+ setUserScrolled(true);
31345
+ setScrollOffset((prev) => Math.min(prev + 3, Math.max(0, buildLogLines.length - contentLines)));
31346
+ }
31347
+ if (key.downArrow) setScrollOffset((prev) => {
31348
+ const next = Math.max(0, prev - 3);
31349
+ if (next === 0) setUserScrolled(false);
31350
+ return next;
31351
+ });
31352
+ });
31353
+ const endIdx = buildLogLines.length - scrollOffset;
31354
+ const startIdx = Math.max(0, endIdx - contentLines);
31355
+ const recentLines = buildLogLines.slice(startIdx, endIdx);
31356
+ return /* @__PURE__ */ jsxs(Fragment, { children: [!(scrollOffset === 0) ? /* @__PURE__ */ jsx(Box, {
31357
+ marginBottom: 1,
31358
+ children: /* @__PURE__ */ jsxs(Text, {
31359
+ dimColor: true,
31360
+ children: [
31361
+ "↑↓ rolar · ",
31362
+ scrollOffset,
31363
+ " linhas abaixo"
31364
+ ]
31365
+ })
31366
+ }) : null, recentLines.map((line, i) => {
30035
31367
  if (line.startsWith("[status] ")) {
30036
31368
  const statusText = line.slice(9);
30037
31369
  return /* @__PURE__ */ jsxs(Text, {
@@ -30108,10 +31440,12 @@ function AgentHelperPane({ lines }) {
30108
31440
  if (i === 0 && line.trim()) return /* @__PURE__ */ jsx(Text, {
30109
31441
  bold: true,
30110
31442
  color: ACCENT_COLOR,
31443
+ wrap: "truncate",
30111
31444
  children: line
30112
31445
  }, i);
30113
31446
  return /* @__PURE__ */ jsx(Text, {
30114
31447
  color: DIM_COLOR,
31448
+ wrap: "truncate",
30115
31449
  children: line
30116
31450
  }, i);
30117
31451
  })
@@ -30147,6 +31481,17 @@ function MarkdownText({ children }) {
30147
31481
  children: children.split("\n").map((line, i) => {
30148
31482
  const trimmed = line.trimStart();
30149
31483
  if (trimmed === "") return /* @__PURE__ */ jsx(Box, { height: 1 }, i);
31484
+ const headingMatch = trimmed.match(/^(#{1,3})\s+(.*)$/);
31485
+ if (headingMatch) {
31486
+ const level = headingMatch[1]?.length ?? 1;
31487
+ const content = headingMatch[2] ?? "";
31488
+ return /* @__PURE__ */ jsx(Text, {
31489
+ bold: true,
31490
+ color: level === 1 ? BRAND_COLOR : level === 2 ? ACCENT_COLOR : void 0,
31491
+ wrap: "wrap",
31492
+ children: renderInline(content, `h${level}-${i}`)
31493
+ }, i);
31494
+ }
30150
31495
  if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) {
30151
31496
  const content = trimmed.slice(2);
30152
31497
  return /* @__PURE__ */ jsxs(Box, {
@@ -30183,7 +31528,7 @@ function OutroScreen({ store }) {
30183
31528
  const [columns] = useStdoutDimensions();
30184
31529
  const sepWidth = Math.min(columns - 8, 80);
30185
31530
  const aiSetup = useMemo(() => checkAiSetup(), []);
30186
- useInput((_input, key) => {
31531
+ useGatedInput((_input, key) => {
30187
31532
  if (key.return || key.escape) {
30188
31533
  store.dismissOutro();
30189
31534
  process.exit(isSuccess$4 ? 0 : 1);
@@ -30234,10 +31579,12 @@ function OutroScreen({ store }) {
30234
31579
  if (i === 0 && line.trim()) return /* @__PURE__ */ jsx(Text, {
30235
31580
  bold: true,
30236
31581
  color: ACCENT_COLOR,
31582
+ wrap: "truncate",
30237
31583
  children: line
30238
31584
  }, i);
30239
31585
  return /* @__PURE__ */ jsx(Text, {
30240
31586
  color: DIM_COLOR,
31587
+ wrap: "truncate",
30241
31588
  children: line
30242
31589
  }, i);
30243
31590
  })
@@ -30381,7 +31728,7 @@ function Separator({ width }) {
30381
31728
  //#endregion
30382
31729
  //#region src/wizard/ui/overlays/ErrorOverlay.tsx
30383
31730
  function ErrorOverlay({ message, onDismiss }) {
30384
- useInput((_input, key) => {
31731
+ useGatedInput((_input, key) => {
30385
31732
  if (key.return || key.escape) onDismiss();
30386
31733
  });
30387
31734
  return /* @__PURE__ */ jsxs(Box, {
@@ -30415,7 +31762,7 @@ function ErrorOverlay({ message, onDismiss }) {
30415
31762
  //#region src/wizard/ui/overlays/ConfirmExitOverlay.tsx
30416
31763
  function ConfirmExitOverlay({ onConfirm, onCancel }) {
30417
31764
  const [selected, setSelected] = useState(1);
30418
- useInput((_input, key) => {
31765
+ useGatedInput((_input, key) => {
30419
31766
  if (key.upArrow || key.downArrow) setSelected((s) => s === 0 ? 1 : 0);
30420
31767
  if (key.return) if (selected === 0) onConfirm();
30421
31768
  else onCancel();
@@ -30462,6 +31809,11 @@ function ConfirmExitOverlay({ onConfirm, onCancel }) {
30462
31809
  * Shows question + hint with pulse animation. Supports text input,
30463
31810
  * confirm (yes/no), and choice selection. User can skip env prompts.
30464
31811
  */
31812
+ /** Preferred prompt box width, clamped to terminal columns minus margin. */
31813
+ function useOverlayWidth() {
31814
+ const [columns] = useStdoutDimensions();
31815
+ return Math.max(30, Math.min(60, columns - 4));
31816
+ }
30465
31817
  function PromptOverlay({ prompt: prompt$1, onSubmit, onSkip }) {
30466
31818
  const [pulse, setPulse] = useState(true);
30467
31819
  useEffect(() => {
@@ -30489,6 +31841,7 @@ function PromptOverlay({ prompt: prompt$1, onSubmit, onSkip }) {
30489
31841
  function TextPrompt({ prompt: prompt$1, pulse, onSubmit, onSkip }) {
30490
31842
  const [value, setValue] = useState("");
30491
31843
  const [mode, setMode] = useState("ask");
31844
+ const width = useOverlayWidth();
30492
31845
  useInput((input, key) => {
30493
31846
  if (mode === "ask") {
30494
31847
  if (input === "1" || key.return) setMode("input");
@@ -30515,7 +31868,7 @@ function TextPrompt({ prompt: prompt$1, pulse, onSubmit, onSkip }) {
30515
31868
  borderColor: pulse ? BRAND_COLOR : ACCENT_COLOR,
30516
31869
  paddingX: 2,
30517
31870
  paddingY: 1,
30518
- width: 60,
31871
+ width,
30519
31872
  children: [
30520
31873
  /* @__PURE__ */ jsxs(Box, {
30521
31874
  gap: 1,
@@ -30530,10 +31883,7 @@ function TextPrompt({ prompt: prompt$1, pulse, onSubmit, onSkip }) {
30530
31883
  }),
30531
31884
  /* @__PURE__ */ jsx(Box, {
30532
31885
  marginTop: 1,
30533
- children: /* @__PURE__ */ jsx(Text, {
30534
- wrap: "wrap",
30535
- children: prompt$1.question
30536
- })
31886
+ children: /* @__PURE__ */ jsx(MarkdownText, { children: prompt$1.question })
30537
31887
  }),
30538
31888
  prompt$1.hint ? /* @__PURE__ */ jsxs(Box, {
30539
31889
  marginTop: 1,
@@ -30542,10 +31892,9 @@ function TextPrompt({ prompt: prompt$1, pulse, onSubmit, onSkip }) {
30542
31892
  color: CYAN_COLOR,
30543
31893
  bold: true,
30544
31894
  children: "Dica:"
30545
- }), /* @__PURE__ */ jsxs(Text, {
30546
- color: DIM_COLOR,
30547
- wrap: "wrap",
30548
- children: [" ", prompt$1.hint]
31895
+ }), /* @__PURE__ */ jsx(Box, {
31896
+ marginLeft: 1,
31897
+ children: /* @__PURE__ */ jsx(MarkdownText, { children: prompt$1.hint })
30549
31898
  })]
30550
31899
  }) : null,
30551
31900
  prompt$1.envKey ? /* @__PURE__ */ jsx(Box, {
@@ -30598,6 +31947,7 @@ function TextPrompt({ prompt: prompt$1, pulse, onSubmit, onSkip }) {
30598
31947
  }
30599
31948
  function ConfirmPrompt({ prompt: prompt$1, pulse, onSubmit }) {
30600
31949
  const [selected, setSelected] = useState(0);
31950
+ const width = useOverlayWidth();
30601
31951
  useInput((_input, key) => {
30602
31952
  if (key.upArrow || key.downArrow) setSelected((s) => s === 0 ? 1 : 0);
30603
31953
  if (key.return) onSubmit(selected === 0 ? "sim" : "não");
@@ -30608,7 +31958,7 @@ function ConfirmPrompt({ prompt: prompt$1, pulse, onSubmit }) {
30608
31958
  borderColor: pulse ? BRAND_COLOR : ACCENT_COLOR,
30609
31959
  paddingX: 2,
30610
31960
  paddingY: 1,
30611
- width: 60,
31961
+ width,
30612
31962
  children: [
30613
31963
  /* @__PURE__ */ jsxs(Box, {
30614
31964
  gap: 1,
@@ -30623,10 +31973,7 @@ function ConfirmPrompt({ prompt: prompt$1, pulse, onSubmit }) {
30623
31973
  }),
30624
31974
  /* @__PURE__ */ jsx(Box, {
30625
31975
  marginTop: 1,
30626
- children: /* @__PURE__ */ jsx(Text, {
30627
- wrap: "wrap",
30628
- children: prompt$1.question
30629
- })
31976
+ children: /* @__PURE__ */ jsx(MarkdownText, { children: prompt$1.question })
30630
31977
  }),
30631
31978
  /* @__PURE__ */ jsx(Box, {
30632
31979
  marginTop: 1,
@@ -30648,6 +31995,7 @@ function ConfirmPrompt({ prompt: prompt$1, pulse, onSubmit }) {
30648
31995
  function ChoicePrompt({ prompt: prompt$1, pulse, onSubmit, onSkip }) {
30649
31996
  const options = prompt$1.options ?? [];
30650
31997
  const [selected, setSelected] = useState(0);
31998
+ const width = useOverlayWidth();
30651
31999
  useInput((_input, key) => {
30652
32000
  if (key.upArrow) setSelected((s) => Math.max(0, s - 1));
30653
32001
  if (key.downArrow) setSelected((s) => Math.min(options.length - 1, s + 1));
@@ -30660,7 +32008,7 @@ function ChoicePrompt({ prompt: prompt$1, pulse, onSubmit, onSkip }) {
30660
32008
  borderColor: pulse ? BRAND_COLOR : ACCENT_COLOR,
30661
32009
  paddingX: 2,
30662
32010
  paddingY: 1,
30663
- width: 60,
32011
+ width,
30664
32012
  children: [
30665
32013
  /* @__PURE__ */ jsxs(Box, {
30666
32014
  gap: 1,
@@ -30675,18 +32023,11 @@ function ChoicePrompt({ prompt: prompt$1, pulse, onSubmit, onSkip }) {
30675
32023
  }),
30676
32024
  /* @__PURE__ */ jsx(Box, {
30677
32025
  marginTop: 1,
30678
- children: /* @__PURE__ */ jsx(Text, {
30679
- wrap: "wrap",
30680
- children: prompt$1.question
30681
- })
32026
+ children: /* @__PURE__ */ jsx(MarkdownText, { children: prompt$1.question })
30682
32027
  }),
30683
32028
  prompt$1.hint ? /* @__PURE__ */ jsx(Box, {
30684
32029
  marginTop: 1,
30685
- children: /* @__PURE__ */ jsx(Text, {
30686
- color: DIM_COLOR,
30687
- wrap: "wrap",
30688
- children: prompt$1.hint
30689
- })
32030
+ children: /* @__PURE__ */ jsx(MarkdownText, { children: prompt$1.hint })
30690
32031
  }) : null,
30691
32032
  /* @__PURE__ */ jsx(Box, {
30692
32033
  marginTop: 1,
@@ -30717,83 +32058,82 @@ function ChoicePrompt({ prompt: prompt$1, pulse, onSubmit, onSkip }) {
30717
32058
  //#region src/wizard/ui/App.tsx
30718
32059
  function App({ store, router, cwd }) {
30719
32060
  useSyncExternalStore(store.subscribe, store.getSnapshot);
32061
+ const [, rows] = useStdoutDimensions();
30720
32062
  const { session } = store;
30721
32063
  const writePromptResponse = useCallback((value, skipped) => {
30722
- writeFileSync(join(cwd, ".veloz-wizard-response.json"), JSON.stringify({
32064
+ const responsePath = join(cwd, ".veloz-wizard-response.json");
32065
+ getSessionLogger().log("prompt-response", "user responded to agent prompt", {
32066
+ skipped,
32067
+ bytes: value.length,
32068
+ promptId: session.agentPrompt?.id ?? null,
32069
+ envKey: session.agentPrompt?.envKey ?? null
32070
+ });
32071
+ writeFileSync(responsePath, JSON.stringify({
30723
32072
  value,
30724
32073
  skipped
30725
32074
  }));
30726
32075
  store.clearAgentPrompt();
30727
- }, [cwd, store]);
30728
- if (session.agentPrompt) return /* @__PURE__ */ jsx(PromptOverlay, {
30729
- prompt: session.agentPrompt,
30730
- onSubmit: (value) => writePromptResponse(value, false),
30731
- onSkip: () => writePromptResponse("", true)
30732
- });
32076
+ }, [
32077
+ cwd,
32078
+ store,
32079
+ session.agentPrompt
32080
+ ]);
30733
32081
  const overlay = router.activeOverlay;
30734
- if (overlay === Overlay.ConfirmExit) return /* @__PURE__ */ jsx(ConfirmExitOverlay, {
32082
+ let baseLayer;
32083
+ if (overlay === Overlay.ConfirmExit) baseLayer = /* @__PURE__ */ jsx(ConfirmExitOverlay, {
30735
32084
  onConfirm: () => {
30736
32085
  router.popOverlay();
30737
32086
  process.exit(130);
30738
32087
  },
30739
32088
  onCancel: () => router.popOverlay()
30740
32089
  });
30741
- if (overlay === Overlay.Error) return /* @__PURE__ */ jsx(ErrorOverlay, {
32090
+ else if (overlay === Overlay.Error) baseLayer = /* @__PURE__ */ jsx(ErrorOverlay, {
30742
32091
  message: session.agentError ?? "Erro desconhecido",
30743
32092
  onDismiss: () => router.popOverlay()
30744
32093
  });
30745
- switch (router.resolve(session)) {
30746
- case Screen.Intro: return /* @__PURE__ */ jsx(IntroScreen, { store });
30747
- case Screen.Auth: return /* @__PURE__ */ jsx(AuthScreen, { store });
30748
- case Screen.Run: return /* @__PURE__ */ jsx(RunScreen, { store });
30749
- case Screen.Outro: return /* @__PURE__ */ jsx(OutroScreen, { store });
30750
- default: return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(IntroScreen, { store }) });
32094
+ else switch (router.resolve(session)) {
32095
+ case Screen.Intro:
32096
+ baseLayer = /* @__PURE__ */ jsx(IntroScreen, { store });
32097
+ break;
32098
+ case Screen.Auth:
32099
+ baseLayer = /* @__PURE__ */ jsx(AuthScreen, { store });
32100
+ break;
32101
+ case Screen.Notes:
32102
+ baseLayer = /* @__PURE__ */ jsx(NotesScreen, { store });
32103
+ break;
32104
+ case Screen.Run:
32105
+ baseLayer = /* @__PURE__ */ jsx(RunScreen, { store });
32106
+ break;
32107
+ case Screen.Outro:
32108
+ baseLayer = /* @__PURE__ */ jsx(OutroScreen, { store });
32109
+ break;
32110
+ default: baseLayer = /* @__PURE__ */ jsx(IntroScreen, { store });
30751
32111
  }
30752
- }
30753
-
30754
- //#endregion
30755
- //#region src/wizard/session.ts
30756
- function createInitialSession() {
30757
- return {
30758
- isAuthenticated: false,
30759
- userName: "",
30760
- orgId: "",
30761
- orgName: "",
30762
- apiKey: "",
30763
- authPhase: "checking",
30764
- authDeviceCode: null,
30765
- authVerificationUrl: null,
30766
- availableOrgs: [],
30767
- selectedOrgId: null,
30768
- detectedFramework: null,
30769
- detectedFrameworkLabel: null,
30770
- selectedFramework: null,
30771
- selectedFrameworkLabel: null,
30772
- packageManager: "npm",
30773
- gitRemote: null,
30774
- isMonorepo: false,
30775
- agentPhase: "idle",
30776
- agentTasks: [],
30777
- agentLogLines: [],
30778
- agentComplete: false,
30779
- agentError: null,
30780
- deployUrl: null,
30781
- retryCount: 0,
30782
- buildLogLines: [],
30783
- buildStreamActive: false,
30784
- buildDeploymentId: null,
30785
- statusMessages: [],
30786
- agentOutputLines: [],
30787
- toolCallLog: [],
30788
- learnCardBlockIdx: 0,
30789
- learnCardComplete: false,
30790
- agentHelperLines: [],
30791
- agentPrompt: null,
30792
- agentSummary: null,
30793
- introConfirmed: false,
30794
- outroDismissed: false,
30795
- agentStartedAt: null
30796
- };
32112
+ const prompt$1 = session.agentPrompt;
32113
+ return /* @__PURE__ */ jsxs(Box, {
32114
+ flexDirection: "column",
32115
+ height: rows,
32116
+ children: [/* @__PURE__ */ jsx(InputFocusProvider, {
32117
+ active: !prompt$1,
32118
+ children: /* @__PURE__ */ jsx(Box, {
32119
+ flexDirection: "column",
32120
+ flexGrow: 1,
32121
+ flexShrink: 1,
32122
+ minHeight: 0,
32123
+ overflow: "hidden",
32124
+ children: baseLayer
32125
+ })
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
+ })
32135
+ }) : null]
32136
+ });
30797
32137
  }
30798
32138
 
30799
32139
  //#endregion
@@ -30874,6 +32214,12 @@ var WizardStore = class {
30874
32214
  this.$session.setKey("introConfirmed", true);
30875
32215
  this.emitChange();
30876
32216
  }
32217
+ confirmNotes(notes) {
32218
+ const trimmed = notes?.trim();
32219
+ this.$session.setKey("userNotes", trimmed && trimmed.length > 0 ? trimmed : null);
32220
+ this.$session.setKey("notesConfirmed", true);
32221
+ this.emitChange();
32222
+ }
30877
32223
  setAuth(data) {
30878
32224
  this.$session.setKey("isAuthenticated", true);
30879
32225
  this.$session.setKey("userName", data.userName);
@@ -30902,7 +32248,32 @@ var WizardStore = class {
30902
32248
  this.$session.setKey("selectedOrgId", orgId);
30903
32249
  this.emitChange();
30904
32250
  }
32251
+ /** User chose "create new" from the org picker — switch to the create screen. */
32252
+ requestOrgCreation() {
32253
+ this.$session.setKey("authPhase", "org-create");
32254
+ this.$session.setKey("newOrgName", null);
32255
+ this.$session.setKey("orgCreateError", null);
32256
+ this.emitChange();
32257
+ }
32258
+ /** User submitted a name on the org-create screen. The runner picks this up via a gate. */
32259
+ submitNewOrgName(name) {
32260
+ this.$session.setKey("newOrgName", name);
32261
+ this.$session.setKey("authPhase", "org-creating");
32262
+ this.$session.setKey("orgCreateError", null);
32263
+ this.emitChange();
32264
+ }
32265
+ /** Creation failed — surface the error and send the user back to the input. */
32266
+ failOrgCreation(error) {
32267
+ this.$session.setKey("orgCreateError", error);
32268
+ this.$session.setKey("newOrgName", null);
32269
+ this.$session.setKey("authPhase", "org-create");
32270
+ this.emitChange();
32271
+ }
30905
32272
  setAgentPhase(phase) {
32273
+ getSessionLogger().logStoreEvent("agentPhase", {
32274
+ from: this.session.agentPhase,
32275
+ to: phase
32276
+ });
30906
32277
  this.$session.setKey("agentPhase", phase);
30907
32278
  if (phase !== "idle" && !this.session.agentStartedAt) this.$session.setKey("agentStartedAt", Date.now());
30908
32279
  this.emitChange();
@@ -30913,6 +32284,14 @@ var WizardStore = class {
30913
32284
  this.emitChange();
30914
32285
  }
30915
32286
  syncTasks(tasks) {
32287
+ getSessionLogger().logStoreEvent("syncTasks", {
32288
+ count: tasks.length,
32289
+ tasks: tasks.map((t) => ({
32290
+ id: t.id,
32291
+ label: t.label,
32292
+ status: t.status
32293
+ }))
32294
+ });
30916
32295
  this.$session.setKey("agentTasks", tasks);
30917
32296
  this.emitChange();
30918
32297
  }
@@ -30925,15 +32304,18 @@ var WizardStore = class {
30925
32304
  this.emitChange();
30926
32305
  }
30927
32306
  setDeployUrl(url) {
32307
+ getSessionLogger().logStoreEvent("setDeployUrl", { url });
30928
32308
  this.$session.setKey("deployUrl", url);
30929
32309
  this.emitChange();
30930
32310
  }
30931
32311
  completeAgent() {
32312
+ getSessionLogger().logStoreEvent("completeAgent");
30932
32313
  this.$session.setKey("agentComplete", true);
30933
32314
  this.$session.setKey("agentPhase", "done");
30934
32315
  this.emitChange();
30935
32316
  }
30936
32317
  failAgent(error) {
32318
+ getSessionLogger().logStoreEvent("failAgent", { error });
30937
32319
  this.$session.setKey("agentError", error);
30938
32320
  this.$session.setKey("agentPhase", "error");
30939
32321
  this.$session.setKey("agentComplete", true);
@@ -30951,6 +32333,7 @@ var WizardStore = class {
30951
32333
  pushStatus(message) {
30952
32334
  const msgs = this.session.statusMessages;
30953
32335
  if (msgs.length > 0 && msgs[msgs.length - 1] === message) return;
32336
+ getSessionLogger().logStoreEvent("status", { message });
30954
32337
  this.$session.setKey("statusMessages", [...msgs, message].slice(-50));
30955
32338
  this.emitChange();
30956
32339
  }
@@ -30989,32 +32372,43 @@ var WizardStore = class {
30989
32372
  }
30990
32373
  /** Show a prompt overlay to the user. */
30991
32374
  setAgentPrompt(prompt$1) {
32375
+ getSessionLogger().logStoreEvent("setAgentPrompt", {
32376
+ id: prompt$1.id,
32377
+ type: prompt$1.type,
32378
+ envKey: prompt$1.envKey,
32379
+ question: prompt$1.question
32380
+ });
30992
32381
  this.$session.setKey("agentPrompt", prompt$1);
30993
32382
  this.emitChange();
30994
32383
  }
30995
32384
  /** Clear the prompt overlay after user responds. */
30996
32385
  clearAgentPrompt() {
32386
+ getSessionLogger().logStoreEvent("clearAgentPrompt");
30997
32387
  this.$session.setKey("agentPrompt", null);
30998
32388
  this.emitChange();
30999
32389
  }
31000
32390
  /** Set the agent's final summary for the outro screen. */
31001
32391
  setAgentSummary(summary) {
32392
+ getSessionLogger().logStoreEvent("setAgentSummary", { bytes: summary.length });
31002
32393
  this.$session.setKey("agentSummary", summary);
31003
32394
  this.emitChange();
31004
32395
  }
31005
32396
  /** Push a build log line from the deployment stream. */
31006
32397
  pushBuildLog(line) {
32398
+ getSessionLogger().logBuildStream("line", { line });
31007
32399
  const lines = [...this.session.buildLogLines, line].slice(-500);
31008
32400
  this.$session.setKey("buildLogLines", lines);
31009
32401
  this.emitChange();
31010
32402
  }
31011
32403
  /** Mark the build log stream as active/inactive. */
31012
- setBuildStreamActive(active) {
31013
- this.$session.setKey("buildStreamActive", active);
32404
+ setBuildStreamActive(active$1) {
32405
+ getSessionLogger().logBuildStream("active", { active: active$1 });
32406
+ this.$session.setKey("buildStreamActive", active$1);
31014
32407
  this.emitChange();
31015
32408
  }
31016
32409
  /** Set the deployment ID being streamed. */
31017
32410
  setBuildDeploymentId(id) {
32411
+ getSessionLogger().logBuildStream("deploymentId", { id });
31018
32412
  this.$session.setKey("buildDeploymentId", id);
31019
32413
  this.emitChange();
31020
32414
  }
@@ -31042,7 +32436,12 @@ function startTUI(cwd) {
31042
32436
  let sigintCount = 0;
31043
32437
  process.on("SIGINT", () => {
31044
32438
  sigintCount++;
31045
- if (sigintCount >= 2) process.exit(130);
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
+ }
31046
32445
  router.pushOverlay(Overlay.ConfirmExit);
31047
32446
  });
31048
32447
  return {
@@ -31053,6 +32452,122 @@ function startTUI(cwd) {
31053
32452
  };
31054
32453
  }
31055
32454
 
32455
+ //#endregion
32456
+ //#region src/wizard/upload-telemetry.ts
32457
+ /**
32458
+ * Upload per-session debug logs to MGC via presigned URLs.
32459
+ *
32460
+ * Flow:
32461
+ * 1. Read meta.json from the session folder
32462
+ * 2. Call `initTelemetry.start` → server creates InitSession row + returns
32463
+ * a presigned PUT URL for each file
32464
+ * 3. PUT each file to MGC directly (no server round-trip)
32465
+ * 4. Call `initTelemetry.finalize` → server patches the row with outcome
32466
+ *
32467
+ * Runs best-effort. Errors are logged to stderr only so they never block
32468
+ * `veloz init`. Disabled via `VELOZ_NO_TELEMETRY=1`.
32469
+ */
32470
+ const CONTENT_TYPES = {
32471
+ "meta.json": "application/json",
32472
+ "events.jsonl": "application/x-ndjson",
32473
+ "sdk-messages.jsonl": "application/x-ndjson",
32474
+ "tool-calls.jsonl": "application/x-ndjson",
32475
+ "store-events.jsonl": "application/x-ndjson",
32476
+ "build-stream.jsonl": "application/x-ndjson",
32477
+ "mcp-tools.jsonl": "application/x-ndjson",
32478
+ "session.log": "text/plain; charset=utf-8",
32479
+ "prompts.md": "text/markdown; charset=utf-8"
32480
+ };
32481
+ function contentTypeFor(filename) {
32482
+ return CONTENT_TYPES[filename] ?? "application/octet-stream";
32483
+ }
32484
+ 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
+ }
32489
+ const metaPath = join(sessionDir, "meta.json");
32490
+ let meta;
32491
+ try {
32492
+ 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`);
32495
+ return;
32496
+ }
32497
+ const authConfig = loadConfig();
32498
+ if (!authConfig.apiKey) {
32499
+ process.stderr.write("[veloz init] sem apiKey — telemetria não enviada\n");
32500
+ return;
32501
+ }
32502
+ const files = readdirSync(sessionDir).filter((name) => {
32503
+ const full = join(sessionDir, name);
32504
+ try {
32505
+ const s = statSync(full);
32506
+ return s.isFile() && s.size > 0;
32507
+ } catch {
32508
+ return false;
32509
+ }
32510
+ });
32511
+ if (files.length === 0) return;
32512
+ const client = createClient(authConfig.apiUrl, () => {
32513
+ const headers = {
32514
+ Authorization: `Bearer ${authConfig.apiKey}`,
32515
+ "User-Agent": "veloz-cli/telemetry"
32516
+ };
32517
+ if (authConfig.organizationId) headers["X-Organization-Id"] = authConfig.organizationId;
32518
+ return headers;
32519
+ });
32520
+ let started;
32521
+ try {
32522
+ started = await client.initTelemetry.start({
32523
+ clientSessionId: meta.sessionId,
32524
+ cwd: meta.cwd,
32525
+ framework: meta.framework,
32526
+ frameworkLabel: meta.frameworkLabel,
32527
+ packageManager: meta.packageManager,
32528
+ isMonorepo: meta.isMonorepo,
32529
+ gitRemote: meta.gitRemote ?? void 0,
32530
+ userName: meta.userName,
32531
+ orgName: meta.orgName,
32532
+ node: meta.node,
32533
+ platform: meta.platform,
32534
+ pid: meta.pid,
32535
+ startedAt: meta.startedAt,
32536
+ files
32537
+ });
32538
+ } catch (err) {
32539
+ process.stderr.write(`[veloz init] telemetria (start) falhou: ${err instanceof Error ? err.message : String(err)}\n`);
32540
+ return;
32541
+ }
32542
+ const failures = (await Promise.allSettled(files.map(async (filename) => {
32543
+ const url = started.uploads[filename];
32544
+ if (!url) return;
32545
+ const body = readFileSync(join(sessionDir, filename));
32546
+ const res = await fetch(url, {
32547
+ method: "PUT",
32548
+ headers: { "Content-Type": contentTypeFor(filename) },
32549
+ body
32550
+ });
32551
+ 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`);
32554
+ try {
32555
+ await client.initTelemetry.finalize({
32556
+ id: started.id,
32557
+ outcome: meta.outcome ?? "unknown",
32558
+ error: meta.error ?? null,
32559
+ deployUrl: meta.deployUrl ?? null,
32560
+ retries: meta.retries,
32561
+ durationMs: meta.durationMs,
32562
+ endedAt: meta.endedAt
32563
+ });
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`);
32569
+ }
32570
+
31056
32571
  //#endregion
31057
32572
  //#region src/wizard/index.ts
31058
32573
  /**
@@ -31102,18 +32617,37 @@ function registerInit(cli$1) {
31102
32617
  outputPolicy: "agent-only",
31103
32618
  async run(c) {
31104
32619
  const cwd = process.cwd();
32620
+ const logger = initSessionLogger({ cwd });
32621
+ logger.log("init", "veloz init invoked", {
32622
+ cwd,
32623
+ framework: c.options.framework ?? null,
32624
+ ci: c.options.ci,
32625
+ isTTY: Boolean(process.stdout.isTTY),
32626
+ argv: process.argv.slice(2)
32627
+ });
32628
+ process.stderr.write(`[veloz init] debug logs: ${logger.dir}\n`);
31105
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
+ });
31106
32636
  const detectedFrameworkId = analysis.framework ? DETECTOR_TO_FRAMEWORK[analysis.framework.name] ?? "nodejs" : null;
31107
32637
  const detectedLabel = analysis.framework?.label ?? null;
31108
32638
  const forcedFramework = c.options.framework;
31109
- if (!process.stdout.isTTY || c.options.ci) return await runHeadless({
31110
- cwd,
31111
- frameworkId: forcedFramework ?? detectedFrameworkId ?? "nodejs",
31112
- frameworkLabel: forcedFramework ? FRAMEWORK_LABELS[forcedFramework] ?? forcedFramework : detectedLabel ?? "Node.js",
31113
- packageManager: analysis.packageManager ?? "npm"
31114
- });
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"
32646
+ });
32647
+ }
31115
32648
  const tui = startTUI();
31116
32649
  const { store } = tui;
32650
+ logger.log("tui", "TUI started");
31117
32651
  store.setDetection({
31118
32652
  framework: detectedFrameworkId,
31119
32653
  frameworkLabel: detectedLabel,
@@ -31125,10 +32659,37 @@ function registerInit(cli$1) {
31125
32659
  store.selectFramework(forcedFramework, FRAMEWORK_LABELS[forcedFramework] ?? forcedFramework);
31126
32660
  store.confirmIntro();
31127
32661
  }
32662
+ logger.log("flow", "awaiting intro confirmation");
31128
32663
  await tui.waitForIntro();
32664
+ logger.log("flow", "intro confirmed");
31129
32665
  const selectedFramework = store.session.selectedFramework ?? detectedFrameworkId ?? "nodejs";
31130
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");
31131
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
32685
+ });
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
32691
+ });
32692
+ logger.log("agent", "invoking runAgent");
31132
32693
  await runAgent({
31133
32694
  store,
31134
32695
  cwd,
@@ -31136,12 +32697,29 @@ function registerInit(cli$1) {
31136
32697
  frameworkLabel: selectedLabel,
31137
32698
  userName: store.session.userName,
31138
32699
  orgName: store.session.orgName,
31139
- packageManager: store.session.packageManager ?? "npm"
32700
+ packageManager: store.session.packageManager ?? "npm",
32701
+ userNotes: store.session.userNotes
31140
32702
  });
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
32708
+ });
32709
+ logger.log("flow", "awaiting outro dismissal");
31141
32710
  await store.getGate("outro", (s) => s.outroDismissed);
32711
+ logger.log("flow", "outro dismissed");
31142
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
32719
+ });
32720
+ await uploadInitTelemetry(logger.dir);
31143
32721
  return {
31144
- success: !store.session.agentError,
32722
+ success: success$1,
31145
32723
  url: store.session.deployUrl ?? void 0
31146
32724
  };
31147
32725
  }
@@ -31150,6 +32728,8 @@ function registerInit(cli$1) {
31150
32728
  async function runHeadless(opts) {
31151
32729
  const { WizardStore: WizardStore$1 } = await import("./store-CFF2J0lW.mjs");
31152
32730
  const { runAgent: run } = await import("./agent-interface-DL5S8SEn.mjs");
32731
+ const logger = getSessionLogger();
32732
+ logger.log("headless", "resolving auth (non-interactive)");
31153
32733
  const config = await requireAuth({ nonInteractive: true });
31154
32734
  const store = new WizardStore$1();
31155
32735
  store.setAuth({
@@ -31163,6 +32743,14 @@ async function runHeadless(opts) {
31163
32743
  const lastLog = s.agentLogLines[s.agentLogLines.length - 1];
31164
32744
  if (lastLog) process.stderr.write(`[veloz init] ${lastLog}\n`);
31165
32745
  });
32746
+ logger.updateMeta({
32747
+ framework: opts.frameworkId,
32748
+ frameworkLabel: opts.frameworkLabel,
32749
+ packageManager: opts.packageManager,
32750
+ userName: "CI",
32751
+ orgId: config.organizationId ?? ""
32752
+ });
32753
+ logger.log("headless", "invoking runAgent");
31166
32754
  await run({
31167
32755
  store,
31168
32756
  cwd: opts.cwd,
@@ -31172,29 +32760,18 @@ async function runHeadless(opts) {
31172
32760
  orgName: "",
31173
32761
  packageManager: opts.packageManager
31174
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);
31175
32770
  return {
31176
- success: !store.session.agentError,
32771
+ success: success$1,
31177
32772
  url: store.session.deployUrl ?? void 0
31178
32773
  };
31179
32774
  }
31180
- function isMultipleOrgsError(error) {
31181
- if (!(error instanceof Error)) return false;
31182
- const e = error;
31183
- return e.data?.code === "MULTIPLE_ORGS" && Array.isArray(e.data?.organizations);
31184
- }
31185
- function isNoOrgError(error) {
31186
- if (!(error instanceof Error)) return false;
31187
- return error.data?.code === "NO_ORGANIZATION";
31188
- }
31189
- /**
31190
- * Build a Better Auth client with org plugin (extends the device auth client).
31191
- */
31192
- function buildOrgAuthClient(apiUrl) {
31193
- return createAuthClient({
31194
- baseURL: apiUrl,
31195
- plugins: [organizationClient()]
31196
- });
31197
- }
31198
32775
  /**
31199
32776
  * Perform device auth with TUI feedback — reuses shared helpers from login.ts.
31200
32777
  * No console.log or ora spinners; all progress goes through the store.
@@ -31209,100 +32786,141 @@ async function performDeviceAuth(store, apiUrl) {
31209
32786
  store.setAuthPhase("polling");
31210
32787
  return pollForToken(authClient, data.device_code, data.interval || 5);
31211
32788
  }
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) => ({
32797
+ id: o.id,
32798
+ name: o.name,
32799
+ slug: o.slug
32800
+ }));
32801
+ }
31212
32802
  /**
31213
- * Build an ORPC API client with explicit credentials (no requireAuth prompts).
32803
+ * Create a new organization via Better Auth. The slug is derived from the name
32804
+ * and suffixed with a random token when a collision occurs.
31214
32805
  */
31215
- function buildApiClient(apiUrl, apiKey, organizationId) {
31216
- return createClient(apiUrl, () => {
31217
- const headers = {
31218
- Authorization: `Bearer ${apiKey}`,
31219
- "User-Agent": "veloz-cli"
31220
- };
31221
- if (organizationId) headers["X-Organization-Id"] = organizationId;
31222
- return headers;
31223
- });
32806
+ async function createOrganization(apiUrl, apiKey, name) {
32807
+ const baseSlug = name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "workspace";
32808
+ let slug = baseSlug;
32809
+ for (let attempt = 0; attempt < 3; attempt++) {
32810
+ const res = await fetch(`${apiUrl}/api/auth/organization/create`, {
32811
+ method: "POST",
32812
+ headers: {
32813
+ Authorization: `Bearer ${apiKey}`,
32814
+ "Content-Type": "application/json",
32815
+ "User-Agent": "veloz-cli"
32816
+ },
32817
+ body: JSON.stringify({
32818
+ name,
32819
+ slug
32820
+ })
32821
+ });
32822
+ if (res.ok) {
32823
+ const data = await res.json();
32824
+ return {
32825
+ id: data.id,
32826
+ name: data.name,
32827
+ slug: data.slug
32828
+ };
32829
+ }
32830
+ const errorText = await res.text().catch(() => "");
32831
+ if (res.status === 400 && errorText.toLowerCase().includes("already")) {
32832
+ slug = `${baseSlug}-${Math.random().toString(36).slice(2, 6)}`;
32833
+ continue;
32834
+ }
32835
+ throw new Error(errorText || `Falha ao criar workspace (${res.status}).`);
32836
+ }
32837
+ throw new Error("Não foi possível gerar um slug único para o workspace.");
31224
32838
  }
31225
32839
  /**
31226
32840
  * Full TUI-aware auth + org resolution.
31227
32841
  *
31228
32842
  * 1. If already authenticated, skip login
31229
32843
  * 2. If not, perform device auth with TUI feedback
31230
- * 3. Resolve organization (single/multi/none)
31231
- * 4. If no org, auto-create one
32844
+ * 3. Always show the org picker (or the create screen if the user has no orgs)
32845
+ * 4. Persist the selected/created org id
31232
32846
  * 5. Update store.setAuth() when done
31233
32847
  */
31234
32848
  async function resolveAuthAndOrg(store) {
32849
+ const logger = getSessionLogger();
31235
32850
  let config = loadConfig();
31236
32851
  if (!isAuthenticated()) {
31237
32852
  const envApiKey = process.env.VELOZ_API_KEY;
31238
32853
  if (envApiKey) {
32854
+ logger.log("auth", "using VELOZ_API_KEY from env");
31239
32855
  saveConfig({ apiKey: envApiKey });
31240
32856
  config = loadConfig();
31241
32857
  } else {
32858
+ logger.log("auth", "starting device auth flow", { apiUrl: config.apiUrl });
31242
32859
  store.setAuthPhase("browser");
31243
32860
  const token = await performDeviceAuth(store, config.apiUrl);
31244
- if (!token) throw new Error("Autenticação cancelada ou expirada.");
32861
+ if (!token) {
32862
+ logger.log("auth", "device auth failed or expired");
32863
+ throw new Error("Autenticação cancelada ou expirada.");
32864
+ }
32865
+ logger.log("auth", "device auth succeeded");
31245
32866
  saveConfig({ apiKey: token });
31246
32867
  config = loadConfig();
31247
32868
  }
31248
- }
31249
- const projectConfig = loadConfig$1();
31250
- const needsOrgResolution = !config.organizationId && !projectConfig?.project?.id;
31251
- let orgName = "";
31252
- if (needsOrgResolution) {
31253
- const client = buildApiClient(config.apiUrl, config.apiKey);
31254
- try {
31255
- await client.projects.list();
31256
- } catch (error) {
31257
- if (isMultipleOrgsError(error)) {
31258
- const orgs = error.data.organizations;
31259
- if (orgs.length === 1) {
31260
- const org = orgs[0];
31261
- saveConfig({ organizationId: org.id });
31262
- config = loadConfig();
31263
- orgName = org.name;
31264
- } else {
31265
- store.setAvailableOrgs(orgs);
31266
- await store.getGate("org-select", (s) => s.selectedOrgId !== null);
31267
- const selectedOrgId = store.session.selectedOrgId;
31268
- const selectedOrg = orgs.find((o) => o.id === selectedOrgId);
31269
- saveConfig({ organizationId: selectedOrgId });
31270
- config = loadConfig();
31271
- orgName = selectedOrg?.name ?? selectedOrgId;
31272
- }
31273
- } else if (isNoOrgError(error)) {
31274
- store.setAuthPhase("no-org");
31275
- const authClient = buildOrgAuthClient(config.apiUrl);
31276
- const userName = process.env.USER ?? process.env.USERNAME ?? "meu-workspace";
31277
- const slug = userName.toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 30);
31278
- const createResult = await authClient.organization.create({
31279
- name: `${userName}'s workspace`,
31280
- slug
31281
- });
31282
- if (createResult.error || !createResult.data) {
31283
- await openBrowser(`${config.apiUrl.includes("localhost") ? "http://localhost:3001" : "https://app.onveloz.com"}/onboarding`);
31284
- throw new Error("Não foi possível criar o workspace automaticamente. Crie um em app.onveloz.com e tente novamente.", { cause: createResult.error });
31285
- }
31286
- const newOrg = createResult.data;
31287
- saveConfig({ organizationId: newOrg.id });
31288
- config = loadConfig();
31289
- orgName = newOrg.name;
31290
- } else throw error;
32869
+ } else logger.log("auth", "already authenticated from config");
32870
+ logger.log("auth", "fetching organizations");
32871
+ const orgs = await listOrganizations(config.apiUrl, config.apiKey);
32872
+ logger.log("auth", "organizations loaded", { count: orgs.length });
32873
+ let chosen;
32874
+ if (orgs.length === 0) {
32875
+ store.setAuthPhase("org-create");
32876
+ chosen = await runOrgCreationFlow(store, config.apiUrl, config.apiKey);
32877
+ } else {
32878
+ store.setAvailableOrgs(orgs);
32879
+ 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);
32881
+ else {
32882
+ const selectedOrgId = store.session.selectedOrgId;
32883
+ const match$3 = orgs.find((o) => o.id === selectedOrgId);
32884
+ if (!match$3) throw new Error("Workspace selecionado não encontrado.");
32885
+ chosen = match$3;
31291
32886
  }
31292
32887
  }
32888
+ saveConfig({ organizationId: chosen.id });
32889
+ config = loadConfig();
31293
32890
  store.setAuth({
31294
32891
  userName: config.apiKey ? "Usuário" : "",
31295
- orgId: config.organizationId ?? "",
31296
- orgName,
32892
+ orgId: chosen.id,
32893
+ orgName: chosen.name,
31297
32894
  apiKey: config.apiKey
31298
32895
  });
31299
32896
  }
32897
+ /**
32898
+ * Drive the org-create screen: wait for the user to submit a name, try to
32899
+ * create it, and retry on failure without leaving the wizard.
32900
+ */
32901
+ async function runOrgCreationFlow(store, apiUrl, apiKey) {
32902
+ store.requestOrgCreation();
32903
+ for (;;) {
32904
+ await store.getGate(`org-create-${Date.now()}-${Math.random()}`, (s) => s.newOrgName !== null && s.authPhase === "org-creating");
32905
+ const name = store.session.newOrgName?.trim() ?? "";
32906
+ if (!name) {
32907
+ store.failOrgCreation("Digite um nome para o workspace.");
32908
+ continue;
32909
+ }
32910
+ try {
32911
+ return await createOrganization(apiUrl, apiKey, name);
32912
+ } catch (error) {
32913
+ const message = error instanceof Error ? error.message : String(error);
32914
+ store.failOrgCreation(message);
32915
+ }
32916
+ }
32917
+ }
31300
32918
 
31301
32919
  //#endregion
31302
32920
  //#region src/index.ts
31303
32921
  if (process.argv.includes("--mcp")) process.env.VELOZ_MCP = "true";
31304
32922
  const cli = Cli.create("veloz", {
31305
- version: "0.0.0-beta.30",
32923
+ version: "0.0.0-beta.31",
31306
32924
  description: "CLI da plataforma Veloz — deploy rápido para o Brasil",
31307
32925
  env: z.object({ VELOZ_ENV: z.string().optional().describe("Ambiente alvo (ex: preview, staging)") }),
31308
32926
  mcp: { command: "npx -y onveloz --mcp" }