smoonb 1.0.11 → 1.0.13
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/CHANGELOG.md +167 -150
- package/README.md +695 -687
- package/README.pt-BR.md +697 -687
- package/npm-shrinkwrap.json +2030 -0
- package/package.json +71 -68
- package/src/commands/backup/index.js +39 -21
- package/src/commands/backup/outcome.js +16 -0
- package/src/commands/backup/steps/00-license.js +427 -427
- package/src/commands/backup/steps/02-database.js +6 -6
- package/src/commands/backup/steps/03-database-separated.js +129 -161
- package/src/commands/backup/steps/04-database-settings.js +24 -40
- package/src/commands/backup/steps/07-storage.js +11 -10
- package/src/commands/backup/steps/08-custom-roles.js +5 -55
- package/src/commands/backup/steps/10-supabase-temp.js +0 -9
- package/src/commands/backup/steps/11-migrations.js +30 -74
- package/src/i18n/locales/en.json +825 -824
- package/src/i18n/locales/pt-BR.json +824 -823
- package/src/interactive/envMapper.js +6 -12
- package/src/utils/docker.js +6 -53
- package/src/utils/pg-dump-diagnostics.js +68 -64
- package/src/utils/supabaseCli.js +26 -32
package/package.json
CHANGED
|
@@ -1,68 +1,71 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "smoonb",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Complete Supabase backup and migration tool. https://www.smoonb.com/#price",
|
|
5
|
-
"preferGlobal": false,
|
|
6
|
-
"preventGlobalInstall": true,
|
|
7
|
-
"main": "index.js",
|
|
8
|
-
"bin": {
|
|
9
|
-
"smoonb": "bin/smoonb.js"
|
|
10
|
-
},
|
|
11
|
-
"scripts": {
|
|
12
|
-
"test": "node --test tests/*.test.js",
|
|
13
|
-
"start": "node bin/smoonb.js",
|
|
14
|
-
"preinstall": "node -e \"if(process.env.npm_config_global) { console.error('\\n❌ SMOONB NÃO DEVE SER INSTALADO GLOBALMENTE!\\n\\n📋 Para usar o smoonb, instale localmente no seu projeto:\\n npm install smoonb\\n\\n💡 Depois execute com:\\n npx smoonb backup\\n\\n🚫 Instalação global cancelada!\\n'); process.exit(1); }\"",
|
|
15
|
-
"postinstall": "echo '\\n✅ smoonb instalado com sucesso!\\n💡 Execute: npx smoonb backup\\n📖 Documentação: https://github.com/almmello/smoonb\\n'",
|
|
16
|
-
"lint": "eslint . --ext .js",
|
|
17
|
-
"lint:fix": "eslint . --ext .js --fix",
|
|
18
|
-
"build": "node scripts/build.js",
|
|
19
|
-
"build:check": "node scripts/build.js",
|
|
20
|
-
"prepublishOnly": "npm test && npm run lint && npm run build",
|
|
21
|
-
"test:docker": "node tests/integration/circular-restore.js",
|
|
22
|
-
"test:restore:docker": "node tests/integration/verified-restore.js"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "smoonb",
|
|
3
|
+
"version": "1.0.13",
|
|
4
|
+
"description": "Complete Supabase backup and migration tool. https://www.smoonb.com/#price",
|
|
5
|
+
"preferGlobal": false,
|
|
6
|
+
"preventGlobalInstall": true,
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"smoonb": "bin/smoonb.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "node --test tests/*.test.js",
|
|
13
|
+
"start": "node bin/smoonb.js",
|
|
14
|
+
"preinstall": "node -e \"if(process.env.npm_config_global) { console.error('\\n❌ SMOONB NÃO DEVE SER INSTALADO GLOBALMENTE!\\n\\n📋 Para usar o smoonb, instale localmente no seu projeto:\\n npm install smoonb\\n\\n💡 Depois execute com:\\n npx smoonb backup\\n\\n🚫 Instalação global cancelada!\\n'); process.exit(1); }\"",
|
|
15
|
+
"postinstall": "echo '\\n✅ smoonb instalado com sucesso!\\n💡 Execute: npx smoonb backup\\n📖 Documentação: https://github.com/almmello/smoonb\\n'",
|
|
16
|
+
"lint": "eslint . --ext .js",
|
|
17
|
+
"lint:fix": "eslint . --ext .js --fix",
|
|
18
|
+
"build": "node scripts/build.js",
|
|
19
|
+
"build:check": "node scripts/build.js",
|
|
20
|
+
"prepublishOnly": "npm test && npm run lint && npm run build",
|
|
21
|
+
"test:docker": "node tests/integration/circular-restore.js",
|
|
22
|
+
"test:restore:docker": "node tests/integration/verified-restore.js",
|
|
23
|
+
"test:backup:docker": "node tests/integration/backup-isolation.js"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"supabase",
|
|
27
|
+
"backup",
|
|
28
|
+
"migration",
|
|
29
|
+
"postgresql",
|
|
30
|
+
"edge-functions",
|
|
31
|
+
"database",
|
|
32
|
+
"cli",
|
|
33
|
+
"tool"
|
|
34
|
+
],
|
|
35
|
+
"author": "Goalmoon Tecnologia LTDA <https://www.smoonb.com>",
|
|
36
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": "^22.0.0 || ^24.0.0 || ^26.0.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@supabase/supabase-js": "^2.93.0",
|
|
42
|
+
"adm-zip": "0.6.1",
|
|
43
|
+
"chalk": "^4.1.2",
|
|
44
|
+
"commander": "^11.1.0",
|
|
45
|
+
"env-paths": "^4.0.0",
|
|
46
|
+
"inquirer": "^8.2.7",
|
|
47
|
+
"supabase": "2.122.1"
|
|
48
|
+
},
|
|
49
|
+
"type": "commonjs",
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"eslint": "^9.38.0"
|
|
52
|
+
},
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "git+https://github.com/almmello/smoonb.git"
|
|
56
|
+
},
|
|
57
|
+
"bugs": {
|
|
58
|
+
"url": "https://github.com/almmello/smoonb/issues"
|
|
59
|
+
},
|
|
60
|
+
"homepage": "https://www.smoonb.com",
|
|
61
|
+
"files": [
|
|
62
|
+
"bin/",
|
|
63
|
+
"src/",
|
|
64
|
+
"README.md",
|
|
65
|
+
"README.pt-BR.md",
|
|
66
|
+
"LICENSE",
|
|
67
|
+
"LICENSE.pt-BR.md",
|
|
68
|
+
"CHANGELOG.md",
|
|
69
|
+
"npm-shrinkwrap.json"
|
|
70
|
+
]
|
|
71
|
+
}
|
|
@@ -4,7 +4,7 @@ const fs = require('fs').promises;
|
|
|
4
4
|
const { ensureDir, writeJson } = require('../../utils/fsx');
|
|
5
5
|
const { showBetaBanner } = require('../../utils/banner');
|
|
6
6
|
const { getDockerVersion } = require('../../utils/docker');
|
|
7
|
-
const { readEnvFile,
|
|
7
|
+
const { readEnvFile, backupEnvFile } = require('../../utils/env');
|
|
8
8
|
const { saveEnvMap } = require('../../utils/envMap');
|
|
9
9
|
const { mapEnvVariablesInteractively, askComponentsFlags, printSupabasePostgresMajorHintOnce } = require('../../interactive/envMapper');
|
|
10
10
|
const { confirm } = require('../../utils/prompt');
|
|
@@ -25,6 +25,11 @@ const step10SupabaseTemp = require('./steps/10-supabase-temp');
|
|
|
25
25
|
const step11Migrations = require('./steps/11-migrations');
|
|
26
26
|
const { sendTelemetry } = require('../../telemetry');
|
|
27
27
|
const ui = require('../../utils/cliUi');
|
|
28
|
+
const { backupOutcome } = require('./outcome');
|
|
29
|
+
|
|
30
|
+
async function captureStep(step, context) {
|
|
31
|
+
try { return await step(context); } catch { return { success: false, reason: 'component_failed' }; }
|
|
32
|
+
}
|
|
28
33
|
|
|
29
34
|
// Exportar FUNÇÃO em vez de objeto Command
|
|
30
35
|
module.exports = async (options) => {
|
|
@@ -106,9 +111,9 @@ module.exports = async (options) => {
|
|
|
106
111
|
];
|
|
107
112
|
const currentEnv = await readEnvFile(envPath);
|
|
108
113
|
const { finalEnv, dePara } = await mapEnvVariablesInteractively(currentEnv, expectedKeys);
|
|
109
|
-
|
|
114
|
+
// Mapping applies to this run only.
|
|
110
115
|
await saveEnvMap(dePara, path.join(backupDir, 'env', 'env-map.json'));
|
|
111
|
-
console.log(chalk.green(`✅ ${getT('
|
|
116
|
+
console.log(chalk.green(`✅ ${getT('backup.environmentPreserved')}`));
|
|
112
117
|
|
|
113
118
|
function getValue(expectedKey) {
|
|
114
119
|
const clientKey = Object.keys(dePara).find(k => dePara[k] === expectedKey);
|
|
@@ -258,17 +263,17 @@ module.exports = async (options) => {
|
|
|
258
263
|
// 1. Backup Database via pg_dumpall Docker
|
|
259
264
|
stepNumber++;
|
|
260
265
|
console.log(chalk.blue(`\n📊 ${stepNumber}/${totalSteps} - ${getT('backup.steps.database.title')}`));
|
|
261
|
-
const databaseResult = await step02Database
|
|
266
|
+
const databaseResult = await captureStep(step02Database, context);
|
|
262
267
|
manifest.components.database = databaseResult;
|
|
263
268
|
|
|
264
269
|
// 3. Backup Database Separado
|
|
265
270
|
stepNumber++;
|
|
266
271
|
console.log(chalk.blue(`\n📊 ${stepNumber}/${totalSteps} - ${getT('backup.steps.database.separated.title')}`));
|
|
267
|
-
const dbSeparatedResult = await step03DatabaseSeparated
|
|
272
|
+
const dbSeparatedResult = await captureStep(step03DatabaseSeparated, context);
|
|
268
273
|
manifest.components.database_separated = {
|
|
269
274
|
success: dbSeparatedResult.success,
|
|
270
|
-
method: 'supabase-cli',
|
|
271
|
-
warnings: dbSeparatedResult.warnings || [],
|
|
275
|
+
method: 'supabase-cli',
|
|
276
|
+
warnings: dbSeparatedResult.warnings || [],
|
|
272
277
|
failed_files: dbSeparatedResult.failedFiles || [],
|
|
273
278
|
files: dbSeparatedResult.files || [],
|
|
274
279
|
total_size_kb: dbSeparatedResult.totalSizeKB || '0.0'
|
|
@@ -277,14 +282,14 @@ module.exports = async (options) => {
|
|
|
277
282
|
// 4. Backup Database Settings
|
|
278
283
|
stepNumber++;
|
|
279
284
|
console.log(chalk.blue(`\n🔧 ${stepNumber}/${totalSteps} - ${getT('backup.steps.databaseSettings.title')}`));
|
|
280
|
-
const databaseSettingsResult = await step04DatabaseSettings
|
|
285
|
+
const databaseSettingsResult = await captureStep(step04DatabaseSettings, context);
|
|
281
286
|
manifest.components.database_settings = databaseSettingsResult;
|
|
282
287
|
|
|
283
288
|
// 5. Backup Auth Settings
|
|
284
289
|
if (flags?.includeAuth) {
|
|
285
290
|
stepNumber++;
|
|
286
291
|
console.log(chalk.blue(`\n🔐 ${stepNumber}/${totalSteps} - ${getT('backup.steps.auth.title')}`));
|
|
287
|
-
const authResult = await step05AuthSettings
|
|
292
|
+
const authResult = await captureStep(step05AuthSettings, context);
|
|
288
293
|
manifest.components.auth_settings = authResult;
|
|
289
294
|
}
|
|
290
295
|
|
|
@@ -292,7 +297,7 @@ module.exports = async (options) => {
|
|
|
292
297
|
if (flags?.includeRealtime) {
|
|
293
298
|
stepNumber++;
|
|
294
299
|
console.log(chalk.blue(`\n🔄 ${stepNumber}/${totalSteps} - ${getT('backup.steps.realtime.title')}`));
|
|
295
|
-
const realtimeResult = await step06RealtimeSettings
|
|
300
|
+
const realtimeResult = await captureStep(step06RealtimeSettings, context);
|
|
296
301
|
manifest.components.realtime = realtimeResult;
|
|
297
302
|
}
|
|
298
303
|
|
|
@@ -300,21 +305,21 @@ module.exports = async (options) => {
|
|
|
300
305
|
if (flags?.includeStorage) {
|
|
301
306
|
stepNumber++;
|
|
302
307
|
console.log(chalk.blue(`\n📦 ${stepNumber}/${totalSteps} - ${getT('backup.steps.storage.title')}`));
|
|
303
|
-
const storageResult = await step07Storage
|
|
308
|
+
const storageResult = await captureStep(step07Storage, context);
|
|
304
309
|
manifest.components.storage = storageResult;
|
|
305
310
|
}
|
|
306
311
|
|
|
307
312
|
// 8. Backup Custom Roles
|
|
308
313
|
stepNumber++;
|
|
309
314
|
console.log(chalk.blue(`\n👥 ${stepNumber}/${totalSteps} - ${getT('backup.steps.roles.title')}`));
|
|
310
|
-
const rolesResult = await step08CustomRoles
|
|
315
|
+
const rolesResult = await captureStep(step08CustomRoles, context);
|
|
311
316
|
manifest.components.custom_roles = rolesResult;
|
|
312
317
|
|
|
313
318
|
// 9. Backup Edge Functions
|
|
314
319
|
if (flags?.includeFunctions) {
|
|
315
320
|
stepNumber++;
|
|
316
321
|
console.log(chalk.blue(`\n⚡ ${stepNumber}/${totalSteps} - ${getT('backup.steps.functions.title')}`));
|
|
317
|
-
const functionsResult = await step09EdgeFunctions
|
|
322
|
+
const functionsResult = await captureStep(step09EdgeFunctions, context);
|
|
318
323
|
manifest.components.edge_functions = functionsResult;
|
|
319
324
|
}
|
|
320
325
|
|
|
@@ -322,7 +327,7 @@ module.exports = async (options) => {
|
|
|
322
327
|
if (flags?.includeTemp) {
|
|
323
328
|
stepNumber++;
|
|
324
329
|
console.log(chalk.blue(`\n📁 ${stepNumber}/${totalSteps} - ${getT('backup.steps.temp.title')}`));
|
|
325
|
-
const supabaseTempResult = await step10SupabaseTemp
|
|
330
|
+
const supabaseTempResult = await captureStep(step10SupabaseTemp, context);
|
|
326
331
|
manifest.components.supabase_temp = supabaseTempResult;
|
|
327
332
|
}
|
|
328
333
|
|
|
@@ -330,21 +335,25 @@ module.exports = async (options) => {
|
|
|
330
335
|
if (flags?.includeMigrations) {
|
|
331
336
|
stepNumber++;
|
|
332
337
|
console.log(chalk.blue(`\n📋 ${stepNumber}/${totalSteps} - ${getT('backup.steps.migrations.title')}`));
|
|
333
|
-
const migrationsResult = await step11Migrations
|
|
338
|
+
const migrationsResult = await captureStep(step11Migrations, context);
|
|
334
339
|
manifest.components.migrations = migrationsResult;
|
|
335
340
|
}
|
|
336
341
|
|
|
342
|
+
const outcome = backupOutcome(manifest.components);
|
|
343
|
+
Object.assign(manifest, outcome);
|
|
344
|
+
if (!outcome.success) process.exitCode = 1;
|
|
337
345
|
// Salvar manifest
|
|
338
346
|
await writeJson(path.join(finalBackupDir, 'backup-manifest.json'), manifest);
|
|
339
347
|
|
|
348
|
+
if (outcome.success) {
|
|
340
349
|
// Exibir resumo final (na ordem de captura)
|
|
341
350
|
console.log(chalk.green(`\n🎉 ${getT('backup.complete.title')}`));
|
|
342
351
|
console.log(chalk.blue(`📁 ${getT('backup.complete.location', { path: finalBackupDir })}`));
|
|
343
352
|
console.log(chalk.green(`📊 ${getT('backup.complete.database', { fileName: databaseResult.fileName, size: `${databaseResult.size} KB` })}`));
|
|
344
|
-
if (dbSeparatedResult.success) {
|
|
345
|
-
console.log(chalk.green(`📊 ${getT('backup.complete.databaseSql', { count: dbSeparatedResult.files?.length || 0, size: `${dbSeparatedResult.totalSizeKB} KB` })}`));
|
|
346
|
-
} else {
|
|
347
|
-
console.log(chalk.yellow(`⚠️ ${getT('backup.steps.database.separated.incomplete', { files: (dbSeparatedResult.failedFiles || []).join(', ') })}`));
|
|
353
|
+
if (dbSeparatedResult.success) {
|
|
354
|
+
console.log(chalk.green(`📊 ${getT('backup.complete.databaseSql', { count: dbSeparatedResult.files?.length || 0, size: `${dbSeparatedResult.totalSizeKB} KB` })}`));
|
|
355
|
+
} else {
|
|
356
|
+
console.log(chalk.yellow(`⚠️ ${getT('backup.steps.database.separated.incomplete', { files: (dbSeparatedResult.failedFiles || []).join(', ') })}`));
|
|
348
357
|
}
|
|
349
358
|
console.log(chalk.green(`🔧 ${getT('backup.complete.databaseSettings', { fileName: databaseSettingsResult.fileName, size: `${databaseSettingsResult.size} KB` })}`));
|
|
350
359
|
|
|
@@ -391,8 +400,17 @@ module.exports = async (options) => {
|
|
|
391
400
|
console.log(chalk.green(`📋 ${getT('backup.complete.migrations', { count: migrationsResult.file_count || 0 })}`));
|
|
392
401
|
}
|
|
393
402
|
|
|
403
|
+
} else {
|
|
404
|
+
console.error('BACKUP INCOMPLETO: ' + outcome.failed_components.join(', '));
|
|
405
|
+
console.log('Arquivos preservados em: ' + finalBackupDir);
|
|
406
|
+
for (const line of require('./outcome').componentSummary(manifest.components)) console.log(line);
|
|
407
|
+
console.log('Consulte backup-manifest.json para os detalhes e refaça o backup após resolver as etapas com falha.');
|
|
408
|
+
}
|
|
409
|
+
|
|
394
410
|
// report.json
|
|
395
411
|
await writeJson(path.join(finalBackupDir, 'report.json'), {
|
|
412
|
+
...outcome,
|
|
413
|
+
results: manifest.components,
|
|
396
414
|
process: 'backup',
|
|
397
415
|
created_at: manifest.created_at,
|
|
398
416
|
project_id: manifest.project_id,
|
|
@@ -415,9 +433,9 @@ module.exports = async (options) => {
|
|
|
415
433
|
enabled: context.telemetryEnabled,
|
|
416
434
|
command: 'backup',
|
|
417
435
|
durationMs: Date.now() - backupStartTime,
|
|
418
|
-
success:
|
|
436
|
+
success: outcome.success
|
|
419
437
|
});
|
|
420
|
-
return {
|
|
438
|
+
return { ...outcome, backupDir: finalBackupDir, manifest };
|
|
421
439
|
|
|
422
440
|
} catch (error) {
|
|
423
441
|
sendTelemetry({
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
function backupOutcome(components) {
|
|
2
|
+
const failed_components = Object.entries(components).filter(([, value]) => value?.success !== true).map(([name]) => name);
|
|
3
|
+
return { success: failed_components.length === 0, status: failed_components.length ? 'incomplete' : 'completed', failed_components };
|
|
4
|
+
}
|
|
5
|
+
function storageComplete(buckets, expected) {
|
|
6
|
+
return buckets.length === expected && buckets.every(b => !b.listingFailed && b.filesSkipped === 0 && b.filesDownloaded === b.totalFiles && b.objectCount === b.totalFiles);
|
|
7
|
+
}
|
|
8
|
+
function componentSummary(components) {
|
|
9
|
+
const labels = { database: 'Banco de dados', database_separated: 'SQL separados', database_settings: 'Configurações do banco', auth_settings: 'Auth', realtime: 'Realtime', storage: 'Storage', custom_roles: 'Roles', edge_functions: 'Edge Functions', supabase_temp: 'Arquivos locais Supabase', migrations: 'Migrations' };
|
|
10
|
+
const reasons = { migration_history_check_failed: 'não foi possível consultar o histórico; confira conexão e permissões', migration_export_failed: 'não foi possível exportar o histórico', history_absent: 'nenhum histórico registrado', history_empty: 'histórico vazio' };
|
|
11
|
+
return Object.entries(components).map(([name, result]) => {
|
|
12
|
+
const count = result?.file_count ?? result?.functions_count ?? result?.totalFiles;
|
|
13
|
+
return `${result?.success === true ? 'OK' : 'FALHA'} - ${labels[name] || name}${count === undefined ? '' : ': ' + count}${result?.reason && result.reason !== 'success' ? ' (' + (reasons[result.reason] || result.reason) + ')' : ''}`;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
module.exports = { backupOutcome, storageComplete, componentSummary };
|