@neetru/cli 2.32.1 → 2.34.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.
- package/CHANGELOG.md +78 -0
- package/README.md +38 -3
- package/dist/commands/admin/sdk-credential.js +3 -1
- package/dist/commands/ai-models.d.ts +2 -1
- package/dist/commands/ai-models.js +4 -5
- package/dist/commands/ai.d.ts +12 -2
- package/dist/commands/ai.js +47 -38
- package/dist/commands/auth.d.ts +3 -2
- package/dist/commands/auth.js +96 -57
- package/dist/commands/build.js +35 -4
- package/dist/commands/chat.d.ts +24 -6
- package/dist/commands/chat.js +43 -7
- package/dist/commands/code.js +11 -2
- package/dist/commands/config.js +15 -13
- package/dist/commands/db-frontdoor.d.ts +116 -0
- package/dist/commands/db-frontdoor.js +572 -0
- package/dist/commands/db-move.d.ts +5 -0
- package/dist/commands/db-move.js +27 -0
- package/dist/commands/db.d.ts +8 -2
- package/dist/commands/db.js +13 -6
- package/dist/commands/deploy.d.ts +14 -5
- package/dist/commands/deploy.js +376 -55
- package/dist/commands/dev-context.js +1 -1
- package/dist/commands/environments-repoint.d.ts +49 -0
- package/dist/commands/environments-repoint.js +219 -0
- package/dist/commands/init.js +7 -1
- package/dist/commands/new.js +14 -0
- package/dist/commands/secrets.d.ts +6 -4
- package/dist/commands/secrets.js +14 -7
- package/dist/commands/servers.d.ts +29 -0
- package/dist/commands/servers.js +108 -1
- package/dist/commands/ui.js +1 -1
- package/dist/commands/workspaces.js +16 -1
- package/dist/index.js +103 -7
- package/dist/lib/ai/auth-discovery.d.ts +9 -12
- package/dist/lib/ai/auth-discovery.js +43 -145
- package/dist/lib/ai/orchestrator.d.ts +13 -6
- package/dist/lib/ai/orchestrator.js +46 -246
- package/dist/lib/ai/provedores.d.ts +57 -0
- package/dist/lib/ai/provedores.js +66 -0
- package/dist/lib/api-client.d.ts +8 -1
- package/dist/lib/api-client.js +18 -2
- package/dist/lib/build-targets.d.ts +48 -10
- package/dist/lib/build-targets.js +64 -10
- package/dist/lib/cli-write.js +6 -3
- package/dist/lib/code-agent-gateway.d.ts +2 -2
- package/dist/lib/code-agent-gateway.js +2 -2
- package/dist/lib/code-agent-settings.d.ts +2 -2
- package/dist/lib/code-agent-settings.js +2 -4
- package/dist/lib/code-agent-types.d.ts +3 -1
- package/dist/lib/config.d.ts +4 -7
- package/dist/lib/config.js +9 -6
- package/dist/lib/deploy-recusa-de-destino.d.ts +9 -0
- package/dist/lib/deploy-recusa-de-destino.js +98 -0
- package/dist/lib/deploy-render-probe.d.ts +117 -0
- package/dist/lib/deploy-render-probe.js +475 -0
- package/dist/lib/deploy-vm-um-app.d.ts +62 -0
- package/dist/lib/deploy-vm-um-app.js +83 -0
- package/dist/lib/segredo-produto.d.ts +25 -5
- package/dist/lib/segredo-produto.js +45 -10
- package/dist/lib/sse.d.ts +4 -5
- package/dist/lib/sse.js +4 -5
- package/package.json +1 -2
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type TenantEnvironmentId } from '../lib/envs.js';
|
|
2
|
+
/** Ambientes que dá pra re-apontar: os do Core menos o dev-local (roda na máquina do dev). */
|
|
3
|
+
export type RepointEnvironment = Exclude<TenantEnvironmentId, 'dev-local'>;
|
|
4
|
+
export declare const REPOINT_ENVIRONMENTS: readonly RepointEnvironment[];
|
|
5
|
+
export interface RepointCheck {
|
|
6
|
+
name: string;
|
|
7
|
+
ok: boolean;
|
|
8
|
+
detail?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface RepointResponse {
|
|
11
|
+
ok: true;
|
|
12
|
+
productId: string;
|
|
13
|
+
slug: string | null;
|
|
14
|
+
dryRun: boolean;
|
|
15
|
+
changed: boolean;
|
|
16
|
+
alreadyPointed: boolean;
|
|
17
|
+
env: RepointEnvironment;
|
|
18
|
+
from: string | null;
|
|
19
|
+
to: string;
|
|
20
|
+
preflight: {
|
|
21
|
+
ok: boolean;
|
|
22
|
+
checks: RepointCheck[];
|
|
23
|
+
};
|
|
24
|
+
appMoved: false;
|
|
25
|
+
pendingDrain: string[];
|
|
26
|
+
auditId: string | null;
|
|
27
|
+
nextStep: string;
|
|
28
|
+
}
|
|
29
|
+
/** Nome legível de cada requisito do `--dry-run` (o Core manda o nome técnico). */
|
|
30
|
+
export declare const REPOINT_CHECK_LABELS: Record<string, string>;
|
|
31
|
+
/** Rótulo de um requisito (os de banco vêm como `database:<dbId>`). */
|
|
32
|
+
export declare function checkLabel(name: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* O que fazer para cada código `REPOINT_*` que a rota devolve. A mensagem do
|
|
35
|
+
* Core (com VMs, bancos e o motivo) vem antes; isto é o "e agora?".
|
|
36
|
+
*/
|
|
37
|
+
export declare const REPOINT_GUIDANCE: Record<string, string>;
|
|
38
|
+
/** Mensagem do Core primeiro (dados reais), depois o que fazer. Pura. */
|
|
39
|
+
export declare function describeRepointError(code: string, serverMessage: string): string;
|
|
40
|
+
export interface EnvironmentsRepointOptions {
|
|
41
|
+
to?: string;
|
|
42
|
+
expectFrom?: string;
|
|
43
|
+
reason?: string;
|
|
44
|
+
dryRun?: boolean;
|
|
45
|
+
yes?: boolean;
|
|
46
|
+
mfaToken?: string;
|
|
47
|
+
json?: boolean;
|
|
48
|
+
}
|
|
49
|
+
export declare function runEnvironmentsRepoint(slug: string, env: string, opts?: EnvironmentsRepointOptions): Promise<void>;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `neetru environments repoint <slug> <env> --to <serverId> --expect-from <serverId> --reason "<motivo>"`
|
|
3
|
+
* — RE-APONTA o ambiente (`staging` | `prod`) de um produto para outra VM
|
|
4
|
+
* (Core bug_7446f7f2).
|
|
5
|
+
*
|
|
6
|
+
* Antes não havia comando para isso: só um PATCH manual no Firestore pelo Dev
|
|
7
|
+
* Core, e o go-live de um produto travava quando ele estava fora.
|
|
8
|
+
*
|
|
9
|
+
* O que muda: SÓ o ponteiro do ambiente (a VM que o ambiente aponta). O app NÃO
|
|
10
|
+
* muda de VM: o próximo `neetru deploy --target vm --env <env> --server <id>`
|
|
11
|
+
* é que leva o app, acerta a porta e o domínio. A promoção de staging para prod
|
|
12
|
+
* passa a usar a VM apontada. A VM antiga continua rodando o que estava lá e
|
|
13
|
+
* fica contada como ocupada por este ambiente.
|
|
14
|
+
*
|
|
15
|
+
* `--dry-run` confere os requisitos sem gravar e sem MFA. Fora dele: admin +
|
|
16
|
+
* MFA (`--mfa-token` ou a sessão de 1h de `neetru auth mfa-session`; sessão
|
|
17
|
+
* recusada pelo Core ⇒ pede um TOTP fresco UMA vez).
|
|
18
|
+
*
|
|
19
|
+
* Endpoint (Core): POST /api/cli/v1/products/{slug}/environments/{env}/server
|
|
20
|
+
* body `{ toServerId, expectedServerId, reason, dryRun? }`.
|
|
21
|
+
*
|
|
22
|
+
* Plain PT-BR em toda string visível ao usuário. O grupo `neetru env` é do
|
|
23
|
+
* NEETRU_ENV local e das variáveis do Cloud Run — por isso o nome `environments`.
|
|
24
|
+
*/
|
|
25
|
+
import chalk from 'chalk';
|
|
26
|
+
import { apiRequest, CliApiError } from '../lib/api-client.js';
|
|
27
|
+
import { handleApiError } from '../lib/api-error.js';
|
|
28
|
+
import { requireToken } from '../lib/cli-read.js';
|
|
29
|
+
import { confirmDestructive, resolveStepUp, withStepUpRetry } from '../lib/cli-write.js';
|
|
30
|
+
import { TENANT_ENVIRONMENT_IDS } from '../lib/envs.js';
|
|
31
|
+
import { log } from '../utils/logger.js';
|
|
32
|
+
import { isSafeExitHalt, safeExit, setSafeExitCode } from '../utils/safe-exit.js';
|
|
33
|
+
export const REPOINT_ENVIRONMENTS = TENANT_ENVIRONMENT_IDS.filter((e) => e !== 'dev-local');
|
|
34
|
+
const SERVER_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
35
|
+
const MAX_REASON = 500;
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Textos (puros — testáveis sem rede)
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
/** Nome legível de cada requisito do `--dry-run` (o Core manda o nome técnico). */
|
|
40
|
+
export const REPOINT_CHECK_LABELS = {
|
|
41
|
+
environment: 'o ambiente existe',
|
|
42
|
+
environment_target_vm: 'o ambiente roda em VM',
|
|
43
|
+
environment_active: 'o ambiente não está arquivado nem suspenso',
|
|
44
|
+
expected_server: 'o ambiente aponta a VM informada em --expect-from',
|
|
45
|
+
no_deploy_in_flight: 'nenhum lançamento em andamento',
|
|
46
|
+
target_ready: 'VM nova no ar e pronta',
|
|
47
|
+
target_fleet: 'VM nova é da frota da Neetru',
|
|
48
|
+
target_role: 'VM nova aceita app e é compartilhada',
|
|
49
|
+
target_public_ip: 'VM nova tem IP público (o ambiente tem domínio público)',
|
|
50
|
+
target_capacity: 'VM nova tem espaço para mais um app',
|
|
51
|
+
same_vm: 'nenhum outro ambiente do produto na VM nova',
|
|
52
|
+
databases: 'bancos do ambiente',
|
|
53
|
+
};
|
|
54
|
+
/** Rótulo de um requisito (os de banco vêm como `database:<dbId>`). */
|
|
55
|
+
export function checkLabel(name) {
|
|
56
|
+
if (name.startsWith('database:'))
|
|
57
|
+
return `banco ${name.slice('database:'.length)} alcançável da VM nova`;
|
|
58
|
+
return REPOINT_CHECK_LABELS[name] ?? name;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* O que fazer para cada código `REPOINT_*` que a rota devolve. A mensagem do
|
|
62
|
+
* Core (com VMs, bancos e o motivo) vem antes; isto é o "e agora?".
|
|
63
|
+
*/
|
|
64
|
+
export const REPOINT_GUIDANCE = {
|
|
65
|
+
REPOINT_INPUT_INVALID: 'Algum dado do pedido está fora do formato: o ambiente é staging ou prod, e as VMs são ids de servidor (letras, números, _ ou -).',
|
|
66
|
+
REPOINT_PRODUCT_NOT_FOUND: 'Produto não encontrado. Confira o slug ou o id do produto.',
|
|
67
|
+
REPOINT_ENV_NOT_FOUND: 'O produto não tem esse ambiente. Ele nasce no primeiro deploy.',
|
|
68
|
+
REPOINT_ENV_NOT_VM: 'Esse ambiente não roda em VM, então não há VM para re-apontar.',
|
|
69
|
+
REPOINT_ENV_INACTIVE: 'O ambiente está arquivado ou suspenso. Re-apontar só vale para ambiente ativo.',
|
|
70
|
+
REPOINT_EXPECTED_MISMATCH: 'O ambiente não aponta a VM que você informou: alguém mudou desde que você olhou. Veja a VM atual no requisito ' +
|
|
71
|
+
'"o ambiente aponta a VM informada" rodando ' +
|
|
72
|
+
'`neetru environments repoint <slug> <env> --to <serverId> --expect-from <serverId> --reason "<motivo>" --dry-run`, ' +
|
|
73
|
+
'e repita com o valor certo. Nada foi gravado.',
|
|
74
|
+
REPOINT_DEPLOY_IN_PROGRESS: 'Há um lançamento em andamento neste ambiente. Espere ele terminar e repita. Nada foi gravado.',
|
|
75
|
+
REPOINT_TARGET_NOT_READY: 'A VM nova não existe, não está no ar, está sem dar sinal há mais de 5 minutos ou ainda não terminou de se preparar.',
|
|
76
|
+
REPOINT_TARGET_NOT_FLEET: 'A VM nova não é uma VM da Neetru (é de cliente ou não tem identidade no Google Cloud do projeto).',
|
|
77
|
+
REPOINT_TARGET_ROLE: 'A VM nova não recebe apps (é só de banco) ou é exclusiva. Escolha uma VM que aceite apps.',
|
|
78
|
+
REPOINT_TARGET_NO_CAPACITY: 'A VM nova está sem espaço para mais um app. Escolha outra VM ou libere espaço nela.',
|
|
79
|
+
REPOINT_TARGET_IP_NOT_PUBLIC: 'O ambiente tem domínio público e a VM nova não tem IP público — o domínio não teria para onde apontar.',
|
|
80
|
+
REPOINT_SAME_VM_COLLISION: 'Outro ambiente do mesmo produto está (ou pode estar) rodando na VM nova. Os dois se sobrescreveriam: use uma VM por ambiente.',
|
|
81
|
+
REPOINT_DB_UNREACHABLE: 'Um banco deste ambiente fica em outra VM e não tem a porta de entrada ligada para a VM nova — o app ficaria sem banco. ' +
|
|
82
|
+
'Ligue antes com `neetru db frontdoor enable <dbId> --app-server <serverId> --reason "<motivo>"` e repita.',
|
|
83
|
+
REPOINT_DB_ENGINE_UNSUPPORTED: 'Um banco deste ambiente fica em outra VM e é de um tipo sem porta de entrada (MySQL, Mongo ou cluster). ' +
|
|
84
|
+
'O app precisa ficar na mesma VM desse banco. Fale com o Dev Core.',
|
|
85
|
+
REPOINT_DB_OP_IN_PROGRESS: 'Um banco deste ambiente está mudando de VM ou ligando a porta de entrada. Espere terminar e repita.',
|
|
86
|
+
REPOINT_READ_FAILED: 'Não foi possível ler um dos requisitos agora. Nada foi gravado — repita em instantes.',
|
|
87
|
+
REPOINT_STATE_CHANGED: 'O ambiente ou um requisito mudou entre a conferência e a gravação. Nada foi gravado — confira e repita.',
|
|
88
|
+
REPOINT_COMMIT_FAILED: 'A gravação não conseguiu terminar. Nada foi gravado — repita em instantes.',
|
|
89
|
+
};
|
|
90
|
+
/** Mensagem do Core primeiro (dados reais), depois o que fazer. Pura. */
|
|
91
|
+
export function describeRepointError(code, serverMessage) {
|
|
92
|
+
const guidance = REPOINT_GUIDANCE[code];
|
|
93
|
+
return guidance ? `${serverMessage}\n ${guidance}` : serverMessage;
|
|
94
|
+
}
|
|
95
|
+
/** Erro da rota: `REPOINT_*` vira mensagem + o que fazer; o resto vai pro handler comum. */
|
|
96
|
+
function failRepoint(err, json) {
|
|
97
|
+
if (isSafeExitHalt(err))
|
|
98
|
+
throw err;
|
|
99
|
+
if (err instanceof CliApiError && typeof err.code === 'string' && err.code.startsWith('REPOINT_')) {
|
|
100
|
+
if (json) {
|
|
101
|
+
console.log(JSON.stringify({
|
|
102
|
+
ok: false,
|
|
103
|
+
error: err.code,
|
|
104
|
+
message: err.message,
|
|
105
|
+
guidance: REPOINT_GUIDANCE[err.code] ?? null,
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
log.error(describeRepointError(err.code, err.message));
|
|
110
|
+
}
|
|
111
|
+
return safeExit(1);
|
|
112
|
+
}
|
|
113
|
+
return handleApiError(err, json);
|
|
114
|
+
}
|
|
115
|
+
function inputError(message, json) {
|
|
116
|
+
if (json)
|
|
117
|
+
console.log(JSON.stringify({ ok: false, error: 'input', message }));
|
|
118
|
+
else
|
|
119
|
+
log.error(message);
|
|
120
|
+
return safeExit(1);
|
|
121
|
+
}
|
|
122
|
+
function printChecks(checks) {
|
|
123
|
+
log.heading('Requisitos');
|
|
124
|
+
for (const c of checks) {
|
|
125
|
+
const mark = c.ok ? chalk.green('✓') : chalk.red('✗');
|
|
126
|
+
log.dim(` ${mark} ${checkLabel(c.name)}${c.detail ? ` — ${c.detail}` : ''}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export async function runEnvironmentsRepoint(slug, env, opts = {}) {
|
|
130
|
+
if (!opts.json)
|
|
131
|
+
log.banner();
|
|
132
|
+
if (!slug || !slug.trim() || slug.includes('/'))
|
|
133
|
+
inputError('Informe o produto (slug ou id).', opts.json);
|
|
134
|
+
if (!REPOINT_ENVIRONMENTS.includes(env)) {
|
|
135
|
+
inputError(`Ambiente inválido: "${env}". Use ${REPOINT_ENVIRONMENTS.join(' ou ')}.`, opts.json);
|
|
136
|
+
}
|
|
137
|
+
// Literal aqui (não num helper): a guarda de step-up da CLI precisa enxergar a rota.
|
|
138
|
+
const rota = `/api/cli/v1/products/${encodeURIComponent(slug)}/environments/${encodeURIComponent(env)}/server`;
|
|
139
|
+
const to = (opts.to ?? '').trim();
|
|
140
|
+
const expectFrom = (opts.expectFrom ?? '').trim();
|
|
141
|
+
const reason = (opts.reason ?? '').trim();
|
|
142
|
+
if (!SERVER_ID_RE.test(to))
|
|
143
|
+
inputError('--to <serverId> é obrigatório (o id da VM nova).', opts.json);
|
|
144
|
+
if (!SERVER_ID_RE.test(expectFrom)) {
|
|
145
|
+
inputError('--expect-from <serverId> é obrigatório (a VM que o ambiente aponta hoje — se mudou, nada é gravado).', opts.json);
|
|
146
|
+
}
|
|
147
|
+
if (!reason || reason.length > MAX_REASON) {
|
|
148
|
+
inputError(`--reason "<texto>" é obrigatório (1 a ${MAX_REASON} caracteres; vai para a auditoria).`, opts.json);
|
|
149
|
+
}
|
|
150
|
+
await requireToken(opts.json);
|
|
151
|
+
if (!opts.json) {
|
|
152
|
+
log.heading(opts.dryRun ? 'Re-apontar a VM do ambiente (só conferência)' : 'Re-apontar a VM do ambiente');
|
|
153
|
+
log.info(`Produto: ${chalk.cyan(slug)}`);
|
|
154
|
+
log.info(`Ambiente: ${chalk.bold(env)}`);
|
|
155
|
+
log.info(`Aponta hoje: ${expectFrom}`);
|
|
156
|
+
log.info(`Vai apontar: ${chalk.bold(to)}`);
|
|
157
|
+
log.dim(' Só o ponteiro muda — o app não muda de VM até o próximo deploy na VM nova.');
|
|
158
|
+
}
|
|
159
|
+
const body = { toServerId: to, expectedServerId: expectFrom, reason };
|
|
160
|
+
// ── Só conferência: nada é gravado, sem MFA. ──────────────────────────────
|
|
161
|
+
if (opts.dryRun) {
|
|
162
|
+
const { headers } = await resolveStepUp({ dryRun: true, json: opts.json, mfaToken: opts.mfaToken });
|
|
163
|
+
let r;
|
|
164
|
+
try {
|
|
165
|
+
r = await apiRequest(rota, {
|
|
166
|
+
method: 'POST',
|
|
167
|
+
body: { ...body, dryRun: true },
|
|
168
|
+
headers,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
failRepoint(err, opts.json);
|
|
173
|
+
}
|
|
174
|
+
if (opts.json) {
|
|
175
|
+
console.log(JSON.stringify({ ...r, ok: r.preflight.ok || r.alreadyPointed }));
|
|
176
|
+
if (!r.preflight.ok && !r.alreadyPointed)
|
|
177
|
+
setSafeExitCode(1);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (r.alreadyPointed) {
|
|
181
|
+
log.info(`O ambiente ${env} já aponta ${to} — não há o que re-apontar.`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
printChecks(r.preflight.checks);
|
|
185
|
+
if (r.preflight.ok) {
|
|
186
|
+
log.success('Tudo certo para re-apontar. Nada foi alterado. Rode de novo sem --dry-run para gravar.');
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
log.error('Algum requisito falhou — o ambiente não seria re-apontado. Nada foi alterado.');
|
|
190
|
+
setSafeExitCode(1);
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
await confirmDestructive(`Re-apontar o ambiente ${env} do produto ${slug} de ${expectFrom} para ${to}? Só o ponteiro muda: o app ` +
|
|
195
|
+
'continua onde está até o próximo deploy na VM nova, e a promoção de staging para prod passa a usar a VM nova.', { yes: opts.yes, json: opts.json });
|
|
196
|
+
const r = await withStepUpRetry({ mfaToken: opts.mfaToken, json: opts.json, yes: opts.yes }, (headers) => apiRequest(rota, { method: 'POST', body, headers }), (err) => failRepoint(err, opts.json));
|
|
197
|
+
if (opts.json) {
|
|
198
|
+
console.log(JSON.stringify(r));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (r.alreadyPointed) {
|
|
202
|
+
log.info(`O ambiente ${env} já apontava ${to} — nada foi gravado.`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (!r.changed) {
|
|
206
|
+
// Não deveria acontecer (o Core responde erro quando não grava), mas sem mentir:
|
|
207
|
+
log.error('O Core respondeu sem gravar o ponteiro. Nada mudou — confira com --dry-run.');
|
|
208
|
+
setSafeExitCode(1);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
log.success(`Ambiente ${env} agora aponta ${r.to} (antes: ${r.from ?? 'nenhuma VM'}).`);
|
|
212
|
+
log.warn('O app NÃO mudou de VM: só o ponteiro do ambiente.');
|
|
213
|
+
if (r.pendingDrain.length > 0) {
|
|
214
|
+
log.dim(` Continua rodando o que estava em ${r.pendingDrain.join(', ')} (nada é desligado), e essa VM segue ` +
|
|
215
|
+
'contada como ocupada por este ambiente.');
|
|
216
|
+
}
|
|
217
|
+
log.info(`Próximo passo: ${r.nextStep}`);
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=environments-repoint.js.map
|
package/dist/commands/init.js
CHANGED
|
@@ -80,7 +80,13 @@ async function resolveTarget(name, cwd, opts) {
|
|
|
80
80
|
// Default: cria subdir <cwd>/<name>
|
|
81
81
|
const insp = await inspectDir(subDir);
|
|
82
82
|
if (insp.exists && !insp.isEmpty) {
|
|
83
|
-
|
|
83
|
+
// bug_89d2497e (etapa 1): o `neetru new` chega aqui pelo `runInit` e não
|
|
84
|
+
// declara --here/--path/--force — "Use --here" mandava o usuário repetir
|
|
85
|
+
// o `neetru new` com uma flag que ele recusa. O conselho cita o comando
|
|
86
|
+
// inteiro. E `--force` com `--yes` cai de novo neste mesmo ramo (ele não
|
|
87
|
+
// lê `opts.force`); sobrescrever é `--path <dir> --force`.
|
|
88
|
+
log.error(`Diretório "${name}" já existe com arquivos. Rode \`neetru init ${name} --here\` (mescla no diretório atual), ` +
|
|
89
|
+
`\`neetru init ${name} --path <outro-diretório>\` ou, para sobrescrever, \`neetru init ${name} --path ${name} --force\`.`);
|
|
84
90
|
safeExit(1); // erro hard em non-interactive — consistente com pre-2.2.0
|
|
85
91
|
}
|
|
86
92
|
return { dir: subDir, mode: 'new', force: false };
|
package/dist/commands/new.js
CHANGED
|
@@ -195,6 +195,20 @@ export async function runNewMacro(name, opts = {}) {
|
|
|
195
195
|
safeExit(1);
|
|
196
196
|
}
|
|
197
197
|
let slug = (opts.productSlug ?? deriveSlug(name)) || deriveSlug(name);
|
|
198
|
+
// bug_89d2497e (etapa 1): nome sem letra nem dígito ("!!!") deriva slug
|
|
199
|
+
// vazio, e quem recusava era o `products create`, com "informe --slug" —
|
|
200
|
+
// flag que o `neetru new` não tem (a dele é --product-slug). Recusa aqui,
|
|
201
|
+
// antes de autenticar e de criar qualquer coisa no Core.
|
|
202
|
+
if (!slug) {
|
|
203
|
+
const msg = `Não dá para derivar um slug de "${name}" — informe --product-slug <slug>.`;
|
|
204
|
+
if (opts.json) {
|
|
205
|
+
console.log(JSON.stringify({ ok: false, error: 'invalid_input', message: msg }));
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
log.error(msg);
|
|
209
|
+
}
|
|
210
|
+
safeExit(1);
|
|
211
|
+
}
|
|
198
212
|
// bug_7dcb68e2666a43f69ef362d2cbff94ed: o padrão era 'dev', token que o Core
|
|
199
213
|
// não conhece — a macro morria no passo [2/5] com 400 validation_failed.
|
|
200
214
|
const env = opts.env
|
|
@@ -28,10 +28,12 @@
|
|
|
28
28
|
* '--value=sk_live…'", "unknown command 'sk_live…'") imprimiria o segredo de
|
|
29
29
|
* volta no terminal.
|
|
30
30
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
31
|
+
* Entrega ao app na VM: é o lado do AGENTE (Parte 2 de bug_8b85353b,
|
|
32
|
+
* `neetru-agent-service-vm`), publicado na 1.6.80 (2026-10-20). No deploy, o
|
|
33
|
+
* agente busca os valores no Core e grava `/etc/neetru/<slug>.product.env`
|
|
34
|
+
* (root, 0600). Por isso o app só vê uma gravação no PRÓXIMO deploy — e a CLI
|
|
35
|
+
* diz isso ao gravar (`notaEntregaDoSegredo`). Até a 2.33.0 ela dizia que o
|
|
36
|
+
* agente "ainda não entrega", frase falsa desde a 1.6.80.
|
|
35
37
|
*/
|
|
36
38
|
import type { Command } from 'commander';
|
|
37
39
|
import { type StepUpOptions } from '../lib/cli-write.js';
|
package/dist/commands/secrets.js
CHANGED
|
@@ -5,7 +5,7 @@ import { renderTable, fmtTimestamp } from '../lib/render.js';
|
|
|
5
5
|
import { requireToken, handleApiError, confirmDestructive, printWriteResult, resolveStepUp, isMfaRequiredError, } from '../lib/cli-write.js';
|
|
6
6
|
import { loadProductConfig } from '../lib/product-config.js';
|
|
7
7
|
import { listarAmbientesTenant, resolverAmbienteTenant, } from '../lib/envs.js';
|
|
8
|
-
import {
|
|
8
|
+
import { CodificacaoInvalida, PedidoCancelado, lerValorDeArquivo, lerValorDeFluxo, notaEntregaDoSegredo, perguntarSemEco, problemaNaChave, problemaNaChaveReservada, problemaNoTextoDoTerminal, problemaNoValor, problemaNosBytes, } from '../lib/segredo-produto.js';
|
|
9
9
|
import { log } from '../utils/logger.js';
|
|
10
10
|
import { safeExit } from '../utils/safe-exit.js';
|
|
11
11
|
function entradaPadrao() {
|
|
@@ -61,8 +61,13 @@ function exigirAmbiente(env, json) {
|
|
|
61
61
|
}
|
|
62
62
|
return resolverAmbienteTenant(env, json);
|
|
63
63
|
}
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
/**
|
|
65
|
+
* `comoMandarOValor`: só o `set` passa (é ele que recebe valor, por --stdin,
|
|
66
|
+
* --from-file ou pelo pedido na tela). O `archive` não passa: lá o conselho é
|
|
67
|
+
* só "passe só o nome" (bug_89d2497e, etapa 1).
|
|
68
|
+
*/
|
|
69
|
+
function exigirChave(chave, json, comoMandarOValor) {
|
|
70
|
+
const problema = problemaNaChave(chave, comoMandarOValor);
|
|
66
71
|
if (problema)
|
|
67
72
|
return falhar(json, 'invalid_key', problema);
|
|
68
73
|
return chave;
|
|
@@ -170,7 +175,7 @@ function cabeRepetirComTotp(ambiente, headers, erro) {
|
|
|
170
175
|
}
|
|
171
176
|
export async function runSecretsSet(opts, entrada = entradaPadrao()) {
|
|
172
177
|
recusarArgumentosExtras(opts.extras, opts.json);
|
|
173
|
-
const chave = exigirChave(opts.key, opts.json);
|
|
178
|
+
const chave = exigirChave(opts.key, opts.json, 'por --stdin, --from-file ou pelo pedido na tela');
|
|
174
179
|
exigirChaveRegistravel(chave, opts.json);
|
|
175
180
|
const ambiente = exigirAmbiente(opts.env, opts.json);
|
|
176
181
|
const origens = [!!opts.generate, opts.fromFile !== undefined, !!opts.stdin].filter(Boolean).length;
|
|
@@ -245,7 +250,7 @@ export async function runSecretsSet(opts, entrada = entradaPadrao()) {
|
|
|
245
250
|
revision: res.revision,
|
|
246
251
|
created: res.created,
|
|
247
252
|
secretRef: res.secretRef,
|
|
248
|
-
note:
|
|
253
|
+
note: notaEntregaDoSegredo(res.environmentId),
|
|
249
254
|
}, `Segredo ${chalk.bold(res.key)} ${acao} em ${res.environmentId} (revisão ${res.revision})` +
|
|
250
255
|
(gerado ? ' — o Core sorteou o valor; ninguém o vê, nem você.' : '.'), opts.json);
|
|
251
256
|
if (opts.json)
|
|
@@ -253,8 +258,10 @@ export async function runSecretsSet(opts, entrada = entradaPadrao()) {
|
|
|
253
258
|
if (!res.created)
|
|
254
259
|
log.dim('O valor anterior deixa de ser entregue a partir do próximo deploy.');
|
|
255
260
|
log.dim(`Nome no Secret Manager: ${res.secretRef} (é o nome, não o valor).`);
|
|
256
|
-
|
|
257
|
-
|
|
261
|
+
// Até a 2.33.0 saía aqui um aviso de que o agente da VM "ainda não entrega"
|
|
262
|
+
// segredos do produto — falso desde a 1.6.80 (bug_8b85353b). Fica só o fato:
|
|
263
|
+
// o app recebe no próximo deploy.
|
|
264
|
+
log.info(notaEntregaDoSegredo(res.environmentId));
|
|
258
265
|
}
|
|
259
266
|
export async function runSecretsList(opts) {
|
|
260
267
|
const ambiente = opts.env?.trim() ? resolverAmbienteTenant(opts.env, opts.json) : undefined;
|
|
@@ -53,7 +53,36 @@ export declare function runServersDeactivate(id: string | undefined, opts: StepU
|
|
|
53
53
|
*/
|
|
54
54
|
export interface ServersForceCleanupOptions extends StepUpOptions {
|
|
55
55
|
reason: string;
|
|
56
|
+
/**
|
|
57
|
+
* core#732 (bug_90ec21cd): id numérico da instância no Google Cloud que o
|
|
58
|
+
* operador CONFERIU. Só faz falta quando o servidor não tem o id da VM
|
|
59
|
+
* registrado (`externalId`) e existe uma VM com o mesmo nome — o Core recusa
|
|
60
|
+
* com `vm_sem_externalid` e mostra o id visto + o comando de conferência.
|
|
61
|
+
* A CLI NUNCA preenche este id a partir da resposta: o operador digita.
|
|
62
|
+
*/
|
|
63
|
+
confirmInstanceId?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* core#732: recusas da guarda de identidade da VM no force-cleanup. A mensagem
|
|
67
|
+
* do Core (com o id visto e o comando de conferência) vem antes; isto é o que
|
|
68
|
+
* fazer. Nenhum texto monta o comando com o id preenchido — de propósito.
|
|
69
|
+
*/
|
|
70
|
+
export declare const FORCE_CLEANUP_IDENTITY_GUIDANCE: Record<string, string>;
|
|
71
|
+
/**
|
|
72
|
+
* Detalhes que o core#732 manda junto da recusa (`{ error: { …, instanceId?,
|
|
73
|
+
* instanceCreatedAt?, docCreatedAt?, outrosServidores?, comandoConferencia? } }`),
|
|
74
|
+
* só os campos conhecidos e com o tipo certo. Nunca vira comando com o id.
|
|
75
|
+
*/
|
|
76
|
+
export interface ForceCleanupIdentityDetails {
|
|
77
|
+
instanceId?: string;
|
|
78
|
+
instanceCreatedAt?: string;
|
|
79
|
+
docCreatedAt?: string;
|
|
80
|
+
outrosServidores?: string[];
|
|
81
|
+
comandoConferencia?: string;
|
|
56
82
|
}
|
|
83
|
+
export declare function forceCleanupIdentityDetails(raw: unknown): ForceCleanupIdentityDetails | undefined;
|
|
84
|
+
/** Linhas legíveis dos detalhes (texto). Mostra o id VISTO, mas não monta comando com ele. */
|
|
85
|
+
export declare function forceCleanupIdentityDetailLines(d: ForceCleanupIdentityDetails | undefined): string[];
|
|
57
86
|
export declare function runServersForceCleanup(id: string, opts: ServersForceCleanupOptions): Promise<void>;
|
|
58
87
|
export interface ServersDispatchOptions extends StepUpOptions {
|
|
59
88
|
params?: string;
|
package/dist/commands/servers.js
CHANGED
|
@@ -342,6 +342,73 @@ export async function runServersDeactivate(id, opts) {
|
|
|
342
342
|
}
|
|
343
343
|
printWriteResult(res.dryRun ? res : { serverId: res.serverId, status: res.status }, `Servidor ${res.serverId} → ${res.status}`, opts.json);
|
|
344
344
|
}
|
|
345
|
+
/** Formato que a rota do Core aceita (id numérico da instância no GCP). */
|
|
346
|
+
const INSTANCE_ID_RE = /^[1-9][0-9]{0,19}$/;
|
|
347
|
+
/**
|
|
348
|
+
* core#732: recusas da guarda de identidade da VM no force-cleanup. A mensagem
|
|
349
|
+
* do Core (com o id visto e o comando de conferência) vem antes; isto é o que
|
|
350
|
+
* fazer. Nenhum texto monta o comando com o id preenchido — de propósito.
|
|
351
|
+
*/
|
|
352
|
+
export const FORCE_CLEANUP_IDENTITY_GUIDANCE = {
|
|
353
|
+
vm_sem_externalid: 'Este servidor não tem o id da VM registrado, e existe no Google Cloud uma VM com o mesmo nome — ' +
|
|
354
|
+
'pode ser OUTRA VM (nome reaproveitado). Nada foi apagado. Rode o comando de conferência acima; se a VM ' +
|
|
355
|
+
'for mesmo a deste servidor, repita o force-cleanup com --confirm-instance-id <id que você conferiu>. ' +
|
|
356
|
+
'A CLI não preenche esse id por você.',
|
|
357
|
+
vm_confirmacao_nao_confere: 'O id passado em --confirm-instance-id não é o da VM que o Core leu (a mensagem acima mostra os dois). ' +
|
|
358
|
+
'Nada foi apagado. Confira de novo antes de repetir.',
|
|
359
|
+
vm_id_divergente: 'O Google Cloud devolveu outra instância ao ler o id registrado deste servidor. Nada foi apagado — ' +
|
|
360
|
+
'fale com o Dev Core antes de tentar de novo.',
|
|
361
|
+
vm_leitura_falhou: 'Não deu para ler a VM no Google Cloud agora. Nada foi apagado. Tente de novo em alguns minutos; ' +
|
|
362
|
+
'se continuar, fale com o Dev Core.',
|
|
363
|
+
// core#732 rodada 2: a confirmação do id NÃO destrava este caso — nada de
|
|
364
|
+
// orientar "repita com o id".
|
|
365
|
+
vm_nome_em_uso_por_outro_servidor: 'Outro servidor registrado e ativo usa uma VM com o mesmo nome e zona — a VM pode ser dele. Nada foi apagado, ' +
|
|
366
|
+
'e NÃO adianta confirmar o id: o Core recusa este caso mesmo com --confirm-instance-id. Confira de qual servidor ' +
|
|
367
|
+
'é a VM e registre o id dela no servidor certo antes de excluir (fale com o Dev Core).',
|
|
368
|
+
};
|
|
369
|
+
export function forceCleanupIdentityDetails(raw) {
|
|
370
|
+
if (!raw || typeof raw !== 'object')
|
|
371
|
+
return undefined;
|
|
372
|
+
const r = raw;
|
|
373
|
+
const texto = (v) => (typeof v === 'string' && v.trim() ? v.trim() : undefined);
|
|
374
|
+
const out = {};
|
|
375
|
+
const instanceId = texto(r.instanceId);
|
|
376
|
+
const instanceCreatedAt = texto(r.instanceCreatedAt);
|
|
377
|
+
const docCreatedAt = texto(r.docCreatedAt);
|
|
378
|
+
const comandoConferencia = texto(r.comandoConferencia);
|
|
379
|
+
const outros = Array.isArray(r.outrosServidores)
|
|
380
|
+
? r.outrosServidores.map(texto).filter((x) => !!x)
|
|
381
|
+
: [];
|
|
382
|
+
if (instanceId)
|
|
383
|
+
out.instanceId = instanceId;
|
|
384
|
+
if (instanceCreatedAt)
|
|
385
|
+
out.instanceCreatedAt = instanceCreatedAt;
|
|
386
|
+
if (docCreatedAt)
|
|
387
|
+
out.docCreatedAt = docCreatedAt;
|
|
388
|
+
if (outros.length > 0)
|
|
389
|
+
out.outrosServidores = outros;
|
|
390
|
+
if (comandoConferencia)
|
|
391
|
+
out.comandoConferencia = comandoConferencia;
|
|
392
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
393
|
+
}
|
|
394
|
+
/** Linhas legíveis dos detalhes (texto). Mostra o id VISTO, mas não monta comando com ele. */
|
|
395
|
+
export function forceCleanupIdentityDetailLines(d) {
|
|
396
|
+
if (!d)
|
|
397
|
+
return [];
|
|
398
|
+
const linhas = [];
|
|
399
|
+
if (d.outrosServidores) {
|
|
400
|
+
linhas.push(`Outro(s) servidor(es) ativo(s) com o mesmo nome e zona: ${d.outrosServidores.join(', ')}`);
|
|
401
|
+
}
|
|
402
|
+
if (d.instanceId)
|
|
403
|
+
linhas.push(`VM vista no Google Cloud: id ${d.instanceId}`);
|
|
404
|
+
if (d.instanceCreatedAt || d.docCreatedAt) {
|
|
405
|
+
linhas.push(`Criação da VM: ${d.instanceCreatedAt ?? '?'} · criação deste servidor: ${d.docCreatedAt ?? '?'} ` +
|
|
406
|
+
'(a VM do próprio servidor nasce segundos depois dele; diferença grande indica VM recriada)');
|
|
407
|
+
}
|
|
408
|
+
if (d.comandoConferencia)
|
|
409
|
+
linhas.push(`Para conferir (só leitura): ${d.comandoConferencia}`);
|
|
410
|
+
return linhas;
|
|
411
|
+
}
|
|
345
412
|
export async function runServersForceCleanup(id, opts) {
|
|
346
413
|
if (!id || !/^[a-zA-Z0-9_-]{1,64}$/.test(id)) {
|
|
347
414
|
log.error('serverId inválido. Uso: neetru servers force-cleanup <serverId> --reason="<motivo min 10 chars>"');
|
|
@@ -351,6 +418,16 @@ export async function runServersForceCleanup(id, opts) {
|
|
|
351
418
|
log.error('--reason obrigatório com pelo menos 10 caracteres (paper-trail audit).');
|
|
352
419
|
safeExit(1);
|
|
353
420
|
}
|
|
421
|
+
// `--confirm-instance-id ""` (ou só espaços) é RECUSADO — não vira "flag não passada".
|
|
422
|
+
const confirmInstanceId = opts.confirmInstanceId === undefined ? undefined : opts.confirmInstanceId.trim();
|
|
423
|
+
if (confirmInstanceId !== undefined && !INSTANCE_ID_RE.test(confirmInstanceId)) {
|
|
424
|
+
const msg = '--confirm-instance-id deve ser o id numérico da VM no Google Cloud (o que você conferiu no comando de conferência).';
|
|
425
|
+
if (opts.json)
|
|
426
|
+
console.log(JSON.stringify({ ok: false, error: 'invalid_param', message: msg }));
|
|
427
|
+
else
|
|
428
|
+
log.error(msg);
|
|
429
|
+
safeExit(1);
|
|
430
|
+
}
|
|
354
431
|
const token = await requireToken(opts.json);
|
|
355
432
|
// Double-confirm (digitar o ID) — dry-run não aplica nada, então pula: nada
|
|
356
433
|
// destrutivo acontece, não faz sentido travar numa confirmação interativa.
|
|
@@ -369,7 +446,11 @@ export async function runServersForceCleanup(id, opts) {
|
|
|
369
446
|
}
|
|
370
447
|
console.log(chalk.yellow.bold('\n⚠ FORCE-CLEANUP é IRREVERSÍVEL.'));
|
|
371
448
|
console.log(chalk.dim(' Server stuck em provisioning/timeout que está bloqueando o pipeline.'));
|
|
372
|
-
console.log(chalk.dim(' Audit pesado + notification all staff serão gerados
|
|
449
|
+
console.log(chalk.dim(' Audit pesado + notification all staff serão gerados.'));
|
|
450
|
+
if (confirmInstanceId) {
|
|
451
|
+
console.log(chalk.dim(` VM a apagar: a instância de id ${confirmInstanceId}, conferida por você.`));
|
|
452
|
+
}
|
|
453
|
+
console.log('');
|
|
373
454
|
const inquirer = await carregarInquirer();
|
|
374
455
|
const ans = await inquirer.prompt([
|
|
375
456
|
{
|
|
@@ -396,12 +477,28 @@ export async function runServersForceCleanup(id, opts) {
|
|
|
396
477
|
body: {
|
|
397
478
|
reason: opts.reason.trim(),
|
|
398
479
|
confirm: 'I-UNDERSTAND-FORCE-CLEANUP',
|
|
480
|
+
...(confirmInstanceId ? { confirmInstanceId } : {}),
|
|
399
481
|
},
|
|
400
482
|
});
|
|
401
483
|
spinner?.stop();
|
|
402
484
|
}
|
|
403
485
|
catch (error) {
|
|
404
486
|
spinner?.fail('Falha no force-cleanup.');
|
|
487
|
+
// core#732: a guarda de identidade da VM recusou — nada foi apagado. Mostra
|
|
488
|
+
// a mensagem do Core (id visto + comando de conferência) e o que fazer. Sem
|
|
489
|
+
// nova tentativa automática e sem montar o comando com o id: o operador
|
|
490
|
+
// confere e digita.
|
|
491
|
+
if (error instanceof CliApiError && FORCE_CLEANUP_IDENTITY_GUIDANCE[error.code]) {
|
|
492
|
+
const guidance = FORCE_CLEANUP_IDENTITY_GUIDANCE[error.code];
|
|
493
|
+
const details = forceCleanupIdentityDetails(error.details);
|
|
494
|
+
if (opts.json) {
|
|
495
|
+
console.log(JSON.stringify({ ok: false, error: error.code, message: error.message, guidance, ...(details ? { details } : {}) }));
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
log.error([error.message, ...forceCleanupIdentityDetailLines(details), guidance].join('\n '));
|
|
499
|
+
}
|
|
500
|
+
safeExit(1);
|
|
501
|
+
}
|
|
405
502
|
handleApiError(error, opts.json);
|
|
406
503
|
}
|
|
407
504
|
if (opts.json) {
|
|
@@ -414,6 +511,16 @@ export async function runServersForceCleanup(id, opts) {
|
|
|
414
511
|
}
|
|
415
512
|
log.success(`Server ${res.serverId} force-cleanup concluído.`);
|
|
416
513
|
log.dim(` Status anterior: ${res.previousStatus} (há ${res.ageMinutes}min)`);
|
|
514
|
+
if (res.vmIdentidade) {
|
|
515
|
+
const identidade = {
|
|
516
|
+
'confere:externalId': 'conferida pelo id registrado no servidor',
|
|
517
|
+
'confere:confirmacao': 'conferida pelo id que você confirmou',
|
|
518
|
+
'ausente:id_ausente': 'a VM registrada já não existia',
|
|
519
|
+
'ausente:nome_ausente': 'não havia VM com o nome do servidor',
|
|
520
|
+
'ausente:nome_de_outro_servidor': 'a VM com esse nome é de OUTRO servidor — não foi tocada',
|
|
521
|
+
};
|
|
522
|
+
log.dim(` Identidade da VM: ${identidade[res.vmIdentidade] ?? res.vmIdentidade}`);
|
|
523
|
+
}
|
|
417
524
|
if (res.vmDeleted) {
|
|
418
525
|
log.dim(' VM GCP: deletada com sucesso.');
|
|
419
526
|
}
|
package/dist/commands/ui.js
CHANGED
|
@@ -1080,7 +1080,7 @@ async function authMenu() {
|
|
|
1080
1080
|
}
|
|
1081
1081
|
export async function runTerminalUi() {
|
|
1082
1082
|
if (!canPrompt()) {
|
|
1083
|
-
log.error('A interface interativa exige um terminal TTY.
|
|
1083
|
+
log.error('A interface interativa exige um terminal TTY. Em CI, use os comandos diretos (ex.: `neetru products list --json`).');
|
|
1084
1084
|
safeExit(1);
|
|
1085
1085
|
}
|
|
1086
1086
|
// Blindagem contra auto-encerramento: enquanto o menu interativo roda, um
|
|
@@ -31,6 +31,7 @@ import { renderTable, fmtTimestamp } from '../lib/render.js';
|
|
|
31
31
|
import { pickProductId, pickProductSlug, pickWorkspace, resolveProductIdentifier, isInteractive, promptChoice, promptText, resolveCustomer, } from '../lib/pickers.js';
|
|
32
32
|
import { DEFAULT_TENANT_ENVIRONMENT_ID, ESCOLHAS_AMBIENTE_TENANT, resolverAmbienteTenant, erroAmbienteTenantInvalido, normalizarAmbienteTenant, } from '../lib/envs.js';
|
|
33
33
|
import { exitWithCliError } from '../lib/cli-error.js';
|
|
34
|
+
import { adviseFlag } from '../cli-kit/command-context.js';
|
|
34
35
|
import { log } from '../utils/logger.js';
|
|
35
36
|
import { safeExit } from '../utils/safe-exit.js';
|
|
36
37
|
/** Formata o erro 400 "Nenhum customer ativo" de forma acionável. */
|
|
@@ -100,11 +101,25 @@ export async function runWorkspacesCreate(opts) {
|
|
|
100
101
|
const label = existingTenant.name ?? existingTenant.id ?? '(sem nome)';
|
|
101
102
|
// Modo script/JSON (--json ou --yes implícito): bloqueia com 409.
|
|
102
103
|
if (opts.json) {
|
|
104
|
+
// bug_89d2497e (etapa 1): o `neetru new` chama este runner com
|
|
105
|
+
// `json: true` e este envelope é o que o usuário dele lê — e o `new`
|
|
106
|
+
// não declara --force-duplicate. O texto sai conforme o comando em
|
|
107
|
+
// execução: só quem declara a flag (o `workspaces create`) ouve "Use
|
|
108
|
+
// --force-duplicate"; os outros recebem o caminho que existe — o
|
|
109
|
+
// `workspaces create` num terminal interativo pergunta se reusa o
|
|
110
|
+
// ambiente existente ou cria outro.
|
|
111
|
+
const message = adviseFlag({
|
|
112
|
+
flag: '--force-duplicate',
|
|
113
|
+
withFlag: `Já existe ambiente ativo '${label}' para produto=${product} env=${env}. Use --force-duplicate para criar mesmo assim.`,
|
|
114
|
+
withoutFlag: `Já existe ambiente ativo '${label}' para produto=${product} env=${env}. Para criar um segundo, rode ` +
|
|
115
|
+
`\`neetru workspaces create --product ${product} --env ${env}\` num terminal interativo: ele pergunta se ` +
|
|
116
|
+
'reusa o existente ou cria outro.',
|
|
117
|
+
});
|
|
103
118
|
console.log(JSON.stringify({
|
|
104
119
|
ok: false,
|
|
105
120
|
error: 'tenant_already_exists',
|
|
106
121
|
status: 409,
|
|
107
|
-
message
|
|
122
|
+
message,
|
|
108
123
|
existing: existingTenant,
|
|
109
124
|
}));
|
|
110
125
|
safeExit(1);
|