@anhuijie/envguard 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.
@@ -0,0 +1,59 @@
1
+ /**
2
+ * envguard diff — compare environment files across environments
3
+ */
4
+
5
+ const { diffEnvs, formatDiffResult } = require('../core/diff');
6
+ const { printHeader, colorize } = require('../utils/format');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ function runDiff(options = {}) {
11
+ const cwd = options.cwd || process.cwd();
12
+
13
+ printHeader('EnvGuard — Environment Diff');
14
+
15
+ const fileA = options.fileA;
16
+ const fileB = options.fileB;
17
+
18
+ if (!fileA || !fileB) {
19
+ console.error('Usage: envguard diff <fileA> <fileB>');
20
+ console.error('Example: envguard diff .env.development .env.production');
21
+ process.exit(1);
22
+ }
23
+
24
+ const pathA = path.resolve(cwd, fileA);
25
+ const pathB = path.resolve(cwd, fileB);
26
+
27
+ if (!fs.existsSync(pathA)) {
28
+ console.error(`File not found: ${fileA}`);
29
+ process.exit(1);
30
+ }
31
+ if (!fs.existsSync(pathB)) {
32
+ console.error(`File not found: ${fileB}`);
33
+ process.exit(1);
34
+ }
35
+
36
+ // Parse both files
37
+ const { parseEnvFile } = require('../core/diff');
38
+ const envA = parseEnvFile(pathA);
39
+ const envB = parseEnvFile(pathB);
40
+
41
+ // Diff
42
+ const labelA = path.basename(fileA);
43
+ const labelB = path.basename(fileB);
44
+ const result = diffEnvs(envA, envB, labelA, labelB);
45
+
46
+ // Output
47
+ console.log(formatDiffResult(result, labelA, labelB));
48
+ console.log('');
49
+
50
+ if (result.summary.onlyInA > 0 || result.summary.onlyInB > 0 || result.summary.valueDiff > 0) {
51
+ console.log(colorize('⚠ Environments are not in sync', 'yellow'));
52
+ } else {
53
+ console.log(colorize('✓ Environments are in sync', 'green'));
54
+ }
55
+
56
+ return result;
57
+ }
58
+
59
+ module.exports = { runDiff };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * envguard docs — generate .env.example and documentation
3
+ */
4
+
5
+ const { parseSchema } = require('../core/schema');
6
+ const { writeDocs } = require('../core/docs');
7
+ const { findConfigFile } = require('../utils/file');
8
+ const { printHeader, colorize } = require('../utils/format');
9
+ const path = require('path');
10
+
11
+ function runDocs(options = {}) {
12
+ const cwd = options.cwd || process.cwd();
13
+
14
+ printHeader('EnvGuard — Generate Docs');
15
+
16
+ // Find config
17
+ const configPath = options.config || findConfigFile(cwd);
18
+ if (!configPath) {
19
+ console.error('No envguard.config.js found. Run "envguard init" to create one.');
20
+ process.exit(1);
21
+ }
22
+
23
+ // Parse schema
24
+ let config;
25
+ try {
26
+ config = parseSchema(configPath);
27
+ } catch (err) {
28
+ console.error(`Config error: ${err.message}`);
29
+ process.exit(1);
30
+ }
31
+
32
+ // Generate docs
33
+ const outputDir = options.output || cwd;
34
+ const projectName = options.projectName || path.basename(cwd);
35
+
36
+ const result = writeDocs(config.schema, outputDir, { projectName });
37
+
38
+ console.log(colorize(`✓ Generated: ${path.relative(cwd, result.examplePath)}`, 'green'));
39
+ console.log(colorize(`✓ Generated: ${path.relative(cwd, result.mdPath)}`, 'green'));
40
+ console.log('');
41
+
42
+ return result;
43
+ }
44
+
45
+ module.exports = { runDocs };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * envguard init — create envguard.config.js template
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { printHeader, colorize } = require('../utils/format');
8
+ const { loadEnvFile } = require('../utils/file');
9
+
10
+ const CONFIG_TEMPLATE = `/**
11
+ * EnvGuard Configuration
12
+ * @see https://github.com/AnhuiJie/envguard#configuration
13
+ */
14
+ module.exports = {
15
+ // Define your environment variable schema
16
+ schema: {
17
+ // ── Application ──────────────────────
18
+ NODE_ENV: {
19
+ required: true,
20
+ type: 'string',
21
+ enum: ['development', 'staging', 'production', 'test'],
22
+ description: 'Application environment',
23
+ },
24
+ PORT: {
25
+ required: false,
26
+ type: 'port',
27
+ default: '3000',
28
+ description: 'Server port',
29
+ },
30
+
31
+ // ── Database ─────────────────────────
32
+ DATABASE_URL: {
33
+ required: true,
34
+ type: 'url',
35
+ description: 'Database connection string',
36
+ },
37
+
38
+ // ── Authentication ───────────────────
39
+ JWT_SECRET: {
40
+ required: true,
41
+ type: 'string',
42
+ description: 'Secret key for JWT signing',
43
+ },
44
+
45
+ // ── External Services ────────────────
46
+ // API_KEY: {
47
+ // required: false,
48
+ // type: 'string',
49
+ // description: 'External API key',
50
+ // },
51
+ },
52
+
53
+ // Security scanning options
54
+ security: {
55
+ // Minimum severity to report: 'low' | 'medium' | 'high' | 'critical'
56
+ minSeverity: 'medium',
57
+ // Keys to ignore during security scan
58
+ ignoreKeys: [],
59
+ },
60
+
61
+ // Documentation generation options
62
+ docs: {
63
+ projectName: '{{PROJECT_NAME}}',
64
+ },
65
+ };
66
+ `;
67
+
68
+ function runInit(options = {}) {
69
+ const cwd = options.cwd || process.cwd();
70
+
71
+ printHeader('EnvGuard — Init');
72
+
73
+ const configPath = path.join(cwd, 'envguard.config.js');
74
+
75
+ if (fs.existsSync(configPath) && !options.force) {
76
+ console.log(colorize('envguard.config.js already exists. Use --force to overwrite.', 'yellow'));
77
+ return;
78
+ }
79
+
80
+ // Try to auto-detect variables from existing .env
81
+ const envObj = loadEnvFile(path.join(cwd, '.env'));
82
+ let content = CONFIG_TEMPLATE;
83
+
84
+ if (Object.keys(envObj).length > 0) {
85
+ console.log(colorize(`Detected ${Object.keys(envObj).length} variable(s) in .env`, 'cyan'));
86
+ console.log('Review and update the generated config as needed.\n');
87
+ }
88
+
89
+ content = content.replace('{{PROJECT_NAME}}', path.basename(cwd));
90
+
91
+ fs.writeFileSync(configPath, content, 'utf-8');
92
+ console.log(colorize(`✓ Created envguard.config.js`, 'green'));
93
+ console.log('');
94
+ console.log('Next steps:');
95
+ console.log(' 1. Edit envguard.config.js to define your schema');
96
+ console.log(' 2. Run: npx envguard validate');
97
+ console.log(' 3. Run: npx envguard check');
98
+ console.log(' 4. Run: npx envguard docs');
99
+ }
100
+
101
+ module.exports = { runInit };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * envguard validate — validate environment variables against schema
3
+ */
4
+
5
+ const { validateEnv } = require('../core/validator');
6
+ const { parseSchema } = require('../core/schema');
7
+ const { formatError, formatWarning, formatSuccess, printHeader, printSummary } = require('../utils/format');
8
+ const { findConfigFile, loadEnvFile } = require('../utils/file');
9
+ const path = require('path');
10
+
11
+ function runValidate(options = {}) {
12
+ const cwd = options.cwd || process.cwd();
13
+
14
+ printHeader('EnvGuard — Validate');
15
+
16
+ // Find config
17
+ const configPath = options.config || findConfigFile(cwd);
18
+ if (!configPath) {
19
+ console.error('No envguard.config.js found. Run "envguard init" to create one.');
20
+ process.exit(1);
21
+ }
22
+
23
+ console.log(`Using config: ${path.relative(cwd, configPath)}`);
24
+
25
+ // Parse schema
26
+ let config;
27
+ try {
28
+ config = parseSchema(configPath);
29
+ } catch (err) {
30
+ console.error(`Config error: ${err.message}`);
31
+ process.exit(1);
32
+ }
33
+
34
+ // Load env values
35
+ let envObj;
36
+ if (options.envFile) {
37
+ envObj = loadEnvFile(path.resolve(cwd, options.envFile));
38
+ } else {
39
+ // Merge process.env with .env file
40
+ envObj = { ...loadEnvFile(path.join(cwd, '.env')), ...process.env };
41
+ }
42
+
43
+ // Validate
44
+ const results = validateEnv(envObj, config.schema);
45
+
46
+ // Print results
47
+ for (const error of results.errors) {
48
+ console.log(formatError(error));
49
+ }
50
+ for (const warning of results.warnings) {
51
+ console.log(formatWarning(warning));
52
+ }
53
+
54
+ // Print checked variables that passed
55
+ const errorKeys = new Set(results.errors.map((e) => e.key));
56
+ const warningKeys = new Set(results.warnings.map((w) => w.key));
57
+ for (const key of Object.keys(config.schema)) {
58
+ if (!errorKeys.has(key) && !warningKeys.has(key)) {
59
+ console.log(formatSuccess(key));
60
+ }
61
+ }
62
+
63
+ printSummary(results);
64
+
65
+ if (!results.valid && !options.allowFailure) {
66
+ process.exit(1);
67
+ }
68
+
69
+ return results;
70
+ }
71
+
72
+ module.exports = { runValidate };
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Multi-environment config diff tool
3
+ * Compares .env files across environments (dev/staging/prod)
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ function parseEnvFile(filePath) {
10
+ if (!fs.existsSync(filePath)) {
11
+ throw new Error(`File not found: ${filePath}`);
12
+ }
13
+
14
+ const content = fs.readFileSync(filePath, 'utf-8');
15
+ const vars = {};
16
+ const lines = content.split('\n');
17
+
18
+ for (const line of lines) {
19
+ const trimmed = line.trim();
20
+ // Skip comments and empty lines
21
+ if (!trimmed || trimmed.startsWith('#')) continue;
22
+
23
+ const eqIndex = trimmed.indexOf('=');
24
+ if (eqIndex === -1) continue;
25
+
26
+ const key = trimmed.slice(0, eqIndex).trim();
27
+ let value = trimmed.slice(eqIndex + 1).trim();
28
+
29
+ // Remove surrounding quotes
30
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
31
+ value = value.slice(1, -1);
32
+ }
33
+
34
+ vars[key] = value;
35
+ }
36
+
37
+ return vars;
38
+ }
39
+
40
+ function diffEnvs(envA, envB, labelA = 'A', labelB = 'B') {
41
+ const keysA = new Set(Object.keys(envA));
42
+ const keysB = new Set(Object.keys(envB));
43
+
44
+ const onlyInA = [...keysA].filter((k) => !keysB.has(k));
45
+ const onlyInB = [...keysB].filter((k) => !keysA.has(k));
46
+ const common = [...keysA].filter((k) => keysB.has(k));
47
+
48
+ const valueDiff = common
49
+ .filter((k) => envA[k] !== envB[k])
50
+ .map((k) => ({
51
+ key: k,
52
+ [labelA]: envA[k],
53
+ [labelB]: envB[k],
54
+ }));
55
+
56
+ return {
57
+ onlyInA: onlyInA.map((k) => ({ key: k, value: envA[k] })),
58
+ onlyInB: onlyInB.map((k) => ({ key: k, value: envB[k] })),
59
+ valueDiff,
60
+ identical: common.filter((k) => envA[k] === envB[k]),
61
+ summary: {
62
+ totalA: keysA.size,
63
+ totalB: keysB.size,
64
+ onlyInA: onlyInA.length,
65
+ onlyInB: onlyInB.length,
66
+ valueDiff: valueDiff.length,
67
+ identical: common.filter((k) => envA[k] === envB[k]).length,
68
+ },
69
+ };
70
+ }
71
+
72
+ function formatDiffResult(result, labelA, labelB) {
73
+ const lines = [];
74
+
75
+ lines.push(`Environment Diff: ${labelA} ↔ ${labelB}`);
76
+ lines.push('═'.repeat(50));
77
+ lines.push('');
78
+
79
+ lines.push(`Summary: ${result.summary.totalA} vars in ${labelA}, ${result.summary.totalB} vars in ${labelB}`);
80
+ lines.push(` ${result.summary.identical} identical, ${result.summary.valueDiff} different`);
81
+ lines.push('');
82
+
83
+ if (result.onlyInA.length > 0) {
84
+ lines.push(`Only in ${labelA}:`);
85
+ for (const item of result.onlyInA) {
86
+ lines.push(` - ${item.key}=${item.value}`);
87
+ }
88
+ lines.push('');
89
+ }
90
+
91
+ if (result.onlyInB.length > 0) {
92
+ lines.push(`Only in ${labelB}:`);
93
+ for (const item of result.onlyInB) {
94
+ lines.push(` - ${item.key}=${item.value}`);
95
+ }
96
+ lines.push('');
97
+ }
98
+
99
+ if (result.valueDiff.length > 0) {
100
+ lines.push('Different values:');
101
+ for (const item of result.valueDiff) {
102
+ lines.push(` - ${item.key}:`);
103
+ lines.push(` ${labelA}: ${item[labelA]}`);
104
+ lines.push(` ${labelB}: ${item[labelB]}`);
105
+ }
106
+ }
107
+
108
+ return lines.join('\n');
109
+ }
110
+
111
+ module.exports = { parseEnvFile, diffEnvs, formatDiffResult };
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Documentation generator - creates .env.example and markdown docs from schema
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ function generateEnvExample(schema, options = {}) {
9
+ const lines = [
10
+ '# Environment Variables',
11
+ '# Generated by EnvGuard — do not edit manually',
12
+ '# Run: npx envguard docs to regenerate',
13
+ '',
14
+ ];
15
+
16
+ // Group by optional tags
17
+ const required = [];
18
+ const optional = [];
19
+
20
+ for (const [key, rule] of Object.entries(schema)) {
21
+ if (rule.required) {
22
+ required.push([key, rule]);
23
+ } else {
24
+ optional.push([key, rule]);
25
+ }
26
+ }
27
+
28
+ if (required.length > 0) {
29
+ lines.push('# ── Required ──────────────────────────');
30
+ for (const [key, rule] of required) {
31
+ lines.push(...formatVarLine(key, rule));
32
+ }
33
+ lines.push('');
34
+ }
35
+
36
+ if (optional.length > 0) {
37
+ lines.push('# ── Optional ──────────────────────────');
38
+ for (const [key, rule] of optional) {
39
+ lines.push(...formatVarLine(key, rule));
40
+ }
41
+ lines.push('');
42
+ }
43
+
44
+ return lines.join('\n');
45
+ }
46
+
47
+ function formatVarLine(key, rule) {
48
+ const lines = [];
49
+ const desc = rule.description || rule.desc || '';
50
+ const type = rule.type || 'string';
51
+
52
+ if (desc) {
53
+ lines.push(`# ${desc}`);
54
+ }
55
+
56
+ let typeInfo = `type: ${type}`;
57
+ if (rule.enum) typeInfo += `, allowed: ${rule.enum.join(' | ')}`;
58
+ if (rule.min !== undefined) typeInfo += `, min: ${rule.min}`;
59
+ if (rule.max !== undefined) typeInfo += `, max: ${rule.max}`;
60
+ lines.push(`# ${typeInfo}`);
61
+
62
+ if (rule.deprecated) {
63
+ lines.push(`# ⚠ DEPRECATED${rule.replacement ? ` — use ${rule.replacement} instead` : ''}`);
64
+ }
65
+
66
+ const defaultVal = rule.default !== undefined ? rule.default : '';
67
+ lines.push(`${key}=${defaultVal}`);
68
+ lines.push('');
69
+
70
+ return lines;
71
+ }
72
+
73
+ function generateMarkdownDoc(schema, options = {}) {
74
+ const projectName = options.projectName || 'Project';
75
+ const lines = [
76
+ `# ${projectName} — Environment Variables`,
77
+ '',
78
+ '> Auto-generated by [EnvGuard](https://github.com/AnhuiJie/envguard)',
79
+ '',
80
+ '| Variable | Required | Type | Default | Description |',
81
+ '|----------|----------|------|---------|-------------|',
82
+ ];
83
+
84
+ for (const [key, rule] of Object.entries(schema)) {
85
+ const required = rule.required ? 'Yes' : 'No';
86
+ const type = rule.type || 'string';
87
+ const defaultVal = rule.default !== undefined ? `\`${rule.default}\`` : '—';
88
+ let desc = rule.description || rule.desc || '';
89
+
90
+ if (rule.enum) desc += ` (allowed: ${rule.enum.map((v) => `\`${v}\``).join(', ')})`;
91
+ if (rule.deprecated) desc += ` ⚠ **Deprecated**${rule.replacement ? `, use \`${rule.replacement}\`` : ''}`;
92
+ if (rule.min !== undefined) desc += ` (min: ${rule.min})`;
93
+ if (rule.max !== undefined) desc += ` (max: ${rule.max})`;
94
+
95
+ lines.push(`| \`${key}\` | ${required} | ${type} | ${defaultVal} | ${desc} |`);
96
+ }
97
+
98
+ lines.push('');
99
+ return lines.join('\n');
100
+ }
101
+
102
+ function writeDocs(schema, outputDir, options = {}) {
103
+ if (!fs.existsSync(outputDir)) {
104
+ fs.mkdirSync(outputDir, { recursive: true });
105
+ }
106
+
107
+ // Write .env.example
108
+ const exampleContent = generateEnvExample(schema, options);
109
+ const examplePath = path.join(outputDir, '.env.example');
110
+ fs.writeFileSync(examplePath, exampleContent, 'utf-8');
111
+
112
+ // Write markdown doc
113
+ const mdContent = generateMarkdownDoc(schema, options);
114
+ const mdPath = path.join(outputDir, 'ENV.md');
115
+ fs.writeFileSync(mdPath, mdContent, 'utf-8');
116
+
117
+ return { examplePath, mdPath };
118
+ }
119
+
120
+ module.exports = { generateEnvExample, generateMarkdownDoc, writeDocs };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * EnvGuard core module — unified API
3
+ */
4
+
5
+ const { parseSchema, validateSchema } = require('./schema');
6
+ const { validateEnv } = require('./validator');
7
+ const { scanForSecrets } = require('./security');
8
+ const { generateEnvExample, generateMarkdownDoc, writeDocs } = require('./docs');
9
+ const { parseEnvFile, diffEnvs, formatDiffResult } = require('./diff');
10
+
11
+ module.exports = {
12
+ parseSchema,
13
+ validateSchema,
14
+ validateEnv,
15
+ scanForSecrets,
16
+ generateEnvExample,
17
+ generateMarkdownDoc,
18
+ writeDocs,
19
+ parseEnvFile,
20
+ diffEnvs,
21
+ formatDiffResult,
22
+ };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Schema definition and parsing for envguard.config.js
3
+ */
4
+
5
+ const SUPPORTED_TYPES = ['string', 'number', 'boolean', 'url', 'email', 'json', 'regex', 'port'];
6
+
7
+ function validateSchema(schema) {
8
+ if (!schema || typeof schema !== 'object') {
9
+ throw new Error('Schema must be a non-empty object');
10
+ }
11
+
12
+ const errors = [];
13
+
14
+ for (const [key, rule] of Object.entries(schema)) {
15
+ if (!rule || typeof rule !== 'object') {
16
+ errors.push(`Variable "${key}": rule must be an object`);
17
+ continue;
18
+ }
19
+
20
+ if (rule.type && !SUPPORTED_TYPES.includes(rule.type)) {
21
+ errors.push(`Variable "${key}": unsupported type "${rule.type}". Supported: ${SUPPORTED_TYPES.join(', ')}`);
22
+ }
23
+
24
+ if (rule.type === 'number' && rule.min !== undefined && rule.max !== undefined && rule.min > rule.max) {
25
+ errors.push(`Variable "${key}": min (${rule.min}) cannot be greater than max (${rule.max})`);
26
+ }
27
+
28
+ if (rule.enum && !Array.isArray(rule.enum)) {
29
+ errors.push(`Variable "${key}": enum must be an array`);
30
+ }
31
+ }
32
+
33
+ if (errors.length > 0) {
34
+ throw new Error(`Schema validation failed:\n ${errors.join('\n ')}`);
35
+ }
36
+
37
+ return true;
38
+ }
39
+
40
+ function parseSchema(configPath) {
41
+ try {
42
+ const fs = require('fs');
43
+ const path = require('path');
44
+
45
+ if (!fs.existsSync(configPath)) {
46
+ throw new Error(`Config file not found: ${configPath}`);
47
+ }
48
+
49
+ // Clear require cache for fresh reload
50
+ delete require.cache[require.resolve(path.resolve(configPath))];
51
+
52
+ const config = require(path.resolve(configPath));
53
+
54
+ if (!config.schema && !config.env) {
55
+ throw new Error('Config must export a "schema" or "env" object');
56
+ }
57
+
58
+ const schema = config.schema || config.env;
59
+ validateSchema(schema);
60
+
61
+ return {
62
+ schema,
63
+ security: config.security || {},
64
+ docs: config.docs || {},
65
+ };
66
+ } catch (err) {
67
+ if (err.code === 'MODULE_NOT_FOUND') {
68
+ throw new Error(`Cannot load config: ${err.message}`);
69
+ }
70
+ throw err;
71
+ }
72
+ }
73
+
74
+ module.exports = { validateSchema, parseSchema, SUPPORTED_TYPES };