agent-workflow-kit-cli 1.0.0-mvp → 1.2.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/dist/cli/commands/add.js +134 -0
- package/dist/cli/commands/doctor.js +21 -0
- package/dist/cli/commands/export.js +3 -2
- package/dist/cli/commands/init.js +32 -5
- package/dist/cli/index.js +17 -1
- package/dist/core/analyzer.js +462 -0
- package/dist/core/detector.js +102 -55
- package/dist/core/renderer.js +13 -3
- package/package.json +1 -1
- package/templates/common/AGENTS.md.hbs +15 -20
- package/templates/common/skills/build-skill/SKILL.md +38 -0
- package/templates/fastapi/rules/python-style.md +12 -17
- package/templates/python-ai/AGENTS.md.hbs +36 -0
- package/templates/python-ai/rules/ai-hardware.md +36 -0
- package/templates/python-ai/rules/ai-style.md +30 -0
- package/templates/python-ai/skills/ai-agent/SKILL.md +24 -0
- package/templates/python-ai/skills/ai-debug/SKILL.md +31 -0
- package/templates/python-ai/skills/ai-model/SKILL.md +25 -0
- package/templates/python-ai/skills/ai-pipeline/SKILL.md +24 -0
- package/templates/react-ts/rules/react-style.md +19 -24
- package/templates/spring-boot/AGENTS.md.hbs +60 -6
- package/templates/spring-boot/rules/code-review.md +22 -0
- package/templates/spring-boot/rules/java-style.md +6 -11
- package/templates/spring-boot/rules/microservice-style.md +22 -0
- package/templates/spring-boot/rules/production-ready.md +20 -0
- package/templates/spring-boot/skills/spring-debug/SKILL.md +35 -0
- package/templates/spring-boot/skills/spring-feature/SKILL.md +24 -12
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
import { promises as fs } from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
/**
|
|
8
|
+
* Helper to recursively find files with a specific extension, avoiding node_modules and build directories.
|
|
9
|
+
*/
|
|
10
|
+
async function findFilesRecursively(dir, extension, limit = 20) {
|
|
11
|
+
const results = [];
|
|
12
|
+
async function traverse(currentDir) {
|
|
13
|
+
if (results.length >= limit)
|
|
14
|
+
return;
|
|
15
|
+
try {
|
|
16
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
if (results.length >= limit)
|
|
19
|
+
return;
|
|
20
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
21
|
+
if (entry.isDirectory()) {
|
|
22
|
+
// Skip build and package manager folders
|
|
23
|
+
if (entry.name.startsWith(".") ||
|
|
24
|
+
entry.name === "node_modules" ||
|
|
25
|
+
entry.name === "target" ||
|
|
26
|
+
entry.name === "build" ||
|
|
27
|
+
entry.name === "dist" ||
|
|
28
|
+
entry.name === "bin" ||
|
|
29
|
+
entry.name === "out" ||
|
|
30
|
+
entry.name === "gradle") {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
await traverse(fullPath);
|
|
34
|
+
}
|
|
35
|
+
else if (entry.isFile() && entry.name.endsWith(extension)) {
|
|
36
|
+
results.push(fullPath);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Ignore read errors
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
await traverse(dir);
|
|
45
|
+
return results;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Simple parser for properties files.
|
|
49
|
+
*/
|
|
50
|
+
export function parsePropertiesSimple(content) {
|
|
51
|
+
const lines = content.split("\n");
|
|
52
|
+
let appName;
|
|
53
|
+
let port;
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
const trimmed = line.trim();
|
|
56
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("!"))
|
|
57
|
+
continue;
|
|
58
|
+
const eqIndex = trimmed.indexOf("=");
|
|
59
|
+
const colonIndex = trimmed.indexOf(":");
|
|
60
|
+
let sepIndex = -1;
|
|
61
|
+
if (eqIndex !== -1 && colonIndex !== -1) {
|
|
62
|
+
sepIndex = Math.min(eqIndex, colonIndex);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
sepIndex = eqIndex !== -1 ? eqIndex : colonIndex;
|
|
66
|
+
}
|
|
67
|
+
if (sepIndex !== -1) {
|
|
68
|
+
const key = trimmed.substring(0, sepIndex).trim();
|
|
69
|
+
const value = trimmed.substring(sepIndex + 1).trim().replace(/^['"]|['"]$/g, "");
|
|
70
|
+
if (key === "spring.application.name") {
|
|
71
|
+
appName = value;
|
|
72
|
+
}
|
|
73
|
+
else if (key === "server.port") {
|
|
74
|
+
port = value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { appName, port };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Simple parser for YAML files.
|
|
82
|
+
*/
|
|
83
|
+
export function parseYamlSimple(content) {
|
|
84
|
+
const lines = content.split("\n");
|
|
85
|
+
const contextPath = [];
|
|
86
|
+
let appName;
|
|
87
|
+
let port;
|
|
88
|
+
for (const line of lines) {
|
|
89
|
+
const trimmed = line.trim();
|
|
90
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
91
|
+
continue;
|
|
92
|
+
// Determine indentation
|
|
93
|
+
const indent = line.length - line.trimStart().length;
|
|
94
|
+
// Pop context paths that are deeper or equal to current indent level
|
|
95
|
+
while (contextPath.length > 0 && indent <= contextPath[contextPath.length - 1].indent) {
|
|
96
|
+
contextPath.pop();
|
|
97
|
+
}
|
|
98
|
+
const colonIndex = trimmed.indexOf(":");
|
|
99
|
+
if (colonIndex !== -1) {
|
|
100
|
+
const key = trimmed.substring(0, colonIndex).trim();
|
|
101
|
+
const value = trimmed.substring(colonIndex + 1).trim();
|
|
102
|
+
contextPath.push({ key, indent });
|
|
103
|
+
const fullKeyPath = contextPath.map((c) => c.key).join(".");
|
|
104
|
+
if (value) {
|
|
105
|
+
const cleanVal = value.replace(/^['"]|['"]$/g, "");
|
|
106
|
+
if (fullKeyPath === "spring.application.name") {
|
|
107
|
+
appName = cleanVal;
|
|
108
|
+
}
|
|
109
|
+
else if (fullKeyPath === "server.port") {
|
|
110
|
+
port = cleanVal;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { appName, port };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Analyzes Java Spring Boot project details.
|
|
119
|
+
*/
|
|
120
|
+
async function analyzeSpringBoot(dir) {
|
|
121
|
+
// 1. Detect Build Tool & Commands
|
|
122
|
+
let buildTool = "maven";
|
|
123
|
+
let buildCommand = "./mvnw";
|
|
124
|
+
let buildVerifyArgs = "verify";
|
|
125
|
+
let isMicroservice = false;
|
|
126
|
+
try {
|
|
127
|
+
const isGradle = (await fs.stat(path.join(dir, "build.gradle")).then((s) => s.isFile()).catch(() => false)) ||
|
|
128
|
+
(await fs.stat(path.join(dir, "build.gradle.kts")).then((s) => s.isFile()).catch(() => false));
|
|
129
|
+
if (isGradle) {
|
|
130
|
+
buildTool = "gradle";
|
|
131
|
+
const hasWrapper = await fs.stat(path.join(dir, "gradlew")).then((s) => s.isFile()).catch(() => false);
|
|
132
|
+
buildCommand = hasWrapper ? "./gradlew" : "gradle";
|
|
133
|
+
buildVerifyArgs = "check";
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
const hasWrapper = await fs.stat(path.join(dir, "mvnw")).then((s) => s.isFile()).catch(() => false);
|
|
137
|
+
buildCommand = hasWrapper ? "./mvnw" : "mvn";
|
|
138
|
+
buildVerifyArgs = "verify";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Use default maven settings
|
|
143
|
+
}
|
|
144
|
+
// Check build files for microservice dependencies
|
|
145
|
+
const buildFilesToCheck = ["pom.xml", "build.gradle", "build.gradle.kts"];
|
|
146
|
+
for (const file of buildFilesToCheck) {
|
|
147
|
+
try {
|
|
148
|
+
const content = await fs.readFile(path.join(dir, file), "utf8");
|
|
149
|
+
if (content.includes("spring-cloud") ||
|
|
150
|
+
content.includes("eureka") ||
|
|
151
|
+
content.includes("openfeign") ||
|
|
152
|
+
content.includes("consul") ||
|
|
153
|
+
content.includes("nacos")) {
|
|
154
|
+
isMicroservice = true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Ignore if file doesn't exist
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// 2. Base Package Path and package-to-dot-notation detection
|
|
162
|
+
let basePackage = "com.acme.app";
|
|
163
|
+
let packagePath = "com/acme/app";
|
|
164
|
+
const javaSrcRoot = path.join(dir, "src/main/java");
|
|
165
|
+
let javaSrcRootExists = false;
|
|
166
|
+
try {
|
|
167
|
+
const stat = await fs.stat(javaSrcRoot);
|
|
168
|
+
javaSrcRootExists = stat.isDirectory();
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// src/main/java doesn't exist
|
|
172
|
+
}
|
|
173
|
+
let basePackageFullPath = javaSrcRoot;
|
|
174
|
+
if (javaSrcRootExists) {
|
|
175
|
+
let currentDir = javaSrcRoot;
|
|
176
|
+
const packageParts = [];
|
|
177
|
+
while (true) {
|
|
178
|
+
try {
|
|
179
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
180
|
+
const subdirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith("."));
|
|
181
|
+
const files = entries.filter((e) => e.isFile());
|
|
182
|
+
// Traverse down if exactly one subdirectory and no Java/static files inside this directory level
|
|
183
|
+
if (subdirs.length === 1 && files.length === 0) {
|
|
184
|
+
const subDirName = subdirs[0].name;
|
|
185
|
+
packageParts.push(subDirName);
|
|
186
|
+
currentDir = path.join(currentDir, subDirName);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (packageParts.length > 0) {
|
|
197
|
+
basePackage = packageParts.join(".");
|
|
198
|
+
packagePath = packageParts.join("/");
|
|
199
|
+
basePackageFullPath = currentDir;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// 3. Package Layout Detection (feature-first vs layer-first)
|
|
203
|
+
let packageLayout = "feature-first";
|
|
204
|
+
if (javaSrcRootExists) {
|
|
205
|
+
try {
|
|
206
|
+
const entries = await fs.readdir(basePackageFullPath, { withFileTypes: true });
|
|
207
|
+
const subdirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith("."));
|
|
208
|
+
const layerKeywords = new Set([
|
|
209
|
+
"controller",
|
|
210
|
+
"controllers",
|
|
211
|
+
"service",
|
|
212
|
+
"services",
|
|
213
|
+
"repository",
|
|
214
|
+
"repositories",
|
|
215
|
+
"entity",
|
|
216
|
+
"entities",
|
|
217
|
+
"model",
|
|
218
|
+
"models",
|
|
219
|
+
"dto",
|
|
220
|
+
"dtos",
|
|
221
|
+
"mapper",
|
|
222
|
+
"mappers",
|
|
223
|
+
"config",
|
|
224
|
+
"configs",
|
|
225
|
+
"web",
|
|
226
|
+
]);
|
|
227
|
+
let matchingLayerDirsCount = 0;
|
|
228
|
+
for (const subdir of subdirs) {
|
|
229
|
+
if (layerKeywords.has(subdir.name.toLowerCase())) {
|
|
230
|
+
matchingLayerDirsCount++;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (matchingLayerDirsCount >= 2) {
|
|
234
|
+
packageLayout = "layer-first";
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
// Try sampling child packages to see if they hold feature sub-packages with layers inside
|
|
238
|
+
let looksLikeFeatureFirst = false;
|
|
239
|
+
const sampleLimit = Math.min(subdirs.length, 3);
|
|
240
|
+
for (let i = 0; i < sampleLimit; i++) {
|
|
241
|
+
const subDirPath = path.join(basePackageFullPath, subdirs[i].name);
|
|
242
|
+
const subEntries = await fs.readdir(subDirPath, { withFileTypes: true });
|
|
243
|
+
const nestedDirs = subEntries.filter((e) => e.isDirectory() && !e.name.startsWith("."));
|
|
244
|
+
const hasLayerInside = nestedDirs.some((nd) => layerKeywords.has(nd.name.toLowerCase()));
|
|
245
|
+
if (hasLayerInside) {
|
|
246
|
+
looksLikeFeatureFirst = true;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
packageLayout = looksLikeFeatureFirst ? "feature-first" : "feature-first"; // Default to feature-first
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// Fallback
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// 4. Validation Library Detection (Jakarta vs Javax)
|
|
258
|
+
let validationLibrary = "jakarta.validation";
|
|
259
|
+
if (javaSrcRootExists) {
|
|
260
|
+
try {
|
|
261
|
+
const javaFiles = await findFilesRecursively(basePackageFullPath, ".java", 20);
|
|
262
|
+
for (const file of javaFiles) {
|
|
263
|
+
const content = await fs.readFile(file, "utf8");
|
|
264
|
+
if (content.includes("import javax.validation.")) {
|
|
265
|
+
validationLibrary = "javax.validation";
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
else if (content.includes("import jakarta.validation.")) {
|
|
269
|
+
validationLibrary = "jakarta.validation";
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
// Fallback
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// 5. Test Framework Detection
|
|
279
|
+
let testFramework = "JUnit 5";
|
|
280
|
+
const javaTestRoot = path.join(dir, "src/test/java");
|
|
281
|
+
let javaTestRootExists = false;
|
|
282
|
+
try {
|
|
283
|
+
const stat = await fs.stat(javaTestRoot);
|
|
284
|
+
javaTestRootExists = stat.isDirectory();
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
// src/test/java doesn't exist
|
|
288
|
+
}
|
|
289
|
+
if (javaTestRootExists) {
|
|
290
|
+
try {
|
|
291
|
+
const testFiles = await findFilesRecursively(javaTestRoot, ".java", 20);
|
|
292
|
+
for (const file of testFiles) {
|
|
293
|
+
const content = await fs.readFile(file, "utf8");
|
|
294
|
+
if (content.includes("org.junit.jupiter.")) {
|
|
295
|
+
testFramework = "JUnit 5";
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
else if (content.includes("org.junit.Test")) {
|
|
299
|
+
testFramework = "JUnit 4";
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
// Fallback
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// 6. Microservice properties scanning (service name and port)
|
|
309
|
+
let springApplicationName = path.basename(dir);
|
|
310
|
+
let serverPort = "8080";
|
|
311
|
+
const configFiles = [
|
|
312
|
+
"application.properties",
|
|
313
|
+
"application.yml",
|
|
314
|
+
"application.yaml",
|
|
315
|
+
"bootstrap.properties",
|
|
316
|
+
"bootstrap.yml",
|
|
317
|
+
"bootstrap.yaml",
|
|
318
|
+
];
|
|
319
|
+
const resourcesDir = path.join(dir, "src/main/resources");
|
|
320
|
+
for (const file of configFiles) {
|
|
321
|
+
try {
|
|
322
|
+
const filePath = path.join(resourcesDir, file);
|
|
323
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
324
|
+
const parsed = file.endsWith(".properties")
|
|
325
|
+
? parsePropertiesSimple(content)
|
|
326
|
+
: parseYamlSimple(content);
|
|
327
|
+
if (parsed.appName) {
|
|
328
|
+
springApplicationName = parsed.appName;
|
|
329
|
+
isMicroservice = true;
|
|
330
|
+
}
|
|
331
|
+
if (parsed.port) {
|
|
332
|
+
serverPort = parsed.port;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
// Ignore if file doesn't exist or is unreadable
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
buildTool,
|
|
341
|
+
buildCommand,
|
|
342
|
+
buildVerifyArgs,
|
|
343
|
+
packageLayout,
|
|
344
|
+
basePackage,
|
|
345
|
+
packagePath,
|
|
346
|
+
validationLibrary,
|
|
347
|
+
testFramework,
|
|
348
|
+
isMicroservice,
|
|
349
|
+
springApplicationName,
|
|
350
|
+
serverPort,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Analyzes React TypeScript project details.
|
|
355
|
+
*/
|
|
356
|
+
async function analyzeReactTs(dir) {
|
|
357
|
+
let packageManager = "npm";
|
|
358
|
+
let runCommand = "npm run";
|
|
359
|
+
try {
|
|
360
|
+
const hasYarnLock = await fs.stat(path.join(dir, "yarn.lock")).then((s) => s.isFile()).catch(() => false);
|
|
361
|
+
const hasPnpmLock = await fs.stat(path.join(dir, "pnpm-lock.yaml")).then((s) => s.isFile()).catch(() => false);
|
|
362
|
+
if (hasYarnLock) {
|
|
363
|
+
packageManager = "yarn";
|
|
364
|
+
runCommand = "yarn";
|
|
365
|
+
}
|
|
366
|
+
else if (hasPnpmLock) {
|
|
367
|
+
packageManager = "pnpm";
|
|
368
|
+
runCommand = "pnpm";
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
// Use default
|
|
373
|
+
}
|
|
374
|
+
return { packageManager, runCommand };
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Analyzes FastAPI project details.
|
|
378
|
+
*/
|
|
379
|
+
async function analyzeFastApi(dir) {
|
|
380
|
+
let packageManager = "pip";
|
|
381
|
+
let runCommand = "python";
|
|
382
|
+
try {
|
|
383
|
+
const hasPoetry = await fs.stat(path.join(dir, "poetry.lock")).then((s) => s.isFile()).catch(() => false);
|
|
384
|
+
const hasPipenv = await fs.stat(path.join(dir, "Pipfile")).then((s) => s.isFile()).catch(() => false);
|
|
385
|
+
if (hasPoetry) {
|
|
386
|
+
packageManager = "poetry";
|
|
387
|
+
runCommand = "poetry run";
|
|
388
|
+
}
|
|
389
|
+
else if (hasPipenv) {
|
|
390
|
+
packageManager = "pipenv";
|
|
391
|
+
runCommand = "pipenv run";
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
// Use default
|
|
396
|
+
}
|
|
397
|
+
return { packageManager, runCommand };
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Analyzes Python AI project details.
|
|
401
|
+
*/
|
|
402
|
+
async function analyzePythonAi(dir) {
|
|
403
|
+
let packageManager = "pip";
|
|
404
|
+
let runCommand = "python";
|
|
405
|
+
let hasGpuLibraries = false;
|
|
406
|
+
let hasHardwareLibraries = false;
|
|
407
|
+
try {
|
|
408
|
+
const hasPoetry = await fs.stat(path.join(dir, "poetry.lock")).then((s) => s.isFile()).catch(() => false);
|
|
409
|
+
const hasPipenv = await fs.stat(path.join(dir, "Pipfile")).then((s) => s.isFile()).catch(() => false);
|
|
410
|
+
if (hasPoetry) {
|
|
411
|
+
packageManager = "poetry";
|
|
412
|
+
runCommand = "poetry run python";
|
|
413
|
+
}
|
|
414
|
+
else if (hasPipenv) {
|
|
415
|
+
packageManager = "pipenv";
|
|
416
|
+
runCommand = "pipenv run python";
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
// Use default
|
|
421
|
+
}
|
|
422
|
+
// Scan requirements / pyproject to check for GPU and Hardware references
|
|
423
|
+
const checkGpuKws = ["torch", "tensorflow", "cuda", "onnxruntime-gpu"];
|
|
424
|
+
const checkHwKws = ["opencv-python", "mediapipe", "pyserial", "pyaudio", "picamera", "opencv"];
|
|
425
|
+
const buildFiles = ["requirements.txt", "pyproject.toml", "Pipfile"];
|
|
426
|
+
for (const file of buildFiles) {
|
|
427
|
+
try {
|
|
428
|
+
const content = await fs.readFile(path.join(dir, file), "utf8");
|
|
429
|
+
if (checkGpuKws.some(kw => content.includes(kw))) {
|
|
430
|
+
hasGpuLibraries = true;
|
|
431
|
+
}
|
|
432
|
+
if (checkHwKws.some(kw => content.includes(kw))) {
|
|
433
|
+
hasHardwareLibraries = true;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
// Ignore
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return { packageManager, runCommand, hasGpuLibraries, hasHardwareLibraries };
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Entry point to analyze module configurations.
|
|
444
|
+
*/
|
|
445
|
+
export async function analyzeModule(dir, stacks) {
|
|
446
|
+
const context = {};
|
|
447
|
+
for (const stack of stacks) {
|
|
448
|
+
if (stack === "spring-boot") {
|
|
449
|
+
context["spring-boot"] = await analyzeSpringBoot(dir);
|
|
450
|
+
}
|
|
451
|
+
else if (stack === "react-ts") {
|
|
452
|
+
context["react-ts"] = await analyzeReactTs(dir);
|
|
453
|
+
}
|
|
454
|
+
else if (stack === "fastapi") {
|
|
455
|
+
context["fastapi"] = await analyzeFastApi(dir);
|
|
456
|
+
}
|
|
457
|
+
else if (stack === "python-ai") {
|
|
458
|
+
context["python-ai"] = await analyzePythonAi(dir);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return context;
|
|
462
|
+
}
|
package/dist/core/detector.js
CHANGED
|
@@ -9,7 +9,7 @@ import path from "path";
|
|
|
9
9
|
*/
|
|
10
10
|
async function detectProjectStackDirect(cwd) {
|
|
11
11
|
const stacks = [];
|
|
12
|
-
// 1. Detect Java Spring Boot (pom.xml)
|
|
12
|
+
// 1. Detect Java Spring Boot (pom.xml, build.gradle, or build.gradle.kts)
|
|
13
13
|
try {
|
|
14
14
|
const pomPath = path.join(cwd, "pom.xml");
|
|
15
15
|
const stat = await fs.stat(pomPath);
|
|
@@ -18,7 +18,25 @@ async function detectProjectStackDirect(cwd) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
catch {
|
|
21
|
-
|
|
21
|
+
try {
|
|
22
|
+
const gradlePath = path.join(cwd, "build.gradle");
|
|
23
|
+
const stat = await fs.stat(gradlePath);
|
|
24
|
+
if (stat.isFile()) {
|
|
25
|
+
stacks.push("spring-boot");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
try {
|
|
30
|
+
const gradleKtsPath = path.join(cwd, "build.gradle.kts");
|
|
31
|
+
const stat = await fs.stat(gradleKtsPath);
|
|
32
|
+
if (stat.isFile()) {
|
|
33
|
+
stacks.push("spring-boot");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Ignore
|
|
38
|
+
}
|
|
39
|
+
}
|
|
22
40
|
}
|
|
23
41
|
// 2. Detect React + TypeScript (package.json containing react dependency)
|
|
24
42
|
try {
|
|
@@ -34,13 +52,28 @@ async function detectProjectStackDirect(cwd) {
|
|
|
34
52
|
catch {
|
|
35
53
|
// Ignore
|
|
36
54
|
}
|
|
37
|
-
// 3. Detect Python FastAPI
|
|
55
|
+
// 3. Detect Python FastAPI & Python AI
|
|
56
|
+
const aiKeywords = [
|
|
57
|
+
"torch",
|
|
58
|
+
"tensorflow",
|
|
59
|
+
"scikit-learn",
|
|
60
|
+
"transformers",
|
|
61
|
+
"langchain",
|
|
62
|
+
"llama-index",
|
|
63
|
+
"opencv-python",
|
|
64
|
+
"mediapipe",
|
|
65
|
+
"numpy",
|
|
66
|
+
"pandas"
|
|
67
|
+
];
|
|
38
68
|
try {
|
|
39
69
|
const pyprojectPath = path.join(cwd, "pyproject.toml");
|
|
40
70
|
const content = await fs.readFile(pyprojectPath, "utf8");
|
|
41
71
|
if (content.includes("fastapi")) {
|
|
42
72
|
stacks.push("fastapi");
|
|
43
73
|
}
|
|
74
|
+
if (aiKeywords.some(kw => content.includes(kw))) {
|
|
75
|
+
stacks.push("python-ai");
|
|
76
|
+
}
|
|
44
77
|
}
|
|
45
78
|
catch {
|
|
46
79
|
try {
|
|
@@ -49,75 +82,89 @@ async function detectProjectStackDirect(cwd) {
|
|
|
49
82
|
if (content.includes("fastapi")) {
|
|
50
83
|
stacks.push("fastapi");
|
|
51
84
|
}
|
|
85
|
+
if (aiKeywords.some(kw => content.includes(kw))) {
|
|
86
|
+
stacks.push("python-ai");
|
|
87
|
+
}
|
|
52
88
|
}
|
|
53
89
|
catch {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
export async function detectProjectStack(cwd) {
|
|
63
|
-
const stacks = await detectProjectStackDirect(cwd);
|
|
64
|
-
// Scan 1-level deep subdirectories for potential monorepos/multimodules
|
|
65
|
-
try {
|
|
66
|
-
const entries = await fs.readdir(cwd, { withFileTypes: true });
|
|
67
|
-
for (const entry of entries) {
|
|
68
|
-
if (entry.isDirectory() &&
|
|
69
|
-
!entry.name.startsWith(".") &&
|
|
70
|
-
entry.name !== "node_modules" &&
|
|
71
|
-
entry.name !== "dist") {
|
|
72
|
-
const subPath = path.join(cwd, entry.name);
|
|
73
|
-
const subStacks = await detectProjectStackDirect(subPath);
|
|
74
|
-
for (const stack of subStacks) {
|
|
75
|
-
if (!stacks.includes(stack)) {
|
|
76
|
-
stacks.push(stack);
|
|
77
|
-
}
|
|
90
|
+
try {
|
|
91
|
+
const pipfilePath = path.join(cwd, "Pipfile");
|
|
92
|
+
const content = await fs.readFile(pipfilePath, "utf8");
|
|
93
|
+
if (content.includes("fastapi")) {
|
|
94
|
+
stacks.push("fastapi");
|
|
95
|
+
}
|
|
96
|
+
if (aiKeywords.some(kw => content.includes(kw))) {
|
|
97
|
+
stacks.push("python-ai");
|
|
78
98
|
}
|
|
79
99
|
}
|
|
100
|
+
catch {
|
|
101
|
+
// Ignore
|
|
102
|
+
}
|
|
80
103
|
}
|
|
81
104
|
}
|
|
82
|
-
catch {
|
|
83
|
-
// Ignore
|
|
84
|
-
}
|
|
85
105
|
return stacks;
|
|
86
106
|
}
|
|
87
107
|
/**
|
|
88
|
-
*
|
|
108
|
+
* Helper to recursively search for modules containing manifest configurations up to maxDepth.
|
|
89
109
|
*/
|
|
90
|
-
|
|
110
|
+
async function findModulesRecursively(baseDir, currentDir, maxDepth, currentDepth = 0) {
|
|
91
111
|
const modules = [];
|
|
92
|
-
|
|
93
|
-
|
|
112
|
+
// Skip standard build and internal directories to avoid deep scans
|
|
113
|
+
const dirName = path.basename(currentDir);
|
|
114
|
+
if (dirName.startsWith(".") ||
|
|
115
|
+
dirName === "node_modules" ||
|
|
116
|
+
dirName === "target" ||
|
|
117
|
+
dirName === "build" ||
|
|
118
|
+
dirName === "dist" ||
|
|
119
|
+
dirName === "bin" ||
|
|
120
|
+
dirName === "out" ||
|
|
121
|
+
dirName === "venv" ||
|
|
122
|
+
dirName === ".venv") {
|
|
123
|
+
return modules;
|
|
124
|
+
}
|
|
125
|
+
// Detect direct stack presets in this current directory
|
|
126
|
+
const directStacks = await detectProjectStackDirect(currentDir);
|
|
127
|
+
if (directStacks.length > 0) {
|
|
94
128
|
modules.push({
|
|
95
|
-
dir:
|
|
96
|
-
name: ".",
|
|
97
|
-
stacks:
|
|
129
|
+
dir: currentDir,
|
|
130
|
+
name: currentDir === baseDir ? "." : path.relative(baseDir, currentDir).replace(/\\/g, "/"),
|
|
131
|
+
stacks: directStacks,
|
|
98
132
|
});
|
|
99
133
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
entry.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (subStacks.length > 0) {
|
|
110
|
-
modules.push({
|
|
111
|
-
dir: subPath,
|
|
112
|
-
name: entry.name,
|
|
113
|
-
stacks: subStacks,
|
|
114
|
-
});
|
|
134
|
+
// If we haven't hit max depth, traverse subdirectories
|
|
135
|
+
if (currentDepth < maxDepth) {
|
|
136
|
+
try {
|
|
137
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
if (entry.isDirectory()) {
|
|
140
|
+
const subPath = path.join(currentDir, entry.name);
|
|
141
|
+
const subModules = await findModulesRecursively(baseDir, subPath, maxDepth, currentDepth + 1);
|
|
142
|
+
modules.push(...subModules);
|
|
115
143
|
}
|
|
116
144
|
}
|
|
117
145
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
146
|
+
catch {
|
|
147
|
+
// Ignore directory read errors
|
|
148
|
+
}
|
|
121
149
|
}
|
|
122
150
|
return modules;
|
|
123
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Scans the workspace directory and subfolders (up to 3 levels deep) for monorepo detection.
|
|
154
|
+
*/
|
|
155
|
+
export async function detectProjectStack(cwd) {
|
|
156
|
+
const modules = await detectProjectModules(cwd);
|
|
157
|
+
const stacks = new Set();
|
|
158
|
+
for (const mod of modules) {
|
|
159
|
+
for (const stack of mod.stacks) {
|
|
160
|
+
stacks.add(stack);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return Array.from(stacks);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Detects stacks grouped by module directories (root + up to 3 levels deep directories).
|
|
167
|
+
*/
|
|
168
|
+
export async function detectProjectModules(cwd) {
|
|
169
|
+
return findModulesRecursively(cwd, cwd, 3);
|
|
170
|
+
}
|