hazo_env 0.8.0 → 0.9.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/CHANGE_LOG.md +9 -0
- package/README.md +2 -1
- package/dist/cli.js +11 -3
- package/dist/envsync/engine.d.ts +8 -0
- package/dist/envsync/engine.d.ts.map +1 -1
- package/dist/envsync/engine.js +33 -23
- package/dist/envsync/service.d.ts.map +1 -1
- package/dist/envsync/service.js +3 -0
- package/package.json +1 -1
package/CHANGE_LOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# hazo_env — Change Log
|
|
2
2
|
|
|
3
|
+
## 0.9.0 — 2026-07-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **`upload-files --skip-env`** (`uploadFiles({ skipEnv: true })`) — restore `files_root` from an archive while leaving the target `.env.local` **completely untouched**: no read, no merge, no backup, no write. For destinations whose env is hand-maintained and must survive a files refresh (e.g. a laptop dev site whose local URLs/secrets differ from the prod archive's — the common case where the default env-merge diff is all `~` clobbers). When set, the returned `diff` is empty (no env comparison is performed) and the CLI prints `.env.local skipped (--skip-env)` in place of the diff.
|
|
7
|
+
- `src/envsync/engine.ts`: `uploadFiles` gains an optional `skipEnv` flag. Files are still wiped-and-repopulated and the stray `__env.local` archive member is still stripped from `files_root`; only the env half is opted out. Orthogonal to `confirm`/`allowProd` — a dry-run with `skipEnv` returns `{ ok: false, diff: [] }` and writes nothing.
|
|
8
|
+
- `src/cli.ts`: `hazo-env sync upload-files --skip-env`.
|
|
9
|
+
- `src/envsync/service.ts`: `POST /files/restore` accepts `{ "skipEnv": true }`; the `serve` control page gains a "skip .env.local" checkbox.
|
|
10
|
+
- **Tests:** `__tests__/envsync-engine-files.test.ts` adds confirmed-restore-with-skipEnv (files replaced, `.env.local` byte-identical, no `.bak-` written) and dry-run-with-skipEnv (empty diff, zero writes) cases.
|
|
11
|
+
|
|
3
12
|
## 0.8.0 — 2026-07-23
|
|
4
13
|
|
|
5
14
|
### Removed / Breaking
|
package/README.md
CHANGED
|
@@ -165,6 +165,7 @@ hazo-env sync upload-db --dump <path> --confirm # pg_restore into target_db
|
|
|
165
165
|
hazo-env sync download-files # tar files_root (+ current .env.local) → work_dir
|
|
166
166
|
hazo-env sync upload-files --archive <path> # dry-run: prints the .env.local diff it would apply
|
|
167
167
|
hazo-env sync upload-files --archive <path> --confirm # actually restores files + merges env overrides
|
|
168
|
+
hazo-env sync upload-files --archive <path> --confirm --skip-env # restore files only; leave .env.local untouched
|
|
168
169
|
|
|
169
170
|
hazo-env serve [--port <n>] [--bind <host>] # HTTP service over the same engine — requires HAZO_ENVSYNC_TOKEN
|
|
170
171
|
```
|
|
@@ -178,7 +179,7 @@ hazo-env current # prints env, role, pattern, app, data_ro
|
|
|
178
179
|
hazo-env doctor [--env <e>] [--all] # red/green validation table
|
|
179
180
|
|
|
180
181
|
hazo-env sync download-db | upload-db --dump <p> [--confirm] [--allow-prod-target]
|
|
181
|
-
hazo-env sync download-files | upload-files --archive <p> [--confirm] [--allow-prod-target]
|
|
182
|
+
hazo-env sync download-files | upload-files --archive <p> [--confirm] [--allow-prod-target] [--skip-env]
|
|
182
183
|
hazo-env serve [--port <n>] [--bind <host>] # requires HAZO_ENVSYNC_TOKEN
|
|
183
184
|
```
|
|
184
185
|
|
package/dist/cli.js
CHANGED
|
@@ -88,6 +88,7 @@ ${pc.bold('hazo-env sync')} — local Postgres/files sync (pg_dump/pg_restore/ta
|
|
|
88
88
|
hazo-env sync download-files Archive files_root (+ .env.local) to work_dir
|
|
89
89
|
hazo-env sync upload-files --archive <path> Restore an archive into files_root + .env.local
|
|
90
90
|
--confirm --allow-prod-target Required to apply (else prints a diff preview)
|
|
91
|
+
--skip-env Restore files only; leave .env.local untouched
|
|
91
92
|
`);
|
|
92
93
|
return;
|
|
93
94
|
}
|
|
@@ -128,14 +129,20 @@ ${pc.bold('hazo-env sync')} — local Postgres/files sync (pg_dump/pg_restore/ta
|
|
|
128
129
|
const archivePath = archiveIdx >= 0 ? subArgs[archiveIdx + 1] : undefined;
|
|
129
130
|
const confirm = subArgs.includes('--confirm');
|
|
130
131
|
const allowProd = subArgs.includes('--allow-prod-target');
|
|
132
|
+
const skipEnv = subArgs.includes('--skip-env');
|
|
131
133
|
if (!archivePath) {
|
|
132
134
|
console.error(pc.red('Error: sync upload-files requires --archive <path>'));
|
|
133
135
|
process.exit(1);
|
|
134
136
|
}
|
|
135
|
-
console.log(`\n${pc.bold('hazo-env sync upload-files')} ${pc.dim(archivePath)}${confirm ? '' : pc.dim(' (preview — pass --confirm to apply)')}\n`);
|
|
136
|
-
const result = await withLock(cfg.work_dir, 'upload_files', () => uploadFiles(cfg, archivePath, { confirm, allowProd, onProgress }));
|
|
137
|
+
console.log(`\n${pc.bold('hazo-env sync upload-files')} ${pc.dim(archivePath)}${skipEnv ? pc.dim(' (--skip-env: .env.local left untouched)') : ''}${confirm ? '' : pc.dim(' (preview — pass --confirm to apply)')}\n`);
|
|
138
|
+
const result = await withLock(cfg.work_dir, 'upload_files', () => uploadFiles(cfg, archivePath, { confirm, allowProd, skipEnv, onProgress }));
|
|
137
139
|
console.log('');
|
|
138
|
-
|
|
140
|
+
if (skipEnv) {
|
|
141
|
+
console.log(` ${pc.dim('· .env.local skipped (--skip-env) — no env diff computed, target left as-is')}`);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
printEnvOverrideDiff(result.diff);
|
|
145
|
+
}
|
|
139
146
|
if (result.ok) {
|
|
140
147
|
console.log(`\n ${pc.green('✓')} Restored -> ${cfg.files_root}\n`);
|
|
141
148
|
}
|
|
@@ -189,6 +196,7 @@ Usage:
|
|
|
189
196
|
hazo-env sync download-files Archive files_root (+ .env.local) to work_dir
|
|
190
197
|
hazo-env sync upload-files --archive <p> Restore an archive into files_root + .env.local
|
|
191
198
|
--confirm --allow-prod-target Required to apply (else prints a diff preview)
|
|
199
|
+
--skip-env Restore files only; leave .env.local untouched
|
|
192
200
|
hazo-env serve Start the envsync HTTP service (HAZO_ENVSYNC_TOKEN required)
|
|
193
201
|
--port <n> --bind <host> Override HAZO_ENVSYNC_PORT / HAZO_ENVSYNC_BIND
|
|
194
202
|
`);
|
package/dist/envsync/engine.d.ts
CHANGED
|
@@ -66,6 +66,14 @@ export declare function downloadFiles(cfg: EnvsyncConfig, opts?: {
|
|
|
66
66
|
export declare function uploadFiles(cfg: EnvsyncConfig, archivePath: string, opts: {
|
|
67
67
|
confirm?: boolean;
|
|
68
68
|
allowProd?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Restore files_root but leave the target `.env.local` completely
|
|
71
|
+
* untouched — no backup, no merge, no write. Use when the destination
|
|
72
|
+
* env is hand-maintained (e.g. a laptop dev site whose local URLs/secrets
|
|
73
|
+
* must survive a prod files refresh). When set, the returned `diff` is
|
|
74
|
+
* empty because no env comparison is performed.
|
|
75
|
+
*/
|
|
76
|
+
skipEnv?: boolean;
|
|
69
77
|
onProgress?: (msg: string) => void;
|
|
70
78
|
}): Promise<{
|
|
71
79
|
ok: true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/envsync/engine.ts"],"names":[],"mappings":"AAaA,OAAO,EAKL,KAAK,eAAe,EACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,YAAY,EAAE,eAAe,EAAE,CAAC;AAEhC,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAaD;;GAEG;AACH,wBAAsB,UAAU,CAC9B,GAAG,EAAE,aAAa,EAClB,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAAE,GAC7E,OAAO,CAAC,iBAAiB,CAAC,CAY5B;AAED;;;;GAIG;AACH,wBAAsB,QAAQ,CAC5B,GAAG,EAAE,aAAa,EAClB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GACnF,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,CAAC,CAwBvB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,aAAa,EAClB,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAAE,GAC7E,OAAO,CAAC,iBAAiB,CAAC,CA0B5B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,aAAa,EAClB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE;
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/envsync/engine.ts"],"names":[],"mappings":"AAaA,OAAO,EAKL,KAAK,eAAe,EACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,YAAY,EAAE,eAAe,EAAE,CAAC;AAEhC,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAaD;;GAEG;AACH,wBAAsB,UAAU,CAC9B,GAAG,EAAE,aAAa,EAClB,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAAE,GAC7E,OAAO,CAAC,iBAAiB,CAAC,CAY5B;AAED;;;;GAIG;AACH,wBAAsB,QAAQ,CAC5B,GAAG,EAAE,aAAa,EAClB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GACnF,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,CAAC,CAwBvB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,aAAa,EAClB,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAAE,GAC7E,OAAO,CAAC,iBAAiB,CAAC,CA0B5B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,aAAa,EAClB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE;IACJ,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC,GACA,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,eAAe,EAAE,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,eAAe,EAAE,CAAA;CAAE,CAAC,CAgEzF"}
|
package/dist/envsync/engine.js
CHANGED
|
@@ -109,33 +109,40 @@ export async function downloadFiles(cfg, opts) {
|
|
|
109
109
|
* the archive, and writes the merged .env.local.
|
|
110
110
|
*/
|
|
111
111
|
export async function uploadFiles(cfg, archivePath, opts) {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
112
|
+
const targetEnvLocalPath = path.join(process.cwd(), '.env.local');
|
|
113
|
+
// When skipEnv is set we never read/merge/diff the archive's env member —
|
|
114
|
+
// the env half of upload-files is opted out of entirely, so the diff is
|
|
115
|
+
// empty and no .env.local is ever backed up or written below.
|
|
116
|
+
let diff = [];
|
|
117
|
+
let merged = {};
|
|
118
|
+
if (!opts.skipEnv) {
|
|
119
|
+
const extractDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hazo_envsync_extract_'));
|
|
120
|
+
let archiveEnvText = '';
|
|
121
|
+
try {
|
|
122
|
+
await runCmd('tar', ['-xzf', archivePath, '-C', extractDir, ENV_LOCAL_MEMBER], {
|
|
123
|
+
onProgress: opts.onProgress,
|
|
124
|
+
});
|
|
125
|
+
const extractedEnvLocal = path.join(extractDir, ENV_LOCAL_MEMBER);
|
|
126
|
+
if (fs.existsSync(extractedEnvLocal)) {
|
|
127
|
+
archiveEnvText = fs.readFileSync(extractedEnvLocal, 'utf8');
|
|
128
|
+
}
|
|
121
129
|
}
|
|
130
|
+
finally {
|
|
131
|
+
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
132
|
+
}
|
|
133
|
+
const archiveEnv = parseDotenvText(archiveEnvText);
|
|
134
|
+
merged = mergeEnvOverrides(archiveEnv, cfg.envOverrides);
|
|
135
|
+
const currentEnvText = fs.existsSync(targetEnvLocalPath)
|
|
136
|
+
? fs.readFileSync(targetEnvLocalPath, 'utf8')
|
|
137
|
+
: '';
|
|
138
|
+
const currentEnv = parseDotenvText(currentEnvText);
|
|
139
|
+
diff = diffEnvAgainstCurrent(merged, currentEnv);
|
|
122
140
|
}
|
|
123
|
-
finally {
|
|
124
|
-
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
125
|
-
}
|
|
126
|
-
const archiveEnv = parseDotenvText(archiveEnvText);
|
|
127
|
-
const merged = mergeEnvOverrides(archiveEnv, cfg.envOverrides);
|
|
128
|
-
const targetEnvLocalPath = path.join(process.cwd(), '.env.local');
|
|
129
|
-
const currentEnvText = fs.existsSync(targetEnvLocalPath)
|
|
130
|
-
? fs.readFileSync(targetEnvLocalPath, 'utf8')
|
|
131
|
-
: '';
|
|
132
|
-
const currentEnv = parseDotenvText(currentEnvText);
|
|
133
|
-
const diff = diffEnvAgainstCurrent(merged, currentEnv);
|
|
134
141
|
if (!opts.confirm) {
|
|
135
142
|
return { ok: false, diff };
|
|
136
143
|
}
|
|
137
144
|
assertNotProd(cfg.target_db, cfg.prodDbNames, opts.allowProd);
|
|
138
|
-
if (fs.existsSync(targetEnvLocalPath)) {
|
|
145
|
+
if (!opts.skipEnv && fs.existsSync(targetEnvLocalPath)) {
|
|
139
146
|
const ts = Date.now();
|
|
140
147
|
fs.copyFileSync(targetEnvLocalPath, `${targetEnvLocalPath}.bak-${ts}`);
|
|
141
148
|
}
|
|
@@ -146,10 +153,13 @@ export async function uploadFiles(cfg, archivePath, opts) {
|
|
|
146
153
|
await runCmd('tar', ['-xzf', archivePath, '-C', cfg.files_root], { onProgress: opts.onProgress });
|
|
147
154
|
// __env.local extracts as a real file inside files_root — it isn't a real
|
|
148
155
|
// asset, so remove it rather than trying to get tar to exclude it during
|
|
149
|
-
// extraction (simplest correct approach per spec).
|
|
156
|
+
// extraction (simplest correct approach per spec). Done regardless of
|
|
157
|
+
// skipEnv: the member is in the archive either way.
|
|
150
158
|
const extractedEnvMember = path.join(cfg.files_root, ENV_LOCAL_MEMBER);
|
|
151
159
|
if (fs.existsSync(extractedEnvMember))
|
|
152
160
|
fs.rmSync(extractedEnvMember, { force: true });
|
|
153
|
-
|
|
161
|
+
if (!opts.skipEnv) {
|
|
162
|
+
fs.writeFileSync(targetEnvLocalPath, serializeDotenv(merged));
|
|
163
|
+
}
|
|
154
164
|
return { ok: true, diff };
|
|
155
165
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/envsync/service.ts"],"names":[],"mappings":"AA4BA,OAAO,IAAI,MAAM,WAAW,CAAC;AAgB7B,MAAM,WAAW,0BAA0B;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;IACpB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/envsync/service.ts"],"names":[],"mappings":"AA4BA,OAAO,IAAI,MAAM,WAAW,CAAC;AAgB7B,MAAM,WAAW,0BAA0B;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;IACpB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AA+VD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,GAAE,0BAA+B,GAAG,oBAAoB,CA8B/F"}
|
package/dist/envsync/service.js
CHANGED
|
@@ -220,6 +220,7 @@ async function handleFilesRestore(req, res) {
|
|
|
220
220
|
const result = await withLock(cfg.work_dir, 'upload_files', () => uploadFiles(cfg, archivePath, {
|
|
221
221
|
confirm: body['confirm'] === true,
|
|
222
222
|
allowProd: body['allowProd'] === true,
|
|
223
|
+
skipEnv: body['skipEnv'] === true,
|
|
223
224
|
onProgress: progressLogger('upload_files'),
|
|
224
225
|
}));
|
|
225
226
|
sendJson(res, 200, result);
|
|
@@ -286,6 +287,7 @@ function controlPageHtml() {
|
|
|
286
287
|
<div class="row">
|
|
287
288
|
<label><input type="checkbox" id="filesConfirm" /> confirm</label>
|
|
288
289
|
<label><input type="checkbox" id="filesAllowProd" /> allow prod</label>
|
|
290
|
+
<label><input type="checkbox" id="filesSkipEnv" /> skip .env.local</label>
|
|
289
291
|
<button id="filesRestore" type="button">Restore Files</button>
|
|
290
292
|
</div>
|
|
291
293
|
</section>
|
|
@@ -335,6 +337,7 @@ document.getElementById('filesRestore').addEventListener('click', function () {
|
|
|
335
337
|
archivePath: document.getElementById('archivePath').value,
|
|
336
338
|
confirm: document.getElementById('filesConfirm').checked,
|
|
337
339
|
allowProd: document.getElementById('filesAllowProd').checked,
|
|
340
|
+
skipEnv: document.getElementById('filesSkipEnv').checked,
|
|
338
341
|
});
|
|
339
342
|
});
|
|
340
343
|
|
package/package.json
CHANGED