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 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**: (Coming Soon)
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
 
@@ -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 config = this.configService.buildInitialConfig(frameworkId, answers.agents, answers.registry, metadata, frameworkDef?.languages || []);
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
@@ -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}...`));
@@ -17,5 +17,7 @@ export declare enum Framework {
17
17
  NextJS = "nextjs",
18
18
  React = "react",
19
19
  ReactNative = "react-native",
20
- Angular = "angular"
20
+ Angular = "angular",
21
+ SpringBoot = "spring-boot",
22
+ Android = "android"
21
23
  }
@@ -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;
@@ -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;
@@ -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.3.2');
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(process.cwd(), 'package.json');
50
- if (await fs_extra_1.default.pathExists(packageJsonPath)) {
51
- try {
52
- const pkg = await fs_extra_1.default.readJson(packageJsonPath);
53
- const deps = {
54
- ...(pkg.dependencies || {}),
55
- ...(pkg.devDependencies || {}),
56
- };
57
- for (const k of Object.keys(deps))
58
- set.add(k);
59
- }
60
- catch {
61
- // ignore
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
- const pubspecPath = path_1.default.join(process.cwd(), 'pubspec.yaml');
65
- if (await fs_extra_1.default.pathExists(pubspecPath)) {
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 loaded = await fs_extra_1.default.readFile(pubspecPath, 'utf8');
68
- // simple parse: only collect keys inside dependencies/dev_dependencies sections
69
- const lines = loaded.split(/\r?\n/);
70
- let currentSection = null;
71
- let sectionIndent = null;
72
- for (const line of lines) {
73
- const sectionMatch = line.match(/^(\s*)(dependencies|dev_dependencies)\s*:/);
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 (!currentSection || sectionIndent === null)
80
- continue;
81
- const entryMatch = line.match(/^(\s*)([a-zA-Z0-9_\-@\\/]+)\s*:/);
82
- if (!entryMatch)
83
- continue;
84
- const indent = entryMatch[1].length;
85
- const key = entryMatch[2];
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
- // ignore
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-skills-standard",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "A CLI to manage and sync AI agent skills standard for Cursor, Claude, Copilot, Windsurf, and more.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {