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/config.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration file management
|
|
3
|
+
* Supports .envcheckrc, .envcheckrc.json, envcheck.config.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, existsSync, writeFileSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Load configuration from file
|
|
11
|
+
*/
|
|
12
|
+
export function loadConfig(cwd = '.') {
|
|
13
|
+
const configFiles = [
|
|
14
|
+
'.envcheckrc',
|
|
15
|
+
'.envcheckrc.json',
|
|
16
|
+
'envcheck.config.json',
|
|
17
|
+
'.envcheckrc.js',
|
|
18
|
+
'envcheck.config.js',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
for (const file of configFiles) {
|
|
22
|
+
const configPath = join(cwd, file);
|
|
23
|
+
|
|
24
|
+
if (existsSync(configPath)) {
|
|
25
|
+
try {
|
|
26
|
+
if (file.endsWith('.js')) {
|
|
27
|
+
// Dynamic import for JS config files
|
|
28
|
+
return loadJSConfig(configPath);
|
|
29
|
+
} else {
|
|
30
|
+
// JSON config
|
|
31
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
32
|
+
return JSON.parse(content);
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.warn(`Warning: Failed to load config from ${file}: ${error.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Load JavaScript config file
|
|
45
|
+
*/
|
|
46
|
+
async function loadJSConfig(configPath) {
|
|
47
|
+
try {
|
|
48
|
+
const module = await import(configPath);
|
|
49
|
+
return module.default || module;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw new Error(`Failed to load JS config: ${error.message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Save configuration to file
|
|
57
|
+
*/
|
|
58
|
+
export function saveConfig(config, cwd = '.', filename = '.envcheckrc.json') {
|
|
59
|
+
const configPath = join(cwd, filename);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const content = JSON.stringify(config, null, 2);
|
|
63
|
+
writeFileSync(configPath, content, 'utf-8');
|
|
64
|
+
return configPath;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new Error(`Failed to save config: ${error.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Merge CLI options with config file
|
|
72
|
+
*/
|
|
73
|
+
export function mergeConfig(cliOptions, fileConfig) {
|
|
74
|
+
if (!fileConfig) return cliOptions;
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
...fileConfig,
|
|
78
|
+
...cliOptions,
|
|
79
|
+
// Merge arrays
|
|
80
|
+
ignore: [...(fileConfig.ignore || []), ...(cliOptions.ignore || [])],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Validate configuration
|
|
86
|
+
*/
|
|
87
|
+
export function validateConfig(config) {
|
|
88
|
+
const errors = [];
|
|
89
|
+
|
|
90
|
+
if (config.format && !['text', 'json', 'github'].includes(config.format)) {
|
|
91
|
+
errors.push(`Invalid format: ${config.format}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (config.failOn && !['missing', 'unused', 'undocumented', 'all', 'none'].includes(config.failOn)) {
|
|
95
|
+
errors.push(`Invalid failOn: ${config.failOn}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (config.ignore && !Array.isArray(config.ignore)) {
|
|
99
|
+
errors.push('ignore must be an array');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return errors;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get default configuration
|
|
107
|
+
*/
|
|
108
|
+
export function getDefaultConfig() {
|
|
109
|
+
return {
|
|
110
|
+
path: '.',
|
|
111
|
+
envFile: '.env.example',
|
|
112
|
+
format: 'text',
|
|
113
|
+
failOn: 'none',
|
|
114
|
+
ignore: ['node_modules/**', 'dist/**', 'build/**', '.git/**'],
|
|
115
|
+
noColor: false,
|
|
116
|
+
quiet: false,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Actions Formatter Module
|
|
3
|
+
*
|
|
4
|
+
* Formats analysis results as GitHub Actions workflow commands for CI/CD integration.
|
|
5
|
+
* Produces ::error and ::warning annotations that appear in GitHub's UI.
|
|
6
|
+
*
|
|
7
|
+
* @see https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Formats analysis results as GitHub Actions annotations
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} result - Analysis result from analyzer
|
|
14
|
+
* @param {Array} result.missing - Missing variable issues
|
|
15
|
+
* @param {Array} result.unused - Unused variable issues
|
|
16
|
+
* @param {Array} result.undocumented - Undocumented variable issues
|
|
17
|
+
* @param {Object} result.summary - Summary statistics
|
|
18
|
+
* @returns {string} GitHub Actions formatted output
|
|
19
|
+
*/
|
|
20
|
+
export function formatGitHub(result) {
|
|
21
|
+
const annotations = [];
|
|
22
|
+
|
|
23
|
+
// Format missing variables as errors
|
|
24
|
+
for (const issue of result.missing) {
|
|
25
|
+
annotations.push(...formatMissingAnnotations(issue));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Format unused variables as warnings
|
|
29
|
+
for (const issue of result.unused) {
|
|
30
|
+
annotations.push(formatUnusedAnnotation(issue));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Format undocumented variables as warnings
|
|
34
|
+
for (const issue of result.undocumented) {
|
|
35
|
+
annotations.push(formatUndocumentedAnnotation(issue));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Add summary notice
|
|
39
|
+
annotations.push(formatSummaryNotice(result.summary));
|
|
40
|
+
|
|
41
|
+
return annotations.join('\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Formats missing variable issue as GitHub error annotations
|
|
46
|
+
* Creates one error annotation per file reference
|
|
47
|
+
*
|
|
48
|
+
* @param {Object} issue - Missing variable issue
|
|
49
|
+
* @param {string} issue.varName - Variable name
|
|
50
|
+
* @param {Array} issue.references - File references where variable is used
|
|
51
|
+
* @returns {Array<string>} Array of error annotations
|
|
52
|
+
*/
|
|
53
|
+
export function formatMissingAnnotations(issue) {
|
|
54
|
+
return issue.references.map(ref => {
|
|
55
|
+
const message = `Missing environment variable: ${issue.varName} is used but not defined in .env.example`;
|
|
56
|
+
return formatErrorAnnotation(ref.filePath, ref.lineNumber, message);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Formats unused variable issue as GitHub warning annotation
|
|
62
|
+
*
|
|
63
|
+
* @param {Object} issue - Unused variable issue
|
|
64
|
+
* @param {string} issue.varName - Variable name
|
|
65
|
+
* @param {Object} issue.definition - Definition location
|
|
66
|
+
* @returns {string} Warning annotation
|
|
67
|
+
*/
|
|
68
|
+
export function formatUnusedAnnotation(issue) {
|
|
69
|
+
const message = `Unused environment variable: ${issue.varName} is defined in .env.example but never used`;
|
|
70
|
+
return formatWarningAnnotation('.env.example', issue.definition.lineNumber, message);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Formats undocumented variable issue as GitHub warning annotation
|
|
75
|
+
*
|
|
76
|
+
* @param {Object} issue - Undocumented variable issue
|
|
77
|
+
* @param {string} issue.varName - Variable name
|
|
78
|
+
* @param {Object} issue.definition - Definition location
|
|
79
|
+
* @returns {string} Warning annotation
|
|
80
|
+
*/
|
|
81
|
+
export function formatUndocumentedAnnotation(issue) {
|
|
82
|
+
const message = `Undocumented environment variable: ${issue.varName} is missing a comment in .env.example`;
|
|
83
|
+
return formatWarningAnnotation('.env.example', issue.definition.lineNumber, message);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Formats summary as GitHub notice annotation
|
|
88
|
+
*
|
|
89
|
+
* @param {Object} summary - Summary statistics
|
|
90
|
+
* @returns {string} Notice annotation
|
|
91
|
+
*/
|
|
92
|
+
export function formatSummaryNotice(summary) {
|
|
93
|
+
const parts = [];
|
|
94
|
+
|
|
95
|
+
if (summary.totalMissing > 0) {
|
|
96
|
+
parts.push(`${summary.totalMissing} missing`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (summary.totalUnused > 0) {
|
|
100
|
+
parts.push(`${summary.totalUnused} unused`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (summary.totalUndocumented > 0) {
|
|
104
|
+
parts.push(`${summary.totalUndocumented} undocumented`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (parts.length === 0) {
|
|
108
|
+
return '::notice::Environment check passed - no issues found';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return `::notice::Environment check completed - ${parts.join(', ')}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Formats a GitHub Actions error annotation
|
|
116
|
+
*
|
|
117
|
+
* @param {string} file - File path
|
|
118
|
+
* @param {number} line - Line number
|
|
119
|
+
* @param {string} message - Error message
|
|
120
|
+
* @returns {string} Formatted error annotation
|
|
121
|
+
*/
|
|
122
|
+
export function formatErrorAnnotation(file, line, message) {
|
|
123
|
+
return `::error file=${escapeProperty(file)},line=${line}::${escapeMessage(message)}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Formats a GitHub Actions warning annotation
|
|
128
|
+
*
|
|
129
|
+
* @param {string} file - File path
|
|
130
|
+
* @param {number} line - Line number
|
|
131
|
+
* @param {string} message - Warning message
|
|
132
|
+
* @returns {string} Formatted warning annotation
|
|
133
|
+
*/
|
|
134
|
+
export function formatWarningAnnotation(file, line, message) {
|
|
135
|
+
return `::warning file=${escapeProperty(file)},line=${line}::${escapeMessage(message)}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Escapes special characters in annotation properties (file, line, col)
|
|
140
|
+
*
|
|
141
|
+
* @param {string} value - Property value to escape
|
|
142
|
+
* @returns {string} Escaped value
|
|
143
|
+
*/
|
|
144
|
+
export function escapeProperty(value) {
|
|
145
|
+
return value
|
|
146
|
+
.replace(/%/g, '%25')
|
|
147
|
+
.replace(/\r/g, '%0D')
|
|
148
|
+
.replace(/\n/g, '%0A')
|
|
149
|
+
.replace(/:/g, '%3A')
|
|
150
|
+
.replace(/,/g, '%2C');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Escapes special characters in annotation messages
|
|
155
|
+
*
|
|
156
|
+
* @param {string} message - Message to escape
|
|
157
|
+
* @returns {string} Escaped message
|
|
158
|
+
*/
|
|
159
|
+
export function escapeMessage(message) {
|
|
160
|
+
return message
|
|
161
|
+
.replace(/%/g, '%25')
|
|
162
|
+
.replace(/\r/g, '%0D')
|
|
163
|
+
.replace(/\n/g, '%0A');
|
|
164
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Formatter Module
|
|
3
|
+
*
|
|
4
|
+
* Formats analysis results as valid JSON for machine parsing and CI/CD integration.
|
|
5
|
+
* Produces structured output with all issue categories and summary statistics.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Formats analysis results as JSON
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} result - Analysis result from analyzer
|
|
12
|
+
* @param {Array} result.missing - Missing variable issues
|
|
13
|
+
* @param {Array} result.unused - Unused variable issues
|
|
14
|
+
* @param {Array} result.undocumented - Undocumented variable issues
|
|
15
|
+
* @param {Object} result.summary - Summary statistics
|
|
16
|
+
* @returns {string} JSON-formatted string
|
|
17
|
+
*/
|
|
18
|
+
export function formatJSON(result) {
|
|
19
|
+
const output = {
|
|
20
|
+
missing: formatMissingIssues(result.missing),
|
|
21
|
+
unused: formatUnusedIssues(result.unused),
|
|
22
|
+
undocumented: formatUndocumentedIssues(result.undocumented),
|
|
23
|
+
summary: formatSummary(result.summary)
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return JSON.stringify(output, null, 2);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Formats missing variable issues for JSON output
|
|
31
|
+
*
|
|
32
|
+
* @param {Array} missing - Array of missing variable issues
|
|
33
|
+
* @returns {Array} Formatted missing issues
|
|
34
|
+
*/
|
|
35
|
+
export function formatMissingIssues(missing) {
|
|
36
|
+
return missing.map(issue => ({
|
|
37
|
+
varName: issue.varName,
|
|
38
|
+
references: issue.references.map(ref => ({
|
|
39
|
+
filePath: ref.filePath,
|
|
40
|
+
lineNumber: ref.lineNumber,
|
|
41
|
+
pattern: ref.pattern
|
|
42
|
+
}))
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Formats unused variable issues for JSON output
|
|
48
|
+
*
|
|
49
|
+
* @param {Array} unused - Array of unused variable issues
|
|
50
|
+
* @returns {Array} Formatted unused issues
|
|
51
|
+
*/
|
|
52
|
+
export function formatUnusedIssues(unused) {
|
|
53
|
+
return unused.map(issue => ({
|
|
54
|
+
varName: issue.varName,
|
|
55
|
+
definition: {
|
|
56
|
+
lineNumber: issue.definition.lineNumber,
|
|
57
|
+
hasComment: issue.definition.hasComment,
|
|
58
|
+
comment: issue.definition.comment
|
|
59
|
+
}
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Formats undocumented variable issues for JSON output
|
|
65
|
+
*
|
|
66
|
+
* @param {Array} undocumented - Array of undocumented variable issues
|
|
67
|
+
* @returns {Array} Formatted undocumented issues
|
|
68
|
+
*/
|
|
69
|
+
export function formatUndocumentedIssues(undocumented) {
|
|
70
|
+
return undocumented.map(issue => ({
|
|
71
|
+
varName: issue.varName,
|
|
72
|
+
references: issue.references.map(ref => ({
|
|
73
|
+
filePath: ref.filePath,
|
|
74
|
+
lineNumber: ref.lineNumber,
|
|
75
|
+
pattern: ref.pattern
|
|
76
|
+
})),
|
|
77
|
+
definition: {
|
|
78
|
+
lineNumber: issue.definition.lineNumber,
|
|
79
|
+
hasComment: issue.definition.hasComment,
|
|
80
|
+
comment: issue.definition.comment
|
|
81
|
+
}
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Formats summary statistics for JSON output
|
|
87
|
+
*
|
|
88
|
+
* @param {Object} summary - Summary statistics object
|
|
89
|
+
* @returns {Object} Formatted summary
|
|
90
|
+
*/
|
|
91
|
+
export function formatSummary(summary) {
|
|
92
|
+
return {
|
|
93
|
+
totalMissing: summary.totalMissing,
|
|
94
|
+
totalUnused: summary.totalUnused,
|
|
95
|
+
totalUndocumented: summary.totalUndocumented,
|
|
96
|
+
totalReferences: summary.totalReferences,
|
|
97
|
+
totalDefinitions: summary.totalDefinitions
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Validates that a string is valid JSON
|
|
103
|
+
*
|
|
104
|
+
* @param {string} jsonString - String to validate
|
|
105
|
+
* @returns {boolean} True if valid JSON, false otherwise
|
|
106
|
+
*/
|
|
107
|
+
export function isValidJSON(jsonString) {
|
|
108
|
+
try {
|
|
109
|
+
JSON.parse(jsonString);
|
|
110
|
+
return true;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table formatter for better visual output
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format data as a table
|
|
7
|
+
*/
|
|
8
|
+
export function formatTable(headers, rows, options = {}) {
|
|
9
|
+
const { maxWidth = 100, padding = 2 } = options;
|
|
10
|
+
|
|
11
|
+
if (rows.length === 0) {
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Calculate column widths
|
|
16
|
+
const columnWidths = headers.map((header, i) => {
|
|
17
|
+
const headerWidth = header.length;
|
|
18
|
+
const maxRowWidth = Math.max(
|
|
19
|
+
...rows.map(row => String(row[i] || '').length)
|
|
20
|
+
);
|
|
21
|
+
return Math.min(Math.max(headerWidth, maxRowWidth) + padding, maxWidth);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Create separator
|
|
25
|
+
const separator = '─'.repeat(columnWidths.reduce((a, b) => a + b, 0) + columnWidths.length + 1);
|
|
26
|
+
|
|
27
|
+
// Format header
|
|
28
|
+
const headerRow = '│ ' + headers.map((header, i) =>
|
|
29
|
+
header.padEnd(columnWidths[i])
|
|
30
|
+
).join('│ ') + '│';
|
|
31
|
+
|
|
32
|
+
// Format rows
|
|
33
|
+
const dataRows = rows.map(row =>
|
|
34
|
+
'│ ' + row.map((cell, i) =>
|
|
35
|
+
String(cell || '').padEnd(columnWidths[i])
|
|
36
|
+
).join('│ ') + '│'
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return [
|
|
40
|
+
'┌' + separator + '┐',
|
|
41
|
+
headerRow,
|
|
42
|
+
'├' + separator + '┤',
|
|
43
|
+
...dataRows,
|
|
44
|
+
'└' + separator + '┘',
|
|
45
|
+
].join('\n');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Format summary statistics
|
|
50
|
+
*/
|
|
51
|
+
export function formatSummary(result) {
|
|
52
|
+
const total = result.missing.length + result.unused.length + result.undocumented.length;
|
|
53
|
+
|
|
54
|
+
const stats = [
|
|
55
|
+
['Category', 'Count', 'Status'],
|
|
56
|
+
['Missing', result.missing.length, result.missing.length > 0 ? '❌' : '✅'],
|
|
57
|
+
['Unused', result.unused.length, result.unused.length > 0 ? '⚠️' : '✅'],
|
|
58
|
+
['Undocumented', result.undocumented.length, result.undocumented.length > 0 ? 'ℹ️' : '✅'],
|
|
59
|
+
['Total Issues', total, total > 0 ? '⚠️' : '✅'],
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
return formatTable(stats[0], stats.slice(1));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Format issues as a tree structure
|
|
67
|
+
*/
|
|
68
|
+
export function formatTree(issues, title) {
|
|
69
|
+
if (issues.length === 0) {
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const lines = [`\n${title}:`];
|
|
74
|
+
|
|
75
|
+
issues.forEach((issue, index) => {
|
|
76
|
+
const isLast = index === issues.length - 1;
|
|
77
|
+
const prefix = isLast ? '└─' : '├─';
|
|
78
|
+
const childPrefix = isLast ? ' ' : '│ ';
|
|
79
|
+
|
|
80
|
+
lines.push(`${prefix} ${issue.varName}`);
|
|
81
|
+
|
|
82
|
+
if (issue.locations) {
|
|
83
|
+
issue.locations.forEach((loc, locIndex) => {
|
|
84
|
+
const isLastLoc = locIndex === issue.locations.length - 1;
|
|
85
|
+
const locPrefix = isLastLoc ? '└─' : '├─';
|
|
86
|
+
lines.push(`${childPrefix}${locPrefix} ${loc.filePath}:${loc.lineNumber}`);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return lines.join('\n');
|
|
92
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Formatter Module
|
|
3
|
+
*
|
|
4
|
+
* Formats analysis results as human-readable text output with:
|
|
5
|
+
* - Colored categories (red, yellow, green)
|
|
6
|
+
* - Emoji icons for visual clarity
|
|
7
|
+
* - File reference listings with line numbers
|
|
8
|
+
* - Summary statistics
|
|
9
|
+
* - Support for --no-color flag
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ANSI color codes for terminal output
|
|
14
|
+
*/
|
|
15
|
+
const COLORS = {
|
|
16
|
+
RED: '\x1b[31m',
|
|
17
|
+
YELLOW: '\x1b[33m',
|
|
18
|
+
GREEN: '\x1b[32m',
|
|
19
|
+
RESET: '\x1b[0m',
|
|
20
|
+
BOLD: '\x1b[1m',
|
|
21
|
+
DIM: '\x1b[2m'
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Emoji icons for issue categories
|
|
26
|
+
*/
|
|
27
|
+
const ICONS = {
|
|
28
|
+
MISSING: '🔴',
|
|
29
|
+
UNUSED: '🟡',
|
|
30
|
+
UNDOCUMENTED: '🟢'
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Formats analysis results as colored text output
|
|
35
|
+
*
|
|
36
|
+
* @param {{missing: Array, unused: Array, undocumented: Array, summary: Object}} result - Analysis result
|
|
37
|
+
* @param {{noColor: boolean, quiet: boolean}} options - Formatting options
|
|
38
|
+
* @returns {string} Formatted text output
|
|
39
|
+
*/
|
|
40
|
+
export function formatText(result, options = {}) {
|
|
41
|
+
const { noColor = false, quiet = false } = options;
|
|
42
|
+
|
|
43
|
+
// If quiet mode and no issues, return empty string
|
|
44
|
+
if (quiet && hasNoIssues(result)) {
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const sections = [];
|
|
49
|
+
|
|
50
|
+
// Format MISSING section
|
|
51
|
+
if (result.missing.length > 0) {
|
|
52
|
+
sections.push(formatMissingSection(result.missing, noColor));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Format UNUSED section
|
|
56
|
+
if (result.unused.length > 0) {
|
|
57
|
+
sections.push(formatUnusedSection(result.unused, noColor));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Format UNDOCUMENTED section
|
|
61
|
+
if (result.undocumented.length > 0) {
|
|
62
|
+
sections.push(formatUndocumentedSection(result.undocumented, noColor));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Add summary line
|
|
66
|
+
sections.push(formatSummary(result.summary, noColor));
|
|
67
|
+
|
|
68
|
+
return sections.join('\n\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Checks if analysis result has no issues
|
|
73
|
+
*
|
|
74
|
+
* @param {{missing: Array, unused: Array, undocumented: Array}} result
|
|
75
|
+
* @returns {boolean} True if no issues found
|
|
76
|
+
*/
|
|
77
|
+
export function hasNoIssues(result) {
|
|
78
|
+
return result.missing.length === 0 &&
|
|
79
|
+
result.unused.length === 0 &&
|
|
80
|
+
result.undocumented.length === 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Formats MISSING variables section
|
|
85
|
+
*
|
|
86
|
+
* @param {Array<{varName: string, references: Array}>} missing - Missing variable issues
|
|
87
|
+
* @param {boolean} noColor - Disable colored output
|
|
88
|
+
* @returns {string} Formatted section
|
|
89
|
+
*/
|
|
90
|
+
export function formatMissingSection(missing, noColor) {
|
|
91
|
+
const icon = ICONS.MISSING;
|
|
92
|
+
const title = colorize('MISSING', COLORS.RED, noColor);
|
|
93
|
+
const count = missing.length;
|
|
94
|
+
|
|
95
|
+
let output = `${icon} ${title} (${count})\n`;
|
|
96
|
+
output += 'Variables used in code but not in .env.example:\n';
|
|
97
|
+
|
|
98
|
+
for (const issue of missing) {
|
|
99
|
+
output += ` - ${colorize(issue.varName, COLORS.BOLD, noColor)}\n`;
|
|
100
|
+
|
|
101
|
+
// List all file references
|
|
102
|
+
for (const ref of issue.references) {
|
|
103
|
+
output += ` ${colorize('→', COLORS.DIM, noColor)} ${ref.filePath}:${ref.lineNumber}\n`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return output.trimEnd();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Formats UNUSED variables section
|
|
112
|
+
*
|
|
113
|
+
* @param {Array<{varName: string, definition: Object}>} unused - Unused variable issues
|
|
114
|
+
* @param {boolean} noColor - Disable colored output
|
|
115
|
+
* @returns {string} Formatted section
|
|
116
|
+
*/
|
|
117
|
+
export function formatUnusedSection(unused, noColor) {
|
|
118
|
+
const icon = ICONS.UNUSED;
|
|
119
|
+
const title = colorize('UNUSED', COLORS.YELLOW, noColor);
|
|
120
|
+
const count = unused.length;
|
|
121
|
+
|
|
122
|
+
let output = `${icon} ${title} (${count})\n`;
|
|
123
|
+
output += 'Variables in .env.example but never used:\n';
|
|
124
|
+
|
|
125
|
+
for (const issue of unused) {
|
|
126
|
+
output += ` - ${colorize(issue.varName, COLORS.BOLD, noColor)}`;
|
|
127
|
+
output += ` ${colorize(`(.env.example:${issue.definition.lineNumber})`, COLORS.DIM, noColor)}\n`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return output.trimEnd();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Formats UNDOCUMENTED variables section
|
|
135
|
+
*
|
|
136
|
+
* @param {Array<{varName: string, references: Array, definition: Object}>} undocumented - Undocumented variable issues
|
|
137
|
+
* @param {boolean} noColor - Disable colored output
|
|
138
|
+
* @returns {string} Formatted section
|
|
139
|
+
*/
|
|
140
|
+
export function formatUndocumentedSection(undocumented, noColor) {
|
|
141
|
+
const icon = ICONS.UNDOCUMENTED;
|
|
142
|
+
const title = colorize('UNDOCUMENTED', COLORS.GREEN, noColor);
|
|
143
|
+
const count = undocumented.length;
|
|
144
|
+
|
|
145
|
+
let output = `${icon} ${title} (${count})\n`;
|
|
146
|
+
output += 'Variables used and defined but missing comments:\n';
|
|
147
|
+
|
|
148
|
+
for (const issue of undocumented) {
|
|
149
|
+
output += ` - ${colorize(issue.varName, COLORS.BOLD, noColor)}`;
|
|
150
|
+
output += ` ${colorize(`(.env.example:${issue.definition.lineNumber})`, COLORS.DIM, noColor)}\n`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return output.trimEnd();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Formats summary statistics line
|
|
158
|
+
*
|
|
159
|
+
* @param {{totalMissing: number, totalUnused: number, totalUndocumented: number}} summary - Summary statistics
|
|
160
|
+
* @param {boolean} noColor - Disable colored output
|
|
161
|
+
* @returns {string} Formatted summary
|
|
162
|
+
*/
|
|
163
|
+
export function formatSummary(summary, noColor) {
|
|
164
|
+
const parts = [];
|
|
165
|
+
|
|
166
|
+
if (summary.totalMissing > 0) {
|
|
167
|
+
parts.push(colorize(`${summary.totalMissing} missing`, COLORS.RED, noColor));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (summary.totalUnused > 0) {
|
|
171
|
+
parts.push(colorize(`${summary.totalUnused} unused`, COLORS.YELLOW, noColor));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (summary.totalUndocumented > 0) {
|
|
175
|
+
parts.push(colorize(`${summary.totalUndocumented} undocumented`, COLORS.GREEN, noColor));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (parts.length === 0) {
|
|
179
|
+
return colorize('✓ No issues found', COLORS.GREEN, noColor);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return `Summary: ${parts.join(', ')}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Applies ANSI color codes to text
|
|
187
|
+
*
|
|
188
|
+
* @param {string} text - Text to colorize
|
|
189
|
+
* @param {string} color - ANSI color code
|
|
190
|
+
* @param {boolean} noColor - Disable colored output
|
|
191
|
+
* @returns {string} Colorized text or plain text if noColor is true
|
|
192
|
+
*/
|
|
193
|
+
export function colorize(text, color, noColor) {
|
|
194
|
+
if (noColor) {
|
|
195
|
+
return text;
|
|
196
|
+
}
|
|
197
|
+
return `${color}${text}${COLORS.RESET}`;
|
|
198
|
+
}
|