@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.
- package/LICENSE +21 -0
- package/README.md +444 -0
- package/package.json +39 -0
- package/src/cli.js +118 -0
- package/src/commands/check.js +63 -0
- package/src/commands/diff.js +59 -0
- package/src/commands/docs.js +45 -0
- package/src/commands/init.js +101 -0
- package/src/commands/validate.js +72 -0
- package/src/core/diff.js +111 -0
- package/src/core/docs.js +120 -0
- package/src/core/index.js +22 -0
- package/src/core/schema.js +74 -0
- package/src/core/security.js +128 -0
- package/src/core/validator.js +170 -0
- package/src/index.js +9 -0
- package/src/utils/file.js +102 -0
- package/src/utils/format.js +68 -0
|
@@ -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 };
|
package/src/core/diff.js
ADDED
|
@@ -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 };
|
package/src/core/docs.js
ADDED
|
@@ -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 };
|