smoonb 0.0.44 → 0.0.46
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/package.json +15 -2
- package/src/commands/backup.js +159 -67
- package/src/commands/restore.js +165 -25
- package/src/interactive/envMapper.js +90 -0
- package/src/utils/env.js +106 -0
- package/src/utils/envMap.js +25 -0
- package/src/utils/supabase.js +2 -2
- package/.smoonbrc +0 -29
- package/.smoonbrc.example +0 -28
- package/backups/backup-2025-10-17T19-18-58-539Z/auth-config.json +0 -7
- package/backups/backup-2025-10-17T19-18-58-539Z/backup-manifest.json +0 -19
- package/backups/backup-2025-10-17T19-18-58-539Z/functions/README.md +0 -4
- package/backups/backup-2025-10-17T19-18-58-539Z/realtime-config.json +0 -7
- package/backups/backup-2025-10-17T19-18-58-539Z/storage/storage-config.json +0 -6
- package/backups/backup-2025-10-17T19-52-20-211Z/auth-config.json +0 -7
- package/backups/backup-2025-10-17T19-52-20-211Z/backup-manifest.json +0 -19
- package/backups/backup-2025-10-17T19-52-20-211Z/database-2025-10-17T19-52-20-215Z.dump +0 -0
- package/backups/backup-2025-10-17T19-52-20-211Z/functions/README.md +0 -4
- package/backups/backup-2025-10-17T19-52-20-211Z/realtime-config.json +0 -7
- package/backups/backup-2025-10-17T19-52-20-211Z/storage/storage-config.json +0 -6
- package/backups/backup-2025-10-17T20-38-13-188Z/auth-config.json +0 -7
- package/backups/backup-2025-10-17T20-38-13-188Z/backup-manifest.json +0 -19
- package/backups/backup-2025-10-17T20-38-13-188Z/database-2025-10-17T20-38-13-194Z.dump +0 -0
- package/backups/backup-2025-10-17T20-38-13-188Z/functions/README.md +0 -4
- package/backups/backup-2025-10-17T20-38-13-188Z/realtime-config.json +0 -7
- package/backups/backup-2025-10-17T20-38-13-188Z/storage/storage-config.json +0 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smoonb",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.46",
|
|
4
4
|
"description": "Complete Supabase backup and migration tool - EXPERIMENTAL VERSION - USE AT YOUR OWN RISK",
|
|
5
5
|
"preferGlobal": false,
|
|
6
6
|
"preventGlobalInstall": true,
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
13
13
|
"start": "node bin/smoonb.js",
|
|
14
14
|
"preinstall": "node -e \"if(process.env.npm_config_global) { console.error('\\n❌ SMOONB NÃO DEVE SER INSTALADO GLOBALMENTE!\\n\\n📋 Para usar o smoonb, instale localmente no seu projeto:\\n npm install smoonb\\n\\n💡 Depois execute com:\\n npx smoonb backup\\n\\n🚫 Instalação global cancelada!\\n'); process.exit(1); }\"",
|
|
15
|
-
"postinstall": "echo '\\n✅ smoonb instalado com sucesso!\\n💡 Execute: npx smoonb backup\\n📖 Documentação: https://github.com/almmello/smoonb\\n'"
|
|
15
|
+
"postinstall": "echo '\\n✅ smoonb instalado com sucesso!\\n💡 Execute: npx smoonb backup\\n📖 Documentação: https://github.com/almmello/smoonb\\n'",
|
|
16
|
+
"lint": "eslint . --ext .js",
|
|
17
|
+
"lint:fix": "eslint . --ext .js --fix",
|
|
18
|
+
"build": "npm run lint"
|
|
16
19
|
},
|
|
17
20
|
"keywords": [
|
|
18
21
|
"supabase",
|
|
@@ -36,6 +39,9 @@
|
|
|
36
39
|
"inquirer": "^8.2.7"
|
|
37
40
|
},
|
|
38
41
|
"type": "commonjs",
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"eslint": "^9.38.0"
|
|
44
|
+
},
|
|
39
45
|
"repository": {
|
|
40
46
|
"type": "git",
|
|
41
47
|
"url": "git+https://github.com/almmello/smoonb.git"
|
|
@@ -44,4 +50,11 @@
|
|
|
44
50
|
"url": "https://github.com/almmello/smoonb/issues"
|
|
45
51
|
},
|
|
46
52
|
"homepage": "https://github.com/almmello/smoonb#readme"
|
|
53
|
+
,
|
|
54
|
+
"files": [
|
|
55
|
+
"bin/",
|
|
56
|
+
"src/",
|
|
57
|
+
"README.md",
|
|
58
|
+
"LICENSE.md"
|
|
59
|
+
]
|
|
47
60
|
}
|
package/src/commands/backup.js
CHANGED
|
@@ -9,6 +9,9 @@ const { readConfig, validateFor } = require('../utils/config');
|
|
|
9
9
|
const { showBetaBanner } = require('../utils/banner');
|
|
10
10
|
const { canPerformCompleteBackup, getDockerVersion } = require('../utils/docker');
|
|
11
11
|
const { captureRealtimeSettings } = require('../utils/realtime-settings');
|
|
12
|
+
const { readEnvFile, writeEnvFile, backupEnvFile } = require('../utils/env');
|
|
13
|
+
const { saveEnvMap } = require('../utils/envMap');
|
|
14
|
+
const { mapEnvVariablesInteractively, askComponentsFlags } = require('../interactive/envMapper');
|
|
12
15
|
|
|
13
16
|
const execAsync = promisify(exec);
|
|
14
17
|
|
|
@@ -17,16 +20,82 @@ module.exports = async (options) => {
|
|
|
17
20
|
showBetaBanner();
|
|
18
21
|
|
|
19
22
|
try {
|
|
20
|
-
//
|
|
21
|
-
|
|
23
|
+
// Consentimento para leitura e escrita do .env.local
|
|
24
|
+
console.log(chalk.yellow('⚠️ O smoonb irá ler e escrever o arquivo .env.local localmente.'));
|
|
25
|
+
console.log(chalk.yellow(' Um backup automático do .env.local será criado antes de qualquer alteração.'));
|
|
26
|
+
const consent = await require('inquirer').prompt([{ type: 'confirm', name: 'ok', message: 'Você consente em prosseguir (S/n):', default: true }]);
|
|
27
|
+
if (!consent.ok) {
|
|
28
|
+
console.log(chalk.red('🚫 Operação cancelada pelo usuário.'));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Carregar configuração existente apenas para defaults de diretório
|
|
33
|
+
const config = await readConfig().catch(() => ({ backup: { outputDir: './backups' }, supabase: {} }));
|
|
22
34
|
validateFor(config, 'backup');
|
|
23
35
|
|
|
24
36
|
// Validação adicional para pré-requisitos obrigatórios
|
|
25
|
-
|
|
37
|
+
// Pré-passo de ENV: criar diretório de backup com timestamp já no início
|
|
38
|
+
const now = new Date();
|
|
39
|
+
const year = now.getFullYear();
|
|
40
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
41
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
42
|
+
const hour = String(now.getHours()).padStart(2, '0');
|
|
43
|
+
const minute = String(now.getMinutes()).padStart(2, '0');
|
|
44
|
+
const second = String(now.getSeconds()).padStart(2, '0');
|
|
45
|
+
|
|
46
|
+
// Resolver diretório de saída (prioriza .env.local mapeado depois, por ora usa default)
|
|
47
|
+
const defaultOutput = options.output || config.backup?.outputDir || './backups';
|
|
48
|
+
const backupDir = path.join(defaultOutput, `backup-${year}-${month}-${day}-${hour}-${minute}-${second}`);
|
|
49
|
+
await ensureDir(backupDir);
|
|
50
|
+
|
|
51
|
+
// Backup e mapeamento do .env.local
|
|
52
|
+
const envPath = path.join(process.cwd(), '.env.local');
|
|
53
|
+
const envBackupPath = path.join(backupDir, 'env', '.env.local');
|
|
54
|
+
await ensureDir(path.dirname(envBackupPath));
|
|
55
|
+
await backupEnvFile(envPath, envBackupPath);
|
|
56
|
+
console.log(chalk.blue(`📁 Backup do .env.local: ${path.relative(process.cwd(), envBackupPath)}`));
|
|
57
|
+
|
|
58
|
+
const expectedKeys = [
|
|
59
|
+
'NEXT_PUBLIC_SUPABASE_URL',
|
|
60
|
+
'NEXT_PUBLIC_SUPABASE_ANON_KEY',
|
|
61
|
+
'SUPABASE_SERVICE_ROLE_KEY',
|
|
62
|
+
'SUPABASE_DB_URL',
|
|
63
|
+
'SUPABASE_PROJECT_ID',
|
|
64
|
+
'SUPABASE_ACCESS_TOKEN',
|
|
65
|
+
'SMOONB_OUTPUT_DIR'
|
|
66
|
+
];
|
|
67
|
+
const currentEnv = await readEnvFile(envPath);
|
|
68
|
+
const { finalEnv, dePara } = await mapEnvVariablesInteractively(currentEnv, expectedKeys);
|
|
69
|
+
await writeEnvFile(envPath, finalEnv);
|
|
70
|
+
await saveEnvMap(dePara, path.join(backupDir, 'env', 'env-map.json'));
|
|
71
|
+
console.log(chalk.green('✅ .env.local atualizado com sucesso. Nenhuma chave renomeada; valores sincronizados.'));
|
|
72
|
+
|
|
73
|
+
function getValue(expectedKey) {
|
|
74
|
+
const clientKey = Object.keys(dePara).find(k => dePara[k] === expectedKey);
|
|
75
|
+
return clientKey ? finalEnv[clientKey] : '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Recalcular outputDir a partir do ENV mapeado
|
|
79
|
+
const resolvedOutputDir = options.output || getValue('SMOONB_OUTPUT_DIR') || config.backup?.outputDir || './backups';
|
|
80
|
+
|
|
81
|
+
// Se mudou o outputDir, movemos o backupDir inicial para o novo local mantendo timestamp
|
|
82
|
+
const finalBackupDir = backupDir.startsWith(path.resolve(resolvedOutputDir))
|
|
83
|
+
? backupDir
|
|
84
|
+
: path.join(resolvedOutputDir, path.basename(backupDir));
|
|
85
|
+
if (finalBackupDir !== backupDir) {
|
|
86
|
+
await ensureDir(resolvedOutputDir);
|
|
87
|
+
await fs.rename(backupDir, finalBackupDir);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const projectId = getValue('SUPABASE_PROJECT_ID');
|
|
91
|
+
const accessToken = getValue('SUPABASE_ACCESS_TOKEN');
|
|
92
|
+
const databaseUrl = getValue('SUPABASE_DB_URL');
|
|
93
|
+
|
|
94
|
+
if (!databaseUrl) {
|
|
26
95
|
console.log(chalk.red('❌ DATABASE_URL NÃO CONFIGURADA'));
|
|
27
96
|
console.log('');
|
|
28
97
|
console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:'));
|
|
29
|
-
console.log(chalk.yellow(' 1. Configurar
|
|
98
|
+
console.log(chalk.yellow(' 1. Configurar SUPABASE_DB_URL no .env.local'));
|
|
30
99
|
console.log(chalk.yellow(' 2. Repetir o comando de backup'));
|
|
31
100
|
console.log('');
|
|
32
101
|
console.log(chalk.blue('💡 Exemplo de configuração:'));
|
|
@@ -36,12 +105,12 @@ module.exports = async (options) => {
|
|
|
36
105
|
process.exit(1);
|
|
37
106
|
}
|
|
38
107
|
|
|
39
|
-
if (!
|
|
108
|
+
if (!accessToken) {
|
|
40
109
|
console.log(chalk.red('❌ ACCESS_TOKEN NÃO CONFIGURADO'));
|
|
41
110
|
console.log('');
|
|
42
111
|
console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:'));
|
|
43
112
|
console.log(chalk.yellow(' 1. Obter Personal Access Token do Supabase'));
|
|
44
|
-
console.log(chalk.yellow(' 2. Configurar
|
|
113
|
+
console.log(chalk.yellow(' 2. Configurar SUPABASE_ACCESS_TOKEN no .env.local'));
|
|
45
114
|
console.log(chalk.yellow(' 3. Repetir o comando de backup'));
|
|
46
115
|
console.log('');
|
|
47
116
|
console.log(chalk.blue('🔗 Como obter o token:'));
|
|
@@ -53,7 +122,7 @@ module.exports = async (options) => {
|
|
|
53
122
|
process.exit(1);
|
|
54
123
|
}
|
|
55
124
|
|
|
56
|
-
console.log(chalk.blue(`🚀 Iniciando backup do projeto: ${
|
|
125
|
+
console.log(chalk.blue(`🚀 Iniciando backup do projeto: ${projectId}`));
|
|
57
126
|
console.log(chalk.gray(`🔍 Verificando dependências Docker...`));
|
|
58
127
|
|
|
59
128
|
// Verificar se é possível fazer backup completo via Docker
|
|
@@ -65,7 +134,9 @@ module.exports = async (options) => {
|
|
|
65
134
|
console.log('');
|
|
66
135
|
|
|
67
136
|
// Proceder com backup completo via Docker
|
|
68
|
-
|
|
137
|
+
// Flags de componentes (não afetam Database)
|
|
138
|
+
const flags = await askComponentsFlags();
|
|
139
|
+
return await performFullBackup({ projectId, accessToken, databaseUrl }, { ...options, flags, backupDir: finalBackupDir, outputDir: resolvedOutputDir });
|
|
69
140
|
} else {
|
|
70
141
|
// Mostrar mensagens educativas e encerrar elegantemente
|
|
71
142
|
showDockerMessagesAndExit(backupCapability.reason);
|
|
@@ -78,28 +149,17 @@ module.exports = async (options) => {
|
|
|
78
149
|
};
|
|
79
150
|
|
|
80
151
|
// Função para backup completo via Docker
|
|
81
|
-
async function performFullBackup(
|
|
82
|
-
|
|
83
|
-
const outputDir = options.
|
|
84
|
-
|
|
85
|
-
// Criar diretório de backup com timestamp humanizado
|
|
86
|
-
const now = new Date();
|
|
87
|
-
const year = now.getFullYear();
|
|
88
|
-
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
89
|
-
const day = String(now.getDate()).padStart(2, '0');
|
|
90
|
-
const hour = String(now.getHours()).padStart(2, '0');
|
|
91
|
-
const minute = String(now.getMinutes()).padStart(2, '0');
|
|
92
|
-
const second = String(now.getSeconds()).padStart(2, '0');
|
|
93
|
-
|
|
94
|
-
const backupDir = path.join(outputDir, `backup-${year}-${month}-${day}-${hour}-${minute}-${second}`);
|
|
95
|
-
await ensureDir(backupDir);
|
|
152
|
+
async function performFullBackup(envCfg, options) {
|
|
153
|
+
const { projectId, accessToken, databaseUrl } = envCfg;
|
|
154
|
+
const outputDir = options.outputDir;
|
|
155
|
+
const backupDir = options.backupDir;
|
|
96
156
|
|
|
97
157
|
console.log(chalk.blue(`📁 Diretório: ${backupDir}`));
|
|
98
158
|
console.log(chalk.gray(`🐳 Backup via Docker Desktop`));
|
|
99
159
|
|
|
100
160
|
const manifest = {
|
|
101
161
|
created_at: new Date().toISOString(),
|
|
102
|
-
project_id:
|
|
162
|
+
project_id: projectId,
|
|
103
163
|
smoonb_version: require('../../package.json').version,
|
|
104
164
|
backup_type: 'pg_dumpall_docker_dashboard_compatible',
|
|
105
165
|
docker_version: await getDockerVersion(),
|
|
@@ -109,12 +169,12 @@ async function performFullBackup(config, options) {
|
|
|
109
169
|
|
|
110
170
|
// 1. Backup Database via pg_dumpall Docker (idêntico ao Dashboard)
|
|
111
171
|
console.log(chalk.blue('\n📊 1/8 - Backup da Database PostgreSQL via pg_dumpall Docker...'));
|
|
112
|
-
const databaseResult = await backupDatabase(
|
|
172
|
+
const databaseResult = await backupDatabase(databaseUrl, backupDir);
|
|
113
173
|
manifest.components.database = databaseResult;
|
|
114
174
|
|
|
115
175
|
// 1.5. Backup Database Separado (SQL files para troubleshooting)
|
|
116
176
|
console.log(chalk.blue('\n📊 1.5/8 - Backup da Database PostgreSQL (arquivos SQL separados)...'));
|
|
117
|
-
const dbSeparatedResult = await backupDatabaseSeparated(
|
|
177
|
+
const dbSeparatedResult = await backupDatabaseSeparated(databaseUrl, backupDir, accessToken);
|
|
118
178
|
manifest.components.database_separated = {
|
|
119
179
|
success: dbSeparatedResult.success,
|
|
120
180
|
method: 'supabase-cli',
|
|
@@ -123,34 +183,42 @@ async function performFullBackup(config, options) {
|
|
|
123
183
|
};
|
|
124
184
|
|
|
125
185
|
// 2. Backup Edge Functions via Docker
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
186
|
+
if (options.flags?.includeFunctions) {
|
|
187
|
+
console.log(chalk.blue('\n⚡ 2/8 - Backup das Edge Functions via Docker...'));
|
|
188
|
+
const functionsResult = await backupEdgeFunctionsWithDocker(projectId, accessToken, backupDir);
|
|
189
|
+
manifest.components.edge_functions = functionsResult;
|
|
190
|
+
}
|
|
129
191
|
|
|
130
192
|
// 3. Backup Auth Settings via API
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
193
|
+
if (options.flags?.includeAuth) {
|
|
194
|
+
console.log(chalk.blue('\n🔐 3/8 - Backup das Auth Settings via API...'));
|
|
195
|
+
const authResult = await backupAuthSettings(projectId, accessToken, backupDir);
|
|
196
|
+
manifest.components.auth_settings = authResult;
|
|
197
|
+
}
|
|
134
198
|
|
|
135
199
|
// 4. Backup Storage via API
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
200
|
+
if (options.flags?.includeStorage) {
|
|
201
|
+
console.log(chalk.blue('\n📦 4/8 - Backup do Storage via API...'));
|
|
202
|
+
const storageResult = await backupStorage(projectId, accessToken, backupDir);
|
|
203
|
+
manifest.components.storage = storageResult;
|
|
204
|
+
}
|
|
139
205
|
|
|
140
206
|
// 5. Backup Custom Roles via SQL
|
|
141
207
|
console.log(chalk.blue('\n👥 5/8 - Backup dos Custom Roles via SQL...'));
|
|
142
|
-
const rolesResult = await backupCustomRoles(
|
|
208
|
+
const rolesResult = await backupCustomRoles(databaseUrl, backupDir, accessToken);
|
|
143
209
|
manifest.components.custom_roles = rolesResult;
|
|
144
210
|
|
|
145
211
|
// 6. Backup das Database Extensions and Settings via SQL
|
|
146
212
|
console.log(chalk.blue('\n🔧 6/8 - Backup das Database Extensions and Settings via SQL...'));
|
|
147
|
-
const databaseSettingsResult = await backupDatabaseSettings(
|
|
213
|
+
const databaseSettingsResult = await backupDatabaseSettings(databaseUrl, projectId, backupDir);
|
|
148
214
|
manifest.components.database_settings = databaseSettingsResult;
|
|
149
215
|
|
|
150
216
|
// 7. Backup Realtime Settings via Captura Interativa
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
217
|
+
if (options.flags?.includeRealtime) {
|
|
218
|
+
console.log(chalk.blue('\n🔄 7/8 - Backup das Realtime Settings via Captura Interativa...'));
|
|
219
|
+
const realtimeResult = await backupRealtimeSettings(projectId, backupDir, options.skipRealtime);
|
|
220
|
+
manifest.components.realtime = realtimeResult;
|
|
221
|
+
}
|
|
154
222
|
|
|
155
223
|
// Salvar manifest
|
|
156
224
|
await writeJson(path.join(backupDir, 'backup-manifest.json'), manifest);
|
|
@@ -160,20 +228,50 @@ async function performFullBackup(config, options) {
|
|
|
160
228
|
console.log(chalk.green(`📊 Database: ${databaseResult.fileName} (${databaseResult.size} KB) - Idêntico ao Dashboard`));
|
|
161
229
|
console.log(chalk.green(`📊 Database SQL: ${dbSeparatedResult.files?.length || 0} arquivos separados (${dbSeparatedResult.totalSizeKB} KB) - Para troubleshooting`));
|
|
162
230
|
console.log(chalk.green(`🔧 Database Settings: ${databaseSettingsResult.fileName} (${databaseSettingsResult.size} KB) - Extensions e Configurações`));
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
231
|
+
if (options.flags?.includeFunctions && manifest.components.edge_functions) {
|
|
232
|
+
const functionsResult = manifest.components.edge_functions;
|
|
233
|
+
console.log(chalk.green(`⚡ Edge Functions: ${functionsResult.success_count || 0}/${functionsResult.functions_count || 0} functions baixadas via Docker`));
|
|
234
|
+
}
|
|
235
|
+
if (options.flags?.includeAuth && manifest.components.auth_settings) {
|
|
236
|
+
const authResult = manifest.components.auth_settings;
|
|
237
|
+
console.log(chalk.green(`🔐 Auth Settings: ${authResult.success ? 'Exportadas via API' : 'Falharam'}`));
|
|
238
|
+
}
|
|
239
|
+
if (options.flags?.includeStorage && manifest.components.storage) {
|
|
240
|
+
const storageResult = manifest.components.storage;
|
|
241
|
+
console.log(chalk.green(`📦 Storage: ${storageResult.buckets?.length || 0} buckets verificados via API`));
|
|
242
|
+
}
|
|
166
243
|
console.log(chalk.green(`👥 Custom Roles: ${rolesResult.roles?.length || 0} roles exportados via SQL`));
|
|
167
244
|
// Determinar mensagem correta baseada no método usado
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
245
|
+
if (options.flags?.includeRealtime && manifest.components.realtime) {
|
|
246
|
+
const realtimeResult = manifest.components.realtime;
|
|
247
|
+
let realtimeMessage = 'Falharam';
|
|
248
|
+
if (realtimeResult.success) {
|
|
249
|
+
if (options.skipRealtime) {
|
|
250
|
+
realtimeMessage = 'Configurações copiadas do backup anterior';
|
|
251
|
+
} else {
|
|
252
|
+
realtimeMessage = 'Configurações capturadas interativamente';
|
|
253
|
+
}
|
|
174
254
|
}
|
|
255
|
+
console.log(chalk.green(`🔄 Realtime: ${realtimeMessage}`));
|
|
175
256
|
}
|
|
176
|
-
|
|
257
|
+
|
|
258
|
+
// report.json
|
|
259
|
+
await writeJson(path.join(backupDir, 'report.json'), {
|
|
260
|
+
process: 'backup',
|
|
261
|
+
created_at: manifest.created_at,
|
|
262
|
+
project_id: manifest.project_id,
|
|
263
|
+
assets: {
|
|
264
|
+
env: path.join(backupDir, 'env', '.env.local'),
|
|
265
|
+
env_map: path.join(backupDir, 'env', 'env-map.json'),
|
|
266
|
+
manifest: path.join(backupDir, 'backup-manifest.json')
|
|
267
|
+
},
|
|
268
|
+
components: {
|
|
269
|
+
includeFunctions: !!options.flags?.includeFunctions,
|
|
270
|
+
includeStorage: !!options.flags?.includeStorage,
|
|
271
|
+
includeAuth: !!options.flags?.includeAuth,
|
|
272
|
+
includeRealtime: !!options.flags?.includeRealtime
|
|
273
|
+
}
|
|
274
|
+
});
|
|
177
275
|
|
|
178
276
|
return { success: true, backupDir, manifest };
|
|
179
277
|
}
|
|
@@ -240,16 +338,14 @@ function showDockerMessagesAndExit(reason) {
|
|
|
240
338
|
}
|
|
241
339
|
|
|
242
340
|
// Backup da database usando pg_dumpall via Docker (idêntico ao Supabase Dashboard)
|
|
243
|
-
async function backupDatabase(
|
|
341
|
+
async function backupDatabase(databaseUrl, backupDir) {
|
|
244
342
|
try {
|
|
245
343
|
console.log(chalk.gray(' - Criando backup completo via pg_dumpall...'));
|
|
246
344
|
|
|
247
345
|
const { execSync } = require('child_process');
|
|
248
|
-
const config = await readConfig();
|
|
249
346
|
|
|
250
347
|
// Extrair credenciais da databaseUrl
|
|
251
|
-
const
|
|
252
|
-
const urlMatch = dbUrl.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
|
|
348
|
+
const urlMatch = databaseUrl.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
|
|
253
349
|
|
|
254
350
|
if (!urlMatch) {
|
|
255
351
|
throw new Error('Database URL inválida');
|
|
@@ -309,14 +405,12 @@ async function backupDatabase(projectId, backupDir) {
|
|
|
309
405
|
}
|
|
310
406
|
|
|
311
407
|
// Backup da database usando arquivos SQL separados via Supabase CLI (para troubleshooting)
|
|
312
|
-
async function backupDatabaseSeparated(
|
|
408
|
+
async function backupDatabaseSeparated(databaseUrl, backupDir, accessToken) {
|
|
313
409
|
try {
|
|
314
410
|
console.log(chalk.gray(' - Criando backups SQL separados via Supabase CLI...'));
|
|
315
411
|
|
|
316
412
|
const { execSync } = require('child_process');
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
const dbUrl = config.supabase.databaseUrl;
|
|
413
|
+
const dbUrl = databaseUrl;
|
|
320
414
|
const files = [];
|
|
321
415
|
let totalSizeKB = 0;
|
|
322
416
|
|
|
@@ -325,7 +419,7 @@ async function backupDatabaseSeparated(projectId, backupDir) {
|
|
|
325
419
|
const schemaFile = path.join(backupDir, 'schema.sql');
|
|
326
420
|
|
|
327
421
|
try {
|
|
328
|
-
execSync(`supabase db dump --db-url "${dbUrl}" -f "${schemaFile}"`, { stdio: 'pipe' });
|
|
422
|
+
execSync(`supabase db dump --db-url "${dbUrl}" -f "${schemaFile}"`, { stdio: 'pipe', env: { ...process.env, SUPABASE_ACCESS_TOKEN: accessToken || '' } });
|
|
329
423
|
const stats = await fs.stat(schemaFile);
|
|
330
424
|
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
331
425
|
files.push({ filename: 'schema.sql', sizeKB });
|
|
@@ -340,7 +434,7 @@ async function backupDatabaseSeparated(projectId, backupDir) {
|
|
|
340
434
|
const dataFile = path.join(backupDir, 'data.sql');
|
|
341
435
|
|
|
342
436
|
try {
|
|
343
|
-
execSync(`supabase db dump --db-url "${dbUrl}" --data-only -f "${dataFile}"`, { stdio: 'pipe' });
|
|
437
|
+
execSync(`supabase db dump --db-url "${dbUrl}" --data-only -f "${dataFile}"`, { stdio: 'pipe', env: { ...process.env, SUPABASE_ACCESS_TOKEN: accessToken || '' } });
|
|
344
438
|
const stats = await fs.stat(dataFile);
|
|
345
439
|
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
346
440
|
files.push({ filename: 'data.sql', sizeKB });
|
|
@@ -355,7 +449,7 @@ async function backupDatabaseSeparated(projectId, backupDir) {
|
|
|
355
449
|
const rolesFile = path.join(backupDir, 'roles.sql');
|
|
356
450
|
|
|
357
451
|
try {
|
|
358
|
-
execSync(`supabase db dump --db-url "${dbUrl}" --role-only -f "${rolesFile}"`, { stdio: 'pipe' });
|
|
452
|
+
execSync(`supabase db dump --db-url "${dbUrl}" --role-only -f "${rolesFile}"`, { stdio: 'pipe', env: { ...process.env, SUPABASE_ACCESS_TOKEN: accessToken || '' } });
|
|
359
453
|
const stats = await fs.stat(rolesFile);
|
|
360
454
|
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
361
455
|
files.push({ filename: 'roles.sql', sizeKB });
|
|
@@ -625,7 +719,7 @@ async function backupStorage(projectId, accessToken, backupDir) {
|
|
|
625
719
|
}
|
|
626
720
|
|
|
627
721
|
// Backup dos Custom Roles via Docker
|
|
628
|
-
async function backupCustomRoles(databaseUrl, backupDir) {
|
|
722
|
+
async function backupCustomRoles(databaseUrl, backupDir, accessToken) {
|
|
629
723
|
try {
|
|
630
724
|
console.log(chalk.gray(' - Exportando Custom Roles via Docker...'));
|
|
631
725
|
|
|
@@ -633,7 +727,7 @@ async function backupCustomRoles(databaseUrl, backupDir) {
|
|
|
633
727
|
|
|
634
728
|
try {
|
|
635
729
|
// ✅ Usar Supabase CLI via Docker para roles
|
|
636
|
-
await execAsync(`supabase db dump --db-url "${databaseUrl}" --role-only -f "${customRolesFile}"
|
|
730
|
+
await execAsync(`supabase db dump --db-url "${databaseUrl}" --role-only -f "${customRolesFile}"`, { env: { ...process.env, SUPABASE_ACCESS_TOKEN: accessToken || '' } });
|
|
637
731
|
|
|
638
732
|
const stats = await fs.stat(customRolesFile);
|
|
639
733
|
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
@@ -652,16 +746,14 @@ async function backupCustomRoles(databaseUrl, backupDir) {
|
|
|
652
746
|
}
|
|
653
747
|
|
|
654
748
|
// Backup das Database Extensions and Settings via SQL
|
|
655
|
-
async function backupDatabaseSettings(projectId, backupDir) {
|
|
749
|
+
async function backupDatabaseSettings(databaseUrl, projectId, backupDir) {
|
|
656
750
|
try {
|
|
657
751
|
console.log(chalk.gray(' - Capturando Database Extensions and Settings...'));
|
|
658
752
|
|
|
659
753
|
const { execSync } = require('child_process');
|
|
660
|
-
const config = await readConfig();
|
|
661
754
|
|
|
662
755
|
// Extrair credenciais da databaseUrl
|
|
663
|
-
const
|
|
664
|
-
const urlMatch = dbUrl.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
|
|
756
|
+
const urlMatch = databaseUrl.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
|
|
665
757
|
|
|
666
758
|
if (!urlMatch) {
|
|
667
759
|
throw new Error('Database URL inválida');
|
package/src/commands/restore.js
CHANGED
|
@@ -4,18 +4,72 @@ const fs = require('fs');
|
|
|
4
4
|
const { readConfig, getSourceProject, getTargetProject } = require('../utils/config');
|
|
5
5
|
const { showBetaBanner } = require('../utils/banner');
|
|
6
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');
|
|
7
10
|
|
|
8
11
|
module.exports = async (options) => {
|
|
9
12
|
showBetaBanner();
|
|
10
13
|
|
|
11
14
|
try {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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'}`));
|
|
16
70
|
|
|
17
71
|
// 1. Listar backups válidos (.backup.gz)
|
|
18
|
-
const validBackups = await listValidBackups(
|
|
72
|
+
const validBackups = await listValidBackups(getValue('SMOONB_OUTPUT_DIR') || './backups');
|
|
19
73
|
|
|
20
74
|
if (validBackups.length === 0) {
|
|
21
75
|
console.error(chalk.red('❌ Nenhum backup válido encontrado'));
|
|
@@ -81,6 +135,26 @@ module.exports = async (options) => {
|
|
|
81
135
|
await restoreRealtimeSettings(selectedBackup.path, targetProject);
|
|
82
136
|
}
|
|
83
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
|
+
|
|
84
158
|
console.log(chalk.green('\n🎉 Restauração completa finalizada!'));
|
|
85
159
|
|
|
86
160
|
} catch (error) {
|
|
@@ -187,7 +261,7 @@ async function askRestoreComponents(backupPath) {
|
|
|
187
261
|
questions.push({
|
|
188
262
|
type: 'confirm',
|
|
189
263
|
name: 'restoreDatabase',
|
|
190
|
-
message: 'Deseja restaurar Database
|
|
264
|
+
message: 'Deseja restaurar Database (S/n):',
|
|
191
265
|
default: true
|
|
192
266
|
});
|
|
193
267
|
|
|
@@ -197,7 +271,7 @@ async function askRestoreComponents(backupPath) {
|
|
|
197
271
|
questions.push({
|
|
198
272
|
type: 'confirm',
|
|
199
273
|
name: 'restoreEdgeFunctions',
|
|
200
|
-
message: 'Deseja restaurar Edge Functions
|
|
274
|
+
message: 'Deseja restaurar Edge Functions (S/n):',
|
|
201
275
|
default: true
|
|
202
276
|
});
|
|
203
277
|
}
|
|
@@ -207,8 +281,8 @@ async function askRestoreComponents(backupPath) {
|
|
|
207
281
|
questions.push({
|
|
208
282
|
type: 'confirm',
|
|
209
283
|
name: 'restoreAuthSettings',
|
|
210
|
-
message: 'Deseja restaurar Auth Settings (
|
|
211
|
-
default:
|
|
284
|
+
message: 'Deseja restaurar Auth Settings (s/N):',
|
|
285
|
+
default: false
|
|
212
286
|
});
|
|
213
287
|
}
|
|
214
288
|
|
|
@@ -218,7 +292,7 @@ async function askRestoreComponents(backupPath) {
|
|
|
218
292
|
questions.push({
|
|
219
293
|
type: 'confirm',
|
|
220
294
|
name: 'restoreStorage',
|
|
221
|
-
message: 'Deseja ver informações de Storage Buckets
|
|
295
|
+
message: 'Deseja ver informações de Storage Buckets (s/N):',
|
|
222
296
|
default: false
|
|
223
297
|
});
|
|
224
298
|
}
|
|
@@ -230,7 +304,7 @@ async function askRestoreComponents(backupPath) {
|
|
|
230
304
|
questions.push({
|
|
231
305
|
type: 'confirm',
|
|
232
306
|
name: 'restoreDatabaseSettings',
|
|
233
|
-
message: 'Deseja restaurar Database Extensions and Settings
|
|
307
|
+
message: 'Deseja restaurar Database Extensions and Settings (s/N):',
|
|
234
308
|
default: false
|
|
235
309
|
});
|
|
236
310
|
}
|
|
@@ -240,8 +314,8 @@ async function askRestoreComponents(backupPath) {
|
|
|
240
314
|
questions.push({
|
|
241
315
|
type: 'confirm',
|
|
242
316
|
name: 'restoreRealtimeSettings',
|
|
243
|
-
message: 'Deseja restaurar Realtime Settings (
|
|
244
|
-
default:
|
|
317
|
+
message: 'Deseja restaurar Realtime Settings (s/N):',
|
|
318
|
+
default: false
|
|
245
319
|
});
|
|
246
320
|
}
|
|
247
321
|
|
|
@@ -401,17 +475,25 @@ async function restoreEdgeFunctions(backupPath, targetProject) {
|
|
|
401
475
|
console.log(chalk.blue('\n⚡ Restaurando Edge Functions...'));
|
|
402
476
|
|
|
403
477
|
try {
|
|
478
|
+
const fs = require('fs').promises;
|
|
404
479
|
const { execSync } = require('child_process');
|
|
405
480
|
const edgeFunctionsDir = path.join(backupPath, 'edge-functions');
|
|
406
481
|
|
|
407
|
-
if (!fs.
|
|
482
|
+
if (!await fs.access(edgeFunctionsDir).then(() => true).catch(() => false)) {
|
|
408
483
|
console.log(chalk.yellow(' ⚠️ Nenhuma Edge Function encontrada no backup'));
|
|
409
484
|
return;
|
|
410
485
|
}
|
|
411
486
|
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
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
|
+
}
|
|
415
497
|
|
|
416
498
|
if (functions.length === 0) {
|
|
417
499
|
console.log(chalk.yellow(' ⚠️ Nenhuma Edge Function encontrada no backup'));
|
|
@@ -420,29 +502,57 @@ async function restoreEdgeFunctions(backupPath, targetProject) {
|
|
|
420
502
|
|
|
421
503
|
console.log(chalk.gray(` - Encontradas ${functions.length} Edge Function(s)`));
|
|
422
504
|
|
|
423
|
-
//
|
|
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
|
+
|
|
424
531
|
console.log(chalk.gray(` - Linkando com projeto ${targetProject.targetProjectId}...`));
|
|
425
532
|
|
|
533
|
+
// Linkar com o projeto destino
|
|
426
534
|
try {
|
|
427
535
|
execSync(`supabase link --project-ref ${targetProject.targetProjectId}`, {
|
|
428
536
|
stdio: 'pipe',
|
|
429
|
-
encoding: 'utf8'
|
|
537
|
+
encoding: 'utf8',
|
|
538
|
+
timeout: 10000,
|
|
539
|
+
env: { ...process.env, SUPABASE_ACCESS_TOKEN: targetProject.targetAccessToken || '' }
|
|
430
540
|
});
|
|
431
541
|
} catch (linkError) {
|
|
432
|
-
console.log(chalk.yellow(
|
|
542
|
+
console.log(chalk.yellow(' ⚠️ Link pode já existir, continuando...'));
|
|
433
543
|
}
|
|
434
544
|
|
|
435
|
-
// Deploy
|
|
545
|
+
// Deploy das Edge Functions
|
|
436
546
|
for (const funcName of functions) {
|
|
437
547
|
console.log(chalk.gray(` - Deployando ${funcName}...`));
|
|
438
548
|
|
|
439
549
|
try {
|
|
440
|
-
const functionPath = path.join(edgeFunctionsDir, funcName);
|
|
441
|
-
|
|
442
550
|
execSync(`supabase functions deploy ${funcName}`, {
|
|
443
|
-
cwd:
|
|
551
|
+
cwd: process.cwd(),
|
|
444
552
|
stdio: 'pipe',
|
|
445
|
-
encoding: 'utf8'
|
|
553
|
+
encoding: 'utf8',
|
|
554
|
+
timeout: 120000,
|
|
555
|
+
env: { ...process.env, SUPABASE_ACCESS_TOKEN: targetProject.targetAccessToken || '' }
|
|
446
556
|
});
|
|
447
557
|
|
|
448
558
|
console.log(chalk.green(` ✅ ${funcName} deployada com sucesso!`));
|
|
@@ -451,11 +561,41 @@ async function restoreEdgeFunctions(backupPath, targetProject) {
|
|
|
451
561
|
}
|
|
452
562
|
}
|
|
453
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
|
+
|
|
454
574
|
} catch (error) {
|
|
455
575
|
console.error(chalk.red(` ❌ Erro ao restaurar Edge Functions: ${error.message}`));
|
|
456
576
|
}
|
|
457
577
|
}
|
|
458
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
|
+
|
|
459
599
|
// Restaurar Storage Buckets (interativo - exibir informações)
|
|
460
600
|
async function restoreStorageBuckets(backupPath, targetProject) {
|
|
461
601
|
console.log(chalk.blue('\n📦 Restaurando Storage Buckets...'));
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const inquirer = require('inquirer');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
|
|
4
|
+
async function mapEnvVariablesInteractively(env, expectedKeys) {
|
|
5
|
+
const finalEnv = { ...env };
|
|
6
|
+
const dePara = {};
|
|
7
|
+
|
|
8
|
+
const allKeys = Object.keys(env);
|
|
9
|
+
|
|
10
|
+
for (const expected of expectedKeys) {
|
|
11
|
+
console.log(chalk.blue(`\n🔧 Mapeando variável: ${expected}`));
|
|
12
|
+
|
|
13
|
+
const choices = [
|
|
14
|
+
...allKeys.map((k, idx) => ({ name: `${idx + 1}. ${k}`, value: k })),
|
|
15
|
+
new inquirer.Separator(),
|
|
16
|
+
{ name: 'Adicionar nova chave com este nome', value: '__ADD_NEW__' }
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const { chosen } = await inquirer.prompt([{
|
|
20
|
+
type: 'list',
|
|
21
|
+
name: 'chosen',
|
|
22
|
+
message: `Selecione a chave correspondente para: ${expected}`,
|
|
23
|
+
choices
|
|
24
|
+
}]);
|
|
25
|
+
|
|
26
|
+
let clientKey = chosen;
|
|
27
|
+
if (chosen === '__ADD_NEW__') {
|
|
28
|
+
clientKey = expected;
|
|
29
|
+
if (Object.prototype.hasOwnProperty.call(finalEnv, clientKey)) {
|
|
30
|
+
// Evitar colisão: gerar sufixo incremental
|
|
31
|
+
let i = 2;
|
|
32
|
+
while (Object.prototype.hasOwnProperty.call(finalEnv, `${clientKey}_${i}`)) i++;
|
|
33
|
+
clientKey = `${clientKey}_${i}`;
|
|
34
|
+
}
|
|
35
|
+
finalEnv[clientKey] = '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const currentValue = finalEnv[clientKey] ?? '';
|
|
39
|
+
const { isCorrect } = await inquirer.prompt([{
|
|
40
|
+
type: 'confirm',
|
|
41
|
+
name: 'isCorrect',
|
|
42
|
+
message: `Valor atual: ${currentValue || '(vazio)'} Este é o valor correto do projeto alvo? (S/n):`,
|
|
43
|
+
default: true
|
|
44
|
+
}]);
|
|
45
|
+
|
|
46
|
+
let valueToWrite = currentValue;
|
|
47
|
+
if (!isCorrect) {
|
|
48
|
+
const { newValue } = await inquirer.prompt([{
|
|
49
|
+
type: 'input',
|
|
50
|
+
name: 'newValue',
|
|
51
|
+
message: `Cole o novo valor para ${clientKey}:`
|
|
52
|
+
}]);
|
|
53
|
+
valueToWrite = newValue || '';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!valueToWrite) {
|
|
57
|
+
const { newValueRequired } = await inquirer.prompt([{
|
|
58
|
+
type: 'input',
|
|
59
|
+
name: 'newValueRequired',
|
|
60
|
+
message: `Valor obrigatório. Informe valor para ${clientKey}:`
|
|
61
|
+
}]);
|
|
62
|
+
valueToWrite = newValueRequired || '';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
finalEnv[clientKey] = valueToWrite;
|
|
66
|
+
if (dePara[clientKey] && dePara[clientKey] !== expected) {
|
|
67
|
+
throw new Error(`Duplicidade de mapeamento detectada para ${clientKey}`);
|
|
68
|
+
}
|
|
69
|
+
dePara[clientKey] = expected;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { finalEnv, dePara };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function askComponentsFlags() {
|
|
76
|
+
const answers = await inquirer.prompt([
|
|
77
|
+
{ type: 'confirm', name: 'includeFunctions', message: 'Deseja incluir Edge Functions (S/n):', default: true },
|
|
78
|
+
{ type: 'confirm', name: 'includeStorage', message: 'Deseja incluir Storage (s/N):', default: false },
|
|
79
|
+
{ type: 'confirm', name: 'includeAuth', message: 'Deseja incluir Auth (s/N):', default: false },
|
|
80
|
+
{ type: 'confirm', name: 'includeRealtime', message: 'Deseja incluir Realtime (s/N):', default: false }
|
|
81
|
+
]);
|
|
82
|
+
return answers;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
mapEnvVariablesInteractively,
|
|
87
|
+
askComponentsFlags
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
|
package/src/utils/env.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const fsp = require('fs').promises;
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
function parseEnvContent(content) {
|
|
6
|
+
const lines = content.replace(/\r\n/g, '\n').split('\n');
|
|
7
|
+
const entries = {};
|
|
8
|
+
for (const line of lines) {
|
|
9
|
+
const trimmed = line.trim();
|
|
10
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
11
|
+
const eqIndex = line.indexOf('=');
|
|
12
|
+
if (eqIndex === -1) continue;
|
|
13
|
+
const key = line.slice(0, eqIndex).trim();
|
|
14
|
+
let value = line.slice(eqIndex + 1);
|
|
15
|
+
// Remove optional quotes
|
|
16
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
17
|
+
value = value.slice(1, -1);
|
|
18
|
+
}
|
|
19
|
+
entries[key] = value;
|
|
20
|
+
}
|
|
21
|
+
return entries;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function stringifyEnv(entries, existingContent) {
|
|
25
|
+
// Best-effort: keep existing comments and order; update or append keys
|
|
26
|
+
const existingLines = (existingContent || '').replace(/\r\n/g, '\n').split('\n');
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
const resultLines = [];
|
|
29
|
+
|
|
30
|
+
for (let line of existingLines) {
|
|
31
|
+
const trimmed = line.trim();
|
|
32
|
+
if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('=')) {
|
|
33
|
+
resultLines.push(line);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const eqIndex = line.indexOf('=');
|
|
37
|
+
const key = line.slice(0, eqIndex).trim();
|
|
38
|
+
if (Object.prototype.hasOwnProperty.call(entries, key)) {
|
|
39
|
+
const rawValue = entries[key] ?? '';
|
|
40
|
+
const needsQuote = /\s|[#]/.test(rawValue);
|
|
41
|
+
const safeValue = needsQuote ? `"${rawValue.replace(/"/g, '\\"')}"` : rawValue;
|
|
42
|
+
resultLines.push(`${key}=${safeValue}`);
|
|
43
|
+
seen.add(key);
|
|
44
|
+
} else {
|
|
45
|
+
resultLines.push(line);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
50
|
+
if (seen.has(key)) continue;
|
|
51
|
+
const rawValue = value ?? '';
|
|
52
|
+
const needsQuote = /\s|[#]/.test(rawValue);
|
|
53
|
+
const safeValue = needsQuote ? `"${rawValue.replace(/"/g, '\\"')}"` : rawValue;
|
|
54
|
+
resultLines.push(`${key}=${safeValue}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Ensure trailing newline
|
|
58
|
+
let out = resultLines.join('\n');
|
|
59
|
+
if (!out.endsWith('\n')) out += '\n';
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function readEnvFile(filePath) {
|
|
64
|
+
try {
|
|
65
|
+
const content = await fsp.readFile(filePath, 'utf8');
|
|
66
|
+
return parseEnvContent(content);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
if (e.code === 'ENOENT') return {};
|
|
69
|
+
throw e;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function writeEnvFile(filePath, entries, options = {}) {
|
|
74
|
+
const dir = path.dirname(filePath);
|
|
75
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
76
|
+
let existing = '';
|
|
77
|
+
try { existing = await fsp.readFile(filePath, 'utf8'); } catch {}
|
|
78
|
+
const content = stringifyEnv(entries, existing);
|
|
79
|
+
await fsp.writeFile(filePath, content, 'utf8');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function backupEnvFile(srcPath, destPath) {
|
|
83
|
+
await fsp.mkdir(path.dirname(destPath), { recursive: true });
|
|
84
|
+
try {
|
|
85
|
+
await fsp.copyFile(srcPath, destPath);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
if (e.code === 'ENOENT') {
|
|
88
|
+
await fsp.writeFile(destPath, '', 'utf8');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
throw e;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function listEnvKeys(env) {
|
|
96
|
+
return Object.keys(env).sort();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
readEnvFile,
|
|
101
|
+
writeEnvFile,
|
|
102
|
+
backupEnvFile,
|
|
103
|
+
listEnvKeys
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const fs = require('fs').promises;
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
async function loadEnvMap(filePath) {
|
|
5
|
+
if (!filePath) return {};
|
|
6
|
+
try {
|
|
7
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
8
|
+
return JSON.parse(content);
|
|
9
|
+
} catch (e) {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function saveEnvMap(map, destPath) {
|
|
15
|
+
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
16
|
+
const json = JSON.stringify(map || {}, null, 2);
|
|
17
|
+
await fs.writeFile(destPath, json, 'utf8');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
loadEnvMap,
|
|
22
|
+
saveEnvMap
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
|
package/src/utils/supabase.js
CHANGED
|
@@ -281,8 +281,8 @@ async function getProjectInfo(projectId) {
|
|
|
281
281
|
* Listar tabelas da database
|
|
282
282
|
*/
|
|
283
283
|
async function listTables() {
|
|
284
|
+
const client = getSupabaseClient();
|
|
284
285
|
try {
|
|
285
|
-
const client = getSupabaseClient();
|
|
286
286
|
|
|
287
287
|
// Usar RPC para listar tabelas
|
|
288
288
|
const { data, error } = await client.rpc('get_tables');
|
|
@@ -315,8 +315,8 @@ async function listTables() {
|
|
|
315
315
|
* Listar extensões instaladas
|
|
316
316
|
*/
|
|
317
317
|
async function listExtensions() {
|
|
318
|
+
const client = getSupabaseClient();
|
|
318
319
|
try {
|
|
319
|
-
const client = getSupabaseClient();
|
|
320
320
|
|
|
321
321
|
// Usar RPC para listar extensões
|
|
322
322
|
const { data, error } = await client.rpc('get_extensions');
|
package/.smoonbrc
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"supabase": {
|
|
3
|
-
"projectId": "xvfgdgdfgdfgdfgdfg",
|
|
4
|
-
"url": "https://dfgdfgdfgdfgdfgdfg.supabase.co",
|
|
5
|
-
"serviceKey": "sdfsdfsdfsdfsdfgyjyuiuyjyujuyjyujuyjyujuy",
|
|
6
|
-
"anonKey": "uyjyujyujyhmnbmbghjghjghghjghjghjghj",
|
|
7
|
-
"databaseUrl": "postgresql://postgres:ghjghjghjghjghjghjghj@db.sdfsdfsdfsdfsdfsdfsdfsdf.supabase.co:5432/postgres",
|
|
8
|
-
"accessToken": "your-personal-access-token-here"
|
|
9
|
-
},
|
|
10
|
-
"backup": {
|
|
11
|
-
"includeFunctions": true,
|
|
12
|
-
"includeStorage": true,
|
|
13
|
-
"includeAuth": true,
|
|
14
|
-
"includeRealtime": true,
|
|
15
|
-
"outputDir": "./backups",
|
|
16
|
-
"pgDumpPath": "C:\\Program Files\\PostgreSQL\\17\\bin\\pg_dump.exe"
|
|
17
|
-
},
|
|
18
|
-
"restore": {
|
|
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
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
package/.smoonbrc.example
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"supabase": {
|
|
3
|
-
"projectId": "your-project-id-here",
|
|
4
|
-
"url": "https://your-project.supabase.co",
|
|
5
|
-
"serviceKey": "your-service-key-here",
|
|
6
|
-
"anonKey": "your-anon-key-here",
|
|
7
|
-
"databaseUrl": "postgresql://postgres:[password]@db.your-project.supabase.co:5432/postgres"
|
|
8
|
-
},
|
|
9
|
-
"backup": {
|
|
10
|
-
"includeFunctions": true,
|
|
11
|
-
"includeStorage": true,
|
|
12
|
-
"includeAuth": true,
|
|
13
|
-
"includeRealtime": true,
|
|
14
|
-
"outputDir": "./backups",
|
|
15
|
-
"pgDumpPath": "C:\\Program Files\\PostgreSQL\\17\\bin\\pg_dump.exe"
|
|
16
|
-
},
|
|
17
|
-
"restore": {
|
|
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
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"timestamp": "2025-10-17T19:18:58.559Z",
|
|
3
|
-
"projectId": "itrnlqsdfsdfsdf",
|
|
4
|
-
"version": "0.1.0-beta",
|
|
5
|
-
"components": {
|
|
6
|
-
"database": false,
|
|
7
|
-
"functions": true,
|
|
8
|
-
"auth": true,
|
|
9
|
-
"storage": true,
|
|
10
|
-
"realtime": true
|
|
11
|
-
},
|
|
12
|
-
"files": {
|
|
13
|
-
"database": null,
|
|
14
|
-
"functions": "functions/",
|
|
15
|
-
"auth": "auth-config.json",
|
|
16
|
-
"storage": "storage/",
|
|
17
|
-
"realtime": "realtime-config.json"
|
|
18
|
-
}
|
|
19
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"timestamp": "2025-10-17T19:52:20.452Z",
|
|
3
|
-
"projectId": "xvfgdgdfgdfgdfgdfg",
|
|
4
|
-
"version": "0.1.0-beta",
|
|
5
|
-
"components": {
|
|
6
|
-
"database": false,
|
|
7
|
-
"functions": true,
|
|
8
|
-
"auth": true,
|
|
9
|
-
"storage": true,
|
|
10
|
-
"realtime": true
|
|
11
|
-
},
|
|
12
|
-
"files": {
|
|
13
|
-
"database": null,
|
|
14
|
-
"functions": "functions/",
|
|
15
|
-
"auth": "auth-config.json",
|
|
16
|
-
"storage": "storage/",
|
|
17
|
-
"realtime": "realtime-config.json"
|
|
18
|
-
}
|
|
19
|
-
}
|
|
File without changes
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"timestamp": "2025-10-17T20:38:13.584Z",
|
|
3
|
-
"projectId": "xvfgdgdfgdfgdfgdfg",
|
|
4
|
-
"version": "0.1.0-beta",
|
|
5
|
-
"components": {
|
|
6
|
-
"database": false,
|
|
7
|
-
"functions": true,
|
|
8
|
-
"auth": true,
|
|
9
|
-
"storage": true,
|
|
10
|
-
"realtime": true
|
|
11
|
-
},
|
|
12
|
-
"files": {
|
|
13
|
-
"database": null,
|
|
14
|
-
"functions": "functions/",
|
|
15
|
-
"auth": "auth-config.json",
|
|
16
|
-
"storage": "storage/",
|
|
17
|
-
"realtime": "realtime-config.json"
|
|
18
|
-
}
|
|
19
|
-
}
|
|
File without changes
|