smoonb 1.0.13 → 1.0.14
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 +8 -0
- package/README.md +695 -695
- package/README.pt-BR.md +697 -697
- package/npm-shrinkwrap.json +2030 -2030
- package/package.json +71 -71
- package/src/commands/backup/index.js +10 -6
- package/src/commands/backup/steps/00-license.js +2 -2
- package/src/i18n/locales/en.json +3 -3
- package/src/i18n/locales/pt-BR.json +3 -3
- package/src/interactive/envMapper.js +4 -1
- package/src/utils/env.js +33 -15
package/package.json
CHANGED
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "smoonb",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Complete Supabase backup and migration tool. https://www.smoonb.com/#price",
|
|
5
|
-
"preferGlobal": false,
|
|
6
|
-
"preventGlobalInstall": true,
|
|
7
|
-
"main": "index.js",
|
|
8
|
-
"bin": {
|
|
9
|
-
"smoonb": "bin/smoonb.js"
|
|
10
|
-
},
|
|
11
|
-
"scripts": {
|
|
12
|
-
"test": "node --test tests/*.test.js",
|
|
13
|
-
"start": "node bin/smoonb.js",
|
|
14
|
-
"preinstall": "node -e \"if(process.env.npm_config_global) { console.error('\\n❌ SMOONB NÃO DEVE SER INSTALADO GLOBALMENTE!\\n\\n📋 Para usar o smoonb, instale localmente no seu projeto:\\n npm install smoonb\\n\\n💡 Depois execute com:\\n npx smoonb backup\\n\\n🚫 Instalação global cancelada!\\n'); process.exit(1); }\"",
|
|
15
|
-
"postinstall": "echo '\\n✅ smoonb instalado com sucesso!\\n💡 Execute: npx smoonb backup\\n📖 Documentação: https://github.com/almmello/smoonb\\n'",
|
|
16
|
-
"lint": "eslint . --ext .js",
|
|
17
|
-
"lint:fix": "eslint . --ext .js --fix",
|
|
18
|
-
"build": "node scripts/build.js",
|
|
19
|
-
"build:check": "node scripts/build.js",
|
|
20
|
-
"prepublishOnly": "npm test && npm run lint && npm run build",
|
|
21
|
-
"test:docker": "node tests/integration/circular-restore.js",
|
|
22
|
-
"test:restore:docker": "node tests/integration/verified-restore.js",
|
|
23
|
-
"test:backup:docker": "node tests/integration/backup-isolation.js"
|
|
24
|
-
},
|
|
25
|
-
"keywords": [
|
|
26
|
-
"supabase",
|
|
27
|
-
"backup",
|
|
28
|
-
"migration",
|
|
29
|
-
"postgresql",
|
|
30
|
-
"edge-functions",
|
|
31
|
-
"database",
|
|
32
|
-
"cli",
|
|
33
|
-
"tool"
|
|
34
|
-
],
|
|
35
|
-
"author": "Goalmoon Tecnologia LTDA <https://www.smoonb.com>",
|
|
36
|
-
"license": "SEE LICENSE IN LICENSE.md",
|
|
37
|
-
"engines": {
|
|
38
|
-
"node": "^22.0.0 || ^24.0.0 || ^26.0.0"
|
|
39
|
-
},
|
|
40
|
-
"dependencies": {
|
|
41
|
-
"@supabase/supabase-js": "^2.93.0",
|
|
42
|
-
"adm-zip": "0.6.1",
|
|
43
|
-
"chalk": "^4.1.2",
|
|
44
|
-
"commander": "^11.1.0",
|
|
45
|
-
"env-paths": "^4.0.0",
|
|
46
|
-
"inquirer": "^8.2.7",
|
|
47
|
-
"supabase": "2.122.1"
|
|
48
|
-
},
|
|
49
|
-
"type": "commonjs",
|
|
50
|
-
"devDependencies": {
|
|
51
|
-
"eslint": "^9.38.0"
|
|
52
|
-
},
|
|
53
|
-
"repository": {
|
|
54
|
-
"type": "git",
|
|
55
|
-
"url": "git+https://github.com/almmello/smoonb.git"
|
|
56
|
-
},
|
|
57
|
-
"bugs": {
|
|
58
|
-
"url": "https://github.com/almmello/smoonb/issues"
|
|
59
|
-
},
|
|
60
|
-
"homepage": "https://www.smoonb.com",
|
|
61
|
-
"files": [
|
|
62
|
-
"bin/",
|
|
63
|
-
"src/",
|
|
64
|
-
"README.md",
|
|
65
|
-
"README.pt-BR.md",
|
|
66
|
-
"LICENSE",
|
|
67
|
-
"LICENSE.pt-BR.md",
|
|
68
|
-
"CHANGELOG.md",
|
|
69
|
-
"npm-shrinkwrap.json"
|
|
70
|
-
]
|
|
71
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "smoonb",
|
|
3
|
+
"version": "1.0.14",
|
|
4
|
+
"description": "Complete Supabase backup and migration tool. https://www.smoonb.com/#price",
|
|
5
|
+
"preferGlobal": false,
|
|
6
|
+
"preventGlobalInstall": true,
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"smoonb": "bin/smoonb.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "node --test tests/*.test.js",
|
|
13
|
+
"start": "node bin/smoonb.js",
|
|
14
|
+
"preinstall": "node -e \"if(process.env.npm_config_global) { console.error('\\n❌ SMOONB NÃO DEVE SER INSTALADO GLOBALMENTE!\\n\\n📋 Para usar o smoonb, instale localmente no seu projeto:\\n npm install smoonb\\n\\n💡 Depois execute com:\\n npx smoonb backup\\n\\n🚫 Instalação global cancelada!\\n'); process.exit(1); }\"",
|
|
15
|
+
"postinstall": "echo '\\n✅ smoonb instalado com sucesso!\\n💡 Execute: npx smoonb backup\\n📖 Documentação: https://github.com/almmello/smoonb\\n'",
|
|
16
|
+
"lint": "eslint . --ext .js",
|
|
17
|
+
"lint:fix": "eslint . --ext .js --fix",
|
|
18
|
+
"build": "node scripts/build.js",
|
|
19
|
+
"build:check": "node scripts/build.js",
|
|
20
|
+
"prepublishOnly": "npm test && npm run lint && npm run build",
|
|
21
|
+
"test:docker": "node tests/integration/circular-restore.js",
|
|
22
|
+
"test:restore:docker": "node tests/integration/verified-restore.js",
|
|
23
|
+
"test:backup:docker": "node tests/integration/backup-isolation.js"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"supabase",
|
|
27
|
+
"backup",
|
|
28
|
+
"migration",
|
|
29
|
+
"postgresql",
|
|
30
|
+
"edge-functions",
|
|
31
|
+
"database",
|
|
32
|
+
"cli",
|
|
33
|
+
"tool"
|
|
34
|
+
],
|
|
35
|
+
"author": "Goalmoon Tecnologia LTDA <https://www.smoonb.com>",
|
|
36
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": "^22.0.0 || ^24.0.0 || ^26.0.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@supabase/supabase-js": "^2.93.0",
|
|
42
|
+
"adm-zip": "0.6.1",
|
|
43
|
+
"chalk": "^4.1.2",
|
|
44
|
+
"commander": "^11.1.0",
|
|
45
|
+
"env-paths": "^4.0.0",
|
|
46
|
+
"inquirer": "^8.2.7",
|
|
47
|
+
"supabase": "2.122.1"
|
|
48
|
+
},
|
|
49
|
+
"type": "commonjs",
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"eslint": "^9.38.0"
|
|
52
|
+
},
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "git+https://github.com/almmello/smoonb.git"
|
|
56
|
+
},
|
|
57
|
+
"bugs": {
|
|
58
|
+
"url": "https://github.com/almmello/smoonb/issues"
|
|
59
|
+
},
|
|
60
|
+
"homepage": "https://www.smoonb.com",
|
|
61
|
+
"files": [
|
|
62
|
+
"bin/",
|
|
63
|
+
"src/",
|
|
64
|
+
"README.md",
|
|
65
|
+
"README.pt-BR.md",
|
|
66
|
+
"LICENSE",
|
|
67
|
+
"LICENSE.pt-BR.md",
|
|
68
|
+
"CHANGELOG.md",
|
|
69
|
+
"npm-shrinkwrap.json"
|
|
70
|
+
]
|
|
71
|
+
}
|
|
@@ -4,7 +4,7 @@ const fs = require('fs').promises;
|
|
|
4
4
|
const { ensureDir, writeJson } = require('../../utils/fsx');
|
|
5
5
|
const { showBetaBanner } = require('../../utils/banner');
|
|
6
6
|
const { getDockerVersion } = require('../../utils/docker');
|
|
7
|
-
const { readEnvFile, backupEnvFile } = require('../../utils/env');
|
|
7
|
+
const { readEnvFile, writeEnvFile, backupEnvFile } = require('../../utils/env');
|
|
8
8
|
const { saveEnvMap } = require('../../utils/envMap');
|
|
9
9
|
const { mapEnvVariablesInteractively, askComponentsFlags, printSupabasePostgresMajorHintOnce } = require('../../interactive/envMapper');
|
|
10
10
|
const { confirm } = require('../../utils/prompt');
|
|
@@ -85,7 +85,7 @@ module.exports = async (options) => {
|
|
|
85
85
|
await ensureDir(backupDir);
|
|
86
86
|
|
|
87
87
|
// Backup e mapeamento do .env.local
|
|
88
|
-
const envBackupPath = path.join(backupDir, 'env', '.env.local');
|
|
88
|
+
const envBackupPath = path.join(backupDir, 'env', '.env.local.before');
|
|
89
89
|
await ensureDir(path.dirname(envBackupPath));
|
|
90
90
|
|
|
91
91
|
// Verificar se o arquivo existe antes de fazer backup
|
|
@@ -93,7 +93,8 @@ module.exports = async (options) => {
|
|
|
93
93
|
await fs.access(envPath);
|
|
94
94
|
await backupEnvFile(envPath, envBackupPath);
|
|
95
95
|
console.log(chalk.blue(`📁 ${getT('env.mapping.backupCreated', { path: path.relative(process.cwd(), envBackupPath) })}`));
|
|
96
|
-
} catch {
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (error.code !== 'ENOENT') throw error;
|
|
97
98
|
// Arquivo não existe, não fazer backup
|
|
98
99
|
console.log(chalk.yellow(`⚠️ ${getT('env.mapping.fileNotFound')}`));
|
|
99
100
|
}
|
|
@@ -110,10 +111,13 @@ module.exports = async (options) => {
|
|
|
110
111
|
'SMOONB_TELEMETRY_ENABLED'
|
|
111
112
|
];
|
|
112
113
|
const currentEnv = await readEnvFile(envPath);
|
|
113
|
-
const { finalEnv, dePara } = await mapEnvVariablesInteractively(currentEnv, expectedKeys)
|
|
114
|
-
|
|
114
|
+
const { finalEnv, dePara } = await mapEnvVariablesInteractively(currentEnv, expectedKeys, async (entry, mapping) => {
|
|
115
|
+
await writeEnvFile(envPath, entry);
|
|
116
|
+
await saveEnvMap(mapping, path.join(backupDir, 'env', 'env-map.json'));
|
|
117
|
+
});
|
|
118
|
+
await backupEnvFile(envPath, path.join(backupDir, 'env', '.env.local'));
|
|
115
119
|
await saveEnvMap(dePara, path.join(backupDir, 'env', 'env-map.json'));
|
|
116
|
-
console.log(chalk.green(`✅ ${getT('backup.
|
|
120
|
+
console.log(chalk.green(`✅ ${getT('backup.environmentSaved')}`));
|
|
117
121
|
|
|
118
122
|
function getValue(expectedKey) {
|
|
119
123
|
const clientKey = Object.keys(dePara).find(k => dePara[k] === expectedKey);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const https = require('https');
|
|
2
2
|
const inquirer = require('inquirer');
|
|
3
3
|
const path = require('path');
|
|
4
|
-
const { readEnvFile } = require('../../../utils/env');
|
|
4
|
+
const { readEnvFile, writeEnvFile } = require('../../../utils/env');
|
|
5
5
|
const { APP_CONFIG } = require('../../../config/appConfig');
|
|
6
6
|
const { t } = require('../../../i18n');
|
|
7
7
|
const ui = require('../../../utils/cliUi');
|
|
@@ -271,7 +271,7 @@ module.exports = async (options) => {
|
|
|
271
271
|
ui.link(` ${appUrl}`);
|
|
272
272
|
process.exit(1);
|
|
273
273
|
}
|
|
274
|
-
|
|
274
|
+
await writeEnvFile(envPath, { [LICENSE_KEY_ENV]: licenseKey });
|
|
275
275
|
process.env[LICENSE_KEY_ENV] = licenseKey;
|
|
276
276
|
}
|
|
277
277
|
|
package/src/i18n/locales/en.json
CHANGED
|
@@ -155,8 +155,8 @@
|
|
|
155
155
|
"disclaimer.operationCancelled": "Operation cancelled by user.",
|
|
156
156
|
|
|
157
157
|
"consent.title": "Smoonb will read .env.local and preserve the project file.",
|
|
158
|
-
"consent.backup": "
|
|
159
|
-
"consent.mapping": "
|
|
158
|
+
"consent.backup": "The previous configuration and the configuration used will be copied to the backup env folder.",
|
|
159
|
+
"consent.mapping": "Each confirmed value will be saved immediately to .env.local, before backup. Other variables will be preserved, and saved values will remain available if execution fails or is canceled.",
|
|
160
160
|
"consent.proceed": "Do you consent to proceed?",
|
|
161
161
|
|
|
162
162
|
"help.configuration": "CONFIGURATION:",
|
|
@@ -821,5 +821,5 @@
|
|
|
821
821
|
"error.storageCredentialsNotConfigured": "Target project credentials not configured. SUPABASE_PROJECT_ID and SUPABASE_ACCESS_TOKEN are required",
|
|
822
822
|
"error.supabaseCredentialsNotConfigured": "Supabase credentials not configured. NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are required",
|
|
823
823
|
"restore.verified.remapUrls": "Update old Supabase service URLs to the new project in data and files? (Includes URLs inside JSON; unrelated text and IDs are preserved.)",
|
|
824
|
-
"backup.
|
|
824
|
+
"backup.environmentSaved": "Configuration saved to .env.local; available on the next run even if backup fails."
|
|
825
825
|
}
|
|
@@ -155,8 +155,8 @@
|
|
|
155
155
|
"disclaimer.operationCancelled": "Operação cancelada pelo usuário.",
|
|
156
156
|
|
|
157
157
|
"consent.title": "O smoonb irá ler .env.local e preservar o arquivo do projeto.",
|
|
158
|
-
"consent.backup": "
|
|
159
|
-
"consent.mapping": "
|
|
158
|
+
"consent.backup": "A configuração anterior e a configuração usada serão copiadas para a pasta env do backup.",
|
|
159
|
+
"consent.mapping": "Cada valor confirmado será salvo imediatamente em .env.local, antes do backup. As demais variáveis serão preservadas, e os valores salvos continuarão disponíveis se a execução falhar ou for cancelada.",
|
|
160
160
|
"consent.proceed": "Você consente em prosseguir?",
|
|
161
161
|
|
|
162
162
|
"help.configuration": "CONFIGURAÇÃO:",
|
|
@@ -820,5 +820,5 @@
|
|
|
820
820
|
"error.storageCredentialsNotConfigured": "Credenciais do projeto destino não configuradas. É necessário SUPABASE_PROJECT_ID e SUPABASE_ACCESS_TOKEN",
|
|
821
821
|
"error.supabaseCredentialsNotConfigured": "Credenciais do Supabase não configuradas. É necessário NEXT_PUBLIC_SUPABASE_URL e SUPABASE_SERVICE_ROLE_KEY",
|
|
822
822
|
"restore.verified.remapUrls": "Atualizar os endereços Supabase do projeto antigo para o novo nos dados e arquivos? (Inclui URLs em campos JSON; outros textos e IDs são preservados.)",
|
|
823
|
-
"backup.
|
|
823
|
+
"backup.environmentSaved": "Configuração salva em .env.local; disponível para a próxima execução, mesmo se o backup falhar."
|
|
824
824
|
}
|
|
@@ -14,7 +14,7 @@ function printSupabasePostgresMajorHintOnce(getT) {
|
|
|
14
14
|
ui.hint(` ${getT('env.supabasePostgresMajor.hintBody')}`);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
async function mapEnvVariablesInteractively(env, expectedKeys) {
|
|
17
|
+
async function mapEnvVariablesInteractively(env, expectedKeys, onMapped = async () => {}) {
|
|
18
18
|
_printedSupabasePostgresMajorHint = false;
|
|
19
19
|
const finalEnv = { ...env };
|
|
20
20
|
const dePara = {};
|
|
@@ -208,6 +208,7 @@ async function mapEnvVariablesInteractively(env, expectedKeys) {
|
|
|
208
208
|
throw new Error(`Duplicidade de mapeamento detectada para ${clientKey}`);
|
|
209
209
|
}
|
|
210
210
|
dePara[clientKey] = expected;
|
|
211
|
+
await onMapped({ [clientKey]: finalEnv[clientKey] }, { ...dePara });
|
|
211
212
|
continue;
|
|
212
213
|
}
|
|
213
214
|
}
|
|
@@ -261,6 +262,7 @@ async function mapEnvVariablesInteractively(env, expectedKeys) {
|
|
|
261
262
|
throw new Error(`Duplicidade de mapeamento detectada para ${clientKey}`);
|
|
262
263
|
}
|
|
263
264
|
dePara[clientKey] = expected;
|
|
265
|
+
await onMapped({ [clientKey]: finalEnv[clientKey] }, { ...dePara });
|
|
264
266
|
}
|
|
265
267
|
|
|
266
268
|
// Após processar todas as variáveis, perguntar sobre idioma padrão se SMOONB_OUTPUT_DIR foi processado
|
|
@@ -288,6 +290,7 @@ async function mapEnvVariablesInteractively(env, expectedKeys) {
|
|
|
288
290
|
}]);
|
|
289
291
|
|
|
290
292
|
finalEnv.SMOONB_LANG = selectedLang;
|
|
293
|
+
await onMapped({ SMOONB_LANG: selectedLang }, { ...dePara });
|
|
291
294
|
console.log(chalk.green(`✅ ${getT('env.language.saved', { lang: selectedLang })}`));
|
|
292
295
|
console.log(chalk.yellow(`💡 ${getT('env.language.note')}`));
|
|
293
296
|
}
|
package/src/utils/env.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fsp = require('fs').promises;
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const { randomUUID } = require('crypto');
|
|
3
4
|
|
|
4
5
|
function parseEnvContent(content) {
|
|
5
6
|
const lines = content.replace(/\r\n/g, '\n').split('\n');
|
|
@@ -9,10 +10,10 @@ function parseEnvContent(content) {
|
|
|
9
10
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
10
11
|
const eqIndex = line.indexOf('=');
|
|
11
12
|
if (eqIndex === -1) continue;
|
|
12
|
-
const key = line.slice(0, eqIndex).trim();
|
|
13
|
+
const key = line.slice(0, eqIndex).trim().replace(/^export\s+/, '');
|
|
13
14
|
let value = line.slice(eqIndex + 1);
|
|
14
15
|
// Remove optional quotes
|
|
15
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
16
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) || (value.startsWith("`") && value.endsWith("`"))) {
|
|
16
17
|
value = value.slice(1, -1);
|
|
17
18
|
}
|
|
18
19
|
entries[key] = value;
|
|
@@ -20,10 +21,19 @@ function parseEnvContent(content) {
|
|
|
20
21
|
return entries;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
function encodeEnvValue(value) {
|
|
25
|
+
const raw = String(value ?? '');
|
|
26
|
+
if (!/[\s#"'`]/.test(raw)) return raw;
|
|
27
|
+
const quote = ["'", '`', '"'].find(q => !raw.includes(q) && (q !== '"' || !/\\[nr]/.test(raw)));
|
|
28
|
+
if (!quote) throw new Error('Environment value cannot be safely quoted; file was not changed');
|
|
29
|
+
return quote + raw + quote;
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
function stringifyEnv(entries, existingContent) {
|
|
24
33
|
// Best-effort: keep existing comments and order; update or append keys
|
|
25
34
|
const existingLines = (existingContent || '').replace(/\r\n/g, '\n').split('\n');
|
|
26
35
|
const seen = new Set();
|
|
36
|
+
const existingEntries = parseEnvContent(existingContent || '');
|
|
27
37
|
const resultLines = [];
|
|
28
38
|
|
|
29
39
|
for (const line of existingLines) {
|
|
@@ -33,12 +43,12 @@ function stringifyEnv(entries, existingContent) {
|
|
|
33
43
|
continue;
|
|
34
44
|
}
|
|
35
45
|
const eqIndex = line.indexOf('=');
|
|
36
|
-
const
|
|
46
|
+
const prefix = line.slice(0, eqIndex);
|
|
47
|
+
const key = prefix.trim().replace(/^export\s+/, '');
|
|
37
48
|
if (Object.prototype.hasOwnProperty.call(entries, key)) {
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
resultLines.push(`${key}=${safeValue}`);
|
|
49
|
+
if (existingEntries[key] === entries[key]) { resultLines.push(line); seen.add(key); continue; }
|
|
50
|
+
const safeValue = encodeEnvValue(entries[key]);
|
|
51
|
+
resultLines.push(`${prefix}=${safeValue}`);
|
|
42
52
|
seen.add(key);
|
|
43
53
|
} else {
|
|
44
54
|
resultLines.push(line);
|
|
@@ -47,15 +57,14 @@ function stringifyEnv(entries, existingContent) {
|
|
|
47
57
|
|
|
48
58
|
for (const [key, value] of Object.entries(entries)) {
|
|
49
59
|
if (seen.has(key)) continue;
|
|
50
|
-
const
|
|
51
|
-
const needsQuote = /\s|[#]/.test(rawValue);
|
|
52
|
-
const safeValue = needsQuote ? `"${rawValue.replace(/"/g, '\\"')}"` : rawValue;
|
|
60
|
+
const safeValue = encodeEnvValue(value);
|
|
53
61
|
resultLines.push(`${key}=${safeValue}`);
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
// Ensure trailing newline
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
const newline = (existingContent || '').includes('\r\n') ? '\r\n' : '\n';
|
|
66
|
+
let out = resultLines.join(newline);
|
|
67
|
+
if (!out.endsWith('\n')) out += newline;
|
|
59
68
|
return out;
|
|
60
69
|
}
|
|
61
70
|
|
|
@@ -73,9 +82,18 @@ async function writeEnvFile(filePath, entries, _options = {}) {
|
|
|
73
82
|
const dir = path.dirname(filePath);
|
|
74
83
|
await fsp.mkdir(dir, { recursive: true });
|
|
75
84
|
let existing = '';
|
|
76
|
-
try { existing = await fsp.readFile(filePath, 'utf8'); } catch {}
|
|
85
|
+
try { existing = await fsp.readFile(filePath, 'utf8'); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
86
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
87
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || /[\r\n\0]/.test(String(value ?? ''))) throw new Error('Invalid environment entry; file was not changed');
|
|
88
|
+
}
|
|
77
89
|
const content = stringifyEnv(entries, existing);
|
|
78
|
-
|
|
90
|
+
if (content === existing) return;
|
|
91
|
+
const temporary = path.join(dir, `.smoonb-env-${randomUUID()}.tmp`);
|
|
92
|
+
try {
|
|
93
|
+
await fsp.writeFile(temporary, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
94
|
+
await fsp.rename(temporary, filePath);
|
|
95
|
+
} finally { await fsp.unlink(temporary).catch(error => { if (error.code !== 'ENOENT') throw error; }); }
|
|
96
|
+
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
async function backupEnvFile(srcPath, destPath) {
|
|
@@ -86,7 +104,7 @@ async function backupEnvFile(srcPath, destPath) {
|
|
|
86
104
|
await fsp.copyFile(srcPath, destPath);
|
|
87
105
|
} catch (e) {
|
|
88
106
|
if (e.code === 'ENOENT') {
|
|
89
|
-
// Arquivo
|
|
107
|
+
// Arquivo não existe, não fazer backup de arquivo vazio
|
|
90
108
|
return;
|
|
91
109
|
}
|
|
92
110
|
throw e;
|