smoonb 0.0.60 → 0.0.62
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 +36 -10
- package/bin/smoonb.js +104 -6
- package/package.json +1 -1
- package/src/commands/backup/index.js +3 -4
- package/src/commands/check.js +2 -2
- package/src/commands/import.js +63 -16
- package/src/commands/restore/index.js +1 -1
- package/src/interactive/envMapper.js +125 -10
package/README.md
CHANGED
|
@@ -244,20 +244,28 @@ npx smoonb restore
|
|
|
244
244
|
|
|
245
245
|
### Importar Backup do Dashboard do Supabase
|
|
246
246
|
|
|
247
|
-
Se você baixou um backup diretamente do Dashboard do Supabase (formato `.backup.gz`), você pode importá-lo para o formato esperado pelo smoonb
|
|
247
|
+
Se você baixou um backup diretamente do Dashboard do Supabase (formato `.backup.gz`), você pode importá-lo para o formato esperado pelo smoonb. O comando também suporta importar arquivos de storage (`.storage.zip`) opcionalmente.
|
|
248
248
|
|
|
249
|
+
**Importar apenas database:**
|
|
249
250
|
```bash
|
|
250
251
|
npx smoonb import --file "caminho/completo/para/db_cluster-04-03-2024@14-16-59.backup.gz"
|
|
251
252
|
```
|
|
252
253
|
|
|
253
|
-
**
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
4. Copia o arquivo para a pasta criada
|
|
258
|
-
5. Deixa o backup pronto para ser encontrado pelo comando `restore`
|
|
254
|
+
**Importar database e storage juntos:**
|
|
255
|
+
```bash
|
|
256
|
+
npx smoonb import --file "backup.backup.gz" --storage "meu-projeto.storage.zip"
|
|
257
|
+
```
|
|
259
258
|
|
|
260
|
-
**
|
|
259
|
+
**O que o comando faz:**
|
|
260
|
+
1. Lê o arquivo `.backup.gz` do Dashboard (obrigatório)
|
|
261
|
+
2. Se fornecido, lê o arquivo `.storage.zip` do Dashboard (opcional)
|
|
262
|
+
3. Extrai informações do nome do arquivo de backup (data e hora)
|
|
263
|
+
4. Cria uma pasta de backup no formato esperado (`backup-YYYY-MM-DD-HH-MM-SS`)
|
|
264
|
+
5. Copia o arquivo de backup para a pasta criada
|
|
265
|
+
6. Se fornecido, copia o arquivo de storage para a mesma pasta
|
|
266
|
+
7. Deixa o backup pronto para ser encontrado pelo comando `restore`
|
|
267
|
+
|
|
268
|
+
**Exemplo completo - Apenas database:**
|
|
261
269
|
```bash
|
|
262
270
|
# 1. Baixar backup do Dashboard do Supabase
|
|
263
271
|
# Arquivo: db_cluster-04-03-2024@14-16-59.backup.gz
|
|
@@ -270,8 +278,26 @@ npx smoonb restore
|
|
|
270
278
|
# O backup importado aparecerá na lista de backups disponíveis
|
|
271
279
|
```
|
|
272
280
|
|
|
281
|
+
**Exemplo completo - Database e Storage:**
|
|
282
|
+
```bash
|
|
283
|
+
# 1. Baixar backup e storage do Dashboard do Supabase
|
|
284
|
+
# Arquivos:
|
|
285
|
+
# - db_cluster-04-03-2024@14-16-59.backup.gz
|
|
286
|
+
# - meu-projeto.storage.zip
|
|
287
|
+
|
|
288
|
+
# 2. Importar ambos os arquivos
|
|
289
|
+
npx smoonb import --file "C:\Downloads\db_cluster-04-03-2024@14-16-59.backup.gz" --storage "C:\Downloads\meu-projeto.storage.zip"
|
|
290
|
+
|
|
291
|
+
# 3. Restaurar o backup importado
|
|
292
|
+
npx smoonb restore
|
|
293
|
+
# O backup importado aparecerá na lista de backups disponíveis
|
|
294
|
+
```
|
|
295
|
+
|
|
273
296
|
**Importante:**
|
|
274
|
-
- O arquivo deve estar no formato do Dashboard: `db_cluster-DD-MM-YYYY@HH-MM-SS.backup.gz`
|
|
297
|
+
- O arquivo de backup é **obrigatório** e deve estar no formato do Dashboard: `db_cluster-DD-MM-YYYY@HH-MM-SS.backup.gz`
|
|
298
|
+
- O arquivo de storage é **opcional** e deve estar no formato: `*.storage.zip`
|
|
299
|
+
- O storage depende de um backup, mas o backup não depende do storage
|
|
300
|
+
- Ambos os arquivos serão copiados para a mesma pasta de backup
|
|
275
301
|
- O caminho pode ser absoluto ou relativo
|
|
276
302
|
- O comando criará a estrutura de pastas necessária automaticamente
|
|
277
303
|
|
|
@@ -296,7 +322,7 @@ npx smoonb check
|
|
|
296
322
|
|---------|-----------|
|
|
297
323
|
| `npx smoonb backup` | Backup completo interativo usando Docker |
|
|
298
324
|
| `npx smoonb restore` | Restauração interativa usando psql (Docker) |
|
|
299
|
-
| `npx smoonb import --file <path
|
|
325
|
+
| `npx smoonb import --file <path> [--storage <path>]` | Importar arquivo .backup.gz e opcionalmente .storage.zip do Dashboard do Supabase |
|
|
300
326
|
| `npx smoonb check` | Verificação de integridade pós-restore |
|
|
301
327
|
|
|
302
328
|
## 🏗️ Arquitetura Técnica
|
package/bin/smoonb.js
CHANGED
|
@@ -40,6 +40,37 @@ program
|
|
|
40
40
|
|
|
41
41
|
🔄 ATUALIZAR PARA ÚLTIMA VERSÃO:
|
|
42
42
|
npm install smoonb@latest
|
|
43
|
+
|
|
44
|
+
📚 MANUAL DE USO - EXEMPLOS COMPLETOS:
|
|
45
|
+
|
|
46
|
+
${chalk.yellow.bold('1. BACKUP - Fazer backup completo do projeto:')}
|
|
47
|
+
${chalk.white('npx smoonb backup')}
|
|
48
|
+
${chalk.gray('# Processo interativo que captura todos os componentes do Supabase')}
|
|
49
|
+
|
|
50
|
+
${chalk.white('npx smoonb backup --skip-realtime')}
|
|
51
|
+
${chalk.gray('# Pula a captura interativa de Realtime Settings')}
|
|
52
|
+
|
|
53
|
+
${chalk.yellow.bold('2. RESTORE - Restaurar backup em um projeto:')}
|
|
54
|
+
${chalk.white('npx smoonb restore')}
|
|
55
|
+
${chalk.gray('# Processo interativo que lista backups disponíveis e permite escolher')}
|
|
56
|
+
|
|
57
|
+
${chalk.yellow.bold('3. IMPORT - Importar backup do Dashboard do Supabase:')}
|
|
58
|
+
${chalk.white('npx smoonb import --file "C:\\Downloads\\db_cluster-04-03-2024@14-16-59.backup.gz"')}
|
|
59
|
+
${chalk.gray('# Importa apenas o arquivo de database')}
|
|
60
|
+
|
|
61
|
+
${chalk.white('npx smoonb import --file "backup.backup.gz" --storage "meu-projeto.storage.zip"')}
|
|
62
|
+
${chalk.gray('# Importa database e storage juntos (storage é opcional)')}
|
|
63
|
+
|
|
64
|
+
${chalk.yellow.bold('4. CHECK - Verificar integridade após restauração:')}
|
|
65
|
+
${chalk.white('npx smoonb check')}
|
|
66
|
+
${chalk.gray('# Verifica conexão, extensões, tabelas, RLS, Realtime e Storage')}
|
|
67
|
+
|
|
68
|
+
${chalk.yellow.bold('💡 DICAS IMPORTANTES:')}
|
|
69
|
+
${chalk.white('• O comando import requer um arquivo de backup (.backup.gz), mas o storage (.storage.zip) é opcional')}
|
|
70
|
+
${chalk.white('• O storage depende de um backup, mas o backup não depende do storage')}
|
|
71
|
+
${chalk.white('• Use caminhos absolutos ou relativos para os arquivos no comando import')}
|
|
72
|
+
${chalk.white('• O formato do arquivo de backup deve ser: db_cluster-DD-MM-YYYY@HH-MM-SS.backup.gz')}
|
|
73
|
+
${chalk.white('• O formato do arquivo de storage deve ser: *.storage.zip')}
|
|
43
74
|
`);
|
|
44
75
|
});
|
|
45
76
|
|
|
@@ -47,28 +78,95 @@ program
|
|
|
47
78
|
program
|
|
48
79
|
.command('backup')
|
|
49
80
|
.description('Fazer backup completo do projeto Supabase usando Supabase CLI')
|
|
50
|
-
.option('-o, --output <dir>', 'Diretório de saída do backup')
|
|
51
81
|
.option('--skip-realtime', 'Pular captura interativa de Realtime Settings')
|
|
82
|
+
.addHelpText('after', `
|
|
83
|
+
${chalk.yellow.bold('Exemplos:')}
|
|
84
|
+
${chalk.white('npx smoonb backup')}
|
|
85
|
+
${chalk.gray('# Processo interativo completo')}
|
|
86
|
+
|
|
87
|
+
${chalk.white('npx smoonb backup --skip-realtime')}
|
|
88
|
+
${chalk.gray('# Pula configuração de Realtime Settings')}
|
|
89
|
+
|
|
90
|
+
${chalk.yellow.bold('O que é capturado:')}
|
|
91
|
+
• Database PostgreSQL (pg_dumpall + SQL separado)
|
|
92
|
+
• Database Extensions and Settings
|
|
93
|
+
• Custom Roles
|
|
94
|
+
• Edge Functions (download automático)
|
|
95
|
+
• Auth Settings (via Management API)
|
|
96
|
+
• Storage Buckets (metadados via Management API)
|
|
97
|
+
• Realtime Settings (7 parâmetros interativos)
|
|
98
|
+
• Supabase .temp (arquivos temporários)
|
|
99
|
+
• Migrations (todas as migrations do projeto)
|
|
100
|
+
`)
|
|
52
101
|
.action(commands.backup);
|
|
53
102
|
|
|
54
103
|
program
|
|
55
104
|
.command('restore')
|
|
56
105
|
.description('Restaurar backup completo usando psql (modo interativo)')
|
|
57
|
-
.
|
|
106
|
+
.addHelpText('after', `
|
|
107
|
+
${chalk.yellow.bold('Exemplos:')}
|
|
108
|
+
${chalk.white('npx smoonb restore')}
|
|
109
|
+
${chalk.gray('# Processo interativo que lista backups disponíveis')}
|
|
110
|
+
|
|
111
|
+
${chalk.yellow.bold('Fluxo do restore:')}
|
|
112
|
+
1. Validação Docker
|
|
113
|
+
2. Consentimento para ler/escrever .env.local
|
|
114
|
+
3. Mapeamento de variáveis de ambiente
|
|
115
|
+
4. Seleção de backup disponível
|
|
116
|
+
5. Seleção de componentes para restaurar
|
|
117
|
+
6. Resumo detalhado e confirmação
|
|
118
|
+
7. Execução da restauração
|
|
119
|
+
|
|
120
|
+
${chalk.yellow.bold('Formatos suportados:')}
|
|
121
|
+
• .backup.gz (compactado) - Descompacta automaticamente
|
|
122
|
+
• .backup (descompactado) - Restaura diretamente
|
|
123
|
+
`)
|
|
58
124
|
.action(commands.restore);
|
|
59
125
|
|
|
60
126
|
program
|
|
61
127
|
.command('check')
|
|
62
128
|
.description('Verificar integridade do projeto Supabase após restauração')
|
|
63
|
-
.
|
|
129
|
+
.addHelpText('after', `
|
|
130
|
+
${chalk.yellow.bold('Exemplos:')}
|
|
131
|
+
${chalk.white('npx smoonb check')}
|
|
132
|
+
${chalk.gray('# Verifica integridade e exibe relatório no console')}
|
|
133
|
+
|
|
134
|
+
${chalk.yellow.bold('O que é verificado:')}
|
|
135
|
+
• Conexão com database
|
|
136
|
+
• Extensões PostgreSQL instaladas
|
|
137
|
+
• Tabelas criadas
|
|
138
|
+
• Políticas RLS (Row Level Security)
|
|
139
|
+
• Publicações Realtime
|
|
140
|
+
• Buckets de Storage
|
|
141
|
+
`)
|
|
64
142
|
.action(commands.check);
|
|
65
143
|
|
|
66
144
|
program
|
|
67
145
|
.command('import')
|
|
68
|
-
.description('Importar arquivo .backup.gz do Dashboard do Supabase')
|
|
69
|
-
.requiredOption('
|
|
146
|
+
.description('Importar arquivo .backup.gz e opcionalmente .storage.zip do Dashboard do Supabase')
|
|
147
|
+
.requiredOption('--file <path>', 'Caminho completo do arquivo .backup.gz a importar')
|
|
148
|
+
.option('--storage <path>', 'Caminho completo do arquivo .storage.zip a importar (opcional)')
|
|
149
|
+
.addHelpText('after', `
|
|
150
|
+
${chalk.yellow.bold('Exemplos:')}
|
|
151
|
+
${chalk.white('npx smoonb import --file "C:\\Downloads\\db_cluster-04-03-2024@14-16-59.backup.gz"')}
|
|
152
|
+
${chalk.gray('# Importa apenas o arquivo de database')}
|
|
153
|
+
|
|
154
|
+
${chalk.white('npx smoonb import --file "backup.backup.gz" --storage "meu-projeto.storage.zip"')}
|
|
155
|
+
${chalk.gray('# Importa database e storage juntos')}
|
|
156
|
+
|
|
157
|
+
${chalk.yellow.bold('Formato dos arquivos:')}
|
|
158
|
+
• Backup: db_cluster-DD-MM-YYYY@HH-MM-SS.backup.gz (obrigatório)
|
|
159
|
+
• Storage: *.storage.zip (opcional)
|
|
160
|
+
|
|
161
|
+
${chalk.yellow.bold('Importante:')}
|
|
162
|
+
• O arquivo de backup é obrigatório
|
|
163
|
+
• O arquivo de storage é opcional e depende de um backup
|
|
164
|
+
• Ambos os arquivos serão copiados para a mesma pasta de backup
|
|
165
|
+
• O backup importado ficará disponível para o comando restore
|
|
166
|
+
• Use caminhos absolutos ou relativos
|
|
167
|
+
`)
|
|
70
168
|
.action(async (options) => {
|
|
71
|
-
await commands.import({ file: options.file });
|
|
169
|
+
await commands.import({ file: options.file, storage: options.storage });
|
|
72
170
|
});
|
|
73
171
|
|
|
74
172
|
// Tratamento de erros
|
package/package.json
CHANGED
|
@@ -66,8 +66,7 @@ module.exports = async (options) => {
|
|
|
66
66
|
const second = String(now.getSeconds()).padStart(2, '0');
|
|
67
67
|
|
|
68
68
|
// Resolver diretório de saída
|
|
69
|
-
const
|
|
70
|
-
const backupDir = path.join(defaultOutput, `backup-${year}-${month}-${day}-${hour}-${minute}-${second}`);
|
|
69
|
+
const backupDir = path.join(defaultOutputDir, `backup-${year}-${month}-${day}-${hour}-${minute}-${second}`);
|
|
71
70
|
await ensureDir(backupDir);
|
|
72
71
|
|
|
73
72
|
// Backup e mapeamento do .env.local
|
|
@@ -78,11 +77,11 @@ module.exports = async (options) => {
|
|
|
78
77
|
console.log(chalk.blue(`📁 Backup do .env.local: ${path.relative(process.cwd(), envBackupPath)}`));
|
|
79
78
|
|
|
80
79
|
const expectedKeys = [
|
|
80
|
+
'SUPABASE_PROJECT_ID',
|
|
81
81
|
'NEXT_PUBLIC_SUPABASE_URL',
|
|
82
82
|
'NEXT_PUBLIC_SUPABASE_ANON_KEY',
|
|
83
83
|
'SUPABASE_SERVICE_ROLE_KEY',
|
|
84
84
|
'SUPABASE_DB_URL',
|
|
85
|
-
'SUPABASE_PROJECT_ID',
|
|
86
85
|
'SUPABASE_ACCESS_TOKEN',
|
|
87
86
|
'SMOONB_OUTPUT_DIR'
|
|
88
87
|
];
|
|
@@ -98,7 +97,7 @@ module.exports = async (options) => {
|
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
// Recalcular outputDir a partir do ENV mapeado
|
|
101
|
-
const resolvedOutputDir =
|
|
100
|
+
const resolvedOutputDir = getValue('SMOONB_OUTPUT_DIR') || defaultOutputDir;
|
|
102
101
|
|
|
103
102
|
// Se mudou o outputDir, movemos o backupDir inicial para o novo local mantendo timestamp
|
|
104
103
|
const finalBackupDir = backupDir.startsWith(path.resolve(resolvedOutputDir))
|
package/src/commands/check.js
CHANGED
|
@@ -7,7 +7,7 @@ const { IntrospectionService } = require('../services/introspect');
|
|
|
7
7
|
const { showBetaBanner } = require('../utils/banner');
|
|
8
8
|
|
|
9
9
|
// Exportar FUNÇÃO em vez de objeto Command
|
|
10
|
-
module.exports = async (
|
|
10
|
+
module.exports = async () => {
|
|
11
11
|
showBetaBanner();
|
|
12
12
|
|
|
13
13
|
try {
|
|
@@ -37,7 +37,7 @@ module.exports = async (options) => {
|
|
|
37
37
|
const report = await performChecks(config, databaseUrl);
|
|
38
38
|
|
|
39
39
|
// Salvar relatório
|
|
40
|
-
const reportPath = path.resolve(
|
|
40
|
+
const reportPath = path.resolve('check-report.json');
|
|
41
41
|
await writeJson(reportPath, report);
|
|
42
42
|
|
|
43
43
|
// Mostrar resumo
|
package/src/commands/import.js
CHANGED
|
@@ -4,38 +4,71 @@ const fs = require('fs').promises;
|
|
|
4
4
|
const { ensureDir } = require('../utils/fsx');
|
|
5
5
|
const { readEnvFile } = require('../utils/env');
|
|
6
6
|
const { showBetaBanner } = require('../utils/banner');
|
|
7
|
+
const { confirm } = require('../utils/prompt');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
* Comando para importar arquivo .backup.gz do Dashboard do Supabase
|
|
10
|
+
* Comando para importar arquivo .backup.gz e opcionalmente .storage.zip do Dashboard do Supabase
|
|
10
11
|
*/
|
|
11
12
|
module.exports = async (options) => {
|
|
12
13
|
showBetaBanner();
|
|
13
14
|
|
|
14
15
|
try {
|
|
15
|
-
//
|
|
16
|
+
// Termo de uso e aviso de risco
|
|
17
|
+
console.log(chalk.yellow.bold('\n⚠️ TERMO DE USO E AVISO DE RISCO\n'));
|
|
18
|
+
console.log(chalk.white('Ao prosseguir, você reconhece e concorda que o Supa Moonbase (smoonb) é fornecido "NO ESTADO EM QUE SE ENCONTRA" ("AS IS") e "CONFORME DISPONIBILIDADE", sem garantias de qualquer natureza—expressas, implícitas ou legais—incluindo, sem limitação, garantias de comercialização, adequação a um fim específico e não violação, na máxima extensão permitida pela lei aplicável. Operações de backup e restauração envolvem riscos, os ambientes variam amplamente e não é possível prever ou validar todas as configurações dos usuários. Você é o único responsável por validar seu ambiente, manter cópias independentes e verificar os resultados antes de utilizá-los em produção. O Supa Moonbase (smoonb) é construído com repositórios públicos, auditáveis e software livre, para auxiliar pessoas a simplificar seus fluxos, sem com isso criar qualquer garantia, promessa de suporte ou compromisso de nível de serviço.\n'));
|
|
19
|
+
console.log(chalk.white('Limitação de responsabilidade (PT-BR) — Na máxima extensão permitida por lei, a Goalmoon, seus contribuidores e licenciadores não serão responsáveis por danos indiretos, incidentais, especiais, consequentes, exemplares ou punitivos (incluindo perda de dados, interrupção de negócios ou lucros cessantes) decorrentes do uso, incapacidade de uso, das operações de backup/restauração realizadas com, ou dos resultados gerados pelo Supa Moonbase (smoonb). Em qualquer hipótese, a responsabilidade total por todas as reivindicações relacionadas ao Supa Moonbase (smoonb) não excederá o valor pago por você pelo Supa Moonbase (smoonb) nos 12 meses anteriores ao evento. Nada neste aviso exclui ou limita responsabilidades onde tais limites sejam proibidos por lei, incluindo (conforme aplicável) dolo ou culpa grave.\n'));
|
|
20
|
+
console.log(chalk.white('Observação para consumidores no Brasil (PT-BR) — Para consumidores brasileiros, este aviso não afasta direitos irrenunciáveis previstos no Código de Defesa do Consumidor (CDC); qualquer limitação aqui prevista só se aplica nos limites da lei e não impede a indenização obrigatória quando cabível.\n'));
|
|
21
|
+
|
|
22
|
+
const termsAccepted = await confirm('Você aceita os Termos de Uso e o Aviso de Risco de Importação?', true);
|
|
23
|
+
if (!termsAccepted) {
|
|
24
|
+
console.log(chalk.red('🚫 Operação cancelada pelo usuário.'));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Validar que o arquivo de backup foi fornecido
|
|
16
29
|
if (!options.file) {
|
|
17
|
-
console.error(chalk.red('❌ Arquivo não fornecido'));
|
|
18
|
-
console.log(chalk.yellow('💡 Use: npx smoonb
|
|
19
|
-
console.log(chalk.
|
|
30
|
+
console.error(chalk.red('❌ Arquivo de backup não fornecido'));
|
|
31
|
+
console.log(chalk.yellow('💡 Use: npx smoonb import --file <caminho-do-backup> [--storage <caminho-do-storage>]'));
|
|
32
|
+
console.log(chalk.white(' Exemplo: npx smoonb import --file "C:\\Downloads\\db_cluster-04-03-2024@14-16-59.backup.gz"'));
|
|
33
|
+
console.log(chalk.white(' Exemplo com storage: npx smoonb import --file "backup.backup.gz" --storage "meu-projeto.storage.zip"'));
|
|
20
34
|
process.exit(1);
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
const sourceFile = path.resolve(options.file);
|
|
24
38
|
|
|
25
|
-
// Verificar se o arquivo existe
|
|
39
|
+
// Verificar se o arquivo de backup existe
|
|
26
40
|
try {
|
|
27
41
|
await fs.access(sourceFile);
|
|
28
42
|
} catch {
|
|
29
|
-
console.error(chalk.red(`❌ Arquivo não encontrado: ${sourceFile}`));
|
|
43
|
+
console.error(chalk.red(`❌ Arquivo de backup não encontrado: ${sourceFile}`));
|
|
30
44
|
process.exit(1);
|
|
31
45
|
}
|
|
32
46
|
|
|
33
|
-
// Verificar se é um arquivo .backup.gz
|
|
47
|
+
// Verificar se é um arquivo .backup.gz ou .backup
|
|
34
48
|
if (!sourceFile.endsWith('.backup.gz') && !sourceFile.endsWith('.backup')) {
|
|
35
|
-
console.error(chalk.red('❌ Arquivo deve ser .backup.gz ou .backup'));
|
|
49
|
+
console.error(chalk.red('❌ Arquivo de backup deve ser .backup.gz ou .backup'));
|
|
36
50
|
process.exit(1);
|
|
37
51
|
}
|
|
38
52
|
|
|
53
|
+
// Validar arquivo de storage se fornecido
|
|
54
|
+
let sourceStorageFile = null;
|
|
55
|
+
if (options.storage) {
|
|
56
|
+
sourceStorageFile = path.resolve(options.storage);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await fs.access(sourceStorageFile);
|
|
60
|
+
} catch {
|
|
61
|
+
console.error(chalk.red(`❌ Arquivo de storage não encontrado: ${sourceStorageFile}`));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Verificar se é um arquivo .storage.zip
|
|
66
|
+
if (!sourceStorageFile.endsWith('.storage.zip')) {
|
|
67
|
+
console.error(chalk.red('❌ Arquivo de storage deve ser .storage.zip'));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
39
72
|
// Ler .env.local para obter SMOONB_OUTPUT_DIR
|
|
40
73
|
const envPath = path.join(process.cwd(), '.env.local');
|
|
41
74
|
let outputDir = './backups';
|
|
@@ -48,15 +81,15 @@ module.exports = async (options) => {
|
|
|
48
81
|
console.log(chalk.yellow('⚠️ Não foi possível ler .env.local, usando diretório padrão: ./backups'));
|
|
49
82
|
}
|
|
50
83
|
|
|
51
|
-
// Extrair informações do nome do arquivo
|
|
84
|
+
// Extrair informações do nome do arquivo de backup
|
|
52
85
|
// Formato esperado: db_cluster-DD-MM-YYYY@HH-MM-SS.backup.gz
|
|
53
86
|
const fileName = path.basename(sourceFile);
|
|
54
87
|
const match = fileName.match(/db_cluster-(\d{2})-(\d{2})-(\d{4})@(\d{2})-(\d{2})-(\d{2})\.backup(\.gz)?/);
|
|
55
88
|
|
|
56
89
|
if (!match) {
|
|
57
|
-
console.error(chalk.red('❌ Nome do arquivo não está no formato esperado do Dashboard'));
|
|
90
|
+
console.error(chalk.red('❌ Nome do arquivo de backup não está no formato esperado do Dashboard'));
|
|
58
91
|
console.log(chalk.yellow('💡 Formato esperado: db_cluster-DD-MM-YYYY@HH-MM-SS.backup.gz'));
|
|
59
|
-
console.log(chalk.
|
|
92
|
+
console.log(chalk.white(` Arquivo recebido: ${fileName}`));
|
|
60
93
|
process.exit(1);
|
|
61
94
|
}
|
|
62
95
|
|
|
@@ -70,17 +103,31 @@ module.exports = async (options) => {
|
|
|
70
103
|
await ensureDir(backupDir);
|
|
71
104
|
console.log(chalk.blue(`📁 Criando diretório de backup: ${backupDirName}`));
|
|
72
105
|
|
|
73
|
-
// Copiar arquivo para o diretório de backup
|
|
106
|
+
// Copiar arquivo de backup para o diretório de backup
|
|
74
107
|
const destFile = path.join(backupDir, fileName);
|
|
75
108
|
await fs.copyFile(sourceFile, destFile);
|
|
76
109
|
|
|
77
|
-
// Obter tamanho do arquivo
|
|
110
|
+
// Obter tamanho do arquivo de backup
|
|
78
111
|
const stats = await fs.stat(destFile);
|
|
79
112
|
const sizeMB = (stats.size / (1024 * 1024)).toFixed(2);
|
|
80
113
|
|
|
81
|
-
console.log(chalk.green(`✅ Arquivo importado com sucesso!`));
|
|
114
|
+
console.log(chalk.green(`✅ Arquivo de backup importado com sucesso!`));
|
|
115
|
+
console.log(chalk.blue(`📦 Backup: ${fileName} (${sizeMB} MB)`));
|
|
116
|
+
|
|
117
|
+
// Copiar arquivo de storage se fornecido
|
|
118
|
+
if (sourceStorageFile) {
|
|
119
|
+
const storageFileName = path.basename(sourceStorageFile);
|
|
120
|
+
const destStorageFile = path.join(backupDir, storageFileName);
|
|
121
|
+
await fs.copyFile(sourceStorageFile, destStorageFile);
|
|
122
|
+
|
|
123
|
+
const storageStats = await fs.stat(destStorageFile);
|
|
124
|
+
const storageSizeMB = (storageStats.size / (1024 * 1024)).toFixed(2);
|
|
125
|
+
|
|
126
|
+
console.log(chalk.green(`✅ Arquivo de storage importado com sucesso!`));
|
|
127
|
+
console.log(chalk.blue(`📦 Storage: ${storageFileName} (${storageSizeMB} MB)`));
|
|
128
|
+
}
|
|
129
|
+
|
|
82
130
|
console.log(chalk.blue(`📁 Localização: ${backupDir}`));
|
|
83
|
-
console.log(chalk.blue(`📦 Arquivo: ${fileName} (${sizeMB} MB)`));
|
|
84
131
|
console.log(chalk.cyan(`\n💡 Próximo passo: Execute 'npx smoonb restore' para restaurar este backup`));
|
|
85
132
|
|
|
86
133
|
} catch (error) {
|
|
@@ -65,11 +65,11 @@ module.exports = async (_options) => {
|
|
|
65
65
|
// Leitura e mapeamento interativo
|
|
66
66
|
const currentEnv = await readEnvFile(envPath);
|
|
67
67
|
const expectedKeys = [
|
|
68
|
+
'SUPABASE_PROJECT_ID',
|
|
68
69
|
'NEXT_PUBLIC_SUPABASE_URL',
|
|
69
70
|
'NEXT_PUBLIC_SUPABASE_ANON_KEY',
|
|
70
71
|
'SUPABASE_SERVICE_ROLE_KEY',
|
|
71
72
|
'SUPABASE_DB_URL',
|
|
72
|
-
'SUPABASE_PROJECT_ID',
|
|
73
73
|
'SUPABASE_ACCESS_TOKEN',
|
|
74
74
|
'SMOONB_OUTPUT_DIR'
|
|
75
75
|
];
|
|
@@ -7,18 +7,92 @@ async function mapEnvVariablesInteractively(env, expectedKeys) {
|
|
|
7
7
|
const dePara = {};
|
|
8
8
|
|
|
9
9
|
const allKeys = Object.keys(env);
|
|
10
|
+
let projectId = null;
|
|
11
|
+
|
|
12
|
+
// Função auxiliar para obter Project ID já mapeado
|
|
13
|
+
function getProjectId() {
|
|
14
|
+
if (projectId) return projectId;
|
|
15
|
+
// Tentar encontrar Project ID já mapeado
|
|
16
|
+
const projectIdKey = Object.keys(dePara).find(k => dePara[k] === 'SUPABASE_PROJECT_ID');
|
|
17
|
+
if (projectIdKey && finalEnv[projectIdKey]) {
|
|
18
|
+
projectId = finalEnv[projectIdKey];
|
|
19
|
+
return projectId;
|
|
20
|
+
}
|
|
21
|
+
// Tentar encontrar no env original
|
|
22
|
+
const originalKey = Object.keys(env).find(k => k === 'SUPABASE_PROJECT_ID' || env[k]?.match(/^[a-z]{20}$/));
|
|
23
|
+
if (originalKey && env[originalKey]) {
|
|
24
|
+
projectId = env[originalKey];
|
|
25
|
+
return projectId;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Função para obter instruções e links específicos para cada variável
|
|
31
|
+
function getVariableInstructions(expected, currentProjectId) {
|
|
32
|
+
const instructions = {
|
|
33
|
+
'SUPABASE_PROJECT_ID': {
|
|
34
|
+
notFound: 'Não foi encontrada uma entrada para a variável SUPABASE_PROJECT_ID.',
|
|
35
|
+
help: 'Encontre em: Dashboard -> Project Settings -> General -> General Settings -> Project ID',
|
|
36
|
+
link: null
|
|
37
|
+
},
|
|
38
|
+
'NEXT_PUBLIC_SUPABASE_URL': {
|
|
39
|
+
notFound: 'Não foi encontrada uma entrada para a variável NEXT_PUBLIC_SUPABASE_URL.',
|
|
40
|
+
help: currentProjectId
|
|
41
|
+
? `Acesse: https://supabase.com/dashboard/project/${currentProjectId}/settings/api`
|
|
42
|
+
: 'Acesse: Dashboard -> Project Settings -> API -> Project URL',
|
|
43
|
+
link: currentProjectId ? `https://supabase.com/dashboard/project/${currentProjectId}/settings/api` : null
|
|
44
|
+
},
|
|
45
|
+
'NEXT_PUBLIC_SUPABASE_ANON_KEY': {
|
|
46
|
+
notFound: 'Não foi encontrada uma entrada para a variável NEXT_PUBLIC_SUPABASE_ANON_KEY.',
|
|
47
|
+
help: currentProjectId
|
|
48
|
+
? `Acesse: https://supabase.com/dashboard/project/${currentProjectId}/settings/api-keys`
|
|
49
|
+
: 'Acesse: Dashboard -> Project Settings -> API -> API Keys -> anon public',
|
|
50
|
+
link: currentProjectId ? `https://supabase.com/dashboard/project/${currentProjectId}/settings/api-keys` : null
|
|
51
|
+
},
|
|
52
|
+
'SUPABASE_SERVICE_ROLE_KEY': {
|
|
53
|
+
notFound: 'Não foi encontrada uma entrada para a variável SUPABASE_SERVICE_ROLE_KEY.',
|
|
54
|
+
help: currentProjectId
|
|
55
|
+
? `Acesse: https://supabase.com/dashboard/project/${currentProjectId}/settings/api-keys`
|
|
56
|
+
: 'Acesse: Dashboard -> Project Settings -> API -> API Keys -> service_role secret',
|
|
57
|
+
link: currentProjectId ? `https://supabase.com/dashboard/project/${currentProjectId}/settings/api-keys` : null
|
|
58
|
+
},
|
|
59
|
+
'SUPABASE_DB_URL': {
|
|
60
|
+
notFound: 'Não foi encontrada uma entrada para a variável SUPABASE_DB_URL.',
|
|
61
|
+
help: currentProjectId
|
|
62
|
+
? `Formato: postgresql://postgres:[DATABASE_PASSWORD]@db.${currentProjectId}.supabase.co:5432/postgres\nPara resetar senha: https://supabase.com/dashboard/project/${currentProjectId}/database/settings`
|
|
63
|
+
: 'Formato: postgresql://postgres:[DATABASE_PASSWORD]@db.[PROJECT_ID].supabase.co:5432/postgres\nPara resetar senha: Dashboard -> Project Settings -> Database -> Database Settings',
|
|
64
|
+
link: currentProjectId ? `https://supabase.com/dashboard/project/${currentProjectId}/database/settings` : null
|
|
65
|
+
},
|
|
66
|
+
'SUPABASE_ACCESS_TOKEN': {
|
|
67
|
+
notFound: 'Não foi encontrada uma entrada para a variável SUPABASE_ACCESS_TOKEN.',
|
|
68
|
+
help: 'Acesse: https://supabase.com/dashboard/account/tokens',
|
|
69
|
+
link: 'https://supabase.com/dashboard/account/tokens'
|
|
70
|
+
},
|
|
71
|
+
'SMOONB_OUTPUT_DIR': {
|
|
72
|
+
notFound: 'Não foi encontrada uma entrada para a variável SMOONB_OUTPUT_DIR.',
|
|
73
|
+
help: 'Diretório padrão para armazenar backups',
|
|
74
|
+
link: null,
|
|
75
|
+
default: './backups'
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return instructions[expected] || {
|
|
80
|
+
notFound: `Não foi encontrada uma entrada para a variável ${expected}.`,
|
|
81
|
+
help: '',
|
|
82
|
+
link: null
|
|
83
|
+
};
|
|
84
|
+
}
|
|
10
85
|
|
|
11
86
|
for (const expected of expectedKeys) {
|
|
12
87
|
console.log(chalk.blue(`\n🔧 Mapeando variável: ${expected}`));
|
|
13
88
|
|
|
14
89
|
let clientKey = undefined;
|
|
15
90
|
|
|
16
|
-
//
|
|
91
|
+
// Se existir chave exatamente igual, pular seleção e ir direto para confirmação
|
|
17
92
|
if (Object.prototype.hasOwnProperty.call(finalEnv, expected)) {
|
|
18
93
|
clientKey = expected;
|
|
19
94
|
} else {
|
|
20
|
-
//
|
|
21
|
-
// 4) Opção explícita para adicionar nova chave
|
|
95
|
+
// Opção explícita para adicionar nova chave
|
|
22
96
|
const choices = [
|
|
23
97
|
...allKeys.map((k, idx) => ({ name: `${idx + 1}. ${k}`, value: k })),
|
|
24
98
|
new inquirer.Separator(),
|
|
@@ -48,30 +122,71 @@ async function mapEnvVariablesInteractively(env, expectedKeys) {
|
|
|
48
122
|
}
|
|
49
123
|
|
|
50
124
|
const currentValue = finalEnv[clientKey] ?? '';
|
|
51
|
-
const
|
|
125
|
+
const currentProjectId = getProjectId();
|
|
126
|
+
const instructions = getVariableInstructions(expected, currentProjectId);
|
|
52
127
|
|
|
53
|
-
|
|
54
|
-
if (!
|
|
128
|
+
// Se não tem valor, mostrar mensagem específica
|
|
129
|
+
if (!currentValue) {
|
|
130
|
+
console.log(chalk.yellow(instructions.notFound));
|
|
131
|
+
if (instructions.help) {
|
|
132
|
+
console.log(chalk.white(instructions.help));
|
|
133
|
+
}
|
|
134
|
+
if (instructions.link) {
|
|
135
|
+
console.log(chalk.cyan(`🔗 ${instructions.link}`));
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
// Se tem valor, perguntar se está correto
|
|
139
|
+
const isCorrect = await confirm(`Valor atual: ${currentValue}\nEste é o valor correto do projeto alvo?`, true);
|
|
140
|
+
if (isCorrect) {
|
|
141
|
+
finalEnv[clientKey] = currentValue;
|
|
142
|
+
if (expected === 'SUPABASE_PROJECT_ID') {
|
|
143
|
+
projectId = currentValue;
|
|
144
|
+
}
|
|
145
|
+
if (dePara[clientKey] && dePara[clientKey] !== expected) {
|
|
146
|
+
throw new Error(`Duplicidade de mapeamento detectada para ${clientKey}`);
|
|
147
|
+
}
|
|
148
|
+
dePara[clientKey] = expected;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Solicitar novo valor
|
|
154
|
+
let valueToWrite = '';
|
|
155
|
+
if (expected === 'SMOONB_OUTPUT_DIR') {
|
|
156
|
+
// Para SMOONB_OUTPUT_DIR, pré-preencher com ./backups
|
|
55
157
|
const { newValue } = await inquirer.prompt([{
|
|
56
158
|
type: 'input',
|
|
57
159
|
name: 'newValue',
|
|
58
|
-
message: `
|
|
160
|
+
message: `Confirme o novo valor para ${expected}:`,
|
|
161
|
+
default: instructions.default || './backups',
|
|
162
|
+
prefix: ''
|
|
163
|
+
}]);
|
|
164
|
+
valueToWrite = newValue || instructions.default || './backups';
|
|
165
|
+
} else {
|
|
166
|
+
const { newValue } = await inquirer.prompt([{
|
|
167
|
+
type: 'input',
|
|
168
|
+
name: 'newValue',
|
|
169
|
+
message: `Cole o novo valor para ${expected}:`,
|
|
59
170
|
prefix: ''
|
|
60
171
|
}]);
|
|
61
172
|
valueToWrite = newValue || '';
|
|
62
173
|
}
|
|
63
174
|
|
|
64
|
-
|
|
175
|
+
// Validar valor obrigatório (exceto SMOONB_OUTPUT_DIR que tem default)
|
|
176
|
+
if (!valueToWrite && expected !== 'SMOONB_OUTPUT_DIR') {
|
|
65
177
|
const { newValueRequired } = await inquirer.prompt([{
|
|
66
178
|
type: 'input',
|
|
67
179
|
name: 'newValueRequired',
|
|
68
|
-
message: `Valor obrigatório. Informe valor para ${
|
|
180
|
+
message: `Valor obrigatório. Informe valor para ${expected}:`,
|
|
69
181
|
prefix: ''
|
|
70
182
|
}]);
|
|
71
183
|
valueToWrite = newValueRequired || '';
|
|
72
184
|
}
|
|
73
185
|
|
|
74
|
-
finalEnv[clientKey] = valueToWrite;
|
|
186
|
+
finalEnv[clientKey] = valueToWrite || (expected === 'SMOONB_OUTPUT_DIR' ? './backups' : '');
|
|
187
|
+
if (expected === 'SUPABASE_PROJECT_ID') {
|
|
188
|
+
projectId = valueToWrite;
|
|
189
|
+
}
|
|
75
190
|
if (dePara[clientKey] && dePara[clientKey] !== expected) {
|
|
76
191
|
throw new Error(`Duplicidade de mapeamento detectada para ${clientKey}`);
|
|
77
192
|
}
|