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/ignore.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ignore Pattern Handler Module
|
|
3
|
+
*
|
|
4
|
+
* Handles loading and matching of ignore patterns from .gitignore, .envcheckignore,
|
|
5
|
+
* and default patterns. Supports glob pattern syntax and negation patterns.
|
|
6
|
+
*
|
|
7
|
+
* Requirements: 1.7.1-1.7.6
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Load ignore patterns from .gitignore and .envcheckignore files
|
|
14
|
+
*
|
|
15
|
+
* @param {string} basePath - Base directory path to search for ignore files
|
|
16
|
+
* @returns {string[]} Array of ignore patterns
|
|
17
|
+
*
|
|
18
|
+
* Preconditions:
|
|
19
|
+
* - basePath is a valid directory path
|
|
20
|
+
*
|
|
21
|
+
* Postconditions:
|
|
22
|
+
* - Returns array of patterns from .gitignore, .envcheckignore, and defaults
|
|
23
|
+
* - Returns at least default patterns if no ignore files exist
|
|
24
|
+
*
|
|
25
|
+
* Requirements: 1.7.1, 1.7.2, 1.7.3
|
|
26
|
+
*/
|
|
27
|
+
export function loadIgnorePatterns(basePath) {
|
|
28
|
+
const patterns = [];
|
|
29
|
+
|
|
30
|
+
// Add default patterns first
|
|
31
|
+
patterns.push(...getDefaultIgnores());
|
|
32
|
+
|
|
33
|
+
// Load .gitignore patterns if exists
|
|
34
|
+
const gitignorePath = `${basePath}/.gitignore`;
|
|
35
|
+
const gitignorePatterns = parseGitignore(gitignorePath);
|
|
36
|
+
patterns.push(...gitignorePatterns);
|
|
37
|
+
|
|
38
|
+
// Load .envcheckignore patterns if exists
|
|
39
|
+
const envcheckignorePath = `${basePath}/.envcheckignore`;
|
|
40
|
+
const envcheckignorePatterns = parseEnvcheckignore(envcheckignorePath);
|
|
41
|
+
patterns.push(...envcheckignorePatterns);
|
|
42
|
+
|
|
43
|
+
return patterns;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if a file path should be ignored based on patterns
|
|
48
|
+
*
|
|
49
|
+
* @param {string} filePath - File path to check (relative or absolute)
|
|
50
|
+
* @param {string[]} patterns - Array of glob patterns
|
|
51
|
+
* @returns {boolean} True if file should be ignored, false otherwise
|
|
52
|
+
*
|
|
53
|
+
* Preconditions:
|
|
54
|
+
* - filePath is a non-empty string
|
|
55
|
+
* - patterns is a valid array (may be empty)
|
|
56
|
+
*
|
|
57
|
+
* Postconditions:
|
|
58
|
+
* - Returns true if filePath matches any pattern
|
|
59
|
+
* - Returns false if no patterns match
|
|
60
|
+
* - Handles negation patterns correctly
|
|
61
|
+
*
|
|
62
|
+
* Requirements: 1.7.5, 1.7.6
|
|
63
|
+
*/
|
|
64
|
+
export function shouldIgnore(filePath, patterns) {
|
|
65
|
+
if (!patterns || patterns.length === 0) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let ignored = false;
|
|
70
|
+
|
|
71
|
+
// Process patterns in order - later patterns can override earlier ones
|
|
72
|
+
for (const pattern of patterns) {
|
|
73
|
+
if (isNegationPattern(pattern)) {
|
|
74
|
+
// Negation pattern - if it matches, un-ignore the file
|
|
75
|
+
const actualPattern = removeNegationPrefix(pattern);
|
|
76
|
+
if (matchGlob(filePath, actualPattern)) {
|
|
77
|
+
ignored = false;
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
// Regular pattern - if it matches, ignore the file
|
|
81
|
+
if (matchGlob(filePath, pattern)) {
|
|
82
|
+
ignored = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return ignored;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Parse a .gitignore file and extract patterns
|
|
92
|
+
*
|
|
93
|
+
* @param {string} filePath - Path to .gitignore file
|
|
94
|
+
* @returns {string[]} Array of ignore patterns
|
|
95
|
+
*
|
|
96
|
+
* Preconditions:
|
|
97
|
+
* - filePath points to a readable file
|
|
98
|
+
*
|
|
99
|
+
* Postconditions:
|
|
100
|
+
* - Returns array of non-empty, non-comment lines
|
|
101
|
+
* - Trims whitespace from patterns
|
|
102
|
+
* - Returns empty array if file doesn't exist or is unreadable
|
|
103
|
+
*
|
|
104
|
+
* Requirements: 1.7.1
|
|
105
|
+
*/
|
|
106
|
+
export function parseGitignore(filePath) {
|
|
107
|
+
try {
|
|
108
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
109
|
+
|
|
110
|
+
return content
|
|
111
|
+
.split('\n')
|
|
112
|
+
.map(line => line.trim())
|
|
113
|
+
.filter(line => line !== '' && !line.startsWith('#'));
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// Return empty array if file doesn't exist or is unreadable
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Parse a .envcheckignore file and extract patterns
|
|
122
|
+
*
|
|
123
|
+
* @param {string} filePath - Path to .envcheckignore file
|
|
124
|
+
* @returns {string[]} Array of ignore patterns
|
|
125
|
+
*
|
|
126
|
+
* Preconditions:
|
|
127
|
+
* - filePath points to a readable file
|
|
128
|
+
*
|
|
129
|
+
* Postconditions:
|
|
130
|
+
* - Returns array of non-empty, non-comment lines
|
|
131
|
+
* - Trims whitespace from patterns
|
|
132
|
+
* - Returns empty array if file doesn't exist or is unreadable
|
|
133
|
+
*
|
|
134
|
+
* Requirements: 1.7.2
|
|
135
|
+
*/
|
|
136
|
+
export function parseEnvcheckignore(filePath) {
|
|
137
|
+
// Same implementation as parseGitignore - both use same format
|
|
138
|
+
try {
|
|
139
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
140
|
+
|
|
141
|
+
return content
|
|
142
|
+
.split('\n')
|
|
143
|
+
.map(line => line.trim())
|
|
144
|
+
.filter(line => line !== '' && !line.startsWith('#'));
|
|
145
|
+
} catch (error) {
|
|
146
|
+
// Return empty array if file doesn't exist or is unreadable
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get default ignore patterns
|
|
153
|
+
*
|
|
154
|
+
* @returns {string[]} Array of default ignore patterns
|
|
155
|
+
*
|
|
156
|
+
* Postconditions:
|
|
157
|
+
* - Returns array containing at least: node_modules, .git, dist, build
|
|
158
|
+
*
|
|
159
|
+
* Requirements: 1.7.3
|
|
160
|
+
*/
|
|
161
|
+
export function getDefaultIgnores() {
|
|
162
|
+
return [
|
|
163
|
+
'node_modules/**',
|
|
164
|
+
'node_modules',
|
|
165
|
+
'.git/**',
|
|
166
|
+
'.git',
|
|
167
|
+
'dist/**',
|
|
168
|
+
'dist',
|
|
169
|
+
'build/**',
|
|
170
|
+
'build',
|
|
171
|
+
'coverage/**',
|
|
172
|
+
'.nyc_output/**',
|
|
173
|
+
'**/*.min.js',
|
|
174
|
+
'**/*.bundle.js',
|
|
175
|
+
'**/vendor/**',
|
|
176
|
+
'**/tmp/**',
|
|
177
|
+
'**/temp/**'
|
|
178
|
+
];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Match a file path against a glob pattern
|
|
183
|
+
*
|
|
184
|
+
* @param {string} filePath - File path to match
|
|
185
|
+
* @param {string} pattern - Glob pattern (supports *, **, ?, [])
|
|
186
|
+
* @returns {boolean} True if path matches pattern
|
|
187
|
+
*
|
|
188
|
+
* Preconditions:
|
|
189
|
+
* - filePath is a non-empty string
|
|
190
|
+
* - pattern is a valid glob pattern
|
|
191
|
+
*
|
|
192
|
+
* Postconditions:
|
|
193
|
+
* - Returns true if filePath matches the glob pattern
|
|
194
|
+
* - Supports * (any characters except /), ** (any characters including /), ? (single char), [] (char class)
|
|
195
|
+
*
|
|
196
|
+
* Requirements: 1.7.5
|
|
197
|
+
*/
|
|
198
|
+
export function matchGlob(filePath, pattern) {
|
|
199
|
+
// Normalize paths to use forward slashes
|
|
200
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
201
|
+
let normalizedPattern = pattern.replace(/\\/g, '/');
|
|
202
|
+
|
|
203
|
+
// If pattern ends with /**, also match the directory itself
|
|
204
|
+
// e.g., node_modules/** should match both "node_modules" and "node_modules/anything"
|
|
205
|
+
if (normalizedPattern.endsWith('/**')) {
|
|
206
|
+
const dirPattern = normalizedPattern.slice(0, -3); // Remove /**
|
|
207
|
+
if (matchGlobInternal(normalizedPath, dirPattern)) {
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return matchGlobInternal(normalizedPath, normalizedPattern);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Validate a glob pattern for correctness
|
|
217
|
+
*
|
|
218
|
+
* @param {string} pattern - Glob pattern to validate
|
|
219
|
+
* @returns {boolean} True if valid
|
|
220
|
+
* @throws {Error} If pattern is invalid
|
|
221
|
+
*/
|
|
222
|
+
export function validateGlobPattern(pattern) {
|
|
223
|
+
if (typeof pattern !== 'string' || pattern.trim() === '') {
|
|
224
|
+
throw new Error('Glob pattern cannot be empty');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let escaped = false;
|
|
228
|
+
let bracketDepth = 0;
|
|
229
|
+
|
|
230
|
+
for (const char of pattern) {
|
|
231
|
+
if (escaped) {
|
|
232
|
+
escaped = false;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (char === '\\') {
|
|
237
|
+
escaped = true;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (char === '[') {
|
|
242
|
+
bracketDepth += 1;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (char === ']') {
|
|
247
|
+
if (bracketDepth === 0) {
|
|
248
|
+
throw new Error(`Invalid glob pattern: "${pattern}"`);
|
|
249
|
+
}
|
|
250
|
+
bracketDepth -= 1;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (escaped || bracketDepth !== 0) {
|
|
255
|
+
throw new Error(`Invalid glob pattern: "${pattern}"`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Internal glob matching implementation
|
|
263
|
+
* @private
|
|
264
|
+
*/
|
|
265
|
+
function matchGlobInternal(filePath, pattern) {
|
|
266
|
+
// Convert glob pattern to regex
|
|
267
|
+
let regexPattern = pattern
|
|
268
|
+
// Escape special regex characters except glob wildcards
|
|
269
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
270
|
+
// Replace ** with a placeholder to handle it separately
|
|
271
|
+
.replace(/\*\*/g, '___DOUBLESTAR___')
|
|
272
|
+
// Replace * with regex for any characters except /
|
|
273
|
+
.replace(/\*/g, '[^/]*')
|
|
274
|
+
// Replace ? with regex for single character
|
|
275
|
+
.replace(/\?/g, '[^/]')
|
|
276
|
+
// Replace ** placeholder with regex for any characters including /
|
|
277
|
+
// Use (.*\/)? to make the directory part optional for patterns like **/*.js
|
|
278
|
+
.replace(/___DOUBLESTAR___\//g, '(.*/)?')
|
|
279
|
+
.replace(/___DOUBLESTAR___/g, '.*');
|
|
280
|
+
|
|
281
|
+
// Add anchors to match entire path
|
|
282
|
+
regexPattern = `^${regexPattern}$`;
|
|
283
|
+
|
|
284
|
+
const regex = new RegExp(regexPattern);
|
|
285
|
+
return regex.test(filePath);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Check if a pattern is a negation pattern
|
|
290
|
+
*
|
|
291
|
+
* @param {string} pattern - Pattern to check
|
|
292
|
+
* @returns {boolean} True if pattern starts with !
|
|
293
|
+
*
|
|
294
|
+
* Requirements: 1.7.6
|
|
295
|
+
*/
|
|
296
|
+
export function isNegationPattern(pattern) {
|
|
297
|
+
return pattern.startsWith('!');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Remove negation prefix from pattern
|
|
302
|
+
*
|
|
303
|
+
* @param {string} pattern - Negation pattern (e.g., "!*.test.js")
|
|
304
|
+
* @returns {string} Pattern without negation prefix (e.g., "*.test.js")
|
|
305
|
+
*
|
|
306
|
+
* Preconditions:
|
|
307
|
+
* - pattern starts with !
|
|
308
|
+
*
|
|
309
|
+
* Requirements: 1.7.6
|
|
310
|
+
*/
|
|
311
|
+
export function removeNegationPrefix(pattern) {
|
|
312
|
+
return pattern.startsWith('!') ? pattern.slice(1) : pattern;
|
|
313
|
+
}
|
package/src/parser.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parses a .env.example file and extracts environment variable definitions.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} filePath - Path to the .env.example file
|
|
7
|
+
* @returns {Promise<Array<{varName: string, hasComment: boolean, comment: string|null, lineNumber: number}>>}
|
|
8
|
+
* @throws {Error} If file cannot be read (not found, permission denied, etc.)
|
|
9
|
+
*/
|
|
10
|
+
export async function parseEnvFile(filePath) {
|
|
11
|
+
let content;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
content = await readFile(filePath, 'utf-8');
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error.code === 'ENOENT') {
|
|
17
|
+
throw new Error(`Environment file not found: ${filePath}`);
|
|
18
|
+
} else if (error.code === 'EACCES' || error.code === 'EPERM') {
|
|
19
|
+
throw new Error(`Permission denied reading file: ${filePath}`);
|
|
20
|
+
} else if (error.code === 'EISDIR') {
|
|
21
|
+
throw new Error(`Path is a directory, not a file: ${filePath}`);
|
|
22
|
+
} else {
|
|
23
|
+
throw new Error(`Error reading file ${filePath}: ${error.message}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Handle both Unix (\n) and Windows (\r\n) line endings
|
|
28
|
+
const lines = content.split(/\r?\n/);
|
|
29
|
+
const definitions = [];
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < lines.length; i++) {
|
|
32
|
+
const line = lines[i];
|
|
33
|
+
const lineNumber = i + 1;
|
|
34
|
+
|
|
35
|
+
// Skip empty lines and comment-only lines
|
|
36
|
+
const trimmed = line.trim();
|
|
37
|
+
if (trimmed === '' || trimmed.startsWith('#')) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Parse variable definition line
|
|
42
|
+
const definition = parseEnvLine(line, lineNumber);
|
|
43
|
+
if (definition) {
|
|
44
|
+
definitions.push(definition);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return definitions;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parses a single line from a .env file.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} line - The line to parse
|
|
55
|
+
* @param {number} lineNumber - The line number (1-indexed)
|
|
56
|
+
* @returns {{varName: string, hasComment: boolean, comment: string|null, lineNumber: number}|null}
|
|
57
|
+
*/
|
|
58
|
+
export function parseEnvLine(line, lineNumber) {
|
|
59
|
+
// Match pattern: VAR_NAME=value # optional comment
|
|
60
|
+
// Variable names must start with letter or underscore, followed by letters, digits, or underscores
|
|
61
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
|
|
62
|
+
|
|
63
|
+
if (!match) {
|
|
64
|
+
// Malformed line - skip gracefully
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const varName = match[1];
|
|
69
|
+
const remainder = match[2];
|
|
70
|
+
|
|
71
|
+
// Extract inline comment if present
|
|
72
|
+
const comment = extractComment(remainder);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
varName,
|
|
76
|
+
hasComment: comment !== null,
|
|
77
|
+
comment,
|
|
78
|
+
lineNumber
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Extracts an inline comment from the value portion of an env line.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} value - The value portion after the = sign
|
|
86
|
+
* @returns {string|null} - The comment text (without # prefix) or null if no comment
|
|
87
|
+
*/
|
|
88
|
+
export function extractComment(value) {
|
|
89
|
+
// Find the first # that indicates a comment
|
|
90
|
+
// Note: This is a simple implementation that doesn't handle # inside quoted strings
|
|
91
|
+
const commentIndex = value.indexOf('#');
|
|
92
|
+
|
|
93
|
+
if (commentIndex === -1) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Extract and trim the comment text
|
|
98
|
+
const commentText = value.substring(commentIndex + 1).trim();
|
|
99
|
+
|
|
100
|
+
return commentText || null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validates if a line is a valid env variable definition.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} line - The line to validate
|
|
107
|
+
* @returns {boolean}
|
|
108
|
+
*/
|
|
109
|
+
export function isValidEnvLine(line) {
|
|
110
|
+
const trimmed = line.trim();
|
|
111
|
+
|
|
112
|
+
// Empty lines and comments are not valid env lines
|
|
113
|
+
if (trimmed === '' || trimmed.startsWith('#')) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check if it matches the VAR_NAME=value pattern
|
|
118
|
+
return /^[A-Z_][A-Z0-9_]*=/.test(trimmed);
|
|
119
|
+
}
|
package/src/plugins.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin system for extensibility
|
|
3
|
+
* Allows users to add custom scanners, formatters, and validators
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Plugin manager
|
|
11
|
+
*/
|
|
12
|
+
export class PluginManager {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.plugins = new Map();
|
|
15
|
+
this.hooks = new Map();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Register a plugin
|
|
20
|
+
*/
|
|
21
|
+
register(name, plugin) {
|
|
22
|
+
if (this.plugins.has(name)) {
|
|
23
|
+
throw new Error(`Plugin ${name} is already registered`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this.plugins.set(name, plugin);
|
|
27
|
+
|
|
28
|
+
// Initialize plugin
|
|
29
|
+
if (plugin.init) {
|
|
30
|
+
plugin.init(this);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get a plugin by name
|
|
38
|
+
*/
|
|
39
|
+
get(name) {
|
|
40
|
+
return this.plugins.get(name);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if a plugin is registered
|
|
45
|
+
*/
|
|
46
|
+
has(name) {
|
|
47
|
+
return this.plugins.has(name);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Register a hook
|
|
52
|
+
*/
|
|
53
|
+
hook(event, callback) {
|
|
54
|
+
if (!this.hooks.has(event)) {
|
|
55
|
+
this.hooks.set(event, []);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.hooks.get(event).push(callback);
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Trigger a hook
|
|
64
|
+
*/
|
|
65
|
+
async trigger(event, data) {
|
|
66
|
+
const callbacks = this.hooks.get(event) || [];
|
|
67
|
+
|
|
68
|
+
for (const callback of callbacks) {
|
|
69
|
+
try {
|
|
70
|
+
await callback(data);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.warn(`Hook ${event} failed: ${error.message}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Load plugins from directory
|
|
79
|
+
*/
|
|
80
|
+
async loadFromDirectory(dir) {
|
|
81
|
+
if (!existsSync(dir)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const { readdirSync } = await import('fs');
|
|
87
|
+
const files = readdirSync(dir);
|
|
88
|
+
|
|
89
|
+
for (const file of files) {
|
|
90
|
+
if (file.endsWith('.js')) {
|
|
91
|
+
const pluginPath = join(dir, file);
|
|
92
|
+
const plugin = await import(pluginPath);
|
|
93
|
+
const name = file.replace('.js', '');
|
|
94
|
+
|
|
95
|
+
this.register(name, plugin.default || plugin);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.warn(`Failed to load plugins from ${dir}: ${error.message}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Example plugin structure
|
|
106
|
+
*/
|
|
107
|
+
export const examplePlugin = {
|
|
108
|
+
name: 'example',
|
|
109
|
+
version: '1.0.0',
|
|
110
|
+
|
|
111
|
+
init(manager) {
|
|
112
|
+
// Register hooks
|
|
113
|
+
manager.hook('beforeScan', async (data) => {
|
|
114
|
+
console.log('Before scan hook');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
manager.hook('afterAnalysis', async (data) => {
|
|
118
|
+
console.log('After analysis hook');
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
// Custom scanner
|
|
123
|
+
scanner: {
|
|
124
|
+
extensions: ['.custom'],
|
|
125
|
+
scanLine(line, filePath, lineNumber) {
|
|
126
|
+
// Custom scanning logic
|
|
127
|
+
return [];
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
// Custom formatter
|
|
132
|
+
formatter: {
|
|
133
|
+
format(result, options) {
|
|
134
|
+
// Custom formatting logic
|
|
135
|
+
return '';
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
};
|