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
@@ -0,0 +1,206 @@
1
+ const chalk = require('chalk');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const { readEnvFile, writeEnvFile, backupEnvFile } = require('../../utils/env');
5
+ const { saveEnvMap } = require('../../utils/envMap');
6
+ const { mapEnvVariablesInteractively } = require('../../interactive/envMapper');
7
+ const { showBetaBanner } = require('../../utils/banner');
8
+ const { listValidBackups, showRestoreSummary } = require('./utils');
9
+ const { confirm } = require('../../utils/prompt');
10
+ const step00DockerValidation = require('../backup/steps/00-docker-validation');
11
+
12
+ // Importar todas as etapas
13
+ const step00BackupSelection = require('./steps/00-backup-selection');
14
+ const step01ComponentsSelection = require('./steps/01-components-selection');
15
+ const step03Database = require('./steps/03-database');
16
+ const step04EdgeFunctions = require('./steps/04-edge-functions');
17
+ const step05AuthSettings = require('./steps/05-auth-settings');
18
+ const step06Storage = require('./steps/06-storage');
19
+ const step07DatabaseSettings = require('./steps/07-database-settings');
20
+ const step08RealtimeSettings = require('./steps/08-realtime-settings');
21
+
22
+ module.exports = async (_options) => {
23
+ showBetaBanner();
24
+
25
+ try {
26
+ // Executar validação Docker ANTES de tudo
27
+ await step00DockerValidation();
28
+
29
+ // Consentimento para leitura e escrita do .env.local
30
+ console.log(chalk.yellow('\n⚠️ O smoonb irá ler e escrever o arquivo .env.local localmente.'));
31
+ console.log(chalk.yellow(' Um backup automático do .env.local será criado antes de qualquer alteração.'));
32
+ console.log(chalk.yellow(' Vamos mapear suas variáveis de ambiente para garantir que todas as chaves necessárias'));
33
+ console.log(chalk.yellow(' estejam presentes e com os valores corretos do projeto de destino.'));
34
+ const consentOk = await confirm('Você consente em prosseguir', true);
35
+ if (!consentOk) {
36
+ console.log(chalk.red('🚫 Operação cancelada pelo usuário.'));
37
+ process.exit(1);
38
+ }
39
+
40
+ // Preparar diretório de processo restore-YYYY-...
41
+ const rootBackupsDir = path.join(process.cwd(), 'backups');
42
+ const now = new Date();
43
+ 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')}`;
44
+ const processDir = path.join(rootBackupsDir, `restore-${ts}`);
45
+ fs.mkdirSync(path.join(processDir, 'env'), { recursive: true });
46
+
47
+ // Backup do .env.local
48
+ const envPath = path.join(process.cwd(), '.env.local');
49
+ const envBackupPath = path.join(processDir, 'env', '.env.local');
50
+ await backupEnvFile(envPath, envBackupPath);
51
+ console.log(chalk.blue(`📁 Backup do .env.local: ${path.relative(process.cwd(), envBackupPath)}`));
52
+
53
+ // Leitura e mapeamento interativo
54
+ const currentEnv = await readEnvFile(envPath);
55
+ const expectedKeys = [
56
+ 'NEXT_PUBLIC_SUPABASE_URL',
57
+ 'NEXT_PUBLIC_SUPABASE_ANON_KEY',
58
+ 'SUPABASE_SERVICE_ROLE_KEY',
59
+ 'SUPABASE_DB_URL',
60
+ 'SUPABASE_PROJECT_ID',
61
+ 'SUPABASE_ACCESS_TOKEN',
62
+ 'SMOONB_OUTPUT_DIR'
63
+ ];
64
+ const { finalEnv, dePara } = await mapEnvVariablesInteractively(currentEnv, expectedKeys);
65
+ await writeEnvFile(envPath, finalEnv);
66
+ await saveEnvMap(dePara, path.join(processDir, 'env', 'env-map.json'));
67
+ console.log(chalk.green('✅ .env.local atualizado com sucesso. Nenhuma chave renomeada; valores sincronizados.'));
68
+
69
+ // Resolver valores esperados a partir do de-para
70
+ function getValue(expectedKey) {
71
+ const clientKey = Object.keys(dePara).find(k => dePara[k] === expectedKey);
72
+ return clientKey ? finalEnv[clientKey] : '';
73
+ }
74
+
75
+ // Construir targetProject a partir do .env.local mapeado
76
+ const targetProject = {
77
+ targetProjectId: getValue('SUPABASE_PROJECT_ID'),
78
+ targetUrl: getValue('NEXT_PUBLIC_SUPABASE_URL'),
79
+ targetAnonKey: getValue('NEXT_PUBLIC_SUPABASE_ANON_KEY'),
80
+ targetServiceKey: getValue('SUPABASE_SERVICE_ROLE_KEY'),
81
+ targetDatabaseUrl: getValue('SUPABASE_DB_URL'),
82
+ targetAccessToken: getValue('SUPABASE_ACCESS_TOKEN')
83
+ };
84
+
85
+ console.log(chalk.blue(`📁 Buscando backups em: ${getValue('SMOONB_OUTPUT_DIR') || './backups'}`));
86
+
87
+ // 1. Listar backups válidos (.backup.gz)
88
+ const validBackups = await listValidBackups(getValue('SMOONB_OUTPUT_DIR') || './backups');
89
+
90
+ if (validBackups.length === 0) {
91
+ console.error(chalk.red('❌ Nenhum backup válido encontrado'));
92
+ console.log(chalk.yellow('💡 Execute primeiro: npx smoonb backup'));
93
+ process.exit(1);
94
+ }
95
+
96
+ // 2. Selecionar backup interativamente
97
+ const selectedBackup = await step00BackupSelection(validBackups);
98
+
99
+ // 3. Perguntar quais componentes restaurar
100
+ const components = await step01ComponentsSelection(selectedBackup.path);
101
+
102
+ // Validar que pelo menos um componente foi selecionado
103
+ if (!Object.values(components).some(Boolean)) {
104
+ console.error(chalk.red('\n❌ Nenhum componente selecionado para restauração!'));
105
+ process.exit(1);
106
+ }
107
+
108
+ // 4. Mostrar resumo detalhado
109
+ console.log(chalk.cyan('\n📋 RESUMO DA RESTAURAÇÃO:\n'));
110
+ console.log(chalk.gray(` 📁 Backup selecionado: ${path.basename(selectedBackup.path)}`));
111
+ console.log(chalk.gray(` 🎯 Projeto destino: ${targetProject.targetProjectId || '(não configurado)'}`));
112
+ console.log(chalk.gray(` 📊 Database: ${components.database ? 'Sim' : 'Não'}`));
113
+ console.log(chalk.gray(` ⚡ Edge Functions: ${components.edgeFunctions ? 'Sim' : 'Não'}`));
114
+ console.log(chalk.gray(` 🔐 Auth Settings: ${components.authSettings ? 'Sim' : 'Não'}`));
115
+ console.log(chalk.gray(` 📦 Storage: ${components.storage ? 'Sim' : 'Não'}`));
116
+ console.log(chalk.gray(` 🔧 Database Settings: ${components.databaseSettings ? 'Sim' : 'Não'}`));
117
+ console.log(chalk.gray(` 🔄 Realtime Settings: ${components.realtimeSettings ? 'Sim' : 'Não'}\n`));
118
+
119
+ // Mostrar resumo técnico adicional
120
+ showRestoreSummary(selectedBackup, components, targetProject);
121
+
122
+ // 5. Confirmar execução
123
+ const finalOk = await confirm('Deseja iniciar a restauração com estas configurações?', true);
124
+ if (!finalOk) {
125
+ console.log(chalk.yellow('🚫 Restauração cancelada.'));
126
+ process.exit(0);
127
+ }
128
+
129
+ // 6. Executar restauração
130
+ console.log(chalk.blue('\n🚀 Iniciando restauração...'));
131
+
132
+ // 6.1 Database (se selecionado)
133
+ if (components.database) {
134
+ await step03Database({
135
+ backupFilePath: path.join(selectedBackup.path, selectedBackup.backupFile),
136
+ targetDatabaseUrl: targetProject.targetDatabaseUrl
137
+ });
138
+ }
139
+
140
+ // 6.2 Edge Functions (se selecionado)
141
+ if (components.edgeFunctions) {
142
+ await step04EdgeFunctions({
143
+ backupPath: selectedBackup.path,
144
+ targetProject
145
+ });
146
+ }
147
+
148
+ // 6.3 Auth Settings (se selecionado)
149
+ if (components.authSettings) {
150
+ await step05AuthSettings({
151
+ backupPath: selectedBackup.path,
152
+ targetProject
153
+ });
154
+ }
155
+
156
+ // 6.4 Storage Buckets (se selecionado)
157
+ if (components.storage) {
158
+ await step06Storage({
159
+ backupPath: selectedBackup.path
160
+ });
161
+ }
162
+
163
+ // 6.5 Database Settings (se selecionado)
164
+ if (components.databaseSettings) {
165
+ await step07DatabaseSettings({
166
+ backupPath: selectedBackup.path,
167
+ targetProject
168
+ });
169
+ }
170
+
171
+ // 6.6 Realtime Settings (se selecionado)
172
+ if (components.realtimeSettings) {
173
+ await step08RealtimeSettings({
174
+ backupPath: selectedBackup.path,
175
+ targetProject
176
+ });
177
+ }
178
+
179
+ // report.json de restauração
180
+ const report = {
181
+ process: 'restore',
182
+ created_at: new Date().toISOString(),
183
+ target_project_id: targetProject.targetProjectId,
184
+ assets: {
185
+ env: path.join(processDir, 'env', '.env.local'),
186
+ env_map: path.join(processDir, 'env', 'env-map.json')
187
+ },
188
+ components: components,
189
+ notes: [
190
+ 'supabase/functions limpo antes e depois do deploy (se Edge Functions selecionado)'
191
+ ]
192
+ };
193
+ try {
194
+ fs.writeFileSync(path.join(processDir, 'report.json'), JSON.stringify(report, null, 2));
195
+ } catch {
196
+ // silencioso
197
+ }
198
+
199
+ console.log(chalk.green('\n🎉 Restauração completa finalizada!'));
200
+
201
+ } catch (error) {
202
+ console.error(chalk.red(`❌ Erro na restauração: ${error.message}`));
203
+ process.exit(1);
204
+ }
205
+ };
206
+
@@ -0,0 +1,38 @@
1
+ const chalk = require('chalk');
2
+ const readline = require('readline');
3
+
4
+ /**
5
+ * Etapa 0: Seleção interativa de backup
6
+ */
7
+ module.exports = async (backups) => {
8
+ console.log(chalk.blue('\n📋 Backups disponíveis:'));
9
+ console.log(chalk.blue('═'.repeat(80)));
10
+
11
+ backups.forEach((backup, index) => {
12
+ const date = new Date(backup.created).toLocaleString('pt-BR');
13
+ const projectInfo = backup.projectId !== 'Desconhecido' ? ` (${backup.projectId})` : '';
14
+
15
+ console.log(`${index + 1}. ${backup.name}${projectInfo}`);
16
+ console.log(` 📅 ${date} | 📦 ${backup.size}`);
17
+ console.log('');
18
+ });
19
+
20
+ const rl = readline.createInterface({
21
+ input: process.stdin,
22
+ output: process.stdout
23
+ });
24
+
25
+ const question = (query) => new Promise(resolve => rl.question(query, resolve));
26
+
27
+ const choice = await question(`\nDigite o número do backup para restaurar (1-${backups.length}): `);
28
+ rl.close();
29
+
30
+ const backupIndex = parseInt(choice) - 1;
31
+
32
+ if (backupIndex < 0 || backupIndex >= backups.length) {
33
+ throw new Error('Número inválido');
34
+ }
35
+
36
+ return backups[backupIndex];
37
+ };
38
+
@@ -0,0 +1,71 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const chalk = require('chalk');
4
+ const { confirm } = require('../../utils/prompt');
5
+
6
+ /**
7
+ * Etapa 1: Perguntar quais componentes restaurar
8
+ */
9
+ module.exports = async (backupPath) => {
10
+ // Database (sempre disponível)
11
+ const restoreDatabase = await confirm('Deseja restaurar Database', true);
12
+
13
+ // Edge Functions
14
+ const edgeFunctionsDir = path.join(backupPath, 'edge-functions');
15
+ let restoreEdgeFunctions = false;
16
+ if (fs.existsSync(edgeFunctionsDir) && fs.readdirSync(edgeFunctionsDir).length > 0) {
17
+ console.log(chalk.cyan('\n⚡ Edge Functions:'));
18
+ console.log(chalk.gray(' As Edge Functions serão copiadas para supabase/functions e implantadas no projeto destino.'));
19
+ console.log(chalk.gray(' A pasta supabase/functions será limpa antes do processo.\n'));
20
+ restoreEdgeFunctions = await confirm('Deseja restaurar Edge Functions', true);
21
+ }
22
+
23
+ // Auth Settings
24
+ let restoreAuthSettings = false;
25
+ if (fs.existsSync(path.join(backupPath, 'auth-settings.json'))) {
26
+ console.log(chalk.cyan('\n🔐 Auth Settings:'));
27
+ console.log(chalk.gray(' As configurações de Auth serão exibidas para configuração manual no Dashboard.'));
28
+ console.log(chalk.gray(' Algumas configurações não podem ser aplicadas automaticamente por questões de segurança.\n'));
29
+ restoreAuthSettings = await confirm('Deseja ver as configurações de Auth Settings', true);
30
+ }
31
+
32
+ // Storage Buckets
33
+ const storageDir = path.join(backupPath, 'storage');
34
+ let restoreStorage = false;
35
+ if (fs.existsSync(storageDir) && fs.readdirSync(storageDir).length > 0) {
36
+ console.log(chalk.cyan('\n📦 Storage:'));
37
+ console.log(chalk.gray(' As informações dos buckets de Storage serão exibidas para migração manual.'));
38
+ console.log(chalk.gray(' Os arquivos precisam ser migrados manualmente usando as ferramentas do Supabase.\n'));
39
+ restoreStorage = await confirm('Deseja ver informações de Storage Buckets', true);
40
+ }
41
+
42
+ // Database Extensions and Settings
43
+ const dbSettingsFiles = fs.readdirSync(backupPath)
44
+ .filter(file => file.startsWith('database-settings-') && file.endsWith('.json'));
45
+ let restoreDatabaseSettings = false;
46
+ if (dbSettingsFiles.length > 0) {
47
+ console.log(chalk.cyan('\n🔧 Database Extensions and Settings:'));
48
+ console.log(chalk.gray(' As extensões e configurações do banco de dados serão restauradas via SQL.'));
49
+ console.log(chalk.gray(' Isso inclui extensões PostgreSQL e configurações específicas do projeto.\n'));
50
+ restoreDatabaseSettings = await confirm('Deseja restaurar Database Extensions and Settings', true);
51
+ }
52
+
53
+ // Realtime Settings
54
+ let restoreRealtimeSettings = false;
55
+ if (fs.existsSync(path.join(backupPath, 'realtime-settings.json'))) {
56
+ console.log(chalk.cyan('\n🔄 Realtime Settings:'));
57
+ console.log(chalk.gray(' As configurações de Realtime serão exibidas para configuração manual no Dashboard.'));
58
+ console.log(chalk.gray(' Algumas configurações precisam ser aplicadas manualmente.\n'));
59
+ restoreRealtimeSettings = await confirm('Deseja ver as configurações de Realtime Settings', true);
60
+ }
61
+
62
+ return {
63
+ database: restoreDatabase,
64
+ edgeFunctions: restoreEdgeFunctions,
65
+ storage: restoreStorage,
66
+ authSettings: restoreAuthSettings,
67
+ databaseSettings: restoreDatabaseSettings,
68
+ realtimeSettings: restoreRealtimeSettings
69
+ };
70
+ };
71
+
@@ -0,0 +1,14 @@
1
+ // Este arquivo não é mais usado diretamente
2
+ // A confirmação agora é feita no index.js com resumo detalhado
3
+ // Mantido para compatibilidade, mas pode ser removido no futuro
4
+
5
+ const { confirm } = require('../../utils/prompt');
6
+
7
+ /**
8
+ * Etapa 2: Confirmar execução (LEGACY - não usado mais)
9
+ * A confirmação agora é feita no index.js após mostrar resumo detalhado
10
+ */
11
+ module.exports = async () => {
12
+ return await confirm('Deseja continuar com a restauração?', true);
13
+ };
14
+
@@ -0,0 +1,81 @@
1
+ const chalk = require('chalk');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+
5
+ /**
6
+ * Etapa 3: Restaurar Database via psql
7
+ */
8
+ module.exports = async ({ backupFilePath, targetDatabaseUrl }) => {
9
+ console.log(chalk.blue('📊 Restaurando Database...'));
10
+
11
+ try {
12
+ const backupDirAbs = path.resolve(path.dirname(backupFilePath));
13
+ const fileName = path.basename(backupFilePath);
14
+ let uncompressedFile = fileName;
15
+
16
+ // Verificar se é arquivo .backup.gz (compactado) ou .backup (descompactado)
17
+ if (fileName.endsWith('.backup.gz')) {
18
+ console.log(chalk.gray(' - Arquivo .backup.gz detectado'));
19
+ console.log(chalk.gray(' - Extraindo arquivo .gz...'));
20
+
21
+ const unzipCmd = [
22
+ 'docker run --rm',
23
+ `-v "${backupDirAbs}:/host"`,
24
+ 'postgres:17 gunzip /host/' + fileName
25
+ ].join(' ');
26
+
27
+ execSync(unzipCmd, { stdio: 'pipe' });
28
+ uncompressedFile = fileName.replace('.gz', '');
29
+ console.log(chalk.gray(' - Arquivo descompactado: ' + uncompressedFile));
30
+ } else if (fileName.endsWith('.backup')) {
31
+ console.log(chalk.gray(' - Arquivo .backup detectado (já descompactado)'));
32
+ console.log(chalk.gray(' - Prosseguindo com restauração direta'));
33
+ } else {
34
+ throw new Error(`Formato de arquivo inválido. Esperado .backup.gz ou .backup, recebido: ${fileName}`);
35
+ }
36
+
37
+ // Extrair credenciais da URL de conexão
38
+ const urlMatch = targetDatabaseUrl.match(/postgresql:\/\/([^@:]+):([^@]+)@(.+)$/);
39
+
40
+ if (!urlMatch) {
41
+ throw new Error('Database URL inválida. Formato esperado: postgresql://user:password@host/database');
42
+ }
43
+
44
+ // Comando psql conforme documentação oficial Supabase
45
+ const restoreCmd = [
46
+ 'docker run --rm --network host',
47
+ `-v "${backupDirAbs}:/host"`,
48
+ `-e PGPASSWORD="${encodeURIComponent(urlMatch[2])}"`,
49
+ 'postgres:17 psql',
50
+ `-d "${targetDatabaseUrl}"`,
51
+ `-f /host/${uncompressedFile}`
52
+ ].join(' ');
53
+
54
+ console.log(chalk.gray(' - Executando psql via Docker...'));
55
+ console.log(chalk.gray(' ℹ️ Seguindo documentação oficial Supabase'));
56
+ console.log(chalk.yellow(' ⚠️ AVISO: Erros como "object already exists" são ESPERADOS'));
57
+ console.log(chalk.yellow(' ⚠️ Isto acontece porque o backup contém CREATE para todos os schemas'));
58
+ console.log(chalk.yellow(' ⚠️ Supabase já tem auth e storage criados, então esses erros são normais'));
59
+
60
+ // Executar comando de restauração
61
+ execSync(restoreCmd, { stdio: 'inherit', encoding: 'utf8' });
62
+
63
+ console.log(chalk.green(' ✅ Database restaurada com sucesso!'));
64
+ console.log(chalk.gray(' ℹ️ Erros "already exists" são normais e não afetam a restauração'));
65
+
66
+ } catch (error) {
67
+ // Erros esperados conforme documentação oficial Supabase
68
+ if (error.message.includes('already exists') ||
69
+ error.message.includes('constraint') ||
70
+ error.message.includes('duplicate') ||
71
+ error.stdout?.includes('already exists')) {
72
+ console.log(chalk.yellow(' ⚠️ Erros esperados encontrados (conforme documentação Supabase)'));
73
+ console.log(chalk.green(' ✅ Database restaurada com sucesso!'));
74
+ console.log(chalk.gray(' ℹ️ Erros são ignorados pois são comandos de CREATE que já existem'));
75
+ } else {
76
+ console.error(chalk.red(` ❌ Erro inesperado na restauração: ${error.message}`));
77
+ throw error;
78
+ }
79
+ }
80
+ };
81
+
@@ -0,0 +1,112 @@
1
+ const chalk = require('chalk');
2
+ const path = require('path');
3
+ const fs = require('fs').promises;
4
+ const { execSync } = require('child_process');
5
+ const { copyDirectoryRecursive } = require('../utils');
6
+
7
+ /**
8
+ * Etapa 4: Restaurar Edge Functions via supabase functions deploy
9
+ */
10
+ module.exports = async ({ backupPath, targetProject }) => {
11
+ console.log(chalk.blue('\n⚡ Restaurando Edge Functions...'));
12
+
13
+ try {
14
+ const edgeFunctionsDir = path.join(backupPath, 'edge-functions');
15
+
16
+ if (!await fs.access(edgeFunctionsDir).then(() => true).catch(() => false)) {
17
+ console.log(chalk.yellow(' ⚠️ Nenhuma Edge Function encontrada no backup'));
18
+ return;
19
+ }
20
+
21
+ const items = await fs.readdir(edgeFunctionsDir);
22
+ const functions = [];
23
+
24
+ for (const item of items) {
25
+ const itemPath = path.join(edgeFunctionsDir, item);
26
+ const stats = await fs.stat(itemPath);
27
+ if (stats.isDirectory()) {
28
+ functions.push(item);
29
+ }
30
+ }
31
+
32
+ if (functions.length === 0) {
33
+ console.log(chalk.yellow(' ⚠️ Nenhuma Edge Function encontrada no backup'));
34
+ return;
35
+ }
36
+
37
+ console.log(chalk.gray(` - Encontradas ${functions.length} Edge Function(s)`));
38
+
39
+ // COPIAR Edge Functions de backups/backup-XXX/edge-functions para supabase/functions
40
+ const supabaseFunctionsDir = path.join(process.cwd(), 'supabase', 'functions');
41
+
42
+ // Criar diretório supabase/functions se não existir
43
+ await fs.mkdir(supabaseFunctionsDir, { recursive: true });
44
+
45
+ // Limpar supabase/functions antes de copiar
46
+ console.log(chalk.gray(' - Limpando supabase/functions...'));
47
+ try {
48
+ await fs.rm(supabaseFunctionsDir, { recursive: true, force: true });
49
+ await fs.mkdir(supabaseFunctionsDir, { recursive: true });
50
+ } catch {
51
+ // Ignorar erro de limpeza se não existir
52
+ }
53
+
54
+ // Copiar cada Edge Function para supabase/functions
55
+ for (const funcName of functions) {
56
+ const backupFuncPath = path.join(edgeFunctionsDir, funcName);
57
+ const targetFuncPath = path.join(supabaseFunctionsDir, funcName);
58
+
59
+ console.log(chalk.gray(` - Copiando ${funcName} para supabase/functions...`));
60
+
61
+ // Copiar recursivamente
62
+ await copyDirectoryRecursive(backupFuncPath, targetFuncPath);
63
+ }
64
+
65
+ console.log(chalk.gray(` - Linkando com projeto ${targetProject.targetProjectId}...`));
66
+
67
+ // Linkar com o projeto destino
68
+ try {
69
+ execSync(`supabase link --project-ref ${targetProject.targetProjectId}`, {
70
+ stdio: 'pipe',
71
+ encoding: 'utf8',
72
+ timeout: 10000,
73
+ env: { ...process.env, SUPABASE_ACCESS_TOKEN: targetProject.targetAccessToken || '' }
74
+ });
75
+ } catch {
76
+ console.log(chalk.yellow(' ⚠️ Link pode já existir, continuando...'));
77
+ }
78
+
79
+ // Deploy das Edge Functions
80
+ for (const funcName of functions) {
81
+ console.log(chalk.gray(` - Deployando ${funcName}...`));
82
+
83
+ try {
84
+ execSync(`supabase functions deploy ${funcName}`, {
85
+ cwd: process.cwd(),
86
+ stdio: 'pipe',
87
+ encoding: 'utf8',
88
+ timeout: 120000,
89
+ env: { ...process.env, SUPABASE_ACCESS_TOKEN: targetProject.targetAccessToken || '' }
90
+ });
91
+
92
+ console.log(chalk.green(` ✅ ${funcName} deployada com sucesso!`));
93
+ } catch (deployError) {
94
+ console.log(chalk.yellow(` ⚠️ ${funcName} - deploy falhou: ${deployError.message}`));
95
+ }
96
+ }
97
+
98
+ // Limpar supabase/functions após deploy
99
+ console.log(chalk.gray(' - Limpando supabase/functions após deploy...'));
100
+ try {
101
+ await fs.rm(supabaseFunctionsDir, { recursive: true, force: true });
102
+ } catch {
103
+ // Ignorar erro de limpeza
104
+ }
105
+
106
+ console.log(chalk.green(' ✅ Edge Functions restauradas com sucesso!'));
107
+
108
+ } catch (error) {
109
+ console.error(chalk.red(` ❌ Erro ao restaurar Edge Functions: ${error.message}`));
110
+ }
111
+ };
112
+
@@ -0,0 +1,51 @@
1
+ const chalk = require('chalk');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const inquirer = require('inquirer');
5
+
6
+ /**
7
+ * Etapa 5: Restaurar Auth Settings (interativo - exibir URL e valores)
8
+ */
9
+ module.exports = async ({ backupPath, targetProject }) => {
10
+ console.log(chalk.blue('\n🔐 Restaurando Auth Settings...'));
11
+
12
+ try {
13
+ const authSettingsPath = path.join(backupPath, 'auth-settings.json');
14
+
15
+ if (!fs.existsSync(authSettingsPath)) {
16
+ console.log(chalk.yellow(' ⚠️ Nenhuma configuração de Auth encontrada no backup'));
17
+ return;
18
+ }
19
+
20
+ const authSettings = JSON.parse(fs.readFileSync(authSettingsPath, 'utf8'));
21
+ const dashboardUrl = `https://supabase.com/dashboard/project/${targetProject.targetProjectId}/auth/url-config`;
22
+
23
+ console.log(chalk.green('\n ✅ URL para configuração manual:'));
24
+ console.log(chalk.cyan(` ${dashboardUrl}`));
25
+ console.log(chalk.yellow('\n 📋 Configure manualmente as seguintes opções:'));
26
+
27
+ if (authSettings.settings?.auth_url_config) {
28
+ Object.entries(authSettings.settings.auth_url_config).forEach(([key, value]) => {
29
+ console.log(chalk.gray(` - ${key}: ${value}`));
30
+ });
31
+ } else if (authSettings.auth_url_config) {
32
+ Object.entries(authSettings.auth_url_config).forEach(([key, value]) => {
33
+ console.log(chalk.gray(` - ${key}: ${value}`));
34
+ });
35
+ }
36
+
37
+ console.log(chalk.yellow('\n ⚠️ Após configurar, pressione Enter para continuar...'));
38
+
39
+ await inquirer.prompt([{
40
+ type: 'input',
41
+ name: 'continue',
42
+ message: 'Pressione Enter para continuar'
43
+ }]);
44
+
45
+ console.log(chalk.green(' ✅ Auth Settings processados'));
46
+
47
+ } catch (error) {
48
+ console.error(chalk.red(` ❌ Erro ao processar Auth Settings: ${error.message}`));
49
+ }
50
+ };
51
+
@@ -0,0 +1,58 @@
1
+ const chalk = require('chalk');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const inquirer = require('inquirer');
5
+
6
+ /**
7
+ * Etapa 6: Restaurar Storage Buckets (interativo - exibir informações)
8
+ */
9
+ module.exports = async ({ backupPath }) => {
10
+ console.log(chalk.blue('\n📦 Restaurando Storage Buckets...'));
11
+
12
+ try {
13
+ const storageDir = path.join(backupPath, 'storage');
14
+
15
+ if (!fs.existsSync(storageDir)) {
16
+ console.log(chalk.yellow(' ⚠️ Nenhum bucket de Storage encontrado no backup'));
17
+ return;
18
+ }
19
+
20
+ const manifestPath = path.join(backupPath, 'backup-manifest.json');
21
+ let manifest = null;
22
+
23
+ if (fs.existsSync(manifestPath)) {
24
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
25
+ }
26
+
27
+ const buckets = manifest?.components?.storage?.buckets || [];
28
+
29
+ if (buckets.length === 0) {
30
+ console.log(chalk.gray(' ℹ️ Nenhum bucket para restaurar'));
31
+ return;
32
+ }
33
+
34
+ console.log(chalk.green(`\n ✅ ${buckets.length} bucket(s) encontrado(s) no backup`));
35
+ buckets.forEach(bucket => {
36
+ console.log(chalk.gray(` - ${bucket.name} (${bucket.public ? 'público' : 'privado'})`));
37
+ });
38
+
39
+ const colabUrl = 'https://colab.research.google.com/github/PLyn/supabase-storage-migrate/blob/main/Supabase_Storage_migration.ipynb';
40
+
41
+ console.log(chalk.yellow('\n ⚠️ Migração de objetos de Storage requer processo manual'));
42
+ console.log(chalk.cyan(` ℹ️ Use o script do Google Colab: ${colabUrl}`));
43
+ console.log(chalk.gray('\n 📋 Instruções:'));
44
+ console.log(chalk.gray(' 1. Execute o script no Google Colab'));
45
+ console.log(chalk.gray(' 2. Configure as credenciais dos projetos (origem e destino)'));
46
+ console.log(chalk.gray(' 3. Execute a migração'));
47
+
48
+ await inquirer.prompt([{
49
+ type: 'input',
50
+ name: 'continue',
51
+ message: 'Pressione Enter para continuar'
52
+ }]);
53
+
54
+ } catch (error) {
55
+ console.error(chalk.red(` ❌ Erro ao processar Storage: ${error.message}`));
56
+ }
57
+ };
58
+