@neetru/cli 2.8.0 → 2.9.3

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 (44) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/commands/admin/sdk-credential.d.ts +19 -0
  3. package/dist/commands/admin/sdk-credential.js +169 -0
  4. package/dist/commands/admin/sdk-credential.js.map +1 -0
  5. package/dist/commands/bug.d.ts +87 -0
  6. package/dist/commands/bug.js +419 -0
  7. package/dist/commands/bug.js.map +1 -0
  8. package/dist/commands/customers.d.ts +17 -0
  9. package/dist/commands/customers.js +160 -0
  10. package/dist/commands/customers.js.map +1 -0
  11. package/dist/commands/db.d.ts +4 -0
  12. package/dist/commands/db.js +380 -64
  13. package/dist/commands/db.js.map +1 -1
  14. package/dist/commands/docs.d.ts +4 -0
  15. package/dist/commands/docs.js +99 -7
  16. package/dist/commands/docs.js.map +1 -1
  17. package/dist/commands/doctor.js +4 -1
  18. package/dist/commands/doctor.js.map +1 -1
  19. package/dist/commands/marketplace.d.ts +36 -0
  20. package/dist/commands/marketplace.js +584 -0
  21. package/dist/commands/marketplace.js.map +1 -0
  22. package/dist/commands/new.js +196 -37
  23. package/dist/commands/new.js.map +1 -1
  24. package/dist/commands/open.d.ts +8 -0
  25. package/dist/commands/open.js +61 -13
  26. package/dist/commands/open.js.map +1 -1
  27. package/dist/commands/products.d.ts +23 -0
  28. package/dist/commands/products.js +39 -1
  29. package/dist/commands/products.js.map +1 -1
  30. package/dist/commands/tenants.js +24 -2
  31. package/dist/commands/tenants.js.map +1 -1
  32. package/dist/commands/ui.d.ts +1 -1
  33. package/dist/commands/ui.js +172 -2
  34. package/dist/commands/ui.js.map +1 -1
  35. package/dist/commands/workspaces.d.ts +10 -1
  36. package/dist/commands/workspaces.js +145 -23
  37. package/dist/commands/workspaces.js.map +1 -1
  38. package/dist/index.js +327 -4
  39. package/dist/index.js.map +1 -1
  40. package/dist/lib/config-schema.d.ts +10 -10
  41. package/dist/lib/pickers.d.ts +25 -0
  42. package/dist/lib/pickers.js +75 -1
  43. package/dist/lib/pickers.js.map +1 -1
  44. package/package.json +5 -2
package/dist/index.js CHANGED
@@ -2,6 +2,16 @@
2
2
  import { Command } from 'commander';
3
3
  import { log } from './utils/logger.js';
4
4
  import { CLI_VERSION } from './version.js';
5
+ // pdv #6 fix: fecha stdin antes de qualquer process.exit() para evitar a
6
+ // asserção libuv "!(handle->flags & UV_HANDLE_CLOSING)" no Windows.
7
+ // stdin fica aberto como handle libuv quando inquirer/ora o pegam; destruir
8
+ // antes do exit garante que o loop de eventos drene limpo.
9
+ process.on('exit', () => {
10
+ try {
11
+ process.stdin.destroy();
12
+ }
13
+ catch { /* silencioso — handle pode já estar fechado */ }
14
+ });
5
15
  const program = new Command();
6
16
  program
7
17
  .name('neetru')
@@ -239,6 +249,47 @@ program
239
249
  const { runSurfaceStatus } = await import('./commands/surface-status.js');
240
250
  await runSurfaceStatus({ clientId: opts.clientId, json: !!opts.json });
241
251
  });
252
+ // ── neetru customers ──────────────────────────────────────────────────
253
+ // pdv #2 fix: operadores podem criar customers pelo CLI sem abrir o painel.
254
+ // Customer = empresa/PJ que paga (CRM comercial).
255
+ // Tenant = instância do produto para esse customer.
256
+ const customersCmd = program
257
+ .command('customers')
258
+ .description('Gerenciar customers (empresa/PJ que paga o serviço Neetru)');
259
+ customersCmd
260
+ .command('list')
261
+ .description('Listar customers')
262
+ .option('--status <status>', 'ativo | em prospecção | suspenso | arquivado')
263
+ .option('--search <texto>', 'busca por nome, email ou CNPJ')
264
+ .option('--json', 'saída em JSON')
265
+ .action(async (opts) => {
266
+ const { runCustomersList } = await import('./commands/customers.js');
267
+ await runCustomersList({ status: opts.status, search: opts.search, json: !!opts.json });
268
+ });
269
+ customersCmd
270
+ .command('get <id>')
271
+ .description('Mostrar detalhes de um customer')
272
+ .option('--json', 'saída em JSON')
273
+ .action(async (id, opts) => {
274
+ const { runCustomersGet } = await import('./commands/customers.js');
275
+ await runCustomersGet(id, { json: !!opts.json });
276
+ });
277
+ customersCmd
278
+ .command('create')
279
+ .description('Criar customer (empresa/PJ) — necessário antes de criar workspace')
280
+ .option('--name <nome>', 'nome do customer (razão social ou nome comercial)')
281
+ .option('--email <email>', 'email de contato')
282
+ .option('--tax-id <cnpj>', 'CNPJ ou CPF (opcional)')
283
+ .option('--json', 'saída em JSON')
284
+ .action(async (opts) => {
285
+ const { runCustomersCreate } = await import('./commands/customers.js');
286
+ await runCustomersCreate({
287
+ name: opts.name,
288
+ email: opts.email,
289
+ taxId: opts.taxId,
290
+ json: !!opts.json,
291
+ });
292
+ });
242
293
  // ── neetru tenants ────────────────────────────────────────────────────
243
294
  const tenantsCmd = program
244
295
  .command('tenants')
@@ -269,7 +320,10 @@ tenantsCmd
269
320
  });
270
321
  tenantsCmd
271
322
  .command('create')
272
- .description('Criar tenant (status inicial: em provisionamento)')
323
+ .description('[STAFF-ONLY] Criar tenant sem OAuth client/secret.\n' +
324
+ 'DEPRECATED: para uso normal (ambiente completo com OAuth), use:\n' +
325
+ ' neetru workspaces create\n' +
326
+ 'Este comando permanece para casos staff-only (registro de tenant pré-existente).')
273
327
  .option('--name <name>', 'nome do tenant')
274
328
  .option('--slug <slug>', 'slug do tenant')
275
329
  .option('--customer <customerId>', 'id do customer')
@@ -450,6 +504,8 @@ workspacesCmd
450
504
  .option('--env <env>', 'dev | staging | prod (default: dev)')
451
505
  .option('--tier <tier>', 'dev | standard | enterprise (default: standard)')
452
506
  .option('--name <name>', 'label do workspace')
507
+ .option('--bind-config', 'grava tenantId retornado no neetru.config.json do cwd')
508
+ .option('--force-duplicate', 'cria mesmo que já exista ambiente ativo para produto+env')
453
509
  .option('--json', 'saída em JSON')
454
510
  .action(async (opts) => {
455
511
  const { runWorkspacesCreate } = await import('./commands/workspaces.js');
@@ -459,6 +515,8 @@ workspacesCmd
459
515
  env: opts.env,
460
516
  tier: opts.tier,
461
517
  name: opts.name,
518
+ bindConfig: !!opts.bindConfig,
519
+ forceDuplicate: !!opts.forceDuplicate,
462
520
  json: !!opts.json,
463
521
  });
464
522
  });
@@ -811,6 +869,15 @@ adminDatabaseCmd
811
869
  const { runDbGet } = await import('./commands/products-db.js');
812
870
  await runDbGet(id, { json: !!opts.json });
813
871
  });
872
+ // gestovendas #12 fix: alias `status` → `get` (skill /neetru documenta como `status`).
873
+ adminDatabaseCmd
874
+ .command('status [id]')
875
+ .description('[Staff] Alias de `get` — mostrar detalhes e status de um banco')
876
+ .option('--json')
877
+ .action(async (id, opts) => {
878
+ const { runDbGet } = await import('./commands/products-db.js');
879
+ await runDbGet(id, { json: !!opts.json });
880
+ });
814
881
  // D-2: `set-status` é ESCRITA explícita. `status` era ambíguo com o READ em `neetru db status`.
815
882
  adminDatabaseCmd
816
883
  .command('set-status [id] [status]')
@@ -845,6 +912,57 @@ adminDatabaseCmd
845
912
  const { runDbDelete } = await import('./commands/products-db.js');
846
913
  await runDbDelete(id, { json: !!opts.json });
847
914
  });
915
+ // ── neetru admin sdk-credential ────────────────────────────────────────────
916
+ // Gerencia credenciais OIDC (SDK) por produto — create / list / revoke.
917
+ // Resolve bug_599d2f033e024bc194aa07639cd26c84: bootstrap CI/IaC desbloqueado.
918
+ // Internamente usa `oidc_clients/{clientId}` no Core (canônico pós-devkit).
919
+ const adminSdkCredentialCmd = adminCmd
920
+ .command('sdk-credential')
921
+ .description('Gerenciar credenciais OIDC (SDK) de produtos — create / list / revoke');
922
+ adminSdkCredentialCmd
923
+ .command('create')
924
+ .description('[Staff] Criar credencial OIDC para produto — exibe clientSecret ONE-TIME')
925
+ .requiredOption('--product-id <id>', 'ID do produto')
926
+ .requiredOption('--name <label>', 'nome/label da credencial (ex: "Backend Prod")')
927
+ .option('--scopes <escopos>', 'escopos separados por vírgula (ex: "entitlements.read,usage.write")')
928
+ .option('--mfa-token <code>', 'código TOTP para step-up MFA (se exigido pelo servidor)')
929
+ .option('--json', 'saída em JSON (machine-readable)')
930
+ .action(async (opts) => {
931
+ const { runSdkCredentialCreate } = await import('./commands/admin/sdk-credential.js');
932
+ await runSdkCredentialCreate({
933
+ productId: opts.productId,
934
+ name: opts.name,
935
+ scopes: opts.scopes,
936
+ mfaToken: opts.mfaToken,
937
+ json: !!opts.json,
938
+ });
939
+ });
940
+ adminSdkCredentialCmd
941
+ .command('list')
942
+ .description('[Staff] Listar credenciais OIDC de um produto')
943
+ .requiredOption('--product-id <id>', 'ID do produto')
944
+ .option('--json', 'saída em JSON')
945
+ .action(async (opts) => {
946
+ const { runSdkCredentialList } = await import('./commands/admin/sdk-credential.js');
947
+ await runSdkCredentialList({
948
+ productId: opts.productId,
949
+ json: !!opts.json,
950
+ });
951
+ });
952
+ adminSdkCredentialCmd
953
+ .command('revoke <clientId>')
954
+ .description('[Staff] Revogar credencial OIDC (status → desativado, irreversível)')
955
+ .requiredOption('--product-id <id>', 'ID do produto dono da credencial')
956
+ .option('--mfa-token <code>', 'código TOTP para step-up MFA')
957
+ .option('--json', 'saída em JSON')
958
+ .action(async (clientId, opts) => {
959
+ const { runSdkCredentialRevoke } = await import('./commands/admin/sdk-credential.js');
960
+ await runSdkCredentialRevoke(clientId, {
961
+ productId: opts.productId,
962
+ mfaToken: opts.mfaToken,
963
+ json: !!opts.json,
964
+ });
965
+ });
848
966
  // ── neetru products db (alias de remoção — D-1) ────────────────────────────
849
967
  // Emite aviso informando o novo caminho. Mantém subcomandos funcionais por
850
968
  // compatibilidade temporária mas deixa claro que foi renomeado.
@@ -855,16 +973,27 @@ const productsDbCmd = productsCmd
855
973
  process.stderr.write('\nAviso: `neetru products db` foi renomeado para `neetru admin database`.\n' +
856
974
  'Por favor, atualize seus scripts. Este alias será removido em breve.\n\n');
857
975
  });
976
+ // gestovendas #2 / pdv #5 fix: alias `list` aceita --product (slug legado) OU
977
+ // --product-id (novo). Se slug recebido, passa como productId para o Core.
978
+ // O Core tenta resolver slug → id server-side (fallback); se o Core não suportar,
979
+ // o operador vê mensagem específica do servidor e não um erro de sintaxe.
858
980
  productsDbCmd
859
981
  .command('list')
860
982
  .description('[RENOMEADO — use: neetru admin database list]')
861
- .option('--product-id <id>')
983
+ .option('--product-id <id>', 'ID do produto (novo)')
984
+ .option('--product <slugOrId>', 'slug ou ID do produto (legado — use --product-id)')
862
985
  .option('--engine <engine>')
863
986
  .option('--status <status>')
864
987
  .option('--json')
865
988
  .action(async (opts) => {
989
+ // Tradução de flag: --product (legado) → productId (novo).
990
+ const productId = opts.productId ?? opts.product;
991
+ if (opts.product && !opts.productId) {
992
+ process.stderr.write(`[DEPRECATED] Use --product-id em vez de --product.\n` +
993
+ ` Próxima major remove o suporte a --product neste alias.\n`);
994
+ }
866
995
  const { runDbList } = await import('./commands/products-db.js');
867
- await runDbList({ productId: opts.productId, engine: opts.engine, status: opts.status, json: !!opts.json });
996
+ await runDbList({ productId, engine: opts.engine, status: opts.status, json: !!opts.json });
868
997
  });
869
998
  productsDbCmd
870
999
  .command('engines')
@@ -1025,6 +1154,74 @@ program
1025
1154
  ctaLabel: opts.ctaLabel,
1026
1155
  });
1027
1156
  });
1157
+ // ── neetru marketplace ────────────────────────────────────────────────
1158
+ const marketplaceCmd = program
1159
+ .command('marketplace')
1160
+ .description('Central de artefatos do ecossistema Neetru (skills, SDK, templates)');
1161
+ const marketplaceSkillsCmd = marketplaceCmd
1162
+ .command('skills')
1163
+ .description('Gerenciar Skills do Claude Code');
1164
+ marketplaceSkillsCmd
1165
+ .command('install')
1166
+ .description('Clonar neetru-libs e instalar skills em ~/.claude/skills/')
1167
+ .action(async () => {
1168
+ const { runSkillsInstall } = await import('./commands/marketplace.js');
1169
+ await runSkillsInstall();
1170
+ });
1171
+ marketplaceSkillsCmd
1172
+ .command('update')
1173
+ .description('Atualizar cache neetru-libs (git pull) e re-copiar skills instaladas')
1174
+ .action(async () => {
1175
+ const { runSkillsUpdate } = await import('./commands/marketplace.js');
1176
+ await runSkillsUpdate();
1177
+ });
1178
+ marketplaceSkillsCmd
1179
+ .command('list')
1180
+ .description('Listar skills disponíveis no cache local')
1181
+ .action(async () => {
1182
+ const { runSkillsList } = await import('./commands/marketplace.js');
1183
+ await runSkillsList();
1184
+ });
1185
+ marketplaceSkillsCmd
1186
+ .command('uninstall')
1187
+ .description('Remover skills de ~/.claude/skills/ (pede confirmação)')
1188
+ .option('--yes', 'pular confirmação interativa')
1189
+ .action(async (opts) => {
1190
+ const { runSkillsUninstall } = await import('./commands/marketplace.js');
1191
+ await runSkillsUninstall({ yes: !!opts.yes });
1192
+ });
1193
+ const marketplaceSdkCmd = marketplaceCmd
1194
+ .command('sdk')
1195
+ .description('Gerenciar @neetru/sdk no projeto atual');
1196
+ marketplaceSdkCmd
1197
+ .command('init')
1198
+ .description('Adicionar @neetru/sdk ao package.json e criar src/neetru.ts')
1199
+ .action(async () => {
1200
+ const { runSdkInit } = await import('./commands/marketplace.js');
1201
+ await runSdkInit();
1202
+ });
1203
+ marketplaceSdkCmd
1204
+ .command('templates')
1205
+ .description('Listar templates de feature disponíveis (auth, billing, usage…)')
1206
+ .action(async () => {
1207
+ const { runSdkTemplates } = await import('./commands/marketplace.js');
1208
+ await runSdkTemplates();
1209
+ });
1210
+ marketplaceSdkCmd
1211
+ .command('add <template>')
1212
+ .description('Copiar template de feature pra src/ do projeto atual')
1213
+ .option('--force', 'sobrescrever arquivos existentes')
1214
+ .action(async (template, opts) => {
1215
+ const { runSdkAddTemplate } = await import('./commands/marketplace.js');
1216
+ await runSdkAddTemplate(template, { force: !!opts.force });
1217
+ });
1218
+ marketplaceCmd
1219
+ .command('browse')
1220
+ .description('Abrir https://github.com/Neetru/neetru-libs no browser')
1221
+ .action(async () => {
1222
+ const { runMarketplaceBrowse } = await import('./commands/marketplace.js');
1223
+ await runMarketplaceBrowse();
1224
+ });
1028
1225
  // ── neetru add ────────────────────────────────────────────────────────
1029
1226
  program
1030
1227
  .command('add <feature>')
@@ -1124,6 +1321,7 @@ dbCmd
1124
1321
  .option('--env <env>', 'ambiente: dev-local (padrão) | staging | production')
1125
1322
  .option('--product-id <id>', 'override do productId (default: slug do neetru.config.json)')
1126
1323
  .option('-y, --yes', 'modo não-interativo (usa defaults)')
1324
+ .option('--json', 'saída em JSON estruturado')
1127
1325
  .action(async (opts) => {
1128
1326
  const { runDbInit } = await import('./commands/db.js');
1129
1327
  await runDbInit({
@@ -1132,6 +1330,7 @@ dbCmd
1132
1330
  env: opts.env,
1133
1331
  productId: opts.productId,
1134
1332
  yes: !!opts.yes,
1333
+ json: !!opts.json,
1135
1334
  });
1136
1335
  });
1137
1336
  // ── neetru db apply ───────────────────────────────────────────────────
@@ -1144,6 +1343,7 @@ dbCmd
1144
1343
  .option('--schema <path>', 'caminho do schema (default: db/schema.ts)')
1145
1344
  .option('--dry-run', 'calcula o diff mas NÃO envia ao Core')
1146
1345
  .option('-y, --yes', 'pula a confirmação interativa')
1346
+ .option('--json', 'saída em JSON estruturado')
1147
1347
  .action(async (opts) => {
1148
1348
  const { runDbApply } = await import('./commands/db.js');
1149
1349
  await runDbApply({
@@ -1152,6 +1352,7 @@ dbCmd
1152
1352
  schemaPath: opts.schema,
1153
1353
  dryRun: !!opts.dryRun,
1154
1354
  yes: !!opts.yes,
1355
+ json: !!opts.json,
1155
1356
  });
1156
1357
  });
1157
1358
  // ── neetru db migrations ──────────────────────────────────────────────
@@ -1436,6 +1637,117 @@ supportTicketsCmd
1436
1637
  const { runSupportTicketsStatus } = await import('./commands/support.js');
1437
1638
  await runSupportTicketsStatus(id, { to: opts.to, json: !!opts.json });
1438
1639
  });
1640
+ // ── neetru bug ───────────────────────────────────────────────────────
1641
+ /**
1642
+ * neetru bug — bugs e ocorrências reportados pela CLI, agentes Claude e staff.
1643
+ *
1644
+ * Subcomandos:
1645
+ * report — reportar novo bug (interativo ou one-shot via flags)
1646
+ * list — listar bugs abertos (ou com filtros)
1647
+ * show <id> — detalhe completo
1648
+ * claim <id> — assumir investigação
1649
+ * resolve <id> — encerrar (resolved/wont_fix/duplicate)
1650
+ * comment <id> — adicionar comentário na thread
1651
+ *
1652
+ * Exemplo rápido (agentes Claude):
1653
+ * neetru bug report --title "X trava" --body "Stack trace…" --category cli --severity high --actor-type agent_claude --json
1654
+ * neetru bug list --status open --json
1655
+ */
1656
+ const bugCmd = program
1657
+ .command('bug')
1658
+ .description('Reportar, listar e resolver bugs e ocorrências da plataforma');
1659
+ bugCmd
1660
+ .command('report')
1661
+ .description('Reportar novo bug (interativo ou flags --title/--body para agentes)')
1662
+ .option('--title <text>', 'título curto do bug')
1663
+ .option('--body <text>', 'descrição detalhada')
1664
+ .option('--category <cat>', 'cli | sdk | core | agent | libs | docs | infra | other', 'other')
1665
+ .option('--severity <sev>', 'critical | high | medium | low', 'medium')
1666
+ .option('--product <id>', 'productId afetado (opcional)')
1667
+ .option('--steps <text>', 'passos para reproduzir (opcional)')
1668
+ .option('--expected <text>', 'comportamento esperado (opcional)')
1669
+ .option('--actual <text>', 'comportamento atual (opcional)')
1670
+ .option('--actor-type <type>', 'cli_principal | agent_claude | staff_session', 'cli_principal')
1671
+ .option('--json', 'saída em JSON (modo não-interativo)')
1672
+ .action(async (opts) => {
1673
+ const { runBugReport } = await import('./commands/bug.js');
1674
+ await runBugReport({
1675
+ title: opts.title,
1676
+ body: opts.body,
1677
+ category: opts.category,
1678
+ severity: opts.severity,
1679
+ product: opts.product,
1680
+ steps: opts.steps,
1681
+ expected: opts.expected,
1682
+ actual: opts.actual,
1683
+ actorType: opts.actorType,
1684
+ json: !!opts.json,
1685
+ });
1686
+ });
1687
+ bugCmd
1688
+ .command('list')
1689
+ .description('Listar bugs e ocorrências')
1690
+ .option('--status <s>', 'open | triaged | in_progress | resolved | wont_fix | duplicate')
1691
+ .option('--category <cat>', 'cli | sdk | core | agent | libs | docs | infra | other')
1692
+ .option('--assigned-to <uid>', 'filtrar por responsável (uid ou session id)')
1693
+ .option('--product <id>', 'filtrar por productId')
1694
+ .option('--limit <n>', 'máximo de resultados (default 20, max 200)')
1695
+ .option('--json', 'saída em JSON')
1696
+ .action(async (opts) => {
1697
+ const { runBugList } = await import('./commands/bug.js');
1698
+ await runBugList({
1699
+ status: opts.status,
1700
+ category: opts.category,
1701
+ assignedTo: opts.assignedTo,
1702
+ product: opts.product,
1703
+ limit: opts.limit,
1704
+ json: !!opts.json,
1705
+ });
1706
+ });
1707
+ bugCmd
1708
+ .command('show <id>')
1709
+ .description('Mostrar detalhe completo de um bug')
1710
+ .option('--json', 'saída em JSON')
1711
+ .action(async (id, opts) => {
1712
+ const { runBugShow } = await import('./commands/bug.js');
1713
+ await runBugShow(id, { json: !!opts.json });
1714
+ });
1715
+ bugCmd
1716
+ .command('claim <id>')
1717
+ .description('Assumir investigação de um bug')
1718
+ .option('--assigned-to <uid>', 'uid ou session id (default: caller)')
1719
+ .option('--json', 'saída em JSON')
1720
+ .action(async (id, opts) => {
1721
+ const { runBugClaim } = await import('./commands/bug.js');
1722
+ await runBugClaim(id, { assignedTo: opts.assignedTo, json: !!opts.json });
1723
+ });
1724
+ bugCmd
1725
+ .command('resolve <id>')
1726
+ .description('Encerrar um bug como resolvido, wont_fix ou duplicate')
1727
+ .option('--summary <text>', 'resumo do que foi feito / por que não corrigir')
1728
+ .option('--commit <sha>', 'commit SHA que corrige o bug (opcional)')
1729
+ .option('--status <s>', 'resolved | wont_fix | duplicate (default: resolved)', 'resolved')
1730
+ .option('--duplicate-of <id>', 'ID do bug original (obrigatório quando --status duplicate)')
1731
+ .option('--json', 'saída em JSON (modo não-interativo)')
1732
+ .action(async (id, opts) => {
1733
+ const { runBugResolve } = await import('./commands/bug.js');
1734
+ await runBugResolve(id, {
1735
+ summary: opts.summary,
1736
+ commit: opts.commit,
1737
+ status: opts.status,
1738
+ duplicateOf: opts.duplicateOf,
1739
+ json: !!opts.json,
1740
+ });
1741
+ });
1742
+ bugCmd
1743
+ .command('comment <id>')
1744
+ .description('Adicionar comentário na thread de um bug')
1745
+ .option('--body <text>', 'corpo do comentário')
1746
+ .option('--json', 'saída em JSON (modo não-interativo)')
1747
+ .action(async (id, opts) => {
1748
+ const { runBugComment } = await import('./commands/bug.js');
1749
+ await runBugComment(id, { body: opts.body, json: !!opts.json });
1750
+ });
1439
1751
  // ── neetru dns ───────────────────────────────────────────────────────
1440
1752
  const dnsCmd = program
1441
1753
  .command('dns')
@@ -1572,9 +1884,20 @@ drCmd
1572
1884
  // Frente B — docs em GCS + Firestore registry. Desacopla conteúdo de docs
1573
1885
  // do deploy do Core (owner edita markdown direto pelo bucket OU via CLI,
1574
1886
  // sem rebuild).
1887
+ //
1888
+ // `neetru docs open [topic]` abre documentação no browser. É o ponto de
1889
+ // entrada pra quem quer LER docs, não publicar.
1575
1890
  const docsCmd = program
1576
1891
  .command('docs')
1577
- .description('Gerenciar registry de docs publicados em gs://neetru-docs');
1892
+ .description('Documentação + registry de docs publicados em gs://neetru-docs');
1893
+ docsCmd
1894
+ .command('open [topic]')
1895
+ .description('Abrir documentação no browser (ex: sdk, cli, db, webhooks, auth)')
1896
+ .option('--json', 'saída em JSON (imprime URL sem abrir browser)')
1897
+ .action(async (topic, opts) => {
1898
+ const { runDocsOpen } = await import('./commands/docs.js');
1899
+ await runDocsOpen(topic, { json: !!opts.json });
1900
+ });
1578
1901
  docsCmd
1579
1902
  .command('publish <file>')
1580
1903
  .description('Publicar arquivo markdown (sobe pro GCS + registry Firestore)')