@neetru/cli 2.31.0 → 2.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +91 -2
- package/README.md +2 -0
- 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 +91 -2
- package/dist/commands/deploy.js +414 -69
- package/dist/commands/deployments.js +11 -6
- package/dist/commands/dev-context.js +15 -5
- package/dist/commands/docs-produto.d.ts +25 -0
- package/dist/commands/docs-produto.js +70 -8
- 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.js +7 -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 +29 -1
- package/dist/commands/new.js +92 -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 +70 -14
- package/dist/lib/api-client.d.ts +14 -0
- package/dist/lib/api-client.js +59 -6
- 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/config-schema.d.ts +8 -8
- 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/sync.d.ts +6 -0
- package/dist/lib/product-docs/sync.js +10 -1
- package/dist/lib/public-secret-scan.d.ts +44 -3
- package/dist/lib/public-secret-scan.js +361 -36
- 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-produto.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/product-docs/check.js.map +0 -1
- package/dist/lib/product-docs/config.js.map +0 -1
- package/dist/lib/product-docs/dates.js.map +0 -1
- package/dist/lib/product-docs/frontmatter.js.map +0 -1
- package/dist/lib/product-docs/git.js.map +0 -1
- package/dist/lib/product-docs/markdown.js.map +0 -1
- package/dist/lib/product-docs/ops.js.map +0 -1
- package/dist/lib/product-docs/structure.js.map +0 -1
- package/dist/lib/product-docs/sync.js.map +0 -1
- package/dist/lib/product-docs/taxonomy.js.map +0 -1
- package/dist/lib/product-docs/templates.js.map +0 -1
- package/dist/lib/public-secret-scan.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
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Erro tipado da CLI (bug_89d2497e, etapa 3).
|
|
3
|
+
*
|
|
4
|
+
* Antes cada comando decidia sozinho como morrer: montava a mensagem à mão,
|
|
5
|
+
* escolhia um `safeExit(n)` e, em `--json`, um formato de payload. O modelo é o
|
|
6
|
+
* `FirebaseError` do firebase-tools (`lib/error.js`): o erro carrega o código
|
|
7
|
+
* de saída, a causa original e os filhos agregados — e quem imprime/encerra é
|
|
8
|
+
* um ponto só (`exitWithCliError`, em `src/lib/cli-error.ts`).
|
|
9
|
+
*
|
|
10
|
+
* Função pura: nada aqui imprime nem encerra o processo. O cli-kit continua sem
|
|
11
|
+
* dependência de logger/process — quem decide a saída é o call site.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Convenção ÚNICA de códigos de saída (a mesma do `handleApiError` desde a
|
|
15
|
+
* auditoria de 2026-06-20 — ver `src/lib/api-error.ts`):
|
|
16
|
+
* 1 — erro do cliente (entrada/recurso: 400, 404, 409, 422, validação)
|
|
17
|
+
* 2 — não autenticado (401)
|
|
18
|
+
* 3 — sem permissão (403)
|
|
19
|
+
* 4 — erro do servidor (5xx), rede ou desconhecido
|
|
20
|
+
*/
|
|
21
|
+
export const EXIT_CODES = {
|
|
22
|
+
client: 1,
|
|
23
|
+
unauthenticated: 2,
|
|
24
|
+
forbidden: 3,
|
|
25
|
+
server: 4,
|
|
26
|
+
};
|
|
27
|
+
export class NeetruCliError extends Error {
|
|
28
|
+
code;
|
|
29
|
+
exit;
|
|
30
|
+
children;
|
|
31
|
+
succeeded;
|
|
32
|
+
details;
|
|
33
|
+
human;
|
|
34
|
+
constructor(message, opts = {}) {
|
|
35
|
+
super(message, opts.cause !== undefined ? { cause: opts.cause } : undefined);
|
|
36
|
+
this.name = 'NeetruCliError';
|
|
37
|
+
this.code = opts.code ?? 'cli_error';
|
|
38
|
+
this.children = [...(opts.children ?? [])];
|
|
39
|
+
this.succeeded = Math.max(0, opts.succeeded ?? 0);
|
|
40
|
+
this.details = { ...(opts.details ?? {}) };
|
|
41
|
+
this.human = opts.human ?? message;
|
|
42
|
+
this.exit =
|
|
43
|
+
opts.exit ??
|
|
44
|
+
(this.children.length > 0
|
|
45
|
+
? Math.max(...this.children.map((c) => c.exit))
|
|
46
|
+
: EXIT_CODES.client);
|
|
47
|
+
}
|
|
48
|
+
/** Falha parcial: parte do lote falhou e parte deu certo. */
|
|
49
|
+
get partial() {
|
|
50
|
+
return this.children.length > 0 && this.succeeded > 0;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Payload `--json`. Sem filhos, é exatamente `{ ok:false, error, ...details,
|
|
54
|
+
* message }` — o formato que os comandos já emitiam. Com filhos, acrescenta
|
|
55
|
+
* `partial`, `succeeded`, `failed` e `children`.
|
|
56
|
+
*/
|
|
57
|
+
toJSON() {
|
|
58
|
+
return { ok: false, ...this.corpoJson() };
|
|
59
|
+
}
|
|
60
|
+
corpoJson() {
|
|
61
|
+
const base = {
|
|
62
|
+
error: this.code,
|
|
63
|
+
...this.details,
|
|
64
|
+
message: this.message,
|
|
65
|
+
};
|
|
66
|
+
if (this.children.length === 0)
|
|
67
|
+
return base;
|
|
68
|
+
return {
|
|
69
|
+
...base,
|
|
70
|
+
partial: this.partial,
|
|
71
|
+
succeeded: this.succeeded,
|
|
72
|
+
failed: this.children.length,
|
|
73
|
+
children: this.children.map((c) => c.corpoJson()),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function isNeetruCliError(x) {
|
|
78
|
+
return x instanceof NeetruCliError;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Texto humano completo do erro, com os filhos indentados. Uma falha parcial
|
|
82
|
+
* diz que é parcial e quanto deu certo.
|
|
83
|
+
*/
|
|
84
|
+
export function renderCliError(error) {
|
|
85
|
+
if (error.children.length === 0)
|
|
86
|
+
return error.human;
|
|
87
|
+
const falhas = error.children.length;
|
|
88
|
+
const resumo = error.partial
|
|
89
|
+
? `Falha parcial: ${falhas} de ${falhas + error.succeeded} itens falharam (${error.succeeded} deram certo).`
|
|
90
|
+
: `${falhas} ${falhas === 1 ? 'item falhou' : 'itens falharam'}.`;
|
|
91
|
+
const filhos = error.children.map((c) => ` - ${renderCliError(c).split('\n').join('\n ')}`);
|
|
92
|
+
return [error.human, ` ${resumo}`, ...filhos].join('\n');
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=errors.js.map
|
package/dist/cli-kit/index.d.ts
CHANGED
|
@@ -11,3 +11,11 @@ export type { Glyphs } from './glyphs.js';
|
|
|
11
11
|
export { ok, err, warn, dim, heading } from './palette.js';
|
|
12
12
|
export { step, errorBlock, sacredPause, } from './format.js';
|
|
13
13
|
export type { ErrorBlockOpts, SacredPauseOpts } from './format.js';
|
|
14
|
+
export { EXIT_CODES, NeetruCliError, isNeetruCliError, renderCliError } from './errors.js';
|
|
15
|
+
export type { NeetruCliErrorOptions } from './errors.js';
|
|
16
|
+
export { invalidChoiceError, formatChoices } from './choices.js';
|
|
17
|
+
export type { InvalidChoiceInput } from './choices.js';
|
|
18
|
+
export { installCommandContext, setCurrentCommand, getCurrentCommand, setRootCommand, getRootCommand, commandDeclaresFlag, commandDeclaresInput, declaredFlags, declaredInputs, commandExists, adviseFlag, adviseCommand, } from './command-context.js';
|
|
19
|
+
export type { FlagAdvice, CommandAdvice } from './command-context.js';
|
|
20
|
+
export { SYNONYMS, MIN_RATIO, MAX_SUGGESTIONS, isSubcommandVerb, wordScore, scoreCandidates, suggestCommands, suggestSubcommands, reservedVerbError, } from './suggest.js';
|
|
21
|
+
export type { ReservedVerbInput } from './suggest.js';
|
package/dist/cli-kit/index.js
CHANGED
|
@@ -9,4 +9,12 @@
|
|
|
9
9
|
export { GLYPHS } from './glyphs.js';
|
|
10
10
|
export { ok, err, warn, dim, heading } from './palette.js';
|
|
11
11
|
export { step, errorBlock, sacredPause, } from './format.js';
|
|
12
|
+
// Camada de CORREÇÃO (bug_89d2497e): erro tipado, conselho validado contra o
|
|
13
|
+
// comando em execução e valores válidos de enum vindos da fonte real.
|
|
14
|
+
export { EXIT_CODES, NeetruCliError, isNeetruCliError, renderCliError } from './errors.js';
|
|
15
|
+
export { invalidChoiceError, formatChoices } from './choices.js';
|
|
16
|
+
export { installCommandContext, setCurrentCommand, getCurrentCommand, setRootCommand, getRootCommand, commandDeclaresFlag, commandDeclaresInput, declaredFlags, declaredInputs, commandExists, adviseFlag, adviseCommand, } from './command-context.js';
|
|
17
|
+
// P105: verbo de subcomando nunca é valor livre; sugestão com piso de qualidade
|
|
18
|
+
// (SYNONYMS + MIN_RATIO, o `suggest_commands` do gcloud).
|
|
19
|
+
export { SYNONYMS, MIN_RATIO, MAX_SUGGESTIONS, isSubcommandVerb, wordScore, scoreCandidates, suggestCommands, suggestSubcommands, reservedVerbError, } from './suggest.js';
|
|
12
20
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { NeetruCliError } from './errors.js';
|
|
2
|
+
/** Verbo digitado → verbo(s) que a CLI usa. */
|
|
3
|
+
export declare const SYNONYMS: Readonly<Record<string, readonly string[]>>;
|
|
4
|
+
/** Mínimo da razão pontuação/pontuação-do-primeiro para entrar na sugestão. */
|
|
5
|
+
export declare const MIN_RATIO = 0.7;
|
|
6
|
+
/** No máximo isto de sugestões. */
|
|
7
|
+
export declare const MAX_SUGGESTIONS = 10;
|
|
8
|
+
/**
|
|
9
|
+
* A palavra é, de fato, verbo de subcomando DESTE comando? Sim quando é um dos
|
|
10
|
+
* subcomandos dados (nome ou apelido) ou um sinônimo (`SYNONYMS`) de um deles.
|
|
11
|
+
* Sem diferenciar caixa, sem espaços nas pontas.
|
|
12
|
+
*
|
|
13
|
+
* Revisão do #87 (BAIXO 6): a 1ª versão recusava uma lista GLOBAL de verbos em
|
|
14
|
+
* qualquer texto livre, e `neetru chat send show` (resposta comum em PT-BR)
|
|
15
|
+
* passou a ser recusado sem que `show` fosse subcomando de `chat`. A recusa
|
|
16
|
+
* agora depende dos subcomandos de verdade de quem pergunta: para `chat send`,
|
|
17
|
+
* só `channels`, `identity`/`apelido`, `send` e `history`; para
|
|
18
|
+
* `chat identity`, os verbos dele (`show`, `set`, `clear`…) e seus sinônimos.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isSubcommandVerb(word: string, subcommands: readonly string[]): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Pontuação de uma palavra digitada contra uma palavra de um comando — o
|
|
23
|
+
* `_WordScore` do gcloud, linha a linha. Sempre ≥ 0; zero = sem relação.
|
|
24
|
+
*/
|
|
25
|
+
export declare function wordScore(index: number, word: string, canonical: string, canonicalLength: number): number;
|
|
26
|
+
/**
|
|
27
|
+
* Candidatos pontuados, do maior para o menor (empate: ordem lexicográfica).
|
|
28
|
+
* Só entra quem pontua > 0. Cada candidato é a lista de palavras do comando.
|
|
29
|
+
*/
|
|
30
|
+
export declare function scoreCandidates(words: readonly string[], candidates: ReadonlyArray<readonly string[]>): Array<{
|
|
31
|
+
command: readonly string[];
|
|
32
|
+
score: number;
|
|
33
|
+
}>;
|
|
34
|
+
/**
|
|
35
|
+
* Comandos sugeridos para as palavras digitadas — o `GetCommandSuggestions`
|
|
36
|
+
* do gcloud: só quem pontua > 0 e faz pelo menos `MIN_RATIO` da pontuação do
|
|
37
|
+
* primeiro, no máximo `MAX_SUGGESTIONS`. Vazio quando nada pontua.
|
|
38
|
+
*/
|
|
39
|
+
export declare function suggestCommands(words: readonly string[], candidates: ReadonlyArray<readonly string[]>): string[];
|
|
40
|
+
/**
|
|
41
|
+
* Sugestões para a palavra digitada, entre os subcomandos dados. Vazio quando
|
|
42
|
+
* nada passa o piso — aí quem chama lista as escolhas, nunca chuta.
|
|
43
|
+
*/
|
|
44
|
+
export declare function suggestSubcommands(word: string, subcommands: readonly string[]): string[];
|
|
45
|
+
export interface ReservedVerbInput {
|
|
46
|
+
/** O verbo recebido como valor, como a pessoa digitou. */
|
|
47
|
+
verb: string;
|
|
48
|
+
/** O comando onde ele chegou, sem o `neetru` (ex.: `'chat identity'`). */
|
|
49
|
+
command: string;
|
|
50
|
+
/**
|
|
51
|
+
* O comando cujos subcomandos são sugeridos, sem o `neetru`. Padrão:
|
|
52
|
+
* `command`. Ex.: a palavra chega em `chat send`, e os subcomandos são de `chat`.
|
|
53
|
+
*/
|
|
54
|
+
parent?: string;
|
|
55
|
+
/** O que o comando esperava naquela posição (ex.: `'um apelido'`). */
|
|
56
|
+
what: string;
|
|
57
|
+
/**
|
|
58
|
+
* Os subcomandos/verbos que o comando aceita — a MESMA lista que ele usa
|
|
59
|
+
* para decidir. Vazio = sem sugestão possível (só a recusa).
|
|
60
|
+
*/
|
|
61
|
+
subcommands: readonly string[];
|
|
62
|
+
/** Apelido → nome do subcomando (ex.: `apelido → identity`): a sugestão vai direto ao nome. */
|
|
63
|
+
aliases?: Readonly<Record<string, string>>;
|
|
64
|
+
/** Como passar a mesma palavra como texto, quando existe jeito. */
|
|
65
|
+
escapeHint?: string;
|
|
66
|
+
/** Código do JSON. Padrão: `'uso_invalido'`. */
|
|
67
|
+
code?: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Recusa um verbo usado como valor e sugere o subcomando certo — ou, sem
|
|
71
|
+
* sugestão que passe o piso, lista as escolhas. Exit 1 (erro do cliente). No
|
|
72
|
+
* JSON: `value`, `suggestions` (comandos completos) e `valid`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function reservedVerbError(input: ReservedVerbInput): NeetruCliError;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verbo de subcomando nunca é valor livre, e sugestão só com piso de qualidade
|
|
3
|
+
* (P105 / bug_89d2497e; o caso que nasceu daqui foi o bug_09574d4b).
|
|
4
|
+
*
|
|
5
|
+
* O bug: `neetru chat identity show` GRAVOU o apelido "show", e
|
|
6
|
+
* `identity set "X"` gravou "set". O comando recebia texto livre posicional e
|
|
7
|
+
* aceitava qualquer palavra — inclusive o verbo que a pessoa usou achando que
|
|
8
|
+
* era subcomando. O gcloud não teria aceitado essa string: ele sabe quais
|
|
9
|
+
* palavras são verbos e, quando a palavra não é escolha válida, sugere o
|
|
10
|
+
* comando certo ou lista as escolhas — nunca adivinha lixo.
|
|
11
|
+
*
|
|
12
|
+
* O MODELO é o `calliope/suggest_commands.py` do gcloud (lido no SDK instalado
|
|
13
|
+
* nesta máquina, `google-cloud-sdk/lib/googlecloudsdk/calliope`):
|
|
14
|
+
* - `SYNONYMS`: verbo digitado → verbo que a CLI de fato usa. O gcloud mapeia
|
|
15
|
+
* para os verbos DELE (`show → describe`); aqui o mapa aponta para os nossos
|
|
16
|
+
* (`describe → show`), e um verbo pode ter mais de um correspondente.
|
|
17
|
+
* - `_WordScore`: só pontua quando uma palavra CONTÉM a outra, e só em começo
|
|
18
|
+
* de palavra (ou depois de `-`). Sem relação de texto, a pontuação é zero e
|
|
19
|
+
* o candidato some. É esse o piso absoluto: nada de distância de edição, que
|
|
20
|
+
* acha "parecido" em qualquer par de palavras curtas.
|
|
21
|
+
* - `MIN_RATIO = 0.7`: entre os que pontuam, só entra quem faz pelo menos 70%
|
|
22
|
+
* da pontuação do primeiro. `MAX_SUGGESTIONS = 10`.
|
|
23
|
+
* - O histórico de uso do usuário (`_GetSurfaceHistoryFrequencies`) NÃO foi
|
|
24
|
+
* portado: a CLI não guarda histórico de comandos, e inventar um seria pior
|
|
25
|
+
* que não ranquear por ele.
|
|
26
|
+
*
|
|
27
|
+
* Quando nada passa o piso, quem chama LISTA as escolhas válidas (o gcloud faz
|
|
28
|
+
* o mesmo em erro de argumento, `parser_extensions.py`) em vez de sugerir.
|
|
29
|
+
*/
|
|
30
|
+
import { formatChoices } from './choices.js';
|
|
31
|
+
import { EXIT_CODES, NeetruCliError } from './errors.js';
|
|
32
|
+
/** Verbo digitado → verbo(s) que a CLI usa. */
|
|
33
|
+
export const SYNONYMS = {
|
|
34
|
+
describe: ['show', 'get'],
|
|
35
|
+
get: ['show', 'describe'],
|
|
36
|
+
show: ['describe', 'get'],
|
|
37
|
+
view: ['show'],
|
|
38
|
+
ver: ['show'],
|
|
39
|
+
list: ['show'],
|
|
40
|
+
update: ['set'],
|
|
41
|
+
modify: ['set', 'update'],
|
|
42
|
+
patch: ['set', 'update'],
|
|
43
|
+
change: ['set', 'update'],
|
|
44
|
+
definir: ['set'],
|
|
45
|
+
unset: ['clear'],
|
|
46
|
+
remove: ['clear', 'delete'],
|
|
47
|
+
delete: ['clear', 'remove'],
|
|
48
|
+
reset: ['clear'],
|
|
49
|
+
limpar: ['clear'],
|
|
50
|
+
};
|
|
51
|
+
/** Mínimo da razão pontuação/pontuação-do-primeiro para entrar na sugestão. */
|
|
52
|
+
export const MIN_RATIO = 0.7;
|
|
53
|
+
/** No máximo isto de sugestões. */
|
|
54
|
+
export const MAX_SUGGESTIONS = 10;
|
|
55
|
+
/**
|
|
56
|
+
* A palavra é, de fato, verbo de subcomando DESTE comando? Sim quando é um dos
|
|
57
|
+
* subcomandos dados (nome ou apelido) ou um sinônimo (`SYNONYMS`) de um deles.
|
|
58
|
+
* Sem diferenciar caixa, sem espaços nas pontas.
|
|
59
|
+
*
|
|
60
|
+
* Revisão do #87 (BAIXO 6): a 1ª versão recusava uma lista GLOBAL de verbos em
|
|
61
|
+
* qualquer texto livre, e `neetru chat send show` (resposta comum em PT-BR)
|
|
62
|
+
* passou a ser recusado sem que `show` fosse subcomando de `chat`. A recusa
|
|
63
|
+
* agora depende dos subcomandos de verdade de quem pergunta: para `chat send`,
|
|
64
|
+
* só `channels`, `identity`/`apelido`, `send` e `history`; para
|
|
65
|
+
* `chat identity`, os verbos dele (`show`, `set`, `clear`…) e seus sinônimos.
|
|
66
|
+
*/
|
|
67
|
+
export function isSubcommandVerb(word, subcommands) {
|
|
68
|
+
const w = word.trim().toLowerCase();
|
|
69
|
+
if (!w)
|
|
70
|
+
return false;
|
|
71
|
+
if (subcommands.includes(w))
|
|
72
|
+
return true;
|
|
73
|
+
return (SYNONYMS[w] ?? []).some((s) => subcommands.includes(s));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Pontuação de uma palavra digitada contra uma palavra de um comando — o
|
|
77
|
+
* `_WordScore` do gcloud, linha a linha. Sempre ≥ 0; zero = sem relação.
|
|
78
|
+
*/
|
|
79
|
+
export function wordScore(index, word, canonical, canonicalLength) {
|
|
80
|
+
if (!word || !canonical)
|
|
81
|
+
return 0;
|
|
82
|
+
let shorter;
|
|
83
|
+
let longer;
|
|
84
|
+
if (canonical.includes(word)) {
|
|
85
|
+
shorter = word;
|
|
86
|
+
longer = canonical;
|
|
87
|
+
}
|
|
88
|
+
else if (word.includes(canonical)) {
|
|
89
|
+
shorter = canonical;
|
|
90
|
+
longer = word;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
// O acerto no meio precisa cair em fronteira de palavra.
|
|
96
|
+
const hit = longer.indexOf(shorter);
|
|
97
|
+
if (hit > 0 && longer[hit - 1] !== '-')
|
|
98
|
+
return 0;
|
|
99
|
+
let score = 10;
|
|
100
|
+
if (canonicalLength === 1)
|
|
101
|
+
score += 30;
|
|
102
|
+
else if (canonicalLength === 2)
|
|
103
|
+
score += 20;
|
|
104
|
+
else if (canonicalLength === 3)
|
|
105
|
+
score += 10;
|
|
106
|
+
if (index === 0)
|
|
107
|
+
score += 25;
|
|
108
|
+
else if (index === 1)
|
|
109
|
+
score += 15;
|
|
110
|
+
else
|
|
111
|
+
score += 5;
|
|
112
|
+
let extra = longer.length - shorter.length;
|
|
113
|
+
if (extra <= 2) {
|
|
114
|
+
extra = 3 - extra;
|
|
115
|
+
if (longer.startsWith(shorter))
|
|
116
|
+
extra *= 2;
|
|
117
|
+
score += extra;
|
|
118
|
+
}
|
|
119
|
+
if (index === 0 && canonicalLength > 1)
|
|
120
|
+
score += 30;
|
|
121
|
+
else if (index > 0 && canonicalLength > index + 1)
|
|
122
|
+
score += 15;
|
|
123
|
+
return score;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Candidatos pontuados, do maior para o menor (empate: ordem lexicográfica).
|
|
127
|
+
* Só entra quem pontua > 0. Cada candidato é a lista de palavras do comando.
|
|
128
|
+
*/
|
|
129
|
+
export function scoreCandidates(words, candidates) {
|
|
130
|
+
const normalizadas = words.map((w) => w.trim().toLowerCase().replace(/_/g, '-')).filter(Boolean);
|
|
131
|
+
const out = [];
|
|
132
|
+
for (const cand of candidates) {
|
|
133
|
+
const casadas = new Set();
|
|
134
|
+
let score = 0;
|
|
135
|
+
cand.forEach((canonica, index) => {
|
|
136
|
+
for (const w of normalizadas) {
|
|
137
|
+
let inc = wordScore(index, w, canonica, cand.length);
|
|
138
|
+
for (const alt of SYNONYMS[w] ?? []) {
|
|
139
|
+
inc = Math.max(inc, wordScore(index, alt, canonica, cand.length));
|
|
140
|
+
}
|
|
141
|
+
if (inc) {
|
|
142
|
+
casadas.add(w);
|
|
143
|
+
score += inc;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
if (normalizadas.length > 0 && casadas.size === normalizadas.length)
|
|
148
|
+
score += 10;
|
|
149
|
+
if (score > 0)
|
|
150
|
+
out.push({ command: cand, score });
|
|
151
|
+
}
|
|
152
|
+
out.sort((a, b) => b.score - a.score || a.command.join(' ').localeCompare(b.command.join(' ')));
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Comandos sugeridos para as palavras digitadas — o `GetCommandSuggestions`
|
|
157
|
+
* do gcloud: só quem pontua > 0 e faz pelo menos `MIN_RATIO` da pontuação do
|
|
158
|
+
* primeiro, no máximo `MAX_SUGGESTIONS`. Vazio quando nada pontua.
|
|
159
|
+
*/
|
|
160
|
+
export function suggestCommands(words, candidates) {
|
|
161
|
+
const pontuados = scoreCandidates(words, candidates);
|
|
162
|
+
if (pontuados.length === 0)
|
|
163
|
+
return [];
|
|
164
|
+
const topo = pontuados[0].score;
|
|
165
|
+
return pontuados
|
|
166
|
+
.filter((p) => p.score / topo >= MIN_RATIO)
|
|
167
|
+
.slice(0, MAX_SUGGESTIONS)
|
|
168
|
+
.map((p) => p.command.join(' '));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Sugestões para a palavra digitada, entre os subcomandos dados. Vazio quando
|
|
172
|
+
* nada passa o piso — aí quem chama lista as escolhas, nunca chuta.
|
|
173
|
+
*/
|
|
174
|
+
export function suggestSubcommands(word, subcommands) {
|
|
175
|
+
return suggestCommands([word], subcommands.map((s) => [s]));
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Recusa um verbo usado como valor e sugere o subcomando certo — ou, sem
|
|
179
|
+
* sugestão que passe o piso, lista as escolhas. Exit 1 (erro do cliente). No
|
|
180
|
+
* JSON: `value`, `suggestions` (comandos completos) e `valid`.
|
|
181
|
+
*/
|
|
182
|
+
export function reservedVerbError(input) {
|
|
183
|
+
const pai = input.parent ?? input.command;
|
|
184
|
+
const apelidoDe = input.aliases?.[input.verb.trim().toLowerCase()];
|
|
185
|
+
const sugestoes = (apelidoDe ? [apelidoDe] : suggestSubcommands(input.verb, input.subcommands)).map((s) => `neetru ${pai} ${s}`);
|
|
186
|
+
const linhas = [
|
|
187
|
+
`"${input.verb}" é palavra de comando da CLI e não vale como ${input.what} em \`neetru ${input.command}\`.`,
|
|
188
|
+
];
|
|
189
|
+
if (sugestoes.length > 0) {
|
|
190
|
+
linhas.push(`Você quis dizer: ${sugestoes.map((s) => `\`${s}\``).join(' ou ')}?`);
|
|
191
|
+
}
|
|
192
|
+
else if (input.subcommands.length > 0) {
|
|
193
|
+
linhas.push(`Subcomandos de \`neetru ${pai}\`: ${formatChoices(input.subcommands)}.`);
|
|
194
|
+
}
|
|
195
|
+
if (input.escapeHint)
|
|
196
|
+
linhas.push(input.escapeHint);
|
|
197
|
+
return new NeetruCliError(linhas.join('\n '), {
|
|
198
|
+
code: input.code ?? 'uso_invalido',
|
|
199
|
+
exit: EXIT_CODES.client,
|
|
200
|
+
details: {
|
|
201
|
+
value: input.verb,
|
|
202
|
+
suggestions: sugestoes,
|
|
203
|
+
valid: [...input.subcommands],
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=suggest.js.map
|
|
@@ -202,7 +202,7 @@ export async function runOidcLoginList(opts) {
|
|
|
202
202
|
}
|
|
203
203
|
log.heading(`Login-clients OIDC — produto ${opts.productId} (${res.count})`);
|
|
204
204
|
renderTable([
|
|
205
|
-
{ header: 'CLIENT ID', value: (r) => r.clientId,
|
|
205
|
+
{ header: 'CLIENT ID', value: (r) => r.clientId, identificador: true },
|
|
206
206
|
{ header: 'NOME', value: (r) => r.name, maxWidth: 24 },
|
|
207
207
|
{ header: 'REDIRECT_URI', value: (r) => (r.redirectUris ?? []).join(', '), maxWidth: 44 },
|
|
208
208
|
{
|
|
@@ -120,7 +120,7 @@ export async function runSdkCredentialList(opts) {
|
|
|
120
120
|
}
|
|
121
121
|
log.heading(`Credenciais OIDC — produto ${opts.productId} (${res.count})`);
|
|
122
122
|
renderTable([
|
|
123
|
-
{ header: 'CLIENT ID', value: (r) => r.clientId,
|
|
123
|
+
{ header: 'CLIENT ID', value: (r) => r.clientId, identificador: true },
|
|
124
124
|
{ header: 'NOME', value: (r) => r.name, maxWidth: 28 },
|
|
125
125
|
{ header: 'ESCOPOS', value: (r) => r.allowedScopes.join(', '), maxWidth: 40 },
|
|
126
126
|
{
|
|
@@ -6,7 +6,7 @@ export type Shell = 'bash' | 'zsh' | 'pwsh';
|
|
|
6
6
|
* IMPORTANTE: incluir TODOS os `program.command('X')` de `src/index.ts`.
|
|
7
7
|
* O regression test `top-level-commands-coverage.test.ts` exige >=95%.
|
|
8
8
|
*/
|
|
9
|
-
export declare const TOP_LEVEL_COMMANDS: readonly ["add", "admin", "agent", "ai", "api-catalog", "ar", "archive", "artifact-registry", "audit", "auth", "autocomplete", "billing", "bootstrap", "bug", "build", "builds", "cloud-run", "code", "config", "customers", "db", "db-hosts", "deploy", "deployments", "dev", "dns", "docs", "doctor", "dr", "env", "hosting", "init", "login", "logout", "logs", "menu", "mobile", "mocks", "new", "open", "ops", "products", "promote", "publish", "schema", "servers", "smoke", "status", "support", "tenants", "ui", "upgrade", "validate", "vm", "whoami", "workspaces"];
|
|
9
|
+
export declare const TOP_LEVEL_COMMANDS: readonly ["add", "admin", "agent", "ai", "api-catalog", "ar", "archive", "artifact-registry", "audit", "auth", "autocomplete", "billing", "bootstrap", "bug", "build", "builds", "cloud-run", "code", "config", "customers", "db", "db-hosts", "deploy", "deployments", "dev", "dns", "docs", "doctor", "dr", "env", "hosting", "init", "login", "logout", "logs", "menu", "mobile", "mocks", "new", "open", "ops", "products", "promote", "publish", "schema", "secrets", "servers", "smoke", "status", "support", "tenants", "ui", "upgrade", "validate", "vm", "whoami", "workspaces"];
|
|
10
10
|
export interface AutocompleteOptions {
|
|
11
11
|
shell?: string;
|
|
12
12
|
}
|
|
@@ -73,6 +73,7 @@ export const TOP_LEVEL_COMMANDS = [
|
|
|
73
73
|
'promote',
|
|
74
74
|
'publish',
|
|
75
75
|
'schema', // neetru schema — inspecionar spec OpenAPI do Core
|
|
76
|
+
'secrets', // neetru secrets — segredos do próprio produto (set/list/archive)
|
|
76
77
|
'servers',
|
|
77
78
|
'smoke', // neetru smoke — percorre páginas do produto local antes do deploy
|
|
78
79
|
'status',
|
package/dist/commands/bug.d.ts
CHANGED
|
@@ -1,3 +1,61 @@
|
|
|
1
|
+
interface BugRow {
|
|
2
|
+
id: string;
|
|
3
|
+
title?: string;
|
|
4
|
+
status?: string;
|
|
5
|
+
severity?: string;
|
|
6
|
+
category?: string;
|
|
7
|
+
assignedTo?: string;
|
|
8
|
+
productId?: string;
|
|
9
|
+
reportedAt?: unknown;
|
|
10
|
+
updatedAt?: unknown;
|
|
11
|
+
reportedBy?: {
|
|
12
|
+
actorType?: string;
|
|
13
|
+
actorId?: string;
|
|
14
|
+
email?: string;
|
|
15
|
+
};
|
|
16
|
+
description?: string;
|
|
17
|
+
reproductionSteps?: string;
|
|
18
|
+
expectedBehavior?: string;
|
|
19
|
+
actualBehavior?: string;
|
|
20
|
+
resolution?: {
|
|
21
|
+
summary?: string;
|
|
22
|
+
commitSha?: string;
|
|
23
|
+
resolvedBy?: string;
|
|
24
|
+
resolvedAt?: unknown;
|
|
25
|
+
};
|
|
26
|
+
comments?: Array<{
|
|
27
|
+
author: string;
|
|
28
|
+
body: string;
|
|
29
|
+
at?: unknown;
|
|
30
|
+
}>;
|
|
31
|
+
environment?: {
|
|
32
|
+
cliVersion?: string;
|
|
33
|
+
sdkVersion?: string;
|
|
34
|
+
nodeVersion?: string;
|
|
35
|
+
os?: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Limpa o que o usuário colou: espaços, reticências da tabela antiga, caixa
|
|
40
|
+
* alta, e aceita o hex sem o `bug_` na frente (`a4f84c1b` → `bug_a4f84c1b`).
|
|
41
|
+
*/
|
|
42
|
+
export declare function normalizarEntradaDeId(entrada: string): string;
|
|
43
|
+
export type ResolucaoDeIdDeBug = {
|
|
44
|
+
ok: true;
|
|
45
|
+
id: string;
|
|
46
|
+
} | {
|
|
47
|
+
ok: false;
|
|
48
|
+
motivo: 'curto_demais' | 'nao_encontrado' | 'ambiguo';
|
|
49
|
+
candidatos: BugRow[];
|
|
50
|
+
};
|
|
51
|
+
/** Pura: decide o id a partir da entrada e da lista. Testável sem rede. */
|
|
52
|
+
export declare function escolherBugPorPrefixo(entrada: string, rows: BugRow[]): ResolucaoDeIdDeBug;
|
|
53
|
+
/**
|
|
54
|
+
* Resolve o id que o usuário digitou pro id completo. Id completo não custa
|
|
55
|
+
* chamada extra; prefixo custa UMA listagem. Falha sai com mensagem que diz o
|
|
56
|
+
* que fazer, em vez do "não encontrado" que mentia.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolverIdDoBug(entrada: string, token: string, json?: boolean): Promise<string>;
|
|
1
59
|
export interface BugReportOptions {
|
|
2
60
|
title?: string;
|
|
3
61
|
body?: string;
|
|
@@ -85,3 +143,4 @@ export interface BugCommentOptions {
|
|
|
85
143
|
* neetru bug comment bug_abc123 --body "Testei em Node 22 — reproduzível."
|
|
86
144
|
*/
|
|
87
145
|
export declare function runBugComment(id: string, opts: BugCommentOptions): Promise<void>;
|
|
146
|
+
export {};
|
package/dist/commands/bug.js
CHANGED
|
@@ -21,6 +21,108 @@ import { printWriteResult } from '../lib/cli-write.js';
|
|
|
21
21
|
import { renderTable, fmtTimestamp } from '../lib/render.js';
|
|
22
22
|
import { log } from '../utils/logger.js';
|
|
23
23
|
import { safeExit } from '../utils/safe-exit.js';
|
|
24
|
+
// ─── Id do bug: inteiro na tela, prefixo como atalho ─────────────────────────
|
|
25
|
+
//
|
|
26
|
+
// bug_a4f84c1b (2026-10-12): o id que o Core gera tem 36 caracteres
|
|
27
|
+
// (`bug_` + uuid sem hífen) e a tabela cortava em 24 com `…`. O usuário copiava
|
|
28
|
+
// o que via, e `bug show` respondia "não encontrado" — negando a EXISTÊNCIA de
|
|
29
|
+
// um bug que existe. O próprio Dev Core caiu nisso consultando bug_a8375858.
|
|
30
|
+
//
|
|
31
|
+
// A tabela mostra o id INTEIRO: é o único handle que funciona sempre. A primeira
|
|
32
|
+
// versão deste conserto mostrava um id curto (`bug_` + 8 hex), e a revisão
|
|
33
|
+
// independente reproduziu em produção o defeito de novo: numa listagem filtrada
|
|
34
|
+
// (`--status resolved`), 38 de 200 linhas estavam fora da janela que a resolução
|
|
35
|
+
// de prefixo enxerga — a tabela imprimia de novo um handle que não resolvia.
|
|
36
|
+
//
|
|
37
|
+
// O prefixo continua aceito como ATALHO, pra quem digita ou cola o que o time
|
|
38
|
+
// escreve no chat (`bug_a4f84c1b`). Ambíguo lista candidatos; não achado NUNCA
|
|
39
|
+
// afirma que o bug não existe — só que não estava no que a listagem devolveu.
|
|
40
|
+
/** Formato do id gerado pelo Core (`bug_${randomUUID().replace(/-/g, '')}`). */
|
|
41
|
+
const ID_COMPLETO = /^bug_[0-9a-f]{32}$/;
|
|
42
|
+
/** Mínimo de hex depois de `bug_` pra aceitar como prefixo. */
|
|
43
|
+
const PREFIXO_MINIMO = 4;
|
|
44
|
+
/** Teto do `?limit` da listagem no Core (`cli/v1/bugs/route.ts`). */
|
|
45
|
+
const LIMITE_DA_LISTAGEM = 200;
|
|
46
|
+
/**
|
|
47
|
+
* Limpa o que o usuário colou: espaços, reticências da tabela antiga, caixa
|
|
48
|
+
* alta, e aceita o hex sem o `bug_` na frente (`a4f84c1b` → `bug_a4f84c1b`).
|
|
49
|
+
*/
|
|
50
|
+
export function normalizarEntradaDeId(entrada) {
|
|
51
|
+
let s = entrada.trim().toLowerCase();
|
|
52
|
+
s = s.replace(/(…|\.\.\.)$/, '');
|
|
53
|
+
if (!s.startsWith('bug_') && /^[0-9a-f]+$/.test(s))
|
|
54
|
+
s = `bug_${s}`;
|
|
55
|
+
return s;
|
|
56
|
+
}
|
|
57
|
+
/** Pura: decide o id a partir da entrada e da lista. Testável sem rede. */
|
|
58
|
+
export function escolherBugPorPrefixo(entrada, rows) {
|
|
59
|
+
const alvo = normalizarEntradaDeId(entrada);
|
|
60
|
+
if (ID_COMPLETO.test(alvo))
|
|
61
|
+
return { ok: true, id: alvo };
|
|
62
|
+
if (alvo.length < 'bug_'.length + PREFIXO_MINIMO) {
|
|
63
|
+
return { ok: false, motivo: 'curto_demais', candidatos: [] };
|
|
64
|
+
}
|
|
65
|
+
const candidatos = rows.filter((r) => typeof r.id === 'string' && r.id.startsWith(alvo));
|
|
66
|
+
if (candidatos.length === 1)
|
|
67
|
+
return { ok: true, id: candidatos[0].id };
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
motivo: candidatos.length === 0 ? 'nao_encontrado' : 'ambiguo',
|
|
71
|
+
candidatos,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Resolve o id que o usuário digitou pro id completo. Id completo não custa
|
|
76
|
+
* chamada extra; prefixo custa UMA listagem. Falha sai com mensagem que diz o
|
|
77
|
+
* que fazer, em vez do "não encontrado" que mentia.
|
|
78
|
+
*/
|
|
79
|
+
export async function resolverIdDoBug(entrada, token, json) {
|
|
80
|
+
const direto = normalizarEntradaDeId(entrada);
|
|
81
|
+
if (ID_COMPLETO.test(direto))
|
|
82
|
+
return direto;
|
|
83
|
+
let rows = [];
|
|
84
|
+
try {
|
|
85
|
+
const res = await apiRequest(`/api/cli/v1/bugs?limit=${LIMITE_DA_LISTAGEM}`, { token });
|
|
86
|
+
rows = Array.isArray(res?.reports) ? res.reports : [];
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
handleApiError(error, json);
|
|
90
|
+
}
|
|
91
|
+
const r = escolherBugPorPrefixo(entrada, rows);
|
|
92
|
+
if (r.ok)
|
|
93
|
+
return r.id;
|
|
94
|
+
let msg;
|
|
95
|
+
if (r.motivo === 'curto_demais') {
|
|
96
|
+
msg =
|
|
97
|
+
`Id "${entrada}" curto demais. Use ao menos bug_ + ${PREFIXO_MINIMO} caracteres ` +
|
|
98
|
+
'(ex.: bug_a4f8) ou o id completo de `neetru bug list --json`.';
|
|
99
|
+
}
|
|
100
|
+
else if (r.motivo === 'ambiguo') {
|
|
101
|
+
const lista = r.candidatos
|
|
102
|
+
.slice(0, 10)
|
|
103
|
+
.map((b) => ` ${b.id} ${b.title ?? ''}`.trimEnd())
|
|
104
|
+
.join('\n');
|
|
105
|
+
msg = `Prefixo "${entrada}" casa com ${r.candidatos.length} bugs — use mais caracteres:\n${lista}`;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// NUNCA afirmar que o bug não existe: a listagem tem teto (200) e, pra quem
|
|
109
|
+
// não é admin, ainda é filtrada por produto no Core. O que dá pra afirmar é
|
|
110
|
+
// só o que foi visto.
|
|
111
|
+
msg =
|
|
112
|
+
`Não achei bug começando com "${direto}" entre os ${rows.length} que a ` +
|
|
113
|
+
'listagem devolveu (os mais recentes que você pode ver). Use o id completo — ' +
|
|
114
|
+
'a coluna ID de `neetru bug list` mostra inteiro.';
|
|
115
|
+
}
|
|
116
|
+
if (json) {
|
|
117
|
+
// Mesmo formato do resto da CLI (`cli-write.ts`): `error` é o CÓDIGO, em texto.
|
|
118
|
+
console.log(JSON.stringify({ ok: false, error: `bug_id_${r.motivo}`, message: msg }));
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
log.error(msg);
|
|
122
|
+
}
|
|
123
|
+
safeExit(1);
|
|
124
|
+
return '';
|
|
125
|
+
}
|
|
24
126
|
// ─── Formatação ──────────────────────────────────────────────────────────────
|
|
25
127
|
function colorStatus(s) {
|
|
26
128
|
if (!s)
|
|
@@ -187,12 +289,14 @@ export async function runBugList(opts) {
|
|
|
187
289
|
return;
|
|
188
290
|
}
|
|
189
291
|
renderTable([
|
|
190
|
-
{ header: 'ID', value: (b) => b.id,
|
|
292
|
+
{ header: 'ID', value: (b) => b.id, identificador: true },
|
|
191
293
|
{ header: 'TÍTULO', value: (b) => b.title ?? '—', maxWidth: 40 },
|
|
192
294
|
{ header: 'STATUS', value: (b) => colorStatus(b.status) },
|
|
193
295
|
{ header: 'SEV', value: (b) => colorSeverity(b.severity) },
|
|
194
296
|
{ header: 'CATEGORIA', value: (b) => b.category ?? '—', maxWidth: 10 },
|
|
195
|
-
|
|
297
|
+
// uid de 28 caracteres que `--assigned-to` aceita de volta: cortado em 24
|
|
298
|
+
// virava um uid que não existe (revisão do #75).
|
|
299
|
+
{ header: 'RESPONSÁVEL', value: (b) => b.assignedTo ?? chalk.dim('—'), identificador: true },
|
|
196
300
|
{ header: 'REPORTADO', value: (b) => fmtTimestamp(b.reportedAt), maxWidth: 14 },
|
|
197
301
|
], res.reports);
|
|
198
302
|
console.log();
|
|
@@ -209,6 +313,7 @@ export async function runBugShow(id, opts) {
|
|
|
209
313
|
safeExit(1);
|
|
210
314
|
}
|
|
211
315
|
const token = await requireToken(opts.json);
|
|
316
|
+
id = await resolverIdDoBug(id, token, opts.json);
|
|
212
317
|
const spinner = opts.json ? null : ora({ text: `Buscando bug ${id}…`, color: 'blue' }).start();
|
|
213
318
|
let res;
|
|
214
319
|
try {
|
|
@@ -286,6 +391,7 @@ export async function runBugClaim(id, opts) {
|
|
|
286
391
|
safeExit(1);
|
|
287
392
|
}
|
|
288
393
|
const token = await requireToken(opts.json);
|
|
394
|
+
id = await resolverIdDoBug(id, token, opts.json);
|
|
289
395
|
const spinner = opts.json ? null : ora({ text: `Assumindo bug ${id}…`, color: 'blue' }).start();
|
|
290
396
|
let res;
|
|
291
397
|
try {
|
|
@@ -352,6 +458,11 @@ export async function runBugResolve(id, opts) {
|
|
|
352
458
|
safeExit(1);
|
|
353
459
|
}
|
|
354
460
|
const token = await requireToken(opts.json);
|
|
461
|
+
id = await resolverIdDoBug(id, token, opts.json);
|
|
462
|
+
// `--duplicate-of` também é id de bug: sem resolver, o Core gravaria a
|
|
463
|
+
// duplicata apontando pra um id que não existe.
|
|
464
|
+
if (duplicateOf)
|
|
465
|
+
duplicateOf = await resolverIdDoBug(duplicateOf, token, opts.json);
|
|
355
466
|
const spinner = opts.json ? null : ora({ text: `Encerrando bug ${id}…`, color: 'blue' }).start();
|
|
356
467
|
let res;
|
|
357
468
|
try {
|
|
@@ -401,6 +512,7 @@ export async function runBugComment(id, opts) {
|
|
|
401
512
|
safeExit(1);
|
|
402
513
|
}
|
|
403
514
|
const token = await requireToken(opts.json);
|
|
515
|
+
id = await resolverIdDoBug(id, token, opts.json);
|
|
404
516
|
const spinner = opts.json ? null : ora({ text: `Comentando bug ${id}…`, color: 'blue' }).start();
|
|
405
517
|
let res;
|
|
406
518
|
try {
|