@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/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...');
@@ -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
+ }
@@ -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
  }
@@ -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 = [EVENT_PERMISSION, CONFLICTS_PERMISSION, ...GIT_PERMISSIONS, AGENT_PERMISSION, ...READ_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())}`);
@@ -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 ausente ainda é acrescentada.
275
- if (args.check || (!report.changes.length && !report.adotada && !report.permissaoAusente))
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.5.1",
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
  }