prelude-context 1.5.0 → 1.7.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 +63 -4
- package/dist/bin/prelude.js +2 -0
- package/dist/bin/prelude.js.map +1 -1
- package/dist/src/commands/export.d.ts.map +1 -1
- package/dist/src/commands/export.js +5 -3
- package/dist/src/commands/export.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +151 -0
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/validate.d.ts +3 -0
- package/dist/src/commands/validate.d.ts.map +1 -0
- package/dist/src/commands/validate.js +203 -0
- package/dist/src/commands/validate.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 +11 -1
- package/dist/src/constants.js.map +1 -1
- package/dist/src/core/claude-md-parser.d.ts +33 -0
- package/dist/src/core/claude-md-parser.d.ts.map +1 -0
- package/dist/src/core/claude-md-parser.js +410 -0
- package/dist/src/core/claude-md-parser.js.map +1 -0
- package/dist/src/core/exporter.d.ts +2 -1
- package/dist/src/core/exporter.d.ts.map +1 -1
- package/dist/src/core/exporter.js +159 -1
- package/dist/src/core/exporter.js.map +1 -1
- package/dist/src/core/infer.d.ts.map +1 -1
- package/dist/src/core/infer.js +936 -52
- 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 +152 -8
- package/dist/src/core/source-scanner.js.map +1 -1
- 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/utils/fs.d.ts.map +1 -1
- package/dist/src/utils/fs.js +4 -1
- package/dist/src/utils/fs.js.map +1 -1
- package/package.json +11 -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,110 @@ 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
|
+
// Try Cargo.toml for Rust projects
|
|
281
|
+
const cargoProjectPath = join(rootDir, 'Cargo.toml');
|
|
282
|
+
if (await fileExists(cargoProjectPath)) {
|
|
283
|
+
try {
|
|
284
|
+
const cargoContent = await readFile(cargoProjectPath, 'utf-8');
|
|
285
|
+
const cargo = parsePyprojectToml(cargoContent);
|
|
286
|
+
const pkg = cargo?.package || {};
|
|
287
|
+
if (!name && pkg.name)
|
|
288
|
+
name = pkg.name;
|
|
289
|
+
if (!description && pkg.description)
|
|
290
|
+
description = pkg.description;
|
|
291
|
+
if (!projectVersion && pkg.version)
|
|
292
|
+
projectVersion = pkg.version;
|
|
293
|
+
if (!license && pkg.license)
|
|
294
|
+
license = pkg.license;
|
|
295
|
+
if (!repository && pkg.repository)
|
|
296
|
+
repository = pkg.repository;
|
|
297
|
+
if (!homepage && pkg.homepage)
|
|
298
|
+
homepage = pkg.homepage;
|
|
299
|
+
if (team.length === 0 && Array.isArray(pkg.authors)) {
|
|
300
|
+
for (const author of pkg.authors) {
|
|
301
|
+
if (typeof author === 'string') {
|
|
302
|
+
team.push({ name: author.replace(/<.*>/, '').trim() });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch { }
|
|
308
|
+
}
|
|
309
|
+
// Try go.mod for Go projects (module name only)
|
|
310
|
+
const goModProjectPath = join(rootDir, 'go.mod');
|
|
311
|
+
if (await fileExists(goModProjectPath) && !name) {
|
|
312
|
+
try {
|
|
313
|
+
const goModContent = await readFile(goModProjectPath, 'utf-8');
|
|
314
|
+
const moduleMatch = goModContent.match(/^module\s+(\S+)/m);
|
|
315
|
+
if (moduleMatch) {
|
|
316
|
+
// Use the last segment of the module path as name
|
|
317
|
+
const parts = moduleMatch[1].split('/');
|
|
318
|
+
name = parts[parts.length - 1];
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch { }
|
|
322
|
+
}
|
|
323
|
+
// Fall back to directory name for name
|
|
324
|
+
if (!name)
|
|
325
|
+
name = basename(rootDir);
|
|
326
|
+
// Fall back to README for description
|
|
327
|
+
if (!description) {
|
|
328
|
+
const readmePath = join(rootDir, 'README.md');
|
|
329
|
+
if (await fileExists(readmePath)) {
|
|
330
|
+
try {
|
|
331
|
+
const readme = await readFile(readmePath, 'utf-8');
|
|
332
|
+
const lines = readme.split('\n').filter(l => l.trim());
|
|
333
|
+
const firstParagraph = lines.find(l => !l.startsWith('#') && !l.startsWith('!') && !l.startsWith('[') && !l.startsWith('<') && !l.startsWith('---') && l.length > 20);
|
|
334
|
+
if (firstParagraph) {
|
|
335
|
+
description = firstParagraph.slice(0, 200);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
catch { }
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (!description)
|
|
342
|
+
description = 'No description provided';
|
|
131
343
|
return {
|
|
132
344
|
$schema: `${SCHEMA_URL}/project.schema.json`,
|
|
133
345
|
version: PRELUDE_VERSION,
|
|
134
346
|
name,
|
|
135
347
|
description,
|
|
136
|
-
projectVersion,
|
|
348
|
+
projectVersion,
|
|
137
349
|
createdAt: getCurrentTimestamp(),
|
|
138
350
|
updatedAt: getCurrentTimestamp(),
|
|
139
351
|
repository,
|
|
@@ -503,24 +715,220 @@ export async function inferStack(rootDir) {
|
|
|
503
715
|
// Check for Python project
|
|
504
716
|
const requirementsPath = join(rootDir, 'requirements.txt');
|
|
505
717
|
const pyprojectPath = join(rootDir, 'pyproject.toml');
|
|
506
|
-
if (await fileExists(
|
|
718
|
+
if (await fileExists(pyprojectPath) || await fileExists(requirementsPath)) {
|
|
507
719
|
stack.language = 'Python';
|
|
508
|
-
|
|
509
|
-
//
|
|
720
|
+
const allPyDeps = new Set();
|
|
721
|
+
// Parse pyproject.toml
|
|
722
|
+
if (await fileExists(pyprojectPath)) {
|
|
723
|
+
try {
|
|
724
|
+
const pyContent = await readFile(pyprojectPath, 'utf-8');
|
|
725
|
+
const pyproject = parsePyprojectToml(pyContent);
|
|
726
|
+
// Package manager detection
|
|
727
|
+
const buildBackend = pyproject?.['build-system']?.['build-backend'] || '';
|
|
728
|
+
if (buildBackend.includes('poetry')) {
|
|
729
|
+
stack.packageManager = 'poetry';
|
|
730
|
+
}
|
|
731
|
+
else if (await fileExists(join(rootDir, 'uv.lock'))) {
|
|
732
|
+
stack.packageManager = 'uv';
|
|
733
|
+
}
|
|
734
|
+
else if (await fileExists(join(rootDir, 'poetry.lock'))) {
|
|
735
|
+
stack.packageManager = 'poetry';
|
|
736
|
+
}
|
|
737
|
+
else if (await fileExists(join(rootDir, 'Pipfile')) || await fileExists(join(rootDir, 'Pipfile.lock'))) {
|
|
738
|
+
stack.packageManager = 'pipenv';
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
stack.packageManager = 'pip';
|
|
742
|
+
}
|
|
743
|
+
// Python version
|
|
744
|
+
const requiresPython = pyproject?.project?.['requires-python'];
|
|
745
|
+
if (requiresPython) {
|
|
746
|
+
stack.runtime = `Python ${requiresPython}`;
|
|
747
|
+
}
|
|
748
|
+
// Collect deps from [project.dependencies]
|
|
749
|
+
const projDeps = pyproject?.project?.dependencies || [];
|
|
750
|
+
const depsRecord = {};
|
|
751
|
+
for (const dep of projDeps) {
|
|
752
|
+
const depName = extractPyDepName(dep);
|
|
753
|
+
const version = dep.slice(depName.length).replace(/^\[.*?\]/, '').trim() || '*';
|
|
754
|
+
depsRecord[depName] = version;
|
|
755
|
+
allPyDeps.add(depName);
|
|
756
|
+
}
|
|
757
|
+
if (Object.keys(depsRecord).length > 0) {
|
|
758
|
+
stack.dependencies = depsRecord;
|
|
759
|
+
}
|
|
760
|
+
// Collect dev/optional deps from [project.optional-dependencies]
|
|
761
|
+
const optDeps = pyproject?.project?.['optional-dependencies'] || {};
|
|
762
|
+
const devDepsRecord = {};
|
|
763
|
+
for (const [, groupDeps] of Object.entries(optDeps)) {
|
|
764
|
+
if (Array.isArray(groupDeps)) {
|
|
765
|
+
for (const dep of groupDeps) {
|
|
766
|
+
if (typeof dep === 'string') {
|
|
767
|
+
const depName = extractPyDepName(dep);
|
|
768
|
+
const version = dep.slice(depName.length).replace(/^\[.*?\]/, '').trim() || '*';
|
|
769
|
+
devDepsRecord[depName] = version;
|
|
770
|
+
allPyDeps.add(depName);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (Object.keys(devDepsRecord).length > 0) {
|
|
776
|
+
stack.devDependencies = devDepsRecord;
|
|
777
|
+
}
|
|
778
|
+
// Also check Poetry-style deps under [tool.poetry.dependencies]
|
|
779
|
+
const poetryDeps = pyproject?.tool?.poetry?.dependencies;
|
|
780
|
+
if (poetryDeps && typeof poetryDeps === 'object') {
|
|
781
|
+
for (const depName of Object.keys(poetryDeps)) {
|
|
782
|
+
if (depName !== 'python')
|
|
783
|
+
allPyDeps.add(depName.toLowerCase());
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
catch { }
|
|
788
|
+
}
|
|
789
|
+
// Parse requirements.txt as fallback/supplement
|
|
510
790
|
if (await fileExists(requirementsPath)) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
791
|
+
try {
|
|
792
|
+
const reqContent = await readFile(requirementsPath, 'utf-8');
|
|
793
|
+
const reqDeps = parseRequirementsTxt(reqContent);
|
|
794
|
+
for (const dep of reqDeps)
|
|
795
|
+
allPyDeps.add(dep);
|
|
796
|
+
if (!stack.dependencies || Object.keys(stack.dependencies).length === 0) {
|
|
797
|
+
const depsRecord = {};
|
|
798
|
+
for (const dep of reqDeps)
|
|
799
|
+
depsRecord[dep] = '*';
|
|
800
|
+
stack.dependencies = depsRecord;
|
|
801
|
+
stack.packageManager = stack.packageManager || 'pip';
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
catch { }
|
|
805
|
+
}
|
|
806
|
+
// === FRAMEWORKS ===
|
|
807
|
+
const pyFrameworks = [];
|
|
808
|
+
// Web frameworks
|
|
809
|
+
if (allPyDeps.has('django'))
|
|
810
|
+
pyFrameworks.push('Django');
|
|
811
|
+
if (allPyDeps.has('flask'))
|
|
812
|
+
pyFrameworks.push('Flask');
|
|
813
|
+
if (allPyDeps.has('fastapi'))
|
|
814
|
+
pyFrameworks.push('FastAPI');
|
|
815
|
+
if (allPyDeps.has('starlette'))
|
|
816
|
+
pyFrameworks.push('Starlette');
|
|
817
|
+
if (allPyDeps.has('tornado'))
|
|
818
|
+
pyFrameworks.push('Tornado');
|
|
819
|
+
if (allPyDeps.has('pyramid'))
|
|
820
|
+
pyFrameworks.push('Pyramid');
|
|
821
|
+
if (allPyDeps.has('sanic'))
|
|
822
|
+
pyFrameworks.push('Sanic');
|
|
823
|
+
if (allPyDeps.has('falcon'))
|
|
824
|
+
pyFrameworks.push('Falcon');
|
|
825
|
+
if (allPyDeps.has('litestar'))
|
|
826
|
+
pyFrameworks.push('Litestar');
|
|
827
|
+
// CLI frameworks
|
|
828
|
+
if (allPyDeps.has('typer'))
|
|
829
|
+
pyFrameworks.push('Typer');
|
|
830
|
+
if (allPyDeps.has('click'))
|
|
831
|
+
pyFrameworks.push('Click');
|
|
832
|
+
// ML/AI frameworks
|
|
833
|
+
if (allPyDeps.has('langchain'))
|
|
834
|
+
pyFrameworks.push('LangChain');
|
|
835
|
+
if (allPyDeps.has('transformers'))
|
|
836
|
+
pyFrameworks.push('Hugging Face Transformers');
|
|
837
|
+
if (allPyDeps.has('torch') || allPyDeps.has('pytorch'))
|
|
838
|
+
pyFrameworks.push('PyTorch');
|
|
839
|
+
if (allPyDeps.has('tensorflow'))
|
|
840
|
+
pyFrameworks.push('TensorFlow');
|
|
841
|
+
if (allPyDeps.has('scikit-learn') || allPyDeps.has('sklearn'))
|
|
842
|
+
pyFrameworks.push('scikit-learn');
|
|
843
|
+
// Data
|
|
844
|
+
if (allPyDeps.has('pandas'))
|
|
845
|
+
pyFrameworks.push('pandas');
|
|
846
|
+
if (allPyDeps.has('numpy'))
|
|
847
|
+
pyFrameworks.push('NumPy');
|
|
848
|
+
if (pyFrameworks.length > 0) {
|
|
849
|
+
stack.frameworks = pyFrameworks;
|
|
850
|
+
stack.framework = pyFrameworks[0];
|
|
851
|
+
}
|
|
852
|
+
// === TESTING ===
|
|
853
|
+
const pyTesting = [];
|
|
854
|
+
if (allPyDeps.has('pytest'))
|
|
855
|
+
pyTesting.push('pytest');
|
|
856
|
+
if (allPyDeps.has('pytest-cov'))
|
|
857
|
+
pyTesting.push('pytest-cov');
|
|
858
|
+
if (allPyDeps.has('hypothesis'))
|
|
859
|
+
pyTesting.push('Hypothesis');
|
|
860
|
+
if (allPyDeps.has('tox'))
|
|
861
|
+
pyTesting.push('tox');
|
|
862
|
+
if (allPyDeps.has('nox'))
|
|
863
|
+
pyTesting.push('nox');
|
|
864
|
+
if (allPyDeps.has('coverage'))
|
|
865
|
+
pyTesting.push('coverage');
|
|
866
|
+
if (pyTesting.length > 0) {
|
|
867
|
+
stack.testingFrameworks = pyTesting;
|
|
868
|
+
}
|
|
869
|
+
// === BUILD TOOLS ===
|
|
870
|
+
const pyBuildTools = [];
|
|
871
|
+
if (allPyDeps.has('setuptools'))
|
|
872
|
+
pyBuildTools.push('setuptools');
|
|
873
|
+
if (allPyDeps.has('wheel'))
|
|
874
|
+
pyBuildTools.push('wheel');
|
|
875
|
+
if (allPyDeps.has('cython'))
|
|
876
|
+
pyBuildTools.push('Cython');
|
|
877
|
+
if (allPyDeps.has('maturin'))
|
|
878
|
+
pyBuildTools.push('Maturin');
|
|
879
|
+
if (pyBuildTools.length > 0) {
|
|
880
|
+
stack.buildTools = pyBuildTools;
|
|
881
|
+
}
|
|
882
|
+
// === ORM/DATABASE ===
|
|
883
|
+
if (allPyDeps.has('sqlalchemy'))
|
|
884
|
+
stack.orm = 'SQLAlchemy';
|
|
885
|
+
else if (allPyDeps.has('tortoise-orm'))
|
|
886
|
+
stack.orm = 'Tortoise ORM';
|
|
887
|
+
else if (allPyDeps.has('peewee'))
|
|
888
|
+
stack.orm = 'Peewee';
|
|
889
|
+
else if (allPyDeps.has('mongoengine'))
|
|
890
|
+
stack.orm = 'MongoEngine';
|
|
891
|
+
else if (allPyDeps.has('django'))
|
|
892
|
+
stack.orm = 'Django ORM';
|
|
893
|
+
const pyDatabases = [];
|
|
894
|
+
if (allPyDeps.has('psycopg2') || allPyDeps.has('psycopg2-binary') || allPyDeps.has('psycopg') || allPyDeps.has('asyncpg'))
|
|
895
|
+
pyDatabases.push('PostgreSQL');
|
|
896
|
+
if (allPyDeps.has('pymongo') || allPyDeps.has('motor'))
|
|
897
|
+
pyDatabases.push('MongoDB');
|
|
898
|
+
if (allPyDeps.has('redis') || allPyDeps.has('aioredis'))
|
|
899
|
+
pyDatabases.push('Redis');
|
|
900
|
+
if (allPyDeps.has('pymysql') || allPyDeps.has('mysqlclient'))
|
|
901
|
+
pyDatabases.push('MySQL');
|
|
902
|
+
if (allPyDeps.has('aiosqlite'))
|
|
903
|
+
pyDatabases.push('SQLite');
|
|
904
|
+
if (allPyDeps.has('supabase'))
|
|
905
|
+
pyDatabases.push('Supabase');
|
|
906
|
+
if (pyDatabases.length > 0) {
|
|
907
|
+
stack.database = pyDatabases.join(', ');
|
|
908
|
+
}
|
|
909
|
+
// === DEPLOYMENT ===
|
|
910
|
+
if (!stack.deployment) {
|
|
911
|
+
if (await fileExists(join(rootDir, 'Dockerfile')))
|
|
912
|
+
stack.deployment = 'Docker';
|
|
913
|
+
else if (await fileExists(join(rootDir, 'Procfile')))
|
|
914
|
+
stack.deployment = 'Heroku';
|
|
915
|
+
else if (await fileExists(join(rootDir, 'fly.toml')))
|
|
916
|
+
stack.deployment = 'Fly.io';
|
|
917
|
+
else if (await fileExists(join(rootDir, 'render.yaml')))
|
|
918
|
+
stack.deployment = 'Render';
|
|
919
|
+
}
|
|
920
|
+
// === CI/CD ===
|
|
921
|
+
if (!stack.cicd || stack.cicd.length === 0) {
|
|
922
|
+
const cicd = [];
|
|
923
|
+
if (await fileExists(join(rootDir, '.github/workflows')))
|
|
924
|
+
cicd.push('GitHub Actions');
|
|
925
|
+
if (await fileExists(join(rootDir, '.gitlab-ci.yml')))
|
|
926
|
+
cicd.push('GitLab CI');
|
|
927
|
+
if (await fileExists(join(rootDir, '.circleci')))
|
|
928
|
+
cicd.push('CircleCI');
|
|
929
|
+
if (await fileExists(join(rootDir, 'tox.ini')))
|
|
930
|
+
cicd.push('tox');
|
|
931
|
+
stack.cicd = cicd;
|
|
524
932
|
}
|
|
525
933
|
}
|
|
526
934
|
// Check for Rust project
|
|
@@ -528,12 +936,255 @@ export async function inferStack(rootDir) {
|
|
|
528
936
|
if (await fileExists(cargoPath)) {
|
|
529
937
|
stack.language = 'Rust';
|
|
530
938
|
stack.packageManager = 'cargo';
|
|
939
|
+
try {
|
|
940
|
+
const cargoContent = await readFile(cargoPath, 'utf-8');
|
|
941
|
+
const cargo = parsePyprojectToml(cargoContent); // TOML parser works for Cargo.toml too
|
|
942
|
+
// Rust edition as runtime
|
|
943
|
+
const edition = cargo?.package?.edition;
|
|
944
|
+
if (edition) {
|
|
945
|
+
stack.runtime = `Rust Edition ${edition}`;
|
|
946
|
+
}
|
|
947
|
+
// Collect all deps
|
|
948
|
+
const cargoDeps = cargo?.dependencies || {};
|
|
949
|
+
const cargoDevDeps = cargo?.['dev-dependencies'] || {};
|
|
950
|
+
const cargoBuildDeps = cargo?.['build-dependencies'] || {};
|
|
951
|
+
const allCrateDeps = new Set([
|
|
952
|
+
...Object.keys(cargoDeps).map((s) => s.toLowerCase()),
|
|
953
|
+
...Object.keys(cargoDevDeps).map((s) => s.toLowerCase()),
|
|
954
|
+
...Object.keys(cargoBuildDeps).map((s) => s.toLowerCase()),
|
|
955
|
+
]);
|
|
956
|
+
// Store deps
|
|
957
|
+
const rustDepsRecord = {};
|
|
958
|
+
for (const [name, spec] of Object.entries(cargoDeps)) {
|
|
959
|
+
rustDepsRecord[name] = typeof spec === 'string' ? spec : '*';
|
|
960
|
+
}
|
|
961
|
+
if (Object.keys(rustDepsRecord).length > 0)
|
|
962
|
+
stack.dependencies = rustDepsRecord;
|
|
963
|
+
const rustDevDepsRecord = {};
|
|
964
|
+
for (const [name, spec] of Object.entries(cargoDevDeps)) {
|
|
965
|
+
rustDevDepsRecord[name] = typeof spec === 'string' ? spec : '*';
|
|
966
|
+
}
|
|
967
|
+
if (Object.keys(rustDevDepsRecord).length > 0)
|
|
968
|
+
stack.devDependencies = rustDevDepsRecord;
|
|
969
|
+
// === FRAMEWORKS ===
|
|
970
|
+
const rustFrameworks = [];
|
|
971
|
+
// Web frameworks
|
|
972
|
+
if (allCrateDeps.has('actix-web'))
|
|
973
|
+
rustFrameworks.push('Actix Web');
|
|
974
|
+
if (allCrateDeps.has('axum'))
|
|
975
|
+
rustFrameworks.push('Axum');
|
|
976
|
+
if (allCrateDeps.has('rocket'))
|
|
977
|
+
rustFrameworks.push('Rocket');
|
|
978
|
+
if (allCrateDeps.has('warp'))
|
|
979
|
+
rustFrameworks.push('Warp');
|
|
980
|
+
if (allCrateDeps.has('tide'))
|
|
981
|
+
rustFrameworks.push('Tide');
|
|
982
|
+
// Async runtime
|
|
983
|
+
if (allCrateDeps.has('tokio'))
|
|
984
|
+
rustFrameworks.push('Tokio');
|
|
985
|
+
if (allCrateDeps.has('async-std'))
|
|
986
|
+
rustFrameworks.push('async-std');
|
|
987
|
+
// CLI
|
|
988
|
+
if (allCrateDeps.has('clap'))
|
|
989
|
+
rustFrameworks.push('Clap');
|
|
990
|
+
if (allCrateDeps.has('structopt'))
|
|
991
|
+
rustFrameworks.push('StructOpt');
|
|
992
|
+
// Serialization
|
|
993
|
+
if (allCrateDeps.has('serde'))
|
|
994
|
+
rustFrameworks.push('Serde');
|
|
995
|
+
// GUI
|
|
996
|
+
if (allCrateDeps.has('tauri'))
|
|
997
|
+
rustFrameworks.push('Tauri');
|
|
998
|
+
if (allCrateDeps.has('egui'))
|
|
999
|
+
rustFrameworks.push('egui');
|
|
1000
|
+
if (allCrateDeps.has('iced'))
|
|
1001
|
+
rustFrameworks.push('Iced');
|
|
1002
|
+
if (rustFrameworks.length > 0) {
|
|
1003
|
+
stack.frameworks = rustFrameworks;
|
|
1004
|
+
stack.framework = rustFrameworks[0];
|
|
1005
|
+
}
|
|
1006
|
+
// === TESTING ===
|
|
1007
|
+
const rustTesting = [];
|
|
1008
|
+
if (allCrateDeps.has('criterion'))
|
|
1009
|
+
rustTesting.push('Criterion (benchmarks)');
|
|
1010
|
+
if (allCrateDeps.has('proptest'))
|
|
1011
|
+
rustTesting.push('proptest');
|
|
1012
|
+
if (allCrateDeps.has('quickcheck'))
|
|
1013
|
+
rustTesting.push('quickcheck');
|
|
1014
|
+
if (allCrateDeps.has('mockall'))
|
|
1015
|
+
rustTesting.push('mockall');
|
|
1016
|
+
if (allCrateDeps.has('rstest'))
|
|
1017
|
+
rustTesting.push('rstest');
|
|
1018
|
+
// Rust has built-in testing
|
|
1019
|
+
rustTesting.unshift('cargo test (built-in)');
|
|
1020
|
+
stack.testingFrameworks = rustTesting;
|
|
1021
|
+
// === ORM/DATABASE ===
|
|
1022
|
+
if (allCrateDeps.has('diesel'))
|
|
1023
|
+
stack.orm = 'Diesel';
|
|
1024
|
+
else if (allCrateDeps.has('sea-orm') || allCrateDeps.has('sea_orm'))
|
|
1025
|
+
stack.orm = 'SeaORM';
|
|
1026
|
+
else if (allCrateDeps.has('sqlx'))
|
|
1027
|
+
stack.orm = 'SQLx';
|
|
1028
|
+
const rustDatabases = [];
|
|
1029
|
+
if (allCrateDeps.has('tokio-postgres') || allCrateDeps.has('sqlx') || allCrateDeps.has('diesel'))
|
|
1030
|
+
rustDatabases.push('PostgreSQL');
|
|
1031
|
+
if (allCrateDeps.has('mongodb'))
|
|
1032
|
+
rustDatabases.push('MongoDB');
|
|
1033
|
+
if (allCrateDeps.has('redis'))
|
|
1034
|
+
rustDatabases.push('Redis');
|
|
1035
|
+
if (allCrateDeps.has('rusqlite'))
|
|
1036
|
+
rustDatabases.push('SQLite');
|
|
1037
|
+
if (rustDatabases.length > 0)
|
|
1038
|
+
stack.database = rustDatabases.join(', ');
|
|
1039
|
+
// === BUILD TOOLS ===
|
|
1040
|
+
const rustBuildTools = [];
|
|
1041
|
+
if (allCrateDeps.has('maturin'))
|
|
1042
|
+
rustBuildTools.push('Maturin');
|
|
1043
|
+
if (allCrateDeps.has('wasm-bindgen'))
|
|
1044
|
+
rustBuildTools.push('wasm-bindgen');
|
|
1045
|
+
if (allCrateDeps.has('napi') || allCrateDeps.has('napi-derive'))
|
|
1046
|
+
rustBuildTools.push('napi-rs');
|
|
1047
|
+
if (rustBuildTools.length > 0)
|
|
1048
|
+
stack.buildTools = rustBuildTools;
|
|
1049
|
+
// === DEPLOYMENT ===
|
|
1050
|
+
if (!stack.deployment) {
|
|
1051
|
+
if (await fileExists(join(rootDir, 'Dockerfile')))
|
|
1052
|
+
stack.deployment = 'Docker';
|
|
1053
|
+
else if (await fileExists(join(rootDir, 'fly.toml')))
|
|
1054
|
+
stack.deployment = 'Fly.io';
|
|
1055
|
+
else if (await fileExists(join(rootDir, 'shuttle.toml')) || allCrateDeps.has('shuttle-runtime'))
|
|
1056
|
+
stack.deployment = 'Shuttle';
|
|
1057
|
+
}
|
|
1058
|
+
// === CI/CD ===
|
|
1059
|
+
if (!stack.cicd || stack.cicd.length === 0) {
|
|
1060
|
+
const cicd = [];
|
|
1061
|
+
if (await fileExists(join(rootDir, '.github/workflows')))
|
|
1062
|
+
cicd.push('GitHub Actions');
|
|
1063
|
+
if (await fileExists(join(rootDir, '.gitlab-ci.yml')))
|
|
1064
|
+
cicd.push('GitLab CI');
|
|
1065
|
+
stack.cicd = cicd;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
catch { }
|
|
531
1069
|
}
|
|
532
1070
|
// Check for Go project
|
|
533
1071
|
const goModPath = join(rootDir, 'go.mod');
|
|
534
1072
|
if (await fileExists(goModPath)) {
|
|
535
1073
|
stack.language = 'Go';
|
|
536
1074
|
stack.packageManager = 'go';
|
|
1075
|
+
try {
|
|
1076
|
+
const goModContent = await readFile(goModPath, 'utf-8');
|
|
1077
|
+
// Parse Go version
|
|
1078
|
+
const goVersionMatch = goModContent.match(/^go\s+(\S+)/m);
|
|
1079
|
+
if (goVersionMatch) {
|
|
1080
|
+
stack.runtime = `Go ${goVersionMatch[1]}`;
|
|
1081
|
+
}
|
|
1082
|
+
// Parse module path
|
|
1083
|
+
const moduleMatch = goModContent.match(/^module\s+(\S+)/m);
|
|
1084
|
+
const modulePath = moduleMatch?.[1] || '';
|
|
1085
|
+
// Parse require block
|
|
1086
|
+
const allGoMods = new Set();
|
|
1087
|
+
const depsRecord = {};
|
|
1088
|
+
// Single-line requires: require github.com/foo/bar v1.2.3
|
|
1089
|
+
const singleReqs = goModContent.matchAll(/^require\s+(\S+)\s+(\S+)/gm);
|
|
1090
|
+
for (const m of singleReqs) {
|
|
1091
|
+
const modName = m[1].toLowerCase();
|
|
1092
|
+
allGoMods.add(modName);
|
|
1093
|
+
depsRecord[m[1]] = m[2];
|
|
1094
|
+
}
|
|
1095
|
+
// Block requires
|
|
1096
|
+
const requireBlocks = goModContent.matchAll(/require\s*\(([\s\S]*?)\)/g);
|
|
1097
|
+
for (const block of requireBlocks) {
|
|
1098
|
+
const lines = block[1].split('\n');
|
|
1099
|
+
for (const line of lines) {
|
|
1100
|
+
const depMatch = line.trim().match(/^(\S+)\s+(\S+)/);
|
|
1101
|
+
if (depMatch && !depMatch[1].startsWith('//')) {
|
|
1102
|
+
allGoMods.add(depMatch[1].toLowerCase());
|
|
1103
|
+
depsRecord[depMatch[1]] = depMatch[2];
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
if (Object.keys(depsRecord).length > 0)
|
|
1108
|
+
stack.dependencies = depsRecord;
|
|
1109
|
+
// Helper: check if any go module path contains a substring
|
|
1110
|
+
const hasGoMod = (name) => [...allGoMods].some(m => m.includes(name));
|
|
1111
|
+
// === FRAMEWORKS ===
|
|
1112
|
+
const goFrameworks = [];
|
|
1113
|
+
if (hasGoMod('gin-gonic/gin'))
|
|
1114
|
+
goFrameworks.push('Gin');
|
|
1115
|
+
if (hasGoMod('labstack/echo'))
|
|
1116
|
+
goFrameworks.push('Echo');
|
|
1117
|
+
if (hasGoMod('gofiber/fiber'))
|
|
1118
|
+
goFrameworks.push('Fiber');
|
|
1119
|
+
if (hasGoMod('go-chi/chi'))
|
|
1120
|
+
goFrameworks.push('Chi');
|
|
1121
|
+
if (hasGoMod('gorilla/mux'))
|
|
1122
|
+
goFrameworks.push('Gorilla Mux');
|
|
1123
|
+
if (hasGoMod('beego'))
|
|
1124
|
+
goFrameworks.push('Beego');
|
|
1125
|
+
if (hasGoMod('grpc'))
|
|
1126
|
+
goFrameworks.push('gRPC');
|
|
1127
|
+
// CLI
|
|
1128
|
+
if (hasGoMod('spf13/cobra'))
|
|
1129
|
+
goFrameworks.push('Cobra');
|
|
1130
|
+
if (hasGoMod('urfave/cli'))
|
|
1131
|
+
goFrameworks.push('urfave/cli');
|
|
1132
|
+
// Config
|
|
1133
|
+
if (hasGoMod('spf13/viper'))
|
|
1134
|
+
goFrameworks.push('Viper');
|
|
1135
|
+
if (goFrameworks.length > 0) {
|
|
1136
|
+
stack.frameworks = goFrameworks;
|
|
1137
|
+
stack.framework = goFrameworks[0];
|
|
1138
|
+
}
|
|
1139
|
+
// === TESTING ===
|
|
1140
|
+
const goTesting = ['go test (built-in)'];
|
|
1141
|
+
if (hasGoMod('stretchr/testify'))
|
|
1142
|
+
goTesting.push('Testify');
|
|
1143
|
+
if (hasGoMod('onsi/ginkgo'))
|
|
1144
|
+
goTesting.push('Ginkgo');
|
|
1145
|
+
if (hasGoMod('onsi/gomega'))
|
|
1146
|
+
goTesting.push('Gomega');
|
|
1147
|
+
stack.testingFrameworks = goTesting;
|
|
1148
|
+
// === ORM/DATABASE ===
|
|
1149
|
+
if (hasGoMod('gorm.io'))
|
|
1150
|
+
stack.orm = 'GORM';
|
|
1151
|
+
else if (hasGoMod('ent/ent'))
|
|
1152
|
+
stack.orm = 'Ent';
|
|
1153
|
+
else if (hasGoMod('sqlc'))
|
|
1154
|
+
stack.orm = 'sqlc';
|
|
1155
|
+
else if (hasGoMod('jmoiron/sqlx'))
|
|
1156
|
+
stack.orm = 'sqlx';
|
|
1157
|
+
const goDatabases = [];
|
|
1158
|
+
if (hasGoMod('lib/pq') || hasGoMod('jackc/pgx') || hasGoMod('pgx'))
|
|
1159
|
+
goDatabases.push('PostgreSQL');
|
|
1160
|
+
if (hasGoMod('go.mongodb.org') || hasGoMod('mongo-driver'))
|
|
1161
|
+
goDatabases.push('MongoDB');
|
|
1162
|
+
if (hasGoMod('go-redis/redis') || hasGoMod('redis/go-redis'))
|
|
1163
|
+
goDatabases.push('Redis');
|
|
1164
|
+
if (hasGoMod('mattn/go-sqlite3'))
|
|
1165
|
+
goDatabases.push('SQLite');
|
|
1166
|
+
if (hasGoMod('go-sql-driver/mysql'))
|
|
1167
|
+
goDatabases.push('MySQL');
|
|
1168
|
+
if (goDatabases.length > 0)
|
|
1169
|
+
stack.database = goDatabases.join(', ');
|
|
1170
|
+
// === DEPLOYMENT ===
|
|
1171
|
+
if (!stack.deployment) {
|
|
1172
|
+
if (await fileExists(join(rootDir, 'Dockerfile')))
|
|
1173
|
+
stack.deployment = 'Docker';
|
|
1174
|
+
else if (await fileExists(join(rootDir, 'fly.toml')))
|
|
1175
|
+
stack.deployment = 'Fly.io';
|
|
1176
|
+
}
|
|
1177
|
+
// === CI/CD ===
|
|
1178
|
+
if (!stack.cicd || stack.cicd.length === 0) {
|
|
1179
|
+
const cicd = [];
|
|
1180
|
+
if (await fileExists(join(rootDir, '.github/workflows')))
|
|
1181
|
+
cicd.push('GitHub Actions');
|
|
1182
|
+
if (await fileExists(join(rootDir, '.gitlab-ci.yml')))
|
|
1183
|
+
cicd.push('GitLab CI');
|
|
1184
|
+
stack.cicd = cicd;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
catch { }
|
|
537
1188
|
}
|
|
538
1189
|
return stack;
|
|
539
1190
|
}
|
|
@@ -608,23 +1259,141 @@ export async function inferArchitecture(rootDir) {
|
|
|
608
1259
|
const hasPackages = relativeDirs.some(d => d === 'packages');
|
|
609
1260
|
const hasApps = relativeDirs.some(d => d === 'apps');
|
|
610
1261
|
const hasServices = relativeDirs.some(d => d === 'services');
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
1262
|
+
// Check for Python CLI entry points from pyproject.toml
|
|
1263
|
+
let hasPythonScripts = false;
|
|
1264
|
+
let hasPythonWebFramework = false;
|
|
1265
|
+
const pyprojectArchPath = join(rootDir, 'pyproject.toml');
|
|
1266
|
+
if (await fileExists(pyprojectArchPath)) {
|
|
1267
|
+
try {
|
|
1268
|
+
const pyContent = await readFile(pyprojectArchPath, 'utf-8');
|
|
1269
|
+
const pyproject = parsePyprojectToml(pyContent);
|
|
1270
|
+
// CLI entry points from [project.scripts]
|
|
1271
|
+
const scripts = pyproject?.project?.scripts;
|
|
1272
|
+
if (scripts && typeof scripts === 'object' && Object.keys(scripts).length > 0) {
|
|
1273
|
+
hasPythonScripts = true;
|
|
1274
|
+
for (const [cmdName, entryPoint] of Object.entries(scripts)) {
|
|
1275
|
+
if (typeof entryPoint === 'string') {
|
|
1276
|
+
architecture.entryPoints = architecture.entryPoints || [];
|
|
1277
|
+
architecture.entryPoints.push({
|
|
1278
|
+
file: entryPoint,
|
|
1279
|
+
purpose: `CLI command: ${cmdName}`
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
// Check for web framework deps
|
|
1285
|
+
const deps = pyproject?.project?.dependencies || [];
|
|
1286
|
+
const depNames = deps.map(extractPyDepName);
|
|
1287
|
+
hasPythonWebFramework = depNames.some(d => ['django', 'flask', 'fastapi', 'starlette', 'sanic', 'tornado', 'falcon', 'litestar'].includes(d));
|
|
1288
|
+
}
|
|
1289
|
+
catch { }
|
|
616
1290
|
}
|
|
617
|
-
|
|
618
|
-
|
|
1291
|
+
// Python-specific entry points
|
|
1292
|
+
if (await fileExists(join(rootDir, 'manage.py'))) {
|
|
1293
|
+
architecture.entryPoints = architecture.entryPoints || [];
|
|
1294
|
+
architecture.entryPoints.push({ file: 'manage.py', purpose: 'Django management' });
|
|
619
1295
|
}
|
|
620
|
-
|
|
621
|
-
|
|
1296
|
+
// Rust architecture detection
|
|
1297
|
+
let hasRustBin = false;
|
|
1298
|
+
let hasRustLib = false;
|
|
1299
|
+
let hasRustWebFramework = false;
|
|
1300
|
+
let hasRustCliFramework = false;
|
|
1301
|
+
const cargoArchPath = join(rootDir, 'Cargo.toml');
|
|
1302
|
+
if (await fileExists(cargoArchPath)) {
|
|
1303
|
+
try {
|
|
1304
|
+
const cargoContent = await readFile(cargoArchPath, 'utf-8');
|
|
1305
|
+
const cargo = parsePyprojectToml(cargoContent);
|
|
1306
|
+
hasRustBin = await fileExists(join(rootDir, 'src/main.rs'));
|
|
1307
|
+
hasRustLib = await fileExists(join(rootDir, 'src/lib.rs'));
|
|
1308
|
+
const cargoDeps = Object.keys(cargo?.dependencies || {}).map(s => s.toLowerCase());
|
|
1309
|
+
hasRustWebFramework = cargoDeps.some(d => ['actix-web', 'axum', 'rocket', 'warp', 'tide'].includes(d));
|
|
1310
|
+
hasRustCliFramework = cargoDeps.some(d => ['clap', 'structopt'].includes(d));
|
|
1311
|
+
// Rust entry points
|
|
1312
|
+
if (hasRustBin) {
|
|
1313
|
+
architecture.entryPoints = architecture.entryPoints || [];
|
|
1314
|
+
architecture.entryPoints.push({ file: 'src/main.rs', purpose: 'Binary entry point' });
|
|
1315
|
+
}
|
|
1316
|
+
if (hasRustLib) {
|
|
1317
|
+
architecture.entryPoints = architecture.entryPoints || [];
|
|
1318
|
+
architecture.entryPoints.push({ file: 'src/lib.rs', purpose: 'Library entry point' });
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
catch { }
|
|
622
1322
|
}
|
|
623
|
-
|
|
624
|
-
|
|
1323
|
+
// Go architecture detection
|
|
1324
|
+
let hasGoCmd = false;
|
|
1325
|
+
let hasGoWebFramework = false;
|
|
1326
|
+
let hasGoCliFramework = false;
|
|
1327
|
+
const goModArchPath = join(rootDir, 'go.mod');
|
|
1328
|
+
if (await fileExists(goModArchPath)) {
|
|
1329
|
+
try {
|
|
1330
|
+
const goModContent = await readFile(goModArchPath, 'utf-8');
|
|
1331
|
+
hasGoCmd = relativeDirs.some(d => d === 'cmd' || d.startsWith('cmd/'));
|
|
1332
|
+
const goMainExists = await fileExists(join(rootDir, 'main.go'));
|
|
1333
|
+
const goModLower = goModContent.toLowerCase();
|
|
1334
|
+
hasGoWebFramework = ['gin-gonic', 'labstack/echo', 'gofiber/fiber', 'go-chi/chi', 'gorilla/mux'].some(f => goModLower.includes(f));
|
|
1335
|
+
hasGoCliFramework = ['spf13/cobra', 'urfave/cli'].some(f => goModLower.includes(f));
|
|
1336
|
+
// Go entry points
|
|
1337
|
+
if (hasGoCmd) {
|
|
1338
|
+
architecture.entryPoints = architecture.entryPoints || [];
|
|
1339
|
+
architecture.entryPoints.push({ file: 'cmd/', purpose: 'CLI entry points' });
|
|
1340
|
+
}
|
|
1341
|
+
else if (goMainExists) {
|
|
1342
|
+
architecture.entryPoints = architecture.entryPoints || [];
|
|
1343
|
+
architecture.entryPoints.push({ file: 'main.go', purpose: 'Application entry' });
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
catch { }
|
|
625
1347
|
}
|
|
626
|
-
|
|
627
|
-
|
|
1348
|
+
// === Score-based architecture type detection ===
|
|
1349
|
+
// Each signal contributes to a score for each type
|
|
1350
|
+
const typeScores = {
|
|
1351
|
+
monorepo: 0, microservices: 0, cli: 0, backend: 0,
|
|
1352
|
+
fullstack: 0, library: 0, frontend: 0
|
|
1353
|
+
};
|
|
1354
|
+
// Monorepo signals
|
|
1355
|
+
if (hasPackages || hasApps)
|
|
1356
|
+
typeScores.monorepo += 10;
|
|
1357
|
+
// Microservices signals
|
|
1358
|
+
if (hasServices)
|
|
1359
|
+
typeScores.microservices += 8;
|
|
1360
|
+
// CLI signals
|
|
1361
|
+
if (hasPythonScripts && !hasPythonWebFramework)
|
|
1362
|
+
typeScores.cli += 8;
|
|
1363
|
+
if (hasRustCliFramework && hasRustBin)
|
|
1364
|
+
typeScores.cli += 8;
|
|
1365
|
+
if (hasGoCliFramework || hasGoCmd)
|
|
1366
|
+
typeScores.cli += 8;
|
|
1367
|
+
if (await fileExists(join(rootDir, 'bin')))
|
|
1368
|
+
typeScores.cli += 5;
|
|
1369
|
+
// Backend signals
|
|
1370
|
+
if (hasPythonWebFramework)
|
|
1371
|
+
typeScores.backend += 8;
|
|
1372
|
+
if (hasRustWebFramework)
|
|
1373
|
+
typeScores.backend += 8;
|
|
1374
|
+
if (hasGoWebFramework)
|
|
1375
|
+
typeScores.backend += 8;
|
|
1376
|
+
if (relativeDirs.some(d => d.includes('api')))
|
|
1377
|
+
typeScores.backend += 2;
|
|
1378
|
+
// Fullstack signals
|
|
1379
|
+
if (hasApp && hasSrc)
|
|
1380
|
+
typeScores.fullstack += 5;
|
|
1381
|
+
if (hasPythonWebFramework && relativeDirs.some(d => d.includes('dashboard') || d.includes('frontend')))
|
|
1382
|
+
typeScores.fullstack += 7;
|
|
1383
|
+
// Library signals
|
|
1384
|
+
if (hasLib && !hasApp && !hasPages)
|
|
1385
|
+
typeScores.library += 5;
|
|
1386
|
+
if (hasRustLib && !hasRustBin)
|
|
1387
|
+
typeScores.library += 8;
|
|
1388
|
+
// Frontend signals
|
|
1389
|
+
if (hasPages || (hasApp && !hasSrc && !hasServices))
|
|
1390
|
+
typeScores.frontend += 5;
|
|
1391
|
+
// Pick highest score, with fallback
|
|
1392
|
+
const sortedTypes = Object.entries(typeScores)
|
|
1393
|
+
.filter(([, score]) => score > 0)
|
|
1394
|
+
.sort((a, b) => b[1] - a[1]);
|
|
1395
|
+
if (sortedTypes.length > 0) {
|
|
1396
|
+
architecture.type = sortedTypes[0][0];
|
|
628
1397
|
}
|
|
629
1398
|
else {
|
|
630
1399
|
architecture.type = 'backend';
|
|
@@ -700,6 +1469,12 @@ export async function inferArchitecture(rootDir) {
|
|
|
700
1469
|
patterns.push('Feature-based organization');
|
|
701
1470
|
if (relativeDirs.some(d => d.includes('modules')))
|
|
702
1471
|
patterns.push('Module pattern');
|
|
1472
|
+
if (relativeDirs.some(d => d.includes('agents')))
|
|
1473
|
+
patterns.push('Agent-based architecture');
|
|
1474
|
+
if (relativeDirs.some(d => d.includes('missions') || d.includes('pipelines')))
|
|
1475
|
+
patterns.push('Pipeline pattern');
|
|
1476
|
+
if (relativeDirs.some(d => d.includes('models') && !d.includes('node_modules')))
|
|
1477
|
+
patterns.push('Data models');
|
|
703
1478
|
architecture.patterns = patterns;
|
|
704
1479
|
// Detect conventions
|
|
705
1480
|
const conventions = [];
|
|
@@ -718,9 +1493,51 @@ export async function inferArchitecture(rootDir) {
|
|
|
718
1493
|
if (await fileExists(join(rootDir, '.husky'))) {
|
|
719
1494
|
conventions.push('Git hooks (Husky)');
|
|
720
1495
|
}
|
|
1496
|
+
// Python conventions
|
|
1497
|
+
if (await fileExists(pyprojectArchPath)) {
|
|
1498
|
+
try {
|
|
1499
|
+
const pyContent = await readFile(pyprojectArchPath, 'utf-8');
|
|
1500
|
+
const pyproject = parsePyprojectToml(pyContent);
|
|
1501
|
+
if (pyproject?.tool?.ruff)
|
|
1502
|
+
conventions.push('Ruff code linting');
|
|
1503
|
+
if (pyproject?.tool?.black)
|
|
1504
|
+
conventions.push('Black code formatting');
|
|
1505
|
+
if (pyproject?.tool?.mypy)
|
|
1506
|
+
conventions.push('mypy type checking');
|
|
1507
|
+
if (pyproject?.tool?.isort)
|
|
1508
|
+
conventions.push('isort import sorting');
|
|
1509
|
+
if (pyproject?.tool?.pytest)
|
|
1510
|
+
conventions.push('pytest configuration');
|
|
1511
|
+
}
|
|
1512
|
+
catch { }
|
|
1513
|
+
}
|
|
1514
|
+
if (await fileExists(join(rootDir, '.flake8'))) {
|
|
1515
|
+
conventions.push('flake8 code linting');
|
|
1516
|
+
}
|
|
1517
|
+
if (await fileExists(join(rootDir, 'mypy.ini')) || await fileExists(join(rootDir, '.mypy.ini'))) {
|
|
1518
|
+
conventions.push('mypy type checking');
|
|
1519
|
+
}
|
|
1520
|
+
// Rust conventions
|
|
1521
|
+
if (await fileExists(cargoArchPath)) {
|
|
1522
|
+
if (await fileExists(join(rootDir, 'rustfmt.toml')) || await fileExists(join(rootDir, '.rustfmt.toml'))) {
|
|
1523
|
+
conventions.push('rustfmt code formatting');
|
|
1524
|
+
}
|
|
1525
|
+
if (await fileExists(join(rootDir, 'clippy.toml')) || await fileExists(join(rootDir, '.clippy.toml'))) {
|
|
1526
|
+
conventions.push('Clippy linting');
|
|
1527
|
+
}
|
|
1528
|
+
// Rust always has these by convention
|
|
1529
|
+
conventions.push('cargo fmt / cargo clippy');
|
|
1530
|
+
}
|
|
1531
|
+
// Go conventions
|
|
1532
|
+
if (await fileExists(goModArchPath)) {
|
|
1533
|
+
if (await fileExists(join(rootDir, '.golangci.yml')) || await fileExists(join(rootDir, '.golangci.yaml'))) {
|
|
1534
|
+
conventions.push('golangci-lint');
|
|
1535
|
+
}
|
|
1536
|
+
conventions.push('gofmt / go vet');
|
|
1537
|
+
}
|
|
721
1538
|
architecture.conventions = conventions;
|
|
722
|
-
// Detect entry points
|
|
723
|
-
const entryPoints = [];
|
|
1539
|
+
// Detect entry points (preserve any already detected, e.g. from pyproject.toml)
|
|
1540
|
+
const entryPoints = architecture.entryPoints || [];
|
|
724
1541
|
if (await fileExists(join(rootDir, 'src/index.ts')))
|
|
725
1542
|
entryPoints.push({ file: 'src/index.ts', purpose: 'Main entry point' });
|
|
726
1543
|
else if (await fileExists(join(rootDir, 'src/index.tsx')))
|
|
@@ -847,6 +1664,73 @@ export async function inferConstraints(rootDir) {
|
|
|
847
1664
|
formatter: 'Prettier'
|
|
848
1665
|
};
|
|
849
1666
|
}
|
|
1667
|
+
// Python code style tools
|
|
1668
|
+
const pyprojectConstraintsPath = join(rootDir, 'pyproject.toml');
|
|
1669
|
+
if (await fileExists(pyprojectConstraintsPath)) {
|
|
1670
|
+
try {
|
|
1671
|
+
const pyContent = await readFile(pyprojectConstraintsPath, 'utf-8');
|
|
1672
|
+
const pyproject = parsePyprojectToml(pyContent);
|
|
1673
|
+
// Linter
|
|
1674
|
+
if (pyproject?.tool?.ruff) {
|
|
1675
|
+
constraints.codeStyle = {
|
|
1676
|
+
...constraints.codeStyle,
|
|
1677
|
+
linter: 'Ruff'
|
|
1678
|
+
};
|
|
1679
|
+
const targetVersion = pyproject.tool.ruff['target-version'];
|
|
1680
|
+
if (targetVersion && typeof targetVersion === 'string') {
|
|
1681
|
+
// py311 → 3.11, py39 → 3.9
|
|
1682
|
+
const nums = targetVersion.replace('py', '');
|
|
1683
|
+
const major = nums[0];
|
|
1684
|
+
const minor = nums.slice(1);
|
|
1685
|
+
if (major && minor) {
|
|
1686
|
+
constraints.mustUse?.push(`Python ${major}.${minor}+`);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
const lineLength = pyproject.tool.ruff['line-length'];
|
|
1690
|
+
if (lineLength) {
|
|
1691
|
+
constraints.preferences?.push({
|
|
1692
|
+
category: 'Code Style',
|
|
1693
|
+
preference: `Line length: ${lineLength}`,
|
|
1694
|
+
rationale: 'Ruff configuration'
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
if (pyproject?.tool?.black) {
|
|
1699
|
+
constraints.codeStyle = {
|
|
1700
|
+
...constraints.codeStyle,
|
|
1701
|
+
formatter: 'Black'
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
if (pyproject?.tool?.mypy) {
|
|
1705
|
+
constraints.mustUse?.push('mypy for type checking');
|
|
1706
|
+
}
|
|
1707
|
+
// Python version constraint (only if not already added from Ruff target-version)
|
|
1708
|
+
const requiresPython = pyproject?.project?.['requires-python'];
|
|
1709
|
+
if (requiresPython && !constraints.mustUse?.some(m => m.startsWith('Python '))) {
|
|
1710
|
+
constraints.mustUse?.push(`Python ${requiresPython}`);
|
|
1711
|
+
}
|
|
1712
|
+
// Testing from pyproject
|
|
1713
|
+
const deps = pyproject?.project?.dependencies || [];
|
|
1714
|
+
const optDeps = pyproject?.project?.['optional-dependencies'] || {};
|
|
1715
|
+
const allDepNames = new Set([
|
|
1716
|
+
...deps.map(extractPyDepName),
|
|
1717
|
+
...Object.values(optDeps).flat().filter((d) => typeof d === 'string').map(extractPyDepName)
|
|
1718
|
+
]);
|
|
1719
|
+
if (allDepNames.has('pytest') || pyproject?.tool?.pytest) {
|
|
1720
|
+
constraints.testing = {
|
|
1721
|
+
required: true,
|
|
1722
|
+
strategy: 'pytest'
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
catch { }
|
|
1727
|
+
}
|
|
1728
|
+
if (await fileExists(join(rootDir, '.flake8'))) {
|
|
1729
|
+
constraints.codeStyle = {
|
|
1730
|
+
...constraints.codeStyle,
|
|
1731
|
+
linter: constraints.codeStyle?.linter || 'flake8'
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
850
1734
|
// Check for TypeScript
|
|
851
1735
|
if (await fileExists(join(rootDir, 'tsconfig.json'))) {
|
|
852
1736
|
constraints.mustUse?.push('TypeScript for type safety');
|