smoonb 0.0.33 → 0.0.35

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/.smoonbrc CHANGED
@@ -4,7 +4,8 @@
4
4
  "url": "https://dfgdfgdfgdfgdfgdfg.supabase.co",
5
5
  "serviceKey": "sdfsdfsdfsdfsdfgyjyuiuyjyujuyjyujuyjyujuy",
6
6
  "anonKey": "uyjyujyujyhmnbmbghjghjghghjghjghjghj",
7
- "databaseUrl": "postgresql://postgres:ghjghjghjghjghjghjghj@db.sdfsdfsdfsdfsdfsdfsdfsdf.supabase.co:5432/postgres"
7
+ "databaseUrl": "postgresql://postgres:ghjghjghjghjghjghjghj@db.sdfsdfsdfsdfsdfsdfsdfsdf.supabase.co:5432/postgres",
8
+ "accessToken": "your-personal-access-token-here"
8
9
  },
9
10
  "backup": {
10
11
  "includeFunctions": true,
@@ -15,7 +16,14 @@
15
16
  "pgDumpPath": "C:\\Program Files\\PostgreSQL\\17\\bin\\pg_dump.exe"
16
17
  },
17
18
  "restore": {
18
- "cleanRestore": false,
19
- "verifyAfterRestore": true
19
+ "verifyAfterRestore": true,
20
+ "targetProject": {
21
+ "targetProjectId": "target-project-id-here",
22
+ "targetUrl": "https://target-project.supabase.co",
23
+ "targetServiceKey": "target-service-key",
24
+ "targetAnonKey": "target-anon-key",
25
+ "targetDatabaseUrl": "postgresql://postgres:[password]@db.target-project.supabase.co:5432/postgres",
26
+ "targetAccessToken": "target-access-token"
27
+ }
20
28
  }
21
29
  }
package/.smoonbrc.example CHANGED
@@ -15,7 +15,14 @@
15
15
  "pgDumpPath": "C:\\Program Files\\PostgreSQL\\17\\bin\\pg_dump.exe"
16
16
  },
17
17
  "restore": {
18
- "cleanRestore": false,
19
- "verifyAfterRestore": true
18
+ "verifyAfterRestore": true,
19
+ "targetProject": {
20
+ "targetProjectId": "target-project-id-here",
21
+ "targetUrl": "https://target-project.supabase.co",
22
+ "targetServiceKey": "target-service-key",
23
+ "targetAnonKey": "target-anon-key",
24
+ "targetDatabaseUrl": "postgresql://postgres:[password]@db.target-project.supabase.co:5432/postgres",
25
+ "targetAccessToken": "target-access-token"
26
+ }
20
27
  }
21
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoonb",
3
- "version": "0.0.33",
3
+ "version": "0.0.35",
4
4
  "description": "Complete Supabase backup and migration tool - EXPERIMENTAL VERSION - USE AT YOUR OWN RISK",
5
5
  "preferGlobal": false,
6
6
  "preventGlobalInstall": true,
@@ -108,37 +108,47 @@ async function performFullBackup(config, options) {
108
108
  };
109
109
 
110
110
  // 1. Backup Database via pg_dumpall Docker (idêntico ao Dashboard)
111
- console.log(chalk.blue('\n📊 1/7 - Backup da Database PostgreSQL via pg_dumpall Docker...'));
111
+ console.log(chalk.blue('\n📊 1/8 - Backup da Database PostgreSQL via pg_dumpall Docker...'));
112
112
  const databaseResult = await backupDatabase(config.supabase.projectId, backupDir);
113
113
  manifest.components.database = databaseResult;
114
114
 
115
+ // 1.5. Backup Database Separado (SQL files para troubleshooting)
116
+ console.log(chalk.blue('\n📊 1.5/8 - Backup da Database PostgreSQL (arquivos SQL separados)...'));
117
+ const dbSeparatedResult = await backupDatabaseSeparated(config.supabase.projectId, backupDir);
118
+ manifest.components.database_separated = {
119
+ success: dbSeparatedResult.success,
120
+ method: 'supabase-cli',
121
+ files: dbSeparatedResult.files || [],
122
+ total_size_kb: dbSeparatedResult.totalSizeKB || '0.0'
123
+ };
124
+
115
125
  // 2. Backup Edge Functions via Docker
116
- console.log(chalk.blue('\n⚡ 2/7 - Backup das Edge Functions via Docker...'));
126
+ console.log(chalk.blue('\n⚡ 2/8 - Backup das Edge Functions via Docker...'));
117
127
  const functionsResult = await backupEdgeFunctionsWithDocker(config.supabase.projectId, config.supabase.accessToken, backupDir);
118
128
  manifest.components.edge_functions = functionsResult;
119
129
 
120
130
  // 3. Backup Auth Settings via API
121
- console.log(chalk.blue('\n🔐 3/7 - Backup das Auth Settings via API...'));
131
+ console.log(chalk.blue('\n🔐 3/8 - Backup das Auth Settings via API...'));
122
132
  const authResult = await backupAuthSettings(config.supabase.projectId, config.supabase.accessToken, backupDir);
123
133
  manifest.components.auth_settings = authResult;
124
134
 
125
135
  // 4. Backup Storage via API
126
- console.log(chalk.blue('\n📦 4/7 - Backup do Storage via API...'));
136
+ console.log(chalk.blue('\n📦 4/8 - Backup do Storage via API...'));
127
137
  const storageResult = await backupStorage(config.supabase.projectId, config.supabase.accessToken, backupDir);
128
138
  manifest.components.storage = storageResult;
129
139
 
130
140
  // 5. Backup Custom Roles via SQL
131
- console.log(chalk.blue('\n👥 5/7 - Backup dos Custom Roles via SQL...'));
141
+ console.log(chalk.blue('\n👥 5/8 - Backup dos Custom Roles via SQL...'));
132
142
  const rolesResult = await backupCustomRoles(config.supabase.databaseUrl, backupDir);
133
143
  manifest.components.custom_roles = rolesResult;
134
144
 
135
145
  // 6. Backup das Database Extensions and Settings via SQL
136
- console.log(chalk.blue('\n🔧 6/7 - Backup das Database Extensions and Settings via SQL...'));
146
+ console.log(chalk.blue('\n🔧 6/8 - Backup das Database Extensions and Settings via SQL...'));
137
147
  const databaseSettingsResult = await backupDatabaseSettings(config.supabase.projectId, backupDir);
138
148
  manifest.components.database_settings = databaseSettingsResult;
139
149
 
140
150
  // 7. Backup Realtime Settings via Captura Interativa
141
- console.log(chalk.blue('\n🔄 7/7 - Backup das Realtime Settings via Captura Interativa...'));
151
+ console.log(chalk.blue('\n🔄 7/8 - Backup das Realtime Settings via Captura Interativa...'));
142
152
  const realtimeResult = await backupRealtimeSettings(config.supabase.projectId, backupDir, options.skipRealtime);
143
153
  manifest.components.realtime = realtimeResult;
144
154
 
@@ -148,6 +158,7 @@ async function performFullBackup(config, options) {
148
158
  console.log(chalk.green('\n🎉 BACKUP COMPLETO FINALIZADO VIA DOCKER!'));
149
159
  console.log(chalk.blue(`📁 Localização: ${backupDir}`));
150
160
  console.log(chalk.green(`📊 Database: ${databaseResult.fileName} (${databaseResult.size} KB) - Idêntico ao Dashboard`));
161
+ console.log(chalk.green(`📊 Database SQL: ${dbSeparatedResult.files?.length || 0} arquivos separados (${dbSeparatedResult.totalSizeKB} KB) - Para troubleshooting`));
151
162
  console.log(chalk.green(`🔧 Database Settings: ${databaseSettingsResult.fileName} (${databaseSettingsResult.size} KB) - Extensions e Configurações`));
152
163
  console.log(chalk.green(`⚡ Edge Functions: ${functionsResult.success_count || 0}/${functionsResult.functions_count || 0} functions baixadas via Docker`));
153
164
  console.log(chalk.green(`🔐 Auth Settings: ${authResult.success ? 'Exportadas via API' : 'Falharam'}`));
@@ -297,6 +308,75 @@ async function backupDatabase(projectId, backupDir) {
297
308
  }
298
309
  }
299
310
 
311
+ // Backup da database usando arquivos SQL separados via Supabase CLI (para troubleshooting)
312
+ async function backupDatabaseSeparated(projectId, backupDir) {
313
+ try {
314
+ console.log(chalk.gray(' - Criando backups SQL separados via Supabase CLI...'));
315
+
316
+ const { execSync } = require('child_process');
317
+ const config = await readConfig();
318
+
319
+ const dbUrl = config.supabase.databaseUrl;
320
+ const files = [];
321
+ let totalSizeKB = 0;
322
+
323
+ // 1. Backup do Schema
324
+ console.log(chalk.gray(' - Exportando schema...'));
325
+ const schemaFile = path.join(backupDir, 'schema.sql');
326
+
327
+ try {
328
+ execSync(`supabase db dump --db-url "${dbUrl}" -f "${schemaFile}"`, { stdio: 'pipe' });
329
+ const stats = await fs.stat(schemaFile);
330
+ const sizeKB = (stats.size / 1024).toFixed(1);
331
+ files.push({ filename: 'schema.sql', sizeKB });
332
+ totalSizeKB += parseFloat(sizeKB);
333
+ console.log(chalk.green(` ✅ Schema: ${sizeKB} KB`));
334
+ } catch (error) {
335
+ console.log(chalk.yellow(` ⚠️ Erro no schema: ${error.message}`));
336
+ }
337
+
338
+ // 2. Backup dos Dados
339
+ console.log(chalk.gray(' - Exportando dados...'));
340
+ const dataFile = path.join(backupDir, 'data.sql');
341
+
342
+ try {
343
+ execSync(`supabase db dump --db-url "${dbUrl}" --data-only -f "${dataFile}"`, { stdio: 'pipe' });
344
+ const stats = await fs.stat(dataFile);
345
+ const sizeKB = (stats.size / 1024).toFixed(1);
346
+ files.push({ filename: 'data.sql', sizeKB });
347
+ totalSizeKB += parseFloat(sizeKB);
348
+ console.log(chalk.green(` ✅ Data: ${sizeKB} KB`));
349
+ } catch (error) {
350
+ console.log(chalk.yellow(` ⚠️ Erro nos dados: ${error.message}`));
351
+ }
352
+
353
+ // 3. Backup dos Roles
354
+ console.log(chalk.gray(' - Exportando roles...'));
355
+ const rolesFile = path.join(backupDir, 'roles.sql');
356
+
357
+ try {
358
+ execSync(`supabase db dump --db-url "${dbUrl}" --role-only -f "${rolesFile}"`, { stdio: 'pipe' });
359
+ const stats = await fs.stat(rolesFile);
360
+ const sizeKB = (stats.size / 1024).toFixed(1);
361
+ files.push({ filename: 'roles.sql', sizeKB });
362
+ totalSizeKB += parseFloat(sizeKB);
363
+ console.log(chalk.green(` ✅ Roles: ${sizeKB} KB`));
364
+ } catch (error) {
365
+ console.log(chalk.yellow(` ⚠️ Erro nos roles: ${error.message}`));
366
+ }
367
+
368
+ return {
369
+ success: files.length > 0,
370
+ files,
371
+ totalSizeKB: totalSizeKB.toFixed(1)
372
+ };
373
+
374
+ } catch (error) {
375
+ console.log(chalk.yellow(` ⚠️ Erro nos backups SQL separados: ${error.message}`));
376
+ return { success: false, files: [], totalSizeKB: '0.0' };
377
+ }
378
+ }
379
+
300
380
  // Backup das Edge Functions via Docker
301
381
  async function backupEdgeFunctionsWithDocker(projectId, accessToken, backupDir) {
302
382
  try {
@@ -1,252 +1,390 @@
1
1
  const chalk = require('chalk');
2
2
  const path = require('path');
3
3
  const fs = require('fs');
4
- const inquirer = require('inquirer');
5
- const { ensureBin, runCommand } = require('../utils/cli');
6
- const { readConfig, validateFor } = require('../utils/config');
4
+ const { readConfig, getSourceProject, getTargetProject } = require('../utils/config');
7
5
  const { showBetaBanner } = require('../utils/banner');
8
6
 
9
- // Exportar FUNÇÃO em vez de objeto Command
10
7
  module.exports = async (options) => {
11
8
  showBetaBanner();
12
9
 
13
10
  try {
14
- // Verificar se psql está disponível
15
- const psqlPath = await ensureBin('psql');
16
- if (!psqlPath) {
17
- console.error(chalk.red('❌ psql não encontrado'));
18
- console.log(chalk.yellow('💡 Instale PostgreSQL:'));
19
- console.log(chalk.yellow(' https://www.postgresql.org/download/'));
20
- process.exit(1);
21
- }
22
-
23
- // Carregar configuração
24
11
  const config = await readConfig();
25
- validateFor(config, 'restore');
26
-
27
- // Resolver URL da database
28
- const databaseUrl = options.dbUrl || config.supabase.databaseUrl;
29
- if (!databaseUrl) {
30
- console.error(chalk.red('❌ databaseUrl não configurada'));
31
- console.log(chalk.yellow('💡 Configure databaseUrl no .smoonbrc ou use --db-url'));
32
- process.exit(1);
33
- }
34
-
35
- console.log(chalk.blue(`🔍 Procurando backups em: ${config.backup.outputDir || './backups'}`));
36
-
37
- // Listar backups disponíveis
38
- const backups = await listAvailableBackups(config.backup.outputDir || './backups');
12
+ const targetProject = getTargetProject(config);
39
13
 
40
- if (backups.length === 0) {
41
- console.error(chalk.red('❌ Nenhum backup encontrado'));
14
+ console.log(chalk.blue(`📁 Buscando backups em: ${config.backup.outputDir || './backups'}`));
15
+
16
+ // 1. Listar backups válidos (.backup.gz)
17
+ const validBackups = await listValidBackups(config.backup.outputDir || './backups');
18
+
19
+ if (validBackups.length === 0) {
20
+ console.error(chalk.red('❌ Nenhum backup válido encontrado'));
42
21
  console.log(chalk.yellow('💡 Execute primeiro: npx smoonb backup'));
43
22
  process.exit(1);
44
23
  }
45
-
46
- // Seleção interativa do backup
47
- const selectedBackup = await selectBackup(backups);
48
24
 
49
- console.log(chalk.blue(`🚀 Iniciando restauração do backup: ${selectedBackup.name}`));
50
- console.log(chalk.blue(`🎯 Database destino: ${databaseUrl.replace(/:[^:]*@/, ':***@')}`));
51
-
52
- // Verificar se é clean restore
53
- if (config.restore.cleanRestore) {
54
- await checkCleanRestore(databaseUrl);
25
+ // 2. Selecionar backup interativamente
26
+ const selectedBackup = await selectBackupInteractive(validBackups);
27
+
28
+ // 3. Perguntar quais componentes restaurar
29
+ const components = await askRestoreComponents(selectedBackup.path);
30
+
31
+ // 4. Mostrar resumo
32
+ showRestoreSummary(selectedBackup, components, targetProject);
33
+
34
+ // 5. Confirmar execução
35
+ const confirmed = await confirmExecution();
36
+ if (!confirmed) {
37
+ console.log(chalk.yellow('Restauração cancelada.'));
38
+ process.exit(0);
55
39
  }
56
-
57
- // Executar restauração
58
- await performRestore(selectedBackup.path, databaseUrl);
59
-
60
- // Verificação pós-restore
61
- if (config.restore.verifyAfterRestore) {
62
- console.log(chalk.blue('\n🔍 Executando verificação pós-restore...'));
63
- console.log(chalk.yellow('💡 Execute manualmente: npx smoonb check'));
40
+
41
+ // 6. Executar restauração
42
+ console.log(chalk.blue('\n🚀 Iniciando restauração...'));
43
+
44
+ // 6.1 Database
45
+ await restoreDatabaseGz(
46
+ path.join(selectedBackup.path, selectedBackup.backupFile),
47
+ targetProject.targetDatabaseUrl
48
+ );
49
+
50
+ // 6.2 Edge Functions (se selecionado)
51
+ if (components.edgeFunctions) {
52
+ await restoreEdgeFunctions(selectedBackup.path, targetProject);
64
53
  }
65
-
66
- console.log(chalk.green('\n🎉 Restauração concluída com sucesso!'));
67
-
54
+
55
+ // 6.3 Storage Buckets (se selecionado)
56
+ if (components.storage) {
57
+ await restoreStorageBuckets(selectedBackup.path, targetProject);
58
+ }
59
+
60
+ // 6.4 Auth Settings (se selecionado)
61
+ if (components.authSettings) {
62
+ await restoreAuthSettings(selectedBackup.path, targetProject);
63
+ }
64
+
65
+ // 6.5 Database Settings (se selecionado)
66
+ if (components.databaseSettings) {
67
+ await restoreDatabaseSettings(selectedBackup.path, targetProject);
68
+ }
69
+
70
+ // 6.6 Realtime Settings (se selecionado)
71
+ if (components.realtimeSettings) {
72
+ await restoreRealtimeSettings(selectedBackup.path, targetProject);
73
+ }
74
+
75
+ console.log(chalk.green('\n🎉 Restauração completa finalizada!'));
76
+
68
77
  } catch (error) {
69
78
  console.error(chalk.red(`❌ Erro na restauração: ${error.message}`));
70
79
  process.exit(1);
71
80
  }
72
81
  };
73
82
 
74
- // Listar backups disponíveis
75
- async function listAvailableBackups(backupsDir) {
83
+ // Listar backups válidos (apenas com .backup.gz)
84
+ async function listValidBackups(backupsDir) {
76
85
  if (!fs.existsSync(backupsDir)) {
77
86
  return [];
78
87
  }
79
88
 
80
89
  const items = fs.readdirSync(backupsDir, { withFileTypes: true });
81
- const backups = [];
90
+ const validBackups = [];
82
91
 
83
92
  for (const item of items) {
84
93
  if (item.isDirectory() && item.name.startsWith('backup-')) {
85
94
  const backupPath = path.join(backupsDir, item.name);
86
- const manifestPath = path.join(backupPath, 'backup-manifest.json');
95
+ const files = fs.readdirSync(backupPath);
96
+ const backupFile = files.find(file => file.endsWith('.backup.gz'));
87
97
 
88
- let manifest = null;
89
- if (fs.existsSync(manifestPath)) {
90
- try {
91
- manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
92
- } catch (error) {
93
- console.warn(chalk.yellow(`⚠️ Erro ao ler manifesto: ${item.name}`));
98
+ if (backupFile) {
99
+ const manifestPath = path.join(backupPath, 'backup-manifest.json');
100
+ let manifest = null;
101
+
102
+ if (fs.existsSync(manifestPath)) {
103
+ try {
104
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
105
+ } catch (error) {
106
+ // Ignorar erro de leitura do manifest
107
+ }
94
108
  }
95
- }
96
-
97
- const stats = fs.statSync(backupPath);
98
-
99
- backups.push({
100
- name: item.name,
101
- path: backupPath,
102
- created: manifest?.created_at || stats.birthtime.toISOString(),
103
- projectId: manifest?.project_id || 'Desconhecido',
104
- size: getDirectorySize(backupPath),
105
- manifest: manifest
106
- });
107
- }
108
- }
109
-
110
- // Ordenar por data de criação (mais recente primeiro)
111
- return backups.sort((a, b) => new Date(b.created) - new Date(a.created));
112
- }
113
109
 
114
- // Calcular tamanho do diretório
115
- function getDirectorySize(dirPath) {
116
- let totalSize = 0;
117
-
118
- function calculateSize(itemPath) {
119
- const stats = fs.statSync(itemPath);
120
-
121
- if (stats.isDirectory()) {
122
- const items = fs.readdirSync(itemPath);
123
- for (const item of items) {
124
- calculateSize(path.join(itemPath, item));
110
+ const stats = fs.statSync(path.join(backupPath, backupFile));
111
+
112
+ validBackups.push({
113
+ name: item.name,
114
+ path: backupPath,
115
+ backupFile: backupFile,
116
+ created: manifest?.created_at || stats.birthtime.toISOString(),
117
+ projectId: manifest?.project_id || 'Desconhecido',
118
+ size: formatBytes(stats.size),
119
+ manifest: manifest
120
+ });
125
121
  }
126
- } else {
127
- totalSize += stats.size;
128
122
  }
129
123
  }
130
-
131
- try {
132
- calculateSize(dirPath);
133
- } catch (error) {
134
- // Ignorar erros de acesso
135
- }
136
-
137
- return formatBytes(totalSize);
124
+
125
+ return validBackups.sort((a, b) => new Date(b.created) - new Date(a.created));
138
126
  }
139
127
 
140
- // Formatar bytes em formato legível
128
+ // Formatar bytes
141
129
  function formatBytes(bytes) {
142
130
  if (bytes === 0) return '0 Bytes';
143
-
144
131
  const k = 1024;
145
132
  const sizes = ['Bytes', 'KB', 'MB', 'GB'];
146
133
  const i = Math.floor(Math.log(bytes) / Math.log(k));
147
-
148
134
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
149
135
  }
150
136
 
151
- // Seleção interativa do backup
152
- async function selectBackup(backups) {
137
+ // Seleção interativa de backup
138
+ async function selectBackupInteractive(backups) {
153
139
  console.log(chalk.blue('\n📋 Backups disponíveis:'));
154
140
  console.log(chalk.blue('═'.repeat(80)));
155
141
 
156
- const choices = backups.map((backup, index) => {
142
+ backups.forEach((backup, index) => {
157
143
  const date = new Date(backup.created).toLocaleString('pt-BR');
158
144
  const projectInfo = backup.projectId !== 'Desconhecido' ? ` (${backup.projectId})` : '';
159
145
 
160
- return {
161
- name: `${index + 1}. ${backup.name}${projectInfo}\n 📅 ${date} | 📦 ${backup.size}`,
162
- value: backup,
163
- short: backup.name
164
- };
146
+ console.log(`${index + 1}. ${backup.name}${projectInfo}`);
147
+ console.log(` 📅 ${date} | 📦 ${backup.size}`);
148
+ console.log('');
149
+ });
150
+
151
+ const readline = require('readline').createInterface({
152
+ input: process.stdin,
153
+ output: process.stdout
165
154
  });
155
+
156
+ const question = (query) => new Promise(resolve => readline.question(query, resolve));
157
+
158
+ const choice = await question(`\nDigite o número do backup para restaurar (1-${backups.length}): `);
159
+ readline.close();
160
+
161
+ const backupIndex = parseInt(choice) - 1;
162
+
163
+ if (backupIndex < 0 || backupIndex >= backups.length) {
164
+ throw new Error('Número inválido');
165
+ }
166
+
167
+ return backups[backupIndex];
168
+ }
166
169
 
167
- const { selectedBackup } = await inquirer.prompt([
168
- {
169
- type: 'list',
170
- name: 'selectedBackup',
171
- message: 'Selecione o backup para restaurar:',
172
- choices: choices,
173
- pageSize: 10
174
- }
175
- ]);
170
+ // Perguntar quais componentes restaurar
171
+ async function askRestoreComponents(backupPath) {
172
+ const components = {
173
+ edgeFunctions: true,
174
+ storage: false,
175
+ authSettings: false,
176
+ databaseSettings: false,
177
+ realtimeSettings: false
178
+ };
179
+
180
+ const readline = require('readline').createInterface({
181
+ input: process.stdin,
182
+ output: process.stdout
183
+ });
184
+
185
+ const question = (query) => new Promise(resolve => readline.question(query, resolve));
186
+
187
+ console.log(chalk.blue('\n📦 Selecione os componentes para restaurar:'));
188
+
189
+ // Edge Functions
190
+ const edgeFunctionsDir = path.join(backupPath, 'edge-functions');
191
+ if (fs.existsSync(edgeFunctionsDir) && fs.readdirSync(edgeFunctionsDir).length > 0) {
192
+ const edgeChoice = await question('Deseja restaurar Edge Functions? (S/n): ');
193
+ components.edgeFunctions = edgeChoice.toLowerCase() !== 'n';
194
+ }
195
+
196
+ // Storage Buckets
197
+ const storageDir = path.join(backupPath, 'storage');
198
+ if (fs.existsSync(storageDir) && fs.readdirSync(storageDir).length > 0) {
199
+ const storageChoice = await question('Deseja restaurar Storage Buckets? (s/N): ');
200
+ components.storage = storageChoice.toLowerCase() === 's';
201
+ }
202
+
203
+ // Auth Settings
204
+ if (fs.existsSync(path.join(backupPath, 'auth-settings.json'))) {
205
+ const authChoice = await question('Deseja restaurar Auth Settings? (s/N): ');
206
+ components.authSettings = authChoice.toLowerCase() === 's';
207
+ }
208
+
209
+ // Database Settings
210
+ const dbSettingsFiles = fs.readdirSync(backupPath)
211
+ .filter(file => file.startsWith('database-settings-') && file.endsWith('.json'));
212
+ if (dbSettingsFiles.length > 0) {
213
+ const dbChoice = await question('Deseja restaurar Database Extensions and Settings? (s/N): ');
214
+ components.databaseSettings = dbChoice.toLowerCase() === 's';
215
+ }
216
+
217
+ // Realtime Settings
218
+ if (fs.existsSync(path.join(backupPath, 'realtime-settings.json'))) {
219
+ const realtimeChoice = await question('Deseja restaurar Realtime Settings? (s/N): ');
220
+ components.realtimeSettings = realtimeChoice.toLowerCase() === 's';
221
+ }
222
+
223
+ readline.close();
224
+ return components;
225
+ }
176
226
 
177
- return selectedBackup;
227
+ // Mostrar resumo da restauração
228
+ function showRestoreSummary(backup, components, targetProject) {
229
+ console.log(chalk.blue('\n📋 Resumo da Restauração:'));
230
+ console.log(chalk.blue('═'.repeat(80)));
231
+ console.log(chalk.cyan(`📦 Backup: ${backup.name}`));
232
+ console.log(chalk.cyan(`📤 Projeto Origem: ${backup.projectId}`));
233
+ console.log(chalk.cyan(`📥 Projeto Destino: ${targetProject.targetProjectId}`));
234
+ console.log('');
235
+ console.log(chalk.cyan('Componentes que serão restaurados:'));
236
+ console.log('');
237
+
238
+ console.log('✅ Database (psql -f via Docker)');
239
+
240
+ if (components.edgeFunctions) {
241
+ const edgeFunctionsDir = path.join(backup.path, 'edge-functions');
242
+ const functions = fs.readdirSync(edgeFunctionsDir).filter(item =>
243
+ fs.statSync(path.join(edgeFunctionsDir, item)).isDirectory()
244
+ );
245
+ console.log(`⚡ Edge Functions: ${functions.length} function(s)`);
246
+ functions.forEach(func => console.log(` - ${func}`));
247
+ }
248
+
249
+ if (components.storage) {
250
+ console.log('📦 Storage Buckets: Restaurar buckets e objetos');
251
+ }
252
+
253
+ if (components.authSettings) {
254
+ console.log('🔐 Auth Settings: Restaurar configurações de autenticação');
255
+ }
256
+
257
+ if (components.databaseSettings) {
258
+ console.log('🔧 Database Settings: Restaurar extensões e configurações');
259
+ }
260
+
261
+ if (components.realtimeSettings) {
262
+ console.log('🔄 Realtime Settings: Restaurar configurações do Realtime');
263
+ }
264
+
265
+ console.log('');
178
266
  }
179
267
 
180
- // Verificar se é possível fazer clean restore
181
- async function checkCleanRestore(databaseUrl) {
268
+ // Confirmar execução
269
+ async function confirmExecution() {
270
+ const readline = require('readline').createInterface({
271
+ input: process.stdin,
272
+ output: process.stdout
273
+ });
274
+
275
+ const question = (query) => new Promise(resolve => readline.question(query, resolve));
276
+
277
+ const confirm = await question('Deseja continuar com a restauração? (s/N): ');
278
+ readline.close();
279
+
280
+ return confirm.toLowerCase() === 's';
281
+ }
282
+
283
+ // Restaurar Database via psql (conforme documentação oficial Supabase: https://supabase.com/docs/guides/platform/migrating-within-supabase/dashboard-restore)
284
+ async function restoreDatabaseGz(backupFilePath, targetDatabaseUrl) {
285
+ console.log(chalk.blue('📊 Restaurando Database...'));
286
+ console.log(chalk.gray(' - Descompactando backup (se necessário)...'));
287
+
182
288
  try {
183
- console.log(chalk.blue('🔍 Verificando se database está vazia...'));
289
+ const { execSync } = require('child_process');
184
290
 
185
- // Verificar se existem tabelas no schema public
186
- const checkQuery = `
187
- SELECT COUNT(*) as table_count
188
- FROM information_schema.tables
189
- WHERE table_schema = 'public' AND table_type = 'BASE TABLE';
190
- `;
291
+ const backupDirAbs = path.resolve(path.dirname(backupFilePath));
292
+ const fileName = path.basename(backupFilePath);
293
+ let uncompressedFile = fileName;
191
294
 
192
- const { stdout } = await runCommand(
193
- `psql "${databaseUrl}" -t -c "${checkQuery}"`
194
- );
295
+ // Descompactar .gz se necessário
296
+ if (fileName.endsWith('.gz')) {
297
+ console.log(chalk.gray(' - Extraindo arquivo .gz...'));
298
+ const unzipCmd = [
299
+ 'docker run --rm',
300
+ `-v "${backupDirAbs}:/host"`,
301
+ 'postgres:17 gunzip /host/' + fileName
302
+ ].join(' ');
303
+
304
+ execSync(unzipCmd, { stdio: 'pipe' });
305
+ uncompressedFile = fileName.replace('.gz', '');
306
+ console.log(chalk.gray(' - Arquivo descompactado: ' + uncompressedFile));
307
+ }
195
308
 
196
- const tableCount = parseInt(stdout.trim());
309
+ // Extrair credenciais da URL de conexão
310
+ const urlMatch = targetDatabaseUrl.match(/postgresql:\/\/([^@:]+):([^@]+)@(.+)$/);
197
311
 
198
- if (tableCount > 0) {
199
- console.error(chalk.red('Database não está vazia!'));
200
- console.log(chalk.yellow('💡 Para clean restore, a database deve estar vazia'));
201
- console.log(chalk.yellow('💡 Opções:'));
202
- console.log(chalk.yellow(' 1. Criar uma nova database'));
203
- console.log(chalk.yellow(' 2. Desabilitar cleanRestore no .smoonbrc'));
204
- console.log(chalk.yellow(' 3. Limpar manualmente a database'));
205
- process.exit(1);
312
+ if (!urlMatch) {
313
+ throw new Error('Database URL inválida. Formato esperado: postgresql://user:password@host/database');
206
314
  }
207
315
 
208
- console.log(chalk.green('✅ Database está vazia, prosseguindo com clean restore'));
316
+ // Comando psql conforme documentação oficial Supabase
317
+ // Formato: psql -d [CONNECTION_STRING] -f /file/path
318
+ // Referência: https://supabase.com/docs/guides/platform/migrating-within-supabase/dashboard-restore
319
+ const restoreCmd = [
320
+ 'docker run --rm --network host',
321
+ `-v "${backupDirAbs}:/host"`,
322
+ `-e PGPASSWORD="${encodeURIComponent(urlMatch[2])}"`,
323
+ 'postgres:17 psql',
324
+ `-d "${targetDatabaseUrl}"`,
325
+ `-f /host/${uncompressedFile}`
326
+ ].join(' ');
209
327
 
210
- } catch (error) {
211
- console.log(chalk.yellow(`⚠️ Não foi possível verificar database: ${error.message}`));
212
- console.log(chalk.yellow('💡 Prosseguindo com restauração...'));
213
- }
214
- }
215
-
216
- // Executar restauração usando psql
217
- async function performRestore(backupDir, databaseUrl) {
218
- const sqlFiles = ['roles.sql', 'schema.sql', 'data.sql'];
219
-
220
- for (const sqlFile of sqlFiles) {
221
- const filePath = path.join(backupDir, sqlFile);
328
+ console.log(chalk.gray(' - Executando psql via Docker...'));
329
+ console.log(chalk.gray(' ℹ️ Seguindo documentação oficial Supabase'));
330
+ console.log(chalk.yellow(' ⚠️ AVISO: Erros como "object already exists" são ESPERADOS'));
331
+ console.log(chalk.yellow(' ⚠️ Isto acontece porque o backup contém CREATE para todos os schemas'));
332
+ console.log(chalk.yellow(' ⚠️ Supabase já tem auth e storage criados, então esses erros são normais'));
222
333
 
223
- if (!fs.existsSync(filePath)) {
224
- console.log(chalk.yellow(`⚠️ Arquivo ${sqlFile} não encontrado, pulando...`));
225
- continue;
226
- }
334
+ // Executar comando de restauração
335
+ execSync(restoreCmd, { stdio: 'inherit', encoding: 'utf8' });
227
336
 
228
- console.log(chalk.blue(`📄 Executando ${sqlFile}...`));
337
+ console.log(chalk.green(' ✅ Database restaurada com sucesso!'));
338
+ console.log(chalk.gray(' ℹ️ Erros "already exists" são normais e não afetam a restauração'));
229
339
 
230
- try {
231
- let command;
232
- if (sqlFile === 'data.sql') {
233
- // Para dados, usar single-transaction
234
- command = `psql "${databaseUrl}" -v ON_ERROR_STOP=1 --single-transaction -f "${filePath}"`;
235
- } else {
236
- // Para roles e schema, usar ON_ERROR_STOP
237
- command = `psql "${databaseUrl}" -v ON_ERROR_STOP=1 -f "${filePath}"`;
238
- }
239
-
240
- const { stdout, stderr } = await runCommand(command);
241
-
242
- if (stderr && !stderr.includes('NOTICE')) {
243
- console.log(chalk.yellow(`⚠️ Avisos em ${sqlFile}: ${stderr}`));
244
- }
245
-
246
- console.log(chalk.green(`✅ ${sqlFile} executado com sucesso`));
247
-
248
- } catch (error) {
249
- throw new Error(`Falha ao executar ${sqlFile}: ${error.message}`);
340
+ } catch (error) {
341
+ // Erros esperados conforme documentação oficial Supabase
342
+ // Referência: https://supabase.com/docs/guides/platform/migrating-within-supabase/dashboard-restore#common-errors
343
+ if (error.message.includes('already exists') ||
344
+ error.message.includes('constraint') ||
345
+ error.message.includes('duplicate') ||
346
+ error.stdout?.includes('already exists')) {
347
+ console.log(chalk.yellow(' ⚠️ Erros esperados encontrados (conforme documentação Supabase)'));
348
+ console.log(chalk.green(' ✅ Database restaurada com sucesso!'));
349
+ console.log(chalk.gray(' ℹ️ Erros são ignorados pois são comandos de CREATE que já existem'));
350
+ } else {
351
+ console.error(chalk.red(` ❌ Erro inesperado na restauração: ${error.message}`));
352
+ throw error;
250
353
  }
251
354
  }
252
- }
355
+ }
356
+
357
+ // Restaurar Edge Functions (placeholder - implementar via Management API)
358
+ async function restoreEdgeFunctions(backupPath, targetProject) {
359
+ console.log(chalk.blue('⚡ Restaurando Edge Functions...'));
360
+ console.log(chalk.yellow(' ℹ️ Deploy de Edge Functions via Management API ainda não implementado'));
361
+ // TODO: Implementar deploy via Supabase Management API
362
+ }
363
+
364
+ // Restaurar Storage Buckets (placeholder)
365
+ async function restoreStorageBuckets(backupPath, targetProject) {
366
+ console.log(chalk.blue('📦 Restaurando Storage Buckets...'));
367
+ console.log(chalk.yellow(' ℹ️ Restauração de Storage Buckets ainda não implementado'));
368
+ // TODO: Implementar restauração via Management API
369
+ }
370
+
371
+ // Restaurar Auth Settings (placeholder)
372
+ async function restoreAuthSettings(backupPath, targetProject) {
373
+ console.log(chalk.blue('🔐 Restaurando Auth Settings...'));
374
+ console.log(chalk.yellow(' ℹ️ Restauração de Auth Settings ainda não implementado'));
375
+ // TODO: Implementar via Management API
376
+ }
377
+
378
+ // Restaurar Database Settings (placeholder)
379
+ async function restoreDatabaseSettings(backupPath, targetProject) {
380
+ console.log(chalk.blue('🔧 Restaurando Database Settings...'));
381
+ console.log(chalk.yellow(' ℹ️ Restauração de Database Settings ainda não implementado'));
382
+ // TODO: Aplicar extensões e configurações via SQL
383
+ }
384
+
385
+ // Restaurar Realtime Settings (placeholder)
386
+ async function restoreRealtimeSettings(backupPath, targetProject) {
387
+ console.log(chalk.blue('🔄 Restaurando Realtime Settings...'));
388
+ console.log(chalk.yellow(' ℹ️ Realtime Settings requerem configuração manual no Dashboard'));
389
+ // TODO: Adicionar instruções de configuração manual
390
+ }
@@ -133,8 +133,37 @@ async function saveConfig(config, targetPath = null) {
133
133
  await fs.promises.writeFile(configPath, jsonContent, 'utf8');
134
134
  }
135
135
 
136
+ /**
137
+ * Obtém configuração do projeto source
138
+ * @param {object} config - Configuração carregada
139
+ * @returns {object} - Configuração do projeto source
140
+ */
141
+ function getSourceProject(config) {
142
+ if (config.projects && config.projects.source) {
143
+ return config.projects.source;
144
+ }
145
+ // Fallback para estrutura antiga
146
+ return config.supabase;
147
+ }
148
+
149
+ /**
150
+ * Obtém configuração do projeto target
151
+ * @param {object} config - Configuração carregada
152
+ * @returns {object} - Configuração do projeto target
153
+ */
154
+ function getTargetProject(config) {
155
+ // Tenta restaurar.targetProject (nova estrutura)
156
+ if (config.restore?.targetProject) {
157
+ return config.restore.targetProject;
158
+ }
159
+
160
+ throw new Error('Projeto destino não configurado. Configure "targetProject" em restore no .smoonbrc');
161
+ }
162
+
136
163
  module.exports = {
137
164
  readConfig,
138
165
  validateFor,
139
- saveConfig
166
+ saveConfig,
167
+ getSourceProject,
168
+ getTargetProject
140
169
  };