@neetru/cli 2.28.7 → 2.28.10

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 (108) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +25 -1
  3. package/dist/commands/admin/oidc-login.d.ts +18 -0
  4. package/dist/commands/admin/oidc-login.js +68 -3
  5. package/dist/commands/admin/oidc-login.js.map +1 -1
  6. package/dist/commands/admin/sdk-credential.js +11 -6
  7. package/dist/commands/admin/sdk-credential.js.map +1 -1
  8. package/dist/commands/agent-release.d.ts +2 -0
  9. package/dist/commands/agent-release.js +41 -29
  10. package/dist/commands/agent-release.js.map +1 -1
  11. package/dist/commands/agent-write.d.ts +1 -0
  12. package/dist/commands/agent-write.js +4 -0
  13. package/dist/commands/agent-write.js.map +1 -1
  14. package/dist/commands/ai-models.js +9 -4
  15. package/dist/commands/ai-models.js.map +1 -1
  16. package/dist/commands/archive.js +9 -2
  17. package/dist/commands/archive.js.map +1 -1
  18. package/dist/commands/auth.js +17 -2
  19. package/dist/commands/auth.js.map +1 -1
  20. package/dist/commands/autocomplete.d.ts +1 -1
  21. package/dist/commands/autocomplete.js +2 -0
  22. package/dist/commands/autocomplete.js.map +1 -1
  23. package/dist/commands/chat-menu.js +20 -6
  24. package/dist/commands/chat-menu.js.map +1 -1
  25. package/dist/commands/chat.d.ts +14 -0
  26. package/dist/commands/chat.js +32 -4
  27. package/dist/commands/chat.js.map +1 -1
  28. package/dist/commands/config.js +7 -0
  29. package/dist/commands/config.js.map +1 -1
  30. package/dist/commands/db-move.d.ts +204 -0
  31. package/dist/commands/db-move.js +890 -0
  32. package/dist/commands/db-move.js.map +1 -0
  33. package/dist/commands/db.d.ts +14 -3
  34. package/dist/commands/db.js +102 -67
  35. package/dist/commands/db.js.map +1 -1
  36. package/dist/commands/deploy.d.ts +15 -0
  37. package/dist/commands/deploy.js +42 -1
  38. package/dist/commands/deploy.js.map +1 -1
  39. package/dist/commands/dev-up.d.ts +5 -0
  40. package/dist/commands/dev-up.js +29 -4
  41. package/dist/commands/dev-up.js.map +1 -1
  42. package/dist/commands/doctor.js +12 -6
  43. package/dist/commands/doctor.js.map +1 -1
  44. package/dist/commands/domains.js +48 -36
  45. package/dist/commands/domains.js.map +1 -1
  46. package/dist/commands/init.d.ts +0 -8
  47. package/dist/commands/init.js +14 -3
  48. package/dist/commands/init.js.map +1 -1
  49. package/dist/commands/logs.js +18 -21
  50. package/dist/commands/logs.js.map +1 -1
  51. package/dist/commands/mobile.d.ts +26 -0
  52. package/dist/commands/mobile.js +344 -0
  53. package/dist/commands/mobile.js.map +1 -0
  54. package/dist/commands/ops.d.ts +20 -0
  55. package/dist/commands/ops.js +70 -17
  56. package/dist/commands/ops.js.map +1 -1
  57. package/dist/commands/products-db.d.ts +1 -0
  58. package/dist/commands/products-db.js +15 -15
  59. package/dist/commands/products-db.js.map +1 -1
  60. package/dist/commands/promote.js +13 -32
  61. package/dist/commands/promote.js.map +1 -1
  62. package/dist/commands/schema.d.ts +4 -0
  63. package/dist/commands/schema.js +25 -16
  64. package/dist/commands/schema.js.map +1 -1
  65. package/dist/commands/servers.d.ts +2 -3
  66. package/dist/commands/servers.js +4 -7
  67. package/dist/commands/servers.js.map +1 -1
  68. package/dist/commands/smoke.d.ts +123 -0
  69. package/dist/commands/smoke.js +529 -0
  70. package/dist/commands/smoke.js.map +1 -0
  71. package/dist/commands/vm-memory.d.ts +53 -0
  72. package/dist/commands/vm-memory.js +307 -0
  73. package/dist/commands/vm-memory.js.map +1 -0
  74. package/dist/commands/vm.d.ts +2 -0
  75. package/dist/commands/vm.js +8 -0
  76. package/dist/commands/vm.js.map +1 -1
  77. package/dist/commands/webhooks.d.ts +2 -0
  78. package/dist/commands/webhooks.js +7 -0
  79. package/dist/commands/webhooks.js.map +1 -1
  80. package/dist/index.js +403 -21
  81. package/dist/index.js.map +1 -1
  82. package/dist/lib/agent-artifact-url.d.ts +19 -0
  83. package/dist/lib/agent-artifact-url.js +48 -0
  84. package/dist/lib/agent-artifact-url.js.map +1 -0
  85. package/dist/lib/api-client.d.ts +11 -3
  86. package/dist/lib/api-client.js +55 -24
  87. package/dist/lib/api-client.js.map +1 -1
  88. package/dist/lib/api-error.js +32 -1
  89. package/dist/lib/api-error.js.map +1 -1
  90. package/dist/lib/auth.d.ts +2 -9
  91. package/dist/lib/auth.js +34 -18
  92. package/dist/lib/auth.js.map +1 -1
  93. package/dist/lib/cli-write.d.ts +31 -0
  94. package/dist/lib/cli-write.js +51 -2
  95. package/dist/lib/cli-write.js.map +1 -1
  96. package/dist/lib/mobile-local/index.d.ts +7 -0
  97. package/dist/lib/mobile-local/index.js +7 -0
  98. package/dist/lib/mobile-local/index.js.map +1 -0
  99. package/dist/lib/mobile-local/mobile-json.d.ts +57 -0
  100. package/dist/lib/mobile-local/mobile-json.js +169 -0
  101. package/dist/lib/mobile-local/mobile-json.js.map +1 -0
  102. package/dist/lib/product-config.d.ts +5 -0
  103. package/dist/lib/product-config.js +37 -0
  104. package/dist/lib/product-config.js.map +1 -0
  105. package/dist/lib/telemetry.d.ts +7 -0
  106. package/dist/lib/telemetry.js +9 -0
  107. package/dist/lib/telemetry.js.map +1 -1
  108. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from 'commander';
2
+ import { Command, Option } from 'commander';
3
3
  import { log } from './utils/logger.js';
4
4
  import { CLI_VERSION } from './version.js';
5
5
  import { installTelemetryFlushHook, markCommandStart, markCommandEnd, } from './lib/telemetry.js';
6
6
  import { safeExit, isSafeExitHalt } from './utils/safe-exit.js';
7
+ import { CliApiError } from './lib/api-client.js';
7
8
  // UV_HANDLE_CLOSING no Windows (bug_818cf6d): o double-destroy de stdin (handler
8
9
  // 'exit' redundante) foi removido em 2026-07-15 (ddd479f), MAS isso não bastou
9
10
  // — a causa dominante é o `uv_async_t` do undici (fetch) ainda vivo quando o
@@ -30,6 +31,11 @@ function handleFatalSignal(err) {
30
31
  return; // saída já agendada dentro do safeExit
31
32
  try {
32
33
  log.error(err instanceof Error ? (err.stack ?? err.message) : String(err));
34
+ // LABS #6 (2026-09-28): se o erro carrega o x-correlation-id do Core, mostra
35
+ // no rodapé pra o usuário informar ao suporte — fecha o loop de debug de campo.
36
+ if (err instanceof CliApiError && err.correlationId) {
37
+ log.error(`ref: ${err.correlationId} — informe ao suporte`);
38
+ }
33
39
  }
34
40
  catch {
35
41
  /* logger indisponível — segue direto pro exit deferido */
@@ -149,7 +155,8 @@ program
149
155
  .option('--here', 'init no cwd atual (modo merge — preserva existentes)')
150
156
  .option('--path <path>', 'custom path target')
151
157
  .option('--force', 'sobrescreve arquivos existentes em conflito (cuidado!)')
152
- .option('--yes', 'modo non-interactive (assume defaults, sai com erro se conflito sem flag)')
158
+ // -y padronizado com bootstrap (`-y, --yes`) — muscle-memory (auditoria UX 2026-09).
159
+ .option('-y, --yes', 'modo non-interactive (assume defaults, sai com erro se conflito sem flag)')
153
160
  .action(async (name, opts) => {
154
161
  const { runInit } = await import('./commands/init.js');
155
162
  await runInit({
@@ -235,11 +242,16 @@ authCmd
235
242
  .command('mfa-session')
236
243
  .description('Abre uma sessão MFA de 1h — troca um código TOTP fresco por um token de sessão ' +
237
244
  'salvo localmente; enquanto ativa, comandos destrutivos não re-promptam MFA.')
238
- .option('--code <totp>', 'código TOTP de 6 dígitos (pula prompt interativo)')
245
+ // Flag TOTP canônica em todo o CLI: `--mfa-token`. `--code` e `--mfa` seguem
246
+ // aceitos como aliases OCULTOS (retrocompat de scripts) — auditoria UX 2026-09
247
+ // (o mesmo código TOTP tinha 3 nomes: --code aqui, --mfa no db, --mfa-token no resto).
248
+ .option('--mfa-token <totp>', 'código TOTP de 6 dígitos (pula prompt interativo)')
249
+ .addOption(new Option('--code <totp>').hideHelp())
250
+ .addOption(new Option('--mfa <totp>').hideHelp())
239
251
  .option('--json', 'saída em JSON')
240
252
  .action(async (opts) => {
241
253
  const { runAuthMfaSession } = await import('./commands/auth.js');
242
- await runAuthMfaSession({ code: opts.code, json: !!opts.json });
254
+ await runAuthMfaSession({ code: opts.mfaToken ?? opts.code ?? opts.mfa, json: !!opts.json });
243
255
  });
244
256
  // ── neetru config ─────────────────────────────────────────────────────
245
257
  const configCmd = program
@@ -432,6 +444,7 @@ program
432
444
  .option('--build-local', 'forçar docker build local em vez de Cloud Build remoto (exige Docker instalado). (target=cloud-run)')
433
445
  .option('--env-file <file>', 'arquivo .env com vars passadas pro deploy')
434
446
  .option('--env <env>', 'ambiente alvo: dev | staging | prod', 'dev')
447
+ .option('--mfa-token <code>', 'código TOTP para step-up MFA (só --env prod; ou use `neetru auth mfa-session`)')
435
448
  .option('--local-artifact', 'F-11: NÃO faz upload pra GCS — manda file:// pro Core (só funciona se o agent compartilha FS com este CLI)')
436
449
  .option('--non-interactive', 'falha em vez de perguntar (modo CI)')
437
450
  .option('--force', 'ignora o gate de schema e prossegue mesmo com divergência de fingerprint')
@@ -460,6 +473,32 @@ program
460
473
  nonInteractive: !!opts.nonInteractive,
461
474
  force: !!opts.force,
462
475
  skipHealthProbe: !!opts.skipHealthProbe,
476
+ mfaToken: opts.mfaToken,
477
+ json: !!opts.json,
478
+ });
479
+ });
480
+ // ── neetru smoke ──────────────────────────────────────────────────────
481
+ // Percorre as páginas do produto LOCAL (antes do deploy) e verifica alcance/
482
+ // login/DB, mais um smoke básico de prod opcional. Abordagem LEVE: só HTTP GET
483
+ // (fetch nativo) — SEM Playwright/headless browser (o CLI publicado no npm não
484
+ // pode ganhar uma dep de ~300MB). Captura de erro de console fica pra um v2.
485
+ program
486
+ .command('smoke')
487
+ .description('Percorre as páginas do produto LOCAL antes do deploy (alcance + login + DB) e um smoke de prod opcional')
488
+ .option('--base-url <url>', 'servidor dev LOCAL a percorrer', 'http://localhost:3000')
489
+ .option('--prod <url>', 'também roda um smoke básico de produção (GET / e health, espera 200)')
490
+ .option('--health-path <path>', 'endpoint de saúde/DB', '/api/health')
491
+ .option('--routes <csv>', 'rotas explícitas separadas por vírgula (sobrescreve a auto-descoberta)')
492
+ .option('--timeout <ms>', 'timeout por request em ms', (v) => Number.parseInt(v, 10), 10000)
493
+ .option('--json', 'saída em JSON de uma linha (machine-readable)')
494
+ .action(async (opts) => {
495
+ const { runSmoke } = await import('./commands/smoke.js');
496
+ await runSmoke({
497
+ baseUrl: opts.baseUrl,
498
+ prod: opts.prod,
499
+ healthPath: opts.healthPath,
500
+ routes: opts.routes,
501
+ timeout: opts.timeout,
463
502
  json: !!opts.json,
464
503
  });
465
504
  });
@@ -678,10 +717,10 @@ serversCmd
678
717
  .option('--zone <zone>', 'zona GCP (ex: us-central1-a)')
679
718
  .option('--machine-type <type>', 'machine type GCP (ex: e2-small)')
680
719
  .option('--role <role>', 'multi (default) | db | app | worker — role da VM (nome/metadado)')
681
- .option('--is-db-vm', 'postura de rede de VM de banco: sem IP público + tag neetru-db-vm (exige step-up MFA)')
720
+ .option('--is-db-vm', 'postura de rede de VM de banco: sem IP público + tag neetru-db-vm')
682
721
  .option('--tenant <tenantId>', 'tenant associado')
683
722
  .option('--customer <customerId>', 'customer associado')
684
- .option('--mfa-token <code>', 'código TOTP para step-up MFA (só usado com --is-db-vm)')
723
+ .option('--mfa-token <code>', 'código TOTP para step-up MFA (ou use `neetru auth mfa-session`)')
685
724
  .option('--json', 'saída em JSON')
686
725
  .action(async (opts) => {
687
726
  const { runServersProvision } = await import('./commands/servers.js');
@@ -1165,6 +1204,43 @@ productsCmd
1165
1204
  const { runProductsUnpublish } = await import('./commands/products.js');
1166
1205
  await runProductsUnpublish(slug, { json: !!opts.json });
1167
1206
  });
1207
+ // Alias amigável do controle de memória por inquilino (design §3.7): resolve a
1208
+ // VM do slug via --server/pickServer e delega ao mesmo core de `vm memory set`.
1209
+ const productsMemoryCmd = productsCmd
1210
+ .command('memory')
1211
+ .description('Controle de memória (RAM) do produto na VM (alias de `vm memory`)');
1212
+ productsMemoryCmd
1213
+ .command('set <slug>')
1214
+ .description('Define o teto de memória do produto na VM (guardrail de soma + step-up MFA)')
1215
+ .option('--memory-max <mb>', 'teto systemd MemoryMax em MB (cgroup)')
1216
+ .option('--max <mb>', 'alias de --memory-max')
1217
+ .option('--max-old-space <mb>', 'NODE_OPTIONS --max-old-space-size em MB (heap V8)')
1218
+ .option('--high <mb>', 'MemoryHigh em MB (throttle suave antes do OOM)')
1219
+ .option('--restart', 'força restart do app após aplicar')
1220
+ .option('--confirm-shrink', 'confirma reduzir abaixo do uso vivo (pode OOM o alvo)')
1221
+ .option('--server <id>', 'VM alvo (default: resolve a VM compartilhada)')
1222
+ .option('--reason <text>', 'motivo (paper-trail no audit do Core)')
1223
+ .option('--mfa <code>', 'código TOTP para step-up MFA')
1224
+ .option('--mfa-token <code>', 'alias de --mfa')
1225
+ .option('-y, --yes', 'pula a confirmação interativa (em --json exige --mfa)')
1226
+ .option('--json', 'saída em JSON')
1227
+ .action(async (slug, opts) => {
1228
+ const { runVmMemorySet } = await import('./commands/vm-memory.js');
1229
+ await runVmMemorySet(slug, {
1230
+ memoryMax: opts.memoryMax,
1231
+ max: opts.max,
1232
+ maxOldSpace: opts.maxOldSpace,
1233
+ high: opts.high,
1234
+ restart: !!opts.restart,
1235
+ confirmShrink: !!opts.confirmShrink,
1236
+ server: opts.server,
1237
+ reason: opts.reason,
1238
+ mfa: opts.mfa,
1239
+ mfaToken: opts.mfaToken,
1240
+ yes: !!opts.yes,
1241
+ json: !!opts.json,
1242
+ });
1243
+ });
1168
1244
  // ── neetru admin database (Phase A — plano de controle staff de bancos) ───────
1169
1245
  // D-1: renomeado de `neetru products db` → `neetru admin database`.
1170
1246
  // Alias `neetru products db` emite aviso de remoção.
@@ -1245,10 +1321,15 @@ adminDatabaseCmd
1245
1321
  .command('set-status [id] [status]')
1246
1322
  .description('[Staff] ESCRITA — forçar status do ciclo de vida de um banco (active/failed/archived/…)')
1247
1323
  .option('--reason <text>', 'motivo da mudança de status')
1324
+ .option('--mfa-token <code>', 'código TOTP para step-up MFA (exigido se o banco está active/degraded)')
1248
1325
  .option('--json')
1249
1326
  .action(async (id, status, opts) => {
1250
1327
  const { runDbSetStatus } = await import('./commands/products-db.js');
1251
- await runDbSetStatus(id, status, { reason: opts.reason, json: !!opts.json });
1328
+ await runDbSetStatus(id, status, {
1329
+ reason: opts.reason,
1330
+ mfaToken: opts.mfaToken,
1331
+ json: !!opts.json,
1332
+ });
1252
1333
  });
1253
1334
  adminDatabaseCmd
1254
1335
  .command('retry [id]')
@@ -1277,15 +1358,32 @@ adminDatabaseCmd
1277
1358
  await runDbDelete(id, { json: !!opts.json, force: !!opts.force });
1278
1359
  });
1279
1360
  // ── neetru admin sdk-credential ────────────────────────────────────────────
1280
- // Gerencia credenciais OIDC (SDK) por produto — create / list / revoke.
1361
+ // Gerencia credenciais OIDC de MÁQUINA (M2M / client_credentials) por produto —
1362
+ // create / list / revoke. É credencial de SERVIDOR (backend do produto ↔ API do
1363
+ // Neetru via @neetru/sdk), NÃO login de usuário no browser (isso é `oidc-login`).
1281
1364
  // Resolve bug_599d2f033e024bc194aa07639cd26c84: bootstrap CI/IaC desbloqueado.
1282
1365
  // Internamente usa `oidc_clients/{clientId}` no Core (canônico pós-devkit).
1283
1366
  const adminSdkCredentialCmd = adminCmd
1284
1367
  .command('sdk-credential')
1285
- .description('Gerenciar credenciais OIDC (SDK) de produtos — create / list / revoke');
1368
+ .description('Credenciais OIDC de MÁQUINA (M2M / client_credentials) de produtos — create / list / revoke. ' +
1369
+ 'Para LOGIN de usuário no browser use `neetru admin oidc-login`.');
1286
1370
  adminSdkCredentialCmd
1287
1371
  .command('create')
1288
- .description('[Staff] Criar credencial OIDC para produto — exibe clientSecret ONE-TIME')
1372
+ .description('[Staff] Criar credencial de MÁQUINA (M2M / client_credentials) p/ servidor-a-servidor do ' +
1373
+ 'produto — exibe clientSecret ONE-TIME. NÃO serve p/ login de usuário no browser ' +
1374
+ '(use `neetru admin oidc-login create`).')
1375
+ .addHelpText('after', [
1376
+ '',
1377
+ 'Qual comando usar:',
1378
+ ' • sdk-credential create → credencial de SERVIDOR/máquina (M2M, grant client_credentials).',
1379
+ ' Backend do produto falando com a API do Neetru via @neetru/sdk. SEM navegador, SEM usuário.',
1380
+ ' • oidc-login create → LOGIN de usuário final no browser (grant authorization_code).',
1381
+ ' É o que autentica a pessoa que abre o produto. Use ESTE p/ resolver "o login quebrou".',
1382
+ '',
1383
+ 'Incidente gestovendas (2026-09): credenciais de máquina foram criadas por engano tentando',
1384
+ 'consertar o login → ficaram mortas (nunca emitiram token) e o login continuou quebrado.',
1385
+ '',
1386
+ ].join('\n'))
1289
1387
  .requiredOption('--product-id <id>', 'ID do produto')
1290
1388
  .requiredOption('--name <label>', 'nome/label da credencial (ex: "Backend Prod")')
1291
1389
  .option('--scopes <escopos>', 'escopos separados por vírgula (ex: "entitlements.read,usage.write")')
@@ -1369,6 +1467,23 @@ adminOidcLoginCmd
1369
1467
  json: !!opts.json,
1370
1468
  });
1371
1469
  });
1470
+ adminOidcLoginCmd
1471
+ .command('rotate <clientId>')
1472
+ .description('[Staff] Rotacionar o client_secret de um login-client OIDC (exige MFA). ' +
1473
+ 'O secret novo NÃO é exibido — o próximo deploy o injeta server-side.')
1474
+ .requiredOption('--product-id <id>', 'ID (ou slug) do produto dono do client')
1475
+ .option('--immediate', 'revoga o secret anterior NA HORA (vazamento) — o login do produto cai até o próximo deploy')
1476
+ .option('--mfa-token <code>', 'código TOTP para step-up MFA (ou use `neetru auth mfa-session`)')
1477
+ .option('--json', 'saída em JSON')
1478
+ .action(async (clientId, opts) => {
1479
+ const { runOidcLoginRotate } = await import('./commands/admin/oidc-login.js');
1480
+ await runOidcLoginRotate(clientId, {
1481
+ productId: opts.productId,
1482
+ immediate: !!opts.immediate,
1483
+ mfaToken: opts.mfaToken,
1484
+ json: !!opts.json,
1485
+ });
1486
+ });
1372
1487
  adminOidcLoginCmd
1373
1488
  .command('list')
1374
1489
  .description('[Staff] Listar login-clients OIDC de um produto (mascarado)')
@@ -1532,12 +1647,64 @@ vmCmd
1532
1647
  .option('--machine-type <type>', 'machine type GCP (default e2-medium)')
1533
1648
  .option('--zone <zone>', 'zona GCP (default southamerica-east1-a)')
1534
1649
  .option('-y, --yes', 'confirma o custo recorrente sem prompt')
1650
+ .option('--mfa-token <code>', 'código TOTP para step-up MFA (ou use `neetru auth mfa-session`)')
1535
1651
  .action(async (opts) => {
1536
1652
  const { runVmProvision } = await import('./commands/vm.js');
1537
1653
  await runVmProvision({
1538
1654
  machineType: opts.machineType,
1539
1655
  zone: opts.zone,
1540
1656
  yes: !!opts.yes,
1657
+ mfaToken: opts.mfaToken,
1658
+ });
1659
+ });
1660
+ // ── neetru vm memory ──────────────────────────────────────────────────
1661
+ // Controle gerenciado de memória (RAM) por inquilino na VM multi-tenant.
1662
+ // Origem: incidente OOM 2026-09-07 (PDV caiu; Suporte ajustou systemd à mão).
1663
+ // GUARDRAIL: a SOMA dos tetos + reserva ≤ RAM×0.90 — validado fail-closed no
1664
+ // Core; superfície sensível de infra → step-up MFA.
1665
+ const vmMemoryCmd = vmCmd
1666
+ .command('memory')
1667
+ .description('Controle de memória (RAM) por inquilino na VM');
1668
+ vmMemoryCmd
1669
+ .command('status [serverId]')
1670
+ .alias('list')
1671
+ .description('Lista os inquilinos da VM: teto, uso vivo, folga + reserva/livre/RAM total')
1672
+ .option('--server <id>', 'VM alvo (alternativa ao argumento posicional)')
1673
+ .option('--json', 'saída em JSON')
1674
+ .action(async (serverId, opts) => {
1675
+ const { runVmMemoryStatus } = await import('./commands/vm-memory.js');
1676
+ await runVmMemoryStatus(serverId, { server: opts.server, json: !!opts.json });
1677
+ });
1678
+ vmMemoryCmd
1679
+ .command('set <slug>')
1680
+ .description('Define o teto de memória de um inquilino (guardrail de soma + step-up MFA)')
1681
+ .option('--memory-max <mb>', 'teto systemd MemoryMax em MB (cgroup)')
1682
+ .option('--max <mb>', 'alias de --memory-max')
1683
+ .option('--max-old-space <mb>', 'NODE_OPTIONS --max-old-space-size em MB (heap V8)')
1684
+ .option('--high <mb>', 'MemoryHigh em MB (throttle suave antes do OOM)')
1685
+ .option('--restart', 'força restart do app após aplicar (default: só se max-old-space muda)')
1686
+ .option('--confirm-shrink', 'confirma reduzir abaixo do uso vivo (pode OOM o alvo)')
1687
+ .option('--server <id>', 'VM alvo (default: resolve a VM compartilhada)')
1688
+ .option('--reason <text>', 'motivo (paper-trail no audit do Core)')
1689
+ .option('--mfa <code>', 'código TOTP para step-up MFA (ou use `neetru auth mfa-session`)')
1690
+ .option('--mfa-token <code>', 'alias de --mfa')
1691
+ .option('-y, --yes', 'pula a confirmação interativa (em --json exige --mfa)')
1692
+ .option('--json', 'saída em JSON')
1693
+ .action(async (slug, opts) => {
1694
+ const { runVmMemorySet } = await import('./commands/vm-memory.js');
1695
+ await runVmMemorySet(slug, {
1696
+ memoryMax: opts.memoryMax,
1697
+ max: opts.max,
1698
+ maxOldSpace: opts.maxOldSpace,
1699
+ high: opts.high,
1700
+ restart: !!opts.restart,
1701
+ confirmShrink: !!opts.confirmShrink,
1702
+ server: opts.server,
1703
+ reason: opts.reason,
1704
+ mfa: opts.mfa,
1705
+ mfaToken: opts.mfaToken,
1706
+ yes: !!opts.yes,
1707
+ json: !!opts.json,
1541
1708
  });
1542
1709
  });
1543
1710
  // ── neetru validate ───────────────────────────────────────────────────
@@ -1846,11 +2013,16 @@ dbMigrationsCmd
1846
2013
  dbMigrationsCmd
1847
2014
  .command('confirm <migrationId>')
1848
2015
  .description('Confirmar migração destrutiva (POST /api/cli/v1/db/migrations/[id]/confirm — exige MFA)')
1849
- .requiredOption('--mfa <token>', 'código TOTP step-up MFA')
2016
+ // Era `requiredOption('--mfa')` — o commander rejeitava ANTES de rodar, matando
2017
+ // o caminho de sessão MFA de 1h (`neetru auth mfa-session`) que o resolveStepUp
2018
+ // promete honrar. Agora `option`: resolveStepUp faz o fail-closed (sessão → --mfa-token
2019
+ // → erro acionável). Flag canônica `--mfa-token`; `--mfa` fica como alias oculto. (auditoria UX 2026-09)
2020
+ .option('--mfa-token <token>', 'código TOTP step-up MFA (ou use `neetru auth mfa-session`)')
2021
+ .addOption(new Option('--mfa <token>').hideHelp())
1850
2022
  .option('--json', 'saída em JSON')
1851
2023
  .action(async (migrationId, opts) => {
1852
2024
  const { runDbMigrationsConfirm } = await import('./commands/db.js');
1853
- await runDbMigrationsConfirm(migrationId, { mfa: opts.mfa, json: !!opts.json });
2025
+ await runDbMigrationsConfirm(migrationId, { mfa: opts.mfaToken ?? opts.mfa, json: !!opts.json });
1854
2026
  });
1855
2027
  // bug_728fa11c fix: cancela migração presa em awaiting_confirmation ou pending.
1856
2028
  dbMigrationsCmd
@@ -1876,14 +2048,16 @@ dbMigrationsCmd
1876
2048
  dbMigrationsCmd
1877
2049
  .command('resolve <migrationId>')
1878
2050
  .description('Destravar migração pending/partially_applied/failed redundante (POST /api/cli/v1/db/migrations/[id]/resolve)')
1879
- .option('--force', 'atestar manualmente sem prova de redundância por hash (exige --mfa ou sessão MFA)')
1880
- .option('--mfa <token>', 'código TOTP step-up MFA (necessário com --force)')
2051
+ .option('--force', 'atestar manualmente sem prova de redundância por hash (exige --mfa-token ou sessão MFA)')
2052
+ // Flag canônica `--mfa-token`; `--mfa` como alias oculto (retrocompat). (auditoria UX 2026-09)
2053
+ .option('--mfa-token <token>', 'código TOTP step-up MFA (necessário com --force; ou use `neetru auth mfa-session`)')
2054
+ .addOption(new Option('--mfa <token>').hideHelp())
1881
2055
  .option('--json', 'saída em JSON')
1882
2056
  .action(async (migrationId, opts) => {
1883
2057
  const { runDbMigrationsResolve } = await import('./commands/db.js');
1884
2058
  await runDbMigrationsResolve(migrationId, {
1885
2059
  force: !!opts.force,
1886
- mfa: opts.mfa,
2060
+ mfa: opts.mfaToken ?? opts.mfa,
1887
2061
  json: !!opts.json,
1888
2062
  });
1889
2063
  });
@@ -1901,11 +2075,139 @@ dbCmd
1901
2075
  dbCmd
1902
2076
  .command('restore <dbId>')
1903
2077
  .description('Restaurar banco a partir de backup (POST /api/cli/v1/db/[id]/restore — exige MFA)')
1904
- .requiredOption('--mfa <token>', 'código TOTP step-up MFA (obrigatório — operação destrutiva)')
2078
+ // Automação (auditoria UX 2026-09): --backup-id/--yes tornam o restore scriptável;
2079
+ // em --json ambos são obrigatórios (fail-closed, nunca trava no inquirer).
2080
+ .option('--backup-id <id>', 'ID do backup a restaurar (pula o prompt; obrigatório em --json)')
2081
+ .option('-y, --yes', 'pula a confirmação textual "RESTAURAR" (obrigatório em --json)')
2082
+ // Flag canônica `--mfa-token`; `--mfa` como alias oculto (retrocompat).
2083
+ .option('--mfa-token <token>', 'código TOTP step-up MFA (obrigatório — operação destrutiva)')
2084
+ .addOption(new Option('--mfa <token>').hideHelp())
1905
2085
  .option('--json', 'saída em JSON')
1906
2086
  .action(async (dbId, opts) => {
1907
2087
  const { runDbRestore } = await import('./commands/db.js');
1908
- await runDbRestore(dbId, { mfa: opts.mfa, json: !!opts.json });
2088
+ await runDbRestore(dbId, {
2089
+ mfa: opts.mfaToken ?? opts.mfa,
2090
+ backupId: opts.backupId,
2091
+ yes: !!opts.yes,
2092
+ json: !!opts.json,
2093
+ });
2094
+ });
2095
+ // ── neetru db move ────────────────────────────────────────────────────
2096
+ // Relocação ATÔMICA de um banco de produto entre VMs (Fase 2 / PR #423).
2097
+ // GRUPO com subcomando default `start` (mesmo padrão de `neetru promote`):
2098
+ // `neetru db move --product <slug> --to <serverId>` cai em `start`.
2099
+ // Só reporta SUCESSO quando o Core confirma o cutover VERIFICADO (state `done`);
2100
+ // rollback é reportado explicitamente como "produto seguiu na origem".
2101
+ const dbMoveCmd = dbCmd
2102
+ .command('move')
2103
+ .description('Relocar (mover) o banco de um produto entre VMs — atômico, com rollback e prova de dados');
2104
+ dbMoveCmd
2105
+ .command('start', { isDefault: true })
2106
+ .description('Iniciar a relocação de um banco (POST /api/cli/v1/db/[id]/relocate — destrutivo, exige MFA)')
2107
+ .option('--product <slug>', 'produto cujo banco será movido (slug ou id)')
2108
+ .option('--db <dbId>', 'dbId explícito (pdb_…) — dispensa a resolução por produto')
2109
+ .option('--env <env>', 'ambiente do banco, para desambiguar quando o produto tem mais de um')
2110
+ .option('--to <serverId>', 'VM destino (obrigatório)')
2111
+ .option('--reason <text>', 'motivo (obrigatório fora de --dry-run; vai pro audit)')
2112
+ .option('--strategy <s>', 'snapshot_quiesce (padrão) | logical_stream', 'snapshot_quiesce')
2113
+ .option('--manual-cutover', 'pausa em verifying_target aguardando `db move confirm`')
2114
+ .option('--max-quiesce-ms <n>', 'teto do congelamento da origem (padrão 15000, máx 60000)')
2115
+ .option('--dry-run', 'só roda o preflight (não provisiona nada, sem MFA)')
2116
+ .option('--no-watch', 'não acompanha a máquina de estados (só dispara e imprime o migrationId)')
2117
+ .option('-y, --yes', 'pula a confirmação interativa (modo script)')
2118
+ .option('--mfa-token <code>', 'código TOTP step-up MFA (ou use `neetru auth mfa-session`)')
2119
+ .option('--json', 'saída em JSON estruturado')
2120
+ .action(async (opts) => {
2121
+ const { runDbMoveStart } = await import('./commands/db-move.js');
2122
+ await runDbMoveStart({
2123
+ product: opts.product,
2124
+ db: opts.db,
2125
+ env: opts.env,
2126
+ to: opts.to,
2127
+ reason: opts.reason,
2128
+ strategy: opts.strategy,
2129
+ manualCutover: !!opts.manualCutover,
2130
+ maxQuiesceMs: opts.maxQuiesceMs,
2131
+ dryRun: !!opts.dryRun,
2132
+ watch: opts.watch !== false,
2133
+ yes: !!opts.yes,
2134
+ mfaToken: opts.mfaToken,
2135
+ json: !!opts.json,
2136
+ });
2137
+ });
2138
+ dbMoveCmd
2139
+ .command('status <migrationId>')
2140
+ .description('Estado da relocação + verifyReport + gates (GET /api/cli/v1/db/relocate/[id])')
2141
+ .option('--json', 'saída em JSON')
2142
+ .action(async (migrationId, opts) => {
2143
+ const { runDbMoveStatus } = await import('./commands/db-move.js');
2144
+ await runDbMoveStatus(migrationId, { json: !!opts.json });
2145
+ });
2146
+ dbMoveCmd
2147
+ .command('confirm <migrationId>')
2148
+ .description('Liberar o cutover manual (POST /api/cli/v1/db/relocate/[id]/confirm — exige MFA)')
2149
+ // Era `requiredOption('--mfa')` — matava a sessão MFA de 1h que runDbMoveConfirm
2150
+ // (resolveStepUp) honra. Agora `option`; flag canônica `--mfa-token`, `--mfa` alias oculto. (auditoria UX 2026-09)
2151
+ .option('--mfa-token <token>', 'código TOTP step-up MFA (ou use `neetru auth mfa-session`)')
2152
+ .addOption(new Option('--mfa <token>').hideHelp())
2153
+ .option('--no-watch', 'não acompanha a máquina de estados após confirmar')
2154
+ .option('--json', 'saída em JSON')
2155
+ .action(async (migrationId, opts) => {
2156
+ const { runDbMoveConfirm } = await import('./commands/db-move.js');
2157
+ await runDbMoveConfirm(migrationId, { mfa: opts.mfaToken ?? opts.mfa, watch: opts.watch !== false, json: !!opts.json });
2158
+ });
2159
+ dbMoveCmd
2160
+ .command('abort <migrationId>')
2161
+ .description('Abortar a relocação — dispara o rollback SEGURO do estado atual (POST /api/cli/v1/db/relocate/[id]/abort — exige MFA)')
2162
+ .requiredOption('--reason <text>', 'motivo do abort (vai pro audit)')
2163
+ .option('-y, --yes', 'pula a confirmação interativa (modo script)')
2164
+ .option('--mfa-token <code>', 'código TOTP step-up MFA (ou use `neetru auth mfa-session`)')
2165
+ .option('--no-watch', 'não acompanha o rollback até um estado terminal (só dispara)')
2166
+ .option('--json', 'saída em JSON')
2167
+ .action(async (migrationId, opts) => {
2168
+ const { runDbMoveAbort } = await import('./commands/db-move.js');
2169
+ await runDbMoveAbort(migrationId, {
2170
+ reason: opts.reason,
2171
+ yes: !!opts.yes,
2172
+ mfaToken: opts.mfaToken,
2173
+ watch: opts.watch !== false,
2174
+ json: !!opts.json,
2175
+ });
2176
+ });
2177
+ // ── neetru db move decommission ───────────────────────────────────────
2178
+ // Comando SEPARADO e explícito que DROPA a ORIGEM de um move JÁ concluído
2179
+ // (fecha a janela de rollback). É o único caminho que destrói a origem —
2180
+ // gated por MFA e recusado client-side se o ledger ainda não aponta pro
2181
+ // destino (espelho da INV-DESTROY). O Core é a autoridade final.
2182
+ dbMoveCmd
2183
+ .command('decommission <migrationId>')
2184
+ .description('Descomissionar (DROPAR) a origem de um move concluído — fecha o rollback (POST /api/cli/v1/db/relocate/[id]/decommission — exige MFA)')
2185
+ .requiredOption('--reason <text>', 'motivo do descomissionamento (vai pro audit)')
2186
+ .option('-y, --yes', 'pula a confirmação interativa (modo script)')
2187
+ .option('--mfa-token <code>', 'código TOTP step-up MFA (ou use `neetru auth mfa-session`)')
2188
+ .option('--json', 'saída em JSON')
2189
+ .action(async (migrationId, opts) => {
2190
+ const { runDbMoveDecommission } = await import('./commands/db-move.js');
2191
+ await runDbMoveDecommission(migrationId, {
2192
+ reason: opts.reason,
2193
+ yes: !!opts.yes,
2194
+ mfaToken: opts.mfaToken,
2195
+ json: !!opts.json,
2196
+ });
2197
+ });
2198
+ // ── neetru db where ───────────────────────────────────────────────────
2199
+ // READ-ONLY: onde o banco de um produto VIVE hoje (VM/serverId + endpoint).
2200
+ // Complementa `neetru db status <dbId>` (que exige o dbId) aceitando --product.
2201
+ dbCmd
2202
+ .command('where')
2203
+ .description('Mostrar em qual VM o banco de um produto vive hoje (GET /api/cli/v1/db)')
2204
+ .option('--product <slug>', 'produto (slug ou id)')
2205
+ .option('--db <dbId>', 'dbId explícito (pdb_…)')
2206
+ .option('--env <env>', 'filtrar por ambiente')
2207
+ .option('--json', 'saída em JSON estruturado')
2208
+ .action(async (opts) => {
2209
+ const { runDbWhere } = await import('./commands/db-move.js');
2210
+ await runDbWhere({ product: opts.product, db: opts.db, env: opts.env, json: !!opts.json });
1909
2211
  });
1910
2212
  // ── neetru db hosts ───────────────────────────────────────────────────
1911
2213
  // Alias: também acessível via `neetru db-hosts` (top-level).
@@ -2050,6 +2352,7 @@ agentCmd
2050
2352
  .option('--binary <arch=path>', 'binário local (sha256+size computados, URL canônica GCS auto-derivada). Repetível.', (val, prev = []) => [...prev, val])
2051
2353
  .option('--artifact <arch=URL@sha256@sizeBytes>', 'artifact com URL/sha256/size explícitos (não toca FS). Repetível.', (val, prev = []) => [...prev, val])
2052
2354
  .option('--dry-run', 'imprime payload sem enviar')
2355
+ .option('--mfa-token <code>', 'código TOTP para step-up MFA (ou use `neetru auth mfa-session`)')
2053
2356
  .action(async (opts) => {
2054
2357
  const { runAgentRelease } = await import('./commands/agent-release.js');
2055
2358
  await runAgentRelease({
@@ -2060,6 +2363,7 @@ agentCmd
2060
2363
  binary: opts.binary,
2061
2364
  artifact: opts.artifact,
2062
2365
  dryRun: !!opts.dryRun,
2366
+ mfaToken: opts.mfaToken,
2063
2367
  });
2064
2368
  });
2065
2369
  agentCmd
@@ -2087,11 +2391,12 @@ const agentCanaryCmd = agentCmd
2087
2391
  .description('Controlar canary rollout de releases do agente');
2088
2392
  agentCanaryCmd
2089
2393
  .command('start <version>')
2090
- .description('Iniciar canary rollout de uma release (phase1 5%)')
2394
+ .description('Iniciar canary rollout de uma release (phase1 5%) — exige MFA')
2395
+ .option('--mfa-token <code>', 'código TOTP para step-up MFA (ou use `neetru auth mfa-session`)')
2091
2396
  .option('--json', 'saída em JSON')
2092
2397
  .action(async (version, opts) => {
2093
2398
  const { runAgentCanaryStart } = await import('./commands/agent-write.js');
2094
- await runAgentCanaryStart(version, { json: !!opts.json });
2399
+ await runAgentCanaryStart(version, { json: !!opts.json, mfaToken: opts.mfaToken });
2095
2400
  });
2096
2401
  agentCanaryCmd
2097
2402
  .command('rollback <version>')
@@ -2535,10 +2840,18 @@ webhooksCmd
2535
2840
  .command('unregister <id>')
2536
2841
  .description('Remover endpoint de webhook')
2537
2842
  .requiredOption('--product-id <id>', 'ID do produto')
2843
+ .option('--yes', 'pula a confirmação interativa (necessário em --json/CI)')
2844
+ .option('--force', 'alias de --yes')
2538
2845
  .option('--json', 'saída em JSON')
2539
2846
  .action(async (id, opts) => {
2540
2847
  const { runWebhooksUnregister } = await import('./commands/webhooks.js');
2541
- await runWebhooksUnregister({ id, productId: opts.productId, json: !!opts.json });
2848
+ await runWebhooksUnregister({
2849
+ id,
2850
+ productId: opts.productId,
2851
+ yes: !!opts.yes,
2852
+ force: !!opts.force,
2853
+ json: !!opts.json,
2854
+ });
2542
2855
  });
2543
2856
  webhooksCmd
2544
2857
  .command('test <id>')
@@ -2605,6 +2918,64 @@ domainsCmd
2605
2918
  const { runDomainsRetry } = await import('./commands/domains.js');
2606
2919
  await runDomainsRetry({ product: opts.product, sub: opts.sub, json: !!opts.json });
2607
2920
  });
2921
+ // ── neetru mobile ───────────────────────────────────────────────────
2922
+ // Devs/IAs registram/listam/revogam Apps Mobile (iOS/Android) do produto
2923
+ // atual (lido de neetru.config.json no cwd). Dev-facing, self-service —
2924
+ // igual `db`/`domains` pra init/list; revoke é destrutivo top-tier (admin
2925
+ // only + step-up MFA), igual `servers deactivate`. Contrato: PR #482 do
2926
+ // neetru-core (POST/GET/DELETE /api/cli/v1/products/{slug}/mobile-apps).
2927
+ function collectFlag(value, previous) {
2928
+ return [...previous, value];
2929
+ }
2930
+ const mobileCmd = program
2931
+ .command('mobile')
2932
+ .description('Gerenciar Apps Mobile (iOS/Android) do produto');
2933
+ mobileCmd
2934
+ .command('init')
2935
+ .description('Registrar um app mobile (iOS/Android) do produto atual')
2936
+ .requiredOption('--platform <ios|android>', 'plataforma do app: ios ou android')
2937
+ .option('--bundle-id <id>', 'Bundle ID do app (obrigatório se --platform ios)')
2938
+ .option('--team-id <id>', 'Apple Team ID, 10 caracteres (obrigatório se --platform ios)')
2939
+ .option('--package-name <name>', 'package name do app (obrigatório se --platform android)')
2940
+ .option('--sha-fingerprint <fp>', 'SHA-256 fingerprint do cert de assinatura (repetível; obrigatório se --platform android)', collectFlag, [])
2941
+ .requiredOption('--redirect-uri <url>', 'Redirect URI https:// (Universal Link/App Link) — nunca custom scheme')
2942
+ .option('--name <label>', 'nome/label opcional do app')
2943
+ .option('--json', 'saída em JSON')
2944
+ .action(async (opts) => {
2945
+ const { runMobileInit } = await import('./commands/mobile.js');
2946
+ await runMobileInit({
2947
+ platform: opts.platform,
2948
+ bundleId: opts.bundleId,
2949
+ teamId: opts.teamId,
2950
+ packageName: opts.packageName,
2951
+ shaFingerprints: opts.shaFingerprint,
2952
+ redirectUri: opts.redirectUri,
2953
+ name: opts.name,
2954
+ json: !!opts.json,
2955
+ });
2956
+ });
2957
+ mobileCmd
2958
+ .command('list')
2959
+ .description('Listar apps mobile registrados do produto atual')
2960
+ .option('--json', 'saída em JSON')
2961
+ .action(async (opts) => {
2962
+ const { runMobileList } = await import('./commands/mobile.js');
2963
+ await runMobileList({ json: !!opts.json });
2964
+ });
2965
+ mobileCmd
2966
+ .command('revoke <appId>')
2967
+ .description('Revogar um app mobile — mata sessões ativas imediatamente (admin + step-up MFA)')
2968
+ .option('--yes', 'pula confirmação interativa (obrigatório em --json/sem TTY)')
2969
+ .option('--mfa-token <code>', 'código TOTP para step-up MFA (senão usa sessão de 1h ou prompt)')
2970
+ .option('--json', 'saída em JSON')
2971
+ .action(async (appId, opts) => {
2972
+ const { runMobileRevoke } = await import('./commands/mobile.js');
2973
+ await runMobileRevoke(appId, {
2974
+ yes: !!opts.yes,
2975
+ mfaToken: opts.mfaToken,
2976
+ json: !!opts.json,
2977
+ });
2978
+ });
2608
2979
  // ── neetru chat ─────────────────────────────────────────────────────
2609
2980
  // Canal leve dev/IA → operador do Core (histórico rolante de 24h) — alternativa
2610
2981
  // ao bug-report pra perguntas/avisos rápidos.
@@ -2613,7 +2984,18 @@ domainsCmd
2613
2984
  registerChatCommand(program);
2614
2985
  }
2615
2986
  // ── parse ─────────────────────────────────────────────────────────────
2616
- if (!process.argv.slice(2).length) {
2987
+ if (process.argv[2] === '--version' || process.argv[2] === '-v') {
2988
+ // B3 (LABS 2026-09-28): `.version(CLI_VERSION, '-V')` liberou `--version` pros
2989
+ // subcomandos que o usam como opção própria (build/deploy/workspaces advance/
2990
+ // deployments create/api-catalog/agent release), mas removeu a long flag do
2991
+ // ROOT — `neetru --version` e `neetru -v` passaram a dar "unknown option".
2992
+ // Reintroduz AMBAS só no TOPO (primeiro token = argv[2]); o `--version <valor>`
2993
+ // dos subcomandos vem sempre DEPOIS do nome do comando, nunca como argv[2],
2994
+ // então não colide.
2995
+ console.log(CLI_VERSION);
2996
+ safeExit(0);
2997
+ }
2998
+ else if (!process.argv.slice(2).length) {
2617
2999
  if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== 'true') {
2618
3000
  const { runTerminalUi } = await import('./commands/ui.js');
2619
3001
  await runTerminalUi();