agent-skills-standard 1.1.2 → 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.
- package/README.md +24 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +24 -153
- package/dist/commands/list-skills.d.ts +3 -0
- package/dist/commands/list-skills.js +20 -7
- package/dist/commands/sync.d.ts +2 -1
- package/dist/commands/sync.js +88 -177
- package/dist/commands/validate-skills.d.ts +5 -0
- package/dist/commands/validate-skills.js +12 -0
- package/dist/constants/index.d.ts +3 -1
- package/dist/constants/index.js +10 -1
- package/dist/index.js +11 -2
- package/dist/models/config.d.ts +7 -6
- package/dist/services/ConfigService.d.ts +8 -0
- package/dist/services/ConfigService.js +87 -0
- package/dist/services/DetectionService.d.ts +6 -0
- package/dist/services/DetectionService.js +113 -0
- package/dist/services/GithubService.d.ts +26 -0
- package/dist/services/GithubService.js +100 -0
- package/dist/services/RegistryService.d.ts +10 -0
- package/dist/services/RegistryService.js +52 -0
- package/dist/services/SkillValidator.d.ts +19 -0
- package/dist/services/SkillValidator.js +264 -0
- package/package.json +17 -20
- package/dist/utils/github.d.ts +0 -10
- package/dist/utils/github.js +0 -39
- package/dist/utils/project.d.ts +0 -2
- package/dist/utils/project.js +0 -60
package/README.md
CHANGED
|
@@ -71,6 +71,30 @@ npx agent-skills-standard sync
|
|
|
71
71
|
|
|
72
72
|
---
|
|
73
73
|
|
|
74
|
+
## ⚙️ Configuration (`.skillsrc`)
|
|
75
|
+
|
|
76
|
+
The `.skillsrc` file allows you to customize how skills are synced to your project.
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
registry: https://github.com/HoangNguyen0403/agent-skills-standard
|
|
80
|
+
agents: [cursor, copilot]
|
|
81
|
+
skills:
|
|
82
|
+
flutter:
|
|
83
|
+
ref: flutter-v1.1.0
|
|
84
|
+
# 🚫 Exclude specific sub-skills from being synced
|
|
85
|
+
exclude: ['getx-navigation']
|
|
86
|
+
# 🔒 Protect local modifications from being overwritten
|
|
87
|
+
custom_overrides: ['bloc-state-management']
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Key Options
|
|
91
|
+
|
|
92
|
+
- **`exclude`**: A list of skill IDs to skip during synchronization. Useful if you want the main framework guidelines but use a different library for a specific sub-task (e.g., excluding `auto-route` if you use `go_router`).
|
|
93
|
+
- **`custom_overrides`**: A list of skill IDs that the CLI should **never** overwrite. Use this when you have modified a standard skill locally to fit your project's unique needs. The CLI will detect these and preserve your local version.
|
|
94
|
+
- **`ref`**: Specify a specific version or tag of the skills from the registry.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
74
98
|
## ✨ Key Features
|
|
75
99
|
|
|
76
100
|
- **🎯 Efficiency First**: Uses a "Search-on-Demand" pattern that only loads information when the AI needs it, saving its "brain power" for your code.
|
package/dist/commands/init.d.ts
CHANGED
package/dist/commands/init.js
CHANGED
|
@@ -7,103 +7,32 @@ exports.InitCommand = void 0;
|
|
|
7
7
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
8
|
const inquirer_1 = __importDefault(require("inquirer"));
|
|
9
9
|
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
10
|
-
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
11
10
|
const path_1 = __importDefault(require("path"));
|
|
12
11
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
13
12
|
const constants_1 = require("../constants");
|
|
14
|
-
const
|
|
15
|
-
const
|
|
13
|
+
const ConfigService_1 = require("../services/ConfigService");
|
|
14
|
+
const DetectionService_1 = require("../services/DetectionService");
|
|
15
|
+
const RegistryService_1 = require("../services/RegistryService");
|
|
16
16
|
class InitCommand {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.detectionService = new DetectionService_1.DetectionService();
|
|
19
|
+
this.registryService = new RegistryService_1.RegistryService();
|
|
20
|
+
this.configService = new ConfigService_1.ConfigService();
|
|
21
|
+
}
|
|
17
22
|
async run() {
|
|
18
23
|
const configPath = path_1.default.join(process.cwd(), '.skillsrc');
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
let packageDeps = {};
|
|
22
|
-
if (await fs_extra_1.default.pathExists(packageJsonPath)) {
|
|
23
|
-
try {
|
|
24
|
-
const pkg = await fs_extra_1.default.readJson(packageJsonPath);
|
|
25
|
-
packageDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
26
|
-
}
|
|
27
|
-
catch (e) {
|
|
28
|
-
console.error(picocolors_1.default.red(`❌ Failed to read package.json: ${e instanceof Error ? e.message : String(e)}`));
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
// Project detection
|
|
32
|
-
const detectionResults = {};
|
|
33
|
-
for (const framework of constants_1.SUPPORTED_FRAMEWORKS) {
|
|
34
|
-
let detected = false;
|
|
35
|
-
// 1. Check characteristic files
|
|
36
|
-
for (const file of framework.detectionFiles) {
|
|
37
|
-
if (await fs_extra_1.default.pathExists(file)) {
|
|
38
|
-
detected = true;
|
|
39
|
-
break;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
// 2. Check dependencies (if not yet detected)
|
|
43
|
-
if (!detected &&
|
|
44
|
-
framework.detectionDependencies &&
|
|
45
|
-
framework.detectionDependencies.length > 0) {
|
|
46
|
-
detected = framework.detectionDependencies.some((dep) => Object.prototype.hasOwnProperty.call(packageDeps, dep));
|
|
47
|
-
}
|
|
48
|
-
detectionResults[framework.id] = detected;
|
|
49
|
-
}
|
|
50
|
-
// Agent detection
|
|
51
|
-
const agentDetection = {};
|
|
52
|
-
for (const agent of constants_1.SUPPORTED_AGENTS) {
|
|
53
|
-
let detected = false;
|
|
54
|
-
for (const file of agent.detectionFiles) {
|
|
55
|
-
if (await fs_extra_1.default.pathExists(file)) {
|
|
56
|
-
detected = true;
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
agentDetection[agent.id] = detected;
|
|
61
|
-
}
|
|
24
|
+
const detectionResults = await this.detectionService.detectFrameworks();
|
|
25
|
+
const agentDetection = await this.detectionService.detectAgents();
|
|
62
26
|
const anyAgentDetected = Object.values(agentDetection).some((d) => d);
|
|
63
|
-
// Helper to determine if an agent should be pre-selected
|
|
64
27
|
const shouldCheck = (id) => anyAgentDetected ? agentDetection[id] : true;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (parsed) {
|
|
74
|
-
const treeResult = await (0, github_1.fetchRepoTree)(parsed.owner, parsed.repo);
|
|
75
|
-
if (treeResult && Array.isArray(treeResult.data.tree)) {
|
|
76
|
-
const allFiles = treeResult.data.tree || [];
|
|
77
|
-
const categories = new Set();
|
|
78
|
-
allFiles.forEach((f) => {
|
|
79
|
-
if (f.path.startsWith('skills/') && f.type === 'tree') {
|
|
80
|
-
const parts = f.path.split('/');
|
|
81
|
-
if (parts.length === 2)
|
|
82
|
-
categories.add(parts[1]);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
if (categories.size > 0)
|
|
86
|
-
supportedCategories = Array.from(categories);
|
|
87
|
-
// Try to fetch metadata.json at the resolved branch
|
|
88
|
-
const metaUrl = `https://raw.githubusercontent.com/${parsed.owner}/${parsed.repo}/${treeResult.branch}/skills/metadata.json`;
|
|
89
|
-
const metaRes = await (0, node_fetch_1.default)(metaUrl);
|
|
90
|
-
if (metaRes.ok) {
|
|
91
|
-
metadata = (await metaRes.json());
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
catch (e) {
|
|
97
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
98
|
-
console.error(picocolors_1.default.red(`❌ Failed to fetch registry metadata or tree: ${msg}`));
|
|
99
|
-
}
|
|
100
|
-
const choices = constants_1.SUPPORTED_FRAMEWORKS.map((f) => {
|
|
101
|
-
const isSupported = supportedCategories.includes(f.id);
|
|
102
|
-
return {
|
|
103
|
-
name: isSupported ? f.name : `${f.name} (Coming Soon)`,
|
|
104
|
-
value: f.id,
|
|
105
|
-
};
|
|
106
|
-
});
|
|
28
|
+
const defaultRegistry = 'https://github.com/HoangNguyen0403/agent-skills-standard';
|
|
29
|
+
const { categories: supportedCategories, metadata } = await this.registryService.discoverRegistry(defaultRegistry);
|
|
30
|
+
const choices = constants_1.SUPPORTED_FRAMEWORKS.map((f) => ({
|
|
31
|
+
name: supportedCategories.includes(f.id)
|
|
32
|
+
? f.name
|
|
33
|
+
: `${f.name} (Coming Soon)`,
|
|
34
|
+
value: f.id,
|
|
35
|
+
}));
|
|
107
36
|
if (await fs_extra_1.default.pathExists(configPath)) {
|
|
108
37
|
const { overwrite } = await inquirer_1.default.prompt([
|
|
109
38
|
{
|
|
@@ -141,75 +70,20 @@ class InitCommand {
|
|
|
141
70
|
type: 'input',
|
|
142
71
|
name: 'registry',
|
|
143
72
|
message: 'Skills Registry URL:',
|
|
144
|
-
default:
|
|
73
|
+
default: defaultRegistry,
|
|
145
74
|
},
|
|
146
75
|
]);
|
|
147
76
|
const framework = answers.framework;
|
|
148
|
-
|
|
149
|
-
const isSupported = supportedCategories.includes(framework);
|
|
150
|
-
if (!isSupported) {
|
|
77
|
+
if (!supportedCategories.includes(framework)) {
|
|
151
78
|
console.log(picocolors_1.default.yellow(`\nNotice: Skills for ${framework} are not yet strictly defined and will be added soon.`));
|
|
152
79
|
console.log(picocolors_1.default.gray('The CLI will still generate rule files with global standards.\n'));
|
|
153
80
|
}
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
selectedFramework.languages.forEach((l) => neededSkills.add(l));
|
|
158
|
-
}
|
|
159
|
-
// Special case: Next.js and React Native imply React
|
|
160
|
-
if (framework === constants_1.Framework.NextJS || framework === constants_1.Framework.ReactNative) {
|
|
161
|
-
neededSkills.add(constants_1.Framework.React);
|
|
162
|
-
}
|
|
163
|
-
// Add framework-specific skills from the registry to potential skills
|
|
164
|
-
const frameworkSkills = constants_1.SKILL_DETECTION_REGISTRY[framework] || [];
|
|
165
|
-
frameworkSkills.forEach((s) => neededSkills.add(s.id));
|
|
166
|
-
const config = {
|
|
167
|
-
registry: answers.registry,
|
|
168
|
-
agents: answers.agents,
|
|
169
|
-
skills: {},
|
|
170
|
-
custom_overrides: [],
|
|
171
|
-
};
|
|
172
|
-
// Initialize all potential skills from the registry folders we know
|
|
173
|
-
// This will be expanded as more frameworks are added to the repo
|
|
174
|
-
const allKnownCategories = [
|
|
175
|
-
...constants_1.SUPPORTED_FRAMEWORKS.map((f) => f.id),
|
|
176
|
-
...constants_1.SUPPORTED_FRAMEWORKS.flatMap((f) => f.languages),
|
|
177
|
-
...Object.values(constants_1.SKILL_DETECTION_REGISTRY).flatMap((skills) => skills.map((s) => s.id)),
|
|
178
|
-
];
|
|
179
|
-
for (const skill of [...new Set(allKnownCategories)]) {
|
|
180
|
-
if (neededSkills.has(skill)) {
|
|
181
|
-
const skillMeta = metadata.categories?.[skill];
|
|
182
|
-
let refStr;
|
|
183
|
-
if (skillMeta?.version && skillMeta?.tag_prefix) {
|
|
184
|
-
refStr = `${skillMeta.tag_prefix}${skillMeta.version}`;
|
|
185
|
-
}
|
|
186
|
-
config.skills[skill] = {
|
|
187
|
-
enabled: true,
|
|
188
|
-
ref: refStr,
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
// Detect common framework skills and disable them if not used
|
|
193
|
-
const projectDeps = await (0, project_1.buildProjectDeps)();
|
|
194
|
-
for (const skill of frameworkSkills) {
|
|
195
|
-
const entry = config.skills[skill.id];
|
|
196
|
-
if (!entry)
|
|
197
|
-
continue;
|
|
198
|
-
// Only rely on declared dependencies for detection.
|
|
199
|
-
const used = skill.packages.some((p) => projectDeps.has(p));
|
|
200
|
-
// If not used, add to parent framework's exclude list so users
|
|
201
|
-
// see which sub-skills were disabled by detection and can opt-in.
|
|
202
|
-
if (!used) {
|
|
203
|
-
const parent = config.skills[framework] || { enabled: true };
|
|
204
|
-
const excludes = parent.exclude ? [...parent.exclude] : [];
|
|
205
|
-
if (!excludes.includes(skill.id))
|
|
206
|
-
excludes.push(skill.id);
|
|
207
|
-
parent.exclude = excludes;
|
|
208
|
-
config.skills[framework] = parent;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
81
|
+
const config = this.configService.buildInitialConfig(framework, answers.agents, answers.registry, metadata);
|
|
82
|
+
const projectDeps = await this.detectionService.getProjectDeps();
|
|
83
|
+
this.configService.applyDependencyExclusions(config, framework, projectDeps);
|
|
211
84
|
const commentHeader = `# Auto-detected configuration generated by agent-skills-standard init
|
|
212
85
|
#
|
|
86
|
+
# Presence in 'skills' list = Active. To disable a skill, remove it from the list.
|
|
213
87
|
# 'exclude': IDs of sub-skills to skip during sync (auto-populated with undetected skills).
|
|
214
88
|
# 'custom_overrides': IDs of skills to PROTECT. Use this if you have modified a standard
|
|
215
89
|
# skill locally and don't want the CLI to overwrite it.
|
|
@@ -219,9 +93,6 @@ class InitCommand {
|
|
|
219
93
|
await fs_extra_1.default.writeFile(configPath, commentHeader + js_yaml_1.default.dump(config));
|
|
220
94
|
console.log(picocolors_1.default.green('\n✅ Initialized .skillsrc with your preferences!'));
|
|
221
95
|
console.log(picocolors_1.default.gray(` Selected framework: ${framework}`));
|
|
222
|
-
console.log(picocolors_1.default.gray(` Auto-enabled languages: ${Array.from(neededSkills)
|
|
223
|
-
.filter((s) => s !== framework)
|
|
224
|
-
.join(', ') || 'none'}`));
|
|
225
96
|
console.log(picocolors_1.default.cyan('\nNext step: Run `agent-skills-standard sync` to generate rule files.'));
|
|
226
97
|
}
|
|
227
98
|
}
|
|
@@ -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
|
|
14
|
-
const
|
|
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
|
|
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 =
|
|
49
|
+
const parsed = this.parseRegistryUrl(registryUrl);
|
|
46
50
|
if (parsed) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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;
|
package/dist/commands/sync.d.ts
CHANGED
package/dist/commands/sync.js
CHANGED
|
@@ -5,207 +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
|
-
|
|
24
|
-
let config =
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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}...`));
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
.map(([key]) => key);
|
|
29
|
+
// 3. Assemble skills (concurrently)
|
|
30
|
+
const enabledCategories = Object.keys(config.skills);
|
|
33
31
|
const skills = await this.assembleFromRemote(enabledCategories, config);
|
|
32
|
+
// 4. Write to disk
|
|
34
33
|
await this.writeToTargets(skills, config);
|
|
35
34
|
console.log(picocolors_1.default.green('✅ All skills synced successfully!'));
|
|
36
35
|
}
|
|
37
36
|
catch (error) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
async checkCLIUpdate() {
|
|
42
|
-
try {
|
|
43
|
-
const pkgPath = path_1.default.join(__dirname, '../../package.json');
|
|
44
|
-
if (!fs_extra_1.default.existsSync(pkgPath))
|
|
45
|
-
return;
|
|
46
|
-
const pkg = await fs_extra_1.default.readJson(pkgPath);
|
|
47
|
-
const currentVersion = pkg.version;
|
|
48
|
-
const res = await (0, node_fetch_1.default)('https://registry.npmjs.org/agent-skills-standard/latest');
|
|
49
|
-
if (res.ok) {
|
|
50
|
-
const remoteData = (await res.json());
|
|
51
|
-
const latestVersion = remoteData.version;
|
|
52
|
-
if (currentVersion !== latestVersion) {
|
|
53
|
-
console.log(picocolors_1.default.magenta(`\n🎁 A newer CLI version is available: ${currentVersion} -> ${latestVersion}`));
|
|
54
|
-
console.log(picocolors_1.default.magenta(` Run ${picocolors_1.default.bold('npm install -g agent-skills-standard')} to update.\n`));
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
console.error(picocolors_1.default.yellow('⚠️ CLI update check failed (non-fatal):'));
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
async checkForUpdates(config, configPath) {
|
|
63
|
-
const githubMatch = config.registry.match(/github\.com\/([^/]+)\/([^/.]+)(?:\.git)?/i);
|
|
64
|
-
if (!githubMatch)
|
|
65
|
-
return config;
|
|
66
|
-
const owner = githubMatch[1];
|
|
67
|
-
const repo = githubMatch[2];
|
|
68
|
-
if (!owner || !repo)
|
|
69
|
-
return config;
|
|
70
|
-
try {
|
|
71
|
-
console.log(picocolors_1.default.gray('🔍 Checking for skill updates...'));
|
|
72
|
-
// 1. Get default branch
|
|
73
|
-
const repoRes = await (0, node_fetch_1.default)(`https://api.github.com/repos/${owner}/${repo}`);
|
|
74
|
-
if (!repoRes.ok)
|
|
75
|
-
return config;
|
|
76
|
-
const repoData = (await repoRes.json());
|
|
77
|
-
const defaultBranch = repoData.default_branch || 'main';
|
|
78
|
-
// 2. Fetch remote metadata.json
|
|
79
|
-
const metaUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${defaultBranch}/skills/metadata.json`;
|
|
80
|
-
const metaRes = await (0, node_fetch_1.default)(metaUrl);
|
|
81
|
-
if (!metaRes.ok)
|
|
82
|
-
return config;
|
|
83
|
-
const remoteMeta = (await metaRes.json());
|
|
84
|
-
const updates = [];
|
|
85
|
-
for (const [cat, catConfig] of Object.entries(config.skills)) {
|
|
86
|
-
if (!catConfig.enabled)
|
|
87
|
-
continue;
|
|
88
|
-
const remoteCat = remoteMeta.categories?.[cat];
|
|
89
|
-
if (!remoteCat || !remoteCat.version || !remoteCat.tag_prefix)
|
|
90
|
-
continue;
|
|
91
|
-
const latestTag = `${remoteCat.tag_prefix}${remoteCat.version}`;
|
|
92
|
-
const currentRef = catConfig.ref || 'main';
|
|
93
|
-
// Update if ref is different and doesn't explicitly contain the latest version string
|
|
94
|
-
if (currentRef !== latestTag) {
|
|
95
|
-
updates.push({
|
|
96
|
-
category: cat,
|
|
97
|
-
from: currentRef,
|
|
98
|
-
to: latestTag,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (updates.length > 0) {
|
|
103
|
-
console.log(picocolors_1.default.yellow(`\n💡 ${updates.length} update(s) available:`));
|
|
104
|
-
updates.forEach((u) => {
|
|
105
|
-
console.log(picocolors_1.default.gray(` - ${picocolors_1.default.cyan(u.category)}: ${u.from} -> ${picocolors_1.default.green(u.to)}`));
|
|
106
|
-
});
|
|
107
|
-
const { updateAll } = await inquirer_1.default.prompt([
|
|
108
|
-
{
|
|
109
|
-
type: 'confirm',
|
|
110
|
-
name: 'updateAll',
|
|
111
|
-
message: 'Would you like to update your .skillsrc to the latest versions?',
|
|
112
|
-
default: true,
|
|
113
|
-
},
|
|
114
|
-
]);
|
|
115
|
-
if (updateAll) {
|
|
116
|
-
for (const u of updates) {
|
|
117
|
-
config.skills[u.category].ref = u.to;
|
|
118
|
-
}
|
|
119
|
-
// Save back to .skillsrc
|
|
120
|
-
await fs_extra_1.default.writeFile(configPath, js_yaml_1.default.dump(config));
|
|
121
|
-
console.log(picocolors_1.default.green('✅ .skillsrc updated successfully.\n'));
|
|
122
|
-
}
|
|
37
|
+
if (error instanceof Error) {
|
|
38
|
+
console.error(picocolors_1.default.red('❌ Sync failed:'), error.message);
|
|
123
39
|
}
|
|
124
40
|
else {
|
|
125
|
-
console.
|
|
41
|
+
console.error(picocolors_1.default.red('❌ Sync failed:'), String(error));
|
|
126
42
|
}
|
|
127
43
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
44
|
+
}
|
|
45
|
+
async checkForUpdates(config) {
|
|
46
|
+
// Simplified implementation for now
|
|
131
47
|
return config;
|
|
132
48
|
}
|
|
133
49
|
async assembleFromRemote(categories, config) {
|
|
134
50
|
const collected = [];
|
|
135
|
-
// Parse owner and repo from registry URL
|
|
136
|
-
// Expected: https://github.com/owner/repo
|
|
137
51
|
const githubMatch = config.registry.match(/github\.com\/([^/]+)\/([^/]+)/i);
|
|
138
52
|
if (!githubMatch) {
|
|
139
|
-
console.log(picocolors_1.default.red('Error: Only GitHub registries
|
|
53
|
+
console.log(picocolors_1.default.red('Error: Only GitHub registries supported.'));
|
|
140
54
|
return [];
|
|
141
55
|
}
|
|
142
56
|
const [owner, repo] = githubMatch.slice(1);
|
|
143
|
-
// We iterate categories individually to handle versioning (different refs)
|
|
144
57
|
for (const category of categories) {
|
|
145
|
-
const
|
|
146
|
-
const ref =
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
console.log(picocolors_1.default.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
const treeData = (await treeRes.json());
|
|
159
|
-
const allFiles = treeData.tree || [];
|
|
160
|
-
// Find all skill folder names first
|
|
161
|
-
const skillFolders = Array.from(new Set(allFiles
|
|
162
|
-
.filter((f) => f.path.startsWith(`skills/${category}/`))
|
|
163
|
-
.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}/`)) {
|
|
164
70
|
const parts = f.path.split('/');
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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)`));
|
|
203
114
|
}
|
|
204
115
|
}
|
|
205
|
-
|
|
206
|
-
console.log(picocolors_1.default.red(`Error: Failed to fetch remote skills for ${category}. ${error}`));
|
|
207
|
-
}
|
|
208
|
-
} // End category loop
|
|
116
|
+
}
|
|
209
117
|
return collected;
|
|
210
118
|
}
|
|
211
119
|
async writeToTargets(skills, config) {
|
|
@@ -217,16 +125,12 @@ class SyncCommand {
|
|
|
217
125
|
continue;
|
|
218
126
|
const basePath = agentDef.path;
|
|
219
127
|
if (basePath) {
|
|
220
|
-
// We do NOT use emptyDir here because we want to preserve user's own custom skills
|
|
221
128
|
await fs_extra_1.default.ensureDir(basePath);
|
|
222
129
|
for (const skill of skills) {
|
|
223
|
-
// Use nested structure: category/skill (e.g. flutter/bloc)
|
|
224
130
|
const skillPath = path_1.default.join(basePath, skill.category, skill.skill);
|
|
225
131
|
await fs_extra_1.default.ensureDir(skillPath);
|
|
226
132
|
for (const fileItem of skill.files) {
|
|
227
133
|
const targetFilePath = path_1.default.join(skillPath, fileItem.name);
|
|
228
|
-
// Check if this specific file is in custom_overrides
|
|
229
|
-
// Supports both specific files and entire folders
|
|
230
134
|
const relativePath = path_1.default
|
|
231
135
|
.relative(process.cwd(), targetFilePath)
|
|
232
136
|
.replace(/\\/g, '/');
|
|
@@ -236,7 +140,14 @@ class SyncCommand {
|
|
|
236
140
|
relativePath.startsWith(`${overridePath.replace(/\/$/, '')}/`));
|
|
237
141
|
});
|
|
238
142
|
if (isOverridden) {
|
|
239
|
-
console.log(picocolors_1.default.yellow(` ⚠️ Skipping overridden
|
|
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}`));
|
|
240
151
|
continue;
|
|
241
152
|
}
|
|
242
153
|
await fs_extra_1.default.outputFile(targetFilePath, fileItem.content);
|