@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
@@ -20,6 +20,9 @@ import { requireToken, handleApiError } from './cli-read.js';
20
20
  import { CliApiError } from './api-client.js';
21
21
  import { log } from '../utils/logger.js';
22
22
  import { safeExit } from '../utils/safe-exit.js';
23
+ import { EXIT_CODES, NeetruCliError } from '../cli-kit/errors.js';
24
+ import { adviseCommand, adviseFlag, declaredFlags } from '../cli-kit/command-context.js';
25
+ import { exitWithCliError } from './cli-error.js';
23
26
  export { requireToken, handleApiError };
24
27
  function getMfaSessionFilePath() {
25
28
  return join(homedir(), '.config', 'neetru-cli', 'mfa-session.json');
@@ -89,6 +92,55 @@ export async function clearMfaSession() {
89
92
  // Ignora — arquivo já inexistente ou sem permissão.
90
93
  }
91
94
  }
95
+ /**
96
+ * uid dono de um token de sessão MFA. O Core emite
97
+ * `<uid_base64url>.<issuedAt_hex>.<hmac_sha256_hex>` (`createCliMfaSessionToken`
98
+ * em `src/lib/auth-mfa-session.ts`): o uid é a parte ASSINADA, não segredo — o
99
+ * servidor só aceita a sessão se ele bater com o uid do Bearer. `null` se o
100
+ * formato não for reconhecido.
101
+ */
102
+ export function mfaSessionOwnerUid(token) {
103
+ const parts = token.split('.');
104
+ if (parts.length !== 3 || !parts[0])
105
+ return null;
106
+ try {
107
+ const uid = Buffer.from(parts[0], 'base64url').toString('utf8');
108
+ return /^[\x21-\x7e]+$/.test(uid) ? uid : null;
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ }
114
+ /**
115
+ * Descarta a sessão MFA salva quando ela não é do `uid` que acabou de logar.
116
+ *
117
+ * A8 (2026-10-13): `neetru login` com outra conta trocava o Bearer mas deixava
118
+ * o `mfa-session.json` da conta anterior. O Core recusa sessão de outro uid,
119
+ * então TODO comando destrutivo tomava 401 `mfa_required` por até 1h (e, antes
120
+ * do conserto da precedência em `resolveStepUp`, nem `--mfa-token` salvava).
121
+ * Sessão de dono desconhecido (arquivo corrompido, formato não reconhecido)
122
+ * também sai — o servidor recusaria do mesmo jeito. Sessão do MESMO uid fica
123
+ * (re-login na mesma conta não deve exigir TOTP de novo). Retorna `true` se
124
+ * apagou.
125
+ */
126
+ export async function discardMfaSessionOfOtherUid(uid) {
127
+ const path = getMfaSessionFilePath();
128
+ if (!existsSync(path))
129
+ return false;
130
+ let owner = null;
131
+ try {
132
+ const data = JSON.parse(readFileSync(path, 'utf8'));
133
+ if (typeof data.mfaSessionToken === 'string')
134
+ owner = mfaSessionOwnerUid(data.mfaSessionToken);
135
+ }
136
+ catch {
137
+ // corrompido → dono desconhecido → descarta
138
+ }
139
+ if (owner !== null && owner === uid)
140
+ return false;
141
+ await clearMfaSession();
142
+ return true;
143
+ }
92
144
  /**
93
145
  * Pede uma confirmação interativa `y/N` no terminal. Resolve `true` se o
94
146
  * operador digitou `y`/`yes` (case-insensitive); `false` caso contrário.
@@ -108,6 +160,31 @@ function promptYesNo(question) {
108
160
  });
109
161
  });
110
162
  }
163
+ /** As flags que pulam a confirmação, na ordem em que o conselho as cita. */
164
+ const FLAGS_DE_CONFIRMACAO = ['--yes', '--force'];
165
+ /**
166
+ * O que dizer quando a confirmação não pode ser perguntada (P105 /
167
+ * bug_89d2497e). Cita só as flags de confirmação que o comando EM EXECUÇÃO
168
+ * declara: o texto fixo "use --yes (ou --force)" chegava a 8 comandos que
169
+ * declaram `--yes` mas não `--force` (medido na árvore do Commander em
170
+ * 2026-10-18: `servers pgbouncer sync-userlist`, `products memory set`,
171
+ * `vm memory set`, `db move start|abort|decommission`, `promote execute`,
172
+ * `mobile revoke`) — quem seguisse com `--force` tomava "unknown option".
173
+ * Sem nenhuma das duas (ou fora do Commander, como no menu), não cita flag.
174
+ */
175
+ export function confirmationAdvice(modo) {
176
+ const flags = declaredFlags(FLAGS_DE_CONFIRMACAO);
177
+ if (modo === 'json') {
178
+ return flags.length > 0
179
+ ? `Operação destrutiva em modo --json exige ${flags[0]}.`
180
+ : 'Operação destrutiva em modo --json exige confirmação, e este comando não tem como confirmar sem perguntar.';
181
+ }
182
+ if (flags.length === 0) {
183
+ return 'Terminal não-interativo: esta operação destrutiva só pode ser confirmada num terminal interativo.';
184
+ }
185
+ const lista = flags.length > 1 ? `${flags[0]} (ou ${flags[1]})` : flags[0];
186
+ return `Terminal não-interativo: use ${lista} para confirmar operações destrutivas.`;
187
+ }
111
188
  /**
112
189
  * Gate de confirmação pra operações DESTRUTIVAS (suspend, deactivate, delete,
113
190
  * rollback, archive, yank). Se `--yes`/`--force` foi passado, retorna sem
@@ -129,7 +206,7 @@ export async function confirmDestructive(message, opts) {
129
206
  console.log(JSON.stringify({
130
207
  ok: false,
131
208
  error: 'confirmation_required',
132
- message: 'Operação destrutiva em modo --json exige --yes.',
209
+ message: confirmationAdvice('json'),
133
210
  }));
134
211
  safeExit(1);
135
212
  }
@@ -137,7 +214,7 @@ export async function confirmDestructive(message, opts) {
137
214
  const confirmed = await promptYesNo('Confirmar?');
138
215
  if (!confirmed) {
139
216
  if (!process.stdin.isTTY) {
140
- log.error('Terminal não-interativo: use --yes (ou --force) para confirmar operações destrutivas.');
217
+ log.error(confirmationAdvice('nao-interativo'));
141
218
  }
142
219
  else {
143
220
  log.dim('Operação cancelada.');
@@ -157,21 +234,56 @@ function promptLine(question) {
157
234
  });
158
235
  });
159
236
  }
237
+ /**
238
+ * O step-up que dá pra mandar SEM interação, na ordem de precedência:
239
+ * 1. `--mfa-token` explícito → `X-Neetru-MFA-Token`;
240
+ * 2. sessão de 1h salva (`~/.config/neetru-cli/mfa-session.json`) →
241
+ * `X-Neetru-MFA-Session` (salvo `skipSession`).
242
+ * `{}` se não houver nenhum dos dois (aí o caller prompta ou falha fechado).
243
+ *
244
+ * A8 (2026-10-13): o código explícito VENCE a sessão. Antes `resolveStepUp`
245
+ * olhava a sessão primeiro e devolvia só `X-Neetru-MFA-Session`, descartando o
246
+ * `--mfa-token` em silêncio — enquanto `withStepUpRetry` fazia o contrário.
247
+ * Depois de `neetru login` com outra conta (sessão do uid anterior no disco,
248
+ * que o Core recusa), ~40 comandos destrutivos davam 401 por até 1h MESMO com
249
+ * `--mfa-token`. Quem digita o código está pedindo que ele seja usado. As duas
250
+ * funções agora decidem por aqui — uma regra só.
251
+ */
252
+ function nonInteractiveStepUp(opts) {
253
+ const code = (opts.mfaToken ?? '').trim();
254
+ const savedSession = opts.skipSession ? null : loadMfaSession();
255
+ // Código explícito + sessão salva: manda OS DOIS (revisão do #78). O Core
256
+ // (`requireCliStepUpMfa`) tenta a sessão primeiro e só usa o TOTP se ela não
257
+ // servir — e o TOTP é de USO ÚNICO (`mfa-lockout`). Mandar só o código gastava
258
+ // o código à toa quando a sessão era válida, e o 2º comando com o mesmo código
259
+ // (agente com o código ditado, script) falhava com mfa_invalid. Com os dois, a
260
+ // sessão de OUTRA conta (o caso do A8) cai e o código ainda salva o pedido.
261
+ if (code) {
262
+ return savedSession
263
+ ? { 'X-Neetru-MFA-Token': code, 'X-Neetru-MFA-Session': savedSession }
264
+ : { 'X-Neetru-MFA-Token': code };
265
+ }
266
+ if (savedSession)
267
+ return { 'X-Neetru-MFA-Session': savedSession };
268
+ return {};
269
+ }
160
270
  /**
161
271
  * Resolve o que uma operação destrutiva TOP-TIER precisa mandar pro servidor:
162
- * - `headers`: `X-Neetru-MFA-Session` (sessão de 1h) OU `X-Neetru-MFA-Token`
163
- * (TOTP fresco) e/ou `X-Neetru-Dry-Run`.
272
+ * - `headers`: `X-Neetru-MFA-Token` (TOTP fresco) OU `X-Neetru-MFA-Session`
273
+ * (sessão de 1h) e/ou `X-Neetru-Dry-Run`.
164
274
  *
165
275
  * Ordem de preferência:
166
276
  * 1. dry-run: sem MFA, só `X-Neetru-Dry-Run: true`.
167
- * 2. Sessão de 1h salva localmente (`~/.config/neetru-cli/mfa-session.json`):
168
- * usa `X-Neetru-MFA-Session` sem pedir TOTP. Silencioso — o operador
169
- * não precisa saber que a sessão está ativa.
170
- * 3. TOTP fresco: do flag `--mfa-token` ou prompt interativo.
171
- * Em terminal não-interativo sem `--mfa-token`, encerra com instrução
172
- * clara — o servidor recusaria de qualquer forma (fail-closed).
277
+ * 2. `--mfa-token` explícito: manda `X-Neetru-MFA-Token` — e também a sessão
278
+ * salva, se houver (o Core tenta a sessão antes e não gasta o TOTP à toa).
279
+ * O código NUNCA é descartado em favor da sessão (A8).
280
+ * 3. Sessão de 1h salva localmente: usa `X-Neetru-MFA-Session` sem pedir
281
+ * TOTP. Silencioso — o operador não precisa saber que a sessão está ativa.
282
+ * 4. Prompt interativo do TOTP. Em `--json`/não-interativo sem código nem
283
+ * sessão, encerra com instrução clara — o servidor recusaria de qualquer
284
+ * forma (fail-closed).
173
285
  *
174
- * feature mfa-session (2026-06-12): o item 2 elimina o re-prompt TOTP a cada
286
+ * feature mfa-session (2026-06-12): o item 3 elimina o re-prompt TOTP a cada
175
287
  * comando destrutivo quando o operador já rodou `neetru auth mfa-session`.
176
288
  */
177
289
  export async function resolveStepUp(opts) {
@@ -180,34 +292,35 @@ export async function resolveStepUp(opts) {
180
292
  headers['X-Neetru-Dry-Run'] = 'true';
181
293
  return { headers };
182
294
  }
183
- // ── Prioridade 2: sessão de 1h salva localmente ─────────────────────────
184
- // Verifica ANTES de pedir TOTP — se há sessão válida, usa sem interação.
185
- const savedSession = opts.skipSession ? null : loadMfaSession();
186
- if (savedSession) {
187
- headers['X-Neetru-MFA-Session'] = savedSession;
188
- return { headers };
189
- }
190
- // ── Prioridade 3: TOTP fresco (flag ou prompt) ─────────────────────────
191
- let code = (opts.mfaToken ?? '').trim();
192
- if (!code) {
193
- // bug auditoria 2026-06-30: em --json o prompt interativo de TOTP NÃO pode
194
- // rodar NEM com TTY — ele escreveria a pergunta no stdout (corrompe o JSON)
195
- // e penduraria um consumidor programático. Fail-CLOSED em --json OU sem TTY;
196
- // só promptar em terminal interativo humano (não-json + TTY).
197
- if (opts.json || opts.nonInteractive || !process.stdin.isTTY) {
198
- const msg = 'Operação destrutiva top-tier exige step-up MFA: passe --mfa-token <código TOTP> ' +
199
- '(modo --json/não-interativo) ou rode `neetru auth mfa-session` para obter uma sessão de 1h.';
200
- if (opts.json) {
201
- console.log(JSON.stringify({ ok: false, error: 'mfa_required', message: msg }));
202
- }
203
- else {
204
- log.error(msg);
205
- }
206
- safeExit(1);
207
- return { headers }; // safeExit não retorna em prod; satisfaz o tipo + testes
208
- }
209
- code = await promptLine('Código MFA (TOTP) para confirmar:');
295
+ // ── Prioridades 2 e 3: código explícito, depois sessão de 1h ────────────
296
+ const ready = nonInteractiveStepUp(opts);
297
+ if (Object.keys(ready).length > 0)
298
+ return { headers: ready };
299
+ // ── Prioridade 4: prompt do TOTP ─────────────────────────────────────────
300
+ // bug auditoria 2026-06-30: em --json o prompt interativo de TOTP NÃO pode
301
+ // rodar NEM com TTY — ele escreveria a pergunta no stdout (corrompe o JSON)
302
+ // e penduraria um consumidor programático. Fail-CLOSED em --json OU sem TTY;
303
+ // só promptar em terminal interativo humano (não-json + TTY).
304
+ if (opts.json || opts.nonInteractive || !process.stdin.isTTY) {
305
+ // bug_89d2497e: o conselho só cita `--mfa-token` se o comando em execução
306
+ // declara a flag (mesmo defeito do bug_757b9746, do lado do step-up). O
307
+ // código de saída segue 1 e o JSON segue `{ ok, error:'mfa_required', message }`.
308
+ // P105: o comando alternativo (`neetru auth mfa-session`) também só é
309
+ // citado se existe na árvore do Commander.
310
+ const sessao = (withCommand, withoutCommand) => adviseCommand({ command: 'auth mfa-session', withCommand, withoutCommand });
311
+ const msg = adviseFlag({
312
+ flag: '--mfa-token',
313
+ withFlag: sessao('Operação destrutiva top-tier exige step-up MFA: passe --mfa-token <código TOTP> ' +
314
+ '(modo --json/não-interativo) ou rode `neetru auth mfa-session` para obter uma sessão de 1h.', 'Operação destrutiva top-tier exige step-up MFA: passe --mfa-token <código TOTP> ' +
315
+ '(modo --json/não-interativo).'),
316
+ withoutFlag: sessao('Operação destrutiva top-tier exige step-up MFA: rode `neetru auth mfa-session` ' +
317
+ 'para obter uma sessão de 1h e repita o comando.', 'Operação destrutiva top-tier exige step-up MFA, e esta CLI não tem, neste comando, ' +
318
+ 'como enviá-lo em modo --json/não-interativo.'),
319
+ });
320
+ exitWithCliError(new NeetruCliError(msg, { code: 'mfa_required', exit: EXIT_CODES.client }), opts.json);
321
+ return { headers }; // safeExit não retorna em prod; satisfaz o tipo + testes
210
322
  }
323
+ const code = await promptLine('Código MFA (TOTP) para confirmar:');
211
324
  if (!code) {
212
325
  log.error('Código MFA não fornecido — operação cancelada.');
213
326
  safeExit(1);
@@ -237,13 +350,9 @@ export function isMfaRequiredError(error) {
237
350
  export async function withStepUpRetry(opts, doRequest, onError,
238
351
  /** Chamado antes do prompt TOTP do retry (ex.: parar o spinner). */
239
352
  beforePrompt) {
240
- const first = {};
241
- const code = (opts.mfaToken ?? '').trim();
242
- const savedSession = code ? null : loadMfaSession();
243
- if (code)
244
- first['X-Neetru-MFA-Token'] = code;
245
- else if (savedSession)
246
- first['X-Neetru-MFA-Session'] = savedSession;
353
+ // Mesma precedência do `resolveStepUp` (A8): código explícito, depois sessão.
354
+ const first = nonInteractiveStepUp(opts);
355
+ const code = first['X-Neetru-MFA-Token'];
247
356
  let firstError;
248
357
  try {
249
358
  return await doRequest(first);
@@ -114,6 +114,10 @@ export declare const neetruConfigSchema: z.ZodObject<{
114
114
  slug: string;
115
115
  runtime: "nextjs" | "node-api";
116
116
  $schema?: string | undefined;
117
+ deploy?: z.objectOutputType<{
118
+ region: z.ZodOptional<z.ZodString>;
119
+ serviceName: z.ZodOptional<z.ZodString>;
120
+ }, z.ZodTypeAny, "passthrough"> | undefined;
117
121
  tenantId?: string | undefined;
118
122
  docs?: z.objectOutputType<{
119
123
  protocolVersion: z.ZodOptional<z.ZodNumber>;
@@ -131,10 +135,6 @@ export declare const neetruConfigSchema: z.ZodObject<{
131
135
  ctaHref?: string | undefined;
132
136
  ctaLabel?: string | undefined;
133
137
  } | undefined;
134
- deploy?: z.objectOutputType<{
135
- region: z.ZodOptional<z.ZodString>;
136
- serviceName: z.ZodOptional<z.ZodString>;
137
- }, z.ZodTypeAny, "passthrough"> | undefined;
138
138
  database?: {
139
139
  engine?: "firestore-instance" | "vm-postgres-single" | "vm-mysql-single" | "nosql-vm" | undefined;
140
140
  dbId?: string | undefined;
@@ -144,6 +144,10 @@ export declare const neetruConfigSchema: z.ZodObject<{
144
144
  slug: string;
145
145
  runtime: "nextjs" | "node-api";
146
146
  $schema?: string | undefined;
147
+ deploy?: z.objectInputType<{
148
+ region: z.ZodOptional<z.ZodString>;
149
+ serviceName: z.ZodOptional<z.ZodString>;
150
+ }, z.ZodTypeAny, "passthrough"> | undefined;
147
151
  tenantId?: string | undefined;
148
152
  docs?: z.objectInputType<{
149
153
  protocolVersion: z.ZodOptional<z.ZodNumber>;
@@ -161,10 +165,6 @@ export declare const neetruConfigSchema: z.ZodObject<{
161
165
  ctaHref?: string | undefined;
162
166
  ctaLabel?: string | undefined;
163
167
  } | undefined;
164
- deploy?: z.objectInputType<{
165
- region: z.ZodOptional<z.ZodString>;
166
- serviceName: z.ZodOptional<z.ZodString>;
167
- }, z.ZodTypeAny, "passthrough"> | undefined;
168
168
  database?: {
169
169
  engine?: "firestore-instance" | "vm-postgres-single" | "vm-mysql-single" | "nosql-vm" | undefined;
170
170
  dbId?: string | undefined;
@@ -1,4 +1,4 @@
1
- import type { NeetruEnv } from './env.js';
1
+ import { type NeetruEnv } from './env.js';
2
2
  /** Mapa de IDs de banco por ambiente canônico. */
3
3
  export type DbJsonIds = Partial<Record<NeetruEnv, string>>;
4
4
  /** Uma entrada de banco lógico no registry. */
@@ -11,6 +11,8 @@
11
11
  import * as fs from 'node:fs';
12
12
  import * as path from 'node:path';
13
13
  import { randomBytes } from 'node:crypto';
14
+ import { avisarSobraDeTemporario, removerArquivo } from '../fs-seguro.js';
15
+ import { NEETRU_ENVS } from './env.js';
14
16
  /** Registry v2 vazio — usado quando o arquivo ainda não existe. */
15
17
  function emptyDbJson() {
16
18
  return { version: 2, databases: [] };
@@ -18,7 +20,8 @@ function emptyDbJson() {
18
20
  // ---------------------------------------------------------------------------
19
21
  // migrateDbJson — qualquer entrada → v2 canônico
20
22
  // ---------------------------------------------------------------------------
21
- const VALID_ENV_KEYS = ['dev-local', 'staging', 'production'];
23
+ // P105: as chaves de `ids` são o alfabeto de banco — a mesma lista que valida.
24
+ const VALID_ENV_KEYS = NEETRU_ENVS;
22
25
  function isPlainObject(value) {
23
26
  return typeof value === 'object' && value !== null && !Array.isArray(value);
24
27
  }
@@ -144,12 +147,17 @@ export function writeDbJson(filePath, data) {
144
147
  fs.renameSync(tmpPath, filePath);
145
148
  }
146
149
  catch (err) {
147
- // Best-effort: remove o temp pra não deixar lixo se algo falhou.
150
+ // Best-effort: remove o temp pra não deixar lixo se algo falhou. Não usa
151
+ // `fs.rmSync`: com acento no caminho (`C:\Users\João\...`), no Node 24 do
152
+ // Windows ele volta sem erro e não apaga (ver `lib/fs-seguro.ts`).
153
+ // `removerArquivo` apaga e confere; se ainda assim falhar, o erro que
154
+ // importa é o original, que segue abaixo — e a sobra vira aviso no stderr
155
+ // com o caminho, em vez de sumir calada (revisão do #79).
148
156
  try {
149
- fs.rmSync(tmpPath, { force: true });
157
+ removerArquivo(tmpPath);
150
158
  }
151
- catch {
152
- /* ignora — limpeza best-effort */
159
+ catch (limpeza) {
160
+ avisarSobraDeTemporario(tmpPath, limpeza);
153
161
  }
154
162
  throw err;
155
163
  }
@@ -23,8 +23,20 @@
23
23
  * Ref: glossário 0.1.4 exporta normalizeEnvId() que usa 'prod' — use-o para
24
24
  * queries inter-sistema quando disponível.
25
25
  */
26
- /** Os 3 ambientes canônicos persistidos no `.neetru/db.json`. */
27
- export type NeetruEnv = 'dev-local' | 'staging' | 'production';
26
+ import type { NeetruCliError } from '../../cli-kit/errors.js';
27
+ /**
28
+ * Os 3 ambientes canônicos persistidos no `.neetru/db.json`. Fonte única: a
29
+ * validação e a lista de "valores válidos" das mensagens saem daqui
30
+ * (bug_89d2497e — antes a lista era copiada à mão em 5 mensagens).
31
+ */
32
+ export declare const NEETRU_ENVS: readonly ["dev-local", "staging", "production"];
33
+ export type NeetruEnv = (typeof NEETRU_ENVS)[number];
34
+ /**
35
+ * Erro de `--env` inválido nos comandos de banco (bug_89d2497e): os valores
36
+ * válidos saem de `NEETRU_ENVS` e os apelidos de `ENV_ALIASES` — as mesmas
37
+ * fontes que validam. Sai como erro do cliente (exit 1), código `invalid_env`.
38
+ */
39
+ export declare function erroAmbienteBancoInvalido(input: unknown): NeetruCliError;
28
40
  /**
29
41
  * Normaliza um nome de ambiente para um dos 3 valores canônicos.
30
42
  *
@@ -32,6 +44,6 @@ export type NeetruEnv = 'dev-local' | 'staging' | 'production';
32
44
  * no stderr listando o valor canônico. Nunca normaliza silenciosamente.
33
45
  *
34
46
  * Faz lowercase + trim antes do lookup. Entrada não-string, vazia ou
35
- * desconhecida lança um `Error` que lista os valores aceitos.
47
+ * desconhecida lança o `NeetruCliError` de `erroAmbienteBancoInvalido`.
36
48
  */
37
49
  export declare function normalizeEnv(input: string): NeetruEnv;
@@ -23,6 +23,13 @@
23
23
  * Ref: glossário 0.1.4 exporta normalizeEnvId() que usa 'prod' — use-o para
24
24
  * queries inter-sistema quando disponível.
25
25
  */
26
+ import { invalidChoiceError } from '../../cli-kit/choices.js';
27
+ /**
28
+ * Os 3 ambientes canônicos persistidos no `.neetru/db.json`. Fonte única: a
29
+ * validação e a lista de "valores válidos" das mensagens saem daqui
30
+ * (bug_89d2497e — antes a lista era copiada à mão em 5 mensagens).
31
+ */
32
+ export const NEETRU_ENVS = ['dev-local', 'staging', 'production'];
26
33
  /** Mapa de aliases aceitos → valor canônico. */
27
34
  const ENV_ALIASES = {
28
35
  dev: 'dev-local',
@@ -43,6 +50,31 @@ const ALIAS_NOTICE = {
43
50
  stg: 'staging',
44
51
  prod: 'production',
45
52
  };
53
+ /** `dev/local → dev-local, stg → staging, prod → production` — de `ENV_ALIASES`. */
54
+ function apelidosLegiveis() {
55
+ const porDestino = new Map();
56
+ for (const [apelido, destino] of Object.entries(ENV_ALIASES)) {
57
+ if (apelido === destino)
58
+ continue;
59
+ porDestino.set(destino, [...(porDestino.get(destino) ?? []), apelido]);
60
+ }
61
+ return [...porDestino].map(([destino, apelidos]) => `${apelidos.join('/')} → ${destino}`).join(', ');
62
+ }
63
+ /**
64
+ * Erro de `--env` inválido nos comandos de banco (bug_89d2497e): os valores
65
+ * válidos saem de `NEETRU_ENVS` e os apelidos de `ENV_ALIASES` — as mesmas
66
+ * fontes que validam. Sai como erro do cliente (exit 1), código `invalid_env`.
67
+ */
68
+ export function erroAmbienteBancoInvalido(input) {
69
+ return invalidChoiceError({
70
+ flag: '--env',
71
+ value: typeof input === 'string' ? input : `(${typeof input})`,
72
+ valid: NEETRU_ENVS,
73
+ subject: 'Ambiente',
74
+ code: 'invalid_env',
75
+ hint: `Apelidos aceitos: ${apelidosLegiveis()}.`,
76
+ });
77
+ }
46
78
  /**
47
79
  * Normaliza um nome de ambiente para um dos 3 valores canônicos.
48
80
  *
@@ -50,20 +82,15 @@ const ALIAS_NOTICE = {
50
82
  * no stderr listando o valor canônico. Nunca normaliza silenciosamente.
51
83
  *
52
84
  * Faz lowercase + trim antes do lookup. Entrada não-string, vazia ou
53
- * desconhecida lança um `Error` que lista os valores aceitos.
85
+ * desconhecida lança o `NeetruCliError` de `erroAmbienteBancoInvalido`.
54
86
  */
55
87
  export function normalizeEnv(input) {
56
- if (typeof input !== 'string') {
57
- throw new Error(`normalizeEnv: ambiente inválido (esperava string, recebeu ${typeof input}). ` +
58
- `Valores aceitos: dev-local, staging, production.`);
59
- }
88
+ if (typeof input !== 'string')
89
+ throw erroAmbienteBancoInvalido(input);
60
90
  const key = input.trim().toLowerCase();
61
91
  const canonical = ENV_ALIASES[key];
62
- if (!canonical) {
63
- throw new Error(`normalizeEnv: ambiente "${input}" não reconhecido. ` +
64
- `Valores aceitos: dev-local, staging, production. ` +
65
- `Aliases aceitos: dev/local → dev-local, stg → staging, prod → production.`);
66
- }
92
+ if (!canonical)
93
+ throw erroAmbienteBancoInvalido(input);
67
94
  // D-4: aviso explícito quando alias não-canônico é usado
68
95
  const notice = ALIAS_NOTICE[key];
69
96
  if (notice) {
@@ -1,6 +1,10 @@
1
1
  import { type RehearseEngine } from './rehearse.js';
2
2
  import type { PipelineDeps } from './types.js';
3
- export declare function generateSql(schemaPath: string, engine?: RehearseEngine): Promise<string>;
3
+ export interface OpcoesGenerateSql {
4
+ /** Remove o `--out` temporário. Injetável só para teste. */
5
+ remover?: (dir: string) => Promise<void>;
6
+ }
7
+ export declare function generateSql(schemaPath: string, engine?: RehearseEngine, { remover }?: OpcoesGenerateSql): Promise<string>;
4
8
  /**
5
9
  * Constrói o conjunto de `PipelineDeps` de PRODUÇÃO.
6
10
  *
@@ -25,6 +25,7 @@ import * as path from 'node:path';
25
25
  import { spawn } from 'node:child_process';
26
26
  import { classifyMigration } from '@neetru/db-classifier';
27
27
  import { apiRequest } from '../api-client.js';
28
+ import { avisarSobraDeTemporario, removerDiretorio } from '../fs-seguro.js';
28
29
  import { buildRehearseInContainer, buildDockerExecFn, } from './rehearse.js';
29
30
  /**
30
31
  * Prova em COMPILE-TIME que `classifyMigration` do `@neetru/db-classifier` é
@@ -273,7 +274,7 @@ function makeTableAndIndexIdempotent(sql) {
273
274
  .replace(/CREATE TABLE (?!IF NOT EXISTS\b)/g, 'CREATE TABLE IF NOT EXISTS ')
274
275
  .replace(/CREATE (UNIQUE )?INDEX (?!IF NOT EXISTS\b)/g, (_match, unique) => `CREATE ${unique ?? ''}INDEX IF NOT EXISTS `);
275
276
  }
276
- export function generateSql(schemaPath, engine = 'postgres') {
277
+ export function generateSql(schemaPath, engine = 'postgres', { remover = removerDiretorio } = {}) {
277
278
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'neetru-drizzle-'));
278
279
  return new Promise((resolve, reject) => {
279
280
  const child = spawn('npx', [
@@ -333,8 +334,27 @@ export function generateSql(schemaPath, engine = 'postgres') {
333
334
  reject(new Error(`falha lendo o .sql gerado em ${tmpDir}: ${err.message}`));
334
335
  }
335
336
  });
336
- }).finally(() => {
337
- fs.rmSync(tmpDir, { recursive: true, force: true });
337
+ }).then(
338
+ // Limpa o --out temporário. Não usa `fs.rmSync`: o `os.tmpdir()` de quem tem
339
+ // acento na home (`C:\Users\João\AppData\Local\Temp`) faz o rmSync do
340
+ // Node 24 no Windows voltar sem erro e não apagar nada — um `neetru-drizzle-*`
341
+ // perdido por geração. `removerDiretorio` apaga e confere (ver `lib/fs-seguro.ts`).
342
+ //
343
+ // A falha da limpeza nunca derruba o resultado nem esconde o erro da
344
+ // geração, mas também não some calada: vira aviso no stderr com o caminho
345
+ // que sobrou (revisão do #79).
346
+ async (sql) => {
347
+ try {
348
+ await remover(tmpDir);
349
+ }
350
+ catch (limpeza) {
351
+ avisarSobraDeTemporario(tmpDir, limpeza);
352
+ }
353
+ return sql;
354
+ }, async (err) => {
355
+ // O erro da geração é o que importa: falha na limpeza não pode escondê-lo.
356
+ await remover(tmpDir).catch((limpeza) => avisarSobraDeTemporario(tmpDir, limpeza));
357
+ throw err;
338
358
  });
339
359
  }
340
360
  /**
@@ -16,12 +16,16 @@
16
16
  * Postgres e sem rede. A fiação real (drizzle-kit / container / HTTP) vive
17
17
  * no `buildPipelineDeps()`.
18
18
  */
19
+ import type { NeetruEnv } from '../db-local/env.js';
19
20
  /** As 5 fases do pipeline, na ordem de execução. */
20
21
  export type PipelinePhase = 'fingerprint' | 'generate' | 'rehearse' | 'classify' | 'push';
21
22
  /** Severidade global da migração — espelha o `@neetru/db-classifier`. */
22
23
  export type MigrationSeverity = 'aditiva' | 'destrutiva';
23
- /** Os 3 ambientes canônicos do `neetru db apply`. */
24
- export type ApplyEnv = 'dev-local' | 'staging' | 'production';
24
+ /**
25
+ * Os 3 ambientes canônicos do `neetru db apply` — o alfabeto de BANCO
26
+ * (`NEETRU_ENVS`, `db-local/env.ts`). P105: era a união escrita à mão.
27
+ */
28
+ export type ApplyEnv = NeetruEnv;
25
29
  /**
26
30
  * Relatório de classificação — subconjunto estrutural do `ClassificationReport`
27
31
  * do `@neetru/db-classifier`. Tipado localmente pra que o orquestrador e seus
@@ -1,20 +1,2 @@
1
- /**
2
- * `@neetru/db-pipeline` — contratos do orquestrador do `neetru db apply`.
3
- *
4
- * O pipeline roda CLIENT-SIDE as fases 1-5 do `neetru db apply`:
5
- * 1. `fingerprint` — hash determinístico do schema.
6
- * 2. `generate` — `drizzle-kit generate` produz o `.sql`.
7
- * 3. `rehearse` — ensaia o `.sql` num Postgres efêmero real.
8
- * 4. `classify` — `@neetru/db-classifier` marca aditiva/destrutiva.
9
- * 5. `push` — adquire lock + envia o artefato validado pro Core.
10
- *
11
- * O pipeline NÃO decide política — ele produz um `.sql` validado e
12
- * classificado e o envia. A decisão de aplicar (ou exigir confirmação numa
13
- * migração destrutiva) é do Core.
14
- *
15
- * Tudo aqui é deps-injetado: o orquestrador é unit-testável sem Docker, sem
16
- * Postgres e sem rede. A fiação real (drizzle-kit / container / HTTP) vive
17
- * no `buildPipelineDeps()`.
18
- */
19
1
  export {};
20
2
  //# sourceMappingURL=types.js.map