@neetru/cli 2.33.0 → 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 (56) hide show
  1. package/CHANGELOG.md +60 -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.js +10 -5
  16. package/dist/commands/db.d.ts +8 -2
  17. package/dist/commands/db.js +13 -6
  18. package/dist/commands/deploy.d.ts +14 -5
  19. package/dist/commands/deploy.js +376 -55
  20. package/dist/commands/dev-context.js +1 -1
  21. package/dist/commands/environments-repoint.d.ts +49 -0
  22. package/dist/commands/environments-repoint.js +219 -0
  23. package/dist/commands/init.js +7 -1
  24. package/dist/commands/new.js +14 -0
  25. package/dist/commands/secrets.d.ts +6 -4
  26. package/dist/commands/secrets.js +14 -7
  27. package/dist/commands/ui.js +1 -1
  28. package/dist/commands/workspaces.js +16 -1
  29. package/dist/index.js +47 -7
  30. package/dist/lib/ai/auth-discovery.d.ts +9 -12
  31. package/dist/lib/ai/auth-discovery.js +43 -145
  32. package/dist/lib/ai/orchestrator.d.ts +13 -6
  33. package/dist/lib/ai/orchestrator.js +46 -246
  34. package/dist/lib/ai/provedores.d.ts +57 -0
  35. package/dist/lib/ai/provedores.js +66 -0
  36. package/dist/lib/build-targets.d.ts +48 -10
  37. package/dist/lib/build-targets.js +64 -10
  38. package/dist/lib/cli-write.js +6 -3
  39. package/dist/lib/code-agent-gateway.d.ts +2 -2
  40. package/dist/lib/code-agent-gateway.js +2 -2
  41. package/dist/lib/code-agent-settings.d.ts +2 -2
  42. package/dist/lib/code-agent-settings.js +2 -4
  43. package/dist/lib/code-agent-types.d.ts +3 -1
  44. package/dist/lib/config.d.ts +4 -7
  45. package/dist/lib/config.js +9 -6
  46. package/dist/lib/deploy-recusa-de-destino.d.ts +9 -0
  47. package/dist/lib/deploy-recusa-de-destino.js +98 -0
  48. package/dist/lib/deploy-render-probe.d.ts +117 -0
  49. package/dist/lib/deploy-render-probe.js +475 -0
  50. package/dist/lib/deploy-vm-um-app.d.ts +62 -0
  51. package/dist/lib/deploy-vm-um-app.js +83 -0
  52. package/dist/lib/segredo-produto.d.ts +25 -5
  53. package/dist/lib/segredo-produto.js +45 -10
  54. package/dist/lib/sse.d.ts +4 -5
  55. package/dist/lib/sse.js +4 -5
  56. package/package.json +1 -2
@@ -0,0 +1,49 @@
1
+ import { type TenantEnvironmentId } from '../lib/envs.js';
2
+ /** Ambientes que dá pra re-apontar: os do Core menos o dev-local (roda na máquina do dev). */
3
+ export type RepointEnvironment = Exclude<TenantEnvironmentId, 'dev-local'>;
4
+ export declare const REPOINT_ENVIRONMENTS: readonly RepointEnvironment[];
5
+ export interface RepointCheck {
6
+ name: string;
7
+ ok: boolean;
8
+ detail?: string;
9
+ }
10
+ export interface RepointResponse {
11
+ ok: true;
12
+ productId: string;
13
+ slug: string | null;
14
+ dryRun: boolean;
15
+ changed: boolean;
16
+ alreadyPointed: boolean;
17
+ env: RepointEnvironment;
18
+ from: string | null;
19
+ to: string;
20
+ preflight: {
21
+ ok: boolean;
22
+ checks: RepointCheck[];
23
+ };
24
+ appMoved: false;
25
+ pendingDrain: string[];
26
+ auditId: string | null;
27
+ nextStep: string;
28
+ }
29
+ /** Nome legível de cada requisito do `--dry-run` (o Core manda o nome técnico). */
30
+ export declare const REPOINT_CHECK_LABELS: Record<string, string>;
31
+ /** Rótulo de um requisito (os de banco vêm como `database:<dbId>`). */
32
+ export declare function checkLabel(name: string): string;
33
+ /**
34
+ * O que fazer para cada código `REPOINT_*` que a rota devolve. A mensagem do
35
+ * Core (com VMs, bancos e o motivo) vem antes; isto é o "e agora?".
36
+ */
37
+ export declare const REPOINT_GUIDANCE: Record<string, string>;
38
+ /** Mensagem do Core primeiro (dados reais), depois o que fazer. Pura. */
39
+ export declare function describeRepointError(code: string, serverMessage: string): string;
40
+ export interface EnvironmentsRepointOptions {
41
+ to?: string;
42
+ expectFrom?: string;
43
+ reason?: string;
44
+ dryRun?: boolean;
45
+ yes?: boolean;
46
+ mfaToken?: string;
47
+ json?: boolean;
48
+ }
49
+ export declare function runEnvironmentsRepoint(slug: string, env: string, opts?: EnvironmentsRepointOptions): Promise<void>;
@@ -0,0 +1,219 @@
1
+ /**
2
+ * `neetru environments repoint <slug> <env> --to <serverId> --expect-from <serverId> --reason "<motivo>"`
3
+ * — RE-APONTA o ambiente (`staging` | `prod`) de um produto para outra VM
4
+ * (Core bug_7446f7f2).
5
+ *
6
+ * Antes não havia comando para isso: só um PATCH manual no Firestore pelo Dev
7
+ * Core, e o go-live de um produto travava quando ele estava fora.
8
+ *
9
+ * O que muda: SÓ o ponteiro do ambiente (a VM que o ambiente aponta). O app NÃO
10
+ * muda de VM: o próximo `neetru deploy --target vm --env <env> --server <id>`
11
+ * é que leva o app, acerta a porta e o domínio. A promoção de staging para prod
12
+ * passa a usar a VM apontada. A VM antiga continua rodando o que estava lá e
13
+ * fica contada como ocupada por este ambiente.
14
+ *
15
+ * `--dry-run` confere os requisitos sem gravar e sem MFA. Fora dele: admin +
16
+ * MFA (`--mfa-token` ou a sessão de 1h de `neetru auth mfa-session`; sessão
17
+ * recusada pelo Core ⇒ pede um TOTP fresco UMA vez).
18
+ *
19
+ * Endpoint (Core): POST /api/cli/v1/products/{slug}/environments/{env}/server
20
+ * body `{ toServerId, expectedServerId, reason, dryRun? }`.
21
+ *
22
+ * Plain PT-BR em toda string visível ao usuário. O grupo `neetru env` é do
23
+ * NEETRU_ENV local e das variáveis do Cloud Run — por isso o nome `environments`.
24
+ */
25
+ import chalk from 'chalk';
26
+ import { apiRequest, CliApiError } from '../lib/api-client.js';
27
+ import { handleApiError } from '../lib/api-error.js';
28
+ import { requireToken } from '../lib/cli-read.js';
29
+ import { confirmDestructive, resolveStepUp, withStepUpRetry } from '../lib/cli-write.js';
30
+ import { TENANT_ENVIRONMENT_IDS } from '../lib/envs.js';
31
+ import { log } from '../utils/logger.js';
32
+ import { isSafeExitHalt, safeExit, setSafeExitCode } from '../utils/safe-exit.js';
33
+ export const REPOINT_ENVIRONMENTS = TENANT_ENVIRONMENT_IDS.filter((e) => e !== 'dev-local');
34
+ const SERVER_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
35
+ const MAX_REASON = 500;
36
+ // ---------------------------------------------------------------------------
37
+ // Textos (puros — testáveis sem rede)
38
+ // ---------------------------------------------------------------------------
39
+ /** Nome legível de cada requisito do `--dry-run` (o Core manda o nome técnico). */
40
+ export const REPOINT_CHECK_LABELS = {
41
+ environment: 'o ambiente existe',
42
+ environment_target_vm: 'o ambiente roda em VM',
43
+ environment_active: 'o ambiente não está arquivado nem suspenso',
44
+ expected_server: 'o ambiente aponta a VM informada em --expect-from',
45
+ no_deploy_in_flight: 'nenhum lançamento em andamento',
46
+ target_ready: 'VM nova no ar e pronta',
47
+ target_fleet: 'VM nova é da frota da Neetru',
48
+ target_role: 'VM nova aceita app e é compartilhada',
49
+ target_public_ip: 'VM nova tem IP público (o ambiente tem domínio público)',
50
+ target_capacity: 'VM nova tem espaço para mais um app',
51
+ same_vm: 'nenhum outro ambiente do produto na VM nova',
52
+ databases: 'bancos do ambiente',
53
+ };
54
+ /** Rótulo de um requisito (os de banco vêm como `database:<dbId>`). */
55
+ export function checkLabel(name) {
56
+ if (name.startsWith('database:'))
57
+ return `banco ${name.slice('database:'.length)} alcançável da VM nova`;
58
+ return REPOINT_CHECK_LABELS[name] ?? name;
59
+ }
60
+ /**
61
+ * O que fazer para cada código `REPOINT_*` que a rota devolve. A mensagem do
62
+ * Core (com VMs, bancos e o motivo) vem antes; isto é o "e agora?".
63
+ */
64
+ export const REPOINT_GUIDANCE = {
65
+ REPOINT_INPUT_INVALID: 'Algum dado do pedido está fora do formato: o ambiente é staging ou prod, e as VMs são ids de servidor (letras, números, _ ou -).',
66
+ REPOINT_PRODUCT_NOT_FOUND: 'Produto não encontrado. Confira o slug ou o id do produto.',
67
+ REPOINT_ENV_NOT_FOUND: 'O produto não tem esse ambiente. Ele nasce no primeiro deploy.',
68
+ REPOINT_ENV_NOT_VM: 'Esse ambiente não roda em VM, então não há VM para re-apontar.',
69
+ REPOINT_ENV_INACTIVE: 'O ambiente está arquivado ou suspenso. Re-apontar só vale para ambiente ativo.',
70
+ REPOINT_EXPECTED_MISMATCH: 'O ambiente não aponta a VM que você informou: alguém mudou desde que você olhou. Veja a VM atual no requisito ' +
71
+ '"o ambiente aponta a VM informada" rodando ' +
72
+ '`neetru environments repoint <slug> <env> --to <serverId> --expect-from <serverId> --reason "<motivo>" --dry-run`, ' +
73
+ 'e repita com o valor certo. Nada foi gravado.',
74
+ REPOINT_DEPLOY_IN_PROGRESS: 'Há um lançamento em andamento neste ambiente. Espere ele terminar e repita. Nada foi gravado.',
75
+ REPOINT_TARGET_NOT_READY: 'A VM nova não existe, não está no ar, está sem dar sinal há mais de 5 minutos ou ainda não terminou de se preparar.',
76
+ REPOINT_TARGET_NOT_FLEET: 'A VM nova não é uma VM da Neetru (é de cliente ou não tem identidade no Google Cloud do projeto).',
77
+ REPOINT_TARGET_ROLE: 'A VM nova não recebe apps (é só de banco) ou é exclusiva. Escolha uma VM que aceite apps.',
78
+ REPOINT_TARGET_NO_CAPACITY: 'A VM nova está sem espaço para mais um app. Escolha outra VM ou libere espaço nela.',
79
+ REPOINT_TARGET_IP_NOT_PUBLIC: 'O ambiente tem domínio público e a VM nova não tem IP público — o domínio não teria para onde apontar.',
80
+ REPOINT_SAME_VM_COLLISION: 'Outro ambiente do mesmo produto está (ou pode estar) rodando na VM nova. Os dois se sobrescreveriam: use uma VM por ambiente.',
81
+ REPOINT_DB_UNREACHABLE: 'Um banco deste ambiente fica em outra VM e não tem a porta de entrada ligada para a VM nova — o app ficaria sem banco. ' +
82
+ 'Ligue antes com `neetru db frontdoor enable <dbId> --app-server <serverId> --reason "<motivo>"` e repita.',
83
+ REPOINT_DB_ENGINE_UNSUPPORTED: 'Um banco deste ambiente fica em outra VM e é de um tipo sem porta de entrada (MySQL, Mongo ou cluster). ' +
84
+ 'O app precisa ficar na mesma VM desse banco. Fale com o Dev Core.',
85
+ REPOINT_DB_OP_IN_PROGRESS: 'Um banco deste ambiente está mudando de VM ou ligando a porta de entrada. Espere terminar e repita.',
86
+ REPOINT_READ_FAILED: 'Não foi possível ler um dos requisitos agora. Nada foi gravado — repita em instantes.',
87
+ REPOINT_STATE_CHANGED: 'O ambiente ou um requisito mudou entre a conferência e a gravação. Nada foi gravado — confira e repita.',
88
+ REPOINT_COMMIT_FAILED: 'A gravação não conseguiu terminar. Nada foi gravado — repita em instantes.',
89
+ };
90
+ /** Mensagem do Core primeiro (dados reais), depois o que fazer. Pura. */
91
+ export function describeRepointError(code, serverMessage) {
92
+ const guidance = REPOINT_GUIDANCE[code];
93
+ return guidance ? `${serverMessage}\n ${guidance}` : serverMessage;
94
+ }
95
+ /** Erro da rota: `REPOINT_*` vira mensagem + o que fazer; o resto vai pro handler comum. */
96
+ function failRepoint(err, json) {
97
+ if (isSafeExitHalt(err))
98
+ throw err;
99
+ if (err instanceof CliApiError && typeof err.code === 'string' && err.code.startsWith('REPOINT_')) {
100
+ if (json) {
101
+ console.log(JSON.stringify({
102
+ ok: false,
103
+ error: err.code,
104
+ message: err.message,
105
+ guidance: REPOINT_GUIDANCE[err.code] ?? null,
106
+ }));
107
+ }
108
+ else {
109
+ log.error(describeRepointError(err.code, err.message));
110
+ }
111
+ return safeExit(1);
112
+ }
113
+ return handleApiError(err, json);
114
+ }
115
+ function inputError(message, json) {
116
+ if (json)
117
+ console.log(JSON.stringify({ ok: false, error: 'input', message }));
118
+ else
119
+ log.error(message);
120
+ return safeExit(1);
121
+ }
122
+ function printChecks(checks) {
123
+ log.heading('Requisitos');
124
+ for (const c of checks) {
125
+ const mark = c.ok ? chalk.green('✓') : chalk.red('✗');
126
+ log.dim(` ${mark} ${checkLabel(c.name)}${c.detail ? ` — ${c.detail}` : ''}`);
127
+ }
128
+ }
129
+ export async function runEnvironmentsRepoint(slug, env, opts = {}) {
130
+ if (!opts.json)
131
+ log.banner();
132
+ if (!slug || !slug.trim() || slug.includes('/'))
133
+ inputError('Informe o produto (slug ou id).', opts.json);
134
+ if (!REPOINT_ENVIRONMENTS.includes(env)) {
135
+ inputError(`Ambiente inválido: "${env}". Use ${REPOINT_ENVIRONMENTS.join(' ou ')}.`, opts.json);
136
+ }
137
+ // Literal aqui (não num helper): a guarda de step-up da CLI precisa enxergar a rota.
138
+ const rota = `/api/cli/v1/products/${encodeURIComponent(slug)}/environments/${encodeURIComponent(env)}/server`;
139
+ const to = (opts.to ?? '').trim();
140
+ const expectFrom = (opts.expectFrom ?? '').trim();
141
+ const reason = (opts.reason ?? '').trim();
142
+ if (!SERVER_ID_RE.test(to))
143
+ inputError('--to <serverId> é obrigatório (o id da VM nova).', opts.json);
144
+ if (!SERVER_ID_RE.test(expectFrom)) {
145
+ inputError('--expect-from <serverId> é obrigatório (a VM que o ambiente aponta hoje — se mudou, nada é gravado).', opts.json);
146
+ }
147
+ if (!reason || reason.length > MAX_REASON) {
148
+ inputError(`--reason "<texto>" é obrigatório (1 a ${MAX_REASON} caracteres; vai para a auditoria).`, opts.json);
149
+ }
150
+ await requireToken(opts.json);
151
+ if (!opts.json) {
152
+ log.heading(opts.dryRun ? 'Re-apontar a VM do ambiente (só conferência)' : 'Re-apontar a VM do ambiente');
153
+ log.info(`Produto: ${chalk.cyan(slug)}`);
154
+ log.info(`Ambiente: ${chalk.bold(env)}`);
155
+ log.info(`Aponta hoje: ${expectFrom}`);
156
+ log.info(`Vai apontar: ${chalk.bold(to)}`);
157
+ log.dim(' Só o ponteiro muda — o app não muda de VM até o próximo deploy na VM nova.');
158
+ }
159
+ const body = { toServerId: to, expectedServerId: expectFrom, reason };
160
+ // ── Só conferência: nada é gravado, sem MFA. ──────────────────────────────
161
+ if (opts.dryRun) {
162
+ const { headers } = await resolveStepUp({ dryRun: true, json: opts.json, mfaToken: opts.mfaToken });
163
+ let r;
164
+ try {
165
+ r = await apiRequest(rota, {
166
+ method: 'POST',
167
+ body: { ...body, dryRun: true },
168
+ headers,
169
+ });
170
+ }
171
+ catch (err) {
172
+ failRepoint(err, opts.json);
173
+ }
174
+ if (opts.json) {
175
+ console.log(JSON.stringify({ ...r, ok: r.preflight.ok || r.alreadyPointed }));
176
+ if (!r.preflight.ok && !r.alreadyPointed)
177
+ setSafeExitCode(1);
178
+ return;
179
+ }
180
+ if (r.alreadyPointed) {
181
+ log.info(`O ambiente ${env} já aponta ${to} — não há o que re-apontar.`);
182
+ return;
183
+ }
184
+ printChecks(r.preflight.checks);
185
+ if (r.preflight.ok) {
186
+ log.success('Tudo certo para re-apontar. Nada foi alterado. Rode de novo sem --dry-run para gravar.');
187
+ }
188
+ else {
189
+ log.error('Algum requisito falhou — o ambiente não seria re-apontado. Nada foi alterado.');
190
+ setSafeExitCode(1);
191
+ }
192
+ return;
193
+ }
194
+ await confirmDestructive(`Re-apontar o ambiente ${env} do produto ${slug} de ${expectFrom} para ${to}? Só o ponteiro muda: o app ` +
195
+ 'continua onde está até o próximo deploy na VM nova, e a promoção de staging para prod passa a usar a VM nova.', { yes: opts.yes, json: opts.json });
196
+ const r = await withStepUpRetry({ mfaToken: opts.mfaToken, json: opts.json, yes: opts.yes }, (headers) => apiRequest(rota, { method: 'POST', body, headers }), (err) => failRepoint(err, opts.json));
197
+ if (opts.json) {
198
+ console.log(JSON.stringify(r));
199
+ return;
200
+ }
201
+ if (r.alreadyPointed) {
202
+ log.info(`O ambiente ${env} já apontava ${to} — nada foi gravado.`);
203
+ return;
204
+ }
205
+ if (!r.changed) {
206
+ // Não deveria acontecer (o Core responde erro quando não grava), mas sem mentir:
207
+ log.error('O Core respondeu sem gravar o ponteiro. Nada mudou — confira com --dry-run.');
208
+ setSafeExitCode(1);
209
+ return;
210
+ }
211
+ log.success(`Ambiente ${env} agora aponta ${r.to} (antes: ${r.from ?? 'nenhuma VM'}).`);
212
+ log.warn('O app NÃO mudou de VM: só o ponteiro do ambiente.');
213
+ if (r.pendingDrain.length > 0) {
214
+ log.dim(` Continua rodando o que estava em ${r.pendingDrain.join(', ')} (nada é desligado), e essa VM segue ` +
215
+ 'contada como ocupada por este ambiente.');
216
+ }
217
+ log.info(`Próximo passo: ${r.nextStep}`);
218
+ }
219
+ //# sourceMappingURL=environments-repoint.js.map
@@ -80,7 +80,13 @@ async function resolveTarget(name, cwd, opts) {
80
80
  // Default: cria subdir <cwd>/<name>
81
81
  const insp = await inspectDir(subDir);
82
82
  if (insp.exists && !insp.isEmpty) {
83
- log.error(`Diretório "${name}" já existe com arquivos. Use --here ou --path ou --force.`);
83
+ // bug_89d2497e (etapa 1): o `neetru new` chega aqui pelo `runInit` e não
84
+ // declara --here/--path/--force — "Use --here" mandava o usuário repetir
85
+ // o `neetru new` com uma flag que ele recusa. O conselho cita o comando
86
+ // inteiro. E `--force` com `--yes` cai de novo neste mesmo ramo (ele não
87
+ // lê `opts.force`); sobrescrever é `--path <dir> --force`.
88
+ log.error(`Diretório "${name}" já existe com arquivos. Rode \`neetru init ${name} --here\` (mescla no diretório atual), ` +
89
+ `\`neetru init ${name} --path <outro-diretório>\` ou, para sobrescrever, \`neetru init ${name} --path ${name} --force\`.`);
84
90
  safeExit(1); // erro hard em non-interactive — consistente com pre-2.2.0
85
91
  }
86
92
  return { dir: subDir, mode: 'new', force: false };
@@ -195,6 +195,20 @@ export async function runNewMacro(name, opts = {}) {
195
195
  safeExit(1);
196
196
  }
197
197
  let slug = (opts.productSlug ?? deriveSlug(name)) || deriveSlug(name);
198
+ // bug_89d2497e (etapa 1): nome sem letra nem dígito ("!!!") deriva slug
199
+ // vazio, e quem recusava era o `products create`, com "informe --slug" —
200
+ // flag que o `neetru new` não tem (a dele é --product-slug). Recusa aqui,
201
+ // antes de autenticar e de criar qualquer coisa no Core.
202
+ if (!slug) {
203
+ const msg = `Não dá para derivar um slug de "${name}" — informe --product-slug <slug>.`;
204
+ if (opts.json) {
205
+ console.log(JSON.stringify({ ok: false, error: 'invalid_input', message: msg }));
206
+ }
207
+ else {
208
+ log.error(msg);
209
+ }
210
+ safeExit(1);
211
+ }
198
212
  // bug_7dcb68e2666a43f69ef362d2cbff94ed: o padrão era 'dev', token que o Core
199
213
  // não conhece — a macro morria no passo [2/5] com 400 validation_failed.
200
214
  const env = opts.env
@@ -28,10 +28,12 @@
28
28
  * '--value=sk_live…'", "unknown command 'sk_live…'") imprimiria o segredo de
29
29
  * volta no terminal.
30
30
  *
31
- * ATENÇÃO (2026-10-16): a entrega ao app na VM depende do lado do AGENTE
32
- * (Parte 2 de bug_8b85353b, `neetru-agent-service-vm`), que ainda não foi
33
- * liberado. Até lá o registro funciona mas o segredo não chega ao app — e a
34
- * CLI diz isso ao gravar, em vez de deixar o dev descobrir em produção.
31
+ * Entrega ao app na VM: é o lado do AGENTE (Parte 2 de bug_8b85353b,
32
+ * `neetru-agent-service-vm`), publicado na 1.6.80 (2026-10-20). No deploy, o
33
+ * agente busca os valores no Core e grava `/etc/neetru/<slug>.product.env`
34
+ * (root, 0600). Por isso o app só vê uma gravação no PRÓXIMO deploy — e a CLI
35
+ * diz isso ao gravar (`notaEntregaDoSegredo`). Até a 2.33.0 ela dizia que o
36
+ * agente "ainda não entrega", frase falsa desde a 1.6.80.
35
37
  */
36
38
  import type { Command } from 'commander';
37
39
  import { type StepUpOptions } from '../lib/cli-write.js';
@@ -5,7 +5,7 @@ import { renderTable, fmtTimestamp } from '../lib/render.js';
5
5
  import { requireToken, handleApiError, confirmDestructive, printWriteResult, resolveStepUp, isMfaRequiredError, } from '../lib/cli-write.js';
6
6
  import { loadProductConfig } from '../lib/product-config.js';
7
7
  import { listarAmbientesTenant, resolverAmbienteTenant, } from '../lib/envs.js';
8
- import { AVISO_AGENTE_PENDENTE, CodificacaoInvalida, PedidoCancelado, lerValorDeArquivo, lerValorDeFluxo, perguntarSemEco, problemaNaChave, problemaNaChaveReservada, problemaNoTextoDoTerminal, problemaNoValor, problemaNosBytes, } from '../lib/segredo-produto.js';
8
+ import { CodificacaoInvalida, PedidoCancelado, lerValorDeArquivo, lerValorDeFluxo, notaEntregaDoSegredo, perguntarSemEco, problemaNaChave, problemaNaChaveReservada, problemaNoTextoDoTerminal, problemaNoValor, problemaNosBytes, } from '../lib/segredo-produto.js';
9
9
  import { log } from '../utils/logger.js';
10
10
  import { safeExit } from '../utils/safe-exit.js';
11
11
  function entradaPadrao() {
@@ -61,8 +61,13 @@ function exigirAmbiente(env, json) {
61
61
  }
62
62
  return resolverAmbienteTenant(env, json);
63
63
  }
64
- function exigirChave(chave, json) {
65
- const problema = problemaNaChave(chave);
64
+ /**
65
+ * `comoMandarOValor`: só o `set` passa (é ele que recebe valor, por --stdin,
66
+ * --from-file ou pelo pedido na tela). O `archive` não passa: lá o conselho é
67
+ * só "passe só o nome" (bug_89d2497e, etapa 1).
68
+ */
69
+ function exigirChave(chave, json, comoMandarOValor) {
70
+ const problema = problemaNaChave(chave, comoMandarOValor);
66
71
  if (problema)
67
72
  return falhar(json, 'invalid_key', problema);
68
73
  return chave;
@@ -170,7 +175,7 @@ function cabeRepetirComTotp(ambiente, headers, erro) {
170
175
  }
171
176
  export async function runSecretsSet(opts, entrada = entradaPadrao()) {
172
177
  recusarArgumentosExtras(opts.extras, opts.json);
173
- const chave = exigirChave(opts.key, opts.json);
178
+ const chave = exigirChave(opts.key, opts.json, 'por --stdin, --from-file ou pelo pedido na tela');
174
179
  exigirChaveRegistravel(chave, opts.json);
175
180
  const ambiente = exigirAmbiente(opts.env, opts.json);
176
181
  const origens = [!!opts.generate, opts.fromFile !== undefined, !!opts.stdin].filter(Boolean).length;
@@ -245,7 +250,7 @@ export async function runSecretsSet(opts, entrada = entradaPadrao()) {
245
250
  revision: res.revision,
246
251
  created: res.created,
247
252
  secretRef: res.secretRef,
248
- note: AVISO_AGENTE_PENDENTE,
253
+ note: notaEntregaDoSegredo(res.environmentId),
249
254
  }, `Segredo ${chalk.bold(res.key)} ${acao} em ${res.environmentId} (revisão ${res.revision})` +
250
255
  (gerado ? ' — o Core sorteou o valor; ninguém o vê, nem você.' : '.'), opts.json);
251
256
  if (opts.json)
@@ -253,8 +258,10 @@ export async function runSecretsSet(opts, entrada = entradaPadrao()) {
253
258
  if (!res.created)
254
259
  log.dim('O valor anterior deixa de ser entregue a partir do próximo deploy.');
255
260
  log.dim(`Nome no Secret Manager: ${res.secretRef} (é o nome, não o valor).`);
256
- log.dim(`Para o app receber: faça um novo deploy em ${res.environmentId}.`);
257
- log.warn(AVISO_AGENTE_PENDENTE);
261
+ // Até a 2.33.0 saía aqui um aviso de que o agente da VM "ainda não entrega"
262
+ // segredos do produto — falso desde a 1.6.80 (bug_8b85353b). Fica só o fato:
263
+ // o app recebe no próximo deploy.
264
+ log.info(notaEntregaDoSegredo(res.environmentId));
258
265
  }
259
266
  export async function runSecretsList(opts) {
260
267
  const ambiente = opts.env?.trim() ? resolverAmbienteTenant(opts.env, opts.json) : undefined;
@@ -1080,7 +1080,7 @@ async function authMenu() {
1080
1080
  }
1081
1081
  export async function runTerminalUi() {
1082
1082
  if (!canPrompt()) {
1083
- log.error('A interface interativa exige um terminal TTY. Use comandos diretos ou --json em CI.');
1083
+ log.error('A interface interativa exige um terminal TTY. Em CI, use os comandos diretos (ex.: `neetru products list --json`).');
1084
1084
  safeExit(1);
1085
1085
  }
1086
1086
  // Blindagem contra auto-encerramento: enquanto o menu interativo roda, um
@@ -31,6 +31,7 @@ import { renderTable, fmtTimestamp } from '../lib/render.js';
31
31
  import { pickProductId, pickProductSlug, pickWorkspace, resolveProductIdentifier, isInteractive, promptChoice, promptText, resolveCustomer, } from '../lib/pickers.js';
32
32
  import { DEFAULT_TENANT_ENVIRONMENT_ID, ESCOLHAS_AMBIENTE_TENANT, resolverAmbienteTenant, erroAmbienteTenantInvalido, normalizarAmbienteTenant, } from '../lib/envs.js';
33
33
  import { exitWithCliError } from '../lib/cli-error.js';
34
+ import { adviseFlag } from '../cli-kit/command-context.js';
34
35
  import { log } from '../utils/logger.js';
35
36
  import { safeExit } from '../utils/safe-exit.js';
36
37
  /** Formata o erro 400 "Nenhum customer ativo" de forma acionável. */
@@ -100,11 +101,25 @@ export async function runWorkspacesCreate(opts) {
100
101
  const label = existingTenant.name ?? existingTenant.id ?? '(sem nome)';
101
102
  // Modo script/JSON (--json ou --yes implícito): bloqueia com 409.
102
103
  if (opts.json) {
104
+ // bug_89d2497e (etapa 1): o `neetru new` chama este runner com
105
+ // `json: true` e este envelope é o que o usuário dele lê — e o `new`
106
+ // não declara --force-duplicate. O texto sai conforme o comando em
107
+ // execução: só quem declara a flag (o `workspaces create`) ouve "Use
108
+ // --force-duplicate"; os outros recebem o caminho que existe — o
109
+ // `workspaces create` num terminal interativo pergunta se reusa o
110
+ // ambiente existente ou cria outro.
111
+ const message = adviseFlag({
112
+ flag: '--force-duplicate',
113
+ withFlag: `Já existe ambiente ativo '${label}' para produto=${product} env=${env}. Use --force-duplicate para criar mesmo assim.`,
114
+ withoutFlag: `Já existe ambiente ativo '${label}' para produto=${product} env=${env}. Para criar um segundo, rode ` +
115
+ `\`neetru workspaces create --product ${product} --env ${env}\` num terminal interativo: ele pergunta se ` +
116
+ 'reusa o existente ou cria outro.',
117
+ });
103
118
  console.log(JSON.stringify({
104
119
  ok: false,
105
120
  error: 'tenant_already_exists',
106
121
  status: 409,
107
- message: `Já existe ambiente ativo '${label}' para produto=${product} env=${env}. Use --force-duplicate para criar mesmo assim.`,
122
+ message,
108
123
  existing: existingTenant,
109
124
  }));
110
125
  safeExit(1);
package/dist/index.js CHANGED
@@ -8,6 +8,8 @@ import { CliApiError } from './lib/api-client.js';
8
8
  import { NeetruCliError } from './cli-kit/errors.js';
9
9
  import { installCommandContext, getCurrentCommand } from './cli-kit/command-context.js';
10
10
  import { emitCliError } from './lib/cli-error.js';
11
+ import { AJUDA_DEPLOY_NA_VM } from './lib/deploy-vm-um-app.js';
12
+ import { MODELOS_DO_AI_NA_TELA, NOMES_DOS_PROVEDORES, PROVEDORES_IA, } from './lib/ai/provedores.js';
11
13
  // UV_HANDLE_CLOSING no Windows (bug_818cf6d): o double-destroy de stdin (handler
12
14
  // 'exit' redundante) foi removido em 2026-07-15 (ddd479f), MAS isso não bastou
13
15
  // — a causa dominante é o `uv_async_t` do undici (fetch) ainda vivo quando o
@@ -114,7 +116,9 @@ installCommandContext(program);
114
116
  const aiCommand = program
115
117
  .command('ai')
116
118
  .description('Abrir REPL de IA Neetru-aware (BYOK ou AI Gateway)')
117
- .option('-m, --model <model>', 'modelo: claude | openai | gemini | groq | nvidia | gateway | auto', 'auto')
119
+ // Sem valor padrão de propósito: sem `-m`, o `runAiRepl` usa o `defaultModel`
120
+ // da config (que começa em `auto`). Um padrão aqui o esconderia.
121
+ .option('-m, --model <model>', `modelo: ${MODELOS_DO_AI_NA_TELA} (sem -m: o defaultModel da config, que começa em auto)`)
118
122
  .action(async (opts) => {
119
123
  const { runAiRepl } = await import('./commands/ai.js');
120
124
  await runAiRepl({ model: opts.model });
@@ -125,7 +129,7 @@ const aiCommand = program
125
129
  // por `neetru code`.
126
130
  aiCommand
127
131
  .command('models [id]')
128
- .description('Lista o catalogo de modelos curados (Groq/NVIDIA/OpenAI); com [id] fixa como default')
132
+ .description(`Lista o catalogo de modelos curados (${NOMES_DOS_PROVEDORES}); com [id] fixa como default`)
129
133
  .action(async (id) => {
130
134
  const { runAiModelsList, runAiModelsUse } = await import('./commands/ai-models.js');
131
135
  if (id)
@@ -138,7 +142,7 @@ program
138
142
  .command('code [prompt...]')
139
143
  .description('Abrir assistente local de codigo via Neetru AI Gateway (Groq/NVIDIA)')
140
144
  .option('-m, --model <model>', 'modelo do AI Gateway (default: llama-3.3-70b-versatile) — veja "neetru ai models"')
141
- .option('--provider <provider>', 'forca o provider (groq|nvidia|openai) — necessario p/ modelo nativo de um catalogo especifico')
145
+ .option('--provider <provider>', `forca o provider (${PROVEDORES_IA.join('|')}) — necessario p/ modelo nativo de um catalogo especifico`)
142
146
  .option('--cwd <dir>', 'repo/diretorio alvo (default: cwd atual)')
143
147
  .option('-f, --file <path>', 'inclui arquivo no contexto inicial (repetivel)', (value, previous) => previous.concat(value), [])
144
148
  .option('--max-turns <n>', 'maximo de rodadas tool/read antes de parar', '8')
@@ -222,10 +226,12 @@ program
222
226
  });
223
227
  });
224
228
  // ── neetru auth ───────────────────────────────────────────────────────
225
- // Wave D — detecta Claude Code/Codex/Gemini instalados e reaproveita creds.
229
+ // Wave D — reaproveita chave já existente na máquina. bug_19d312f3: os
230
+ // provedores são os da lista única (`lib/ai/provedores.ts`); Anthropic e Gemini
231
+ // saíram (vetados).
226
232
  const authCmd = program
227
233
  .command('auth')
228
- .description('Gerenciar credenciais de IA (Claude, Codex, Gemini)');
234
+ .description(`Gerenciar credenciais de IA (${NOMES_DOS_PROVEDORES})`);
229
235
  authCmd
230
236
  .command('status')
231
237
  .description('Listar provedores detectados e estado das credenciais')
@@ -244,7 +250,7 @@ authCmd
244
250
  });
245
251
  authCmd
246
252
  .command('pull')
247
- .description('Copiar creds dos CLIs detectados pra config do Neetru')
253
+ .description('Copiar pra config do Neetru a chave achada no arquivo do Codex (as do ambiente já são lidas direto)')
248
254
  .option('--json', 'saída JSON')
249
255
  .action(async (opts) => {
250
256
  const { runAuthPull } = await import('./commands/auth.js');
@@ -459,6 +465,8 @@ program
459
465
  program
460
466
  .command('deploy')
461
467
  .description('Deployar produto via pipeline do Neetru Core')
468
+ // bug_6d92ef71: um app por produto e ambiente na VM, e como ter dois.
469
+ .addHelpText('after', AJUDA_DEPLOY_NA_VM)
462
470
  .option('--product <slug>', 'slug do produto')
463
471
  .option('--version <version>', 'versão a deployar')
464
472
  .option('--stack <stack>', 'node | nextjs | docker | php-apache | static (gera Dockerfile automático para node/nextjs)')
@@ -1964,6 +1972,36 @@ envCmd
1964
1972
  json: !!opts.json,
1965
1973
  });
1966
1974
  });
1975
+ // ── neetru environments ───────────────────────────────────────────────
1976
+ // Ambientes (staging/prod) de um produto no Core. Separado de `neetru env`, que
1977
+ // cuida do NEETRU_ENV local e das variáveis do Cloud Run.
1978
+ const environmentsCmd = program
1979
+ .command('environments')
1980
+ .description('Ambientes (staging/prod) de um produto no Core');
1981
+ // bug_7446f7f2: re-apontar a VM do ambiente sem PATCH manual no Firestore.
1982
+ environmentsCmd
1983
+ .command('repoint <slug> <env>')
1984
+ .description('Re-apontar o ambiente (staging|prod) para outra VM — só o ponteiro; o app muda no próximo deploy ' +
1985
+ '(POST /api/cli/v1/products/[slug]/environments/[env]/server — exige MFA)')
1986
+ .requiredOption('--to <serverId>', 'VM nova (id do servidor)')
1987
+ .requiredOption('--expect-from <serverId>', 'VM que o ambiente aponta hoje (se mudou, nada é gravado)')
1988
+ .requiredOption('--reason <text>', 'motivo (vai para o registro de auditoria)')
1989
+ .option('--dry-run', 'só confere os requisitos (não altera nada, sem MFA)')
1990
+ .option('-y, --yes', 'pula a confirmação interativa (modo script)')
1991
+ .option('--mfa-token <code>', 'código TOTP step-up MFA (ou use `neetru auth mfa-session`)')
1992
+ .option('--json', 'saída em JSON estruturado')
1993
+ .action(async (slug, env, opts) => {
1994
+ const { runEnvironmentsRepoint } = await import('./commands/environments-repoint.js');
1995
+ await runEnvironmentsRepoint(slug, env, {
1996
+ to: opts.to,
1997
+ expectFrom: opts.expectFrom,
1998
+ reason: opts.reason,
1999
+ dryRun: !!opts.dryRun,
2000
+ yes: !!opts.yes,
2001
+ mfaToken: opts.mfaToken,
2002
+ json: !!opts.json,
2003
+ });
2004
+ });
1967
2005
  // ── neetru db ─────────────────────────────────────────────────────────
1968
2006
  // M1 — árvore completa de banco por produto.
1969
2007
  // Subcomandos novos: list, status, apply, migrations list/confirm/cancel.
@@ -2163,7 +2201,9 @@ dbCmd
2163
2201
  .option('--backup-id <id>', 'ID do backup a restaurar (pula o prompt; obrigatório em --json)')
2164
2202
  .option('-y, --yes', 'pula a confirmação textual "RESTAURAR" (obrigatório em --json)')
2165
2203
  // Flag canônica `--mfa-token`; `--mfa` como alias oculto (retrocompat).
2166
- .option('--mfa-token <token>', 'código TOTP step-up MFA (obrigatório — operação destrutiva)')
2204
+ // bug_757b9746: deixou de ser obrigatória — com a sessão de 1h ativa o
2205
+ // restore vai sem código, como os outros comandos destrutivos.
2206
+ .option('--mfa-token <token>', 'código TOTP step-up MFA (ou use `neetru auth mfa-session`)')
2167
2207
  .addOption(new Option('--mfa <token>').hideHelp())
2168
2208
  .option('--json', 'saída em JSON')
2169
2209
  .action(async (dbId, opts) => {
@@ -1,8 +1,11 @@
1
- export type Provider = 'anthropic' | 'openai' | 'gemini' | 'groq' | 'nvidia';
1
+ import { type ProvedorIa } from './provedores.js';
2
+ export type Provider = ProvedorIa;
2
3
  export interface ProviderStatus {
3
4
  provider: Provider;
4
- binary: string;
5
- npmPackage: string;
5
+ /** Programa de terceiro que guarda a chave (hoje só o `codex`). Ausente = provedor só de chave. */
6
+ binary?: string;
7
+ /** Pacote npm desse programa (hoje só `@openai/codex`). */
8
+ npmPackage?: string;
6
9
  installed: boolean;
7
10
  credentialSource: 'env' | 'config-file' | 'binary-config' | null;
8
11
  credentialPath?: string;
@@ -10,18 +13,12 @@ export interface ProviderStatus {
10
13
  hint: string;
11
14
  }
12
15
  /**
13
- * Detecta todos os providers conhecidos. Read-only — não escreve nada,
14
- * não chama API. Seguro pra rodar em startup do `neetru ai`.
15
- *
16
- * Cobre só os providers que têm CLI concorrente (claude/codex/gemini) — o
17
- * wizard de setup usa esta lista pra ofertar install/login per-binário.
18
- * Groq e NVIDIA NIM são API-key-only (sem CLI pra instalar); a descoberta
19
- * deles vive em `discoveredKeys` (env direto).
16
+ * Estado de cada provedor da lista única, na ordem dela. Read-only — não
17
+ * escreve nada, não chama API. Seguro pra rodar em startup do `neetru ai`.
20
18
  */
21
19
  export declare function detectAllProviders(): ProviderStatus[];
22
20
  /**
23
- * Conveniência: retorna apenas providers com apiKey extraída (pronta pra usar).
24
- * Inclui os CLI-discoverable (claude/codex/gemini) + os API-key-only (groq/nvidia).
21
+ * Conveniência: retorna apenas os provedores com chave achada (pronta pra usar).
25
22
  */
26
23
  export declare function discoveredKeys(): Partial<Record<Provider, string>>;
27
24
  /**