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.
Files changed (2) hide show
  1. package/package.json +30 -0
  2. 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);