smoonb 0.0.34 → 0.0.36

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.34",
3
+ "version": "0.0.36",
4
4
  "description": "Complete Supabase backup and migration tool - EXPERIMENTAL VERSION - USE AT YOUR OWN RISK",
5
5
  "preferGlobal": false,
6
6
  "preventGlobalInstall": true,
@@ -414,23 +414,40 @@ async function backupEdgeFunctionsWithDocker(projectId, accessToken, backupDir)
414
414
  let successCount = 0;
415
415
  let errorCount = 0;
416
416
 
417
- // ✅ Baixar cada Edge Function usando Supabase CLI via Docker
417
+ // ✅ Baixar cada Edge Function DIRETAMENTE para o backup (sem tocar em ./supabase/functions)
418
418
  for (const func of functions) {
419
419
  try {
420
420
  console.log(chalk.gray(` - Baixando: ${func.name}...`));
421
421
 
422
- // Usar comando oficial do Supabase CLI via Docker
423
- await execAsync(`supabase functions download ${func.name}`, {
424
- cwd: process.cwd(),
425
- timeout: 60000 // 60 segundos timeout
422
+ // Criar diretório da função DIRETAMENTE no backup
423
+ const functionTargetDir = path.join(functionsDir, func.name);
424
+ await ensureDir(functionTargetDir);
425
+
426
+ // Baixar Edge Function via Supabase CLI DIRETAMENTE para o backup
427
+ const { execSync } = require('child_process');
428
+ const tempBackupDir = path.join(backupDir, 'temp-supabase-download');
429
+
430
+ // Criar estrutura temp para download sem contaminar ./supabase/
431
+ await ensureDir(tempBackupDir);
432
+
433
+ // Download para diretório temporário
434
+ execSync(`supabase functions download ${func.name}`, {
435
+ cwd: tempBackupDir,
436
+ timeout: 60000,
437
+ stdio: 'pipe'
426
438
  });
427
439
 
428
- // Mover arquivos baixados para o diretório de backup
429
- const sourceDir = path.join(process.cwd(), 'supabase', 'functions', func.name);
430
- const targetDir = path.join(functionsDir, func.name);
440
+ // Mover de temp para o backup final
441
+ const tempFunctionDir = path.join(tempBackupDir, 'supabase', 'functions', func.name);
431
442
 
432
- if (await fs.access(sourceDir).then(() => true).catch(() => false)) {
433
- await copyDir(sourceDir, targetDir);
443
+ // Verificar se existe usando fs.promises.access
444
+ try {
445
+ await fs.access(tempFunctionDir);
446
+ await copyDir(tempFunctionDir, functionTargetDir);
447
+
448
+ // Limpar diretório temporário
449
+ await fs.rm(tempBackupDir, { recursive: true, force: true }).catch(() => {});
450
+
434
451
  console.log(chalk.green(` ✅ ${func.name} baixada com sucesso`));
435
452
  successCount++;
436
453
 
@@ -438,7 +455,7 @@ async function backupEdgeFunctionsWithDocker(projectId, accessToken, backupDir)
438
455
  name: func.name,
439
456
  slug: func.name,
440
457
  version: func.version || 'unknown',
441
- files: await fs.access(targetDir).then(() => fs.readdir(targetDir)).catch(() => [])
458
+ files: await fs.readdir(functionTargetDir).catch(() => [])
442
459
  });
443
460
  } else {
444
461
  throw new Error('Diretório não encontrado após download');
@@ -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
  };