@xlameiro/env-typegen 0.1.2 → 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
@@ -99,11 +99,11 @@ var require_picocolors = __commonJS({
99
99
  });
100
100
 
101
101
  // src/cli.ts
102
- import { createRequire } from "module";
103
102
  import { realpathSync } from "fs";
104
- import path7 from "path";
105
- import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "url";
106
- 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";
107
107
 
108
108
  // src/config.ts
109
109
  import { existsSync } from "fs";
@@ -422,29 +422,73 @@ var VALID_ENV_VAR_TYPES = /* @__PURE__ */ new Set([
422
422
  function isEnvVarType(value) {
423
423
  return VALID_ENV_VAR_TYPES.has(value);
424
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
+ }
425
474
  function parseCommentBlock(lines) {
426
- let annotatedType;
427
- let description;
428
- let isRequired = false;
475
+ const state = { isRequired: false };
429
476
  for (const line of lines) {
430
- const content = line.replace(/^#\s*/, "").trimEnd();
431
- if (content.startsWith("@description ")) {
432
- description = content.slice("@description ".length).trim();
433
- } else if (content.startsWith("@type ")) {
434
- const typeStr = content.slice("@type ".length).trim();
435
- if (isEnvVarType(typeStr)) {
436
- annotatedType = typeStr;
437
- }
438
- } else if (content.trim() === "@required") {
439
- isRequired = true;
440
- } else if (content.trim() === "@optional") {
441
- } else if (description === void 0 && content.trim().length > 0) {
442
- description = content.trim();
443
- }
477
+ processAnnotationContent(state, line.replace(/^#\s*/, "").trimEnd());
444
478
  }
445
- const result = { isRequired };
446
- if (annotatedType !== void 0) result.annotatedType = annotatedType;
447
- 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;
448
492
  return result;
449
493
  }
450
494
 
@@ -510,6 +554,18 @@ function parseEnvFileContent(content, filePath, options) {
510
554
  if (currentGroup !== void 0) {
511
555
  parsedVar.group = currentGroup;
512
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
+ }
513
569
  vars.push(parsedVar);
514
570
  commentBlock = [];
515
571
  } else {
@@ -518,6 +574,10 @@ function parseEnvFileContent(content, filePath, options) {
518
574
  }
519
575
  return { filePath, vars, groups };
520
576
  }
577
+ function parseEnvFile(filePath) {
578
+ const content = readFileSync(filePath, "utf8");
579
+ return parseEnvFileContent(content, filePath);
580
+ }
521
581
 
522
582
  // src/utils/file.ts
523
583
  import { mkdir, readFile, writeFile } from "fs/promises";
@@ -651,8 +711,1118 @@ async function runGenerate(options) {
651
711
  }
652
712
  }
653
713
 
654
- // src/watch.ts
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";
655
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
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);
1817
+ }
1818
+ if (params.command === "diff") {
1819
+ return runDiffCommand(parsed);
1820
+ }
1821
+ return runDoctorCommand(parsed);
1822
+ }
1823
+
1824
+ // src/watch.ts
1825
+ import path12 from "path";
656
1826
  import { watch } from "chokidar";
657
1827
  function debounce(fn, delay) {
658
1828
  let timer;
@@ -702,7 +1872,7 @@ function startWatch({ inputPath, runOptions, cwd = process.cwd() }) {
702
1872
  for (const event of ["add", "change", "unlink"]) {
703
1873
  inputWatcher.on(event, handleChange);
704
1874
  }
705
- const configPaths = CONFIG_FILE_NAMES.map((name) => path6.resolve(cwd, name));
1875
+ const configPaths = CONFIG_FILE_NAMES.map((name) => path12.resolve(cwd, name));
706
1876
  const configWatcher = watch(configPaths, { persistent: true, ignoreInitial: true });
707
1877
  for (const event of ["add", "change"]) {
708
1878
  configWatcher.on(event, (eventPath) => void handleConfigChange(eventPath));
@@ -718,11 +1888,14 @@ function startWatch({ inputPath, runOptions, cwd = process.cwd() }) {
718
1888
  // src/cli.ts
719
1889
  var _require = createRequire(import.meta.url);
720
1890
  var VERSION = _require("../package.json").version;
721
- var HELP_TEXT = [
1891
+ var HELP_TEXT2 = [
722
1892
  "env-typegen \u2014 Generate TypeScript types from .env.example",
723
1893
  "",
724
1894
  "Usage:",
725
- " 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]",
726
1899
  "",
727
1900
  "Options:",
728
1901
  " -i, --input <path> Path to .env.example file(s). May be specified multiple times.",
@@ -739,6 +1912,7 @@ var HELP_TEXT = [
739
1912
  " -v, --version Print version",
740
1913
  " -h, --help Show this help"
741
1914
  ].join("\n");
1915
+ var VALIDATION_SUBCOMMANDS = /* @__PURE__ */ new Set(["check", "diff", "doctor"]);
742
1916
  var FORMAT_TO_GENERATOR = {
743
1917
  ts: "typescript",
744
1918
  typescript: "typescript",
@@ -749,32 +1923,56 @@ var FORMAT_TO_GENERATOR = {
749
1923
  function normalizeGenerator(input) {
750
1924
  return FORMAT_TO_GENERATOR[input] ?? FORMAT_TO_GENERATOR[input.toLowerCase()];
751
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
+ }
752
1939
  function getErrorMessage(errorValue) {
753
1940
  if (errorValue instanceof Error) {
754
1941
  return errorValue.message;
755
1942
  }
756
1943
  return inspect(errorValue, { depth: 2 });
757
1944
  }
758
- function resolveConfigRelative(value, configDir) {
759
- if (value === void 0 || path7.isAbsolute(value)) return value;
760
- return path7.resolve(configDir, value);
1945
+ function resolveConfigRelative2(value, configDir) {
1946
+ if (value === void 0 || path13.isAbsolute(value)) return value;
1947
+ return path13.resolve(configDir, value);
761
1948
  }
762
- function applyConfigPaths(config, configDir) {
1949
+ function applyConfigPaths2(config, configDir) {
763
1950
  let input;
764
1951
  if (Array.isArray(config.input)) {
765
- input = config.input.map((v) => resolveConfigRelative(v, configDir) ?? v);
1952
+ input = config.input.map((v) => resolveConfigRelative2(v, configDir) ?? v);
766
1953
  } else {
767
- input = resolveConfigRelative(config.input, configDir);
1954
+ input = resolveConfigRelative2(config.input, configDir);
768
1955
  }
769
- const output = resolveConfigRelative(config.output, configDir);
1956
+ const output = resolveConfigRelative2(config.output, configDir);
770
1957
  return {
771
1958
  ...config,
772
1959
  ...input !== void 0 && { input },
773
1960
  ...output !== void 0 && { output }
774
1961
  };
775
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
+ }
776
1969
  async function runCli(argv = process.argv.slice(2)) {
777
- 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({
778
1976
  args: argv,
779
1977
  options: {
780
1978
  input: { type: "string", short: "i", multiple: true },
@@ -796,19 +1994,19 @@ async function runCli(argv = process.argv.slice(2)) {
796
1994
  return;
797
1995
  }
798
1996
  if (values.help === true) {
799
- console.log(HELP_TEXT);
1997
+ console.log(HELP_TEXT2);
800
1998
  return;
801
1999
  }
802
2000
  let fileConfig;
803
2001
  if (values.config === void 0) {
804
2002
  fileConfig = await loadConfig(process.cwd());
805
2003
  } else {
806
- const configPath = path7.resolve(values.config);
807
- const configDir = path7.dirname(configPath);
808
- 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);
809
2007
  const rawConfig = mod.default;
810
2008
  if (rawConfig) {
811
- fileConfig = applyConfigPaths(rawConfig, configDir);
2009
+ fileConfig = applyConfigPaths2(rawConfig, configDir);
812
2010
  }
813
2011
  }
814
2012
  const cliInput = values.input?.length ? values.input : void 0;
@@ -818,21 +2016,7 @@ async function runCli(argv = process.argv.slice(2)) {
818
2016
  process.exit(1);
819
2017
  }
820
2018
  const output = values.output ?? fileConfig?.output ?? "env.generated.ts";
821
- const rawFormats = values.format;
822
- const rawGenerators = values.generator;
823
- const requested = [...rawFormats ?? [], ...rawGenerators ?? []].map(String);
824
- let generators;
825
- if (requested.length > 0) {
826
- const normalizedGenerators = requested.map((item) => normalizeGenerator(item)).filter((item) => item !== void 0);
827
- const invalid = requested.filter((item) => normalizeGenerator(item) === void 0);
828
- if (invalid.length > 0) {
829
- error(`Unknown format(s): ${invalid.join(", ")}. Valid: ts, zod, t3, declaration`);
830
- process.exit(1);
831
- }
832
- generators = [...new Set(normalizedGenerators)];
833
- } else {
834
- generators = fileConfig?.generators ?? ["typescript", "zod", "t3", "declaration"];
835
- }
2019
+ const generators = resolveGenerators(values.format, values.generator, fileConfig?.generators);
836
2020
  const shouldFormat = values["no-format"] === true ? false : fileConfig?.format ?? true;
837
2021
  const useStdout = values.stdout ?? false;
838
2022
  const isDryRun = values["dry-run"] ?? false;
@@ -856,7 +2040,7 @@ async function runCli(argv = process.argv.slice(2)) {
856
2040
  }
857
2041
  if (process.argv[1] !== void 0 && (() => {
858
2042
  try {
859
- return realpathSync(path7.resolve(process.argv[1])) === realpathSync(fileURLToPath(import.meta.url));
2043
+ return realpathSync(path13.resolve(process.argv[1])) === realpathSync(fileURLToPath(import.meta.url));
860
2044
  } catch {
861
2045
  return false;
862
2046
  }