cf-envsync 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,518 @@
1
+ <p align="center">
2
+ <br />
3
+ <code>.env</code>&nbsp;&nbsp;→&nbsp;&nbsp;<strong>Cloudflare Workers</strong>&nbsp;&nbsp;→&nbsp;&nbsp;done.
4
+ <br />
5
+ <br />
6
+ </p>
7
+
8
+ <h1 align="center">envsync</h1>
9
+
10
+ <p align="center">
11
+ One <code>.env</code> file. Every Worker. Every environment.<br />
12
+ No SaaS. No dashboard. Just your repo.
13
+ </p>
14
+
15
+ <p align="center">
16
+ <a href="#quick-start">Quick Start</a>&nbsp;&nbsp;|&nbsp;&nbsp;<a href="#commands">Commands</a>&nbsp;&nbsp;|&nbsp;&nbsp;<a href="#configuration">Config</a>&nbsp;&nbsp;|&nbsp;&nbsp;<a href="#why">Why?</a>
17
+ </p>
18
+
19
+ ---
20
+
21
+ ```
22
+ .env.{environment} ──→ envsync ──→ Cloudflare Workers secrets
23
+ + │ (per worker, per env)
24
+ .env.local ──→ │
25
+ (per-developer) ├──→ .dev.vars for each app
26
+ │ (local dev)
27
+ └──→ validation
28
+ (nothing missing)
29
+ ```
30
+
31
+ ---
32
+
33
+ ## The Problem
34
+
35
+ If you're building on **Cloudflare Workers** with **dotenvx** encryption in a **monorepo**, you know the pain:
36
+
37
+ - **dotenvx breaks Git** — Same plaintext, different ciphertext every time. Two devs touch `.env` = guaranteed merge conflict.
38
+ - **Three layers that don't sync** — Vite reads `.env`, wrangler reads `.dev.vars`, production reads from the dashboard. Forget to update one? Silent failure.
39
+ - **N Workers x M Environments x manual labor** — `wrangler secret put` one key at a time, per worker, per environment.
40
+ - **Per-developer secrets** — OAuth callback URLs differ per dev tunnel. No way to enforce they're set.
41
+ - **No way to verify what's deployed** — "Is production using the new key or the old one?" Push and pray.
42
+
43
+ Every existing tool solves one piece. **envsync connects them all.**
44
+
45
+ ---
46
+
47
+ ## Quick Start
48
+
49
+ ```bash
50
+ bun add -d cf-envsync
51
+ ```
52
+
53
+ ```bash
54
+ # Initialize (scans wrangler.jsonc files in monorepos)
55
+ envsync init --monorepo
56
+
57
+ # Generate .dev.vars for local development
58
+ envsync dev
59
+
60
+ # Push secrets to staging
61
+ envsync push staging
62
+
63
+ # Validate nothing is missing before deploying
64
+ envsync validate
65
+ ```
66
+
67
+ ### Requirements
68
+
69
+ - [Node.js](https://nodejs.org) >= 18 or [Bun](https://bun.sh)
70
+ - [wrangler](https://developers.cloudflare.com/workers/wrangler/) CLI (peer dependency, for push/pull/diff)
71
+ - [dotenvx](https://dotenvx.com) (optional, for encryption)
72
+
73
+ ---
74
+
75
+ ## Commands
76
+
77
+ ### `envsync dev` — Generate `.dev.vars`
78
+
79
+ The command you'll use most. Merges `.env` + `.env.local` and writes `.dev.vars` for each app.
80
+
81
+ ```bash
82
+ envsync dev # All apps
83
+ envsync dev api # Just api
84
+ envsync dev api web # Multiple apps
85
+ envsync dev --env staging # Use staging values for local dev
86
+ ```
87
+
88
+ ```
89
+ $ envsync dev
90
+
91
+ apps/api/.dev.vars
92
+ ├ DATABASE_URL ← .env
93
+ ├ TWITCH_CLIENT_SECRET ← .env (shared)
94
+ ├ TWITCH_CLIENT_ID ← .env (shared)
95
+ ├ JWT_SECRET ← .env (shared)
96
+ ├ API_URL ← .env
97
+ └ OAUTH_REDIRECT_URL ← .env.local (per-dev override)
98
+
99
+ apps/web/.dev.vars
100
+ ├ AUTH_SECRET ← .env
101
+ ├ VITE_API_URL ← .env
102
+ └ VITE_OAUTH_REDIRECT_URL ← .env.local (per-dev override)
103
+
104
+ ⚠ Missing in .env.local: DEV_TUNNEL_URL (required per-dev override)
105
+ → echo "DEV_TUNNEL_URL=https://your-tunnel.example.com" >> .env.local
106
+
107
+ Done!
108
+ ```
109
+
110
+ Every key shows exactly where its value came from. Missing per-dev overrides are caught immediately.
111
+
112
+ ---
113
+
114
+ ### `envsync push` — Deploy secrets
115
+
116
+ Push secrets to Cloudflare Workers via `wrangler secret bulk`. One command, all workers.
117
+
118
+ ```bash
119
+ envsync push staging # All apps → staging workers
120
+ envsync push production # All apps → production workers
121
+ envsync push staging api # Just api's staging worker
122
+ envsync push production --shared # Only shared secrets (JWT_SECRET, etc.)
123
+ ```
124
+
125
+ ```
126
+ $ envsync push staging --dry-run
127
+
128
+ Pushing secrets for api → my-app-api-staging (staging)...
129
+ Would push 4 secrets to worker "my-app-api-staging"
130
+ DATABASE_URL
131
+ TWITCH_CLIENT_ID (shared)
132
+ TWITCH_CLIENT_SECRET (shared)
133
+ JWT_SECRET (shared)
134
+
135
+ Pushing secrets for web → my-app-web-staging (staging)...
136
+ Would push 1 secrets to worker "my-app-web-staging"
137
+ AUTH_SECRET
138
+
139
+ Done!
140
+ ```
141
+
142
+ Use `--dry-run` to preview. Use `--force` to skip confirmation prompts (CI-friendly).
143
+
144
+ ---
145
+
146
+ ### `envsync diff` — Compare environments
147
+
148
+ Two modes: **local vs remote** and **env vs env**.
149
+
150
+ ```bash
151
+ # Local .env.production vs what's actually on Cloudflare
152
+ envsync diff production
153
+ envsync diff production api
154
+
155
+ # Compare two environments side-by-side
156
+ envsync diff staging production
157
+ ```
158
+
159
+ ```
160
+ $ envsync diff staging production
161
+
162
+ stream-collector
163
+ TWITCH_CLIENT_ID stag**** prod**** ✔ expected
164
+ TWITCH_CLIENT_SECRET stag**** prod**** ✔ expected
165
+ YOUTUBE_API_KEY stag**** (missing) ✘ missing in production!
166
+
167
+ 1 key(s) missing
168
+ ```
169
+
170
+ Catch missing keys before they break production.
171
+
172
+ ---
173
+
174
+ ### `envsync validate` — Catch missing keys
175
+
176
+ Checks all apps across all environments against `.env.example`.
177
+
178
+ ```bash
179
+ envsync validate # All environments, all apps
180
+ envsync validate staging # Just staging
181
+ envsync validate staging api # Just api in staging
182
+ ```
183
+
184
+ ```
185
+ $ envsync validate
186
+
187
+ Checking against .env.example...
188
+
189
+ local
190
+ ✔ api: all 8 keys present
191
+ ✔ web: all 6 keys present
192
+ ✔ stream-collector: all 6 keys present
193
+
194
+ staging
195
+ ✔ api: all 6 keys present
196
+ ✔ web: all 3 keys present
197
+ ✔ stream-collector: all 4 keys present
198
+
199
+ production
200
+ ✔ api: all 6 keys present
201
+ ✔ web: all 3 keys present
202
+ ✘ stream-collector: 3/4 keys
203
+ missing: YOUTUBE_API_KEY
204
+
205
+ ⚠ 1 environment(s) have issues
206
+ ```
207
+
208
+ Exits with code 1 on failure — plug it into CI.
209
+
210
+ ---
211
+
212
+ ### `envsync pull` — Scaffold from remote
213
+
214
+ Pull secret key names from Cloudflare and scaffold empty entries in your local `.env` file. (Values are not available via the API — only key names.)
215
+
216
+ ```bash
217
+ envsync pull staging
218
+ envsync pull production api
219
+ ```
220
+
221
+ ---
222
+
223
+ ### `envsync list` — See the full picture
224
+
225
+ ```bash
226
+ envsync list # Summary table
227
+ envsync list api --keys # Detailed key list for one app
228
+ ```
229
+
230
+ ```
231
+ $ envsync list
232
+
233
+ App local staging production
234
+ ──────────────── ───────────────── ─────────────────────────── ───────────────────
235
+ api (dev) my-app-api-staging my-app-api
236
+ 4 secrets, 2 vars 4 secrets, 2 vars 4 secrets, 2 vars
237
+
238
+ web (dev) my-app-web-staging my-app-web
239
+ 1 secret, 2 vars 1 secret, 2 vars 1 secret, 2 vars
240
+
241
+ stream-collector (dev) my-app-collector-staging my-app-collector
242
+ 4 secrets 4 secrets 3 secrets
243
+
244
+ Shared secrets (3): JWT_SECRET, TWITCH_CLIENT_ID, TWITCH_CLIENT_SECRET
245
+ Per-dev overrides (local only): OAUTH_REDIRECT_URL, DEV_TUNNEL_URL, VITE_OAUTH_REDIRECT_URL
246
+
247
+ .env files status:
248
+ ├ .env ✔ (11 keys)
249
+ ├ .env.staging ✔ (11 keys)
250
+ ├ .env.production ✔ (10 keys)
251
+ └ .env.local ✔ (3 overrides)
252
+ ```
253
+
254
+ ---
255
+
256
+ ### `envsync init` — Project setup
257
+
258
+ Interactive setup that scans your repo and generates everything.
259
+
260
+ ```bash
261
+ envsync init # Single project
262
+ envsync init --monorepo # Scans for wrangler.jsonc files
263
+ ```
264
+
265
+ What it does:
266
+ - Scans `wrangler.jsonc` files to discover workers and environments
267
+ - Detects shared secrets across apps
268
+ - Creates `envsync.config.ts`, `.env.example`, and empty `.env.{environment}` files
269
+ - Adds `.env.local`, `.env.keys`, `**/.dev.vars` to `.gitignore`
270
+ - Registers the custom Git merge driver in `.gitattributes`
271
+
272
+ ---
273
+
274
+ ### `envsync normalize` — Sort keys
275
+
276
+ Alphabetically sorts keys in all `.env*` files. Reduces diff noise, prevents merge conflicts.
277
+
278
+ ```bash
279
+ envsync normalize # All .env* files recursively
280
+ envsync normalize .env.staging # Specific file
281
+ ```
282
+
283
+ ---
284
+
285
+ ### `envsync merge` — Git merge driver
286
+
287
+ A 3-way merge driver that understands dotenvx encryption. Registered automatically by `envsync init`.
288
+
289
+ ```
290
+ # .gitattributes (auto-generated)
291
+ .env merge=envsync
292
+ .env.* merge=envsync
293
+ ```
294
+
295
+ How it works:
296
+
297
+ 1. Decrypts all three versions (base, ours, theirs)
298
+ 2. 3-way merge at the **key level** — not the encrypted ciphertext
299
+ 3. Only real conflicts get conflict markers
300
+ 4. Re-encrypts the merged result
301
+
302
+ No more fake conflicts from identical values with different ciphertext.
303
+
304
+ ---
305
+
306
+ ## Configuration
307
+
308
+ ### `envsync.config.ts`
309
+
310
+ The recommended way to configure envsync. Full type checking, autocomplete, and comments.
311
+
312
+ ```ts
313
+ import { defineConfig } from "cf-envsync";
314
+
315
+ export default defineConfig({
316
+ environments: ["local", "staging", "production"],
317
+
318
+ envFiles: {
319
+ pattern: ".env.{env}", // local → .env, staging → .env.staging
320
+ local: ".env.local", // per-developer overrides (gitignored)
321
+ perApp: true, // allow apps/api/.env.staging etc.
322
+ },
323
+
324
+ encryption: "dotenvx",
325
+
326
+ apps: {
327
+ api: {
328
+ path: "apps/api",
329
+ workers: {
330
+ staging: "my-api-staging",
331
+ production: "my-api",
332
+ },
333
+ secrets: ["DATABASE_URL", "JWT_SECRET"],
334
+ vars: ["API_URL", "ENVIRONMENT"],
335
+ },
336
+ web: {
337
+ path: "apps/web",
338
+ workers: {
339
+ staging: "my-web-staging",
340
+ production: "my-web",
341
+ },
342
+ secrets: ["AUTH_SECRET"],
343
+ vars: ["VITE_API_URL", "VITE_APP_URL"],
344
+ },
345
+ },
346
+
347
+ shared: ["JWT_SECRET"],
348
+
349
+ local: {
350
+ overrides: ["DEV_TUNNEL_URL"],
351
+ perApp: {
352
+ api: ["OAUTH_REDIRECT_URL"],
353
+ web: ["VITE_OAUTH_REDIRECT_URL"],
354
+ },
355
+ },
356
+ });
357
+ ```
358
+
359
+ <details>
360
+ <summary><strong>Also works with plain JSON</strong></summary>
361
+
362
+ `envsync.json` and `envsync.jsonc` are also supported:
363
+
364
+ ```jsonc
365
+ {
366
+ "environments": ["local", "staging", "production"],
367
+ "envFiles": {
368
+ "pattern": ".env.{env}",
369
+ "local": ".env.local",
370
+ "perApp": true
371
+ },
372
+ "encryption": "dotenvx",
373
+ "apps": {
374
+ "api": {
375
+ "path": "apps/api",
376
+ "workers": { "staging": "my-api-staging", "production": "my-api" },
377
+ "secrets": ["DATABASE_URL", "JWT_SECRET"],
378
+ "vars": ["API_URL", "ENVIRONMENT"]
379
+ }
380
+ }
381
+ }
382
+ ```
383
+
384
+ </details>
385
+
386
+ <details>
387
+ <summary><strong>JSDoc (for .js configs)</strong></summary>
388
+
389
+ If you prefer plain JavaScript, use JSDoc for type checking:
390
+
391
+ ```js
392
+ // envsync.config.js
393
+ /** @type {import("cf-envsync").EnvSyncConfig} */
394
+ export default {
395
+ environments: ["local", "staging", "production"],
396
+ // ...
397
+ };
398
+ ```
399
+
400
+ </details>
401
+
402
+ <details>
403
+ <summary><strong>Config reference</strong></summary>
404
+
405
+ | Field | Type | Description |
406
+ |-------|------|-------------|
407
+ | `environments` | `string[]` | Available environments |
408
+ | `envFiles.pattern` | `string` | File naming pattern. `{env}` is replaced. `local` falls back to `.env` |
409
+ | `envFiles.local` | `string` | Per-developer override file (gitignored) |
410
+ | `envFiles.perApp` | `boolean` | Allow per-app `.env.{env}` files for app-specific overrides |
411
+ | `encryption` | `"dotenvx" \| "none"` | Encryption method for `.env` files |
412
+ | `apps.{name}.path` | `string` | Path to app directory relative to project root |
413
+ | `apps.{name}.workers` | `Record<string, string>` | Worker name per environment |
414
+ | `apps.{name}.secrets` | `string[]` | Secret keys pushed via `wrangler secret bulk` |
415
+ | `apps.{name}.vars` | `string[]` | Non-secret env vars (not pushed as secrets) |
416
+ | `shared` | `string[]` | Keys with the same value across multiple apps |
417
+ | `local.overrides` | `string[]` | Keys each developer must set in `.env.local` |
418
+ | `local.perApp` | `Record<string, string[]>` | Per-app developer override keys |
419
+
420
+ </details>
421
+
422
+ Config file search order: `envsync.config.ts` > `.js` > `.mjs` > `envsync.json` > `envsync.jsonc`
423
+
424
+ ### File structure
425
+
426
+ ```
427
+ project/
428
+ ├── envsync.config.ts # Config (committed)
429
+
430
+ ├── .env # Local shared secrets (encrypted, committed)
431
+ ├── .env.staging # Staging secrets (encrypted, committed)
432
+ ├── .env.production # Production secrets (encrypted, committed)
433
+ ├── .env.local # Per-developer overrides (gitignored)
434
+ ├── .env.example # Key reference (committed)
435
+ ├── .env.keys # dotenvx private keys (gitignored)
436
+
437
+ ├── apps/
438
+ │ ├── api/
439
+ │ │ ├── wrangler.jsonc
440
+ │ │ ├── .dev.vars # ← generated by envsync dev
441
+ │ │ └── .env.staging # [optional] api-specific staging overrides
442
+ │ ├── web/
443
+ │ │ ├── wrangler.jsonc
444
+ │ │ └── .dev.vars # ← generated
445
+ │ └── stream-collector/
446
+ │ ├── wrangler.jsonc
447
+ │ ├── .dev.vars # ← generated
448
+ │ └── .env # app-specific secrets (YOUTUBE_API_KEY, etc.)
449
+
450
+ └── .gitignore # .env.local, .env.keys, **/.dev.vars
451
+ ```
452
+
453
+ ### Merge priority
454
+
455
+ Values are merged in this order (last wins):
456
+
457
+ ```
458
+ root .env.{env} → app .env.{env} → .env.local (local env only)
459
+ ```
460
+
461
+ ---
462
+
463
+ ## Single project
464
+
465
+ Works the same way. Just one app with `path: "."`:
466
+
467
+ ```ts
468
+ import { defineConfig } from "cf-envsync";
469
+
470
+ export default defineConfig({
471
+ environments: ["local", "staging", "production"],
472
+ envFiles: { pattern: ".env.{env}", local: ".env.local", perApp: false },
473
+ encryption: "dotenvx",
474
+ apps: {
475
+ default: {
476
+ path: ".",
477
+ workers: { staging: "my-worker-staging", production: "my-worker" },
478
+ secrets: ["DATABASE_URL", "API_KEY"],
479
+ },
480
+ },
481
+ });
482
+ ```
483
+
484
+ ---
485
+
486
+ ## Why?
487
+
488
+ <table>
489
+ <tr><th>Tool</th><th>What it does</th><th>What it doesn't</th></tr>
490
+ <tr><td><strong>dotenvx</strong></td><td>Encrypts .env, safe to commit</td><td>Git merge conflicts, no CF Workers sync</td></tr>
491
+ <tr><td><strong>Infisical / Doppler</strong></td><td>Centralized secrets, CF Workers sync</td><td>SaaS dependency, overkill for small teams</td></tr>
492
+ <tr><td><strong>wrangler secret</strong></td><td>Sets CF Workers secrets</td><td>One key at a time, no bulk diff, no .env integration</td></tr>
493
+ <tr><td><strong>CF Secrets Store</strong></td><td>Account-level secrets</td><td>Broken local dev, no .env sync</td></tr>
494
+ <tr><td><strong>.dev.vars</strong></td><td>Local dev secrets</td><td>Doesn't sync with anything</td></tr>
495
+ </table>
496
+
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.
498
+
499
+ No SaaS. No dashboard. Just a CLI, your `.env` files, and Cloudflare's API.
500
+
501
+ ---
502
+
503
+ ## Tech Stack
504
+
505
+ | | |
506
+ |---|---|
507
+ | **Runtime** | [Node.js](https://nodejs.org) >= 18 or [Bun](https://bun.sh) |
508
+ | **CLI framework** | [citty](https://github.com/unjs/citty) |
509
+ | **Output** | [consola](https://github.com/unjs/consola) |
510
+ | **Config loading** | [jiti](https://github.com/unjs/jiti) |
511
+ | **Encryption** | [@dotenvx/dotenvx](https://dotenvx.com) |
512
+ | **CF Secrets** | [wrangler](https://developers.cloudflare.com/workers/wrangler/) CLI (shell out) |
513
+
514
+ ---
515
+
516
+ ## License
517
+
518
+ MIT