smoonb 0.0.61 → 0.0.63

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 CHANGED
@@ -210,38 +210,57 @@ backups/backup-2025-10-31-09-37-54/
210
210
 
211
211
  ### Restauração Interativa
212
212
 
213
+ **Restaurar backup existente:**
213
214
  ```bash
214
215
  npx smoonb restore
215
216
  ```
216
217
 
218
+ **Importar e restaurar diretamente do Dashboard:**
219
+ ```bash
220
+ # Apenas database
221
+ npx smoonb restore --file "C:\Downloads\db_cluster-04-03-2024@14-16-59.backup.gz"
222
+
223
+ # Database e storage juntos
224
+ npx smoonb restore --file "backup.backup.gz" --storage "meu-projeto.storage.zip"
225
+ ```
226
+
217
227
  **Fluxo interativo do restore:**
218
228
 
219
229
  1. **Validação Docker** - Verifica se o Docker está rodando
220
- 2. **Consentimento** - Pede permissão para ler/escrever `.env.local`
221
- 3. **Mapeamento de Variáveis** - Mapeia variáveis para o projeto de destino
222
- 4. **Backup do .env.local** - Cria backup automático
223
- 5. **Seleção de Backup** - Lista e permite escolher qual backup restaurar
224
- 6. **Seleção de Componentes** - Pergunta quais componentes restaurar:
230
+ 2. **Termo de Uso** - Exibe e solicita aceitação dos termos
231
+ 3. **Consentimento** - Pede permissão para ler/escrever `.env.local`
232
+ 4. **Mapeamento de Variáveis** - Mapeia variáveis para o projeto de destino
233
+ 5. **Backup do .env.local** - Cria backup automático
234
+ 6. **Seleção de Backup** - Lista e permite escolher qual backup restaurar (pula se `--file` fornecido)
235
+ - Se `--file` for fornecido: importa automaticamente e auto-seleciona o backup
236
+ - Se `--storage` for fornecido junto com `--file`: importa também o arquivo de storage
237
+ 7. **Seleção de Componentes** - Pergunta quais componentes restaurar:
225
238
  - 📊 Database (sempre disponível)
226
239
  - ⚡ Edge Functions (se disponível no backup)
227
240
  - 🔐 Auth Settings (se disponível no backup)
228
241
  - 📦 Storage (se disponível no backup)
229
242
  - 🔧 Database Extensions and Settings (se disponível no backup)
230
243
  - 🔄 Realtime Settings (se disponível no backup)
231
- 7. **Resumo Detalhado** - Mostra backup selecionado, projeto destino e componentes
232
- 8. **Confirmação Final** - Confirma antes de iniciar
233
- 9. **Execução da Restauração:**
234
- - 📊 Database - Restaura via `psql` (suporta `.backup.gz` e `.backup`)
235
- - ⚡ Edge Functions - Copia e faz deploy no projeto destino
236
- - 🔐 Auth Settings - Exibe configurações para aplicação manual
237
- - 📦 Storage - Exibe informações para migração manual
238
- - 🔧 Database Settings - Restaura extensões e configurações via SQL
239
- - 🔄 Realtime Settings - Exibe configurações para aplicação manual
244
+ 8. **Resumo Detalhado** - Mostra backup selecionado, projeto destino e componentes
245
+ 9. **Confirmação Final** - Confirma antes de iniciar
246
+ 10. **Execução da Restauração:**
247
+ - 📊 Database - Restaura via `psql` (suporta `.backup.gz` e `.backup`)
248
+ - ⚡ Edge Functions - Copia e faz deploy no projeto destino
249
+ - 🔐 Auth Settings - Exibe configurações para aplicação manual
250
+ - 📦 Storage - Exibe informações para migração manual
251
+ - 🔧 Database Settings - Restaura extensões e configurações via SQL
252
+ - 🔄 Realtime Settings - Exibe configurações para aplicação manual
240
253
 
241
254
  **Formato de arquivos suportados:**
242
255
  - ✅ `.backup.gz` (compactado) - Descompacta automaticamente antes de restaurar
243
256
  - ✅ `.backup` (descompactado) - Restaura diretamente
244
257
 
258
+ **Quando usar `--file`:**
259
+ - Importa automaticamente o arquivo de backup antes de restaurar
260
+ - Elimina a etapa de seleção de backup
261
+ - Se `--storage` fornecido, importa também o arquivo de storage
262
+ - Útil para restaurar backups baixados diretamente do Dashboard do Supabase
263
+
245
264
  ### Importar Backup do Dashboard do Supabase
246
265
 
247
266
  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.
@@ -265,7 +284,7 @@ npx smoonb import --file "backup.backup.gz" --storage "meu-projeto.storage.zip"
265
284
  6. Se fornecido, copia o arquivo de storage para a mesma pasta
266
285
  7. Deixa o backup pronto para ser encontrado pelo comando `restore`
267
286
 
268
- **Exemplo completo - Apenas database:**
287
+ **Exemplo completo - Apenas database (usando import + restore):**
269
288
  ```bash
270
289
  # 1. Baixar backup do Dashboard do Supabase
271
290
  # Arquivo: db_cluster-04-03-2024@14-16-59.backup.gz
@@ -278,7 +297,16 @@ npx smoonb restore
278
297
  # O backup importado aparecerá na lista de backups disponíveis
279
298
  ```
280
299
 
281
- **Exemplo completo - Database e Storage:**
300
+ **Exemplo completo - Apenas database (usando restore diretamente):**
301
+ ```bash
302
+ # 1. Baixar backup do Dashboard do Supabase
303
+ # Arquivo: db_cluster-04-03-2024@14-16-59.backup.gz
304
+
305
+ # 2. Importar e restaurar diretamente (pula seleção de backup)
306
+ npx smoonb restore --file "C:\Downloads\db_cluster-04-03-2024@14-16-59.backup.gz"
307
+ ```
308
+
309
+ **Exemplo completo - Database e Storage (usando import + restore):**
282
310
  ```bash
283
311
  # 1. Baixar backup e storage do Dashboard do Supabase
284
312
  # Arquivos:
@@ -293,6 +321,17 @@ npx smoonb restore
293
321
  # O backup importado aparecerá na lista de backups disponíveis
294
322
  ```
295
323
 
324
+ **Exemplo completo - Database e Storage (usando restore diretamente):**
325
+ ```bash
326
+ # 1. Baixar backup e storage do Dashboard do Supabase
327
+ # Arquivos:
328
+ # - db_cluster-04-03-2024@14-16-59.backup.gz
329
+ # - meu-projeto.storage.zip
330
+
331
+ # 2. Importar e restaurar diretamente (pula seleção de backup)
332
+ npx smoonb restore --file "C:\Downloads\db_cluster-04-03-2024@14-16-59.backup.gz" --storage "C:\Downloads\meu-projeto.storage.zip"
333
+ ```
334
+
296
335
  **Importante:**
297
336
  - O arquivo de backup é **obrigatório** e deve estar no formato do Dashboard: `db_cluster-DD-MM-YYYY@HH-MM-SS.backup.gz`
298
337
  - O arquivo de storage é **opcional** e deve estar no formato: `*.storage.zip`
@@ -301,6 +340,10 @@ npx smoonb restore
301
340
  - O caminho pode ser absoluto ou relativo
302
341
  - O comando criará a estrutura de pastas necessária automaticamente
303
342
 
343
+ **Diferença entre `import` e `restore --file`:**
344
+ - `import`: Apenas importa o arquivo e cria a estrutura de backup. Você precisa executar `restore` depois.
345
+ - `restore --file`: Importa o arquivo automaticamente e já inicia o processo de restauração, pulando a etapa de seleção de backup.
346
+
304
347
  ### Verificação Pós-Restore
305
348
 
306
349
  ```bash
@@ -322,6 +365,7 @@ npx smoonb check
322
365
  |---------|-----------|
323
366
  | `npx smoonb backup` | Backup completo interativo usando Docker |
324
367
  | `npx smoonb restore` | Restauração interativa usando psql (Docker) |
368
+ | `npx smoonb restore --file <path> [--storage <path>]` | Importar e restaurar diretamente arquivo .backup.gz e opcionalmente .storage.zip do Dashboard |
325
369
  | `npx smoonb import --file <path> [--storage <path>]` | Importar arquivo .backup.gz e opcionalmente .storage.zip do Dashboard do Supabase |
326
370
  | `npx smoonb check` | Verificação de integridade pós-restore |
327
371
 
package/bin/smoonb.js CHANGED
@@ -53,6 +53,12 @@ ${chalk.yellow.bold('1. BACKUP - Fazer backup completo do projeto:')}
53
53
  ${chalk.yellow.bold('2. RESTORE - Restaurar backup em um projeto:')}
54
54
  ${chalk.white('npx smoonb restore')}
55
55
  ${chalk.gray('# Processo interativo que lista backups disponíveis e permite escolher')}
56
+
57
+ ${chalk.white('npx smoonb restore --file "C:\\Downloads\\db_cluster-04-03-2024@14-16-59.backup.gz"')}
58
+ ${chalk.gray('# Importa e restaura diretamente o arquivo de backup (pula seleção)')}
59
+
60
+ ${chalk.white('npx smoonb restore --file "backup.backup.gz" --storage "meu-projeto.storage.zip"')}
61
+ ${chalk.gray('# Importa e restaura backup e storage juntos')}
56
62
 
57
63
  ${chalk.yellow.bold('3. IMPORT - Importar backup do Dashboard do Supabase:')}
58
64
  ${chalk.white('npx smoonb import --file "C:\\Downloads\\db_cluster-04-03-2024@14-16-59.backup.gz"')}
@@ -103,25 +109,41 @@ ${chalk.yellow.bold('O que é capturado:')}
103
109
  program
104
110
  .command('restore')
105
111
  .description('Restaurar backup completo usando psql (modo interativo)')
112
+ .option('--file <path>', 'Caminho completo do arquivo .backup.gz a importar e restaurar (opcional)')
113
+ .option('--storage <path>', 'Caminho completo do arquivo .storage.zip a importar junto com o backup (opcional)')
106
114
  .addHelpText('after', `
107
115
  ${chalk.yellow.bold('Exemplos:')}
108
116
  ${chalk.white('npx smoonb restore')}
109
117
  ${chalk.gray('# Processo interativo que lista backups disponíveis')}
118
+
119
+ ${chalk.white('npx smoonb restore --file "C:\\Downloads\\db_cluster-04-03-2024@14-16-59.backup.gz"')}
120
+ ${chalk.gray('# Importa e restaura diretamente o arquivo de backup')}
121
+
122
+ ${chalk.white('npx smoonb restore --file "backup.backup.gz" --storage "meu-projeto.storage.zip"')}
123
+ ${chalk.gray('# Importa e restaura backup e storage juntos')}
110
124
 
111
125
  ${chalk.yellow.bold('Fluxo do restore:')}
112
126
  1. Validação Docker
113
127
  2. Consentimento para ler/escrever .env.local
114
128
  3. Mapeamento de variáveis de ambiente
115
- 4. Seleção de backup disponível
129
+ 4. Seleção de backup disponível (pula se --file fornecido)
116
130
  5. Seleção de componentes para restaurar
117
131
  6. Resumo detalhado e confirmação
118
132
  7. Execução da restauração
119
133
 
134
+ ${chalk.yellow.bold('Quando usar --file:')}
135
+ • Importa automaticamente o arquivo de backup antes de restaurar
136
+ • Elimina a etapa de seleção de backup
137
+ • Se --storage fornecido, importa também o arquivo de storage
138
+ • Útil para restaurar backups baixados diretamente do Dashboard
139
+
120
140
  ${chalk.yellow.bold('Formatos suportados:')}
121
141
  • .backup.gz (compactado) - Descompacta automaticamente
122
142
  • .backup (descompactado) - Restaura diretamente
123
143
  `)
124
- .action(commands.restore);
144
+ .action(async (options) => {
145
+ await commands.restore({ file: options.file, storage: options.storage });
146
+ });
125
147
 
126
148
  program
127
149
  .command('check')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoonb",
3
- "version": "0.0.61",
3
+ "version": "0.0.63",
4
4
  "description": "Complete Supabase backup and migration tool - EXPERIMENTAL VERSION - USE AT YOUR OWN RISK",
5
5
  "preferGlobal": false,
6
6
  "preventGlobalInstall": true,
@@ -77,11 +77,11 @@ module.exports = async (options) => {
77
77
  console.log(chalk.blue(`📁 Backup do .env.local: ${path.relative(process.cwd(), envBackupPath)}`));
78
78
 
79
79
  const expectedKeys = [
80
+ 'SUPABASE_PROJECT_ID',
80
81
  'NEXT_PUBLIC_SUPABASE_URL',
81
82
  'NEXT_PUBLIC_SUPABASE_ANON_KEY',
82
83
  'SUPABASE_SERVICE_ROLE_KEY',
83
84
  'SUPABASE_DB_URL',
84
- 'SUPABASE_PROJECT_ID',
85
85
  'SUPABASE_ACCESS_TOKEN',
86
86
  'SMOONB_OUTPUT_DIR'
87
87
  ];
@@ -4,6 +4,7 @@ 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
10
  * Comando para importar arquivo .backup.gz e opcionalmente .storage.zip do Dashboard do Supabase
@@ -12,6 +13,18 @@ module.exports = async (options) => {
12
13
  showBetaBanner();
13
14
 
14
15
  try {
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
+
15
28
  // Validar que o arquivo de backup foi fornecido
16
29
  if (!options.file) {
17
30
  console.error(chalk.red('❌ Arquivo de backup não fornecido'));
@@ -1,12 +1,14 @@
1
1
  const chalk = require('chalk');
2
2
  const path = require('path');
3
3
  const fs = require('fs');
4
+ const fsPromises = require('fs').promises;
4
5
  const { readEnvFile, writeEnvFile, backupEnvFile } = require('../../utils/env');
5
6
  const { saveEnvMap } = require('../../utils/envMap');
6
7
  const { mapEnvVariablesInteractively } = require('../../interactive/envMapper');
7
8
  const { showBetaBanner } = require('../../utils/banner');
8
9
  const { listValidBackups, showRestoreSummary } = require('./utils');
9
10
  const { confirm } = require('../../utils/prompt');
11
+ const { ensureDir } = require('../../utils/fsx');
10
12
  const step00DockerValidation = require('../backup/steps/00-docker-validation');
11
13
 
12
14
  // Importar todas as etapas
@@ -19,7 +21,81 @@ const step06Storage = require('./steps/06-storage');
19
21
  const step07DatabaseSettings = require('./steps/07-database-settings');
20
22
  const step08RealtimeSettings = require('./steps/08-realtime-settings');
21
23
 
22
- module.exports = async (_options) => {
24
+ /**
25
+ * Função auxiliar para importar arquivo de backup e storage (reutiliza lógica do comando import)
26
+ */
27
+ async function importBackupFile(sourceFile, sourceStorageFile, outputDir) {
28
+ // Validar arquivo de backup
29
+ try {
30
+ await fsPromises.access(sourceFile);
31
+ } catch {
32
+ throw new Error(`Arquivo de backup não encontrado: ${sourceFile}`);
33
+ }
34
+
35
+ // Verificar se é um arquivo .backup.gz ou .backup
36
+ if (!sourceFile.endsWith('.backup.gz') && !sourceFile.endsWith('.backup')) {
37
+ throw new Error('Arquivo de backup deve ser .backup.gz ou .backup');
38
+ }
39
+
40
+ // Validar arquivo de storage se fornecido
41
+ if (sourceStorageFile) {
42
+ try {
43
+ await fsPromises.access(sourceStorageFile);
44
+ } catch {
45
+ throw new Error(`Arquivo de storage não encontrado: ${sourceStorageFile}`);
46
+ }
47
+
48
+ // Verificar se é um arquivo .storage.zip
49
+ if (!sourceStorageFile.endsWith('.storage.zip')) {
50
+ throw new Error('Arquivo de storage deve ser .storage.zip');
51
+ }
52
+ }
53
+
54
+ // Extrair informações do nome do arquivo de backup
55
+ // Formato esperado: db_cluster-DD-MM-YYYY@HH-MM-SS.backup.gz
56
+ const fileName = path.basename(sourceFile);
57
+ const match = fileName.match(/db_cluster-(\d{2})-(\d{2})-(\d{4})@(\d{2})-(\d{2})-(\d{2})\.backup(\.gz)?/);
58
+
59
+ if (!match) {
60
+ throw new Error('Nome do arquivo de backup não está no formato esperado do Dashboard. Formato esperado: db_cluster-DD-MM-YYYY@HH-MM-SS.backup.gz');
61
+ }
62
+
63
+ const [, day, month, year, hour, minute, second] = match;
64
+
65
+ // Criar nome da pasta no formato backup-YYYY-MM-DD-HH-MM-SS
66
+ const backupDirName = `backup-${year}-${month}-${day}-${hour}-${minute}-${second}`;
67
+ const backupDir = path.join(outputDir, backupDirName);
68
+
69
+ // Criar diretório de backup
70
+ await ensureDir(backupDir);
71
+ console.log(chalk.blue(`📁 Importando backup para: ${backupDirName}`));
72
+
73
+ // Copiar arquivo de backup para o diretório de backup
74
+ const destFile = path.join(backupDir, fileName);
75
+ await fsPromises.copyFile(sourceFile, destFile);
76
+
77
+ // Obter tamanho do arquivo de backup
78
+ const stats = await fsPromises.stat(destFile);
79
+ const sizeMB = (stats.size / (1024 * 1024)).toFixed(2);
80
+
81
+ console.log(chalk.green(`✅ Arquivo de backup importado: ${fileName} (${sizeMB} MB)`));
82
+
83
+ // Copiar arquivo de storage se fornecido
84
+ if (sourceStorageFile) {
85
+ const storageFileName = path.basename(sourceStorageFile);
86
+ const destStorageFile = path.join(backupDir, storageFileName);
87
+ await fsPromises.copyFile(sourceStorageFile, destStorageFile);
88
+
89
+ const storageStats = await fsPromises.stat(destStorageFile);
90
+ const storageSizeMB = (storageStats.size / (1024 * 1024)).toFixed(2);
91
+
92
+ console.log(chalk.green(`✅ Arquivo de storage importado: ${storageFileName} (${storageSizeMB} MB)`));
93
+ }
94
+
95
+ return backupDir;
96
+ }
97
+
98
+ module.exports = async (options) => {
23
99
  showBetaBanner();
24
100
 
25
101
  try {
@@ -65,11 +141,11 @@ module.exports = async (_options) => {
65
141
  // Leitura e mapeamento interativo
66
142
  const currentEnv = await readEnvFile(envPath);
67
143
  const expectedKeys = [
144
+ 'SUPABASE_PROJECT_ID',
68
145
  'NEXT_PUBLIC_SUPABASE_URL',
69
146
  'NEXT_PUBLIC_SUPABASE_ANON_KEY',
70
147
  'SUPABASE_SERVICE_ROLE_KEY',
71
148
  'SUPABASE_DB_URL',
72
- 'SUPABASE_PROJECT_ID',
73
149
  'SUPABASE_ACCESS_TOKEN',
74
150
  'SMOONB_OUTPUT_DIR'
75
151
  ];
@@ -94,20 +170,43 @@ module.exports = async (_options) => {
94
170
  targetAccessToken: getValue('SUPABASE_ACCESS_TOKEN')
95
171
  };
96
172
 
97
- console.log(chalk.blue(`📁 Buscando backups em: ${getValue('SMOONB_OUTPUT_DIR') || './backups'}`));
173
+ const outputDir = getValue('SMOONB_OUTPUT_DIR') || './backups';
174
+ let selectedBackup = null;
98
175
 
99
- // 1. Listar backups válidos (.backup.gz)
100
- const validBackups = await listValidBackups(getValue('SMOONB_OUTPUT_DIR') || './backups');
101
-
102
- if (validBackups.length === 0) {
103
- console.error(chalk.red('❌ Nenhum backup válido encontrado'));
104
- console.log(chalk.yellow('💡 Execute primeiro: npx smoonb backup'));
105
- process.exit(1);
176
+ // Se --file foi fornecido, importar o arquivo e auto-selecionar
177
+ if (options.file) {
178
+ const sourceFile = path.resolve(options.file);
179
+ const sourceStorageFile = options.storage ? path.resolve(options.storage) : null;
180
+
181
+ console.log(chalk.blue(`📁 Importando arquivo de backup...`));
182
+ const importedBackupDir = await importBackupFile(sourceFile, sourceStorageFile, outputDir);
183
+
184
+ // Listar backups válidos para encontrar o backup importado
185
+ const validBackups = await listValidBackups(outputDir);
186
+ selectedBackup = validBackups.find(b => b.path === importedBackupDir);
187
+
188
+ if (!selectedBackup) {
189
+ throw new Error('Não foi possível encontrar o backup importado');
190
+ }
191
+
192
+ console.log(chalk.green(`✅ Backup importado e selecionado automaticamente: ${path.basename(selectedBackup.path)}`));
193
+ } else {
194
+ // Fluxo normal: listar e selecionar backup interativamente
195
+ console.log(chalk.blue(`📁 Buscando backups em: ${outputDir}`));
196
+
197
+ // 1. Listar backups válidos (.backup.gz)
198
+ const validBackups = await listValidBackups(outputDir);
199
+
200
+ if (validBackups.length === 0) {
201
+ console.error(chalk.red('❌ Nenhum backup válido encontrado'));
202
+ console.log(chalk.yellow('💡 Execute primeiro: npx smoonb backup'));
203
+ process.exit(1);
204
+ }
205
+
206
+ // 2. Selecionar backup interativamente
207
+ selectedBackup = await step00BackupSelection(validBackups);
106
208
  }
107
209
 
108
- // 2. Selecionar backup interativamente
109
- const selectedBackup = await step00BackupSelection(validBackups);
110
-
111
210
  // 3. Perguntar quais componentes restaurar
112
211
  const components = await step01ComponentsSelection(selectedBackup.path);
113
212
 
@@ -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
- // 3) Se existir chave exatamente igual, pular seleção e ir direto para confirmação
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
- // 2) Remover o caractere '?' do início da pergunta definindo prefix: ''
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 isCorrect = await confirm(`Valor atual: ${currentValue || '(vazio)'}\nEste é o valor correto do projeto alvo?`, true);
125
+ const currentProjectId = getProjectId();
126
+ const instructions = getVariableInstructions(expected, currentProjectId);
52
127
 
53
- let valueToWrite = currentValue;
54
- if (!isCorrect) {
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: `Cole o novo valor para ${clientKey}:`,
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
- if (!valueToWrite) {
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 ${clientKey}:`,
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
  }