smoonb 1.0.12 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ 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.13] - 2026-11-05
8
+
9
+ ### Fixed
10
+ - Query migration history in read-only mode before export. Missing or empty history is a successful zero-file backup, not an export error. Connection/permission failures remain failures.
11
+ - Keep a component-by-component summary for incomplete backups, including successful components and actionable migration failure reasons.
12
+ - Suppress only the bundled CLI's generic upgrade advertisement; retain database diagnostics.
13
+ - Verify exported migration file count against the recorded history count.
14
+
7
15
  ## [1.0.12] - 2026-11-05
8
16
 
9
17
  ### Fixed
package/README.md CHANGED
@@ -690,4 +690,6 @@ Additional restore-path test: `npm run test:restore:docker`. Uses disposable Pos
690
690
 
691
691
  Use Node.js LTS 22, 24 or 26 and a running Docker engine. Supabase CLI 2.122.1 is an exact package dependency, with npm-shrinkwrap.json locking distribution dependencies. The global CLI and npm latest do not determine backup compatibility. The v2 channel receives maintenance; no fixed LTS term is assumed. The first export may need to download the PostgreSQL image used by the CLI.
692
692
 
693
- SQL, roles, Functions and migrations run in temporary workspaces. Project files are preserved. Failed requested components yield exit code 1 and incomplete status; successful files remain available. Environment mappings apply to the current run without rewriting the project file. Upgrade legacy installations explicitly after publication: `npm install --save-exact smoonb@1.0.12`.
693
+ SQL, roles, Functions and migrations run in temporary workspaces. Project files are preserved. Failed requested components yield exit code 1 and incomplete status; successful files remain available. Environment mappings apply to the current run without rewriting the project file. Upgrade legacy installations explicitly after publication: `npm install --save-exact smoonb@1.0.13`.
694
+
695
+ In 1.0.13, databases without migration history complete that component with zero files. Connection/permission failures still produce an incomplete backup, with a summary of every component.
package/README.pt-BR.md CHANGED
@@ -692,4 +692,6 @@ Use Node.js LTS 22, 24 ou 26 e Docker ativo. O pacote inclui Supabase CLI 2.122.
692
692
 
693
693
  O backup usa diretórios temporários para SQL, roles, Functions e migrations, preserva os arquivos do projeto e retorna código 1/status incomplete quando um componente solicitado falha. Os arquivos concluídos ficam disponíveis, mas o conjunto não deve ser tratado como completo. O mapeamento de ambiente vale para a execução, sem regravar o arquivo do projeto.
694
694
 
695
- Após publicação, no Sicoop execute `npm install --save-exact smoonb@1.0.12`, confira `npx smoonb --version` e execute um novo backup. A versão 0.0.60 não consulta o validador e não pode ser bloqueada remotamente. Não reutilize um backup incompleto como se fosse completo.
695
+ Após publicação, no Sicoop execute `npm install --save-exact smoonb@1.0.13`, confira `npx smoonb --version` e execute um novo backup. A versão 0.0.60 não consulta o validador e não pode ser bloqueada remotamente. Não reutilize um backup incompleto como se fosse completo.
696
+
697
+ Na 1.0.13, bancos sem histórico de migrations concluem essa etapa com zero arquivos. Falhas reais de conexão/permissão continuam produzindo backup incompleto, sempre com resumo de todas as etapas.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "smoonb",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "smoonb",
9
- "version": "1.0.12",
9
+ "version": "1.0.13",
10
10
  "hasInstallScript": true,
11
11
  "license": "SEE LICENSE IN LICENSE.md",
12
12
  "dependencies": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoonb",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "description": "Complete Supabase backup and migration tool. https://www.smoonb.com/#price",
5
5
  "preferGlobal": false,
6
6
  "preventGlobalInstall": true,
@@ -11,8 +11,8 @@
11
11
  "scripts": {
12
12
  "test": "node --test tests/*.test.js",
13
13
  "start": "node bin/smoonb.js",
14
- "preinstall": "node -e \"if(process.env.npm_config_global) { console.error('\\n\u274c SMOONB N\u00c3O DEVE SER INSTALADO GLOBALMENTE!\\n\\n\ud83d\udccb Para usar o smoonb, instale localmente no seu projeto:\\n npm install smoonb\\n\\n\ud83d\udca1 Depois execute com:\\n npx smoonb backup\\n\\n\ud83d\udeab Instala\u00e7\u00e3o global cancelada!\\n'); process.exit(1); }\"",
15
- "postinstall": "echo '\\n\u2705 smoonb instalado com sucesso!\\n\ud83d\udca1 Execute: npx smoonb backup\\n\ud83d\udcd6 Documenta\u00e7\u00e3o: https://github.com/almmello/smoonb\\n'",
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
16
  "lint": "eslint . --ext .js",
17
17
  "lint:fix": "eslint . --ext .js --fix",
18
18
  "build": "node scripts/build.js",
@@ -403,6 +403,8 @@ module.exports = async (options) => {
403
403
  } else {
404
404
  console.error('BACKUP INCOMPLETO: ' + outcome.failed_components.join(', '));
405
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.');
406
408
  }
407
409
 
408
410
  // report.json
@@ -5,4 +5,12 @@ function backupOutcome(components) {
5
5
  function storageComplete(buckets, expected) {
6
6
  return buckets.length === expected && buckets.every(b => !b.listingFailed && b.filesSkipped === 0 && b.filesDownloaded === b.totalFiles && b.objectCount === b.totalFiles);
7
7
  }
8
- module.exports = { backupOutcome, storageComplete };
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 };
@@ -2,12 +2,12 @@ const chalk = require('chalk');
2
2
  const path = require('path');
3
3
  const fs = require('fs').promises;
4
4
  const { t } = require('../../../i18n');
5
- const { createPgDumpDiagnostics, hasReplicaRolePrologue } = require('../../../utils/pg-dump-diagnostics');
5
+ const { createPgDumpDiagnostics, hasReplicaRolePrologue, withoutCliUpgradeNotice } = require('../../../utils/pg-dump-diagnostics');
6
6
 
7
7
  const { isolatedSupabase } = require('../../../utils/supabaseCli');
8
8
  async function runWithElapsedTicker(_command, args, env) {
9
9
  const result = await isolatedSupabase(args, env, { capture: true });
10
- let stderr = String(result.stderr || '');
10
+ let stderr = withoutCliUpgradeNotice(result.stderr || '');
11
11
  const secrets = Object.values(env || {}).filter(Boolean);
12
12
  const databaseUrl = args[args.indexOf('--db-url') + 1];
13
13
  if (databaseUrl) {
@@ -2,16 +2,34 @@ const path = require('path');
2
2
  const fs = require('fs').promises;
3
3
  const os = require('os');
4
4
  const { runSupabase } = require('../../../utils/supabaseCli');
5
+ const { runSql } = require('../../restore/pg');
5
6
  const { copyDirectoryRecursive } = require('../../restore/utils');
6
- module.exports = async ({ databaseUrl, accessToken, backupDir }) => {
7
+ module.exports = async ({ databaseUrl, accessToken, backupDir, postgresMajor = 17 }, { query = runSql, run = runSupabase } = {}) => {
7
8
  const root = await fs.mkdtemp(path.join(os.tmpdir(), 'smoonb-migrations-'));
9
+ let phase = 'migration_history_check_failed';
8
10
  try {
11
+ const history = await query(databaseUrl, "BEGIN READ ONLY; SET LOCAL statement_timeout = '20s'; SELECT to_regclass('supabase_migrations.schema_migrations') IS NOT NULL; COMMIT;", { postgresMajor });
12
+ if (!['t', 'f'].includes(history.trim())) throw new Error('Unexpected history response');
13
+ let count = 0;
14
+ if (history.trim() === 't') {
15
+ const value = await query(databaseUrl, "BEGIN READ ONLY; SET LOCAL statement_timeout = '20s'; SELECT count(*) FROM supabase_migrations.schema_migrations; COMMIT;", { postgresMajor });
16
+ if (!/^\d+$/.test(value.trim())) throw new Error('Unexpected migration count');
17
+ count = Number(value.trim());
18
+ }
19
+ if (count === 0) {
20
+ await fs.mkdir(path.join(backupDir, 'migrations'), { recursive: true });
21
+ console.log('Migrations: nenhum histórico registrado no banco; nada a exportar.');
22
+ return { success: true, file_count: 0, reason: history.trim() === 'f' ? 'history_absent' : 'history_empty' };
23
+ }
24
+ phase = 'migration_export_failed';
9
25
  await fs.mkdir(path.join(root, 'supabase', 'migrations'), { recursive: true });
10
26
  await fs.writeFile(path.join(root, 'supabase', 'config.toml'), 'project_id = "smoonb-backup"\n');
11
- runSupabase('supabase', ['migration', 'fetch', '--db-url', databaseUrl, '--workdir', root, '--yes'], { cwd: root, env: { ...process.env, SUPABASE_ACCESS_TOKEN: accessToken }, stdio: 'pipe' });
27
+ run('supabase', ['migration', 'fetch', '--db-url', databaseUrl, '--workdir', root, '--yes'], { cwd: root, env: { ...process.env, SUPABASE_ACCESS_TOKEN: accessToken }, stdio: 'pipe' });
12
28
  const source = path.join(root, 'supabase', 'migrations');
13
29
  await copyDirectoryRecursive(source, path.join(backupDir, 'migrations'));
14
- return { success: true, file_count: (await fs.readdir(source)).length };
15
- } catch { return { success: false, reason: 'migration_export_failed' }; }
30
+ const fileCount = (await fs.readdir(source)).filter(name => name.endsWith('.sql')).length;
31
+ if (fileCount !== count) throw new Error('Migration export count mismatch');
32
+ return { success: true, file_count: fileCount };
33
+ } catch { return { success: false, reason: phase, action: 'Check database connectivity and access to supabase_migrations.schema_migrations; retry the backup.' }; }
16
34
  finally { await fs.rm(root, { recursive: true, force: true }); }
17
35
  };
@@ -1,64 +1,68 @@
1
- const { StringDecoder } = require('string_decoder');
2
- const fs = require('fs').promises;
3
-
4
- // Only the exact, known pg_dump circular-FK block is consolidated. All other
5
- // stderr (including errors and unfamiliar/localized warnings) remains visible.
6
- function createPgDumpDiagnostics(writeStderr) {
7
- const decoder = new StringDecoder('utf8');
8
- let pending = '';
9
- let inCircularWarning = false;
10
- const warnings = [];
11
- const tables = new Set();
12
- const hints = new Set([
13
- 'pg_dump: hint: You might not be able to restore the dump without using --disable-triggers or temporarily dropping the constraints.',
14
- 'pg_dump: hint: Consider using a full dump instead of a --data-only dump to avoid this problem.'
15
- ]);
16
- function line(raw) {
17
- const text = raw.replace(/\r?\n$/, '');
18
- if (/^pg_dump: warning: there are circular foreign-key constraints (?:among these tables|on this table):$/.test(text)) {
19
- inCircularWarning = true;
20
- warnings.push(text);
21
- } else if (inCircularWarning && text.startsWith('pg_dump: detail: ')) {
22
- tables.add(text.slice('pg_dump: detail: '.length));
23
- warnings.push(text);
24
- } else if (inCircularWarning && hints.has(text)) {
25
- warnings.push(text);
26
- } else {
27
- inCircularWarning = false;
28
- writeStderr(raw);
29
- }
30
- }
31
- function consume(text) {
32
- pending += text;
33
- let end;
34
- while ((end = pending.indexOf('\n')) !== -1) {
35
- line(pending.slice(0, end + 1));
36
- pending = pending.slice(end + 1);
37
- }
38
- }
39
- return {
40
- write(chunk) { consume(decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))); },
41
- end() {
42
- consume(decoder.end());
43
- if (pending) line(pending);
44
- pending = '';
45
- return { circularForeignKeys: warnings.length > 0, tables: [...tables], messages: warnings };
46
- }
47
- };
48
- }
49
-
50
- // Supabase prepends this command before pg_dump output. Check the first line,
51
- // not arbitrary table contents: a data row containing SQL is not protection.
52
- async function hasReplicaRolePrologue(file) {
53
- const handle = await fs.open(file, 'r');
54
- try {
55
- const buffer = Buffer.alloc(256);
56
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
57
- const firstLine = buffer.toString('utf8', 0, bytesRead).split('\n')[0].replace(/^\uFEFF/, '').trim();
58
- return /^SET\s+session_replication_role\s*=\s*(?:replica|'replica');$/i.test(firstLine);
59
- } finally {
60
- await handle.close();
61
- }
62
- }
63
-
64
- module.exports = { createPgDumpDiagnostics, hasReplicaRolePrologue };
1
+ const { StringDecoder } = require('string_decoder');
2
+ const fs = require('fs').promises;
3
+
4
+ // Only the exact, known pg_dump circular-FK block is consolidated. All other
5
+ // stderr (including errors and unfamiliar/localized warnings) remains visible.
6
+ function createPgDumpDiagnostics(writeStderr) {
7
+ const decoder = new StringDecoder('utf8');
8
+ let pending = '';
9
+ let inCircularWarning = false;
10
+ const warnings = [];
11
+ const tables = new Set();
12
+ const hints = new Set([
13
+ 'pg_dump: hint: You might not be able to restore the dump without using --disable-triggers or temporarily dropping the constraints.',
14
+ 'pg_dump: hint: Consider using a full dump instead of a --data-only dump to avoid this problem.'
15
+ ]);
16
+ function line(raw) {
17
+ const text = raw.replace(/\r?\n$/, '');
18
+ if (/^pg_dump: warning: there are circular foreign-key constraints (?:among these tables|on this table):$/.test(text)) {
19
+ inCircularWarning = true;
20
+ warnings.push(text);
21
+ } else if (inCircularWarning && text.startsWith('pg_dump: detail: ')) {
22
+ tables.add(text.slice('pg_dump: detail: '.length));
23
+ warnings.push(text);
24
+ } else if (inCircularWarning && hints.has(text)) {
25
+ warnings.push(text);
26
+ } else {
27
+ inCircularWarning = false;
28
+ writeStderr(raw);
29
+ }
30
+ }
31
+ function consume(text) {
32
+ pending += text;
33
+ let end;
34
+ while ((end = pending.indexOf('\n')) !== -1) {
35
+ line(pending.slice(0, end + 1));
36
+ pending = pending.slice(end + 1);
37
+ }
38
+ }
39
+ return {
40
+ write(chunk) { consume(decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))); },
41
+ end() {
42
+ consume(decoder.end());
43
+ if (pending) line(pending);
44
+ pending = '';
45
+ return { circularForeignKeys: warnings.length > 0, tables: [...tables], messages: warnings };
46
+ }
47
+ };
48
+ }
49
+
50
+ // Supabase prepends this command before pg_dump output. Check the first line,
51
+ // not arbitrary table contents: a data row containing SQL is not protection.
52
+ async function hasReplicaRolePrologue(file) {
53
+ const handle = await fs.open(file, 'r');
54
+ try {
55
+ const buffer = Buffer.alloc(256);
56
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
57
+ const firstLine = buffer.toString('utf8', 0, bytesRead).split('\n')[0].replace(/^\uFEFF/, '').trim();
58
+ return /^SET\s+session_replication_role\s*=\s*(?:replica|'replica');$/i.test(firstLine);
59
+ } finally {
60
+ await handle.close();
61
+ }
62
+ }
63
+
64
+ // The bundled CLI is deliberately pinned. Suppress only its generic upgrade advert.
65
+ function withoutCliUpgradeNotice(text) {
66
+ return String(text).split(/(?<=\n)/).filter(line => !/^A new version of Supabase CLI is available: v[0-9.]+ \(currently installed v[0-9.]+\)\r?\n?$/.test(line) && !/^We recommend updating regularly for new features and bug fixes: https:\/\/supabase\.com\/docs\/guides\/cli\/getting-started#updating-the-supabase-cli\r?\n?$/.test(line)).join('');
67
+ }
68
+ module.exports = { createPgDumpDiagnostics, hasReplicaRolePrologue, withoutCliUpgradeNotice };