smoonb 0.0.21 → 0.0.24

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
@@ -25,25 +25,42 @@ O **smoonb** resolve o problema das ferramentas existentes que fazem backup apen
25
25
 
26
26
  ## 🚀 Instalação
27
27
 
28
+ **⚠️ IMPORTANTE: Instale APENAS localmente no projeto!**
29
+
28
30
  ```bash
29
- # Instalar localmente no projeto
31
+ # ✅ CORRETO - Instalar localmente no projeto
30
32
  npm install smoonb
31
33
 
32
- # Usar com npx
34
+ # ✅ CORRETO - Usar com npx
33
35
  npx smoonb --help
36
+
37
+ # ❌ ERRADO - NÃO instalar globalmente
38
+ npm install -g smoonb # ← Isso será bloqueado!
34
39
  ```
35
40
 
41
+ **💡 Por que apenas local?**
42
+ - **🔒 Segurança**: Evita conflitos de versão
43
+ - **📦 Isolamento**: Cada projeto usa sua versão
44
+ - **🔄 Atualizações**: Controle granular por projeto
45
+ - **🛡️ Estabilidade**: Evita quebras em outros projetos
46
+
36
47
  ## 📋 Pré-requisitos
37
48
 
38
- ### 1. Supabase CLI
49
+ ### 1. Docker Desktop
39
50
  ```bash
40
- npm install -g supabase
51
+ # Instalar Docker Desktop
52
+ # Windows/macOS: https://docs.docker.com/desktop/install/
53
+ # Linux: https://docs.docker.com/engine/install/
54
+
55
+ # Verificar se está rodando
56
+ docker --version
57
+ docker ps
41
58
  ```
42
59
 
43
- ### 2. PostgreSQL (psql)
44
- - **Windows**: https://www.postgresql.org/download/windows/
45
- - **macOS**: `brew install postgresql`
46
- - **Linux**: `sudo apt-get install postgresql-client`
60
+ ### 2. Supabase CLI
61
+ ```bash
62
+ npm install -g supabase
63
+ ```
47
64
 
48
65
  ## ⚙️ Configuração
49
66
 
package/bin/smoonb.js CHANGED
@@ -70,6 +70,7 @@ program
70
70
  .command('backup')
71
71
  .description('Fazer backup completo do projeto Supabase usando Supabase CLI')
72
72
  .option('-o, --output <dir>', 'Diretório de saída do backup')
73
+ .option('--skip-realtime', 'Pular captura interativa de Realtime Settings')
73
74
  .action(commands.backup);
74
75
 
75
76
  program
package/package.json CHANGED
@@ -1,14 +1,18 @@
1
1
  {
2
2
  "name": "smoonb",
3
- "version": "0.0.21",
3
+ "version": "0.0.24",
4
4
  "description": "Complete Supabase backup and migration tool - EXPERIMENTAL VERSION - USE AT YOUR OWN RISK",
5
+ "preferGlobal": false,
6
+ "preventGlobalInstall": true,
5
7
  "main": "index.js",
6
8
  "bin": {
7
9
  "smoonb": "bin/smoonb.js"
8
10
  },
9
11
  "scripts": {
10
12
  "test": "echo \"Error: no test specified\" && exit 1",
11
- "start": "node bin/smoonb.js"
13
+ "start": "node bin/smoonb.js",
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'"
12
16
  },
13
17
  "keywords": [
14
18
  "supabase",
@@ -8,6 +8,7 @@ const { sha256 } = require('../utils/hash');
8
8
  const { readConfig, validateFor } = require('../utils/config');
9
9
  const { showBetaBanner } = require('../utils/banner');
10
10
  const { canPerformCompleteBackup, getDockerVersion } = require('../utils/docker');
11
+ const { captureRealtimeSettings } = require('../utils/realtime-settings');
11
12
 
12
13
  const execAsync = promisify(exec);
13
14
 
@@ -135,9 +136,9 @@ async function performFullBackup(config, options) {
135
136
  const rolesResult = await backupCustomRoles(config.supabase.databaseUrl, backupDir);
136
137
  manifest.components.custom_roles = rolesResult;
137
138
 
138
- // 6. Backup Realtime Settings via SQL
139
- console.log(chalk.blue('\n🔄 6/6 - Backup das Realtime Settings via SQL...'));
140
- const realtimeResult = await backupRealtimeSettings(config.supabase.databaseUrl, backupDir);
139
+ // 6. Backup Realtime Settings via Captura Interativa
140
+ console.log(chalk.blue('\n🔄 6/6 - Backup das Realtime Settings via Captura Interativa...'));
141
+ const realtimeResult = await backupRealtimeSettings(config.supabase.projectId, backupDir, options.skipRealtime);
141
142
  manifest.components.realtime = realtimeResult;
142
143
 
143
144
  // Salvar manifest
@@ -230,7 +231,7 @@ async function backupDatabaseWithDocker(databaseUrl, backupDir) {
230
231
  const schemaFile = path.join(backupDir, 'schema.sql');
231
232
 
232
233
  try {
233
- await execAsync(`supabase db dump --db-url "${databaseUrl}" --schema-only -f "${schemaFile}"`);
234
+ await execAsync(`supabase db dump --db-url "${databaseUrl}" -f "${schemaFile}"`);
234
235
 
235
236
  const schemaValidation = await validateSqlFile(schemaFile);
236
237
  if (schemaValidation.valid) {
@@ -524,102 +525,48 @@ async function backupStorage(projectId, accessToken, backupDir) {
524
525
  }
525
526
  }
526
527
 
527
- // Backup dos Custom Roles via SQL
528
+ // Backup dos Custom Roles via Docker
528
529
  async function backupCustomRoles(databaseUrl, backupDir) {
529
530
  try {
530
- console.log(chalk.gray(' - Exportando Custom Roles...'));
531
+ console.log(chalk.gray(' - Exportando Custom Roles via Docker...'));
531
532
 
532
533
  const customRolesFile = path.join(backupDir, 'custom-roles.sql');
533
534
 
534
- // Query para obter roles customizados com senhas
535
- const customRolesQuery = `
536
- -- Custom Roles Backup
537
- -- Roles customizados com senhas
538
-
539
- SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolconnlimit, rolpassword
540
- FROM pg_roles
541
- WHERE rolname NOT IN ('postgres', 'supabase_admin', 'supabase_auth_admin', 'supabase_storage_admin', 'supabase_read_only_user', 'authenticator', 'anon', 'authenticated', 'service_role')
542
- ORDER BY rolname;
543
- `;
544
-
545
- // Executar query e salvar resultado
546
- const { stdout } = await execAsync(
547
- `psql "${databaseUrl}" -t -c "${customRolesQuery}"`
548
- );
549
-
550
- const rolesContent = `-- Custom Roles Backup
551
- -- Generated at: ${new Date().toISOString()}
552
-
553
- ${customRolesQuery}
554
-
555
- -- Results:
556
- ${stdout}
557
- `;
558
-
559
- await fs.promises.writeFile(customRolesFile, rolesContent);
560
-
561
- const stats = fs.statSync(customRolesFile);
562
- const sizeKB = (stats.size / 1024).toFixed(1);
563
-
564
- console.log(chalk.green(` ✅ Custom Roles exportados: ${sizeKB} KB`));
565
-
566
- return { success: true, roles: [{ filename: 'custom-roles.sql', sizeKB }] };
535
+ try {
536
+ // Usar Supabase CLI via Docker para roles
537
+ await execAsync(`supabase db dump --db-url "${databaseUrl}" --role-only -f "${customRolesFile}"`);
538
+
539
+ const stats = fs.statSync(customRolesFile);
540
+ const sizeKB = (stats.size / 1024).toFixed(1);
541
+
542
+ console.log(chalk.green(` ✅ Custom Roles exportados via Docker: ${sizeKB} KB`));
543
+
544
+ return { success: true, roles: [{ filename: 'custom-roles.sql', sizeKB }] };
545
+ } catch (error) {
546
+ console.log(chalk.yellow(` ⚠️ Erro ao exportar Custom Roles via Docker: ${error.message}`));
547
+ return { success: false, roles: [] };
548
+ }
567
549
  } catch (error) {
568
- console.log(chalk.yellow(` ⚠️ Erro ao exportar Custom Roles: ${error.message}`));
550
+ console.log(chalk.yellow(` ⚠️ Erro no backup dos Custom Roles: ${error.message}`));
569
551
  return { success: false, roles: [] };
570
552
  }
571
553
  }
572
554
 
573
- // Backup das Realtime Settings via SQL
574
- async function backupRealtimeSettings(databaseUrl, backupDir) {
555
+ // Backup das Realtime Settings via Captura Interativa
556
+ async function backupRealtimeSettings(projectId, backupDir, skipInteractive = false) {
575
557
  try {
576
- console.log(chalk.gray(' - Exportando Realtime Settings...'));
558
+ console.log(chalk.gray(' - Capturando Realtime Settings interativamente...'));
577
559
 
578
- const realtimeFile = path.join(backupDir, 'realtime-settings.sql');
560
+ const result = await captureRealtimeSettings(projectId, backupDir, skipInteractive);
579
561
 
580
- // Query para obter configurações de Realtime
581
- const realtimeQuery = `
582
- -- Realtime Settings Backup
583
- -- Publicações e configurações de Realtime
584
-
585
- -- Publicações
586
- SELECT pubname, puballtables, pubinsert, pubupdate, pubdelete, pubtruncate
587
- FROM pg_publication
588
- ORDER BY pubname;
589
-
590
- -- Tabelas publicadas
591
- SELECT p.pubname, c.relname as table_name, n.nspname as schema_name
592
- FROM pg_publication_tables pt
593
- JOIN pg_publication p ON p.oid = pt.ptpubid
594
- JOIN pg_class c ON c.oid = pt.ptrelid
595
- JOIN pg_namespace n ON n.oid = c.relnamespace
596
- ORDER BY p.pubname, n.nspname, c.relname;
597
- `;
598
-
599
- // Executar query e salvar resultado
600
- const { stdout } = await execAsync(
601
- `psql "${databaseUrl}" -t -c "${realtimeQuery}"`
602
- );
603
-
604
- const realtimeContent = `-- Realtime Settings Backup
605
- -- Generated at: ${new Date().toISOString()}
606
-
607
- ${realtimeQuery}
608
-
609
- -- Results:
610
- ${stdout}
611
- `;
612
-
613
- await fs.promises.writeFile(realtimeFile, realtimeContent);
614
-
615
- const stats = fs.statSync(realtimeFile);
562
+ const stats = fs.statSync(path.join(backupDir, 'realtime-settings.json'));
616
563
  const sizeKB = (stats.size / 1024).toFixed(1);
617
564
 
618
- console.log(chalk.green(` ✅ Realtime Settings exportados: ${sizeKB} KB`));
565
+ console.log(chalk.green(` ✅ Realtime Settings capturadas: ${sizeKB} KB`));
619
566
 
620
- return { success: true };
567
+ return { success: true, settings: result };
621
568
  } catch (error) {
622
- console.log(chalk.yellow(` ⚠️ Erro ao exportar Realtime Settings: ${error.message}`));
569
+ console.log(chalk.yellow(` ⚠️ Erro ao capturar Realtime Settings: ${error.message}`));
623
570
  return { success: false };
624
571
  }
625
572
  }
@@ -0,0 +1,205 @@
1
+ const readline = require('readline');
2
+ const fs = require('fs').promises;
3
+ const path = require('path');
4
+
5
+ /**
6
+ * Captura configurações de Realtime Settings interativamente
7
+ * @param {string} projectId - ID do projeto Supabase
8
+ * @param {string} backupDir - Diretório do backup atual
9
+ * @param {boolean} skipInteractive - Se deve pular a etapa interativa
10
+ * @returns {Promise<Object>} Configurações capturadas
11
+ */
12
+ async function captureRealtimeSettings(projectId, backupDir, skipInteractive = false) {
13
+ const settingsFile = path.join(backupDir, 'realtime-settings.json');
14
+ const dashboardUrl = `https://supabase.com/dashboard/project/${projectId}/realtime/settings`;
15
+
16
+ // Tentar ler configurações de backup anterior
17
+ const previousSettings = await getPreviousRealtimeSettings(backupDir);
18
+
19
+ if (skipInteractive && previousSettings) {
20
+ console.log('📋 Usando configurações de Realtime Settings do backup anterior...');
21
+ await fs.writeFile(settingsFile, JSON.stringify(previousSettings, null, 2));
22
+ return previousSettings;
23
+ }
24
+
25
+ if (previousSettings && !skipInteractive) {
26
+ const shouldReuse = await askToReusePreviousSettings();
27
+ if (shouldReuse) {
28
+ console.log('📋 Reutilizando configurações de Realtime Settings do backup anterior...');
29
+ await fs.writeFile(settingsFile, JSON.stringify(previousSettings, null, 2));
30
+ return previousSettings;
31
+ }
32
+ }
33
+
34
+ // Capturar configurações interativamente
35
+ console.log('\n🔧 Configurações de Realtime Settings');
36
+ console.log('═'.repeat(50));
37
+ console.log(`📱 Acesse: ${dashboardUrl}`);
38
+ console.log('📝 Anote os valores dos 4 parâmetros abaixo:\n');
39
+
40
+ const settings = await captureSettingsInteractively(projectId, previousSettings);
41
+
42
+ // Salvar configurações
43
+ await fs.writeFile(settingsFile, JSON.stringify(settings, null, 2));
44
+ console.log('\n✅ Configurações de Realtime Settings salvas!');
45
+
46
+ return settings;
47
+ }
48
+
49
+ /**
50
+ * Busca configurações de backup anterior
51
+ * @param {string} backupDir - Diretório do backup atual
52
+ * @returns {Promise<Object|null>} Configurações anteriores ou null
53
+ */
54
+ async function getPreviousRealtimeSettings(backupDir) {
55
+ try {
56
+ // Buscar em backups anteriores
57
+ const backupsDir = path.dirname(backupDir);
58
+ const entries = await fs.readdir(backupsDir, { withFileTypes: true });
59
+ const backupDirs = entries
60
+ .filter(entry => entry.isDirectory() && entry.name.startsWith('backup-'))
61
+ .map(entry => entry.name)
62
+ .sort()
63
+ .reverse(); // Mais recente primeiro
64
+
65
+ for (const backupName of backupDirs) {
66
+ const settingsPath = path.join(backupsDir, backupName, 'realtime-settings.json');
67
+ try {
68
+ const content = await fs.readFile(settingsPath, 'utf8');
69
+ const settings = JSON.parse(content);
70
+ if (settings.realtime_settings && settings.realtime_settings.settings) {
71
+ return settings;
72
+ }
73
+ } catch (error) {
74
+ // Continuar para próximo backup
75
+ continue;
76
+ }
77
+ }
78
+
79
+ return null;
80
+ } catch (error) {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Pergunta se deve reutilizar configurações anteriores
87
+ * @returns {Promise<boolean>} true se deve reutilizar
88
+ */
89
+ async function askToReusePreviousSettings() {
90
+ const rl = readline.createInterface({
91
+ input: process.stdin,
92
+ output: process.stdout
93
+ });
94
+
95
+ return new Promise((resolve) => {
96
+ rl.question('🔄 Foi identificada uma gravação anterior de Realtime Settings.\n Deseja reutilizar as configurações anteriores? (S/n): ', (answer) => {
97
+ rl.close();
98
+ const shouldReuse = !answer.toLowerCase().startsWith('n');
99
+ resolve(shouldReuse);
100
+ });
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Captura configurações interativamente via perguntas
106
+ * @param {string} projectId - ID do projeto Supabase
107
+ * @param {Object} previousSettings - Configurações anteriores para usar como padrão
108
+ * @returns {Promise<Object>} Configurações capturadas
109
+ */
110
+ async function captureSettingsInteractively(projectId, previousSettings) {
111
+ const rl = readline.createInterface({
112
+ input: process.stdin,
113
+ output: process.stdout
114
+ });
115
+
116
+ const askQuestion = (question, defaultValue) => {
117
+ return new Promise((resolve) => {
118
+ rl.question(`${question} [${defaultValue}]: `, (answer) => {
119
+ resolve(answer.trim() || defaultValue);
120
+ });
121
+ });
122
+ };
123
+
124
+ try {
125
+ console.log('📋 Responda as perguntas abaixo (pressione Enter para usar o valor padrão):\n');
126
+
127
+ // Valores padrão baseados na imagem ou configurações anteriores
128
+ const defaults = previousSettings?.realtime_settings?.settings || {
129
+ allow_public_access: { value: true },
130
+ database_connection_pool_size: { value: 2 },
131
+ max_concurrent_clients: { value: 200 },
132
+ max_events_per_second: { value: 100 }
133
+ };
134
+
135
+ const allowPublicAccess = await askQuestion(
136
+ '1. Allow public access (true/false):',
137
+ defaults.allow_public_access.value
138
+ );
139
+
140
+ const poolSize = await askQuestion(
141
+ '2. Database connection pool size:',
142
+ defaults.database_connection_pool_size.value
143
+ );
144
+
145
+ const maxClients = await askQuestion(
146
+ '3. Max concurrent clients:',
147
+ defaults.max_concurrent_clients.value
148
+ );
149
+
150
+ const maxEvents = await askQuestion(
151
+ '4. Max events per second:',
152
+ defaults.max_events_per_second.value
153
+ );
154
+
155
+ const settings = {
156
+ realtime_settings: {
157
+ note: "Configurações de Realtime Settings capturadas interativamente",
158
+ dashboard_url: `https://supabase.com/dashboard/project/${projectId}/realtime/settings`,
159
+ captured_at: new Date().toISOString(),
160
+ settings: {
161
+ allow_public_access: {
162
+ label: "Allow public access",
163
+ description: "If disabled, only private channels will be allowed",
164
+ value: allowPublicAccess === 'true'
165
+ },
166
+ database_connection_pool_size: {
167
+ label: "Database connection pool size",
168
+ description: "Realtime Authorization uses this database pool to check client access",
169
+ value: parseInt(poolSize)
170
+ },
171
+ max_concurrent_clients: {
172
+ label: "Max concurrent clients",
173
+ description: "Sets maximum number of concurrent clients that can connect to your Realtime service",
174
+ value: parseInt(maxClients)
175
+ },
176
+ max_events_per_second: {
177
+ label: "Max events per second",
178
+ description: "Sets maximum number of events per second that can be sent to your Realtime service",
179
+ value: parseInt(maxEvents)
180
+ }
181
+ },
182
+ restore_instructions: {
183
+ url: `https://supabase.com/dashboard/project/${projectId}/realtime/settings`,
184
+ steps: [
185
+ "1. Acesse a URL acima",
186
+ "2. Configure 'Allow public access' conforme o valor em settings.allow_public_access.value",
187
+ "3. Configure 'Database connection pool size' conforme o valor em settings.database_connection_pool_size.value",
188
+ "4. Configure 'Max concurrent clients' conforme o valor em settings.max_concurrent_clients.value",
189
+ "5. Configure 'Max events per second' conforme o valor em settings.max_events_per_second.value",
190
+ "6. Clique em 'Save changes'"
191
+ ]
192
+ }
193
+ }
194
+ };
195
+
196
+ return settings;
197
+
198
+ } finally {
199
+ rl.close();
200
+ }
201
+ }
202
+
203
+ module.exports = {
204
+ captureRealtimeSettings
205
+ };