@neetru/cli 2.28.6 → 2.28.8
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 +56 -0
- package/README.md +23 -0
- package/dist/commands/admin/oidc-login.js +6 -3
- package/dist/commands/admin/oidc-login.js.map +1 -1
- package/dist/commands/admin/sdk-credential.js +11 -6
- package/dist/commands/admin/sdk-credential.js.map +1 -1
- package/dist/commands/agent-release.js +14 -27
- package/dist/commands/agent-release.js.map +1 -1
- package/dist/commands/ai-models.js +9 -4
- package/dist/commands/ai-models.js.map +1 -1
- package/dist/commands/archive.js +9 -2
- package/dist/commands/archive.js.map +1 -1
- package/dist/commands/auth.js +17 -2
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/autocomplete.d.ts +1 -1
- package/dist/commands/autocomplete.js +2 -0
- package/dist/commands/autocomplete.js.map +1 -1
- package/dist/commands/chat-menu.js +20 -6
- package/dist/commands/chat-menu.js.map +1 -1
- package/dist/commands/chat.d.ts +14 -0
- package/dist/commands/chat.js +32 -4
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/config.js +7 -0
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/db-move.d.ts +204 -0
- package/dist/commands/db-move.js +890 -0
- package/dist/commands/db-move.js.map +1 -0
- package/dist/commands/db.d.ts +14 -3
- package/dist/commands/db.js +102 -67
- package/dist/commands/db.js.map +1 -1
- package/dist/commands/deploy.js +13 -1
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev-up.d.ts +5 -0
- package/dist/commands/dev-up.js +29 -4
- package/dist/commands/dev-up.js.map +1 -1
- package/dist/commands/domains.js +48 -36
- package/dist/commands/domains.js.map +1 -1
- package/dist/commands/init.d.ts +0 -8
- package/dist/commands/init.js +14 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/logs.js +18 -21
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/mobile.d.ts +26 -0
- package/dist/commands/mobile.js +344 -0
- package/dist/commands/mobile.js.map +1 -0
- package/dist/commands/ops.d.ts +3 -0
- package/dist/commands/ops.js +21 -0
- package/dist/commands/ops.js.map +1 -1
- package/dist/commands/promote.js +13 -32
- package/dist/commands/promote.js.map +1 -1
- package/dist/commands/schema.d.ts +4 -0
- package/dist/commands/schema.js +25 -16
- package/dist/commands/schema.js.map +1 -1
- package/dist/commands/servers.d.ts +8 -6
- package/dist/commands/servers.js +11 -4
- package/dist/commands/servers.js.map +1 -1
- package/dist/commands/smoke.d.ts +123 -0
- package/dist/commands/smoke.js +529 -0
- package/dist/commands/smoke.js.map +1 -0
- package/dist/commands/vm-memory.d.ts +53 -0
- package/dist/commands/vm-memory.js +307 -0
- package/dist/commands/vm-memory.js.map +1 -0
- package/dist/commands/webhooks.d.ts +2 -0
- package/dist/commands/webhooks.js +7 -0
- package/dist/commands/webhooks.js.map +1 -1
- package/dist/index.js +360 -19
- package/dist/index.js.map +1 -1
- package/dist/lib/mobile-local/index.d.ts +7 -0
- package/dist/lib/mobile-local/index.js +7 -0
- package/dist/lib/mobile-local/index.js.map +1 -0
- package/dist/lib/mobile-local/mobile-json.d.ts +57 -0
- package/dist/lib/mobile-local/mobile-json.js +169 -0
- package/dist/lib/mobile-local/mobile-json.js.map +1 -0
- package/dist/lib/product-config.d.ts +5 -0
- package/dist/lib/product-config.js +37 -0
- package/dist/lib/product-config.js.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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';
|
|
@@ -149,7 +149,8 @@ program
|
|
|
149
149
|
.option('--here', 'init no cwd atual (modo merge — preserva existentes)')
|
|
150
150
|
.option('--path <path>', 'custom path target')
|
|
151
151
|
.option('--force', 'sobrescreve arquivos existentes em conflito (cuidado!)')
|
|
152
|
-
|
|
152
|
+
// -y padronizado com bootstrap (`-y, --yes`) — muscle-memory (auditoria UX 2026-09).
|
|
153
|
+
.option('-y, --yes', 'modo non-interactive (assume defaults, sai com erro se conflito sem flag)')
|
|
153
154
|
.action(async (name, opts) => {
|
|
154
155
|
const { runInit } = await import('./commands/init.js');
|
|
155
156
|
await runInit({
|
|
@@ -235,11 +236,16 @@ authCmd
|
|
|
235
236
|
.command('mfa-session')
|
|
236
237
|
.description('Abre uma sessão MFA de 1h — troca um código TOTP fresco por um token de sessão ' +
|
|
237
238
|
'salvo localmente; enquanto ativa, comandos destrutivos não re-promptam MFA.')
|
|
238
|
-
|
|
239
|
+
// Flag TOTP canônica em todo o CLI: `--mfa-token`. `--code` e `--mfa` seguem
|
|
240
|
+
// aceitos como aliases OCULTOS (retrocompat de scripts) — auditoria UX 2026-09
|
|
241
|
+
// (o mesmo código TOTP tinha 3 nomes: --code aqui, --mfa no db, --mfa-token no resto).
|
|
242
|
+
.option('--mfa-token <totp>', 'código TOTP de 6 dígitos (pula prompt interativo)')
|
|
243
|
+
.addOption(new Option('--code <totp>').hideHelp())
|
|
244
|
+
.addOption(new Option('--mfa <totp>').hideHelp())
|
|
239
245
|
.option('--json', 'saída em JSON')
|
|
240
246
|
.action(async (opts) => {
|
|
241
247
|
const { runAuthMfaSession } = await import('./commands/auth.js');
|
|
242
|
-
await runAuthMfaSession({ code: opts.code, json: !!opts.json });
|
|
248
|
+
await runAuthMfaSession({ code: opts.mfaToken ?? opts.code ?? opts.mfa, json: !!opts.json });
|
|
243
249
|
});
|
|
244
250
|
// ── neetru config ─────────────────────────────────────────────────────
|
|
245
251
|
const configCmd = program
|
|
@@ -463,6 +469,31 @@ program
|
|
|
463
469
|
json: !!opts.json,
|
|
464
470
|
});
|
|
465
471
|
});
|
|
472
|
+
// ── neetru smoke ──────────────────────────────────────────────────────
|
|
473
|
+
// Percorre as páginas do produto LOCAL (antes do deploy) e verifica alcance/
|
|
474
|
+
// login/DB, mais um smoke básico de prod opcional. Abordagem LEVE: só HTTP GET
|
|
475
|
+
// (fetch nativo) — SEM Playwright/headless browser (o CLI publicado no npm não
|
|
476
|
+
// pode ganhar uma dep de ~300MB). Captura de erro de console fica pra um v2.
|
|
477
|
+
program
|
|
478
|
+
.command('smoke')
|
|
479
|
+
.description('Percorre as páginas do produto LOCAL antes do deploy (alcance + login + DB) e um smoke de prod opcional')
|
|
480
|
+
.option('--base-url <url>', 'servidor dev LOCAL a percorrer', 'http://localhost:3000')
|
|
481
|
+
.option('--prod <url>', 'também roda um smoke básico de produção (GET / e health, espera 200)')
|
|
482
|
+
.option('--health-path <path>', 'endpoint de saúde/DB', '/api/health')
|
|
483
|
+
.option('--routes <csv>', 'rotas explícitas separadas por vírgula (sobrescreve a auto-descoberta)')
|
|
484
|
+
.option('--timeout <ms>', 'timeout por request em ms', (v) => Number.parseInt(v, 10), 10000)
|
|
485
|
+
.option('--json', 'saída em JSON de uma linha (machine-readable)')
|
|
486
|
+
.action(async (opts) => {
|
|
487
|
+
const { runSmoke } = await import('./commands/smoke.js');
|
|
488
|
+
await runSmoke({
|
|
489
|
+
baseUrl: opts.baseUrl,
|
|
490
|
+
prod: opts.prod,
|
|
491
|
+
healthPath: opts.healthPath,
|
|
492
|
+
routes: opts.routes,
|
|
493
|
+
timeout: opts.timeout,
|
|
494
|
+
json: !!opts.json,
|
|
495
|
+
});
|
|
496
|
+
});
|
|
466
497
|
// ── neetru status ─────────────────────────────────────────────────────
|
|
467
498
|
// Default: saúde das superfícies públicas (control plane). Com --client-id,
|
|
468
499
|
// delega ao status de workspace legado (backward-compat).
|
|
@@ -767,20 +798,25 @@ serversCmd
|
|
|
767
798
|
});
|
|
768
799
|
// bug_c42fa12b (CRITICAL 26/05): force-cleanup é recovery quando VM stuck
|
|
769
800
|
// em status='timeout_agent_register'/'falha_provisionamento' e flow normal
|
|
770
|
-
// (
|
|
771
|
-
//
|
|
772
|
-
//
|
|
801
|
+
// não funciona. P0.4 (2026-08-19, Core): a rota passou a exigir step-up MFA
|
|
802
|
+
// incondicionalmente (deixou de ser bypass intencional) — este CLI nunca
|
|
803
|
+
// tinha sido atualizado pra acompanhar (batia 401 sempre). Corrigido: exige
|
|
804
|
+
// --mfa-token (ou sessão de 1h via `neetru auth mfa-session`), com --dry-run.
|
|
773
805
|
serversCmd
|
|
774
806
|
.command('force-cleanup <serverId>')
|
|
775
|
-
.description('Recovery — força limpeza de VM stuck (
|
|
807
|
+
.description('Recovery — força limpeza de VM stuck (exige step-up MFA). Audita + notifica staff.')
|
|
776
808
|
.requiredOption('--reason <text>', 'motivo (min 10 chars, paper-trail audit)')
|
|
777
809
|
.option('--yes', 'pula a confirmação interativa (modo script)')
|
|
810
|
+
.option('--mfa-token <code>', 'código TOTP para step-up MFA (ou use `neetru auth mfa-session`)')
|
|
811
|
+
.option('--dry-run', 'valida e retorna o efeito sem aplicar (sem MFA)')
|
|
778
812
|
.option('--json', 'saída em JSON')
|
|
779
813
|
.action(async (id, opts) => {
|
|
780
814
|
const { runServersForceCleanup } = await import('./commands/servers.js');
|
|
781
815
|
await runServersForceCleanup(id, {
|
|
782
816
|
reason: opts.reason,
|
|
783
817
|
yes: !!opts.yes,
|
|
818
|
+
mfaToken: opts.mfaToken,
|
|
819
|
+
dryRun: !!opts.dryRun,
|
|
784
820
|
json: !!opts.json,
|
|
785
821
|
});
|
|
786
822
|
});
|
|
@@ -1160,6 +1196,43 @@ productsCmd
|
|
|
1160
1196
|
const { runProductsUnpublish } = await import('./commands/products.js');
|
|
1161
1197
|
await runProductsUnpublish(slug, { json: !!opts.json });
|
|
1162
1198
|
});
|
|
1199
|
+
// Alias amigável do controle de memória por inquilino (design §3.7): resolve a
|
|
1200
|
+
// VM do slug via --server/pickServer e delega ao mesmo core de `vm memory set`.
|
|
1201
|
+
const productsMemoryCmd = productsCmd
|
|
1202
|
+
.command('memory')
|
|
1203
|
+
.description('Controle de memória (RAM) do produto na VM (alias de `vm memory`)');
|
|
1204
|
+
productsMemoryCmd
|
|
1205
|
+
.command('set <slug>')
|
|
1206
|
+
.description('Define o teto de memória do produto na VM (guardrail de soma + step-up MFA)')
|
|
1207
|
+
.option('--memory-max <mb>', 'teto systemd MemoryMax em MB (cgroup)')
|
|
1208
|
+
.option('--max <mb>', 'alias de --memory-max')
|
|
1209
|
+
.option('--max-old-space <mb>', 'NODE_OPTIONS --max-old-space-size em MB (heap V8)')
|
|
1210
|
+
.option('--high <mb>', 'MemoryHigh em MB (throttle suave antes do OOM)')
|
|
1211
|
+
.option('--restart', 'força restart do app após aplicar')
|
|
1212
|
+
.option('--confirm-shrink', 'confirma reduzir abaixo do uso vivo (pode OOM o alvo)')
|
|
1213
|
+
.option('--server <id>', 'VM alvo (default: resolve a VM compartilhada)')
|
|
1214
|
+
.option('--reason <text>', 'motivo (paper-trail no audit do Core)')
|
|
1215
|
+
.option('--mfa <code>', 'código TOTP para step-up MFA')
|
|
1216
|
+
.option('--mfa-token <code>', 'alias de --mfa')
|
|
1217
|
+
.option('-y, --yes', 'pula a confirmação interativa (em --json exige --mfa)')
|
|
1218
|
+
.option('--json', 'saída em JSON')
|
|
1219
|
+
.action(async (slug, opts) => {
|
|
1220
|
+
const { runVmMemorySet } = await import('./commands/vm-memory.js');
|
|
1221
|
+
await runVmMemorySet(slug, {
|
|
1222
|
+
memoryMax: opts.memoryMax,
|
|
1223
|
+
max: opts.max,
|
|
1224
|
+
maxOldSpace: opts.maxOldSpace,
|
|
1225
|
+
high: opts.high,
|
|
1226
|
+
restart: !!opts.restart,
|
|
1227
|
+
confirmShrink: !!opts.confirmShrink,
|
|
1228
|
+
server: opts.server,
|
|
1229
|
+
reason: opts.reason,
|
|
1230
|
+
mfa: opts.mfa,
|
|
1231
|
+
mfaToken: opts.mfaToken,
|
|
1232
|
+
yes: !!opts.yes,
|
|
1233
|
+
json: !!opts.json,
|
|
1234
|
+
});
|
|
1235
|
+
});
|
|
1163
1236
|
// ── neetru admin database (Phase A — plano de controle staff de bancos) ───────
|
|
1164
1237
|
// D-1: renomeado de `neetru products db` → `neetru admin database`.
|
|
1165
1238
|
// Alias `neetru products db` emite aviso de remoção.
|
|
@@ -1272,15 +1345,32 @@ adminDatabaseCmd
|
|
|
1272
1345
|
await runDbDelete(id, { json: !!opts.json, force: !!opts.force });
|
|
1273
1346
|
});
|
|
1274
1347
|
// ── neetru admin sdk-credential ────────────────────────────────────────────
|
|
1275
|
-
// Gerencia credenciais OIDC (
|
|
1348
|
+
// Gerencia credenciais OIDC de MÁQUINA (M2M / client_credentials) por produto —
|
|
1349
|
+
// create / list / revoke. É credencial de SERVIDOR (backend do produto ↔ API do
|
|
1350
|
+
// Neetru via @neetru/sdk), NÃO login de usuário no browser (isso é `oidc-login`).
|
|
1276
1351
|
// Resolve bug_599d2f033e024bc194aa07639cd26c84: bootstrap CI/IaC desbloqueado.
|
|
1277
1352
|
// Internamente usa `oidc_clients/{clientId}` no Core (canônico pós-devkit).
|
|
1278
1353
|
const adminSdkCredentialCmd = adminCmd
|
|
1279
1354
|
.command('sdk-credential')
|
|
1280
|
-
.description('
|
|
1355
|
+
.description('Credenciais OIDC de MÁQUINA (M2M / client_credentials) de produtos — create / list / revoke. ' +
|
|
1356
|
+
'Para LOGIN de usuário no browser use `neetru admin oidc-login`.');
|
|
1281
1357
|
adminSdkCredentialCmd
|
|
1282
1358
|
.command('create')
|
|
1283
|
-
.description('[Staff] Criar credencial
|
|
1359
|
+
.description('[Staff] Criar credencial de MÁQUINA (M2M / client_credentials) p/ servidor-a-servidor do ' +
|
|
1360
|
+
'produto — exibe clientSecret ONE-TIME. NÃO serve p/ login de usuário no browser ' +
|
|
1361
|
+
'(use `neetru admin oidc-login create`).')
|
|
1362
|
+
.addHelpText('after', [
|
|
1363
|
+
'',
|
|
1364
|
+
'Qual comando usar:',
|
|
1365
|
+
' • sdk-credential create → credencial de SERVIDOR/máquina (M2M, grant client_credentials).',
|
|
1366
|
+
' Backend do produto falando com a API do Neetru via @neetru/sdk. SEM navegador, SEM usuário.',
|
|
1367
|
+
' • oidc-login create → LOGIN de usuário final no browser (grant authorization_code).',
|
|
1368
|
+
' É o que autentica a pessoa que abre o produto. Use ESTE p/ resolver "o login quebrou".',
|
|
1369
|
+
'',
|
|
1370
|
+
'Incidente gestovendas (2026-09): credenciais de máquina foram criadas por engano tentando',
|
|
1371
|
+
'consertar o login → ficaram mortas (nunca emitiram token) e o login continuou quebrado.',
|
|
1372
|
+
'',
|
|
1373
|
+
].join('\n'))
|
|
1284
1374
|
.requiredOption('--product-id <id>', 'ID do produto')
|
|
1285
1375
|
.requiredOption('--name <label>', 'nome/label da credencial (ex: "Backend Prod")')
|
|
1286
1376
|
.option('--scopes <escopos>', 'escopos separados por vírgula (ex: "entitlements.read,usage.write")')
|
|
@@ -1535,6 +1625,56 @@ vmCmd
|
|
|
1535
1625
|
yes: !!opts.yes,
|
|
1536
1626
|
});
|
|
1537
1627
|
});
|
|
1628
|
+
// ── neetru vm memory ──────────────────────────────────────────────────
|
|
1629
|
+
// Controle gerenciado de memória (RAM) por inquilino na VM multi-tenant.
|
|
1630
|
+
// Origem: incidente OOM 2026-09-07 (PDV caiu; Suporte ajustou systemd à mão).
|
|
1631
|
+
// GUARDRAIL: a SOMA dos tetos + reserva ≤ RAM×0.90 — validado fail-closed no
|
|
1632
|
+
// Core; superfície sensível de infra → step-up MFA.
|
|
1633
|
+
const vmMemoryCmd = vmCmd
|
|
1634
|
+
.command('memory')
|
|
1635
|
+
.description('Controle de memória (RAM) por inquilino na VM');
|
|
1636
|
+
vmMemoryCmd
|
|
1637
|
+
.command('status [serverId]')
|
|
1638
|
+
.alias('list')
|
|
1639
|
+
.description('Lista os inquilinos da VM: teto, uso vivo, folga + reserva/livre/RAM total')
|
|
1640
|
+
.option('--server <id>', 'VM alvo (alternativa ao argumento posicional)')
|
|
1641
|
+
.option('--json', 'saída em JSON')
|
|
1642
|
+
.action(async (serverId, opts) => {
|
|
1643
|
+
const { runVmMemoryStatus } = await import('./commands/vm-memory.js');
|
|
1644
|
+
await runVmMemoryStatus(serverId, { server: opts.server, json: !!opts.json });
|
|
1645
|
+
});
|
|
1646
|
+
vmMemoryCmd
|
|
1647
|
+
.command('set <slug>')
|
|
1648
|
+
.description('Define o teto de memória de um inquilino (guardrail de soma + step-up MFA)')
|
|
1649
|
+
.option('--memory-max <mb>', 'teto systemd MemoryMax em MB (cgroup)')
|
|
1650
|
+
.option('--max <mb>', 'alias de --memory-max')
|
|
1651
|
+
.option('--max-old-space <mb>', 'NODE_OPTIONS --max-old-space-size em MB (heap V8)')
|
|
1652
|
+
.option('--high <mb>', 'MemoryHigh em MB (throttle suave antes do OOM)')
|
|
1653
|
+
.option('--restart', 'força restart do app após aplicar (default: só se max-old-space muda)')
|
|
1654
|
+
.option('--confirm-shrink', 'confirma reduzir abaixo do uso vivo (pode OOM o alvo)')
|
|
1655
|
+
.option('--server <id>', 'VM alvo (default: resolve a VM compartilhada)')
|
|
1656
|
+
.option('--reason <text>', 'motivo (paper-trail no audit do Core)')
|
|
1657
|
+
.option('--mfa <code>', 'código TOTP para step-up MFA (ou use `neetru auth mfa-session`)')
|
|
1658
|
+
.option('--mfa-token <code>', 'alias de --mfa')
|
|
1659
|
+
.option('-y, --yes', 'pula a confirmação interativa (em --json exige --mfa)')
|
|
1660
|
+
.option('--json', 'saída em JSON')
|
|
1661
|
+
.action(async (slug, opts) => {
|
|
1662
|
+
const { runVmMemorySet } = await import('./commands/vm-memory.js');
|
|
1663
|
+
await runVmMemorySet(slug, {
|
|
1664
|
+
memoryMax: opts.memoryMax,
|
|
1665
|
+
max: opts.max,
|
|
1666
|
+
maxOldSpace: opts.maxOldSpace,
|
|
1667
|
+
high: opts.high,
|
|
1668
|
+
restart: !!opts.restart,
|
|
1669
|
+
confirmShrink: !!opts.confirmShrink,
|
|
1670
|
+
server: opts.server,
|
|
1671
|
+
reason: opts.reason,
|
|
1672
|
+
mfa: opts.mfa,
|
|
1673
|
+
mfaToken: opts.mfaToken,
|
|
1674
|
+
yes: !!opts.yes,
|
|
1675
|
+
json: !!opts.json,
|
|
1676
|
+
});
|
|
1677
|
+
});
|
|
1538
1678
|
// ── neetru validate ───────────────────────────────────────────────────
|
|
1539
1679
|
program
|
|
1540
1680
|
.command('validate')
|
|
@@ -1841,11 +1981,16 @@ dbMigrationsCmd
|
|
|
1841
1981
|
dbMigrationsCmd
|
|
1842
1982
|
.command('confirm <migrationId>')
|
|
1843
1983
|
.description('Confirmar migração destrutiva (POST /api/cli/v1/db/migrations/[id]/confirm — exige MFA)')
|
|
1844
|
-
|
|
1984
|
+
// Era `requiredOption('--mfa')` — o commander rejeitava ANTES de rodar, matando
|
|
1985
|
+
// o caminho de sessão MFA de 1h (`neetru auth mfa-session`) que o resolveStepUp
|
|
1986
|
+
// promete honrar. Agora `option`: resolveStepUp faz o fail-closed (sessão → --mfa-token
|
|
1987
|
+
// → erro acionável). Flag canônica `--mfa-token`; `--mfa` fica como alias oculto. (auditoria UX 2026-09)
|
|
1988
|
+
.option('--mfa-token <token>', 'código TOTP step-up MFA (ou use `neetru auth mfa-session`)')
|
|
1989
|
+
.addOption(new Option('--mfa <token>').hideHelp())
|
|
1845
1990
|
.option('--json', 'saída em JSON')
|
|
1846
1991
|
.action(async (migrationId, opts) => {
|
|
1847
1992
|
const { runDbMigrationsConfirm } = await import('./commands/db.js');
|
|
1848
|
-
await runDbMigrationsConfirm(migrationId, { mfa: opts.mfa, json: !!opts.json });
|
|
1993
|
+
await runDbMigrationsConfirm(migrationId, { mfa: opts.mfaToken ?? opts.mfa, json: !!opts.json });
|
|
1849
1994
|
});
|
|
1850
1995
|
// bug_728fa11c fix: cancela migração presa em awaiting_confirmation ou pending.
|
|
1851
1996
|
dbMigrationsCmd
|
|
@@ -1871,14 +2016,16 @@ dbMigrationsCmd
|
|
|
1871
2016
|
dbMigrationsCmd
|
|
1872
2017
|
.command('resolve <migrationId>')
|
|
1873
2018
|
.description('Destravar migração pending/partially_applied/failed redundante (POST /api/cli/v1/db/migrations/[id]/resolve)')
|
|
1874
|
-
.option('--force', 'atestar manualmente sem prova de redundância por hash (exige --mfa ou sessão MFA)')
|
|
1875
|
-
|
|
2019
|
+
.option('--force', 'atestar manualmente sem prova de redundância por hash (exige --mfa-token ou sessão MFA)')
|
|
2020
|
+
// Flag canônica `--mfa-token`; `--mfa` como alias oculto (retrocompat). (auditoria UX 2026-09)
|
|
2021
|
+
.option('--mfa-token <token>', 'código TOTP step-up MFA (necessário com --force; ou use `neetru auth mfa-session`)')
|
|
2022
|
+
.addOption(new Option('--mfa <token>').hideHelp())
|
|
1876
2023
|
.option('--json', 'saída em JSON')
|
|
1877
2024
|
.action(async (migrationId, opts) => {
|
|
1878
2025
|
const { runDbMigrationsResolve } = await import('./commands/db.js');
|
|
1879
2026
|
await runDbMigrationsResolve(migrationId, {
|
|
1880
2027
|
force: !!opts.force,
|
|
1881
|
-
mfa: opts.mfa,
|
|
2028
|
+
mfa: opts.mfaToken ?? opts.mfa,
|
|
1882
2029
|
json: !!opts.json,
|
|
1883
2030
|
});
|
|
1884
2031
|
});
|
|
@@ -1896,11 +2043,139 @@ dbCmd
|
|
|
1896
2043
|
dbCmd
|
|
1897
2044
|
.command('restore <dbId>')
|
|
1898
2045
|
.description('Restaurar banco a partir de backup (POST /api/cli/v1/db/[id]/restore — exige MFA)')
|
|
1899
|
-
|
|
2046
|
+
// Automação (auditoria UX 2026-09): --backup-id/--yes tornam o restore scriptável;
|
|
2047
|
+
// em --json ambos são obrigatórios (fail-closed, nunca trava no inquirer).
|
|
2048
|
+
.option('--backup-id <id>', 'ID do backup a restaurar (pula o prompt; obrigatório em --json)')
|
|
2049
|
+
.option('-y, --yes', 'pula a confirmação textual "RESTAURAR" (obrigatório em --json)')
|
|
2050
|
+
// Flag canônica `--mfa-token`; `--mfa` como alias oculto (retrocompat).
|
|
2051
|
+
.option('--mfa-token <token>', 'código TOTP step-up MFA (obrigatório — operação destrutiva)')
|
|
2052
|
+
.addOption(new Option('--mfa <token>').hideHelp())
|
|
1900
2053
|
.option('--json', 'saída em JSON')
|
|
1901
2054
|
.action(async (dbId, opts) => {
|
|
1902
2055
|
const { runDbRestore } = await import('./commands/db.js');
|
|
1903
|
-
await runDbRestore(dbId, {
|
|
2056
|
+
await runDbRestore(dbId, {
|
|
2057
|
+
mfa: opts.mfaToken ?? opts.mfa,
|
|
2058
|
+
backupId: opts.backupId,
|
|
2059
|
+
yes: !!opts.yes,
|
|
2060
|
+
json: !!opts.json,
|
|
2061
|
+
});
|
|
2062
|
+
});
|
|
2063
|
+
// ── neetru db move ────────────────────────────────────────────────────
|
|
2064
|
+
// Relocação ATÔMICA de um banco de produto entre VMs (Fase 2 / PR #423).
|
|
2065
|
+
// GRUPO com subcomando default `start` (mesmo padrão de `neetru promote`):
|
|
2066
|
+
// `neetru db move --product <slug> --to <serverId>` cai em `start`.
|
|
2067
|
+
// Só reporta SUCESSO quando o Core confirma o cutover VERIFICADO (state `done`);
|
|
2068
|
+
// rollback é reportado explicitamente como "produto seguiu na origem".
|
|
2069
|
+
const dbMoveCmd = dbCmd
|
|
2070
|
+
.command('move')
|
|
2071
|
+
.description('Relocar (mover) o banco de um produto entre VMs — atômico, com rollback e prova de dados');
|
|
2072
|
+
dbMoveCmd
|
|
2073
|
+
.command('start', { isDefault: true })
|
|
2074
|
+
.description('Iniciar a relocação de um banco (POST /api/cli/v1/db/[id]/relocate — destrutivo, exige MFA)')
|
|
2075
|
+
.option('--product <slug>', 'produto cujo banco será movido (slug ou id)')
|
|
2076
|
+
.option('--db <dbId>', 'dbId explícito (pdb_…) — dispensa a resolução por produto')
|
|
2077
|
+
.option('--env <env>', 'ambiente do banco, para desambiguar quando o produto tem mais de um')
|
|
2078
|
+
.option('--to <serverId>', 'VM destino (obrigatório)')
|
|
2079
|
+
.option('--reason <text>', 'motivo (obrigatório fora de --dry-run; vai pro audit)')
|
|
2080
|
+
.option('--strategy <s>', 'snapshot_quiesce (padrão) | logical_stream', 'snapshot_quiesce')
|
|
2081
|
+
.option('--manual-cutover', 'pausa em verifying_target aguardando `db move confirm`')
|
|
2082
|
+
.option('--max-quiesce-ms <n>', 'teto do congelamento da origem (padrão 15000, máx 60000)')
|
|
2083
|
+
.option('--dry-run', 'só roda o preflight (não provisiona nada, sem MFA)')
|
|
2084
|
+
.option('--no-watch', 'não acompanha a máquina de estados (só dispara e imprime o migrationId)')
|
|
2085
|
+
.option('-y, --yes', 'pula a confirmação interativa (modo script)')
|
|
2086
|
+
.option('--mfa-token <code>', 'código TOTP step-up MFA (ou use `neetru auth mfa-session`)')
|
|
2087
|
+
.option('--json', 'saída em JSON estruturado')
|
|
2088
|
+
.action(async (opts) => {
|
|
2089
|
+
const { runDbMoveStart } = await import('./commands/db-move.js');
|
|
2090
|
+
await runDbMoveStart({
|
|
2091
|
+
product: opts.product,
|
|
2092
|
+
db: opts.db,
|
|
2093
|
+
env: opts.env,
|
|
2094
|
+
to: opts.to,
|
|
2095
|
+
reason: opts.reason,
|
|
2096
|
+
strategy: opts.strategy,
|
|
2097
|
+
manualCutover: !!opts.manualCutover,
|
|
2098
|
+
maxQuiesceMs: opts.maxQuiesceMs,
|
|
2099
|
+
dryRun: !!opts.dryRun,
|
|
2100
|
+
watch: opts.watch !== false,
|
|
2101
|
+
yes: !!opts.yes,
|
|
2102
|
+
mfaToken: opts.mfaToken,
|
|
2103
|
+
json: !!opts.json,
|
|
2104
|
+
});
|
|
2105
|
+
});
|
|
2106
|
+
dbMoveCmd
|
|
2107
|
+
.command('status <migrationId>')
|
|
2108
|
+
.description('Estado da relocação + verifyReport + gates (GET /api/cli/v1/db/relocate/[id])')
|
|
2109
|
+
.option('--json', 'saída em JSON')
|
|
2110
|
+
.action(async (migrationId, opts) => {
|
|
2111
|
+
const { runDbMoveStatus } = await import('./commands/db-move.js');
|
|
2112
|
+
await runDbMoveStatus(migrationId, { json: !!opts.json });
|
|
2113
|
+
});
|
|
2114
|
+
dbMoveCmd
|
|
2115
|
+
.command('confirm <migrationId>')
|
|
2116
|
+
.description('Liberar o cutover manual (POST /api/cli/v1/db/relocate/[id]/confirm — exige MFA)')
|
|
2117
|
+
// Era `requiredOption('--mfa')` — matava a sessão MFA de 1h que runDbMoveConfirm
|
|
2118
|
+
// (resolveStepUp) honra. Agora `option`; flag canônica `--mfa-token`, `--mfa` alias oculto. (auditoria UX 2026-09)
|
|
2119
|
+
.option('--mfa-token <token>', 'código TOTP step-up MFA (ou use `neetru auth mfa-session`)')
|
|
2120
|
+
.addOption(new Option('--mfa <token>').hideHelp())
|
|
2121
|
+
.option('--no-watch', 'não acompanha a máquina de estados após confirmar')
|
|
2122
|
+
.option('--json', 'saída em JSON')
|
|
2123
|
+
.action(async (migrationId, opts) => {
|
|
2124
|
+
const { runDbMoveConfirm } = await import('./commands/db-move.js');
|
|
2125
|
+
await runDbMoveConfirm(migrationId, { mfa: opts.mfaToken ?? opts.mfa, watch: opts.watch !== false, json: !!opts.json });
|
|
2126
|
+
});
|
|
2127
|
+
dbMoveCmd
|
|
2128
|
+
.command('abort <migrationId>')
|
|
2129
|
+
.description('Abortar a relocação — dispara o rollback SEGURO do estado atual (POST /api/cli/v1/db/relocate/[id]/abort — exige MFA)')
|
|
2130
|
+
.requiredOption('--reason <text>', 'motivo do abort (vai pro audit)')
|
|
2131
|
+
.option('-y, --yes', 'pula a confirmação interativa (modo script)')
|
|
2132
|
+
.option('--mfa-token <code>', 'código TOTP step-up MFA (ou use `neetru auth mfa-session`)')
|
|
2133
|
+
.option('--no-watch', 'não acompanha o rollback até um estado terminal (só dispara)')
|
|
2134
|
+
.option('--json', 'saída em JSON')
|
|
2135
|
+
.action(async (migrationId, opts) => {
|
|
2136
|
+
const { runDbMoveAbort } = await import('./commands/db-move.js');
|
|
2137
|
+
await runDbMoveAbort(migrationId, {
|
|
2138
|
+
reason: opts.reason,
|
|
2139
|
+
yes: !!opts.yes,
|
|
2140
|
+
mfaToken: opts.mfaToken,
|
|
2141
|
+
watch: opts.watch !== false,
|
|
2142
|
+
json: !!opts.json,
|
|
2143
|
+
});
|
|
2144
|
+
});
|
|
2145
|
+
// ── neetru db move decommission ───────────────────────────────────────
|
|
2146
|
+
// Comando SEPARADO e explícito que DROPA a ORIGEM de um move JÁ concluído
|
|
2147
|
+
// (fecha a janela de rollback). É o único caminho que destrói a origem —
|
|
2148
|
+
// gated por MFA e recusado client-side se o ledger ainda não aponta pro
|
|
2149
|
+
// destino (espelho da INV-DESTROY). O Core é a autoridade final.
|
|
2150
|
+
dbMoveCmd
|
|
2151
|
+
.command('decommission <migrationId>')
|
|
2152
|
+
.description('Descomissionar (DROPAR) a origem de um move concluído — fecha o rollback (POST /api/cli/v1/db/relocate/[id]/decommission — exige MFA)')
|
|
2153
|
+
.requiredOption('--reason <text>', 'motivo do descomissionamento (vai pro audit)')
|
|
2154
|
+
.option('-y, --yes', 'pula a confirmação interativa (modo script)')
|
|
2155
|
+
.option('--mfa-token <code>', 'código TOTP step-up MFA (ou use `neetru auth mfa-session`)')
|
|
2156
|
+
.option('--json', 'saída em JSON')
|
|
2157
|
+
.action(async (migrationId, opts) => {
|
|
2158
|
+
const { runDbMoveDecommission } = await import('./commands/db-move.js');
|
|
2159
|
+
await runDbMoveDecommission(migrationId, {
|
|
2160
|
+
reason: opts.reason,
|
|
2161
|
+
yes: !!opts.yes,
|
|
2162
|
+
mfaToken: opts.mfaToken,
|
|
2163
|
+
json: !!opts.json,
|
|
2164
|
+
});
|
|
2165
|
+
});
|
|
2166
|
+
// ── neetru db where ───────────────────────────────────────────────────
|
|
2167
|
+
// READ-ONLY: onde o banco de um produto VIVE hoje (VM/serverId + endpoint).
|
|
2168
|
+
// Complementa `neetru db status <dbId>` (que exige o dbId) aceitando --product.
|
|
2169
|
+
dbCmd
|
|
2170
|
+
.command('where')
|
|
2171
|
+
.description('Mostrar em qual VM o banco de um produto vive hoje (GET /api/cli/v1/db)')
|
|
2172
|
+
.option('--product <slug>', 'produto (slug ou id)')
|
|
2173
|
+
.option('--db <dbId>', 'dbId explícito (pdb_…)')
|
|
2174
|
+
.option('--env <env>', 'filtrar por ambiente')
|
|
2175
|
+
.option('--json', 'saída em JSON estruturado')
|
|
2176
|
+
.action(async (opts) => {
|
|
2177
|
+
const { runDbWhere } = await import('./commands/db-move.js');
|
|
2178
|
+
await runDbWhere({ product: opts.product, db: opts.db, env: opts.env, json: !!opts.json });
|
|
1904
2179
|
});
|
|
1905
2180
|
// ── neetru db hosts ───────────────────────────────────────────────────
|
|
1906
2181
|
// Alias: também acessível via `neetru db-hosts` (top-level).
|
|
@@ -2530,10 +2805,18 @@ webhooksCmd
|
|
|
2530
2805
|
.command('unregister <id>')
|
|
2531
2806
|
.description('Remover endpoint de webhook')
|
|
2532
2807
|
.requiredOption('--product-id <id>', 'ID do produto')
|
|
2808
|
+
.option('--yes', 'pula a confirmação interativa (necessário em --json/CI)')
|
|
2809
|
+
.option('--force', 'alias de --yes')
|
|
2533
2810
|
.option('--json', 'saída em JSON')
|
|
2534
2811
|
.action(async (id, opts) => {
|
|
2535
2812
|
const { runWebhooksUnregister } = await import('./commands/webhooks.js');
|
|
2536
|
-
await runWebhooksUnregister({
|
|
2813
|
+
await runWebhooksUnregister({
|
|
2814
|
+
id,
|
|
2815
|
+
productId: opts.productId,
|
|
2816
|
+
yes: !!opts.yes,
|
|
2817
|
+
force: !!opts.force,
|
|
2818
|
+
json: !!opts.json,
|
|
2819
|
+
});
|
|
2537
2820
|
});
|
|
2538
2821
|
webhooksCmd
|
|
2539
2822
|
.command('test <id>')
|
|
@@ -2600,6 +2883,64 @@ domainsCmd
|
|
|
2600
2883
|
const { runDomainsRetry } = await import('./commands/domains.js');
|
|
2601
2884
|
await runDomainsRetry({ product: opts.product, sub: opts.sub, json: !!opts.json });
|
|
2602
2885
|
});
|
|
2886
|
+
// ── neetru mobile ───────────────────────────────────────────────────
|
|
2887
|
+
// Devs/IAs registram/listam/revogam Apps Mobile (iOS/Android) do produto
|
|
2888
|
+
// atual (lido de neetru.config.json no cwd). Dev-facing, self-service —
|
|
2889
|
+
// igual `db`/`domains` pra init/list; revoke é destrutivo top-tier (admin
|
|
2890
|
+
// only + step-up MFA), igual `servers deactivate`. Contrato: PR #482 do
|
|
2891
|
+
// neetru-core (POST/GET/DELETE /api/cli/v1/products/{slug}/mobile-apps).
|
|
2892
|
+
function collectFlag(value, previous) {
|
|
2893
|
+
return [...previous, value];
|
|
2894
|
+
}
|
|
2895
|
+
const mobileCmd = program
|
|
2896
|
+
.command('mobile')
|
|
2897
|
+
.description('Gerenciar Apps Mobile (iOS/Android) do produto');
|
|
2898
|
+
mobileCmd
|
|
2899
|
+
.command('init')
|
|
2900
|
+
.description('Registrar um app mobile (iOS/Android) do produto atual')
|
|
2901
|
+
.requiredOption('--platform <ios|android>', 'plataforma do app: ios ou android')
|
|
2902
|
+
.option('--bundle-id <id>', 'Bundle ID do app (obrigatório se --platform ios)')
|
|
2903
|
+
.option('--team-id <id>', 'Apple Team ID, 10 caracteres (obrigatório se --platform ios)')
|
|
2904
|
+
.option('--package-name <name>', 'package name do app (obrigatório se --platform android)')
|
|
2905
|
+
.option('--sha-fingerprint <fp>', 'SHA-256 fingerprint do cert de assinatura (repetível; obrigatório se --platform android)', collectFlag, [])
|
|
2906
|
+
.requiredOption('--redirect-uri <url>', 'Redirect URI https:// (Universal Link/App Link) — nunca custom scheme')
|
|
2907
|
+
.option('--name <label>', 'nome/label opcional do app')
|
|
2908
|
+
.option('--json', 'saída em JSON')
|
|
2909
|
+
.action(async (opts) => {
|
|
2910
|
+
const { runMobileInit } = await import('./commands/mobile.js');
|
|
2911
|
+
await runMobileInit({
|
|
2912
|
+
platform: opts.platform,
|
|
2913
|
+
bundleId: opts.bundleId,
|
|
2914
|
+
teamId: opts.teamId,
|
|
2915
|
+
packageName: opts.packageName,
|
|
2916
|
+
shaFingerprints: opts.shaFingerprint,
|
|
2917
|
+
redirectUri: opts.redirectUri,
|
|
2918
|
+
name: opts.name,
|
|
2919
|
+
json: !!opts.json,
|
|
2920
|
+
});
|
|
2921
|
+
});
|
|
2922
|
+
mobileCmd
|
|
2923
|
+
.command('list')
|
|
2924
|
+
.description('Listar apps mobile registrados do produto atual')
|
|
2925
|
+
.option('--json', 'saída em JSON')
|
|
2926
|
+
.action(async (opts) => {
|
|
2927
|
+
const { runMobileList } = await import('./commands/mobile.js');
|
|
2928
|
+
await runMobileList({ json: !!opts.json });
|
|
2929
|
+
});
|
|
2930
|
+
mobileCmd
|
|
2931
|
+
.command('revoke <appId>')
|
|
2932
|
+
.description('Revogar um app mobile — mata sessões ativas imediatamente (admin + step-up MFA)')
|
|
2933
|
+
.option('--yes', 'pula confirmação interativa (obrigatório em --json/sem TTY)')
|
|
2934
|
+
.option('--mfa-token <code>', 'código TOTP para step-up MFA (senão usa sessão de 1h ou prompt)')
|
|
2935
|
+
.option('--json', 'saída em JSON')
|
|
2936
|
+
.action(async (appId, opts) => {
|
|
2937
|
+
const { runMobileRevoke } = await import('./commands/mobile.js');
|
|
2938
|
+
await runMobileRevoke(appId, {
|
|
2939
|
+
yes: !!opts.yes,
|
|
2940
|
+
mfaToken: opts.mfaToken,
|
|
2941
|
+
json: !!opts.json,
|
|
2942
|
+
});
|
|
2943
|
+
});
|
|
2603
2944
|
// ── neetru chat ─────────────────────────────────────────────────────
|
|
2604
2945
|
// Canal leve dev/IA → operador do Core (histórico rolante de 24h) — alternativa
|
|
2605
2946
|
// ao bug-report pra perguntas/avisos rápidos.
|