@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
@@ -27,14 +27,21 @@ 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';
33
36
  import { readDbJson, schemaFingerprint } from '../lib/db-local/index.js';
34
37
  // bug_863ae5: pre-flight IAM check.
35
38
  import { assertIamReady } from '../lib/iam-preflight.js';
39
+ import { ALLOW_PUBLIC_SECRET_FLAG, MIN_OVERRIDE_REASON_LENGTH, buildOverrideRecord, collectPublicEnvSecrets, formatFindingsMessage, scanDirectoryForPublicSecrets, scanTarballForPublicSecrets, } from '../lib/public-secret-scan.js';
36
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';
37
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';
38
45
  /**
39
46
  * bug_a5786e03 (2026-05-31): o Core/agente só conhecem os stacks canônicos
40
47
  * `node|docker|php-apache|static` (PRODUCT_STACKS). `nextjs` é um alias de
@@ -86,6 +93,113 @@ export function resolveDeployVersion(manifestVersion, explicitVersion, json) {
86
93
  }
87
94
  return version;
88
95
  }
96
+ /**
97
+ * Valida o motivo do `--allow-public-secret` ANTES de qualquer build/upload.
98
+ * `undefined` = flag não passada.
99
+ */
100
+ export function validateAllowPublicSecretReason(raw, json) {
101
+ if (raw === undefined)
102
+ return undefined;
103
+ const reason = String(raw).trim();
104
+ if (reason.length < MIN_OVERRIDE_REASON_LENGTH) {
105
+ deployFail(json, 'invalid_override_reason', `${ALLOW_PUBLIC_SECRET_FLAG} exige um motivo de verdade (mínimo ${MIN_OVERRIDE_REASON_LENGTH} caracteres) — ` +
106
+ 'ele fica registrado no deploy. Ex.: --allow-public-secret "rotação da chave em andamento, ticket #123".');
107
+ }
108
+ return reason;
109
+ }
110
+ /**
111
+ * 2.30.0 — gate de segredo no pacote PÚBLICO (decisão do dono 2026-10-03: a
112
+ * credencial nrt_ é SÓ DE SERVIDOR). Roda ANTES do upload. Achou → falha
113
+ * FECHADO (exit 1, `public_secret_in_bundle`), a não ser que haja override com
114
+ * motivo — aí segue com aviso alto e devolve o registro pro corpo do deploy.
115
+ */
116
+ export async function runPublicSecretGate(source, ctx) {
117
+ const lg = scopedLog(ctx.json);
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
+ });
128
+ let result;
129
+ try {
130
+ result =
131
+ source.kind === 'tar'
132
+ ? await scanTarballForPublicSecrets(source.path, { stack: ctx.stack, envSecrets })
133
+ : await scanDirectoryForPublicSecrets(source.root, { stack: ctx.stack, envSecrets });
134
+ }
135
+ catch (err) {
136
+ // Não conseguir LER o pacote é falha fechada também (não dá pra afirmar que está limpo).
137
+ deployFail(ctx.json, 'public_secret_scan_failed', `Não consegui varrer o pacote em busca de segredos: ${err.message}`);
138
+ }
139
+ if (result.skippedLarge.length > 0) {
140
+ lg.warn(`Varredura de segredo: ${result.skippedLarge.length} arquivo(s) público(s) grande(s) demais não foram varridos ` +
141
+ `(ex.: ${result.skippedLarge[0]}).`);
142
+ }
143
+ if (result.findings.length === 0) {
144
+ lg.dim(` Varredura de segredo no pacote público: ok (${result.scannedFiles} arquivo(s)).`);
145
+ return { scanned: true, scannedFiles: result.scannedFiles };
146
+ }
147
+ if (!ctx.allowReason) {
148
+ deployFail(ctx.json, 'public_secret_in_bundle', formatFindingsMessage(result.findings));
149
+ }
150
+ const override = buildOverrideRecord(ctx.allowReason, result.findings);
151
+ lg.warn(formatFindingsMessage(result.findings).split('\n')[0].replace('Deploy BLOQUEADO.', 'Deploy LIBERADO por override.'));
152
+ lg.warn(`OVERRIDE ${ALLOW_PUBLIC_SECRET_FLAG} — motivo registrado no deploy: "${ctx.allowReason}". ` +
153
+ 'Trate a chave exposta como VAZADA: rotacione-a assim que possível.');
154
+ return { scanned: true, scannedFiles: result.scannedFiles, override };
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
+ }
89
203
  /** `true` pra `prod`/`production` (o Core normaliza `production`→`prod`). */
90
204
  export function isProdEnv(envName) {
91
205
  const e = (envName ?? '').trim().toLowerCase();
@@ -94,7 +208,7 @@ export function isProdEnv(envName) {
94
208
  /**
95
209
  * M7 (auditoria cli/v1 2026-09-29): `POST /api/cli/v1/deploy` com
96
210
  * `environmentId` prod exige step-up MFA no Core (mesmo gate de `promote`).
97
- * 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
98
212
  * --json/--non-interactive/sem TTY) só quando o alvo é prod. Chamado logo
99
213
  * antes do POST — depois do build/upload — pra o TOTP não vencer no meio.
100
214
  */
@@ -108,26 +222,74 @@ export async function resolveDeployStepUpHeaders(opts, envName, nonInteractive)
108
222
  });
109
223
  return headers;
110
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
+ }
111
255
  const TERMINAL_STATUSES = new Set(['success', 'failed', 'cancelled']);
112
- /** Estágios de ambiente válidos para o deploy workspace. */
113
- 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
+ }
114
279
  /**
115
280
  * Mapeia o `envName` do deploy ('dev' | 'staging' | 'prod' — vocabulário do
116
281
  * `--env` deste comando) pro `targetEnv` que `neetru build` entende
117
282
  * ('dev' | 'workspace' | 'prod' — vocabulário de `BuildOptions`/SDK).
118
283
  * 'staging' vira 'workspace' (mesmo conceito de ambiente não-dev/não-prod).
119
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.
120
286
  *
121
287
  * Fix (2026-07-22, achado da revisão adversarial): sem isso, todo build
122
288
  * implícito de `neetru deploy --env dev`/`--env staging` (quando não há
123
289
  * artifact em cache) forçava NEXT_PUBLIC_NEETRU_ENV=prod incondicionalmente.
124
290
  */
125
291
  export function deployEnvToBuildTargetEnv(envName) {
126
- if (envName === 'dev')
127
- return 'dev';
128
- if (envName === 'staging')
129
- return 'workspace';
130
- return 'prod';
292
+ return deployEnvParaRuntime(envName) ?? 'prod';
131
293
  }
132
294
  /** Candidatos default de arquivo de source para um deploy workspace. */
133
295
  const WORKSPACE_SOURCE_CANDIDATES = [
@@ -168,6 +330,16 @@ async function loadLocalConfig(cwd) {
168
330
  }
169
331
  return null;
170
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
+ }
171
343
  /**
172
344
  * Nomes de variáveis que NUNCA devem ser injetados no deploy —
173
345
  * podem alterar o comportamento do runtime de formas perigosas.
@@ -241,10 +413,82 @@ const SDK_CRED_ENV_KEYS = new Set([
241
413
  'NEETRU_API_KEY',
242
414
  'NEETRU_CLIENT_ID',
243
415
  ]);
244
- 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) {
245
489
  const lg = scopedLog(json);
246
- const content = await fsp.readFile(envFile, 'utf8');
247
490
  const out = {};
491
+ let descartouSegredoNaVm = false;
248
492
  if (target === 'vm') {
249
493
  // (3) Lembrete único: a credencial SDK não precisa ir no --env-file.
250
494
  lg.info('A credencial SDK é injetada automaticamente na VM pelo Core/agente — ' +
@@ -290,13 +534,23 @@ async function loadEnvFile(envFile, target = 'cloud-run', json) {
290
534
  }
291
535
  // (1) Segredos são dropados aqui (o Core rejeitaria com HTTP 400).
292
536
  if (isSecretEnvVarKey(k)) {
293
- lg.warn(`Variável com aparência de segredo ignorada (${k}): segredos não podem ` +
294
- 'ir no deploy via env (vazariam no Firestore, legível por todo o staff). ' +
295
- '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
+ }
296
547
  continue;
297
548
  }
298
549
  out[k] = v;
299
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);
300
554
  return out;
301
555
  }
302
556
  // handleApiError unificado em ../lib/api-error.js (auditoria qualidade 2026-06-20).
@@ -335,7 +589,7 @@ async function pickProduct(opts, nonInteractive, cwd) {
335
589
  ]);
336
590
  return slug;
337
591
  }
338
- async function pickTarget(opts, nonInteractive) {
592
+ export async function pickTarget(opts, nonInteractive, runtime) {
339
593
  if (opts.target)
340
594
  return opts.target;
341
595
  // Não-interativo (CI/scripts) mantém `vm` como default — mudar pra workspace
@@ -343,17 +597,27 @@ async function pickTarget(opts, nonInteractive) {
343
597
  // interativo o menu já oferece workspace como recomendado/primeiro.
344
598
  if (nonInteractive)
345
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';
346
604
  const { target } = await inquirer.prompt([
347
605
  {
348
606
  type: 'list',
349
607
  name: 'target',
350
608
  message: 'Onde deployar?',
351
- default: 'workspace',
352
- choices: [
353
- { name: 'Workspace — runtime compartilhado, mais barato (recomendado)', value: 'workspace' },
354
- { name: 'Cloud Run — serviço dedicado, escala a zero', value: 'cloud-run' },
355
- { name: 'VM existente — custo fixo, sem cold start', value: 'vm' },
356
- ],
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
+ ],
357
621
  },
358
622
  ]);
359
623
  return target;
@@ -566,7 +830,15 @@ async function uploadArtifactToGcs(tarballAbs, productSlug, envName, sizeBytes,
566
830
  async function resolveArtifact(opts, productSlug, envName, cwd) {
567
831
  // Caso 1: caller já fez upload e passou URL+sha pronta.
568
832
  if (opts.artifactUrl && opts.artifactSha256) {
569
- return { artifactUrl: opts.artifactUrl, artifactSha256: opts.artifactSha256 };
833
+ // 2.30.0: artefato remoto não passa pelo CLI — não dá pra varrer. Avisa (o
834
+ // gate vale pro fluxo padrão build→upload e pro --artifact local).
835
+ scopedLog(opts.json).warn('Varredura de segredo NÃO feita: --artifact-url aponta um pacote remoto. ' +
836
+ 'Prefira `neetru deploy` com build local ou --artifact <arquivo> (que são varridos).');
837
+ return {
838
+ artifactUrl: opts.artifactUrl,
839
+ artifactSha256: opts.artifactSha256,
840
+ publicSecretScan: { scanned: false, reason: 'remote_artifact' },
841
+ };
570
842
  }
571
843
  // Decide se vamos fazer upload pra GCS ou manter file://.
572
844
  const keepLocal = shouldKeepLocalArtifact(opts);
@@ -584,12 +856,15 @@ async function resolveArtifact(opts, productSlug, envName, cwd) {
584
856
  hash.update(chunk);
585
857
  const stat = await fsp.stat(abs);
586
858
  const sha256 = hash.digest('hex');
859
+ // 2.30.0: varredura de segredo ANTES de qualquer envio.
860
+ const publicSecretScan = await runPublicSecretGate({ kind: 'tar', path: abs }, { cwd, envFile: opts.envFile, stack: opts.stack, allowReason: opts.allowPublicSecret, json: opts.json });
587
861
  if (keepLocal) {
588
862
  lg.warn('Modo --local-artifact: mandando file:// pro Core. Só funciona se o agent compartilha FS com este CLI.');
589
863
  return {
590
864
  artifactUrl: `file://${abs.replace(/\\/g, '/')}`,
591
865
  artifactSha256: sha256,
592
866
  artifactSizeBytes: stat.size,
867
+ publicSecretScan,
593
868
  };
594
869
  }
595
870
  const uploaded = await uploadArtifactToGcs(abs, productSlug, envName, stat.size, opts.json);
@@ -598,6 +873,7 @@ async function resolveArtifact(opts, productSlug, envName, cwd) {
598
873
  artifactSha256: sha256,
599
874
  artifactSizeBytes: stat.size,
600
875
  deployId: uploaded.deployId,
876
+ publicSecretScan,
601
877
  };
602
878
  }
603
879
  // Caso 3: procura manifest existente, senão chama runBuild.
@@ -638,6 +914,14 @@ async function resolveArtifact(opts, productSlug, envName, cwd) {
638
914
  });
639
915
  }
640
916
  const tarAbs = path.resolve(cwd, manifest.tarball);
917
+ // 2.30.0: varredura de segredo no pacote que VAI ser enviado, antes do upload.
918
+ const publicSecretScan = await runPublicSecretGate({ kind: 'tar', path: tarAbs }, {
919
+ cwd,
920
+ envFile: opts.envFile,
921
+ stack: manifest.stack ?? opts.stack,
922
+ allowReason: opts.allowPublicSecret,
923
+ json: opts.json,
924
+ });
641
925
  if (keepLocal) {
642
926
  lg.warn('Modo --local-artifact: mandando file:// pro Core. Só funciona se o agent compartilha FS com este CLI.');
643
927
  return {
@@ -645,6 +929,7 @@ async function resolveArtifact(opts, productSlug, envName, cwd) {
645
929
  artifactSha256: manifest.sha256,
646
930
  artifactSizeBytes: manifest.sizeBytes,
647
931
  manifest,
932
+ publicSecretScan,
648
933
  };
649
934
  }
650
935
  const uploaded = await uploadArtifactToGcs(tarAbs, productSlug, envName, manifest.sizeBytes, opts.json);
@@ -654,6 +939,7 @@ async function resolveArtifact(opts, productSlug, envName, cwd) {
654
939
  artifactSizeBytes: manifest.sizeBytes,
655
940
  manifest,
656
941
  deployId: uploaded.deployId,
942
+ publicSecretScan,
657
943
  };
658
944
  }
659
945
  async function pollDeploymentStatus(deploymentId, json) {
@@ -888,16 +1174,32 @@ async function probeDeployedHealth(domain) {
888
1174
  * Resolve o arquivo de source `.ts` de um deploy workspace. Ordem de busca:
889
1175
  * `--artifact <arquivo>` → campo `source` do neetru.config.json → candidatos
890
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.
891
1182
  */
892
1183
  async function resolveWorkspaceSource(opts, cwd) {
893
- const cfg = (await loadLocalConfig(cwd));
894
- const candidates = [opts.artifact, cfg?.source, ...WORKSPACE_SOURCE_CANDIDATES].filter((c) => typeof c === 'string' && c.length > 0);
895
- for (const c of candidates) {
896
- 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);
897
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);
898
1198
  return {
899
- relPath: path.relative(cwd, abs) || c,
900
- 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,
901
1203
  };
902
1204
  }
903
1205
  }
@@ -921,11 +1223,38 @@ const WORKSPACE_SOURCE_GCS_THRESHOLD_BYTES = 1.5 * 1024 * 1024;
921
1223
  * inline. O Core aceita os dois (campo `sourceUrl` no schema).
922
1224
  */
923
1225
  async function runWorkspaceDeploy(opts, productSlug, envName, cwd, nonInteractive) {
924
- if (!WORKSPACE_ENVS.has(envName)) {
925
- deployFail(opts.json, 'invalid_env', `Ambiente inválido: "${envName}". Use dev, staging ou prod.`);
926
- }
1226
+ validarAmbienteDoDeploy(envName, opts.json);
927
1227
  const source = await resolveWorkspaceSource(opts, cwd);
928
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
+ }
929
1258
  if (!nonInteractive) {
930
1259
  log.heading('Resumo');
931
1260
  log.dim(` produto: ${productSlug}`);
@@ -1026,6 +1355,7 @@ async function runWorkspaceDeploy(opts, productSlug, envName, cwd, nonInteractiv
1026
1355
  headers: mfaHeaders,
1027
1356
  body: deployBody,
1028
1357
  });
1358
+ mostrarAvisosDoCore(resp.warnings, opts.json);
1029
1359
  }
1030
1360
  catch (err) {
1031
1361
  spinner.fail('Falha ao publicar.');
@@ -1033,9 +1363,11 @@ async function runWorkspaceDeploy(opts, productSlug, envName, cwd, nonInteractiv
1033
1363
  }
1034
1364
  if (resp.status === 'live') {
1035
1365
  spinner.succeed(`Deploy ${chalk.bold('live')} · ${resp.deploymentId}`);
1366
+ const docsSync = await postDeployDocs(opts, cwd, envName);
1036
1367
  if (opts.json) {
1037
1368
  console.log(JSON.stringify({
1038
1369
  ok: true,
1370
+ ...(docsSync ? { docs: { sync: docsSync } } : {}),
1039
1371
  status: 'live',
1040
1372
  target: 'workspace',
1041
1373
  deploymentId: resp.deploymentId,
@@ -1053,9 +1385,11 @@ async function runWorkspaceDeploy(opts, productSlug, envName, cwd, nonInteractiv
1053
1385
  // aplicou o deploy mas ainda não confirmou saúde. Sucesso COM AVISO — não
1054
1386
  // tratamos como falha nem fingimos "Live" confirmado.
1055
1387
  spinner.warn(`Deploy aplicado — ${deployStatusLabel(resp.status)} · ${resp.deploymentId}`);
1388
+ const docsSync = await postDeployDocs(opts, cwd, envName);
1056
1389
  if (opts.json) {
1057
1390
  console.log(JSON.stringify({
1058
1391
  ok: true,
1392
+ ...(docsSync ? { docs: { sync: docsSync } } : {}),
1059
1393
  status: resp.status,
1060
1394
  verified: false,
1061
1395
  target: 'workspace',
@@ -1358,28 +1692,20 @@ async function resolveCloudRunImage(opts, productSlug, envName, cwd) {
1358
1692
  * Deploy `target=cloud-run` — cria/atualiza o serviço Cloud Run dedicado do
1359
1693
  * ambiente. Síncrono: o POST retorna com o desfecho (`live`/`failed`/...).
1360
1694
  */
1361
- async function runCloudRunDeploy(opts, productSlug, envName, cwd, nonInteractive) {
1362
- if (!WORKSPACE_ENVS.has(envName)) {
1363
- deployFail(opts.json, 'invalid_env', `Ambiente inválido: "${envName}". Use dev, staging ou prod.`);
1364
- }
1365
- // Fix bug_ec17af083d7045a1a00f24255efb716c:
1366
- // Cloud Run exige uma imagem de container — não um tarball source.
1367
- // Se o dev passou --artifact (útil pra target=vm/workspace), informamos
1368
- // claramente o que fazer em vez de silenciosamente ignorar a flag.
1369
- if (opts.artifact || opts.artifactUrl) {
1370
- deployFail(opts.json, 'artifact_not_supported_cloud_run', 'target=cloud-run não aceita --artifact / --artifact-url (tarball source).\n' +
1371
- ' Cloud Run precisa de uma imagem de container OCI já buildada e pushada.\n' +
1372
- '\n' +
1373
- ' Opções:\n' +
1374
- ' 1. --image <uri> — URI de imagem já no Artifact Registry.\n' +
1375
- ' Exemplo: --image=us-central1-docker.pkg.dev/neetru/gestovendas-staging/app:1.0.0\n' +
1376
- '\n' +
1377
- ' 2. --registry <host|host/project/repo> — CLI usa Cloud Build remoto automaticamente.\n' +
1378
- ' Não exige Docker instalado. Requer gcloud autenticado.\n' +
1379
- ' Exemplo: --registry=us-central1-docker.pkg.dev/neetru/gestovendas-staging\n' +
1380
- '\n' +
1381
- ' 3. Mude o target pra vm ou workspace se quiser usar o tarball:\n' +
1382
- ' --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);
1383
1709
  }
1384
1710
  // bug_863ae5: pre-flight IAM check — abort cedo se SA Core não tiver permissão.
1385
1711
  // MASTER-HIGH-048 (2.11.0): prefere auth.json sobre Conf legado.
@@ -1387,11 +1713,17 @@ async function runCloudRunDeploy(opts, productSlug, envName, cwd, nonInteractive
1387
1713
  if (deployToken) {
1388
1714
  await assertIamReady(deployToken, 'deploy.cloud-run', opts.json);
1389
1715
  }
1390
- const imageUrl = await resolveCloudRunImage(opts, productSlug, envName, cwd);
1391
- let envVars;
1392
- if (opts.envFile) {
1393
- envVars = await loadEnvFile(path.resolve(cwd, opts.envFile), 'cloud-run', opts.json);
1716
+ // 2.30.0: varredura de segredo nas pastas públicas ANTES de buildar a imagem.
1717
+ // Com --image (imagem pronta) não há o que varrer aqui — avisa.
1718
+ let publicSecretScan;
1719
+ if (opts.image) {
1720
+ scopedLog(opts.json).warn('Varredura de segredo NÃO feita: --image aponta uma imagem já buildada (o CLI não vê o conteúdo).');
1721
+ publicSecretScan = { scanned: false, reason: 'prebuilt_image' };
1722
+ }
1723
+ else {
1724
+ publicSecretScan = await runPublicSecretGate({ kind: 'dir', root: cwd }, { cwd, envFile: opts.envFile, stack: opts.stack, allowReason: opts.allowPublicSecret, json: opts.json });
1394
1725
  }
1726
+ const imageUrl = await resolveCloudRunImage(opts, productSlug, envName, cwd);
1395
1727
  if (!nonInteractive) {
1396
1728
  log.heading('Resumo');
1397
1729
  log.dim(` produto: ${productSlug}`);
@@ -1421,8 +1753,10 @@ async function runCloudRunDeploy(opts, productSlug, envName, cwd, nonInteractive
1421
1753
  imageUrl,
1422
1754
  ...(opts.port ? { port: opts.port } : {}),
1423
1755
  ...(envVars && Object.keys(envVars).length > 0 ? { envVars } : {}),
1756
+ ...(publicSecretScan.override ? { publicSecretOverride: publicSecretScan.override } : {}),
1424
1757
  },
1425
1758
  });
1759
+ mostrarAvisosDoCore(resp.warnings, opts.json);
1426
1760
  }
1427
1761
  catch (err) {
1428
1762
  spinner.fail('Falha ao publicar.');
@@ -1430,9 +1764,11 @@ async function runCloudRunDeploy(opts, productSlug, envName, cwd, nonInteractive
1430
1764
  }
1431
1765
  if (resp.status === 'live') {
1432
1766
  spinner.succeed(`Deploy ${chalk.bold('live')} · ${resp.deploymentId}`);
1767
+ const docsSync = await postDeployDocs(opts, cwd, envName);
1433
1768
  if (opts.json) {
1434
1769
  console.log(JSON.stringify({
1435
1770
  ok: true,
1771
+ ...(docsSync ? { docs: { sync: docsSync } } : {}),
1436
1772
  status: 'live',
1437
1773
  target: 'cloud-run',
1438
1774
  deploymentId: resp.deploymentId,
@@ -1454,9 +1790,11 @@ async function runCloudRunDeploy(opts, productSlug, envName, cwd, nonInteractive
1454
1790
  // aplicou o deploy mas ainda não confirmou saúde. Sucesso COM AVISO — não
1455
1791
  // tratamos como falha nem fingimos "Live" confirmado.
1456
1792
  spinner.warn(`Deploy aplicado — ${deployStatusLabel(resp.status)} · ${resp.deploymentId}`);
1793
+ const docsSync = await postDeployDocs(opts, cwd, envName);
1457
1794
  if (opts.json) {
1458
1795
  console.log(JSON.stringify({
1459
1796
  ok: true,
1797
+ ...(docsSync ? { docs: { sync: docsSync } } : {}),
1460
1798
  status: resp.status,
1461
1799
  verified: false,
1462
1800
  target: 'cloud-run',
@@ -1562,18 +1900,115 @@ async function checkSchemaGate(cwd, opts) {
1562
1900
  }
1563
1901
  return true;
1564
1902
  }
1903
+ /**
1904
+ * Protocolo de docs v1 (decisão do dono 2026-10-03): depois de um deploy bem
1905
+ * sucedido, publica os docs no Core SÓ se for produção e `docs.publishOnDeploy`
1906
+ * estiver ligado. Nunca lança — falha do sync não derruba o deploy.
1907
+ */
1908
+ async function postDeployDocs(opts, cwd, envName) {
1909
+ if (opts.semDocs)
1910
+ return undefined;
1911
+ try {
1912
+ const { deployDocsPostSync } = await import('./docs-produto.js');
1913
+ const r = await deployDocsPostSync(cwd, envName, opts.json);
1914
+ return r.attempted ? r : undefined;
1915
+ }
1916
+ catch {
1917
+ return undefined;
1918
+ }
1919
+ }
1565
1920
  export async function runDeploy(opts) {
1566
1921
  if (!opts.json)
1567
1922
  log.banner();
1568
1923
  const cwd = process.cwd();
1569
1924
  const nonInteractive = ensureNonInteractive(opts);
1925
+ // 2.30.0: motivo do override validado ANTES de build/upload.
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;
1570
1938
  const productSlug = await pickProduct(opts, nonInteractive, cwd);
1571
1939
  // GAP-M1-5 — gate de schema: detecta drift antes de prosseguir.
1572
1940
  const canDeploy = await checkSchemaGate(cwd, opts);
1573
1941
  if (!canDeploy) {
1574
1942
  safeExit(1);
1575
1943
  }
1576
- const target = await pickTarget(opts, nonInteractive);
1944
+ // Protocolo de docs v1 (decisão do dono 2026-10-03): `docs check` antes do
1945
+ // deploy SÓ AVISA — nunca bloqueia, em nenhum ambiente.
1946
+ let docsCheck;
1947
+ if (!opts.semDocs) {
1948
+ try {
1949
+ const { deployDocsPrecheck } = await import('./docs-produto.js');
1950
+ docsCheck = await deployDocsPrecheck(cwd, (opts.env ?? 'dev').toLowerCase(), opts.json);
1951
+ }
1952
+ catch {
1953
+ docsCheck = undefined;
1954
+ }
1955
+ }
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
+ }
1577
2012
  // Caminho workspace (Fase 1 do Fluxo de Ambientes) — sem server/tar/upload.
1578
2013
  if (target === 'workspace') {
1579
2014
  await runWorkspaceDeploy(opts, productSlug, (opts.env ?? 'dev').toLowerCase(), cwd, nonInteractive);
@@ -1581,9 +2016,21 @@ export async function runDeploy(opts) {
1581
2016
  }
1582
2017
  // Caminho cloud-run (Fase 3) — imagem de container, serviço dedicado.
1583
2018
  if (target === 'cloud-run') {
1584
- await runCloudRunDeploy(opts, productSlug, (opts.env ?? 'dev').toLowerCase(), cwd, nonInteractive);
2019
+ await runCloudRunDeploy(opts, productSlug, (opts.env ?? 'dev').toLowerCase(), cwd, nonInteractive, envFileTexto);
1585
2020
  return;
1586
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
+ }
1587
2034
  let serverChoice = null;
1588
2035
  if (target === 'vm') {
1589
2036
  serverChoice = await pickServer(opts, nonInteractive);
@@ -1594,20 +2041,27 @@ export async function runDeploy(opts) {
1594
2041
  // motivou o guard antigo (Core rejeitava sem --port); o Core agora ignora
1595
2042
  // porta em deploy VM, então exigi-la aqui só atrapalharia scripts/CI.
1596
2043
  // --port continua aceito (compat) mas é ignorado com WARN — ver abaixo.
1597
- if (target === 'vm' && opts.port && !opts.json) {
1598
- 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
+ }
1599
2061
  }
1600
2062
  const { domain } = await pickDomain(opts, nonInteractive, productSlug);
1601
- // F-11: env name pra GCS path (`gs://neetru-deploy-artifacts/<product>/<env>/…`).
1602
- // Default 'dev' quando não definido — alinhado com o middleware OAuth.
1603
- const envName = (opts.env ?? 'dev').toLowerCase();
1604
2063
  // Build / artifact (default: faz upload pra GCS via signed URL).
1605
2064
  const artifact = await resolveArtifact(opts, productSlug, envName, cwd);
1606
- // EnvVars.
1607
- let envVars;
1608
- if (opts.envFile) {
1609
- envVars = await loadEnvFile(path.resolve(cwd, opts.envFile), target, opts.json);
1610
- }
1611
2065
  // Confirmação interativa.
1612
2066
  if (!nonInteractive) {
1613
2067
  log.heading('Resumo');
@@ -1616,7 +2070,13 @@ export async function runDeploy(opts) {
1616
2070
  // no resumo — só cloud-run/workspace mostravam. `ambiente` é o parâmetro que
1617
2071
  // decide se secrets/rota de prod são tocados; tem que aparecer. Em prod,
1618
2072
  // destacado (log.warn) pra não passar batido num Enter.
1619
- 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)) {
1620
2080
  log.warn(` ambiente: ${envName} ← PRODUÇÃO`);
1621
2081
  }
1622
2082
  else {
@@ -1660,6 +2120,14 @@ export async function runDeploy(opts) {
1660
2120
  artifactSha256: artifact.artifactSha256,
1661
2121
  artifactSizeBytes: artifact.artifactSizeBytes,
1662
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
+ }
1663
2131
  if (target === 'vm') {
1664
2132
  body.serverId = serverChoice.id;
1665
2133
  // P6 (2026-07-19): NÃO enviar porta em deploy VM — é detalhe interno do
@@ -1671,6 +2139,10 @@ export async function runDeploy(opts) {
1671
2139
  body.domain = domain;
1672
2140
  if (envVars && Object.keys(envVars).length > 0)
1673
2141
  body.envVars = envVars;
2142
+ // 2.30.0: override da varredura de segredo vai NO deploy (só caminho + tipo
2143
+ // de padrão, nunca o valor). O Core lê do corpo cru (como prodDirectOverride).
2144
+ if (artifact.publicSecretScan.override)
2145
+ body.publicSecretOverride = artifact.publicSecretScan.override;
1674
2146
  const mfaHeaders = await resolveDeployStepUpHeaders(opts, envName, nonInteractive);
1675
2147
  let deploy;
1676
2148
  const dispatchSpinner = ora({ text: 'Enviando comando ao Core…', color: 'blue' }).start();
@@ -1680,6 +2152,7 @@ export async function runDeploy(opts) {
1680
2152
  headers: mfaHeaders,
1681
2153
  body,
1682
2154
  });
2155
+ mostrarAvisosDoCore(deploy.warnings, opts.json);
1683
2156
  dispatchSpinner.succeed(`Deploy criado: ${chalk.bold(deploy.deploymentId)}`);
1684
2157
  }
1685
2158
  catch (err) {
@@ -1707,6 +2180,9 @@ export async function runDeploy(opts) {
1707
2180
  }
1708
2181
  safeExit(1);
1709
2182
  }
2183
+ // Docs no Core (opt-in `docs.publishOnDeploy`, só produção). O pipeline já
2184
+ // aplicou a versão; falha aqui só vira aviso.
2185
+ const docsSync = await postDeployDocs(opts, cwd, envName);
1710
2186
  // ── Fase 0 #1: probe de saúde INDEPENDENTE antes de declarar "Live" ────────
1711
2187
  // O pipeline pode reportar `success` com o site quebrado (outage PDV
1712
2188
  // 2026-09-07). Só declaramos "Live" se um GET real ao domínio confirmar 200 +
@@ -1724,6 +2200,8 @@ export async function runDeploy(opts) {
1724
2200
  url: final.domain ? `https://${final.domain}` : null,
1725
2201
  artifactUrl: artifact.artifactUrl,
1726
2202
  steps: final.steps,
2203
+ publicSecretScan: artifact.publicSecretScan,
2204
+ ...(docsCheck || docsSync ? { docs: { check: docsCheck ?? null, sync: docsSync ?? null } } : {}),
1727
2205
  };
1728
2206
  if (opts.skipHealthProbe) {
1729
2207
  if (opts.json) {