@fontdata/ai-harness 2.6.0 → 3.0.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 +34 -1
- package/dist/banco-dados/ambiente.js +98 -0
- package/dist/banco-dados/cadastro.js +179 -0
- package/dist/banco-dados/command.js +432 -0
- package/dist/banco-dados/conexao.js +127 -0
- package/dist/banco-dados/drivers/index.js +13 -0
- package/dist/banco-dados/drivers/mariadb.js +121 -0
- package/dist/banco-dados/drivers/postgres.js +157 -0
- package/dist/banco-dados/drivers/sqlite.js +92 -0
- package/dist/banco-dados/drivers/tipos.js +29 -0
- package/dist/banco-dados/ferramentas.js +241 -0
- package/dist/banco-dados/lgpd/aplicar.js +168 -0
- package/dist/banco-dados/lgpd/detectar.js +210 -0
- package/dist/banco-dados/lgpd/mascarar.js +114 -0
- package/dist/banco-dados/mcp.js +101 -0
- package/dist/banco-dados/somente-leitura.js +221 -0
- package/dist/cli.js +22 -0
- package/dist/config/config.js +253 -2
- package/dist/doctor/checks.js +107 -2
- package/dist/init.js +25 -1
- package/dist/install/flags.js +9 -1
- package/dist/install/mcp.js +77 -0
- package/dist/install/plan.js +26 -3
- package/dist/install/settings.js +21 -2
- package/dist/update/update.js +108 -14
- package/package.json +4 -1
package/dist/doctor/checks.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
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-dados/ambiente.js';
|
|
5
|
+
import { caminhoCadastro, lerCadastro } from '../banco-dados/cadastro.js';
|
|
4
6
|
import { allCommands, loadCatalog } from '../catalog.js';
|
|
5
7
|
import { CONFIG_PATH, readConfig } from '../config/config.js';
|
|
6
8
|
import { readArtifact, validateAllWorks, WORKS_DIR } from '../contratos/artefatos.js';
|
|
@@ -396,6 +398,108 @@ function checkConfig(problems) {
|
|
|
396
398
|
correcao: `Corrija o ${CONFIG_PATH}; enquanto isso, valem os padrões (segurança em bloqueio, demais hooks em aviso).`,
|
|
397
399
|
}));
|
|
398
400
|
}
|
|
401
|
+
const LINHAS_IGNORE_SETTINGS_LOCAL = new Set(['.claude/settings.local.json', '.claude/', '.claude', 'settings.local.json']);
|
|
402
|
+
function settingsLocalIgnorado(root, env) {
|
|
403
|
+
const opcoes = { cwd: root, env: { ...process.env, ...env }, stdio: 'ignore', timeout: 5000 };
|
|
404
|
+
const repositorio = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], opcoes);
|
|
405
|
+
if (!repositorio.error && repositorio.status === 0) {
|
|
406
|
+
return spawnSync('git', ['check-ignore', '-q', SETTINGS_LOCAL_PATH], opcoes).status === 0;
|
|
407
|
+
}
|
|
408
|
+
const gitignore = join(root, '.gitignore');
|
|
409
|
+
if (!existsSync(gitignore))
|
|
410
|
+
return false;
|
|
411
|
+
return readFileSync(gitignore, 'utf8')
|
|
412
|
+
.split(/\r?\n/)
|
|
413
|
+
.map((linha) => linha.trim().replace(/^\//, '').replace(/^\*\*\//, ''))
|
|
414
|
+
.some((linha) => LINHAS_IGNORE_SETTINGS_LOCAL.has(linha));
|
|
415
|
+
}
|
|
416
|
+
function checkBancoProjeto(root, config, env) {
|
|
417
|
+
const conexoes = config.banco_dados.conexoes.flatMap((nome) => config.banco_dados.projeto[nome] ?? []);
|
|
418
|
+
if (!conexoes.length)
|
|
419
|
+
return [];
|
|
420
|
+
const findings = [];
|
|
421
|
+
for (const { nome, dsn_env } of conexoes) {
|
|
422
|
+
if (lerVariavel(dsn_env, root, env))
|
|
423
|
+
continue;
|
|
424
|
+
findings.push({
|
|
425
|
+
id: 'banco-variavel-ausente',
|
|
426
|
+
severidade: 'aviso',
|
|
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} --projeto\` ou no ambiente do terminal.`,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
const envLocal = lerEnvSettingsLocal(root);
|
|
432
|
+
const variaveis = [...new Set(conexoes.map(({ dsn_env }) => dsn_env).filter((variavel) => envLocal[variavel]))];
|
|
433
|
+
if (!variaveis.length)
|
|
434
|
+
return findings;
|
|
435
|
+
const caminho = join(root, SETTINGS_LOCAL_PATH);
|
|
436
|
+
const lista = variaveis.join(', ');
|
|
437
|
+
if (!settingsLocalIgnorado(root, env)) {
|
|
438
|
+
findings.push({
|
|
439
|
+
id: 'banco-settings-local-versionado',
|
|
440
|
+
severidade: 'aviso',
|
|
441
|
+
mensagem: `O ${caminho} guarda o DSN de ${lista} e não está ignorado pelo git; a senha pode ir para o repositório.`,
|
|
442
|
+
correcao: `Acrescente ao .gitignore da raiz a linha \`.claude/settings.local.json\`. Se o arquivo já foi commitado, rode também \`git rm --cached .claude/settings.local.json\`.`,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
const modo = process.platform !== 'win32' ? statSync(caminho).mode & 0o777 : 0;
|
|
446
|
+
if (modo & 0o077) {
|
|
447
|
+
findings.push({
|
|
448
|
+
id: 'banco-settings-local-permissao',
|
|
449
|
+
severidade: 'aviso',
|
|
450
|
+
mensagem: `O ${caminho} guarda o DSN de ${lista} e pode ser lido por outros usuários desta máquina (modo ${modo.toString(8)}).`,
|
|
451
|
+
correcao: `Restrinja ao seu usuário: \`chmod 600 ${caminho}\`.`,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
return findings;
|
|
455
|
+
}
|
|
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
|
+
export function checkBanco(root, config, env = process.env) {
|
|
458
|
+
if (!config.banco_dados.conexoes.length)
|
|
459
|
+
return [];
|
|
460
|
+
const findings = checkBancoProjeto(root, config, env);
|
|
461
|
+
const doCadastro = config.banco_dados.conexoes.filter((nome) => !(nome in config.banco_dados.projeto));
|
|
462
|
+
if (!doCadastro.length)
|
|
463
|
+
return findings;
|
|
464
|
+
const caminho = caminhoCadastro(env);
|
|
465
|
+
const modo = process.platform !== 'win32' && existsSync(caminho) ? statSync(caminho).mode & 0o777 : 0;
|
|
466
|
+
if (modo & 0o077) {
|
|
467
|
+
findings.push({
|
|
468
|
+
id: 'banco-cadastro-permissao',
|
|
469
|
+
severidade: 'aviso',
|
|
470
|
+
mensagem: `O cadastro de conexões ${caminho} pode ser lido por outros usuários desta máquina (modo ${modo.toString(8)}).`,
|
|
471
|
+
correcao: `Restrinja ao seu usuário: \`chmod 600 ${caminho}\`.`,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
const { conexoes, problemas } = lerCadastro(caminho);
|
|
475
|
+
for (const mensagem of problemas) {
|
|
476
|
+
findings.push({
|
|
477
|
+
id: 'banco-cadastro-invalido',
|
|
478
|
+
severidade: 'aviso',
|
|
479
|
+
mensagem,
|
|
480
|
+
correcao: `Corrija o JSON de ${caminho} (formato {"versao": 1, "conexoes": {...}}) ou recadastre as conexões.`,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
if (problemas.length)
|
|
484
|
+
return findings;
|
|
485
|
+
for (const nome of doCadastro.filter((nome) => !(nome in conexoes))) {
|
|
486
|
+
findings.push({
|
|
487
|
+
id: 'banco-conexao-ausente',
|
|
488
|
+
severidade: 'aviso',
|
|
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}\`.`,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
for (const nome of doCadastro.filter((nome) => conexoes[nome]?.mascaramento === false)) {
|
|
494
|
+
findings.push({
|
|
495
|
+
id: 'banco-mascaramento-desligado',
|
|
496
|
+
severidade: 'aviso',
|
|
497
|
+
mensagem: `A conexão ${nome}, usada por este projeto, está com o mascaramento LGPD desligado no cadastro desta máquina (${caminho}).`,
|
|
498
|
+
correcao: `Os dados pessoais da conexão ${nome} chegam sem máscara ao Claude. Se não for de propósito, remova "mascaramento": false dela em ${caminho}.`,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
return findings;
|
|
502
|
+
}
|
|
399
503
|
/** Diagnostica a instalação do harness no projeto; nunca altera arquivos. */
|
|
400
504
|
export function runDoctor(root, options = {}) {
|
|
401
505
|
const { findings: lockFindings, lock } = checkLock(root);
|
|
@@ -410,6 +514,7 @@ export function runDoctor(root, options = {}) {
|
|
|
410
514
|
...checkSettings(root),
|
|
411
515
|
...checkGitIgnore(root),
|
|
412
516
|
...checkConfig(configProblems),
|
|
517
|
+
...checkBanco(root, config),
|
|
413
518
|
...checkWorks(root),
|
|
414
519
|
...checkStaleBlocks(root, options.now ?? new Date(), options.staleBlockDays ?? config.bloqueios.dias_para_aviso),
|
|
415
520
|
...checkEventLogs(root),
|
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_dados.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-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)
|
|
@@ -48,6 +53,7 @@ export function parseInitArgs(argv) {
|
|
|
48
53
|
hooks: undefined,
|
|
49
54
|
agents: undefined,
|
|
50
55
|
comments: undefined,
|
|
56
|
+
mcpBanco: undefined,
|
|
51
57
|
yes: false,
|
|
52
58
|
hasSelectionFlags: false,
|
|
53
59
|
};
|
|
@@ -68,6 +74,8 @@ export function parseInitArgs(argv) {
|
|
|
68
74
|
args.agents = false;
|
|
69
75
|
else if (token === '--sem-comentarios')
|
|
70
76
|
args.comments = false;
|
|
77
|
+
else if (token === '--sem-banco-dados')
|
|
78
|
+
args.mcpBanco = false;
|
|
71
79
|
else
|
|
72
80
|
args.yes = true;
|
|
73
81
|
continue;
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
/** 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';
|
|
9
|
+
const PIN_PREFIX = '@fontdata/ai-harness@';
|
|
10
|
+
function isPlainObject(value) {
|
|
11
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
/** Servidor MCP de banco com a versão do pacote fixada no npx, no mesmo formato do plugin. */
|
|
14
|
+
export function bancoMcpServer(versao) {
|
|
15
|
+
return { command: 'npx', args: ['-y', `${PIN_PREFIX}${versao}`, 'banco-dados-mcp'] };
|
|
16
|
+
}
|
|
17
|
+
function pinOf(entrada) {
|
|
18
|
+
if (!isPlainObject(entrada) || !Array.isArray(entrada.args))
|
|
19
|
+
return undefined;
|
|
20
|
+
return entrada.args.find((arg) => typeof arg === 'string' && arg.includes(PIN_PREFIX));
|
|
21
|
+
}
|
|
22
|
+
/** Entrada gravada pelo harness: os args fixam `@fontdata/ai-harness@<versão>`. */
|
|
23
|
+
export function ehEntradaGerenciada(entrada) {
|
|
24
|
+
return pinOf(entrada) !== undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Acrescenta os servidores ausentes em `mcpServers` do `.mcp.json`, preservando o resto. Com
|
|
28
|
+
* `atualizarGerenciada`, regrava também a entrada gerenciada que fixa outra versão; entrada
|
|
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.
|
|
31
|
+
*/
|
|
32
|
+
export function mergeMcpServers(root, servidores, now = new Date(), { atualizarGerenciada = false, removerGerenciadas = [] } = {}) {
|
|
33
|
+
const file = join(root, MCP_PATH);
|
|
34
|
+
const original = existsSync(file) ? readFileSync(file, 'utf8') : null;
|
|
35
|
+
let config = {};
|
|
36
|
+
if (original !== null) {
|
|
37
|
+
let parsed;
|
|
38
|
+
try {
|
|
39
|
+
parsed = JSON.parse(original);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
parsed = undefined;
|
|
43
|
+
}
|
|
44
|
+
if (!isPlainObject(parsed) || (parsed.mcpServers !== undefined && !isPlainObject(parsed.mcpServers))) {
|
|
45
|
+
throw new Error(`O ${MCP_PATH} está com JSON inválido; corrija-o antes de registrar o servidor MCP de banco.`);
|
|
46
|
+
}
|
|
47
|
+
config = parsed;
|
|
48
|
+
}
|
|
49
|
+
const mcpServers = { ...(config.mcpServers ?? {}) };
|
|
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
|
+
}
|
|
57
|
+
for (const [nome, servidor] of Object.entries(servidores)) {
|
|
58
|
+
const atual = mcpServers[nome];
|
|
59
|
+
if (atual === undefined) {
|
|
60
|
+
mcpServers[nome] = servidor;
|
|
61
|
+
changed = true;
|
|
62
|
+
}
|
|
63
|
+
else if (atualizarGerenciada && ehEntradaGerenciada(atual) && pinOf(atual) !== pinOf(servidor)) {
|
|
64
|
+
mcpServers[nome] = { ...atual, command: servidor.command, args: servidor.args };
|
|
65
|
+
changed = true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!changed)
|
|
69
|
+
return { changed: false, backup: null };
|
|
70
|
+
let backup = null;
|
|
71
|
+
if (original !== null) {
|
|
72
|
+
backup = `${MCP_PATH}.bak-${timestamp(now)}`;
|
|
73
|
+
copyFileSync(file, join(root, backup));
|
|
74
|
+
}
|
|
75
|
+
writeFileSync(file, `${JSON.stringify({ ...config, mcpServers }, null, 2)}\n`);
|
|
76
|
+
return { changed: true, backup };
|
|
77
|
+
}
|
package/dist/install/plan.js
CHANGED
|
@@ -6,8 +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 { BANCO_MCP_NOME, 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
|
-
import { combineHooks, HARNESS_DENY,
|
|
12
|
+
import { combineHooks, HARNESS_DENY, harnessPermissions, 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. */
|
|
12
14
|
export const PROJECT_RULE_PATH = '.claude/rules/harness-projeto.md';
|
|
13
15
|
/** Esqueleto da rule de projeto; os comandos `-legado` renomeados pelo update entram na última seção. */
|
|
@@ -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() {
|
|
@@ -162,10 +165,14 @@ export function executePlan(root, plan, pkg, now = new Date()) {
|
|
|
162
165
|
const permission = needsEventPermission(plan);
|
|
163
166
|
const hooks = combineHooks(plan.registerHooks ? projectHooksConfig() : {}, plan.registerComments ? commentsHooksConfig() : {});
|
|
164
167
|
if (Object.keys(hooks).length || permission) {
|
|
165
|
-
mergeSettings(root, { hooks, allow: permission ?
|
|
168
|
+
mergeSettings(root, { hooks, allow: permission ? harnessPermissions({ banco: bancoAtivo(plan) }) : [], deny: permission ? HARNESS_DENY : [] }, now);
|
|
166
169
|
}
|
|
167
170
|
if (plan.registerComments)
|
|
168
171
|
ensureCommentsBaseline(root);
|
|
172
|
+
if (plan.registerMcp)
|
|
173
|
+
mergeMcpServers(root, { [BANCO_MCP_NOME]: 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);
|
|
@@ -186,6 +193,10 @@ export function executePlan(root, plan, pkg, now = new Date()) {
|
|
|
186
193
|
writeLock(root, lock);
|
|
187
194
|
return lock;
|
|
188
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
|
+
}
|
|
189
200
|
/** Skills e agents do harness registram o rastro com `npx @fontdata/ai-harness evento`. */
|
|
190
201
|
function needsEventPermission(plan) {
|
|
191
202
|
return plan.items.some((item) => item.tipo === 'skill' || item.tipo === 'agent');
|
|
@@ -202,10 +213,22 @@ export function describePlan(plan) {
|
|
|
202
213
|
if (plan.registerHooks)
|
|
203
214
|
lines.push('Hooks do harness registrados em .claude/settings.json (sem duplicar os existentes).');
|
|
204
215
|
if (needsEventPermission(plan)) {
|
|
205
|
-
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, ` +
|
|
206
217
|
'o /git-commit-push, os sub-agents e as consultas de leitura não pedirem aprovação (inclusive no modo auto); ' +
|
|
207
218
|
'o push forçado fica bloqueado em permissions.deny.');
|
|
208
219
|
}
|
|
220
|
+
if (plan.registerMcp) {
|
|
221
|
+
lines.push(`Servidor MCP de banco (somente leitura) registrado em ${MCP_PATH}, sem duplicar os servidores existentes.`);
|
|
222
|
+
}
|
|
223
|
+
else if (plan.mcpRecusado) {
|
|
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.`);
|
|
225
|
+
}
|
|
226
|
+
else if (plan.mcpSkipReason === 'plugin') {
|
|
227
|
+
lines.push(`Servidor MCP de banco não registrado em ${MCP_PATH}: o plugin fontdata@ai-harness já o fornece.`);
|
|
228
|
+
}
|
|
229
|
+
else if (plan.mcpSkipReason === 'config') {
|
|
230
|
+
lines.push(`Servidor MCP de banco não registrado em ${MCP_PATH}: banco_dados.registrar_mcp está false no .ai-harness/config.json.`);
|
|
231
|
+
}
|
|
209
232
|
if (plan.registerComments) {
|
|
210
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).');
|
|
211
234
|
}
|
package/dist/install/settings.js
CHANGED
|
@@ -33,6 +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'];
|
|
37
|
+
/**
|
|
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.
|
|
40
|
+
*/
|
|
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}`));
|
|
36
44
|
/** Regras de `permissions.allow` que o init e o update gravam para as skills e os agents do harness. */
|
|
37
45
|
export const HARNESS_PERMISSIONS = [
|
|
38
46
|
EVENT_PERMISSION,
|
|
@@ -41,7 +49,12 @@ export const HARNESS_PERMISSIONS = [
|
|
|
41
49
|
AGENT_PERMISSION,
|
|
42
50
|
...TOOL_PERMISSIONS,
|
|
43
51
|
...READ_PERMISSIONS,
|
|
52
|
+
...BANCO_PERMISSIONS,
|
|
44
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
|
+
}
|
|
45
58
|
/** O push liberado não vale para reescrever o histórico remoto: `deny` vence o `allow`. */
|
|
46
59
|
export const HARNESS_DENY = [
|
|
47
60
|
'Bash(git push --force:*)',
|
|
@@ -54,7 +67,7 @@ export const HARNESS_DENY = [
|
|
|
54
67
|
function isPlainObject(value) {
|
|
55
68
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
56
69
|
}
|
|
57
|
-
function timestamp(date) {
|
|
70
|
+
export function timestamp(date) {
|
|
58
71
|
const pad = (n) => String(n).padStart(2, '0');
|
|
59
72
|
return (`${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-` +
|
|
60
73
|
`${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`);
|
|
@@ -86,7 +99,7 @@ export function mergeHooksIntoSettings(root, hooks, now = new Date()) {
|
|
|
86
99
|
* Acrescenta hooks e regras de `permissions.allow` ao `.claude/settings.json` do projeto
|
|
87
100
|
* sem perder o que já existe. Um hook é considerado presente quando algum grupo do mesmo
|
|
88
101
|
* evento já tem o mesmo `command`, e uma regra quando já está na lista, o que torna a
|
|
89
|
-
* 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
|
|
90
103
|
* original; JSON inválido é recusado sem tocar em nada.
|
|
91
104
|
*/
|
|
92
105
|
export function mergeSettings(root, additions, now = new Date()) {
|
|
@@ -149,6 +162,12 @@ export function mergeSettings(root, additions, now = new Date()) {
|
|
|
149
162
|
}
|
|
150
163
|
next.permissions = { ...permissions, [key]: list };
|
|
151
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
|
+
}
|
|
152
171
|
if (!changed)
|
|
153
172
|
return { changed: false, backup: null };
|
|
154
173
|
let backup = null;
|
package/dist/update/update.js
CHANGED
|
@@ -2,15 +2,45 @@ 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, migrarSecaoBanco, readConfig, syncGitignore, temSecaoBancoLegada, 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 { BANCO_MCP_NOME, BANCO_MCP_NOME_LEGADO, 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
|
-
import { combineHooks, HARNESS_DENY,
|
|
13
|
+
import { BANCO_PERMISSIONS_LEGADAS, combineHooks, HARNESS_DENY, harnessPermissions, hasAllowRule, mergeSettings, SETTINGS_PATH, } 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
|
+
if (ehEntradaGerenciada(servers[BANCO_MCP_NOME_LEGADO]))
|
|
36
|
+
return 'renomear';
|
|
37
|
+
const atual = servers[BANCO_MCP_NOME];
|
|
38
|
+
if (atual === undefined)
|
|
39
|
+
return 'faltando';
|
|
40
|
+
const pin = bancoMcpServer(versao).args.find((arg) => arg.startsWith('@fontdata/'));
|
|
41
|
+
const args = isPlainObject(atual) && Array.isArray(atual.args) ? atual.args : [];
|
|
42
|
+
return ehEntradaGerenciada(atual) && !args.includes(pin) ? 'versao' : null;
|
|
43
|
+
}
|
|
14
44
|
/** `<id>-legado.md`, com sufixo numérico quando o nome já está em uso. */
|
|
15
45
|
function legacyPath(root, path) {
|
|
16
46
|
const base = path.replace(/\.md$/, '');
|
|
@@ -20,7 +50,7 @@ function legacyPath(root, path) {
|
|
|
20
50
|
return candidate;
|
|
21
51
|
}
|
|
22
52
|
}
|
|
23
|
-
export function checkUpdate(root, catalog = loadCatalog(), legacy, home) {
|
|
53
|
+
export function checkUpdate(root, catalog = loadCatalog(), legacy, home, { semBanco = false } = {}) {
|
|
24
54
|
const recorded = readLock(root);
|
|
25
55
|
const lock = recorded ?? adoptLegacyInstall(root, legacy);
|
|
26
56
|
if (!lock) {
|
|
@@ -108,10 +138,14 @@ export function checkUpdate(root, catalog = loadCatalog(), legacy, home) {
|
|
|
108
138
|
addItem('rule', 'comentarios', true);
|
|
109
139
|
addItem('hook', 'comentarios', true);
|
|
110
140
|
}
|
|
141
|
+
const registrarMcpConfig = readConfig(root).config.banco_dados.registrar_mcp;
|
|
142
|
+
const permissoesBanco = !semBanco && registrarMcpConfig;
|
|
111
143
|
const permissoesFaltando = [
|
|
112
|
-
...
|
|
144
|
+
...harnessPermissions({ banco: permissoesBanco }).filter((rule) => !hasAllowRule(root, rule)),
|
|
113
145
|
...HARNESS_DENY.filter((rule) => !hasAllowRule(root, rule, 'deny')).map((rule) => `deny ${rule}`),
|
|
114
146
|
];
|
|
147
|
+
const permissoesLegadas = BANCO_PERMISSIONS_LEGADAS.filter((rule) => hasAllowRule(root, rule));
|
|
148
|
+
const registrarMcp = !semBanco && registrarMcpConfig && !isHarnessPluginEnabled(root, home);
|
|
115
149
|
return {
|
|
116
150
|
changes,
|
|
117
151
|
lock,
|
|
@@ -121,6 +155,12 @@ export function checkUpdate(root, catalog = loadCatalog(), legacy, home) {
|
|
|
121
155
|
registrarComentarios: adotada,
|
|
122
156
|
permissaoAusente: permissoesFaltando.length > 0,
|
|
123
157
|
permissoesFaltando,
|
|
158
|
+
permissoesLegadas,
|
|
159
|
+
permissoesBanco,
|
|
160
|
+
registrarMcp,
|
|
161
|
+
mudancaMcp: registrarMcp ? mcpChange(root, packageInfo().versao) : null,
|
|
162
|
+
recusarMcp: semBanco && registrarMcpConfig,
|
|
163
|
+
renomearSecaoBanco: temSecaoBancoLegada(root),
|
|
124
164
|
};
|
|
125
165
|
}
|
|
126
166
|
/**
|
|
@@ -159,7 +199,10 @@ export function applyUpdate(root, report, options, pkg, now = new Date()) {
|
|
|
159
199
|
return { de: path, para };
|
|
160
200
|
});
|
|
161
201
|
const hooks = combineHooks(report.registrarHooks ? projectHooksConfig() : {}, report.registrarComentarios ? commentsHooksConfig() : {});
|
|
162
|
-
|
|
202
|
+
const ajustes = [];
|
|
203
|
+
const settings = mergeSettings(root, { hooks, allow: harnessPermissions({ banco: report.permissoesBanco }), deny: HARNESS_DENY, removeAllow: BANCO_PERMISSIONS_LEGADAS }, now);
|
|
204
|
+
if (settings.changed)
|
|
205
|
+
ajustes.push(`${SETTINGS_PATH} atualizado${settings.backup ? ` (backup em ${settings.backup})` : ''}.`);
|
|
163
206
|
if (report.registrarComentarios)
|
|
164
207
|
ensureCommentsBaseline(root);
|
|
165
208
|
if (report.adotada) {
|
|
@@ -172,6 +215,29 @@ export function applyUpdate(root, report, options, pkg, now = new Date()) {
|
|
|
172
215
|
writeFileSync(projectRule, projectRuleContent(renamed.map(({ para }) => para)));
|
|
173
216
|
}
|
|
174
217
|
}
|
|
218
|
+
const avisos = [];
|
|
219
|
+
if (report.registrarMcp && report.mudancaMcp === 'invalido')
|
|
220
|
+
avisos.push(MCP_INVALIDO);
|
|
221
|
+
else if (report.registrarMcp) {
|
|
222
|
+
// Uma falha no .mcp.json nunca deixa o update pela metade, sem lock.
|
|
223
|
+
try {
|
|
224
|
+
const mcp = mergeMcpServers(root, { [BANCO_MCP_NOME]: bancoMcpServer(pkg.versao) }, now, {
|
|
225
|
+
atualizarGerenciada: true,
|
|
226
|
+
removerGerenciadas: [BANCO_MCP_NOME_LEGADO],
|
|
227
|
+
});
|
|
228
|
+
if (mcp.changed)
|
|
229
|
+
ajustes.push(`${MCP_PATH} com mcpServers.${BANCO_MCP_NOME} na versão ${pkg.versao}${mcp.backup ? ` (backup em ${mcp.backup})` : ''}.`);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
avisos.push(error instanceof Error ? error.message : String(error));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (report.renomearSecaoBanco && migrarSecaoBanco(root))
|
|
236
|
+
ajustes.push(`seção banco renomeada para banco_dados em ${CONFIG_PATH}.`);
|
|
237
|
+
if (report.recusarMcp) {
|
|
238
|
+
gravarRegistrarMcp(root, false);
|
|
239
|
+
ajustes.push(`banco_dados.registrar_mcp: false gravado em ${CONFIG_PATH}.`);
|
|
240
|
+
}
|
|
175
241
|
const itemOf = new Map(report.changes.map((change) => [change.caminho, change.item]));
|
|
176
242
|
const writtenHash = new Map(write.map((file) => [file.path, hashContent(file.content)]));
|
|
177
243
|
const dropped = new Set([...remove, ...report.stale, ...legacy]);
|
|
@@ -205,8 +271,15 @@ export function applyUpdate(root, report, options, pkg, now = new Date()) {
|
|
|
205
271
|
itens.push({ ...added, arquivos: added.arquivos.sort((a, b) => a.caminho.localeCompare(b.caminho)) });
|
|
206
272
|
}
|
|
207
273
|
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 };
|
|
274
|
+
return { written: write.map((file) => file.path), removed: remove, kept, renamed, avisos, ajustes };
|
|
209
275
|
}
|
|
276
|
+
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.`;
|
|
277
|
+
const MCP_DESCRIPTIONS = {
|
|
278
|
+
ausente: 'não existe',
|
|
279
|
+
faltando: `não tem a entrada ${BANCO_MCP_NOME}`,
|
|
280
|
+
versao: `fixa outra versão do servidor ${BANCO_MCP_NOME}`,
|
|
281
|
+
renomear: `tem a entrada ${BANCO_MCP_NOME_LEGADO} do harness, com o nome antigo; ela é trocada pela ${BANCO_MCP_NOME}`,
|
|
282
|
+
};
|
|
210
283
|
const DESCRIPTIONS = {
|
|
211
284
|
novo: 'novo no pacote, será gravado',
|
|
212
285
|
alterado: 'mudou no pacote, será atualizado',
|
|
@@ -230,20 +303,34 @@ export function describeUpdate(report) {
|
|
|
230
303
|
`Faltam permissões do harness em .claude/settings.json (${report.permissoesFaltando.join(', ')}); o --yes as acrescenta.`,
|
|
231
304
|
]
|
|
232
305
|
: [];
|
|
306
|
+
const legadas = report.permissoesLegadas.length
|
|
307
|
+
? [`Permissões do MCP de banco com o nome antigo em .claude/settings.json (${report.permissoesLegadas.join(', ')}); o --yes as remove.`]
|
|
308
|
+
: [];
|
|
309
|
+
const mcp = [
|
|
310
|
+
...(report.mudancaMcp === 'invalido'
|
|
311
|
+
? [`O ${MCP_PATH} está com JSON inválido; o --yes pula o registro do MCP de banco e não toca no arquivo.`]
|
|
312
|
+
: report.mudancaMcp
|
|
313
|
+
? [`O ${MCP_PATH} ${MCP_DESCRIPTIONS[report.mudancaMcp]}; o --yes grava mcpServers.${BANCO_MCP_NOME} na versão ${packageInfo().versao}.`]
|
|
314
|
+
: []),
|
|
315
|
+
...(report.renomearSecaoBanco ? [`A seção banco de ${CONFIG_PATH} agora se chama banco_dados; o --yes a renomeia.`] : []),
|
|
316
|
+
...(report.recusarMcp ? [`--sem-banco-dados: o --yes grava banco_dados.registrar_mcp: false em ${CONFIG_PATH} e não registra o MCP.`] : []),
|
|
317
|
+
];
|
|
233
318
|
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');
|
|
319
|
+
const status = adopted.length || permission.length || legadas.length || mcp.length ? 'Nenhum arquivo a atualizar.' : 'A instalação está em dia: nenhum arquivo a atualizar.';
|
|
320
|
+
return [...adopted, ...permission, ...legadas, ...mcp, status].join('\n');
|
|
236
321
|
}
|
|
237
322
|
return [
|
|
238
323
|
...adopted,
|
|
239
324
|
...permission,
|
|
325
|
+
...legadas,
|
|
326
|
+
...mcp,
|
|
240
327
|
`Mudanças em relação ao pacote ${packageInfo().versao} (instalado: ${report.lock.pacote.versao}):`,
|
|
241
328
|
...report.changes.map((change) => ` ${change.tipo.padEnd(16)} ${change.caminho}${change.destino ? ` → ${change.destino}` : ''} (${DESCRIPTIONS[change.tipo]})`),
|
|
242
329
|
].join('\n');
|
|
243
330
|
}
|
|
244
|
-
const UPDATE_FLAGS = ['--check', '--yes', '-y', '--sim', '--forcar'];
|
|
331
|
+
const UPDATE_FLAGS = ['--check', '--yes', '-y', '--sim', '--forcar', '--sem-banco-dados'];
|
|
245
332
|
export function parseUpdateArgs(argv) {
|
|
246
|
-
const args = { path: undefined, check: false, yes: false, force: false };
|
|
333
|
+
const args = { path: undefined, check: false, yes: false, force: false, semBanco: false };
|
|
247
334
|
for (const token of argv) {
|
|
248
335
|
if (!token.startsWith('-')) {
|
|
249
336
|
if (args.path !== undefined)
|
|
@@ -254,6 +341,8 @@ export function parseUpdateArgs(argv) {
|
|
|
254
341
|
args.check = true;
|
|
255
342
|
else if (token === '--forcar')
|
|
256
343
|
args.force = true;
|
|
344
|
+
else if (token === '--sem-banco-dados')
|
|
345
|
+
args.semBanco = true;
|
|
257
346
|
else if (token === '--yes' || token === '-y' || token === '--sim')
|
|
258
347
|
args.yes = true;
|
|
259
348
|
else {
|
|
@@ -263,16 +352,17 @@ export function parseUpdateArgs(argv) {
|
|
|
263
352
|
}
|
|
264
353
|
return args;
|
|
265
354
|
}
|
|
266
|
-
/** `ai-harness update [caminho] [--check] [--yes] [--forcar]`; sem `--yes` só simula. */
|
|
355
|
+
/** `ai-harness update [caminho] [--check] [--yes] [--forcar] [--sem-banco-dados]`; sem `--yes` só simula. */
|
|
267
356
|
export function runUpdateCommand(argv) {
|
|
268
357
|
const args = parseUpdateArgs(argv);
|
|
269
358
|
const root = resolve(process.cwd(), args.path ?? '.');
|
|
270
359
|
if (!existsSync(root))
|
|
271
360
|
throw new Error(`Pasta não encontrada: ${root}`);
|
|
272
|
-
const report = checkUpdate(root);
|
|
361
|
+
const report = checkUpdate(root, undefined, undefined, undefined, { semBanco: args.semBanco });
|
|
273
362
|
console.log(describeUpdate(report));
|
|
274
|
-
// Sem arquivo a atualizar, a adoção ainda grava o lock, e a permissão
|
|
275
|
-
|
|
363
|
+
// Sem arquivo a atualizar, a adoção ainda grava o lock, e a permissão e o MCP ainda são acrescentados.
|
|
364
|
+
const pendente = report.adotada || report.permissaoAusente || report.permissoesLegadas.length > 0 || report.mudancaMcp !== null || report.recusarMcp || report.renomearSecaoBanco;
|
|
365
|
+
if (args.check || (!report.changes.length && !pendente))
|
|
276
366
|
return 0;
|
|
277
367
|
if (!args.yes) {
|
|
278
368
|
const conflicts = report.changes.some((change) => change.tipo === 'conflito');
|
|
@@ -282,6 +372,10 @@ export function runUpdateCommand(argv) {
|
|
|
282
372
|
const result = applyUpdate(root, report, { force: args.force }, packageInfo());
|
|
283
373
|
console.log(`\nAtualização aplicada: ${result.written.length} gravado(s), ${result.removed.length} apagado(s), ` +
|
|
284
374
|
`${result.renamed.length} renomeado(s), ${result.kept.length} preservado(s).`);
|
|
375
|
+
for (const ajuste of result.ajustes)
|
|
376
|
+
console.log(`Gravado: ${ajuste}`);
|
|
377
|
+
for (const aviso of result.avisos)
|
|
378
|
+
console.log(`Aviso: ${aviso}`);
|
|
285
379
|
if (result.renamed.length) {
|
|
286
380
|
console.log('Comandos personalizados renomeados (leve o que for do projeto para uma rule e apague o arquivo):\n' +
|
|
287
381
|
result.renamed.map(({ de, para }) => ` ${de} → ${para}`).join('\n'));
|