smoonb 0.0.7 → 0.0.8
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/README.md +164 -169
- package/bin/smoonb.js +7 -23
- package/package.json +1 -1
- package/src/commands/backup.js +140 -249
- package/src/commands/check.js +209 -349
- package/src/commands/functions.js +123 -349
- package/src/commands/restore.js +122 -294
- package/src/index.js +12 -21
- package/src/services/introspect.js +299 -0
- package/src/utils/cli.js +87 -0
- package/src/utils/config.js +140 -0
- package/src/utils/fsx.js +110 -0
- package/src/utils/hash.js +40 -0
- package/src/commands/secrets.js +0 -361
package/src/commands/backup.js
CHANGED
|
@@ -1,286 +1,177 @@
|
|
|
1
|
-
|
|
2
|
-
* Comando de backup completo do projeto Supabase
|
|
3
|
-
* Implementação técnica real baseada em pesquisa extensiva
|
|
4
|
-
*/
|
|
5
|
-
|
|
1
|
+
const { Command } = require('commander');
|
|
6
2
|
const chalk = require('chalk');
|
|
7
|
-
const { execSync } = require('child_process');
|
|
8
|
-
const fs = require('fs');
|
|
9
3
|
const path = require('path');
|
|
10
|
-
const {
|
|
11
|
-
const {
|
|
4
|
+
const { ensureBin, runCommand } = require('../utils/cli');
|
|
5
|
+
const { ensureDir, writeJson, copyDir } = require('../utils/fsx');
|
|
6
|
+
const { sha256 } = require('../utils/hash');
|
|
7
|
+
const { readConfig, validateFor } = require('../utils/config');
|
|
8
|
+
const { IntrospectionService } = require('../services/introspect');
|
|
9
|
+
const { showBetaBanner } = require('../index');
|
|
10
|
+
|
|
11
|
+
const backupCommand = new Command('backup')
|
|
12
|
+
.description('Backup completo do projeto Supabase usando Supabase CLI')
|
|
13
|
+
.option('-o, --output <dir>', 'Diretório de saída')
|
|
14
|
+
.action(async (options) => {
|
|
15
|
+
showBetaBanner();
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
// Verificar se Supabase CLI está disponível
|
|
19
|
+
const supabasePath = await ensureBin('supabase');
|
|
20
|
+
if (!supabasePath) {
|
|
21
|
+
console.error(chalk.red('❌ Supabase CLI não encontrado'));
|
|
22
|
+
console.log(chalk.yellow('💡 Instale o Supabase CLI:'));
|
|
23
|
+
console.log(chalk.yellow(' npm install -g supabase'));
|
|
24
|
+
console.log(chalk.yellow(' ou visite: https://supabase.com/docs/guides/cli'));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
12
27
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
*/
|
|
17
|
-
async function backupCommand(options) {
|
|
18
|
-
console.log(chalk.red.bold('🚀 smoonb - EXPERIMENTAL VERSION'));
|
|
19
|
-
console.log(chalk.red.bold('⚠️ VERSÃO EXPERIMENTAL - NUNCA TESTADA EM PRODUÇÃO!'));
|
|
20
|
-
console.log(chalk.red.bold('🚨 USE POR SUA CONTA E RISCO - Pode causar perda de dados!'));
|
|
21
|
-
console.log(chalk.red.bold('❌ NÃO NOS RESPONSABILIZAMOS por qualquer perda de dados!\n'));
|
|
22
|
-
|
|
23
|
-
console.log(chalk.cyan.bold('🚀 Iniciando backup COMPLETO do projeto Supabase...\n'));
|
|
28
|
+
// Carregar e validar configuração
|
|
29
|
+
const config = await readConfig();
|
|
30
|
+
validateFor(config, 'backup');
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
console.log(chalk.yellow('💡 Opções:'));
|
|
32
|
-
console.log(chalk.gray(' 1. Use: npx smoonb backup --project-id <seu-project-id>'));
|
|
33
|
-
console.log(chalk.gray(' 2. Configure: npx smoonb config --init'));
|
|
34
|
-
console.log(chalk.gray(' 3. Ou defina SUPABASE_PROJECT_ID no ambiente'));
|
|
35
|
-
console.log(chalk.gray(' 4. Ou edite .smoonbrc e configure o projectId'));
|
|
36
|
-
console.log(chalk.gray(' 5. Substitua "your-project-id-here" por seu ID real'));
|
|
37
|
-
process.exit(1);
|
|
38
|
-
}
|
|
32
|
+
const databaseUrl = config.supabase.databaseUrl;
|
|
33
|
+
if (!databaseUrl) {
|
|
34
|
+
console.error(chalk.red('❌ databaseUrl não configurada'));
|
|
35
|
+
console.log(chalk.yellow('💡 Configure databaseUrl no .smoonbrc'));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
39
38
|
|
|
40
|
-
|
|
39
|
+
// Resolver diretório de saída
|
|
40
|
+
const outputDir = options.output || config.backup.outputDir;
|
|
41
|
+
|
|
42
|
+
// Criar diretório de backup com timestamp
|
|
43
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
44
|
+
const backupDir = path.join(outputDir, `backup-${timestamp}`);
|
|
45
|
+
await ensureDir(backupDir);
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const outputDir = config?.backup?.outputDir || options.output;
|
|
45
|
-
|
|
46
|
-
// Criar diretório de backup com timestamp
|
|
47
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
48
|
-
const backupDir = path.resolve(outputDir, `backup-${timestamp}`);
|
|
49
|
-
await fs.promises.mkdir(backupDir, { recursive: true });
|
|
47
|
+
console.log(chalk.blue(`🚀 Iniciando backup do projeto: ${config.supabase.projectId}`));
|
|
48
|
+
console.log(chalk.blue(`📁 Diretório: ${backupDir}`));
|
|
50
49
|
|
|
51
|
-
|
|
50
|
+
// 1. Backup da Database usando Supabase CLI
|
|
51
|
+
console.log(chalk.blue('\n📊 1/3 - Backup da Database PostgreSQL...'));
|
|
52
|
+
await backupDatabaseWithSupabaseCLI(databaseUrl, backupDir);
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (dbBackupFile) {
|
|
57
|
-
console.log(chalk.green('✅ Database backupado:'), path.basename(dbBackupFile));
|
|
58
|
-
} else {
|
|
59
|
-
console.log(chalk.yellow('⚠️ Database não foi backupada (credenciais não configuradas)'));
|
|
60
|
-
}
|
|
54
|
+
// 2. Gerar inventário real
|
|
55
|
+
console.log(chalk.blue('\n🔍 2/3 - Gerando inventário completo...'));
|
|
56
|
+
await generateInventory(config, backupDir);
|
|
61
57
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const functionsDir = await backupEdgeFunctions(projectId, backupDir);
|
|
66
|
-
console.log(chalk.green('✅ Edge Functions backupadas:'), functionsDir);
|
|
67
|
-
}
|
|
58
|
+
// 3. Backup das Edge Functions locais
|
|
59
|
+
console.log(chalk.blue('\n⚡ 3/3 - Backup das Edge Functions locais...'));
|
|
60
|
+
await backupLocalFunctions(backupDir);
|
|
68
61
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
console.log(chalk.blue.bold('\n🔐 3/5 - Backup das configurações de Auth...'));
|
|
72
|
-
const authConfig = await backupAuthSettings(projectId, backupDir);
|
|
73
|
-
console.log(chalk.green('✅ Auth settings backupadas:'), authConfig);
|
|
74
|
-
}
|
|
62
|
+
// Gerar manifesto do backup
|
|
63
|
+
await generateBackupManifest(config, backupDir);
|
|
75
64
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
console.log(chalk.blue.bold('\n📁 4/5 - Backup dos Storage Objects...'));
|
|
79
|
-
const storageBackup = await backupStorageObjects(projectId, backupDir);
|
|
80
|
-
console.log(chalk.green('✅ Storage Objects backupados:'), storageBackup);
|
|
81
|
-
}
|
|
65
|
+
console.log(chalk.green('\n🎉 Backup completo finalizado!'));
|
|
66
|
+
console.log(chalk.blue(`📁 Localização: ${backupDir}`));
|
|
82
67
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const realtimeConfig = await backupRealtimeSettings(projectId, backupDir);
|
|
87
|
-
console.log(chalk.green('✅ Realtime settings backupadas:'), realtimeConfig);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error(chalk.red(`❌ Erro no backup: ${error.message}`));
|
|
70
|
+
process.exit(1);
|
|
88
71
|
}
|
|
72
|
+
});
|
|
89
73
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const manifestPath = path.join(backupDir, 'backup-manifest.json');
|
|
112
|
-
await fs.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
|
113
|
-
|
|
114
|
-
console.log(chalk.green.bold('\n🎉 BACKUP COMPLETO FINALIZADO COM SUCESSO!'));
|
|
115
|
-
console.log(chalk.blue('📁 Diretório:'), backupDir);
|
|
116
|
-
console.log(chalk.blue('🆔 Project ID:'), options.projectId);
|
|
117
|
-
console.log(chalk.blue('📋 Manifesto:'), 'backup-manifest.json');
|
|
118
|
-
console.log(chalk.yellow('\n💡 Este backup inclui TODOS os componentes do Supabase!'));
|
|
119
|
-
console.log(chalk.yellow('🔄 Use "smoonb restore" para restaurar em outro projeto'));
|
|
120
|
-
|
|
74
|
+
// Backup da database usando Supabase CLI
|
|
75
|
+
async function backupDatabaseWithSupabaseCLI(databaseUrl, backupDir) {
|
|
76
|
+
try {
|
|
77
|
+
console.log(chalk.blue(' - Exportando roles...'));
|
|
78
|
+
const { stdout: rolesOutput } = await runCommand(
|
|
79
|
+
`supabase db dump --db-url "${databaseUrl}" -f roles.sql --role-only`
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
console.log(chalk.blue(' - Exportando schema...'));
|
|
83
|
+
const { stdout: schemaOutput } = await runCommand(
|
|
84
|
+
`supabase db dump --db-url "${databaseUrl}" -f schema.sql`
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
console.log(chalk.blue(' - Exportando dados...'));
|
|
88
|
+
const { stdout: dataOutput } = await runCommand(
|
|
89
|
+
`supabase db dump --db-url "${databaseUrl}" -f data.sql --use-copy --data-only`
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
console.log(chalk.green('✅ Database exportada com sucesso'));
|
|
121
93
|
} catch (error) {
|
|
122
|
-
|
|
123
|
-
console.error(chalk.gray('Stack trace:'), error.stack);
|
|
124
|
-
process.exit(1);
|
|
94
|
+
throw new Error(`Falha no backup da database: ${error.message}`);
|
|
125
95
|
}
|
|
126
96
|
}
|
|
127
97
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
* Formato Custom é mais confiável para restauração
|
|
131
|
-
*/
|
|
132
|
-
async function backupDatabase(projectId, outputDir) {
|
|
98
|
+
// Gerar inventário completo
|
|
99
|
+
async function generateInventory(config, backupDir) {
|
|
133
100
|
try {
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// Verificar se a URL contém placeholder de senha
|
|
146
|
-
if (dbUrl.includes('[password]')) {
|
|
147
|
-
console.log(chalk.yellow('⚠️ Database URL contém placeholder [password]'));
|
|
148
|
-
console.log(chalk.gray(' - Substitua [password] pela senha real da database'));
|
|
149
|
-
console.log(chalk.gray(' - Ou configure DATABASE_URL completa no ambiente'));
|
|
150
|
-
return null;
|
|
101
|
+
const introspection = new IntrospectionService(config);
|
|
102
|
+
const inventory = await introspection.generateFullInventory();
|
|
103
|
+
|
|
104
|
+
// Salvar inventário em arquivos separados
|
|
105
|
+
const inventoryDir = path.join(backupDir, 'inventory');
|
|
106
|
+
await ensureDir(inventoryDir);
|
|
107
|
+
|
|
108
|
+
for (const [component, data] of Object.entries(inventory.components)) {
|
|
109
|
+
const filePath = path.join(inventoryDir, `${component}.json`);
|
|
110
|
+
await writeJson(filePath, data);
|
|
151
111
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const pgDumpPath = findPgDumpPath();
|
|
155
|
-
console.log(chalk.gray(` - Usando pg_dump: ${pgDumpPath}`));
|
|
156
|
-
|
|
157
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
158
|
-
const filename = `database-${timestamp}.dump`;
|
|
159
|
-
const filepath = path.join(outputDir, filename);
|
|
160
|
-
|
|
161
|
-
console.log(chalk.gray(' - Executando pg_dump com formato Custom (-Fc)...'));
|
|
162
|
-
|
|
163
|
-
// Usar formato Custom (-Fc) para restauração mais segura
|
|
164
|
-
const command = `"${pgDumpPath}" "${dbUrl}" -Fc -f "${filepath}"`;
|
|
165
|
-
execSync(command, { stdio: 'pipe' });
|
|
166
|
-
|
|
167
|
-
return filepath;
|
|
112
|
+
|
|
113
|
+
console.log(chalk.green('✅ Inventário completo gerado'));
|
|
168
114
|
} catch (error) {
|
|
169
|
-
console.log(chalk.yellow(
|
|
170
|
-
console.log(chalk.gray(' - Verifique se DATABASE_URL está correta'));
|
|
171
|
-
console.log(chalk.gray(' - Verifique se pg_dump está instalado'));
|
|
172
|
-
console.log(chalk.gray(' - Verifique se as credenciais estão corretas'));
|
|
173
|
-
console.log(chalk.gray(' - Configure pgDumpPath no .smoonbrc se necessário'));
|
|
174
|
-
return null;
|
|
115
|
+
console.log(chalk.yellow(`⚠️ Erro ao gerar inventário: ${error.message}`));
|
|
175
116
|
}
|
|
176
117
|
}
|
|
177
118
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
119
|
+
// Backup das Edge Functions locais
|
|
120
|
+
async function backupLocalFunctions(backupDir) {
|
|
121
|
+
const localFunctionsPath = 'supabase/functions';
|
|
122
|
+
|
|
182
123
|
try {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
console.log(chalk.gray(' - Copiando código das Edge Functions...'));
|
|
189
|
-
|
|
190
|
-
// Copiar código das functions (Windows compatible)
|
|
191
|
-
const { execSync } = require('child_process');
|
|
192
|
-
execSync(`xcopy "supabase\\functions\\*" "${functionsBackupDir}\\" /E /I /Y`, { stdio: 'pipe' });
|
|
124
|
+
const fs = require('fs');
|
|
125
|
+
if (fs.existsSync(localFunctionsPath)) {
|
|
126
|
+
const functionsBackupDir = path.join(backupDir, 'functions');
|
|
127
|
+
await copyDir(localFunctionsPath, functionsBackupDir);
|
|
128
|
+
console.log(chalk.green('✅ Edge Functions locais copiadas'));
|
|
193
129
|
} else {
|
|
194
|
-
console.log(chalk.
|
|
195
|
-
|
|
196
|
-
// Criar arquivo placeholder
|
|
197
|
-
const placeholderPath = path.join(functionsBackupDir, 'README.md');
|
|
198
|
-
await fs.promises.writeFile(placeholderPath,
|
|
199
|
-
'# Edge Functions Backup\n\nNenhuma Edge Function local foi encontrada.\nUse o Supabase CLI para fazer backup das functions remotas.'
|
|
200
|
-
);
|
|
130
|
+
console.log(chalk.yellow('⚠️ Diretório supabase/functions não encontrado'));
|
|
201
131
|
}
|
|
202
|
-
|
|
203
|
-
return functionsBackupDir;
|
|
204
132
|
} catch (error) {
|
|
205
|
-
console.log(chalk.yellow(
|
|
206
|
-
return null;
|
|
133
|
+
console.log(chalk.yellow(`⚠️ Erro ao copiar Edge Functions: ${error.message}`));
|
|
207
134
|
}
|
|
208
135
|
}
|
|
209
136
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
137
|
+
// Gerar manifesto do backup
|
|
138
|
+
async function generateBackupManifest(config, backupDir) {
|
|
139
|
+
const manifest = {
|
|
140
|
+
created_at: new Date().toISOString(),
|
|
141
|
+
project_id: config.supabase.projectId,
|
|
142
|
+
smoonb_version: require('../../package.json').version,
|
|
143
|
+
backup_type: 'complete',
|
|
144
|
+
files: {
|
|
145
|
+
roles: 'roles.sql',
|
|
146
|
+
schema: 'schema.sql',
|
|
147
|
+
data: 'data.sql'
|
|
148
|
+
},
|
|
149
|
+
hashes: {},
|
|
150
|
+
inventory: {}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Calcular hashes dos arquivos SQL
|
|
154
|
+
const fs = require('fs');
|
|
155
|
+
for (const [type, filename] of Object.entries(manifest.files)) {
|
|
156
|
+
const filePath = path.join(backupDir, filename);
|
|
157
|
+
if (fs.existsSync(filePath)) {
|
|
158
|
+
manifest.hashes[type] = await sha256(filePath);
|
|
159
|
+
}
|
|
232
160
|
}
|
|
233
|
-
}
|
|
234
161
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const storageConfig = {
|
|
245
|
-
timestamp: new Date().toISOString(),
|
|
246
|
-
projectId: projectId,
|
|
247
|
-
buckets: [],
|
|
248
|
-
objects: []
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
const storageConfigPath = path.join(storageBackupDir, 'storage-config.json');
|
|
252
|
-
await fs.promises.writeFile(storageConfigPath, JSON.stringify(storageConfig, null, 2));
|
|
253
|
-
|
|
254
|
-
console.log(chalk.gray(' - Configurações de Storage exportadas'));
|
|
255
|
-
return storageBackupDir;
|
|
256
|
-
} catch (error) {
|
|
257
|
-
console.log(chalk.yellow('⚠️ Backup dos Storage Objects falhou:'), error.message);
|
|
258
|
-
return null;
|
|
162
|
+
// Adicionar referências ao inventário
|
|
163
|
+
const inventoryDir = path.join(backupDir, 'inventory');
|
|
164
|
+
if (fs.existsSync(inventoryDir)) {
|
|
165
|
+
const inventoryFiles = fs.readdirSync(inventoryDir);
|
|
166
|
+
manifest.inventory = inventoryFiles.reduce((acc, file) => {
|
|
167
|
+
const component = path.basename(file, '.json');
|
|
168
|
+
acc[component] = `inventory/${file}`;
|
|
169
|
+
return acc;
|
|
170
|
+
}, {});
|
|
259
171
|
}
|
|
260
|
-
}
|
|
261
172
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
*/
|
|
265
|
-
async function backupRealtimeSettings(projectId, outputDir) {
|
|
266
|
-
try {
|
|
267
|
-
const realtimeConfig = {
|
|
268
|
-
timestamp: new Date().toISOString(),
|
|
269
|
-
projectId: projectId,
|
|
270
|
-
enabled: false,
|
|
271
|
-
channels: [],
|
|
272
|
-
settings: {}
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
const realtimeConfigPath = path.join(outputDir, 'realtime-config.json');
|
|
276
|
-
await fs.promises.writeFile(realtimeConfigPath, JSON.stringify(realtimeConfig, null, 2));
|
|
277
|
-
|
|
278
|
-
console.log(chalk.gray(' - Configurações de Realtime exportadas'));
|
|
279
|
-
return realtimeConfigPath;
|
|
280
|
-
} catch (error) {
|
|
281
|
-
console.log(chalk.yellow('⚠️ Backup das configurações de Realtime falhou:'), error.message);
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
173
|
+
const manifestPath = path.join(backupDir, 'backup-manifest.json');
|
|
174
|
+
await writeJson(manifestPath, manifest);
|
|
284
175
|
}
|
|
285
176
|
|
|
286
|
-
module.exports = backupCommand;
|
|
177
|
+
module.exports = backupCommand;
|