@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
@@ -11,11 +11,12 @@
11
11
  * GET /api/cli/v1/products/{slug}/mobile-apps — role admin|operator
12
12
  * DELETE /api/cli/v1/products/{slug}/mobile-apps/{appId} — role admin ONLY + step-up MFA
13
13
  *
14
- * `redirectUri` PRECISA ser `https://` (Universal Link/App Link, RFC 8252) —
15
- * o Core rejeita custom scheme (`meuapp://`) com 400. Isso é decisão de
16
- * segurança do owner, não limitação temporária — a CLI valida o mesmo
17
- * localmente pra falhar rápido com mensagem clara, mas NUNCA oferece
18
- * custom scheme como alternativa.
14
+ * `redirectUri` PRECISA ser `https://` (Universal Link/App Link, RFC 8252)
15
+ * ou `http://localhost`/`http://127.0.0.1` (loopback de dev) — mesma regra
16
+ * do Core (`validateRedirectUris`). O Core rejeita custom scheme
17
+ * (`meuapp://`) com 400. Isso é decisão de segurança do owner, não
18
+ * limitação temporária — a CLI valida o mesmo localmente pra falhar rápido
19
+ * com mensagem clara, mas NUNCA oferece custom scheme como alternativa.
19
20
  *
20
21
  * O `id` retornado pelo Core é `<slug>__<platform>` — o app é PKCE público
21
22
  * (Authorization Code + PKCE, sem client secret).
@@ -42,6 +43,28 @@ const MOBILE_JSON_REL = '.neetru/mobile.json';
42
43
  function jsonError(message, error = 'error') {
43
44
  console.log(JSON.stringify({ ok: false, error, message }));
44
45
  }
46
+ /**
47
+ * bug_afcf958a (low): espelha EXATAMENTE a regra de `redirectUri` do Core
48
+ * (`_oidc-clients-core.ts` → `validateRedirectUris`, usada também pelo
49
+ * registro de apps mobile em `_product-mobile-apps-core.ts`): aceita
50
+ * `https://` ou `http://` com host `localhost`/`127.0.0.1` (loopback de
51
+ * dev, RFC 8252). NUNCA custom scheme (`meuapp://`) — isso o Core rejeita
52
+ * com 400, decisão de segurança do owner, e a CLI não pode ser MAIS
53
+ * permissiva que o servidor que vai validar de novo.
54
+ */
55
+ function isAcceptableMobileRedirectUri(uri) {
56
+ let parsed;
57
+ try {
58
+ parsed = new URL(uri);
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ if (parsed.protocol === 'https:')
64
+ return true;
65
+ return (parsed.protocol === 'http:' &&
66
+ (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1'));
67
+ }
45
68
  function mobileStatusColor(status) {
46
69
  switch (status) {
47
70
  case 'active': return chalk.green(status);
@@ -82,8 +105,9 @@ export async function runMobileInit(opts = {}) {
82
105
  safeExit(1);
83
106
  return;
84
107
  }
85
- if (!redirectUri.startsWith('https://')) {
86
- const msg = `--redirect-uri precisa ser https:// (Universal Link/App Link). ` +
108
+ if (!isAcceptableMobileRedirectUri(redirectUri)) {
109
+ const msg = `--redirect-uri precisa ser https:// (Universal Link/App Link) ou http://localhost ` +
110
+ `(ou http://127.0.0.1) para dev — mesma regra do Core. ` +
87
111
  `Custom scheme (ex: meuapp://) NÃO é aceito — decisão de segurança do owner (RFC 8252), ` +
88
112
  `não é limitação temporária. Recebido: "${redirectUri}"`;
89
113
  if (opts.json) {
@@ -269,7 +293,7 @@ export async function runMobileList(opts = {}) {
269
293
  return;
270
294
  }
271
295
  renderTable([
272
- { header: 'ID', value: (r) => r.id, maxWidth: 30 },
296
+ { header: 'ID', value: (r) => r.id, identificador: true },
273
297
  { header: 'Platform', value: (r) => r.platform, maxWidth: 10 },
274
298
  {
275
299
  header: 'Bundle/Package',
@@ -277,7 +301,7 @@ export async function runMobileList(opts = {}) {
277
301
  maxWidth: 30,
278
302
  },
279
303
  { header: 'Status', value: (r) => mobileStatusColor(r.status), maxWidth: 14 },
280
- { header: 'OIDC Client ID', value: (r) => r.oidcClientId, maxWidth: 34 },
304
+ { header: 'OIDC Client ID', value: (r) => r.oidcClientId, identificador: true },
281
305
  ], resp.apps);
282
306
  }
283
307
  /**
@@ -1,6 +1,10 @@
1
1
  export interface NewMacroOptions {
2
2
  productSlug?: string;
3
- env?: 'dev' | 'staging' | 'prod';
3
+ /**
4
+ * bug_7dcb68e2666a43f69ef362d2cbff94ed: era `'dev' | 'staging' | 'prod'`.
5
+ * `'dev'` não existe no enum do Core — ver `../lib/envs.ts`.
6
+ */
7
+ env?: string;
4
8
  tier?: 'dev' | 'standard' | 'enterprise';
5
9
  customer?: string;
6
10
  /** Pula scaffold local (útil quando o repo já existe). */
@@ -28,4 +32,28 @@ export interface NewMacroOptions {
28
32
  semDocs?: boolean;
29
33
  json?: boolean;
30
34
  }
35
+ /**
36
+ * Decisão PURA de qual `customerId` a macro deve usar.
37
+ *
38
+ * bug_7dcb68e2666a43f69ef362d2cbff94ed — antes a macro assumia `'self'` em
39
+ * silêncio. `'self'` é um sentinel que o Core só aceita para produto
40
+ * **Neetru-owned** (`resolveDefaultCustomer`); para qualquer outro produto ele
41
+ * recusa a criação do workspace. Como a macro cria o produto no passo [1/5] e
42
+ * o workspace no [2/5], o resultado era um produto órfão no Core e um erro
43
+ * incompreensível no meio da macro.
44
+ *
45
+ * Agora a macro exige `--customer` explícito quando o produto não é
46
+ * Neetru-owned — e diz isso, em vez de deixar o Core recusar depois.
47
+ */
48
+ export declare function decidirCustomerDaMacro(entrada: {
49
+ fromFlag?: string;
50
+ produtoEhNeetru: boolean;
51
+ slug: string;
52
+ }): {
53
+ ok: true;
54
+ customerId: string;
55
+ } | {
56
+ ok: false;
57
+ mensagem: string;
58
+ };
31
59
  export declare function runNewMacro(name: string, opts?: NewMacroOptions): Promise<void>;
@@ -27,6 +27,8 @@ import { runInit } from './init.js';
27
27
  import { runDbInit } from './db.js';
28
28
  import { requireToken } from '../lib/cli-read.js';
29
29
  import { config } from '../lib/config.js';
30
+ import { isNeetruOwnedProduct, isNeetruOwnedSlug } from '../lib/pickers.js';
31
+ import { DEFAULT_TENANT_ENVIRONMENT_ID, resolverAmbienteTenant, } from '../lib/envs.js';
30
32
  import { safeExit } from '../utils/safe-exit.js';
31
33
  /**
32
34
  * Captura erro de um passo do macro como mensagem human-readable.
@@ -140,15 +142,65 @@ function buildDashboardUrl(productId) {
140
142
  const baseUrl = String(config.get('neetruCoreUrl') ?? 'https://core.neetru.com').replace(/\/+$/, '');
141
143
  return `${baseUrl}/products/${encodeURIComponent(productId)}`;
142
144
  }
145
+ /**
146
+ * Decisão PURA de qual `customerId` a macro deve usar.
147
+ *
148
+ * bug_7dcb68e2666a43f69ef362d2cbff94ed — antes a macro assumia `'self'` em
149
+ * silêncio. `'self'` é um sentinel que o Core só aceita para produto
150
+ * **Neetru-owned** (`resolveDefaultCustomer`); para qualquer outro produto ele
151
+ * recusa a criação do workspace. Como a macro cria o produto no passo [1/5] e
152
+ * o workspace no [2/5], o resultado era um produto órfão no Core e um erro
153
+ * incompreensível no meio da macro.
154
+ *
155
+ * Agora a macro exige `--customer` explícito quando o produto não é
156
+ * Neetru-owned — e diz isso, em vez de deixar o Core recusar depois.
157
+ */
158
+ export function decidirCustomerDaMacro(entrada) {
159
+ const informado = entrada.fromFlag?.trim();
160
+ if (informado)
161
+ return { ok: true, customerId: informado };
162
+ // Produto da própria Neetru → o Core injeta o customer interno sozinho.
163
+ if (entrada.produtoEhNeetru)
164
+ return { ok: true, customerId: 'self' };
165
+ return {
166
+ ok: false,
167
+ mensagem: `O produto "${entrada.slug}" não é Neetru-owned — o Core exige um customer explícito.\n\n` +
168
+ ' Customer = a entidade comercial (PJ) que vai operar o ambiente.\n' +
169
+ ' Sem ele o Core recusa a criação do workspace, e o `neetru new` não\n' +
170
+ ' tem como adivinhar qual é.\n\n' +
171
+ ' Rode de novo informando o customer:\n' +
172
+ ` neetru new "${entrada.slug}" --customer <customerId>\n\n` +
173
+ ' Para descobrir ou criar um customerId:\n' +
174
+ ' neetru customers list\n' +
175
+ ' neetru customers create --name "..." --email "..."\n' +
176
+ ' • Painel: https://core.neetru.com/customers\n',
177
+ };
178
+ }
179
+ /**
180
+ * Descobre se o produto alvo é Neetru-owned ANTES de criar qualquer coisa.
181
+ * Se o slug já existe no Core, pergunta ao Core; se ainda não existe, só o
182
+ * espelho de slugs conhecidos pode responder.
183
+ */
184
+ async function produtoDoSlugEhNeetru(token, slug) {
185
+ if (isNeetruOwnedSlug(slug))
186
+ return true;
187
+ const existente = await lookupProductBySlug(token, slug);
188
+ if (!existente)
189
+ return false;
190
+ return isNeetruOwnedProduct(token, existente.id);
191
+ }
143
192
  export async function runNewMacro(name, opts = {}) {
144
193
  if (!name || !name.trim()) {
145
194
  log.error('Uso: neetru new <name>');
146
195
  safeExit(1);
147
196
  }
148
197
  let slug = (opts.productSlug ?? deriveSlug(name)) || deriveSlug(name);
149
- const env = opts.env ?? 'dev';
198
+ // bug_7dcb68e2666a43f69ef362d2cbff94ed: o padrão era 'dev', token que o Core
199
+ // não conhece — a macro morria no passo [2/5] com 400 validation_failed.
200
+ const env = opts.env
201
+ ? resolverAmbienteTenant(opts.env, opts.json)
202
+ : DEFAULT_TENANT_ENVIRONMENT_ID;
150
203
  const tier = opts.tier ?? 'standard';
151
- const customer = opts.customer ?? 'self';
152
204
  if (!opts.json) {
153
205
  log.banner();
154
206
  log.heading(`neetru new ${name}`);
@@ -158,6 +210,28 @@ export async function runNewMacro(name, opts = {}) {
158
210
  const summary = [];
159
211
  // ─── Autenticar (obrigatório para lookup de conflito e criação) ──────────
160
212
  const token = await requireToken(opts.json);
213
+ // ─── Customer — resolvido ANTES do passo [1/5] ───────────────────────────
214
+ // Falhar aqui evita o pior cenário do bug: produto criado no Core e macro
215
+ // abortada no passo seguinte, deixando lixo que o dev precisa limpar à mão.
216
+ const decisaoCustomer = decidirCustomerDaMacro({
217
+ fromFlag: opts.customer,
218
+ produtoEhNeetru: await produtoDoSlugEhNeetru(token, slug),
219
+ slug,
220
+ });
221
+ if (!decisaoCustomer.ok) {
222
+ if (opts.json) {
223
+ console.log(JSON.stringify({
224
+ ok: false,
225
+ error: 'customer_required',
226
+ message: decisaoCustomer.mensagem,
227
+ }));
228
+ }
229
+ else {
230
+ log.error(decisaoCustomer.mensagem);
231
+ }
232
+ safeExit(1);
233
+ }
234
+ const customer = decisaoCustomer.customerId;
161
235
  // ─── Step 1 — Criar produto (com tratamento de conflito de slug) ─────────
162
236
  let product;
163
237
  let isLinked = false;
@@ -184,14 +258,23 @@ export async function runNewMacro(name, opts = {}) {
184
258
  console.log();
185
259
  log.success(`[1/5] Vinculado ao produto existente: ${resolution.product.id} (${resolution.product.slug ?? slug})`);
186
260
  }
261
+ // Produto VINCULADO, não criado — nada aqui veio do servidor. Antes
262
+ // este objeto chutava `planTemplate: 'free'` e
263
+ // `environments: ['dev','prod']`: a mesma mentira do bug
264
+ // bug_db50b21f84e8400f9c3f6286b69c353b, agora inventada localmente
265
+ // sobre um produto preexistente que pode ter qualquer configuração.
266
+ // `null` + não-confirmado é o que de fato sabemos.
187
267
  product = {
188
268
  ok: true,
189
269
  productId: resolution.product.id,
190
270
  slug: resolution.product.slug ?? slug,
191
271
  name: resolution.product.name ?? name,
192
272
  ownerTeam: 'neetru-eng',
193
- planTemplate: 'free',
194
- environments: ['dev', 'prod'],
273
+ planTemplate: null,
274
+ environments: null,
275
+ environmentsRequested: [],
276
+ environmentsConfirmed: false,
277
+ planTemplateConfirmed: false,
195
278
  createdAt: null,
196
279
  dashboardUrl: buildDashboardUrl(resolution.product.id),
197
280
  };
@@ -284,6 +367,11 @@ export async function runNewMacro(name, opts = {}) {
284
367
  // Use `--open` para abrir explicitamente. Útil em dev local; indesejável em CI.
285
368
  const dashboardUrl = buildWorkspaceDashboardUrl(workspaceId);
286
369
  if (opts.open) {
370
+ // bug_d414f21c: este `try/catch` dá falso conforto. A falha de `spawn` por
371
+ // binário ausente (WSL/container sem `xdg-open`) chega como evento 'error'
372
+ // ASSÍNCRONO, depois que `openBrowser` já retornou — catch nenhum aqui pega.
373
+ // Quem segura isso é o listener dentro de `safeOpenUrl` (open.ts); sem ele o
374
+ // processo morria neste passo [4/5] e o passo [5/5] (`runDbInit`) nunca rodava.
287
375
  try {
288
376
  openBrowser(dashboardUrl);
289
377
  if (!opts.json)
@@ -35,20 +35,30 @@ export function safeOpenUrl(url) {
35
35
  log.warn(`URL inválida (protocolo não permitido): ${url}`);
36
36
  return;
37
37
  }
38
+ const avisarFallback = () => {
39
+ log.warn(`Não consegui abrir browser. Acesse manualmente: ${url}`);
40
+ };
38
41
  try {
39
42
  if (process.platform === 'win32') {
40
43
  // rundll32 url.dll,FileProtocolHandler não passa pelo shell — sem injeção.
44
+ // `spawnSync` falha de forma SÍNCRONA, então o `catch` abaixo já cobre.
41
45
  spawnSync('rundll32', ['url.dll,FileProtocolHandler', url], { shell: false });
42
46
  return;
43
47
  }
44
- if (process.platform === 'darwin') {
45
- spawn('open', [url], { detached: true, stdio: 'ignore', shell: false }).unref();
46
- return;
47
- }
48
- spawn('xdg-open', [url], { detached: true, stdio: 'ignore', shell: false }).unref();
48
+ // bug_d414f21c: `spawn` com binário ausente (WSL/container sem `xdg-open`)
49
+ // emite 'error' (ENOENT) de forma ASSÍNCRONA. Sem listener o Node promove
50
+ // isso a exceção não capturada e DERRUBA o processo — o `try/catch` daqui
51
+ // não pega nada, porque a falha chega depois do `return`. O 59c13b8 colocou
52
+ // o listener no `openBrowser` do login e deixou este helper (usado por
53
+ // `neetru open`, `docs open`, `workspaces open` e `new --open`) sem ele.
54
+ const filho = process.platform === 'darwin'
55
+ ? spawn('open', [url], { detached: true, stdio: 'ignore', shell: false })
56
+ : spawn('xdg-open', [url], { detached: true, stdio: 'ignore', shell: false });
57
+ filho.on('error', avisarFallback);
58
+ filho.unref();
49
59
  }
50
60
  catch (_e) {
51
- log.warn(`Não consegui abrir browser. Acesse manualmente: ${url}`);
61
+ avisarFallback();
52
62
  }
53
63
  }
54
64
  /** @deprecated Use safeOpenUrl — mantido para compatibilidade interna. */
@@ -145,7 +145,7 @@ export async function runOpsSmokeObservability(opts) {
145
145
  export async function runOpsRegToken(serverId, opts) {
146
146
  const token = await requireToken(opts.json);
147
147
  // H5 (auditoria cli/v1 2026-09-29): o token dá credencial de agente pra
148
- // QUALQUER server — o Core exige step-up MFA (sessão 1h → --mfa-token → prompt).
148
+ // QUALQUER server — o Core exige step-up MFA (--mfa-token → sessão 1h → prompt).
149
149
  const { headers } = await resolveStepUp({ mfaToken: opts.mfaToken, json: opts.json });
150
150
  const spinner = opts.json ? null : ora({ text: `Gerando registration token para ${serverId}…`, color: 'blue' }).start();
151
151
  let res;
@@ -34,6 +34,7 @@ export declare function runDbRotate(id: string | undefined, opts: {
34
34
  export declare function runDbDelete(id: string | undefined, opts: {
35
35
  json?: boolean;
36
36
  force?: boolean;
37
+ mfaToken?: string;
37
38
  }): Promise<void>;
38
39
  export declare function runDbEngines(opts: {
39
40
  json?: boolean;
@@ -23,6 +23,12 @@ import { pickDatabase, pickProductId, pickServer, promptChoice, promptText, } fr
23
23
  import { log } from '../utils/logger.js';
24
24
  // bug_863ae5: pre-flight IAM check antes de operações de provisionamento.
25
25
  import { assertIamReady } from '../lib/iam-preflight.js';
26
+ // QUINTA cópia do defeito do `--env` (bug_7dcb68e2): este comando oferecia e
27
+ // usava `'dev'` por padrão, token que o Core recusa. A fonte única do alfabeto
28
+ // de BANCO é `db-local/env.js` — NÃO `lib/envs.js`, que é o alfabeto de
29
+ // tenant/workspace/deployment e usa `'prod'` onde o banco usa `'production'`.
30
+ import { normalizeEnv, erroAmbienteBancoInvalido, NEETRU_ENVS, } from '../lib/db-local/env.js';
31
+ import { exitWithCliError } from '../lib/cli-error.js';
26
32
  function colorStatus(s) {
27
33
  if (s === 'active')
28
34
  return chalk.green(s);
@@ -62,8 +68,8 @@ export async function runDbList(opts) {
62
68
  }
63
69
  log.heading(`Bancos de dados (${res.count})`);
64
70
  renderTable([
65
- { header: 'ID', value: (r) => r.id, maxWidth: 20 },
66
- { header: 'PRODUTO', value: (r) => r.productId, maxWidth: 18 },
71
+ { header: 'ID', value: (r) => r.id, identificador: true },
72
+ { header: 'PRODUTO', value: (r) => r.productId, identificador: true },
67
73
  { header: 'LABEL', value: (r) => r.label, maxWidth: 22 },
68
74
  { header: 'ENGINE', value: (r) => r.engine, maxWidth: 20 },
69
75
  { header: 'ENV', value: (r) => r.environment },
@@ -74,6 +80,7 @@ export async function runDbCreate(opts) {
74
80
  const token = await requireToken(opts.json);
75
81
  const productId = await pickProductId(token, {
76
82
  fromFlag: opts.productId,
83
+ inputs: ['--product-id'],
77
84
  json: opts.json,
78
85
  message: 'Produto do banco:',
79
86
  });
@@ -89,15 +96,27 @@ export async function runDbCreate(opts) {
89
96
  { name: 'vm-mysql-cluster', value: 'vm-mysql-cluster' },
90
97
  { name: 'nosql-vm (MongoDB)', value: 'nosql-vm' },
91
98
  ], { default: 'firestore-instance', json: opts.json }));
92
- const environment = opts.env ??
93
- (await promptChoice('Ambiente:', [
94
- { name: 'dev', value: 'dev' },
95
- { name: 'staging', value: 'staging' },
96
- { name: 'production', value: 'production' },
97
- ], { default: 'dev', json: opts.json }));
99
+ // bug_7dcb68e2 (5ª cópia): o prompt oferecia `'dev'` E o usava como padrão.
100
+ // `_createProductDatabaseCore` (neetru-core:337) valida por igualdade estrita
101
+ // contra ['dev-local','staging','production'] — quem aceitava o padrão
102
+ // sugerido pela própria ferramenta tomava erro. `normalizeEnv` colapsa os
103
+ // apelidos (dev→dev-local, prod→production) e avisa no stderr, em vez de
104
+ // reescrever em silêncio.
105
+ let environment;
106
+ try {
107
+ environment = opts.env
108
+ ? normalizeEnv(opts.env)
109
+ : (await promptChoice('Ambiente:',
110
+ // bug_89d2497e: o que a CLI OFERECE sai da mesma lista que valida.
111
+ NEETRU_ENVS.map((v) => ({ name: v === 'dev-local' ? 'dev-local (padrão)' : v, value: v })), { default: 'dev-local', json: opts.json }));
112
+ }
113
+ catch {
114
+ return exitWithCliError(erroAmbienteBancoInvalido(opts.env), opts.json);
115
+ }
98
116
  const serverId = opts.serverId ??
99
117
  (engine.startsWith('vm-')
100
118
  ? await pickServer(token, {
119
+ inputs: ['--server-id'],
101
120
  json: opts.json,
102
121
  status: 'online',
103
122
  capacity: true,
@@ -294,11 +313,25 @@ export async function runDbDelete(id, opts) {
294
313
  json: opts.json,
295
314
  message: 'Banco para arquivar:',
296
315
  });
316
+ // bug_757b9746 (2026-10-11): `/delete` exige step-up MFA no Core
317
+ // (`requireCliStepUpMfa`), mas este comando chamava `apiRequest` SEM header
318
+ // nenhum — nem `X-Neetru-MFA-Token`, nem `X-Neetru-MFA-Session`. A sessão de
319
+ // 1h ERA gravada e ERA carregada; `admin database delete` é que nunca pedia
320
+ // por ela. O Core devolvia 401 mesmo com sessão ativa, e o hint do
321
+ // `handleApiError` mandava passar `--mfa-token` — flag que o subcomando nem
322
+ // declarava. Resultado: impossível arquivar banco pela CLI, e o anti-replay
323
+ // do Core recusava o mesmo TOTP nas tentativas seguintes.
324
+ // Mesmo hotfix que `runDbRotate` levou em 2026-08-16, pelo mesmo motivo.
325
+ const { headers: mfaHeaders } = await resolveStepUp({
326
+ mfaToken: opts.mfaToken,
327
+ json: opts.json,
328
+ });
297
329
  try {
298
330
  const res = await apiRequest(`/api/cli/v1/db/${encodeURIComponent(databaseId)}/delete`, {
299
331
  token,
300
332
  method: 'POST',
301
333
  body: { force: !!opts.force },
334
+ headers: mfaHeaders,
302
335
  });
303
336
  if (opts.json) {
304
337
  console.log(JSON.stringify(res));
@@ -39,7 +39,13 @@ export interface ProductsCreateOptions {
39
39
  description?: string;
40
40
  ownerTeam?: string;
41
41
  planTemplate?: string;
42
- /** CSV de envs (ex: "dev,prod"). */
42
+ /**
43
+ * CSV de envs (ex: "dev,prod").
44
+ *
45
+ * ATENÇÃO (bug bug_db50b21f84e8400f9c3f6286b69c353b): hoje o Core NÃO honra
46
+ * esse campo — ver `AMBIENTES_AUTO_PROVISIONADOS` abaixo. Continua aceito pra
47
+ * não quebrar script existente, mas a saída marca que o pedido foi ignorado.
48
+ */
43
49
  env?: string;
44
50
  /** Pula prompts — exige pelo menos `name`. */
45
51
  yes?: boolean;
@@ -51,8 +57,16 @@ export interface ProductCreatedResult {
51
57
  slug: string;
52
58
  name: string;
53
59
  ownerTeam: string;
54
- planTemplate: ProductPlanTemplate;
55
- environments: string[];
60
+ /** Plano-template CONFIRMADO pelo servidor. `null` quando ele não devolveu. */
61
+ planTemplate: ProductPlanTemplate | null;
62
+ /** Ambientes CONFIRMADOS pelo servidor. `null` quando ele não devolveu. */
63
+ environments: string[] | null;
64
+ /** O que o usuário pediu (`--env` ou default). Pedido ≠ gravado. */
65
+ environmentsRequested: string[];
66
+ /** `true` só quando a resposta do servidor trouxe `environments`. */
67
+ environmentsConfirmed: boolean;
68
+ /** `true` só quando a resposta do servidor trouxe `planTemplate`. */
69
+ planTemplateConfirmed: boolean;
56
70
  createdAt: string | number | null;
57
71
  dashboardUrl: string;
58
72
  }
@@ -17,6 +17,7 @@ import { requireToken, handleApiError } from '../lib/cli-read.js';
17
17
  import { printWriteResult } from '../lib/cli-write.js';
18
18
  import { renderTable } from '../lib/render.js';
19
19
  import { pickProductSlug } from '../lib/pickers.js';
20
+ import { TENANT_ENVIRONMENT_IDS, VOCABULARIOS_DE_AMBIENTE } from '../lib/envs.js';
20
21
  import { log } from '../utils/logger.js';
21
22
  import { safeExit } from '../utils/safe-exit.js';
22
23
  // ─── Erros tipados ─────────────────────────────────────────────────────────
@@ -82,7 +83,7 @@ export async function runProductsList(opts) {
82
83
  }
83
84
  log.heading(`Produtos (${res.count})`);
84
85
  renderTable([
85
- { header: 'ID', value: (r) => r.id, maxWidth: 22 },
86
+ { header: 'ID', value: (r) => r.id, identificador: true },
86
87
  { header: 'NOME', value: (r) => r.name ?? '—', maxWidth: 26 },
87
88
  { header: 'SLUG', value: (r) => r.slug ?? '—', maxWidth: 22 },
88
89
  { header: 'STATUS', value: (r) => colorStatus(r.status) },
@@ -101,6 +102,7 @@ async function runProductsPublishToggle(slug, published, opts) {
101
102
  const token = await requireToken(opts.json);
102
103
  const productSlug = await pickProductSlug(token, {
103
104
  fromFlag: slug,
105
+ inputs: ['<slug>'],
104
106
  json: opts.json,
105
107
  message: published ? 'Produto a publicar:' : 'Produto a despublicar:',
106
108
  });
@@ -128,6 +130,25 @@ export async function runProductsUnpublish(slug, opts) {
128
130
  return runProductsPublishToggle(slug, false, opts);
129
131
  }
130
132
  const VALID_PLAN_TEMPLATES = ['free', 'starter', 'pro'];
133
+ /**
134
+ * Ambientes que o Core cria sozinho ao criar o produto.
135
+ *
136
+ * Não é escolha da CLI: `_products-core.ts` (Core) chama `_ensureEnvironmentCore`
137
+ * pra 'staging' e 'prod' logo depois de gravar o doc do produto, e deixa
138
+ * 'dev-local' de fora de propósito (é local, não existe no servidor).
139
+ * Serve só pra EXPLICAR por que o `--env` do usuário não valeu — não é promessa
140
+ * de que os dois ambientes existem, porque lá a criação é best-effort.
141
+ *
142
+ * P105: derivado do alfabeto do Core (todos menos o local) em vez de escrito à mão.
143
+ */
144
+ const AMBIENTES_AUTO_PROVISIONADOS = TENANT_ENVIRONMENT_IDS.filter((e) => e !== 'dev-local');
145
+ /**
146
+ * O `environments` que a CLI manda quando o `--env` não vem. O Core IGNORA o
147
+ * campo (ver acima); o valor de sempre (`dev`, `prod`) é o vocabulário do
148
+ * `--env` do deploy para `dev-local` e `prod`, e agora sai da tabela
149
+ * `VOCABULARIOS_DE_AMBIENTE` em vez de uma lista à mão (P105) — mesmo payload.
150
+ */
151
+ const AMBIENTES_PEDIDOS_POR_PADRAO = TENANT_ENVIRONMENT_IDS.filter((e) => e !== 'staging').map((id) => VOCABULARIOS_DE_AMBIENTE[id].deploy);
131
152
  /**
132
153
  * Deriva um slug `kebab-case` a partir do `name`. Mantém apenas [a-z0-9-]
133
154
  * + colapsa hífens + corta hífens nas pontas. Não verifica unicidade — isso é
@@ -164,7 +185,9 @@ export async function runProductsCreate(opts) {
164
185
  let description = opts.description ?? '';
165
186
  let ownerTeam = opts.ownerTeam?.trim() || 'neetru-eng';
166
187
  let planTemplate = (opts.planTemplate ?? 'free').toLowerCase();
167
- let environments = parseEnvCsv(opts.env, ['dev', 'prod']);
188
+ // Só o que o usuário PEDIU. O nome deixa explícito que isso nunca é resposta
189
+ // do servidor — ver bug bug_db50b21f84e8400f9c3f6286b69c353b.
190
+ const environmentsRequested = parseEnvCsv(opts.env, [...AMBIENTES_PEDIDOS_POR_PADRAO]);
168
191
  const interactive = !opts.yes && !opts.json && process.stdin.isTTY;
169
192
  if (interactive) {
170
193
  const answers = await inquirer.prompt([
@@ -204,13 +227,11 @@ export async function runProductsCreate(opts) {
204
227
  choices: VALID_PLAN_TEMPLATES,
205
228
  default: 'free',
206
229
  },
207
- {
208
- type: 'input',
209
- name: 'env',
210
- message: 'Ambientes (csv):',
211
- when: () => !opts.env,
212
- default: environments.join(','),
213
- },
230
+ // Não existe mais pergunta de "Ambientes (csv)" aqui.
231
+ // Motivo (bug bug_db50b21f84e8400f9c3f6286b69c353b): a resposta era
232
+ // descartada pelo servidor e ainda assim impressa como se tivesse valido.
233
+ // Perguntar algo que não é honrado é pior que não perguntar — o Core
234
+ // auto-provisiona staging+prod sozinho.
214
235
  ]);
215
236
  if (answers.name)
216
237
  name = answers.name.trim();
@@ -222,8 +243,6 @@ export async function runProductsCreate(opts) {
222
243
  ownerTeam = answers.ownerTeam.trim();
223
244
  if (answers.planTemplate)
224
245
  planTemplate = answers.planTemplate;
225
- if (answers.env)
226
- environments = parseEnvCsv(answers.env, environments);
227
246
  }
228
247
  // Validação final
229
248
  if (!name) {
@@ -271,8 +290,13 @@ export async function runProductsCreate(opts) {
271
290
  slug,
272
291
  description: description || undefined,
273
292
  ownerTeam,
293
+ // `planTemplate` e `environments` continuam no corpo de propósito: o dia
294
+ // que o Core declarar esses campos no `cliProductCreateSchema` e
295
+ // devolvê-los na resposta, a CLI passa a reportar o valor REAL sem novo
296
+ // deploy. Enquanto isso o zod do Core os descarta em silêncio — por isso
297
+ // nada abaixo trata o que mandamos como se tivesse sido gravado.
274
298
  planTemplate,
275
- environments,
299
+ environments: environmentsRequested,
276
300
  },
277
301
  });
278
302
  spinner?.stop();
@@ -292,14 +316,30 @@ export async function runProductsCreate(opts) {
292
316
  handleApiError(error, opts.json);
293
317
  }
294
318
  const productId = res.productId ?? res.id ?? slug;
319
+ // BUG bug_db50b21f84e8400f9c3f6286b69c353b — POR QUE isto deixou de ser
320
+ // `res.environments ?? environments`:
321
+ // o POST /api/cli/v1/products do Core valida o corpo com `cliProductCreateSchema`,
322
+ // que NÃO declara `environments` nem `planTemplate`; o zod (não-strict) descarta
323
+ // chave desconhecida em silêncio, sem 400. A resposta também não devolve esses
324
+ // campos. O fallback antigo caía no valor PEDIDO e a tabela imprimia como se
325
+ // fosse o gravado — quem usou `--env dev,prod` saía convencido de ter escolhido
326
+ // ambientes que nunca existiram. Agora só é verdade o que o servidor devolveu;
327
+ // o resto vai marcado como não confirmado, aqui e no `--json`.
328
+ const environmentsConfirmed = Array.isArray(res.environments);
329
+ const planTemplateConfirmed = typeof res.planTemplate === 'string';
330
+ // `name` e `ownerTeam` seguem com fallback porque o schema do Core ACEITA os
331
+ // dois — eles foram gravados, o servidor só não os ecoa de volta.
295
332
  const result = {
296
333
  ok: true,
297
334
  productId,
298
335
  slug: res.slug ?? slug,
299
336
  name: res.name ?? name,
300
337
  ownerTeam: res.ownerTeam ?? ownerTeam,
301
- planTemplate: res.planTemplate ?? planTemplate,
302
- environments: res.environments ?? environments,
338
+ planTemplate: planTemplateConfirmed ? res.planTemplate : null,
339
+ environments: environmentsConfirmed ? res.environments : null,
340
+ environmentsRequested,
341
+ environmentsConfirmed,
342
+ planTemplateConfirmed,
303
343
  createdAt: res.createdAt ?? null,
304
344
  dashboardUrl: buildDashboardUrl(productId),
305
345
  };
@@ -316,12 +356,40 @@ export async function runProductsCreate(opts) {
316
356
  { k: 'slug', v: chalk.cyan(result.slug) },
317
357
  { k: 'name', v: result.name },
318
358
  { k: 'ownerTeam', v: result.ownerTeam },
319
- { k: 'planTemplate', v: chalk.yellow(result.planTemplate) },
320
- { k: 'environments', v: result.environments.join(', ') },
359
+ {
360
+ k: 'planTemplate',
361
+ v: result.planTemplate
362
+ ? chalk.yellow(result.planTemplate)
363
+ : chalk.dim('— (não confirmado pelo servidor)'),
364
+ },
365
+ {
366
+ k: 'environments',
367
+ v: result.environments
368
+ ? result.environments.join(', ')
369
+ : // Curto de propósito: a coluna trunca em 60 e o recado longo some
370
+ // num `…`. A explicação inteira vai na linha abaixo da tabela.
371
+ chalk.dim(`${AMBIENTES_AUTO_PROVISIONADOS.join(', ')} (não confirmados)`),
372
+ },
321
373
  { k: 'createdAt', v: result.createdAt ? String(result.createdAt) : '—' },
322
374
  { k: 'dashboardUrl', v: chalk.blue(result.dashboardUrl) },
323
375
  ]);
324
376
  console.log();
377
+ // Estas duas linhas existem por causa do bug: o usuário precisa sair do
378
+ // comando sabendo o que NÃO foi decidido por ele.
379
+ // (Só no caminho humano: `log.warn`/`log.dim` escrevem em stdout e sujariam
380
+ // o `--json`, que já carrega `environmentsConfirmed` pra quem é máquina.)
381
+ if (!result.environmentsConfirmed) {
382
+ log.dim(`environments: o Core provisiona ${AMBIENTES_AUTO_PROVISIONADOS.join(' e ')} por conta própria ` +
383
+ `e não devolve o campo nesta resposta — confira o que existe de fato com ` +
384
+ `"neetru workspaces list --product ${result.productId}".`);
385
+ }
386
+ // Quem digitou `--env` merece saber que o pedido foi pro lixo, em vez de
387
+ // descobrir depois procurando um ambiente que nunca foi criado.
388
+ if (!result.environmentsConfirmed && opts.env) {
389
+ log.warn(`--env (${result.environmentsRequested.join(', ')}) foi IGNORADO pelo Core — ele não aceita escolha de ambientes na criação do produto.`);
390
+ }
391
+ if (!result.environmentsConfirmed)
392
+ console.log();
325
393
  log.dim(`Abra no painel: neetru workspaces open ${result.productId}`);
326
394
  return result;
327
395
  }