zod-envkit 1.0.3 → 1.0.5
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 +76 -5
- package/README.RU.md +278 -0
- package/README.md +36 -8
- package/dist/cli.cjs +99 -52
- package/dist/cli.js +99 -52
- package/dist/index.cjs +9 -2
- package/dist/index.d.cts +18 -5
- package/dist/index.d.ts +18 -5
- package/dist/index.js +7 -1
- package/package.json +34 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
## [1.0.5](https://github.com/nxtxe/zod-envkit/compare/v1.0.4...v1.0.5) (2026-01-26)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* release 1.0.5 ([4d48479](https://github.com/nxtxe/zod-envkit/commit/4d48479b62819d4c17cc05eb14c64f387806cbec))
|
|
7
|
+
|
|
8
|
+
## [1.0.4](https://github.com/nxtxe/zod-envkit/compare/v1.0.3...v1.0.4) (2026-01-26)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **ci:** retry install to avoid rollup optional deps issue ([356e2c2](https://github.com/nxtxe/zod-envkit/commit/356e2c2b8980484767be29062d5f8eb4cdb4fde4))
|
|
14
|
+
* **ci:** retry install to avoid rollup optional deps issue ([7063e29](https://github.com/nxtxe/zod-envkit/commit/7063e29bb5cc3c7a8151a2afade035300ac5cff0))
|
|
15
|
+
* **cli:** stabilize release and execution behavior ([13f9b4f](https://github.com/nxtxe/zod-envkit/commit/13f9b4fe9f8893ec0d0b5b0f2373ef24cad76108))
|
|
16
|
+
* **release:** publish fix ([6811915](https://github.com/nxtxe/zod-envkit/commit/681191593e681d48b2e6490fcdd49f2b06a6bda5))
|
|
17
|
+
* **release:** publish repair ([a7f6557](https://github.com/nxtxe/zod-envkit/commit/a7f655758b3c214bb977f1e34cbb10246b39234a))
|
|
18
|
+
* **release:** publish retry ([1c55df0](https://github.com/nxtxe/zod-envkit/commit/1c55df00cf48623178d0a7cc81dfc68fc7b8dac0))
|
|
19
|
+
* **release:** retry with granular npm token ([c851824](https://github.com/nxtxe/zod-envkit/commit/c851824ba1777d31e14f7ea5306dc92adcab4de7))
|
|
20
|
+
* trigger patch release ([2c978e5](https://github.com/nxtxe/zod-envkit/commit/2c978e5b93b159117ca52eeed84e07063e9ecfea))
|
|
21
|
+
|
|
22
|
+
## [1.0.4](https://github.com/nxtxe/zod-envkit/compare/v1.0.3...v1.0.4) (2026-01-26)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
* **ci:** retry install to avoid rollup optional deps issue ([356e2c2](https://github.com/nxtxe/zod-envkit/commit/356e2c2b8980484767be29062d5f8eb4cdb4fde4))
|
|
28
|
+
* **ci:** retry install to avoid rollup optional deps issue ([7063e29](https://github.com/nxtxe/zod-envkit/commit/7063e29bb5cc3c7a8151a2afade035300ac5cff0))
|
|
29
|
+
* **cli:** stabilize release and execution behavior ([13f9b4f](https://github.com/nxtxe/zod-envkit/commit/13f9b4fe9f8893ec0d0b5b0f2373ef24cad76108))
|
|
30
|
+
* **release:** publish fix ([6811915](https://github.com/nxtxe/zod-envkit/commit/681191593e681d48b2e6490fcdd49f2b06a6bda5))
|
|
31
|
+
* **release:** publish repair ([a7f6557](https://github.com/nxtxe/zod-envkit/commit/a7f655758b3c214bb977f1e34cbb10246b39234a))
|
|
32
|
+
* **release:** publish retry ([1c55df0](https://github.com/nxtxe/zod-envkit/commit/1c55df00cf48623178d0a7cc81dfc68fc7b8dac0))
|
|
33
|
+
* **release:** retry with granular npm token ([c851824](https://github.com/nxtxe/zod-envkit/commit/c851824ba1777d31e14f7ea5306dc92adcab4de7))
|
|
34
|
+
|
|
1
35
|
# Changelog
|
|
2
36
|
|
|
3
37
|
All notable changes to this project will be documented in this file.
|
|
@@ -5,17 +39,54 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
5
39
|
|
|
6
40
|
---
|
|
7
41
|
|
|
8
|
-
## [1.0.
|
|
42
|
+
## [1.0.5] – 2026-01-26
|
|
9
43
|
|
|
10
44
|
### Added
|
|
11
|
-
- `
|
|
12
|
-
- `zod-envkit
|
|
45
|
+
- `mustLoadEnv` helper for fail-fast env loading (throws on invalid env)
|
|
46
|
+
- Default CLI behavior: running `zod-envkit` without subcommand now behaves like `zod-envkit generate`
|
|
47
|
+
- Better secret masking in `zod-envkit show` (TOKEN / SECRET / PASSWORD / *_KEY / PRIVATE)
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
- Public API (`loadEnv`, `mustLoadEnv`, `formatZodError`) stabilized and separated from CLI/generators
|
|
51
|
+
- CLI error handling improved:
|
|
52
|
+
- no stack traces for user errors
|
|
53
|
+
- consistent human-readable messages
|
|
54
|
+
- strict exit codes (`0` success, `1` error)
|
|
55
|
+
- `ENV.md` generation now produces fully centered, width-aware Markdown tables
|
|
56
|
+
- CLI now reliably resolves `env.meta.json` from:
|
|
57
|
+
- project root
|
|
58
|
+
- `./examples/`
|
|
59
|
+
- explicit `-c/--config` path
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
- TypeScript type narrowing issues in CLI (`fs.readFileSync` with undefined paths)
|
|
63
|
+
- Potential double execution when running CLI without subcommands
|
|
64
|
+
- Inconsistent env loading behavior across CLI commands
|
|
65
|
+
|
|
66
|
+
[1.0.5]: https://www.npmjs.com/package/zod-envkit/v/1.0.5
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## [1.0.4] – 2026-01-26
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
- `zod-envkit show` command to display env status in a readable table (with secret masking)
|
|
74
|
+
- `zod-envkit check` command to validate required variables (CI-friendly exit codes)
|
|
13
75
|
|
|
14
76
|
### Changed
|
|
15
77
|
- CLI now also searches for `env.meta.json` in `./examples/` by default
|
|
16
|
-
- CLI output and documentation updated accordingly
|
|
17
78
|
|
|
18
|
-
[1.0.
|
|
79
|
+
[1.0.4]: https://www.npmjs.com/package/zod-envkit/v/1.0.4
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## [1.0.3] – 2026-01-26
|
|
84
|
+
- Git tag only (not published to npm)
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## [1.0.2] – 2026-01-26
|
|
89
|
+
- Git tag only (not published to npm)
|
|
19
90
|
|
|
20
91
|
---
|
|
21
92
|
|
package/README.RU.md
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
<div align="center">
|
|
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://github.com/nxtxe/zod-envkit">
|
|
9
|
+
<img src="https://github.com/nxtxe/zod-envkit/actions/workflows/release.yml/badge.svg" />
|
|
10
|
+
</a>
|
|
11
|
+
<a href="https://www.npmjs.com/package/zod-envkit">
|
|
12
|
+
<img src="https://img.shields.io/npm/v/zod-envkit.svg?maxAge=100" alt="npm version" />
|
|
13
|
+
</a>
|
|
14
|
+
<a href="https://www.npmjs.com/package/zod-envkit">
|
|
15
|
+
<img src="https://img.shields.io/npm/dt/zod-envkit.svg?maxAge=100" alt="npm downloads" />
|
|
16
|
+
</a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p>
|
|
20
|
+
<a href="./README.md">English</a> |
|
|
21
|
+
<a href="./README.RU.md">Русский</a>
|
|
22
|
+
</p>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
Ниже — русский перевод текста, сохраняя структуру и смысл оригинала.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
Типобезопасная валидация и документация переменных окружения с помощью **Zod**.
|
|
30
|
+
|
|
31
|
+
**zod-envkit** — это небольшая библиотека + CLI, которая помогает воспринимать переменные окружения как
|
|
32
|
+
**явный контракт времени выполнения**, а не как игру в угадайку.
|
|
33
|
+
|
|
34
|
+
* валидация `process.env` при старте приложения
|
|
35
|
+
* полностью типизированные переменные окружения
|
|
36
|
+
* генерация `.env.example`
|
|
37
|
+
* генерация читаемой документации (`ENV.md`)
|
|
38
|
+
* просмотр и проверка состояния env через CLI
|
|
39
|
+
* быстрый фейл в CI/CD до деплоя
|
|
40
|
+
|
|
41
|
+
Никакого облака. Никакой магии. Только код.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Зачем
|
|
46
|
+
|
|
47
|
+
Переменные окружения критичны, но почти всегда обрабатываются плохо.
|
|
48
|
+
|
|
49
|
+
Типичные проблемы:
|
|
50
|
+
|
|
51
|
+
* `process.env` — это просто `string | undefined`
|
|
52
|
+
* отсутствующие или некорректные переменные падают **во время выполнения**
|
|
53
|
+
* `.env.example` и документация быстро устаревают
|
|
54
|
+
* CI/CD ломается поздно и непредсказуемо
|
|
55
|
+
|
|
56
|
+
**zod-envkit** решает это, делая env:
|
|
57
|
+
|
|
58
|
+
* валидируемым на раннем этапе
|
|
59
|
+
* типизированным
|
|
60
|
+
* документированным
|
|
61
|
+
* проверяемым в CI
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Установка
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm install zod-envkit
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
yarn add zod-envkit
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pnpm add zod-envkit
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Использование библиотеки (runtime-валидация)
|
|
82
|
+
|
|
83
|
+
Создайте один файл, отвечающий за загрузку и валидацию env.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import "dotenv/config";
|
|
87
|
+
import { z } from "zod";
|
|
88
|
+
import { loadEnv, mustLoadEnv, formatZodError } from "zod-envkit";
|
|
89
|
+
|
|
90
|
+
const EnvSchema = z.object({
|
|
91
|
+
NODE_ENV: z.enum(["development", "test", "production"]),
|
|
92
|
+
PORT: z.coerce.number().int().min(1).max(65535),
|
|
93
|
+
DATABASE_URL: z.string().url(),
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Вариант 1 — безопасный режим (без исключений)
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const result = loadEnv(EnvSchema);
|
|
101
|
+
|
|
102
|
+
if (!result.ok) {
|
|
103
|
+
console.error("Некорректное окружение:\n" + formatZodError(result.error));
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const env = result.env;
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Вариант 2 — fail-fast (рекомендуется)
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
export const env = mustLoadEnv(EnvSchema);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Теперь:
|
|
117
|
+
|
|
118
|
+
* `env.PORT` — это **number**
|
|
119
|
+
* `env.DATABASE_URL` — это **string**
|
|
120
|
+
* TypeScript всё знает на этапе компиляции
|
|
121
|
+
* приложение падает сразу, если env некорректен
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Использование CLI
|
|
126
|
+
|
|
127
|
+
CLI работает на основе простого мета-файла: `env.meta.json`.
|
|
128
|
+
|
|
129
|
+
По умолчанию он ищется в:
|
|
130
|
+
|
|
131
|
+
* `./env.meta.json`
|
|
132
|
+
* `./examples/env.meta.json`
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
### Пример `env.meta.json`
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"NODE_ENV": {
|
|
141
|
+
"description": "Режим выполнения",
|
|
142
|
+
"example": "development",
|
|
143
|
+
"required": true
|
|
144
|
+
},
|
|
145
|
+
"PORT": {
|
|
146
|
+
"description": "HTTP-порт",
|
|
147
|
+
"example": "3000",
|
|
148
|
+
"required": true
|
|
149
|
+
},
|
|
150
|
+
"DATABASE_URL": {
|
|
151
|
+
"description": "Строка подключения к Postgres",
|
|
152
|
+
"example": "postgresql://user:pass@localhost:5432/db",
|
|
153
|
+
"required": true
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Команды CLI
|
|
161
|
+
|
|
162
|
+
### Генерация `.env.example` и `ENV.md`
|
|
163
|
+
|
|
164
|
+
(Поведение по умолчанию)
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npx zod-envkit
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
или явно:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
npx zod-envkit generate
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
### Просмотр текущего состояния окружения
|
|
179
|
+
|
|
180
|
+
Загружает `.env`, маскирует секреты и показывает читаемую таблицу.
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
npx zod-envkit show
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Показывает:
|
|
187
|
+
|
|
188
|
+
* какие переменные обязательны
|
|
189
|
+
* какие присутствуют
|
|
190
|
+
* замаскированные значения секретов (`TOKEN`, `SECRET`, `*_KEY` и т.д.)
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
### Проверка обязательных переменных (удобно для CI)
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
npx zod-envkit check
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
* завершает процесс с кодом `1`, если отсутствует хотя бы одна обязательная переменная
|
|
201
|
+
* идеально для CI/CD пайплайнов и pre-deploy проверок
|
|
202
|
+
|
|
203
|
+
Пример шага в CI:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
npx zod-envkit check
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
### Опции CLI
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
zod-envkit --help
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Часто используемые флаги:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
zod-envkit show \
|
|
221
|
+
--config examples/env.meta.json \
|
|
222
|
+
--env-file .env
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Почему не просто dotenv?
|
|
228
|
+
|
|
229
|
+
`dotenv`:
|
|
230
|
+
|
|
231
|
+
* ❌ нет валидации
|
|
232
|
+
* ❌ нет типов
|
|
233
|
+
* ❌ нет документации
|
|
234
|
+
* ❌ нет проверок для CI
|
|
235
|
+
|
|
236
|
+
`zod-envkit`:
|
|
237
|
+
|
|
238
|
+
* ✅ валидация
|
|
239
|
+
* ✅ вывод типов для TypeScript
|
|
240
|
+
* ✅ документация
|
|
241
|
+
* ✅ CLI-инструменты
|
|
242
|
+
|
|
243
|
+
Они созданы для использования **вместе**.
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Принципы дизайна
|
|
248
|
+
|
|
249
|
+
* явная конфигурация вместо магии
|
|
250
|
+
* отсутствие привязки к фреймворкам
|
|
251
|
+
* маленький и предсказуемый API
|
|
252
|
+
* библиотека и CLI независимы, но дополняют друг друга
|
|
253
|
+
* переменные окружения — это runtime-контракт
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Roadmap
|
|
258
|
+
|
|
259
|
+
* [ ] проверка согласованности schema ↔ meta
|
|
260
|
+
* [ ] группировка секций в сгенерированной документации
|
|
261
|
+
* [ ] более аккуратный и человеко-понятный вывод ошибок
|
|
262
|
+
* [ ] экспорт в JSON Schema
|
|
263
|
+
* [ ] более строгие режимы валидации для production
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Для кого это?
|
|
268
|
+
|
|
269
|
+
* backend- и fullstack-проекты
|
|
270
|
+
* сервисы на Node.js и Bun
|
|
271
|
+
* CI/CD пайплайны
|
|
272
|
+
* команды, которые хотят, чтобы ошибки env находились рано, а не поздно
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Лицензия
|
|
277
|
+
|
|
278
|
+
MIT
|
package/README.md
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
</p>
|
|
6
6
|
<br />
|
|
7
7
|
<p>
|
|
8
|
+
<a href="https://github.com/nxtxe/zod-envkit">
|
|
9
|
+
<img src="https://github.com/nxtxe/zod-envkit/actions/workflows/release.yml/badge.svg" />
|
|
10
|
+
</a>
|
|
8
11
|
<a href="https://www.npmjs.com/package/zod-envkit">
|
|
9
12
|
<img src="https://img.shields.io/npm/v/zod-envkit.svg?maxAge=100" alt="npm version" />
|
|
10
13
|
</a>
|
|
@@ -12,17 +15,25 @@
|
|
|
12
15
|
<img src="https://img.shields.io/npm/dt/zod-envkit.svg?maxAge=100" alt="npm downloads" />
|
|
13
16
|
</a>
|
|
14
17
|
</p>
|
|
18
|
+
|
|
19
|
+
<p>
|
|
20
|
+
<a href="./README.md">English</a> |
|
|
21
|
+
<a href="./README.RU.md">Русский</a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
15
24
|
</div>
|
|
16
25
|
|
|
17
26
|
Type-safe environment variable validation and documentation using **Zod**.
|
|
18
27
|
|
|
19
|
-
**zod-envkit** is a small library + CLI that helps you treat environment variables as an
|
|
28
|
+
**zod-envkit** is a small library + CLI that helps you treat environment variables as an
|
|
29
|
+
**explicit runtime contract**, not an implicit guessing game.
|
|
20
30
|
|
|
21
31
|
- validate `process.env` at startup
|
|
22
32
|
- get fully typed environment variables
|
|
23
33
|
- generate `.env.example`
|
|
24
34
|
- generate readable documentation (`ENV.md`)
|
|
25
35
|
- inspect and verify env state via CLI
|
|
36
|
+
- fail fast in CI/CD before deploy
|
|
26
37
|
|
|
27
38
|
No cloud. No magic. Just code.
|
|
28
39
|
|
|
@@ -32,7 +43,7 @@ No cloud. No magic. Just code.
|
|
|
32
43
|
|
|
33
44
|
Environment variables are critical, but usually poorly handled.
|
|
34
45
|
|
|
35
|
-
|
|
46
|
+
Typical problems:
|
|
36
47
|
- `process.env` is just `string | undefined`
|
|
37
48
|
- missing or invalid variables fail **at runtime**
|
|
38
49
|
- `.env.example` and docs get outdated
|
|
@@ -52,7 +63,9 @@ The usual problems:
|
|
|
52
63
|
npm install zod-envkit
|
|
53
64
|
````
|
|
54
65
|
|
|
55
|
-
|
|
66
|
+
```bash
|
|
67
|
+
yarn add zod-envkit
|
|
68
|
+
```
|
|
56
69
|
|
|
57
70
|
```bash
|
|
58
71
|
pnpm add zod-envkit
|
|
@@ -67,14 +80,18 @@ Create a single file responsible for loading and validating env.
|
|
|
67
80
|
```ts
|
|
68
81
|
import "dotenv/config";
|
|
69
82
|
import { z } from "zod";
|
|
70
|
-
import { loadEnv, formatZodError } from "zod-envkit";
|
|
83
|
+
import { loadEnv, mustLoadEnv, formatZodError } from "zod-envkit";
|
|
71
84
|
|
|
72
85
|
const EnvSchema = z.object({
|
|
73
86
|
NODE_ENV: z.enum(["development", "test", "production"]),
|
|
74
87
|
PORT: z.coerce.number().int().min(1).max(65535),
|
|
75
88
|
DATABASE_URL: z.string().url(),
|
|
76
89
|
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Option 1 — safe mode (no throw)
|
|
77
93
|
|
|
94
|
+
```ts
|
|
78
95
|
const result = loadEnv(EnvSchema);
|
|
79
96
|
|
|
80
97
|
if (!result.ok) {
|
|
@@ -85,6 +102,12 @@ if (!result.ok) {
|
|
|
85
102
|
export const env = result.env;
|
|
86
103
|
```
|
|
87
104
|
|
|
105
|
+
### Option 2 — fail-fast (recommended)
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
export const env = mustLoadEnv(EnvSchema);
|
|
109
|
+
```
|
|
110
|
+
|
|
88
111
|
Now:
|
|
89
112
|
|
|
90
113
|
* `env.PORT` is a **number**
|
|
@@ -155,11 +178,11 @@ Loads `.env`, masks secrets, and displays a readable table.
|
|
|
155
178
|
npx zod-envkit show
|
|
156
179
|
```
|
|
157
180
|
|
|
158
|
-
|
|
181
|
+
Shows:
|
|
159
182
|
|
|
160
183
|
* which variables are required
|
|
161
184
|
* which are present
|
|
162
|
-
* masked values for secrets
|
|
185
|
+
* masked values for secrets (`TOKEN`, `SECRET`, `*_KEY`, etc.)
|
|
163
186
|
|
|
164
187
|
---
|
|
165
188
|
|
|
@@ -170,7 +193,13 @@ npx zod-envkit check
|
|
|
170
193
|
```
|
|
171
194
|
|
|
172
195
|
* exits with code `1` if any required variable is missing
|
|
173
|
-
*
|
|
196
|
+
* ideal for CI/CD pipelines and pre-deploy checks
|
|
197
|
+
|
|
198
|
+
Example CI step:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
npx zod-envkit check
|
|
202
|
+
```
|
|
174
203
|
|
|
175
204
|
---
|
|
176
205
|
|
|
@@ -242,4 +271,3 @@ They are designed to be used **together**.
|
|
|
242
271
|
## License
|
|
243
272
|
|
|
244
273
|
MIT
|
|
245
|
-
|
package/dist/cli.cjs
CHANGED
|
@@ -29,17 +29,56 @@ var import_node_path = __toESM(require("path"), 1);
|
|
|
29
29
|
var import_commander = require("commander");
|
|
30
30
|
var import_dotenv = __toESM(require("dotenv"), 1);
|
|
31
31
|
|
|
32
|
+
// src/messages.ts
|
|
33
|
+
var messages = {
|
|
34
|
+
en: {
|
|
35
|
+
META_NOT_FOUND: "env meta file not found.",
|
|
36
|
+
META_TRIED: "Tried:",
|
|
37
|
+
META_TIP: "Tip:",
|
|
38
|
+
GENERATED: "Generated: {example}, {docs}",
|
|
39
|
+
ENV_OK: "Environment looks good.",
|
|
40
|
+
MISSING_ENV: "Missing required environment variables:",
|
|
41
|
+
META_PARSE_FAILED: "Failed to read/parse env meta file:"
|
|
42
|
+
},
|
|
43
|
+
ru: {
|
|
44
|
+
META_NOT_FOUND: "\u0424\u0430\u0439\u043B env.meta.json \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D.",
|
|
45
|
+
META_TRIED: "\u041F\u0440\u043E\u0431\u043E\u0432\u0430\u043B\u0438:",
|
|
46
|
+
META_TIP: "\u041F\u043E\u0434\u0441\u043A\u0430\u0437\u043A\u0430:",
|
|
47
|
+
GENERATED: "\u0421\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043E: {example}, {docs}",
|
|
48
|
+
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.",
|
|
49
|
+
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:",
|
|
50
|
+
META_PARSE_FAILED: "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C/\u0440\u0430\u0441\u043F\u0430\u0440\u0441\u0438\u0442\u044C env meta \u0444\u0430\u0439\u043B:"
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// src/i18n.ts
|
|
55
|
+
function resolveLang(cliLang) {
|
|
56
|
+
if (cliLang === "ru" || cliLang === "en") return cliLang;
|
|
57
|
+
const envLang = process.env.LANG?.toLowerCase();
|
|
58
|
+
if (envLang?.startsWith("ru")) return "ru";
|
|
59
|
+
return "en";
|
|
60
|
+
}
|
|
61
|
+
function t(lang, key, vars) {
|
|
62
|
+
let text = messages[lang][key] ?? messages.en[key];
|
|
63
|
+
if (vars) {
|
|
64
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
65
|
+
text = text.replace(`{${k}}`, v);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return text;
|
|
69
|
+
}
|
|
70
|
+
|
|
32
71
|
// src/generate.ts
|
|
33
72
|
function strLen(s) {
|
|
34
|
-
return s.length;
|
|
73
|
+
return (s ?? "").length;
|
|
35
74
|
}
|
|
36
75
|
function padCenter(text, width) {
|
|
37
|
-
const
|
|
38
|
-
const diff = width - strLen(
|
|
39
|
-
if (diff <= 0) return
|
|
76
|
+
const t2 = text ?? "";
|
|
77
|
+
const diff = width - strLen(t2);
|
|
78
|
+
if (diff <= 0) return t2;
|
|
40
79
|
const left = Math.floor(diff / 2);
|
|
41
80
|
const right = diff - left;
|
|
42
|
-
return " ".repeat(left) +
|
|
81
|
+
return " ".repeat(left) + t2 + " ".repeat(right);
|
|
43
82
|
}
|
|
44
83
|
function makeDivider(width, align) {
|
|
45
84
|
if (width < 3) width = 3;
|
|
@@ -54,7 +93,7 @@ function generateEnvExample(meta) {
|
|
|
54
93
|
lines.push(`${key}=${m.example ?? ""}`);
|
|
55
94
|
lines.push("");
|
|
56
95
|
}
|
|
57
|
-
return lines.join("\n").
|
|
96
|
+
return lines.join("\n").trimEnd() + "\n";
|
|
58
97
|
}
|
|
59
98
|
function generateEnvDocs(meta) {
|
|
60
99
|
const headers = ["Key", "Required", "Example", "Description"];
|
|
@@ -99,73 +138,83 @@ function generateEnvDocs(meta) {
|
|
|
99
138
|
}
|
|
100
139
|
|
|
101
140
|
// src/cli.ts
|
|
141
|
+
function fail(lang, key, details) {
|
|
142
|
+
console.error(`\u274C ${t(lang, key)}`);
|
|
143
|
+
if (details?.length) for (const d of details) console.error(d);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
102
146
|
function maskValue(key, value) {
|
|
103
147
|
const k = key.toUpperCase();
|
|
104
|
-
const isSecret = k.includes("TOKEN") || k.includes("SECRET") || k.includes("PASSWORD") || k.includes("API_KEY") || k.includes("
|
|
148
|
+
const isSecret = k.includes("TOKEN") || k.includes("SECRET") || k.includes("PASSWORD") || k.includes("API_KEY") || k.endsWith("_KEY") || k.includes("PRIVATE");
|
|
105
149
|
if (!isSecret) return value;
|
|
106
150
|
if (value.length <= 4) return "*".repeat(value.length);
|
|
107
151
|
return value.slice(0, 2) + "*".repeat(Math.max(1, value.length - 4)) + value.slice(-2);
|
|
108
152
|
}
|
|
109
153
|
function padCenter2(text, width) {
|
|
110
|
-
const
|
|
111
|
-
const diff = width -
|
|
112
|
-
if (diff <= 0) return
|
|
154
|
+
const v = text ?? "";
|
|
155
|
+
const diff = width - v.length;
|
|
156
|
+
if (diff <= 0) return v;
|
|
113
157
|
const left = Math.floor(diff / 2);
|
|
114
158
|
const right = diff - left;
|
|
115
|
-
return " ".repeat(left) +
|
|
159
|
+
return " ".repeat(left) + v + " ".repeat(right);
|
|
116
160
|
}
|
|
117
161
|
function printTable(rows, headers) {
|
|
118
162
|
const widths = {};
|
|
119
163
|
for (const h of headers) widths[h] = h.length;
|
|
120
164
|
for (const row of rows) {
|
|
121
|
-
for (const h of headers)
|
|
122
|
-
widths[h] = Math.max(widths[h], (row[h] ?? "").length);
|
|
123
|
-
}
|
|
165
|
+
for (const h of headers) widths[h] = Math.max(widths[h], (row[h] ?? "").length);
|
|
124
166
|
}
|
|
125
167
|
for (const h of headers) widths[h] += 2;
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
console.log(
|
|
129
|
-
console.log(
|
|
168
|
+
const headerLine = "|" + headers.map((h) => " " + padCenter2(h, widths[h]) + " ").join("|") + "|";
|
|
169
|
+
const sepLine = "|" + headers.map((h) => ":" + "-".repeat(Math.max(3, widths[h])) + ":").join("|") + "|";
|
|
170
|
+
console.log(headerLine);
|
|
171
|
+
console.log(sepLine);
|
|
130
172
|
for (const row of rows) {
|
|
131
|
-
|
|
132
|
-
console.log(rowLine);
|
|
173
|
+
console.log("|" + headers.map((h) => " " + padCenter2(row[h] ?? "", widths[h]) + " ").join("|") + "|");
|
|
133
174
|
}
|
|
134
175
|
}
|
|
135
|
-
function
|
|
176
|
+
function loadDotEnv(envFile) {
|
|
177
|
+
import_dotenv.default.config({ path: import_node_path.default.resolve(process.cwd(), envFile), quiet: true });
|
|
178
|
+
}
|
|
179
|
+
function loadMeta(lang, configFile) {
|
|
136
180
|
const cwd = process.cwd();
|
|
137
181
|
const candidates = [
|
|
138
182
|
import_node_path.default.resolve(cwd, configFile),
|
|
139
|
-
// ./env.meta.json (по умолчанию)
|
|
140
183
|
import_node_path.default.resolve(cwd, "examples", configFile),
|
|
141
|
-
// ./examples/env.meta.json
|
|
142
184
|
import_node_path.default.resolve(cwd, "examples", "env.meta.json")
|
|
143
|
-
// явный fallback
|
|
144
185
|
];
|
|
145
|
-
const
|
|
146
|
-
if (!
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
186
|
+
const found = candidates.find((p) => import_node_fs.default.existsSync(p));
|
|
187
|
+
if (!found) {
|
|
188
|
+
fail(lang, "META_NOT_FOUND", [
|
|
189
|
+
t(lang, "META_TRIED"),
|
|
190
|
+
...candidates.map((p) => `- ${p}`),
|
|
191
|
+
"",
|
|
192
|
+
t(lang, "META_TIP"),
|
|
193
|
+
" npx zod-envkit show -c examples/env.meta.json"
|
|
194
|
+
]);
|
|
195
|
+
}
|
|
196
|
+
const configPath = found;
|
|
197
|
+
try {
|
|
198
|
+
const raw = import_node_fs.default.readFileSync(configPath, "utf8");
|
|
199
|
+
return { meta: JSON.parse(raw), configPath };
|
|
200
|
+
} catch {
|
|
201
|
+
fail(lang, "META_PARSE_FAILED", [`- ${configPath}`]);
|
|
153
202
|
}
|
|
154
|
-
const raw = import_node_fs.default.readFileSync(configPath, "utf8");
|
|
155
|
-
return JSON.parse(raw);
|
|
156
203
|
}
|
|
157
204
|
var program = new import_commander.Command();
|
|
158
|
-
program.name("zod-envkit").description("Env docs + runtime checks for Node.js projects");
|
|
205
|
+
program.name("zod-envkit").description("Env docs + runtime checks for Node.js projects").showHelpAfterError().showSuggestionAfterError().option("--lang <lang>", "CLI language (en | ru)");
|
|
159
206
|
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
|
|
207
|
+
const lang = resolveLang(program.opts().lang);
|
|
208
|
+
const { meta } = loadMeta(lang, opts.config);
|
|
161
209
|
import_node_fs.default.writeFileSync(opts.outExample, generateEnvExample(meta), "utf8");
|
|
162
210
|
import_node_fs.default.writeFileSync(opts.outDocs, generateEnvDocs(meta), "utf8");
|
|
163
|
-
console.log(
|
|
211
|
+
console.log(t(lang, "GENERATED", { example: opts.outExample, docs: opts.outDocs }));
|
|
212
|
+
process.exit(0);
|
|
164
213
|
});
|
|
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
214
|
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
|
-
|
|
168
|
-
|
|
215
|
+
const lang = resolveLang(program.opts().lang);
|
|
216
|
+
loadDotEnv(opts.envFile);
|
|
217
|
+
const { meta } = loadMeta(lang, opts.config);
|
|
169
218
|
const rows = Object.entries(meta).map(([key, m]) => {
|
|
170
219
|
const required = m.required === false ? "no" : "yes";
|
|
171
220
|
const raw = process.env[key];
|
|
@@ -180,10 +229,12 @@ program.command("show").description("Show current env status (loads .env, masks
|
|
|
180
229
|
};
|
|
181
230
|
});
|
|
182
231
|
printTable(rows, ["Key", "Required", "Present", "Value", "Description"]);
|
|
232
|
+
process.exit(0);
|
|
183
233
|
});
|
|
184
234
|
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
|
-
|
|
186
|
-
|
|
235
|
+
const lang = resolveLang(program.opts().lang);
|
|
236
|
+
loadDotEnv(opts.envFile);
|
|
237
|
+
const { meta } = loadMeta(lang, opts.config);
|
|
187
238
|
const missing = [];
|
|
188
239
|
for (const [key, m] of Object.entries(meta)) {
|
|
189
240
|
const required = m.required !== false;
|
|
@@ -192,18 +243,14 @@ program.command("check").description("Exit with code 1 if any required env var i
|
|
|
192
243
|
if (!raw || raw.length === 0) missing.push(key);
|
|
193
244
|
}
|
|
194
245
|
if (missing.length) {
|
|
195
|
-
console.error(
|
|
246
|
+
console.error(`\u274C ${t(lang, "MISSING_ENV")}`);
|
|
196
247
|
for (const k of missing) console.error(`- ${k}`);
|
|
197
248
|
process.exit(1);
|
|
198
249
|
}
|
|
199
|
-
console.log(
|
|
250
|
+
console.log(`\u2705 ${t(lang, "ENV_OK")}`);
|
|
251
|
+
process.exit(0);
|
|
200
252
|
});
|
|
253
|
+
var known = /* @__PURE__ */ new Set(["generate", "show", "check", "-h", "--help", "-V", "--version", "--lang"]);
|
|
254
|
+
var hasCommand = process.argv.slice(2).some((a) => known.has(a));
|
|
255
|
+
if (!hasCommand) process.argv.splice(2, 0, "generate");
|
|
201
256
|
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
|
@@ -6,17 +6,56 @@ import path from "path";
|
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import dotenv from "dotenv";
|
|
8
8
|
|
|
9
|
+
// src/messages.ts
|
|
10
|
+
var messages = {
|
|
11
|
+
en: {
|
|
12
|
+
META_NOT_FOUND: "env meta file not found.",
|
|
13
|
+
META_TRIED: "Tried:",
|
|
14
|
+
META_TIP: "Tip:",
|
|
15
|
+
GENERATED: "Generated: {example}, {docs}",
|
|
16
|
+
ENV_OK: "Environment looks good.",
|
|
17
|
+
MISSING_ENV: "Missing required environment variables:",
|
|
18
|
+
META_PARSE_FAILED: "Failed to read/parse env meta file:"
|
|
19
|
+
},
|
|
20
|
+
ru: {
|
|
21
|
+
META_NOT_FOUND: "\u0424\u0430\u0439\u043B env.meta.json \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D.",
|
|
22
|
+
META_TRIED: "\u041F\u0440\u043E\u0431\u043E\u0432\u0430\u043B\u0438:",
|
|
23
|
+
META_TIP: "\u041F\u043E\u0434\u0441\u043A\u0430\u0437\u043A\u0430:",
|
|
24
|
+
GENERATED: "\u0421\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043E: {example}, {docs}",
|
|
25
|
+
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.",
|
|
26
|
+
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:",
|
|
27
|
+
META_PARSE_FAILED: "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C/\u0440\u0430\u0441\u043F\u0430\u0440\u0441\u0438\u0442\u044C env meta \u0444\u0430\u0439\u043B:"
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/i18n.ts
|
|
32
|
+
function resolveLang(cliLang) {
|
|
33
|
+
if (cliLang === "ru" || cliLang === "en") return cliLang;
|
|
34
|
+
const envLang = process.env.LANG?.toLowerCase();
|
|
35
|
+
if (envLang?.startsWith("ru")) return "ru";
|
|
36
|
+
return "en";
|
|
37
|
+
}
|
|
38
|
+
function t(lang, key, vars) {
|
|
39
|
+
let text = messages[lang][key] ?? messages.en[key];
|
|
40
|
+
if (vars) {
|
|
41
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
42
|
+
text = text.replace(`{${k}}`, v);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
47
|
+
|
|
9
48
|
// src/generate.ts
|
|
10
49
|
function strLen(s) {
|
|
11
|
-
return s.length;
|
|
50
|
+
return (s ?? "").length;
|
|
12
51
|
}
|
|
13
52
|
function padCenter(text, width) {
|
|
14
|
-
const
|
|
15
|
-
const diff = width - strLen(
|
|
16
|
-
if (diff <= 0) return
|
|
53
|
+
const t2 = text ?? "";
|
|
54
|
+
const diff = width - strLen(t2);
|
|
55
|
+
if (diff <= 0) return t2;
|
|
17
56
|
const left = Math.floor(diff / 2);
|
|
18
57
|
const right = diff - left;
|
|
19
|
-
return " ".repeat(left) +
|
|
58
|
+
return " ".repeat(left) + t2 + " ".repeat(right);
|
|
20
59
|
}
|
|
21
60
|
function makeDivider(width, align) {
|
|
22
61
|
if (width < 3) width = 3;
|
|
@@ -31,7 +70,7 @@ function generateEnvExample(meta) {
|
|
|
31
70
|
lines.push(`${key}=${m.example ?? ""}`);
|
|
32
71
|
lines.push("");
|
|
33
72
|
}
|
|
34
|
-
return lines.join("\n").
|
|
73
|
+
return lines.join("\n").trimEnd() + "\n";
|
|
35
74
|
}
|
|
36
75
|
function generateEnvDocs(meta) {
|
|
37
76
|
const headers = ["Key", "Required", "Example", "Description"];
|
|
@@ -76,73 +115,83 @@ function generateEnvDocs(meta) {
|
|
|
76
115
|
}
|
|
77
116
|
|
|
78
117
|
// src/cli.ts
|
|
118
|
+
function fail(lang, key, details) {
|
|
119
|
+
console.error(`\u274C ${t(lang, key)}`);
|
|
120
|
+
if (details?.length) for (const d of details) console.error(d);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
79
123
|
function maskValue(key, value) {
|
|
80
124
|
const k = key.toUpperCase();
|
|
81
|
-
const isSecret = k.includes("TOKEN") || k.includes("SECRET") || k.includes("PASSWORD") || k.includes("API_KEY") || k.includes("
|
|
125
|
+
const isSecret = k.includes("TOKEN") || k.includes("SECRET") || k.includes("PASSWORD") || k.includes("API_KEY") || k.endsWith("_KEY") || k.includes("PRIVATE");
|
|
82
126
|
if (!isSecret) return value;
|
|
83
127
|
if (value.length <= 4) return "*".repeat(value.length);
|
|
84
128
|
return value.slice(0, 2) + "*".repeat(Math.max(1, value.length - 4)) + value.slice(-2);
|
|
85
129
|
}
|
|
86
130
|
function padCenter2(text, width) {
|
|
87
|
-
const
|
|
88
|
-
const diff = width -
|
|
89
|
-
if (diff <= 0) return
|
|
131
|
+
const v = text ?? "";
|
|
132
|
+
const diff = width - v.length;
|
|
133
|
+
if (diff <= 0) return v;
|
|
90
134
|
const left = Math.floor(diff / 2);
|
|
91
135
|
const right = diff - left;
|
|
92
|
-
return " ".repeat(left) +
|
|
136
|
+
return " ".repeat(left) + v + " ".repeat(right);
|
|
93
137
|
}
|
|
94
138
|
function printTable(rows, headers) {
|
|
95
139
|
const widths = {};
|
|
96
140
|
for (const h of headers) widths[h] = h.length;
|
|
97
141
|
for (const row of rows) {
|
|
98
|
-
for (const h of headers)
|
|
99
|
-
widths[h] = Math.max(widths[h], (row[h] ?? "").length);
|
|
100
|
-
}
|
|
142
|
+
for (const h of headers) widths[h] = Math.max(widths[h], (row[h] ?? "").length);
|
|
101
143
|
}
|
|
102
144
|
for (const h of headers) widths[h] += 2;
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
console.log(
|
|
106
|
-
console.log(
|
|
145
|
+
const headerLine = "|" + headers.map((h) => " " + padCenter2(h, widths[h]) + " ").join("|") + "|";
|
|
146
|
+
const sepLine = "|" + headers.map((h) => ":" + "-".repeat(Math.max(3, widths[h])) + ":").join("|") + "|";
|
|
147
|
+
console.log(headerLine);
|
|
148
|
+
console.log(sepLine);
|
|
107
149
|
for (const row of rows) {
|
|
108
|
-
|
|
109
|
-
console.log(rowLine);
|
|
150
|
+
console.log("|" + headers.map((h) => " " + padCenter2(row[h] ?? "", widths[h]) + " ").join("|") + "|");
|
|
110
151
|
}
|
|
111
152
|
}
|
|
112
|
-
function
|
|
153
|
+
function loadDotEnv(envFile) {
|
|
154
|
+
dotenv.config({ path: path.resolve(process.cwd(), envFile), quiet: true });
|
|
155
|
+
}
|
|
156
|
+
function loadMeta(lang, configFile) {
|
|
113
157
|
const cwd = process.cwd();
|
|
114
158
|
const candidates = [
|
|
115
159
|
path.resolve(cwd, configFile),
|
|
116
|
-
// ./env.meta.json (по умолчанию)
|
|
117
160
|
path.resolve(cwd, "examples", configFile),
|
|
118
|
-
// ./examples/env.meta.json
|
|
119
161
|
path.resolve(cwd, "examples", "env.meta.json")
|
|
120
|
-
// явный fallback
|
|
121
162
|
];
|
|
122
|
-
const
|
|
123
|
-
if (!
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
163
|
+
const found = candidates.find((p) => fs.existsSync(p));
|
|
164
|
+
if (!found) {
|
|
165
|
+
fail(lang, "META_NOT_FOUND", [
|
|
166
|
+
t(lang, "META_TRIED"),
|
|
167
|
+
...candidates.map((p) => `- ${p}`),
|
|
168
|
+
"",
|
|
169
|
+
t(lang, "META_TIP"),
|
|
170
|
+
" npx zod-envkit show -c examples/env.meta.json"
|
|
171
|
+
]);
|
|
172
|
+
}
|
|
173
|
+
const configPath = found;
|
|
174
|
+
try {
|
|
175
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
176
|
+
return { meta: JSON.parse(raw), configPath };
|
|
177
|
+
} catch {
|
|
178
|
+
fail(lang, "META_PARSE_FAILED", [`- ${configPath}`]);
|
|
130
179
|
}
|
|
131
|
-
const raw = fs.readFileSync(configPath, "utf8");
|
|
132
|
-
return JSON.parse(raw);
|
|
133
180
|
}
|
|
134
181
|
var program = new Command();
|
|
135
|
-
program.name("zod-envkit").description("Env docs + runtime checks for Node.js projects");
|
|
182
|
+
program.name("zod-envkit").description("Env docs + runtime checks for Node.js projects").showHelpAfterError().showSuggestionAfterError().option("--lang <lang>", "CLI language (en | ru)");
|
|
136
183
|
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
|
|
184
|
+
const lang = resolveLang(program.opts().lang);
|
|
185
|
+
const { meta } = loadMeta(lang, opts.config);
|
|
138
186
|
fs.writeFileSync(opts.outExample, generateEnvExample(meta), "utf8");
|
|
139
187
|
fs.writeFileSync(opts.outDocs, generateEnvDocs(meta), "utf8");
|
|
140
|
-
console.log(
|
|
188
|
+
console.log(t(lang, "GENERATED", { example: opts.outExample, docs: opts.outDocs }));
|
|
189
|
+
process.exit(0);
|
|
141
190
|
});
|
|
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
191
|
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
|
-
|
|
145
|
-
|
|
192
|
+
const lang = resolveLang(program.opts().lang);
|
|
193
|
+
loadDotEnv(opts.envFile);
|
|
194
|
+
const { meta } = loadMeta(lang, opts.config);
|
|
146
195
|
const rows = Object.entries(meta).map(([key, m]) => {
|
|
147
196
|
const required = m.required === false ? "no" : "yes";
|
|
148
197
|
const raw = process.env[key];
|
|
@@ -157,10 +206,12 @@ program.command("show").description("Show current env status (loads .env, masks
|
|
|
157
206
|
};
|
|
158
207
|
});
|
|
159
208
|
printTable(rows, ["Key", "Required", "Present", "Value", "Description"]);
|
|
209
|
+
process.exit(0);
|
|
160
210
|
});
|
|
161
211
|
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
|
-
|
|
163
|
-
|
|
212
|
+
const lang = resolveLang(program.opts().lang);
|
|
213
|
+
loadDotEnv(opts.envFile);
|
|
214
|
+
const { meta } = loadMeta(lang, opts.config);
|
|
164
215
|
const missing = [];
|
|
165
216
|
for (const [key, m] of Object.entries(meta)) {
|
|
166
217
|
const required = m.required !== false;
|
|
@@ -169,18 +220,14 @@ program.command("check").description("Exit with code 1 if any required env var i
|
|
|
169
220
|
if (!raw || raw.length === 0) missing.push(key);
|
|
170
221
|
}
|
|
171
222
|
if (missing.length) {
|
|
172
|
-
console.error(
|
|
223
|
+
console.error(`\u274C ${t(lang, "MISSING_ENV")}`);
|
|
173
224
|
for (const k of missing) console.error(`- ${k}`);
|
|
174
225
|
process.exit(1);
|
|
175
226
|
}
|
|
176
|
-
console.log(
|
|
227
|
+
console.log(`\u2705 ${t(lang, "ENV_OK")}`);
|
|
228
|
+
process.exit(0);
|
|
177
229
|
});
|
|
230
|
+
var known = /* @__PURE__ */ new Set(["generate", "show", "check", "-h", "--help", "-V", "--version", "--lang"]);
|
|
231
|
+
var hasCommand = process.argv.slice(2).some((a) => known.has(a));
|
|
232
|
+
if (!hasCommand) process.argv.splice(2, 0, "generate");
|
|
178
233
|
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
|
-
}
|
package/dist/index.cjs
CHANGED
|
@@ -21,7 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
formatZodError: () => formatZodError,
|
|
24
|
-
loadEnv: () => loadEnv
|
|
24
|
+
loadEnv: () => loadEnv,
|
|
25
|
+
mustLoadEnv: () => mustLoadEnv
|
|
25
26
|
});
|
|
26
27
|
module.exports = __toCommonJS(index_exports);
|
|
27
28
|
function loadEnv(schema, opts) {
|
|
@@ -30,11 +31,17 @@ function loadEnv(schema, opts) {
|
|
|
30
31
|
if (opts?.throwOnError) throw parsed.error;
|
|
31
32
|
return { ok: false, error: parsed.error };
|
|
32
33
|
}
|
|
34
|
+
function mustLoadEnv(schema) {
|
|
35
|
+
const res = loadEnv(schema);
|
|
36
|
+
if (res.ok) return res.env;
|
|
37
|
+
throw res.error;
|
|
38
|
+
}
|
|
33
39
|
function formatZodError(err) {
|
|
34
40
|
return err.issues.map((i) => `- ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
35
41
|
}
|
|
36
42
|
// Annotate the CommonJS export names for ESM import in node:
|
|
37
43
|
0 && (module.exports = {
|
|
38
44
|
formatZodError,
|
|
39
|
-
loadEnv
|
|
45
|
+
loadEnv,
|
|
46
|
+
mustLoadEnv
|
|
40
47
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
type
|
|
4
|
-
|
|
3
|
+
type LoadEnvOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* If true, throws ZodError instead of returning { ok: false }
|
|
6
|
+
*/
|
|
5
7
|
throwOnError?: boolean;
|
|
6
|
-
}
|
|
8
|
+
};
|
|
9
|
+
type LoadEnvOk<T extends z.ZodTypeAny> = {
|
|
7
10
|
ok: true;
|
|
8
11
|
env: z.infer<T>;
|
|
9
|
-
}
|
|
12
|
+
};
|
|
13
|
+
type LoadEnvFail = {
|
|
10
14
|
ok: false;
|
|
11
15
|
error: z.ZodError;
|
|
12
16
|
};
|
|
17
|
+
declare function loadEnv<T extends z.ZodTypeAny>(schema: T, opts?: LoadEnvOptions): LoadEnvOk<T> | LoadEnvFail;
|
|
18
|
+
/**
|
|
19
|
+
* Convenience wrapper around loadEnv(schema, { throwOnError: true })
|
|
20
|
+
* Returns typed env or throws ZodError
|
|
21
|
+
*/
|
|
22
|
+
declare function mustLoadEnv<T extends z.ZodTypeAny>(schema: T): z.infer<T>;
|
|
23
|
+
/**
|
|
24
|
+
* Human-friendly ZodError formatting (one issue per line)
|
|
25
|
+
*/
|
|
13
26
|
declare function formatZodError(err: z.ZodError): string;
|
|
14
27
|
|
|
15
|
-
export { type
|
|
28
|
+
export { type LoadEnvFail, type LoadEnvOk, type LoadEnvOptions, formatZodError, loadEnv, mustLoadEnv };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
type
|
|
4
|
-
|
|
3
|
+
type LoadEnvOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* If true, throws ZodError instead of returning { ok: false }
|
|
6
|
+
*/
|
|
5
7
|
throwOnError?: boolean;
|
|
6
|
-
}
|
|
8
|
+
};
|
|
9
|
+
type LoadEnvOk<T extends z.ZodTypeAny> = {
|
|
7
10
|
ok: true;
|
|
8
11
|
env: z.infer<T>;
|
|
9
|
-
}
|
|
12
|
+
};
|
|
13
|
+
type LoadEnvFail = {
|
|
10
14
|
ok: false;
|
|
11
15
|
error: z.ZodError;
|
|
12
16
|
};
|
|
17
|
+
declare function loadEnv<T extends z.ZodTypeAny>(schema: T, opts?: LoadEnvOptions): LoadEnvOk<T> | LoadEnvFail;
|
|
18
|
+
/**
|
|
19
|
+
* Convenience wrapper around loadEnv(schema, { throwOnError: true })
|
|
20
|
+
* Returns typed env or throws ZodError
|
|
21
|
+
*/
|
|
22
|
+
declare function mustLoadEnv<T extends z.ZodTypeAny>(schema: T): z.infer<T>;
|
|
23
|
+
/**
|
|
24
|
+
* Human-friendly ZodError formatting (one issue per line)
|
|
25
|
+
*/
|
|
13
26
|
declare function formatZodError(err: z.ZodError): string;
|
|
14
27
|
|
|
15
|
-
export { type
|
|
28
|
+
export { type LoadEnvFail, type LoadEnvOk, type LoadEnvOptions, formatZodError, loadEnv, mustLoadEnv };
|
package/dist/index.js
CHANGED
|
@@ -5,10 +5,16 @@ function loadEnv(schema, opts) {
|
|
|
5
5
|
if (opts?.throwOnError) throw parsed.error;
|
|
6
6
|
return { ok: false, error: parsed.error };
|
|
7
7
|
}
|
|
8
|
+
function mustLoadEnv(schema) {
|
|
9
|
+
const res = loadEnv(schema);
|
|
10
|
+
if (res.ok) return res.env;
|
|
11
|
+
throw res.error;
|
|
12
|
+
}
|
|
8
13
|
function formatZodError(err) {
|
|
9
14
|
return err.issues.map((i) => `- ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
10
15
|
}
|
|
11
16
|
export {
|
|
12
17
|
formatZodError,
|
|
13
|
-
loadEnv
|
|
18
|
+
loadEnv,
|
|
19
|
+
mustLoadEnv
|
|
14
20
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zod-envkit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Validate environment variables with Zod and generate .env.example",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "",
|
|
7
|
+
"homepage": "https://github.com/nxtxe/zod-envkit#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/nxtxe/zod-envkit.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/nxtxe/zod-envkit/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"env",
|
|
17
|
+
"dotenv",
|
|
18
|
+
"zod",
|
|
19
|
+
"validation",
|
|
20
|
+
"cli"
|
|
21
|
+
],
|
|
5
22
|
"type": "module",
|
|
6
23
|
"main": "dist/index.cjs",
|
|
7
24
|
"module": "dist/index.js",
|
|
@@ -22,39 +39,36 @@
|
|
|
22
39
|
"CHANGELOG.md",
|
|
23
40
|
"LICENSE"
|
|
24
41
|
],
|
|
25
|
-
"repository": {
|
|
26
|
-
"type": "git",
|
|
27
|
-
"url": "git+https://github.com/nxtxe/zod-envkit.git"
|
|
28
|
-
},
|
|
29
|
-
"bugs": {
|
|
30
|
-
"url": "https://github.com/nxtxe/zod-envkit/issues"
|
|
31
|
-
},
|
|
32
|
-
"homepage": "https://github.com/nxtxe/zod-envkit#readme",
|
|
33
42
|
"scripts": {
|
|
34
43
|
"build": "tsup src/index.ts src/cli.ts --format esm,cjs --dts",
|
|
35
44
|
"dev": "tsup src/index.ts src/cli.ts --watch --dts",
|
|
36
45
|
"test": "vitest run",
|
|
37
|
-
"
|
|
46
|
+
"release": "semantic-release"
|
|
38
47
|
},
|
|
39
|
-
"keywords": [
|
|
40
|
-
"env",
|
|
41
|
-
"dotenv",
|
|
42
|
-
"zod",
|
|
43
|
-
"validation",
|
|
44
|
-
"cli"
|
|
45
|
-
],
|
|
46
|
-
"author": "",
|
|
47
|
-
"license": "MIT",
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"commander": "^13.1.0",
|
|
50
50
|
"dotenv": "^17.2.3",
|
|
51
51
|
"zod": "^4.3.6"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
55
|
+
"@semantic-release/commit-analyzer": "^11.1.0",
|
|
56
|
+
"@semantic-release/git": "^10.0.1",
|
|
57
|
+
"@semantic-release/github": "^9.2.6",
|
|
58
|
+
"@semantic-release/npm": "^11.0.3",
|
|
59
|
+
"@semantic-release/release-notes-generator": "^12.1.0",
|
|
54
60
|
"@types/node": "^25.0.10",
|
|
55
61
|
"eslint": "^9.39.2",
|
|
62
|
+
"semantic-release": "^22.0.12",
|
|
56
63
|
"tsup": "^8.5.1",
|
|
57
64
|
"typescript": "^5.9.3",
|
|
58
65
|
"vitest": "^3.2.4"
|
|
59
|
-
}
|
|
66
|
+
},
|
|
67
|
+
"publishConfig": {
|
|
68
|
+
"access": "public"
|
|
69
|
+
},
|
|
70
|
+
"engines": {
|
|
71
|
+
"node": ">=18"
|
|
72
|
+
},
|
|
73
|
+
"packageManager": "pnpm@9.15.9"
|
|
60
74
|
}
|