smoonb 0.0.45 → 0.0.47
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/package.json +15 -2
- package/src/commands/backup.js +159 -67
- package/src/commands/restore.js +83 -7
- package/src/interactive/envMapper.js +104 -0
- package/src/utils/env.js +106 -0
- package/src/utils/envMap.js +25 -0
- package/src/utils/supabase.js +2 -2
- package/.smoonbrc +0 -29
- package/.smoonbrc.example +0 -28
- package/backups/backup-2025-10-17T19-18-58-539Z/auth-config.json +0 -7
- package/backups/backup-2025-10-17T19-18-58-539Z/backup-manifest.json +0 -19
- package/backups/backup-2025-10-17T19-18-58-539Z/functions/README.md +0 -4
- package/backups/backup-2025-10-17T19-18-58-539Z/realtime-config.json +0 -7
- package/backups/backup-2025-10-17T19-18-58-539Z/storage/storage-config.json +0 -6
- package/backups/backup-2025-10-17T19-52-20-211Z/auth-config.json +0 -7
- package/backups/backup-2025-10-17T19-52-20-211Z/backup-manifest.json +0 -19
- package/backups/backup-2025-10-17T19-52-20-211Z/database-2025-10-17T19-52-20-215Z.dump +0 -0
- package/backups/backup-2025-10-17T19-52-20-211Z/functions/README.md +0 -4
- package/backups/backup-2025-10-17T19-52-20-211Z/realtime-config.json +0 -7
- package/backups/backup-2025-10-17T19-52-20-211Z/storage/storage-config.json +0 -6
- package/backups/backup-2025-10-17T20-38-13-188Z/auth-config.json +0 -7
- package/backups/backup-2025-10-17T20-38-13-188Z/backup-manifest.json +0 -19
- package/backups/backup-2025-10-17T20-38-13-188Z/database-2025-10-17T20-38-13-194Z.dump +0 -0
- package/backups/backup-2025-10-17T20-38-13-188Z/functions/README.md +0 -4
- package/backups/backup-2025-10-17T20-38-13-188Z/realtime-config.json +0 -7
- 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.
|
|
3
|
+
"version": "0.0.47",
|
|
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
|
}
|
package/src/commands/backup.js
CHANGED
|
@@ -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
|
-
//
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
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
|
|
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: ${
|
|
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
|
-
|
|
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(
|
|
82
|
-
|
|
83
|
-
const outputDir = options.
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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');
|
package/src/commands/restore.js
CHANGED
|
@@ -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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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(
|
|
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) {
|
|
@@ -461,7 +535,8 @@ async function restoreEdgeFunctions(backupPath, targetProject) {
|
|
|
461
535
|
execSync(`supabase link --project-ref ${targetProject.targetProjectId}`, {
|
|
462
536
|
stdio: 'pipe',
|
|
463
537
|
encoding: 'utf8',
|
|
464
|
-
timeout: 10000
|
|
538
|
+
timeout: 10000,
|
|
539
|
+
env: { ...process.env, SUPABASE_ACCESS_TOKEN: targetProject.targetAccessToken || '' }
|
|
465
540
|
});
|
|
466
541
|
} catch (linkError) {
|
|
467
542
|
console.log(chalk.yellow(' ⚠️ Link pode já existir, continuando...'));
|
|
@@ -476,7 +551,8 @@ async function restoreEdgeFunctions(backupPath, targetProject) {
|
|
|
476
551
|
cwd: process.cwd(),
|
|
477
552
|
stdio: 'pipe',
|
|
478
553
|
encoding: 'utf8',
|
|
479
|
-
timeout: 120000
|
|
554
|
+
timeout: 120000,
|
|
555
|
+
env: { ...process.env, SUPABASE_ACCESS_TOKEN: targetProject.targetAccessToken || '' }
|
|
480
556
|
});
|
|
481
557
|
|
|
482
558
|
console.log(chalk.green(` ✅ ${funcName} deployada com sucesso!`));
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
let clientKey = undefined;
|
|
14
|
+
|
|
15
|
+
// 3) Se existir chave exatamente igual, pular seleção e ir direto para confirmação
|
|
16
|
+
if (Object.prototype.hasOwnProperty.call(finalEnv, expected)) {
|
|
17
|
+
clientKey = expected;
|
|
18
|
+
} else {
|
|
19
|
+
// 2) Remover o caractere '?' do início da pergunta definindo prefix: ''
|
|
20
|
+
// 4) Opção explícita para adicionar nova chave
|
|
21
|
+
const choices = [
|
|
22
|
+
...allKeys.map((k, idx) => ({ name: `${idx + 1}. ${k}`, value: k })),
|
|
23
|
+
new inquirer.Separator(),
|
|
24
|
+
{ name: 'Adicione uma nova chave para mim', value: '__ADD_NEW__' }
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const { chosen } = await inquirer.prompt([{
|
|
28
|
+
type: 'list',
|
|
29
|
+
name: 'chosen',
|
|
30
|
+
message: `Selecione a chave correspondente para: ${expected}`,
|
|
31
|
+
choices,
|
|
32
|
+
loop: false,
|
|
33
|
+
prefix: ''
|
|
34
|
+
}]);
|
|
35
|
+
|
|
36
|
+
clientKey = chosen;
|
|
37
|
+
if (chosen === '__ADD_NEW__') {
|
|
38
|
+
clientKey = expected;
|
|
39
|
+
if (Object.prototype.hasOwnProperty.call(finalEnv, clientKey)) {
|
|
40
|
+
// Evitar colisão: gerar sufixo incremental
|
|
41
|
+
let i = 2;
|
|
42
|
+
while (Object.prototype.hasOwnProperty.call(finalEnv, `${clientKey}_${i}`)) i++;
|
|
43
|
+
clientKey = `${clientKey}_${i}`;
|
|
44
|
+
}
|
|
45
|
+
finalEnv[clientKey] = '';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const currentValue = finalEnv[clientKey] ?? '';
|
|
50
|
+
const { isCorrect } = await inquirer.prompt([{
|
|
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
|
+
}]);
|
|
57
|
+
|
|
58
|
+
let valueToWrite = currentValue;
|
|
59
|
+
if (!isCorrect) {
|
|
60
|
+
const { newValue } = await inquirer.prompt([{
|
|
61
|
+
type: 'input',
|
|
62
|
+
name: 'newValue',
|
|
63
|
+
message: `Cole o novo valor para ${clientKey}:`,
|
|
64
|
+
prefix: ''
|
|
65
|
+
}]);
|
|
66
|
+
valueToWrite = newValue || '';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!valueToWrite) {
|
|
70
|
+
const { newValueRequired } = await inquirer.prompt([{
|
|
71
|
+
type: 'input',
|
|
72
|
+
name: 'newValueRequired',
|
|
73
|
+
message: `Valor obrigatório. Informe valor para ${clientKey}:`,
|
|
74
|
+
prefix: ''
|
|
75
|
+
}]);
|
|
76
|
+
valueToWrite = newValueRequired || '';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
finalEnv[clientKey] = valueToWrite;
|
|
80
|
+
if (dePara[clientKey] && dePara[clientKey] !== expected) {
|
|
81
|
+
throw new Error(`Duplicidade de mapeamento detectada para ${clientKey}`);
|
|
82
|
+
}
|
|
83
|
+
dePara[clientKey] = expected;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { finalEnv, dePara };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function askComponentsFlags() {
|
|
90
|
+
const answers = await inquirer.prompt([
|
|
91
|
+
{ type: 'confirm', name: 'includeFunctions', message: 'Deseja incluir Edge Functions (S/n):', default: true },
|
|
92
|
+
{ type: 'confirm', name: 'includeStorage', message: 'Deseja incluir Storage (s/N):', default: false },
|
|
93
|
+
{ type: 'confirm', name: 'includeAuth', message: 'Deseja incluir Auth (s/N):', default: false },
|
|
94
|
+
{ type: 'confirm', name: 'includeRealtime', message: 'Deseja incluir Realtime (s/N):', default: false }
|
|
95
|
+
]);
|
|
96
|
+
return answers;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
mapEnvVariablesInteractively,
|
|
101
|
+
askComponentsFlags
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
|
package/src/utils/env.js
ADDED
|
@@ -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
|
+
|
package/src/utils/supabase.js
CHANGED
|
@@ -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,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,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
|
-
}
|
|
File without changes
|
|
@@ -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
|
-
}
|
|
File without changes
|