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/package.json CHANGED
@@ -1,68 +1,71 @@
1
- {
2
- "name": "smoonb",
3
- "version": "1.0.11",
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
- "keywords": [
25
- "supabase",
26
- "backup",
27
- "migration",
28
- "postgresql",
29
- "edge-functions",
30
- "database",
31
- "cli",
32
- "tool"
33
- ],
34
- "author": "Goalmoon Tecnologia LTDA <https://www.smoonb.com>",
35
- "license": "SEE LICENSE IN LICENSE.md",
36
- "engines": {
37
- "node": ">=20.0.0"
38
- },
39
- "dependencies": {
40
- "@supabase/supabase-js": "^2.93.0",
41
- "adm-zip": "^0.5.16",
42
- "chalk": "^4.1.2",
43
- "commander": "^11.1.0",
44
- "env-paths": "^4.0.0",
45
- "inquirer": "^8.2.7"
46
- },
47
- "type": "commonjs",
48
- "devDependencies": {
49
- "eslint": "^9.38.0"
50
- },
51
- "repository": {
52
- "type": "git",
53
- "url": "git+https://github.com/almmello/smoonb.git"
54
- },
55
- "bugs": {
56
- "url": "https://github.com/almmello/smoonb/issues"
57
- },
58
- "homepage": "https://www.smoonb.com",
59
- "files": [
60
- "bin/",
61
- "src/",
62
- "README.md",
63
- "README.pt-BR.md",
64
- "LICENSE",
65
- "LICENSE.pt-BR.md",
66
- "CHANGELOG.md"
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, writeEnvFile, backupEnvFile } = require('../../utils/env');
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
- await writeEnvFile(envPath, finalEnv);
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('env.mapping.updated')}`));
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(context);
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(context);
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(context);
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(context);
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(context);
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(context);
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(context);
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(context);
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(context);
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(context);
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: true
436
+ success: outcome.success
419
437
  });
420
- return { success: true, backupDir: finalBackupDir, manifest };
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 };