ghostbuster-cli 0.2.2__tar.gz → 0.3.0__tar.gz

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.
Files changed (45) hide show
  1. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/CHANGELOG.md +12 -0
  2. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/PKG-INFO +6 -6
  3. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/README.md +5 -5
  4. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/pyproject.toml +1 -1
  5. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/__init__.py +1 -1
  6. ghostbuster_cli-0.3.0/src/ghostbuster/core/dead_imports.py +416 -0
  7. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/core/orphan_files.py +3 -0
  8. ghostbuster_cli-0.3.0/src/ghostbuster/core/phantom_env.py +246 -0
  9. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/fixers/import_fixer.py +26 -0
  10. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/tests/test_cli.py +1 -1
  11. ghostbuster_cli-0.3.0/tests/test_node_scanner.py +191 -0
  12. ghostbuster_cli-0.2.2/src/ghostbuster/core/dead_imports.py +0 -273
  13. ghostbuster_cli-0.2.2/src/ghostbuster/core/phantom_env.py +0 -196
  14. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/.github/workflows/ci.yml +0 -0
  15. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/.github/workflows/publish.yml +0 -0
  16. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/.gitignore +0 -0
  17. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/.pre-commit-hooks.yaml +0 -0
  18. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/CONTRIBUTING.md +0 -0
  19. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/LICENSE +0 -0
  20. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/assets/npm_package_icon.png +0 -0
  21. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/assets/readme_header.png +0 -0
  22. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/assets/terminal_icon.png +0 -0
  23. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/__main__.py +0 -0
  24. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/cli/__init__.py +0 -0
  25. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/cli/app.py +0 -0
  26. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/cli/bust.py +0 -0
  27. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/cli/display.py +0 -0
  28. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/cli/scan.py +0 -0
  29. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/config.py +0 -0
  30. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/core/__init__.py +0 -0
  31. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/core/models.py +0 -0
  32. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/core/scanner.py +0 -0
  33. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/core/scoring.py +0 -0
  34. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/core/zombie_code.py +0 -0
  35. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/fixers/__init__.py +0 -0
  36. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/fixers/env_fixer.py +0 -0
  37. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/fixers/gitignore_fixer.py +0 -0
  38. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/tests/__init__.py +0 -0
  39. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/tests/conftest.py +0 -0
  40. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/tests/test_dead_imports.py +0 -0
  41. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/tests/test_fixers.py +0 -0
  42. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/tests/test_orphan_files.py +0 -0
  43. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/tests/test_phantom_env.py +0 -0
  44. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/tests/test_scoring.py +0 -0
  45. {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/tests/test_zombie_code.py +0 -0
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] - 2026-08-29
11
+
12
+ ### Added
13
+
14
+ - **Node.js, JavaScript, and TypeScript Support**:
15
+ - **Dead Imports**: Scans `package.json` (`dependencies`, `devDependencies`, `peerDependencies`) and verifies against imports in `.js`, `.mjs`, `.cjs`, `.jsx`, `.ts`, `.mts`, `.cts`, `.tsx`, `.vue`, and `.svelte` files.
16
+ - **Scoped Packages & Subpaths**: Supports `@scope/pkg` and `pkg/subpath` imports.
17
+ - **Phantom Env**: Scans `process.env.VAR`, `process.env['VAR']`, `import.meta.env.VITE_VAR`, and `NEXT_PUBLIC_VAR`.
18
+ - **Auto-Fix for `package.json`**: `ghostbuster bust --confirm` automatically removes dead dependencies from `package.json` with preserved JSON formatting.
19
+ - **Orphan Files**: Added detection for Node log files (`npm-debug.log`, `yarn-error.log`, `pnpm-debug.log`).
20
+ - **Full-Stack Scanning**: Simultaneously scans Python backends and JS/TS frontends in monorepos with a unified Ghost Score.
21
+
10
22
  ## [0.2.2] - 2026-08-29
11
23
 
12
24
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ghostbuster-cli
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Find and bust the ghosts haunting your codebase - unused deps, dead code, orphan files, and phantom env vars.
5
5
  Project-URL: Homepage, https://github.com/adewanggar/ghostbuster-cli
6
6
  Project-URL: Documentation, https://github.com/adewanggar/ghostbuster-cli#readme
@@ -225,11 +225,11 @@ Ghostbuster uses **pure AST analysis** - no runtime imports, no code execution,
225
225
 
226
226
  ## Roadmap
227
227
 
228
- - <img src="https://raw.githubusercontent.com/adewanggar/ghostbuster-cli/main/assets/npm_package_icon.png" width="18" height="18" valign="middle" /> Node.js / TypeScript support (scan `package.json`, dead exports)
229
- - Pre-commit hook integration
230
- - GitHub Actions reporter (comment Ghost Score on PRs)
231
- - Config inheritance for monorepos
232
- - Ghost Score history tracking & trend chart
228
+ - [x] <img src="https://raw.githubusercontent.com/adewanggar/ghostbuster-cli/main/assets/npm_package_icon.png" width="18" height="18" valign="middle" /> Node.js / JavaScript / TypeScript support (`package.json`, `process.env`, `import.meta.env`)
229
+ - [x] Pre-commit hook integration (`.pre-commit-hooks.yaml`)
230
+ - [ ] GitHub Actions reporter (comment Ghost Score on PRs)
231
+ - [ ] Config inheritance for monorepos
232
+ - [ ] Ghost Score history tracking & trend chart
233
233
 
234
234
  ## Contributing
235
235
 
@@ -187,11 +187,11 @@ Ghostbuster uses **pure AST analysis** - no runtime imports, no code execution,
187
187
 
188
188
  ## Roadmap
189
189
 
190
- - <img src="https://raw.githubusercontent.com/adewanggar/ghostbuster-cli/main/assets/npm_package_icon.png" width="18" height="18" valign="middle" /> Node.js / TypeScript support (scan `package.json`, dead exports)
191
- - Pre-commit hook integration
192
- - GitHub Actions reporter (comment Ghost Score on PRs)
193
- - Config inheritance for monorepos
194
- - Ghost Score history tracking & trend chart
190
+ - [x] <img src="https://raw.githubusercontent.com/adewanggar/ghostbuster-cli/main/assets/npm_package_icon.png" width="18" height="18" valign="middle" /> Node.js / JavaScript / TypeScript support (`package.json`, `process.env`, `import.meta.env`)
191
+ - [x] Pre-commit hook integration (`.pre-commit-hooks.yaml`)
192
+ - [ ] GitHub Actions reporter (comment Ghost Score on PRs)
193
+ - [ ] Config inheritance for monorepos
194
+ - [ ] Ghost Score history tracking & trend chart
195
195
 
196
196
  ## Contributing
197
197
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ghostbuster-cli"
7
- version = "0.2.2"
7
+ version = "0.3.0"
8
8
  description = "Find and bust the ghosts haunting your codebase - unused deps, dead code, orphan files, and phantom env vars."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,3 +1,3 @@
1
1
  """Ghostbuster — Find and bust the ghosts haunting your codebase."""
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.3.0"
@@ -0,0 +1,416 @@
1
+ """Dead Import Scanner - detects dependencies declared but never imported.
2
+
3
+ Checks requirements.txt, pyproject.toml, and package.json for declared
4
+ dependencies, then walks Python, JavaScript, and TypeScript files to see which
5
+ are actually imported.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import ast
11
+ import json
12
+ import re
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ from ghostbuster.core.models import Ghost, GhostCategory, Severity
17
+
18
+ # Mapping from PyPI package names to their actual import names.
19
+ PACKAGE_TO_IMPORT: dict[str, str | list[str]] = {
20
+ "pillow": "PIL",
21
+ "scikit-learn": "sklearn",
22
+ "scikit-image": "skimage",
23
+ "opencv-python": "cv2",
24
+ "opencv-python-headless": "cv2",
25
+ "python-dateutil": "dateutil",
26
+ "pyyaml": "yaml",
27
+ "beautifulsoup4": "bs4",
28
+ "python-dotenv": "dotenv",
29
+ "pymysql": "pymysql",
30
+ "attrs": "attr",
31
+ "google-cloud-storage": "google.cloud.storage",
32
+ "google-auth": "google.auth",
33
+ "python-jose": "jose",
34
+ "python-multipart": "multipart",
35
+ "ujson": "ujson",
36
+ "msgpack": "msgpack",
37
+ "setuptools": ["setuptools", "pkg_resources"],
38
+ "typing-extensions": "typing_extensions",
39
+ "importlib-metadata": "importlib_metadata",
40
+ "importlib-resources": "importlib_resources",
41
+ }
42
+
43
+ # Python tool packages that are run via CLI or plugins
44
+ TOOL_PACKAGES: set[str] = {
45
+ "pip",
46
+ "setuptools",
47
+ "wheel",
48
+ "build",
49
+ "twine",
50
+ "ruff",
51
+ "black",
52
+ "isort",
53
+ "flake8",
54
+ "mypy",
55
+ "pytest",
56
+ "pytest-cov",
57
+ "pytest-xdist",
58
+ "pytest-asyncio",
59
+ "pre-commit",
60
+ "tox",
61
+ "nox",
62
+ "sphinx",
63
+ "ipython",
64
+ "ipdb",
65
+ "debugpy",
66
+ "hatchling",
67
+ "hatch",
68
+ "poetry",
69
+ "flit",
70
+ "pdm",
71
+ "autopep8",
72
+ "pyright",
73
+ "pylint",
74
+ "bandit",
75
+ "safety",
76
+ "coverage",
77
+ }
78
+
79
+ # Node.js tool/build packages that are run via CLI or config
80
+ NODE_TOOL_PACKAGES: set[str] = {
81
+ "typescript",
82
+ "eslint",
83
+ "prettier",
84
+ "vite",
85
+ "webpack",
86
+ "rollup",
87
+ "tsup",
88
+ "esbuild",
89
+ "jest",
90
+ "vitest",
91
+ "mocha",
92
+ "chai",
93
+ "husky",
94
+ "lint-staged",
95
+ "rimraf",
96
+ "nodemon",
97
+ "concurrently",
98
+ "cross-env",
99
+ "tailwindcss",
100
+ "postcss",
101
+ "autoprefixer",
102
+ "dotenv-cli",
103
+ }
104
+
105
+ # Directories to skip when scanning source files
106
+ SKIP_DIRS: set[str] = {
107
+ "venv",
108
+ ".venv",
109
+ "node_modules",
110
+ ".git",
111
+ "__pycache__",
112
+ ".tox",
113
+ ".nox",
114
+ "build",
115
+ "dist",
116
+ ".next",
117
+ ".nuxt",
118
+ ".turbo",
119
+ "coverage",
120
+ }
121
+
122
+ JS_TS_EXTENSIONS: set[str] = {
123
+ ".js",
124
+ ".mjs",
125
+ ".cjs",
126
+ ".jsx",
127
+ ".ts",
128
+ ".mts",
129
+ ".cts",
130
+ ".tsx",
131
+ ".vue",
132
+ ".svelte",
133
+ }
134
+
135
+
136
+ class DeadImportScanner:
137
+ """Detects dependencies declared in config but never imported in code."""
138
+
139
+ name = "dead-import"
140
+
141
+ def scan(self, path: Path) -> list[Ghost]:
142
+ """Scan a project directory for dead imports across Python and Node.js."""
143
+ declared = self._find_declared_dependencies(path)
144
+ if not declared:
145
+ return []
146
+
147
+ imported = self._find_all_imports(path)
148
+ ghosts: list[Ghost] = []
149
+
150
+ for dep_name, source_file in declared:
151
+ is_node = source_file.startswith("package.json")
152
+
153
+ if is_node:
154
+ # Node.js package handling
155
+ if self._is_node_tool_package(dep_name):
156
+ continue
157
+
158
+ if not self._is_node_dep_used(dep_name, imported):
159
+ ghosts.append(
160
+ Ghost(
161
+ category=GhostCategory.DEAD_IMPORT,
162
+ name=dep_name,
163
+ message=f"Package '{dep_name}' is declared in {source_file} but never imported",
164
+ file_path=path / "package.json",
165
+ severity=Severity.MEDIUM,
166
+ fixable=True,
167
+ suggestion=f"Remove '{dep_name}' from package.json",
168
+ )
169
+ )
170
+ else:
171
+ # Python package handling
172
+ normalized = self._normalize_package_name(dep_name)
173
+ if normalized in TOOL_PACKAGES:
174
+ continue
175
+
176
+ import_names = self._get_import_names(normalized)
177
+ is_used = any(
178
+ self._import_matches(imp, name) for imp in imported for name in import_names
179
+ )
180
+
181
+ if not is_used:
182
+ ghosts.append(
183
+ Ghost(
184
+ category=GhostCategory.DEAD_IMPORT,
185
+ name=dep_name,
186
+ message=f"Package '{dep_name}' is declared in {source_file} but never imported",
187
+ file_path=path / (source_file.split(" [")[0]),
188
+ severity=Severity.MEDIUM,
189
+ fixable=True,
190
+ suggestion=f"Remove '{dep_name}' from {source_file.split(' [')[0]}",
191
+ )
192
+ )
193
+
194
+ return ghosts
195
+
196
+ def _is_node_tool_package(self, pkg_name: str) -> bool:
197
+ """Check if a Node package is a known CLI/type/build tool."""
198
+ if pkg_name.startswith("@types/"):
199
+ return True
200
+ if pkg_name.startswith("@vitejs/") or pkg_name.startswith("@babel/"):
201
+ return True
202
+ return pkg_name.lower() in NODE_TOOL_PACKAGES
203
+
204
+ def _is_node_dep_used(self, dep_name: str, imported: set[str]) -> bool:
205
+ """Check if a declared Node dependency is used in imports."""
206
+ dep_lower = dep_name.lower()
207
+ for imp in imported:
208
+ imp_lower = imp.lower()
209
+ if imp_lower == dep_lower or imp_lower.startswith(dep_lower + "/"):
210
+ return True
211
+ return False
212
+
213
+ def _normalize_package_name(self, name: str) -> str:
214
+ """Normalize a Python package name: lowercase, hyphens to underscores."""
215
+ name = re.split(r"[>=<!\[;@]", name)[0].strip()
216
+ return name.lower().replace("-", "_")
217
+
218
+ def _get_import_names(self, normalized: str) -> list[str]:
219
+ """Get all possible import names for a normalized package name."""
220
+ original = normalized.replace("_", "-")
221
+ if original in PACKAGE_TO_IMPORT:
222
+ mapping = PACKAGE_TO_IMPORT[original]
223
+ if isinstance(mapping, list):
224
+ return [m.lower().replace("-", "_") for m in mapping]
225
+ return [mapping.lower().replace("-", "_")]
226
+
227
+ if normalized in PACKAGE_TO_IMPORT:
228
+ mapping = PACKAGE_TO_IMPORT[normalized]
229
+ if isinstance(mapping, list):
230
+ return [m.lower().replace("-", "_") for m in mapping]
231
+ return [mapping.lower().replace("-", "_")]
232
+
233
+ return [normalized]
234
+
235
+ def _import_matches(self, imported: str, expected: str) -> bool:
236
+ """Check if an imported module matches an expected package name."""
237
+ imported_lower = imported.lower().replace("-", "_")
238
+ expected_lower = expected.lower().replace("-", "_")
239
+ return imported_lower == expected_lower or imported_lower.startswith(expected_lower + ".")
240
+
241
+ def _find_declared_dependencies(self, path: Path) -> list[tuple[str, str]]:
242
+ """Find all declared dependencies from Python and Node.js config files."""
243
+ deps: list[tuple[str, str]] = []
244
+
245
+ # Check requirements.txt
246
+ req_file = path / "requirements.txt"
247
+ if req_file.exists():
248
+ deps.extend(self._parse_requirements_txt(req_file))
249
+
250
+ # Check pyproject.toml
251
+ pyproject = path / "pyproject.toml"
252
+ if pyproject.exists():
253
+ deps.extend(self._parse_pyproject_toml(pyproject))
254
+
255
+ # Check package.json (Node.js)
256
+ package_json = path / "package.json"
257
+ if package_json.exists():
258
+ deps.extend(self._parse_package_json(package_json))
259
+
260
+ return deps
261
+
262
+ def _parse_requirements_txt(self, filepath: Path) -> list[tuple[str, str]]:
263
+ """Parse a requirements.txt file for package names."""
264
+ deps: list[tuple[str, str]] = []
265
+ try:
266
+ for line in filepath.read_text(encoding="utf-8").splitlines():
267
+ line = line.strip()
268
+ if not line or line.startswith("#") or line.startswith("-"):
269
+ continue
270
+ name = re.split(r"[>=<!\[;@]", line)[0].strip()
271
+ if name:
272
+ deps.append((name, "requirements.txt"))
273
+ except OSError:
274
+ pass
275
+ return deps
276
+
277
+ def _parse_pyproject_toml(self, filepath: Path) -> list[tuple[str, str]]:
278
+ """Parse pyproject.toml for declared dependencies."""
279
+ deps: list[tuple[str, str]] = []
280
+ try:
281
+ if sys.version_info >= (3, 11):
282
+ import tomllib
283
+ else:
284
+ import tomli as tomllib # type: ignore[no-redef]
285
+
286
+ data = tomllib.loads(filepath.read_text(encoding="utf-8"))
287
+ project_deps = data.get("project", {}).get("dependencies", [])
288
+ for dep in project_deps:
289
+ name = re.split(r"[>=<!\[;@]", dep)[0].strip()
290
+ if name:
291
+ deps.append((name, "pyproject.toml"))
292
+
293
+ # Also check optional dependencies
294
+ optional = data.get("project", {}).get("optional-dependencies", {})
295
+ for group_name, group_deps in optional.items():
296
+ for dep in group_deps:
297
+ name = re.split(r"[>=<!\[;@]", dep)[0].strip()
298
+ if name:
299
+ deps.append((name, f"pyproject.toml [optional: {group_name}]"))
300
+
301
+ # Support Poetry: [tool.poetry.dependencies]
302
+ poetry_deps = data.get("tool", {}).get("poetry", {}).get("dependencies", {})
303
+ if isinstance(poetry_deps, dict):
304
+ for dep_name in poetry_deps:
305
+ if dep_name.lower() != "python":
306
+ deps.append((dep_name, "pyproject.toml [poetry.dependencies]"))
307
+
308
+ # Support Poetry groups: [tool.poetry.group.<group>.dependencies]
309
+ poetry_groups = data.get("tool", {}).get("poetry", {}).get("group", {})
310
+ if isinstance(poetry_groups, dict):
311
+ for group_name, group_data in poetry_groups.items():
312
+ if isinstance(group_data, dict):
313
+ g_deps = group_data.get("dependencies", {})
314
+ if isinstance(g_deps, dict):
315
+ for dep_name in g_deps:
316
+ if dep_name.lower() != "python":
317
+ deps.append(
318
+ (dep_name, f"pyproject.toml [poetry.group.{group_name}]")
319
+ )
320
+
321
+ except (OSError, Exception):
322
+ pass
323
+ return deps
324
+
325
+ def _parse_package_json(self, filepath: Path) -> list[tuple[str, str]]:
326
+ """Parse package.json for declared dependencies."""
327
+ deps: list[tuple[str, str]] = []
328
+ try:
329
+ data = json.loads(filepath.read_text(encoding="utf-8"))
330
+ for section in ["dependencies", "devDependencies", "peerDependencies"]:
331
+ sec_deps = data.get(section, {})
332
+ if isinstance(sec_deps, dict):
333
+ for dep_name in sec_deps:
334
+ deps.append((dep_name, f"package.json [{section}]"))
335
+ except (OSError, ValueError):
336
+ pass
337
+ return deps
338
+
339
+ def _find_all_imports(self, path: Path) -> set[str]:
340
+ """Walk Python and JS/TS files and collect all imported module/package names."""
341
+ imports: set[str] = set()
342
+
343
+ for file_path in path.rglob("*"):
344
+ if not file_path.is_file():
345
+ continue
346
+
347
+ parts = file_path.relative_to(path).parts
348
+ if any(p in SKIP_DIRS for p in parts):
349
+ continue
350
+
351
+ suffix = file_path.suffix.lower()
352
+
353
+ # Python files
354
+ if suffix == ".py":
355
+ self._extract_python_imports(file_path, imports)
356
+ # JS/TS/Vue/Svelte files
357
+ elif suffix in JS_TS_EXTENSIONS:
358
+ self._extract_js_ts_imports(file_path, imports)
359
+
360
+ return imports
361
+
362
+ def _extract_python_imports(self, filepath: Path, imports: set[str]) -> None:
363
+ """Extract imports from a Python file."""
364
+ try:
365
+ source = filepath.read_text(encoding="utf-8", errors="ignore")
366
+ tree = ast.parse(source, filename=str(filepath))
367
+ for node in ast.walk(tree):
368
+ if isinstance(node, ast.Import):
369
+ for alias in node.names:
370
+ imports.add(alias.name)
371
+ elif isinstance(node, ast.ImportFrom) and node.module:
372
+ imports.add(node.module)
373
+ except (SyntaxError, OSError):
374
+ pass
375
+
376
+ def _extract_js_ts_imports(self, filepath: Path, imports: set[str]) -> None:
377
+ """Extract imports/requires from a JavaScript or TypeScript file."""
378
+ try:
379
+ source = filepath.read_text(encoding="utf-8", errors="ignore")
380
+
381
+ # Patterns:
382
+ # 1. import ... from 'pkg'
383
+ # 2. import 'pkg'
384
+ # 3. export ... from 'pkg'
385
+ # 4. require('pkg')
386
+ # 5. import('pkg')
387
+ patterns = [
388
+ r"""(?:import|export)\s+(?:[\w\s{},*]+from\s+)?['"]([^'"]+)['"]""",
389
+ r"""(?:require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)""",
390
+ ]
391
+
392
+ for pattern in patterns:
393
+ for match in re.finditer(pattern, source):
394
+ raw_specifier = match.group(1).strip()
395
+ # Skip relative or absolute paths
396
+ if (
397
+ raw_specifier.startswith(".")
398
+ or raw_specifier.startswith("/")
399
+ or raw_specifier.startswith("#")
400
+ ):
401
+ continue
402
+
403
+ # Handle scoped package: @org/pkg/sub -> @org/pkg
404
+ if raw_specifier.startswith("@"):
405
+ parts = raw_specifier.split("/")
406
+ if len(parts) >= 2:
407
+ imports.add(f"{parts[0]}/{parts[1]}")
408
+ else:
409
+ imports.add(raw_specifier)
410
+ else:
411
+ # Unscoped package: pkg/sub -> pkg
412
+ pkg_root = raw_specifier.split("/")[0]
413
+ imports.add(pkg_root)
414
+
415
+ except OSError:
416
+ pass
@@ -47,6 +47,9 @@ IGNORABLE_PATTERNS: dict[str, str] = {
47
47
  "Thumbs.db": "Windows thumbnail cache",
48
48
  ".env": "Environment variables file (may contain secrets)",
49
49
  ".env.local": "Local environment variables",
50
+ "npm-debug.log": "NPM debug log file",
51
+ "yarn-error.log": "Yarn error log file",
52
+ "pnpm-debug.log": "pnpm debug log file",
50
53
  }
51
54
 
52
55
  # Size threshold for flagging large files (10 MB)