@fontdata/ai-harness 2.5.1 → 2.7.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 +4 -1
- package/README.md +35 -1
- package/dist/banco/ambiente.js +98 -0
- package/dist/banco/cadastro.js +179 -0
- package/dist/banco/command.js +432 -0
- package/dist/banco/conexao.js +127 -0
- package/dist/banco/drivers/index.js +13 -0
- package/dist/banco/drivers/mariadb.js +121 -0
- package/dist/banco/drivers/postgres.js +157 -0
- package/dist/banco/drivers/sqlite.js +92 -0
- package/dist/banco/drivers/tipos.js +29 -0
- package/dist/banco/ferramentas.js +241 -0
- package/dist/banco/lgpd/aplicar.js +168 -0
- package/dist/banco/lgpd/detectar.js +210 -0
- package/dist/banco/lgpd/mascarar.js +114 -0
- package/dist/banco/mcp.js +101 -0
- package/dist/banco/somente-leitura.js +221 -0
- package/dist/cli.js +15 -0
- package/dist/config/config.js +206 -2
- package/dist/doctor/checks.js +107 -2
- package/dist/init.js +25 -1
- package/dist/install/flags.js +4 -1
- package/dist/install/mcp.js +66 -0
- package/dist/install/plan.js +19 -0
- package/dist/install/settings.js +17 -2
- package/dist/update/update.js +78 -11
- package/package.json +4 -1
package/dist/init.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { EOL } from 'node:os';
|
|
2
|
+
import { EOL, homedir } from 'node:os';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
import * as p from '@clack/prompts';
|
|
5
5
|
import { allCommands, loadCatalog, recommendedCommands } from './catalog.js';
|
|
@@ -11,6 +11,17 @@ import { describePlan, executePlan, packageInfo, planInstall, resolveInstallOpti
|
|
|
11
11
|
import { detectProjectKind } from './install/selection.js';
|
|
12
12
|
export { detectProjectKind, neededSkillsFor, resolveSelection } from './install/selection.js';
|
|
13
13
|
const PLUGIN_HOOKS_NOTE = 'O plugin fontdata@ai-harness está habilitado: os hooks já rodam por ele e não foram instalados no projeto, para não rodarem duas vezes.';
|
|
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
|
+
/** 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
|
+
export function decidirRegistroMcp(options, root, home = homedir()) {
|
|
17
|
+
return options.mcpBanco && readConfig(root).config.banco.registrar_mcp && !isHarnessPluginEnabled(root, home);
|
|
18
|
+
}
|
|
19
|
+
function applyMcpDecision(plan, options, root) {
|
|
20
|
+
plan.registerMcp = decidirRegistroMcp(options, root);
|
|
21
|
+
plan.mcpRecusado = !options.mcpBanco;
|
|
22
|
+
if (!plan.registerMcp && options.mcpBanco)
|
|
23
|
+
plan.mcpSkipReason = isHarnessPluginEnabled(root) ? 'plugin' : 'config';
|
|
24
|
+
}
|
|
14
25
|
/** Encerra o fluxo com mensagem amigável quando o usuário cancela (Ctrl+C). */
|
|
15
26
|
function bail(value) {
|
|
16
27
|
if (p.isCancel(value)) {
|
|
@@ -97,6 +108,7 @@ function runNonInteractive(args) {
|
|
|
97
108
|
if (skippedForPlugin)
|
|
98
109
|
options.hooks = false;
|
|
99
110
|
const plan = planInstall(catalog, repoPath, options);
|
|
111
|
+
applyMcpDecision(plan, options, repoPath);
|
|
100
112
|
const summary = [describePlan(plan), ...(skippedForPlugin ? [PLUGIN_HOOKS_NOTE] : [])].join(EOL + EOL);
|
|
101
113
|
if (!args.yes) {
|
|
102
114
|
console.log(`Simulação — nada foi gravado em ${repoPath}.\n\n${summary}\n\nRode de novo com --yes para aplicar.`);
|
|
@@ -184,6 +196,16 @@ async function runWizard(args) {
|
|
|
184
196
|
initialValue: true,
|
|
185
197
|
}));
|
|
186
198
|
}
|
|
199
|
+
let mcpBanco = true;
|
|
200
|
+
if (isHarnessPluginEnabled(repoPath)) {
|
|
201
|
+
p.note(PLUGIN_MCP_NOTE, 'Banco');
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
mcpBanco = bail(await p.confirm({
|
|
205
|
+
message: 'Registrar o servidor MCP de banco (somente leitura) no .mcp.json?',
|
|
206
|
+
initialValue: true,
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
187
209
|
const agents = bail(await p.confirm({
|
|
188
210
|
message: 'Instalar os agents somente leitura do harness (revisor-testes, qa, investigador, auditor-plano)?',
|
|
189
211
|
initialValue: true,
|
|
@@ -201,6 +223,7 @@ async function runWizard(args) {
|
|
|
201
223
|
hooks,
|
|
202
224
|
agents,
|
|
203
225
|
comments,
|
|
226
|
+
mcpBanco,
|
|
204
227
|
};
|
|
205
228
|
let plan = planInstall(catalog, repoPath, options);
|
|
206
229
|
// 6. Confirmação final
|
|
@@ -221,6 +244,7 @@ async function runWizard(args) {
|
|
|
221
244
|
if (conflict === 'skip')
|
|
222
245
|
plan = planInstall(catalog, repoPath, { ...options, conflict });
|
|
223
246
|
}
|
|
247
|
+
applyMcpDecision(plan, options, repoPath);
|
|
224
248
|
// Execução
|
|
225
249
|
const s = p.spinner();
|
|
226
250
|
s.start('Instalando...');
|
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', '--yes', '-y', '--sim'];
|
|
10
|
+
const BOOLEAN_FLAGS = ['--sem-harness', '--sem-hooks', '--sem-agents', '--sem-comentarios', '--sem-banco', '--yes', '-y', '--sim'];
|
|
11
11
|
function pick(flag, value, options) {
|
|
12
12
|
const chosen = options[value];
|
|
13
13
|
if (chosen === undefined) {
|
|
@@ -48,6 +48,7 @@ export function parseInitArgs(argv) {
|
|
|
48
48
|
hooks: undefined,
|
|
49
49
|
agents: undefined,
|
|
50
50
|
comments: undefined,
|
|
51
|
+
mcpBanco: undefined,
|
|
51
52
|
yes: false,
|
|
52
53
|
hasSelectionFlags: false,
|
|
53
54
|
};
|
|
@@ -68,6 +69,8 @@ export function parseInitArgs(argv) {
|
|
|
68
69
|
args.agents = false;
|
|
69
70
|
else if (token === '--sem-comentarios')
|
|
70
71
|
args.comments = false;
|
|
72
|
+
else if (token === '--sem-banco')
|
|
73
|
+
args.mcpBanco = false;
|
|
71
74
|
else
|
|
72
75
|
args.yes = true;
|
|
73
76
|
continue;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { timestamp } from './settings.js';
|
|
4
|
+
export const MCP_PATH = '.mcp.json';
|
|
5
|
+
const PIN_PREFIX = '@fontdata/ai-harness@';
|
|
6
|
+
function isPlainObject(value) {
|
|
7
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
/** Servidor MCP de banco com a versão do pacote fixada no npx, no mesmo formato do plugin. */
|
|
10
|
+
export function bancoMcpServer(versao) {
|
|
11
|
+
return { command: 'npx', args: ['-y', `${PIN_PREFIX}${versao}`, 'banco-mcp'] };
|
|
12
|
+
}
|
|
13
|
+
function pinOf(entrada) {
|
|
14
|
+
if (!isPlainObject(entrada) || !Array.isArray(entrada.args))
|
|
15
|
+
return undefined;
|
|
16
|
+
return entrada.args.find((arg) => typeof arg === 'string' && arg.includes(PIN_PREFIX));
|
|
17
|
+
}
|
|
18
|
+
/** Entrada gravada pelo harness: os args fixam `@fontdata/ai-harness@<versão>`. */
|
|
19
|
+
export function ehEntradaGerenciada(entrada) {
|
|
20
|
+
return pinOf(entrada) !== undefined;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Acrescenta os servidores ausentes em `mcpServers` do `.mcp.json`, preservando o resto. Com
|
|
24
|
+
* `atualizarGerenciada`, regrava também a entrada gerenciada que fixa outra versão; entrada
|
|
25
|
+
* personalizada nunca é tocada. Backup datado só quando muda; JSON inválido é recusado sem tocar.
|
|
26
|
+
*/
|
|
27
|
+
export function mergeMcpServers(root, servidores, now = new Date(), { atualizarGerenciada = false } = {}) {
|
|
28
|
+
const file = join(root, MCP_PATH);
|
|
29
|
+
const original = existsSync(file) ? readFileSync(file, 'utf8') : null;
|
|
30
|
+
let config = {};
|
|
31
|
+
if (original !== null) {
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = JSON.parse(original);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
parsed = undefined;
|
|
38
|
+
}
|
|
39
|
+
if (!isPlainObject(parsed) || (parsed.mcpServers !== undefined && !isPlainObject(parsed.mcpServers))) {
|
|
40
|
+
throw new Error(`O ${MCP_PATH} está com JSON inválido; corrija-o antes de registrar o servidor MCP de banco.`);
|
|
41
|
+
}
|
|
42
|
+
config = parsed;
|
|
43
|
+
}
|
|
44
|
+
const mcpServers = { ...(config.mcpServers ?? {}) };
|
|
45
|
+
let changed = false;
|
|
46
|
+
for (const [nome, servidor] of Object.entries(servidores)) {
|
|
47
|
+
const atual = mcpServers[nome];
|
|
48
|
+
if (atual === undefined) {
|
|
49
|
+
mcpServers[nome] = servidor;
|
|
50
|
+
changed = true;
|
|
51
|
+
}
|
|
52
|
+
else if (atualizarGerenciada && ehEntradaGerenciada(atual) && pinOf(atual) !== pinOf(servidor)) {
|
|
53
|
+
mcpServers[nome] = { ...atual, command: servidor.command, args: servidor.args };
|
|
54
|
+
changed = true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!changed)
|
|
58
|
+
return { changed: false, backup: null };
|
|
59
|
+
let backup = null;
|
|
60
|
+
if (original !== null) {
|
|
61
|
+
backup = `${MCP_PATH}.bak-${timestamp(now)}`;
|
|
62
|
+
copyFileSync(file, join(root, backup));
|
|
63
|
+
}
|
|
64
|
+
writeFileSync(file, `${JSON.stringify({ ...config, mcpServers }, null, 2)}\n`);
|
|
65
|
+
return { changed: true, backup };
|
|
66
|
+
}
|
package/dist/install/plan.js
CHANGED
|
@@ -6,6 +6,8 @@ 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';
|
|
10
|
+
import { gravarRegistrarMcp } from '../config/config.js';
|
|
9
11
|
import { COMMENTS_RULE_PATH, commentsHooksConfig, ensureCommentsBaseline } from '../comentarios/instalar.js';
|
|
10
12
|
import { combineHooks, HARNESS_DENY, HARNESS_PERMISSIONS, mergeSettings } from './settings.js';
|
|
11
13
|
/** Rule de convenção com o que é específico do projeto (D-38): é do projeto, então fica fora do lock. */
|
|
@@ -27,6 +29,7 @@ export function resolveInstallOptions(args) {
|
|
|
27
29
|
hooks: args.hooks ?? true,
|
|
28
30
|
agents: args.agents ?? true,
|
|
29
31
|
comments: args.comments ?? true,
|
|
32
|
+
mcpBanco: args.mcpBanco ?? true,
|
|
30
33
|
};
|
|
31
34
|
}
|
|
32
35
|
export function packageInfo() {
|
|
@@ -166,6 +169,10 @@ export function executePlan(root, plan, pkg, now = new Date()) {
|
|
|
166
169
|
}
|
|
167
170
|
if (plan.registerComments)
|
|
168
171
|
ensureCommentsBaseline(root);
|
|
172
|
+
if (plan.registerMcp)
|
|
173
|
+
mergeMcpServers(root, { banco: bancoMcpServer(pkg.versao) }, now);
|
|
174
|
+
if (plan.mcpRecusado)
|
|
175
|
+
gravarRegistrarMcp(root, false);
|
|
169
176
|
const items = new Map();
|
|
170
177
|
for (const item of previous?.itens ?? [])
|
|
171
178
|
items.set(`${item.tipo}:${item.id}`, item);
|
|
@@ -206,6 +213,18 @@ export function describePlan(plan) {
|
|
|
206
213
|
'o /git-commit-push, os sub-agents e as consultas de leitura não pedirem aprovação (inclusive no modo auto); ' +
|
|
207
214
|
'o push forçado fica bloqueado em permissions.deny.');
|
|
208
215
|
}
|
|
216
|
+
if (plan.registerMcp) {
|
|
217
|
+
lines.push(`Servidor MCP de banco (somente leitura) registrado em ${MCP_PATH}, sem duplicar os servidores existentes.`);
|
|
218
|
+
}
|
|
219
|
+
else if (plan.mcpRecusado) {
|
|
220
|
+
lines.push(`Servidor MCP de banco não registrado (recusado); a escolha fica em banco.registrar_mcp: false no .ai-harness/config.json.`);
|
|
221
|
+
}
|
|
222
|
+
else if (plan.mcpSkipReason === 'plugin') {
|
|
223
|
+
lines.push(`Servidor MCP de banco não registrado em ${MCP_PATH}: o plugin fontdata@ai-harness já o fornece.`);
|
|
224
|
+
}
|
|
225
|
+
else if (plan.mcpSkipReason === 'config') {
|
|
226
|
+
lines.push(`Servidor MCP de banco não registrado em ${MCP_PATH}: banco.registrar_mcp está false no .ai-harness/config.json.`);
|
|
227
|
+
}
|
|
209
228
|
if (plan.registerComments) {
|
|
210
229
|
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).');
|
|
211
230
|
}
|
package/dist/install/settings.js
CHANGED
|
@@ -9,6 +9,8 @@ export const CONFLICTS_PERMISSION = 'Bash(npx @fontdata/ai-harness conflitos:*)'
|
|
|
9
9
|
export const GIT_PERMISSIONS = ['Bash(git add:*)', 'Bash(git commit:*)', 'Bash(git push:*)'];
|
|
10
10
|
/** O /executar-plan e as skills de fluxo delegam a sub-agents a cada task e gate. */
|
|
11
11
|
export const AGENT_PERMISSION = 'Agent';
|
|
12
|
+
/** Leitura e busca de arquivos e a invocação das skills, que todos os fluxos usam o tempo todo. */
|
|
13
|
+
export const TOOL_PERMISSIONS = ['Read', 'Grep', 'Glob', 'Skill'];
|
|
12
14
|
/**
|
|
13
15
|
* Comandos só de leitura que as skills, os comandos e os agents rodam. Ficam de fora os que regravam a instalação
|
|
14
16
|
* (`init`, `update`, `migrar`), os de teste e build, que variam por projeto, e o `npm audit`, cujo prefixo
|
|
@@ -31,8 +33,21 @@ export const READ_PERMISSIONS = [
|
|
|
31
33
|
'Bash(gh pr diff:*)',
|
|
32
34
|
'Bash(npm outdated:*)',
|
|
33
35
|
];
|
|
36
|
+
/**
|
|
37
|
+
* Ferramentas de esquema do servidor MCP de banco, registrado pelo `.mcp.json` ou pelo plugin. A `consultar`
|
|
38
|
+
* fica de fora: lê linhas das tabelas e continua pedindo aprovação.
|
|
39
|
+
*/
|
|
40
|
+
export const BANCO_PERMISSIONS = ['mcp__banco__', 'mcp__plugin_fontdata_banco__'].flatMap((prefixo) => ['listar_conexoes', 'listar_tabelas', 'descrever_tabela', 'consultar'].map((ferramenta) => `${prefixo}${ferramenta}`));
|
|
34
41
|
/** Regras de `permissions.allow` que o init e o update gravam para as skills e os agents do harness. */
|
|
35
|
-
export const HARNESS_PERMISSIONS = [
|
|
42
|
+
export const HARNESS_PERMISSIONS = [
|
|
43
|
+
EVENT_PERMISSION,
|
|
44
|
+
CONFLICTS_PERMISSION,
|
|
45
|
+
...GIT_PERMISSIONS,
|
|
46
|
+
AGENT_PERMISSION,
|
|
47
|
+
...TOOL_PERMISSIONS,
|
|
48
|
+
...READ_PERMISSIONS,
|
|
49
|
+
...BANCO_PERMISSIONS,
|
|
50
|
+
];
|
|
36
51
|
/** O push liberado não vale para reescrever o histórico remoto: `deny` vence o `allow`. */
|
|
37
52
|
export const HARNESS_DENY = [
|
|
38
53
|
'Bash(git push --force:*)',
|
|
@@ -45,7 +60,7 @@ export const HARNESS_DENY = [
|
|
|
45
60
|
function isPlainObject(value) {
|
|
46
61
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
47
62
|
}
|
|
48
|
-
function timestamp(date) {
|
|
63
|
+
export function timestamp(date) {
|
|
49
64
|
const pad = (n) => String(n).padStart(2, '0');
|
|
50
65
|
return (`${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-` +
|
|
51
66
|
`${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`);
|
package/dist/update/update.js
CHANGED
|
@@ -2,15 +2,43 @@ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync
|
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { allCommands, loadCatalog } from '../catalog.js';
|
|
4
4
|
import { commentsHooksConfig, ensureCommentsBaseline } from '../comentarios/instalar.js';
|
|
5
|
-
import { readConfig, syncGitignore, writeDefaultConfig } from '../config/config.js';
|
|
5
|
+
import { CONFIG_PATH, gravarRegistrarMcp, readConfig, syncGitignore, writeDefaultConfig } from '../config/config.js';
|
|
6
6
|
import { applyAtomically } from '../install/atomic.js';
|
|
7
7
|
import { suggestFlag } from '../install/flags.js';
|
|
8
8
|
import { HOOKS_ITEM_ID, isHarnessPluginEnabled, projectHooksConfig } from '../install/hooks.js';
|
|
9
|
+
import { bancoMcpServer, ehEntradaGerenciada, MCP_PATH, mergeMcpServers } from '../install/mcp.js';
|
|
9
10
|
import { hashContent, LOCK_PATH, LOCK_VERSION, readLock, writeLock } from '../install/lock.js';
|
|
10
11
|
import { itemSources, packageInfo, PROJECT_RULE_PATH, projectRuleContent } from '../install/plan.js';
|
|
11
12
|
import { neededSkillsFor } from '../install/selection.js';
|
|
12
13
|
import { combineHooks, HARNESS_DENY, HARNESS_PERMISSIONS, hasAllowRule, mergeSettings } from '../install/settings.js';
|
|
13
14
|
import { adoptLegacyInstall } from './legado.js';
|
|
15
|
+
function isPlainObject(value) {
|
|
16
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
/** Espelha o `mergeMcpServers` com `atualizarGerenciada`; JSON inválido vira `invalido`, e o apply pula o registro. */
|
|
19
|
+
function mcpChange(root, versao) {
|
|
20
|
+
const file = join(root, MCP_PATH);
|
|
21
|
+
if (!existsSync(file))
|
|
22
|
+
return 'ausente';
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return 'invalido';
|
|
29
|
+
}
|
|
30
|
+
if (!isPlainObject(parsed))
|
|
31
|
+
return 'invalido';
|
|
32
|
+
const servers = parsed.mcpServers === undefined ? {} : parsed.mcpServers;
|
|
33
|
+
if (!isPlainObject(servers))
|
|
34
|
+
return 'invalido';
|
|
35
|
+
const atual = servers.banco;
|
|
36
|
+
if (atual === undefined)
|
|
37
|
+
return 'faltando';
|
|
38
|
+
const pin = bancoMcpServer(versao).args.find((arg) => arg.startsWith('@fontdata/'));
|
|
39
|
+
const args = isPlainObject(atual) && Array.isArray(atual.args) ? atual.args : [];
|
|
40
|
+
return ehEntradaGerenciada(atual) && !args.includes(pin) ? 'versao' : null;
|
|
41
|
+
}
|
|
14
42
|
/** `<id>-legado.md`, com sufixo numérico quando o nome já está em uso. */
|
|
15
43
|
function legacyPath(root, path) {
|
|
16
44
|
const base = path.replace(/\.md$/, '');
|
|
@@ -20,7 +48,7 @@ function legacyPath(root, path) {
|
|
|
20
48
|
return candidate;
|
|
21
49
|
}
|
|
22
50
|
}
|
|
23
|
-
export function checkUpdate(root, catalog = loadCatalog(), legacy, home) {
|
|
51
|
+
export function checkUpdate(root, catalog = loadCatalog(), legacy, home, { semBanco = false } = {}) {
|
|
24
52
|
const recorded = readLock(root);
|
|
25
53
|
const lock = recorded ?? adoptLegacyInstall(root, legacy);
|
|
26
54
|
if (!lock) {
|
|
@@ -112,6 +140,8 @@ export function checkUpdate(root, catalog = loadCatalog(), legacy, home) {
|
|
|
112
140
|
...HARNESS_PERMISSIONS.filter((rule) => !hasAllowRule(root, rule)),
|
|
113
141
|
...HARNESS_DENY.filter((rule) => !hasAllowRule(root, rule, 'deny')).map((rule) => `deny ${rule}`),
|
|
114
142
|
];
|
|
143
|
+
const registrarMcpConfig = readConfig(root).config.banco.registrar_mcp;
|
|
144
|
+
const registrarMcp = !semBanco && registrarMcpConfig && !isHarnessPluginEnabled(root, home);
|
|
115
145
|
return {
|
|
116
146
|
changes,
|
|
117
147
|
lock,
|
|
@@ -121,6 +151,9 @@ export function checkUpdate(root, catalog = loadCatalog(), legacy, home) {
|
|
|
121
151
|
registrarComentarios: adotada,
|
|
122
152
|
permissaoAusente: permissoesFaltando.length > 0,
|
|
123
153
|
permissoesFaltando,
|
|
154
|
+
registrarMcp,
|
|
155
|
+
mudancaMcp: registrarMcp ? mcpChange(root, packageInfo().versao) : null,
|
|
156
|
+
recusarMcp: semBanco && registrarMcpConfig,
|
|
124
157
|
};
|
|
125
158
|
}
|
|
126
159
|
/**
|
|
@@ -172,6 +205,20 @@ export function applyUpdate(root, report, options, pkg, now = new Date()) {
|
|
|
172
205
|
writeFileSync(projectRule, projectRuleContent(renamed.map(({ para }) => para)));
|
|
173
206
|
}
|
|
174
207
|
}
|
|
208
|
+
const avisos = [];
|
|
209
|
+
if (report.registrarMcp && report.mudancaMcp === 'invalido')
|
|
210
|
+
avisos.push(MCP_INVALIDO);
|
|
211
|
+
else if (report.registrarMcp) {
|
|
212
|
+
// Uma falha no .mcp.json nunca deixa o update pela metade, sem lock.
|
|
213
|
+
try {
|
|
214
|
+
mergeMcpServers(root, { banco: bancoMcpServer(pkg.versao) }, now, { atualizarGerenciada: true });
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
avisos.push(error instanceof Error ? error.message : String(error));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (report.recusarMcp)
|
|
221
|
+
gravarRegistrarMcp(root, false);
|
|
175
222
|
const itemOf = new Map(report.changes.map((change) => [change.caminho, change.item]));
|
|
176
223
|
const writtenHash = new Map(write.map((file) => [file.path, hashContent(file.content)]));
|
|
177
224
|
const dropped = new Set([...remove, ...report.stale, ...legacy]);
|
|
@@ -205,8 +252,14 @@ export function applyUpdate(root, report, options, pkg, now = new Date()) {
|
|
|
205
252
|
itens.push({ ...added, arquivos: added.arquivos.sort((a, b) => a.caminho.localeCompare(b.caminho)) });
|
|
206
253
|
}
|
|
207
254
|
writeLock(root, { lock_versao: LOCK_VERSION, pacote: pkg, gerado_em: now.toISOString(), itens });
|
|
208
|
-
return { written: write.map((file) => file.path), removed: remove, kept, renamed };
|
|
255
|
+
return { written: write.map((file) => file.path), removed: remove, kept, renamed, avisos };
|
|
209
256
|
}
|
|
257
|
+
const MCP_INVALIDO = `O ${MCP_PATH} está com JSON inválido: o registro do MCP de banco foi pulado e o arquivo não foi tocado. Corrija-o e rode o update de novo.`;
|
|
258
|
+
const MCP_DESCRIPTIONS = {
|
|
259
|
+
ausente: 'não existe',
|
|
260
|
+
faltando: 'não tem a entrada banco',
|
|
261
|
+
versao: 'fixa outra versão do servidor banco',
|
|
262
|
+
};
|
|
210
263
|
const DESCRIPTIONS = {
|
|
211
264
|
novo: 'novo no pacote, será gravado',
|
|
212
265
|
alterado: 'mudou no pacote, será atualizado',
|
|
@@ -230,20 +283,29 @@ export function describeUpdate(report) {
|
|
|
230
283
|
`Faltam permissões do harness em .claude/settings.json (${report.permissoesFaltando.join(', ')}); o --yes as acrescenta.`,
|
|
231
284
|
]
|
|
232
285
|
: [];
|
|
286
|
+
const mcp = [
|
|
287
|
+
...(report.mudancaMcp === 'invalido'
|
|
288
|
+
? [`O ${MCP_PATH} está com JSON inválido; o --yes pula o registro do MCP de banco e não toca no arquivo.`]
|
|
289
|
+
: report.mudancaMcp
|
|
290
|
+
? [`O ${MCP_PATH} ${MCP_DESCRIPTIONS[report.mudancaMcp]}; o --yes grava mcpServers.banco na versão ${packageInfo().versao}.`]
|
|
291
|
+
: []),
|
|
292
|
+
...(report.recusarMcp ? [`--sem-banco: o --yes grava banco.registrar_mcp: false em ${CONFIG_PATH} e não registra o MCP.`] : []),
|
|
293
|
+
];
|
|
233
294
|
if (!report.changes.length) {
|
|
234
|
-
const status = adopted.length || permission.length ? 'Nenhum arquivo a atualizar.' : 'A instalação está em dia: nenhum arquivo a atualizar.';
|
|
235
|
-
return [...adopted, ...permission, status].join('\n');
|
|
295
|
+
const status = adopted.length || permission.length || mcp.length ? 'Nenhum arquivo a atualizar.' : 'A instalação está em dia: nenhum arquivo a atualizar.';
|
|
296
|
+
return [...adopted, ...permission, ...mcp, status].join('\n');
|
|
236
297
|
}
|
|
237
298
|
return [
|
|
238
299
|
...adopted,
|
|
239
300
|
...permission,
|
|
301
|
+
...mcp,
|
|
240
302
|
`Mudanças em relação ao pacote ${packageInfo().versao} (instalado: ${report.lock.pacote.versao}):`,
|
|
241
303
|
...report.changes.map((change) => ` ${change.tipo.padEnd(16)} ${change.caminho}${change.destino ? ` → ${change.destino}` : ''} (${DESCRIPTIONS[change.tipo]})`),
|
|
242
304
|
].join('\n');
|
|
243
305
|
}
|
|
244
|
-
const UPDATE_FLAGS = ['--check', '--yes', '-y', '--sim', '--forcar'];
|
|
306
|
+
const UPDATE_FLAGS = ['--check', '--yes', '-y', '--sim', '--forcar', '--sem-banco'];
|
|
245
307
|
export function parseUpdateArgs(argv) {
|
|
246
|
-
const args = { path: undefined, check: false, yes: false, force: false };
|
|
308
|
+
const args = { path: undefined, check: false, yes: false, force: false, semBanco: false };
|
|
247
309
|
for (const token of argv) {
|
|
248
310
|
if (!token.startsWith('-')) {
|
|
249
311
|
if (args.path !== undefined)
|
|
@@ -254,6 +316,8 @@ export function parseUpdateArgs(argv) {
|
|
|
254
316
|
args.check = true;
|
|
255
317
|
else if (token === '--forcar')
|
|
256
318
|
args.force = true;
|
|
319
|
+
else if (token === '--sem-banco')
|
|
320
|
+
args.semBanco = true;
|
|
257
321
|
else if (token === '--yes' || token === '-y' || token === '--sim')
|
|
258
322
|
args.yes = true;
|
|
259
323
|
else {
|
|
@@ -263,16 +327,17 @@ export function parseUpdateArgs(argv) {
|
|
|
263
327
|
}
|
|
264
328
|
return args;
|
|
265
329
|
}
|
|
266
|
-
/** `ai-harness update [caminho] [--check] [--yes] [--forcar]`; sem `--yes` só simula. */
|
|
330
|
+
/** `ai-harness update [caminho] [--check] [--yes] [--forcar] [--sem-banco]`; sem `--yes` só simula. */
|
|
267
331
|
export function runUpdateCommand(argv) {
|
|
268
332
|
const args = parseUpdateArgs(argv);
|
|
269
333
|
const root = resolve(process.cwd(), args.path ?? '.');
|
|
270
334
|
if (!existsSync(root))
|
|
271
335
|
throw new Error(`Pasta não encontrada: ${root}`);
|
|
272
|
-
const report = checkUpdate(root);
|
|
336
|
+
const report = checkUpdate(root, undefined, undefined, undefined, { semBanco: args.semBanco });
|
|
273
337
|
console.log(describeUpdate(report));
|
|
274
|
-
// Sem arquivo a atualizar, a adoção ainda grava o lock, e a permissão
|
|
275
|
-
|
|
338
|
+
// Sem arquivo a atualizar, a adoção ainda grava o lock, e a permissão e o MCP ainda são acrescentados.
|
|
339
|
+
const pendente = report.adotada || report.permissaoAusente || report.mudancaMcp !== null || report.recusarMcp;
|
|
340
|
+
if (args.check || (!report.changes.length && !pendente))
|
|
276
341
|
return 0;
|
|
277
342
|
if (!args.yes) {
|
|
278
343
|
const conflicts = report.changes.some((change) => change.tipo === 'conflito');
|
|
@@ -282,6 +347,8 @@ export function runUpdateCommand(argv) {
|
|
|
282
347
|
const result = applyUpdate(root, report, { force: args.force }, packageInfo());
|
|
283
348
|
console.log(`\nAtualização aplicada: ${result.written.length} gravado(s), ${result.removed.length} apagado(s), ` +
|
|
284
349
|
`${result.renamed.length} renomeado(s), ${result.kept.length} preservado(s).`);
|
|
350
|
+
for (const aviso of result.avisos)
|
|
351
|
+
console.log(`Aviso: ${aviso}`);
|
|
285
352
|
if (result.renamed.length) {
|
|
286
353
|
console.log('Comandos personalizados renomeados (leve o que for do projeto para uma rule e apague o arquivo):\n' +
|
|
287
354
|
result.renamed.map(({ de, para }) => ` ${de} → ${para}`).join('\n'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fontdata/ai-harness",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "Harness de engenharia com IA da Fontdata para Claude Code: roda os slash commands direto na pasta do projeto via npx, sem instalar nada — ou instala-os em .claude/commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -43,10 +43,13 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@clack/prompts": "^0.7.0",
|
|
46
|
+
"mysql2": "^3.24.5",
|
|
47
|
+
"pg": "^8.23.1",
|
|
46
48
|
"yaml": "^2.9.1"
|
|
47
49
|
},
|
|
48
50
|
"devDependencies": {
|
|
49
51
|
"@types/node": "^22.20.2",
|
|
52
|
+
"@types/pg": "^8.23.1",
|
|
50
53
|
"typescript": "^7.0.2",
|
|
51
54
|
"vitest": "^5.0.0"
|
|
52
55
|
}
|