@neetru/cli 2.29.0 → 2.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +141 -0
- package/README.md +63 -17
- package/dist/cli-kit/choices.d.ts +33 -0
- package/dist/cli-kit/choices.js +32 -0
- package/dist/cli-kit/command-context.d.ts +98 -0
- package/dist/cli-kit/command-context.js +108 -0
- package/dist/cli-kit/errors.d.ts +79 -0
- package/dist/cli-kit/errors.js +94 -0
- package/dist/cli-kit/index.d.ts +8 -0
- package/dist/cli-kit/index.js +8 -0
- package/dist/cli-kit/suggest.d.ts +74 -0
- package/dist/cli-kit/suggest.js +207 -0
- package/dist/commands/admin/oidc-login.js +1 -1
- package/dist/commands/admin/sdk-credential.js +1 -1
- package/dist/commands/autocomplete.d.ts +1 -1
- package/dist/commands/autocomplete.js +1 -0
- package/dist/commands/bug.d.ts +59 -0
- package/dist/commands/bug.js +114 -2
- package/dist/commands/build.d.ts +24 -1
- package/dist/commands/build.js +195 -73
- package/dist/commands/chat.d.ts +94 -1
- package/dist/commands/chat.js +258 -26
- package/dist/commands/customers.js +1 -1
- package/dist/commands/db-move.js +3 -3
- package/dist/commands/db.js +17 -24
- package/dist/commands/deploy.d.ts +136 -2
- package/dist/commands/deploy.js +547 -69
- package/dist/commands/deployments.js +11 -6
- package/dist/commands/dev-context.js +15 -5
- package/dist/commands/dev-up.d.ts +7 -5
- package/dist/commands/dev-up.js +24 -23
- package/dist/commands/docs-produto.d.ts +90 -0
- package/dist/commands/docs-produto.js +384 -0
- package/dist/commands/doctor.js +36 -1
- package/dist/commands/domains.js +85 -4
- package/dist/commands/env.d.ts +9 -3
- package/dist/commands/env.js +23 -3
- package/dist/commands/infra-read.js +2 -2
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +14 -1
- package/dist/commands/login.js +35 -8
- package/dist/commands/logs.js +12 -4
- package/dist/commands/mobile.js +33 -9
- package/dist/commands/new.d.ts +31 -1
- package/dist/commands/new.js +94 -4
- package/dist/commands/open.js +16 -6
- package/dist/commands/ops.js +1 -1
- package/dist/commands/products-db.d.ts +1 -0
- package/dist/commands/products-db.js +41 -8
- package/dist/commands/products.d.ts +17 -3
- package/dist/commands/products.js +84 -16
- package/dist/commands/promote.js +30 -5
- package/dist/commands/secrets.d.ts +91 -0
- package/dist/commands/secrets.js +471 -0
- package/dist/commands/servers.js +6 -4
- package/dist/commands/status.js +2 -2
- package/dist/commands/support.js +5 -3
- package/dist/commands/tenants.js +28 -16
- package/dist/commands/upgrade.d.ts +9 -0
- package/dist/commands/upgrade.js +50 -1
- package/dist/commands/vm-memory.js +1 -0
- package/dist/commands/webhooks.d.ts +2 -0
- package/dist/commands/webhooks.js +9 -3
- package/dist/commands/workspaces.js +42 -11
- package/dist/index.js +142 -20
- package/dist/lib/api-client.d.ts +22 -1
- package/dist/lib/api-client.js +79 -8
- package/dist/lib/api-error.d.ts +14 -0
- package/dist/lib/api-error.js +107 -46
- package/dist/lib/build-targets.d.ts +127 -0
- package/dist/lib/build-targets.js +348 -0
- package/dist/lib/cli-error.d.ts +13 -0
- package/dist/lib/cli-error.js +24 -0
- package/dist/lib/cli-write.d.ts +43 -9
- package/dist/lib/cli-write.js +154 -45
- package/dist/lib/compose-generator.d.ts +13 -9
- package/dist/lib/compose-generator.js +28 -60
- package/dist/lib/config-schema.d.ts +36 -2
- package/dist/lib/config-schema.js +11 -0
- package/dist/lib/db-local/db-json.d.ts +1 -1
- package/dist/lib/db-local/db-json.js +13 -5
- package/dist/lib/db-local/env.d.ts +15 -3
- package/dist/lib/db-local/env.js +37 -10
- package/dist/lib/db-pipeline/build-deps.d.ts +5 -1
- package/dist/lib/db-pipeline/build-deps.js +23 -3
- package/dist/lib/db-pipeline/types.d.ts +6 -2
- package/dist/lib/db-pipeline/types.js +0 -18
- package/dist/lib/deploy-target-contract.d.ts +152 -0
- package/dist/lib/deploy-target-contract.js +411 -0
- package/dist/lib/env-file-do-build.d.ts +19 -0
- package/dist/lib/env-file-do-build.js +44 -0
- package/dist/lib/envs.d.ts +108 -0
- package/dist/lib/envs.js +127 -0
- package/dist/lib/fs-seguro.d.ts +63 -0
- package/dist/lib/fs-seguro.js +108 -0
- package/dist/lib/mobile-local/mobile-json.js +10 -4
- package/dist/lib/pickers.d.ts +44 -7
- package/dist/lib/pickers.js +60 -4
- package/dist/lib/product-docs/check.d.ts +62 -0
- package/dist/lib/product-docs/check.js +489 -0
- package/dist/lib/product-docs/config.d.ts +66 -0
- package/dist/lib/product-docs/config.js +234 -0
- package/dist/lib/product-docs/dates.d.ts +17 -0
- package/dist/lib/product-docs/dates.js +41 -0
- package/dist/lib/product-docs/frontmatter.d.ts +43 -0
- package/dist/lib/product-docs/frontmatter.js +302 -0
- package/dist/lib/product-docs/git.d.ts +16 -0
- package/dist/lib/product-docs/git.js +92 -0
- package/dist/lib/product-docs/markdown.d.ts +27 -0
- package/dist/lib/product-docs/markdown.js +98 -0
- package/dist/lib/product-docs/ops.d.ts +133 -0
- package/dist/lib/product-docs/ops.js +975 -0
- package/dist/lib/product-docs/structure.d.ts +61 -0
- package/dist/lib/product-docs/structure.js +239 -0
- package/dist/lib/product-docs/sync.d.ts +107 -0
- package/dist/lib/product-docs/sync.js +322 -0
- package/dist/lib/product-docs/taxonomy.d.ts +46 -0
- package/dist/lib/product-docs/taxonomy.js +163 -0
- package/dist/lib/product-docs/templates.d.ts +53 -0
- package/dist/lib/product-docs/templates.js +245 -0
- package/dist/lib/public-secret-scan.d.ts +117 -0
- package/dist/lib/public-secret-scan.js +771 -0
- package/dist/lib/render.d.ts +16 -0
- package/dist/lib/render.js +34 -4
- package/dist/lib/segredo-produto.d.ts +124 -0
- package/dist/lib/segredo-produto.js +336 -0
- package/dist/lib/texto-utf8.d.ts +96 -0
- package/dist/lib/texto-utf8.js +196 -0
- package/package.json +2 -1
- package/dist/cli-kit/format.js.map +0 -1
- package/dist/cli-kit/glyphs.js.map +0 -1
- package/dist/cli-kit/index.js.map +0 -1
- package/dist/cli-kit/palette.js.map +0 -1
- package/dist/commands/add.js.map +0 -1
- package/dist/commands/admin/iam-check.js.map +0 -1
- package/dist/commands/admin/oidc-login.js.map +0 -1
- package/dist/commands/admin/sdk-credential.js.map +0 -1
- package/dist/commands/agent-release.js.map +0 -1
- package/dist/commands/agent-write.js.map +0 -1
- package/dist/commands/ai-models.js.map +0 -1
- package/dist/commands/ai.js.map +0 -1
- package/dist/commands/api-catalog.js.map +0 -1
- package/dist/commands/archive.js.map +0 -1
- package/dist/commands/artifact-registry.js.map +0 -1
- package/dist/commands/audit.js.map +0 -1
- package/dist/commands/auth.js.map +0 -1
- package/dist/commands/autocomplete.js.map +0 -1
- package/dist/commands/billing.js.map +0 -1
- package/dist/commands/bootstrap.js.map +0 -1
- package/dist/commands/bug.js.map +0 -1
- package/dist/commands/build.js.map +0 -1
- package/dist/commands/chat-menu.js.map +0 -1
- package/dist/commands/chat.js.map +0 -1
- package/dist/commands/cloud-run.js.map +0 -1
- package/dist/commands/code.js.map +0 -1
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/customers.js.map +0 -1
- package/dist/commands/db-move.js.map +0 -1
- package/dist/commands/db.js.map +0 -1
- package/dist/commands/deploy.js.map +0 -1
- package/dist/commands/deployments.js.map +0 -1
- package/dist/commands/dev-context.js.map +0 -1
- package/dist/commands/dev-up.js.map +0 -1
- package/dist/commands/dev.js.map +0 -1
- package/dist/commands/docs.js.map +0 -1
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/domains.js.map +0 -1
- package/dist/commands/dr.js.map +0 -1
- package/dist/commands/env.js.map +0 -1
- package/dist/commands/hosting-write.js.map +0 -1
- package/dist/commands/infra-read.js.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/login.js.map +0 -1
- package/dist/commands/logout.js.map +0 -1
- package/dist/commands/logs.js.map +0 -1
- package/dist/commands/mobile.js.map +0 -1
- package/dist/commands/mocks.js.map +0 -1
- package/dist/commands/new.js.map +0 -1
- package/dist/commands/open.js.map +0 -1
- package/dist/commands/ops.js.map +0 -1
- package/dist/commands/products-db.js.map +0 -1
- package/dist/commands/products.js.map +0 -1
- package/dist/commands/promote.js.map +0 -1
- package/dist/commands/publish.js.map +0 -1
- package/dist/commands/schema.js.map +0 -1
- package/dist/commands/servers.js.map +0 -1
- package/dist/commands/smoke.js.map +0 -1
- package/dist/commands/status.js.map +0 -1
- package/dist/commands/support.js.map +0 -1
- package/dist/commands/surface-status.js.map +0 -1
- package/dist/commands/tenants.js.map +0 -1
- package/dist/commands/ui.js.map +0 -1
- package/dist/commands/upgrade.js.map +0 -1
- package/dist/commands/validate.js.map +0 -1
- package/dist/commands/vm-memory.js.map +0 -1
- package/dist/commands/vm.js.map +0 -1
- package/dist/commands/webhooks.js.map +0 -1
- package/dist/commands/whoami.js.map +0 -1
- package/dist/commands/workspaces.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lib/agent-artifact-url.js.map +0 -1
- package/dist/lib/agent-commands.js.map +0 -1
- package/dist/lib/ai/auth-discovery.js.map +0 -1
- package/dist/lib/ai/context.js.map +0 -1
- package/dist/lib/ai/orchestrator.js.map +0 -1
- package/dist/lib/api-client.js.map +0 -1
- package/dist/lib/api-error.js.map +0 -1
- package/dist/lib/auth.js.map +0 -1
- package/dist/lib/cli-read.js.map +0 -1
- package/dist/lib/cli-write.js.map +0 -1
- package/dist/lib/code-agent-budget.js.map +0 -1
- package/dist/lib/code-agent-files.js.map +0 -1
- package/dist/lib/code-agent-gateway.js.map +0 -1
- package/dist/lib/code-agent-git-tools.js.map +0 -1
- package/dist/lib/code-agent-protocol.js.map +0 -1
- package/dist/lib/code-agent-session.js.map +0 -1
- package/dist/lib/code-agent-settings.js.map +0 -1
- package/dist/lib/code-agent-skills.js.map +0 -1
- package/dist/lib/code-agent-subagents.js.map +0 -1
- package/dist/lib/code-agent-tools.js.map +0 -1
- package/dist/lib/code-agent-tui.js.map +0 -1
- package/dist/lib/code-agent-types.js.map +0 -1
- package/dist/lib/code-agent-ui.js.map +0 -1
- package/dist/lib/code-agent-web-tools.js.map +0 -1
- package/dist/lib/code-agent-write-tools.js.map +0 -1
- package/dist/lib/compose-generator.js.map +0 -1
- package/dist/lib/config-schema.js.map +0 -1
- package/dist/lib/config.js.map +0 -1
- package/dist/lib/db-local/db-json.js.map +0 -1
- package/dist/lib/db-local/env.js.map +0 -1
- package/dist/lib/db-local/fingerprint.js.map +0 -1
- package/dist/lib/db-local/index.js.map +0 -1
- package/dist/lib/db-pipeline/build-deps.js.map +0 -1
- package/dist/lib/db-pipeline/errors.js.map +0 -1
- package/dist/lib/db-pipeline/index.js.map +0 -1
- package/dist/lib/db-pipeline/pipeline.js.map +0 -1
- package/dist/lib/db-pipeline/rehearse.js.map +0 -1
- package/dist/lib/db-pipeline/types.js.map +0 -1
- package/dist/lib/dev-local-context.js.map +0 -1
- package/dist/lib/iam-preflight.js.map +0 -1
- package/dist/lib/mobile-local/index.js.map +0 -1
- package/dist/lib/mobile-local/mobile-json.js.map +0 -1
- package/dist/lib/pickers.js.map +0 -1
- package/dist/lib/product-config.js.map +0 -1
- package/dist/lib/render.js.map +0 -1
- package/dist/lib/schema-cache.js.map +0 -1
- package/dist/lib/sse.js.map +0 -1
- package/dist/lib/telemetry.js.map +0 -1
- package/dist/lib/token-resolver.js.map +0 -1
- package/dist/lib/url-allowlist.js.map +0 -1
- package/dist/lib/version-check.js.map +0 -1
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/logo.js.map +0 -1
- package/dist/utils/safe-exit.js.map +0 -1
- package/dist/utils/spinner-registry.js.map +0 -1
- package/dist/version.js.map +0 -1
- package/templates/dev/Dockerfile.agent +0 -39
- package/templates/dev/agent-entrypoint.sh +0 -41
- package/templates/dev/docker-compose.dev.yml +0 -63
package/dist/lib/render.d.ts
CHANGED
|
@@ -4,7 +4,23 @@ export interface TableColumn<T> {
|
|
|
4
4
|
value: (row: T) => string;
|
|
5
5
|
/** Largura máxima da coluna (trunca com `…`). Default: 40. */
|
|
6
6
|
maxWidth?: number;
|
|
7
|
+
/**
|
|
8
|
+
* A célula é um IDENTIFICADOR que o usuário copia pra outro comando
|
|
9
|
+
* (`show`, `get`, `claim`…). Nunca é cortada — `maxWidth` é ignorado.
|
|
10
|
+
*
|
|
11
|
+
* 2026-10-12: as colunas ID tinham `maxWidth` de 18 a 36, mas `bug_` tem 36
|
|
12
|
+
* caracteres, `tkt_<uuid>` 40 e `tenant_<uuid>` 43. A tabela imprimia um id
|
|
13
|
+
* que NÃO EXISTE (`bug_a4f84c1b0184448cbb0…`), o usuário copiava, e o comando
|
|
14
|
+
* seguinte respondia "não encontrado" — negando a existência de algo que
|
|
15
|
+
* existe. Um id cortado não é um id mais curto; é um id falso.
|
|
16
|
+
*
|
|
17
|
+
* Célula com FORMA de identificador já não é cortada mesmo sem esta opção
|
|
18
|
+
* (ver `pareceIdentificador`). A opção continua valendo para deixar a
|
|
19
|
+
* intenção explícita na coluna e para um id que um dia não tenha essa forma.
|
|
20
|
+
*/
|
|
21
|
+
identificador?: boolean;
|
|
7
22
|
}
|
|
23
|
+
export declare function pareceIdentificador(celula: string): boolean;
|
|
8
24
|
/**
|
|
9
25
|
* Imprime uma tabela. Quando `rows` está vazio, imprime uma linha discreta.
|
|
10
26
|
*/
|
package/dist/lib/render.js
CHANGED
|
@@ -5,6 +5,30 @@
|
|
|
5
5
|
* largura máxima de cada uma, trunca células longas e pinta o header.
|
|
6
6
|
*/
|
|
7
7
|
import chalk from 'chalk';
|
|
8
|
+
// eslint-disable-next-line no-control-regex
|
|
9
|
+
const ANSI = /\u001b\[[0-9;]*m/g;
|
|
10
|
+
/**
|
|
11
|
+
* Forma de IDENTIFICADOR: um token só, sem espaço, começando por letra ou
|
|
12
|
+
* dígito — `bug_<hex>`, `tkt_<uuid>`, `tenant_<uuid>`, uid do Firebase (28),
|
|
13
|
+
* auto-id do Firestore (20), slug, e-mail, `host:porta`, caminho relativo.
|
|
14
|
+
*
|
|
15
|
+
* Revisão do PR #75: a guarda de fonte (`id-que-existe.test.ts`) só via a
|
|
16
|
+
* coluna escrita do jeito que a regex dela espera, e deixou passar a
|
|
17
|
+
* RESPONSÁVEL do `bug list` (uid de 28 cortado em 24, que `--assigned-to`
|
|
18
|
+
* aceita de volta). Decidir aqui, pelo DADO, protege a próxima coluna sem
|
|
19
|
+
* depender de como alguém a escreveu (`maxWidth` ausente, coluna em várias
|
|
20
|
+
* linhas, `chalk.cyan(r.id)`, `r.slug ?? r.id`…).
|
|
21
|
+
*
|
|
22
|
+
* Custo aceito: um slug longo, um e-mail, um hash ou uma URL (sem `?`, `&`,
|
|
23
|
+
* `=`, que a regex não aceita) também deixam de ser cortados e podem alargar
|
|
24
|
+
* a tabela. Também são handles que alguém copia; cortados, seriam tão falsos
|
|
25
|
+
* quanto um id cortado. Texto com espaço (título, descrição, mensagem)
|
|
26
|
+
* continua truncando no `maxWidth`.
|
|
27
|
+
*/
|
|
28
|
+
const FORMA_DE_IDENTIFICADOR = /^[A-Za-z0-9][A-Za-z0-9_.:@\/-]*$/;
|
|
29
|
+
export function pareceIdentificador(celula) {
|
|
30
|
+
return FORMA_DE_IDENTIFICADOR.test(celula.replace(ANSI, ''));
|
|
31
|
+
}
|
|
8
32
|
function truncate(s, max) {
|
|
9
33
|
if (s.length <= max)
|
|
10
34
|
return s;
|
|
@@ -12,10 +36,10 @@ function truncate(s, max) {
|
|
|
12
36
|
return s.slice(0, max);
|
|
13
37
|
return `${s.slice(0, max - 1)}…`;
|
|
14
38
|
}
|
|
15
|
-
// Comprimento visível ignorando ANSI escapes (chalk)
|
|
39
|
+
// Comprimento visível ignorando ANSI escapes (chalk) — a mesma `ANSI` que
|
|
40
|
+
// `pareceIdentificador` usa, em vez de uma segunda cópia da regex.
|
|
16
41
|
function visibleLength(s) {
|
|
17
|
-
|
|
18
|
-
return s.replace(/\[[0-9;]*m/g, '').length;
|
|
42
|
+
return s.replace(ANSI, '').length;
|
|
19
43
|
}
|
|
20
44
|
function padEndVisible(s, width) {
|
|
21
45
|
const len = visibleLength(s);
|
|
@@ -29,7 +53,13 @@ export function renderTable(columns, rows) {
|
|
|
29
53
|
console.log(chalk.dim(' (nenhum resultado)'));
|
|
30
54
|
return;
|
|
31
55
|
}
|
|
32
|
-
const cells = rows.map((row) => columns.map((col) =>
|
|
56
|
+
const cells = rows.map((row) => columns.map((col) => {
|
|
57
|
+
const celula = col.value(row) ?? '';
|
|
58
|
+
// Identificador nunca é cortado: pela opção da coluna OU pela forma do dado.
|
|
59
|
+
if (col.identificador || pareceIdentificador(celula))
|
|
60
|
+
return celula;
|
|
61
|
+
return truncate(celula, col.maxWidth ?? 40);
|
|
62
|
+
}));
|
|
33
63
|
const widths = columns.map((col, i) => {
|
|
34
64
|
const headerLen = col.header.length;
|
|
35
65
|
const maxCell = cells.reduce((m, r) => Math.max(m, visibleLength(r[i])), 0);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
export { CARACTERE_DE_TROCA, CodificacaoInvalida, decodificarUtf8, semBomInicial, type OrigemDosBytes, } from './texto-utf8.js';
|
|
2
|
+
/**
|
|
3
|
+
* Aviso de que o segredo registrado ainda não chega ao app na VM. Com data:
|
|
4
|
+
* quando o agente for liberado, a frase fica visivelmente velha em vez de
|
|
5
|
+
* mentir calada. Mesmo fato que o Core declara no `assertNoSecretEnvVars`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const AVISO_AGENTE_PENDENTE: string;
|
|
8
|
+
/**
|
|
9
|
+
* O comando que registra `chave` no ambiente do deploy. `env` é o `--env` cru
|
|
10
|
+
* do deploy (`dev`, `production`…): sai no nome canônico que o Core aceita, ou
|
|
11
|
+
* `<ambiente>` quando não dá para saber.
|
|
12
|
+
*/
|
|
13
|
+
export declare function comandoParaRegistrarSegredo(chave: string, env: string | undefined): string;
|
|
14
|
+
/** Nome de variável de ambiente em CAIXA_ALTA, 2 a 64 caracteres. */
|
|
15
|
+
export declare const CHAVE_SEGREDO_RE: RegExp;
|
|
16
|
+
/** Teto do valor: o do arquivo de ambiente que o agente escreve na VM. */
|
|
17
|
+
export declare const TETO_VALOR_BYTES = 4096;
|
|
18
|
+
/** Entropia aceita no `--generate` (bytes sorteados pelo Core). */
|
|
19
|
+
export declare const BYTES_GERADO_MIN = 16;
|
|
20
|
+
export declare const BYTES_GERADO_MAX = 128;
|
|
21
|
+
/**
|
|
22
|
+
* Teto do que se aceita LER (arquivo/stdin) antes de validar. Bem acima do
|
|
23
|
+
* `TETO_VALOR_BYTES` para a mensagem de "grande demais" sair certa, e baixo o
|
|
24
|
+
* bastante para um `--from-file` apontado por engano para um arquivo enorme
|
|
25
|
+
* (ou que nunca acaba, como `/dev/zero`) não encher a memória: a leitura para
|
|
26
|
+
* em `TETO_LEITURA_BYTES + 1` bytes, nunca lê o arquivo inteiro.
|
|
27
|
+
*/
|
|
28
|
+
export declare const TETO_LEITURA_BYTES: number;
|
|
29
|
+
/**
|
|
30
|
+
* Variáveis que o Core e o agente JÁ entregam ao app na VM, por outro canal.
|
|
31
|
+
* Espelho de `CORE_INJECTED_ENV_KEYS` (`neetru_core/src/lib/core-injected-env.ts`),
|
|
32
|
+
* na mesma ordem: o teste `reservadas de hoje` trava o literal e a conferência
|
|
33
|
+
* opt-in (`NEETRU_CORE_DIR`) compara com o Core.
|
|
34
|
+
*/
|
|
35
|
+
export declare const CHAVES_INJETADAS_PELO_CORE: readonly string[];
|
|
36
|
+
/** Famílias inteiras entregues por um canal do Core (hoje: módulos). Espelho de `CORE_INJECTED_ENV_PREFIXES`. */
|
|
37
|
+
export declare const PREFIXOS_INJETADOS_PELO_CORE: readonly string[];
|
|
38
|
+
/**
|
|
39
|
+
* Variáveis que mudam como o processo carrega código: um "segredo" com esse
|
|
40
|
+
* nome é execução de código na VM. Espelho de `LOADER_ENV_KEYS` do Core.
|
|
41
|
+
*/
|
|
42
|
+
export declare const CHAVES_DO_CARREGADOR: readonly string[];
|
|
43
|
+
/** O Core já entrega esta variável ao app na VM (nome exato ou família)? */
|
|
44
|
+
export declare function chaveInjetadaPeloCore(chave: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Por que o Core recusaria registrar esta chave como segredo do produto, ou
|
|
47
|
+
* `null` se ele aceita. Só chama depois de `problemaNaChave` aprovar o
|
|
48
|
+
* formato. Espelho de `assertValidProductSecretKey` (a parte da reserva).
|
|
49
|
+
*
|
|
50
|
+
* Revisão do #81 (M1): sem isto, a CLI pedia o valor, mandava, e só então o
|
|
51
|
+
* Core dizia "reservada". O nome exato é repetido na mensagem porque é um nome
|
|
52
|
+
* da lista fixa; a família `NEETRU_MODULE_` é citada pelo prefixo, sem
|
|
53
|
+
* repetir o resto do que foi digitado.
|
|
54
|
+
*/
|
|
55
|
+
export declare function problemaNaChaveReservada(chave: string): string | null;
|
|
56
|
+
/**
|
|
57
|
+
* Por que a chave é recusada, ou `null` se serve. NUNCA repete o que foi
|
|
58
|
+
* digitado: `KEY=valor` e o valor colado no lugar do nome são os dois erros
|
|
59
|
+
* mais prováveis, e ecoar a entrada imprimiria o segredo.
|
|
60
|
+
*/
|
|
61
|
+
export declare function problemaNaChave(chave: string | undefined): string | null;
|
|
62
|
+
/**
|
|
63
|
+
* Por que o valor é recusado, ou `null` se serve. Mesma regra do Core e do
|
|
64
|
+
* agente: o valor vira uma linha de `EnvironmentFile` do systemd, e o agente
|
|
65
|
+
* recusa a resposta INTEIRA (todos os segredos do produto) se um valor tiver
|
|
66
|
+
* quebra de linha, NUL, aspas, barra invertida ou espaço nas pontas.
|
|
67
|
+
*/
|
|
68
|
+
export declare function problemaNoValor(valor: string): string | null;
|
|
69
|
+
/** Valida `--bytes` (só faz sentido com `--generate`). `null` se serve. */
|
|
70
|
+
export declare function problemaNosBytes(bytes: number): string | null;
|
|
71
|
+
/**
|
|
72
|
+
* Tira UMA quebra de linha do fim (`\n` ou `\r\n`) — a que `echo`, `cat` e
|
|
73
|
+
* os editores deixam. Só uma: o resto de espaço ou quebra nas pontas é
|
|
74
|
+
* recusado por `problemaNoValor`, com mensagem, em vez de ser "consertado"
|
|
75
|
+
* em silêncio (cortar demais grava um segredo diferente do que o dev tem).
|
|
76
|
+
*/
|
|
77
|
+
export declare function semQuebraFinal(texto: string): string;
|
|
78
|
+
/**
|
|
79
|
+
* Texto que OUTRO decodificador já converteu (o `readline` do pedido na tela,
|
|
80
|
+
* ou um fluxo com `setEncoding`): os bytes originais se perderam e o U+FFFD é
|
|
81
|
+
* o único rastro de byte que não era UTF-8. Recusa quando ele aparece.
|
|
82
|
+
* Limite declarado: um U+FFFD digitado de propósito também é recusado — por
|
|
83
|
+
* esta porta não dá para distinguir.
|
|
84
|
+
*/
|
|
85
|
+
export declare function problemaNoTextoDoTerminal(texto: string): string | null;
|
|
86
|
+
/** O mínimo de um arquivo aberto que a leitura usa (o `FileHandle` do Node; trocável no teste). */
|
|
87
|
+
export interface ArquivoAberto {
|
|
88
|
+
read(buffer: Buffer, offset: number, length: number, position: number | null): Promise<{
|
|
89
|
+
bytesRead: number;
|
|
90
|
+
}>;
|
|
91
|
+
close(): Promise<void>;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Lê o valor de um arquivo. Erro de leitura vira mensagem sem conteúdo e sem
|
|
95
|
+
* o caminho (revisão do #81, B2: `--from-file <valor>` repetia o valor em
|
|
96
|
+
* "Arquivo não encontrado: <valor>").
|
|
97
|
+
*
|
|
98
|
+
* Lê no máximo `TETO_LEITURA_BYTES + 1` bytes (revisão do #81: o `readFile`
|
|
99
|
+
* de antes punha o arquivo inteiro na memória antes de conferir o teto).
|
|
100
|
+
*/
|
|
101
|
+
export declare function lerValorDeArquivo(caminho: string, abrir?: (caminho: string) => Promise<ArquivoAberto>): Promise<string>;
|
|
102
|
+
/** Lê o valor inteiro de um fluxo (a entrada padrão, no `--stdin`). */
|
|
103
|
+
export declare function lerValorDeFluxo(fluxo: NodeJS.ReadableStream): Promise<string>;
|
|
104
|
+
/** Pedido cancelado (Ctrl+C ou fim da entrada antes da resposta). */
|
|
105
|
+
export declare class PedidoCancelado extends Error {
|
|
106
|
+
constructor();
|
|
107
|
+
}
|
|
108
|
+
export interface FluxosDoPedido {
|
|
109
|
+
/** De onde vêm as teclas (o terminal). */
|
|
110
|
+
entrada: NodeJS.ReadableStream;
|
|
111
|
+
/** Onde a PERGUNTA aparece. Nunca recebe o que é digitado. */
|
|
112
|
+
saida: NodeJS.WritableStream;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Faz as perguntas SEM ECO, numa interface só (duas interfaces seguidas no
|
|
116
|
+
* mesmo fluxo perdem a linha que a primeira já tinha lido).
|
|
117
|
+
*
|
|
118
|
+
* Como o eco some: o `readline` desenha o que é digitado no `output` dele —
|
|
119
|
+
* aqui um fluxo MUDO, que descarta tudo. A pergunta vai direto para `saida`,
|
|
120
|
+
* fora do `readline`. Com `terminal: true` o `readline` também liga o modo
|
|
121
|
+
* cru do terminal (`setRawMode`), então nem o eco do próprio sistema
|
|
122
|
+
* operacional aparece.
|
|
123
|
+
*/
|
|
124
|
+
export declare function perguntarSemEco(perguntas: string[], fluxos: FluxosDoPedido): Promise<string[]>;
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entrada e validação do VALOR de um segredo próprio do produto
|
|
3
|
+
* (bug_8b85353b, Parte 3 — `neetru secrets`).
|
|
4
|
+
*
|
|
5
|
+
* REGRA DURA que molda este módulo: o valor NUNCA vem da linha de comando.
|
|
6
|
+
* Tudo o que vai no argv fica no histórico do shell (`~/.bash_history`,
|
|
7
|
+
* `PSReadLine`), aparece em `ps`/Gerenciador de Tarefas para qualquer
|
|
8
|
+
* processo da máquina e costuma ir parar em log de CI. Por isso o valor só
|
|
9
|
+
* entra por três portas:
|
|
10
|
+
*
|
|
11
|
+
* 1. `--stdin` — lido da entrada padrão (pipe);
|
|
12
|
+
* 2. `--from-file <caminho>` — lido de um arquivo;
|
|
13
|
+
* 3. prompt SEM ECO — o terminal não mostra o que é digitado.
|
|
14
|
+
*
|
|
15
|
+
* (A quarta saída — `--generate` — nem passa por aqui: o Core sorteia o valor
|
|
16
|
+
* e ninguém o vê.)
|
|
17
|
+
*
|
|
18
|
+
* E o valor NUNCA é impresso: nenhuma mensagem deste módulo repete o que foi
|
|
19
|
+
* digitado — nem quando a entrada está errada, porque a entrada errada pode
|
|
20
|
+
* ser justamente o valor colado no lugar do nome.
|
|
21
|
+
*
|
|
22
|
+
* Espelhos do Core (`neetru_core/src/lib/product-secrets.ts`, origin/main):
|
|
23
|
+
* `CHAVE_SEGREDO_RE` = `PRODUCT_SECRET_KEY_RE`, `TETO_VALOR_BYTES` =
|
|
24
|
+
* `PRODUCT_SECRET_MAX_VALUE_BYTES`, `problemaNoValor` = `productSecretValueProblem`,
|
|
25
|
+
* `BYTES_GERADO_*` = os limites de `generateProductSecretValue`, e
|
|
26
|
+
* `problemaNaChaveReservada` = `isReservedProductSecretKey` (listas de
|
|
27
|
+
* `core-injected-env.ts`). Validar aqui também é para o valor recusado não
|
|
28
|
+
* sair da máquina. Se o Core mudar a regra, ele continua sendo o juiz final —
|
|
29
|
+
* aqui é só falhar mais cedo.
|
|
30
|
+
*/
|
|
31
|
+
import { open } from 'node:fs/promises';
|
|
32
|
+
import { createInterface } from 'node:readline';
|
|
33
|
+
import { Writable } from 'node:stream';
|
|
34
|
+
import { normalizarAmbienteTenant } from './envs.js';
|
|
35
|
+
import { CARACTERE_DE_TROCA, decodificarUtf8, recusaNaoUtf8, semBomInicial } from './texto-utf8.js';
|
|
36
|
+
// A leitura estrita em UTF-8 mora em `texto-utf8.ts` (o `--env-file` do deploy e do build usa a
|
|
37
|
+
// mesma). Reexportada para quem já importava daqui.
|
|
38
|
+
export { CARACTERE_DE_TROCA, CodificacaoInvalida, decodificarUtf8, semBomInicial, } from './texto-utf8.js';
|
|
39
|
+
/**
|
|
40
|
+
* Aviso de que o segredo registrado ainda não chega ao app na VM. Com data:
|
|
41
|
+
* quando o agente for liberado, a frase fica visivelmente velha em vez de
|
|
42
|
+
* mentir calada. Mesmo fato que o Core declara no `assertNoSecretEnvVars`.
|
|
43
|
+
*/
|
|
44
|
+
export const AVISO_AGENTE_PENDENTE = 'Atenção: em 2026-10-16 o agente da VM ainda não entrega segredos do produto ao app ' +
|
|
45
|
+
'(Parte 2 de bug_8b85353b). O registro já vale e passa a chegar ao app no primeiro ' +
|
|
46
|
+
'deploy depois que o agente for atualizado.';
|
|
47
|
+
/**
|
|
48
|
+
* O comando que registra `chave` no ambiente do deploy. `env` é o `--env` cru
|
|
49
|
+
* do deploy (`dev`, `production`…): sai no nome canônico que o Core aceita, ou
|
|
50
|
+
* `<ambiente>` quando não dá para saber.
|
|
51
|
+
*/
|
|
52
|
+
export function comandoParaRegistrarSegredo(chave, env) {
|
|
53
|
+
return `neetru secrets set ${chave} --env ${normalizarAmbienteTenant(env) ?? '<ambiente>'}`;
|
|
54
|
+
}
|
|
55
|
+
/** Nome de variável de ambiente em CAIXA_ALTA, 2 a 64 caracteres. */
|
|
56
|
+
export const CHAVE_SEGREDO_RE = /^[A-Z][A-Z0-9_]{1,63}$/;
|
|
57
|
+
/** Teto do valor: o do arquivo de ambiente que o agente escreve na VM. */
|
|
58
|
+
export const TETO_VALOR_BYTES = 4096;
|
|
59
|
+
/** Entropia aceita no `--generate` (bytes sorteados pelo Core). */
|
|
60
|
+
export const BYTES_GERADO_MIN = 16;
|
|
61
|
+
export const BYTES_GERADO_MAX = 128;
|
|
62
|
+
/**
|
|
63
|
+
* Teto do que se aceita LER (arquivo/stdin) antes de validar. Bem acima do
|
|
64
|
+
* `TETO_VALOR_BYTES` para a mensagem de "grande demais" sair certa, e baixo o
|
|
65
|
+
* bastante para um `--from-file` apontado por engano para um arquivo enorme
|
|
66
|
+
* (ou que nunca acaba, como `/dev/zero`) não encher a memória: a leitura para
|
|
67
|
+
* em `TETO_LEITURA_BYTES + 1` bytes, nunca lê o arquivo inteiro.
|
|
68
|
+
*/
|
|
69
|
+
export const TETO_LEITURA_BYTES = 64 * 1024;
|
|
70
|
+
/**
|
|
71
|
+
* Variáveis que o Core e o agente JÁ entregam ao app na VM, por outro canal.
|
|
72
|
+
* Espelho de `CORE_INJECTED_ENV_KEYS` (`neetru_core/src/lib/core-injected-env.ts`),
|
|
73
|
+
* na mesma ordem: o teste `reservadas de hoje` trava o literal e a conferência
|
|
74
|
+
* opt-in (`NEETRU_CORE_DIR`) compara com o Core.
|
|
75
|
+
*/
|
|
76
|
+
export const CHAVES_INJETADAS_PELO_CORE = [
|
|
77
|
+
'PORT',
|
|
78
|
+
'NODE_ENV',
|
|
79
|
+
'HOME',
|
|
80
|
+
'NODE_OPTIONS',
|
|
81
|
+
'NEETRU_ENV',
|
|
82
|
+
'NEETRU_SDK_CLIENT_SECRET',
|
|
83
|
+
'NEETRU_CLIENT_ID',
|
|
84
|
+
'NEETRU_CLIENT_SECRET',
|
|
85
|
+
'NEETRU_OIDC_CLIENT_ID',
|
|
86
|
+
'NEETRU_OIDC_CLIENT_SECRET',
|
|
87
|
+
'NEETRU_OAUTH_CLIENT_ID',
|
|
88
|
+
'NEETRU_OAUTH_CLIENT_SECRET',
|
|
89
|
+
'NEETRU_API_KEY',
|
|
90
|
+
];
|
|
91
|
+
/** Famílias inteiras entregues por um canal do Core (hoje: módulos). Espelho de `CORE_INJECTED_ENV_PREFIXES`. */
|
|
92
|
+
export const PREFIXOS_INJETADOS_PELO_CORE = ['NEETRU_MODULE_'];
|
|
93
|
+
/**
|
|
94
|
+
* Variáveis que mudam como o processo carrega código: um "segredo" com esse
|
|
95
|
+
* nome é execução de código na VM. Espelho de `LOADER_ENV_KEYS` do Core.
|
|
96
|
+
*/
|
|
97
|
+
export const CHAVES_DO_CARREGADOR = [
|
|
98
|
+
'LD_PRELOAD',
|
|
99
|
+
'LD_LIBRARY_PATH',
|
|
100
|
+
'LD_AUDIT',
|
|
101
|
+
'DYLD_INSERT_LIBRARIES',
|
|
102
|
+
'DYLD_LIBRARY_PATH',
|
|
103
|
+
'PATH',
|
|
104
|
+
'NODE_PATH',
|
|
105
|
+
'NODE_OPTIONS',
|
|
106
|
+
];
|
|
107
|
+
/** O Core já entrega esta variável ao app na VM (nome exato ou família)? */
|
|
108
|
+
export function chaveInjetadaPeloCore(chave) {
|
|
109
|
+
return CHAVES_INJETADAS_PELO_CORE.includes(chave) || PREFIXOS_INJETADOS_PELO_CORE.some((p) => chave.startsWith(p));
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Por que o Core recusaria registrar esta chave como segredo do produto, ou
|
|
113
|
+
* `null` se ele aceita. Só chama depois de `problemaNaChave` aprovar o
|
|
114
|
+
* formato. Espelho de `assertValidProductSecretKey` (a parte da reserva).
|
|
115
|
+
*
|
|
116
|
+
* Revisão do #81 (M1): sem isto, a CLI pedia o valor, mandava, e só então o
|
|
117
|
+
* Core dizia "reservada". O nome exato é repetido na mensagem porque é um nome
|
|
118
|
+
* da lista fixa; a família `NEETRU_MODULE_` é citada pelo prefixo, sem
|
|
119
|
+
* repetir o resto do que foi digitado.
|
|
120
|
+
*/
|
|
121
|
+
export function problemaNaChaveReservada(chave) {
|
|
122
|
+
if (CHAVES_DO_CARREGADOR.includes(chave)) {
|
|
123
|
+
return (`A chave ${chave} é reservada: ela muda como o processo carrega código (bibliotecas, ` +
|
|
124
|
+
'caminhos, opções do Node) e não pode vir de um segredo do produto. Escolha outro nome.');
|
|
125
|
+
}
|
|
126
|
+
if (CHAVES_INJETADAS_PELO_CORE.includes(chave)) {
|
|
127
|
+
return (`A chave ${chave} é reservada: o Core já a entrega ao app na VM por outro canal ` +
|
|
128
|
+
'(credencial SDK, OIDC ou CLI, ambiente ou unit do app). Não precisa registrá-la; ' +
|
|
129
|
+
'para um segredo seu, escolha outro nome.');
|
|
130
|
+
}
|
|
131
|
+
const prefixo = PREFIXOS_INJETADOS_PELO_CORE.find((p) => chave.startsWith(p));
|
|
132
|
+
if (prefixo) {
|
|
133
|
+
return (`Chaves que começam com ${prefixo} são reservadas: o Core já as entrega ao app na VM ` +
|
|
134
|
+
'quando o módulo está ativo. Não precisa registrá-las; para um segredo seu, escolha ' +
|
|
135
|
+
'outro nome.');
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Por que a chave é recusada, ou `null` se serve. NUNCA repete o que foi
|
|
141
|
+
* digitado: `KEY=valor` e o valor colado no lugar do nome são os dois erros
|
|
142
|
+
* mais prováveis, e ecoar a entrada imprimiria o segredo.
|
|
143
|
+
*/
|
|
144
|
+
export function problemaNaChave(chave) {
|
|
145
|
+
if (typeof chave === 'string' && CHAVE_SEGREDO_RE.test(chave))
|
|
146
|
+
return null;
|
|
147
|
+
if (typeof chave === 'string' && chave.includes('=')) {
|
|
148
|
+
return ('Parece que você passou CHAVE=VALOR. O valor do segredo nunca vai na linha de ' +
|
|
149
|
+
'comando (fica no histórico do shell). Passe só o nome e mande o valor por ' +
|
|
150
|
+
'--stdin, --from-file ou pelo pedido na tela. Se o valor já foi digitado, ' +
|
|
151
|
+
'considere-o exposto e troque-o.');
|
|
152
|
+
}
|
|
153
|
+
return ('Nome de segredo inválido. Use CAIXA_ALTA com letras, dígitos e underscore, ' +
|
|
154
|
+
'começando por letra, de 2 a 64 caracteres (ex.: NEETRU_WEBHOOK_SECRET). ' +
|
|
155
|
+
'O que foi digitado não é repetido aqui, caso seja o próprio valor.');
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Por que o valor é recusado, ou `null` se serve. Mesma regra do Core e do
|
|
159
|
+
* agente: o valor vira uma linha de `EnvironmentFile` do systemd, e o agente
|
|
160
|
+
* recusa a resposta INTEIRA (todos os segredos do produto) se um valor tiver
|
|
161
|
+
* quebra de linha, NUL, aspas, barra invertida ou espaço nas pontas.
|
|
162
|
+
*/
|
|
163
|
+
export function problemaNoValor(valor) {
|
|
164
|
+
if (valor.length === 0)
|
|
165
|
+
return 'O valor do segredo está vazio.';
|
|
166
|
+
if (Buffer.byteLength(valor, 'utf8') > TETO_VALOR_BYTES) {
|
|
167
|
+
return `O valor do segredo passa de ${TETO_VALOR_BYTES} bytes (teto do arquivo de ambiente na VM).`;
|
|
168
|
+
}
|
|
169
|
+
if (/[\r\n\0"'\\]/.test(valor) || valor !== valor.trim()) {
|
|
170
|
+
return ('O valor do segredo tem caractere que o arquivo de ambiente da VM não aceita ' +
|
|
171
|
+
'(quebra de linha, NUL, aspas, barra invertida, ou espaço ou caractere invisível ' +
|
|
172
|
+
'nas pontas, como a marca BOM de arquivo salvo em "UTF-8 com BOM"). Para chave ' +
|
|
173
|
+
'PEM ou JSON, grave em base64 e decodifique no app.');
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
/** Valida `--bytes` (só faz sentido com `--generate`). `null` se serve. */
|
|
178
|
+
export function problemaNosBytes(bytes) {
|
|
179
|
+
if (!Number.isInteger(bytes) || bytes < BYTES_GERADO_MIN || bytes > BYTES_GERADO_MAX) {
|
|
180
|
+
return `--bytes deve ser um número inteiro de ${BYTES_GERADO_MIN} a ${BYTES_GERADO_MAX}.`;
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Tira UMA quebra de linha do fim (`\n` ou `\r\n`) — a que `echo`, `cat` e
|
|
186
|
+
* os editores deixam. Só uma: o resto de espaço ou quebra nas pontas é
|
|
187
|
+
* recusado por `problemaNoValor`, com mensagem, em vez de ser "consertado"
|
|
188
|
+
* em silêncio (cortar demais grava um segredo diferente do que o dev tem).
|
|
189
|
+
*/
|
|
190
|
+
export function semQuebraFinal(texto) {
|
|
191
|
+
if (texto.endsWith('\r\n'))
|
|
192
|
+
return texto.slice(0, -2);
|
|
193
|
+
if (texto.endsWith('\n'))
|
|
194
|
+
return texto.slice(0, -1);
|
|
195
|
+
return texto;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Texto que OUTRO decodificador já converteu (o `readline` do pedido na tela,
|
|
199
|
+
* ou um fluxo com `setEncoding`): os bytes originais se perderam e o U+FFFD é
|
|
200
|
+
* o único rastro de byte que não era UTF-8. Recusa quando ele aparece.
|
|
201
|
+
* Limite declarado: um U+FFFD digitado de propósito também é recusado — por
|
|
202
|
+
* esta porta não dá para distinguir.
|
|
203
|
+
*/
|
|
204
|
+
export function problemaNoTextoDoTerminal(texto) {
|
|
205
|
+
if (!texto.includes(CARACTERE_DE_TROCA))
|
|
206
|
+
return null;
|
|
207
|
+
return ('O terminal mandou um caractere que não está em UTF-8, e ele foi trocado por um caractere de ' +
|
|
208
|
+
'substituição: o segredo gravado seria diferente do digitado. Ajuste o terminal para UTF-8 ' +
|
|
209
|
+
'(ex.: LANG=pt_BR.UTF-8), ou grave o valor num arquivo UTF-8 e use --from-file. ' +
|
|
210
|
+
'O conteúdo não é repetido aqui. Nada foi enviado.');
|
|
211
|
+
}
|
|
212
|
+
/** Nunca repete o caminho: o engano provável é colar o VALOR no lugar dele. */
|
|
213
|
+
const CAMINHO_NAO_REPETIDO = '(o caminho não é repetido aqui, caso seja o próprio valor)';
|
|
214
|
+
function mensagemDeLeitura(err) {
|
|
215
|
+
const codigo = err?.code;
|
|
216
|
+
if (codigo === 'ENOENT')
|
|
217
|
+
return `Arquivo não encontrado ${CAMINHO_NAO_REPETIDO}.`;
|
|
218
|
+
if (codigo === 'EISDIR')
|
|
219
|
+
return `O caminho é uma pasta, não um arquivo ${CAMINHO_NAO_REPETIDO}.`;
|
|
220
|
+
return `Não consegui ler o arquivo (${codigo ?? 'erro desconhecido'}) ${CAMINHO_NAO_REPETIDO}.`;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Lê o valor de um arquivo. Erro de leitura vira mensagem sem conteúdo e sem
|
|
224
|
+
* o caminho (revisão do #81, B2: `--from-file <valor>` repetia o valor em
|
|
225
|
+
* "Arquivo não encontrado: <valor>").
|
|
226
|
+
*
|
|
227
|
+
* Lê no máximo `TETO_LEITURA_BYTES + 1` bytes (revisão do #81: o `readFile`
|
|
228
|
+
* de antes punha o arquivo inteiro na memória antes de conferir o teto).
|
|
229
|
+
*/
|
|
230
|
+
export async function lerValorDeArquivo(caminho, abrir = (c) => open(c, 'r')) {
|
|
231
|
+
const bruto = Buffer.alloc(TETO_LEITURA_BYTES + 1);
|
|
232
|
+
let lidos = 0;
|
|
233
|
+
let arquivo;
|
|
234
|
+
try {
|
|
235
|
+
arquivo = await abrir(caminho);
|
|
236
|
+
while (lidos < bruto.length) {
|
|
237
|
+
const { bytesRead } = await arquivo.read(bruto, lidos, bruto.length - lidos, null);
|
|
238
|
+
if (bytesRead === 0)
|
|
239
|
+
break;
|
|
240
|
+
lidos += bytesRead;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
throw new Error(mensagemDeLeitura(err));
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
await arquivo?.close().catch(() => undefined);
|
|
248
|
+
}
|
|
249
|
+
if (lidos > TETO_LEITURA_BYTES) {
|
|
250
|
+
throw new Error(`O arquivo passa de ${TETO_LEITURA_BYTES} bytes — não parece um segredo.`);
|
|
251
|
+
}
|
|
252
|
+
return semQuebraFinal(semBomInicial(decodificarUtf8(bruto.subarray(0, lidos), 'arquivo')));
|
|
253
|
+
}
|
|
254
|
+
/** Lê o valor inteiro de um fluxo (a entrada padrão, no `--stdin`). */
|
|
255
|
+
export async function lerValorDeFluxo(fluxo) {
|
|
256
|
+
const partes = [];
|
|
257
|
+
let total = 0;
|
|
258
|
+
for await (const pedaco of fluxo) {
|
|
259
|
+
// Pedaço em texto = alguém já decodificou o fluxo (`setEncoding`), e o
|
|
260
|
+
// U+FFFD é o único rastro de byte trocado. Os pedaços em bytes são
|
|
261
|
+
// conferidos JUNTOS no fim: uma letra acentuada pode vir partida em dois.
|
|
262
|
+
// Revisão do #81 (M-r3-1): o U+FFFD que chega em bytes (EF BF BD, a
|
|
263
|
+
// conversão com perda feita antes da CLI) também é recusado, lá no fim,
|
|
264
|
+
// por `decodificarUtf8` — a regra não depende mais do formato do pedaço.
|
|
265
|
+
if (typeof pedaco === 'string' && pedaco.includes(CARACTERE_DE_TROCA))
|
|
266
|
+
throw recusaNaoUtf8('entrada');
|
|
267
|
+
const buf = typeof pedaco === 'string' ? Buffer.from(pedaco, 'utf8') : pedaco;
|
|
268
|
+
total += buf.length;
|
|
269
|
+
if (total > TETO_LEITURA_BYTES) {
|
|
270
|
+
throw new Error(`A entrada passa de ${TETO_LEITURA_BYTES} bytes — não parece um segredo.`);
|
|
271
|
+
}
|
|
272
|
+
partes.push(buf);
|
|
273
|
+
}
|
|
274
|
+
return semQuebraFinal(semBomInicial(decodificarUtf8(Buffer.concat(partes), 'entrada')));
|
|
275
|
+
}
|
|
276
|
+
/** Pedido cancelado (Ctrl+C ou fim da entrada antes da resposta). */
|
|
277
|
+
export class PedidoCancelado extends Error {
|
|
278
|
+
constructor() {
|
|
279
|
+
super('Pedido cancelado — nada foi enviado.');
|
|
280
|
+
this.name = 'PedidoCancelado';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Faz as perguntas SEM ECO, numa interface só (duas interfaces seguidas no
|
|
285
|
+
* mesmo fluxo perdem a linha que a primeira já tinha lido).
|
|
286
|
+
*
|
|
287
|
+
* Como o eco some: o `readline` desenha o que é digitado no `output` dele —
|
|
288
|
+
* aqui um fluxo MUDO, que descarta tudo. A pergunta vai direto para `saida`,
|
|
289
|
+
* fora do `readline`. Com `terminal: true` o `readline` também liga o modo
|
|
290
|
+
* cru do terminal (`setRawMode`), então nem o eco do próprio sistema
|
|
291
|
+
* operacional aparece.
|
|
292
|
+
*/
|
|
293
|
+
export function perguntarSemEco(perguntas, fluxos) {
|
|
294
|
+
const mudo = new Writable({
|
|
295
|
+
write(_pedaco, _codificacao, pronto) {
|
|
296
|
+
pronto();
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
const rl = createInterface({ input: fluxos.entrada, output: mudo, terminal: true });
|
|
300
|
+
const respostas = [];
|
|
301
|
+
return new Promise((resolve, reject) => {
|
|
302
|
+
let terminado = false;
|
|
303
|
+
const encerrar = (erro) => {
|
|
304
|
+
if (terminado)
|
|
305
|
+
return;
|
|
306
|
+
terminado = true;
|
|
307
|
+
rl.close();
|
|
308
|
+
if (erro)
|
|
309
|
+
reject(erro);
|
|
310
|
+
else
|
|
311
|
+
resolve(respostas);
|
|
312
|
+
};
|
|
313
|
+
// Ctrl+C em modo cru não vira sinal: chega aqui. Sem este ouvinte o
|
|
314
|
+
// readline só pausa, e o comando ficaria pendurado.
|
|
315
|
+
rl.on('SIGINT', () => {
|
|
316
|
+
fluxos.saida.write('\n');
|
|
317
|
+
encerrar(new PedidoCancelado());
|
|
318
|
+
});
|
|
319
|
+
rl.on('close', () => encerrar(new PedidoCancelado()));
|
|
320
|
+
const proxima = (i) => {
|
|
321
|
+
if (i >= perguntas.length) {
|
|
322
|
+
encerrar();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
fluxos.saida.write(` ${perguntas[i]} `);
|
|
326
|
+
rl.question('', (resposta) => {
|
|
327
|
+
// O Enter também não foi ecoado: a quebra de linha é nossa.
|
|
328
|
+
fluxos.saida.write('\n');
|
|
329
|
+
respostas.push(resposta);
|
|
330
|
+
proxima(i + 1);
|
|
331
|
+
});
|
|
332
|
+
};
|
|
333
|
+
proxima(0);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
//# sourceMappingURL=segredo-produto.js.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O texto não está em UTF-8 (ou traz U+FFFD) — recusa com o código
|
|
3
|
+
* `invalid_encoding`. A mensagem diz como salvar o arquivo, nunca o que ele
|
|
4
|
+
* contém.
|
|
5
|
+
*/
|
|
6
|
+
export declare class CodificacaoInvalida extends Error {
|
|
7
|
+
constructor(mensagem: string);
|
|
8
|
+
}
|
|
9
|
+
/** U+FFFD: o caractere que um decodificador UTF-8 põe no lugar de byte que não é UTF-8. */
|
|
10
|
+
export declare const CARACTERE_DE_TROCA = "\uFFFD";
|
|
11
|
+
/**
|
|
12
|
+
* De onde vieram os bytes (só muda o texto da recusa): `arquivo` e `entrada`
|
|
13
|
+
* são o `--from-file` e o `--stdin` do segredo; `env-file` é o `--env-file`
|
|
14
|
+
* do deploy e do build.
|
|
15
|
+
*/
|
|
16
|
+
export type OrigemDosBytes = 'arquivo' | 'entrada' | 'env-file';
|
|
17
|
+
/**
|
|
18
|
+
* Tira UMA marca BOM (U+FEFF) do começo — a que o Bloco de Notas ("UTF-8 com
|
|
19
|
+
* BOM") e o `Set-Content -Encoding utf8` do Windows PowerShell 5.1 gravam no
|
|
20
|
+
* arquivo (medido: `ef bb bf …`). O PowerShell 7 grava sem a marca. Revisão do
|
|
21
|
+
* #81 (B3): sem isto o `trim()` de `problemaNoValor` via a marca como espaço
|
|
22
|
+
* e recusava com "espaço nas pontas", sem espaço nenhum à vista. Só uma, pelo
|
|
23
|
+
* mesmo motivo de `semQuebraFinal`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function semBomInicial(texto: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Como converter um arquivo ANSI para UTF-8 — uma receita por versão do
|
|
28
|
+
* PowerShell, porque o `Get-Content` lê diferente nas duas.
|
|
29
|
+
*
|
|
30
|
+
* Revisão do #81 (M-r3-1): a receita era só a do 5.1. No Windows PowerShell
|
|
31
|
+
* 5.1, o `Get-Content` lê arquivo sem marca em ANSI, e o `Set-Content
|
|
32
|
+
* -Encoding utf8` grava UTF-8 COM a marca BOM (`semBomInicial` a tira). No
|
|
33
|
+
* PowerShell 7, o `Get-Content` lê arquivo sem marca em UTF-8 (o padrão é
|
|
34
|
+
* `utf8NoBOM`): o "é" do ANSI vira U+FFFD antes de chegar à CLI, e o
|
|
35
|
+
* `Set-Content -Encoding utf8` (no 7, sem BOM) grava esse U+FFFD em UTF-8
|
|
36
|
+
* válido. Lá a leitura certa é `-Encoding ansi` (página ANSI da cultura,
|
|
37
|
+
* existe a partir do 7.4, segundo a documentação do `Get-Content`). No 5.1,
|
|
38
|
+
* `-Encoding ansi` dá erro de parâmetro e não cria o arquivo novo (medido), e
|
|
39
|
+
* a receita do 5.1 rodada no 7 grava U+FFFD — que `decodificarUtf8` recusa.
|
|
40
|
+
* Assim, a receita da versão errada nunca passa calada.
|
|
41
|
+
*/
|
|
42
|
+
export declare const COMO_CONVERTER_ANSI: string;
|
|
43
|
+
/**
|
|
44
|
+
* UTF-16 tem marca (FF FE / FE FF), e a marca vence o `-Encoding` na leitura
|
|
45
|
+
* (medido no 5.1: o arquivo do `>` lido com `-Encoding UTF8` sai certo). Por
|
|
46
|
+
* isso a mesma receita serve nas duas versões.
|
|
47
|
+
*/
|
|
48
|
+
export declare const COMO_CONVERTER_UTF16: string;
|
|
49
|
+
/** Exportada para o `--stdin` que já chega decodificado (`setEncoding`). */
|
|
50
|
+
export declare function recusaNaoUtf8(origem: OrigemDosBytes): CodificacaoInvalida;
|
|
51
|
+
/**
|
|
52
|
+
* O que impede estes bytes de serem lidos como o arquivo de origem — ou `null`
|
|
53
|
+
* quando são UTF-8 limpo:
|
|
54
|
+
*
|
|
55
|
+
* 1. `utf16`: marca UTF-16 (FF FE / FE FF) — antes caía em recusa que
|
|
56
|
+
* citava "UTF-8 com BOM" (segredo) ou virava chave com NUL (env-file);
|
|
57
|
+
* 2. `nao-utf8`: bytes que não são UTF-8 (`isUtf8`) — o ANSI do 5.1;
|
|
58
|
+
* 3. `troca-gravada`: U+FFFD em UTF-8 válido (revisão do #81, M-r3-1) — a
|
|
59
|
+
* conversão com perda feita antes da CLI. Limite declarado: um U+FFFD
|
|
60
|
+
* gravado de propósito cai aqui também (não dá para distinguir).
|
|
61
|
+
*
|
|
62
|
+
* A marca BOM do UTF-8 (EF BB BF) é UTF-8 válido: não é problema.
|
|
63
|
+
*
|
|
64
|
+
* Fonte única da regra: `decodificarUtf8` recusa por ela, e o `env switch` e a
|
|
65
|
+
* varredura de segredo público decidem por ela (rodada final 2 do #81).
|
|
66
|
+
*/
|
|
67
|
+
export type ProblemaDeCodificacao = 'utf16' | 'nao-utf8' | 'troca-gravada';
|
|
68
|
+
export declare function problemaDeCodificacao(bruto: Buffer): ProblemaDeCodificacao | null;
|
|
69
|
+
/**
|
|
70
|
+
* Converte os bytes lidos em texto, RECUSANDO o que não é o arquivo de origem
|
|
71
|
+
* (os três casos de `problemaDeCodificacao`). No U+FFFD, a CLI fica mais
|
|
72
|
+
* estrita que o Core.
|
|
73
|
+
*
|
|
74
|
+
* A marca BOM do UTF-8 (EF BB BF) passa daqui, e quem chama a tira com
|
|
75
|
+
* `semBomInicial`.
|
|
76
|
+
*/
|
|
77
|
+
export declare function decodificarUtf8(bruto: Buffer, origem: OrigemDosBytes): string;
|
|
78
|
+
/**
|
|
79
|
+
* `neetru env switch` (rodada final 2 do #81, achado BAIXA): o comando lia o
|
|
80
|
+
* `.env.local` com `readFile(…, 'utf-8')` e REGRAVAVA o arquivo inteiro. Medido
|
|
81
|
+
* no processo real: um `.env.local` ANSI com `NOME_LOJA=Café` terminava com
|
|
82
|
+
* `ef bf bd` no lugar do `e9`, e um UTF-16 virava texto ilegível — exit 0 e
|
|
83
|
+
* "✓" nos dois. Agora ele recusa ANTES de gravar e não toca no arquivo.
|
|
84
|
+
*
|
|
85
|
+
* Recusa só o que a regravação estragaria (`utf16` e `nao-utf8`). Um U+FFFD
|
|
86
|
+
* que já está no arquivo (`troca-gravada`) sai byte a byte igual — o comando
|
|
87
|
+
* não perde nada ali, então segue.
|
|
88
|
+
*/
|
|
89
|
+
export declare function recusaDoEnvSwitch(arquivo: string, problema: 'utf16' | 'nao-utf8'): CodificacaoInvalida;
|
|
90
|
+
/**
|
|
91
|
+
* Varredura de segredo público do `neetru deploy` (rodada final 2 do #81): um
|
|
92
|
+
* `.env*` que o bundler lê não está em UTF-8 limpo. A varredura segue — ela
|
|
93
|
+
* confere o valor como o bundler o lê —, mas o app vai receber outro valor, e
|
|
94
|
+
* isso não passa calado. Nunca repete o conteúdo.
|
|
95
|
+
*/
|
|
96
|
+
export declare function avisoEnvDoBundlerForaDeUtf8(arquivo: string, problema: ProblemaDeCodificacao): string;
|