@neetru/cli 2.31.0 → 2.32.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 (243) hide show
  1. package/CHANGELOG.md +91 -2
  2. package/README.md +2 -0
  3. package/dist/cli-kit/choices.d.ts +33 -0
  4. package/dist/cli-kit/choices.js +32 -0
  5. package/dist/cli-kit/command-context.d.ts +98 -0
  6. package/dist/cli-kit/command-context.js +108 -0
  7. package/dist/cli-kit/errors.d.ts +79 -0
  8. package/dist/cli-kit/errors.js +94 -0
  9. package/dist/cli-kit/index.d.ts +8 -0
  10. package/dist/cli-kit/index.js +8 -0
  11. package/dist/cli-kit/suggest.d.ts +74 -0
  12. package/dist/cli-kit/suggest.js +207 -0
  13. package/dist/commands/admin/oidc-login.js +1 -1
  14. package/dist/commands/admin/sdk-credential.js +1 -1
  15. package/dist/commands/autocomplete.d.ts +1 -1
  16. package/dist/commands/autocomplete.js +1 -0
  17. package/dist/commands/bug.d.ts +59 -0
  18. package/dist/commands/bug.js +114 -2
  19. package/dist/commands/build.d.ts +24 -1
  20. package/dist/commands/build.js +195 -73
  21. package/dist/commands/chat.d.ts +94 -1
  22. package/dist/commands/chat.js +258 -26
  23. package/dist/commands/customers.js +1 -1
  24. package/dist/commands/db-move.js +3 -3
  25. package/dist/commands/db.js +17 -24
  26. package/dist/commands/deploy.d.ts +91 -2
  27. package/dist/commands/deploy.js +414 -69
  28. package/dist/commands/deployments.js +11 -6
  29. package/dist/commands/dev-context.js +15 -5
  30. package/dist/commands/docs-produto.d.ts +25 -0
  31. package/dist/commands/docs-produto.js +70 -8
  32. package/dist/commands/doctor.js +36 -1
  33. package/dist/commands/domains.js +85 -4
  34. package/dist/commands/env.d.ts +9 -3
  35. package/dist/commands/env.js +23 -3
  36. package/dist/commands/infra-read.js +2 -2
  37. package/dist/commands/init.js +7 -1
  38. package/dist/commands/login.js +35 -8
  39. package/dist/commands/logs.js +12 -4
  40. package/dist/commands/mobile.js +33 -9
  41. package/dist/commands/new.d.ts +29 -1
  42. package/dist/commands/new.js +92 -4
  43. package/dist/commands/open.js +16 -6
  44. package/dist/commands/ops.js +1 -1
  45. package/dist/commands/products-db.d.ts +1 -0
  46. package/dist/commands/products-db.js +41 -8
  47. package/dist/commands/products.d.ts +17 -3
  48. package/dist/commands/products.js +84 -16
  49. package/dist/commands/promote.js +30 -5
  50. package/dist/commands/secrets.d.ts +91 -0
  51. package/dist/commands/secrets.js +471 -0
  52. package/dist/commands/servers.js +6 -4
  53. package/dist/commands/status.js +2 -2
  54. package/dist/commands/support.js +5 -3
  55. package/dist/commands/tenants.js +28 -16
  56. package/dist/commands/upgrade.d.ts +9 -0
  57. package/dist/commands/upgrade.js +50 -1
  58. package/dist/commands/vm-memory.js +1 -0
  59. package/dist/commands/webhooks.d.ts +2 -0
  60. package/dist/commands/webhooks.js +9 -3
  61. package/dist/commands/workspaces.js +42 -11
  62. package/dist/index.js +70 -14
  63. package/dist/lib/api-client.d.ts +14 -0
  64. package/dist/lib/api-client.js +59 -6
  65. package/dist/lib/api-error.d.ts +14 -0
  66. package/dist/lib/api-error.js +107 -46
  67. package/dist/lib/build-targets.d.ts +127 -0
  68. package/dist/lib/build-targets.js +348 -0
  69. package/dist/lib/cli-error.d.ts +13 -0
  70. package/dist/lib/cli-error.js +24 -0
  71. package/dist/lib/cli-write.d.ts +43 -9
  72. package/dist/lib/cli-write.js +154 -45
  73. package/dist/lib/config-schema.d.ts +8 -8
  74. package/dist/lib/db-local/db-json.d.ts +1 -1
  75. package/dist/lib/db-local/db-json.js +13 -5
  76. package/dist/lib/db-local/env.d.ts +15 -3
  77. package/dist/lib/db-local/env.js +37 -10
  78. package/dist/lib/db-pipeline/build-deps.d.ts +5 -1
  79. package/dist/lib/db-pipeline/build-deps.js +23 -3
  80. package/dist/lib/db-pipeline/types.d.ts +6 -2
  81. package/dist/lib/db-pipeline/types.js +0 -18
  82. package/dist/lib/deploy-target-contract.d.ts +152 -0
  83. package/dist/lib/deploy-target-contract.js +411 -0
  84. package/dist/lib/env-file-do-build.d.ts +19 -0
  85. package/dist/lib/env-file-do-build.js +44 -0
  86. package/dist/lib/envs.d.ts +108 -0
  87. package/dist/lib/envs.js +127 -0
  88. package/dist/lib/fs-seguro.d.ts +63 -0
  89. package/dist/lib/fs-seguro.js +108 -0
  90. package/dist/lib/mobile-local/mobile-json.js +10 -4
  91. package/dist/lib/pickers.d.ts +44 -7
  92. package/dist/lib/pickers.js +60 -4
  93. package/dist/lib/product-docs/sync.d.ts +6 -0
  94. package/dist/lib/product-docs/sync.js +10 -1
  95. package/dist/lib/public-secret-scan.d.ts +44 -3
  96. package/dist/lib/public-secret-scan.js +361 -36
  97. package/dist/lib/render.d.ts +16 -0
  98. package/dist/lib/render.js +34 -4
  99. package/dist/lib/segredo-produto.d.ts +124 -0
  100. package/dist/lib/segredo-produto.js +336 -0
  101. package/dist/lib/texto-utf8.d.ts +96 -0
  102. package/dist/lib/texto-utf8.js +196 -0
  103. package/package.json +2 -1
  104. package/dist/cli-kit/format.js.map +0 -1
  105. package/dist/cli-kit/glyphs.js.map +0 -1
  106. package/dist/cli-kit/index.js.map +0 -1
  107. package/dist/cli-kit/palette.js.map +0 -1
  108. package/dist/commands/add.js.map +0 -1
  109. package/dist/commands/admin/iam-check.js.map +0 -1
  110. package/dist/commands/admin/oidc-login.js.map +0 -1
  111. package/dist/commands/admin/sdk-credential.js.map +0 -1
  112. package/dist/commands/agent-release.js.map +0 -1
  113. package/dist/commands/agent-write.js.map +0 -1
  114. package/dist/commands/ai-models.js.map +0 -1
  115. package/dist/commands/ai.js.map +0 -1
  116. package/dist/commands/api-catalog.js.map +0 -1
  117. package/dist/commands/archive.js.map +0 -1
  118. package/dist/commands/artifact-registry.js.map +0 -1
  119. package/dist/commands/audit.js.map +0 -1
  120. package/dist/commands/auth.js.map +0 -1
  121. package/dist/commands/autocomplete.js.map +0 -1
  122. package/dist/commands/billing.js.map +0 -1
  123. package/dist/commands/bootstrap.js.map +0 -1
  124. package/dist/commands/bug.js.map +0 -1
  125. package/dist/commands/build.js.map +0 -1
  126. package/dist/commands/chat-menu.js.map +0 -1
  127. package/dist/commands/chat.js.map +0 -1
  128. package/dist/commands/cloud-run.js.map +0 -1
  129. package/dist/commands/code.js.map +0 -1
  130. package/dist/commands/config.js.map +0 -1
  131. package/dist/commands/customers.js.map +0 -1
  132. package/dist/commands/db-move.js.map +0 -1
  133. package/dist/commands/db.js.map +0 -1
  134. package/dist/commands/deploy.js.map +0 -1
  135. package/dist/commands/deployments.js.map +0 -1
  136. package/dist/commands/dev-context.js.map +0 -1
  137. package/dist/commands/dev-up.js.map +0 -1
  138. package/dist/commands/dev.js.map +0 -1
  139. package/dist/commands/docs-produto.js.map +0 -1
  140. package/dist/commands/docs.js.map +0 -1
  141. package/dist/commands/doctor.js.map +0 -1
  142. package/dist/commands/domains.js.map +0 -1
  143. package/dist/commands/dr.js.map +0 -1
  144. package/dist/commands/env.js.map +0 -1
  145. package/dist/commands/hosting-write.js.map +0 -1
  146. package/dist/commands/infra-read.js.map +0 -1
  147. package/dist/commands/init.js.map +0 -1
  148. package/dist/commands/login.js.map +0 -1
  149. package/dist/commands/logout.js.map +0 -1
  150. package/dist/commands/logs.js.map +0 -1
  151. package/dist/commands/mobile.js.map +0 -1
  152. package/dist/commands/mocks.js.map +0 -1
  153. package/dist/commands/new.js.map +0 -1
  154. package/dist/commands/open.js.map +0 -1
  155. package/dist/commands/ops.js.map +0 -1
  156. package/dist/commands/products-db.js.map +0 -1
  157. package/dist/commands/products.js.map +0 -1
  158. package/dist/commands/promote.js.map +0 -1
  159. package/dist/commands/publish.js.map +0 -1
  160. package/dist/commands/schema.js.map +0 -1
  161. package/dist/commands/servers.js.map +0 -1
  162. package/dist/commands/smoke.js.map +0 -1
  163. package/dist/commands/status.js.map +0 -1
  164. package/dist/commands/support.js.map +0 -1
  165. package/dist/commands/surface-status.js.map +0 -1
  166. package/dist/commands/tenants.js.map +0 -1
  167. package/dist/commands/ui.js.map +0 -1
  168. package/dist/commands/upgrade.js.map +0 -1
  169. package/dist/commands/validate.js.map +0 -1
  170. package/dist/commands/vm-memory.js.map +0 -1
  171. package/dist/commands/vm.js.map +0 -1
  172. package/dist/commands/webhooks.js.map +0 -1
  173. package/dist/commands/whoami.js.map +0 -1
  174. package/dist/commands/workspaces.js.map +0 -1
  175. package/dist/index.js.map +0 -1
  176. package/dist/lib/agent-artifact-url.js.map +0 -1
  177. package/dist/lib/agent-commands.js.map +0 -1
  178. package/dist/lib/ai/auth-discovery.js.map +0 -1
  179. package/dist/lib/ai/context.js.map +0 -1
  180. package/dist/lib/ai/orchestrator.js.map +0 -1
  181. package/dist/lib/api-client.js.map +0 -1
  182. package/dist/lib/api-error.js.map +0 -1
  183. package/dist/lib/auth.js.map +0 -1
  184. package/dist/lib/cli-read.js.map +0 -1
  185. package/dist/lib/cli-write.js.map +0 -1
  186. package/dist/lib/code-agent-budget.js.map +0 -1
  187. package/dist/lib/code-agent-files.js.map +0 -1
  188. package/dist/lib/code-agent-gateway.js.map +0 -1
  189. package/dist/lib/code-agent-git-tools.js.map +0 -1
  190. package/dist/lib/code-agent-protocol.js.map +0 -1
  191. package/dist/lib/code-agent-session.js.map +0 -1
  192. package/dist/lib/code-agent-settings.js.map +0 -1
  193. package/dist/lib/code-agent-skills.js.map +0 -1
  194. package/dist/lib/code-agent-subagents.js.map +0 -1
  195. package/dist/lib/code-agent-tools.js.map +0 -1
  196. package/dist/lib/code-agent-tui.js.map +0 -1
  197. package/dist/lib/code-agent-types.js.map +0 -1
  198. package/dist/lib/code-agent-ui.js.map +0 -1
  199. package/dist/lib/code-agent-web-tools.js.map +0 -1
  200. package/dist/lib/code-agent-write-tools.js.map +0 -1
  201. package/dist/lib/compose-generator.js.map +0 -1
  202. package/dist/lib/config-schema.js.map +0 -1
  203. package/dist/lib/config.js.map +0 -1
  204. package/dist/lib/db-local/db-json.js.map +0 -1
  205. package/dist/lib/db-local/env.js.map +0 -1
  206. package/dist/lib/db-local/fingerprint.js.map +0 -1
  207. package/dist/lib/db-local/index.js.map +0 -1
  208. package/dist/lib/db-pipeline/build-deps.js.map +0 -1
  209. package/dist/lib/db-pipeline/errors.js.map +0 -1
  210. package/dist/lib/db-pipeline/index.js.map +0 -1
  211. package/dist/lib/db-pipeline/pipeline.js.map +0 -1
  212. package/dist/lib/db-pipeline/rehearse.js.map +0 -1
  213. package/dist/lib/db-pipeline/types.js.map +0 -1
  214. package/dist/lib/dev-local-context.js.map +0 -1
  215. package/dist/lib/iam-preflight.js.map +0 -1
  216. package/dist/lib/mobile-local/index.js.map +0 -1
  217. package/dist/lib/mobile-local/mobile-json.js.map +0 -1
  218. package/dist/lib/pickers.js.map +0 -1
  219. package/dist/lib/product-config.js.map +0 -1
  220. package/dist/lib/product-docs/check.js.map +0 -1
  221. package/dist/lib/product-docs/config.js.map +0 -1
  222. package/dist/lib/product-docs/dates.js.map +0 -1
  223. package/dist/lib/product-docs/frontmatter.js.map +0 -1
  224. package/dist/lib/product-docs/git.js.map +0 -1
  225. package/dist/lib/product-docs/markdown.js.map +0 -1
  226. package/dist/lib/product-docs/ops.js.map +0 -1
  227. package/dist/lib/product-docs/structure.js.map +0 -1
  228. package/dist/lib/product-docs/sync.js.map +0 -1
  229. package/dist/lib/product-docs/taxonomy.js.map +0 -1
  230. package/dist/lib/product-docs/templates.js.map +0 -1
  231. package/dist/lib/public-secret-scan.js.map +0 -1
  232. package/dist/lib/render.js.map +0 -1
  233. package/dist/lib/schema-cache.js.map +0 -1
  234. package/dist/lib/sse.js.map +0 -1
  235. package/dist/lib/telemetry.js.map +0 -1
  236. package/dist/lib/token-resolver.js.map +0 -1
  237. package/dist/lib/url-allowlist.js.map +0 -1
  238. package/dist/lib/version-check.js.map +0 -1
  239. package/dist/utils/logger.js.map +0 -1
  240. package/dist/utils/logo.js.map +0 -1
  241. package/dist/utils/safe-exit.js.map +0 -1
  242. package/dist/utils/spinner-registry.js.map +0 -1
  243. package/dist/version.js.map +0 -1
package/dist/index.js CHANGED
@@ -5,6 +5,9 @@ import { CLI_VERSION } from './version.js';
5
5
  import { installTelemetryFlushHook, markCommandStart, markCommandEnd, } from './lib/telemetry.js';
6
6
  import { safeExit, isSafeExitHalt } from './utils/safe-exit.js';
7
7
  import { CliApiError } from './lib/api-client.js';
8
+ import { NeetruCliError } from './cli-kit/errors.js';
9
+ import { installCommandContext, getCurrentCommand } from './cli-kit/command-context.js';
10
+ import { emitCliError } from './lib/cli-error.js';
8
11
  // UV_HANDLE_CLOSING no Windows (bug_818cf6d): o double-destroy de stdin (handler
9
12
  // 'exit' redundante) foi removido em 2026-07-15 (ddd479f), MAS isso não bastou
10
13
  // — a causa dominante é o `uv_async_t` do undici (fetch) ainda vivo quando o
@@ -29,6 +32,20 @@ installTelemetryFlushHook();
29
32
  function handleFatalSignal(err) {
30
33
  if (isSafeExitHalt(err))
31
34
  return; // saída já agendada dentro do safeExit
35
+ // bug_89d2497e: erro tipado lançado e não tratado sai com o código DELE (e,
36
+ // se o comando em execução recebeu --json, como JSON) — não com o 1 genérico.
37
+ if (err instanceof NeetruCliError) {
38
+ let code = err.exit;
39
+ try {
40
+ code = emitCliError(err, Boolean(getCurrentCommand()?.opts().json));
41
+ }
42
+ catch {
43
+ /* logger indisponível — segue direto pro exit deferido */
44
+ }
45
+ process.exitCode = code;
46
+ setTimeout(() => process.exit(code), 300);
47
+ return;
48
+ }
32
49
  try {
33
50
  log.error(err instanceof Error ? (err.stack ?? err.message) : String(err));
34
51
  // LABS #6 (2026-09-28): se o erro carrega o x-correlation-id do Core, mostra
@@ -89,6 +106,10 @@ program.hook('preAction', (_thisCmd, actionCmd) => {
89
106
  process.once('exit', (code) => {
90
107
  markCommandEnd(typeof code === 'number' ? code : 0);
91
108
  });
109
+ // bug_89d2497e: guarda o comando que o Commander vai executar, para que um
110
+ // conselho de erro (ex.: "passe --mfa-token") só cite flag que ESSE comando
111
+ // declara. Ver `src/cli-kit/command-context.ts`.
112
+ installCommandContext(program);
92
113
  // ── neetru ai ─────────────────────────────────────────────────────────
93
114
  const aiCommand = program
94
115
  .command('ai')
@@ -176,9 +197,9 @@ program
176
197
  .command('new <name>')
177
198
  .description('Criar produto, workspace, scaffold e abrir painel')
178
199
  .option('--product-slug <slug>', 'slug do produto (default: derivado de <name>)')
179
- .option('--env <env>', 'dev (default) | staging | prod', 'dev')
200
+ .option('--env <env>', 'dev-local (padrão) | staging | prod')
180
201
  .option('--tier <tier>', 'standard (default) | dev | enterprise', 'standard')
181
- .option('--customer <customerId>', 'customerId dono do workspace (default: self)')
202
+ .option('--customer <customerId>', 'customerId dono do workspace (OBRIGATÓRIO se o produto não for Neetru-owned)')
182
203
  .option('--skip-init', 'pula o scaffold local')
183
204
  .option('--open', 'abre o browser após criar o workspace (opt-in)')
184
205
  .option('--skip-open', '(obsoleto — browser não abre por default; use --open para abrir)')
@@ -403,11 +424,15 @@ program
403
424
  .command('build')
404
425
  .description('Construir tarball do produto pronto para deploy')
405
426
  .option('--product <slug>', 'override do slug (default: lê neetru.config.json)')
427
+ // bug_678ddc98: monorepo — aponta pro diretório do app sem precisar de `cd`.
428
+ // `--cwd` é alias porque é o nome que a maioria das ferramentas usa.
429
+ .option('--app <dir>', 'diretório do app a construir (monorepo: ex. apps/web). O artefato continua saindo no diretório atual')
430
+ .option('--cwd <dir>', 'alias de --app')
406
431
  .option('--stack <stack>', 'node | docker | php-apache | static (default: detectado)')
407
432
  .option('--output <dir>', 'diretório de saída', '.neetru-build')
408
433
  .option('--version <version>', 'versão (default: detectada de package.json ou timestamp)')
409
434
  .option('--no-cache', 'invalida caches do build (recria lockfile, --no-cache no docker)')
410
- .option('--no-install', 'pula `npm install` (assume node_modules pronto — útil quando `next dev` ativo no Windows)')
435
+ .option('--no-install', 'pula o install (assume node_modules pronto — útil quando `next dev` ativo no Windows)')
411
436
  .option('--env-file <path>', 'arquivo .env com vars de ambiente a injetar no build (ex: NEXT_PUBLIC_API_URL)')
412
437
  .option('--target-env <env>', 'dev | workspace | prod — força NEXT_PUBLIC_NEETRU_ENV no bundle (default: prod; ' +
413
438
  'neutraliza .env.local vencendo .env.production — incidente 2026-07-22)', 'prod')
@@ -416,6 +441,9 @@ program
416
441
  const { runBuild } = await import('./commands/build.js');
417
442
  await runBuild({
418
443
  product: opts.product,
444
+ // bug_678ddc98: `--app` vence `--cwd` quando os dois vêm juntos (o
445
+ // canônico ganha do alias, em vez de escolher em silêncio).
446
+ app: opts.app ?? opts.cwd,
419
447
  stack: opts.stack,
420
448
  output: opts.output,
421
449
  version: opts.version,
@@ -602,7 +630,7 @@ tenantsCmd
602
630
  .option('--slug <slug>', 'slug do tenant')
603
631
  .option('--customer <customerId>', 'id do customer')
604
632
  .option('--product <productId>', 'id do produto (interativo se ausente)')
605
- .option('--env <env>', 'dev | staging | prod (default: dev)')
633
+ .option('--env <env>', 'dev-local | staging | prod (default: dev-local)')
606
634
  .option('--domain <domain>', 'domínio primário')
607
635
  .option('--json', 'saída em JSON')
608
636
  .action(async (opts) => {
@@ -624,7 +652,7 @@ tenantsCmd
624
652
  .option('--slug <slug>', 'slug do tenant')
625
653
  .option('--customer <customerId>', 'id do customer')
626
654
  .option('--product <productId>', 'id do produto (interativo se ausente)')
627
- .option('--env <env>', 'dev | staging | prod (default: dev)')
655
+ .option('--env <env>', 'dev-local | staging | prod (default: dev-local)')
628
656
  .option('--domain <domain>', 'domínio primário')
629
657
  .option('--json', 'saída em JSON')
630
658
  .action(async (id, opts) => {
@@ -901,8 +929,8 @@ workspacesCmd
901
929
  .command('create')
902
930
  .description('Criar workspace (retorna OAuth secret one-time)')
903
931
  .option('--product <productId>', 'id do produto (interativo se ausente)')
904
- .option('--customer <customerId>', 'id do customer (default: self/Neetru)')
905
- .option('--env <env>', 'dev | staging | prod (default: dev)')
932
+ .option('--customer <customerId>', 'id do customer (omitir só em produto Neetru-owned)')
933
+ .option('--env <env>', 'dev-local | staging | prod (default: dev-local)')
906
934
  .option('--tier <tier>', 'dev | standard | enterprise (default: standard)')
907
935
  .option('--name <name>', 'label do workspace')
908
936
  .option('--bind-config', 'grava tenantId retornado no neetru.config.json do cwd')
@@ -939,7 +967,7 @@ workspacesCmd
939
967
  .command('list')
940
968
  .description('Listar workspaces')
941
969
  .option('--product <productId>', 'filtra por productId')
942
- .option('--env <env>', 'dev | staging | prod')
970
+ .option('--env <env>', 'dev-local | staging | prod')
943
971
  .option('--status <status>', 'active | paused | provisioning | …')
944
972
  .option('--json', 'saída em JSON')
945
973
  .action(async (opts) => {
@@ -976,7 +1004,7 @@ deploymentsCmd
976
1004
  .option('--product <productId>', 'id do produto (interativo se ausente)')
977
1005
  .option('--tenant <tenantId>', 'id do tenant alvo (interativo se ausente)')
978
1006
  .option('--version <version>', 'versao a deployar')
979
- .option('--env <env>', 'dev | staging | prod (default: dev)')
1007
+ .option('--env <env>', 'dev-local | staging | prod (default: dev-local)')
980
1008
  .option('--server <serverId>', 'id do server alvo (interativo se ausente)')
981
1009
  .option('--json', 'saída em JSON')
982
1010
  .action(async (opts) => {
@@ -1209,7 +1237,10 @@ productsCmd
1209
1237
  .option('--description <text>', 'descrição (opcional)')
1210
1238
  .option('--owner-team <team>', 'time dono (default: neetru-eng)')
1211
1239
  .option('--plan-template <tier>', 'free | starter | pro (default: free)')
1212
- .option('--env <csv>', 'ambientes separados por vírgula (default: dev,prod)')
1240
+ // bug bug_db50b21f84e8400f9c3f6286b69c353b: o Core descarta `environments` no
1241
+ // POST. A flag segue aceita pra não quebrar script existente, mas o texto
1242
+ // precisa dizer a verdade — antes prometia uma escolha que não acontecia.
1243
+ .option('--env <csv>', 'IGNORADO hoje: o Core provisiona staging e prod sozinho')
1213
1244
  .option('--yes', 'pula prompts (exige --name; resto usa defaults)')
1214
1245
  .option('--json', 'saída em JSON (machine-readable)')
1215
1246
  .action(async (opts) => {
@@ -1389,10 +1420,15 @@ adminDatabaseCmd
1389
1420
  .command('delete [id]')
1390
1421
  .description('[Staff] Arquivar banco por soft-delete')
1391
1422
  .option('--force', 'Requerido para DBs em status=active (passa force=true ao Core)')
1423
+ // bug_757b9746: a rota exige step-up MFA e o hint de erro já mandava usar
1424
+ // `--mfa-token`, mas o subcomando não declarava a flag. Sem sessão de 1h
1425
+ // ativa (`neetru auth mfa-session`), em --json/não-interativo este era o
1426
+ // único caminho possível — e ele não existia.
1427
+ .option('--mfa-token <codigo>', 'Código TOTP para o step-up MFA (dispensado com `neetru auth mfa-session`)')
1392
1428
  .option('--json')
1393
1429
  .action(async (id, opts) => {
1394
1430
  const { runDbDelete } = await import('./commands/products-db.js');
1395
- await runDbDelete(id, { json: !!opts.json, force: !!opts.force });
1431
+ await runDbDelete(id, { json: !!opts.json, force: !!opts.force, mfaToken: opts.mfaToken });
1396
1432
  });
1397
1433
  // ── neetru admin sdk-credential ────────────────────────────────────────────
1398
1434
  // Gerencia credenciais OIDC de MÁQUINA (M2M / client_credentials) por produto —
@@ -1613,10 +1649,14 @@ productsDbCmd
1613
1649
  .command('set-status [id] [status]')
1614
1650
  .description('[RENOMEADO — use: neetru admin database set-status]')
1615
1651
  .option('--reason <text>')
1652
+ // A7 (2026-10-13): a rota exige step-up MFA com o banco active/degraded; o
1653
+ // alias depreciado não declarava a flag e, em --json/CI sem sessão de 1h,
1654
+ // não havia como mandar o código (o `admin database set-status` já tinha).
1655
+ .option('--mfa-token <code>', 'código TOTP para step-up MFA (exigido se o banco está active/degraded)')
1616
1656
  .option('--json')
1617
1657
  .action(async (id, status, opts) => {
1618
1658
  const { runDbSetStatus } = await import('./commands/products-db.js');
1619
- await runDbSetStatus(id, status, { reason: opts.reason, json: !!opts.json });
1659
+ await runDbSetStatus(id, status, { reason: opts.reason, mfaToken: opts.mfaToken, json: !!opts.json });
1620
1660
  });
1621
1661
  productsDbCmd
1622
1662
  .command('retry [id]')
@@ -1638,10 +1678,14 @@ productsDbCmd
1638
1678
  productsDbCmd
1639
1679
  .command('delete [id]')
1640
1680
  .description('[RENOMEADO — use: neetru admin database delete]')
1681
+ // bug_757b9746: o alias roteia pro MESMO runDbDelete, que agora faz step-up
1682
+ // MFA. Sem a flag aqui o alias reproduziria o bug inteiro (401 com instrução
1683
+ // de usar uma flag inexistente).
1684
+ .option('--mfa-token <codigo>', 'Código TOTP para o step-up MFA (dispensado com `neetru auth mfa-session`)')
1641
1685
  .option('--json')
1642
1686
  .action(async (id, opts) => {
1643
1687
  const { runDbDelete } = await import('./commands/products-db.js');
1644
- await runDbDelete(id, { json: !!opts.json });
1688
+ await runDbDelete(id, { json: !!opts.json, mfaToken: opts.mfaToken });
1645
1689
  });
1646
1690
  // ── neetru logs ───────────────────────────────────────────────────────
1647
1691
  program
@@ -2052,7 +2096,7 @@ dbMigrationsCmd
2052
2096
  .description('Confirmar migração destrutiva (POST /api/cli/v1/db/migrations/[id]/confirm — exige MFA)')
2053
2097
  // Era `requiredOption('--mfa')` — o commander rejeitava ANTES de rodar, matando
2054
2098
  // o caminho de sessão MFA de 1h (`neetru auth mfa-session`) que o resolveStepUp
2055
- // promete honrar. Agora `option`: resolveStepUp faz o fail-closed (sessão → --mfa-token
2099
+ // promete honrar. Agora `option`: resolveStepUp faz o fail-closed (--mfa-token → sessão
2056
2100
  // → erro acionável). Flag canônica `--mfa-token`; `--mfa` fica como alias oculto. (auditoria UX 2026-09)
2057
2101
  .option('--mfa-token <token>', 'código TOTP step-up MFA (ou use `neetru auth mfa-session`)')
2058
2102
  .addOption(new Option('--mfa <token>').hideHelp())
@@ -2941,6 +2985,7 @@ webhooksCmd
2941
2985
  .requiredOption('--product-id <id>', 'ID do produto')
2942
2986
  .option('--yes', 'pula a confirmação interativa (necessário em --json/CI)')
2943
2987
  .option('--force', 'alias de --yes')
2988
+ .option('--mfa-token <code>', 'código TOTP para step-up MFA (ou use `neetru auth mfa-session`)')
2944
2989
  .option('--json', 'saída em JSON')
2945
2990
  .action(async (id, opts) => {
2946
2991
  const { runWebhooksUnregister } = await import('./commands/webhooks.js');
@@ -2949,6 +2994,7 @@ webhooksCmd
2949
2994
  productId: opts.productId,
2950
2995
  yes: !!opts.yes,
2951
2996
  force: !!opts.force,
2997
+ mfaToken: opts.mfaToken,
2952
2998
  json: !!opts.json,
2953
2999
  });
2954
3000
  });
@@ -3017,6 +3063,16 @@ domainsCmd
3017
3063
  const { runDomainsRetry } = await import('./commands/domains.js');
3018
3064
  await runDomainsRetry({ product: opts.product, sub: opts.sub, json: !!opts.json });
3019
3065
  });
3066
+ // ── neetru secrets ──────────────────────────────────────────────────
3067
+ // Segredos PRÓPRIOS do produto (bug_8b85353b, Parte 3): registrar/trocar
3068
+ // (`set`, inclusive `--generate`), listar só nomes (`list`) e arquivar
3069
+ // (`archive`). O valor vai direto pro Secret Manager e nunca entra pelo argv.
3070
+ // Contrato: core#632 (GET/POST/DELETE /api/cli/v1/products/{slug}/secrets).
3071
+ // @top-level: secrets
3072
+ {
3073
+ const { registerSecretsCommand } = await import('./commands/secrets.js');
3074
+ registerSecretsCommand(program);
3075
+ }
3020
3076
  // ── neetru mobile ───────────────────────────────────────────────────
3021
3077
  // Devs/IAs registram/listam/revogam Apps Mobile (iOS/Android) do produto
3022
3078
  // atual (lido de neetru.config.json no cwd). Dev-facing, self-service —
@@ -50,6 +50,20 @@ interface StreamOptions {
50
50
  export declare function assertTrustedApiHost(rawUrl: string): void;
51
51
  /** @deprecated Nome legado — agora é fail-closed (throw), não só warning. Use `assertTrustedApiHost`. */
52
52
  export declare const maybeWarnUntrustedHost: typeof assertTrustedApiHost;
53
+ /**
54
+ * URL absoluta de um path da API do Core: MESMA base (`neetruApiUrl`) e MESMA
55
+ * guarda fail-closed de `apiRequest`/`apiStream`. Lança `CliNetworkError` se a
56
+ * base estiver fora da allowlist — antes de qualquer Bearer sair da máquina.
57
+ *
58
+ * Todo `fetch` que leve o token da CLI e não passe por `apiRequest`/`apiStream`
59
+ * (ex.: o SSE de `neetru logs -f`, que precisa ler o corpo aos poucos) monta a
60
+ * URL por aqui. C7 (2026-10-13): o stream do `logs -f` lia uma chave
61
+ * `apiBase` (que nem existe no schema do config) e fazia `fetch` direto, sem a
62
+ * guarda — com `"apiBase":"https://coletor.malicioso"` no `config.json`, o
63
+ * segundo request ia pra lá com `Bearer nrt_…`, reabrindo o ataque que o
64
+ * LABS #4 fechou aqui; e o `neetruApiUrl` de staging/local era ignorado.
65
+ */
66
+ export declare function resolveTrustedApiUrl(path: string): string;
53
67
  /** @deprecated Sem estado agora (fail-closed não tem flag one-shot). No-op mantido p/ compat de teste. */
54
68
  export declare function _resetUntrustedHostWarningForTests(): void;
55
69
  export declare function apiRequest<T>(path: string, opts?: RequestOptions): Promise<T>;
@@ -69,16 +69,32 @@ export function assertTrustedApiHost(rawUrl) {
69
69
  }
70
70
  /** @deprecated Nome legado — agora é fail-closed (throw), não só warning. Use `assertTrustedApiHost`. */
71
71
  export const maybeWarnUntrustedHost = assertTrustedApiHost;
72
+ /**
73
+ * URL absoluta de um path da API do Core: MESMA base (`neetruApiUrl`) e MESMA
74
+ * guarda fail-closed de `apiRequest`/`apiStream`. Lança `CliNetworkError` se a
75
+ * base estiver fora da allowlist — antes de qualquer Bearer sair da máquina.
76
+ *
77
+ * Todo `fetch` que leve o token da CLI e não passe por `apiRequest`/`apiStream`
78
+ * (ex.: o SSE de `neetru logs -f`, que precisa ler o corpo aos poucos) monta a
79
+ * URL por aqui. C7 (2026-10-13): o stream do `logs -f` lia uma chave
80
+ * `apiBase` (que nem existe no schema do config) e fazia `fetch` direto, sem a
81
+ * guarda — com `"apiBase":"https://coletor.malicioso"` no `config.json`, o
82
+ * segundo request ia pra lá com `Bearer nrt_…`, reabrindo o ataque que o
83
+ * LABS #4 fechou aqui; e o `neetruApiUrl` de staging/local era ignorado.
84
+ */
85
+ export function resolveTrustedApiUrl(path) {
86
+ const baseUrl = buildBaseUrl();
87
+ assertTrustedApiHost(baseUrl);
88
+ return `${baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
89
+ }
72
90
  /** @deprecated Sem estado agora (fail-closed não tem flag one-shot). No-op mantido p/ compat de teste. */
73
91
  export function _resetUntrustedHostWarningForTests() {
74
92
  /* no-op */
75
93
  }
76
94
  export async function apiRequest(path, opts = {}) {
77
- const baseUrl = buildBaseUrl();
78
- const url = `${baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
79
95
  // LABS #4 (2026-09-28): fail-closed — bloqueia host fora da allowlist ANTES
80
96
  // de anexar o Bearer (era só warning que mandava o token assim mesmo).
81
- assertTrustedApiHost(baseUrl);
97
+ const url = resolveTrustedApiUrl(path);
82
98
  // MASTER-HIGH-048 (2.11.0): prefere auth.json (canonico) sobre Conf (legacy).
83
99
  const token = opts.token ?? getActiveTokenSync();
84
100
  const headers = {
@@ -185,6 +201,45 @@ export async function apiRequest(path, opts = {}) {
185
201
  else if (typeof parsed === 'string' && parsed.trim() !== '') {
186
202
  message = parsed;
187
203
  }
204
+ else if (errBody && typeof errBody === 'object') {
205
+ // bug_a3c4d7a8 / achado 2026-10-12: o Core tem TRÊS formatos de erro e
206
+ // este ramo cobre o terceiro, que antes caía no chão inteiro.
207
+ //
208
+ // 1. `{ error: { code, message } }` — canônico (`cliJsonError`)
209
+ // 2. `{ error: "x", error_description }` — RFC 6749 (OAuth)
210
+ // 3. `{ ok: false, code, reason, ... }` — rotas que respondem direto
211
+ //
212
+ // O formato 3 NÃO tem a chave `error`, então o `if (errBody?.error)` acima
213
+ // era pulado e tudo virava `Erro HTTP <status>`. Medido no campo: o
214
+ // `GET /servers/{id}/logs?source=startup` devolve 410 com `code`, um
215
+ // `reason` de dez linhas e quatro `alternatives` acionáveis — e o operador
216
+ // lia apenas "Requisição rejeitada (410): Erro HTTP 410". Pior: o
217
+ // `servers get` RECOMENDA esse comando justamente quando o agente morreu,
218
+ // então a mensagem inútil chega no pior momento possível.
219
+ const cru = errBody;
220
+ if (typeof cru.code === 'string' && cru.code)
221
+ code = cru.code;
222
+ // `reason` primeiro: é o campo que as rotas do formato 3 usam pro texto
223
+ // longo. `message`/`detail`/`hint` cobrem as variações que já existem.
224
+ for (const chave of ['reason', 'message', 'detail', 'hint']) {
225
+ const v = cru[chave];
226
+ if (typeof v === 'string' && v.trim() !== '') {
227
+ message = v;
228
+ break;
229
+ }
230
+ }
231
+ // O que fazer EM VEZ disso é a parte que salva o operador; sem anexar
232
+ // aqui, ela morre no servidor. Só strings — nada de despejar objeto na
233
+ // cara de quem lê.
234
+ const alt = cru.alternatives;
235
+ if (alt && typeof alt === 'object' && !Array.isArray(alt)) {
236
+ const linhas = Object.entries(alt)
237
+ .filter(([, v]) => typeof v === 'string' && v.trim() !== '')
238
+ .map(([k, v]) => ` ${k}: ${v}`);
239
+ if (linhas.length > 0)
240
+ message = `${message}\n\nAlternativas:\n${linhas.join('\n')}`;
241
+ }
242
+ }
188
243
  // LABS #6 (2026-09-28): captura o x-correlation-id do Core p/ o rodapé do erro.
189
244
  const correlationId = response.headers.get('x-correlation-id') ?? undefined;
190
245
  throw new CliApiError(response.status, code, message, correlationId, parseRetryAfter(response.headers.get('retry-after')));
@@ -200,10 +255,8 @@ export async function apiRequest(path, opts = {}) {
200
255
  * `opts.signal` (Ctrl-C). Erros HTTP antes do stream abrir viram `CliApiError`.
201
256
  */
202
257
  export async function* apiStream(path, opts = {}) {
203
- const baseUrl = buildBaseUrl();
204
- const url = `${baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
205
258
  // LABS #4 (2026-09-28): fail-closed também no stream.
206
- assertTrustedApiHost(baseUrl);
259
+ const url = resolveTrustedApiUrl(path);
207
260
  // MASTER-HIGH-048 (2.11.0): mesmo helper aplica ao stream NDJSON.
208
261
  const token = opts.token ?? getActiveTokenSync();
209
262
  const headers = {
@@ -1,3 +1,4 @@
1
+ import { NeetruCliError } from '../cli-kit/errors.js';
1
2
  /** Exit codes canônicos, exportados pra teste/documentação. */
2
3
  export declare const API_ERROR_EXIT: {
3
4
  readonly client: 1;
@@ -5,4 +6,17 @@ export declare const API_ERROR_EXIT: {
5
6
  readonly forbidden: 3;
6
7
  readonly server: 4;
7
8
  };
9
+ /**
10
+ * O que fazer diante de `401 mfa_required`. bug_757b9746 nasceu aqui: o texto
11
+ * mandava passar `--mfa-token` a qualquer comando, inclusive aos que não
12
+ * declaravam a flag. Agora a flag só é citada se o comando em execução a
13
+ * declara; senão, a alternativa que existe sempre — a sessão de 1h.
14
+ */
15
+ export declare function mfaRequiredAdvice(): string;
16
+ /**
17
+ * Traduz qualquer erro para o `NeetruCliError` canônico — sem imprimir nem
18
+ * encerrar. Exportado para teste e para quem precise agregar erros de API
19
+ * como filhos de uma falha parcial.
20
+ */
21
+ export declare function cliErrorFromApiError(error: unknown): NeetruCliError;
8
22
  export declare function handleApiError(error: unknown, json?: boolean): never;
@@ -16,23 +16,29 @@
16
16
  *
17
17
  * Imprime a mensagem adequada (texto humano ou JSON com `--json`) e encerra o
18
18
  * processo — nunca retorna (`never`).
19
+ *
20
+ * bug_89d2497e (2026-10-17): o erro de API vira um `NeetruCliError` (cli-kit)
21
+ * com o exit code da tabela acima e o erro original em `cause`; quem imprime e
22
+ * encerra é `exitWithCliError`. O formato do JSON e os códigos de saída são os
23
+ * mesmos de antes. Um `NeetruCliError` que chegue aqui (ex.: lançado dentro do
24
+ * `try` de um comando) sai com o código DELE, sem ser reclassificado.
19
25
  */
20
26
  import { CliApiError, CliNetworkError } from './api-client.js';
21
- import { log } from '../utils/logger.js';
22
- import { safeExit } from '../utils/safe-exit.js';
23
- /** Exit codes canônicos, exportados pra teste/documentação. */
24
- export const API_ERROR_EXIT = {
25
- client: 1, // 4xx de input/recurso
26
- unauthenticated: 2, // 401
27
- forbidden: 3, // 403
28
- server: 4, // 5xx / rede / desconhecido
29
- };
30
- function emit(json, payload, text) {
31
- if (json)
32
- console.log(JSON.stringify(payload));
33
- else
34
- log.error(text);
27
+ import { EXIT_CODES, NeetruCliError } from '../cli-kit/errors.js';
28
+ import { adviseCommand, adviseFlag } from '../cli-kit/command-context.js';
29
+ import { exitWithCliError } from './cli-error.js';
30
+ /**
31
+ * Reconhece o `SafeExitHalt` de `utils/safe-exit.ts` pela marca, sem importar
32
+ * `isSafeExitHalt`: dezenas de testes mockam `safe-exit.js` só com `safeExit`,
33
+ * e ler outra exportação de um mock incompleto lança no vitest.
34
+ */
35
+ function isSafeExitHalt(x) {
36
+ return (typeof x === 'object' &&
37
+ x !== null &&
38
+ x.__neetruSafeExit === true);
35
39
  }
40
+ /** Exit codes canônicos, exportados pra teste/documentação. */
41
+ export const API_ERROR_EXIT = EXIT_CODES;
36
42
  /**
37
43
  * A mensagem do 401 vale ser exibida? O Core legado mandava só
38
44
  * `{ error: 'Unauthorized' }` (o parser vira message='Unauthorized') — aí o
@@ -45,7 +51,30 @@ function isInformative401Message(message) {
45
51
  return false;
46
52
  return !/^(unauthori[sz]ed|erro http 401)$/i.test(m);
47
53
  }
48
- export function handleApiError(error, json) {
54
+ /**
55
+ * O que fazer diante de `401 mfa_required`. bug_757b9746 nasceu aqui: o texto
56
+ * mandava passar `--mfa-token` a qualquer comando, inclusive aos que não
57
+ * declaravam a flag. Agora a flag só é citada se o comando em execução a
58
+ * declara; senão, a alternativa que existe sempre — a sessão de 1h.
59
+ */
60
+ export function mfaRequiredAdvice() {
61
+ // P105: o comando alternativo também é validado — contra a árvore do
62
+ // Commander. Sem ele (ou sem árvore conhecida), a dica não o cita.
63
+ const sessao = (withCommand, withoutCommand) => adviseCommand({ command: 'auth mfa-session', withCommand, withoutCommand });
64
+ return adviseFlag({
65
+ flag: '--mfa-token',
66
+ withFlag: sessao(' Passe --mfa-token <código TOTP> ou rode `neetru auth mfa-session` (sessão de 1h).', ' Passe --mfa-token <código TOTP>.'),
67
+ withoutFlag: sessao(' Rode `neetru auth mfa-session` (sessão de 1h) e repita o comando.', ' Este comando exige step-up MFA e esta CLI não tem, nele, como enviá-lo.'),
68
+ });
69
+ }
70
+ /**
71
+ * Traduz qualquer erro para o `NeetruCliError` canônico — sem imprimir nem
72
+ * encerrar. Exportado para teste e para quem precise agregar erros de API
73
+ * como filhos de uma falha parcial.
74
+ */
75
+ export function cliErrorFromApiError(error) {
76
+ if (error instanceof NeetruCliError)
77
+ return error;
49
78
  if (error instanceof CliApiError) {
50
79
  const s = error.status;
51
80
  if (s === 401) {
@@ -54,48 +83,80 @@ export function handleApiError(error, json) {
54
83
  // Antes TODO 401 virava "Token inválido ou revogado" — inclusive o de MFA
55
84
  // faltando, mandando o operador fazer `neetru login` à toa.
56
85
  if (error.code === 'mfa_required') {
57
- emit(json, { ok: false, error: 'mfa_required', code: error.code, status: s, message: error.message }, `Step-up MFA exigido: ${error.message}
58
- ` +
59
- ' Passe --mfa-token <código TOTP> ou rode `neetru auth mfa-session` (sessão de 1h).');
60
- safeExit(API_ERROR_EXIT.unauthenticated);
86
+ return new NeetruCliError(error.message, {
87
+ code: 'mfa_required',
88
+ exit: EXIT_CODES.unauthenticated,
89
+ cause: error,
90
+ details: { code: error.code, status: s },
91
+ human: `Step-up MFA exigido: ${error.message}\n${mfaRequiredAdvice()}`,
92
+ });
61
93
  }
62
94
  const serverMsg = isInformative401Message(error.message) ? error.message : '';
63
- emit(json, {
64
- ok: false,
65
- error: 'unauthorized',
66
- ...(error.code && error.code !== 'http_401' ? { code: error.code } : {}),
67
- status: s,
68
- message: error.message,
69
- }, serverMsg
70
- ? `Não autenticado: ${serverMsg}${/neetru login/.test(serverMsg) ? '' : ' — execute: neetru login'}`
71
- : 'Token inválido ou revogado. Execute: neetru login');
72
- safeExit(API_ERROR_EXIT.unauthenticated);
95
+ return new NeetruCliError(error.message, {
96
+ code: 'unauthorized',
97
+ exit: EXIT_CODES.unauthenticated,
98
+ cause: error,
99
+ details: {
100
+ ...(error.code && error.code !== 'http_401' ? { code: error.code } : {}),
101
+ status: s,
102
+ },
103
+ human: serverMsg
104
+ ? `Não autenticado: ${serverMsg}${/neetru login/.test(serverMsg) ? '' : ' — execute: neetru login'}`
105
+ : 'Token inválido ou revogado. Execute: neetru login',
106
+ });
73
107
  }
74
108
  if (s === 403) {
75
- emit(json, { ok: false, error: 'forbidden', status: s, message: error.message }, `Acesso negado: ${error.message}`);
76
- safeExit(API_ERROR_EXIT.forbidden);
109
+ return new NeetruCliError(error.message, {
110
+ code: 'forbidden',
111
+ exit: EXIT_CODES.forbidden,
112
+ cause: error,
113
+ details: { status: s },
114
+ human: `Acesso negado: ${error.message}`,
115
+ });
77
116
  }
78
117
  if (s >= 400 && s < 500) {
79
- emit(json, {
80
- ok: false,
81
- error: s === 404 ? 'not_found' : (error.code ?? 'client_error'),
82
- status: s,
83
- message: error.message,
84
- }, s === 404
85
- ? error.message || 'Recurso não encontrado.'
86
- : `Requisição rejeitada (${s}): ${error.message}`);
87
- safeExit(API_ERROR_EXIT.client);
118
+ // Valores válidos de um enum recusado pelo Core (ex.: `environmentId
119
+ // inválido — use: dev-local | staging | prod.`) vêm no `message` do
120
+ // próprio Core, que é a fonte que validou: o texto passa inteiro, sem
121
+ // corte e sem lista copiada do lado de cá.
122
+ return new NeetruCliError(error.message, {
123
+ code: s === 404 ? 'not_found' : (error.code ?? 'client_error'),
124
+ exit: EXIT_CODES.client,
125
+ cause: error,
126
+ details: { status: s },
127
+ human: s === 404
128
+ ? error.message || 'Recurso não encontrado.'
129
+ : `Requisição rejeitada (${s}): ${error.message}`,
130
+ });
88
131
  }
89
132
  // 5xx (incl. 501 — não implementado).
90
- emit(json, { ok: false, error: error.code ?? 'server_error', status: s, message: error.message }, `Erro do servidor (${s}): ${error.message}`);
91
- safeExit(API_ERROR_EXIT.server);
133
+ return new NeetruCliError(error.message, {
134
+ code: error.code ?? 'server_error',
135
+ exit: EXIT_CODES.server,
136
+ cause: error,
137
+ details: { status: s },
138
+ human: `Erro do servidor (${s}): ${error.message}`,
139
+ });
92
140
  }
93
141
  if (error instanceof CliNetworkError) {
94
- emit(json, { ok: false, error: 'network', message: error.message }, `Falha de rede: ${error.message}`);
95
- safeExit(API_ERROR_EXIT.server);
142
+ return new NeetruCliError(error.message, {
143
+ code: 'network',
144
+ exit: EXIT_CODES.server,
145
+ cause: error,
146
+ human: `Falha de rede: ${error.message}`,
147
+ });
96
148
  }
97
149
  const message = error instanceof Error ? error.message : String(error);
98
- emit(json, { ok: false, error: 'unknown', message }, message);
99
- safeExit(API_ERROR_EXIT.server);
150
+ return new NeetruCliError(message, { code: 'unknown', exit: EXIT_CODES.server, cause: error });
151
+ }
152
+ export function handleApiError(error, json) {
153
+ // `safeExit` lança `SafeExitHalt` em produção para parar o caller. Dentro de
154
+ // `try { …; safeExit(n) } catch (e) { handleApiError(e) }` o sinal caía aqui
155
+ // e virava um segundo erro "[object Object]" — e, em `--json`, uma SEGUNDA
156
+ // linha de JSON no stdout. A saída já está agendada com o código certo:
157
+ // o sinal só segue adiante.
158
+ if (isSafeExitHalt(error))
159
+ throw error;
160
+ return exitWithCliError(cliErrorFromApiError(error), json);
100
161
  }
101
162
  //# sourceMappingURL=api-error.js.map