@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
package/dist/config/config.js
CHANGED
|
@@ -16,13 +16,53 @@ export const TIPOS_CONEXAO_PROJETO = ['postgres', 'mariadb', 'sqlite'];
|
|
|
16
16
|
function isObject(value) {
|
|
17
17
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
18
18
|
}
|
|
19
|
+
/** Seção do banco no config; até a 2.7.0 ela se chamava `banco`. */
|
|
20
|
+
export const BANCO_CHAVE = 'banco_dados';
|
|
21
|
+
export const BANCO_CHAVE_LEGADA = 'banco';
|
|
22
|
+
/** Só a seção com o nome antigo existe: ela vale e o update a renomeia. */
|
|
23
|
+
function soSecaoLegada(raw) {
|
|
24
|
+
return !(BANCO_CHAVE in raw) && BANCO_CHAVE_LEGADA in raw;
|
|
25
|
+
}
|
|
26
|
+
/** A seção do banco no JSON bruto: `banco_dados`, ou a `banco` antiga quando só ela existe. */
|
|
27
|
+
export function secaoBanco(raw) {
|
|
28
|
+
const secao = soSecaoLegada(raw) ? raw[BANCO_CHAVE_LEGADA] : raw[BANCO_CHAVE];
|
|
29
|
+
return isObject(secao) ? secao : {};
|
|
30
|
+
}
|
|
31
|
+
/** O JSON com a seção do banco gravada como `banco_dados`; a `banco` antiga é trocada na mesma posição. */
|
|
32
|
+
export function comSecaoBanco(raw, secao) {
|
|
33
|
+
if (!soSecaoLegada(raw))
|
|
34
|
+
return { ...raw, [BANCO_CHAVE]: secao };
|
|
35
|
+
return Object.fromEntries(Object.entries(raw).map(([chave, valor]) => (chave === BANCO_CHAVE_LEGADA ? [BANCO_CHAVE, secao] : [chave, valor])));
|
|
36
|
+
}
|
|
37
|
+
function lerJsonBruto(root) {
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(readFileSync(join(root, CONFIG_PATH), 'utf8'));
|
|
40
|
+
return isObject(parsed) ? parsed : undefined;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** O config ainda tem a seção do banco com o nome antigo (`banco`), que o update renomeia. */
|
|
47
|
+
export function temSecaoBancoLegada(root) {
|
|
48
|
+
const raw = lerJsonBruto(root);
|
|
49
|
+
return raw !== undefined && soSecaoLegada(raw);
|
|
50
|
+
}
|
|
51
|
+
/** Renomeia a seção `banco` para `banco_dados`, preservando o resto do JSON; devolve se mudou. */
|
|
52
|
+
export function migrarSecaoBanco(root) {
|
|
53
|
+
const raw = lerJsonBruto(root);
|
|
54
|
+
if (!raw || !soSecaoLegada(raw))
|
|
55
|
+
return false;
|
|
56
|
+
writeFileSync(join(root, CONFIG_PATH), `${JSON.stringify(comSecaoBanco(raw, secaoBanco(raw)), null, 2)}\n`);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
19
59
|
export function defaultConfig() {
|
|
20
60
|
return {
|
|
21
61
|
config_versao: CONFIG_VERSION,
|
|
22
62
|
hooks: { ...DEFAULT_HOOK_MODES },
|
|
23
63
|
rastro: { versionar: false },
|
|
24
64
|
bloqueios: { dias_para_aviso: 7 },
|
|
25
|
-
|
|
65
|
+
banco_dados: {
|
|
26
66
|
conexoes: [],
|
|
27
67
|
producao: 'bloqueio',
|
|
28
68
|
limite_linhas: 200,
|
|
@@ -41,23 +81,23 @@ const SO_NO_CADASTRO = 'O mascaramento LGPD só pode ser desligado no cadastro d
|
|
|
41
81
|
const REGRA_DE_COLUNA = /^[^.\s]+(\.[^.\s]+){0,2}$/;
|
|
42
82
|
function lerLgpd(raw, lgpd, problems) {
|
|
43
83
|
if (!isObject(raw)) {
|
|
44
|
-
problems.push(`
|
|
84
|
+
problems.push(`banco_dados.lgpd deve ser um objeto com mascarar_colunas e liberar_colunas em ${CONFIG_PATH}.`);
|
|
45
85
|
return;
|
|
46
86
|
}
|
|
47
87
|
if ('desligar' in raw)
|
|
48
|
-
problems.push(`${SO_NO_CADASTRO};
|
|
88
|
+
problems.push(`${SO_NO_CADASTRO}; banco_dados.lgpd.desligar em ${CONFIG_PATH} foi ignorado.`);
|
|
49
89
|
for (const chave of ['mascarar_colunas', 'liberar_colunas']) {
|
|
50
90
|
if (!(chave in raw))
|
|
51
91
|
continue;
|
|
52
92
|
const lista = raw[chave];
|
|
53
93
|
if (!Array.isArray(lista)) {
|
|
54
|
-
problems.push(`
|
|
94
|
+
problems.push(`banco_dados.lgpd.${chave} deve ser uma lista de colunas em ${CONFIG_PATH}.`);
|
|
55
95
|
continue;
|
|
56
96
|
}
|
|
57
97
|
lgpd[chave] = lista.filter((regra) => {
|
|
58
98
|
if (typeof regra === 'string' && REGRA_DE_COLUNA.test(regra))
|
|
59
99
|
return true;
|
|
60
|
-
problems.push(`Entrada ${JSON.stringify(regra)} ignorada em
|
|
100
|
+
problems.push(`Entrada ${JSON.stringify(regra)} ignorada em banco_dados.lgpd.${chave} de ${CONFIG_PATH}; use coluna, tabela.coluna ou schema.tabela.coluna.`);
|
|
61
101
|
return false;
|
|
62
102
|
});
|
|
63
103
|
}
|
|
@@ -83,7 +123,7 @@ function motivoConexaoInvalida(entrada) {
|
|
|
83
123
|
function lerConexoes(conexoes, banco, problems) {
|
|
84
124
|
banco.declarado = !(Array.isArray(conexoes) && conexoes.length === 0);
|
|
85
125
|
if (!Array.isArray(conexoes)) {
|
|
86
|
-
problems.push(`
|
|
126
|
+
problems.push(`banco_dados.conexoes deve ser uma lista de nomes ou de objetos { nome, dsn_env } em ${CONFIG_PATH}.`);
|
|
87
127
|
return;
|
|
88
128
|
}
|
|
89
129
|
for (const entrada of conexoes) {
|
|
@@ -94,17 +134,17 @@ function lerConexoes(conexoes, banco, problems) {
|
|
|
94
134
|
else if (isObject(entrada)) {
|
|
95
135
|
const motivo = motivoConexaoInvalida(entrada);
|
|
96
136
|
if (motivo) {
|
|
97
|
-
problems.push(`Entrada ${citada} descartada em
|
|
137
|
+
problems.push(`Entrada ${citada} descartada em banco_dados.conexoes de ${CONFIG_PATH}: ${motivo}.`);
|
|
98
138
|
continue;
|
|
99
139
|
}
|
|
100
140
|
nome = entrada.nome;
|
|
101
141
|
}
|
|
102
142
|
else {
|
|
103
|
-
problems.push(`Entrada ${citada} descartada em
|
|
143
|
+
problems.push(`Entrada ${citada} descartada em banco_dados.conexoes de ${CONFIG_PATH}; use um nome (texto não vazio) ou um objeto { nome, dsn_env }.`);
|
|
104
144
|
continue;
|
|
105
145
|
}
|
|
106
146
|
if (banco.conexoes.includes(nome)) {
|
|
107
|
-
problems.push(`Entrada ${citada} descartada em
|
|
147
|
+
problems.push(`Entrada ${citada} descartada em banco_dados.conexoes de ${CONFIG_PATH}: o nome "${nome}" já foi declarado antes.`);
|
|
108
148
|
continue;
|
|
109
149
|
}
|
|
110
150
|
banco.conexoes.push(nome);
|
|
@@ -112,8 +152,8 @@ function lerConexoes(conexoes, banco, problems) {
|
|
|
112
152
|
continue;
|
|
113
153
|
for (const chave of Object.keys(entrada).filter((chave) => !CHAVES_CONEXAO_PROJETO.includes(chave))) {
|
|
114
154
|
problems.push(chave === 'mascaramento'
|
|
115
|
-
? `${SO_NO_CADASTRO}; mascaramento da entrada ${citada} em
|
|
116
|
-
: `Chave desconhecida ${chave} ignorada na entrada ${citada} de
|
|
155
|
+
? `${SO_NO_CADASTRO}; mascaramento da entrada ${citada} em banco_dados.conexoes de ${CONFIG_PATH} foi ignorado.`
|
|
156
|
+
: `Chave desconhecida ${chave} ignorada na entrada ${citada} de banco_dados.conexoes em ${CONFIG_PATH}.`);
|
|
117
157
|
}
|
|
118
158
|
banco.projeto[nome] = {
|
|
119
159
|
nome,
|
|
@@ -130,28 +170,28 @@ function lerBanco(raw, banco, problems) {
|
|
|
130
170
|
if (MODOS_PRODUCAO.includes(raw.producao))
|
|
131
171
|
banco.producao = raw.producao;
|
|
132
172
|
else
|
|
133
|
-
problems.push(`Modo "${String(raw.producao)}" inválido para
|
|
173
|
+
problems.push(`Modo "${String(raw.producao)}" inválido para banco_dados.producao em ${CONFIG_PATH}; use aviso ou bloqueio.`);
|
|
134
174
|
}
|
|
135
175
|
if ('limite_linhas' in raw) {
|
|
136
176
|
if (inteiroEntre(raw.limite_linhas, 1, 5000))
|
|
137
177
|
banco.limite_linhas = raw.limite_linhas;
|
|
138
178
|
else
|
|
139
|
-
problems.push(`
|
|
179
|
+
problems.push(`banco_dados.limite_linhas deve ser um inteiro de 1 a 5000 em ${CONFIG_PATH}.`);
|
|
140
180
|
}
|
|
141
181
|
if ('timeout_segundos' in raw) {
|
|
142
182
|
if (inteiroEntre(raw.timeout_segundos, 1, 600))
|
|
143
183
|
banco.timeout_segundos = raw.timeout_segundos;
|
|
144
184
|
else
|
|
145
|
-
problems.push(`
|
|
185
|
+
problems.push(`banco_dados.timeout_segundos deve ser um inteiro de 1 a 600 em ${CONFIG_PATH}.`);
|
|
146
186
|
}
|
|
147
187
|
if ('registrar_mcp' in raw) {
|
|
148
188
|
if (typeof raw.registrar_mcp === 'boolean')
|
|
149
189
|
banco.registrar_mcp = raw.registrar_mcp;
|
|
150
190
|
else
|
|
151
|
-
problems.push(`
|
|
191
|
+
problems.push(`banco_dados.registrar_mcp deve ser true ou false em ${CONFIG_PATH}.`);
|
|
152
192
|
}
|
|
153
193
|
if ('mascaramento' in raw)
|
|
154
|
-
problems.push(`${SO_NO_CADASTRO};
|
|
194
|
+
problems.push(`${SO_NO_CADASTRO}; banco_dados.mascaramento em ${CONFIG_PATH} foi ignorado.`);
|
|
155
195
|
if ('lgpd' in raw)
|
|
156
196
|
lerLgpd(raw.lgpd, banco.lgpd, problems);
|
|
157
197
|
}
|
|
@@ -192,17 +232,24 @@ export function readConfig(root) {
|
|
|
192
232
|
else
|
|
193
233
|
problems.push(`bloqueios.dias_para_aviso deve ser um inteiro positivo em ${CONFIG_PATH}.`);
|
|
194
234
|
}
|
|
195
|
-
|
|
196
|
-
|
|
235
|
+
const secao = soSecaoLegada(raw) ? raw[BANCO_CHAVE_LEGADA] : raw[BANCO_CHAVE];
|
|
236
|
+
if (isObject(secao))
|
|
237
|
+
lerBanco(secao, config.banco_dados, problems);
|
|
238
|
+
if (soSecaoLegada(raw)) {
|
|
239
|
+
problems.push(`A seção banco de ${CONFIG_PATH} agora se chama banco_dados; rode npx @fontdata/ai-harness update --yes para renomeá-la.`);
|
|
240
|
+
}
|
|
241
|
+
else if (BANCO_CHAVE_LEGADA in raw) {
|
|
242
|
+
problems.push(`A seção banco de ${CONFIG_PATH} foi ignorada: vale a banco_dados. Apague a banco.`);
|
|
243
|
+
}
|
|
197
244
|
return { config, problems };
|
|
198
245
|
}
|
|
199
|
-
/** Cria a config com os padrões, sem a seção
|
|
246
|
+
/** Cria a config com os padrões, sem a seção banco_dados; retorna false se já existia (nunca sobrescreve). */
|
|
200
247
|
export function writeDefaultConfig(root) {
|
|
201
248
|
const file = join(root, CONFIG_PATH);
|
|
202
249
|
if (existsSync(file))
|
|
203
250
|
return false;
|
|
204
251
|
mkdirSync(dirname(file), { recursive: true });
|
|
205
|
-
const {
|
|
252
|
+
const { banco_dados: _banco, ...gravada } = defaultConfig();
|
|
206
253
|
writeFileSync(file, `${JSON.stringify(gravada, null, 2)}\n`);
|
|
207
254
|
return true;
|
|
208
255
|
}
|
|
@@ -222,10 +269,10 @@ function lerJsonParaEscrita(file, chave) {
|
|
|
222
269
|
export function validarConfigParaEscrita(root) {
|
|
223
270
|
const file = join(root, CONFIG_PATH);
|
|
224
271
|
if (existsSync(file))
|
|
225
|
-
lerJsonParaEscrita(file, '
|
|
272
|
+
lerJsonParaEscrita(file, 'banco_dados.conexoes');
|
|
226
273
|
}
|
|
227
274
|
/**
|
|
228
|
-
* Grava a conexão em `
|
|
275
|
+
* Grava a conexão em `banco_dados.conexoes`, preservando o resto do JSON; sem config, cria uma com os padrões.
|
|
229
276
|
* Se o nome já existir (objeto ou string), lança, ou troca a entrada na mesma posição com `substituir`.
|
|
230
277
|
* JSON inválido é recusado sem tocar no arquivo.
|
|
231
278
|
*/
|
|
@@ -234,14 +281,14 @@ export function gravarConexaoProjeto(root, conexao, { substituir = false } = {})
|
|
|
234
281
|
if (motivo)
|
|
235
282
|
throw new Error(`Conexão ${JSON.stringify(conexao.nome)} inválida: ${motivo}.`);
|
|
236
283
|
const file = join(root, CONFIG_PATH);
|
|
237
|
-
const raw = existsSync(file) ? lerJsonParaEscrita(file, '
|
|
238
|
-
const banco = raw
|
|
284
|
+
const raw = existsSync(file) ? lerJsonParaEscrita(file, 'banco_dados.conexoes') : undefined;
|
|
285
|
+
const banco = raw ? secaoBanco(raw) : {};
|
|
239
286
|
const conexoes = banco.conexoes === undefined ? [] : banco.conexoes;
|
|
240
287
|
if (!Array.isArray(conexoes))
|
|
241
|
-
throw new Error(`
|
|
288
|
+
throw new Error(`banco_dados.conexoes em ${CONFIG_PATH} não é uma lista; corrija-o antes de gravar a conexão ${conexao.nome}.`);
|
|
242
289
|
const posicao = conexoes.findIndex((entrada) => (isObject(entrada) ? entrada.nome : entrada) === conexao.nome);
|
|
243
290
|
if (posicao !== -1 && !substituir)
|
|
244
|
-
throw new Error(`A conexão ${conexao.nome} já está em
|
|
291
|
+
throw new Error(`A conexão ${conexao.nome} já está em banco_dados.conexoes de ${CONFIG_PATH}.`);
|
|
245
292
|
const entrada = {
|
|
246
293
|
nome: conexao.nome,
|
|
247
294
|
dsn_env: conexao.dsn_env,
|
|
@@ -251,28 +298,52 @@ export function gravarConexaoProjeto(root, conexao, { substituir = false } = {})
|
|
|
251
298
|
const proximas = posicao === -1 ? [...conexoes, entrada] : conexoes.map((atual, i) => (i === posicao ? entrada : atual));
|
|
252
299
|
if (!raw)
|
|
253
300
|
writeDefaultConfig(root);
|
|
254
|
-
const base = raw ?? lerJsonParaEscrita(file, '
|
|
255
|
-
writeFileSync(file, `${JSON.stringify(
|
|
301
|
+
const base = raw ?? lerJsonParaEscrita(file, 'banco_dados.conexoes');
|
|
302
|
+
writeFileSync(file, `${JSON.stringify(comSecaoBanco(base, { ...banco, conexoes: proximas }), null, 2)}\n`);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Tira de `banco_dados.conexoes` a primeira entrada com o nome (objeto, mesmo inválido, ou texto), preservando
|
|
306
|
+
* o resto do JSON, e devolve o que leu. Sem config ou sem o nome, devolve null e não escreve.
|
|
307
|
+
* JSON inválido ou `conexoes` que não é lista lança sem tocar no arquivo.
|
|
308
|
+
*/
|
|
309
|
+
export function removerConexaoProjeto(root, nome) {
|
|
310
|
+
const file = join(root, CONFIG_PATH);
|
|
311
|
+
if (!existsSync(file))
|
|
312
|
+
return null;
|
|
313
|
+
const raw = lerJsonParaEscrita(file, 'banco_dados.conexoes');
|
|
314
|
+
const banco = secaoBanco(raw);
|
|
315
|
+
const conexoes = banco.conexoes === undefined ? [] : banco.conexoes;
|
|
316
|
+
if (!Array.isArray(conexoes))
|
|
317
|
+
throw new Error(`banco_dados.conexoes em ${CONFIG_PATH} não é uma lista; corrija-o antes de remover a conexão ${nome}.`);
|
|
318
|
+
const posicao = conexoes.findIndex((entrada) => (isObject(entrada) ? entrada.nome : entrada) === nome);
|
|
319
|
+
if (posicao === -1)
|
|
320
|
+
return null;
|
|
321
|
+
const entrada = conexoes[posicao];
|
|
322
|
+
const proximas = conexoes.filter((_atual, i) => i !== posicao);
|
|
323
|
+
writeFileSync(file, `${JSON.stringify(comSecaoBanco(raw, { ...banco, conexoes: proximas }), null, 2)}\n`);
|
|
324
|
+
if (!isObject(entrada))
|
|
325
|
+
return nome;
|
|
326
|
+
return typeof entrada.dsn_env === 'string' ? { nome, dsn_env: entrada.dsn_env } : { nome };
|
|
256
327
|
}
|
|
257
328
|
/**
|
|
258
|
-
* Grava só `
|
|
329
|
+
* Grava só `banco_dados.registrar_mcp`, preservando o resto do JSON; sem config, cria uma com os padrões
|
|
259
330
|
* e essa chave. JSON inválido é recusado sem tocar no arquivo.
|
|
260
331
|
*/
|
|
261
332
|
export function gravarRegistrarMcp(root, valor) {
|
|
262
333
|
const file = join(root, CONFIG_PATH);
|
|
263
334
|
let raw;
|
|
264
335
|
if (existsSync(file)) {
|
|
265
|
-
raw = lerJsonParaEscrita(file, '
|
|
336
|
+
raw = lerJsonParaEscrita(file, 'banco_dados.registrar_mcp');
|
|
266
337
|
}
|
|
267
338
|
else {
|
|
268
|
-
const {
|
|
339
|
+
const { banco_dados: _banco, ...padrao } = defaultConfig();
|
|
269
340
|
raw = padrao;
|
|
270
341
|
}
|
|
271
|
-
const banco =
|
|
342
|
+
const banco = secaoBanco(raw);
|
|
272
343
|
if (banco.registrar_mcp === valor)
|
|
273
344
|
return;
|
|
274
345
|
mkdirSync(dirname(file), { recursive: true });
|
|
275
|
-
writeFileSync(file, `${JSON.stringify(
|
|
346
|
+
writeFileSync(file, `${JSON.stringify(comSecaoBanco(raw, { ...banco, registrar_mcp: valor }), null, 2)}\n`);
|
|
276
347
|
}
|
|
277
348
|
const BLOCK_START = '# ai-harness (gerenciado pelo harness; não edite entre estas linhas)';
|
|
278
349
|
const BLOCK_END = '# /ai-harness';
|
package/dist/doctor/checks.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
2
2
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { lerEnvSettingsLocal, lerVariavel, SETTINGS_LOCAL_PATH } from '../banco/ambiente.js';
|
|
5
|
-
import { caminhoCadastro, lerCadastro } from '../banco/cadastro.js';
|
|
4
|
+
import { lerEnvSettingsLocal, lerVariavel, SETTINGS_LOCAL_PATH } from '../banco-dados/ambiente.js';
|
|
5
|
+
import { caminhoCadastro, lerCadastro } from '../banco-dados/cadastro.js';
|
|
6
6
|
import { allCommands, loadCatalog } from '../catalog.js';
|
|
7
7
|
import { CONFIG_PATH, readConfig } from '../config/config.js';
|
|
8
8
|
import { readArtifact, validateAllWorks, WORKS_DIR } from '../contratos/artefatos.js';
|
|
@@ -414,7 +414,7 @@ function settingsLocalIgnorado(root, env) {
|
|
|
414
414
|
.some((linha) => LINHAS_IGNORE_SETTINGS_LOCAL.has(linha));
|
|
415
415
|
}
|
|
416
416
|
function checkBancoProjeto(root, config, env) {
|
|
417
|
-
const conexoes = config.
|
|
417
|
+
const conexoes = config.banco_dados.conexoes.flatMap((nome) => config.banco_dados.projeto[nome] ?? []);
|
|
418
418
|
if (!conexoes.length)
|
|
419
419
|
return [];
|
|
420
420
|
const findings = [];
|
|
@@ -424,8 +424,8 @@ function checkBancoProjeto(root, config, env) {
|
|
|
424
424
|
findings.push({
|
|
425
425
|
id: 'banco-variavel-ausente',
|
|
426
426
|
severidade: 'aviso',
|
|
427
|
-
mensagem: `A conexão ${nome}, declarada em
|
|
428
|
-
correcao: `Defina ${dsn_env} com \`npx @fontdata/ai-harness banco adicionar ${nome}
|
|
427
|
+
mensagem: `A conexão ${nome}, declarada em banco_dados.conexoes de ${join(root, CONFIG_PATH)}, lê o DSN da variável ${dsn_env}, que não está definida no ambiente, em ${SETTINGS_LOCAL_PATH} nem no .env.`,
|
|
428
|
+
correcao: `Defina ${dsn_env} com \`npx @fontdata/ai-harness banco-dados adicionar ${nome}\` ou no ambiente do terminal.`,
|
|
429
429
|
});
|
|
430
430
|
}
|
|
431
431
|
const envLocal = lerEnvSettingsLocal(root);
|
|
@@ -453,12 +453,12 @@ function checkBancoProjeto(root, config, env) {
|
|
|
453
453
|
}
|
|
454
454
|
return findings;
|
|
455
455
|
}
|
|
456
|
-
/** Confere o cadastro desta máquina e as variáveis das conexões do projeto contra `
|
|
456
|
+
/** Confere o cadastro desta máquina e as variáveis das conexões do projeto contra `banco_dados.conexoes`, sem abrir conexão. */
|
|
457
457
|
export function checkBanco(root, config, env = process.env) {
|
|
458
|
-
if (!config.
|
|
458
|
+
if (!config.banco_dados.conexoes.length)
|
|
459
459
|
return [];
|
|
460
460
|
const findings = checkBancoProjeto(root, config, env);
|
|
461
|
-
const doCadastro = config.
|
|
461
|
+
const doCadastro = config.banco_dados.conexoes.filter((nome) => !(nome in config.banco_dados.projeto));
|
|
462
462
|
if (!doCadastro.length)
|
|
463
463
|
return findings;
|
|
464
464
|
const caminho = caminhoCadastro(env);
|
|
@@ -486,8 +486,8 @@ export function checkBanco(root, config, env = process.env) {
|
|
|
486
486
|
findings.push({
|
|
487
487
|
id: 'banco-conexao-ausente',
|
|
488
488
|
severidade: 'aviso',
|
|
489
|
-
mensagem: `A conexão ${nome}, citada em
|
|
490
|
-
correcao: `Cadastre-a com \`npx @fontdata/ai-harness banco adicionar ${nome}\`.`,
|
|
489
|
+
mensagem: `A conexão ${nome}, citada em banco_dados.conexoes de ${join(root, CONFIG_PATH)}, não está no cadastro desta máquina (${caminho}).`,
|
|
490
|
+
correcao: `Cadastre-a com \`npx @fontdata/ai-harness banco-dados adicionar ${nome} --global\`.`,
|
|
491
491
|
});
|
|
492
492
|
}
|
|
493
493
|
for (const nome of doCadastro.filter((nome) => conexoes[nome]?.mascaramento === false)) {
|
package/dist/init.js
CHANGED
|
@@ -14,7 +14,7 @@ const PLUGIN_HOOKS_NOTE = 'O plugin fontdata@ai-harness está habilitado: os hoo
|
|
|
14
14
|
const PLUGIN_MCP_NOTE = 'O plugin fontdata@ai-harness está habilitado: o servidor MCP de banco já roda por ele e não foi registrado no .mcp.json, para não duplicar.';
|
|
15
15
|
/** Registra o MCP de banco no .mcp.json quando não foi recusado (flag, wizard ou config) e o plugin não o fornece. */
|
|
16
16
|
export function decidirRegistroMcp(options, root, home = homedir()) {
|
|
17
|
-
return options.mcpBanco && readConfig(root).config.
|
|
17
|
+
return options.mcpBanco && readConfig(root).config.banco_dados.registrar_mcp && !isHarnessPluginEnabled(root, home);
|
|
18
18
|
}
|
|
19
19
|
function applyMcpDecision(plan, options, root) {
|
|
20
20
|
plan.registerMcp = decidirRegistroMcp(options, root);
|
package/dist/install/flags.js
CHANGED
|
@@ -7,7 +7,7 @@ const CLAUDE_MD = {
|
|
|
7
7
|
};
|
|
8
8
|
const CONFLICTS = { sobrescrever: 'overwrite', pular: 'skip' };
|
|
9
9
|
const VALUE_FLAGS = ['--escopo', '--comandos', '--claude-md', '--conflito'];
|
|
10
|
-
const BOOLEAN_FLAGS = ['--sem-harness', '--sem-hooks', '--sem-agents', '--sem-comentarios', '--sem-banco', '--yes', '-y', '--sim'];
|
|
10
|
+
const BOOLEAN_FLAGS = ['--sem-harness', '--sem-hooks', '--sem-agents', '--sem-comentarios', '--sem-banco-dados', '--yes', '-y', '--sim'];
|
|
11
11
|
function pick(flag, value, options) {
|
|
12
12
|
const chosen = options[value];
|
|
13
13
|
if (chosen === undefined) {
|
|
@@ -28,8 +28,13 @@ function distance(a, b) {
|
|
|
28
28
|
}
|
|
29
29
|
return row[b.length] ?? 0;
|
|
30
30
|
}
|
|
31
|
+
/** Flags que mudaram de nome: o erro aponta a nova, que a distância de edição não alcança. */
|
|
32
|
+
const RENAMED_FLAGS = { '--sem-banco': '--sem-banco-dados' };
|
|
31
33
|
/** Flag conhecida mais parecida com a digitada, para sugerir em mensagens de erro. */
|
|
32
34
|
export function suggestFlag(token, flags) {
|
|
35
|
+
const renamed = RENAMED_FLAGS[token];
|
|
36
|
+
if (renamed && flags.includes(renamed))
|
|
37
|
+
return renamed;
|
|
33
38
|
const [best] = flags
|
|
34
39
|
.map((flag) => ({ flag, d: distance(token, flag) }))
|
|
35
40
|
.filter(({ d }) => d <= 2)
|
|
@@ -69,7 +74,7 @@ export function parseInitArgs(argv) {
|
|
|
69
74
|
args.agents = false;
|
|
70
75
|
else if (token === '--sem-comentarios')
|
|
71
76
|
args.comments = false;
|
|
72
|
-
else if (token === '--sem-banco')
|
|
77
|
+
else if (token === '--sem-banco-dados')
|
|
73
78
|
args.mcpBanco = false;
|
|
74
79
|
else
|
|
75
80
|
args.yes = true;
|
package/dist/install/mcp.js
CHANGED
|
@@ -2,13 +2,17 @@ import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { timestamp } from './settings.js';
|
|
4
4
|
export const MCP_PATH = '.mcp.json';
|
|
5
|
+
/** Nome do servidor de banco no `.mcp.json`; as ferramentas ficam `mcp__banco_dados__*`. */
|
|
6
|
+
export const BANCO_MCP_NOME = 'banco_dados';
|
|
7
|
+
/** Nome do servidor até a 2.7.0; a entrada gerenciada com ele é trocada pela nova no update. */
|
|
8
|
+
export const BANCO_MCP_NOME_LEGADO = 'banco';
|
|
5
9
|
const PIN_PREFIX = '@fontdata/ai-harness@';
|
|
6
10
|
function isPlainObject(value) {
|
|
7
11
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
12
|
}
|
|
9
13
|
/** Servidor MCP de banco com a versão do pacote fixada no npx, no mesmo formato do plugin. */
|
|
10
14
|
export function bancoMcpServer(versao) {
|
|
11
|
-
return { command: 'npx', args: ['-y', `${PIN_PREFIX}${versao}`, 'banco-mcp'] };
|
|
15
|
+
return { command: 'npx', args: ['-y', `${PIN_PREFIX}${versao}`, 'banco-dados-mcp'] };
|
|
12
16
|
}
|
|
13
17
|
function pinOf(entrada) {
|
|
14
18
|
if (!isPlainObject(entrada) || !Array.isArray(entrada.args))
|
|
@@ -22,9 +26,10 @@ export function ehEntradaGerenciada(entrada) {
|
|
|
22
26
|
/**
|
|
23
27
|
* Acrescenta os servidores ausentes em `mcpServers` do `.mcp.json`, preservando o resto. Com
|
|
24
28
|
* `atualizarGerenciada`, regrava também a entrada gerenciada que fixa outra versão; entrada
|
|
25
|
-
* personalizada nunca é tocada.
|
|
29
|
+
* personalizada nunca é tocada. Os nomes de `removerGerenciadas` saem só quando a entrada é gerenciada. Backup
|
|
30
|
+
* datado só quando muda; JSON inválido é recusado sem tocar.
|
|
26
31
|
*/
|
|
27
|
-
export function mergeMcpServers(root, servidores, now = new Date(), { atualizarGerenciada = false } = {}) {
|
|
32
|
+
export function mergeMcpServers(root, servidores, now = new Date(), { atualizarGerenciada = false, removerGerenciadas = [] } = {}) {
|
|
28
33
|
const file = join(root, MCP_PATH);
|
|
29
34
|
const original = existsSync(file) ? readFileSync(file, 'utf8') : null;
|
|
30
35
|
let config = {};
|
|
@@ -43,6 +48,12 @@ export function mergeMcpServers(root, servidores, now = new Date(), { atualizarG
|
|
|
43
48
|
}
|
|
44
49
|
const mcpServers = { ...(config.mcpServers ?? {}) };
|
|
45
50
|
let changed = false;
|
|
51
|
+
for (const nome of removerGerenciadas) {
|
|
52
|
+
if (!ehEntradaGerenciada(mcpServers[nome]))
|
|
53
|
+
continue;
|
|
54
|
+
delete mcpServers[nome];
|
|
55
|
+
changed = true;
|
|
56
|
+
}
|
|
46
57
|
for (const [nome, servidor] of Object.entries(servidores)) {
|
|
47
58
|
const atual = mcpServers[nome];
|
|
48
59
|
if (atual === undefined) {
|
package/dist/install/plan.js
CHANGED
|
@@ -6,10 +6,10 @@ import { applyAtomically } from './atomic.js';
|
|
|
6
6
|
import { buildLockItem, LOCK_VERSION, readLock, writeLock } from './lock.js';
|
|
7
7
|
import { HOOKS_ITEM_ID, PROJECT_HOOKS_DIR, projectHooksConfig } from './hooks.js';
|
|
8
8
|
import { neededSkillsFor, resolveSelection } from './selection.js';
|
|
9
|
-
import { bancoMcpServer, MCP_PATH, mergeMcpServers } from './mcp.js';
|
|
9
|
+
import { BANCO_MCP_NOME, bancoMcpServer, MCP_PATH, mergeMcpServers } from './mcp.js';
|
|
10
10
|
import { gravarRegistrarMcp } from '../config/config.js';
|
|
11
11
|
import { COMMENTS_RULE_PATH, commentsHooksConfig, ensureCommentsBaseline } from '../comentarios/instalar.js';
|
|
12
|
-
import { combineHooks, HARNESS_DENY,
|
|
12
|
+
import { combineHooks, HARNESS_DENY, harnessPermissions, mergeSettings } from './settings.js';
|
|
13
13
|
/** Rule de convenção com o que é específico do projeto (D-38): é do projeto, então fica fora do lock. */
|
|
14
14
|
export const PROJECT_RULE_PATH = '.claude/rules/harness-projeto.md';
|
|
15
15
|
/** Esqueleto da rule de projeto; os comandos `-legado` renomeados pelo update entram na última seção. */
|
|
@@ -165,12 +165,12 @@ export function executePlan(root, plan, pkg, now = new Date()) {
|
|
|
165
165
|
const permission = needsEventPermission(plan);
|
|
166
166
|
const hooks = combineHooks(plan.registerHooks ? projectHooksConfig() : {}, plan.registerComments ? commentsHooksConfig() : {});
|
|
167
167
|
if (Object.keys(hooks).length || permission) {
|
|
168
|
-
mergeSettings(root, { hooks, allow: permission ?
|
|
168
|
+
mergeSettings(root, { hooks, allow: permission ? harnessPermissions({ banco: bancoAtivo(plan) }) : [], deny: permission ? HARNESS_DENY : [] }, now);
|
|
169
169
|
}
|
|
170
170
|
if (plan.registerComments)
|
|
171
171
|
ensureCommentsBaseline(root);
|
|
172
172
|
if (plan.registerMcp)
|
|
173
|
-
mergeMcpServers(root, {
|
|
173
|
+
mergeMcpServers(root, { [BANCO_MCP_NOME]: bancoMcpServer(pkg.versao) }, now);
|
|
174
174
|
if (plan.mcpRecusado)
|
|
175
175
|
gravarRegistrarMcp(root, false);
|
|
176
176
|
const items = new Map();
|
|
@@ -193,6 +193,10 @@ export function executePlan(root, plan, pkg, now = new Date()) {
|
|
|
193
193
|
writeLock(root, lock);
|
|
194
194
|
return lock;
|
|
195
195
|
}
|
|
196
|
+
/** O banco não foi recusado (`--sem-banco-dados` ou `banco_dados.registrar_mcp: false`): as permissões do MCP entram. */
|
|
197
|
+
function bancoAtivo(plan) {
|
|
198
|
+
return !plan.mcpRecusado && plan.mcpSkipReason !== 'config';
|
|
199
|
+
}
|
|
196
200
|
/** Skills e agents do harness registram o rastro com `npx @fontdata/ai-harness evento`. */
|
|
197
201
|
function needsEventPermission(plan) {
|
|
198
202
|
return plan.items.some((item) => item.tipo === 'skill' || item.tipo === 'agent');
|
|
@@ -209,7 +213,7 @@ export function describePlan(plan) {
|
|
|
209
213
|
if (plan.registerHooks)
|
|
210
214
|
lines.push('Hooks do harness registrados em .claude/settings.json (sem duplicar os existentes).');
|
|
211
215
|
if (needsEventPermission(plan)) {
|
|
212
|
-
lines.push(`Permissões ${
|
|
216
|
+
lines.push(`Permissões ${harnessPermissions({ banco: bancoAtivo(plan) }).join(', ')} em .claude/settings.json, para o rastro, a checagem de conflitos, ` +
|
|
213
217
|
'o /git-commit-push, os sub-agents e as consultas de leitura não pedirem aprovação (inclusive no modo auto); ' +
|
|
214
218
|
'o push forçado fica bloqueado em permissions.deny.');
|
|
215
219
|
}
|
|
@@ -217,13 +221,13 @@ export function describePlan(plan) {
|
|
|
217
221
|
lines.push(`Servidor MCP de banco (somente leitura) registrado em ${MCP_PATH}, sem duplicar os servidores existentes.`);
|
|
218
222
|
}
|
|
219
223
|
else if (plan.mcpRecusado) {
|
|
220
|
-
lines.push(`Servidor MCP de banco não registrado (recusado); a escolha fica em
|
|
224
|
+
lines.push(`Servidor MCP de banco não registrado (recusado); a escolha fica em banco_dados.registrar_mcp: false no .ai-harness/config.json.`);
|
|
221
225
|
}
|
|
222
226
|
else if (plan.mcpSkipReason === 'plugin') {
|
|
223
227
|
lines.push(`Servidor MCP de banco não registrado em ${MCP_PATH}: o plugin fontdata@ai-harness já o fornece.`);
|
|
224
228
|
}
|
|
225
229
|
else if (plan.mcpSkipReason === 'config') {
|
|
226
|
-
lines.push(`Servidor MCP de banco não registrado em ${MCP_PATH}:
|
|
230
|
+
lines.push(`Servidor MCP de banco não registrado em ${MCP_PATH}: banco_dados.registrar_mcp está false no .ai-harness/config.json.`);
|
|
227
231
|
}
|
|
228
232
|
if (plan.registerComments) {
|
|
229
233
|
lines.push('Padrão de comentários: rule carregada em toda sessão, hook que bloqueia a violação e catraca do legado (gerada só na primeira instalação).');
|
package/dist/install/settings.js
CHANGED
|
@@ -33,11 +33,14 @@ export const READ_PERMISSIONS = [
|
|
|
33
33
|
'Bash(gh pr diff:*)',
|
|
34
34
|
'Bash(npm outdated:*)',
|
|
35
35
|
];
|
|
36
|
+
const BANCO_TOOLS = ['listar_conexoes', 'listar_tabelas', 'descrever_tabela', 'consultar'];
|
|
36
37
|
/**
|
|
37
|
-
* Ferramentas
|
|
38
|
-
*
|
|
38
|
+
* Ferramentas do servidor MCP de banco, registrado pelo `.mcp.json` (`banco_dados`) ou pelo plugin
|
|
39
|
+
* (`banco_dados`). A `consultar` também é liberada: os dados pessoais chegam mascarados.
|
|
39
40
|
*/
|
|
40
|
-
export const BANCO_PERMISSIONS = ['
|
|
41
|
+
export const BANCO_PERMISSIONS = ['mcp__banco_dados__', 'mcp__plugin_fontdata_banco_dados__'].flatMap((prefixo) => BANCO_TOOLS.map((ferramenta) => `${prefixo}${ferramenta}`));
|
|
42
|
+
/** As mesmas regras com o nome antigo do servidor (`banco`, até a 2.7.0); o update as remove. */
|
|
43
|
+
export const BANCO_PERMISSIONS_LEGADAS = ['mcp__banco__', 'mcp__plugin_fontdata_banco__'].flatMap((prefixo) => BANCO_TOOLS.map((ferramenta) => `${prefixo}${ferramenta}`));
|
|
41
44
|
/** Regras de `permissions.allow` que o init e o update gravam para as skills e os agents do harness. */
|
|
42
45
|
export const HARNESS_PERMISSIONS = [
|
|
43
46
|
EVENT_PERMISSION,
|
|
@@ -48,6 +51,10 @@ export const HARNESS_PERMISSIONS = [
|
|
|
48
51
|
...READ_PERMISSIONS,
|
|
49
52
|
...BANCO_PERMISSIONS,
|
|
50
53
|
];
|
|
54
|
+
/** As regras do harness; sem o banco (recusado com `--sem-banco-dados` ou `banco_dados.registrar_mcp: false`), sem as do MCP. */
|
|
55
|
+
export function harnessPermissions({ banco }) {
|
|
56
|
+
return banco ? HARNESS_PERMISSIONS : HARNESS_PERMISSIONS.filter((rule) => !BANCO_PERMISSIONS.includes(rule));
|
|
57
|
+
}
|
|
51
58
|
/** O push liberado não vale para reescrever o histórico remoto: `deny` vence o `allow`. */
|
|
52
59
|
export const HARNESS_DENY = [
|
|
53
60
|
'Bash(git push --force:*)',
|
|
@@ -92,7 +99,7 @@ export function mergeHooksIntoSettings(root, hooks, now = new Date()) {
|
|
|
92
99
|
* Acrescenta hooks e regras de `permissions.allow` ao `.claude/settings.json` do projeto
|
|
93
100
|
* sem perder o que já existe. Um hook é considerado presente quando algum grupo do mesmo
|
|
94
101
|
* evento já tem o mesmo `command`, e uma regra quando já está na lista, o que torna a
|
|
95
|
-
* operação idempotente. Tudo é gravado de uma vez, com um único backup datado do arquivo
|
|
102
|
+
* operação idempotente. As regras de `removeAllow` saem de `permissions.allow`. Tudo é gravado de uma vez, com um único backup datado do arquivo
|
|
96
103
|
* original; JSON inválido é recusado sem tocar em nada.
|
|
97
104
|
*/
|
|
98
105
|
export function mergeSettings(root, additions, now = new Date()) {
|
|
@@ -155,6 +162,12 @@ export function mergeSettings(root, additions, now = new Date()) {
|
|
|
155
162
|
}
|
|
156
163
|
next.permissions = { ...permissions, [key]: list };
|
|
157
164
|
}
|
|
165
|
+
const remove = additions.removeAllow ?? [];
|
|
166
|
+
const currentAllow = next.permissions?.allow;
|
|
167
|
+
if (remove.length && currentAllow?.some((rule) => remove.includes(rule))) {
|
|
168
|
+
next.permissions = { ...next.permissions, allow: currentAllow.filter((rule) => !remove.includes(rule)) };
|
|
169
|
+
changed = true;
|
|
170
|
+
}
|
|
158
171
|
if (!changed)
|
|
159
172
|
return { changed: false, backup: null };
|
|
160
173
|
let backup = null;
|