skill-os 0.1.2 → 0.1.5
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/lib/auth.js +114 -0
- package/lib/config.js +95 -0
- package/lib/local.js +173 -0
- package/lib/remote.js +304 -0
- package/lib/sync.js +190 -0
- package/lib/utils.js +139 -0
- package/package.json +2 -2
- package/skill-os.js +180 -621
package/lib/sync.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本地索引同步命令
|
|
3
|
+
*
|
|
4
|
+
* 扫描 skills/ 目录下所有包含 SKILL.md 的技能目录,
|
|
5
|
+
* 解析 frontmatter 元数据,生成 SKILL_INDEX.json。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
getRepoRoot,
|
|
14
|
+
parseSkillMdFrontmatter,
|
|
15
|
+
skillSchema,
|
|
16
|
+
validateDirectoryStructure,
|
|
17
|
+
Validator,
|
|
18
|
+
} = require('./utils');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 递归扫描目录,找到所有包含 SKILL.md 的叶子目录
|
|
22
|
+
* @param {string} dir - 当前扫描的目录
|
|
23
|
+
* @returns {string[]} - 包含 SKILL.md 的目录绝对路径列表
|
|
24
|
+
*/
|
|
25
|
+
function findSkillDirs(dir) {
|
|
26
|
+
const results = [];
|
|
27
|
+
if (!fs.existsSync(dir)) return results;
|
|
28
|
+
|
|
29
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
30
|
+
|
|
31
|
+
// 如果当前目录包含 SKILL.md,则视为技能目录
|
|
32
|
+
const hasSkillMd = entries.some(e => e.isFile() && e.name === 'SKILL.md');
|
|
33
|
+
if (hasSkillMd) {
|
|
34
|
+
results.push(dir);
|
|
35
|
+
return results; // 不再深入子目录
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 递归扫描子目录
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
41
|
+
results.push(...findSkillDirs(path.join(dir, entry.name)));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return results;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 从目录路径提取相对于 skills/ 的技能路径
|
|
50
|
+
* 例如:/repo/skills/system/security/selinux-manager → system/security/selinux-manager
|
|
51
|
+
*/
|
|
52
|
+
function extractSkillPath(skillDir, skillsRoot) {
|
|
53
|
+
return path.relative(skillsRoot, skillDir).split(path.sep).join('/');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* skill-os sync 命令实现
|
|
58
|
+
*/
|
|
59
|
+
function cmdSync() {
|
|
60
|
+
const repoRoot = getRepoRoot();
|
|
61
|
+
const skillsDir = path.join(repoRoot, 'skills');
|
|
62
|
+
const indexPath = path.join(repoRoot, 'SKILL_INDEX.json');
|
|
63
|
+
|
|
64
|
+
console.log(`\n${chalk.bold('🔄 Syncing skills index...')}`);
|
|
65
|
+
console.log(`${chalk.dim(`Repository: ${repoRoot}`)}`);
|
|
66
|
+
console.log(`${chalk.dim(`Skills dir: ${skillsDir}`)}`);
|
|
67
|
+
console.log(`${chalk.dim('─'.repeat(50))}\n`);
|
|
68
|
+
|
|
69
|
+
if (!fs.existsSync(skillsDir)) {
|
|
70
|
+
console.error(chalk.red('Error: skills/ directory not found'));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 1. 加载旧索引(用于对比)
|
|
75
|
+
let oldSkills = {};
|
|
76
|
+
if (fs.existsSync(indexPath)) {
|
|
77
|
+
try {
|
|
78
|
+
const old = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
|
|
79
|
+
oldSkills = old.skills || {};
|
|
80
|
+
} catch (e) {
|
|
81
|
+
// 忽略解析错误
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 2. 扫描所有技能目录
|
|
86
|
+
const skillDirs = findSkillDirs(skillsDir);
|
|
87
|
+
const newSkills = {};
|
|
88
|
+
const errors = [];
|
|
89
|
+
let validCount = 0;
|
|
90
|
+
let skippedCount = 0;
|
|
91
|
+
|
|
92
|
+
for (const skillDir of skillDirs) {
|
|
93
|
+
const skillPath = extractSkillPath(skillDir, skillsDir);
|
|
94
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
95
|
+
|
|
96
|
+
// 解析 frontmatter
|
|
97
|
+
const fm = parseSkillMdFrontmatter(skillMdPath);
|
|
98
|
+
|
|
99
|
+
if (!fm || !fm.name) {
|
|
100
|
+
errors.push({ path: skillPath, error: 'Missing or invalid frontmatter (no "name" field)' });
|
|
101
|
+
skippedCount++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 基础校验
|
|
106
|
+
const v = new Validator();
|
|
107
|
+
const result = v.validate(fm, skillSchema);
|
|
108
|
+
|
|
109
|
+
if (!result.valid) {
|
|
110
|
+
const msgs = result.errors.map(e => e.stack.replace('instance.', '')).join('; ');
|
|
111
|
+
errors.push({ path: skillPath, error: msgs });
|
|
112
|
+
skippedCount++;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 构建索引条目
|
|
117
|
+
newSkills[skillPath] = {
|
|
118
|
+
name: fm.name,
|
|
119
|
+
version: String(fm.version || '0.1.0'),
|
|
120
|
+
description: fm.description || '',
|
|
121
|
+
status: fm.status || 'stable',
|
|
122
|
+
layer: fm.layer || skillPath.split('/')[0],
|
|
123
|
+
lifecycle: fm.lifecycle || 'usage',
|
|
124
|
+
category: fm.category || (skillPath.split('/').length >= 2 ? skillPath.split('/')[1] : ''),
|
|
125
|
+
tags: fm.tags || [],
|
|
126
|
+
dependencies: fm.dependencies || [],
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (fm.author) {
|
|
130
|
+
newSkills[skillPath].author = fm.author;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
validCount++;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 3. 计算变更统计
|
|
137
|
+
const oldKeys = new Set(Object.keys(oldSkills));
|
|
138
|
+
const newKeys = new Set(Object.keys(newSkills));
|
|
139
|
+
|
|
140
|
+
const added = [...newKeys].filter(k => !oldKeys.has(k));
|
|
141
|
+
const removed = [...oldKeys].filter(k => !newKeys.has(k));
|
|
142
|
+
const updated = [...newKeys].filter(k => oldKeys.has(k) &&
|
|
143
|
+
JSON.stringify(newSkills[k]) !== JSON.stringify(oldSkills[k]));
|
|
144
|
+
|
|
145
|
+
// 4. 写入索引
|
|
146
|
+
const indexData = {
|
|
147
|
+
version: '3.0',
|
|
148
|
+
repository: 'https://code.alibaba-inc.com/os-copilot/skill-os',
|
|
149
|
+
skills: newSkills,
|
|
150
|
+
generated_at: new Date().toISOString(),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
fs.writeFileSync(indexPath, JSON.stringify(indexData, null, 2) + '\n', 'utf-8');
|
|
154
|
+
|
|
155
|
+
// 5. 输出结果
|
|
156
|
+
if (added.length > 0) {
|
|
157
|
+
console.log(chalk.green(` + ${added.length} new skill(s):`));
|
|
158
|
+
added.forEach(p => console.log(chalk.green(` + ${p}`)));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (updated.length > 0) {
|
|
162
|
+
console.log(chalk.yellow(` ~ ${updated.length} updated skill(s):`));
|
|
163
|
+
updated.forEach(p => console.log(chalk.yellow(` ~ ${p}`)));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (removed.length > 0) {
|
|
167
|
+
console.log(chalk.red(` - ${removed.length} removed skill(s):`));
|
|
168
|
+
removed.forEach(p => console.log(chalk.red(` - ${p}`)));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (added.length === 0 && updated.length === 0 && removed.length === 0) {
|
|
172
|
+
console.log(chalk.dim(' No changes detected.'));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (errors.length > 0) {
|
|
176
|
+
console.log(`\n${chalk.yellow(`⚠ ${errors.length} skill(s) skipped due to errors:`)}`);
|
|
177
|
+
errors.forEach(({ path: p, error }) => {
|
|
178
|
+
console.log(chalk.yellow(` ✗ ${p}: ${error}`));
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log(`\n${chalk.bold('📊 Summary:')}`);
|
|
183
|
+
console.log(` Total scanned: ${chalk.cyan(skillDirs.length)}`);
|
|
184
|
+
console.log(` Valid: ${chalk.green(validCount)}`);
|
|
185
|
+
console.log(` Skipped: ${skippedCount > 0 ? chalk.yellow(skippedCount) : chalk.dim(skippedCount)}`);
|
|
186
|
+
console.log(` Index written: ${chalk.cyan(indexPath)}`);
|
|
187
|
+
console.log(`\n${chalk.green('✅ Sync complete!')}\n`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = { cmdSync };
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 共享工具函数
|
|
3
|
+
*
|
|
4
|
+
* 提供 CLI 全局使用的辅助函数:图标映射、颜色格式化、仓库定位、
|
|
5
|
+
* SKILL.md 解析、Schema 校验等。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
const yaml = require('js-yaml');
|
|
12
|
+
const { Validator } = require('jsonschema');
|
|
13
|
+
|
|
14
|
+
// ── 图标映射 ────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
const layerIcons = {
|
|
17
|
+
core: "🧠",
|
|
18
|
+
system: "🔧",
|
|
19
|
+
runtime: "📦",
|
|
20
|
+
application: "🚀",
|
|
21
|
+
cloud: "☁️",
|
|
22
|
+
package: "📦",
|
|
23
|
+
network: "🌐",
|
|
24
|
+
storage: "💾",
|
|
25
|
+
user: "👤",
|
|
26
|
+
meta: "🛠️",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function formatLayerIcon(skillPath) {
|
|
30
|
+
const layer = skillPath.split("/")[0];
|
|
31
|
+
return layerIcons[layer] || "📄";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatStatus(status) {
|
|
35
|
+
switch (status?.toLowerCase()) {
|
|
36
|
+
case 'stable': return chalk.green('git');
|
|
37
|
+
case 'upload': return chalk.cyan('upload');
|
|
38
|
+
default: return status || 'git';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── 仓库定位 ────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function getRepoRoot() {
|
|
45
|
+
let current = path.resolve(path.dirname(__dirname)); // cli/ 所在的父目录
|
|
46
|
+
if (fs.existsSync(path.join(current, 'SKILL_INDEX.json'))) {
|
|
47
|
+
return current;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (fs.existsSync(path.join(process.cwd(), 'SKILL_INDEX.json'))) {
|
|
51
|
+
return process.cwd();
|
|
52
|
+
}
|
|
53
|
+
return current;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function loadIndex() {
|
|
57
|
+
const repoRoot = getRepoRoot();
|
|
58
|
+
const indexPath = path.join(repoRoot, 'SKILL_INDEX.json');
|
|
59
|
+
|
|
60
|
+
if (!fs.existsSync(indexPath)) {
|
|
61
|
+
console.error(chalk.red('Error: SKILL_INDEX.json not found'));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const raw = fs.readFileSync(indexPath, 'utf-8');
|
|
66
|
+
return JSON.parse(raw);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── SKILL.md 解析 ───────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
function parseSkillMdFrontmatter(skillMdPath) {
|
|
72
|
+
if (!fs.existsSync(skillMdPath)) return {};
|
|
73
|
+
const content = fs.readFileSync(skillMdPath, 'utf8');
|
|
74
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
75
|
+
if (!match) return {};
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
return yaml.load(match[1]) || {};
|
|
79
|
+
} catch (e) {
|
|
80
|
+
return {};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Schema & 校验 ───────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
const skillSchema = {
|
|
87
|
+
id: "/SkillMetadata",
|
|
88
|
+
type: "object",
|
|
89
|
+
properties: {
|
|
90
|
+
name: { type: "string", minLength: 1 },
|
|
91
|
+
version: { type: ["string", "number"] },
|
|
92
|
+
description: { type: "string" },
|
|
93
|
+
layer: { enum: ["core", "system", "runtime", "application"] },
|
|
94
|
+
lifecycle: { enum: ["production", "maintenance", "operations", "usage", "meta"] },
|
|
95
|
+
category: { type: ["string", "null"] },
|
|
96
|
+
tags: { type: "array", items: { type: "string" } },
|
|
97
|
+
status: { enum: ["stable", "beta", "placeholder", "upload"] },
|
|
98
|
+
dependencies: { type: "array", items: { type: "string" } },
|
|
99
|
+
permissions: {
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: {
|
|
102
|
+
requires_root: { type: "boolean" },
|
|
103
|
+
dangerous_operations: { type: "array", items: { type: "string" } }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
required: ["name", "version", "layer", "lifecycle"]
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
function validateDirectoryStructure(skillDir) {
|
|
111
|
+
const errors = [];
|
|
112
|
+
if (!fs.existsSync(skillDir)) {
|
|
113
|
+
return { isValid: false, errors: ["Directory does not exist"] };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const skillMd = path.join(skillDir, 'SKILL.md');
|
|
117
|
+
if (!fs.existsSync(skillMd)) {
|
|
118
|
+
errors.push("Missing SKILL.md file");
|
|
119
|
+
} else {
|
|
120
|
+
const content = fs.readFileSync(skillMd, 'utf8');
|
|
121
|
+
if (!content.includes('---')) {
|
|
122
|
+
errors.push("SKILL.md missing frontmatter");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { isValid: errors.length === 0, errors };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
layerIcons,
|
|
131
|
+
formatLayerIcon,
|
|
132
|
+
formatStatus,
|
|
133
|
+
getRepoRoot,
|
|
134
|
+
loadIndex,
|
|
135
|
+
parseSkillMdFrontmatter,
|
|
136
|
+
skillSchema,
|
|
137
|
+
validateDirectoryStructure,
|
|
138
|
+
Validator,
|
|
139
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skill-os",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Skill-OS CLI for managing OS skills",
|
|
5
5
|
"main": "skill-os.js",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"chalk": "^4.1.2",
|
|
21
|
-
"commander": "^
|
|
21
|
+
"commander": "^11.1.0",
|
|
22
22
|
"form-data": "^4.0.5",
|
|
23
23
|
"js-yaml": "^4.1.0",
|
|
24
24
|
"jsonschema": "^1.4.1",
|