agent-workflow-kit-cli 1.0.0-mvp → 1.1.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/doctor.js +21 -0
- package/dist/cli/commands/init.js +32 -5
- package/dist/cli/index.js +1 -1
- package/dist/core/analyzer.js +462 -0
- package/dist/core/detector.js +39 -3
- 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
|
@@ -75,6 +75,27 @@ npx agent-workflow-kit-cli doctor || exit 1
|
|
|
75
75
|
console.log(chalk.gray("Running: ruff check ."));
|
|
76
76
|
await execa("ruff", ["check", "."], { cwd, stdio: "inherit" });
|
|
77
77
|
}
|
|
78
|
+
else if (stack === "python-ai") {
|
|
79
|
+
let cmd = "ruff";
|
|
80
|
+
let args = ["check", "."];
|
|
81
|
+
try {
|
|
82
|
+
const hasPoetry = await fs.stat(path.join(cwd, "poetry.lock")).then(s => s.isFile()).catch(() => false);
|
|
83
|
+
if (hasPoetry) {
|
|
84
|
+
cmd = "poetry";
|
|
85
|
+
args = ["run", "python", "-m", "ruff", "check", "."];
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const hasPipenv = await fs.stat(path.join(cwd, "Pipfile")).then(s => s.isFile()).catch(() => false);
|
|
89
|
+
if (hasPipenv) {
|
|
90
|
+
cmd = "pipenv";
|
|
91
|
+
args = ["run", "python", "-m", "ruff", "check", "."];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch { }
|
|
96
|
+
console.log(chalk.gray(`Running: ${cmd} ${args.join(" ")}`));
|
|
97
|
+
await execa(cmd, args, { cwd, stdio: "inherit" });
|
|
98
|
+
}
|
|
78
99
|
else if (stack === "react-ts") {
|
|
79
100
|
console.log(chalk.gray("Running: npx tsc --noEmit"));
|
|
80
101
|
await execa("npx", ["tsc", "--noEmit"], { cwd, stdio: "inherit" });
|
|
@@ -9,6 +9,7 @@ import { fileURLToPath } from "url";
|
|
|
9
9
|
import { detectProjectModules } from "../../core/detector.js";
|
|
10
10
|
import { renderTemplate, readStaticTemplateFile, getStackRules, getStackSkills, } from "../../core/renderer.js";
|
|
11
11
|
import { updateFileWithBlock, writeRuleWithChunking, } from "../../core/emitter.js";
|
|
12
|
+
import { analyzeModule } from "../../core/analyzer.js";
|
|
12
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
14
|
const __dirname = path.dirname(__filename);
|
|
14
15
|
function printSuccessAndNextSteps(options) {
|
|
@@ -20,9 +21,9 @@ function printSuccessAndNextSteps(options) {
|
|
|
20
21
|
console.log(chalk.gray(` - Root: ${chalk.underline("AGENTS.md")}`));
|
|
21
22
|
console.log(chalk.gray(` - Stack rules: ${chalk.underline(".agents/rules/")}`));
|
|
22
23
|
console.log(chalk.white(`2. Setup automatic git pre-commit hook validation:`));
|
|
23
|
-
console.log(chalk.cyan(`
|
|
24
|
+
console.log(chalk.cyan(` npx agent-workflow-kit-cli doctor --install-hook`));
|
|
24
25
|
console.log(chalk.white(`3. Export custom skills to register with your AI agent (e.g. Antigravity):`));
|
|
25
|
-
console.log(chalk.cyan(`
|
|
26
|
+
console.log(chalk.cyan(` npx agent-workflow-kit-cli export antigravity`));
|
|
26
27
|
console.log(chalk.dim("------------------------------------------\n"));
|
|
27
28
|
}
|
|
28
29
|
}
|
|
@@ -86,10 +87,12 @@ export async function runInit(options) {
|
|
|
86
87
|
// Process each module
|
|
87
88
|
for (const mod of modules) {
|
|
88
89
|
console.log(chalk.cyan(`\nProcessing module: ${mod.name} (stacks: ${mod.stacks.join(", ")})`));
|
|
90
|
+
const analysis = await analyzeModule(mod.dir, mod.stacks);
|
|
89
91
|
let stackContent = "";
|
|
90
92
|
for (const stack of mod.stacks) {
|
|
91
93
|
try {
|
|
92
|
-
const
|
|
94
|
+
const stackCtx = analysis[stack] || {};
|
|
95
|
+
const rendered = await renderTemplate(`${stack}/AGENTS.md.hbs`, stackCtx);
|
|
93
96
|
stackContent += rendered + "\n\n";
|
|
94
97
|
}
|
|
95
98
|
catch (err) {
|
|
@@ -116,14 +119,38 @@ export async function runInit(options) {
|
|
|
116
119
|
console.log(chalk.green(`✔️ Created ${mod.name}/AGENTS.md`));
|
|
117
120
|
}
|
|
118
121
|
}
|
|
122
|
+
// Copy common skills for this module
|
|
123
|
+
try {
|
|
124
|
+
const commonSkills = await getStackSkills("common");
|
|
125
|
+
for (const skill of commonSkills) {
|
|
126
|
+
const relativeSkillPath = `common/skills/${skill}`;
|
|
127
|
+
try {
|
|
128
|
+
const skillContent = await readStaticTemplateFile(relativeSkillPath, {});
|
|
129
|
+
const targetSkillPath = path.join(mod.dir, ".agents", "skills", skill);
|
|
130
|
+
await fs.mkdir(path.dirname(targetSkillPath), { recursive: true });
|
|
131
|
+
await fs.writeFile(targetSkillPath, skillContent, "utf8");
|
|
132
|
+
console.log(chalk.green(`✔️ Wrote common skill ${skill} to ${mod.name}/.agents/skills/${skill}`));
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
console.error(chalk.red(`Failed to copy common skill ${skill}: ${err instanceof Error ? err.message : String(err)}`));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Ignore
|
|
141
|
+
}
|
|
119
142
|
// Copy rules and skills for each stack in this module
|
|
120
143
|
for (const stack of mod.stacks) {
|
|
144
|
+
const stackCtx = analysis[stack] || {};
|
|
121
145
|
// A. Rules
|
|
122
146
|
const rules = await getStackRules(stack);
|
|
123
147
|
for (const rule of rules) {
|
|
148
|
+
if (rule === "microservice-style.md" && stackCtx.isMicroservice !== true) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
124
151
|
const relativeRulePath = `${stack}/rules/${rule}`;
|
|
125
152
|
try {
|
|
126
|
-
const ruleContent = await readStaticTemplateFile(relativeRulePath);
|
|
153
|
+
const ruleContent = await readStaticTemplateFile(relativeRulePath, stackCtx);
|
|
127
154
|
const targetRulePath = path.join(mod.dir, ".agents", "rules", rule);
|
|
128
155
|
if (options.dryRun) {
|
|
129
156
|
console.log(chalk.gray(`[Dry Run] Would write rule to ${targetRulePath} (length: ${ruleContent.length} chars)`));
|
|
@@ -142,7 +169,7 @@ export async function runInit(options) {
|
|
|
142
169
|
for (const skill of skills) {
|
|
143
170
|
const relativeSkillPath = `${stack}/skills/${skill}`;
|
|
144
171
|
try {
|
|
145
|
-
const skillContent = await readStaticTemplateFile(relativeSkillPath);
|
|
172
|
+
const skillContent = await readStaticTemplateFile(relativeSkillPath, stackCtx);
|
|
146
173
|
const targetSkillPath = path.join(mod.dir, ".agents", "skills", skill);
|
|
147
174
|
if (options.dryRun) {
|
|
148
175
|
console.log(chalk.gray(`[Dry Run] Would write skill to ${targetSkillPath}`));
|
package/dist/cli/index.js
CHANGED
|
@@ -13,7 +13,7 @@ export function runCli() {
|
|
|
13
13
|
program
|
|
14
14
|
.name("agent-workflow-kit")
|
|
15
15
|
.description("Generate AI coding workflows/rules/templates for Codex and Antigravity")
|
|
16
|
-
.version("1.
|
|
16
|
+
.version("1.1.0");
|
|
17
17
|
program
|
|
18
18
|
.command("init")
|
|
19
19
|
.description("Initialize agent guidelines and skills for the repository")
|
|
@@ -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,6 +82,9 @@ 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
90
|
// Ignore
|
package/dist/core/renderer.js
CHANGED
|
@@ -6,6 +6,10 @@ import handlebars from "handlebars";
|
|
|
6
6
|
import { promises as fs } from "fs";
|
|
7
7
|
import path from "path";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
|
+
// Register custom Handlebars helpers
|
|
10
|
+
handlebars.registerHelper("eq", function (a, b) {
|
|
11
|
+
return a === b;
|
|
12
|
+
});
|
|
9
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
14
|
const __dirname = path.dirname(__filename);
|
|
11
15
|
// The templates directory is located at '../../templates' relative to 'dist/core/renderer.js'
|
|
@@ -22,12 +26,18 @@ export async function renderTemplate(templatePath, context) {
|
|
|
22
26
|
return compiled(context);
|
|
23
27
|
}
|
|
24
28
|
/**
|
|
25
|
-
* Reads a static
|
|
29
|
+
* Reads a static template file (rules or skills) with optional Handlebars interpolation.
|
|
26
30
|
* @param filePath Relative path inside templates folder (e.g. 'spring-boot/rules/java-style.md')
|
|
31
|
+
* @param context Optional key-value data for compilation
|
|
27
32
|
*/
|
|
28
|
-
export async function readStaticTemplateFile(filePath) {
|
|
33
|
+
export async function readStaticTemplateFile(filePath, context) {
|
|
29
34
|
const fullPath = path.join(TEMPLATES_DIR, filePath);
|
|
30
|
-
|
|
35
|
+
const fileContent = await fs.readFile(fullPath, "utf8");
|
|
36
|
+
if (context) {
|
|
37
|
+
const compiled = handlebars.compile(fileContent);
|
|
38
|
+
return compiled(context);
|
|
39
|
+
}
|
|
40
|
+
return fileContent;
|
|
31
41
|
}
|
|
32
42
|
/**
|
|
33
43
|
* Gets all rules files for a stack.
|