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.
@@ -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(` run: npx agent-workflow-kit-cli doctor --install-hook`));
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(` run: npx agent-workflow-kit-cli export antigravity`));
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 rendered = await renderTemplate(`${stack}/AGENTS.md.hbs`, {});
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.0.0-mvp");
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
+ }
@@ -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
- // Ignore
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 (pyproject.toml or requirements.txt containing 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
@@ -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 rules file as-is without Handlebars interpolation.
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
- return fs.readFile(fullPath, "utf8");
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.