@xlameiro/env-typegen 0.1.1 → 0.1.3

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
@@ -100,9 +100,10 @@ var require_picocolors = __commonJS({
100
100
 
101
101
  // src/cli.ts
102
102
  import { realpathSync } from "fs";
103
- import path6 from "path";
104
- import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "url";
105
- import { inspect, parseArgs } from "util";
103
+ import { createRequire } from "module";
104
+ import path13 from "path";
105
+ import { fileURLToPath, pathToFileURL as pathToFileURL5 } from "url";
106
+ import { inspect, parseArgs as parseArgs2 } from "util";
106
107
 
107
108
  // src/config.ts
108
109
  import { existsSync } from "fs";
@@ -180,7 +181,7 @@ function generateT3Env(parsed) {
180
181
  const effectiveType = variable.annotatedType ?? variable.inferredType;
181
182
  let zodExpr = toT3ZodType(effectiveType);
182
183
  if (variable.description !== void 0) {
183
- zodExpr += `.describe("${variable.description}")`;
184
+ zodExpr += `.describe("${variable.description.replace(/"/g, '\\"')}")`;
184
185
  }
185
186
  if (variable.isOptional) {
186
187
  zodExpr += ".optional()";
@@ -195,7 +196,7 @@ function generateT3Env(parsed) {
195
196
  const effectiveType = variable.annotatedType ?? variable.inferredType;
196
197
  let zodExpr = toT3ZodType(effectiveType);
197
198
  if (variable.description !== void 0) {
198
- zodExpr += `.describe("${variable.description}")`;
199
+ zodExpr += `.describe("${variable.description.replace(/"/g, '\\"')}")`;
199
200
  }
200
201
  if (variable.isOptional) {
201
202
  zodExpr += ".optional()";
@@ -397,7 +398,9 @@ var inferenceRules = [
397
398
  // src/inferrer/type-inferrer.ts
398
399
  var sortedRules = [...inferenceRules].sort((left, right) => left.priority - right.priority);
399
400
  function inferType(key, value, options) {
400
- for (const rule of sortedRules) {
401
+ const extra = options?.extraRules;
402
+ const rules = extra && extra.length > 0 ? [...extra].sort((a, b) => a.priority - b.priority).concat(sortedRules) : sortedRules;
403
+ for (const rule of rules) {
401
404
  if (rule.match(key, value)) {
402
405
  return rule.type;
403
406
  }
@@ -419,36 +422,80 @@ var VALID_ENV_VAR_TYPES = /* @__PURE__ */ new Set([
419
422
  function isEnvVarType(value) {
420
423
  return VALID_ENV_VAR_TYPES.has(value);
421
424
  }
425
+ function applyTypeAnnotation(state, content) {
426
+ const typeStr = content.slice("@type ".length).trim();
427
+ if (isEnvVarType(typeStr)) state.annotatedType = typeStr;
428
+ }
429
+ function applyEnumAnnotation(state, content) {
430
+ const values = content.slice("@enum ".length).trim().split(",").map((v) => v.trim()).filter((v) => v.length > 0);
431
+ if (values.length > 0) state.enumValues = values;
432
+ }
433
+ function applyMinAnnotation(state, content) {
434
+ const num = Number(content.slice("@min ".length).trim());
435
+ if (Number.isFinite(num)) state.minConstraint = num;
436
+ }
437
+ function applyMaxAnnotation(state, content) {
438
+ const num = Number(content.slice("@max ".length).trim());
439
+ if (Number.isFinite(num)) state.maxConstraint = num;
440
+ }
441
+ function applyRuntimeAnnotation(state, content) {
442
+ const scope = content.slice("@runtime ".length).trim();
443
+ if (scope === "server" || scope === "client" || scope === "edge") {
444
+ state.runtime = scope;
445
+ }
446
+ }
447
+ function processAnnotationContent(state, content) {
448
+ const trimmed = content.trim();
449
+ if (trimmed === "@required") {
450
+ state.isRequired = true;
451
+ return;
452
+ }
453
+ if (trimmed === "@secret") {
454
+ state.isSecret = true;
455
+ return;
456
+ }
457
+ if (trimmed === "@optional") return;
458
+ if (content.startsWith("@description ")) {
459
+ state.description = content.slice("@description ".length).trim();
460
+ } else if (content.startsWith("@type ")) {
461
+ applyTypeAnnotation(state, content);
462
+ } else if (content.startsWith("@enum ")) {
463
+ applyEnumAnnotation(state, content);
464
+ } else if (content.startsWith("@min ")) {
465
+ applyMinAnnotation(state, content);
466
+ } else if (content.startsWith("@max ")) {
467
+ applyMaxAnnotation(state, content);
468
+ } else if (content.startsWith("@runtime ")) {
469
+ applyRuntimeAnnotation(state, content);
470
+ } else if (state.description === void 0 && trimmed.length > 0) {
471
+ state.description = trimmed;
472
+ }
473
+ }
422
474
  function parseCommentBlock(lines) {
423
- let annotatedType;
424
- let description;
425
- let isRequired = false;
475
+ const state = { isRequired: false };
426
476
  for (const line of lines) {
427
- const content = line.replace(/^#\s*/, "").trimEnd();
428
- if (content.startsWith("@description ")) {
429
- description = content.slice("@description ".length).trim();
430
- } else if (content.startsWith("@type ")) {
431
- const typeStr = content.slice("@type ".length).trim();
432
- if (isEnvVarType(typeStr)) {
433
- annotatedType = typeStr;
434
- }
435
- } else if (content.trim() === "@required") {
436
- isRequired = true;
437
- } else if (content.trim() === "@optional") {
438
- } else if (description === void 0 && content.trim().length > 0) {
439
- description = content.trim();
440
- }
477
+ processAnnotationContent(state, line.replace(/^#\s*/, "").trimEnd());
441
478
  }
442
- const result = { isRequired };
443
- if (annotatedType !== void 0) result.annotatedType = annotatedType;
444
- if (description !== void 0) result.description = description;
479
+ let constraints;
480
+ if (state.minConstraint !== void 0 || state.maxConstraint !== void 0) {
481
+ constraints = {};
482
+ if (state.minConstraint !== void 0) constraints.min = state.minConstraint;
483
+ if (state.maxConstraint !== void 0) constraints.max = state.maxConstraint;
484
+ }
485
+ const result = { isRequired: state.isRequired };
486
+ if (state.annotatedType !== void 0) result.annotatedType = state.annotatedType;
487
+ if (state.description !== void 0) result.description = state.description;
488
+ if (state.enumValues !== void 0) result.enumValues = state.enumValues;
489
+ if (constraints !== void 0) result.constraints = constraints;
490
+ if (state.runtime !== void 0) result.runtime = state.runtime;
491
+ if (state.isSecret !== void 0) result.isSecret = state.isSecret;
445
492
  return result;
446
493
  }
447
494
 
448
495
  // src/parser/env-parser.ts
449
496
  var ENV_VAR_RE = /^([A-Z_][A-Z0-9_]*)=(.*)$/;
450
497
  var SECTION_HEADER_RE = /^#\s+[-=]{3,}\s+(.+?)\s+[-=]{3,}\s*$/;
451
- function parseEnvFileContent(content, filePath) {
498
+ function parseEnvFileContent(content, filePath, options) {
452
499
  const lines = content.split("\n");
453
500
  const vars = [];
454
501
  const groups = [];
@@ -481,7 +528,11 @@ function parseEnvFileContent(content, filePath) {
481
528
  const key = envMatch[1] ?? "";
482
529
  const rawValue = envMatch[2] ?? "";
483
530
  const annotations = parseCommentBlock(commentBlock);
484
- const inferredType = inferType(key, rawValue);
531
+ const inferredType = inferType(
532
+ key,
533
+ rawValue,
534
+ ...options?.inferenceRules !== void 0 ? [{ extraRules: options.inferenceRules }] : []
535
+ );
485
536
  const isRequired = rawValue.length > 0 || annotations.isRequired;
486
537
  const isOptional = rawValue.length === 0 && !annotations.isRequired;
487
538
  const isClientSide = key.startsWith("NEXT_PUBLIC_");
@@ -503,6 +554,18 @@ function parseEnvFileContent(content, filePath) {
503
554
  if (currentGroup !== void 0) {
504
555
  parsedVar.group = currentGroup;
505
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
+ }
506
569
  vars.push(parsedVar);
507
570
  commentBlock = [];
508
571
  } else {
@@ -511,6 +574,10 @@ function parseEnvFileContent(content, filePath) {
511
574
  }
512
575
  return { filePath, vars, groups };
513
576
  }
577
+ function parseEnvFile(filePath) {
578
+ const content = readFileSync(filePath, "utf8");
579
+ return parseEnvFileContent(content, filePath);
580
+ }
514
581
 
515
582
  // src/utils/file.ts
516
583
  import { mkdir, readFile, writeFile } from "fs/promises";
@@ -529,7 +596,11 @@ import { format } from "prettier";
529
596
  async function formatOutput(content, parser = "typescript") {
530
597
  try {
531
598
  return await format(content, { parser });
532
- } catch {
599
+ } catch (err) {
600
+ console.warn(
601
+ "env-typegen: Prettier formatting failed, writing unformatted output.",
602
+ err instanceof Error ? err.message : String(err)
603
+ );
533
604
  return content;
534
605
  }
535
606
  }
@@ -555,6 +626,12 @@ function deriveOutputPath(base, generator, isSingle) {
555
626
  const outExt = generator === "declaration" ? ".d.ts" : baseExt;
556
627
  return `${noExt}.${generator}${outExt}`;
557
628
  }
629
+ function deriveOutputBaseForInput(output, inputPath) {
630
+ const dir = path5.dirname(output);
631
+ const ext = path5.extname(output);
632
+ const stem = path5.basename(inputPath, path5.extname(inputPath));
633
+ return path5.join(dir, `${stem}${ext}`);
634
+ }
558
635
  function buildOutput(generator, parsed) {
559
636
  switch (generator) {
560
637
  case "typescript":
@@ -580,7 +657,11 @@ async function persistOutput(params) {
580
657
  }
581
658
  if (dryRun) {
582
659
  if (!silent) {
583
- success(`Dry run: ${outputPath}`);
660
+ if (!isSingle) {
661
+ console.log(`// --- ${generator}: ${outputPath} ---`);
662
+ }
663
+ console.log(generated);
664
+ success(`Dry run: would write ${outputPath}`);
584
665
  }
585
666
  return;
586
667
  }
@@ -597,47 +678,1207 @@ async function runGenerate(options) {
597
678
  format: shouldFormat,
598
679
  stdout = false,
599
680
  dryRun = false,
600
- silent = false
681
+ silent = false,
682
+ inferenceRules: inferenceRules2
601
683
  } = options;
684
+ const inputs = Array.isArray(input) ? input : [input];
685
+ const hasMultipleInputs = inputs.length > 1;
602
686
  const isSingle = generators.length === 1;
603
- const content = await readEnvFile(input);
604
- const parsed = parseEnvFileContent(content, input);
605
- for (const generator of generators) {
606
- let generated = buildOutput(generator, parsed);
607
- if (shouldFormat) {
608
- generated = await formatOutput(generated);
609
- }
610
- const outputPath = deriveOutputPath(output, generator, isSingle);
611
- await persistOutput({
612
- generated,
613
- generator,
614
- outputPath,
615
- isSingle,
616
- stdout,
617
- dryRun,
618
- silent
687
+ for (const inputPath of inputs) {
688
+ const outputBase = hasMultipleInputs ? deriveOutputBaseForInput(output, inputPath) : output;
689
+ const content = await readEnvFile(inputPath);
690
+ const parsed = parseEnvFileContent(
691
+ content,
692
+ inputPath,
693
+ inferenceRules2 !== void 0 ? { inferenceRules: inferenceRules2 } : void 0
694
+ );
695
+ for (const generator of generators) {
696
+ let generated = buildOutput(generator, parsed);
697
+ if (shouldFormat && !dryRun) {
698
+ generated = await formatOutput(generated);
699
+ }
700
+ const outputPath = deriveOutputPath(outputBase, generator, isSingle);
701
+ await persistOutput({
702
+ generated,
703
+ generator,
704
+ outputPath,
705
+ isSingle,
706
+ stdout,
707
+ dryRun,
708
+ silent
709
+ });
710
+ }
711
+ }
712
+ }
713
+
714
+ // src/validation-command.ts
715
+ import path11 from "path";
716
+ import { pathToFileURL as pathToFileURL4 } from "url";
717
+ import { parseArgs } from "util";
718
+
719
+ // src/cloud/connectors.ts
720
+ import { readFile as readFile2 } from "fs/promises";
721
+ import path6 from "path";
722
+ function isRecord(value) {
723
+ return typeof value === "object" && value !== null && !Array.isArray(value);
724
+ }
725
+ function readEntryValue(entry, keys) {
726
+ for (const key of keys) {
727
+ const value = entry[key];
728
+ if (typeof value === "string") return value;
729
+ }
730
+ return void 0;
731
+ }
732
+ function parseVercelPayload(value) {
733
+ const entries = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value.envs) ? value.envs : [];
734
+ const result = {};
735
+ for (const entry of entries) {
736
+ if (!isRecord(entry)) continue;
737
+ const key = readEntryValue(entry, ["key", "name"]);
738
+ if (key === void 0) continue;
739
+ const envValue = readEntryValue(entry, ["value", "targetValue", "content"]) ?? "";
740
+ result[key] = envValue;
741
+ }
742
+ return result;
743
+ }
744
+ function parseCloudflarePayload(value) {
745
+ const entries = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value.result) ? value.result : [];
746
+ const result = {};
747
+ for (const entry of entries) {
748
+ if (!isRecord(entry)) continue;
749
+ const key = readEntryValue(entry, ["name", "key"]);
750
+ if (key === void 0) continue;
751
+ const envValue = readEntryValue(entry, ["text", "value", "secret"]) ?? "";
752
+ result[key] = envValue;
753
+ }
754
+ return result;
755
+ }
756
+ function parseAwsPayload(value) {
757
+ const entries = isRecord(value) && Array.isArray(value.Parameters) ? value.Parameters : [];
758
+ const result = {};
759
+ for (const entry of entries) {
760
+ if (!isRecord(entry)) continue;
761
+ const name = readEntryValue(entry, ["Name", "name"]);
762
+ if (name === void 0) continue;
763
+ const key = name.split("/").filter((part) => part.length > 0).pop() ?? name;
764
+ const envValue = readEntryValue(entry, ["Value", "value"]) ?? "";
765
+ result[key] = envValue;
766
+ }
767
+ return result;
768
+ }
769
+ function parseProviderPayload(provider, value) {
770
+ if (provider === "vercel") return parseVercelPayload(value);
771
+ if (provider === "cloudflare") return parseCloudflarePayload(value);
772
+ return parseAwsPayload(value);
773
+ }
774
+ async function loadCloudSource(options) {
775
+ const resolvedPath = path6.resolve(options.filePath);
776
+ const raw = await readFile2(resolvedPath, "utf8");
777
+ const parsed = JSON.parse(raw);
778
+ return parseProviderPayload(options.provider, parsed);
779
+ }
780
+
781
+ // src/contract.ts
782
+ import { existsSync as existsSync2 } from "fs";
783
+ import path7 from "path";
784
+ import { pathToFileURL as pathToFileURL2 } from "url";
785
+ var SECRET_KEY_RE = /(SECRET|TOKEN|PASSWORD|PRIVATE|API_KEY|ACCESS_KEY|CLIENT_SECRET)/i;
786
+ var CONTRACT_FILE_NAMES = ["env.contract.ts", "env.contract.mjs", "env.contract.js"];
787
+ function isRecord2(value) {
788
+ return typeof value === "object" && value !== null && !Array.isArray(value);
789
+ }
790
+ function isExpected(value) {
791
+ if (!isRecord2(value)) return false;
792
+ const typeValue = value.type;
793
+ if (typeof typeValue !== "string") return false;
794
+ const validTypes = /* @__PURE__ */ new Set([
795
+ "string",
796
+ "number",
797
+ "boolean",
798
+ "enum",
799
+ "url",
800
+ "email",
801
+ "json",
802
+ "semver",
803
+ "unknown"
804
+ ]);
805
+ if (!validTypes.has(typeValue)) return false;
806
+ if (typeValue === "enum") {
807
+ return Array.isArray(value.values) && value.values.every((item) => typeof item === "string");
808
+ }
809
+ return true;
810
+ }
811
+ function isEnvContractVariable(value) {
812
+ if (!isRecord2(value)) return false;
813
+ if (!isExpected(value.expected)) return false;
814
+ if (typeof value.required !== "boolean") return false;
815
+ if (typeof value.clientSide !== "boolean") return false;
816
+ if (value.description !== void 0 && typeof value.description !== "string") return false;
817
+ if (value.secret !== void 0 && typeof value.secret !== "boolean") return false;
818
+ return true;
819
+ }
820
+ function isEnvContract(value) {
821
+ if (!isRecord2(value)) return false;
822
+ if (value.schemaVersion !== 1) return false;
823
+ if (!isRecord2(value.variables)) return false;
824
+ return Object.values(value.variables).every((item) => isEnvContractVariable(item));
825
+ }
826
+ function isLegacyContract(value) {
827
+ if (!isRecord2(value)) return false;
828
+ if (!Array.isArray(value.vars)) return false;
829
+ return value.vars.every((entry) => isRecord2(entry) && typeof entry.name === "string");
830
+ }
831
+ function mapEnvVarTypeToExpected(type) {
832
+ if (type === "number") return { type: "number" };
833
+ if (type === "boolean") return { type: "boolean" };
834
+ if (type === "url") return { type: "url" };
835
+ if (type === "email") return { type: "email" };
836
+ if (type === "json") return { type: "json" };
837
+ if (type === "semver") return { type: "semver" };
838
+ if (type === "unknown") return { type: "unknown" };
839
+ return { type: "string" };
840
+ }
841
+ function shouldTreatAsSecret(key) {
842
+ return SECRET_KEY_RE.test(key);
843
+ }
844
+ function toExpectedFromLegacyEntry(entry) {
845
+ if (entry.enumValues !== void 0 && entry.enumValues.length > 0) {
846
+ return { type: "enum", values: entry.enumValues };
847
+ }
848
+ if (entry.expectedType === "number") {
849
+ return {
850
+ type: "number",
851
+ ...entry.constraints?.min !== void 0 && { min: entry.constraints.min },
852
+ ...entry.constraints?.max !== void 0 && { max: entry.constraints.max }
853
+ };
854
+ }
855
+ if (entry.expectedType === "boolean") return { type: "boolean" };
856
+ if (entry.expectedType === "url") return { type: "url" };
857
+ if (entry.expectedType === "email") return { type: "email" };
858
+ if (entry.expectedType === "json") return { type: "json" };
859
+ if (entry.expectedType === "semver") return { type: "semver" };
860
+ if (entry.expectedType === "unknown") return { type: "unknown" };
861
+ return { type: "string" };
862
+ }
863
+ function convertLegacyContract(contract) {
864
+ const variables = {};
865
+ for (const entry of contract.vars) {
866
+ const clientSide = entry.runtime === "client" || entry.name.startsWith("NEXT_PUBLIC_");
867
+ variables[entry.name] = {
868
+ expected: toExpectedFromLegacyEntry(entry),
869
+ required: entry.required,
870
+ clientSide,
871
+ ...entry.description !== void 0 && { description: entry.description },
872
+ ...(entry.isSecret ?? shouldTreatAsSecret(entry.name)) && { secret: true }
873
+ };
874
+ }
875
+ return {
876
+ schemaVersion: 1,
877
+ variables
878
+ };
879
+ }
880
+ function buildContractFromExample(examplePath) {
881
+ const parsed = parseEnvFile(examplePath);
882
+ const variables = {};
883
+ for (const variable of parsed.vars) {
884
+ const effectiveType = variable.annotatedType ?? variable.inferredType;
885
+ variables[variable.key] = {
886
+ expected: mapEnvVarTypeToExpected(effectiveType),
887
+ required: variable.isRequired,
888
+ clientSide: variable.isClientSide,
889
+ ...variable.description !== void 0 && { description: variable.description },
890
+ ...shouldTreatAsSecret(variable.key) && { secret: true }
891
+ };
892
+ }
893
+ return {
894
+ schemaVersion: 1,
895
+ variables
896
+ };
897
+ }
898
+ function findDefaultContractPath(cwd) {
899
+ for (const fileName of CONTRACT_FILE_NAMES) {
900
+ const candidatePath = path7.resolve(cwd, fileName);
901
+ if (existsSync2(candidatePath)) return candidatePath;
902
+ }
903
+ return void 0;
904
+ }
905
+ async function loadValidationContract(options) {
906
+ const { fallbackExamplePath, contractPath, cwd = process.cwd() } = options;
907
+ const discoveredContractPath = findDefaultContractPath(cwd);
908
+ const resolvedContractPath = contractPath !== void 0 ? path7.resolve(cwd, contractPath) : discoveredContractPath;
909
+ if (resolvedContractPath !== void 0 && existsSync2(resolvedContractPath)) {
910
+ const moduleUrl = pathToFileURL2(resolvedContractPath).href;
911
+ const moduleValue = await import(moduleUrl);
912
+ const candidate = moduleValue.default ?? moduleValue.contract;
913
+ if (isEnvContract(candidate)) {
914
+ return candidate;
915
+ }
916
+ if (isLegacyContract(candidate)) {
917
+ return convertLegacyContract(candidate);
918
+ }
919
+ throw new Error(
920
+ `Invalid contract at ${resolvedContractPath}. Export default must match EnvContract.`
921
+ );
922
+ }
923
+ return buildContractFromExample(path7.resolve(cwd, fallbackExamplePath));
924
+ }
925
+
926
+ // src/plugins.ts
927
+ import path8 from "path";
928
+ import { pathToFileURL as pathToFileURL3 } from "url";
929
+ function isRecord3(value) {
930
+ return typeof value === "object" && value !== null && !Array.isArray(value);
931
+ }
932
+ function isPlugin(value) {
933
+ if (!isRecord3(value)) return false;
934
+ if (typeof value.name !== "string") return false;
935
+ if (value.transformContract !== void 0 && typeof value.transformContract !== "function") {
936
+ return false;
937
+ }
938
+ if (value.transformSource !== void 0 && typeof value.transformSource !== "function") {
939
+ return false;
940
+ }
941
+ if (value.transformReport !== void 0 && typeof value.transformReport !== "function") {
942
+ return false;
943
+ }
944
+ return true;
945
+ }
946
+ async function loadPluginFromPath(pluginPath, cwd) {
947
+ const resolvedPath = path8.resolve(cwd, pluginPath);
948
+ const moduleValue = await import(pathToFileURL3(resolvedPath).href);
949
+ const candidate = moduleValue.default ?? moduleValue.plugin;
950
+ if (isPlugin(candidate)) return candidate;
951
+ throw new Error(`Invalid plugin at ${resolvedPath}. Expected a plugin object export.`);
952
+ }
953
+ async function loadPlugins(options) {
954
+ const cwd = options.cwd ?? process.cwd();
955
+ const plugins = [];
956
+ const references = [...options.configPlugins ?? [], ...options.pluginPaths];
957
+ for (const reference of references) {
958
+ if (typeof reference === "string") {
959
+ plugins.push(await loadPluginFromPath(reference, cwd));
960
+ continue;
961
+ }
962
+ if (isPlugin(reference)) {
963
+ plugins.push(reference);
964
+ continue;
965
+ }
966
+ throw new Error("Invalid plugin reference in configuration.");
967
+ }
968
+ return plugins;
969
+ }
970
+ function applyContractPlugins(contract, plugins) {
971
+ let next = contract;
972
+ for (const plugin of plugins) {
973
+ if (plugin.transformContract === void 0) continue;
974
+ next = plugin.transformContract(next);
975
+ }
976
+ return next;
977
+ }
978
+ function applySourcePlugins(params, plugins) {
979
+ let next = params.values;
980
+ for (const plugin of plugins) {
981
+ if (plugin.transformSource === void 0) continue;
982
+ next = plugin.transformSource({ environment: params.environment, values: next });
983
+ }
984
+ return next;
985
+ }
986
+ function applyReportPlugins(report, plugins) {
987
+ let next = report;
988
+ for (const plugin of plugins) {
989
+ if (plugin.transformReport === void 0) continue;
990
+ next = plugin.transformReport(next);
991
+ }
992
+ return next;
993
+ }
994
+
995
+ // 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-.]+)?$/;
998
+ function detectReceivedType(value) {
999
+ const normalized = value.trim();
1000
+ if (normalized.length === 0) return "unknown";
1001
+ if (["true", "false", "1", "0", "yes", "no"].includes(normalized.toLowerCase())) return "boolean";
1002
+ if (!Number.isNaN(Number(normalized)) && Number.isFinite(Number(normalized))) return "number";
1003
+ if (SEMVER_RE.test(normalized)) return "semver";
1004
+ try {
1005
+ const url = new URL(normalized);
1006
+ if (url.protocol.length > 0) return "url";
1007
+ } catch {
1008
+ }
1009
+ if (EMAIL_RE.test(normalized)) return "email";
1010
+ try {
1011
+ const parsed = JSON.parse(normalized);
1012
+ if (typeof parsed === "object" && parsed !== null) return "json";
1013
+ } catch {
1014
+ }
1015
+ return "string";
1016
+ }
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 };
1034
+ }
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 };
1040
+ }
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 };
1046
+ }
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
+ }
1056
+ }
1057
+ if (expected.type === "email") {
1058
+ if (!EMAIL_RE.test(normalized))
1059
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1060
+ return { isValid: true, receivedType };
1061
+ }
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
+ }
1070
+ }
1071
+ if (expected.type === "semver") {
1072
+ if (!SEMVER_RE.test(normalized))
1073
+ return { isValid: false, receivedType, issueType: "invalid_value" };
1074
+ return { isValid: true, receivedType };
1075
+ }
1076
+ return { isValid: true, receivedType };
1077
+ }
1078
+ function toIssueCode(issueType) {
1079
+ if (issueType === "missing") return "ENV_MISSING";
1080
+ if (issueType === "extra") return "ENV_EXTRA";
1081
+ if (issueType === "invalid_type") return "ENV_INVALID_TYPE";
1082
+ if (issueType === "invalid_value") return "ENV_INVALID_VALUE";
1083
+ if (issueType === "conflict") return "ENV_CONFLICT";
1084
+ return "ENV_SECRET_EXPOSED";
1085
+ }
1086
+ function toIssueValue(value, debugValues) {
1087
+ if (!debugValues) return null;
1088
+ if (value === void 0) return null;
1089
+ return value;
1090
+ }
1091
+ function createIssue(params) {
1092
+ return {
1093
+ code: toIssueCode(params.type),
1094
+ type: params.type,
1095
+ severity: params.severity,
1096
+ key: params.key,
1097
+ environment: params.environment,
1098
+ message: params.message,
1099
+ value: toIssueValue(params.value, params.debugValues),
1100
+ ...params.expected !== void 0 && { expected: params.expected },
1101
+ ...params.receivedType !== void 0 && { receivedType: params.receivedType }
1102
+ };
1103
+ }
1104
+ function dedupeIssues(issues) {
1105
+ const seen = /* @__PURE__ */ new Set();
1106
+ const unique = [];
1107
+ for (const issue of issues) {
1108
+ const token = [
1109
+ issue.code,
1110
+ issue.type,
1111
+ issue.severity,
1112
+ issue.environment,
1113
+ issue.key,
1114
+ issue.message,
1115
+ issue.receivedType ?? ""
1116
+ ].join("|");
1117
+ if (seen.has(token)) continue;
1118
+ seen.add(token);
1119
+ unique.push(issue);
1120
+ }
1121
+ return unique;
1122
+ }
1123
+ function buildReport(env, issues, recommendations) {
1124
+ const dedupedIssues = dedupeIssues(issues);
1125
+ const errors = dedupedIssues.filter((item) => item.severity === "error").length;
1126
+ const warnings = dedupedIssues.filter((item) => item.severity === "warning").length;
1127
+ const status = errors > 0 ? "fail" : "ok";
1128
+ return {
1129
+ schemaVersion: 1,
1130
+ status,
1131
+ summary: {
1132
+ errors,
1133
+ warnings,
1134
+ total: dedupedIssues.length
1135
+ },
1136
+ issues: dedupedIssues,
1137
+ meta: {
1138
+ env,
1139
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1140
+ },
1141
+ ...recommendations !== void 0 && recommendations.length > 0 && { recommendations }
1142
+ };
1143
+ }
1144
+ function isClientSecret(variable, key) {
1145
+ return variable.secret === true && (variable.clientSide || key.startsWith("NEXT_PUBLIC_"));
1146
+ }
1147
+ function validateAgainstContract(options) {
1148
+ const issues = [];
1149
+ const contractKeys = new Set(Object.keys(options.contract.variables));
1150
+ 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
+ }
1198
+ }
1199
+ for (const [key, value] of Object.entries(options.values)) {
1200
+ if (contractKeys.has(key)) continue;
1201
+ const severity = options.strict ? "error" : "warning";
1202
+ issues.push(
1203
+ createIssue({
1204
+ type: "extra",
1205
+ severity,
1206
+ key,
1207
+ environment: options.environment,
1208
+ message: `Variable ${key} is not defined in the contract.`,
1209
+ value,
1210
+ debugValues: options.debugValues
1211
+ })
1212
+ );
1213
+ }
1214
+ return buildReport(options.environment, issues);
1215
+ }
1216
+ function collectUnionKeys(contract, sources) {
1217
+ const union = new Set(Object.keys(contract.variables));
1218
+ for (const source of Object.values(sources)) {
1219
+ for (const key of Object.keys(source)) {
1220
+ union.add(key);
1221
+ }
1222
+ }
1223
+ return union;
1224
+ }
1225
+ function diffEnvironmentSources(options) {
1226
+ const issues = [];
1227
+ const sourceNames = Object.keys(options.sources);
1228
+ const unionKeys = collectUnionKeys(options.contract, options.sources);
1229
+ for (const key of unionKeys) {
1230
+ const variable = options.contract.variables[key];
1231
+ const valuesBySource = sourceNames.map((sourceName) => ({
1232
+ sourceName,
1233
+ value: options.sources[sourceName]?.[key]
1234
+ }));
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
+ );
1241
+ if (present.length === 0 && variable?.required === true) {
1242
+ for (const entry of missing) {
1243
+ issues.push(
1244
+ createIssue({
1245
+ type: "missing",
1246
+ severity: "error",
1247
+ key,
1248
+ environment: entry.sourceName,
1249
+ message: `Required variable ${key} is missing in ${entry.sourceName}.`,
1250
+ debugValues: options.debugValues,
1251
+ expected: variable.expected
1252
+ })
1253
+ );
1254
+ }
1255
+ continue;
1256
+ }
1257
+ 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
+ }
1271
+ }
1272
+ const typeBySource = /* @__PURE__ */ new Map();
1273
+ 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
+ }
1343
+ }
1344
+ }
1345
+ return buildReport("diff", issues);
1346
+ }
1347
+ function buildRecommendations(issues) {
1348
+ const codes = new Set(issues.map((item) => item.code));
1349
+ const recommendations = [];
1350
+ if (codes.has("ENV_MISSING")) {
1351
+ recommendations.push("Add missing required variables to each target environment.");
1352
+ }
1353
+ if (codes.has("ENV_EXTRA")) {
1354
+ recommendations.push(
1355
+ "Remove undeclared variables or add them to env.contract.ts intentionally."
1356
+ );
1357
+ }
1358
+ if (codes.has("ENV_INVALID_TYPE") || codes.has("ENV_INVALID_VALUE")) {
1359
+ recommendations.push(
1360
+ "Normalize variable values so they match the expected contract types and constraints."
1361
+ );
1362
+ }
1363
+ if (codes.has("ENV_CONFLICT")) {
1364
+ recommendations.push("Align variable semantics across environments to avoid drift.");
1365
+ }
1366
+ if (codes.has("ENV_SECRET_EXPOSED")) {
1367
+ recommendations.push(
1368
+ "Move secret variables to server-only scope and avoid NEXT_PUBLIC_ exposure for secrets."
1369
+ );
1370
+ }
1371
+ return recommendations;
1372
+ }
1373
+ function buildDoctorReport(options) {
1374
+ const merged = [...options.checkReport.issues, ...options.diffReport.issues];
1375
+ const recommendations = buildRecommendations(merged);
1376
+ return buildReport("doctor", merged, recommendations);
1377
+ }
1378
+
1379
+ // src/validation/env-source.ts
1380
+ import { readFile as readFile3 } from "fs/promises";
1381
+ import path9 from "path";
1382
+ function stripWrappingQuotes(value) {
1383
+ if (value.length < 2) return value;
1384
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1385
+ return value.slice(1, -1);
1386
+ }
1387
+ return value;
1388
+ }
1389
+ function parseEnvSourceContent(content) {
1390
+ const result = {};
1391
+ const lines = content.split("\n");
1392
+ for (const line of lines) {
1393
+ const trimmed = line.trim();
1394
+ if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
1395
+ const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(trimmed);
1396
+ if (match === null) continue;
1397
+ const key = match[1] ?? "";
1398
+ const rawValue = match[2] ?? "";
1399
+ result[key] = stripWrappingQuotes(rawValue.trim());
1400
+ }
1401
+ return result;
1402
+ }
1403
+ async function loadEnvSource(options) {
1404
+ const resolvedPath = path9.resolve(options.filePath);
1405
+ try {
1406
+ const content = await readFile3(resolvedPath, "utf8");
1407
+ return parseEnvSourceContent(content);
1408
+ } catch (errorValue) {
1409
+ if (options.allowMissing === true && errorValue instanceof Error && "code" in errorValue && errorValue.code === "ENOENT") {
1410
+ return {};
1411
+ }
1412
+ throw errorValue;
1413
+ }
1414
+ }
1415
+
1416
+ // src/validation/output.ts
1417
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
1418
+ import path10 from "path";
1419
+ function toJsonString(report, mode) {
1420
+ if (mode === "pretty") return `${JSON.stringify(report, null, 2)}
1421
+ `;
1422
+ return `${JSON.stringify(report)}
1423
+ `;
1424
+ }
1425
+ function formatIssue(issue) {
1426
+ const expected = issue.expected !== void 0 ? ` expected=${issue.expected.type}` : "";
1427
+ const received = issue.receivedType !== void 0 ? ` received=${issue.receivedType}` : "";
1428
+ return `${issue.severity.toUpperCase()} [${issue.code}] ${issue.environment}:${issue.key} ${issue.message}${expected}${received}`;
1429
+ }
1430
+ function formatHumanReport(report) {
1431
+ const lines = [];
1432
+ lines.push(
1433
+ `Status: ${report.status.toUpperCase()} (errors=${report.summary.errors}, warnings=${report.summary.warnings}, total=${report.summary.total})`
1434
+ );
1435
+ if (report.issues.length > 0) {
1436
+ lines.push("");
1437
+ lines.push("Issues:");
1438
+ for (const issue of report.issues) {
1439
+ lines.push(`- ${formatIssue(issue)}`);
1440
+ }
1441
+ }
1442
+ if (report.recommendations !== void 0 && report.recommendations.length > 0) {
1443
+ lines.push("");
1444
+ lines.push("Recommendations:");
1445
+ for (const recommendation of report.recommendations) {
1446
+ lines.push(`- ${recommendation}`);
1447
+ }
1448
+ }
1449
+ return `${lines.join("\n")}
1450
+ `;
1451
+ }
1452
+ async function persistJsonOutput(outputFile, report) {
1453
+ const resolvedPath = path10.resolve(outputFile);
1454
+ await mkdir2(path10.dirname(resolvedPath), { recursive: true });
1455
+ await writeFile2(resolvedPath, `${JSON.stringify(report, null, 2)}
1456
+ `, "utf8");
1457
+ }
1458
+ async function emitValidationReport(options) {
1459
+ const { report, outputFile, jsonMode } = options;
1460
+ if (outputFile !== void 0) {
1461
+ await persistJsonOutput(outputFile, report);
1462
+ }
1463
+ if (jsonMode === "off") {
1464
+ process.stdout.write(formatHumanReport(report));
1465
+ return;
1466
+ }
1467
+ process.stdout.write(toJsonString(report, jsonMode));
1468
+ }
1469
+
1470
+ // src/validation-command.ts
1471
+ var HELP_TEXT = {
1472
+ check: [
1473
+ "Usage: env-typegen check [options]",
1474
+ "",
1475
+ "Options:",
1476
+ " --env <path> Environment file to validate (default: .env)",
1477
+ " --contract <path> Contract file path (default: env.contract.ts)",
1478
+ " --example <path> Fallback .env.example used to bootstrap contract",
1479
+ " --strict Validate extras as errors (default: true)",
1480
+ " --no-strict Downgrade extras to warnings",
1481
+ " --json Emit machine-readable JSON report",
1482
+ " --json=pretty Emit pretty JSON report",
1483
+ " --output-file <path> Persist JSON report to a file",
1484
+ " --debug-values Include raw values in issues (unsafe for CI logs)",
1485
+ " --cloud-provider <name> vercel | cloudflare | aws",
1486
+ " --cloud-file <path> Cloud snapshot JSON file",
1487
+ " --plugin <path> Plugin module path (repeatable)",
1488
+ " -c, --config <path> Config file path",
1489
+ " -h, --help Show this help"
1490
+ ].join("\n"),
1491
+ diff: [
1492
+ "Usage: env-typegen diff [options]",
1493
+ "",
1494
+ "Options:",
1495
+ " --targets <list> Comma-separated targets (default: .env,.env.example,.env.production)",
1496
+ " --contract <path> Contract file path (default: env.contract.ts)",
1497
+ " --example <path> Fallback .env.example used to bootstrap contract",
1498
+ " --strict Validate extras as errors (default: true)",
1499
+ " --no-strict Downgrade extras to warnings",
1500
+ " --json Emit machine-readable JSON report",
1501
+ " --json=pretty Emit pretty JSON report",
1502
+ " --output-file <path> Persist JSON report to a file",
1503
+ " --debug-values Include raw values in issues (unsafe for CI logs)",
1504
+ " --cloud-provider <name> vercel | cloudflare | aws",
1505
+ " --cloud-file <path> Cloud snapshot JSON file added to diff sources",
1506
+ " --plugin <path> Plugin module path (repeatable)",
1507
+ " -c, --config <path> Config file path",
1508
+ " -h, --help Show this help"
1509
+ ].join("\n"),
1510
+ doctor: [
1511
+ "Usage: env-typegen doctor [options]",
1512
+ "",
1513
+ "Options:",
1514
+ " --env <path> Environment file to validate (default: .env)",
1515
+ " --targets <list> Comma-separated targets for drift analysis",
1516
+ " --contract <path> Contract file path (default: env.contract.ts)",
1517
+ " --example <path> Fallback .env.example used to bootstrap contract",
1518
+ " --strict Validate extras as errors (default: true)",
1519
+ " --no-strict Downgrade extras to warnings",
1520
+ " --json Emit machine-readable JSON report",
1521
+ " --json=pretty Emit pretty JSON report",
1522
+ " --output-file <path> Persist JSON report to a file",
1523
+ " --debug-values Include raw values in issues (unsafe for CI logs)",
1524
+ " --cloud-provider <name> vercel | cloudflare | aws",
1525
+ " --cloud-file <path> Cloud snapshot JSON file",
1526
+ " --plugin <path> Plugin module path (repeatable)",
1527
+ " -c, --config <path> Config file path",
1528
+ " -h, --help Show this help"
1529
+ ].join("\n")
1530
+ };
1531
+ function resolveConfigRelative(value, configDir) {
1532
+ if (value === void 0 || path11.isAbsolute(value)) return value;
1533
+ return path11.resolve(configDir, value);
1534
+ }
1535
+ function resolvePluginReference(reference, configDir) {
1536
+ if (typeof reference === "string" && !path11.isAbsolute(reference)) {
1537
+ return path11.resolve(configDir, reference);
1538
+ }
1539
+ return reference;
1540
+ }
1541
+ function applyConfigPaths(config, configDir) {
1542
+ let input;
1543
+ if (Array.isArray(config.input)) {
1544
+ input = config.input.map((item) => resolveConfigRelative(item, configDir) ?? item);
1545
+ } else {
1546
+ input = resolveConfigRelative(config.input, configDir);
1547
+ }
1548
+ const output = resolveConfigRelative(config.output, configDir);
1549
+ const schemaFile = resolveConfigRelative(config.schemaFile, configDir);
1550
+ return {
1551
+ ...config,
1552
+ ...input !== void 0 && { input },
1553
+ ...output !== void 0 && { output },
1554
+ ...schemaFile !== void 0 && { schemaFile },
1555
+ ...config.diffTargets !== void 0 && {
1556
+ diffTargets: config.diffTargets.map(
1557
+ (target) => resolveConfigRelative(target, configDir) ?? target
1558
+ )
1559
+ },
1560
+ ...config.plugins !== void 0 && {
1561
+ plugins: config.plugins.map((reference) => resolvePluginReference(reference, configDir))
1562
+ }
1563
+ };
1564
+ }
1565
+ async function loadCommandConfig(configPath) {
1566
+ if (configPath === void 0) {
1567
+ return loadConfig(process.cwd());
1568
+ }
1569
+ const resolvedPath = path11.resolve(configPath);
1570
+ const configDir = path11.dirname(resolvedPath);
1571
+ const moduleValue = await import(pathToFileURL4(resolvedPath).href);
1572
+ if (moduleValue.default === void 0) return void 0;
1573
+ return applyConfigPaths(moduleValue.default, configDir);
1574
+ }
1575
+ function preprocessJsonArguments(argv) {
1576
+ const normalizedArgs = [];
1577
+ let assignedMode = "off";
1578
+ for (const item of argv) {
1579
+ if (item === "--json=pretty") {
1580
+ normalizedArgs.push("--json");
1581
+ assignedMode = "pretty";
1582
+ continue;
1583
+ }
1584
+ if (item === "--json=compact") {
1585
+ normalizedArgs.push("--json");
1586
+ assignedMode = "compact";
1587
+ continue;
1588
+ }
1589
+ normalizedArgs.push(item);
1590
+ }
1591
+ return { normalizedArgs, assignedMode };
1592
+ }
1593
+ function parseValidationArgs(argv) {
1594
+ const { normalizedArgs, assignedMode } = preprocessJsonArguments(argv);
1595
+ const { values } = parseArgs({
1596
+ args: normalizedArgs,
1597
+ options: {
1598
+ env: { type: "string", multiple: true },
1599
+ targets: { type: "string" },
1600
+ contract: { type: "string" },
1601
+ example: { type: "string" },
1602
+ strict: { type: "boolean" },
1603
+ "no-strict": { type: "boolean" },
1604
+ json: { type: "boolean" },
1605
+ "output-file": { type: "string" },
1606
+ "debug-values": { type: "boolean" },
1607
+ "cloud-provider": { type: "string" },
1608
+ "cloud-file": { type: "string" },
1609
+ plugin: { type: "string", multiple: true },
1610
+ config: { type: "string", short: "c" },
1611
+ help: { type: "boolean", short: "h" }
1612
+ }
1613
+ });
1614
+ const castValues = values;
1615
+ const jsonMode = castValues.json === true ? assignedMode === "off" ? "compact" : assignedMode : "off";
1616
+ return { values: castValues, jsonMode };
1617
+ }
1618
+ function resolveStrict(values, fileConfig) {
1619
+ if (values["no-strict"] === true) return false;
1620
+ if (values.strict !== void 0) return values.strict;
1621
+ if (fileConfig?.strict !== void 0) return fileConfig.strict;
1622
+ return true;
1623
+ }
1624
+ function parseCloudProvider(value) {
1625
+ if (value === void 0) return void 0;
1626
+ if (value === "vercel" || value === "cloudflare" || value === "aws") return value;
1627
+ throw new Error(`Unknown cloud provider: ${value}. Valid: vercel, cloudflare, aws`);
1628
+ }
1629
+ function parseTargets(values, fileConfig) {
1630
+ const fromCli = values.targets;
1631
+ if (fromCli !== void 0) {
1632
+ return fromCli.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
1633
+ }
1634
+ if (fileConfig?.diffTargets !== void 0 && fileConfig.diffTargets.length > 0) {
1635
+ return fileConfig.diffTargets;
1636
+ }
1637
+ return [".env", ".env.example", ".env.production"];
1638
+ }
1639
+ async function prepareCommonContext(values) {
1640
+ const fileConfig = await loadCommandConfig(values.config);
1641
+ const strict = resolveStrict(values, fileConfig);
1642
+ const debugValues = values["debug-values"] ?? false;
1643
+ const contractPath = values.contract ?? fileConfig?.schemaFile;
1644
+ const fallbackExamplePath = values.example ?? ".env.example";
1645
+ const cloudProvider = parseCloudProvider(values["cloud-provider"]);
1646
+ const cloudFile = values["cloud-file"];
1647
+ const pluginLoadOptions = {
1648
+ pluginPaths: values.plugin ?? [],
1649
+ cwd: process.cwd(),
1650
+ ...fileConfig?.plugins !== void 0 && { configPlugins: fileConfig.plugins }
1651
+ };
1652
+ const plugins = await loadPlugins(pluginLoadOptions);
1653
+ return {
1654
+ fileConfig,
1655
+ strict,
1656
+ debugValues,
1657
+ outputFile: values["output-file"],
1658
+ contractPath,
1659
+ fallbackExamplePath,
1660
+ cloudProvider,
1661
+ cloudFile,
1662
+ plugins
1663
+ };
1664
+ }
1665
+ async function emitAndReturnExitCode(report, params) {
1666
+ const emitOptions = {
1667
+ report,
1668
+ jsonMode: params.jsonMode,
1669
+ ...params.outputFile !== void 0 && { outputFile: params.outputFile }
1670
+ };
1671
+ await emitValidationReport(emitOptions);
1672
+ return report.status === "fail" ? 1 : 0;
1673
+ }
1674
+ async function runCheckCommand(args) {
1675
+ const context = await prepareCommonContext(args.values);
1676
+ const loadContractOptions = {
1677
+ fallbackExamplePath: context.fallbackExamplePath,
1678
+ cwd: process.cwd(),
1679
+ ...context.contractPath !== void 0 && { contractPath: context.contractPath }
1680
+ };
1681
+ const contract = applyContractPlugins(
1682
+ await loadValidationContract(loadContractOptions),
1683
+ context.plugins
1684
+ );
1685
+ const provider = context.cloudProvider;
1686
+ let environment = args.values.env?.[0] ?? ".env";
1687
+ let sourceValues;
1688
+ if (provider !== void 0) {
1689
+ const cloudFile = context.cloudFile ?? `${provider}.env.json`;
1690
+ sourceValues = await loadCloudSource({ provider, filePath: cloudFile });
1691
+ environment = `cloud:${provider}`;
1692
+ } else {
1693
+ sourceValues = await loadEnvSource({ filePath: environment, allowMissing: true });
1694
+ }
1695
+ sourceValues = applySourcePlugins({ environment, values: sourceValues }, context.plugins);
1696
+ const report = applyReportPlugins(
1697
+ validateAgainstContract({
1698
+ contract,
1699
+ values: sourceValues,
1700
+ environment,
1701
+ strict: context.strict,
1702
+ debugValues: context.debugValues
1703
+ }),
1704
+ context.plugins
1705
+ );
1706
+ return emitAndReturnExitCode(report, {
1707
+ jsonMode: args.jsonMode,
1708
+ ...context.outputFile !== void 0 && { outputFile: context.outputFile }
1709
+ });
1710
+ }
1711
+ async function runDiffCommand(args) {
1712
+ const context = await prepareCommonContext(args.values);
1713
+ const loadContractOptions = {
1714
+ fallbackExamplePath: context.fallbackExamplePath,
1715
+ cwd: process.cwd(),
1716
+ ...context.contractPath !== void 0 && { contractPath: context.contractPath }
1717
+ };
1718
+ const contract = applyContractPlugins(
1719
+ await loadValidationContract(loadContractOptions),
1720
+ context.plugins
1721
+ );
1722
+ const sources = {};
1723
+ for (const target of parseTargets(args.values, context.fileConfig)) {
1724
+ const values = await loadEnvSource({ filePath: target, allowMissing: true });
1725
+ sources[target] = applySourcePlugins({ environment: target, values }, context.plugins);
1726
+ }
1727
+ if (context.cloudProvider !== void 0) {
1728
+ const cloudFile = context.cloudFile ?? `${context.cloudProvider}.env.json`;
1729
+ const cloudEnvironment = `cloud:${context.cloudProvider}`;
1730
+ const cloudValues = await loadCloudSource({
1731
+ provider: context.cloudProvider,
1732
+ filePath: cloudFile
1733
+ });
1734
+ sources[cloudEnvironment] = applySourcePlugins(
1735
+ { environment: cloudEnvironment, values: cloudValues },
1736
+ context.plugins
1737
+ );
1738
+ }
1739
+ const report = applyReportPlugins(
1740
+ diffEnvironmentSources({
1741
+ contract,
1742
+ sources,
1743
+ strict: context.strict,
1744
+ debugValues: context.debugValues
1745
+ }),
1746
+ context.plugins
1747
+ );
1748
+ return emitAndReturnExitCode(report, {
1749
+ jsonMode: args.jsonMode,
1750
+ ...context.outputFile !== void 0 && { outputFile: context.outputFile }
1751
+ });
1752
+ }
1753
+ async function runDoctorCommand(args) {
1754
+ const context = await prepareCommonContext(args.values);
1755
+ const loadContractOptions = {
1756
+ fallbackExamplePath: context.fallbackExamplePath,
1757
+ cwd: process.cwd(),
1758
+ ...context.contractPath !== void 0 && { contractPath: context.contractPath }
1759
+ };
1760
+ const contract = applyContractPlugins(
1761
+ await loadValidationContract(loadContractOptions),
1762
+ context.plugins
1763
+ );
1764
+ const checkEnvironment = args.values.env?.[0] ?? ".env";
1765
+ let checkValues = await loadEnvSource({ filePath: checkEnvironment, allowMissing: true });
1766
+ checkValues = applySourcePlugins(
1767
+ { environment: checkEnvironment, values: checkValues },
1768
+ context.plugins
1769
+ );
1770
+ const checkReport = validateAgainstContract({
1771
+ contract,
1772
+ values: checkValues,
1773
+ environment: checkEnvironment,
1774
+ strict: context.strict,
1775
+ debugValues: context.debugValues
1776
+ });
1777
+ const sources = {};
1778
+ for (const target of parseTargets(args.values, context.fileConfig)) {
1779
+ const values = await loadEnvSource({ filePath: target, allowMissing: true });
1780
+ sources[target] = applySourcePlugins({ environment: target, values }, context.plugins);
1781
+ }
1782
+ if (context.cloudProvider !== void 0) {
1783
+ const cloudFile = context.cloudFile ?? `${context.cloudProvider}.env.json`;
1784
+ const cloudEnvironment = `cloud:${context.cloudProvider}`;
1785
+ const cloudValues = await loadCloudSource({
1786
+ provider: context.cloudProvider,
1787
+ filePath: cloudFile
619
1788
  });
1789
+ sources[cloudEnvironment] = applySourcePlugins(
1790
+ { environment: cloudEnvironment, values: cloudValues },
1791
+ context.plugins
1792
+ );
1793
+ }
1794
+ const diffReport = diffEnvironmentSources({
1795
+ contract,
1796
+ sources,
1797
+ strict: context.strict,
1798
+ debugValues: context.debugValues
1799
+ });
1800
+ const report = applyReportPlugins(
1801
+ buildDoctorReport({ checkReport, diffReport }),
1802
+ context.plugins
1803
+ );
1804
+ return emitAndReturnExitCode(report, {
1805
+ jsonMode: args.jsonMode,
1806
+ ...context.outputFile !== void 0 && { outputFile: context.outputFile }
1807
+ });
1808
+ }
1809
+ async function runValidationCommand(params) {
1810
+ const parsed = parseValidationArgs(params.argv);
1811
+ if (parsed.values.help === true) {
1812
+ console.log(HELP_TEXT[params.command]);
1813
+ return 0;
1814
+ }
1815
+ if (params.command === "check") {
1816
+ return runCheckCommand(parsed);
620
1817
  }
1818
+ if (params.command === "diff") {
1819
+ return runDiffCommand(parsed);
1820
+ }
1821
+ return runDoctorCommand(parsed);
621
1822
  }
622
1823
 
623
1824
  // src/watch.ts
1825
+ import path12 from "path";
624
1826
  import { watch } from "chokidar";
625
- function startWatch({ inputPath, runOptions }) {
626
- log(`Watching ${inputPath} for changes...`);
1827
+ function debounce(fn, delay) {
1828
+ let timer;
1829
+ return (...args) => {
1830
+ if (timer !== void 0) clearTimeout(timer);
1831
+ timer = setTimeout(() => {
1832
+ timer = void 0;
1833
+ fn(...args);
1834
+ }, delay);
1835
+ };
1836
+ }
1837
+ function startWatch({ inputPath, runOptions, cwd = process.cwd() }) {
1838
+ const inputLabel = Array.isArray(inputPath) ? inputPath.join(", ") : inputPath;
1839
+ log(`Watching ${inputLabel} for changes...`);
627
1840
  void runGenerate(runOptions).catch((err) => {
628
1841
  const message = err instanceof Error ? err.message : JSON.stringify(err);
629
1842
  error(message);
630
1843
  });
631
- const watcher = watch(inputPath, { persistent: true });
632
- watcher.on("change", () => {
633
- log(`${inputPath} changed, regenerating...`);
1844
+ const handleChange = debounce((eventPath) => {
1845
+ log(`${eventPath} changed, regenerating...`);
634
1846
  void runGenerate(runOptions).catch((err) => {
635
1847
  const message = err instanceof Error ? err.message : JSON.stringify(err);
636
1848
  error(message);
637
1849
  });
638
- });
1850
+ }, 200);
1851
+ const handleConfigChange = debounce(async (eventPath) => {
1852
+ log(`Config file ${eventPath} changed, reloading...`);
1853
+ try {
1854
+ const reloaded = await loadConfig(cwd);
1855
+ if (reloaded) {
1856
+ runOptions.generators = reloaded.generators ?? runOptions.generators;
1857
+ runOptions.format = reloaded.format ?? runOptions.format;
1858
+ if (reloaded.inferenceRules !== void 0) {
1859
+ runOptions.inferenceRules = reloaded.inferenceRules;
1860
+ }
1861
+ }
1862
+ void runGenerate(runOptions).catch((err) => {
1863
+ const message = err instanceof Error ? err.message : JSON.stringify(err);
1864
+ error(message);
1865
+ });
1866
+ } catch (err) {
1867
+ const message = err instanceof Error ? err.message : JSON.stringify(err);
1868
+ error(`Failed to reload config: ${message}`);
1869
+ }
1870
+ }, 200);
1871
+ const inputWatcher = watch(inputPath, { persistent: true });
1872
+ for (const event of ["add", "change", "unlink"]) {
1873
+ inputWatcher.on(event, handleChange);
1874
+ }
1875
+ const configPaths = CONFIG_FILE_NAMES.map((name) => path12.resolve(cwd, name));
1876
+ const configWatcher = watch(configPaths, { persistent: true, ignoreInitial: true });
1877
+ for (const event of ["add", "change"]) {
1878
+ configWatcher.on(event, (eventPath) => void handleConfigChange(eventPath));
1879
+ }
639
1880
  process.on("SIGINT", () => {
640
- void watcher.close().then(() => {
1881
+ void Promise.all([inputWatcher.close(), configWatcher.close()]).then(() => {
641
1882
  log("Watcher stopped.");
642
1883
  process.exit(0);
643
1884
  });
@@ -645,15 +1886,19 @@ function startWatch({ inputPath, runOptions }) {
645
1886
  }
646
1887
 
647
1888
  // src/cli.ts
648
- var VERSION = "0.1.0";
649
- var HELP_TEXT = [
1889
+ var _require = createRequire(import.meta.url);
1890
+ var VERSION = _require("../package.json").version;
1891
+ var HELP_TEXT2 = [
650
1892
  "env-typegen \u2014 Generate TypeScript types from .env.example",
651
1893
  "",
652
1894
  "Usage:",
653
- " env-typegen -i <path> [options]",
1895
+ " env-typegen [generate] -i <path> [options]",
1896
+ " env-typegen check [options]",
1897
+ " env-typegen diff [options]",
1898
+ " env-typegen doctor [options]",
654
1899
  "",
655
1900
  "Options:",
656
- " -i, --input <path> Path to .env.example file",
1901
+ " -i, --input <path> Path to .env.example file(s). May be specified multiple times.",
657
1902
  " -o, --output <path> Output file path (default: env.generated.ts)",
658
1903
  " -f, --format <name> Generator format: ts|zod|t3|declaration",
659
1904
  " May be specified multiple times.",
@@ -667,6 +1912,7 @@ var HELP_TEXT = [
667
1912
  " -v, --version Print version",
668
1913
  " -h, --help Show this help"
669
1914
  ].join("\n");
1915
+ var VALIDATION_SUBCOMMANDS = /* @__PURE__ */ new Set(["check", "diff", "doctor"]);
670
1916
  var FORMAT_TO_GENERATOR = {
671
1917
  ts: "typescript",
672
1918
  typescript: "typescript",
@@ -677,30 +1923,59 @@ var FORMAT_TO_GENERATOR = {
677
1923
  function normalizeGenerator(input) {
678
1924
  return FORMAT_TO_GENERATOR[input] ?? FORMAT_TO_GENERATOR[input.toLowerCase()];
679
1925
  }
1926
+ function resolveGenerators(rawFormats, rawGenerators, fallback) {
1927
+ const requested = [...rawFormats ?? [], ...rawGenerators ?? []].map(String);
1928
+ if (requested.length === 0) {
1929
+ return fallback ?? ["typescript", "zod", "t3", "declaration"];
1930
+ }
1931
+ const normalizedGenerators = requested.map((item) => normalizeGenerator(item)).filter((item) => item !== void 0);
1932
+ const invalid = requested.filter((item) => normalizeGenerator(item) === void 0);
1933
+ if (invalid.length > 0) {
1934
+ error(`Unknown format(s): ${invalid.join(", ")}. Valid: ts, zod, t3, declaration`);
1935
+ process.exit(1);
1936
+ }
1937
+ return [...new Set(normalizedGenerators)];
1938
+ }
680
1939
  function getErrorMessage(errorValue) {
681
1940
  if (errorValue instanceof Error) {
682
1941
  return errorValue.message;
683
1942
  }
684
1943
  return inspect(errorValue, { depth: 2 });
685
1944
  }
686
- function resolveConfigRelative(value, configDir) {
687
- if (value === void 0 || path6.isAbsolute(value)) return value;
688
- return path6.resolve(configDir, value);
1945
+ function resolveConfigRelative2(value, configDir) {
1946
+ if (value === void 0 || path13.isAbsolute(value)) return value;
1947
+ return path13.resolve(configDir, value);
689
1948
  }
690
- function applyConfigPaths(config, configDir) {
691
- const input = resolveConfigRelative(config.input, configDir);
692
- const output = resolveConfigRelative(config.output, configDir);
1949
+ function applyConfigPaths2(config, configDir) {
1950
+ let input;
1951
+ if (Array.isArray(config.input)) {
1952
+ input = config.input.map((v) => resolveConfigRelative2(v, configDir) ?? v);
1953
+ } else {
1954
+ input = resolveConfigRelative2(config.input, configDir);
1955
+ }
1956
+ const output = resolveConfigRelative2(config.output, configDir);
693
1957
  return {
694
1958
  ...config,
695
1959
  ...input !== void 0 && { input },
696
1960
  ...output !== void 0 && { output }
697
1961
  };
698
1962
  }
1963
+ async function runValidationSubcommand(subcommand, argv) {
1964
+ const exitCode = await runValidationCommand({ command: subcommand, argv });
1965
+ if (exitCode !== 0) {
1966
+ process.exitCode = exitCode;
1967
+ }
1968
+ }
699
1969
  async function runCli(argv = process.argv.slice(2)) {
700
- const { values } = parseArgs({
1970
+ const maybeSubcommand = argv[0];
1971
+ if (maybeSubcommand !== void 0 && VALIDATION_SUBCOMMANDS.has(maybeSubcommand)) {
1972
+ await runValidationSubcommand(maybeSubcommand, argv.slice(1));
1973
+ return;
1974
+ }
1975
+ const { values } = parseArgs2({
701
1976
  args: argv,
702
1977
  options: {
703
- input: { type: "string", short: "i" },
1978
+ input: { type: "string", short: "i", multiple: true },
704
1979
  output: { type: "string", short: "o" },
705
1980
  generator: { type: "string", short: "g", multiple: true },
706
1981
  format: { type: "string", short: "f", multiple: true },
@@ -719,42 +1994,29 @@ async function runCli(argv = process.argv.slice(2)) {
719
1994
  return;
720
1995
  }
721
1996
  if (values.help === true) {
722
- console.log(HELP_TEXT);
1997
+ console.log(HELP_TEXT2);
723
1998
  return;
724
1999
  }
725
2000
  let fileConfig;
726
2001
  if (values.config === void 0) {
727
2002
  fileConfig = await loadConfig(process.cwd());
728
2003
  } else {
729
- const configPath = path6.resolve(values.config);
730
- const configDir = path6.dirname(configPath);
731
- const mod = await import(pathToFileURL2(configPath).href);
2004
+ const configPath = path13.resolve(values.config);
2005
+ const configDir = path13.dirname(configPath);
2006
+ const mod = await import(pathToFileURL5(configPath).href);
732
2007
  const rawConfig = mod.default;
733
2008
  if (rawConfig) {
734
- fileConfig = applyConfigPaths(rawConfig, configDir);
2009
+ fileConfig = applyConfigPaths2(rawConfig, configDir);
735
2010
  }
736
2011
  }
737
- const input = values.input ?? fileConfig?.input;
2012
+ const cliInput = values.input?.length ? values.input : void 0;
2013
+ const input = cliInput ?? fileConfig?.input;
738
2014
  if (input === void 0) {
739
2015
  error("No input file specified. Use -i <path> or set input in env-typegen.config.ts");
740
2016
  process.exit(1);
741
2017
  }
742
2018
  const output = values.output ?? fileConfig?.output ?? "env.generated.ts";
743
- const rawFormats = values.format;
744
- const rawGenerators = values.generator;
745
- const requested = [...rawFormats ?? [], ...rawGenerators ?? []].map(String);
746
- let generators;
747
- if (requested.length > 0) {
748
- const normalizedGenerators = requested.map((item) => normalizeGenerator(item)).filter((item) => item !== void 0);
749
- const invalid = requested.filter((item) => normalizeGenerator(item) === void 0);
750
- if (invalid.length > 0) {
751
- error(`Unknown format(s): ${invalid.join(", ")}. Valid: ts, zod, t3, declaration`);
752
- process.exit(1);
753
- }
754
- generators = [...new Set(normalizedGenerators)];
755
- } else {
756
- generators = fileConfig?.generators ?? ["typescript"];
757
- }
2019
+ const generators = resolveGenerators(values.format, values.generator, fileConfig?.generators);
758
2020
  const shouldFormat = values["no-format"] === true ? false : fileConfig?.format ?? true;
759
2021
  const useStdout = values.stdout ?? false;
760
2022
  const isDryRun = values["dry-run"] ?? false;
@@ -767,7 +2029,8 @@ async function runCli(argv = process.argv.slice(2)) {
767
2029
  format: shouldFormat,
768
2030
  stdout: useStdout,
769
2031
  dryRun: isDryRun,
770
- silent: isSilent
2032
+ silent: isSilent,
2033
+ ...fileConfig?.inferenceRules !== void 0 && { inferenceRules: fileConfig.inferenceRules }
771
2034
  };
772
2035
  if (shouldWatch) {
773
2036
  startWatch({ inputPath: input, runOptions: options });
@@ -777,7 +2040,7 @@ async function runCli(argv = process.argv.slice(2)) {
777
2040
  }
778
2041
  if (process.argv[1] !== void 0 && (() => {
779
2042
  try {
780
- return realpathSync(path6.resolve(process.argv[1])) === realpathSync(fileURLToPath(import.meta.url));
2043
+ return realpathSync(path13.resolve(process.argv[1])) === realpathSync(fileURLToPath(import.meta.url));
781
2044
  } catch {
782
2045
  return false;
783
2046
  }