smoonb 0.0.47 → 0.0.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/src/commands/backup/index.js +290 -0
  3. package/src/commands/backup/steps/00-docker-validation.js +24 -0
  4. package/src/commands/backup/steps/01-database.js +72 -0
  5. package/src/commands/backup/steps/02-database-separated.js +82 -0
  6. package/src/commands/backup/steps/03-database-settings.js +178 -0
  7. package/src/commands/backup/steps/04-auth-settings.js +43 -0
  8. package/src/commands/backup/steps/05-realtime-settings.js +26 -0
  9. package/src/commands/backup/steps/06-storage.js +90 -0
  10. package/src/commands/backup/steps/07-custom-roles.js +39 -0
  11. package/src/commands/backup/steps/08-edge-functions.js +159 -0
  12. package/src/commands/backup/steps/09-supabase-temp.js +48 -0
  13. package/src/commands/backup/steps/10-migrations.js +80 -0
  14. package/src/commands/backup/utils.js +69 -0
  15. package/src/commands/restore/index.js +190 -0
  16. package/src/commands/restore/steps/00-backup-selection.js +38 -0
  17. package/src/commands/restore/steps/01-components-selection.js +84 -0
  18. package/src/commands/restore/steps/02-confirmation.js +19 -0
  19. package/src/commands/restore/steps/03-database.js +81 -0
  20. package/src/commands/restore/steps/04-edge-functions.js +112 -0
  21. package/src/commands/restore/steps/05-auth-settings.js +51 -0
  22. package/src/commands/restore/steps/06-storage.js +58 -0
  23. package/src/commands/restore/steps/07-database-settings.js +65 -0
  24. package/src/commands/restore/steps/08-realtime-settings.js +50 -0
  25. package/src/commands/restore/utils.js +139 -0
  26. package/src/utils/fsExtra.js +98 -0
  27. package/src/utils/supabaseLink.js +82 -0
  28. package/src/commands/backup.js +0 -939
  29. package/src/commands/restore.js +0 -786
@@ -0,0 +1,190 @@
1
+ const chalk = require('chalk');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const inquirer = require('inquirer');
5
+ const { readEnvFile, writeEnvFile, backupEnvFile } = require('../../utils/env');
6
+ const { saveEnvMap } = require('../../utils/envMap');
7
+ const { mapEnvVariablesInteractively } = require('../../interactive/envMapper');
8
+ const { showBetaBanner } = require('../../utils/banner');
9
+ const { listValidBackups, showRestoreSummary } = require('./utils');
10
+
11
+ // Importar todas as etapas
12
+ const step00BackupSelection = require('./steps/00-backup-selection');
13
+ const step01ComponentsSelection = require('./steps/01-components-selection');
14
+ const step02Confirmation = require('./steps/02-confirmation');
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
+ // Consentimento para leitura e escrita do .env.local
27
+ console.log(chalk.yellow('⚠️ O smoonb irá ler e escrever o arquivo .env.local localmente.'));
28
+ console.log(chalk.yellow(' Um backup automático do .env.local será criado antes de qualquer alteração.'));
29
+ const consent = await inquirer.prompt([{ type: 'confirm', name: 'ok', message: 'Você consente em prosseguir (S/n):', default: true }]);
30
+ if (!consent.ok) {
31
+ console.log(chalk.red('🚫 Operação cancelada pelo usuário.'));
32
+ process.exit(1);
33
+ }
34
+
35
+ // Preparar diretório de processo restore-YYYY-...
36
+ const rootBackupsDir = path.join(process.cwd(), 'backups');
37
+ const now = new Date();
38
+ 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')}`;
39
+ const processDir = path.join(rootBackupsDir, `restore-${ts}`);
40
+ fs.mkdirSync(path.join(processDir, 'env'), { recursive: true });
41
+
42
+ // Backup do .env.local
43
+ const envPath = path.join(process.cwd(), '.env.local');
44
+ const envBackupPath = path.join(processDir, 'env', '.env.local');
45
+ await backupEnvFile(envPath, envBackupPath);
46
+ console.log(chalk.blue(`📁 Backup do .env.local: ${path.relative(process.cwd(), envBackupPath)}`));
47
+
48
+ // Leitura e mapeamento interativo
49
+ const currentEnv = await readEnvFile(envPath);
50
+ const expectedKeys = [
51
+ 'NEXT_PUBLIC_SUPABASE_URL',
52
+ 'NEXT_PUBLIC_SUPABASE_ANON_KEY',
53
+ 'SUPABASE_SERVICE_ROLE_KEY',
54
+ 'SUPABASE_DB_URL',
55
+ 'SUPABASE_PROJECT_ID',
56
+ 'SUPABASE_ACCESS_TOKEN',
57
+ 'SMOONB_OUTPUT_DIR'
58
+ ];
59
+ const { finalEnv, dePara } = await mapEnvVariablesInteractively(currentEnv, expectedKeys);
60
+ await writeEnvFile(envPath, finalEnv);
61
+ await saveEnvMap(dePara, path.join(processDir, 'env', 'env-map.json'));
62
+ console.log(chalk.green('✅ .env.local atualizado com sucesso. Nenhuma chave renomeada; valores sincronizados.'));
63
+
64
+ // Resolver valores esperados a partir do de-para
65
+ function getValue(expectedKey) {
66
+ const clientKey = Object.keys(dePara).find(k => dePara[k] === expectedKey);
67
+ return clientKey ? finalEnv[clientKey] : '';
68
+ }
69
+
70
+ // Construir targetProject a partir do .env.local mapeado
71
+ const targetProject = {
72
+ targetProjectId: getValue('SUPABASE_PROJECT_ID'),
73
+ targetUrl: getValue('NEXT_PUBLIC_SUPABASE_URL'),
74
+ targetAnonKey: getValue('NEXT_PUBLIC_SUPABASE_ANON_KEY'),
75
+ targetServiceKey: getValue('SUPABASE_SERVICE_ROLE_KEY'),
76
+ targetDatabaseUrl: getValue('SUPABASE_DB_URL'),
77
+ targetAccessToken: getValue('SUPABASE_ACCESS_TOKEN')
78
+ };
79
+
80
+ console.log(chalk.blue(`📁 Buscando backups em: ${getValue('SMOONB_OUTPUT_DIR') || './backups'}`));
81
+
82
+ // 1. Listar backups válidos (.backup.gz)
83
+ const validBackups = await listValidBackups(getValue('SMOONB_OUTPUT_DIR') || './backups');
84
+
85
+ if (validBackups.length === 0) {
86
+ console.error(chalk.red('❌ Nenhum backup válido encontrado'));
87
+ console.log(chalk.yellow('💡 Execute primeiro: npx smoonb backup'));
88
+ process.exit(1);
89
+ }
90
+
91
+ // 2. Selecionar backup interativamente
92
+ const selectedBackup = await step00BackupSelection(validBackups);
93
+
94
+ // 3. Perguntar quais componentes restaurar
95
+ const components = await step01ComponentsSelection(selectedBackup.path);
96
+
97
+ // Validar que pelo menos um componente foi selecionado
98
+ if (!Object.values(components).some(Boolean)) {
99
+ console.error(chalk.red('\n❌ Nenhum componente selecionado para restauração!'));
100
+ process.exit(1);
101
+ }
102
+
103
+ // 4. Mostrar resumo
104
+ showRestoreSummary(selectedBackup, components, targetProject);
105
+
106
+ // 5. Confirmar execução
107
+ const confirmed = await step02Confirmation();
108
+ if (!confirmed) {
109
+ console.log(chalk.yellow('Restauração cancelada.'));
110
+ process.exit(0);
111
+ }
112
+
113
+ // 6. Executar restauração
114
+ console.log(chalk.blue('\n🚀 Iniciando restauração...'));
115
+
116
+ // 6.1 Database (se selecionado)
117
+ if (components.database) {
118
+ await step03Database({
119
+ backupFilePath: path.join(selectedBackup.path, selectedBackup.backupFile),
120
+ targetDatabaseUrl: targetProject.targetDatabaseUrl
121
+ });
122
+ }
123
+
124
+ // 6.2 Edge Functions (se selecionado)
125
+ if (components.edgeFunctions) {
126
+ await step04EdgeFunctions({
127
+ backupPath: selectedBackup.path,
128
+ targetProject
129
+ });
130
+ }
131
+
132
+ // 6.3 Auth Settings (se selecionado)
133
+ if (components.authSettings) {
134
+ await step05AuthSettings({
135
+ backupPath: selectedBackup.path,
136
+ targetProject
137
+ });
138
+ }
139
+
140
+ // 6.4 Storage Buckets (se selecionado)
141
+ if (components.storage) {
142
+ await step06Storage({
143
+ backupPath: selectedBackup.path
144
+ });
145
+ }
146
+
147
+ // 6.5 Database Settings (se selecionado)
148
+ if (components.databaseSettings) {
149
+ await step07DatabaseSettings({
150
+ backupPath: selectedBackup.path,
151
+ targetProject
152
+ });
153
+ }
154
+
155
+ // 6.6 Realtime Settings (se selecionado)
156
+ if (components.realtimeSettings) {
157
+ await step08RealtimeSettings({
158
+ backupPath: selectedBackup.path,
159
+ targetProject
160
+ });
161
+ }
162
+
163
+ // report.json de restauração
164
+ const report = {
165
+ process: 'restore',
166
+ created_at: new Date().toISOString(),
167
+ target_project_id: targetProject.targetProjectId,
168
+ assets: {
169
+ env: path.join(processDir, 'env', '.env.local'),
170
+ env_map: path.join(processDir, 'env', 'env-map.json')
171
+ },
172
+ components: components,
173
+ notes: [
174
+ 'supabase/functions limpo antes e depois do deploy (se Edge Functions selecionado)'
175
+ ]
176
+ };
177
+ try {
178
+ fs.writeFileSync(path.join(processDir, 'report.json'), JSON.stringify(report, null, 2));
179
+ } catch {
180
+ // silencioso
181
+ }
182
+
183
+ console.log(chalk.green('\n🎉 Restauração completa finalizada!'));
184
+
185
+ } catch (error) {
186
+ console.error(chalk.red(`❌ Erro na restauração: ${error.message}`));
187
+ process.exit(1);
188
+ }
189
+ };
190
+
@@ -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,84 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const inquirer = require('inquirer');
4
+
5
+ /**
6
+ * Etapa 1: Perguntar quais componentes restaurar
7
+ */
8
+ module.exports = async (backupPath) => {
9
+ const questions = [];
10
+
11
+ // Database
12
+ questions.push({
13
+ type: 'confirm',
14
+ name: 'restoreDatabase',
15
+ message: 'Deseja restaurar Database (S/n):',
16
+ default: true
17
+ });
18
+
19
+ // Edge Functions
20
+ const edgeFunctionsDir = path.join(backupPath, 'edge-functions');
21
+ if (fs.existsSync(edgeFunctionsDir) && fs.readdirSync(edgeFunctionsDir).length > 0) {
22
+ questions.push({
23
+ type: 'confirm',
24
+ name: 'restoreEdgeFunctions',
25
+ message: 'Deseja restaurar Edge Functions (S/n):',
26
+ default: true
27
+ });
28
+ }
29
+
30
+ // Auth Settings
31
+ if (fs.existsSync(path.join(backupPath, 'auth-settings.json'))) {
32
+ questions.push({
33
+ type: 'confirm',
34
+ name: 'restoreAuthSettings',
35
+ message: 'Deseja restaurar Auth Settings (s/N):',
36
+ default: false
37
+ });
38
+ }
39
+
40
+ // Storage Buckets
41
+ const storageDir = path.join(backupPath, 'storage');
42
+ if (fs.existsSync(storageDir) && fs.readdirSync(storageDir).length > 0) {
43
+ questions.push({
44
+ type: 'confirm',
45
+ name: 'restoreStorage',
46
+ message: 'Deseja ver informações de Storage Buckets (s/N):',
47
+ default: false
48
+ });
49
+ }
50
+
51
+ // Database Extensions and Settings
52
+ const dbSettingsFiles = fs.readdirSync(backupPath)
53
+ .filter(file => file.startsWith('database-settings-') && file.endsWith('.json'));
54
+ if (dbSettingsFiles.length > 0) {
55
+ questions.push({
56
+ type: 'confirm',
57
+ name: 'restoreDatabaseSettings',
58
+ message: 'Deseja restaurar Database Extensions and Settings (s/N):',
59
+ default: false
60
+ });
61
+ }
62
+
63
+ // Realtime Settings
64
+ if (fs.existsSync(path.join(backupPath, 'realtime-settings.json'))) {
65
+ questions.push({
66
+ type: 'confirm',
67
+ name: 'restoreRealtimeSettings',
68
+ message: 'Deseja restaurar Realtime Settings (s/N):',
69
+ default: false
70
+ });
71
+ }
72
+
73
+ const answers = await inquirer.prompt(questions);
74
+
75
+ return {
76
+ database: answers.restoreDatabase,
77
+ edgeFunctions: answers.restoreEdgeFunctions || false,
78
+ storage: answers.restoreStorage || false,
79
+ authSettings: answers.restoreAuthSettings || false,
80
+ databaseSettings: answers.restoreDatabaseSettings || false,
81
+ realtimeSettings: answers.restoreRealtimeSettings || false
82
+ };
83
+ };
84
+
@@ -0,0 +1,19 @@
1
+ const readline = require('readline');
2
+
3
+ /**
4
+ * Etapa 2: Confirmar execução
5
+ */
6
+ module.exports = async () => {
7
+ const rl = readline.createInterface({
8
+ input: process.stdin,
9
+ output: process.stdout
10
+ });
11
+
12
+ const question = (query) => new Promise(resolve => rl.question(query, resolve));
13
+
14
+ const confirm = await question('Deseja continuar com a restauração? (s/N): ');
15
+ rl.close();
16
+
17
+ return confirm.toLowerCase() === 's';
18
+ };
19
+
@@ -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 (cleanError) {
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 (linkError) {
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 (cleanError) {
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
+