smoonb 0.0.8 → 0.0.9
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 +12 -10
- package/bin/smoonb.js +1 -2
- package/package.json +3 -3
- package/src/commands/check.js +2 -1
- package/src/commands/restore.js +120 -26
- package/src/services/introspect.js +1 -1
package/README.md
CHANGED
|
@@ -107,16 +107,18 @@ backups/backup-2024-01-15T10-30-45-123Z/
|
|
|
107
107
|
└── [código das Edge Functions locais]
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
### Restauração
|
|
110
|
+
### Restauração Interativa
|
|
111
111
|
```bash
|
|
112
|
-
npx smoonb restore
|
|
112
|
+
npx smoonb restore
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
-
**Processo:**
|
|
116
|
-
1.
|
|
117
|
-
2.
|
|
118
|
-
3.
|
|
119
|
-
4. Executa `
|
|
115
|
+
**Processo interativo:**
|
|
116
|
+
1. Lista todos os backups disponíveis
|
|
117
|
+
2. Permite seleção numerada do backup desejado
|
|
118
|
+
3. Verifica se database está vazia (clean restore)
|
|
119
|
+
4. Executa `roles.sql` (roles e permissões)
|
|
120
|
+
5. Executa `schema.sql` (estrutura das tabelas)
|
|
121
|
+
6. Executa `data.sql` (dados com COPY)
|
|
120
122
|
|
|
121
123
|
### Verificação Pós-Restore
|
|
122
124
|
```bash
|
|
@@ -145,7 +147,7 @@ npx smoonb functions push
|
|
|
145
147
|
| Comando | Descrição |
|
|
146
148
|
|---------|-----------|
|
|
147
149
|
| `npx smoonb backup` | Backup completo usando Supabase CLI |
|
|
148
|
-
| `npx smoonb restore` | Restauração usando psql |
|
|
150
|
+
| `npx smoonb restore` | Restauração interativa usando psql |
|
|
149
151
|
| `npx smoonb check` | Verificação de integridade |
|
|
150
152
|
| `npx smoonb functions` | Gerenciar Edge Functions |
|
|
151
153
|
| `npx smoonb config` | Configurar credenciais |
|
|
@@ -179,8 +181,8 @@ npx smoonb backup
|
|
|
179
181
|
# 3. Configurar .smoonbrc com credenciais do novo projeto
|
|
180
182
|
npx smoonb config --init
|
|
181
183
|
|
|
182
|
-
# 4. Restaurar backup
|
|
183
|
-
npx smoonb restore
|
|
184
|
+
# 4. Restaurar backup (modo interativo)
|
|
185
|
+
npx smoonb restore
|
|
184
186
|
|
|
185
187
|
# 5. Verificar integridade
|
|
186
188
|
npx smoonb check
|
package/bin/smoonb.js
CHANGED
|
@@ -74,8 +74,7 @@ program
|
|
|
74
74
|
|
|
75
75
|
program
|
|
76
76
|
.command('restore')
|
|
77
|
-
.description('Restaurar backup completo usando psql')
|
|
78
|
-
.option('-b, --backup-dir <dir>', 'Diretório do backup a ser restaurado')
|
|
77
|
+
.description('Restaurar backup completo usando psql (modo interativo)')
|
|
79
78
|
.option('--db-url <url>', 'URL da database de destino (override)')
|
|
80
79
|
.action(commands.restore);
|
|
81
80
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smoonb",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Complete Supabase backup and migration tool - EXPERIMENTAL VERSION - USE AT YOUR OWN RISK",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
"node": ">=16.0.0"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"commander": "^11.1.0",
|
|
30
29
|
"@supabase/supabase-js": "^2.38.0",
|
|
31
30
|
"chalk": "^4.1.2",
|
|
32
|
-
"
|
|
31
|
+
"commander": "^11.1.0",
|
|
32
|
+
"inquirer": "^8.2.7"
|
|
33
33
|
},
|
|
34
34
|
"type": "commonjs",
|
|
35
35
|
"repository": {
|
package/src/commands/check.js
CHANGED
|
@@ -3,7 +3,8 @@ const chalk = require('chalk');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const { ensureBin, runCommand } = require('../utils/cli');
|
|
6
|
-
const { readConfig, validateFor
|
|
6
|
+
const { readConfig, validateFor } = require('../utils/config');
|
|
7
|
+
const { writeJson } = require('../utils/fsx');
|
|
7
8
|
const { IntrospectionService } = require('../services/introspect');
|
|
8
9
|
const { showBetaBanner } = require('../index');
|
|
9
10
|
|
package/src/commands/restore.js
CHANGED
|
@@ -2,13 +2,13 @@ const { Command } = require('commander');
|
|
|
2
2
|
const chalk = require('chalk');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const fs = require('fs');
|
|
5
|
+
const inquirer = require('inquirer');
|
|
5
6
|
const { ensureBin, runCommand } = require('../utils/cli');
|
|
6
7
|
const { readConfig, validateFor } = require('../utils/config');
|
|
7
8
|
const { showBetaBanner } = require('../index');
|
|
8
9
|
|
|
9
10
|
const restoreCommand = new Command('restore')
|
|
10
|
-
.description('Restaurar backup do projeto Supabase usando psql')
|
|
11
|
-
.option('-b, --backup-dir <dir>', 'Diretório do backup')
|
|
11
|
+
.description('Restaurar backup do projeto Supabase usando psql (modo interativo)')
|
|
12
12
|
.option('--db-url <url>', 'URL da database de destino (override)')
|
|
13
13
|
.action(async (options) => {
|
|
14
14
|
showBetaBanner();
|
|
@@ -35,31 +35,21 @@ const restoreCommand = new Command('restore')
|
|
|
35
35
|
process.exit(1);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
let backupDir = options.backupDir;
|
|
40
|
-
if (!backupDir) {
|
|
41
|
-
// Procurar backup mais recente
|
|
42
|
-
const backupsDir = config.backup.outputDir || './backups';
|
|
43
|
-
if (fs.existsSync(backupsDir)) {
|
|
44
|
-
const backups = fs.readdirSync(backupsDir)
|
|
45
|
-
.filter(dir => dir.startsWith('backup-'))
|
|
46
|
-
.sort()
|
|
47
|
-
.reverse();
|
|
48
|
-
|
|
49
|
-
if (backups.length > 0) {
|
|
50
|
-
backupDir = path.join(backupsDir, backups[0]);
|
|
51
|
-
console.log(chalk.blue(`📁 Usando backup mais recente: ${backups[0]}`));
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
38
|
+
console.log(chalk.blue(`🔍 Procurando backups em: ${config.backup.outputDir || './backups'}`));
|
|
55
39
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
40
|
+
// Listar backups disponíveis
|
|
41
|
+
const backups = await listAvailableBackups(config.backup.outputDir || './backups');
|
|
42
|
+
|
|
43
|
+
if (backups.length === 0) {
|
|
44
|
+
console.error(chalk.red('❌ Nenhum backup encontrado'));
|
|
45
|
+
console.log(chalk.yellow('💡 Execute primeiro: npx smoonb backup'));
|
|
59
46
|
process.exit(1);
|
|
60
47
|
}
|
|
61
48
|
|
|
62
|
-
|
|
49
|
+
// Seleção interativa do backup
|
|
50
|
+
const selectedBackup = await selectBackup(backups);
|
|
51
|
+
|
|
52
|
+
console.log(chalk.blue(`🚀 Iniciando restauração do backup: ${selectedBackup.name}`));
|
|
63
53
|
console.log(chalk.blue(`🎯 Database destino: ${databaseUrl.replace(/:[^:]*@/, ':***@')}`));
|
|
64
54
|
|
|
65
55
|
// Verificar se é clean restore
|
|
@@ -68,13 +58,11 @@ const restoreCommand = new Command('restore')
|
|
|
68
58
|
}
|
|
69
59
|
|
|
70
60
|
// Executar restauração
|
|
71
|
-
await performRestore(
|
|
61
|
+
await performRestore(selectedBackup.path, databaseUrl);
|
|
72
62
|
|
|
73
63
|
// Verificação pós-restore
|
|
74
64
|
if (config.restore.verifyAfterRestore) {
|
|
75
65
|
console.log(chalk.blue('\n🔍 Executando verificação pós-restore...'));
|
|
76
|
-
// TODO: Implementar verificação automática
|
|
77
|
-
console.log(chalk.yellow('⚠️ Verificação automática não implementada ainda'));
|
|
78
66
|
console.log(chalk.yellow('💡 Execute manualmente: npx smoonb check'));
|
|
79
67
|
}
|
|
80
68
|
|
|
@@ -86,6 +74,112 @@ const restoreCommand = new Command('restore')
|
|
|
86
74
|
}
|
|
87
75
|
});
|
|
88
76
|
|
|
77
|
+
// Listar backups disponíveis
|
|
78
|
+
async function listAvailableBackups(backupsDir) {
|
|
79
|
+
if (!fs.existsSync(backupsDir)) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const items = fs.readdirSync(backupsDir, { withFileTypes: true });
|
|
84
|
+
const backups = [];
|
|
85
|
+
|
|
86
|
+
for (const item of items) {
|
|
87
|
+
if (item.isDirectory() && item.name.startsWith('backup-')) {
|
|
88
|
+
const backupPath = path.join(backupsDir, item.name);
|
|
89
|
+
const manifestPath = path.join(backupPath, 'backup-manifest.json');
|
|
90
|
+
|
|
91
|
+
let manifest = null;
|
|
92
|
+
if (fs.existsSync(manifestPath)) {
|
|
93
|
+
try {
|
|
94
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.warn(chalk.yellow(`⚠️ Erro ao ler manifesto: ${item.name}`));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const stats = fs.statSync(backupPath);
|
|
101
|
+
|
|
102
|
+
backups.push({
|
|
103
|
+
name: item.name,
|
|
104
|
+
path: backupPath,
|
|
105
|
+
created: manifest?.created_at || stats.birthtime.toISOString(),
|
|
106
|
+
projectId: manifest?.project_id || 'Desconhecido',
|
|
107
|
+
size: getDirectorySize(backupPath),
|
|
108
|
+
manifest: manifest
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Ordenar por data de criação (mais recente primeiro)
|
|
114
|
+
return backups.sort((a, b) => new Date(b.created) - new Date(a.created));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Calcular tamanho do diretório
|
|
118
|
+
function getDirectorySize(dirPath) {
|
|
119
|
+
let totalSize = 0;
|
|
120
|
+
|
|
121
|
+
function calculateSize(itemPath) {
|
|
122
|
+
const stats = fs.statSync(itemPath);
|
|
123
|
+
|
|
124
|
+
if (stats.isDirectory()) {
|
|
125
|
+
const items = fs.readdirSync(itemPath);
|
|
126
|
+
for (const item of items) {
|
|
127
|
+
calculateSize(path.join(itemPath, item));
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
totalSize += stats.size;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
calculateSize(dirPath);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
// Ignorar erros de acesso
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return formatBytes(totalSize);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Formatar bytes em formato legível
|
|
144
|
+
function formatBytes(bytes) {
|
|
145
|
+
if (bytes === 0) return '0 Bytes';
|
|
146
|
+
|
|
147
|
+
const k = 1024;
|
|
148
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
149
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
150
|
+
|
|
151
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Seleção interativa do backup
|
|
155
|
+
async function selectBackup(backups) {
|
|
156
|
+
console.log(chalk.blue('\n📋 Backups disponíveis:'));
|
|
157
|
+
console.log(chalk.blue('═'.repeat(80)));
|
|
158
|
+
|
|
159
|
+
const choices = backups.map((backup, index) => {
|
|
160
|
+
const date = new Date(backup.created).toLocaleString('pt-BR');
|
|
161
|
+
const projectInfo = backup.projectId !== 'Desconhecido' ? ` (${backup.projectId})` : '';
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
name: `${index + 1}. ${backup.name}${projectInfo}\n 📅 ${date} | 📦 ${backup.size}`,
|
|
165
|
+
value: backup,
|
|
166
|
+
short: backup.name
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const { selectedBackup } = await inquirer.prompt([
|
|
171
|
+
{
|
|
172
|
+
type: 'list',
|
|
173
|
+
name: 'selectedBackup',
|
|
174
|
+
message: 'Selecione o backup para restaurar:',
|
|
175
|
+
choices: choices,
|
|
176
|
+
pageSize: 10
|
|
177
|
+
}
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
return selectedBackup;
|
|
181
|
+
}
|
|
182
|
+
|
|
89
183
|
// Verificar se é possível fazer clean restore
|
|
90
184
|
async function checkCleanRestore(databaseUrl) {
|
|
91
185
|
try {
|