@danielszlaski/envguard 0.1.0 → 0.1.2
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 +28 -27
- package/dist/cli.js +4 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/fix.d.ts.map +1 -1
- package/dist/commands/fix.js +17 -13
- package/dist/commands/fix.js.map +1 -1
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/scan.js +56 -46
- package/dist/commands/scan.js.map +1 -1
- package/dist/utils/logger.d.ts +71 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +136 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +4 -3
- package/src/commands/fix.ts +18 -10
- package/src/commands/scan.ts +57 -43
- package/src/utils/logger.ts +146 -0
- package/test-project/src/lambda1/.env.example +2 -2
- package/test-project/src/lambda2/.env.example +6 -2
- package/test-project/src/payment/.env.example +5 -5
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Logger = void 0;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
/**
|
|
9
|
+
* Minimalist red-themed logger inspired by Serverless Framework v4
|
|
10
|
+
* Uses minimal colors and simple symbols for a clean, professional look
|
|
11
|
+
*/
|
|
12
|
+
class Logger {
|
|
13
|
+
/**
|
|
14
|
+
* Start an animated spinner with a message
|
|
15
|
+
*/
|
|
16
|
+
static startSpinner(message) {
|
|
17
|
+
this.spinnerMessage = message;
|
|
18
|
+
this.currentFrame = 0;
|
|
19
|
+
if (this.spinnerInterval) {
|
|
20
|
+
this.stopSpinner();
|
|
21
|
+
}
|
|
22
|
+
process.stdout.write('\n');
|
|
23
|
+
this.spinnerInterval = setInterval(() => {
|
|
24
|
+
const frame = this.spinnerFrames[this.currentFrame];
|
|
25
|
+
process.stdout.write(`\r${chalk_1.default.dim(frame)} ${this.spinnerMessage}`);
|
|
26
|
+
this.currentFrame = (this.currentFrame + 1) % this.spinnerFrames.length;
|
|
27
|
+
}, 80);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Stop the spinner and clear the line
|
|
31
|
+
*/
|
|
32
|
+
static stopSpinner(finalMessage) {
|
|
33
|
+
if (this.spinnerInterval) {
|
|
34
|
+
clearInterval(this.spinnerInterval);
|
|
35
|
+
this.spinnerInterval = null;
|
|
36
|
+
}
|
|
37
|
+
if (finalMessage) {
|
|
38
|
+
process.stdout.write(`\r${finalMessage}\n`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
process.stdout.write('\r\x1b[K'); // Clear line
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Log a header/section message (minimalist, no icons)
|
|
46
|
+
*/
|
|
47
|
+
static header(message) {
|
|
48
|
+
console.log(chalk_1.default.dim(message));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Log a success message with checkmark
|
|
52
|
+
*/
|
|
53
|
+
static success(message, indent = false) {
|
|
54
|
+
const prefix = indent ? ' ' : '';
|
|
55
|
+
console.log(`${prefix}${chalk_1.default.dim('✔')} ${message}`);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Log an error message (red themed)
|
|
59
|
+
*/
|
|
60
|
+
static error(message, indent = false) {
|
|
61
|
+
const prefix = indent ? ' ' : '';
|
|
62
|
+
console.log(`${prefix}${chalk_1.default.red('✖')} ${chalk_1.default.red(message)}`);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Log a warning message (yellow/amber)
|
|
66
|
+
*/
|
|
67
|
+
static warning(message, indent = false) {
|
|
68
|
+
const prefix = indent ? ' ' : '';
|
|
69
|
+
console.log(`${prefix}${chalk_1.default.hex('#FFA500')('⚠')} ${chalk_1.default.hex('#FFA500')(message)}`);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Log an info message (very minimal)
|
|
73
|
+
*/
|
|
74
|
+
static info(message, indent = false) {
|
|
75
|
+
const prefix = indent ? ' ' : '';
|
|
76
|
+
console.log(`${prefix}${chalk_1.default.dim(message)}`);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Log a list item (error themed in red)
|
|
80
|
+
*/
|
|
81
|
+
static errorItem(message, indent = 1) {
|
|
82
|
+
const prefix = ' '.repeat(indent);
|
|
83
|
+
console.log(`${prefix}${chalk_1.default.red('•')} ${chalk_1.default.red(message)}`);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Log a list item (warning themed, yellow/amber)
|
|
87
|
+
*/
|
|
88
|
+
static warningItem(message, indent = 1) {
|
|
89
|
+
const prefix = ' '.repeat(indent);
|
|
90
|
+
console.log(`${prefix}${chalk_1.default.hex('#FFA500')('•')} ${chalk_1.default.hex('#FFA500')(message)}`);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Log a list item (info themed, very dim)
|
|
94
|
+
*/
|
|
95
|
+
static infoItem(message, indent = 1) {
|
|
96
|
+
const prefix = ' '.repeat(indent);
|
|
97
|
+
console.log(`${prefix}${chalk_1.default.dim('•')} ${chalk_1.default.dim(message)}`);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Log a divider
|
|
101
|
+
*/
|
|
102
|
+
static divider() {
|
|
103
|
+
console.log(chalk_1.default.dim('─'.repeat(50)));
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Log a blank line
|
|
107
|
+
*/
|
|
108
|
+
static blank() {
|
|
109
|
+
console.log();
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Log a final summary message (Serverless-style)
|
|
113
|
+
*/
|
|
114
|
+
static summary(message) {
|
|
115
|
+
console.log(`\n${chalk_1.default.dim('✔')} ${message}\n`);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Log a path/file reference (dimmed)
|
|
119
|
+
*/
|
|
120
|
+
static path(message, indent = false) {
|
|
121
|
+
const prefix = indent ? ' ' : '';
|
|
122
|
+
console.log(`${prefix}${chalk_1.default.dim(message)}`);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Log deployment-style message (like "Deploying to stage dev")
|
|
126
|
+
*/
|
|
127
|
+
static deployment(message) {
|
|
128
|
+
console.log(`\n${message}\n`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
exports.Logger = Logger;
|
|
132
|
+
Logger.spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
133
|
+
Logger.spinnerInterval = null;
|
|
134
|
+
Logger.currentFrame = 0;
|
|
135
|
+
Logger.spinnerMessage = '';
|
|
136
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { Command } from 'commander';
|
|
|
4
4
|
import { scanCommand } from './commands/scan';
|
|
5
5
|
import { fixCommand } from './commands/fix';
|
|
6
6
|
import { checkCommand } from './commands/check';
|
|
7
|
+
import { Logger } from './utils/logger';
|
|
7
8
|
|
|
8
9
|
const program = new Command();
|
|
9
10
|
|
|
@@ -21,7 +22,7 @@ program
|
|
|
21
22
|
try {
|
|
22
23
|
await scanCommand(options);
|
|
23
24
|
} catch (error) {
|
|
24
|
-
|
|
25
|
+
Logger.error(`${error}`);
|
|
25
26
|
process.exit(1);
|
|
26
27
|
}
|
|
27
28
|
});
|
|
@@ -33,7 +34,7 @@ program
|
|
|
33
34
|
try {
|
|
34
35
|
await fixCommand();
|
|
35
36
|
} catch (error) {
|
|
36
|
-
|
|
37
|
+
Logger.error(`${error}`);
|
|
37
38
|
process.exit(1);
|
|
38
39
|
}
|
|
39
40
|
});
|
|
@@ -46,7 +47,7 @@ program
|
|
|
46
47
|
try {
|
|
47
48
|
await scanCommand({ ci: true, strict: options.strict });
|
|
48
49
|
} catch (error) {
|
|
49
|
-
|
|
50
|
+
Logger.error(`${error}`);
|
|
50
51
|
process.exit(1);
|
|
51
52
|
}
|
|
52
53
|
});
|
package/src/commands/fix.ts
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
3
|
import { CodeScanner } from '../scanner/codeScanner';
|
|
5
4
|
import { EnvParser } from '../parser/envParser';
|
|
6
5
|
import { EnvAnalyzer } from '../analyzer/envAnalyzer';
|
|
6
|
+
import { Logger } from '../utils/logger';
|
|
7
7
|
|
|
8
8
|
export async function fixCommand() {
|
|
9
9
|
const rootDir = process.cwd();
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Logger.startSpinner('Generating .env.example files...');
|
|
12
12
|
|
|
13
13
|
// Step 1: Find all .env files
|
|
14
14
|
const scanner = new CodeScanner(rootDir);
|
|
15
15
|
const envFiles = await scanner.findEnvFiles();
|
|
16
16
|
|
|
17
|
+
Logger.stopSpinner();
|
|
18
|
+
|
|
17
19
|
if (envFiles.length === 0) {
|
|
18
|
-
|
|
20
|
+
Logger.warning('No .env files found in the project');
|
|
21
|
+
Logger.blank();
|
|
19
22
|
return { success: false };
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
Logger.success(`Found ${envFiles.length} .env file(s)`);
|
|
26
|
+
Logger.blank();
|
|
23
27
|
|
|
24
28
|
const parser = new EnvParser();
|
|
25
29
|
const analyzer = new EnvAnalyzer();
|
|
@@ -31,17 +35,19 @@ export async function fixCommand() {
|
|
|
31
35
|
const relativePath = path.relative(rootDir, envDir);
|
|
32
36
|
const displayPath = relativePath || '.';
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
Logger.path(`Processing ${displayPath}/`);
|
|
35
39
|
|
|
36
40
|
// Step 3: Scan code files in this directory and subdirectories
|
|
37
41
|
const usedVars = await scanDirectoryForVars(rootDir, envDir, scanner);
|
|
38
42
|
|
|
39
43
|
if (usedVars.size === 0) {
|
|
40
|
-
|
|
44
|
+
Logger.info('No environment variables found in code', true);
|
|
45
|
+
Logger.blank();
|
|
41
46
|
continue;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
Logger.success(`Found ${usedVars.size} variable(s) used in this directory`, true);
|
|
50
|
+
Logger.blank();
|
|
45
51
|
|
|
46
52
|
// Step 4: Parse existing .env.example to preserve comments
|
|
47
53
|
const examplePath = path.join(envDir, '.env.example');
|
|
@@ -53,7 +59,8 @@ export async function fixCommand() {
|
|
|
53
59
|
// Step 6: Write to .env.example
|
|
54
60
|
fs.writeFileSync(examplePath, newContent, 'utf-8');
|
|
55
61
|
|
|
56
|
-
|
|
62
|
+
Logger.success(`Generated ${path.relative(rootDir, examplePath)}`, true);
|
|
63
|
+
Logger.blank();
|
|
57
64
|
|
|
58
65
|
totalVars += usedVars.size;
|
|
59
66
|
|
|
@@ -62,11 +69,12 @@ export async function fixCommand() {
|
|
|
62
69
|
const missingFromEnv = Array.from(usedVars.keys()).filter(v => !definedVars.has(v));
|
|
63
70
|
|
|
64
71
|
if (missingFromEnv.length > 0) {
|
|
65
|
-
|
|
72
|
+
Logger.warning(`Missing from .env: ${missingFromEnv.join(', ')}`, true);
|
|
73
|
+
Logger.blank();
|
|
66
74
|
}
|
|
67
75
|
}
|
|
68
76
|
|
|
69
|
-
|
|
77
|
+
Logger.summary(`Generated ${envFiles.length} .env.example file(s) with ${totalVars} total variables`);
|
|
70
78
|
|
|
71
79
|
return { success: true };
|
|
72
80
|
}
|
package/src/commands/scan.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
3
|
import { glob } from 'glob';
|
|
5
4
|
import { CodeScanner } from '../scanner/codeScanner';
|
|
6
5
|
import { EnvParser, EnvEntry } from '../parser/envParser';
|
|
@@ -9,6 +8,7 @@ import { EnvAnalyzer } from '../analyzer/envAnalyzer';
|
|
|
9
8
|
import { Issue } from '../types';
|
|
10
9
|
import { isKnownRuntimeVar, getRuntimeVarCategory } from '../constants/knownEnvVars';
|
|
11
10
|
import { ConfigLoader } from '../config/configLoader';
|
|
11
|
+
import { Logger } from '../utils/logger';
|
|
12
12
|
|
|
13
13
|
export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
|
|
14
14
|
const rootDir = process.cwd();
|
|
@@ -19,19 +19,23 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
|
|
|
19
19
|
// CLI options override config file
|
|
20
20
|
const strictMode = options.strict !== undefined ? options.strict : config.strict;
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
Logger.startSpinner('Scanning codebase for environment variables...');
|
|
23
23
|
|
|
24
24
|
// Step 1: Find all .env files and serverless.yml files
|
|
25
25
|
const scanner = new CodeScanner(rootDir);
|
|
26
26
|
const envFiles = await scanner.findEnvFiles();
|
|
27
27
|
const serverlessFiles = await scanner.findServerlessFiles();
|
|
28
28
|
|
|
29
|
+
Logger.stopSpinner();
|
|
30
|
+
|
|
29
31
|
if (envFiles.length === 0 && serverlessFiles.length === 0) {
|
|
30
|
-
|
|
32
|
+
Logger.warning('No .env or serverless.yml files found in the project');
|
|
33
|
+
Logger.blank();
|
|
31
34
|
return { success: false, issues: [] };
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
Logger.success(`Found ${envFiles.length} .env file(s) and ${serverlessFiles.length} serverless.yml file(s)`);
|
|
38
|
+
Logger.blank();
|
|
35
39
|
|
|
36
40
|
const parser = new EnvParser();
|
|
37
41
|
const serverlessParser = new ServerlessParser();
|
|
@@ -43,15 +47,17 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
|
|
|
43
47
|
const serverlessDir = path.dirname(serverlessFilePath);
|
|
44
48
|
const relativePath = path.relative(rootDir, serverlessFilePath);
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
Logger.path(`Checking ${relativePath}`);
|
|
51
|
+
Logger.blank();
|
|
47
52
|
|
|
48
53
|
// Parse serverless.yml
|
|
49
54
|
const serverlessVars = serverlessParser.parse(serverlessFilePath);
|
|
50
|
-
|
|
55
|
+
Logger.info(`Found ${serverlessVars.size} variable(s) in serverless.yml`, true);
|
|
51
56
|
|
|
52
57
|
// Scan code files in this directory to see what's actually used
|
|
53
58
|
const usedVars = await scanDirectoryForCodeVars(rootDir, serverlessDir, scanner);
|
|
54
|
-
|
|
59
|
+
Logger.info(`Found ${usedVars.size} variable(s) used in code`, true);
|
|
60
|
+
Logger.blank();
|
|
55
61
|
|
|
56
62
|
// Check for unused variables in serverless.yml
|
|
57
63
|
const unusedServerlessVars: string[] = [];
|
|
@@ -87,24 +93,24 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
|
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
if (unusedServerlessVars.length > 0) {
|
|
90
|
-
|
|
91
|
-
unusedServerlessVars.forEach((varName
|
|
92
|
-
|
|
96
|
+
Logger.warning('Unused variables in serverless.yml:', true);
|
|
97
|
+
unusedServerlessVars.forEach((varName) => {
|
|
98
|
+
Logger.warningItem(varName, 2);
|
|
93
99
|
allIssues.push({
|
|
94
100
|
type: 'unused',
|
|
95
101
|
varName,
|
|
96
102
|
details: `Defined in serverless.yml but never used in code`,
|
|
97
103
|
});
|
|
98
104
|
});
|
|
99
|
-
|
|
105
|
+
Logger.blank();
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
if (missingFromServerless.length > 0) {
|
|
103
|
-
|
|
104
|
-
missingFromServerless.forEach((item
|
|
105
|
-
|
|
109
|
+
Logger.error('Missing from serverless.yml:', true);
|
|
110
|
+
missingFromServerless.forEach((item) => {
|
|
111
|
+
Logger.errorItem(item.varName, 2);
|
|
106
112
|
if (item.locations && item.locations.length > 0) {
|
|
107
|
-
|
|
113
|
+
Logger.info(`Used in: ${item.locations.slice(0, 2).join(', ')}`, true);
|
|
108
114
|
}
|
|
109
115
|
allIssues.push({
|
|
110
116
|
type: 'missing',
|
|
@@ -113,16 +119,17 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
|
|
|
113
119
|
locations: item.locations,
|
|
114
120
|
});
|
|
115
121
|
});
|
|
116
|
-
|
|
122
|
+
Logger.blank();
|
|
117
123
|
}
|
|
118
124
|
|
|
119
125
|
if (unusedServerlessVars.length === 0 && missingFromServerless.length === 0) {
|
|
120
|
-
|
|
126
|
+
Logger.success('No issues in this serverless.yml', true);
|
|
127
|
+
Logger.blank();
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
// Show skipped runtime variables in non-strict mode
|
|
124
131
|
if (!strictMode && skippedRuntimeVars.length > 0) {
|
|
125
|
-
|
|
132
|
+
Logger.info('Skipped known runtime variables (use --strict to show):', true);
|
|
126
133
|
// Group by category
|
|
127
134
|
const grouped = new Map<string, string[]>();
|
|
128
135
|
for (const { varName, category } of skippedRuntimeVars) {
|
|
@@ -132,9 +139,9 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
|
|
|
132
139
|
grouped.get(category)!.push(varName);
|
|
133
140
|
}
|
|
134
141
|
for (const [category, vars] of grouped.entries()) {
|
|
135
|
-
|
|
142
|
+
Logger.info(`${category}: ${vars.join(', ')}`, true);
|
|
136
143
|
}
|
|
137
|
-
|
|
144
|
+
Logger.blank();
|
|
138
145
|
}
|
|
139
146
|
}
|
|
140
147
|
|
|
@@ -144,21 +151,23 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
|
|
|
144
151
|
const relativePath = path.relative(rootDir, envDir);
|
|
145
152
|
const displayPath = relativePath || '.';
|
|
146
153
|
|
|
147
|
-
|
|
154
|
+
Logger.path(`Checking ${displayPath}/`);
|
|
155
|
+
Logger.blank();
|
|
148
156
|
|
|
149
157
|
// Step 3: Scan code files in this directory and subdirectories
|
|
150
158
|
const usedVars = await scanDirectoryForVars(rootDir, envDir, scanner);
|
|
151
159
|
|
|
152
|
-
|
|
160
|
+
Logger.info(`Found ${usedVars.size} variable(s) used in this scope`, true);
|
|
153
161
|
|
|
154
162
|
// Step 4: Parse .env file
|
|
155
163
|
const definedVars = parser.parse(envFilePath);
|
|
156
|
-
|
|
164
|
+
Logger.info(`Found ${definedVars.size} variable(s) in .env`, true);
|
|
157
165
|
|
|
158
166
|
// Step 5: Parse .env.example
|
|
159
167
|
const examplePath = path.join(envDir, '.env.example');
|
|
160
168
|
const exampleVars = parser.parseExample(examplePath);
|
|
161
|
-
|
|
169
|
+
Logger.info(`Found ${exampleVars.size} variable(s) in .env.example`, true);
|
|
170
|
+
Logger.blank();
|
|
162
171
|
|
|
163
172
|
// Step 6: Analyze and find issues
|
|
164
173
|
const result = analyzer.analyze(usedVars, definedVars, exampleVars);
|
|
@@ -170,55 +179,60 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
|
|
|
170
179
|
const undocumentedIssues = result.issues.filter(i => i.type === 'undocumented');
|
|
171
180
|
|
|
172
181
|
if (missingIssues.length > 0) {
|
|
173
|
-
|
|
174
|
-
missingIssues.forEach((issue
|
|
175
|
-
|
|
182
|
+
Logger.error('Missing from .env:', true);
|
|
183
|
+
missingIssues.forEach((issue) => {
|
|
184
|
+
Logger.errorItem(issue.varName, 2);
|
|
176
185
|
if (issue.locations && issue.locations.length > 0) {
|
|
177
|
-
|
|
186
|
+
Logger.info(`Used in: ${issue.locations.slice(0, 2).join(', ')}`, true);
|
|
178
187
|
}
|
|
179
188
|
});
|
|
180
|
-
|
|
189
|
+
Logger.blank();
|
|
181
190
|
}
|
|
182
191
|
|
|
183
192
|
if (unusedIssues.length > 0) {
|
|
184
|
-
|
|
185
|
-
unusedIssues.forEach((issue
|
|
186
|
-
|
|
193
|
+
Logger.warning('Unused variables:', true);
|
|
194
|
+
unusedIssues.forEach((issue) => {
|
|
195
|
+
Logger.warningItem(issue.varName, 2);
|
|
187
196
|
});
|
|
188
|
-
|
|
197
|
+
Logger.blank();
|
|
189
198
|
}
|
|
190
199
|
|
|
191
200
|
if (undocumentedIssues.length > 0) {
|
|
192
|
-
|
|
193
|
-
undocumentedIssues.forEach((issue
|
|
194
|
-
|
|
201
|
+
Logger.info('Missing from .env.example:', true);
|
|
202
|
+
undocumentedIssues.forEach((issue) => {
|
|
203
|
+
Logger.infoItem(issue.varName, 2);
|
|
195
204
|
});
|
|
196
|
-
|
|
205
|
+
Logger.blank();
|
|
197
206
|
}
|
|
198
207
|
|
|
199
208
|
allIssues.push(...result.issues);
|
|
200
209
|
} else {
|
|
201
|
-
|
|
210
|
+
Logger.success('No issues in this directory', true);
|
|
211
|
+
Logger.blank();
|
|
202
212
|
}
|
|
203
213
|
}
|
|
204
214
|
|
|
205
215
|
// Display summary
|
|
206
|
-
|
|
216
|
+
Logger.divider();
|
|
207
217
|
if (allIssues.length === 0) {
|
|
208
|
-
|
|
218
|
+
Logger.summary('No issues found! All environment variables are in sync.');
|
|
209
219
|
return { success: true, issues: [] };
|
|
210
220
|
}
|
|
211
221
|
|
|
212
|
-
|
|
222
|
+
Logger.blank();
|
|
223
|
+
Logger.warning(`Total: ${allIssues.length} issue(s) across ${envFiles.length} location(s)`);
|
|
224
|
+
Logger.blank();
|
|
213
225
|
|
|
214
226
|
// Suggest fix
|
|
215
227
|
if (!options.ci) {
|
|
216
|
-
|
|
228
|
+
Logger.info('Run `envguard fix` to auto-generate .env.example files');
|
|
229
|
+
Logger.blank();
|
|
217
230
|
}
|
|
218
231
|
|
|
219
232
|
// Exit with error code in CI mode
|
|
220
233
|
if (options.ci) {
|
|
221
|
-
|
|
234
|
+
Logger.error('Issues found. Exiting with error code 1.');
|
|
235
|
+
Logger.blank();
|
|
222
236
|
process.exit(1);
|
|
223
237
|
}
|
|
224
238
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimalist red-themed logger inspired by Serverless Framework v4
|
|
5
|
+
* Uses minimal colors and simple symbols for a clean, professional look
|
|
6
|
+
*/
|
|
7
|
+
export class Logger {
|
|
8
|
+
private static spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
9
|
+
private static spinnerInterval: NodeJS.Timeout | null = null;
|
|
10
|
+
private static currentFrame = 0;
|
|
11
|
+
private static spinnerMessage = '';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Start an animated spinner with a message
|
|
15
|
+
*/
|
|
16
|
+
static startSpinner(message: string) {
|
|
17
|
+
this.spinnerMessage = message;
|
|
18
|
+
this.currentFrame = 0;
|
|
19
|
+
|
|
20
|
+
if (this.spinnerInterval) {
|
|
21
|
+
this.stopSpinner();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
process.stdout.write('\n');
|
|
25
|
+
this.spinnerInterval = setInterval(() => {
|
|
26
|
+
const frame = this.spinnerFrames[this.currentFrame];
|
|
27
|
+
process.stdout.write(`\r${chalk.dim(frame)} ${this.spinnerMessage}`);
|
|
28
|
+
this.currentFrame = (this.currentFrame + 1) % this.spinnerFrames.length;
|
|
29
|
+
}, 80);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Stop the spinner and clear the line
|
|
34
|
+
*/
|
|
35
|
+
static stopSpinner(finalMessage?: string) {
|
|
36
|
+
if (this.spinnerInterval) {
|
|
37
|
+
clearInterval(this.spinnerInterval);
|
|
38
|
+
this.spinnerInterval = null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (finalMessage) {
|
|
42
|
+
process.stdout.write(`\r${finalMessage}\n`);
|
|
43
|
+
} else {
|
|
44
|
+
process.stdout.write('\r\x1b[K'); // Clear line
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Log a header/section message (minimalist, no icons)
|
|
50
|
+
*/
|
|
51
|
+
static header(message: string) {
|
|
52
|
+
console.log(chalk.dim(message));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Log a success message with checkmark
|
|
57
|
+
*/
|
|
58
|
+
static success(message: string, indent = false) {
|
|
59
|
+
const prefix = indent ? ' ' : '';
|
|
60
|
+
console.log(`${prefix}${chalk.dim('✔')} ${message}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Log an error message (red themed)
|
|
65
|
+
*/
|
|
66
|
+
static error(message: string, indent = false) {
|
|
67
|
+
const prefix = indent ? ' ' : '';
|
|
68
|
+
console.log(`${prefix}${chalk.red('✖')} ${chalk.red(message)}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Log a warning message (yellow/amber)
|
|
73
|
+
*/
|
|
74
|
+
static warning(message: string, indent = false) {
|
|
75
|
+
const prefix = indent ? ' ' : '';
|
|
76
|
+
console.log(`${prefix}${chalk.hex('#FFA500')('⚠')} ${chalk.hex('#FFA500')(message)}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Log an info message (very minimal)
|
|
81
|
+
*/
|
|
82
|
+
static info(message: string, indent = false) {
|
|
83
|
+
const prefix = indent ? ' ' : '';
|
|
84
|
+
console.log(`${prefix}${chalk.dim(message)}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Log a list item (error themed in red)
|
|
89
|
+
*/
|
|
90
|
+
static errorItem(message: string, indent = 1) {
|
|
91
|
+
const prefix = ' '.repeat(indent);
|
|
92
|
+
console.log(`${prefix}${chalk.red('•')} ${chalk.red(message)}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Log a list item (warning themed, yellow/amber)
|
|
97
|
+
*/
|
|
98
|
+
static warningItem(message: string, indent = 1) {
|
|
99
|
+
const prefix = ' '.repeat(indent);
|
|
100
|
+
console.log(`${prefix}${chalk.hex('#FFA500')('•')} ${chalk.hex('#FFA500')(message)}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Log a list item (info themed, very dim)
|
|
105
|
+
*/
|
|
106
|
+
static infoItem(message: string, indent = 1) {
|
|
107
|
+
const prefix = ' '.repeat(indent);
|
|
108
|
+
console.log(`${prefix}${chalk.dim('•')} ${chalk.dim(message)}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Log a divider
|
|
113
|
+
*/
|
|
114
|
+
static divider() {
|
|
115
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Log a blank line
|
|
120
|
+
*/
|
|
121
|
+
static blank() {
|
|
122
|
+
console.log();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Log a final summary message (Serverless-style)
|
|
127
|
+
*/
|
|
128
|
+
static summary(message: string) {
|
|
129
|
+
console.log(`\n${chalk.dim('✔')} ${message}\n`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Log a path/file reference (dimmed)
|
|
134
|
+
*/
|
|
135
|
+
static path(message: string, indent = false) {
|
|
136
|
+
const prefix = indent ? ' ' : '';
|
|
137
|
+
console.log(`${prefix}${chalk.dim(message)}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Log deployment-style message (like "Deploying to stage dev")
|
|
142
|
+
*/
|
|
143
|
+
static deployment(message: string) {
|
|
144
|
+
console.log(`\n${message}\n`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Auto-generated by envguard
|
|
2
2
|
# Do not put actual secrets in this file - use .env instead
|
|
3
3
|
|
|
4
|
-
# Used in:
|
|
4
|
+
# Used in: src/lambda1/handler.js
|
|
5
5
|
# Format: your-secret-here
|
|
6
6
|
CUSTOM_KEY=
|
|
7
7
|
|
|
8
|
-
# Used in:
|
|
8
|
+
# Used in: src/lambda1/handler.js
|
|
9
9
|
# Format: your-secret-here
|
|
10
10
|
STRIPE_SECRET_KEY=
|
|
11
11
|
|