@xlameiro/env-typegen 0.1.3 → 0.1.5

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/dist/cli.js CHANGED
@@ -99,7 +99,7 @@ var require_picocolors = __commonJS({
99
99
  });
100
100
 
101
101
  // src/cli.ts
102
- import { realpathSync } from "fs";
102
+ import { existsSync as existsSync6, realpathSync } from "fs";
103
103
  import { createRequire } from "module";
104
104
  import path13 from "path";
105
105
  import { fileURLToPath, pathToFileURL as pathToFileURL5 } from "url";
@@ -110,14 +110,27 @@ import { existsSync } from "fs";
110
110
  import path from "path";
111
111
  import { pathToFileURL } from "url";
112
112
  var CONFIG_FILE_NAMES = [
113
- "env-typegen.config.ts",
114
113
  "env-typegen.config.mjs",
115
- "env-typegen.config.js"
114
+ "env-typegen.config.js",
115
+ "env-typegen.config.ts"
116
116
  ];
117
117
  async function loadConfig(cwd = process.cwd()) {
118
118
  for (const name of CONFIG_FILE_NAMES) {
119
119
  const filePath = path.resolve(cwd, name);
120
120
  if (existsSync(filePath)) {
121
+ if (filePath.endsWith(".ts")) {
122
+ throw new Error(
123
+ `Config file "${name}" was found but TypeScript files cannot be loaded directly at runtime.
124
+ Rename it to "env-typegen.config.mjs" and use ESM export syntax:
125
+
126
+ // env-typegen.config.mjs
127
+ import { defineConfig } from "@xlameiro/env-typegen";
128
+ export default defineConfig({ input: ".env.example" });
129
+
130
+ Tip: keep env-typegen.config.ts for IDE autocompletion and create a sibling
131
+ env-typegen.config.mjs for runtime loading.`
132
+ );
133
+ }
121
134
  const fileUrl = pathToFileURL(filePath).href;
122
135
  const mod = await import(fileUrl);
123
136
  return mod.default;
@@ -133,12 +146,13 @@ import path5 from "path";
133
146
  import path2 from "path";
134
147
  function generateDeclaration(parsed) {
135
148
  const fileName = path2.basename(parsed.filePath);
136
- const lines = [];
137
- lines.push("// Generated by env-typegen \u2014 do not edit manually");
138
- lines.push(`// Source: ${fileName}`);
139
- lines.push("");
140
- lines.push("declare namespace NodeJS {");
141
- lines.push(" interface ProcessEnv {");
149
+ const lines = [
150
+ "// Generated by env-typegen \u2014 do not edit manually",
151
+ `// Source: ${fileName}`,
152
+ "",
153
+ "declare namespace NodeJS {",
154
+ " interface ProcessEnv {"
155
+ ];
142
156
  for (const variable of parsed.vars) {
143
157
  const effectiveType = variable.annotatedType ?? variable.inferredType;
144
158
  const optional = variable.isOptional ? "?" : "";
@@ -153,12 +167,14 @@ function generateDeclaration(parsed) {
153
167
  }
154
168
  lines.push(propLine);
155
169
  }
156
- lines.push(" }");
157
- lines.push("}");
170
+ lines.push(" }", "}");
158
171
  return lines.join("\n") + "\n";
159
172
  }
160
173
 
161
174
  // src/generators/t3-generator.ts
175
+ function escapeJsStringLiteral(value) {
176
+ return value.replaceAll("\\", String.raw`\\`).replaceAll('"', String.raw`\"`);
177
+ }
162
178
  function toT3ZodType(envVarType) {
163
179
  if (envVarType === "number") return "z.coerce.number()";
164
180
  if (envVarType === "boolean") return "z.coerce.boolean()";
@@ -166,51 +182,47 @@ function toT3ZodType(envVarType) {
166
182
  if (envVarType === "email") return "z.string().email()";
167
183
  return "z.string()";
168
184
  }
185
+ function buildZodExpr(variable) {
186
+ const effectiveType = variable.annotatedType ?? variable.inferredType;
187
+ let zodExpr = toT3ZodType(effectiveType);
188
+ if (variable.description !== void 0) {
189
+ zodExpr += `.describe("${escapeJsStringLiteral(variable.description)}")`;
190
+ }
191
+ if (variable.isOptional) {
192
+ zodExpr += ".optional()";
193
+ }
194
+ return zodExpr;
195
+ }
169
196
  function generateT3Env(parsed) {
170
197
  const serverVars = parsed.vars.filter((v) => !v.isClientSide);
171
198
  const clientVars = parsed.vars.filter((v) => v.isClientSide);
172
- const lines = [];
173
- lines.push("// Generated by env-typegen \u2014 do not edit manually");
174
- lines.push('import { createEnv } from "@t3-oss/env-nextjs";');
175
- lines.push('import { z } from "zod";');
176
- lines.push("");
177
- lines.push("export const env = createEnv({");
199
+ const lines = [
200
+ "// Generated by env-typegen \u2014 do not edit manually",
201
+ 'import { createEnv } from "@t3-oss/env-nextjs";',
202
+ 'import { z } from "zod";',
203
+ "",
204
+ "export const env = createEnv({"
205
+ ];
178
206
  if (serverVars.length > 0) {
179
- lines.push(" server: {");
180
- for (const variable of serverVars) {
181
- const effectiveType = variable.annotatedType ?? variable.inferredType;
182
- let zodExpr = toT3ZodType(effectiveType);
183
- if (variable.description !== void 0) {
184
- zodExpr += `.describe("${variable.description.replace(/"/g, '\\"')}")`;
185
- }
186
- if (variable.isOptional) {
187
- zodExpr += ".optional()";
188
- }
189
- lines.push(` ${variable.key}: ${zodExpr},`);
190
- }
191
- lines.push(" },");
207
+ lines.push(
208
+ " server: {",
209
+ ...serverVars.map((v) => ` ${v.key}: ${buildZodExpr(v)},`),
210
+ " },"
211
+ );
192
212
  }
193
213
  if (clientVars.length > 0) {
194
- lines.push(" client: {");
195
- for (const variable of clientVars) {
196
- const effectiveType = variable.annotatedType ?? variable.inferredType;
197
- let zodExpr = toT3ZodType(effectiveType);
198
- if (variable.description !== void 0) {
199
- zodExpr += `.describe("${variable.description.replace(/"/g, '\\"')}")`;
200
- }
201
- if (variable.isOptional) {
202
- zodExpr += ".optional()";
203
- }
204
- lines.push(` ${variable.key}: ${zodExpr},`);
205
- }
206
- lines.push(" },");
207
- }
208
- lines.push(" runtimeEnv: {");
209
- for (const variable of parsed.vars) {
210
- lines.push(` ${variable.key}: process.env.${variable.key},`);
214
+ lines.push(
215
+ " client: {",
216
+ ...clientVars.map((v) => ` ${v.key}: ${buildZodExpr(v)},`),
217
+ " },"
218
+ );
211
219
  }
212
- lines.push(" },");
213
- lines.push("});");
220
+ lines.push(
221
+ " runtimeEnv: {",
222
+ ...parsed.vars.map((v) => ` ${v.key}: process.env.${v.key},`),
223
+ " },",
224
+ "});"
225
+ );
214
226
  return lines.join("\n") + "\n";
215
227
  }
216
228
 
@@ -227,12 +239,14 @@ function generateTypeScriptTypes(parsed) {
227
239
  const fileName = path3.basename(parsed.filePath);
228
240
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
229
241
  const lines = [];
230
- lines.push("// Generated by env-typegen \u2014 do not edit manually");
231
- lines.push(`// Source: ${fileName}`);
232
- lines.push(`// Generated at: ${timestamp}`);
233
- lines.push("");
234
- lines.push("declare namespace NodeJS {");
235
- lines.push(" interface ProcessEnv {");
242
+ lines.push(
243
+ "// Generated by env-typegen \u2014 do not edit manually",
244
+ `// Source: ${fileName}`,
245
+ `// Generated at: ${timestamp}`,
246
+ "",
247
+ "declare namespace NodeJS {",
248
+ " interface ProcessEnv {"
249
+ );
236
250
  for (const variable of parsed.vars) {
237
251
  const effectiveType = variable.annotatedType ?? variable.inferredType;
238
252
  const optional = variable.isOptional ? "?" : "";
@@ -247,10 +261,7 @@ function generateTypeScriptTypes(parsed) {
247
261
  }
248
262
  lines.push(propLine);
249
263
  }
250
- lines.push(" }");
251
- lines.push("}");
252
- lines.push("");
253
- lines.push("export type EnvVars = {");
264
+ lines.push(" }", "}", "", "export type EnvVars = {");
254
265
  for (const variable of parsed.vars) {
255
266
  const effectiveType = variable.annotatedType ?? variable.inferredType;
256
267
  const tsType = toTsType(effectiveType);
@@ -260,9 +271,11 @@ function generateTypeScriptTypes(parsed) {
260
271
  lines.push("};");
261
272
  if (hasClientVars) {
262
273
  const clientKeyUnion = clientVars.map((v) => `"${v.key}"`).join(" | ");
263
- lines.push("");
264
- lines.push(`export type ServerEnvVars = Omit<EnvVars, ${clientKeyUnion}>;`);
265
- lines.push(`export type ClientEnvVars = Pick<EnvVars, ${clientKeyUnion}>;`);
274
+ lines.push(
275
+ "",
276
+ `export type ServerEnvVars = Omit<EnvVars, ${clientKeyUnion}>;`,
277
+ `export type ClientEnvVars = Pick<EnvVars, ${clientKeyUnion}>;`
278
+ );
266
279
  }
267
280
  return lines.join("\n") + "\n";
268
281
  }
@@ -279,27 +292,29 @@ function generateZodSchema(parsed) {
279
292
  const serverVars = parsed.vars.filter((v) => !v.isClientSide);
280
293
  const clientVars = parsed.vars.filter((v) => v.isClientSide);
281
294
  const lines = [];
282
- lines.push("// Generated by env-typegen \u2014 do not edit manually");
283
- lines.push('import { z } from "zod";');
284
- lines.push("");
285
- lines.push("export const serverEnvSchema = z.object({");
295
+ lines.push(
296
+ "// Generated by env-typegen \u2014 do not edit manually",
297
+ 'import { z } from "zod";',
298
+ "",
299
+ "export const serverEnvSchema = z.object({"
300
+ );
286
301
  for (const variable of serverVars) {
287
302
  const effectiveType = variable.annotatedType ?? variable.inferredType;
288
303
  const zodExpr = variable.isOptional ? `${toZodType(effectiveType)}.optional()` : toZodType(effectiveType);
289
304
  lines.push(` ${variable.key}: ${zodExpr},`);
290
305
  }
291
- lines.push("});");
292
- lines.push("");
293
- lines.push("export const clientEnvSchema = z.object({");
306
+ lines.push("});", "", "export const clientEnvSchema = z.object({");
294
307
  for (const variable of clientVars) {
295
308
  const effectiveType = variable.annotatedType ?? variable.inferredType;
296
309
  const zodExpr = variable.isOptional ? `${toZodType(effectiveType)}.optional()` : toZodType(effectiveType);
297
310
  lines.push(` ${variable.key}: ${zodExpr},`);
298
311
  }
299
- lines.push("});");
300
- lines.push("");
301
- lines.push("export const envSchema = serverEnvSchema.merge(clientEnvSchema);");
302
- lines.push("export type Env = z.infer<typeof envSchema>;");
312
+ lines.push(
313
+ "});",
314
+ "",
315
+ "export const envSchema = serverEnvSchema.merge(clientEnvSchema);",
316
+ "export type Env = z.infer<typeof envSchema>;"
317
+ );
303
318
  return lines.join("\n") + "\n";
304
319
  }
305
320
 
@@ -359,7 +374,9 @@ var inferenceRules = [
359
374
  {
360
375
  id: "P8_numeric_literal",
361
376
  priority: 8,
362
- match: (_key, value) => /^\d+(\.\d+)?$/.test(value),
377
+ // Non-capturing group with \d keeps the dot/digit boundary unambiguous,
378
+ // eliminating super-linear backtracking (ReDoS-safe).
379
+ match: (_key, value) => /^\d+(?:\.\d+)?$/.test(value),
363
380
  type: "number"
364
381
  },
365
382
  {
@@ -377,7 +394,9 @@ var inferenceRules = [
377
394
  {
378
395
  id: "P11_email_literal",
379
396
  priority: 11,
380
- match: (_key, value) => /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value),
397
+ // Dots are excluded from each domain-segment character class so that the
398
+ // literal \. separators are unambiguous, preventing super-linear backtracking.
399
+ match: (_key, value) => /^[^@\s]+@[^@\s.]+(?:\.[^@\s.]+)+$/.test(value),
381
400
  type: "email"
382
401
  },
383
402
  {
@@ -494,7 +513,50 @@ function parseCommentBlock(lines) {
494
513
 
495
514
  // src/parser/env-parser.ts
496
515
  var ENV_VAR_RE = /^([A-Z_][A-Z0-9_]*)=(.*)$/;
497
- var SECTION_HEADER_RE = /^#\s+[-=]{3,}\s+(.+?)\s+[-=]{3,}\s*$/;
516
+ var SECTION_HEADER_RE = /^#\s+[-=]{3,}\s+(\S+(?:\s+\S+)*)\s+[-=]{3,}\s*$/;
517
+ function buildParsedVar(params, commentBlock, options) {
518
+ const annotations = parseCommentBlock(commentBlock);
519
+ const extraRules = options?.inferenceRules;
520
+ const inferredType = inferType(
521
+ params.key,
522
+ params.rawValue,
523
+ ...extraRules === void 0 ? [] : [{ extraRules }]
524
+ );
525
+ const isRequired = params.rawValue.length > 0 || annotations.isRequired;
526
+ const isOptional = params.rawValue.length === 0 && !annotations.isRequired;
527
+ const isClientSide = params.key.startsWith("NEXT_PUBLIC_");
528
+ const parsedVar = {
529
+ key: params.key,
530
+ rawValue: params.rawValue,
531
+ inferredType,
532
+ isRequired,
533
+ isOptional,
534
+ isClientSide,
535
+ lineNumber: params.lineNumber
536
+ };
537
+ if (annotations.annotatedType !== void 0) {
538
+ parsedVar.annotatedType = annotations.annotatedType;
539
+ }
540
+ if (annotations.description !== void 0) {
541
+ parsedVar.description = annotations.description;
542
+ }
543
+ if (params.currentGroup !== void 0) {
544
+ parsedVar.group = params.currentGroup;
545
+ }
546
+ if (annotations.enumValues !== void 0) {
547
+ parsedVar.enumValues = annotations.enumValues;
548
+ }
549
+ if (annotations.constraints !== void 0) {
550
+ parsedVar.constraints = annotations.constraints;
551
+ }
552
+ if (annotations.runtime !== void 0) {
553
+ parsedVar.runtime = annotations.runtime;
554
+ }
555
+ if (annotations.isSecret !== void 0) {
556
+ parsedVar.isSecret = annotations.isSecret;
557
+ }
558
+ return parsedVar;
559
+ }
498
560
  function parseEnvFileContent(content, filePath, options) {
499
561
  const lines = content.split("\n");
500
562
  const vars = [];
@@ -524,53 +586,18 @@ function parseEnvFileContent(content, filePath, options) {
524
586
  continue;
525
587
  }
526
588
  const envMatch = ENV_VAR_RE.exec(trimmed);
527
- if (envMatch !== null) {
528
- const key = envMatch[1] ?? "";
529
- const rawValue = envMatch[2] ?? "";
530
- const annotations = parseCommentBlock(commentBlock);
531
- const inferredType = inferType(
532
- key,
533
- rawValue,
534
- ...options?.inferenceRules !== void 0 ? [{ extraRules: options.inferenceRules }] : []
535
- );
536
- const isRequired = rawValue.length > 0 || annotations.isRequired;
537
- const isOptional = rawValue.length === 0 && !annotations.isRequired;
538
- const isClientSide = key.startsWith("NEXT_PUBLIC_");
539
- const parsedVar = {
540
- key,
541
- rawValue,
542
- inferredType,
543
- isRequired,
544
- isOptional,
545
- isClientSide,
546
- lineNumber
547
- };
548
- if (annotations.annotatedType !== void 0) {
549
- parsedVar.annotatedType = annotations.annotatedType;
550
- }
551
- if (annotations.description !== void 0) {
552
- parsedVar.description = annotations.description;
553
- }
554
- if (currentGroup !== void 0) {
555
- parsedVar.group = currentGroup;
556
- }
557
- if (annotations.enumValues !== void 0) {
558
- parsedVar.enumValues = annotations.enumValues;
559
- }
560
- if (annotations.constraints !== void 0) {
561
- parsedVar.constraints = annotations.constraints;
562
- }
563
- if (annotations.runtime !== void 0) {
564
- parsedVar.runtime = annotations.runtime;
565
- }
566
- if (annotations.isSecret !== void 0) {
567
- parsedVar.isSecret = annotations.isSecret;
568
- }
569
- vars.push(parsedVar);
570
- commentBlock = [];
571
- } else {
589
+ if (envMatch === null) {
572
590
  commentBlock = [];
591
+ continue;
573
592
  }
593
+ vars.push(
594
+ buildParsedVar(
595
+ { key: envMatch[1] ?? "", rawValue: envMatch[2] ?? "", lineNumber, currentGroup },
596
+ commentBlock,
597
+ options
598
+ )
599
+ );
600
+ commentBlock = [];
574
601
  }
575
602
  return { filePath, vars, groups };
576
603
  }
@@ -583,7 +610,15 @@ function parseEnvFile(filePath) {
583
610
  import { mkdir, readFile, writeFile } from "fs/promises";
584
611
  import path4 from "path";
585
612
  async function readEnvFile(filePath) {
586
- return readFile(path4.resolve(filePath), "utf8");
613
+ const resolved = path4.resolve(filePath);
614
+ try {
615
+ return await readFile(resolved, "utf8");
616
+ } catch (err) {
617
+ if (err instanceof Error && err.code === "ENOENT") {
618
+ throw new Error(`File not found: ${filePath}`);
619
+ }
620
+ throw err;
621
+ }
587
622
  }
588
623
  async function writeOutput(filePath, content) {
589
624
  const resolved = path4.resolve(filePath);
@@ -607,14 +642,15 @@ async function formatOutput(content, parser = "typescript") {
607
642
 
608
643
  // src/utils/logger.ts
609
644
  var import_picocolors = __toESM(require_picocolors(), 1);
645
+ var { green, red, yellow } = import_picocolors.default;
610
646
  function log(message) {
611
647
  console.log(message);
612
648
  }
613
649
  function error(message) {
614
- console.error((0, import_picocolors.red)(`\u2716 ${message}`));
650
+ console.error(red(`\u2716 ${message}`));
615
651
  }
616
652
  function success(message) {
617
- console.log((0, import_picocolors.green)(`\u2714 ${message}`));
653
+ console.log(green(`\u2714 ${message}`));
618
654
  }
619
655
 
620
656
  // src/pipeline.ts
@@ -657,10 +693,6 @@ async function persistOutput(params) {
657
693
  }
658
694
  if (dryRun) {
659
695
  if (!silent) {
660
- if (!isSingle) {
661
- console.log(`// --- ${generator}: ${outputPath} ---`);
662
- }
663
- console.log(generated);
664
696
  success(`Dry run: would write ${outputPath}`);
665
697
  }
666
698
  return;
@@ -690,7 +722,7 @@ async function runGenerate(options) {
690
722
  const parsed = parseEnvFileContent(
691
723
  content,
692
724
  inputPath,
693
- inferenceRules2 !== void 0 ? { inferenceRules: inferenceRules2 } : void 0
725
+ inferenceRules2 === void 0 ? void 0 : { inferenceRules: inferenceRules2 }
694
726
  );
695
727
  for (const generator of generators) {
696
728
  let generated = buildOutput(generator, parsed);
@@ -712,6 +744,7 @@ async function runGenerate(options) {
712
744
  }
713
745
 
714
746
  // src/validation-command.ts
747
+ import { existsSync as existsSync4 } from "fs";
715
748
  import path11 from "path";
716
749
  import { pathToFileURL as pathToFileURL4 } from "url";
717
750
  import { parseArgs } from "util";
@@ -729,8 +762,13 @@ function readEntryValue(entry, keys) {
729
762
  }
730
763
  return void 0;
731
764
  }
765
+ function getVercelEntries(value) {
766
+ if (Array.isArray(value)) return value;
767
+ if (isRecord(value) && Array.isArray(value.envs)) return value.envs;
768
+ return [];
769
+ }
732
770
  function parseVercelPayload(value) {
733
- const entries = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value.envs) ? value.envs : [];
771
+ const entries = getVercelEntries(value);
734
772
  const result = {};
735
773
  for (const entry of entries) {
736
774
  if (!isRecord(entry)) continue;
@@ -741,8 +779,13 @@ function parseVercelPayload(value) {
741
779
  }
742
780
  return result;
743
781
  }
782
+ function getCloudflareEntries(value) {
783
+ if (Array.isArray(value)) return value;
784
+ if (isRecord(value) && Array.isArray(value.result)) return value.result;
785
+ return [];
786
+ }
744
787
  function parseCloudflarePayload(value) {
745
- const entries = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value.result) ? value.result : [];
788
+ const entries = getCloudflareEntries(value);
746
789
  const result = {};
747
790
  for (const entry of entries) {
748
791
  if (!isRecord(entry)) continue;
@@ -760,7 +803,7 @@ function parseAwsPayload(value) {
760
803
  if (!isRecord(entry)) continue;
761
804
  const name = readEntryValue(entry, ["Name", "name"]);
762
805
  if (name === void 0) continue;
763
- const key = name.split("/").filter((part) => part.length > 0).pop() ?? name;
806
+ const key = name.split("/").findLast((part) => part.length > 0) ?? name;
764
807
  const envValue = readEntryValue(entry, ["Value", "value"]) ?? "";
765
808
  result[key] = envValue;
766
809
  }
@@ -773,7 +816,15 @@ function parseProviderPayload(provider, value) {
773
816
  }
774
817
  async function loadCloudSource(options) {
775
818
  const resolvedPath = path6.resolve(options.filePath);
776
- const raw = await readFile2(resolvedPath, "utf8");
819
+ let raw;
820
+ try {
821
+ raw = await readFile2(resolvedPath, "utf8");
822
+ } catch (err) {
823
+ if (err instanceof Error && err.code === "ENOENT") {
824
+ throw new Error(`File not found: ${options.filePath}`);
825
+ }
826
+ throw err;
827
+ }
777
828
  const parsed = JSON.parse(raw);
778
829
  return parseProviderPayload(options.provider, parsed);
779
830
  }
@@ -905,7 +956,7 @@ function findDefaultContractPath(cwd) {
905
956
  async function loadValidationContract(options) {
906
957
  const { fallbackExamplePath, contractPath, cwd = process.cwd() } = options;
907
958
  const discoveredContractPath = findDefaultContractPath(cwd);
908
- const resolvedContractPath = contractPath !== void 0 ? path7.resolve(cwd, contractPath) : discoveredContractPath;
959
+ const resolvedContractPath = contractPath === void 0 ? discoveredContractPath : path7.resolve(cwd, contractPath);
909
960
  if (resolvedContractPath !== void 0 && existsSync2(resolvedContractPath)) {
910
961
  const moduleUrl = pathToFileURL2(resolvedContractPath).href;
911
962
  const moduleValue = await import(moduleUrl);
@@ -924,6 +975,7 @@ async function loadValidationContract(options) {
924
975
  }
925
976
 
926
977
  // src/plugins.ts
978
+ import { existsSync as existsSync3 } from "fs";
927
979
  import path8 from "path";
928
980
  import { pathToFileURL as pathToFileURL3 } from "url";
929
981
  function isRecord3(value) {
@@ -945,10 +997,20 @@ function isPlugin(value) {
945
997
  }
946
998
  async function loadPluginFromPath(pluginPath, cwd) {
947
999
  const resolvedPath = path8.resolve(cwd, pluginPath);
1000
+ if (!existsSync3(resolvedPath)) {
1001
+ throw new Error(`Plugin not found: ${pluginPath}`);
1002
+ }
948
1003
  const moduleValue = await import(pathToFileURL3(resolvedPath).href);
949
1004
  const candidate = moduleValue.default ?? moduleValue.plugin;
950
1005
  if (isPlugin(candidate)) return candidate;
951
- throw new Error(`Invalid plugin at ${resolvedPath}. Expected a plugin object export.`);
1006
+ throw new Error(
1007
+ `Invalid plugin at ${resolvedPath}.
1008
+ Expected a default export matching:
1009
+ { name: string,
1010
+ transformSource?(ctx: { environment: string; values: Record<string, string> }): Record<string, string>,
1011
+ transformReport?(report: ValidationReport): ValidationReport,
1012
+ transformContract?(contract: EnvContract): EnvContract }`
1013
+ );
952
1014
  }
953
1015
  async function loadPlugins(options) {
954
1016
  const cwd = options.cwd ?? process.cwd();
@@ -993,8 +1055,8 @@ function applyReportPlugins(report, plugins) {
993
1055
  }
994
1056
 
995
1057
  // src/validation/engine.ts
996
- var EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
997
- var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
1058
+ var EMAIL_RE = /^[^@\s]+@[^@\s.]+(?:\.[^@\s.]+)+$/;
1059
+ var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
998
1060
  function detectReceivedType(value) {
999
1061
  const normalized = value.trim();
1000
1062
  if (normalized.length === 0) return "unknown";
@@ -1014,65 +1076,72 @@ function detectReceivedType(value) {
1014
1076
  }
1015
1077
  return "string";
1016
1078
  }
1017
- function validateValueAgainstExpected(expected, rawValue) {
1018
- const normalized = rawValue.trim();
1019
- const receivedType = detectReceivedType(normalized);
1020
- if (expected.type === "unknown") return { isValid: true, receivedType };
1021
- if (expected.type === "string") return { isValid: true, receivedType };
1022
- if (expected.type === "number") {
1023
- const parsed = Number(normalized);
1024
- if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
1025
- return { isValid: false, receivedType, issueType: "invalid_type" };
1026
- }
1027
- if (expected.min !== void 0 && parsed < expected.min) {
1028
- return { isValid: false, receivedType, issueType: "invalid_value" };
1029
- }
1030
- if (expected.max !== void 0 && parsed > expected.max) {
1031
- return { isValid: false, receivedType, issueType: "invalid_value" };
1032
- }
1033
- return { isValid: true, receivedType };
1079
+ function validateNumber(expected, normalized, receivedType) {
1080
+ const parsed = Number(normalized);
1081
+ if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
1082
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1034
1083
  }
1035
- if (expected.type === "boolean") {
1036
- if (!["true", "false", "1", "0", "yes", "no"].includes(normalized.toLowerCase())) {
1037
- return { isValid: false, receivedType, issueType: "invalid_type" };
1038
- }
1039
- return { isValid: true, receivedType };
1084
+ if (expected.min !== void 0 && parsed < expected.min) {
1085
+ return { isValid: false, receivedType, issueType: "invalid_value" };
1040
1086
  }
1041
- if (expected.type === "enum") {
1042
- if (!expected.values.includes(normalized)) {
1043
- return { isValid: false, receivedType, issueType: "invalid_value" };
1044
- }
1045
- return { isValid: true, receivedType };
1087
+ if (expected.max !== void 0 && parsed > expected.max) {
1088
+ return { isValid: false, receivedType, issueType: "invalid_value" };
1046
1089
  }
1047
- if (expected.type === "url") {
1048
- try {
1049
- const value = new URL(normalized);
1050
- if (value.protocol.length === 0)
1051
- return { isValid: false, receivedType, issueType: "invalid_type" };
1052
- return { isValid: true, receivedType };
1053
- } catch {
1054
- return { isValid: false, receivedType, issueType: "invalid_type" };
1055
- }
1090
+ return { isValid: true, receivedType };
1091
+ }
1092
+ function validateBoolean(normalized, receivedType) {
1093
+ if (!["true", "false", "1", "0", "yes", "no"].includes(normalized.toLowerCase())) {
1094
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1095
+ }
1096
+ return { isValid: true, receivedType };
1097
+ }
1098
+ function validateEnum(expected, normalized, receivedType) {
1099
+ if (!expected.values.includes(normalized)) {
1100
+ return { isValid: false, receivedType, issueType: "invalid_value" };
1056
1101
  }
1057
- if (expected.type === "email") {
1058
- if (!EMAIL_RE.test(normalized))
1102
+ return { isValid: true, receivedType };
1103
+ }
1104
+ function validateUrl(normalized, receivedType) {
1105
+ try {
1106
+ const value = new URL(normalized);
1107
+ if (value.protocol.length === 0)
1059
1108
  return { isValid: false, receivedType, issueType: "invalid_type" };
1060
1109
  return { isValid: true, receivedType };
1110
+ } catch {
1111
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1061
1112
  }
1062
- if (expected.type === "json") {
1063
- try {
1064
- const parsed = JSON.parse(normalized);
1065
- if (typeof parsed === "object" && parsed !== null) return { isValid: true, receivedType };
1066
- return { isValid: false, receivedType, issueType: "invalid_type" };
1067
- } catch {
1068
- return { isValid: false, receivedType, issueType: "invalid_type" };
1069
- }
1113
+ }
1114
+ function validateEmail(normalized, receivedType) {
1115
+ if (!EMAIL_RE.test(normalized))
1116
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1117
+ return { isValid: true, receivedType };
1118
+ }
1119
+ function validateJson(normalized, receivedType) {
1120
+ try {
1121
+ const parsed = JSON.parse(normalized);
1122
+ if (typeof parsed === "object" && parsed !== null) return { isValid: true, receivedType };
1123
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1124
+ } catch {
1125
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1070
1126
  }
1071
- if (expected.type === "semver") {
1072
- if (!SEMVER_RE.test(normalized))
1073
- return { isValid: false, receivedType, issueType: "invalid_value" };
1127
+ }
1128
+ function validateSemver(normalized, receivedType) {
1129
+ if (!SEMVER_RE.test(normalized))
1130
+ return { isValid: false, receivedType, issueType: "invalid_value" };
1131
+ return { isValid: true, receivedType };
1132
+ }
1133
+ function validateValueAgainstExpected(expected, rawValue) {
1134
+ const normalized = rawValue.trim();
1135
+ const receivedType = detectReceivedType(normalized);
1136
+ if (expected.type === "unknown" || expected.type === "string")
1074
1137
  return { isValid: true, receivedType };
1075
- }
1138
+ if (expected.type === "number") return validateNumber(expected, normalized, receivedType);
1139
+ if (expected.type === "boolean") return validateBoolean(normalized, receivedType);
1140
+ if (expected.type === "enum") return validateEnum(expected, normalized, receivedType);
1141
+ if (expected.type === "url") return validateUrl(normalized, receivedType);
1142
+ if (expected.type === "email") return validateEmail(normalized, receivedType);
1143
+ if (expected.type === "json") return validateJson(normalized, receivedType);
1144
+ if (expected.type === "semver") return validateSemver(normalized, receivedType);
1076
1145
  return { isValid: true, receivedType };
1077
1146
  }
1078
1147
  function toIssueCode(issueType) {
@@ -1144,57 +1213,62 @@ function buildReport(env, issues, recommendations) {
1144
1213
  function isClientSecret(variable, key) {
1145
1214
  return variable.secret === true && (variable.clientSide || key.startsWith("NEXT_PUBLIC_"));
1146
1215
  }
1216
+ function checkContractVariable(key, variable, context) {
1217
+ const { options, issues } = context;
1218
+ const value = options.values[key];
1219
+ const hasValue = value !== void 0;
1220
+ if (variable.required && !hasValue) {
1221
+ issues.push(
1222
+ createIssue({
1223
+ type: "missing",
1224
+ severity: "error",
1225
+ key,
1226
+ environment: options.environment,
1227
+ message: `Required variable ${key} is missing.`,
1228
+ debugValues: options.debugValues,
1229
+ expected: variable.expected
1230
+ })
1231
+ );
1232
+ return;
1233
+ }
1234
+ if (!hasValue) return;
1235
+ const validation = validateValueAgainstExpected(variable.expected, value);
1236
+ if (!validation.isValid) {
1237
+ const message = validation.issueType === "invalid_type" ? `Variable ${key} has invalid type.` : `Variable ${key} has invalid value.`;
1238
+ issues.push(
1239
+ createIssue({
1240
+ type: validation.issueType,
1241
+ severity: "error",
1242
+ key,
1243
+ environment: options.environment,
1244
+ message,
1245
+ value,
1246
+ debugValues: options.debugValues,
1247
+ expected: variable.expected,
1248
+ receivedType: validation.receivedType
1249
+ })
1250
+ );
1251
+ }
1252
+ if (isClientSecret(variable, key)) {
1253
+ issues.push(
1254
+ createIssue({
1255
+ type: "secret_exposed",
1256
+ severity: "error",
1257
+ key,
1258
+ environment: options.environment,
1259
+ message: `Secret variable ${key} is marked as client-side.`,
1260
+ value,
1261
+ debugValues: options.debugValues,
1262
+ expected: variable.expected
1263
+ })
1264
+ );
1265
+ }
1266
+ }
1147
1267
  function validateAgainstContract(options) {
1148
1268
  const issues = [];
1149
1269
  const contractKeys = new Set(Object.keys(options.contract.variables));
1150
1270
  for (const [key, variable] of Object.entries(options.contract.variables)) {
1151
- const value = options.values[key];
1152
- const hasValue = value !== void 0 && value.trim().length > 0;
1153
- if (variable.required && !hasValue) {
1154
- issues.push(
1155
- createIssue({
1156
- type: "missing",
1157
- severity: "error",
1158
- key,
1159
- environment: options.environment,
1160
- message: `Required variable ${key} is missing.`,
1161
- debugValues: options.debugValues,
1162
- expected: variable.expected
1163
- })
1164
- );
1165
- continue;
1166
- }
1167
- if (!hasValue) continue;
1168
- const validation = validateValueAgainstExpected(variable.expected, value);
1169
- if (!validation.isValid) {
1170
- issues.push(
1171
- createIssue({
1172
- type: validation.issueType,
1173
- severity: "error",
1174
- key,
1175
- environment: options.environment,
1176
- message: validation.issueType === "invalid_type" ? `Variable ${key} has invalid type.` : `Variable ${key} has invalid value.`,
1177
- value,
1178
- debugValues: options.debugValues,
1179
- expected: variable.expected,
1180
- receivedType: validation.receivedType
1181
- })
1182
- );
1183
- }
1184
- if (isClientSecret(variable, key)) {
1185
- issues.push(
1186
- createIssue({
1187
- type: "secret_exposed",
1188
- severity: "error",
1189
- key,
1190
- environment: options.environment,
1191
- message: `Secret variable ${key} is marked as client-side.`,
1192
- value,
1193
- debugValues: options.debugValues,
1194
- expected: variable.expected
1195
- })
1196
- );
1197
- }
1271
+ checkContractVariable(key, variable, { options, issues });
1198
1272
  }
1199
1273
  for (const [key, value] of Object.entries(options.values)) {
1200
1274
  if (contractKeys.has(key)) continue;
@@ -1222,6 +1296,97 @@ function collectUnionKeys(contract, sources) {
1222
1296
  }
1223
1297
  return union;
1224
1298
  }
1299
+ function diffMissingEntries(key, missing, context) {
1300
+ const { variable, options, issues } = context;
1301
+ for (const entry of missing) {
1302
+ issues.push(
1303
+ createIssue({
1304
+ type: "missing",
1305
+ severity: "error",
1306
+ key,
1307
+ environment: entry.sourceName,
1308
+ message: `Variable ${key} is missing in ${entry.sourceName}.`,
1309
+ debugValues: options.debugValues,
1310
+ ...variable !== void 0 && { expected: variable.expected }
1311
+ })
1312
+ );
1313
+ }
1314
+ }
1315
+ function diffTypeConflicts(key, present, context) {
1316
+ const { variable, options, issues } = context;
1317
+ const typeBySource = /* @__PURE__ */ new Map();
1318
+ for (const entry of present) {
1319
+ typeBySource.set(entry.sourceName, detectReceivedType(entry.value ?? ""));
1320
+ }
1321
+ if (new Set(typeBySource.values()).size <= 1) return;
1322
+ for (const [sourceName, detectedType] of typeBySource.entries()) {
1323
+ issues.push(
1324
+ createIssue({
1325
+ type: "conflict",
1326
+ severity: "error",
1327
+ key,
1328
+ environment: sourceName,
1329
+ message: `Variable ${key} has conflicting inferred type across environments.`,
1330
+ debugValues: options.debugValues,
1331
+ receivedType: detectedType,
1332
+ ...options.sources[sourceName]?.[key] !== void 0 && {
1333
+ value: options.sources[sourceName]?.[key]
1334
+ },
1335
+ ...variable !== void 0 && { expected: variable.expected }
1336
+ })
1337
+ );
1338
+ }
1339
+ }
1340
+ function diffPresentEntry(key, entry, context) {
1341
+ const { variable, options, issues } = context;
1342
+ if (entry.value === void 0) return;
1343
+ if (variable === void 0) {
1344
+ const severity = options.strict ? "error" : "warning";
1345
+ issues.push(
1346
+ createIssue({
1347
+ type: "extra",
1348
+ severity,
1349
+ key,
1350
+ environment: entry.sourceName,
1351
+ message: `Variable ${key} is not defined in the contract.`,
1352
+ value: entry.value,
1353
+ debugValues: options.debugValues
1354
+ })
1355
+ );
1356
+ return;
1357
+ }
1358
+ const validation = validateValueAgainstExpected(variable.expected, entry.value);
1359
+ if (!validation.isValid) {
1360
+ const message = validation.issueType === "invalid_type" ? `Variable ${key} has invalid type in ${entry.sourceName}.` : `Variable ${key} has invalid value in ${entry.sourceName}.`;
1361
+ issues.push(
1362
+ createIssue({
1363
+ type: validation.issueType,
1364
+ severity: "error",
1365
+ key,
1366
+ environment: entry.sourceName,
1367
+ message,
1368
+ value: entry.value,
1369
+ debugValues: options.debugValues,
1370
+ expected: variable.expected,
1371
+ receivedType: validation.receivedType
1372
+ })
1373
+ );
1374
+ }
1375
+ if (isClientSecret(variable, key)) {
1376
+ issues.push(
1377
+ createIssue({
1378
+ type: "secret_exposed",
1379
+ severity: "error",
1380
+ key,
1381
+ environment: entry.sourceName,
1382
+ message: `Secret variable ${key} is marked as client-side.`,
1383
+ value: entry.value,
1384
+ debugValues: options.debugValues,
1385
+ expected: variable.expected
1386
+ })
1387
+ );
1388
+ }
1389
+ }
1225
1390
  function diffEnvironmentSources(options) {
1226
1391
  const issues = [];
1227
1392
  const sourceNames = Object.keys(options.sources);
@@ -1232,12 +1397,8 @@ function diffEnvironmentSources(options) {
1232
1397
  sourceName,
1233
1398
  value: options.sources[sourceName]?.[key]
1234
1399
  }));
1235
- const present = valuesBySource.filter(
1236
- (entry) => entry.value !== void 0 && entry.value !== ""
1237
- );
1238
- const missing = valuesBySource.filter(
1239
- (entry) => entry.value === void 0 || entry.value === ""
1240
- );
1400
+ const present = valuesBySource.filter((entry) => entry.value !== void 0);
1401
+ const missing = valuesBySource.filter((entry) => entry.value === void 0);
1241
1402
  if (present.length === 0 && variable?.required === true) {
1242
1403
  for (const entry of missing) {
1243
1404
  issues.push(
@@ -1254,92 +1415,13 @@ function diffEnvironmentSources(options) {
1254
1415
  }
1255
1416
  continue;
1256
1417
  }
1418
+ const ctx = { variable, options, issues };
1257
1419
  if (present.length > 0) {
1258
- for (const entry of missing) {
1259
- issues.push(
1260
- createIssue({
1261
- type: "missing",
1262
- severity: "error",
1263
- key,
1264
- environment: entry.sourceName,
1265
- message: `Variable ${key} is missing in ${entry.sourceName}.`,
1266
- debugValues: options.debugValues,
1267
- ...variable !== void 0 && { expected: variable.expected }
1268
- })
1269
- );
1270
- }
1420
+ diffMissingEntries(key, missing, ctx);
1271
1421
  }
1272
- const typeBySource = /* @__PURE__ */ new Map();
1422
+ diffTypeConflicts(key, present, ctx);
1273
1423
  for (const entry of present) {
1274
- const detected = detectReceivedType(entry.value ?? "");
1275
- typeBySource.set(entry.sourceName, detected);
1276
- }
1277
- if (new Set(typeBySource.values()).size > 1) {
1278
- for (const [sourceName, detectedType] of typeBySource.entries()) {
1279
- issues.push(
1280
- createIssue({
1281
- type: "conflict",
1282
- severity: "error",
1283
- key,
1284
- environment: sourceName,
1285
- message: `Variable ${key} has conflicting inferred type across environments.`,
1286
- debugValues: options.debugValues,
1287
- receivedType: detectedType,
1288
- ...options.sources[sourceName]?.[key] !== void 0 && {
1289
- value: options.sources[sourceName]?.[key]
1290
- },
1291
- ...variable !== void 0 && { expected: variable.expected }
1292
- })
1293
- );
1294
- }
1295
- }
1296
- for (const entry of present) {
1297
- if (entry.value === void 0) continue;
1298
- if (variable === void 0) {
1299
- const severity = options.strict ? "error" : "warning";
1300
- issues.push(
1301
- createIssue({
1302
- type: "extra",
1303
- severity,
1304
- key,
1305
- environment: entry.sourceName,
1306
- message: `Variable ${key} is not defined in the contract.`,
1307
- value: entry.value,
1308
- debugValues: options.debugValues
1309
- })
1310
- );
1311
- continue;
1312
- }
1313
- const validation = validateValueAgainstExpected(variable.expected, entry.value);
1314
- if (!validation.isValid) {
1315
- issues.push(
1316
- createIssue({
1317
- type: validation.issueType,
1318
- severity: "error",
1319
- key,
1320
- environment: entry.sourceName,
1321
- message: validation.issueType === "invalid_type" ? `Variable ${key} has invalid type in ${entry.sourceName}.` : `Variable ${key} has invalid value in ${entry.sourceName}.`,
1322
- value: entry.value,
1323
- debugValues: options.debugValues,
1324
- expected: variable.expected,
1325
- receivedType: validation.receivedType
1326
- })
1327
- );
1328
- }
1329
- if (isClientSecret(variable, key)) {
1330
- issues.push(
1331
- createIssue({
1332
- type: "secret_exposed",
1333
- severity: "error",
1334
- key,
1335
- environment: entry.sourceName,
1336
- message: `Secret variable ${key} is marked as client-side.`,
1337
- value: entry.value,
1338
- debugValues: options.debugValues,
1339
- expected: variable.expected
1340
- })
1341
- );
1342
- }
1424
+ diffPresentEntry(key, entry, ctx);
1343
1425
  }
1344
1426
  }
1345
1427
  return buildReport("diff", issues);
@@ -1392,7 +1474,7 @@ function parseEnvSourceContent(content) {
1392
1474
  for (const line of lines) {
1393
1475
  const trimmed = line.trim();
1394
1476
  if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
1395
- const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(trimmed);
1477
+ const match = /^(?:export\s+)?([A-Za-z_]\w*)=(.*)$/.exec(trimmed);
1396
1478
  if (match === null) continue;
1397
1479
  const key = match[1] ?? "";
1398
1480
  const rawValue = match[2] ?? "";
@@ -1405,11 +1487,11 @@ async function loadEnvSource(options) {
1405
1487
  try {
1406
1488
  const content = await readFile3(resolvedPath, "utf8");
1407
1489
  return parseEnvSourceContent(content);
1408
- } catch (errorValue) {
1409
- if (options.allowMissing === true && errorValue instanceof Error && "code" in errorValue && errorValue.code === "ENOENT") {
1490
+ } catch (error_) {
1491
+ if (options.allowMissing === true && error_ instanceof Error && "code" in error_ && error_.code === "ENOENT") {
1410
1492
  return {};
1411
1493
  }
1412
- throw errorValue;
1494
+ throw error_;
1413
1495
  }
1414
1496
  }
1415
1497
 
@@ -1423,8 +1505,8 @@ function toJsonString(report, mode) {
1423
1505
  `;
1424
1506
  }
1425
1507
  function formatIssue(issue) {
1426
- const expected = issue.expected !== void 0 ? ` expected=${issue.expected.type}` : "";
1427
- const received = issue.receivedType !== void 0 ? ` received=${issue.receivedType}` : "";
1508
+ const expected = issue.expected === void 0 ? "" : ` expected=${issue.expected.type}`;
1509
+ const received = issue.receivedType === void 0 ? "" : ` received=${issue.receivedType}`;
1428
1510
  return `${issue.severity.toUpperCase()} [${issue.code}] ${issue.environment}:${issue.key} ${issue.message}${expected}${received}`;
1429
1511
  }
1430
1512
  function formatHumanReport(report) {
@@ -1433,15 +1515,13 @@ function formatHumanReport(report) {
1433
1515
  `Status: ${report.status.toUpperCase()} (errors=${report.summary.errors}, warnings=${report.summary.warnings}, total=${report.summary.total})`
1434
1516
  );
1435
1517
  if (report.issues.length > 0) {
1436
- lines.push("");
1437
- lines.push("Issues:");
1518
+ lines.push("", "Issues:");
1438
1519
  for (const issue of report.issues) {
1439
1520
  lines.push(`- ${formatIssue(issue)}`);
1440
1521
  }
1441
1522
  }
1442
1523
  if (report.recommendations !== void 0 && report.recommendations.length > 0) {
1443
- lines.push("");
1444
- lines.push("Recommendations:");
1524
+ lines.push("", "Recommendations:");
1445
1525
  for (const recommendation of report.recommendations) {
1446
1526
  lines.push(`- ${recommendation}`);
1447
1527
  }
@@ -1486,7 +1566,11 @@ var HELP_TEXT = {
1486
1566
  " --cloud-file <path> Cloud snapshot JSON file",
1487
1567
  " --plugin <path> Plugin module path (repeatable)",
1488
1568
  " -c, --config <path> Config file path",
1489
- " -h, --help Show this help"
1569
+ " -h, --help Show this help",
1570
+ "",
1571
+ "Exit codes:",
1572
+ " 0 All checks passed (status: ok or warn)",
1573
+ " 1 One or more checks failed (status: fail) or invalid usage"
1490
1574
  ].join("\n"),
1491
1575
  diff: [
1492
1576
  "Usage: env-typegen diff [options]",
@@ -1505,7 +1589,11 @@ var HELP_TEXT = {
1505
1589
  " --cloud-file <path> Cloud snapshot JSON file added to diff sources",
1506
1590
  " --plugin <path> Plugin module path (repeatable)",
1507
1591
  " -c, --config <path> Config file path",
1508
- " -h, --help Show this help"
1592
+ " -h, --help Show this help",
1593
+ "",
1594
+ "Exit codes:",
1595
+ " 0 All checks passed (status: ok or warn)",
1596
+ " 1 One or more checks failed (status: fail) or invalid usage"
1509
1597
  ].join("\n"),
1510
1598
  doctor: [
1511
1599
  "Usage: env-typegen doctor [options]",
@@ -1525,7 +1613,11 @@ var HELP_TEXT = {
1525
1613
  " --cloud-file <path> Cloud snapshot JSON file",
1526
1614
  " --plugin <path> Plugin module path (repeatable)",
1527
1615
  " -c, --config <path> Config file path",
1528
- " -h, --help Show this help"
1616
+ " -h, --help Show this help",
1617
+ "",
1618
+ "Exit codes:",
1619
+ " 0 All checks passed (status: ok or warn)",
1620
+ " 1 One or more checks failed (status: fail) or invalid usage"
1529
1621
  ].join("\n")
1530
1622
  };
1531
1623
  function resolveConfigRelative(value, configDir) {
@@ -1567,6 +1659,9 @@ async function loadCommandConfig(configPath) {
1567
1659
  return loadConfig(process.cwd());
1568
1660
  }
1569
1661
  const resolvedPath = path11.resolve(configPath);
1662
+ if (!existsSync4(resolvedPath)) {
1663
+ throw new Error(`Config file not found: ${configPath}`);
1664
+ }
1570
1665
  const configDir = path11.dirname(resolvedPath);
1571
1666
  const moduleValue = await import(pathToFileURL4(resolvedPath).href);
1572
1667
  if (moduleValue.default === void 0) return void 0;
@@ -1612,7 +1707,10 @@ function parseValidationArgs(argv) {
1612
1707
  }
1613
1708
  });
1614
1709
  const castValues = values;
1615
- const jsonMode = castValues.json === true ? assignedMode === "off" ? "compact" : assignedMode : "off";
1710
+ let jsonMode = "off";
1711
+ if (castValues.json === true) {
1712
+ jsonMode = assignedMode === "off" ? "compact" : assignedMode;
1713
+ }
1616
1714
  return { values: castValues, jsonMode };
1617
1715
  }
1618
1716
  function resolveStrict(values, fileConfig) {
@@ -1685,12 +1783,12 @@ async function runCheckCommand(args) {
1685
1783
  const provider = context.cloudProvider;
1686
1784
  let environment = args.values.env?.[0] ?? ".env";
1687
1785
  let sourceValues;
1688
- if (provider !== void 0) {
1786
+ if (provider === void 0) {
1787
+ sourceValues = await loadEnvSource({ filePath: environment, allowMissing: true });
1788
+ } else {
1689
1789
  const cloudFile = context.cloudFile ?? `${provider}.env.json`;
1690
1790
  sourceValues = await loadCloudSource({ provider, filePath: cloudFile });
1691
1791
  environment = `cloud:${provider}`;
1692
- } else {
1693
- sourceValues = await loadEnvSource({ filePath: environment, allowMissing: true });
1694
1792
  }
1695
1793
  sourceValues = applySourcePlugins({ environment, values: sourceValues }, context.plugins);
1696
1794
  const report = applyReportPlugins(
@@ -1822,8 +1920,9 @@ async function runValidationCommand(params) {
1822
1920
  }
1823
1921
 
1824
1922
  // src/watch.ts
1825
- import path12 from "path";
1826
1923
  import { watch } from "chokidar";
1924
+ import { existsSync as existsSync5 } from "fs";
1925
+ import path12 from "path";
1827
1926
  function debounce(fn, delay) {
1828
1927
  let timer;
1829
1928
  return (...args) => {
@@ -1835,6 +1934,13 @@ function debounce(fn, delay) {
1835
1934
  };
1836
1935
  }
1837
1936
  function startWatch({ inputPath, runOptions, cwd = process.cwd() }) {
1937
+ const inputPaths = Array.isArray(inputPath) ? inputPath : [inputPath];
1938
+ for (const singlePath of inputPaths) {
1939
+ if (!existsSync5(singlePath)) {
1940
+ error(`File not found: ${singlePath}`);
1941
+ return process.exit(1);
1942
+ }
1943
+ }
1838
1944
  const inputLabel = Array.isArray(inputPath) ? inputPath.join(", ") : inputPath;
1839
1945
  log(`Watching ${inputLabel} for changes...`);
1840
1946
  void runGenerate(runOptions).catch((err) => {
@@ -1858,6 +1964,9 @@ function startWatch({ inputPath, runOptions, cwd = process.cwd() }) {
1858
1964
  if (reloaded.inferenceRules !== void 0) {
1859
1965
  runOptions.inferenceRules = reloaded.inferenceRules;
1860
1966
  }
1967
+ if (reloaded.output !== void 0) {
1968
+ runOptions.output = reloaded.output;
1969
+ }
1861
1970
  }
1862
1971
  void runGenerate(runOptions).catch((err) => {
1863
1972
  const message = err instanceof Error ? err.message : JSON.stringify(err);
@@ -1875,7 +1984,7 @@ function startWatch({ inputPath, runOptions, cwd = process.cwd() }) {
1875
1984
  const configPaths = CONFIG_FILE_NAMES.map((name) => path12.resolve(cwd, name));
1876
1985
  const configWatcher = watch(configPaths, { persistent: true, ignoreInitial: true });
1877
1986
  for (const event of ["add", "change"]) {
1878
- configWatcher.on(event, (eventPath) => void handleConfigChange(eventPath));
1987
+ configWatcher.on(event, (eventPath) => handleConfigChange(eventPath));
1879
1988
  }
1880
1989
  process.on("SIGINT", () => {
1881
1990
  void Promise.all([inputWatcher.close(), configWatcher.close()]).then(() => {
@@ -1899,7 +2008,9 @@ var HELP_TEXT2 = [
1899
2008
  "",
1900
2009
  "Options:",
1901
2010
  " -i, --input <path> Path to .env.example file(s). May be specified multiple times.",
1902
- " -o, --output <path> Output file path (default: env.generated.ts)",
2011
+ " -o, --output <path> Output base path (default: env.generated.ts).",
2012
+ " With multiple generators, suffixes are appended:",
2013
+ " env.generated.typescript.ts, .zod.ts, .t3.ts, .declaration.d.ts",
1903
2014
  " -f, --format <name> Generator format: ts|zod|t3|declaration",
1904
2015
  " May be specified multiple times.",
1905
2016
  " -g, --generator <name> Backward-compatible alias for --format",
@@ -1910,7 +2021,16 @@ var HELP_TEXT2 = [
1910
2021
  " -w, --watch Watch for changes and regenerate",
1911
2022
  " -c, --config <path> Path to config file",
1912
2023
  " -v, --version Print version",
1913
- " -h, --help Show this help"
2024
+ " -h, --help Show this help",
2025
+ "",
2026
+ "Config file:",
2027
+ " Auto-discovered in order: env-typegen.config.mjs \u2192 .js \u2192 .ts (in cwd)",
2028
+ " CLI flags always override config file values.",
2029
+ " Use defineConfig() from @xlameiro/env-typegen for IDE autocompletion.",
2030
+ "",
2031
+ "Exit codes:",
2032
+ " 0 Success \u2014 files generated without errors",
2033
+ " 1 Error \u2014 invalid flags or generation failed"
1914
2034
  ].join("\n");
1915
2035
  var VALIDATION_SUBCOMMANDS = /* @__PURE__ */ new Set(["check", "diff", "doctor"]);
1916
2036
  var FORMAT_TO_GENERATOR = {
@@ -1966,6 +2086,15 @@ async function runValidationSubcommand(subcommand, argv) {
1966
2086
  process.exitCode = exitCode;
1967
2087
  }
1968
2088
  }
2089
+ async function loadExplicitConfig(configPath, userPath) {
2090
+ if (!existsSync6(configPath)) {
2091
+ throw new Error(`Config file not found: ${userPath}`);
2092
+ }
2093
+ const configDir = path13.dirname(configPath);
2094
+ const mod = await import(pathToFileURL5(configPath).href);
2095
+ const rawConfig = mod.default;
2096
+ return rawConfig ? applyConfigPaths2(rawConfig, configDir) : void 0;
2097
+ }
1969
2098
  async function runCli(argv = process.argv.slice(2)) {
1970
2099
  const maybeSubcommand = argv[0];
1971
2100
  if (maybeSubcommand !== void 0 && VALIDATION_SUBCOMMANDS.has(maybeSubcommand)) {
@@ -2001,13 +2130,7 @@ async function runCli(argv = process.argv.slice(2)) {
2001
2130
  if (values.config === void 0) {
2002
2131
  fileConfig = await loadConfig(process.cwd());
2003
2132
  } else {
2004
- const configPath = path13.resolve(values.config);
2005
- const configDir = path13.dirname(configPath);
2006
- const mod = await import(pathToFileURL5(configPath).href);
2007
- const rawConfig = mod.default;
2008
- if (rawConfig) {
2009
- fileConfig = applyConfigPaths2(rawConfig, configDir);
2010
- }
2133
+ fileConfig = await loadExplicitConfig(path13.resolve(values.config), values.config);
2011
2134
  }
2012
2135
  const cliInput = values.input?.length ? values.input : void 0;
2013
2136
  const input = cliInput ?? fileConfig?.input;