@fontdata/ai-harness 2.7.0 → 3.1.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/.claude-plugin/plugin.json +2 -2
- package/README.md +16 -15
- package/dist/{banco → banco-dados}/ambiente.js +17 -3
- package/dist/{banco → banco-dados}/command.js +79 -41
- package/dist/{banco → banco-dados}/conexao.js +4 -4
- package/dist/{banco → banco-dados}/ferramentas.js +9 -10
- package/dist/cli.js +17 -10
- package/dist/config/config.js +104 -33
- package/dist/doctor/checks.js +10 -10
- package/dist/init.js +1 -1
- package/dist/install/flags.js +7 -2
- package/dist/install/mcp.js +14 -3
- package/dist/install/plan.js +11 -7
- package/dist/install/settings.js +17 -4
- package/dist/update/update.js +47 -20
- package/package.json +1 -1
- /package/dist/{banco → banco-dados}/cadastro.js +0 -0
- /package/dist/{banco → banco-dados}/drivers/index.js +0 -0
- /package/dist/{banco → banco-dados}/drivers/mariadb.js +0 -0
- /package/dist/{banco → banco-dados}/drivers/postgres.js +0 -0
- /package/dist/{banco → banco-dados}/drivers/sqlite.js +0 -0
- /package/dist/{banco → banco-dados}/drivers/tipos.js +0 -0
- /package/dist/{banco → banco-dados}/lgpd/aplicar.js +0 -0
- /package/dist/{banco → banco-dados}/lgpd/detectar.js +0 -0
- /package/dist/{banco → banco-dados}/lgpd/mascarar.js +0 -0
- /package/dist/{banco → banco-dados}/mcp.js +0 -0
- /package/dist/{banco → banco-dados}/somente-leitura.js +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fontdata",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"mcpServers": {
|
|
5
|
-
"
|
|
5
|
+
"banco_dados": { "command": "npx", "args": ["-y", "@fontdata/ai-harness@3.1.0", "banco-dados-mcp"] }
|
|
6
6
|
},
|
|
7
7
|
"description": "Harness de engenharia com IA da Fontdata para Claude Code",
|
|
8
8
|
"author": { "name": "Fontdata" },
|
package/README.md
CHANGED
|
@@ -96,7 +96,7 @@ npx @fontdata/ai-harness init --comandos resolver-bug,nova-tarefa --sem-harness
|
|
|
96
96
|
| `--sem-harness` | Não inclui o comando-mestre `/harness` |
|
|
97
97
|
| `--sem-hooks` / `--sem-agents` | Não instala hooks / agents |
|
|
98
98
|
| `--sem-comentarios` | Não instala a rule de comentários, o hook e a catraca do legado |
|
|
99
|
-
| `--sem-banco` | Não registra o servidor MCP de banco no `.mcp.json` e lembra a recusa no config |
|
|
99
|
+
| `--sem-banco-dados` | Não registra o servidor MCP de banco no `.mcp.json` e lembra a recusa no config |
|
|
100
100
|
| `--claude-md criar\|manter\|backup\|sobrescrever` | O que fazer com o `CLAUDE.md` (padrão: `criar`) |
|
|
101
101
|
| `--conflito sobrescrever\|pular` | Arquivos que já existem (padrão: `pular`) |
|
|
102
102
|
| `--yes` | Aplica; sem ele, só simula |
|
|
@@ -129,26 +129,27 @@ da conexão e o nome da variável de ambiente (`dsn_env`), e cada dev guarda o D
|
|
|
129
129
|
`.claude/settings.local.json` (fora do git), no `.env` ou no ambiente:
|
|
130
130
|
|
|
131
131
|
```bash
|
|
132
|
-
npx @fontdata/ai-harness banco adicionar erp
|
|
133
|
-
npx @fontdata/ai-harness banco testar
|
|
132
|
+
npx @fontdata/ai-harness banco-dados adicionar erp # na raiz do projeto: pergunta os dados, testa e grava ERP_DATABASE_URL
|
|
133
|
+
npx @fontdata/ai-harness banco-dados testar
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
-
O `adicionar
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
136
|
+
O `adicionar` grava `{ "nome": "erp", "dsn_env": "ERP_DATABASE_URL" }` em `banco_dados.conexoes` e a variável no
|
|
137
|
+
`.claude/settings.local.json`; fora da raiz de um projeto (sem `.git` nem `.ai-harness/config.json`), ele recusa sem
|
|
138
|
+
gravar nada. Faça commit do `config.json`; cada colega roda o mesmo comando, que grava só a variável.
|
|
139
|
+
`"producao": true` na entrada vale para a equipe toda, e `banco-dados remover erp` tira a conexão do projeto. Quem
|
|
140
|
+
prefere um cadastro por máquina (`~/.config/ai-harness/conexoes.json`) usa `banco-dados adicionar erp-dev --global` e
|
|
141
|
+
`banco-dados usar erp-dev` no projeto.
|
|
141
142
|
|
|
142
|
-
O servidor MCP `
|
|
143
|
-
tudo somente leitura. Ele vem no plugin; sem plugin, o `init` e o `update` o registram no `.mcp.json`
|
|
144
|
-
pula). Conexão de produção nasce bloqueada. Use um usuário de banco só com leitura.
|
|
143
|
+
O servidor MCP `banco_dados` entrega ao Claude `listar_conexoes`, `listar_tabelas`, `descrever_tabela` e
|
|
144
|
+
`consultar`, tudo somente leitura. Ele vem no plugin; sem plugin, o `init` e o `update` o registram no `.mcp.json`
|
|
145
|
+
(`--sem-banco-dados` pula). Conexão de produção nasce bloqueada. Use um usuário de banco só com leitura.
|
|
145
146
|
|
|
146
147
|
Os dados pessoais (CPF, CNPJ, e-mail, telefone, nome, endereço e afins) saem com **mascaramento LGPD**, feito no
|
|
147
148
|
servidor antes de a resposta chegar ao Claude; os filtros rodam sobre o dado real. Por isso a `consultar` fica
|
|
148
149
|
liberada nas permissões, sem pedir aprovação. Falsos positivos e colunas extras se ajustam em
|
|
149
|
-
`
|
|
150
|
+
`banco_dados.lgpd.liberar_colunas` e `banco_dados.lgpd.mascarar_colunas`; desligar, só no cadastro da máquina. Conexões do
|
|
150
151
|
projeto, cadastro, limites do filtro, produção, mascaramento e dúvidas frequentes estão no
|
|
151
|
-
[guia de banco de dados](docs/guias/banco-
|
|
152
|
+
[guia de banco de dados](docs/guias/banco-dados.md).
|
|
152
153
|
|
|
153
154
|
## Subcomandos do CLI
|
|
154
155
|
|
|
@@ -161,8 +162,8 @@ projeto, cadastro, limites do filtro, produção, mascaramento e dúvidas freque
|
|
|
161
162
|
| `memoria [caminho]` | Índice de quem mexeu em cada arquivo (`--arquivo <caminho>`, `--regressao <id>`) |
|
|
162
163
|
| `evento <evento>` | Grava no rastro do trabalho (usado por fluxos e agents) |
|
|
163
164
|
| `conflitos <id>` | Aponta trabalhos em execução que mexem nos mesmos arquivos; `--aguardar` espera liberar (usado pelo `/executar-plan`) |
|
|
164
|
-
| `banco <subcomando>` | Conexões de banco somente leitura: `adicionar`, `listar`, `testar`, `usar`, `remover` |
|
|
165
|
-
| `banco-mcp` | Servidor MCP de leitura do banco, iniciado pelo Claude Code |
|
|
165
|
+
| `banco-dados <subcomando>` | Conexões de banco somente leitura: `adicionar`, `listar`, `testar`, `usar`, `remover` |
|
|
166
|
+
| `banco-dados-mcp` | Servidor MCP de leitura do banco, iniciado pelo Claude Code |
|
|
166
167
|
| `comentarios [caminho]` | Instala o hook do padrão de comentários com a catraca do legado (`--yes`) |
|
|
167
168
|
| `list` | Lista o catálogo |
|
|
168
169
|
|
|
@@ -81,12 +81,27 @@ export function gravarVariavelSettingsLocal(raiz, nome, valor) {
|
|
|
81
81
|
const envAtual = ehObjeto(dados.env) ? dados.env : {};
|
|
82
82
|
if (envAtual[nome] === valor)
|
|
83
83
|
return { changed: false };
|
|
84
|
-
const novo = { ...dados, env: { ...envAtual, [nome]: valor } };
|
|
85
84
|
const modo = leitura.existe ? statSync(caminho).mode & 0o777 : 0o600;
|
|
86
85
|
mkdirSync(dirname(caminho), { recursive: true });
|
|
86
|
+
gravarAtomico(caminho, { ...dados, env: { ...envAtual, [nome]: valor } }, modo);
|
|
87
|
+
return { changed: true };
|
|
88
|
+
}
|
|
89
|
+
/** Tira a variável do env do settings.local.json com a mesma gravação atômica; arquivo ou chave ausente não escreve. */
|
|
90
|
+
export function removerVariavelSettingsLocal(raiz, nome) {
|
|
91
|
+
const caminho = join(raiz, SETTINGS_LOCAL_PATH);
|
|
92
|
+
const leitura = lerSettings(caminho);
|
|
93
|
+
if ('erro' in leitura)
|
|
94
|
+
throw new Error(leitura.erro);
|
|
95
|
+
if (!leitura.existe || !ehObjeto(leitura.dados.env) || !Object.hasOwn(leitura.dados.env, nome))
|
|
96
|
+
return { changed: false };
|
|
97
|
+
const { [nome]: _removida, ...envRestante } = leitura.dados.env;
|
|
98
|
+
gravarAtomico(caminho, { ...leitura.dados, env: envRestante }, statSync(caminho).mode & 0o777);
|
|
99
|
+
return { changed: true };
|
|
100
|
+
}
|
|
101
|
+
function gravarAtomico(caminho, dados, modo) {
|
|
87
102
|
const temporario = `${caminho}.${process.pid}.${Date.now()}.tmp`;
|
|
88
103
|
try {
|
|
89
|
-
writeFileSync(temporario, `${JSON.stringify(
|
|
104
|
+
writeFileSync(temporario, `${JSON.stringify(dados, null, 2)}\n`, { mode: modo });
|
|
90
105
|
chmodSync(temporario, modo);
|
|
91
106
|
renameSync(temporario, caminho);
|
|
92
107
|
}
|
|
@@ -94,5 +109,4 @@ export function gravarVariavelSettingsLocal(raiz, nome, valor) {
|
|
|
94
109
|
rmSync(temporario, { force: true });
|
|
95
110
|
throw erro;
|
|
96
111
|
}
|
|
97
|
-
return { changed: true };
|
|
98
112
|
}
|
|
@@ -2,24 +2,28 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { EOL } from 'node:os';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
import * as p from '@clack/prompts';
|
|
5
|
-
import { CONFIG_PATH, gravarConexaoProjeto, readConfig, REGRA_DSN_ENV, REGRA_NOME_CONEXAO, validarConfigParaEscrita, writeDefaultConfig, } from '../config/config.js';
|
|
5
|
+
import { comSecaoBanco, CONFIG_PATH, gravarConexaoProjeto, readConfig, removerConexaoProjeto, REGRA_DSN_ENV, REGRA_NOME_CONEXAO, secaoBanco, validarConfigParaEscrita, writeDefaultConfig, } from '../config/config.js';
|
|
6
6
|
import { suggestFlag } from '../install/flags.js';
|
|
7
7
|
import { caminhoCadastro, lerCadastro, ocultarSenha, removerConexao, resumirConexao, salvarConexao, TIPOS_BANCO, } from './cadastro.js';
|
|
8
|
-
import { gravarVariavelSettingsLocal, lerEnvSettingsLocal, lerVariavel, SETTINGS_LOCAL_PATH, validarSettingsLocal } from './ambiente.js';
|
|
8
|
+
import { gravarVariavelSettingsLocal, lerEnvSettingsLocal, lerVariavel, removerVariavelSettingsLocal, SETTINGS_LOCAL_PATH, validarSettingsLocal, } from './ambiente.js';
|
|
9
9
|
import { interpretarDsn, resolverConexoes } from './conexao.js';
|
|
10
10
|
import { criarDriver as criarDriverPadrao } from './drivers/index.js';
|
|
11
11
|
const AJUDA = [
|
|
12
|
-
'Uso: ai-harness banco <subcomando>',
|
|
12
|
+
'Uso: ai-harness banco-dados <subcomando>',
|
|
13
13
|
'',
|
|
14
|
-
' adicionar <nome> [--dsn <dsn>] [--producao] [--sem-testar] [--substituir]',
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
14
|
+
' adicionar <nome> [--env <VAR>] [--dsn <dsn>] [--producao] [--sem-testar] [--substituir]',
|
|
15
|
+
' grava no projeto: o DSN na variável <NOME>_DATABASE_URL (ou --env) do .claude/settings.local.json',
|
|
16
|
+
' e a conexão { nome, dsn_env } em banco_dados.conexoes do .ai-harness/config.json',
|
|
17
|
+
' (sem --dsn, pergunta os dados)',
|
|
18
|
+
' adicionar <nome> --global [--dsn <dsn>] [--producao] [--sem-testar] [--substituir]',
|
|
19
|
+
' cadastra a conexão nesta máquina',
|
|
19
20
|
' listar mostra as conexões cadastradas e as do projeto, sem a senha',
|
|
20
21
|
' testar [nome] testa a conexão pedida ou todas as do projeto',
|
|
21
|
-
' usar <nome...> grava
|
|
22
|
-
'
|
|
22
|
+
' usar <nome...> grava banco_dados.conexoes no .ai-harness/config.json, para conexões do cadastro global',
|
|
23
|
+
' (mantém as entradas { nome, dsn_env })',
|
|
24
|
+
' remover <nome> [--global]',
|
|
25
|
+
' tira a conexão do projeto e a variável dela do .claude/settings.local.json;',
|
|
26
|
+
' com --global, apaga a conexão do cadastro desta máquina',
|
|
23
27
|
].join(EOL);
|
|
24
28
|
const PORTA_PADRAO = { postgres: '5432', mariadb: '3306' };
|
|
25
29
|
const ESQUEMA = { postgres: 'postgres', mariadb: 'mysql' };
|
|
@@ -118,14 +122,15 @@ function conexaoDoDsn(dsn, cwd, producao) {
|
|
|
118
122
|
return conexao;
|
|
119
123
|
}
|
|
120
124
|
async function adicionar(ctx, argv) {
|
|
121
|
-
const { posicionais, marcadas, valores } = lerArgumentos(argv, ['--producao', '--sem-testar', '--substituir', '--
|
|
125
|
+
const { posicionais, marcadas, valores } = lerArgumentos(argv, ['--producao', '--sem-testar', '--substituir', '--global'], ['--dsn', '--env']);
|
|
122
126
|
const [nome, ...sobra] = posicionais;
|
|
123
|
-
if (!nome || sobra.length > 0)
|
|
124
|
-
throw new Error('Uso: ai-harness banco adicionar <nome> [--
|
|
125
|
-
|
|
127
|
+
if (!nome || sobra.length > 0) {
|
|
128
|
+
throw new Error('Uso: ai-harness banco-dados adicionar <nome> [--global | --env <VAR>] [--dsn <dsn>] [--producao] [--sem-testar] [--substituir]');
|
|
129
|
+
}
|
|
130
|
+
if (!marcadas.has('--global'))
|
|
126
131
|
return adicionarNoProjeto(ctx, nome, marcadas, valores);
|
|
127
132
|
if (valores['--env'] !== undefined)
|
|
128
|
-
throw new Error('--env
|
|
133
|
+
throw new Error('--env não vale com --global.');
|
|
129
134
|
const substituir = marcadas.has('--substituir');
|
|
130
135
|
if (!substituir && nome in lerCadastro(ctx.cadastro).conexoes) {
|
|
131
136
|
ctx.erro(`A conexão ${nome} já existe em ${ctx.cadastro}. Use --substituir para trocá-la.`);
|
|
@@ -144,31 +149,35 @@ async function adicionar(ctx, argv) {
|
|
|
144
149
|
}
|
|
145
150
|
salvarConexao(nome, conexao, { substituir, caminho: ctx.cadastro });
|
|
146
151
|
ctx.log(`Conexão ${nome} gravada em ${ctx.cadastro}: ${resumirConexao(conexao)}${conexao.producao ? ' (produção)' : ''}.`);
|
|
147
|
-
ctx.log(`No projeto, rode: npx @fontdata/ai-harness banco usar ${nome}`);
|
|
152
|
+
ctx.log(`No projeto, rode: npx @fontdata/ai-harness banco-dados usar ${nome}`);
|
|
148
153
|
return 0;
|
|
149
154
|
}
|
|
150
|
-
/** Procura o nome no JSON bruto de
|
|
155
|
+
/** Procura o nome no JSON bruto de banco_dados.conexoes; recusa lista inválida antes de qualquer gravação. */
|
|
151
156
|
function entradaDeclarada(cwd, nome) {
|
|
152
157
|
validarConfigParaEscrita(cwd);
|
|
153
158
|
const arquivo = join(cwd, CONFIG_PATH);
|
|
154
159
|
if (!existsSync(arquivo))
|
|
155
160
|
return { forma: 'ausente' };
|
|
156
161
|
const dados = JSON.parse(readFileSync(arquivo, 'utf8'));
|
|
157
|
-
const banco = isObject(dados)
|
|
162
|
+
const banco = isObject(dados) ? secaoBanco(dados) : {};
|
|
158
163
|
if (banco.conexoes === undefined)
|
|
159
164
|
return { forma: 'ausente' };
|
|
160
165
|
if (!Array.isArray(banco.conexoes))
|
|
161
|
-
throw new Error(`
|
|
166
|
+
throw new Error(`banco_dados.conexoes em ${CONFIG_PATH} não é uma lista; corrija-o antes de gravar a conexão ${nome}.`);
|
|
162
167
|
const achada = banco.conexoes.some((entrada) => (isObject(entrada) ? entrada.nome : entrada) === nome);
|
|
163
168
|
if (!achada)
|
|
164
169
|
return { forma: 'ausente' };
|
|
165
|
-
const conexao = readConfig(cwd).config.
|
|
170
|
+
const conexao = readConfig(cwd).config.banco_dados.projeto[nome];
|
|
166
171
|
return conexao ? { forma: 'objeto', conexao } : { forma: 'outra' };
|
|
167
172
|
}
|
|
168
173
|
function variavelPadrao(nome) {
|
|
169
174
|
return `${nome.toUpperCase().replace(/-/g, '_')}_DATABASE_URL`;
|
|
170
175
|
}
|
|
171
176
|
async function adicionarNoProjeto(ctx, nome, marcadas, valores) {
|
|
177
|
+
if (!existsSync(join(ctx.cwd, '.git')) && !existsSync(join(ctx.cwd, CONFIG_PATH))) {
|
|
178
|
+
ctx.erro(`Nenhum projeto em ${ctx.cwd} (sem .git nem ${CONFIG_PATH}). Rode na raiz do projeto ou use --global para cadastrar nesta máquina.`);
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
172
181
|
const substituir = marcadas.has('--substituir');
|
|
173
182
|
if (!REGRA_NOME_CONEXAO.test(nome)) {
|
|
174
183
|
ctx.erro(`Nome de conexão inválido: ${JSON.stringify(nome)}; use só minúsculas, dígitos, - e _, começando por minúscula ou dígito.`);
|
|
@@ -183,7 +192,7 @@ async function adicionarNoProjeto(ctx, nome, marcadas, valores) {
|
|
|
183
192
|
const declarada = entradaDeclarada(ctx.cwd, nome);
|
|
184
193
|
let variavel = pedida ?? variavelPadrao(nome);
|
|
185
194
|
if (declarada.forma === 'outra' && !substituir) {
|
|
186
|
-
ctx.erro(`A conexão ${nome} já está em
|
|
195
|
+
ctx.erro(`A conexão ${nome} já está em banco_dados.conexoes de ${CONFIG_PATH} sem dsn_env. Use --substituir para trocá-la pela conexão do projeto.`);
|
|
187
196
|
return 1;
|
|
188
197
|
}
|
|
189
198
|
if (declarada.forma === 'objeto') {
|
|
@@ -234,7 +243,7 @@ async function adicionarNoProjeto(ctx, nome, marcadas, valores) {
|
|
|
234
243
|
const { changed } = gravarVariavelSettingsLocal(ctx.cwd, variavel, valor);
|
|
235
244
|
ctx.log(changed ? `Variável ${variavel} gravada em ${SETTINGS_LOCAL_PATH}.` : `Variável ${variavel} já estava com esse valor em ${SETTINGS_LOCAL_PATH}.`);
|
|
236
245
|
if (declarada.forma === 'objeto' && !promover) {
|
|
237
|
-
ctx.log(`Conexão ${nome} já declarada em
|
|
246
|
+
ctx.log(`Conexão ${nome} já declarada em banco_dados.conexoes de ${CONFIG_PATH}; a entrada não foi alterada.`);
|
|
238
247
|
return 0;
|
|
239
248
|
}
|
|
240
249
|
const entrada = declarada.forma === 'objeto'
|
|
@@ -246,7 +255,7 @@ async function adicionarNoProjeto(ctx, nome, marcadas, valores) {
|
|
|
246
255
|
...(producao ? { producao: true } : {}),
|
|
247
256
|
};
|
|
248
257
|
gravarConexaoProjeto(ctx.cwd, entrada, { substituir: declarada.forma !== 'ausente' });
|
|
249
|
-
ctx.log(`Conexão ${nome} gravada em
|
|
258
|
+
ctx.log(`Conexão ${nome} gravada em banco_dados.conexoes de ${CONFIG_PATH} (dsn_env ${variavel})${entrada.producao ? ', produção' : ''}.`);
|
|
250
259
|
if (declarada.forma === 'ausente')
|
|
251
260
|
ctx.log(`Versione o ${CONFIG_PATH}; cada colega define ${variavel} no próprio ambiente.`);
|
|
252
261
|
return 0;
|
|
@@ -257,10 +266,10 @@ function listar(ctx, argv) {
|
|
|
257
266
|
for (const problema of problemas)
|
|
258
267
|
ctx.erro(problema);
|
|
259
268
|
const { config } = readConfig(ctx.cwd);
|
|
260
|
-
const doProjeto = config.
|
|
269
|
+
const doProjeto = config.banco_dados.conexoes.flatMap((nome) => config.banco_dados.projeto[nome] ?? []);
|
|
261
270
|
const nomes = Object.keys(conexoes).sort();
|
|
262
271
|
if (nomes.length === 0 && doProjeto.length === 0) {
|
|
263
|
-
ctx.log(
|
|
272
|
+
ctx.log('Nenhuma conexão. Na raiz do projeto, rode: npx @fontdata/ai-harness banco-dados adicionar <nome>');
|
|
264
273
|
return 0;
|
|
265
274
|
}
|
|
266
275
|
for (const nome of nomes) {
|
|
@@ -291,16 +300,16 @@ function listar(ctx, argv) {
|
|
|
291
300
|
async function testar(ctx, argv) {
|
|
292
301
|
const { posicionais } = lerArgumentos(argv, []);
|
|
293
302
|
if (posicionais.length > 1)
|
|
294
|
-
throw new Error('Uso: ai-harness banco testar [nome]');
|
|
303
|
+
throw new Error('Uso: ai-harness banco-dados testar [nome]');
|
|
295
304
|
const [nome] = posicionais;
|
|
296
305
|
const cadastro = lerCadastro(ctx.cadastro);
|
|
297
|
-
const doProjeto = nome !== undefined && nome in readConfig(ctx.cwd).config.
|
|
306
|
+
const doProjeto = nome !== undefined && nome in readConfig(ctx.cwd).config.banco_dados.projeto;
|
|
298
307
|
let alvos;
|
|
299
308
|
let ok = true;
|
|
300
309
|
if (nome !== undefined && !doProjeto) {
|
|
301
310
|
const conexao = cadastro.conexoes[nome];
|
|
302
311
|
if (!conexao) {
|
|
303
|
-
ctx.erro(`Conexão ${nome} não encontrada em ${ctx.cadastro}; rode banco adicionar ${nome}.`);
|
|
312
|
+
ctx.erro(`Conexão ${nome} não encontrada em ${ctx.cadastro}; rode banco-dados adicionar ${nome}.`);
|
|
304
313
|
return 1;
|
|
305
314
|
}
|
|
306
315
|
alvos = [{ nome, ...conexao }];
|
|
@@ -313,7 +322,7 @@ async function testar(ctx, argv) {
|
|
|
313
322
|
for (const problema of resolvidas.problemas)
|
|
314
323
|
ctx.erro(problema);
|
|
315
324
|
for (const faltando of resolvidas.faltando.filter((item) => nome === undefined || item === nome)) {
|
|
316
|
-
ctx.erro(`Conexão ${faltando} não encontrada nesta máquina; rode banco adicionar ${faltando}.`);
|
|
325
|
+
ctx.erro(`Conexão ${faltando} não encontrada nesta máquina; rode banco-dados adicionar ${faltando} --global.`);
|
|
317
326
|
ok = false;
|
|
318
327
|
}
|
|
319
328
|
for (const ausente of resolvidas.variaveisAusentes.filter(pedida)) {
|
|
@@ -327,7 +336,7 @@ async function testar(ctx, argv) {
|
|
|
327
336
|
}
|
|
328
337
|
alvos = resolvidas.conexoes.filter(pedida);
|
|
329
338
|
if (alvos.length === 0 && ok) {
|
|
330
|
-
ctx.erro('Nenhuma conexão de banco configurada neste projeto; rode npx @fontdata/ai-harness banco
|
|
339
|
+
ctx.erro('Nenhuma conexão de banco configurada neste projeto; rode npx @fontdata/ai-harness banco-dados adicionar <nome>.');
|
|
331
340
|
return 1;
|
|
332
341
|
}
|
|
333
342
|
}
|
|
@@ -346,7 +355,7 @@ async function testar(ctx, argv) {
|
|
|
346
355
|
function usar(ctx, argv) {
|
|
347
356
|
const { posicionais: nomes } = lerArgumentos(argv, []);
|
|
348
357
|
if (nomes.length === 0)
|
|
349
|
-
throw new Error('Uso: ai-harness banco usar <nome...>');
|
|
358
|
+
throw new Error('Uso: ai-harness banco-dados usar <nome...>');
|
|
350
359
|
writeDefaultConfig(ctx.cwd);
|
|
351
360
|
const arquivo = join(ctx.cwd, CONFIG_PATH);
|
|
352
361
|
let dados;
|
|
@@ -354,11 +363,11 @@ function usar(ctx, argv) {
|
|
|
354
363
|
dados = JSON.parse(readFileSync(arquivo, 'utf8'));
|
|
355
364
|
}
|
|
356
365
|
catch (erro) {
|
|
357
|
-
throw new Error(`${CONFIG_PATH} está com JSON inválido (${erro.message}); corrija antes de rodar banco usar.`);
|
|
366
|
+
throw new Error(`${CONFIG_PATH} está com JSON inválido (${erro.message}); corrija antes de rodar banco-dados usar.`);
|
|
358
367
|
}
|
|
359
368
|
if (!isObject(dados))
|
|
360
|
-
throw new Error(`${CONFIG_PATH} precisa ser um objeto JSON; corrija antes de rodar banco usar.`);
|
|
361
|
-
const banco =
|
|
369
|
+
throw new Error(`${CONFIG_PATH} precisa ser um objeto JSON; corrija antes de rodar banco-dados usar.`);
|
|
370
|
+
const banco = secaoBanco(dados);
|
|
362
371
|
const objetos = new Map();
|
|
363
372
|
for (const entrada of Array.isArray(banco.conexoes) ? banco.conexoes : []) {
|
|
364
373
|
if (isObject(entrada) && typeof entrada.nome === 'string' && !objetos.has(entrada.nome))
|
|
@@ -366,20 +375,22 @@ function usar(ctx, argv) {
|
|
|
366
375
|
}
|
|
367
376
|
const unicos = [...new Set(nomes)];
|
|
368
377
|
const proximas = unicos.map((nome) => objetos.get(nome) ?? nome);
|
|
369
|
-
writeFileSync(arquivo, `${JSON.stringify(
|
|
370
|
-
ctx.log(`
|
|
378
|
+
writeFileSync(arquivo, `${JSON.stringify(comSecaoBanco(dados, { ...banco, conexoes: proximas }), null, 2)}\n`);
|
|
379
|
+
ctx.log(`banco_dados.conexoes em ${CONFIG_PATH}: ${unicos.join(', ')}.`);
|
|
371
380
|
const { conexoes } = lerCadastro(ctx.cadastro);
|
|
372
381
|
const ausentes = unicos.filter((nome) => !(nome in conexoes) && !objetos.has(nome));
|
|
373
382
|
if (ausentes.length > 0) {
|
|
374
|
-
ctx.erro(`Aviso: sem cadastro nesta máquina para ${ausentes.join(', ')}; rode banco adicionar <nome> para cada uma.`);
|
|
383
|
+
ctx.erro(`Aviso: sem cadastro nesta máquina para ${ausentes.join(', ')}; rode banco-dados adicionar <nome> --global para cada uma.`);
|
|
375
384
|
}
|
|
376
385
|
return 0;
|
|
377
386
|
}
|
|
378
387
|
function remover(ctx, argv) {
|
|
379
|
-
const { posicionais } = lerArgumentos(argv, []);
|
|
388
|
+
const { posicionais, marcadas } = lerArgumentos(argv, ['--global']);
|
|
380
389
|
const [nome, ...sobra] = posicionais;
|
|
381
390
|
if (!nome || sobra.length > 0)
|
|
382
|
-
throw new Error('Uso: ai-harness banco remover <nome>');
|
|
391
|
+
throw new Error('Uso: ai-harness banco-dados remover <nome> [--global]');
|
|
392
|
+
if (!marcadas.has('--global'))
|
|
393
|
+
return removerDoProjeto(ctx, nome);
|
|
383
394
|
if (!removerConexao(nome, ctx.cadastro)) {
|
|
384
395
|
ctx.erro(`Conexão ${nome} não existe em ${ctx.cadastro}.`);
|
|
385
396
|
return 1;
|
|
@@ -387,7 +398,34 @@ function remover(ctx, argv) {
|
|
|
387
398
|
ctx.log(`Conexão ${nome} removida de ${ctx.cadastro}.`);
|
|
388
399
|
return 0;
|
|
389
400
|
}
|
|
390
|
-
|
|
401
|
+
function removerDoProjeto(ctx, nome) {
|
|
402
|
+
validarSettingsLocal(ctx.cwd);
|
|
403
|
+
validarConfigParaEscrita(ctx.cwd);
|
|
404
|
+
const removida = removerConexaoProjeto(ctx.cwd, nome);
|
|
405
|
+
if (removida === null) {
|
|
406
|
+
const noCadastro = nome in lerCadastro(ctx.cadastro).conexoes;
|
|
407
|
+
ctx.erro(`Conexão ${nome} não está em banco_dados.conexoes de ${CONFIG_PATH}.${noCadastro ? ` Para o cadastro desta máquina, use remover ${nome} --global.` : ''}`);
|
|
408
|
+
return 1;
|
|
409
|
+
}
|
|
410
|
+
ctx.log(`Conexão ${nome} removida de banco_dados.conexoes de ${CONFIG_PATH}.`);
|
|
411
|
+
const variavel = typeof removida === 'string' ? undefined : removida.dsn_env;
|
|
412
|
+
if (variavel === undefined)
|
|
413
|
+
return 0;
|
|
414
|
+
const atual = lerEnvSettingsLocal(ctx.cwd)[variavel];
|
|
415
|
+
if (atual !== undefined)
|
|
416
|
+
ctx.sensiveis.push(sensivel(atual));
|
|
417
|
+
const outra = Object.values(readConfig(ctx.cwd).config.banco_dados.projeto).find((conexao) => conexao.dsn_env === variavel);
|
|
418
|
+
if (outra)
|
|
419
|
+
ctx.log(`Variável ${variavel} mantida em ${SETTINGS_LOCAL_PATH}: a conexão ${outra.nome} também a usa.`);
|
|
420
|
+
else if (removerVariavelSettingsLocal(ctx.cwd, variavel).changed)
|
|
421
|
+
ctx.log(`Variável ${variavel} removida de ${SETTINGS_LOCAL_PATH}.`);
|
|
422
|
+
ctx.log(`Se ${variavel} estiver definida no .env ou no ambiente, ela não foi tocada.`);
|
|
423
|
+
return 0;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* `ai-harness banco-dados <subcomando>`: conexões do projeto (padrão), cadastro global de conexões da máquina
|
|
427
|
+
* (`--global`) e escolha das conexões do cadastro que o projeto usa.
|
|
428
|
+
*/
|
|
391
429
|
export async function runBancoCommand(argv, opcoes = {}) {
|
|
392
430
|
const saida = opcoes.saida ?? { log: (texto) => console.log(texto), erro: (texto) => console.error(texto) };
|
|
393
431
|
const cadastro = opcoes.caminhoCadastro ?? caminhoCadastro();
|
|
@@ -420,7 +458,7 @@ export async function runBancoCommand(argv, opcoes = {}) {
|
|
|
420
458
|
return 0;
|
|
421
459
|
default:
|
|
422
460
|
if (subcomando !== undefined)
|
|
423
|
-
ctx.erro(`Subcomando desconhecido: banco ${subcomando}`);
|
|
461
|
+
ctx.erro(`Subcomando desconhecido: banco-dados ${subcomando}`);
|
|
424
462
|
ctx.erro(AJUDA);
|
|
425
463
|
return 1;
|
|
426
464
|
}
|
|
@@ -72,7 +72,7 @@ function resolverDoProjeto(declarada, raiz, env, saida) {
|
|
|
72
72
|
saida.descartadas.push({ nome, motivo });
|
|
73
73
|
saida.problemas.push(`Conexão ${nome} descartada: ${motivo}.`);
|
|
74
74
|
}
|
|
75
|
-
/** Percorre
|
|
75
|
+
/** Percorre banco_dados.conexoes na ordem; sem a lista declarada no projeto, vale o DATABASE_URL do .env como a conexão `env`. */
|
|
76
76
|
export function resolverConexoes(raiz, opcoes = {}) {
|
|
77
77
|
const { config, problems } = readConfig(raiz);
|
|
78
78
|
const saida = {
|
|
@@ -83,7 +83,7 @@ export function resolverConexoes(raiz, opcoes = {}) {
|
|
|
83
83
|
problemas: [...problems],
|
|
84
84
|
segredos: [],
|
|
85
85
|
};
|
|
86
|
-
const { conexoes: nomes, projeto, declarado } = config.
|
|
86
|
+
const { conexoes: nomes, projeto, declarado } = config.banco_dados;
|
|
87
87
|
if (!declarado) {
|
|
88
88
|
const url = lerDatabaseUrl(raiz);
|
|
89
89
|
if (url) {
|
|
@@ -118,10 +118,10 @@ export function resolverConexoes(raiz, opcoes = {}) {
|
|
|
118
118
|
}
|
|
119
119
|
export function escolherConexao(resolvidas, nome) {
|
|
120
120
|
if (resolvidas.length === 0) {
|
|
121
|
-
throw new Error('Nenhuma conexão de banco configurada neste projeto; rode npx @fontdata/ai-harness banco
|
|
121
|
+
throw new Error('Nenhuma conexão de banco configurada neste projeto; rode npx @fontdata/ai-harness banco-dados adicionar <nome> na raiz do projeto');
|
|
122
122
|
}
|
|
123
123
|
const escolhida = nome === undefined ? resolvidas[0] : resolvidas.find((conexao) => conexao.nome === nome);
|
|
124
124
|
if (!escolhida)
|
|
125
|
-
throw new Error(`Conexão ${nome} não está em
|
|
125
|
+
throw new Error(`Conexão ${nome} não está em banco_dados.conexoes deste projeto`);
|
|
126
126
|
return escolhida;
|
|
127
127
|
}
|
|
@@ -6,7 +6,7 @@ import { categoriaDaColuna, mascararResultado, mascararTexto } from './lgpd/apli
|
|
|
6
6
|
import { validarLeitura } from './somente-leitura.js';
|
|
7
7
|
const CONEXAO = {
|
|
8
8
|
type: 'string',
|
|
9
|
-
description: 'Nome da conexão do projeto; sem ele vale a primeira de
|
|
9
|
+
description: 'Nome da conexão do projeto; sem ele vale a primeira de banco_dados.conexoes.',
|
|
10
10
|
};
|
|
11
11
|
export const FERRAMENTAS = [
|
|
12
12
|
{
|
|
@@ -42,7 +42,7 @@ export const FERRAMENTAS = [
|
|
|
42
42
|
limite: {
|
|
43
43
|
type: 'integer',
|
|
44
44
|
minimum: 1,
|
|
45
|
-
description: 'Máximo de linhas devolvidas; nunca passa de
|
|
45
|
+
description: 'Máximo de linhas devolvidas; nunca passa de banco_dados.limite_linhas do projeto.',
|
|
46
46
|
},
|
|
47
47
|
conexao: CONEXAO,
|
|
48
48
|
},
|
|
@@ -54,17 +54,16 @@ export const FERRAMENTAS = [
|
|
|
54
54
|
export const INSTRUCOES = [
|
|
55
55
|
'Use estas ferramentas para ler o banco de dados do projeto; o acesso é somente leitura.',
|
|
56
56
|
'Nunca peça nem repita credenciais, senhas ou DSNs no chat.',
|
|
57
|
-
'As conexões vêm do cadastro da máquina ou do projeto: uma conexão do projeto é declarada em
|
|
58
|
-
'Sem conexão configurada, oriente o usuário a rodar
|
|
57
|
+
'As conexões vêm do cadastro da máquina ou do projeto: uma conexão do projeto é declarada em banco_dados.conexoes do .ai-harness/config.json com dsn_env (o nome da variável), e o valor da variável fica no .claude/settings.local.json, no .env ou no ambiente.',
|
|
58
|
+
'Sem conexão configurada, oriente o usuário a rodar na raiz do projeto `npx @fontdata/ai-harness banco-dados adicionar <nome>`.',
|
|
59
59
|
'Se a variável de uma conexão do projeto não estiver definida, oriente a defini-la no .claude/settings.local.json, no .env ou no ambiente.',
|
|
60
60
|
'Antes de consultar, rode listar_tabelas e descrever_tabela para conhecer o esquema.',
|
|
61
61
|
'Em consultar, selecione só as colunas necessárias e filtre as linhas; evite SELECT *.',
|
|
62
62
|
'Somente leitura: comandos que alteram dados ou esquema são recusados.',
|
|
63
63
|
'Os dados pessoais (CPF, e-mail, telefone, nome e afins) vêm mascarados, e o rodapé de consultar diz o que foi mascarado. Não tente contornar o mascaramento: não concatene, fatie, converta nem compare valores para reconstruir o original. Peça só as colunas necessárias.',
|
|
64
64
|
].join('\n');
|
|
65
|
-
const ORIENTACAO_USAR = 'rode npx @fontdata/ai-harness banco usar <nome> no projeto';
|
|
66
65
|
function orientarAdicionar(nome) {
|
|
67
|
-
return `Conexão ${nome} não encontrada nesta máquina; rode npx @fontdata/ai-harness banco adicionar ${nome}`;
|
|
66
|
+
return `Conexão ${nome} não encontrada nesta máquina; rode npx @fontdata/ai-harness banco-dados adicionar ${nome} --global`;
|
|
68
67
|
}
|
|
69
68
|
function orientarVariavel(nome, variavel) {
|
|
70
69
|
return `A variável ${variavel} da conexão ${nome} não está definida; defina-a no .claude/settings.local.json, no .env ou no ambiente`;
|
|
@@ -125,7 +124,7 @@ function lerLimite(valor, banco) {
|
|
|
125
124
|
}
|
|
126
125
|
return Math.min(valor, banco.limite_linhas);
|
|
127
126
|
}
|
|
128
|
-
/** Sem nome, vale a primeira declarada em
|
|
127
|
+
/** Sem nome, vale a primeira declarada em banco_dados.conexoes; se ela não estiver disponível, o erro é dela, nunca outra conexão. */
|
|
129
128
|
function escolher(resolvidas, banco, nome) {
|
|
130
129
|
const alvo = nome ?? banco.conexoes[0];
|
|
131
130
|
if (alvo !== undefined) {
|
|
@@ -173,7 +172,7 @@ async function executar(nome, args, contexto, banco, resolvidas) {
|
|
|
173
172
|
if (descartadas.length > 0)
|
|
174
173
|
dados.descartadas = descartadas;
|
|
175
174
|
if (conexoes.length + faltando.length + variaveisAusentes.length + descartadas.length === 0) {
|
|
176
|
-
dados.orientacao =
|
|
175
|
+
dados.orientacao = 'Nenhuma conexão configurada; rode npx @fontdata/ai-harness banco-dados adicionar <nome> na raiz do projeto';
|
|
177
176
|
}
|
|
178
177
|
if (resolvidas.problemas.length > 0)
|
|
179
178
|
dados.problemas = resolvidas.problemas;
|
|
@@ -183,7 +182,7 @@ async function executar(nome, args, contexto, banco, resolvidas) {
|
|
|
183
182
|
return erro(`Ferramenta desconhecida: ${nome}`);
|
|
184
183
|
const conexao = escolher(resolvidas, banco, argumentoTexto(args, 'conexao', false));
|
|
185
184
|
if (conexao.producao && banco.producao === 'bloqueio') {
|
|
186
|
-
return erro(`Conexão ${conexao.nome} é de produção e está bloqueada (
|
|
185
|
+
return erro(`Conexão ${conexao.nome} é de produção e está bloqueada (banco_dados.producao: "bloqueio"). Use uma cópia ou troque banco_dados.producao para "aviso" em .ai-harness/config.json de propósito.`);
|
|
187
186
|
}
|
|
188
187
|
let dados;
|
|
189
188
|
let rodape = '';
|
|
@@ -229,7 +228,7 @@ export async function executarFerramenta(nome, args, contexto) {
|
|
|
229
228
|
conexoes = resolvidas.conexoes;
|
|
230
229
|
segredos = resolvidas.segredos;
|
|
231
230
|
const argumentos = typeof args === 'object' && args !== null && !Array.isArray(args) ? args : {};
|
|
232
|
-
const resultado = await executar(nome, argumentos, contexto, config.
|
|
231
|
+
const resultado = await executar(nome, argumentos, contexto, config.banco_dados, resolvidas);
|
|
233
232
|
const texto = limpar(resultado.content[0].text, conexoes, segredos);
|
|
234
233
|
resultado.content[0].text = resultado.isError ? mascararTexto(texto) : texto;
|
|
235
234
|
return resultado;
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { allCommands, loadCatalog } from './catalog.js';
|
|
6
6
|
import { runCommand, runnableIds, suggestIds } from './run.js';
|
|
7
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
/** Comandos renomeados depois da 2.7.0: o nome antigo aponta o novo em vez de cair em "Comando desconhecido". */
|
|
9
|
+
const RENAMED_COMMANDS = { banco: 'banco-dados', 'banco-mcp': 'banco-dados-mcp' };
|
|
8
10
|
const RESERVED = new Set([
|
|
9
11
|
'init',
|
|
10
12
|
'doctor',
|
|
@@ -14,8 +16,8 @@ const RESERVED = new Set([
|
|
|
14
16
|
'conflitos',
|
|
15
17
|
'comentarios',
|
|
16
18
|
'memoria',
|
|
17
|
-
'banco',
|
|
18
|
-
'banco-mcp',
|
|
19
|
+
'banco-dados',
|
|
20
|
+
'banco-dados-mcp',
|
|
19
21
|
'list',
|
|
20
22
|
'run',
|
|
21
23
|
'help',
|
|
@@ -49,15 +51,15 @@ Gerenciar:
|
|
|
49
51
|
init [caminho] Instala os slash commands em .claude/commands do projeto (interativo)
|
|
50
52
|
doctor [caminho] Diagnostica a instalação (arquivos, lock, skills, agents, hooks, Node)
|
|
51
53
|
update [caminho] Atualiza o que foi instalado: --check só lista, --yes aplica, --forcar sobrescreve conflitos
|
|
52
|
-
--sem-banco: não registra o MCP de banco no .mcp.json e lembra a recusa no config
|
|
54
|
+
--sem-banco-dados: não registra o MCP de banco no .mcp.json e lembra a recusa no config
|
|
53
55
|
migrar [caminho] Move docs/specs, plans, bugs e investigacoes para docs/trabalhos/<id>/ (--yes aplica)
|
|
54
56
|
--reclassificar: planos já migrados em em-execucao viram concluido ou legado
|
|
55
57
|
comentarios [caminho] Instala o hook de comentários e a catraca do legado (--yes aplica)
|
|
56
58
|
memoria [caminho] Índice de quem mexeu em cada arquivo (--arquivo <caminho>, --regressao <id do bug>)
|
|
57
59
|
evento <evento> Grava no rastro do trabalho (--trabalho, --task, --nome, --resultado, --dados '{json}')
|
|
58
60
|
conflitos <id> Aponta trabalhos em execução que mexem nos mesmos arquivos (--aguardar espera liberar)
|
|
59
|
-
banco <subcomando> Conexões de banco somente leitura: adicionar, listar, testar, usar, remover
|
|
60
|
-
banco-mcp
|
|
61
|
+
banco-dados <subcomando> Conexões de banco somente leitura: adicionar, listar, testar, usar, remover
|
|
62
|
+
banco-dados-mcp Servidor MCP de leitura do banco, usado pelo Claude Code (stdio)
|
|
61
63
|
list Lista todos os comandos disponíveis no catálogo
|
|
62
64
|
|
|
63
65
|
Init sem perguntas (CI):
|
|
@@ -66,7 +68,7 @@ Init sem perguntas (CI):
|
|
|
66
68
|
--sem-hooks não instala os hooks do harness
|
|
67
69
|
--sem-agents não instala os agents somente leitura
|
|
68
70
|
--sem-comentarios não instala a rule, o hook e a catraca de comentários
|
|
69
|
-
--sem-banco
|
|
71
|
+
--sem-banco-dados não registra o servidor MCP de banco no .mcp.json
|
|
70
72
|
--claude-md criar|manter|backup|sobrescrever (padrão: criar)
|
|
71
73
|
--conflito sobrescrever|pular (padrão: pular)
|
|
72
74
|
--yes aplica; sem ele o init só simula
|
|
@@ -152,12 +154,12 @@ async function main() {
|
|
|
152
154
|
const { runConflictsCommand } = await import('./trabalhos/conflitos.js');
|
|
153
155
|
process.exit(await runConflictsCommand(argv.slice(1)));
|
|
154
156
|
}
|
|
155
|
-
if (cmd === 'banco') {
|
|
156
|
-
const { runBancoCommand } = await import('./banco/command.js');
|
|
157
|
+
if (cmd === 'banco-dados') {
|
|
158
|
+
const { runBancoCommand } = await import('./banco-dados/command.js');
|
|
157
159
|
process.exit(await runBancoCommand(argv.slice(1)));
|
|
158
160
|
}
|
|
159
|
-
if (cmd === 'banco-mcp') {
|
|
160
|
-
const { iniciarServidorMcp } = await import('./banco/mcp.js');
|
|
161
|
+
if (cmd === 'banco-dados-mcp') {
|
|
162
|
+
const { iniciarServidorMcp } = await import('./banco-dados/mcp.js');
|
|
161
163
|
await iniciarServidorMcp({ entrada: process.stdin, saida: process.stdout });
|
|
162
164
|
return;
|
|
163
165
|
}
|
|
@@ -180,6 +182,11 @@ async function main() {
|
|
|
180
182
|
const { args, claudeFlags } = splitArgs(argv.slice(1));
|
|
181
183
|
process.exit(runCommand(cmd, args, claudeFlags));
|
|
182
184
|
}
|
|
185
|
+
const renamed = RENAMED_COMMANDS[cmd];
|
|
186
|
+
if (renamed) {
|
|
187
|
+
console.error(`O comando ${cmd} mudou de nome: use ${renamed}.`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
183
190
|
if (!RESERVED.has(cmd)) {
|
|
184
191
|
const near = suggestIds(cmd.replace(/^\//, ''), ids);
|
|
185
192
|
console.error(`Comando desconhecido: ${cmd}`);
|