zod-envkit 1.2.3 → 1.3.0

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/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [1.3.0](https://github.com/nxtxe/zod-envkit/compare/v1.2.3...v1.3.0) (2026-03-04)
2
+
3
+
4
+ ### Features
5
+
6
+ * polish examples and install docs for 1.3.0 ([182ed34](https://github.com/nxtxe/zod-envkit/commit/182ed349f60c2e7cacb002f12c3a3642001eb19e))
7
+
1
8
  ## [1.2.3](https://github.com/nxtxe/zod-envkit/compare/v1.2.2...v1.2.3) (2026-02-19)
2
9
 
3
10
 
@@ -20,10 +27,6 @@
20
27
 
21
28
  * strengthen CLI + env contract guarantees ([d516fbb](https://github.com/nxtxe/zod-envkit/commit/d516fbb4c8248745f7d74c1f07defaf4529c33ec))
22
29
 
23
- # Changelog
24
-
25
- All notable changes to this project will be documented in this file.
26
- This project follows [Semantic Versioning](https://semver.org/).
27
30
 
28
31
  ## [1.2.0] – 2026-02-16
29
32
 
@@ -223,3 +226,9 @@ This project follows [Semantic Versioning](https://semver.org/).
223
226
  * Small, framework-agnostic core
224
227
 
225
228
  [1.0.0]: https://www.npmjs.com/package/zod-envkit/v/1.0.0
229
+
230
+
231
+ # Changelog
232
+
233
+ All notable changes to this project will be documented in this file.
234
+ This project follows [Semantic Versioning](https://semver.org/).
@@ -57,7 +57,7 @@ function generateEnvExample(meta) {
57
57
  function buildRows(entries) {
58
58
  return entries.map(([key, m]) => {
59
59
  const req = m.required === false ? "no" : "yes";
60
- const dep = m.deprecated ? "\u26A0\uFE0F" : "";
60
+ const dep = m.deprecated === true ? "\u26A0\uFE0F" : typeof m.deprecated === "string" ? `\u26A0\uFE0F ${m.deprecated}` : "";
61
61
  const link = m.link ? m.link : "";
62
62
  return {
63
63
  Key: key,
@@ -182,16 +182,25 @@ function getUnknownEnv(meta, env = process.env) {
182
182
  return unknown;
183
183
  }
184
184
  var SECRET_PATTERNS = [
185
- (k) => k.includes("TOKEN"),
186
185
  (k) => k.includes("SECRET"),
187
186
  (k) => k.includes("PASSWORD"),
187
+ (k) => k.includes("PASS"),
188
+ (k) => k.includes("PWD"),
188
189
  (k) => k.includes("PRIVATE"),
189
190
  (k) => k.includes("API_KEY"),
190
- (k) => k.endsWith("_KEY")
191
+ (k) => k.endsWith("_KEY"),
192
+ (k) => k === "KEY",
193
+ (k) => k === "API",
194
+ (k) => k.includes("TOKEN"),
195
+ (k) => k.includes("JWT"),
196
+ (k) => k.includes("SESSION"),
197
+ (k) => k.includes("CREDENTIAL"),
198
+ (k) => k.includes("CREDS"),
199
+ (k) => k.includes("DATABASE_URL") || k === "DB_URL",
200
+ (k) => k.includes("CONNECTION_STRING")
191
201
  ];
192
202
  function isSecretKey(key) {
193
- const k = key.toUpperCase();
194
- return SECRET_PATTERNS.some((fn) => fn(k));
203
+ return SECRET_PATTERNS.some((fn) => fn(key.toUpperCase()));
195
204
  }
196
205
  function checkEnv(meta, env = process.env) {
197
206
  const missing = getMissingEnv(meta, env);
@@ -42,7 +42,13 @@ var messages = {
42
42
  INVALID_FORMAT: "Invalid docs format",
43
43
  INVALID_MASK_MODE: "Invalid mask mode",
44
44
  INVALID_SORT: "Invalid sort mode",
45
- INIT_INPUT_EMPTY: "Input env file is empty or not found:"
45
+ INIT_INPUT_EMPTY: "Input env file is empty or not found:",
46
+ SCHEMA_LOAD_FAILED: "Failed to load schema file:",
47
+ SCHEMA_NOT_OBJECT: "Schema file must export a Zod object (z.object(...)).",
48
+ SCHEMA_VARS_NOT_IN_META: "Schema variables not listed in env.meta.json:",
49
+ META_VARS_NOT_IN_SCHEMA: "env.meta.json variables not in schema:",
50
+ SCHEMA_HINT_ADD_TO_META: "Hint: add these keys to env.meta.json for docs and CLI.",
51
+ META_HINT_SYNC_SCHEMA: "Hint: add these to your Zod schema or remove from env.meta.json."
46
52
  },
47
53
  ru: {
48
54
  META_NOT_FOUND: "\u0424\u0430\u0439\u043B env.meta.json \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D.",
@@ -58,7 +64,13 @@ var messages = {
58
64
  INVALID_FORMAT: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430\u0446\u0438\u0438",
59
65
  INVALID_MASK_MODE: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0440\u0435\u0436\u0438\u043C \u043C\u0430\u0441\u043A\u0438\u0440\u043E\u0432\u043A\u0438",
60
66
  INVALID_SORT: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0440\u0435\u0436\u0438\u043C \u0441\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0438",
61
- INIT_INPUT_EMPTY: "\u0424\u0430\u0439\u043B \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u044F \u043F\u0443\u0441\u0442 \u0438\u043B\u0438 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D:"
67
+ INIT_INPUT_EMPTY: "\u0424\u0430\u0439\u043B \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u044F \u043F\u0443\u0441\u0442 \u0438\u043B\u0438 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D:",
68
+ SCHEMA_LOAD_FAILED: "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0444\u0430\u0439\u043B \u0441\u0445\u0435\u043C\u044B:",
69
+ SCHEMA_NOT_OBJECT: "\u0424\u0430\u0439\u043B \u0441\u0445\u0435\u043C\u044B \u0434\u043E\u043B\u0436\u0435\u043D \u044D\u043A\u0441\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C Zod object (z.object(...)).",
70
+ SCHEMA_VARS_NOT_IN_META: "\u041F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u044B\u0435 \u0441\u0445\u0435\u043C\u044B \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044E\u0442 \u0432 env.meta.json:",
71
+ META_VARS_NOT_IN_SCHEMA: "\u041F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u044B\u0435 env.meta.json \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044E\u0442 \u0432 \u0441\u0445\u0435\u043C\u0435:",
72
+ SCHEMA_HINT_ADD_TO_META: "\u041F\u043E\u0434\u0441\u043A\u0430\u0437\u043A\u0430: \u0434\u043E\u0431\u0430\u0432\u044C\u0442\u0435 \u044D\u0442\u0438 \u043A\u043B\u044E\u0447\u0438 \u0432 env.meta.json \u0434\u043B\u044F \u0434\u043E\u043A\u043E\u0432 \u0438 CLI.",
73
+ META_HINT_SYNC_SCHEMA: "\u041F\u043E\u0434\u0441\u043A\u0430\u0437\u043A\u0430: \u0434\u043E\u0431\u0430\u0432\u044C\u0442\u0435 \u0438\u0445 \u0432 Zod-\u0441\u0445\u0435\u043C\u0443 \u0438\u043B\u0438 \u0443\u0434\u0430\u043B\u0438\u0442\u0435 \u0438\u0437 env.meta.json."
62
74
  }
63
75
  };
64
76
 
@@ -226,7 +238,7 @@ function generateEnvExample(meta) {
226
238
  function buildRows(entries) {
227
239
  return entries.map(([key, m]) => {
228
240
  const req = m.required === false ? "no" : "yes";
229
- const dep = m.deprecated ? "\u26A0\uFE0F" : "";
241
+ const dep = m.deprecated === true ? "\u26A0\uFE0F" : typeof m.deprecated === "string" ? `\u26A0\uFE0F ${m.deprecated}` : "";
230
242
  const link = m.link ? m.link : "";
231
243
  return {
232
244
  Key: key,
@@ -422,16 +434,25 @@ function getUnknownEnv(meta, env = process.env) {
422
434
  return unknown;
423
435
  }
424
436
  var SECRET_PATTERNS = [
425
- (k) => k.includes("TOKEN"),
426
437
  (k) => k.includes("SECRET"),
427
438
  (k) => k.includes("PASSWORD"),
439
+ (k) => k.includes("PASS"),
440
+ (k) => k.includes("PWD"),
428
441
  (k) => k.includes("PRIVATE"),
429
442
  (k) => k.includes("API_KEY"),
430
- (k) => k.endsWith("_KEY")
443
+ (k) => k.endsWith("_KEY"),
444
+ (k) => k === "KEY",
445
+ (k) => k === "API",
446
+ (k) => k.includes("TOKEN"),
447
+ (k) => k.includes("JWT"),
448
+ (k) => k.includes("SESSION"),
449
+ (k) => k.includes("CREDENTIAL"),
450
+ (k) => k.includes("CREDS"),
451
+ (k) => k.includes("DATABASE_URL") || k === "DB_URL",
452
+ (k) => k.includes("CONNECTION_STRING")
431
453
  ];
432
454
  function isSecretKey(key) {
433
- const k = key.toUpperCase();
434
- return SECRET_PATTERNS.some((fn) => fn(k));
455
+ return SECRET_PATTERNS.some((fn) => fn(key.toUpperCase()));
435
456
  }
436
457
 
437
458
  // src/cli/lib/mask.ts
@@ -554,9 +575,46 @@ function registerShow(program2, getLang2) {
554
575
  });
555
576
  }
556
577
 
578
+ // src/cli/lib/schema.ts
579
+ var import_node_path3 = __toESM(require("path"), 1);
580
+ var import_node_module = require("module");
581
+ var import_node_url = require("url");
582
+ var require2 = (0, import_node_module.createRequire)(import_node_path3.default.join(process.cwd(), "package.json"));
583
+ async function loadSchemaFile(filePath, lang) {
584
+ const absolutePath = import_node_path3.default.isAbsolute(filePath) ? filePath : import_node_path3.default.resolve(process.cwd(), filePath);
585
+ let mod;
586
+ try {
587
+ const url = (0, import_node_url.pathToFileURL)(absolutePath).href;
588
+ mod = await import(url);
589
+ } catch {
590
+ try {
591
+ mod = require2(absolutePath);
592
+ } catch (e) {
593
+ fail(lang, "SCHEMA_LOAD_FAILED", [`- ${absolutePath}`, String(e)]);
594
+ }
595
+ }
596
+ const raw = mod != null && typeof mod === "object" ? mod.default ?? mod : void 0;
597
+ const schema = raw != null && typeof raw === "object" ? raw.default ?? raw.schema ?? raw : void 0;
598
+ if (schema == null) {
599
+ fail(lang, "SCHEMA_NOT_OBJECT", [`- ${absolutePath}`]);
600
+ }
601
+ const shape = schema != null && typeof schema === "object" && "shape" in schema && typeof schema.shape === "object" ? schema.shape : void 0;
602
+ if (shape == null || !Object.keys(shape).length) {
603
+ fail(lang, "SCHEMA_NOT_OBJECT", [`- ${absolutePath}`]);
604
+ }
605
+ return { keys: Object.keys(shape) };
606
+ }
607
+
557
608
  // src/cli/commands/check.ts
558
609
  function registerCheck(program2, getLang2) {
559
- program2.command("check").description("Exit with code 1 if env is invalid (loads dotenv)").option("-c, --config <file>", "Path to env meta json", "env.meta.json").option("--dotenv <list>", "Comma-separated dotenv files (default: .env)", ".env").option("--strict", "Fail if unknown env vars are present (dotenv-only)").action((opts) => {
610
+ program2.command("check").description("Exit with code 1 if env is invalid (loads dotenv)").option("-c, --config <file>", "Path to env meta json", "env.meta.json").option("--dotenv <list>", "Comma-separated dotenv files (default: .env)", ".env").option("--strict", "Fail if unknown env vars are present (dotenv-only)").option(
611
+ "--schema <file>",
612
+ "Path to JS file exporting Zod object; run schema\u2194meta consistency check"
613
+ ).option(
614
+ "--schema-mode <mode>",
615
+ "Schema\u2194meta consistency: warn (report, exit 0) or strict (report, exit 1)",
616
+ "strict"
617
+ ).action(async (opts) => {
560
618
  const lang = getLang2();
561
619
  const loaded = loadDotEnv(opts.dotenv);
562
620
  const { meta } = loadMeta(lang, opts.config);
@@ -570,6 +628,37 @@ function registerCheck(program2, getLang2) {
570
628
  fail(lang, "UNKNOWN_ENV", unknown.map((k) => `- ${k}`));
571
629
  }
572
630
  }
631
+ if (opts.schema) {
632
+ const schemaMode = String(opts.schemaMode ?? "strict").toLowerCase();
633
+ if (schemaMode !== "warn" && schemaMode !== "strict") {
634
+ fail(lang, "INVALID_FORMAT", ["--schema-mode must be warn or strict"]);
635
+ }
636
+ const { keys: schemaKeys } = await loadSchemaFile(opts.schema, lang);
637
+ const metaKeys = new Set(Object.keys(meta));
638
+ const inSchemaNotMeta = schemaKeys.filter((k) => !metaKeys.has(k));
639
+ const inMetaNotSchema = [...metaKeys].filter((k) => !schemaKeys.includes(k));
640
+ const hasMismatch = inSchemaNotMeta.length > 0 || inMetaNotSchema.length > 0;
641
+ if (hasMismatch) {
642
+ const lines = [];
643
+ if (inSchemaNotMeta.length) {
644
+ lines.push(`\u274C ${t(lang, "SCHEMA_VARS_NOT_IN_META")}`);
645
+ inSchemaNotMeta.forEach((k) => lines.push(`- ${k}`));
646
+ lines.push(` ${t(lang, "SCHEMA_HINT_ADD_TO_META")}`);
647
+ lines.push("");
648
+ }
649
+ if (inMetaNotSchema.length) {
650
+ lines.push(`\u274C ${t(lang, "META_VARS_NOT_IN_SCHEMA")}`);
651
+ inMetaNotSchema.sort((a, b) => a.localeCompare(b)).forEach((k) => lines.push(`- ${k}`));
652
+ lines.push(` ${t(lang, "META_HINT_SYNC_SCHEMA")}`);
653
+ }
654
+ const out = lines.join("\n");
655
+ if (schemaMode === "strict") {
656
+ console.error(out);
657
+ process.exit(1);
658
+ }
659
+ console.warn(out);
660
+ }
661
+ }
573
662
  console.log(`\u2705 ${t(lang, "ENV_OK")}`);
574
663
  process.exit(0);
575
664
  });
@@ -577,10 +666,10 @@ function registerCheck(program2, getLang2) {
577
666
 
578
667
  // src/cli/commands/init.ts
579
668
  var import_node_fs4 = __toESM(require("fs"), 1);
580
- var import_node_path3 = __toESM(require("path"), 1);
669
+ var import_node_path4 = __toESM(require("path"), 1);
581
670
  var import_dotenv5 = __toESM(require("dotenv"), 1);
582
671
  function readEnvFile(file) {
583
- const abs = import_node_path3.default.resolve(process.cwd(), file);
672
+ const abs = import_node_path4.default.resolve(process.cwd(), file);
584
673
  if (!import_node_fs4.default.existsSync(abs)) return {};
585
674
  const raw = import_node_fs4.default.readFileSync(abs, "utf8");
586
675
  return import_dotenv5.default.parse(raw);
package/dist/cli/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  getMissingEnv,
6
6
  getUnknownEnv,
7
7
  isSecretKey
8
- } from "../chunk-RI23O6XK.js";
8
+ } from "../chunk-J4V5ODI6.js";
9
9
 
10
10
  // src/cli/index.ts
11
11
  import { Command } from "commander";
@@ -26,7 +26,13 @@ var messages = {
26
26
  INVALID_FORMAT: "Invalid docs format",
27
27
  INVALID_MASK_MODE: "Invalid mask mode",
28
28
  INVALID_SORT: "Invalid sort mode",
29
- INIT_INPUT_EMPTY: "Input env file is empty or not found:"
29
+ INIT_INPUT_EMPTY: "Input env file is empty or not found:",
30
+ SCHEMA_LOAD_FAILED: "Failed to load schema file:",
31
+ SCHEMA_NOT_OBJECT: "Schema file must export a Zod object (z.object(...)).",
32
+ SCHEMA_VARS_NOT_IN_META: "Schema variables not listed in env.meta.json:",
33
+ META_VARS_NOT_IN_SCHEMA: "env.meta.json variables not in schema:",
34
+ SCHEMA_HINT_ADD_TO_META: "Hint: add these keys to env.meta.json for docs and CLI.",
35
+ META_HINT_SYNC_SCHEMA: "Hint: add these to your Zod schema or remove from env.meta.json."
30
36
  },
31
37
  ru: {
32
38
  META_NOT_FOUND: "\u0424\u0430\u0439\u043B env.meta.json \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D.",
@@ -42,7 +48,13 @@ var messages = {
42
48
  INVALID_FORMAT: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430\u0446\u0438\u0438",
43
49
  INVALID_MASK_MODE: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0440\u0435\u0436\u0438\u043C \u043C\u0430\u0441\u043A\u0438\u0440\u043E\u0432\u043A\u0438",
44
50
  INVALID_SORT: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0440\u0435\u0436\u0438\u043C \u0441\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0438",
45
- INIT_INPUT_EMPTY: "\u0424\u0430\u0439\u043B \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u044F \u043F\u0443\u0441\u0442 \u0438\u043B\u0438 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D:"
51
+ INIT_INPUT_EMPTY: "\u0424\u0430\u0439\u043B \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u044F \u043F\u0443\u0441\u0442 \u0438\u043B\u0438 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D:",
52
+ SCHEMA_LOAD_FAILED: "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0444\u0430\u0439\u043B \u0441\u0445\u0435\u043C\u044B:",
53
+ SCHEMA_NOT_OBJECT: "\u0424\u0430\u0439\u043B \u0441\u0445\u0435\u043C\u044B \u0434\u043E\u043B\u0436\u0435\u043D \u044D\u043A\u0441\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C Zod object (z.object(...)).",
54
+ SCHEMA_VARS_NOT_IN_META: "\u041F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u044B\u0435 \u0441\u0445\u0435\u043C\u044B \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044E\u0442 \u0432 env.meta.json:",
55
+ META_VARS_NOT_IN_SCHEMA: "\u041F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u044B\u0435 env.meta.json \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044E\u0442 \u0432 \u0441\u0445\u0435\u043C\u0435:",
56
+ SCHEMA_HINT_ADD_TO_META: "\u041F\u043E\u0434\u0441\u043A\u0430\u0437\u043A\u0430: \u0434\u043E\u0431\u0430\u0432\u044C\u0442\u0435 \u044D\u0442\u0438 \u043A\u043B\u044E\u0447\u0438 \u0432 env.meta.json \u0434\u043B\u044F \u0434\u043E\u043A\u043E\u0432 \u0438 CLI.",
57
+ META_HINT_SYNC_SCHEMA: "\u041F\u043E\u0434\u0441\u043A\u0430\u0437\u043A\u0430: \u0434\u043E\u0431\u0430\u0432\u044C\u0442\u0435 \u0438\u0445 \u0432 Zod-\u0441\u0445\u0435\u043C\u0443 \u0438\u043B\u0438 \u0443\u0434\u0430\u043B\u0438\u0442\u0435 \u0438\u0437 env.meta.json."
46
58
  }
47
59
  };
48
60
 
@@ -342,9 +354,46 @@ function registerShow(program2, getLang2) {
342
354
  });
343
355
  }
344
356
 
357
+ // src/cli/lib/schema.ts
358
+ import path3 from "path";
359
+ import { createRequire } from "module";
360
+ import { pathToFileURL } from "url";
361
+ var require2 = createRequire(path3.join(process.cwd(), "package.json"));
362
+ async function loadSchemaFile(filePath, lang) {
363
+ const absolutePath = path3.isAbsolute(filePath) ? filePath : path3.resolve(process.cwd(), filePath);
364
+ let mod;
365
+ try {
366
+ const url = pathToFileURL(absolutePath).href;
367
+ mod = await import(url);
368
+ } catch {
369
+ try {
370
+ mod = require2(absolutePath);
371
+ } catch (e) {
372
+ fail(lang, "SCHEMA_LOAD_FAILED", [`- ${absolutePath}`, String(e)]);
373
+ }
374
+ }
375
+ const raw = mod != null && typeof mod === "object" ? mod.default ?? mod : void 0;
376
+ const schema = raw != null && typeof raw === "object" ? raw.default ?? raw.schema ?? raw : void 0;
377
+ if (schema == null) {
378
+ fail(lang, "SCHEMA_NOT_OBJECT", [`- ${absolutePath}`]);
379
+ }
380
+ const shape = schema != null && typeof schema === "object" && "shape" in schema && typeof schema.shape === "object" ? schema.shape : void 0;
381
+ if (shape == null || !Object.keys(shape).length) {
382
+ fail(lang, "SCHEMA_NOT_OBJECT", [`- ${absolutePath}`]);
383
+ }
384
+ return { keys: Object.keys(shape) };
385
+ }
386
+
345
387
  // src/cli/commands/check.ts
346
388
  function registerCheck(program2, getLang2) {
347
- program2.command("check").description("Exit with code 1 if env is invalid (loads dotenv)").option("-c, --config <file>", "Path to env meta json", "env.meta.json").option("--dotenv <list>", "Comma-separated dotenv files (default: .env)", ".env").option("--strict", "Fail if unknown env vars are present (dotenv-only)").action((opts) => {
389
+ program2.command("check").description("Exit with code 1 if env is invalid (loads dotenv)").option("-c, --config <file>", "Path to env meta json", "env.meta.json").option("--dotenv <list>", "Comma-separated dotenv files (default: .env)", ".env").option("--strict", "Fail if unknown env vars are present (dotenv-only)").option(
390
+ "--schema <file>",
391
+ "Path to JS file exporting Zod object; run schema\u2194meta consistency check"
392
+ ).option(
393
+ "--schema-mode <mode>",
394
+ "Schema\u2194meta consistency: warn (report, exit 0) or strict (report, exit 1)",
395
+ "strict"
396
+ ).action(async (opts) => {
348
397
  const lang = getLang2();
349
398
  const loaded = loadDotEnv(opts.dotenv);
350
399
  const { meta } = loadMeta(lang, opts.config);
@@ -358,6 +407,37 @@ function registerCheck(program2, getLang2) {
358
407
  fail(lang, "UNKNOWN_ENV", unknown.map((k) => `- ${k}`));
359
408
  }
360
409
  }
410
+ if (opts.schema) {
411
+ const schemaMode = String(opts.schemaMode ?? "strict").toLowerCase();
412
+ if (schemaMode !== "warn" && schemaMode !== "strict") {
413
+ fail(lang, "INVALID_FORMAT", ["--schema-mode must be warn or strict"]);
414
+ }
415
+ const { keys: schemaKeys } = await loadSchemaFile(opts.schema, lang);
416
+ const metaKeys = new Set(Object.keys(meta));
417
+ const inSchemaNotMeta = schemaKeys.filter((k) => !metaKeys.has(k));
418
+ const inMetaNotSchema = [...metaKeys].filter((k) => !schemaKeys.includes(k));
419
+ const hasMismatch = inSchemaNotMeta.length > 0 || inMetaNotSchema.length > 0;
420
+ if (hasMismatch) {
421
+ const lines = [];
422
+ if (inSchemaNotMeta.length) {
423
+ lines.push(`\u274C ${t(lang, "SCHEMA_VARS_NOT_IN_META")}`);
424
+ inSchemaNotMeta.forEach((k) => lines.push(`- ${k}`));
425
+ lines.push(` ${t(lang, "SCHEMA_HINT_ADD_TO_META")}`);
426
+ lines.push("");
427
+ }
428
+ if (inMetaNotSchema.length) {
429
+ lines.push(`\u274C ${t(lang, "META_VARS_NOT_IN_SCHEMA")}`);
430
+ inMetaNotSchema.sort((a, b) => a.localeCompare(b)).forEach((k) => lines.push(`- ${k}`));
431
+ lines.push(` ${t(lang, "META_HINT_SYNC_SCHEMA")}`);
432
+ }
433
+ const out = lines.join("\n");
434
+ if (schemaMode === "strict") {
435
+ console.error(out);
436
+ process.exit(1);
437
+ }
438
+ console.warn(out);
439
+ }
440
+ }
361
441
  console.log(`\u2705 ${t(lang, "ENV_OK")}`);
362
442
  process.exit(0);
363
443
  });
@@ -365,10 +445,10 @@ function registerCheck(program2, getLang2) {
365
445
 
366
446
  // src/cli/commands/init.ts
367
447
  import fs4 from "fs";
368
- import path3 from "path";
448
+ import path4 from "path";
369
449
  import dotenv3 from "dotenv";
370
450
  function readEnvFile(file) {
371
- const abs = path3.resolve(process.cwd(), file);
451
+ const abs = path4.resolve(process.cwd(), file);
372
452
  if (!fs4.existsSync(abs)) return {};
373
453
  const raw = fs4.readFileSync(abs, "utf8");
374
454
  return dotenv3.parse(raw);
package/dist/index.cjs CHANGED
@@ -92,7 +92,7 @@ function generateEnvExample(meta) {
92
92
  function buildRows(entries) {
93
93
  return entries.map(([key, m]) => {
94
94
  const req = m.required === false ? "no" : "yes";
95
- const dep = m.deprecated ? "\u26A0\uFE0F" : "";
95
+ const dep = m.deprecated === true ? "\u26A0\uFE0F" : typeof m.deprecated === "string" ? `\u26A0\uFE0F ${m.deprecated}` : "";
96
96
  const link = m.link ? m.link : "";
97
97
  return {
98
98
  Key: key,
@@ -217,16 +217,25 @@ function getUnknownEnv(meta, env = process.env) {
217
217
  return unknown;
218
218
  }
219
219
  var SECRET_PATTERNS = [
220
- (k) => k.includes("TOKEN"),
221
220
  (k) => k.includes("SECRET"),
222
221
  (k) => k.includes("PASSWORD"),
222
+ (k) => k.includes("PASS"),
223
+ (k) => k.includes("PWD"),
223
224
  (k) => k.includes("PRIVATE"),
224
225
  (k) => k.includes("API_KEY"),
225
- (k) => k.endsWith("_KEY")
226
+ (k) => k.endsWith("_KEY"),
227
+ (k) => k === "KEY",
228
+ (k) => k === "API",
229
+ (k) => k.includes("TOKEN"),
230
+ (k) => k.includes("JWT"),
231
+ (k) => k.includes("SESSION"),
232
+ (k) => k.includes("CREDENTIAL"),
233
+ (k) => k.includes("CREDS"),
234
+ (k) => k.includes("DATABASE_URL") || k === "DB_URL",
235
+ (k) => k.includes("CONNECTION_STRING")
226
236
  ];
227
237
  function isSecretKey(key) {
228
- const k = key.toUpperCase();
229
- return SECRET_PATTERNS.some((fn) => fn(k));
238
+ return SECRET_PATTERNS.some((fn) => fn(key.toUpperCase()));
230
239
  }
231
240
  function checkEnv(meta, env = process.env) {
232
241
  const missing = getMissingEnv(meta, env);
@@ -253,7 +262,8 @@ function formatZodError(err) {
253
262
  return pa.localeCompare(pb);
254
263
  }).map((issue) => {
255
264
  const path = issue.path.length ? issue.path.join(".") : "(root)";
256
- return `- ${path}: ${issue.message.trim()}`;
265
+ const msg = issue.message.trim() || "(validation failed)";
266
+ return `- ${path}: ${msg}`;
257
267
  }).join("\n");
258
268
  }
259
269
  // Annotate the CommonJS export names for ESM import in node:
package/dist/index.d.cts CHANGED
@@ -6,6 +6,8 @@ import { z } from 'zod';
6
6
  * This file is part of the stable public API: it powers the CLI generators,
7
7
  * and is intended to be reusable by consumers.
8
8
  *
9
+ * Stable in 1.2.
10
+ *
9
11
  * @public
10
12
  * @since 1.0.0
11
13
  */
@@ -40,10 +42,11 @@ type EnvMetaEntry = {
40
42
  default?: string;
41
43
  /**
42
44
  * Mark variable as deprecated in docs.
45
+ * Use `true` for a generic warning (⚠️) or a string to show an explanation (e.g. "Use FOO instead").
43
46
  *
44
47
  * @since 1.1.0
45
48
  */
46
- deprecated?: boolean;
49
+ deprecated?: boolean | string;
47
50
  /**
48
51
  * Version when the variable was introduced (documentation only).
49
52
  *
@@ -142,6 +145,9 @@ declare function generateEnvExample(meta: EnvMeta): string;
142
145
  */
143
146
  declare function generateEnvDocs(meta: EnvMeta, opts?: GenerateDocsOptions): string;
144
147
 
148
+ /**
149
+ * Env validation helpers. Stable in 1.2.
150
+ */
145
151
  /**
146
152
  * Result of validating an env object against {@link EnvMeta}.
147
153
  *
@@ -175,7 +181,8 @@ declare function getUnknownEnv(meta: EnvMeta, env?: NodeJS.ProcessEnv): string[]
175
181
  /**
176
182
  * Detect whether an env key name looks like a secret.
177
183
  *
178
- * Used by the CLI to mask values (TOKEN/SECRET/PASSWORD/*_KEY/PRIVATE).
184
+ * Used by the CLI to mask values (e.g. SECRET, PASSWORD, TOKEN, *_KEY, connection strings).
185
+ * Matching is case-insensitive (key is normalized to uppercase).
179
186
  *
180
187
  * @public
181
188
  * @since 1.2.0
@@ -241,6 +248,9 @@ type LoadEnvFail = {
241
248
  * - returns `{ ok: false, error }` by default
242
249
  * - throws `ZodError` if `opts.throwOnError === true`
243
250
  *
251
+ * @remarks Load dotenv (e.g. `import "dotenv/config"`) before calling so `process.env` is populated.
252
+ * @see {@link mustLoadEnv} — fail-fast variant that returns env directly.
253
+ *
244
254
  * @example
245
255
  * ```ts
246
256
  * const result = loadEnv(EnvSchema);
@@ -260,6 +270,9 @@ declare function loadEnv<T extends z.ZodTypeAny>(schema: T, opts?: LoadEnvOption
260
270
  *
261
271
  * Equivalent to: `loadEnv(schema, { throwOnError: true })` but returns typed env directly.
262
272
  *
273
+ * @remarks Load dotenv (e.g. `import "dotenv/config"`) before calling so `process.env` is populated.
274
+ * @see {@link loadEnv} — returns a result object instead of throwing.
275
+ *
263
276
  * @example
264
277
  * ```ts
265
278
  * export const env = mustLoadEnv(EnvSchema);
@@ -274,8 +287,13 @@ declare function mustLoadEnv<T extends z.ZodTypeAny>(schema: T): z.infer<T>;
274
287
  /**
275
288
  * Format `ZodError` into a human-friendly multi-line message (one issue per line).
276
289
  *
277
- * Output format (stable in 1.x):
278
- * - path: message
290
+ * Output format (stable in 1.x): `path: message`
291
+ *
292
+ * @example
293
+ * ```ts
294
+ * const result = loadEnv(EnvSchema);
295
+ * if (!result.ok) console.error(formatZodError(result.error));
296
+ * ```
279
297
  *
280
298
  * @public
281
299
  * @since 1.0.0
package/dist/index.d.ts CHANGED
@@ -6,6 +6,8 @@ import { z } from 'zod';
6
6
  * This file is part of the stable public API: it powers the CLI generators,
7
7
  * and is intended to be reusable by consumers.
8
8
  *
9
+ * Stable in 1.2.
10
+ *
9
11
  * @public
10
12
  * @since 1.0.0
11
13
  */
@@ -40,10 +42,11 @@ type EnvMetaEntry = {
40
42
  default?: string;
41
43
  /**
42
44
  * Mark variable as deprecated in docs.
45
+ * Use `true` for a generic warning (⚠️) or a string to show an explanation (e.g. "Use FOO instead").
43
46
  *
44
47
  * @since 1.1.0
45
48
  */
46
- deprecated?: boolean;
49
+ deprecated?: boolean | string;
47
50
  /**
48
51
  * Version when the variable was introduced (documentation only).
49
52
  *
@@ -142,6 +145,9 @@ declare function generateEnvExample(meta: EnvMeta): string;
142
145
  */
143
146
  declare function generateEnvDocs(meta: EnvMeta, opts?: GenerateDocsOptions): string;
144
147
 
148
+ /**
149
+ * Env validation helpers. Stable in 1.2.
150
+ */
145
151
  /**
146
152
  * Result of validating an env object against {@link EnvMeta}.
147
153
  *
@@ -175,7 +181,8 @@ declare function getUnknownEnv(meta: EnvMeta, env?: NodeJS.ProcessEnv): string[]
175
181
  /**
176
182
  * Detect whether an env key name looks like a secret.
177
183
  *
178
- * Used by the CLI to mask values (TOKEN/SECRET/PASSWORD/*_KEY/PRIVATE).
184
+ * Used by the CLI to mask values (e.g. SECRET, PASSWORD, TOKEN, *_KEY, connection strings).
185
+ * Matching is case-insensitive (key is normalized to uppercase).
179
186
  *
180
187
  * @public
181
188
  * @since 1.2.0
@@ -241,6 +248,9 @@ type LoadEnvFail = {
241
248
  * - returns `{ ok: false, error }` by default
242
249
  * - throws `ZodError` if `opts.throwOnError === true`
243
250
  *
251
+ * @remarks Load dotenv (e.g. `import "dotenv/config"`) before calling so `process.env` is populated.
252
+ * @see {@link mustLoadEnv} — fail-fast variant that returns env directly.
253
+ *
244
254
  * @example
245
255
  * ```ts
246
256
  * const result = loadEnv(EnvSchema);
@@ -260,6 +270,9 @@ declare function loadEnv<T extends z.ZodTypeAny>(schema: T, opts?: LoadEnvOption
260
270
  *
261
271
  * Equivalent to: `loadEnv(schema, { throwOnError: true })` but returns typed env directly.
262
272
  *
273
+ * @remarks Load dotenv (e.g. `import "dotenv/config"`) before calling so `process.env` is populated.
274
+ * @see {@link loadEnv} — returns a result object instead of throwing.
275
+ *
263
276
  * @example
264
277
  * ```ts
265
278
  * export const env = mustLoadEnv(EnvSchema);
@@ -274,8 +287,13 @@ declare function mustLoadEnv<T extends z.ZodTypeAny>(schema: T): z.infer<T>;
274
287
  /**
275
288
  * Format `ZodError` into a human-friendly multi-line message (one issue per line).
276
289
  *
277
- * Output format (stable in 1.x):
278
- * - path: message
290
+ * Output format (stable in 1.x): `path: message`
291
+ *
292
+ * @example
293
+ * ```ts
294
+ * const result = loadEnv(EnvSchema);
295
+ * if (!result.ok) console.error(formatZodError(result.error));
296
+ * ```
279
297
  *
280
298
  * @public
281
299
  * @since 1.0.0
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  getUnknownEnv,
7
7
  isSecretKey,
8
8
  sortMetaEntries
9
- } from "./chunk-RI23O6XK.js";
9
+ } from "./chunk-J4V5ODI6.js";
10
10
 
11
11
  // src/index.ts
12
12
  function loadEnv(schema, opts) {
@@ -27,7 +27,8 @@ function formatZodError(err) {
27
27
  return pa.localeCompare(pb);
28
28
  }).map((issue) => {
29
29
  const path = issue.path.length ? issue.path.join(".") : "(root)";
30
- return `- ${path}: ${issue.message.trim()}`;
30
+ const msg = issue.message.trim() || "(validation failed)";
31
+ return `- ${path}: ${msg}`;
31
32
  }).join("\n");
32
33
  }
33
34
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zod-envkit",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "Validate environment variables with Zod and generate .env.example",
5
5
  "license": "MIT",
6
6
  "author": "",
@@ -53,8 +53,9 @@
53
53
  "ci:all": "pnpm ci:smoke && pnpm test && pnpm ci:docs",
54
54
  "prepublishOnly": "pnpm build && pnpm test",
55
55
  "docs:api": "typedoc --tsconfig tsconfig.docs.json --entryPoints src/index.ts",
56
- "docs:build": "pnpm docs:api && vitepress build docs",
57
- "docs:dev": "vitepress dev docs",
56
+ "docs:build": "pnpm docs:api && node scripts/copy-changelog-to-docs.cjs && vitepress build docs",
57
+ "docs:dev": "node scripts/copy-changelog-to-docs.cjs && vitepress dev docs",
58
+ "docs:links": "find docs -name '*.md' -not -path '*/.vitepress/*' -exec pnpm exec markdown-link-check -c docs/.markdown-link-check.json {} \\;",
58
59
  "release": "semantic-release"
59
60
  },
60
61
  "dependencies": {
@@ -72,6 +73,7 @@
72
73
  "@types/node": "^25.0.10",
73
74
  "eslint": "^9.39.2",
74
75
  "execa": "^9.6.1",
76
+ "markdown-link-check": "3.10.0",
75
77
  "semantic-release": "^22.0.12",
76
78
  "tmp-promise": "^3.0.3",
77
79
  "tsup": "^8.5.1",