devflow-toolkit 0.3.0__py3-none-any.whl
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.
- devflow/__init__.py +0 -0
- devflow/adapters/__init__.py +47 -0
- devflow/adapters/agents.py +49 -0
- devflow/adapters/base.py +81 -0
- devflow/adapters/claude_code.py +120 -0
- devflow/adapters/codex.py +111 -0
- devflow/adapters/detector.py +118 -0
- devflow/adapters/instructions.py +263 -0
- devflow/adapters/kiro.py +211 -0
- devflow/adapters/opencode.py +237 -0
- devflow/adapters/registry.py +41 -0
- devflow/cli.py +39 -0
- devflow/commands/__init__.py +6 -0
- devflow/commands/approve.py +98 -0
- devflow/commands/harness.py +98 -0
- devflow/commands/init.py +544 -0
- devflow/commands/spec.py +514 -0
- devflow/commands/status.py +156 -0
- devflow/core/__init__.py +15 -0
- devflow/core/architecture.py +469 -0
- devflow/core/artifacts/__init__.py +29 -0
- devflow/core/artifacts/errors.py +19 -0
- devflow/core/artifacts/models.py +82 -0
- devflow/core/artifacts/store.py +130 -0
- devflow/core/clock.py +67 -0
- devflow/core/config.py +706 -0
- devflow/core/constitution.py +166 -0
- devflow/core/discovery.py +1304 -0
- devflow/core/ports.py +159 -0
- devflow/core/profile_manager.py +311 -0
- devflow/core/project.py +533 -0
- devflow/core/spec_workflow.py +932 -0
- devflow/core/specification.py +2443 -0
- devflow/core/state/__init__.py +75 -0
- devflow/core/state/errors.py +39 -0
- devflow/core/state/events.py +725 -0
- devflow/core/state/locator.py +116 -0
- devflow/core/state/lock.py +69 -0
- devflow/core/state/manager.py +960 -0
- devflow/core/state/models.py +164 -0
- devflow/core/state/transitions.py +114 -0
- devflow/models/__init__.py +6 -0
- devflow/models/profile.py +63 -0
- devflow/models/project.py +47 -0
- devflow/profiles/ai-rag/constitution/ai-rag-standards.md +49 -0
- devflow/profiles/ai-rag/profile.yaml +28 -0
- devflow/profiles/base/constitution/architecture.md +58 -0
- devflow/profiles/base/constitution/code-standards.md +85 -0
- devflow/profiles/base/constitution/constitution.md +85 -0
- devflow/profiles/base/profile.yaml +15 -0
- devflow/profiles/django-backend/constitution/django-standards.md +64 -0
- devflow/profiles/django-backend/profile.yaml +24 -0
- devflow/profiles/fastapi-backend/constitution/fastapi-standards.md +59 -0
- devflow/profiles/fastapi-backend/profile.yaml +23 -0
- devflow/prompts/__init__.py +200 -0
- devflow/prompts/discovery.md +119 -0
- devflow/prompts/spec.greenfield.md +149 -0
- devflow/prompts/spec.md +116 -0
- devflow_toolkit-0.3.0.dist-info/METADATA +628 -0
- devflow_toolkit-0.3.0.dist-info/RECORD +63 -0
- devflow_toolkit-0.3.0.dist-info/WHEEL +4 -0
- devflow_toolkit-0.3.0.dist-info/entry_points.txt +2 -0
- devflow_toolkit-0.3.0.dist-info/licenses/LICENSE +21 -0
devflow/core/project.py
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
"""Project inspection and technology detection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import tomllib
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Iterator
|
|
11
|
+
|
|
12
|
+
from devflow.models.project import Project
|
|
13
|
+
|
|
14
|
+
SKIP_DIRECTORIES = {
|
|
15
|
+
".git",
|
|
16
|
+
".venv",
|
|
17
|
+
"venv",
|
|
18
|
+
"env",
|
|
19
|
+
"node_modules",
|
|
20
|
+
"__pycache__",
|
|
21
|
+
".mypy_cache",
|
|
22
|
+
".pytest_cache",
|
|
23
|
+
".ruff_cache",
|
|
24
|
+
".tox",
|
|
25
|
+
".nox",
|
|
26
|
+
"dist",
|
|
27
|
+
"build",
|
|
28
|
+
"coverage",
|
|
29
|
+
"htmlcov",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
SOURCE_EXTENSIONS = {
|
|
33
|
+
".py",
|
|
34
|
+
".js",
|
|
35
|
+
".jsx",
|
|
36
|
+
".ts",
|
|
37
|
+
".tsx",
|
|
38
|
+
".vue",
|
|
39
|
+
".svelte",
|
|
40
|
+
".go",
|
|
41
|
+
".rs",
|
|
42
|
+
".php",
|
|
43
|
+
".rb",
|
|
44
|
+
".java",
|
|
45
|
+
".kt",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
FRAMEWORK_DEPENDENCIES = {
|
|
49
|
+
"django": "Django",
|
|
50
|
+
"djangorestframework": "Django REST Framework",
|
|
51
|
+
"fastapi": "FastAPI",
|
|
52
|
+
"flask": "Flask",
|
|
53
|
+
"starlette": "Starlette",
|
|
54
|
+
"express": "Express",
|
|
55
|
+
"nestjs": "NestJS",
|
|
56
|
+
"react": "React",
|
|
57
|
+
"vue": "Vue",
|
|
58
|
+
"svelte": "Svelte",
|
|
59
|
+
"next": "Next.js",
|
|
60
|
+
"langchain": "LangChain",
|
|
61
|
+
"llama-index": "LlamaIndex",
|
|
62
|
+
"chromadb": "ChromaDB",
|
|
63
|
+
"faiss-cpu": "FAISS",
|
|
64
|
+
"sentence-transformers": "Sentence Transformers",
|
|
65
|
+
"openai": "OpenAI",
|
|
66
|
+
"anthropic": "Anthropic",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
AI_DEPENDENCIES = {
|
|
70
|
+
"langchain",
|
|
71
|
+
"llama-index",
|
|
72
|
+
"chromadb",
|
|
73
|
+
"faiss-cpu",
|
|
74
|
+
"sentence-transformers",
|
|
75
|
+
"pgvector",
|
|
76
|
+
"qdrant-client",
|
|
77
|
+
"weaviate-client",
|
|
78
|
+
"pinecone-client",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
FRONTEND_FRAMEWORKS = {
|
|
82
|
+
"React",
|
|
83
|
+
"Vue",
|
|
84
|
+
"Svelte",
|
|
85
|
+
"Next.js",
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
BACKEND_FRAMEWORKS = {
|
|
89
|
+
"Django",
|
|
90
|
+
"Django REST Framework",
|
|
91
|
+
"FastAPI",
|
|
92
|
+
"Flask",
|
|
93
|
+
"Starlette",
|
|
94
|
+
"Express",
|
|
95
|
+
"NestJS",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
DATABASE_DEPENDENCIES = {
|
|
99
|
+
"postgresql": {
|
|
100
|
+
"psycopg",
|
|
101
|
+
"psycopg2",
|
|
102
|
+
"asyncpg",
|
|
103
|
+
"pg8000",
|
|
104
|
+
},
|
|
105
|
+
"mysql": {
|
|
106
|
+
"mysqlclient",
|
|
107
|
+
"pymysql",
|
|
108
|
+
"aiomysql",
|
|
109
|
+
},
|
|
110
|
+
"mongodb": {
|
|
111
|
+
"pymongo",
|
|
112
|
+
"motor",
|
|
113
|
+
"beanie",
|
|
114
|
+
},
|
|
115
|
+
"sqlite": {
|
|
116
|
+
"aiosqlite",
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_PROJECT_MARKERS = (
|
|
121
|
+
".devflow/config.yaml",
|
|
122
|
+
"pyproject.toml",
|
|
123
|
+
"manage.py",
|
|
124
|
+
"package.json",
|
|
125
|
+
".git",
|
|
126
|
+
"go.mod",
|
|
127
|
+
"Cargo.toml",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def canonicalize_dependency(name: str) -> str:
|
|
132
|
+
"""Normalize a package name for comparisons."""
|
|
133
|
+
return re.sub(r"[-_.]+", "-", name.strip().lower())
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _dependency_name(requirement: str) -> str | None:
|
|
137
|
+
"""Extract a package name from a requirements.txt entry."""
|
|
138
|
+
value = requirement.strip()
|
|
139
|
+
if not value or value.startswith("#"):
|
|
140
|
+
return None
|
|
141
|
+
if value.startswith("-"):
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
value = value.split(";", maxsplit=1)[0].strip()
|
|
145
|
+
if not value:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
if " @ " in value:
|
|
149
|
+
value = value.split(" @ ", maxsplit=1)[0].strip()
|
|
150
|
+
|
|
151
|
+
match = re.match(r"([A-Za-z0-9][A-Za-z0-9._-]*)", value)
|
|
152
|
+
if not match:
|
|
153
|
+
return None
|
|
154
|
+
return canonicalize_dependency(match.group(1))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _is_symlink_escape(path: Path, root: Path) -> bool:
|
|
158
|
+
"""Return whether a path is a symlink or resolves outside the root."""
|
|
159
|
+
if path.is_symlink():
|
|
160
|
+
return True
|
|
161
|
+
try:
|
|
162
|
+
path.resolve().relative_to(root.resolve())
|
|
163
|
+
except ValueError:
|
|
164
|
+
return True
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def iter_project_files(
|
|
169
|
+
root: Path,
|
|
170
|
+
extensions: set[str] | None = None,
|
|
171
|
+
max_files: int = 5000,
|
|
172
|
+
) -> Iterator[Path]:
|
|
173
|
+
"""Yield project files while skipping generated and vendor directories."""
|
|
174
|
+
resolved_root = root.resolve()
|
|
175
|
+
count = 0
|
|
176
|
+
for dirpath, dirnames, filenames in os.walk(resolved_root):
|
|
177
|
+
dirnames[:] = sorted(
|
|
178
|
+
name
|
|
179
|
+
for name in dirnames
|
|
180
|
+
if name not in SKIP_DIRECTORIES
|
|
181
|
+
and not name.startswith(".")
|
|
182
|
+
and not (Path(dirpath) / name).is_symlink()
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
for filename in sorted(filenames):
|
|
186
|
+
if filename.startswith("."):
|
|
187
|
+
continue
|
|
188
|
+
path = Path(dirpath) / filename
|
|
189
|
+
if _is_symlink_escape(path, resolved_root):
|
|
190
|
+
continue
|
|
191
|
+
if extensions is not None and path.suffix not in extensions:
|
|
192
|
+
continue
|
|
193
|
+
yield path
|
|
194
|
+
count += 1
|
|
195
|
+
if count >= max_files:
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class ProjectDetector:
|
|
200
|
+
"""Inspect a project and detect its stack and applicable profiles."""
|
|
201
|
+
|
|
202
|
+
def find_root(self, start: Path) -> Path:
|
|
203
|
+
"""Find the nearest likely project root from a starting path."""
|
|
204
|
+
current = start.resolve()
|
|
205
|
+
if current.is_file():
|
|
206
|
+
current = current.parent
|
|
207
|
+
|
|
208
|
+
for candidate in (current, *current.parents):
|
|
209
|
+
if any(
|
|
210
|
+
(candidate / marker).exists()
|
|
211
|
+
for marker in _PROJECT_MARKERS
|
|
212
|
+
):
|
|
213
|
+
return candidate
|
|
214
|
+
return current
|
|
215
|
+
|
|
216
|
+
def detect(self, root: Path) -> Project:
|
|
217
|
+
"""Inspect a project and return its detected context."""
|
|
218
|
+
resolved = root.resolve()
|
|
219
|
+
if not resolved.exists():
|
|
220
|
+
msg = f"Project path does not exist: {resolved}"
|
|
221
|
+
raise FileNotFoundError(msg)
|
|
222
|
+
if not resolved.is_dir():
|
|
223
|
+
msg = f"Project path is not a directory: {resolved}"
|
|
224
|
+
raise NotADirectoryError(msg)
|
|
225
|
+
|
|
226
|
+
pyproject, pyproject_name = self._read_pyproject(resolved)
|
|
227
|
+
package_json = self._read_package_json(resolved)
|
|
228
|
+
|
|
229
|
+
dependencies: set[str] = set()
|
|
230
|
+
manifests: list[str] = []
|
|
231
|
+
|
|
232
|
+
if pyproject is not None:
|
|
233
|
+
manifests.append("pyproject.toml")
|
|
234
|
+
dependencies.update(self._pyproject_dependencies(pyproject))
|
|
235
|
+
|
|
236
|
+
if package_json is not None:
|
|
237
|
+
manifests.append("package.json")
|
|
238
|
+
dependencies.update(self._package_dependencies(package_json))
|
|
239
|
+
|
|
240
|
+
requirements = self._requirements_files(resolved)
|
|
241
|
+
for path in requirements:
|
|
242
|
+
manifests.append(path.relative_to(resolved).as_posix())
|
|
243
|
+
dependencies.update(self._read_requirements(path))
|
|
244
|
+
|
|
245
|
+
source_files = [
|
|
246
|
+
path.relative_to(resolved).as_posix()
|
|
247
|
+
for path in iter_project_files(
|
|
248
|
+
resolved,
|
|
249
|
+
extensions=SOURCE_EXTENSIONS,
|
|
250
|
+
)
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
languages = self._detect_languages(
|
|
254
|
+
resolved,
|
|
255
|
+
dependencies,
|
|
256
|
+
source_files,
|
|
257
|
+
)
|
|
258
|
+
frameworks = self._detect_frameworks(dependencies)
|
|
259
|
+
databases = self._detect_databases(dependencies)
|
|
260
|
+
project_type = self._detect_project_type(frameworks)
|
|
261
|
+
|
|
262
|
+
name = pyproject_name
|
|
263
|
+
if not name and package_json is not None:
|
|
264
|
+
value = package_json.get("name")
|
|
265
|
+
if isinstance(value, str) and value.strip():
|
|
266
|
+
name = value.strip()
|
|
267
|
+
if not name:
|
|
268
|
+
name = resolved.name
|
|
269
|
+
|
|
270
|
+
return Project(
|
|
271
|
+
root=resolved,
|
|
272
|
+
name=name,
|
|
273
|
+
project_type=project_type,
|
|
274
|
+
languages=languages,
|
|
275
|
+
frameworks=frameworks,
|
|
276
|
+
databases=databases,
|
|
277
|
+
dependencies=dependencies,
|
|
278
|
+
manifests=manifests,
|
|
279
|
+
source_files=source_files,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def _project_file(self, root: Path, name: str) -> Path | None:
|
|
283
|
+
"""Return a contained project file, ignoring symlinks and escapes."""
|
|
284
|
+
path = root / name
|
|
285
|
+
if not path.is_file() or _is_symlink_escape(path, root):
|
|
286
|
+
return None
|
|
287
|
+
return path
|
|
288
|
+
|
|
289
|
+
def _read_pyproject(
|
|
290
|
+
self,
|
|
291
|
+
root: Path,
|
|
292
|
+
) -> tuple[dict[str, Any] | None, str | None]:
|
|
293
|
+
path = self._project_file(root, "pyproject.toml")
|
|
294
|
+
if path is None:
|
|
295
|
+
return None, None
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
with path.open("rb") as handle:
|
|
299
|
+
data = tomllib.load(handle)
|
|
300
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
301
|
+
return None, None
|
|
302
|
+
|
|
303
|
+
project = data.get("project")
|
|
304
|
+
name = None
|
|
305
|
+
if isinstance(project, dict):
|
|
306
|
+
value = project.get("name")
|
|
307
|
+
if isinstance(value, str) and value.strip():
|
|
308
|
+
name = value.strip()
|
|
309
|
+
|
|
310
|
+
return data, name
|
|
311
|
+
|
|
312
|
+
def _read_package_json(
|
|
313
|
+
self,
|
|
314
|
+
root: Path,
|
|
315
|
+
) -> dict[str, Any] | None:
|
|
316
|
+
path = self._project_file(root, "package.json")
|
|
317
|
+
if path is None:
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
with path.open(encoding="utf-8") as handle:
|
|
322
|
+
data = json.load(handle)
|
|
323
|
+
except (OSError, json.JSONDecodeError):
|
|
324
|
+
return None
|
|
325
|
+
return data if isinstance(data, dict) else None
|
|
326
|
+
|
|
327
|
+
def _pyproject_dependencies(
|
|
328
|
+
self,
|
|
329
|
+
data: dict[str, Any],
|
|
330
|
+
) -> set[str]:
|
|
331
|
+
dependencies: set[str] = set()
|
|
332
|
+
project = data.get("project")
|
|
333
|
+
if isinstance(project, dict):
|
|
334
|
+
dependencies.update(
|
|
335
|
+
self._parse_requirements(project.get("dependencies"))
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
optional = project.get("optional-dependencies")
|
|
339
|
+
if isinstance(optional, dict):
|
|
340
|
+
for values in optional.values():
|
|
341
|
+
dependencies.update(self._parse_requirements(values))
|
|
342
|
+
|
|
343
|
+
groups = data.get("dependency-groups")
|
|
344
|
+
if isinstance(groups, dict):
|
|
345
|
+
for values in groups.values():
|
|
346
|
+
dependencies.update(self._parse_requirements(values))
|
|
347
|
+
|
|
348
|
+
tool = data.get("tool")
|
|
349
|
+
poetry = tool.get("poetry") if isinstance(tool, dict) else None
|
|
350
|
+
if isinstance(poetry, dict):
|
|
351
|
+
poetry_dependencies = poetry.get("dependencies")
|
|
352
|
+
if isinstance(poetry_dependencies, dict):
|
|
353
|
+
for name in poetry_dependencies:
|
|
354
|
+
if name.lower() != "python":
|
|
355
|
+
dependencies.add(canonicalize_dependency(name))
|
|
356
|
+
|
|
357
|
+
poetry_groups = poetry.get("group")
|
|
358
|
+
if isinstance(poetry_groups, dict):
|
|
359
|
+
for group in poetry_groups.values():
|
|
360
|
+
if not isinstance(group, dict):
|
|
361
|
+
continue
|
|
362
|
+
values = group.get("dependencies")
|
|
363
|
+
if isinstance(values, dict):
|
|
364
|
+
for name in values:
|
|
365
|
+
dependencies.add(canonicalize_dependency(name))
|
|
366
|
+
|
|
367
|
+
return dependencies
|
|
368
|
+
|
|
369
|
+
def _package_dependencies(
|
|
370
|
+
self,
|
|
371
|
+
data: dict[str, Any],
|
|
372
|
+
) -> set[str]:
|
|
373
|
+
dependencies: set[str] = set()
|
|
374
|
+
for section in (
|
|
375
|
+
"dependencies",
|
|
376
|
+
"devDependencies",
|
|
377
|
+
"peerDependencies",
|
|
378
|
+
"optionalDependencies",
|
|
379
|
+
):
|
|
380
|
+
values = data.get(section)
|
|
381
|
+
if isinstance(values, dict):
|
|
382
|
+
for name in values:
|
|
383
|
+
dependencies.add(canonicalize_dependency(name))
|
|
384
|
+
return dependencies
|
|
385
|
+
|
|
386
|
+
def _requirements_files(self, root: Path) -> list[Path]:
|
|
387
|
+
candidates = list(root.glob("requirements*.txt"))
|
|
388
|
+
requirements_dir = root / "requirements"
|
|
389
|
+
if requirements_dir.is_dir() and not requirements_dir.is_symlink():
|
|
390
|
+
candidates.extend(requirements_dir.glob("*.txt"))
|
|
391
|
+
|
|
392
|
+
paths = {
|
|
393
|
+
path
|
|
394
|
+
for path in candidates
|
|
395
|
+
if path.is_file() and not _is_symlink_escape(path, root)
|
|
396
|
+
}
|
|
397
|
+
return sorted(paths)
|
|
398
|
+
|
|
399
|
+
def _read_requirements(self, path: Path) -> set[str]:
|
|
400
|
+
try:
|
|
401
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
402
|
+
except OSError:
|
|
403
|
+
return set()
|
|
404
|
+
return self._parse_requirements(lines)
|
|
405
|
+
|
|
406
|
+
def _parse_requirements(self, values: Any) -> set[str]:
|
|
407
|
+
if not isinstance(values, list):
|
|
408
|
+
return set()
|
|
409
|
+
dependencies = set()
|
|
410
|
+
for value in values:
|
|
411
|
+
if not isinstance(value, str):
|
|
412
|
+
continue
|
|
413
|
+
name = _dependency_name(value)
|
|
414
|
+
if name:
|
|
415
|
+
dependencies.add(name)
|
|
416
|
+
return dependencies
|
|
417
|
+
|
|
418
|
+
def _detect_languages(
|
|
419
|
+
self,
|
|
420
|
+
root: Path,
|
|
421
|
+
dependencies: set[str],
|
|
422
|
+
source_files: list[str],
|
|
423
|
+
) -> list[str]:
|
|
424
|
+
languages: list[str] = []
|
|
425
|
+
suffixes = {Path(name).suffix for name in source_files}
|
|
426
|
+
|
|
427
|
+
has_python = (
|
|
428
|
+
".py" in suffixes
|
|
429
|
+
or "python" in dependencies
|
|
430
|
+
or any(
|
|
431
|
+
self._project_file(root, marker) is not None
|
|
432
|
+
for marker in ("manage.py", "requirements.txt")
|
|
433
|
+
)
|
|
434
|
+
)
|
|
435
|
+
if has_python:
|
|
436
|
+
languages.append("python")
|
|
437
|
+
|
|
438
|
+
has_package_json = self._project_file(root, "package.json") is not None
|
|
439
|
+
has_typescript = (
|
|
440
|
+
".ts" in suffixes
|
|
441
|
+
or ".tsx" in suffixes
|
|
442
|
+
or "typescript" in dependencies
|
|
443
|
+
)
|
|
444
|
+
if has_package_json or ".js" in suffixes or ".jsx" in suffixes:
|
|
445
|
+
languages.append("javascript")
|
|
446
|
+
if has_typescript:
|
|
447
|
+
languages.append("typescript")
|
|
448
|
+
|
|
449
|
+
if self._project_file(root, "go.mod") is not None or ".go" in suffixes:
|
|
450
|
+
languages.append("go")
|
|
451
|
+
if self._project_file(root, "Cargo.toml") is not None or ".rs" in suffixes:
|
|
452
|
+
languages.append("rust")
|
|
453
|
+
if (
|
|
454
|
+
self._project_file(root, "composer.json") is not None
|
|
455
|
+
or ".php" in suffixes
|
|
456
|
+
):
|
|
457
|
+
languages.append("php")
|
|
458
|
+
if self._project_file(root, "Gemfile") is not None or ".rb" in suffixes:
|
|
459
|
+
languages.append("ruby")
|
|
460
|
+
if ".java" in suffixes:
|
|
461
|
+
languages.append("java")
|
|
462
|
+
|
|
463
|
+
return languages
|
|
464
|
+
|
|
465
|
+
def _detect_frameworks(
|
|
466
|
+
self,
|
|
467
|
+
dependencies: set[str],
|
|
468
|
+
) -> list[str]:
|
|
469
|
+
frameworks: list[str] = []
|
|
470
|
+
for dependency, label in FRAMEWORK_DEPENDENCIES.items():
|
|
471
|
+
if self._has_dependency(dependencies, dependency):
|
|
472
|
+
frameworks.append(label)
|
|
473
|
+
return frameworks
|
|
474
|
+
|
|
475
|
+
def _detect_databases(
|
|
476
|
+
self,
|
|
477
|
+
dependencies: set[str],
|
|
478
|
+
) -> list[str]:
|
|
479
|
+
databases: list[str] = []
|
|
480
|
+
for label, names in DATABASE_DEPENDENCIES.items():
|
|
481
|
+
if any(
|
|
482
|
+
self._has_dependency(dependencies, name)
|
|
483
|
+
for name in names
|
|
484
|
+
):
|
|
485
|
+
databases.append(label)
|
|
486
|
+
|
|
487
|
+
if "chromadb" in dependencies:
|
|
488
|
+
if "vector-database" not in databases:
|
|
489
|
+
databases.append("vector-database")
|
|
490
|
+
return databases
|
|
491
|
+
|
|
492
|
+
def _detect_project_type(self, frameworks: list[str]) -> str:
|
|
493
|
+
frontend = any(
|
|
494
|
+
framework in FRONTEND_FRAMEWORKS
|
|
495
|
+
for framework in frameworks
|
|
496
|
+
)
|
|
497
|
+
backend = any(
|
|
498
|
+
framework in BACKEND_FRAMEWORKS
|
|
499
|
+
for framework in frameworks
|
|
500
|
+
)
|
|
501
|
+
ai = any(
|
|
502
|
+
framework
|
|
503
|
+
in {
|
|
504
|
+
"LangChain",
|
|
505
|
+
"LlamaIndex",
|
|
506
|
+
"ChromaDB",
|
|
507
|
+
"FAISS",
|
|
508
|
+
"Sentence Transformers",
|
|
509
|
+
}
|
|
510
|
+
for framework in frameworks
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
if ai:
|
|
514
|
+
return "ai"
|
|
515
|
+
if frontend and backend:
|
|
516
|
+
return "fullstack"
|
|
517
|
+
if backend:
|
|
518
|
+
return "backend"
|
|
519
|
+
if frontend:
|
|
520
|
+
return "frontend"
|
|
521
|
+
return "unknown"
|
|
522
|
+
|
|
523
|
+
def _has_dependency(
|
|
524
|
+
self,
|
|
525
|
+
dependencies: set[str],
|
|
526
|
+
name: str,
|
|
527
|
+
) -> bool:
|
|
528
|
+
canonical = canonicalize_dependency(name)
|
|
529
|
+
return any(
|
|
530
|
+
dependency == canonical
|
|
531
|
+
or dependency.startswith(f"{canonical}-")
|
|
532
|
+
for dependency in dependencies
|
|
533
|
+
)
|