@neetru/cli 2.0.0 → 2.1.1

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