@scuton/dotenv-guard 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/src/cli.ts ADDED
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, existsSync } from 'fs';
4
+ import { syncCheck } from './core/sync.js';
5
+ import { validate, inferRules } from './core/validator.js';
6
+ import { checkLeaks, installPreCommitHook } from './core/leak.js';
7
+ import { generateExample, ensureGitignore } from './core/generator.js';
8
+ import { parseEnvFileToMap } from './core/parser.js';
9
+ import { red, green, yellow, gray, bold, dim } from './utils/colors.js';
10
+
11
+ const args = process.argv.slice(2);
12
+ const command = args[0];
13
+
14
+ function showHelp() {
15
+ console.log(`
16
+ ${bold('dotenv-guard')} — Validate env vars, sync .env files, prevent leaks
17
+
18
+ ${bold('Usage:')}
19
+ dotenv-guard <command> [options]
20
+
21
+ ${bold('Commands:')}
22
+ sync Compare .env with .env.example
23
+ validate Validate env var types (from .env.example hints)
24
+ leak-check Scan for leaked secrets in git
25
+ init Create .env.example + update .gitignore
26
+ install-hook Install git pre-commit hook to block .env commits
27
+
28
+ ${bold('Options:')}
29
+ --env <path> Path to .env file (default: .env)
30
+ --example <path> Path to .env.example (default: .env.example)
31
+ -h, --help Show help
32
+ -v, --version Show version
33
+
34
+ ${bold('Programmatic:')}
35
+ import 'dotenv-guard/auto';
36
+ // Auto-check on import
37
+ import { guard } from 'dotenv-guard'; // Manual check
38
+ `);
39
+ }
40
+
41
+ // Arg parse
42
+ const envPath = args.includes('--env') ? args[args.indexOf('--env') + 1] : '.env';
43
+ const examplePath = args.includes('--example') ? args[args.indexOf('--example') + 1] : '.env.example';
44
+
45
+ switch (command) {
46
+ case 'sync': {
47
+ try {
48
+ const result = syncCheck(envPath, examplePath);
49
+ console.log('');
50
+ console.log(bold(' dotenv-guard sync'));
51
+ console.log(gray(` ${result.envPath} ↔ ${result.examplePath}`));
52
+ console.log('');
53
+
54
+ if (result.missing.length > 0) {
55
+ console.log(red(` ✖ Missing (${result.missing.length}):`));
56
+ result.missing.forEach(k => console.log(yellow(` → ${k}`)));
57
+ console.log('');
58
+ }
59
+
60
+ if (result.extra.length > 0) {
61
+ console.log(yellow(` ⚠ Extra (${result.extra.length}):`));
62
+ result.extra.forEach(k => console.log(gray(` → ${k}`)));
63
+ console.log('');
64
+ }
65
+
66
+ console.log(green(` ✓ Synced: ${result.synced.length} variables`));
67
+
68
+ if (result.missing.length > 0) {
69
+ console.log('');
70
+ console.log(dim(` Add missing variables to ${result.envPath} to fix.`));
71
+ }
72
+ console.log('');
73
+
74
+ process.exit(result.missing.length > 0 ? 1 : 0);
75
+ } catch (err: any) {
76
+ console.error(red(`\n ✖ ${err.message}\n`));
77
+ process.exit(1);
78
+ }
79
+ break;
80
+ }
81
+
82
+ case 'validate': {
83
+ try {
84
+ if (!existsSync(examplePath)) {
85
+ console.error(red(`\n ✖ ${examplePath} not found. Run "dotenv-guard init" first.\n`));
86
+ process.exit(1);
87
+ }
88
+
89
+ const exampleContent = readFileSync(examplePath, 'utf-8');
90
+ const rules = inferRules(exampleContent);
91
+
92
+ // Mevcut env değerlerini al
93
+ const envMap = existsSync(envPath) ? parseEnvFileToMap(readFileSync(envPath, 'utf-8')) : new Map();
94
+ const env: Record<string, string | undefined> = {};
95
+
96
+ // process.env + .env dosyası birleştir
97
+ for (const rule of rules) {
98
+ env[rule.key] = process.env[rule.key] || envMap.get(rule.key);
99
+ }
100
+
101
+ const errors = validate(env, rules);
102
+
103
+ console.log('');
104
+ console.log(bold(' dotenv-guard validate'));
105
+ console.log('');
106
+
107
+ if (errors.length === 0) {
108
+ console.log(green(` ✓ All ${rules.length} variables are valid`));
109
+ } else {
110
+ console.log(red(` ✖ ${errors.length} validation error(s):`));
111
+ console.log('');
112
+ errors.forEach(e => {
113
+ console.log(` ${red('✖')} ${bold(e.key)} ${e.message}`);
114
+ if (e.value) console.log(gray(` current value: "${e.value}"`));
115
+ });
116
+ }
117
+ console.log('');
118
+
119
+ process.exit(errors.length > 0 ? 1 : 0);
120
+ } catch (err: any) {
121
+ console.error(red(`\n ✖ ${err.message}\n`));
122
+ process.exit(1);
123
+ }
124
+ break;
125
+ }
126
+
127
+ case 'leak-check': {
128
+ const result = checkLeaks('.');
129
+ console.log('');
130
+ console.log(bold(' dotenv-guard leak-check'));
131
+ console.log('');
132
+
133
+ // .gitignore check
134
+ if (result.gitignoreHasEnv) {
135
+ console.log(green(' ✓ .gitignore includes .env'));
136
+ } else {
137
+ console.log(red(' ✖ .gitignore does NOT include .env'));
138
+ console.log(gray(' Run: dotenv-guard init'));
139
+ }
140
+
141
+ // Pre-commit hook
142
+ if (result.preCommitHookInstalled) {
143
+ console.log(green(' ✓ Pre-commit hook installed'));
144
+ } else {
145
+ console.log(yellow(' ⚠ No pre-commit hook'));
146
+ console.log(gray(' Run: dotenv-guard install-hook'));
147
+ }
148
+
149
+ // Leaks
150
+ if (result.leaks.length === 0) {
151
+ console.log(green(' ✓ No .env files tracked in git'));
152
+ } else {
153
+ console.log(red(` ✖ ${result.leaks.length} leaked variable(s) in git:`));
154
+ console.log('');
155
+ result.leaks.forEach(l => {
156
+ const icon = l.severity === 'high' ? red('HIGH') : yellow('MED');
157
+ console.log(` [${icon}] ${l.file}:${l.line} — ${bold(l.key)}`);
158
+ });
159
+ console.log('');
160
+ console.log(dim(' Remove tracked files: git rm --cached .env'));
161
+ }
162
+ console.log('');
163
+
164
+ process.exit(result.leaks.length > 0 ? 1 : 0);
165
+ break;
166
+ }
167
+
168
+ case 'init': {
169
+ console.log('');
170
+ console.log(bold(' dotenv-guard init'));
171
+ console.log('');
172
+
173
+ // .env.example oluştur
174
+ if (existsSync('.env') && !existsSync('.env.example')) {
175
+ generateExample('.env', '.env.example');
176
+ console.log(green(' ✓ Created .env.example from .env'));
177
+ } else if (existsSync('.env.example')) {
178
+ console.log(gray(' • .env.example already exists'));
179
+ } else {
180
+ console.log(yellow(' ⚠ No .env file found — create one first'));
181
+ }
182
+
183
+ // .gitignore güncelle
184
+ const updated = ensureGitignore('.');
185
+ if (updated) {
186
+ console.log(green(' ✓ Updated .gitignore with .env entries'));
187
+ } else {
188
+ console.log(gray(' • .gitignore already includes .env'));
189
+ }
190
+
191
+ console.log('');
192
+ break;
193
+ }
194
+
195
+ case 'install-hook': {
196
+ try {
197
+ installPreCommitHook('.');
198
+ console.log('');
199
+ console.log(green(bold(' ✓ Pre-commit hook installed')));
200
+ console.log(gray(' .env files will be blocked from commits'));
201
+ console.log(gray(' Bypass with: git commit --no-verify'));
202
+ console.log('');
203
+ } catch (err: any) {
204
+ console.error(red(`\n ✖ ${err.message}\n`));
205
+ process.exit(1);
206
+ }
207
+ break;
208
+ }
209
+
210
+ case '-v':
211
+ case '--version':
212
+ console.log('1.0.0');
213
+ break;
214
+
215
+ case '-h':
216
+ case '--help':
217
+ case undefined:
218
+ showHelp();
219
+ break;
220
+
221
+ default:
222
+ console.error(red(`\n Unknown command: ${command}`));
223
+ showHelp();
224
+ process.exit(1);
225
+ }
@@ -0,0 +1,49 @@
1
+ import { readFileSync, writeFileSync, existsSync, appendFileSync } from 'fs';
2
+ import { parseEnvFile } from './parser.js';
3
+
4
+ // .env'den .env.example oluştur (değerleri maskele)
5
+ export function generateExample(envPath: string = '.env', outputPath: string = '.env.example'): string {
6
+ if (!existsSync(envPath)) throw new Error(`${envPath} not found`);
7
+
8
+ const content = readFileSync(envPath, 'utf-8');
9
+ const entries = parseEnvFile(content);
10
+
11
+ const lines: string[] = ['# Environment Variables', '# Copy this file to .env and fill in the values', ''];
12
+
13
+ for (const entry of entries) {
14
+ // Değeri maskele ama tip hint'i bırak
15
+ let placeholder = '';
16
+ if (entry.value.match(/^https?:\/\//)) placeholder = 'https://... # type:url';
17
+ else if (entry.value.match(/^\d+$/)) placeholder = '0 # type:number';
18
+ else if (entry.value.match(/^(true|false)$/i)) placeholder = 'false # type:boolean';
19
+ else placeholder = '# type:string';
20
+
21
+ lines.push(`${entry.key}=${placeholder}`);
22
+ }
23
+
24
+ const output = lines.join('\n') + '\n';
25
+ writeFileSync(outputPath, output, 'utf-8');
26
+ return output;
27
+ }
28
+
29
+ // .gitignore'a .env ekle
30
+ export function ensureGitignore(dir: string = '.'): boolean {
31
+ const gitignorePath = `${dir}/.gitignore`;
32
+ const envEntries = ['.env', '.env.local', '.env.*.local'];
33
+
34
+ let content = '';
35
+ if (existsSync(gitignorePath)) {
36
+ content = readFileSync(gitignorePath, 'utf-8');
37
+ }
38
+
39
+ const lines = content.split('\n');
40
+ const toAdd = envEntries.filter(e => !lines.some(l => l.trim() === e));
41
+
42
+ if (toAdd.length > 0) {
43
+ const addition = '\n# Environment variables\n' + toAdd.join('\n') + '\n';
44
+ appendFileSync(gitignorePath, addition, 'utf-8');
45
+ return true;
46
+ }
47
+
48
+ return false;
49
+ }
@@ -0,0 +1,115 @@
1
+ import { execSync } from 'child_process';
2
+ import { readFileSync, existsSync, mkdirSync, writeFileSync, chmodSync } from 'fs';
3
+
4
+ export interface LeakResult {
5
+ leaks: LeakEntry[];
6
+ gitignoreHasEnv: boolean;
7
+ preCommitHookInstalled: boolean;
8
+ }
9
+
10
+ export interface LeakEntry {
11
+ file: string;
12
+ line: number;
13
+ key: string;
14
+ severity: 'high' | 'medium' | 'low';
15
+ }
16
+
17
+ // Hassas key pattern'leri
18
+ const SENSITIVE_PATTERNS = [
19
+ /API[_-]?KEY/i,
20
+ /SECRET/i,
21
+ /PASSWORD/i,
22
+ /TOKEN/i,
23
+ /PRIVATE[_-]?KEY/i,
24
+ /DATABASE[_-]?URL/i,
25
+ /MONGO[_-]?URI/i,
26
+ /REDIS[_-]?URL/i,
27
+ /AWS[_-]?ACCESS/i,
28
+ /STRIPE/i,
29
+ /SENDGRID/i,
30
+ /TWILIO/i,
31
+ /ANTHROPIC/i,
32
+ /OPENAI/i,
33
+ ];
34
+
35
+ export function checkLeaks(dir: string = '.'): LeakResult {
36
+ const leaks: LeakEntry[] = [];
37
+
38
+ // .gitignore kontrolü
39
+ let gitignoreHasEnv = false;
40
+ const gitignorePath = `${dir}/.gitignore`;
41
+ if (existsSync(gitignorePath)) {
42
+ const content = readFileSync(gitignorePath, 'utf-8');
43
+ gitignoreHasEnv = content.split('\n').some(line =>
44
+ line.trim() === '.env' || line.trim() === '.env*' || line.trim() === '.env.local'
45
+ );
46
+ }
47
+
48
+ // Git tracked files'da .env var mı?
49
+ try {
50
+ const tracked = execSync('git ls-files', { cwd: dir, encoding: 'utf-8' });
51
+ const envFiles = tracked.split('\n').filter(f =>
52
+ f.match(/^\.env($|\.)/) && !f.endsWith('.example') && !f.endsWith('.template')
53
+ );
54
+
55
+ for (const file of envFiles) {
56
+ const content = readFileSync(`${dir}/${file}`, 'utf-8');
57
+ const lines = content.split('\n');
58
+ for (let i = 0; i < lines.length; i++) {
59
+ const match = lines[i].match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)/);
60
+ if (match && match[2].trim().length > 0) {
61
+ const key = match[1];
62
+ const severity = SENSITIVE_PATTERNS.some(p => p.test(key)) ? 'high' : 'medium';
63
+ leaks.push({ file, line: i + 1, key, severity });
64
+ }
65
+ }
66
+ }
67
+ } catch {
68
+ // git yoksa veya git repo değilse skip
69
+ }
70
+
71
+ // Pre-commit hook var mı?
72
+ let preCommitHookInstalled = false;
73
+ const hookPath = `${dir}/.git/hooks/pre-commit`;
74
+ if (existsSync(hookPath)) {
75
+ const hookContent = readFileSync(hookPath, 'utf-8');
76
+ preCommitHookInstalled = hookContent.includes('dotenv-guard');
77
+ }
78
+
79
+ return { leaks, gitignoreHasEnv, preCommitHookInstalled };
80
+ }
81
+
82
+ // Pre-commit hook oluştur
83
+ export function installPreCommitHook(dir: string = '.'): void {
84
+ const hookDir = `${dir}/.git/hooks`;
85
+ const hookPath = `${hookDir}/pre-commit`;
86
+
87
+ const hookScript = `#!/bin/sh
88
+ # dotenv-guard pre-commit hook — prevent .env leaks
89
+ # Installed by: npx dotenv-guard install-hook
90
+
91
+ ENV_FILES=$(git diff --cached --name-only | grep -E '^\\.env' | grep -v '\\.example$' | grep -v '\\.template$')
92
+
93
+ if [ -n "$ENV_FILES" ]; then
94
+ echo ""
95
+ echo "\\033[31m✖ dotenv-guard: Blocked commit — .env file(s) detected:\\033[0m"
96
+ echo ""
97
+ for f in $ENV_FILES; do
98
+ echo " \\033[33m→ $f\\033[0m"
99
+ done
100
+ echo ""
101
+ echo " Remove with: git reset HEAD <file>"
102
+ echo " Or force commit: git commit --no-verify"
103
+ echo ""
104
+ exit 1
105
+ fi
106
+ `;
107
+
108
+ mkdirSync(hookDir, { recursive: true });
109
+ writeFileSync(hookPath, hookScript, 'utf-8');
110
+ try {
111
+ chmodSync(hookPath, '755');
112
+ } catch {
113
+ // Windows'ta chmod çalışmayabilir
114
+ }
115
+ }
@@ -0,0 +1,50 @@
1
+ export interface EnvEntry {
2
+ key: string;
3
+ value: string;
4
+ line: number;
5
+ comment?: string;
6
+ raw: string;
7
+ }
8
+
9
+ export function parseEnvFile(content: string): EnvEntry[] {
10
+ const entries: EnvEntry[] = [];
11
+ const lines = content.split('\n');
12
+
13
+ for (let i = 0; i < lines.length; i++) {
14
+ const raw = lines[i];
15
+ const trimmed = raw.trim();
16
+
17
+ // Boş satır veya comment
18
+ if (!trimmed || trimmed.startsWith('#')) continue;
19
+
20
+ // KEY=VALUE parse
21
+ const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)/);
22
+ if (!match) continue;
23
+
24
+ const key = match[1];
25
+ let value = match[2];
26
+
27
+ // Tırnak temizle
28
+ if ((value.startsWith('"') && value.endsWith('"')) ||
29
+ (value.startsWith("'") && value.endsWith("'"))) {
30
+ value = value.slice(1, -1);
31
+ }
32
+
33
+ // Inline comment temizle (tırnaksız değerlerde)
34
+ const commentMatch = value.match(/\s+#\s/);
35
+ let comment: string | undefined;
36
+ if (commentMatch && !match[2].startsWith('"') && !match[2].startsWith("'")) {
37
+ comment = value.slice(commentMatch.index! + commentMatch[0].length);
38
+ value = value.slice(0, commentMatch.index);
39
+ }
40
+
41
+ entries.push({ key, value, line: i + 1, comment, raw });
42
+ }
43
+
44
+ return entries;
45
+ }
46
+
47
+ export function parseEnvFileToMap(content: string): Map<string, string> {
48
+ const entries = parseEnvFile(content);
49
+ return new Map(entries.map(e => [e.key, e.value]));
50
+ }
@@ -0,0 +1,34 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import { parseEnvFile } from './parser.js';
3
+
4
+ export interface SyncResult {
5
+ missing: string[]; // .env.example'da var ama .env'de yok
6
+ extra: string[]; // .env'de var ama .env.example'da yok
7
+ synced: string[]; // İkisinde de var
8
+ examplePath: string;
9
+ envPath: string;
10
+ }
11
+
12
+ export function syncCheck(
13
+ envPath: string = '.env',
14
+ examplePath: string = '.env.example'
15
+ ): SyncResult {
16
+ if (!existsSync(examplePath)) {
17
+ throw new Error(`${examplePath} not found. Run "dotenv-guard init" to create one.`);
18
+ }
19
+
20
+ const exampleContent = readFileSync(examplePath, 'utf-8');
21
+ const exampleKeys = new Set(parseEnvFile(exampleContent).map(e => e.key));
22
+
23
+ let envKeys = new Set<string>();
24
+ if (existsSync(envPath)) {
25
+ const envContent = readFileSync(envPath, 'utf-8');
26
+ envKeys = new Set(parseEnvFile(envContent).map(e => e.key));
27
+ }
28
+
29
+ const missing = [...exampleKeys].filter(k => !envKeys.has(k));
30
+ const extra = [...envKeys].filter(k => !exampleKeys.has(k));
31
+ const synced = [...exampleKeys].filter(k => envKeys.has(k));
32
+
33
+ return { missing, extra, synced, examplePath, envPath };
34
+ }
@@ -0,0 +1,126 @@
1
+ export type EnvType = 'string' | 'number' | 'boolean' | 'url' | 'email' | 'port' | 'enum';
2
+
3
+ export interface ValidationRule {
4
+ key: string;
5
+ type?: EnvType;
6
+ required?: boolean;
7
+ enum?: string[];
8
+ pattern?: RegExp;
9
+ min?: number;
10
+ max?: number;
11
+ }
12
+
13
+ export interface ValidationError {
14
+ key: string;
15
+ message: string;
16
+ value?: string;
17
+ }
18
+
19
+ export function validate(
20
+ env: Record<string, string | undefined>,
21
+ rules: ValidationRule[]
22
+ ): ValidationError[] {
23
+ const errors: ValidationError[] = [];
24
+
25
+ for (const rule of rules) {
26
+ const value = env[rule.key];
27
+
28
+ // Required check
29
+ if (rule.required !== false && (value === undefined || value === '')) {
30
+ errors.push({ key: rule.key, message: 'is required but missing or empty' });
31
+ continue;
32
+ }
33
+
34
+ if (value === undefined || value === '') continue;
35
+
36
+ // Type checks
37
+ switch (rule.type) {
38
+ case 'number': {
39
+ const num = Number(value);
40
+ if (isNaN(num)) {
41
+ errors.push({ key: rule.key, value, message: `must be a number, got "${value}"` });
42
+ } else {
43
+ if (rule.min !== undefined && num < rule.min)
44
+ errors.push({ key: rule.key, value, message: `must be >= ${rule.min}` });
45
+ if (rule.max !== undefined && num > rule.max)
46
+ errors.push({ key: rule.key, value, message: `must be <= ${rule.max}` });
47
+ }
48
+ break;
49
+ }
50
+ case 'boolean':
51
+ if (!['true', 'false', '1', '0', 'yes', 'no'].includes(value.toLowerCase()))
52
+ errors.push({ key: rule.key, value, message: 'must be a boolean (true/false/1/0/yes/no)' });
53
+ break;
54
+ case 'url':
55
+ try { new URL(value); }
56
+ catch { errors.push({ key: rule.key, value, message: 'must be a valid URL' }); }
57
+ break;
58
+ case 'email':
59
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
60
+ errors.push({ key: rule.key, value, message: 'must be a valid email' });
61
+ break;
62
+ case 'port': {
63
+ const port = Number(value);
64
+ if (isNaN(port) || port < 1 || port > 65535)
65
+ errors.push({ key: rule.key, value, message: 'must be a valid port (1-65535)' });
66
+ break;
67
+ }
68
+ case 'enum':
69
+ if (rule.enum && !rule.enum.includes(value))
70
+ errors.push({ key: rule.key, value, message: `must be one of: ${rule.enum.join(', ')}` });
71
+ break;
72
+ }
73
+
74
+ // Custom pattern
75
+ if (rule.pattern && !rule.pattern.test(value))
76
+ errors.push({ key: rule.key, value, message: `does not match required pattern` });
77
+ }
78
+
79
+ return errors;
80
+ }
81
+
82
+ // .env.example'dan otomatik rule çıkar (comment'lerdeki type hint'lerden)
83
+ export function inferRules(exampleContent: string): ValidationRule[] {
84
+ const lines = exampleContent.split('\n');
85
+ const rules: ValidationRule[] = [];
86
+
87
+ for (const line of lines) {
88
+ const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)/);
89
+ if (!match) continue;
90
+
91
+ const key = match[1];
92
+ const rest = match[2];
93
+ const rule: ValidationRule = { key, required: true };
94
+
95
+ // Comment'ten type hint çıkar
96
+ const commentMatch = rest.match(/#\s*type:\s*(\w+)/i);
97
+ if (commentMatch) {
98
+ rule.type = commentMatch[1].toLowerCase() as EnvType;
99
+ }
100
+
101
+ // Comment'ten required hint
102
+ if (rest.match(/#.*optional/i)) {
103
+ rule.required = false;
104
+ }
105
+
106
+ // Comment'ten enum hint: # enum:development,staging,production
107
+ const enumMatch = rest.match(/#\s*enum:\s*([^\s]+)/i);
108
+ if (enumMatch) {
109
+ rule.type = 'enum';
110
+ rule.enum = enumMatch[1].split(',');
111
+ }
112
+
113
+ // Value'dan type tahmin et
114
+ if (!rule.type) {
115
+ const value = rest.split('#')[0].trim().replace(/^["']|["']$/g, '');
116
+ if (value.match(/^https?:\/\//)) rule.type = 'url';
117
+ else if (value.match(/^\d+$/) && key.match(/PORT/i)) rule.type = 'port';
118
+ else if (value.match(/^(true|false)$/i)) rule.type = 'boolean';
119
+ else if (value.match(/^\d+$/)) rule.type = 'number';
120
+ }
121
+
122
+ rules.push(rule);
123
+ }
124
+
125
+ return rules;
126
+ }
package/src/index.ts ADDED
@@ -0,0 +1,42 @@
1
+ export { parseEnvFile, parseEnvFileToMap } from './core/parser.js';
2
+ export type { EnvEntry } from './core/parser.js';
3
+ import { syncCheck as _syncCheck } from './core/sync.js';
4
+ export { _syncCheck as syncCheck };
5
+ export type { SyncResult } from './core/sync.js';
6
+ export { validate, inferRules } from './core/validator.js';
7
+ export type { ValidationRule, ValidationError, EnvType } from './core/validator.js';
8
+ export { checkLeaks, installPreCommitHook } from './core/leak.js';
9
+ export type { LeakResult, LeakEntry } from './core/leak.js';
10
+ export { generateExample, ensureGitignore } from './core/generator.js';
11
+
12
+ // Convenience: guard fonksiyonu — tek çağrıda her şeyi kontrol et
13
+ export function guard(options?: {
14
+ envPath?: string;
15
+ examplePath?: string;
16
+ exitOnError?: boolean;
17
+ }): { ok: boolean; errors: string[] } {
18
+ const envPath = options?.envPath ?? '.env';
19
+ const examplePath = options?.examplePath ?? '.env.example';
20
+ const exitOnError = options?.exitOnError ?? true;
21
+ const errors: string[] = [];
22
+
23
+ try {
24
+ const sync = _syncCheck(envPath, examplePath);
25
+ if (sync.missing.length > 0) {
26
+ errors.push(`Missing variables: ${sync.missing.join(', ')}`);
27
+ }
28
+ } catch (err: any) {
29
+ if (!err.message.includes('not found')) {
30
+ errors.push(err.message);
31
+ }
32
+ }
33
+
34
+ if (errors.length > 0 && exitOnError) {
35
+ console.error('\n dotenv-guard errors:\n');
36
+ errors.forEach(e => console.error(` ✖ ${e}`));
37
+ console.error('');
38
+ process.exit(1);
39
+ }
40
+
41
+ return { ok: errors.length === 0, errors };
42
+ }
@@ -0,0 +1,13 @@
1
+ // chalk kullanmıyoruz — sıfır dependency
2
+ const isColorSupported = process.env.NO_COLOR === undefined && process.stdout.isTTY;
3
+
4
+ const wrap = (code: number, resetCode: number) => (str: string) =>
5
+ isColorSupported ? `\x1b[${code}m${str}\x1b[${resetCode}m` : str;
6
+
7
+ export const red = wrap(31, 39);
8
+ export const green = wrap(32, 39);
9
+ export const yellow = wrap(33, 39);
10
+ export const blue = wrap(34, 39);
11
+ export const gray = wrap(90, 39);
12
+ export const bold = wrap(1, 22);
13
+ export const dim = wrap(2, 22);
@@ -0,0 +1,20 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { resolve } from 'path';
3
+
4
+ export function resolveFilePath(filePath: string, cwd?: string): string {
5
+ if (filePath.startsWith('/') || filePath.startsWith('\\')) return filePath;
6
+ return resolve(cwd || process.cwd(), filePath);
7
+ }
8
+
9
+ export function readFileSafe(filePath: string): string | null {
10
+ try {
11
+ if (!existsSync(filePath)) return null;
12
+ return readFileSync(filePath, 'utf-8');
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ export function pluralize(count: number, singular: string, plural?: string): string {
19
+ return count === 1 ? singular : (plural || singular + 's');
20
+ }