zod-envkit 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -15
- package/README.md +99 -76
- package/dist/cli.cjs +104 -5
- package/dist/cli.js +104 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
All notable changes to this project will be documented in this file.
|
|
4
|
-
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
5
4
|
This project follows [Semantic Versioning](https://semver.org/).
|
|
6
5
|
|
|
7
6
|
---
|
|
8
7
|
|
|
8
|
+
## [1.0.2] – 2026-01-26
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `zod-envkit show` command to display environment status in a readable table (with secret masking)
|
|
12
|
+
- `zod-envkit check` command to validate required variables with CI-friendly exit codes
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- CLI now also searches for `env.meta.json` in `./examples/` by default
|
|
16
|
+
- CLI output and documentation updated accordingly
|
|
17
|
+
|
|
18
|
+
[1.0.2]: https://www.npmjs.com/package/zod-envkit/v/1.0.2
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## [1.0.1] – 2026-01-26
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- Packaging improvements for npm (ship only compiled output and docs)
|
|
26
|
+
- Documentation updates
|
|
27
|
+
|
|
28
|
+
[1.0.1]: https://www.npmjs.com/package/zod-envkit/v/1.0.1
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
9
32
|
## [1.0.0] – 2026-01-26
|
|
10
33
|
|
|
11
34
|
### Added
|
|
@@ -16,11 +39,9 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
16
39
|
- CLI tool to generate:
|
|
17
40
|
- `.env.example`
|
|
18
41
|
- `ENV.md` documentation
|
|
19
|
-
- Automatic type inference for validated environment variables
|
|
20
42
|
- Support for ESM and CommonJS builds
|
|
21
43
|
- TypeScript declaration files (`.d.ts`) included
|
|
22
44
|
- Basic test suite using Vitest
|
|
23
|
-
- Clear and documented API surface
|
|
24
45
|
- MIT license
|
|
25
46
|
|
|
26
47
|
### Design Decisions
|
|
@@ -29,15 +50,4 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
29
50
|
- Environment variables treated as an explicit runtime contract
|
|
30
51
|
- Small, framework-agnostic core
|
|
31
52
|
|
|
32
|
-
---
|
|
33
|
-
|
|
34
53
|
[1.0.0]: https://www.npmjs.com/package/zod-envkit/v/1.0.0
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
## [1.0.1] – 2026-01-26
|
|
38
|
-
|
|
39
|
-
### Changed
|
|
40
|
-
- Documentation and packaging improvements
|
|
41
|
-
- Minor CLI/docs adjustments
|
|
42
|
-
|
|
43
|
-
[1.0.1]: https://www.npmjs.com/package/zod-envkit/v/1.0.1
|
package/README.md
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
<br />
|
|
3
|
+
<p>
|
|
4
|
+
<img src="./zod-envkit.svg" width="546" alt="zod-envkit" />
|
|
5
|
+
</p>
|
|
6
|
+
<br />
|
|
7
|
+
<p>
|
|
8
|
+
<a href="https://www.npmjs.com/package/zod-envkit">
|
|
9
|
+
<img src="https://img.shields.io/npm/v/zod-envkit.svg?maxAge=100" alt="npm version" />
|
|
10
|
+
</a>
|
|
11
|
+
<a href="https://www.npmjs.com/package/zod-envkit">
|
|
12
|
+
<img src="https://img.shields.io/npm/dt/zod-envkit.svg?maxAge=100" alt="npm downloads" />
|
|
13
|
+
</a>
|
|
13
14
|
</p>
|
|
14
15
|
</div>
|
|
15
16
|
|
|
16
|
-
Type-safe environment variable validation and documentation using Zod
|
|
17
|
+
Type-safe environment variable validation and documentation using **Zod**.
|
|
18
|
+
|
|
19
|
+
**zod-envkit** is a small library + CLI that helps you treat environment variables as an **explicit runtime contract**, not an implicit guessing game.
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
- treat environment variables as an **explicit contract**, not guesswork
|
|
21
|
+
- validate `process.env` at startup
|
|
22
|
+
- get fully typed environment variables
|
|
23
|
+
- generate `.env.example`
|
|
24
|
+
- generate readable documentation (`ENV.md`)
|
|
25
|
+
- inspect and verify env state via CLI
|
|
24
26
|
|
|
25
27
|
No cloud. No magic. Just code.
|
|
26
28
|
|
|
@@ -28,18 +30,19 @@ No cloud. No magic. Just code.
|
|
|
28
30
|
|
|
29
31
|
## Why
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
Environment variables are critical, but usually poorly handled.
|
|
34
|
+
|
|
35
|
+
The usual problems:
|
|
32
36
|
- `process.env` is just `string | undefined`
|
|
33
|
-
-
|
|
34
|
-
- `.env.example` and
|
|
37
|
+
- missing or invalid variables fail **at runtime**
|
|
38
|
+
- `.env.example` and docs get outdated
|
|
39
|
+
- CI/CD breaks late and unpredictably
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- up-to-date `.env.example`
|
|
42
|
-
- up-to-date documentation
|
|
41
|
+
**zod-envkit** solves this by making env:
|
|
42
|
+
- validated early
|
|
43
|
+
- typed
|
|
44
|
+
- documented
|
|
45
|
+
- checkable in CI
|
|
43
46
|
|
|
44
47
|
---
|
|
45
48
|
|
|
@@ -57,7 +60,9 @@ pnpm add zod-envkit
|
|
|
57
60
|
|
|
58
61
|
---
|
|
59
62
|
|
|
60
|
-
##
|
|
63
|
+
## Library usage (runtime validation)
|
|
64
|
+
|
|
65
|
+
Create a single file responsible for loading and validating env.
|
|
61
66
|
|
|
62
67
|
```ts
|
|
63
68
|
import "dotenv/config";
|
|
@@ -67,7 +72,7 @@ import { loadEnv, formatZodError } from "zod-envkit";
|
|
|
67
72
|
const EnvSchema = z.object({
|
|
68
73
|
NODE_ENV: z.enum(["development", "test", "production"]),
|
|
69
74
|
PORT: z.coerce.number().int().min(1).max(65535),
|
|
70
|
-
DATABASE_URL: z.string().url()
|
|
75
|
+
DATABASE_URL: z.string().url(),
|
|
71
76
|
});
|
|
72
77
|
|
|
73
78
|
const result = loadEnv(EnvSchema);
|
|
@@ -85,86 +90,102 @@ Now:
|
|
|
85
90
|
* `env.PORT` is a **number**
|
|
86
91
|
* `env.DATABASE_URL` is a **string**
|
|
87
92
|
* TypeScript knows everything at compile time
|
|
93
|
+
* your app fails fast if env is invalid
|
|
88
94
|
|
|
89
95
|
---
|
|
90
96
|
|
|
91
|
-
## CLI
|
|
97
|
+
## CLI usage
|
|
98
|
+
|
|
99
|
+
The CLI works from a simple metadata file: `env.meta.json`.
|
|
92
100
|
|
|
93
|
-
|
|
101
|
+
By default, it is searched in:
|
|
102
|
+
|
|
103
|
+
* `./env.meta.json`
|
|
104
|
+
* `./examples/env.meta.json`
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
### Example `env.meta.json`
|
|
94
109
|
|
|
95
110
|
```json
|
|
96
111
|
{
|
|
97
112
|
"NODE_ENV": {
|
|
98
|
-
"description": "
|
|
99
|
-
"example": "development"
|
|
113
|
+
"description": "Runtime mode",
|
|
114
|
+
"example": "development",
|
|
115
|
+
"required": true
|
|
100
116
|
},
|
|
101
117
|
"PORT": {
|
|
102
|
-
"description": "HTTP
|
|
103
|
-
"example": "3000"
|
|
118
|
+
"description": "HTTP port",
|
|
119
|
+
"example": "3000",
|
|
120
|
+
"required": true
|
|
104
121
|
},
|
|
105
122
|
"DATABASE_URL": {
|
|
106
|
-
"description": "
|
|
107
|
-
"example": "postgresql://user:pass@localhost:5432/db"
|
|
123
|
+
"description": "Postgres connection string",
|
|
124
|
+
"example": "postgresql://user:pass@localhost:5432/db",
|
|
125
|
+
"required": true
|
|
108
126
|
}
|
|
109
127
|
}
|
|
110
128
|
```
|
|
111
129
|
|
|
112
130
|
---
|
|
113
131
|
|
|
114
|
-
|
|
132
|
+
## CLI commands
|
|
133
|
+
|
|
134
|
+
### Generate `.env.example` and `ENV.md`
|
|
135
|
+
|
|
136
|
+
(Default behavior)
|
|
115
137
|
|
|
116
138
|
```bash
|
|
117
139
|
npx zod-envkit
|
|
118
140
|
```
|
|
119
141
|
|
|
120
|
-
|
|
142
|
+
or explicitly:
|
|
121
143
|
|
|
122
144
|
```bash
|
|
123
|
-
|
|
145
|
+
npx zod-envkit generate
|
|
124
146
|
```
|
|
125
147
|
|
|
126
148
|
---
|
|
127
149
|
|
|
128
|
-
###
|
|
150
|
+
### Show current environment status
|
|
129
151
|
|
|
130
|
-
|
|
152
|
+
Loads `.env`, masks secrets, and displays a readable table.
|
|
131
153
|
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
|
|
154
|
+
```bash
|
|
155
|
+
npx zod-envkit show
|
|
156
|
+
```
|
|
135
157
|
|
|
136
|
-
|
|
137
|
-
PORT=3000
|
|
158
|
+
Example output:
|
|
138
159
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
160
|
+
* which variables are required
|
|
161
|
+
* which are present
|
|
162
|
+
* masked values for secrets
|
|
142
163
|
|
|
143
|
-
|
|
164
|
+
---
|
|
144
165
|
|
|
145
|
-
|
|
146
|
-
# Environment variables
|
|
166
|
+
### Validate required variables (CI-friendly)
|
|
147
167
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
| NODE_ENV | yes | development | Application runtime environment |
|
|
151
|
-
| PORT | yes | 3000 | HTTP server port |
|
|
152
|
-
| DATABASE_URL | yes | postgresql://... | PostgreSQL connection string |
|
|
168
|
+
```bash
|
|
169
|
+
npx zod-envkit check
|
|
153
170
|
```
|
|
154
171
|
|
|
172
|
+
* exits with code `1` if any required variable is missing
|
|
173
|
+
* perfect for CI/CD pipelines and pre-deploy checks
|
|
174
|
+
|
|
155
175
|
---
|
|
156
176
|
|
|
157
|
-
|
|
177
|
+
### CLI options
|
|
158
178
|
|
|
159
179
|
```bash
|
|
160
180
|
zod-envkit --help
|
|
161
181
|
```
|
|
162
182
|
|
|
183
|
+
Common flags:
|
|
184
|
+
|
|
163
185
|
```bash
|
|
164
|
-
zod-envkit \
|
|
165
|
-
--config env.meta.json \
|
|
166
|
-
--
|
|
167
|
-
--out-docs ENV.md
|
|
186
|
+
zod-envkit show \
|
|
187
|
+
--config examples/env.meta.json \
|
|
188
|
+
--env-file .env
|
|
168
189
|
```
|
|
169
190
|
|
|
170
191
|
---
|
|
@@ -176,6 +197,7 @@ zod-envkit \
|
|
|
176
197
|
* ❌ no validation
|
|
177
198
|
* ❌ no types
|
|
178
199
|
* ❌ no documentation
|
|
200
|
+
* ❌ no CI checks
|
|
179
201
|
|
|
180
202
|
`zod-envkit`:
|
|
181
203
|
|
|
@@ -184,39 +206,40 @@ zod-envkit \
|
|
|
184
206
|
* ✅ documentation
|
|
185
207
|
* ✅ CLI tooling
|
|
186
208
|
|
|
187
|
-
They
|
|
209
|
+
They are designed to be used **together**.
|
|
188
210
|
|
|
189
211
|
---
|
|
190
212
|
|
|
191
|
-
## Design
|
|
213
|
+
## Design principles
|
|
192
214
|
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
215
|
+
* explicit configuration over magic
|
|
216
|
+
* no framework coupling
|
|
217
|
+
* small and predictable API
|
|
218
|
+
* library and CLI are independent but complementary
|
|
219
|
+
* environment variables are a runtime contract
|
|
198
220
|
|
|
199
221
|
---
|
|
200
222
|
|
|
201
223
|
## Roadmap
|
|
202
224
|
|
|
203
225
|
* [ ] schema ↔ meta consistency checks
|
|
204
|
-
* [ ]
|
|
205
|
-
* [ ]
|
|
206
|
-
* [ ] prettier, human-friendly error output
|
|
226
|
+
* [ ] grouped sections in generated docs
|
|
227
|
+
* [ ] prettier, more human-friendly error output
|
|
207
228
|
* [ ] JSON schema export
|
|
229
|
+
* [ ] stricter validation modes for production
|
|
208
230
|
|
|
209
231
|
---
|
|
210
232
|
|
|
211
233
|
## Who is this for?
|
|
212
234
|
|
|
213
235
|
* backend and fullstack projects
|
|
214
|
-
* Node.js
|
|
236
|
+
* Node.js and Bun services
|
|
215
237
|
* CI/CD pipelines
|
|
216
|
-
* teams that
|
|
238
|
+
* teams that want env errors to fail fast, not late
|
|
217
239
|
|
|
218
240
|
---
|
|
219
241
|
|
|
220
242
|
## License
|
|
221
243
|
|
|
222
244
|
MIT
|
|
245
|
+
|
package/dist/cli.cjs
CHANGED
|
@@ -27,6 +27,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
27
|
var import_node_fs = __toESM(require("fs"), 1);
|
|
28
28
|
var import_node_path = __toESM(require("path"), 1);
|
|
29
29
|
var import_commander = require("commander");
|
|
30
|
+
var import_dotenv = __toESM(require("dotenv"), 1);
|
|
30
31
|
|
|
31
32
|
// src/generate.ts
|
|
32
33
|
function strLen(s) {
|
|
@@ -98,13 +99,111 @@ function generateEnvDocs(meta) {
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
// src/cli.ts
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
102
|
+
function maskValue(key, value) {
|
|
103
|
+
const k = key.toUpperCase();
|
|
104
|
+
const isSecret = k.includes("TOKEN") || k.includes("SECRET") || k.includes("PASSWORD") || k.includes("API_KEY") || k.includes("KEY");
|
|
105
|
+
if (!isSecret) return value;
|
|
106
|
+
if (value.length <= 4) return "*".repeat(value.length);
|
|
107
|
+
return value.slice(0, 2) + "*".repeat(Math.max(1, value.length - 4)) + value.slice(-2);
|
|
108
|
+
}
|
|
109
|
+
function padCenter2(text, width) {
|
|
110
|
+
const t = text ?? "";
|
|
111
|
+
const diff = width - t.length;
|
|
112
|
+
if (diff <= 0) return t;
|
|
113
|
+
const left = Math.floor(diff / 2);
|
|
114
|
+
const right = diff - left;
|
|
115
|
+
return " ".repeat(left) + t + " ".repeat(right);
|
|
116
|
+
}
|
|
117
|
+
function printTable(rows, headers) {
|
|
118
|
+
const widths = {};
|
|
119
|
+
for (const h of headers) widths[h] = h.length;
|
|
120
|
+
for (const row of rows) {
|
|
121
|
+
for (const h of headers) {
|
|
122
|
+
widths[h] = Math.max(widths[h], (row[h] ?? "").length);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (const h of headers) widths[h] += 2;
|
|
126
|
+
const line = "|" + headers.map((h) => " " + padCenter2(h, widths[h]) + " ").join("|") + "|";
|
|
127
|
+
const sep = "|" + headers.map((h) => ":" + "-".repeat(Math.max(3, widths[h])) + ":").join("|") + "|";
|
|
128
|
+
console.log(line);
|
|
129
|
+
console.log(sep);
|
|
130
|
+
for (const row of rows) {
|
|
131
|
+
const rowLine = "|" + headers.map((h) => " " + padCenter2(row[h] ?? "", widths[h]) + " ").join("|") + "|";
|
|
132
|
+
console.log(rowLine);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function loadMeta(configFile) {
|
|
136
|
+
const cwd = process.cwd();
|
|
137
|
+
const candidates = [
|
|
138
|
+
import_node_path.default.resolve(cwd, configFile),
|
|
139
|
+
// ./env.meta.json (по умолчанию)
|
|
140
|
+
import_node_path.default.resolve(cwd, "examples", configFile),
|
|
141
|
+
// ./examples/env.meta.json
|
|
142
|
+
import_node_path.default.resolve(cwd, "examples", "env.meta.json")
|
|
143
|
+
// явный fallback
|
|
144
|
+
];
|
|
145
|
+
const configPath = candidates.find((p) => import_node_fs.default.existsSync(p));
|
|
146
|
+
if (!configPath) {
|
|
147
|
+
console.error("\u274C env meta file not found.");
|
|
148
|
+
console.error("Tried:");
|
|
149
|
+
for (const p of candidates) console.error(`- ${p}`);
|
|
150
|
+
console.error("\nTip:");
|
|
151
|
+
console.error(" zod-envkit show -c examples/env.meta.json");
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
104
154
|
const raw = import_node_fs.default.readFileSync(configPath, "utf8");
|
|
105
|
-
|
|
155
|
+
return JSON.parse(raw);
|
|
156
|
+
}
|
|
157
|
+
var program = new import_commander.Command();
|
|
158
|
+
program.name("zod-envkit").description("Env docs + runtime checks for Node.js projects");
|
|
159
|
+
program.command("generate").description("Generate .env.example and ENV.md from env.meta.json").option("-c, --config <file>", "Path to env meta json", "env.meta.json").option("--out-example <file>", "Output file for .env.example", ".env.example").option("--out-docs <file>", "Output file for docs", "ENV.md").action((opts) => {
|
|
160
|
+
const meta = loadMeta(opts.config);
|
|
106
161
|
import_node_fs.default.writeFileSync(opts.outExample, generateEnvExample(meta), "utf8");
|
|
107
162
|
import_node_fs.default.writeFileSync(opts.outDocs, generateEnvDocs(meta), "utf8");
|
|
108
163
|
console.log(`Generated: ${opts.outExample}, ${opts.outDocs}`);
|
|
109
164
|
});
|
|
110
|
-
program.
|
|
165
|
+
program.option("-c, --config <file>", "Path to env meta json", "env.meta.json").option("--out-example <file>", "Output file for .env.example", ".env.example").option("--out-docs <file>", "Output file for docs", "ENV.md");
|
|
166
|
+
program.command("show").description("Show current env status (loads .env, masks secrets)").option("-c, --config <file>", "Path to env meta json", "env.meta.json").option("--env-file <file>", "Path to .env file", ".env").action((opts) => {
|
|
167
|
+
import_dotenv.default.config({ path: import_node_path.default.resolve(process.cwd(), opts.envFile), quiet: true });
|
|
168
|
+
const meta = loadMeta(opts.config);
|
|
169
|
+
const rows = Object.entries(meta).map(([key, m]) => {
|
|
170
|
+
const required = m.required === false ? "no" : "yes";
|
|
171
|
+
const raw = process.env[key];
|
|
172
|
+
const present = raw && raw.length > 0 ? "yes" : "no";
|
|
173
|
+
const value = raw ? maskValue(key, raw) : "";
|
|
174
|
+
return {
|
|
175
|
+
Key: key,
|
|
176
|
+
Required: required,
|
|
177
|
+
Present: present,
|
|
178
|
+
Value: value,
|
|
179
|
+
Description: m.description ?? ""
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
printTable(rows, ["Key", "Required", "Present", "Value", "Description"]);
|
|
183
|
+
});
|
|
184
|
+
program.command("check").description("Exit with code 1 if any required env var is missing (loads .env)").option("-c, --config <file>", "Path to env meta json", "env.meta.json").option("--env-file <file>", "Path to .env file", ".env").action((opts) => {
|
|
185
|
+
import_dotenv.default.config({ path: import_node_path.default.resolve(process.cwd(), opts.envFile) });
|
|
186
|
+
const meta = loadMeta(opts.config);
|
|
187
|
+
const missing = [];
|
|
188
|
+
for (const [key, m] of Object.entries(meta)) {
|
|
189
|
+
const required = m.required !== false;
|
|
190
|
+
if (!required) continue;
|
|
191
|
+
const raw = process.env[key];
|
|
192
|
+
if (!raw || raw.length === 0) missing.push(key);
|
|
193
|
+
}
|
|
194
|
+
if (missing.length) {
|
|
195
|
+
console.error("\u274C Missing required environment variables:");
|
|
196
|
+
for (const k of missing) console.error(`- ${k}`);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
console.log("\u2705 Environment looks good.");
|
|
200
|
+
});
|
|
201
|
+
program.parse(process.argv);
|
|
202
|
+
var hasSubcommand = process.argv.slice(2).some((a) => ["generate", "show", "check"].includes(a));
|
|
203
|
+
if (!hasSubcommand) {
|
|
204
|
+
const opts = program.opts();
|
|
205
|
+
const meta = loadMeta(opts.config ?? "env.meta.json");
|
|
206
|
+
import_node_fs.default.writeFileSync(opts.outExample ?? ".env.example", generateEnvExample(meta), "utf8");
|
|
207
|
+
import_node_fs.default.writeFileSync(opts.outDocs ?? "ENV.md", generateEnvDocs(meta), "utf8");
|
|
208
|
+
console.log(`Generated: ${opts.outExample ?? ".env.example"}, ${opts.outDocs ?? "ENV.md"}`);
|
|
209
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import fs from "fs";
|
|
5
5
|
import path from "path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
|
+
import dotenv from "dotenv";
|
|
7
8
|
|
|
8
9
|
// src/generate.ts
|
|
9
10
|
function strLen(s) {
|
|
@@ -75,13 +76,111 @@ function generateEnvDocs(meta) {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
// src/cli.ts
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
79
|
+
function maskValue(key, value) {
|
|
80
|
+
const k = key.toUpperCase();
|
|
81
|
+
const isSecret = k.includes("TOKEN") || k.includes("SECRET") || k.includes("PASSWORD") || k.includes("API_KEY") || k.includes("KEY");
|
|
82
|
+
if (!isSecret) return value;
|
|
83
|
+
if (value.length <= 4) return "*".repeat(value.length);
|
|
84
|
+
return value.slice(0, 2) + "*".repeat(Math.max(1, value.length - 4)) + value.slice(-2);
|
|
85
|
+
}
|
|
86
|
+
function padCenter2(text, width) {
|
|
87
|
+
const t = text ?? "";
|
|
88
|
+
const diff = width - t.length;
|
|
89
|
+
if (diff <= 0) return t;
|
|
90
|
+
const left = Math.floor(diff / 2);
|
|
91
|
+
const right = diff - left;
|
|
92
|
+
return " ".repeat(left) + t + " ".repeat(right);
|
|
93
|
+
}
|
|
94
|
+
function printTable(rows, headers) {
|
|
95
|
+
const widths = {};
|
|
96
|
+
for (const h of headers) widths[h] = h.length;
|
|
97
|
+
for (const row of rows) {
|
|
98
|
+
for (const h of headers) {
|
|
99
|
+
widths[h] = Math.max(widths[h], (row[h] ?? "").length);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (const h of headers) widths[h] += 2;
|
|
103
|
+
const line = "|" + headers.map((h) => " " + padCenter2(h, widths[h]) + " ").join("|") + "|";
|
|
104
|
+
const sep = "|" + headers.map((h) => ":" + "-".repeat(Math.max(3, widths[h])) + ":").join("|") + "|";
|
|
105
|
+
console.log(line);
|
|
106
|
+
console.log(sep);
|
|
107
|
+
for (const row of rows) {
|
|
108
|
+
const rowLine = "|" + headers.map((h) => " " + padCenter2(row[h] ?? "", widths[h]) + " ").join("|") + "|";
|
|
109
|
+
console.log(rowLine);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function loadMeta(configFile) {
|
|
113
|
+
const cwd = process.cwd();
|
|
114
|
+
const candidates = [
|
|
115
|
+
path.resolve(cwd, configFile),
|
|
116
|
+
// ./env.meta.json (по умолчанию)
|
|
117
|
+
path.resolve(cwd, "examples", configFile),
|
|
118
|
+
// ./examples/env.meta.json
|
|
119
|
+
path.resolve(cwd, "examples", "env.meta.json")
|
|
120
|
+
// явный fallback
|
|
121
|
+
];
|
|
122
|
+
const configPath = candidates.find((p) => fs.existsSync(p));
|
|
123
|
+
if (!configPath) {
|
|
124
|
+
console.error("\u274C env meta file not found.");
|
|
125
|
+
console.error("Tried:");
|
|
126
|
+
for (const p of candidates) console.error(`- ${p}`);
|
|
127
|
+
console.error("\nTip:");
|
|
128
|
+
console.error(" zod-envkit show -c examples/env.meta.json");
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
81
131
|
const raw = fs.readFileSync(configPath, "utf8");
|
|
82
|
-
|
|
132
|
+
return JSON.parse(raw);
|
|
133
|
+
}
|
|
134
|
+
var program = new Command();
|
|
135
|
+
program.name("zod-envkit").description("Env docs + runtime checks for Node.js projects");
|
|
136
|
+
program.command("generate").description("Generate .env.example and ENV.md from env.meta.json").option("-c, --config <file>", "Path to env meta json", "env.meta.json").option("--out-example <file>", "Output file for .env.example", ".env.example").option("--out-docs <file>", "Output file for docs", "ENV.md").action((opts) => {
|
|
137
|
+
const meta = loadMeta(opts.config);
|
|
83
138
|
fs.writeFileSync(opts.outExample, generateEnvExample(meta), "utf8");
|
|
84
139
|
fs.writeFileSync(opts.outDocs, generateEnvDocs(meta), "utf8");
|
|
85
140
|
console.log(`Generated: ${opts.outExample}, ${opts.outDocs}`);
|
|
86
141
|
});
|
|
87
|
-
program.
|
|
142
|
+
program.option("-c, --config <file>", "Path to env meta json", "env.meta.json").option("--out-example <file>", "Output file for .env.example", ".env.example").option("--out-docs <file>", "Output file for docs", "ENV.md");
|
|
143
|
+
program.command("show").description("Show current env status (loads .env, masks secrets)").option("-c, --config <file>", "Path to env meta json", "env.meta.json").option("--env-file <file>", "Path to .env file", ".env").action((opts) => {
|
|
144
|
+
dotenv.config({ path: path.resolve(process.cwd(), opts.envFile), quiet: true });
|
|
145
|
+
const meta = loadMeta(opts.config);
|
|
146
|
+
const rows = Object.entries(meta).map(([key, m]) => {
|
|
147
|
+
const required = m.required === false ? "no" : "yes";
|
|
148
|
+
const raw = process.env[key];
|
|
149
|
+
const present = raw && raw.length > 0 ? "yes" : "no";
|
|
150
|
+
const value = raw ? maskValue(key, raw) : "";
|
|
151
|
+
return {
|
|
152
|
+
Key: key,
|
|
153
|
+
Required: required,
|
|
154
|
+
Present: present,
|
|
155
|
+
Value: value,
|
|
156
|
+
Description: m.description ?? ""
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
printTable(rows, ["Key", "Required", "Present", "Value", "Description"]);
|
|
160
|
+
});
|
|
161
|
+
program.command("check").description("Exit with code 1 if any required env var is missing (loads .env)").option("-c, --config <file>", "Path to env meta json", "env.meta.json").option("--env-file <file>", "Path to .env file", ".env").action((opts) => {
|
|
162
|
+
dotenv.config({ path: path.resolve(process.cwd(), opts.envFile) });
|
|
163
|
+
const meta = loadMeta(opts.config);
|
|
164
|
+
const missing = [];
|
|
165
|
+
for (const [key, m] of Object.entries(meta)) {
|
|
166
|
+
const required = m.required !== false;
|
|
167
|
+
if (!required) continue;
|
|
168
|
+
const raw = process.env[key];
|
|
169
|
+
if (!raw || raw.length === 0) missing.push(key);
|
|
170
|
+
}
|
|
171
|
+
if (missing.length) {
|
|
172
|
+
console.error("\u274C Missing required environment variables:");
|
|
173
|
+
for (const k of missing) console.error(`- ${k}`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
console.log("\u2705 Environment looks good.");
|
|
177
|
+
});
|
|
178
|
+
program.parse(process.argv);
|
|
179
|
+
var hasSubcommand = process.argv.slice(2).some((a) => ["generate", "show", "check"].includes(a));
|
|
180
|
+
if (!hasSubcommand) {
|
|
181
|
+
const opts = program.opts();
|
|
182
|
+
const meta = loadMeta(opts.config ?? "env.meta.json");
|
|
183
|
+
fs.writeFileSync(opts.outExample ?? ".env.example", generateEnvExample(meta), "utf8");
|
|
184
|
+
fs.writeFileSync(opts.outDocs ?? "ENV.md", generateEnvDocs(meta), "utf8");
|
|
185
|
+
console.log(`Generated: ${opts.outExample ?? ".env.example"}, ${opts.outDocs ?? "ENV.md"}`);
|
|
186
|
+
}
|