@codexa/cli 8.6.0 → 8.6.9
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/commands/architect.ts +760 -760
- package/commands/check.ts +131 -131
- package/commands/clear.ts +170 -170
- package/commands/decide.ts +249 -249
- package/commands/discover.ts +1071 -1071
- package/commands/knowledge.ts +361 -361
- package/commands/patterns.ts +621 -621
- package/commands/plan.ts +376 -376
- package/commands/product.ts +626 -626
- package/commands/research.ts +754 -754
- package/commands/review.ts +463 -463
- package/commands/standards.ts +200 -200
- package/commands/task.ts +623 -623
- package/commands/utils.ts +1021 -1021
- package/db/connection.ts +32 -32
- package/db/schema.ts +719 -719
- package/detectors/README.md +109 -109
- package/detectors/dotnet.ts +357 -357
- package/detectors/flutter.ts +350 -350
- package/detectors/go.ts +324 -324
- package/detectors/index.ts +387 -387
- package/detectors/jvm.ts +433 -433
- package/detectors/loader.ts +128 -128
- package/detectors/node.ts +493 -493
- package/detectors/python.ts +423 -423
- package/detectors/rust.ts +348 -348
- package/gates/standards-validator.ts +204 -204
- package/gates/validator.ts +441 -441
- package/package.json +44 -43
- package/protocol/process-return.ts +450 -450
- package/protocol/subagent-protocol.ts +401 -401
- package/workflow.ts +783 -782
package/detectors/python.ts
CHANGED
|
@@ -1,424 +1,424 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Python Ecosystem Detector
|
|
3
|
-
*
|
|
4
|
-
* Detects Python projects including:
|
|
5
|
-
* - Frameworks: Django, Flask, FastAPI, Starlette
|
|
6
|
-
* - ORMs: SQLAlchemy, Django ORM, Tortoise, Peewee
|
|
7
|
-
* - Testing: pytest, unittest, nose
|
|
8
|
-
* - Package managers: pip, poetry, pipenv, uv
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { join } from "path";
|
|
12
|
-
import {
|
|
13
|
-
registerDetector,
|
|
14
|
-
Detector,
|
|
15
|
-
DetectorResult,
|
|
16
|
-
DetectedTechnology,
|
|
17
|
-
fileExists,
|
|
18
|
-
dirExists,
|
|
19
|
-
findFiles,
|
|
20
|
-
readText,
|
|
21
|
-
parseToml,
|
|
22
|
-
} from "./index";
|
|
23
|
-
|
|
24
|
-
interface Dependency {
|
|
25
|
-
name: string;
|
|
26
|
-
version?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function parseRequirements(content: string): Dependency[] {
|
|
30
|
-
const deps: Dependency[] = [];
|
|
31
|
-
for (const line of content.split("\n")) {
|
|
32
|
-
const trimmed = line.trim();
|
|
33
|
-
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
|
|
34
|
-
|
|
35
|
-
// Parse formats: package==1.0.0, package>=1.0.0, package~=1.0.0, package
|
|
36
|
-
const match = trimmed.match(/^([a-zA-Z0-9_-]+)(?:\[.*?\])?(?:([<>=!~]+)(.+))?/);
|
|
37
|
-
if (match) {
|
|
38
|
-
deps.push({
|
|
39
|
-
name: match[1].toLowerCase(),
|
|
40
|
-
version: match[3],
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return deps;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function parsePyproject(content: string): Dependency[] {
|
|
48
|
-
const deps: Dependency[] = [];
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
const toml = parseToml(content);
|
|
52
|
-
|
|
53
|
-
// Poetry format
|
|
54
|
-
const poetryDeps = toml?.tool?.poetry?.dependencies || {};
|
|
55
|
-
const poetryDevDeps = toml?.tool?.poetry?.["dev-dependencies"] || {};
|
|
56
|
-
|
|
57
|
-
for (const [name, value] of Object.entries({ ...poetryDeps, ...poetryDevDeps })) {
|
|
58
|
-
if (name === "python") continue;
|
|
59
|
-
deps.push({
|
|
60
|
-
name: name.toLowerCase(),
|
|
61
|
-
version: typeof value === "string" ? value : undefined,
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// PEP 621 format (project.dependencies)
|
|
66
|
-
const projectDeps = toml?.project?.dependencies || [];
|
|
67
|
-
const optionalDeps = Object.values(toml?.project?.["optional-dependencies"] || {}).flat();
|
|
68
|
-
|
|
69
|
-
for (const dep of [...projectDeps, ...optionalDeps]) {
|
|
70
|
-
if (typeof dep === "string") {
|
|
71
|
-
const match = dep.match(/^([a-zA-Z0-9_-]+)/);
|
|
72
|
-
if (match) {
|
|
73
|
-
deps.push({ name: match[1].toLowerCase() });
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
} catch {
|
|
78
|
-
// Fallback to regex parsing
|
|
79
|
-
const depMatches = content.matchAll(/^\s*"?([a-zA-Z0-9_-]+)"?\s*[=<>]/gm);
|
|
80
|
-
for (const match of depMatches) {
|
|
81
|
-
deps.push({ name: match[1].toLowerCase() });
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return deps;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const pythonDetector: Detector = {
|
|
89
|
-
name: "python",
|
|
90
|
-
ecosystem: "python",
|
|
91
|
-
priority: 85,
|
|
92
|
-
markers: [
|
|
93
|
-
{ type: "file", pattern: "pyproject.toml", weight: 1.0 },
|
|
94
|
-
{ type: "file", pattern: "setup.py", weight: 0.9 },
|
|
95
|
-
{ type: "file", pattern: "setup.cfg", weight: 0.9 },
|
|
96
|
-
{ type: "file", pattern: "requirements.txt", weight: 0.8 },
|
|
97
|
-
{ type: "file", pattern: "Pipfile", weight: 0.9 },
|
|
98
|
-
{ type: "file", pattern: "poetry.lock", weight: 1.0 },
|
|
99
|
-
{ type: "file", pattern: "Pipfile.lock", weight: 0.9 },
|
|
100
|
-
{ type: "file", pattern: "uv.lock", weight: 1.0 },
|
|
101
|
-
{ type: "directory", pattern: "__pycache__", weight: 0.4 },
|
|
102
|
-
{ type: "directory", pattern: ".venv", weight: 0.5 },
|
|
103
|
-
{ type: "directory", pattern: "venv", weight: 0.5 },
|
|
104
|
-
{ type: "glob", pattern: "**/*.py", weight: 0.6 },
|
|
105
|
-
],
|
|
106
|
-
|
|
107
|
-
async detect(cwd: string): Promise<DetectorResult | null> {
|
|
108
|
-
const technologies: DetectedTechnology[] = [];
|
|
109
|
-
const structure: Record<string, string> = {};
|
|
110
|
-
const configFiles: string[] = [];
|
|
111
|
-
|
|
112
|
-
// Check for Python files
|
|
113
|
-
const pyFiles = findFiles(cwd, "**/*.py");
|
|
114
|
-
if (pyFiles.length === 0) {
|
|
115
|
-
// Check for config files even without .py files
|
|
116
|
-
const hasPyproject = fileExists(join(cwd, "pyproject.toml"));
|
|
117
|
-
const hasRequirements = fileExists(join(cwd, "requirements.txt"));
|
|
118
|
-
|
|
119
|
-
if (!hasPyproject && !hasRequirements) {
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Collect all dependencies
|
|
125
|
-
const allDeps = new Map<string, string | undefined>();
|
|
126
|
-
|
|
127
|
-
// Parse requirements.txt
|
|
128
|
-
if (fileExists(join(cwd, "requirements.txt"))) {
|
|
129
|
-
configFiles.push("requirements.txt");
|
|
130
|
-
const content = readText(join(cwd, "requirements.txt"));
|
|
131
|
-
if (content) {
|
|
132
|
-
for (const dep of parseRequirements(content)) {
|
|
133
|
-
allDeps.set(dep.name, dep.version);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Also check for dev requirements
|
|
139
|
-
const devReqFiles = ["requirements-dev.txt", "requirements.dev.txt", "dev-requirements.txt"];
|
|
140
|
-
for (const f of devReqFiles) {
|
|
141
|
-
if (fileExists(join(cwd, f))) {
|
|
142
|
-
configFiles.push(f);
|
|
143
|
-
const content = readText(join(cwd, f));
|
|
144
|
-
if (content) {
|
|
145
|
-
for (const dep of parseRequirements(content)) {
|
|
146
|
-
allDeps.set(dep.name, dep.version);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Parse pyproject.toml
|
|
153
|
-
if (fileExists(join(cwd, "pyproject.toml"))) {
|
|
154
|
-
configFiles.push("pyproject.toml");
|
|
155
|
-
const content = readText(join(cwd, "pyproject.toml"));
|
|
156
|
-
if (content) {
|
|
157
|
-
for (const dep of parsePyproject(content)) {
|
|
158
|
-
allDeps.set(dep.name, dep.version);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Detect package manager from pyproject.toml
|
|
162
|
-
if (content.includes("[tool.poetry]")) {
|
|
163
|
-
technologies.push({
|
|
164
|
-
name: "Poetry",
|
|
165
|
-
confidence: 1.0,
|
|
166
|
-
source: "pyproject.toml [tool.poetry]",
|
|
167
|
-
category: "build",
|
|
168
|
-
});
|
|
169
|
-
} else if (content.includes("[tool.pdm]")) {
|
|
170
|
-
technologies.push({
|
|
171
|
-
name: "PDM",
|
|
172
|
-
confidence: 1.0,
|
|
173
|
-
source: "pyproject.toml [tool.pdm]",
|
|
174
|
-
category: "build",
|
|
175
|
-
});
|
|
176
|
-
} else if (content.includes("[tool.hatch]")) {
|
|
177
|
-
technologies.push({
|
|
178
|
-
name: "Hatch",
|
|
179
|
-
confidence: 1.0,
|
|
180
|
-
source: "pyproject.toml [tool.hatch]",
|
|
181
|
-
category: "build",
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Package manager detection from lock files
|
|
188
|
-
if (fileExists(join(cwd, "poetry.lock"))) {
|
|
189
|
-
technologies.push({
|
|
190
|
-
name: "Poetry",
|
|
191
|
-
confidence: 1.0,
|
|
192
|
-
source: "poetry.lock",
|
|
193
|
-
category: "build",
|
|
194
|
-
});
|
|
195
|
-
configFiles.push("poetry.lock");
|
|
196
|
-
} else if (fileExists(join(cwd, "Pipfile.lock"))) {
|
|
197
|
-
technologies.push({
|
|
198
|
-
name: "Pipenv",
|
|
199
|
-
confidence: 1.0,
|
|
200
|
-
source: "Pipfile.lock",
|
|
201
|
-
category: "build",
|
|
202
|
-
});
|
|
203
|
-
configFiles.push("Pipfile.lock");
|
|
204
|
-
} else if (fileExists(join(cwd, "uv.lock"))) {
|
|
205
|
-
technologies.push({
|
|
206
|
-
name: "uv",
|
|
207
|
-
confidence: 1.0,
|
|
208
|
-
source: "uv.lock",
|
|
209
|
-
category: "build",
|
|
210
|
-
});
|
|
211
|
-
configFiles.push("uv.lock");
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Add Python runtime
|
|
215
|
-
technologies.push({
|
|
216
|
-
name: "Python",
|
|
217
|
-
confidence: 1.0,
|
|
218
|
-
source: "Project files",
|
|
219
|
-
category: "runtime",
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
// Web Framework Detection
|
|
223
|
-
const webFrameworks = [
|
|
224
|
-
{ deps: ["django"], name: "Django", confidence: 1.0 },
|
|
225
|
-
{ deps: ["fastapi"], name: "FastAPI", confidence: 1.0 },
|
|
226
|
-
{ deps: ["flask"], name: "Flask", confidence: 1.0 },
|
|
227
|
-
{ deps: ["starlette"], name: "Starlette", confidence: 0.95 },
|
|
228
|
-
{ deps: ["litestar"], name: "Litestar", confidence: 1.0 },
|
|
229
|
-
{ deps: ["tornado"], name: "Tornado", confidence: 0.95 },
|
|
230
|
-
{ deps: ["pyramid"], name: "Pyramid", confidence: 0.95 },
|
|
231
|
-
{ deps: ["aiohttp"], name: "aiohttp", confidence: 0.9 },
|
|
232
|
-
{ deps: ["sanic"], name: "Sanic", confidence: 0.95 },
|
|
233
|
-
{ deps: ["quart"], name: "Quart", confidence: 0.95 },
|
|
234
|
-
{ deps: ["falcon"], name: "Falcon", confidence: 0.95 },
|
|
235
|
-
{ deps: ["blacksheep"], name: "BlackSheep", confidence: 0.95 },
|
|
236
|
-
];
|
|
237
|
-
|
|
238
|
-
for (const fw of webFrameworks) {
|
|
239
|
-
const found = fw.deps.find(d => allDeps.has(d));
|
|
240
|
-
if (found) {
|
|
241
|
-
technologies.push({
|
|
242
|
-
name: fw.name,
|
|
243
|
-
version: allDeps.get(found),
|
|
244
|
-
confidence: fw.confidence,
|
|
245
|
-
source: `Dependency: ${found}`,
|
|
246
|
-
category: "backend",
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// ORM Detection
|
|
252
|
-
const orms = [
|
|
253
|
-
{ deps: ["sqlalchemy", "sqlmodel"], name: "SQLAlchemy", confidence: 1.0 },
|
|
254
|
-
{ deps: ["tortoise-orm"], name: "Tortoise ORM", confidence: 1.0 },
|
|
255
|
-
{ deps: ["peewee"], name: "Peewee", confidence: 1.0 },
|
|
256
|
-
{ deps: ["sqlmodel"], name: "SQLModel", confidence: 1.0 },
|
|
257
|
-
{ deps: ["piccolo"], name: "Piccolo", confidence: 1.0 },
|
|
258
|
-
{ deps: ["ormar"], name: "Ormar", confidence: 1.0 },
|
|
259
|
-
{ deps: ["odmantic"], name: "ODMantic", confidence: 1.0 },
|
|
260
|
-
{ deps: ["mongoengine"], name: "MongoEngine", confidence: 1.0 },
|
|
261
|
-
{ deps: ["beanie"], name: "Beanie", confidence: 1.0 },
|
|
262
|
-
];
|
|
263
|
-
|
|
264
|
-
for (const orm of orms) {
|
|
265
|
-
const found = orm.deps.find(d => allDeps.has(d));
|
|
266
|
-
if (found) {
|
|
267
|
-
technologies.push({
|
|
268
|
-
name: orm.name,
|
|
269
|
-
version: allDeps.get(found),
|
|
270
|
-
confidence: orm.confidence,
|
|
271
|
-
source: `Dependency: ${found}`,
|
|
272
|
-
category: "orm",
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Database Detection
|
|
278
|
-
const databases = [
|
|
279
|
-
{ deps: ["psycopg2", "psycopg2-binary", "asyncpg", "psycopg"], name: "PostgreSQL", confidence: 0.9 },
|
|
280
|
-
{ deps: ["pymysql", "mysqlclient", "aiomysql"], name: "MySQL", confidence: 0.9 },
|
|
281
|
-
{ deps: ["pymongo", "motor"], name: "MongoDB", confidence: 0.9 },
|
|
282
|
-
{ deps: ["redis", "aioredis"], name: "Redis", confidence: 0.9 },
|
|
283
|
-
{ deps: ["elasticsearch", "elasticsearch-dsl"], name: "Elasticsearch", confidence: 0.9 },
|
|
284
|
-
{ deps: ["cassandra-driver"], name: "Cassandra", confidence: 0.9 },
|
|
285
|
-
];
|
|
286
|
-
|
|
287
|
-
const detectedDbs = new Set<string>();
|
|
288
|
-
for (const db of databases) {
|
|
289
|
-
const found = db.deps.find(d => allDeps.has(d));
|
|
290
|
-
if (found && !detectedDbs.has(db.name)) {
|
|
291
|
-
technologies.push({
|
|
292
|
-
name: db.name,
|
|
293
|
-
confidence: db.confidence,
|
|
294
|
-
source: `Dependency: ${found}`,
|
|
295
|
-
category: "database",
|
|
296
|
-
});
|
|
297
|
-
detectedDbs.add(db.name);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Testing Detection
|
|
302
|
-
const testingTools = [
|
|
303
|
-
{ deps: ["pytest"], name: "pytest", confidence: 1.0 },
|
|
304
|
-
{ deps: ["unittest"], name: "unittest", confidence: 0.8 },
|
|
305
|
-
{ deps: ["nose2"], name: "nose2", confidence: 0.9 },
|
|
306
|
-
{ deps: ["hypothesis"], name: "Hypothesis", confidence: 0.9 },
|
|
307
|
-
{ deps: ["locust"], name: "Locust", confidence: 0.9 },
|
|
308
|
-
{ deps: ["behave"], name: "Behave (BDD)", confidence: 0.9 },
|
|
309
|
-
{ deps: ["pytest-asyncio"], name: "pytest-asyncio", confidence: 0.9 },
|
|
310
|
-
];
|
|
311
|
-
|
|
312
|
-
for (const test of testingTools) {
|
|
313
|
-
const found = test.deps.find(d => allDeps.has(d));
|
|
314
|
-
if (found) {
|
|
315
|
-
technologies.push({
|
|
316
|
-
name: test.name,
|
|
317
|
-
version: allDeps.get(found),
|
|
318
|
-
confidence: test.confidence,
|
|
319
|
-
source: `Dependency: ${found}`,
|
|
320
|
-
category: "testing",
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// ML/Data Science (common in Python)
|
|
326
|
-
const mlTools = [
|
|
327
|
-
{ deps: ["tensorflow", "keras"], name: "TensorFlow", confidence: 1.0 },
|
|
328
|
-
{ deps: ["torch", "pytorch"], name: "PyTorch", confidence: 1.0 },
|
|
329
|
-
{ deps: ["scikit-learn", "sklearn"], name: "scikit-learn", confidence: 1.0 },
|
|
330
|
-
{ deps: ["pandas"], name: "Pandas", confidence: 0.9 },
|
|
331
|
-
{ deps: ["numpy"], name: "NumPy", confidence: 0.8 },
|
|
332
|
-
{ deps: ["transformers"], name: "Hugging Face Transformers", confidence: 1.0 },
|
|
333
|
-
{ deps: ["langchain"], name: "LangChain", confidence: 1.0 },
|
|
334
|
-
{ deps: ["openai"], name: "OpenAI SDK", confidence: 0.9 },
|
|
335
|
-
];
|
|
336
|
-
|
|
337
|
-
for (const ml of mlTools) {
|
|
338
|
-
const found = ml.deps.find(d => allDeps.has(d));
|
|
339
|
-
if (found) {
|
|
340
|
-
technologies.push({
|
|
341
|
-
name: ml.name,
|
|
342
|
-
version: allDeps.get(found),
|
|
343
|
-
confidence: ml.confidence,
|
|
344
|
-
source: `Dependency: ${found}`,
|
|
345
|
-
category: "backend",
|
|
346
|
-
metadata: { type: "ml/data-science" },
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Structure detection
|
|
352
|
-
const structurePaths = [
|
|
353
|
-
{ key: "api", paths: ["api", "app/api", "src/api", "app/routers", "routers"] },
|
|
354
|
-
{ key: "models", paths: ["models", "app/models", "src/models"] },
|
|
355
|
-
{ key: "services", paths: ["services", "app/services", "src/services"] },
|
|
356
|
-
{ key: "schemas", paths: ["schemas", "app/schemas", "src/schemas"] },
|
|
357
|
-
{ key: "tests", paths: ["tests", "test", "app/tests"] },
|
|
358
|
-
{ key: "migrations", paths: ["migrations", "alembic", "app/migrations"] },
|
|
359
|
-
{ key: "static", paths: ["static", "app/static"] },
|
|
360
|
-
{ key: "templates", paths: ["templates", "app/templates"] },
|
|
361
|
-
];
|
|
362
|
-
|
|
363
|
-
for (const { key, paths } of structurePaths) {
|
|
364
|
-
for (const p of paths) {
|
|
365
|
-
if (dirExists(join(cwd, p))) {
|
|
366
|
-
structure[key] = p;
|
|
367
|
-
break;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Django specific
|
|
373
|
-
if (allDeps.has("django")) {
|
|
374
|
-
const djangoStructure = [
|
|
375
|
-
{ key: "apps", paths: ["apps", "src/apps"] },
|
|
376
|
-
{ key: "settings", paths: ["config", "settings", "core/settings"] },
|
|
377
|
-
];
|
|
378
|
-
for (const { key, paths } of djangoStructure) {
|
|
379
|
-
for (const p of paths) {
|
|
380
|
-
if (dirExists(join(cwd, p))) {
|
|
381
|
-
structure[key] = p;
|
|
382
|
-
break;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Config files
|
|
389
|
-
const configPatterns = [
|
|
390
|
-
"setup.py",
|
|
391
|
-
"setup.cfg",
|
|
392
|
-
"Pipfile",
|
|
393
|
-
"tox.ini",
|
|
394
|
-
".flake8",
|
|
395
|
-
".pylintrc",
|
|
396
|
-
"mypy.ini",
|
|
397
|
-
".mypy.ini",
|
|
398
|
-
"pytest.ini",
|
|
399
|
-
"conftest.py",
|
|
400
|
-
"alembic.ini",
|
|
401
|
-
".pre-commit-config.yaml",
|
|
402
|
-
".python-version",
|
|
403
|
-
"runtime.txt",
|
|
404
|
-
];
|
|
405
|
-
|
|
406
|
-
for (const pattern of configPatterns) {
|
|
407
|
-
if (fileExists(join(cwd, pattern))) {
|
|
408
|
-
configFiles.push(pattern);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
return {
|
|
413
|
-
ecosystem: "python",
|
|
414
|
-
technologies,
|
|
415
|
-
structure,
|
|
416
|
-
configFiles: [...new Set(configFiles)],
|
|
417
|
-
};
|
|
418
|
-
},
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
// Register the detector
|
|
422
|
-
registerDetector(pythonDetector);
|
|
423
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Python Ecosystem Detector
|
|
3
|
+
*
|
|
4
|
+
* Detects Python projects including:
|
|
5
|
+
* - Frameworks: Django, Flask, FastAPI, Starlette
|
|
6
|
+
* - ORMs: SQLAlchemy, Django ORM, Tortoise, Peewee
|
|
7
|
+
* - Testing: pytest, unittest, nose
|
|
8
|
+
* - Package managers: pip, poetry, pipenv, uv
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import {
|
|
13
|
+
registerDetector,
|
|
14
|
+
Detector,
|
|
15
|
+
DetectorResult,
|
|
16
|
+
DetectedTechnology,
|
|
17
|
+
fileExists,
|
|
18
|
+
dirExists,
|
|
19
|
+
findFiles,
|
|
20
|
+
readText,
|
|
21
|
+
parseToml,
|
|
22
|
+
} from "./index";
|
|
23
|
+
|
|
24
|
+
interface Dependency {
|
|
25
|
+
name: string;
|
|
26
|
+
version?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseRequirements(content: string): Dependency[] {
|
|
30
|
+
const deps: Dependency[] = [];
|
|
31
|
+
for (const line of content.split("\n")) {
|
|
32
|
+
const trimmed = line.trim();
|
|
33
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
|
|
34
|
+
|
|
35
|
+
// Parse formats: package==1.0.0, package>=1.0.0, package~=1.0.0, package
|
|
36
|
+
const match = trimmed.match(/^([a-zA-Z0-9_-]+)(?:\[.*?\])?(?:([<>=!~]+)(.+))?/);
|
|
37
|
+
if (match) {
|
|
38
|
+
deps.push({
|
|
39
|
+
name: match[1].toLowerCase(),
|
|
40
|
+
version: match[3],
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return deps;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parsePyproject(content: string): Dependency[] {
|
|
48
|
+
const deps: Dependency[] = [];
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const toml = parseToml(content);
|
|
52
|
+
|
|
53
|
+
// Poetry format
|
|
54
|
+
const poetryDeps = toml?.tool?.poetry?.dependencies || {};
|
|
55
|
+
const poetryDevDeps = toml?.tool?.poetry?.["dev-dependencies"] || {};
|
|
56
|
+
|
|
57
|
+
for (const [name, value] of Object.entries({ ...poetryDeps, ...poetryDevDeps })) {
|
|
58
|
+
if (name === "python") continue;
|
|
59
|
+
deps.push({
|
|
60
|
+
name: name.toLowerCase(),
|
|
61
|
+
version: typeof value === "string" ? value : undefined,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// PEP 621 format (project.dependencies)
|
|
66
|
+
const projectDeps = toml?.project?.dependencies || [];
|
|
67
|
+
const optionalDeps = Object.values(toml?.project?.["optional-dependencies"] || {}).flat();
|
|
68
|
+
|
|
69
|
+
for (const dep of [...projectDeps, ...optionalDeps]) {
|
|
70
|
+
if (typeof dep === "string") {
|
|
71
|
+
const match = dep.match(/^([a-zA-Z0-9_-]+)/);
|
|
72
|
+
if (match) {
|
|
73
|
+
deps.push({ name: match[1].toLowerCase() });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// Fallback to regex parsing
|
|
79
|
+
const depMatches = content.matchAll(/^\s*"?([a-zA-Z0-9_-]+)"?\s*[=<>]/gm);
|
|
80
|
+
for (const match of depMatches) {
|
|
81
|
+
deps.push({ name: match[1].toLowerCase() });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return deps;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const pythonDetector: Detector = {
|
|
89
|
+
name: "python",
|
|
90
|
+
ecosystem: "python",
|
|
91
|
+
priority: 85,
|
|
92
|
+
markers: [
|
|
93
|
+
{ type: "file", pattern: "pyproject.toml", weight: 1.0 },
|
|
94
|
+
{ type: "file", pattern: "setup.py", weight: 0.9 },
|
|
95
|
+
{ type: "file", pattern: "setup.cfg", weight: 0.9 },
|
|
96
|
+
{ type: "file", pattern: "requirements.txt", weight: 0.8 },
|
|
97
|
+
{ type: "file", pattern: "Pipfile", weight: 0.9 },
|
|
98
|
+
{ type: "file", pattern: "poetry.lock", weight: 1.0 },
|
|
99
|
+
{ type: "file", pattern: "Pipfile.lock", weight: 0.9 },
|
|
100
|
+
{ type: "file", pattern: "uv.lock", weight: 1.0 },
|
|
101
|
+
{ type: "directory", pattern: "__pycache__", weight: 0.4 },
|
|
102
|
+
{ type: "directory", pattern: ".venv", weight: 0.5 },
|
|
103
|
+
{ type: "directory", pattern: "venv", weight: 0.5 },
|
|
104
|
+
{ type: "glob", pattern: "**/*.py", weight: 0.6 },
|
|
105
|
+
],
|
|
106
|
+
|
|
107
|
+
async detect(cwd: string): Promise<DetectorResult | null> {
|
|
108
|
+
const technologies: DetectedTechnology[] = [];
|
|
109
|
+
const structure: Record<string, string> = {};
|
|
110
|
+
const configFiles: string[] = [];
|
|
111
|
+
|
|
112
|
+
// Check for Python files
|
|
113
|
+
const pyFiles = findFiles(cwd, "**/*.py");
|
|
114
|
+
if (pyFiles.length === 0) {
|
|
115
|
+
// Check for config files even without .py files
|
|
116
|
+
const hasPyproject = fileExists(join(cwd, "pyproject.toml"));
|
|
117
|
+
const hasRequirements = fileExists(join(cwd, "requirements.txt"));
|
|
118
|
+
|
|
119
|
+
if (!hasPyproject && !hasRequirements) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Collect all dependencies
|
|
125
|
+
const allDeps = new Map<string, string | undefined>();
|
|
126
|
+
|
|
127
|
+
// Parse requirements.txt
|
|
128
|
+
if (fileExists(join(cwd, "requirements.txt"))) {
|
|
129
|
+
configFiles.push("requirements.txt");
|
|
130
|
+
const content = readText(join(cwd, "requirements.txt"));
|
|
131
|
+
if (content) {
|
|
132
|
+
for (const dep of parseRequirements(content)) {
|
|
133
|
+
allDeps.set(dep.name, dep.version);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Also check for dev requirements
|
|
139
|
+
const devReqFiles = ["requirements-dev.txt", "requirements.dev.txt", "dev-requirements.txt"];
|
|
140
|
+
for (const f of devReqFiles) {
|
|
141
|
+
if (fileExists(join(cwd, f))) {
|
|
142
|
+
configFiles.push(f);
|
|
143
|
+
const content = readText(join(cwd, f));
|
|
144
|
+
if (content) {
|
|
145
|
+
for (const dep of parseRequirements(content)) {
|
|
146
|
+
allDeps.set(dep.name, dep.version);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Parse pyproject.toml
|
|
153
|
+
if (fileExists(join(cwd, "pyproject.toml"))) {
|
|
154
|
+
configFiles.push("pyproject.toml");
|
|
155
|
+
const content = readText(join(cwd, "pyproject.toml"));
|
|
156
|
+
if (content) {
|
|
157
|
+
for (const dep of parsePyproject(content)) {
|
|
158
|
+
allDeps.set(dep.name, dep.version);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Detect package manager from pyproject.toml
|
|
162
|
+
if (content.includes("[tool.poetry]")) {
|
|
163
|
+
technologies.push({
|
|
164
|
+
name: "Poetry",
|
|
165
|
+
confidence: 1.0,
|
|
166
|
+
source: "pyproject.toml [tool.poetry]",
|
|
167
|
+
category: "build",
|
|
168
|
+
});
|
|
169
|
+
} else if (content.includes("[tool.pdm]")) {
|
|
170
|
+
technologies.push({
|
|
171
|
+
name: "PDM",
|
|
172
|
+
confidence: 1.0,
|
|
173
|
+
source: "pyproject.toml [tool.pdm]",
|
|
174
|
+
category: "build",
|
|
175
|
+
});
|
|
176
|
+
} else if (content.includes("[tool.hatch]")) {
|
|
177
|
+
technologies.push({
|
|
178
|
+
name: "Hatch",
|
|
179
|
+
confidence: 1.0,
|
|
180
|
+
source: "pyproject.toml [tool.hatch]",
|
|
181
|
+
category: "build",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Package manager detection from lock files
|
|
188
|
+
if (fileExists(join(cwd, "poetry.lock"))) {
|
|
189
|
+
technologies.push({
|
|
190
|
+
name: "Poetry",
|
|
191
|
+
confidence: 1.0,
|
|
192
|
+
source: "poetry.lock",
|
|
193
|
+
category: "build",
|
|
194
|
+
});
|
|
195
|
+
configFiles.push("poetry.lock");
|
|
196
|
+
} else if (fileExists(join(cwd, "Pipfile.lock"))) {
|
|
197
|
+
technologies.push({
|
|
198
|
+
name: "Pipenv",
|
|
199
|
+
confidence: 1.0,
|
|
200
|
+
source: "Pipfile.lock",
|
|
201
|
+
category: "build",
|
|
202
|
+
});
|
|
203
|
+
configFiles.push("Pipfile.lock");
|
|
204
|
+
} else if (fileExists(join(cwd, "uv.lock"))) {
|
|
205
|
+
technologies.push({
|
|
206
|
+
name: "uv",
|
|
207
|
+
confidence: 1.0,
|
|
208
|
+
source: "uv.lock",
|
|
209
|
+
category: "build",
|
|
210
|
+
});
|
|
211
|
+
configFiles.push("uv.lock");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Add Python runtime
|
|
215
|
+
technologies.push({
|
|
216
|
+
name: "Python",
|
|
217
|
+
confidence: 1.0,
|
|
218
|
+
source: "Project files",
|
|
219
|
+
category: "runtime",
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Web Framework Detection
|
|
223
|
+
const webFrameworks = [
|
|
224
|
+
{ deps: ["django"], name: "Django", confidence: 1.0 },
|
|
225
|
+
{ deps: ["fastapi"], name: "FastAPI", confidence: 1.0 },
|
|
226
|
+
{ deps: ["flask"], name: "Flask", confidence: 1.0 },
|
|
227
|
+
{ deps: ["starlette"], name: "Starlette", confidence: 0.95 },
|
|
228
|
+
{ deps: ["litestar"], name: "Litestar", confidence: 1.0 },
|
|
229
|
+
{ deps: ["tornado"], name: "Tornado", confidence: 0.95 },
|
|
230
|
+
{ deps: ["pyramid"], name: "Pyramid", confidence: 0.95 },
|
|
231
|
+
{ deps: ["aiohttp"], name: "aiohttp", confidence: 0.9 },
|
|
232
|
+
{ deps: ["sanic"], name: "Sanic", confidence: 0.95 },
|
|
233
|
+
{ deps: ["quart"], name: "Quart", confidence: 0.95 },
|
|
234
|
+
{ deps: ["falcon"], name: "Falcon", confidence: 0.95 },
|
|
235
|
+
{ deps: ["blacksheep"], name: "BlackSheep", confidence: 0.95 },
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
for (const fw of webFrameworks) {
|
|
239
|
+
const found = fw.deps.find(d => allDeps.has(d));
|
|
240
|
+
if (found) {
|
|
241
|
+
technologies.push({
|
|
242
|
+
name: fw.name,
|
|
243
|
+
version: allDeps.get(found),
|
|
244
|
+
confidence: fw.confidence,
|
|
245
|
+
source: `Dependency: ${found}`,
|
|
246
|
+
category: "backend",
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ORM Detection
|
|
252
|
+
const orms = [
|
|
253
|
+
{ deps: ["sqlalchemy", "sqlmodel"], name: "SQLAlchemy", confidence: 1.0 },
|
|
254
|
+
{ deps: ["tortoise-orm"], name: "Tortoise ORM", confidence: 1.0 },
|
|
255
|
+
{ deps: ["peewee"], name: "Peewee", confidence: 1.0 },
|
|
256
|
+
{ deps: ["sqlmodel"], name: "SQLModel", confidence: 1.0 },
|
|
257
|
+
{ deps: ["piccolo"], name: "Piccolo", confidence: 1.0 },
|
|
258
|
+
{ deps: ["ormar"], name: "Ormar", confidence: 1.0 },
|
|
259
|
+
{ deps: ["odmantic"], name: "ODMantic", confidence: 1.0 },
|
|
260
|
+
{ deps: ["mongoengine"], name: "MongoEngine", confidence: 1.0 },
|
|
261
|
+
{ deps: ["beanie"], name: "Beanie", confidence: 1.0 },
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
for (const orm of orms) {
|
|
265
|
+
const found = orm.deps.find(d => allDeps.has(d));
|
|
266
|
+
if (found) {
|
|
267
|
+
technologies.push({
|
|
268
|
+
name: orm.name,
|
|
269
|
+
version: allDeps.get(found),
|
|
270
|
+
confidence: orm.confidence,
|
|
271
|
+
source: `Dependency: ${found}`,
|
|
272
|
+
category: "orm",
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Database Detection
|
|
278
|
+
const databases = [
|
|
279
|
+
{ deps: ["psycopg2", "psycopg2-binary", "asyncpg", "psycopg"], name: "PostgreSQL", confidence: 0.9 },
|
|
280
|
+
{ deps: ["pymysql", "mysqlclient", "aiomysql"], name: "MySQL", confidence: 0.9 },
|
|
281
|
+
{ deps: ["pymongo", "motor"], name: "MongoDB", confidence: 0.9 },
|
|
282
|
+
{ deps: ["redis", "aioredis"], name: "Redis", confidence: 0.9 },
|
|
283
|
+
{ deps: ["elasticsearch", "elasticsearch-dsl"], name: "Elasticsearch", confidence: 0.9 },
|
|
284
|
+
{ deps: ["cassandra-driver"], name: "Cassandra", confidence: 0.9 },
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
const detectedDbs = new Set<string>();
|
|
288
|
+
for (const db of databases) {
|
|
289
|
+
const found = db.deps.find(d => allDeps.has(d));
|
|
290
|
+
if (found && !detectedDbs.has(db.name)) {
|
|
291
|
+
technologies.push({
|
|
292
|
+
name: db.name,
|
|
293
|
+
confidence: db.confidence,
|
|
294
|
+
source: `Dependency: ${found}`,
|
|
295
|
+
category: "database",
|
|
296
|
+
});
|
|
297
|
+
detectedDbs.add(db.name);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Testing Detection
|
|
302
|
+
const testingTools = [
|
|
303
|
+
{ deps: ["pytest"], name: "pytest", confidence: 1.0 },
|
|
304
|
+
{ deps: ["unittest"], name: "unittest", confidence: 0.8 },
|
|
305
|
+
{ deps: ["nose2"], name: "nose2", confidence: 0.9 },
|
|
306
|
+
{ deps: ["hypothesis"], name: "Hypothesis", confidence: 0.9 },
|
|
307
|
+
{ deps: ["locust"], name: "Locust", confidence: 0.9 },
|
|
308
|
+
{ deps: ["behave"], name: "Behave (BDD)", confidence: 0.9 },
|
|
309
|
+
{ deps: ["pytest-asyncio"], name: "pytest-asyncio", confidence: 0.9 },
|
|
310
|
+
];
|
|
311
|
+
|
|
312
|
+
for (const test of testingTools) {
|
|
313
|
+
const found = test.deps.find(d => allDeps.has(d));
|
|
314
|
+
if (found) {
|
|
315
|
+
technologies.push({
|
|
316
|
+
name: test.name,
|
|
317
|
+
version: allDeps.get(found),
|
|
318
|
+
confidence: test.confidence,
|
|
319
|
+
source: `Dependency: ${found}`,
|
|
320
|
+
category: "testing",
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ML/Data Science (common in Python)
|
|
326
|
+
const mlTools = [
|
|
327
|
+
{ deps: ["tensorflow", "keras"], name: "TensorFlow", confidence: 1.0 },
|
|
328
|
+
{ deps: ["torch", "pytorch"], name: "PyTorch", confidence: 1.0 },
|
|
329
|
+
{ deps: ["scikit-learn", "sklearn"], name: "scikit-learn", confidence: 1.0 },
|
|
330
|
+
{ deps: ["pandas"], name: "Pandas", confidence: 0.9 },
|
|
331
|
+
{ deps: ["numpy"], name: "NumPy", confidence: 0.8 },
|
|
332
|
+
{ deps: ["transformers"], name: "Hugging Face Transformers", confidence: 1.0 },
|
|
333
|
+
{ deps: ["langchain"], name: "LangChain", confidence: 1.0 },
|
|
334
|
+
{ deps: ["openai"], name: "OpenAI SDK", confidence: 0.9 },
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
for (const ml of mlTools) {
|
|
338
|
+
const found = ml.deps.find(d => allDeps.has(d));
|
|
339
|
+
if (found) {
|
|
340
|
+
technologies.push({
|
|
341
|
+
name: ml.name,
|
|
342
|
+
version: allDeps.get(found),
|
|
343
|
+
confidence: ml.confidence,
|
|
344
|
+
source: `Dependency: ${found}`,
|
|
345
|
+
category: "backend",
|
|
346
|
+
metadata: { type: "ml/data-science" },
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Structure detection
|
|
352
|
+
const structurePaths = [
|
|
353
|
+
{ key: "api", paths: ["api", "app/api", "src/api", "app/routers", "routers"] },
|
|
354
|
+
{ key: "models", paths: ["models", "app/models", "src/models"] },
|
|
355
|
+
{ key: "services", paths: ["services", "app/services", "src/services"] },
|
|
356
|
+
{ key: "schemas", paths: ["schemas", "app/schemas", "src/schemas"] },
|
|
357
|
+
{ key: "tests", paths: ["tests", "test", "app/tests"] },
|
|
358
|
+
{ key: "migrations", paths: ["migrations", "alembic", "app/migrations"] },
|
|
359
|
+
{ key: "static", paths: ["static", "app/static"] },
|
|
360
|
+
{ key: "templates", paths: ["templates", "app/templates"] },
|
|
361
|
+
];
|
|
362
|
+
|
|
363
|
+
for (const { key, paths } of structurePaths) {
|
|
364
|
+
for (const p of paths) {
|
|
365
|
+
if (dirExists(join(cwd, p))) {
|
|
366
|
+
structure[key] = p;
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Django specific
|
|
373
|
+
if (allDeps.has("django")) {
|
|
374
|
+
const djangoStructure = [
|
|
375
|
+
{ key: "apps", paths: ["apps", "src/apps"] },
|
|
376
|
+
{ key: "settings", paths: ["config", "settings", "core/settings"] },
|
|
377
|
+
];
|
|
378
|
+
for (const { key, paths } of djangoStructure) {
|
|
379
|
+
for (const p of paths) {
|
|
380
|
+
if (dirExists(join(cwd, p))) {
|
|
381
|
+
structure[key] = p;
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Config files
|
|
389
|
+
const configPatterns = [
|
|
390
|
+
"setup.py",
|
|
391
|
+
"setup.cfg",
|
|
392
|
+
"Pipfile",
|
|
393
|
+
"tox.ini",
|
|
394
|
+
".flake8",
|
|
395
|
+
".pylintrc",
|
|
396
|
+
"mypy.ini",
|
|
397
|
+
".mypy.ini",
|
|
398
|
+
"pytest.ini",
|
|
399
|
+
"conftest.py",
|
|
400
|
+
"alembic.ini",
|
|
401
|
+
".pre-commit-config.yaml",
|
|
402
|
+
".python-version",
|
|
403
|
+
"runtime.txt",
|
|
404
|
+
];
|
|
405
|
+
|
|
406
|
+
for (const pattern of configPatterns) {
|
|
407
|
+
if (fileExists(join(cwd, pattern))) {
|
|
408
|
+
configFiles.push(pattern);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
ecosystem: "python",
|
|
414
|
+
technologies,
|
|
415
|
+
structure,
|
|
416
|
+
configFiles: [...new Set(configFiles)],
|
|
417
|
+
};
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// Register the detector
|
|
422
|
+
registerDetector(pythonDetector);
|
|
423
|
+
|
|
424
424
|
export default pythonDetector;
|