@xlameiro/env-typegen 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,15 +1,16 @@
1
1
  'use strict';
2
2
 
3
3
  var fs = require('fs');
4
- var path5 = require('path');
4
+ var path6 = require('path');
5
5
  var url = require('url');
6
6
  var promises = require('fs/promises');
7
7
  var prettier = require('prettier');
8
8
  var picocolors = require('picocolors');
9
+ var util = require('util');
9
10
 
10
11
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
12
 
12
- var path5__default = /*#__PURE__*/_interopDefault(path5);
13
+ var path6__default = /*#__PURE__*/_interopDefault(path6);
13
14
 
14
15
  // src/parser/comment-parser.ts
15
16
  var VALID_ENV_VAR_TYPES = /* @__PURE__ */ new Set([
@@ -25,28 +26,73 @@ var VALID_ENV_VAR_TYPES = /* @__PURE__ */ new Set([
25
26
  function isEnvVarType(value) {
26
27
  return VALID_ENV_VAR_TYPES.has(value);
27
28
  }
29
+ function applyTypeAnnotation(state, content) {
30
+ const typeStr = content.slice("@type ".length).trim();
31
+ if (isEnvVarType(typeStr)) state.annotatedType = typeStr;
32
+ }
33
+ function applyEnumAnnotation(state, content) {
34
+ const values = content.slice("@enum ".length).trim().split(",").map((v) => v.trim()).filter((v) => v.length > 0);
35
+ if (values.length > 0) state.enumValues = values;
36
+ }
37
+ function applyMinAnnotation(state, content) {
38
+ const num = Number(content.slice("@min ".length).trim());
39
+ if (Number.isFinite(num)) state.minConstraint = num;
40
+ }
41
+ function applyMaxAnnotation(state, content) {
42
+ const num = Number(content.slice("@max ".length).trim());
43
+ if (Number.isFinite(num)) state.maxConstraint = num;
44
+ }
45
+ function applyRuntimeAnnotation(state, content) {
46
+ const scope = content.slice("@runtime ".length).trim();
47
+ if (scope === "server" || scope === "client" || scope === "edge") {
48
+ state.runtime = scope;
49
+ }
50
+ }
51
+ function processAnnotationContent(state, content) {
52
+ const trimmed = content.trim();
53
+ if (trimmed === "@required") {
54
+ state.isRequired = true;
55
+ return;
56
+ }
57
+ if (trimmed === "@secret") {
58
+ state.isSecret = true;
59
+ return;
60
+ }
61
+ if (trimmed === "@optional") return;
62
+ if (content.startsWith("@description ")) {
63
+ state.description = content.slice("@description ".length).trim();
64
+ } else if (content.startsWith("@type ")) {
65
+ applyTypeAnnotation(state, content);
66
+ } else if (content.startsWith("@enum ")) {
67
+ applyEnumAnnotation(state, content);
68
+ } else if (content.startsWith("@min ")) {
69
+ applyMinAnnotation(state, content);
70
+ } else if (content.startsWith("@max ")) {
71
+ applyMaxAnnotation(state, content);
72
+ } else if (content.startsWith("@runtime ")) {
73
+ applyRuntimeAnnotation(state, content);
74
+ } else if (state.description === void 0 && trimmed.length > 0) {
75
+ state.description = trimmed;
76
+ }
77
+ }
28
78
  function parseCommentBlock(lines) {
29
- let annotatedType;
30
- let description;
31
- let isRequired = false;
79
+ const state = { isRequired: false };
32
80
  for (const line of lines) {
33
- const content = line.replace(/^#\s*/, "").trimEnd();
34
- if (content.startsWith("@description ")) {
35
- description = content.slice("@description ".length).trim();
36
- } else if (content.startsWith("@type ")) {
37
- const typeStr = content.slice("@type ".length).trim();
38
- if (isEnvVarType(typeStr)) {
39
- annotatedType = typeStr;
40
- }
41
- } else if (content.trim() === "@required") {
42
- isRequired = true;
43
- } else if (content.trim() === "@optional") ; else if (description === void 0 && content.trim().length > 0) {
44
- description = content.trim();
45
- }
81
+ processAnnotationContent(state, line.replace(/^#\s*/, "").trimEnd());
46
82
  }
47
- const result = { isRequired };
48
- if (annotatedType !== void 0) result.annotatedType = annotatedType;
49
- if (description !== void 0) result.description = description;
83
+ let constraints;
84
+ if (state.minConstraint !== void 0 || state.maxConstraint !== void 0) {
85
+ constraints = {};
86
+ if (state.minConstraint !== void 0) constraints.min = state.minConstraint;
87
+ if (state.maxConstraint !== void 0) constraints.max = state.maxConstraint;
88
+ }
89
+ const result = { isRequired: state.isRequired };
90
+ if (state.annotatedType !== void 0) result.annotatedType = state.annotatedType;
91
+ if (state.description !== void 0) result.description = state.description;
92
+ if (state.enumValues !== void 0) result.enumValues = state.enumValues;
93
+ if (constraints !== void 0) result.constraints = constraints;
94
+ if (state.runtime !== void 0) result.runtime = state.runtime;
95
+ if (state.isSecret !== void 0) result.isSecret = state.isSecret;
50
96
  return result;
51
97
  }
52
98
 
@@ -103,7 +149,9 @@ var inferenceRules = [
103
149
  {
104
150
  id: "P8_numeric_literal",
105
151
  priority: 8,
106
- match: (_key, value) => /^\d+(\.\d+)?$/.test(value),
152
+ // Non-capturing group with \d keeps the dot/digit boundary unambiguous,
153
+ // eliminating super-linear backtracking (ReDoS-safe).
154
+ match: (_key, value) => /^\d+(?:\.\d+)?$/.test(value),
107
155
  type: "number"
108
156
  },
109
157
  {
@@ -121,7 +169,9 @@ var inferenceRules = [
121
169
  {
122
170
  id: "P11_email_literal",
123
171
  priority: 11,
124
- match: (_key, value) => /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value),
172
+ // Dots are excluded from each domain-segment character class so that the
173
+ // literal \. separators are unambiguous, preventing super-linear backtracking.
174
+ match: (_key, value) => /^[^@\s]+@[^@\s.]+(?:\.[^@\s.]+)+$/.test(value),
125
175
  type: "email"
126
176
  },
127
177
  {
@@ -155,7 +205,50 @@ function inferTypesFromParsedVars(parsed, options) {
155
205
  return parsed.vars.map((item) => inferType(item.key, item.rawValue, options));
156
206
  }
157
207
  var ENV_VAR_RE = /^([A-Z_][A-Z0-9_]*)=(.*)$/;
158
- var SECTION_HEADER_RE = /^#\s+[-=]{3,}\s+(.+?)\s+[-=]{3,}\s*$/;
208
+ var SECTION_HEADER_RE = /^#\s+[-=]{3,}\s+(\S+(?:\s+\S+)*)\s+[-=]{3,}\s*$/;
209
+ function buildParsedVar(params, commentBlock, options) {
210
+ const annotations = parseCommentBlock(commentBlock);
211
+ const extraRules = options?.inferenceRules;
212
+ const inferredType = inferType(
213
+ params.key,
214
+ params.rawValue,
215
+ ...extraRules === void 0 ? [] : [{ extraRules }]
216
+ );
217
+ const isRequired = params.rawValue.length > 0 || annotations.isRequired;
218
+ const isOptional = params.rawValue.length === 0 && !annotations.isRequired;
219
+ const isClientSide = params.key.startsWith("NEXT_PUBLIC_");
220
+ const parsedVar = {
221
+ key: params.key,
222
+ rawValue: params.rawValue,
223
+ inferredType,
224
+ isRequired,
225
+ isOptional,
226
+ isClientSide,
227
+ lineNumber: params.lineNumber
228
+ };
229
+ if (annotations.annotatedType !== void 0) {
230
+ parsedVar.annotatedType = annotations.annotatedType;
231
+ }
232
+ if (annotations.description !== void 0) {
233
+ parsedVar.description = annotations.description;
234
+ }
235
+ if (params.currentGroup !== void 0) {
236
+ parsedVar.group = params.currentGroup;
237
+ }
238
+ if (annotations.enumValues !== void 0) {
239
+ parsedVar.enumValues = annotations.enumValues;
240
+ }
241
+ if (annotations.constraints !== void 0) {
242
+ parsedVar.constraints = annotations.constraints;
243
+ }
244
+ if (annotations.runtime !== void 0) {
245
+ parsedVar.runtime = annotations.runtime;
246
+ }
247
+ if (annotations.isSecret !== void 0) {
248
+ parsedVar.isSecret = annotations.isSecret;
249
+ }
250
+ return parsedVar;
251
+ }
159
252
  function parseEnvFileContent(content, filePath, options) {
160
253
  const lines = content.split("\n");
161
254
  const vars = [];
@@ -185,41 +278,18 @@ function parseEnvFileContent(content, filePath, options) {
185
278
  continue;
186
279
  }
187
280
  const envMatch = ENV_VAR_RE.exec(trimmed);
188
- if (envMatch !== null) {
189
- const key = envMatch[1] ?? "";
190
- const rawValue = envMatch[2] ?? "";
191
- const annotations = parseCommentBlock(commentBlock);
192
- const inferredType = inferType(
193
- key,
194
- rawValue,
195
- ...options?.inferenceRules !== void 0 ? [{ extraRules: options.inferenceRules }] : []
196
- );
197
- const isRequired = rawValue.length > 0 || annotations.isRequired;
198
- const isOptional = rawValue.length === 0 && !annotations.isRequired;
199
- const isClientSide = key.startsWith("NEXT_PUBLIC_");
200
- const parsedVar = {
201
- key,
202
- rawValue,
203
- inferredType,
204
- isRequired,
205
- isOptional,
206
- isClientSide,
207
- lineNumber
208
- };
209
- if (annotations.annotatedType !== void 0) {
210
- parsedVar.annotatedType = annotations.annotatedType;
211
- }
212
- if (annotations.description !== void 0) {
213
- parsedVar.description = annotations.description;
214
- }
215
- if (currentGroup !== void 0) {
216
- parsedVar.group = currentGroup;
217
- }
218
- vars.push(parsedVar);
219
- commentBlock = [];
220
- } else {
281
+ if (envMatch === null) {
221
282
  commentBlock = [];
283
+ continue;
222
284
  }
285
+ vars.push(
286
+ buildParsedVar(
287
+ { key: envMatch[1] ?? "", rawValue: envMatch[2] ?? "", lineNumber, currentGroup },
288
+ commentBlock,
289
+ options
290
+ )
291
+ );
292
+ commentBlock = [];
223
293
  }
224
294
  return { filePath, vars, groups };
225
295
  }
@@ -235,15 +305,17 @@ function toTsType(envVarType) {
235
305
  function generateTypeScriptTypes(parsed) {
236
306
  const clientVars = parsed.vars.filter((v) => v.isClientSide);
237
307
  const hasClientVars = clientVars.length > 0;
238
- const fileName = path5__default.default.basename(parsed.filePath);
308
+ const fileName = path6__default.default.basename(parsed.filePath);
239
309
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
240
310
  const lines = [];
241
- lines.push("// Generated by env-typegen \u2014 do not edit manually");
242
- lines.push(`// Source: ${fileName}`);
243
- lines.push(`// Generated at: ${timestamp}`);
244
- lines.push("");
245
- lines.push("declare namespace NodeJS {");
246
- lines.push(" interface ProcessEnv {");
311
+ lines.push(
312
+ "// Generated by env-typegen \u2014 do not edit manually",
313
+ `// Source: ${fileName}`,
314
+ `// Generated at: ${timestamp}`,
315
+ "",
316
+ "declare namespace NodeJS {",
317
+ " interface ProcessEnv {"
318
+ );
247
319
  for (const variable of parsed.vars) {
248
320
  const effectiveType = variable.annotatedType ?? variable.inferredType;
249
321
  const optional = variable.isOptional ? "?" : "";
@@ -258,10 +330,7 @@ function generateTypeScriptTypes(parsed) {
258
330
  }
259
331
  lines.push(propLine);
260
332
  }
261
- lines.push(" }");
262
- lines.push("}");
263
- lines.push("");
264
- lines.push("export type EnvVars = {");
333
+ lines.push(" }", "}", "", "export type EnvVars = {");
265
334
  for (const variable of parsed.vars) {
266
335
  const effectiveType = variable.annotatedType ?? variable.inferredType;
267
336
  const tsType = toTsType(effectiveType);
@@ -271,28 +340,34 @@ function generateTypeScriptTypes(parsed) {
271
340
  lines.push("};");
272
341
  if (hasClientVars) {
273
342
  const clientKeyUnion = clientVars.map((v) => `"${v.key}"`).join(" | ");
274
- lines.push("");
275
- lines.push(`export type ServerEnvVars = Omit<EnvVars, ${clientKeyUnion}>;`);
276
- lines.push(`export type ClientEnvVars = Pick<EnvVars, ${clientKeyUnion}>;`);
343
+ lines.push(
344
+ "",
345
+ `export type ServerEnvVars = Omit<EnvVars, ${clientKeyUnion}>;`,
346
+ `export type ClientEnvVars = Pick<EnvVars, ${clientKeyUnion}>;`
347
+ );
277
348
  }
278
349
  return lines.join("\n") + "\n";
279
350
  }
280
351
  function generateEnvValidation(parsed) {
281
352
  const required = parsed.vars.filter((v) => v.isRequired).map((v) => v.key);
282
353
  const lines = [];
283
- lines.push("// Generated by env-typegen \u2014 do not edit manually");
284
- lines.push("");
285
- lines.push("export function validateEnv(): void {");
354
+ lines.push(
355
+ "// Generated by env-typegen \u2014 do not edit manually",
356
+ "",
357
+ "export function validateEnv(): void {"
358
+ );
286
359
  if (required.length === 0) {
287
360
  lines.push(" // No required environment variables defined");
288
361
  } else {
289
362
  const keyList = required.map((k) => `"${k}"`).join(", ");
290
- lines.push(` const required = [${keyList}];`);
291
- lines.push(" for (const key of required) {");
292
- lines.push(" if (!process.env[key]) {");
293
- lines.push(" throw new Error(`Missing required environment variable: ${key}`);");
294
- lines.push(" }");
295
- lines.push(" }");
363
+ lines.push(
364
+ ` const required = [${keyList}];`,
365
+ " for (const key of required) {",
366
+ " if (!process.env[key]) {",
367
+ " throw new Error(`Missing required environment variable: ${key}`);",
368
+ " }",
369
+ " }"
370
+ );
296
371
  }
297
372
  lines.push("}");
298
373
  return lines.join("\n") + "\n";
@@ -310,37 +385,40 @@ function generateZodSchema(parsed) {
310
385
  const serverVars = parsed.vars.filter((v) => !v.isClientSide);
311
386
  const clientVars = parsed.vars.filter((v) => v.isClientSide);
312
387
  const lines = [];
313
- lines.push("// Generated by env-typegen \u2014 do not edit manually");
314
- lines.push('import { z } from "zod";');
315
- lines.push("");
316
- lines.push("export const serverEnvSchema = z.object({");
388
+ lines.push(
389
+ "// Generated by env-typegen \u2014 do not edit manually",
390
+ 'import { z } from "zod";',
391
+ "",
392
+ "export const serverEnvSchema = z.object({"
393
+ );
317
394
  for (const variable of serverVars) {
318
395
  const effectiveType = variable.annotatedType ?? variable.inferredType;
319
396
  const zodExpr = variable.isOptional ? `${toZodType(effectiveType)}.optional()` : toZodType(effectiveType);
320
397
  lines.push(` ${variable.key}: ${zodExpr},`);
321
398
  }
322
- lines.push("});");
323
- lines.push("");
324
- lines.push("export const clientEnvSchema = z.object({");
399
+ lines.push("});", "", "export const clientEnvSchema = z.object({");
325
400
  for (const variable of clientVars) {
326
401
  const effectiveType = variable.annotatedType ?? variable.inferredType;
327
402
  const zodExpr = variable.isOptional ? `${toZodType(effectiveType)}.optional()` : toZodType(effectiveType);
328
403
  lines.push(` ${variable.key}: ${zodExpr},`);
329
404
  }
330
- lines.push("});");
331
- lines.push("");
332
- lines.push("export const envSchema = serverEnvSchema.merge(clientEnvSchema);");
333
- lines.push("export type Env = z.infer<typeof envSchema>;");
405
+ lines.push(
406
+ "});",
407
+ "",
408
+ "export const envSchema = serverEnvSchema.merge(clientEnvSchema);",
409
+ "export type Env = z.infer<typeof envSchema>;"
410
+ );
334
411
  return lines.join("\n") + "\n";
335
412
  }
336
413
  function generateDeclaration(parsed) {
337
- const fileName = path5__default.default.basename(parsed.filePath);
338
- const lines = [];
339
- lines.push("// Generated by env-typegen \u2014 do not edit manually");
340
- lines.push(`// Source: ${fileName}`);
341
- lines.push("");
342
- lines.push("declare namespace NodeJS {");
343
- lines.push(" interface ProcessEnv {");
414
+ const fileName = path6__default.default.basename(parsed.filePath);
415
+ const lines = [
416
+ "// Generated by env-typegen \u2014 do not edit manually",
417
+ `// Source: ${fileName}`,
418
+ "",
419
+ "declare namespace NodeJS {",
420
+ " interface ProcessEnv {"
421
+ ];
344
422
  for (const variable of parsed.vars) {
345
423
  const effectiveType = variable.annotatedType ?? variable.inferredType;
346
424
  const optional = variable.isOptional ? "?" : "";
@@ -355,12 +433,14 @@ function generateDeclaration(parsed) {
355
433
  }
356
434
  lines.push(propLine);
357
435
  }
358
- lines.push(" }");
359
- lines.push("}");
436
+ lines.push(" }", "}");
360
437
  return lines.join("\n") + "\n";
361
438
  }
362
439
 
363
440
  // src/generators/t3-generator.ts
441
+ function escapeJsStringLiteral(value) {
442
+ return value.replaceAll("\\", String.raw`\\`).replaceAll('"', String.raw`\"`);
443
+ }
364
444
  function toT3ZodType(envVarType) {
365
445
  if (envVarType === "number") return "z.coerce.number()";
366
446
  if (envVarType === "boolean") return "z.coerce.boolean()";
@@ -368,65 +448,93 @@ function toT3ZodType(envVarType) {
368
448
  if (envVarType === "email") return "z.string().email()";
369
449
  return "z.string()";
370
450
  }
451
+ function buildZodExpr(variable) {
452
+ const effectiveType = variable.annotatedType ?? variable.inferredType;
453
+ let zodExpr = toT3ZodType(effectiveType);
454
+ if (variable.description !== void 0) {
455
+ zodExpr += `.describe("${escapeJsStringLiteral(variable.description)}")`;
456
+ }
457
+ if (variable.isOptional) {
458
+ zodExpr += ".optional()";
459
+ }
460
+ return zodExpr;
461
+ }
371
462
  function generateT3Env(parsed) {
372
463
  const serverVars = parsed.vars.filter((v) => !v.isClientSide);
373
464
  const clientVars = parsed.vars.filter((v) => v.isClientSide);
374
- const lines = [];
375
- lines.push("// Generated by env-typegen \u2014 do not edit manually");
376
- lines.push('import { createEnv } from "@t3-oss/env-nextjs";');
377
- lines.push('import { z } from "zod";');
378
- lines.push("");
379
- lines.push("export const env = createEnv({");
465
+ const lines = [
466
+ "// Generated by env-typegen \u2014 do not edit manually",
467
+ 'import { createEnv } from "@t3-oss/env-nextjs";',
468
+ 'import { z } from "zod";',
469
+ "",
470
+ "export const env = createEnv({"
471
+ ];
380
472
  if (serverVars.length > 0) {
381
- lines.push(" server: {");
382
- for (const variable of serverVars) {
383
- const effectiveType = variable.annotatedType ?? variable.inferredType;
384
- let zodExpr = toT3ZodType(effectiveType);
385
- if (variable.description !== void 0) {
386
- zodExpr += `.describe("${variable.description.replace(/"/g, '\\"')}")`;
387
- }
388
- if (variable.isOptional) {
389
- zodExpr += ".optional()";
390
- }
391
- lines.push(` ${variable.key}: ${zodExpr},`);
392
- }
393
- lines.push(" },");
473
+ lines.push(
474
+ " server: {",
475
+ ...serverVars.map((v) => ` ${v.key}: ${buildZodExpr(v)},`),
476
+ " },"
477
+ );
394
478
  }
395
479
  if (clientVars.length > 0) {
396
- lines.push(" client: {");
397
- for (const variable of clientVars) {
398
- const effectiveType = variable.annotatedType ?? variable.inferredType;
399
- let zodExpr = toT3ZodType(effectiveType);
400
- if (variable.description !== void 0) {
401
- zodExpr += `.describe("${variable.description.replace(/"/g, '\\"')}")`;
402
- }
403
- if (variable.isOptional) {
404
- zodExpr += ".optional()";
405
- }
406
- lines.push(` ${variable.key}: ${zodExpr},`);
407
- }
408
- lines.push(" },");
409
- }
410
- lines.push(" runtimeEnv: {");
411
- for (const variable of parsed.vars) {
412
- lines.push(` ${variable.key}: process.env.${variable.key},`);
480
+ lines.push(
481
+ " client: {",
482
+ ...clientVars.map((v) => ` ${v.key}: ${buildZodExpr(v)},`),
483
+ " },"
484
+ );
413
485
  }
414
- lines.push(" },");
415
- lines.push("});");
486
+ lines.push(
487
+ " runtimeEnv: {",
488
+ ...parsed.vars.map((v) => ` ${v.key}: process.env.${v.key},`),
489
+ " },",
490
+ "});"
491
+ );
416
492
  return lines.join("\n") + "\n";
417
493
  }
494
+ var CONTRACT_FILE_NAMES = [
495
+ "env.contract.ts",
496
+ "env.contract.mjs",
497
+ "env.contract.js"
498
+ ];
499
+ function defineContract(contract) {
500
+ return contract;
501
+ }
502
+ async function loadContract(cwd = process.cwd()) {
503
+ for (const name of CONTRACT_FILE_NAMES) {
504
+ const filePath = path6__default.default.resolve(cwd, name);
505
+ if (fs.existsSync(filePath)) {
506
+ const fileUrl = url.pathToFileURL(filePath).href;
507
+ const mod = await import(fileUrl);
508
+ return mod.default;
509
+ }
510
+ }
511
+ return void 0;
512
+ }
418
513
  var CONFIG_FILE_NAMES = [
419
- "env-typegen.config.ts",
420
514
  "env-typegen.config.mjs",
421
- "env-typegen.config.js"
515
+ "env-typegen.config.js",
516
+ "env-typegen.config.ts"
422
517
  ];
423
518
  function defineConfig(config) {
424
519
  return config;
425
520
  }
426
521
  async function loadConfig(cwd = process.cwd()) {
427
522
  for (const name of CONFIG_FILE_NAMES) {
428
- const filePath = path5__default.default.resolve(cwd, name);
523
+ const filePath = path6__default.default.resolve(cwd, name);
429
524
  if (fs.existsSync(filePath)) {
525
+ if (filePath.endsWith(".ts")) {
526
+ throw new Error(
527
+ `Config file "${name}" was found but TypeScript files cannot be loaded directly at runtime.
528
+ Rename it to "env-typegen.config.mjs" and use ESM export syntax:
529
+
530
+ // env-typegen.config.mjs
531
+ import { defineConfig } from "@xlameiro/env-typegen";
532
+ export default defineConfig({ input: ".env.example" });
533
+
534
+ Tip: keep env-typegen.config.ts for IDE autocompletion and create a sibling
535
+ env-typegen.config.mjs for runtime loading.`
536
+ );
537
+ }
430
538
  const fileUrl = url.pathToFileURL(filePath).href;
431
539
  const mod = await import(fileUrl);
432
540
  return mod.default;
@@ -435,11 +543,11 @@ async function loadConfig(cwd = process.cwd()) {
435
543
  return void 0;
436
544
  }
437
545
  async function readEnvFile(filePath) {
438
- return promises.readFile(path5__default.default.resolve(filePath), "utf8");
546
+ return promises.readFile(path6__default.default.resolve(filePath), "utf8");
439
547
  }
440
548
  async function writeOutput(filePath, content) {
441
- const resolved = path5__default.default.resolve(filePath);
442
- await promises.mkdir(path5__default.default.dirname(resolved), { recursive: true });
549
+ const resolved = path6__default.default.resolve(filePath);
550
+ await promises.mkdir(path6__default.default.dirname(resolved), { recursive: true });
443
551
  await promises.writeFile(resolved, content, "utf8");
444
552
  }
445
553
  async function formatOutput(content, parser = "typescript") {
@@ -453,6 +561,15 @@ async function formatOutput(content, parser = "typescript") {
453
561
  return content;
454
562
  }
455
563
  }
564
+ function log(message) {
565
+ console.log(message);
566
+ }
567
+ function warn(message) {
568
+ console.warn(picocolors.yellow(`\u26A0 ${message}`));
569
+ }
570
+ function error(message) {
571
+ console.error(picocolors.red(`\u2716 ${message}`));
572
+ }
456
573
  function success(message) {
457
574
  console.log(picocolors.green(`\u2714 ${message}`));
458
575
  }
@@ -460,17 +577,17 @@ function success(message) {
460
577
  // src/pipeline.ts
461
578
  function deriveOutputPath(base, generator, isSingle) {
462
579
  if (isSingle) return base;
463
- const ext = path5__default.default.extname(base);
580
+ const ext = path6__default.default.extname(base);
464
581
  const noExt = ext.length > 0 ? base.slice(0, -ext.length) : base;
465
582
  const baseExt = ext.length > 0 ? ext : ".ts";
466
583
  const outExt = generator === "declaration" ? ".d.ts" : baseExt;
467
584
  return `${noExt}.${generator}${outExt}`;
468
585
  }
469
586
  function deriveOutputBaseForInput(output, inputPath) {
470
- const dir = path5__default.default.dirname(output);
471
- const ext = path5__default.default.extname(output);
472
- const stem = path5__default.default.basename(inputPath, path5__default.default.extname(inputPath));
473
- return path5__default.default.join(dir, `${stem}${ext}`);
587
+ const dir = path6__default.default.dirname(output);
588
+ const ext = path6__default.default.extname(output);
589
+ const stem = path6__default.default.basename(inputPath, path6__default.default.extname(inputPath));
590
+ return path6__default.default.join(dir, `${stem}${ext}`);
474
591
  }
475
592
  function buildOutput(generator, parsed) {
476
593
  switch (generator) {
@@ -530,7 +647,7 @@ async function runGenerate(options) {
530
647
  const parsed = parseEnvFileContent(
531
648
  content,
532
649
  inputPath,
533
- inferenceRules2 !== void 0 ? { inferenceRules: inferenceRules2 } : void 0
650
+ inferenceRules2 === void 0 ? void 0 : { inferenceRules: inferenceRules2 }
534
651
  );
535
652
  for (const generator of generators) {
536
653
  let generated = buildOutput(generator, parsed);
@@ -551,7 +668,1450 @@ async function runGenerate(options) {
551
668
  }
552
669
  }
553
670
 
671
+ // src/validator/contract-validator.ts
672
+ var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
673
+ function buildExpected(entry) {
674
+ if (entry.enumValues !== void 0 && entry.enumValues.length > 0) {
675
+ return { type: "enum", values: entry.enumValues };
676
+ }
677
+ switch (entry.expectedType) {
678
+ case "number":
679
+ return {
680
+ type: "number",
681
+ ...entry.constraints?.min !== void 0 && { min: entry.constraints.min },
682
+ ...entry.constraints?.max !== void 0 && { max: entry.constraints.max }
683
+ };
684
+ case "boolean":
685
+ return { type: "boolean" };
686
+ case "url":
687
+ return { type: "url" };
688
+ case "email":
689
+ return { type: "email" };
690
+ case "json":
691
+ return { type: "json" };
692
+ case "semver":
693
+ return { type: "semver" };
694
+ case "unknown":
695
+ return { type: "unknown" };
696
+ default:
697
+ return { type: "string" };
698
+ }
699
+ }
700
+ function checkSecretExposed(variable, entry, environment) {
701
+ if (entry.isSecret !== true) return void 0;
702
+ if (variable.rawValue === "") return void 0;
703
+ return {
704
+ code: "ENV_SECRET_EXPOSED",
705
+ key: variable.key,
706
+ expected: buildExpected(entry),
707
+ environment,
708
+ severity: "error"
709
+ };
710
+ }
711
+ function checkInvalidType(variable, entry, environment) {
712
+ const value = variable.rawValue;
713
+ const expected = buildExpected(entry);
714
+ switch (entry.expectedType) {
715
+ case "number": {
716
+ if (!Number.isFinite(Number(value))) {
717
+ return {
718
+ code: "ENV_INVALID_TYPE",
719
+ key: variable.key,
720
+ expected,
721
+ environment,
722
+ severity: "error"
723
+ };
724
+ }
725
+ break;
726
+ }
727
+ case "boolean": {
728
+ if (value !== "true" && value !== "false") {
729
+ return {
730
+ code: "ENV_INVALID_TYPE",
731
+ key: variable.key,
732
+ expected,
733
+ environment,
734
+ severity: "error"
735
+ };
736
+ }
737
+ break;
738
+ }
739
+ case "url": {
740
+ try {
741
+ new URL(value);
742
+ } catch {
743
+ return {
744
+ code: "ENV_INVALID_TYPE",
745
+ key: variable.key,
746
+ expected,
747
+ environment,
748
+ severity: "error"
749
+ };
750
+ }
751
+ break;
752
+ }
753
+ case "email": {
754
+ if (!value.includes("@") || !value.includes(".")) {
755
+ return {
756
+ code: "ENV_INVALID_TYPE",
757
+ key: variable.key,
758
+ expected,
759
+ environment,
760
+ severity: "error"
761
+ };
762
+ }
763
+ break;
764
+ }
765
+ case "semver": {
766
+ if (!SEMVER_RE.test(value)) {
767
+ return {
768
+ code: "ENV_INVALID_TYPE",
769
+ key: variable.key,
770
+ expected,
771
+ environment,
772
+ severity: "error"
773
+ };
774
+ }
775
+ break;
776
+ }
777
+ }
778
+ return void 0;
779
+ }
780
+ function checkNumericConstraints(variable, entry, environment) {
781
+ if (entry.expectedType !== "number" || entry.constraints === void 0) return void 0;
782
+ const num = Number(variable.rawValue);
783
+ if (!Number.isFinite(num)) return void 0;
784
+ const { min, max } = entry.constraints;
785
+ const outsideMin = min !== void 0 && num < min;
786
+ const outsideMax = max !== void 0 && num > max;
787
+ if (outsideMin || outsideMax) {
788
+ return {
789
+ code: "ENV_INVALID_VALUE",
790
+ key: variable.key,
791
+ expected: buildExpected(entry),
792
+ environment,
793
+ severity: "error"
794
+ };
795
+ }
796
+ return void 0;
797
+ }
798
+ function checkInvalidValue(variable, entry, environment) {
799
+ const value = variable.rawValue;
800
+ if (entry.enumValues !== void 0 && entry.enumValues.length > 0) {
801
+ if (!entry.enumValues.includes(value)) {
802
+ return {
803
+ code: "ENV_INVALID_VALUE",
804
+ key: variable.key,
805
+ expected: { type: "enum", values: entry.enumValues },
806
+ environment,
807
+ severity: "error"
808
+ };
809
+ }
810
+ return void 0;
811
+ }
812
+ return checkNumericConstraints(variable, entry, environment);
813
+ }
814
+ function checkVariable(variable, context) {
815
+ const { contractByName, environment, strict } = context;
816
+ const entry = contractByName.get(variable.key);
817
+ if (entry === void 0) {
818
+ return [
819
+ {
820
+ code: "ENV_EXTRA",
821
+ key: variable.key,
822
+ expected: { type: "string" },
823
+ environment,
824
+ severity: strict ? "error" : "warning"
825
+ }
826
+ ];
827
+ }
828
+ const found = [];
829
+ const secretIssue = checkSecretExposed(variable, entry, environment);
830
+ if (secretIssue !== void 0) found.push(secretIssue);
831
+ if (variable.rawValue === "") return found;
832
+ const typeIssue = checkInvalidType(variable, entry, environment);
833
+ if (typeIssue !== void 0) {
834
+ found.push(typeIssue);
835
+ return found;
836
+ }
837
+ const valueIssue = checkInvalidValue(variable, entry, environment);
838
+ if (valueIssue !== void 0) found.push(valueIssue);
839
+ return found;
840
+ }
841
+ function validateContract(parsed, contract, opts = {}) {
842
+ const environment = opts.environment ?? "local";
843
+ const strict = opts.strict ?? true;
844
+ const issues = [];
845
+ const contractByName = new Map(
846
+ contract.vars.map((entry) => [entry.name, entry])
847
+ );
848
+ const parsedByKey = new Map(parsed.vars.map((v) => [v.key, v]));
849
+ for (const entry of contract.vars) {
850
+ if (!entry.required) continue;
851
+ if (parsedByKey.has(entry.name)) continue;
852
+ issues.push({
853
+ code: "ENV_MISSING",
854
+ key: entry.name,
855
+ expected: buildExpected(entry),
856
+ environment,
857
+ severity: "error"
858
+ });
859
+ }
860
+ const context = { contractByName, environment, strict };
861
+ for (const variable of parsed.vars) {
862
+ const perVarIssues = checkVariable(variable, context);
863
+ for (const issue of perVarIssues) {
864
+ issues.push(issue);
865
+ }
866
+ }
867
+ return { issues };
868
+ }
869
+
870
+ // src/reporting/ci-reporter.ts
871
+ var ISSUE_TYPE_MAP = {
872
+ ENV_MISSING: "Required variable is missing from the env file",
873
+ ENV_EXTRA: "Variable is present in the env file but not declared in the contract",
874
+ ENV_INVALID_TYPE: "Value cannot be coerced to the declared type",
875
+ ENV_INVALID_VALUE: "Value fails a constraint check (enum, min, max)",
876
+ ENV_CONFLICT: "Variable has inconsistent values across environments",
877
+ ENV_SECRET_EXPOSED: "Secret variable has a non-empty value in a public env file"
878
+ };
879
+ function toIssue(vi) {
880
+ const issue = {
881
+ code: vi.code,
882
+ type: ISSUE_TYPE_MAP[vi.code],
883
+ key: vi.key,
884
+ expected: vi.expected,
885
+ environment: vi.environment,
886
+ severity: vi.severity,
887
+ value: null
888
+ };
889
+ if (vi.received !== void 0) {
890
+ issue.received = vi.received;
891
+ }
892
+ return issue;
893
+ }
894
+ function buildCiReport(result, meta) {
895
+ const issues = result.issues.map(toIssue);
896
+ const errors = issues.filter((i) => i.severity === "error").length;
897
+ const warnings = issues.filter((i) => i.severity === "warning").length;
898
+ return {
899
+ schemaVersion: 1,
900
+ status: errors > 0 ? "fail" : "ok",
901
+ summary: { errors, warnings },
902
+ issues,
903
+ meta
904
+ };
905
+ }
906
+ function formatCiReport(report, opts) {
907
+ return JSON.stringify(report, null, opts?.pretty === true ? 2 : void 0);
908
+ }
909
+ async function resolveContract(contractPath, cwd) {
910
+ if (contractPath !== void 0) {
911
+ const absPath = path6__default.default.resolve(cwd ?? process.cwd(), contractPath);
912
+ if (!fs.existsSync(absPath)) {
913
+ throw new Error(`Contract file not found: ${absPath}`);
914
+ }
915
+ const mod = await import(url.pathToFileURL(absPath).href);
916
+ if (mod.default === void 0) {
917
+ throw new Error(`Contract file has no default export: ${absPath}`);
918
+ }
919
+ return mod.default;
920
+ }
921
+ const contract = await loadContract(cwd);
922
+ if (contract === void 0) {
923
+ throw new Error("No contract file found");
924
+ }
925
+ return contract;
926
+ }
927
+ function outputJson(result, input, pretty) {
928
+ const report = buildCiReport(result, {
929
+ env: input,
930
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
931
+ });
932
+ const formatOpts = {};
933
+ if (pretty) formatOpts.pretty = true;
934
+ log(formatCiReport(report, formatOpts));
935
+ }
936
+ function outputHuman(result) {
937
+ if (result.issues.length === 0) {
938
+ success("No issues found");
939
+ return;
940
+ }
941
+ for (const issue of result.issues) {
942
+ const msg = `[${issue.code}] ${issue.key}`;
943
+ if (issue.severity === "error") {
944
+ error(msg);
945
+ } else {
946
+ warn(msg);
947
+ }
948
+ }
949
+ }
950
+ async function runCheck(opts) {
951
+ const contract = await resolveContract(opts.contract, opts.cwd);
952
+ const parsed = parseEnvFile(opts.input);
953
+ const validatorOpts = {};
954
+ if (opts.environment !== void 0) validatorOpts.environment = opts.environment;
955
+ if (opts.strict !== void 0) validatorOpts.strict = opts.strict;
956
+ const result = validateContract(parsed, contract, validatorOpts);
957
+ const hasErrors = result.issues.some((issue) => issue.severity === "error");
958
+ const status = hasErrors ? "fail" : "ok";
959
+ if (!opts.silent) {
960
+ if (opts.json) {
961
+ outputJson(result, opts.input, opts.pretty);
962
+ } else {
963
+ outputHuman(result);
964
+ }
965
+ }
966
+ return status;
967
+ }
968
+ function isRecord(value) {
969
+ return typeof value === "object" && value !== null && !Array.isArray(value);
970
+ }
971
+ function readEntryValue(entry, keys) {
972
+ for (const key of keys) {
973
+ const value = entry[key];
974
+ if (typeof value === "string") return value;
975
+ }
976
+ return void 0;
977
+ }
978
+ function getVercelEntries(value) {
979
+ if (Array.isArray(value)) return value;
980
+ if (isRecord(value) && Array.isArray(value.envs)) return value.envs;
981
+ return [];
982
+ }
983
+ function parseVercelPayload(value) {
984
+ const entries = getVercelEntries(value);
985
+ const result = {};
986
+ for (const entry of entries) {
987
+ if (!isRecord(entry)) continue;
988
+ const key = readEntryValue(entry, ["key", "name"]);
989
+ if (key === void 0) continue;
990
+ const envValue = readEntryValue(entry, ["value", "targetValue", "content"]) ?? "";
991
+ result[key] = envValue;
992
+ }
993
+ return result;
994
+ }
995
+ function getCloudflareEntries(value) {
996
+ if (Array.isArray(value)) return value;
997
+ if (isRecord(value) && Array.isArray(value.result)) return value.result;
998
+ return [];
999
+ }
1000
+ function parseCloudflarePayload(value) {
1001
+ const entries = getCloudflareEntries(value);
1002
+ const result = {};
1003
+ for (const entry of entries) {
1004
+ if (!isRecord(entry)) continue;
1005
+ const key = readEntryValue(entry, ["name", "key"]);
1006
+ if (key === void 0) continue;
1007
+ const envValue = readEntryValue(entry, ["text", "value", "secret"]) ?? "";
1008
+ result[key] = envValue;
1009
+ }
1010
+ return result;
1011
+ }
1012
+ function parseAwsPayload(value) {
1013
+ const entries = isRecord(value) && Array.isArray(value.Parameters) ? value.Parameters : [];
1014
+ const result = {};
1015
+ for (const entry of entries) {
1016
+ if (!isRecord(entry)) continue;
1017
+ const name = readEntryValue(entry, ["Name", "name"]);
1018
+ if (name === void 0) continue;
1019
+ const key = name.split("/").findLast((part) => part.length > 0) ?? name;
1020
+ const envValue = readEntryValue(entry, ["Value", "value"]) ?? "";
1021
+ result[key] = envValue;
1022
+ }
1023
+ return result;
1024
+ }
1025
+ function parseProviderPayload(provider, value) {
1026
+ if (provider === "vercel") return parseVercelPayload(value);
1027
+ if (provider === "cloudflare") return parseCloudflarePayload(value);
1028
+ return parseAwsPayload(value);
1029
+ }
1030
+ async function loadCloudSource(options) {
1031
+ const resolvedPath = path6__default.default.resolve(options.filePath);
1032
+ const raw = await promises.readFile(resolvedPath, "utf8");
1033
+ const parsed = JSON.parse(raw);
1034
+ return parseProviderPayload(options.provider, parsed);
1035
+ }
1036
+ var SECRET_KEY_RE = /(SECRET|TOKEN|PASSWORD|PRIVATE|API_KEY|ACCESS_KEY|CLIENT_SECRET)/i;
1037
+ var CONTRACT_FILE_NAMES2 = ["env.contract.ts", "env.contract.mjs", "env.contract.js"];
1038
+ function isRecord2(value) {
1039
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1040
+ }
1041
+ function isExpected(value) {
1042
+ if (!isRecord2(value)) return false;
1043
+ const typeValue = value.type;
1044
+ if (typeof typeValue !== "string") return false;
1045
+ const validTypes = /* @__PURE__ */ new Set([
1046
+ "string",
1047
+ "number",
1048
+ "boolean",
1049
+ "enum",
1050
+ "url",
1051
+ "email",
1052
+ "json",
1053
+ "semver",
1054
+ "unknown"
1055
+ ]);
1056
+ if (!validTypes.has(typeValue)) return false;
1057
+ if (typeValue === "enum") {
1058
+ return Array.isArray(value.values) && value.values.every((item) => typeof item === "string");
1059
+ }
1060
+ return true;
1061
+ }
1062
+ function isEnvContractVariable(value) {
1063
+ if (!isRecord2(value)) return false;
1064
+ if (!isExpected(value.expected)) return false;
1065
+ if (typeof value.required !== "boolean") return false;
1066
+ if (typeof value.clientSide !== "boolean") return false;
1067
+ if (value.description !== void 0 && typeof value.description !== "string") return false;
1068
+ if (value.secret !== void 0 && typeof value.secret !== "boolean") return false;
1069
+ return true;
1070
+ }
1071
+ function isEnvContract(value) {
1072
+ if (!isRecord2(value)) return false;
1073
+ if (value.schemaVersion !== 1) return false;
1074
+ if (!isRecord2(value.variables)) return false;
1075
+ return Object.values(value.variables).every((item) => isEnvContractVariable(item));
1076
+ }
1077
+ function isLegacyContract(value) {
1078
+ if (!isRecord2(value)) return false;
1079
+ if (!Array.isArray(value.vars)) return false;
1080
+ return value.vars.every((entry) => isRecord2(entry) && typeof entry.name === "string");
1081
+ }
1082
+ function mapEnvVarTypeToExpected(type) {
1083
+ if (type === "number") return { type: "number" };
1084
+ if (type === "boolean") return { type: "boolean" };
1085
+ if (type === "url") return { type: "url" };
1086
+ if (type === "email") return { type: "email" };
1087
+ if (type === "json") return { type: "json" };
1088
+ if (type === "semver") return { type: "semver" };
1089
+ if (type === "unknown") return { type: "unknown" };
1090
+ return { type: "string" };
1091
+ }
1092
+ function shouldTreatAsSecret(key) {
1093
+ return SECRET_KEY_RE.test(key);
1094
+ }
1095
+ function toExpectedFromLegacyEntry(entry) {
1096
+ if (entry.enumValues !== void 0 && entry.enumValues.length > 0) {
1097
+ return { type: "enum", values: entry.enumValues };
1098
+ }
1099
+ if (entry.expectedType === "number") {
1100
+ return {
1101
+ type: "number",
1102
+ ...entry.constraints?.min !== void 0 && { min: entry.constraints.min },
1103
+ ...entry.constraints?.max !== void 0 && { max: entry.constraints.max }
1104
+ };
1105
+ }
1106
+ if (entry.expectedType === "boolean") return { type: "boolean" };
1107
+ if (entry.expectedType === "url") return { type: "url" };
1108
+ if (entry.expectedType === "email") return { type: "email" };
1109
+ if (entry.expectedType === "json") return { type: "json" };
1110
+ if (entry.expectedType === "semver") return { type: "semver" };
1111
+ if (entry.expectedType === "unknown") return { type: "unknown" };
1112
+ return { type: "string" };
1113
+ }
1114
+ function convertLegacyContract(contract) {
1115
+ const variables = {};
1116
+ for (const entry of contract.vars) {
1117
+ const clientSide = entry.runtime === "client" || entry.name.startsWith("NEXT_PUBLIC_");
1118
+ variables[entry.name] = {
1119
+ expected: toExpectedFromLegacyEntry(entry),
1120
+ required: entry.required,
1121
+ clientSide,
1122
+ ...entry.description !== void 0 && { description: entry.description },
1123
+ ...(entry.isSecret ?? shouldTreatAsSecret(entry.name)) && { secret: true }
1124
+ };
1125
+ }
1126
+ return {
1127
+ schemaVersion: 1,
1128
+ variables
1129
+ };
1130
+ }
1131
+ function buildContractFromExample(examplePath) {
1132
+ const parsed = parseEnvFile(examplePath);
1133
+ const variables = {};
1134
+ for (const variable of parsed.vars) {
1135
+ const effectiveType = variable.annotatedType ?? variable.inferredType;
1136
+ variables[variable.key] = {
1137
+ expected: mapEnvVarTypeToExpected(effectiveType),
1138
+ required: variable.isRequired,
1139
+ clientSide: variable.isClientSide,
1140
+ ...variable.description !== void 0 && { description: variable.description },
1141
+ ...shouldTreatAsSecret(variable.key) && { secret: true }
1142
+ };
1143
+ }
1144
+ return {
1145
+ schemaVersion: 1,
1146
+ variables
1147
+ };
1148
+ }
1149
+ function findDefaultContractPath(cwd) {
1150
+ for (const fileName of CONTRACT_FILE_NAMES2) {
1151
+ const candidatePath = path6__default.default.resolve(cwd, fileName);
1152
+ if (fs.existsSync(candidatePath)) return candidatePath;
1153
+ }
1154
+ return void 0;
1155
+ }
1156
+ async function loadValidationContract(options) {
1157
+ const { fallbackExamplePath, contractPath, cwd = process.cwd() } = options;
1158
+ const discoveredContractPath = findDefaultContractPath(cwd);
1159
+ const resolvedContractPath = contractPath === void 0 ? discoveredContractPath : path6__default.default.resolve(cwd, contractPath);
1160
+ if (resolvedContractPath !== void 0 && fs.existsSync(resolvedContractPath)) {
1161
+ const moduleUrl = url.pathToFileURL(resolvedContractPath).href;
1162
+ const moduleValue = await import(moduleUrl);
1163
+ const candidate = moduleValue.default ?? moduleValue.contract;
1164
+ if (isEnvContract(candidate)) {
1165
+ return candidate;
1166
+ }
1167
+ if (isLegacyContract(candidate)) {
1168
+ return convertLegacyContract(candidate);
1169
+ }
1170
+ throw new Error(
1171
+ `Invalid contract at ${resolvedContractPath}. Export default must match EnvContract.`
1172
+ );
1173
+ }
1174
+ return buildContractFromExample(path6__default.default.resolve(cwd, fallbackExamplePath));
1175
+ }
1176
+ function isRecord3(value) {
1177
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1178
+ }
1179
+ function isPlugin(value) {
1180
+ if (!isRecord3(value)) return false;
1181
+ if (typeof value.name !== "string") return false;
1182
+ if (value.transformContract !== void 0 && typeof value.transformContract !== "function") {
1183
+ return false;
1184
+ }
1185
+ if (value.transformSource !== void 0 && typeof value.transformSource !== "function") {
1186
+ return false;
1187
+ }
1188
+ if (value.transformReport !== void 0 && typeof value.transformReport !== "function") {
1189
+ return false;
1190
+ }
1191
+ return true;
1192
+ }
1193
+ async function loadPluginFromPath(pluginPath, cwd) {
1194
+ const resolvedPath = path6__default.default.resolve(cwd, pluginPath);
1195
+ const moduleValue = await import(url.pathToFileURL(resolvedPath).href);
1196
+ const candidate = moduleValue.default ?? moduleValue.plugin;
1197
+ if (isPlugin(candidate)) return candidate;
1198
+ throw new Error(
1199
+ `Invalid plugin at ${resolvedPath}.
1200
+ Expected a default export matching:
1201
+ { name: string,
1202
+ transformSource?(ctx: { environment: string; values: Record<string, string> }): Record<string, string>,
1203
+ transformReport?(report: ValidationReport): ValidationReport,
1204
+ transformContract?(contract: EnvContract): EnvContract }`
1205
+ );
1206
+ }
1207
+ async function loadPlugins(options) {
1208
+ const cwd = options.cwd ?? process.cwd();
1209
+ const plugins = [];
1210
+ const references = [...options.configPlugins ?? [], ...options.pluginPaths];
1211
+ for (const reference of references) {
1212
+ if (typeof reference === "string") {
1213
+ plugins.push(await loadPluginFromPath(reference, cwd));
1214
+ continue;
1215
+ }
1216
+ if (isPlugin(reference)) {
1217
+ plugins.push(reference);
1218
+ continue;
1219
+ }
1220
+ throw new Error("Invalid plugin reference in configuration.");
1221
+ }
1222
+ return plugins;
1223
+ }
1224
+ function applyContractPlugins(contract, plugins) {
1225
+ let next = contract;
1226
+ for (const plugin of plugins) {
1227
+ if (plugin.transformContract === void 0) continue;
1228
+ next = plugin.transformContract(next);
1229
+ }
1230
+ return next;
1231
+ }
1232
+ function applySourcePlugins(params, plugins) {
1233
+ let next = params.values;
1234
+ for (const plugin of plugins) {
1235
+ if (plugin.transformSource === void 0) continue;
1236
+ next = plugin.transformSource({ environment: params.environment, values: next });
1237
+ }
1238
+ return next;
1239
+ }
1240
+ function applyReportPlugins(report, plugins) {
1241
+ let next = report;
1242
+ for (const plugin of plugins) {
1243
+ if (plugin.transformReport === void 0) continue;
1244
+ next = plugin.transformReport(next);
1245
+ }
1246
+ return next;
1247
+ }
1248
+
1249
+ // src/validation/engine.ts
1250
+ var EMAIL_RE = /^[^@\s]+@[^@\s.]+(?:\.[^@\s.]+)+$/;
1251
+ var SEMVER_RE2 = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
1252
+ function detectReceivedType(value) {
1253
+ const normalized = value.trim();
1254
+ if (normalized.length === 0) return "unknown";
1255
+ if (["true", "false", "1", "0", "yes", "no"].includes(normalized.toLowerCase())) return "boolean";
1256
+ if (!Number.isNaN(Number(normalized)) && Number.isFinite(Number(normalized))) return "number";
1257
+ if (SEMVER_RE2.test(normalized)) return "semver";
1258
+ try {
1259
+ const url = new URL(normalized);
1260
+ if (url.protocol.length > 0) return "url";
1261
+ } catch {
1262
+ }
1263
+ if (EMAIL_RE.test(normalized)) return "email";
1264
+ try {
1265
+ const parsed = JSON.parse(normalized);
1266
+ if (typeof parsed === "object" && parsed !== null) return "json";
1267
+ } catch {
1268
+ }
1269
+ return "string";
1270
+ }
1271
+ function validateNumber(expected, normalized, receivedType) {
1272
+ const parsed = Number(normalized);
1273
+ if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
1274
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1275
+ }
1276
+ if (expected.min !== void 0 && parsed < expected.min) {
1277
+ return { isValid: false, receivedType, issueType: "invalid_value" };
1278
+ }
1279
+ if (expected.max !== void 0 && parsed > expected.max) {
1280
+ return { isValid: false, receivedType, issueType: "invalid_value" };
1281
+ }
1282
+ return { isValid: true, receivedType };
1283
+ }
1284
+ function validateBoolean(normalized, receivedType) {
1285
+ if (!["true", "false", "1", "0", "yes", "no"].includes(normalized.toLowerCase())) {
1286
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1287
+ }
1288
+ return { isValid: true, receivedType };
1289
+ }
1290
+ function validateEnum(expected, normalized, receivedType) {
1291
+ if (!expected.values.includes(normalized)) {
1292
+ return { isValid: false, receivedType, issueType: "invalid_value" };
1293
+ }
1294
+ return { isValid: true, receivedType };
1295
+ }
1296
+ function validateUrl(normalized, receivedType) {
1297
+ try {
1298
+ const value = new URL(normalized);
1299
+ if (value.protocol.length === 0)
1300
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1301
+ return { isValid: true, receivedType };
1302
+ } catch {
1303
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1304
+ }
1305
+ }
1306
+ function validateEmail(normalized, receivedType) {
1307
+ if (!EMAIL_RE.test(normalized))
1308
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1309
+ return { isValid: true, receivedType };
1310
+ }
1311
+ function validateJson(normalized, receivedType) {
1312
+ try {
1313
+ const parsed = JSON.parse(normalized);
1314
+ if (typeof parsed === "object" && parsed !== null) return { isValid: true, receivedType };
1315
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1316
+ } catch {
1317
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1318
+ }
1319
+ }
1320
+ function validateSemver(normalized, receivedType) {
1321
+ if (!SEMVER_RE2.test(normalized))
1322
+ return { isValid: false, receivedType, issueType: "invalid_value" };
1323
+ return { isValid: true, receivedType };
1324
+ }
1325
+ function validateValueAgainstExpected(expected, rawValue) {
1326
+ const normalized = rawValue.trim();
1327
+ const receivedType = detectReceivedType(normalized);
1328
+ if (expected.type === "unknown" || expected.type === "string")
1329
+ return { isValid: true, receivedType };
1330
+ if (expected.type === "number") return validateNumber(expected, normalized, receivedType);
1331
+ if (expected.type === "boolean") return validateBoolean(normalized, receivedType);
1332
+ if (expected.type === "enum") return validateEnum(expected, normalized, receivedType);
1333
+ if (expected.type === "url") return validateUrl(normalized, receivedType);
1334
+ if (expected.type === "email") return validateEmail(normalized, receivedType);
1335
+ if (expected.type === "json") return validateJson(normalized, receivedType);
1336
+ if (expected.type === "semver") return validateSemver(normalized, receivedType);
1337
+ return { isValid: true, receivedType };
1338
+ }
1339
+ function toIssueCode(issueType) {
1340
+ if (issueType === "missing") return "ENV_MISSING";
1341
+ if (issueType === "extra") return "ENV_EXTRA";
1342
+ if (issueType === "invalid_type") return "ENV_INVALID_TYPE";
1343
+ if (issueType === "invalid_value") return "ENV_INVALID_VALUE";
1344
+ if (issueType === "conflict") return "ENV_CONFLICT";
1345
+ return "ENV_SECRET_EXPOSED";
1346
+ }
1347
+ function toIssueValue(value, debugValues) {
1348
+ if (!debugValues) return null;
1349
+ if (value === void 0) return null;
1350
+ return value;
1351
+ }
1352
+ function createIssue(params) {
1353
+ return {
1354
+ code: toIssueCode(params.type),
1355
+ type: params.type,
1356
+ severity: params.severity,
1357
+ key: params.key,
1358
+ environment: params.environment,
1359
+ message: params.message,
1360
+ value: toIssueValue(params.value, params.debugValues),
1361
+ ...params.expected !== void 0 && { expected: params.expected },
1362
+ ...params.receivedType !== void 0 && { receivedType: params.receivedType }
1363
+ };
1364
+ }
1365
+ function dedupeIssues(issues) {
1366
+ const seen = /* @__PURE__ */ new Set();
1367
+ const unique = [];
1368
+ for (const issue of issues) {
1369
+ const token = [
1370
+ issue.code,
1371
+ issue.type,
1372
+ issue.severity,
1373
+ issue.environment,
1374
+ issue.key,
1375
+ issue.message,
1376
+ issue.receivedType ?? ""
1377
+ ].join("|");
1378
+ if (seen.has(token)) continue;
1379
+ seen.add(token);
1380
+ unique.push(issue);
1381
+ }
1382
+ return unique;
1383
+ }
1384
+ function buildReport(env, issues, recommendations) {
1385
+ const dedupedIssues = dedupeIssues(issues);
1386
+ const errors = dedupedIssues.filter((item) => item.severity === "error").length;
1387
+ const warnings = dedupedIssues.filter((item) => item.severity === "warning").length;
1388
+ const status = errors > 0 ? "fail" : "ok";
1389
+ return {
1390
+ schemaVersion: 1,
1391
+ status,
1392
+ summary: {
1393
+ errors,
1394
+ warnings,
1395
+ total: dedupedIssues.length
1396
+ },
1397
+ issues: dedupedIssues,
1398
+ meta: {
1399
+ env,
1400
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1401
+ },
1402
+ ...recommendations !== void 0 && recommendations.length > 0 && { recommendations }
1403
+ };
1404
+ }
1405
+ function isClientSecret(variable, key) {
1406
+ return variable.secret === true && (variable.clientSide || key.startsWith("NEXT_PUBLIC_"));
1407
+ }
1408
+ function checkContractVariable(key, variable, context) {
1409
+ const { options, issues } = context;
1410
+ const value = options.values[key];
1411
+ const hasValue = value !== void 0 && value.trim().length > 0;
1412
+ if (variable.required && !hasValue) {
1413
+ issues.push(
1414
+ createIssue({
1415
+ type: "missing",
1416
+ severity: "error",
1417
+ key,
1418
+ environment: options.environment,
1419
+ message: `Required variable ${key} is missing.`,
1420
+ debugValues: options.debugValues,
1421
+ expected: variable.expected
1422
+ })
1423
+ );
1424
+ return;
1425
+ }
1426
+ if (!hasValue) return;
1427
+ const validation = validateValueAgainstExpected(variable.expected, value);
1428
+ if (!validation.isValid) {
1429
+ const message = validation.issueType === "invalid_type" ? `Variable ${key} has invalid type.` : `Variable ${key} has invalid value.`;
1430
+ issues.push(
1431
+ createIssue({
1432
+ type: validation.issueType,
1433
+ severity: "error",
1434
+ key,
1435
+ environment: options.environment,
1436
+ message,
1437
+ value,
1438
+ debugValues: options.debugValues,
1439
+ expected: variable.expected,
1440
+ receivedType: validation.receivedType
1441
+ })
1442
+ );
1443
+ }
1444
+ if (isClientSecret(variable, key)) {
1445
+ issues.push(
1446
+ createIssue({
1447
+ type: "secret_exposed",
1448
+ severity: "error",
1449
+ key,
1450
+ environment: options.environment,
1451
+ message: `Secret variable ${key} is marked as client-side.`,
1452
+ value,
1453
+ debugValues: options.debugValues,
1454
+ expected: variable.expected
1455
+ })
1456
+ );
1457
+ }
1458
+ }
1459
+ function validateAgainstContract(options) {
1460
+ const issues = [];
1461
+ const contractKeys = new Set(Object.keys(options.contract.variables));
1462
+ for (const [key, variable] of Object.entries(options.contract.variables)) {
1463
+ checkContractVariable(key, variable, { options, issues });
1464
+ }
1465
+ for (const [key, value] of Object.entries(options.values)) {
1466
+ if (contractKeys.has(key)) continue;
1467
+ const severity = options.strict ? "error" : "warning";
1468
+ issues.push(
1469
+ createIssue({
1470
+ type: "extra",
1471
+ severity,
1472
+ key,
1473
+ environment: options.environment,
1474
+ message: `Variable ${key} is not defined in the contract.`,
1475
+ value,
1476
+ debugValues: options.debugValues
1477
+ })
1478
+ );
1479
+ }
1480
+ return buildReport(options.environment, issues);
1481
+ }
1482
+ function collectUnionKeys(contract, sources) {
1483
+ const union = new Set(Object.keys(contract.variables));
1484
+ for (const source of Object.values(sources)) {
1485
+ for (const key of Object.keys(source)) {
1486
+ union.add(key);
1487
+ }
1488
+ }
1489
+ return union;
1490
+ }
1491
+ function diffMissingEntries(key, missing, context) {
1492
+ const { variable, options, issues } = context;
1493
+ for (const entry of missing) {
1494
+ issues.push(
1495
+ createIssue({
1496
+ type: "missing",
1497
+ severity: "error",
1498
+ key,
1499
+ environment: entry.sourceName,
1500
+ message: `Variable ${key} is missing in ${entry.sourceName}.`,
1501
+ debugValues: options.debugValues,
1502
+ ...variable !== void 0 && { expected: variable.expected }
1503
+ })
1504
+ );
1505
+ }
1506
+ }
1507
+ function diffTypeConflicts(key, present, context) {
1508
+ const { variable, options, issues } = context;
1509
+ const typeBySource = /* @__PURE__ */ new Map();
1510
+ for (const entry of present) {
1511
+ typeBySource.set(entry.sourceName, detectReceivedType(entry.value ?? ""));
1512
+ }
1513
+ if (new Set(typeBySource.values()).size <= 1) return;
1514
+ for (const [sourceName, detectedType] of typeBySource.entries()) {
1515
+ issues.push(
1516
+ createIssue({
1517
+ type: "conflict",
1518
+ severity: "error",
1519
+ key,
1520
+ environment: sourceName,
1521
+ message: `Variable ${key} has conflicting inferred type across environments.`,
1522
+ debugValues: options.debugValues,
1523
+ receivedType: detectedType,
1524
+ ...options.sources[sourceName]?.[key] !== void 0 && {
1525
+ value: options.sources[sourceName]?.[key]
1526
+ },
1527
+ ...variable !== void 0 && { expected: variable.expected }
1528
+ })
1529
+ );
1530
+ }
1531
+ }
1532
+ function diffPresentEntry(key, entry, context) {
1533
+ const { variable, options, issues } = context;
1534
+ if (entry.value === void 0) return;
1535
+ if (variable === void 0) {
1536
+ const severity = options.strict ? "error" : "warning";
1537
+ issues.push(
1538
+ createIssue({
1539
+ type: "extra",
1540
+ severity,
1541
+ key,
1542
+ environment: entry.sourceName,
1543
+ message: `Variable ${key} is not defined in the contract.`,
1544
+ value: entry.value,
1545
+ debugValues: options.debugValues
1546
+ })
1547
+ );
1548
+ return;
1549
+ }
1550
+ const validation = validateValueAgainstExpected(variable.expected, entry.value);
1551
+ if (!validation.isValid) {
1552
+ const message = validation.issueType === "invalid_type" ? `Variable ${key} has invalid type in ${entry.sourceName}.` : `Variable ${key} has invalid value in ${entry.sourceName}.`;
1553
+ issues.push(
1554
+ createIssue({
1555
+ type: validation.issueType,
1556
+ severity: "error",
1557
+ key,
1558
+ environment: entry.sourceName,
1559
+ message,
1560
+ value: entry.value,
1561
+ debugValues: options.debugValues,
1562
+ expected: variable.expected,
1563
+ receivedType: validation.receivedType
1564
+ })
1565
+ );
1566
+ }
1567
+ if (isClientSecret(variable, key)) {
1568
+ issues.push(
1569
+ createIssue({
1570
+ type: "secret_exposed",
1571
+ severity: "error",
1572
+ key,
1573
+ environment: entry.sourceName,
1574
+ message: `Secret variable ${key} is marked as client-side.`,
1575
+ value: entry.value,
1576
+ debugValues: options.debugValues,
1577
+ expected: variable.expected
1578
+ })
1579
+ );
1580
+ }
1581
+ }
1582
+ function diffEnvironmentSources(options) {
1583
+ const issues = [];
1584
+ const sourceNames = Object.keys(options.sources);
1585
+ const unionKeys = collectUnionKeys(options.contract, options.sources);
1586
+ for (const key of unionKeys) {
1587
+ const variable = options.contract.variables[key];
1588
+ const valuesBySource = sourceNames.map((sourceName) => ({
1589
+ sourceName,
1590
+ value: options.sources[sourceName]?.[key]
1591
+ }));
1592
+ const present = valuesBySource.filter(
1593
+ (entry) => entry.value !== void 0 && entry.value !== ""
1594
+ );
1595
+ const missing = valuesBySource.filter(
1596
+ (entry) => entry.value === void 0 || entry.value === ""
1597
+ );
1598
+ if (present.length === 0 && variable?.required === true) {
1599
+ for (const entry of missing) {
1600
+ issues.push(
1601
+ createIssue({
1602
+ type: "missing",
1603
+ severity: "error",
1604
+ key,
1605
+ environment: entry.sourceName,
1606
+ message: `Required variable ${key} is missing in ${entry.sourceName}.`,
1607
+ debugValues: options.debugValues,
1608
+ expected: variable.expected
1609
+ })
1610
+ );
1611
+ }
1612
+ continue;
1613
+ }
1614
+ const ctx = { variable, options, issues };
1615
+ if (present.length > 0) {
1616
+ diffMissingEntries(key, missing, ctx);
1617
+ }
1618
+ diffTypeConflicts(key, present, ctx);
1619
+ for (const entry of present) {
1620
+ diffPresentEntry(key, entry, ctx);
1621
+ }
1622
+ }
1623
+ return buildReport("diff", issues);
1624
+ }
1625
+ function buildRecommendations(issues) {
1626
+ const codes = new Set(issues.map((item) => item.code));
1627
+ const recommendations = [];
1628
+ if (codes.has("ENV_MISSING")) {
1629
+ recommendations.push("Add missing required variables to each target environment.");
1630
+ }
1631
+ if (codes.has("ENV_EXTRA")) {
1632
+ recommendations.push(
1633
+ "Remove undeclared variables or add them to env.contract.ts intentionally."
1634
+ );
1635
+ }
1636
+ if (codes.has("ENV_INVALID_TYPE") || codes.has("ENV_INVALID_VALUE")) {
1637
+ recommendations.push(
1638
+ "Normalize variable values so they match the expected contract types and constraints."
1639
+ );
1640
+ }
1641
+ if (codes.has("ENV_CONFLICT")) {
1642
+ recommendations.push("Align variable semantics across environments to avoid drift.");
1643
+ }
1644
+ if (codes.has("ENV_SECRET_EXPOSED")) {
1645
+ recommendations.push(
1646
+ "Move secret variables to server-only scope and avoid NEXT_PUBLIC_ exposure for secrets."
1647
+ );
1648
+ }
1649
+ return recommendations;
1650
+ }
1651
+ function buildDoctorReport(options) {
1652
+ const merged = [...options.checkReport.issues, ...options.diffReport.issues];
1653
+ const recommendations = buildRecommendations(merged);
1654
+ return buildReport("doctor", merged, recommendations);
1655
+ }
1656
+ function stripWrappingQuotes(value) {
1657
+ if (value.length < 2) return value;
1658
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1659
+ return value.slice(1, -1);
1660
+ }
1661
+ return value;
1662
+ }
1663
+ function parseEnvSourceContent(content) {
1664
+ const result = {};
1665
+ const lines = content.split("\n");
1666
+ for (const line of lines) {
1667
+ const trimmed = line.trim();
1668
+ if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
1669
+ const match = /^(?:export\s+)?([A-Za-z_]\w*)=(.*)$/.exec(trimmed);
1670
+ if (match === null) continue;
1671
+ const key = match[1] ?? "";
1672
+ const rawValue = match[2] ?? "";
1673
+ result[key] = stripWrappingQuotes(rawValue.trim());
1674
+ }
1675
+ return result;
1676
+ }
1677
+ async function loadEnvSource(options) {
1678
+ const resolvedPath = path6__default.default.resolve(options.filePath);
1679
+ try {
1680
+ const content = await promises.readFile(resolvedPath, "utf8");
1681
+ return parseEnvSourceContent(content);
1682
+ } catch (error_) {
1683
+ if (options.allowMissing === true && error_ instanceof Error && "code" in error_ && error_.code === "ENOENT") {
1684
+ return {};
1685
+ }
1686
+ throw error_;
1687
+ }
1688
+ }
1689
+ function toJsonString(report, mode) {
1690
+ if (mode === "pretty") return `${JSON.stringify(report, null, 2)}
1691
+ `;
1692
+ return `${JSON.stringify(report)}
1693
+ `;
1694
+ }
1695
+ function formatIssue(issue) {
1696
+ const expected = issue.expected === void 0 ? "" : ` expected=${issue.expected.type}`;
1697
+ const received = issue.receivedType === void 0 ? "" : ` received=${issue.receivedType}`;
1698
+ return `${issue.severity.toUpperCase()} [${issue.code}] ${issue.environment}:${issue.key} ${issue.message}${expected}${received}`;
1699
+ }
1700
+ function formatHumanReport(report) {
1701
+ const lines = [];
1702
+ lines.push(
1703
+ `Status: ${report.status.toUpperCase()} (errors=${report.summary.errors}, warnings=${report.summary.warnings}, total=${report.summary.total})`
1704
+ );
1705
+ if (report.issues.length > 0) {
1706
+ lines.push("", "Issues:");
1707
+ for (const issue of report.issues) {
1708
+ lines.push(`- ${formatIssue(issue)}`);
1709
+ }
1710
+ }
1711
+ if (report.recommendations !== void 0 && report.recommendations.length > 0) {
1712
+ lines.push("", "Recommendations:");
1713
+ for (const recommendation of report.recommendations) {
1714
+ lines.push(`- ${recommendation}`);
1715
+ }
1716
+ }
1717
+ return `${lines.join("\n")}
1718
+ `;
1719
+ }
1720
+ async function persistJsonOutput(outputFile, report) {
1721
+ const resolvedPath = path6__default.default.resolve(outputFile);
1722
+ await promises.mkdir(path6__default.default.dirname(resolvedPath), { recursive: true });
1723
+ await promises.writeFile(resolvedPath, `${JSON.stringify(report, null, 2)}
1724
+ `, "utf8");
1725
+ }
1726
+ async function emitValidationReport(options) {
1727
+ const { report, outputFile, jsonMode } = options;
1728
+ if (outputFile !== void 0) {
1729
+ await persistJsonOutput(outputFile, report);
1730
+ }
1731
+ if (jsonMode === "off") {
1732
+ process.stdout.write(formatHumanReport(report));
1733
+ return;
1734
+ }
1735
+ process.stdout.write(toJsonString(report, jsonMode));
1736
+ }
1737
+
1738
+ // src/validation-command.ts
1739
+ var HELP_TEXT = {
1740
+ check: [
1741
+ "Usage: env-typegen check [options]",
1742
+ "",
1743
+ "Options:",
1744
+ " --env <path> Environment file to validate (default: .env)",
1745
+ " --contract <path> Contract file path (default: env.contract.ts)",
1746
+ " --example <path> Fallback .env.example used to bootstrap contract",
1747
+ " --strict Validate extras as errors (default: true)",
1748
+ " --no-strict Downgrade extras to warnings",
1749
+ " --json Emit machine-readable JSON report",
1750
+ " --json=pretty Emit pretty JSON report",
1751
+ " --output-file <path> Persist JSON report to a file",
1752
+ " --debug-values Include raw values in issues (unsafe for CI logs)",
1753
+ " --cloud-provider <name> vercel | cloudflare | aws",
1754
+ " --cloud-file <path> Cloud snapshot JSON file",
1755
+ " --plugin <path> Plugin module path (repeatable)",
1756
+ " -c, --config <path> Config file path",
1757
+ " -h, --help Show this help",
1758
+ "",
1759
+ "Exit codes:",
1760
+ " 0 All checks passed (status: ok or warn)",
1761
+ " 1 One or more checks failed (status: fail) or invalid usage"
1762
+ ].join("\n"),
1763
+ diff: [
1764
+ "Usage: env-typegen diff [options]",
1765
+ "",
1766
+ "Options:",
1767
+ " --targets <list> Comma-separated targets (default: .env,.env.example,.env.production)",
1768
+ " --contract <path> Contract file path (default: env.contract.ts)",
1769
+ " --example <path> Fallback .env.example used to bootstrap contract",
1770
+ " --strict Validate extras as errors (default: true)",
1771
+ " --no-strict Downgrade extras to warnings",
1772
+ " --json Emit machine-readable JSON report",
1773
+ " --json=pretty Emit pretty JSON report",
1774
+ " --output-file <path> Persist JSON report to a file",
1775
+ " --debug-values Include raw values in issues (unsafe for CI logs)",
1776
+ " --cloud-provider <name> vercel | cloudflare | aws",
1777
+ " --cloud-file <path> Cloud snapshot JSON file added to diff sources",
1778
+ " --plugin <path> Plugin module path (repeatable)",
1779
+ " -c, --config <path> Config file path",
1780
+ " -h, --help Show this help",
1781
+ "",
1782
+ "Exit codes:",
1783
+ " 0 All checks passed (status: ok or warn)",
1784
+ " 1 One or more checks failed (status: fail) or invalid usage"
1785
+ ].join("\n"),
1786
+ doctor: [
1787
+ "Usage: env-typegen doctor [options]",
1788
+ "",
1789
+ "Options:",
1790
+ " --env <path> Environment file to validate (default: .env)",
1791
+ " --targets <list> Comma-separated targets for drift analysis",
1792
+ " --contract <path> Contract file path (default: env.contract.ts)",
1793
+ " --example <path> Fallback .env.example used to bootstrap contract",
1794
+ " --strict Validate extras as errors (default: true)",
1795
+ " --no-strict Downgrade extras to warnings",
1796
+ " --json Emit machine-readable JSON report",
1797
+ " --json=pretty Emit pretty JSON report",
1798
+ " --output-file <path> Persist JSON report to a file",
1799
+ " --debug-values Include raw values in issues (unsafe for CI logs)",
1800
+ " --cloud-provider <name> vercel | cloudflare | aws",
1801
+ " --cloud-file <path> Cloud snapshot JSON file",
1802
+ " --plugin <path> Plugin module path (repeatable)",
1803
+ " -c, --config <path> Config file path",
1804
+ " -h, --help Show this help",
1805
+ "",
1806
+ "Exit codes:",
1807
+ " 0 All checks passed (status: ok or warn)",
1808
+ " 1 One or more checks failed (status: fail) or invalid usage"
1809
+ ].join("\n")
1810
+ };
1811
+ function resolveConfigRelative(value, configDir) {
1812
+ if (value === void 0 || path6__default.default.isAbsolute(value)) return value;
1813
+ return path6__default.default.resolve(configDir, value);
1814
+ }
1815
+ function resolvePluginReference(reference, configDir) {
1816
+ if (typeof reference === "string" && !path6__default.default.isAbsolute(reference)) {
1817
+ return path6__default.default.resolve(configDir, reference);
1818
+ }
1819
+ return reference;
1820
+ }
1821
+ function applyConfigPaths(config, configDir) {
1822
+ let input;
1823
+ if (Array.isArray(config.input)) {
1824
+ input = config.input.map((item) => resolveConfigRelative(item, configDir) ?? item);
1825
+ } else {
1826
+ input = resolveConfigRelative(config.input, configDir);
1827
+ }
1828
+ const output = resolveConfigRelative(config.output, configDir);
1829
+ const schemaFile = resolveConfigRelative(config.schemaFile, configDir);
1830
+ return {
1831
+ ...config,
1832
+ ...input !== void 0 && { input },
1833
+ ...output !== void 0 && { output },
1834
+ ...schemaFile !== void 0 && { schemaFile },
1835
+ ...config.diffTargets !== void 0 && {
1836
+ diffTargets: config.diffTargets.map(
1837
+ (target) => resolveConfigRelative(target, configDir) ?? target
1838
+ )
1839
+ },
1840
+ ...config.plugins !== void 0 && {
1841
+ plugins: config.plugins.map((reference) => resolvePluginReference(reference, configDir))
1842
+ }
1843
+ };
1844
+ }
1845
+ async function loadCommandConfig(configPath) {
1846
+ if (configPath === void 0) {
1847
+ return loadConfig(process.cwd());
1848
+ }
1849
+ const resolvedPath = path6__default.default.resolve(configPath);
1850
+ const configDir = path6__default.default.dirname(resolvedPath);
1851
+ const moduleValue = await import(url.pathToFileURL(resolvedPath).href);
1852
+ if (moduleValue.default === void 0) return void 0;
1853
+ return applyConfigPaths(moduleValue.default, configDir);
1854
+ }
1855
+ function preprocessJsonArguments(argv) {
1856
+ const normalizedArgs = [];
1857
+ let assignedMode = "off";
1858
+ for (const item of argv) {
1859
+ if (item === "--json=pretty") {
1860
+ normalizedArgs.push("--json");
1861
+ assignedMode = "pretty";
1862
+ continue;
1863
+ }
1864
+ if (item === "--json=compact") {
1865
+ normalizedArgs.push("--json");
1866
+ assignedMode = "compact";
1867
+ continue;
1868
+ }
1869
+ normalizedArgs.push(item);
1870
+ }
1871
+ return { normalizedArgs, assignedMode };
1872
+ }
1873
+ function parseValidationArgs(argv) {
1874
+ const { normalizedArgs, assignedMode } = preprocessJsonArguments(argv);
1875
+ const { values } = util.parseArgs({
1876
+ args: normalizedArgs,
1877
+ options: {
1878
+ env: { type: "string", multiple: true },
1879
+ targets: { type: "string" },
1880
+ contract: { type: "string" },
1881
+ example: { type: "string" },
1882
+ strict: { type: "boolean" },
1883
+ "no-strict": { type: "boolean" },
1884
+ json: { type: "boolean" },
1885
+ "output-file": { type: "string" },
1886
+ "debug-values": { type: "boolean" },
1887
+ "cloud-provider": { type: "string" },
1888
+ "cloud-file": { type: "string" },
1889
+ plugin: { type: "string", multiple: true },
1890
+ config: { type: "string", short: "c" },
1891
+ help: { type: "boolean", short: "h" }
1892
+ }
1893
+ });
1894
+ const castValues = values;
1895
+ let jsonMode = "off";
1896
+ if (castValues.json === true) {
1897
+ jsonMode = assignedMode === "off" ? "compact" : assignedMode;
1898
+ }
1899
+ return { values: castValues, jsonMode };
1900
+ }
1901
+ function resolveStrict(values, fileConfig) {
1902
+ if (values["no-strict"] === true) return false;
1903
+ if (values.strict !== void 0) return values.strict;
1904
+ if (fileConfig?.strict !== void 0) return fileConfig.strict;
1905
+ return true;
1906
+ }
1907
+ function parseCloudProvider(value) {
1908
+ if (value === void 0) return void 0;
1909
+ if (value === "vercel" || value === "cloudflare" || value === "aws") return value;
1910
+ throw new Error(`Unknown cloud provider: ${value}. Valid: vercel, cloudflare, aws`);
1911
+ }
1912
+ function parseTargets(values, fileConfig) {
1913
+ const fromCli = values.targets;
1914
+ if (fromCli !== void 0) {
1915
+ return fromCli.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
1916
+ }
1917
+ if (fileConfig?.diffTargets !== void 0 && fileConfig.diffTargets.length > 0) {
1918
+ return fileConfig.diffTargets;
1919
+ }
1920
+ return [".env", ".env.example", ".env.production"];
1921
+ }
1922
+ async function prepareCommonContext(values) {
1923
+ const fileConfig = await loadCommandConfig(values.config);
1924
+ const strict = resolveStrict(values, fileConfig);
1925
+ const debugValues = values["debug-values"] ?? false;
1926
+ const contractPath = values.contract ?? fileConfig?.schemaFile;
1927
+ const fallbackExamplePath = values.example ?? ".env.example";
1928
+ const cloudProvider = parseCloudProvider(values["cloud-provider"]);
1929
+ const cloudFile = values["cloud-file"];
1930
+ const pluginLoadOptions = {
1931
+ pluginPaths: values.plugin ?? [],
1932
+ cwd: process.cwd(),
1933
+ ...fileConfig?.plugins !== void 0 && { configPlugins: fileConfig.plugins }
1934
+ };
1935
+ const plugins = await loadPlugins(pluginLoadOptions);
1936
+ return {
1937
+ fileConfig,
1938
+ strict,
1939
+ debugValues,
1940
+ outputFile: values["output-file"],
1941
+ contractPath,
1942
+ fallbackExamplePath,
1943
+ cloudProvider,
1944
+ cloudFile,
1945
+ plugins
1946
+ };
1947
+ }
1948
+ async function emitAndReturnExitCode(report, params) {
1949
+ const emitOptions = {
1950
+ report,
1951
+ jsonMode: params.jsonMode,
1952
+ ...params.outputFile !== void 0 && { outputFile: params.outputFile }
1953
+ };
1954
+ await emitValidationReport(emitOptions);
1955
+ return report.status === "fail" ? 1 : 0;
1956
+ }
1957
+ async function runCheckCommand(args) {
1958
+ const context = await prepareCommonContext(args.values);
1959
+ const loadContractOptions = {
1960
+ fallbackExamplePath: context.fallbackExamplePath,
1961
+ cwd: process.cwd(),
1962
+ ...context.contractPath !== void 0 && { contractPath: context.contractPath }
1963
+ };
1964
+ const contract = applyContractPlugins(
1965
+ await loadValidationContract(loadContractOptions),
1966
+ context.plugins
1967
+ );
1968
+ const provider = context.cloudProvider;
1969
+ let environment = args.values.env?.[0] ?? ".env";
1970
+ let sourceValues;
1971
+ if (provider === void 0) {
1972
+ sourceValues = await loadEnvSource({ filePath: environment, allowMissing: true });
1973
+ } else {
1974
+ const cloudFile = context.cloudFile ?? `${provider}.env.json`;
1975
+ sourceValues = await loadCloudSource({ provider, filePath: cloudFile });
1976
+ environment = `cloud:${provider}`;
1977
+ }
1978
+ sourceValues = applySourcePlugins({ environment, values: sourceValues }, context.plugins);
1979
+ const report = applyReportPlugins(
1980
+ validateAgainstContract({
1981
+ contract,
1982
+ values: sourceValues,
1983
+ environment,
1984
+ strict: context.strict,
1985
+ debugValues: context.debugValues
1986
+ }),
1987
+ context.plugins
1988
+ );
1989
+ return emitAndReturnExitCode(report, {
1990
+ jsonMode: args.jsonMode,
1991
+ ...context.outputFile !== void 0 && { outputFile: context.outputFile }
1992
+ });
1993
+ }
1994
+ async function runDiffCommand(args) {
1995
+ const context = await prepareCommonContext(args.values);
1996
+ const loadContractOptions = {
1997
+ fallbackExamplePath: context.fallbackExamplePath,
1998
+ cwd: process.cwd(),
1999
+ ...context.contractPath !== void 0 && { contractPath: context.contractPath }
2000
+ };
2001
+ const contract = applyContractPlugins(
2002
+ await loadValidationContract(loadContractOptions),
2003
+ context.plugins
2004
+ );
2005
+ const sources = {};
2006
+ for (const target of parseTargets(args.values, context.fileConfig)) {
2007
+ const values = await loadEnvSource({ filePath: target, allowMissing: true });
2008
+ sources[target] = applySourcePlugins({ environment: target, values }, context.plugins);
2009
+ }
2010
+ if (context.cloudProvider !== void 0) {
2011
+ const cloudFile = context.cloudFile ?? `${context.cloudProvider}.env.json`;
2012
+ const cloudEnvironment = `cloud:${context.cloudProvider}`;
2013
+ const cloudValues = await loadCloudSource({
2014
+ provider: context.cloudProvider,
2015
+ filePath: cloudFile
2016
+ });
2017
+ sources[cloudEnvironment] = applySourcePlugins(
2018
+ { environment: cloudEnvironment, values: cloudValues },
2019
+ context.plugins
2020
+ );
2021
+ }
2022
+ const report = applyReportPlugins(
2023
+ diffEnvironmentSources({
2024
+ contract,
2025
+ sources,
2026
+ strict: context.strict,
2027
+ debugValues: context.debugValues
2028
+ }),
2029
+ context.plugins
2030
+ );
2031
+ return emitAndReturnExitCode(report, {
2032
+ jsonMode: args.jsonMode,
2033
+ ...context.outputFile !== void 0 && { outputFile: context.outputFile }
2034
+ });
2035
+ }
2036
+ async function runDoctorCommand(args) {
2037
+ const context = await prepareCommonContext(args.values);
2038
+ const loadContractOptions = {
2039
+ fallbackExamplePath: context.fallbackExamplePath,
2040
+ cwd: process.cwd(),
2041
+ ...context.contractPath !== void 0 && { contractPath: context.contractPath }
2042
+ };
2043
+ const contract = applyContractPlugins(
2044
+ await loadValidationContract(loadContractOptions),
2045
+ context.plugins
2046
+ );
2047
+ const checkEnvironment = args.values.env?.[0] ?? ".env";
2048
+ let checkValues = await loadEnvSource({ filePath: checkEnvironment, allowMissing: true });
2049
+ checkValues = applySourcePlugins(
2050
+ { environment: checkEnvironment, values: checkValues },
2051
+ context.plugins
2052
+ );
2053
+ const checkReport = validateAgainstContract({
2054
+ contract,
2055
+ values: checkValues,
2056
+ environment: checkEnvironment,
2057
+ strict: context.strict,
2058
+ debugValues: context.debugValues
2059
+ });
2060
+ const sources = {};
2061
+ for (const target of parseTargets(args.values, context.fileConfig)) {
2062
+ const values = await loadEnvSource({ filePath: target, allowMissing: true });
2063
+ sources[target] = applySourcePlugins({ environment: target, values }, context.plugins);
2064
+ }
2065
+ if (context.cloudProvider !== void 0) {
2066
+ const cloudFile = context.cloudFile ?? `${context.cloudProvider}.env.json`;
2067
+ const cloudEnvironment = `cloud:${context.cloudProvider}`;
2068
+ const cloudValues = await loadCloudSource({
2069
+ provider: context.cloudProvider,
2070
+ filePath: cloudFile
2071
+ });
2072
+ sources[cloudEnvironment] = applySourcePlugins(
2073
+ { environment: cloudEnvironment, values: cloudValues },
2074
+ context.plugins
2075
+ );
2076
+ }
2077
+ const diffReport = diffEnvironmentSources({
2078
+ contract,
2079
+ sources,
2080
+ strict: context.strict,
2081
+ debugValues: context.debugValues
2082
+ });
2083
+ const report = applyReportPlugins(
2084
+ buildDoctorReport({ checkReport, diffReport }),
2085
+ context.plugins
2086
+ );
2087
+ return emitAndReturnExitCode(report, {
2088
+ jsonMode: args.jsonMode,
2089
+ ...context.outputFile !== void 0 && { outputFile: context.outputFile }
2090
+ });
2091
+ }
2092
+ async function runValidationCommand(params) {
2093
+ const parsed = parseValidationArgs(params.argv);
2094
+ if (parsed.values.help === true) {
2095
+ console.log(HELP_TEXT[params.command]);
2096
+ return 0;
2097
+ }
2098
+ if (params.command === "check") {
2099
+ return runCheckCommand(parsed);
2100
+ }
2101
+ if (params.command === "diff") {
2102
+ return runDiffCommand(parsed);
2103
+ }
2104
+ return runDoctorCommand(parsed);
2105
+ }
2106
+
2107
+ exports.CONTRACT_FILE_NAMES = CONTRACT_FILE_NAMES;
2108
+ exports.applyContractPlugins = applyContractPlugins;
2109
+ exports.applyReportPlugins = applyReportPlugins;
2110
+ exports.applySourcePlugins = applySourcePlugins;
2111
+ exports.buildCiReport = buildCiReport;
554
2112
  exports.defineConfig = defineConfig;
2113
+ exports.defineContract = defineContract;
2114
+ exports.formatCiReport = formatCiReport;
555
2115
  exports.generateDeclaration = generateDeclaration;
556
2116
  exports.generateEnvValidation = generateEnvValidation;
557
2117
  exports.generateT3Env = generateT3Env;
@@ -560,10 +2120,16 @@ exports.generateZodSchema = generateZodSchema;
560
2120
  exports.inferType = inferType;
561
2121
  exports.inferTypes = inferTypesFromParsedVars;
562
2122
  exports.inferenceRules = inferenceRules;
2123
+ exports.loadCloudSource = loadCloudSource;
563
2124
  exports.loadConfig = loadConfig;
2125
+ exports.loadContract = loadContract;
2126
+ exports.loadPlugins = loadPlugins;
564
2127
  exports.parseCommentBlock = parseCommentBlock;
565
2128
  exports.parseEnvFile = parseEnvFile;
566
2129
  exports.parseEnvFileContent = parseEnvFileContent;
2130
+ exports.runCheck = runCheck;
567
2131
  exports.runGenerate = runGenerate;
2132
+ exports.runValidationCommand = runValidationCommand;
2133
+ exports.validateContract = validateContract;
568
2134
  //# sourceMappingURL=index.cjs.map
569
2135
  //# sourceMappingURL=index.cjs.map