smoonb 0.0.86 → 0.0.88

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 CHANGED
@@ -91,6 +91,8 @@ docker ps
91
91
  npm install -g supabase
92
92
  ```
93
93
 
94
+ We recommend **Supabase CLI v2.72 or newer** for new features and bug fixes. To update: [Updating the Supabase CLI](https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli).
95
+
94
96
  ### 3. Supabase Personal Access Token
95
97
  You need to obtain a Supabase personal access token to use the Management API:
96
98
 
package/README.pt-BR.md CHANGED
@@ -91,6 +91,8 @@ docker ps
91
91
  npm install -g supabase
92
92
  ```
93
93
 
94
+ Recomendamos **Supabase CLI v2.72 ou mais recente** para novos recursos e correções. Para atualizar: [Atualizando o Supabase CLI](https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli).
95
+
94
96
  ### 3. Personal Access Token do Supabase
95
97
  É necessário obter um token de acesso pessoal do Supabase para usar a Management API:
96
98
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoonb",
3
- "version": "0.0.86",
3
+ "version": "0.0.88",
4
4
  "description": "Complete Supabase backup and migration tool - EXPERIMENTAL VERSION - USE AT YOUR OWN RISK",
5
5
  "preferGlobal": false,
6
6
  "preventGlobalInstall": true,
@@ -34,7 +34,7 @@
34
34
  "node": ">=16.0.0"
35
35
  },
36
36
  "dependencies": {
37
- "@supabase/supabase-js": "^2.38.0",
37
+ "@supabase/supabase-js": "^2.93.0",
38
38
  "adm-zip": "^0.5.16",
39
39
  "chalk": "^4.1.2",
40
40
  "commander": "^11.1.0",
@@ -9,18 +9,19 @@ const { saveEnvMap } = require('../../utils/envMap');
9
9
  const { mapEnvVariablesInteractively, askComponentsFlags } = require('../../interactive/envMapper');
10
10
  const { confirm } = require('../../utils/prompt');
11
11
 
12
- // Importar todas as etapas
12
+ // Importar todas as etapas (arquivos 00..11)
13
13
  const step00DockerValidation = require('./steps/00-docker-validation');
14
- const step01Database = require('./steps/01-database');
15
- const step02DatabaseSeparated = require('./steps/02-database-separated');
16
- const step03DatabaseSettings = require('./steps/03-database-settings');
17
- const step04AuthSettings = require('./steps/04-auth-settings');
18
- const step05RealtimeSettings = require('./steps/05-realtime-settings');
19
- const step06Storage = require('./steps/06-storage');
20
- const step07CustomRoles = require('./steps/07-custom-roles');
21
- const step08EdgeFunctions = require('./steps/08-edge-functions');
22
- const step09SupabaseTemp = require('./steps/09-supabase-temp');
23
- const step10Migrations = require('./steps/10-migrations');
14
+ const step01PostgresVersion = require('./steps/01-postgres-version');
15
+ const step02Database = require('./steps/02-database');
16
+ const step03DatabaseSeparated = require('./steps/03-database-separated');
17
+ const step04DatabaseSettings = require('./steps/04-database-settings');
18
+ const step05AuthSettings = require('./steps/05-auth-settings');
19
+ const step06RealtimeSettings = require('./steps/06-realtime-settings');
20
+ const step07Storage = require('./steps/07-storage');
21
+ const step08CustomRoles = require('./steps/08-custom-roles');
22
+ const step09EdgeFunctions = require('./steps/09-edge-functions');
23
+ const step10SupabaseTemp = require('./steps/10-supabase-temp');
24
+ const step11Migrations = require('./steps/11-migrations');
24
25
 
25
26
  // Exportar FUNÇÃO em vez de objeto Command
26
27
  module.exports = async (options) => {
@@ -218,21 +219,26 @@ module.exports = async (options) => {
218
219
  console.log(chalk.white(`🐳 ${getT('backup.start.docker')}`));
219
220
 
220
221
  // Contar etapas totais para numeração
221
- // Etapas fixas: Database, Database Separado, Database Settings, Custom Roles (4)
222
+ // Etapas fixas: Postgres version, Database, Database Separado, Database Settings, Custom Roles (5)
222
223
  // Etapas condicionais: Auth, Realtime, Storage, Functions, Temp, Migrations
223
224
  let stepNumber = 0;
224
- const totalSteps = 4 + (flags?.includeAuth ? 1 : 0) + (flags?.includeRealtime ? 1 : 0) + (flags?.includeStorage ? 1 : 0) + (flags?.includeFunctions ? 1 : 0) + (flags?.includeTemp ? 1 : 0) + (flags?.includeMigrations ? 1 : 0);
225
+ const totalSteps = 5 + (flags?.includeAuth ? 1 : 0) + (flags?.includeRealtime ? 1 : 0) + (flags?.includeStorage ? 1 : 0) + (flags?.includeFunctions ? 1 : 0) + (flags?.includeTemp ? 1 : 0) + (flags?.includeMigrations ? 1 : 0);
225
226
 
226
- // 1. Backup Database via pg_dumpall Docker
227
+ // 1. Postgres version (detect + optional override)
228
+ stepNumber++;
229
+ console.log(chalk.blue(`\n📊 ${stepNumber}/${totalSteps} - ${getT('backup.steps.postgresVersion.title')}`));
230
+ await step01PostgresVersion(context);
231
+
232
+ // 2. Backup Database via pg_dumpall Docker
227
233
  stepNumber++;
228
234
  console.log(chalk.blue(`\n📊 ${stepNumber}/${totalSteps} - ${getT('backup.steps.database.title')}`));
229
- const databaseResult = await step01Database(context);
235
+ const databaseResult = await step02Database(context);
230
236
  manifest.components.database = databaseResult;
231
237
 
232
- // 2. Backup Database Separado
238
+ // 3. Backup Database Separado
233
239
  stepNumber++;
234
240
  console.log(chalk.blue(`\n📊 ${stepNumber}/${totalSteps} - ${getT('backup.steps.database.separated.title')}`));
235
- const dbSeparatedResult = await step02DatabaseSeparated(context);
241
+ const dbSeparatedResult = await step03DatabaseSeparated(context);
236
242
  manifest.components.database_separated = {
237
243
  success: dbSeparatedResult.success,
238
244
  method: 'supabase-cli',
@@ -240,63 +246,63 @@ module.exports = async (options) => {
240
246
  total_size_kb: dbSeparatedResult.totalSizeKB || '0.0'
241
247
  };
242
248
 
243
- // 3. Backup Database Settings
249
+ // 4. Backup Database Settings
244
250
  stepNumber++;
245
251
  console.log(chalk.blue(`\n🔧 ${stepNumber}/${totalSteps} - ${getT('backup.steps.databaseSettings.title')}`));
246
- const databaseSettingsResult = await step03DatabaseSettings(context);
252
+ const databaseSettingsResult = await step04DatabaseSettings(context);
247
253
  manifest.components.database_settings = databaseSettingsResult;
248
254
 
249
- // 4. Backup Auth Settings
255
+ // 5. Backup Auth Settings
250
256
  if (flags?.includeAuth) {
251
257
  stepNumber++;
252
258
  console.log(chalk.blue(`\n🔐 ${stepNumber}/${totalSteps} - ${getT('backup.steps.auth.title')}`));
253
- const authResult = await step04AuthSettings(context);
259
+ const authResult = await step05AuthSettings(context);
254
260
  manifest.components.auth_settings = authResult;
255
261
  }
256
262
 
257
- // 5. Backup Realtime Settings
263
+ // 6. Backup Realtime Settings
258
264
  if (flags?.includeRealtime) {
259
265
  stepNumber++;
260
266
  console.log(chalk.blue(`\n🔄 ${stepNumber}/${totalSteps} - ${getT('backup.steps.realtime.title')}`));
261
- const realtimeResult = await step05RealtimeSettings(context);
267
+ const realtimeResult = await step06RealtimeSettings(context);
262
268
  manifest.components.realtime = realtimeResult;
263
269
  }
264
270
 
265
- // 6. Backup Storage
271
+ // 7. Backup Storage
266
272
  if (flags?.includeStorage) {
267
273
  stepNumber++;
268
274
  console.log(chalk.blue(`\n📦 ${stepNumber}/${totalSteps} - ${getT('backup.steps.storage.title')}`));
269
- const storageResult = await step06Storage(context);
275
+ const storageResult = await step07Storage(context);
270
276
  manifest.components.storage = storageResult;
271
277
  }
272
278
 
273
- // 7. Backup Custom Roles
279
+ // 8. Backup Custom Roles
274
280
  stepNumber++;
275
281
  console.log(chalk.blue(`\n👥 ${stepNumber}/${totalSteps} - ${getT('backup.steps.roles.title')}`));
276
- const rolesResult = await step07CustomRoles(context);
282
+ const rolesResult = await step08CustomRoles(context);
277
283
  manifest.components.custom_roles = rolesResult;
278
284
 
279
- // 8. Backup Edge Functions
285
+ // 9. Backup Edge Functions
280
286
  if (flags?.includeFunctions) {
281
287
  stepNumber++;
282
288
  console.log(chalk.blue(`\n⚡ ${stepNumber}/${totalSteps} - ${getT('backup.steps.functions.title')}`));
283
- const functionsResult = await step08EdgeFunctions(context);
289
+ const functionsResult = await step09EdgeFunctions(context);
284
290
  manifest.components.edge_functions = functionsResult;
285
291
  }
286
292
 
287
- // 9. Backup Supabase .temp
293
+ // 10. Backup Supabase .temp
288
294
  if (flags?.includeTemp) {
289
295
  stepNumber++;
290
296
  console.log(chalk.blue(`\n📁 ${stepNumber}/${totalSteps} - ${getT('backup.steps.temp.title')}`));
291
- const supabaseTempResult = await step09SupabaseTemp(context);
297
+ const supabaseTempResult = await step10SupabaseTemp(context);
292
298
  manifest.components.supabase_temp = supabaseTempResult;
293
299
  }
294
300
 
295
- // 10. Backup Migrations
301
+ // 11. Backup Migrations
296
302
  if (flags?.includeMigrations) {
297
303
  stepNumber++;
298
304
  console.log(chalk.blue(`\n📋 ${stepNumber}/${totalSteps} - ${getT('backup.steps.migrations.title')}`));
299
- const migrationsResult = await step10Migrations(context);
305
+ const migrationsResult = await step11Migrations(context);
300
306
  manifest.components.migrations = migrationsResult;
301
307
  }
302
308
 
@@ -15,7 +15,7 @@ module.exports = async () => {
15
15
  const backupCapability = await canPerformCompleteBackup();
16
16
 
17
17
  if (!backupCapability.canBackupComplete) {
18
- showDockerMessagesAndExit(backupCapability.reason);
18
+ showDockerMessagesAndExit(backupCapability.reason, backupCapability);
19
19
  }
20
20
 
21
21
  console.log(chalk.green(`✅ ${getT('docker.validation.detected')}`));
@@ -0,0 +1,54 @@
1
+ const chalk = require('chalk');
2
+ const inquirer = require('inquirer');
3
+ const { getPostgresServerMajor } = require('../utils');
4
+ const { t } = require('../../../i18n');
5
+
6
+ const POSSIBLE_MAJORS = [15, 16, 17, 18];
7
+
8
+ /**
9
+ * Step: Mostra a versão do Postgres detectada e permite ao usuário sobrescrever com um menu.
10
+ * Preenche context.postgresMajor para o step 02-database usar.
11
+ */
12
+ module.exports = async (context) => {
13
+ const getT = global.smoonbI18n?.t || t;
14
+ const { databaseUrl } = context;
15
+
16
+ if (!databaseUrl) {
17
+ context.postgresMajor = 17;
18
+ return { success: true, postgresMajor: 17 };
19
+ }
20
+
21
+ let detectedMajor = null;
22
+ try {
23
+ detectedMajor = await getPostgresServerMajor(databaseUrl);
24
+ } catch (err) {
25
+ console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.postgresVersion.detectError', { message: err.message })}`));
26
+ console.log(chalk.white(` - ${getT('backup.steps.postgresVersion.usingDefault')}`));
27
+ context.postgresMajor = 17;
28
+ return { success: true, postgresMajor: 17 };
29
+ }
30
+
31
+ const major = detectedMajor != null ? detectedMajor : 17;
32
+ const choices = [
33
+ { name: getT('backup.steps.postgresVersion.useDetected', { major, image: `postgres:${major}` }), value: major },
34
+ ...POSSIBLE_MAJORS.filter(m => m !== major).map(m => ({
35
+ name: `postgres:${m}`,
36
+ value: m
37
+ }))
38
+ ];
39
+
40
+ console.log(chalk.white(` - ${getT('backup.steps.postgresVersion.found', { major, image: `postgres:${major}` })}`));
41
+ console.log(chalk.white(` - ${getT('backup.steps.postgresVersion.proceedWith')}`));
42
+
43
+ const { postgresMajor } = await inquirer.prompt([{
44
+ type: 'list',
45
+ name: 'postgresMajor',
46
+ message: getT('backup.steps.postgresVersion.selectVersion'),
47
+ choices,
48
+ default: major
49
+ }]);
50
+
51
+ context.postgresMajor = postgresMajor;
52
+ console.log(chalk.green(` ✅ ${getT('backup.steps.postgresVersion.selected', { image: `postgres:${postgresMajor}` })}`));
53
+ return { success: true, postgresMajor };
54
+ };
@@ -3,6 +3,7 @@ const path = require('path');
3
3
  const fs = require('fs').promises;
4
4
  const { spawn } = require('child_process');
5
5
  const { t } = require('../../../i18n');
6
+ const { getPostgresServerMajor } = require('../utils');
6
7
 
7
8
  function formatBytes(bytes) {
8
9
  if (bytes === 0) return '0 B';
@@ -32,10 +33,11 @@ async function exists(filePath) {
32
33
  }
33
34
 
34
35
  /**
35
- * Etapa 1: Backup Database via pg_dumpall Docker (idêntico ao Dashboard)
36
+ * Etapa 2: Backup Database via pg_dumpall Docker (idêntico ao Dashboard)
36
37
  * Com feedback de progresso: tamanho do arquivo, velocidade e tempo decorrido.
38
+ * Usa context.postgresMajor (definido no step Postgres version) ou detecta se não definido.
37
39
  */
38
- module.exports = async ({ databaseUrl, backupDir }) => {
40
+ module.exports = async ({ databaseUrl, backupDir, postgresMajor: contextMajor }) => {
39
41
  try {
40
42
  const getT = global.smoonbI18n?.t || t;
41
43
  console.log(chalk.white(` - ${getT('backup.steps.database.creating')}`));
@@ -49,6 +51,18 @@ module.exports = async ({ databaseUrl, backupDir }) => {
49
51
 
50
52
  const [, username, password, host, port] = urlMatch;
51
53
 
54
+ let postgresMajor = contextMajor;
55
+ if (postgresMajor == null && databaseUrl) {
56
+ try {
57
+ postgresMajor = await getPostgresServerMajor(databaseUrl);
58
+ } catch {
59
+ postgresMajor = 17;
60
+ }
61
+ }
62
+ if (postgresMajor == null) postgresMajor = 17;
63
+ const postgresImage = `postgres:${postgresMajor}`;
64
+ console.log(chalk.white(` - ${getT('backup.steps.database.postgresImage', { image: postgresImage, major: postgresMajor })}`));
65
+
52
66
  const now = new Date();
53
67
  const day = String(now.getDate()).padStart(2, '0');
54
68
  const month = String(now.getMonth() + 1).padStart(2, '0');
@@ -65,7 +79,7 @@ module.exports = async ({ databaseUrl, backupDir }) => {
65
79
  'run', '--rm', '--network', 'host',
66
80
  '-v', `${backupDirAbs}:/host`,
67
81
  '-e', `PGPASSWORD=${password}`,
68
- 'postgres:17', 'pg_dumpall',
82
+ postgresImage, 'pg_dumpall',
69
83
  '-h', host,
70
84
  '-p', port,
71
85
  '-U', username,
@@ -124,7 +138,7 @@ module.exports = async ({ databaseUrl, backupDir }) => {
124
138
  const gzipArgs = [
125
139
  'run', '--rm',
126
140
  '-v', `${backupDirAbs}:/host`,
127
- 'postgres:17', 'gzip', `/host/${fileName}`
141
+ postgresImage, 'gzip', `/host/${fileName}`
128
142
  ];
129
143
 
130
144
  const gzipStart = Date.now();
@@ -1,10 +1,49 @@
1
1
  const chalk = require('chalk');
2
+ const { spawn } = require('child_process');
2
3
  const { t } = require('../../i18n');
3
4
 
5
+ /**
6
+ * Obtém a versão major do Postgres no servidor (ex: 17) via psql em container.
7
+ * @param {string} databaseUrl
8
+ * @returns {Promise<number|null>} major (15, 17, 18, ...) ou null
9
+ */
10
+ async function getPostgresServerMajor(databaseUrl) {
11
+ const urlMatch = databaseUrl.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
12
+ if (!urlMatch) return null;
13
+ const [, username, password, host, port, database] = urlMatch;
14
+ const bootstrapImage = 'postgres:17';
15
+ const args = [
16
+ 'run', '--rm', '--network', 'host',
17
+ '-e', `PGPASSWORD=${password}`,
18
+ bootstrapImage, 'psql',
19
+ '-h', host, '-p', port, '-U', username, '-d', (database && database.trim()) || 'postgres',
20
+ '-t', '-A', '-c', "SELECT current_setting('server_version_num')"
21
+ ];
22
+ const stdout = await new Promise((resolve, reject) => {
23
+ const proc = spawn('docker', args, { stdio: ['ignore', 'pipe', 'pipe'] });
24
+ let out = '';
25
+ let err = '';
26
+ proc.stdout.on('data', (chunk) => { out += chunk.toString(); });
27
+ proc.stderr.on('data', (chunk) => { err += chunk.toString(); });
28
+ proc.on('close', (code) => {
29
+ if (code !== 0) reject(new Error(err.trim() || `psql exited with code ${code}`));
30
+ else resolve(out.trim());
31
+ });
32
+ proc.on('error', reject);
33
+ });
34
+ const versionNum = parseInt(stdout, 10);
35
+ if (Number.isNaN(versionNum) || versionNum < 10000) {
36
+ throw new Error(`Invalid server_version_num: ${stdout}`);
37
+ }
38
+ return Math.floor(versionNum / 10000);
39
+ }
40
+
4
41
  /**
5
42
  * Função para mostrar mensagens educativas e encerrar elegantemente
43
+ * @param {string} reason - Motivo do bloqueio
44
+ * @param {object} [data] - Dados extras (ex: { supabaseCliVersion } para supabase_cli_outdated)
6
45
  */
7
- function showDockerMessagesAndExit(reason) {
46
+ function showDockerMessagesAndExit(reason, data = {}) {
8
47
  const getT = global.smoonbI18n?.t || t;
9
48
 
10
49
  console.log('');
@@ -47,6 +86,26 @@ function showDockerMessagesAndExit(reason) {
47
86
  console.log('');
48
87
  console.log(chalk.gray(`💡 ${getT('supabase.requiredComponents')}`));
49
88
  break;
89
+
90
+ case 'supabase_cli_outdated':
91
+ console.log(chalk.red(`❌ ${getT('supabase.cliOutdated', { version: data.supabaseCliVersion || '?', latest: data.supabaseCliLatest || '?' })}`));
92
+ console.log('');
93
+ console.log(chalk.yellow(`📋 ${getT('supabase.cliUpdateInstructions')}`));
94
+ console.log(chalk.cyan(` ${getT('supabase.cliUpdateCommandExamples')}`));
95
+ console.log(chalk.cyan(` ${getT('supabase.cliUpdateCommandGlobal')}`));
96
+ console.log(chalk.cyan(` ${getT('supabase.cliUpdateCommandLocal')}`));
97
+ console.log('');
98
+ console.log(chalk.gray(`💡 ${getT('supabase.cliUpdateLink')}`));
99
+ break;
100
+
101
+ case 'supabase_cli_latest_unknown':
102
+ console.log(chalk.red(`❌ ${getT('supabase.cliLatestUnknown')}`));
103
+ console.log('');
104
+ console.log(chalk.yellow(`📋 ${getT('supabase.cliLatestErrorLabel')}`));
105
+ console.log(chalk.gray(` ${data.latestError || getT('supabase.cliLatestErrorUnknown')}`));
106
+ console.log('');
107
+ console.log(chalk.gray(`💡 ${getT('supabase.cliUpdateLink')}`));
108
+ break;
50
109
  }
51
110
 
52
111
  console.log('');
@@ -58,6 +117,7 @@ function showDockerMessagesAndExit(reason) {
58
117
  }
59
118
 
60
119
  module.exports = {
61
- showDockerMessagesAndExit
120
+ showDockerMessagesAndExit,
121
+ getPostgresServerMajor
62
122
  };
63
123
 
@@ -72,6 +72,16 @@
72
72
  "supabase.installLink": "Installation: npm install -g supabase",
73
73
  "supabase.required": "Supabase CLI is required for full Supabase backup",
74
74
  "supabase.requiredComponents": "Supabase CLI is required for full Supabase backup\n - PostgreSQL Database\n - Edge Functions\n - All components via Docker",
75
+ "supabase.cliUpdateRecommended": "Supabase CLI {version} detected. We recommend v2.72 or newer for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli",
76
+ "supabase.cliOutdated": "Supabase CLI {version} is not the latest ({latest}). Update to the latest version.",
77
+ "supabase.cliUpdateInstructions": "Update the Supabase CLI and run smoonb again:",
78
+ "supabase.cliUpdateCommandExamples": "Examples (depending on how you installed):",
79
+ "supabase.cliUpdateCommandGlobal": "npm install -g supabase@latest (global)",
80
+ "supabase.cliUpdateCommandLocal": "npm install supabase@latest (local/project)",
81
+ "supabase.cliUpdateLink": "Docs: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli",
82
+ "supabase.cliLatestUnknown": "Could not fetch the latest Supabase CLI version. Cannot continue.",
83
+ "supabase.cliLatestErrorLabel": "Error:",
84
+ "supabase.cliLatestErrorUnknown": "Unknown error",
75
85
 
76
86
  "backup.components": "Backup Components:",
77
87
  "backup.database": "Database PostgreSQL (pg_dumpall + separate SQL)",
@@ -311,8 +321,18 @@
311
321
  "backup.start.directory": "Directory: {path}",
312
322
  "backup.start.docker": "Backup via Docker Desktop",
313
323
 
324
+ "backup.steps.postgresVersion.title": "Postgres version",
325
+ "backup.steps.postgresVersion.found": "Postgres version found: major {major} (image {image}).",
326
+ "backup.steps.postgresVersion.proceedWith": "We will proceed with this version. You can override below.",
327
+ "backup.steps.postgresVersion.selectVersion": "Select Postgres image version to use for dump:",
328
+ "backup.steps.postgresVersion.useDetected": "Use detected (postgres:{major})",
329
+ "backup.steps.postgresVersion.selected": "Using image: {image}",
330
+ "backup.steps.postgresVersion.detectError": "Could not detect server version: {message}",
331
+ "backup.steps.postgresVersion.usingDefault": "Using default postgres:17",
332
+
314
333
  "backup.steps.database.title": "PostgreSQL Database Backup via pg_dumpall Docker...",
315
334
  "backup.steps.database.creating": "Creating full backup via pg_dumpall...",
335
+ "backup.steps.database.postgresImage": "Postgres server major {major} detected. Using image: {image}",
316
336
  "backup.steps.database.executing": "Executing pg_dumpall via Docker...",
317
337
  "backup.steps.database.separated.title": "PostgreSQL Database Backup (separate SQL files)...",
318
338
  "backup.steps.database.separated.creating": "Creating separate SQL backups via Supabase CLI...",
@@ -72,6 +72,16 @@
72
72
  "supabase.installLink": "Instalação: npm install -g supabase",
73
73
  "supabase.required": "Supabase CLI é obrigatório para backup completo do Supabase",
74
74
  "supabase.requiredComponents": "Supabase CLI é obrigatório para backup completo do Supabase\n - Database PostgreSQL\n - Edge Functions\n - Todos os componentes via Docker",
75
+ "supabase.cliUpdateRecommended": "Supabase CLI {version} detectado. Recomendamos v2.72 ou mais recente para novos recursos e correções: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli",
76
+ "supabase.cliOutdated": "Supabase CLI {version} não é a versão mais recente ({latest}). Atualize para a última versão.",
77
+ "supabase.cliUpdateInstructions": "Atualize o Supabase CLI e execute o smoonb novamente:",
78
+ "supabase.cliUpdateCommandExamples": "Exemplos (conforme a forma de instalação):",
79
+ "supabase.cliUpdateCommandGlobal": "npm install -g supabase@latest (global)",
80
+ "supabase.cliUpdateCommandLocal": "npm install supabase@latest (local/projeto)",
81
+ "supabase.cliUpdateLink": "Documentação: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli",
82
+ "supabase.cliLatestUnknown": "Não foi possível obter a última versão do Supabase CLI. Não é possível continuar.",
83
+ "supabase.cliLatestErrorLabel": "Erro:",
84
+ "supabase.cliLatestErrorUnknown": "Erro desconhecido",
75
85
 
76
86
  "backup.components": "Componentes de Backup:",
77
87
  "backup.database": "Database PostgreSQL (pg_dumpall + SQL separados)",
@@ -311,8 +321,18 @@
311
321
  "backup.start.directory": "Diretório: {path}",
312
322
  "backup.start.docker": "Backup via Docker Desktop",
313
323
 
324
+ "backup.steps.postgresVersion.title": "Versão do Postgres",
325
+ "backup.steps.postgresVersion.found": "Versão do Postgres encontrada: major {major} (imagem {image}).",
326
+ "backup.steps.postgresVersion.proceedWith": "Seguiremos com esta versão. Você pode sobrescrever abaixo.",
327
+ "backup.steps.postgresVersion.selectVersion": "Selecione a versão da imagem Postgres para o dump:",
328
+ "backup.steps.postgresVersion.useDetected": "Usar detectada (postgres:{major})",
329
+ "backup.steps.postgresVersion.selected": "Usando imagem: {image}",
330
+ "backup.steps.postgresVersion.detectError": "Não foi possível detectar a versão do servidor: {message}",
331
+ "backup.steps.postgresVersion.usingDefault": "Usando padrão postgres:17",
332
+
314
333
  "backup.steps.database.title": "Backup da Database PostgreSQL via pg_dumpall Docker...",
315
334
  "backup.steps.database.creating": "Criando backup completo via pg_dumpall...",
335
+ "backup.steps.database.postgresImage": "Postgres servidor major {major} detectado. Usando imagem: {image}",
316
336
  "backup.steps.database.executing": "Executando pg_dumpall via Docker...",
317
337
  "backup.steps.database.separated.title": "Backup da Database PostgreSQL (arquivos SQL separados)...",
318
338
  "backup.steps.database.separated.creating": "Criando backups SQL separados via Supabase CLI...",
@@ -37,7 +37,6 @@ async function detectDockerRunning() {
37
37
  */
38
38
  async function detectSupabaseCLI() {
39
39
  try {
40
- // Tentar executar supabase --version
41
40
  await execAsync('supabase --version');
42
41
  return true;
43
42
  } catch {
@@ -45,6 +44,62 @@ async function detectSupabaseCLI() {
45
44
  }
46
45
  }
47
46
 
47
+ /**
48
+ * Obtém a versão instalada do Supabase CLI (ex: "2.51.0").
49
+ * @returns {Promise<string|null>} Semver ou null se não disponível
50
+ */
51
+ async function getSupabaseCLIVersion() {
52
+ try {
53
+ const { stdout } = await execAsync('supabase --version');
54
+ const match = stdout.match(/(\d+)\.(\d+)\.(\d+)/);
55
+ return match ? `${match[1]}.${match[2]}.${match[3]}` : null;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Obtém a versão latest do Supabase CLI no npm.
63
+ * @returns {Promise<{version: string|null, error: string|null}>} version ou error preenchido
64
+ */
65
+ async function getSupabaseCLILatestVersion() {
66
+ try {
67
+ const res = await fetch('https://registry.npmjs.org/supabase/latest', {
68
+ headers: { Accept: 'application/json' }
69
+ });
70
+ if (!res.ok) {
71
+ return { version: null, error: `HTTP ${res.status} ${res.statusText}` };
72
+ }
73
+ const data = await res.json();
74
+ const version = data.version || null;
75
+ if (!version) {
76
+ return { version: null, error: 'Resposta do registro npm sem campo version' };
77
+ }
78
+ return { version, error: null };
79
+ } catch (err) {
80
+ const message = err && typeof err.message === 'string' ? err.message : String(err);
81
+ return { version: null, error: message };
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Compara duas versões semver (ex: "2.51.0", "2.72.7").
87
+ * @param {string} a
88
+ * @param {string} b
89
+ * @returns {number} -1 se a < b, 0 se a === b, 1 se a > b
90
+ */
91
+ function compareSemver(a, b) {
92
+ const pa = a.split('.').map(Number);
93
+ const pb = b.split('.').map(Number);
94
+ for (let i = 0; i < 3; i++) {
95
+ const va = pa[i] || 0;
96
+ const vb = pb[i] || 0;
97
+ if (va < vb) return -1;
98
+ if (va > vb) return 1;
99
+ }
100
+ return 0;
101
+ }
102
+
48
103
  /**
49
104
  * Detecta Docker Desktop completo com versão
50
105
  * @returns {Promise<{installed: boolean, running: boolean, version: string}>}
@@ -133,6 +188,27 @@ async function canPerformCompleteBackup() {
133
188
  dockerStatus
134
189
  };
135
190
  }
191
+
192
+ const supabaseCliVersion = await getSupabaseCLIVersion();
193
+ const latestResult = await getSupabaseCLILatestVersion();
194
+ if (latestResult.error) {
195
+ return {
196
+ canBackupComplete: false,
197
+ reason: 'supabase_cli_latest_unknown',
198
+ latestError: latestResult.error,
199
+ supabaseCliVersion: supabaseCliVersion || null,
200
+ dockerStatus
201
+ };
202
+ }
203
+ if (supabaseCliVersion && latestResult.version && compareSemver(supabaseCliVersion, latestResult.version) < 0) {
204
+ return {
205
+ canBackupComplete: false,
206
+ reason: 'supabase_cli_outdated',
207
+ supabaseCliVersion,
208
+ supabaseCliLatest: latestResult.version,
209
+ dockerStatus
210
+ };
211
+ }
136
212
 
137
213
  return {
138
214
  canBackupComplete: true,
@@ -147,5 +223,8 @@ module.exports = {
147
223
  detectDockerDependencies,
148
224
  detectDockerDesktop,
149
225
  getDockerVersion,
226
+ getSupabaseCLIVersion,
227
+ getSupabaseCLILatestVersion,
228
+ compareSemver,
150
229
  canPerformCompleteBackup
151
230
  };