smoonb 0.0.7 → 0.0.8
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 +164 -169
- package/bin/smoonb.js +7 -23
- package/package.json +1 -1
- package/src/commands/backup.js +140 -249
- package/src/commands/check.js +209 -349
- package/src/commands/functions.js +123 -349
- package/src/commands/restore.js +122 -294
- package/src/index.js +12 -21
- package/src/services/introspect.js +299 -0
- package/src/utils/cli.js +87 -0
- package/src/utils/config.js +140 -0
- package/src/utils/fsx.js +110 -0
- package/src/utils/hash.js +40 -0
- package/src/commands/secrets.js +0 -361
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
const { createClient } = require('@supabase/supabase-js');
|
|
2
|
+
const { runCommand } = require('./cli');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Serviço de introspecção do banco de dados Supabase
|
|
6
|
+
*/
|
|
7
|
+
class IntrospectionService {
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.config = config;
|
|
10
|
+
this.supabase = createClient(config.supabase.url, config.supabase.serviceKey);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Executa query SQL no banco
|
|
15
|
+
* @param {string} query - Query SQL
|
|
16
|
+
* @returns {Promise<any[]>} - Resultado da query
|
|
17
|
+
*/
|
|
18
|
+
async executeQuery(query) {
|
|
19
|
+
const { data, error } = await this.supabase.rpc('exec_sql', { sql: query });
|
|
20
|
+
if (error) {
|
|
21
|
+
throw new Error(`Erro na query: ${error.message}`);
|
|
22
|
+
}
|
|
23
|
+
return data || [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Obtém inventário de extensões
|
|
28
|
+
* @returns {Promise<object>} - Lista de extensões
|
|
29
|
+
*/
|
|
30
|
+
async getExtensions() {
|
|
31
|
+
try {
|
|
32
|
+
const query = `SELECT extname, extversion FROM pg_extension ORDER BY extname;`;
|
|
33
|
+
const result = await this.executeQuery(query);
|
|
34
|
+
return {
|
|
35
|
+
extensions: result.map(row => ({
|
|
36
|
+
name: row.extname,
|
|
37
|
+
version: row.extversion
|
|
38
|
+
}))
|
|
39
|
+
};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.warn('⚠️ Não foi possível obter extensões:', error.message);
|
|
42
|
+
return { extensions: [] };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Obtém inventário de tabelas
|
|
48
|
+
* @returns {Promise<object>} - Lista de tabelas
|
|
49
|
+
*/
|
|
50
|
+
async getTables() {
|
|
51
|
+
try {
|
|
52
|
+
const query = `
|
|
53
|
+
SELECT schemaname, tablename, tableowner
|
|
54
|
+
FROM pg_tables
|
|
55
|
+
WHERE schemaname IN ('public', 'auth', 'storage')
|
|
56
|
+
ORDER BY schemaname, tablename;
|
|
57
|
+
`;
|
|
58
|
+
const result = await this.executeQuery(query);
|
|
59
|
+
return {
|
|
60
|
+
tables: result.map(row => ({
|
|
61
|
+
schema: row.schemaname,
|
|
62
|
+
name: row.tablename,
|
|
63
|
+
owner: row.tableowner
|
|
64
|
+
}))
|
|
65
|
+
};
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.warn('⚠️ Não foi possível obter tabelas:', error.message);
|
|
68
|
+
return { tables: [] };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Obtém inventário de políticas RLS
|
|
74
|
+
* @returns {Promise<object>} - Lista de políticas
|
|
75
|
+
*/
|
|
76
|
+
async getPolicies() {
|
|
77
|
+
try {
|
|
78
|
+
const query = `
|
|
79
|
+
SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual, with_check
|
|
80
|
+
FROM pg_policies
|
|
81
|
+
ORDER BY schemaname, tablename, policyname;
|
|
82
|
+
`;
|
|
83
|
+
const result = await this.executeQuery(query);
|
|
84
|
+
return {
|
|
85
|
+
policies: result.map(row => ({
|
|
86
|
+
schema: row.schemaname,
|
|
87
|
+
table: row.tablename,
|
|
88
|
+
name: row.policyname,
|
|
89
|
+
permissive: row.permissive,
|
|
90
|
+
roles: row.roles,
|
|
91
|
+
command: row.cmd,
|
|
92
|
+
qual: row.qual,
|
|
93
|
+
withCheck: row.with_check
|
|
94
|
+
}))
|
|
95
|
+
};
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.warn('⚠️ Não foi possível obter políticas:', error.message);
|
|
98
|
+
return { policies: [] };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Obtém inventário de configurações RLS
|
|
104
|
+
* @returns {Promise<object>} - Status RLS por tabela
|
|
105
|
+
*/
|
|
106
|
+
async getRLSStatus() {
|
|
107
|
+
try {
|
|
108
|
+
const query = `
|
|
109
|
+
SELECT n.nspname as schema_name, c.relname as table_name, c.relrowsecurity, c.relforcerowsecurity
|
|
110
|
+
FROM pg_class c
|
|
111
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
112
|
+
WHERE c.relkind = 'r' AND n.nspname IN ('public', 'auth', 'storage')
|
|
113
|
+
ORDER BY n.nspname, c.relname;
|
|
114
|
+
`;
|
|
115
|
+
const result = await this.executeQuery(query);
|
|
116
|
+
return {
|
|
117
|
+
rlsStatus: result.map(row => ({
|
|
118
|
+
schema: row.schema_name,
|
|
119
|
+
table: row.table_name,
|
|
120
|
+
rowSecurityEnabled: row.relrowsecurity,
|
|
121
|
+
forceRowSecurity: row.relforcerowsecurity
|
|
122
|
+
}))
|
|
123
|
+
};
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.warn('⚠️ Não foi possível obter status RLS:', error.message);
|
|
126
|
+
return { rlsStatus: [] };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Obtém inventário de publicações Realtime
|
|
132
|
+
* @returns {Promise<object>} - Lista de publicações
|
|
133
|
+
*/
|
|
134
|
+
async getRealtimePublications() {
|
|
135
|
+
try {
|
|
136
|
+
const publicationsQuery = `SELECT pubname FROM pg_publication ORDER BY pubname;`;
|
|
137
|
+
const publications = await this.executeQuery(publicationsQuery);
|
|
138
|
+
|
|
139
|
+
const publicationTablesQuery = `
|
|
140
|
+
SELECT p.pubname, c.relname as table_name, n.nspname as schema_name
|
|
141
|
+
FROM pg_publication_tables pt
|
|
142
|
+
JOIN pg_publication p ON p.oid = pt.ptpubid
|
|
143
|
+
JOIN pg_class c ON c.oid = pt.ptrelid
|
|
144
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
145
|
+
ORDER BY p.pubname, n.nspname, c.relname;
|
|
146
|
+
`;
|
|
147
|
+
const publicationTables = await this.executeQuery(publicationTablesQuery);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
publications: publications.map(row => ({
|
|
151
|
+
name: row.pubname,
|
|
152
|
+
tables: publicationTables
|
|
153
|
+
.filter(pt => pt.pubname === row.pubname)
|
|
154
|
+
.map(pt => ({
|
|
155
|
+
schema: pt.schema_name,
|
|
156
|
+
table: pt.table_name
|
|
157
|
+
}))
|
|
158
|
+
}))
|
|
159
|
+
};
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.warn('⚠️ Não foi possível obter publicações Realtime:', error.message);
|
|
162
|
+
return { publications: [] };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Obtém inventário de buckets de Storage
|
|
168
|
+
* @returns {Promise<object>} - Lista de buckets e objetos
|
|
169
|
+
*/
|
|
170
|
+
async getStorageInventory() {
|
|
171
|
+
try {
|
|
172
|
+
const { data: buckets, error: bucketsError } = await this.supabase.storage.listBuckets();
|
|
173
|
+
|
|
174
|
+
if (bucketsError) {
|
|
175
|
+
throw new Error(bucketsError.message);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const inventory = {
|
|
179
|
+
buckets: []
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
for (const bucket of buckets) {
|
|
183
|
+
const bucketInfo = {
|
|
184
|
+
id: bucket.id,
|
|
185
|
+
name: bucket.name,
|
|
186
|
+
public: bucket.public,
|
|
187
|
+
file_size_limit: bucket.file_size_limit,
|
|
188
|
+
allowed_mime_types: bucket.allowed_mime_types,
|
|
189
|
+
objects: []
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
// Listar objetos do bucket (sem baixar conteúdo)
|
|
194
|
+
const { data: objects, error: objectsError } = await this.supabase.storage
|
|
195
|
+
.from(bucket.name)
|
|
196
|
+
.list('', { limit: 1000 });
|
|
197
|
+
|
|
198
|
+
if (!objectsError && objects) {
|
|
199
|
+
bucketInfo.objects = objects.map(obj => ({
|
|
200
|
+
name: obj.name,
|
|
201
|
+
size: obj.metadata?.size,
|
|
202
|
+
last_modified: obj.updated_at,
|
|
203
|
+
content_type: obj.metadata?.mimetype
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.warn(`⚠️ Não foi possível listar objetos do bucket ${bucket.name}:`, error.message);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
inventory.buckets.push(bucketInfo);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return inventory;
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.warn('⚠️ Não foi possível obter inventário de Storage:', error.message);
|
|
216
|
+
return { buckets: [] };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Obtém inventário de Edge Functions
|
|
222
|
+
* @returns {Promise<object>} - Lista de functions
|
|
223
|
+
*/
|
|
224
|
+
async getEdgeFunctions() {
|
|
225
|
+
try {
|
|
226
|
+
const { stdout } = await runCommand('supabase functions list', {
|
|
227
|
+
env: { ...process.env, SUPABASE_ACCESS_TOKEN: this.config.supabase.serviceKey }
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Parse da saída do comando
|
|
231
|
+
const functions = [];
|
|
232
|
+
const lines = stdout.split('\n');
|
|
233
|
+
|
|
234
|
+
for (const line of lines) {
|
|
235
|
+
const trimmed = line.trim();
|
|
236
|
+
if (trimmed && !trimmed.startsWith('NAME') && !trimmed.startsWith('-')) {
|
|
237
|
+
const parts = trimmed.split(/\s+/);
|
|
238
|
+
if (parts.length >= 2) {
|
|
239
|
+
functions.push({
|
|
240
|
+
name: parts[0],
|
|
241
|
+
version: parts[1] || 'unknown',
|
|
242
|
+
status: parts[2] || 'unknown'
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return { functions };
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.warn('⚠️ Não foi possível obter Edge Functions:', error.message);
|
|
251
|
+
return { functions: [] };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Gera inventário completo
|
|
257
|
+
* @returns {Promise<object>} - Inventário completo
|
|
258
|
+
*/
|
|
259
|
+
async generateFullInventory() {
|
|
260
|
+
console.log('🔍 Gerando inventário completo...');
|
|
261
|
+
|
|
262
|
+
const inventory = {
|
|
263
|
+
generated_at: new Date().toISOString(),
|
|
264
|
+
project_id: this.config.supabase.projectId,
|
|
265
|
+
components: {}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// Extensões
|
|
269
|
+
if (this.config.backup.includeRealtime) {
|
|
270
|
+
inventory.components.extensions = await this.getExtensions();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Tabelas
|
|
274
|
+
inventory.components.tables = await this.getTables();
|
|
275
|
+
|
|
276
|
+
// Políticas RLS
|
|
277
|
+
inventory.components.policies = await this.getPolicies();
|
|
278
|
+
inventory.components.rlsStatus = await this.getRLSStatus();
|
|
279
|
+
|
|
280
|
+
// Realtime
|
|
281
|
+
if (this.config.backup.includeRealtime) {
|
|
282
|
+
inventory.components.realtime = await this.getRealtimePublications();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Storage
|
|
286
|
+
if (this.config.backup.includeStorage) {
|
|
287
|
+
inventory.components.storage = await this.getStorageInventory();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Edge Functions
|
|
291
|
+
if (this.config.backup.includeFunctions) {
|
|
292
|
+
inventory.components.functions = await this.getEdgeFunctions();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return inventory;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
module.exports = { IntrospectionService };
|
package/src/utils/cli.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const { promisify } = require('util');
|
|
3
|
+
const exec = promisify(require('child_process').exec);
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detecta se um binário está disponível no PATH
|
|
7
|
+
* @param {string} name - Nome do binário (ex: 'supabase', 'psql')
|
|
8
|
+
* @returns {Promise<string|null>} - Caminho do binário ou null se não encontrado
|
|
9
|
+
*/
|
|
10
|
+
async function ensureBin(name) {
|
|
11
|
+
try {
|
|
12
|
+
const command = process.platform === 'win32' ? `where ${name}` : `which ${name}`;
|
|
13
|
+
const { stdout } = await exec(command);
|
|
14
|
+
const path = stdout.trim().split('\n')[0];
|
|
15
|
+
return path || null;
|
|
16
|
+
} catch (error) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Executa um comando e retorna resultado estruturado
|
|
23
|
+
* @param {string} cmd - Comando principal
|
|
24
|
+
* @param {string[]} args - Argumentos do comando
|
|
25
|
+
* @param {object} opts - Opções adicionais (cwd, env, etc.)
|
|
26
|
+
* @returns {Promise<{code: number, stdout: string, stderr: string}>}
|
|
27
|
+
*/
|
|
28
|
+
async function run(cmd, args = [], opts = {}) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const child = spawn(cmd, args, {
|
|
31
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
32
|
+
...opts
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
let stdout = '';
|
|
36
|
+
let stderr = '';
|
|
37
|
+
|
|
38
|
+
child.stdout.on('data', (data) => {
|
|
39
|
+
stdout += data.toString();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
child.stderr.on('data', (data) => {
|
|
43
|
+
stderr += data.toString();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
child.on('close', (code) => {
|
|
47
|
+
if (code === 0) {
|
|
48
|
+
resolve({ code, stdout, stderr });
|
|
49
|
+
} else {
|
|
50
|
+
const error = new Error(`Comando falhou com código ${code}: ${cmd} ${args.join(' ')}`);
|
|
51
|
+
error.code = code;
|
|
52
|
+
error.stdout = stdout;
|
|
53
|
+
error.stderr = stderr;
|
|
54
|
+
reject(error);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
child.on('error', (error) => {
|
|
59
|
+
reject(new Error(`Erro ao executar comando: ${error.message}`));
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Executa um comando simples (string completa)
|
|
66
|
+
* @param {string} command - Comando completo
|
|
67
|
+
* @param {object} opts - Opções adicionais
|
|
68
|
+
* @returns {Promise<{code: number, stdout: string, stderr: string}>}
|
|
69
|
+
*/
|
|
70
|
+
async function runCommand(command, opts = {}) {
|
|
71
|
+
try {
|
|
72
|
+
const { stdout, stderr } = await exec(command, opts);
|
|
73
|
+
return { code: 0, stdout, stderr };
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
code: error.code || 1,
|
|
77
|
+
stdout: error.stdout || '',
|
|
78
|
+
stderr: error.stderr || error.message
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
ensureBin,
|
|
85
|
+
run,
|
|
86
|
+
runCommand
|
|
87
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Lê configuração do .smoonbrc
|
|
7
|
+
* @returns {Promise<object>} - Configuração carregada com defaults
|
|
8
|
+
*/
|
|
9
|
+
async function readConfig() {
|
|
10
|
+
const configPaths = [
|
|
11
|
+
path.join(process.cwd(), '.smoonbrc'),
|
|
12
|
+
path.join(os.homedir(), '.smoonbrc')
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
let configContent = null;
|
|
16
|
+
let configPath = null;
|
|
17
|
+
|
|
18
|
+
for (const configPathCandidate of configPaths) {
|
|
19
|
+
try {
|
|
20
|
+
if (fs.existsSync(configPathCandidate)) {
|
|
21
|
+
configContent = fs.readFileSync(configPathCandidate, 'utf8');
|
|
22
|
+
configPath = configPathCandidate;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
} catch (error) {
|
|
26
|
+
// Continue para próximo caminho
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!configContent) {
|
|
31
|
+
throw new Error('Arquivo .smoonbrc não encontrado. Execute: npx smoonb config --init');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let config;
|
|
35
|
+
try {
|
|
36
|
+
config = JSON.parse(configContent);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw new Error(`Erro ao parsear .smoonbrc: ${error.message}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Aplicar defaults
|
|
42
|
+
const defaultConfig = {
|
|
43
|
+
backup: {
|
|
44
|
+
includeFunctions: true,
|
|
45
|
+
includeStorage: true,
|
|
46
|
+
includeAuth: true,
|
|
47
|
+
includeRealtime: true,
|
|
48
|
+
outputDir: './backups'
|
|
49
|
+
},
|
|
50
|
+
restore: {
|
|
51
|
+
cleanRestore: true,
|
|
52
|
+
verifyAfterRestore: true
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const mergedConfig = mergeDeep(defaultConfig, config);
|
|
57
|
+
|
|
58
|
+
// Warning para pgDumpPath deprecated
|
|
59
|
+
if (mergedConfig.backup?.pgDumpPath) {
|
|
60
|
+
console.warn('⚠️ backup.pgDumpPath será ignorado na v0.0.8 (usando Supabase CLI).');
|
|
61
|
+
delete mergedConfig.backup.pgDumpPath;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return mergedConfig;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Valida configuração para uma ação específica
|
|
69
|
+
* @param {object} config - Configuração carregada
|
|
70
|
+
* @param {string} action - Ação ('backup', 'restore', 'inventory')
|
|
71
|
+
* @throws {Error} - Se configuração inválida
|
|
72
|
+
*/
|
|
73
|
+
function validateFor(config, action) {
|
|
74
|
+
const errors = [];
|
|
75
|
+
|
|
76
|
+
switch (action) {
|
|
77
|
+
case 'backup':
|
|
78
|
+
case 'restore':
|
|
79
|
+
if (!config.supabase?.databaseUrl) {
|
|
80
|
+
errors.push('supabase.databaseUrl é obrigatório');
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
|
|
84
|
+
case 'inventory':
|
|
85
|
+
if (!config.supabase?.url) {
|
|
86
|
+
errors.push('supabase.url é obrigatório');
|
|
87
|
+
}
|
|
88
|
+
if (!config.supabase?.serviceKey) {
|
|
89
|
+
errors.push('supabase.serviceKey é obrigatório');
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (errors.length > 0) {
|
|
95
|
+
throw new Error(`Configuração inválida para ${action}: ${errors.join(', ')}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Merge profundo de objetos
|
|
101
|
+
* @param {object} target - Objeto destino
|
|
102
|
+
* @param {object} source - Objeto origem
|
|
103
|
+
* @returns {object} - Objeto mesclado
|
|
104
|
+
*/
|
|
105
|
+
function mergeDeep(target, source) {
|
|
106
|
+
const result = { ...target };
|
|
107
|
+
|
|
108
|
+
for (const key in source) {
|
|
109
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
110
|
+
result[key] = mergeDeep(target[key] || {}, source[key]);
|
|
111
|
+
} else {
|
|
112
|
+
result[key] = source[key];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Salva configuração no .smoonbrc
|
|
121
|
+
* @param {object} config - Configuração para salvar
|
|
122
|
+
* @param {string} targetPath - Caminho de destino (opcional)
|
|
123
|
+
*/
|
|
124
|
+
async function saveConfig(config, targetPath = null) {
|
|
125
|
+
const configPath = targetPath || path.join(process.cwd(), '.smoonbrc');
|
|
126
|
+
|
|
127
|
+
// Remover pgDumpPath se existir
|
|
128
|
+
if (config.backup?.pgDumpPath) {
|
|
129
|
+
delete config.backup.pgDumpPath;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const jsonContent = JSON.stringify(config, null, 2);
|
|
133
|
+
await fs.promises.writeFile(configPath, jsonContent, 'utf8');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = {
|
|
137
|
+
readConfig,
|
|
138
|
+
validateFor,
|
|
139
|
+
saveConfig
|
|
140
|
+
};
|
package/src/utils/fsx.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Copia um diretório recursivamente
|
|
6
|
+
* @param {string} src - Diretório origem
|
|
7
|
+
* @param {string} dest - Diretório destino
|
|
8
|
+
* @returns {Promise<void>}
|
|
9
|
+
*/
|
|
10
|
+
async function copyDir(src, dest) {
|
|
11
|
+
try {
|
|
12
|
+
// Node.js 18+ tem fs.promises.cp
|
|
13
|
+
if (fs.promises.cp) {
|
|
14
|
+
await fs.promises.cp(src, dest, { recursive: true });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
} catch (error) {
|
|
18
|
+
// Fallback para fs-extra se disponível
|
|
19
|
+
try {
|
|
20
|
+
const fse = require('fs-extra');
|
|
21
|
+
await fse.copy(src, dest);
|
|
22
|
+
return;
|
|
23
|
+
} catch (fseError) {
|
|
24
|
+
// Fallback manual usando fs nativo
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Fallback manual usando fs nativo
|
|
29
|
+
await copyDirManual(src, dest);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Implementação manual de cópia de diretório
|
|
34
|
+
* @param {string} src - Diretório origem
|
|
35
|
+
* @param {string} dest - Diretório destino
|
|
36
|
+
*/
|
|
37
|
+
async function copyDirManual(src, dest) {
|
|
38
|
+
const stat = await fs.promises.stat(src);
|
|
39
|
+
|
|
40
|
+
if (stat.isDirectory()) {
|
|
41
|
+
await fs.promises.mkdir(dest, { recursive: true });
|
|
42
|
+
const entries = await fs.promises.readdir(src);
|
|
43
|
+
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
const srcPath = path.join(src, entry);
|
|
46
|
+
const destPath = path.join(dest, entry);
|
|
47
|
+
await copyDirManual(srcPath, destPath);
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
await fs.promises.copyFile(src, dest);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Cria diretório se não existir
|
|
56
|
+
* @param {string} dirPath - Caminho do diretório
|
|
57
|
+
* @returns {Promise<void>}
|
|
58
|
+
*/
|
|
59
|
+
async function ensureDir(dirPath) {
|
|
60
|
+
try {
|
|
61
|
+
await fs.promises.mkdir(dirPath, { recursive: true });
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error.code !== 'EEXIST') {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Verifica se um arquivo existe
|
|
71
|
+
* @param {string} filePath - Caminho do arquivo
|
|
72
|
+
* @returns {Promise<boolean>}
|
|
73
|
+
*/
|
|
74
|
+
async function exists(filePath) {
|
|
75
|
+
try {
|
|
76
|
+
await fs.promises.access(filePath);
|
|
77
|
+
return true;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Escreve JSON de forma segura
|
|
85
|
+
* @param {string} filePath - Caminho do arquivo
|
|
86
|
+
* @param {any} data - Dados para escrever
|
|
87
|
+
* @param {number} spaces - Espaços para indentação (padrão: 2)
|
|
88
|
+
*/
|
|
89
|
+
async function writeJson(filePath, data, spaces = 2) {
|
|
90
|
+
const json = JSON.stringify(data, null, spaces);
|
|
91
|
+
await fs.promises.writeFile(filePath, json, 'utf8');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Lê JSON de forma segura
|
|
96
|
+
* @param {string} filePath - Caminho do arquivo
|
|
97
|
+
* @returns {Promise<any>}
|
|
98
|
+
*/
|
|
99
|
+
async function readJson(filePath) {
|
|
100
|
+
const content = await fs.promises.readFile(filePath, 'utf8');
|
|
101
|
+
return JSON.parse(content);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
copyDir,
|
|
106
|
+
ensureDir,
|
|
107
|
+
exists,
|
|
108
|
+
writeJson,
|
|
109
|
+
readJson
|
|
110
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Calcula SHA256 de um arquivo usando stream
|
|
6
|
+
* @param {string} filePath - Caminho do arquivo
|
|
7
|
+
* @returns {Promise<string>} - Hash SHA256 em hexadecimal
|
|
8
|
+
*/
|
|
9
|
+
async function sha256(filePath) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const hash = crypto.createHash('sha256');
|
|
12
|
+
const stream = fs.createReadStream(filePath);
|
|
13
|
+
|
|
14
|
+
stream.on('data', (data) => {
|
|
15
|
+
hash.update(data);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
stream.on('end', () => {
|
|
19
|
+
resolve(hash.digest('hex'));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
stream.on('error', (error) => {
|
|
23
|
+
reject(error);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Calcula SHA256 de uma string
|
|
30
|
+
* @param {string} data - String para calcular hash
|
|
31
|
+
* @returns {string} - Hash SHA256 em hexadecimal
|
|
32
|
+
*/
|
|
33
|
+
function sha256String(data) {
|
|
34
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
sha256,
|
|
39
|
+
sha256String
|
|
40
|
+
};
|