@gudroniy/env-doctor 1.0.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/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # env-doctor
2
+
3
+ CLI, который проверяет `.env` файлы на пропущенные переменные, несовпадение типов и расхождения между окружениями (dev / staging / production) — **до** того, как это сломает деплой.
4
+
5
+ ## Проблема
6
+
7
+ Классическая ситуация: в `.env` локально всё работает, а на проде забыли добавить одну переменную — и приложение падает после деплоя. Или переменная есть, но записана не в том формате (например, `PORT=abc` вместо числа). Обычно это ловят вручную, глазами, сравнивая файлы.
8
+
9
+ `env-doctor` автоматизирует эту проверку.
10
+
11
+ ## Установка
12
+
13
+ ```bash
14
+ npm install -g env-doctor
15
+ ```
16
+
17
+ Или без глобальной установки:
18
+
19
+ ```bash
20
+ npx env-doctor check
21
+ ```
22
+
23
+ ## Быстрый старт
24
+
25
+ 1. Сгенерируй схему на основе рабочего `.env`:
26
+
27
+ ```bash
28
+ env-doctor init --file .env
29
+ ```
30
+
31
+ Это создаст `env.schema.json` — файл с описанием ожидаемых переменных и их типов (определяются автоматически).
32
+
33
+ 2. Отредактируй схему при необходимости — пометь необязательные переменные (`"required": false`), поправь типы.
34
+
35
+ 3. Проверь любой другой `.env` файл на соответствие схеме:
36
+
37
+ ```bash
38
+ env-doctor check .env.production
39
+ ```
40
+
41
+ Без аргументов проверяются все файлы вида `.env*` в текущей папке:
42
+
43
+ ```bash
44
+ env-doctor check
45
+ ```
46
+
47
+ ## Пример вывода
48
+
49
+ ```
50
+ .env.production
51
+ ✘ Переменная "API_KEY" обязательна, но пустая
52
+ ✘ Переменная "PORT" должна быть типа number, получено "not-a-number"
53
+ ✘ Отсутствует обязательная переменная "STRIPE_WEBHOOK_SECRET"
54
+ ⚠ Переменная "LEGACY_FLAG" не описана в схеме
55
+
56
+ Итого: 3 ошибок, 1 предупреждений
57
+ ```
58
+
59
+ ## Использование в CI
60
+
61
+ Команда завершается с кодом `1`, если найдены ошибки — удобно добавить проверку в pipeline перед деплоем:
62
+
63
+ ```yaml
64
+ - run: npx env-doctor check .env.production --strict
65
+ ```
66
+
67
+ Флаг `--strict` также считает ошибкой любые переменные, не описанные в схеме (например, забытые старые ключи).
68
+
69
+ ## Поддерживаемые типы
70
+
71
+ `string`, `number`, `boolean`, `url`, `email`
72
+
73
+ ## Лицензия
74
+
75
+ MIT
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ const { program } = require('commander');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const chalk = require('chalk');
6
+ const { parseEnvFile } = require('../src/parser');
7
+ const { inferType } = require('../src/inferType');
8
+ const { checkEnv } = require('../src/validators');
9
+
10
+ const DEFAULT_SCHEMA_FILE = 'env.schema.json';
11
+
12
+ program
13
+ .name('env-doctor')
14
+ .description(
15
+ 'Проверяет .env файлы на пропущенные переменные, несоответствие типов и расхождения между окружениями'
16
+ )
17
+ .version('1.0.0');
18
+
19
+ program
20
+ .command('init')
21
+ .description('Сгенерировать env.schema.json на основе существующего .env файла')
22
+ .option('-f, --file <path>', 'Путь к исходному .env файлу', '.env')
23
+ .option('-o, --output <path>', 'Путь для сохранения схемы', DEFAULT_SCHEMA_FILE)
24
+ .action((opts) => {
25
+ const filePath = path.resolve(process.cwd(), opts.file);
26
+ if (!fs.existsSync(filePath)) {
27
+ console.error(chalk.red(`Файл не найден: ${filePath}`));
28
+ process.exit(1);
29
+ }
30
+
31
+ const { values } = parseEnvFile(filePath);
32
+ const schema = {};
33
+ for (const [key, value] of Object.entries(values)) {
34
+ schema[key] = { required: true, type: inferType(value) };
35
+ }
36
+
37
+ const schemaPath = path.resolve(process.cwd(), opts.output);
38
+ fs.writeFileSync(schemaPath, JSON.stringify(schema, null, 2) + '\n');
39
+
40
+ console.log(chalk.green(`✔ Схема создана: ${opts.output} (${Object.keys(schema).length} переменных)`));
41
+ console.log(chalk.gray(' Отредактируй файл вручную: пометь опциональные переменные, поправь типы при необходимости.'));
42
+ });
43
+
44
+ program
45
+ .command('check [files...]')
46
+ .description('Проверить .env файлы на соответствие схеме')
47
+ .option('-s, --strict', 'Считать лишние переменные ошибкой, а не предупреждением')
48
+ .option('--schema <path>', 'Путь к файлу схемы', DEFAULT_SCHEMA_FILE)
49
+ .action((files, opts) => {
50
+ const schemaPath = path.resolve(process.cwd(), opts.schema);
51
+ if (!fs.existsSync(schemaPath)) {
52
+ console.error(chalk.red(`Схема не найдена: ${schemaPath}`));
53
+ console.error(chalk.gray(' Запусти "env-doctor init" сначала, чтобы её создать.'));
54
+ process.exit(1);
55
+ }
56
+ const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));
57
+
58
+ let targetFiles = files && files.length ? files : null;
59
+ if (!targetFiles) {
60
+ targetFiles = fs
61
+ .readdirSync(process.cwd())
62
+ .filter((f) => /^\.env(\..+)?$/.test(f));
63
+ }
64
+
65
+ if (targetFiles.length === 0) {
66
+ console.error(chalk.red('Не найдено ни одного .env файла для проверки.'));
67
+ process.exit(1);
68
+ }
69
+
70
+ let totalErrors = 0;
71
+ let totalWarnings = 0;
72
+
73
+ for (const file of targetFiles) {
74
+ const filePath = path.resolve(process.cwd(), file);
75
+ console.log(chalk.bold(`\n${file}`));
76
+
77
+ if (!fs.existsSync(filePath)) {
78
+ console.log(chalk.red(' ✘ файл не найден'));
79
+ totalErrors++;
80
+ continue;
81
+ }
82
+
83
+ const { values, duplicates } = parseEnvFile(filePath);
84
+ const issues = checkEnv(schema, values, { strict: Boolean(opts.strict) });
85
+
86
+ duplicates.forEach((d) => {
87
+ issues.push({
88
+ level: 'warning',
89
+ key: d.key,
90
+ message: `Дублирующийся ключ "${d.key}" (строка ${d.line})`,
91
+ });
92
+ });
93
+
94
+ if (issues.length === 0) {
95
+ console.log(chalk.green(' ✔ Всё в порядке'));
96
+ continue;
97
+ }
98
+
99
+ issues.forEach((issue) => {
100
+ const icon = issue.level === 'error' ? chalk.red('✘') : chalk.yellow('⚠');
101
+ console.log(` ${icon} ${issue.message}`);
102
+ if (issue.level === 'error') totalErrors++;
103
+ else totalWarnings++;
104
+ });
105
+ }
106
+
107
+ console.log('');
108
+ console.log(chalk.bold(`Итого: ${totalErrors} ошибок, ${totalWarnings} предупреждений`));
109
+
110
+ if (totalErrors > 0) process.exit(1);
111
+ });
112
+
113
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@gudroniy/env-doctor",
3
+ "version": "1.0.0",
4
+ "description": "CLI, который проверяет .env файлы на пропущенные переменные, несовпадение типов и расхождения между окружениями",
5
+ "bin": {
6
+ "env-doctor": "./bin/env-doctor.js"
7
+ },
8
+ "main": "src/index.js",
9
+ "license": "MIT",
10
+ "files": [
11
+ "bin",
12
+ "src",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "test": "node --test"
17
+ },
18
+ "keywords": ["env", "dotenv", "cli", "devtools", "validation"],
19
+ "dependencies": {
20
+ "commander": "^12.0.0",
21
+ "chalk": "^4.1.2"
22
+ },
23
+ "engines": {
24
+ "node": ">=14"
25
+ }
26
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Пытается угадать тип значения переменной окружения.
3
+ * Используется командой `init` для генерации схемы.
4
+ */
5
+ function inferType(value) {
6
+ if (value === '') return 'string';
7
+ if (/^(true|false)$/i.test(value)) return 'boolean';
8
+ if (/^-?\d+(\.\d+)?$/.test(value)) return 'number';
9
+ if (/^https?:\/\/.+/i.test(value)) return 'url';
10
+ if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'email';
11
+ return 'string';
12
+ }
13
+
14
+ module.exports = { inferType };
package/src/parser.js ADDED
@@ -0,0 +1,42 @@
1
+ const fs = require('fs');
2
+
3
+ /**
4
+ * Парсит .env файл в объект ключ-значение.
5
+ * Возвращает также список дублирующихся ключей.
6
+ */
7
+ function parseEnvFile(filePath) {
8
+ const content = fs.readFileSync(filePath, 'utf-8');
9
+ const lines = content.split(/\r?\n/);
10
+ const values = {};
11
+ const duplicates = [];
12
+ const seen = new Set();
13
+
14
+ lines.forEach((rawLine, idx) => {
15
+ const line = rawLine.trim();
16
+ if (!line || line.startsWith('#')) return;
17
+
18
+ const eqIndex = line.indexOf('=');
19
+ if (eqIndex === -1) return;
20
+
21
+ const key = line.slice(0, eqIndex).trim();
22
+ let value = line.slice(eqIndex + 1).trim();
23
+
24
+ // убираем окружающие кавычки
25
+ if (
26
+ (value.startsWith('"') && value.endsWith('"')) ||
27
+ (value.startsWith("'") && value.endsWith("'"))
28
+ ) {
29
+ value = value.slice(1, -1);
30
+ }
31
+
32
+ if (seen.has(key)) {
33
+ duplicates.push({ key, line: idx + 1 });
34
+ }
35
+ seen.add(key);
36
+ values[key] = value;
37
+ });
38
+
39
+ return { values, duplicates };
40
+ }
41
+
42
+ module.exports = { parseEnvFile };
@@ -0,0 +1,64 @@
1
+ function validateType(value, expectedType) {
2
+ if (!expectedType || expectedType === 'string') return true;
3
+ if (expectedType === 'number') return /^-?\d+(\.\d+)?$/.test(value);
4
+ if (expectedType === 'boolean') return /^(true|false)$/i.test(value);
5
+ if (expectedType === 'url') return /^https?:\/\/.+/i.test(value);
6
+ if (expectedType === 'email') return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
7
+ return true;
8
+ }
9
+
10
+ /**
11
+ * Сравнивает значения из .env файла со схемой.
12
+ * Возвращает список проблем: { level: 'error' | 'warning', key, message }
13
+ */
14
+ function checkEnv(schema, envValues, { strict = false } = {}) {
15
+ const issues = [];
16
+
17
+ for (const [key, rule] of Object.entries(schema)) {
18
+ const hasKey = Object.prototype.hasOwnProperty.call(envValues, key);
19
+
20
+ if (rule.required && !hasKey) {
21
+ issues.push({
22
+ level: 'error',
23
+ key,
24
+ message: `Отсутствует обязательная переменная "${key}"`,
25
+ });
26
+ continue;
27
+ }
28
+
29
+ if (hasKey) {
30
+ const value = envValues[key];
31
+
32
+ if (rule.required && value === '') {
33
+ issues.push({
34
+ level: 'error',
35
+ key,
36
+ message: `Переменная "${key}" обязательна, но пустая`,
37
+ });
38
+ continue;
39
+ }
40
+
41
+ if (value !== '' && rule.type && !validateType(value, rule.type)) {
42
+ issues.push({
43
+ level: 'error',
44
+ key,
45
+ message: `Переменная "${key}" должна быть типа ${rule.type}, получено "${value}"`,
46
+ });
47
+ }
48
+ }
49
+ }
50
+
51
+ for (const key of Object.keys(envValues)) {
52
+ if (!schema[key]) {
53
+ issues.push({
54
+ level: strict ? 'error' : 'warning',
55
+ key,
56
+ message: `Переменная "${key}" не описана в схеме`,
57
+ });
58
+ }
59
+ }
60
+
61
+ return issues;
62
+ }
63
+
64
+ module.exports = { checkEnv, validateType };