@neetru/cli 2.32.1 → 2.34.0

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 (63) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +38 -3
  3. package/dist/commands/admin/sdk-credential.js +3 -1
  4. package/dist/commands/ai-models.d.ts +2 -1
  5. package/dist/commands/ai-models.js +4 -5
  6. package/dist/commands/ai.d.ts +12 -2
  7. package/dist/commands/ai.js +47 -38
  8. package/dist/commands/auth.d.ts +3 -2
  9. package/dist/commands/auth.js +96 -57
  10. package/dist/commands/build.js +35 -4
  11. package/dist/commands/chat.d.ts +24 -6
  12. package/dist/commands/chat.js +43 -7
  13. package/dist/commands/code.js +11 -2
  14. package/dist/commands/config.js +15 -13
  15. package/dist/commands/db-frontdoor.d.ts +116 -0
  16. package/dist/commands/db-frontdoor.js +572 -0
  17. package/dist/commands/db-move.d.ts +5 -0
  18. package/dist/commands/db-move.js +27 -0
  19. package/dist/commands/db.d.ts +8 -2
  20. package/dist/commands/db.js +13 -6
  21. package/dist/commands/deploy.d.ts +14 -5
  22. package/dist/commands/deploy.js +376 -55
  23. package/dist/commands/dev-context.js +1 -1
  24. package/dist/commands/environments-repoint.d.ts +49 -0
  25. package/dist/commands/environments-repoint.js +219 -0
  26. package/dist/commands/init.js +7 -1
  27. package/dist/commands/new.js +14 -0
  28. package/dist/commands/secrets.d.ts +6 -4
  29. package/dist/commands/secrets.js +14 -7
  30. package/dist/commands/servers.d.ts +29 -0
  31. package/dist/commands/servers.js +108 -1
  32. package/dist/commands/ui.js +1 -1
  33. package/dist/commands/workspaces.js +16 -1
  34. package/dist/index.js +103 -7
  35. package/dist/lib/ai/auth-discovery.d.ts +9 -12
  36. package/dist/lib/ai/auth-discovery.js +43 -145
  37. package/dist/lib/ai/orchestrator.d.ts +13 -6
  38. package/dist/lib/ai/orchestrator.js +46 -246
  39. package/dist/lib/ai/provedores.d.ts +57 -0
  40. package/dist/lib/ai/provedores.js +66 -0
  41. package/dist/lib/api-client.d.ts +8 -1
  42. package/dist/lib/api-client.js +18 -2
  43. package/dist/lib/build-targets.d.ts +48 -10
  44. package/dist/lib/build-targets.js +64 -10
  45. package/dist/lib/cli-write.js +6 -3
  46. package/dist/lib/code-agent-gateway.d.ts +2 -2
  47. package/dist/lib/code-agent-gateway.js +2 -2
  48. package/dist/lib/code-agent-settings.d.ts +2 -2
  49. package/dist/lib/code-agent-settings.js +2 -4
  50. package/dist/lib/code-agent-types.d.ts +3 -1
  51. package/dist/lib/config.d.ts +4 -7
  52. package/dist/lib/config.js +9 -6
  53. package/dist/lib/deploy-recusa-de-destino.d.ts +9 -0
  54. package/dist/lib/deploy-recusa-de-destino.js +98 -0
  55. package/dist/lib/deploy-render-probe.d.ts +117 -0
  56. package/dist/lib/deploy-render-probe.js +475 -0
  57. package/dist/lib/deploy-vm-um-app.d.ts +62 -0
  58. package/dist/lib/deploy-vm-um-app.js +83 -0
  59. package/dist/lib/segredo-produto.d.ts +25 -5
  60. package/dist/lib/segredo-produto.js +45 -10
  61. package/dist/lib/sse.d.ts +4 -5
  62. package/dist/lib/sse.js +4 -5
  63. package/package.json +1 -2
@@ -80,25 +80,63 @@ export interface LayoutStandalone {
80
80
  /** Entrypoint relativo à raiz do tar: `server.js` ou `apps/web/server.js`. */
81
81
  entrypoint: string;
82
82
  }
83
+ /**
84
+ * O que o próprio Next.js declarou sobre o lugar do app dentro do standalone.
85
+ *
86
+ * - `declarado`: `relativeAppDir` válido, já em formato POSIX (`''` = app na
87
+ * raiz do standalone, `'apps/web'` = monorepo);
88
+ * - `invalido`: o campo existe mas aponta pra fora da raiz de tracing
89
+ * (absoluto ou com `..`) — o `outputFileTracingRoot` não está ACIMA do app;
90
+ * - `ausente`: sem o arquivo, JSON ilegível ou sem o campo (Next.js antigo).
91
+ */
92
+ export type RelativeAppDirLido = {
93
+ tipo: 'declarado';
94
+ prefixo: string;
95
+ } | {
96
+ tipo: 'invalido';
97
+ valor: string;
98
+ } | {
99
+ tipo: 'ausente';
100
+ };
101
+ /**
102
+ * Lê `relativeAppDir` de `<appDir>/.next/required-server-files.json`.
103
+ *
104
+ * bug_678ddc98 (comportamento esperado do relato): é o Next.js que decide onde o
105
+ * `server.js` cai dentro do standalone, e ele grava essa decisão neste arquivo
106
+ * no MESMO build — `relativeAppDir = path.relative(outputFileTracingRoot,
107
+ * appDir)`, a mesma conta que ele usa pra escrever
108
+ * `.next/standalone/<relativeAppDir>/server.js`. Ler a declaração dispensa
109
+ * adivinhar pela árvore.
110
+ *
111
+ * Num build feito no Windows o valor vem com `\`; aqui sai sempre com `/`, que
112
+ * é o formato do membro no tar e do `entrypoint` no deploy.
113
+ */
114
+ export declare function lerRelativeAppDir(appDir: string): RelativeAppDirLido;
83
115
  /**
84
116
  * Acha ONDE o `server.js` do Next.js ficou dentro de `.next/standalone`.
85
117
  *
86
118
  * Em repo de app único ele está na raiz; em monorepo o Next.js aninha o app sob
87
119
  * o caminho dele relativo ao `outputFileTracingRoot` (`apps/web/server.js`).
88
120
  * Não dá pra deduzir esse prefixo do `next.config` (o `outputFileTracingRoot`
89
- * pode ser implícito, vindo da detecção de workspace do próprio Next.js), então
90
- * a verdade é a árvore PÓS-BUILD — a mesma lógica do `isStandaloneBuildDir`.
121
+ * pode ser implícito, vindo da detecção de workspace do próprio Next.js).
91
122
  *
92
- * Varredura em largura, pulando `node_modules` (pacote dependente pode ter um
93
- * `server.js` qualquer e envenenar a busca). Entre candidatos, ganha o que
94
- * TAMBÉM tem um diretório `.next` ao lado — é o par que o Next.js sempre emite
95
- * junto (`server.js` + `.next/` com os chunks de servidor). Empate real resolve
96
- * pelo mais raso e, em seguida, por ordem alfabética (determinismo: dois builds
97
- * da mesma árvore têm que dar o mesmo tarball).
123
+ * 1. Primeiro vale o que o Next.js DECLAROU (`relativeAppDir` em
124
+ * `.next/required-server-files.json`, ver `lerRelativeAppDir`). Se ele
125
+ * declarou um lugar e o `server.js` não está lá — ou declarou um lugar fora
126
+ * da raiz de tracing — devolve `null`: o build está incoerente e escolher
127
+ * outro `server.js` da árvore empacotaria o app errado.
128
+ * 2. Sem a declaração (Next.js antigo), a verdade é a árvore PÓS-BUILD — a
129
+ * mesma lógica do `isStandaloneBuildDir`. Varredura em largura, pulando
130
+ * `node_modules` (pacote dependente pode ter um `server.js` qualquer e
131
+ * envenenar a busca). Entre candidatos, ganha o que TAMBÉM tem um diretório
132
+ * `.next` ao lado — é o par que o Next.js sempre emite junto (`server.js` +
133
+ * `.next/` com os chunks de servidor). Empate real resolve pelo mais raso e,
134
+ * em seguida, por ordem alfabética (determinismo: dois builds da mesma árvore
135
+ * têm que dar o mesmo tarball).
98
136
  *
99
137
  * Devolve `null` quando não há standalone nenhum (build não-standalone) ou
100
- * quando há standalone mas nenhum `server.js` — nesse segundo caso quem chama
101
- * precisa FALHAR, nunca empacotar um tarball sem entrypoint.
138
+ * quando há standalone mas nenhum `server.js` no lugar certo — nesse segundo
139
+ * caso quem chama precisa FALHAR, nunca empacotar um tarball sem entrypoint.
102
140
  */
103
141
  export declare function resolveStandaloneLayout(appDir: string): LayoutStandalone | null;
104
142
  /**
@@ -230,25 +230,65 @@ export function comandoExecBin(pm, bin, args) {
230
230
  }
231
231
  /** Profundidade máxima de varredura dentro do standalone à procura do server.js. */
232
232
  const MAX_PROFUNDIDADE_STANDALONE = 6;
233
+ /**
234
+ * Lê `relativeAppDir` de `<appDir>/.next/required-server-files.json`.
235
+ *
236
+ * bug_678ddc98 (comportamento esperado do relato): é o Next.js que decide onde o
237
+ * `server.js` cai dentro do standalone, e ele grava essa decisão neste arquivo
238
+ * no MESMO build — `relativeAppDir = path.relative(outputFileTracingRoot,
239
+ * appDir)`, a mesma conta que ele usa pra escrever
240
+ * `.next/standalone/<relativeAppDir>/server.js`. Ler a declaração dispensa
241
+ * adivinhar pela árvore.
242
+ *
243
+ * Num build feito no Windows o valor vem com `\`; aqui sai sempre com `/`, que
244
+ * é o formato do membro no tar e do `entrypoint` no deploy.
245
+ */
246
+ export function lerRelativeAppDir(appDir) {
247
+ const json = lerJson(path.join(appDir, '.next', 'required-server-files.json'));
248
+ const bruto = json?.relativeAppDir;
249
+ if (typeof bruto !== 'string')
250
+ return { tipo: 'ausente' };
251
+ const posix = bruto.replace(/\\/g, '/');
252
+ if (posix.startsWith('/') || /^[A-Za-z]:/.test(posix))
253
+ return { tipo: 'invalido', valor: bruto };
254
+ const partes = posix.split('/').filter((p) => p !== '' && p !== '.');
255
+ if (partes.includes('..'))
256
+ return { tipo: 'invalido', valor: bruto };
257
+ return { tipo: 'declarado', prefixo: partes.join('/') };
258
+ }
259
+ function ehArquivo(p) {
260
+ try {
261
+ return fs.statSync(p).isFile();
262
+ }
263
+ catch {
264
+ return false;
265
+ }
266
+ }
233
267
  /**
234
268
  * Acha ONDE o `server.js` do Next.js ficou dentro de `.next/standalone`.
235
269
  *
236
270
  * Em repo de app único ele está na raiz; em monorepo o Next.js aninha o app sob
237
271
  * o caminho dele relativo ao `outputFileTracingRoot` (`apps/web/server.js`).
238
272
  * Não dá pra deduzir esse prefixo do `next.config` (o `outputFileTracingRoot`
239
- * pode ser implícito, vindo da detecção de workspace do próprio Next.js), então
240
- * a verdade é a árvore PÓS-BUILD — a mesma lógica do `isStandaloneBuildDir`.
273
+ * pode ser implícito, vindo da detecção de workspace do próprio Next.js).
241
274
  *
242
- * Varredura em largura, pulando `node_modules` (pacote dependente pode ter um
243
- * `server.js` qualquer e envenenar a busca). Entre candidatos, ganha o que
244
- * TAMBÉM tem um diretório `.next` ao lado — é o par que o Next.js sempre emite
245
- * junto (`server.js` + `.next/` com os chunks de servidor). Empate real resolve
246
- * pelo mais raso e, em seguida, por ordem alfabética (determinismo: dois builds
247
- * da mesma árvore têm que dar o mesmo tarball).
275
+ * 1. Primeiro vale o que o Next.js DECLAROU (`relativeAppDir` em
276
+ * `.next/required-server-files.json`, ver `lerRelativeAppDir`). Se ele
277
+ * declarou um lugar e o `server.js` não está lá — ou declarou um lugar fora
278
+ * da raiz de tracing — devolve `null`: o build está incoerente e escolher
279
+ * outro `server.js` da árvore empacotaria o app errado.
280
+ * 2. Sem a declaração (Next.js antigo), a verdade é a árvore PÓS-BUILD — a
281
+ * mesma lógica do `isStandaloneBuildDir`. Varredura em largura, pulando
282
+ * `node_modules` (pacote dependente pode ter um `server.js` qualquer e
283
+ * envenenar a busca). Entre candidatos, ganha o que TAMBÉM tem um diretório
284
+ * `.next` ao lado — é o par que o Next.js sempre emite junto (`server.js` +
285
+ * `.next/` com os chunks de servidor). Empate real resolve pelo mais raso e,
286
+ * em seguida, por ordem alfabética (determinismo: dois builds da mesma árvore
287
+ * têm que dar o mesmo tarball).
248
288
  *
249
289
  * Devolve `null` quando não há standalone nenhum (build não-standalone) ou
250
- * quando há standalone mas nenhum `server.js` — nesse segundo caso quem chama
251
- * precisa FALHAR, nunca empacotar um tarball sem entrypoint.
290
+ * quando há standalone mas nenhum `server.js` no lugar certo — nesse segundo
291
+ * caso quem chama precisa FALHAR, nunca empacotar um tarball sem entrypoint.
252
292
  */
253
293
  export function resolveStandaloneLayout(appDir) {
254
294
  const standaloneDir = path.join(appDir, '.next', 'standalone');
@@ -259,6 +299,20 @@ export function resolveStandaloneLayout(appDir) {
259
299
  catch {
260
300
  return null;
261
301
  }
302
+ const declarado = lerRelativeAppDir(appDir);
303
+ if (declarado.tipo === 'invalido')
304
+ return null;
305
+ if (declarado.tipo === 'declarado') {
306
+ const prefixo = declarado.prefixo;
307
+ const server = path.join(standaloneDir, ...prefixo.split('/').filter(Boolean), 'server.js');
308
+ if (!ehArquivo(server))
309
+ return null;
310
+ return {
311
+ standaloneDir,
312
+ appPrefix: prefixo,
313
+ entrypoint: prefixo ? `${prefixo}/server.js` : 'server.js',
314
+ };
315
+ }
262
316
  const candidatos = [];
263
317
  const fila = [
264
318
  { dir: standaloneDir, prefix: '', profundidade: 0 },
@@ -308,14 +308,17 @@ export async function resolveStepUp(opts) {
308
308
  // P105: o comando alternativo (`neetru auth mfa-session`) também só é
309
309
  // citado se existe na árvore do Commander.
310
310
  const sessao = (withCommand, withoutCommand) => adviseCommand({ command: 'auth mfa-session', withCommand, withoutCommand });
311
+ // Etapa 1 do bug_89d2497e: o texto dizia "modo --json/não-interativo", e
312
+ // `vm provision` e `agent release` chegam aqui sem declarar --json. O
313
+ // ramo é o não-interativo (`--json` é um caso dele), e é isso que se diz.
311
314
  const msg = adviseFlag({
312
315
  flag: '--mfa-token',
313
316
  withFlag: sessao('Operação destrutiva top-tier exige step-up MFA: passe --mfa-token <código TOTP> ' +
314
- '(modo --json/não-interativo) ou rode `neetru auth mfa-session` para obter uma sessão de 1h.', 'Operação destrutiva top-tier exige step-up MFA: passe --mfa-token <código TOTP> ' +
315
- '(modo --json/não-interativo).'),
317
+ '(modo não-interativo) ou rode `neetru auth mfa-session` para obter uma sessão de 1h.', 'Operação destrutiva top-tier exige step-up MFA: passe --mfa-token <código TOTP> ' +
318
+ '(modo não-interativo).'),
316
319
  withoutFlag: sessao('Operação destrutiva top-tier exige step-up MFA: rode `neetru auth mfa-session` ' +
317
320
  'para obter uma sessão de 1h e repita o comando.', 'Operação destrutiva top-tier exige step-up MFA, e esta CLI não tem, neste comando, ' +
318
- 'como enviá-lo em modo --json/não-interativo.'),
321
+ 'como enviá-lo em modo não-interativo.'),
319
322
  });
320
323
  exitWithCliError(new NeetruCliError(msg, { code: 'mfa_required', exit: EXIT_CODES.client }), opts.json);
321
324
  return { headers }; // safeExit não retorna em prod; satisfaz o tipo + testes
@@ -16,8 +16,8 @@ export declare function callGateway(settings: CodeSettings, messages: CodeMessag
16
16
  * resposta se o AI Gateway suportar"). Mesmo padrão de `streamGateway` em
17
17
  * `src/lib/ai/orchestrator.ts` (fetch direto + `readSseLines` compartilhado) —
18
18
  * não passa por `apiRequest`/`apiStream` porque nenhum dos dois fala SSE, e
19
- * não importa o orchestrator inteiro só por essa função (ele carrega
20
- * @anthropic-ai/sdk + openai, que `neetru code` não precisa).
19
+ * não importa o orchestrator inteiro só por essa função (ele carrega o SDK
20
+ * `openai`, que `neetru code` não precisa).
21
21
  *
22
22
  * Acumula os deltas e retorna o texto completo — o protocolo
23
23
  * `NEETRU_TOOL`/`NEETRU_PATCH` só pode ser parseado sobre a resposta inteira,
@@ -34,8 +34,8 @@ export async function callGateway(settings, messages, signal) {
34
34
  * resposta se o AI Gateway suportar"). Mesmo padrão de `streamGateway` em
35
35
  * `src/lib/ai/orchestrator.ts` (fetch direto + `readSseLines` compartilhado) —
36
36
  * não passa por `apiRequest`/`apiStream` porque nenhum dos dois fala SSE, e
37
- * não importa o orchestrator inteiro só por essa função (ele carrega
38
- * @anthropic-ai/sdk + openai, que `neetru code` não precisa).
37
+ * não importa o orchestrator inteiro só por essa função (ele carrega o SDK
38
+ * `openai`, que `neetru code` não precisa).
39
39
  *
40
40
  * Acumula os deltas e retorna o texto completo — o protocolo
41
41
  * `NEETRU_TOOL`/`NEETRU_PATCH` só pode ser parseado sobre a resposta inteira,
@@ -1,3 +1,4 @@
1
+ import { type ProvedorIa } from './ai/provedores.js';
1
2
  export declare const DEFAULT_CODE_MODEL = "llama-3.3-70b-versatile";
2
3
  export declare const DEFAULT_CODE_MAX_TURNS = 8;
3
4
  export declare const DEFAULT_CODE_MAX_TOKENS = 4096;
@@ -24,8 +25,7 @@ export interface ResolvedCodeModel {
24
25
  }
25
26
  export declare function isAnthropicModel(model: string | undefined): boolean;
26
27
  export declare function resolveCodeModel(input: ResolveCodeModelInput): ResolvedCodeModel;
27
- declare const KNOWN_CATALOG_PROVIDERS: readonly ["groq", "nvidia", "openai"];
28
- type CatalogProvider = (typeof KNOWN_CATALOG_PROVIDERS)[number];
28
+ type CatalogProvider = ProvedorIa;
29
29
  /**
30
30
  * GAP-M1-16 (2026-07-10): resolve o provider a forçar no request — prioridade
31
31
  * `--provider` (flag explícita da tarefa) sobre `gatewayProvider` configurado
@@ -1,3 +1,4 @@
1
+ import { ehProvedorIa } from './ai/provedores.js';
1
2
  export const DEFAULT_CODE_MODEL = 'llama-3.3-70b-versatile';
2
3
  export const DEFAULT_CODE_MAX_TURNS = 8;
3
4
  export const DEFAULT_CODE_MAX_TOKENS = 4096;
@@ -37,7 +38,6 @@ export function resolveCodeModel(input) {
37
38
  }
38
39
  return { model: DEFAULT_CODE_MODEL };
39
40
  }
40
- const KNOWN_CATALOG_PROVIDERS = ['groq', 'nvidia', 'openai'];
41
41
  /**
42
42
  * Nomes de modelo NATIVOS de um único provider do catálogo curado
43
43
  * (model-catalog.ts no Core, GAP-M1-16) — duplicados aqui de propósito pra
@@ -68,9 +68,7 @@ const NATIVE_MODEL_PROVIDER = {
68
68
  export function resolveCodeProvider(cliProvider, configuredProvider, model) {
69
69
  const explicit = firstNonEmpty(cliProvider, configuredProvider);
70
70
  if (explicit) {
71
- return KNOWN_CATALOG_PROVIDERS.includes(explicit)
72
- ? explicit
73
- : undefined;
71
+ return ehProvedorIa(explicit) ? explicit : undefined;
74
72
  }
75
73
  // Nenhum dos dois informado — tenta inferir do nome do modelo antes de
76
74
  // desistir pro WRRR livre (ver doc de NATIVE_MODEL_PROVIDER acima).
@@ -3,6 +3,7 @@
3
3
  * (code.ts, code-agent-ui.ts, code-agent-gateway.ts, code-agent-tools.ts,
4
4
  * code-agent-session.ts) — evita import circular de tipo entre eles.
5
5
  */
6
+ import type { ProvedorIa } from './ai/provedores.js';
6
7
  export type CodeRole = 'system' | 'user' | 'assistant';
7
8
  export interface CodeMessage {
8
9
  role: CodeRole;
@@ -33,7 +34,8 @@ export interface CodeSettings {
33
34
  files: string[];
34
35
  model: string;
35
36
  /** GAP-M1-16 (2026-07-10) — `neetru ai models` lista as opções; ausente = WRRR entre todos. */
36
- provider?: 'groq' | 'nvidia' | 'openai';
37
+ provider?: ProvedorIa;
38
+ /** Aviso mostrado ao abrir: modelo trocado e/ou `--provider` fora da lista (uma linha cada). */
37
39
  modelWarning?: string;
38
40
  maxTurns: number;
39
41
  maxTokens: number;
@@ -1,25 +1,22 @@
1
1
  import Conf from 'conf';
2
+ import { type ModeloDoAi, type ProvedorIa } from './ai/provedores.js';
2
3
  interface NeetruConfig {
3
- anthropicApiKey?: string;
4
4
  openaiApiKey?: string;
5
- geminiApiKey?: string;
6
5
  groqApiKey?: string;
7
6
  nvidiaApiKey?: string;
8
7
  groqModel?: string;
9
8
  nvidiaModel?: string;
10
- claudeModel?: string;
11
9
  openaiModel?: string;
12
- geminiModel?: string;
13
10
  gatewayModel?: string;
14
- gatewayProvider?: 'groq' | 'nvidia' | 'openai';
11
+ gatewayProvider?: ProvedorIa;
15
12
  gatewayEffort?: 'low' | 'medium' | 'high';
16
13
  neetruApiKey?: string;
17
14
  neetruApiUrl: string;
18
- defaultModel: 'claude' | 'openai' | 'gemini' | 'groq' | 'nvidia' | 'gateway' | 'auto';
15
+ defaultModel: ModeloDoAi;
19
16
  projectName?: string;
20
17
  'telemetry.enabled'?: boolean;
21
18
  }
22
19
  export declare const config: Conf<NeetruConfig>;
23
20
  export declare function getKey(name: keyof NeetruConfig): string | boolean | undefined;
24
- export declare function requireKey(name: 'anthropicApiKey' | 'openaiApiKey' | 'geminiApiKey' | 'neetruApiKey'): string;
21
+ export declare function requireKey(name: 'openaiApiKey' | 'neetruApiKey'): string;
25
22
  export {};
@@ -1,4 +1,5 @@
1
1
  import Conf from 'conf';
2
+ import { PROVEDORES_IA } from './ai/provedores.js';
2
3
  const defaults = {
3
4
  neetruApiUrl: 'https://api.neetru.com',
4
5
  defaultModel: 'auto',
@@ -13,24 +14,26 @@ export const config = new Conf({
13
14
  configFileMode: 0o600,
14
15
  defaults,
15
16
  schema: {
16
- anthropicApiKey: { type: 'string' },
17
17
  openaiApiKey: { type: 'string' },
18
- geminiApiKey: { type: 'string' },
19
18
  groqApiKey: { type: 'string' },
20
19
  nvidiaApiKey: { type: 'string' },
21
20
  groqModel: { type: 'string' },
22
21
  nvidiaModel: { type: 'string' },
23
- claudeModel: { type: 'string' },
24
22
  openaiModel: { type: 'string' },
25
- geminiModel: { type: 'string' },
26
23
  gatewayModel: { type: 'string' },
27
- gatewayProvider: { type: 'string', enum: ['groq', 'nvidia', 'openai'] },
24
+ gatewayProvider: { type: 'string', enum: [...PROVEDORES_IA] },
28
25
  gatewayEffort: { type: 'string', enum: ['low', 'medium', 'high'] },
29
26
  neetruApiKey: { type: 'string' },
30
27
  neetruApiUrl: { type: 'string', default: 'https://api.neetru.com' },
28
+ // SEM `enum` de propósito (bug_19d312f3): o `conf` valida o arquivo inteiro
29
+ // em TODA leitura, e um config.json com `defaultModel: "claude"` ou
30
+ // `"gemini"` (gravado por versão antiga) derrubaria qualquer comando da CLI
31
+ // com "Config schema violation". Quem confere o valor: `neetru config set
32
+ // defaultModel` (recusa o que não está em `MODELOS_DO_AI`), o `neetru ai`
33
+ // sem `-m` (`runAiRepl` sai com 1 e diz como voltar ao `auto`) e o `chat()`
34
+ // do orchestrator, para quem o chama sem modelo.
31
35
  defaultModel: {
32
36
  type: 'string',
33
- enum: ['claude', 'openai', 'gemini', 'groq', 'nvidia', 'gateway', 'auto'],
34
37
  default: 'auto',
35
38
  },
36
39
  'telemetry.enabled': { type: 'boolean' },
@@ -0,0 +1,9 @@
1
+ /** O que dizer sobre a recusa, em linhas. Exportado para teste. */
2
+ export declare function explicacaoDaRecusaDeDestino(productSlug: string, ambiente: string, destinoAtual: string, destinoPedido: string): string[];
3
+ /**
4
+ * Se `err` é a recusa de destino do Core, devolve o mesmo erro canônico do
5
+ * `handleApiError` (mesmo código, mesmo exit, mesmo `message`) com a explicação
6
+ * no texto humano e, no `--json`, em `hint` — mais `ambiente`, `destinoAtual` e
7
+ * `destinoPedido`. Qualquer outro erro volta intacto.
8
+ */
9
+ export declare function explicarRecusaDeDestino(err: unknown, productSlug: string): unknown;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Recusa do Core "Ambiente <x> tem destino '<a>', não '<b>'" — o que a CLI diz
3
+ * ao dev quando o deploy esbarra nela.
4
+ *
5
+ * ORIGEM — bug_6d92ef71 (rodada 4) e bug_fe30f035 (Core, aberto).
6
+ *
7
+ * O aviso do `--port` e o `neetru deploy --help` (`deploy-vm-um-app.ts`)
8
+ * ensinam que dois apps na VM são dois produtos: `neetru products create` e o
9
+ * deploy de cada um com o seu `--product`. A revisão da cli#99 mediu no Core
10
+ * (origin/main dc45be28; conferido de novo em f60ea772) que esse caminho
11
+ * esbarra, hoje, num defeito do Core:
12
+ * - `_createProductCore` (`_products-core.ts`) cria staging e prod chamando
13
+ * `_ensureEnvironmentCore` sem `target`, e o padrão é 'workspace';
14
+ * - `_deployToVmCore` (e `_deployToCloudRunCore`) chama `_ensureEnvironmentCore`
15
+ * pedindo o próprio destino, mas o destino só vale na CRIAÇÃO: o ambiente que
16
+ * já existe volta como está, e o deploy lança "Ambiente prod tem destino
17
+ * 'workspace', não 'vm'. Migre o destino antes (changeEnvironmentTarget) ou
18
+ * use o destino atual." — a rota `/api/cli/v1/deploy` responde 400
19
+ * (`vm_deploy_failed` / `cloud_run_deploy_failed`) com esse texto;
20
+ * - `changeEnvironmentTarget` não tem rota `/api/cli/v1`, e a CLI não tem
21
+ * comando que troque o destino de um ambiente.
22
+ * Ou seja: o 1º deploy na VM de um produto criado pelo `neetru products create`
23
+ * é recusado, e o recado manda o dev para um caminho que ele não tem.
24
+ *
25
+ * O conserto do destino é do Core (bug_fe30f035). Aqui a CLI só deixa de largar
26
+ * o dev diante do recado: reconhece a recusa e diz o que aconteceu e o que dá
27
+ * para fazer. Só age quando o Core recusa DE FATO — quando o Core for
28
+ * consertado, o produto novo deixa de cair aqui e este texto não fica dizendo
29
+ * coisa velha. Se o Core mudar o recado, o reconhecimento falha fechado: o erro
30
+ * segue como sempre seguiu (`handleApiError`, com o texto do Core).
31
+ *
32
+ * Conferido de novo em 24/10 na main do Core (582124ab): a mesma cadeia. O
33
+ * conserto proposto (core#791, aberto) faz o deploy trocar o destino de um
34
+ * ambiente que nunca serviu e, para o que já serviu, troca o recado por outro
35
+ * sem a cauda `changeEnvironmentTarget` — que esta regex exige. Com ele no ar,
36
+ * nada cai aqui e o erro do Core passa como veio.
37
+ */
38
+ import { CliApiError } from './api-client.js';
39
+ import { NeetruCliError } from '../cli-kit/errors.js';
40
+ import { cliErrorFromApiError } from './api-error.js';
41
+ /**
42
+ * O recado do Core, de `_vm-deploy-core.ts` e `_cloud-run-deploy-core.ts`.
43
+ * A cauda `Migre o destino antes (changeEnvironmentTarget)` faz parte do
44
+ * reconhecimento de propósito: é ela que manda o dev para o caminho que ele não
45
+ * tem. A recusa do workspace (`… A Fase 1 do fluxo de ambientes só publica no
46
+ * destino workspace.`) não a traz e fica de fora.
47
+ */
48
+ const RECUSA_DE_DESTINO = /Ambiente (\S+) tem destino '([^']+)', não '([^']+)'\. Migre o destino antes \(changeEnvironmentTarget\)/;
49
+ /** Destinos em que um app roda — os únicos que vale sugerir em `--target`. */
50
+ const DESTINOS_DE_APP = new Set(['vm', 'cloud-run']);
51
+ /** O que dizer sobre a recusa, em linhas. Exportado para teste. */
52
+ export function explicacaoDaRecusaDeDestino(productSlug, ambiente, destinoAtual, destinoPedido) {
53
+ const linhas = [
54
+ `O ambiente ${ambiente} de ${productSlug} está com destino '${destinoAtual}', e este deploy pediu '${destinoPedido}'.`,
55
+ 'A CLI não tem comando para trocar o destino de um ambiente (o changeEnvironmentTarget do recado acima',
56
+ 'não está ao alcance dela).',
57
+ ];
58
+ if (destinoAtual === 'workspace') {
59
+ linhas.push('Produto criado pelo `neetru products create` nasce com staging e prod no destino workspace, e hoje o', '1º deploy dele na VM ou no Cloud Run é recusado assim. É defeito do Core (bug_fe30f035): até o conserto,', 'este deploy não segue pela CLI — leve o caso a um admin do Core.');
60
+ }
61
+ else if (DESTINOS_DE_APP.has(destinoAtual)) {
62
+ linhas.push(`Para seguir no destino atual, refaça o deploy com --target ${destinoAtual}; para trocar o destino,`, 'leve o caso a um admin do Core.');
63
+ }
64
+ else {
65
+ linhas.push('Para trocar o destino, leve o caso a um admin do Core.');
66
+ }
67
+ return linhas;
68
+ }
69
+ /**
70
+ * Se `err` é a recusa de destino do Core, devolve o mesmo erro canônico do
71
+ * `handleApiError` (mesmo código, mesmo exit, mesmo `message`) com a explicação
72
+ * no texto humano e, no `--json`, em `hint` — mais `ambiente`, `destinoAtual` e
73
+ * `destinoPedido`. Qualquer outro erro volta intacto.
74
+ */
75
+ export function explicarRecusaDeDestino(err, productSlug) {
76
+ if (!(err instanceof CliApiError) || err.status !== 400)
77
+ return err;
78
+ const achado = RECUSA_DE_DESTINO.exec(err.message);
79
+ if (!achado)
80
+ return err;
81
+ const [, ambiente, destinoAtual, destinoPedido] = achado;
82
+ const linhas = explicacaoDaRecusaDeDestino(productSlug, ambiente, destinoAtual, destinoPedido);
83
+ const canonico = cliErrorFromApiError(err);
84
+ return new NeetruCliError(canonico.message, {
85
+ code: canonico.code,
86
+ exit: canonico.exit,
87
+ cause: err,
88
+ details: {
89
+ ...canonico.details,
90
+ ambiente,
91
+ destinoAtual,
92
+ destinoPedido,
93
+ hint: linhas.join(' '),
94
+ },
95
+ human: [canonico.human, ...linhas.map((l) => ` ${l}`)].join('\n'),
96
+ });
97
+ }
98
+ //# sourceMappingURL=deploy-recusa-de-destino.js.map
@@ -0,0 +1,117 @@
1
+ /** Veredito da checagem da página inicial. */
2
+ export type RenderVerdict = 'ok' | 'inline_scripts_blocked' | 'unverifiable';
3
+ export interface RenderCheck {
4
+ verdict: RenderVerdict;
5
+ /** URL final conferida (depois de redirects na mesma origem). */
6
+ url: string;
7
+ httpStatus: number | null;
8
+ /** Scripts embutidos executáveis achados no HTML lido. */
9
+ inlineScripts: number;
10
+ /** Quantos deles a CSP bloqueia. */
11
+ blockedInlineScripts: number;
12
+ /** Diretiva que bloqueou (ex.: `script-src 'self'`), cortada. */
13
+ blockingDirective: string | null;
14
+ /** Por que não deu pra conferir (só em `unverifiable`). */
15
+ reason: string | null;
16
+ }
17
+ /** Resultado bruto de UMA requisição à página inicial. */
18
+ export type RenderProbeInput = {
19
+ kind: 'response';
20
+ httpStatus: number;
21
+ contentType: string | null;
22
+ /** Valores crus de `Content-Security-Policy` (o fetch junta repetidos com ", "). */
23
+ cspHeaderValues: string[];
24
+ html: string;
25
+ } | {
26
+ kind: 'network_error';
27
+ error: string;
28
+ };
29
+ /** Teto de bytes lidos do HTML (os scripts do Next aparecem já no começo do body). */
30
+ export declare const RENDER_BODY_MAX_BYTES: number;
31
+ /** Redirects seguidos dentro da MESMA origem (ex.: `/` → `/pt-BR`). */
32
+ export declare const RENDER_MAX_REDIRECTS = 2;
33
+ /** Uma política CSP: diretiva (minúscula) → lista de fontes. */
34
+ export interface CspPolicy {
35
+ directives: Map<string, string[]>;
36
+ }
37
+ /**
38
+ * Lê UMA política serializada (`a b; c d`). Diretiva repetida: vale a primeira
39
+ * (CSP3 §2.2.1). Nome de diretiva inválido é ignorado. PURA.
40
+ */
41
+ export declare function parseCspPolicy(text: string): CspPolicy;
42
+ /**
43
+ * Lê o valor de um cabeçalho `Content-Security-Policy`: várias políticas vêm
44
+ * separadas por vírgula (cabeçalhos repetidos também chegam assim, juntados
45
+ * pelo fetch). Política vazia é descartada. PURA.
46
+ */
47
+ export declare function parseCspHeader(value: string): CspPolicy[];
48
+ /**
49
+ * Lista de fontes que governa um `<script>` embutido: `script-src-elem`, senão
50
+ * `script-src`, senão `default-src`. `undefined` = a política não restringe
51
+ * script. PURA.
52
+ */
53
+ export declare function scriptElementSources(policy: CspPolicy): {
54
+ name: string;
55
+ sources: string[];
56
+ } | undefined;
57
+ export interface InlineScript {
58
+ nonce: string | null;
59
+ content: string;
60
+ }
61
+ /**
62
+ * Este `<script>` embutido roda sob esta lista de fontes? Regras do navegador
63
+ * (CSP3 "Does element match source list for type and source"):
64
+ * - nonce do elemento igual a um `'nonce-…'` da lista → roda;
65
+ * - hash do conteúdo igual a um `'sha256|384|512-…'` → roda;
66
+ * - `'unsafe-inline'` → roda, MAS só se a lista não tem nonce, hash nem
67
+ * `'strict-dynamic'` (com qualquer um deles o navegador ignora o unsafe-inline);
68
+ * - o resto (`'self'`, hosts, `'none'`, lista vazia) NÃO libera script embutido.
69
+ * PURA.
70
+ */
71
+ export declare function inlineScriptAllowed(script: InlineScript, sources: readonly string[]): boolean;
72
+ /** Scripts embutidos executáveis do HTML (sem `src`, conteúdo não vazio). PURA. */
73
+ export declare function extractInlineScripts(html: string): InlineScript[];
74
+ /** Políticas de `<meta http-equiv="Content-Security-Policy" content="…">`. PURA. */
75
+ export declare function extractMetaCspPolicies(html: string): CspPolicy[];
76
+ export interface InlineScriptCspAnalysis {
77
+ inlineScripts: number;
78
+ blockedInlineScripts: number;
79
+ blockingDirective: string | null;
80
+ }
81
+ /**
82
+ * Quantos scripts embutidos a CSP efetiva bloqueia. Um script só roda se TODAS
83
+ * as políticas em vigor (cabeçalhos + meta) deixarem. PURA.
84
+ */
85
+ export declare function analyzeInlineScriptCsp(input: {
86
+ cspHeaderValues: readonly string[];
87
+ html: string;
88
+ }): InlineScriptCspAnalysis;
89
+ /**
90
+ * Classifica a resposta da página inicial. PURA.
91
+ * - rede/não-2xx/não-HTML → `unverifiable` (não é prova de nada);
92
+ * - HTML com script embutido que a CSP bloqueia → `inline_scripts_blocked`;
93
+ * - o resto (sem CSP, CSP que libera, sem script embutido) → `ok`.
94
+ */
95
+ export declare function classifyRenderProbe(url: string, input: RenderProbeInput): RenderCheck;
96
+ /** URL da página inicial do domínio publicado. `null` se o domínio não forma URL. PURA. */
97
+ export declare function buildRenderProbeUrl(domain: string): string | null;
98
+ export interface RenderProbeDeps {
99
+ fetchImpl: (url: string, init: RequestInit) => Promise<Response>;
100
+ /** Timeout por requisição, cobrindo a leitura do corpo (ms). */
101
+ timeoutMs: number;
102
+ maxRedirects?: number;
103
+ }
104
+ /**
105
+ * Abre a página inicial e classifica. Uma tentativa por salto (a saúde acabou
106
+ * de passar — não é hora de esperar app subir). `redirect:'manual'`: segue no
107
+ * máximo `RENDER_MAX_REDIRECTS` saltos e só na MESMA origem; redirect pra fora
108
+ * (login de terceiro, outro host) → `unverifiable`. Nunca lança.
109
+ */
110
+ export declare function runRenderProbe(url: string, deps: RenderProbeDeps): Promise<RenderCheck>;
111
+ /** O que foi bloqueado, em uma frase (vai no `detail` do JSON e na tela). PURA. */
112
+ export declare function describeRenderBlock(render: RenderCheck): string;
113
+ /**
114
+ * O efeito e onde se conserta — mesma frase do alerta do Core
115
+ * (`renderBlockedExplanation`), sem o prefixo da saúde. PURA.
116
+ */
117
+ export declare const RENDER_BLOCKED_FIX: string;