@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/index.cjs CHANGED
@@ -5,12 +5,13 @@ var path6 = require('path');
5
5
  var url = require('url');
6
6
  var promises = require('fs/promises');
7
7
  var prettier = require('prettier');
8
- var picocolors = require('picocolors');
8
+ var pc = require('picocolors');
9
9
  var util = require('util');
10
10
 
11
11
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
12
 
13
13
  var path6__default = /*#__PURE__*/_interopDefault(path6);
14
+ var pc__default = /*#__PURE__*/_interopDefault(pc);
14
15
 
15
16
  // src/parser/comment-parser.ts
16
17
  var VALID_ENV_VAR_TYPES = /* @__PURE__ */ new Set([
@@ -149,7 +150,9 @@ var inferenceRules = [
149
150
  {
150
151
  id: "P8_numeric_literal",
151
152
  priority: 8,
152
- match: (_key, value) => /^\d+(\.\d+)?$/.test(value),
153
+ // Non-capturing group with \d keeps the dot/digit boundary unambiguous,
154
+ // eliminating super-linear backtracking (ReDoS-safe).
155
+ match: (_key, value) => /^\d+(?:\.\d+)?$/.test(value),
153
156
  type: "number"
154
157
  },
155
158
  {
@@ -167,7 +170,9 @@ var inferenceRules = [
167
170
  {
168
171
  id: "P11_email_literal",
169
172
  priority: 11,
170
- match: (_key, value) => /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value),
173
+ // Dots are excluded from each domain-segment character class so that the
174
+ // literal \. separators are unambiguous, preventing super-linear backtracking.
175
+ match: (_key, value) => /^[^@\s]+@[^@\s.]+(?:\.[^@\s.]+)+$/.test(value),
171
176
  type: "email"
172
177
  },
173
178
  {
@@ -201,7 +206,50 @@ function inferTypesFromParsedVars(parsed, options) {
201
206
  return parsed.vars.map((item) => inferType(item.key, item.rawValue, options));
202
207
  }
203
208
  var ENV_VAR_RE = /^([A-Z_][A-Z0-9_]*)=(.*)$/;
204
- var SECTION_HEADER_RE = /^#\s+[-=]{3,}\s+(.+?)\s+[-=]{3,}\s*$/;
209
+ var SECTION_HEADER_RE = /^#\s+[-=]{3,}\s+(\S+(?:\s+\S+)*)\s+[-=]{3,}\s*$/;
210
+ function buildParsedVar(params, commentBlock, options) {
211
+ const annotations = parseCommentBlock(commentBlock);
212
+ const extraRules = options?.inferenceRules;
213
+ const inferredType = inferType(
214
+ params.key,
215
+ params.rawValue,
216
+ ...extraRules === void 0 ? [] : [{ extraRules }]
217
+ );
218
+ const isRequired = params.rawValue.length > 0 || annotations.isRequired;
219
+ const isOptional = params.rawValue.length === 0 && !annotations.isRequired;
220
+ const isClientSide = params.key.startsWith("NEXT_PUBLIC_");
221
+ const parsedVar = {
222
+ key: params.key,
223
+ rawValue: params.rawValue,
224
+ inferredType,
225
+ isRequired,
226
+ isOptional,
227
+ isClientSide,
228
+ lineNumber: params.lineNumber
229
+ };
230
+ if (annotations.annotatedType !== void 0) {
231
+ parsedVar.annotatedType = annotations.annotatedType;
232
+ }
233
+ if (annotations.description !== void 0) {
234
+ parsedVar.description = annotations.description;
235
+ }
236
+ if (params.currentGroup !== void 0) {
237
+ parsedVar.group = params.currentGroup;
238
+ }
239
+ if (annotations.enumValues !== void 0) {
240
+ parsedVar.enumValues = annotations.enumValues;
241
+ }
242
+ if (annotations.constraints !== void 0) {
243
+ parsedVar.constraints = annotations.constraints;
244
+ }
245
+ if (annotations.runtime !== void 0) {
246
+ parsedVar.runtime = annotations.runtime;
247
+ }
248
+ if (annotations.isSecret !== void 0) {
249
+ parsedVar.isSecret = annotations.isSecret;
250
+ }
251
+ return parsedVar;
252
+ }
205
253
  function parseEnvFileContent(content, filePath, options) {
206
254
  const lines = content.split("\n");
207
255
  const vars = [];
@@ -231,53 +279,18 @@ function parseEnvFileContent(content, filePath, options) {
231
279
  continue;
232
280
  }
233
281
  const envMatch = ENV_VAR_RE.exec(trimmed);
234
- if (envMatch !== null) {
235
- const key = envMatch[1] ?? "";
236
- const rawValue = envMatch[2] ?? "";
237
- const annotations = parseCommentBlock(commentBlock);
238
- const inferredType = inferType(
239
- key,
240
- rawValue,
241
- ...options?.inferenceRules !== void 0 ? [{ extraRules: options.inferenceRules }] : []
242
- );
243
- const isRequired = rawValue.length > 0 || annotations.isRequired;
244
- const isOptional = rawValue.length === 0 && !annotations.isRequired;
245
- const isClientSide = key.startsWith("NEXT_PUBLIC_");
246
- const parsedVar = {
247
- key,
248
- rawValue,
249
- inferredType,
250
- isRequired,
251
- isOptional,
252
- isClientSide,
253
- lineNumber
254
- };
255
- if (annotations.annotatedType !== void 0) {
256
- parsedVar.annotatedType = annotations.annotatedType;
257
- }
258
- if (annotations.description !== void 0) {
259
- parsedVar.description = annotations.description;
260
- }
261
- if (currentGroup !== void 0) {
262
- parsedVar.group = currentGroup;
263
- }
264
- if (annotations.enumValues !== void 0) {
265
- parsedVar.enumValues = annotations.enumValues;
266
- }
267
- if (annotations.constraints !== void 0) {
268
- parsedVar.constraints = annotations.constraints;
269
- }
270
- if (annotations.runtime !== void 0) {
271
- parsedVar.runtime = annotations.runtime;
272
- }
273
- if (annotations.isSecret !== void 0) {
274
- parsedVar.isSecret = annotations.isSecret;
275
- }
276
- vars.push(parsedVar);
277
- commentBlock = [];
278
- } else {
282
+ if (envMatch === null) {
279
283
  commentBlock = [];
284
+ continue;
280
285
  }
286
+ vars.push(
287
+ buildParsedVar(
288
+ { key: envMatch[1] ?? "", rawValue: envMatch[2] ?? "", lineNumber, currentGroup },
289
+ commentBlock,
290
+ options
291
+ )
292
+ );
293
+ commentBlock = [];
281
294
  }
282
295
  return { filePath, vars, groups };
283
296
  }
@@ -296,12 +309,14 @@ function generateTypeScriptTypes(parsed) {
296
309
  const fileName = path6__default.default.basename(parsed.filePath);
297
310
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
298
311
  const lines = [];
299
- lines.push("// Generated by env-typegen \u2014 do not edit manually");
300
- lines.push(`// Source: ${fileName}`);
301
- lines.push(`// Generated at: ${timestamp}`);
302
- lines.push("");
303
- lines.push("declare namespace NodeJS {");
304
- lines.push(" interface ProcessEnv {");
312
+ lines.push(
313
+ "// Generated by env-typegen \u2014 do not edit manually",
314
+ `// Source: ${fileName}`,
315
+ `// Generated at: ${timestamp}`,
316
+ "",
317
+ "declare namespace NodeJS {",
318
+ " interface ProcessEnv {"
319
+ );
305
320
  for (const variable of parsed.vars) {
306
321
  const effectiveType = variable.annotatedType ?? variable.inferredType;
307
322
  const optional = variable.isOptional ? "?" : "";
@@ -316,10 +331,7 @@ function generateTypeScriptTypes(parsed) {
316
331
  }
317
332
  lines.push(propLine);
318
333
  }
319
- lines.push(" }");
320
- lines.push("}");
321
- lines.push("");
322
- lines.push("export type EnvVars = {");
334
+ lines.push(" }", "}", "", "export type EnvVars = {");
323
335
  for (const variable of parsed.vars) {
324
336
  const effectiveType = variable.annotatedType ?? variable.inferredType;
325
337
  const tsType = toTsType(effectiveType);
@@ -329,28 +341,34 @@ function generateTypeScriptTypes(parsed) {
329
341
  lines.push("};");
330
342
  if (hasClientVars) {
331
343
  const clientKeyUnion = clientVars.map((v) => `"${v.key}"`).join(" | ");
332
- lines.push("");
333
- lines.push(`export type ServerEnvVars = Omit<EnvVars, ${clientKeyUnion}>;`);
334
- lines.push(`export type ClientEnvVars = Pick<EnvVars, ${clientKeyUnion}>;`);
344
+ lines.push(
345
+ "",
346
+ `export type ServerEnvVars = Omit<EnvVars, ${clientKeyUnion}>;`,
347
+ `export type ClientEnvVars = Pick<EnvVars, ${clientKeyUnion}>;`
348
+ );
335
349
  }
336
350
  return lines.join("\n") + "\n";
337
351
  }
338
352
  function generateEnvValidation(parsed) {
339
353
  const required = parsed.vars.filter((v) => v.isRequired).map((v) => v.key);
340
354
  const lines = [];
341
- lines.push("// Generated by env-typegen \u2014 do not edit manually");
342
- lines.push("");
343
- lines.push("export function validateEnv(): void {");
355
+ lines.push(
356
+ "// Generated by env-typegen \u2014 do not edit manually",
357
+ "",
358
+ "export function validateEnv(): void {"
359
+ );
344
360
  if (required.length === 0) {
345
361
  lines.push(" // No required environment variables defined");
346
362
  } else {
347
363
  const keyList = required.map((k) => `"${k}"`).join(", ");
348
- lines.push(` const required = [${keyList}];`);
349
- lines.push(" for (const key of required) {");
350
- lines.push(" if (!process.env[key]) {");
351
- lines.push(" throw new Error(`Missing required environment variable: ${key}`);");
352
- lines.push(" }");
353
- lines.push(" }");
364
+ lines.push(
365
+ ` const required = [${keyList}];`,
366
+ " for (const key of required) {",
367
+ " if (!process.env[key]) {",
368
+ " throw new Error(`Missing required environment variable: ${key}`);",
369
+ " }",
370
+ " }"
371
+ );
354
372
  }
355
373
  lines.push("}");
356
374
  return lines.join("\n") + "\n";
@@ -368,37 +386,40 @@ function generateZodSchema(parsed) {
368
386
  const serverVars = parsed.vars.filter((v) => !v.isClientSide);
369
387
  const clientVars = parsed.vars.filter((v) => v.isClientSide);
370
388
  const lines = [];
371
- lines.push("// Generated by env-typegen \u2014 do not edit manually");
372
- lines.push('import { z } from "zod";');
373
- lines.push("");
374
- lines.push("export const serverEnvSchema = z.object({");
389
+ lines.push(
390
+ "// Generated by env-typegen \u2014 do not edit manually",
391
+ 'import { z } from "zod";',
392
+ "",
393
+ "export const serverEnvSchema = z.object({"
394
+ );
375
395
  for (const variable of serverVars) {
376
396
  const effectiveType = variable.annotatedType ?? variable.inferredType;
377
397
  const zodExpr = variable.isOptional ? `${toZodType(effectiveType)}.optional()` : toZodType(effectiveType);
378
398
  lines.push(` ${variable.key}: ${zodExpr},`);
379
399
  }
380
- lines.push("});");
381
- lines.push("");
382
- lines.push("export const clientEnvSchema = z.object({");
400
+ lines.push("});", "", "export const clientEnvSchema = z.object({");
383
401
  for (const variable of clientVars) {
384
402
  const effectiveType = variable.annotatedType ?? variable.inferredType;
385
403
  const zodExpr = variable.isOptional ? `${toZodType(effectiveType)}.optional()` : toZodType(effectiveType);
386
404
  lines.push(` ${variable.key}: ${zodExpr},`);
387
405
  }
388
- lines.push("});");
389
- lines.push("");
390
- lines.push("export const envSchema = serverEnvSchema.merge(clientEnvSchema);");
391
- lines.push("export type Env = z.infer<typeof envSchema>;");
406
+ lines.push(
407
+ "});",
408
+ "",
409
+ "export const envSchema = serverEnvSchema.merge(clientEnvSchema);",
410
+ "export type Env = z.infer<typeof envSchema>;"
411
+ );
392
412
  return lines.join("\n") + "\n";
393
413
  }
394
414
  function generateDeclaration(parsed) {
395
415
  const fileName = path6__default.default.basename(parsed.filePath);
396
- const lines = [];
397
- lines.push("// Generated by env-typegen \u2014 do not edit manually");
398
- lines.push(`// Source: ${fileName}`);
399
- lines.push("");
400
- lines.push("declare namespace NodeJS {");
401
- lines.push(" interface ProcessEnv {");
416
+ const lines = [
417
+ "// Generated by env-typegen \u2014 do not edit manually",
418
+ `// Source: ${fileName}`,
419
+ "",
420
+ "declare namespace NodeJS {",
421
+ " interface ProcessEnv {"
422
+ ];
402
423
  for (const variable of parsed.vars) {
403
424
  const effectiveType = variable.annotatedType ?? variable.inferredType;
404
425
  const optional = variable.isOptional ? "?" : "";
@@ -413,12 +434,14 @@ function generateDeclaration(parsed) {
413
434
  }
414
435
  lines.push(propLine);
415
436
  }
416
- lines.push(" }");
417
- lines.push("}");
437
+ lines.push(" }", "}");
418
438
  return lines.join("\n") + "\n";
419
439
  }
420
440
 
421
441
  // src/generators/t3-generator.ts
442
+ function escapeJsStringLiteral(value) {
443
+ return value.replaceAll("\\", String.raw`\\`).replaceAll('"', String.raw`\"`);
444
+ }
422
445
  function toT3ZodType(envVarType) {
423
446
  if (envVarType === "number") return "z.coerce.number()";
424
447
  if (envVarType === "boolean") return "z.coerce.boolean()";
@@ -426,51 +449,47 @@ function toT3ZodType(envVarType) {
426
449
  if (envVarType === "email") return "z.string().email()";
427
450
  return "z.string()";
428
451
  }
452
+ function buildZodExpr(variable) {
453
+ const effectiveType = variable.annotatedType ?? variable.inferredType;
454
+ let zodExpr = toT3ZodType(effectiveType);
455
+ if (variable.description !== void 0) {
456
+ zodExpr += `.describe("${escapeJsStringLiteral(variable.description)}")`;
457
+ }
458
+ if (variable.isOptional) {
459
+ zodExpr += ".optional()";
460
+ }
461
+ return zodExpr;
462
+ }
429
463
  function generateT3Env(parsed) {
430
464
  const serverVars = parsed.vars.filter((v) => !v.isClientSide);
431
465
  const clientVars = parsed.vars.filter((v) => v.isClientSide);
432
- const lines = [];
433
- lines.push("// Generated by env-typegen \u2014 do not edit manually");
434
- lines.push('import { createEnv } from "@t3-oss/env-nextjs";');
435
- lines.push('import { z } from "zod";');
436
- lines.push("");
437
- lines.push("export const env = createEnv({");
466
+ const lines = [
467
+ "// Generated by env-typegen \u2014 do not edit manually",
468
+ 'import { createEnv } from "@t3-oss/env-nextjs";',
469
+ 'import { z } from "zod";',
470
+ "",
471
+ "export const env = createEnv({"
472
+ ];
438
473
  if (serverVars.length > 0) {
439
- lines.push(" server: {");
440
- for (const variable of serverVars) {
441
- const effectiveType = variable.annotatedType ?? variable.inferredType;
442
- let zodExpr = toT3ZodType(effectiveType);
443
- if (variable.description !== void 0) {
444
- zodExpr += `.describe("${variable.description.replace(/"/g, '\\"')}")`;
445
- }
446
- if (variable.isOptional) {
447
- zodExpr += ".optional()";
448
- }
449
- lines.push(` ${variable.key}: ${zodExpr},`);
450
- }
451
- lines.push(" },");
474
+ lines.push(
475
+ " server: {",
476
+ ...serverVars.map((v) => ` ${v.key}: ${buildZodExpr(v)},`),
477
+ " },"
478
+ );
452
479
  }
453
480
  if (clientVars.length > 0) {
454
- lines.push(" client: {");
455
- for (const variable of clientVars) {
456
- const effectiveType = variable.annotatedType ?? variable.inferredType;
457
- let zodExpr = toT3ZodType(effectiveType);
458
- if (variable.description !== void 0) {
459
- zodExpr += `.describe("${variable.description.replace(/"/g, '\\"')}")`;
460
- }
461
- if (variable.isOptional) {
462
- zodExpr += ".optional()";
463
- }
464
- lines.push(` ${variable.key}: ${zodExpr},`);
465
- }
466
- lines.push(" },");
467
- }
468
- lines.push(" runtimeEnv: {");
469
- for (const variable of parsed.vars) {
470
- lines.push(` ${variable.key}: process.env.${variable.key},`);
481
+ lines.push(
482
+ " client: {",
483
+ ...clientVars.map((v) => ` ${v.key}: ${buildZodExpr(v)},`),
484
+ " },"
485
+ );
471
486
  }
472
- lines.push(" },");
473
- lines.push("});");
487
+ lines.push(
488
+ " runtimeEnv: {",
489
+ ...parsed.vars.map((v) => ` ${v.key}: process.env.${v.key},`),
490
+ " },",
491
+ "});"
492
+ );
474
493
  return lines.join("\n") + "\n";
475
494
  }
476
495
  var CONTRACT_FILE_NAMES = [
@@ -493,9 +512,9 @@ async function loadContract(cwd = process.cwd()) {
493
512
  return void 0;
494
513
  }
495
514
  var CONFIG_FILE_NAMES = [
496
- "env-typegen.config.ts",
497
515
  "env-typegen.config.mjs",
498
- "env-typegen.config.js"
516
+ "env-typegen.config.js",
517
+ "env-typegen.config.ts"
499
518
  ];
500
519
  function defineConfig(config) {
501
520
  return config;
@@ -504,6 +523,19 @@ async function loadConfig(cwd = process.cwd()) {
504
523
  for (const name of CONFIG_FILE_NAMES) {
505
524
  const filePath = path6__default.default.resolve(cwd, name);
506
525
  if (fs.existsSync(filePath)) {
526
+ if (filePath.endsWith(".ts")) {
527
+ throw new Error(
528
+ `Config file "${name}" was found but TypeScript files cannot be loaded directly at runtime.
529
+ Rename it to "env-typegen.config.mjs" and use ESM export syntax:
530
+
531
+ // env-typegen.config.mjs
532
+ import { defineConfig } from "@xlameiro/env-typegen";
533
+ export default defineConfig({ input: ".env.example" });
534
+
535
+ Tip: keep env-typegen.config.ts for IDE autocompletion and create a sibling
536
+ env-typegen.config.mjs for runtime loading.`
537
+ );
538
+ }
507
539
  const fileUrl = url.pathToFileURL(filePath).href;
508
540
  const mod = await import(fileUrl);
509
541
  return mod.default;
@@ -512,7 +544,15 @@ async function loadConfig(cwd = process.cwd()) {
512
544
  return void 0;
513
545
  }
514
546
  async function readEnvFile(filePath) {
515
- return promises.readFile(path6__default.default.resolve(filePath), "utf8");
547
+ const resolved = path6__default.default.resolve(filePath);
548
+ try {
549
+ return await promises.readFile(resolved, "utf8");
550
+ } catch (err) {
551
+ if (err instanceof Error && err.code === "ENOENT") {
552
+ throw new Error(`File not found: ${filePath}`);
553
+ }
554
+ throw err;
555
+ }
516
556
  }
517
557
  async function writeOutput(filePath, content) {
518
558
  const resolved = path6__default.default.resolve(filePath);
@@ -530,17 +570,18 @@ async function formatOutput(content, parser = "typescript") {
530
570
  return content;
531
571
  }
532
572
  }
573
+ var { green, red, yellow } = pc__default.default;
533
574
  function log(message) {
534
575
  console.log(message);
535
576
  }
536
577
  function warn(message) {
537
- console.warn(picocolors.yellow(`\u26A0 ${message}`));
578
+ console.warn(yellow(`\u26A0 ${message}`));
538
579
  }
539
580
  function error(message) {
540
- console.error(picocolors.red(`\u2716 ${message}`));
581
+ console.error(red(`\u2716 ${message}`));
541
582
  }
542
583
  function success(message) {
543
- console.log(picocolors.green(`\u2714 ${message}`));
584
+ console.log(green(`\u2714 ${message}`));
544
585
  }
545
586
 
546
587
  // src/pipeline.ts
@@ -583,10 +624,6 @@ async function persistOutput(params) {
583
624
  }
584
625
  if (dryRun) {
585
626
  if (!silent) {
586
- if (!isSingle) {
587
- console.log(`// --- ${generator}: ${outputPath} ---`);
588
- }
589
- console.log(generated);
590
627
  success(`Dry run: would write ${outputPath}`);
591
628
  }
592
629
  return;
@@ -616,7 +653,7 @@ async function runGenerate(options) {
616
653
  const parsed = parseEnvFileContent(
617
654
  content,
618
655
  inputPath,
619
- inferenceRules2 !== void 0 ? { inferenceRules: inferenceRules2 } : void 0
656
+ inferenceRules2 === void 0 ? void 0 : { inferenceRules: inferenceRules2 }
620
657
  );
621
658
  for (const generator of generators) {
622
659
  let generated = buildOutput(generator, parsed);
@@ -944,8 +981,13 @@ function readEntryValue(entry, keys) {
944
981
  }
945
982
  return void 0;
946
983
  }
984
+ function getVercelEntries(value) {
985
+ if (Array.isArray(value)) return value;
986
+ if (isRecord(value) && Array.isArray(value.envs)) return value.envs;
987
+ return [];
988
+ }
947
989
  function parseVercelPayload(value) {
948
- const entries = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value.envs) ? value.envs : [];
990
+ const entries = getVercelEntries(value);
949
991
  const result = {};
950
992
  for (const entry of entries) {
951
993
  if (!isRecord(entry)) continue;
@@ -956,8 +998,13 @@ function parseVercelPayload(value) {
956
998
  }
957
999
  return result;
958
1000
  }
1001
+ function getCloudflareEntries(value) {
1002
+ if (Array.isArray(value)) return value;
1003
+ if (isRecord(value) && Array.isArray(value.result)) return value.result;
1004
+ return [];
1005
+ }
959
1006
  function parseCloudflarePayload(value) {
960
- const entries = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value.result) ? value.result : [];
1007
+ const entries = getCloudflareEntries(value);
961
1008
  const result = {};
962
1009
  for (const entry of entries) {
963
1010
  if (!isRecord(entry)) continue;
@@ -975,7 +1022,7 @@ function parseAwsPayload(value) {
975
1022
  if (!isRecord(entry)) continue;
976
1023
  const name = readEntryValue(entry, ["Name", "name"]);
977
1024
  if (name === void 0) continue;
978
- const key = name.split("/").filter((part) => part.length > 0).pop() ?? name;
1025
+ const key = name.split("/").findLast((part) => part.length > 0) ?? name;
979
1026
  const envValue = readEntryValue(entry, ["Value", "value"]) ?? "";
980
1027
  result[key] = envValue;
981
1028
  }
@@ -988,7 +1035,15 @@ function parseProviderPayload(provider, value) {
988
1035
  }
989
1036
  async function loadCloudSource(options) {
990
1037
  const resolvedPath = path6__default.default.resolve(options.filePath);
991
- const raw = await promises.readFile(resolvedPath, "utf8");
1038
+ let raw;
1039
+ try {
1040
+ raw = await promises.readFile(resolvedPath, "utf8");
1041
+ } catch (err) {
1042
+ if (err instanceof Error && err.code === "ENOENT") {
1043
+ throw new Error(`File not found: ${options.filePath}`);
1044
+ }
1045
+ throw err;
1046
+ }
992
1047
  const parsed = JSON.parse(raw);
993
1048
  return parseProviderPayload(options.provider, parsed);
994
1049
  }
@@ -1115,7 +1170,7 @@ function findDefaultContractPath(cwd) {
1115
1170
  async function loadValidationContract(options) {
1116
1171
  const { fallbackExamplePath, contractPath, cwd = process.cwd() } = options;
1117
1172
  const discoveredContractPath = findDefaultContractPath(cwd);
1118
- const resolvedContractPath = contractPath !== void 0 ? path6__default.default.resolve(cwd, contractPath) : discoveredContractPath;
1173
+ const resolvedContractPath = contractPath === void 0 ? discoveredContractPath : path6__default.default.resolve(cwd, contractPath);
1119
1174
  if (resolvedContractPath !== void 0 && fs.existsSync(resolvedContractPath)) {
1120
1175
  const moduleUrl = url.pathToFileURL(resolvedContractPath).href;
1121
1176
  const moduleValue = await import(moduleUrl);
@@ -1151,10 +1206,20 @@ function isPlugin(value) {
1151
1206
  }
1152
1207
  async function loadPluginFromPath(pluginPath, cwd) {
1153
1208
  const resolvedPath = path6__default.default.resolve(cwd, pluginPath);
1209
+ if (!fs.existsSync(resolvedPath)) {
1210
+ throw new Error(`Plugin not found: ${pluginPath}`);
1211
+ }
1154
1212
  const moduleValue = await import(url.pathToFileURL(resolvedPath).href);
1155
1213
  const candidate = moduleValue.default ?? moduleValue.plugin;
1156
1214
  if (isPlugin(candidate)) return candidate;
1157
- throw new Error(`Invalid plugin at ${resolvedPath}. Expected a plugin object export.`);
1215
+ throw new Error(
1216
+ `Invalid plugin at ${resolvedPath}.
1217
+ Expected a default export matching:
1218
+ { name: string,
1219
+ transformSource?(ctx: { environment: string; values: Record<string, string> }): Record<string, string>,
1220
+ transformReport?(report: ValidationReport): ValidationReport,
1221
+ transformContract?(contract: EnvContract): EnvContract }`
1222
+ );
1158
1223
  }
1159
1224
  async function loadPlugins(options) {
1160
1225
  const cwd = options.cwd ?? process.cwd();
@@ -1199,8 +1264,8 @@ function applyReportPlugins(report, plugins) {
1199
1264
  }
1200
1265
 
1201
1266
  // src/validation/engine.ts
1202
- var EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
1203
- var SEMVER_RE2 = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
1267
+ var EMAIL_RE = /^[^@\s]+@[^@\s.]+(?:\.[^@\s.]+)+$/;
1268
+ var SEMVER_RE2 = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
1204
1269
  function detectReceivedType(value) {
1205
1270
  const normalized = value.trim();
1206
1271
  if (normalized.length === 0) return "unknown";
@@ -1220,65 +1285,72 @@ function detectReceivedType(value) {
1220
1285
  }
1221
1286
  return "string";
1222
1287
  }
1223
- function validateValueAgainstExpected(expected, rawValue) {
1224
- const normalized = rawValue.trim();
1225
- const receivedType = detectReceivedType(normalized);
1226
- if (expected.type === "unknown") return { isValid: true, receivedType };
1227
- if (expected.type === "string") return { isValid: true, receivedType };
1228
- if (expected.type === "number") {
1229
- const parsed = Number(normalized);
1230
- if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
1231
- return { isValid: false, receivedType, issueType: "invalid_type" };
1232
- }
1233
- if (expected.min !== void 0 && parsed < expected.min) {
1234
- return { isValid: false, receivedType, issueType: "invalid_value" };
1235
- }
1236
- if (expected.max !== void 0 && parsed > expected.max) {
1237
- return { isValid: false, receivedType, issueType: "invalid_value" };
1238
- }
1239
- return { isValid: true, receivedType };
1288
+ function validateNumber(expected, normalized, receivedType) {
1289
+ const parsed = Number(normalized);
1290
+ if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
1291
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1240
1292
  }
1241
- if (expected.type === "boolean") {
1242
- if (!["true", "false", "1", "0", "yes", "no"].includes(normalized.toLowerCase())) {
1243
- return { isValid: false, receivedType, issueType: "invalid_type" };
1244
- }
1245
- return { isValid: true, receivedType };
1293
+ if (expected.min !== void 0 && parsed < expected.min) {
1294
+ return { isValid: false, receivedType, issueType: "invalid_value" };
1246
1295
  }
1247
- if (expected.type === "enum") {
1248
- if (!expected.values.includes(normalized)) {
1249
- return { isValid: false, receivedType, issueType: "invalid_value" };
1250
- }
1251
- return { isValid: true, receivedType };
1296
+ if (expected.max !== void 0 && parsed > expected.max) {
1297
+ return { isValid: false, receivedType, issueType: "invalid_value" };
1252
1298
  }
1253
- if (expected.type === "url") {
1254
- try {
1255
- const value = new URL(normalized);
1256
- if (value.protocol.length === 0)
1257
- return { isValid: false, receivedType, issueType: "invalid_type" };
1258
- return { isValid: true, receivedType };
1259
- } catch {
1260
- return { isValid: false, receivedType, issueType: "invalid_type" };
1261
- }
1299
+ return { isValid: true, receivedType };
1300
+ }
1301
+ function validateBoolean(normalized, receivedType) {
1302
+ if (!["true", "false", "1", "0", "yes", "no"].includes(normalized.toLowerCase())) {
1303
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1262
1304
  }
1263
- if (expected.type === "email") {
1264
- if (!EMAIL_RE.test(normalized))
1305
+ return { isValid: true, receivedType };
1306
+ }
1307
+ function validateEnum(expected, normalized, receivedType) {
1308
+ if (!expected.values.includes(normalized)) {
1309
+ return { isValid: false, receivedType, issueType: "invalid_value" };
1310
+ }
1311
+ return { isValid: true, receivedType };
1312
+ }
1313
+ function validateUrl(normalized, receivedType) {
1314
+ try {
1315
+ const value = new URL(normalized);
1316
+ if (value.protocol.length === 0)
1265
1317
  return { isValid: false, receivedType, issueType: "invalid_type" };
1266
1318
  return { isValid: true, receivedType };
1319
+ } catch {
1320
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1267
1321
  }
1268
- if (expected.type === "json") {
1269
- try {
1270
- const parsed = JSON.parse(normalized);
1271
- if (typeof parsed === "object" && parsed !== null) return { isValid: true, receivedType };
1272
- return { isValid: false, receivedType, issueType: "invalid_type" };
1273
- } catch {
1274
- return { isValid: false, receivedType, issueType: "invalid_type" };
1275
- }
1322
+ }
1323
+ function validateEmail(normalized, receivedType) {
1324
+ if (!EMAIL_RE.test(normalized))
1325
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1326
+ return { isValid: true, receivedType };
1327
+ }
1328
+ function validateJson(normalized, receivedType) {
1329
+ try {
1330
+ const parsed = JSON.parse(normalized);
1331
+ if (typeof parsed === "object" && parsed !== null) return { isValid: true, receivedType };
1332
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1333
+ } catch {
1334
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1276
1335
  }
1277
- if (expected.type === "semver") {
1278
- if (!SEMVER_RE2.test(normalized))
1279
- return { isValid: false, receivedType, issueType: "invalid_value" };
1336
+ }
1337
+ function validateSemver(normalized, receivedType) {
1338
+ if (!SEMVER_RE2.test(normalized))
1339
+ return { isValid: false, receivedType, issueType: "invalid_value" };
1340
+ return { isValid: true, receivedType };
1341
+ }
1342
+ function validateValueAgainstExpected(expected, rawValue) {
1343
+ const normalized = rawValue.trim();
1344
+ const receivedType = detectReceivedType(normalized);
1345
+ if (expected.type === "unknown" || expected.type === "string")
1280
1346
  return { isValid: true, receivedType };
1281
- }
1347
+ if (expected.type === "number") return validateNumber(expected, normalized, receivedType);
1348
+ if (expected.type === "boolean") return validateBoolean(normalized, receivedType);
1349
+ if (expected.type === "enum") return validateEnum(expected, normalized, receivedType);
1350
+ if (expected.type === "url") return validateUrl(normalized, receivedType);
1351
+ if (expected.type === "email") return validateEmail(normalized, receivedType);
1352
+ if (expected.type === "json") return validateJson(normalized, receivedType);
1353
+ if (expected.type === "semver") return validateSemver(normalized, receivedType);
1282
1354
  return { isValid: true, receivedType };
1283
1355
  }
1284
1356
  function toIssueCode(issueType) {
@@ -1350,57 +1422,62 @@ function buildReport(env, issues, recommendations) {
1350
1422
  function isClientSecret(variable, key) {
1351
1423
  return variable.secret === true && (variable.clientSide || key.startsWith("NEXT_PUBLIC_"));
1352
1424
  }
1425
+ function checkContractVariable(key, variable, context) {
1426
+ const { options, issues } = context;
1427
+ const value = options.values[key];
1428
+ const hasValue = value !== void 0;
1429
+ if (variable.required && !hasValue) {
1430
+ issues.push(
1431
+ createIssue({
1432
+ type: "missing",
1433
+ severity: "error",
1434
+ key,
1435
+ environment: options.environment,
1436
+ message: `Required variable ${key} is missing.`,
1437
+ debugValues: options.debugValues,
1438
+ expected: variable.expected
1439
+ })
1440
+ );
1441
+ return;
1442
+ }
1443
+ if (!hasValue) return;
1444
+ const validation = validateValueAgainstExpected(variable.expected, value);
1445
+ if (!validation.isValid) {
1446
+ const message = validation.issueType === "invalid_type" ? `Variable ${key} has invalid type.` : `Variable ${key} has invalid value.`;
1447
+ issues.push(
1448
+ createIssue({
1449
+ type: validation.issueType,
1450
+ severity: "error",
1451
+ key,
1452
+ environment: options.environment,
1453
+ message,
1454
+ value,
1455
+ debugValues: options.debugValues,
1456
+ expected: variable.expected,
1457
+ receivedType: validation.receivedType
1458
+ })
1459
+ );
1460
+ }
1461
+ if (isClientSecret(variable, key)) {
1462
+ issues.push(
1463
+ createIssue({
1464
+ type: "secret_exposed",
1465
+ severity: "error",
1466
+ key,
1467
+ environment: options.environment,
1468
+ message: `Secret variable ${key} is marked as client-side.`,
1469
+ value,
1470
+ debugValues: options.debugValues,
1471
+ expected: variable.expected
1472
+ })
1473
+ );
1474
+ }
1475
+ }
1353
1476
  function validateAgainstContract(options) {
1354
1477
  const issues = [];
1355
1478
  const contractKeys = new Set(Object.keys(options.contract.variables));
1356
1479
  for (const [key, variable] of Object.entries(options.contract.variables)) {
1357
- const value = options.values[key];
1358
- const hasValue = value !== void 0 && value.trim().length > 0;
1359
- if (variable.required && !hasValue) {
1360
- issues.push(
1361
- createIssue({
1362
- type: "missing",
1363
- severity: "error",
1364
- key,
1365
- environment: options.environment,
1366
- message: `Required variable ${key} is missing.`,
1367
- debugValues: options.debugValues,
1368
- expected: variable.expected
1369
- })
1370
- );
1371
- continue;
1372
- }
1373
- if (!hasValue) continue;
1374
- const validation = validateValueAgainstExpected(variable.expected, value);
1375
- if (!validation.isValid) {
1376
- issues.push(
1377
- createIssue({
1378
- type: validation.issueType,
1379
- severity: "error",
1380
- key,
1381
- environment: options.environment,
1382
- message: validation.issueType === "invalid_type" ? `Variable ${key} has invalid type.` : `Variable ${key} has invalid value.`,
1383
- value,
1384
- debugValues: options.debugValues,
1385
- expected: variable.expected,
1386
- receivedType: validation.receivedType
1387
- })
1388
- );
1389
- }
1390
- if (isClientSecret(variable, key)) {
1391
- issues.push(
1392
- createIssue({
1393
- type: "secret_exposed",
1394
- severity: "error",
1395
- key,
1396
- environment: options.environment,
1397
- message: `Secret variable ${key} is marked as client-side.`,
1398
- value,
1399
- debugValues: options.debugValues,
1400
- expected: variable.expected
1401
- })
1402
- );
1403
- }
1480
+ checkContractVariable(key, variable, { options, issues });
1404
1481
  }
1405
1482
  for (const [key, value] of Object.entries(options.values)) {
1406
1483
  if (contractKeys.has(key)) continue;
@@ -1428,6 +1505,97 @@ function collectUnionKeys(contract, sources) {
1428
1505
  }
1429
1506
  return union;
1430
1507
  }
1508
+ function diffMissingEntries(key, missing, context) {
1509
+ const { variable, options, issues } = context;
1510
+ for (const entry of missing) {
1511
+ issues.push(
1512
+ createIssue({
1513
+ type: "missing",
1514
+ severity: "error",
1515
+ key,
1516
+ environment: entry.sourceName,
1517
+ message: `Variable ${key} is missing in ${entry.sourceName}.`,
1518
+ debugValues: options.debugValues,
1519
+ ...variable !== void 0 && { expected: variable.expected }
1520
+ })
1521
+ );
1522
+ }
1523
+ }
1524
+ function diffTypeConflicts(key, present, context) {
1525
+ const { variable, options, issues } = context;
1526
+ const typeBySource = /* @__PURE__ */ new Map();
1527
+ for (const entry of present) {
1528
+ typeBySource.set(entry.sourceName, detectReceivedType(entry.value ?? ""));
1529
+ }
1530
+ if (new Set(typeBySource.values()).size <= 1) return;
1531
+ for (const [sourceName, detectedType] of typeBySource.entries()) {
1532
+ issues.push(
1533
+ createIssue({
1534
+ type: "conflict",
1535
+ severity: "error",
1536
+ key,
1537
+ environment: sourceName,
1538
+ message: `Variable ${key} has conflicting inferred type across environments.`,
1539
+ debugValues: options.debugValues,
1540
+ receivedType: detectedType,
1541
+ ...options.sources[sourceName]?.[key] !== void 0 && {
1542
+ value: options.sources[sourceName]?.[key]
1543
+ },
1544
+ ...variable !== void 0 && { expected: variable.expected }
1545
+ })
1546
+ );
1547
+ }
1548
+ }
1549
+ function diffPresentEntry(key, entry, context) {
1550
+ const { variable, options, issues } = context;
1551
+ if (entry.value === void 0) return;
1552
+ if (variable === void 0) {
1553
+ const severity = options.strict ? "error" : "warning";
1554
+ issues.push(
1555
+ createIssue({
1556
+ type: "extra",
1557
+ severity,
1558
+ key,
1559
+ environment: entry.sourceName,
1560
+ message: `Variable ${key} is not defined in the contract.`,
1561
+ value: entry.value,
1562
+ debugValues: options.debugValues
1563
+ })
1564
+ );
1565
+ return;
1566
+ }
1567
+ const validation = validateValueAgainstExpected(variable.expected, entry.value);
1568
+ if (!validation.isValid) {
1569
+ const message = validation.issueType === "invalid_type" ? `Variable ${key} has invalid type in ${entry.sourceName}.` : `Variable ${key} has invalid value in ${entry.sourceName}.`;
1570
+ issues.push(
1571
+ createIssue({
1572
+ type: validation.issueType,
1573
+ severity: "error",
1574
+ key,
1575
+ environment: entry.sourceName,
1576
+ message,
1577
+ value: entry.value,
1578
+ debugValues: options.debugValues,
1579
+ expected: variable.expected,
1580
+ receivedType: validation.receivedType
1581
+ })
1582
+ );
1583
+ }
1584
+ if (isClientSecret(variable, key)) {
1585
+ issues.push(
1586
+ createIssue({
1587
+ type: "secret_exposed",
1588
+ severity: "error",
1589
+ key,
1590
+ environment: entry.sourceName,
1591
+ message: `Secret variable ${key} is marked as client-side.`,
1592
+ value: entry.value,
1593
+ debugValues: options.debugValues,
1594
+ expected: variable.expected
1595
+ })
1596
+ );
1597
+ }
1598
+ }
1431
1599
  function diffEnvironmentSources(options) {
1432
1600
  const issues = [];
1433
1601
  const sourceNames = Object.keys(options.sources);
@@ -1438,12 +1606,8 @@ function diffEnvironmentSources(options) {
1438
1606
  sourceName,
1439
1607
  value: options.sources[sourceName]?.[key]
1440
1608
  }));
1441
- const present = valuesBySource.filter(
1442
- (entry) => entry.value !== void 0 && entry.value !== ""
1443
- );
1444
- const missing = valuesBySource.filter(
1445
- (entry) => entry.value === void 0 || entry.value === ""
1446
- );
1609
+ const present = valuesBySource.filter((entry) => entry.value !== void 0);
1610
+ const missing = valuesBySource.filter((entry) => entry.value === void 0);
1447
1611
  if (present.length === 0 && variable?.required === true) {
1448
1612
  for (const entry of missing) {
1449
1613
  issues.push(
@@ -1460,92 +1624,13 @@ function diffEnvironmentSources(options) {
1460
1624
  }
1461
1625
  continue;
1462
1626
  }
1627
+ const ctx = { variable, options, issues };
1463
1628
  if (present.length > 0) {
1464
- for (const entry of missing) {
1465
- issues.push(
1466
- createIssue({
1467
- type: "missing",
1468
- severity: "error",
1469
- key,
1470
- environment: entry.sourceName,
1471
- message: `Variable ${key} is missing in ${entry.sourceName}.`,
1472
- debugValues: options.debugValues,
1473
- ...variable !== void 0 && { expected: variable.expected }
1474
- })
1475
- );
1476
- }
1629
+ diffMissingEntries(key, missing, ctx);
1477
1630
  }
1478
- const typeBySource = /* @__PURE__ */ new Map();
1631
+ diffTypeConflicts(key, present, ctx);
1479
1632
  for (const entry of present) {
1480
- const detected = detectReceivedType(entry.value ?? "");
1481
- typeBySource.set(entry.sourceName, detected);
1482
- }
1483
- if (new Set(typeBySource.values()).size > 1) {
1484
- for (const [sourceName, detectedType] of typeBySource.entries()) {
1485
- issues.push(
1486
- createIssue({
1487
- type: "conflict",
1488
- severity: "error",
1489
- key,
1490
- environment: sourceName,
1491
- message: `Variable ${key} has conflicting inferred type across environments.`,
1492
- debugValues: options.debugValues,
1493
- receivedType: detectedType,
1494
- ...options.sources[sourceName]?.[key] !== void 0 && {
1495
- value: options.sources[sourceName]?.[key]
1496
- },
1497
- ...variable !== void 0 && { expected: variable.expected }
1498
- })
1499
- );
1500
- }
1501
- }
1502
- for (const entry of present) {
1503
- if (entry.value === void 0) continue;
1504
- if (variable === void 0) {
1505
- const severity = options.strict ? "error" : "warning";
1506
- issues.push(
1507
- createIssue({
1508
- type: "extra",
1509
- severity,
1510
- key,
1511
- environment: entry.sourceName,
1512
- message: `Variable ${key} is not defined in the contract.`,
1513
- value: entry.value,
1514
- debugValues: options.debugValues
1515
- })
1516
- );
1517
- continue;
1518
- }
1519
- const validation = validateValueAgainstExpected(variable.expected, entry.value);
1520
- if (!validation.isValid) {
1521
- issues.push(
1522
- createIssue({
1523
- type: validation.issueType,
1524
- severity: "error",
1525
- key,
1526
- environment: entry.sourceName,
1527
- message: validation.issueType === "invalid_type" ? `Variable ${key} has invalid type in ${entry.sourceName}.` : `Variable ${key} has invalid value in ${entry.sourceName}.`,
1528
- value: entry.value,
1529
- debugValues: options.debugValues,
1530
- expected: variable.expected,
1531
- receivedType: validation.receivedType
1532
- })
1533
- );
1534
- }
1535
- if (isClientSecret(variable, key)) {
1536
- issues.push(
1537
- createIssue({
1538
- type: "secret_exposed",
1539
- severity: "error",
1540
- key,
1541
- environment: entry.sourceName,
1542
- message: `Secret variable ${key} is marked as client-side.`,
1543
- value: entry.value,
1544
- debugValues: options.debugValues,
1545
- expected: variable.expected
1546
- })
1547
- );
1548
- }
1633
+ diffPresentEntry(key, entry, ctx);
1549
1634
  }
1550
1635
  }
1551
1636
  return buildReport("diff", issues);
@@ -1594,7 +1679,7 @@ function parseEnvSourceContent(content) {
1594
1679
  for (const line of lines) {
1595
1680
  const trimmed = line.trim();
1596
1681
  if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
1597
- const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(trimmed);
1682
+ const match = /^(?:export\s+)?([A-Za-z_]\w*)=(.*)$/.exec(trimmed);
1598
1683
  if (match === null) continue;
1599
1684
  const key = match[1] ?? "";
1600
1685
  const rawValue = match[2] ?? "";
@@ -1607,11 +1692,11 @@ async function loadEnvSource(options) {
1607
1692
  try {
1608
1693
  const content = await promises.readFile(resolvedPath, "utf8");
1609
1694
  return parseEnvSourceContent(content);
1610
- } catch (errorValue) {
1611
- if (options.allowMissing === true && errorValue instanceof Error && "code" in errorValue && errorValue.code === "ENOENT") {
1695
+ } catch (error_) {
1696
+ if (options.allowMissing === true && error_ instanceof Error && "code" in error_ && error_.code === "ENOENT") {
1612
1697
  return {};
1613
1698
  }
1614
- throw errorValue;
1699
+ throw error_;
1615
1700
  }
1616
1701
  }
1617
1702
  function toJsonString(report, mode) {
@@ -1621,8 +1706,8 @@ function toJsonString(report, mode) {
1621
1706
  `;
1622
1707
  }
1623
1708
  function formatIssue(issue) {
1624
- const expected = issue.expected !== void 0 ? ` expected=${issue.expected.type}` : "";
1625
- const received = issue.receivedType !== void 0 ? ` received=${issue.receivedType}` : "";
1709
+ const expected = issue.expected === void 0 ? "" : ` expected=${issue.expected.type}`;
1710
+ const received = issue.receivedType === void 0 ? "" : ` received=${issue.receivedType}`;
1626
1711
  return `${issue.severity.toUpperCase()} [${issue.code}] ${issue.environment}:${issue.key} ${issue.message}${expected}${received}`;
1627
1712
  }
1628
1713
  function formatHumanReport(report) {
@@ -1631,15 +1716,13 @@ function formatHumanReport(report) {
1631
1716
  `Status: ${report.status.toUpperCase()} (errors=${report.summary.errors}, warnings=${report.summary.warnings}, total=${report.summary.total})`
1632
1717
  );
1633
1718
  if (report.issues.length > 0) {
1634
- lines.push("");
1635
- lines.push("Issues:");
1719
+ lines.push("", "Issues:");
1636
1720
  for (const issue of report.issues) {
1637
1721
  lines.push(`- ${formatIssue(issue)}`);
1638
1722
  }
1639
1723
  }
1640
1724
  if (report.recommendations !== void 0 && report.recommendations.length > 0) {
1641
- lines.push("");
1642
- lines.push("Recommendations:");
1725
+ lines.push("", "Recommendations:");
1643
1726
  for (const recommendation of report.recommendations) {
1644
1727
  lines.push(`- ${recommendation}`);
1645
1728
  }
@@ -1684,7 +1767,11 @@ var HELP_TEXT = {
1684
1767
  " --cloud-file <path> Cloud snapshot JSON file",
1685
1768
  " --plugin <path> Plugin module path (repeatable)",
1686
1769
  " -c, --config <path> Config file path",
1687
- " -h, --help Show this help"
1770
+ " -h, --help Show this help",
1771
+ "",
1772
+ "Exit codes:",
1773
+ " 0 All checks passed (status: ok or warn)",
1774
+ " 1 One or more checks failed (status: fail) or invalid usage"
1688
1775
  ].join("\n"),
1689
1776
  diff: [
1690
1777
  "Usage: env-typegen diff [options]",
@@ -1703,7 +1790,11 @@ var HELP_TEXT = {
1703
1790
  " --cloud-file <path> Cloud snapshot JSON file added to diff sources",
1704
1791
  " --plugin <path> Plugin module path (repeatable)",
1705
1792
  " -c, --config <path> Config file path",
1706
- " -h, --help Show this help"
1793
+ " -h, --help Show this help",
1794
+ "",
1795
+ "Exit codes:",
1796
+ " 0 All checks passed (status: ok or warn)",
1797
+ " 1 One or more checks failed (status: fail) or invalid usage"
1707
1798
  ].join("\n"),
1708
1799
  doctor: [
1709
1800
  "Usage: env-typegen doctor [options]",
@@ -1723,7 +1814,11 @@ var HELP_TEXT = {
1723
1814
  " --cloud-file <path> Cloud snapshot JSON file",
1724
1815
  " --plugin <path> Plugin module path (repeatable)",
1725
1816
  " -c, --config <path> Config file path",
1726
- " -h, --help Show this help"
1817
+ " -h, --help Show this help",
1818
+ "",
1819
+ "Exit codes:",
1820
+ " 0 All checks passed (status: ok or warn)",
1821
+ " 1 One or more checks failed (status: fail) or invalid usage"
1727
1822
  ].join("\n")
1728
1823
  };
1729
1824
  function resolveConfigRelative(value, configDir) {
@@ -1765,6 +1860,9 @@ async function loadCommandConfig(configPath) {
1765
1860
  return loadConfig(process.cwd());
1766
1861
  }
1767
1862
  const resolvedPath = path6__default.default.resolve(configPath);
1863
+ if (!fs.existsSync(resolvedPath)) {
1864
+ throw new Error(`Config file not found: ${configPath}`);
1865
+ }
1768
1866
  const configDir = path6__default.default.dirname(resolvedPath);
1769
1867
  const moduleValue = await import(url.pathToFileURL(resolvedPath).href);
1770
1868
  if (moduleValue.default === void 0) return void 0;
@@ -1810,7 +1908,10 @@ function parseValidationArgs(argv) {
1810
1908
  }
1811
1909
  });
1812
1910
  const castValues = values;
1813
- const jsonMode = castValues.json === true ? assignedMode === "off" ? "compact" : assignedMode : "off";
1911
+ let jsonMode = "off";
1912
+ if (castValues.json === true) {
1913
+ jsonMode = assignedMode === "off" ? "compact" : assignedMode;
1914
+ }
1814
1915
  return { values: castValues, jsonMode };
1815
1916
  }
1816
1917
  function resolveStrict(values, fileConfig) {
@@ -1883,12 +1984,12 @@ async function runCheckCommand(args) {
1883
1984
  const provider = context.cloudProvider;
1884
1985
  let environment = args.values.env?.[0] ?? ".env";
1885
1986
  let sourceValues;
1886
- if (provider !== void 0) {
1987
+ if (provider === void 0) {
1988
+ sourceValues = await loadEnvSource({ filePath: environment, allowMissing: true });
1989
+ } else {
1887
1990
  const cloudFile = context.cloudFile ?? `${provider}.env.json`;
1888
1991
  sourceValues = await loadCloudSource({ provider, filePath: cloudFile });
1889
1992
  environment = `cloud:${provider}`;
1890
- } else {
1891
- sourceValues = await loadEnvSource({ filePath: environment, allowMissing: true });
1892
1993
  }
1893
1994
  sourceValues = applySourcePlugins({ environment, values: sourceValues }, context.plugins);
1894
1995
  const report = applyReportPlugins(