@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
@@ -1,33 +1,47 @@
1
1
  /**
2
2
  * `neetru auth` — gerencia credenciais dos provedores de IA usados pelo
3
- * `neetru ai` (Anthropic, OpenAI, Gemini).
3
+ * `neetru ai`: os da lista única (`lib/ai/provedores.ts` — Groq, NVIDIA NIM e
4
+ * OpenAI). O AI Gateway da Neetru não precisa de chave: basta `neetru login`.
4
5
  *
5
6
  * Sub-comandos:
6
- * neetru auth status → lista provedores detectados (binário + creds)
7
- * neetru auth setup → wizard interativo: detecta, instala faltantes,
8
- * guia login
9
- * neetru auth pull → puxa creds dos CLIs concorrentes pra config do Neetru
7
+ * neetru auth status → estado de cada provedor (chave achada ou não)
8
+ * neetru auth setup → wizard interativo: mostra o estado e pede a chave
9
+ * que falta
10
+ * neetru auth pull → copia pra config do Neetru a chave achada no
11
+ * arquivo do Codex (a do ambiente já é lida direto)
10
12
  *
11
- * Objetivo: zerar fricção do "anthropicApiKey não configurada". Se user
12
- * já tem Claude Code/Codex/Gemini, reaproveita.
13
+ * bug_19d312f3: o wizard oferecia Claude (Anthropic) e Gemini (Google), vetados
14
+ * pela casa, e não oferecia Groq nem NVIDIA. Agora ele percorre a lista única.
13
15
  */
14
16
  import chalk from 'chalk';
15
17
  import * as readline from 'readline/promises';
16
18
  import inquirer from 'inquirer';
17
19
  import { detectAllProviders, installGlobal, } from '../lib/ai/auth-discovery.js';
20
+ import { CHAVE_DO_PROVEDOR, NOME_DO_PROVEDOR, PROVEDORES_IA } from '../lib/ai/provedores.js';
18
21
  import { config } from '../lib/config.js';
19
22
  import { log } from '../utils/logger.js';
20
- import { saveMfaSession } from '../lib/cli-write.js';
21
- const PROVIDER_TO_CONFIG = {
22
- anthropic: 'anthropicApiKey',
23
- openai: 'openaiApiKey',
24
- gemini: 'geminiApiKey',
25
- groq: 'groqApiKey',
26
- nvidia: 'nvidiaApiKey',
27
- };
28
- function badge(s) {
29
- if (s.apiKey)
23
+ import { loadMfaSession, saveMfaSession } from '../lib/cli-write.js';
24
+ /**
25
+ * Chave achada na máquina que pode ser copiada para a config do Neetru.
26
+ *
27
+ * A do ambiente (GROQ_API_KEY, NVIDIA_API_KEY, OPENAI_API_KEY) NÃO é copiada:
28
+ * a CLI já a lê direto da variável a cada uso, e a cópia passaria a valer mais
29
+ * que ela (a config vem primeiro) — trocar a chave na variável deixaria a CLI
30
+ * usando a antiga, com 401 sem pista de onde ela vem (revisão do #104). A do
31
+ * arquivo do Codex continua sendo copiada, como antes.
32
+ */
33
+ function chaveCopiavel(s) {
34
+ return s.credentialSource === 'env' ? undefined : s.apiKey;
35
+ }
36
+ function badge(s, naConfig = false) {
37
+ // Chave gravada na config do Neetru também conta como pronto (revisão do
38
+ // #104: o assistente mostrava "sem chave" e pedia de novo um provedor já
39
+ // configurado com `neetru config set`).
40
+ if (s.apiKey || naConfig)
30
41
  return chalk.green('✓ pronto');
42
+ // Provedor só de chave (Groq, NVIDIA): não há programa para instalar.
43
+ if (!s.binary)
44
+ return chalk.dim('- sem chave');
31
45
  if (s.installed && s.credentialSource)
32
46
  return chalk.yellow('! login parcial');
33
47
  if (s.installed)
@@ -35,31 +49,14 @@ function badge(s) {
35
49
  return chalk.dim('- não instalado');
36
50
  }
37
51
  function rowName(p) {
38
- switch (p) {
39
- case 'anthropic':
40
- return 'Claude (Anthropic)';
41
- case 'openai':
42
- return 'Codex (OpenAI)';
43
- case 'gemini':
44
- return 'Gemini (Google)';
45
- case 'groq':
46
- return 'Groq';
47
- case 'nvidia':
48
- return 'NVIDIA NIM';
49
- }
52
+ return NOME_DO_PROVEDOR[p];
50
53
  }
51
54
  /**
52
55
  * `neetru auth status` — pura leitura, mostra tabela.
53
56
  */
54
57
  export async function runAuthStatus(opts = {}) {
55
58
  const providers = detectAllProviders();
56
- const configured = {
57
- anthropic: !!config.get('anthropicApiKey'),
58
- openai: !!config.get('openaiApiKey'),
59
- gemini: !!config.get('geminiApiKey'),
60
- groq: !!config.get('groqApiKey'),
61
- nvidia: !!config.get('nvidiaApiKey'),
62
- };
59
+ const configured = Object.fromEntries(PROVEDORES_IA.map((p) => [p, !!config.get(CHAVE_DO_PROVEDOR[p])]));
63
60
  if (opts.json) {
64
61
  console.log(JSON.stringify({
65
62
  providers: providers.map((p) => ({
@@ -72,33 +69,46 @@ export async function runAuthStatus(opts = {}) {
72
69
  }
73
70
  console.log(chalk.bold('\nNeetru Auth — status dos provedores de IA\n'));
74
71
  for (const p of providers) {
75
- const configuredHere = configured[p.provider] ? chalk.green('(neetru config OK)') : '';
76
- console.log(` ${badge(p)} ${chalk.bold(rowName(p.provider))} ${configuredHere}`);
77
- console.log(` ${chalk.dim(p.hint)}`);
72
+ const naConfig = configured[p.provider];
73
+ const configuredHere = naConfig ? chalk.green('(neetru config OK)') : '';
74
+ console.log(` ${badge(p, naConfig)} ${chalk.bold(rowName(p.provider))} ${configuredHere}`);
75
+ // A chave da config vem antes da achada na máquina (`resolveKeyPool` no
76
+ // orchestrator): com ela gravada, é ela que a CLI usa.
77
+ const dica = naConfig ? 'usando a chave gravada na config do Neetru' : p.hint;
78
+ console.log(` ${chalk.dim(dica)}`);
78
79
  }
79
80
  console.log();
80
81
  console.log(chalk.dim('Para configurar tudo de uma vez: neetru auth setup'));
81
82
  }
82
83
  /**
83
- * `neetru auth pull` — sem perguntar, copia toda apiKey extraída dos CLIs
84
- * concorrentes pra config do Neetru. Útil pra CI/script.
84
+ * `neetru auth pull` — sem perguntar, copia pra config do Neetru a chave achada
85
+ * no arquivo do Codex. A do ambiente não é copiada: a CLI já a lê direto da
86
+ * variável (ver `chaveCopiavel`). Não sobrescreve chave já configurada.
85
87
  */
86
88
  export async function runAuthPull(opts = {}) {
87
89
  const providers = detectAllProviders();
88
90
  const copied = [];
91
+ const doAmbiente = [];
89
92
  for (const p of providers) {
90
- if (!p.apiKey)
93
+ const chave = chaveCopiavel(p);
94
+ if (!chave) {
95
+ if (p.apiKey)
96
+ doAmbiente.push(p.provider);
91
97
  continue;
92
- const cfgKey = PROVIDER_TO_CONFIG[p.provider];
98
+ }
99
+ const cfgKey = CHAVE_DO_PROVEDOR[p.provider];
93
100
  if (config.get(cfgKey))
94
101
  continue; // não sobrescreve
95
- config.set(cfgKey, p.apiKey);
102
+ config.set(cfgKey, chave);
96
103
  copied.push(p.provider);
97
104
  }
98
105
  if (opts.json) {
99
106
  console.log(JSON.stringify({ copied }, null, 2));
100
107
  return;
101
108
  }
109
+ if (doAmbiente.length) {
110
+ log.dim(`Lidas direto do ambiente, sem cópia: ${doAmbiente.map(rowName).join(', ')}.`);
111
+ }
102
112
  if (copied.length === 0) {
103
113
  log.info('Nenhuma cred nova para copiar (ou já estão configuradas).');
104
114
  return;
@@ -126,28 +136,32 @@ export async function runAuthSetup(opts = {}) {
126
136
  // Modo não-interativo: só roda pull silencioso.
127
137
  return runAuthPull(opts);
128
138
  }
139
+ const naConfig = (p) => !!config.get(CHAVE_DO_PROVEDOR[p.provider]);
129
140
  console.log(chalk.bold('\nNeetru Auth — wizard de configuração\n'));
130
141
  console.log(chalk.dim('Detectado:\n'));
131
142
  for (const p of providers) {
132
- console.log(` ${badge(p)} ${rowName(p.provider)}`);
143
+ console.log(` ${badge(p, naConfig(p))} ${rowName(p.provider)}`);
133
144
  }
134
145
  console.log();
135
- // Pull silencioso de tudo que já tá pronto.
146
+ // Pull silencioso do que dá para copiar (o arquivo do Codex). A chave do
147
+ // ambiente não é copiada: a CLI já a lê direto (ver `chaveCopiavel`).
136
148
  const autoConfigured = [];
137
149
  for (const p of providers) {
138
- if (!p.apiKey)
150
+ const chave = chaveCopiavel(p);
151
+ if (!chave)
139
152
  continue;
140
- const cfgKey = PROVIDER_TO_CONFIG[p.provider];
153
+ const cfgKey = CHAVE_DO_PROVEDOR[p.provider];
141
154
  if (!config.get(cfgKey)) {
142
- config.set(cfgKey, p.apiKey);
155
+ config.set(cfgKey, chave);
143
156
  autoConfigured.push(p.provider);
144
157
  }
145
158
  }
146
159
  if (autoConfigured.length) {
147
160
  log.success(`Reaproveitadas creds de: ${autoConfigured.join(', ')}`);
148
161
  }
149
- // Identifica provedores que precisam de ação (sem cred ou sem binário).
150
- const needsAction = providers.filter((p) => !p.apiKey);
162
+ // Identifica provedores que precisam de ação: sem chave achada na máquina E
163
+ // sem chave na config do Neetru.
164
+ const needsAction = providers.filter((p) => !p.apiKey && !naConfig(p));
151
165
  if (needsAction.length === 0) {
152
166
  log.success('Todos os provedores prontos. Você pode rodar `neetru ai`.');
153
167
  return;
@@ -167,8 +181,9 @@ export async function runAuthSetup(opts = {}) {
167
181
  }
168
182
  for (const p of wanted) {
169
183
  console.log(chalk.bold(`\n→ ${rowName(p.provider)}`));
170
- // Instalar binário se ausente.
171
- if (!p.installed) {
184
+ // Instalar binário se ausente. Provedor só de chave (Groq, NVIDIA) não
185
+ // tem programa: vai direto pedir a chave.
186
+ if (p.npmPackage && !p.installed) {
172
187
  const install = await confirm(rl, ` Instalar ${p.npmPackage} globalmente?`, true);
173
188
  if (install) {
174
189
  log.info(`Rodando: npm install -g ${p.npmPackage}`);
@@ -184,10 +199,10 @@ export async function runAuthSetup(opts = {}) {
184
199
  continue;
185
200
  }
186
201
  }
187
- else {
202
+ else if (p.binary) {
188
203
  log.dim(` ${p.binary} já instalado.`);
189
204
  }
190
- // Pedir api key direto (mais simples que mandar usuário pro `claude /login`).
205
+ // Pedir api key direto (mais simples que mandar o usuário logar no programa).
191
206
  const provideKey = await confirm(rl, ` Colar API key agora? (alternativa: ${p.hint.replace(/^.*— /, '')})`, true);
192
207
  if (provideKey) {
193
208
  // Auditoria UX 2026-09: a API key é SEGREDO — não pode ecoar na tela/
@@ -206,16 +221,19 @@ export async function runAuthSetup(opts = {}) {
206
221
  rl.resume();
207
222
  }
208
223
  if (key) {
209
- config.set(PROVIDER_TO_CONFIG[p.provider], key);
224
+ config.set(CHAVE_DO_PROVEDOR[p.provider], key);
210
225
  log.success(`${rowName(p.provider)} configurado.`);
211
226
  }
212
227
  else {
213
228
  log.warn('Vazio — não configurado.');
214
229
  }
215
230
  }
216
- else {
231
+ else if (p.binary) {
217
232
  log.dim(`Use o login do ${p.binary} ou rode "neetru auth pull" depois pra puxar.`);
218
233
  }
234
+ else {
235
+ log.dim(`Depois, ${p.hint.replace(/^.*— /, '')}.`);
236
+ }
219
237
  }
220
238
  console.log();
221
239
  log.success('Wizard finalizado. Rode `neetru ai` pra testar.');
@@ -292,6 +310,27 @@ export async function runAuthMfaSession(opts = {}) {
292
310
  }
293
311
  // Salva o token localmente para uso automático em operações destrutivas.
294
312
  await saveMfaSession(res.mfaSessionToken, res.expiresAt);
313
+ // bug_757b9746: `saveMfaSession` engole a falha de gravação, e este comando
314
+ // respondia `saved: true` + "usarão esta sessão automaticamente" sem conferir
315
+ // nada — se a gravação falhasse, o comando destrutivo seguinte pedia o TOTP de
316
+ // novo e a mensagem tinha mentido. Confere lendo de volta pelo MESMO caminho
317
+ // que os comandos destrutivos usam (`loadMfaSession`): pega a gravação que
318
+ // falhou e também o `expiresAt` que o relógio desta máquina já dá por vencido.
319
+ if (loadMfaSession() !== res.mfaSessionToken) {
320
+ const msg = 'A sessão MFA foi aberta no Core, mas não ficou gravada nesta máquina ' +
321
+ '(~/.config/neetru-cli/mfa-session.json) ou o relógio daqui já a dá por vencida. ' +
322
+ 'Os comandos destrutivos vão pedir o código TOTP de novo. Confira a permissão ' +
323
+ 'dessa pasta e a hora do sistema, e rode `neetru auth mfa-session` outra vez com um código novo.';
324
+ if (opts.json) {
325
+ console.log(JSON.stringify({ ok: false, error: 'mfa_session_not_saved', message: msg, saved: false }));
326
+ }
327
+ else {
328
+ log.error(msg);
329
+ }
330
+ const { safeExit } = await import('../utils/safe-exit.js');
331
+ safeExit(1);
332
+ return;
333
+ }
295
334
  if (opts.json) {
296
335
  console.log(JSON.stringify({
297
336
  ok: true,
@@ -30,12 +30,13 @@ import { safeExit } from '../utils/safe-exit.js';
30
30
  import { normalizeTarget } from './env.js';
31
31
  import { RUNTIME_ENVIRONMENT_IDS } from '../lib/envs.js';
32
32
  import { formatChoices } from '../cli-kit/choices.js';
33
+ import { adviseFlag } from '../cli-kit/command-context.js';
33
34
  import { registerSpinner, unregisterSpinner } from '../utils/spinner-registry.js';
34
35
  import { CodificacaoInvalida, decodificarUtf8, semBomInicial } from '../lib/texto-utf8.js';
35
36
  import { variaveisDoEnvFile } from '../lib/env-file-do-build.js';
36
37
  // bug_678ddc98: detecção de alvo — diretório do app, gerenciador de pacote e
37
38
  // layout do standalone (possivelmente aninhado). Ver `src/lib/build-targets.ts`.
38
- import { detectPackageManager, comandoInstall, comandoRunScript, comandoExecBin, resolveStandaloneLayout, baseDeInjecaoNoTar, } from '../lib/build-targets.js';
39
+ import { detectPackageManager, comandoInstall, comandoRunScript, comandoExecBin, resolveStandaloneLayout, lerRelativeAppDir, baseDeInjecaoNoTar, } from '../lib/build-targets.js';
39
40
  const CONFIG_FILENAMES = ['neetru.config.json', '.neetru.json'];
40
41
  function fileExists(p) {
41
42
  try {
@@ -542,12 +543,20 @@ pm = { name: 'npm', rootDir: cwd, source: 'default' }) {
542
543
  }
543
544
  }
544
545
  else {
545
- lg.dim(` --no-cache ignorado: ${pm.name} não tem \`cache clean --force\`.`);
546
+ lg.dim(` --no-cache ignorado: ${pm.name} não tem equivalente ao \`npm cache clean --force\`.`);
546
547
  }
547
548
  }
548
549
  if (process.platform === 'win32') {
550
+ // bug_89d2497e (etapa 1): o `neetru deploy` chega aqui pelo `runBuild`
551
+ // (sem artefato pronto) e não declara --no-install — quem seguia o
552
+ // conselho batia em "unknown option". Só o `neetru build` ouve a flag; o
553
+ // deploy recebe só o pedido de pausar o dev server.
549
554
  lg.warn(`Se houver \`next dev\` ativo na pasta, ele pode conflitar com o ${install.cmd} install (file locks).\n` +
550
- ` Pause o dev server antes do build, ou use --no-install pra pular esta etapa.`);
555
+ adviseFlag({
556
+ flag: '--no-install',
557
+ withFlag: ' Pause o dev server antes do build, ou use --no-install pra pular esta etapa.',
558
+ withoutFlag: ' Pause o dev server antes de continuar.',
559
+ }));
551
560
  }
552
561
  // bug_678ddc98: o install roda na RAIZ do workspace (onde mora o lockfile),
553
562
  // não no diretório do app — `pnpm install` dentro de `apps/web` sem o
@@ -620,9 +629,31 @@ pm = { name: 'npm', rootDir: cwd, source: 'default' }) {
620
629
  // Next.js aninha o app dentro do standalone (`apps/web/server.js`) e esse
621
630
  // caminho fixo apontava pro vazio. Agora vem do layout real pós-build.
622
631
  if (standalone && isStandaloneBuildDir(cwd) && !layout) {
632
+ // bug_678ddc98: quando o Next.js declarou o lugar do app
633
+ // (`relativeAppDir`), o motivo do aborto é outro e a mensagem diz qual.
634
+ const declarado = lerRelativeAppDir(cwd);
635
+ if (declarado.tipo === 'declarado') {
636
+ const prefixo = declarado.prefixo;
637
+ throw new Error(`O Next.js registrou o app ${prefixo ? `em \`${prefixo}\`` : 'na raiz do standalone'} ` +
638
+ '(campo `relativeAppDir` de `.next/required-server-files.json`), ' +
639
+ `mas \`.next/standalone/${prefixo ? `${prefixo}/` : ''}server.js\` não existe. ` +
640
+ 'Isso costuma indicar uma pasta `.next` com restos de builds diferentes: apague a pasta `.next` do app, ' +
641
+ 'rode o build de novo e confira se `output: "standalone"` está ativo. ' +
642
+ 'Abortando: um tarball sem entrypoint sobe e nunca inicia.');
643
+ }
644
+ if (declarado.tipo === 'invalido') {
645
+ throw new Error(`O campo \`relativeAppDir\` de \`.next/required-server-files.json\` vale "${declarado.valor}" ` +
646
+ 'e aponta para fora do standalone: o `outputFileTracingRoot` do next.config precisa ser uma pasta ' +
647
+ 'ACIMA do app (no monorepo, a raiz do repositório). ' +
648
+ 'Abortando: um tarball sem entrypoint sobe e nunca inicia.');
649
+ }
623
650
  throw new Error('Existe `.next/standalone` mas nenhum `server.js` foi encontrado dentro dele. ' +
624
651
  'Num monorepo o Next.js aninha o app sob o `outputFileTracingRoot` (ex: `apps/web/server.js`) — ' +
625
- 'confira se o build rodou no diretório certo (use `--app <dir>`) e se `output: "standalone"` está ativo. ' +
652
+ // bug_89d2497e (etapa 1): o `neetru deploy` chega aqui pelo `runBuild` e
653
+ // não declara --app; o conselho cita o `neetru build` inteiro. O artefato
654
+ // sai no diretório de onde o build roda, que é onde o deploy o procura.
655
+ 'confira se o build rodou no diretório certo (rode `neetru build --app <dir>` no diretório de onde vai rodar o deploy) ' +
656
+ 'e se `output: "standalone"` está ativo. ' +
626
657
  'Abortando: um tarball sem entrypoint sobe e nunca inicia.');
627
658
  }
628
659
  if (layout?.appPrefix) {
@@ -1,5 +1,5 @@
1
1
  import type { Command } from 'commander';
2
- import type { NeetruCliError } from '../cli-kit/errors.js';
2
+ import { NeetruCliError } from '../cli-kit/errors.js';
3
3
  type ActorType = 'agent_claude' | 'human' | 'cli';
4
4
  type ChatChannelType = 'global' | 'product' | 'custom';
5
5
  export interface ChatChannelsOptions {
@@ -55,8 +55,9 @@ export interface ChatIdentityOptions {
55
55
  * nome. Sem aviso: a saída era o mesmo "✓ Apelido definido" do caminho feliz.
56
56
  *
57
57
  * Agora `show`/`ver` lê, `set`/`definir` grava o resto, `clear`/`limpar` limpa,
58
- * e o posicional antigo (`identity "PDV · IA"`) segue funcionando. O preço: não
59
- * dá pra ter um apelido que seja SÓ uma dessas palavras.
58
+ * e o posicional antigo (`identity "PDV · IA"`) segue funcionando — REVOGADO em
59
+ * 22/10, ver DEFAULT INVERTIDO abaixo. O preço: não dá pra ter um apelido que
60
+ * seja SÓ uma dessas palavras.
60
61
  *
61
62
  * P105 (2026-10-18): o conserto acima cobria só as palavras que ele conhecia.
62
63
  * `identity describe` gravava "describe", `identity list` gravava "list",
@@ -72,6 +73,20 @@ export interface ChatIdentityOptions {
72
73
  * Os caminhos citados (`neetru chat identity …`, `neetru chat send --as`) saem
73
74
  * da árvore do Commander (`contextoDeIdentidadeDaArvore`), não de literal
74
75
  * (revisão do #87, BAIXO 5).
76
+ *
77
+ * DEFAULT INVERTIDO (2026-10-22, encerramento do bug_09574d4b derrubado): os
78
+ * dois consertos acima eram LISTA DE PERMISSÃO — recusavam as 18 palavras que
79
+ * conheciam e gravavam todo o resto. Medido na 2.33.0 publicada: `identity
80
+ * mostrar`, `identity status`, `identity whoami`, `identity atual`, `identity
81
+ * apelido` (o alias do próprio comando) e `identity "Dev Core"` gravavam o
82
+ * argumento como apelido da credencial COMPARTILHADA, com o mesmo "✓ Apelido
83
+ * definido" do caminho feliz. Allowlist de verbo nunca fecha esse padrão: a
84
+ * pessoa sempre acha mais um verbo. Agora o posicional solto NÃO grava nunca —
85
+ * qualquer primeira palavra fora de `show`/`set`/`clear` (e seus equivalentes)
86
+ * é recusada, lista os subcomandos e ensina `identity set "<texto>"`. As únicas
87
+ * portas de escrita são `identity set "Nome"`, `identity clear` e o `--as` do
88
+ * `send` (que avisa no stderr). O preço, aceito: `identity "Nome"` — a forma
89
+ * posicional da CH3 — deixou de gravar.
75
90
  */
76
91
  export type PedidoDeIdentidade = {
77
92
  acao: 'mostrar';
@@ -122,10 +137,13 @@ interface ChatIdentityResponse {
122
137
  email?: string;
123
138
  }
124
139
  /**
125
- * `neetru chat identity [apelido]` — apelido inerente à credencial (CH3).
140
+ * O que `neetru chat identity` faz depois de `interpretarArgsDeIdentidade`
141
+ * decidir — apelido inerente à credencial (CH3). Quem chega aqui com texto já
142
+ * passou por `identity set "Nome"`, `identity clear` ou `send --as`: o
143
+ * posicional solto não chega (default invertido, bug_09574d4b).
126
144
  *
127
- * Sem argumento: mostra o apelido atual (GET /api/cli/v1/chat/identity).
128
- * Com argumento: persiste o apelido na credencial (POST). A partir daí, TODA
145
+ * `undefined`: mostra o apelido atual (GET /api/cli/v1/chat/identity).
146
+ * Texto: persiste o apelido na credencial (POST). A partir daí, TODA
129
147
  * mensagem postada com este token aparece com este nome — a IA se identifica em
130
148
  * nome do dev dono do token. Apelido vazio ("") limpa (volta ao email).
131
149
  *
@@ -33,7 +33,9 @@ import { log } from '../utils/logger.js';
33
33
  import { safeExit } from '../utils/safe-exit.js';
34
34
  import { fmtTimestamp } from '../lib/render.js';
35
35
  import { exitWithCliError } from '../lib/cli-error.js';
36
- import { isSubcommandVerb, reservedVerbError } from '../cli-kit/suggest.js';
36
+ import { EXIT_CODES, NeetruCliError } from '../cli-kit/errors.js';
37
+ import { formatChoices } from '../cli-kit/choices.js';
38
+ import { isSubcommandVerb, reservedVerbError, suggestSubcommands } from '../cli-kit/suggest.js';
37
39
  import { commandDeclaresFlag, declaredFlags } from '../cli-kit/command-context.js';
38
40
  // ─── Helpers de display ───────────────────────────────────────────────────────
39
41
  function colorRole(role) {
@@ -305,7 +307,35 @@ export function interpretarArgsDeIdentidade(args, ctx = CONTEXTO_DE_IDENTIDADE_P
305
307
  ? `Para gravar "${texto}" como apelido: neetru ${ctx.comando} set "${texto}"`
306
308
  : comoGravarVerboComoApelido(primeiro.trim(), ctx));
307
309
  }
308
- return { acao: 'definir', apelido: args.join(' ') };
310
+ // Default INVERTIDO: o que não é subcomando de `identity` é recusado — nunca
311
+ // gravado como apelido (ver o bloco acima de `PedidoDeIdentidade`).
312
+ return subcomandoDesconhecido(args, ctx);
313
+ }
314
+ /**
315
+ * `identity <qualquer outra coisa>`: recusa SEM escrever. Não diz que a palavra
316
+ * é "de comando" (`Dev Core` não é) — diz que não é subcomando de `identity`,
317
+ * lista os que existem e ensina a forma explícita de gravar o mesmo texto.
318
+ * Sugestão só quando passa o piso do gcloud (`suggestSubcommands`).
319
+ */
320
+ function subcomandoDesconhecido(args, ctx) {
321
+ const texto = args.join(' ');
322
+ const sugestoes = suggestSubcommands(args[0].trim(), VERBOS_DE_IDENTIDADE).map((s) => `neetru ${ctx.comando} ${s}`);
323
+ const linhas = [
324
+ `"${texto}" não é subcomando de \`neetru ${ctx.comando}\` — e o argumento solto não grava apelido.`,
325
+ ];
326
+ if (sugestoes.length > 0)
327
+ linhas.push(`Você quis dizer: ${sugestoes.map((s) => `\`${s}\``).join(' ou ')}?`);
328
+ linhas.push(`Subcomandos de \`neetru ${ctx.comando}\`: ${formatChoices(VERBOS_DE_IDENTIDADE)} ` +
329
+ `(sem subcomando, \`neetru ${ctx.comando}\` mostra o apelido atual).`);
330
+ linhas.push(texto.trim()
331
+ ? `Para gravar "${texto.trim()}" como apelido: neetru ${ctx.comando} set "${texto.trim()}"`
332
+ : `Para limpar o apelido: neetru ${ctx.comando} clear`);
333
+ const erro = new NeetruCliError(linhas.join('\n '), {
334
+ code: 'uso_invalido',
335
+ exit: EXIT_CODES.client,
336
+ details: { value: texto, suggestions: sugestoes, valid: [...VERBOS_DE_IDENTIDADE] },
337
+ });
338
+ return { acao: 'erro', mensagem: erro.message, erro };
309
339
  }
310
340
  function subcomandosIrmaos(command) {
311
341
  const irmaos = command?.parent?.commands ?? [];
@@ -360,10 +390,13 @@ function respondeuEmModoMaquina(res, opts) {
360
390
  return false;
361
391
  }
362
392
  /**
363
- * `neetru chat identity [apelido]` — apelido inerente à credencial (CH3).
393
+ * O que `neetru chat identity` faz depois de `interpretarArgsDeIdentidade`
394
+ * decidir — apelido inerente à credencial (CH3). Quem chega aqui com texto já
395
+ * passou por `identity set "Nome"`, `identity clear` ou `send --as`: o
396
+ * posicional solto não chega (default invertido, bug_09574d4b).
364
397
  *
365
- * Sem argumento: mostra o apelido atual (GET /api/cli/v1/chat/identity).
366
- * Com argumento: persiste o apelido na credencial (POST). A partir daí, TODA
398
+ * `undefined`: mostra o apelido atual (GET /api/cli/v1/chat/identity).
399
+ * Texto: persiste o apelido na credencial (POST). A partir daí, TODA
367
400
  * mensagem postada com este token aparece com este nome — a IA se identifica em
368
401
  * nome do dev dono do token. Apelido vazio ("") limpa (volta ao email).
369
402
  *
@@ -617,7 +650,9 @@ export async function runChatHistory(opts) {
617
650
  console.log(JSON.stringify({ ok: true, count: messages.length, messages }));
618
651
  return;
619
652
  }
620
- const janela = initialSinceMs !== undefined ? 'desde o cursor --since' : `últimas ${sinceHours}h`;
653
+ // bug_89d2497e (etapa 1): o `neetru chat` também chama este runner e não tem
654
+ // --since; o rótulo descreve a janela sem citar a flag.
655
+ const janela = initialSinceMs !== undefined ? 'desde o cursor informado' : `últimas ${sinceHours}h`;
621
656
  console.log(chalk.bold(`\nHistórico — ${label} (${janela} · ${messages.length} msg)\n`));
622
657
  if (messages.length === 0) {
623
658
  log.dim('Nenhuma mensagem no período.');
@@ -825,7 +860,8 @@ export function registerChatCommand(program) {
825
860
  .command('identity [args...]')
826
861
  .alias('apelido')
827
862
  .description('Apelido da credencial no chat (persiste por token). ' +
828
- '`identity` ou `identity show` lê; `identity set "Nome"` grava; `identity clear` limpa.')
863
+ '`identity` ou `identity show` lê; `identity set "Nome"` grava; `identity clear` limpa. ' +
864
+ 'Qualquer outra palavra é recusada — nunca vira apelido.')
829
865
  .option('--json', 'saída em JSON')
830
866
  .action(async (args, _cmdOpts, command) => {
831
867
  // O `--json` vai pro pai `chat` (ver `opcaoHerdada`): ler só `cmdOpts.json`
@@ -11,6 +11,7 @@ import { parseToolRequests, extractPatch } from '../lib/code-agent-protocol.js';
11
11
  import { applyUnifiedPatch, checkPatchApplies, getGitDiff, getProjectOverview, listFiles, readFileContext, renderColoredDiff, runCodeTool, truncateForGateway, } from '../lib/code-agent-files.js';
12
12
  import { compactCodeHistory, compactHistoryToFit, MAX_HISTORY_TOTAL_CHARS, saveCodeSession, autoSaveCodeSession, listCodeSessions, loadCodeSession, logAppliedPatch, CodeAgentStateMachine, } from '../lib/code-agent-session.js';
13
13
  import { DEFAULT_CODE_MAX_TOKENS, DEFAULT_CODE_MAX_TURNS, DEFAULT_CODE_TEMPERATURE, ULTRACODE_EFFORT, ULTRACODE_MAX_TOKENS, ULTRACODE_MAX_TURNS, parseBoundedNumber, resolveCodeEffort, resolveCodeModel, resolveCodeProvider, } from '../lib/code-agent-settings.js';
14
+ import { PROVEDORES_IA, ehProvedorIa } from '../lib/ai/provedores.js';
14
15
  import { callGateway, streamGatewayChat, AI_GATEWAY_CHAT_PATH } from '../lib/code-agent-gateway.js';
15
16
  import { createBudgetTracker, decideBudgetAction, MAX_CONSECUTIVE_PATCH_REPAIRS, MAX_TOOLS_PER_TURN, SINGLE_ACTION_TOOLS, } from '../lib/code-agent-budget.js';
16
17
  import { CliApiError } from '../lib/api-client.js';
@@ -45,7 +46,15 @@ export function resolveSettings(opts) {
45
46
  envModel: process.env.NEETRU_CODE_MODEL,
46
47
  configuredGatewayModel: config.get('gatewayModel'),
47
48
  });
48
- const provider = resolveCodeProvider(asOptionalString(opts.provider), config.get('gatewayProvider'), model.model);
49
+ const pedidoDeProvider = asOptionalString(opts.provider);
50
+ const provider = resolveCodeProvider(pedidoDeProvider, config.get('gatewayProvider'), model.model);
51
+ // Revisão do #104 (bug_19d312f3): `--provider gemini` (ou qualquer nome fora
52
+ // da lista única) era ignorado calado e o pedido seguia sem provedor forçado.
53
+ // Continua seguindo (fail-open, ver `resolveCodeProvider`), mas avisa.
54
+ const avisoDeProvider = pedidoDeProvider && !ehProvedorIa(pedidoDeProvider)
55
+ ? `--provider "${pedidoDeProvider}" não existe; use ${PROVEDORES_IA.join(' | ')}. ` +
56
+ 'Seguindo sem provedor forçado: o AI Gateway escolhe.'
57
+ : undefined;
49
58
  const ultracode = opts.ultracode === true;
50
59
  const objectiveMode = opts.goal === true || opts.objective === true || ultracode;
51
60
  const effort = ultracode
@@ -56,7 +65,7 @@ export function resolveSettings(opts) {
56
65
  files: normalizeFiles(opts.file),
57
66
  model: model.model,
58
67
  provider,
59
- modelWarning: model.warning,
68
+ modelWarning: [model.warning, avisoDeProvider].filter(Boolean).join('\n') || undefined,
60
69
  // GAP-M1-18: `--ultracode` sobrepoe `--max-turns`/`--max-tokens`
61
70
  // explicitos — e um preset "tudo pra cima", nao so um default alternativo.
62
71
  maxTurns: ultracode
@@ -1,22 +1,16 @@
1
1
  import chalk from 'chalk';
2
2
  import { config } from '../lib/config.js';
3
+ import { CHAVE_DE_MODELO_DO_PROVEDOR, CHAVE_DO_PROVEDOR, MODELOS_DO_AI_NA_TELA, PROVEDORES_IA, ehModeloDoAi, } from '../lib/ai/provedores.js';
3
4
  import { classifyApiUrl, isOverrideEnabled } from '../lib/url-allowlist.js';
4
5
  import { log } from '../utils/logger.js';
5
6
  import { safeExit } from '../utils/safe-exit.js';
6
7
  const SETTABLE_KEYS = [
7
- 'anthropicApiKey',
8
- 'openaiApiKey',
9
- 'geminiApiKey',
10
- // Groq + NVIDIA NIM (OpenAI-compatible). Valor pode ser pool comma-separated.
11
- 'groqApiKey',
12
- 'nvidiaApiKey',
13
- 'groqModel',
14
- 'nvidiaModel',
15
- // Override de modelo por provider primário (claude/openai/gemini) — eram
16
- // documentados mas faltavam aqui → `config set` rejeitava (auditoria 2026-06-20).
17
- 'claudeModel',
18
- 'openaiModel',
19
- 'geminiModel',
8
+ // bug_19d312f3: chave de API e override de modelo de cada provedor vêm da
9
+ // lista única (`provedores.ts` — Groq, NVIDIA NIM, OpenAI). O valor da chave
10
+ // pode ser um pool separado por vírgula. `anthropicApiKey`, `geminiApiKey`,
11
+ // `claudeModel` e `geminiModel` saíram (Anthropic e Gemini vetados).
12
+ ...PROVEDORES_IA.map((p) => CHAVE_DO_PROVEDOR[p]),
13
+ ...PROVEDORES_IA.map((p) => CHAVE_DE_MODELO_DO_PROVEDOR[p]),
20
14
  // AI Gateway do Core (`neetru login` + `defaultModel=gateway`) — sem chave
21
15
  // própria, só override do modelo enviado no body de `/api/cli/v1/ai/chat`.
22
16
  'gatewayModel',
@@ -36,6 +30,14 @@ export function configSet(key, value, opts = {}) {
36
30
  log.error(`Chave inválida: "${key}". Chaves disponíveis: ${SETTABLE_KEYS.join(', ')}`);
37
31
  safeExit(1);
38
32
  }
33
+ // bug_19d312f3: o schema da config não tem mais `enum` em `defaultModel`
34
+ // (para um valor antigo como `claude` não derrubar a leitura) — a conferência
35
+ // do valor gravado fica aqui.
36
+ if (key === 'defaultModel' && !ehModeloDoAi(value)) {
37
+ log.error(`Valor inválido para defaultModel: "${value}". Use: ${MODELOS_DO_AI_NA_TELA}.`);
38
+ safeExit(1);
39
+ return;
40
+ }
39
41
  // MASTER-HIGH-048 (2.11.0): set manual de `neetruApiKey` está deprecated.
40
42
  // O storage canonico agora é `~/.config/neetru-cli/auth.json` (via `neetru login`).
41
43
  // O Conf storage será removido em @neetru/cli@2.12.