smoonb 0.0.70 → 0.0.72

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.
@@ -1,11 +1,16 @@
1
1
  const chalk = require('chalk');
2
2
  const path = require('path');
3
+ const fs = require('fs').promises;
4
+ const AdmZip = require('adm-zip');
5
+ const { createClient } = require('@supabase/supabase-js');
3
6
  const { ensureDir, writeJson } = require('../../../utils/fsx');
7
+ const { confirm } = require('../../../utils/prompt');
4
8
 
5
9
  /**
6
10
  * Etapa 6: Backup Storage via Supabase API
11
+ * Agora faz backup completo: metadados + download de todos os arquivos + ZIP no padrão do Dashboard
7
12
  */
8
- module.exports = async ({ projectId, accessToken, backupDir }) => {
13
+ module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supabaseServiceKey }) => {
9
14
  try {
10
15
  const storageDir = path.join(backupDir, 'storage');
11
16
  await ensureDir(storageDir);
@@ -37,7 +42,25 @@ module.exports = async ({ projectId, accessToken, backupDir }) => {
37
42
 
38
43
  console.log(chalk.white(` - Encontrados ${buckets.length} buckets`));
39
44
 
45
+ // Validar credenciais do Supabase para download de arquivos
46
+ if (!supabaseUrl || !supabaseServiceKey) {
47
+ console.log(chalk.yellow(' ⚠️ Credenciais do Supabase não disponíveis. Fazendo backup apenas de metadados...'));
48
+ return await backupMetadataOnly(buckets, storageDir, projectId, accessToken);
49
+ }
50
+
51
+ // Criar cliente Supabase para download de arquivos
52
+ const supabase = createClient(supabaseUrl, supabaseServiceKey);
53
+
54
+ // Criar estrutura temporária para armazenar arquivos baixados
55
+ const tempStorageDir = path.join(backupDir, 'storage_temp');
56
+ await ensureDir(tempStorageDir);
57
+
58
+ // Criar estrutura: storage_temp/project-id/bucket-name/arquivos...
59
+ const projectStorageDir = path.join(tempStorageDir, projectId);
60
+ await ensureDir(projectStorageDir);
61
+
40
62
  const processedBuckets = [];
63
+ let totalFilesDownloaded = 0;
41
64
 
42
65
  for (const bucket of buckets || []) {
43
66
  try {
@@ -69,22 +92,222 @@ module.exports = async ({ projectId, accessToken, backupDir }) => {
69
92
  const bucketPath = path.join(storageDir, `${bucket.name}.json`);
70
93
  await writeJson(bucketPath, bucketInfo);
71
94
 
95
+ // Baixar todos os arquivos do bucket
96
+ const bucketDir = path.join(projectStorageDir, bucket.name);
97
+ await ensureDir(bucketDir);
98
+
99
+ // Listar todos os arquivos recursivamente usando Supabase client
100
+ console.log(chalk.white(` - Listando arquivos do bucket ${bucket.name}...`));
101
+ const allFiles = await listAllFilesRecursively(supabase, bucket.name, '');
102
+
103
+ let filesDownloaded = 0;
104
+ if (allFiles.length > 0) {
105
+ console.log(chalk.white(` - Baixando ${allFiles.length} arquivo(s) do bucket ${bucket.name}...`));
106
+
107
+ for (const filePath of allFiles) {
108
+ try {
109
+ // Baixar arquivo do Storage
110
+ const { data: fileData, error: downloadError } = await supabase.storage
111
+ .from(bucket.name)
112
+ .download(filePath);
113
+
114
+ if (downloadError) {
115
+ console.log(chalk.yellow(` ⚠️ Erro ao baixar ${filePath}: ${downloadError.message}`));
116
+ continue;
117
+ }
118
+
119
+ // Criar estrutura de pastas local se necessário
120
+ const localFilePath = path.join(bucketDir, filePath);
121
+ const localFileDir = path.dirname(localFilePath);
122
+ await ensureDir(localFileDir);
123
+
124
+ // Salvar arquivo localmente
125
+ const arrayBuffer = await fileData.arrayBuffer();
126
+ const buffer = Buffer.from(arrayBuffer);
127
+ await fs.writeFile(localFilePath, buffer);
128
+ filesDownloaded++;
129
+
130
+ // Mostrar progresso a cada 10 arquivos ou se for o último
131
+ if (filesDownloaded % 10 === 0 || filesDownloaded === allFiles.length) {
132
+ console.log(chalk.white(` - Baixados ${filesDownloaded}/${allFiles.length} arquivo(s)...`));
133
+ }
134
+ } catch (fileError) {
135
+ console.log(chalk.yellow(` ⚠️ Erro ao processar arquivo ${filePath}: ${fileError.message}`));
136
+ }
137
+ }
138
+ }
139
+
140
+ totalFilesDownloaded += filesDownloaded;
72
141
  processedBuckets.push({
73
142
  name: bucket.name,
74
- objectCount: objects?.length || 0
143
+ objectCount: objects?.length || 0,
144
+ filesDownloaded: filesDownloaded,
145
+ totalFiles: allFiles.length
75
146
  });
76
147
 
77
- console.log(chalk.green(` ✅ Bucket ${bucket.name}: ${objects?.length || 0} objetos`));
148
+ console.log(chalk.green(` ✅ Bucket ${bucket.name}: ${filesDownloaded}/${allFiles.length} arquivo(s) baixado(s)`));
78
149
  } catch (error) {
79
150
  console.log(chalk.yellow(` ⚠️ Erro ao processar bucket ${bucket.name}: ${error.message}`));
80
151
  }
81
152
  }
82
153
 
83
- console.log(chalk.green(`✅ Storage backupado: ${processedBuckets.length} buckets`));
84
- return { success: true, buckets: processedBuckets };
154
+ // Criar ZIP no padrão do Dashboard: {project-id}.storage.zip
155
+ console.log(chalk.white('\n - Criando arquivo ZIP no padrão do Dashboard...'));
156
+ const zipFileName = `${projectId}.storage.zip`;
157
+ const zipFilePath = path.join(backupDir, zipFileName);
158
+
159
+ const zip = new AdmZip();
160
+
161
+ // Adicionar toda a estrutura de pastas ao ZIP
162
+ // Estrutura: project-id/bucket-name/arquivos...
163
+ await addDirectoryToZip(zip, projectStorageDir, projectId);
164
+
165
+ // Salvar ZIP
166
+ zip.writeZip(zipFilePath);
167
+ const zipStats = await fs.stat(zipFilePath);
168
+ const zipSizeMB = (zipStats.size / (1024 * 1024)).toFixed(2);
169
+
170
+ console.log(chalk.green(` ✅ Arquivo ZIP criado: ${zipFileName} (${zipSizeMB} MB)`));
171
+
172
+ // Perguntar ao usuário se deseja limpar a estrutura temporária
173
+ const tempDirName = path.basename(tempStorageDir);
174
+ const shouldCleanup = await confirm(` Deseja limpar ${tempDirName} após o backup`, false);
175
+
176
+ if (shouldCleanup) {
177
+ console.log(chalk.white(` - Limpando estrutura temporária...`));
178
+ try {
179
+ await fs.rm(tempStorageDir, { recursive: true, force: true });
180
+ console.log(chalk.green(` ✅ Estrutura temporária removida`));
181
+ } catch (cleanupError) {
182
+ console.log(chalk.yellow(` ⚠️ Erro ao limpar estrutura temporária: ${cleanupError.message}`));
183
+ }
184
+ } else {
185
+ console.log(chalk.white(` ℹ️ Estrutura temporária mantida em: ${path.relative(process.cwd(), tempStorageDir)}`));
186
+ }
187
+
188
+ console.log(chalk.green(`✅ Storage backupado: ${processedBuckets.length} buckets, ${totalFilesDownloaded} arquivo(s) baixado(s)`));
189
+ return {
190
+ success: true,
191
+ buckets: processedBuckets,
192
+ zipFile: zipFileName,
193
+ zipSizeMB: zipSizeMB,
194
+ totalFiles: totalFilesDownloaded,
195
+ tempDirCleaned: shouldCleanup
196
+ };
85
197
  } catch (error) {
86
198
  console.log(chalk.yellow(`⚠️ Erro no backup do Storage: ${error.message}`));
87
199
  return { success: false, buckets: [] };
88
200
  }
89
201
  };
90
202
 
203
+ /**
204
+ * Backup apenas de metadados (fallback quando não há credenciais do Supabase)
205
+ */
206
+ async function backupMetadataOnly(buckets, storageDir, projectId, accessToken) {
207
+ const processedBuckets = [];
208
+
209
+ for (const bucket of buckets || []) {
210
+ try {
211
+ console.log(chalk.white(` - Processando bucket: ${bucket.name}`));
212
+
213
+ const objectsResponse = await fetch(`https://api.supabase.com/v1/projects/${projectId}/storage/buckets/${bucket.name}/objects`, {
214
+ headers: {
215
+ 'Authorization': `Bearer ${accessToken}`,
216
+ 'Content-Type': 'application/json'
217
+ }
218
+ });
219
+
220
+ let objects = [];
221
+ if (objectsResponse.ok) {
222
+ objects = await objectsResponse.json();
223
+ }
224
+
225
+ const bucketInfo = {
226
+ id: bucket.id,
227
+ name: bucket.name,
228
+ public: bucket.public,
229
+ file_size_limit: bucket.file_size_limit,
230
+ allowed_mime_types: bucket.allowed_mime_types,
231
+ objects: objects || []
232
+ };
233
+
234
+ const bucketPath = path.join(storageDir, `${bucket.name}.json`);
235
+ await writeJson(bucketPath, bucketInfo);
236
+
237
+ processedBuckets.push({
238
+ name: bucket.name,
239
+ objectCount: objects?.length || 0
240
+ });
241
+
242
+ console.log(chalk.green(` ✅ Bucket ${bucket.name}: ${objects?.length || 0} objetos`));
243
+ } catch (error) {
244
+ console.log(chalk.yellow(` ⚠️ Erro ao processar bucket ${bucket.name}: ${error.message}`));
245
+ }
246
+ }
247
+
248
+ console.log(chalk.green(`✅ Storage backupado (apenas metadados): ${processedBuckets.length} buckets`));
249
+ return { success: true, buckets: processedBuckets };
250
+ }
251
+
252
+ /**
253
+ * Lista todos os arquivos recursivamente de um bucket do Storage
254
+ */
255
+ async function listAllFilesRecursively(supabase, bucketName, folderPath = '') {
256
+ const allFiles = [];
257
+
258
+ try {
259
+ // Listar arquivos e pastas no caminho atual
260
+ const { data: items, error } = await supabase.storage
261
+ .from(bucketName)
262
+ .list(folderPath, {
263
+ limit: 1000,
264
+ sortBy: { column: 'name', order: 'asc' }
265
+ });
266
+
267
+ if (error) {
268
+ console.log(chalk.yellow(` ⚠️ Erro ao listar ${folderPath || 'raiz'}: ${error.message}`));
269
+ return allFiles;
270
+ }
271
+
272
+ if (!items || items.length === 0) {
273
+ return allFiles;
274
+ }
275
+
276
+ for (const item of items) {
277
+ const itemPath = folderPath ? `${folderPath}/${item.name}` : item.name;
278
+
279
+ if (item.id === null) {
280
+ // É uma pasta, listar recursivamente
281
+ const subFiles = await listAllFilesRecursively(supabase, bucketName, itemPath);
282
+ allFiles.push(...subFiles);
283
+ } else {
284
+ // É um arquivo
285
+ allFiles.push(itemPath);
286
+ }
287
+ }
288
+ } catch (error) {
289
+ console.log(chalk.yellow(` ⚠️ Erro ao processar ${folderPath || 'raiz'}: ${error.message}`));
290
+ }
291
+
292
+ return allFiles;
293
+ }
294
+
295
+ /**
296
+ * Adiciona um diretório recursivamente ao ZIP mantendo a estrutura de pastas
297
+ */
298
+ async function addDirectoryToZip(zip, dirPath, basePath = '') {
299
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
300
+
301
+ for (const entry of entries) {
302
+ const fullPath = path.join(dirPath, entry.name);
303
+ const zipPath = basePath ? `${basePath}/${entry.name}` : entry.name;
304
+
305
+ if (entry.isDirectory()) {
306
+ await addDirectoryToZip(zip, fullPath, zipPath);
307
+ } else {
308
+ const fileContent = await fs.readFile(fullPath);
309
+ zip.addFile(zipPath, fileContent);
310
+ }
311
+ }
312
+ }
313
+
@@ -1,63 +1,57 @@
1
1
  const chalk = require('chalk');
2
+ const { t } = require('../../i18n');
2
3
 
3
4
  /**
4
5
  * Função para mostrar mensagens educativas e encerrar elegantemente
5
6
  */
6
7
  function showDockerMessagesAndExit(reason) {
8
+ const getT = global.smoonbI18n?.t || t;
9
+
7
10
  console.log('');
8
11
 
9
12
  switch (reason) {
10
13
  case 'docker_not_installed':
11
- console.log(chalk.red('❌ DOCKER DESKTOP NÃO ENCONTRADO'));
14
+ console.log(chalk.red(`❌ ${getT('docker.notInstalled')}`));
12
15
  console.log('');
13
- console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:'));
14
- console.log(chalk.yellow(' 1. Instalar Docker Desktop'));
15
- console.log(chalk.yellow(' 2. Executar Docker Desktop'));
16
- console.log(chalk.yellow(' 3. Repetir o comando de backup'));
16
+ console.log(chalk.yellow(`📋 ${getT('docker.instructions')}`));
17
+ console.log(chalk.yellow(` 1. ${getT('docker.installDocker')}`));
18
+ console.log(chalk.yellow(` 2. ${getT('docker.runDocker')}`));
19
+ console.log(chalk.yellow(` 3. ${getT('docker.repeatCommand')}`));
17
20
  console.log('');
18
- console.log(chalk.blue('🔗 Download: https://docs.docker.com/desktop/install/'));
21
+ console.log(chalk.blue(`🔗 ${getT('docker.download')}`));
19
22
  console.log('');
20
- console.log(chalk.gray('💡 O Docker Desktop é obrigatório para backup completo do Supabase'));
21
- console.log(chalk.gray(' - Database PostgreSQL'));
22
- console.log(chalk.gray(' - Edge Functions'));
23
- console.log(chalk.gray(' - Todos os componentes via Supabase CLI'));
23
+ console.log(chalk.gray(`💡 ${getT('docker.requiredComponents')}`));
24
24
  break;
25
25
 
26
26
  case 'docker_not_running':
27
- console.log(chalk.red('❌ DOCKER DESKTOP NÃO ESTÁ EXECUTANDO'));
27
+ console.log(chalk.red(`❌ ${getT('docker.notRunning')}`));
28
28
  console.log('');
29
- console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:'));
30
- console.log(chalk.yellow(' 1. Abrir Docker Desktop'));
31
- console.log(chalk.yellow(' 2. Aguardar inicialização completa'));
32
- console.log(chalk.yellow(' 3. Repetir o comando de backup'));
29
+ console.log(chalk.yellow(`📋 ${getT('docker.instructions')}`));
30
+ console.log(chalk.yellow(` 1. ${getT('docker.runDocker')}`));
31
+ console.log(chalk.yellow(` 2. ${getT('docker.waitInitialization')}`));
32
+ console.log(chalk.yellow(` 3. ${getT('docker.repeatCommand')}`));
33
33
  console.log('');
34
- console.log(chalk.blue('💡 Dica: Docker Desktop deve estar rodando em segundo plano'));
34
+ console.log(chalk.blue(`💡 ${getT('docker.tip')}`));
35
35
  console.log('');
36
- console.log(chalk.gray('💡 O Docker Desktop é obrigatório para backup completo do Supabase'));
37
- console.log(chalk.gray(' - Database PostgreSQL'));
38
- console.log(chalk.gray(' - Edge Functions'));
39
- console.log(chalk.gray(' - Todos os componentes via Supabase CLI'));
36
+ console.log(chalk.gray(`💡 ${getT('docker.requiredComponents')}`));
40
37
  break;
41
38
 
42
39
  case 'supabase_cli_not_found':
43
- console.log(chalk.red('❌ SUPABASE CLI NÃO ENCONTRADO'));
40
+ console.log(chalk.red(`❌ ${getT('supabase.cliNotFound')}`));
44
41
  console.log('');
45
- console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:'));
46
- console.log(chalk.yellow(' 1. Instalar Supabase CLI'));
47
- console.log(chalk.yellow(' 2. Repetir o comando de backup'));
42
+ console.log(chalk.yellow(`📋 ${getT('supabase.installInstructions')}`));
43
+ console.log(chalk.yellow(` 1. ${getT('supabase.installCli')}`));
44
+ console.log(chalk.yellow(` 2. ${getT('supabase.repeatCommand')}`));
48
45
  console.log('');
49
- console.log(chalk.blue('🔗 Instalação: npm install -g supabase'));
46
+ console.log(chalk.blue(`🔗 ${getT('supabase.installLink')}`));
50
47
  console.log('');
51
- console.log(chalk.gray('💡 O Supabase CLI é obrigatório para backup completo do Supabase'));
52
- console.log(chalk.gray(' - Database PostgreSQL'));
53
- console.log(chalk.gray(' - Edge Functions'));
54
- console.log(chalk.gray(' - Todos os componentes via Docker'));
48
+ console.log(chalk.gray(`💡 ${getT('supabase.requiredComponents')}`));
55
49
  break;
56
50
  }
57
51
 
58
52
  console.log('');
59
- console.log(chalk.red('🚫 Backup cancelado - Pré-requisitos não atendidos'));
60
- console.log(chalk.gray(' Instale os componentes necessários e tente novamente'));
53
+ console.log(chalk.red(`🚫 ${getT('docker.cancelled')}`));
54
+ console.log(chalk.gray(` ${getT('docker.installComponents')}`));
61
55
  console.log('');
62
56
 
63
57
  process.exit(1);
@@ -5,17 +5,20 @@ const { readConfig, validateFor } = require('../utils/config');
5
5
  const { writeJson } = require('../utils/fsx');
6
6
  const { IntrospectionService } = require('../services/introspect');
7
7
  const { showBetaBanner } = require('../utils/banner');
8
+ const { t } = require('../i18n');
8
9
 
9
10
  // Exportar FUNÇÃO em vez de objeto Command
10
11
  module.exports = async () => {
11
12
  showBetaBanner();
12
13
 
13
14
  try {
15
+ const getT = global.smoonbI18n?.t || t;
16
+
14
17
  // Verificar se psql está disponível
15
18
  const psqlPath = await ensureBin('psql');
16
19
  if (!psqlPath) {
17
- console.error(chalk.red('❌ psql não encontrado'));
18
- console.log(chalk.yellow('💡 Instale PostgreSQL:'));
20
+ console.error(chalk.red(`❌ ${getT('check.psqlNotFound')}`));
21
+ console.log(chalk.yellow(`💡 ${getT('check.installPostgres')}`));
19
22
  console.log(chalk.yellow(' https://www.postgresql.org/download/'));
20
23
  process.exit(1);
21
24
  }
@@ -26,12 +29,12 @@ module.exports = async () => {
26
29
 
27
30
  const databaseUrl = config.supabase.databaseUrl;
28
31
  if (!databaseUrl) {
29
- console.error(chalk.red('❌ databaseUrl não configurada'));
30
- console.log(chalk.yellow('💡 Configure databaseUrl no .smoonbrc'));
32
+ console.error(chalk.red(`❌ ${getT('check.databaseUrlNotConfigured')}`));
33
+ console.log(chalk.yellow(`💡 ${getT('check.configureDatabaseUrl')}`));
31
34
  process.exit(1);
32
35
  }
33
36
 
34
- console.log(chalk.blue(`🔍 Verificando integridade do projeto: ${config.supabase.projectId}`));
37
+ console.log(chalk.blue(`🔍 ${getT('check.start', { projectId: config.supabase.projectId })}`));
35
38
 
36
39
  // Executar verificações
37
40
  const report = await performChecks(config, databaseUrl);
@@ -43,11 +46,13 @@ module.exports = async () => {
43
46
  // Mostrar resumo
44
47
  showCheckSummary(report);
45
48
 
46
- console.log(chalk.green('\n🎉 Verificação concluída!'));
47
- console.log(chalk.blue(`📋 Relatório salvo em: ${reportPath}`));
49
+ const getT = global.smoonbI18n?.t || t;
50
+ console.log(chalk.green(`\n🎉 ${getT('check.done')}`));
51
+ console.log(chalk.blue(`📋 ${getT('check.reportSaved', { path: reportPath })}`));
48
52
 
49
53
  } catch (error) {
50
- console.error(chalk.red(`❌ Erro na verificação: ${error.message}`));
54
+ const getT = global.smoonbI18n?.t || t;
55
+ console.error(chalk.red(`❌ ${getT('check.error', { message: error.message })}`));
51
56
  process.exit(1);
52
57
  }
53
58
  };