banco-sync-cli 1.0.0 → 1.0.1
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/README.md +79 -0
- package/bin/index.js +172 -120
- package/lib/config.js +2 -2
- package/lib/db.js +5 -5
- package/lib/diff.js +2 -2
- package/lib/gemini.js +18 -18
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# banco-sync-cli
|
|
2
|
+
|
|
3
|
+
`banco-sync-cli` é um utilitário CLI em Node.js projetado para manter seus arquivos locais de migração/criação SQL em perfeita sincronia com o estado atual de um banco de dados PostgreSQL. Ele utiliza a API do Gemini (`gemini-2.5-flash`) para analisar as diferenças de estrutura e atualizar os arquivos em tempo real, exibindo um diff colorido no terminal (no estilo `git diff`) antes de salvar as alterações.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Recursos
|
|
8
|
+
|
|
9
|
+
- **Suporte Exclusivo a PostgreSQL**: Coleta metadados de tabelas, colunas, tipos de dados, nulidade, chaves primárias e valores padrões.
|
|
10
|
+
- **Integração com Gemini API**: Usa o modelo `gemini-2.5-flash` com saídas em JSON estruturado para garantir modificações de esquema precisas, preservando comentários, formatação e estilo dos arquivos originais.
|
|
11
|
+
- **Diff Visual Colorida**: Exibe modificações em verde (adições) e vermelho (remoções) com trechos de contexto antes de aplicar as mudanças.
|
|
12
|
+
- **Armazenamento Seguro**: Armazena as credenciais e chaves de API na pasta Home do usuário (`~/.banco-sync-cli/config.json`) indexadas pelo caminho absoluto do projeto, prevenindo vazamentos acidentais em repositórios Git.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Instalação e Execução
|
|
17
|
+
|
|
18
|
+
Como a ferramenta foi estruturada para ser executada via `npx`, você pode executá-la diretamente no diretório do projeto:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Executar a CLI a partir do diretório raiz
|
|
22
|
+
npx . <comando>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Comandos
|
|
28
|
+
|
|
29
|
+
### 1. `login`
|
|
30
|
+
|
|
31
|
+
Configura ou atualiza as credenciais do banco de dados PostgreSQL, a chave de API do Gemini e a pasta de migrações SQL.
|
|
32
|
+
|
|
33
|
+
Você pode executar o login de forma **interativa** (respondendo às perguntas na tela):
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx . login
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Ou de forma **não-interativa** passando parâmetros por linha de comando. Se todos os parâmetros obrigatórios forem fornecidos, os prompts interativos serão ignorados:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx . login \
|
|
43
|
+
--host "meu-ip-ou-host" \
|
|
44
|
+
--port 5432 \
|
|
45
|
+
--user "meu_usuario" \
|
|
46
|
+
--password "minha_senha" \
|
|
47
|
+
--database "meu_banco" \
|
|
48
|
+
--schema "public" \
|
|
49
|
+
--gemini-api-key "MINHA_API_KEY_DO_GEMINI" \
|
|
50
|
+
--sql-folder "caminho/da/pasta/migrations"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
#### Opções do Comando `login`:
|
|
54
|
+
- `--host <host>`: IP ou Host de conexão do PostgreSQL.
|
|
55
|
+
- `--port <port>`: Porta de conexão (Padrão: `5432`).
|
|
56
|
+
- `--user <user>`: Usuário do banco (Padrão: `postgres`).
|
|
57
|
+
- `--password <password>`: Senha de acesso ao banco.
|
|
58
|
+
- `--database <database>`: Nome do banco de dados PostgreSQL.
|
|
59
|
+
- `--schema <schema>`: Schema do banco a ser lido (Padrão: `public`).
|
|
60
|
+
- `--ssl`: Adiciona esta flag caso a conexão precise de criptografia SSL (desabilitada por padrão).
|
|
61
|
+
- `--gemini-api-key <key>`: Chave de acesso à API do Gemini.
|
|
62
|
+
- `--sql-folder <folder>`: Caminho relativo ou absoluto da pasta que contém seus arquivos `.sql`.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### 2. `sync`
|
|
67
|
+
|
|
68
|
+
Conecta-se ao banco de dados configurado, extrai a estrutura do banco e compara os arquivos SQL definidos.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npx . sync
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### Fluxo do Comando `sync`:
|
|
75
|
+
1. **Seleção de arquivos:** Pergunta se você deseja sincronizar **todos** os arquivos da pasta configurada ou apenas um **específico**.
|
|
76
|
+
2. **Coleta de Esquema:** Conecta ao PostgreSQL e lê a estrutura atual.
|
|
77
|
+
3. **Análise do Gemini:** Compara o arquivo `.sql` com o esquema do banco de dados. O Gemini analisa e atualiza o conteúdo do arquivo mantendo apenas a sintaxe que já existia (por exemplo, adicionando uma nova coluna a uma tabela existente).
|
|
78
|
+
4. **Exibição do Diff:** Mostra um preview visual no terminal das alterações propostas (linhas adicionadas e removidas).
|
|
79
|
+
5. **Confirmação:** Solicita sua autorização final (`Y/n`) para escrever as atualizações diretamente no arquivo local.
|
package/bin/index.js
CHANGED
|
@@ -16,137 +16,189 @@ const program = new Command();
|
|
|
16
16
|
|
|
17
17
|
program
|
|
18
18
|
.name('banco-sync-cli')
|
|
19
|
-
.description('CLI
|
|
20
|
-
.version('1.0.0')
|
|
19
|
+
.description('Ferramenta CLI para sincronizar arquivos SQL com a estrutura do banco usando a API do Gemini')
|
|
20
|
+
.version('1.0.0', '-V, --version', 'Exibe a versão atual')
|
|
21
|
+
.helpOption('-h, --help', 'Exibe a ajuda do comando')
|
|
22
|
+
.addHelpCommand('help [comando]', 'Exibe a ajuda do comando');
|
|
21
23
|
|
|
22
24
|
// Login Command
|
|
23
25
|
program
|
|
24
26
|
.command('login')
|
|
25
|
-
.description('
|
|
26
|
-
.
|
|
27
|
-
|
|
27
|
+
.description('Configura as credenciais do banco PostgreSQL, a chave de API do Gemini e a pasta de arquivos SQL')
|
|
28
|
+
.option('--host <host>', 'Host/IP do banco de dados')
|
|
29
|
+
.option('--port <port>', 'Porta do banco de dados')
|
|
30
|
+
.option('--user <user>', 'Usuário do banco de dados')
|
|
31
|
+
.option('--password <password>', 'Senha do banco de dados')
|
|
32
|
+
.option('--database <database>', 'Nome do banco de dados')
|
|
33
|
+
.option('--schema <schema>', 'Schema do banco de dados (padrão: public)')
|
|
34
|
+
.option('--ssl', 'Habilitar conexão SSL (rejectUnauthorized=false)')
|
|
35
|
+
.option('--gemini-api-key <key>', 'Chave de API do Gemini')
|
|
36
|
+
.option('--sql-folder <folder>', 'Caminho da pasta com os arquivos SQL')
|
|
37
|
+
.action(async (options) => {
|
|
38
|
+
console.log(chalk.bold.cyan('\n--- Configurar banco-sync-cli (PostgreSQL) ---\n'));
|
|
28
39
|
|
|
29
40
|
const currentConfig = getConfig() || {};
|
|
30
41
|
const currentDb = currentConfig.dbConfig || {};
|
|
31
42
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
type: 'input',
|
|
51
|
-
name: 'port',
|
|
52
|
-
message: 'Database port:',
|
|
53
|
-
default: (answers) => {
|
|
54
|
-
if (currentDb.port) return currentDb.port.toString();
|
|
55
|
-
return answers.dbType === 'postgres' ? '5432' : '3306';
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
type: 'input',
|
|
60
|
-
name: 'user',
|
|
61
|
-
message: 'Database user:',
|
|
62
|
-
default: (answers) => {
|
|
63
|
-
if (currentDb.user) return currentDb.user;
|
|
64
|
-
return answers.dbType === 'postgres' ? 'postgres' : 'root';
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
type: 'password',
|
|
69
|
-
name: 'password',
|
|
70
|
-
message: 'Database password:',
|
|
71
|
-
mask: '*',
|
|
72
|
-
default: currentDb.password || ''
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
type: 'input',
|
|
76
|
-
name: 'database',
|
|
77
|
-
message: 'Database name:',
|
|
78
|
-
default: currentDb.database || '',
|
|
79
|
-
validate: input => input.trim() !== '' ? true : 'Database name is required'
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
type: 'confirm',
|
|
83
|
-
name: 'ssl',
|
|
84
|
-
message: 'Enable SSL (rejectUnauthorized=false)?',
|
|
85
|
-
default: currentDb.ssl || false,
|
|
86
|
-
when: (answers) => answers.dbType === 'postgres'
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
type: 'password',
|
|
90
|
-
name: 'geminiApiKey',
|
|
91
|
-
message: 'Gemini API Key:',
|
|
92
|
-
mask: '*',
|
|
93
|
-
default: currentConfig.geminiApiKey || '',
|
|
94
|
-
validate: input => input.trim() !== '' ? true : 'Gemini API Key is required'
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
type: 'input',
|
|
98
|
-
name: 'sqlFolder',
|
|
99
|
-
message: 'Path to the folder with SQL files:',
|
|
100
|
-
default: currentConfig.sqlFolder || './',
|
|
101
|
-
validate: input => {
|
|
102
|
-
const resolved = path.resolve(process.cwd(), input);
|
|
103
|
-
if (!fs.existsSync(resolved)) {
|
|
104
|
-
return `Directory does not exist: ${resolved}`;
|
|
105
|
-
}
|
|
106
|
-
if (!fs.statSync(resolved).isDirectory()) {
|
|
107
|
-
return `Path is not a directory: ${resolved}`;
|
|
108
|
-
}
|
|
109
|
-
return true;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
]);
|
|
43
|
+
const host = options.host || currentDb.host;
|
|
44
|
+
const port = options.port || currentDb.port;
|
|
45
|
+
const user = options.user || currentDb.user;
|
|
46
|
+
const password = options.password || currentDb.password;
|
|
47
|
+
const database = options.database || currentDb.database;
|
|
48
|
+
const schema = options.schema || currentDb.schema;
|
|
49
|
+
const ssl = options.ssl !== undefined ? options.ssl : currentDb.ssl;
|
|
50
|
+
const geminiApiKey = options.geminiApiKey || currentConfig.geminiApiKey;
|
|
51
|
+
const sqlFolder = options.sqlFolder || currentConfig.sqlFolder;
|
|
52
|
+
|
|
53
|
+
const hasRequiredOptions =
|
|
54
|
+
host &&
|
|
55
|
+
port &&
|
|
56
|
+
user &&
|
|
57
|
+
password !== undefined &&
|
|
58
|
+
database &&
|
|
59
|
+
geminiApiKey &&
|
|
60
|
+
sqlFolder;
|
|
113
61
|
|
|
114
|
-
|
|
115
|
-
|
|
62
|
+
let projectConfig;
|
|
63
|
+
|
|
64
|
+
if (hasRequiredOptions) {
|
|
65
|
+
console.log(chalk.gray('Configuração fornecida via opções de linha de comando. Ignorando prompts interativos.'));
|
|
66
|
+
const resolved = path.resolve(process.cwd(), sqlFolder);
|
|
67
|
+
if (!fs.existsSync(resolved)) {
|
|
68
|
+
console.error(chalk.red(`\nO diretório não existe: ${resolved}`));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
projectConfig = {
|
|
73
|
+
dbType: 'postgres',
|
|
116
74
|
dbConfig: {
|
|
117
|
-
host
|
|
118
|
-
port: parseInt(
|
|
119
|
-
user
|
|
120
|
-
password
|
|
121
|
-
database
|
|
122
|
-
|
|
75
|
+
host,
|
|
76
|
+
port: parseInt(port, 10),
|
|
77
|
+
user,
|
|
78
|
+
password,
|
|
79
|
+
database,
|
|
80
|
+
schema: schema || 'public',
|
|
81
|
+
ssl: !!ssl
|
|
123
82
|
},
|
|
124
|
-
geminiApiKey
|
|
125
|
-
sqlFolder
|
|
83
|
+
geminiApiKey,
|
|
84
|
+
sqlFolder
|
|
126
85
|
};
|
|
86
|
+
} else {
|
|
87
|
+
try {
|
|
88
|
+
const answers = await inquirer.prompt([
|
|
89
|
+
{
|
|
90
|
+
type: 'input',
|
|
91
|
+
name: 'host',
|
|
92
|
+
message: 'Host do banco de dados:',
|
|
93
|
+
default: () => host || 'localhost'
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
type: 'input',
|
|
97
|
+
name: 'port',
|
|
98
|
+
message: 'Porta do banco de dados:',
|
|
99
|
+
default: () => port?.toString() || '5432'
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
type: 'input',
|
|
103
|
+
name: 'user',
|
|
104
|
+
message: 'Usuário do banco de dados:',
|
|
105
|
+
default: () => user || 'postgres'
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
type: 'password',
|
|
109
|
+
name: 'password',
|
|
110
|
+
message: 'Senha do banco de dados:',
|
|
111
|
+
mask: '*',
|
|
112
|
+
default: password || ''
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
type: 'input',
|
|
116
|
+
name: 'database',
|
|
117
|
+
message: 'Nome do banco de dados:',
|
|
118
|
+
default: database || '',
|
|
119
|
+
validate: input => input.trim() !== '' ? true : 'O nome do banco de dados é obrigatório'
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
type: 'input',
|
|
123
|
+
name: 'schema',
|
|
124
|
+
message: 'Schema do banco de dados:',
|
|
125
|
+
default: schema || 'public'
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
type: 'confirm',
|
|
129
|
+
name: 'ssl',
|
|
130
|
+
message: 'Habilitar SSL (rejectUnauthorized=false)?',
|
|
131
|
+
default: ssl || false
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
type: 'password',
|
|
135
|
+
name: 'geminiApiKey',
|
|
136
|
+
message: 'Chave de API do Gemini:',
|
|
137
|
+
mask: '*',
|
|
138
|
+
default: geminiApiKey || '',
|
|
139
|
+
validate: input => input.trim() !== '' ? true : 'A chave de API do Gemini é obrigatória'
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
type: 'input',
|
|
143
|
+
name: 'sqlFolder',
|
|
144
|
+
message: 'Caminho da pasta com os arquivos SQL:',
|
|
145
|
+
default: sqlFolder || './',
|
|
146
|
+
validate: input => {
|
|
147
|
+
const resolved = path.resolve(process.cwd(), input);
|
|
148
|
+
if (!fs.existsSync(resolved)) {
|
|
149
|
+
return `O diretório não existe: ${resolved}`;
|
|
150
|
+
}
|
|
151
|
+
if (!fs.statSync(resolved).isDirectory()) {
|
|
152
|
+
return `O caminho não é um diretório: ${resolved}`;
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
projectConfig = {
|
|
160
|
+
dbType: 'postgres',
|
|
161
|
+
dbConfig: {
|
|
162
|
+
host: answers.host,
|
|
163
|
+
port: parseInt(answers.port, 10),
|
|
164
|
+
user: answers.user,
|
|
165
|
+
password: answers.password,
|
|
166
|
+
database: answers.database,
|
|
167
|
+
schema: answers.schema || 'public',
|
|
168
|
+
ssl: answers.ssl || false
|
|
169
|
+
},
|
|
170
|
+
geminiApiKey: answers.geminiApiKey,
|
|
171
|
+
sqlFolder: answers.sqlFolder
|
|
172
|
+
};
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error(chalk.red('\nFalha nos prompts de configuração:'), error.message);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
127
178
|
|
|
179
|
+
try {
|
|
128
180
|
saveConfig(projectConfig);
|
|
129
|
-
console.log(chalk.bold.green('\
|
|
181
|
+
console.log(chalk.bold.green('\nConfiguração salva com sucesso!\n'));
|
|
130
182
|
} catch (error) {
|
|
131
|
-
console.error(chalk.red('\
|
|
183
|
+
console.error(chalk.red('\nFalha ao salvar a configuração:'), error.message);
|
|
132
184
|
}
|
|
133
185
|
});
|
|
134
186
|
|
|
135
187
|
// Sync Command
|
|
136
188
|
program
|
|
137
189
|
.command('sync')
|
|
138
|
-
.description('
|
|
190
|
+
.description('Sincroniza arquivo(s) SQL com a estrutura atual do banco de dados')
|
|
139
191
|
.action(async () => {
|
|
140
192
|
const config = getConfig();
|
|
141
193
|
if (!config) {
|
|
142
|
-
console.log(chalk.bold.red('\
|
|
143
|
-
console.log(`
|
|
194
|
+
console.log(chalk.bold.red('\nNenhuma configuração encontrada para esta pasta.'));
|
|
195
|
+
console.log(`Por favor, execute o comando de login primeiro:\n ${chalk.cyan('npx banco-sync-cli login')}\n`);
|
|
144
196
|
process.exit(1);
|
|
145
197
|
}
|
|
146
198
|
|
|
147
199
|
const absoluteSqlFolder = path.resolve(process.cwd(), config.sqlFolder);
|
|
148
200
|
if (!fs.existsSync(absoluteSqlFolder)) {
|
|
149
|
-
console.error(chalk.red(`\
|
|
201
|
+
console.error(chalk.red(`\nPasta de arquivos SQL não encontrada em: ${absoluteSqlFolder}`));
|
|
150
202
|
process.exit(1);
|
|
151
203
|
}
|
|
152
204
|
|
|
@@ -156,12 +208,12 @@ program
|
|
|
156
208
|
sqlFiles = fs.readdirSync(absoluteSqlFolder)
|
|
157
209
|
.filter(file => file.toLowerCase().endsWith('.sql'));
|
|
158
210
|
} catch (err) {
|
|
159
|
-
console.error(chalk.red(`\
|
|
211
|
+
console.error(chalk.red(`\nErro ao ler o diretório: ${err.message}`));
|
|
160
212
|
process.exit(1);
|
|
161
213
|
}
|
|
162
214
|
|
|
163
215
|
if (sqlFiles.length === 0) {
|
|
164
|
-
console.log(chalk.yellow(`\
|
|
216
|
+
console.log(chalk.yellow(`\nNenhum arquivo .sql encontrado na pasta: ${absoluteSqlFolder}\n`));
|
|
165
217
|
process.exit(0);
|
|
166
218
|
}
|
|
167
219
|
|
|
@@ -171,10 +223,10 @@ program
|
|
|
171
223
|
{
|
|
172
224
|
type: 'list',
|
|
173
225
|
name: 'syncScope',
|
|
174
|
-
message: '
|
|
226
|
+
message: 'O que você deseja sincronizar?',
|
|
175
227
|
choices: [
|
|
176
|
-
{ name: '
|
|
177
|
-
{ name: '
|
|
228
|
+
{ name: 'Todos os arquivos SQL da pasta', value: 'all' },
|
|
229
|
+
{ name: 'Selecionar um arquivo SQL específico', value: 'single' }
|
|
178
230
|
]
|
|
179
231
|
}
|
|
180
232
|
]);
|
|
@@ -187,7 +239,7 @@ program
|
|
|
187
239
|
{
|
|
188
240
|
type: 'list',
|
|
189
241
|
name: 'selectedFile',
|
|
190
|
-
message: '
|
|
242
|
+
message: 'Selecione o arquivo SQL para sincronizar:',
|
|
191
243
|
choices: sqlFiles
|
|
192
244
|
}
|
|
193
245
|
]);
|
|
@@ -195,13 +247,13 @@ program
|
|
|
195
247
|
}
|
|
196
248
|
|
|
197
249
|
// 2. Connect to database and fetch schema
|
|
198
|
-
const dbSpinner = ora('
|
|
250
|
+
const dbSpinner = ora('Conectando ao banco de dados e obtendo estrutura...').start();
|
|
199
251
|
let schema;
|
|
200
252
|
try {
|
|
201
253
|
schema = await fetchSchema(config.dbType, config.dbConfig);
|
|
202
|
-
dbSpinner.succeed('
|
|
254
|
+
dbSpinner.succeed('Estrutura do banco de dados obtida com sucesso!');
|
|
203
255
|
} catch (err) {
|
|
204
|
-
dbSpinner.fail(`
|
|
256
|
+
dbSpinner.fail(`Falha ao obter estrutura do banco de dados: ${err.message}`);
|
|
205
257
|
process.exit(1);
|
|
206
258
|
}
|
|
207
259
|
|
|
@@ -210,7 +262,7 @@ program
|
|
|
210
262
|
const filePath = path.join(absoluteSqlFolder, fileName);
|
|
211
263
|
const originalContent = fs.readFileSync(filePath, 'utf-8');
|
|
212
264
|
|
|
213
|
-
const geminiSpinner = ora(`
|
|
265
|
+
const geminiSpinner = ora(`Analisando e comparando ${chalk.bold(fileName)} com o Gemini...`).start();
|
|
214
266
|
|
|
215
267
|
try {
|
|
216
268
|
const result = await syncSqlWithGemini(
|
|
@@ -220,7 +272,7 @@ program
|
|
|
220
272
|
fileName
|
|
221
273
|
);
|
|
222
274
|
|
|
223
|
-
geminiSpinner.succeed(`
|
|
275
|
+
geminiSpinner.succeed(`Análise concluída para ${fileName}`);
|
|
224
276
|
|
|
225
277
|
const { updatedSql, changes } = result;
|
|
226
278
|
|
|
@@ -232,7 +284,7 @@ program
|
|
|
232
284
|
}
|
|
233
285
|
|
|
234
286
|
// Print changes summary
|
|
235
|
-
console.log(chalk.bold('
|
|
287
|
+
console.log(chalk.bold('Resumo das alterações propostas:'));
|
|
236
288
|
changes.forEach(change => console.log(` - ${change}`));
|
|
237
289
|
console.log();
|
|
238
290
|
|
|
@@ -241,28 +293,28 @@ program
|
|
|
241
293
|
{
|
|
242
294
|
type: 'confirm',
|
|
243
295
|
name: 'confirmSave',
|
|
244
|
-
message: `
|
|
296
|
+
message: `Deseja aplicar estas alterações no arquivo ${chalk.cyan(fileName)}?`,
|
|
245
297
|
default: true
|
|
246
298
|
}
|
|
247
299
|
]);
|
|
248
300
|
|
|
249
301
|
if (confirmSave) {
|
|
250
302
|
fs.writeFileSync(filePath, updatedSql, 'utf-8');
|
|
251
|
-
console.log(chalk.green(`\
|
|
303
|
+
console.log(chalk.green(`\nAlterações salvas com sucesso em ${fileName}!\n`));
|
|
252
304
|
} else {
|
|
253
|
-
console.log(chalk.yellow(`\
|
|
305
|
+
console.log(chalk.yellow(`\nAlterações ignoradas para ${fileName}.\n`));
|
|
254
306
|
}
|
|
255
307
|
|
|
256
308
|
} catch (err) {
|
|
257
|
-
geminiSpinner.fail(`
|
|
309
|
+
geminiSpinner.fail(`Falha ao sincronizar ${fileName}: ${err.message}`);
|
|
258
310
|
console.error(err);
|
|
259
311
|
}
|
|
260
312
|
}
|
|
261
313
|
|
|
262
|
-
console.log(chalk.bold.green('
|
|
314
|
+
console.log(chalk.bold.green('Processo de sincronização concluído!\n'));
|
|
263
315
|
|
|
264
316
|
} catch (err) {
|
|
265
|
-
console.error(chalk.red('\
|
|
317
|
+
console.error(chalk.red('\nO comando de sincronização falhou:'), err.message);
|
|
266
318
|
}
|
|
267
319
|
});
|
|
268
320
|
|
package/lib/config.js
CHANGED
|
@@ -24,7 +24,7 @@ function loadStore() {
|
|
|
24
24
|
const data = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
25
25
|
return JSON.parse(data);
|
|
26
26
|
} catch (error) {
|
|
27
|
-
console.error('
|
|
27
|
+
console.error('Erro ao ler o arquivo de configuração:', error.message);
|
|
28
28
|
return {};
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -38,7 +38,7 @@ function saveStore(store) {
|
|
|
38
38
|
try {
|
|
39
39
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(store, null, 2), 'utf-8');
|
|
40
40
|
} catch (error) {
|
|
41
|
-
console.error('
|
|
41
|
+
console.error('Erro ao gravar o arquivo de configuração:', error.message);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
package/lib/db.js
CHANGED
|
@@ -13,7 +13,7 @@ export async function fetchSchema(dbType, config) {
|
|
|
13
13
|
} else if (dbType === 'mysql') {
|
|
14
14
|
return fetchMysqlSchema(config);
|
|
15
15
|
} else {
|
|
16
|
-
throw new Error(`
|
|
16
|
+
throw new Error(`Tipo de banco de dados não suportado: ${dbType}`);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -46,11 +46,11 @@ async function fetchPostgresSchema(config) {
|
|
|
46
46
|
FROM
|
|
47
47
|
information_schema.columns
|
|
48
48
|
WHERE
|
|
49
|
-
table_schema =
|
|
49
|
+
table_schema = $1
|
|
50
50
|
ORDER BY
|
|
51
51
|
table_name, ordinal_position;
|
|
52
52
|
`;
|
|
53
|
-
const colsResult = await client.query(columnsQuery);
|
|
53
|
+
const colsResult = await client.query(columnsQuery, [config.schema || 'public']);
|
|
54
54
|
|
|
55
55
|
// 2. Get primary keys
|
|
56
56
|
const pkQuery = `
|
|
@@ -63,10 +63,10 @@ async function fetchPostgresSchema(config) {
|
|
|
63
63
|
ON tc.constraint_name = kcu.constraint_name
|
|
64
64
|
AND tc.table_schema = kcu.table_schema
|
|
65
65
|
WHERE
|
|
66
|
-
tc.table_schema =
|
|
66
|
+
tc.table_schema = $1
|
|
67
67
|
AND tc.constraint_type = 'PRIMARY KEY';
|
|
68
68
|
`;
|
|
69
|
-
const pkResult = await client.query(pkQuery);
|
|
69
|
+
const pkResult = await client.query(pkQuery, [config.schema || 'public']);
|
|
70
70
|
|
|
71
71
|
// Create a set of primary keys for lookup "tableName.columnName"
|
|
72
72
|
const primaryKeys = new Set(
|
package/lib/diff.js
CHANGED
|
@@ -12,11 +12,11 @@ export function printDiff(filename, oldStr, newStr) {
|
|
|
12
12
|
const patch = structuredPatch(filename, filename, oldStr, newStr, '', '', { context: 3 });
|
|
13
13
|
|
|
14
14
|
if (patch.hunks.length === 0) {
|
|
15
|
-
console.log(chalk.gray(`
|
|
15
|
+
console.log(chalk.gray(`Nenhuma alteração detectada para ${filename}.`));
|
|
16
16
|
return false;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
console.log(chalk.bold(`\nDiff
|
|
19
|
+
console.log(chalk.bold(`\nDiff para ${chalk.cyan(filename)}:`));
|
|
20
20
|
console.log(chalk.gray('--------------------------------------------------'));
|
|
21
21
|
|
|
22
22
|
for (const hunk of patch.hunks) {
|
package/lib/gemini.js
CHANGED
|
@@ -13,36 +13,36 @@ import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
|
13
13
|
export async function syncSqlWithGemini(apiKey, dbSchema, sqlContent, fileName) {
|
|
14
14
|
const genAI = new GoogleGenerativeAI(apiKey);
|
|
15
15
|
|
|
16
|
-
// Using gemini-1.5-flash as it is fast, highly accurate, and supports structured JSON outputs
|
|
17
16
|
const model = genAI.getGenerativeModel({
|
|
18
|
-
model: 'gemini-
|
|
19
|
-
systemInstruction: `
|
|
17
|
+
model: 'gemini-2.5-flash',
|
|
18
|
+
systemInstruction: `Você é um engenheiro de SQL especialista. Sua tarefa é comparar um arquivo SQL fornecido contendo definições de tabelas de banco de dados (como configurações de esquema, migrações ou scripts DDL) com o esquema real do banco de dados (fornecido como uma representação JSON de tabelas, colunas, tipos, valores padrão e restrições atuais).
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
Você deve modificar o arquivo SQL para que as definições de quaisquer tabelas, colunas e propriedades no arquivo correspondam ao estado real do banco de dados.
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
1.
|
|
25
|
-
2.
|
|
26
|
-
3.
|
|
27
|
-
4.
|
|
28
|
-
5.
|
|
29
|
-
6.
|
|
22
|
+
Regras:
|
|
23
|
+
1. Modifique APENAS as estruturas (tabelas, colunas, tipos de dados, restrições, valores padrão) que já estão definidas ou referenciadas no arquivo SQL. Não adicione tabelas completamente novas ao arquivo que já não estejam presentes lá, a menos que o arquivo SQL esteja completamente vazio.
|
|
24
|
+
2. Se uma tabela definida no arquivo SQL possuir colunas no esquema real do banco de dados que estão ausentes no arquivo SQL, adicione essas colunas à definição da tabela no arquivo SQL.
|
|
25
|
+
3. Se uma coluna no arquivo SQL tiver um tipo de dados, nulidade ou valor padrão diferente do esquema do banco de dados, atualize-a para corresponder ao esquema do banco de dados.
|
|
26
|
+
4. Mantenha o estilo original, sintaxe, capitalização, formatação, comentários e estrutura do arquivo SQL o máximo possível. Altere apenas as definições de coluna ou restrições que precisam de atualização.
|
|
27
|
+
5. Retorne o código SQL atualizado completo na resposta JSON sob a chave 'updatedSql'.
|
|
28
|
+
6. Não inclua formatação de bloco de código markdown (como \`\`\`sql) dentro dos valores de string retornados no JSON. Retorne texto puro.
|
|
29
|
+
7. O resumo das alterações ('changes') DEVE ser gerado em português do Brasil.`
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
const prompt = `
|
|
33
|
-
|
|
33
|
+
Nome do Arquivo SQL: ${fileName}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
Conteúdo Atual do Arquivo SQL:
|
|
36
36
|
---
|
|
37
37
|
${sqlContent}
|
|
38
38
|
---
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
Esquema Real do Banco de Dados (JSON):
|
|
41
41
|
---
|
|
42
42
|
${JSON.stringify(dbSchema, null, 2)}
|
|
43
43
|
---
|
|
44
44
|
|
|
45
|
-
Compare
|
|
45
|
+
Compare o conteúdo do arquivo SQL com o esquema real do banco de dados. Gere o conteúdo atualizado do arquivo SQL para alinhá-lo com o esquema do banco de dados e liste as alterações feitas.
|
|
46
46
|
`;
|
|
47
47
|
|
|
48
48
|
const generationConfig = {
|
|
@@ -52,14 +52,14 @@ Compare the SQL file content with the actual database schema. Generate the updat
|
|
|
52
52
|
properties: {
|
|
53
53
|
updatedSql: {
|
|
54
54
|
type: 'string',
|
|
55
|
-
description: '
|
|
55
|
+
description: 'O conteúdo do arquivo SQL atualizado completo, mantendo todo o estilo e formatação originais, mas com os ajustes estruturais para corresponder ao esquema do banco de dados.'
|
|
56
56
|
},
|
|
57
57
|
changes: {
|
|
58
58
|
type: 'array',
|
|
59
59
|
items: {
|
|
60
60
|
type: 'string'
|
|
61
61
|
},
|
|
62
|
-
description: '
|
|
62
|
+
description: 'Uma lista de frases curtas em português descrevendo as modificações realizadas (ex: "Adicionada coluna idade à tabela usuarios", "Alterado tipo da coluna email para varchar(255)").'
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
65
|
required: ['updatedSql', 'changes']
|
|
@@ -77,6 +77,6 @@ Compare the SQL file content with the actual database schema. Generate the updat
|
|
|
77
77
|
|
|
78
78
|
return JSON.parse(responseText);
|
|
79
79
|
} catch (error) {
|
|
80
|
-
throw new Error(`
|
|
80
|
+
throw new Error(`Falha na chamada da API do Gemini: ${error.message}`);
|
|
81
81
|
}
|
|
82
82
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "banco-sync-cli",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "CLI
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Ferramenta CLI para sincronizar arquivos SQL com a estrutura do banco usando a API do Gemini",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/index.js",
|
|
7
7
|
"bin": {
|