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/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
+ };