@mostajs/env-sync 0.1.0
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 +4 -0
- package/README.md +26 -0
- package/bin/cli.mjs +18 -0
- package/llms.txt +6 -0
- package/package.json +36 -0
- package/src/index.js +69 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# Changelog — @mostajs/env-sync
|
|
2
|
+
## [0.1.0] — 2026-06-22
|
|
3
|
+
### Ajouté
|
|
4
|
+
- Upsert idempotent de clés gérées dans un .env local ou distant SSH (préserve le reste) ; secrets masqués au rapport ; `--dry-run` ; reload optionnel. 5 tests. Extrait des `set-prod-env.sh`/`set-demo-env.sh` bespoke.
|
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @mostajs/env-sync
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
|
+
|
|
5
|
+
Upsert **idempotent** de clés gérées dans un `.env` (local **ou** distant SSH) en **préservant le reste** — pour pousser secrets/config au déploiement sans écraser le `.env` distant. Zéro-dépendance (node:fs + node:child_process). Membre de `mosta-deploy-stack`.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
// env-sync.config.mjs
|
|
9
|
+
export default {
|
|
10
|
+
source: '.env.production',
|
|
11
|
+
keys: ['NODE_ENV', 'SMTP_PASS', 'EVENTS_SECRET'],
|
|
12
|
+
target: { ssh: 'amia', file: '/home/hmd/prod/incubator/.env',
|
|
13
|
+
reload: 'pm2 startOrReload ecosystem.config.cjs --update-env' },
|
|
14
|
+
};
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
mosta-env-sync --config env-sync.config.mjs --dry-run # rapport (secrets masqués), rien écrit
|
|
19
|
+
mosta-env-sync --config env-sync.config.mjs # pousse les clés gérées + reload
|
|
20
|
+
```
|
|
21
|
+
Local : `target: { file: './.env' }`. Distant : `target: { ssh, file, reload? }`.
|
|
22
|
+
|
|
23
|
+
## API
|
|
24
|
+
`envSync(config, { dryRun })` · `parseEnv` · `buildBlock` · `upsertLocal` · `buildRemoteScript` · `mask`.
|
|
25
|
+
|
|
26
|
+
Tests : `npm test`. Licence : AGPL-3.0-or-later.
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @mostajs/env-sync — CLI. Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
// Usage : mosta-env-sync [--config env-sync.config.mjs] [--dry-run]
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { envSync } from '../src/index.js';
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const val = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d; };
|
|
9
|
+
const cfgPath = resolve(process.cwd(), val('--config', 'env-sync.config.mjs'));
|
|
10
|
+
const mod = await import(pathToFileURL(cfgPath).href).catch((e) => { console.error(`✗ config introuvable : ${cfgPath}\n ${e.message}`); process.exit(2); });
|
|
11
|
+
const config = mod.default || mod.config || mod;
|
|
12
|
+
const dryRun = args.includes('--dry-run');
|
|
13
|
+
try {
|
|
14
|
+
const r = envSync(config, { dryRun });
|
|
15
|
+
console.log(`▶ Clés ${dryRun ? '(dry-run) ' : ''}poussées vers ${config.target?.ssh ? config.target.ssh + ':' : ''}${config.target?.file || ''} :`);
|
|
16
|
+
for (const p of r.pushed) console.log(` ${p.key}=${p.value}`);
|
|
17
|
+
console.log(dryRun ? '✓ dry-run (rien écrit)' : '✓ env synchronisé');
|
|
18
|
+
} catch (e) { console.error(`✗ ${e.message}`); process.exit(1); }
|
package/llms.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# @mostajs/env-sync
|
|
2
|
+
Upsert idempotent de clés gérées dans un .env (local ou distant SSH), en PRÉSERVANT le reste. Zéro-dép. Membre de mosta-deploy-stack.
|
|
3
|
+
## API
|
|
4
|
+
envSync({source:'.env.production'|{K:V}, keys:[...], target:{file}|{ssh,file,reload?}}, {dryRun}) → {pushed:[{key,value(masquée)}],target}
|
|
5
|
+
parseEnv · buildBlock(source,keys) · upsertLocal(text,keys,block) · buildRemoteScript(file,keys,block,reload) · mask(k,v)
|
|
6
|
+
CLI: mosta-env-sync --config env-sync.config.mjs [--dry-run]
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mostajs/env-sync",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Upsert idempotent de clés gérées dans un .env (local ou distant SSH) en préservant le reste — pour les déploiements. Piloté par un manifeste. Zéro-dépendance. Membre de mosta-deploy-stack.",
|
|
5
|
+
"license": "AGPL-3.0-or-later",
|
|
6
|
+
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "src/index.js",
|
|
9
|
+
"bin": {
|
|
10
|
+
"mosta-env-sync": "bin/cli.mjs"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"bin",
|
|
15
|
+
"llms.txt",
|
|
16
|
+
"README.md",
|
|
17
|
+
"CHANGELOG.md"
|
|
18
|
+
],
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./src/index.js"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mostajs",
|
|
24
|
+
"env",
|
|
25
|
+
"dotenv",
|
|
26
|
+
"deploy",
|
|
27
|
+
"ssh",
|
|
28
|
+
"idempotent"
|
|
29
|
+
],
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@mostajs/mjs-unit": "^0.3.0"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "bash test-scripts/run-tests.sh"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// @mostajs/env-sync — upsert idempotent de clés gérées dans un .env (local ou distant SSH), en PRÉSERVANT le reste. Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
2
|
+
// Remplace les set-prod-env.sh / set-demo-env.sh bespoke : on ne pousse QUE les clés gérées (depuis une source), le reste du .env distant est intact.
|
|
3
|
+
// Zéro-dépendance (node:fs + node:child_process pour SSH). Membre de mosta-deploy-stack.
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
const SECRET = /(PASS|SECRET|TOKEN|KEY|API)/i;
|
|
8
|
+
export const mask = (k, v) => (SECRET.test(k) ? '********' : v);
|
|
9
|
+
|
|
10
|
+
/** Parse un texte .env → objet { clé: valeur } (ignore commentaires et lignes vides). */
|
|
11
|
+
export function parseEnv(text = '') {
|
|
12
|
+
const o = {};
|
|
13
|
+
for (const line of String(text).split('\n')) {
|
|
14
|
+
const t = line.trim(); if (!t || t.startsWith('#')) continue;
|
|
15
|
+
const i = t.indexOf('='); if (i < 0) continue;
|
|
16
|
+
o[t.slice(0, i).trim()] = t.slice(i + 1);
|
|
17
|
+
}
|
|
18
|
+
return o;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Bloc à pousser = les clés gérées présentes dans la source, dans l'ordre demandé. */
|
|
22
|
+
export function buildBlock(source, keys) {
|
|
23
|
+
const env = typeof source === 'object' && source !== null ? source : parseEnv(source);
|
|
24
|
+
return keys.filter((k) => k in env).map((k) => `${k}=${env[k]}`).join('\n');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Upsert local : retire les clés gérées du texte cible puis ajoute le bloc (idempotent). */
|
|
28
|
+
export function upsertLocal(targetText = '', keys, block) {
|
|
29
|
+
const drop = new Set(keys);
|
|
30
|
+
const kept = String(targetText).split('\n').filter((l) => { const m = l.match(/^([A-Za-z_][A-Za-z0-9_]*)=/); return !(m && drop.has(m[1])); });
|
|
31
|
+
while (kept.length && kept[kept.length - 1].trim() === '') kept.pop();
|
|
32
|
+
return (kept.join('\n') + (kept.length ? '\n' : '') + block + '\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Script shell distant : (option backup) sauvegarde, retire les clés gérées du .env, ajoute le bloc, puis (option) recharge. */
|
|
36
|
+
export function buildRemoteScript(file, keys, block, reload = '', backup = false) {
|
|
37
|
+
const pattern = '^(' + keys.join('|') + ')=';
|
|
38
|
+
return `set -e\n`
|
|
39
|
+
+ (backup ? `[ -f '${file}' ] && cp '${file}' "${file}.bak-$(date +%Y%m%d-%H%M%S)" || true\n` : '')
|
|
40
|
+
+ `grep -vE '${pattern}' '${file}' > '${file}.tmp' 2>/dev/null || true\n`
|
|
41
|
+
+ `cat >> '${file}.tmp' <<'__MOSTA_ENV__'\n${block}\n__MOSTA_ENV__\n`
|
|
42
|
+
+ `mv '${file}.tmp' '${file}'\n`
|
|
43
|
+
+ (reload ? `${reload}\n` : '');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Pousse les clés gérées vers la cible. config = {
|
|
48
|
+
* source: '.env.production' | { K:V }, keys: [...],
|
|
49
|
+
* target: { file } (local) | { ssh:'host', file:'/chemin/.env', reload?:'pm2 …' },
|
|
50
|
+
* } opts: { dryRun }. Renvoie { pushed:[{key,value(masquée)}], target }.
|
|
51
|
+
*/
|
|
52
|
+
export function envSync(config = {}, { dryRun = false } = {}) {
|
|
53
|
+
const { source, keys = [], target = {} } = config;
|
|
54
|
+
if (!keys.length) throw new Error('@mostajs/env-sync: config.keys requis');
|
|
55
|
+
const srcText = typeof source === 'string' && existsSync(source) ? readFileSync(source, 'utf8') : source;
|
|
56
|
+
const env = typeof srcText === 'object' && srcText !== null ? srcText : parseEnv(srcText || '');
|
|
57
|
+
const block = buildBlock(env, keys);
|
|
58
|
+
const pushed = keys.filter((k) => k in env).map((k) => ({ key: k, value: mask(k, env[k]) }));
|
|
59
|
+
if (dryRun) return { dryRun: true, pushed, target, block };
|
|
60
|
+
if (target.ssh) {
|
|
61
|
+
execFileSync('ssh', [target.ssh, 'bash', '-s'], { input: buildRemoteScript(target.file, keys, block, target.reload || '', target.backup), stdio: ['pipe', 'inherit', 'inherit'] });
|
|
62
|
+
} else if (target.file) {
|
|
63
|
+
const cur = existsSync(target.file) ? readFileSync(target.file, 'utf8') : '';
|
|
64
|
+
writeFileSync(target.file, upsertLocal(cur, keys, block));
|
|
65
|
+
} else throw new Error('@mostajs/env-sync: target.file ou target.ssh requis');
|
|
66
|
+
return { pushed, target };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default { envSync, parseEnv, buildBlock, upsertLocal, buildRemoteScript, mask };
|