prelude-context 1.4.3 → 1.6.0
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/README.md +76 -0
- package/dist/bin/prelude.js +4 -0
- package/dist/bin/prelude.js.map +1 -1
- package/dist/src/commands/mcp-config.d.ts +3 -0
- package/dist/src/commands/mcp-config.d.ts.map +1 -0
- package/dist/src/commands/mcp-config.js +72 -0
- package/dist/src/commands/mcp-config.js.map +1 -0
- package/dist/src/commands/serve.d.ts +3 -0
- package/dist/src/commands/serve.d.ts.map +1 -0
- package/dist/src/commands/serve.js +29 -0
- package/dist/src/commands/serve.js.map +1 -0
- package/dist/src/constants.d.ts +1 -1
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +8 -1
- package/dist/src/constants.js.map +1 -1
- package/dist/src/core/infer.d.ts.map +1 -1
- package/dist/src/core/infer.js +541 -39
- package/dist/src/core/infer.js.map +1 -1
- package/dist/src/core/source-scanner.d.ts.map +1 -1
- package/dist/src/core/source-scanner.js +95 -8
- package/dist/src/core/source-scanner.js.map +1 -1
- package/dist/src/mcp/server.d.ts +3 -0
- package/dist/src/mcp/server.d.ts.map +1 -0
- package/dist/src/mcp/server.js +162 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/schema/stack.d.ts +3 -3
- package/dist/src/schema/stack.js +1 -1
- package/dist/src/schema/stack.js.map +1 -1
- package/dist/src/schema/state.d.ts +12 -12
- package/dist/src/utils/fs.d.ts.map +1 -1
- package/dist/src/utils/fs.js +3 -1
- package/dist/src/utils/fs.js.map +1 -1
- package/package.json +12 -10
- package/schemas/stack.schema.json +1 -1
package/dist/src/core/infer.js
CHANGED
|
@@ -6,6 +6,134 @@ import { scanSources } from './source-scanner.js';
|
|
|
6
6
|
// --- ADD THESE CONSTANTS ---
|
|
7
7
|
const PRELUDE_VERSION = "1.0.0";
|
|
8
8
|
const SCHEMA_URL = "https://adjective.us/prelude/schemas/v1";
|
|
9
|
+
// --------------------------
|
|
10
|
+
// --- Minimal pyproject.toml parser ---
|
|
11
|
+
function setNested(obj, table, key, value) {
|
|
12
|
+
const parts = table ? table.split('.') : [];
|
|
13
|
+
let current = obj;
|
|
14
|
+
for (const part of parts) {
|
|
15
|
+
if (!(part in current))
|
|
16
|
+
current[part] = {};
|
|
17
|
+
current = current[part];
|
|
18
|
+
}
|
|
19
|
+
current[key] = value;
|
|
20
|
+
}
|
|
21
|
+
function parsePyprojectToml(content) {
|
|
22
|
+
const result = {};
|
|
23
|
+
let currentTable = '';
|
|
24
|
+
let collectingArray = null;
|
|
25
|
+
let collectingKey = '';
|
|
26
|
+
for (const line of content.split('\n')) {
|
|
27
|
+
const trimmed = line.trim();
|
|
28
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
29
|
+
if (collectingArray !== null)
|
|
30
|
+
continue;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
// Handle multiline array continuation
|
|
34
|
+
if (collectingArray !== null) {
|
|
35
|
+
if (trimmed === ']') {
|
|
36
|
+
setNested(result, currentTable, collectingKey, collectingArray);
|
|
37
|
+
collectingArray = null;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
// Inline table in array: {key = "value", ...}
|
|
41
|
+
if (trimmed.startsWith('{')) {
|
|
42
|
+
const obj = {};
|
|
43
|
+
const pairPattern = /(\w+)\s*=\s*["']([^"']*)["']/g;
|
|
44
|
+
let m;
|
|
45
|
+
while ((m = pairPattern.exec(trimmed)) !== null) {
|
|
46
|
+
obj[m[1]] = m[2];
|
|
47
|
+
}
|
|
48
|
+
if (Object.keys(obj).length > 0)
|
|
49
|
+
collectingArray.push(obj);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
// String in array: "value",
|
|
53
|
+
const strMatch = trimmed.match(/^["']([^"']*)["']/);
|
|
54
|
+
if (strMatch) {
|
|
55
|
+
collectingArray.push(strMatch[1]);
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// Table header: [section] or [section.subsection]
|
|
60
|
+
const tableMatch = trimmed.match(/^\[([^\]]+)\]$/);
|
|
61
|
+
if (tableMatch) {
|
|
62
|
+
currentTable = tableMatch[1];
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
// Key = value
|
|
66
|
+
const kvMatch = trimmed.match(/^([a-zA-Z_-][a-zA-Z0-9_.-]*)\s*=\s*(.*)$/);
|
|
67
|
+
if (!kvMatch)
|
|
68
|
+
continue;
|
|
69
|
+
const key = kvMatch[1].trim();
|
|
70
|
+
const rawValue = kvMatch[2].trim();
|
|
71
|
+
if (rawValue === '[') {
|
|
72
|
+
collectingArray = [];
|
|
73
|
+
collectingKey = key;
|
|
74
|
+
}
|
|
75
|
+
else if (rawValue.startsWith('[')) {
|
|
76
|
+
// Inline array
|
|
77
|
+
const items = [];
|
|
78
|
+
// Check for inline tables in array
|
|
79
|
+
const inlineTablePattern = /\{([^}]+)\}/g;
|
|
80
|
+
let itMatch;
|
|
81
|
+
while ((itMatch = inlineTablePattern.exec(rawValue)) !== null) {
|
|
82
|
+
const obj = {};
|
|
83
|
+
const pairPattern = /(\w+)\s*=\s*["']([^"']*)["']/g;
|
|
84
|
+
let m;
|
|
85
|
+
while ((m = pairPattern.exec(itMatch[1])) !== null) {
|
|
86
|
+
obj[m[1]] = m[2];
|
|
87
|
+
}
|
|
88
|
+
if (Object.keys(obj).length > 0)
|
|
89
|
+
items.push(obj);
|
|
90
|
+
}
|
|
91
|
+
if (items.length === 0) {
|
|
92
|
+
// Plain string array
|
|
93
|
+
const strPattern = /["']([^"']*)["']/g;
|
|
94
|
+
let m;
|
|
95
|
+
while ((m = strPattern.exec(rawValue)) !== null) {
|
|
96
|
+
items.push(m[1]);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
setNested(result, currentTable, key, items);
|
|
100
|
+
}
|
|
101
|
+
else if (rawValue.startsWith('{')) {
|
|
102
|
+
// Inline table
|
|
103
|
+
const obj = {};
|
|
104
|
+
const pairPattern = /(\w+)\s*=\s*["']([^"']*)["']/g;
|
|
105
|
+
let m;
|
|
106
|
+
while ((m = pairPattern.exec(rawValue)) !== null) {
|
|
107
|
+
obj[m[1]] = m[2];
|
|
108
|
+
}
|
|
109
|
+
setNested(result, currentTable, key, obj);
|
|
110
|
+
}
|
|
111
|
+
else if (rawValue.startsWith('"') || rawValue.startsWith("'")) {
|
|
112
|
+
const quote = rawValue[0];
|
|
113
|
+
const endIdx = rawValue.indexOf(quote, 1);
|
|
114
|
+
if (endIdx > 0) {
|
|
115
|
+
setNested(result, currentTable, key, rawValue.slice(1, endIdx));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else if (rawValue === 'true' || rawValue === 'false') {
|
|
119
|
+
setNested(result, currentTable, key, rawValue === 'true');
|
|
120
|
+
}
|
|
121
|
+
else if (/^\d+$/.test(rawValue)) {
|
|
122
|
+
setNested(result, currentTable, key, parseInt(rawValue, 10));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
function extractPyDepName(dep) {
|
|
128
|
+
return dep.split(/[>=<!~\[;@ ]/)[0].trim().toLowerCase();
|
|
129
|
+
}
|
|
130
|
+
function parseRequirementsTxt(content) {
|
|
131
|
+
return content.split('\n')
|
|
132
|
+
.map(line => line.trim())
|
|
133
|
+
.filter(line => line && !line.startsWith('#') && !line.startsWith('-') && !line.startsWith('git+'))
|
|
134
|
+
.map(line => extractPyDepName(line))
|
|
135
|
+
.filter(name => name.length > 0);
|
|
136
|
+
}
|
|
9
137
|
async function scanMonorepoPackages(rootDir) {
|
|
10
138
|
const packages = [];
|
|
11
139
|
// Check common monorepo locations
|
|
@@ -95,26 +223,12 @@ export async function inferProjectMetadata(rootDir) {
|
|
|
95
223
|
if (hasPackageJson) {
|
|
96
224
|
projectData = await readJSON(packageJsonPath);
|
|
97
225
|
}
|
|
98
|
-
|
|
99
|
-
let description = projectData.description || '
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const lines = readme.split('\n').filter(l => l.trim());
|
|
105
|
-
// Try to get first meaningful paragraph
|
|
106
|
-
const firstParagraph = lines.find(l => !l.startsWith('#') && l.length > 20);
|
|
107
|
-
if (firstParagraph && (!projectData.description || projectData.description === '')) {
|
|
108
|
-
description = firstParagraph.slice(0, 200);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
catch { }
|
|
112
|
-
}
|
|
113
|
-
const name = projectData.name || basename(rootDir);
|
|
114
|
-
const projectVersion = projectData.version; // Use the renamed field
|
|
115
|
-
const repository = projectData.repository?.url || projectData.repository;
|
|
116
|
-
const license = projectData.license;
|
|
117
|
-
const homepage = projectData.homepage;
|
|
226
|
+
let name = projectData.name || '';
|
|
227
|
+
let description = projectData.description || '';
|
|
228
|
+
let projectVersion = projectData.version;
|
|
229
|
+
let repository = projectData.repository?.url || projectData.repository;
|
|
230
|
+
let license = projectData.license;
|
|
231
|
+
let homepage = projectData.homepage;
|
|
118
232
|
// Detect team info from package.json
|
|
119
233
|
const team = [];
|
|
120
234
|
if (projectData.author) {
|
|
@@ -128,12 +242,67 @@ export async function inferProjectMetadata(rootDir) {
|
|
|
128
242
|
if (projectData.contributors) {
|
|
129
243
|
team.push(...projectData.contributors);
|
|
130
244
|
}
|
|
245
|
+
// Try pyproject.toml for Python projects
|
|
246
|
+
const pyprojectPath = join(rootDir, 'pyproject.toml');
|
|
247
|
+
if (await fileExists(pyprojectPath)) {
|
|
248
|
+
try {
|
|
249
|
+
const pyContent = await readFile(pyprojectPath, 'utf-8');
|
|
250
|
+
const pyproject = parsePyprojectToml(pyContent);
|
|
251
|
+
const proj = pyproject?.project || {};
|
|
252
|
+
if (!name && proj.name)
|
|
253
|
+
name = proj.name;
|
|
254
|
+
if (!description && proj.description)
|
|
255
|
+
description = proj.description;
|
|
256
|
+
if (!projectVersion && proj.version)
|
|
257
|
+
projectVersion = proj.version;
|
|
258
|
+
// License from pyproject.toml
|
|
259
|
+
if (!license) {
|
|
260
|
+
if (typeof proj.license === 'string')
|
|
261
|
+
license = proj.license;
|
|
262
|
+
else if (proj.license?.text)
|
|
263
|
+
license = proj.license.text;
|
|
264
|
+
}
|
|
265
|
+
// Authors from pyproject.toml
|
|
266
|
+
if (team.length === 0 && Array.isArray(proj.authors)) {
|
|
267
|
+
for (const author of proj.authors) {
|
|
268
|
+
if (typeof author === 'object' && author.name) {
|
|
269
|
+
team.push({ name: author.name, email: author.email });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Python version as runtime info
|
|
274
|
+
if (proj['requires-python']) {
|
|
275
|
+
// Store for later use but don't set here — stack handles runtime
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch { }
|
|
279
|
+
}
|
|
280
|
+
// Fall back to directory name for name
|
|
281
|
+
if (!name)
|
|
282
|
+
name = basename(rootDir);
|
|
283
|
+
// Fall back to README for description
|
|
284
|
+
if (!description) {
|
|
285
|
+
const readmePath = join(rootDir, 'README.md');
|
|
286
|
+
if (await fileExists(readmePath)) {
|
|
287
|
+
try {
|
|
288
|
+
const readme = await readFile(readmePath, 'utf-8');
|
|
289
|
+
const lines = readme.split('\n').filter(l => l.trim());
|
|
290
|
+
const firstParagraph = lines.find(l => !l.startsWith('#') && !l.startsWith('!') && !l.startsWith('[') && !l.startsWith('<') && !l.startsWith('---') && l.length > 20);
|
|
291
|
+
if (firstParagraph) {
|
|
292
|
+
description = firstParagraph.slice(0, 200);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
catch { }
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (!description)
|
|
299
|
+
description = 'No description provided';
|
|
131
300
|
return {
|
|
132
301
|
$schema: `${SCHEMA_URL}/project.schema.json`,
|
|
133
302
|
version: PRELUDE_VERSION,
|
|
134
303
|
name,
|
|
135
304
|
description,
|
|
136
|
-
projectVersion,
|
|
305
|
+
projectVersion,
|
|
137
306
|
createdAt: getCurrentTimestamp(),
|
|
138
307
|
updatedAt: getCurrentTimestamp(),
|
|
139
308
|
repository,
|
|
@@ -503,24 +672,220 @@ export async function inferStack(rootDir) {
|
|
|
503
672
|
// Check for Python project
|
|
504
673
|
const requirementsPath = join(rootDir, 'requirements.txt');
|
|
505
674
|
const pyprojectPath = join(rootDir, 'pyproject.toml');
|
|
506
|
-
if (await fileExists(
|
|
675
|
+
if (await fileExists(pyprojectPath) || await fileExists(requirementsPath)) {
|
|
507
676
|
stack.language = 'Python';
|
|
508
|
-
|
|
509
|
-
//
|
|
677
|
+
const allPyDeps = new Set();
|
|
678
|
+
// Parse pyproject.toml
|
|
679
|
+
if (await fileExists(pyprojectPath)) {
|
|
680
|
+
try {
|
|
681
|
+
const pyContent = await readFile(pyprojectPath, 'utf-8');
|
|
682
|
+
const pyproject = parsePyprojectToml(pyContent);
|
|
683
|
+
// Package manager detection
|
|
684
|
+
const buildBackend = pyproject?.['build-system']?.['build-backend'] || '';
|
|
685
|
+
if (buildBackend.includes('poetry')) {
|
|
686
|
+
stack.packageManager = 'poetry';
|
|
687
|
+
}
|
|
688
|
+
else if (await fileExists(join(rootDir, 'uv.lock'))) {
|
|
689
|
+
stack.packageManager = 'uv';
|
|
690
|
+
}
|
|
691
|
+
else if (await fileExists(join(rootDir, 'poetry.lock'))) {
|
|
692
|
+
stack.packageManager = 'poetry';
|
|
693
|
+
}
|
|
694
|
+
else if (await fileExists(join(rootDir, 'Pipfile')) || await fileExists(join(rootDir, 'Pipfile.lock'))) {
|
|
695
|
+
stack.packageManager = 'pipenv';
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
stack.packageManager = 'pip';
|
|
699
|
+
}
|
|
700
|
+
// Python version
|
|
701
|
+
const requiresPython = pyproject?.project?.['requires-python'];
|
|
702
|
+
if (requiresPython) {
|
|
703
|
+
stack.runtime = `Python ${requiresPython}`;
|
|
704
|
+
}
|
|
705
|
+
// Collect deps from [project.dependencies]
|
|
706
|
+
const projDeps = pyproject?.project?.dependencies || [];
|
|
707
|
+
const depsRecord = {};
|
|
708
|
+
for (const dep of projDeps) {
|
|
709
|
+
const depName = extractPyDepName(dep);
|
|
710
|
+
const version = dep.slice(depName.length).replace(/^\[.*?\]/, '').trim() || '*';
|
|
711
|
+
depsRecord[depName] = version;
|
|
712
|
+
allPyDeps.add(depName);
|
|
713
|
+
}
|
|
714
|
+
if (Object.keys(depsRecord).length > 0) {
|
|
715
|
+
stack.dependencies = depsRecord;
|
|
716
|
+
}
|
|
717
|
+
// Collect dev/optional deps from [project.optional-dependencies]
|
|
718
|
+
const optDeps = pyproject?.project?.['optional-dependencies'] || {};
|
|
719
|
+
const devDepsRecord = {};
|
|
720
|
+
for (const [, groupDeps] of Object.entries(optDeps)) {
|
|
721
|
+
if (Array.isArray(groupDeps)) {
|
|
722
|
+
for (const dep of groupDeps) {
|
|
723
|
+
if (typeof dep === 'string') {
|
|
724
|
+
const depName = extractPyDepName(dep);
|
|
725
|
+
const version = dep.slice(depName.length).replace(/^\[.*?\]/, '').trim() || '*';
|
|
726
|
+
devDepsRecord[depName] = version;
|
|
727
|
+
allPyDeps.add(depName);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (Object.keys(devDepsRecord).length > 0) {
|
|
733
|
+
stack.devDependencies = devDepsRecord;
|
|
734
|
+
}
|
|
735
|
+
// Also check Poetry-style deps under [tool.poetry.dependencies]
|
|
736
|
+
const poetryDeps = pyproject?.tool?.poetry?.dependencies;
|
|
737
|
+
if (poetryDeps && typeof poetryDeps === 'object') {
|
|
738
|
+
for (const depName of Object.keys(poetryDeps)) {
|
|
739
|
+
if (depName !== 'python')
|
|
740
|
+
allPyDeps.add(depName.toLowerCase());
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
catch { }
|
|
745
|
+
}
|
|
746
|
+
// Parse requirements.txt as fallback/supplement
|
|
510
747
|
if (await fileExists(requirementsPath)) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
748
|
+
try {
|
|
749
|
+
const reqContent = await readFile(requirementsPath, 'utf-8');
|
|
750
|
+
const reqDeps = parseRequirementsTxt(reqContent);
|
|
751
|
+
for (const dep of reqDeps)
|
|
752
|
+
allPyDeps.add(dep);
|
|
753
|
+
if (!stack.dependencies || Object.keys(stack.dependencies).length === 0) {
|
|
754
|
+
const depsRecord = {};
|
|
755
|
+
for (const dep of reqDeps)
|
|
756
|
+
depsRecord[dep] = '*';
|
|
757
|
+
stack.dependencies = depsRecord;
|
|
758
|
+
stack.packageManager = stack.packageManager || 'pip';
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
catch { }
|
|
762
|
+
}
|
|
763
|
+
// === FRAMEWORKS ===
|
|
764
|
+
const pyFrameworks = [];
|
|
765
|
+
// Web frameworks
|
|
766
|
+
if (allPyDeps.has('django'))
|
|
767
|
+
pyFrameworks.push('Django');
|
|
768
|
+
if (allPyDeps.has('flask'))
|
|
769
|
+
pyFrameworks.push('Flask');
|
|
770
|
+
if (allPyDeps.has('fastapi'))
|
|
771
|
+
pyFrameworks.push('FastAPI');
|
|
772
|
+
if (allPyDeps.has('starlette'))
|
|
773
|
+
pyFrameworks.push('Starlette');
|
|
774
|
+
if (allPyDeps.has('tornado'))
|
|
775
|
+
pyFrameworks.push('Tornado');
|
|
776
|
+
if (allPyDeps.has('pyramid'))
|
|
777
|
+
pyFrameworks.push('Pyramid');
|
|
778
|
+
if (allPyDeps.has('sanic'))
|
|
779
|
+
pyFrameworks.push('Sanic');
|
|
780
|
+
if (allPyDeps.has('falcon'))
|
|
781
|
+
pyFrameworks.push('Falcon');
|
|
782
|
+
if (allPyDeps.has('litestar'))
|
|
783
|
+
pyFrameworks.push('Litestar');
|
|
784
|
+
// CLI frameworks
|
|
785
|
+
if (allPyDeps.has('typer'))
|
|
786
|
+
pyFrameworks.push('Typer');
|
|
787
|
+
if (allPyDeps.has('click'))
|
|
788
|
+
pyFrameworks.push('Click');
|
|
789
|
+
// ML/AI frameworks
|
|
790
|
+
if (allPyDeps.has('langchain'))
|
|
791
|
+
pyFrameworks.push('LangChain');
|
|
792
|
+
if (allPyDeps.has('transformers'))
|
|
793
|
+
pyFrameworks.push('Hugging Face Transformers');
|
|
794
|
+
if (allPyDeps.has('torch') || allPyDeps.has('pytorch'))
|
|
795
|
+
pyFrameworks.push('PyTorch');
|
|
796
|
+
if (allPyDeps.has('tensorflow'))
|
|
797
|
+
pyFrameworks.push('TensorFlow');
|
|
798
|
+
if (allPyDeps.has('scikit-learn') || allPyDeps.has('sklearn'))
|
|
799
|
+
pyFrameworks.push('scikit-learn');
|
|
800
|
+
// Data
|
|
801
|
+
if (allPyDeps.has('pandas'))
|
|
802
|
+
pyFrameworks.push('pandas');
|
|
803
|
+
if (allPyDeps.has('numpy'))
|
|
804
|
+
pyFrameworks.push('NumPy');
|
|
805
|
+
if (pyFrameworks.length > 0) {
|
|
806
|
+
stack.frameworks = pyFrameworks;
|
|
807
|
+
stack.framework = pyFrameworks[0];
|
|
808
|
+
}
|
|
809
|
+
// === TESTING ===
|
|
810
|
+
const pyTesting = [];
|
|
811
|
+
if (allPyDeps.has('pytest'))
|
|
812
|
+
pyTesting.push('pytest');
|
|
813
|
+
if (allPyDeps.has('pytest-cov'))
|
|
814
|
+
pyTesting.push('pytest-cov');
|
|
815
|
+
if (allPyDeps.has('hypothesis'))
|
|
816
|
+
pyTesting.push('Hypothesis');
|
|
817
|
+
if (allPyDeps.has('tox'))
|
|
818
|
+
pyTesting.push('tox');
|
|
819
|
+
if (allPyDeps.has('nox'))
|
|
820
|
+
pyTesting.push('nox');
|
|
821
|
+
if (allPyDeps.has('coverage'))
|
|
822
|
+
pyTesting.push('coverage');
|
|
823
|
+
if (pyTesting.length > 0) {
|
|
824
|
+
stack.testingFrameworks = pyTesting;
|
|
825
|
+
}
|
|
826
|
+
// === BUILD TOOLS ===
|
|
827
|
+
const pyBuildTools = [];
|
|
828
|
+
if (allPyDeps.has('setuptools'))
|
|
829
|
+
pyBuildTools.push('setuptools');
|
|
830
|
+
if (allPyDeps.has('wheel'))
|
|
831
|
+
pyBuildTools.push('wheel');
|
|
832
|
+
if (allPyDeps.has('cython'))
|
|
833
|
+
pyBuildTools.push('Cython');
|
|
834
|
+
if (allPyDeps.has('maturin'))
|
|
835
|
+
pyBuildTools.push('Maturin');
|
|
836
|
+
if (pyBuildTools.length > 0) {
|
|
837
|
+
stack.buildTools = pyBuildTools;
|
|
838
|
+
}
|
|
839
|
+
// === ORM/DATABASE ===
|
|
840
|
+
if (allPyDeps.has('sqlalchemy'))
|
|
841
|
+
stack.orm = 'SQLAlchemy';
|
|
842
|
+
else if (allPyDeps.has('tortoise-orm'))
|
|
843
|
+
stack.orm = 'Tortoise ORM';
|
|
844
|
+
else if (allPyDeps.has('peewee'))
|
|
845
|
+
stack.orm = 'Peewee';
|
|
846
|
+
else if (allPyDeps.has('mongoengine'))
|
|
847
|
+
stack.orm = 'MongoEngine';
|
|
848
|
+
else if (allPyDeps.has('django'))
|
|
849
|
+
stack.orm = 'Django ORM';
|
|
850
|
+
const pyDatabases = [];
|
|
851
|
+
if (allPyDeps.has('psycopg2') || allPyDeps.has('psycopg2-binary') || allPyDeps.has('psycopg') || allPyDeps.has('asyncpg'))
|
|
852
|
+
pyDatabases.push('PostgreSQL');
|
|
853
|
+
if (allPyDeps.has('pymongo') || allPyDeps.has('motor'))
|
|
854
|
+
pyDatabases.push('MongoDB');
|
|
855
|
+
if (allPyDeps.has('redis') || allPyDeps.has('aioredis'))
|
|
856
|
+
pyDatabases.push('Redis');
|
|
857
|
+
if (allPyDeps.has('pymysql') || allPyDeps.has('mysqlclient'))
|
|
858
|
+
pyDatabases.push('MySQL');
|
|
859
|
+
if (allPyDeps.has('aiosqlite'))
|
|
860
|
+
pyDatabases.push('SQLite');
|
|
861
|
+
if (allPyDeps.has('supabase'))
|
|
862
|
+
pyDatabases.push('Supabase');
|
|
863
|
+
if (pyDatabases.length > 0) {
|
|
864
|
+
stack.database = pyDatabases.join(', ');
|
|
865
|
+
}
|
|
866
|
+
// === DEPLOYMENT ===
|
|
867
|
+
if (!stack.deployment) {
|
|
868
|
+
if (await fileExists(join(rootDir, 'Dockerfile')))
|
|
869
|
+
stack.deployment = 'Docker';
|
|
870
|
+
else if (await fileExists(join(rootDir, 'Procfile')))
|
|
871
|
+
stack.deployment = 'Heroku';
|
|
872
|
+
else if (await fileExists(join(rootDir, 'fly.toml')))
|
|
873
|
+
stack.deployment = 'Fly.io';
|
|
874
|
+
else if (await fileExists(join(rootDir, 'render.yaml')))
|
|
875
|
+
stack.deployment = 'Render';
|
|
876
|
+
}
|
|
877
|
+
// === CI/CD ===
|
|
878
|
+
if (!stack.cicd || stack.cicd.length === 0) {
|
|
879
|
+
const cicd = [];
|
|
880
|
+
if (await fileExists(join(rootDir, '.github/workflows')))
|
|
881
|
+
cicd.push('GitHub Actions');
|
|
882
|
+
if (await fileExists(join(rootDir, '.gitlab-ci.yml')))
|
|
883
|
+
cicd.push('GitLab CI');
|
|
884
|
+
if (await fileExists(join(rootDir, '.circleci')))
|
|
885
|
+
cicd.push('CircleCI');
|
|
886
|
+
if (await fileExists(join(rootDir, 'tox.ini')))
|
|
887
|
+
cicd.push('tox');
|
|
888
|
+
stack.cicd = cicd;
|
|
524
889
|
}
|
|
525
890
|
}
|
|
526
891
|
// Check for Rust project
|
|
@@ -608,12 +973,52 @@ export async function inferArchitecture(rootDir) {
|
|
|
608
973
|
const hasPackages = relativeDirs.some(d => d === 'packages');
|
|
609
974
|
const hasApps = relativeDirs.some(d => d === 'apps');
|
|
610
975
|
const hasServices = relativeDirs.some(d => d === 'services');
|
|
976
|
+
// Check for Python CLI entry points from pyproject.toml
|
|
977
|
+
let hasPythonScripts = false;
|
|
978
|
+
let hasPythonWebFramework = false;
|
|
979
|
+
const pyprojectArchPath = join(rootDir, 'pyproject.toml');
|
|
980
|
+
if (await fileExists(pyprojectArchPath)) {
|
|
981
|
+
try {
|
|
982
|
+
const pyContent = await readFile(pyprojectArchPath, 'utf-8');
|
|
983
|
+
const pyproject = parsePyprojectToml(pyContent);
|
|
984
|
+
// CLI entry points from [project.scripts]
|
|
985
|
+
const scripts = pyproject?.project?.scripts;
|
|
986
|
+
if (scripts && typeof scripts === 'object' && Object.keys(scripts).length > 0) {
|
|
987
|
+
hasPythonScripts = true;
|
|
988
|
+
for (const [cmdName, entryPoint] of Object.entries(scripts)) {
|
|
989
|
+
if (typeof entryPoint === 'string') {
|
|
990
|
+
architecture.entryPoints = architecture.entryPoints || [];
|
|
991
|
+
architecture.entryPoints.push({
|
|
992
|
+
file: entryPoint,
|
|
993
|
+
purpose: `CLI command: ${cmdName}`
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
// Check for web framework deps
|
|
999
|
+
const deps = pyproject?.project?.dependencies || [];
|
|
1000
|
+
const depNames = deps.map(extractPyDepName);
|
|
1001
|
+
hasPythonWebFramework = depNames.some(d => ['django', 'flask', 'fastapi', 'starlette', 'sanic', 'tornado', 'falcon', 'litestar'].includes(d));
|
|
1002
|
+
}
|
|
1003
|
+
catch { }
|
|
1004
|
+
}
|
|
1005
|
+
// Python-specific entry points
|
|
1006
|
+
if (await fileExists(join(rootDir, 'manage.py'))) {
|
|
1007
|
+
architecture.entryPoints = architecture.entryPoints || [];
|
|
1008
|
+
architecture.entryPoints.push({ file: 'manage.py', purpose: 'Django management' });
|
|
1009
|
+
}
|
|
611
1010
|
if (hasPackages || hasApps) {
|
|
612
1011
|
architecture.type = 'monorepo';
|
|
613
1012
|
}
|
|
614
1013
|
else if (hasServices) {
|
|
615
1014
|
architecture.type = 'microservices';
|
|
616
1015
|
}
|
|
1016
|
+
else if (hasPythonScripts && !hasPythonWebFramework) {
|
|
1017
|
+
architecture.type = 'cli';
|
|
1018
|
+
}
|
|
1019
|
+
else if (hasPythonWebFramework) {
|
|
1020
|
+
architecture.type = 'backend';
|
|
1021
|
+
}
|
|
617
1022
|
else if (hasApp && hasSrc) {
|
|
618
1023
|
architecture.type = 'fullstack';
|
|
619
1024
|
}
|
|
@@ -700,6 +1105,12 @@ export async function inferArchitecture(rootDir) {
|
|
|
700
1105
|
patterns.push('Feature-based organization');
|
|
701
1106
|
if (relativeDirs.some(d => d.includes('modules')))
|
|
702
1107
|
patterns.push('Module pattern');
|
|
1108
|
+
if (relativeDirs.some(d => d.includes('agents')))
|
|
1109
|
+
patterns.push('Agent-based architecture');
|
|
1110
|
+
if (relativeDirs.some(d => d.includes('missions') || d.includes('pipelines')))
|
|
1111
|
+
patterns.push('Pipeline pattern');
|
|
1112
|
+
if (relativeDirs.some(d => d.includes('models') && !d.includes('node_modules')))
|
|
1113
|
+
patterns.push('Data models');
|
|
703
1114
|
architecture.patterns = patterns;
|
|
704
1115
|
// Detect conventions
|
|
705
1116
|
const conventions = [];
|
|
@@ -718,9 +1129,33 @@ export async function inferArchitecture(rootDir) {
|
|
|
718
1129
|
if (await fileExists(join(rootDir, '.husky'))) {
|
|
719
1130
|
conventions.push('Git hooks (Husky)');
|
|
720
1131
|
}
|
|
1132
|
+
// Python conventions
|
|
1133
|
+
if (await fileExists(pyprojectArchPath)) {
|
|
1134
|
+
try {
|
|
1135
|
+
const pyContent = await readFile(pyprojectArchPath, 'utf-8');
|
|
1136
|
+
const pyproject = parsePyprojectToml(pyContent);
|
|
1137
|
+
if (pyproject?.tool?.ruff)
|
|
1138
|
+
conventions.push('Ruff code linting');
|
|
1139
|
+
if (pyproject?.tool?.black)
|
|
1140
|
+
conventions.push('Black code formatting');
|
|
1141
|
+
if (pyproject?.tool?.mypy)
|
|
1142
|
+
conventions.push('mypy type checking');
|
|
1143
|
+
if (pyproject?.tool?.isort)
|
|
1144
|
+
conventions.push('isort import sorting');
|
|
1145
|
+
if (pyproject?.tool?.pytest)
|
|
1146
|
+
conventions.push('pytest configuration');
|
|
1147
|
+
}
|
|
1148
|
+
catch { }
|
|
1149
|
+
}
|
|
1150
|
+
if (await fileExists(join(rootDir, '.flake8'))) {
|
|
1151
|
+
conventions.push('flake8 code linting');
|
|
1152
|
+
}
|
|
1153
|
+
if (await fileExists(join(rootDir, 'mypy.ini')) || await fileExists(join(rootDir, '.mypy.ini'))) {
|
|
1154
|
+
conventions.push('mypy type checking');
|
|
1155
|
+
}
|
|
721
1156
|
architecture.conventions = conventions;
|
|
722
|
-
// Detect entry points
|
|
723
|
-
const entryPoints = [];
|
|
1157
|
+
// Detect entry points (preserve any already detected, e.g. from pyproject.toml)
|
|
1158
|
+
const entryPoints = architecture.entryPoints || [];
|
|
724
1159
|
if (await fileExists(join(rootDir, 'src/index.ts')))
|
|
725
1160
|
entryPoints.push({ file: 'src/index.ts', purpose: 'Main entry point' });
|
|
726
1161
|
else if (await fileExists(join(rootDir, 'src/index.tsx')))
|
|
@@ -847,6 +1282,73 @@ export async function inferConstraints(rootDir) {
|
|
|
847
1282
|
formatter: 'Prettier'
|
|
848
1283
|
};
|
|
849
1284
|
}
|
|
1285
|
+
// Python code style tools
|
|
1286
|
+
const pyprojectConstraintsPath = join(rootDir, 'pyproject.toml');
|
|
1287
|
+
if (await fileExists(pyprojectConstraintsPath)) {
|
|
1288
|
+
try {
|
|
1289
|
+
const pyContent = await readFile(pyprojectConstraintsPath, 'utf-8');
|
|
1290
|
+
const pyproject = parsePyprojectToml(pyContent);
|
|
1291
|
+
// Linter
|
|
1292
|
+
if (pyproject?.tool?.ruff) {
|
|
1293
|
+
constraints.codeStyle = {
|
|
1294
|
+
...constraints.codeStyle,
|
|
1295
|
+
linter: 'Ruff'
|
|
1296
|
+
};
|
|
1297
|
+
const targetVersion = pyproject.tool.ruff['target-version'];
|
|
1298
|
+
if (targetVersion && typeof targetVersion === 'string') {
|
|
1299
|
+
// py311 → 3.11, py39 → 3.9
|
|
1300
|
+
const nums = targetVersion.replace('py', '');
|
|
1301
|
+
const major = nums[0];
|
|
1302
|
+
const minor = nums.slice(1);
|
|
1303
|
+
if (major && minor) {
|
|
1304
|
+
constraints.mustUse?.push(`Python ${major}.${minor}+`);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
const lineLength = pyproject.tool.ruff['line-length'];
|
|
1308
|
+
if (lineLength) {
|
|
1309
|
+
constraints.preferences?.push({
|
|
1310
|
+
category: 'Code Style',
|
|
1311
|
+
preference: `Line length: ${lineLength}`,
|
|
1312
|
+
rationale: 'Ruff configuration'
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
if (pyproject?.tool?.black) {
|
|
1317
|
+
constraints.codeStyle = {
|
|
1318
|
+
...constraints.codeStyle,
|
|
1319
|
+
formatter: 'Black'
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
if (pyproject?.tool?.mypy) {
|
|
1323
|
+
constraints.mustUse?.push('mypy for type checking');
|
|
1324
|
+
}
|
|
1325
|
+
// Python version constraint (only if not already added from Ruff target-version)
|
|
1326
|
+
const requiresPython = pyproject?.project?.['requires-python'];
|
|
1327
|
+
if (requiresPython && !constraints.mustUse?.some(m => m.startsWith('Python '))) {
|
|
1328
|
+
constraints.mustUse?.push(`Python ${requiresPython}`);
|
|
1329
|
+
}
|
|
1330
|
+
// Testing from pyproject
|
|
1331
|
+
const deps = pyproject?.project?.dependencies || [];
|
|
1332
|
+
const optDeps = pyproject?.project?.['optional-dependencies'] || {};
|
|
1333
|
+
const allDepNames = new Set([
|
|
1334
|
+
...deps.map(extractPyDepName),
|
|
1335
|
+
...Object.values(optDeps).flat().filter((d) => typeof d === 'string').map(extractPyDepName)
|
|
1336
|
+
]);
|
|
1337
|
+
if (allDepNames.has('pytest') || pyproject?.tool?.pytest) {
|
|
1338
|
+
constraints.testing = {
|
|
1339
|
+
required: true,
|
|
1340
|
+
strategy: 'pytest'
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
catch { }
|
|
1345
|
+
}
|
|
1346
|
+
if (await fileExists(join(rootDir, '.flake8'))) {
|
|
1347
|
+
constraints.codeStyle = {
|
|
1348
|
+
...constraints.codeStyle,
|
|
1349
|
+
linter: constraints.codeStyle?.linter || 'flake8'
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
850
1352
|
// Check for TypeScript
|
|
851
1353
|
if (await fileExists(join(rootDir, 'tsconfig.json'))) {
|
|
852
1354
|
constraints.mustUse?.push('TypeScript for type safety');
|