@super-repo/envx 0.2.3-b.2 → 0.2.3-b.4

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.
Files changed (42) hide show
  1. package/README.md +970 -104
  2. package/dist/auto.js.map +1 -1
  3. package/dist/chunks/commands-D3eQPQO6.js +1502 -0
  4. package/dist/chunks/commands-D3eQPQO6.js.map +1 -0
  5. package/dist/chunks/{src-CDuEfaCY.js → src-D0n2wHDg.js} +0 -0
  6. package/dist/chunks/src-D0n2wHDg.js.map +1 -0
  7. package/dist/cli.js +1 -1
  8. package/dist/commands/audit.d.ts +13 -0
  9. package/dist/commands/audit.d.ts.map +1 -0
  10. package/dist/commands/bake.d.ts +18 -0
  11. package/dist/commands/bake.d.ts.map +1 -0
  12. package/dist/commands/diff.d.ts +16 -0
  13. package/dist/commands/diff.d.ts.map +1 -0
  14. package/dist/commands/doctor.d.ts +16 -0
  15. package/dist/commands/doctor.d.ts.map +1 -0
  16. package/dist/commands/encrypt.d.ts.map +1 -1
  17. package/dist/commands/hook.d.ts +18 -0
  18. package/dist/commands/hook.d.ts.map +1 -0
  19. package/dist/commands/index.d.ts.map +1 -1
  20. package/dist/commands/index.js +1 -1
  21. package/dist/commands/info.d.ts +10 -0
  22. package/dist/commands/info.d.ts.map +1 -0
  23. package/dist/commands/rotate.d.ts +13 -0
  24. package/dist/commands/rotate.d.ts.map +1 -0
  25. package/dist/commands/run.d.ts.map +1 -1
  26. package/dist/commands/template.d.ts +13 -0
  27. package/dist/commands/template.d.ts.map +1 -0
  28. package/dist/commands/types.d.ts +14 -0
  29. package/dist/commands/types.d.ts.map +1 -0
  30. package/dist/commands/watch.d.ts +14 -0
  31. package/dist/commands/watch.d.ts.map +1 -0
  32. package/dist/index.d.ts +16 -4
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +57 -28
  35. package/dist/index.js.map +1 -1
  36. package/docs/auto-detection.md +217 -0
  37. package/docs/configuration.md +224 -0
  38. package/docs/recipes.md +234 -0
  39. package/package.json +6 -4
  40. package/dist/chunks/commands-B8vc6UKO.js +0 -354
  41. package/dist/chunks/commands-B8vc6UKO.js.map +0 -1
  42. package/dist/chunks/src-CDuEfaCY.js.map +0 -1
@@ -0,0 +1,1502 @@
1
+ import { E as decryptValueAsymmetric, S as parseEnv, _ as resolveEnvPaths, a as rotateFiles, b as expandRecord, c as defaultKeysPath, d as DEFAULT_NODE_ENV_MAP, f as detectEnvironment, g as resolveCwdOrWorkspace, h as loadEnv, i as auditFiles, k as isEncrypted, l as readKeysFile, m as listEnvFiles, n as loadDotenvxConfig, o as decryptFiles, p as findWorkspaceRoot, s as encryptFiles, t as writeProcessed, v as validateCmdVariable, w as toRecord, x as log, y as expandEnvSrc } from "./src-D0n2wHDg.js";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import yargs from "yargs";
5
+ import { execSync, spawn } from "child_process";
6
+ //#region src/commands/audit.ts
7
+ /**
8
+ * Walk the repo looking for plaintext secrets — committed AWS keys,
9
+ * GitHub PATs, JWTs, Stripe live keys, OpenAI/Anthropic keys, PEM
10
+ * private-key blocks, etc. Designed to run as a pre-commit / pre-push
11
+ * gate so secrets never reach the remote.
12
+ *
13
+ * Findings are printed with the file path, line number, pattern label,
14
+ * and a redacted snippet (the actual secret payload is not printed —
15
+ * only the first/last 4 chars). Exit 1 when any finding is reported.
16
+ */
17
+ var auditCommand = {
18
+ command: "audit [paths..]",
19
+ describe: "Scan one or more paths for plaintext secrets. Exit 1 on findings (suitable as a CI / pre-commit gate).",
20
+ builder: (yargs) => yargs.positional("paths", {
21
+ type: "string",
22
+ array: true,
23
+ describe: "Directories to scan. Default: current working directory."
24
+ }).option("ignore", {
25
+ type: "array",
26
+ string: true,
27
+ default: [],
28
+ describe: "Additional directory or file names to skip (added to the built-in ignore list)."
29
+ }).option("max", {
30
+ type: "number",
31
+ default: 50,
32
+ describe: "Stop after reporting this many findings (0 = unlimited)."
33
+ }).option("json", {
34
+ type: "boolean",
35
+ default: false,
36
+ describe: "Emit findings as JSON for downstream tools."
37
+ }),
38
+ handler: (argv) => {
39
+ const rawPaths = argv["paths"];
40
+ const roots = (rawPaths && rawPaths.length > 0 ? rawPaths : ["."]).map((p) => path.resolve(process.cwd(), p));
41
+ const ignore = argv["ignore"];
42
+ const max = argv["max"];
43
+ const asJson = argv["json"];
44
+ const { findings, filesScanned } = auditFiles({
45
+ roots,
46
+ ignore,
47
+ max
48
+ });
49
+ if (asJson) {
50
+ process.stdout.write(JSON.stringify({
51
+ filesScanned,
52
+ findings
53
+ }, null, 2) + "\n");
54
+ if (findings.length > 0) process.exit(1);
55
+ return;
56
+ }
57
+ log.info(`audit: scanned ${filesScanned} file(s) across ${roots.length} root(s)`);
58
+ if (findings.length === 0) {
59
+ log.success("no plaintext secrets found");
60
+ return;
61
+ }
62
+ log.dim("─".repeat(60));
63
+ const grouped = groupByPattern(findings);
64
+ for (const [patternId, items] of grouped) {
65
+ console.log("");
66
+ log.warn(`${items[0].label} (${items.length} finding(s), id=${patternId})`);
67
+ for (const f of items) {
68
+ const rel = path.relative(process.cwd(), f.file);
69
+ console.log(` ${rel}:${String(f.line)}`);
70
+ console.log(` ${f.snippet.trim()}`);
71
+ }
72
+ }
73
+ console.log("");
74
+ log.dim("─".repeat(60));
75
+ log.error(`audit: ${findings.length} finding(s) — review and remove`);
76
+ log.dim("tip: move secrets into encrypted .env files (`envx encrypt`) and reference them via process.env");
77
+ process.exit(1);
78
+ }
79
+ };
80
+ function groupByPattern(findings) {
81
+ const map = /* @__PURE__ */ new Map();
82
+ for (const f of findings) {
83
+ const arr = map.get(f.patternId);
84
+ if (arr) arr.push(f);
85
+ else map.set(f.patternId, [f]);
86
+ }
87
+ return [...map.entries()];
88
+ }
89
+ //#endregion
90
+ //#region src/commands/bake.ts
91
+ /**
92
+ * Build-time resolver. Loads env files (decrypts, runs resolvers,
93
+ * applies defaults, expands `${VAR}`, validates against the schema,
94
+ * mirrors public vars), then writes the fully-resolved env to a
95
+ * sealed file the bundler can pick up at compile time.
96
+ *
97
+ * Two output formats:
98
+ * - `.env`-style (default) — `KEY=value\n` per line, `KEY=` style
99
+ * - JSON (`--json`) — `{ "KEY": "value", ... }` for tools
100
+ * that prefer structured input
101
+ *
102
+ * The output file is plaintext — the entire point — so it must NEVER
103
+ * be committed. The command writes a `# DO NOT COMMIT` banner at the
104
+ * top and refuses to write under `.git/`.
105
+ */
106
+ var bakeCommand = {
107
+ command: "bake",
108
+ describe: "Resolve every ref / encrypted value into a sealed file the bundler can pick up at build time. Output is plaintext — never commit it.",
109
+ builder: (yargs) => yargs.option("out", {
110
+ type: "string",
111
+ default: ".env.resolved",
112
+ describe: "Output path (relative to cwd). Default: `.env.resolved`. Use `-` for stdout."
113
+ }).option("json", {
114
+ type: "boolean",
115
+ default: false,
116
+ describe: "Emit as JSON instead of the .env format."
117
+ }).option("only", {
118
+ type: "array",
119
+ string: true,
120
+ default: [],
121
+ describe: "Restrict the bake to specific keys (repeatable). Default: every key envx loaded."
122
+ }).option("public-only", {
123
+ type: "boolean",
124
+ default: false,
125
+ describe: "Bake only the keys safe to ship in a client bundle: every PUBLIC_* source key plus its mirrored copies. Server-only secrets are excluded."
126
+ }),
127
+ handler: (argv) => {
128
+ const cfg = argv["__envxConfig"] ?? {};
129
+ const out = argv["out"];
130
+ const asJson = argv["json"];
131
+ const only = new Set(argv["only"] ?? []);
132
+ const publicOnly = argv["public-only"];
133
+ const before = new Set(Object.keys(process.env));
134
+ loadEnv({
135
+ envFiles: argv["env"],
136
+ variables: argv["variables"],
137
+ cascade: argv["cascade"],
138
+ vault: argv["vault"],
139
+ envPath: argv["env-path"],
140
+ override: argv["override"],
141
+ quiet: true,
142
+ ...cfg.autoDetect !== void 0 ? { autoDetect: cfg.autoDetect } : {},
143
+ ...cfg.nodeEnvMap !== void 0 ? { nodeEnvMap: cfg.nodeEnvMap } : {},
144
+ ...cfg.required !== void 0 ? { required: cfg.required } : {},
145
+ ...cfg.expand !== void 0 ? { expand: cfg.expand } : {},
146
+ ...cfg.defaults !== void 0 ? { defaults: cfg.defaults } : {},
147
+ ...cfg.workspaceRoot !== void 0 ? { workspaceRoot: cfg.workspaceRoot } : {},
148
+ ...cfg.schema !== void 0 ? { schema: cfg.schema } : {},
149
+ ...cfg.resolvers !== void 0 ? { resolvers: cfg.resolvers } : {},
150
+ ...cfg.publicPrefixes !== void 0 ? { publicPrefixes: cfg.publicPrefixes } : {},
151
+ ...cfg.publicSource !== void 0 ? { publicSource: cfg.publicSource } : {}
152
+ });
153
+ let keys = Object.keys(process.env).filter((k) => !before.has(k)).filter((k) => only.size === 0 || only.has(k)).sort();
154
+ if (publicOnly) {
155
+ const source = cfg.publicSource ?? "PUBLIC_";
156
+ const targetPrefixes = cfg.publicPrefixes ?? [];
157
+ keys = keys.filter((k) => k.startsWith(source) || targetPrefixes.some((p) => k.startsWith(p)));
158
+ }
159
+ if (keys.length === 0) {
160
+ log.warn("bake: nothing to write — no keys matched");
161
+ process.exit(0);
162
+ }
163
+ const values = {};
164
+ for (const k of keys) {
165
+ const v = process.env[k];
166
+ if (typeof v === "string") values[k] = v;
167
+ }
168
+ let rendered;
169
+ if (asJson) rendered = JSON.stringify(values, null, 2) + "\n";
170
+ else {
171
+ const lines = [];
172
+ lines.push("# Generated by `envx bake` — DO NOT COMMIT.");
173
+ lines.push("# This file contains plaintext resolved secrets.");
174
+ lines.push("# Add it to .gitignore.");
175
+ lines.push("");
176
+ for (const k of keys) lines.push(`${k}=${formatValue(values[k])}`);
177
+ rendered = lines.join("\n") + "\n";
178
+ }
179
+ if (out === "-") {
180
+ process.stdout.write(rendered);
181
+ return;
182
+ }
183
+ const outAbs = path.resolve(process.cwd(), out);
184
+ if (outAbs.split(path.sep).includes(".git") || outAbs.endsWith(".git") || outAbs.includes(`${path.sep}.git${path.sep}`)) {
185
+ log.error(`bake: refusing to write under .git/ — change --out`);
186
+ process.exit(1);
187
+ }
188
+ fs.writeFileSync(outAbs, rendered);
189
+ log.success(`bake: wrote ${outAbs} (${String(keys.length)} key${keys.length === 1 ? "" : "s"}, ${asJson ? "json" : "dotenv"})`);
190
+ log.dim("hint: add the path to .gitignore — this file is plaintext");
191
+ }
192
+ };
193
+ /**
194
+ * Conservative .env value rendering. Quote whenever the value contains
195
+ * whitespace, `#`, `$`, quotes, or newlines so downstream parsers
196
+ * (including envx itself) round-trip cleanly.
197
+ */
198
+ function formatValue(v) {
199
+ if (v === "") return "\"\"";
200
+ if (!/[\s#$"'`\\]/.test(v)) return v;
201
+ return `"${v.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;
202
+ }
203
+ //#endregion
204
+ //#region src/commands/debug.ts
205
+ var debugCommand = {
206
+ command: "debug",
207
+ describe: "Show which env files would be loaded and which variables would be applied, without loading them.",
208
+ handler: (argv) => {
209
+ const paths = resolveEnvPaths({
210
+ envFiles: argv["env"],
211
+ cascade: argv["cascade"]
212
+ });
213
+ const rawVars = argv["variables"];
214
+ const variables = rawVars ? Object.fromEntries(rawVars.map(validateCmdVariable)) : {};
215
+ log.info(`Paths: ${JSON.stringify(paths)}`);
216
+ log.info(`Variables: ${JSON.stringify(variables)}`);
217
+ }
218
+ };
219
+ //#endregion
220
+ //#region src/commands/decrypt.ts
221
+ /** Mirror of `dotenvx decrypt` from upstream — see also encrypt.ts. */
222
+ var decryptCommand = {
223
+ command: "decrypt",
224
+ describe: "Decrypt the values in one or more .env files in place using the matching private key in .env.keys.",
225
+ builder: (yargs) => yargs.option("env-keys-file", {
226
+ alias: "fk",
227
+ type: "string",
228
+ describe: "Path to the .env.keys file (default: ./.env.keys at cwd, regardless of --dir / --env-path)."
229
+ }).option("key", {
230
+ alias: "k",
231
+ type: "array",
232
+ string: true,
233
+ describe: "Specific keys (or picomatch globs) to decrypt. Default: all keys."
234
+ }).option("exclude-key", {
235
+ alias: "ek",
236
+ type: "array",
237
+ string: true,
238
+ describe: "Keys (or picomatch globs) to leave encrypted."
239
+ }).option("stdout", {
240
+ type: "boolean",
241
+ default: false,
242
+ describe: "Write the decrypted env contents to stdout instead of saving in place."
243
+ }).help("h").alias("h", "help"),
244
+ handler: (argv) => {
245
+ const envFiles = argv["env"] ?? [".env"];
246
+ const keys = argv["key"];
247
+ const excludeKeys = argv["exclude-key"];
248
+ const envKeysFile = argv["env-keys-file"];
249
+ const stdout = argv["stdout"] ?? false;
250
+ const result = decryptFiles({
251
+ envFiles,
252
+ ...keys ? { keys } : {},
253
+ ...excludeKeys ? { excludeKeys } : {},
254
+ ...envKeysFile ? { envKeysFile } : {}
255
+ });
256
+ let hadError = false;
257
+ for (const processed of result.processedEnvs) {
258
+ if (processed.error) {
259
+ hadError = true;
260
+ log.error(`${processed.envFilepath}: ${processed.error.message}`);
261
+ if (processed.error.help) log.dim(processed.error.help);
262
+ continue;
263
+ }
264
+ if (stdout) process.stdout.write(processed.envSrc);
265
+ }
266
+ if (!stdout) {
267
+ const { written } = writeProcessed(result.processedEnvs);
268
+ for (const w of written) log.success(`decrypted ${w}`);
269
+ if (written.length === 0 && result.unchangedFilepaths.length > 0 && !hadError) log.dim(`no changes (${result.unchangedFilepaths.join(", ")})`);
270
+ }
271
+ if (hadError) process.exitCode = 1;
272
+ }
273
+ };
274
+ //#endregion
275
+ //#region src/commands/diff.ts
276
+ /**
277
+ * Compare two env files key-by-key. Reports:
278
+ * - keys present only in A
279
+ * - keys present only in B
280
+ * - keys present in both with mismatched values
281
+ *
282
+ * Encrypted values are decrypted with .env.keys when available, so the
283
+ * comparison reflects logical values (not ciphertext). When secrets
284
+ * differ, only the fact that they differ is shown — values are masked.
285
+ *
286
+ * Exit code: 0 when files are equivalent, 1 otherwise. Useful as a CI
287
+ * gate to block prod deploys when staging/prod env shapes diverge.
288
+ */
289
+ var diffCommand = {
290
+ command: "diff <a> <b>",
291
+ describe: "Compare two env files; report keys-only-in-a, keys-only-in-b, and value mismatches. Exit 1 on differences.",
292
+ builder: (yargs) => yargs.positional("a", {
293
+ type: "string",
294
+ demandOption: true,
295
+ describe: "Path to the first env file."
296
+ }).positional("b", {
297
+ type: "string",
298
+ demandOption: true,
299
+ describe: "Path to the second env file."
300
+ }).option("show-values", {
301
+ type: "boolean",
302
+ default: false,
303
+ describe: "Show actual differing values (off by default — values are masked since they are usually secrets)."
304
+ }).option("ignore-keys", {
305
+ type: "array",
306
+ default: [],
307
+ describe: "Keys to ignore (repeatable)."
308
+ }),
309
+ handler: (argv) => {
310
+ const cfg = argv["__envxConfig"] ?? {};
311
+ const aPath = argv["a"];
312
+ const bPath = argv["b"];
313
+ const showValues = argv["show-values"];
314
+ const ignore = new Set(argv["ignore-keys"] ?? []);
315
+ const aResolved = resolveOne(aPath);
316
+ const bResolved = resolveOne(bPath);
317
+ if (!fs.existsSync(aResolved)) {
318
+ log.error(`file not found: ${aResolved}`);
319
+ process.exit(1);
320
+ }
321
+ if (!fs.existsSync(bResolved)) {
322
+ log.error(`file not found: ${bResolved}`);
323
+ process.exit(1);
324
+ }
325
+ const keysFilePath = (cfg.envKeysFile && path.resolve(process.cwd(), cfg.envKeysFile)) ?? resolveCwdOrWorkspace(".env.keys");
326
+ const aMap = loadDecrypted(aResolved, keysFilePath);
327
+ const bMap = loadDecrypted(bResolved, keysFilePath);
328
+ const allKeys = new Set([...Object.keys(aMap), ...Object.keys(bMap)]);
329
+ const onlyA = [];
330
+ const onlyB = [];
331
+ const mismatched = [];
332
+ for (const k of [...allKeys].sort()) {
333
+ if (ignore.has(k)) continue;
334
+ const inA = k in aMap;
335
+ const inB = k in bMap;
336
+ if (inA && !inB) onlyA.push(k);
337
+ else if (!inA && inB) onlyB.push(k);
338
+ else if (aMap[k] !== bMap[k]) mismatched.push({
339
+ key: k,
340
+ a: aMap[k],
341
+ b: bMap[k]
342
+ });
343
+ }
344
+ log.info(`diff: ${path.relative(process.cwd(), aResolved)} ⇄ ${path.relative(process.cwd(), bResolved)}`);
345
+ log.dim("─".repeat(60));
346
+ if (onlyA.length === 0 && onlyB.length === 0 && mismatched.length === 0) {
347
+ log.success("identical (after decryption)");
348
+ process.exit(0);
349
+ }
350
+ if (onlyA.length > 0) {
351
+ console.log("");
352
+ log.warn(`only in a (${onlyA.length}):`);
353
+ for (const k of onlyA) console.log(` - ${k}`);
354
+ }
355
+ if (onlyB.length > 0) {
356
+ console.log("");
357
+ log.warn(`only in b (${onlyB.length}):`);
358
+ for (const k of onlyB) console.log(` + ${k}`);
359
+ }
360
+ if (mismatched.length > 0) {
361
+ console.log("");
362
+ log.warn(`mismatched values (${mismatched.length}):`);
363
+ for (const m of mismatched) if (showValues) {
364
+ console.log(` ~ ${m.key}`);
365
+ console.log(` a = ${m.a}`);
366
+ console.log(` b = ${m.b}`);
367
+ } else console.log(` ~ ${m.key} (values differ — pass --show-values to display)`);
368
+ }
369
+ console.log("");
370
+ log.dim("─".repeat(60));
371
+ log.error(`diff: ${onlyA.length} only-in-a, ${onlyB.length} only-in-b, ${mismatched.length} mismatched`);
372
+ process.exit(1);
373
+ }
374
+ };
375
+ function resolveOne(p) {
376
+ if (path.isAbsolute(p)) return p;
377
+ const direct = path.resolve(process.cwd(), p);
378
+ if (fs.existsSync(direct)) return direct;
379
+ return resolveCwdOrWorkspace(p);
380
+ }
381
+ function loadDecrypted(filePath, envKeysFile) {
382
+ const raw = toRecord(parseEnv(fs.readFileSync(filePath, "utf8")));
383
+ const keys = readKeysFile(envKeysFile);
384
+ const out = {};
385
+ for (const [k, v] of Object.entries(raw)) {
386
+ if (!isEncrypted(v)) {
387
+ out[k] = v;
388
+ continue;
389
+ }
390
+ let decrypted = null;
391
+ for (const privateKeyHex of keys.values()) try {
392
+ decrypted = decryptValueAsymmetric(v, privateKeyHex);
393
+ break;
394
+ } catch {}
395
+ out[k] = decrypted ?? v;
396
+ }
397
+ return out;
398
+ }
399
+ //#endregion
400
+ //#region src/commands/doctor.ts
401
+ /**
402
+ * Run a battery of health checks against the resolved configuration:
403
+ * 1. Workspace + config visibility
404
+ * 2. Env files exist (at least one)
405
+ * 3. Keys file present when any encrypted values exist
406
+ * 4. Every encrypted value decrypts cleanly
407
+ * 5. `required` keys are filled after a dry load
408
+ * 6. Expansion has no cycles and no unresolved references
409
+ *
410
+ * Each check prints a green tick / red cross / yellow tilde. A non-zero
411
+ * exit indicates at least one hard failure (cross). Warnings (tildes)
412
+ * do not affect the exit code so this is safe to run in CI as a gate.
413
+ */
414
+ var doctorCommand = {
415
+ command: "doctor",
416
+ describe: "Run health checks: workspace + config, keys file, decryptability, required keys, expand cycles. Exits non-zero on hard failures.",
417
+ builder: (yargs) => yargs.option("ignore-required", {
418
+ type: "boolean",
419
+ default: false,
420
+ describe: "Skip the `required` post-load check. Useful when running in environments where the secrets aren't expected to be present yet."
421
+ }),
422
+ handler: (argv) => {
423
+ const cfg = argv["__envxConfig"] ?? {};
424
+ const cfgSource = argv["__envxConfigSource"];
425
+ const ignoreRequired = argv["ignore-required"];
426
+ const workspaceRoot = findWorkspaceRoot();
427
+ log.info("envx doctor");
428
+ log.dim("─".repeat(60));
429
+ let failures = 0;
430
+ let warnings = 0;
431
+ section("Workspace + config");
432
+ pass(`workspace root: ${workspaceRoot}`);
433
+ if (cfgSource) pass(`config: ${cfgSource}`);
434
+ else {
435
+ warn("config: built-in defaults (no envx.config.* found)");
436
+ warnings += 1;
437
+ }
438
+ section("Env files");
439
+ const paths = resolveEnvPaths({
440
+ ...argv["env"] !== void 0 ? { envFiles: argv["env"] } : {},
441
+ ...argv["cascade"] !== void 0 ? { cascade: argv["cascade"] } : {},
442
+ ...cfg.autoDetect !== void 0 ? { autoDetect: cfg.autoDetect } : {},
443
+ ...cfg.nodeEnvMap !== void 0 ? { nodeEnvMap: cfg.nodeEnvMap } : {}
444
+ });
445
+ const subdir = argv["env-path"] ?? (argv["vault"] ? "vault" : "");
446
+ const resolvedPaths = paths.map((p) => path.isAbsolute(p) ? p : subdir ? resolveCwdOrWorkspace(path.join(subdir, p)) : resolveCwdOrWorkspace(p));
447
+ const existing = resolvedPaths.filter((p) => fs.existsSync(p));
448
+ if (existing.length === 0) {
449
+ fail(`no env files found (looked at ${resolvedPaths.length} candidate(s))`);
450
+ for (const p of resolvedPaths) log.dim(` ✗ ${p}`);
451
+ failures += 1;
452
+ } else {
453
+ pass(`${existing.length} of ${resolvedPaths.length} candidate file(s) exist`);
454
+ for (const p of existing) log.dim(` ✓ ${p}`);
455
+ for (const p of resolvedPaths.filter((rp) => !fs.existsSync(rp))) log.dim(` · ${p} (skipped — not present)`);
456
+ }
457
+ section("Keys file");
458
+ const keysPath = (cfg.envKeysFile && path.resolve(process.cwd(), cfg.envKeysFile)) ?? defaultKeysPath();
459
+ if (!existing.some((p) => fileHasEncryptedValues(p))) {
460
+ pass("no encrypted values present — keys file not required");
461
+ log.dim(` (would look at ${keysPath})`);
462
+ } else if (fs.existsSync(keysPath)) pass(`keys file present: ${keysPath}`);
463
+ else {
464
+ fail(`keys file missing: ${keysPath}`);
465
+ log.dim(" hint: run `envx encrypt` from a machine that has the private key, or restore .env.keys from your secret store");
466
+ failures += 1;
467
+ }
468
+ section("Decryptability");
469
+ const filesWithSecrets = existing.filter((p) => fileHasEncryptedValues(p));
470
+ if (filesWithSecrets.length === 0) pass("no encrypted values to verify");
471
+ else try {
472
+ const failed = decryptFiles({
473
+ envFiles: filesWithSecrets,
474
+ envKeysFile: keysPath
475
+ }).processedEnvs.filter((p) => p.error);
476
+ if (failed.length === 0) pass(`${filesWithSecrets.length} file(s) decrypt cleanly`);
477
+ else for (const f of failed) {
478
+ fail(`${f.envFilepath}: ${f.error?.code ?? "unknown"} — ${f.error?.message ?? ""}`);
479
+ failures += 1;
480
+ }
481
+ } catch (e) {
482
+ fail(`decrypt error: ${e.message}`);
483
+ failures += 1;
484
+ }
485
+ section("Required keys");
486
+ const required = cfg.required ?? [];
487
+ if (ignoreRequired) {
488
+ warn("skipped (--ignore-required)");
489
+ warnings += 1;
490
+ } else if (required.length === 0) pass("no `required` keys configured");
491
+ else {
492
+ const snapshot = { ...process.env };
493
+ try {
494
+ loadEnv({
495
+ envFiles: argv["env"],
496
+ variables: argv["variables"],
497
+ cascade: argv["cascade"],
498
+ vault: argv["vault"],
499
+ envPath: argv["env-path"],
500
+ override: argv["override"],
501
+ quiet: true,
502
+ ...cfg.autoDetect !== void 0 ? { autoDetect: cfg.autoDetect } : {},
503
+ ...cfg.nodeEnvMap !== void 0 ? { nodeEnvMap: cfg.nodeEnvMap } : {},
504
+ ...cfg.expand !== void 0 ? { expand: cfg.expand } : {},
505
+ ...cfg.defaults !== void 0 ? { defaults: cfg.defaults } : {},
506
+ ...cfg.workspaceRoot !== void 0 ? { workspaceRoot: cfg.workspaceRoot } : {}
507
+ });
508
+ const missing = required.filter((k) => process.env[k] === void 0 || process.env[k] === "");
509
+ if (missing.length === 0) pass(`${required.length} key(s) present after load`);
510
+ else {
511
+ for (const m of missing) fail(`missing: ${m}`);
512
+ failures += missing.length;
513
+ }
514
+ } finally {
515
+ for (const k of Object.keys(process.env)) if (!(k in snapshot)) delete process.env[k];
516
+ Object.assign(process.env, snapshot);
517
+ }
518
+ }
519
+ section("Expansion sanity");
520
+ const merged = {};
521
+ for (const f of existing) {
522
+ const lines = parseEnv(fs.readFileSync(f, "utf8"));
523
+ Object.assign(merged, toRecord(lines));
524
+ }
525
+ const { unresolved, cycles } = expandRecord(merged, { onMissing: "leave" });
526
+ if (cycles.length === 0 && unresolved.length === 0) pass("no cycles, no unresolved ${VAR} references");
527
+ else {
528
+ if (cycles.length > 0) {
529
+ for (const c of cycles) fail(`cycle: ${c.join(" → ")}`);
530
+ failures += cycles.length;
531
+ }
532
+ if (unresolved.length > 0) {
533
+ warn(`unresolved references: ${unresolved.join(", ")}`);
534
+ warnings += 1;
535
+ }
536
+ }
537
+ log.dim("─".repeat(60));
538
+ if (failures === 0 && warnings === 0) {
539
+ log.success(`doctor: all checks passed`);
540
+ process.exit(0);
541
+ }
542
+ if (failures === 0) {
543
+ log.warn(`doctor: ${warnings} warning(s), 0 failure(s)`);
544
+ process.exit(0);
545
+ }
546
+ log.error(`doctor: ${failures} failure(s), ${warnings} warning(s)`);
547
+ process.exit(1);
548
+ }
549
+ };
550
+ function section(title) {
551
+ console.log("");
552
+ log.info(title);
553
+ }
554
+ function pass(msg) {
555
+ log.success(` ✓ ${msg}`);
556
+ }
557
+ function fail(msg) {
558
+ log.error(` ✗ ${msg}`);
559
+ }
560
+ function warn(msg) {
561
+ log.warn(` ~ ${msg}`);
562
+ }
563
+ function fileHasEncryptedValues(filePath) {
564
+ try {
565
+ return parseEnv(fs.readFileSync(filePath, "utf8")).some((l) => l.type === "kv" && isEncrypted(l.value));
566
+ } catch {
567
+ return false;
568
+ }
569
+ }
570
+ //#endregion
571
+ //#region src/commands/encrypt.ts
572
+ /**
573
+ * Mirrors the upstream `dotenvx encrypt` flags so existing muscle
574
+ * memory carries over. The `--env` global doubles as the file list
575
+ * (the upstream calls it `--env-file`); this CLI keeps a single
576
+ * canonical name across subcommands.
577
+ */
578
+ var encryptCommand = {
579
+ command: "encrypt",
580
+ describe: "Encrypt the values in one or more .env files. Generates a private key in .env.keys on first run.",
581
+ builder: (yargs) => yargs.option("env-keys-file", {
582
+ alias: "fk",
583
+ type: "string",
584
+ describe: "Path to the .env.keys file (default: ./.env.keys at cwd, regardless of --dir / --env-path)."
585
+ }).option("key", {
586
+ alias: "k",
587
+ type: "array",
588
+ string: true,
589
+ describe: "Specific keys (or picomatch globs) to encrypt. Default: all keys."
590
+ }).option("pattern", {
591
+ alias: "p",
592
+ type: "array",
593
+ string: true,
594
+ describe: "Glob pattern(s) selecting which keys to encrypt. Comma-separated values supported (e.g. `--pattern '*_SECRET,*_TOKEN'`). Repeatable. Equivalent to --key but accepts CSV."
595
+ }).option("secrets", {
596
+ type: "boolean",
597
+ default: false,
598
+ describe: "Shorthand for the conventional secret-key globs: *_SECRET, *_TOKEN, *_KEY, *_PASSWORD, PASSWORD*, API_*. Mutually exclusive with --key/--pattern."
599
+ }).option("exclude-key", {
600
+ alias: "ek",
601
+ type: "array",
602
+ string: true,
603
+ describe: "Keys (or picomatch globs) to leave plaintext."
604
+ }).option("stdout", {
605
+ type: "boolean",
606
+ default: false,
607
+ describe: "Write the encrypted env contents to stdout instead of saving in place."
608
+ }).help("h").alias("h", "help"),
609
+ handler: (argv) => {
610
+ const envFiles = argv["env"] ?? [".env"];
611
+ const keys = argv["key"];
612
+ const patterns = argv["pattern"];
613
+ const useSecrets = argv["secrets"] ?? false;
614
+ const excludeKeys = argv["exclude-key"];
615
+ const envKeysFile = argv["env-keys-file"];
616
+ const stdout = argv["stdout"] ?? false;
617
+ const SECRET_PRESETS = [
618
+ "*_SECRET",
619
+ "*_TOKEN",
620
+ "*_KEY",
621
+ "*_PASSWORD",
622
+ "PASSWORD*",
623
+ "API_*"
624
+ ];
625
+ const fromPatterns = patterns?.flatMap((p) => p.split(",").map((s) => s.trim()).filter(Boolean)) ?? [];
626
+ const include = [
627
+ ...keys ?? [],
628
+ ...fromPatterns,
629
+ ...useSecrets ? SECRET_PRESETS : []
630
+ ];
631
+ if ((keys || patterns) && useSecrets) log.warn("encrypt: --secrets combined with --key/--pattern; the union of all globs is encrypted");
632
+ const result = encryptFiles({
633
+ envFiles,
634
+ ...include.length > 0 ? { keys: include } : {},
635
+ ...excludeKeys ? { excludeKeys } : {},
636
+ ...envKeysFile ? { envKeysFile } : {}
637
+ });
638
+ let hadError = false;
639
+ for (const processed of result.processedEnvs) {
640
+ if (processed.error) {
641
+ hadError = true;
642
+ log.error(`${processed.envFilepath}: ${processed.error.message}`);
643
+ if (processed.error.help) log.dim(processed.error.help);
644
+ continue;
645
+ }
646
+ if (stdout) {
647
+ process.stdout.write(processed.envSrc);
648
+ continue;
649
+ }
650
+ if (processed.privateKeyAdded) log.success(`key added to .env.keys (${processed.privateKeyName ?? "<unknown>"})`);
651
+ }
652
+ if (!stdout) {
653
+ const { written } = writeProcessed(result.processedEnvs);
654
+ for (const w of written) log.success(`encrypted ${w}`);
655
+ if (written.length === 0 && result.unchangedFilepaths.length > 0 && !hadError) log.dim(`no changes (${result.unchangedFilepaths.join(", ")})`);
656
+ }
657
+ if (hadError) process.exitCode = 1;
658
+ }
659
+ };
660
+ //#endregion
661
+ //#region src/commands/expand.ts
662
+ /**
663
+ * Decrypts (when needed) and expands variable references in an env
664
+ * file. Mirrors the workflow `decrypt-vault` action — but cycle-safe,
665
+ * supports `${VAR:-default}` / `${VAR:?msg}`, and never silently
666
+ * truncates after N passes.
667
+ */
668
+ var expandCommand = {
669
+ command: "expand",
670
+ describe: "Decrypt (if needed) and expand ${VAR}/$VAR references in an env file. Outputs to stdout by default.",
671
+ builder: (yargs) => yargs.option("output", {
672
+ type: "string",
673
+ describe: "Write the expanded result to this file (default: stdout)."
674
+ }).option("env-keys-file", {
675
+ alias: "fk",
676
+ type: "string",
677
+ describe: "Path to .env.keys (default: ./.env.keys at cwd, regardless of --dir / --env-path)."
678
+ }).option("on-missing", {
679
+ type: "string",
680
+ choices: [
681
+ "leave",
682
+ "empty",
683
+ "throw"
684
+ ],
685
+ default: "leave",
686
+ describe: "How to handle ${UNRESOLVED_VAR}: leave it literal, substitute empty, or fail."
687
+ }).help("h").alias("h", "help"),
688
+ handler: (argv) => {
689
+ const envFiles = argv["env"] ?? [".env"];
690
+ if (envFiles.length !== 1) {
691
+ log.error(`expand operates on a single env file at a time; got ${String(envFiles.length)}.`);
692
+ process.exit(1);
693
+ }
694
+ const envFile = envFiles[0];
695
+ const filepath = path.resolve(envFile);
696
+ if (!fs.existsSync(filepath)) {
697
+ log.error(`env file not found: ${envFile}`);
698
+ process.exit(1);
699
+ }
700
+ let envSrc = fs.readFileSync(filepath, "utf8");
701
+ if (parseEnv(envSrc).some((l) => l.type === "kv" && isEncrypted(l.value))) {
702
+ const envKeysFile = argv["env-keys-file"];
703
+ const processed = decryptFiles({
704
+ envFiles: [envFile],
705
+ ...envKeysFile ? { envKeysFile } : {}
706
+ }).processedEnvs[0];
707
+ if (processed?.error) {
708
+ log.error(`${envFile}: ${processed.error.message}`);
709
+ if (processed.error.help) log.dim(processed.error.help);
710
+ process.exit(1);
711
+ }
712
+ envSrc = processed.envSrc;
713
+ }
714
+ const onMissing = argv["on-missing"];
715
+ const result = expandEnvSrc(envSrc, { onMissing });
716
+ for (const v of result.unresolved) log.warn(`unresolved variable: ${v}`);
717
+ for (const cycle of result.cycles) log.warn(`cycle: ${cycle.join(" → ")}`);
718
+ const output = argv["output"];
719
+ if (output) {
720
+ fs.writeFileSync(path.resolve(output), result.envSrc);
721
+ log.success(`expanded ${envFile} → ${output}`);
722
+ } else {
723
+ process.stdout.write(result.envSrc);
724
+ if (!result.envSrc.endsWith("\n")) process.stdout.write("\n");
725
+ }
726
+ }
727
+ };
728
+ //#endregion
729
+ //#region src/commands/hook.ts
730
+ /**
731
+ * Print eval-able shell exports for every variable loaded from envx's
732
+ * resolved env files. Use to inject the env into the parent shell:
733
+ *
734
+ * eval "$(envx hook bash)"
735
+ *
736
+ * Three flavors:
737
+ * - bash / zsh → `export KEY='value'`
738
+ * - fish → `set -x KEY 'value'`
739
+ * - powershell → `$env:KEY = 'value'`
740
+ *
741
+ * Diff against the pre-load process.env so we only emit the keys that
742
+ * envx actually loaded (rather than re-exporting every var that
743
+ * happened to already be set).
744
+ */
745
+ var hookCommand = {
746
+ command: "hook <shell>",
747
+ describe: "Print shell exports for the loaded env. `eval \"$(envx hook bash)\"` injects them into the parent shell.",
748
+ builder: (yargs) => yargs.positional("shell", {
749
+ type: "string",
750
+ choices: [
751
+ "bash",
752
+ "zsh",
753
+ "fish",
754
+ "powershell",
755
+ "pwsh"
756
+ ],
757
+ demandOption: true,
758
+ describe: "Target shell flavor."
759
+ }).option("only", {
760
+ type: "array",
761
+ default: [],
762
+ describe: "Restrict output to specific keys. By default every key envx loaded is exported."
763
+ }),
764
+ handler: (argv) => {
765
+ const cfg = argv["__envxConfig"] ?? {};
766
+ const shell = argv["shell"];
767
+ const only = new Set(argv["only"] ?? []);
768
+ const before = new Set(Object.keys(process.env));
769
+ loadEnv({
770
+ envFiles: argv["env"],
771
+ variables: argv["variables"],
772
+ cascade: argv["cascade"],
773
+ vault: argv["vault"],
774
+ envPath: argv["env-path"],
775
+ override: argv["override"],
776
+ quiet: true,
777
+ ...cfg.autoDetect !== void 0 ? { autoDetect: cfg.autoDetect } : {},
778
+ ...cfg.nodeEnvMap !== void 0 ? { nodeEnvMap: cfg.nodeEnvMap } : {},
779
+ ...cfg.required !== void 0 ? { required: cfg.required } : {},
780
+ ...cfg.expand !== void 0 ? { expand: cfg.expand } : {},
781
+ ...cfg.defaults !== void 0 ? { defaults: cfg.defaults } : {},
782
+ ...cfg.workspaceRoot !== void 0 ? { workspaceRoot: cfg.workspaceRoot } : {}
783
+ });
784
+ const newKeys = Object.keys(process.env).filter((k) => !before.has(k)).filter((k) => only.size === 0 || only.has(k)).sort();
785
+ const lines = [];
786
+ for (const k of newKeys) {
787
+ const v = process.env[k];
788
+ if (v === void 0) continue;
789
+ lines.push(formatExport(shell, k, v));
790
+ }
791
+ if (lines.length > 0) process.stdout.write(lines.join("\n") + "\n");
792
+ }
793
+ };
794
+ function formatExport(shell, key, value) {
795
+ switch (shell) {
796
+ case "bash":
797
+ case "zsh": return `export ${key}=${shellQuote(value)}`;
798
+ case "fish": return `set -x ${key} ${shellQuote(value)}`;
799
+ case "powershell":
800
+ case "pwsh": return `$env:${key} = ${psQuote(value)}`;
801
+ }
802
+ }
803
+ /**
804
+ * POSIX-shell single-quote escaping. Single quotes have no escape inside
805
+ * single-quoted strings, so the trick is `'` → `'\''`.
806
+ */
807
+ function shellQuote(s) {
808
+ return `'${s.replace(/'/g, "'\\''")}'`;
809
+ }
810
+ /** PowerShell single-quote escaping: `'` → `''`. */
811
+ function psQuote(s) {
812
+ return `'${s.replace(/'/g, "''")}'`;
813
+ }
814
+ //#endregion
815
+ //#region src/commands/info.ts
816
+ /**
817
+ * Print everything envx knows about the current invocation: which
818
+ * config file it found, the resolved settings, which platform signals
819
+ * are present, what auto-detection produced, the NODE_ENV → suffix
820
+ * map (built-in + user overrides), and the env file paths it would
821
+ * load. Read-only — does not mutate process.env or write any files.
822
+ */
823
+ var infoCommand = {
824
+ command: "info",
825
+ describe: "Show resolved configuration, auto-detection details, NODE_ENV mappings, and which env files would be loaded.",
826
+ builder: (yargs) => yargs.help("h").alias("h", "help"),
827
+ handler: (argv) => {
828
+ const cfg = argv["__envxConfig"] ?? {};
829
+ const cfgSource = argv["__envxConfigSource"];
830
+ const cfgOrigin = argv["__envxConfigOrigin"];
831
+ const workspaceRoot = findWorkspaceRoot();
832
+ const cwd = process.cwd();
833
+ log.info("envx info");
834
+ log.dim("─".repeat(60));
835
+ log.info("Workspace");
836
+ line("cwd", cwd);
837
+ line("workspace root", workspaceRoot === cwd ? `${workspaceRoot} (none detected — using cwd)` : workspaceRoot);
838
+ blank();
839
+ log.info("Config");
840
+ line("source", cfgSource ?? "(none — built-in defaults)");
841
+ line("origin", cfgOrigin ?? "defaults");
842
+ blank();
843
+ log.info("Resolved settings");
844
+ line("envFiles", argv["env"] ?? cfg.envFiles ?? [".env"]);
845
+ line("envPath", argv["env-path"] ?? cfg.envPath ?? "(none)");
846
+ line("cascade", formatCascade(argv["cascade"] ?? cfg.cascade));
847
+ line("envKeysFile", formatPath(argv["env-keys-file"] ?? cfg.envKeysFile, cwd, workspaceRoot));
848
+ line("override", argv["override"] ?? cfg.override ?? false);
849
+ line("quiet", argv["quiet"] ?? cfg.quiet ?? true);
850
+ line("autoDetect", cfg.autoDetect ?? true);
851
+ line("expand", cfg.expand ?? false);
852
+ line("required", cfg.required && cfg.required.length > 0 ? cfg.required : "(none)");
853
+ line("defaults", cfg.defaults && Object.keys(cfg.defaults).length > 0 ? cfg.defaults : "(none)");
854
+ line("workspaceRoot (config)", cfg.workspaceRoot ?? "(auto-detect via findWorkspaceRoot)");
855
+ line("schema", cfg.schema ? "(configured — runs safeParse() after load)" : "(none)");
856
+ line("resolvers", cfg.resolvers && Object.keys(cfg.resolvers).length > 0 ? Object.keys(cfg.resolvers).join(", ") : "(none)");
857
+ line("profiles", cfg.profiles && Object.keys(cfg.profiles).length > 0 ? Object.keys(cfg.profiles).join(", ") : "(none)");
858
+ line("active profile", argv["profile"] ?? "(none)");
859
+ blank();
860
+ log.info("Platform signals (process.env)");
861
+ line("VERCEL", process.env["VERCEL"] ?? "(unset)");
862
+ line("VERCEL_ENV", process.env["VERCEL_ENV"] ?? "(unset)");
863
+ line("NETLIFY", process.env["NETLIFY"] ?? "(unset)");
864
+ line("CONTEXT", process.env["CONTEXT"] ?? "(unset)");
865
+ line("NODE_ENV", process.env.NODE_ENV ?? "(unset)");
866
+ blank();
867
+ log.info("Auto-detection");
868
+ const autoDetect = cfg.autoDetect ?? true;
869
+ if (!autoDetect) line("status", "disabled (autoDetect: false)");
870
+ else {
871
+ const detected = detectEnvironment({ ...cfg.nodeEnvMap !== void 0 ? { nodeEnvMap: cfg.nodeEnvMap } : {} });
872
+ line("source", pickDetectionSource());
873
+ line("detected", detected);
874
+ line("→ env file", detected === "root" ? ".env" : `.env.${detected}`);
875
+ }
876
+ blank();
877
+ log.info("NODE_ENV → suffix mapping");
878
+ log.dim(" (any NODE_ENV value not in this map passes through lowercased,");
879
+ log.dim(" so e.g. NODE_ENV=qa loads .env.qa)");
880
+ const userMap = cfg.nodeEnvMap ?? {};
881
+ const merged = {
882
+ ...DEFAULT_NODE_ENV_MAP,
883
+ ...userMap
884
+ };
885
+ const allKeys = Array.from(new Set([...Object.keys(merged)])).sort();
886
+ for (const k of allKeys) {
887
+ const v = merged[k] ?? "";
888
+ const suffix = v === "" ? "(no suffix → .env)" : `.env.${v}`;
889
+ const origin = userMap[k] === void 0 ? "default" : DEFAULT_NODE_ENV_MAP[k] === void 0 ? "config" : "config (override)";
890
+ line(` ${k.padEnd(14)} → ${suffix}`, origin);
891
+ }
892
+ blank();
893
+ log.info("Resolved env file paths (would be loaded in this order)");
894
+ const paths = resolveEnvPaths({
895
+ ...argv["env"] !== void 0 ? { envFiles: argv["env"] } : {},
896
+ ...argv["cascade"] !== void 0 ? { cascade: argv["cascade"] } : {},
897
+ autoDetect,
898
+ ...cfg.nodeEnvMap !== void 0 ? { nodeEnvMap: cfg.nodeEnvMap } : {}
899
+ });
900
+ if (paths.length === 0) line("(none)", "");
901
+ else {
902
+ const subdir = argv["env-path"] ?? (argv["vault"] ? "vault" : "");
903
+ for (const p of paths) {
904
+ if (path.isAbsolute(p)) {
905
+ log.dim(` ${p}`);
906
+ continue;
907
+ }
908
+ const resolved = subdir ? resolveCwdOrWorkspace(path.join(subdir, p)) : resolveCwdOrWorkspace(p);
909
+ log.dim(` ${p} → ${resolved}`);
910
+ }
911
+ }
912
+ }
913
+ };
914
+ function line(key, value, suffix) {
915
+ const v = value === void 0 || value === null ? "(unset)" : Array.isArray(value) ? value.length === 0 ? "[]" : JSON.stringify(value) : typeof value === "object" ? JSON.stringify(value) : String(value);
916
+ const tail = suffix ? ` ${dim(`(${suffix})`)}` : "";
917
+ log.dim(` ${key.padEnd(14)} ${v}${tail}`);
918
+ }
919
+ function blank() {
920
+ console.log("");
921
+ }
922
+ function dim(s) {
923
+ return `\x1b[2m${s}\x1b[22m`;
924
+ }
925
+ function formatCascade(cascade) {
926
+ if (cascade === void 0 || cascade === false) return "(off)";
927
+ if (cascade === true) return "true (uses auto-detected env name)";
928
+ return String(cascade);
929
+ }
930
+ function pickDetectionSource() {
931
+ if (process.env["VERCEL"]) return "Vercel (VERCEL_ENV)";
932
+ if (process.env["NETLIFY"]) return "Netlify (CONTEXT)";
933
+ if (process.env.NODE_ENV) return "NODE_ENV";
934
+ return "(no signals — fallback to .env)";
935
+ }
936
+ function formatPath(raw, cwd, wsRoot) {
937
+ if (!raw) return `${resolveCwdOrWorkspace(".env.keys", cwd)} (default: cwd-first, ws-root fallback)`;
938
+ if (path.isAbsolute(raw)) return `${raw} (absolute)`;
939
+ return `${raw} → ${path.resolve(cwd, raw)} (or ${path.resolve(wsRoot, raw)} if not at cwd)`;
940
+ }
941
+ //#endregion
942
+ //#region src/commands/print.ts
943
+ var printCommand = {
944
+ command: "print <variable>",
945
+ describe: "Load env files and print the value of a single variable.",
946
+ builder: (yargs) => yargs.positional("variable", {
947
+ describe: "Name of the variable to print",
948
+ type: "string",
949
+ demandOption: true
950
+ }),
951
+ handler: (argv) => {
952
+ loadEnv({
953
+ envFiles: argv["env"],
954
+ variables: argv["variables"],
955
+ cascade: argv["cascade"],
956
+ vault: argv["vault"],
957
+ envPath: argv["env-path"],
958
+ override: argv["override"],
959
+ quiet: argv["quiet"]
960
+ });
961
+ const name = argv["variable"];
962
+ const value = process.env[name];
963
+ process.stdout.write(value != null ? `${value}\n` : "\n");
964
+ }
965
+ };
966
+ //#endregion
967
+ //#region src/commands/rotate.ts
968
+ /**
969
+ * Rotate the asymmetric keypair for one or more env files. Generates a
970
+ * fresh secp256k1 keypair, decrypts existing values with the old
971
+ * private key, re-encrypts with the new public key, and updates both
972
+ * the `ENVX_PUBLIC_KEY*` header in the env file and the
973
+ * `ENVX_PRIVATE_KEY*` entry in `.env.keys`.
974
+ *
975
+ * Files without a public-key header surface an error — run
976
+ * `envx encrypt` against them first to set up the keypair.
977
+ */
978
+ var rotateCommand = {
979
+ command: "rotate",
980
+ describe: "Rotate the asymmetric keypair for one or more env files.",
981
+ builder: (yargs) => yargs.option("env-keys-file", {
982
+ alias: "fk",
983
+ type: "string",
984
+ describe: "Path to the .env.keys file (default: ./.env.keys at cwd). Relative paths resolve against each env file's directory."
985
+ }).option("key", {
986
+ alias: "k",
987
+ type: "array",
988
+ string: true,
989
+ describe: "Specific keys (or picomatch globs) to rotate. Default: all encrypted keys."
990
+ }).option("exclude-key", {
991
+ alias: "ek",
992
+ type: "array",
993
+ string: true,
994
+ describe: "Keys (or picomatch globs) to leave with their existing ciphertext."
995
+ }).help("h").alias("h", "help"),
996
+ handler: (argv) => {
997
+ const envFiles = argv["env"] ?? [".env"];
998
+ const keys = argv["key"];
999
+ const excludeKeys = argv["exclude-key"];
1000
+ const envKeysFile = argv["env-keys-file"];
1001
+ const result = rotateFiles({
1002
+ envFiles,
1003
+ ...keys ? { keys } : {},
1004
+ ...excludeKeys ? { excludeKeys } : {},
1005
+ ...envKeysFile ? { envKeysFile } : {}
1006
+ });
1007
+ let hadError = false;
1008
+ for (const processed of result.processedEnvs) if (processed.error) {
1009
+ hadError = true;
1010
+ log.error(`${processed.envFilepath}: ${processed.error.message}`);
1011
+ if (processed.error.help) log.dim(processed.error.help);
1012
+ }
1013
+ const { written } = writeProcessed(result.processedEnvs);
1014
+ for (const w of written) log.success(`rotated ${w}`);
1015
+ if (written.length === 0 && result.unchangedFilepaths.length > 0 && !hadError) log.dim(`no changes (${result.unchangedFilepaths.join(", ")})`);
1016
+ if (hadError) process.exitCode = 1;
1017
+ }
1018
+ };
1019
+ //#endregion
1020
+ //#region src/commands/run.ts
1021
+ var runCommand = {
1022
+ command: "run [command..]",
1023
+ aliases: ["$0"],
1024
+ describe: "Load env files into process.env and execute a command. Default subcommand — `dotenvx-run [command]` is equivalent.",
1025
+ builder: (yargs) => yargs.positional("command", {
1026
+ describe: "Command to execute after loading env files",
1027
+ type: "string",
1028
+ array: true
1029
+ }),
1030
+ handler: (argv) => {
1031
+ const command = (argv["command"] ?? []).join(" ");
1032
+ const cfg = argv["__envxConfig"] ?? {};
1033
+ loadEnv({
1034
+ envFiles: argv["env"],
1035
+ variables: argv["variables"],
1036
+ cascade: argv["cascade"],
1037
+ vault: argv["vault"],
1038
+ envPath: argv["env-path"],
1039
+ override: argv["override"],
1040
+ quiet: argv["quiet"],
1041
+ ...cfg.autoDetect !== void 0 ? { autoDetect: cfg.autoDetect } : {},
1042
+ ...cfg.nodeEnvMap !== void 0 ? { nodeEnvMap: cfg.nodeEnvMap } : {},
1043
+ ...cfg.required !== void 0 ? { required: cfg.required } : {},
1044
+ ...cfg.expand !== void 0 ? { expand: cfg.expand } : {},
1045
+ ...cfg.defaults !== void 0 ? { defaults: cfg.defaults } : {},
1046
+ ...cfg.workspaceRoot !== void 0 ? { workspaceRoot: cfg.workspaceRoot } : {},
1047
+ ...cfg.schema !== void 0 ? { schema: cfg.schema } : {},
1048
+ ...cfg.resolvers !== void 0 ? { resolvers: cfg.resolvers } : {},
1049
+ ...Array.isArray(argv["public-prefix"]) && argv["public-prefix"].length > 0 ? { publicPrefixes: argv["public-prefix"] } : cfg.publicPrefixes !== void 0 ? { publicPrefixes: cfg.publicPrefixes } : {},
1050
+ ...cfg.publicSource !== void 0 ? { publicSource: cfg.publicSource } : {}
1051
+ });
1052
+ if (!command) return;
1053
+ log.info(`Running: ${command}`);
1054
+ try {
1055
+ execSync(command, { stdio: "inherit" });
1056
+ log.success(`Command completed: ${command}`);
1057
+ } catch (error) {
1058
+ const msg = error instanceof Error ? error.message : String(error);
1059
+ log.error(`Command failed: ${msg}`);
1060
+ process.exit(1);
1061
+ }
1062
+ }
1063
+ };
1064
+ //#endregion
1065
+ //#region src/commands/template.ts
1066
+ /**
1067
+ * Generate (or check) a stripped `.env.example` from one or more real
1068
+ * env files. Values are removed; keys, declaration order, comments,
1069
+ * and blank lines are preserved so the example file stays readable.
1070
+ *
1071
+ * Three modes:
1072
+ * - default: write the rendered template to --out (default `.env.example`)
1073
+ * - --stdout: print to stdout (don't touch disk)
1074
+ * - --check: exit non-zero if the on-disk template is out of date
1075
+ */
1076
+ var templateCommand = {
1077
+ command: "template",
1078
+ describe: "Emit a value-stripped .env.example from your real env files (or `--check` it for drift).",
1079
+ builder: (yargs) => yargs.option("out", {
1080
+ type: "string",
1081
+ describe: "Output path. Default: `<cwd>/.env.example`."
1082
+ }).option("stdout", {
1083
+ type: "boolean",
1084
+ default: false,
1085
+ describe: "Print template to stdout instead of writing to disk."
1086
+ }).option("check", {
1087
+ type: "boolean",
1088
+ default: false,
1089
+ describe: "Compare the rendered template against the on-disk file; exit 1 on drift. Use this in CI."
1090
+ }).option("annotate", {
1091
+ type: "boolean",
1092
+ default: true,
1093
+ describe: "Prefix each KEY= with a `# from <source-file>` comment so reviewers can see where each key came from."
1094
+ }),
1095
+ handler: (argv) => {
1096
+ const cfg = argv["__envxConfig"] ?? {};
1097
+ const outArg = argv["out"];
1098
+ const useStdout = argv["stdout"];
1099
+ const check = argv["check"];
1100
+ const annotate = argv["annotate"];
1101
+ const paths = resolveEnvPaths({
1102
+ ...argv["env"] !== void 0 ? { envFiles: argv["env"] } : {},
1103
+ ...argv["cascade"] !== void 0 ? { cascade: argv["cascade"] } : {},
1104
+ ...cfg.autoDetect !== void 0 ? { autoDetect: cfg.autoDetect } : {},
1105
+ ...cfg.nodeEnvMap !== void 0 ? { nodeEnvMap: cfg.nodeEnvMap } : {}
1106
+ });
1107
+ const subdir = argv["env-path"] ?? (argv["vault"] ? "vault" : "");
1108
+ const existing = paths.map((p) => path.isAbsolute(p) ? p : subdir ? resolveCwdOrWorkspace(path.join(subdir, p)) : resolveCwdOrWorkspace(p)).filter((p) => fs.existsSync(p));
1109
+ if (existing.length === 0) {
1110
+ log.error("no env files found to template from");
1111
+ process.exit(1);
1112
+ }
1113
+ const rendered = renderTemplate(existing, { annotate });
1114
+ if (useStdout) {
1115
+ process.stdout.write(rendered);
1116
+ return;
1117
+ }
1118
+ const outPath = path.resolve(process.cwd(), outArg ?? ".env.example");
1119
+ if (check) {
1120
+ if ((fs.existsSync(outPath) ? fs.readFileSync(outPath, "utf8") : "") === rendered) {
1121
+ log.success(`template up to date: ${outPath}`);
1122
+ return;
1123
+ }
1124
+ log.error(`template drift: ${outPath} is out of date`);
1125
+ log.dim("hint: run `envx template` (without --check) to update it");
1126
+ process.exit(1);
1127
+ }
1128
+ fs.writeFileSync(outPath, rendered);
1129
+ log.success(`wrote template: ${outPath} (${countKeys(rendered)} key(s))`);
1130
+ }
1131
+ };
1132
+ function renderTemplate(files, opts) {
1133
+ const seen = /* @__PURE__ */ new Set();
1134
+ const out = [];
1135
+ out.push("# Generated by `envx template` — do not edit by hand.");
1136
+ out.push("# Run `envx template` to regenerate, or `envx template --check` in CI.");
1137
+ out.push("");
1138
+ for (const file of files) {
1139
+ const display = path.relative(process.cwd(), file) || file;
1140
+ const lines = parseEnv(fs.readFileSync(file, "utf8"));
1141
+ const localKeys = [];
1142
+ for (const ln of lines) {
1143
+ if (ln.type !== "kv") continue;
1144
+ if (seen.has(ln.key)) continue;
1145
+ seen.add(ln.key);
1146
+ localKeys.push(ln);
1147
+ }
1148
+ if (localKeys.length === 0) continue;
1149
+ out.push(`# ── ${display} ─────────────────────────────────`);
1150
+ for (const ln of localKeys) {
1151
+ if (ln.type !== "kv") continue;
1152
+ if (opts.annotate) out.push(`# from ${display}`);
1153
+ out.push(`${ln.key}=`);
1154
+ out.push("");
1155
+ }
1156
+ }
1157
+ return out.join("\n");
1158
+ }
1159
+ function countKeys(rendered) {
1160
+ return rendered.split("\n").filter((l) => /^[A-Za-z_][A-Za-z0-9_]*=/.test(l)).length;
1161
+ }
1162
+ //#endregion
1163
+ //#region src/commands/types.ts
1164
+ /**
1165
+ * Emit a TypeScript declaration file (`env.d.ts`) declaring every key
1166
+ * found across the resolved env files. Adds the keys to `NodeJS.ProcessEnv`
1167
+ * via interface merging, so `process.env.MY_KEY` autocompletes and
1168
+ * type-checks across the project.
1169
+ *
1170
+ * Keys listed in `config.required` are typed as `string` (always set);
1171
+ * everything else is `string | undefined`. When a Zod schema is
1172
+ * configured we look at its `.shape` (when present) to pick up the same
1173
+ * required-vs-optional information.
1174
+ */
1175
+ var typesCommand = {
1176
+ command: "types",
1177
+ describe: "Emit env.d.ts declaring every loaded key on NodeJS.ProcessEnv (string | undefined; required keys narrow to string).",
1178
+ builder: (yargs) => yargs.option("out", {
1179
+ type: "string",
1180
+ default: "env.d.ts",
1181
+ describe: "Output path (relative to cwd)."
1182
+ }).option("stdout", {
1183
+ type: "boolean",
1184
+ default: false,
1185
+ describe: "Print to stdout instead of writing to disk."
1186
+ }),
1187
+ handler: (argv) => {
1188
+ const cfg = argv["__envxConfig"] ?? {};
1189
+ const out = argv["out"];
1190
+ const useStdout = argv["stdout"];
1191
+ const paths = resolveEnvPaths({
1192
+ ...argv["env"] !== void 0 ? { envFiles: argv["env"] } : {},
1193
+ ...argv["cascade"] !== void 0 ? { cascade: argv["cascade"] } : {},
1194
+ ...cfg.autoDetect !== void 0 ? { autoDetect: cfg.autoDetect } : {},
1195
+ ...cfg.nodeEnvMap !== void 0 ? { nodeEnvMap: cfg.nodeEnvMap } : {}
1196
+ });
1197
+ const subdir = argv["env-path"] ?? (argv["vault"] ? "vault" : "");
1198
+ const resolved = paths.map((p) => path.isAbsolute(p) ? p : subdir ? resolveCwdOrWorkspace(path.join(subdir, p)) : resolveCwdOrWorkspace(p)).filter((p) => fs.existsSync(p));
1199
+ const keysFromFiles = /* @__PURE__ */ new Set();
1200
+ for (const f of resolved) {
1201
+ const lines = parseEnv(fs.readFileSync(f, "utf8"));
1202
+ for (const k of Object.keys(toRecord(lines))) keysFromFiles.add(k);
1203
+ }
1204
+ const required = new Set(cfg.required ?? []);
1205
+ const schemaShape = extractZodShape(cfg.schema);
1206
+ for (const [k, isOptional] of Object.entries(schemaShape)) {
1207
+ keysFromFiles.add(k);
1208
+ if (!isOptional) required.add(k);
1209
+ }
1210
+ const allKeys = [...keysFromFiles].sort();
1211
+ if (allKeys.length === 0) {
1212
+ log.error("no keys found — load at least one env file or configure a schema");
1213
+ process.exit(1);
1214
+ }
1215
+ const lines = [];
1216
+ lines.push("// Generated by `envx types` — do not edit by hand.");
1217
+ lines.push("// Run `envx types` to regenerate.");
1218
+ lines.push("");
1219
+ lines.push("declare global {");
1220
+ lines.push(" namespace NodeJS {");
1221
+ lines.push(" interface ProcessEnv {");
1222
+ for (const k of allKeys) {
1223
+ const optional = required.has(k) ? "" : "?";
1224
+ lines.push(` readonly ${k}${optional}: string;`);
1225
+ }
1226
+ lines.push(" }");
1227
+ lines.push(" }");
1228
+ lines.push("}");
1229
+ lines.push("");
1230
+ lines.push("export {};");
1231
+ lines.push("");
1232
+ const rendered = lines.join("\n");
1233
+ if (useStdout) {
1234
+ process.stdout.write(rendered);
1235
+ return;
1236
+ }
1237
+ const outPath = path.resolve(process.cwd(), out);
1238
+ fs.writeFileSync(outPath, rendered);
1239
+ log.success(`wrote ${outPath} (${allKeys.length} key(s), ${required.size} required)`);
1240
+ }
1241
+ };
1242
+ /**
1243
+ * Extract a `{ key → isOptional }` map from a duck-typed Zod schema.
1244
+ * Looks for `.shape` (ZodObject). Returns `{}` if the schema doesn't
1245
+ * follow that shape — we still emit types for every file-found key,
1246
+ * just without the required/optional refinement.
1247
+ */
1248
+ function extractZodShape(schema) {
1249
+ if (!schema || typeof schema !== "object") return {};
1250
+ const shape = schema.shape;
1251
+ if (!shape || typeof shape !== "object") return {};
1252
+ const out = {};
1253
+ for (const [k, v] of Object.entries(shape)) {
1254
+ if (!v || typeof v !== "object") continue;
1255
+ const tn = v._def?.typeName ?? "";
1256
+ out[k] = tn === "ZodOptional" || tn === "ZodDefault";
1257
+ }
1258
+ return out;
1259
+ }
1260
+ //#endregion
1261
+ //#region src/commands/watch.ts
1262
+ /**
1263
+ * Spawn a child process with the loaded env, then watch every resolved
1264
+ * env file. On change, kill the child (SIGTERM, then SIGKILL after a
1265
+ * grace period) and respawn with a fresh load.
1266
+ *
1267
+ * Differences from `nodemon`:
1268
+ * - Only env files are watched (not source code).
1269
+ * - Restart preserves the parent shell's stdin/stdout/stderr.
1270
+ * - Cascade-resolved paths are watched (so adding `.env.local` while
1271
+ * watching is picked up if it lives at one of the resolved paths).
1272
+ */
1273
+ var watchCommand = {
1274
+ command: "watch [command..]",
1275
+ describe: "Load env files, spawn a command, and restart it whenever any of the resolved env files change.",
1276
+ builder: (yargs) => yargs.positional("command", {
1277
+ type: "string",
1278
+ array: true,
1279
+ describe: "Command to run (e.g. `node app.js`)."
1280
+ }).option("debounce", {
1281
+ type: "number",
1282
+ default: 200,
1283
+ describe: "Debounce window (ms) for coalescing rapid file events."
1284
+ }).option("kill-grace", {
1285
+ type: "number",
1286
+ default: 1500,
1287
+ describe: "Grace period (ms) between SIGTERM and SIGKILL when restarting the child."
1288
+ }),
1289
+ handler: (argv) => {
1290
+ const cfg = argv["__envxConfig"] ?? {};
1291
+ const command = (argv["command"] ?? []).join(" ");
1292
+ const debounceMs = argv["debounce"];
1293
+ const killGraceMs = argv["kill-grace"];
1294
+ if (!command) {
1295
+ log.error("watch: no command supplied. Example: envx watch -- node app.js");
1296
+ process.exit(1);
1297
+ }
1298
+ const loadEnvOptions = () => ({
1299
+ envFiles: argv["env"],
1300
+ variables: argv["variables"],
1301
+ cascade: argv["cascade"],
1302
+ vault: argv["vault"],
1303
+ envPath: argv["env-path"],
1304
+ override: argv["override"],
1305
+ quiet: argv["quiet"],
1306
+ ...cfg.autoDetect !== void 0 ? { autoDetect: cfg.autoDetect } : {},
1307
+ ...cfg.nodeEnvMap !== void 0 ? { nodeEnvMap: cfg.nodeEnvMap } : {},
1308
+ ...cfg.required !== void 0 ? { required: cfg.required } : {},
1309
+ ...cfg.expand !== void 0 ? { expand: cfg.expand } : {},
1310
+ ...cfg.defaults !== void 0 ? { defaults: cfg.defaults } : {},
1311
+ ...cfg.workspaceRoot !== void 0 ? { workspaceRoot: cfg.workspaceRoot } : {},
1312
+ ...cfg.schema !== void 0 ? { schema: cfg.schema } : {},
1313
+ ...cfg.resolvers !== void 0 ? { resolvers: cfg.resolvers } : {}
1314
+ });
1315
+ const watchedPaths = resolveWatchedPaths(argv, cfg);
1316
+ if (watchedPaths.length === 0) log.warn("watch: no env files exist to watch — running anyway");
1317
+ else {
1318
+ log.info(`watch: ${watchedPaths.length} file(s)`);
1319
+ for (const p of watchedPaths) log.dim(` • ${p}`);
1320
+ }
1321
+ let child = null;
1322
+ let restarting = false;
1323
+ let pendingTimer = null;
1324
+ const start = () => {
1325
+ const snapshot = { ...process.env };
1326
+ loadEnv(loadEnvOptions());
1327
+ const childEnv = { ...process.env };
1328
+ for (const k of Object.keys(process.env)) if (!(k in snapshot)) delete process.env[k];
1329
+ Object.assign(process.env, snapshot);
1330
+ log.info(`▶ ${command}`);
1331
+ child = spawn(command, {
1332
+ shell: true,
1333
+ stdio: "inherit",
1334
+ env: childEnv
1335
+ });
1336
+ child.on("exit", (code) => {
1337
+ if (restarting) return;
1338
+ const c = code ?? 0;
1339
+ if (c === 0) log.success(`exit 0`);
1340
+ else log.warn(`exit ${String(c)}`);
1341
+ child = null;
1342
+ });
1343
+ };
1344
+ const restart = () => {
1345
+ if (restarting) return;
1346
+ restarting = true;
1347
+ const cur = child;
1348
+ if (!cur) {
1349
+ restarting = false;
1350
+ start();
1351
+ return;
1352
+ }
1353
+ cur.kill("SIGTERM");
1354
+ const t = setTimeout(() => {
1355
+ if (cur && !cur.killed) cur.kill("SIGKILL");
1356
+ }, killGraceMs);
1357
+ cur.once("exit", () => {
1358
+ clearTimeout(t);
1359
+ restarting = false;
1360
+ start();
1361
+ });
1362
+ };
1363
+ for (const p of watchedPaths) try {
1364
+ fs.watch(p, () => {
1365
+ if (pendingTimer) clearTimeout(pendingTimer);
1366
+ pendingTimer = setTimeout(() => {
1367
+ log.info(`↻ env changed — restarting`);
1368
+ restart();
1369
+ }, debounceMs);
1370
+ });
1371
+ } catch (e) {
1372
+ log.warn(`watch failed for ${p}: ${e.message}`);
1373
+ }
1374
+ const shutdown = () => {
1375
+ if (child && !child.killed) child.kill("SIGTERM");
1376
+ process.exit(0);
1377
+ };
1378
+ process.on("SIGINT", shutdown);
1379
+ process.on("SIGTERM", shutdown);
1380
+ start();
1381
+ }
1382
+ };
1383
+ function resolveWatchedPaths(argv, cfg) {
1384
+ const paths = resolveEnvPaths({
1385
+ ...argv["env"] !== void 0 ? { envFiles: argv["env"] } : {},
1386
+ ...argv["cascade"] !== void 0 ? { cascade: argv["cascade"] } : {},
1387
+ ...cfg.autoDetect !== void 0 ? { autoDetect: cfg.autoDetect } : {},
1388
+ ...cfg.nodeEnvMap !== void 0 ? { nodeEnvMap: cfg.nodeEnvMap } : {}
1389
+ });
1390
+ const subdir = argv["env-path"] ?? (argv["vault"] ? "vault" : "");
1391
+ return paths.map((p) => path.isAbsolute(p) ? p : subdir ? resolveCwdOrWorkspace(path.join(subdir, p)) : resolveCwdOrWorkspace(p)).filter((p) => fs.existsSync(p));
1392
+ }
1393
+ //#endregion
1394
+ //#region src/commands/index.ts
1395
+ /**
1396
+ * Build the yargs CLI for `dotenvx-run`. Global options (env, variables,
1397
+ * cascade, vault, override, quiet) are registered on the root so every
1398
+ * subcommand inherits them.
1399
+ */
1400
+ function createCli(argvInput) {
1401
+ return yargs(argvInput).scriptName("envx").usage("$0 <command> [options]").option("config", {
1402
+ alias: "c",
1403
+ type: "string",
1404
+ describe: "Path to an envx config file (default: discovered from package.json `envx.config` or envx.config.{ts,js,json})."
1405
+ }).option("env", {
1406
+ alias: "e",
1407
+ type: "array",
1408
+ describe: "Env files to load. Default: [\".env\"] (or `envFiles` from config; or every .env* under --env-path when set). Auto-detects environment when omitted."
1409
+ }).option("env-path", {
1410
+ alias: ["dir", "d"],
1411
+ type: "string",
1412
+ describe: "Subdirectory of the workspace root holding the env files (e.g. `vault`). When set and --env is omitted, every .env* file in that directory is included."
1413
+ }).option("variables", {
1414
+ alias: "v",
1415
+ type: "array",
1416
+ describe: "Inline variables in the form name=value (repeatable).",
1417
+ default: []
1418
+ }).option("cascade", {
1419
+ type: "string",
1420
+ describe: "Cascade load order: .env, .env.<cascade>, .env.local, .env.<cascade>.local"
1421
+ }).option("vault", {
1422
+ alias: "va",
1423
+ type: "boolean",
1424
+ describe: "Shortcut for `--env-path vault`."
1425
+ }).option("override", {
1426
+ alias: "o",
1427
+ type: "boolean",
1428
+ describe: "Override existing process.env values. Conflicts with --cascade."
1429
+ }).option("quiet", {
1430
+ alias: "q",
1431
+ type: "boolean",
1432
+ describe: "Suppress dotenv's own output."
1433
+ }).option("profile", {
1434
+ alias: "P",
1435
+ type: "string",
1436
+ describe: "Named profile from `profiles` in envx.config.* — fields override the base config per-field."
1437
+ }).option("public-prefix", {
1438
+ type: "array",
1439
+ string: true,
1440
+ describe: "Framework prefix to mirror PUBLIC_ vars under (repeatable). e.g. `--public-prefix VITE_ --public-prefix NEXT_PUBLIC_`. Overrides `publicPrefixes` from envx.config.*."
1441
+ }).middleware((argv) => {
1442
+ const configPath = argv["config"];
1443
+ let loaded;
1444
+ try {
1445
+ loaded = loadDotenvxConfig({ ...configPath ? { configPath } : {} });
1446
+ } catch (e) {
1447
+ log.error(`config error: ${e.message}`);
1448
+ process.exit(1);
1449
+ }
1450
+ if (loaded.source) log.dim(`config: ${loaded.source} (${loaded.origin})`);
1451
+ let cfg = loaded.config;
1452
+ const profileName = argv["profile"];
1453
+ if (profileName) {
1454
+ const profile = cfg.profiles?.[profileName];
1455
+ if (!profile) {
1456
+ log.error(`unknown profile: '${profileName}' (available: ${Object.keys(cfg.profiles ?? {}).join(", ") || "none"})`);
1457
+ process.exit(1);
1458
+ }
1459
+ cfg = {
1460
+ ...cfg,
1461
+ ...profile
1462
+ };
1463
+ log.dim(`profile: ${profileName}`);
1464
+ }
1465
+ argv["__envxConfig"] = cfg;
1466
+ argv["__envxConfigSource"] = loaded.source;
1467
+ argv["__envxConfigOrigin"] = loaded.origin;
1468
+ if (argv["cascade"] === void 0 && cfg.cascade !== void 0) argv["cascade"] = cfg.cascade;
1469
+ if (argv["override"] === void 0) argv["override"] = cfg.override ?? false;
1470
+ if (argv["quiet"] === void 0) argv["quiet"] = cfg.quiet ?? true;
1471
+ if (argv["vault"] === void 0) argv["vault"] = false;
1472
+ if (argv["env-keys-file"] === void 0 && cfg.envKeysFile !== void 0) argv["env-keys-file"] = cfg.envKeysFile;
1473
+ if (argv["env-path"] === void 0) {
1474
+ if (cfg.envPath !== void 0) argv["env-path"] = cfg.envPath;
1475
+ else if (argv["vault"]) argv["env-path"] = "vault";
1476
+ }
1477
+ const userPassedEnv = Array.isArray(argv["env"]) && argv["env"].length > 0;
1478
+ let files;
1479
+ if (userPassedEnv) files = argv["env"];
1480
+ else if (cfg.envFiles && cfg.envFiles.length > 0) files = [...cfg.envFiles];
1481
+ else if (typeof argv["env-path"] === "string") {
1482
+ const subdir = argv["env-path"];
1483
+ const discovered = listEnvFiles(resolveCwdOrWorkspace(subdir));
1484
+ if (discovered.length > 0) {
1485
+ files = discovered;
1486
+ log.dim(`env-path ${subdir}: discovered ${String(discovered.length)} file(s) — ${discovered.join(", ")}`);
1487
+ } else {
1488
+ files = [".env"];
1489
+ log.warn(`env-path ${subdir}: no .env* files found; falling back to [".env"]`);
1490
+ }
1491
+ } else files = [".env"];
1492
+ if (typeof argv["env-path"] === "string") {
1493
+ const dir = resolveCwdOrWorkspace(argv["env-path"]);
1494
+ files = files.map((f) => path.isAbsolute(f) ? f : path.join(dir, f));
1495
+ }
1496
+ argv["env"] = files;
1497
+ }).command(runCommand).command(printCommand).command(debugCommand).command(encryptCommand).command(decryptCommand).command(expandCommand).command(rotateCommand).command(infoCommand).command(doctorCommand).command(templateCommand).command(diffCommand).command(hookCommand).command(typesCommand).command(watchCommand).command(auditCommand).command(bakeCommand).help("h").alias("h", "help").version().strictCommands().demandCommand(0).recommendCommands().epilog("Examples:\n envx -- node app.js # load .env (auto-detected) and run\n envx --env dev -- pnpm start # load .env.dev\n envx print DATABASE_URL # print one variable\n envx debug --cascade prod # show resolved paths\n envx encrypt -e .env.prod --secrets # encrypt only conventional secret keys\n envx decrypt -e .env.prod -k FOO # decrypt only FOO\n envx expand -e vault/.env.prod # decrypt + expand to stdout\n envx doctor # health check (CI gate)\n envx template --check # verify .env.example is up to date\n envx diff .env.dev .env.prod # compare two env files\n eval \"$(envx hook bash)\" # inject env into the parent shell\n envx types # emit env.d.ts for typed process.env\n envx watch -- node app.js # restart on env change\n envx audit # scan repo for plaintext secrets\n envx --profile staging -- node app.js # apply named profile from envx.config.*");
1498
+ }
1499
+ //#endregion
1500
+ export { createCli as t };
1501
+
1502
+ //# sourceMappingURL=commands-D3eQPQO6.js.map