cf-envsync 0.3.0 → 0.3.2
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 +163 -31
- package/dist/index.js +120 -37
- package/package.json +1 -1
- package/src/types/config.ts +5 -0
package/README.md
CHANGED
|
@@ -73,16 +73,16 @@ pnpm add -D cf-envsync
|
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
75
|
# Initialize (scans wrangler.jsonc files in monorepos)
|
|
76
|
-
envsync init --monorepo
|
|
76
|
+
npx envsync init --monorepo
|
|
77
77
|
|
|
78
78
|
# Generate .dev.vars for local development
|
|
79
|
-
envsync dev
|
|
79
|
+
npx envsync dev
|
|
80
80
|
|
|
81
81
|
# Push secrets to staging
|
|
82
|
-
envsync push staging
|
|
82
|
+
npx envsync push staging
|
|
83
83
|
|
|
84
84
|
# Validate nothing is missing before deploying
|
|
85
|
-
envsync validate
|
|
85
|
+
npx envsync validate
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
### Requirements
|
|
@@ -93,6 +93,135 @@ envsync validate
|
|
|
93
93
|
|
|
94
94
|
---
|
|
95
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
|
+
npx 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
|
+
npx envsync encrypt staging
|
|
147
|
+
npx 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
|
+
npx 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
|
+
npx envsync push staging --dry-run
|
|
192
|
+
|
|
193
|
+
# Push for real
|
|
194
|
+
npx 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
|
+
npx 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: npx envsync validate
|
|
215
|
+
|
|
216
|
+
- name: Push secrets to production
|
|
217
|
+
run: npx envsync push production --force
|
|
218
|
+
env:
|
|
219
|
+
ENVSYNC_PASSWORD: ${{ secrets.ENVSYNC_PASSWORD }}
|
|
220
|
+
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
96
225
|
## Commands
|
|
97
226
|
|
|
98
227
|
### `envsync dev` — Generate `.dev.vars`
|
|
@@ -100,10 +229,10 @@ envsync validate
|
|
|
100
229
|
The command you'll use most. Merges `.env` + `.env.local` and writes `.dev.vars` for each app.
|
|
101
230
|
|
|
102
231
|
```bash
|
|
103
|
-
envsync dev # All apps
|
|
104
|
-
envsync dev api # Just api
|
|
105
|
-
envsync dev api web # Multiple apps
|
|
106
|
-
envsync dev --env staging # Use staging values for local dev
|
|
232
|
+
npx envsync dev # All apps
|
|
233
|
+
npx envsync dev api # Just api
|
|
234
|
+
npx envsync dev api web # Multiple apps
|
|
235
|
+
npx envsync dev --env staging # Use staging values for local dev
|
|
107
236
|
```
|
|
108
237
|
|
|
109
238
|
```
|
|
@@ -130,6 +259,8 @@ Done!
|
|
|
130
259
|
|
|
131
260
|
Every key shows exactly where its value came from. Missing per-dev overrides are caught immediately.
|
|
132
261
|
|
|
262
|
+
> **Vite / non-wrangler apps:** Set `devFile: ".env.local"` in your app config. See [the tutorial](#4-local-development).
|
|
263
|
+
|
|
133
264
|
---
|
|
134
265
|
|
|
135
266
|
### `envsync push` — Deploy secrets
|
|
@@ -137,10 +268,10 @@ Every key shows exactly where its value came from. Missing per-dev overrides are
|
|
|
137
268
|
Push secrets to Cloudflare Workers via `wrangler secret bulk`. One command, all workers.
|
|
138
269
|
|
|
139
270
|
```bash
|
|
140
|
-
envsync push staging # All apps → staging workers
|
|
141
|
-
envsync push production # All apps → production workers
|
|
142
|
-
envsync push staging api # Just api's staging worker
|
|
143
|
-
envsync push production --shared # Only shared secrets (JWT_SECRET, etc.)
|
|
271
|
+
npx envsync push staging # All apps → staging workers
|
|
272
|
+
npx envsync push production # All apps → production workers
|
|
273
|
+
npx envsync push staging api # Just api's staging worker
|
|
274
|
+
npx envsync push production --shared # Only shared secrets (JWT_SECRET, etc.)
|
|
144
275
|
```
|
|
145
276
|
|
|
146
277
|
```
|
|
@@ -170,11 +301,11 @@ Two modes: **local vs remote** and **env vs env**.
|
|
|
170
301
|
|
|
171
302
|
```bash
|
|
172
303
|
# Local .env.production vs what's actually on Cloudflare
|
|
173
|
-
envsync diff production
|
|
174
|
-
envsync diff production api
|
|
304
|
+
npx envsync diff production
|
|
305
|
+
npx envsync diff production api
|
|
175
306
|
|
|
176
307
|
# Compare two environments side-by-side
|
|
177
|
-
envsync diff staging production
|
|
308
|
+
npx envsync diff staging production
|
|
178
309
|
```
|
|
179
310
|
|
|
180
311
|
```
|
|
@@ -197,9 +328,9 @@ Catch missing keys before they break production.
|
|
|
197
328
|
Checks all apps across all environments against `.env.example`.
|
|
198
329
|
|
|
199
330
|
```bash
|
|
200
|
-
envsync validate # All environments, all apps
|
|
201
|
-
envsync validate staging # Just staging
|
|
202
|
-
envsync validate staging api # Just api in staging
|
|
331
|
+
npx envsync validate # All environments, all apps
|
|
332
|
+
npx envsync validate staging # Just staging
|
|
333
|
+
npx envsync validate staging api # Just api in staging
|
|
203
334
|
```
|
|
204
335
|
|
|
205
336
|
```
|
|
@@ -235,8 +366,8 @@ Exits with code 1 on failure — plug it into CI.
|
|
|
235
366
|
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.)
|
|
236
367
|
|
|
237
368
|
```bash
|
|
238
|
-
envsync pull staging
|
|
239
|
-
envsync pull production api
|
|
369
|
+
npx envsync pull staging
|
|
370
|
+
npx envsync pull production api
|
|
240
371
|
```
|
|
241
372
|
|
|
242
373
|
---
|
|
@@ -244,8 +375,8 @@ envsync pull production api
|
|
|
244
375
|
### `envsync list` — See the full picture
|
|
245
376
|
|
|
246
377
|
```bash
|
|
247
|
-
envsync list # Summary table
|
|
248
|
-
envsync list api --keys # Detailed key list for one app
|
|
378
|
+
npx envsync list # Summary table
|
|
379
|
+
npx envsync list api --keys # Detailed key list for one app
|
|
249
380
|
```
|
|
250
381
|
|
|
251
382
|
```
|
|
@@ -279,8 +410,8 @@ $ envsync list
|
|
|
279
410
|
Interactive setup that scans your repo and generates everything.
|
|
280
411
|
|
|
281
412
|
```bash
|
|
282
|
-
envsync init # Single project
|
|
283
|
-
envsync init --monorepo # Scans for wrangler.jsonc files
|
|
413
|
+
npx envsync init # Single project
|
|
414
|
+
npx envsync init --monorepo # Scans for wrangler.jsonc files
|
|
284
415
|
```
|
|
285
416
|
|
|
286
417
|
What it does:
|
|
@@ -298,8 +429,8 @@ What it does:
|
|
|
298
429
|
Alphabetically sorts keys in all `.env*` files. Reduces diff noise, prevents merge conflicts.
|
|
299
430
|
|
|
300
431
|
```bash
|
|
301
|
-
envsync normalize # All .env* files recursively
|
|
302
|
-
envsync normalize .env.staging # Specific file
|
|
432
|
+
npx envsync normalize # All .env* files recursively
|
|
433
|
+
npx envsync normalize .env.staging # Specific file
|
|
303
434
|
```
|
|
304
435
|
|
|
305
436
|
---
|
|
@@ -309,9 +440,9 @@ envsync normalize .env.staging # Specific file
|
|
|
309
440
|
Encrypts plain-text values in a `.env` file using password-based encryption (AES-256-GCM). Only available when `encryption: "password"`.
|
|
310
441
|
|
|
311
442
|
```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
|
|
443
|
+
npx envsync encrypt staging # Encrypt all plain values in .env.staging
|
|
444
|
+
npx envsync encrypt production # Encrypt .env.production
|
|
445
|
+
npx envsync encrypt staging --dry-run # Preview without writing
|
|
315
446
|
```
|
|
316
447
|
|
|
317
448
|
```
|
|
@@ -459,6 +590,7 @@ export default {
|
|
|
459
590
|
| `apps.{name}.workers` | `Record<string, string>` | Worker name per environment |
|
|
460
591
|
| `apps.{name}.secrets` | `string[]` | Secret keys pushed via `wrangler secret bulk` |
|
|
461
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 |
|
|
462
594
|
| `shared` | `string[]` | Keys with the same value across multiple apps |
|
|
463
595
|
| `local.overrides` | `string[]` | Keys each developer must set in `.env.local` |
|
|
464
596
|
| `local.perApp` | `Record<string, string[]>` | Per-app developer override keys |
|
|
@@ -505,8 +637,8 @@ ENVSYNC_PASSWORD_PRODUCTION=production-password
|
|
|
505
637
|
EOF
|
|
506
638
|
|
|
507
639
|
# Encrypt plain values
|
|
508
|
-
envsync encrypt staging
|
|
509
|
-
envsync encrypt production
|
|
640
|
+
npx envsync encrypt staging
|
|
641
|
+
npx envsync encrypt production
|
|
510
642
|
```
|
|
511
643
|
|
|
512
644
|
### File structure
|
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) {
|
|
@@ -13166,6 +13176,16 @@ async function loadEnvFile(filePath, env2, projectRoot, encryption) {
|
|
|
13166
13176
|
return {};
|
|
13167
13177
|
}
|
|
13168
13178
|
const content = await readFile(filePath);
|
|
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
|
+
}
|
|
13169
13189
|
if (encryption === "password") {
|
|
13170
13190
|
const envMap = parsePlainEnv(content);
|
|
13171
13191
|
const password = findPassword(env2, projectRoot);
|
|
@@ -13228,17 +13248,22 @@ function getLocalOverridePath(config) {
|
|
|
13228
13248
|
return join3(config.projectRoot, config.raw.envFiles.local);
|
|
13229
13249
|
}
|
|
13230
13250
|
var init_env_file = __esm(() => {
|
|
13251
|
+
init_dist2();
|
|
13231
13252
|
init_encryption();
|
|
13232
13253
|
init_config();
|
|
13233
13254
|
init_fs();
|
|
13234
13255
|
});
|
|
13235
13256
|
|
|
13236
13257
|
// src/core/resolver.ts
|
|
13237
|
-
import { normalize } from "node:path";
|
|
13258
|
+
import { normalize, relative } from "node:path";
|
|
13238
13259
|
async function resolveAppEnv(config, app, environment) {
|
|
13239
13260
|
const layers = [];
|
|
13240
13261
|
const encryption = config.raw.encryption;
|
|
13241
13262
|
const rootEnvPath = getRootEnvPath(config, environment);
|
|
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
|
+
}
|
|
13242
13267
|
const rootEnv = await loadEnvFile(rootEnvPath, environment, config.projectRoot, encryption);
|
|
13243
13268
|
if (Object.keys(rootEnv).length > 0) {
|
|
13244
13269
|
layers.push({ source: rootEnvPath, map: rootEnv });
|
|
@@ -13281,8 +13306,12 @@ function findMissingOverrides(config, app, localEnv) {
|
|
|
13281
13306
|
}
|
|
13282
13307
|
return missing;
|
|
13283
13308
|
}
|
|
13309
|
+
var warnedMissingFiles;
|
|
13284
13310
|
var init_resolver = __esm(() => {
|
|
13311
|
+
init_dist2();
|
|
13285
13312
|
init_env_file();
|
|
13313
|
+
init_fs();
|
|
13314
|
+
warnedMissingFiles = new Set;
|
|
13286
13315
|
});
|
|
13287
13316
|
|
|
13288
13317
|
// src/commands/dev.ts
|
|
@@ -13290,13 +13319,13 @@ var exports_dev = {};
|
|
|
13290
13319
|
__export(exports_dev, {
|
|
13291
13320
|
default: () => dev_default
|
|
13292
13321
|
});
|
|
13293
|
-
import { join as join4, relative } from "node:path";
|
|
13322
|
+
import { join as join4, relative as relative2 } from "node:path";
|
|
13294
13323
|
function parseAppNames(args) {
|
|
13295
13324
|
const rest = args._;
|
|
13296
13325
|
return rest?.length ? rest : undefined;
|
|
13297
13326
|
}
|
|
13298
13327
|
function formatSource(source, key, projectRoot, sharedKeys, localOverrideKeys) {
|
|
13299
|
-
const rel =
|
|
13328
|
+
const rel = relative2(projectRoot, source);
|
|
13300
13329
|
if (localOverrideKeys.has(key))
|
|
13301
13330
|
return `${rel} (per-dev override)`;
|
|
13302
13331
|
if (sharedKeys.has(key))
|
|
@@ -13352,26 +13381,21 @@ var init_dev = __esm(() => {
|
|
|
13352
13381
|
const localEnv = await loadEnvFile(localOverridePath);
|
|
13353
13382
|
for (const app of apps) {
|
|
13354
13383
|
const resolved = await resolveAppEnv(config, app, environment);
|
|
13355
|
-
const devVarsPath = join4(app.absolutePath, ".dev.vars");
|
|
13356
|
-
const relDevVars = relative(config.projectRoot, devVarsPath);
|
|
13357
13384
|
if (Object.keys(resolved.map).length === 0) {
|
|
13358
13385
|
consola.warn(` No env vars resolved for ${app.name}. Skipping.`);
|
|
13359
13386
|
continue;
|
|
13360
13387
|
}
|
|
13361
|
-
|
|
13362
|
-
|
|
13363
|
-
|
|
13364
|
-
|
|
13365
|
-
|
|
13366
|
-
|
|
13367
|
-
|
|
13368
|
-
|
|
13369
|
-
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}`);
|
|
13370
13398
|
}
|
|
13371
|
-
} else {
|
|
13372
|
-
await writeEnvFile(devVarsPath, resolved.map);
|
|
13373
|
-
consola.log(`
|
|
13374
|
-
${relDevVars}`);
|
|
13375
13399
|
for (let i2 = 0;i2 < resolved.entries.length; i2++) {
|
|
13376
13400
|
const entry = resolved.entries[i2];
|
|
13377
13401
|
const isLast = i2 === resolved.entries.length - 1;
|
|
@@ -13385,12 +13409,18 @@ var init_dev = __esm(() => {
|
|
|
13385
13409
|
if (missing.length > 0) {
|
|
13386
13410
|
for (const key of missing) {
|
|
13387
13411
|
consola.warn(`
|
|
13388
|
-
⚠ Missing in ${
|
|
13389
|
-
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)}`);
|
|
13390
13414
|
}
|
|
13391
13415
|
}
|
|
13392
13416
|
}
|
|
13393
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
|
+
}
|
|
13394
13424
|
consola.success(`
|
|
13395
13425
|
Done!`);
|
|
13396
13426
|
}
|
|
@@ -13443,7 +13473,7 @@ var init_process = () => {};
|
|
|
13443
13473
|
|
|
13444
13474
|
// src/core/wrangler.ts
|
|
13445
13475
|
async function checkWrangler() {
|
|
13446
|
-
const result = await exec(["wrangler", "--version"]);
|
|
13476
|
+
const result = await exec(["npx", "wrangler", "--version"]);
|
|
13447
13477
|
return result.success;
|
|
13448
13478
|
}
|
|
13449
13479
|
function envFlag(environment) {
|
|
@@ -13454,6 +13484,7 @@ function envFlag(environment) {
|
|
|
13454
13484
|
async function pushSecrets(workerName, secrets, environment, cwd) {
|
|
13455
13485
|
const json = JSON.stringify(secrets);
|
|
13456
13486
|
const args = [
|
|
13487
|
+
"npx",
|
|
13457
13488
|
"wrangler",
|
|
13458
13489
|
"secret",
|
|
13459
13490
|
"bulk",
|
|
@@ -13473,6 +13504,7 @@ async function pushSecrets(workerName, secrets, environment, cwd) {
|
|
|
13473
13504
|
}
|
|
13474
13505
|
async function listSecrets(workerName, environment, cwd) {
|
|
13475
13506
|
const args = [
|
|
13507
|
+
"npx",
|
|
13476
13508
|
"wrangler",
|
|
13477
13509
|
"secret",
|
|
13478
13510
|
"list",
|
|
@@ -13550,7 +13582,7 @@ var init_push = __esm(() => {
|
|
|
13550
13582
|
if (!args["dry-run"]) {
|
|
13551
13583
|
const hasWrangler = await checkWrangler();
|
|
13552
13584
|
if (!hasWrangler) {
|
|
13553
|
-
consola.error("wrangler CLI not found. Install it with: npm i -
|
|
13585
|
+
consola.error("wrangler CLI not found. Install it with: npm i -D wrangler");
|
|
13554
13586
|
process.exit(1);
|
|
13555
13587
|
}
|
|
13556
13588
|
}
|
|
@@ -13572,14 +13604,33 @@ var init_push = __esm(() => {
|
|
|
13572
13604
|
consola.warn("No apps to process.");
|
|
13573
13605
|
return;
|
|
13574
13606
|
}
|
|
13607
|
+
if (args.force && !args["dry-run"]) {
|
|
13608
|
+
const targets = apps.map((app) => {
|
|
13609
|
+
const w2 = getWorkerName(app, environment);
|
|
13610
|
+
return w2 ? `${app.name} → ${w2}` : null;
|
|
13611
|
+
}).filter(Boolean);
|
|
13612
|
+
if (targets.length > 0) {
|
|
13613
|
+
consola.warn(`Force-pushing to ${environment}: ${targets.join(", ")}`);
|
|
13614
|
+
}
|
|
13615
|
+
}
|
|
13575
13616
|
const sharedKeys = new Set(config.raw.shared ?? []);
|
|
13576
|
-
|
|
13617
|
+
let hasFailure = false;
|
|
13618
|
+
if (args.shared) {
|
|
13619
|
+
if (sharedKeys.size === 0) {
|
|
13620
|
+
consola.error("No shared keys defined in config. Nothing to push with --shared.");
|
|
13621
|
+
process.exit(1);
|
|
13622
|
+
}
|
|
13623
|
+
consola.info(`--shared: pushing only shared keys (${[...sharedKeys].join(", ")})`);
|
|
13624
|
+
}
|
|
13625
|
+
for (let i2 = 0;i2 < apps.length; i2++) {
|
|
13626
|
+
const app = apps[i2];
|
|
13627
|
+
const progress = apps.length > 1 ? `[${i2 + 1}/${apps.length}] ` : "";
|
|
13577
13628
|
const workerName = getWorkerName(app, environment);
|
|
13578
13629
|
if (!workerName) {
|
|
13579
|
-
consola.warn(
|
|
13630
|
+
consola.warn(`${progress}No worker defined for ${app.name} in ${environment}. Skipping.`);
|
|
13580
13631
|
continue;
|
|
13581
13632
|
}
|
|
13582
|
-
consola.start(
|
|
13633
|
+
consola.start(`${progress}Pushing secrets for ${app.name} → ${workerName} (${environment})...`);
|
|
13583
13634
|
const resolved = await resolveAppEnv(config, app, environment);
|
|
13584
13635
|
let secretsToPush;
|
|
13585
13636
|
if (args.shared) {
|
|
@@ -13600,7 +13651,8 @@ var init_push = __esm(() => {
|
|
|
13600
13651
|
}
|
|
13601
13652
|
const keyCount = Object.keys(secretsToPush).length;
|
|
13602
13653
|
if (keyCount === 0) {
|
|
13603
|
-
|
|
13654
|
+
const reason = args.shared ? " (no shared keys for this app)" : "";
|
|
13655
|
+
consola.warn(` No secrets to push for ${app.name}${reason}. Skipping.`);
|
|
13604
13656
|
continue;
|
|
13605
13657
|
}
|
|
13606
13658
|
if (args["dry-run"]) {
|
|
@@ -13623,8 +13675,13 @@ var init_push = __esm(() => {
|
|
|
13623
13675
|
consola.success(` Pushed ${keyCount} secrets to ${workerName}`);
|
|
13624
13676
|
} else {
|
|
13625
13677
|
consola.error(` Failed to push secrets to ${workerName}`);
|
|
13678
|
+
hasFailure = true;
|
|
13626
13679
|
}
|
|
13627
13680
|
}
|
|
13681
|
+
if (hasFailure) {
|
|
13682
|
+
consola.error("Some pushes failed.");
|
|
13683
|
+
process.exit(1);
|
|
13684
|
+
}
|
|
13628
13685
|
consola.success("Done!");
|
|
13629
13686
|
}
|
|
13630
13687
|
});
|
|
@@ -13666,7 +13723,7 @@ var init_pull = __esm(() => {
|
|
|
13666
13723
|
}
|
|
13667
13724
|
const hasWrangler = await checkWrangler();
|
|
13668
13725
|
if (!hasWrangler) {
|
|
13669
|
-
consola.error("wrangler CLI not found. Install it with: npm i -
|
|
13726
|
+
consola.error("wrangler CLI not found. Install it with: npm i -D wrangler");
|
|
13670
13727
|
process.exit(1);
|
|
13671
13728
|
}
|
|
13672
13729
|
const rawConfig = await loadConfig();
|
|
@@ -13760,11 +13817,18 @@ var init_validate = __esm(() => {
|
|
|
13760
13817
|
let environments;
|
|
13761
13818
|
let appNames;
|
|
13762
13819
|
if (envArg && config.environments.includes(envArg)) {
|
|
13820
|
+
if (envArg in config.apps) {
|
|
13821
|
+
consola.error(`"${envArg}" is both an environment and an app name. This is ambiguous.`);
|
|
13822
|
+
consola.info(` To validate environment: envsync validate ${envArg} --
|
|
13823
|
+
` + ` To validate app: envsync validate -- ${envArg}`);
|
|
13824
|
+
process.exit(1);
|
|
13825
|
+
}
|
|
13763
13826
|
environments = [envArg];
|
|
13764
13827
|
appNames = parseAppNames4(args);
|
|
13765
13828
|
} else if (envArg) {
|
|
13766
13829
|
environments = config.environments;
|
|
13767
13830
|
appNames = parseAppNames4(args, 0);
|
|
13831
|
+
consola.info(`"${envArg}" is not an environment. Treating as app name. Validating all environments.`);
|
|
13768
13832
|
} else {
|
|
13769
13833
|
environments = config.environments;
|
|
13770
13834
|
appNames = undefined;
|
|
@@ -13783,10 +13847,12 @@ var init_validate = __esm(() => {
|
|
|
13783
13847
|
...config.raw.local?.overrides ?? [],
|
|
13784
13848
|
...Object.values(config.raw.local?.perApp ?? {}).flat()
|
|
13785
13849
|
]);
|
|
13850
|
+
const totalChecks = environments.length * apps.length;
|
|
13786
13851
|
consola.log(`
|
|
13787
|
-
Checking against .env.example
|
|
13852
|
+
Checking against .env.example... (${environments.length} env × ${apps.length} app${apps.length > 1 ? "s" : ""})`);
|
|
13788
13853
|
const results = [];
|
|
13789
13854
|
let hasIssues = false;
|
|
13855
|
+
let checkIdx = 0;
|
|
13790
13856
|
for (const environment of environments) {
|
|
13791
13857
|
consola.log(`
|
|
13792
13858
|
${environment}`);
|
|
@@ -13951,8 +14017,15 @@ var init_diff = __esm(() => {
|
|
|
13951
14017
|
process.exit(1);
|
|
13952
14018
|
}
|
|
13953
14019
|
const target = args.target;
|
|
13954
|
-
const
|
|
13955
|
-
|
|
14020
|
+
const isEnv = target && config.environments.includes(target);
|
|
14021
|
+
const isApp = target && target in config.apps;
|
|
14022
|
+
if (isEnv && isApp) {
|
|
14023
|
+
consola.error(`"${target}" is both an environment and an app name. This is ambiguous.`);
|
|
14024
|
+
consola.info(` To compare environments: envsync diff ${env1} ${target} --
|
|
14025
|
+
` + ` To diff local vs remote: envsync diff ${env1} -- ${target}`);
|
|
14026
|
+
process.exit(1);
|
|
14027
|
+
}
|
|
14028
|
+
if (isEnv) {
|
|
13956
14029
|
const env2 = target;
|
|
13957
14030
|
const appNames = parseAppNames5(args, 2);
|
|
13958
14031
|
const apps = resolveApps(config, appNames);
|
|
@@ -13996,7 +14069,7 @@ var init_diff = __esm(() => {
|
|
|
13996
14069
|
} else {
|
|
13997
14070
|
const hasWrangler = await checkWrangler();
|
|
13998
14071
|
if (!hasWrangler) {
|
|
13999
|
-
consola.error("wrangler CLI not found. Install it with: npm i -
|
|
14072
|
+
consola.error("wrangler CLI not found. Install it with: npm i -D wrangler");
|
|
14000
14073
|
process.exit(1);
|
|
14001
14074
|
}
|
|
14002
14075
|
const appNames = target ? [target, ...parseAppNames5(args, 2) ?? []] : parseAppNames5(args, 1);
|
|
@@ -14049,7 +14122,7 @@ var exports_init = {};
|
|
|
14049
14122
|
__export(exports_init, {
|
|
14050
14123
|
default: () => init_default
|
|
14051
14124
|
});
|
|
14052
|
-
import { join as join6, relative as
|
|
14125
|
+
import { join as join6, relative as relative3, basename } from "node:path";
|
|
14053
14126
|
function generateConfigTS(config) {
|
|
14054
14127
|
const lines = [
|
|
14055
14128
|
`import { defineConfig } from "cf-envsync";`,
|
|
@@ -14133,7 +14206,16 @@ var init_init = __esm(() => {
|
|
|
14133
14206
|
const existingConfig = CONFIG_FILES.find((f3) => fileExists(join6(cwd, f3)));
|
|
14134
14207
|
if (existingConfig) {
|
|
14135
14208
|
consola.warn(`${existingConfig} already exists.`);
|
|
14136
|
-
const
|
|
14209
|
+
const existingContent = await readFile(join6(cwd, existingConfig));
|
|
14210
|
+
const lines = existingContent.split(`
|
|
14211
|
+
`);
|
|
14212
|
+
const preview = lines.length > 20 ? [...lines.slice(0, 20), ` ... (${lines.length - 20} more lines)`].join(`
|
|
14213
|
+
`) : existingContent;
|
|
14214
|
+
consola.log(`
|
|
14215
|
+
Current config:
|
|
14216
|
+
${preview}
|
|
14217
|
+
`);
|
|
14218
|
+
const overwrite = await consola.prompt("Overwrite with new config?", {
|
|
14137
14219
|
type: "confirm"
|
|
14138
14220
|
});
|
|
14139
14221
|
if (!overwrite) {
|
|
@@ -14160,12 +14242,13 @@ var init_init = __esm(() => {
|
|
|
14160
14242
|
return (name === "wrangler.json" || name === "wrangler.jsonc") && !f3.includes("node_modules");
|
|
14161
14243
|
});
|
|
14162
14244
|
if (wranglerFiles.length === 0) {
|
|
14163
|
-
consola.warn(
|
|
14245
|
+
consola.warn(`No wrangler.json or wrangler.jsonc files found (searched all subdirectories, excluding node_modules).
|
|
14246
|
+
` + " Falling back to manual configuration.");
|
|
14164
14247
|
}
|
|
14165
14248
|
for (const wranglerFile of wranglerFiles.sort()) {
|
|
14166
14249
|
const fullPath = join6(cwd, wranglerFile);
|
|
14167
14250
|
const appDir = join6(cwd, wranglerFile, "..");
|
|
14168
|
-
const appPath =
|
|
14251
|
+
const appPath = relative3(cwd, appDir);
|
|
14169
14252
|
const appName = basename(appDir);
|
|
14170
14253
|
consola.info(` Found ${wranglerFile}`);
|
|
14171
14254
|
let wranglerConfig = {};
|
package/package.json
CHANGED
package/src/types/config.ts
CHANGED
|
@@ -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
|
}
|