smoonb 0.0.46 → 0.0.48

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.
Files changed (30) hide show
  1. package/package.json +1 -1
  2. package/src/commands/backup/index.js +290 -0
  3. package/src/commands/backup/steps/00-docker-validation.js +24 -0
  4. package/src/commands/backup/steps/01-database.js +72 -0
  5. package/src/commands/backup/steps/02-database-separated.js +82 -0
  6. package/src/commands/backup/steps/03-database-settings.js +178 -0
  7. package/src/commands/backup/steps/04-auth-settings.js +43 -0
  8. package/src/commands/backup/steps/05-realtime-settings.js +26 -0
  9. package/src/commands/backup/steps/06-storage.js +90 -0
  10. package/src/commands/backup/steps/07-custom-roles.js +39 -0
  11. package/src/commands/backup/steps/08-edge-functions.js +159 -0
  12. package/src/commands/backup/steps/09-supabase-temp.js +48 -0
  13. package/src/commands/backup/steps/10-migrations.js +80 -0
  14. package/src/commands/backup/utils.js +69 -0
  15. package/src/commands/restore/index.js +190 -0
  16. package/src/commands/restore/steps/00-backup-selection.js +38 -0
  17. package/src/commands/restore/steps/01-components-selection.js +84 -0
  18. package/src/commands/restore/steps/02-confirmation.js +19 -0
  19. package/src/commands/restore/steps/03-database.js +81 -0
  20. package/src/commands/restore/steps/04-edge-functions.js +112 -0
  21. package/src/commands/restore/steps/05-auth-settings.js +51 -0
  22. package/src/commands/restore/steps/06-storage.js +58 -0
  23. package/src/commands/restore/steps/07-database-settings.js +65 -0
  24. package/src/commands/restore/steps/08-realtime-settings.js +50 -0
  25. package/src/commands/restore/utils.js +139 -0
  26. package/src/interactive/envMapper.js +37 -23
  27. package/src/utils/fsExtra.js +98 -0
  28. package/src/utils/supabaseLink.js +82 -0
  29. package/src/commands/backup.js +0 -939
  30. package/src/commands/restore.js +0 -786
@@ -1,939 +0,0 @@
1
- const chalk = require('chalk');
2
- const path = require('path');
3
- const fs = require('fs').promises;
4
- const { exec } = require('child_process');
5
- const { promisify } = require('util');
6
- const { ensureDir, writeJson, copyDir } = require('../utils/fsx');
7
- const { sha256 } = require('../utils/hash');
8
- const { readConfig, validateFor } = require('../utils/config');
9
- const { showBetaBanner } = require('../utils/banner');
10
- const { canPerformCompleteBackup, getDockerVersion } = require('../utils/docker');
11
- const { captureRealtimeSettings } = require('../utils/realtime-settings');
12
- const { readEnvFile, writeEnvFile, backupEnvFile } = require('../utils/env');
13
- const { saveEnvMap } = require('../utils/envMap');
14
- const { mapEnvVariablesInteractively, askComponentsFlags } = require('../interactive/envMapper');
15
-
16
- const execAsync = promisify(exec);
17
-
18
- // Exportar FUNÇÃO em vez de objeto Command
19
- module.exports = async (options) => {
20
- showBetaBanner();
21
-
22
- try {
23
- // Consentimento para leitura e escrita do .env.local
24
- console.log(chalk.yellow('⚠️ O smoonb irá ler e escrever o arquivo .env.local localmente.'));
25
- console.log(chalk.yellow(' Um backup automático do .env.local será criado antes de qualquer alteração.'));
26
- const consent = await require('inquirer').prompt([{ type: 'confirm', name: 'ok', message: 'Você consente em prosseguir (S/n):', default: true }]);
27
- if (!consent.ok) {
28
- console.log(chalk.red('🚫 Operação cancelada pelo usuário.'));
29
- process.exit(1);
30
- }
31
-
32
- // Carregar configuração existente apenas para defaults de diretório
33
- const config = await readConfig().catch(() => ({ backup: { outputDir: './backups' }, supabase: {} }));
34
- validateFor(config, 'backup');
35
-
36
- // Validação adicional para pré-requisitos obrigatórios
37
- // Pré-passo de ENV: criar diretório de backup com timestamp já no início
38
- const now = new Date();
39
- const year = now.getFullYear();
40
- const month = String(now.getMonth() + 1).padStart(2, '0');
41
- const day = String(now.getDate()).padStart(2, '0');
42
- const hour = String(now.getHours()).padStart(2, '0');
43
- const minute = String(now.getMinutes()).padStart(2, '0');
44
- const second = String(now.getSeconds()).padStart(2, '0');
45
-
46
- // Resolver diretório de saída (prioriza .env.local mapeado depois, por ora usa default)
47
- const defaultOutput = options.output || config.backup?.outputDir || './backups';
48
- const backupDir = path.join(defaultOutput, `backup-${year}-${month}-${day}-${hour}-${minute}-${second}`);
49
- await ensureDir(backupDir);
50
-
51
- // Backup e mapeamento do .env.local
52
- const envPath = path.join(process.cwd(), '.env.local');
53
- const envBackupPath = path.join(backupDir, 'env', '.env.local');
54
- await ensureDir(path.dirname(envBackupPath));
55
- await backupEnvFile(envPath, envBackupPath);
56
- console.log(chalk.blue(`📁 Backup do .env.local: ${path.relative(process.cwd(), envBackupPath)}`));
57
-
58
- const expectedKeys = [
59
- 'NEXT_PUBLIC_SUPABASE_URL',
60
- 'NEXT_PUBLIC_SUPABASE_ANON_KEY',
61
- 'SUPABASE_SERVICE_ROLE_KEY',
62
- 'SUPABASE_DB_URL',
63
- 'SUPABASE_PROJECT_ID',
64
- 'SUPABASE_ACCESS_TOKEN',
65
- 'SMOONB_OUTPUT_DIR'
66
- ];
67
- const currentEnv = await readEnvFile(envPath);
68
- const { finalEnv, dePara } = await mapEnvVariablesInteractively(currentEnv, expectedKeys);
69
- await writeEnvFile(envPath, finalEnv);
70
- await saveEnvMap(dePara, path.join(backupDir, 'env', 'env-map.json'));
71
- console.log(chalk.green('✅ .env.local atualizado com sucesso. Nenhuma chave renomeada; valores sincronizados.'));
72
-
73
- function getValue(expectedKey) {
74
- const clientKey = Object.keys(dePara).find(k => dePara[k] === expectedKey);
75
- return clientKey ? finalEnv[clientKey] : '';
76
- }
77
-
78
- // Recalcular outputDir a partir do ENV mapeado
79
- const resolvedOutputDir = options.output || getValue('SMOONB_OUTPUT_DIR') || config.backup?.outputDir || './backups';
80
-
81
- // Se mudou o outputDir, movemos o backupDir inicial para o novo local mantendo timestamp
82
- const finalBackupDir = backupDir.startsWith(path.resolve(resolvedOutputDir))
83
- ? backupDir
84
- : path.join(resolvedOutputDir, path.basename(backupDir));
85
- if (finalBackupDir !== backupDir) {
86
- await ensureDir(resolvedOutputDir);
87
- await fs.rename(backupDir, finalBackupDir);
88
- }
89
-
90
- const projectId = getValue('SUPABASE_PROJECT_ID');
91
- const accessToken = getValue('SUPABASE_ACCESS_TOKEN');
92
- const databaseUrl = getValue('SUPABASE_DB_URL');
93
-
94
- if (!databaseUrl) {
95
- console.log(chalk.red('❌ DATABASE_URL NÃO CONFIGURADA'));
96
- console.log('');
97
- console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:'));
98
- console.log(chalk.yellow(' 1. Configurar SUPABASE_DB_URL no .env.local'));
99
- console.log(chalk.yellow(' 2. Repetir o comando de backup'));
100
- console.log('');
101
- console.log(chalk.blue('💡 Exemplo de configuração:'));
102
- console.log(chalk.gray(' "databaseUrl": "postgresql://postgres:[senha]@db.[projeto].supabase.co:5432/postgres"'));
103
- console.log('');
104
- console.log(chalk.red('🚫 Backup cancelado - Configuração incompleta'));
105
- process.exit(1);
106
- }
107
-
108
- if (!accessToken) {
109
- console.log(chalk.red('❌ ACCESS_TOKEN NÃO CONFIGURADO'));
110
- console.log('');
111
- console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:'));
112
- console.log(chalk.yellow(' 1. Obter Personal Access Token do Supabase'));
113
- console.log(chalk.yellow(' 2. Configurar SUPABASE_ACCESS_TOKEN no .env.local'));
114
- console.log(chalk.yellow(' 3. Repetir o comando de backup'));
115
- console.log('');
116
- console.log(chalk.blue('🔗 Como obter o token:'));
117
- console.log(chalk.gray(' 1. Acesse: https://supabase.com/dashboard/account/tokens'));
118
- console.log(chalk.gray(' 2. Clique: "Generate new token"'));
119
- console.log(chalk.gray(' 3. Copie o token (formato: sbp_...)'));
120
- console.log('');
121
- console.log(chalk.red('🚫 Backup cancelado - Token não configurado'));
122
- process.exit(1);
123
- }
124
-
125
- console.log(chalk.blue(`🚀 Iniciando backup do projeto: ${projectId}`));
126
- console.log(chalk.gray(`🔍 Verificando dependências Docker...`));
127
-
128
- // Verificar se é possível fazer backup completo via Docker
129
- const backupCapability = await canPerformCompleteBackup();
130
-
131
- if (backupCapability.canBackupComplete) {
132
- console.log(chalk.green('✅ Docker Desktop detectado e funcionando'));
133
- console.log(chalk.gray(`🐳 Versão: ${backupCapability.dockerStatus.version}`));
134
- console.log('');
135
-
136
- // Proceder com backup completo via Docker
137
- // Flags de componentes (não afetam Database)
138
- const flags = await askComponentsFlags();
139
- return await performFullBackup({ projectId, accessToken, databaseUrl }, { ...options, flags, backupDir: finalBackupDir, outputDir: resolvedOutputDir });
140
- } else {
141
- // Mostrar mensagens educativas e encerrar elegantemente
142
- showDockerMessagesAndExit(backupCapability.reason);
143
- }
144
-
145
- } catch (error) {
146
- console.error(chalk.red(`❌ Erro no backup: ${error.message}`));
147
- process.exit(1);
148
- }
149
- };
150
-
151
- // Função para backup completo via Docker
152
- async function performFullBackup(envCfg, options) {
153
- const { projectId, accessToken, databaseUrl } = envCfg;
154
- const outputDir = options.outputDir;
155
- const backupDir = options.backupDir;
156
-
157
- console.log(chalk.blue(`📁 Diretório: ${backupDir}`));
158
- console.log(chalk.gray(`🐳 Backup via Docker Desktop`));
159
-
160
- const manifest = {
161
- created_at: new Date().toISOString(),
162
- project_id: projectId,
163
- smoonb_version: require('../../package.json').version,
164
- backup_type: 'pg_dumpall_docker_dashboard_compatible',
165
- docker_version: await getDockerVersion(),
166
- dashboard_compatible: true,
167
- components: {}
168
- };
169
-
170
- // 1. Backup Database via pg_dumpall Docker (idêntico ao Dashboard)
171
- console.log(chalk.blue('\n📊 1/8 - Backup da Database PostgreSQL via pg_dumpall Docker...'));
172
- const databaseResult = await backupDatabase(databaseUrl, backupDir);
173
- manifest.components.database = databaseResult;
174
-
175
- // 1.5. Backup Database Separado (SQL files para troubleshooting)
176
- console.log(chalk.blue('\n📊 1.5/8 - Backup da Database PostgreSQL (arquivos SQL separados)...'));
177
- const dbSeparatedResult = await backupDatabaseSeparated(databaseUrl, backupDir, accessToken);
178
- manifest.components.database_separated = {
179
- success: dbSeparatedResult.success,
180
- method: 'supabase-cli',
181
- files: dbSeparatedResult.files || [],
182
- total_size_kb: dbSeparatedResult.totalSizeKB || '0.0'
183
- };
184
-
185
- // 2. Backup Edge Functions via Docker
186
- if (options.flags?.includeFunctions) {
187
- console.log(chalk.blue('\n⚡ 2/8 - Backup das Edge Functions via Docker...'));
188
- const functionsResult = await backupEdgeFunctionsWithDocker(projectId, accessToken, backupDir);
189
- manifest.components.edge_functions = functionsResult;
190
- }
191
-
192
- // 3. Backup Auth Settings via API
193
- if (options.flags?.includeAuth) {
194
- console.log(chalk.blue('\n🔐 3/8 - Backup das Auth Settings via API...'));
195
- const authResult = await backupAuthSettings(projectId, accessToken, backupDir);
196
- manifest.components.auth_settings = authResult;
197
- }
198
-
199
- // 4. Backup Storage via API
200
- if (options.flags?.includeStorage) {
201
- console.log(chalk.blue('\n📦 4/8 - Backup do Storage via API...'));
202
- const storageResult = await backupStorage(projectId, accessToken, backupDir);
203
- manifest.components.storage = storageResult;
204
- }
205
-
206
- // 5. Backup Custom Roles via SQL
207
- console.log(chalk.blue('\n👥 5/8 - Backup dos Custom Roles via SQL...'));
208
- const rolesResult = await backupCustomRoles(databaseUrl, backupDir, accessToken);
209
- manifest.components.custom_roles = rolesResult;
210
-
211
- // 6. Backup das Database Extensions and Settings via SQL
212
- console.log(chalk.blue('\n🔧 6/8 - Backup das Database Extensions and Settings via SQL...'));
213
- const databaseSettingsResult = await backupDatabaseSettings(databaseUrl, projectId, backupDir);
214
- manifest.components.database_settings = databaseSettingsResult;
215
-
216
- // 7. Backup Realtime Settings via Captura Interativa
217
- if (options.flags?.includeRealtime) {
218
- console.log(chalk.blue('\n🔄 7/8 - Backup das Realtime Settings via Captura Interativa...'));
219
- const realtimeResult = await backupRealtimeSettings(projectId, backupDir, options.skipRealtime);
220
- manifest.components.realtime = realtimeResult;
221
- }
222
-
223
- // Salvar manifest
224
- await writeJson(path.join(backupDir, 'backup-manifest.json'), manifest);
225
-
226
- console.log(chalk.green('\n🎉 BACKUP COMPLETO FINALIZADO VIA DOCKER!'));
227
- console.log(chalk.blue(`📁 Localização: ${backupDir}`));
228
- console.log(chalk.green(`📊 Database: ${databaseResult.fileName} (${databaseResult.size} KB) - Idêntico ao Dashboard`));
229
- console.log(chalk.green(`📊 Database SQL: ${dbSeparatedResult.files?.length || 0} arquivos separados (${dbSeparatedResult.totalSizeKB} KB) - Para troubleshooting`));
230
- console.log(chalk.green(`🔧 Database Settings: ${databaseSettingsResult.fileName} (${databaseSettingsResult.size} KB) - Extensions e Configurações`));
231
- if (options.flags?.includeFunctions && manifest.components.edge_functions) {
232
- const functionsResult = manifest.components.edge_functions;
233
- console.log(chalk.green(`⚡ Edge Functions: ${functionsResult.success_count || 0}/${functionsResult.functions_count || 0} functions baixadas via Docker`));
234
- }
235
- if (options.flags?.includeAuth && manifest.components.auth_settings) {
236
- const authResult = manifest.components.auth_settings;
237
- console.log(chalk.green(`🔐 Auth Settings: ${authResult.success ? 'Exportadas via API' : 'Falharam'}`));
238
- }
239
- if (options.flags?.includeStorage && manifest.components.storage) {
240
- const storageResult = manifest.components.storage;
241
- console.log(chalk.green(`📦 Storage: ${storageResult.buckets?.length || 0} buckets verificados via API`));
242
- }
243
- console.log(chalk.green(`👥 Custom Roles: ${rolesResult.roles?.length || 0} roles exportados via SQL`));
244
- // Determinar mensagem correta baseada no método usado
245
- if (options.flags?.includeRealtime && manifest.components.realtime) {
246
- const realtimeResult = manifest.components.realtime;
247
- let realtimeMessage = 'Falharam';
248
- if (realtimeResult.success) {
249
- if (options.skipRealtime) {
250
- realtimeMessage = 'Configurações copiadas do backup anterior';
251
- } else {
252
- realtimeMessage = 'Configurações capturadas interativamente';
253
- }
254
- }
255
- console.log(chalk.green(`🔄 Realtime: ${realtimeMessage}`));
256
- }
257
-
258
- // report.json
259
- await writeJson(path.join(backupDir, 'report.json'), {
260
- process: 'backup',
261
- created_at: manifest.created_at,
262
- project_id: manifest.project_id,
263
- assets: {
264
- env: path.join(backupDir, 'env', '.env.local'),
265
- env_map: path.join(backupDir, 'env', 'env-map.json'),
266
- manifest: path.join(backupDir, 'backup-manifest.json')
267
- },
268
- components: {
269
- includeFunctions: !!options.flags?.includeFunctions,
270
- includeStorage: !!options.flags?.includeStorage,
271
- includeAuth: !!options.flags?.includeAuth,
272
- includeRealtime: !!options.flags?.includeRealtime
273
- }
274
- });
275
-
276
- return { success: true, backupDir, manifest };
277
- }
278
-
279
- // Função para mostrar mensagens educativas e encerrar elegantemente
280
- function showDockerMessagesAndExit(reason) {
281
- console.log('');
282
-
283
- switch (reason) {
284
- case 'docker_not_installed':
285
- console.log(chalk.red('❌ DOCKER DESKTOP NÃO ENCONTRADO'));
286
- console.log('');
287
- console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:'));
288
- console.log(chalk.yellow(' 1. Instalar Docker Desktop'));
289
- console.log(chalk.yellow(' 2. Executar Docker Desktop'));
290
- console.log(chalk.yellow(' 3. Repetir o comando de backup'));
291
- console.log('');
292
- console.log(chalk.blue('🔗 Download: https://docs.docker.com/desktop/install/'));
293
- console.log('');
294
- console.log(chalk.gray('💡 O Docker Desktop é obrigatório para backup completo do Supabase'));
295
- console.log(chalk.gray(' - Database PostgreSQL'));
296
- console.log(chalk.gray(' - Edge Functions'));
297
- console.log(chalk.gray(' - Todos os componentes via Supabase CLI'));
298
- break;
299
-
300
- case 'docker_not_running':
301
- console.log(chalk.red('❌ DOCKER DESKTOP NÃO ESTÁ EXECUTANDO'));
302
- console.log('');
303
- console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:'));
304
- console.log(chalk.yellow(' 1. Abrir Docker Desktop'));
305
- console.log(chalk.yellow(' 2. Aguardar inicialização completa'));
306
- console.log(chalk.yellow(' 3. Repetir o comando de backup'));
307
- console.log('');
308
- console.log(chalk.blue('💡 Dica: Docker Desktop deve estar rodando em segundo plano'));
309
- console.log('');
310
- console.log(chalk.gray('💡 O Docker Desktop é obrigatório para backup completo do Supabase'));
311
- console.log(chalk.gray(' - Database PostgreSQL'));
312
- console.log(chalk.gray(' - Edge Functions'));
313
- console.log(chalk.gray(' - Todos os componentes via Supabase CLI'));
314
- break;
315
-
316
- case 'supabase_cli_not_found':
317
- console.log(chalk.red('❌ SUPABASE CLI NÃO ENCONTRADO'));
318
- console.log('');
319
- console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:'));
320
- console.log(chalk.yellow(' 1. Instalar Supabase CLI'));
321
- console.log(chalk.yellow(' 2. Repetir o comando de backup'));
322
- console.log('');
323
- console.log(chalk.blue('🔗 Instalação: npm install -g supabase'));
324
- console.log('');
325
- console.log(chalk.gray('💡 O Supabase CLI é obrigatório para backup completo do Supabase'));
326
- console.log(chalk.gray(' - Database PostgreSQL'));
327
- console.log(chalk.gray(' - Edge Functions'));
328
- console.log(chalk.gray(' - Todos os componentes via Docker'));
329
- break;
330
- }
331
-
332
- console.log('');
333
- console.log(chalk.red('🚫 Backup cancelado - Pré-requisitos não atendidos'));
334
- console.log(chalk.gray(' Instale os componentes necessários e tente novamente'));
335
- console.log('');
336
-
337
- process.exit(1);
338
- }
339
-
340
- // Backup da database usando pg_dumpall via Docker (idêntico ao Supabase Dashboard)
341
- async function backupDatabase(databaseUrl, backupDir) {
342
- try {
343
- console.log(chalk.gray(' - Criando backup completo via pg_dumpall...'));
344
-
345
- const { execSync } = require('child_process');
346
-
347
- // Extrair credenciais da databaseUrl
348
- const urlMatch = databaseUrl.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
349
-
350
- if (!urlMatch) {
351
- throw new Error('Database URL inválida');
352
- }
353
-
354
- const [, username, password, host, port, database] = urlMatch;
355
-
356
- // Gerar nome do arquivo igual ao dashboard
357
- const now = new Date();
358
- const day = String(now.getDate()).padStart(2, '0');
359
- const month = String(now.getMonth() + 1).padStart(2, '0');
360
- const year = now.getFullYear();
361
- const hours = String(now.getHours()).padStart(2, '0');
362
- const minutes = String(now.getMinutes()).padStart(2, '0');
363
- const seconds = String(now.getSeconds()).padStart(2, '0');
364
-
365
- const fileName = `db_cluster-${day}-${month}-${year}@${hours}-${minutes}-${seconds}.backup`;
366
-
367
- // CORREÇÃO: Usar caminho absoluto igual às Edge Functions
368
- const backupDirAbs = path.resolve(backupDir);
369
-
370
- // Comando pg_dumpall via Docker (mesma abordagem das Edge Functions)
371
- const dockerCmd = [
372
- 'docker run --rm --network host',
373
- `-v "${backupDirAbs}:/host"`,
374
- `-e PGPASSWORD="${password}"`,
375
- 'postgres:17 pg_dumpall',
376
- `-h ${host}`,
377
- `-p ${port}`,
378
- `-U ${username}`,
379
- `-f /host/${fileName}`
380
- ].join(' ');
381
-
382
- console.log(chalk.gray(` - Executando pg_dumpall via Docker...`));
383
- execSync(dockerCmd, { stdio: 'pipe' });
384
-
385
- // Compactar igual ao Supabase Dashboard
386
- const gzipCmd = [
387
- 'docker run --rm',
388
- `-v "${backupDirAbs}:/host"`,
389
- `postgres:17 gzip /host/${fileName}`
390
- ].join(' ');
391
-
392
- execSync(gzipCmd, { stdio: 'pipe' });
393
-
394
- const finalFileName = `${fileName}.gz`;
395
- const stats = await fs.stat(path.join(backupDir, finalFileName));
396
- const sizeKB = (stats.size / 1024).toFixed(1);
397
-
398
- console.log(chalk.green(` ✅ Database backup: ${finalFileName} (${sizeKB} KB)`));
399
-
400
- return { success: true, size: sizeKB, fileName: finalFileName };
401
- } catch (error) {
402
- console.log(chalk.yellow(` ⚠️ Erro no backup do database: ${error.message}`));
403
- return { success: false };
404
- }
405
- }
406
-
407
- // Backup da database usando arquivos SQL separados via Supabase CLI (para troubleshooting)
408
- async function backupDatabaseSeparated(databaseUrl, backupDir, accessToken) {
409
- try {
410
- console.log(chalk.gray(' - Criando backups SQL separados via Supabase CLI...'));
411
-
412
- const { execSync } = require('child_process');
413
- const dbUrl = databaseUrl;
414
- const files = [];
415
- let totalSizeKB = 0;
416
-
417
- // 1. Backup do Schema
418
- console.log(chalk.gray(' - Exportando schema...'));
419
- const schemaFile = path.join(backupDir, 'schema.sql');
420
-
421
- try {
422
- execSync(`supabase db dump --db-url "${dbUrl}" -f "${schemaFile}"`, { stdio: 'pipe', env: { ...process.env, SUPABASE_ACCESS_TOKEN: accessToken || '' } });
423
- const stats = await fs.stat(schemaFile);
424
- const sizeKB = (stats.size / 1024).toFixed(1);
425
- files.push({ filename: 'schema.sql', sizeKB });
426
- totalSizeKB += parseFloat(sizeKB);
427
- console.log(chalk.green(` ✅ Schema: ${sizeKB} KB`));
428
- } catch (error) {
429
- console.log(chalk.yellow(` ⚠️ Erro no schema: ${error.message}`));
430
- }
431
-
432
- // 2. Backup dos Dados
433
- console.log(chalk.gray(' - Exportando dados...'));
434
- const dataFile = path.join(backupDir, 'data.sql');
435
-
436
- try {
437
- execSync(`supabase db dump --db-url "${dbUrl}" --data-only -f "${dataFile}"`, { stdio: 'pipe', env: { ...process.env, SUPABASE_ACCESS_TOKEN: accessToken || '' } });
438
- const stats = await fs.stat(dataFile);
439
- const sizeKB = (stats.size / 1024).toFixed(1);
440
- files.push({ filename: 'data.sql', sizeKB });
441
- totalSizeKB += parseFloat(sizeKB);
442
- console.log(chalk.green(` ✅ Data: ${sizeKB} KB`));
443
- } catch (error) {
444
- console.log(chalk.yellow(` ⚠️ Erro nos dados: ${error.message}`));
445
- }
446
-
447
- // 3. Backup dos Roles
448
- console.log(chalk.gray(' - Exportando roles...'));
449
- const rolesFile = path.join(backupDir, 'roles.sql');
450
-
451
- try {
452
- execSync(`supabase db dump --db-url "${dbUrl}" --role-only -f "${rolesFile}"`, { stdio: 'pipe', env: { ...process.env, SUPABASE_ACCESS_TOKEN: accessToken || '' } });
453
- const stats = await fs.stat(rolesFile);
454
- const sizeKB = (stats.size / 1024).toFixed(1);
455
- files.push({ filename: 'roles.sql', sizeKB });
456
- totalSizeKB += parseFloat(sizeKB);
457
- console.log(chalk.green(` ✅ Roles: ${sizeKB} KB`));
458
- } catch (error) {
459
- console.log(chalk.yellow(` ⚠️ Erro nos roles: ${error.message}`));
460
- }
461
-
462
- return {
463
- success: files.length > 0,
464
- files,
465
- totalSizeKB: totalSizeKB.toFixed(1)
466
- };
467
-
468
- } catch (error) {
469
- console.log(chalk.yellow(` ⚠️ Erro nos backups SQL separados: ${error.message}`));
470
- return { success: false, files: [], totalSizeKB: '0.0' };
471
- }
472
- }
473
-
474
- // Backup das Edge Functions via Docker
475
- async function backupEdgeFunctionsWithDocker(projectId, accessToken, backupDir) {
476
- try {
477
- const functionsDir = path.join(backupDir, 'edge-functions');
478
- await ensureDir(functionsDir);
479
-
480
- console.log(chalk.gray(' - Listando Edge Functions via Management API...'));
481
-
482
- // ✅ Usar fetch direto para Management API com Personal Access Token
483
- const functionsResponse = await fetch(`https://api.supabase.com/v1/projects/${projectId}/functions`, {
484
- headers: {
485
- 'Authorization': `Bearer ${accessToken}`,
486
- 'Content-Type': 'application/json'
487
- }
488
- });
489
-
490
- if (!functionsResponse.ok) {
491
- console.log(chalk.yellow(` ⚠️ Erro ao listar Edge Functions: ${functionsResponse.status} ${functionsResponse.statusText}`));
492
- return { success: false, reason: 'api_error', functions: [] };
493
- }
494
-
495
- const functions = await functionsResponse.json();
496
-
497
- if (!functions || functions.length === 0) {
498
- console.log(chalk.gray(' - Nenhuma Edge Function encontrada'));
499
- await writeJson(path.join(functionsDir, 'README.md'), {
500
- message: 'Nenhuma Edge Function encontrada neste projeto'
501
- });
502
- return { success: true, reason: 'no_functions', functions: [] };
503
- }
504
-
505
- console.log(chalk.gray(` - Encontradas ${functions.length} Edge Function(s)`));
506
-
507
- const downloadedFunctions = [];
508
- let successCount = 0;
509
- let errorCount = 0;
510
-
511
- // ✅ Baixar cada Edge Function via Supabase CLI
512
- // Nota: O CLI ignora o cwd e sempre baixa para supabase/functions
513
- for (const func of functions) {
514
- try {
515
- console.log(chalk.gray(` - Baixando: ${func.name}...`));
516
-
517
- // Criar diretório da função NO BACKUP
518
- const functionTargetDir = path.join(functionsDir, func.name);
519
- await ensureDir(functionTargetDir);
520
-
521
- // Diretório temporário onde o supabase CLI irá baixar (supabase/functions)
522
- const tempDownloadDir = path.join(process.cwd(), 'supabase', 'functions', func.name);
523
-
524
- // Baixar Edge Function via Supabase CLI (sempre vai para supabase/functions)
525
- const { execSync } = require('child_process');
526
-
527
- execSync(`supabase functions download ${func.name}`, {
528
- timeout: 60000,
529
- stdio: 'pipe'
530
- });
531
-
532
- // ✅ COPIAR arquivos de supabase/functions para o backup
533
- try {
534
- const stat = await fs.stat(tempDownloadDir);
535
- if (stat.isDirectory()) {
536
- const files = await fs.readdir(tempDownloadDir);
537
- for (const file of files) {
538
- const srcPath = path.join(tempDownloadDir, file);
539
- const dstPath = path.join(functionTargetDir, file);
540
-
541
- const fileStats = await fs.stat(srcPath);
542
- if (fileStats.isDirectory()) {
543
- // Copiar diretórios recursivamente
544
- await fs.cp(srcPath, dstPath, { recursive: true });
545
- } else {
546
- // Copiar arquivos
547
- await fs.copyFile(srcPath, dstPath);
548
- }
549
- }
550
- }
551
- } catch (copyError) {
552
- // Arquivos não foram baixados, continuar
553
- console.log(chalk.yellow(` ⚠️ Nenhum arquivo encontrado em ${tempDownloadDir}`));
554
- }
555
-
556
- // ✅ LIMPAR supabase/functions após copiar
557
- try {
558
- await fs.rm(tempDownloadDir, { recursive: true, force: true });
559
- } catch (cleanError) {
560
- // Ignorar erro de limpeza
561
- }
562
-
563
- console.log(chalk.green(` ✅ ${func.name} baixada com sucesso`));
564
- successCount++;
565
-
566
- downloadedFunctions.push({
567
- name: func.name,
568
- slug: func.name,
569
- version: func.version || 'unknown',
570
- files: await fs.readdir(functionTargetDir).catch(() => [])
571
- });
572
-
573
- } catch (error) {
574
- console.log(chalk.yellow(` ⚠️ Erro ao baixar ${func.name}: ${error.message}`));
575
- errorCount++;
576
- }
577
- }
578
-
579
- console.log(chalk.green(`📊 Backup de Edge Functions concluído:`));
580
- console.log(chalk.green(` ✅ Sucessos: ${successCount}`));
581
- console.log(chalk.green(` ❌ Erros: ${errorCount}`));
582
-
583
- return {
584
- success: true,
585
- reason: 'success',
586
- functions: downloadedFunctions,
587
- functions_count: functions.length,
588
- success_count: successCount,
589
- error_count: errorCount,
590
- method: 'docker'
591
- };
592
-
593
- } catch (error) {
594
- console.log(chalk.yellow(` ⚠️ Erro durante backup de Edge Functions: ${error.message}`));
595
- console.log('⏭️ Continuando com outros componentes...');
596
- return { success: false, reason: 'download_error', error: error.message, functions: [] };
597
- }
598
- }
599
-
600
- // Backup das Auth Settings via Management API
601
- async function backupAuthSettings(projectId, accessToken, backupDir) {
602
- try {
603
- console.log(chalk.gray(' - Exportando configurações de Auth via Management API...'));
604
-
605
- // ✅ Usar fetch direto para Management API com Personal Access Token
606
- const authResponse = await fetch(`https://api.supabase.com/v1/projects/${projectId}/config/auth`, {
607
- headers: {
608
- 'Authorization': `Bearer ${accessToken}`,
609
- 'Content-Type': 'application/json'
610
- }
611
- });
612
-
613
- if (!authResponse.ok) {
614
- console.log(chalk.yellow(` ⚠️ Erro ao obter Auth Settings: ${authResponse.status} ${authResponse.statusText}`));
615
- return { success: false };
616
- }
617
-
618
- const authSettings = await authResponse.json();
619
-
620
- // Salvar configurações de Auth
621
- const authSettingsPath = path.join(backupDir, 'auth-settings.json');
622
- await writeJson(authSettingsPath, {
623
- project_id: projectId,
624
- timestamp: new Date().toISOString(),
625
- settings: authSettings
626
- });
627
-
628
- console.log(chalk.green(`✅ Auth Settings exportadas: ${path.basename(authSettingsPath)}`));
629
- return { success: true };
630
-
631
- } catch (error) {
632
- console.log(chalk.yellow(` ⚠️ Erro no backup das Auth Settings: ${error.message}`));
633
- return { success: false };
634
- }
635
- }
636
-
637
- // Backup do Storage via Supabase API
638
- async function backupStorage(projectId, accessToken, backupDir) {
639
- try {
640
- const storageDir = path.join(backupDir, 'storage');
641
- await ensureDir(storageDir);
642
-
643
- console.log(chalk.gray(' - Listando buckets de Storage via Management API...'));
644
-
645
- // ✅ Usar fetch direto para Management API com Personal Access Token
646
- const storageResponse = await fetch(`https://api.supabase.com/v1/projects/${projectId}/storage/buckets`, {
647
- headers: {
648
- 'Authorization': `Bearer ${accessToken}`,
649
- 'Content-Type': 'application/json'
650
- }
651
- });
652
-
653
- if (!storageResponse.ok) {
654
- console.log(chalk.yellow(` ⚠️ Erro ao listar buckets: ${storageResponse.status} ${storageResponse.statusText}`));
655
- return { success: false, buckets: [] };
656
- }
657
-
658
- const buckets = await storageResponse.json();
659
-
660
- if (!buckets || buckets.length === 0) {
661
- console.log(chalk.gray(' - Nenhum bucket encontrado'));
662
- await writeJson(path.join(storageDir, 'README.md'), {
663
- message: 'Nenhum bucket de Storage encontrado neste projeto'
664
- });
665
- return { success: true, buckets: [] };
666
- }
667
-
668
- console.log(chalk.gray(` - Encontrados ${buckets.length} buckets`));
669
-
670
- const processedBuckets = [];
671
-
672
- for (const bucket of buckets || []) {
673
- try {
674
- console.log(chalk.gray(` - Processando bucket: ${bucket.name}`));
675
-
676
- // ✅ Listar objetos do bucket via Management API com Personal Access Token
677
- const objectsResponse = await fetch(`https://api.supabase.com/v1/projects/${projectId}/storage/buckets/${bucket.name}/objects`, {
678
- headers: {
679
- 'Authorization': `Bearer ${accessToken}`,
680
- 'Content-Type': 'application/json'
681
- }
682
- });
683
-
684
- let objects = [];
685
- if (objectsResponse.ok) {
686
- objects = await objectsResponse.json();
687
- }
688
-
689
- const bucketInfo = {
690
- id: bucket.id,
691
- name: bucket.name,
692
- public: bucket.public,
693
- file_size_limit: bucket.file_size_limit,
694
- allowed_mime_types: bucket.allowed_mime_types,
695
- objects: objects || []
696
- };
697
-
698
- // Salvar informações do bucket
699
- const bucketPath = path.join(storageDir, `${bucket.name}.json`);
700
- await writeJson(bucketPath, bucketInfo);
701
-
702
- processedBuckets.push({
703
- name: bucket.name,
704
- objectCount: objects?.length || 0
705
- });
706
-
707
- console.log(chalk.green(` ✅ Bucket ${bucket.name}: ${objects?.length || 0} objetos`));
708
- } catch (error) {
709
- console.log(chalk.yellow(` ⚠️ Erro ao processar bucket ${bucket.name}: ${error.message}`));
710
- }
711
- }
712
-
713
- console.log(chalk.green(`✅ Storage backupado: ${processedBuckets.length} buckets`));
714
- return { success: true, buckets: processedBuckets };
715
- } catch (error) {
716
- console.log(chalk.yellow(`⚠️ Erro no backup do Storage: ${error.message}`));
717
- return { success: false, buckets: [] };
718
- }
719
- }
720
-
721
- // Backup dos Custom Roles via Docker
722
- async function backupCustomRoles(databaseUrl, backupDir, accessToken) {
723
- try {
724
- console.log(chalk.gray(' - Exportando Custom Roles via Docker...'));
725
-
726
- const customRolesFile = path.join(backupDir, 'custom-roles.sql');
727
-
728
- try {
729
- // ✅ Usar Supabase CLI via Docker para roles
730
- await execAsync(`supabase db dump --db-url "${databaseUrl}" --role-only -f "${customRolesFile}"`, { env: { ...process.env, SUPABASE_ACCESS_TOKEN: accessToken || '' } });
731
-
732
- const stats = await fs.stat(customRolesFile);
733
- const sizeKB = (stats.size / 1024).toFixed(1);
734
-
735
- console.log(chalk.green(` ✅ Custom Roles exportados via Docker: ${sizeKB} KB`));
736
-
737
- return { success: true, roles: [{ filename: 'custom-roles.sql', sizeKB }] };
738
- } catch (error) {
739
- console.log(chalk.yellow(` ⚠️ Erro ao exportar Custom Roles via Docker: ${error.message}`));
740
- return { success: false, roles: [] };
741
- }
742
- } catch (error) {
743
- console.log(chalk.yellow(` ⚠️ Erro no backup dos Custom Roles: ${error.message}`));
744
- return { success: false, roles: [] };
745
- }
746
- }
747
-
748
- // Backup das Database Extensions and Settings via SQL
749
- async function backupDatabaseSettings(databaseUrl, projectId, backupDir) {
750
- try {
751
- console.log(chalk.gray(' - Capturando Database Extensions and Settings...'));
752
-
753
- const { execSync } = require('child_process');
754
-
755
- // Extrair credenciais da databaseUrl
756
- const urlMatch = databaseUrl.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
757
-
758
- if (!urlMatch) {
759
- throw new Error('Database URL inválida');
760
- }
761
-
762
- const [, username, password, host, port, database] = urlMatch;
763
-
764
- // Gerar nome do arquivo
765
- const now = new Date();
766
- const day = String(now.getDate()).padStart(2, '0');
767
- const month = String(now.getMonth() + 1).padStart(2, '0');
768
- const year = now.getFullYear();
769
- const hours = String(now.getHours()).padStart(2, '0');
770
- const minutes = String(now.getMinutes()).padStart(2, '0');
771
- const seconds = String(now.getSeconds()).padStart(2, '0');
772
-
773
- const fileName = `database-settings-${day}-${month}-${year}@${hours}-${minutes}-${seconds}.json`;
774
-
775
- // Usar caminho absoluto igual às outras funções
776
- const backupDirAbs = path.resolve(backupDir);
777
-
778
- // Script SQL para capturar todas as configurações
779
- const sqlScript = `
780
- -- Database Extensions and Settings Backup
781
- -- Generated at: ${new Date().toISOString()}
782
-
783
- -- 1. Capturar extensões instaladas
784
- SELECT json_agg(
785
- json_build_object(
786
- 'name', extname,
787
- 'version', extversion,
788
- 'schema', extnamespace::regnamespace
789
- )
790
- ) as extensions
791
- FROM pg_extension;
792
-
793
- -- 2. Capturar configurações PostgreSQL importantes
794
- SELECT json_agg(
795
- json_build_object(
796
- 'name', name,
797
- 'setting', setting,
798
- 'unit', unit,
799
- 'context', context,
800
- 'description', short_desc
801
- )
802
- ) as postgres_settings
803
- FROM pg_settings
804
- WHERE name IN (
805
- 'statement_timeout',
806
- 'idle_in_transaction_session_timeout',
807
- 'lock_timeout',
808
- 'shared_buffers',
809
- 'work_mem',
810
- 'maintenance_work_mem',
811
- 'effective_cache_size',
812
- 'max_connections',
813
- 'log_statement',
814
- 'log_min_duration_statement',
815
- 'timezone',
816
- 'log_timezone',
817
- 'default_transaction_isolation',
818
- 'default_transaction_read_only',
819
- 'checkpoint_completion_target',
820
- 'wal_buffers',
821
- 'max_wal_size',
822
- 'min_wal_size'
823
- );
824
-
825
- -- 3. Capturar configurações específicas dos roles Supabase
826
- SELECT json_agg(
827
- json_build_object(
828
- 'role', rolname,
829
- 'config', rolconfig
830
- )
831
- ) as role_configurations
832
- FROM pg_roles
833
- WHERE rolname IN ('anon', 'authenticated', 'authenticator', 'postgres', 'service_role')
834
- AND rolconfig IS NOT NULL;
835
-
836
- -- 4. Capturar configurações de PGAudit (se existir)
837
- SELECT json_agg(
838
- json_build_object(
839
- 'role', rolname,
840
- 'config', rolconfig
841
- )
842
- ) as pgaudit_configurations
843
- FROM pg_roles
844
- WHERE rolconfig IS NOT NULL
845
- AND EXISTS (
846
- SELECT 1 FROM unnest(rolconfig) AS config
847
- WHERE config LIKE '%pgaudit%'
848
- );
849
- `;
850
-
851
- // Salvar script SQL temporário
852
- const sqlFile = path.join(backupDir, 'temp_settings.sql');
853
- await fs.writeFile(sqlFile, sqlScript);
854
-
855
- // Executar via Docker
856
- const dockerCmd = [
857
- 'docker run --rm --network host',
858
- `-v "${backupDirAbs}:/host"`,
859
- `-e PGPASSWORD="${password}"`,
860
- 'postgres:17 psql',
861
- `-h ${host}`,
862
- `-p ${port}`,
863
- `-U ${username}`,
864
- `-d ${database}`,
865
- '-f /host/temp_settings.sql',
866
- '-t', // Tuples only
867
- '-A' // Unaligned output
868
- ].join(' ');
869
-
870
- console.log(chalk.gray(' - Executando queries de configurações via Docker...'));
871
- const output = execSync(dockerCmd, { stdio: 'pipe', encoding: 'utf8' });
872
-
873
- // Processar output e criar JSON estruturado
874
- const lines = output.trim().split('\n').filter(line => line.trim());
875
-
876
- const result = {
877
- database_settings: {
878
- note: "Configurações específicas do database Supabase capturadas via SQL",
879
- captured_at: new Date().toISOString(),
880
- project_id: projectId,
881
- extensions: lines[0] ? JSON.parse(lines[0]) : [],
882
- postgres_settings: lines[1] ? JSON.parse(lines[1]) : [],
883
- role_configurations: lines[2] ? JSON.parse(lines[2]) : [],
884
- pgaudit_configurations: lines[3] ? JSON.parse(lines[3]) : [],
885
- restore_instructions: {
886
- note: "Estas configurações precisam ser aplicadas manualmente após a restauração do database",
887
- steps: [
888
- "1. Restaurar o database usando o arquivo .backup.gz",
889
- "2. Aplicar configurações de Postgres via SQL:",
890
- " ALTER DATABASE postgres SET setting_name TO 'value';",
891
- "3. Aplicar configurações de roles via SQL:",
892
- " ALTER ROLE role_name SET setting_name TO 'value';",
893
- "4. Habilitar extensões necessárias via Dashboard ou SQL:",
894
- " CREATE EXTENSION IF NOT EXISTS extension_name;",
895
- "5. Verificar configurações aplicadas:",
896
- " SELECT name, setting FROM pg_settings WHERE name IN (...);"
897
- ]
898
- }
899
- }
900
- };
901
-
902
- // Salvar arquivo JSON
903
- const jsonFile = path.join(backupDir, fileName);
904
- await fs.writeFile(jsonFile, JSON.stringify(result, null, 2));
905
-
906
- // Limpar arquivo temporário
907
- await fs.unlink(sqlFile);
908
-
909
- const stats = await fs.stat(jsonFile);
910
- const sizeKB = (stats.size / 1024).toFixed(1);
911
-
912
- console.log(chalk.green(` ✅ Database Settings: ${fileName} (${sizeKB} KB)`));
913
-
914
- return { success: true, size: sizeKB, fileName: fileName };
915
- } catch (error) {
916
- console.log(chalk.yellow(` ⚠️ Erro no backup das Database Settings: ${error.message}`));
917
- return { success: false };
918
- }
919
- }
920
-
921
- // Backup das Realtime Settings via Captura Interativa
922
- async function backupRealtimeSettings(projectId, backupDir, skipInteractive = false) {
923
- try {
924
- console.log(chalk.gray(' - Capturando Realtime Settings interativamente...'));
925
-
926
- const result = await captureRealtimeSettings(projectId, backupDir, skipInteractive);
927
-
928
- const stats = await fs.stat(path.join(backupDir, 'realtime-settings.json'));
929
- const sizeKB = (stats.size / 1024).toFixed(1);
930
-
931
- console.log(chalk.green(` ✅ Realtime Settings capturadas: ${sizeKB} KB`));
932
-
933
- return { success: true, settings: result };
934
- } catch (error) {
935
- console.log(chalk.yellow(` ⚠️ Erro ao capturar Realtime Settings: ${error.message}`));
936
- return { success: false };
937
- }
938
- }
939
-