agent-skills-standard 1.3.2 → 1.4.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 +36 -1
- package/dist/commands/init.js +4 -1
- package/dist/commands/sync.js +17 -0
- package/dist/constants/enums.d.ts +3 -1
- package/dist/constants/enums.js +2 -0
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.js +42 -0
- package/dist/constants/skills.js +33 -0
- package/dist/index.js +1 -1
- package/dist/services/ConfigService.d.ts +5 -0
- package/dist/services/ConfigService.js +28 -0
- package/dist/services/DetectionService.d.ts +7 -0
- package/dist/services/DetectionService.js +148 -37
- package/dist/services/__tests__/ConfigService.spec.js +58 -0
- package/dist/services/__tests__/DetectionService.spec.js +202 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,6 +33,36 @@ Modern AI coding agents are powerful, but they have major flaws:
|
|
|
33
33
|
|
|
34
34
|
---
|
|
35
35
|
|
|
36
|
+
## 🛠 The Solution: Digital DNA for AI
|
|
37
|
+
|
|
38
|
+
Agent Skills Standard treats instructions as **versioned dependencies**, much like software libraries.
|
|
39
|
+
|
|
40
|
+
- **🎯 Smart Loading**: We use a "Search-on-Demand" pattern. The AI only looks at detailed examples when it specifically needs them, saving its memory for your actual code.
|
|
41
|
+
- **🚀 High-Density Language**: We use a specialized "Compressed Syntax" that is **40% more efficient** than normal English. This means the AI understands more while using fewer resources.
|
|
42
|
+
- **🔁 One-Click Sync**: A single command ensures your AI tool stays up-to-date with your team's latest standards.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## ✨ Features
|
|
47
|
+
|
|
48
|
+
- **🛡️ Multi-Agent Support**: Out-of-the-box mapping for Cursor, Claude Dev, GitHub Copilot, and more.
|
|
49
|
+
- **📦 Modular Registry**: Don't load everything. Only enable the skills your project actually uses (e.g., `Flutter + BLoC + Clean Architecture`).
|
|
50
|
+
- **🔄 Dynamic Re-detection**: Automatically re-enables excluded skills if matching dependencies (like `Retrofit`, `Room`, or `Navigation`) are added to your project.
|
|
51
|
+
- **🔒 Secure Overrides**: Lock specific files in your project so they never get overwritten by the central registry.
|
|
52
|
+
- **📊 Semantic Tagging**: Skills are tagged with triggers that tell the AI _exactly_ when to apply them.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## � Token Economy & Optimization
|
|
57
|
+
|
|
58
|
+
To ensure AI efficiency, this project follows a strict **Token Economy**. Every skill is audited for its footprint in the AI's context window.
|
|
59
|
+
|
|
60
|
+
### 📏 Our Standards
|
|
61
|
+
|
|
62
|
+
- **High-Density**: Core rules in `SKILL.md` are kept under **70 lines**.
|
|
63
|
+
- **Efficiency**: Target **< 500 tokens** per primary skill file.
|
|
64
|
+
- **Progressive Disclosure**: Heavy examples, checklists, and implementation guides are moved to the `references/` directory and are only loaded by the agent when specific context matches.
|
|
65
|
+
|
|
36
66
|
## 🚀 Installation
|
|
37
67
|
|
|
38
68
|
You can run the tool instantly without installing, or install it globally for convenience:
|
|
@@ -115,7 +145,12 @@ The CLI connects to the [Official Skills Registry](https://github.com/HoangNguye
|
|
|
115
145
|
- **React**: Hooks, Patterns, Performance.
|
|
116
146
|
- **NestJS**: Architecture, Microservices, Security.
|
|
117
147
|
- **Next.js**: App Router, RSC, FSD Architecture.
|
|
118
|
-
- **Golang**:
|
|
148
|
+
- **Golang**: Clean Architecture, API Design, Concurrency.
|
|
149
|
+
- **Angular**: Standalone, Signals, Control Flow.
|
|
150
|
+
- **Kotlin**: Idiomatic Patterns, Coroutines, Flow.
|
|
151
|
+
- **Spring Boot**: Architecture, Security, Data, Test.
|
|
152
|
+
- **Android Native**: Architecture, Compose, DI, Performance, Testing, WorkManager.
|
|
153
|
+
- **iOS Native**: Architecture, UI/Layout, Lifecycle, Security, Performance.
|
|
119
154
|
|
|
120
155
|
---
|
|
121
156
|
|
package/dist/commands/init.js
CHANGED
|
@@ -74,7 +74,10 @@ class InitCommand {
|
|
|
74
74
|
]);
|
|
75
75
|
const frameworkId = answers.framework;
|
|
76
76
|
const frameworkDef = constants_1.SUPPORTED_FRAMEWORKS.find((f) => f.id === frameworkId);
|
|
77
|
-
const
|
|
77
|
+
const languages = frameworkDef
|
|
78
|
+
? await this.detectionService.detectLanguages(frameworkDef)
|
|
79
|
+
: [];
|
|
80
|
+
const config = this.configService.buildInitialConfig(frameworkId, answers.agents, answers.registry, metadata, languages);
|
|
78
81
|
const projectDeps = await this.detectionService.getProjectDeps();
|
|
79
82
|
this.configService.applyDependencyExclusions(config, frameworkId, projectDeps);
|
|
80
83
|
const commentHeader = `# Auto-detected configuration generated by agent-skills-standard init
|
package/dist/commands/sync.js
CHANGED
|
@@ -9,6 +9,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
10
10
|
const constants_1 = require("../constants");
|
|
11
11
|
const ConfigService_1 = require("../services/ConfigService");
|
|
12
|
+
const DetectionService_1 = require("../services/DetectionService");
|
|
12
13
|
const GithubService_1 = require("../services/GithubService");
|
|
13
14
|
class SyncCommand {
|
|
14
15
|
constructor() {
|
|
@@ -23,6 +24,22 @@ class SyncCommand {
|
|
|
23
24
|
console.log(picocolors_1.default.red('❌ Error: .skillsrc not found. Run `init` first.'));
|
|
24
25
|
return;
|
|
25
26
|
}
|
|
27
|
+
// --- DYNAMIC RE-DETECTION ---
|
|
28
|
+
const detectionService = new DetectionService_1.DetectionService();
|
|
29
|
+
const projectDeps = await detectionService.getProjectDeps();
|
|
30
|
+
let configChanged = false;
|
|
31
|
+
const categoriesToReconcile = Object.keys(config.skills);
|
|
32
|
+
for (const cat of categoriesToReconcile) {
|
|
33
|
+
const reenabled = this.configService.reconcileDependencies(config, cat, projectDeps);
|
|
34
|
+
if (reenabled.length > 0) {
|
|
35
|
+
console.log(picocolors_1.default.yellow(`✨ Dynamic Re-detection: Re-enabling [${reenabled.join(', ')}] in '${cat}' category.`));
|
|
36
|
+
configChanged = true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (configChanged) {
|
|
40
|
+
await this.configService.saveConfig(config);
|
|
41
|
+
}
|
|
42
|
+
// ----------------------------
|
|
26
43
|
// 2. Check for updates
|
|
27
44
|
config = await this.checkForUpdates(config);
|
|
28
45
|
console.log(picocolors_1.default.cyan(`🚀 Syncing skills from ${config.registry}...`));
|
package/dist/constants/enums.js
CHANGED
|
@@ -23,4 +23,6 @@ var Framework;
|
|
|
23
23
|
Framework["React"] = "react";
|
|
24
24
|
Framework["ReactNative"] = "react-native";
|
|
25
25
|
Framework["Angular"] = "angular";
|
|
26
|
+
Framework["SpringBoot"] = "spring-boot";
|
|
27
|
+
Framework["Android"] = "android";
|
|
26
28
|
})(Framework || (exports.Framework = Framework = {}));
|
|
@@ -14,6 +14,7 @@ export interface FrameworkDefinition {
|
|
|
14
14
|
languages: string[];
|
|
15
15
|
detectionFiles: string[];
|
|
16
16
|
detectionDependencies?: string[];
|
|
17
|
+
languageDetection?: Record<string, string[]>;
|
|
17
18
|
}
|
|
18
19
|
export declare const getAgentDefinition: (id: Agent) => AgentDefinition;
|
|
19
20
|
export declare const getFrameworkDefinition: (id: Framework) => FrameworkDefinition;
|
package/dist/constants/index.js
CHANGED
|
@@ -97,6 +97,10 @@ const getFrameworkDefinition = (id) => {
|
|
|
97
97
|
languages: ['typescript', 'javascript'],
|
|
98
98
|
detectionFiles: ['nest-cli.json'],
|
|
99
99
|
detectionDependencies: ['@nestjs/core'],
|
|
100
|
+
languageDetection: {
|
|
101
|
+
typescript: ['tsconfig.json'],
|
|
102
|
+
javascript: ['jsconfig.json'],
|
|
103
|
+
},
|
|
100
104
|
};
|
|
101
105
|
case enums_1.Framework.Golang:
|
|
102
106
|
return {
|
|
@@ -112,6 +116,10 @@ const getFrameworkDefinition = (id) => {
|
|
|
112
116
|
languages: ['typescript', 'javascript'],
|
|
113
117
|
detectionFiles: ['next.config.js', 'next.config.mjs'],
|
|
114
118
|
detectionDependencies: ['next'],
|
|
119
|
+
languageDetection: {
|
|
120
|
+
typescript: ['tsconfig.json'],
|
|
121
|
+
javascript: ['jsconfig.json'],
|
|
122
|
+
},
|
|
115
123
|
};
|
|
116
124
|
case enums_1.Framework.React:
|
|
117
125
|
return {
|
|
@@ -120,6 +128,10 @@ const getFrameworkDefinition = (id) => {
|
|
|
120
128
|
languages: ['typescript', 'javascript'],
|
|
121
129
|
detectionFiles: [],
|
|
122
130
|
detectionDependencies: ['react', 'react-dom'],
|
|
131
|
+
languageDetection: {
|
|
132
|
+
typescript: ['tsconfig.json'],
|
|
133
|
+
javascript: ['jsconfig.json'],
|
|
134
|
+
},
|
|
123
135
|
};
|
|
124
136
|
case enums_1.Framework.ReactNative:
|
|
125
137
|
return {
|
|
@@ -128,6 +140,10 @@ const getFrameworkDefinition = (id) => {
|
|
|
128
140
|
languages: ['typescript', 'javascript'],
|
|
129
141
|
detectionFiles: ['metro.config.js'],
|
|
130
142
|
detectionDependencies: ['react-native'],
|
|
143
|
+
languageDetection: {
|
|
144
|
+
typescript: ['tsconfig.json'],
|
|
145
|
+
javascript: ['jsconfig.json'],
|
|
146
|
+
},
|
|
131
147
|
};
|
|
132
148
|
case enums_1.Framework.Angular:
|
|
133
149
|
return {
|
|
@@ -136,6 +152,32 @@ const getFrameworkDefinition = (id) => {
|
|
|
136
152
|
languages: ['typescript'],
|
|
137
153
|
detectionFiles: ['angular.json'],
|
|
138
154
|
};
|
|
155
|
+
case enums_1.Framework.SpringBoot:
|
|
156
|
+
return {
|
|
157
|
+
id,
|
|
158
|
+
name: 'Spring Boot',
|
|
159
|
+
languages: ['java', 'kotlin'],
|
|
160
|
+
detectionFiles: ['pom.xml', 'build.gradle', 'build.gradle.kts'],
|
|
161
|
+
languageDetection: {
|
|
162
|
+
kotlin: ['src/main/kotlin', 'build.gradle.kts'],
|
|
163
|
+
java: ['src/main/java'],
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
case enums_1.Framework.Android:
|
|
167
|
+
return {
|
|
168
|
+
id,
|
|
169
|
+
name: 'Android',
|
|
170
|
+
languages: ['kotlin', 'java'],
|
|
171
|
+
detectionFiles: [
|
|
172
|
+
'build.gradle',
|
|
173
|
+
'build.gradle.kts',
|
|
174
|
+
'AndroidManifest.xml',
|
|
175
|
+
],
|
|
176
|
+
languageDetection: {
|
|
177
|
+
kotlin: ['src/main/kotlin', 'build.gradle.kts'],
|
|
178
|
+
java: ['src/main/java'],
|
|
179
|
+
},
|
|
180
|
+
};
|
|
139
181
|
}
|
|
140
182
|
};
|
|
141
183
|
exports.getFrameworkDefinition = getFrameworkDefinition;
|
package/dist/constants/skills.js
CHANGED
|
@@ -54,4 +54,37 @@ exports.SKILL_DETECTION_REGISTRY = {
|
|
|
54
54
|
packages: ['@nestjs/passport', 'passport', 'helmet'],
|
|
55
55
|
},
|
|
56
56
|
],
|
|
57
|
+
[enums_1.Framework.Android]: [
|
|
58
|
+
{
|
|
59
|
+
id: 'compose',
|
|
60
|
+
packages: ['androidx.compose.ui'],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'navigation',
|
|
64
|
+
packages: ['androidx.navigation:navigation-compose'],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'legacy-navigation',
|
|
68
|
+
packages: [
|
|
69
|
+
'androidx.navigation:navigation-fragment',
|
|
70
|
+
'androidx.navigation:navigation-ui',
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'di',
|
|
75
|
+
packages: ['hilt-android', 'dagger-android'],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'persistence',
|
|
79
|
+
packages: ['androidx.room:room-runtime'],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'networking',
|
|
83
|
+
packages: ['retrofit'],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'concurrency',
|
|
87
|
+
packages: ['kotlinx-coroutines-android'],
|
|
88
|
+
},
|
|
89
|
+
],
|
|
57
90
|
};
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ const program = new commander_1.Command();
|
|
|
10
10
|
program
|
|
11
11
|
.name('agent-skills-standard')
|
|
12
12
|
.description('A CLI to manage and sync AI agent skills for Cursor, Claude, Copilot, Windsurf, and more.')
|
|
13
|
-
.version('1.
|
|
13
|
+
.version('1.4.0');
|
|
14
14
|
program
|
|
15
15
|
.command('init')
|
|
16
16
|
.description('Initialize a .skillsrc configuration file interactively')
|
|
@@ -5,4 +5,9 @@ export declare class ConfigService {
|
|
|
5
5
|
saveConfig(config: SkillConfig, cwd?: string): Promise<void>;
|
|
6
6
|
buildInitialConfig(framework: string, agents: string[], registry: string, metadata: Partial<RegistryMetadata>, languages?: string[]): SkillConfig;
|
|
7
7
|
applyDependencyExclusions(config: SkillConfig, framework: string, projectDeps: Set<string>): void;
|
|
8
|
+
/**
|
|
9
|
+
* Automatically re-enables skills that were previously excluded if their dependencies
|
|
10
|
+
* are now present in the project.
|
|
11
|
+
*/
|
|
12
|
+
reconcileDependencies(config: SkillConfig, framework: string, projectDeps: Set<string>): string[];
|
|
8
13
|
}
|
|
@@ -91,5 +91,33 @@ class ConfigService {
|
|
|
91
91
|
category.exclude = Array.from(exclusions);
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Automatically re-enables skills that were previously excluded if their dependencies
|
|
96
|
+
* are now present in the project.
|
|
97
|
+
*/
|
|
98
|
+
reconcileDependencies(config, framework, projectDeps) {
|
|
99
|
+
const category = config.skills[framework];
|
|
100
|
+
if (!category || !category.exclude)
|
|
101
|
+
return [];
|
|
102
|
+
const reenabled = [];
|
|
103
|
+
const detections = constants_1.SKILL_DETECTION_REGISTRY[framework] || [];
|
|
104
|
+
const currentExclusions = new Set(category.exclude);
|
|
105
|
+
for (const detection of detections) {
|
|
106
|
+
if (currentExclusions.has(detection.id)) {
|
|
107
|
+
const hasDep = Array.from(projectDeps).some((d) => detection.packages.some((pkg) => d.toLowerCase().includes(pkg.toLowerCase())));
|
|
108
|
+
if (hasDep) {
|
|
109
|
+
currentExclusions.delete(detection.id);
|
|
110
|
+
reenabled.push(detection.id);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (reenabled.length > 0) {
|
|
115
|
+
category.exclude = Array.from(currentExclusions);
|
|
116
|
+
if (category.exclude.length === 0) {
|
|
117
|
+
delete category.exclude;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return reenabled;
|
|
121
|
+
}
|
|
94
122
|
}
|
|
95
123
|
exports.ConfigService = ConfigService;
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
import { FrameworkDefinition } from '../constants';
|
|
1
2
|
export declare class DetectionService {
|
|
2
3
|
detectFrameworks(): Promise<Record<string, boolean>>;
|
|
4
|
+
detectLanguages(framework: FrameworkDefinition): Promise<string[]>;
|
|
3
5
|
detectAgents(): Promise<Record<string, boolean>>;
|
|
4
6
|
getProjectDeps(): Promise<Set<string>>;
|
|
7
|
+
private parsePackageJson;
|
|
8
|
+
private parsePubspecYaml;
|
|
9
|
+
private parseGradleDependencies;
|
|
10
|
+
private parseVersionCatalogs;
|
|
11
|
+
private parseMavenPom;
|
|
5
12
|
private getPackageDeps;
|
|
6
13
|
}
|
|
@@ -30,6 +30,23 @@ class DetectionService {
|
|
|
30
30
|
}
|
|
31
31
|
return results;
|
|
32
32
|
}
|
|
33
|
+
async detectLanguages(framework) {
|
|
34
|
+
if (!framework.languageDetection) {
|
|
35
|
+
return framework.languages;
|
|
36
|
+
}
|
|
37
|
+
const detectedLanguages = [];
|
|
38
|
+
for (const [lang, files] of Object.entries(framework.languageDetection)) {
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
if (await fs_extra_1.default.pathExists(path_1.default.join(process.cwd(), file))) {
|
|
41
|
+
detectedLanguages.push(lang);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return detectedLanguages.length > 0
|
|
47
|
+
? detectedLanguages
|
|
48
|
+
: framework.languages;
|
|
49
|
+
}
|
|
33
50
|
async detectAgents() {
|
|
34
51
|
const results = {};
|
|
35
52
|
for (const agent of constants_1.SUPPORTED_AGENTS) {
|
|
@@ -45,54 +62,148 @@ class DetectionService {
|
|
|
45
62
|
return results;
|
|
46
63
|
}
|
|
47
64
|
async getProjectDeps() {
|
|
65
|
+
const cwd = process.cwd();
|
|
66
|
+
const results = await Promise.all([
|
|
67
|
+
this.parsePackageJson(cwd),
|
|
68
|
+
this.parsePubspecYaml(cwd),
|
|
69
|
+
this.parseGradleDependencies(cwd),
|
|
70
|
+
this.parseVersionCatalogs(cwd),
|
|
71
|
+
this.parseMavenPom(cwd),
|
|
72
|
+
]);
|
|
73
|
+
const combined = new Set();
|
|
74
|
+
for (const set of results) {
|
|
75
|
+
for (const item of set)
|
|
76
|
+
combined.add(item);
|
|
77
|
+
}
|
|
78
|
+
return combined;
|
|
79
|
+
}
|
|
80
|
+
async parsePackageJson(cwd) {
|
|
48
81
|
const set = new Set();
|
|
49
|
-
const packageJsonPath = path_1.default.join(
|
|
50
|
-
if (await fs_extra_1.default.pathExists(packageJsonPath))
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
82
|
+
const packageJsonPath = path_1.default.join(cwd, 'package.json');
|
|
83
|
+
if (!(await fs_extra_1.default.pathExists(packageJsonPath)))
|
|
84
|
+
return set;
|
|
85
|
+
try {
|
|
86
|
+
const pkg = await fs_extra_1.default.readJson(packageJsonPath);
|
|
87
|
+
const deps = {
|
|
88
|
+
...(pkg.dependencies || {}),
|
|
89
|
+
...(pkg.devDependencies || {}),
|
|
90
|
+
};
|
|
91
|
+
Object.keys(deps).forEach((k) => set.add(k));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
/* ignore */
|
|
95
|
+
}
|
|
96
|
+
return set;
|
|
97
|
+
}
|
|
98
|
+
async parsePubspecYaml(cwd) {
|
|
99
|
+
const set = new Set();
|
|
100
|
+
const pubspecPath = path_1.default.join(cwd, 'pubspec.yaml');
|
|
101
|
+
if (!(await fs_extra_1.default.pathExists(pubspecPath)))
|
|
102
|
+
return set;
|
|
103
|
+
try {
|
|
104
|
+
const content = await fs_extra_1.default.readFile(pubspecPath, 'utf8');
|
|
105
|
+
const lines = content.split(/\r?\n/);
|
|
106
|
+
let currentSection = null;
|
|
107
|
+
let sectionIndent = null;
|
|
108
|
+
for (const line of lines) {
|
|
109
|
+
const sectionMatch = line.match(/^(\s*)(dependencies|dev_dependencies)\s*:/);
|
|
110
|
+
if (sectionMatch) {
|
|
111
|
+
currentSection = sectionMatch[2];
|
|
112
|
+
sectionIndent = sectionMatch[1].length;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (!currentSection || sectionIndent === null)
|
|
116
|
+
continue;
|
|
117
|
+
const entryMatch = line.match(/^(\s*)([a-zA-Z0-9_\-@\\/]+)\s*:/);
|
|
118
|
+
if (!entryMatch)
|
|
119
|
+
continue;
|
|
120
|
+
const indent = entryMatch[1].length;
|
|
121
|
+
const key = entryMatch[2];
|
|
122
|
+
if (indent > sectionIndent && key !== 'sdk' && key !== 'flutter') {
|
|
123
|
+
set.add(key);
|
|
124
|
+
}
|
|
62
125
|
}
|
|
63
126
|
}
|
|
64
|
-
|
|
65
|
-
|
|
127
|
+
catch {
|
|
128
|
+
/* ignore */
|
|
129
|
+
}
|
|
130
|
+
return set;
|
|
131
|
+
}
|
|
132
|
+
async parseGradleDependencies(cwd) {
|
|
133
|
+
const set = new Set();
|
|
134
|
+
const gradleRegex = /(?:implementation|api|ksp|kapt|annotationProcessor|compileOnly|runtimeOnly)\s*\(?\s*['"]([^'":\s]+)(?::[^'"]*)?['"]\s*\)?/g;
|
|
135
|
+
const scanDir = async (dir, depth) => {
|
|
136
|
+
if (depth > 3)
|
|
137
|
+
return;
|
|
66
138
|
try {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (sectionMatch) {
|
|
75
|
-
currentSection = sectionMatch[2];
|
|
76
|
-
sectionIndent = sectionMatch[1].length;
|
|
77
|
-
continue;
|
|
139
|
+
const entries = await fs_extra_1.default.readdir(dir, { withFileTypes: true });
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
142
|
+
if (entry.isDirectory()) {
|
|
143
|
+
if (['node_modules', '.git', 'build', '.gradle'].includes(entry.name))
|
|
144
|
+
continue;
|
|
145
|
+
await scanDir(fullPath, depth + 1);
|
|
78
146
|
}
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// only include keys that are nested under the current section and are not known non-packages
|
|
87
|
-
if (indent > sectionIndent && key !== 'sdk' && key !== 'flutter') {
|
|
88
|
-
set.add(key);
|
|
147
|
+
else if (entry.name === 'build.gradle' ||
|
|
148
|
+
entry.name === 'build.gradle.kts') {
|
|
149
|
+
const content = await fs_extra_1.default.readFile(fullPath, 'utf8');
|
|
150
|
+
let match;
|
|
151
|
+
while ((match = gradleRegex.exec(content)) !== null) {
|
|
152
|
+
set.add(match[1]);
|
|
153
|
+
}
|
|
89
154
|
}
|
|
90
155
|
}
|
|
91
156
|
}
|
|
92
157
|
catch {
|
|
93
|
-
|
|
158
|
+
/* ignore */
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
await scanDir(cwd, 0);
|
|
162
|
+
return set;
|
|
163
|
+
}
|
|
164
|
+
async parseVersionCatalogs(cwd) {
|
|
165
|
+
const set = new Set();
|
|
166
|
+
const tomlPaths = [
|
|
167
|
+
path_1.default.join(cwd, 'gradle', 'libs.versions.toml'),
|
|
168
|
+
path_1.default.join(cwd, 'gradle.libs.versions.toml'),
|
|
169
|
+
];
|
|
170
|
+
for (const tomlPath of tomlPaths) {
|
|
171
|
+
if (!(await fs_extra_1.default.pathExists(tomlPath)))
|
|
172
|
+
continue;
|
|
173
|
+
try {
|
|
174
|
+
const content = await fs_extra_1.default.readFile(tomlPath, 'utf8');
|
|
175
|
+
const moduleRegex = /module\s*=\s*['"]([^'":\s]+)(?::[^'"]*)?['"]/g;
|
|
176
|
+
const groupRegex = /group\s*=\s*['"]([^'":\s]+)['"]/g;
|
|
177
|
+
let match;
|
|
178
|
+
while ((match = moduleRegex.exec(content)) !== null) {
|
|
179
|
+
set.add(match[1]);
|
|
180
|
+
}
|
|
181
|
+
while ((match = groupRegex.exec(content)) !== null) {
|
|
182
|
+
set.add(match[1]);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
/* ignore */
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return set;
|
|
190
|
+
}
|
|
191
|
+
async parseMavenPom(cwd) {
|
|
192
|
+
const set = new Set();
|
|
193
|
+
const pomPath = path_1.default.join(cwd, 'pom.xml');
|
|
194
|
+
if (!(await fs_extra_1.default.pathExists(pomPath)))
|
|
195
|
+
return set;
|
|
196
|
+
try {
|
|
197
|
+
const content = await fs_extra_1.default.readFile(pomPath, 'utf8');
|
|
198
|
+
const regex = /<dependency>[\s\S]*?<artifactId>([^<]+)<\/artifactId>[\s\S]*?<\/dependency>/g;
|
|
199
|
+
let match;
|
|
200
|
+
while ((match = regex.exec(content)) !== null) {
|
|
201
|
+
set.add(match[1]);
|
|
94
202
|
}
|
|
95
203
|
}
|
|
204
|
+
catch {
|
|
205
|
+
/* ignore */
|
|
206
|
+
}
|
|
96
207
|
return set;
|
|
97
208
|
}
|
|
98
209
|
// kept for internal usage if needed, or can be replaced by getProjectDeps
|
|
@@ -210,4 +210,62 @@ vitest_1.vi.mock('js-yaml');
|
|
|
210
210
|
});
|
|
211
211
|
});
|
|
212
212
|
});
|
|
213
|
+
(0, vitest_1.describe)('reconcileDependencies', () => {
|
|
214
|
+
(0, vitest_1.it)('should re-enable skills if dependencies are found', () => {
|
|
215
|
+
const config = {
|
|
216
|
+
registry: 'https://example.com',
|
|
217
|
+
agents: ['cursor'],
|
|
218
|
+
skills: {
|
|
219
|
+
android: {
|
|
220
|
+
ref: 'v1.0.0',
|
|
221
|
+
exclude: ['networking', 'persistence'],
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
custom_overrides: [],
|
|
225
|
+
};
|
|
226
|
+
// networking is detected by 'retrofit'
|
|
227
|
+
// persistence is detected by 'androidx.room:room-runtime'
|
|
228
|
+
const projectDeps = new Set(['retrofit', 'some-other-dep']);
|
|
229
|
+
const reenabled = configService.reconcileDependencies(config, 'android', projectDeps);
|
|
230
|
+
(0, vitest_1.expect)(reenabled).toEqual(['networking']);
|
|
231
|
+
const category = config.skills.android;
|
|
232
|
+
(0, vitest_1.expect)(category.exclude).toEqual(['persistence']);
|
|
233
|
+
});
|
|
234
|
+
(0, vitest_1.it)('should remove exclude key if all skills are re-enabled', () => {
|
|
235
|
+
const config = {
|
|
236
|
+
registry: 'https://example.com',
|
|
237
|
+
agents: ['cursor'],
|
|
238
|
+
skills: {
|
|
239
|
+
android: {
|
|
240
|
+
ref: 'v1.0.0',
|
|
241
|
+
exclude: ['networking'],
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
custom_overrides: [],
|
|
245
|
+
};
|
|
246
|
+
const projectDeps = new Set(['retrofit']);
|
|
247
|
+
const reenabled = configService.reconcileDependencies(config, 'android', projectDeps);
|
|
248
|
+
(0, vitest_1.expect)(reenabled).toEqual(['networking']);
|
|
249
|
+
const category = config.skills.android;
|
|
250
|
+
(0, vitest_1.expect)(category.exclude).toBeUndefined();
|
|
251
|
+
});
|
|
252
|
+
(0, vitest_1.it)('should return empty if no skills are re-enabled', () => {
|
|
253
|
+
const config = {
|
|
254
|
+
registry: 'https://example.com',
|
|
255
|
+
agents: ['cursor'],
|
|
256
|
+
skills: {
|
|
257
|
+
android: {
|
|
258
|
+
ref: 'v1.0.0',
|
|
259
|
+
exclude: ['persistence'],
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
custom_overrides: [],
|
|
263
|
+
};
|
|
264
|
+
const projectDeps = new Set(['some-other-dep']);
|
|
265
|
+
const reenabled = configService.reconcileDependencies(config, 'android', projectDeps);
|
|
266
|
+
(0, vitest_1.expect)(reenabled).toEqual([]);
|
|
267
|
+
const category = config.skills.android;
|
|
268
|
+
(0, vitest_1.expect)(category.exclude).toEqual(['persistence']);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
213
271
|
});
|
|
@@ -118,5 +118,207 @@ dev_dependencies:
|
|
|
118
118
|
const deps = await detectionService.getProjectDeps();
|
|
119
119
|
(0, vitest_1.expect)(deps.size).toBe(0);
|
|
120
120
|
});
|
|
121
|
+
(0, vitest_1.it)('should collect dependencies from build.gradle', async () => {
|
|
122
|
+
const mockGradle = `
|
|
123
|
+
dependencies {
|
|
124
|
+
implementation 'androidx.compose.ui:ui:1.5.0'
|
|
125
|
+
api "androidx.navigation:navigation-compose:2.7.0"
|
|
126
|
+
}
|
|
127
|
+
`;
|
|
128
|
+
vitest_1.vi.mocked(fs_extra_1.default.readdir).mockImplementation((dir) => {
|
|
129
|
+
if (dir === process.cwd()) {
|
|
130
|
+
return Promise.resolve([
|
|
131
|
+
{ name: 'build.gradle', isDirectory: () => false },
|
|
132
|
+
]);
|
|
133
|
+
}
|
|
134
|
+
return Promise.resolve([]);
|
|
135
|
+
});
|
|
136
|
+
vitest_1.vi.mocked(fs_extra_1.default.readFile).mockImplementation((path) => {
|
|
137
|
+
if (path.endsWith('build.gradle')) {
|
|
138
|
+
return Promise.resolve(mockGradle);
|
|
139
|
+
}
|
|
140
|
+
return Promise.resolve('');
|
|
141
|
+
});
|
|
142
|
+
const deps = await detectionService.getProjectDeps();
|
|
143
|
+
(0, vitest_1.expect)(deps.has('androidx.compose.ui')).toBe(true);
|
|
144
|
+
(0, vitest_1.expect)(deps.has('androidx.navigation')).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
(0, vitest_1.it)('should collect dependencies from build.gradle.kts', async () => {
|
|
147
|
+
const mockGradleKts = `
|
|
148
|
+
dependencies {
|
|
149
|
+
implementation("androidx.room:room-runtime:2.5.0")
|
|
150
|
+
ksp("androidx.room:room-compiler:2.5.0")
|
|
151
|
+
}
|
|
152
|
+
`;
|
|
153
|
+
vitest_1.vi.mocked(fs_extra_1.default.readdir).mockImplementation((dir) => {
|
|
154
|
+
if (dir === process.cwd()) {
|
|
155
|
+
return Promise.resolve([
|
|
156
|
+
{ name: 'build.gradle.kts', isDirectory: () => false },
|
|
157
|
+
]);
|
|
158
|
+
}
|
|
159
|
+
return Promise.resolve([]);
|
|
160
|
+
});
|
|
161
|
+
vitest_1.vi.mocked(fs_extra_1.default.readFile).mockImplementation((path) => {
|
|
162
|
+
if (path.endsWith('build.gradle.kts')) {
|
|
163
|
+
return Promise.resolve(mockGradleKts);
|
|
164
|
+
}
|
|
165
|
+
return Promise.resolve('');
|
|
166
|
+
});
|
|
167
|
+
const deps = await detectionService.getProjectDeps();
|
|
168
|
+
(0, vitest_1.expect)(deps.has('androidx.room')).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
(0, vitest_1.it)('should collect dependencies from recursive build.gradle files (multi-module)', async () => {
|
|
171
|
+
const mockRootGradle = `// Empty root`;
|
|
172
|
+
const mockAppGradle = `dependencies { implementation 'androidx.compose.ui:ui:1.5.0' }`;
|
|
173
|
+
vitest_1.vi.mocked(fs_extra_1.default.readdir).mockImplementation((dir) => {
|
|
174
|
+
if (dir === process.cwd()) {
|
|
175
|
+
return Promise.resolve([
|
|
176
|
+
{ name: 'build.gradle', isDirectory: () => false },
|
|
177
|
+
{ name: 'app', isDirectory: () => true },
|
|
178
|
+
]);
|
|
179
|
+
}
|
|
180
|
+
if (dir.endsWith('app')) {
|
|
181
|
+
return Promise.resolve([
|
|
182
|
+
{ name: 'build.gradle', isDirectory: () => false },
|
|
183
|
+
]);
|
|
184
|
+
}
|
|
185
|
+
return Promise.resolve([]);
|
|
186
|
+
});
|
|
187
|
+
vitest_1.vi.mocked(fs_extra_1.default.readFile).mockImplementation((path) => {
|
|
188
|
+
if (path.endsWith('app/build.gradle')) {
|
|
189
|
+
return Promise.resolve(mockAppGradle);
|
|
190
|
+
}
|
|
191
|
+
return Promise.resolve(mockRootGradle);
|
|
192
|
+
});
|
|
193
|
+
const deps = await detectionService.getProjectDeps();
|
|
194
|
+
(0, vitest_1.expect)(deps.has('androidx.compose.ui')).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
(0, vitest_1.it)('should collect dependencies from Version Catalogs (libs.versions.toml)', async () => {
|
|
197
|
+
const mockToml = `
|
|
198
|
+
[libraries]
|
|
199
|
+
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
|
200
|
+
room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
|
|
201
|
+
`;
|
|
202
|
+
vitest_1.vi.mocked(fs_extra_1.default.pathExists).mockImplementation((p) => {
|
|
203
|
+
return Promise.resolve(p.endsWith('libs.versions.toml'));
|
|
204
|
+
});
|
|
205
|
+
vitest_1.vi.mocked(fs_extra_1.default.readFile).mockImplementation(() => Promise.resolve(mockToml));
|
|
206
|
+
const deps = await detectionService.getProjectDeps();
|
|
207
|
+
(0, vitest_1.expect)(deps.has('com.squareup.retrofit2')).toBe(true);
|
|
208
|
+
(0, vitest_1.expect)(deps.has('androidx.room')).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
(0, vitest_1.it)('should collect dependencies from pom.xml', async () => {
|
|
211
|
+
const mockPom = `
|
|
212
|
+
<project>
|
|
213
|
+
<dependencies>
|
|
214
|
+
<dependency>
|
|
215
|
+
<groupId>org.springframework.boot</groupId>
|
|
216
|
+
<artifactId>spring-boot-starter-web</artifactId>
|
|
217
|
+
</dependency>
|
|
218
|
+
</dependencies>
|
|
219
|
+
</project>
|
|
220
|
+
`;
|
|
221
|
+
vitest_1.vi.mocked(fs_extra_1.default.pathExists).mockImplementation((p) => {
|
|
222
|
+
return Promise.resolve(p.endsWith('pom.xml'));
|
|
223
|
+
});
|
|
224
|
+
vitest_1.vi.mocked(fs_extra_1.default.readFile).mockImplementation(() => Promise.resolve(mockPom));
|
|
225
|
+
const deps = await detectionService.getProjectDeps();
|
|
226
|
+
(0, vitest_1.expect)(deps.has('spring-boot-starter-web')).toBe(true);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
(0, vitest_1.describe)('detectLanguages', () => {
|
|
230
|
+
(0, vitest_1.it)('should return default languages if no detection rules defined', async () => {
|
|
231
|
+
const framework = {
|
|
232
|
+
id: 'golang',
|
|
233
|
+
name: 'Go',
|
|
234
|
+
languages: ['go'],
|
|
235
|
+
detectionFiles: ['go.mod'],
|
|
236
|
+
};
|
|
237
|
+
const langs = await detectionService.detectLanguages(framework);
|
|
238
|
+
(0, vitest_1.expect)(langs).toEqual(['go']);
|
|
239
|
+
});
|
|
240
|
+
(0, vitest_1.it)('should detect Kotlin for Spring Boot if src/main/kotlin exists', async () => {
|
|
241
|
+
const framework = {
|
|
242
|
+
id: 'spring-boot',
|
|
243
|
+
name: 'Spring Boot',
|
|
244
|
+
languages: ['java', 'kotlin'],
|
|
245
|
+
detectionFiles: ['pom.xml'],
|
|
246
|
+
languageDetection: {
|
|
247
|
+
kotlin: ['src/main/kotlin'],
|
|
248
|
+
java: ['src/main/java'],
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
vitest_1.vi.mocked(fs_extra_1.default.pathExists).mockImplementation((p) => {
|
|
252
|
+
return Promise.resolve(p.endsWith('src/main/kotlin'));
|
|
253
|
+
});
|
|
254
|
+
const langs = await detectionService.detectLanguages(framework);
|
|
255
|
+
(0, vitest_1.expect)(langs).toEqual(['kotlin']);
|
|
256
|
+
});
|
|
257
|
+
(0, vitest_1.it)('should detect Java for Spring Boot if src/main/java exists', async () => {
|
|
258
|
+
const framework = {
|
|
259
|
+
id: 'spring-boot',
|
|
260
|
+
name: 'Spring Boot',
|
|
261
|
+
languages: ['java', 'kotlin'],
|
|
262
|
+
detectionFiles: ['pom.xml'],
|
|
263
|
+
languageDetection: {
|
|
264
|
+
kotlin: ['src/main/kotlin'],
|
|
265
|
+
java: ['src/main/java'],
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
vitest_1.vi.mocked(fs_extra_1.default.pathExists).mockImplementation((p) => {
|
|
269
|
+
return Promise.resolve(p.endsWith('src/main/java'));
|
|
270
|
+
});
|
|
271
|
+
const langs = await detectionService.detectLanguages(framework);
|
|
272
|
+
(0, vitest_1.expect)(langs).toEqual(['java']);
|
|
273
|
+
});
|
|
274
|
+
(0, vitest_1.it)('should return all default languages if no language-specific files found', async () => {
|
|
275
|
+
const framework = {
|
|
276
|
+
id: 'spring-boot',
|
|
277
|
+
name: 'Spring Boot',
|
|
278
|
+
languages: ['java', 'kotlin'],
|
|
279
|
+
detectionFiles: ['pom.xml'],
|
|
280
|
+
languageDetection: {
|
|
281
|
+
kotlin: ['src/main/kotlin'],
|
|
282
|
+
java: ['src/main/java'],
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
vitest_1.vi.mocked(fs_extra_1.default.pathExists).mockResolvedValue();
|
|
286
|
+
const langs = await detectionService.detectLanguages(framework);
|
|
287
|
+
(0, vitest_1.expect)(langs).toEqual(['java', 'kotlin']);
|
|
288
|
+
});
|
|
289
|
+
(0, vitest_1.it)('should detect TypeScript for NestJS if tsconfig.json exists', async () => {
|
|
290
|
+
const framework = {
|
|
291
|
+
id: 'nestjs',
|
|
292
|
+
name: 'NestJS',
|
|
293
|
+
languages: ['typescript', 'javascript'],
|
|
294
|
+
detectionFiles: ['nest-cli.json'],
|
|
295
|
+
languageDetection: {
|
|
296
|
+
typescript: ['tsconfig.json'],
|
|
297
|
+
javascript: ['jsconfig.json'],
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
vitest_1.vi.mocked(fs_extra_1.default.pathExists).mockImplementation((p) => {
|
|
301
|
+
return Promise.resolve(p.endsWith('tsconfig.json'));
|
|
302
|
+
});
|
|
303
|
+
const langs = await detectionService.detectLanguages(framework);
|
|
304
|
+
(0, vitest_1.expect)(langs).toEqual(['typescript']);
|
|
305
|
+
});
|
|
306
|
+
(0, vitest_1.it)('should detect JavaScript for NestJS if jsconfig.json exists', async () => {
|
|
307
|
+
const framework = {
|
|
308
|
+
id: 'nestjs',
|
|
309
|
+
name: 'NestJS',
|
|
310
|
+
languages: ['typescript', 'javascript'],
|
|
311
|
+
detectionFiles: ['nest-cli.json'],
|
|
312
|
+
languageDetection: {
|
|
313
|
+
typescript: ['tsconfig.json'],
|
|
314
|
+
javascript: ['jsconfig.json'],
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
vitest_1.vi.mocked(fs_extra_1.default.pathExists).mockImplementation((p) => {
|
|
318
|
+
return Promise.resolve(p.endsWith('jsconfig.json'));
|
|
319
|
+
});
|
|
320
|
+
const langs = await detectionService.detectLanguages(framework);
|
|
321
|
+
(0, vitest_1.expect)(langs).toEqual(['javascript']);
|
|
322
|
+
});
|
|
121
323
|
});
|
|
122
324
|
});
|