zod-envkit 1.2.2 → 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 +20 -4
- package/dist/{chunk-RI23O6XK.js → chunk-J4V5ODI6.js} +14 -5
- package/dist/cli/index.cjs +160 -33
- package/dist/cli/index.js +147 -29
- package/dist/index.cjs +23 -6
- package/dist/index.d.cts +19 -3
- package/dist/index.d.ts +19 -3
- package/dist/index.js +10 -2
- package/package.json +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
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
|
+
|
|
8
|
+
## [1.2.3](https://github.com/nxtxe/zod-envkit/compare/v1.2.2...v1.2.3) (2026-02-19)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* reliability polish (meta fallback, strict isolation, deterministic errors) ([a439230](https://github.com/nxtxe/zod-envkit/commit/a439230ccd06f7c7dbd70963cdfe33ed9fae9c57))
|
|
14
|
+
|
|
1
15
|
## [1.2.2](https://github.com/nxtxe/zod-envkit/compare/v1.2.1...v1.2.2) (2026-02-16)
|
|
2
16
|
|
|
3
17
|
|
|
@@ -13,10 +27,6 @@
|
|
|
13
27
|
|
|
14
28
|
* strengthen CLI + env contract guarantees ([d516fbb](https://github.com/nxtxe/zod-envkit/commit/d516fbb4c8248745f7d74c1f07defaf4529c33ec))
|
|
15
29
|
|
|
16
|
-
# Changelog
|
|
17
|
-
|
|
18
|
-
All notable changes to this project will be documented in this file.
|
|
19
|
-
This project follows [Semantic Versioning](https://semver.org/).
|
|
20
30
|
|
|
21
31
|
## [1.2.0] – 2026-02-16
|
|
22
32
|
|
|
@@ -216,3 +226,9 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
216
226
|
* Small, framework-agnostic core
|
|
217
227
|
|
|
218
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
|
-
|
|
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);
|
package/dist/cli/index.cjs
CHANGED
|
@@ -33,6 +33,8 @@ var messages = {
|
|
|
33
33
|
META_TRIED: "Tried:",
|
|
34
34
|
META_TIP: "Tip:",
|
|
35
35
|
META_PARSE_FAILED: "Failed to read/parse env meta file:",
|
|
36
|
+
META_FALLBACK_EXAMPLE: "env.meta.json not found, falling back to .env.example (minimal meta).",
|
|
37
|
+
META_FALLBACK_TIP: "Tip: create env.meta.json for richer docs, grouping, and stable CLI behavior.",
|
|
36
38
|
GENERATED: "Generated: {example}, {docs}",
|
|
37
39
|
ENV_OK: "Environment looks good.",
|
|
38
40
|
MISSING_ENV: "Missing required environment variables:",
|
|
@@ -40,13 +42,21 @@ var messages = {
|
|
|
40
42
|
INVALID_FORMAT: "Invalid docs format",
|
|
41
43
|
INVALID_MASK_MODE: "Invalid mask mode",
|
|
42
44
|
INVALID_SORT: "Invalid sort mode",
|
|
43
|
-
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."
|
|
44
52
|
},
|
|
45
53
|
ru: {
|
|
46
54
|
META_NOT_FOUND: "\u0424\u0430\u0439\u043B env.meta.json \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D.",
|
|
47
55
|
META_TRIED: "\u041F\u0440\u043E\u0431\u043E\u0432\u0430\u043B\u0438:",
|
|
48
56
|
META_TIP: "\u041F\u043E\u0434\u0441\u043A\u0430\u0437\u043A\u0430:",
|
|
49
57
|
META_PARSE_FAILED: "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C \u0438\u043B\u0438 \u0440\u0430\u0441\u043F\u0430\u0440\u0441\u0438\u0442\u044C env meta \u0444\u0430\u0439\u043B:",
|
|
58
|
+
META_FALLBACK_EXAMPLE: "env.meta.json \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D, \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u043C .env.example \u043A\u0430\u043A \u043C\u0438\u043D\u0438\u043C\u0430\u043B\u044C\u043D\u0443\u044E \u043C\u0435\u0442\u0443.",
|
|
59
|
+
META_FALLBACK_TIP: "\u041F\u043E\u0434\u0441\u043A\u0430\u0437\u043A\u0430: \u043B\u0443\u0447\u0448\u0435 \u0441\u043E\u0437\u0434\u0430\u0442\u044C env.meta.json \u2014 \u0431\u0443\u0434\u0435\u0442 \u0431\u043E\u0433\u0430\u0447\u0435 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430\u0446\u0438\u044F \u0438 \u0441\u0442\u0430\u0431\u0438\u043B\u044C\u043D\u0435\u0435 CLI-\u043A\u043E\u043D\u0442\u0440\u0430\u043A\u0442.",
|
|
50
60
|
GENERATED: "\u0421\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043E: {example}, {docs}",
|
|
51
61
|
ENV_OK: "\u041F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u044B\u0435 \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u044F \u0432 \u043F\u043E\u0440\u044F\u0434\u043A\u0435.",
|
|
52
62
|
MISSING_ENV: "\u041E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044E\u0442 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u044B\u0435 \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u044F:",
|
|
@@ -54,11 +64,20 @@ var messages = {
|
|
|
54
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",
|
|
55
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",
|
|
56
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",
|
|
57
|
-
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."
|
|
58
74
|
}
|
|
59
75
|
};
|
|
60
76
|
|
|
61
77
|
// src/i18n.ts
|
|
78
|
+
function escapeRegExp(source) {
|
|
79
|
+
return source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
80
|
+
}
|
|
62
81
|
function resolveLang(cliLang) {
|
|
63
82
|
if (cliLang === "ru" || cliLang === "en") return cliLang;
|
|
64
83
|
const envLang = process.env.LANG?.toLowerCase();
|
|
@@ -69,7 +88,8 @@ function t(lang, key, vars) {
|
|
|
69
88
|
let text = messages[lang][key] ?? messages.en[key];
|
|
70
89
|
if (vars) {
|
|
71
90
|
for (const [k, v] of Object.entries(vars)) {
|
|
72
|
-
|
|
91
|
+
const pattern = new RegExp(`\\{${escapeRegExp(k)}\\}`, "g");
|
|
92
|
+
text = text.replace(pattern, v);
|
|
73
93
|
}
|
|
74
94
|
}
|
|
75
95
|
return text;
|
|
@@ -95,6 +115,7 @@ var import_node_fs2 = __toESM(require("fs"), 1);
|
|
|
95
115
|
// src/cli/lib/meta.ts
|
|
96
116
|
var import_node_fs = __toESM(require("fs"), 1);
|
|
97
117
|
var import_node_path = __toESM(require("path"), 1);
|
|
118
|
+
var import_dotenv = __toESM(require("dotenv"), 1);
|
|
98
119
|
|
|
99
120
|
// src/cli/lib/fail.ts
|
|
100
121
|
function fail(lang, key, details) {
|
|
@@ -118,24 +139,44 @@ function resolveMetaPath(configFile) {
|
|
|
118
139
|
const found = candidates.find((p) => import_node_fs.default.existsSync(p));
|
|
119
140
|
return { candidates, found };
|
|
120
141
|
}
|
|
142
|
+
function buildMetaFromEnvExample(examplePath) {
|
|
143
|
+
const raw = import_node_fs.default.readFileSync(examplePath, "utf8");
|
|
144
|
+
const parsed = import_dotenv.default.parse(raw);
|
|
145
|
+
const meta = {};
|
|
146
|
+
const keys = Object.keys(parsed).sort((a, b) => a.localeCompare(b));
|
|
147
|
+
for (const key of keys) {
|
|
148
|
+
meta[key] = {
|
|
149
|
+
example: parsed[key] ?? "",
|
|
150
|
+
required: true,
|
|
151
|
+
description: ""
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return meta;
|
|
155
|
+
}
|
|
121
156
|
function loadMeta(lang, configFile) {
|
|
122
157
|
const { candidates, found } = resolveMetaPath(configFile);
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
]);
|
|
158
|
+
if (found) {
|
|
159
|
+
try {
|
|
160
|
+
const raw = import_node_fs.default.readFileSync(found, "utf8");
|
|
161
|
+
return { meta: JSON.parse(raw), configPath: found };
|
|
162
|
+
} catch {
|
|
163
|
+
fail(lang, "META_PARSE_FAILED", [`- ${found}`]);
|
|
164
|
+
}
|
|
131
165
|
}
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
166
|
+
const examplePath = import_node_path.default.resolve(process.cwd(), ".env.example");
|
|
167
|
+
if (import_node_fs.default.existsSync(examplePath)) {
|
|
168
|
+
console.warn(`\u26A0\uFE0F ${t(lang, "META_FALLBACK_EXAMPLE")}`);
|
|
169
|
+
console.warn(` ${t(lang, "META_FALLBACK_TIP")}`);
|
|
170
|
+
console.warn("");
|
|
171
|
+
return { meta: buildMetaFromEnvExample(examplePath), configPath: examplePath };
|
|
138
172
|
}
|
|
173
|
+
fail(lang, "META_NOT_FOUND", [
|
|
174
|
+
t(lang, "META_TRIED"),
|
|
175
|
+
...candidates.map((p) => `- ${p}`),
|
|
176
|
+
"",
|
|
177
|
+
t(lang, "META_TIP"),
|
|
178
|
+
" npx zod-envkit show -c examples/env.meta.json"
|
|
179
|
+
]);
|
|
139
180
|
}
|
|
140
181
|
|
|
141
182
|
// src/generate.ts
|
|
@@ -197,7 +238,7 @@ function generateEnvExample(meta) {
|
|
|
197
238
|
function buildRows(entries) {
|
|
198
239
|
return entries.map(([key, m]) => {
|
|
199
240
|
const req = m.required === false ? "no" : "yes";
|
|
200
|
-
const dep = m.deprecated ? "\u26A0\uFE0F" : "";
|
|
241
|
+
const dep = m.deprecated === true ? "\u26A0\uFE0F" : typeof m.deprecated === "string" ? `\u26A0\uFE0F ${m.deprecated}` : "";
|
|
201
242
|
const link = m.link ? m.link : "";
|
|
202
243
|
return {
|
|
203
244
|
Key: key,
|
|
@@ -345,12 +386,15 @@ function registerGenerate(program2, getLang2) {
|
|
|
345
386
|
// src/cli/lib/dotenv.ts
|
|
346
387
|
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
347
388
|
var import_node_path2 = __toESM(require("path"), 1);
|
|
348
|
-
var
|
|
389
|
+
var import_dotenv2 = __toESM(require("dotenv"), 1);
|
|
349
390
|
function resolvePath(p) {
|
|
350
391
|
return import_node_path2.default.resolve(process.cwd(), p);
|
|
351
392
|
}
|
|
352
393
|
function loadDotEnv(files) {
|
|
353
|
-
|
|
394
|
+
let list = files?.split(",").map((s) => s.trim()).filter(Boolean) ?? [".env"];
|
|
395
|
+
if (list.length === 0) {
|
|
396
|
+
list = [".env"];
|
|
397
|
+
}
|
|
354
398
|
const loaded = [];
|
|
355
399
|
const skipped = [];
|
|
356
400
|
const merged = {};
|
|
@@ -361,7 +405,7 @@ function loadDotEnv(files) {
|
|
|
361
405
|
continue;
|
|
362
406
|
}
|
|
363
407
|
const raw = import_node_fs3.default.readFileSync(abs, "utf8");
|
|
364
|
-
const parsed =
|
|
408
|
+
const parsed = import_dotenv2.default.parse(raw);
|
|
365
409
|
for (const [k, v] of Object.entries(parsed)) merged[k] = v;
|
|
366
410
|
for (const [k, v] of Object.entries(parsed)) process.env[k] = v;
|
|
367
411
|
loaded.push(f);
|
|
@@ -390,16 +434,25 @@ function getUnknownEnv(meta, env = process.env) {
|
|
|
390
434
|
return unknown;
|
|
391
435
|
}
|
|
392
436
|
var SECRET_PATTERNS = [
|
|
393
|
-
(k) => k.includes("TOKEN"),
|
|
394
437
|
(k) => k.includes("SECRET"),
|
|
395
438
|
(k) => k.includes("PASSWORD"),
|
|
439
|
+
(k) => k.includes("PASS"),
|
|
440
|
+
(k) => k.includes("PWD"),
|
|
396
441
|
(k) => k.includes("PRIVATE"),
|
|
397
442
|
(k) => k.includes("API_KEY"),
|
|
398
|
-
(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")
|
|
399
453
|
];
|
|
400
454
|
function isSecretKey(key) {
|
|
401
|
-
|
|
402
|
-
return SECRET_PATTERNS.some((fn) => fn(k));
|
|
455
|
+
return SECRET_PATTERNS.some((fn) => fn(key.toUpperCase()));
|
|
403
456
|
}
|
|
404
457
|
|
|
405
458
|
// src/cli/lib/mask.ts
|
|
@@ -479,12 +532,18 @@ function isSortMode2(v) {
|
|
|
479
532
|
return v === "alpha" || v === "required-first" || v === "none";
|
|
480
533
|
}
|
|
481
534
|
function registerShow(program2, getLang2) {
|
|
482
|
-
program2.command("show").description("Show current env status (loads dotenv, masks secrets)").option("-c, --config <file>", "Path to env meta json", "env.meta.json").option("--dotenv <list>", "Comma-separated dotenv files (default: .env)", ".env").option("--mask-mode <mode>", "Mask mode (partial | full | none)"
|
|
535
|
+
program2.command("show").description("Show current env status (loads dotenv, masks secrets)").option("-c, --config <file>", "Path to env meta json", "env.meta.json").option("--dotenv <list>", "Comma-separated dotenv files (default: .env)", ".env").option("--mask-mode <mode>", "Mask mode (partial | full | none)").option("--no-mask", "Alias for --mask-mode none").option("--sort <mode>", "Sort mode (alpha | required-first | none)", "none").action((opts) => {
|
|
483
536
|
const lang = getLang2();
|
|
484
537
|
loadDotEnv(String(opts.dotenv ?? ".env"));
|
|
485
538
|
const { meta } = loadMeta(lang, String(opts.config ?? "env.meta.json"));
|
|
486
|
-
|
|
487
|
-
|
|
539
|
+
let modeRaw;
|
|
540
|
+
if (opts.mask === false) {
|
|
541
|
+
modeRaw = "none";
|
|
542
|
+
} else if (opts.maskMode) {
|
|
543
|
+
modeRaw = String(opts.maskMode);
|
|
544
|
+
} else {
|
|
545
|
+
modeRaw = "partial";
|
|
546
|
+
}
|
|
488
547
|
if (!isMaskMode(modeRaw)) {
|
|
489
548
|
console.error(`\u274C ${t(lang, "INVALID_MASK_MODE")}: ${modeRaw}`);
|
|
490
549
|
console.error(`- partial | full | none`);
|
|
@@ -516,9 +575,46 @@ function registerShow(program2, getLang2) {
|
|
|
516
575
|
});
|
|
517
576
|
}
|
|
518
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
|
+
|
|
519
608
|
// src/cli/commands/check.ts
|
|
520
609
|
function registerCheck(program2, getLang2) {
|
|
521
|
-
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)").
|
|
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) => {
|
|
522
618
|
const lang = getLang2();
|
|
523
619
|
const loaded = loadDotEnv(opts.dotenv);
|
|
524
620
|
const { meta } = loadMeta(lang, opts.config);
|
|
@@ -532,6 +628,37 @@ function registerCheck(program2, getLang2) {
|
|
|
532
628
|
fail(lang, "UNKNOWN_ENV", unknown.map((k) => `- ${k}`));
|
|
533
629
|
}
|
|
534
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
|
+
}
|
|
535
662
|
console.log(`\u2705 ${t(lang, "ENV_OK")}`);
|
|
536
663
|
process.exit(0);
|
|
537
664
|
});
|
|
@@ -539,13 +666,13 @@ function registerCheck(program2, getLang2) {
|
|
|
539
666
|
|
|
540
667
|
// src/cli/commands/init.ts
|
|
541
668
|
var import_node_fs4 = __toESM(require("fs"), 1);
|
|
542
|
-
var
|
|
543
|
-
var
|
|
669
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
670
|
+
var import_dotenv5 = __toESM(require("dotenv"), 1);
|
|
544
671
|
function readEnvFile(file) {
|
|
545
|
-
const abs =
|
|
672
|
+
const abs = import_node_path4.default.resolve(process.cwd(), file);
|
|
546
673
|
if (!import_node_fs4.default.existsSync(abs)) return {};
|
|
547
674
|
const raw = import_node_fs4.default.readFileSync(abs, "utf8");
|
|
548
|
-
return
|
|
675
|
+
return import_dotenv5.default.parse(raw);
|
|
549
676
|
}
|
|
550
677
|
function metaFromEnv(env, defaultGroup) {
|
|
551
678
|
const meta = {};
|
package/dist/cli/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
getMissingEnv,
|
|
6
6
|
getUnknownEnv,
|
|
7
7
|
isSecretKey
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-J4V5ODI6.js";
|
|
9
9
|
|
|
10
10
|
// src/cli/index.ts
|
|
11
11
|
import { Command } from "commander";
|
|
@@ -17,6 +17,8 @@ var messages = {
|
|
|
17
17
|
META_TRIED: "Tried:",
|
|
18
18
|
META_TIP: "Tip:",
|
|
19
19
|
META_PARSE_FAILED: "Failed to read/parse env meta file:",
|
|
20
|
+
META_FALLBACK_EXAMPLE: "env.meta.json not found, falling back to .env.example (minimal meta).",
|
|
21
|
+
META_FALLBACK_TIP: "Tip: create env.meta.json for richer docs, grouping, and stable CLI behavior.",
|
|
20
22
|
GENERATED: "Generated: {example}, {docs}",
|
|
21
23
|
ENV_OK: "Environment looks good.",
|
|
22
24
|
MISSING_ENV: "Missing required environment variables:",
|
|
@@ -24,13 +26,21 @@ var messages = {
|
|
|
24
26
|
INVALID_FORMAT: "Invalid docs format",
|
|
25
27
|
INVALID_MASK_MODE: "Invalid mask mode",
|
|
26
28
|
INVALID_SORT: "Invalid sort mode",
|
|
27
|
-
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."
|
|
28
36
|
},
|
|
29
37
|
ru: {
|
|
30
38
|
META_NOT_FOUND: "\u0424\u0430\u0439\u043B env.meta.json \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D.",
|
|
31
39
|
META_TRIED: "\u041F\u0440\u043E\u0431\u043E\u0432\u0430\u043B\u0438:",
|
|
32
40
|
META_TIP: "\u041F\u043E\u0434\u0441\u043A\u0430\u0437\u043A\u0430:",
|
|
33
41
|
META_PARSE_FAILED: "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C \u0438\u043B\u0438 \u0440\u0430\u0441\u043F\u0430\u0440\u0441\u0438\u0442\u044C env meta \u0444\u0430\u0439\u043B:",
|
|
42
|
+
META_FALLBACK_EXAMPLE: "env.meta.json \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D, \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u043C .env.example \u043A\u0430\u043A \u043C\u0438\u043D\u0438\u043C\u0430\u043B\u044C\u043D\u0443\u044E \u043C\u0435\u0442\u0443.",
|
|
43
|
+
META_FALLBACK_TIP: "\u041F\u043E\u0434\u0441\u043A\u0430\u0437\u043A\u0430: \u043B\u0443\u0447\u0448\u0435 \u0441\u043E\u0437\u0434\u0430\u0442\u044C env.meta.json \u2014 \u0431\u0443\u0434\u0435\u0442 \u0431\u043E\u0433\u0430\u0447\u0435 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430\u0446\u0438\u044F \u0438 \u0441\u0442\u0430\u0431\u0438\u043B\u044C\u043D\u0435\u0435 CLI-\u043A\u043E\u043D\u0442\u0440\u0430\u043A\u0442.",
|
|
34
44
|
GENERATED: "\u0421\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043E: {example}, {docs}",
|
|
35
45
|
ENV_OK: "\u041F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u044B\u0435 \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u044F \u0432 \u043F\u043E\u0440\u044F\u0434\u043A\u0435.",
|
|
36
46
|
MISSING_ENV: "\u041E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044E\u0442 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u044B\u0435 \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u044F:",
|
|
@@ -38,11 +48,20 @@ var messages = {
|
|
|
38
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",
|
|
39
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",
|
|
40
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",
|
|
41
|
-
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."
|
|
42
58
|
}
|
|
43
59
|
};
|
|
44
60
|
|
|
45
61
|
// src/i18n.ts
|
|
62
|
+
function escapeRegExp(source) {
|
|
63
|
+
return source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
64
|
+
}
|
|
46
65
|
function resolveLang(cliLang) {
|
|
47
66
|
if (cliLang === "ru" || cliLang === "en") return cliLang;
|
|
48
67
|
const envLang = process.env.LANG?.toLowerCase();
|
|
@@ -53,7 +72,8 @@ function t(lang, key, vars) {
|
|
|
53
72
|
let text = messages[lang][key] ?? messages.en[key];
|
|
54
73
|
if (vars) {
|
|
55
74
|
for (const [k, v] of Object.entries(vars)) {
|
|
56
|
-
|
|
75
|
+
const pattern = new RegExp(`\\{${escapeRegExp(k)}\\}`, "g");
|
|
76
|
+
text = text.replace(pattern, v);
|
|
57
77
|
}
|
|
58
78
|
}
|
|
59
79
|
return text;
|
|
@@ -79,6 +99,7 @@ import fs2 from "fs";
|
|
|
79
99
|
// src/cli/lib/meta.ts
|
|
80
100
|
import fs from "fs";
|
|
81
101
|
import path from "path";
|
|
102
|
+
import dotenv from "dotenv";
|
|
82
103
|
|
|
83
104
|
// src/cli/lib/fail.ts
|
|
84
105
|
function fail(lang, key, details) {
|
|
@@ -102,24 +123,44 @@ function resolveMetaPath(configFile) {
|
|
|
102
123
|
const found = candidates.find((p) => fs.existsSync(p));
|
|
103
124
|
return { candidates, found };
|
|
104
125
|
}
|
|
126
|
+
function buildMetaFromEnvExample(examplePath) {
|
|
127
|
+
const raw = fs.readFileSync(examplePath, "utf8");
|
|
128
|
+
const parsed = dotenv.parse(raw);
|
|
129
|
+
const meta = {};
|
|
130
|
+
const keys = Object.keys(parsed).sort((a, b) => a.localeCompare(b));
|
|
131
|
+
for (const key of keys) {
|
|
132
|
+
meta[key] = {
|
|
133
|
+
example: parsed[key] ?? "",
|
|
134
|
+
required: true,
|
|
135
|
+
description: ""
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return meta;
|
|
139
|
+
}
|
|
105
140
|
function loadMeta(lang, configFile) {
|
|
106
141
|
const { candidates, found } = resolveMetaPath(configFile);
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
]);
|
|
142
|
+
if (found) {
|
|
143
|
+
try {
|
|
144
|
+
const raw = fs.readFileSync(found, "utf8");
|
|
145
|
+
return { meta: JSON.parse(raw), configPath: found };
|
|
146
|
+
} catch {
|
|
147
|
+
fail(lang, "META_PARSE_FAILED", [`- ${found}`]);
|
|
148
|
+
}
|
|
115
149
|
}
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
150
|
+
const examplePath = path.resolve(process.cwd(), ".env.example");
|
|
151
|
+
if (fs.existsSync(examplePath)) {
|
|
152
|
+
console.warn(`\u26A0\uFE0F ${t(lang, "META_FALLBACK_EXAMPLE")}`);
|
|
153
|
+
console.warn(` ${t(lang, "META_FALLBACK_TIP")}`);
|
|
154
|
+
console.warn("");
|
|
155
|
+
return { meta: buildMetaFromEnvExample(examplePath), configPath: examplePath };
|
|
122
156
|
}
|
|
157
|
+
fail(lang, "META_NOT_FOUND", [
|
|
158
|
+
t(lang, "META_TRIED"),
|
|
159
|
+
...candidates.map((p) => `- ${p}`),
|
|
160
|
+
"",
|
|
161
|
+
t(lang, "META_TIP"),
|
|
162
|
+
" npx zod-envkit show -c examples/env.meta.json"
|
|
163
|
+
]);
|
|
123
164
|
}
|
|
124
165
|
|
|
125
166
|
// src/cli/commands/generate.ts
|
|
@@ -166,12 +207,15 @@ function registerGenerate(program2, getLang2) {
|
|
|
166
207
|
// src/cli/lib/dotenv.ts
|
|
167
208
|
import fs3 from "fs";
|
|
168
209
|
import path2 from "path";
|
|
169
|
-
import
|
|
210
|
+
import dotenv2 from "dotenv";
|
|
170
211
|
function resolvePath(p) {
|
|
171
212
|
return path2.resolve(process.cwd(), p);
|
|
172
213
|
}
|
|
173
214
|
function loadDotEnv(files) {
|
|
174
|
-
|
|
215
|
+
let list = files?.split(",").map((s) => s.trim()).filter(Boolean) ?? [".env"];
|
|
216
|
+
if (list.length === 0) {
|
|
217
|
+
list = [".env"];
|
|
218
|
+
}
|
|
175
219
|
const loaded = [];
|
|
176
220
|
const skipped = [];
|
|
177
221
|
const merged = {};
|
|
@@ -182,7 +226,7 @@ function loadDotEnv(files) {
|
|
|
182
226
|
continue;
|
|
183
227
|
}
|
|
184
228
|
const raw = fs3.readFileSync(abs, "utf8");
|
|
185
|
-
const parsed =
|
|
229
|
+
const parsed = dotenv2.parse(raw);
|
|
186
230
|
for (const [k, v] of Object.entries(parsed)) merged[k] = v;
|
|
187
231
|
for (const [k, v] of Object.entries(parsed)) process.env[k] = v;
|
|
188
232
|
loaded.push(f);
|
|
@@ -267,12 +311,18 @@ function isSortMode2(v) {
|
|
|
267
311
|
return v === "alpha" || v === "required-first" || v === "none";
|
|
268
312
|
}
|
|
269
313
|
function registerShow(program2, getLang2) {
|
|
270
|
-
program2.command("show").description("Show current env status (loads dotenv, masks secrets)").option("-c, --config <file>", "Path to env meta json", "env.meta.json").option("--dotenv <list>", "Comma-separated dotenv files (default: .env)", ".env").option("--mask-mode <mode>", "Mask mode (partial | full | none)"
|
|
314
|
+
program2.command("show").description("Show current env status (loads dotenv, masks secrets)").option("-c, --config <file>", "Path to env meta json", "env.meta.json").option("--dotenv <list>", "Comma-separated dotenv files (default: .env)", ".env").option("--mask-mode <mode>", "Mask mode (partial | full | none)").option("--no-mask", "Alias for --mask-mode none").option("--sort <mode>", "Sort mode (alpha | required-first | none)", "none").action((opts) => {
|
|
271
315
|
const lang = getLang2();
|
|
272
316
|
loadDotEnv(String(opts.dotenv ?? ".env"));
|
|
273
317
|
const { meta } = loadMeta(lang, String(opts.config ?? "env.meta.json"));
|
|
274
|
-
|
|
275
|
-
|
|
318
|
+
let modeRaw;
|
|
319
|
+
if (opts.mask === false) {
|
|
320
|
+
modeRaw = "none";
|
|
321
|
+
} else if (opts.maskMode) {
|
|
322
|
+
modeRaw = String(opts.maskMode);
|
|
323
|
+
} else {
|
|
324
|
+
modeRaw = "partial";
|
|
325
|
+
}
|
|
276
326
|
if (!isMaskMode(modeRaw)) {
|
|
277
327
|
console.error(`\u274C ${t(lang, "INVALID_MASK_MODE")}: ${modeRaw}`);
|
|
278
328
|
console.error(`- partial | full | none`);
|
|
@@ -304,9 +354,46 @@ function registerShow(program2, getLang2) {
|
|
|
304
354
|
});
|
|
305
355
|
}
|
|
306
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
|
+
|
|
307
387
|
// src/cli/commands/check.ts
|
|
308
388
|
function registerCheck(program2, getLang2) {
|
|
309
|
-
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)").
|
|
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) => {
|
|
310
397
|
const lang = getLang2();
|
|
311
398
|
const loaded = loadDotEnv(opts.dotenv);
|
|
312
399
|
const { meta } = loadMeta(lang, opts.config);
|
|
@@ -320,6 +407,37 @@ function registerCheck(program2, getLang2) {
|
|
|
320
407
|
fail(lang, "UNKNOWN_ENV", unknown.map((k) => `- ${k}`));
|
|
321
408
|
}
|
|
322
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
|
+
}
|
|
323
441
|
console.log(`\u2705 ${t(lang, "ENV_OK")}`);
|
|
324
442
|
process.exit(0);
|
|
325
443
|
});
|
|
@@ -327,13 +445,13 @@ function registerCheck(program2, getLang2) {
|
|
|
327
445
|
|
|
328
446
|
// src/cli/commands/init.ts
|
|
329
447
|
import fs4 from "fs";
|
|
330
|
-
import
|
|
331
|
-
import
|
|
448
|
+
import path4 from "path";
|
|
449
|
+
import dotenv3 from "dotenv";
|
|
332
450
|
function readEnvFile(file) {
|
|
333
|
-
const abs =
|
|
451
|
+
const abs = path4.resolve(process.cwd(), file);
|
|
334
452
|
if (!fs4.existsSync(abs)) return {};
|
|
335
453
|
const raw = fs4.readFileSync(abs, "utf8");
|
|
336
|
-
return
|
|
454
|
+
return dotenv3.parse(raw);
|
|
337
455
|
}
|
|
338
456
|
function metaFromEnv(env, defaultGroup) {
|
|
339
457
|
const meta = {};
|
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
|
-
|
|
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);
|
|
@@ -247,7 +256,15 @@ function mustLoadEnv(schema) {
|
|
|
247
256
|
throw res.error;
|
|
248
257
|
}
|
|
249
258
|
function formatZodError(err) {
|
|
250
|
-
return err.issues.
|
|
259
|
+
return err.issues.slice().sort((a, b) => {
|
|
260
|
+
const pa = a.path.length ? a.path.join(".") : "";
|
|
261
|
+
const pb = b.path.length ? b.path.join(".") : "";
|
|
262
|
+
return pa.localeCompare(pb);
|
|
263
|
+
}).map((issue) => {
|
|
264
|
+
const path = issue.path.length ? issue.path.join(".") : "(root)";
|
|
265
|
+
const msg = issue.message.trim() || "(validation failed)";
|
|
266
|
+
return `- ${path}: ${msg}`;
|
|
267
|
+
}).join("\n");
|
|
251
268
|
}
|
|
252
269
|
// Annotate the CommonJS export names for ESM import in node:
|
|
253
270
|
0 && (module.exports = {
|
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 (
|
|
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,9 +287,12 @@ 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
|
*
|
|
290
|
+
* Output format (stable in 1.x): `path: message`
|
|
291
|
+
*
|
|
277
292
|
* @example
|
|
278
293
|
* ```ts
|
|
279
|
-
*
|
|
294
|
+
* const result = loadEnv(EnvSchema);
|
|
295
|
+
* if (!result.ok) console.error(formatZodError(result.error));
|
|
280
296
|
* ```
|
|
281
297
|
*
|
|
282
298
|
* @public
|
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 (
|
|
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,9 +287,12 @@ 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
|
*
|
|
290
|
+
* Output format (stable in 1.x): `path: message`
|
|
291
|
+
*
|
|
277
292
|
* @example
|
|
278
293
|
* ```ts
|
|
279
|
-
*
|
|
294
|
+
* const result = loadEnv(EnvSchema);
|
|
295
|
+
* if (!result.ok) console.error(formatZodError(result.error));
|
|
280
296
|
* ```
|
|
281
297
|
*
|
|
282
298
|
* @public
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
getUnknownEnv,
|
|
7
7
|
isSecretKey,
|
|
8
8
|
sortMetaEntries
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-J4V5ODI6.js";
|
|
10
10
|
|
|
11
11
|
// src/index.ts
|
|
12
12
|
function loadEnv(schema, opts) {
|
|
@@ -21,7 +21,15 @@ function mustLoadEnv(schema) {
|
|
|
21
21
|
throw res.error;
|
|
22
22
|
}
|
|
23
23
|
function formatZodError(err) {
|
|
24
|
-
return err.issues.
|
|
24
|
+
return err.issues.slice().sort((a, b) => {
|
|
25
|
+
const pa = a.path.length ? a.path.join(".") : "";
|
|
26
|
+
const pb = b.path.length ? b.path.join(".") : "";
|
|
27
|
+
return pa.localeCompare(pb);
|
|
28
|
+
}).map((issue) => {
|
|
29
|
+
const path = issue.path.length ? issue.path.join(".") : "(root)";
|
|
30
|
+
const msg = issue.message.trim() || "(validation failed)";
|
|
31
|
+
return `- ${path}: ${msg}`;
|
|
32
|
+
}).join("\n");
|
|
25
33
|
}
|
|
26
34
|
export {
|
|
27
35
|
checkEnv,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zod-envkit",
|
|
3
|
-
"version": "1.
|
|
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",
|