cf-envsync 0.2.0 → 0.3.1
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 +250 -9
- package/dist/index.js +444 -76
- package/package.json +3 -2
- package/src/types/config.ts +6 -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.
|
|
@@ -75,7 +89,136 @@ envsync validate
|
|
|
75
89
|
|
|
76
90
|
- [Node.js](https://nodejs.org) >= 18 or [Bun](https://bun.sh)
|
|
77
91
|
- [wrangler](https://developers.cloudflare.com/workers/wrangler/) CLI (peer dependency, for push/pull/diff)
|
|
78
|
-
- [dotenvx](https://dotenvx.com) (optional,
|
|
92
|
+
- [dotenvx](https://dotenvx.com) (optional, only if using `encryption: "dotenvx"`)
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 5-Minute Tutorial
|
|
97
|
+
|
|
98
|
+
A complete walkthrough: project setup → local dev → deploy to staging → validate.
|
|
99
|
+
|
|
100
|
+
### 1. Initialize your project
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# In your monorepo root
|
|
104
|
+
npm install -D cf-envsync
|
|
105
|
+
envsync init --monorepo
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
This scans for `wrangler.jsonc` files, discovers your workers, and generates:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
envsync.config.ts ← config with all apps/workers detected
|
|
112
|
+
.env.example ← key reference (committed to git)
|
|
113
|
+
.env ← local shared secrets
|
|
114
|
+
.env.staging ← staging secrets (empty, you'll fill these)
|
|
115
|
+
.env.production ← production secrets (empty)
|
|
116
|
+
.gitignore ← updated with .env.local, .env.password, **/.dev.vars
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 2. Fill in your secrets
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# .env — local development values (committed, encrypted)
|
|
123
|
+
DATABASE_URL=postgres://localhost:5432/mydb
|
|
124
|
+
JWT_SECRET=dev_jwt_secret
|
|
125
|
+
AUTH_SECRET=dev_auth_secret
|
|
126
|
+
API_URL=http://localhost:8787
|
|
127
|
+
|
|
128
|
+
# .env.staging — staging values
|
|
129
|
+
DATABASE_URL=postgres://staging-db.example.com/mydb
|
|
130
|
+
JWT_SECRET=staging_jwt_secret_abc
|
|
131
|
+
AUTH_SECRET=staging_auth_secret_xyz
|
|
132
|
+
API_URL=https://api-staging.example.com
|
|
133
|
+
|
|
134
|
+
# .env.local — YOUR dev-specific overrides (gitignored, each dev has their own)
|
|
135
|
+
OAUTH_REDIRECT_URL=https://my-tunnel.ngrok.io/callback
|
|
136
|
+
DEV_TUNNEL_URL=https://my-tunnel.ngrok.io
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 3. Encrypt before committing (optional)
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# Set a password
|
|
143
|
+
echo "ENVSYNC_PASSWORD=my-team-password" > .env.password
|
|
144
|
+
|
|
145
|
+
# Encrypt all plain values
|
|
146
|
+
envsync encrypt staging
|
|
147
|
+
envsync encrypt production
|
|
148
|
+
|
|
149
|
+
# Now .env.staging looks like:
|
|
150
|
+
# DATABASE_URL=envsync:v1:base64payload...
|
|
151
|
+
# JWT_SECRET=envsync:v1:base64payload...
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 4. Local development
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
envsync dev
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
This reads `.env` + `.env.local`, merges them, and writes `.dev.vars` into each app directory. Start wrangler as usual — it reads `.dev.vars` automatically.
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
apps/api/.dev.vars ← DATABASE_URL, JWT_SECRET, API_URL, OAUTH_REDIRECT_URL
|
|
164
|
+
apps/web/.dev.vars ← AUTH_SECRET, VITE_API_URL, VITE_OAUTH_REDIRECT_URL
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
> **Vite / non-wrangler apps:** `.dev.vars` is for wrangler only. If your app runs with `vite dev`, set `devFile` in your config:
|
|
168
|
+
>
|
|
169
|
+
> ```ts
|
|
170
|
+
> apps: {
|
|
171
|
+
> web: {
|
|
172
|
+
> path: "apps/web",
|
|
173
|
+
> devFile: ".env.local", // Vite reads this
|
|
174
|
+
> // or generate both:
|
|
175
|
+
> // devFile: [".dev.vars", ".env.local"],
|
|
176
|
+
> },
|
|
177
|
+
> }
|
|
178
|
+
> ```
|
|
179
|
+
|
|
180
|
+
If you forgot to set a per-dev override, envsync tells you:
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
⚠ Missing in .env.local: DEV_TUNNEL_URL (required per-dev override)
|
|
184
|
+
→ echo "DEV_TUNNEL_URL=https://your-tunnel.example.com" >> .env.local
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 5. Push to staging
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# Preview first
|
|
191
|
+
envsync push staging --dry-run
|
|
192
|
+
|
|
193
|
+
# Push for real
|
|
194
|
+
envsync push staging
|
|
195
|
+
# Push 4 secrets to worker "my-api-staging" (staging)? yes
|
|
196
|
+
# ✓ Pushed 4 secrets to my-api-staging
|
|
197
|
+
# Push 1 secrets to worker "my-web-staging" (staging)? yes
|
|
198
|
+
# ✓ Pushed 1 secrets to my-web-staging
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### 6. Validate before deploying
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
envsync validate
|
|
205
|
+
# Checks every app × every environment against .env.example
|
|
206
|
+
# Exit code 1 if anything is missing → safe for CI
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### 7. CI/CD integration
|
|
210
|
+
|
|
211
|
+
```yaml
|
|
212
|
+
# GitHub Actions example
|
|
213
|
+
- name: Validate env vars
|
|
214
|
+
run: envsync validate
|
|
215
|
+
|
|
216
|
+
- name: Push secrets to production
|
|
217
|
+
run: envsync push production --force
|
|
218
|
+
env:
|
|
219
|
+
ENVSYNC_PASSWORD: ${{ secrets.ENVSYNC_PASSWORD }}
|
|
220
|
+
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
|
|
221
|
+
```
|
|
79
222
|
|
|
80
223
|
---
|
|
81
224
|
|
|
@@ -116,6 +259,8 @@ Done!
|
|
|
116
259
|
|
|
117
260
|
Every key shows exactly where its value came from. Missing per-dev overrides are caught immediately.
|
|
118
261
|
|
|
262
|
+
> **Vite / non-wrangler apps:** Set `devFile: ".env.local"` in your app config. See [the tutorial](#4-local-development).
|
|
263
|
+
|
|
119
264
|
---
|
|
120
265
|
|
|
121
266
|
### `envsync push` — Deploy secrets
|
|
@@ -270,10 +415,11 @@ envsync init --monorepo # Scans for wrangler.jsonc files
|
|
|
270
415
|
```
|
|
271
416
|
|
|
272
417
|
What it does:
|
|
418
|
+
- Asks for encryption method (`password`, `dotenvx`, or `none`)
|
|
273
419
|
- Scans `wrangler.jsonc` files to discover workers and environments
|
|
274
420
|
- Detects shared secrets across apps
|
|
275
421
|
- Creates `envsync.config.ts`, `.env.example`, and empty `.env.{environment}` files
|
|
276
|
-
- Adds `.env.local`, `.env.keys`, `**/.dev.vars` to `.gitignore`
|
|
422
|
+
- Adds `.env.local`, `.env.keys`, `.env.password`, `**/.dev.vars` to `.gitignore`
|
|
277
423
|
- Registers the custom Git merge driver in `.gitattributes`
|
|
278
424
|
|
|
279
425
|
---
|
|
@@ -289,9 +435,33 @@ envsync normalize .env.staging # Specific file
|
|
|
289
435
|
|
|
290
436
|
---
|
|
291
437
|
|
|
438
|
+
### `envsync encrypt` — Encrypt plain values
|
|
439
|
+
|
|
440
|
+
Encrypts plain-text values in a `.env` file using password-based encryption (AES-256-GCM). Only available when `encryption: "password"`.
|
|
441
|
+
|
|
442
|
+
```bash
|
|
443
|
+
envsync encrypt staging # Encrypt all plain values in .env.staging
|
|
444
|
+
envsync encrypt production # Encrypt .env.production
|
|
445
|
+
envsync encrypt staging --dry-run # Preview without writing
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
```
|
|
449
|
+
$ envsync encrypt staging
|
|
450
|
+
|
|
451
|
+
DATABASE_URL: encrypted
|
|
452
|
+
API_KEY: encrypted
|
|
453
|
+
JWT_SECRET: encrypted
|
|
454
|
+
|
|
455
|
+
Encrypted 3 values in .env.staging (0 skipped)
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Already-encrypted and empty values are skipped automatically.
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
292
462
|
### `envsync merge` — Git merge driver
|
|
293
463
|
|
|
294
|
-
A 3-way merge driver that understands dotenvx encryption. Registered automatically by `envsync init`.
|
|
464
|
+
A 3-way merge driver that understands both dotenvx and password encryption. Registered automatically by `envsync init`.
|
|
295
465
|
|
|
296
466
|
```
|
|
297
467
|
# .gitattributes (auto-generated)
|
|
@@ -301,10 +471,10 @@ A 3-way merge driver that understands dotenvx encryption. Registered automatical
|
|
|
301
471
|
|
|
302
472
|
How it works:
|
|
303
473
|
|
|
304
|
-
1. Decrypts all three versions (base, ours, theirs)
|
|
474
|
+
1. Decrypts all three versions (base, ours, theirs) — supports both dotenvx and password encryption
|
|
305
475
|
2. 3-way merge at the **key level** — not the encrypted ciphertext
|
|
306
476
|
3. Only real conflicts get conflict markers
|
|
307
|
-
4. Re-encrypts the merged result
|
|
477
|
+
4. Re-encrypts the merged result (password mode uses `encryptEnvMap`, dotenvx uses `dotenvx encrypt`)
|
|
308
478
|
|
|
309
479
|
No more fake conflicts from identical values with different ciphertext.
|
|
310
480
|
|
|
@@ -415,11 +585,12 @@ export default {
|
|
|
415
585
|
| `envFiles.pattern` | `string` | File naming pattern. `{env}` is replaced. `local` falls back to `.env` |
|
|
416
586
|
| `envFiles.local` | `string` | Per-developer override file (gitignored) |
|
|
417
587
|
| `envFiles.perApp` | `boolean` | Allow per-app `.env.{env}` files for app-specific overrides |
|
|
418
|
-
| `encryption` | `"dotenvx" \| "none"` | Encryption method for `.env` files |
|
|
588
|
+
| `encryption` | `"password" \| "dotenvx" \| "none"` | Encryption method for `.env` files |
|
|
419
589
|
| `apps.{name}.path` | `string` | Path to app directory relative to project root |
|
|
420
590
|
| `apps.{name}.workers` | `Record<string, string>` | Worker name per environment |
|
|
421
591
|
| `apps.{name}.secrets` | `string[]` | Secret keys pushed via `wrangler secret bulk` |
|
|
422
592
|
| `apps.{name}.vars` | `string[]` | Non-secret env vars (not pushed as secrets) |
|
|
593
|
+
| `apps.{name}.devFile` | `string \| string[]` | Output file(s) for `envsync dev`. Default: `".dev.vars"`. Use `".env.local"` for Vite apps, or an array for both |
|
|
423
594
|
| `shared` | `string[]` | Keys with the same value across multiple apps |
|
|
424
595
|
| `local.overrides` | `string[]` | Keys each developer must set in `.env.local` |
|
|
425
596
|
| `local.perApp` | `Record<string, string[]>` | Per-app developer override keys |
|
|
@@ -428,6 +599,48 @@ export default {
|
|
|
428
599
|
|
|
429
600
|
Config file search order: `envsync.config.ts` > `.js` > `.mjs` > `envsync.json` > `envsync.jsonc`
|
|
430
601
|
|
|
602
|
+
### Encryption
|
|
603
|
+
|
|
604
|
+
envsync supports three encryption modes:
|
|
605
|
+
|
|
606
|
+
| Mode | How it works | Dependencies |
|
|
607
|
+
|------|-------------|--------------|
|
|
608
|
+
| `"password"` | AES-256-GCM, per-value encryption with a shared password. Values stored as `envsync:v1:{base64}`. | None (uses `node:crypto`) |
|
|
609
|
+
| `"dotenvx"` | ECIES public/private key encryption via dotenvx CLI. | `dotenvx` CLI |
|
|
610
|
+
| `"none"` | No encryption. `.env` files stored in plain text. | None |
|
|
611
|
+
|
|
612
|
+
#### Password encryption
|
|
613
|
+
|
|
614
|
+
Per-value encryption means each key-value pair is encrypted independently — git diffs are readable at the key level, and merges work cleanly.
|
|
615
|
+
|
|
616
|
+
```
|
|
617
|
+
# .env.staging (committed, encrypted)
|
|
618
|
+
DATABASE_URL=envsync:v1:base64encodedpayload...
|
|
619
|
+
API_KEY=envsync:v1:base64encodedpayload...
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
**Password source** (checked in order):
|
|
623
|
+
|
|
624
|
+
1. `ENVSYNC_PASSWORD_{ENV}` env var (e.g. `ENVSYNC_PASSWORD_STAGING`)
|
|
625
|
+
2. `ENVSYNC_PASSWORD` env var (generic fallback)
|
|
626
|
+
3. `.env.password` file with `ENVSYNC_PASSWORD_{ENV}=xxx` (env-specific)
|
|
627
|
+
4. `.env.password` file with `ENVSYNC_PASSWORD=xxx` (generic fallback)
|
|
628
|
+
|
|
629
|
+
```bash
|
|
630
|
+
# Quick setup
|
|
631
|
+
echo "ENVSYNC_PASSWORD=your-strong-password" > .env.password
|
|
632
|
+
|
|
633
|
+
# Or per-environment passwords
|
|
634
|
+
cat > .env.password << 'EOF'
|
|
635
|
+
ENVSYNC_PASSWORD_STAGING=staging-password
|
|
636
|
+
ENVSYNC_PASSWORD_PRODUCTION=production-password
|
|
637
|
+
EOF
|
|
638
|
+
|
|
639
|
+
# Encrypt plain values
|
|
640
|
+
envsync encrypt staging
|
|
641
|
+
envsync encrypt production
|
|
642
|
+
```
|
|
643
|
+
|
|
431
644
|
### File structure
|
|
432
645
|
|
|
433
646
|
```
|
|
@@ -440,6 +653,7 @@ project/
|
|
|
440
653
|
├── .env.local # Per-developer overrides (gitignored)
|
|
441
654
|
├── .env.example # Key reference (committed)
|
|
442
655
|
├── .env.keys # dotenvx private keys (gitignored)
|
|
656
|
+
├── .env.password # Password encryption keys (gitignored)
|
|
443
657
|
│
|
|
444
658
|
├── apps/
|
|
445
659
|
│ ├── api/
|
|
@@ -454,7 +668,7 @@ project/
|
|
|
454
668
|
│ ├── .dev.vars # ← generated
|
|
455
669
|
│ └── .env # app-specific secrets (YOUTUBE_API_KEY, etc.)
|
|
456
670
|
│
|
|
457
|
-
└── .gitignore # .env.local, .env.keys, **/.dev.vars
|
|
671
|
+
└── .gitignore # .env.local, .env.keys, .env.password, **/.dev.vars
|
|
458
672
|
```
|
|
459
673
|
|
|
460
674
|
### Merge priority
|
|
@@ -501,12 +715,39 @@ export default defineConfig({
|
|
|
501
715
|
<tr><td><strong>.dev.vars</strong></td><td>Local dev secrets</td><td>Doesn't sync with anything</td></tr>
|
|
502
716
|
</table>
|
|
503
717
|
|
|
504
|
-
**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.
|
|
718
|
+
**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).
|
|
505
719
|
|
|
506
720
|
No SaaS. No dashboard. Just a CLI, your `.env` files, and Cloudflare's API.
|
|
507
721
|
|
|
508
722
|
---
|
|
509
723
|
|
|
724
|
+
## Testing
|
|
725
|
+
|
|
726
|
+
150 tests across 20 files covering utils, core modules, and all 10 commands.
|
|
727
|
+
|
|
728
|
+
```bash
|
|
729
|
+
bun test # Run tests
|
|
730
|
+
bun test --coverage # Run with coverage report
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
### Coverage
|
|
734
|
+
|
|
735
|
+
| File | % Funcs | % Lines |
|
|
736
|
+
|------|---------|---------|
|
|
737
|
+
| **All files** | **83.93** | **76.13** |
|
|
738
|
+
| src/core/resolver.ts | 100.00 | 100.00 |
|
|
739
|
+
| src/core/wrangler.ts | 100.00 | 100.00 |
|
|
740
|
+
| src/core/env-file.ts | 100.00 | 98.65 |
|
|
741
|
+
| src/core/encryption.ts | 100.00 | 95.65 |
|
|
742
|
+
| src/utils/fs.ts | 100.00 | 97.44 |
|
|
743
|
+
| src/core/config.ts | 71.43 | 76.23 |
|
|
744
|
+
| src/commands/merge.ts | 75.00 | 28.40 |
|
|
745
|
+
| src/utils/output.ts | 25.00 | 12.70 |
|
|
746
|
+
|
|
747
|
+
> `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.
|
|
748
|
+
|
|
749
|
+
---
|
|
750
|
+
|
|
510
751
|
## Tech Stack
|
|
511
752
|
|
|
512
753
|
| | |
|
|
@@ -515,7 +756,7 @@ No SaaS. No dashboard. Just a CLI, your `.env` files, and Cloudflare's API.
|
|
|
515
756
|
| **CLI framework** | [citty](https://github.com/unjs/citty) |
|
|
516
757
|
| **Output** | [consola](https://github.com/unjs/consola) |
|
|
517
758
|
| **Config loading** | [jiti](https://github.com/unjs/jiti) |
|
|
518
|
-
| **Encryption** | [@dotenvx/dotenvx](https://dotenvx.com) |
|
|
759
|
+
| **Encryption** | `node:crypto` AES-256-GCM (password mode) / [@dotenvx/dotenvx](https://dotenvx.com) (dotenvx mode) |
|
|
519
760
|
| **CF Secrets** | [wrangler](https://developers.cloudflare.com/workers/wrangler/) CLI (shell out) |
|
|
520
761
|
|
|
521
762
|
---
|
package/dist/index.js
CHANGED
|
@@ -2315,11 +2315,13 @@ function resolveConfig(config, cwd) {
|
|
|
2315
2315
|
allKeys.push(key);
|
|
2316
2316
|
}
|
|
2317
2317
|
}
|
|
2318
|
+
const devFiles = app.devFile ? Array.isArray(app.devFile) ? app.devFile : [app.devFile] : [".dev.vars"];
|
|
2318
2319
|
apps[name] = {
|
|
2319
2320
|
...app,
|
|
2320
2321
|
name,
|
|
2321
2322
|
absolutePath: resolve2(projectRoot, app.path),
|
|
2322
|
-
allKeys
|
|
2323
|
+
allKeys,
|
|
2324
|
+
devFiles
|
|
2323
2325
|
};
|
|
2324
2326
|
}
|
|
2325
2327
|
return {
|
|
@@ -2333,15 +2335,23 @@ function resolveApps(config, appNames) {
|
|
|
2333
2335
|
if (!appNames || appNames.length === 0) {
|
|
2334
2336
|
return Object.values(config.apps);
|
|
2335
2337
|
}
|
|
2338
|
+
const available = Object.keys(config.apps);
|
|
2336
2339
|
const resolved = [];
|
|
2340
|
+
const unknown = [];
|
|
2337
2341
|
for (const name of appNames) {
|
|
2338
2342
|
const app = config.apps[name];
|
|
2339
2343
|
if (!app) {
|
|
2340
|
-
|
|
2344
|
+
unknown.push(name);
|
|
2341
2345
|
continue;
|
|
2342
2346
|
}
|
|
2343
2347
|
resolved.push(app);
|
|
2344
2348
|
}
|
|
2349
|
+
if (unknown.length > 0) {
|
|
2350
|
+
for (const name of unknown) {
|
|
2351
|
+
consola.error(`Unknown app: "${name}". Available: ${available.join(", ")}`);
|
|
2352
|
+
}
|
|
2353
|
+
process.exit(1);
|
|
2354
|
+
}
|
|
2345
2355
|
return resolved;
|
|
2346
2356
|
}
|
|
2347
2357
|
function getWorkerName(app, environment) {
|
|
@@ -13007,6 +13017,9 @@ var require_main2 = __commonJS((exports, module) => {
|
|
|
13007
13017
|
});
|
|
13008
13018
|
|
|
13009
13019
|
// src/core/encryption.ts
|
|
13020
|
+
import { readFileSync } from "node:fs";
|
|
13021
|
+
import { join as join2 } from "node:path";
|
|
13022
|
+
import { scryptSync, randomBytes, createCipheriv, createDecipheriv } from "node:crypto";
|
|
13010
13023
|
function decryptEnvContent(content, privateKey) {
|
|
13011
13024
|
if (privateKey) {
|
|
13012
13025
|
const prev = process.env.DOTENV_PRIVATE_KEY;
|
|
@@ -13023,27 +13036,164 @@ function decryptEnvContent(content, privateKey) {
|
|
|
13023
13036
|
}
|
|
13024
13037
|
return import_dotenvx.parse(content);
|
|
13025
13038
|
}
|
|
13026
|
-
function
|
|
13039
|
+
function loadEnvKeysFileSync(filePath) {
|
|
13040
|
+
try {
|
|
13041
|
+
const content = readFileSync(filePath, "utf-8");
|
|
13042
|
+
const result = {};
|
|
13043
|
+
for (const line of content.split(`
|
|
13044
|
+
`)) {
|
|
13045
|
+
const trimmed = line.trim();
|
|
13046
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
13047
|
+
continue;
|
|
13048
|
+
const eqIdx = trimmed.indexOf("=");
|
|
13049
|
+
if (eqIdx === -1)
|
|
13050
|
+
continue;
|
|
13051
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
13052
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
13053
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
13054
|
+
value = value.slice(1, -1);
|
|
13055
|
+
}
|
|
13056
|
+
result[key] = value;
|
|
13057
|
+
}
|
|
13058
|
+
return result;
|
|
13059
|
+
} catch {
|
|
13060
|
+
return {};
|
|
13061
|
+
}
|
|
13062
|
+
}
|
|
13063
|
+
function findPrivateKey(env2, projectRoot) {
|
|
13027
13064
|
if (env2) {
|
|
13028
13065
|
const envKey = `DOTENV_PRIVATE_KEY_${env2.toUpperCase()}`;
|
|
13029
13066
|
if (process.env[envKey])
|
|
13030
13067
|
return process.env[envKey];
|
|
13031
13068
|
}
|
|
13032
|
-
|
|
13069
|
+
if (process.env.DOTENV_PRIVATE_KEY)
|
|
13070
|
+
return process.env.DOTENV_PRIVATE_KEY;
|
|
13071
|
+
if (projectRoot) {
|
|
13072
|
+
const keysFile = loadEnvKeysFileSync(join2(projectRoot, ".env.keys"));
|
|
13073
|
+
if (env2) {
|
|
13074
|
+
const envKey = `DOTENV_PRIVATE_KEY_${env2.toUpperCase()}`;
|
|
13075
|
+
if (keysFile[envKey])
|
|
13076
|
+
return keysFile[envKey];
|
|
13077
|
+
}
|
|
13078
|
+
if (keysFile.DOTENV_PRIVATE_KEY)
|
|
13079
|
+
return keysFile.DOTENV_PRIVATE_KEY;
|
|
13080
|
+
}
|
|
13081
|
+
return;
|
|
13082
|
+
}
|
|
13083
|
+
function isEnvsyncEncrypted(value) {
|
|
13084
|
+
return value.startsWith(ENVSYNC_PREFIX);
|
|
13085
|
+
}
|
|
13086
|
+
function encryptValue(plaintext, password) {
|
|
13087
|
+
const salt = randomBytes(16);
|
|
13088
|
+
const iv = randomBytes(12);
|
|
13089
|
+
const key = scryptSync(password, salt, 32);
|
|
13090
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
13091
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
13092
|
+
const tag = cipher.getAuthTag();
|
|
13093
|
+
const payload = Buffer.concat([salt, iv, encrypted, tag]);
|
|
13094
|
+
return ENVSYNC_PREFIX + payload.toString("base64");
|
|
13095
|
+
}
|
|
13096
|
+
function decryptValue(token, password) {
|
|
13097
|
+
if (!token.startsWith(ENVSYNC_PREFIX)) {
|
|
13098
|
+
throw new Error("Not an envsync-encrypted value");
|
|
13099
|
+
}
|
|
13100
|
+
const payload = Buffer.from(token.slice(ENVSYNC_PREFIX.length), "base64");
|
|
13101
|
+
const salt = payload.subarray(0, 16);
|
|
13102
|
+
const iv = payload.subarray(16, 28);
|
|
13103
|
+
const tag = payload.subarray(payload.length - 16);
|
|
13104
|
+
const encrypted = payload.subarray(28, payload.length - 16);
|
|
13105
|
+
const key = scryptSync(password, salt, 32);
|
|
13106
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
13107
|
+
decipher.setAuthTag(tag);
|
|
13108
|
+
return decipher.update(encrypted) + decipher.final("utf8");
|
|
13109
|
+
}
|
|
13110
|
+
function decryptEnvMap(envMap, password) {
|
|
13111
|
+
const result = {};
|
|
13112
|
+
for (const [key, value] of Object.entries(envMap)) {
|
|
13113
|
+
result[key] = isEnvsyncEncrypted(value) ? decryptValue(value, password) : value;
|
|
13114
|
+
}
|
|
13115
|
+
return result;
|
|
13116
|
+
}
|
|
13117
|
+
function encryptEnvMap(envMap, password) {
|
|
13118
|
+
const result = {};
|
|
13119
|
+
for (const [key, value] of Object.entries(envMap)) {
|
|
13120
|
+
if (isEnvsyncEncrypted(value) || value === "") {
|
|
13121
|
+
result[key] = value;
|
|
13122
|
+
} else {
|
|
13123
|
+
result[key] = encryptValue(value, password);
|
|
13124
|
+
}
|
|
13125
|
+
}
|
|
13126
|
+
return result;
|
|
13127
|
+
}
|
|
13128
|
+
function findPassword(env2, projectRoot) {
|
|
13129
|
+
if (env2) {
|
|
13130
|
+
const envKey = `ENVSYNC_PASSWORD_${env2.toUpperCase()}`;
|
|
13131
|
+
if (process.env[envKey])
|
|
13132
|
+
return process.env[envKey];
|
|
13133
|
+
}
|
|
13134
|
+
if (process.env.ENVSYNC_PASSWORD)
|
|
13135
|
+
return process.env.ENVSYNC_PASSWORD;
|
|
13136
|
+
if (projectRoot) {
|
|
13137
|
+
const passwordFile = loadEnvKeysFileSync(join2(projectRoot, ".env.password"));
|
|
13138
|
+
if (env2) {
|
|
13139
|
+
const envKey = `ENVSYNC_PASSWORD_${env2.toUpperCase()}`;
|
|
13140
|
+
if (passwordFile[envKey])
|
|
13141
|
+
return passwordFile[envKey];
|
|
13142
|
+
}
|
|
13143
|
+
if (passwordFile.ENVSYNC_PASSWORD)
|
|
13144
|
+
return passwordFile.ENVSYNC_PASSWORD;
|
|
13145
|
+
}
|
|
13146
|
+
return;
|
|
13033
13147
|
}
|
|
13034
|
-
var import_dotenvx;
|
|
13148
|
+
var import_dotenvx, ENVSYNC_PREFIX = "envsync:v1:";
|
|
13035
13149
|
var init_encryption = __esm(() => {
|
|
13036
13150
|
import_dotenvx = __toESM(require_main2(), 1);
|
|
13037
13151
|
});
|
|
13038
13152
|
|
|
13039
13153
|
// src/core/env-file.ts
|
|
13040
|
-
import { join as
|
|
13041
|
-
|
|
13154
|
+
import { join as join3 } from "node:path";
|
|
13155
|
+
function parsePlainEnv(content) {
|
|
13156
|
+
const result = {};
|
|
13157
|
+
for (const line of content.split(`
|
|
13158
|
+
`)) {
|
|
13159
|
+
const trimmed = line.trim();
|
|
13160
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
13161
|
+
continue;
|
|
13162
|
+
const eqIdx = trimmed.indexOf("=");
|
|
13163
|
+
if (eqIdx === -1)
|
|
13164
|
+
continue;
|
|
13165
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
13166
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
13167
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
13168
|
+
value = value.slice(1, -1);
|
|
13169
|
+
}
|
|
13170
|
+
result[key] = value;
|
|
13171
|
+
}
|
|
13172
|
+
return result;
|
|
13173
|
+
}
|
|
13174
|
+
async function loadEnvFile(filePath, env2, projectRoot, encryption) {
|
|
13042
13175
|
if (!fileExists(filePath)) {
|
|
13043
13176
|
return {};
|
|
13044
13177
|
}
|
|
13045
13178
|
const content = await readFile(filePath);
|
|
13046
|
-
|
|
13179
|
+
if (encryption) {
|
|
13180
|
+
const hasEnvsyncValues = content.includes("envsync:v1:");
|
|
13181
|
+
const hasDotenvxValues = content.includes("encrypted:");
|
|
13182
|
+
if (encryption === "dotenvx" && hasEnvsyncValues) {
|
|
13183
|
+
consola.warn(`${filePath}: config uses encryption: "dotenvx" but file contains "envsync:v1:" (password-encrypted) values. ` + `Check your encryption setting.`);
|
|
13184
|
+
}
|
|
13185
|
+
if (encryption === "password" && hasDotenvxValues && !hasEnvsyncValues) {
|
|
13186
|
+
consola.warn(`${filePath}: config uses encryption: "password" but file contains dotenvx-encrypted values. ` + `Check your encryption setting.`);
|
|
13187
|
+
}
|
|
13188
|
+
}
|
|
13189
|
+
if (encryption === "password") {
|
|
13190
|
+
const envMap = parsePlainEnv(content);
|
|
13191
|
+
const password = findPassword(env2, projectRoot);
|
|
13192
|
+
if (!password)
|
|
13193
|
+
return envMap;
|
|
13194
|
+
return decryptEnvMap(envMap, password);
|
|
13195
|
+
}
|
|
13196
|
+
const privateKey = findPrivateKey(env2, projectRoot);
|
|
13047
13197
|
return decryptEnvContent(content, privateKey);
|
|
13048
13198
|
}
|
|
13049
13199
|
async function writeEnvFile(filePath, envMap) {
|
|
@@ -13087,34 +13237,41 @@ function filterForApp(envMap, app) {
|
|
|
13087
13237
|
function getRootEnvPath(config, environment) {
|
|
13088
13238
|
const pattern = config.raw.envFiles.pattern;
|
|
13089
13239
|
const relativePath = resolveEnvFilePath(pattern, environment);
|
|
13090
|
-
return
|
|
13240
|
+
return join3(config.projectRoot, relativePath);
|
|
13091
13241
|
}
|
|
13092
13242
|
function getAppEnvPath(config, app, environment) {
|
|
13093
13243
|
const pattern = config.raw.envFiles.pattern;
|
|
13094
13244
|
const relativePath = resolveEnvFilePath(pattern, environment);
|
|
13095
|
-
return
|
|
13245
|
+
return join3(app.absolutePath, relativePath);
|
|
13096
13246
|
}
|
|
13097
13247
|
function getLocalOverridePath(config) {
|
|
13098
|
-
return
|
|
13248
|
+
return join3(config.projectRoot, config.raw.envFiles.local);
|
|
13099
13249
|
}
|
|
13100
13250
|
var init_env_file = __esm(() => {
|
|
13251
|
+
init_dist2();
|
|
13101
13252
|
init_encryption();
|
|
13102
13253
|
init_config();
|
|
13103
13254
|
init_fs();
|
|
13104
13255
|
});
|
|
13105
13256
|
|
|
13106
13257
|
// src/core/resolver.ts
|
|
13258
|
+
import { normalize, relative } from "node:path";
|
|
13107
13259
|
async function resolveAppEnv(config, app, environment) {
|
|
13108
13260
|
const layers = [];
|
|
13261
|
+
const encryption = config.raw.encryption;
|
|
13109
13262
|
const rootEnvPath = getRootEnvPath(config, environment);
|
|
13110
|
-
|
|
13263
|
+
if (!fileExists(rootEnvPath) && environment !== "local" && !warnedMissingFiles.has(rootEnvPath)) {
|
|
13264
|
+
warnedMissingFiles.add(rootEnvPath);
|
|
13265
|
+
consola.warn(`Missing env file: ${relative(config.projectRoot, rootEnvPath)} (create it or run \`envsync init\`)`);
|
|
13266
|
+
}
|
|
13267
|
+
const rootEnv = await loadEnvFile(rootEnvPath, environment, config.projectRoot, encryption);
|
|
13111
13268
|
if (Object.keys(rootEnv).length > 0) {
|
|
13112
13269
|
layers.push({ source: rootEnvPath, map: rootEnv });
|
|
13113
13270
|
}
|
|
13114
13271
|
if (config.raw.envFiles.perApp) {
|
|
13115
13272
|
const appEnvPath = getAppEnvPath(config, app, environment);
|
|
13116
|
-
if (appEnvPath !== rootEnvPath) {
|
|
13117
|
-
const appEnv = await loadEnvFile(appEnvPath, environment);
|
|
13273
|
+
if (normalize(appEnvPath) !== normalize(rootEnvPath)) {
|
|
13274
|
+
const appEnv = await loadEnvFile(appEnvPath, environment, config.projectRoot, encryption);
|
|
13118
13275
|
if (Object.keys(appEnv).length > 0) {
|
|
13119
13276
|
layers.push({ source: appEnvPath, map: appEnv });
|
|
13120
13277
|
}
|
|
@@ -13149,8 +13306,12 @@ function findMissingOverrides(config, app, localEnv) {
|
|
|
13149
13306
|
}
|
|
13150
13307
|
return missing;
|
|
13151
13308
|
}
|
|
13309
|
+
var warnedMissingFiles;
|
|
13152
13310
|
var init_resolver = __esm(() => {
|
|
13311
|
+
init_dist2();
|
|
13153
13312
|
init_env_file();
|
|
13313
|
+
init_fs();
|
|
13314
|
+
warnedMissingFiles = new Set;
|
|
13154
13315
|
});
|
|
13155
13316
|
|
|
13156
13317
|
// src/commands/dev.ts
|
|
@@ -13158,13 +13319,13 @@ var exports_dev = {};
|
|
|
13158
13319
|
__export(exports_dev, {
|
|
13159
13320
|
default: () => dev_default
|
|
13160
13321
|
});
|
|
13161
|
-
import { join as
|
|
13322
|
+
import { join as join4, relative as relative2 } from "node:path";
|
|
13162
13323
|
function parseAppNames(args) {
|
|
13163
13324
|
const rest = args._;
|
|
13164
13325
|
return rest?.length ? rest : undefined;
|
|
13165
13326
|
}
|
|
13166
13327
|
function formatSource(source, key, projectRoot, sharedKeys, localOverrideKeys) {
|
|
13167
|
-
const rel =
|
|
13328
|
+
const rel = relative2(projectRoot, source);
|
|
13168
13329
|
if (localOverrideKeys.has(key))
|
|
13169
13330
|
return `${rel} (per-dev override)`;
|
|
13170
13331
|
if (sharedKeys.has(key))
|
|
@@ -13220,26 +13381,21 @@ var init_dev = __esm(() => {
|
|
|
13220
13381
|
const localEnv = await loadEnvFile(localOverridePath);
|
|
13221
13382
|
for (const app of apps) {
|
|
13222
13383
|
const resolved = await resolveAppEnv(config, app, environment);
|
|
13223
|
-
const devVarsPath = join3(app.absolutePath, ".dev.vars");
|
|
13224
|
-
const relDevVars = relative(config.projectRoot, devVarsPath);
|
|
13225
13384
|
if (Object.keys(resolved.map).length === 0) {
|
|
13226
13385
|
consola.warn(` No env vars resolved for ${app.name}. Skipping.`);
|
|
13227
13386
|
continue;
|
|
13228
13387
|
}
|
|
13229
|
-
|
|
13230
|
-
|
|
13231
|
-
|
|
13232
|
-
|
|
13233
|
-
|
|
13234
|
-
|
|
13235
|
-
|
|
13236
|
-
|
|
13237
|
-
consola.log(`
|
|
13388
|
+
for (const devFileName of app.devFiles) {
|
|
13389
|
+
const devFilePath = join4(app.absolutePath, devFileName);
|
|
13390
|
+
const relDevFile = relative2(config.projectRoot, devFilePath);
|
|
13391
|
+
if (args["dry-run"]) {
|
|
13392
|
+
consola.log(`
|
|
13393
|
+
${relDevFile}`);
|
|
13394
|
+
} else {
|
|
13395
|
+
await writeEnvFile(devFilePath, resolved.map);
|
|
13396
|
+
consola.log(`
|
|
13397
|
+
${relDevFile}`);
|
|
13238
13398
|
}
|
|
13239
|
-
} else {
|
|
13240
|
-
await writeEnvFile(devVarsPath, resolved.map);
|
|
13241
|
-
consola.log(`
|
|
13242
|
-
${relDevVars}`);
|
|
13243
13399
|
for (let i2 = 0;i2 < resolved.entries.length; i2++) {
|
|
13244
13400
|
const entry = resolved.entries[i2];
|
|
13245
13401
|
const isLast = i2 === resolved.entries.length - 1;
|
|
@@ -13253,12 +13409,18 @@ var init_dev = __esm(() => {
|
|
|
13253
13409
|
if (missing.length > 0) {
|
|
13254
13410
|
for (const key of missing) {
|
|
13255
13411
|
consola.warn(`
|
|
13256
|
-
⚠ Missing in ${
|
|
13257
|
-
consola.log(` → echo "${key}=<your-value>" >> ${
|
|
13412
|
+
⚠ Missing in ${relative2(config.projectRoot, localOverridePath)}: ${key} (required per-dev override)`);
|
|
13413
|
+
consola.log(` → echo "${key}=<your-value>" >> ${relative2(config.projectRoot, localOverridePath)}`);
|
|
13258
13414
|
}
|
|
13259
13415
|
}
|
|
13260
13416
|
}
|
|
13261
13417
|
}
|
|
13418
|
+
if (environment !== "local") {
|
|
13419
|
+
const hasOverrides = (config.raw.local?.overrides?.length ?? 0) > 0 || Object.keys(config.raw.local?.perApp ?? {}).length > 0;
|
|
13420
|
+
if (hasOverrides) {
|
|
13421
|
+
consola.info(`Per-dev overrides are only applied in "local" environment (current: ${environment}).`);
|
|
13422
|
+
}
|
|
13423
|
+
}
|
|
13262
13424
|
consola.success(`
|
|
13263
13425
|
Done!`);
|
|
13264
13426
|
}
|
|
@@ -13440,14 +13602,33 @@ var init_push = __esm(() => {
|
|
|
13440
13602
|
consola.warn("No apps to process.");
|
|
13441
13603
|
return;
|
|
13442
13604
|
}
|
|
13605
|
+
if (args.force && !args["dry-run"]) {
|
|
13606
|
+
const targets = apps.map((app) => {
|
|
13607
|
+
const w2 = getWorkerName(app, environment);
|
|
13608
|
+
return w2 ? `${app.name} → ${w2}` : null;
|
|
13609
|
+
}).filter(Boolean);
|
|
13610
|
+
if (targets.length > 0) {
|
|
13611
|
+
consola.warn(`Force-pushing to ${environment}: ${targets.join(", ")}`);
|
|
13612
|
+
}
|
|
13613
|
+
}
|
|
13443
13614
|
const sharedKeys = new Set(config.raw.shared ?? []);
|
|
13444
|
-
|
|
13615
|
+
let hasFailure = false;
|
|
13616
|
+
if (args.shared) {
|
|
13617
|
+
if (sharedKeys.size === 0) {
|
|
13618
|
+
consola.error("No shared keys defined in config. Nothing to push with --shared.");
|
|
13619
|
+
process.exit(1);
|
|
13620
|
+
}
|
|
13621
|
+
consola.info(`--shared: pushing only shared keys (${[...sharedKeys].join(", ")})`);
|
|
13622
|
+
}
|
|
13623
|
+
for (let i2 = 0;i2 < apps.length; i2++) {
|
|
13624
|
+
const app = apps[i2];
|
|
13625
|
+
const progress = apps.length > 1 ? `[${i2 + 1}/${apps.length}] ` : "";
|
|
13445
13626
|
const workerName = getWorkerName(app, environment);
|
|
13446
13627
|
if (!workerName) {
|
|
13447
|
-
consola.warn(
|
|
13628
|
+
consola.warn(`${progress}No worker defined for ${app.name} in ${environment}. Skipping.`);
|
|
13448
13629
|
continue;
|
|
13449
13630
|
}
|
|
13450
|
-
consola.start(
|
|
13631
|
+
consola.start(`${progress}Pushing secrets for ${app.name} → ${workerName} (${environment})...`);
|
|
13451
13632
|
const resolved = await resolveAppEnv(config, app, environment);
|
|
13452
13633
|
let secretsToPush;
|
|
13453
13634
|
if (args.shared) {
|
|
@@ -13468,7 +13649,8 @@ var init_push = __esm(() => {
|
|
|
13468
13649
|
}
|
|
13469
13650
|
const keyCount = Object.keys(secretsToPush).length;
|
|
13470
13651
|
if (keyCount === 0) {
|
|
13471
|
-
|
|
13652
|
+
const reason = args.shared ? " (no shared keys for this app)" : "";
|
|
13653
|
+
consola.warn(` No secrets to push for ${app.name}${reason}. Skipping.`);
|
|
13472
13654
|
continue;
|
|
13473
13655
|
}
|
|
13474
13656
|
if (args["dry-run"]) {
|
|
@@ -13491,8 +13673,13 @@ var init_push = __esm(() => {
|
|
|
13491
13673
|
consola.success(` Pushed ${keyCount} secrets to ${workerName}`);
|
|
13492
13674
|
} else {
|
|
13493
13675
|
consola.error(` Failed to push secrets to ${workerName}`);
|
|
13676
|
+
hasFailure = true;
|
|
13494
13677
|
}
|
|
13495
13678
|
}
|
|
13679
|
+
if (hasFailure) {
|
|
13680
|
+
consola.error("Some pushes failed.");
|
|
13681
|
+
process.exit(1);
|
|
13682
|
+
}
|
|
13496
13683
|
consola.success("Done!");
|
|
13497
13684
|
}
|
|
13498
13685
|
});
|
|
@@ -13565,7 +13752,7 @@ var init_pull = __esm(() => {
|
|
|
13565
13752
|
}
|
|
13566
13753
|
consola.info(` Found ${remoteKeys.length} remote keys`);
|
|
13567
13754
|
const envFilePath = getRootEnvPath(config, environment);
|
|
13568
|
-
const localEnv = await loadEnvFile(envFilePath, environment);
|
|
13755
|
+
const localEnv = await loadEnvFile(envFilePath, environment, config.projectRoot, config.raw.encryption);
|
|
13569
13756
|
const localKeys = new Set(Object.keys(localEnv));
|
|
13570
13757
|
const missingKeys = remoteKeys.filter((k2) => !localKeys.has(k2));
|
|
13571
13758
|
if (missingKeys.length === 0) {
|
|
@@ -13590,7 +13777,7 @@ var exports_validate = {};
|
|
|
13590
13777
|
__export(exports_validate, {
|
|
13591
13778
|
default: () => validate_default
|
|
13592
13779
|
});
|
|
13593
|
-
import { join as
|
|
13780
|
+
import { join as join5 } from "node:path";
|
|
13594
13781
|
function parseAppNames4(args, skip = 1) {
|
|
13595
13782
|
const rest = args._?.slice(skip);
|
|
13596
13783
|
return rest?.length ? rest : undefined;
|
|
@@ -13628,17 +13815,24 @@ var init_validate = __esm(() => {
|
|
|
13628
13815
|
let environments;
|
|
13629
13816
|
let appNames;
|
|
13630
13817
|
if (envArg && config.environments.includes(envArg)) {
|
|
13818
|
+
if (envArg in config.apps) {
|
|
13819
|
+
consola.error(`"${envArg}" is both an environment and an app name. This is ambiguous.`);
|
|
13820
|
+
consola.info(` To validate environment: envsync validate ${envArg} --
|
|
13821
|
+
` + ` To validate app: envsync validate -- ${envArg}`);
|
|
13822
|
+
process.exit(1);
|
|
13823
|
+
}
|
|
13631
13824
|
environments = [envArg];
|
|
13632
13825
|
appNames = parseAppNames4(args);
|
|
13633
13826
|
} else if (envArg) {
|
|
13634
13827
|
environments = config.environments;
|
|
13635
13828
|
appNames = parseAppNames4(args, 0);
|
|
13829
|
+
consola.info(`"${envArg}" is not an environment. Treating as app name. Validating all environments.`);
|
|
13636
13830
|
} else {
|
|
13637
13831
|
environments = config.environments;
|
|
13638
13832
|
appNames = undefined;
|
|
13639
13833
|
}
|
|
13640
13834
|
const apps = resolveApps(config, appNames);
|
|
13641
|
-
const examplePath =
|
|
13835
|
+
const examplePath = join5(config.projectRoot, ".env.example");
|
|
13642
13836
|
if (!fileExists(examplePath)) {
|
|
13643
13837
|
consola.error("No .env.example found at project root.");
|
|
13644
13838
|
process.exit(1);
|
|
@@ -13651,10 +13845,12 @@ var init_validate = __esm(() => {
|
|
|
13651
13845
|
...config.raw.local?.overrides ?? [],
|
|
13652
13846
|
...Object.values(config.raw.local?.perApp ?? {}).flat()
|
|
13653
13847
|
]);
|
|
13848
|
+
const totalChecks = environments.length * apps.length;
|
|
13654
13849
|
consola.log(`
|
|
13655
|
-
Checking against .env.example
|
|
13850
|
+
Checking against .env.example... (${environments.length} env × ${apps.length} app${apps.length > 1 ? "s" : ""})`);
|
|
13656
13851
|
const results = [];
|
|
13657
13852
|
let hasIssues = false;
|
|
13853
|
+
let checkIdx = 0;
|
|
13658
13854
|
for (const environment of environments) {
|
|
13659
13855
|
consola.log(`
|
|
13660
13856
|
${environment}`);
|
|
@@ -13736,10 +13932,10 @@ function printDiff(entries) {
|
|
|
13736
13932
|
for (const entry of entries) {
|
|
13737
13933
|
switch (entry.status) {
|
|
13738
13934
|
case "added":
|
|
13739
|
-
consola.log(` + ${entry.key} = ${maskValue(entry.
|
|
13935
|
+
consola.log(` + ${entry.key} = ${maskValue(entry.localValue)}`);
|
|
13740
13936
|
break;
|
|
13741
13937
|
case "removed":
|
|
13742
|
-
consola.log(` - ${entry.key}
|
|
13938
|
+
consola.log(` - ${entry.key}`);
|
|
13743
13939
|
break;
|
|
13744
13940
|
case "changed":
|
|
13745
13941
|
consola.log(` ~ ${entry.key}`);
|
|
@@ -13819,8 +14015,15 @@ var init_diff = __esm(() => {
|
|
|
13819
14015
|
process.exit(1);
|
|
13820
14016
|
}
|
|
13821
14017
|
const target = args.target;
|
|
13822
|
-
const
|
|
13823
|
-
|
|
14018
|
+
const isEnv = target && config.environments.includes(target);
|
|
14019
|
+
const isApp = target && target in config.apps;
|
|
14020
|
+
if (isEnv && isApp) {
|
|
14021
|
+
consola.error(`"${target}" is both an environment and an app name. This is ambiguous.`);
|
|
14022
|
+
consola.info(` To compare environments: envsync diff ${env1} ${target} --
|
|
14023
|
+
` + ` To diff local vs remote: envsync diff ${env1} -- ${target}`);
|
|
14024
|
+
process.exit(1);
|
|
14025
|
+
}
|
|
14026
|
+
if (isEnv) {
|
|
13824
14027
|
const env2 = target;
|
|
13825
14028
|
const appNames = parseAppNames5(args, 2);
|
|
13826
14029
|
const apps = resolveApps(config, appNames);
|
|
@@ -13917,7 +14120,7 @@ var exports_init = {};
|
|
|
13917
14120
|
__export(exports_init, {
|
|
13918
14121
|
default: () => init_default
|
|
13919
14122
|
});
|
|
13920
|
-
import { join as
|
|
14123
|
+
import { join as join6, relative as relative3, basename } from "node:path";
|
|
13921
14124
|
function generateConfigTS(config) {
|
|
13922
14125
|
const lines = [
|
|
13923
14126
|
`import { defineConfig } from "cf-envsync";`,
|
|
@@ -13998,10 +14201,19 @@ var init_init = __esm(() => {
|
|
|
13998
14201
|
},
|
|
13999
14202
|
async run({ args }) {
|
|
14000
14203
|
const cwd = process.cwd();
|
|
14001
|
-
const existingConfig = CONFIG_FILES.find((f3) => fileExists(
|
|
14204
|
+
const existingConfig = CONFIG_FILES.find((f3) => fileExists(join6(cwd, f3)));
|
|
14002
14205
|
if (existingConfig) {
|
|
14003
14206
|
consola.warn(`${existingConfig} already exists.`);
|
|
14004
|
-
const
|
|
14207
|
+
const existingContent = await readFile(join6(cwd, existingConfig));
|
|
14208
|
+
const lines = existingContent.split(`
|
|
14209
|
+
`);
|
|
14210
|
+
const preview = lines.length > 20 ? [...lines.slice(0, 20), ` ... (${lines.length - 20} more lines)`].join(`
|
|
14211
|
+
`) : existingContent;
|
|
14212
|
+
consola.log(`
|
|
14213
|
+
Current config:
|
|
14214
|
+
${preview}
|
|
14215
|
+
`);
|
|
14216
|
+
const overwrite = await consola.prompt("Overwrite with new config?", {
|
|
14005
14217
|
type: "confirm"
|
|
14006
14218
|
});
|
|
14007
14219
|
if (!overwrite) {
|
|
@@ -14011,7 +14223,7 @@ var init_init = __esm(() => {
|
|
|
14011
14223
|
}
|
|
14012
14224
|
const encryption = await consola.prompt("Encryption method:", {
|
|
14013
14225
|
type: "select",
|
|
14014
|
-
options: ["dotenvx", "none"]
|
|
14226
|
+
options: ["password", "dotenvx", "none"]
|
|
14015
14227
|
});
|
|
14016
14228
|
const defaultEnvs = "local, staging, production";
|
|
14017
14229
|
const envsInput = await consola.prompt("Environments (comma-separated):", {
|
|
@@ -14028,12 +14240,13 @@ var init_init = __esm(() => {
|
|
|
14028
14240
|
return (name === "wrangler.json" || name === "wrangler.jsonc") && !f3.includes("node_modules");
|
|
14029
14241
|
});
|
|
14030
14242
|
if (wranglerFiles.length === 0) {
|
|
14031
|
-
consola.warn(
|
|
14243
|
+
consola.warn(`No wrangler.json or wrangler.jsonc files found (searched all subdirectories, excluding node_modules).
|
|
14244
|
+
` + " Falling back to manual configuration.");
|
|
14032
14245
|
}
|
|
14033
14246
|
for (const wranglerFile of wranglerFiles.sort()) {
|
|
14034
|
-
const fullPath =
|
|
14035
|
-
const appDir =
|
|
14036
|
-
const appPath =
|
|
14247
|
+
const fullPath = join6(cwd, wranglerFile);
|
|
14248
|
+
const appDir = join6(cwd, wranglerFile, "..");
|
|
14249
|
+
const appPath = relative3(cwd, appDir);
|
|
14037
14250
|
const appName = basename(appDir);
|
|
14038
14251
|
consola.info(` Found ${wranglerFile}`);
|
|
14039
14252
|
let wranglerConfig = {};
|
|
@@ -14133,10 +14346,10 @@ var init_init = __esm(() => {
|
|
|
14133
14346
|
apps,
|
|
14134
14347
|
...shared.length > 0 ? { shared } : {}
|
|
14135
14348
|
};
|
|
14136
|
-
const configPath =
|
|
14349
|
+
const configPath = join6(cwd, "envsync.config.ts");
|
|
14137
14350
|
const tsContent = generateConfigTS(config);
|
|
14138
14351
|
if (existingConfig && existingConfig !== "envsync.config.ts") {
|
|
14139
|
-
const oldPath =
|
|
14352
|
+
const oldPath = join6(cwd, existingConfig);
|
|
14140
14353
|
if (fileExists(oldPath)) {
|
|
14141
14354
|
const { unlink } = await import("node:fs/promises");
|
|
14142
14355
|
await unlink(oldPath);
|
|
@@ -14144,7 +14357,7 @@ var init_init = __esm(() => {
|
|
|
14144
14357
|
}
|
|
14145
14358
|
await writeFile(configPath, tsContent);
|
|
14146
14359
|
consola.success("Created envsync.config.ts");
|
|
14147
|
-
const examplePath =
|
|
14360
|
+
const examplePath = join6(cwd, ".env.example");
|
|
14148
14361
|
if (!fileExists(examplePath)) {
|
|
14149
14362
|
const allKeys = new Set;
|
|
14150
14363
|
for (const app of Object.values(apps)) {
|
|
@@ -14167,21 +14380,21 @@ var init_init = __esm(() => {
|
|
|
14167
14380
|
for (const env2 of environments) {
|
|
14168
14381
|
if (env2 === "local")
|
|
14169
14382
|
continue;
|
|
14170
|
-
const envFile =
|
|
14383
|
+
const envFile = join6(cwd, `.env.${env2}`);
|
|
14171
14384
|
if (!fileExists(envFile)) {
|
|
14172
14385
|
await writeFile(envFile, `# ${env2} environment variables
|
|
14173
14386
|
`);
|
|
14174
14387
|
consola.success(`Created .env.${env2}`);
|
|
14175
14388
|
}
|
|
14176
14389
|
}
|
|
14177
|
-
const rootEnv =
|
|
14390
|
+
const rootEnv = join6(cwd, ".env");
|
|
14178
14391
|
if (!fileExists(rootEnv)) {
|
|
14179
14392
|
await writeFile(rootEnv, `# Local environment variables
|
|
14180
14393
|
`);
|
|
14181
14394
|
consola.success("Created .env");
|
|
14182
14395
|
}
|
|
14183
|
-
const gitignorePath =
|
|
14184
|
-
const gitignoreEntries = [".env.local", ".env.keys", "**/.dev.vars"];
|
|
14396
|
+
const gitignorePath = join6(cwd, ".gitignore");
|
|
14397
|
+
const gitignoreEntries = [".env.local", ".env.keys", ".env.password", "**/.dev.vars"];
|
|
14185
14398
|
if (fileExists(gitignorePath)) {
|
|
14186
14399
|
const existing = await readFile(gitignorePath);
|
|
14187
14400
|
const toAdd = gitignoreEntries.filter((e2) => !existing.includes(e2));
|
|
@@ -14201,7 +14414,7 @@ var init_init = __esm(() => {
|
|
|
14201
14414
|
`);
|
|
14202
14415
|
consola.success(`Created .gitignore`);
|
|
14203
14416
|
}
|
|
14204
|
-
const gitattrsPath =
|
|
14417
|
+
const gitattrsPath = join6(cwd, ".gitattributes");
|
|
14205
14418
|
const mergeDriverLines = `.env merge=envsync
|
|
14206
14419
|
.env.* merge=envsync
|
|
14207
14420
|
`;
|
|
@@ -14217,7 +14430,7 @@ var init_init = __esm(() => {
|
|
|
14217
14430
|
await writeFile(gitattrsPath, mergeDriverLines);
|
|
14218
14431
|
consola.success("Created .gitattributes with merge driver");
|
|
14219
14432
|
}
|
|
14220
|
-
const gitConfigPath =
|
|
14433
|
+
const gitConfigPath = join6(cwd, ".git", "config");
|
|
14221
14434
|
if (fileExists(gitConfigPath)) {
|
|
14222
14435
|
const gitConfig = await readFile(gitConfigPath);
|
|
14223
14436
|
if (!gitConfig.includes('[merge "envsync"]')) {
|
|
@@ -14232,14 +14445,18 @@ var init_init = __esm(() => {
|
|
|
14232
14445
|
}
|
|
14233
14446
|
consola.info(`
|
|
14234
14447
|
Next steps:`);
|
|
14235
|
-
if (encryption === "
|
|
14448
|
+
if (encryption === "password") {
|
|
14449
|
+
consola.info(' 1. Set a password: echo "ENVSYNC_PASSWORD=your-secret" > .env.password');
|
|
14450
|
+
consola.info(" 2. Add values to your .env files (plain KEY=VALUE)");
|
|
14451
|
+
consola.info(" 3. envsync encrypt staging (encrypts plain values in .env.staging)");
|
|
14452
|
+
} else if (encryption === "dotenvx") {
|
|
14236
14453
|
consola.info(' 1. dotenvx set DATABASE_URL "value" -f .env');
|
|
14237
14454
|
for (const env2 of environments.filter((e2) => e2 !== "local")) {
|
|
14238
14455
|
consola.info(` 2. dotenvx set DATABASE_URL "${env2}_value" -f .env.${env2}`);
|
|
14239
14456
|
}
|
|
14240
14457
|
}
|
|
14241
|
-
consola.info(` 3. echo "OAUTH_REDIRECT_URL=https://your-tunnel/callback" >> .env.local`);
|
|
14242
|
-
consola.info("
|
|
14458
|
+
consola.info(` ${encryption === "password" ? "4" : "3"}. echo "OAUTH_REDIRECT_URL=https://your-tunnel/callback" >> .env.local`);
|
|
14459
|
+
consola.info(` ${encryption === "password" ? "5" : "4"}. envsync dev`);
|
|
14243
14460
|
}
|
|
14244
14461
|
});
|
|
14245
14462
|
});
|
|
@@ -14333,6 +14550,10 @@ var init_normalize = __esm(() => {
|
|
|
14333
14550
|
// src/commands/merge.ts
|
|
14334
14551
|
var exports_merge = {};
|
|
14335
14552
|
__export(exports_merge, {
|
|
14553
|
+
parseEnvLines: () => parseEnvLines,
|
|
14554
|
+
isPasswordEncrypted: () => isPasswordEncrypted,
|
|
14555
|
+
isEncrypted: () => isEncrypted,
|
|
14556
|
+
isDotenvxEncrypted: () => isDotenvxEncrypted,
|
|
14336
14557
|
default: () => merge_default
|
|
14337
14558
|
});
|
|
14338
14559
|
function parseEnvLines(content) {
|
|
@@ -14353,9 +14574,15 @@ function parseEnvLines(content) {
|
|
|
14353
14574
|
};
|
|
14354
14575
|
});
|
|
14355
14576
|
}
|
|
14356
|
-
function
|
|
14577
|
+
function isDotenvxEncrypted(content) {
|
|
14357
14578
|
return content.includes("encrypted:");
|
|
14358
14579
|
}
|
|
14580
|
+
function isPasswordEncrypted(content) {
|
|
14581
|
+
return content.includes("envsync:v1:");
|
|
14582
|
+
}
|
|
14583
|
+
function isEncrypted(content) {
|
|
14584
|
+
return isDotenvxEncrypted(content) || isPasswordEncrypted(content);
|
|
14585
|
+
}
|
|
14359
14586
|
var merge_default;
|
|
14360
14587
|
var init_merge = __esm(() => {
|
|
14361
14588
|
init_dist();
|
|
@@ -14391,11 +14618,24 @@ var init_merge = __esm(() => {
|
|
|
14391
14618
|
readFile(args.ours),
|
|
14392
14619
|
readFile(args.theirs)
|
|
14393
14620
|
]);
|
|
14394
|
-
const
|
|
14395
|
-
const
|
|
14396
|
-
const
|
|
14397
|
-
const
|
|
14398
|
-
const
|
|
14621
|
+
const wasDotenvx = isDotenvxEncrypted(oursContent);
|
|
14622
|
+
const wasPassword = isPasswordEncrypted(oursContent);
|
|
14623
|
+
const wasEncrypted = wasDotenvx || wasPassword;
|
|
14624
|
+
const privateKey = findPrivateKey(undefined, process.cwd());
|
|
14625
|
+
const password = findPassword(undefined, process.cwd());
|
|
14626
|
+
function decryptContent(content) {
|
|
14627
|
+
if (isPasswordEncrypted(content)) {
|
|
14628
|
+
const plain = Object.fromEntries(parseEnvLines(content).filter((e2) => e2.key).map((e2) => [e2.key, e2.value ?? ""]));
|
|
14629
|
+
return password ? decryptEnvMap(plain, password) : plain;
|
|
14630
|
+
}
|
|
14631
|
+
if (isDotenvxEncrypted(content)) {
|
|
14632
|
+
return decryptEnvContent(content, privateKey);
|
|
14633
|
+
}
|
|
14634
|
+
return Object.fromEntries(parseEnvLines(content).filter((e2) => e2.key).map((e2) => [e2.key, e2.value ?? ""]));
|
|
14635
|
+
}
|
|
14636
|
+
const baseParsed = decryptContent(baseContent);
|
|
14637
|
+
const oursParsed = decryptContent(oursContent);
|
|
14638
|
+
const theirsParsed = decryptContent(theirsContent);
|
|
14399
14639
|
const baseMap = new Map(Object.entries(baseParsed));
|
|
14400
14640
|
const oursMap = new Map(Object.entries(oursParsed));
|
|
14401
14641
|
const theirsMap = new Map(Object.entries(theirsParsed));
|
|
@@ -14452,10 +14692,19 @@ var init_merge = __esm(() => {
|
|
|
14452
14692
|
`) + `
|
|
14453
14693
|
`;
|
|
14454
14694
|
if (wasEncrypted && !hasConflicts) {
|
|
14455
|
-
|
|
14456
|
-
|
|
14457
|
-
|
|
14458
|
-
|
|
14695
|
+
if (wasPassword && password) {
|
|
14696
|
+
const plainMap = Object.fromEntries(parseEnvLines(mergedContent).filter((e2) => e2.key).map((e2) => [e2.key, e2.value ?? ""]));
|
|
14697
|
+
const encrypted = encryptEnvMap(plainMap, password);
|
|
14698
|
+
const encLines = Object.entries(encrypted).map(([k2, v2]) => `${k2}=${v2}`);
|
|
14699
|
+
await writeFile(args.ours, encLines.join(`
|
|
14700
|
+
`) + `
|
|
14701
|
+
`);
|
|
14702
|
+
} else {
|
|
14703
|
+
await writeFile(args.ours, mergedContent);
|
|
14704
|
+
const result = await exec(["dotenvx", "encrypt", "-f", args.ours]);
|
|
14705
|
+
if (!result.success) {
|
|
14706
|
+
consola.warn("Could not re-encrypt merged file:", result.stderr);
|
|
14707
|
+
}
|
|
14459
14708
|
}
|
|
14460
14709
|
} else {
|
|
14461
14710
|
await writeFile(args.ours, mergedContent);
|
|
@@ -14612,6 +14861,124 @@ var init_list = __esm(() => {
|
|
|
14612
14861
|
});
|
|
14613
14862
|
});
|
|
14614
14863
|
|
|
14864
|
+
// src/commands/encrypt.ts
|
|
14865
|
+
var exports_encrypt = {};
|
|
14866
|
+
__export(exports_encrypt, {
|
|
14867
|
+
default: () => encrypt_default
|
|
14868
|
+
});
|
|
14869
|
+
function parseLines(content) {
|
|
14870
|
+
return content.split(`
|
|
14871
|
+
`).map((line) => {
|
|
14872
|
+
const trimmed = line.trim();
|
|
14873
|
+
if (trimmed === "" || trimmed.startsWith("#")) {
|
|
14874
|
+
return { raw: line };
|
|
14875
|
+
}
|
|
14876
|
+
const eqIdx = line.indexOf("=");
|
|
14877
|
+
if (eqIdx === -1) {
|
|
14878
|
+
return { raw: line };
|
|
14879
|
+
}
|
|
14880
|
+
return {
|
|
14881
|
+
key: line.slice(0, eqIdx).trim(),
|
|
14882
|
+
value: line.slice(eqIdx + 1),
|
|
14883
|
+
raw: line
|
|
14884
|
+
};
|
|
14885
|
+
});
|
|
14886
|
+
}
|
|
14887
|
+
var encrypt_default;
|
|
14888
|
+
var init_encrypt = __esm(() => {
|
|
14889
|
+
init_dist();
|
|
14890
|
+
init_dist2();
|
|
14891
|
+
init_config();
|
|
14892
|
+
init_env_file();
|
|
14893
|
+
init_fs();
|
|
14894
|
+
init_encryption();
|
|
14895
|
+
encrypt_default = defineCommand({
|
|
14896
|
+
meta: {
|
|
14897
|
+
name: "encrypt",
|
|
14898
|
+
description: "Encrypt plain .env values with password encryption"
|
|
14899
|
+
},
|
|
14900
|
+
args: {
|
|
14901
|
+
env: {
|
|
14902
|
+
type: "positional",
|
|
14903
|
+
description: "Target environment (e.g. staging, production)",
|
|
14904
|
+
required: true
|
|
14905
|
+
},
|
|
14906
|
+
"dry-run": {
|
|
14907
|
+
type: "boolean",
|
|
14908
|
+
description: "Preview changes without writing",
|
|
14909
|
+
default: false
|
|
14910
|
+
}
|
|
14911
|
+
},
|
|
14912
|
+
async run({ args }) {
|
|
14913
|
+
const environment = args.env;
|
|
14914
|
+
const rawConfig = await loadConfig();
|
|
14915
|
+
const errors = validateConfig(rawConfig);
|
|
14916
|
+
if (errors.length > 0) {
|
|
14917
|
+
for (const err of errors)
|
|
14918
|
+
consola.error(err);
|
|
14919
|
+
process.exit(1);
|
|
14920
|
+
}
|
|
14921
|
+
const config = resolveConfig(rawConfig);
|
|
14922
|
+
if (config.raw.encryption !== "password") {
|
|
14923
|
+
consola.error('encrypt command requires encryption: "password" in config.');
|
|
14924
|
+
process.exit(1);
|
|
14925
|
+
}
|
|
14926
|
+
if (!config.environments.includes(environment)) {
|
|
14927
|
+
consola.error(`Unknown environment: "${environment}". Available: ${config.environments.join(", ")}`);
|
|
14928
|
+
process.exit(1);
|
|
14929
|
+
}
|
|
14930
|
+
const password = findPassword(environment, config.projectRoot);
|
|
14931
|
+
if (!password) {
|
|
14932
|
+
consola.error(`No password found. Set ENVSYNC_PASSWORD or ENVSYNC_PASSWORD_${environment.toUpperCase()}, or create .env.password`);
|
|
14933
|
+
process.exit(1);
|
|
14934
|
+
}
|
|
14935
|
+
const envFilePath = getRootEnvPath(config, environment);
|
|
14936
|
+
if (!fileExists(envFilePath)) {
|
|
14937
|
+
consola.error(`File not found: ${envFilePath}`);
|
|
14938
|
+
process.exit(1);
|
|
14939
|
+
}
|
|
14940
|
+
const content = await readFile(envFilePath);
|
|
14941
|
+
const lines = parseLines(content);
|
|
14942
|
+
let encryptedCount = 0;
|
|
14943
|
+
let skippedCount = 0;
|
|
14944
|
+
const outputLines = [];
|
|
14945
|
+
for (const line of lines) {
|
|
14946
|
+
if (!line.key || line.value === undefined) {
|
|
14947
|
+
outputLines.push(line.raw);
|
|
14948
|
+
continue;
|
|
14949
|
+
}
|
|
14950
|
+
const value = line.value.trim();
|
|
14951
|
+
if (isEnvsyncEncrypted(value) || value === "") {
|
|
14952
|
+
outputLines.push(line.raw);
|
|
14953
|
+
skippedCount++;
|
|
14954
|
+
continue;
|
|
14955
|
+
}
|
|
14956
|
+
let plainValue = value;
|
|
14957
|
+
if (plainValue.startsWith('"') && plainValue.endsWith('"') || plainValue.startsWith("'") && plainValue.endsWith("'")) {
|
|
14958
|
+
plainValue = plainValue.slice(1, -1);
|
|
14959
|
+
}
|
|
14960
|
+
const encrypted = encryptValue(plainValue, password);
|
|
14961
|
+
outputLines.push(`${line.key}=${encrypted}`);
|
|
14962
|
+
encryptedCount++;
|
|
14963
|
+
consola.log(` ${line.key}: encrypted`);
|
|
14964
|
+
}
|
|
14965
|
+
if (encryptedCount === 0) {
|
|
14966
|
+
consola.info("No values to encrypt.");
|
|
14967
|
+
return;
|
|
14968
|
+
}
|
|
14969
|
+
const dryRun = args["dry-run"];
|
|
14970
|
+
if (dryRun) {
|
|
14971
|
+
consola.info(`[dry-run] Would encrypt ${encryptedCount} values in ${envFilePath}`);
|
|
14972
|
+
return;
|
|
14973
|
+
}
|
|
14974
|
+
await writeFile(envFilePath, outputLines.join(`
|
|
14975
|
+
`) + `
|
|
14976
|
+
`);
|
|
14977
|
+
consola.success(`Encrypted ${encryptedCount} values in ${envFilePath} (${skippedCount} skipped)`);
|
|
14978
|
+
}
|
|
14979
|
+
});
|
|
14980
|
+
});
|
|
14981
|
+
|
|
14615
14982
|
// src/index.ts
|
|
14616
14983
|
init_dist();
|
|
14617
14984
|
var main = defineCommand({
|
|
@@ -14629,7 +14996,8 @@ var main = defineCommand({
|
|
|
14629
14996
|
init: () => Promise.resolve().then(() => (init_init(), exports_init)).then((m2) => m2.default),
|
|
14630
14997
|
normalize: () => Promise.resolve().then(() => (init_normalize(), exports_normalize)).then((m2) => m2.default),
|
|
14631
14998
|
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)
|
|
14999
|
+
list: () => Promise.resolve().then(() => (init_list(), exports_list)).then((m2) => m2.default),
|
|
15000
|
+
encrypt: () => Promise.resolve().then(() => (init_encrypt(), exports_encrypt)).then((m2) => m2.default)
|
|
14633
15001
|
}
|
|
14634
15002
|
});
|
|
14635
15003
|
runMain(main);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cf-envsync",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Sync .env files to Cloudflare Workers secrets, .dev.vars, and more",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -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"
|
package/src/types/config.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface EnvSyncConfig {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
/** Encryption method */
|
|
17
|
-
encryption: "dotenvx" | "none";
|
|
17
|
+
encryption: "dotenvx" | "password" | "none";
|
|
18
18
|
|
|
19
19
|
/** App definitions */
|
|
20
20
|
apps: Record<string, AppConfig>;
|
|
@@ -41,6 +41,9 @@ export interface AppConfig {
|
|
|
41
41
|
secrets?: string[];
|
|
42
42
|
/** Var keys for this app (non-secret env vars) */
|
|
43
43
|
vars?: string[];
|
|
44
|
+
/** Output file(s) for `envsync dev`. Defaults to ".dev.vars".
|
|
45
|
+
* Use an array to generate multiple files, e.g. [".dev.vars", ".env.local"] */
|
|
46
|
+
devFile?: string | string[];
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
/** Resolved config after defaults and path resolution */
|
|
@@ -62,4 +65,6 @@ export interface ResolvedAppConfig extends AppConfig {
|
|
|
62
65
|
absolutePath: string;
|
|
63
66
|
/** All keys this app needs (secrets + vars + local overrides) */
|
|
64
67
|
allKeys: string[];
|
|
68
|
+
/** Normalized output file names for `envsync dev` (always an array) */
|
|
69
|
+
devFiles: string[];
|
|
65
70
|
}
|