@neetru/cli 2.32.0 → 2.33.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.
- package/CHANGELOG.md +26 -0
- package/dist/commands/bug.js +4 -4
- package/dist/commands/db-frontdoor.d.ts +116 -0
- package/dist/commands/db-frontdoor.js +567 -0
- package/dist/commands/db-move.d.ts +5 -0
- package/dist/commands/db-move.js +27 -0
- package/dist/commands/db.js +3 -9
- package/dist/commands/deploy.js +9 -9
- package/dist/commands/init.js +2 -1
- package/dist/commands/products.js +2 -2
- package/dist/commands/servers.d.ts +29 -0
- package/dist/commands/servers.js +110 -2
- package/dist/index.js +56 -0
- package/dist/lib/api-client.d.ts +8 -1
- package/dist/lib/api-client.js +18 -2
- package/dist/lib/inquirer-sob-demanda.d.ts +14 -0
- package/dist/lib/inquirer-sob-demanda.js +18 -0
- package/dist/lib/pickers.js +5 -3
- package/package.json +1 -1
package/dist/commands/deploy.js
CHANGED
|
@@ -21,7 +21,7 @@ import * as fs from 'node:fs';
|
|
|
21
21
|
import { promises as fsp } from 'node:fs';
|
|
22
22
|
import { spawnSync } from 'node:child_process';
|
|
23
23
|
import * as path from 'node:path';
|
|
24
|
-
import
|
|
24
|
+
import { carregarInquirer } from '../lib/inquirer-sob-demanda.js';
|
|
25
25
|
import chalk from 'chalk';
|
|
26
26
|
import ora from 'ora';
|
|
27
27
|
import { log } from '../utils/logger.js';
|
|
@@ -576,7 +576,7 @@ async function pickProduct(opts, nonInteractive, cwd) {
|
|
|
576
576
|
if (catalog.products.length === 0) {
|
|
577
577
|
deployFail(opts.json, 'no_products', 'Nenhum produto público encontrado. Crie um via `neetru publish` antes.');
|
|
578
578
|
}
|
|
579
|
-
const { slug } = await
|
|
579
|
+
const { slug } = await (await carregarInquirer()).prompt([
|
|
580
580
|
{
|
|
581
581
|
type: 'list',
|
|
582
582
|
name: 'slug',
|
|
@@ -601,7 +601,7 @@ export async function pickTarget(opts, nonInteractive, runtime) {
|
|
|
601
601
|
// handler .ts, não um app Next.js. Recomendar "Workspace" aqui levava quem
|
|
602
602
|
// aceita o padrão direto ao aviso do contrato (confirmação final do PR #74).
|
|
603
603
|
const ehNext = runtime === 'nextjs';
|
|
604
|
-
const { target } = await
|
|
604
|
+
const { target } = await (await carregarInquirer()).prompt([
|
|
605
605
|
{
|
|
606
606
|
type: 'list',
|
|
607
607
|
name: 'target',
|
|
@@ -674,7 +674,7 @@ async function pickServer(opts, nonInteractive) {
|
|
|
674
674
|
if (nonInteractive) {
|
|
675
675
|
deployFail(opts.json, 'server_required', '--server <id> é obrigatório em modo não interativo.');
|
|
676
676
|
}
|
|
677
|
-
const { serverId } = await
|
|
677
|
+
const { serverId } = await (await carregarInquirer()).prompt([
|
|
678
678
|
{
|
|
679
679
|
type: 'list',
|
|
680
680
|
name: 'serverId',
|
|
@@ -704,7 +704,7 @@ async function pickDomain(opts, nonInteractive, productSlug) {
|
|
|
704
704
|
if (nonInteractive)
|
|
705
705
|
return { domain };
|
|
706
706
|
if (!domain) {
|
|
707
|
-
const { d } = await
|
|
707
|
+
const { d } = await (await carregarInquirer()).prompt([
|
|
708
708
|
{
|
|
709
709
|
type: 'input',
|
|
710
710
|
name: 'd',
|
|
@@ -1264,7 +1264,7 @@ async function runWorkspaceDeploy(opts, productSlug, envName, cwd, nonInteractiv
|
|
|
1264
1264
|
if (sourceSizeBytes > WORKSPACE_SOURCE_GCS_THRESHOLD_BYTES) {
|
|
1265
1265
|
log.dim(` upload: via GCS (source grande — acima de ${Math.round(WORKSPACE_SOURCE_GCS_THRESHOLD_BYTES / 1024)} KB)`);
|
|
1266
1266
|
}
|
|
1267
|
-
const { ok } = await
|
|
1267
|
+
const { ok } = await (await carregarInquirer()).prompt([
|
|
1268
1268
|
{ type: 'confirm', name: 'ok', message: 'Confirmar deploy?', default: true },
|
|
1269
1269
|
]);
|
|
1270
1270
|
if (!ok) {
|
|
@@ -1730,7 +1730,7 @@ envFileTexto) {
|
|
|
1730
1730
|
log.dim(` ambiente: ${envName}`);
|
|
1731
1731
|
log.dim(' destino: cloud-run (serviço dedicado)');
|
|
1732
1732
|
log.dim(` imagem: ${imageUrl}`);
|
|
1733
|
-
const { ok } = await
|
|
1733
|
+
const { ok } = await (await carregarInquirer()).prompt([
|
|
1734
1734
|
{ type: 'confirm', name: 'ok', message: 'Confirmar deploy?', default: true },
|
|
1735
1735
|
]);
|
|
1736
1736
|
if (!ok) {
|
|
@@ -1886,7 +1886,7 @@ async function checkSchemaGate(cwd, opts) {
|
|
|
1886
1886
|
log.error(msg);
|
|
1887
1887
|
return false;
|
|
1888
1888
|
}
|
|
1889
|
-
const { continuar } = await
|
|
1889
|
+
const { continuar } = await (await carregarInquirer()).prompt([
|
|
1890
1890
|
{
|
|
1891
1891
|
type: 'confirm',
|
|
1892
1892
|
name: 'continuar',
|
|
@@ -2095,7 +2095,7 @@ export async function runDeploy(opts) {
|
|
|
2095
2095
|
log.dim(` sha256: ${artifact.artifactSha256.slice(0, 16)}…`);
|
|
2096
2096
|
// Em prod o default do confirm é FALSE — Enter não deploya produção sem
|
|
2097
2097
|
// uma decisão explícita (auditoria UX 2026-09).
|
|
2098
|
-
const { ok } = await
|
|
2098
|
+
const { ok } = await (await carregarInquirer()).prompt([
|
|
2099
2099
|
{ type: 'confirm', name: 'ok', message: 'Confirmar deploy?', default: envName !== 'prod' },
|
|
2100
2100
|
]);
|
|
2101
2101
|
if (!ok) {
|
package/dist/commands/init.js
CHANGED
|
@@ -7,6 +7,7 @@ import ora from 'ora';
|
|
|
7
7
|
// todo `neetru init --yes` (CI e scripts) e pelo 1º teste do init.test.ts,
|
|
8
8
|
// que estourava o timeout de 10 s com a máquina carregada.
|
|
9
9
|
import { log } from '../utils/logger.js';
|
|
10
|
+
import { carregarInquirer } from '../lib/inquirer-sob-demanda.js';
|
|
10
11
|
import { safeExit } from '../utils/safe-exit.js';
|
|
11
12
|
/**
|
|
12
13
|
* Inspeciona um diretório pra decidir se é projeto existente, vazio, ou
|
|
@@ -86,7 +87,7 @@ async function resolveTarget(name, cwd, opts) {
|
|
|
86
87
|
}
|
|
87
88
|
// ── Interactive ──────────────────────────────────────────────────────
|
|
88
89
|
// Só aqui há pergunta: o inquirer é carregado agora (ver o topo do arquivo).
|
|
89
|
-
const
|
|
90
|
+
const inquirer = await carregarInquirer();
|
|
90
91
|
console.log();
|
|
91
92
|
log.info(`Onde você quer inicializar o projeto "${name}"?`);
|
|
92
93
|
console.log();
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Saída: tabela (default) ou `--json`.
|
|
10
10
|
*/
|
|
11
11
|
import chalk from 'chalk';
|
|
12
|
-
import
|
|
12
|
+
import { carregarInquirer } from '../lib/inquirer-sob-demanda.js';
|
|
13
13
|
import ora from 'ora';
|
|
14
14
|
import { apiRequest, CliApiError } from '../lib/api-client.js';
|
|
15
15
|
import { config } from '../lib/config.js';
|
|
@@ -190,7 +190,7 @@ export async function runProductsCreate(opts) {
|
|
|
190
190
|
const environmentsRequested = parseEnvCsv(opts.env, [...AMBIENTES_PEDIDOS_POR_PADRAO]);
|
|
191
191
|
const interactive = !opts.yes && !opts.json && process.stdin.isTTY;
|
|
192
192
|
if (interactive) {
|
|
193
|
-
const answers = await
|
|
193
|
+
const answers = await (await carregarInquirer()).prompt([
|
|
194
194
|
{
|
|
195
195
|
type: 'input',
|
|
196
196
|
name: 'name',
|
|
@@ -53,7 +53,36 @@ export declare function runServersDeactivate(id: string | undefined, opts: StepU
|
|
|
53
53
|
*/
|
|
54
54
|
export interface ServersForceCleanupOptions extends StepUpOptions {
|
|
55
55
|
reason: string;
|
|
56
|
+
/**
|
|
57
|
+
* core#732 (bug_90ec21cd): id numérico da instância no Google Cloud que o
|
|
58
|
+
* operador CONFERIU. Só faz falta quando o servidor não tem o id da VM
|
|
59
|
+
* registrado (`externalId`) e existe uma VM com o mesmo nome — o Core recusa
|
|
60
|
+
* com `vm_sem_externalid` e mostra o id visto + o comando de conferência.
|
|
61
|
+
* A CLI NUNCA preenche este id a partir da resposta: o operador digita.
|
|
62
|
+
*/
|
|
63
|
+
confirmInstanceId?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* core#732: recusas da guarda de identidade da VM no force-cleanup. A mensagem
|
|
67
|
+
* do Core (com o id visto e o comando de conferência) vem antes; isto é o que
|
|
68
|
+
* fazer. Nenhum texto monta o comando com o id preenchido — de propósito.
|
|
69
|
+
*/
|
|
70
|
+
export declare const FORCE_CLEANUP_IDENTITY_GUIDANCE: Record<string, string>;
|
|
71
|
+
/**
|
|
72
|
+
* Detalhes que o core#732 manda junto da recusa (`{ error: { …, instanceId?,
|
|
73
|
+
* instanceCreatedAt?, docCreatedAt?, outrosServidores?, comandoConferencia? } }`),
|
|
74
|
+
* só os campos conhecidos e com o tipo certo. Nunca vira comando com o id.
|
|
75
|
+
*/
|
|
76
|
+
export interface ForceCleanupIdentityDetails {
|
|
77
|
+
instanceId?: string;
|
|
78
|
+
instanceCreatedAt?: string;
|
|
79
|
+
docCreatedAt?: string;
|
|
80
|
+
outrosServidores?: string[];
|
|
81
|
+
comandoConferencia?: string;
|
|
56
82
|
}
|
|
83
|
+
export declare function forceCleanupIdentityDetails(raw: unknown): ForceCleanupIdentityDetails | undefined;
|
|
84
|
+
/** Linhas legíveis dos detalhes (texto). Mostra o id VISTO, mas não monta comando com ele. */
|
|
85
|
+
export declare function forceCleanupIdentityDetailLines(d: ForceCleanupIdentityDetails | undefined): string[];
|
|
57
86
|
export declare function runServersForceCleanup(id: string, opts: ServersForceCleanupOptions): Promise<void>;
|
|
58
87
|
export interface ServersDispatchOptions extends StepUpOptions {
|
|
59
88
|
params?: string;
|
package/dist/commands/servers.js
CHANGED
|
@@ -12,6 +12,7 @@ import { apiRequest, CliApiError } from '../lib/api-client.js';
|
|
|
12
12
|
import { requireToken, handleApiError } from '../lib/cli-read.js';
|
|
13
13
|
import { confirmDestructive, printWriteResult, resolveStepUp, } from '../lib/cli-write.js';
|
|
14
14
|
import { renderTable } from '../lib/render.js';
|
|
15
|
+
import { carregarInquirer } from '../lib/inquirer-sob-demanda.js';
|
|
15
16
|
import { pickServer, promptText, promptChoice } from '../lib/pickers.js';
|
|
16
17
|
import { AGENT_COMMAND_ALLOWLIST, AGENT_COMMAND_CHOICES, commandRequiresMfa, } from '../lib/agent-commands.js';
|
|
17
18
|
import { log } from '../utils/logger.js';
|
|
@@ -341,6 +342,73 @@ export async function runServersDeactivate(id, opts) {
|
|
|
341
342
|
}
|
|
342
343
|
printWriteResult(res.dryRun ? res : { serverId: res.serverId, status: res.status }, `Servidor ${res.serverId} → ${res.status}`, opts.json);
|
|
343
344
|
}
|
|
345
|
+
/** Formato que a rota do Core aceita (id numérico da instância no GCP). */
|
|
346
|
+
const INSTANCE_ID_RE = /^[1-9][0-9]{0,19}$/;
|
|
347
|
+
/**
|
|
348
|
+
* core#732: recusas da guarda de identidade da VM no force-cleanup. A mensagem
|
|
349
|
+
* do Core (com o id visto e o comando de conferência) vem antes; isto é o que
|
|
350
|
+
* fazer. Nenhum texto monta o comando com o id preenchido — de propósito.
|
|
351
|
+
*/
|
|
352
|
+
export const FORCE_CLEANUP_IDENTITY_GUIDANCE = {
|
|
353
|
+
vm_sem_externalid: 'Este servidor não tem o id da VM registrado, e existe no Google Cloud uma VM com o mesmo nome — ' +
|
|
354
|
+
'pode ser OUTRA VM (nome reaproveitado). Nada foi apagado. Rode o comando de conferência acima; se a VM ' +
|
|
355
|
+
'for mesmo a deste servidor, repita o force-cleanup com --confirm-instance-id <id que você conferiu>. ' +
|
|
356
|
+
'A CLI não preenche esse id por você.',
|
|
357
|
+
vm_confirmacao_nao_confere: 'O id passado em --confirm-instance-id não é o da VM que o Core leu (a mensagem acima mostra os dois). ' +
|
|
358
|
+
'Nada foi apagado. Confira de novo antes de repetir.',
|
|
359
|
+
vm_id_divergente: 'O Google Cloud devolveu outra instância ao ler o id registrado deste servidor. Nada foi apagado — ' +
|
|
360
|
+
'fale com o Dev Core antes de tentar de novo.',
|
|
361
|
+
vm_leitura_falhou: 'Não deu para ler a VM no Google Cloud agora. Nada foi apagado. Tente de novo em alguns minutos; ' +
|
|
362
|
+
'se continuar, fale com o Dev Core.',
|
|
363
|
+
// core#732 rodada 2: a confirmação do id NÃO destrava este caso — nada de
|
|
364
|
+
// orientar "repita com o id".
|
|
365
|
+
vm_nome_em_uso_por_outro_servidor: 'Outro servidor registrado e ativo usa uma VM com o mesmo nome e zona — a VM pode ser dele. Nada foi apagado, ' +
|
|
366
|
+
'e NÃO adianta confirmar o id: o Core recusa este caso mesmo com --confirm-instance-id. Confira de qual servidor ' +
|
|
367
|
+
'é a VM e registre o id dela no servidor certo antes de excluir (fale com o Dev Core).',
|
|
368
|
+
};
|
|
369
|
+
export function forceCleanupIdentityDetails(raw) {
|
|
370
|
+
if (!raw || typeof raw !== 'object')
|
|
371
|
+
return undefined;
|
|
372
|
+
const r = raw;
|
|
373
|
+
const texto = (v) => (typeof v === 'string' && v.trim() ? v.trim() : undefined);
|
|
374
|
+
const out = {};
|
|
375
|
+
const instanceId = texto(r.instanceId);
|
|
376
|
+
const instanceCreatedAt = texto(r.instanceCreatedAt);
|
|
377
|
+
const docCreatedAt = texto(r.docCreatedAt);
|
|
378
|
+
const comandoConferencia = texto(r.comandoConferencia);
|
|
379
|
+
const outros = Array.isArray(r.outrosServidores)
|
|
380
|
+
? r.outrosServidores.map(texto).filter((x) => !!x)
|
|
381
|
+
: [];
|
|
382
|
+
if (instanceId)
|
|
383
|
+
out.instanceId = instanceId;
|
|
384
|
+
if (instanceCreatedAt)
|
|
385
|
+
out.instanceCreatedAt = instanceCreatedAt;
|
|
386
|
+
if (docCreatedAt)
|
|
387
|
+
out.docCreatedAt = docCreatedAt;
|
|
388
|
+
if (outros.length > 0)
|
|
389
|
+
out.outrosServidores = outros;
|
|
390
|
+
if (comandoConferencia)
|
|
391
|
+
out.comandoConferencia = comandoConferencia;
|
|
392
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
393
|
+
}
|
|
394
|
+
/** Linhas legíveis dos detalhes (texto). Mostra o id VISTO, mas não monta comando com ele. */
|
|
395
|
+
export function forceCleanupIdentityDetailLines(d) {
|
|
396
|
+
if (!d)
|
|
397
|
+
return [];
|
|
398
|
+
const linhas = [];
|
|
399
|
+
if (d.outrosServidores) {
|
|
400
|
+
linhas.push(`Outro(s) servidor(es) ativo(s) com o mesmo nome e zona: ${d.outrosServidores.join(', ')}`);
|
|
401
|
+
}
|
|
402
|
+
if (d.instanceId)
|
|
403
|
+
linhas.push(`VM vista no Google Cloud: id ${d.instanceId}`);
|
|
404
|
+
if (d.instanceCreatedAt || d.docCreatedAt) {
|
|
405
|
+
linhas.push(`Criação da VM: ${d.instanceCreatedAt ?? '?'} · criação deste servidor: ${d.docCreatedAt ?? '?'} ` +
|
|
406
|
+
'(a VM do próprio servidor nasce segundos depois dele; diferença grande indica VM recriada)');
|
|
407
|
+
}
|
|
408
|
+
if (d.comandoConferencia)
|
|
409
|
+
linhas.push(`Para conferir (só leitura): ${d.comandoConferencia}`);
|
|
410
|
+
return linhas;
|
|
411
|
+
}
|
|
344
412
|
export async function runServersForceCleanup(id, opts) {
|
|
345
413
|
if (!id || !/^[a-zA-Z0-9_-]{1,64}$/.test(id)) {
|
|
346
414
|
log.error('serverId inválido. Uso: neetru servers force-cleanup <serverId> --reason="<motivo min 10 chars>"');
|
|
@@ -350,6 +418,16 @@ export async function runServersForceCleanup(id, opts) {
|
|
|
350
418
|
log.error('--reason obrigatório com pelo menos 10 caracteres (paper-trail audit).');
|
|
351
419
|
safeExit(1);
|
|
352
420
|
}
|
|
421
|
+
// `--confirm-instance-id ""` (ou só espaços) é RECUSADO — não vira "flag não passada".
|
|
422
|
+
const confirmInstanceId = opts.confirmInstanceId === undefined ? undefined : opts.confirmInstanceId.trim();
|
|
423
|
+
if (confirmInstanceId !== undefined && !INSTANCE_ID_RE.test(confirmInstanceId)) {
|
|
424
|
+
const msg = '--confirm-instance-id deve ser o id numérico da VM no Google Cloud (o que você conferiu no comando de conferência).';
|
|
425
|
+
if (opts.json)
|
|
426
|
+
console.log(JSON.stringify({ ok: false, error: 'invalid_param', message: msg }));
|
|
427
|
+
else
|
|
428
|
+
log.error(msg);
|
|
429
|
+
safeExit(1);
|
|
430
|
+
}
|
|
353
431
|
const token = await requireToken(opts.json);
|
|
354
432
|
// Double-confirm (digitar o ID) — dry-run não aplica nada, então pula: nada
|
|
355
433
|
// destrutivo acontece, não faz sentido travar numa confirmação interativa.
|
|
@@ -368,8 +446,12 @@ export async function runServersForceCleanup(id, opts) {
|
|
|
368
446
|
}
|
|
369
447
|
console.log(chalk.yellow.bold('\n⚠ FORCE-CLEANUP é IRREVERSÍVEL.'));
|
|
370
448
|
console.log(chalk.dim(' Server stuck em provisioning/timeout que está bloqueando o pipeline.'));
|
|
371
|
-
console.log(chalk.dim(' Audit pesado + notification all staff serão gerados
|
|
372
|
-
|
|
449
|
+
console.log(chalk.dim(' Audit pesado + notification all staff serão gerados.'));
|
|
450
|
+
if (confirmInstanceId) {
|
|
451
|
+
console.log(chalk.dim(` VM a apagar: a instância de id ${confirmInstanceId}, conferida por você.`));
|
|
452
|
+
}
|
|
453
|
+
console.log('');
|
|
454
|
+
const inquirer = await carregarInquirer();
|
|
373
455
|
const ans = await inquirer.prompt([
|
|
374
456
|
{
|
|
375
457
|
type: 'input',
|
|
@@ -395,12 +477,28 @@ export async function runServersForceCleanup(id, opts) {
|
|
|
395
477
|
body: {
|
|
396
478
|
reason: opts.reason.trim(),
|
|
397
479
|
confirm: 'I-UNDERSTAND-FORCE-CLEANUP',
|
|
480
|
+
...(confirmInstanceId ? { confirmInstanceId } : {}),
|
|
398
481
|
},
|
|
399
482
|
});
|
|
400
483
|
spinner?.stop();
|
|
401
484
|
}
|
|
402
485
|
catch (error) {
|
|
403
486
|
spinner?.fail('Falha no force-cleanup.');
|
|
487
|
+
// core#732: a guarda de identidade da VM recusou — nada foi apagado. Mostra
|
|
488
|
+
// a mensagem do Core (id visto + comando de conferência) e o que fazer. Sem
|
|
489
|
+
// nova tentativa automática e sem montar o comando com o id: o operador
|
|
490
|
+
// confere e digita.
|
|
491
|
+
if (error instanceof CliApiError && FORCE_CLEANUP_IDENTITY_GUIDANCE[error.code]) {
|
|
492
|
+
const guidance = FORCE_CLEANUP_IDENTITY_GUIDANCE[error.code];
|
|
493
|
+
const details = forceCleanupIdentityDetails(error.details);
|
|
494
|
+
if (opts.json) {
|
|
495
|
+
console.log(JSON.stringify({ ok: false, error: error.code, message: error.message, guidance, ...(details ? { details } : {}) }));
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
log.error([error.message, ...forceCleanupIdentityDetailLines(details), guidance].join('\n '));
|
|
499
|
+
}
|
|
500
|
+
safeExit(1);
|
|
501
|
+
}
|
|
404
502
|
handleApiError(error, opts.json);
|
|
405
503
|
}
|
|
406
504
|
if (opts.json) {
|
|
@@ -413,6 +511,16 @@ export async function runServersForceCleanup(id, opts) {
|
|
|
413
511
|
}
|
|
414
512
|
log.success(`Server ${res.serverId} force-cleanup concluído.`);
|
|
415
513
|
log.dim(` Status anterior: ${res.previousStatus} (há ${res.ageMinutes}min)`);
|
|
514
|
+
if (res.vmIdentidade) {
|
|
515
|
+
const identidade = {
|
|
516
|
+
'confere:externalId': 'conferida pelo id registrado no servidor',
|
|
517
|
+
'confere:confirmacao': 'conferida pelo id que você confirmou',
|
|
518
|
+
'ausente:id_ausente': 'a VM registrada já não existia',
|
|
519
|
+
'ausente:nome_ausente': 'não havia VM com o nome do servidor',
|
|
520
|
+
'ausente:nome_de_outro_servidor': 'a VM com esse nome é de OUTRO servidor — não foi tocada',
|
|
521
|
+
};
|
|
522
|
+
log.dim(` Identidade da VM: ${identidade[res.vmIdentidade] ?? res.vmIdentidade}`);
|
|
523
|
+
}
|
|
416
524
|
if (res.vmDeleted) {
|
|
417
525
|
log.dim(' VM GCP: deletada com sucesso.');
|
|
418
526
|
}
|
package/dist/index.js
CHANGED
|
@@ -854,6 +854,7 @@ serversCmd
|
|
|
854
854
|
.option('--yes', 'pula a confirmação interativa (modo script)')
|
|
855
855
|
.option('--mfa-token <code>', 'código TOTP para step-up MFA (ou use `neetru auth mfa-session`)')
|
|
856
856
|
.option('--dry-run', 'valida e retorna o efeito sem aplicar (sem MFA)')
|
|
857
|
+
.option('--confirm-instance-id <id>', 'id numérico da VM no Google Cloud que você conferiu — só quando o Core pedir (servidor sem o id da VM registrado)')
|
|
857
858
|
.option('--json', 'saída em JSON')
|
|
858
859
|
.action(async (id, opts) => {
|
|
859
860
|
const { runServersForceCleanup } = await import('./commands/servers.js');
|
|
@@ -862,6 +863,7 @@ serversCmd
|
|
|
862
863
|
yes: !!opts.yes,
|
|
863
864
|
mfaToken: opts.mfaToken,
|
|
864
865
|
dryRun: !!opts.dryRun,
|
|
866
|
+
confirmInstanceId: opts.confirmInstanceId,
|
|
865
867
|
json: !!opts.json,
|
|
866
868
|
});
|
|
867
869
|
});
|
|
@@ -2281,6 +2283,60 @@ dbMoveCmd
|
|
|
2281
2283
|
json: !!opts.json,
|
|
2282
2284
|
});
|
|
2283
2285
|
});
|
|
2286
|
+
// ── neetru db frontdoor ───────────────────────────────────────────────
|
|
2287
|
+
// Porta de entrada PRIVADA de um banco Postgres em VM para um app em OUTRA VM,
|
|
2288
|
+
// sem mover o banco (Core #727, bug_5c0e7738). O Core prepara o PgBouncer na
|
|
2289
|
+
// VM do banco, testa a partir da VM do app e na própria VM do banco, e só então
|
|
2290
|
+
// grava o novo endereço. `enable` repetido RETOMA a mesma operação.
|
|
2291
|
+
const dbFrontdoorCmd = dbCmd
|
|
2292
|
+
.command('frontdoor')
|
|
2293
|
+
.description('Porta de entrada privada do banco para um app em outra VM da rede interna (sem mover o banco)');
|
|
2294
|
+
dbFrontdoorCmd
|
|
2295
|
+
.command('enable <dbId>')
|
|
2296
|
+
.description('Ligar a porta de entrada para a VM do app (POST /api/cli/v1/db/[id]/frontdoor — exige MFA; repetir retoma)')
|
|
2297
|
+
.requiredOption('--app-server <serverId>', 'VM onde o app roda ou vai rodar (recebe a liberação na rede interna)')
|
|
2298
|
+
.requiredOption('--reason <text>', 'motivo (vai para o registro de auditoria)')
|
|
2299
|
+
.option('--dry-run', 'só confere os requisitos (não altera nada, sem MFA)')
|
|
2300
|
+
.option('-y, --yes', 'pula a confirmação interativa (modo script)')
|
|
2301
|
+
.option('--mfa-token <code>', 'código TOTP step-up MFA (ou use `neetru auth mfa-session`)')
|
|
2302
|
+
.option('--json', 'saída em JSON estruturado')
|
|
2303
|
+
.action(async (dbId, opts) => {
|
|
2304
|
+
const { runDbFrontdoorEnable } = await import('./commands/db-frontdoor.js');
|
|
2305
|
+
await runDbFrontdoorEnable(dbId, {
|
|
2306
|
+
appServer: opts.appServer,
|
|
2307
|
+
reason: opts.reason,
|
|
2308
|
+
dryRun: !!opts.dryRun,
|
|
2309
|
+
yes: !!opts.yes,
|
|
2310
|
+
mfaToken: opts.mfaToken,
|
|
2311
|
+
json: !!opts.json,
|
|
2312
|
+
});
|
|
2313
|
+
});
|
|
2314
|
+
dbFrontdoorCmd
|
|
2315
|
+
.command('status <dbId>')
|
|
2316
|
+
.description('Estado da porta de entrada e da última operação (GET /api/cli/v1/db/[id]/frontdoor)')
|
|
2317
|
+
.option('--json', 'saída em JSON')
|
|
2318
|
+
.action(async (dbId, opts) => {
|
|
2319
|
+
const { runDbFrontdoorStatus } = await import('./commands/db-frontdoor.js');
|
|
2320
|
+
await runDbFrontdoorStatus(dbId, { json: !!opts.json });
|
|
2321
|
+
});
|
|
2322
|
+
dbFrontdoorCmd
|
|
2323
|
+
.command('disable <dbId>')
|
|
2324
|
+
.description('Desligar a porta de entrada — o app volta ao endereço local da VM do banco (POST /api/cli/v1/db/[id]/frontdoor/disable — exige MFA)')
|
|
2325
|
+
.requiredOption('--reason <text>', 'motivo (vai para o registro de auditoria)')
|
|
2326
|
+
.option('--force', 'obrigatório para desligar uma porta ligada (o desfazer normal) ou quando não se sabe onde o app roda: o app volta ao endereço local da VM do banco e, se ele roda em outra VM, fica sem banco')
|
|
2327
|
+
.option('-y, --yes', 'pula a confirmação interativa (modo script)')
|
|
2328
|
+
.option('--mfa-token <code>', 'código TOTP step-up MFA (ou use `neetru auth mfa-session`)')
|
|
2329
|
+
.option('--json', 'saída em JSON')
|
|
2330
|
+
.action(async (dbId, opts) => {
|
|
2331
|
+
const { runDbFrontdoorDisable } = await import('./commands/db-frontdoor.js');
|
|
2332
|
+
await runDbFrontdoorDisable(dbId, {
|
|
2333
|
+
reason: opts.reason,
|
|
2334
|
+
force: !!opts.force,
|
|
2335
|
+
yes: !!opts.yes,
|
|
2336
|
+
mfaToken: opts.mfaToken,
|
|
2337
|
+
json: !!opts.json,
|
|
2338
|
+
});
|
|
2339
|
+
});
|
|
2284
2340
|
// ── neetru db where ───────────────────────────────────────────────────
|
|
2285
2341
|
// READ-ONLY: onde o banco de um produto VIVE hoje (VM/serverId + endpoint).
|
|
2286
2342
|
// Complementa `neetru db status <dbId>` (que exige o dbId) aceitando --product.
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -16,7 +16,14 @@ export declare class CliApiError extends Error {
|
|
|
16
16
|
* por quem faz vários envios em sequência (ex.: `neetru docs sync`).
|
|
17
17
|
*/
|
|
18
18
|
readonly retryAfterSeconds?: number;
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Campos EXTRAS do erro canônico `{ error: { code, message, ...detalhes } }`
|
|
21
|
+
* (ex.: core#732 — `instanceId`, `instanceCreatedAt`, `docCreatedAt`,
|
|
22
|
+
* `outrosServidores`, `comandoConferencia` no force-cleanup). Ausente quando
|
|
23
|
+
* o corpo só traz `code`/`message`. Quem usa valida o tipo de cada campo.
|
|
24
|
+
*/
|
|
25
|
+
readonly details?: Readonly<Record<string, unknown>>;
|
|
26
|
+
constructor(status: number, code: string, message: string, correlationId?: string, retryAfterSeconds?: number, details?: Readonly<Record<string, unknown>>);
|
|
20
27
|
}
|
|
21
28
|
/** `Retry-After` em segundos (aceita número ou data HTTP). undefined se ausente/ilegível. */
|
|
22
29
|
export declare function parseRetryAfter(value: string | null | undefined, now?: number): number | undefined;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -17,13 +17,22 @@ export class CliApiError extends Error {
|
|
|
17
17
|
* por quem faz vários envios em sequência (ex.: `neetru docs sync`).
|
|
18
18
|
*/
|
|
19
19
|
retryAfterSeconds;
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Campos EXTRAS do erro canônico `{ error: { code, message, ...detalhes } }`
|
|
22
|
+
* (ex.: core#732 — `instanceId`, `instanceCreatedAt`, `docCreatedAt`,
|
|
23
|
+
* `outrosServidores`, `comandoConferencia` no force-cleanup). Ausente quando
|
|
24
|
+
* o corpo só traz `code`/`message`. Quem usa valida o tipo de cada campo.
|
|
25
|
+
*/
|
|
26
|
+
details;
|
|
27
|
+
constructor(status, code, message, correlationId, retryAfterSeconds, details) {
|
|
21
28
|
super(message);
|
|
22
29
|
this.name = 'CliApiError';
|
|
23
30
|
this.status = status;
|
|
24
31
|
this.code = code;
|
|
25
32
|
this.correlationId = correlationId;
|
|
26
33
|
this.retryAfterSeconds = retryAfterSeconds;
|
|
34
|
+
if (details && Object.keys(details).length > 0)
|
|
35
|
+
this.details = details;
|
|
27
36
|
}
|
|
28
37
|
}
|
|
29
38
|
/** `Retry-After` em segundos (aceita número ou data HTTP). undefined se ausente/ilegível. */
|
|
@@ -182,6 +191,7 @@ export async function apiRequest(path, opts = {}) {
|
|
|
182
191
|
const errBody = parsed;
|
|
183
192
|
let code = `http_${response.status}`;
|
|
184
193
|
let message = `Erro HTTP ${response.status}`;
|
|
194
|
+
let details;
|
|
185
195
|
if (errBody?.error) {
|
|
186
196
|
if (typeof errBody.error === 'string') {
|
|
187
197
|
// Formato RFC. Auditoria cli/v1 2026-09-29: o 401 do `requireCliAuth`
|
|
@@ -196,6 +206,12 @@ export async function apiRequest(path, opts = {}) {
|
|
|
196
206
|
// Formato interno Neetru
|
|
197
207
|
code = errBody.error.code ?? code;
|
|
198
208
|
message = errBody.error.message ?? message;
|
|
209
|
+
// Campos além de `code`/`message` (core#732): vão em `details`, crus.
|
|
210
|
+
const { code: _c, message: _m, ...extras } = errBody.error;
|
|
211
|
+
void _c;
|
|
212
|
+
void _m;
|
|
213
|
+
if (Object.keys(extras).length > 0)
|
|
214
|
+
details = extras;
|
|
199
215
|
}
|
|
200
216
|
}
|
|
201
217
|
else if (typeof parsed === 'string' && parsed.trim() !== '') {
|
|
@@ -242,7 +258,7 @@ export async function apiRequest(path, opts = {}) {
|
|
|
242
258
|
}
|
|
243
259
|
// LABS #6 (2026-09-28): captura o x-correlation-id do Core p/ o rodapé do erro.
|
|
244
260
|
const correlationId = response.headers.get('x-correlation-id') ?? undefined;
|
|
245
|
-
throw new CliApiError(response.status, code, message, correlationId, parseRetryAfter(response.headers.get('retry-after')));
|
|
261
|
+
throw new CliApiError(response.status, code, message, correlationId, parseRetryAfter(response.headers.get('retry-after')), details);
|
|
246
262
|
}
|
|
247
263
|
return parsed;
|
|
248
264
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `inquirer` sob demanda (P115, 2026-10-18; o `db.ts` já fazia o mesmo desde
|
|
3
|
+
* o bug_b1e9af59, e o `init.ts` desde o P112).
|
|
4
|
+
*
|
|
5
|
+
* O `inquirer` é o pacote mais caro de carregar a frio no grafo da CLI: ~0,31 s
|
|
6
|
+
* sozinho e ~1 s com a máquina carregada (medido no P115). Quem o importa no
|
|
7
|
+
* topo faz TODO caminho do comando pagar a carga — inclusive `--json`,
|
|
8
|
+
* `--yes` e os não interativos, que nunca perguntam nada. Os comandos carregam
|
|
9
|
+
* o inquirer por aqui, só no ponto em que vão perguntar.
|
|
10
|
+
*
|
|
11
|
+
* A guarda `guarda-inquirer-sob-demanda.test.ts` impede import estático novo
|
|
12
|
+
* do inquirer em `src/`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function carregarInquirer(): Promise<typeof import('inquirer').default>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `inquirer` sob demanda (P115, 2026-10-18; o `db.ts` já fazia o mesmo desde
|
|
3
|
+
* o bug_b1e9af59, e o `init.ts` desde o P112).
|
|
4
|
+
*
|
|
5
|
+
* O `inquirer` é o pacote mais caro de carregar a frio no grafo da CLI: ~0,31 s
|
|
6
|
+
* sozinho e ~1 s com a máquina carregada (medido no P115). Quem o importa no
|
|
7
|
+
* topo faz TODO caminho do comando pagar a carga — inclusive `--json`,
|
|
8
|
+
* `--yes` e os não interativos, que nunca perguntam nada. Os comandos carregam
|
|
9
|
+
* o inquirer por aqui, só no ponto em que vão perguntar.
|
|
10
|
+
*
|
|
11
|
+
* A guarda `guarda-inquirer-sob-demanda.test.ts` impede import estático novo
|
|
12
|
+
* do inquirer em `src/`.
|
|
13
|
+
*/
|
|
14
|
+
export async function carregarInquirer() {
|
|
15
|
+
const { default: inquirer } = await import('inquirer');
|
|
16
|
+
return inquirer;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=inquirer-sob-demanda.js.map
|
package/dist/lib/pickers.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import
|
|
2
|
+
import { carregarInquirer } from './inquirer-sob-demanda.js';
|
|
3
3
|
import { apiRequest } from './api-client.js';
|
|
4
4
|
import { handleApiError } from './api-error.js';
|
|
5
5
|
import { adviseCommand, declaredInputs } from '../cli-kit/command-context.js';
|
|
@@ -76,6 +76,8 @@ export async function resolveOrPick(opts) {
|
|
|
76
76
|
: `Múltiplos candidatos encontrados, e este comando não recebe ${sujeito} por argumento: ` +
|
|
77
77
|
`rode num terminal interativo para escolher.\nDisponiveis:\n${available}`, opts.json);
|
|
78
78
|
}
|
|
79
|
+
// Só aqui o picker pergunta: o inquirer é carregado agora (P115).
|
|
80
|
+
const inquirer = await carregarInquirer();
|
|
79
81
|
const choices = items.map((item) => ({
|
|
80
82
|
name: opts.format(item),
|
|
81
83
|
value: opts.getId(item),
|
|
@@ -107,7 +109,7 @@ export async function promptText(message, opts = {}) {
|
|
|
107
109
|
return '';
|
|
108
110
|
failInput(`Informe ${message}`, opts.json);
|
|
109
111
|
}
|
|
110
|
-
const answer = await
|
|
112
|
+
const answer = await (await carregarInquirer()).prompt([
|
|
111
113
|
{
|
|
112
114
|
type: 'input',
|
|
113
115
|
name: 'value',
|
|
@@ -131,7 +133,7 @@ export async function promptChoice(message, choices, opts = {}) {
|
|
|
131
133
|
return opts.default;
|
|
132
134
|
failInput(`Escolha ${message}`, opts.json);
|
|
133
135
|
}
|
|
134
|
-
const answer = await
|
|
136
|
+
const answer = await (await carregarInquirer()).prompt([
|
|
135
137
|
{
|
|
136
138
|
type: 'list',
|
|
137
139
|
name: 'picked',
|
package/package.json
CHANGED