cf-envsync 0.1.0 → 0.3.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/README.md +125 -9
- package/dist/index.js +334 -47
- package/package.json +4 -3
- package/src/types/config.ts +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,20 @@
|
|
|
7
7
|
|
|
8
8
|
<h1 align="center">envsync</h1>
|
|
9
9
|
|
|
10
|
+
<p align="center">
|
|
11
|
+
<a href="https://www.npmjs.com/package/cf-envsync"><img src="https://img.shields.io/npm/v/cf-envsync.svg" alt="npm version" /></a>
|
|
12
|
+
<a href="https://www.npmjs.com/package/cf-envsync"><img src="https://img.shields.io/npm/dm/cf-envsync.svg" alt="npm downloads" /></a>
|
|
13
|
+
<a href="https://github.com/hakkokimkr/cf-envsync/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/cf-envsync.svg" alt="license" /></a>
|
|
14
|
+
<a href="https://github.com/hakkokimkr/cf-envsync"><img src="https://img.shields.io/github/stars/hakkokimkr/cf-envsync.svg?style=social" alt="GitHub stars" /></a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
<a href="https://nodejs.org/"><img src="https://img.shields.io/node/v/cf-envsync.svg" alt="node version" /></a>
|
|
19
|
+
<a href="https://github.com/hakkokimkr/cf-envsync"><img src="https://img.shields.io/github/last-commit/hakkokimkr/cf-envsync.svg" alt="last commit" /></a>
|
|
20
|
+
<a href="https://github.com/hakkokimkr/cf-envsync/issues"><img src="https://img.shields.io/github/issues/hakkokimkr/cf-envsync.svg" alt="issues" /></a>
|
|
21
|
+
<img src="https://img.shields.io/badge/TypeScript-100%25-blue.svg" alt="TypeScript" />
|
|
22
|
+
</p>
|
|
23
|
+
|
|
10
24
|
<p align="center">
|
|
11
25
|
One <code>.env</code> file. Every Worker. Every environment.<br />
|
|
12
26
|
No SaaS. No dashboard. Just your repo.
|
|
@@ -47,7 +61,14 @@ Every existing tool solves one piece. **envsync connects them all.**
|
|
|
47
61
|
## Quick Start
|
|
48
62
|
|
|
49
63
|
```bash
|
|
64
|
+
# bun
|
|
50
65
|
bun add -d cf-envsync
|
|
66
|
+
|
|
67
|
+
# npm
|
|
68
|
+
npm install -D cf-envsync
|
|
69
|
+
|
|
70
|
+
# pnpm
|
|
71
|
+
pnpm add -D cf-envsync
|
|
51
72
|
```
|
|
52
73
|
|
|
53
74
|
```bash
|
|
@@ -68,7 +89,7 @@ envsync validate
|
|
|
68
89
|
|
|
69
90
|
- [Node.js](https://nodejs.org) >= 18 or [Bun](https://bun.sh)
|
|
70
91
|
- [wrangler](https://developers.cloudflare.com/workers/wrangler/) CLI (peer dependency, for push/pull/diff)
|
|
71
|
-
- [dotenvx](https://dotenvx.com) (optional,
|
|
92
|
+
- [dotenvx](https://dotenvx.com) (optional, only if using `encryption: "dotenvx"`)
|
|
72
93
|
|
|
73
94
|
---
|
|
74
95
|
|
|
@@ -263,10 +284,11 @@ envsync init --monorepo # Scans for wrangler.jsonc files
|
|
|
263
284
|
```
|
|
264
285
|
|
|
265
286
|
What it does:
|
|
287
|
+
- Asks for encryption method (`password`, `dotenvx`, or `none`)
|
|
266
288
|
- Scans `wrangler.jsonc` files to discover workers and environments
|
|
267
289
|
- Detects shared secrets across apps
|
|
268
290
|
- Creates `envsync.config.ts`, `.env.example`, and empty `.env.{environment}` files
|
|
269
|
-
- Adds `.env.local`, `.env.keys`, `**/.dev.vars` to `.gitignore`
|
|
291
|
+
- Adds `.env.local`, `.env.keys`, `.env.password`, `**/.dev.vars` to `.gitignore`
|
|
270
292
|
- Registers the custom Git merge driver in `.gitattributes`
|
|
271
293
|
|
|
272
294
|
---
|
|
@@ -282,9 +304,33 @@ envsync normalize .env.staging # Specific file
|
|
|
282
304
|
|
|
283
305
|
---
|
|
284
306
|
|
|
307
|
+
### `envsync encrypt` — Encrypt plain values
|
|
308
|
+
|
|
309
|
+
Encrypts plain-text values in a `.env` file using password-based encryption (AES-256-GCM). Only available when `encryption: "password"`.
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
envsync encrypt staging # Encrypt all plain values in .env.staging
|
|
313
|
+
envsync encrypt production # Encrypt .env.production
|
|
314
|
+
envsync encrypt staging --dry-run # Preview without writing
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
$ envsync encrypt staging
|
|
319
|
+
|
|
320
|
+
DATABASE_URL: encrypted
|
|
321
|
+
API_KEY: encrypted
|
|
322
|
+
JWT_SECRET: encrypted
|
|
323
|
+
|
|
324
|
+
Encrypted 3 values in .env.staging (0 skipped)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Already-encrypted and empty values are skipped automatically.
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
285
331
|
### `envsync merge` — Git merge driver
|
|
286
332
|
|
|
287
|
-
A 3-way merge driver that understands dotenvx encryption. Registered automatically by `envsync init`.
|
|
333
|
+
A 3-way merge driver that understands both dotenvx and password encryption. Registered automatically by `envsync init`.
|
|
288
334
|
|
|
289
335
|
```
|
|
290
336
|
# .gitattributes (auto-generated)
|
|
@@ -294,10 +340,10 @@ A 3-way merge driver that understands dotenvx encryption. Registered automatical
|
|
|
294
340
|
|
|
295
341
|
How it works:
|
|
296
342
|
|
|
297
|
-
1. Decrypts all three versions (base, ours, theirs)
|
|
343
|
+
1. Decrypts all three versions (base, ours, theirs) — supports both dotenvx and password encryption
|
|
298
344
|
2. 3-way merge at the **key level** — not the encrypted ciphertext
|
|
299
345
|
3. Only real conflicts get conflict markers
|
|
300
|
-
4. Re-encrypts the merged result
|
|
346
|
+
4. Re-encrypts the merged result (password mode uses `encryptEnvMap`, dotenvx uses `dotenvx encrypt`)
|
|
301
347
|
|
|
302
348
|
No more fake conflicts from identical values with different ciphertext.
|
|
303
349
|
|
|
@@ -408,7 +454,7 @@ export default {
|
|
|
408
454
|
| `envFiles.pattern` | `string` | File naming pattern. `{env}` is replaced. `local` falls back to `.env` |
|
|
409
455
|
| `envFiles.local` | `string` | Per-developer override file (gitignored) |
|
|
410
456
|
| `envFiles.perApp` | `boolean` | Allow per-app `.env.{env}` files for app-specific overrides |
|
|
411
|
-
| `encryption` | `"dotenvx" \| "none"` | Encryption method for `.env` files |
|
|
457
|
+
| `encryption` | `"password" \| "dotenvx" \| "none"` | Encryption method for `.env` files |
|
|
412
458
|
| `apps.{name}.path` | `string` | Path to app directory relative to project root |
|
|
413
459
|
| `apps.{name}.workers` | `Record<string, string>` | Worker name per environment |
|
|
414
460
|
| `apps.{name}.secrets` | `string[]` | Secret keys pushed via `wrangler secret bulk` |
|
|
@@ -421,6 +467,48 @@ export default {
|
|
|
421
467
|
|
|
422
468
|
Config file search order: `envsync.config.ts` > `.js` > `.mjs` > `envsync.json` > `envsync.jsonc`
|
|
423
469
|
|
|
470
|
+
### Encryption
|
|
471
|
+
|
|
472
|
+
envsync supports three encryption modes:
|
|
473
|
+
|
|
474
|
+
| Mode | How it works | Dependencies |
|
|
475
|
+
|------|-------------|--------------|
|
|
476
|
+
| `"password"` | AES-256-GCM, per-value encryption with a shared password. Values stored as `envsync:v1:{base64}`. | None (uses `node:crypto`) |
|
|
477
|
+
| `"dotenvx"` | ECIES public/private key encryption via dotenvx CLI. | `dotenvx` CLI |
|
|
478
|
+
| `"none"` | No encryption. `.env` files stored in plain text. | None |
|
|
479
|
+
|
|
480
|
+
#### Password encryption
|
|
481
|
+
|
|
482
|
+
Per-value encryption means each key-value pair is encrypted independently — git diffs are readable at the key level, and merges work cleanly.
|
|
483
|
+
|
|
484
|
+
```
|
|
485
|
+
# .env.staging (committed, encrypted)
|
|
486
|
+
DATABASE_URL=envsync:v1:base64encodedpayload...
|
|
487
|
+
API_KEY=envsync:v1:base64encodedpayload...
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
**Password source** (checked in order):
|
|
491
|
+
|
|
492
|
+
1. `ENVSYNC_PASSWORD_{ENV}` env var (e.g. `ENVSYNC_PASSWORD_STAGING`)
|
|
493
|
+
2. `ENVSYNC_PASSWORD` env var (generic fallback)
|
|
494
|
+
3. `.env.password` file with `ENVSYNC_PASSWORD_{ENV}=xxx` (env-specific)
|
|
495
|
+
4. `.env.password` file with `ENVSYNC_PASSWORD=xxx` (generic fallback)
|
|
496
|
+
|
|
497
|
+
```bash
|
|
498
|
+
# Quick setup
|
|
499
|
+
echo "ENVSYNC_PASSWORD=your-strong-password" > .env.password
|
|
500
|
+
|
|
501
|
+
# Or per-environment passwords
|
|
502
|
+
cat > .env.password << 'EOF'
|
|
503
|
+
ENVSYNC_PASSWORD_STAGING=staging-password
|
|
504
|
+
ENVSYNC_PASSWORD_PRODUCTION=production-password
|
|
505
|
+
EOF
|
|
506
|
+
|
|
507
|
+
# Encrypt plain values
|
|
508
|
+
envsync encrypt staging
|
|
509
|
+
envsync encrypt production
|
|
510
|
+
```
|
|
511
|
+
|
|
424
512
|
### File structure
|
|
425
513
|
|
|
426
514
|
```
|
|
@@ -433,6 +521,7 @@ project/
|
|
|
433
521
|
├── .env.local # Per-developer overrides (gitignored)
|
|
434
522
|
├── .env.example # Key reference (committed)
|
|
435
523
|
├── .env.keys # dotenvx private keys (gitignored)
|
|
524
|
+
├── .env.password # Password encryption keys (gitignored)
|
|
436
525
|
│
|
|
437
526
|
├── apps/
|
|
438
527
|
│ ├── api/
|
|
@@ -447,7 +536,7 @@ project/
|
|
|
447
536
|
│ ├── .dev.vars # ← generated
|
|
448
537
|
│ └── .env # app-specific secrets (YOUTUBE_API_KEY, etc.)
|
|
449
538
|
│
|
|
450
|
-
└── .gitignore # .env.local, .env.keys, **/.dev.vars
|
|
539
|
+
└── .gitignore # .env.local, .env.keys, .env.password, **/.dev.vars
|
|
451
540
|
```
|
|
452
541
|
|
|
453
542
|
### Merge priority
|
|
@@ -494,12 +583,39 @@ export default defineConfig({
|
|
|
494
583
|
<tr><td><strong>.dev.vars</strong></td><td>Local dev secrets</td><td>Doesn't sync with anything</td></tr>
|
|
495
584
|
</table>
|
|
496
585
|
|
|
497
|
-
**envsync** fills the gap: encrypted `.env` files as the single source of truth, synced to every target — Workers secrets, `.dev.vars`, validation — with monorepo and multi-environment support built in.
|
|
586
|
+
**envsync** fills the gap: encrypted `.env` files as the single source of truth, synced to every target — Workers secrets, `.dev.vars`, validation — with monorepo and multi-environment support built in. Choose password encryption (zero dependencies, per-value, merge-friendly) or dotenvx (ECIES key pairs).
|
|
498
587
|
|
|
499
588
|
No SaaS. No dashboard. Just a CLI, your `.env` files, and Cloudflare's API.
|
|
500
589
|
|
|
501
590
|
---
|
|
502
591
|
|
|
592
|
+
## Testing
|
|
593
|
+
|
|
594
|
+
150 tests across 20 files covering utils, core modules, and all 10 commands.
|
|
595
|
+
|
|
596
|
+
```bash
|
|
597
|
+
bun test # Run tests
|
|
598
|
+
bun test --coverage # Run with coverage report
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### Coverage
|
|
602
|
+
|
|
603
|
+
| File | % Funcs | % Lines |
|
|
604
|
+
|------|---------|---------|
|
|
605
|
+
| **All files** | **83.93** | **76.13** |
|
|
606
|
+
| src/core/resolver.ts | 100.00 | 100.00 |
|
|
607
|
+
| src/core/wrangler.ts | 100.00 | 100.00 |
|
|
608
|
+
| src/core/env-file.ts | 100.00 | 98.65 |
|
|
609
|
+
| src/core/encryption.ts | 100.00 | 95.65 |
|
|
610
|
+
| src/utils/fs.ts | 100.00 | 97.44 |
|
|
611
|
+
| src/core/config.ts | 71.43 | 76.23 |
|
|
612
|
+
| src/commands/merge.ts | 75.00 | 28.40 |
|
|
613
|
+
| src/utils/output.ts | 25.00 | 12.70 |
|
|
614
|
+
|
|
615
|
+
> `merge.ts` and `output.ts` line coverage is lower because their command handlers and print functions are tested via CLI integration tests (spawned subprocesses), which bun's coverage instrumentation does not trace into.
|
|
616
|
+
|
|
617
|
+
---
|
|
618
|
+
|
|
503
619
|
## Tech Stack
|
|
504
620
|
|
|
505
621
|
| | |
|
|
@@ -508,7 +624,7 @@ No SaaS. No dashboard. Just a CLI, your `.env` files, and Cloudflare's API.
|
|
|
508
624
|
| **CLI framework** | [citty](https://github.com/unjs/citty) |
|
|
509
625
|
| **Output** | [consola](https://github.com/unjs/consola) |
|
|
510
626
|
| **Config loading** | [jiti](https://github.com/unjs/jiti) |
|
|
511
|
-
| **Encryption** | [@dotenvx/dotenvx](https://dotenvx.com) |
|
|
627
|
+
| **Encryption** | `node:crypto` AES-256-GCM (password mode) / [@dotenvx/dotenvx](https://dotenvx.com) (dotenvx mode) |
|
|
512
628
|
| **CF Secrets** | [wrangler](https://developers.cloudflare.com/workers/wrangler/) CLI (shell out) |
|
|
513
629
|
|
|
514
630
|
---
|
package/dist/index.js
CHANGED
|
@@ -13007,6 +13007,9 @@ var require_main2 = __commonJS((exports, module) => {
|
|
|
13007
13007
|
});
|
|
13008
13008
|
|
|
13009
13009
|
// src/core/encryption.ts
|
|
13010
|
+
import { readFileSync } from "node:fs";
|
|
13011
|
+
import { join as join2 } from "node:path";
|
|
13012
|
+
import { scryptSync, randomBytes, createCipheriv, createDecipheriv } from "node:crypto";
|
|
13010
13013
|
function decryptEnvContent(content, privateKey) {
|
|
13011
13014
|
if (privateKey) {
|
|
13012
13015
|
const prev = process.env.DOTENV_PRIVATE_KEY;
|
|
@@ -13023,27 +13026,154 @@ function decryptEnvContent(content, privateKey) {
|
|
|
13023
13026
|
}
|
|
13024
13027
|
return import_dotenvx.parse(content);
|
|
13025
13028
|
}
|
|
13026
|
-
function
|
|
13029
|
+
function loadEnvKeysFileSync(filePath) {
|
|
13030
|
+
try {
|
|
13031
|
+
const content = readFileSync(filePath, "utf-8");
|
|
13032
|
+
const result = {};
|
|
13033
|
+
for (const line of content.split(`
|
|
13034
|
+
`)) {
|
|
13035
|
+
const trimmed = line.trim();
|
|
13036
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
13037
|
+
continue;
|
|
13038
|
+
const eqIdx = trimmed.indexOf("=");
|
|
13039
|
+
if (eqIdx === -1)
|
|
13040
|
+
continue;
|
|
13041
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
13042
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
13043
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
13044
|
+
value = value.slice(1, -1);
|
|
13045
|
+
}
|
|
13046
|
+
result[key] = value;
|
|
13047
|
+
}
|
|
13048
|
+
return result;
|
|
13049
|
+
} catch {
|
|
13050
|
+
return {};
|
|
13051
|
+
}
|
|
13052
|
+
}
|
|
13053
|
+
function findPrivateKey(env2, projectRoot) {
|
|
13027
13054
|
if (env2) {
|
|
13028
13055
|
const envKey = `DOTENV_PRIVATE_KEY_${env2.toUpperCase()}`;
|
|
13029
13056
|
if (process.env[envKey])
|
|
13030
13057
|
return process.env[envKey];
|
|
13031
13058
|
}
|
|
13032
|
-
|
|
13059
|
+
if (process.env.DOTENV_PRIVATE_KEY)
|
|
13060
|
+
return process.env.DOTENV_PRIVATE_KEY;
|
|
13061
|
+
if (projectRoot) {
|
|
13062
|
+
const keysFile = loadEnvKeysFileSync(join2(projectRoot, ".env.keys"));
|
|
13063
|
+
if (env2) {
|
|
13064
|
+
const envKey = `DOTENV_PRIVATE_KEY_${env2.toUpperCase()}`;
|
|
13065
|
+
if (keysFile[envKey])
|
|
13066
|
+
return keysFile[envKey];
|
|
13067
|
+
}
|
|
13068
|
+
if (keysFile.DOTENV_PRIVATE_KEY)
|
|
13069
|
+
return keysFile.DOTENV_PRIVATE_KEY;
|
|
13070
|
+
}
|
|
13071
|
+
return;
|
|
13072
|
+
}
|
|
13073
|
+
function isEnvsyncEncrypted(value) {
|
|
13074
|
+
return value.startsWith(ENVSYNC_PREFIX);
|
|
13075
|
+
}
|
|
13076
|
+
function encryptValue(plaintext, password) {
|
|
13077
|
+
const salt = randomBytes(16);
|
|
13078
|
+
const iv = randomBytes(12);
|
|
13079
|
+
const key = scryptSync(password, salt, 32);
|
|
13080
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
13081
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
13082
|
+
const tag = cipher.getAuthTag();
|
|
13083
|
+
const payload = Buffer.concat([salt, iv, encrypted, tag]);
|
|
13084
|
+
return ENVSYNC_PREFIX + payload.toString("base64");
|
|
13085
|
+
}
|
|
13086
|
+
function decryptValue(token, password) {
|
|
13087
|
+
if (!token.startsWith(ENVSYNC_PREFIX)) {
|
|
13088
|
+
throw new Error("Not an envsync-encrypted value");
|
|
13089
|
+
}
|
|
13090
|
+
const payload = Buffer.from(token.slice(ENVSYNC_PREFIX.length), "base64");
|
|
13091
|
+
const salt = payload.subarray(0, 16);
|
|
13092
|
+
const iv = payload.subarray(16, 28);
|
|
13093
|
+
const tag = payload.subarray(payload.length - 16);
|
|
13094
|
+
const encrypted = payload.subarray(28, payload.length - 16);
|
|
13095
|
+
const key = scryptSync(password, salt, 32);
|
|
13096
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
13097
|
+
decipher.setAuthTag(tag);
|
|
13098
|
+
return decipher.update(encrypted) + decipher.final("utf8");
|
|
13099
|
+
}
|
|
13100
|
+
function decryptEnvMap(envMap, password) {
|
|
13101
|
+
const result = {};
|
|
13102
|
+
for (const [key, value] of Object.entries(envMap)) {
|
|
13103
|
+
result[key] = isEnvsyncEncrypted(value) ? decryptValue(value, password) : value;
|
|
13104
|
+
}
|
|
13105
|
+
return result;
|
|
13106
|
+
}
|
|
13107
|
+
function encryptEnvMap(envMap, password) {
|
|
13108
|
+
const result = {};
|
|
13109
|
+
for (const [key, value] of Object.entries(envMap)) {
|
|
13110
|
+
if (isEnvsyncEncrypted(value) || value === "") {
|
|
13111
|
+
result[key] = value;
|
|
13112
|
+
} else {
|
|
13113
|
+
result[key] = encryptValue(value, password);
|
|
13114
|
+
}
|
|
13115
|
+
}
|
|
13116
|
+
return result;
|
|
13117
|
+
}
|
|
13118
|
+
function findPassword(env2, projectRoot) {
|
|
13119
|
+
if (env2) {
|
|
13120
|
+
const envKey = `ENVSYNC_PASSWORD_${env2.toUpperCase()}`;
|
|
13121
|
+
if (process.env[envKey])
|
|
13122
|
+
return process.env[envKey];
|
|
13123
|
+
}
|
|
13124
|
+
if (process.env.ENVSYNC_PASSWORD)
|
|
13125
|
+
return process.env.ENVSYNC_PASSWORD;
|
|
13126
|
+
if (projectRoot) {
|
|
13127
|
+
const passwordFile = loadEnvKeysFileSync(join2(projectRoot, ".env.password"));
|
|
13128
|
+
if (env2) {
|
|
13129
|
+
const envKey = `ENVSYNC_PASSWORD_${env2.toUpperCase()}`;
|
|
13130
|
+
if (passwordFile[envKey])
|
|
13131
|
+
return passwordFile[envKey];
|
|
13132
|
+
}
|
|
13133
|
+
if (passwordFile.ENVSYNC_PASSWORD)
|
|
13134
|
+
return passwordFile.ENVSYNC_PASSWORD;
|
|
13135
|
+
}
|
|
13136
|
+
return;
|
|
13033
13137
|
}
|
|
13034
|
-
var import_dotenvx;
|
|
13138
|
+
var import_dotenvx, ENVSYNC_PREFIX = "envsync:v1:";
|
|
13035
13139
|
var init_encryption = __esm(() => {
|
|
13036
13140
|
import_dotenvx = __toESM(require_main2(), 1);
|
|
13037
13141
|
});
|
|
13038
13142
|
|
|
13039
13143
|
// src/core/env-file.ts
|
|
13040
|
-
import { join as
|
|
13041
|
-
|
|
13144
|
+
import { join as join3 } from "node:path";
|
|
13145
|
+
function parsePlainEnv(content) {
|
|
13146
|
+
const result = {};
|
|
13147
|
+
for (const line of content.split(`
|
|
13148
|
+
`)) {
|
|
13149
|
+
const trimmed = line.trim();
|
|
13150
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
13151
|
+
continue;
|
|
13152
|
+
const eqIdx = trimmed.indexOf("=");
|
|
13153
|
+
if (eqIdx === -1)
|
|
13154
|
+
continue;
|
|
13155
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
13156
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
13157
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
13158
|
+
value = value.slice(1, -1);
|
|
13159
|
+
}
|
|
13160
|
+
result[key] = value;
|
|
13161
|
+
}
|
|
13162
|
+
return result;
|
|
13163
|
+
}
|
|
13164
|
+
async function loadEnvFile(filePath, env2, projectRoot, encryption) {
|
|
13042
13165
|
if (!fileExists(filePath)) {
|
|
13043
13166
|
return {};
|
|
13044
13167
|
}
|
|
13045
13168
|
const content = await readFile(filePath);
|
|
13046
|
-
|
|
13169
|
+
if (encryption === "password") {
|
|
13170
|
+
const envMap = parsePlainEnv(content);
|
|
13171
|
+
const password = findPassword(env2, projectRoot);
|
|
13172
|
+
if (!password)
|
|
13173
|
+
return envMap;
|
|
13174
|
+
return decryptEnvMap(envMap, password);
|
|
13175
|
+
}
|
|
13176
|
+
const privateKey = findPrivateKey(env2, projectRoot);
|
|
13047
13177
|
return decryptEnvContent(content, privateKey);
|
|
13048
13178
|
}
|
|
13049
13179
|
async function writeEnvFile(filePath, envMap) {
|
|
@@ -13087,15 +13217,15 @@ function filterForApp(envMap, app) {
|
|
|
13087
13217
|
function getRootEnvPath(config, environment) {
|
|
13088
13218
|
const pattern = config.raw.envFiles.pattern;
|
|
13089
13219
|
const relativePath = resolveEnvFilePath(pattern, environment);
|
|
13090
|
-
return
|
|
13220
|
+
return join3(config.projectRoot, relativePath);
|
|
13091
13221
|
}
|
|
13092
13222
|
function getAppEnvPath(config, app, environment) {
|
|
13093
13223
|
const pattern = config.raw.envFiles.pattern;
|
|
13094
13224
|
const relativePath = resolveEnvFilePath(pattern, environment);
|
|
13095
|
-
return
|
|
13225
|
+
return join3(app.absolutePath, relativePath);
|
|
13096
13226
|
}
|
|
13097
13227
|
function getLocalOverridePath(config) {
|
|
13098
|
-
return
|
|
13228
|
+
return join3(config.projectRoot, config.raw.envFiles.local);
|
|
13099
13229
|
}
|
|
13100
13230
|
var init_env_file = __esm(() => {
|
|
13101
13231
|
init_encryption();
|
|
@@ -13104,17 +13234,19 @@ var init_env_file = __esm(() => {
|
|
|
13104
13234
|
});
|
|
13105
13235
|
|
|
13106
13236
|
// src/core/resolver.ts
|
|
13237
|
+
import { normalize } from "node:path";
|
|
13107
13238
|
async function resolveAppEnv(config, app, environment) {
|
|
13108
13239
|
const layers = [];
|
|
13240
|
+
const encryption = config.raw.encryption;
|
|
13109
13241
|
const rootEnvPath = getRootEnvPath(config, environment);
|
|
13110
|
-
const rootEnv = await loadEnvFile(rootEnvPath, environment);
|
|
13242
|
+
const rootEnv = await loadEnvFile(rootEnvPath, environment, config.projectRoot, encryption);
|
|
13111
13243
|
if (Object.keys(rootEnv).length > 0) {
|
|
13112
13244
|
layers.push({ source: rootEnvPath, map: rootEnv });
|
|
13113
13245
|
}
|
|
13114
13246
|
if (config.raw.envFiles.perApp) {
|
|
13115
13247
|
const appEnvPath = getAppEnvPath(config, app, environment);
|
|
13116
|
-
if (appEnvPath !== rootEnvPath) {
|
|
13117
|
-
const appEnv = await loadEnvFile(appEnvPath, environment);
|
|
13248
|
+
if (normalize(appEnvPath) !== normalize(rootEnvPath)) {
|
|
13249
|
+
const appEnv = await loadEnvFile(appEnvPath, environment, config.projectRoot, encryption);
|
|
13118
13250
|
if (Object.keys(appEnv).length > 0) {
|
|
13119
13251
|
layers.push({ source: appEnvPath, map: appEnv });
|
|
13120
13252
|
}
|
|
@@ -13158,7 +13290,7 @@ var exports_dev = {};
|
|
|
13158
13290
|
__export(exports_dev, {
|
|
13159
13291
|
default: () => dev_default
|
|
13160
13292
|
});
|
|
13161
|
-
import { join as
|
|
13293
|
+
import { join as join4, relative } from "node:path";
|
|
13162
13294
|
function parseAppNames(args) {
|
|
13163
13295
|
const rest = args._;
|
|
13164
13296
|
return rest?.length ? rest : undefined;
|
|
@@ -13220,7 +13352,7 @@ var init_dev = __esm(() => {
|
|
|
13220
13352
|
const localEnv = await loadEnvFile(localOverridePath);
|
|
13221
13353
|
for (const app of apps) {
|
|
13222
13354
|
const resolved = await resolveAppEnv(config, app, environment);
|
|
13223
|
-
const devVarsPath =
|
|
13355
|
+
const devVarsPath = join4(app.absolutePath, ".dev.vars");
|
|
13224
13356
|
const relDevVars = relative(config.projectRoot, devVarsPath);
|
|
13225
13357
|
if (Object.keys(resolved.map).length === 0) {
|
|
13226
13358
|
consola.warn(` No env vars resolved for ${app.name}. Skipping.`);
|
|
@@ -13565,7 +13697,7 @@ var init_pull = __esm(() => {
|
|
|
13565
13697
|
}
|
|
13566
13698
|
consola.info(` Found ${remoteKeys.length} remote keys`);
|
|
13567
13699
|
const envFilePath = getRootEnvPath(config, environment);
|
|
13568
|
-
const localEnv = await loadEnvFile(envFilePath, environment);
|
|
13700
|
+
const localEnv = await loadEnvFile(envFilePath, environment, config.projectRoot, config.raw.encryption);
|
|
13569
13701
|
const localKeys = new Set(Object.keys(localEnv));
|
|
13570
13702
|
const missingKeys = remoteKeys.filter((k2) => !localKeys.has(k2));
|
|
13571
13703
|
if (missingKeys.length === 0) {
|
|
@@ -13590,7 +13722,7 @@ var exports_validate = {};
|
|
|
13590
13722
|
__export(exports_validate, {
|
|
13591
13723
|
default: () => validate_default
|
|
13592
13724
|
});
|
|
13593
|
-
import { join as
|
|
13725
|
+
import { join as join5 } from "node:path";
|
|
13594
13726
|
function parseAppNames4(args, skip = 1) {
|
|
13595
13727
|
const rest = args._?.slice(skip);
|
|
13596
13728
|
return rest?.length ? rest : undefined;
|
|
@@ -13638,7 +13770,7 @@ var init_validate = __esm(() => {
|
|
|
13638
13770
|
appNames = undefined;
|
|
13639
13771
|
}
|
|
13640
13772
|
const apps = resolveApps(config, appNames);
|
|
13641
|
-
const examplePath =
|
|
13773
|
+
const examplePath = join5(config.projectRoot, ".env.example");
|
|
13642
13774
|
if (!fileExists(examplePath)) {
|
|
13643
13775
|
consola.error("No .env.example found at project root.");
|
|
13644
13776
|
process.exit(1);
|
|
@@ -13736,10 +13868,10 @@ function printDiff(entries) {
|
|
|
13736
13868
|
for (const entry of entries) {
|
|
13737
13869
|
switch (entry.status) {
|
|
13738
13870
|
case "added":
|
|
13739
|
-
consola.log(` + ${entry.key} = ${maskValue(entry.
|
|
13871
|
+
consola.log(` + ${entry.key} = ${maskValue(entry.localValue)}`);
|
|
13740
13872
|
break;
|
|
13741
13873
|
case "removed":
|
|
13742
|
-
consola.log(` - ${entry.key}
|
|
13874
|
+
consola.log(` - ${entry.key}`);
|
|
13743
13875
|
break;
|
|
13744
13876
|
case "changed":
|
|
13745
13877
|
consola.log(` ~ ${entry.key}`);
|
|
@@ -13917,7 +14049,7 @@ var exports_init = {};
|
|
|
13917
14049
|
__export(exports_init, {
|
|
13918
14050
|
default: () => init_default
|
|
13919
14051
|
});
|
|
13920
|
-
import { join as
|
|
14052
|
+
import { join as join6, relative as relative2, basename } from "node:path";
|
|
13921
14053
|
function generateConfigTS(config) {
|
|
13922
14054
|
const lines = [
|
|
13923
14055
|
`import { defineConfig } from "cf-envsync";`,
|
|
@@ -13998,7 +14130,7 @@ var init_init = __esm(() => {
|
|
|
13998
14130
|
},
|
|
13999
14131
|
async run({ args }) {
|
|
14000
14132
|
const cwd = process.cwd();
|
|
14001
|
-
const existingConfig = CONFIG_FILES.find((f3) => fileExists(
|
|
14133
|
+
const existingConfig = CONFIG_FILES.find((f3) => fileExists(join6(cwd, f3)));
|
|
14002
14134
|
if (existingConfig) {
|
|
14003
14135
|
consola.warn(`${existingConfig} already exists.`);
|
|
14004
14136
|
const overwrite = await consola.prompt("Overwrite?", {
|
|
@@ -14011,7 +14143,7 @@ var init_init = __esm(() => {
|
|
|
14011
14143
|
}
|
|
14012
14144
|
const encryption = await consola.prompt("Encryption method:", {
|
|
14013
14145
|
type: "select",
|
|
14014
|
-
options: ["dotenvx", "none"]
|
|
14146
|
+
options: ["password", "dotenvx", "none"]
|
|
14015
14147
|
});
|
|
14016
14148
|
const defaultEnvs = "local, staging, production";
|
|
14017
14149
|
const envsInput = await consola.prompt("Environments (comma-separated):", {
|
|
@@ -14031,8 +14163,8 @@ var init_init = __esm(() => {
|
|
|
14031
14163
|
consola.warn("No wrangler config files found. Creating manually.");
|
|
14032
14164
|
}
|
|
14033
14165
|
for (const wranglerFile of wranglerFiles.sort()) {
|
|
14034
|
-
const fullPath =
|
|
14035
|
-
const appDir =
|
|
14166
|
+
const fullPath = join6(cwd, wranglerFile);
|
|
14167
|
+
const appDir = join6(cwd, wranglerFile, "..");
|
|
14036
14168
|
const appPath = relative2(cwd, appDir);
|
|
14037
14169
|
const appName = basename(appDir);
|
|
14038
14170
|
consola.info(` Found ${wranglerFile}`);
|
|
@@ -14133,10 +14265,10 @@ var init_init = __esm(() => {
|
|
|
14133
14265
|
apps,
|
|
14134
14266
|
...shared.length > 0 ? { shared } : {}
|
|
14135
14267
|
};
|
|
14136
|
-
const configPath =
|
|
14268
|
+
const configPath = join6(cwd, "envsync.config.ts");
|
|
14137
14269
|
const tsContent = generateConfigTS(config);
|
|
14138
14270
|
if (existingConfig && existingConfig !== "envsync.config.ts") {
|
|
14139
|
-
const oldPath =
|
|
14271
|
+
const oldPath = join6(cwd, existingConfig);
|
|
14140
14272
|
if (fileExists(oldPath)) {
|
|
14141
14273
|
const { unlink } = await import("node:fs/promises");
|
|
14142
14274
|
await unlink(oldPath);
|
|
@@ -14144,7 +14276,7 @@ var init_init = __esm(() => {
|
|
|
14144
14276
|
}
|
|
14145
14277
|
await writeFile(configPath, tsContent);
|
|
14146
14278
|
consola.success("Created envsync.config.ts");
|
|
14147
|
-
const examplePath =
|
|
14279
|
+
const examplePath = join6(cwd, ".env.example");
|
|
14148
14280
|
if (!fileExists(examplePath)) {
|
|
14149
14281
|
const allKeys = new Set;
|
|
14150
14282
|
for (const app of Object.values(apps)) {
|
|
@@ -14167,21 +14299,21 @@ var init_init = __esm(() => {
|
|
|
14167
14299
|
for (const env2 of environments) {
|
|
14168
14300
|
if (env2 === "local")
|
|
14169
14301
|
continue;
|
|
14170
|
-
const envFile =
|
|
14302
|
+
const envFile = join6(cwd, `.env.${env2}`);
|
|
14171
14303
|
if (!fileExists(envFile)) {
|
|
14172
14304
|
await writeFile(envFile, `# ${env2} environment variables
|
|
14173
14305
|
`);
|
|
14174
14306
|
consola.success(`Created .env.${env2}`);
|
|
14175
14307
|
}
|
|
14176
14308
|
}
|
|
14177
|
-
const rootEnv =
|
|
14309
|
+
const rootEnv = join6(cwd, ".env");
|
|
14178
14310
|
if (!fileExists(rootEnv)) {
|
|
14179
14311
|
await writeFile(rootEnv, `# Local environment variables
|
|
14180
14312
|
`);
|
|
14181
14313
|
consola.success("Created .env");
|
|
14182
14314
|
}
|
|
14183
|
-
const gitignorePath =
|
|
14184
|
-
const gitignoreEntries = [".env.local", ".env.keys", "**/.dev.vars"];
|
|
14315
|
+
const gitignorePath = join6(cwd, ".gitignore");
|
|
14316
|
+
const gitignoreEntries = [".env.local", ".env.keys", ".env.password", "**/.dev.vars"];
|
|
14185
14317
|
if (fileExists(gitignorePath)) {
|
|
14186
14318
|
const existing = await readFile(gitignorePath);
|
|
14187
14319
|
const toAdd = gitignoreEntries.filter((e2) => !existing.includes(e2));
|
|
@@ -14201,7 +14333,7 @@ var init_init = __esm(() => {
|
|
|
14201
14333
|
`);
|
|
14202
14334
|
consola.success(`Created .gitignore`);
|
|
14203
14335
|
}
|
|
14204
|
-
const gitattrsPath =
|
|
14336
|
+
const gitattrsPath = join6(cwd, ".gitattributes");
|
|
14205
14337
|
const mergeDriverLines = `.env merge=envsync
|
|
14206
14338
|
.env.* merge=envsync
|
|
14207
14339
|
`;
|
|
@@ -14217,7 +14349,7 @@ var init_init = __esm(() => {
|
|
|
14217
14349
|
await writeFile(gitattrsPath, mergeDriverLines);
|
|
14218
14350
|
consola.success("Created .gitattributes with merge driver");
|
|
14219
14351
|
}
|
|
14220
|
-
const gitConfigPath =
|
|
14352
|
+
const gitConfigPath = join6(cwd, ".git", "config");
|
|
14221
14353
|
if (fileExists(gitConfigPath)) {
|
|
14222
14354
|
const gitConfig = await readFile(gitConfigPath);
|
|
14223
14355
|
if (!gitConfig.includes('[merge "envsync"]')) {
|
|
@@ -14232,14 +14364,18 @@ var init_init = __esm(() => {
|
|
|
14232
14364
|
}
|
|
14233
14365
|
consola.info(`
|
|
14234
14366
|
Next steps:`);
|
|
14235
|
-
if (encryption === "
|
|
14367
|
+
if (encryption === "password") {
|
|
14368
|
+
consola.info(' 1. Set a password: echo "ENVSYNC_PASSWORD=your-secret" > .env.password');
|
|
14369
|
+
consola.info(" 2. Add values to your .env files (plain KEY=VALUE)");
|
|
14370
|
+
consola.info(" 3. envsync encrypt staging (encrypts plain values in .env.staging)");
|
|
14371
|
+
} else if (encryption === "dotenvx") {
|
|
14236
14372
|
consola.info(' 1. dotenvx set DATABASE_URL "value" -f .env');
|
|
14237
14373
|
for (const env2 of environments.filter((e2) => e2 !== "local")) {
|
|
14238
14374
|
consola.info(` 2. dotenvx set DATABASE_URL "${env2}_value" -f .env.${env2}`);
|
|
14239
14375
|
}
|
|
14240
14376
|
}
|
|
14241
|
-
consola.info(` 3. echo "OAUTH_REDIRECT_URL=https://your-tunnel/callback" >> .env.local`);
|
|
14242
|
-
consola.info("
|
|
14377
|
+
consola.info(` ${encryption === "password" ? "4" : "3"}. echo "OAUTH_REDIRECT_URL=https://your-tunnel/callback" >> .env.local`);
|
|
14378
|
+
consola.info(` ${encryption === "password" ? "5" : "4"}. envsync dev`);
|
|
14243
14379
|
}
|
|
14244
14380
|
});
|
|
14245
14381
|
});
|
|
@@ -14333,6 +14469,10 @@ var init_normalize = __esm(() => {
|
|
|
14333
14469
|
// src/commands/merge.ts
|
|
14334
14470
|
var exports_merge = {};
|
|
14335
14471
|
__export(exports_merge, {
|
|
14472
|
+
parseEnvLines: () => parseEnvLines,
|
|
14473
|
+
isPasswordEncrypted: () => isPasswordEncrypted,
|
|
14474
|
+
isEncrypted: () => isEncrypted,
|
|
14475
|
+
isDotenvxEncrypted: () => isDotenvxEncrypted,
|
|
14336
14476
|
default: () => merge_default
|
|
14337
14477
|
});
|
|
14338
14478
|
function parseEnvLines(content) {
|
|
@@ -14353,9 +14493,15 @@ function parseEnvLines(content) {
|
|
|
14353
14493
|
};
|
|
14354
14494
|
});
|
|
14355
14495
|
}
|
|
14356
|
-
function
|
|
14496
|
+
function isDotenvxEncrypted(content) {
|
|
14357
14497
|
return content.includes("encrypted:");
|
|
14358
14498
|
}
|
|
14499
|
+
function isPasswordEncrypted(content) {
|
|
14500
|
+
return content.includes("envsync:v1:");
|
|
14501
|
+
}
|
|
14502
|
+
function isEncrypted(content) {
|
|
14503
|
+
return isDotenvxEncrypted(content) || isPasswordEncrypted(content);
|
|
14504
|
+
}
|
|
14359
14505
|
var merge_default;
|
|
14360
14506
|
var init_merge = __esm(() => {
|
|
14361
14507
|
init_dist();
|
|
@@ -14391,11 +14537,24 @@ var init_merge = __esm(() => {
|
|
|
14391
14537
|
readFile(args.ours),
|
|
14392
14538
|
readFile(args.theirs)
|
|
14393
14539
|
]);
|
|
14394
|
-
const
|
|
14395
|
-
const
|
|
14396
|
-
const
|
|
14397
|
-
const
|
|
14398
|
-
const
|
|
14540
|
+
const wasDotenvx = isDotenvxEncrypted(oursContent);
|
|
14541
|
+
const wasPassword = isPasswordEncrypted(oursContent);
|
|
14542
|
+
const wasEncrypted = wasDotenvx || wasPassword;
|
|
14543
|
+
const privateKey = findPrivateKey(undefined, process.cwd());
|
|
14544
|
+
const password = findPassword(undefined, process.cwd());
|
|
14545
|
+
function decryptContent(content) {
|
|
14546
|
+
if (isPasswordEncrypted(content)) {
|
|
14547
|
+
const plain = Object.fromEntries(parseEnvLines(content).filter((e2) => e2.key).map((e2) => [e2.key, e2.value ?? ""]));
|
|
14548
|
+
return password ? decryptEnvMap(plain, password) : plain;
|
|
14549
|
+
}
|
|
14550
|
+
if (isDotenvxEncrypted(content)) {
|
|
14551
|
+
return decryptEnvContent(content, privateKey);
|
|
14552
|
+
}
|
|
14553
|
+
return Object.fromEntries(parseEnvLines(content).filter((e2) => e2.key).map((e2) => [e2.key, e2.value ?? ""]));
|
|
14554
|
+
}
|
|
14555
|
+
const baseParsed = decryptContent(baseContent);
|
|
14556
|
+
const oursParsed = decryptContent(oursContent);
|
|
14557
|
+
const theirsParsed = decryptContent(theirsContent);
|
|
14399
14558
|
const baseMap = new Map(Object.entries(baseParsed));
|
|
14400
14559
|
const oursMap = new Map(Object.entries(oursParsed));
|
|
14401
14560
|
const theirsMap = new Map(Object.entries(theirsParsed));
|
|
@@ -14452,10 +14611,19 @@ var init_merge = __esm(() => {
|
|
|
14452
14611
|
`) + `
|
|
14453
14612
|
`;
|
|
14454
14613
|
if (wasEncrypted && !hasConflicts) {
|
|
14455
|
-
|
|
14456
|
-
|
|
14457
|
-
|
|
14458
|
-
|
|
14614
|
+
if (wasPassword && password) {
|
|
14615
|
+
const plainMap = Object.fromEntries(parseEnvLines(mergedContent).filter((e2) => e2.key).map((e2) => [e2.key, e2.value ?? ""]));
|
|
14616
|
+
const encrypted = encryptEnvMap(plainMap, password);
|
|
14617
|
+
const encLines = Object.entries(encrypted).map(([k2, v2]) => `${k2}=${v2}`);
|
|
14618
|
+
await writeFile(args.ours, encLines.join(`
|
|
14619
|
+
`) + `
|
|
14620
|
+
`);
|
|
14621
|
+
} else {
|
|
14622
|
+
await writeFile(args.ours, mergedContent);
|
|
14623
|
+
const result = await exec(["dotenvx", "encrypt", "-f", args.ours]);
|
|
14624
|
+
if (!result.success) {
|
|
14625
|
+
consola.warn("Could not re-encrypt merged file:", result.stderr);
|
|
14626
|
+
}
|
|
14459
14627
|
}
|
|
14460
14628
|
} else {
|
|
14461
14629
|
await writeFile(args.ours, mergedContent);
|
|
@@ -14612,6 +14780,124 @@ var init_list = __esm(() => {
|
|
|
14612
14780
|
});
|
|
14613
14781
|
});
|
|
14614
14782
|
|
|
14783
|
+
// src/commands/encrypt.ts
|
|
14784
|
+
var exports_encrypt = {};
|
|
14785
|
+
__export(exports_encrypt, {
|
|
14786
|
+
default: () => encrypt_default
|
|
14787
|
+
});
|
|
14788
|
+
function parseLines(content) {
|
|
14789
|
+
return content.split(`
|
|
14790
|
+
`).map((line) => {
|
|
14791
|
+
const trimmed = line.trim();
|
|
14792
|
+
if (trimmed === "" || trimmed.startsWith("#")) {
|
|
14793
|
+
return { raw: line };
|
|
14794
|
+
}
|
|
14795
|
+
const eqIdx = line.indexOf("=");
|
|
14796
|
+
if (eqIdx === -1) {
|
|
14797
|
+
return { raw: line };
|
|
14798
|
+
}
|
|
14799
|
+
return {
|
|
14800
|
+
key: line.slice(0, eqIdx).trim(),
|
|
14801
|
+
value: line.slice(eqIdx + 1),
|
|
14802
|
+
raw: line
|
|
14803
|
+
};
|
|
14804
|
+
});
|
|
14805
|
+
}
|
|
14806
|
+
var encrypt_default;
|
|
14807
|
+
var init_encrypt = __esm(() => {
|
|
14808
|
+
init_dist();
|
|
14809
|
+
init_dist2();
|
|
14810
|
+
init_config();
|
|
14811
|
+
init_env_file();
|
|
14812
|
+
init_fs();
|
|
14813
|
+
init_encryption();
|
|
14814
|
+
encrypt_default = defineCommand({
|
|
14815
|
+
meta: {
|
|
14816
|
+
name: "encrypt",
|
|
14817
|
+
description: "Encrypt plain .env values with password encryption"
|
|
14818
|
+
},
|
|
14819
|
+
args: {
|
|
14820
|
+
env: {
|
|
14821
|
+
type: "positional",
|
|
14822
|
+
description: "Target environment (e.g. staging, production)",
|
|
14823
|
+
required: true
|
|
14824
|
+
},
|
|
14825
|
+
"dry-run": {
|
|
14826
|
+
type: "boolean",
|
|
14827
|
+
description: "Preview changes without writing",
|
|
14828
|
+
default: false
|
|
14829
|
+
}
|
|
14830
|
+
},
|
|
14831
|
+
async run({ args }) {
|
|
14832
|
+
const environment = args.env;
|
|
14833
|
+
const rawConfig = await loadConfig();
|
|
14834
|
+
const errors = validateConfig(rawConfig);
|
|
14835
|
+
if (errors.length > 0) {
|
|
14836
|
+
for (const err of errors)
|
|
14837
|
+
consola.error(err);
|
|
14838
|
+
process.exit(1);
|
|
14839
|
+
}
|
|
14840
|
+
const config = resolveConfig(rawConfig);
|
|
14841
|
+
if (config.raw.encryption !== "password") {
|
|
14842
|
+
consola.error('encrypt command requires encryption: "password" in config.');
|
|
14843
|
+
process.exit(1);
|
|
14844
|
+
}
|
|
14845
|
+
if (!config.environments.includes(environment)) {
|
|
14846
|
+
consola.error(`Unknown environment: "${environment}". Available: ${config.environments.join(", ")}`);
|
|
14847
|
+
process.exit(1);
|
|
14848
|
+
}
|
|
14849
|
+
const password = findPassword(environment, config.projectRoot);
|
|
14850
|
+
if (!password) {
|
|
14851
|
+
consola.error(`No password found. Set ENVSYNC_PASSWORD or ENVSYNC_PASSWORD_${environment.toUpperCase()}, or create .env.password`);
|
|
14852
|
+
process.exit(1);
|
|
14853
|
+
}
|
|
14854
|
+
const envFilePath = getRootEnvPath(config, environment);
|
|
14855
|
+
if (!fileExists(envFilePath)) {
|
|
14856
|
+
consola.error(`File not found: ${envFilePath}`);
|
|
14857
|
+
process.exit(1);
|
|
14858
|
+
}
|
|
14859
|
+
const content = await readFile(envFilePath);
|
|
14860
|
+
const lines = parseLines(content);
|
|
14861
|
+
let encryptedCount = 0;
|
|
14862
|
+
let skippedCount = 0;
|
|
14863
|
+
const outputLines = [];
|
|
14864
|
+
for (const line of lines) {
|
|
14865
|
+
if (!line.key || line.value === undefined) {
|
|
14866
|
+
outputLines.push(line.raw);
|
|
14867
|
+
continue;
|
|
14868
|
+
}
|
|
14869
|
+
const value = line.value.trim();
|
|
14870
|
+
if (isEnvsyncEncrypted(value) || value === "") {
|
|
14871
|
+
outputLines.push(line.raw);
|
|
14872
|
+
skippedCount++;
|
|
14873
|
+
continue;
|
|
14874
|
+
}
|
|
14875
|
+
let plainValue = value;
|
|
14876
|
+
if (plainValue.startsWith('"') && plainValue.endsWith('"') || plainValue.startsWith("'") && plainValue.endsWith("'")) {
|
|
14877
|
+
plainValue = plainValue.slice(1, -1);
|
|
14878
|
+
}
|
|
14879
|
+
const encrypted = encryptValue(plainValue, password);
|
|
14880
|
+
outputLines.push(`${line.key}=${encrypted}`);
|
|
14881
|
+
encryptedCount++;
|
|
14882
|
+
consola.log(` ${line.key}: encrypted`);
|
|
14883
|
+
}
|
|
14884
|
+
if (encryptedCount === 0) {
|
|
14885
|
+
consola.info("No values to encrypt.");
|
|
14886
|
+
return;
|
|
14887
|
+
}
|
|
14888
|
+
const dryRun = args["dry-run"];
|
|
14889
|
+
if (dryRun) {
|
|
14890
|
+
consola.info(`[dry-run] Would encrypt ${encryptedCount} values in ${envFilePath}`);
|
|
14891
|
+
return;
|
|
14892
|
+
}
|
|
14893
|
+
await writeFile(envFilePath, outputLines.join(`
|
|
14894
|
+
`) + `
|
|
14895
|
+
`);
|
|
14896
|
+
consola.success(`Encrypted ${encryptedCount} values in ${envFilePath} (${skippedCount} skipped)`);
|
|
14897
|
+
}
|
|
14898
|
+
});
|
|
14899
|
+
});
|
|
14900
|
+
|
|
14615
14901
|
// src/index.ts
|
|
14616
14902
|
init_dist();
|
|
14617
14903
|
var main = defineCommand({
|
|
@@ -14629,7 +14915,8 @@ var main = defineCommand({
|
|
|
14629
14915
|
init: () => Promise.resolve().then(() => (init_init(), exports_init)).then((m2) => m2.default),
|
|
14630
14916
|
normalize: () => Promise.resolve().then(() => (init_normalize(), exports_normalize)).then((m2) => m2.default),
|
|
14631
14917
|
merge: () => Promise.resolve().then(() => (init_merge(), exports_merge)).then((m2) => m2.default),
|
|
14632
|
-
list: () => Promise.resolve().then(() => (init_list(), exports_list)).then((m2) => m2.default)
|
|
14918
|
+
list: () => Promise.resolve().then(() => (init_list(), exports_list)).then((m2) => m2.default),
|
|
14919
|
+
encrypt: () => Promise.resolve().then(() => (init_encrypt(), exports_encrypt)).then((m2) => m2.default)
|
|
14633
14920
|
}
|
|
14634
14921
|
});
|
|
14635
14922
|
runMain(main);
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cf-envsync",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Sync .env files to Cloudflare Workers secrets, .dev.vars, and more",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/define-config.ts"
|
|
8
8
|
},
|
|
9
9
|
"bin": {
|
|
10
|
-
"envsync": "
|
|
10
|
+
"envsync": "dist/index.js"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"dist",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"dev": "bun run src/index.ts",
|
|
19
19
|
"build": "bun build src/index.ts --outdir dist --target node --external jiti",
|
|
20
20
|
"prepublishOnly": "bun run build",
|
|
21
|
-
"test": "bun test"
|
|
21
|
+
"test": "bun test",
|
|
22
|
+
"test:coverage": "bun test --coverage"
|
|
22
23
|
},
|
|
23
24
|
"peerDependencies": {
|
|
24
25
|
"wrangler": ">=3"
|