@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
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { EOL } from 'node:os';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import * as p from '@clack/prompts';
|
|
5
|
+
import { comSecaoBanco, CONFIG_PATH, gravarConexaoProjeto, readConfig, REGRA_DSN_ENV, REGRA_NOME_CONEXAO, secaoBanco, validarConfigParaEscrita, writeDefaultConfig, } from '../config/config.js';
|
|
6
|
+
import { suggestFlag } from '../install/flags.js';
|
|
7
|
+
import { caminhoCadastro, lerCadastro, ocultarSenha, removerConexao, resumirConexao, salvarConexao, TIPOS_BANCO, } from './cadastro.js';
|
|
8
|
+
import { gravarVariavelSettingsLocal, lerEnvSettingsLocal, lerVariavel, SETTINGS_LOCAL_PATH, validarSettingsLocal } from './ambiente.js';
|
|
9
|
+
import { interpretarDsn, resolverConexoes } from './conexao.js';
|
|
10
|
+
import { criarDriver as criarDriverPadrao } from './drivers/index.js';
|
|
11
|
+
const AJUDA = [
|
|
12
|
+
'Uso: ai-harness banco-dados <subcomando>',
|
|
13
|
+
'',
|
|
14
|
+
' adicionar <nome> [--dsn <dsn>] [--producao] [--sem-testar] [--substituir]',
|
|
15
|
+
' cadastra a conexão nesta máquina (sem --dsn, pergunta os dados)',
|
|
16
|
+
' adicionar <nome> --projeto [--env <VAR>] [--dsn <dsn>] [--producao] [--sem-testar] [--substituir]',
|
|
17
|
+
' grava o DSN na variável <NOME>_DATABASE_URL (ou --env) do .claude/settings.local.json',
|
|
18
|
+
' e a conexão { nome, dsn_env } em banco_dados.conexoes do .ai-harness/config.json',
|
|
19
|
+
' listar mostra as conexões cadastradas e as do projeto, sem a senha',
|
|
20
|
+
' testar [nome] testa a conexão pedida ou todas as do projeto',
|
|
21
|
+
' usar <nome...> grava banco_dados.conexoes no .ai-harness/config.json (mantém as entradas { nome, dsn_env })',
|
|
22
|
+
' remover <nome> apaga a conexão do cadastro',
|
|
23
|
+
].join(EOL);
|
|
24
|
+
const PORTA_PADRAO = { postgres: '5432', mariadb: '3306' };
|
|
25
|
+
const ESQUEMA = { postgres: 'postgres', mariadb: 'mysql' };
|
|
26
|
+
function mascarar(ctx, texto) {
|
|
27
|
+
return ctx.sensiveis.reduce((resultado, conexao) => ocultarSenha(resultado, conexao), texto);
|
|
28
|
+
}
|
|
29
|
+
function sensivel(dsn) {
|
|
30
|
+
return { tipo: 'postgres', dsn };
|
|
31
|
+
}
|
|
32
|
+
function isObject(valor) {
|
|
33
|
+
return typeof valor === 'object' && valor !== null && !Array.isArray(valor);
|
|
34
|
+
}
|
|
35
|
+
function alvoDe(conexao, cwd) {
|
|
36
|
+
return conexao.tipo === 'sqlite' ? resolve(cwd, conexao.caminho ?? '') : (conexao.dsn ?? '');
|
|
37
|
+
}
|
|
38
|
+
async function testarConexao(ctx, conexao) {
|
|
39
|
+
let driver;
|
|
40
|
+
try {
|
|
41
|
+
driver = await ctx.criarDriver(conexao.tipo, alvoDe(conexao, ctx.cwd));
|
|
42
|
+
await driver.testar();
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
catch (erro) {
|
|
46
|
+
return erro instanceof Error ? erro.message : String(erro);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
await driver?.fechar().catch(() => { });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function lerArgumentos(argv, flags, comValor = []) {
|
|
53
|
+
const posicionais = [];
|
|
54
|
+
const marcadas = new Set();
|
|
55
|
+
const valores = {};
|
|
56
|
+
for (let i = 0; i < argv.length; i++) {
|
|
57
|
+
const token = argv[i] ?? '';
|
|
58
|
+
if (!token.startsWith('-'))
|
|
59
|
+
posicionais.push(token);
|
|
60
|
+
else if (comValor.includes(token)) {
|
|
61
|
+
const valor = argv[++i];
|
|
62
|
+
if (valor === undefined)
|
|
63
|
+
throw new Error(`${token} exige um valor.`);
|
|
64
|
+
valores[token] = valor;
|
|
65
|
+
}
|
|
66
|
+
else if (flags.includes(token))
|
|
67
|
+
marcadas.add(token);
|
|
68
|
+
else {
|
|
69
|
+
const sugestao = suggestFlag(token, [...flags, ...comValor]);
|
|
70
|
+
throw new Error(`Flag desconhecida: ${token}.${sugestao ? ` Você quis dizer ${sugestao}?` : ''}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { posicionais, marcadas, valores };
|
|
74
|
+
}
|
|
75
|
+
function interrompido(valor) {
|
|
76
|
+
if (p.isCancel(valor))
|
|
77
|
+
throw new Error('Operação cancelada.');
|
|
78
|
+
return valor;
|
|
79
|
+
}
|
|
80
|
+
/** Usuário, senha e banco vão com `encodeURIComponent`; porta vazia vira a padrão do tipo. */
|
|
81
|
+
export function montarDsn(dados) {
|
|
82
|
+
const usuario = encodeURIComponent(dados.usuario.trim());
|
|
83
|
+
const credencial = dados.senha ? `${usuario}:${encodeURIComponent(dados.senha)}` : usuario;
|
|
84
|
+
const porta = dados.porta?.trim() || PORTA_PADRAO[dados.tipo];
|
|
85
|
+
return `${ESQUEMA[dados.tipo]}://${credencial}@${dados.host.trim()}:${porta}/${encodeURIComponent(dados.banco.trim())}`;
|
|
86
|
+
}
|
|
87
|
+
async function perguntarConexao() {
|
|
88
|
+
const tipo = interrompido(await p.select({
|
|
89
|
+
message: 'Tipo do banco',
|
|
90
|
+
options: TIPOS_BANCO.map((valor) => ({ value: valor, label: valor })),
|
|
91
|
+
}));
|
|
92
|
+
const obrigatorio = (valor) => (valor?.trim() ? undefined : 'Obrigatório');
|
|
93
|
+
if (tipo === 'sqlite') {
|
|
94
|
+
const caminho = interrompido(await p.text({ message: 'Caminho do arquivo SQLite', validate: obrigatorio }));
|
|
95
|
+
const producao = interrompido(await p.confirm({ message: 'É banco de produção?', initialValue: false }));
|
|
96
|
+
return { tipo, caminho: caminho.trim(), ...(producao ? { producao } : {}) };
|
|
97
|
+
}
|
|
98
|
+
const host = interrompido(await p.text({ message: 'Host', initialValue: 'localhost', validate: obrigatorio }));
|
|
99
|
+
const porta = interrompido(await p.text({ message: 'Porta', initialValue: PORTA_PADRAO[tipo] }));
|
|
100
|
+
const banco = interrompido(await p.text({ message: 'Banco', validate: obrigatorio }));
|
|
101
|
+
const usuario = interrompido(await p.text({ message: 'Usuário', validate: obrigatorio }));
|
|
102
|
+
const senha = interrompido(await p.password({ message: 'Senha' }));
|
|
103
|
+
const producao = interrompido(await p.confirm({ message: 'É banco de produção?', initialValue: false }));
|
|
104
|
+
const dsn = montarDsn({ tipo, host, porta, banco, usuario, senha });
|
|
105
|
+
return { tipo, dsn, ...(producao ? { producao } : {}) };
|
|
106
|
+
}
|
|
107
|
+
function conexaoDoDsn(dsn, cwd, producao) {
|
|
108
|
+
const interpretada = interpretarDsn(dsn, cwd);
|
|
109
|
+
const conexao = { tipo: interpretada.tipo };
|
|
110
|
+
if (interpretada.tipo === 'sqlite') {
|
|
111
|
+
const texto = dsn.trim();
|
|
112
|
+
conexao.caminho = /^(sqlite|file):/i.test(texto) ? texto.replace(/^(sqlite|file):/i, '').replace(/^\/\/(?=\/)/, '') : texto;
|
|
113
|
+
}
|
|
114
|
+
else if (interpretada.dsn)
|
|
115
|
+
conexao.dsn = interpretada.dsn;
|
|
116
|
+
if (producao)
|
|
117
|
+
conexao.producao = true;
|
|
118
|
+
return conexao;
|
|
119
|
+
}
|
|
120
|
+
async function adicionar(ctx, argv) {
|
|
121
|
+
const { posicionais, marcadas, valores } = lerArgumentos(argv, ['--producao', '--sem-testar', '--substituir', '--projeto'], ['--dsn', '--env']);
|
|
122
|
+
const [nome, ...sobra] = posicionais;
|
|
123
|
+
if (!nome || sobra.length > 0)
|
|
124
|
+
throw new Error('Uso: ai-harness banco-dados adicionar <nome> [--projeto [--env <VAR>]] [--dsn <dsn>] [--producao] [--sem-testar] [--substituir]');
|
|
125
|
+
if (marcadas.has('--projeto'))
|
|
126
|
+
return adicionarNoProjeto(ctx, nome, marcadas, valores);
|
|
127
|
+
if (valores['--env'] !== undefined)
|
|
128
|
+
throw new Error('--env só vale com --projeto.');
|
|
129
|
+
const substituir = marcadas.has('--substituir');
|
|
130
|
+
if (!substituir && nome in lerCadastro(ctx.cadastro).conexoes) {
|
|
131
|
+
ctx.erro(`A conexão ${nome} já existe em ${ctx.cadastro}. Use --substituir para trocá-la.`);
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
const dsn = valores['--dsn'];
|
|
135
|
+
const conexao = dsn === undefined ? await perguntarConexao() : conexaoDoDsn(dsn, ctx.cwd, marcadas.has('--producao'));
|
|
136
|
+
ctx.sensiveis.push(conexao);
|
|
137
|
+
if (!marcadas.has('--sem-testar')) {
|
|
138
|
+
const falha = await testarConexao(ctx, conexao);
|
|
139
|
+
if (falha !== null) {
|
|
140
|
+
ctx.erro(`Falha ao conectar em ${nome} (${resumirConexao(conexao)}): ${falha}`);
|
|
141
|
+
ctx.erro('Nada foi gravado. Confira os dados ou use --sem-testar para gravar mesmo assim.');
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
salvarConexao(nome, conexao, { substituir, caminho: ctx.cadastro });
|
|
146
|
+
ctx.log(`Conexão ${nome} gravada em ${ctx.cadastro}: ${resumirConexao(conexao)}${conexao.producao ? ' (produção)' : ''}.`);
|
|
147
|
+
ctx.log(`No projeto, rode: npx @fontdata/ai-harness banco-dados usar ${nome}`);
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
/** Procura o nome no JSON bruto de banco_dados.conexoes; recusa lista inválida antes de qualquer gravação. */
|
|
151
|
+
function entradaDeclarada(cwd, nome) {
|
|
152
|
+
validarConfigParaEscrita(cwd);
|
|
153
|
+
const arquivo = join(cwd, CONFIG_PATH);
|
|
154
|
+
if (!existsSync(arquivo))
|
|
155
|
+
return { forma: 'ausente' };
|
|
156
|
+
const dados = JSON.parse(readFileSync(arquivo, 'utf8'));
|
|
157
|
+
const banco = isObject(dados) ? secaoBanco(dados) : {};
|
|
158
|
+
if (banco.conexoes === undefined)
|
|
159
|
+
return { forma: 'ausente' };
|
|
160
|
+
if (!Array.isArray(banco.conexoes))
|
|
161
|
+
throw new Error(`banco_dados.conexoes em ${CONFIG_PATH} não é uma lista; corrija-o antes de gravar a conexão ${nome}.`);
|
|
162
|
+
const achada = banco.conexoes.some((entrada) => (isObject(entrada) ? entrada.nome : entrada) === nome);
|
|
163
|
+
if (!achada)
|
|
164
|
+
return { forma: 'ausente' };
|
|
165
|
+
const conexao = readConfig(cwd).config.banco_dados.projeto[nome];
|
|
166
|
+
return conexao ? { forma: 'objeto', conexao } : { forma: 'outra' };
|
|
167
|
+
}
|
|
168
|
+
function variavelPadrao(nome) {
|
|
169
|
+
return `${nome.toUpperCase().replace(/-/g, '_')}_DATABASE_URL`;
|
|
170
|
+
}
|
|
171
|
+
async function adicionarNoProjeto(ctx, nome, marcadas, valores) {
|
|
172
|
+
const substituir = marcadas.has('--substituir');
|
|
173
|
+
if (!REGRA_NOME_CONEXAO.test(nome)) {
|
|
174
|
+
ctx.erro(`Nome de conexão inválido: ${JSON.stringify(nome)}; use só minúsculas, dígitos, - e _, começando por minúscula ou dígito.`);
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
const pedida = valores['--env'];
|
|
178
|
+
if (pedida !== undefined && !REGRA_DSN_ENV.test(pedida)) {
|
|
179
|
+
ctx.erro(`--env inválido: ${JSON.stringify(pedida)}; use letras, dígitos e _, sem começar por dígito.`);
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
182
|
+
validarSettingsLocal(ctx.cwd);
|
|
183
|
+
const declarada = entradaDeclarada(ctx.cwd, nome);
|
|
184
|
+
let variavel = pedida ?? variavelPadrao(nome);
|
|
185
|
+
if (declarada.forma === 'outra' && !substituir) {
|
|
186
|
+
ctx.erro(`A conexão ${nome} já está em banco_dados.conexoes de ${CONFIG_PATH} sem dsn_env. Use --substituir para trocá-la pela conexão do projeto.`);
|
|
187
|
+
return 1;
|
|
188
|
+
}
|
|
189
|
+
if (declarada.forma === 'objeto') {
|
|
190
|
+
const atual = declarada.conexao;
|
|
191
|
+
if (pedida !== undefined && pedida !== atual.dsn_env) {
|
|
192
|
+
ctx.erro(`A conexão ${nome} já está declarada em ${CONFIG_PATH} com dsn_env ${atual.dsn_env}; rode sem --env ou com --env ${atual.dsn_env}.`);
|
|
193
|
+
return 1;
|
|
194
|
+
}
|
|
195
|
+
variavel = atual.dsn_env;
|
|
196
|
+
}
|
|
197
|
+
let valor;
|
|
198
|
+
let tipoEscolhido;
|
|
199
|
+
let producao = marcadas.has('--producao');
|
|
200
|
+
if (valores['--dsn'] === undefined) {
|
|
201
|
+
const perguntada = await perguntarConexao();
|
|
202
|
+
valor = perguntada.tipo === 'sqlite' ? `sqlite:${perguntada.caminho ?? ''}` : (perguntada.dsn ?? '');
|
|
203
|
+
tipoEscolhido = perguntada.tipo;
|
|
204
|
+
producao ||= perguntada.producao === true;
|
|
205
|
+
}
|
|
206
|
+
else
|
|
207
|
+
valor = valores['--dsn'].trim();
|
|
208
|
+
ctx.sensiveis.push(sensivel(valor));
|
|
209
|
+
const promover = declarada.forma === 'objeto' && producao && !declarada.conexao.producao;
|
|
210
|
+
if (promover && !substituir) {
|
|
211
|
+
ctx.erro(`A conexão ${nome} está declarada em ${CONFIG_PATH} sem produção. Use --substituir junto com --producao para marcá-la como produção.`);
|
|
212
|
+
return 1;
|
|
213
|
+
}
|
|
214
|
+
const alvo = interpretarDsn(valor, ctx.cwd);
|
|
215
|
+
if (declarada.forma === 'objeto' && declarada.conexao.tipo !== undefined && declarada.conexao.tipo !== alvo.tipo) {
|
|
216
|
+
ctx.erro(`O DSN informado é do tipo ${alvo.tipo}, mas a conexão ${nome} declara tipo ${declarada.conexao.tipo} em ${CONFIG_PATH}. Nada foi gravado.`);
|
|
217
|
+
return 1;
|
|
218
|
+
}
|
|
219
|
+
const anterior = lerEnvSettingsLocal(ctx.cwd)[variavel];
|
|
220
|
+
if (anterior !== undefined)
|
|
221
|
+
ctx.sensiveis.push(sensivel(anterior));
|
|
222
|
+
if (anterior !== undefined && anterior !== valor && !substituir) {
|
|
223
|
+
ctx.erro(`A variável ${variavel} já está definida em ${SETTINGS_LOCAL_PATH} com outro valor. Use --substituir para trocá-la.`);
|
|
224
|
+
return 1;
|
|
225
|
+
}
|
|
226
|
+
if (!marcadas.has('--sem-testar')) {
|
|
227
|
+
const falha = await testarConexao(ctx, alvo);
|
|
228
|
+
if (falha !== null) {
|
|
229
|
+
ctx.erro(`Falha ao conectar em ${nome} (${resumirConexao(conexaoDoDsn(valor, ctx.cwd, false))}): ${falha}`);
|
|
230
|
+
ctx.erro('Nada foi gravado. Confira os dados ou use --sem-testar para gravar mesmo assim.');
|
|
231
|
+
return 1;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const { changed } = gravarVariavelSettingsLocal(ctx.cwd, variavel, valor);
|
|
235
|
+
ctx.log(changed ? `Variável ${variavel} gravada em ${SETTINGS_LOCAL_PATH}.` : `Variável ${variavel} já estava com esse valor em ${SETTINGS_LOCAL_PATH}.`);
|
|
236
|
+
if (declarada.forma === 'objeto' && !promover) {
|
|
237
|
+
ctx.log(`Conexão ${nome} já declarada em banco_dados.conexoes de ${CONFIG_PATH}; a entrada não foi alterada.`);
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
const entrada = declarada.forma === 'objeto'
|
|
241
|
+
? { ...declarada.conexao, producao: true }
|
|
242
|
+
: {
|
|
243
|
+
nome,
|
|
244
|
+
dsn_env: variavel,
|
|
245
|
+
...(tipoEscolhido === undefined ? {} : { tipo: tipoEscolhido }),
|
|
246
|
+
...(producao ? { producao: true } : {}),
|
|
247
|
+
};
|
|
248
|
+
gravarConexaoProjeto(ctx.cwd, entrada, { substituir: declarada.forma !== 'ausente' });
|
|
249
|
+
ctx.log(`Conexão ${nome} gravada em banco_dados.conexoes de ${CONFIG_PATH} (dsn_env ${variavel})${entrada.producao ? ', produção' : ''}.`);
|
|
250
|
+
if (declarada.forma === 'ausente')
|
|
251
|
+
ctx.log(`Versione o ${CONFIG_PATH}; cada colega define ${variavel} no próprio ambiente.`);
|
|
252
|
+
return 0;
|
|
253
|
+
}
|
|
254
|
+
function listar(ctx, argv) {
|
|
255
|
+
lerArgumentos(argv, []);
|
|
256
|
+
const { conexoes, problemas } = lerCadastro(ctx.cadastro);
|
|
257
|
+
for (const problema of problemas)
|
|
258
|
+
ctx.erro(problema);
|
|
259
|
+
const { config } = readConfig(ctx.cwd);
|
|
260
|
+
const doProjeto = config.banco_dados.conexoes.flatMap((nome) => config.banco_dados.projeto[nome] ?? []);
|
|
261
|
+
const nomes = Object.keys(conexoes).sort();
|
|
262
|
+
if (nomes.length === 0 && doProjeto.length === 0) {
|
|
263
|
+
ctx.log(`Nenhuma conexão cadastrada em ${ctx.cadastro}. Cadastre com: npx @fontdata/ai-harness banco-dados adicionar <nome>`);
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
for (const nome of nomes) {
|
|
267
|
+
const conexao = conexoes[nome];
|
|
268
|
+
ctx.log(`${nome} ${conexao.tipo} ${resumirConexao(conexao)} ${conexao.producao ? 'produção' : 'não produção'}`);
|
|
269
|
+
}
|
|
270
|
+
if (doProjeto.length > 0)
|
|
271
|
+
ctx.log(`Conexões do projeto em ${CONFIG_PATH}:`);
|
|
272
|
+
for (const declarada of doProjeto) {
|
|
273
|
+
const lida = lerVariavel(declarada.dsn_env, ctx.cwd);
|
|
274
|
+
let tipo = declarada.tipo ?? '?';
|
|
275
|
+
if (lida) {
|
|
276
|
+
ctx.sensiveis.push(sensivel(lida.valor));
|
|
277
|
+
if (declarada.tipo === undefined) {
|
|
278
|
+
try {
|
|
279
|
+
tipo = interpretarDsn(lida.valor, ctx.cwd).tipo;
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
tipo = 'DSN inválido';
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const fonte = lida ? `(${lida.fonte})` : 'ausente';
|
|
287
|
+
ctx.log(`${declarada.nome} ${tipo} ${declarada.dsn_env} ${fonte} ${declarada.producao ? 'produção' : 'não produção'}`);
|
|
288
|
+
}
|
|
289
|
+
return 0;
|
|
290
|
+
}
|
|
291
|
+
async function testar(ctx, argv) {
|
|
292
|
+
const { posicionais } = lerArgumentos(argv, []);
|
|
293
|
+
if (posicionais.length > 1)
|
|
294
|
+
throw new Error('Uso: ai-harness banco-dados testar [nome]');
|
|
295
|
+
const [nome] = posicionais;
|
|
296
|
+
const cadastro = lerCadastro(ctx.cadastro);
|
|
297
|
+
const doProjeto = nome !== undefined && nome in readConfig(ctx.cwd).config.banco_dados.projeto;
|
|
298
|
+
let alvos;
|
|
299
|
+
let ok = true;
|
|
300
|
+
if (nome !== undefined && !doProjeto) {
|
|
301
|
+
const conexao = cadastro.conexoes[nome];
|
|
302
|
+
if (!conexao) {
|
|
303
|
+
ctx.erro(`Conexão ${nome} não encontrada em ${ctx.cadastro}; rode banco-dados adicionar ${nome}.`);
|
|
304
|
+
return 1;
|
|
305
|
+
}
|
|
306
|
+
alvos = [{ nome, ...conexao }];
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
const resolvidas = resolverConexoes(ctx.cwd, { cadastro });
|
|
310
|
+
const pedida = (item) => nome === undefined || item.nome === nome;
|
|
311
|
+
ctx.sensiveis.push(...resolvidas.segredos.map(sensivel));
|
|
312
|
+
if (nome === undefined)
|
|
313
|
+
for (const problema of resolvidas.problemas)
|
|
314
|
+
ctx.erro(problema);
|
|
315
|
+
for (const faltando of resolvidas.faltando.filter((item) => nome === undefined || item === nome)) {
|
|
316
|
+
ctx.erro(`Conexão ${faltando} não encontrada nesta máquina; rode banco-dados adicionar ${faltando}.`);
|
|
317
|
+
ok = false;
|
|
318
|
+
}
|
|
319
|
+
for (const ausente of resolvidas.variaveisAusentes.filter(pedida)) {
|
|
320
|
+
ctx.erro(`A variável ${ausente.variavel} da conexão ${ausente.nome} não está definida; defina-a no ${SETTINGS_LOCAL_PATH}, no .env ou no ambiente.`);
|
|
321
|
+
ok = false;
|
|
322
|
+
}
|
|
323
|
+
for (const descartada of resolvidas.descartadas.filter(pedida)) {
|
|
324
|
+
if (nome !== undefined)
|
|
325
|
+
ctx.erro(`Conexão ${descartada.nome} descartada: ${descartada.motivo}.`);
|
|
326
|
+
ok = false;
|
|
327
|
+
}
|
|
328
|
+
alvos = resolvidas.conexoes.filter(pedida);
|
|
329
|
+
if (alvos.length === 0 && ok) {
|
|
330
|
+
ctx.erro('Nenhuma conexão de banco configurada neste projeto; rode npx @fontdata/ai-harness banco-dados usar <nome>.');
|
|
331
|
+
return 1;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
for (const alvo of alvos) {
|
|
335
|
+
ctx.sensiveis.push(alvo);
|
|
336
|
+
const falha = await testarConexao(ctx, alvo);
|
|
337
|
+
if (falha === null)
|
|
338
|
+
ctx.log(`ok ${alvo.nome} ${resumirConexao(alvo)}`);
|
|
339
|
+
else {
|
|
340
|
+
ok = false;
|
|
341
|
+
ctx.erro(`falhou ${alvo.nome} ${resumirConexao(alvo)}: ${falha}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return ok ? 0 : 1;
|
|
345
|
+
}
|
|
346
|
+
function usar(ctx, argv) {
|
|
347
|
+
const { posicionais: nomes } = lerArgumentos(argv, []);
|
|
348
|
+
if (nomes.length === 0)
|
|
349
|
+
throw new Error('Uso: ai-harness banco-dados usar <nome...>');
|
|
350
|
+
writeDefaultConfig(ctx.cwd);
|
|
351
|
+
const arquivo = join(ctx.cwd, CONFIG_PATH);
|
|
352
|
+
let dados;
|
|
353
|
+
try {
|
|
354
|
+
dados = JSON.parse(readFileSync(arquivo, 'utf8'));
|
|
355
|
+
}
|
|
356
|
+
catch (erro) {
|
|
357
|
+
throw new Error(`${CONFIG_PATH} está com JSON inválido (${erro.message}); corrija antes de rodar banco-dados usar.`);
|
|
358
|
+
}
|
|
359
|
+
if (!isObject(dados))
|
|
360
|
+
throw new Error(`${CONFIG_PATH} precisa ser um objeto JSON; corrija antes de rodar banco-dados usar.`);
|
|
361
|
+
const banco = secaoBanco(dados);
|
|
362
|
+
const objetos = new Map();
|
|
363
|
+
for (const entrada of Array.isArray(banco.conexoes) ? banco.conexoes : []) {
|
|
364
|
+
if (isObject(entrada) && typeof entrada.nome === 'string' && !objetos.has(entrada.nome))
|
|
365
|
+
objetos.set(entrada.nome, entrada);
|
|
366
|
+
}
|
|
367
|
+
const unicos = [...new Set(nomes)];
|
|
368
|
+
const proximas = unicos.map((nome) => objetos.get(nome) ?? nome);
|
|
369
|
+
writeFileSync(arquivo, `${JSON.stringify(comSecaoBanco(dados, { ...banco, conexoes: proximas }), null, 2)}\n`);
|
|
370
|
+
ctx.log(`banco_dados.conexoes em ${CONFIG_PATH}: ${unicos.join(', ')}.`);
|
|
371
|
+
const { conexoes } = lerCadastro(ctx.cadastro);
|
|
372
|
+
const ausentes = unicos.filter((nome) => !(nome in conexoes) && !objetos.has(nome));
|
|
373
|
+
if (ausentes.length > 0) {
|
|
374
|
+
ctx.erro(`Aviso: sem cadastro nesta máquina para ${ausentes.join(', ')}; rode banco-dados adicionar <nome> para cada uma.`);
|
|
375
|
+
}
|
|
376
|
+
return 0;
|
|
377
|
+
}
|
|
378
|
+
function remover(ctx, argv) {
|
|
379
|
+
const { posicionais } = lerArgumentos(argv, []);
|
|
380
|
+
const [nome, ...sobra] = posicionais;
|
|
381
|
+
if (!nome || sobra.length > 0)
|
|
382
|
+
throw new Error('Uso: ai-harness banco-dados remover <nome>');
|
|
383
|
+
if (!removerConexao(nome, ctx.cadastro)) {
|
|
384
|
+
ctx.erro(`Conexão ${nome} não existe em ${ctx.cadastro}.`);
|
|
385
|
+
return 1;
|
|
386
|
+
}
|
|
387
|
+
ctx.log(`Conexão ${nome} removida de ${ctx.cadastro}.`);
|
|
388
|
+
return 0;
|
|
389
|
+
}
|
|
390
|
+
/** `ai-harness banco-dados <subcomando>`: cadastro de conexões da máquina e escolha das conexões do projeto. */
|
|
391
|
+
export async function runBancoCommand(argv, opcoes = {}) {
|
|
392
|
+
const saida = opcoes.saida ?? { log: (texto) => console.log(texto), erro: (texto) => console.error(texto) };
|
|
393
|
+
const cadastro = opcoes.caminhoCadastro ?? caminhoCadastro();
|
|
394
|
+
const cwd = opcoes.cwd ?? process.cwd();
|
|
395
|
+
const lido = lerCadastro(cadastro);
|
|
396
|
+
const ctx = {
|
|
397
|
+
cwd,
|
|
398
|
+
cadastro,
|
|
399
|
+
criarDriver: opcoes.criarDriver ?? criarDriverPadrao,
|
|
400
|
+
log: (texto) => saida.log(mascarar(ctx, texto)),
|
|
401
|
+
erro: (texto) => saida.erro(mascarar(ctx, texto)),
|
|
402
|
+
sensiveis: [...Object.values(lido.conexoes), ...resolverConexoes(cwd, { cadastro: lido }).segredos.map(sensivel)],
|
|
403
|
+
};
|
|
404
|
+
const [subcomando, ...resto] = argv;
|
|
405
|
+
try {
|
|
406
|
+
switch (subcomando) {
|
|
407
|
+
case 'adicionar':
|
|
408
|
+
return await adicionar(ctx, resto);
|
|
409
|
+
case 'listar':
|
|
410
|
+
return listar(ctx, resto);
|
|
411
|
+
case 'testar':
|
|
412
|
+
return await testar(ctx, resto);
|
|
413
|
+
case 'usar':
|
|
414
|
+
return usar(ctx, resto);
|
|
415
|
+
case 'remover':
|
|
416
|
+
return remover(ctx, resto);
|
|
417
|
+
case '--help':
|
|
418
|
+
case '-h':
|
|
419
|
+
ctx.log(AJUDA);
|
|
420
|
+
return 0;
|
|
421
|
+
default:
|
|
422
|
+
if (subcomando !== undefined)
|
|
423
|
+
ctx.erro(`Subcomando desconhecido: banco-dados ${subcomando}`);
|
|
424
|
+
ctx.erro(AJUDA);
|
|
425
|
+
return 1;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
catch (erro) {
|
|
429
|
+
ctx.erro(erro instanceof Error ? erro.message : String(erro));
|
|
430
|
+
return 1;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { readConfig } from '../config/config.js';
|
|
3
|
+
import { lerVariavel, lerVariavelDotEnv } from './ambiente.js';
|
|
4
|
+
import { lerCadastro } from './cadastro.js';
|
|
5
|
+
const EXTENSAO_SQLITE = /\.(db|sqlite|sqlite3)$/i;
|
|
6
|
+
export function raizDoProjeto(env = process.env, cwd = process.cwd()) {
|
|
7
|
+
return env.CLAUDE_PROJECT_DIR || cwd;
|
|
8
|
+
}
|
|
9
|
+
/** Descobre o tipo pelo esquema do DSN; o erro cita só o esquema, nunca o DSN inteiro. */
|
|
10
|
+
export function interpretarDsn(dsn, raiz) {
|
|
11
|
+
const texto = dsn.trim();
|
|
12
|
+
if (/^postgres(ql)?:\/\//i.test(texto))
|
|
13
|
+
return { tipo: 'postgres', dsn: texto };
|
|
14
|
+
if (/^(mysql|mariadb):\/\//i.test(texto))
|
|
15
|
+
return { tipo: 'mariadb', dsn: texto };
|
|
16
|
+
const semEsquema = /^(sqlite|file):/i.test(texto) ? texto.replace(/^(sqlite|file):/i, '').replace(/^\/\/(?=\/)/, '') : undefined;
|
|
17
|
+
if (semEsquema !== undefined || EXTENSAO_SQLITE.test(texto)) {
|
|
18
|
+
const caminho = semEsquema ?? texto;
|
|
19
|
+
if (!caminho)
|
|
20
|
+
throw new Error('DSN sqlite sem caminho do arquivo');
|
|
21
|
+
return { tipo: 'sqlite', caminho: resolve(raiz, caminho) };
|
|
22
|
+
}
|
|
23
|
+
const esquema = /^([a-z][a-z0-9+.-]*):/i.exec(texto)?.[1];
|
|
24
|
+
const descricao = esquema ? `esquema "${esquema}:" não reconhecido` : 'DSN não reconhecido';
|
|
25
|
+
throw new Error(`${descricao}; use postgres://, postgresql://, mysql://, mariadb://, sqlite: ou um arquivo .db/.sqlite/.sqlite3`);
|
|
26
|
+
}
|
|
27
|
+
export function lerDatabaseUrl(raiz) {
|
|
28
|
+
return lerVariavelDotEnv(raiz, 'DATABASE_URL');
|
|
29
|
+
}
|
|
30
|
+
function daConexaoCadastrada(nome, conexao, raiz) {
|
|
31
|
+
const resolvida = {
|
|
32
|
+
nome,
|
|
33
|
+
tipo: conexao.tipo,
|
|
34
|
+
producao: conexao.producao === true,
|
|
35
|
+
mascaramento: conexao.mascaramento !== false,
|
|
36
|
+
origem: 'cadastro',
|
|
37
|
+
};
|
|
38
|
+
if (conexao.tipo === 'sqlite')
|
|
39
|
+
resolvida.caminho = resolve(raiz, conexao.caminho ?? '');
|
|
40
|
+
else if (conexao.dsn)
|
|
41
|
+
resolvida.dsn = conexao.dsn;
|
|
42
|
+
return resolvida;
|
|
43
|
+
}
|
|
44
|
+
function resolverDoProjeto(declarada, raiz, env, saida) {
|
|
45
|
+
const { nome, dsn_env: variavel } = declarada;
|
|
46
|
+
const lida = lerVariavel(variavel, raiz, env);
|
|
47
|
+
if (!lida) {
|
|
48
|
+
saida.variaveisAusentes.push({ nome, variavel });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
saida.segredos.push(lida.valor);
|
|
52
|
+
let motivo;
|
|
53
|
+
try {
|
|
54
|
+
const interpretado = interpretarDsn(lida.valor, raiz);
|
|
55
|
+
if (declarada.tipo === undefined || declarada.tipo === interpretado.tipo) {
|
|
56
|
+
saida.conexoes.push({
|
|
57
|
+
nome,
|
|
58
|
+
producao: declarada.producao,
|
|
59
|
+
mascaramento: true,
|
|
60
|
+
...interpretado,
|
|
61
|
+
origem: 'projeto',
|
|
62
|
+
variavel,
|
|
63
|
+
fonte: lida.fonte,
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
motivo = `o DSN da variável ${variavel} é do tipo ${interpretado.tipo}, mas a conexão declara tipo ${declarada.tipo}`;
|
|
68
|
+
}
|
|
69
|
+
catch (erro) {
|
|
70
|
+
motivo = `o DSN da variável ${variavel} é inválido: ${erro.message}`;
|
|
71
|
+
}
|
|
72
|
+
saida.descartadas.push({ nome, motivo });
|
|
73
|
+
saida.problemas.push(`Conexão ${nome} descartada: ${motivo}.`);
|
|
74
|
+
}
|
|
75
|
+
/** Percorre banco_dados.conexoes na ordem; sem a lista declarada no projeto, vale o DATABASE_URL do .env como a conexão `env`. */
|
|
76
|
+
export function resolverConexoes(raiz, opcoes = {}) {
|
|
77
|
+
const { config, problems } = readConfig(raiz);
|
|
78
|
+
const saida = {
|
|
79
|
+
conexoes: [],
|
|
80
|
+
faltando: [],
|
|
81
|
+
variaveisAusentes: [],
|
|
82
|
+
descartadas: [],
|
|
83
|
+
problemas: [...problems],
|
|
84
|
+
segredos: [],
|
|
85
|
+
};
|
|
86
|
+
const { conexoes: nomes, projeto, declarado } = config.banco_dados;
|
|
87
|
+
if (!declarado) {
|
|
88
|
+
const url = lerDatabaseUrl(raiz);
|
|
89
|
+
if (url) {
|
|
90
|
+
saida.segredos.push(url);
|
|
91
|
+
try {
|
|
92
|
+
saida.conexoes.push({ nome: 'env', producao: false, mascaramento: true, ...interpretarDsn(url, raiz), origem: 'env' });
|
|
93
|
+
}
|
|
94
|
+
catch (erro) {
|
|
95
|
+
saida.problemas.push(`DATABASE_URL do .env ignorado: ${erro.message}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return saida;
|
|
99
|
+
}
|
|
100
|
+
let cadastro;
|
|
101
|
+
for (const nome of nomes) {
|
|
102
|
+
const declarada = projeto[nome];
|
|
103
|
+
if (declarada) {
|
|
104
|
+
resolverDoProjeto(declarada, raiz, opcoes.env ?? process.env, saida);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (!cadastro) {
|
|
108
|
+
cadastro = opcoes.cadastro ?? lerCadastro();
|
|
109
|
+
saida.problemas.push(...cadastro.problemas);
|
|
110
|
+
}
|
|
111
|
+
const conexao = cadastro.conexoes[nome];
|
|
112
|
+
if (conexao)
|
|
113
|
+
saida.conexoes.push(daConexaoCadastrada(nome, conexao, raiz));
|
|
114
|
+
else
|
|
115
|
+
saida.faltando.push(nome);
|
|
116
|
+
}
|
|
117
|
+
return saida;
|
|
118
|
+
}
|
|
119
|
+
export function escolherConexao(resolvidas, nome) {
|
|
120
|
+
if (resolvidas.length === 0) {
|
|
121
|
+
throw new Error('Nenhuma conexão de banco configurada neste projeto; rode npx @fontdata/ai-harness banco-dados usar <nome>');
|
|
122
|
+
}
|
|
123
|
+
const escolhida = nome === undefined ? resolvidas[0] : resolvidas.find((conexao) => conexao.nome === nome);
|
|
124
|
+
if (!escolhida)
|
|
125
|
+
throw new Error(`Conexão ${nome} não está em banco_dados.conexoes deste projeto`);
|
|
126
|
+
return escolhida;
|
|
127
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** `alvo` é o DSN no Postgres e no MariaDB e o caminho do arquivo no SQLite. */
|
|
2
|
+
export async function criarDriver(tipo, alvo) {
|
|
3
|
+
switch (tipo) {
|
|
4
|
+
case 'postgres':
|
|
5
|
+
return (await import('./postgres.js')).criarDriverPostgres(alvo);
|
|
6
|
+
case 'mariadb':
|
|
7
|
+
return (await import('./mariadb.js')).criarDriverMariadb(alvo);
|
|
8
|
+
case 'sqlite':
|
|
9
|
+
return (await import('./sqlite.js')).criarDriverSqlite(alvo);
|
|
10
|
+
default:
|
|
11
|
+
throw new Error(`Tipo de banco desconhecido: ${String(tipo)}`);
|
|
12
|
+
}
|
|
13
|
+
}
|