@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.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "fontdata",
3
- "version": "2.7.0",
3
+ "version": "3.1.0",
4
4
  "mcpServers": {
5
- "banco": { "command": "npx", "args": ["-y", "@fontdata/ai-harness@2.7.0", "banco-mcp"] }
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 --projeto # pergunta os dados, testa e grava ERP_DATABASE_URL
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 --projeto` grava `{ "nome": "erp", "dsn_env": "ERP_DATABASE_URL" }` em `banco.conexoes` e a variável
137
- no `.claude/settings.local.json`. Faça commit do `config.json`; cada colega roda o mesmo comando, que grava só a
138
- variável. `"producao": true` na entrada vale para a equipe toda. Quem prefere um cadastro por máquina
139
- (`~/.config/ai-harness/conexoes.json`) usa `banco adicionar erp-dev` sem `--projeto` e `banco usar erp-dev` no
140
- projeto.
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 `banco` entrega ao Claude `listar_conexoes`, `listar_tabelas`, `descrever_tabela` e `consultar`,
143
- tudo somente leitura. Ele vem no plugin; sem plugin, o `init` e o `update` o registram no `.mcp.json` (`--sem-banco`
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
- `banco.lgpd.liberar_colunas` e `banco.lgpd.mascarar_colunas`; desligar, só no cadastro da máquina. Conexões do
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-de-dados.md).
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(novo, null, 2)}\n`, { mode: modo });
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
- ' cadastra a conexão nesta máquina (sem --dsn, pergunta os dados)',
16
- ' adicionar <nome> --projeto [--env <VAR>] [--dsn <dsn>] [--producao] [--sem-testar] [--substituir]',
17
- ' grava o DSN na variável <NOME>_DATABASE_URL (ou --env) do .claude/settings.local.json',
18
- ' e a conexão { nome, dsn_env } em banco.conexoes do .ai-harness/config.json',
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 banco.conexoes no .ai-harness/config.json (mantém as entradas { nome, dsn_env })',
22
- ' remover <nome> apaga a conexão do cadastro',
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', '--projeto'], ['--dsn', '--env']);
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> [--projeto [--env <VAR>]] [--dsn <dsn>] [--producao] [--sem-testar] [--substituir]');
125
- if (marcadas.has('--projeto'))
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 só vale com --projeto.');
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 banco.conexoes; recusa lista inválida antes de qualquer gravação. */
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) && isObject(dados.banco) ? dados.banco : {};
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(`banco.conexoes em ${CONFIG_PATH} não é uma lista; corrija-o antes de gravar a conexão ${nome}.`);
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.banco.projeto[nome];
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 banco.conexoes de ${CONFIG_PATH} sem dsn_env. Use --substituir para trocá-la pela conexão do projeto.`);
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 banco.conexoes de ${CONFIG_PATH}; a entrada não foi alterada.`);
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 banco.conexoes de ${CONFIG_PATH} (dsn_env ${variavel})${entrada.producao ? ', produção' : ''}.`);
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.banco.conexoes.flatMap((nome) => config.banco.projeto[nome] ?? []);
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(`Nenhuma conexão cadastrada em ${ctx.cadastro}. Cadastre com: npx @fontdata/ai-harness banco adicionar <nome>`);
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.banco.projeto;
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 usar <nome>.');
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 = isObject(dados.banco) ? dados.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({ ...dados, banco: { ...banco, conexoes: proximas } }, null, 2)}\n`);
370
- ctx.log(`banco.conexoes em ${CONFIG_PATH}: ${unicos.join(', ')}.`);
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
- /** `ai-harness banco <subcomando>`: cadastro de conexões da máquina e escolha das conexões do projeto. */
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 banco.conexoes na ordem; sem a lista declarada no projeto, vale o DATABASE_URL do .env como a conexão `env`. */
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.banco;
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 usar <nome>');
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 banco.conexoes deste projeto`);
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 banco.conexoes.',
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 banco.limite_linhas do projeto.',
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 banco.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 no terminal `npx @fontdata/ai-harness banco adicionar <nome>` e, no projeto, `npx @fontdata/ai-harness banco usar <nome>`.',
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 banco.conexoes; se ela não estiver disponível, o erro é dela, nunca outra conexão. */
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 = `Nenhuma conexão configurada; rode npx @fontdata/ai-harness banco adicionar <nome> e ${ORIENTACAO_USAR}`;
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 (banco.producao: "bloqueio"). Use uma cópia ou troque banco.producao para "aviso" em .ai-harness/config.json de propósito.`);
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.banco, resolvidas);
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 Servidor MCP de leitura do banco, usado pelo Claude Code (stdio)
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 não registra o servidor MCP de banco no .mcp.json
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}`);