@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
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `neetru secrets` — segredos PRÓPRIOS do produto (bug_8b85353b, Parte 3 de 3).
|
|
3
|
+
*
|
|
4
|
+
* Subcomandos:
|
|
5
|
+
* neetru secrets set <CHAVE> --env <ambiente> [--generate | --from-file <caminho> | --stdin]
|
|
6
|
+
* neetru secrets list [--env <ambiente>] [--include-archived]
|
|
7
|
+
* neetru secrets archive <CHAVE> --env <ambiente>
|
|
8
|
+
*
|
|
9
|
+
* Contrato Core (core#632, `src/app/api/cli/v1/products/[slug]/secrets/route.ts`):
|
|
10
|
+
* GET /api/cli/v1/products/{slug}/secrets?environmentId=&includeArchived=
|
|
11
|
+
* — NOMES e metadado, nunca valores. Roles admin|operator|viewer.
|
|
12
|
+
* POST /api/cli/v1/products/{slug}/secrets { environmentId, key, value }
|
|
13
|
+
* | { environmentId, key, generate: true, bytes? }
|
|
14
|
+
* — cria ou troca o valor. Roles admin|operator. `prod` exige step-up MFA.
|
|
15
|
+
* DELETE /api/cli/v1/products/{slug}/secrets?environmentId=&key=
|
|
16
|
+
* — arquiva (soft delete). Roles admin|operator. `prod` exige step-up MFA.
|
|
17
|
+
*
|
|
18
|
+
* Por que existe: o Core recusa segredo em `envVars` do deploy VM (o documento
|
|
19
|
+
* do deploy é lido por toda a equipe no Firestore), e até aqui a CLI não tinha
|
|
20
|
+
* caminho nenhum para o segredo do PRÓPRIO produto — `neetru env set` é só
|
|
21
|
+
* Cloud Run. O valor sai daqui direto para o Secret Manager, por TLS, e não
|
|
22
|
+
* fica em lugar nenhum: nem no Firestore, nem no log, nem na saída desta CLI.
|
|
23
|
+
*
|
|
24
|
+
* O VALOR nunca vem do argv (ver `src/lib/segredo-produto.ts`). O grupo e os
|
|
25
|
+
* três subcomandos aceitam argumentos desconhecidos de propósito —
|
|
26
|
+
* `allowUnknownOption` — para RECUSAR com mensagem própria um `--value=...` ou
|
|
27
|
+
* um valor colado onde não devia: o erro padrão do commander ("unknown option
|
|
28
|
+
* '--value=sk_live…'", "unknown command 'sk_live…'") imprimiria o segredo de
|
|
29
|
+
* volta no terminal.
|
|
30
|
+
*
|
|
31
|
+
* ATENÇÃO (2026-10-16): a entrega ao app na VM depende do lado do AGENTE
|
|
32
|
+
* (Parte 2 de bug_8b85353b, `neetru-agent-service-vm`), que ainda não foi
|
|
33
|
+
* liberado. Até lá o registro funciona mas o segredo não chega ao app — e a
|
|
34
|
+
* CLI diz isso ao gravar, em vez de deixar o dev descobrir em produção.
|
|
35
|
+
*/
|
|
36
|
+
import type { Command } from 'commander';
|
|
37
|
+
import { type StepUpOptions } from '../lib/cli-write.js';
|
|
38
|
+
import { type TenantEnvironmentId } from '../lib/envs.js';
|
|
39
|
+
import { type FluxosDoPedido } from '../lib/segredo-produto.js';
|
|
40
|
+
type Origem = 'generated' | 'provided';
|
|
41
|
+
type Situacao = 'active' | 'arquivado';
|
|
42
|
+
/** Linha de `GET …/secrets`. O Core nunca manda valor — e só estes campos saem daqui. */
|
|
43
|
+
export interface SegredoListado {
|
|
44
|
+
key: string;
|
|
45
|
+
environmentId: TenantEnvironmentId;
|
|
46
|
+
origin: Origem;
|
|
47
|
+
status: Situacao;
|
|
48
|
+
revision: number;
|
|
49
|
+
createdAt: string;
|
|
50
|
+
updatedAt: string;
|
|
51
|
+
updatedBy: string;
|
|
52
|
+
}
|
|
53
|
+
/** Dependências de entrada — trocáveis no teste; em produção, o terminal. */
|
|
54
|
+
export interface EntradaDoValor extends FluxosDoPedido {
|
|
55
|
+
/** A entrada é um terminal interativo? (pode perguntar o valor na tela) */
|
|
56
|
+
interativo: boolean;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Só os campos públicos de uma linha. O Core já passa por `toPublicProductSecret`;
|
|
60
|
+
* filtrar de novo aqui é para um campo novo do servidor nunca chegar à tela
|
|
61
|
+
* sem alguém decidir que ele pode.
|
|
62
|
+
*/
|
|
63
|
+
export declare function linhaPublica(s: SegredoListado): SegredoListado;
|
|
64
|
+
export interface SecretsSetOptions extends StepUpOptions {
|
|
65
|
+
key?: string;
|
|
66
|
+
product?: string;
|
|
67
|
+
env?: string;
|
|
68
|
+
generate?: boolean;
|
|
69
|
+
/** Texto cru do `--bytes` (validado aqui). */
|
|
70
|
+
bytes?: string;
|
|
71
|
+
fromFile?: string;
|
|
72
|
+
stdin?: boolean;
|
|
73
|
+
/** Argumentos que sobraram depois do nome — sempre recusados. */
|
|
74
|
+
extras?: string[];
|
|
75
|
+
}
|
|
76
|
+
export declare function runSecretsSet(opts: SecretsSetOptions, entrada?: EntradaDoValor): Promise<void>;
|
|
77
|
+
export interface SecretsListOptions {
|
|
78
|
+
product?: string;
|
|
79
|
+
env?: string;
|
|
80
|
+
includeArchived?: boolean;
|
|
81
|
+
json?: boolean;
|
|
82
|
+
}
|
|
83
|
+
export declare function runSecretsList(opts: SecretsListOptions): Promise<void>;
|
|
84
|
+
export interface SecretsArchiveOptions extends StepUpOptions {
|
|
85
|
+
key?: string;
|
|
86
|
+
product?: string;
|
|
87
|
+
env?: string;
|
|
88
|
+
}
|
|
89
|
+
export declare function runSecretsArchive(opts: SecretsArchiveOptions): Promise<void>;
|
|
90
|
+
export declare function registerSecretsCommand(program: Command): void;
|
|
91
|
+
export {};
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { apiRequest } from '../lib/api-client.js';
|
|
4
|
+
import { renderTable, fmtTimestamp } from '../lib/render.js';
|
|
5
|
+
import { requireToken, handleApiError, confirmDestructive, printWriteResult, resolveStepUp, isMfaRequiredError, } from '../lib/cli-write.js';
|
|
6
|
+
import { loadProductConfig } from '../lib/product-config.js';
|
|
7
|
+
import { listarAmbientesTenant, resolverAmbienteTenant, } from '../lib/envs.js';
|
|
8
|
+
import { AVISO_AGENTE_PENDENTE, CodificacaoInvalida, PedidoCancelado, lerValorDeArquivo, lerValorDeFluxo, perguntarSemEco, problemaNaChave, problemaNaChaveReservada, problemaNoTextoDoTerminal, problemaNoValor, problemaNosBytes, } from '../lib/segredo-produto.js';
|
|
9
|
+
import { log } from '../utils/logger.js';
|
|
10
|
+
import { safeExit } from '../utils/safe-exit.js';
|
|
11
|
+
function entradaPadrao() {
|
|
12
|
+
return { entrada: process.stdin, saida: process.stderr, interativo: !!process.stdin.isTTY };
|
|
13
|
+
}
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Helpers
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
function falhar(json, codigo, mensagem) {
|
|
18
|
+
if (json)
|
|
19
|
+
console.log(JSON.stringify({ ok: false, error: codigo, message: mensagem }));
|
|
20
|
+
else
|
|
21
|
+
log.error(mensagem);
|
|
22
|
+
return safeExit(1);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Só os campos públicos de uma linha. O Core já passa por `toPublicProductSecret`;
|
|
26
|
+
* filtrar de novo aqui é para um campo novo do servidor nunca chegar à tela
|
|
27
|
+
* sem alguém decidir que ele pode.
|
|
28
|
+
*/
|
|
29
|
+
export function linhaPublica(s) {
|
|
30
|
+
return {
|
|
31
|
+
key: s.key,
|
|
32
|
+
environmentId: s.environmentId,
|
|
33
|
+
origin: s.origin,
|
|
34
|
+
status: s.status,
|
|
35
|
+
revision: s.revision,
|
|
36
|
+
createdAt: s.createdAt,
|
|
37
|
+
updatedAt: s.updatedAt,
|
|
38
|
+
updatedBy: s.updatedBy,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function textoOrigem(o) {
|
|
42
|
+
return o === 'generated' ? 'sorteado pelo Core' : 'informado';
|
|
43
|
+
}
|
|
44
|
+
function textoSituacao(s) {
|
|
45
|
+
return s === 'active' ? chalk.green('ativo') : chalk.dim('arquivado');
|
|
46
|
+
}
|
|
47
|
+
async function resolverProduto(product, json) {
|
|
48
|
+
const explicito = product?.trim();
|
|
49
|
+
if (explicito)
|
|
50
|
+
return explicito;
|
|
51
|
+
const cfg = await loadProductConfig();
|
|
52
|
+
if (cfg?.slug)
|
|
53
|
+
return cfg.slug;
|
|
54
|
+
return falhar(json, 'product_required', 'Não sei de qual produto é o segredo: passe --product <slug> ou rode o comando na pasta ' +
|
|
55
|
+
'do produto (com neetru.config.json).');
|
|
56
|
+
}
|
|
57
|
+
function exigirAmbiente(env, json) {
|
|
58
|
+
if (!env?.trim()) {
|
|
59
|
+
return falhar(json, 'environment_required', `--env é obrigatório (${listarAmbientesTenant()}). Segredo no ambiente errado não dá ` +
|
|
60
|
+
'erro: o app só não o encontra — por isso não há ambiente padrão aqui.');
|
|
61
|
+
}
|
|
62
|
+
return resolverAmbienteTenant(env, json);
|
|
63
|
+
}
|
|
64
|
+
function exigirChave(chave, json) {
|
|
65
|
+
const problema = problemaNaChave(chave);
|
|
66
|
+
if (problema)
|
|
67
|
+
return falhar(json, 'invalid_key', problema);
|
|
68
|
+
return chave;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Chave que o Core reserva (ele mesmo já a entrega ao app) — recusada ANTES
|
|
72
|
+
* de pedir o valor. Revisão do #81 (M1): antes a CLI pedia, mandava, e só o
|
|
73
|
+
* Core recusava. O `archive` não passa por aqui: só o formato, como no Core.
|
|
74
|
+
*/
|
|
75
|
+
function exigirChaveRegistravel(chave, json) {
|
|
76
|
+
const problema = problemaNaChaveReservada(chave);
|
|
77
|
+
if (problema)
|
|
78
|
+
falhar(json, 'reserved_key', problema);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Recusa argumento ou opção que o subcomando não conhece — sem repeti-lo. O
|
|
82
|
+
* erro padrão do commander ("unknown option '--value=sk_live…'", "unknown
|
|
83
|
+
* command 'sk_live…'") imprimiria o segredo de volta. Revisão do #81 (B2):
|
|
84
|
+
* vale também para o grupo (`secrets --value=… set`), `list` e `archive`.
|
|
85
|
+
*/
|
|
86
|
+
function recusarArgumentoDesconhecido(json) {
|
|
87
|
+
return falhar(json, 'unknown_argument', 'Argumento não reconhecido. O que foi digitado não é repetido aqui, caso seja um segredo; ' +
|
|
88
|
+
'se for, considere-o exposto e troque-o. Veja `neetru secrets <subcomando> --help`.');
|
|
89
|
+
}
|
|
90
|
+
/** Recusa argumento que sobrou na linha de comando — sem repeti-lo. */
|
|
91
|
+
function recusarArgumentosExtras(extras, json) {
|
|
92
|
+
if (!extras || extras.length === 0)
|
|
93
|
+
return;
|
|
94
|
+
falhar(json, 'value_in_argv', 'Argumento não reconhecido depois do nome do segredo. O valor nunca vai na linha de ' +
|
|
95
|
+
'comando (fica no histórico do shell e aparece para outros processos). Use --stdin, ' +
|
|
96
|
+
'--from-file <caminho> ou deixe a CLI perguntar o valor na tela; se o Core puder ' +
|
|
97
|
+
'sortear o valor, use --generate. Se você digitou o valor aí, considere-o exposto e ' +
|
|
98
|
+
'troque-o. (O que foi digitado não é repetido aqui.)');
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Lê o valor pela porta escolhida. `null` = `--generate` (o Core sorteia).
|
|
102
|
+
* Nunca imprime o valor, nem em erro.
|
|
103
|
+
*
|
|
104
|
+
* Nenhuma recusa (`falhar` → `safeExit`) fica DENTRO do `try`: o `safeExit`
|
|
105
|
+
* para o fluxo lançando um sinal, e um `catch` em volta o reprocessaria como
|
|
106
|
+
* erro de leitura — saída dupla e mensagem errada.
|
|
107
|
+
*/
|
|
108
|
+
async function lerValor(opts, entrada) {
|
|
109
|
+
if (opts.generate)
|
|
110
|
+
return null;
|
|
111
|
+
if (opts.stdin && entrada.interativo) {
|
|
112
|
+
// `--stdin` sem pipe leria do próprio terminal — e o terminal mostraria
|
|
113
|
+
// cada tecla. Para digitar, o caminho é o pedido sem eco.
|
|
114
|
+
return falhar(opts.json, 'stdin_is_terminal', '--stdin espera o valor por um pipe (ex.: printf %s "$VALOR" | neetru secrets set ...). ' +
|
|
115
|
+
'Para digitar o valor, rode sem --stdin: a CLI pergunta e o que você digita não aparece.');
|
|
116
|
+
}
|
|
117
|
+
const perguntarNaTela = opts.fromFile === undefined && !opts.stdin;
|
|
118
|
+
if (perguntarNaTela && (opts.json || opts.nonInteractive || !entrada.interativo)) {
|
|
119
|
+
return falhar(opts.json, 'value_source_required', 'De onde vem o valor? Sem terminal interativo, use --stdin (ex.: ' +
|
|
120
|
+
'printf %s "$VALOR" | neetru secrets set CHAVE --env prod --stdin), ' +
|
|
121
|
+
'--from-file <caminho>, ou --generate para o Core sortear.');
|
|
122
|
+
}
|
|
123
|
+
let lido;
|
|
124
|
+
try {
|
|
125
|
+
if (opts.fromFile !== undefined)
|
|
126
|
+
lido = [await lerValorDeArquivo(opts.fromFile)];
|
|
127
|
+
else if (opts.stdin)
|
|
128
|
+
lido = [await lerValorDeFluxo(entrada.entrada)];
|
|
129
|
+
else
|
|
130
|
+
lido = await perguntarSemEco(['Valor do segredo (não aparece na tela):', 'Repita o valor:'], entrada);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
const codigo = err instanceof PedidoCancelado
|
|
134
|
+
? 'cancelled'
|
|
135
|
+
: err instanceof CodificacaoInvalida
|
|
136
|
+
? 'invalid_encoding'
|
|
137
|
+
: 'value_read_failed';
|
|
138
|
+
return falhar(opts.json, codigo, err instanceof Error ? err.message : String(err));
|
|
139
|
+
}
|
|
140
|
+
// Revisão do #81 (M-r2-1): o `readline` decodifica as teclas e troca byte
|
|
141
|
+
// que não é UTF-8 por U+FFFD, sem erro — o segredo gravado seria outro.
|
|
142
|
+
// Vem antes do "não batem": as duas respostas trocadas batem entre si.
|
|
143
|
+
const problemaNaTela = perguntarNaTela ? lido.map(problemaNoTextoDoTerminal).find((p) => p !== null) : undefined;
|
|
144
|
+
if (problemaNaTela)
|
|
145
|
+
return falhar(opts.json, 'invalid_encoding', problemaNaTela);
|
|
146
|
+
if (perguntarNaTela && lido[0] !== lido[1]) {
|
|
147
|
+
return falhar(opts.json, 'value_mismatch', 'Os dois valores digitados não batem. Nada foi enviado.');
|
|
148
|
+
}
|
|
149
|
+
const valor = lido[0];
|
|
150
|
+
const problema = problemaNoValor(valor);
|
|
151
|
+
if (problema)
|
|
152
|
+
return falhar(opts.json, 'invalid_value', `${problema} Nada foi enviado.`);
|
|
153
|
+
return valor;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Step-up MFA do Core para ESCRITA em produção (dev-local/staging só pedem o
|
|
157
|
+
* papel admin|operator). Resolve ANTES do primeiro envio — a ordem é
|
|
158
|
+
* `--mfa-token` → sessão de 1h → pergunta do TOTP — para o pedido com o
|
|
159
|
+
* valor não ir ao Core só para voltar 401.
|
|
160
|
+
*/
|
|
161
|
+
async function cabecalhosDeProducao(ambiente, opts, skipSession = false) {
|
|
162
|
+
if (ambiente !== 'prod')
|
|
163
|
+
return {};
|
|
164
|
+
const { headers } = await resolveStepUp({ ...opts, dryRun: false, skipSession });
|
|
165
|
+
return headers;
|
|
166
|
+
}
|
|
167
|
+
/** O Core recusou a sessão de 1h e ainda dá para pedir um TOTP fresco? */
|
|
168
|
+
function cabeRepetirComTotp(ambiente, headers, erro) {
|
|
169
|
+
return ambiente === 'prod' && isMfaRequiredError(erro) && !headers['X-Neetru-MFA-Token'];
|
|
170
|
+
}
|
|
171
|
+
export async function runSecretsSet(opts, entrada = entradaPadrao()) {
|
|
172
|
+
recusarArgumentosExtras(opts.extras, opts.json);
|
|
173
|
+
const chave = exigirChave(opts.key, opts.json);
|
|
174
|
+
exigirChaveRegistravel(chave, opts.json);
|
|
175
|
+
const ambiente = exigirAmbiente(opts.env, opts.json);
|
|
176
|
+
const origens = [!!opts.generate, opts.fromFile !== undefined, !!opts.stdin].filter(Boolean).length;
|
|
177
|
+
if (origens > 1) {
|
|
178
|
+
falhar(opts.json, 'value_source_conflict', 'Escolha UMA origem do valor: --generate, --from-file ou --stdin.');
|
|
179
|
+
}
|
|
180
|
+
let bytes;
|
|
181
|
+
if (opts.bytes !== undefined) {
|
|
182
|
+
if (!opts.generate) {
|
|
183
|
+
falhar(opts.json, 'invalid_input', '--bytes só vale com --generate (é o tamanho do valor sorteado).');
|
|
184
|
+
}
|
|
185
|
+
bytes = Number(opts.bytes);
|
|
186
|
+
const problema = problemaNosBytes(bytes);
|
|
187
|
+
if (problema)
|
|
188
|
+
falhar(opts.json, 'invalid_input', problema);
|
|
189
|
+
}
|
|
190
|
+
const slug = await resolverProduto(opts.product, opts.json);
|
|
191
|
+
// Login ANTES do valor (revisão do #81): sem sessão, o dev não digita o
|
|
192
|
+
// segredo duas vezes só para ler "Nenhuma sessão ativa".
|
|
193
|
+
const token = await requireToken(opts.json);
|
|
194
|
+
const valor = await lerValor(opts, entrada);
|
|
195
|
+
const body = valor === null
|
|
196
|
+
? { environmentId: ambiente, key: chave, generate: true, ...(bytes !== undefined ? { bytes } : {}) }
|
|
197
|
+
: { environmentId: ambiente, key: chave, value: valor };
|
|
198
|
+
let headers = await cabecalhosDeProducao(ambiente, opts);
|
|
199
|
+
const spinner = opts.json
|
|
200
|
+
? null
|
|
201
|
+
: ora({ text: `Gravando ${chave} (${ambiente}) no Secret Manager…`, color: 'blue' }).start();
|
|
202
|
+
let res = null;
|
|
203
|
+
let falha = null;
|
|
204
|
+
try {
|
|
205
|
+
res = await apiRequest(`/api/cli/v1/products/${encodeURIComponent(slug)}/secrets`, {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
token,
|
|
208
|
+
headers,
|
|
209
|
+
body,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
catch (erro) {
|
|
213
|
+
falha = erro;
|
|
214
|
+
}
|
|
215
|
+
if (res === null && cabeRepetirComTotp(ambiente, headers, falha)) {
|
|
216
|
+
// A sessão de 1h salva foi recusada pelo Core: pede um TOTP fresco, uma vez.
|
|
217
|
+
// FORA de qualquer `try`: o fail-closed do `resolveStepUp` (safeExit) não
|
|
218
|
+
// pode ser reprocessado como erro de API (mesma regra do `withStepUpRetry`).
|
|
219
|
+
spinner?.stop();
|
|
220
|
+
headers = await cabecalhosDeProducao(ambiente, opts, true);
|
|
221
|
+
try {
|
|
222
|
+
res = await apiRequest(`/api/cli/v1/products/${encodeURIComponent(slug)}/secrets`, {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
token,
|
|
225
|
+
headers,
|
|
226
|
+
body,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
catch (erro) {
|
|
230
|
+
falha = erro;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (res === null) {
|
|
234
|
+
spinner?.fail('O Core não gravou o segredo.');
|
|
235
|
+
return handleApiError(falha, opts.json);
|
|
236
|
+
}
|
|
237
|
+
spinner?.stop();
|
|
238
|
+
const gerado = res.origin === 'generated';
|
|
239
|
+
const acao = res.created ? 'registrado' : 'trocado';
|
|
240
|
+
printWriteResult({
|
|
241
|
+
product: slug,
|
|
242
|
+
key: res.key,
|
|
243
|
+
environmentId: res.environmentId,
|
|
244
|
+
origin: res.origin,
|
|
245
|
+
revision: res.revision,
|
|
246
|
+
created: res.created,
|
|
247
|
+
secretRef: res.secretRef,
|
|
248
|
+
note: AVISO_AGENTE_PENDENTE,
|
|
249
|
+
}, `Segredo ${chalk.bold(res.key)} ${acao} em ${res.environmentId} (revisão ${res.revision})` +
|
|
250
|
+
(gerado ? ' — o Core sorteou o valor; ninguém o vê, nem você.' : '.'), opts.json);
|
|
251
|
+
if (opts.json)
|
|
252
|
+
return;
|
|
253
|
+
if (!res.created)
|
|
254
|
+
log.dim('O valor anterior deixa de ser entregue a partir do próximo deploy.');
|
|
255
|
+
log.dim(`Nome no Secret Manager: ${res.secretRef} (é o nome, não o valor).`);
|
|
256
|
+
log.dim(`Para o app receber: faça um novo deploy em ${res.environmentId}.`);
|
|
257
|
+
log.warn(AVISO_AGENTE_PENDENTE);
|
|
258
|
+
}
|
|
259
|
+
export async function runSecretsList(opts) {
|
|
260
|
+
const ambiente = opts.env?.trim() ? resolverAmbienteTenant(opts.env, opts.json) : undefined;
|
|
261
|
+
const slug = await resolverProduto(opts.product, opts.json);
|
|
262
|
+
const token = await requireToken(opts.json);
|
|
263
|
+
const qs = new URLSearchParams();
|
|
264
|
+
if (ambiente)
|
|
265
|
+
qs.set('environmentId', ambiente);
|
|
266
|
+
if (opts.includeArchived)
|
|
267
|
+
qs.set('includeArchived', 'true');
|
|
268
|
+
const sufixo = qs.toString() ? `?${qs.toString()}` : '';
|
|
269
|
+
const spinner = opts.json ? null : ora({ text: `Buscando segredos de ${slug}…`, color: 'blue' }).start();
|
|
270
|
+
let res;
|
|
271
|
+
try {
|
|
272
|
+
res = await apiRequest(`/api/cli/v1/products/${encodeURIComponent(slug)}/secrets${sufixo}`, {
|
|
273
|
+
token,
|
|
274
|
+
});
|
|
275
|
+
spinner?.stop();
|
|
276
|
+
}
|
|
277
|
+
catch (erro) {
|
|
278
|
+
spinner?.fail('Não consegui listar os segredos.');
|
|
279
|
+
handleApiError(erro, opts.json);
|
|
280
|
+
}
|
|
281
|
+
const linhas = (res.secrets ?? []).map(linhaPublica);
|
|
282
|
+
if (opts.json) {
|
|
283
|
+
console.log(JSON.stringify({ ok: true, productId: res.productId, slug: res.slug, count: linhas.length, secrets: linhas }));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
log.heading(`Segredos de ${chalk.bold(res.slug ?? slug)}${ambiente ? ` — ${ambiente}` : ''} (${linhas.length})`);
|
|
287
|
+
log.dim('Só nomes e dados de controle: o valor nunca sai do Secret Manager.');
|
|
288
|
+
if (linhas.length === 0) {
|
|
289
|
+
log.dim('Nenhum segredo registrado.');
|
|
290
|
+
log.dim('Para registrar: neetru secrets set CHAVE --env <ambiente> [--generate]');
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
renderTable([
|
|
294
|
+
{ header: 'Chave', value: (r) => r.key, identificador: true },
|
|
295
|
+
{ header: 'Ambiente', value: (r) => r.environmentId, identificador: true },
|
|
296
|
+
{ header: 'Origem', value: (r) => textoOrigem(r.origin), maxWidth: 20 },
|
|
297
|
+
{ header: 'Situação', value: (r) => textoSituacao(r.status), maxWidth: 12 },
|
|
298
|
+
{ header: 'Revisão', value: (r) => String(r.revision), maxWidth: 8 },
|
|
299
|
+
{ header: 'Atualizado em', value: (r) => fmtTimestamp(r.updatedAt), maxWidth: 20 },
|
|
300
|
+
{ header: 'Por', value: (r) => r.updatedBy, maxWidth: 32 },
|
|
301
|
+
], linhas);
|
|
302
|
+
}
|
|
303
|
+
export async function runSecretsArchive(opts) {
|
|
304
|
+
// Só o formato: uma chave que virou reservada depois de registrada precisa
|
|
305
|
+
// poder ser arquivada (mesma regra do Core em `_archiveProductSecretCore`).
|
|
306
|
+
const chave = exigirChave(opts.key, opts.json);
|
|
307
|
+
const ambiente = exigirAmbiente(opts.env, opts.json);
|
|
308
|
+
const slug = await resolverProduto(opts.product, opts.json);
|
|
309
|
+
const token = await requireToken(opts.json);
|
|
310
|
+
await confirmDestructive(`Arquivar o segredo ${chave} de ${slug} em ${ambiente}? O app deixa de recebê-lo a partir ` +
|
|
311
|
+
'do próximo deploy (a versão no Secret Manager fica guardada para rollback).', opts);
|
|
312
|
+
const qs = new URLSearchParams({ environmentId: ambiente, key: chave }).toString();
|
|
313
|
+
let headers = await cabecalhosDeProducao(ambiente, opts);
|
|
314
|
+
const spinner = opts.json ? null : ora({ text: `Arquivando ${chave} (${ambiente})…`, color: 'blue' }).start();
|
|
315
|
+
let res = null;
|
|
316
|
+
let falha = null;
|
|
317
|
+
try {
|
|
318
|
+
res = await apiRequest(`/api/cli/v1/products/${encodeURIComponent(slug)}/secrets?${qs}`, {
|
|
319
|
+
method: 'DELETE',
|
|
320
|
+
token,
|
|
321
|
+
headers,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
catch (erro) {
|
|
325
|
+
falha = erro;
|
|
326
|
+
}
|
|
327
|
+
if (res === null && cabeRepetirComTotp(ambiente, headers, falha)) {
|
|
328
|
+
// Sessão de 1h recusada: TOTP fresco, uma vez, FORA de qualquer `try`.
|
|
329
|
+
spinner?.stop();
|
|
330
|
+
headers = await cabecalhosDeProducao(ambiente, opts, true);
|
|
331
|
+
try {
|
|
332
|
+
res = await apiRequest(`/api/cli/v1/products/${encodeURIComponent(slug)}/secrets?${qs}`, {
|
|
333
|
+
method: 'DELETE',
|
|
334
|
+
token,
|
|
335
|
+
headers,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
catch (erro) {
|
|
339
|
+
falha = erro;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (res === null) {
|
|
343
|
+
spinner?.fail('O Core não arquivou o segredo.');
|
|
344
|
+
return handleApiError(falha, opts.json);
|
|
345
|
+
}
|
|
346
|
+
spinner?.stop();
|
|
347
|
+
printWriteResult({ product: slug, key: res.key, environmentId: res.environmentId, archived: res.archived }, res.archived
|
|
348
|
+
? `Segredo ${chalk.bold(res.key)} arquivado em ${res.environmentId}. Faça um novo deploy para o app deixar de recebê-lo.`
|
|
349
|
+
: `Segredo ${chalk.bold(res.key)} já estava arquivado em ${res.environmentId} — nada mudou.`, opts.json);
|
|
350
|
+
}
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
// Registro no commander
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
const AJUDA_SET = `
|
|
355
|
+
O valor NUNCA vai na linha de comando (fica no histórico do shell). Ele entra por:
|
|
356
|
+
--stdin printf %s "$VALOR" | neetru secrets set CHAVE --env prod --stdin
|
|
357
|
+
--from-file <caminho> neetru secrets set CHAVE --env prod --from-file ./valor.txt
|
|
358
|
+
pergunta na tela neetru secrets set CHAVE --env staging (o que você digita não aparece)
|
|
359
|
+
Ou o Core sorteia, e ninguém vê o valor:
|
|
360
|
+
--generate neetru secrets set NEETRU_WEBHOOK_SECRET --env prod --generate
|
|
361
|
+
|
|
362
|
+
O valor é lido em UTF-8 (com ou sem a marca BOM); arquivo em ANSI ou UTF-16 é recusado,
|
|
363
|
+
e também o que traz o caractere de substituição (U+FFFD), rastro de conversão que perdeu letra.
|
|
364
|
+
Para converter um arquivo ANSI em UTF-8:
|
|
365
|
+
Windows PowerShell 5.1 Get-Content <arquivo> | Set-Content -Encoding utf8 <arquivo-novo>
|
|
366
|
+
PowerShell 7.4 ou mais Get-Content -Encoding ansi <arquivo> | Set-Content -Encoding utf8 <arquivo-novo>
|
|
367
|
+
No Windows PowerShell 5.1, o pipe troca letra acentuada por "?" antes de chegar aqui, e a
|
|
368
|
+
CLI não tem como perceber: para valor com acento, use --from-file com arquivo em UTF-8.
|
|
369
|
+
|
|
370
|
+
Produção (--env prod) pede o código MFA: --mfa-token <código> ou \`neetru auth mfa-session\`.
|
|
371
|
+
`;
|
|
372
|
+
export function registerSecretsCommand(program) {
|
|
373
|
+
const secretsCmd = program
|
|
374
|
+
.command('secrets')
|
|
375
|
+
.description('Gerenciar segredos do próprio produto (o valor vai direto pro Secret Manager)')
|
|
376
|
+
// Revisão do #81 (B2): opção desconhecida ANTES do subcomando
|
|
377
|
+
// (`secrets --value=… set`) ou o valor no lugar do subcomando caíam no erro
|
|
378
|
+
// padrão do commander, que repete o argumento. Sem argumento nenhum, segue
|
|
379
|
+
// a ajuda, como antes. O `helpCommand(true)` é necessário porque, com
|
|
380
|
+
// `.action` no grupo, o commander desliga o subcomando implícito `help`
|
|
381
|
+
// (`secrets help set` passaria a ser recusado).
|
|
382
|
+
.helpCommand(true)
|
|
383
|
+
.allowUnknownOption()
|
|
384
|
+
.allowExcessArguments()
|
|
385
|
+
.action((_opts, cmd) => {
|
|
386
|
+
if (cmd.args.length === 0)
|
|
387
|
+
cmd.help({ error: true });
|
|
388
|
+
recusarArgumentoDesconhecido();
|
|
389
|
+
});
|
|
390
|
+
secretsCmd
|
|
391
|
+
.command('set <chave>')
|
|
392
|
+
.description('Registrar um segredo do produto ou trocar o valor dele (o valor nunca vai na linha de comando)')
|
|
393
|
+
.option('--env <ambiente>', 'ambiente: dev-local | staging | prod (obrigatório)')
|
|
394
|
+
.option('--product <slug>', 'slug do produto (padrão: o do neetru.config.json da pasta)')
|
|
395
|
+
.option('--generate', 'o Core sorteia o valor e ninguém o vê, nem você')
|
|
396
|
+
.option('--bytes <n>', 'tamanho do valor sorteado, de 16 a 128 bytes (padrão 32); só com --generate')
|
|
397
|
+
.option('--from-file <caminho>', 'lê o valor de um arquivo (uma quebra de linha no fim é ignorada)')
|
|
398
|
+
.option('--stdin', 'lê o valor da entrada padrão (uma quebra de linha no fim é ignorada)')
|
|
399
|
+
.option('--mfa-token <código>', 'código TOTP, exigido em prod (ou use `neetru auth mfa-session`)')
|
|
400
|
+
.option('--non-interactive', 'nunca pergunta nada na tela (CI)')
|
|
401
|
+
.option('--json', 'saída em JSON')
|
|
402
|
+
// Para RECUSAR `--value=…` e valor colado depois do nome sem ecoá-los: o
|
|
403
|
+
// erro padrão do commander repetiria o argumento (e o segredo) na tela.
|
|
404
|
+
.allowUnknownOption()
|
|
405
|
+
.allowExcessArguments()
|
|
406
|
+
.addHelpText('after', AJUDA_SET)
|
|
407
|
+
.action(async (chave, opts, cmd) => {
|
|
408
|
+
await runSecretsSet({
|
|
409
|
+
key: chave,
|
|
410
|
+
extras: cmd.args.slice(1),
|
|
411
|
+
env: opts.env,
|
|
412
|
+
product: opts.product,
|
|
413
|
+
generate: !!opts.generate,
|
|
414
|
+
bytes: opts.bytes,
|
|
415
|
+
fromFile: opts.fromFile,
|
|
416
|
+
stdin: !!opts.stdin,
|
|
417
|
+
mfaToken: opts.mfaToken,
|
|
418
|
+
nonInteractive: !!opts.nonInteractive,
|
|
419
|
+
json: !!opts.json,
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
secretsCmd
|
|
423
|
+
.command('list')
|
|
424
|
+
.description('Listar os segredos do produto — só nomes e dados de controle, nunca o valor')
|
|
425
|
+
.option('--env <ambiente>', 'só um ambiente: dev-local | staging | prod')
|
|
426
|
+
.option('--product <slug>', 'slug do produto (padrão: o do neetru.config.json da pasta)')
|
|
427
|
+
.option('--include-archived', 'mostra também os arquivados')
|
|
428
|
+
.option('--json', 'saída em JSON')
|
|
429
|
+
// Recusa própria, sem repetir o argumento (ver o grupo acima).
|
|
430
|
+
.allowUnknownOption()
|
|
431
|
+
.allowExcessArguments()
|
|
432
|
+
.action(async (opts, cmd) => {
|
|
433
|
+
if (cmd.args.length > 0)
|
|
434
|
+
recusarArgumentoDesconhecido(!!opts.json);
|
|
435
|
+
await runSecretsList({
|
|
436
|
+
env: opts.env,
|
|
437
|
+
product: opts.product,
|
|
438
|
+
includeArchived: !!opts.includeArchived,
|
|
439
|
+
json: !!opts.json,
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
secretsCmd
|
|
443
|
+
.command('archive <chave>')
|
|
444
|
+
.description('Arquivar um segredo: o app deixa de recebê-lo a partir do próximo deploy')
|
|
445
|
+
.option('--env <ambiente>', 'ambiente: dev-local | staging | prod (obrigatório)')
|
|
446
|
+
.option('--product <slug>', 'slug do produto (padrão: o do neetru.config.json da pasta)')
|
|
447
|
+
.option('--yes', 'pula a confirmação (necessário em --json/CI)')
|
|
448
|
+
.option('--force', 'mesmo que --yes')
|
|
449
|
+
.option('--mfa-token <código>', 'código TOTP, exigido em prod (ou use `neetru auth mfa-session`)')
|
|
450
|
+
.option('--non-interactive', 'nunca pergunta nada na tela (CI)')
|
|
451
|
+
.option('--json', 'saída em JSON')
|
|
452
|
+
// Recusa própria, sem repetir o argumento (ver o grupo acima). Também não
|
|
453
|
+
// arquiva com argumento sobrando: o commander o ignoraria calado.
|
|
454
|
+
.allowUnknownOption()
|
|
455
|
+
.allowExcessArguments()
|
|
456
|
+
.action(async (chave, opts, cmd) => {
|
|
457
|
+
if (cmd.args.length > 1)
|
|
458
|
+
recusarArgumentoDesconhecido(!!opts.json);
|
|
459
|
+
await runSecretsArchive({
|
|
460
|
+
key: chave,
|
|
461
|
+
env: opts.env,
|
|
462
|
+
product: opts.product,
|
|
463
|
+
yes: !!opts.yes,
|
|
464
|
+
force: !!opts.force,
|
|
465
|
+
mfaToken: opts.mfaToken,
|
|
466
|
+
nonInteractive: !!opts.nonInteractive,
|
|
467
|
+
json: !!opts.json,
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
//# sourceMappingURL=secrets.js.map
|
package/dist/commands/servers.js
CHANGED
|
@@ -53,7 +53,7 @@ export async function runServersList(opts) {
|
|
|
53
53
|
}
|
|
54
54
|
log.heading(`Servers (${res.count})`);
|
|
55
55
|
const columns = [
|
|
56
|
-
{ header: 'ID', value: (r) => r.id,
|
|
56
|
+
{ header: 'ID', value: (r) => r.id, identificador: true },
|
|
57
57
|
{ header: 'NOME', value: (r) => r.name ?? '—', maxWidth: 24 },
|
|
58
58
|
{ header: 'PROVIDER', value: (r) => r.provider ?? '—' },
|
|
59
59
|
{ header: 'REGIÃO', value: (r) => r.region ?? '—' },
|
|
@@ -100,7 +100,7 @@ export async function runServersFleetStatus(opts) {
|
|
|
100
100
|
return chalk.green(`${pct}%`);
|
|
101
101
|
};
|
|
102
102
|
renderTable([
|
|
103
|
-
{ header: 'ID', value: (r) => r.id,
|
|
103
|
+
{ header: 'ID', value: (r) => r.id, identificador: true },
|
|
104
104
|
{ header: 'NOME', value: (r) => r.name ?? '—', maxWidth: 20 },
|
|
105
105
|
{ header: 'ROLE', value: (r) => r.role ?? 'multi' },
|
|
106
106
|
{ header: 'STATUS', value: (r) => r.status ?? '—' },
|
|
@@ -321,6 +321,7 @@ export async function runServersDeactivate(id, opts) {
|
|
|
321
321
|
const token = await requireToken(opts.json);
|
|
322
322
|
const serverId = await pickServer(token, {
|
|
323
323
|
fromFlag: id,
|
|
324
|
+
inputs: ['<serverId>'],
|
|
324
325
|
json: opts.json,
|
|
325
326
|
message: 'Server a desativar:',
|
|
326
327
|
});
|
|
@@ -428,6 +429,7 @@ export async function runServersDispatch(id, commandType, opts) {
|
|
|
428
429
|
const token = await requireToken(opts.json);
|
|
429
430
|
const serverId = await pickServer(token, {
|
|
430
431
|
fromFlag: id,
|
|
432
|
+
inputs: ['<serverId>'],
|
|
431
433
|
json: opts.json,
|
|
432
434
|
message: 'Server alvo:',
|
|
433
435
|
});
|
|
@@ -603,13 +605,13 @@ export async function runServersPgbouncerSyncUserlist(serverId, opts = {}) {
|
|
|
603
605
|
log.success(`Userlist do PgBouncer sincronizado na VM ${resp.serverId} (mode ${resp.mode}).`);
|
|
604
606
|
}
|
|
605
607
|
renderTable([
|
|
606
|
-
{ header: 'DB', value: (r) => r.dbId,
|
|
608
|
+
{ header: 'DB', value: (r) => r.dbId, identificador: true },
|
|
607
609
|
{ header: 'PAPÉIS', value: (r) => r.roles.join(', '), maxWidth: 50 },
|
|
608
610
|
], plan.databases);
|
|
609
611
|
if (plan.skipped.length > 0) {
|
|
610
612
|
log.heading('Bancos deixados de fora (papel fora do formato)');
|
|
611
613
|
renderTable([
|
|
612
|
-
{ header: 'DB', value: (r) => r.dbId,
|
|
614
|
+
{ header: 'DB', value: (r) => r.dbId, identificador: true },
|
|
613
615
|
{ header: 'MOTIVO', value: (r) => r.reason, maxWidth: 50 },
|
|
614
616
|
], plan.skipped);
|
|
615
617
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -82,11 +82,11 @@ export async function runStatus(opts) {
|
|
|
82
82
|
}
|
|
83
83
|
console.log();
|
|
84
84
|
console.log(chalk.bold(' Last Build'));
|
|
85
|
-
console.log(` ${chalk.dim(' ID:')} ${status.lastBuildId ? chalk.cyan(status.lastBuildId
|
|
85
|
+
console.log(` ${chalk.dim(' ID:')} ${status.lastBuildId ? chalk.cyan(status.lastBuildId) : chalk.dim('—')}`);
|
|
86
86
|
console.log(` ${chalk.dim(' Status:')} ${colorStatus(status.lastBuildStatus)}`);
|
|
87
87
|
console.log();
|
|
88
88
|
console.log(chalk.bold(' Last Deployment'));
|
|
89
|
-
console.log(` ${chalk.dim(' ID:')} ${status.lastDeploymentId ? chalk.cyan(status.lastDeploymentId
|
|
89
|
+
console.log(` ${chalk.dim(' ID:')} ${status.lastDeploymentId ? chalk.cyan(status.lastDeploymentId) : chalk.dim('—')}`);
|
|
90
90
|
console.log(` ${chalk.dim(' Status:')} ${colorStatus(status.lastDeploymentStatus)}`);
|
|
91
91
|
if (status.expiresAt) {
|
|
92
92
|
const expiresIn = Math.ceil((new Date(status.expiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24));
|
package/dist/commands/support.js
CHANGED
|
@@ -66,13 +66,15 @@ export async function runSupportTicketsList(opts) {
|
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
68
|
renderTable([
|
|
69
|
-
{ header: 'ID', value: (t) => t.id,
|
|
69
|
+
{ header: 'ID', value: (t) => t.id, identificador: true },
|
|
70
70
|
{ header: 'TÍTULO', value: (t) => t.title ?? '—', maxWidth: 36 },
|
|
71
71
|
{ header: 'STATUS', value: (t) => colorStatus(t.status) },
|
|
72
72
|
{ header: 'SEVERITY', value: (t) => t.severity ?? '—' },
|
|
73
|
-
{ header: 'PRODUTO', value: (t) => t.productId ?? '—',
|
|
73
|
+
{ header: 'PRODUTO', value: (t) => t.productId ?? '—', identificador: true },
|
|
74
74
|
{ header: 'SLA', value: (t) => (t.slaBreached ? chalk.red('estourado') : 'ok') },
|
|
75
|
-
|
|
75
|
+
// uid de 28 caracteres (o mesmo que `bug list/claim --assigned-to` aceita):
|
|
76
|
+
// cortado em 22 virava um uid que não existe (revisão do #75).
|
|
77
|
+
{ header: 'ASSIGNED', value: (t) => t.assignedTo ?? chalk.dim('—'), identificador: true },
|
|
76
78
|
], res.tickets);
|
|
77
79
|
console.log();
|
|
78
80
|
}
|