code-explore 0.1.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 (36) hide show
  1. code_explore-0.1.0/.editorconfig +104 -0
  2. code_explore-0.1.0/.github/workflows/publish.yml +35 -0
  3. code_explore-0.1.0/.gitignore +45 -0
  4. code_explore-0.1.0/PKG-INFO +67 -0
  5. code_explore-0.1.0/README.md +31 -0
  6. code_explore-0.1.0/code_explore/__init__.py +3 -0
  7. code_explore-0.1.0/code_explore/analyzer/__init__.py +13 -0
  8. code_explore-0.1.0/code_explore/analyzer/dependencies.py +328 -0
  9. code_explore-0.1.0/code_explore/analyzer/language.py +240 -0
  10. code_explore-0.1.0/code_explore/analyzer/metrics.py +144 -0
  11. code_explore-0.1.0/code_explore/analyzer/patterns.py +371 -0
  12. code_explore-0.1.0/code_explore/api/__init__.py +1 -0
  13. code_explore-0.1.0/code_explore/api/main.py +197 -0
  14. code_explore-0.1.0/code_explore/cli/__init__.py +1 -0
  15. code_explore-0.1.0/code_explore/cli/main.py +557 -0
  16. code_explore-0.1.0/code_explore/database.py +207 -0
  17. code_explore-0.1.0/code_explore/indexer/__init__.py +1 -0
  18. code_explore-0.1.0/code_explore/indexer/embeddings.py +181 -0
  19. code_explore-0.1.0/code_explore/models.py +106 -0
  20. code_explore-0.1.0/code_explore/scanner/__init__.py +1 -0
  21. code_explore-0.1.0/code_explore/scanner/git_info.py +94 -0
  22. code_explore-0.1.0/code_explore/scanner/local.py +70 -0
  23. code_explore-0.1.0/code_explore/scanner/readme.py +70 -0
  24. code_explore-0.1.0/code_explore/search/__init__.py +1 -0
  25. code_explore-0.1.0/code_explore/search/fulltext.py +137 -0
  26. code_explore-0.1.0/code_explore/search/hybrid.py +92 -0
  27. code_explore-0.1.0/code_explore/search/semantic.py +76 -0
  28. code_explore-0.1.0/code_explore/summarizer/__init__.py +1 -0
  29. code_explore-0.1.0/code_explore/summarizer/ollama.py +130 -0
  30. code_explore-0.1.0/pyproject.toml +66 -0
  31. code_explore-0.1.0/tests/__init__.py +0 -0
  32. code_explore-0.1.0/tests/conftest.py +106 -0
  33. code_explore-0.1.0/tests/test_cli.py +50 -0
  34. code_explore-0.1.0/tests/test_database.py +118 -0
  35. code_explore-0.1.0/tests/test_models.py +90 -0
  36. code_explore-0.1.0/tests/test_search_hybrid.py +95 -0
@@ -0,0 +1,104 @@
1
+ # EditorConfig for EnterpriseApp
2
+ root = true
3
+
4
+ [*]
5
+ indent_style = space
6
+ indent_size = 4
7
+ end_of_line = lf
8
+ charset = utf-8
9
+ trim_trailing_whitespace = true
10
+ insert_final_newline = true
11
+
12
+ [*.{cs,csx}]
13
+ indent_size = 4
14
+
15
+ # C# Code Style
16
+ [*.cs]
17
+ # Organize usings
18
+ dotnet_sort_system_directives_first = true
19
+ dotnet_separate_import_directive_groups = false
20
+
21
+ # this. preferences
22
+ dotnet_style_qualification_for_field = false:suggestion
23
+ dotnet_style_qualification_for_property = false:suggestion
24
+ dotnet_style_qualification_for_method = false:suggestion
25
+ dotnet_style_qualification_for_event = false:suggestion
26
+
27
+ # Language keywords vs BCL types preferences
28
+ dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
29
+ dotnet_style_predefined_type_for_member_access = true:suggestion
30
+
31
+ # var preferences
32
+ csharp_style_var_for_built_in_types = true:suggestion
33
+ csharp_style_var_when_type_is_apparent = true:suggestion
34
+ csharp_style_var_elsewhere = true:suggestion
35
+
36
+ # Expression-bodied members
37
+ csharp_style_expression_bodied_methods = when_on_single_line:suggestion
38
+ csharp_style_expression_bodied_constructors = false:suggestion
39
+ csharp_style_expression_bodied_operators = when_on_single_line:suggestion
40
+ csharp_style_expression_bodied_properties = true:suggestion
41
+ csharp_style_expression_bodied_indexers = true:suggestion
42
+ csharp_style_expression_bodied_accessors = true:suggestion
43
+ csharp_style_expression_bodied_lambdas = true:suggestion
44
+ csharp_style_expression_bodied_local_functions = when_on_single_line:suggestion
45
+
46
+ # Pattern matching preferences
47
+ csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
48
+ csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
49
+
50
+ # Null checking preferences
51
+ csharp_style_throw_expression = true:suggestion
52
+ csharp_style_conditional_delegate_call = true:suggestion
53
+
54
+ # Code-block preferences
55
+ csharp_prefer_braces = true:suggestion
56
+ csharp_prefer_simple_using_statement = true:suggestion
57
+
58
+ # Using directive preferences
59
+ csharp_using_directive_placement = outside_namespace:suggestion
60
+
61
+ # Namespace preferences
62
+ csharp_style_namespace_declarations = file_scoped:suggestion
63
+
64
+ # Primary constructor preferences
65
+ csharp_style_prefer_primary_constructors = true:suggestion
66
+
67
+ # Naming conventions
68
+ dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
69
+ dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
70
+ dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
71
+
72
+ dotnet_naming_symbols.interface.applicable_kinds = interface
73
+ dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
74
+
75
+ dotnet_naming_style.begins_with_i.required_prefix = I
76
+ dotnet_naming_style.begins_with_i.capitalization = pascal_case
77
+
78
+ dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
79
+ dotnet_naming_rule.types_should_be_pascal_case.symbols = types
80
+ dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
81
+
82
+ dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
83
+ dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
84
+
85
+ dotnet_naming_style.pascal_case.capitalization = pascal_case
86
+
87
+ dotnet_naming_rule.private_fields_should_be_camel_case.severity = suggestion
88
+ dotnet_naming_rule.private_fields_should_be_camel_case.symbols = private_fields
89
+ dotnet_naming_rule.private_fields_should_be_camel_case.style = camel_case_with_underscore
90
+
91
+ dotnet_naming_symbols.private_fields.applicable_kinds = field
92
+ dotnet_naming_symbols.private_fields.applicable_accessibilities = private
93
+
94
+ dotnet_naming_style.camel_case_with_underscore.required_prefix = _
95
+ dotnet_naming_style.camel_case_with_underscore.capitalization = camel_case
96
+
97
+ [*.{json,yml,yaml}]
98
+ indent_size = 2
99
+
100
+ [*.md]
101
+ trim_trailing_whitespace = false
102
+
103
+ [*.razor]
104
+ indent_size = 4
@@ -0,0 +1,35 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-python@v5
13
+ with:
14
+ python-version: "3.12"
15
+ - name: Install build tools
16
+ run: pip install build
17
+ - name: Build package
18
+ run: python -m build
19
+ - uses: actions/upload-artifact@v4
20
+ with:
21
+ name: dist
22
+ path: dist/
23
+
24
+ publish:
25
+ needs: build
26
+ runs-on: ubuntu-latest
27
+ environment: pypi
28
+ permissions:
29
+ id-token: write
30
+ steps:
31
+ - uses: actions/download-artifact@v4
32
+ with:
33
+ name: dist
34
+ path: dist/
35
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,45 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ *.egg
9
+ .eggs/
10
+ *.whl
11
+ .venv/
12
+ venv/
13
+
14
+ # IDE
15
+ .idea/
16
+ .vscode/
17
+ *.swp
18
+ *~
19
+
20
+ # OS
21
+ .DS_Store
22
+ Thumbs.db
23
+ Desktop.ini
24
+
25
+ # Environment
26
+ .env
27
+ .env.local
28
+ .env.*.local
29
+
30
+ # Claude / AI tools (local runtime state)
31
+ .claude/
32
+ .claude-flow/
33
+ .swarm/
34
+
35
+ # Generated output
36
+ output/
37
+ specs/
38
+
39
+ # Logs
40
+ logs/
41
+ *.log
42
+
43
+ # Database (local user data)
44
+ *.db
45
+ *.db-journal
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: code-explore
3
+ Version: 0.1.0
4
+ Summary: Developer knowledge base CLI — scan, index, and search your programming projects
5
+ Project-URL: Homepage, https://github.com/aipioneers/code-explore
6
+ Project-URL: Repository, https://github.com/aipioneers/code-explore
7
+ Project-URL: Issues, https://github.com/aipioneers/code-explore/issues
8
+ Author-email: Tobias Oberrauch <tobias.oberrauch@gmail.com>
9
+ License-Expression: MIT
10
+ Keywords: cli,code-search,developer-tools,knowledge-base,project-indexer
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: gitpython>=3.1.0
23
+ Requires-Dist: httpx>=0.24.0
24
+ Requires-Dist: lancedb>=0.4.0
25
+ Requires-Dist: pyarrow>=14.0.0
26
+ Requires-Dist: pydantic>=2.0.0
27
+ Requires-Dist: rich>=13.0.0
28
+ Requires-Dist: typer>=0.9.0
29
+ Provides-Extra: api
30
+ Requires-Dist: fastapi>=0.100.0; extra == 'api'
31
+ Requires-Dist: uvicorn>=0.23.0; extra == 'api'
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
34
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # code-explore
38
+
39
+ Developer knowledge base CLI — scan, index, and search your programming projects.
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ pip install code-explore
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ```bash
50
+ # Short alias (3 chars):
51
+ cex scan ~/Projects
52
+ cex index
53
+ cex search "YouTube API alle Videos"
54
+ cex show data-youtube
55
+ cex stats
56
+
57
+ # Full command also works:
58
+ code-explore scan ~/Projects
59
+ ```
60
+
61
+ ## Features
62
+
63
+ - Scan local project directories and extract metadata (languages, dependencies, patterns)
64
+ - Generate AI summaries using local Ollama models
65
+ - Create vector embeddings for semantic search (multilingual)
66
+ - Hybrid search combining fulltext + semantic ranking
67
+ - Incremental indexing with change detection
@@ -0,0 +1,31 @@
1
+ # code-explore
2
+
3
+ Developer knowledge base CLI — scan, index, and search your programming projects.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install code-explore
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ # Short alias (3 chars):
15
+ cex scan ~/Projects
16
+ cex index
17
+ cex search "YouTube API alle Videos"
18
+ cex show data-youtube
19
+ cex stats
20
+
21
+ # Full command also works:
22
+ code-explore scan ~/Projects
23
+ ```
24
+
25
+ ## Features
26
+
27
+ - Scan local project directories and extract metadata (languages, dependencies, patterns)
28
+ - Generate AI summaries using local Ollama models
29
+ - Create vector embeddings for semantic search (multilingual)
30
+ - Hybrid search combining fulltext + semantic ranking
31
+ - Incremental indexing with change detection
@@ -0,0 +1,3 @@
1
+ """Code Explore - Personal developer knowledge base."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,13 @@
1
+ """Code analysis - languages, metrics, dependencies, patterns."""
2
+
3
+ from code_explore.analyzer.dependencies import detect_dependencies
4
+ from code_explore.analyzer.language import detect_languages
5
+ from code_explore.analyzer.metrics import calculate_metrics
6
+ from code_explore.analyzer.patterns import detect_patterns
7
+
8
+ __all__ = [
9
+ "detect_dependencies",
10
+ "detect_languages",
11
+ "calculate_metrics",
12
+ "detect_patterns",
13
+ ]
@@ -0,0 +1,328 @@
1
+ """Parse dependency files to extract project dependencies."""
2
+
3
+ import json
4
+ import re
5
+ from pathlib import Path
6
+
7
+ from code_explore.models import DependencyInfo
8
+
9
+
10
+ def _read_file(path: Path) -> str:
11
+ try:
12
+ return path.read_text(encoding="utf-8", errors="replace")
13
+ except OSError:
14
+ return ""
15
+
16
+
17
+ def _parse_package_json(path: Path) -> list[DependencyInfo]:
18
+ content = _read_file(path)
19
+ if not content:
20
+ return []
21
+ try:
22
+ data = json.loads(content)
23
+ except (json.JSONDecodeError, ValueError):
24
+ return []
25
+
26
+ deps: list[DependencyInfo] = []
27
+ for name, version in (data.get("dependencies") or {}).items():
28
+ deps.append(DependencyInfo(name=name, version=version, dev=False, source="package.json"))
29
+ for name, version in (data.get("devDependencies") or {}).items():
30
+ deps.append(DependencyInfo(name=name, version=version, dev=True, source="package.json"))
31
+ return deps
32
+
33
+
34
+ def _parse_requirements_txt(path: Path) -> list[DependencyInfo]:
35
+ content = _read_file(path)
36
+ if not content:
37
+ return []
38
+
39
+ deps: list[DependencyInfo] = []
40
+ for line in content.splitlines():
41
+ line = line.strip()
42
+ if not line or line.startswith("#") or line.startswith("-"):
43
+ continue
44
+ match = re.match(r"^([A-Za-z0-9_][A-Za-z0-9._-]*)\s*(?:([><=!~]+.*))?", line)
45
+ if match:
46
+ name = match.group(1)
47
+ version = match.group(2)
48
+ if version:
49
+ version = version.strip()
50
+ deps.append(DependencyInfo(
51
+ name=name, version=version or None, dev=False, source=path.name,
52
+ ))
53
+ return deps
54
+
55
+
56
+ def _parse_pyproject_toml(path: Path) -> list[DependencyInfo]:
57
+ content = _read_file(path)
58
+ if not content:
59
+ return []
60
+
61
+ deps: list[DependencyInfo] = []
62
+
63
+ in_deps = False
64
+ in_dev_deps = False
65
+ for line in content.splitlines():
66
+ stripped = line.strip()
67
+
68
+ if stripped.startswith("["):
69
+ in_deps = stripped in (
70
+ "[project]",
71
+ "[tool.poetry.dependencies]",
72
+ )
73
+ in_dev_deps = stripped in (
74
+ "[tool.poetry.dev-dependencies]",
75
+ "[tool.poetry.group.dev.dependencies]",
76
+ )
77
+ if stripped == "[project]":
78
+ in_deps = False
79
+ continue
80
+
81
+ if stripped == "dependencies = [":
82
+ in_deps = True
83
+ continue
84
+
85
+ if in_deps and stripped.startswith("]"):
86
+ in_deps = False
87
+ continue
88
+
89
+ if in_deps:
90
+ match = re.match(r'"([A-Za-z0-9_][A-Za-z0-9._-]*)\s*(?:([><=!~]+[^"]*))?"', stripped)
91
+ if match:
92
+ deps.append(DependencyInfo(
93
+ name=match.group(1),
94
+ version=match.group(2) or None,
95
+ dev=False,
96
+ source="pyproject.toml",
97
+ ))
98
+ continue
99
+
100
+ toml_match = re.match(
101
+ r'([A-Za-z0-9_][A-Za-z0-9._-]*)\s*=\s*["\{]([^"}\n]*)', stripped,
102
+ )
103
+ if toml_match:
104
+ name = toml_match.group(1)
105
+ if name == "python":
106
+ continue
107
+ version_str = toml_match.group(2).strip().rstrip('"')
108
+ deps.append(DependencyInfo(
109
+ name=name,
110
+ version=version_str or None,
111
+ dev=in_dev_deps,
112
+ source="pyproject.toml",
113
+ ))
114
+
115
+ if in_dev_deps:
116
+ toml_match = re.match(
117
+ r'([A-Za-z0-9_][A-Za-z0-9._-]*)\s*=\s*["\{]([^"}\n]*)', stripped,
118
+ )
119
+ if toml_match:
120
+ name = toml_match.group(1)
121
+ if name == "python":
122
+ continue
123
+ version_str = toml_match.group(2).strip().rstrip('"')
124
+ deps.append(DependencyInfo(
125
+ name=name,
126
+ version=version_str or None,
127
+ dev=True,
128
+ source="pyproject.toml",
129
+ ))
130
+
131
+ return deps
132
+
133
+
134
+ def _parse_setup_py(path: Path) -> list[DependencyInfo]:
135
+ content = _read_file(path)
136
+ if not content:
137
+ return []
138
+
139
+ deps: list[DependencyInfo] = []
140
+ for match in re.finditer(r"install_requires\s*=\s*\[(.*?)]", content, re.DOTALL):
141
+ block = match.group(1)
142
+ for pkg in re.findall(r"['\"]([A-Za-z0-9_][A-Za-z0-9._-]*(?:\s*[><=!~]+[^'\"]*)?)['\"]", block):
143
+ parts = re.match(r"([A-Za-z0-9_][A-Za-z0-9._-]*)\s*(.*)", pkg)
144
+ if parts:
145
+ deps.append(DependencyInfo(
146
+ name=parts.group(1),
147
+ version=parts.group(2).strip() or None,
148
+ dev=False,
149
+ source="setup.py",
150
+ ))
151
+ return deps
152
+
153
+
154
+ def _parse_cargo_toml(path: Path) -> list[DependencyInfo]:
155
+ content = _read_file(path)
156
+ if not content:
157
+ return []
158
+
159
+ deps: list[DependencyInfo] = []
160
+ in_deps = False
161
+ in_dev_deps = False
162
+
163
+ for line in content.splitlines():
164
+ stripped = line.strip()
165
+ if stripped.startswith("["):
166
+ in_deps = stripped == "[dependencies]"
167
+ in_dev_deps = stripped in ("[dev-dependencies]", "[build-dependencies]")
168
+ continue
169
+
170
+ if in_deps or in_dev_deps:
171
+ match = re.match(r'([A-Za-z0-9_][A-Za-z0-9_-]*)\s*=\s*"([^"]*)"', stripped)
172
+ if match:
173
+ deps.append(DependencyInfo(
174
+ name=match.group(1),
175
+ version=match.group(2),
176
+ dev=in_dev_deps,
177
+ source="Cargo.toml",
178
+ ))
179
+ continue
180
+ match = re.match(r'([A-Za-z0-9_][A-Za-z0-9_-]*)\s*=\s*\{.*?version\s*=\s*"([^"]*)"', stripped)
181
+ if match:
182
+ deps.append(DependencyInfo(
183
+ name=match.group(1),
184
+ version=match.group(2),
185
+ dev=in_dev_deps,
186
+ source="Cargo.toml",
187
+ ))
188
+
189
+ return deps
190
+
191
+
192
+ def _parse_go_mod(path: Path) -> list[DependencyInfo]:
193
+ content = _read_file(path)
194
+ if not content:
195
+ return []
196
+
197
+ deps: list[DependencyInfo] = []
198
+ in_require = False
199
+
200
+ for line in content.splitlines():
201
+ stripped = line.strip()
202
+ if stripped.startswith("require ("):
203
+ in_require = True
204
+ continue
205
+ if stripped == ")":
206
+ in_require = False
207
+ continue
208
+ if stripped.startswith("require "):
209
+ parts = stripped[len("require "):].strip().split()
210
+ if len(parts) >= 2:
211
+ deps.append(DependencyInfo(
212
+ name=parts[0], version=parts[1], dev=False, source="go.mod",
213
+ ))
214
+ continue
215
+ if in_require:
216
+ parts = stripped.split()
217
+ if len(parts) >= 2 and not parts[0].startswith("//"):
218
+ deps.append(DependencyInfo(
219
+ name=parts[0], version=parts[1], dev=False, source="go.mod",
220
+ ))
221
+
222
+ return deps
223
+
224
+
225
+ def _parse_pom_xml(path: Path) -> list[DependencyInfo]:
226
+ content = _read_file(path)
227
+ if not content:
228
+ return []
229
+
230
+ deps: list[DependencyInfo] = []
231
+ for match in re.finditer(
232
+ r"<dependency>\s*"
233
+ r"<groupId>([^<]+)</groupId>\s*"
234
+ r"<artifactId>([^<]+)</artifactId>\s*"
235
+ r"(?:<version>([^<]+)</version>)?",
236
+ content,
237
+ ):
238
+ group_id = match.group(1).strip()
239
+ artifact_id = match.group(2).strip()
240
+ version = match.group(3).strip() if match.group(3) else None
241
+ scope_match = re.search(r"<scope>([^<]+)</scope>", content[match.start():match.start() + 500])
242
+ is_dev = scope_match and scope_match.group(1).strip() in ("test", "provided")
243
+ deps.append(DependencyInfo(
244
+ name=f"{group_id}:{artifact_id}",
245
+ version=version,
246
+ dev=bool(is_dev),
247
+ source="pom.xml",
248
+ ))
249
+
250
+ return deps
251
+
252
+
253
+ def _parse_gemfile(path: Path) -> list[DependencyInfo]:
254
+ content = _read_file(path)
255
+ if not content:
256
+ return []
257
+
258
+ deps: list[DependencyInfo] = []
259
+ in_dev_group = False
260
+
261
+ for line in content.splitlines():
262
+ stripped = line.strip()
263
+ if stripped.startswith("group :development") or stripped.startswith("group :test"):
264
+ in_dev_group = True
265
+ continue
266
+ if stripped == "end" and in_dev_group:
267
+ in_dev_group = False
268
+ continue
269
+
270
+ match = re.match(r"gem\s+['\"]([^'\"]+)['\"](?:\s*,\s*['\"]([^'\"]*)['\"])?", stripped)
271
+ if match:
272
+ deps.append(DependencyInfo(
273
+ name=match.group(1),
274
+ version=match.group(2) or None,
275
+ dev=in_dev_group,
276
+ source="Gemfile",
277
+ ))
278
+
279
+ return deps
280
+
281
+
282
+ def _parse_composer_json(path: Path) -> list[DependencyInfo]:
283
+ content = _read_file(path)
284
+ if not content:
285
+ return []
286
+ try:
287
+ data = json.loads(content)
288
+ except (json.JSONDecodeError, ValueError):
289
+ return []
290
+
291
+ deps: list[DependencyInfo] = []
292
+ for name, version in (data.get("require") or {}).items():
293
+ if name == "php" or name.startswith("ext-"):
294
+ continue
295
+ deps.append(DependencyInfo(name=name, version=version, dev=False, source="composer.json"))
296
+ for name, version in (data.get("require-dev") or {}).items():
297
+ deps.append(DependencyInfo(name=name, version=version, dev=True, source="composer.json"))
298
+ return deps
299
+
300
+
301
+ _PARSERS: dict[str, callable] = {
302
+ "package.json": _parse_package_json,
303
+ "requirements.txt": _parse_requirements_txt,
304
+ "requirements-dev.txt": _parse_requirements_txt,
305
+ "requirements_dev.txt": _parse_requirements_txt,
306
+ "pyproject.toml": _parse_pyproject_toml,
307
+ "setup.py": _parse_setup_py,
308
+ "Cargo.toml": _parse_cargo_toml,
309
+ "go.mod": _parse_go_mod,
310
+ "pom.xml": _parse_pom_xml,
311
+ "Gemfile": _parse_gemfile,
312
+ "composer.json": _parse_composer_json,
313
+ }
314
+
315
+
316
+ def detect_dependencies(project_path: str | Path) -> list[DependencyInfo]:
317
+ root = Path(project_path)
318
+ if not root.is_dir():
319
+ return []
320
+
321
+ all_deps: list[DependencyInfo] = []
322
+
323
+ for filename, parser in _PARSERS.items():
324
+ dep_file = root / filename
325
+ if dep_file.is_file():
326
+ all_deps.extend(parser(dep_file))
327
+
328
+ return all_deps