skill-os 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +30 -0
- package/skill-os.js +822 -0
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "skill-os",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Skill-OS CLI for managing OS skills",
|
|
5
|
+
"main": "skill-os.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"skill-os": "skill-os.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"cli",
|
|
14
|
+
"skills",
|
|
15
|
+
"os-copilot"
|
|
16
|
+
],
|
|
17
|
+
"author": "OS-Copilot Team",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"chalk": "^4.1.2",
|
|
21
|
+
"commander": "^13.1.0",
|
|
22
|
+
"inquirer": "^8.2.6",
|
|
23
|
+
"js-yaml": "^4.1.0",
|
|
24
|
+
"jsonschema": "^1.4.1",
|
|
25
|
+
"node-fetch": "^2.7.0"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=14.0.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/skill-os.js
ADDED
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require('commander');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const yaml = require('js-yaml');
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const inquirer = require('inquirer');
|
|
9
|
+
const { execSync } = require('child_process');
|
|
10
|
+
|
|
11
|
+
// Define specific layer icons matching original python CLI
|
|
12
|
+
const layerIcons = {
|
|
13
|
+
core: "🧠",
|
|
14
|
+
system: "🔧",
|
|
15
|
+
runtime: "📦",
|
|
16
|
+
application: "🚀",
|
|
17
|
+
cloud: "☁️",
|
|
18
|
+
package: "📦",
|
|
19
|
+
network: "🌐",
|
|
20
|
+
storage: "💾",
|
|
21
|
+
user: "👤",
|
|
22
|
+
meta: "🛠️",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function formatLayerIcon(skillPath) {
|
|
26
|
+
const layer = skillPath.split("/")[0];
|
|
27
|
+
return layerIcons[layer] || "📄";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatStatus(status) {
|
|
31
|
+
switch (status?.toLowerCase()) {
|
|
32
|
+
case 'stable': return chalk.green(status);
|
|
33
|
+
case 'beta': return chalk.yellow(status);
|
|
34
|
+
case 'placeholder': return chalk.dim(status);
|
|
35
|
+
default: return status || 'unknown';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getRepoRoot() {
|
|
40
|
+
let current = path.dirname(__dirname); // Usually cli is in the repo root
|
|
41
|
+
if (fs.existsSync(path.join(current, 'SKILL_INDEX.json'))) {
|
|
42
|
+
return current;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (fs.existsSync(path.join(process.cwd(), 'SKILL_INDEX.json'))) {
|
|
46
|
+
return process.cwd();
|
|
47
|
+
}
|
|
48
|
+
return current;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function loadIndex() {
|
|
52
|
+
const repoRoot = getRepoRoot();
|
|
53
|
+
const indexPath = path.join(repoRoot, 'SKILL_INDEX.json');
|
|
54
|
+
|
|
55
|
+
if (!fs.existsSync(indexPath)) {
|
|
56
|
+
console.error(chalk.red('Error: SKILL_INDEX.json not found'));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const raw = fs.readFileSync(indexPath, 'utf-8');
|
|
61
|
+
return JSON.parse(raw);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ----------------------------------------------------------------------
|
|
65
|
+
// Commmands Implementations
|
|
66
|
+
// ----------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
function cmdList() {
|
|
69
|
+
const index = loadIndex();
|
|
70
|
+
const skills = index.skills || {};
|
|
71
|
+
|
|
72
|
+
console.log(`\n${chalk.bold('📚 Skill-OS Available Skills')}`);
|
|
73
|
+
console.log(`${chalk.dim('─'.repeat(60))}\n`);
|
|
74
|
+
|
|
75
|
+
const layers = {};
|
|
76
|
+
for (const [skillPath, info] of Object.entries(skills)) {
|
|
77
|
+
const layer = skillPath.split('/')[0];
|
|
78
|
+
if (!layers[layer]) layers[layer] = [];
|
|
79
|
+
layers[layer].push({ path: skillPath, info });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const layer of Object.keys(layers).sort()) {
|
|
83
|
+
const icon = layerIcons[layer] || "📄";
|
|
84
|
+
console.log(`${chalk.bold(icon + ' ' + layer.toUpperCase())}`);
|
|
85
|
+
|
|
86
|
+
const sortedSkills = layers[layer].sort((a, b) => a.path.localeCompare(b.path));
|
|
87
|
+
|
|
88
|
+
for (const { path: skillPath, info } of sortedSkills) {
|
|
89
|
+
const status = formatStatus(info.status);
|
|
90
|
+
const name = info.name || skillPath;
|
|
91
|
+
const version = info.version || '?';
|
|
92
|
+
const desc = (info.description || '').substring(0, 50);
|
|
93
|
+
|
|
94
|
+
console.log(` ${chalk.cyan(skillPath)}`);
|
|
95
|
+
console.log(` ${name} (${version}) [${status}]`);
|
|
96
|
+
console.log(` ${chalk.dim(desc + '...')}`);
|
|
97
|
+
}
|
|
98
|
+
console.log();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function cmdSearch(query) {
|
|
103
|
+
const lowercaseQuery = query.toLowerCase();
|
|
104
|
+
const index = loadIndex();
|
|
105
|
+
const skills = index.skills || {};
|
|
106
|
+
|
|
107
|
+
const matches = [];
|
|
108
|
+
for (const [skillPath, info] of Object.entries(skills)) {
|
|
109
|
+
const searchable = `${skillPath} ${info.name || ''} ${info.description || ''}`.toLowerCase();
|
|
110
|
+
if (searchable.includes(lowercaseQuery)) {
|
|
111
|
+
matches.push({ path: skillPath, info });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (matches.length === 0) {
|
|
116
|
+
console.log(chalk.yellow(`No skills found matching '${query}'`));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log(`\n${chalk.bold(`🔍 Search Results for '${query}'`)}`);
|
|
121
|
+
console.log(`${chalk.dim(`Found ${matches.length} skill(s)`)}\n`);
|
|
122
|
+
|
|
123
|
+
for (const { path: skillPath, info } of matches) {
|
|
124
|
+
const icon = formatLayerIcon(skillPath);
|
|
125
|
+
const status = formatStatus(info.status);
|
|
126
|
+
const name = info.name || skillPath;
|
|
127
|
+
const version = info.version || '?';
|
|
128
|
+
const desc = info.description || '';
|
|
129
|
+
|
|
130
|
+
console.log(`${icon} ${chalk.cyan(skillPath)}`);
|
|
131
|
+
console.log(` ${chalk.bold(name)} (${version}) [${status}]`);
|
|
132
|
+
console.log(` ${desc}\n`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function cmdInfo(skillPath) {
|
|
137
|
+
const index = loadIndex();
|
|
138
|
+
const skills = index.skills || {};
|
|
139
|
+
|
|
140
|
+
if (!skills[skillPath]) {
|
|
141
|
+
console.error(chalk.red(`Error: Skill '${skillPath}' not found`));
|
|
142
|
+
console.log(chalk.dim("Use 'skill-os list' to see available skills"));
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const info = skills[skillPath];
|
|
147
|
+
const icon = formatLayerIcon(skillPath);
|
|
148
|
+
const status = formatStatus(info.status);
|
|
149
|
+
|
|
150
|
+
console.log(`\n${icon} ${chalk.bold(info.name || skillPath)}`);
|
|
151
|
+
console.log(chalk.dim('─'.repeat(40)));
|
|
152
|
+
console.log(` Path: ${chalk.cyan(skillPath)}`);
|
|
153
|
+
console.log(` Version: ${info.version || 'unknown'}`);
|
|
154
|
+
console.log(` Status: ${status}`);
|
|
155
|
+
console.log(` Description: ${info.description || 'No description'}`);
|
|
156
|
+
|
|
157
|
+
if (info.dependencies && info.dependencies.length > 0) {
|
|
158
|
+
console.log(` Dependencies: ${info.dependencies.join(', ')}`);
|
|
159
|
+
} else {
|
|
160
|
+
console.log(` Dependencies: ${chalk.dim('None')}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const repoRoot = getRepoRoot();
|
|
164
|
+
const skillMdPath = path.join(repoRoot, skillPath, 'SKILL.md');
|
|
165
|
+
if (fs.existsSync(skillMdPath)) {
|
|
166
|
+
console.log(`\n ${chalk.dim(`SKILL.md: ${skillMdPath}`)}`);
|
|
167
|
+
}
|
|
168
|
+
console.log();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function cmdInstall(skillPath, options) {
|
|
172
|
+
const index = loadIndex();
|
|
173
|
+
const skills = index.skills || {};
|
|
174
|
+
|
|
175
|
+
// Expand ~ relative to HOME
|
|
176
|
+
const homeDir = require('os').homedir();
|
|
177
|
+
const rawTarget = options.target.startsWith('~/') ?
|
|
178
|
+
path.join(homeDir, options.target.slice(2)) :
|
|
179
|
+
path.resolve(options.target);
|
|
180
|
+
|
|
181
|
+
const targetDir = path.resolve(rawTarget);
|
|
182
|
+
|
|
183
|
+
if (!skills[skillPath]) {
|
|
184
|
+
console.error(chalk.red(`Error: Skill '${skillPath}' not found`));
|
|
185
|
+
console.log(chalk.dim("Use 'skill-os list' to see available skills"));
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const info = skills[skillPath];
|
|
190
|
+
const repoRoot = getRepoRoot();
|
|
191
|
+
const sourceDir = path.join(repoRoot, skillPath);
|
|
192
|
+
|
|
193
|
+
if (!fs.existsSync(sourceDir)) {
|
|
194
|
+
console.error(chalk.red(`Error: Skill directory not found: ${sourceDir}`));
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const skillName = path.basename(skillPath);
|
|
199
|
+
const installTarget = path.join(targetDir, skillName);
|
|
200
|
+
|
|
201
|
+
if (fs.existsSync(installTarget)) {
|
|
202
|
+
if (options.force) {
|
|
203
|
+
console.log(chalk.yellow(`Removing existing: ${installTarget}`));
|
|
204
|
+
fs.rmSync(installTarget, { recursive: true, force: true });
|
|
205
|
+
} else {
|
|
206
|
+
console.error(chalk.red(`Error: Target already exists: ${installTarget}`));
|
|
207
|
+
console.log(chalk.dim("Use --force to overwrite"));
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
213
|
+
|
|
214
|
+
console.log(`\n${chalk.bold(`📥 Installing ${info.name || skillPath}...`)}`);
|
|
215
|
+
console.log(` Source: ${chalk.cyan(sourceDir)}`);
|
|
216
|
+
console.log(` Target: ${chalk.cyan(installTarget)}`);
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
// Simple recursive copy
|
|
220
|
+
fs.cpSync(sourceDir, installTarget, { recursive: true });
|
|
221
|
+
console.log(`\n${chalk.green('✓ Successfully installed!')}`);
|
|
222
|
+
|
|
223
|
+
console.log(`\n${chalk.bold('📋 Usage:')}`);
|
|
224
|
+
console.log(` SKILL.md: ${installTarget}/SKILL.md`);
|
|
225
|
+
|
|
226
|
+
const deps = info.dependencies || [];
|
|
227
|
+
if (deps.length > 0) {
|
|
228
|
+
console.log(`\n${chalk.yellow('⚠️ Dependencies required:')}`);
|
|
229
|
+
deps.forEach(dep => console.log(` - ${dep}`));
|
|
230
|
+
console.log(`\n Install with: ${chalk.dim(`pip install ${deps.join(' ')}`)}`);
|
|
231
|
+
}
|
|
232
|
+
} catch (e) {
|
|
233
|
+
console.error(chalk.red(`Error during installation: ${e.message}`));
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function cmdCreate(skillPath, options) {
|
|
239
|
+
const repoRoot = getRepoRoot();
|
|
240
|
+
const targetDir = path.join(repoRoot, 'skills', skillPath);
|
|
241
|
+
|
|
242
|
+
if (fs.existsSync(targetDir)) {
|
|
243
|
+
if (!options.force) {
|
|
244
|
+
console.error(chalk.red(`Error: Directory already exists: ${targetDir}`));
|
|
245
|
+
console.log(chalk.dim("Use --force to overwrite"));
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
252
|
+
|
|
253
|
+
const parts = skillPath.split('/');
|
|
254
|
+
if (parts.length < 3) {
|
|
255
|
+
console.error(chalk.red("Error: Invalid path format"));
|
|
256
|
+
console.log(chalk.dim("Expected: <layer>/<category>/<skill>"));
|
|
257
|
+
console.log(chalk.dim("Example: system/security/cve-repair"));
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const layer = parts[0];
|
|
262
|
+
const category = parts[1];
|
|
263
|
+
const skillName = parts[parts.length - 1];
|
|
264
|
+
|
|
265
|
+
const validLayers = ["core", "system", "runtime", "application"];
|
|
266
|
+
if (!validLayers.includes(layer)) {
|
|
267
|
+
console.log(chalk.yellow(`Warning: Layer '${layer}' not in spec`));
|
|
268
|
+
console.log(chalk.dim(`Valid layers: ${validLayers.join(', ')}`));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const titleCaseName = skillName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
272
|
+
|
|
273
|
+
const skillMdContent = `---
|
|
274
|
+
name: ${skillName}
|
|
275
|
+
version: 0.1.0
|
|
276
|
+
description: TODO: Add skill description here
|
|
277
|
+
author: Your Name
|
|
278
|
+
|
|
279
|
+
layer: ${layer}
|
|
280
|
+
category: ${category}
|
|
281
|
+
lifecycle: usage # production | maintenance | operations | usage | meta
|
|
282
|
+
|
|
283
|
+
# Tags: first tag MUST be the category name
|
|
284
|
+
tags:
|
|
285
|
+
- ${category}
|
|
286
|
+
- TODO
|
|
287
|
+
|
|
288
|
+
status: placeholder
|
|
289
|
+
dependencies: []
|
|
290
|
+
|
|
291
|
+
# Optional: Permissions (for security classification)
|
|
292
|
+
# permissions:
|
|
293
|
+
# requires_root: false
|
|
294
|
+
# dangerous_operations: []
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
# ${titleCaseName}
|
|
298
|
+
|
|
299
|
+
TODO: Add skill documentation here.
|
|
300
|
+
|
|
301
|
+
## 能力概览
|
|
302
|
+
|
|
303
|
+
- TODO: List capabilities
|
|
304
|
+
|
|
305
|
+
## 使用示例
|
|
306
|
+
|
|
307
|
+
\`\`\`bash
|
|
308
|
+
# TODO: Add usage examples
|
|
309
|
+
\`\`\`
|
|
310
|
+
|
|
311
|
+
## TODO
|
|
312
|
+
|
|
313
|
+
- [ ] Implement core functionality
|
|
314
|
+
- [ ] Add scripts if needed
|
|
315
|
+
- [ ] Run: skill-os validate ${skillPath}
|
|
316
|
+
- [ ] Run: skill-os sync ${skillPath}
|
|
317
|
+
`;
|
|
318
|
+
|
|
319
|
+
const skillMdPath = path.join(targetDir, "SKILL.md");
|
|
320
|
+
fs.writeFileSync(skillMdPath, skillMdContent, 'utf-8');
|
|
321
|
+
|
|
322
|
+
console.log(`\n${chalk.green('✓ Created skill scaffold:')}`);
|
|
323
|
+
console.log(` ${chalk.cyan(targetDir)}`);
|
|
324
|
+
console.log(" └── SKILL.md");
|
|
325
|
+
|
|
326
|
+
console.log(`\n${chalk.bold('📋 Next Steps:')}`);
|
|
327
|
+
console.log(` 1. Edit ${chalk.cyan(skillMdPath)}`);
|
|
328
|
+
console.log(` 2. Validate: ${chalk.dim(`skill-os validate ${skillPath}`)}`);
|
|
329
|
+
console.log(` 3. Sync: ${chalk.dim(`skill-os sync ${skillPath}`)}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function cmdDownload(skillName, options) {
|
|
333
|
+
const targetDir = options.platform
|
|
334
|
+
? path.join(process.cwd(), `.${options.platform}`, "skills")
|
|
335
|
+
: process.cwd();
|
|
336
|
+
|
|
337
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
338
|
+
|
|
339
|
+
let serverUrl = options.url || process.env.SKILL_OS_REGISTRY || "https://example.com/skills";
|
|
340
|
+
serverUrl = serverUrl.replace(/\/$/, ""); // Trim trailing slash
|
|
341
|
+
|
|
342
|
+
const downloadUrl = `${serverUrl}/${skillName}.tar.gz`;
|
|
343
|
+
|
|
344
|
+
console.log(`\n${chalk.bold(`📥 Downloading ${skillName}...`)}`);
|
|
345
|
+
console.log(` Source: ${chalk.cyan(downloadUrl)}`);
|
|
346
|
+
console.log(` Target: ${chalk.cyan(targetDir)}\n`);
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
// Using curl synchronously for simplicity, similar to the bash version
|
|
350
|
+
execSync(`curl -L -O ${downloadUrl}`, { cwd: targetDir, stdio: 'inherit' });
|
|
351
|
+
console.log(`\n${chalk.green(`✓ Successfully downloaded ${skillName} to ${targetDir}`)}`);
|
|
352
|
+
} catch (e) {
|
|
353
|
+
console.error(`\n${chalk.red(`✗ Failed to download ${skillName}: command returned error code ${e.status}`)}`);
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ----------------------------------------------------------------------
|
|
359
|
+
// YAML / Sync Helpers
|
|
360
|
+
// ----------------------------------------------------------------------
|
|
361
|
+
|
|
362
|
+
function parseSkillMdFrontmatter(skillMdPath) {
|
|
363
|
+
if (!fs.existsSync(skillMdPath)) return {};
|
|
364
|
+
const content = fs.readFileSync(skillMdPath, 'utf8');
|
|
365
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
366
|
+
if (!match) return {};
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
return yaml.load(match[1]) || {};
|
|
370
|
+
} catch (e) {
|
|
371
|
+
return {};
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function loadYamlIndex() {
|
|
376
|
+
const repoRoot = getRepoRoot();
|
|
377
|
+
const yamlPath = path.join(repoRoot, 'SKILL_INDEX.yaml');
|
|
378
|
+
if (!fs.existsSync(yamlPath)) {
|
|
379
|
+
console.error(chalk.red('Error: SKILL_INDEX.yaml not found'));
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
const raw = fs.readFileSync(yamlPath, 'utf8');
|
|
383
|
+
return { data: yaml.load(raw), yamlPath };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function syncToJson(repoRoot) {
|
|
387
|
+
const yamlPath = path.join(repoRoot, 'SKILL_INDEX.yaml');
|
|
388
|
+
const jsonPath = path.join(repoRoot, 'SKILL_INDEX.json');
|
|
389
|
+
|
|
390
|
+
const data = yaml.load(fs.readFileSync(yamlPath, 'utf8'));
|
|
391
|
+
data.generated_at = new Date().toISOString();
|
|
392
|
+
|
|
393
|
+
fs.writeFileSync(jsonPath, JSON.stringify(data, null, 2), 'utf8');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function appendSkillToYaml(skillPath, skillEntry, yamlPath) {
|
|
397
|
+
let content = fs.readFileSync(yamlPath, 'utf8');
|
|
398
|
+
const escapedPath = skillPath.replace(/[.*+?^$\\{}()|[\\]\\\\]/g, '\\\\$&');
|
|
399
|
+
|
|
400
|
+
// JS Regex to match existing block (simplified approximation matching python's pattern)
|
|
401
|
+
// Find " skill/path:" and everything indented under it until the next line that isn't indented by at least 4 spaces
|
|
402
|
+
const pattern = new RegExp(`(^|\\n) ${escapedPath}:\\n(?: .*\\n)*`);
|
|
403
|
+
|
|
404
|
+
let newBlock = `
|
|
405
|
+
${skillPath}:
|
|
406
|
+
name: ${skillEntry.name}
|
|
407
|
+
version: "${skillEntry.version}"
|
|
408
|
+
description: ${skillEntry.description}
|
|
409
|
+
status: ${skillEntry.status}
|
|
410
|
+
layer: ${skillEntry.layer}
|
|
411
|
+
lifecycle: ${skillEntry.lifecycle}
|
|
412
|
+
category: ${skillEntry.category}
|
|
413
|
+
tags:
|
|
414
|
+
`;
|
|
415
|
+
const tags = skillEntry.tags || [];
|
|
416
|
+
if (tags.length) {
|
|
417
|
+
tags.forEach(t => newBlock += ` - ${t}\n`);
|
|
418
|
+
} else {
|
|
419
|
+
newBlock += ` tags: []\n`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const deps = skillEntry.dependencies || [];
|
|
423
|
+
if (deps.length) {
|
|
424
|
+
newBlock += ` dependencies:\n`;
|
|
425
|
+
deps.forEach(d => newBlock += ` - ${d}\n`);
|
|
426
|
+
} else {
|
|
427
|
+
newBlock += ` dependencies: []\n`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (pattern.test(content)) {
|
|
431
|
+
content = content.replace(pattern, `$1${newBlock.trimStart()}`);
|
|
432
|
+
} else {
|
|
433
|
+
content = content.trimEnd() + '\n' + newBlock;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
fs.writeFileSync(yamlPath, content, 'utf8');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function cmdSync(skillPath) {
|
|
440
|
+
const repoRoot = getRepoRoot();
|
|
441
|
+
const skillDir = path.join(repoRoot, 'skills', skillPath);
|
|
442
|
+
|
|
443
|
+
if (!fs.existsSync(skillDir)) {
|
|
444
|
+
console.error(chalk.red(`Error: Skill directory not found: ${skillDir}`));
|
|
445
|
+
console.log(chalk.dim(`Create it first with: skill-os create ${skillPath}`));
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
450
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
451
|
+
console.error(chalk.red(`Error: SKILL.md not found in ${skillDir}`));
|
|
452
|
+
process.exit(1);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
console.log(`\n${chalk.bold('📝 Skill-OS Sync')}`);
|
|
456
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
457
|
+
console.log(`Skill path: ${chalk.cyan(skillPath)}\n`);
|
|
458
|
+
|
|
459
|
+
const fm = parseSkillMdFrontmatter(skillMdPath);
|
|
460
|
+
const { data: indexData, yamlPath } = loadYamlIndex();
|
|
461
|
+
const skills = indexData.skills || {};
|
|
462
|
+
const existing = skills[skillPath] || {};
|
|
463
|
+
const isUpdate = !!existing.name;
|
|
464
|
+
|
|
465
|
+
if (isUpdate) {
|
|
466
|
+
console.log(`${chalk.yellow('⚠ Skill already registered, updating...')}\n`);
|
|
467
|
+
} else {
|
|
468
|
+
console.log(`${chalk.green('✨ Registering new skill...')}\n`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
console.log(`${chalk.bold('Please confirm or enter skill metadata:')}\n`);
|
|
472
|
+
|
|
473
|
+
const pathParts = skillPath.split('/');
|
|
474
|
+
const inferredCategory = pathParts.length >= 2 ? pathParts[1] : '';
|
|
475
|
+
|
|
476
|
+
const questions = [
|
|
477
|
+
{ name: 'name', message: ' name', default: existing.name || fm.name || pathParts[pathParts.length - 1] },
|
|
478
|
+
{ name: 'description', message: ' description', default: existing.description || fm.description || '' },
|
|
479
|
+
{ name: 'version', message: ' version', default: existing.version || fm.version || '1.0.0' },
|
|
480
|
+
{ name: 'status', message: ` status ${chalk.dim('(stable, beta, placeholder)')}`, default: existing.status || fm.status || 'beta' },
|
|
481
|
+
{ name: 'dependencies', message: ` dependencies ${chalk.dim('(comma-separated)')}`, default: (existing.dependencies || fm.dependencies || []).join(', ') }
|
|
482
|
+
];
|
|
483
|
+
|
|
484
|
+
const answers = await inquirer.prompt(questions);
|
|
485
|
+
|
|
486
|
+
const depsArr = answers.dependencies.split(',').map(d => d.trim()).filter(Boolean);
|
|
487
|
+
const category = existing.category || fm.category || inferredCategory;
|
|
488
|
+
let tags = existing.tags || fm.tags || [];
|
|
489
|
+
|
|
490
|
+
if (category && (!tags.length || tags[0] !== category)) {
|
|
491
|
+
tags = [category, ...tags.filter(t => t !== category)];
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const skillEntry = {
|
|
495
|
+
name: answers.name,
|
|
496
|
+
version: answers.version,
|
|
497
|
+
description: answers.description,
|
|
498
|
+
status: answers.status,
|
|
499
|
+
layer: existing.layer || fm.layer || pathParts[0],
|
|
500
|
+
lifecycle: existing.lifecycle || fm.lifecycle || 'usage',
|
|
501
|
+
category: category,
|
|
502
|
+
tags: tags,
|
|
503
|
+
dependencies: depsArr
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
console.log(`\n${chalk.bold('📋 Summary:')}`);
|
|
507
|
+
console.log(` ${chalk.cyan(skillPath)}:`);
|
|
508
|
+
for (const [k, v] of Object.entries(skillEntry)) {
|
|
509
|
+
console.log(` ${k}: ${Array.isArray(v) ? (v.length ? JSON.stringify(v) : '[]') : v}`);
|
|
510
|
+
}
|
|
511
|
+
console.log();
|
|
512
|
+
|
|
513
|
+
const { confirm } = await inquirer.prompt([{
|
|
514
|
+
type: 'confirm',
|
|
515
|
+
name: 'confirm',
|
|
516
|
+
message: 'Confirm and save?',
|
|
517
|
+
default: true
|
|
518
|
+
}]);
|
|
519
|
+
|
|
520
|
+
if (!confirm) {
|
|
521
|
+
console.log(chalk.yellow('Cancelled'));
|
|
522
|
+
process.exit(0);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
console.log(`\n${chalk.dim('Saving SKILL_INDEX.yaml...')}`);
|
|
526
|
+
appendSkillToYaml(skillPath, skillEntry, yamlPath);
|
|
527
|
+
|
|
528
|
+
console.log(chalk.dim('Syncing to SKILL_INDEX.json...'));
|
|
529
|
+
syncToJson(repoRoot);
|
|
530
|
+
|
|
531
|
+
console.log(`\n${chalk.green(`✅ Skill ${isUpdate ? 'updated' : 'registered'} successfully!`)}`);
|
|
532
|
+
console.log(`\n${chalk.bold('📋 Next steps:')}`);
|
|
533
|
+
console.log(" git add SKILL_INDEX.yaml SKILL_INDEX.json");
|
|
534
|
+
console.log(` git commit -m 'feat: ${isUpdate ? 'update' : 'add'} ${skillPath} skill'`);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function findSkillMds(dir, fileList = []) {
|
|
538
|
+
const files = fs.readdirSync(dir);
|
|
539
|
+
for (const file of files) {
|
|
540
|
+
const stat = fs.statSync(path.join(dir, file));
|
|
541
|
+
if (stat.isDirectory()) {
|
|
542
|
+
findSkillMds(path.join(dir, file), fileList);
|
|
543
|
+
} else if (file === 'SKILL.md') {
|
|
544
|
+
fileList.push(path.join(dir, file));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return fileList;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function cmdSyncAll(options) {
|
|
551
|
+
const repoRoot = getRepoRoot();
|
|
552
|
+
const skillsDir = path.join(repoRoot, 'skills');
|
|
553
|
+
|
|
554
|
+
console.log(`\n${chalk.bold('🔄 Skill-OS Sync All')}`);
|
|
555
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
556
|
+
console.log(`Scanning: ${chalk.cyan(skillsDir)}\n`);
|
|
557
|
+
|
|
558
|
+
const skillMds = findSkillMds(skillsDir);
|
|
559
|
+
if (skillMds.length === 0) {
|
|
560
|
+
console.log(chalk.yellow('No SKILL.md files found'));
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
console.log(`Found ${chalk.green(skillMds.length)} skills\n`);
|
|
565
|
+
|
|
566
|
+
const newSkills = {};
|
|
567
|
+
const errors = [];
|
|
568
|
+
|
|
569
|
+
skillMds.sort().forEach(skillMd => {
|
|
570
|
+
const relPath = path.relative(skillsDir, path.dirname(skillMd));
|
|
571
|
+
const fm = parseSkillMdFrontmatter(skillMd);
|
|
572
|
+
|
|
573
|
+
if (!fm || Object.keys(fm).length === 0) {
|
|
574
|
+
errors.push(` ⚠ ${relPath}: Failed to parse frontmatter`);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
let status = fm.status || 'beta';
|
|
579
|
+
let icon = '✓';
|
|
580
|
+
if (status === 'placeholder') {
|
|
581
|
+
if (!options.includePlaceholder) {
|
|
582
|
+
status = 'beta'; // Default placeholder -> beta
|
|
583
|
+
}
|
|
584
|
+
icon = '🔸';
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const name = fm.name || path.basename(path.dirname(skillMd));
|
|
588
|
+
const layer = fm.layer || relPath.split('/')[0];
|
|
589
|
+
const category = fm.category || (relPath.includes('/') ? relPath.split('/')[1] : '');
|
|
590
|
+
let tags = fm.tags || [];
|
|
591
|
+
|
|
592
|
+
if (tags && category && tags[0] !== category) {
|
|
593
|
+
tags = [category, ...tags.filter(t => t !== category)];
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
let description = fm.description || '';
|
|
597
|
+
if (description.length > 200) description = description.substring(0, 200);
|
|
598
|
+
|
|
599
|
+
newSkills[relPath] = {
|
|
600
|
+
name,
|
|
601
|
+
version: fm.version || '0.1.0',
|
|
602
|
+
description,
|
|
603
|
+
status,
|
|
604
|
+
layer,
|
|
605
|
+
lifecycle: fm.lifecycle || 'usage',
|
|
606
|
+
category,
|
|
607
|
+
tags: tags.slice(0, 5),
|
|
608
|
+
dependencies: fm.dependencies || []
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
console.log(` ${icon} ${chalk.cyan(relPath)} (${layer}/${newSkills[relPath].lifecycle})`);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
if (errors.length) {
|
|
615
|
+
console.log(`\n${chalk.yellow('Warnings:')}`);
|
|
616
|
+
errors.forEach(e => console.log(e));
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const { data: indexData, yamlPath } = loadYamlIndex();
|
|
620
|
+
indexData.skills = newSkills;
|
|
621
|
+
indexData.version = "3.0";
|
|
622
|
+
|
|
623
|
+
const yamlDump = yaml.dump(indexData, {
|
|
624
|
+
sortKeys: false,
|
|
625
|
+
noCompatMode: true
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
const header = `# Skill-OS 技能索引
|
|
629
|
+
# ====================
|
|
630
|
+
# Generated: ${new Date().toISOString()}
|
|
631
|
+
# Layers: core | system | runtime | application | meta
|
|
632
|
+
# Lifecycle: production | maintenance | operations | usage | meta
|
|
633
|
+
|
|
634
|
+
`;
|
|
635
|
+
|
|
636
|
+
fs.writeFileSync(yamlPath, header + yamlDump, 'utf8');
|
|
637
|
+
console.log(`\n${chalk.green(`✓ Updated ${path.basename(yamlPath)}`)}`);
|
|
638
|
+
|
|
639
|
+
syncToJson(repoRoot);
|
|
640
|
+
console.log(`${chalk.green(`✓ Updated SKILL_INDEX.json`)}`);
|
|
641
|
+
|
|
642
|
+
console.log(`\n${chalk.bold('📊 Summary:')}`);
|
|
643
|
+
console.log(` Total skills: ${Object.keys(newSkills).length}`);
|
|
644
|
+
|
|
645
|
+
const layerCounts = {};
|
|
646
|
+
for (const info of Object.values(newSkills)) {
|
|
647
|
+
const layer = info.layer || 'unknown';
|
|
648
|
+
layerCounts[layer] = (layerCounts[layer] || 0) + 1;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
for (const layer of Object.keys(layerCounts).sort()) {
|
|
652
|
+
console.log(` ${formatLayerIcon(layer + '/')} ${layer}: ${layerCounts[layer]}`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// ----------------------------------------------------------------------
|
|
657
|
+
// Validation Helpers
|
|
658
|
+
// ----------------------------------------------------------------------
|
|
659
|
+
|
|
660
|
+
const { Validator } = require('jsonschema');
|
|
661
|
+
|
|
662
|
+
const skillSchema = {
|
|
663
|
+
id: "/SkillMetadata",
|
|
664
|
+
type: "object",
|
|
665
|
+
properties: {
|
|
666
|
+
name: { type: "string", minLength: 1 },
|
|
667
|
+
version: { type: ["string", "number"] },
|
|
668
|
+
description: { type: "string" },
|
|
669
|
+
layer: { enum: ["core", "system", "runtime", "application"] },
|
|
670
|
+
lifecycle: { enum: ["production", "maintenance", "operations", "usage", "meta"] },
|
|
671
|
+
category: { type: ["string", "null"] },
|
|
672
|
+
tags: { type: "array", items: { type: "string" } },
|
|
673
|
+
status: { enum: ["stable", "beta", "placeholder"] },
|
|
674
|
+
dependencies: { type: "array", items: { type: "string" } },
|
|
675
|
+
permissions: {
|
|
676
|
+
type: "object",
|
|
677
|
+
properties: {
|
|
678
|
+
requires_root: { type: "boolean" },
|
|
679
|
+
dangerous_operations: { type: "array", items: { type: "string" } }
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
},
|
|
683
|
+
required: ["name", "version", "layer", "lifecycle", "status"]
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
function validateDirectoryStructure(skillDir) {
|
|
687
|
+
const errors = [];
|
|
688
|
+
if (!fs.existsSync(skillDir)) {
|
|
689
|
+
return { isValid: false, errors: ["Directory does not exist"] };
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const skillMd = path.join(skillDir, 'SKILL.md');
|
|
693
|
+
if (!fs.existsSync(skillMd)) {
|
|
694
|
+
errors.push("Missing SKILL.md file");
|
|
695
|
+
} else {
|
|
696
|
+
const content = fs.readFileSync(skillMd, 'utf8');
|
|
697
|
+
if (!content.includes('---')) {
|
|
698
|
+
errors.push("SKILL.md missing frontmatter");
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return { isValid: errors.length === 0, errors };
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function cmdValidate(skillPath) {
|
|
706
|
+
const repoRoot = getRepoRoot();
|
|
707
|
+
const skillDir = path.join(repoRoot, 'skills', skillPath);
|
|
708
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
709
|
+
|
|
710
|
+
console.log(`\n${chalk.bold(`🔍 Validating Skill: ${skillPath}`)}`);
|
|
711
|
+
console.log(`${chalk.dim('─'.repeat(50))}\n`);
|
|
712
|
+
|
|
713
|
+
const { isValid: isDirValid, errors: dirErrors } = validateDirectoryStructure(skillDir);
|
|
714
|
+
if (isDirValid) {
|
|
715
|
+
console.log(`${chalk.green('✓ Directory structure valid')}`);
|
|
716
|
+
} else {
|
|
717
|
+
console.log(`${chalk.red('✗ Directory structure errors:')}`);
|
|
718
|
+
dirErrors.forEach(err => console.log(` - ${err}`));
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
722
|
+
console.log(`${chalk.red('✗ Fatal: SKILL.md not found')}`);
|
|
723
|
+
process.exit(1);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const fm = parseSkillMdFrontmatter(skillMdPath);
|
|
727
|
+
|
|
728
|
+
// Check if empty frontmatter
|
|
729
|
+
if (!fm || Object.keys(fm).length === 0) {
|
|
730
|
+
console.log(`${chalk.red('✗ SKILL.md missing or invalid frontmatter')}`);
|
|
731
|
+
process.exit(1);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const v = new Validator();
|
|
735
|
+
const result = v.validate(fm, skillSchema);
|
|
736
|
+
|
|
737
|
+
// Custom tag validation: first tag MUST be category
|
|
738
|
+
if (fm.category && fm.tags && fm.tags.length > 0) {
|
|
739
|
+
if (fm.tags[0] !== fm.category) {
|
|
740
|
+
result.errors.push({ stack: `tags[0] must equal category name '${fm.category}'` });
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (result.valid && result.errors.length === 0) {
|
|
745
|
+
console.log(`${chalk.green('✓ SKILL.md is spec compliant')}`);
|
|
746
|
+
console.log(`\n${chalk.bold('📋 Parsed Metadata:')}`);
|
|
747
|
+
console.log(` name: ${fm.name}`);
|
|
748
|
+
console.log(` version: ${fm.version}`);
|
|
749
|
+
console.log(` layer: ${fm.layer}`);
|
|
750
|
+
console.log(` lifecycle: ${fm.lifecycle}`);
|
|
751
|
+
console.log(` category: ${fm.category || '(missing)'}`);
|
|
752
|
+
console.log(` tags: ${JSON.stringify(fm.tags || [])}`);
|
|
753
|
+
if (fm.permissions) {
|
|
754
|
+
console.log(` requires_root: ${fm.permissions.requires_root}`);
|
|
755
|
+
}
|
|
756
|
+
} else {
|
|
757
|
+
console.log(`${chalk.red('✗ SKILL.md validation errors:')}`);
|
|
758
|
+
result.errors.forEach(err => console.log(` - ${err.stack.replace('instance.', '')}`));
|
|
759
|
+
process.exit(1);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
console.log(`\n${chalk.green('✅ Validation passed!')}\n`);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// ----------------------------------------------------------------------
|
|
766
|
+
// CLI Setup
|
|
767
|
+
// ----------------------------------------------------------------------
|
|
768
|
+
|
|
769
|
+
program
|
|
770
|
+
.name('skill-os')
|
|
771
|
+
.description('Skill-OS CLI');
|
|
772
|
+
|
|
773
|
+
program.command('list')
|
|
774
|
+
.description('List all available skills')
|
|
775
|
+
.action(cmdList);
|
|
776
|
+
|
|
777
|
+
program.command('search')
|
|
778
|
+
.description('Search for skills')
|
|
779
|
+
.argument('<query>', 'Search query')
|
|
780
|
+
.action(cmdSearch);
|
|
781
|
+
|
|
782
|
+
program.command('info')
|
|
783
|
+
.description('Show skill details')
|
|
784
|
+
.argument('<path>', 'Skill path (e.g., package/package/rpm_search)')
|
|
785
|
+
.action(cmdInfo);
|
|
786
|
+
|
|
787
|
+
program.command('install')
|
|
788
|
+
.description('Install a skill to target directory')
|
|
789
|
+
.argument('<path>', 'Skill path to install')
|
|
790
|
+
.option('-t, --target <dir>', 'Target directory', '~/.skills')
|
|
791
|
+
.option('-f, --force', 'Force overwrite if exists', false)
|
|
792
|
+
.action(cmdInstall);
|
|
793
|
+
|
|
794
|
+
program.command('create')
|
|
795
|
+
.description('Create a new skill scaffold')
|
|
796
|
+
.argument('<path>', 'Skill path to create (e.g., system/migration)')
|
|
797
|
+
.option('-f, --force', 'Force overwrite if exists', false)
|
|
798
|
+
.action(cmdCreate);
|
|
799
|
+
|
|
800
|
+
program.command('download')
|
|
801
|
+
.description('Download a skill package')
|
|
802
|
+
.argument('<skill_name>', 'Name of the skill to download')
|
|
803
|
+
.option('--platform <platform>', 'Specify the platform (e.g., qoder will download to .qoder/skills/)')
|
|
804
|
+
.option('--url <url>', 'Specify the base URL for the registry (defaults to SKILL_OS_REGISTRY env var)')
|
|
805
|
+
.action(cmdDownload);
|
|
806
|
+
|
|
807
|
+
program.command('sync')
|
|
808
|
+
.description('Register or update a skill in the index with interactive prompts')
|
|
809
|
+
.argument('<path>', 'Skill path to sync (e.g., cloud/alicloud-api)')
|
|
810
|
+
.action(cmdSync);
|
|
811
|
+
|
|
812
|
+
program.command('sync-all')
|
|
813
|
+
.description('Regenerate index from all SKILL.md files (one-click sync)')
|
|
814
|
+
.option('--include-placeholder', 'Include placeholder skills in output', false)
|
|
815
|
+
.action(cmdSyncAll);
|
|
816
|
+
|
|
817
|
+
program.command('validate')
|
|
818
|
+
.description('Validate a skill against spec')
|
|
819
|
+
.argument('<path>', 'Skill path to validate (e.g., system/security/cve-repair)')
|
|
820
|
+
.action(cmdValidate);
|
|
821
|
+
|
|
822
|
+
program.parse(process.argv);
|