smoonb 0.0.47 → 0.0.49

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 (46) hide show
  1. package/README.md +339 -87
  2. package/bin/smoonb.js +2 -2
  3. package/package.json +1 -1
  4. package/src/commands/backup/index.js +316 -0
  5. package/src/commands/backup/steps/00-docker-validation.js +24 -0
  6. package/src/commands/backup/steps/01-database.js +72 -0
  7. package/src/commands/backup/steps/02-database-separated.js +82 -0
  8. package/src/commands/backup/steps/03-database-settings.js +178 -0
  9. package/src/commands/backup/steps/04-auth-settings.js +43 -0
  10. package/src/commands/backup/steps/05-realtime-settings.js +26 -0
  11. package/src/commands/backup/steps/06-storage.js +90 -0
  12. package/src/commands/backup/steps/07-custom-roles.js +39 -0
  13. package/src/commands/backup/steps/08-edge-functions.js +153 -0
  14. package/src/commands/backup/steps/09-supabase-temp.js +42 -0
  15. package/src/commands/backup/steps/10-migrations.js +74 -0
  16. package/src/commands/backup/utils.js +69 -0
  17. package/src/commands/check.js +0 -1
  18. package/src/commands/config.js +0 -1
  19. package/src/commands/functions.js +1 -1
  20. package/src/commands/restore/index.js +206 -0
  21. package/src/commands/restore/steps/00-backup-selection.js +38 -0
  22. package/src/commands/restore/steps/01-components-selection.js +71 -0
  23. package/src/commands/restore/steps/02-confirmation.js +14 -0
  24. package/src/commands/restore/steps/03-database.js +81 -0
  25. package/src/commands/restore/steps/04-edge-functions.js +112 -0
  26. package/src/commands/restore/steps/05-auth-settings.js +51 -0
  27. package/src/commands/restore/steps/06-storage.js +58 -0
  28. package/src/commands/restore/steps/07-database-settings.js +65 -0
  29. package/src/commands/restore/steps/08-realtime-settings.js +50 -0
  30. package/src/commands/restore/utils.js +139 -0
  31. package/src/index.js +3 -3
  32. package/src/interactive/envMapper.js +38 -14
  33. package/src/utils/cli.js +1 -1
  34. package/src/utils/config.js +1 -3
  35. package/src/utils/docker.js +3 -3
  36. package/src/utils/env.js +2 -3
  37. package/src/utils/envMap.js +1 -1
  38. package/src/utils/fsExtra.js +98 -0
  39. package/src/utils/fsx.js +2 -2
  40. package/src/utils/prompt.js +34 -0
  41. package/src/utils/realtime-settings.js +2 -2
  42. package/src/utils/supabase.js +10 -10
  43. package/src/utils/supabaseLink.js +82 -0
  44. package/src/utils/validation.js +2 -2
  45. package/src/commands/backup.js +0 -939
  46. package/src/commands/restore.js +0 -786
@@ -1,786 +0,0 @@
1
- const chalk = require('chalk');
2
- const path = require('path');
3
- const fs = require('fs');
4
- const { readConfig, getSourceProject, getTargetProject } = require('../utils/config');
5
- const { showBetaBanner } = require('../utils/banner');
6
- const inquirer = require('inquirer');
7
- const { readEnvFile, writeEnvFile, backupEnvFile } = require('../utils/env');
8
- const { saveEnvMap } = require('../utils/envMap');
9
- const { mapEnvVariablesInteractively } = require('../interactive/envMapper');
10
-
11
- module.exports = async (options) => {
12
- showBetaBanner();
13
-
14
- try {
15
- // Consentimento para leitura e escrita do .env.local
16
- console.log(chalk.yellow('⚠️ O smoonb irá ler e escrever o arquivo .env.local localmente.'));
17
- console.log(chalk.yellow(' Um backup automático do .env.local será criado antes de qualquer alteração.'));
18
- const consent = await inquirer.prompt([{ type: 'confirm', name: 'ok', message: 'Você consente em prosseguir (S/n):', default: true }]);
19
- if (!consent.ok) {
20
- console.log(chalk.red('🚫 Operação cancelada pelo usuário.'));
21
- process.exit(1);
22
- }
23
-
24
- // Preparar diretório de processo restore-YYYY-...
25
- const rootBackupsDir = path.join(process.cwd(), 'backups');
26
- const now = new Date();
27
- const ts = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}-${String(now.getHours()).padStart(2,'0')}-${String(now.getMinutes()).padStart(2,'0')}-${String(now.getSeconds()).padStart(2,'0')}`;
28
- const processDir = path.join(rootBackupsDir, `restore-${ts}`);
29
- fs.mkdirSync(path.join(processDir, 'env'), { recursive: true });
30
-
31
- // Backup do .env.local
32
- const envPath = path.join(process.cwd(), '.env.local');
33
- const envBackupPath = path.join(processDir, 'env', '.env.local');
34
- await backupEnvFile(envPath, envBackupPath);
35
- console.log(chalk.blue(`📁 Backup do .env.local: ${path.relative(process.cwd(), envBackupPath)}`));
36
-
37
- // Leitura e mapeamento interativo
38
- const currentEnv = await readEnvFile(envPath);
39
- const expectedKeys = [
40
- 'NEXT_PUBLIC_SUPABASE_URL',
41
- 'NEXT_PUBLIC_SUPABASE_ANON_KEY',
42
- 'SUPABASE_SERVICE_ROLE_KEY',
43
- 'SUPABASE_DB_URL',
44
- 'SUPABASE_PROJECT_ID',
45
- 'SUPABASE_ACCESS_TOKEN',
46
- 'SMOONB_OUTPUT_DIR'
47
- ];
48
- const { finalEnv, dePara } = await mapEnvVariablesInteractively(currentEnv, expectedKeys);
49
- await writeEnvFile(envPath, finalEnv);
50
- await saveEnvMap(dePara, path.join(processDir, 'env', 'env-map.json'));
51
- console.log(chalk.green('✅ .env.local atualizado com sucesso. Nenhuma chave renomeada; valores sincronizados.'));
52
-
53
- // Resolver valores esperados a partir do de-para
54
- function getValue(expectedKey) {
55
- const clientKey = Object.keys(dePara).find(k => dePara[k] === expectedKey);
56
- return clientKey ? finalEnv[clientKey] : '';
57
- }
58
-
59
- // Construir targetProject a partir do .env.local mapeado
60
- const targetProject = {
61
- targetProjectId: getValue('SUPABASE_PROJECT_ID'),
62
- targetUrl: getValue('NEXT_PUBLIC_SUPABASE_URL'),
63
- targetAnonKey: getValue('NEXT_PUBLIC_SUPABASE_ANON_KEY'),
64
- targetServiceKey: getValue('SUPABASE_SERVICE_ROLE_KEY'),
65
- targetDatabaseUrl: getValue('SUPABASE_DB_URL'),
66
- targetAccessToken: getValue('SUPABASE_ACCESS_TOKEN')
67
- };
68
-
69
- console.log(chalk.blue(`📁 Buscando backups em: ${getValue('SMOONB_OUTPUT_DIR') || './backups'}`));
70
-
71
- // 1. Listar backups válidos (.backup.gz)
72
- const validBackups = await listValidBackups(getValue('SMOONB_OUTPUT_DIR') || './backups');
73
-
74
- if (validBackups.length === 0) {
75
- console.error(chalk.red('❌ Nenhum backup válido encontrado'));
76
- console.log(chalk.yellow('💡 Execute primeiro: npx smoonb backup'));
77
- process.exit(1);
78
- }
79
-
80
- // 2. Selecionar backup interativamente
81
- const selectedBackup = await selectBackupInteractive(validBackups);
82
-
83
- // 3. Perguntar quais componentes restaurar
84
- const components = await askRestoreComponents(selectedBackup.path);
85
-
86
- // Validar que pelo menos um componente foi selecionado
87
- if (!Object.values(components).some(Boolean)) {
88
- console.error(chalk.red('\n❌ Nenhum componente selecionado para restauração!'));
89
- process.exit(1);
90
- }
91
-
92
- // 4. Mostrar resumo
93
- showRestoreSummary(selectedBackup, components, targetProject);
94
-
95
- // 5. Confirmar execução
96
- const confirmed = await confirmExecution();
97
- if (!confirmed) {
98
- console.log(chalk.yellow('Restauração cancelada.'));
99
- process.exit(0);
100
- }
101
-
102
- // 6. Executar restauração
103
- console.log(chalk.blue('\n🚀 Iniciando restauração...'));
104
-
105
- // 6.1 Database (se selecionado)
106
- if (components.database) {
107
- await restoreDatabaseGz(
108
- path.join(selectedBackup.path, selectedBackup.backupFile),
109
- targetProject.targetDatabaseUrl
110
- );
111
- }
112
-
113
- // 6.2 Edge Functions (se selecionado)
114
- if (components.edgeFunctions) {
115
- await restoreEdgeFunctions(selectedBackup.path, targetProject);
116
- }
117
-
118
- // 6.3 Auth Settings (se selecionado)
119
- if (components.authSettings) {
120
- await restoreAuthSettings(selectedBackup.path, targetProject);
121
- }
122
-
123
- // 6.4 Storage Buckets (se selecionado)
124
- if (components.storage) {
125
- await restoreStorageBuckets(selectedBackup.path, targetProject);
126
- }
127
-
128
- // 6.5 Database Settings (se selecionado)
129
- if (components.databaseSettings) {
130
- await restoreDatabaseSettings(selectedBackup.path, targetProject);
131
- }
132
-
133
- // 6.6 Realtime Settings (se selecionado)
134
- if (components.realtimeSettings) {
135
- await restoreRealtimeSettings(selectedBackup.path, targetProject);
136
- }
137
-
138
- // report.json de restauração
139
- const report = {
140
- process: 'restore',
141
- created_at: new Date().toISOString(),
142
- target_project_id: targetProject.targetProjectId,
143
- assets: {
144
- env: path.join(processDir, 'env', '.env.local'),
145
- env_map: path.join(processDir, 'env', 'env-map.json')
146
- },
147
- components: components,
148
- notes: [
149
- 'supabase/functions limpo antes e depois do deploy (se Edge Functions selecionado)'
150
- ]
151
- };
152
- try {
153
- require('fs').writeFileSync(path.join(processDir, 'report.json'), JSON.stringify(report, null, 2));
154
- } catch (e) {
155
- // silencioso
156
- }
157
-
158
- console.log(chalk.green('\n🎉 Restauração completa finalizada!'));
159
-
160
- } catch (error) {
161
- console.error(chalk.red(`❌ Erro na restauração: ${error.message}`));
162
- process.exit(1);
163
- }
164
- };
165
-
166
- // Listar backups válidos (aceita .backup.gz e .backup)
167
- async function listValidBackups(backupsDir) {
168
- if (!fs.existsSync(backupsDir)) {
169
- return [];
170
- }
171
-
172
- const items = fs.readdirSync(backupsDir, { withFileTypes: true });
173
- const validBackups = [];
174
-
175
- for (const item of items) {
176
- if (item.isDirectory() && item.name.startsWith('backup-')) {
177
- const backupPath = path.join(backupsDir, item.name);
178
- const files = fs.readdirSync(backupPath);
179
- // Aceitar tanto .backup.gz quanto .backup
180
- const backupFile = files.find(file =>
181
- file.endsWith('.backup.gz') || file.endsWith('.backup')
182
- );
183
-
184
- if (backupFile) {
185
- const manifestPath = path.join(backupPath, 'backup-manifest.json');
186
- let manifest = null;
187
-
188
- if (fs.existsSync(manifestPath)) {
189
- try {
190
- manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
191
- } catch (error) {
192
- // Ignorar erro de leitura do manifest
193
- }
194
- }
195
-
196
- const stats = fs.statSync(path.join(backupPath, backupFile));
197
-
198
- validBackups.push({
199
- name: item.name,
200
- path: backupPath,
201
- backupFile: backupFile,
202
- created: manifest?.created_at || stats.birthtime.toISOString(),
203
- projectId: manifest?.project_id || 'Desconhecido',
204
- size: formatBytes(stats.size),
205
- manifest: manifest
206
- });
207
- }
208
- }
209
- }
210
-
211
- return validBackups.sort((a, b) => new Date(b.created) - new Date(a.created));
212
- }
213
-
214
- // Formatar bytes
215
- function formatBytes(bytes) {
216
- if (bytes === 0) return '0 Bytes';
217
- const k = 1024;
218
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
219
- const i = Math.floor(Math.log(bytes) / Math.log(k));
220
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
221
- }
222
-
223
- // Seleção interativa de backup
224
- async function selectBackupInteractive(backups) {
225
- console.log(chalk.blue('\n📋 Backups disponíveis:'));
226
- console.log(chalk.blue('═'.repeat(80)));
227
-
228
- backups.forEach((backup, index) => {
229
- const date = new Date(backup.created).toLocaleString('pt-BR');
230
- const projectInfo = backup.projectId !== 'Desconhecido' ? ` (${backup.projectId})` : '';
231
-
232
- console.log(`${index + 1}. ${backup.name}${projectInfo}`);
233
- console.log(` 📅 ${date} | 📦 ${backup.size}`);
234
- console.log('');
235
- });
236
-
237
- const readline = require('readline').createInterface({
238
- input: process.stdin,
239
- output: process.stdout
240
- });
241
-
242
- const question = (query) => new Promise(resolve => readline.question(query, resolve));
243
-
244
- const choice = await question(`\nDigite o número do backup para restaurar (1-${backups.length}): `);
245
- readline.close();
246
-
247
- const backupIndex = parseInt(choice) - 1;
248
-
249
- if (backupIndex < 0 || backupIndex >= backups.length) {
250
- throw new Error('Número inválido');
251
- }
252
-
253
- return backups[backupIndex];
254
- }
255
-
256
- // Perguntar quais componentes restaurar
257
- async function askRestoreComponents(backupPath) {
258
- const questions = [];
259
-
260
- // Database
261
- questions.push({
262
- type: 'confirm',
263
- name: 'restoreDatabase',
264
- message: 'Deseja restaurar Database (S/n):',
265
- default: true
266
- });
267
-
268
- // Edge Functions
269
- const edgeFunctionsDir = path.join(backupPath, 'edge-functions');
270
- if (fs.existsSync(edgeFunctionsDir) && fs.readdirSync(edgeFunctionsDir).length > 0) {
271
- questions.push({
272
- type: 'confirm',
273
- name: 'restoreEdgeFunctions',
274
- message: 'Deseja restaurar Edge Functions (S/n):',
275
- default: true
276
- });
277
- }
278
-
279
- // Auth Settings
280
- if (fs.existsSync(path.join(backupPath, 'auth-settings.json'))) {
281
- questions.push({
282
- type: 'confirm',
283
- name: 'restoreAuthSettings',
284
- message: 'Deseja restaurar Auth Settings (s/N):',
285
- default: false
286
- });
287
- }
288
-
289
- // Storage Buckets
290
- const storageDir = path.join(backupPath, 'storage');
291
- if (fs.existsSync(storageDir) && fs.readdirSync(storageDir).length > 0) {
292
- questions.push({
293
- type: 'confirm',
294
- name: 'restoreStorage',
295
- message: 'Deseja ver informações de Storage Buckets (s/N):',
296
- default: false
297
- });
298
- }
299
-
300
- // Database Extensions and Settings
301
- const dbSettingsFiles = fs.readdirSync(backupPath)
302
- .filter(file => file.startsWith('database-settings-') && file.endsWith('.json'));
303
- if (dbSettingsFiles.length > 0) {
304
- questions.push({
305
- type: 'confirm',
306
- name: 'restoreDatabaseSettings',
307
- message: 'Deseja restaurar Database Extensions and Settings (s/N):',
308
- default: false
309
- });
310
- }
311
-
312
- // Realtime Settings
313
- if (fs.existsSync(path.join(backupPath, 'realtime-settings.json'))) {
314
- questions.push({
315
- type: 'confirm',
316
- name: 'restoreRealtimeSettings',
317
- message: 'Deseja restaurar Realtime Settings (s/N):',
318
- default: false
319
- });
320
- }
321
-
322
- const answers = await inquirer.prompt(questions);
323
-
324
- return {
325
- database: answers.restoreDatabase,
326
- edgeFunctions: answers.restoreEdgeFunctions || false,
327
- storage: answers.restoreStorage || false,
328
- authSettings: answers.restoreAuthSettings || false,
329
- databaseSettings: answers.restoreDatabaseSettings || false,
330
- realtimeSettings: answers.restoreRealtimeSettings || false
331
- };
332
- }
333
-
334
- // Mostrar resumo da restauração
335
- function showRestoreSummary(backup, components, targetProject) {
336
- console.log(chalk.blue('\n📋 Resumo da Restauração:'));
337
- console.log(chalk.blue('═'.repeat(80)));
338
- console.log(chalk.cyan(`📦 Backup: ${backup.name}`));
339
- console.log(chalk.cyan(`📤 Projeto Origem: ${backup.projectId}`));
340
- console.log(chalk.cyan(`📥 Projeto Destino: ${targetProject.targetProjectId}`));
341
- console.log('');
342
- console.log(chalk.cyan('Componentes que serão restaurados:'));
343
- console.log('');
344
-
345
- if (components.database) {
346
- console.log('✅ Database (psql -f via Docker)');
347
- }
348
-
349
- if (components.edgeFunctions) {
350
- const edgeFunctionsDir = path.join(backup.path, 'edge-functions');
351
- const functions = fs.readdirSync(edgeFunctionsDir).filter(item =>
352
- fs.statSync(path.join(edgeFunctionsDir, item)).isDirectory()
353
- );
354
- console.log(`⚡ Edge Functions: ${functions.length} function(s)`);
355
- functions.forEach(func => console.log(` - ${func}`));
356
- }
357
-
358
- if (components.authSettings) {
359
- console.log('🔐 Auth Settings: Exibir URL e valores para configuração manual');
360
- }
361
-
362
- if (components.storage) {
363
- console.log('📦 Storage Buckets: Exibir informações e instruções do Google Colab');
364
- }
365
-
366
- if (components.databaseSettings) {
367
- console.log('🔧 Database Extensions and Settings: Restaurar via SQL');
368
- }
369
-
370
- if (components.realtimeSettings) {
371
- console.log('🔄 Realtime Settings: Exibir URL e valores para configuração manual');
372
- }
373
-
374
- console.log('');
375
- }
376
-
377
- // Confirmar execução
378
- async function confirmExecution() {
379
- const readline = require('readline').createInterface({
380
- input: process.stdin,
381
- output: process.stdout
382
- });
383
-
384
- const question = (query) => new Promise(resolve => readline.question(query, resolve));
385
-
386
- const confirm = await question('Deseja continuar com a restauração? (s/N): ');
387
- readline.close();
388
-
389
- return confirm.toLowerCase() === 's';
390
- }
391
-
392
- // Restaurar Database via psql (conforme documentação oficial Supabase: https://supabase.com/docs/guides/platform/migrating-within-supabase/dashboard-restore)
393
- // Aceita tanto arquivos .backup.gz quanto .backup já descompactados
394
- async function restoreDatabaseGz(backupFilePath, targetDatabaseUrl) {
395
- console.log(chalk.blue('📊 Restaurando Database...'));
396
-
397
- try {
398
- const { execSync } = require('child_process');
399
-
400
- const backupDirAbs = path.resolve(path.dirname(backupFilePath));
401
- const fileName = path.basename(backupFilePath);
402
- let uncompressedFile = fileName;
403
-
404
- // Verificar se é arquivo .backup.gz (compactado) ou .backup (descompactado)
405
- if (fileName.endsWith('.backup.gz')) {
406
- console.log(chalk.gray(' - Arquivo .backup.gz detectado'));
407
- console.log(chalk.gray(' - Extraindo arquivo .gz...'));
408
-
409
- const unzipCmd = [
410
- 'docker run --rm',
411
- `-v "${backupDirAbs}:/host"`,
412
- 'postgres:17 gunzip /host/' + fileName
413
- ].join(' ');
414
-
415
- execSync(unzipCmd, { stdio: 'pipe' });
416
- uncompressedFile = fileName.replace('.gz', '');
417
- console.log(chalk.gray(' - Arquivo descompactado: ' + uncompressedFile));
418
- } else if (fileName.endsWith('.backup')) {
419
- console.log(chalk.gray(' - Arquivo .backup detectado (já descompactado)'));
420
- console.log(chalk.gray(' - Prosseguindo com restauração direta'));
421
- } else {
422
- throw new Error(`Formato de arquivo inválido. Esperado .backup.gz ou .backup, recebido: ${fileName}`);
423
- }
424
-
425
- // Extrair credenciais da URL de conexão
426
- const urlMatch = targetDatabaseUrl.match(/postgresql:\/\/([^@:]+):([^@]+)@(.+)$/);
427
-
428
- if (!urlMatch) {
429
- throw new Error('Database URL inválida. Formato esperado: postgresql://user:password@host/database');
430
- }
431
-
432
- // Comando psql conforme documentação oficial Supabase
433
- // Formato: psql -d [CONNECTION_STRING] -f /file/path
434
- // Referência: https://supabase.com/docs/guides/platform/migrating-within-supabase/dashboard-restore
435
- const restoreCmd = [
436
- 'docker run --rm --network host',
437
- `-v "${backupDirAbs}:/host"`,
438
- `-e PGPASSWORD="${encodeURIComponent(urlMatch[2])}"`,
439
- 'postgres:17 psql',
440
- `-d "${targetDatabaseUrl}"`,
441
- `-f /host/${uncompressedFile}`
442
- ].join(' ');
443
-
444
- console.log(chalk.gray(' - Executando psql via Docker...'));
445
- console.log(chalk.gray(' ℹ️ Seguindo documentação oficial Supabase'));
446
- console.log(chalk.yellow(' ⚠️ AVISO: Erros como "object already exists" são ESPERADOS'));
447
- console.log(chalk.yellow(' ⚠️ Isto acontece porque o backup contém CREATE para todos os schemas'));
448
- console.log(chalk.yellow(' ⚠️ Supabase já tem auth e storage criados, então esses erros são normais'));
449
-
450
- // Executar comando de restauração
451
- execSync(restoreCmd, { stdio: 'inherit', encoding: 'utf8' });
452
-
453
- console.log(chalk.green(' ✅ Database restaurada com sucesso!'));
454
- console.log(chalk.gray(' ℹ️ Erros "already exists" são normais e não afetam a restauração'));
455
-
456
- } catch (error) {
457
- // Erros esperados conforme documentação oficial Supabase
458
- // Referência: https://supabase.com/docs/guides/platform/migrating-within-supabase/dashboard-restore#common-errors
459
- if (error.message.includes('already exists') ||
460
- error.message.includes('constraint') ||
461
- error.message.includes('duplicate') ||
462
- error.stdout?.includes('already exists')) {
463
- console.log(chalk.yellow(' ⚠️ Erros esperados encontrados (conforme documentação Supabase)'));
464
- console.log(chalk.green(' ✅ Database restaurada com sucesso!'));
465
- console.log(chalk.gray(' ℹ️ Erros são ignorados pois são comandos de CREATE que já existem'));
466
- } else {
467
- console.error(chalk.red(` ❌ Erro inesperado na restauração: ${error.message}`));
468
- throw error;
469
- }
470
- }
471
- }
472
-
473
- // Restaurar Edge Functions via supabase functions deploy
474
- async function restoreEdgeFunctions(backupPath, targetProject) {
475
- console.log(chalk.blue('\n⚡ Restaurando Edge Functions...'));
476
-
477
- try {
478
- const fs = require('fs').promises;
479
- const { execSync } = require('child_process');
480
- const edgeFunctionsDir = path.join(backupPath, 'edge-functions');
481
-
482
- if (!await fs.access(edgeFunctionsDir).then(() => true).catch(() => false)) {
483
- console.log(chalk.yellow(' ⚠️ Nenhuma Edge Function encontrada no backup'));
484
- return;
485
- }
486
-
487
- const items = await fs.readdir(edgeFunctionsDir);
488
- const functions = [];
489
-
490
- for (const item of items) {
491
- const itemPath = path.join(edgeFunctionsDir, item);
492
- const stats = await fs.stat(itemPath);
493
- if (stats.isDirectory()) {
494
- functions.push(item);
495
- }
496
- }
497
-
498
- if (functions.length === 0) {
499
- console.log(chalk.yellow(' ⚠️ Nenhuma Edge Function encontrada no backup'));
500
- return;
501
- }
502
-
503
- console.log(chalk.gray(` - Encontradas ${functions.length} Edge Function(s)`));
504
-
505
- // ✅ COPIAR Edge Functions de backups/backup-XXX/edge-functions para supabase/functions
506
- const supabaseFunctionsDir = path.join(process.cwd(), 'supabase', 'functions');
507
-
508
- // Criar diretório supabase/functions se não existir
509
- await fs.mkdir(supabaseFunctionsDir, { recursive: true });
510
-
511
- // Limpar supabase/functions antes de copiar
512
- console.log(chalk.gray(' - Limpando supabase/functions...'));
513
- try {
514
- await fs.rm(supabaseFunctionsDir, { recursive: true, force: true });
515
- await fs.mkdir(supabaseFunctionsDir, { recursive: true });
516
- } catch (cleanError) {
517
- // Ignorar erro de limpeza se não existir
518
- }
519
-
520
- // Copiar cada Edge Function para supabase/functions
521
- for (const funcName of functions) {
522
- const backupFuncPath = path.join(edgeFunctionsDir, funcName);
523
- const targetFuncPath = path.join(supabaseFunctionsDir, funcName);
524
-
525
- console.log(chalk.gray(` - Copiando ${funcName} para supabase/functions...`));
526
-
527
- // Copiar recursivamente
528
- await copyDirectoryRecursive(backupFuncPath, targetFuncPath);
529
- }
530
-
531
- console.log(chalk.gray(` - Linkando com projeto ${targetProject.targetProjectId}...`));
532
-
533
- // Linkar com o projeto destino
534
- try {
535
- execSync(`supabase link --project-ref ${targetProject.targetProjectId}`, {
536
- stdio: 'pipe',
537
- encoding: 'utf8',
538
- timeout: 10000,
539
- env: { ...process.env, SUPABASE_ACCESS_TOKEN: targetProject.targetAccessToken || '' }
540
- });
541
- } catch (linkError) {
542
- console.log(chalk.yellow(' ⚠️ Link pode já existir, continuando...'));
543
- }
544
-
545
- // Deploy das Edge Functions
546
- for (const funcName of functions) {
547
- console.log(chalk.gray(` - Deployando ${funcName}...`));
548
-
549
- try {
550
- execSync(`supabase functions deploy ${funcName}`, {
551
- cwd: process.cwd(),
552
- stdio: 'pipe',
553
- encoding: 'utf8',
554
- timeout: 120000,
555
- env: { ...process.env, SUPABASE_ACCESS_TOKEN: targetProject.targetAccessToken || '' }
556
- });
557
-
558
- console.log(chalk.green(` ✅ ${funcName} deployada com sucesso!`));
559
- } catch (deployError) {
560
- console.log(chalk.yellow(` ⚠️ ${funcName} - deploy falhou: ${deployError.message}`));
561
- }
562
- }
563
-
564
- // Limpar supabase/functions após deploy
565
- console.log(chalk.gray(' - Limpando supabase/functions após deploy...'));
566
- try {
567
- await fs.rm(supabaseFunctionsDir, { recursive: true, force: true });
568
- } catch (cleanError) {
569
- // Ignorar erro de limpeza
570
- }
571
-
572
- console.log(chalk.green(' ✅ Edge Functions restauradas com sucesso!'));
573
-
574
- } catch (error) {
575
- console.error(chalk.red(` ❌ Erro ao restaurar Edge Functions: ${error.message}`));
576
- }
577
- }
578
-
579
- // Função auxiliar para copiar diretório recursivamente
580
- async function copyDirectoryRecursive(src, dest) {
581
- const fs = require('fs').promises;
582
-
583
- await fs.mkdir(dest, { recursive: true });
584
-
585
- const entries = await fs.readdir(src, { withFileTypes: true });
586
-
587
- for (const entry of entries) {
588
- const srcPath = path.join(src, entry.name);
589
- const destPath = path.join(dest, entry.name);
590
-
591
- if (entry.isDirectory()) {
592
- await copyDirectoryRecursive(srcPath, destPath);
593
- } else {
594
- await fs.copyFile(srcPath, destPath);
595
- }
596
- }
597
- }
598
-
599
- // Restaurar Storage Buckets (interativo - exibir informações)
600
- async function restoreStorageBuckets(backupPath, targetProject) {
601
- console.log(chalk.blue('\n📦 Restaurando Storage Buckets...'));
602
-
603
- try {
604
- const storageDir = path.join(backupPath, 'storage');
605
-
606
- if (!fs.existsSync(storageDir)) {
607
- console.log(chalk.yellow(' ⚠️ Nenhum bucket de Storage encontrado no backup'));
608
- return;
609
- }
610
-
611
- const manifestPath = path.join(backupPath, 'backup-manifest.json');
612
- let manifest = null;
613
-
614
- if (fs.existsSync(manifestPath)) {
615
- manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
616
- }
617
-
618
- const buckets = manifest?.components?.storage?.buckets || [];
619
-
620
- if (buckets.length === 0) {
621
- console.log(chalk.gray(' ℹ️ Nenhum bucket para restaurar'));
622
- return;
623
- }
624
-
625
- console.log(chalk.green(`\n ✅ ${buckets.length} bucket(s) encontrado(s) no backup`));
626
- buckets.forEach(bucket => {
627
- console.log(chalk.gray(` - ${bucket.name} (${bucket.public ? 'público' : 'privado'})`));
628
- });
629
-
630
- const colabUrl = 'https://colab.research.google.com/github/PLyn/supabase-storage-migrate/blob/main/Supabase_Storage_migration.ipynb';
631
-
632
- console.log(chalk.yellow('\n ⚠️ Migração de objetos de Storage requer processo manual'));
633
- console.log(chalk.cyan(` ℹ️ Use o script do Google Colab: ${colabUrl}`));
634
- console.log(chalk.gray('\n 📋 Instruções:'));
635
- console.log(chalk.gray(' 1. Execute o script no Google Colab'));
636
- console.log(chalk.gray(' 2. Configure as credenciais dos projetos (origem e destino)'));
637
- console.log(chalk.gray(' 3. Execute a migração'));
638
-
639
- await inquirer.prompt([{
640
- type: 'input',
641
- name: 'continue',
642
- message: 'Pressione Enter para continuar'
643
- }]);
644
-
645
- } catch (error) {
646
- console.error(chalk.red(` ❌ Erro ao processar Storage: ${error.message}`));
647
- }
648
- }
649
-
650
- // Restaurar Auth Settings (interativo - exibir URL e valores)
651
- async function restoreAuthSettings(backupPath, targetProject) {
652
- console.log(chalk.blue('\n🔐 Restaurando Auth Settings...'));
653
-
654
- try {
655
- const authSettingsPath = path.join(backupPath, 'auth-settings.json');
656
-
657
- if (!fs.existsSync(authSettingsPath)) {
658
- console.log(chalk.yellow(' ⚠️ Nenhuma configuração de Auth encontrada no backup'));
659
- return;
660
- }
661
-
662
- const authSettings = JSON.parse(fs.readFileSync(authSettingsPath, 'utf8'));
663
- const dashboardUrl = `https://supabase.com/dashboard/project/${targetProject.targetProjectId}/auth/url-config`;
664
-
665
- console.log(chalk.green('\n ✅ URL para configuração manual:'));
666
- console.log(chalk.cyan(` ${dashboardUrl}`));
667
- console.log(chalk.yellow('\n 📋 Configure manualmente as seguintes opções:'));
668
-
669
- if (authSettings.auth_url_config) {
670
- Object.entries(authSettings.auth_url_config).forEach(([key, value]) => {
671
- console.log(chalk.gray(` - ${key}: ${value}`));
672
- });
673
- }
674
-
675
- console.log(chalk.yellow('\n ⚠️ Após configurar, pressione Enter para continuar...'));
676
-
677
- await inquirer.prompt([{
678
- type: 'input',
679
- name: 'continue',
680
- message: 'Pressione Enter para continuar'
681
- }]);
682
-
683
- console.log(chalk.green(' ✅ Auth Settings processados'));
684
-
685
- } catch (error) {
686
- console.error(chalk.red(` ❌ Erro ao processar Auth Settings: ${error.message}`));
687
- }
688
- }
689
-
690
- // Restaurar Database Settings (via SQL)
691
- async function restoreDatabaseSettings(backupPath, targetProject) {
692
- console.log(chalk.blue('\n🔧 Restaurando Database Settings...'));
693
-
694
- try {
695
- const files = fs.readdirSync(backupPath);
696
- const dbSettingsFile = files.find(f => f.startsWith('database-settings-') && f.endsWith('.json'));
697
-
698
- if (!dbSettingsFile) {
699
- console.log(chalk.yellow(' ⚠️ Nenhuma configuração de Database encontrada no backup'));
700
- return;
701
- }
702
-
703
- const dbSettings = JSON.parse(fs.readFileSync(path.join(backupPath, dbSettingsFile), 'utf8'));
704
- const { execSync } = require('child_process');
705
-
706
- if (dbSettings.extensions && dbSettings.extensions.length > 0) {
707
- console.log(chalk.gray(` - Habilitando ${dbSettings.extensions.length} extension(s)...`));
708
-
709
- for (const ext of dbSettings.extensions) {
710
- console.log(chalk.gray(` - ${ext}`));
711
-
712
- const sqlCommand = `CREATE EXTENSION IF NOT EXISTS ${ext};`;
713
-
714
- const urlMatch = targetProject.targetDatabaseUrl.match(/postgresql:\/\/([^@:]+):([^@]+)@(.+)$/);
715
-
716
- if (!urlMatch) {
717
- console.log(chalk.yellow(` ⚠️ URL inválida para ${ext}`));
718
- continue;
719
- }
720
-
721
- const dockerCmd = [
722
- 'docker run --rm',
723
- '--network host',
724
- `-e PGPASSWORD="${encodeURIComponent(urlMatch[2])}"`,
725
- 'postgres:17 psql',
726
- `-d "${targetProject.targetDatabaseUrl}"`,
727
- `-c "${sqlCommand}"`
728
- ].join(' ');
729
-
730
- try {
731
- execSync(dockerCmd, { stdio: 'pipe', encoding: 'utf8' });
732
- } catch (sqlError) {
733
- console.log(chalk.yellow(` ⚠️ ${ext} - extension já existe ou não pode ser habilitada`));
734
- }
735
- }
736
- }
737
-
738
- console.log(chalk.green(' ✅ Database Settings restaurados com sucesso!'));
739
-
740
- } catch (error) {
741
- console.error(chalk.red(` ❌ Erro ao restaurar Database Settings: ${error.message}`));
742
- }
743
- }
744
-
745
- // Restaurar Realtime Settings (interativo - exibir URL e valores)
746
- async function restoreRealtimeSettings(backupPath, targetProject) {
747
- console.log(chalk.blue('\n🔄 Restaurando Realtime Settings...'));
748
-
749
- try {
750
- const realtimeSettingsPath = path.join(backupPath, 'realtime-settings.json');
751
-
752
- if (!fs.existsSync(realtimeSettingsPath)) {
753
- console.log(chalk.yellow(' ⚠️ Nenhuma configuração de Realtime encontrada no backup'));
754
- return;
755
- }
756
-
757
- const realtimeSettings = JSON.parse(fs.readFileSync(realtimeSettingsPath, 'utf8'));
758
- const dashboardUrl = `https://supabase.com/dashboard/project/${targetProject.targetProjectId}/realtime/settings`;
759
-
760
- console.log(chalk.green('\n ✅ URL para configuração manual:'));
761
- console.log(chalk.cyan(` ${dashboardUrl}`));
762
- console.log(chalk.yellow('\n 📋 Configure manualmente as seguintes opções:'));
763
-
764
- if (realtimeSettings.realtime_settings?.settings) {
765
- Object.entries(realtimeSettings.realtime_settings.settings).forEach(([key, setting]) => {
766
- console.log(chalk.gray(` - ${setting.label}: ${setting.value}`));
767
- if (setting.description) {
768
- console.log(chalk.gray(` ${setting.description}`));
769
- }
770
- });
771
- }
772
-
773
- console.log(chalk.yellow('\n ⚠️ Após configurar, pressione Enter para continuar...'));
774
-
775
- await inquirer.prompt([{
776
- type: 'input',
777
- name: 'continue',
778
- message: 'Pressione Enter para continuar'
779
- }]);
780
-
781
- console.log(chalk.green(' ✅ Realtime Settings processados'));
782
-
783
- } catch (error) {
784
- console.error(chalk.red(` ❌ Erro ao processar Realtime Settings: ${error.message}`));
785
- }
786
- }