agent-skills-standard 1.2.0 → 1.3.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.
@@ -13,7 +13,6 @@ const constants_1 = require("../constants");
13
13
  const ConfigService_1 = require("../services/ConfigService");
14
14
  const DetectionService_1 = require("../services/DetectionService");
15
15
  const RegistryService_1 = require("../services/RegistryService");
16
- const project_1 = require("../utils/project");
17
16
  class InitCommand {
18
17
  constructor() {
19
18
  this.detectionService = new DetectionService_1.DetectionService();
@@ -80,7 +79,7 @@ class InitCommand {
80
79
  console.log(picocolors_1.default.gray('The CLI will still generate rule files with global standards.\n'));
81
80
  }
82
81
  const config = this.configService.buildInitialConfig(framework, answers.agents, answers.registry, metadata);
83
- const projectDeps = await (0, project_1.buildProjectDeps)();
82
+ const projectDeps = await this.detectionService.getProjectDeps();
84
83
  this.configService.applyDependencyExclusions(config, framework, projectDeps);
85
84
  const commentHeader = `# Auto-detected configuration generated by agent-skills-standard init
86
85
  #
@@ -1,3 +1,6 @@
1
1
  export declare class ListSkillsCommand {
2
+ private detectionService;
3
+ private githubService;
2
4
  run(): Promise<void>;
5
+ private parseRegistryUrl;
3
6
  }
@@ -10,9 +10,13 @@ const js_yaml_1 = __importDefault(require("js-yaml"));
10
10
  const path_1 = __importDefault(require("path"));
11
11
  const picocolors_1 = __importDefault(require("picocolors"));
12
12
  const constants_1 = require("../constants");
13
- const github_1 = require("../utils/github");
14
- const project_1 = require("../utils/project");
13
+ const DetectionService_1 = require("../services/DetectionService");
14
+ const GithubService_1 = require("../services/GithubService");
15
15
  class ListSkillsCommand {
16
+ constructor() {
17
+ this.detectionService = new DetectionService_1.DetectionService();
18
+ this.githubService = new GithubService_1.GithubService(process.env.GITHUB_TOKEN);
19
+ }
16
20
  async run() {
17
21
  const choices = constants_1.SUPPORTED_FRAMEWORKS.map((f) => ({
18
22
  name: f.name,
@@ -27,7 +31,7 @@ class ListSkillsCommand {
27
31
  default: choices.find((c) => c.value === constants_1.Framework.Flutter)?.value,
28
32
  },
29
33
  ]);
30
- const projectDeps = await (0, project_1.buildProjectDeps)();
34
+ const projectDeps = await this.detectionService.getProjectDeps();
31
35
  console.log(picocolors_1.default.green(`\nAvailable skills for ${framework}:`));
32
36
  // Determine registry URL (from .skillsrc or default)
33
37
  const defaultRegistry = 'https://github.com/HoangNguyen0403/agent-skills-standard';
@@ -42,11 +46,14 @@ class ListSkillsCommand {
42
46
  // Try to fetch the repo tree to list all skills under skills/<framework>/
43
47
  let skillFolders = [];
44
48
  try {
45
- const parsed = (0, github_1.parseRegistryUrl)(registryUrl);
49
+ const parsed = this.parseRegistryUrl(registryUrl);
46
50
  if (parsed) {
47
- const treeResult = await (0, github_1.fetchRepoTree)(parsed.owner, parsed.repo);
48
- if (treeResult && Array.isArray(treeResult.data.tree)) {
49
- const allFiles = treeResult.data.tree || [];
51
+ // We assume 'main' branch if not specified (legacy behavior of fetchRepoTree handled default,
52
+ // but for listing skills, 'main' is a safe default for now or we could fetch repo info)
53
+ // I'll stick to 'main' to keep it simple as in RegistryService refactor.
54
+ const treeResult = await this.githubService.getRepoTree(parsed.owner, parsed.repo, 'main');
55
+ if (treeResult && Array.isArray(treeResult.tree)) {
56
+ const allFiles = treeResult.tree || [];
50
57
  skillFolders = Array.from(new Set(allFiles
51
58
  .filter((f) => f.path.startsWith(`skills/${framework}/`))
52
59
  .map((f) => {
@@ -77,5 +84,11 @@ class ListSkillsCommand {
77
84
  }
78
85
  console.log('\nTip: Use the .skillsrc exclude array to disable/enable sub-skills before running sync.');
79
86
  }
87
+ parseRegistryUrl(registryUrl) {
88
+ const m = registryUrl.match(/github\.com\/([^/]+)\/([^/]+)/i);
89
+ if (!m)
90
+ return null;
91
+ return { owner: m[1], repo: m[2].replace(/\.git$/, '') };
92
+ }
80
93
  }
81
94
  exports.ListSkillsCommand = ListSkillsCommand;
@@ -1,6 +1,7 @@
1
1
  export declare class SyncCommand {
2
+ private configService;
3
+ private githubService;
2
4
  run(): Promise<void>;
3
- private checkCLIUpdate;
4
5
  private checkForUpdates;
5
6
  private assembleFromRemote;
6
7
  private writeToTargets;
@@ -5,203 +5,115 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.SyncCommand = void 0;
7
7
  const fs_extra_1 = __importDefault(require("fs-extra"));
8
- const inquirer_1 = __importDefault(require("inquirer"));
9
- const js_yaml_1 = __importDefault(require("js-yaml"));
10
- const node_fetch_1 = __importDefault(require("node-fetch"));
11
8
  const path_1 = __importDefault(require("path"));
12
9
  const picocolors_1 = __importDefault(require("picocolors"));
13
10
  const constants_1 = require("../constants");
11
+ const ConfigService_1 = require("../services/ConfigService");
12
+ const GithubService_1 = require("../services/GithubService");
14
13
  class SyncCommand {
14
+ constructor() {
15
+ this.configService = new ConfigService_1.ConfigService();
16
+ this.githubService = new GithubService_1.GithubService(process.env.GITHUB_TOKEN);
17
+ }
15
18
  async run() {
16
- const configPath = path_1.default.join(process.cwd(), '.skillsrc');
17
- if (!(await fs_extra_1.default.pathExists(configPath))) {
18
- console.log(picocolors_1.default.red('❌ Error: .skillsrc not found in current directory.'));
19
- console.log(picocolors_1.default.yellow('Run `agent-skills-standard init` first.'));
20
- return;
21
- }
22
19
  try {
23
- const fileContent = await fs_extra_1.default.readFile(configPath, 'utf8');
24
- let config = js_yaml_1.default.load(fileContent);
25
- // 1. Check for CLI updates
26
- await this.checkCLIUpdate();
27
- // 2. Check for skill updates before syncing
28
- config = await this.checkForUpdates(config, configPath);
20
+ // 1. Load Config
21
+ let config = await this.configService.loadConfig();
22
+ if (!config) {
23
+ console.log(picocolors_1.default.red('❌ Error: .skillsrc not found. Run `init` first.'));
24
+ return;
25
+ }
26
+ // 2. Check for updates
27
+ config = await this.checkForUpdates(config);
29
28
  console.log(picocolors_1.default.cyan(`🚀 Syncing skills from ${config.registry}...`));
29
+ // 3. Assemble skills (concurrently)
30
30
  const enabledCategories = Object.keys(config.skills);
31
31
  const skills = await this.assembleFromRemote(enabledCategories, config);
32
+ // 4. Write to disk
32
33
  await this.writeToTargets(skills, config);
33
34
  console.log(picocolors_1.default.green('✅ All skills synced successfully!'));
34
35
  }
35
36
  catch (error) {
36
- console.error(picocolors_1.default.red('❌ Failed to sync skills:'), error);
37
- }
38
- }
39
- async checkCLIUpdate() {
40
- try {
41
- const pkgPath = path_1.default.join(__dirname, '../../package.json');
42
- if (!fs_extra_1.default.existsSync(pkgPath))
43
- return;
44
- const pkg = await fs_extra_1.default.readJson(pkgPath);
45
- const currentVersion = pkg.version;
46
- const res = await (0, node_fetch_1.default)('https://registry.npmjs.org/agent-skills-standard/latest');
47
- if (res.ok) {
48
- const remoteData = (await res.json());
49
- const latestVersion = remoteData.version;
50
- if (currentVersion !== latestVersion) {
51
- console.log(picocolors_1.default.magenta(`\n🎁 A newer CLI version is available: ${currentVersion} -> ${latestVersion}`));
52
- console.log(picocolors_1.default.magenta(` Run ${picocolors_1.default.bold('npm install -g agent-skills-standard')} to update.\n`));
53
- }
54
- }
55
- }
56
- catch {
57
- console.error(picocolors_1.default.yellow('⚠️ CLI update check failed (non-fatal):'));
58
- }
59
- }
60
- async checkForUpdates(config, configPath) {
61
- const githubMatch = config.registry.match(/github\.com\/([^/]+)\/([^/.]+)(?:\.git)?/i);
62
- if (!githubMatch)
63
- return config;
64
- const owner = githubMatch[1];
65
- const repo = githubMatch[2];
66
- if (!owner || !repo)
67
- return config;
68
- try {
69
- console.log(picocolors_1.default.gray('🔍 Checking for skill updates...'));
70
- // 1. Get default branch
71
- const repoRes = await (0, node_fetch_1.default)(`https://api.github.com/repos/${owner}/${repo}`);
72
- if (!repoRes.ok)
73
- return config;
74
- const repoData = (await repoRes.json());
75
- const defaultBranch = repoData.default_branch || 'main';
76
- // 2. Fetch remote metadata.json
77
- const metaUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${defaultBranch}/skills/metadata.json`;
78
- const metaRes = await (0, node_fetch_1.default)(metaUrl);
79
- if (!metaRes.ok)
80
- return config;
81
- const remoteMeta = (await metaRes.json());
82
- const updates = [];
83
- for (const [cat, catConfig] of Object.entries(config.skills)) {
84
- const remoteCat = remoteMeta.categories?.[cat];
85
- if (!remoteCat || !remoteCat.version || !remoteCat.tag_prefix)
86
- continue;
87
- const latestTag = `${remoteCat.tag_prefix}${remoteCat.version}`;
88
- const currentRef = catConfig.ref || 'main';
89
- // Update if ref is different and doesn't explicitly contain the latest version string
90
- if (currentRef !== latestTag) {
91
- updates.push({
92
- category: cat,
93
- from: currentRef,
94
- to: latestTag,
95
- });
96
- }
97
- }
98
- if (updates.length > 0) {
99
- console.log(picocolors_1.default.yellow(`\n💡 ${updates.length} update(s) available:`));
100
- updates.forEach((u) => {
101
- console.log(picocolors_1.default.gray(` - ${picocolors_1.default.cyan(u.category)}: ${u.from} -> ${picocolors_1.default.green(u.to)}`));
102
- });
103
- const { updateAll } = await inquirer_1.default.prompt([
104
- {
105
- type: 'confirm',
106
- name: 'updateAll',
107
- message: 'Would you like to update your .skillsrc to the latest versions?',
108
- default: true,
109
- },
110
- ]);
111
- if (updateAll) {
112
- for (const u of updates) {
113
- config.skills[u.category].ref = u.to;
114
- }
115
- // Save back to .skillsrc
116
- await fs_extra_1.default.writeFile(configPath, js_yaml_1.default.dump(config));
117
- console.log(picocolors_1.default.green('✅ .skillsrc updated successfully.\n'));
118
- }
37
+ if (error instanceof Error) {
38
+ console.error(picocolors_1.default.red('❌ Sync failed:'), error.message);
119
39
  }
120
40
  else {
121
- console.log(picocolors_1.default.gray('✨ All skills are up to date.\n'));
41
+ console.error(picocolors_1.default.red('❌ Sync failed:'), String(error));
122
42
  }
123
43
  }
124
- catch {
125
- console.log(picocolors_1.default.gray(' (Could not check for updates, proceeding with current versions)'));
126
- }
44
+ }
45
+ async checkForUpdates(config) {
46
+ // Simplified implementation for now
127
47
  return config;
128
48
  }
129
49
  async assembleFromRemote(categories, config) {
130
50
  const collected = [];
131
- // Parse owner and repo from registry URL
132
- // Expected: https://github.com/owner/repo
133
51
  const githubMatch = config.registry.match(/github\.com\/([^/]+)\/([^/]+)/i);
134
52
  if (!githubMatch) {
135
- console.log(picocolors_1.default.red('Error: Only GitHub registries are supported for auto-discovery.'));
53
+ console.log(picocolors_1.default.red('Error: Only GitHub registries supported.'));
136
54
  return [];
137
55
  }
138
56
  const [owner, repo] = githubMatch.slice(1);
139
- // We iterate categories individually to handle versioning (different refs)
140
57
  for (const category of categories) {
141
- const categoryConfig = config.skills[category];
142
- const ref = categoryConfig.ref || 'main';
143
- const rawBaseUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/skills/`;
144
- const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${ref}?recursive=1`;
145
- try {
146
- console.log(picocolors_1.default.gray(` - Discovering ${category} skills via GitHub API (${ref})...`));
147
- const treeRes = await (0, node_fetch_1.default)(treeUrl, {
148
- headers: { Accept: 'application/vnd.github.v3+json' },
149
- });
150
- if (!treeRes.ok) {
151
- console.log(picocolors_1.default.red(` ❌ Failed to fetch ${category}@${ref} (Status: ${treeRes.status}). Skipping.`));
152
- continue;
153
- }
154
- const treeData = (await treeRes.json());
155
- const allFiles = treeData.tree || [];
156
- // Find all skill folder names first
157
- const skillFolders = Array.from(new Set(allFiles
158
- .filter((f) => f.path.startsWith(`skills/${category}/`))
159
- .map((f) => {
58
+ const catConfig = config.skills[category];
59
+ const ref = catConfig.ref || 'main';
60
+ console.log(picocolors_1.default.gray(` - Discovering ${category} (${ref})...`));
61
+ const treeData = await this.githubService.getRepoTree(owner, repo, ref);
62
+ if (!treeData) {
63
+ console.log(picocolors_1.default.red(` ❌ Failed to fetch ${category}@${ref}.`));
64
+ continue;
65
+ }
66
+ // Identify skill folders
67
+ const skillFolders = new Set();
68
+ treeData.tree.forEach((f) => {
69
+ if (f.path.startsWith(`skills/${category}/`)) {
160
70
  const parts = f.path.split('/');
161
- return parts[2]; // skills/category/SKILL_NAME/file.md
162
- })
163
- .filter(Boolean)));
164
- for (const skillFolderName of skillFolders) {
165
- if (categoryConfig.include &&
166
- !categoryConfig.include.includes(skillFolderName))
167
- continue;
168
- if (categoryConfig.exclude &&
169
- categoryConfig.exclude.includes(skillFolderName))
170
- continue;
171
- const skillFiles = [];
172
- // Find all files belonging to this specific skill
173
- const skillSourceFiles = allFiles.filter((f) => f.path.startsWith(`skills/${category}/${skillFolderName}/`));
174
- for (const fileItem of skillSourceFiles) {
175
- // Get relative path within the skill folder (e.g., "SKILL.md" or "references/REFERENCE.md")
176
- const relativePath = fileItem.path.replace(`skills/${category}/${skillFolderName}/`, '');
177
- // Only sync supported files and directories based on standard
178
- const isSupported = relativePath === 'SKILL.md' ||
179
- relativePath.startsWith('references/') ||
180
- relativePath.startsWith('scripts/') ||
181
- relativePath.startsWith('assets/');
182
- if (isSupported && fileItem.type === 'blob') {
183
- const fileUrl = `${rawBaseUrl}${category}/${skillFolderName}/${relativePath}`;
184
- const fileRes = await (0, node_fetch_1.default)(fileUrl);
185
- if (fileRes.ok) {
186
- const content = await fileRes.text();
187
- skillFiles.push({ name: relativePath, content });
188
- }
189
- }
190
- }
191
- if (skillFiles.length > 0) {
192
- collected.push({
193
- category,
194
- skill: skillFolderName,
195
- files: skillFiles,
196
- });
197
- console.log(picocolors_1.default.gray(` + Fetched ${category}/${skillFolderName} (${skillFiles.length} files)`));
198
- }
71
+ if (parts[2])
72
+ skillFolders.add(parts[2]);
73
+ }
74
+ });
75
+ // Filter included/excluded
76
+ const foldersToSync = Array.from(skillFolders).filter((folder) => {
77
+ if (catConfig.include && !catConfig.include.includes(folder))
78
+ return false;
79
+ if (catConfig.exclude && catConfig.exclude.includes(folder))
80
+ return false;
81
+ return true;
82
+ });
83
+ for (const skill of foldersToSync) {
84
+ // Gather files for this skill
85
+ const skillSourceFiles = treeData.tree.filter((f) => f.path.startsWith(`skills/${category}/${skill}/`) &&
86
+ f.type === 'blob');
87
+ // Prepare download tasks
88
+ const downloadTasks = skillSourceFiles
89
+ .map((f) => ({
90
+ owner,
91
+ repo,
92
+ ref,
93
+ path: f.path,
94
+ }))
95
+ .filter((t) => {
96
+ const relative = t.path.replace(`skills/${category}/${skill}/`, '');
97
+ return (relative === 'SKILL.md' ||
98
+ relative.startsWith('references/') ||
99
+ relative.startsWith('scripts/') ||
100
+ relative.startsWith('assets/'));
101
+ });
102
+ // DOWNLOAD CONCURRENTLY
103
+ const files = await this.githubService.downloadFilesConcurrent(downloadTasks);
104
+ if (files.length > 0) {
105
+ collected.push({
106
+ category,
107
+ skill,
108
+ files: files.map((f) => ({
109
+ name: f.path.replace(`skills/${category}/${skill}/`, ''),
110
+ content: f.content,
111
+ })),
112
+ });
113
+ console.log(picocolors_1.default.gray(` + Fetched ${category}/${skill} (${files.length} files)`));
199
114
  }
200
115
  }
201
- catch (error) {
202
- console.log(picocolors_1.default.red(`Error: Failed to fetch remote skills for ${category}. ${error}`));
203
- }
204
- } // End category loop
116
+ }
205
117
  return collected;
206
118
  }
207
119
  async writeToTargets(skills, config) {
@@ -213,16 +125,12 @@ class SyncCommand {
213
125
  continue;
214
126
  const basePath = agentDef.path;
215
127
  if (basePath) {
216
- // We do NOT use emptyDir here because we want to preserve user's own custom skills
217
128
  await fs_extra_1.default.ensureDir(basePath);
218
129
  for (const skill of skills) {
219
- // Use nested structure: category/skill (e.g. flutter/bloc)
220
130
  const skillPath = path_1.default.join(basePath, skill.category, skill.skill);
221
131
  await fs_extra_1.default.ensureDir(skillPath);
222
132
  for (const fileItem of skill.files) {
223
133
  const targetFilePath = path_1.default.join(skillPath, fileItem.name);
224
- // Check if this specific file is in custom_overrides
225
- // Supports both specific files and entire folders
226
134
  const relativePath = path_1.default
227
135
  .relative(process.cwd(), targetFilePath)
228
136
  .replace(/\\/g, '/');
@@ -232,7 +140,14 @@ class SyncCommand {
232
140
  relativePath.startsWith(`${overridePath.replace(/\/$/, '')}/`));
233
141
  });
234
142
  if (isOverridden) {
235
- console.log(picocolors_1.default.yellow(` ⚠️ Skipping overridden item: ${relativePath}`));
143
+ console.log(picocolors_1.default.yellow(` ⚠️ Skipping overridden: ${relativePath}`));
144
+ continue;
145
+ }
146
+ // SECURITY: Path Traversal Check
147
+ const resolvedTarget = path_1.default.resolve(targetFilePath);
148
+ const resolvedBase = path_1.default.resolve(skillPath);
149
+ if (!resolvedTarget.startsWith(resolvedBase)) {
150
+ console.log(picocolors_1.default.red(` ❌ Security Error: Invalid path ${fileItem.name}`));
236
151
  continue;
237
152
  }
238
153
  await fs_extra_1.default.outputFile(targetFilePath, fileItem.content);
@@ -0,0 +1,5 @@
1
+ export declare class ValidateCommand {
2
+ run(options?: {
3
+ all?: boolean;
4
+ }): Promise<void>;
5
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ValidateCommand = void 0;
4
+ const SkillValidator_1 = require("../services/SkillValidator");
5
+ class ValidateCommand {
6
+ async run(options = {}) {
7
+ const validator = new SkillValidator_1.SkillValidator();
8
+ const exitCode = await validator.run(options.all ?? false);
9
+ process.exit(exitCode);
10
+ }
11
+ }
12
+ exports.ValidateCommand = ValidateCommand;
package/dist/index.js CHANGED
@@ -5,11 +5,12 @@ const commander_1 = require("commander");
5
5
  const init_1 = require("./commands/init");
6
6
  const list_skills_1 = require("./commands/list-skills");
7
7
  const sync_1 = require("./commands/sync");
8
+ const validate_skills_1 = require("./commands/validate-skills");
8
9
  const program = new commander_1.Command();
9
10
  program
10
11
  .name('agent-skills-standard')
11
12
  .description('A CLI to manage and sync AI agent skills for Cursor, Claude, Copilot, Windsurf, and more.')
12
- .version('1.2.0');
13
+ .version('1.3.0');
13
14
  program
14
15
  .command('init')
15
16
  .description('Initialize a .skillsrc configuration file interactively')
@@ -31,4 +32,12 @@ program
31
32
  const cmd = new list_skills_1.ListSkillsCommand();
32
33
  await cmd.run();
33
34
  });
35
+ program
36
+ .command('validate')
37
+ .description('Validate skill format and token efficiency standards')
38
+ .option('--all', 'Validate all skills instead of only changed ones')
39
+ .action(async (options) => {
40
+ const cmd = new validate_skills_1.ValidateCommand();
41
+ await cmd.run(options);
42
+ });
34
43
  program.parse();
@@ -3,6 +3,7 @@ export interface SkillEntry {
3
3
  exclude?: string[];
4
4
  include?: string[];
5
5
  }
6
+ export type CategoryConfig = SkillEntry;
6
7
  export interface SkillConfig {
7
8
  registry: string;
8
9
  agents: string[];
@@ -1,8 +1,8 @@
1
1
  import { SkillConfig } from '../models/config';
2
2
  import { RegistryMetadata } from '../models/types';
3
3
  export declare class ConfigService {
4
+ loadConfig(cwd?: string): Promise<SkillConfig | null>;
5
+ saveConfig(config: SkillConfig, cwd?: string): Promise<void>;
4
6
  buildInitialConfig(framework: string, agents: string[], registry: string, metadata: Partial<RegistryMetadata>): SkillConfig;
5
- private getAllKnownCategories;
6
- private createSkillEntry;
7
7
  applyDependencyExclusions(config: SkillConfig, framework: string, projectDeps: Set<string>): void;
8
8
  }
@@ -1,70 +1,87 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.ConfigService = void 0;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const js_yaml_1 = __importDefault(require("js-yaml"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const zod_1 = require("zod");
4
11
  const constants_1 = require("../constants");
12
+ const SkillConfigSchema = zod_1.z.object({
13
+ registry: zod_1.z.string().url(),
14
+ agents: zod_1.z.array(zod_1.z.string()).optional(),
15
+ skills: zod_1.z.record(zod_1.z.string(), // Category name
16
+ zod_1.z.object({
17
+ ref: zod_1.z.string().optional(),
18
+ include: zod_1.z.array(zod_1.z.string()).optional(),
19
+ exclude: zod_1.z.array(zod_1.z.string()).optional(),
20
+ })),
21
+ custom_overrides: zod_1.z.array(zod_1.z.string()).optional(),
22
+ });
5
23
  class ConfigService {
6
- buildInitialConfig(framework, agents, registry, metadata) {
7
- const selectedFramework = constants_1.SUPPORTED_FRAMEWORKS.find((f) => f.id === framework);
8
- const neededSkills = new Set();
9
- constants_1.UNIVERSAL_SKILLS.forEach((s) => neededSkills.add(s));
10
- neededSkills.add(framework);
11
- if (selectedFramework) {
12
- selectedFramework.languages.forEach((l) => neededSkills.add(l));
13
- }
14
- // Special cases
15
- if (framework === constants_1.Framework.NextJS || framework === constants_1.Framework.ReactNative) {
16
- neededSkills.add(constants_1.Framework.React);
24
+ async loadConfig(cwd = process.cwd()) {
25
+ const configPath = path_1.default.join(cwd, '.skillsrc');
26
+ if (!(await fs_extra_1.default.pathExists(configPath))) {
27
+ return null;
17
28
  }
18
- const frameworkSkills = constants_1.SKILL_DETECTION_REGISTRY[framework] || [];
19
- frameworkSkills.forEach((s) => neededSkills.add(s.id));
20
- const config = {
21
- registry,
22
- agents,
23
- skills: {},
24
- custom_overrides: [],
25
- };
26
- const allKnownCategories = this.getAllKnownCategories();
27
- for (const skill of [...new Set(allKnownCategories)]) {
28
- if (neededSkills.has(skill)) {
29
- config.skills[skill] = this.createSkillEntry(skill, metadata);
29
+ try {
30
+ const content = await fs_extra_1.default.readFile(configPath, 'utf8');
31
+ const rawConfig = js_yaml_1.default.load(content);
32
+ // Validate with Zod
33
+ const parsed = SkillConfigSchema.safeParse(rawConfig);
34
+ if (!parsed.success) {
35
+ throw new Error(`Invalid .skillsrc format: ${parsed.error.message}`);
30
36
  }
37
+ return parsed.data;
38
+ }
39
+ catch (error) {
40
+ throw new Error(`Failed to load config: ${error}`);
31
41
  }
32
- return config;
33
42
  }
34
- getAllKnownCategories() {
35
- return [
36
- ...constants_1.UNIVERSAL_SKILLS,
37
- ...constants_1.SUPPORTED_FRAMEWORKS.map((f) => f.id),
38
- ...constants_1.SUPPORTED_FRAMEWORKS.flatMap((f) => f.languages),
39
- ...Object.values(constants_1.SKILL_DETECTION_REGISTRY).flatMap((skills) => skills.map((s) => s.id)),
40
- ];
43
+ async saveConfig(config, cwd = process.cwd()) {
44
+ const configPath = path_1.default.join(cwd, '.skillsrc');
45
+ await fs_extra_1.default.writeFile(configPath, js_yaml_1.default.dump(config));
41
46
  }
42
- createSkillEntry(skill, metadata) {
43
- const skillMeta = metadata.categories?.[skill];
44
- let refStr;
45
- if (skillMeta?.version && skillMeta?.tag_prefix) {
46
- refStr = `${skillMeta.tag_prefix}${skillMeta.version}`;
47
+ buildInitialConfig(framework, agents, registry, metadata) {
48
+ const skills = {};
49
+ // Add main framework
50
+ skills[framework] = {
51
+ ref: metadata.categories?.[framework]?.version
52
+ ? `${metadata.categories[framework].tag_prefix || ''}${metadata.categories[framework].version}`
53
+ : 'main',
54
+ };
55
+ // Add common category if available
56
+ if (metadata.categories?.['common']) {
57
+ skills['common'] = {
58
+ ref: `${metadata.categories['common'].tag_prefix || ''}${metadata.categories['common'].version}`,
59
+ };
47
60
  }
48
61
  return {
49
- ref: refStr,
62
+ registry,
63
+ agents,
64
+ skills,
65
+ custom_overrides: [],
50
66
  };
51
67
  }
52
68
  applyDependencyExclusions(config, framework, projectDeps) {
53
- const frameworkSkills = constants_1.SKILL_DETECTION_REGISTRY[framework] || [];
54
- for (const skill of frameworkSkills) {
55
- const entry = config.skills[skill.id];
56
- if (!entry)
57
- continue;
58
- const used = skill.packages.some((p) => projectDeps.has(p));
59
- if (!used) {
60
- const parent = config.skills[framework] || {};
61
- const excludes = parent.exclude ? [...parent.exclude] : [];
62
- if (!excludes.includes(skill.id))
63
- excludes.push(skill.id);
64
- parent.exclude = excludes;
65
- config.skills[framework] = parent;
69
+ const category = config.skills[framework];
70
+ if (!category)
71
+ return;
72
+ const exclusions = new Set(category.exclude || []);
73
+ // mapping of "sub-skill identifier" to "package keywords"
74
+ // from the centralized SKILL_DETECTION_REGISTRY
75
+ const detections = constants_1.SKILL_DETECTION_REGISTRY[framework] || [];
76
+ for (const detection of detections) {
77
+ const hasDep = Array.from(projectDeps).some((d) => detection.packages.some((pkg) => d.toLowerCase().includes(pkg.toLowerCase())));
78
+ if (!hasDep) {
79
+ exclusions.add(detection.id);
66
80
  }
67
81
  }
82
+ if (exclusions.size > 0) {
83
+ category.exclude = Array.from(exclusions);
84
+ }
68
85
  }
69
86
  }
70
87
  exports.ConfigService = ConfigService;
@@ -1,5 +1,6 @@
1
1
  export declare class DetectionService {
2
2
  detectFrameworks(): Promise<Record<string, boolean>>;
3
3
  detectAgents(): Promise<Record<string, boolean>>;
4
+ getProjectDeps(): Promise<Set<string>>;
4
5
  private getPackageDeps;
5
6
  }