@neetru/cli 2.33.0 → 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 +60 -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.js +10 -5
- 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/ui.js +1 -1
- package/dist/commands/workspaces.js +16 -1
- package/dist/index.js +47 -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/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
|
@@ -230,25 +230,65 @@ export function comandoExecBin(pm, bin, args) {
|
|
|
230
230
|
}
|
|
231
231
|
/** Profundidade máxima de varredura dentro do standalone à procura do server.js. */
|
|
232
232
|
const MAX_PROFUNDIDADE_STANDALONE = 6;
|
|
233
|
+
/**
|
|
234
|
+
* Lê `relativeAppDir` de `<appDir>/.next/required-server-files.json`.
|
|
235
|
+
*
|
|
236
|
+
* bug_678ddc98 (comportamento esperado do relato): é o Next.js que decide onde o
|
|
237
|
+
* `server.js` cai dentro do standalone, e ele grava essa decisão neste arquivo
|
|
238
|
+
* no MESMO build — `relativeAppDir = path.relative(outputFileTracingRoot,
|
|
239
|
+
* appDir)`, a mesma conta que ele usa pra escrever
|
|
240
|
+
* `.next/standalone/<relativeAppDir>/server.js`. Ler a declaração dispensa
|
|
241
|
+
* adivinhar pela árvore.
|
|
242
|
+
*
|
|
243
|
+
* Num build feito no Windows o valor vem com `\`; aqui sai sempre com `/`, que
|
|
244
|
+
* é o formato do membro no tar e do `entrypoint` no deploy.
|
|
245
|
+
*/
|
|
246
|
+
export function lerRelativeAppDir(appDir) {
|
|
247
|
+
const json = lerJson(path.join(appDir, '.next', 'required-server-files.json'));
|
|
248
|
+
const bruto = json?.relativeAppDir;
|
|
249
|
+
if (typeof bruto !== 'string')
|
|
250
|
+
return { tipo: 'ausente' };
|
|
251
|
+
const posix = bruto.replace(/\\/g, '/');
|
|
252
|
+
if (posix.startsWith('/') || /^[A-Za-z]:/.test(posix))
|
|
253
|
+
return { tipo: 'invalido', valor: bruto };
|
|
254
|
+
const partes = posix.split('/').filter((p) => p !== '' && p !== '.');
|
|
255
|
+
if (partes.includes('..'))
|
|
256
|
+
return { tipo: 'invalido', valor: bruto };
|
|
257
|
+
return { tipo: 'declarado', prefixo: partes.join('/') };
|
|
258
|
+
}
|
|
259
|
+
function ehArquivo(p) {
|
|
260
|
+
try {
|
|
261
|
+
return fs.statSync(p).isFile();
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
233
267
|
/**
|
|
234
268
|
* Acha ONDE o `server.js` do Next.js ficou dentro de `.next/standalone`.
|
|
235
269
|
*
|
|
236
270
|
* Em repo de app único ele está na raiz; em monorepo o Next.js aninha o app sob
|
|
237
271
|
* o caminho dele relativo ao `outputFileTracingRoot` (`apps/web/server.js`).
|
|
238
272
|
* Não dá pra deduzir esse prefixo do `next.config` (o `outputFileTracingRoot`
|
|
239
|
-
* pode ser implícito, vindo da detecção de workspace do próprio Next.js)
|
|
240
|
-
* a verdade é a árvore PÓS-BUILD — a mesma lógica do `isStandaloneBuildDir`.
|
|
273
|
+
* pode ser implícito, vindo da detecção de workspace do próprio Next.js).
|
|
241
274
|
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
275
|
+
* 1. Primeiro vale o que o Next.js DECLAROU (`relativeAppDir` em
|
|
276
|
+
* `.next/required-server-files.json`, ver `lerRelativeAppDir`). Se ele
|
|
277
|
+
* declarou um lugar e o `server.js` não está lá — ou declarou um lugar fora
|
|
278
|
+
* da raiz de tracing — devolve `null`: o build está incoerente e escolher
|
|
279
|
+
* outro `server.js` da árvore empacotaria o app errado.
|
|
280
|
+
* 2. Sem a declaração (Next.js antigo), a verdade é a árvore PÓS-BUILD — a
|
|
281
|
+
* mesma lógica do `isStandaloneBuildDir`. Varredura em largura, pulando
|
|
282
|
+
* `node_modules` (pacote dependente pode ter um `server.js` qualquer e
|
|
283
|
+
* envenenar a busca). Entre candidatos, ganha o que TAMBÉM tem um diretório
|
|
284
|
+
* `.next` ao lado — é o par que o Next.js sempre emite junto (`server.js` +
|
|
285
|
+
* `.next/` com os chunks de servidor). Empate real resolve pelo mais raso e,
|
|
286
|
+
* em seguida, por ordem alfabética (determinismo: dois builds da mesma árvore
|
|
287
|
+
* têm que dar o mesmo tarball).
|
|
248
288
|
*
|
|
249
289
|
* Devolve `null` quando não há standalone nenhum (build não-standalone) ou
|
|
250
|
-
* quando há standalone mas nenhum `server.js` — nesse segundo
|
|
251
|
-
* precisa FALHAR, nunca empacotar um tarball sem entrypoint.
|
|
290
|
+
* quando há standalone mas nenhum `server.js` no lugar certo — nesse segundo
|
|
291
|
+
* caso quem chama precisa FALHAR, nunca empacotar um tarball sem entrypoint.
|
|
252
292
|
*/
|
|
253
293
|
export function resolveStandaloneLayout(appDir) {
|
|
254
294
|
const standaloneDir = path.join(appDir, '.next', 'standalone');
|
|
@@ -259,6 +299,20 @@ export function resolveStandaloneLayout(appDir) {
|
|
|
259
299
|
catch {
|
|
260
300
|
return null;
|
|
261
301
|
}
|
|
302
|
+
const declarado = lerRelativeAppDir(appDir);
|
|
303
|
+
if (declarado.tipo === 'invalido')
|
|
304
|
+
return null;
|
|
305
|
+
if (declarado.tipo === 'declarado') {
|
|
306
|
+
const prefixo = declarado.prefixo;
|
|
307
|
+
const server = path.join(standaloneDir, ...prefixo.split('/').filter(Boolean), 'server.js');
|
|
308
|
+
if (!ehArquivo(server))
|
|
309
|
+
return null;
|
|
310
|
+
return {
|
|
311
|
+
standaloneDir,
|
|
312
|
+
appPrefix: prefixo,
|
|
313
|
+
entrypoint: prefixo ? `${prefixo}/server.js` : 'server.js',
|
|
314
|
+
};
|
|
315
|
+
}
|
|
262
316
|
const candidatos = [];
|
|
263
317
|
const fila = [
|
|
264
318
|
{ dir: standaloneDir, prefix: '', profundidade: 0 },
|
package/dist/lib/cli-write.js
CHANGED
|
@@ -308,14 +308,17 @@ export async function resolveStepUp(opts) {
|
|
|
308
308
|
// P105: o comando alternativo (`neetru auth mfa-session`) também só é
|
|
309
309
|
// citado se existe na árvore do Commander.
|
|
310
310
|
const sessao = (withCommand, withoutCommand) => adviseCommand({ command: 'auth mfa-session', withCommand, withoutCommand });
|
|
311
|
+
// Etapa 1 do bug_89d2497e: o texto dizia "modo --json/não-interativo", e
|
|
312
|
+
// `vm provision` e `agent release` chegam aqui sem declarar --json. O
|
|
313
|
+
// ramo é o não-interativo (`--json` é um caso dele), e é isso que se diz.
|
|
311
314
|
const msg = adviseFlag({
|
|
312
315
|
flag: '--mfa-token',
|
|
313
316
|
withFlag: sessao('Operação destrutiva top-tier exige step-up MFA: passe --mfa-token <código TOTP> ' +
|
|
314
|
-
'(modo
|
|
315
|
-
'(modo
|
|
317
|
+
'(modo 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> ' +
|
|
318
|
+
'(modo não-interativo).'),
|
|
316
319
|
withoutFlag: sessao('Operação destrutiva top-tier exige step-up MFA: rode `neetru auth mfa-session` ' +
|
|
317
320
|
'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
|
|
321
|
+
'como enviá-lo em modo não-interativo.'),
|
|
319
322
|
});
|
|
320
323
|
exitWithCliError(new NeetruCliError(msg, { code: 'mfa_required', exit: EXIT_CODES.client }), opts.json);
|
|
321
324
|
return { headers }; // safeExit não retorna em prod; satisfaz o tipo + testes
|
|
@@ -16,8 +16,8 @@ export declare function callGateway(settings: CodeSettings, messages: CodeMessag
|
|
|
16
16
|
* resposta se o AI Gateway suportar"). Mesmo padrão de `streamGateway` em
|
|
17
17
|
* `src/lib/ai/orchestrator.ts` (fetch direto + `readSseLines` compartilhado) —
|
|
18
18
|
* não passa por `apiRequest`/`apiStream` porque nenhum dos dois fala SSE, e
|
|
19
|
-
* não importa o orchestrator inteiro só por essa função (ele carrega
|
|
20
|
-
*
|
|
19
|
+
* não importa o orchestrator inteiro só por essa função (ele carrega o SDK
|
|
20
|
+
* `openai`, que `neetru code` não precisa).
|
|
21
21
|
*
|
|
22
22
|
* Acumula os deltas e retorna o texto completo — o protocolo
|
|
23
23
|
* `NEETRU_TOOL`/`NEETRU_PATCH` só pode ser parseado sobre a resposta inteira,
|
|
@@ -34,8 +34,8 @@ export async function callGateway(settings, messages, signal) {
|
|
|
34
34
|
* resposta se o AI Gateway suportar"). Mesmo padrão de `streamGateway` em
|
|
35
35
|
* `src/lib/ai/orchestrator.ts` (fetch direto + `readSseLines` compartilhado) —
|
|
36
36
|
* não passa por `apiRequest`/`apiStream` porque nenhum dos dois fala SSE, e
|
|
37
|
-
* não importa o orchestrator inteiro só por essa função (ele carrega
|
|
38
|
-
*
|
|
37
|
+
* não importa o orchestrator inteiro só por essa função (ele carrega o SDK
|
|
38
|
+
* `openai`, que `neetru code` não precisa).
|
|
39
39
|
*
|
|
40
40
|
* Acumula os deltas e retorna o texto completo — o protocolo
|
|
41
41
|
* `NEETRU_TOOL`/`NEETRU_PATCH` só pode ser parseado sobre a resposta inteira,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ProvedorIa } from './ai/provedores.js';
|
|
1
2
|
export declare const DEFAULT_CODE_MODEL = "llama-3.3-70b-versatile";
|
|
2
3
|
export declare const DEFAULT_CODE_MAX_TURNS = 8;
|
|
3
4
|
export declare const DEFAULT_CODE_MAX_TOKENS = 4096;
|
|
@@ -24,8 +25,7 @@ export interface ResolvedCodeModel {
|
|
|
24
25
|
}
|
|
25
26
|
export declare function isAnthropicModel(model: string | undefined): boolean;
|
|
26
27
|
export declare function resolveCodeModel(input: ResolveCodeModelInput): ResolvedCodeModel;
|
|
27
|
-
|
|
28
|
-
type CatalogProvider = (typeof KNOWN_CATALOG_PROVIDERS)[number];
|
|
28
|
+
type CatalogProvider = ProvedorIa;
|
|
29
29
|
/**
|
|
30
30
|
* GAP-M1-16 (2026-07-10): resolve o provider a forçar no request — prioridade
|
|
31
31
|
* `--provider` (flag explícita da tarefa) sobre `gatewayProvider` configurado
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ehProvedorIa } from './ai/provedores.js';
|
|
1
2
|
export const DEFAULT_CODE_MODEL = 'llama-3.3-70b-versatile';
|
|
2
3
|
export const DEFAULT_CODE_MAX_TURNS = 8;
|
|
3
4
|
export const DEFAULT_CODE_MAX_TOKENS = 4096;
|
|
@@ -37,7 +38,6 @@ export function resolveCodeModel(input) {
|
|
|
37
38
|
}
|
|
38
39
|
return { model: DEFAULT_CODE_MODEL };
|
|
39
40
|
}
|
|
40
|
-
const KNOWN_CATALOG_PROVIDERS = ['groq', 'nvidia', 'openai'];
|
|
41
41
|
/**
|
|
42
42
|
* Nomes de modelo NATIVOS de um único provider do catálogo curado
|
|
43
43
|
* (model-catalog.ts no Core, GAP-M1-16) — duplicados aqui de propósito pra
|
|
@@ -68,9 +68,7 @@ const NATIVE_MODEL_PROVIDER = {
|
|
|
68
68
|
export function resolveCodeProvider(cliProvider, configuredProvider, model) {
|
|
69
69
|
const explicit = firstNonEmpty(cliProvider, configuredProvider);
|
|
70
70
|
if (explicit) {
|
|
71
|
-
return
|
|
72
|
-
? explicit
|
|
73
|
-
: undefined;
|
|
71
|
+
return ehProvedorIa(explicit) ? explicit : undefined;
|
|
74
72
|
}
|
|
75
73
|
// Nenhum dos dois informado — tenta inferir do nome do modelo antes de
|
|
76
74
|
// desistir pro WRRR livre (ver doc de NATIVE_MODEL_PROVIDER acima).
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* (code.ts, code-agent-ui.ts, code-agent-gateway.ts, code-agent-tools.ts,
|
|
4
4
|
* code-agent-session.ts) — evita import circular de tipo entre eles.
|
|
5
5
|
*/
|
|
6
|
+
import type { ProvedorIa } from './ai/provedores.js';
|
|
6
7
|
export type CodeRole = 'system' | 'user' | 'assistant';
|
|
7
8
|
export interface CodeMessage {
|
|
8
9
|
role: CodeRole;
|
|
@@ -33,7 +34,8 @@ export interface CodeSettings {
|
|
|
33
34
|
files: string[];
|
|
34
35
|
model: string;
|
|
35
36
|
/** GAP-M1-16 (2026-07-10) — `neetru ai models` lista as opções; ausente = WRRR entre todos. */
|
|
36
|
-
provider?:
|
|
37
|
+
provider?: ProvedorIa;
|
|
38
|
+
/** Aviso mostrado ao abrir: modelo trocado e/ou `--provider` fora da lista (uma linha cada). */
|
|
37
39
|
modelWarning?: string;
|
|
38
40
|
maxTurns: number;
|
|
39
41
|
maxTokens: number;
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
import Conf from 'conf';
|
|
2
|
+
import { type ModeloDoAi, type ProvedorIa } from './ai/provedores.js';
|
|
2
3
|
interface NeetruConfig {
|
|
3
|
-
anthropicApiKey?: string;
|
|
4
4
|
openaiApiKey?: string;
|
|
5
|
-
geminiApiKey?: string;
|
|
6
5
|
groqApiKey?: string;
|
|
7
6
|
nvidiaApiKey?: string;
|
|
8
7
|
groqModel?: string;
|
|
9
8
|
nvidiaModel?: string;
|
|
10
|
-
claudeModel?: string;
|
|
11
9
|
openaiModel?: string;
|
|
12
|
-
geminiModel?: string;
|
|
13
10
|
gatewayModel?: string;
|
|
14
|
-
gatewayProvider?:
|
|
11
|
+
gatewayProvider?: ProvedorIa;
|
|
15
12
|
gatewayEffort?: 'low' | 'medium' | 'high';
|
|
16
13
|
neetruApiKey?: string;
|
|
17
14
|
neetruApiUrl: string;
|
|
18
|
-
defaultModel:
|
|
15
|
+
defaultModel: ModeloDoAi;
|
|
19
16
|
projectName?: string;
|
|
20
17
|
'telemetry.enabled'?: boolean;
|
|
21
18
|
}
|
|
22
19
|
export declare const config: Conf<NeetruConfig>;
|
|
23
20
|
export declare function getKey(name: keyof NeetruConfig): string | boolean | undefined;
|
|
24
|
-
export declare function requireKey(name: '
|
|
21
|
+
export declare function requireKey(name: 'openaiApiKey' | 'neetruApiKey'): string;
|
|
25
22
|
export {};
|
package/dist/lib/config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Conf from 'conf';
|
|
2
|
+
import { PROVEDORES_IA } from './ai/provedores.js';
|
|
2
3
|
const defaults = {
|
|
3
4
|
neetruApiUrl: 'https://api.neetru.com',
|
|
4
5
|
defaultModel: 'auto',
|
|
@@ -13,24 +14,26 @@ export const config = new Conf({
|
|
|
13
14
|
configFileMode: 0o600,
|
|
14
15
|
defaults,
|
|
15
16
|
schema: {
|
|
16
|
-
anthropicApiKey: { type: 'string' },
|
|
17
17
|
openaiApiKey: { type: 'string' },
|
|
18
|
-
geminiApiKey: { type: 'string' },
|
|
19
18
|
groqApiKey: { type: 'string' },
|
|
20
19
|
nvidiaApiKey: { type: 'string' },
|
|
21
20
|
groqModel: { type: 'string' },
|
|
22
21
|
nvidiaModel: { type: 'string' },
|
|
23
|
-
claudeModel: { type: 'string' },
|
|
24
22
|
openaiModel: { type: 'string' },
|
|
25
|
-
geminiModel: { type: 'string' },
|
|
26
23
|
gatewayModel: { type: 'string' },
|
|
27
|
-
gatewayProvider: { type: 'string', enum: [
|
|
24
|
+
gatewayProvider: { type: 'string', enum: [...PROVEDORES_IA] },
|
|
28
25
|
gatewayEffort: { type: 'string', enum: ['low', 'medium', 'high'] },
|
|
29
26
|
neetruApiKey: { type: 'string' },
|
|
30
27
|
neetruApiUrl: { type: 'string', default: 'https://api.neetru.com' },
|
|
28
|
+
// SEM `enum` de propósito (bug_19d312f3): o `conf` valida o arquivo inteiro
|
|
29
|
+
// em TODA leitura, e um config.json com `defaultModel: "claude"` ou
|
|
30
|
+
// `"gemini"` (gravado por versão antiga) derrubaria qualquer comando da CLI
|
|
31
|
+
// com "Config schema violation". Quem confere o valor: `neetru config set
|
|
32
|
+
// defaultModel` (recusa o que não está em `MODELOS_DO_AI`), o `neetru ai`
|
|
33
|
+
// sem `-m` (`runAiRepl` sai com 1 e diz como voltar ao `auto`) e o `chat()`
|
|
34
|
+
// do orchestrator, para quem o chama sem modelo.
|
|
31
35
|
defaultModel: {
|
|
32
36
|
type: 'string',
|
|
33
|
-
enum: ['claude', 'openai', 'gemini', 'groq', 'nvidia', 'gateway', 'auto'],
|
|
34
37
|
default: 'auto',
|
|
35
38
|
},
|
|
36
39
|
'telemetry.enabled': { type: 'boolean' },
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** O que dizer sobre a recusa, em linhas. Exportado para teste. */
|
|
2
|
+
export declare function explicacaoDaRecusaDeDestino(productSlug: string, ambiente: string, destinoAtual: string, destinoPedido: string): string[];
|
|
3
|
+
/**
|
|
4
|
+
* Se `err` é a recusa de destino do Core, devolve o mesmo erro canônico do
|
|
5
|
+
* `handleApiError` (mesmo código, mesmo exit, mesmo `message`) com a explicação
|
|
6
|
+
* no texto humano e, no `--json`, em `hint` — mais `ambiente`, `destinoAtual` e
|
|
7
|
+
* `destinoPedido`. Qualquer outro erro volta intacto.
|
|
8
|
+
*/
|
|
9
|
+
export declare function explicarRecusaDeDestino(err: unknown, productSlug: string): unknown;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recusa do Core "Ambiente <x> tem destino '<a>', não '<b>'" — o que a CLI diz
|
|
3
|
+
* ao dev quando o deploy esbarra nela.
|
|
4
|
+
*
|
|
5
|
+
* ORIGEM — bug_6d92ef71 (rodada 4) e bug_fe30f035 (Core, aberto).
|
|
6
|
+
*
|
|
7
|
+
* O aviso do `--port` e o `neetru deploy --help` (`deploy-vm-um-app.ts`)
|
|
8
|
+
* ensinam que dois apps na VM são dois produtos: `neetru products create` e o
|
|
9
|
+
* deploy de cada um com o seu `--product`. A revisão da cli#99 mediu no Core
|
|
10
|
+
* (origin/main dc45be28; conferido de novo em f60ea772) que esse caminho
|
|
11
|
+
* esbarra, hoje, num defeito do Core:
|
|
12
|
+
* - `_createProductCore` (`_products-core.ts`) cria staging e prod chamando
|
|
13
|
+
* `_ensureEnvironmentCore` sem `target`, e o padrão é 'workspace';
|
|
14
|
+
* - `_deployToVmCore` (e `_deployToCloudRunCore`) chama `_ensureEnvironmentCore`
|
|
15
|
+
* pedindo o próprio destino, mas o destino só vale na CRIAÇÃO: o ambiente que
|
|
16
|
+
* já existe volta como está, e o deploy lança "Ambiente prod tem destino
|
|
17
|
+
* 'workspace', não 'vm'. Migre o destino antes (changeEnvironmentTarget) ou
|
|
18
|
+
* use o destino atual." — a rota `/api/cli/v1/deploy` responde 400
|
|
19
|
+
* (`vm_deploy_failed` / `cloud_run_deploy_failed`) com esse texto;
|
|
20
|
+
* - `changeEnvironmentTarget` não tem rota `/api/cli/v1`, e a CLI não tem
|
|
21
|
+
* comando que troque o destino de um ambiente.
|
|
22
|
+
* Ou seja: o 1º deploy na VM de um produto criado pelo `neetru products create`
|
|
23
|
+
* é recusado, e o recado manda o dev para um caminho que ele não tem.
|
|
24
|
+
*
|
|
25
|
+
* O conserto do destino é do Core (bug_fe30f035). Aqui a CLI só deixa de largar
|
|
26
|
+
* o dev diante do recado: reconhece a recusa e diz o que aconteceu e o que dá
|
|
27
|
+
* para fazer. Só age quando o Core recusa DE FATO — quando o Core for
|
|
28
|
+
* consertado, o produto novo deixa de cair aqui e este texto não fica dizendo
|
|
29
|
+
* coisa velha. Se o Core mudar o recado, o reconhecimento falha fechado: o erro
|
|
30
|
+
* segue como sempre seguiu (`handleApiError`, com o texto do Core).
|
|
31
|
+
*
|
|
32
|
+
* Conferido de novo em 24/10 na main do Core (582124ab): a mesma cadeia. O
|
|
33
|
+
* conserto proposto (core#791, aberto) faz o deploy trocar o destino de um
|
|
34
|
+
* ambiente que nunca serviu e, para o que já serviu, troca o recado por outro
|
|
35
|
+
* sem a cauda `changeEnvironmentTarget` — que esta regex exige. Com ele no ar,
|
|
36
|
+
* nada cai aqui e o erro do Core passa como veio.
|
|
37
|
+
*/
|
|
38
|
+
import { CliApiError } from './api-client.js';
|
|
39
|
+
import { NeetruCliError } from '../cli-kit/errors.js';
|
|
40
|
+
import { cliErrorFromApiError } from './api-error.js';
|
|
41
|
+
/**
|
|
42
|
+
* O recado do Core, de `_vm-deploy-core.ts` e `_cloud-run-deploy-core.ts`.
|
|
43
|
+
* A cauda `Migre o destino antes (changeEnvironmentTarget)` faz parte do
|
|
44
|
+
* reconhecimento de propósito: é ela que manda o dev para o caminho que ele não
|
|
45
|
+
* tem. A recusa do workspace (`… A Fase 1 do fluxo de ambientes só publica no
|
|
46
|
+
* destino workspace.`) não a traz e fica de fora.
|
|
47
|
+
*/
|
|
48
|
+
const RECUSA_DE_DESTINO = /Ambiente (\S+) tem destino '([^']+)', não '([^']+)'\. Migre o destino antes \(changeEnvironmentTarget\)/;
|
|
49
|
+
/** Destinos em que um app roda — os únicos que vale sugerir em `--target`. */
|
|
50
|
+
const DESTINOS_DE_APP = new Set(['vm', 'cloud-run']);
|
|
51
|
+
/** O que dizer sobre a recusa, em linhas. Exportado para teste. */
|
|
52
|
+
export function explicacaoDaRecusaDeDestino(productSlug, ambiente, destinoAtual, destinoPedido) {
|
|
53
|
+
const linhas = [
|
|
54
|
+
`O ambiente ${ambiente} de ${productSlug} está com destino '${destinoAtual}', e este deploy pediu '${destinoPedido}'.`,
|
|
55
|
+
'A CLI não tem comando para trocar o destino de um ambiente (o changeEnvironmentTarget do recado acima',
|
|
56
|
+
'não está ao alcance dela).',
|
|
57
|
+
];
|
|
58
|
+
if (destinoAtual === 'workspace') {
|
|
59
|
+
linhas.push('Produto criado pelo `neetru products create` nasce com staging e prod no destino workspace, e hoje o', '1º deploy dele na VM ou no Cloud Run é recusado assim. É defeito do Core (bug_fe30f035): até o conserto,', 'este deploy não segue pela CLI — leve o caso a um admin do Core.');
|
|
60
|
+
}
|
|
61
|
+
else if (DESTINOS_DE_APP.has(destinoAtual)) {
|
|
62
|
+
linhas.push(`Para seguir no destino atual, refaça o deploy com --target ${destinoAtual}; para trocar o destino,`, 'leve o caso a um admin do Core.');
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
linhas.push('Para trocar o destino, leve o caso a um admin do Core.');
|
|
66
|
+
}
|
|
67
|
+
return linhas;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Se `err` é a recusa de destino do Core, devolve o mesmo erro canônico do
|
|
71
|
+
* `handleApiError` (mesmo código, mesmo exit, mesmo `message`) com a explicação
|
|
72
|
+
* no texto humano e, no `--json`, em `hint` — mais `ambiente`, `destinoAtual` e
|
|
73
|
+
* `destinoPedido`. Qualquer outro erro volta intacto.
|
|
74
|
+
*/
|
|
75
|
+
export function explicarRecusaDeDestino(err, productSlug) {
|
|
76
|
+
if (!(err instanceof CliApiError) || err.status !== 400)
|
|
77
|
+
return err;
|
|
78
|
+
const achado = RECUSA_DE_DESTINO.exec(err.message);
|
|
79
|
+
if (!achado)
|
|
80
|
+
return err;
|
|
81
|
+
const [, ambiente, destinoAtual, destinoPedido] = achado;
|
|
82
|
+
const linhas = explicacaoDaRecusaDeDestino(productSlug, ambiente, destinoAtual, destinoPedido);
|
|
83
|
+
const canonico = cliErrorFromApiError(err);
|
|
84
|
+
return new NeetruCliError(canonico.message, {
|
|
85
|
+
code: canonico.code,
|
|
86
|
+
exit: canonico.exit,
|
|
87
|
+
cause: err,
|
|
88
|
+
details: {
|
|
89
|
+
...canonico.details,
|
|
90
|
+
ambiente,
|
|
91
|
+
destinoAtual,
|
|
92
|
+
destinoPedido,
|
|
93
|
+
hint: linhas.join(' '),
|
|
94
|
+
},
|
|
95
|
+
human: [canonico.human, ...linhas.map((l) => ` ${l}`)].join('\n'),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=deploy-recusa-de-destino.js.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/** Veredito da checagem da página inicial. */
|
|
2
|
+
export type RenderVerdict = 'ok' | 'inline_scripts_blocked' | 'unverifiable';
|
|
3
|
+
export interface RenderCheck {
|
|
4
|
+
verdict: RenderVerdict;
|
|
5
|
+
/** URL final conferida (depois de redirects na mesma origem). */
|
|
6
|
+
url: string;
|
|
7
|
+
httpStatus: number | null;
|
|
8
|
+
/** Scripts embutidos executáveis achados no HTML lido. */
|
|
9
|
+
inlineScripts: number;
|
|
10
|
+
/** Quantos deles a CSP bloqueia. */
|
|
11
|
+
blockedInlineScripts: number;
|
|
12
|
+
/** Diretiva que bloqueou (ex.: `script-src 'self'`), cortada. */
|
|
13
|
+
blockingDirective: string | null;
|
|
14
|
+
/** Por que não deu pra conferir (só em `unverifiable`). */
|
|
15
|
+
reason: string | null;
|
|
16
|
+
}
|
|
17
|
+
/** Resultado bruto de UMA requisição à página inicial. */
|
|
18
|
+
export type RenderProbeInput = {
|
|
19
|
+
kind: 'response';
|
|
20
|
+
httpStatus: number;
|
|
21
|
+
contentType: string | null;
|
|
22
|
+
/** Valores crus de `Content-Security-Policy` (o fetch junta repetidos com ", "). */
|
|
23
|
+
cspHeaderValues: string[];
|
|
24
|
+
html: string;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'network_error';
|
|
27
|
+
error: string;
|
|
28
|
+
};
|
|
29
|
+
/** Teto de bytes lidos do HTML (os scripts do Next aparecem já no começo do body). */
|
|
30
|
+
export declare const RENDER_BODY_MAX_BYTES: number;
|
|
31
|
+
/** Redirects seguidos dentro da MESMA origem (ex.: `/` → `/pt-BR`). */
|
|
32
|
+
export declare const RENDER_MAX_REDIRECTS = 2;
|
|
33
|
+
/** Uma política CSP: diretiva (minúscula) → lista de fontes. */
|
|
34
|
+
export interface CspPolicy {
|
|
35
|
+
directives: Map<string, string[]>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Lê UMA política serializada (`a b; c d`). Diretiva repetida: vale a primeira
|
|
39
|
+
* (CSP3 §2.2.1). Nome de diretiva inválido é ignorado. PURA.
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseCspPolicy(text: string): CspPolicy;
|
|
42
|
+
/**
|
|
43
|
+
* Lê o valor de um cabeçalho `Content-Security-Policy`: várias políticas vêm
|
|
44
|
+
* separadas por vírgula (cabeçalhos repetidos também chegam assim, juntados
|
|
45
|
+
* pelo fetch). Política vazia é descartada. PURA.
|
|
46
|
+
*/
|
|
47
|
+
export declare function parseCspHeader(value: string): CspPolicy[];
|
|
48
|
+
/**
|
|
49
|
+
* Lista de fontes que governa um `<script>` embutido: `script-src-elem`, senão
|
|
50
|
+
* `script-src`, senão `default-src`. `undefined` = a política não restringe
|
|
51
|
+
* script. PURA.
|
|
52
|
+
*/
|
|
53
|
+
export declare function scriptElementSources(policy: CspPolicy): {
|
|
54
|
+
name: string;
|
|
55
|
+
sources: string[];
|
|
56
|
+
} | undefined;
|
|
57
|
+
export interface InlineScript {
|
|
58
|
+
nonce: string | null;
|
|
59
|
+
content: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Este `<script>` embutido roda sob esta lista de fontes? Regras do navegador
|
|
63
|
+
* (CSP3 "Does element match source list for type and source"):
|
|
64
|
+
* - nonce do elemento igual a um `'nonce-…'` da lista → roda;
|
|
65
|
+
* - hash do conteúdo igual a um `'sha256|384|512-…'` → roda;
|
|
66
|
+
* - `'unsafe-inline'` → roda, MAS só se a lista não tem nonce, hash nem
|
|
67
|
+
* `'strict-dynamic'` (com qualquer um deles o navegador ignora o unsafe-inline);
|
|
68
|
+
* - o resto (`'self'`, hosts, `'none'`, lista vazia) NÃO libera script embutido.
|
|
69
|
+
* PURA.
|
|
70
|
+
*/
|
|
71
|
+
export declare function inlineScriptAllowed(script: InlineScript, sources: readonly string[]): boolean;
|
|
72
|
+
/** Scripts embutidos executáveis do HTML (sem `src`, conteúdo não vazio). PURA. */
|
|
73
|
+
export declare function extractInlineScripts(html: string): InlineScript[];
|
|
74
|
+
/** Políticas de `<meta http-equiv="Content-Security-Policy" content="…">`. PURA. */
|
|
75
|
+
export declare function extractMetaCspPolicies(html: string): CspPolicy[];
|
|
76
|
+
export interface InlineScriptCspAnalysis {
|
|
77
|
+
inlineScripts: number;
|
|
78
|
+
blockedInlineScripts: number;
|
|
79
|
+
blockingDirective: string | null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Quantos scripts embutidos a CSP efetiva bloqueia. Um script só roda se TODAS
|
|
83
|
+
* as políticas em vigor (cabeçalhos + meta) deixarem. PURA.
|
|
84
|
+
*/
|
|
85
|
+
export declare function analyzeInlineScriptCsp(input: {
|
|
86
|
+
cspHeaderValues: readonly string[];
|
|
87
|
+
html: string;
|
|
88
|
+
}): InlineScriptCspAnalysis;
|
|
89
|
+
/**
|
|
90
|
+
* Classifica a resposta da página inicial. PURA.
|
|
91
|
+
* - rede/não-2xx/não-HTML → `unverifiable` (não é prova de nada);
|
|
92
|
+
* - HTML com script embutido que a CSP bloqueia → `inline_scripts_blocked`;
|
|
93
|
+
* - o resto (sem CSP, CSP que libera, sem script embutido) → `ok`.
|
|
94
|
+
*/
|
|
95
|
+
export declare function classifyRenderProbe(url: string, input: RenderProbeInput): RenderCheck;
|
|
96
|
+
/** URL da página inicial do domínio publicado. `null` se o domínio não forma URL. PURA. */
|
|
97
|
+
export declare function buildRenderProbeUrl(domain: string): string | null;
|
|
98
|
+
export interface RenderProbeDeps {
|
|
99
|
+
fetchImpl: (url: string, init: RequestInit) => Promise<Response>;
|
|
100
|
+
/** Timeout por requisição, cobrindo a leitura do corpo (ms). */
|
|
101
|
+
timeoutMs: number;
|
|
102
|
+
maxRedirects?: number;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Abre a página inicial e classifica. Uma tentativa por salto (a saúde acabou
|
|
106
|
+
* de passar — não é hora de esperar app subir). `redirect:'manual'`: segue no
|
|
107
|
+
* máximo `RENDER_MAX_REDIRECTS` saltos e só na MESMA origem; redirect pra fora
|
|
108
|
+
* (login de terceiro, outro host) → `unverifiable`. Nunca lança.
|
|
109
|
+
*/
|
|
110
|
+
export declare function runRenderProbe(url: string, deps: RenderProbeDeps): Promise<RenderCheck>;
|
|
111
|
+
/** O que foi bloqueado, em uma frase (vai no `detail` do JSON e na tela). PURA. */
|
|
112
|
+
export declare function describeRenderBlock(render: RenderCheck): string;
|
|
113
|
+
/**
|
|
114
|
+
* O efeito e onde se conserta — mesma frase do alerta do Core
|
|
115
|
+
* (`renderBlockedExplanation`), sem o prefixo da saúde. PURA.
|
|
116
|
+
*/
|
|
117
|
+
export declare const RENDER_BLOCKED_FIX: string;
|