helpforbegs 0.0.0-stage → 1.0.1

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.
@@ -0,0 +1,351 @@
1
+ // src/configReader.js
2
+ // WHY: Packages don't just live in require() or import statements.
3
+ // Many critical packages are declared inside configuration files (ESLint plugins,
4
+ // Babel presets, Prettier plugins, tsconfig compiler types, and peerDependencies).
5
+ // Scanning configs ensures beginners never accidentally uninstall tools their configs rely on.
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ /**
11
+ * Safely parses JSON files, stripping single-line and multi-line comments
12
+ * which are common in JSONC files like tsconfig.json and .eslintrc.
13
+ *
14
+ * @param {string} filePath
15
+ * @returns {any|null}
16
+ */
17
+ function readJsonWithComments(filePath) {
18
+ if (!fs.existsSync(filePath)) {
19
+ return null;
20
+ }
21
+ try {
22
+ const raw = fs.readFileSync(filePath, 'utf8');
23
+ // Remove single-line comments (//...) and multi-line comments (/*...*/)
24
+ const cleanJson = raw
25
+ .replace(/\/\*[\s\S]*?\*\//g, '')
26
+ .replace(/\/\/.*/g, '');
27
+ return JSON.parse(cleanJson);
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Extracts custom path aliases from tsconfig.json / jsconfig.json and package.json imports.
35
+ * e.g. "@/*" -> "@", "~/components/*" -> "~"
36
+ *
37
+ * @param {string} projectDir
38
+ * @param {object} [pkgJson]
39
+ * @returns {Set<string>}
40
+ */
41
+ function extractPathAliases(projectDir, pkgJson = {}) {
42
+ const aliases = new Set();
43
+
44
+ // 1. From tsconfig.json or jsconfig.json
45
+ const tsconfigPath = path.join(projectDir, 'tsconfig.json');
46
+ const jsconfigPath = path.join(projectDir, 'jsconfig.json');
47
+
48
+ const config = readJsonWithComments(tsconfigPath) || readJsonWithComments(jsconfigPath);
49
+ if (config && config.compilerOptions && config.compilerOptions.paths) {
50
+ for (const pattern of Object.keys(config.compilerOptions.paths)) {
51
+ // e.g. "@/*" -> "@", "utils/*" -> "utils"
52
+ const clean = pattern.replace(/\/\*$/, '').replace(/\*$/, '');
53
+ if (clean) {
54
+ aliases.add(clean);
55
+ }
56
+ }
57
+ }
58
+
59
+ // 2. From package.json "imports" field (Node.js subpath imports like "#internal/*")
60
+ if (pkgJson && pkgJson.imports && typeof pkgJson.imports === 'object') {
61
+ for (const key of Object.keys(pkgJson.imports)) {
62
+ const clean = key.replace(/\/\*$/, '').replace(/\*$/, '');
63
+ if (clean) {
64
+ aliases.add(clean);
65
+ }
66
+ }
67
+ }
68
+
69
+ return aliases;
70
+ }
71
+
72
+ /**
73
+ * Reads the user's .helpforbegsignore file if present.
74
+ * Lines starting with # are comments.
75
+ *
76
+ * @param {string} projectDir
77
+ * @returns {Set<string>}
78
+ */
79
+ function readIgnoreFile(projectDir) {
80
+ const ignoreFilePath = path.join(projectDir, '.helpforbegsignore');
81
+ const ignored = new Set();
82
+
83
+ if (!fs.existsSync(ignoreFilePath)) {
84
+ return ignored;
85
+ }
86
+
87
+ try {
88
+ const content = fs.readFileSync(ignoreFilePath, 'utf8');
89
+ const lines = content.split(/\r?\n/);
90
+ for (const rawLine of lines) {
91
+ const line = rawLine.trim();
92
+ if (!line || line.startsWith('#')) {
93
+ continue;
94
+ }
95
+ ignored.add(line);
96
+ }
97
+ } catch {
98
+ // Ignore read errors
99
+ }
100
+
101
+ return ignored;
102
+ }
103
+
104
+ /**
105
+ * Reads node_modules to find all peerDependencies declared by installed packages.
106
+ *
107
+ * @param {string} projectDir
108
+ * @returns {Set<string>}
109
+ */
110
+ function collectPeerDependencies(projectDir) {
111
+ const peers = new Set();
112
+ const nodeModulesPath = path.join(projectDir, 'node_modules');
113
+
114
+ if (!fs.existsSync(nodeModulesPath)) {
115
+ return peers;
116
+ }
117
+
118
+ function readPeers(pkgJsonPath) {
119
+ try {
120
+ if (fs.existsSync(pkgJsonPath)) {
121
+ const data = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
122
+ if (data.peerDependencies && typeof data.peerDependencies === 'object') {
123
+ for (const peerName of Object.keys(data.peerDependencies)) {
124
+ peers.add(peerName);
125
+ }
126
+ }
127
+ }
128
+ } catch {
129
+ // Ignore unparseable files
130
+ }
131
+ }
132
+
133
+ try {
134
+ const entries = fs.readdirSync(nodeModulesPath, { withFileTypes: true });
135
+ for (const entry of entries) {
136
+ if (!entry.isDirectory()) continue;
137
+
138
+ if (entry.name.startsWith('@')) {
139
+ const scopePath = path.join(nodeModulesPath, entry.name);
140
+ try {
141
+ const scopedEntries = fs.readdirSync(scopePath, { withFileTypes: true });
142
+ for (const scoped of scopedEntries) {
143
+ if (scoped.isDirectory()) {
144
+ readPeers(path.join(scopePath, scoped.name, 'package.json'));
145
+ }
146
+ }
147
+ } catch {
148
+ // Skip unreadable scope
149
+ }
150
+ } else if (!entry.name.startsWith('.')) {
151
+ readPeers(path.join(nodeModulesPath, entry.name, 'package.json'));
152
+ }
153
+ }
154
+ } catch {
155
+ return peers;
156
+ }
157
+
158
+ return peers;
159
+ }
160
+
161
+ /**
162
+ * Scans ESLint, Prettier, Babel, PostCSS, and TypeScript configuration files
163
+ * for declared packages, plugins, presets, and extends.
164
+ *
165
+ * @param {string} projectDir
166
+ * @returns {{ usedPackages: Set<string>, hasTsConfig: boolean }}
167
+ */
168
+ function scanConfigFiles(projectDir) {
169
+ const usedPackages = new Set();
170
+ let hasTsConfig = false;
171
+
172
+ // 1. TypeScript tsconfig.json / jsconfig.json
173
+ const tsconfigPath = path.join(projectDir, 'tsconfig.json');
174
+ if (fs.existsSync(tsconfigPath)) {
175
+ hasTsConfig = true;
176
+ const tsconfig = readJsonWithComments(tsconfigPath);
177
+ if (tsconfig) {
178
+ // Extends: e.g. "@tsconfig/node18/tsconfig.json" -> "@tsconfig/node18"
179
+ if (typeof tsconfig.extends === 'string') {
180
+ const ext = tsconfig.extends;
181
+ if (!ext.startsWith('.')) {
182
+ const parts = ext.split('/');
183
+ const pkg = ext.startsWith('@') ? `${parts[0]}/${parts[1]}` : parts[0];
184
+ usedPackages.add(pkg);
185
+ }
186
+ }
187
+
188
+ // Types: e.g. ["jest", "node"] -> "@types/jest", "@types/node"
189
+ if (tsconfig.compilerOptions && Array.isArray(tsconfig.compilerOptions.types)) {
190
+ for (const typeItem of tsconfig.compilerOptions.types) {
191
+ usedPackages.add(typeItem);
192
+ usedPackages.add(`@types/${typeItem}`);
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ // 2. ESLint configs (.eslintrc, .eslintrc.json, .eslintrc.js, etc.)
199
+ const eslintFiles = [
200
+ '.eslintrc',
201
+ '.eslintrc.json',
202
+ '.eslintrc.js',
203
+ '.eslintrc.cjs',
204
+ '.eslintrc.yaml',
205
+ '.eslintrc.yml',
206
+ 'eslint.config.js',
207
+ 'eslint.config.mjs',
208
+ ];
209
+
210
+ for (const name of eslintFiles) {
211
+ const fullPath = path.join(projectDir, name);
212
+ if (!fs.existsSync(fullPath)) continue;
213
+
214
+ // For JS configs, they will also be parsed by fileFinder + astParser,
215
+ // but reading the raw text or JSON captures plugin strings too
216
+ try {
217
+ const content = fs.readFileSync(fullPath, 'utf8');
218
+ const json = name.endsWith('.json') || name === '.eslintrc' ? readJsonWithComments(fullPath) : null;
219
+
220
+ if (json) {
221
+ // Handle extends
222
+ const rawExtends = Array.isArray(json.extends) ? json.extends : [json.extends].filter(Boolean);
223
+ for (const ext of rawExtends) {
224
+ if (typeof ext !== 'string') continue;
225
+ if (ext.startsWith('plugin:')) {
226
+ const pluginPart = ext.slice('plugin:'.length).split('/')[0];
227
+ if (pluginPart.startsWith('@')) {
228
+ usedPackages.add(`${pluginPart}/eslint-plugin`);
229
+ } else {
230
+ usedPackages.add(`eslint-plugin-${pluginPart}`);
231
+ }
232
+ } else if (!ext.startsWith('eslint:') && !ext.startsWith('.')) {
233
+ // e.g. "airbnb" -> "eslint-config-airbnb", "prettier" -> "eslint-config-prettier"
234
+ if (!ext.startsWith('eslint-config-') && !ext.includes('/')) {
235
+ usedPackages.add(`eslint-config-${ext}`);
236
+ } else {
237
+ usedPackages.add(ext);
238
+ }
239
+ }
240
+ }
241
+
242
+ // Handle plugins: e.g. ["react", "@typescript-eslint"]
243
+ if (Array.isArray(json.plugins)) {
244
+ for (const plugin of json.plugins) {
245
+ if (typeof plugin !== 'string') continue;
246
+ if (plugin.startsWith('@')) {
247
+ usedPackages.add(`${plugin}/eslint-plugin`);
248
+ } else if (!plugin.startsWith('eslint-plugin-')) {
249
+ usedPackages.add(`eslint-plugin-${plugin}`);
250
+ } else {
251
+ usedPackages.add(plugin);
252
+ }
253
+ }
254
+ }
255
+ } else {
256
+ // Regex search for plugin/config package names inside JS/YAML eslint configs
257
+ const pluginRegex = /['"`](eslint-(?:config|plugin)-[^'"`]+|@[^'"`]+\/eslint-(?:config|plugin)[^'"`]*)['"`]/g;
258
+ let match;
259
+ while ((match = pluginRegex.exec(content)) !== null) {
260
+ usedPackages.add(match[1]);
261
+ }
262
+ }
263
+ } catch {
264
+ // Ignore config read errors
265
+ }
266
+ }
267
+
268
+ // 3. Babel configs (.babelrc, babel.config.json, babel.config.js)
269
+ const babelFiles = ['.babelrc', '.babelrc.json', 'babel.config.json', 'babel.config.js'];
270
+ for (const name of babelFiles) {
271
+ const fullPath = path.join(projectDir, name);
272
+ if (!fs.existsSync(fullPath)) continue;
273
+
274
+ const json = readJsonWithComments(fullPath);
275
+ if (json) {
276
+ const presets = Array.isArray(json.presets) ? json.presets : [];
277
+ for (const preset of presets) {
278
+ const presetName = Array.isArray(preset) ? preset[0] : preset;
279
+ if (typeof presetName === 'string') {
280
+ if (presetName.startsWith('@')) {
281
+ usedPackages.add(presetName);
282
+ } else if (!presetName.startsWith('babel-preset-') && !presetName.startsWith('.')) {
283
+ usedPackages.add(`babel-preset-${presetName}`);
284
+ } else {
285
+ usedPackages.add(presetName);
286
+ }
287
+ }
288
+ }
289
+
290
+ const plugins = Array.isArray(json.plugins) ? json.plugins : [];
291
+ for (const plugin of plugins) {
292
+ const pluginName = Array.isArray(plugin) ? plugin[0] : plugin;
293
+ if (typeof pluginName === 'string') {
294
+ if (pluginName.startsWith('@')) {
295
+ usedPackages.add(pluginName);
296
+ } else if (!pluginName.startsWith('babel-plugin-') && !pluginName.startsWith('.')) {
297
+ usedPackages.add(`babel-plugin-${pluginName}`);
298
+ } else {
299
+ usedPackages.add(pluginName);
300
+ }
301
+ }
302
+ }
303
+ }
304
+ }
305
+
306
+ // 4. Prettier configs (.prettierrc, .prettierrc.json)
307
+ const prettierFiles = ['.prettierrc', '.prettierrc.json'];
308
+ for (const name of prettierFiles) {
309
+ const fullPath = path.join(projectDir, name);
310
+ if (!fs.existsSync(fullPath)) continue;
311
+ const json = readJsonWithComments(fullPath);
312
+ if (json && Array.isArray(json.plugins)) {
313
+ for (const plug of json.plugins) {
314
+ if (typeof plug === 'string') {
315
+ usedPackages.add(plug);
316
+ }
317
+ }
318
+ }
319
+ }
320
+
321
+ return {
322
+ usedPackages,
323
+ hasTsConfig,
324
+ };
325
+ }
326
+
327
+ /**
328
+ * Checks whether dotenv is invoked via CLI flags in package.json scripts
329
+ * e.g. "-r dotenv/config" or "node --require dotenv/config app.js"
330
+ *
331
+ * @param {Record<string, string>} scripts
332
+ * @returns {boolean}
333
+ */
334
+ function checkDotenvInScripts(scripts = {}) {
335
+ const dotenvFlagRegex = /(?:-r|--require)\s+dotenv(?:\/config)?/;
336
+ for (const cmd of Object.values(scripts)) {
337
+ if (typeof cmd === 'string' && dotenvFlagRegex.test(cmd)) {
338
+ return true;
339
+ }
340
+ }
341
+ return false;
342
+ }
343
+
344
+ module.exports = {
345
+ readJsonWithComments,
346
+ extractPathAliases,
347
+ readIgnoreFile,
348
+ collectPeerDependencies,
349
+ scanConfigFiles,
350
+ checkDotenvInScripts,
351
+ };
@@ -0,0 +1,130 @@
1
+ // src/fallbackParser.js
2
+ // WHY: In the real world, developers might have files with experimental syntax,
3
+ // syntax typos, or custom template tags that strict AST parsers choke on.
4
+ // Instead of crashing the whole CLI, we fall back to a line-by-line regex scanner
5
+ // for that file, and flag a gentle note in the report: "Could not fully read <file>".
6
+
7
+ const { builtinModules } = require('module');
8
+ const path = require('path');
9
+
10
+ const BUILTINS = new Set(builtinModules);
11
+
12
+ /**
13
+ * Normalizes a raw module specifier into a clean npm package name.
14
+ * Returns null if it is a relative file, built-in, or alias.
15
+ *
16
+ * @param {string} rawSpecifier - e.g. "lodash/debounce", "./utils", "node:fs"
17
+ * @param {Set<string>} [knownAliases=new Set()]
18
+ * @returns {string|null}
19
+ */
20
+ function normalizePackageName(rawSpecifier, knownAliases = new Set()) {
21
+ if (!rawSpecifier || typeof rawSpecifier !== 'string') {
22
+ return null;
23
+ }
24
+
25
+ const trimmed = rawSpecifier.trim();
26
+
27
+ // Skip relative paths (./foo, ../bar) and root-relative/absolute paths
28
+ if (trimmed.startsWith('.') || trimmed.startsWith('/') || path.isAbsolute(trimmed)) {
29
+ return null;
30
+ }
31
+
32
+ // Skip common framework path aliases (@/components, ~/utils, #internal)
33
+ if (trimmed.startsWith('@/') || trimmed.startsWith('~/') || trimmed.startsWith('#')) {
34
+ return null;
35
+ }
36
+
37
+ // Skip custom tsconfig/jsconfig aliases passed in
38
+ for (const alias of knownAliases) {
39
+ if (trimmed === alias || trimmed.startsWith(alias + '/')) {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ // Any specifier starting with the 'node:' prefix (e.g. node:fs, node:test)
45
+ // is reserved by Node.js for built-in modules and is never an npm package.
46
+ if (trimmed.startsWith('node:')) {
47
+ return null;
48
+ }
49
+
50
+ // Skip Node.js built-in modules without prefix (fs, path, crypto, http, etc.)
51
+ if (BUILTINS.has(trimmed)) {
52
+ return null;
53
+ }
54
+
55
+ const withoutNodePrefix = trimmed;
56
+
57
+ // Scoped packages: @scope/pkg/deep -> @scope/pkg
58
+ if (withoutNodePrefix.startsWith('@')) {
59
+ const parts = withoutNodePrefix.split('/');
60
+ if (parts.length >= 2) {
61
+ return `${parts[0]}/${parts[1]}`;
62
+ }
63
+ return withoutNodePrefix;
64
+ }
65
+
66
+ // Regular packages: lodash/fp -> lodash
67
+ return withoutNodePrefix.split('/')[0];
68
+ }
69
+
70
+ /**
71
+ * Fallback regex scanner for files that fail to parse with Babel.
72
+ *
73
+ * @param {string} code - File content
74
+ * @param {string} filePath - Path for reporting line numbers
75
+ * @param {object} [options]
76
+ * @param {Set<string>} [options.knownAliases]
77
+ * @returns {Array<{ packageName: string, rawSpecifier: string, line: number, isTypeOnly: boolean }>}
78
+ */
79
+ function scanWithRegex(code, filePath, options = {}) {
80
+ const knownAliases = options.knownAliases || new Set();
81
+ const results = [];
82
+ const lines = code.split(/\r?\n/);
83
+
84
+ // Regexes matching common import/require patterns
85
+ const requireRegex = /\brequire\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
86
+ const requireResolveRegex = /\brequire\.resolve\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
87
+ const dynamicImportRegex = /\bimport\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
88
+ const staticImportRegex = /\bimport\s+(?:type\s+)?(?:[\s\S]*?from\s+)?['"`]([^'"`]+)['"`]/g;
89
+ const exportFromRegex = /\bexport\s+(?:type\s+)?(?:[\s\S]*?from\s+)?['"`]([^'"`]+)['"`]/g;
90
+
91
+ lines.forEach((lineText, index) => {
92
+ const lineNumber = index + 1;
93
+
94
+ // Check each pattern on this line
95
+ const patterns = [
96
+ { regex: requireRegex, isTypeOnly: false },
97
+ { regex: requireResolveRegex, isTypeOnly: false },
98
+ { regex: dynamicImportRegex, isTypeOnly: false },
99
+ { regex: staticImportRegex, isTypeOnly: lineText.includes('import type') },
100
+ { regex: exportFromRegex, isTypeOnly: lineText.includes('export type') },
101
+ ];
102
+
103
+ for (const { regex, isTypeOnly } of patterns) {
104
+ // Reset regex index for each line
105
+ regex.lastIndex = 0;
106
+ let match;
107
+ while ((match = regex.exec(lineText)) !== null) {
108
+ const rawSpecifier = match[1];
109
+ const packageName = normalizePackageName(rawSpecifier, knownAliases);
110
+
111
+ if (packageName) {
112
+ results.push({
113
+ packageName,
114
+ rawSpecifier,
115
+ line: lineNumber,
116
+ isTypeOnly,
117
+ });
118
+ }
119
+ }
120
+ }
121
+ });
122
+
123
+ return results;
124
+ }
125
+
126
+ module.exports = {
127
+ scanWithRegex,
128
+ normalizePackageName,
129
+ BUILTINS,
130
+ };
@@ -0,0 +1,196 @@
1
+ // src/fileFinder.js
2
+ // WHY: To analyze a project's dependencies accurately, we must discover every
3
+ // single JavaScript/TypeScript file where code might be imported, while skipping
4
+ // heavy dependency folders (like node_modules) and generated build outputs (dist/).
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ // 1 Megabyte in bytes. We skip files larger than this to prevent memory spikes
10
+ // and keep scanning blazing fast, especially if someone accidentally committed a minified bundle.
11
+ const MAX_FILE_SIZE_BYTES = 1024 * 1024; // 1 MB
12
+
13
+ // Folders that should always be ignored.
14
+ // Scanning these would produce false positives or freeze the CLI.
15
+ const DEFAULT_IGNORED_DIRS = new Set([
16
+ 'node_modules',
17
+ '.git',
18
+ 'dist',
19
+ 'build',
20
+ 'coverage',
21
+ '.next',
22
+ '.nuxt',
23
+ 'out',
24
+ '.cache',
25
+ '.turbo',
26
+ 'fixtures',
27
+ '__fixtures__',
28
+ ]);
29
+
30
+ // Extensions we want to inspect for imports and requires.
31
+ const SUPPORTED_EXTENSIONS = new Set([
32
+ '.js',
33
+ '.mjs',
34
+ '.cjs',
35
+ '.jsx',
36
+ '.ts',
37
+ '.tsx',
38
+ ]);
39
+
40
+ /**
41
+ * Parses a .gitignore file into a list of pattern matchers.
42
+ * Supports basic gitignore patterns (*, /dir, dir/, filename).
43
+ *
44
+ * @param {string} rootDir
45
+ * @returns {Array<(relPath: string, isDirectory: boolean) => boolean>}
46
+ */
47
+ function loadGitignoreMatchers(rootDir) {
48
+ const gitignorePath = path.join(rootDir, '.gitignore');
49
+ if (!fs.existsSync(gitignorePath)) {
50
+ return [];
51
+ }
52
+
53
+ try {
54
+ const content = fs.readFileSync(gitignorePath, 'utf8');
55
+ const lines = content.split(/\r?\n/);
56
+ const matchers = [];
57
+
58
+ for (const rawLine of lines) {
59
+ const line = rawLine.trim();
60
+ // Ignore empty lines and comments
61
+ if (!line || line.startsWith('#')) {
62
+ continue;
63
+ }
64
+
65
+ const isDirOnly = line.endsWith('/');
66
+ let cleanPattern = line.replace(/\/+$/, ''); // Remove trailing slashes
67
+ const isRootAnchored = cleanPattern.startsWith('/');
68
+ if (isRootAnchored) {
69
+ cleanPattern = cleanPattern.slice(1);
70
+ }
71
+
72
+ // Convert a simple glob (*, ?) into a regular expression
73
+ // e.g. *.log -> ^.*\.log$
74
+ const escaped = cleanPattern
75
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
76
+ .replace(/\*/g, '.*')
77
+ .replace(/\?/g, '.');
78
+
79
+ const regex = new RegExp(isRootAnchored ? `^${escaped}` : `(^|/)${escaped}(/|$)`);
80
+
81
+ matchers.push((relPath, isDirectory) => {
82
+ // Normalize Windows backslashes to forward slashes for matching
83
+ const normalized = relPath.replace(/\\/g, '/');
84
+ if (isDirOnly && !isDirectory) {
85
+ return false;
86
+ }
87
+ return regex.test(normalized);
88
+ });
89
+ }
90
+
91
+ return matchers;
92
+ } catch {
93
+ // If .gitignore cannot be read, continue without crashing
94
+ return [];
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Scans a folder recursively to find all source code files.
100
+ *
101
+ * @param {string} rootDir - Absolute path to the project directory to scan
102
+ * @param {object} options
103
+ * @param {string[]} [options.extraIgnores=[]] - Additional patterns or folders to ignore
104
+ * @returns {{ files: Array<{ fullPath: string, relativePath: string, ext: string }>, skippedLargeFiles: Array<{ fullPath: string, relativePath: string, size: number }> }}
105
+ */
106
+ function findFiles(rootDir, options = {}) {
107
+ const files = [];
108
+ const skippedLargeFiles = [];
109
+ const gitignoreMatchers = loadGitignoreMatchers(rootDir);
110
+
111
+ function walk(currentDir) {
112
+ let entries;
113
+ try {
114
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
115
+ } catch {
116
+ // Permission error or unreadable directory; skip gracefully
117
+ return;
118
+ }
119
+
120
+ for (const entry of entries) {
121
+ const fullPath = path.join(currentDir, entry.name);
122
+ const relativePath = path.relative(rootDir, fullPath);
123
+
124
+ // Check standard ignored folders
125
+ if (entry.isDirectory()) {
126
+ if (DEFAULT_IGNORED_DIRS.has(entry.name)) {
127
+ continue;
128
+ }
129
+
130
+ // Check gitignore for directory
131
+ const isIgnoredByGitignore = gitignoreMatchers.some((matcher) =>
132
+ matcher(relativePath, true)
133
+ );
134
+ if (isIgnoredByGitignore) {
135
+ continue;
136
+ }
137
+
138
+ // Recurse into subdirectory
139
+ walk(fullPath);
140
+ } else if (entry.isFile()) {
141
+ const ext = path.extname(entry.name).toLowerCase();
142
+
143
+ // Check if the file extension is one of our target JS/TS code formats.
144
+ // NOTE: Config files like `vite.config.js` or `.eslintrc.js` end in .js,
145
+ // so they are intentionally included here so their imports count as usage!
146
+ if (!SUPPORTED_EXTENSIONS.has(ext)) {
147
+ continue;
148
+ }
149
+
150
+ // Check gitignore for file
151
+ const isIgnoredByGitignore = gitignoreMatchers.some((matcher) =>
152
+ matcher(relativePath, false)
153
+ );
154
+ if (isIgnoredByGitignore) {
155
+ continue;
156
+ }
157
+
158
+ // Check file size to avoid processing massive files
159
+ try {
160
+ const stats = fs.statSync(fullPath);
161
+ if (stats.size > MAX_FILE_SIZE_BYTES) {
162
+ skippedLargeFiles.push({
163
+ fullPath,
164
+ relativePath,
165
+ size: stats.size,
166
+ });
167
+ continue;
168
+ }
169
+ } catch {
170
+ // If stat fails (e.g. broken symlink), skip safely
171
+ continue;
172
+ }
173
+
174
+ files.push({
175
+ fullPath,
176
+ relativePath,
177
+ ext,
178
+ });
179
+ }
180
+ }
181
+ }
182
+
183
+ walk(rootDir);
184
+
185
+ return {
186
+ files,
187
+ skippedLargeFiles,
188
+ };
189
+ }
190
+
191
+ module.exports = {
192
+ findFiles,
193
+ MAX_FILE_SIZE_BYTES,
194
+ DEFAULT_IGNORED_DIRS,
195
+ SUPPORTED_EXTENSIONS,
196
+ };