@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
@@ -27,6 +27,9 @@ import ora from 'ora';
27
27
  import { log } from '../utils/logger.js';
28
28
  import { apiRequest, CliNetworkError } from '../lib/api-client.js';
29
29
  import { handleApiError } from '../lib/api-error.js';
30
+ import { exitWithCliError } from '../lib/cli-error.js';
31
+ import { invalidChoiceError } from '../cli-kit/choices.js';
32
+ import { DEPLOY_ENVIRONMENT_IDS, deployEnvParaRuntime, } from '../lib/envs.js';
30
33
  import { resolveStepUp } from '../lib/cli-write.js';
31
34
  import { runBuild } from './build.js';
32
35
  import { formatDeployConsole, isDeployUnverified, deployStatusLabel } from './deployments.js';
@@ -35,7 +38,10 @@ import { readDbJson, schemaFingerprint } from '../lib/db-local/index.js';
35
38
  import { assertIamReady } from '../lib/iam-preflight.js';
36
39
  import { ALLOW_PUBLIC_SECRET_FLAG, MIN_OVERRIDE_REASON_LENGTH, buildOverrideRecord, collectPublicEnvSecrets, formatFindingsMessage, scanDirectoryForPublicSecrets, scanTarballForPublicSecrets, } from '../lib/public-secret-scan.js';
37
40
  import { getActiveTokenSync } from '../lib/token-resolver.js';
41
+ import { BYTES_FAREJADOS, checkDeployTargetContract, farejarConteudoDePacote, mensagemSourceAcimaDoTeto, mensagemSourceNaoEhHandler, pareceArquivoDePacote, WORKSPACE_SOURCE_MAX_BYTES, } from '../lib/deploy-target-contract.js';
38
42
  import { safeExit } from '../utils/safe-exit.js';
43
+ import { AVISO_AGENTE_PENDENTE, CHAVES_DO_CARREGADOR, CHAVE_SEGREDO_RE, chaveInjetadaPeloCore, comandoParaRegistrarSegredo, problemaNaChaveReservada, } from '../lib/segredo-produto.js';
44
+ import { avisoEnvDoBundlerForaDeUtf8, CodificacaoInvalida, decodificarUtf8, semBomInicial, } from '../lib/texto-utf8.js';
39
45
  /**
40
46
  * bug_a5786e03 (2026-05-31): o Core/agente só conhecem os stacks canônicos
41
47
  * `node|docker|php-apache|static` (PRODUCT_STACKS). `nextjs` é um alias de
@@ -109,7 +115,16 @@ export function validateAllowPublicSecretReason(raw, json) {
109
115
  */
110
116
  export async function runPublicSecretGate(source, ctx) {
111
117
  const lg = scopedLog(ctx.json);
112
- const envSecrets = await collectPublicEnvSecrets(ctx.cwd, ctx.envFile);
118
+ // Rodada final 2 do cli#81: `.env*` fora de UTF-8 limpo não para a varredura
119
+ // (ela confere o valor como o bundler o lê), mas não passa calado. Em --json
120
+ // o aviso vai ao stderr: o stdout segue com a linha JSON única.
121
+ const envSecrets = await collectPublicEnvSecrets(ctx.cwd, ctx.envFile, process.env, (arquivo, problema) => {
122
+ const aviso = avisoEnvDoBundlerForaDeUtf8(arquivo, problema);
123
+ if (ctx.json)
124
+ console.error(aviso);
125
+ else
126
+ lg.warn(aviso);
127
+ });
113
128
  let result;
114
129
  try {
115
130
  result =
@@ -138,6 +153,53 @@ export async function runPublicSecretGate(source, ctx) {
138
153
  'Trate a chave exposta como VAZADA: rotacione-a assim que possível.');
139
154
  return { scanned: true, scannedFiles: result.scannedFiles, override };
140
155
  }
156
+ /**
157
+ * bug_678ddc98 (elo que faltava): decide se este deploy precisa DIZER ao Core
158
+ * qual é o entrypoint, em vez de deixar o agente adivinhar.
159
+ *
160
+ * O #68 ensinou o `neetru build` a empacotar monorepo e a calcular o entrypoint
161
+ * aninhado (`apps/web/server.js`), mas a cadeia parava no `manifest.json`:
162
+ * `manifest.entrypoint` nunca foi lido por ninguém — era campo write-only.
163
+ * Num monorepo o tarball fica assim:
164
+ *
165
+ * <versionDir>/node_modules/… ← tracejado, na RAIZ
166
+ * <versionDir>/packages/ui/… ← pacotes internos do workspace
167
+ * <versionDir>/apps/web/server.js ← entrypoint REAL
168
+ * <versionDir>/apps/web/package.json ← o package.json DO APP mora aqui
169
+ *
170
+ * O agente (`neetru-agent-service-vm/src/lib/deploy-plan.ts:117-124`) infere o
171
+ * comando de start olhando SÓ a raiz do versionDir: `server.js` na raiz,
172
+ * `.next/` na raiz, `.next/standalone/server.js`, e `scripts.start` do
173
+ * `package.json` da raiz. Em monorepo os três primeiros não existem, e o
174
+ * quarto não ajuda: o Next.js escreve o package.json do app em
175
+ * `<appPrefix>/package.json` (`next/dist/build/utils.js:1197`), então o que
176
+ * houver na raiz é o package.json do WORKSPACE — que ou não tem `start`, ou
177
+ * tem um `start` de ferramenta de monorepo (`turbo start`), nunca
178
+ * `node apps/web/server.js`. Nos dois casos o agente erra: cai no fallback
179
+ * `'npm start'` (deploy-plan.ts:124) e o systemd entra em crash-loop.
180
+ *
181
+ * REGRA: só mandamos o entrypoint quando o agente NÃO consegue acertar sozinho,
182
+ * isto é quando o standalone está aninhado (`appPrefix` preenchido). Para app
183
+ * único o agente já resolve certo — e mandar `pkg.main`/`index.js` ali
184
+ * ATROPELARIA o `npm start` de produtos como o gestovendas, que dependem de
185
+ * `scripts.start`. Conserto que quebra o caso que funcionava não é conserto.
186
+ *
187
+ * Também não mandamos para stack não-node: ali `entrypoint` é `index.html` /
188
+ * `index.php` / referência de imagem, que não é "arquivo para o `node` rodar".
189
+ *
190
+ * ATENÇÃO — a cadeia ainda NÃO fecha só com isto: o Core descarta o campo hoje
191
+ * (`deployRequestSchema` não o declara, e objeto zod sem `.passthrough()` remove
192
+ * chave desconhecida em silêncio). Ver "o que falta no Core" na descrição do PR.
193
+ */
194
+ export function entrypointParaDeploy(manifest) {
195
+ if (!manifest)
196
+ return undefined;
197
+ if (manifest.stack !== 'node')
198
+ return undefined;
199
+ if (!manifest.appPrefix)
200
+ return undefined;
201
+ return manifest.entrypoint ?? undefined;
202
+ }
141
203
  /** `true` pra `prod`/`production` (o Core normaliza `production`→`prod`). */
142
204
  export function isProdEnv(envName) {
143
205
  const e = (envName ?? '').trim().toLowerCase();
@@ -146,7 +208,7 @@ export function isProdEnv(envName) {
146
208
  /**
147
209
  * M7 (auditoria cli/v1 2026-09-29): `POST /api/cli/v1/deploy` com
148
210
  * `environmentId` prod exige step-up MFA no Core (mesmo gate de `promote`).
149
- * Resolve os headers (sessão 1h → --mfa-token → prompt TOTP; fail-closed em
211
+ * Resolve os headers (--mfa-token → sessão 1h → prompt TOTP; fail-closed em
150
212
  * --json/--non-interactive/sem TTY) só quando o alvo é prod. Chamado logo
151
213
  * antes do POST — depois do build/upload — pra o TOTP não vencer no meio.
152
214
  */
@@ -160,26 +222,74 @@ export async function resolveDeployStepUpHeaders(opts, envName, nonInteractive)
160
222
  });
161
223
  return headers;
162
224
  }
225
+ /**
226
+ * Mostra os `warnings[]` que o Core devolve no deploy.
227
+ *
228
+ * Medido em 2026-10-12: o Core devolve `warnings` nos TRES alvos e o comentario
229
+ * dele diz, literalmente, "`warnings[]` na resposta JSON (o CLI exibe pro
230
+ * operador)" (`cli/v1/deploy/route.ts:309`). `grep warnings` em `deploy.ts`
231
+ * devolvia ZERO — a promessa nunca foi cumprida.
232
+ *
233
+ * O caso que mais doia: a CLI manda o ambiente default e o Core normaliza, e o
234
+ * aviso de que `environmentId` foi ASSUMIDO disparava em todo deploy sem
235
+ * `--env` — e sumia 100% das vezes. O operador achava que tinha escolhido.
236
+ *
237
+ * Vai pro STDERR de proposito: em `--json` o stdout e contrato de maquina (uma
238
+ * linha JSON) e nao pode ser poluido. Quem le JSON ja recebe `warnings` no
239
+ * proprio corpo.
240
+ */
241
+ export function mostrarAvisosDoCore(warnings, json) {
242
+ if (!Array.isArray(warnings))
243
+ return;
244
+ const linhas = warnings.filter((w) => typeof w === 'string' && w.trim() !== '');
245
+ if (linhas.length === 0)
246
+ return;
247
+ if (json) {
248
+ for (const w of linhas)
249
+ process.stderr.write(`[aviso do Core] ${w}\n`);
250
+ return;
251
+ }
252
+ for (const w of linhas)
253
+ log.warn(` aviso do Core: ${w}`);
254
+ }
163
255
  const TERMINAL_STATUSES = new Set(['success', 'failed', 'cancelled']);
164
- /** Estágios de ambiente válidos para o deploy workspace. */
165
- const WORKSPACE_ENVS = new Set(['dev', 'staging', 'prod']);
256
+ /**
257
+ * Estágios de ambiente válidos para o deploy workspace/cloud-run. P105: era
258
+ * `new Set(['dev','staging','prod'])` escrito à mão; agora sai da tabela
259
+ * `VOCABULARIOS_DE_AMBIENTE` (`lib/envs.ts`), derivada de
260
+ * `TENANT_ENVIRONMENT_IDS`.
261
+ */
262
+ const WORKSPACE_ENVS = new Set(DEPLOY_ENVIRONMENT_IDS);
263
+ /**
264
+ * `--env` do deploy workspace/cloud-run: fora do vocabulário, encerra com exit
265
+ * 1 e os valores válidos da MESMA lista que validou (`invalid_env`, com `flag`,
266
+ * `value` e `valid` no JSON). Exportada para teste.
267
+ */
268
+ export function validarAmbienteDoDeploy(envName, json) {
269
+ if (WORKSPACE_ENVS.has(envName))
270
+ return;
271
+ exitWithCliError(invalidChoiceError({
272
+ flag: '--env',
273
+ value: envName,
274
+ valid: DEPLOY_ENVIRONMENT_IDS,
275
+ subject: 'Ambiente',
276
+ code: 'invalid_env',
277
+ }), json);
278
+ }
166
279
  /**
167
280
  * Mapeia o `envName` do deploy ('dev' | 'staging' | 'prod' — vocabulário do
168
281
  * `--env` deste comando) pro `targetEnv` que `neetru build` entende
169
282
  * ('dev' | 'workspace' | 'prod' — vocabulário de `BuildOptions`/SDK).
170
283
  * 'staging' vira 'workspace' (mesmo conceito de ambiente não-dev/não-prod).
171
284
  * Valor desconhecido cai em 'prod' (mesmo default conservador de `runBuild`).
285
+ * P105: a correspondência sai de `VOCABULARIOS_DE_AMBIENTE`, não de `if`s.
172
286
  *
173
287
  * Fix (2026-07-22, achado da revisão adversarial): sem isso, todo build
174
288
  * implícito de `neetru deploy --env dev`/`--env staging` (quando não há
175
289
  * artifact em cache) forçava NEXT_PUBLIC_NEETRU_ENV=prod incondicionalmente.
176
290
  */
177
291
  export function deployEnvToBuildTargetEnv(envName) {
178
- if (envName === 'dev')
179
- return 'dev';
180
- if (envName === 'staging')
181
- return 'workspace';
182
- return 'prod';
292
+ return deployEnvParaRuntime(envName) ?? 'prod';
183
293
  }
184
294
  /** Candidatos default de arquivo de source para um deploy workspace. */
185
295
  const WORKSPACE_SOURCE_CANDIDATES = [
@@ -220,6 +330,16 @@ async function loadLocalConfig(cwd) {
220
330
  }
221
331
  return null;
222
332
  }
333
+ /**
334
+ * `runtime` do neetru.config.json do diretório atual, só quando o config é
335
+ * DESTE produto. `--product outro` com o config do diretório atual: o `runtime`
336
+ * lido é de OUTRO produto e não diz nada sobre este deploy (revisão do PR #74).
337
+ * Sem `--product`, ou config sem `slug`, conta como deste produto.
338
+ */
339
+ function runtimeDoConfigDesteProduto(cfg, product) {
340
+ const configEhDesteProduto = !product || !cfg?.slug || cfg.slug === product;
341
+ return configEhDesteProduto ? cfg?.runtime : undefined;
342
+ }
223
343
  /**
224
344
  * Nomes de variáveis que NUNCA devem ser injetados no deploy —
225
345
  * podem alterar o comportamento do runtime de formas perigosas.
@@ -293,10 +413,82 @@ const SDK_CRED_ENV_KEYS = new Set([
293
413
  'NEETRU_API_KEY',
294
414
  'NEETRU_CLIENT_ID',
295
415
  ]);
296
- async function loadEnvFile(envFile, target = 'cloud-run', json) {
416
+ /**
417
+ * Aviso do segredo descartado no `--env-file` de um deploy VM.
418
+ *
419
+ * bug_8b85353b (Parte 3): até aqui a mensagem mandava "usar o Secret Manager e
420
+ * referenciar por nome na VM" — mecanismo que NÃO existia para segredo do
421
+ * próprio produto (era a 4ª cópia do padrão que o core#632 corrigiu nas outras
422
+ * três). O caminho real é `neetru secrets set`, que grava no registro
423
+ * `product_secrets` do Core com o valor direto no Secret Manager. `env` é o
424
+ * `--env` do deploy, para o comando sugerido já sair com o ambiente certo.
425
+ *
426
+ * Revisão do #81 (M1): o comando só é sugerido quando o Core o aceitaria.
427
+ * Chave reservada (o Core já a entrega ao app: credencial SDK/OIDC/CLI,
428
+ * módulo, unit) é recusada em `assertValidProductSecretKey`, e nome fora de
429
+ * CAIXA_ALTA é recusado pela própria CLI (`problemaNaChave`) — sugerir
430
+ * `neetru secrets set` nesses casos mandava o dev para um caminho que falha.
431
+ */
432
+ export function avisoSegredoDescartadoNaVm(chave, env) {
433
+ const inicio = `Variável com aparência de segredo ignorada (${chave}): `;
434
+ if (CHAVES_DO_CARREGADOR.includes(chave)) {
435
+ return inicio + 'ela muda como o processo carrega código e não pode vir de um segredo do produto.';
436
+ }
437
+ if (chaveInjetadaPeloCore(chave)) {
438
+ return (inicio +
439
+ 'o Core já a entrega ao app na VM por outro canal (credencial SDK, OIDC ou CLI, módulo ' +
440
+ 'ativo, ambiente ou unit do app). Não precisa passá-la no --env-file nem registrá-la como ' +
441
+ 'segredo do produto.');
442
+ }
443
+ if (!CHAVE_SEGREDO_RE.test(chave)) {
444
+ return (inicio +
445
+ 'segredo não vai no deploy por variável de ambiente (ficaria gravado no Firestore, que ' +
446
+ 'toda a equipe lê), e o registro de segredos do produto só aceita nome em CAIXA_ALTA ' +
447
+ '(letras, dígitos e underscore, começando por letra, de 2 a 64 caracteres). Renomeie a ' +
448
+ `variável no app e registre-a com \`${comandoParaRegistrarSegredo('<NOME>', env)}\`.`);
449
+ }
450
+ return (inicio +
451
+ 'segredo não vai no deploy por variável de ambiente (ficaria gravado no Firestore, que toda ' +
452
+ `a equipe lê). Registre-o com \`${comandoParaRegistrarSegredo(chave, env)}\` — o valor vai ` +
453
+ 'direto para o Secret Manager; use --generate quando o Core puder sortear o valor.');
454
+ }
455
+ /**
456
+ * O aviso desta chave manda para `neetru secrets set`? Não manda quando o
457
+ * Core reserva a chave — aí o aviso de que o agente ainda não entrega
458
+ * segredos do produto também não cabe.
459
+ */
460
+ export function segredoDescartadoApontaSecretsSet(chave) {
461
+ return problemaNaChaveReservada(chave) === null;
462
+ }
463
+ /**
464
+ * Lê o `--env-file` do deploy e devolve o texto. Revisão do cli#81 (N-r3-1):
465
+ * `readFile(…, 'utf8')` trocava cada byte que não é UTF-8 por U+FFFD sem aviso
466
+ * — um .env ANSI com `NOME_LOJA=Café` ia ao deploy como `Caf\uFFFD`. Agora o
467
+ * arquivo é lido em bytes e recusado com `invalid_encoding`, pela mesma regra
468
+ * do `neetru secrets set --from-file`. `runDeploy` chama esta função antes do
469
+ * catálogo, de qualquer pergunta e de qualquer chamada de rede (rodada final 3).
470
+ */
471
+ async function lerEnvFileDoDeploy(envFile, json) {
472
+ const bruto = await fsp.readFile(envFile);
473
+ try {
474
+ return semBomInicial(decodificarUtf8(bruto, 'env-file'));
475
+ }
476
+ catch (err) {
477
+ if (err instanceof CodificacaoInvalida)
478
+ deployFail(json, 'invalid_encoding', err.message);
479
+ throw err;
480
+ }
481
+ }
482
+ /**
483
+ * Monta as variáveis do `--env-file` que `lerEnvFileDoDeploy` já leu. Fica
484
+ * para depois do `pickTarget` porque os avisos dependem do alvo.
485
+ */
486
+ function envVarsDoEnvFile(content, target = 'cloud-run', json,
487
+ /** `--env` do deploy (só para o aviso de segredo descartado na VM). */
488
+ env) {
297
489
  const lg = scopedLog(json);
298
- const content = await fsp.readFile(envFile, 'utf8');
299
490
  const out = {};
491
+ let descartouSegredoNaVm = false;
300
492
  if (target === 'vm') {
301
493
  // (3) Lembrete único: a credencial SDK não precisa ir no --env-file.
302
494
  lg.info('A credencial SDK é injetada automaticamente na VM pelo Core/agente — ' +
@@ -342,13 +534,23 @@ async function loadEnvFile(envFile, target = 'cloud-run', json) {
342
534
  }
343
535
  // (1) Segredos são dropados aqui (o Core rejeitaria com HTTP 400).
344
536
  if (isSecretEnvVarKey(k)) {
345
- lg.warn(`Variável com aparência de segredo ignorada (${k}): segredos não podem ` +
346
- 'ir no deploy via env (vazariam no Firestore, legível por todo o staff). ' +
347
- 'Use o Secret Manager e referencie por nome na VM.');
537
+ if (target === 'vm') {
538
+ if (segredoDescartadoApontaSecretsSet(k))
539
+ descartouSegredoNaVm = true;
540
+ lg.warn(avisoSegredoDescartadoNaVm(k, env));
541
+ }
542
+ else {
543
+ lg.warn(`Variável com aparência de segredo ignorada (${k}): segredos não podem ` +
544
+ 'ir no deploy via env (vazariam no Firestore, legível por todo o staff). ' +
545
+ 'Use o Secret Manager e referencie por nome na VM.');
546
+ }
348
547
  continue;
349
548
  }
350
549
  out[k] = v;
351
550
  }
551
+ // Uma vez só, depois da lista: o registro ainda não chega ao app na VM.
552
+ if (descartouSegredoNaVm)
553
+ lg.warn(AVISO_AGENTE_PENDENTE);
352
554
  return out;
353
555
  }
354
556
  // handleApiError unificado em ../lib/api-error.js (auditoria qualidade 2026-06-20).
@@ -387,7 +589,7 @@ async function pickProduct(opts, nonInteractive, cwd) {
387
589
  ]);
388
590
  return slug;
389
591
  }
390
- async function pickTarget(opts, nonInteractive) {
592
+ export async function pickTarget(opts, nonInteractive, runtime) {
391
593
  if (opts.target)
392
594
  return opts.target;
393
595
  // Não-interativo (CI/scripts) mantém `vm` como default — mudar pra workspace
@@ -395,17 +597,27 @@ async function pickTarget(opts, nonInteractive) {
395
597
  // interativo o menu já oferece workspace como recomendado/primeiro.
396
598
  if (nonInteractive)
397
599
  return 'vm';
600
+ // Produto `runtime: nextjs` (o padrão do `neetru init`): o workspace roda UM
601
+ // handler .ts, não um app Next.js. Recomendar "Workspace" aqui levava quem
602
+ // aceita o padrão direto ao aviso do contrato (confirmação final do PR #74).
603
+ const ehNext = runtime === 'nextjs';
398
604
  const { target } = await inquirer.prompt([
399
605
  {
400
606
  type: 'list',
401
607
  name: 'target',
402
608
  message: 'Onde deployar?',
403
- default: 'workspace',
404
- choices: [
405
- { name: 'Workspace — runtime compartilhado, mais barato (recomendado)', value: 'workspace' },
406
- { name: 'Cloud Run — serviço dedicado, escala a zero', value: 'cloud-run' },
407
- { name: 'VM existente — custo fixo, sem cold start', value: 'vm' },
408
- ],
609
+ default: ehNext ? 'vm' : 'workspace',
610
+ choices: ehNext
611
+ ? [
612
+ { name: 'VM existente — serve o app Next.js (recomendado)', value: 'vm' },
613
+ { name: 'Cloud Run — serviço dedicado, escala a zero (imagem)', value: 'cloud-run' },
614
+ { name: 'Workspace — só UM handler .ts, não o app Next.js', value: 'workspace' },
615
+ ]
616
+ : [
617
+ { name: 'Workspace — runtime compartilhado, mais barato (recomendado)', value: 'workspace' },
618
+ { name: 'Cloud Run — serviço dedicado, escala a zero', value: 'cloud-run' },
619
+ { name: 'VM existente — custo fixo, sem cold start', value: 'vm' },
620
+ ],
409
621
  },
410
622
  ]);
411
623
  return target;
@@ -962,16 +1174,32 @@ async function probeDeployedHealth(domain) {
962
1174
  * Resolve o arquivo de source `.ts` de um deploy workspace. Ordem de busca:
963
1175
  * `--artifact <arquivo>` → campo `source` do neetru.config.json → candidatos
964
1176
  * default (`handler.ts`, `src/handler.ts`, `index.ts`, `src/index.ts`).
1177
+ *
1178
+ * Devolve também os primeiros BYTES CRUS (`inicio`) e de onde o arquivo veio
1179
+ * (`origem`): o farejador de pacote precisa dos bytes antes da decodificação
1180
+ * UTF-8 (depois dela o gzip já virou U+FFFD) e a mensagem de recusa precisa
1181
+ * dizer qual porta apontou o arquivo.
965
1182
  */
966
1183
  async function resolveWorkspaceSource(opts, cwd) {
967
- const cfg = (await loadLocalConfig(cwd));
968
- const candidates = [opts.artifact, cfg?.source, ...WORKSPACE_SOURCE_CANDIDATES].filter((c) => typeof c === 'string' && c.length > 0);
969
- for (const c of candidates) {
970
- const abs = path.resolve(cwd, c);
1184
+ const cfg = await loadLocalConfig(cwd);
1185
+ const candidates = [
1186
+ { caminho: opts.artifact, origem: 'artifact' },
1187
+ { caminho: cfg?.source, origem: 'config' },
1188
+ ...WORKSPACE_SOURCE_CANDIDATES.map((c) => ({ caminho: c, origem: 'padrao' })),
1189
+ ];
1190
+ for (const { caminho, origem } of candidates) {
1191
+ if (typeof caminho !== 'string' || caminho.length === 0)
1192
+ continue;
1193
+ const abs = path.resolve(cwd, caminho);
971
1194
  if (fileExists(abs)) {
1195
+ // Uma leitura só: `toString('utf8')` é exatamente o que
1196
+ // `readFile(abs, 'utf8')` fazia.
1197
+ const bruto = await fsp.readFile(abs);
972
1198
  return {
973
- relPath: path.relative(cwd, abs) || c,
974
- content: await fsp.readFile(abs, 'utf8'),
1199
+ relPath: path.relative(cwd, abs) || caminho,
1200
+ content: bruto.toString('utf8'),
1201
+ inicio: bruto.subarray(0, BYTES_FAREJADOS),
1202
+ origem,
975
1203
  };
976
1204
  }
977
1205
  }
@@ -995,11 +1223,38 @@ const WORKSPACE_SOURCE_GCS_THRESHOLD_BYTES = 1.5 * 1024 * 1024;
995
1223
  * inline. O Core aceita os dois (campo `sourceUrl` no schema).
996
1224
  */
997
1225
  async function runWorkspaceDeploy(opts, productSlug, envName, cwd, nonInteractive) {
998
- if (!WORKSPACE_ENVS.has(envName)) {
999
- deployFail(opts.json, 'invalid_env', `Ambiente inválido: "${envName}". Use dev, staging ou prod.`);
1000
- }
1226
+ validarAmbienteDoDeploy(envName, opts.json);
1001
1227
  const source = await resolveWorkspaceSource(opts, cwd);
1002
1228
  const sourceSizeBytes = Buffer.byteLength(source.content, 'utf8');
1229
+ // bug_a8375858 — PACOTE no workspace, pela porta que o gate não vê.
1230
+ //
1231
+ // O gate de contrato (`deploy-target-contract.ts`) só enxerga a extensão de
1232
+ // `--artifact`. Um pacote apontado pelo `source` do neetru.config.json, ou
1233
+ // com extensão que a lista não prevê (`.bin`, nome sem extensão…), chegava
1234
+ // até aqui e só batia no teto abaixo. Lido como UTF-8 o binário incha ~1,75x,
1235
+ // então um pacote abaixo de ~1,15 MiB passava no teto, subia e voltava do
1236
+ // Core como erro de compilação do esbuild, que não diz a causa (revisão do
1237
+ // PR #74). Recusa aqui, ANTES de qualquer signed URL ou POST, pelo nome e
1238
+ // pelos bytes crus — e vem ANTES do teto, pra que um pacote grande ouça
1239
+ // "é pacote" e não "é grande".
1240
+ const motivoDePacote = pareceArquivoDePacote(source.relPath)
1241
+ ? 'a extensão é de pacote de aplicação'
1242
+ : farejarConteudoDePacote(source.inicio);
1243
+ if (motivoDePacote) {
1244
+ deployFail(opts.json, 'artifact_nao_eh_handler_workspace', mensagemSourceNaoEhHandler(source.relPath, motivoDePacote, source.origem));
1245
+ }
1246
+ // bug_a8375858 — teto de source, depois da leitura: na prática, o handler
1247
+ // que simplesmente cresceu demais.
1248
+ //
1249
+ // O que faltava: a CLI conhecia só o limiar de 1.5 MiB do bug_eae4bef6, que
1250
+ // decide *inline vs GCS*, e NUNCA o teto absoluto do Core
1251
+ // (`WORKSPACE_SOURCE_MAX_BYTES`, 2 MiB, aplicado nos dois caminhos). Então
1252
+ // tudo acima de 2 MiB subia pro GCS pra só depois voltar 413 "Falha ao
1253
+ // resolver a source do deploy" — mensagem que aponta a dimensão errada.
1254
+ if (sourceSizeBytes > WORKSPACE_SOURCE_MAX_BYTES) {
1255
+ const cfgTeto = await loadLocalConfig(cwd);
1256
+ deployFail(opts.json, 'source_acima_do_teto_workspace', mensagemSourceAcimaDoTeto(source.relPath, sourceSizeBytes, cfgTeto?.runtime));
1257
+ }
1003
1258
  if (!nonInteractive) {
1004
1259
  log.heading('Resumo');
1005
1260
  log.dim(` produto: ${productSlug}`);
@@ -1100,6 +1355,7 @@ async function runWorkspaceDeploy(opts, productSlug, envName, cwd, nonInteractiv
1100
1355
  headers: mfaHeaders,
1101
1356
  body: deployBody,
1102
1357
  });
1358
+ mostrarAvisosDoCore(resp.warnings, opts.json);
1103
1359
  }
1104
1360
  catch (err) {
1105
1361
  spinner.fail('Falha ao publicar.');
@@ -1436,28 +1692,20 @@ async function resolveCloudRunImage(opts, productSlug, envName, cwd) {
1436
1692
  * Deploy `target=cloud-run` — cria/atualiza o serviço Cloud Run dedicado do
1437
1693
  * ambiente. Síncrono: o POST retorna com o desfecho (`live`/`failed`/...).
1438
1694
  */
1439
- async function runCloudRunDeploy(opts, productSlug, envName, cwd, nonInteractive) {
1440
- if (!WORKSPACE_ENVS.has(envName)) {
1441
- deployFail(opts.json, 'invalid_env', `Ambiente inválido: "${envName}". Use dev, staging ou prod.`);
1442
- }
1443
- // Fix bug_ec17af083d7045a1a00f24255efb716c:
1444
- // Cloud Run exige uma imagem de container — não um tarball source.
1445
- // Se o dev passou --artifact (útil pra target=vm/workspace), informamos
1446
- // claramente o que fazer em vez de silenciosamente ignorar a flag.
1447
- if (opts.artifact || opts.artifactUrl) {
1448
- deployFail(opts.json, 'artifact_not_supported_cloud_run', 'target=cloud-run não aceita --artifact / --artifact-url (tarball source).\n' +
1449
- ' Cloud Run precisa de uma imagem de container OCI já buildada e pushada.\n' +
1450
- '\n' +
1451
- ' Opções:\n' +
1452
- ' 1. --image <uri> — URI de imagem já no Artifact Registry.\n' +
1453
- ' Exemplo: --image=us-central1-docker.pkg.dev/neetru/gestovendas-staging/app:1.0.0\n' +
1454
- '\n' +
1455
- ' 2. --registry <host|host/project/repo> — CLI usa Cloud Build remoto automaticamente.\n' +
1456
- ' Não exige Docker instalado. Requer gcloud autenticado.\n' +
1457
- ' Exemplo: --registry=us-central1-docker.pkg.dev/neetru/gestovendas-staging\n' +
1458
- '\n' +
1459
- ' 3. Mude o target pra vm ou workspace se quiser usar o tarball:\n' +
1460
- ' --target=vm --artifact=' + (opts.artifact ?? opts.artifactUrl ?? '<file>'));
1695
+ async function runCloudRunDeploy(opts, productSlug, envName, cwd, nonInteractive,
1696
+ /** O `--env-file` já lido e conferido por `runDeploy` (`lerEnvFileDoDeploy`). */
1697
+ envFileTexto) {
1698
+ validarAmbienteDoDeploy(envName, opts.json);
1699
+ // bug_ec17af08 (cloud-run não aceita tarball) agora mora na tabela do
1700
+ // `deploy-target-contract.ts`, avaliada em `runDeploy` logo após o
1701
+ // `pickTarget` — ANTES de qualquer build/upload, pra TODOS os alvos. O
1702
+ // código de erro e o texto são os mesmos; só deixou de ser um `if` solto.
1703
+ // Rodadas finais 2 e 3 do cli#81: o `--env-file` era lido depois de
1704
+ // `resolveCloudRunImage`, que manda a imagem ao Cloud Build. Agora
1705
+ // `runDeploy` o lê e confere antes de tudo; aqui só se montam as variáveis.
1706
+ let envVars;
1707
+ if (envFileTexto !== undefined) {
1708
+ envVars = envVarsDoEnvFile(envFileTexto, 'cloud-run', opts.json);
1461
1709
  }
1462
1710
  // bug_863ae5: pre-flight IAM check — abort cedo se SA Core não tiver permissão.
1463
1711
  // MASTER-HIGH-048 (2.11.0): prefere auth.json sobre Conf legado.
@@ -1476,10 +1724,6 @@ async function runCloudRunDeploy(opts, productSlug, envName, cwd, nonInteractive
1476
1724
  publicSecretScan = await runPublicSecretGate({ kind: 'dir', root: cwd }, { cwd, envFile: opts.envFile, stack: opts.stack, allowReason: opts.allowPublicSecret, json: opts.json });
1477
1725
  }
1478
1726
  const imageUrl = await resolveCloudRunImage(opts, productSlug, envName, cwd);
1479
- let envVars;
1480
- if (opts.envFile) {
1481
- envVars = await loadEnvFile(path.resolve(cwd, opts.envFile), 'cloud-run', opts.json);
1482
- }
1483
1727
  if (!nonInteractive) {
1484
1728
  log.heading('Resumo');
1485
1729
  log.dim(` produto: ${productSlug}`);
@@ -1512,6 +1756,7 @@ async function runCloudRunDeploy(opts, productSlug, envName, cwd, nonInteractive
1512
1756
  ...(publicSecretScan.override ? { publicSecretOverride: publicSecretScan.override } : {}),
1513
1757
  },
1514
1758
  });
1759
+ mostrarAvisosDoCore(resp.warnings, opts.json);
1515
1760
  }
1516
1761
  catch (err) {
1517
1762
  spinner.fail('Falha ao publicar.');
@@ -1679,6 +1924,17 @@ export async function runDeploy(opts) {
1679
1924
  const nonInteractive = ensureNonInteractive(opts);
1680
1925
  // 2.30.0: motivo do override validado ANTES de build/upload.
1681
1926
  opts = { ...opts, allowPublicSecret: validateAllowPublicSecretReason(opts.allowPublicSecret, opts.json) };
1927
+ // Rodadas finais 2 e 3 do cli#81: o `--env-file` é lido e conferido AQUI,
1928
+ // antes do catálogo e da pergunta do `pickProduct`, da pergunta do
1929
+ // `checkSchemaGate` e do `pickTarget`, da lista de servidores, do build e
1930
+ // do upload. Um arquivo fora de UTF-8 sai como `invalid_encoding` antes de
1931
+ // tudo isso. As variáveis só são montadas depois do `pickTarget`, porque os
1932
+ // avisos dependem do alvo. Com `--target workspace` o arquivo não é lido: o
1933
+ // workspace o descarta, com o aviso do contrato de alvo. Sem `--target`, o
1934
+ // menu ainda pode escolher o workspace, e então o arquivo foi conferido à toa.
1935
+ const envFileTexto = opts.envFile && opts.target !== 'workspace'
1936
+ ? await lerEnvFileDoDeploy(path.resolve(cwd, opts.envFile), opts.json)
1937
+ : undefined;
1682
1938
  const productSlug = await pickProduct(opts, nonInteractive, cwd);
1683
1939
  // GAP-M1-5 — gate de schema: detecta drift antes de prosseguir.
1684
1940
  const canDeploy = await checkSchemaGate(cwd, opts);
@@ -1697,7 +1953,62 @@ export async function runDeploy(opts) {
1697
1953
  docsCheck = undefined;
1698
1954
  }
1699
1955
  }
1700
- const target = await pickTarget(opts, nonInteractive);
1956
+ // O `runtime` do config serve a duas coisas aqui: recomendar o alvo certo no
1957
+ // menu (que só abre em modo interativo e sem `--target`) e o gate de contrato
1958
+ // logo abaixo. Uma leitura só, e só vale se o config é DESTE produto.
1959
+ const runtimeDoConfig = runtimeDoConfigDesteProduto(await loadLocalConfig(cwd), opts.product);
1960
+ const target = await pickTarget(opts, nonInteractive, runtimeDoConfig);
1961
+ // bug_a8375858 — GATE DE CONTRATO `target × runtime × flags`.
1962
+ //
1963
+ // Roda aqui, entre a escolha do alvo e QUALQUER trabalho caro (build,
1964
+ // empacotamento, signed URL, upload pro GCS, POST no Core). Era exatamente
1965
+ // esse o custo do bug: `--target workspace` com tarball de 20 MB subia
1966
+ // ~35 MB pro GCS e só então voltava um 413 que falava de tamanho quando o
1967
+ // problema era o TIPO do alvo.
1968
+ //
1969
+ // O `runtime` já vinha no neetru.config.json e já era lido aqui (o
1970
+ // `loadLocalConfig` devolve o JSON cru) — só não era olhado. Ausente =
1971
+ // nenhuma opinião; o gate não inventa bloqueio pra repo antigo.
1972
+ {
1973
+ const veredito = checkDeployTargetContract({
1974
+ target,
1975
+ runtime: runtimeDoConfig,
1976
+ flags: {
1977
+ artifact: opts.artifact,
1978
+ artifactUrl: opts.artifactUrl,
1979
+ artifactSha256: opts.artifactSha256,
1980
+ image: opts.image,
1981
+ registry: opts.registry,
1982
+ registryRepo: opts.registryRepo,
1983
+ buildLocal: opts.buildLocal,
1984
+ server: opts.server,
1985
+ port: opts.port,
1986
+ stack: opts.stack,
1987
+ envFile: opts.envFile,
1988
+ domain: opts.domain,
1989
+ localArtifact: opts.localArtifact,
1990
+ },
1991
+ });
1992
+ for (const aviso of veredito.warnings) {
1993
+ // Em `--json` o stdout é contrato de 1 linha — aviso vai pro stderr
1994
+ // (bug_6d92ef71: calar por completo em modo máquina é pior).
1995
+ if (opts.json)
1996
+ console.error(aviso);
1997
+ else
1998
+ log.warn(aviso);
1999
+ }
2000
+ if (veredito.errors.length > 0) {
2001
+ const primeiro = veredito.errors[0];
2002
+ // `deployFail` emite 1 erro e sai; os demais entram no corpo pra que uma
2003
+ // rodada só mostre tudo que está errado (meio-conserto é não contar os
2004
+ // outros e o dev voltar aqui amanhã).
2005
+ const extras = veredito.errors
2006
+ .slice(1)
2007
+ .map((e) => `\n\nTambém: ${e.message}`)
2008
+ .join('');
2009
+ deployFail(opts.json, primeiro.code, primeiro.message + extras);
2010
+ }
2011
+ }
1701
2012
  // Caminho workspace (Fase 1 do Fluxo de Ambientes) — sem server/tar/upload.
1702
2013
  if (target === 'workspace') {
1703
2014
  await runWorkspaceDeploy(opts, productSlug, (opts.env ?? 'dev').toLowerCase(), cwd, nonInteractive);
@@ -1705,9 +2016,21 @@ export async function runDeploy(opts) {
1705
2016
  }
1706
2017
  // Caminho cloud-run (Fase 3) — imagem de container, serviço dedicado.
1707
2018
  if (target === 'cloud-run') {
1708
- await runCloudRunDeploy(opts, productSlug, (opts.env ?? 'dev').toLowerCase(), cwd, nonInteractive);
2019
+ await runCloudRunDeploy(opts, productSlug, (opts.env ?? 'dev').toLowerCase(), cwd, nonInteractive, envFileTexto);
1709
2020
  return;
1710
2021
  }
2022
+ // F-11: env name pra GCS path (`gs://neetru-deploy-artifacts/<product>/<env>/…`).
2023
+ // Default 'dev' quando não definido — alinhado com o middleware OAuth.
2024
+ const envName = (opts.env ?? 'dev').toLowerCase();
2025
+ // EnvVars. Rodadas finais 2 e 3 do cli#81: o `--env-file` era lido DEPOIS
2026
+ // de `resolveArtifact`. Um arquivo fora de UTF-8 só era recusado depois do
2027
+ // build interno (que recusava no stderr, e o `deploy --json` ficava sem a
2028
+ // linha JSON) ou depois de o pacote em cache subir ao GCS. Agora o começo de
2029
+ // `runDeploy` o lê e confere; aqui só se montam as variáveis.
2030
+ let envVars;
2031
+ if (envFileTexto !== undefined) {
2032
+ envVars = envVarsDoEnvFile(envFileTexto, target, opts.json, envName);
2033
+ }
1711
2034
  let serverChoice = null;
1712
2035
  if (target === 'vm') {
1713
2036
  serverChoice = await pickServer(opts, nonInteractive);
@@ -1718,20 +2041,27 @@ export async function runDeploy(opts) {
1718
2041
  // motivou o guard antigo (Core rejeitava sem --port); o Core agora ignora
1719
2042
  // porta em deploy VM, então exigi-la aqui só atrapalharia scripts/CI.
1720
2043
  // --port continua aceito (compat) mas é ignorado com WARN — ver abaixo.
1721
- if (target === 'vm' && opts.port && !opts.json) {
1722
- log.warn('A porta é gerenciada pelo Core; --port é ignorado em deploy staging/prod.');
2044
+ //
2045
+ // bug_6d92ef71 (low): o `!opts.json` calava o aviso POR COMPLETO em modo
2046
+ // machine-readable — nem no stdout (certo, quebraria o contrato de 1 linha
2047
+ // JSON do --json), nem em canal nenhum. Quem roda via script nunca
2048
+ // descobria que --port foi descartado. Mantém o aviso humano em stdout no
2049
+ // modo interativo e manda a MESMA informação pro stderr em --json — stderr
2050
+ // não faz parte do contrato de saída parseável, então não quebra nada.
2051
+ // Mensagem também deixou de dizer "staging/prod": o guard dispara pra
2052
+ // QUALQUER ambiente em target=vm (inclusive dev).
2053
+ if (target === 'vm' && opts.port) {
2054
+ const portIgnoredMsg = 'A porta é gerenciada pelo Core em deploy VM (aloca por produto/ambiente/servidor); --port é ignorado.';
2055
+ if (opts.json) {
2056
+ console.error(portIgnoredMsg);
2057
+ }
2058
+ else {
2059
+ log.warn(portIgnoredMsg);
2060
+ }
1723
2061
  }
1724
2062
  const { domain } = await pickDomain(opts, nonInteractive, productSlug);
1725
- // F-11: env name pra GCS path (`gs://neetru-deploy-artifacts/<product>/<env>/…`).
1726
- // Default 'dev' quando não definido — alinhado com o middleware OAuth.
1727
- const envName = (opts.env ?? 'dev').toLowerCase();
1728
2063
  // Build / artifact (default: faz upload pra GCS via signed URL).
1729
2064
  const artifact = await resolveArtifact(opts, productSlug, envName, cwd);
1730
- // EnvVars.
1731
- let envVars;
1732
- if (opts.envFile) {
1733
- envVars = await loadEnvFile(path.resolve(cwd, opts.envFile), target, opts.json);
1734
- }
1735
2065
  // Confirmação interativa.
1736
2066
  if (!nonInteractive) {
1737
2067
  log.heading('Resumo');
@@ -1740,7 +2070,13 @@ export async function runDeploy(opts) {
1740
2070
  // no resumo — só cloud-run/workspace mostravam. `ambiente` é o parâmetro que
1741
2071
  // decide se secrets/rota de prod são tocados; tem que aparecer. Em prod,
1742
2072
  // destacado (log.warn) pra não passar batido num Enter.
1743
- if (envName === 'prod') {
2073
+ // 2026-10-12: era `envName === 'prod'`, mas `isProdEnv` (logo acima, e a
2074
+ // guarda que decide o step-up MFA) aceita TAMBEM `production`. Entao um
2075
+ // `--env production` passava pelo gate de MFA como producao e aparecia no
2076
+ // resumo como ambiente qualquer — sem o destaque, com o Enter do operador
2077
+ // no meio. Conserto pela metade: a mesma pergunta respondida de dois jeitos
2078
+ // no mesmo arquivo.
2079
+ if (isProdEnv(envName)) {
1744
2080
  log.warn(` ambiente: ${envName} ← PRODUÇÃO`);
1745
2081
  }
1746
2082
  else {
@@ -1784,6 +2120,14 @@ export async function runDeploy(opts) {
1784
2120
  artifactSha256: artifact.artifactSha256,
1785
2121
  artifactSizeBytes: artifact.artifactSizeBytes,
1786
2122
  };
2123
+ // bug_678ddc98: standalone aninhado de monorepo — o agente não tem como
2124
+ // inferir `apps/web/server.js` olhando a raiz do tarball. Ver
2125
+ // `entrypointParaDeploy` (e, no PR, o que ainda falta do lado do Core).
2126
+ const entrypointAninhado = entrypointParaDeploy(artifact.manifest);
2127
+ if (entrypointAninhado) {
2128
+ body.entrypoint = entrypointAninhado;
2129
+ log.dim(` entrypoint: ${entrypointAninhado} (standalone aninhado — monorepo)`);
2130
+ }
1787
2131
  if (target === 'vm') {
1788
2132
  body.serverId = serverChoice.id;
1789
2133
  // P6 (2026-07-19): NÃO enviar porta em deploy VM — é detalhe interno do
@@ -1808,6 +2152,7 @@ export async function runDeploy(opts) {
1808
2152
  headers: mfaHeaders,
1809
2153
  body,
1810
2154
  });
2155
+ mostrarAvisosDoCore(deploy.warnings, opts.json);
1811
2156
  dispatchSpinner.succeed(`Deploy criado: ${chalk.bold(deploy.deploymentId)}`);
1812
2157
  }
1813
2158
  catch (err) {