@neetru/cli 2.29.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 (259) hide show
  1. package/CHANGELOG.md +141 -0
  2. package/README.md +63 -17
  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 +136 -2
  27. package/dist/commands/deploy.js +547 -69
  28. package/dist/commands/deployments.js +11 -6
  29. package/dist/commands/dev-context.js +15 -5
  30. package/dist/commands/dev-up.d.ts +7 -5
  31. package/dist/commands/dev-up.js +24 -23
  32. package/dist/commands/docs-produto.d.ts +90 -0
  33. package/dist/commands/docs-produto.js +384 -0
  34. package/dist/commands/doctor.js +36 -1
  35. package/dist/commands/domains.js +85 -4
  36. package/dist/commands/env.d.ts +9 -3
  37. package/dist/commands/env.js +23 -3
  38. package/dist/commands/infra-read.js +2 -2
  39. package/dist/commands/init.d.ts +2 -0
  40. package/dist/commands/init.js +14 -1
  41. package/dist/commands/login.js +35 -8
  42. package/dist/commands/logs.js +12 -4
  43. package/dist/commands/mobile.js +33 -9
  44. package/dist/commands/new.d.ts +31 -1
  45. package/dist/commands/new.js +94 -4
  46. package/dist/commands/open.js +16 -6
  47. package/dist/commands/ops.js +1 -1
  48. package/dist/commands/products-db.d.ts +1 -0
  49. package/dist/commands/products-db.js +41 -8
  50. package/dist/commands/products.d.ts +17 -3
  51. package/dist/commands/products.js +84 -16
  52. package/dist/commands/promote.js +30 -5
  53. package/dist/commands/secrets.d.ts +91 -0
  54. package/dist/commands/secrets.js +471 -0
  55. package/dist/commands/servers.js +6 -4
  56. package/dist/commands/status.js +2 -2
  57. package/dist/commands/support.js +5 -3
  58. package/dist/commands/tenants.js +28 -16
  59. package/dist/commands/upgrade.d.ts +9 -0
  60. package/dist/commands/upgrade.js +50 -1
  61. package/dist/commands/vm-memory.js +1 -0
  62. package/dist/commands/webhooks.d.ts +2 -0
  63. package/dist/commands/webhooks.js +9 -3
  64. package/dist/commands/workspaces.js +42 -11
  65. package/dist/index.js +142 -20
  66. package/dist/lib/api-client.d.ts +22 -1
  67. package/dist/lib/api-client.js +79 -8
  68. package/dist/lib/api-error.d.ts +14 -0
  69. package/dist/lib/api-error.js +107 -46
  70. package/dist/lib/build-targets.d.ts +127 -0
  71. package/dist/lib/build-targets.js +348 -0
  72. package/dist/lib/cli-error.d.ts +13 -0
  73. package/dist/lib/cli-error.js +24 -0
  74. package/dist/lib/cli-write.d.ts +43 -9
  75. package/dist/lib/cli-write.js +154 -45
  76. package/dist/lib/compose-generator.d.ts +13 -9
  77. package/dist/lib/compose-generator.js +28 -60
  78. package/dist/lib/config-schema.d.ts +36 -2
  79. package/dist/lib/config-schema.js +11 -0
  80. package/dist/lib/db-local/db-json.d.ts +1 -1
  81. package/dist/lib/db-local/db-json.js +13 -5
  82. package/dist/lib/db-local/env.d.ts +15 -3
  83. package/dist/lib/db-local/env.js +37 -10
  84. package/dist/lib/db-pipeline/build-deps.d.ts +5 -1
  85. package/dist/lib/db-pipeline/build-deps.js +23 -3
  86. package/dist/lib/db-pipeline/types.d.ts +6 -2
  87. package/dist/lib/db-pipeline/types.js +0 -18
  88. package/dist/lib/deploy-target-contract.d.ts +152 -0
  89. package/dist/lib/deploy-target-contract.js +411 -0
  90. package/dist/lib/env-file-do-build.d.ts +19 -0
  91. package/dist/lib/env-file-do-build.js +44 -0
  92. package/dist/lib/envs.d.ts +108 -0
  93. package/dist/lib/envs.js +127 -0
  94. package/dist/lib/fs-seguro.d.ts +63 -0
  95. package/dist/lib/fs-seguro.js +108 -0
  96. package/dist/lib/mobile-local/mobile-json.js +10 -4
  97. package/dist/lib/pickers.d.ts +44 -7
  98. package/dist/lib/pickers.js +60 -4
  99. package/dist/lib/product-docs/check.d.ts +62 -0
  100. package/dist/lib/product-docs/check.js +489 -0
  101. package/dist/lib/product-docs/config.d.ts +66 -0
  102. package/dist/lib/product-docs/config.js +234 -0
  103. package/dist/lib/product-docs/dates.d.ts +17 -0
  104. package/dist/lib/product-docs/dates.js +41 -0
  105. package/dist/lib/product-docs/frontmatter.d.ts +43 -0
  106. package/dist/lib/product-docs/frontmatter.js +302 -0
  107. package/dist/lib/product-docs/git.d.ts +16 -0
  108. package/dist/lib/product-docs/git.js +92 -0
  109. package/dist/lib/product-docs/markdown.d.ts +27 -0
  110. package/dist/lib/product-docs/markdown.js +98 -0
  111. package/dist/lib/product-docs/ops.d.ts +133 -0
  112. package/dist/lib/product-docs/ops.js +975 -0
  113. package/dist/lib/product-docs/structure.d.ts +61 -0
  114. package/dist/lib/product-docs/structure.js +239 -0
  115. package/dist/lib/product-docs/sync.d.ts +107 -0
  116. package/dist/lib/product-docs/sync.js +322 -0
  117. package/dist/lib/product-docs/taxonomy.d.ts +46 -0
  118. package/dist/lib/product-docs/taxonomy.js +163 -0
  119. package/dist/lib/product-docs/templates.d.ts +53 -0
  120. package/dist/lib/product-docs/templates.js +245 -0
  121. package/dist/lib/public-secret-scan.d.ts +117 -0
  122. package/dist/lib/public-secret-scan.js +771 -0
  123. package/dist/lib/render.d.ts +16 -0
  124. package/dist/lib/render.js +34 -4
  125. package/dist/lib/segredo-produto.d.ts +124 -0
  126. package/dist/lib/segredo-produto.js +336 -0
  127. package/dist/lib/texto-utf8.d.ts +96 -0
  128. package/dist/lib/texto-utf8.js +196 -0
  129. package/package.json +2 -1
  130. package/dist/cli-kit/format.js.map +0 -1
  131. package/dist/cli-kit/glyphs.js.map +0 -1
  132. package/dist/cli-kit/index.js.map +0 -1
  133. package/dist/cli-kit/palette.js.map +0 -1
  134. package/dist/commands/add.js.map +0 -1
  135. package/dist/commands/admin/iam-check.js.map +0 -1
  136. package/dist/commands/admin/oidc-login.js.map +0 -1
  137. package/dist/commands/admin/sdk-credential.js.map +0 -1
  138. package/dist/commands/agent-release.js.map +0 -1
  139. package/dist/commands/agent-write.js.map +0 -1
  140. package/dist/commands/ai-models.js.map +0 -1
  141. package/dist/commands/ai.js.map +0 -1
  142. package/dist/commands/api-catalog.js.map +0 -1
  143. package/dist/commands/archive.js.map +0 -1
  144. package/dist/commands/artifact-registry.js.map +0 -1
  145. package/dist/commands/audit.js.map +0 -1
  146. package/dist/commands/auth.js.map +0 -1
  147. package/dist/commands/autocomplete.js.map +0 -1
  148. package/dist/commands/billing.js.map +0 -1
  149. package/dist/commands/bootstrap.js.map +0 -1
  150. package/dist/commands/bug.js.map +0 -1
  151. package/dist/commands/build.js.map +0 -1
  152. package/dist/commands/chat-menu.js.map +0 -1
  153. package/dist/commands/chat.js.map +0 -1
  154. package/dist/commands/cloud-run.js.map +0 -1
  155. package/dist/commands/code.js.map +0 -1
  156. package/dist/commands/config.js.map +0 -1
  157. package/dist/commands/customers.js.map +0 -1
  158. package/dist/commands/db-move.js.map +0 -1
  159. package/dist/commands/db.js.map +0 -1
  160. package/dist/commands/deploy.js.map +0 -1
  161. package/dist/commands/deployments.js.map +0 -1
  162. package/dist/commands/dev-context.js.map +0 -1
  163. package/dist/commands/dev-up.js.map +0 -1
  164. package/dist/commands/dev.js.map +0 -1
  165. package/dist/commands/docs.js.map +0 -1
  166. package/dist/commands/doctor.js.map +0 -1
  167. package/dist/commands/domains.js.map +0 -1
  168. package/dist/commands/dr.js.map +0 -1
  169. package/dist/commands/env.js.map +0 -1
  170. package/dist/commands/hosting-write.js.map +0 -1
  171. package/dist/commands/infra-read.js.map +0 -1
  172. package/dist/commands/init.js.map +0 -1
  173. package/dist/commands/login.js.map +0 -1
  174. package/dist/commands/logout.js.map +0 -1
  175. package/dist/commands/logs.js.map +0 -1
  176. package/dist/commands/mobile.js.map +0 -1
  177. package/dist/commands/mocks.js.map +0 -1
  178. package/dist/commands/new.js.map +0 -1
  179. package/dist/commands/open.js.map +0 -1
  180. package/dist/commands/ops.js.map +0 -1
  181. package/dist/commands/products-db.js.map +0 -1
  182. package/dist/commands/products.js.map +0 -1
  183. package/dist/commands/promote.js.map +0 -1
  184. package/dist/commands/publish.js.map +0 -1
  185. package/dist/commands/schema.js.map +0 -1
  186. package/dist/commands/servers.js.map +0 -1
  187. package/dist/commands/smoke.js.map +0 -1
  188. package/dist/commands/status.js.map +0 -1
  189. package/dist/commands/support.js.map +0 -1
  190. package/dist/commands/surface-status.js.map +0 -1
  191. package/dist/commands/tenants.js.map +0 -1
  192. package/dist/commands/ui.js.map +0 -1
  193. package/dist/commands/upgrade.js.map +0 -1
  194. package/dist/commands/validate.js.map +0 -1
  195. package/dist/commands/vm-memory.js.map +0 -1
  196. package/dist/commands/vm.js.map +0 -1
  197. package/dist/commands/webhooks.js.map +0 -1
  198. package/dist/commands/whoami.js.map +0 -1
  199. package/dist/commands/workspaces.js.map +0 -1
  200. package/dist/index.js.map +0 -1
  201. package/dist/lib/agent-artifact-url.js.map +0 -1
  202. package/dist/lib/agent-commands.js.map +0 -1
  203. package/dist/lib/ai/auth-discovery.js.map +0 -1
  204. package/dist/lib/ai/context.js.map +0 -1
  205. package/dist/lib/ai/orchestrator.js.map +0 -1
  206. package/dist/lib/api-client.js.map +0 -1
  207. package/dist/lib/api-error.js.map +0 -1
  208. package/dist/lib/auth.js.map +0 -1
  209. package/dist/lib/cli-read.js.map +0 -1
  210. package/dist/lib/cli-write.js.map +0 -1
  211. package/dist/lib/code-agent-budget.js.map +0 -1
  212. package/dist/lib/code-agent-files.js.map +0 -1
  213. package/dist/lib/code-agent-gateway.js.map +0 -1
  214. package/dist/lib/code-agent-git-tools.js.map +0 -1
  215. package/dist/lib/code-agent-protocol.js.map +0 -1
  216. package/dist/lib/code-agent-session.js.map +0 -1
  217. package/dist/lib/code-agent-settings.js.map +0 -1
  218. package/dist/lib/code-agent-skills.js.map +0 -1
  219. package/dist/lib/code-agent-subagents.js.map +0 -1
  220. package/dist/lib/code-agent-tools.js.map +0 -1
  221. package/dist/lib/code-agent-tui.js.map +0 -1
  222. package/dist/lib/code-agent-types.js.map +0 -1
  223. package/dist/lib/code-agent-ui.js.map +0 -1
  224. package/dist/lib/code-agent-web-tools.js.map +0 -1
  225. package/dist/lib/code-agent-write-tools.js.map +0 -1
  226. package/dist/lib/compose-generator.js.map +0 -1
  227. package/dist/lib/config-schema.js.map +0 -1
  228. package/dist/lib/config.js.map +0 -1
  229. package/dist/lib/db-local/db-json.js.map +0 -1
  230. package/dist/lib/db-local/env.js.map +0 -1
  231. package/dist/lib/db-local/fingerprint.js.map +0 -1
  232. package/dist/lib/db-local/index.js.map +0 -1
  233. package/dist/lib/db-pipeline/build-deps.js.map +0 -1
  234. package/dist/lib/db-pipeline/errors.js.map +0 -1
  235. package/dist/lib/db-pipeline/index.js.map +0 -1
  236. package/dist/lib/db-pipeline/pipeline.js.map +0 -1
  237. package/dist/lib/db-pipeline/rehearse.js.map +0 -1
  238. package/dist/lib/db-pipeline/types.js.map +0 -1
  239. package/dist/lib/dev-local-context.js.map +0 -1
  240. package/dist/lib/iam-preflight.js.map +0 -1
  241. package/dist/lib/mobile-local/index.js.map +0 -1
  242. package/dist/lib/mobile-local/mobile-json.js.map +0 -1
  243. package/dist/lib/pickers.js.map +0 -1
  244. package/dist/lib/product-config.js.map +0 -1
  245. package/dist/lib/render.js.map +0 -1
  246. package/dist/lib/schema-cache.js.map +0 -1
  247. package/dist/lib/sse.js.map +0 -1
  248. package/dist/lib/telemetry.js.map +0 -1
  249. package/dist/lib/token-resolver.js.map +0 -1
  250. package/dist/lib/url-allowlist.js.map +0 -1
  251. package/dist/lib/version-check.js.map +0 -1
  252. package/dist/utils/logger.js.map +0 -1
  253. package/dist/utils/logo.js.map +0 -1
  254. package/dist/utils/safe-exit.js.map +0 -1
  255. package/dist/utils/spinner-registry.js.map +0 -1
  256. package/dist/version.js.map +0 -1
  257. package/templates/dev/Dockerfile.agent +0 -39
  258. package/templates/dev/agent-entrypoint.sh +0 -41
  259. package/templates/dev/docker-compose.dev.yml +0 -63
@@ -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 },
@@ -9,6 +9,8 @@ interface InitOptions {
9
9
  path?: string;
10
10
  /** Modo non-interactive — sai com erro se prompt for necessário. */
11
11
  yes?: boolean;
12
+ /** Não roda `neetru docs init` depois do scaffold (protocolo de docs v1). */
13
+ semDocs?: boolean;
12
14
  }
13
15
  export declare function runInit(opts: InitOptions): Promise<void>;
14
16
  export {};
@@ -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();
@@ -271,6 +277,13 @@ export async function runInit(opts) {
271
277
  },
272
278
  }, null, 2));
273
279
  spinner.succeed(mode === 'merge' ? 'Scaffold mesclado!' : 'Projeto criado!');
280
+ // Protocolo de docs de produto v1 (2026-10-03): o produto nasce com o
281
+ // mínimo do primeiro dia (README, CHANGELOG, docs/…). Idempotente — em modo
282
+ // merge nunca sobrescreve doc existente. Falha aqui não derruba o init.
283
+ if (!opts.semDocs) {
284
+ const { initDocsAfterScaffold } = await import('./docs-produto.js');
285
+ await initDocsAfterScaffold(dir);
286
+ }
274
287
  console.log();
275
288
  log.info(`Diretório: ${dir}`);
276
289
  log.info(`Modo: ${mode === 'merge' ? 'MESCLAR (preserva arquivos existentes)' : 'NOVO (scaffold completo)'}`);
@@ -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;
@@ -11,11 +11,12 @@
11
11
  * GET /api/cli/v1/products/{slug}/mobile-apps — role admin|operator
12
12
  * DELETE /api/cli/v1/products/{slug}/mobile-apps/{appId} — role admin ONLY + step-up MFA
13
13
  *
14
- * `redirectUri` PRECISA ser `https://` (Universal Link/App Link, RFC 8252) —
15
- * o Core rejeita custom scheme (`meuapp://`) com 400. Isso é decisão de
16
- * segurança do owner, não limitação temporária — a CLI valida o mesmo
17
- * localmente pra falhar rápido com mensagem clara, mas NUNCA oferece
18
- * custom scheme como alternativa.
14
+ * `redirectUri` PRECISA ser `https://` (Universal Link/App Link, RFC 8252)
15
+ * ou `http://localhost`/`http://127.0.0.1` (loopback de dev) — mesma regra
16
+ * do Core (`validateRedirectUris`). O Core rejeita custom scheme
17
+ * (`meuapp://`) com 400. Isso é decisão de segurança do owner, não
18
+ * limitação temporária — a CLI valida o mesmo localmente pra falhar rápido
19
+ * com mensagem clara, mas NUNCA oferece custom scheme como alternativa.
19
20
  *
20
21
  * O `id` retornado pelo Core é `<slug>__<platform>` — o app é PKCE público
21
22
  * (Authorization Code + PKCE, sem client secret).
@@ -42,6 +43,28 @@ const MOBILE_JSON_REL = '.neetru/mobile.json';
42
43
  function jsonError(message, error = 'error') {
43
44
  console.log(JSON.stringify({ ok: false, error, message }));
44
45
  }
46
+ /**
47
+ * bug_afcf958a (low): espelha EXATAMENTE a regra de `redirectUri` do Core
48
+ * (`_oidc-clients-core.ts` → `validateRedirectUris`, usada também pelo
49
+ * registro de apps mobile em `_product-mobile-apps-core.ts`): aceita
50
+ * `https://` ou `http://` com host `localhost`/`127.0.0.1` (loopback de
51
+ * dev, RFC 8252). NUNCA custom scheme (`meuapp://`) — isso o Core rejeita
52
+ * com 400, decisão de segurança do owner, e a CLI não pode ser MAIS
53
+ * permissiva que o servidor que vai validar de novo.
54
+ */
55
+ function isAcceptableMobileRedirectUri(uri) {
56
+ let parsed;
57
+ try {
58
+ parsed = new URL(uri);
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ if (parsed.protocol === 'https:')
64
+ return true;
65
+ return (parsed.protocol === 'http:' &&
66
+ (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1'));
67
+ }
45
68
  function mobileStatusColor(status) {
46
69
  switch (status) {
47
70
  case 'active': return chalk.green(status);
@@ -82,8 +105,9 @@ export async function runMobileInit(opts = {}) {
82
105
  safeExit(1);
83
106
  return;
84
107
  }
85
- if (!redirectUri.startsWith('https://')) {
86
- const msg = `--redirect-uri precisa ser https:// (Universal Link/App Link). ` +
108
+ if (!isAcceptableMobileRedirectUri(redirectUri)) {
109
+ const msg = `--redirect-uri precisa ser https:// (Universal Link/App Link) ou http://localhost ` +
110
+ `(ou http://127.0.0.1) para dev — mesma regra do Core. ` +
87
111
  `Custom scheme (ex: meuapp://) NÃO é aceito — decisão de segurança do owner (RFC 8252), ` +
88
112
  `não é limitação temporária. Recebido: "${redirectUri}"`;
89
113
  if (opts.json) {
@@ -269,7 +293,7 @@ export async function runMobileList(opts = {}) {
269
293
  return;
270
294
  }
271
295
  renderTable([
272
- { header: 'ID', value: (r) => r.id, maxWidth: 30 },
296
+ { header: 'ID', value: (r) => r.id, identificador: true },
273
297
  { header: 'Platform', value: (r) => r.platform, maxWidth: 10 },
274
298
  {
275
299
  header: 'Bundle/Package',
@@ -277,7 +301,7 @@ export async function runMobileList(opts = {}) {
277
301
  maxWidth: 30,
278
302
  },
279
303
  { header: 'Status', value: (r) => mobileStatusColor(r.status), maxWidth: 14 },
280
- { header: 'OIDC Client ID', value: (r) => r.oidcClientId, maxWidth: 34 },
304
+ { header: 'OIDC Client ID', value: (r) => r.oidcClientId, identificador: true },
281
305
  ], resp.apps);
282
306
  }
283
307
  /**
@@ -1,6 +1,10 @@
1
1
  export interface NewMacroOptions {
2
2
  productSlug?: string;
3
- env?: 'dev' | 'staging' | 'prod';
3
+ /**
4
+ * bug_7dcb68e2666a43f69ef362d2cbff94ed: era `'dev' | 'staging' | 'prod'`.
5
+ * `'dev'` não existe no enum do Core — ver `../lib/envs.ts`.
6
+ */
7
+ env?: string;
4
8
  tier?: 'dev' | 'standard' | 'enterprise';
5
9
  customer?: string;
6
10
  /** Pula scaffold local (útil quando o repo já existe). */
@@ -24,6 +28,32 @@ export interface NewMacroOptions {
24
28
  * rodar `neetru db init` manualmente depois.
25
29
  */
26
30
  skipDbInit?: boolean;
31
+ /** Não cria o `docs/` do produto no scaffold (repassado ao `runInit`). */
32
+ semDocs?: boolean;
27
33
  json?: boolean;
28
34
  }
35
+ /**
36
+ * Decisão PURA de qual `customerId` a macro deve usar.
37
+ *
38
+ * bug_7dcb68e2666a43f69ef362d2cbff94ed — antes a macro assumia `'self'` em
39
+ * silêncio. `'self'` é um sentinel que o Core só aceita para produto
40
+ * **Neetru-owned** (`resolveDefaultCustomer`); para qualquer outro produto ele
41
+ * recusa a criação do workspace. Como a macro cria o produto no passo [1/5] e
42
+ * o workspace no [2/5], o resultado era um produto órfão no Core e um erro
43
+ * incompreensível no meio da macro.
44
+ *
45
+ * Agora a macro exige `--customer` explícito quando o produto não é
46
+ * Neetru-owned — e diz isso, em vez de deixar o Core recusar depois.
47
+ */
48
+ export declare function decidirCustomerDaMacro(entrada: {
49
+ fromFlag?: string;
50
+ produtoEhNeetru: boolean;
51
+ slug: string;
52
+ }): {
53
+ ok: true;
54
+ customerId: string;
55
+ } | {
56
+ ok: false;
57
+ mensagem: string;
58
+ };
29
59
  export declare function runNewMacro(name: string, opts?: NewMacroOptions): Promise<void>;