@neetru/cli 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/commands/agent-release.d.ts +13 -0
  2. package/dist/commands/agent-release.js +204 -0
  3. package/dist/commands/agent-release.js.map +1 -0
  4. package/dist/commands/agent-write.d.ts +12 -0
  5. package/dist/commands/agent-write.js +94 -0
  6. package/dist/commands/agent-write.js.map +1 -0
  7. package/dist/commands/api-catalog.d.ts +20 -0
  8. package/dist/commands/api-catalog.js +126 -0
  9. package/dist/commands/api-catalog.js.map +1 -0
  10. package/dist/commands/audit.d.ts +8 -0
  11. package/dist/commands/audit.js +69 -0
  12. package/dist/commands/audit.js.map +1 -0
  13. package/dist/commands/billing.d.ts +6 -0
  14. package/dist/commands/billing.js +69 -0
  15. package/dist/commands/billing.js.map +1 -0
  16. package/dist/commands/build.js +10 -3
  17. package/dist/commands/build.js.map +1 -1
  18. package/dist/commands/cloud-run.d.ts +11 -0
  19. package/dist/commands/cloud-run.js +87 -0
  20. package/dist/commands/cloud-run.js.map +1 -0
  21. package/dist/commands/deploy.d.ts +7 -0
  22. package/dist/commands/deploy.js +150 -17
  23. package/dist/commands/deploy.js.map +1 -1
  24. package/dist/commands/deployments.d.ts +11 -0
  25. package/dist/commands/deployments.js +69 -0
  26. package/dist/commands/deployments.js.map +1 -0
  27. package/dist/commands/dr.d.ts +11 -0
  28. package/dist/commands/dr.js +79 -0
  29. package/dist/commands/dr.js.map +1 -0
  30. package/dist/commands/infra-read.d.ts +9 -0
  31. package/dist/commands/infra-read.js +113 -0
  32. package/dist/commands/infra-read.js.map +1 -0
  33. package/dist/commands/init.js +91 -27
  34. package/dist/commands/init.js.map +1 -1
  35. package/dist/commands/products-db.d.ts +37 -0
  36. package/dist/commands/products-db.js +230 -0
  37. package/dist/commands/products-db.js.map +1 -0
  38. package/dist/commands/products.d.ts +12 -0
  39. package/dist/commands/products.js +97 -0
  40. package/dist/commands/products.js.map +1 -0
  41. package/dist/commands/servers.d.ts +23 -0
  42. package/dist/commands/servers.js +166 -0
  43. package/dist/commands/servers.js.map +1 -0
  44. package/dist/commands/support.d.ts +25 -0
  45. package/dist/commands/support.js +184 -0
  46. package/dist/commands/support.js.map +1 -0
  47. package/dist/commands/surface-status.d.ts +5 -0
  48. package/dist/commands/surface-status.js +63 -0
  49. package/dist/commands/surface-status.js.map +1 -0
  50. package/dist/commands/tenants.d.ts +34 -0
  51. package/dist/commands/tenants.js +179 -0
  52. package/dist/commands/tenants.js.map +1 -0
  53. package/dist/commands/workspaces.d.ts +15 -0
  54. package/dist/commands/workspaces.js +72 -0
  55. package/dist/commands/workspaces.js.map +1 -0
  56. package/dist/index.js +761 -4
  57. package/dist/index.js.map +1 -1
  58. package/dist/lib/api-client.d.ts +18 -0
  59. package/dist/lib/api-client.js +124 -4
  60. package/dist/lib/api-client.js.map +1 -1
  61. package/dist/lib/cli-read.d.ts +13 -0
  62. package/dist/lib/cli-read.js +103 -0
  63. package/dist/lib/cli-read.js.map +1 -0
  64. package/dist/lib/cli-write.d.ts +47 -0
  65. package/dist/lib/cli-write.js +137 -0
  66. package/dist/lib/cli-write.js.map +1 -0
  67. package/dist/lib/config-schema.d.ts +10 -10
  68. package/dist/lib/render.d.ts +16 -0
  69. package/dist/lib/render.js +74 -0
  70. package/dist/lib/render.js.map +1 -0
  71. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -110,6 +110,8 @@ program
110
110
  .option('--artifact-url <url>', 'URL pública/signed do artifact (GCS, S3)')
111
111
  .option('--artifact-sha256 <sha>', 'sha256 do artifact (par com --artifact-url)')
112
112
  .option('--env-file <file>', 'arquivo .env com vars passadas pro deploy')
113
+ .option('--env <env>', 'ambiente alvo: dev | staging | prod', 'dev')
114
+ .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)')
113
115
  .option('--non-interactive', 'falha em vez de perguntar (modo CI)')
114
116
  .action(async (opts) => {
115
117
  const { runDeploy } = await import('./commands/deploy.js');
@@ -125,18 +127,560 @@ program
125
127
  artifactUrl: opts.artifactUrl,
126
128
  artifactSha256: opts.artifactSha256,
127
129
  envFile: opts.envFile,
130
+ env: opts.env,
131
+ localArtifact: !!opts.localArtifact,
128
132
  nonInteractive: !!opts.nonInteractive,
129
133
  });
130
134
  });
131
135
  // ── neetru status ─────────────────────────────────────────────────────
136
+ // Default: saúde das superfícies públicas (control plane). Com --client-id,
137
+ // delega ao status de workspace legado (backward-compat).
132
138
  program
133
139
  .command('status')
134
- .description('Mostra o status atual do workspace (build/deploy último, domínio, tier)')
135
- .option('--client-id <id>', 'oauthClientId do workspace (UI Core /workspaces/{id})')
140
+ .description('Saúde das superfícies públicas (landing/api/portal/políticas). --client-id = status de workspace')
141
+ .option('--client-id <id>', 'oauthClientId do workspace (status de workspace legado)')
136
142
  .option('--json', 'saída em JSON')
137
143
  .action(async (opts) => {
138
- const { runStatus } = await import('./commands/status.js');
139
- await runStatus({ clientId: opts.clientId, json: !!opts.json });
144
+ const { runSurfaceStatus } = await import('./commands/surface-status.js');
145
+ await runSurfaceStatus({ clientId: opts.clientId, json: !!opts.json });
146
+ });
147
+ // ── neetru tenants ────────────────────────────────────────────────────
148
+ const tenantsCmd = program
149
+ .command('tenants')
150
+ .description('Control plane — inspeção de tenants (read-only)');
151
+ tenantsCmd
152
+ .command('list')
153
+ .description('Lista tenants (filtros --status / --product)')
154
+ .option('--status <status>', 'ativo | em provisionamento | suspenso | arquivado')
155
+ .option('--product <productId>', 'filtra por productId')
156
+ .option('--limit <n>', 'máximo de resultados (default 100, max 500)')
157
+ .option('--json', 'saída em JSON')
158
+ .action(async (opts) => {
159
+ const { runTenantsList } = await import('./commands/tenants.js');
160
+ await runTenantsList({
161
+ status: opts.status,
162
+ product: opts.product,
163
+ limit: opts.limit,
164
+ json: !!opts.json,
165
+ });
166
+ });
167
+ tenantsCmd
168
+ .command('get <id>')
169
+ .description('Detalha um tenant pelo ID')
170
+ .option('--json', 'saída em JSON')
171
+ .action(async (id, opts) => {
172
+ const { runTenantsGet } = await import('./commands/tenants.js');
173
+ await runTenantsGet(id, { json: !!opts.json });
174
+ });
175
+ tenantsCmd
176
+ .command('create')
177
+ .description('Cria um tenant (status inicial: em provisionamento)')
178
+ .requiredOption('--name <name>', 'nome do tenant')
179
+ .requiredOption('--slug <slug>', 'slug do tenant')
180
+ .requiredOption('--customer <customerId>', 'id do customer')
181
+ .requiredOption('--product <productId>', 'id do produto')
182
+ .requiredOption('--env <env>', 'dev | staging | prod')
183
+ .option('--domain <domain>', 'domínio primário')
184
+ .option('--json', 'saída em JSON')
185
+ .action(async (opts) => {
186
+ const { runTenantsCreate } = await import('./commands/tenants.js');
187
+ await runTenantsCreate({
188
+ name: opts.name,
189
+ slug: opts.slug,
190
+ customer: opts.customer,
191
+ product: opts.product,
192
+ env: opts.env,
193
+ domain: opts.domain,
194
+ json: !!opts.json,
195
+ });
196
+ });
197
+ tenantsCmd
198
+ .command('update <id>')
199
+ .description('Atualiza dados de um tenant existente')
200
+ .requiredOption('--name <name>', 'nome do tenant')
201
+ .requiredOption('--slug <slug>', 'slug do tenant')
202
+ .requiredOption('--customer <customerId>', 'id do customer')
203
+ .requiredOption('--product <productId>', 'id do produto')
204
+ .requiredOption('--env <env>', 'dev | staging | prod')
205
+ .option('--domain <domain>', 'domínio primário')
206
+ .option('--json', 'saída em JSON')
207
+ .action(async (id, opts) => {
208
+ const { runTenantsUpdate } = await import('./commands/tenants.js');
209
+ await runTenantsUpdate(id, {
210
+ name: opts.name,
211
+ slug: opts.slug,
212
+ customer: opts.customer,
213
+ product: opts.product,
214
+ env: opts.env,
215
+ domain: opts.domain,
216
+ json: !!opts.json,
217
+ });
218
+ });
219
+ tenantsCmd
220
+ .command('suspend <id>')
221
+ .description('Suspende um tenant (destrutivo — confirmação interativa)')
222
+ .option('--yes', 'pula a confirmação interativa (modo script)')
223
+ .option('--force', 'alias de --yes')
224
+ .option('--json', 'saída em JSON')
225
+ .action(async (id, opts) => {
226
+ const { runTenantsSuspend } = await import('./commands/tenants.js');
227
+ await runTenantsSuspend(id, { yes: !!opts.yes, force: !!opts.force, json: !!opts.json });
228
+ });
229
+ tenantsCmd
230
+ .command('reactivate <id>')
231
+ .description('Reativa um tenant suspenso')
232
+ .option('--json', 'saída em JSON')
233
+ .action(async (id, opts) => {
234
+ const { runTenantsReactivate } = await import('./commands/tenants.js');
235
+ await runTenantsReactivate(id, { json: !!opts.json });
236
+ });
237
+ // ── neetru audit ──────────────────────────────────────────────────────
238
+ const auditCmd = program
239
+ .command('audit')
240
+ .description('Control plane — trilha de auditoria do Core (read-only)');
241
+ auditCmd
242
+ .command('tail')
243
+ .description('Últimos N eventos de audit_logs (mais recente primeiro)')
244
+ .option('-n, --limit <n>', 'número de eventos (default 50, max 200)')
245
+ .option('--action <substring>', 'filtra por substring da ação (ex: billing, tenant.create)')
246
+ .option('--severity <level>', 'info | warning | critical')
247
+ .option('--actor <substring>', 'filtra por uid ou email do ator')
248
+ .option('--json', 'saída em JSON')
249
+ .action(async (opts) => {
250
+ const { runAuditTail } = await import('./commands/audit.js');
251
+ await runAuditTail({
252
+ limit: opts.limit,
253
+ action: opts.action,
254
+ severity: opts.severity,
255
+ actor: opts.actor,
256
+ json: !!opts.json,
257
+ });
258
+ });
259
+ // ── neetru billing ────────────────────────────────────────────────────
260
+ const billingCmd = program
261
+ .command('billing')
262
+ .description('Control plane — billing/contabilidade (read-only)');
263
+ billingCmd
264
+ .command('summary')
265
+ .description('Sumário contábil do mês corrente (MRR, invoices, subscriptions)')
266
+ .option('--year <YYYY>', 'ano (default: corrente)')
267
+ .option('--month <1-12>', 'mês (default: corrente)')
268
+ .option('--json', 'saída em JSON')
269
+ .action(async (opts) => {
270
+ const { runBillingSummary } = await import('./commands/billing.js');
271
+ await runBillingSummary({ year: opts.year, month: opts.month, json: !!opts.json });
272
+ });
273
+ // ── neetru servers ────────────────────────────────────────────────────
274
+ const serversCmd = program
275
+ .command('servers')
276
+ .description('Control plane — inventário de servers (read-only)');
277
+ serversCmd
278
+ .command('list')
279
+ .description('Lista servers (filtros --status / --provider / --capacity)')
280
+ .option('--status <status>', 'online (operacional + heartbeat fresco)')
281
+ .option('--provider <provider>', 'hetzner | digitalocean | aws | ...')
282
+ .option('--capacity', 'inclui colunas de RAM/CPU/disco')
283
+ .option('--json', 'saída em JSON')
284
+ .action(async (opts) => {
285
+ const { runServersList } = await import('./commands/servers.js');
286
+ await runServersList({
287
+ status: opts.status,
288
+ provider: opts.provider,
289
+ capacity: !!opts.capacity,
290
+ json: !!opts.json,
291
+ });
292
+ });
293
+ serversCmd
294
+ .command('provision')
295
+ .description('Provisiona uma VM GCP (admin only) — cria server + registration token')
296
+ .requiredOption('--name <name>', 'nome do servidor')
297
+ .requiredOption('--zone <zone>', 'zona GCP (ex: us-central1-a)')
298
+ .requiredOption('--machine-type <type>', 'machine type GCP (ex: e2-small)')
299
+ .option('--tenant <tenantId>', 'tenant associado')
300
+ .option('--customer <customerId>', 'customer associado')
301
+ .option('--json', 'saída em JSON')
302
+ .action(async (opts) => {
303
+ const { runServersProvision } = await import('./commands/servers.js');
304
+ await runServersProvision({
305
+ name: opts.name,
306
+ zone: opts.zone,
307
+ machineType: opts.machineType,
308
+ tenant: opts.tenant,
309
+ customer: opts.customer,
310
+ json: !!opts.json,
311
+ });
312
+ });
313
+ serversCmd
314
+ .command('deactivate <serverId>')
315
+ .description('Desativa um server (destrutivo top-tier — confirmação + step-up MFA)')
316
+ .option('--yes', 'pula a confirmação interativa (modo script)')
317
+ .option('--force', 'alias de --yes')
318
+ .option('--dry-run', 'valida e mostra o efeito sem aplicar')
319
+ .option('--mfa-token <code>', 'código TOTP para step-up MFA')
320
+ .option('--json', 'saída em JSON')
321
+ .action(async (id, opts) => {
322
+ const { runServersDeactivate } = await import('./commands/servers.js');
323
+ await runServersDeactivate(id, {
324
+ yes: !!opts.yes,
325
+ force: !!opts.force,
326
+ dryRun: !!opts.dryRun,
327
+ mfaToken: opts.mfaToken,
328
+ json: !!opts.json,
329
+ });
330
+ });
331
+ serversCmd
332
+ .command('dispatch <serverId> <commandType>')
333
+ .description('Enfileira um comando pro agente Linux do server')
334
+ .option('--params <json>', 'objeto JSON de params do comando')
335
+ .option('--json', 'saída em JSON')
336
+ .action(async (id, commandType, opts) => {
337
+ const { runServersDispatch } = await import('./commands/servers.js');
338
+ await runServersDispatch(id, commandType, { params: opts.params, json: !!opts.json });
339
+ });
340
+ // ── neetru workspaces ─────────────────────────────────────────────────
341
+ const workspacesCmd = program
342
+ .command('workspaces')
343
+ .description('Control plane — runtime shared de workspaces');
344
+ workspacesCmd
345
+ .command('create')
346
+ .description('Cria um workspace (devolve OAuth client secret one-time)')
347
+ .requiredOption('--product <productId>', 'id do produto')
348
+ .requiredOption('--customer <customerId>', 'id do customer')
349
+ .requiredOption('--env <env>', 'dev | staging | prod')
350
+ .requiredOption('--tier <tier>', 'dev | standard | enterprise')
351
+ .option('--name <name>', 'label do workspace')
352
+ .option('--json', 'saída em JSON')
353
+ .action(async (opts) => {
354
+ const { runWorkspacesCreate } = await import('./commands/workspaces.js');
355
+ await runWorkspacesCreate({
356
+ product: opts.product,
357
+ customer: opts.customer,
358
+ env: opts.env,
359
+ tier: opts.tier,
360
+ name: opts.name,
361
+ json: !!opts.json,
362
+ });
363
+ });
364
+ workspacesCmd
365
+ .command('advance <workspaceId>')
366
+ .description('Promove uma versão de bundle pra "running" no workspace')
367
+ .requiredOption('--product <slug>', 'productSlug do bundle')
368
+ .requiredOption('--version <version>', 'versão do bundle a ativar')
369
+ .option('--json', 'saída em JSON')
370
+ .action(async (id, opts) => {
371
+ const { runWorkspacesAdvance } = await import('./commands/workspaces.js');
372
+ await runWorkspacesAdvance(id, {
373
+ product: opts.product,
374
+ version: opts.version,
375
+ json: !!opts.json,
376
+ });
377
+ });
378
+ // ── neetru deployments ────────────────────────────────────────────────
379
+ const deploymentsCmd = program
380
+ .command('deployments')
381
+ .description('Control plane — recurso deployments/ (paridade com a UI)');
382
+ deploymentsCmd
383
+ .command('create')
384
+ .description('Cria um deployment (dispara comando deploy pro agente)')
385
+ .requiredOption('--product <productId>', 'id do produto')
386
+ .requiredOption('--tenant <tenantId>', 'id do tenant alvo')
387
+ .requiredOption('--version <version>', 'versão a deployar')
388
+ .requiredOption('--env <env>', 'dev | staging | prod')
389
+ .requiredOption('--server <serverId>', 'id do server alvo')
390
+ .option('--json', 'saída em JSON')
391
+ .action(async (opts) => {
392
+ const { runDeploymentsCreate } = await import('./commands/deployments.js');
393
+ await runDeploymentsCreate({
394
+ product: opts.product,
395
+ tenant: opts.tenant,
396
+ version: opts.version,
397
+ env: opts.env,
398
+ server: opts.server,
399
+ json: !!opts.json,
400
+ });
401
+ });
402
+ deploymentsCmd
403
+ .command('rollback <deploymentId>')
404
+ .description('Rollback de um deployment (destrutivo top-tier — confirmação + step-up MFA)')
405
+ .option('--yes', 'pula a confirmação interativa (modo script)')
406
+ .option('--force', 'alias de --yes')
407
+ .option('--dry-run', 'valida e mostra o efeito sem aplicar')
408
+ .option('--mfa-token <code>', 'código TOTP para step-up MFA')
409
+ .option('--json', 'saída em JSON')
410
+ .action(async (id, opts) => {
411
+ const { runDeploymentsRollback } = await import('./commands/deployments.js');
412
+ await runDeploymentsRollback(id, {
413
+ yes: !!opts.yes,
414
+ force: !!opts.force,
415
+ dryRun: !!opts.dryRun,
416
+ mfaToken: opts.mfaToken,
417
+ json: !!opts.json,
418
+ });
419
+ });
420
+ // ── neetru cloud-run ──────────────────────────────────────────────────
421
+ const cloudRunCmd = program
422
+ .command('cloud-run')
423
+ .description('Control plane — serviços Cloud Run (admin only)');
424
+ cloudRunCmd
425
+ .command('pause <service>')
426
+ .description('Pausa um serviço Cloud Run (scale-to-zero)')
427
+ .option('--json', 'saída em JSON')
428
+ .action(async (service, opts) => {
429
+ const { runCloudRunPause } = await import('./commands/cloud-run.js');
430
+ await runCloudRunPause(service, { json: !!opts.json });
431
+ });
432
+ cloudRunCmd
433
+ .command('resume <service>')
434
+ .description('Retoma um serviço Cloud Run pausado')
435
+ .option('--min-instances <n>', 'minInstanceCount')
436
+ .option('--max-instances <n>', 'maxInstanceCount')
437
+ .option('--json', 'saída em JSON')
438
+ .action(async (service, opts) => {
439
+ const { runCloudRunResume } = await import('./commands/cloud-run.js');
440
+ await runCloudRunResume(service, {
441
+ minInstances: opts.minInstances,
442
+ maxInstances: opts.maxInstances,
443
+ json: !!opts.json,
444
+ });
445
+ });
446
+ cloudRunCmd
447
+ .command('delete <service>')
448
+ .description('Exclui um serviço Cloud Run (IRREVERSÍVEL — confirmação + step-up MFA)')
449
+ .option('--yes', 'pula a confirmação interativa (modo script)')
450
+ .option('--force', 'alias de --yes')
451
+ .option('--dry-run', 'valida e mostra o efeito sem aplicar')
452
+ .option('--mfa-token <code>', 'código TOTP para step-up MFA')
453
+ .option('--json', 'saída em JSON')
454
+ .action(async (service, opts) => {
455
+ const { runCloudRunDelete } = await import('./commands/cloud-run.js');
456
+ await runCloudRunDelete(service, {
457
+ yes: !!opts.yes,
458
+ force: !!opts.force,
459
+ dryRun: !!opts.dryRun,
460
+ mfaToken: opts.mfaToken,
461
+ json: !!opts.json,
462
+ });
463
+ });
464
+ // ── neetru api-catalog ────────────────────────────────────────────────
465
+ const apiCatalogCmd = program
466
+ .command('api-catalog')
467
+ .description('Control plane — registry de APIs versionadas (apis/)');
468
+ apiCatalogCmd
469
+ .command('create <slug>')
470
+ .description('Registra uma nova API no catálogo')
471
+ .requiredOption('--name <name>', 'nome da API')
472
+ .requiredOption('--base-url <url>', 'base URL')
473
+ .requiredOption('--version <version>', 'versão')
474
+ .requiredOption('--auth-method <method>', 'método de auth')
475
+ .requiredOption('--status <status>', 'status (ex: active | deprecated)')
476
+ .option('--description <text>', 'descrição')
477
+ .option('--docs-url <url>', 'URL da documentação')
478
+ .option('--owner-team <team>', 'time responsável')
479
+ .option('--contact-email <email>', 'email de contato')
480
+ .option('--scopes <csv>', 'scopes obrigatórios (separados por vírgula)')
481
+ .option('--json', 'saída em JSON')
482
+ .action(async (slug, opts) => {
483
+ const { runApiCatalogCreate } = await import('./commands/api-catalog.js');
484
+ await runApiCatalogCreate(slug, {
485
+ name: opts.name,
486
+ baseUrl: opts.baseUrl,
487
+ version: opts.version,
488
+ authMethod: opts.authMethod,
489
+ status: opts.status,
490
+ description: opts.description,
491
+ docsUrl: opts.docsUrl,
492
+ ownerTeam: opts.ownerTeam,
493
+ contactEmail: opts.contactEmail,
494
+ scopes: opts.scopes,
495
+ json: !!opts.json,
496
+ });
497
+ });
498
+ apiCatalogCmd
499
+ .command('update <slug>')
500
+ .description('Atualiza uma entrada do catálogo (slug é imutável)')
501
+ .requiredOption('--name <name>', 'nome da API')
502
+ .requiredOption('--base-url <url>', 'base URL')
503
+ .requiredOption('--version <version>', 'versão')
504
+ .requiredOption('--auth-method <method>', 'método de auth')
505
+ .requiredOption('--status <status>', 'status (ex: active | deprecated)')
506
+ .option('--description <text>', 'descrição')
507
+ .option('--docs-url <url>', 'URL da documentação')
508
+ .option('--owner-team <team>', 'time responsável')
509
+ .option('--contact-email <email>', 'email de contato')
510
+ .option('--scopes <csv>', 'scopes obrigatórios (separados por vírgula)')
511
+ .option('--json', 'saída em JSON')
512
+ .action(async (slug, opts) => {
513
+ const { runApiCatalogUpdate } = await import('./commands/api-catalog.js');
514
+ await runApiCatalogUpdate(slug, {
515
+ name: opts.name,
516
+ baseUrl: opts.baseUrl,
517
+ version: opts.version,
518
+ authMethod: opts.authMethod,
519
+ status: opts.status,
520
+ description: opts.description,
521
+ docsUrl: opts.docsUrl,
522
+ ownerTeam: opts.ownerTeam,
523
+ contactEmail: opts.contactEmail,
524
+ scopes: opts.scopes,
525
+ json: !!opts.json,
526
+ });
527
+ });
528
+ apiCatalogCmd
529
+ .command('archive <slug>')
530
+ .description('Soft-delete de uma API (--unarchive reativa)')
531
+ .option('--unarchive', 'reativa em vez de arquivar')
532
+ .option('--yes', 'pula a confirmação interativa (modo script)')
533
+ .option('--force', 'alias de --yes')
534
+ .option('--json', 'saída em JSON')
535
+ .action(async (slug, opts) => {
536
+ const { runApiCatalogArchive } = await import('./commands/api-catalog.js');
537
+ await runApiCatalogArchive(slug, {
538
+ unarchive: !!opts.unarchive,
539
+ yes: !!opts.yes,
540
+ force: !!opts.force,
541
+ json: !!opts.json,
542
+ });
543
+ });
544
+ apiCatalogCmd
545
+ .command('delete <slug>')
546
+ .description('Delete físico de uma API (IRREVERSÍVEL — confirmação + step-up MFA)')
547
+ .option('--yes', 'pula a confirmação interativa (modo script)')
548
+ .option('--force', 'alias de --yes')
549
+ .option('--dry-run', 'valida e mostra o efeito sem aplicar')
550
+ .option('--mfa-token <code>', 'código TOTP para step-up MFA')
551
+ .option('--json', 'saída em JSON')
552
+ .action(async (slug, opts) => {
553
+ const { runApiCatalogDelete } = await import('./commands/api-catalog.js');
554
+ await runApiCatalogDelete(slug, {
555
+ yes: !!opts.yes,
556
+ force: !!opts.force,
557
+ dryRun: !!opts.dryRun,
558
+ mfaToken: opts.mfaToken,
559
+ json: !!opts.json,
560
+ });
561
+ });
562
+ // ── neetru products ───────────────────────────────────────────────────
563
+ const productsCmd = program
564
+ .command('products')
565
+ .description('Control plane — registry interno de produtos SaaS (read-only)');
566
+ productsCmd
567
+ .command('list')
568
+ .description('Lista produtos internos (registry products/, filtro --status)')
569
+ .option('--status <status>', 'ativo | beta | descontinuado')
570
+ .option('--limit <n>', 'máximo de resultados (default 100, max 500)')
571
+ .option('--json', 'saída em JSON')
572
+ .action(async (opts) => {
573
+ const { runProductsList } = await import('./commands/products.js');
574
+ await runProductsList({ status: opts.status, limit: opts.limit, json: !!opts.json });
575
+ });
576
+ productsCmd
577
+ .command('publish <slug>')
578
+ .description('Publica um produto no catálogo público (published=true)')
579
+ .option('--json', 'saída em JSON')
580
+ .action(async (slug, opts) => {
581
+ const { runProductsPublish } = await import('./commands/products.js');
582
+ await runProductsPublish(slug, { json: !!opts.json });
583
+ });
584
+ productsCmd
585
+ .command('unpublish <slug>')
586
+ .description('Remove um produto do catálogo público (published=false)')
587
+ .option('--json', 'saída em JSON')
588
+ .action(async (slug, opts) => {
589
+ const { runProductsUnpublish } = await import('./commands/products.js');
590
+ await runProductsUnpublish(slug, { json: !!opts.json });
591
+ });
592
+ // ── neetru products db (Phase A — per-product DB isolation) ────────────
593
+ const productsDbCmd = productsCmd
594
+ .command('db')
595
+ .description('Bancos de dados isolados por produto (firestore-instance | cloud-sql-* | vm-*)');
596
+ productsDbCmd
597
+ .command('list')
598
+ .description('Lista bancos. Filtros: --product-id, --engine, --status')
599
+ .option('--product-id <id>')
600
+ .option('--engine <engine>')
601
+ .option('--status <status>')
602
+ .option('--json')
603
+ .action(async (opts) => {
604
+ const { runDbList } = await import('./commands/products-db.js');
605
+ await runDbList({
606
+ productId: opts.productId,
607
+ engine: opts.engine,
608
+ status: opts.status,
609
+ json: !!opts.json,
610
+ });
611
+ });
612
+ productsDbCmd
613
+ .command('engines')
614
+ .description('Lista engines suportados')
615
+ .option('--json')
616
+ .action(async (opts) => {
617
+ const { runDbEngines } = await import('./commands/products-db.js');
618
+ await runDbEngines({ json: !!opts.json });
619
+ });
620
+ productsDbCmd
621
+ .command('create')
622
+ .description('Cria um banco (Phase A: registra + audit; provisionamento real é Phase B)')
623
+ .requiredOption('--product-id <id>')
624
+ .requiredOption('--label <label>')
625
+ .requiredOption('--engine <engine>', 'firestore-instance|cloud-sql-postgres|cloud-sql-mysql|vm-postgres-single|vm-postgres-cluster|vm-mysql-single|vm-mysql-cluster')
626
+ .requiredOption('--environment <env>', 'dev|staging|production')
627
+ .option('--region <region>', 'região GCP', 'us-central1')
628
+ .option('--server-id <serverId>', 'target VM (obrigatório pra engines vm-*)')
629
+ .option('--replica-count <n>', 'pra engines *-cluster (default 2)')
630
+ .option('--json')
631
+ .action(async (opts) => {
632
+ const { runDbCreate } = await import('./commands/products-db.js');
633
+ await runDbCreate({
634
+ productId: opts.productId,
635
+ label: opts.label,
636
+ engine: opts.engine,
637
+ environment: opts.environment,
638
+ region: opts.region,
639
+ serverId: opts.serverId,
640
+ replicaCount: opts.replicaCount,
641
+ json: !!opts.json,
642
+ });
643
+ });
644
+ productsDbCmd
645
+ .command('get <id>')
646
+ .description('Detalhes de um banco')
647
+ .option('--json')
648
+ .action(async (id, opts) => {
649
+ const { runDbGet } = await import('./commands/products-db.js');
650
+ await runDbGet(id, { json: !!opts.json });
651
+ });
652
+ productsDbCmd
653
+ .command('status <id> <status>')
654
+ .description('Atualiza status (requested|provisioning|active|degraded|failed|archived)')
655
+ .option('--reason <text>')
656
+ .option('--json')
657
+ .action(async (id, status, opts) => {
658
+ const { runDbStatus } = await import('./commands/products-db.js');
659
+ await runDbStatus(id, status, { reason: opts.reason, json: !!opts.json });
660
+ });
661
+ productsDbCmd
662
+ .command('retry <id>')
663
+ .description('Re-enfileira provisionamento (status failed/degraded → requested)')
664
+ .option('--json')
665
+ .action(async (id, opts) => {
666
+ const { runDbRetry } = await import('./commands/products-db.js');
667
+ await runDbRetry(id, { json: !!opts.json });
668
+ });
669
+ productsDbCmd
670
+ .command('rotate <id>')
671
+ .description('Rotaciona credenciais (admin; Phase A: intent + audit)')
672
+ .option('--json')
673
+ .action(async (id, opts) => {
674
+ const { runDbRotate } = await import('./commands/products-db.js');
675
+ await runDbRotate(id, { json: !!opts.json });
676
+ });
677
+ productsDbCmd
678
+ .command('delete <id>')
679
+ .description('Arquiva o banco (soft-delete; admin)')
680
+ .option('--json')
681
+ .action(async (id, opts) => {
682
+ const { runDbDelete } = await import('./commands/products-db.js');
683
+ await runDbDelete(id, { json: !!opts.json });
140
684
  });
141
685
  // ── neetru logs ───────────────────────────────────────────────────────
142
686
  program
@@ -332,6 +876,219 @@ program
332
876
  const { runAutocomplete } = await import('./commands/autocomplete.js');
333
877
  await runAutocomplete(shell);
334
878
  });
879
+ // ── neetru agent release ──────────────────────────────────────────────
880
+ const agentCmd = program
881
+ .command('agent')
882
+ .description('Operações sobre o binário do Neetru Agent (Linux daemon)');
883
+ agentCmd
884
+ .command('release')
885
+ .description('Registra nova release do agente em agent_releases (Firestore)')
886
+ .requiredOption('--version <semver>', 'versão (ex: 1.2.0 ou 1.2.0-beta.1)')
887
+ .option('--channel <channel>', 'stable | beta | canary (default: beta)', 'beta')
888
+ .option('--changelog <text|@file>', 'changelog markdown inline ou "@path" pra ler de arquivo')
889
+ .option('--min-core <revision>', 'min revision Cloud Run requerida (ex: 00037-qm8)')
890
+ .option('--binary <arch=path>', 'binário local (sha256+size computados, URL canônica GCS auto-derivada). Repetível.', (val, prev = []) => [...prev, val])
891
+ .option('--artifact <arch=URL@sha256@sizeBytes>', 'artifact com URL/sha256/size explícitos (não toca FS). Repetível.', (val, prev = []) => [...prev, val])
892
+ .option('--dry-run', 'imprime payload sem enviar')
893
+ .action(async (opts) => {
894
+ const { runAgentRelease } = await import('./commands/agent-release.js');
895
+ await runAgentRelease({
896
+ version: opts.version,
897
+ channel: opts.channel,
898
+ changelog: opts.changelog,
899
+ minCore: opts.minCore,
900
+ binary: opts.binary,
901
+ artifact: opts.artifact,
902
+ dryRun: !!opts.dryRun,
903
+ });
904
+ });
905
+ agentCmd
906
+ .command('yank <version>')
907
+ .description('Soft-revoke de uma release do agente (destrutivo top-tier — confirmação + step-up MFA)')
908
+ .requiredOption('--reason <text>', 'motivo do yank (mínimo 5 caracteres)')
909
+ .option('--yes', 'pula a confirmação interativa (modo script)')
910
+ .option('--force', 'alias de --yes')
911
+ .option('--dry-run', 'valida e mostra o efeito sem aplicar')
912
+ .option('--mfa-token <code>', 'código TOTP para step-up MFA')
913
+ .option('--json', 'saída em JSON')
914
+ .action(async (version, opts) => {
915
+ const { runAgentYank } = await import('./commands/agent-write.js');
916
+ await runAgentYank(version, {
917
+ reason: opts.reason,
918
+ yes: !!opts.yes,
919
+ force: !!opts.force,
920
+ dryRun: !!opts.dryRun,
921
+ mfaToken: opts.mfaToken,
922
+ json: !!opts.json,
923
+ });
924
+ });
925
+ const agentCanaryCmd = agentCmd
926
+ .command('canary')
927
+ .description('Controla o canary rollout de releases do agente');
928
+ agentCanaryCmd
929
+ .command('start <version>')
930
+ .description('Inicia o canary rollout de uma release (phase1 = 5%)')
931
+ .option('--json', 'saída em JSON')
932
+ .action(async (version, opts) => {
933
+ const { runAgentCanaryStart } = await import('./commands/agent-write.js');
934
+ await runAgentCanaryStart(version, { json: !!opts.json });
935
+ });
936
+ agentCanaryCmd
937
+ .command('rollback <version>')
938
+ .description('Rollback do canary de uma release (destrutivo top-tier — confirmação + step-up MFA)')
939
+ .requiredOption('--reason <text>', 'motivo do rollback (mínimo 5 caracteres)')
940
+ .option('--yes', 'pula a confirmação interativa (modo script)')
941
+ .option('--force', 'alias de --yes')
942
+ .option('--dry-run', 'valida e mostra o efeito sem aplicar')
943
+ .option('--mfa-token <code>', 'código TOTP para step-up MFA')
944
+ .option('--json', 'saída em JSON')
945
+ .action(async (version, opts) => {
946
+ const { runAgentCanaryRollback } = await import('./commands/agent-write.js');
947
+ await runAgentCanaryRollback(version, {
948
+ reason: opts.reason,
949
+ yes: !!opts.yes,
950
+ force: !!opts.force,
951
+ dryRun: !!opts.dryRun,
952
+ mfaToken: opts.mfaToken,
953
+ json: !!opts.json,
954
+ });
955
+ });
956
+ // ── neetru support ───────────────────────────────────────────────────
957
+ const supportCmd = program
958
+ .command('support')
959
+ .description('Support tickets — inbox staff via CLI (F5 §4.2.6)');
960
+ const supportTicketsCmd = supportCmd
961
+ .command('tickets')
962
+ .description('Operações sobre support_tickets');
963
+ supportTicketsCmd
964
+ .command('list')
965
+ .description('Lista tickets com filtros')
966
+ .option('--status <s>', 'open | in_progress | resolved | closed')
967
+ .option('--severity <s>', 'sev1 | sev2 | sev3 | sev4')
968
+ .option('--product <id>', 'filtra por productId')
969
+ .option('--customer <id>', 'filtra por customerId/tenantId')
970
+ .option('--breached', 'apenas tickets com SLA estourado')
971
+ .option('--limit <n>', 'máximo de resultados (default 100, max 500)')
972
+ .option('--json', 'saída em JSON')
973
+ .action(async (opts) => {
974
+ const { runSupportTicketsList } = await import('./commands/support.js');
975
+ await runSupportTicketsList({
976
+ status: opts.status,
977
+ severity: opts.severity,
978
+ product: opts.product,
979
+ customer: opts.customer,
980
+ breached: !!opts.breached,
981
+ limit: opts.limit,
982
+ json: !!opts.json,
983
+ });
984
+ });
985
+ supportTicketsCmd
986
+ .command('describe <id>')
987
+ .description('Detalha um ticket + thread de mensagens')
988
+ .option('--json', 'saída em JSON')
989
+ .action(async (id, opts) => {
990
+ const { runSupportTicketsDescribe } = await import('./commands/support.js');
991
+ await runSupportTicketsDescribe(id, { json: !!opts.json });
992
+ });
993
+ supportTicketsCmd
994
+ .command('reply <id>')
995
+ .description('Anexa mensagem staff ao ticket (transiciona open → in_progress se aplicável)')
996
+ .requiredOption('--message <text>', 'corpo da mensagem')
997
+ .option('--json', 'saída em JSON')
998
+ .action(async (id, opts) => {
999
+ const { runSupportTicketsReply } = await import('./commands/support.js');
1000
+ await runSupportTicketsReply(id, { message: opts.message, json: !!opts.json });
1001
+ });
1002
+ supportTicketsCmd
1003
+ .command('assign <id>')
1004
+ .description('Atribui ticket a um staff (uid)')
1005
+ .requiredOption('--to <staffUid>', 'uid do staff destino')
1006
+ .option('--json', 'saída em JSON')
1007
+ .action(async (id, opts) => {
1008
+ const { runSupportTicketsAssign } = await import('./commands/support.js');
1009
+ await runSupportTicketsAssign(id, { to: opts.to, json: !!opts.json });
1010
+ });
1011
+ supportTicketsCmd
1012
+ .command('status <id>')
1013
+ .description('Transiciona status do ticket (FSM)')
1014
+ .requiredOption('--to <status>', 'open | in_progress | resolved | closed')
1015
+ .option('--json', 'saída em JSON')
1016
+ .action(async (id, opts) => {
1017
+ const { runSupportTicketsStatus } = await import('./commands/support.js');
1018
+ await runSupportTicketsStatus(id, { to: opts.to, json: !!opts.json });
1019
+ });
1020
+ // ── neetru dns ───────────────────────────────────────────────────────
1021
+ const dnsCmd = program.command('dns').description('Cloud DNS — managed zones');
1022
+ dnsCmd
1023
+ .command('zones')
1024
+ .description('Operações sobre managed zones')
1025
+ .command('list')
1026
+ .description('Lista managed zones do projeto')
1027
+ .option('--json', 'saída em JSON')
1028
+ .action(async (opts) => {
1029
+ const { runDnsZonesList } = await import('./commands/infra-read.js');
1030
+ await runDnsZonesList({ json: !!opts.json });
1031
+ });
1032
+ // ── neetru hosting ───────────────────────────────────────────────────
1033
+ program
1034
+ .command('hosting')
1035
+ .description('Customer domains — hosting setup')
1036
+ .command('list')
1037
+ .description('Lista customer domains com scope de tenant aplicado')
1038
+ .option('--json', 'saída em JSON')
1039
+ .action(async (opts) => {
1040
+ const { runHostingList } = await import('./commands/infra-read.js');
1041
+ await runHostingList({ json: !!opts.json });
1042
+ });
1043
+ // ── neetru builds ────────────────────────────────────────────────────
1044
+ program
1045
+ .command('builds')
1046
+ .description('Cloud Build — builds live (status + duração)')
1047
+ .command('list')
1048
+ .description('Lista Cloud Builds recentes (global + regional)')
1049
+ .option('--json', 'saída em JSON')
1050
+ .action(async (opts) => {
1051
+ const { runBuildsList } = await import('./commands/infra-read.js');
1052
+ await runBuildsList({ json: !!opts.json });
1053
+ });
1054
+ // ── neetru dr ────────────────────────────────────────────────────────
1055
+ // Disaster Recovery — listar exports + restore assistido (admin + step-up MFA).
1056
+ const drCmd = program
1057
+ .command('dr')
1058
+ .description('Disaster Recovery — exports + restore (admin only, RUNBOOK_DR_DRILL)');
1059
+ const drExportsCmd = drCmd
1060
+ .command('exports')
1061
+ .description('Operações sobre exports de Firestore em gs://neetru-backups');
1062
+ drExportsCmd
1063
+ .command('list')
1064
+ .description('Lista exports disponíveis (sort: mais recente primeiro)')
1065
+ .option('--json', 'saída em JSON')
1066
+ .action(async (opts) => {
1067
+ const { runDrExportsList } = await import('./commands/dr.js');
1068
+ await runDrExportsList({ json: !!opts.json });
1069
+ });
1070
+ drCmd
1071
+ .command('restore')
1072
+ .description('Dispara restore Firestore (IRREVERSÍVEL no destino — destrutivo top-tier)')
1073
+ .requiredOption('--gcs-path <path>', 'gs://neetru-backups/firestore-exports/<stamp>/')
1074
+ .requiredOption('--target-project <project>', 'projeto GCP de destino (deve ser STAGING)')
1075
+ .option('--yes', 'pula a confirmação interativa (modo script)')
1076
+ .option('--force', 'alias de --yes')
1077
+ .option('--dry-run', 'valida e mostra o efeito sem aplicar')
1078
+ .option('--mfa-token <code>', 'código TOTP para step-up MFA')
1079
+ .option('--json', 'saída em JSON')
1080
+ .action(async (opts) => {
1081
+ const { runDrRestore } = await import('./commands/dr.js');
1082
+ await runDrRestore({
1083
+ gcsPath: opts.gcsPath,
1084
+ targetProject: opts.targetProject,
1085
+ yes: !!opts.yes,
1086
+ force: !!opts.force,
1087
+ dryRun: !!opts.dryRun,
1088
+ mfaToken: opts.mfaToken,
1089
+ json: !!opts.json,
1090
+ });
1091
+ });
335
1092
  // ── parse ─────────────────────────────────────────────────────────────
336
1093
  program.parse(process.argv);
337
1094
  if (!process.argv.slice(2).length) {