smoonb 1.0.4 → 1.0.6

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 CHANGED
@@ -4,84 +4,66 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
- ## [1.0.4] - 2025-01-29
7
+ ## [1.0.6] - 2026-02-21
8
8
 
9
9
  ### Changed
10
10
 
11
- - Release 1.0.4 Build and lint verification.
11
+ - **Storage backup (step 6) — robustness and feedback:**
12
+ - Added `withTimeout` helper: each Storage `.list()` call times out after 30s; each file `.download()` times out after 2 minutes.
13
+ - Added `withRetry` helper: up to 3 attempts with exponential backoff (2s → 4s → 8s) on both listing and download failures.
14
+ - Inline progress during file listing: live `\r`-updated line shows the current folder being scanned and the running file count (`→ Scanning bucket/folder (N found)`). No more silent hangs.
15
+ - After listing completes, a summary line shows the total files found.
16
+ - Retry warnings show attempt number, failure reason and next retry delay.
17
+ - Download failures after all retries are counted as `filesSkipped` and reported in the bucket summary.
18
+ - Bucket summary now distinguishes between full success and partial success with skips.
12
19
 
13
20
  ---
14
21
 
15
- ## [1.0.3] - 2025-01-29
22
+ ## [1.0.5] - 2026-02-21
16
23
 
17
24
  ### Added
18
25
 
19
- - **License binding by installation** – One license is bound to a single installation (machine). On first use, the license is linked to a persistent `installationId` and cannot be moved. To use smoonb on another machine, revoke the license in the desktop app and generate a new one.
20
- - **Installation ID** – Generated and stored on first run via `env-paths` (Windows: `%APPDATA%/smoonb-nodejs/Config`, macOS/Linux: `~/.config/smoonb-nodejs`). File: `config.json` with `installationId` (uuid v4) and `createdAt`. Never stores the license key.
21
- - **Validation payload** – License validation now sends `installationId` and `cliVersion` with `licenseKey` and `correlationId`.
26
+ - **Flexible Supabase CLI version check** – Instead of requiring the exact latest version, smoonb now accepts any CLI version that is at most 1 minor behind the latest (same major). The minimum accepted version is computed as `major.(minor_latest - 1).0` at runtime from the npm registry. Special case: if `minor_latest === 0`, only the same major.minor is accepted.
27
+ - **`--skip-supabase-version-check` flag** – New opt-in flag for both `backup` and `restore` commands. When set, the Supabase CLI version check is skipped entirely and a warning is displayed. Useful when package managers (e.g. Scoop on Windows) lag behind on updates and the user wants to proceed at their own risk.
22
28
 
23
29
  ### Changed
24
30
 
25
- - **LICENSE_ALREADY_BOUND** When the server returns this reason, the CLI shows a clear message: "This license is already linked to another machine and cannot be moved" and instructs to revoke and generate a new license at https://www.smoonb.com.
26
- - **Diagnostic bundle** Includes masked `InstallationId` (e.g. `2f3a…91bc`) and server `Reason` (or reason code) on validation failure; HTTP status and response body (truncated) on network/HTTP errors.
27
- - **Telemetry** Uses the same `installationId` as license validation (shared util); removed duplicate persistence in `~/.smoonb/installation-id`.
28
-
29
- ### Fixed
30
-
31
- - No offline cache: if validation fails (network or backend), the CLI does not run and shows the full diagnostic bundle.
31
+ - Error message for outdated Supabase CLI now shows the computed minimum accepted version (`minVersion`) and the current latest, instead of only the latest.
32
+ - README (EN and PT-BR): updated Supabase CLI prerequisites section with the new version policy, added examples with `--skip-supabase-version-check`, updated commands table and troubleshooting section.
33
+ - i18n strings (`en.json`, `pt-BR.json`): removed specific version number references in favour of evergreen language ("always use the latest version").
32
34
 
33
35
  ---
34
36
 
35
- ## [1.0.2] - 2025-01-29
36
-
37
- ### Fixed
38
-
39
- - **Backup report** – When Realtime settings are reused from a previous backup file, the final summary now correctly shows "Settings imported from backup {name}" (or the pt-BR equivalent) instead of "Configurations captured interactively"
40
-
41
- ---
42
-
43
- ## [1.0.1] - 2025-01-29
44
-
45
- ### Removed
46
-
47
- - **`check` command** – Post-restore integrity verification command removed from CLI, help and documentation
48
- - **`--skip-realtime`** – Backup option removed; Realtime settings are always captured interactively
49
- - **`--file` and `--storage` on restore** – Restore no longer accepts direct file paths; use `import` then `restore` to select from the list
50
- - **`--lang`** – CLI language option removed; language is determined by `SMOONB_LANG` (env or `.env.local`) and system locale only
37
+ ## [1.0.4] - 2025-01-29
51
38
 
52
39
  ### Changed
53
40
 
54
- - README and README.pt-BR: all `#` comment lines removed from code blocks (replaced with normal text) to avoid light-gray rendering issues
55
- - Restore flow: backup selection is always interactive (no skip via `--file`)
56
- - i18n: `detectLocale` no longer reads `--lang` from argv; precedence is SMOONB_LANG → system locale → en
41
+ - Release 1.0.4 Build and lint verification.
57
42
 
58
43
  ---
59
44
 
60
- ## [1.0.0] - 2025-01-29
45
+ ## [1.0.3] - 2025-01-29
61
46
 
62
47
  ### Added
63
48
 
64
- - Commercial release. Use requires an active license and valid subscription (or trial). See https://www.smoonb.com/#price
65
- - License validation (step 00) at the start of backup and restore; CLI aborts if validation fails
66
- - Restore disclaimer before execution: expected errors during DB restore, link to Supabase docs, wait for completion and test result
67
- - Environment variables: `SMOONB_LICENSE_KEY` (required), `SMOONB_TELEMETRY_ENABLED` (optional), `SUPABASE_POSTGRES_MAJOR` (required for backup and restore)
68
- - i18n (en, pt-BR) for all user-facing messages and help
49
+ - **License binding by installation** One license is bound to a single installation (machine). On first use, the license is linked to a persistent `installationId` and cannot be moved. To use smoonb on another machine, revoke the license in the desktop app and generate a new one.
50
+ - **Installation ID** – Generated and stored on first run via `env-paths` (Windows: `%APPDATA%/smoonb-nodejs/Config`, macOS/Linux: `~/.config/smoonb-nodejs`). File: `config.json` with `installationId` (uuid v4) and `createdAt`. Never stores the license key.
51
+ - **Validation payload** License validation now sends `installationId` and `cliVersion` with `licenseKey` and `correlationId`.
69
52
 
70
53
  ### Changed
71
54
 
72
- - Backup flow: terms license validation Docker consent variable mapping (license not in wizard) component selection summary execution
73
- - Restore flow: terms license validation Docker consent mapping (includes `SUPABASE_POSTGRES_MAJOR`) backup selection component selection summary disclaimer execution
74
- - Help and README aligned with commercial product; legal disclaimers unchanged
75
- - Documentation and links point to https://www.smoonb.com (terms, privacy, #price, #faq)
55
+ - **LICENSE_ALREADY_BOUND** When the server returns this reason, the CLI shows a clear message: "This license is already linked to another machine and cannot be moved" and instructs to revoke and generate a new license at https://www.smoonb.com.
56
+ - **Diagnostic bundle** Includes masked `InstallationId` (e.g. `2f3a…91bc`) and server `Reason` (or reason code) on validation failure; HTTP status and response body (truncated) on network/HTTP errors.
57
+ - **Telemetry** Uses the same `installationId` as license validation (shared util); removed duplicate persistence in `~/.smoonb/installation-id`.
76
58
 
77
59
  ### Fixed
78
60
 
79
- - `instructions` used before definition in interactive env mapper; blocks moved after `getVariableInstructions()`
61
+ - No offline cache: if validation fails (network or backend), the CLI does not run and shows the full diagnostic bundle.
62
+
63
+ ---
80
64
 
81
65
  ---
82
66
 
67
+ [1.0.6]: https://github.com/almmello/smoonb/releases/tag/v1.0.6
68
+ [1.0.5]: https://github.com/almmello/smoonb/releases/tag/v1.0.5
83
69
  [1.0.4]: https://github.com/almmello/smoonb/releases/tag/v1.0.4
84
- [1.0.3]: https://github.com/almmello/smoonb/releases/tag/v1.0.3
85
- [1.0.2]: https://github.com/almmello/smoonb/releases/tag/v1.0.2
86
- [1.0.1]: https://github.com/almmello/smoonb/releases/tag/v1.0.1
87
- [1.0.0]: https://github.com/almmello/smoonb/releases/tag/v1.0.0
package/README.md CHANGED
@@ -92,7 +92,11 @@ docker ps
92
92
  npm install -g supabase
93
93
  ```
94
94
 
95
- 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
+ **Version policy:** smoonb accepts any Supabase CLI version that is **at most 1 minor behind the latest** (same major). We recommend always using the latest version. Versions that are too old are blocked with an error message.
96
+
97
+ If your package manager (e.g. **Scoop** on Windows) lags behind on updates and you want to proceed anyway, use the `--skip-supabase-version-check` flag (see usage section below).
98
+
99
+ To update the Supabase CLI: [Updating the Supabase CLI](https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli).
96
100
 
97
101
  ### 3. Supabase Personal Access Token
98
102
  You need to obtain a Supabase personal access token to use the Management API:
@@ -190,6 +194,17 @@ Language is detected automatically in the following order of precedence:
190
194
  npx smoonb backup
191
195
  ```
192
196
 
197
+ **Available options:**
198
+
199
+ | Flag | Description |
200
+ |------|-------------|
201
+ | `--skip-supabase-version-check` | Skips the Supabase CLI version check. Use when your package manager (e.g. Scoop) lags behind on updates. A warning is shown and the user assumes the risk. |
202
+
203
+ **Example with flag:**
204
+ ```bash
205
+ npx smoonb backup --skip-supabase-version-check
206
+ ```
207
+
193
208
  **Interactive backup flow:**
194
209
 
195
210
  1. **Terms of use** - Displays and requests acceptance of terms
@@ -252,6 +267,17 @@ backups/backup-2025-10-31-09-37-54/
252
267
  npx smoonb restore
253
268
  ```
254
269
 
270
+ **Available options:**
271
+
272
+ | Flag | Description |
273
+ |------|-------------|
274
+ | `--skip-supabase-version-check` | Skips the Supabase CLI version check. Use when your package manager (e.g. Scoop) lags behind on updates. A warning is shown and the user assumes the risk. |
275
+
276
+ **Example with flag:**
277
+ ```bash
278
+ npx smoonb restore --skip-supabase-version-check
279
+ ```
280
+
255
281
  **Interactive restore flow:**
256
282
 
257
283
  1. **Terms of use** - Displays and requests acceptance of terms
@@ -345,7 +371,9 @@ After running `import`, run `restore` to choose the imported backup from the lis
345
371
  | Command | Description |
346
372
  |---------|-------------|
347
373
  | `npx smoonb backup` | Full interactive backup using Docker |
374
+ | `npx smoonb backup --skip-supabase-version-check` | Backup skipping the Supabase CLI version check |
348
375
  | `npx smoonb restore` | Interactive restoration using psql (Docker) |
376
+ | `npx smoonb restore --skip-supabase-version-check` | Restore skipping the Supabase CLI version check |
349
377
  | `npx smoonb import --file <path> [--storage <path>]` | Import .backup.gz file and optionally .storage.zip from Supabase Dashboard |
350
378
 
351
379
  ## 🏗️ Technical Architecture
@@ -519,6 +547,28 @@ If not, start Docker Desktop (Windows/macOS) or run `sudo systemctl start docker
519
547
  npm install -g supabase
520
548
  ```
521
549
 
550
+ ### Supabase CLI outdated (below minimum accepted version)
551
+
552
+ smoonb accepts versions up to **1 minor behind the latest**. If your installed version is below that threshold, you will see a message like:
553
+
554
+ ```
555
+ ❌ Supabase CLI X.Y.Z is below the minimum accepted version (X.W.0). Latest: X.V.Z. Update to at least version X.W.0.
556
+ ```
557
+
558
+ **Option 1 — Update the Supabase CLI:**
559
+ ```bash
560
+ npm install -g supabase@latest # global install
561
+ npm install supabase@latest # local/project install
562
+ ```
563
+
564
+ **Option 2 — Skip the check (temporary, user assumes the risk):**
565
+ ```bash
566
+ npx smoonb backup --skip-supabase-version-check
567
+ npx smoonb restore --skip-supabase-version-check
568
+ ```
569
+
570
+ > **Note for Scoop users (Windows):** the Scoop repository may take a few days to update after a release. Use `--skip-supabase-version-check` until Scoop ships the updated version.
571
+
522
572
  ### Invalid or missing Personal Access Token
523
573
 
524
574
  1. Verify if `SUPABASE_ACCESS_TOKEN` is in `.env.local`
package/README.pt-BR.md CHANGED
@@ -92,7 +92,11 @@ docker ps
92
92
  npm install -g supabase
93
93
  ```
94
94
 
95
- 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
+ **Política de versão:** o smoonb aceita qualquer versão do Supabase CLI que esteja **no máximo 1 minor atrás da latest** (mesma major). Recomendamos sempre usar a versão mais recente. Versões muito antigas são bloqueadas com mensagem de erro.
96
+
97
+ Se o seu gerenciador de pacotes (ex.: **Scoop** no Windows) instala com atraso e você quer avançar mesmo assim, use a flag `--skip-supabase-version-check` (veja a seção de uso abaixo).
98
+
99
+ Para atualizar o Supabase CLI: [Atualizando o Supabase CLI](https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli).
96
100
 
97
101
  ### 3. Personal Access Token do Supabase
98
102
  É necessário obter um token de acesso pessoal do Supabase para usar a Management API:
@@ -190,6 +194,17 @@ O idioma é detectado automaticamente na seguinte ordem de precedência:
190
194
  npx smoonb backup
191
195
  ```
192
196
 
197
+ **Opções disponíveis:**
198
+
199
+ | Flag | Descrição |
200
+ |------|-----------|
201
+ | `--skip-supabase-version-check` | Pula a checagem de versão do Supabase CLI. Use quando seu gerenciador de pacotes (ex.: Scoop) instala com atraso. Um aviso é exibido e o usuário assume o risco. |
202
+
203
+ **Exemplo com a flag:**
204
+ ```bash
205
+ npx smoonb backup --skip-supabase-version-check
206
+ ```
207
+
193
208
  **Fluxo interativo do backup:**
194
209
 
195
210
  1. **Termo de uso** - Exibe e solicita aceitação dos termos
@@ -252,6 +267,17 @@ backups/backup-2025-10-31-09-37-54/
252
267
  npx smoonb restore
253
268
  ```
254
269
 
270
+ **Opções disponíveis:**
271
+
272
+ | Flag | Descrição |
273
+ |------|-----------|
274
+ | `--skip-supabase-version-check` | Pula a checagem de versão do Supabase CLI. Use quando seu gerenciador de pacotes (ex.: Scoop) instala com atraso. Um aviso é exibido e o usuário assume o risco. |
275
+
276
+ **Exemplo com a flag:**
277
+ ```bash
278
+ npx smoonb restore --skip-supabase-version-check
279
+ ```
280
+
255
281
  **Fluxo interativo do restore:**
256
282
 
257
283
  1. **Termo de uso** - Exibe e solicita aceitação dos termos
@@ -345,7 +371,9 @@ Após executar `import`, execute `restore` para escolher o backup importado na l
345
371
  | Comando | Descrição |
346
372
  |---------|-----------|
347
373
  | `npx smoonb backup` | Backup completo interativo usando Docker |
374
+ | `npx smoonb backup --skip-supabase-version-check` | Backup pulando a checagem de versão do Supabase CLI |
348
375
  | `npx smoonb restore` | Restauração interativa usando psql (Docker) |
376
+ | `npx smoonb restore --skip-supabase-version-check` | Restore pulando a checagem de versão do Supabase CLI |
349
377
  | `npx smoonb import --file <path> [--storage <path>]` | Importar arquivo .backup.gz e opcionalmente .storage.zip do Dashboard do Supabase |
350
378
 
351
379
  ## 🏗️ Arquitetura Técnica
@@ -519,6 +547,28 @@ Se não estiver, inicie o Docker Desktop (Windows/macOS) ou execute `sudo system
519
547
  npm install -g supabase
520
548
  ```
521
549
 
550
+ ### Supabase CLI desatualizado (abaixo da versão mínima aceita)
551
+
552
+ O smoonb aceita versões até **1 minor atrás da latest**. Se sua versão instalada estiver abaixo desse limite, você verá uma mensagem como:
553
+
554
+ ```
555
+ ❌ Supabase CLI X.Y.Z está abaixo da versão mínima aceita (X.W.0). Versão latest: X.V.Z. Atualize para pelo menos a versão X.W.0.
556
+ ```
557
+
558
+ **Opção 1 — Atualizar o Supabase CLI:**
559
+ ```bash
560
+ npm install -g supabase@latest # instalação global
561
+ npm install supabase@latest # instalação local/projeto
562
+ ```
563
+
564
+ **Opção 2 — Pular a checagem (temporariamente, risco do usuário):**
565
+ ```bash
566
+ npx smoonb backup --skip-supabase-version-check
567
+ npx smoonb restore --skip-supabase-version-check
568
+ ```
569
+
570
+ > **Nota para usuários de Scoop (Windows):** o repositório do Scoop pode levar alguns dias para atualizar após um release. Use `--skip-supabase-version-check` até que o Scoop disponibilize a versão atualizada.
571
+
522
572
  ### Personal Access Token inválido ou ausente
523
573
 
524
574
  1. Verificar se `SUPABASE_ACCESS_TOKEN` está no `.env.local`
package/bin/smoonb.js CHANGED
@@ -79,6 +79,10 @@ program
79
79
  const getT = global.smoonbI18n?.t || t;
80
80
  return getT('help.commands.backupDesc');
81
81
  })
82
+ .option('--skip-supabase-version-check', () => {
83
+ const getT = global.smoonbI18n?.t || t;
84
+ return getT('help.commands.skipSupabaseVersionCheck');
85
+ })
82
86
  .addHelpText('after', () => {
83
87
  const getT = global.smoonbI18n?.t || t;
84
88
  return `
@@ -86,6 +90,9 @@ ${chalk.yellow.bold(getT('help.commands.backupExamples'))}
86
90
  ${chalk.white(getT('help.commands.backupExample1'))}
87
91
  ${chalk.white(getT('help.commands.backupExample1Desc'))}
88
92
 
93
+ ${chalk.white(getT('help.commands.backupExample3'))}
94
+ ${chalk.white(getT('help.commands.backupExample3Desc'))}
95
+
89
96
  ${chalk.yellow.bold(getT('help.commands.backupFlow'))}
90
97
  ${getT('help.commands.backupFlow1')}
91
98
  ${getT('help.commands.backupFlow2')}
@@ -120,6 +127,10 @@ program
120
127
  const getT = global.smoonbI18n?.t || t;
121
128
  return getT('help.commands.restoreDesc');
122
129
  })
130
+ .option('--skip-supabase-version-check', () => {
131
+ const getT = global.smoonbI18n?.t || t;
132
+ return getT('help.commands.skipSupabaseVersionCheck');
133
+ })
123
134
  .addHelpText('after', () => {
124
135
  const getT = global.smoonbI18n?.t || t;
125
136
  return `
@@ -127,6 +138,9 @@ ${chalk.yellow.bold(getT('help.commands.restoreExamples'))}
127
138
  ${chalk.white(getT('help.commands.restoreExample1'))}
128
139
  ${chalk.white(getT('help.commands.restoreExample1Desc'))}
129
140
 
141
+ ${chalk.white(getT('help.commands.restoreExample4'))}
142
+ ${chalk.white(getT('help.commands.restoreExample4Desc'))}
143
+
130
144
  ${chalk.yellow.bold(getT('help.commands.restoreFlow'))}
131
145
  ${getT('help.commands.restoreFlow1')}
132
146
  ${getT('help.commands.restoreFlow2')}
@@ -144,7 +158,7 @@ ${chalk.yellow.bold(getT('help.commands.restoreFormats'))}
144
158
  ${getT('help.commands.restoreFormats2')}
145
159
  `;
146
160
  })
147
- .action(() => commands.restore({}));
161
+ .action((options) => commands.restore(options));
148
162
 
149
163
  program
150
164
  .command('import')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoonb",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Complete Supabase backup and migration tool. https://www.smoonb.com/#price",
5
5
  "preferGlobal": false,
6
6
  "preventGlobalInstall": true,
@@ -51,7 +51,7 @@ module.exports = async (options) => {
51
51
  const licenseResult = await step00License({ envPath, command: 'backup' });
52
52
 
53
53
  // Executar validação Docker
54
- await step01DockerValidation();
54
+ await step01DockerValidation({ skipSupabaseVersionCheck: !!(options && options.skipSupabaseVersionCheck) });
55
55
 
56
56
  // Consentimento para leitura e escrita do .env.local
57
57
  console.log(chalk.yellow(`\n⚠️ ${getT('consent.title')}`));
@@ -6,17 +6,25 @@ const { t } = require('../../../i18n');
6
6
  /**
7
7
  * Etapa 0: Validação Docker
8
8
  * Deve ocorrer antes de tudo
9
+ * @param {object} [options]
10
+ * @param {boolean} [options.skipSupabaseVersionCheck=false]
9
11
  */
10
- module.exports = async () => {
12
+ module.exports = async (options = {}) => {
11
13
  const getT = global.smoonbI18n?.t || t;
12
14
  console.log(chalk.blue(`\n🐳 ${getT('docker.validation.title')}`));
13
15
  console.log(chalk.cyan(`🔍 ${getT('docker.validation.checking')}`));
14
16
 
15
- const backupCapability = await canPerformCompleteBackup();
17
+ const backupCapability = await canPerformCompleteBackup({
18
+ skipSupabaseVersionCheck: options.skipSupabaseVersionCheck || false
19
+ });
16
20
 
17
21
  if (!backupCapability.canBackupComplete) {
18
22
  showDockerMessagesAndExit(backupCapability.reason, backupCapability);
19
23
  }
24
+
25
+ if (backupCapability.supabaseVersionCheckSkipped) {
26
+ console.log(chalk.yellow(`⚠️ ${getT('supabase.cliVersionSkipWarning')}`));
27
+ }
20
28
 
21
29
  console.log(chalk.green(`✅ ${getT('docker.validation.detected')}`));
22
30
  console.log(chalk.white(`🐳 ${getT('docker.validation.version', { version: backupCapability.dockerStatus.version })}`));
@@ -7,9 +7,52 @@ const { ensureDir, writeJson } = require('../../../utils/fsx');
7
7
  const { confirm } = require('../../../utils/prompt');
8
8
  const { t } = require('../../../i18n');
9
9
 
10
+ const TIMEOUT_LIST_MS = 30_000; // 30s por chamada de listagem
11
+ const TIMEOUT_DOWNLOAD_MS = 360_000; // 2min por download de arquivo
12
+ const MAX_RETRIES = 7;
13
+ const RETRY_BASE_DELAY_MS = 2_000; // backoff: 2s → 4s → 8s
14
+
15
+ /**
16
+ * Executa uma promise com timeout. Lança Error se o tempo esgotar.
17
+ * @param {Promise} promise
18
+ * @param {number} ms
19
+ * @returns {Promise}
20
+ */
21
+ function withTimeout(promise, ms) {
22
+ let id;
23
+ const timer = new Promise((_, reject) => {
24
+ id = setTimeout(() => reject(new Error(`Timeout (${ms / 1000}s)`)), ms);
25
+ });
26
+ return Promise.race([promise, timer]).finally(() => clearTimeout(id));
27
+ }
28
+
29
+ /**
30
+ * Executa fn() com retry automático e backoff exponencial.
31
+ * @param {Function} fn - Fábrica de Promise (chamada a cada tentativa)
32
+ * @param {number} maxAttempts
33
+ * @param {number} baseDelayMs
34
+ * @param {Function} [onRetry] - Callback(attempt, max, err, delayMs)
35
+ */
36
+ async function withRetry(fn, maxAttempts = MAX_RETRIES, baseDelayMs = RETRY_BASE_DELAY_MS, onRetry) {
37
+ let lastError;
38
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
39
+ try {
40
+ return await fn();
41
+ } catch (err) {
42
+ lastError = err;
43
+ if (attempt < maxAttempts) {
44
+ const delay = baseDelayMs * Math.pow(2, attempt - 1);
45
+ if (onRetry) onRetry(attempt, maxAttempts, err, delay);
46
+ await new Promise(r => setTimeout(r, delay));
47
+ }
48
+ }
49
+ }
50
+ throw lastError;
51
+ }
52
+
10
53
  /**
11
54
  * Etapa 6: Backup Storage via Supabase API
12
- * Agora faz backup completo: metadados + download de todos os arquivos + ZIP no padrão do Dashboard
55
+ * Backup completo: metadados + download de todos os arquivos + ZIP no padrão do Dashboard
13
56
  */
14
57
  module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supabaseServiceKey }) => {
15
58
  try {
@@ -18,15 +61,14 @@ module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supaba
18
61
  await ensureDir(storageDir);
19
62
 
20
63
  console.log(chalk.white(` - ${getT('backup.steps.storage.listing')}`));
21
-
22
- // Usar fetch direto para Management API com Personal Access Token
64
+
23
65
  const storageResponse = await fetch(`https://api.supabase.com/v1/projects/${projectId}/storage/buckets`, {
24
- headers: {
66
+ headers: {
25
67
  'Authorization': `Bearer ${accessToken}`,
26
68
  'Content-Type': 'application/json'
27
69
  }
28
70
  });
29
-
71
+
30
72
  if (!storageResponse.ok) {
31
73
  console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.storage.listBucketsError', { status: storageResponse.status, statusText: storageResponse.statusText })}`));
32
74
  return { success: false, buckets: [] };
@@ -44,20 +86,16 @@ module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supaba
44
86
 
45
87
  console.log(chalk.white(` - ${getT('backup.steps.storage.found', { count: buckets.length })}`));
46
88
 
47
- // Validar credenciais do Supabase para download de arquivos
48
89
  if (!supabaseUrl || !supabaseServiceKey) {
49
90
  console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.storage.credentialsNotAvailable')}`));
50
91
  return await backupMetadataOnly(buckets, storageDir, projectId, accessToken);
51
92
  }
52
93
 
53
- // Criar cliente Supabase para download de arquivos
54
94
  const supabase = createClient(supabaseUrl, supabaseServiceKey);
55
95
 
56
- // Criar estrutura temporária para armazenar arquivos baixados
57
96
  const tempStorageDir = path.join(backupDir, 'storage_temp');
58
97
  await ensureDir(tempStorageDir);
59
-
60
- // Criar estrutura: storage_temp/project-id/bucket-name/arquivos...
98
+
61
99
  const projectStorageDir = path.join(tempStorageDir, projectId);
62
100
  await ensureDir(projectStorageDir);
63
101
 
@@ -67,10 +105,9 @@ module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supaba
67
105
  for (const bucket of buckets || []) {
68
106
  try {
69
107
  console.log(chalk.white(` - ${getT('backup.steps.storage.processing', { bucketName: bucket.name })}`));
70
-
71
- // Listar objetos do bucket via Management API com Personal Access Token
108
+
72
109
  const objectsResponse = await fetch(`https://api.supabase.com/v1/projects/${projectId}/storage/buckets/${bucket.name}/objects`, {
73
- headers: {
110
+ headers: {
74
111
  'Authorization': `Bearer ${accessToken}`,
75
112
  'Content-Type': 'application/json'
76
113
  }
@@ -90,51 +127,85 @@ module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supaba
90
127
  objects: objects || []
91
128
  };
92
129
 
93
- // Salvar informações do bucket
94
130
  const bucketPath = path.join(storageDir, `${bucket.name}.json`);
95
131
  await writeJson(bucketPath, bucketInfo);
96
132
 
97
- // Baixar todos os arquivos do bucket
98
133
  const bucketDir = path.join(projectStorageDir, bucket.name);
99
134
  await ensureDir(bucketDir);
100
-
101
- // Listar todos os arquivos recursivamente usando Supabase client
135
+
136
+ // ── Listagem com progresso inline ──────────────────────────────
102
137
  console.log(chalk.white(` - ${getT('backup.steps.storage.listingFiles', { bucketName: bucket.name })}`));
103
- const allFiles = await listAllFilesRecursively(supabase, bucket.name, '');
104
-
138
+ const counter = { total: 0 };
139
+ let allFiles = [];
140
+ let listingFailed = false;
141
+
142
+ try {
143
+ allFiles = await listAllFilesRecursively(supabase, bucket.name, '', counter, getT);
144
+ } catch (listErr) {
145
+ listingFailed = true;
146
+ console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.storage.listFailed', { bucketName: bucket.name, message: listErr.message })}`));
147
+ } finally {
148
+ // Encerra a linha de progresso (\r) iniciada por listAllFilesRecursively
149
+ process.stdout.write('\n');
150
+ }
151
+
152
+ if (!listingFailed) {
153
+ console.log(chalk.white(` - ${getT('backup.steps.storage.totalFound', { count: allFiles.length })}`));
154
+ }
155
+
156
+ // ── Download com retry ──────────────────────────────────────────
105
157
  let filesDownloaded = 0;
158
+ let filesSkipped = 0;
159
+
106
160
  if (allFiles.length > 0) {
107
161
  console.log(chalk.white(` - ${getT('backup.steps.storage.downloading', { count: allFiles.length, bucketName: bucket.name })}`));
108
-
162
+
109
163
  for (const filePath of allFiles) {
110
164
  try {
111
- // Baixar arquivo do Storage
112
- const { data: fileData, error: downloadError } = await supabase.storage
113
- .from(bucket.name)
114
- .download(filePath);
165
+ let fileData = null;
166
+ let downloadError = null;
167
+
168
+ await withRetry(
169
+ async () => {
170
+ const result = await withTimeout(
171
+ supabase.storage.from(bucket.name).download(filePath),
172
+ TIMEOUT_DOWNLOAD_MS
173
+ );
174
+ // Erros estruturais do Supabase (ex.: 404) não devem ser retentados
175
+ if (result.error) {
176
+ downloadError = result.error;
177
+ fileData = null;
178
+ } else {
179
+ fileData = result.data;
180
+ downloadError = null;
181
+ }
182
+ },
183
+ MAX_RETRIES,
184
+ RETRY_BASE_DELAY_MS,
185
+ (attempt, max, err, delay) => {
186
+ console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.storage.downloadRetry', { path: filePath, attempt, max, delay: delay / 1000 })}`));
187
+ }
188
+ );
115
189
 
116
190
  if (downloadError) {
191
+ filesSkipped++;
117
192
  console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.storage.downloadError', { path: filePath, message: downloadError.message })}`));
118
193
  continue;
119
194
  }
120
195
 
121
- // Criar estrutura de pastas local se necessário
122
196
  const localFilePath = path.join(bucketDir, filePath);
123
- const localFileDir = path.dirname(localFilePath);
124
- await ensureDir(localFileDir);
197
+ await ensureDir(path.dirname(localFilePath));
125
198
 
126
- // Salvar arquivo localmente
127
- const arrayBuffer = await fileData.arrayBuffer();
128
- const buffer = Buffer.from(arrayBuffer);
199
+ const buffer = Buffer.from(await fileData.arrayBuffer());
129
200
  await fs.writeFile(localFilePath, buffer);
130
201
  filesDownloaded++;
131
202
 
132
- // Mostrar progresso a cada 10 arquivos ou se for o último
133
203
  if (filesDownloaded % 10 === 0 || filesDownloaded === allFiles.length) {
134
204
  console.log(chalk.white(` - ${getT('backup.steps.storage.downloaded', { current: filesDownloaded, total: allFiles.length })}`));
135
205
  }
136
206
  } catch (fileError) {
137
- console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.storage.processFileError', { path: filePath, message: fileError.message })}`));
207
+ filesSkipped++;
208
+ console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.storage.downloadFailed', { path: filePath, message: fileError.message })}`));
138
209
  }
139
210
  }
140
211
  }
@@ -143,37 +214,36 @@ module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supaba
143
214
  processedBuckets.push({
144
215
  name: bucket.name,
145
216
  objectCount: objects?.length || 0,
146
- filesDownloaded: filesDownloaded,
217
+ filesDownloaded,
218
+ filesSkipped,
147
219
  totalFiles: allFiles.length
148
220
  });
149
221
 
150
- console.log(chalk.green(` ✅ ${getT('backup.steps.storage.bucketDone', { bucketName: bucket.name, downloaded: filesDownloaded, total: allFiles.length })}`));
222
+ if (filesSkipped > 0) {
223
+ console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.storage.bucketDoneWithSkips', { bucketName: bucket.name, downloaded: filesDownloaded, skipped: filesSkipped, total: allFiles.length })}`));
224
+ } else {
225
+ console.log(chalk.green(` ✅ ${getT('backup.steps.storage.bucketDone', { bucketName: bucket.name, downloaded: filesDownloaded, total: allFiles.length })}`));
226
+ }
151
227
  } catch (error) {
152
228
  console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.storage.processBucketError', { bucketName: bucket.name, message: error.message })}`));
153
229
  }
154
230
  }
155
231
 
156
- // Criar ZIP no padrão do Dashboard: {project-id}.storage.zip
232
+ // ── Criar ZIP no padrão do Dashboard ───────────────────────────────
157
233
  console.log(chalk.white(`\n - ${getT('backup.steps.storage.creatingZip')}`));
158
234
  const zipFileName = `${projectId}.storage.zip`;
159
235
  const zipFilePath = path.join(backupDir, zipFileName);
160
-
236
+
161
237
  const zip = new AdmZip();
162
-
163
- // Adicionar toda a estrutura de pastas ao ZIP
164
- // Estrutura: project-id/bucket-name/arquivos...
165
238
  await addDirectoryToZip(zip, projectStorageDir, projectId);
166
-
167
- // Salvar ZIP
168
239
  zip.writeZip(zipFilePath);
169
240
  const zipStats = await fs.stat(zipFilePath);
170
241
  const zipSizeMB = (zipStats.size / (1024 * 1024)).toFixed(2);
171
-
242
+
172
243
  console.log(chalk.green(` ✅ ${getT('backup.steps.storage.zipCreated', { fileName: zipFileName, size: zipSizeMB })}`));
173
244
 
174
- // Perguntar ao usuário se deseja limpar a estrutura temporária
175
245
  const shouldCleanup = await confirm(` ${getT('backup.steps.storage.cleanup')}`, false);
176
-
246
+
177
247
  if (shouldCleanup) {
178
248
  console.log(chalk.white(` - ${getT('backup.steps.storage.cleanupRemoving')}`));
179
249
  try {
@@ -187,11 +257,11 @@ module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supaba
187
257
  }
188
258
 
189
259
  console.log(chalk.green(`✅ ${getT('backup.steps.storage.done', { buckets: processedBuckets.length, files: totalFilesDownloaded })}`));
190
- return {
191
- success: true,
260
+ return {
261
+ success: true,
192
262
  buckets: processedBuckets,
193
263
  zipFile: zipFileName,
194
- zipSizeMB: zipSizeMB,
264
+ zipSizeMB,
195
265
  totalFiles: totalFilesDownloaded,
196
266
  tempDirCleaned: shouldCleanup
197
267
  };
@@ -202,8 +272,10 @@ module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supaba
202
272
  }
203
273
  };
204
274
 
275
+ // ── Funções auxiliares ────────────────────────────────────────────────────────
276
+
205
277
  /**
206
- * Backup apenas de metadados (fallback quando não há credenciais do Supabase)
278
+ * Backup apenas de metadados (fallback sem credenciais Supabase)
207
279
  */
208
280
  async function backupMetadataOnly(buckets, storageDir, projectId, accessToken) {
209
281
  const processedBuckets = [];
@@ -211,9 +283,9 @@ async function backupMetadataOnly(buckets, storageDir, projectId, accessToken) {
211
283
  for (const bucket of buckets || []) {
212
284
  try {
213
285
  console.log(chalk.white(` - Processando bucket: ${bucket.name}`));
214
-
286
+
215
287
  const objectsResponse = await fetch(`https://api.supabase.com/v1/projects/${projectId}/storage/buckets/${bucket.name}/objects`, {
216
- headers: {
288
+ headers: {
217
289
  'Authorization': `Bearer ${accessToken}`,
218
290
  'Content-Type': 'application/json'
219
291
  }
@@ -236,11 +308,7 @@ async function backupMetadataOnly(buckets, storageDir, projectId, accessToken) {
236
308
  const bucketPath = path.join(storageDir, `${bucket.name}.json`);
237
309
  await writeJson(bucketPath, bucketInfo);
238
310
 
239
- processedBuckets.push({
240
- name: bucket.name,
241
- objectCount: objects?.length || 0
242
- });
243
-
311
+ processedBuckets.push({ name: bucket.name, objectCount: objects?.length || 0 });
244
312
  console.log(chalk.green(` ✅ Bucket ${bucket.name}: ${objects?.length || 0} objetos`));
245
313
  } catch (error) {
246
314
  console.log(chalk.yellow(` ⚠️ Erro ao processar bucket ${bucket.name}: ${error.message}`));
@@ -252,59 +320,97 @@ async function backupMetadataOnly(buckets, storageDir, projectId, accessToken) {
252
320
  }
253
321
 
254
322
  /**
255
- * Lista todos os arquivos recursivamente de um bucket do Storage
323
+ * Lista recursivamente todos os arquivos de um bucket com:
324
+ * - progresso inline via \r (pasta atual + total encontrado)
325
+ * - timeout por chamada (TIMEOUT_LIST_MS)
326
+ * - retry com backoff exponencial (MAX_RETRIES)
327
+ *
328
+ * O CALLER é responsável por emitir \n após o retorno para encerrar a linha \r.
329
+ *
330
+ * @param {object} supabase
331
+ * @param {string} bucketName
332
+ * @param {string} folderPath
333
+ * @param {{ total: number }} counter - Contador compartilhado entre chamadas recursivas
334
+ * @param {Function} getT - Função de tradução
335
+ * @returns {Promise<string[]>}
256
336
  */
257
- async function listAllFilesRecursively(supabase, bucketName, folderPath = '') {
337
+ async function listAllFilesRecursively(supabase, bucketName, folderPath = '', counter = { total: 0 }, getT) {
258
338
  const allFiles = [];
259
- const getT = global.smoonbI18n?.t || t;
260
-
339
+ const label = folderPath ? `${bucketName}/${folderPath}` : `${bucketName}/`;
340
+ const displayLabel = label.length > 55 ? `...${label.slice(-52)}` : label;
341
+
342
+ // Progresso inline: mostra pasta atual + arquivos encontrados até agora
343
+ process.stdout.write(
344
+ chalk.gray(`\r → ${getT('backup.steps.storage.scanningFolder', { path: displayLabel, count: counter.total })} `)
345
+ );
346
+
347
+ let result;
261
348
  try {
262
- // Listar arquivos e pastas no caminho atual
263
- const { data: items, error } = await supabase.storage
264
- .from(bucketName)
265
- .list(folderPath, {
266
- limit: 1000,
267
- sortBy: { column: 'name', order: 'asc' }
268
- });
349
+ result = await withRetry(
350
+ () => withTimeout(
351
+ supabase.storage.from(bucketName).list(folderPath, {
352
+ limit: 1000,
353
+ sortBy: { column: 'name', order: 'asc' }
354
+ }),
355
+ TIMEOUT_LIST_MS
356
+ ),
357
+ MAX_RETRIES,
358
+ RETRY_BASE_DELAY_MS,
359
+ (attempt, max, err, delay) => {
360
+ // Encerra linha \r antes de imprimir o aviso
361
+ process.stdout.write('\n');
362
+ console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.storage.listRetry', { path: displayLabel, attempt, max, delay: delay / 1000, message: err.message })}`));
363
+ }
364
+ );
365
+ } catch (err) {
366
+ // Esgotou as tentativas — propaga para o caller lidar
367
+ throw err;
368
+ }
269
369
 
270
- if (error) {
271
- console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.storage.listError', { path: folderPath || 'raiz', message: error.message })}`));
272
- return allFiles;
273
- }
370
+ const { data: items, error } = result;
274
371
 
275
- if (!items || items.length === 0) {
276
- return allFiles;
277
- }
372
+ if (error) {
373
+ process.stdout.write('\n');
374
+ console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.storage.listError', { path: label, message: error.message })}`));
375
+ return allFiles;
376
+ }
278
377
 
279
- for (const item of items) {
280
- const itemPath = folderPath ? `${folderPath}/${item.name}` : item.name;
281
-
282
- if (item.id === null) {
283
- // É uma pasta, listar recursivamente
284
- const subFiles = await listAllFilesRecursively(supabase, bucketName, itemPath);
285
- allFiles.push(...subFiles);
286
- } else {
287
- // É um arquivo
288
- allFiles.push(itemPath);
289
- }
378
+ if (!items || items.length === 0) {
379
+ return allFiles;
380
+ }
381
+
382
+ for (const item of items) {
383
+ const itemPath = folderPath ? `${folderPath}/${item.name}` : item.name;
384
+
385
+ if (item.id === null) {
386
+ // Pasta listar recursivamente
387
+ const subFiles = await listAllFilesRecursively(supabase, bucketName, itemPath, counter, getT);
388
+ allFiles.push(...subFiles);
389
+ } else {
390
+ // Arquivo
391
+ allFiles.push(itemPath);
392
+ counter.total++;
393
+ // Atualizar display com novo total
394
+ const dl = label.length > 55 ? `...${label.slice(-52)}` : label;
395
+ process.stdout.write(
396
+ chalk.gray(`\r → ${getT('backup.steps.storage.scanningFolder', { path: dl, count: counter.total })} `)
397
+ );
290
398
  }
291
- } catch (error) {
292
- console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.storage.processError', { path: folderPath || 'raiz', message: error.message })}`));
293
399
  }
294
400
 
295
401
  return allFiles;
296
402
  }
297
403
 
298
404
  /**
299
- * Adiciona um diretório recursivamente ao ZIP mantendo a estrutura de pastas
405
+ * Adiciona diretório recursivamente ao ZIP mantendo estrutura de pastas
300
406
  */
301
407
  async function addDirectoryToZip(zip, dirPath, basePath = '') {
302
408
  const entries = await fs.readdir(dirPath, { withFileTypes: true });
303
-
409
+
304
410
  for (const entry of entries) {
305
411
  const fullPath = path.join(dirPath, entry.name);
306
412
  const zipPath = basePath ? `${basePath}/${entry.name}` : entry.name;
307
-
413
+
308
414
  if (entry.isDirectory()) {
309
415
  await addDirectoryToZip(zip, fullPath, zipPath);
310
416
  } else {
@@ -313,4 +419,3 @@ async function addDirectoryToZip(zip, dirPath, basePath = '') {
313
419
  }
314
420
  }
315
421
  }
316
-
@@ -83,7 +83,7 @@ function showDockerMessagesAndExit(reason, data = {}) {
83
83
  break;
84
84
 
85
85
  case 'supabase_cli_outdated':
86
- console.log(chalk.red(`❌ ${getT('supabase.cliOutdated', { version: data.supabaseCliVersion || '?', latest: data.supabaseCliLatest || '?' })}`));
86
+ console.log(chalk.red(`❌ ${getT('supabase.cliOutdated', { version: data.supabaseCliVersion || '?', minVersion: data.supabaseCliMinVersion || '?', latest: data.supabaseCliLatest || '?' })}`));
87
87
  console.log('');
88
88
  console.log(chalk.yellow(`📋 ${getT('supabase.cliUpdateInstructions')}`));
89
89
  console.log(chalk.cyan(` ${getT('supabase.cliUpdateCommandExamples')}`));
@@ -25,7 +25,7 @@ const step07DatabaseSettings = require('./steps/07-database-settings');
25
25
  const step08RealtimeSettings = require('./steps/08-realtime-settings');
26
26
  const { sendTelemetry } = require('../../telemetry');
27
27
 
28
- module.exports = async () => {
28
+ module.exports = async (options = {}) => {
29
29
  showBetaBanner();
30
30
  const restoreStartTime = Date.now();
31
31
  let telemetryEnabled = (process.env.SMOONB_TELEMETRY_ENABLED || 'true') !== 'false';
@@ -49,7 +49,7 @@ module.exports = async () => {
49
49
  await step00License({ envPath: envPathForLicense, command: 'restore' });
50
50
 
51
51
  // Executar validação Docker
52
- await step01DockerValidation();
52
+ await step01DockerValidation({ skipSupabaseVersionCheck: !!(options && options.skipSupabaseVersionCheck) });
53
53
 
54
54
  // Consentimento para leitura e escrita do .env.local
55
55
  ui.warn(`\n⚠️ ${getT('consent.title')}`);
@@ -72,8 +72,10 @@
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.",
75
+ "supabase.cliVersionPolicy": "Version policy: accepts any version up to 1 minor behind the latest (same major). Use --skip-supabase-version-check to bypass the check.",
76
+ "supabase.cliUpdateRecommended": "Supabase CLI {version} detected. We recommend always using the latest version for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli",
77
+ "supabase.cliOutdated": "Supabase CLI {version} is below the minimum accepted version ({minVersion}). Latest: {latest}. Update to at least version {minVersion}.",
78
+ "supabase.cliVersionSkipWarning": "Supabase CLI version check disabled (--skip-supabase-version-check). Proceeding without version check — user assumes the risk.",
77
79
  "supabase.cliUpdateInstructions": "Update the Supabase CLI and run smoonb again:",
78
80
  "supabase.cliUpdateCommandExamples": "Examples (depending on how you installed):",
79
81
  "supabase.cliUpdateCommandGlobal": "npm install -g supabase@latest (global)",
@@ -166,6 +168,7 @@
166
168
  "help.tip5": "• Storage file format must be: *.storage.zip",
167
169
 
168
170
  "help.commands.backupDesc": "Create a full backup of your Supabase project using Supabase CLI",
171
+ "help.commands.skipSupabaseVersionCheck": "Skip the Supabase CLI version check (user assumes the risk)",
169
172
  "help.commands.backupSkipRealtime": "Skip interactive capture of Realtime Settings",
170
173
  "help.commands.backupPostgresMajor": "Postgres major version for dump (15, 17). Non-interactive; no menu.",
171
174
  "help.commands.backupExamples": "Examples:",
@@ -173,6 +176,8 @@
173
176
  "help.commands.backupExample1Desc": "# Complete interactive process",
174
177
  "help.commands.backupExample2": "npx smoonb backup --skip-realtime",
175
178
  "help.commands.backupExample2Desc": "# Skips Realtime Settings configuration",
179
+ "help.commands.backupExample3": "npx smoonb backup --skip-supabase-version-check",
180
+ "help.commands.backupExample3Desc": "# Skips Supabase CLI version check (use when your package manager lags behind, e.g. Scoop)",
176
181
  "help.commands.backupWhat": "What is captured:",
177
182
  "help.commands.backupWhat1": "• Database PostgreSQL (pg_dumpall + separate SQL)",
178
183
  "help.commands.backupWhat2": "• Database Extensions and Settings",
@@ -206,6 +211,8 @@
206
211
  "help.commands.restoreExample2Desc": "# Imports and restores backup file directly",
207
212
  "help.commands.restoreExample3": "npx smoonb restore --file \"backup.backup.gz\" --storage \"my-project.storage.zip\"",
208
213
  "help.commands.restoreExample3Desc": "# Imports and restores backup and storage together",
214
+ "help.commands.restoreExample4": "npx smoonb restore --skip-supabase-version-check",
215
+ "help.commands.restoreExample4Desc": "# Skips Supabase CLI version check (use when your package manager lags behind, e.g. Scoop)",
209
216
  "help.commands.restoreFlow": "Restore flow:",
210
217
  "help.commands.restoreFlow1": "1. Terms of use",
211
218
  "help.commands.restoreFlow2": "2. License validation (SMOONB_LICENSE_KEY)",
@@ -428,9 +435,16 @@
428
435
  "backup.steps.storage.found": "Found {count} buckets",
429
436
  "backup.steps.storage.processing": "Processing bucket: {bucketName}",
430
437
  "backup.steps.storage.listingFiles": "Listing files from bucket {bucketName}...",
438
+ "backup.steps.storage.scanningFolder": "Scanning {path} ({count} found)",
439
+ "backup.steps.storage.totalFound": "Total: {count} file(s) found",
440
+ "backup.steps.storage.listRetry": "Attempt {attempt}/{max} to list {path} failed ({message}). Retrying in {delay}s...",
441
+ "backup.steps.storage.listFailed": "Failed to list bucket {bucketName} after all attempts: {message}",
431
442
  "backup.steps.storage.downloading": "Downloading {count} file(s) from bucket {bucketName}...",
432
443
  "backup.steps.storage.downloaded": "Downloaded {current}/{total} file(s)...",
444
+ "backup.steps.storage.downloadRetry": "File {path}: attempt {attempt}/{max} failed. Retrying in {delay}s...",
445
+ "backup.steps.storage.downloadFailed": "File {path}: failed after all attempts: {message}",
433
446
  "backup.steps.storage.bucketDone": "Bucket {bucketName}: {downloaded}/{total} file(s) downloaded",
447
+ "backup.steps.storage.bucketDoneWithSkips": "Bucket {bucketName}: {downloaded}/{total} file(s) downloaded, {skipped} skipped",
434
448
  "backup.steps.storage.creatingZip": "Creating ZIP file in Dashboard format...",
435
449
  "backup.steps.storage.zipCreated": "ZIP file created: {fileName} ({size} MB)",
436
450
  "backup.steps.storage.cleanup": "Do you want to clean storage_temp after backup",
@@ -72,8 +72,10 @@
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.",
75
+ "supabase.cliVersionPolicy": "Política de versão: aceita qualquer versão até 1 minor atrás da latest (mesma major). Use --skip-supabase-version-check para pular a checagem.",
76
+ "supabase.cliUpdateRecommended": "Supabase CLI {version} detectado. Recomendamos sempre usar a versão mais recente para obter novos recursos e correções: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli",
77
+ "supabase.cliOutdated": "Supabase CLI {version} está abaixo da versão mínima aceita ({minVersion}). Versão latest: {latest}. Atualize para pelo menos a versão {minVersion}.",
78
+ "supabase.cliVersionSkipWarning": "Checagem de versão do Supabase CLI desabilitada (--skip-supabase-version-check). Prosseguindo sem verificar a versão — o usuário assume o risco.",
77
79
  "supabase.cliUpdateInstructions": "Atualize o Supabase CLI e execute o smoonb novamente:",
78
80
  "supabase.cliUpdateCommandExamples": "Exemplos (conforme a forma de instalação):",
79
81
  "supabase.cliUpdateCommandGlobal": "npm install -g supabase@latest (global)",
@@ -166,6 +168,7 @@
166
168
  "help.tip5": "• O formato do arquivo de storage deve ser: *.storage.zip",
167
169
 
168
170
  "help.commands.backupDesc": "Fazer backup completo do projeto Supabase usando Supabase CLI",
171
+ "help.commands.skipSupabaseVersionCheck": "Pular checagem de versão do Supabase CLI (o usuário assume o risco)",
169
172
  "help.commands.backupSkipRealtime": "Pular captura interativa de Realtime Settings",
170
173
  "help.commands.backupPostgresMajor": "Versão major do Postgres para o dump (15, 17). Não interativo; sem menu.",
171
174
  "help.commands.backupExamples": "Exemplos:",
@@ -173,6 +176,8 @@
173
176
  "help.commands.backupExample1Desc": "# Processo interativo completo",
174
177
  "help.commands.backupExample2": "npx smoonb backup --skip-realtime",
175
178
  "help.commands.backupExample2Desc": "# Pula configuração de Realtime Settings",
179
+ "help.commands.backupExample3": "npx smoonb backup --skip-supabase-version-check",
180
+ "help.commands.backupExample3Desc": "# Pula checagem de versão do Supabase CLI (use quando seu gerenciador de pacotes instala com atraso, ex.: Scoop)",
176
181
  "help.commands.backupWhat": "O que é capturado:",
177
182
  "help.commands.backupWhat1": "• Database PostgreSQL (pg_dumpall + SQL separado)",
178
183
  "help.commands.backupWhat2": "• Database Extensions and Settings",
@@ -206,6 +211,8 @@
206
211
  "help.commands.restoreExample2Desc": "# Importa e restaura diretamente o arquivo de backup",
207
212
  "help.commands.restoreExample3": "npx smoonb restore --file \"backup.backup.gz\" --storage \"meu-projeto.storage.zip\"",
208
213
  "help.commands.restoreExample3Desc": "# Importa e restaura backup e storage juntos",
214
+ "help.commands.restoreExample4": "npx smoonb restore --skip-supabase-version-check",
215
+ "help.commands.restoreExample4Desc": "# Pula checagem de versão do Supabase CLI (use quando seu gerenciador de pacotes instala com atraso, ex.: Scoop)",
209
216
  "help.commands.restoreFlow": "Fluxo do restore:",
210
217
  "help.commands.restoreFlow1": "1. Termo de uso",
211
218
  "help.commands.restoreFlow2": "2. Validação de licença (SMOONB_LICENSE_KEY)",
@@ -428,9 +435,16 @@
428
435
  "backup.steps.storage.found": "Encontrados {count} buckets",
429
436
  "backup.steps.storage.processing": "Processando bucket: {bucketName}",
430
437
  "backup.steps.storage.listingFiles": "Listando arquivos do bucket {bucketName}...",
438
+ "backup.steps.storage.scanningFolder": "Escaneando {path} ({count} encontrado(s))",
439
+ "backup.steps.storage.totalFound": "Total: {count} arquivo(s) encontrado(s)",
440
+ "backup.steps.storage.listRetry": "Tentativa {attempt}/{max} ao listar {path} falhou ({message}). Retentando em {delay}s...",
441
+ "backup.steps.storage.listFailed": "Falha ao listar bucket {bucketName} após {max} tentativas: {message}",
431
442
  "backup.steps.storage.downloading": "Baixando {count} arquivo(s) do bucket {bucketName}...",
432
443
  "backup.steps.storage.downloaded": "Baixados {current}/{total} arquivo(s)...",
444
+ "backup.steps.storage.downloadRetry": "Arquivo {path}: tentativa {attempt}/{max} falhou. Retentando em {delay}s...",
445
+ "backup.steps.storage.downloadFailed": "Arquivo {path}: falhou após todas as tentativas: {message}",
433
446
  "backup.steps.storage.bucketDone": "Bucket {bucketName}: {downloaded}/{total} arquivo(s) baixado(s)",
447
+ "backup.steps.storage.bucketDoneWithSkips": "Bucket {bucketName}: {downloaded}/{total} arquivo(s) baixado(s), {skipped} pulado(s)",
434
448
  "backup.steps.storage.creatingZip": "Criando arquivo ZIP no padrão do Dashboard...",
435
449
  "backup.steps.storage.zipCreated": "Arquivo ZIP criado: {fileName} ({size} MB)",
436
450
  "backup.steps.storage.cleanup": "Deseja limpar storage_temp após o backup",
@@ -100,6 +100,21 @@ function compareSemver(a, b) {
100
100
  return 0;
101
101
  }
102
102
 
103
+ /**
104
+ * Calcula a versão mínima aceita: no máximo 1 minor atrás da latest.
105
+ * Ex: latest "2.76.12" → "2.75.0"; latest "3.0.5" → "3.0.0"
106
+ * @param {string} latestVersion
107
+ * @returns {string|null}
108
+ */
109
+ function getMinAcceptableVersion(latestVersion) {
110
+ if (!latestVersion) return null;
111
+ const parts = latestVersion.split('.').map(Number);
112
+ const major = parts[0] || 0;
113
+ const minor = parts[1] || 0;
114
+ const minMinor = Math.max(0, minor - 1);
115
+ return `${major}.${minMinor}.0`;
116
+ }
117
+
103
118
  /**
104
119
  * Detecta Docker Desktop completo com versão
105
120
  * @returns {Promise<{installed: boolean, running: boolean, version: string}>}
@@ -159,9 +174,11 @@ async function detectDockerDependencies() {
159
174
 
160
175
  /**
161
176
  * Detecta se é possível fazer backup completo via Docker
177
+ * @param {object} [opts]
178
+ * @param {boolean} [opts.skipSupabaseVersionCheck=false] - Pular checagem de versão do Supabase CLI
162
179
  * @returns {Promise<{canBackupComplete: boolean, reason?: string, dockerStatus: any}>}
163
180
  */
164
- async function canPerformCompleteBackup() {
181
+ async function canPerformCompleteBackup({ skipSupabaseVersionCheck = false } = {}) {
165
182
  const dockerStatus = await detectDockerDesktop();
166
183
 
167
184
  if (!dockerStatus.installed) {
@@ -189,6 +206,14 @@ async function canPerformCompleteBackup() {
189
206
  };
190
207
  }
191
208
 
209
+ if (skipSupabaseVersionCheck) {
210
+ return {
211
+ canBackupComplete: true,
212
+ supabaseVersionCheckSkipped: true,
213
+ dockerStatus
214
+ };
215
+ }
216
+
192
217
  const supabaseCliVersion = await getSupabaseCLIVersion();
193
218
  const latestResult = await getSupabaseCLILatestVersion();
194
219
  if (latestResult.error) {
@@ -200,12 +225,15 @@ async function canPerformCompleteBackup() {
200
225
  dockerStatus
201
226
  };
202
227
  }
203
- if (supabaseCliVersion && latestResult.version && compareSemver(supabaseCliVersion, latestResult.version) < 0) {
228
+
229
+ const minVersion = getMinAcceptableVersion(latestResult.version);
230
+ if (supabaseCliVersion && minVersion && compareSemver(supabaseCliVersion, minVersion) < 0) {
204
231
  return {
205
232
  canBackupComplete: false,
206
233
  reason: 'supabase_cli_outdated',
207
234
  supabaseCliVersion,
208
235
  supabaseCliLatest: latestResult.version,
236
+ supabaseCliMinVersion: minVersion,
209
237
  dockerStatus
210
238
  };
211
239
  }
@@ -226,5 +254,6 @@ module.exports = {
226
254
  getSupabaseCLIVersion,
227
255
  getSupabaseCLILatestVersion,
228
256
  compareSemver,
257
+ getMinAcceptableVersion,
229
258
  canPerformCompleteBackup
230
259
  };