envprobe 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 +316 -0
- package/bin/envcheck.js +68 -0
- package/package.json +49 -0
- package/src/analyzer.js +179 -0
- package/src/autocomplete.js +135 -0
- package/src/cache.js +114 -0
- package/src/cli.js +606 -0
- package/src/config.js +118 -0
- package/src/formatters/github.js +164 -0
- package/src/formatters/json.js +114 -0
- package/src/formatters/table.js +92 -0
- package/src/formatters/text.js +198 -0
- package/src/ignore.js +313 -0
- package/src/parser.js +119 -0
- package/src/plugins.js +138 -0
- package/src/progress.js +181 -0
- package/src/repl.js +416 -0
- package/src/scanner.js +182 -0
- package/src/scanners/go.js +89 -0
- package/src/scanners/javascript.js +93 -0
- package/src/scanners/python.js +97 -0
- package/src/scanners/ruby.js +90 -0
- package/src/scanners/rust.js +103 -0
- package/src/scanners/shell.js +125 -0
- package/src/security.js +411 -0
- package/src/suggestions.js +154 -0
- package/src/utils.js +57 -0
- package/src/watch.js +131 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { validateGlobPattern } from './ignore.js';
|
|
5
|
+
import { loadConfig, mergeConfig } from './config.js';
|
|
6
|
+
import { Spinner } from './progress.js';
|
|
7
|
+
import { generateSuggestions, findSimilarVariables } from './suggestions.js';
|
|
8
|
+
import { startWatchMode } from './watch.js';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse command-line arguments into a structured options object
|
|
15
|
+
* @param {string[]} args - Command-line arguments (typically process.argv.slice(2))
|
|
16
|
+
* @returns {CLIOptions} Parsed options object
|
|
17
|
+
* @throws {Error} If arguments are invalid
|
|
18
|
+
*/
|
|
19
|
+
export function parseArguments(args) {
|
|
20
|
+
const options = {
|
|
21
|
+
path: '.',
|
|
22
|
+
envFile: '.env.example',
|
|
23
|
+
format: 'text',
|
|
24
|
+
failOn: 'none',
|
|
25
|
+
ignore: [],
|
|
26
|
+
noColor: false,
|
|
27
|
+
quiet: false,
|
|
28
|
+
version: false,
|
|
29
|
+
help: false,
|
|
30
|
+
watch: false,
|
|
31
|
+
suggestions: true,
|
|
32
|
+
progress: true,
|
|
33
|
+
config: null,
|
|
34
|
+
fix: false,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
let i = 0;
|
|
38
|
+
while (i < args.length) {
|
|
39
|
+
const arg = args[i];
|
|
40
|
+
|
|
41
|
+
// Handle flags
|
|
42
|
+
if (arg === '--help' || arg === '-h') {
|
|
43
|
+
options.help = true;
|
|
44
|
+
i++;
|
|
45
|
+
} else if (arg === '--version' || arg === '-v') {
|
|
46
|
+
options.version = true;
|
|
47
|
+
i++;
|
|
48
|
+
} else if (arg === '--no-color') {
|
|
49
|
+
options.noColor = true;
|
|
50
|
+
i++;
|
|
51
|
+
} else if (arg === '--quiet' || arg === '-q') {
|
|
52
|
+
options.quiet = true;
|
|
53
|
+
i++;
|
|
54
|
+
} else if (arg === '--watch' || arg === '-w') {
|
|
55
|
+
options.watch = true;
|
|
56
|
+
i++;
|
|
57
|
+
} else if (arg === '--no-suggestions') {
|
|
58
|
+
options.suggestions = false;
|
|
59
|
+
i++;
|
|
60
|
+
} else if (arg === '--no-progress') {
|
|
61
|
+
options.progress = false;
|
|
62
|
+
i++;
|
|
63
|
+
} else if (arg === '--fix') {
|
|
64
|
+
options.fix = true;
|
|
65
|
+
i++;
|
|
66
|
+
} else if (arg === '--config' || arg === '-c') {
|
|
67
|
+
if (i + 1 >= args.length) {
|
|
68
|
+
throw new Error('--config requires a value');
|
|
69
|
+
}
|
|
70
|
+
options.config = args[i + 1];
|
|
71
|
+
i += 2;
|
|
72
|
+
} else if (arg === '--env-file') {
|
|
73
|
+
if (i + 1 >= args.length) {
|
|
74
|
+
throw new Error('--env-file requires a value');
|
|
75
|
+
}
|
|
76
|
+
options.envFile = args[i + 1];
|
|
77
|
+
i += 2;
|
|
78
|
+
} else if (arg === '--format' || arg === '-f') {
|
|
79
|
+
if (i + 1 >= args.length) {
|
|
80
|
+
throw new Error('--format requires a value');
|
|
81
|
+
}
|
|
82
|
+
options.format = args[i + 1];
|
|
83
|
+
i += 2;
|
|
84
|
+
} else if (arg === '--fail-on') {
|
|
85
|
+
if (i + 1 >= args.length) {
|
|
86
|
+
throw new Error('--fail-on requires a value');
|
|
87
|
+
}
|
|
88
|
+
options.failOn = args[i + 1];
|
|
89
|
+
i += 2;
|
|
90
|
+
} else if (arg === '--ignore' || arg === '-i') {
|
|
91
|
+
if (i + 1 >= args.length) {
|
|
92
|
+
throw new Error('--ignore requires a value');
|
|
93
|
+
}
|
|
94
|
+
options.ignore.push(args[i + 1]);
|
|
95
|
+
i += 2;
|
|
96
|
+
} else if (arg.startsWith('--')) {
|
|
97
|
+
throw new Error(`Unrecognized flag: ${arg}`);
|
|
98
|
+
} else if (arg.startsWith('-') && arg !== '-') {
|
|
99
|
+
throw new Error(`Unrecognized flag: ${arg}`);
|
|
100
|
+
} else {
|
|
101
|
+
// Positional argument (path)
|
|
102
|
+
options.path = arg;
|
|
103
|
+
i++;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Validate options
|
|
108
|
+
validateOptions(options);
|
|
109
|
+
|
|
110
|
+
return options;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Validate parsed CLI options
|
|
115
|
+
* @param {CLIOptions} options - Parsed options to validate
|
|
116
|
+
* @throws {Error} If options are invalid
|
|
117
|
+
*/
|
|
118
|
+
function validateOptions(options) {
|
|
119
|
+
// Skip validation if help or version flags are set
|
|
120
|
+
if (options.help || options.version) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Validate format
|
|
125
|
+
const validFormats = ['text', 'json', 'github'];
|
|
126
|
+
if (!validFormats.includes(options.format)) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Invalid --format value: "${options.format}". Must be one of: ${validFormats.join(', ')}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Validate failOn
|
|
133
|
+
const validFailOn = ['missing', 'unused', 'undocumented', 'all', 'none'];
|
|
134
|
+
if (!validFailOn.includes(options.failOn)) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Invalid --fail-on value: "${options.failOn}". Must be one of: ${validFailOn.join(', ')}`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Validate ignore patterns (basic check for invalid glob patterns)
|
|
141
|
+
for (const pattern of options.ignore) {
|
|
142
|
+
if (pattern.trim() === '') {
|
|
143
|
+
throw new Error('--ignore pattern cannot be empty');
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
validateGlobPattern(pattern);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
throw new Error(error.message.replace('Invalid glob pattern', 'Invalid --ignore pattern'));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Validate path is not empty
|
|
153
|
+
if (options.path.trim() === '') {
|
|
154
|
+
throw new Error('Path argument cannot be empty');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Validate envFile is not empty
|
|
158
|
+
if (options.envFile.trim() === '') {
|
|
159
|
+
throw new Error('--env-file path cannot be empty');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Display help message
|
|
165
|
+
*/
|
|
166
|
+
export function displayHelp() {
|
|
167
|
+
const helpText = `
|
|
168
|
+
envcheck - Validate environment variable usage across your codebase
|
|
169
|
+
|
|
170
|
+
USAGE:
|
|
171
|
+
envcheck [path] [options]
|
|
172
|
+
envcheck --repl Start interactive REPL mode
|
|
173
|
+
envcheck Start interactive REPL mode (no args)
|
|
174
|
+
|
|
175
|
+
ARGUMENTS:
|
|
176
|
+
path Directory or file to scan (default: ".")
|
|
177
|
+
|
|
178
|
+
OPTIONS:
|
|
179
|
+
--env-file <path> Path to .env.example file (default: ".env.example")
|
|
180
|
+
--format, -f <format> Output format: text, json, github (default: "text")
|
|
181
|
+
--fail-on <condition> Exit with code 1 if condition met:
|
|
182
|
+
missing, unused, undocumented, all, none (default: "none")
|
|
183
|
+
--ignore, -i <pattern> Glob pattern to ignore (repeatable)
|
|
184
|
+
--config, -c <path> Load configuration from file
|
|
185
|
+
--watch, -w Watch mode - rerun on file changes
|
|
186
|
+
--fix Auto-fix issues by updating .env.example
|
|
187
|
+
--no-color Disable colored output
|
|
188
|
+
--no-suggestions Disable intelligent suggestions
|
|
189
|
+
--no-progress Disable progress indicators
|
|
190
|
+
--quiet, -q Suppress output when no issues found
|
|
191
|
+
--repl, -r Start interactive REPL mode
|
|
192
|
+
--version, -v Display version number
|
|
193
|
+
--help, -h Display this help message
|
|
194
|
+
|
|
195
|
+
EXAMPLES:
|
|
196
|
+
envcheck .
|
|
197
|
+
envcheck ./src --env-file .env.production.example
|
|
198
|
+
envcheck . --format json --fail-on missing
|
|
199
|
+
envcheck . --ignore "**/*.test.js" --ignore "**/dist/**"
|
|
200
|
+
envcheck . --format github --fail-on all
|
|
201
|
+
envcheck . --watch
|
|
202
|
+
envcheck . --fix
|
|
203
|
+
envcheck . --config .envcheckrc.json
|
|
204
|
+
envcheck --repl
|
|
205
|
+
|
|
206
|
+
REPL MODE:
|
|
207
|
+
Start an interactive session with persistent configuration:
|
|
208
|
+
envcheck
|
|
209
|
+
envcheck --repl
|
|
210
|
+
|
|
211
|
+
REPL Commands:
|
|
212
|
+
:help Show REPL help
|
|
213
|
+
:config Show current configuration
|
|
214
|
+
:set <key> <value> Set configuration (path, format, etc.)
|
|
215
|
+
:history Show command history
|
|
216
|
+
:results Show previous results
|
|
217
|
+
:exit Exit REPL
|
|
218
|
+
|
|
219
|
+
REPL Examples:
|
|
220
|
+
:set path ./src
|
|
221
|
+
:set format json
|
|
222
|
+
. --fail-on missing
|
|
223
|
+
envcheck . --format github
|
|
224
|
+
|
|
225
|
+
EXIT CODES:
|
|
226
|
+
0 Success (no issues or issues don't match --fail-on)
|
|
227
|
+
1 Validation failed (issues match --fail-on condition)
|
|
228
|
+
2 Error (invalid arguments, file not found, etc.)
|
|
229
|
+
|
|
230
|
+
For more information, visit: https://github.com/yourusername/envcheck
|
|
231
|
+
`;
|
|
232
|
+
|
|
233
|
+
console.log(helpText.trim());
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Display version number
|
|
238
|
+
*/
|
|
239
|
+
export function displayVersion() {
|
|
240
|
+
try {
|
|
241
|
+
const packageJsonPath = join(__dirname, '..', 'package.json');
|
|
242
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
243
|
+
console.log(`envcheck v${packageJson.version}`);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
console.log('envcheck (version unknown)');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* @typedef {Object} CLIOptions
|
|
251
|
+
* @property {string} path - Directory or file to scan
|
|
252
|
+
* @property {string} envFile - Path to .env.example file
|
|
253
|
+
* @property {'text'|'json'|'github'} format - Output format
|
|
254
|
+
* @property {'missing'|'unused'|'undocumented'|'all'|'none'} failOn - Exit code condition
|
|
255
|
+
* @property {string[]} ignore - Glob patterns to ignore
|
|
256
|
+
* @property {boolean} noColor - Disable colored output
|
|
257
|
+
* @property {boolean} quiet - Suppress output when no issues
|
|
258
|
+
* @property {boolean} version - Display version
|
|
259
|
+
* @property {boolean} help - Display help
|
|
260
|
+
*/
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Determine exit code based on analysis results and --fail-on flag
|
|
264
|
+
*
|
|
265
|
+
* @param {{missing: Array, unused: Array, undocumented: Array}} result - Analysis result
|
|
266
|
+
* @param {'missing'|'unused'|'undocumented'|'all'|'none'} failOn - Exit code condition
|
|
267
|
+
* @returns {number} Exit code (0 = success, 1 = validation failed)
|
|
268
|
+
*
|
|
269
|
+
* Preconditions:
|
|
270
|
+
* - result is a valid AnalysisResult object
|
|
271
|
+
* - failOn is one of: 'missing', 'unused', 'undocumented', 'all', 'none'
|
|
272
|
+
*
|
|
273
|
+
* Postconditions:
|
|
274
|
+
* - Returns 0 if no issues match failOn criteria
|
|
275
|
+
* - Returns 1 if issues match failOn criteria
|
|
276
|
+
* - 'all' fails if any category has issues
|
|
277
|
+
* - 'none' always returns 0
|
|
278
|
+
*
|
|
279
|
+
* Requirements: 1.8.1-1.8.5
|
|
280
|
+
*/
|
|
281
|
+
export function determineExitCode(result, failOn) {
|
|
282
|
+
if (failOn === 'none') {
|
|
283
|
+
return 0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (failOn === 'all') {
|
|
287
|
+
return (result.missing.length > 0 || result.unused.length > 0 || result.undocumented.length > 0) ? 1 : 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (failOn === 'missing') {
|
|
291
|
+
return result.missing.length > 0 ? 1 : 0;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (failOn === 'unused') {
|
|
295
|
+
return result.unused.length > 0 ? 1 : 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (failOn === 'undocumented') {
|
|
299
|
+
return result.undocumented.length > 0 ? 1 : 0;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return 0;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Main CLI runner - orchestrates the entire workflow
|
|
307
|
+
*
|
|
308
|
+
* @param {string[]} args - Command-line arguments (typically process.argv.slice(2))
|
|
309
|
+
* @returns {Promise<number>} Exit code (0 = success, 1 = validation failed, 2 = error)
|
|
310
|
+
*
|
|
311
|
+
* Preconditions:
|
|
312
|
+
* - args is a valid array of strings
|
|
313
|
+
* - Node.js runtime is available with required modules
|
|
314
|
+
*
|
|
315
|
+
* Postconditions:
|
|
316
|
+
* - Returns exit code 0, 1, or 2
|
|
317
|
+
* - Output is written to stdout or stderr
|
|
318
|
+
* - No unhandled exceptions escape
|
|
319
|
+
*
|
|
320
|
+
* Requirements: 1.5.1-1.5.10, 1.8.1-1.8.5
|
|
321
|
+
*/
|
|
322
|
+
export async function run(args) {
|
|
323
|
+
let spinner = null;
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
// Step 1: Parse command-line arguments
|
|
327
|
+
const options = parseArguments(args);
|
|
328
|
+
|
|
329
|
+
// Handle special flags
|
|
330
|
+
if (options.help) {
|
|
331
|
+
displayHelp();
|
|
332
|
+
return 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (options.version) {
|
|
336
|
+
displayVersion();
|
|
337
|
+
return 0;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Load config file if specified or found
|
|
341
|
+
let fileConfig = null;
|
|
342
|
+
if (options.config) {
|
|
343
|
+
fileConfig = loadConfig(options.config);
|
|
344
|
+
} else {
|
|
345
|
+
fileConfig = loadConfig(options.path);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Merge config file with CLI options
|
|
349
|
+
const mergedOptions = mergeConfig(options, fileConfig);
|
|
350
|
+
|
|
351
|
+
// Watch mode
|
|
352
|
+
if (mergedOptions.watch) {
|
|
353
|
+
const runValidation = async () => {
|
|
354
|
+
await runOnce(mergedOptions);
|
|
355
|
+
};
|
|
356
|
+
await startWatchMode(mergedOptions.path, mergedOptions, runValidation);
|
|
357
|
+
return 0;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Run once
|
|
361
|
+
return await runOnce(mergedOptions);
|
|
362
|
+
|
|
363
|
+
} catch (error) {
|
|
364
|
+
if (spinner) spinner.fail();
|
|
365
|
+
// Handle errors gracefully with better messages
|
|
366
|
+
console.error(`\n❌ Error: ${error.message}`);
|
|
367
|
+
|
|
368
|
+
// Provide helpful hints for common errors
|
|
369
|
+
if (error.code === 'ENOENT') {
|
|
370
|
+
console.error(`\n💡 Tip: Check that the file or directory exists`);
|
|
371
|
+
} else if (error.message.includes('Invalid')) {
|
|
372
|
+
console.error(`\n💡 Tip: Run 'envcheck --help' to see valid options`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return 2;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Run validation once (used by both normal and watch mode)
|
|
381
|
+
*/
|
|
382
|
+
async function runOnce(options) {
|
|
383
|
+
let spinner = null;
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
// Step 2: Load ignore patterns
|
|
387
|
+
if (options.progress && !options.quiet) {
|
|
388
|
+
spinner = new Spinner('Loading configuration...');
|
|
389
|
+
spinner.start();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const { loadIgnorePatterns } = await import('./ignore.js');
|
|
393
|
+
const ignorePatterns = loadIgnorePatterns(options.path);
|
|
394
|
+
|
|
395
|
+
// Add CLI-provided ignore patterns
|
|
396
|
+
ignorePatterns.push(...options.ignore);
|
|
397
|
+
|
|
398
|
+
// Step 3: Scan codebase for files
|
|
399
|
+
if (spinner) spinner.update('Scanning files...');
|
|
400
|
+
|
|
401
|
+
const { scan: scanFiles } = await import('./scanner.js');
|
|
402
|
+
const filePaths = await scanFiles(options.path, ignorePatterns);
|
|
403
|
+
|
|
404
|
+
if (spinner) spinner.update(`Analyzing ${filePaths.length} files...`);
|
|
405
|
+
|
|
406
|
+
// Step 4: Scan files for environment variable references
|
|
407
|
+
const references = await scanFilesForEnvVars(filePaths);
|
|
408
|
+
|
|
409
|
+
// Step 5: Parse .env.example file
|
|
410
|
+
if (spinner) spinner.update('Parsing .env.example...');
|
|
411
|
+
|
|
412
|
+
const { parseEnvFile } = await import('./parser.js');
|
|
413
|
+
const definitions = await parseEnvFile(options.envFile);
|
|
414
|
+
|
|
415
|
+
// Step 6: Analyze and categorize issues
|
|
416
|
+
if (spinner) spinner.update('Analyzing issues...');
|
|
417
|
+
|
|
418
|
+
const { analyzeIssues } = await import('./analyzer.js');
|
|
419
|
+
const result = analyzeIssues(references, definitions);
|
|
420
|
+
|
|
421
|
+
if (spinner) {
|
|
422
|
+
const totalIssues = result.missing.length + result.unused.length + result.undocumented.length;
|
|
423
|
+
if (totalIssues === 0) {
|
|
424
|
+
spinner.succeed('Analysis complete - no issues found! ✨');
|
|
425
|
+
} else {
|
|
426
|
+
spinner.info(`Analysis complete - found ${totalIssues} issue(s)`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Step 7: Generate suggestions
|
|
431
|
+
if (options.suggestions && !options.quiet) {
|
|
432
|
+
generateSuggestions(result);
|
|
433
|
+
|
|
434
|
+
// Add typo detection for missing variables
|
|
435
|
+
for (const missing of result.missing) {
|
|
436
|
+
const definedVars = definitions.map(d => d.varName);
|
|
437
|
+
const similar = findSimilarVariables(missing.varName, definedVars);
|
|
438
|
+
|
|
439
|
+
if (similar.length > 0) {
|
|
440
|
+
console.log(`\n💡 Did you mean '${similar[0].name}' instead of '${missing.varName}'? (${similar[0].similarity}% similar)`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Step 8: Auto-fix if requested
|
|
446
|
+
if (options.fix && result.missing.length > 0) {
|
|
447
|
+
const { generateEnvExampleFix } = await import('./suggestions.js');
|
|
448
|
+
const { readFileSync, writeFileSync } = await import('fs');
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
const existingContent = readFileSync(options.envFile, 'utf-8');
|
|
452
|
+
const fixedContent = generateEnvExampleFix(result, existingContent);
|
|
453
|
+
writeFileSync(options.envFile, fixedContent, 'utf-8');
|
|
454
|
+
console.log(`\n✅ Auto-fixed ${result.missing.length} missing variable(s) in ${options.envFile}`);
|
|
455
|
+
} catch (error) {
|
|
456
|
+
console.error(`\n⚠️ Failed to auto-fix: ${error.message}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Step 9: Format and display output
|
|
461
|
+
const output = await formatOutput(result, options);
|
|
462
|
+
|
|
463
|
+
// Only output if not quiet mode or if there are issues
|
|
464
|
+
if (!options.quiet || (result.missing.length > 0 || result.unused.length > 0 || result.undocumented.length > 0)) {
|
|
465
|
+
console.log(output);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Step 10: Determine exit code based on --fail-on flag
|
|
469
|
+
const exitCode = determineExitCode(result, options.failOn);
|
|
470
|
+
|
|
471
|
+
return exitCode;
|
|
472
|
+
|
|
473
|
+
} catch (error) {
|
|
474
|
+
if (spinner) spinner.fail('Analysis failed');
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Scan multiple files for environment variable references
|
|
481
|
+
*
|
|
482
|
+
* @param {string[]} filePaths - Array of file paths to scan
|
|
483
|
+
* @returns {Promise<Array<{varName: string, filePath: string, lineNumber: number, pattern: string}>>}
|
|
484
|
+
*
|
|
485
|
+
* Preconditions:
|
|
486
|
+
* - filePaths is a valid array of file paths
|
|
487
|
+
* - All files in filePaths exist and are readable
|
|
488
|
+
*
|
|
489
|
+
* Postconditions:
|
|
490
|
+
* - Returns array of all env var references found across all files
|
|
491
|
+
* - Each reference has valid varName, filePath, lineNumber, pattern
|
|
492
|
+
*
|
|
493
|
+
* Requirements: 1.1.1-1.1.8
|
|
494
|
+
*/
|
|
495
|
+
async function scanFilesForEnvVars(filePaths) {
|
|
496
|
+
const { createReadStream } = await import('fs');
|
|
497
|
+
const { createInterface } = await import('readline');
|
|
498
|
+
const path = await import('path');
|
|
499
|
+
|
|
500
|
+
const jsScanner = await import('./scanners/javascript.js');
|
|
501
|
+
const pyScanner = await import('./scanners/python.js');
|
|
502
|
+
const goScanner = await import('./scanners/go.js');
|
|
503
|
+
const rbScanner = await import('./scanners/ruby.js');
|
|
504
|
+
const rsScanner = await import('./scanners/rust.js');
|
|
505
|
+
const shScanner = await import('./scanners/shell.js');
|
|
506
|
+
|
|
507
|
+
const scannersByExtension = new Map([
|
|
508
|
+
['.js', jsScanner],
|
|
509
|
+
['.jsx', jsScanner],
|
|
510
|
+
['.ts', jsScanner],
|
|
511
|
+
['.tsx', jsScanner],
|
|
512
|
+
['.mjs', jsScanner],
|
|
513
|
+
['.cjs', jsScanner],
|
|
514
|
+
['.py', pyScanner],
|
|
515
|
+
['.go', goScanner],
|
|
516
|
+
['.rb', rbScanner],
|
|
517
|
+
['.rs', rsScanner],
|
|
518
|
+
['.sh', shScanner],
|
|
519
|
+
['.bash', shScanner],
|
|
520
|
+
['.zsh', shScanner],
|
|
521
|
+
]);
|
|
522
|
+
|
|
523
|
+
const concurrency = Math.max(
|
|
524
|
+
1,
|
|
525
|
+
Math.min(32, Number(process.env.ENVCHECK_SCAN_CONCURRENCY) || 8)
|
|
526
|
+
);
|
|
527
|
+
const workerCount = Math.min(concurrency, filePaths.length);
|
|
528
|
+
const referencesPerWorker = Array.from({ length: workerCount }, () => []);
|
|
529
|
+
let currentIndex = 0;
|
|
530
|
+
|
|
531
|
+
async function scanSingleFile(filePath, scanner) {
|
|
532
|
+
const references = [];
|
|
533
|
+
const stream = createReadStream(filePath, { encoding: 'utf-8' });
|
|
534
|
+
const lineReader = createInterface({
|
|
535
|
+
input: stream,
|
|
536
|
+
crlfDelay: Infinity,
|
|
537
|
+
});
|
|
538
|
+
let lineNumber = 0;
|
|
539
|
+
|
|
540
|
+
for await (const line of lineReader) {
|
|
541
|
+
lineNumber += 1;
|
|
542
|
+
references.push(...scanner.scanLine(line, filePath, lineNumber));
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return references;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function runWorker(workerIndex) {
|
|
549
|
+
while (currentIndex < filePaths.length) {
|
|
550
|
+
const index = currentIndex;
|
|
551
|
+
currentIndex += 1;
|
|
552
|
+
const filePath = filePaths[index];
|
|
553
|
+
|
|
554
|
+
try {
|
|
555
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
556
|
+
const scanner = scannersByExtension.get(ext);
|
|
557
|
+
if (!scanner) {
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const references = await scanSingleFile(filePath, scanner);
|
|
562
|
+
referencesPerWorker[workerIndex].push(...references);
|
|
563
|
+
} catch (error) {
|
|
564
|
+
console.warn(`Warning: Error scanning ${filePath}: ${error.message}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
await Promise.all(
|
|
570
|
+
Array.from({ length: workerCount }, (_, workerIndex) => runWorker(workerIndex))
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
return referencesPerWorker.flat();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Format analysis results based on output format option
|
|
578
|
+
*
|
|
579
|
+
* @param {{missing: Array, unused: Array, undocumented: Array, summary: Object}} result - Analysis result
|
|
580
|
+
* @param {CLIOptions} options - CLI options including format and display flags
|
|
581
|
+
* @returns {Promise<string>} Formatted output string
|
|
582
|
+
*
|
|
583
|
+
* Preconditions:
|
|
584
|
+
* - result is a valid AnalysisResult object
|
|
585
|
+
* - options.format is one of: 'text', 'json', 'github'
|
|
586
|
+
*
|
|
587
|
+
* Postconditions:
|
|
588
|
+
* - Returns formatted string ready for console output
|
|
589
|
+
* - Format matches options.format
|
|
590
|
+
* - Colors are omitted if options.noColor is true
|
|
591
|
+
*
|
|
592
|
+
* Requirements: 1.6.1-1.6.6
|
|
593
|
+
*/
|
|
594
|
+
async function formatOutput(result, options) {
|
|
595
|
+
if (options.format === 'json') {
|
|
596
|
+
const { formatJSON } = await import('./formatters/json.js');
|
|
597
|
+
return formatJSON(result);
|
|
598
|
+
} else if (options.format === 'github') {
|
|
599
|
+
const { formatGitHub } = await import('./formatters/github.js');
|
|
600
|
+
return formatGitHub(result);
|
|
601
|
+
} else {
|
|
602
|
+
// Default to text format
|
|
603
|
+
const { formatText } = await import('./formatters/text.js');
|
|
604
|
+
return formatText(result, { noColor: options.noColor, quiet: options.quiet });
|
|
605
|
+
}
|
|
606
|
+
}
|