smoonb 0.0.47 → 0.0.49
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 +339 -87
- package/bin/smoonb.js +2 -2
- package/package.json +1 -1
- package/src/commands/backup/index.js +316 -0
- package/src/commands/backup/steps/00-docker-validation.js +24 -0
- package/src/commands/backup/steps/01-database.js +72 -0
- package/src/commands/backup/steps/02-database-separated.js +82 -0
- package/src/commands/backup/steps/03-database-settings.js +178 -0
- package/src/commands/backup/steps/04-auth-settings.js +43 -0
- package/src/commands/backup/steps/05-realtime-settings.js +26 -0
- package/src/commands/backup/steps/06-storage.js +90 -0
- package/src/commands/backup/steps/07-custom-roles.js +39 -0
- package/src/commands/backup/steps/08-edge-functions.js +153 -0
- package/src/commands/backup/steps/09-supabase-temp.js +42 -0
- package/src/commands/backup/steps/10-migrations.js +74 -0
- package/src/commands/backup/utils.js +69 -0
- package/src/commands/check.js +0 -1
- package/src/commands/config.js +0 -1
- package/src/commands/functions.js +1 -1
- package/src/commands/restore/index.js +206 -0
- package/src/commands/restore/steps/00-backup-selection.js +38 -0
- package/src/commands/restore/steps/01-components-selection.js +71 -0
- package/src/commands/restore/steps/02-confirmation.js +14 -0
- package/src/commands/restore/steps/03-database.js +81 -0
- package/src/commands/restore/steps/04-edge-functions.js +112 -0
- package/src/commands/restore/steps/05-auth-settings.js +51 -0
- package/src/commands/restore/steps/06-storage.js +58 -0
- package/src/commands/restore/steps/07-database-settings.js +65 -0
- package/src/commands/restore/steps/08-realtime-settings.js +50 -0
- package/src/commands/restore/utils.js +139 -0
- package/src/index.js +3 -3
- package/src/interactive/envMapper.js +38 -14
- package/src/utils/cli.js +1 -1
- package/src/utils/config.js +1 -3
- package/src/utils/docker.js +3 -3
- package/src/utils/env.js +2 -3
- package/src/utils/envMap.js +1 -1
- package/src/utils/fsExtra.js +98 -0
- package/src/utils/fsx.js +2 -2
- package/src/utils/prompt.js +34 -0
- package/src/utils/realtime-settings.js +2 -2
- package/src/utils/supabase.js +10 -10
- package/src/utils/supabaseLink.js +82 -0
- package/src/utils/validation.js +2 -2
- package/src/commands/backup.js +0 -939
- package/src/commands/restore.js +0 -786
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Etapa 7: Restaurar Database Settings (via SQL)
|
|
8
|
+
*/
|
|
9
|
+
module.exports = async ({ backupPath, targetProject }) => {
|
|
10
|
+
console.log(chalk.blue('\n🔧 Restaurando Database Settings...'));
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const files = fs.readdirSync(backupPath);
|
|
14
|
+
const dbSettingsFile = files.find(f => f.startsWith('database-settings-') && f.endsWith('.json'));
|
|
15
|
+
|
|
16
|
+
if (!dbSettingsFile) {
|
|
17
|
+
console.log(chalk.yellow(' ⚠️ Nenhuma configuração de Database encontrada no backup'));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const dbSettingsData = JSON.parse(fs.readFileSync(path.join(backupPath, dbSettingsFile), 'utf8'));
|
|
22
|
+
const dbSettings = dbSettingsData.database_settings || dbSettingsData;
|
|
23
|
+
|
|
24
|
+
const extensions = dbSettings.extensions || [];
|
|
25
|
+
|
|
26
|
+
if (extensions.length > 0) {
|
|
27
|
+
console.log(chalk.gray(` - Habilitando ${extensions.length} extension(s)...`));
|
|
28
|
+
|
|
29
|
+
for (const ext of extensions) {
|
|
30
|
+
const extName = typeof ext === 'string' ? ext : ext.name;
|
|
31
|
+
console.log(chalk.gray(` - ${extName}`));
|
|
32
|
+
|
|
33
|
+
const sqlCommand = `CREATE EXTENSION IF NOT EXISTS ${extName};`;
|
|
34
|
+
|
|
35
|
+
const urlMatch = targetProject.targetDatabaseUrl.match(/postgresql:\/\/([^@:]+):([^@]+)@(.+)$/);
|
|
36
|
+
|
|
37
|
+
if (!urlMatch) {
|
|
38
|
+
console.log(chalk.yellow(` ⚠️ URL inválida para ${extName}`));
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const dockerCmd = [
|
|
43
|
+
'docker run --rm',
|
|
44
|
+
'--network host',
|
|
45
|
+
`-e PGPASSWORD="${encodeURIComponent(urlMatch[2])}"`,
|
|
46
|
+
'postgres:17 psql',
|
|
47
|
+
`-d "${targetProject.targetDatabaseUrl}"`,
|
|
48
|
+
`-c "${sqlCommand}"`
|
|
49
|
+
].join(' ');
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
execSync(dockerCmd, { stdio: 'pipe', encoding: 'utf8' });
|
|
53
|
+
} catch {
|
|
54
|
+
console.log(chalk.yellow(` ⚠️ ${extName} - extension já existe ou não pode ser habilitada`));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(chalk.green(' ✅ Database Settings restaurados com sucesso!'));
|
|
60
|
+
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error(chalk.red(` ❌ Erro ao restaurar Database Settings: ${error.message}`));
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Etapa 8: Restaurar Realtime Settings (interativo - exibir URL e valores)
|
|
8
|
+
*/
|
|
9
|
+
module.exports = async ({ backupPath, targetProject }) => {
|
|
10
|
+
console.log(chalk.blue('\n🔄 Restaurando Realtime Settings...'));
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const realtimeSettingsPath = path.join(backupPath, 'realtime-settings.json');
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(realtimeSettingsPath)) {
|
|
16
|
+
console.log(chalk.yellow(' ⚠️ Nenhuma configuração de Realtime encontrada no backup'));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const realtimeSettings = JSON.parse(fs.readFileSync(realtimeSettingsPath, 'utf8'));
|
|
21
|
+
const dashboardUrl = `https://supabase.com/dashboard/project/${targetProject.targetProjectId}/realtime/settings`;
|
|
22
|
+
|
|
23
|
+
console.log(chalk.green('\n ✅ URL para configuração manual:'));
|
|
24
|
+
console.log(chalk.cyan(` ${dashboardUrl}`));
|
|
25
|
+
console.log(chalk.yellow('\n 📋 Configure manualmente as seguintes opções:'));
|
|
26
|
+
|
|
27
|
+
if (realtimeSettings.realtime_settings?.settings) {
|
|
28
|
+
Object.values(realtimeSettings.realtime_settings.settings).forEach((setting) => {
|
|
29
|
+
console.log(chalk.gray(` - ${setting.label}: ${setting.value}`));
|
|
30
|
+
if (setting.description) {
|
|
31
|
+
console.log(chalk.gray(` ${setting.description}`));
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(chalk.yellow('\n ⚠️ Após configurar, pressione Enter para continuar...'));
|
|
37
|
+
|
|
38
|
+
await inquirer.prompt([{
|
|
39
|
+
type: 'input',
|
|
40
|
+
name: 'continue',
|
|
41
|
+
message: 'Pressione Enter para continuar'
|
|
42
|
+
}]);
|
|
43
|
+
|
|
44
|
+
console.log(chalk.green(' ✅ Realtime Settings processados'));
|
|
45
|
+
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error(chalk.red(` ❌ Erro ao processar Realtime Settings: ${error.message}`));
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Listar backups válidos (aceita .backup.gz e .backup)
|
|
7
|
+
*/
|
|
8
|
+
async function listValidBackups(backupsDir) {
|
|
9
|
+
if (!fs.existsSync(backupsDir)) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const items = fs.readdirSync(backupsDir, { withFileTypes: true });
|
|
14
|
+
const validBackups = [];
|
|
15
|
+
|
|
16
|
+
for (const item of items) {
|
|
17
|
+
if (item.isDirectory() && item.name.startsWith('backup-')) {
|
|
18
|
+
const backupPath = path.join(backupsDir, item.name);
|
|
19
|
+
const files = fs.readdirSync(backupPath);
|
|
20
|
+
// Aceitar tanto .backup.gz quanto .backup
|
|
21
|
+
const backupFile = files.find(file =>
|
|
22
|
+
file.endsWith('.backup.gz') || file.endsWith('.backup')
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
if (backupFile) {
|
|
26
|
+
const manifestPath = path.join(backupPath, 'backup-manifest.json');
|
|
27
|
+
let manifest = null;
|
|
28
|
+
|
|
29
|
+
if (fs.existsSync(manifestPath)) {
|
|
30
|
+
try {
|
|
31
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
32
|
+
} catch {
|
|
33
|
+
// Ignorar erro de leitura do manifest
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const stats = fs.statSync(path.join(backupPath, backupFile));
|
|
38
|
+
|
|
39
|
+
validBackups.push({
|
|
40
|
+
name: item.name,
|
|
41
|
+
path: backupPath,
|
|
42
|
+
backupFile: backupFile,
|
|
43
|
+
created: manifest?.created_at || stats.birthtime.toISOString(),
|
|
44
|
+
projectId: manifest?.project_id || 'Desconhecido',
|
|
45
|
+
size: formatBytes(stats.size),
|
|
46
|
+
manifest: manifest
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return validBackups.sort((a, b) => new Date(b.created) - new Date(a.created));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Formatar bytes
|
|
57
|
+
*/
|
|
58
|
+
function formatBytes(bytes) {
|
|
59
|
+
if (bytes === 0) return '0 Bytes';
|
|
60
|
+
const k = 1024;
|
|
61
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
62
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
63
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Mostrar resumo da restauração
|
|
68
|
+
*/
|
|
69
|
+
function showRestoreSummary(backup, components, targetProject) {
|
|
70
|
+
console.log(chalk.blue('\n📋 Resumo da Restauração:'));
|
|
71
|
+
console.log(chalk.blue('═'.repeat(80)));
|
|
72
|
+
console.log(chalk.cyan(`📦 Backup: ${backup.name}`));
|
|
73
|
+
console.log(chalk.cyan(`📤 Projeto Origem: ${backup.projectId}`));
|
|
74
|
+
console.log(chalk.cyan(`📥 Projeto Destino: ${targetProject.targetProjectId}`));
|
|
75
|
+
console.log('');
|
|
76
|
+
console.log(chalk.cyan('Componentes que serão restaurados:'));
|
|
77
|
+
console.log('');
|
|
78
|
+
|
|
79
|
+
if (components.database) {
|
|
80
|
+
console.log('✅ Database (psql -f via Docker)');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (components.edgeFunctions) {
|
|
84
|
+
const edgeFunctionsDir = path.join(backup.path, 'edge-functions');
|
|
85
|
+
const functions = fs.readdirSync(edgeFunctionsDir).filter(item =>
|
|
86
|
+
fs.statSync(path.join(edgeFunctionsDir, item)).isDirectory()
|
|
87
|
+
);
|
|
88
|
+
console.log(`⚡ Edge Functions: ${functions.length} function(s)`);
|
|
89
|
+
functions.forEach(func => console.log(` - ${func}`));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (components.authSettings) {
|
|
93
|
+
console.log('🔐 Auth Settings: Exibir URL e valores para configuração manual');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (components.storage) {
|
|
97
|
+
console.log('📦 Storage Buckets: Exibir informações e instruções do Google Colab');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (components.databaseSettings) {
|
|
101
|
+
console.log('🔧 Database Extensions and Settings: Restaurar via SQL');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (components.realtimeSettings) {
|
|
105
|
+
console.log('🔄 Realtime Settings: Exibir URL e valores para configuração manual');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log('');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Função auxiliar para copiar diretório recursivamente
|
|
113
|
+
*/
|
|
114
|
+
async function copyDirectoryRecursive(src, dest) {
|
|
115
|
+
const fs = require('fs').promises;
|
|
116
|
+
|
|
117
|
+
await fs.mkdir(dest, { recursive: true });
|
|
118
|
+
|
|
119
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
120
|
+
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
const srcPath = path.join(src, entry.name);
|
|
123
|
+
const destPath = path.join(dest, entry.name);
|
|
124
|
+
|
|
125
|
+
if (entry.isDirectory()) {
|
|
126
|
+
await copyDirectoryRecursive(srcPath, destPath);
|
|
127
|
+
} else {
|
|
128
|
+
await fs.copyFile(srcPath, destPath);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
listValidBackups,
|
|
135
|
+
formatBytes,
|
|
136
|
+
showRestoreSummary,
|
|
137
|
+
copyDirectoryRecursive
|
|
138
|
+
};
|
|
139
|
+
|
package/src/index.js
CHANGED
|
@@ -125,7 +125,7 @@ function checkPrerequisites() {
|
|
|
125
125
|
const { execSync } = require('child_process');
|
|
126
126
|
const npmVersion = execSync('npm --version', { encoding: 'utf8' }).trim();
|
|
127
127
|
prerequisites.npm = { installed: true, version: npmVersion };
|
|
128
|
-
} catch
|
|
128
|
+
} catch {
|
|
129
129
|
prerequisites.npm = { installed: false, version: null };
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -134,7 +134,7 @@ function checkPrerequisites() {
|
|
|
134
134
|
const { execSync } = require('child_process');
|
|
135
135
|
const supabaseVersion = execSync('supabase --version', { encoding: 'utf8' }).trim();
|
|
136
136
|
prerequisites.supabase_cli = { installed: true, version: supabaseVersion };
|
|
137
|
-
} catch
|
|
137
|
+
} catch {
|
|
138
138
|
prerequisites.supabase_cli = { installed: false, version: null };
|
|
139
139
|
}
|
|
140
140
|
|
|
@@ -143,7 +143,7 @@ function checkPrerequisites() {
|
|
|
143
143
|
const { execSync } = require('child_process');
|
|
144
144
|
const pgDumpVersion = execSync('pg_dump --version', { encoding: 'utf8' }).trim();
|
|
145
145
|
prerequisites.pg_dump = { installed: true, version: pgDumpVersion };
|
|
146
|
-
} catch
|
|
146
|
+
} catch {
|
|
147
147
|
prerequisites.pg_dump = { installed: false, version: null };
|
|
148
148
|
}
|
|
149
149
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const inquirer = require('inquirer');
|
|
2
2
|
const chalk = require('chalk');
|
|
3
|
+
const { confirm } = require('../utils/prompt');
|
|
3
4
|
|
|
4
5
|
async function mapEnvVariablesInteractively(env, expectedKeys) {
|
|
5
6
|
const finalEnv = { ...env };
|
|
@@ -47,13 +48,7 @@ async function mapEnvVariablesInteractively(env, expectedKeys) {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
const currentValue = finalEnv[clientKey] ?? '';
|
|
50
|
-
const
|
|
51
|
-
type: 'confirm',
|
|
52
|
-
name: 'isCorrect',
|
|
53
|
-
message: `Valor atual: ${currentValue || '(vazio)'} Este é o valor correto do projeto alvo? (S/n):`,
|
|
54
|
-
default: true,
|
|
55
|
-
prefix: ''
|
|
56
|
-
}]);
|
|
51
|
+
const isCorrect = await confirm(`Valor atual: ${currentValue || '(vazio)'}\nEste é o valor correto do projeto alvo?`, true);
|
|
57
52
|
|
|
58
53
|
let valueToWrite = currentValue;
|
|
59
54
|
if (!isCorrect) {
|
|
@@ -87,13 +82,42 @@ async function mapEnvVariablesInteractively(env, expectedKeys) {
|
|
|
87
82
|
}
|
|
88
83
|
|
|
89
84
|
async function askComponentsFlags() {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
85
|
+
// Explicação sobre Edge Functions
|
|
86
|
+
console.log(chalk.cyan('\n⚡ Edge Functions:'));
|
|
87
|
+
console.log(chalk.gray(' Vamos apagar as funções existentes na pasta supabase/functions, fazer um reset no link'));
|
|
88
|
+
console.log(chalk.gray(' entre a ferramenta e o projeto, e baixar novamente as funções do servidor.'));
|
|
89
|
+
console.log(chalk.gray(' Você terá a opção de manter ou apagar as funções na pasta após o backup.\n'));
|
|
90
|
+
|
|
91
|
+
const includeFunctions = await confirm('Deseja incluir Edge Functions', true);
|
|
92
|
+
|
|
93
|
+
// Explicação sobre .temp
|
|
94
|
+
console.log(chalk.cyan('\n📁 Supabase .temp:'));
|
|
95
|
+
console.log(chalk.gray(' Vamos copiar os arquivos existentes (se existirem) na pasta supabase/.temp.'));
|
|
96
|
+
console.log(chalk.gray(' Você terá a opção de manter ou apagar os arquivos nesta pasta após o backup.\n'));
|
|
97
|
+
|
|
98
|
+
// Explicação sobre Migrations
|
|
99
|
+
console.log(chalk.cyan('\n📋 Migrations:'));
|
|
100
|
+
console.log(chalk.gray(' Vamos apagar as migrations existentes (se existirem) na pasta supabase/migrations,'));
|
|
101
|
+
console.log(chalk.gray(' fazer um reset no link entre a ferramenta e o projeto, e baixar novamente as migrations'));
|
|
102
|
+
console.log(chalk.gray(' do servidor. Você terá a opção de manter ou apagar as migrations na pasta após o backup.\n'));
|
|
103
|
+
|
|
104
|
+
// Continuar com outras perguntas
|
|
105
|
+
const includeStorage = await confirm('Deseja incluir Storage', true);
|
|
106
|
+
const includeAuth = await confirm('Deseja incluir Auth', true);
|
|
107
|
+
const includeRealtime = await confirm('Deseja incluir Realtime', true);
|
|
108
|
+
const cleanFunctions = await confirm('Deseja limpar supabase/functions após o backup', false);
|
|
109
|
+
const cleanTemp = await confirm('Deseja apagar supabase/.temp após o backup', false);
|
|
110
|
+
const cleanMigrations = await confirm('Deseja apagar supabase/migrations após o backup', false);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
includeFunctions,
|
|
114
|
+
includeStorage,
|
|
115
|
+
includeAuth,
|
|
116
|
+
includeRealtime,
|
|
117
|
+
cleanFunctions,
|
|
118
|
+
cleanTemp,
|
|
119
|
+
cleanMigrations
|
|
120
|
+
};
|
|
97
121
|
}
|
|
98
122
|
|
|
99
123
|
module.exports = {
|
package/src/utils/cli.js
CHANGED
package/src/utils/config.js
CHANGED
|
@@ -13,16 +13,14 @@ async function readConfig() {
|
|
|
13
13
|
];
|
|
14
14
|
|
|
15
15
|
let configContent = null;
|
|
16
|
-
let configPath = null;
|
|
17
16
|
|
|
18
17
|
for (const configPathCandidate of configPaths) {
|
|
19
18
|
try {
|
|
20
19
|
if (fs.existsSync(configPathCandidate)) {
|
|
21
20
|
configContent = fs.readFileSync(configPathCandidate, 'utf8');
|
|
22
|
-
configPath = configPathCandidate;
|
|
23
21
|
break;
|
|
24
22
|
}
|
|
25
|
-
} catch
|
|
23
|
+
} catch {
|
|
26
24
|
// Continue para próximo caminho
|
|
27
25
|
}
|
|
28
26
|
}
|
package/src/utils/docker.js
CHANGED
|
@@ -12,7 +12,7 @@ async function detectDockerInstallation() {
|
|
|
12
12
|
// Tentar executar docker --version
|
|
13
13
|
await execAsync('docker --version');
|
|
14
14
|
return { installed: true, running: false }; // Instalado mas não sabemos se está rodando
|
|
15
|
-
} catch
|
|
15
|
+
} catch {
|
|
16
16
|
return { installed: false, running: false };
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -26,7 +26,7 @@ async function detectDockerRunning() {
|
|
|
26
26
|
// Tentar executar docker ps
|
|
27
27
|
await execAsync('docker ps');
|
|
28
28
|
return true;
|
|
29
|
-
} catch
|
|
29
|
+
} catch {
|
|
30
30
|
return false;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
@@ -40,7 +40,7 @@ async function detectSupabaseCLI() {
|
|
|
40
40
|
// Tentar executar supabase --version
|
|
41
41
|
await execAsync('supabase --version');
|
|
42
42
|
return true;
|
|
43
|
-
} catch
|
|
43
|
+
} catch {
|
|
44
44
|
return false;
|
|
45
45
|
}
|
|
46
46
|
}
|
package/src/utils/env.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
1
|
const fsp = require('fs').promises;
|
|
3
2
|
const path = require('path');
|
|
4
3
|
|
|
@@ -27,7 +26,7 @@ function stringifyEnv(entries, existingContent) {
|
|
|
27
26
|
const seen = new Set();
|
|
28
27
|
const resultLines = [];
|
|
29
28
|
|
|
30
|
-
for (
|
|
29
|
+
for (const line of existingLines) {
|
|
31
30
|
const trimmed = line.trim();
|
|
32
31
|
if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('=')) {
|
|
33
32
|
resultLines.push(line);
|
|
@@ -70,7 +69,7 @@ async function readEnvFile(filePath) {
|
|
|
70
69
|
}
|
|
71
70
|
}
|
|
72
71
|
|
|
73
|
-
async function writeEnvFile(filePath, entries,
|
|
72
|
+
async function writeEnvFile(filePath, entries, _options = {}) {
|
|
74
73
|
const dir = path.dirname(filePath);
|
|
75
74
|
await fsp.mkdir(dir, { recursive: true });
|
|
76
75
|
let existing = '';
|
package/src/utils/envMap.js
CHANGED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const fs = require('fs').promises;
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Copia um diretório de forma segura (retorna 0 se src não existir)
|
|
6
|
+
* @param {string} src - Diretório origem
|
|
7
|
+
* @param {string} dest - Diretório destino
|
|
8
|
+
* @returns {Promise<number>} - Quantidade de arquivos copiados (0 se src não existir)
|
|
9
|
+
*/
|
|
10
|
+
async function copyDirSafe(src, dest) {
|
|
11
|
+
try {
|
|
12
|
+
await fs.access(src);
|
|
13
|
+
} catch {
|
|
14
|
+
return 0; // Diretório não existe, retornar 0
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const stats = await fs.stat(src);
|
|
18
|
+
if (!stats.isDirectory()) {
|
|
19
|
+
return 0; // Não é diretório
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Criar diretório destino
|
|
23
|
+
await fs.mkdir(dest, { recursive: true });
|
|
24
|
+
|
|
25
|
+
// Contar e copiar arquivos
|
|
26
|
+
let count = 0;
|
|
27
|
+
const entries = await fs.readdir(src);
|
|
28
|
+
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
const srcPath = path.join(src, entry);
|
|
31
|
+
const destPath = path.join(dest, entry);
|
|
32
|
+
|
|
33
|
+
const entryStats = await fs.stat(srcPath);
|
|
34
|
+
if (entryStats.isDirectory()) {
|
|
35
|
+
count += await copyDirSafe(srcPath, destPath);
|
|
36
|
+
} else {
|
|
37
|
+
await fs.copyFile(srcPath, destPath);
|
|
38
|
+
count++;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return count;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Limpa um diretório completamente (rm -rf) e recria (mkdir -p)
|
|
47
|
+
* @param {string} dir - Diretório a limpar
|
|
48
|
+
* @returns {Promise<void>}
|
|
49
|
+
*/
|
|
50
|
+
async function cleanDir(dir) {
|
|
51
|
+
try {
|
|
52
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
53
|
+
} catch {
|
|
54
|
+
// Ignorar erro se não existir
|
|
55
|
+
}
|
|
56
|
+
await fs.mkdir(dir, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Conta arquivos em um diretório recursivamente
|
|
61
|
+
* @param {string} dir - Diretório a contar
|
|
62
|
+
* @returns {Promise<number>} - Número de arquivos (0 se não existir)
|
|
63
|
+
*/
|
|
64
|
+
async function countFiles(dir) {
|
|
65
|
+
try {
|
|
66
|
+
await fs.access(dir);
|
|
67
|
+
} catch {
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const stats = await fs.stat(dir);
|
|
72
|
+
if (!stats.isDirectory()) {
|
|
73
|
+
return 1; // É um arquivo
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let count = 0;
|
|
77
|
+
const entries = await fs.readdir(dir);
|
|
78
|
+
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
const entryPath = path.join(dir, entry);
|
|
81
|
+
const entryStats = await fs.stat(entryPath);
|
|
82
|
+
|
|
83
|
+
if (entryStats.isDirectory()) {
|
|
84
|
+
count += await countFiles(entryPath);
|
|
85
|
+
} else {
|
|
86
|
+
count++;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return count;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
copyDirSafe,
|
|
95
|
+
cleanDir,
|
|
96
|
+
countFiles
|
|
97
|
+
};
|
|
98
|
+
|
package/src/utils/fsx.js
CHANGED
|
@@ -14,13 +14,13 @@ async function copyDir(src, dest) {
|
|
|
14
14
|
await fs.promises.cp(src, dest, { recursive: true });
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
|
-
} catch
|
|
17
|
+
} catch {
|
|
18
18
|
// Fallback para fs-extra se disponível
|
|
19
19
|
try {
|
|
20
20
|
const fse = require('fs-extra');
|
|
21
21
|
await fse.copy(src, dest);
|
|
22
22
|
return;
|
|
23
|
-
} catch
|
|
23
|
+
} catch {
|
|
24
24
|
// Fallback manual usando fs nativo
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Função helper para fazer perguntas de confirmação customizadas
|
|
5
|
+
* Mostra (S/n) ou (s/N) em português em vez de (Y/n) ou (y/N)
|
|
6
|
+
*/
|
|
7
|
+
async function confirm(question, defaultYes = true) {
|
|
8
|
+
const rl = readline.createInterface({
|
|
9
|
+
input: process.stdin,
|
|
10
|
+
output: process.stdout
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const promptText = defaultYes ? '(S/n)' : '(s/N)';
|
|
14
|
+
const fullQuestion = `${question} ${promptText}: `;
|
|
15
|
+
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
rl.question(fullQuestion, (answer) => {
|
|
18
|
+
rl.close();
|
|
19
|
+
|
|
20
|
+
if (!answer || answer.trim() === '') {
|
|
21
|
+
resolve(defaultYes);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const normalized = answer.trim().toLowerCase();
|
|
26
|
+
resolve(normalized === 's' || normalized === 'sim' || normalized === 'y' || normalized === 'yes');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
confirm
|
|
33
|
+
};
|
|
34
|
+
|
|
@@ -70,14 +70,14 @@ async function getPreviousRealtimeSettings(backupDir) {
|
|
|
70
70
|
if (settings.realtime_settings && settings.realtime_settings.settings) {
|
|
71
71
|
return settings;
|
|
72
72
|
}
|
|
73
|
-
} catch
|
|
73
|
+
} catch {
|
|
74
74
|
// Continuar para próximo backup
|
|
75
75
|
continue;
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
return null;
|
|
80
|
-
} catch
|
|
80
|
+
} catch {
|
|
81
81
|
return null;
|
|
82
82
|
}
|
|
83
83
|
}
|
package/src/utils/supabase.js
CHANGED
|
@@ -43,7 +43,7 @@ function findPgDumpPath() {
|
|
|
43
43
|
} else if (fs.existsSync(pgPath)) {
|
|
44
44
|
return pgPath;
|
|
45
45
|
}
|
|
46
|
-
} catch
|
|
46
|
+
} catch {
|
|
47
47
|
// Continuar para o próximo caminho
|
|
48
48
|
continue;
|
|
49
49
|
}
|
|
@@ -244,10 +244,10 @@ async function testConnection() {
|
|
|
244
244
|
const client = getSupabaseClient();
|
|
245
245
|
|
|
246
246
|
// Tentar uma operação simples
|
|
247
|
-
const {
|
|
247
|
+
const { error: testError } = await client.from('_smoonb_test').select('*').limit(1);
|
|
248
248
|
|
|
249
|
-
if (
|
|
250
|
-
throw
|
|
249
|
+
if (testError && testError.code !== 'PGRST116') { // PGRST116 = tabela não existe (esperado)
|
|
250
|
+
throw testError;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
return { success: true, message: 'Conexão estabelecida com sucesso' };
|
|
@@ -261,7 +261,7 @@ async function testConnection() {
|
|
|
261
261
|
*/
|
|
262
262
|
async function getProjectInfo(projectId) {
|
|
263
263
|
try {
|
|
264
|
-
|
|
264
|
+
getSupabaseClient(); // Garantir que cliente está disponível
|
|
265
265
|
|
|
266
266
|
// TODO: Implementar busca real de informações do projeto
|
|
267
267
|
// Por enquanto, retornar informações básicas
|
|
@@ -292,7 +292,7 @@ async function listTables() {
|
|
|
292
292
|
}
|
|
293
293
|
|
|
294
294
|
return data || [];
|
|
295
|
-
} catch
|
|
295
|
+
} catch {
|
|
296
296
|
// Fallback: tentar query direta
|
|
297
297
|
try {
|
|
298
298
|
const { data, error } = await client
|
|
@@ -326,7 +326,7 @@ async function listExtensions() {
|
|
|
326
326
|
}
|
|
327
327
|
|
|
328
328
|
return data || [];
|
|
329
|
-
} catch
|
|
329
|
+
} catch {
|
|
330
330
|
// Fallback: tentar query direta
|
|
331
331
|
try {
|
|
332
332
|
const { data, error } = await client
|
|
@@ -349,7 +349,7 @@ async function listExtensions() {
|
|
|
349
349
|
*/
|
|
350
350
|
async function getAuthSettings() {
|
|
351
351
|
try {
|
|
352
|
-
|
|
352
|
+
getSupabaseClient(); // Garantir que cliente está disponível
|
|
353
353
|
|
|
354
354
|
// TODO: Implementar busca real de configurações de Auth
|
|
355
355
|
// Por enquanto, retornar estrutura básica
|
|
@@ -383,7 +383,7 @@ async function getStorageSettings() {
|
|
|
383
383
|
// Para cada bucket, listar objetos
|
|
384
384
|
const bucketsWithObjects = [];
|
|
385
385
|
for (const bucket of buckets) {
|
|
386
|
-
const { data: objects
|
|
386
|
+
const { data: objects } = await client.storage
|
|
387
387
|
.from(bucket.name)
|
|
388
388
|
.list();
|
|
389
389
|
|
|
@@ -408,7 +408,7 @@ async function getStorageSettings() {
|
|
|
408
408
|
*/
|
|
409
409
|
async function getRealtimeSettings() {
|
|
410
410
|
try {
|
|
411
|
-
|
|
411
|
+
getSupabaseClient(); // Garantir que cliente está disponível
|
|
412
412
|
|
|
413
413
|
// TODO: Implementar busca real de configurações de Realtime
|
|
414
414
|
// Por enquanto, retornar estrutura básica
|