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 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
- consola.warn(`Unknown app: "${name}". Skipping.`);
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 = relative(projectRoot, source);
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
- if (args["dry-run"]) {
13362
- consola.log(`
13363
- ${relDevVars}`);
13364
- for (let i2 = 0;i2 < resolved.entries.length; i2++) {
13365
- const entry = resolved.entries[i2];
13366
- const isLast = i2 === resolved.entries.length - 1;
13367
- const prefix = isLast ? "└" : "├";
13368
- const src2 = formatSource(entry.source, entry.key, config.projectRoot, sharedKeys, localOverrideKeys);
13369
- consola.log(` ${prefix} ${entry.key.padEnd(24)} ← ${src2}`);
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 ${relative(config.projectRoot, localOverridePath)}: ${key} (required per-dev override)`);
13389
- consola.log(` → echo "${key}=<your-value>" >> ${relative(config.projectRoot, localOverridePath)}`);
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 -g wrangler");
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
- for (const app of apps) {
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(` No worker defined for ${app.name} in ${environment}. Skipping.`);
13630
+ consola.warn(`${progress}No worker defined for ${app.name} in ${environment}. Skipping.`);
13580
13631
  continue;
13581
13632
  }
13582
- consola.start(`Pushing secrets for ${app.name} → ${workerName} (${environment})...`);
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
- consola.warn(` No secrets to push for ${app.name}. Skipping.`);
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 -g wrangler");
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 isEnvVsEnv = target && config.environments.includes(target);
13955
- if (isEnvVsEnv) {
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 -g wrangler");
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 relative2, basename } from "node:path";
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 overwrite = await consola.prompt("Overwrite?", {
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("No wrangler config files found. Creating manually.");
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 = relative2(cwd, appDir);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cf-envsync",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Sync .env files to Cloudflare Workers secrets, .dev.vars, and more",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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
  }