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.
Files changed (26) hide show
  1. package/package.json +15 -2
  2. package/src/commands/backup.js +159 -67
  3. package/src/commands/restore.js +165 -25
  4. package/src/interactive/envMapper.js +90 -0
  5. package/src/utils/env.js +106 -0
  6. package/src/utils/envMap.js +25 -0
  7. package/src/utils/supabase.js +2 -2
  8. package/.smoonbrc +0 -29
  9. package/.smoonbrc.example +0 -28
  10. package/backups/backup-2025-10-17T19-18-58-539Z/auth-config.json +0 -7
  11. package/backups/backup-2025-10-17T19-18-58-539Z/backup-manifest.json +0 -19
  12. package/backups/backup-2025-10-17T19-18-58-539Z/functions/README.md +0 -4
  13. package/backups/backup-2025-10-17T19-18-58-539Z/realtime-config.json +0 -7
  14. package/backups/backup-2025-10-17T19-18-58-539Z/storage/storage-config.json +0 -6
  15. package/backups/backup-2025-10-17T19-52-20-211Z/auth-config.json +0 -7
  16. package/backups/backup-2025-10-17T19-52-20-211Z/backup-manifest.json +0 -19
  17. package/backups/backup-2025-10-17T19-52-20-211Z/database-2025-10-17T19-52-20-215Z.dump +0 -0
  18. package/backups/backup-2025-10-17T19-52-20-211Z/functions/README.md +0 -4
  19. package/backups/backup-2025-10-17T19-52-20-211Z/realtime-config.json +0 -7
  20. package/backups/backup-2025-10-17T19-52-20-211Z/storage/storage-config.json +0 -6
  21. package/backups/backup-2025-10-17T20-38-13-188Z/auth-config.json +0 -7
  22. package/backups/backup-2025-10-17T20-38-13-188Z/backup-manifest.json +0 -19
  23. package/backups/backup-2025-10-17T20-38-13-188Z/database-2025-10-17T20-38-13-194Z.dump +0 -0
  24. package/backups/backup-2025-10-17T20-38-13-188Z/functions/README.md +0 -4
  25. package/backups/backup-2025-10-17T20-38-13-188Z/realtime-config.json +0 -7
  26. 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.44",
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
  }
@@ -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
- // Carregar e validar configuração
21
- const config = await readConfig();
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
- if (!config.supabase.databaseUrl) {
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 databaseUrl no .smoonbrc'));
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 (!config.supabase.accessToken) {
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 accessToken no .smoonbrc'));
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: ${config.supabase.projectId}`));
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
- return await performFullBackup(config, options);
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(config, options) {
82
- // Resolver diretório de saída
83
- const outputDir = options.output || config.backup.outputDir;
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: config.supabase.projectId,
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(config.supabase.projectId, backupDir);
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(config.supabase.projectId, backupDir);
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
- console.log(chalk.blue('\n⚡ 2/8 - Backup das Edge Functions via Docker...'));
127
- const functionsResult = await backupEdgeFunctionsWithDocker(config.supabase.projectId, config.supabase.accessToken, backupDir);
128
- manifest.components.edge_functions = functionsResult;
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
- console.log(chalk.blue('\n🔐 3/8 - Backup das Auth Settings via API...'));
132
- const authResult = await backupAuthSettings(config.supabase.projectId, config.supabase.accessToken, backupDir);
133
- manifest.components.auth_settings = authResult;
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
- console.log(chalk.blue('\n📦 4/8 - Backup do Storage via API...'));
137
- const storageResult = await backupStorage(config.supabase.projectId, config.supabase.accessToken, backupDir);
138
- manifest.components.storage = storageResult;
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(config.supabase.databaseUrl, backupDir);
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(config.supabase.projectId, backupDir);
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
- console.log(chalk.blue('\n🔄 7/8 - Backup das Realtime Settings via Captura Interativa...'));
152
- const realtimeResult = await backupRealtimeSettings(config.supabase.projectId, backupDir, options.skipRealtime);
153
- manifest.components.realtime = realtimeResult;
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
- console.log(chalk.green(`⚡ Edge Functions: ${functionsResult.success_count || 0}/${functionsResult.functions_count || 0} functions baixadas via Docker`));
164
- console.log(chalk.green(`🔐 Auth Settings: ${authResult.success ? 'Exportadas via API' : 'Falharam'}`));
165
- console.log(chalk.green(`📦 Storage: ${storageResult.buckets?.length || 0} buckets verificados via API`));
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
- let realtimeMessage = 'Falharam';
169
- if (realtimeResult.success) {
170
- if (options.skipRealtime) {
171
- realtimeMessage = 'Configurações copiadas do backup anterior';
172
- } else {
173
- realtimeMessage = 'Configurações capturadas interativamente';
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
- console.log(chalk.green(`🔄 Realtime: ${realtimeMessage}`));
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(projectId, backupDir) {
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 dbUrl = config.supabase.databaseUrl;
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(projectId, backupDir) {
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 config = await readConfig();
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 dbUrl = config.supabase.databaseUrl;
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');
@@ -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
- const config = await readConfig();
13
- const targetProject = getTargetProject(config);
14
-
15
- console.log(chalk.blue(`📁 Buscando backups em: ${config.backup.outputDir || './backups'}`));
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(config.backup.outputDir || './backups');
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 (interativo)?',
211
- default: true
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 (interativo)?',
244
- default: true
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.existsSync(edgeFunctionsDir)) {
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 functions = fs.readdirSync(edgeFunctionsDir).filter(item =>
413
- fs.statSync(path.join(edgeFunctionsDir, item)).isDirectory()
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
- // Link com projeto target
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(` ⚠️ Link pode já existir, continuando...`));
542
+ console.log(chalk.yellow(' ⚠️ Link pode já existir, continuando...'));
433
543
  }
434
544
 
435
- // Deploy de cada função
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: functionPath,
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
+
@@ -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
+
@@ -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,7 +0,0 @@
1
- {
2
- "timestamp": "2025-10-17T19:18:58.553Z",
3
- "projectId": "itrnlqsdfsdfsdf",
4
- "providers": [],
5
- "policies": [],
6
- "settings": {}
7
- }
@@ -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,4 +0,0 @@
1
- # Edge Functions Backup
2
-
3
- Nenhuma Edge Function local foi encontrada.
4
- Use o Supabase CLI para fazer backup das functions remotas.
@@ -1,7 +0,0 @@
1
- {
2
- "timestamp": "2025-10-17T19:18:58.558Z",
3
- "projectId": "itrnlqsdfsdfsdf",
4
- "enabled": false,
5
- "channels": [],
6
- "settings": {}
7
- }
@@ -1,6 +0,0 @@
1
- {
2
- "timestamp": "2025-10-17T19:18:58.556Z",
3
- "projectId": "itrnlqsdfsdfsdf",
4
- "buckets": [],
5
- "objects": []
6
- }
@@ -1,7 +0,0 @@
1
- {
2
- "timestamp": "2025-10-17T19:52:20.446Z",
3
- "projectId": "xvfgdgdfgdfgdfgdfg",
4
- "providers": [],
5
- "policies": [],
6
- "settings": {}
7
- }
@@ -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
- }
@@ -1,4 +0,0 @@
1
- # Edge Functions Backup
2
-
3
- Nenhuma Edge Function local foi encontrada.
4
- Use o Supabase CLI para fazer backup das functions remotas.
@@ -1,7 +0,0 @@
1
- {
2
- "timestamp": "2025-10-17T19:52:20.450Z",
3
- "projectId": "xvfgdgdfgdfgdfgdfg",
4
- "enabled": false,
5
- "channels": [],
6
- "settings": {}
7
- }
@@ -1,6 +0,0 @@
1
- {
2
- "timestamp": "2025-10-17T19:52:20.449Z",
3
- "projectId": "xvfgdgdfgdfgdfgdfg",
4
- "buckets": [],
5
- "objects": []
6
- }
@@ -1,7 +0,0 @@
1
- {
2
- "timestamp": "2025-10-17T20:38:13.574Z",
3
- "projectId": "xvfgdgdfgdfgdfgdfg",
4
- "providers": [],
5
- "policies": [],
6
- "settings": {}
7
- }
@@ -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
- }
@@ -1,4 +0,0 @@
1
- # Edge Functions Backup
2
-
3
- Nenhuma Edge Function local foi encontrada.
4
- Use o Supabase CLI para fazer backup das functions remotas.
@@ -1,7 +0,0 @@
1
- {
2
- "timestamp": "2025-10-17T20:38:13.582Z",
3
- "projectId": "xvfgdgdfgdfgdfgdfg",
4
- "enabled": false,
5
- "channels": [],
6
- "settings": {}
7
- }
@@ -1,6 +0,0 @@
1
- {
2
- "timestamp": "2025-10-17T20:38:13.578Z",
3
- "projectId": "xvfgdgdfgdfgdfgdfg",
4
- "buckets": [],
5
- "objects": []
6
- }