@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
@@ -13,6 +13,7 @@ import chalk from 'chalk';
13
13
  import { apiRequest, CliApiError, CliNetworkError } from '../lib/api-client.js';
14
14
  import { requireToken, handleApiError, confirmDestructive, printWriteResult, resolveStepUp, } from '../lib/cli-write.js';
15
15
  import { pickProductId, pickServer, pickTenant, promptChoice, promptText } from '../lib/pickers.js';
16
+ import { DEFAULT_TENANT_ENVIRONMENT_ID, ESCOLHAS_AMBIENTE_TENANT, resolverAmbienteTenant, } from '../lib/envs.js';
16
17
  import { log } from '../utils/logger.js';
17
18
  import { safeExit } from '../utils/safe-exit.js';
18
19
  /** Monta o texto do console de deploy (estilo GCP): um bloco por step. */
@@ -39,12 +40,16 @@ export async function runDeploymentsCreate(opts) {
39
40
  message: 'Tenant alvo:',
40
41
  });
41
42
  const version = opts.version ?? (await promptText('Versao:', { json: opts.json }));
42
- const env = opts.env ??
43
- (await promptChoice('Ambiente:', [
44
- { name: 'dev', value: 'dev' },
45
- { name: 'staging', value: 'staging' },
46
- { name: 'prod', value: 'prod' },
47
- ], { default: 'dev', json: opts.json }));
43
+ // bug_7dcb68e2666a43f69ef362d2cbff94ed: POST /api/cli/v1/deployments valida
44
+ // environmentId contra ['dev-local','staging','prod'] por igualdade ESTRITA.
45
+ // Oferecer 'dev' no prompt (e usa-lo como padrao) garantia 400
46
+ // validation_failed. Alfabeto unico em ../lib/envs.js.
47
+ const env = opts.env
48
+ ? resolverAmbienteTenant(opts.env, opts.json)
49
+ : (await promptChoice('Ambiente:', [...ESCOLHAS_AMBIENTE_TENANT], {
50
+ default: DEFAULT_TENANT_ENVIRONMENT_ID,
51
+ json: opts.json,
52
+ }));
48
53
  const server = await pickServer(token, {
49
54
  fromFlag: opts.server,
50
55
  status: 'online',
@@ -7,12 +7,22 @@ import { listProductDatabases, listProducts, listWorkspaces, resolveProductIdent
7
7
  import { requireToken, handleApiError } from '../lib/cli-read.js';
8
8
  import { log } from '../utils/logger.js';
9
9
  import { safeExit } from '../utils/safe-exit.js';
10
- function normalizeEnv(value) {
10
+ import { invalidChoiceError } from '../cli-kit/choices.js';
11
+ import { exitWithCliError } from '../lib/cli-error.js';
12
+ /** Ambientes aceitos por `neetru dev sync --env` — validação e mensagem saem daqui. */
13
+ const SYNC_ENVS = ['dev', 'dev-local', 'dev-core'];
14
+ function normalizeEnv(value, json) {
11
15
  const raw = (value ?? 'dev').trim().toLowerCase();
12
- if (raw === 'dev' || raw === 'dev-local' || raw === 'dev-core')
16
+ if (SYNC_ENVS.includes(raw))
13
17
  return raw;
14
- log.error(`Ambiente "${value}" inválido para dev-local. Use: dev | dev-local | dev-core.`);
15
- safeExit(1);
18
+ // bug_89d2497e: a lista da mensagem é a MESMA que validou (`SYNC_ENVS`).
19
+ return exitWithCliError(invalidChoiceError({
20
+ flag: '--env',
21
+ value,
22
+ valid: SYNC_ENVS,
23
+ subject: 'Ambiente do dev-local',
24
+ code: 'invalid_env',
25
+ }), json);
16
26
  }
17
27
  function envMatches(rowEnv, wanted) {
18
28
  const env = (rowEnv ?? '').toLowerCase();
@@ -79,7 +89,7 @@ async function resolveProductForSync(token, opts) {
79
89
  }
80
90
  async function syncFromCore(cwd, opts) {
81
91
  const token = await requireToken(opts.json);
82
- const env = normalizeEnv(opts.env);
92
+ const env = normalizeEnv(opts.env, opts.json);
83
93
  const current = resolveDevContext(cwd);
84
94
  if (current?.mode === 'prototype' && !opts.product && !opts.productId) {
85
95
  log.error('Este projeto está marcado como protótipo local. Use `neetru dev use --product <slug>` para ligar ao Core.');
@@ -1,6 +1,7 @@
1
1
  import { type CheckResult } from '../lib/product-docs/check.js';
2
2
  import { type InitDocsResult } from '../lib/product-docs/ops.js';
3
3
  import { type SyncResult } from '../lib/product-docs/sync.js';
4
+ import { NeetruCliError } from '../cli-kit/errors.js';
4
5
  export interface DocsInitOptions {
5
6
  comApi?: boolean;
6
7
  migrar?: boolean;
@@ -37,6 +38,30 @@ export interface DocsSyncOptions {
37
38
  offline?: boolean;
38
39
  json?: boolean;
39
40
  }
41
+ /**
42
+ * As falhas de envio como UM erro tipado com filhos (P105 / bug_89d2497e,
43
+ * etapa 3 aplicada ao `docs sync`). Com docs que deram certo (enviados ou já
44
+ * iguais no Core) a falha se reporta como PARCIAL — no texto ("Falha parcial:
45
+ * 1 de 3 itens falharam") e no JSON (`partial`, `succeeded`, `failed`,
46
+ * `children`) — e sai com o código do filho mais grave. `null` sem falhas.
47
+ */
48
+ export declare function erroDeSyncComFalhas(r: SyncResult): NeetruCliError | null;
49
+ /** O desfecho do `docs sync`, o MESMO para a saída humana e o `--json`. */
50
+ export interface DesfechoDoSync {
51
+ ok: boolean;
52
+ exit: number;
53
+ /** As falhas de envio como erro tipado, ou `null` sem falhas. */
54
+ erro: NeetruCliError | null;
55
+ }
56
+ /**
57
+ * Uma regra só para os dois modos (revisão do #87, BAIXO 2). Qualquer doc que
58
+ * falhou no envio faz o comando falhar com o código do filho mais grave —
59
+ * MESMO que o lote termine num status informativo (`core-sem-suporte` no meio
60
+ * do lote: o doc 1 tomou 500 e o doc 2 tomou 405). Antes a saída humana
61
+ * escondia a falha e saía 0 enquanto o `--json` saía 4. Sem falhas de envio,
62
+ * vale a regra de sempre: status de `SYNC_FAILURE` sai 1, o resto sai 0.
63
+ */
64
+ export declare function desfechoDoSync(r: SyncResult): DesfechoDoSync;
40
65
  export declare function runDocsSync(opts: DocsSyncOptions): Promise<void>;
41
66
  export interface DeployDocsPrecheck {
42
67
  ran: boolean;
@@ -19,6 +19,10 @@ import { checkProductDocs } from '../lib/product-docs/check.js';
19
19
  import { resolveTaxonomy } from '../lib/product-docs/taxonomy.js';
20
20
  import { createProductDoc, DocsNewError, initProductDocs, migrateProductDocs, } from '../lib/product-docs/ops.js';
21
21
  import { syncProductDocs } from '../lib/product-docs/sync.js';
22
+ import { NeetruCliError } from '../cli-kit/errors.js';
23
+ import { CliApiError, CliNetworkError } from '../lib/api-client.js';
24
+ import { cliErrorFromApiError } from '../lib/api-error.js';
25
+ import { exitWithCliError } from '../lib/cli-error.js';
22
26
  function fail(json, code, message) {
23
27
  if (json)
24
28
  console.log(JSON.stringify({ ok: false, error: code, message }));
@@ -207,8 +211,7 @@ function printSync(r) {
207
211
  log.warn(`Não enviado (erro no check: ${b.codes.join(', ')}): ${b.file}`);
208
212
  for (const x of r.recusados)
209
213
  log.warn(`Não pode subir: ${x.file} — ${x.motivo}.`);
210
- for (const f of r.falhas)
211
- log.error(`Falha em ${f.slug}: ${f.message}`);
214
+ // As falhas de envio saem no erro tipado (`erroDeSyncComFalhas`), uma vez só.
212
215
  if (r.orfaos.length) {
213
216
  log.warn(`${r.orfaos.length} doc(s) existem no Core mas não no repositório (nada foi apagado): ${r.orfaos.join(', ')}`);
214
217
  log.dim(' A remoção de docs de produto ainda não existe no Core — eles continuam lá até o Core oferecer isso.');
@@ -216,6 +219,52 @@ function printSync(r) {
216
219
  }
217
220
  /** Status de sync que contam como falha do comando `neetru docs sync`. */
218
221
  const SYNC_FAILURE = ['parcial', 'sem-login', 'limite-excedido', 'produto-nao-encontrado', 'falha'];
222
+ /**
223
+ * Código de saída de um doc que o Core recusou: a MESMA tabela do
224
+ * `handleApiError` (401→2, 403→3, outro 4xx→1, 5xx e sem resposta→4), lida do
225
+ * próprio `cliErrorFromApiError` em vez de copiada.
226
+ */
227
+ function exitDaFalha(httpStatus) {
228
+ const erro = httpStatus > 0 ? new CliApiError(httpStatus, 'docs_sync', '') : new CliNetworkError('');
229
+ return cliErrorFromApiError(erro).exit;
230
+ }
231
+ /**
232
+ * As falhas de envio como UM erro tipado com filhos (P105 / bug_89d2497e,
233
+ * etapa 3 aplicada ao `docs sync`). Com docs que deram certo (enviados ou já
234
+ * iguais no Core) a falha se reporta como PARCIAL — no texto ("Falha parcial:
235
+ * 1 de 3 itens falharam") e no JSON (`partial`, `succeeded`, `failed`,
236
+ * `children`) — e sai com o código do filho mais grave. `null` sem falhas.
237
+ */
238
+ export function erroDeSyncComFalhas(r) {
239
+ if (r.falhas.length === 0)
240
+ return null;
241
+ const children = r.falhas.map((f) => new NeetruCliError(f.message, {
242
+ code: f.httpStatus > 0 ? `http_${f.httpStatus}` : 'sem_resposta',
243
+ exit: exitDaFalha(f.httpStatus),
244
+ details: { slug: f.slug, httpStatus: f.httpStatus },
245
+ human: `Falha em ${f.slug}: ${f.message}`,
246
+ }));
247
+ return new NeetruCliError(r.message, {
248
+ code: r.status === 'parcial' ? 'docs_sync_parcial' : 'docs_sync_falha',
249
+ children,
250
+ succeeded: r.enviados.length + r.inalterados.length,
251
+ });
252
+ }
253
+ /**
254
+ * Uma regra só para os dois modos (revisão do #87, BAIXO 2). Qualquer doc que
255
+ * falhou no envio faz o comando falhar com o código do filho mais grave —
256
+ * MESMO que o lote termine num status informativo (`core-sem-suporte` no meio
257
+ * do lote: o doc 1 tomou 500 e o doc 2 tomou 405). Antes a saída humana
258
+ * escondia a falha e saía 0 enquanto o `--json` saía 4. Sem falhas de envio,
259
+ * vale a regra de sempre: status de `SYNC_FAILURE` sai 1, o resto sai 0.
260
+ */
261
+ export function desfechoDoSync(r) {
262
+ const erro = erroDeSyncComFalhas(r);
263
+ if (erro)
264
+ return { ok: false, exit: erro.exit, erro };
265
+ const failed = SYNC_FAILURE.includes(r.status);
266
+ return { ok: !failed, exit: failed ? 1 : 0, erro: null };
267
+ }
219
268
  export async function runDocsSync(opts) {
220
269
  const repo = await loadRepoOrExit(opts.json);
221
270
  const tax = await resolveTaxonomy({ offline: opts.offline });
@@ -226,22 +275,35 @@ export async function runDocsSync(opts) {
226
275
  ? undefined
227
276
  : (ms, motivo) => log.dim(` ${motivo === 'ritmo' ? 'Respeitando o limite de envios do Core' : 'O Core pediu para esperar'}: aguardando ${Math.ceil(ms / 1000)} s…`),
228
277
  });
229
- const failed = SYNC_FAILURE.includes(r.status);
278
+ const desfecho = desfechoDoSync(r);
230
279
  if (opts.json) {
231
- console.log(JSON.stringify({ ok: !failed, ...r }));
232
- if (failed)
233
- safeExit(1);
280
+ // O resultado inteiro de antes e, com falhas de envio, o agregado do erro
281
+ // tipado (`error`, `partial`, `succeeded`, `failed`, `children`).
282
+ let agregado = {};
283
+ if (desfecho.erro) {
284
+ const { ok: _ok, ...resto } = desfecho.erro.toJSON();
285
+ agregado = resto;
286
+ }
287
+ console.log(JSON.stringify({ ok: desfecho.ok, ...r, ...agregado }));
288
+ if (desfecho.exit !== 0)
289
+ safeExit(desfecho.exit);
234
290
  return;
235
291
  }
236
292
  log.heading(`Publicar docs de ${repo.slug} no Core${r.dryRun ? ' (simulação)' : ''}`);
237
293
  printSync(r);
294
+ if (desfecho.erro) {
295
+ // O cabeçalho é a mensagem do status (inclusive o "Core sem suporte"); as
296
+ // falhas de envio vêm logo abaixo, uma por doc — nunca somem.
297
+ exitWithCliError(desfecho.erro, false);
298
+ return;
299
+ }
238
300
  if (r.status === 'core-sem-suporte' || r.status === 'desligado' || r.status === 'nada-a-enviar') {
239
301
  log.info(r.message);
240
302
  return;
241
303
  }
242
- if (failed) {
304
+ if (desfecho.exit !== 0) {
243
305
  log.error(r.message);
244
- safeExit(1);
306
+ safeExit(desfecho.exit);
245
307
  }
246
308
  log.success(r.message);
247
309
  }
@@ -25,7 +25,13 @@ import { log } from '../utils/logger.js';
25
25
  // Fix do bug onde 2.0.0 ficava hardcoded depois de bumps.
26
26
  import { CLI_VERSION } from '../version.js';
27
27
  import { safeExit } from '../utils/safe-exit.js';
28
+ import { RUNTIME_ENVIRONMENT_IDS, TENANT_ENVIRONMENT_IDS } from '../lib/envs.js';
28
29
  const NPM_REGISTRY = 'https://registry.npmjs.org/@neetru/cli';
30
+ /**
31
+ * `NEETRU_ENV` que o doctor ainda aceita sem estar em alfabeto nenhum — legado
32
+ * declarado (ver `checkNeetruEnv`), candidato a deprecação.
33
+ */
34
+ const NEETRU_ENV_LEGADOS = ['beta'];
29
35
  /* ── Checks individuais — exportados pra teste ────────────────────── */
30
36
  export async function checkToken() {
31
37
  const t = getActiveTokenSync();
@@ -92,7 +98,36 @@ export async function checkConfigSchema(cwd) {
92
98
  export async function checkNeetruEnv(cwd) {
93
99
  // bug: 'production' não é valor canônico do SDK/Core — o correto é 'prod'.
94
100
  // 'production' causava doctor OK mas runtime falhava ao comparar NEETRU_ENV.
95
- const valid = ['dev', 'workspace', 'beta', 'prod'];
101
+ // 2026-10-12: a lista rejeitava `dev-local`, que o Core INJETA.
102
+ //
103
+ // O Core grava `NEETRU_ENV = environmentId` no deploy
104
+ // (`_vm-deploy-readiness.ts:9` e `:76`), e `environmentId` só pode ser
105
+ // `dev-local`, `staging` ou `prod` (`ENVIRONMENT_IDS`, schemas/envs.ts:39).
106
+ // Com `dev-local` fora desta lista, o doctor reprovava com exit 1 uma
107
+ // configuração que o próprio Core tinha acabado de escrever — e em CI isso
108
+ // quebra o build sobre uma config correta.
109
+ //
110
+ // `staging` entra aqui pelo mesmo motivo (o Core o produz), mas com uma
111
+ // ressalva REGISTRADA e não resolvida: o tipo `NeetruEnv` do SDK
112
+ // (`src/types.ts:873`) lista `dev | workspace | prod | dev-local` e NÃO
113
+ // inclui `staging`. Procurei o guard de runtime (`resolveNeetruEnv`) e ele
114
+ // não existe no `dist` publicado do SDK — só é citado na doc e no código de
115
+ // deploy do Core. Então NÃO consegui confirmar se `staging` quebra em
116
+ // runtime. O doctor para de reprovar (o Core produz esse valor), e a dúvida
117
+ // fica anotada em vez de virar silêncio nos dois sentidos.
118
+ //
119
+ // `workspace` e `beta` ficam por compatibilidade: estavam aceitos antes e
120
+ // remover valor de uma lista de validação quebra quem já configurou. `beta`
121
+ // não aparece em nenhum alfabeto atual — candidato a deprecação declarada,
122
+ // não a remoção silenciosa.
123
+ //
124
+ // P105: a lista era escrita à mão. Agora é a UNIÃO das duas fontes — o
125
+ // `NEETRU_ENV` do SDK (`RUNTIME_ENVIRONMENT_IDS`) e o `environmentId` que o
126
+ // Core injeta (`TENANT_ENVIRONMENT_IDS`) — mais o legado declarado abaixo.
127
+ // Mesmo conjunto de antes; só a ordem do texto "esperado …" mudou.
128
+ const valid = [
129
+ ...new Set([...RUNTIME_ENVIRONMENT_IDS, ...TENANT_ENVIRONMENT_IDS, ...NEETRU_ENV_LEGADOS]),
130
+ ];
96
131
  // Procura .env.local pra ver NEETRU_ENV.
97
132
  try {
98
133
  const raw = await fs.readFile(path.join(cwd, '.env.local'), 'utf8');
@@ -53,6 +53,56 @@ function outcomeColor(outcome) {
53
53
  default: return chalk.dim(outcome);
54
54
  }
55
55
  }
56
+ /**
57
+ * Texto plain PT-BR do motivo de um `outcome: 'skipped'` (bug_6e9fe576).
58
+ * `stage_sem_subdominio`/`subdomain_cloud_run_unsupported` são esperados
59
+ * (dev-local e produto Cloud Run nunca têm domínio por VM); os outros dois
60
+ * exigem ação de quem pediu o reconcile. `undefined` cobre o Core anterior
61
+ * ao #677, que nunca manda `reason` — nesse caso dizemos isso explicitamente
62
+ * em vez de deixar a célula vazia (o ponto do bug original era exatamente
63
+ * "pulei e não disse por quê").
64
+ */
65
+ function skipReasonText(reason) {
66
+ switch (reason) {
67
+ case 'stage_sem_subdominio':
68
+ return 'ambiente local, sem domínio público (esperado)';
69
+ case 'subdomain_cloud_run_unsupported':
70
+ return 'produto Cloud Run, não usa domínio por VM (esperado)';
71
+ case 'subdomain_deploy_config_missing':
72
+ return 'ambiente sem configuração de deploy — corrija o ambiente';
73
+ case 'subdomain_hard_cap_reached':
74
+ return 'limite de subdomínios atingido — requer ação do operador';
75
+ default:
76
+ return 'motivo não informado por este Core (atualize o Core para ver detalhes)';
77
+ }
78
+ }
79
+ /** Célula da coluna "Motivo/erro" — reason em pulados, err em erros, vazio nos demais. */
80
+ function reconcileNote(r) {
81
+ if (r.outcome === 'skipped')
82
+ return skipReasonText(r.reason);
83
+ if (r.outcome === 'error')
84
+ return r.err ?? '';
85
+ return '';
86
+ }
87
+ /** Pulados que exigem ação de quem pediu o reconcile (os outros dois são esperados). */
88
+ const SKIP_EXIGE_ACAO = new Set([
89
+ 'subdomain_deploy_config_missing',
90
+ 'subdomain_hard_cap_reached',
91
+ ]);
92
+ /**
93
+ * A execução teve erro? Olha a contagem `errors` ALÉM do `ok` do Core: o Core
94
+ * anterior ao #677 manda `ok: true` FIXO mesmo com erro (é o próprio
95
+ * bug_6e9fe576), e a CLI publicada conversa com ele até o deploy. Decidir só
96
+ * pelo `ok` reabria a mentira: "Erros: 1" seguido de "tudo já existe" com
97
+ * exit 0 (revisão do cli#80).
98
+ */
99
+ function reconcileFalhou(resp) {
100
+ return resp.ok === false || resp.errors > 0;
101
+ }
102
+ /** Ambientes pulados por um motivo que exige ação (só o Core #677+ informa o motivo). */
103
+ function pendentesDeAcao(resp) {
104
+ return resp.details.filter((r) => r.outcome === 'skipped' && r.reason !== undefined && SKIP_EXIGE_ACAO.has(r.reason));
105
+ }
56
106
  export async function runDomainsList(opts) {
57
107
  if (!opts.product) {
58
108
  const msg = '--product <slug> é obrigatório.';
@@ -95,7 +145,7 @@ export async function runDomainsList(opts) {
95
145
  { header: 'Cert', value: (r) => certStatusColor(r.certStatus), maxWidth: 18 },
96
146
  { header: 'Alvo', value: (r) => r.targetKind ?? '—', maxWidth: 14 },
97
147
  { header: 'Porta', value: (r) => r.upstreamPort != null ? String(r.upstreamPort) : '—', maxWidth: 8 },
98
- { header: 'VM', value: (r) => r.serverId ?? '—', maxWidth: 22 },
148
+ { header: 'VM', value: (r) => r.serverId ?? '—', identificador: true },
99
149
  { header: 'Atualizado', value: (r) => fmtTimestamp(r.updatedAt), maxWidth: 20 },
100
150
  ], resp.subdomains);
101
151
  }
@@ -127,8 +177,15 @@ export async function runDomainsReconcile(opts) {
127
177
  spinner?.fail('Falha ao reconciliar domínios.');
128
178
  handleApiError(err, opts.json);
129
179
  }
180
+ // bug_6e9fe576: `ok:false` é o Core sinalizando que a execução teve erro
181
+ // real (não é exceção de rede/HTTP — a resposta é 200, o resumo que reporta
182
+ // a falha). Trata igual ao resto da CLI que agrega um resultado em lote
183
+ // (`doctor`/`smoke`): --json imprime o corpo e sai já com o código certo,
184
+ // sem cair nos prints humanos abaixo.
130
185
  if (opts.json) {
131
186
  console.log(JSON.stringify(resp));
187
+ if (reconcileFalhou(resp) || pendentesDeAcao(resp).length > 0)
188
+ safeExit(1);
132
189
  return;
133
190
  }
134
191
  log.heading(`Reconcile — "${chalk.bold(resp.productSlug)}"`);
@@ -147,15 +204,39 @@ export async function runDomainsReconcile(opts) {
147
204
  { header: 'Resultado', value: (r) => outcomeColor(r.outcome), maxWidth: 12 },
148
205
  { header: 'FQDN', value: (r) => r.fqdn || '—', maxWidth: 44 },
149
206
  { header: 'Status', value: (r) => subdomainStatusColor(r.status), maxWidth: 18 },
150
- { header: 'Erro', value: (r) => r.err ?? '', maxWidth: 40 },
207
+ { header: 'Motivo/erro', value: reconcileNote, maxWidth: 46 },
151
208
  ], resp.details);
152
209
  }
210
+ // bug_6e9fe576: "Todos os domínios obrigatórios já existem." era impresso
211
+ // mesmo com `skipped > 0` — dois ambientes target=workspace, nada criado,
212
+ // nada existente, e a CLI dizia que tudo estava ok. Agora a frase de
213
+ // sucesso-sem-criação só sai quando NADA foi pulado; com pulados, aponta
214
+ // pra tabela acima (que já tem o motivo, com ou sem `reason` do Core).
215
+ const falhou = reconcileFalhou(resp);
216
+ const exigeAcao = pendentesDeAcao(resp);
153
217
  if (resp.created > 0) {
154
218
  log.success(`${resp.created} subdomínio(s) criado(s). Execute \`neetru domains list --product ${opts.product}\` para ver o status.`);
155
219
  }
156
- else if (resp.errors === 0) {
157
- log.success('Todos os domínios obrigatórios já existem.');
220
+ if (falhou) {
221
+ log.error('Reconcile encontrou erro(s). Veja os detalhes acima.');
158
222
  }
223
+ if (exigeAcao.length > 0) {
224
+ const quais = exigeAcao.map((r) => r.envType).join(', ');
225
+ log.warn(`${exigeAcao.length} ambiente(s) precisam de ação (${quais}) — o domínio não foi garantido. Veja o motivo na tabela acima.`);
226
+ }
227
+ if (resp.created === 0 && !falhou && exigeAcao.length === 0) {
228
+ if (resp.skipped > 0) {
229
+ log.warn(`Nenhum domínio criado — ${resp.skipped} ambiente(s) pulado(s). Veja o motivo na tabela acima.`);
230
+ }
231
+ else {
232
+ log.success('Todos os domínios obrigatórios já existem.');
233
+ }
234
+ }
235
+ // Exit 1 quando houve erro (pela contagem local, não só pelo `ok` do Core) ou
236
+ // quando um ambiente ficou sem domínio por um motivo que exige ação — como o
237
+ // doctor/smoke, que derivam o exit do resultado que a própria CLI conta.
238
+ if (falhou || exigeAcao.length > 0)
239
+ safeExit(1);
159
240
  }
160
241
  export async function runDomainsRetry(opts) {
161
242
  if (!opts.product) {
@@ -1,6 +1,12 @@
1
- declare const SUPPORTED: readonly ["dev", "workspace", "prod", "production"];
2
- export type EnvTarget = (typeof SUPPORTED)[number];
3
- type StoredEnvTarget = 'dev' | 'workspace' | 'prod';
1
+ import { type RuntimeEnvironmentId } from '../lib/envs.js';
2
+ /**
3
+ * O `NEETRU_ENV` do SDK sai da tabela `VOCABULARIOS_DE_AMBIENTE` (`lib/envs.ts`,
4
+ * P105) — era `['dev','workspace','prod']` escrito à mão — mais o apelido
5
+ * `production`, aceito na entrada e gravado como `prod`.
6
+ */
7
+ declare const APELIDO_PRODUCTION = "production";
8
+ export type EnvTarget = RuntimeEnvironmentId | typeof APELIDO_PRODUCTION;
9
+ type StoredEnvTarget = RuntimeEnvironmentId;
4
10
  export interface EnvSwitchOptions {
5
11
  target: string;
6
12
  cwd?: string;
@@ -8,7 +8,15 @@ import * as fs from 'node:fs/promises';
8
8
  import * as fsSync from 'node:fs';
9
9
  import * as path from 'node:path';
10
10
  import { log } from '../utils/logger.js';
11
- const SUPPORTED = ['dev', 'workspace', 'prod', 'production'];
11
+ import { problemaDeCodificacao, recusaDoEnvSwitch } from '../lib/texto-utf8.js';
12
+ import { RUNTIME_ENVIRONMENT_IDS } from '../lib/envs.js';
13
+ /**
14
+ * O `NEETRU_ENV` do SDK sai da tabela `VOCABULARIOS_DE_AMBIENTE` (`lib/envs.ts`,
15
+ * P105) — era `['dev','workspace','prod']` escrito à mão — mais o apelido
16
+ * `production`, aceito na entrada e gravado como `prod`.
17
+ */
18
+ const APELIDO_PRODUCTION = 'production';
19
+ const SUPPORTED = [...RUNTIME_ENVIRONMENT_IDS, APELIDO_PRODUCTION];
12
20
  /**
13
21
  * Normaliza um target de ambiente (aceita o alias `production` → `prod`) ou
14
22
  * `null` se não reconhecido. Exportada (2026-07-22) pra `neetru build
@@ -18,7 +26,7 @@ const SUPPORTED = ['dev', 'workspace', 'prod', 'production'];
18
26
  export function normalizeTarget(t) {
19
27
  if (!SUPPORTED.includes(t))
20
28
  return null;
21
- return t === 'production' ? 'prod' : t;
29
+ return t === APELIDO_PRODUCTION ? 'prod' : t;
22
30
  }
23
31
  /**
24
32
  * Substitui ou adiciona `NEETRU_ENV=<value>` em `content` (texto do .env).
@@ -60,7 +68,19 @@ export async function runEnvSwitch(opts) {
60
68
  const filePath = path.join(cwd, file);
61
69
  let content = '';
62
70
  if (fsSync.existsSync(filePath)) {
63
- content = await fs.readFile(filePath, 'utf-8');
71
+ // Rodada final 2 do cli#81 (BAIXA): `readFile(…, 'utf-8')` trocava cada
72
+ // byte que não é UTF-8 por U+FFFD, e a regravação abaixo levava a troca ao
73
+ // disco — um ANSI com `Café` terminava `Caf\uFFFD`, com exit 0 e "✓".
74
+ // Agora lê em bytes e recusa ANTES de gravar o que a regravação estragaria.
75
+ const bruto = await fs.readFile(filePath);
76
+ const problema = problemaDeCodificacao(bruto);
77
+ if (problema === 'utf16' || problema === 'nao-utf8') {
78
+ log.error(recusaDoEnvSwitch(file, problema).message);
79
+ safeExit(1);
80
+ return;
81
+ }
82
+ // A marca BOM fica no texto e volta ao disco como estava.
83
+ content = bruto.toString('utf8');
64
84
  }
65
85
  const updated = applyEnvSwitch(content, storedTarget);
66
86
  await fs.writeFile(filePath, updated, 'utf-8');
@@ -68,7 +68,7 @@ export async function runHostingList(opts) {
68
68
  }
69
69
  renderTable([
70
70
  { header: 'HOST', value: (d) => d.host ?? '—', maxWidth: 32 },
71
- { header: 'TENANT', value: (d) => d.tenantId ?? '—', maxWidth: 22 },
71
+ { header: 'TENANT', value: (d) => d.tenantId ?? '—', identificador: true },
72
72
  { header: 'STATUS', value: (d) => d.status ?? '—' },
73
73
  { header: 'CERT', value: (d) => d.certStatus ?? '—' },
74
74
  { header: 'TARGET', value: (d) => d.targetService ?? '—', maxWidth: 24 },
@@ -98,7 +98,7 @@ export async function runBuildsList(opts) {
98
98
  }
99
99
  log.heading(`Cloud Builds (${res.builds.length})`);
100
100
  renderTable([
101
- { header: 'ID', value: (b) => b.id, maxWidth: 36 },
101
+ { header: 'ID', value: (b) => b.id, identificador: true },
102
102
  { header: 'STATUS', value: (b) => b.status },
103
103
  { header: 'REGIÃO', value: (b) => b.region ?? 'global' },
104
104
  { header: 'SOURCE', value: (b) => b.source ?? '—', maxWidth: 28 },
@@ -1,7 +1,11 @@
1
1
  import * as fs from 'fs/promises';
2
2
  import * as path from 'path';
3
3
  import ora from 'ora';
4
- import inquirer from 'inquirer';
4
+ // `inquirer` NÃO é importado aqui: só o caminho interativo de `resolveTarget`
5
+ // pergunta, e ele o carrega na hora (P112, 2026-10-18). O import a frio do
6
+ // inquirer custava ~0,35 s sozinho e ~1,1 s com a suíte rodando — pago por
7
+ // todo `neetru init --yes` (CI e scripts) e pelo 1º teste do init.test.ts,
8
+ // que estourava o timeout de 10 s com a máquina carregada.
5
9
  import { log } from '../utils/logger.js';
6
10
  import { safeExit } from '../utils/safe-exit.js';
7
11
  /**
@@ -81,6 +85,8 @@ async function resolveTarget(name, cwd, opts) {
81
85
  return { dir: subDir, mode: 'new', force: false };
82
86
  }
83
87
  // ── Interactive ──────────────────────────────────────────────────────
88
+ // Só aqui há pergunta: o inquirer é carregado agora (ver o topo do arquivo).
89
+ const { default: inquirer } = await import('inquirer');
84
90
  console.log();
85
91
  log.info(`Onde você quer inicializar o projeto "${name}"?`);
86
92
  console.log();
@@ -21,6 +21,7 @@ import { spawn } from 'node:child_process';
21
21
  import { config } from '../lib/config.js';
22
22
  import { saveToken } from '../lib/auth.js';
23
23
  import { apiRequest, CliApiError, CliNetworkError } from '../lib/api-client.js';
24
+ import { discardMfaSessionOfOtherUid } from '../lib/cli-write.js';
24
25
  import { log } from '../utils/logger.js';
25
26
  import { safeExit } from '../utils/safe-exit.js';
26
27
  const TOKEN_PATTERN = /^nrt_[a-z0-9]{8,16}_[A-Za-z0-9_\-=]{20,64}$/;
@@ -51,28 +52,52 @@ function openBrowser(url) {
51
52
  catch {
52
53
  return; // URL inválida → não abre (caller já imprimiu pra copiar/colar)
53
54
  }
55
+ // bug_d414f21c (medium): sem binário de abrir browser (ex.: WSL sem
56
+ // `xdg-open`), `spawn(...)` emite 'error' (ENOENT) ASSINCRONAMENTE — sem
57
+ // listener, o Node trata como exceção não capturada e derruba o processo,
58
+ // abortando o polling do device-code antes de começar. Registra o handler
59
+ // e avisa o usuário (ele já tem a URL impressa pra copiar/colar); nunca
60
+ // deixa esse erro propagar.
61
+ const warnFallback = () => {
62
+ log.warn('Não consegui abrir o navegador; abra a URL acima manualmente.');
63
+ };
54
64
  try {
65
+ let child;
55
66
  if (process.platform === 'win32') {
56
- spawn('rundll32', ['url.dll,FileProtocolHandler', url], {
67
+ child = spawn('rundll32', ['url.dll,FileProtocolHandler', url], {
57
68
  detached: true,
58
69
  stdio: 'ignore',
59
70
  windowsHide: true,
60
- }).unref();
61
- return;
71
+ });
62
72
  }
63
- if (process.platform === 'darwin') {
64
- spawn('open', [url], { detached: true, stdio: 'ignore' }).unref();
65
- return;
73
+ else if (process.platform === 'darwin') {
74
+ child = spawn('open', [url], { detached: true, stdio: 'ignore' });
75
+ }
76
+ else {
77
+ child = spawn('xdg-open', [url], { detached: true, stdio: 'ignore' });
66
78
  }
67
- spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
79
+ child.on('error', warnFallback);
80
+ child.unref();
68
81
  }
69
82
  catch {
70
- // ignore — caller já printou a URL.
83
+ warnFallback();
71
84
  }
72
85
  }
73
86
  function sleep(ms) {
74
87
  return new Promise((res) => setTimeout(res, ms));
75
88
  }
89
+ /**
90
+ * A8 (2026-10-13): o login trocava o Bearer mas mantinha o `mfa-session.json`
91
+ * da conta anterior. O Core só aceita sessão MFA do MESMO uid do Bearer, então
92
+ * por até 1h todo comando destrutivo da conta nova tomava 401. Sessão de outro
93
+ * uid sai junto com a troca de conta; a da mesma conta fica.
94
+ */
95
+ async function discardForeignMfaSession(uid, json) {
96
+ if (await discardMfaSessionOfOtherUid(uid)) {
97
+ if (!json)
98
+ log.dim('Sessão MFA de 1h da conta anterior descartada (rode `neetru auth mfa-session` se precisar).');
99
+ }
100
+ }
76
101
  async function pollForToken(deviceCode, initialIntervalSec, expiresAtMs, json) {
77
102
  let intervalMs = initialIntervalSec * 1000;
78
103
  const spinner = json
@@ -143,6 +168,7 @@ export async function runLogin(opts = {}) {
143
168
  const path = await saveToken(token, me.email);
144
169
  // Compat: alguns comandos legados ainda leem do Conf.
145
170
  config.set('neetruApiKey', token);
171
+ await discardForeignMfaSession(me.uid, json);
146
172
  if (json) {
147
173
  emitJson({ ok: true, email: me.email, role: me.role, path });
148
174
  }
@@ -213,6 +239,7 @@ export async function runLogin(opts = {}) {
213
239
  const path = await saveToken(accessToken, me.email);
214
240
  // Compat: legacy commands podem usar Conf — mantemos o token lá também.
215
241
  config.set('neetruApiKey', accessToken);
242
+ await discardForeignMfaSession(me.uid, json);
216
243
  if (json) {
217
244
  emitJson({ ok: true, email: me.email, role: me.role, path });
218
245
  }
@@ -10,9 +10,8 @@
10
10
  import { existsSync, readFileSync, statSync, openSync, readSync, closeSync } from 'node:fs';
11
11
  import { join } from 'node:path';
12
12
  import chalk from 'chalk';
13
- import { apiRequest, CliNetworkError } from '../lib/api-client.js';
13
+ import { apiRequest, CliNetworkError, resolveTrustedApiUrl } from '../lib/api-client.js';
14
14
  import { handleApiError } from '../lib/api-error.js';
15
- import { config } from '../lib/config.js';
16
15
  import { getActiveTokenSync } from '../lib/token-resolver.js';
17
16
  import { log } from '../utils/logger.js';
18
17
  import { safeExit } from '../utils/safe-exit.js';
@@ -239,7 +238,6 @@ async function fetchLogs(clientId, params) {
239
238
  async function runSseFollow(clientId, opts, jsonMode) {
240
239
  // MASTER-HIGH-048 (2.11.0): SSE stream usa o mesmo resolver canonico.
241
240
  const apiKey = getActiveTokenSync() ?? undefined;
242
- const apiBase = config.get('apiBase') ?? 'https://api.neetru.com';
243
241
  const qs = new URLSearchParams({ clientId });
244
242
  if (opts.level)
245
243
  qs.set('level', opts.level);
@@ -249,7 +247,17 @@ async function runSseFollow(clientId, opts, jsonMode) {
249
247
  qs.set('channel', opts.channel);
250
248
  if (opts.correlationId)
251
249
  qs.set('correlationId', opts.correlationId);
252
- const streamUrl = `${apiBase}/api/cli/v1/logs/stream?${qs.toString()}`;
250
+ // C7 (2026-10-13): a base e a guarda são as MESMAS do `apiRequest` (que fez o
251
+ // primeiro request deste comando). Antes o stream lia `config.get('apiBase')`
252
+ // — chave fora do schema — e mandava o Bearer pra qualquer host gravado ali,
253
+ // além de ignorar o `neetruApiUrl` de staging/local.
254
+ let streamUrl;
255
+ try {
256
+ streamUrl = resolveTrustedApiUrl(`/api/cli/v1/logs/stream?${qs.toString()}`);
257
+ }
258
+ catch (error) {
259
+ handleApiError(error, jsonMode);
260
+ }
253
261
  let attempts = 0;
254
262
  const MAX_RECONNECTS = 5;
255
263
  let lastEventId;