itismyskillmarket 1.3.2 → 1.3.3
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/CHANGELOG.md +147 -8
- package/dist/index.js +282 -6
- package/package.json +1 -1
- package/src/cli.ts +24 -7
- package/src/commands/github-install.ts +538 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,150 @@
|
|
|
1
|
+
# SkillMarket v1.3.3 更新日志
|
|
2
|
+
|
|
3
|
+
**日期**: 2026-04-30
|
|
4
|
+
**版本**: 1.3.3
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 🚀 新功能:GitHub 第三方库支持 (Beta)
|
|
9
|
+
|
|
10
|
+
### 1. 支持 GitHub URL 和简写格式
|
|
11
|
+
|
|
12
|
+
现在可以直接从 GitHub 仓库安装 skills:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# GitHub URL 格式
|
|
16
|
+
skm install https://github.com/owner/repo
|
|
17
|
+
skm install https://github.com/owner/repo/tree/main/skills/my-skill
|
|
18
|
+
|
|
19
|
+
# 简写格式
|
|
20
|
+
skm install owner/repo
|
|
21
|
+
skm install owner/repo#branch
|
|
22
|
+
skm install owner/repo@commit-hash
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 2. 自动检测 Skill 本体
|
|
26
|
+
|
|
27
|
+
安装时会自动检测仓库中的 skill 文件:
|
|
28
|
+
|
|
29
|
+
- ✅ `SKILL.md` - skill 定义文件(必须)
|
|
30
|
+
- ✅ `package.json` - 包配置文件(可选)
|
|
31
|
+
- ✅ `metadata.json` - 元数据文件(可选)
|
|
32
|
+
- ✅ 平台目录(`opencode/`, `cursor/`, `vscode/`, `claude/` 等)
|
|
33
|
+
|
|
34
|
+
**检测输出示例**:
|
|
35
|
+
```
|
|
36
|
+
Detecting skill...
|
|
37
|
+
SKILL.md: ✅
|
|
38
|
+
package.json: ✅
|
|
39
|
+
Detected platforms: opencode, vscode
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 3. 平台判断和格式转换
|
|
43
|
+
|
|
44
|
+
- 自动判断 skill 支持的平台
|
|
45
|
+
- 如果某些平台文件缺失,会自动生成适配文件
|
|
46
|
+
- 支持的平台:OpenCode, Cursor, VSCode, Claude Code, Codex, Antigravity
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# 安装到指定平台(自动生成缺失的平台文件)
|
|
50
|
+
skm install owner/repo --platform opencode,claude
|
|
51
|
+
|
|
52
|
+
# 指定分支
|
|
53
|
+
skm install owner/repo#dev --platform vscode
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 4. 版本控制
|
|
57
|
+
|
|
58
|
+
支持指定分支、tag 或 commit:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# 指定分支
|
|
62
|
+
skm install owner/repo#main
|
|
63
|
+
skm install owner/repo -b develop
|
|
64
|
+
|
|
65
|
+
# 指定 commit
|
|
66
|
+
skm install owner/repo@abc1234
|
|
67
|
+
|
|
68
|
+
# 指定 tag(通过分支名)
|
|
69
|
+
skm install owner/repo#v1.0.0
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 5. 技术实现
|
|
73
|
+
|
|
74
|
+
**新增模块**:`src/commands/github-install.ts`
|
|
75
|
+
|
|
76
|
+
| 函数名 | 功能 |
|
|
77
|
+
|--------|------|
|
|
78
|
+
| `parseGitHubUrl()` | 解析 GitHub URL 和简写格式 |
|
|
79
|
+
| `detectSkillFromGitHub()` | 从 GitHub API 检测 skill |
|
|
80
|
+
| `installFromGitHub()` | 从 GitHub 安装 skill |
|
|
81
|
+
| `generatePlatformAdapters()` | 为缺失平台生成适配文件 |
|
|
82
|
+
|
|
83
|
+
**支持的 URL 格式**:
|
|
84
|
+
| 格式 | 示例 |
|
|
85
|
+
|------|------|
|
|
86
|
+
| 完整 URL | `https://github.com/owner/repo` |
|
|
87
|
+
| 完整 URL + 路径 | `https://github.com/owner/repo/tree/main/path` |
|
|
88
|
+
| 简写 | `owner/repo` |
|
|
89
|
+
| 简写 + 分支 | `owner/repo#branch` |
|
|
90
|
+
| 简写 + commit | `owner/repo@commit` |
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 🔧 技术实现
|
|
95
|
+
|
|
96
|
+
### GitHub 安装新增函数
|
|
97
|
+
|
|
98
|
+
| 函数名 | 功能 |
|
|
99
|
+
|--------|------|
|
|
100
|
+
| `parseGitHubUrl()` | 解析 GitHub URL |
|
|
101
|
+
| `detectSkillFromGitHub()` | 检测 skill 本体 |
|
|
102
|
+
| `installFromGitHub()` | 主安装函数 |
|
|
103
|
+
| `generatePlatformAdapters()` | 格式转换 |
|
|
104
|
+
|
|
105
|
+
### 更新接口
|
|
106
|
+
|
|
107
|
+
**GitHubInstallOptions** 接口:
|
|
108
|
+
```typescript
|
|
109
|
+
export interface GitHubInstallOptions {
|
|
110
|
+
platforms?: string[]; // 目标平台
|
|
111
|
+
force?: boolean; // 强制覆盖
|
|
112
|
+
branch?: string; // 指定分支
|
|
113
|
+
commit?: string; // 指定 commit
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### CLI 参数更新
|
|
118
|
+
|
|
119
|
+
| 命令 | 参数 | 说明 |
|
|
120
|
+
|------|------|------|
|
|
121
|
+
| `skm install` | `-b, --branch` | GitHub 分支 |
|
|
122
|
+
| `skm install` | `-c, --commit` | GitHub commit hash |
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 📦 完整版本历史
|
|
127
|
+
|
|
128
|
+
| 版本 | 日期 | 描述 |
|
|
129
|
+
|------|------|------|
|
|
130
|
+
| 1.3.3 | 2026-04-30 | GitHub 第三方库支持 |
|
|
131
|
+
| 1.3.2 | 2026-04-30 | 增强卸载命令:--all, --dry-run, --yes |
|
|
132
|
+
| 1.3.1 | 2026-04-29 | Bug 修复,workflow 改进 |
|
|
133
|
+
| 1.3.0 | 2026-04-23 | 独立搜索命令,改进分页逻辑 |
|
|
134
|
+
| 1.2.6 | 2026-04-22 | 添加搜索功能(--search) |
|
|
135
|
+
| 1.2.5 | 2026-04-16 | 文档更新 |
|
|
136
|
+
| 1.2.4 | 2026-04-16 | 修复版本号硬编码问题 |
|
|
137
|
+
| 1.2.3 | 2026-04-15 | 跨平台 Skill 安装支持 |
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 贡献者
|
|
142
|
+
|
|
143
|
+
- wxc2004 (wanxuchen)
|
|
144
|
+
- Sisyphus Agent
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
1
148
|
# SkillMarket v1.3.2 更新日志
|
|
2
149
|
|
|
3
150
|
**日期**: 2026-04-30
|
|
@@ -90,14 +237,6 @@ VSCode ✅ Uninstalled
|
|
|
90
237
|
⚠️ 1 platform(s) failed to uninstall. Continue with local cleanup? (y/N): _
|
|
91
238
|
```
|
|
92
239
|
|
|
93
|
-
### 5. 更新帮助文档
|
|
94
|
-
|
|
95
|
-
`skm --help` 现在包含卸载命令的完整说明:
|
|
96
|
-
|
|
97
|
-
```bash
|
|
98
|
-
skm uninstall --help
|
|
99
|
-
```
|
|
100
|
-
|
|
101
240
|
---
|
|
102
241
|
|
|
103
242
|
## 🔧 技术实现
|
package/dist/index.js
CHANGED
|
@@ -974,6 +974,272 @@ Uninstalling all skills...
|
|
|
974
974
|
return { success: successCount, failed: failedCount };
|
|
975
975
|
}
|
|
976
976
|
|
|
977
|
+
// src/commands/github-install.ts
|
|
978
|
+
import fs10 from "fs-extra";
|
|
979
|
+
import path9 from "path";
|
|
980
|
+
var GITHUB_URL_PATTERNS = [
|
|
981
|
+
/^https?:\/\/github\.com\/([^/]+)\/([^/]+)(?:\/tree\/([^/]+)(?:\/(.+))?)?$/,
|
|
982
|
+
/^([^/]+)\/([^/]+)(?:#(.+))?$/,
|
|
983
|
+
// owner/repo#branch
|
|
984
|
+
/^([^/]+)\/([^/]+)@(.+)$/
|
|
985
|
+
// owner/repo@commit
|
|
986
|
+
];
|
|
987
|
+
function parseGitHubUrl(input) {
|
|
988
|
+
input = input.replace(/\/$/, "");
|
|
989
|
+
for (const pattern of GITHUB_URL_PATTERNS) {
|
|
990
|
+
const match = input.match(pattern);
|
|
991
|
+
if (match) {
|
|
992
|
+
const owner = match[1];
|
|
993
|
+
const repo = match[2].replace(/\.git$/, "");
|
|
994
|
+
const branch = match[3] || "main";
|
|
995
|
+
const commitOrPath = match[4] || match[3];
|
|
996
|
+
const path10 = match[5] || void 0;
|
|
997
|
+
return {
|
|
998
|
+
owner,
|
|
999
|
+
repo,
|
|
1000
|
+
branch: commitOrPath && !commitOrPath.includes("/") ? commitOrPath : branch,
|
|
1001
|
+
commit: commitOrPath?.match(/^[0-9a-f]{40}$/) ? commitOrPath : void 0,
|
|
1002
|
+
path: path10
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
async function fetchGitHubFile(source, filePath) {
|
|
1009
|
+
const ref = source.commit || source.branch || "main";
|
|
1010
|
+
const url = `https://api.github.com/repos/${source.owner}/${source.repo}/contents/${filePath}?ref=${ref}`;
|
|
1011
|
+
try {
|
|
1012
|
+
const response = await fetch(url, {
|
|
1013
|
+
headers: {
|
|
1014
|
+
"Accept": "application/vnd.github.v3.raw",
|
|
1015
|
+
"User-Agent": "SkillMarket"
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
if (!response.ok) {
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
return await response.text();
|
|
1022
|
+
} catch {
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
async function fetchGitHubDir(source, dirPath = "") {
|
|
1027
|
+
const ref = source.commit || source.branch || "main";
|
|
1028
|
+
const url = `https://api.github.com/repos/${source.owner}/${source.repo}/contents/${dirPath}?ref=${ref}`;
|
|
1029
|
+
try {
|
|
1030
|
+
const response = await fetch(url, {
|
|
1031
|
+
headers: {
|
|
1032
|
+
"Accept": "application/vnd.github.v3+json",
|
|
1033
|
+
"User-Agent": "SkillMarket"
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
if (!response.ok) {
|
|
1037
|
+
return [];
|
|
1038
|
+
}
|
|
1039
|
+
const data = await response.json();
|
|
1040
|
+
return Array.isArray(data) ? data : [];
|
|
1041
|
+
} catch {
|
|
1042
|
+
return [];
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
async function detectSkillFromGitHub(source) {
|
|
1046
|
+
const result = {
|
|
1047
|
+
hasSkillMd: false,
|
|
1048
|
+
hasPackageJson: false,
|
|
1049
|
+
hasMetadataJson: false,
|
|
1050
|
+
platforms: [],
|
|
1051
|
+
files: []
|
|
1052
|
+
};
|
|
1053
|
+
const basePath = source.path || "";
|
|
1054
|
+
const skillMdPath = basePath ? `${basePath}/SKILL.md` : "SKILL.md";
|
|
1055
|
+
const skillMdContent = await fetchGitHubFile(source, skillMdPath);
|
|
1056
|
+
if (skillMdContent !== null) {
|
|
1057
|
+
result.hasSkillMd = true;
|
|
1058
|
+
result.files.push(skillMdPath);
|
|
1059
|
+
}
|
|
1060
|
+
const packageJsonPath = basePath ? `${basePath}/package.json` : "package.json";
|
|
1061
|
+
const packageJsonContent = await fetchGitHubFile(source, packageJsonPath);
|
|
1062
|
+
if (packageJsonContent !== null) {
|
|
1063
|
+
result.hasPackageJson = true;
|
|
1064
|
+
result.files.push(packageJsonPath);
|
|
1065
|
+
try {
|
|
1066
|
+
const pkg = JSON.parse(packageJsonContent);
|
|
1067
|
+
result.skillId = pkg.name?.split("/").pop() || pkg.skillmarket?.id;
|
|
1068
|
+
result.displayName = pkg.skillmarket?.displayName;
|
|
1069
|
+
result.description = pkg.description;
|
|
1070
|
+
if (pkg.skillmarket?.platforms) {
|
|
1071
|
+
result.platforms = pkg.skillmarket.platforms;
|
|
1072
|
+
}
|
|
1073
|
+
} catch {
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
const metadataPath = basePath ? `${basePath}/metadata.json` : "metadata.json";
|
|
1077
|
+
const metadataContent = await fetchGitHubFile(source, metadataPath);
|
|
1078
|
+
if (metadataContent !== null) {
|
|
1079
|
+
result.hasMetadataJson = true;
|
|
1080
|
+
result.files.push(metadataPath);
|
|
1081
|
+
}
|
|
1082
|
+
const platformDirs = ["opencode", "cursor", "vscode", "claude", "codex", "antigravity"];
|
|
1083
|
+
for (const dir of platformDirs) {
|
|
1084
|
+
const dirPath = basePath ? `${basePath}/${dir}` : dir;
|
|
1085
|
+
const files = await fetchGitHubDir(source, dirPath);
|
|
1086
|
+
if (files.length > 0) {
|
|
1087
|
+
const platform = dir;
|
|
1088
|
+
if (!result.platforms.includes(platform)) {
|
|
1089
|
+
result.platforms.push(platform);
|
|
1090
|
+
}
|
|
1091
|
+
result.files.push(dirPath);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
if (result.platforms.length === 0) {
|
|
1095
|
+
result.platforms.push("opencode");
|
|
1096
|
+
}
|
|
1097
|
+
return result;
|
|
1098
|
+
}
|
|
1099
|
+
async function generatePlatformAdapters(skillId, existingPlatforms, targetPlatforms, sourceDir) {
|
|
1100
|
+
const skillsDir = getSkillsDir();
|
|
1101
|
+
const skillVersionDir = path9.join(skillsDir, `${skillId}@github`);
|
|
1102
|
+
for (const platform of targetPlatforms) {
|
|
1103
|
+
if (existingPlatforms.includes(platform)) {
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
const platformDir = path9.join(skillVersionDir, platform);
|
|
1107
|
+
await fs10.ensureDir(platformDir);
|
|
1108
|
+
const sourceSkillMd = path9.join(sourceDir, "SKILL.md");
|
|
1109
|
+
const targetSkillMd = path9.join(platformDir, "SKILL.md");
|
|
1110
|
+
if (await fs10.pathExists(sourceSkillMd)) {
|
|
1111
|
+
await fs10.copy(sourceSkillMd, targetSkillMd);
|
|
1112
|
+
}
|
|
1113
|
+
if (platform === "opencode" || platform === "cursor" || platform === "codex" || platform === "antigravity") {
|
|
1114
|
+
} else if (platform === "vscode") {
|
|
1115
|
+
const skillJson = {
|
|
1116
|
+
name: skillId,
|
|
1117
|
+
description: `Skill: ${skillId}`,
|
|
1118
|
+
version: "1.0.0"
|
|
1119
|
+
};
|
|
1120
|
+
await fs10.writeJson(path9.join(platformDir, "skill.json"), skillJson, { spaces: 2 });
|
|
1121
|
+
} else if (platform === "claude") {
|
|
1122
|
+
const skillJson = {
|
|
1123
|
+
name: skillId,
|
|
1124
|
+
description: `Skill: ${skillId}`,
|
|
1125
|
+
version: "1.0.0"
|
|
1126
|
+
};
|
|
1127
|
+
await fs10.writeJson(path9.join(platformDir, "skill.json"), skillJson, { spaces: 2 });
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
async function installFromGitHub(input, options) {
|
|
1132
|
+
let source = parseGitHubUrl(input);
|
|
1133
|
+
if (!source) {
|
|
1134
|
+
throw new Error(`Invalid GitHub URL or format: ${input}`);
|
|
1135
|
+
}
|
|
1136
|
+
if (options?.branch) source.branch = options.branch;
|
|
1137
|
+
if (options?.commit) source.commit = options.commit;
|
|
1138
|
+
console.log(`Installing from GitHub: ${source.owner}/${source.repo}`);
|
|
1139
|
+
if (source.branch) console.log(` Branch: ${source.branch}`);
|
|
1140
|
+
if (source.commit) console.log(` Commit: ${source.commit}`);
|
|
1141
|
+
if (source.path) console.log(` Path: ${source.path}`);
|
|
1142
|
+
console.log("\nDetecting skill...");
|
|
1143
|
+
const detected = await detectSkillFromGitHub(source);
|
|
1144
|
+
if (!detected.hasSkillMd && !detected.hasPackageJson) {
|
|
1145
|
+
throw new Error("No skill found in this repository (missing SKILL.md and package.json)");
|
|
1146
|
+
}
|
|
1147
|
+
console.log(` SKILL.md: ${detected.hasSkillMd ? "\u2705" : "\u274C"}`);
|
|
1148
|
+
console.log(` package.json: ${detected.hasPackageJson ? "\u2705" : "\u274C"}`);
|
|
1149
|
+
console.log(` Detected platforms: ${detected.platforms.join(", ") || "none"}`);
|
|
1150
|
+
const skillId = detected.skillId || source.repo;
|
|
1151
|
+
const version = `github-${source.commit?.substring(0, 7) || source.branch || "main"}`;
|
|
1152
|
+
console.log(`
|
|
1153
|
+
Setting up skill: ${skillId}@${version}`);
|
|
1154
|
+
await ensureMarketDirs();
|
|
1155
|
+
const skillsDir = getSkillsDir();
|
|
1156
|
+
const skillVersionDir = path9.join(skillsDir, `${skillId}@${version}`);
|
|
1157
|
+
await fs10.ensureDir(skillVersionDir);
|
|
1158
|
+
console.log("Downloading files...");
|
|
1159
|
+
const basePath = source.path || "";
|
|
1160
|
+
const skillMdPath = basePath ? `${basePath}/SKILL.md` : "SKILL.md";
|
|
1161
|
+
const skillMdContent = await fetchGitHubFile(source, skillMdPath);
|
|
1162
|
+
if (skillMdContent) {
|
|
1163
|
+
await fs10.writeFile(path9.join(skillVersionDir, "SKILL.md"), skillMdContent);
|
|
1164
|
+
console.log(" \u2705 SKILL.md");
|
|
1165
|
+
}
|
|
1166
|
+
const packageJsonPath = basePath ? `${basePath}/package.json` : "package.json";
|
|
1167
|
+
const packageJsonContent = await fetchGitHubFile(source, packageJsonPath);
|
|
1168
|
+
if (packageJsonContent) {
|
|
1169
|
+
await fs10.writeFile(path9.join(skillVersionDir, "package.json"), packageJsonContent);
|
|
1170
|
+
console.log(" \u2705 package.json");
|
|
1171
|
+
}
|
|
1172
|
+
const metadataPath = basePath ? `${basePath}/metadata.json` : "metadata.json";
|
|
1173
|
+
const metadataContent = await fetchGitHubFile(source, metadataPath);
|
|
1174
|
+
if (metadataContent) {
|
|
1175
|
+
await fs10.writeFile(path9.join(skillVersionDir, "metadata.json"), metadataContent);
|
|
1176
|
+
console.log(" \u2705 metadata.json");
|
|
1177
|
+
}
|
|
1178
|
+
const targetPlatforms = options?.platforms || detected.platforms;
|
|
1179
|
+
const existingPlatforms = detected.platforms;
|
|
1180
|
+
const missingPlatforms = targetPlatforms.filter((p) => !existingPlatforms.includes(p));
|
|
1181
|
+
if (missingPlatforms.length > 0) {
|
|
1182
|
+
console.log(`
|
|
1183
|
+
Generating adapters for missing platforms: ${missingPlatforms.join(", ")}`);
|
|
1184
|
+
await generatePlatformAdapters(skillId, existingPlatforms, targetPlatforms, skillVersionDir);
|
|
1185
|
+
console.log(" \u2705 Platform adapters generated");
|
|
1186
|
+
}
|
|
1187
|
+
const skillDir = path9.join(skillsDir, skillId);
|
|
1188
|
+
await fs10.ensureDir(skillDir);
|
|
1189
|
+
const latestLink = path9.join(skillDir, "latest");
|
|
1190
|
+
try {
|
|
1191
|
+
await fs10.remove(latestLink);
|
|
1192
|
+
await fs10.symlink(skillVersionDir, latestLink, "junction");
|
|
1193
|
+
} catch {
|
|
1194
|
+
await fs10.copy(skillVersionDir, path9.join(skillDir, "latest"), { overwrite: true });
|
|
1195
|
+
}
|
|
1196
|
+
console.log(`
|
|
1197
|
+
Installing to ${targetPlatforms.length} platform(s)...
|
|
1198
|
+
`);
|
|
1199
|
+
const results = [];
|
|
1200
|
+
for (const platform of targetPlatforms) {
|
|
1201
|
+
const adapter = getAdapterByPlatform(platform);
|
|
1202
|
+
if (!adapter) {
|
|
1203
|
+
console.log(`${platform.padEnd(12)} \u274C Unknown platform`);
|
|
1204
|
+
results.push({ name: platform, status: "failed", error: "Unknown platform" });
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
try {
|
|
1208
|
+
const isInstalled = await adapter.isInstalled(skillId);
|
|
1209
|
+
if (isInstalled && !options?.force) {
|
|
1210
|
+
console.log(`${adapter.name.padEnd(12)} \u26A0\uFE0F Already installed (use --force to overwrite)`);
|
|
1211
|
+
results.push({ name: adapter.name, status: "skipped" });
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
await adapter.install(skillId, skillVersionDir);
|
|
1215
|
+
console.log(`${adapter.name.padEnd(12)} \u2705 Installed successfully`);
|
|
1216
|
+
results.push({ name: adapter.name, status: "installed" });
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
console.log(`${adapter.name.padEnd(12)} \u274C Failed: ${error}`);
|
|
1219
|
+
results.push({ name: adapter.name, status: "failed", error: String(error) });
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
const installed = results.filter((r) => r.status === "installed").length;
|
|
1223
|
+
const skipped = results.filter((r) => r.status === "skipped").length;
|
|
1224
|
+
const failed = results.filter((r) => r.status === "failed").length;
|
|
1225
|
+
console.log(`
|
|
1226
|
+
\u{1F4CA} Summary: ${installed} installed, ${skipped} skipped, ${failed} failed`);
|
|
1227
|
+
console.log(`
|
|
1228
|
+
Installing to platforms: ${targetPlatforms.join(", ")}`);
|
|
1229
|
+
console.log(" (Platform installation logic needs to be completed)");
|
|
1230
|
+
const registry = await loadRegistry();
|
|
1231
|
+
registry.skills[skillId] = {
|
|
1232
|
+
id: skillId,
|
|
1233
|
+
version,
|
|
1234
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1235
|
+
platforms: targetPlatforms
|
|
1236
|
+
};
|
|
1237
|
+
await saveRegistry(registry);
|
|
1238
|
+
console.log(`
|
|
1239
|
+
\u2705 ${skillId}@${version} installed successfully from GitHub!`);
|
|
1240
|
+
console.log(` Source: ${source.owner}/${source.repo}`);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
977
1243
|
// src/cli.ts
|
|
978
1244
|
var __filename = fileURLToPath(import.meta.url);
|
|
979
1245
|
var __dirname = dirname(__filename);
|
|
@@ -1051,14 +1317,24 @@ var infoCmd = program.command("info").description("Display skill information");
|
|
|
1051
1317
|
infoCmd.argument("<skill-id>", "Skill ID to show info").action((skillId) => {
|
|
1052
1318
|
showSkillInfo(skillId);
|
|
1053
1319
|
});
|
|
1054
|
-
var installCmd = program.command("install").description("Install a skill
|
|
1055
|
-
installCmd.argument("<skill>", "Skill ID
|
|
1320
|
+
var installCmd = program.command("install").description("Install a skill from npm or GitHub");
|
|
1321
|
+
installCmd.argument("<skill>", "Skill ID, npm package, or GitHub URL (owner/repo, https://github.com/owner/repo)").option("-p, --platform <platforms>", "Target platforms (comma-separated: opencode,claude,vscode)").option("-f, --force", "Overwrite if already installed").option("-v, --version <version>", "Specific version to install (npm only)").option("-b, --branch <branch>", "GitHub branch to install from").option("-c, --commit <commit>", "GitHub commit hash to install from").action(async (skill, opts) => {
|
|
1056
1322
|
try {
|
|
1057
1323
|
const platforms = opts.platform ? opts.platform.split(",").map((p) => p.trim()) : void 0;
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1324
|
+
const githubSource = parseGitHubUrl(skill);
|
|
1325
|
+
if (githubSource) {
|
|
1326
|
+
await installFromGitHub(skill, {
|
|
1327
|
+
platforms,
|
|
1328
|
+
force: opts.force,
|
|
1329
|
+
branch: opts.branch,
|
|
1330
|
+
commit: opts.commit
|
|
1331
|
+
});
|
|
1332
|
+
} else {
|
|
1333
|
+
await installSkill(skill, opts.version, {
|
|
1334
|
+
platforms,
|
|
1335
|
+
force: opts.force
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1062
1338
|
} catch (err) {
|
|
1063
1339
|
console.error("Installation failed:", err);
|
|
1064
1340
|
process.exit(1);
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -48,6 +48,7 @@ import { installSkill } from './commands/install.js'; // 安装命令
|
|
|
48
48
|
import { syncPlatformLinks } from './commands/sync.js'; // 同步命令
|
|
49
49
|
import { updateSkill } from './commands/update.js'; // 更新命令
|
|
50
50
|
import { uninstallSkill, uninstallAll } from './commands/uninstall.js'; // 卸载命令
|
|
51
|
+
import { installFromGitHub, parseGitHubUrl } from './commands/github-install.js'; // GitHub 安装
|
|
51
52
|
import { detectPlatforms, getAllAdapters, OpenCodeAdapter, ClaudeAdapter, VSCodeAdapter } from './adapters/index.js'; // 平台适配器
|
|
52
53
|
|
|
53
54
|
// -----------------------------------------------------------------------------
|
|
@@ -236,22 +237,38 @@ infoCmd
|
|
|
236
237
|
* skm install brainstorming@1.0.0
|
|
237
238
|
* skm install brainstorming --platform opencode
|
|
238
239
|
*/
|
|
239
|
-
const installCmd = program.command('install').description('Install a skill
|
|
240
|
+
const installCmd = program.command('install').description('Install a skill from npm or GitHub');
|
|
240
241
|
installCmd
|
|
241
|
-
.argument('<skill>', 'Skill ID
|
|
242
|
+
.argument('<skill>', 'Skill ID, npm package, or GitHub URL (owner/repo, https://github.com/owner/repo)')
|
|
242
243
|
.option('-p, --platform <platforms>', 'Target platforms (comma-separated: opencode,claude,vscode)')
|
|
243
244
|
.option('-f, --force', 'Overwrite if already installed')
|
|
244
|
-
.option('-v, --version <version>', 'Specific version to install')
|
|
245
|
+
.option('-v, --version <version>', 'Specific version to install (npm only)')
|
|
246
|
+
.option('-b, --branch <branch>', 'GitHub branch to install from')
|
|
247
|
+
.option('-c, --commit <commit>', 'GitHub commit hash to install from')
|
|
245
248
|
.action(async (skill, opts) => {
|
|
246
249
|
try {
|
|
247
250
|
const platforms = opts.platform
|
|
248
251
|
? opts.platform.split(',').map((p: string) => p.trim())
|
|
249
252
|
: undefined;
|
|
250
253
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
254
|
+
// 检测是否为 GitHub URL 或 owner/repo 格式
|
|
255
|
+
const githubSource = parseGitHubUrl(skill);
|
|
256
|
+
|
|
257
|
+
if (githubSource) {
|
|
258
|
+
// GitHub 安装
|
|
259
|
+
await installFromGitHub(skill, {
|
|
260
|
+
platforms,
|
|
261
|
+
force: opts.force,
|
|
262
|
+
branch: opts.branch,
|
|
263
|
+
commit: opts.commit
|
|
264
|
+
});
|
|
265
|
+
} else {
|
|
266
|
+
// npm 安装
|
|
267
|
+
await installSkill(skill, opts.version, {
|
|
268
|
+
platforms,
|
|
269
|
+
force: opts.force
|
|
270
|
+
});
|
|
271
|
+
}
|
|
255
272
|
} catch (err) {
|
|
256
273
|
console.error('Installation failed:', err);
|
|
257
274
|
process.exit(1);
|
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* GitHub Skill 安装模块
|
|
4
|
+
* =============================================================================
|
|
5
|
+
*
|
|
6
|
+
* 本模块实现从 GitHub 仓库安装 skill 的功能。
|
|
7
|
+
*
|
|
8
|
+
* 功能:
|
|
9
|
+
* 1. 检测 GitHub URL 格式
|
|
10
|
+
* 2. 从 GitHub API 获取仓库内容
|
|
11
|
+
* 3. 检测 skill 本体(SKILL.md、package.json)
|
|
12
|
+
* 4. 判断所属平台(OpenCode/Cursor/VSCode/Claude 等)
|
|
13
|
+
* 5. 格式转换(如果不全,则补充平台特定文件)
|
|
14
|
+
* 6. 版本控制(支持 branch/tag/commit)
|
|
15
|
+
*
|
|
16
|
+
* @module commands/github-install
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// -----------------------------------------------------------------------------
|
|
20
|
+
// 导入依赖
|
|
21
|
+
// -----------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
import fs from 'fs-extra';
|
|
24
|
+
import path from 'path';
|
|
25
|
+
import { getSkillsDir, ensureMarketDirs } from '../utils/dirs.js';
|
|
26
|
+
import { loadRegistry, saveRegistry } from './registry.js';
|
|
27
|
+
import { detectPlatforms, getAdapterByPlatform } from '../adapters/index.js';
|
|
28
|
+
import type { Platform } from '../constants.js';
|
|
29
|
+
import type { PlatformAdapter, InstalledSkill } from '../types.js';
|
|
30
|
+
|
|
31
|
+
// -----------------------------------------------------------------------------
|
|
32
|
+
// GitHub URL 解析
|
|
33
|
+
// -----------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* GitHub URL 模式
|
|
37
|
+
* 支持格式:
|
|
38
|
+
* - https://github.com/owner/repo
|
|
39
|
+
* - https://github.com/owner/repo/tree/branch/path
|
|
40
|
+
* - owner/repo
|
|
41
|
+
* - owner/repo#branch
|
|
42
|
+
* - owner/repo@commit
|
|
43
|
+
*/
|
|
44
|
+
const GITHUB_URL_PATTERNS = [
|
|
45
|
+
/^https?:\/\/github\.com\/([^/]+)\/([^/]+)(?:\/tree\/([^/]+)(?:\/(.+))?)?$/,
|
|
46
|
+
/^([^/]+)\/([^/]+)(?:#(.+))?$/, // owner/repo#branch
|
|
47
|
+
/^([^/]+)\/([^/]+)@(.+)$/, // owner/repo@commit
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
export interface GitHubSkillSource {
|
|
51
|
+
owner: string;
|
|
52
|
+
repo: string;
|
|
53
|
+
branch?: string;
|
|
54
|
+
commit?: string;
|
|
55
|
+
path?: string; // 子目录路径
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 解析 GitHub URL 或简写
|
|
60
|
+
*
|
|
61
|
+
* @param {string} input - 用户输入(URL 或 owner/repo 格式)
|
|
62
|
+
* @returns {GitHubSkillSource | null} 解析结果
|
|
63
|
+
*/
|
|
64
|
+
export function parseGitHubUrl(input: string): GitHubSkillSource | null {
|
|
65
|
+
// 移除尾部斜杠
|
|
66
|
+
input = input.replace(/\/$/, '');
|
|
67
|
+
|
|
68
|
+
for (const pattern of GITHUB_URL_PATTERNS) {
|
|
69
|
+
const match = input.match(pattern);
|
|
70
|
+
if (match) {
|
|
71
|
+
const owner = match[1];
|
|
72
|
+
const repo = match[2].replace(/\.git$/, '');
|
|
73
|
+
const branch = match[3] || 'main'; // 默认 main 分支
|
|
74
|
+
const commitOrPath = match[4] || match[3];
|
|
75
|
+
const path = match[5] || undefined;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
owner,
|
|
79
|
+
repo,
|
|
80
|
+
branch: commitOrPath && !commitOrPath.includes('/') ? commitOrPath : branch,
|
|
81
|
+
commit: commitOrPath?.match(/^[0-9a-f]{40}$/) ? commitOrPath : undefined,
|
|
82
|
+
path
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// -----------------------------------------------------------------------------
|
|
91
|
+
// GitHub API 查询
|
|
92
|
+
// -----------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
interface GitHubFile {
|
|
95
|
+
name: string;
|
|
96
|
+
path: string;
|
|
97
|
+
type: 'file' | 'dir';
|
|
98
|
+
download_url?: string;
|
|
99
|
+
content?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 从 GitHub API 获取文件内容
|
|
104
|
+
*
|
|
105
|
+
* @param {GitHubSkillSource} source - GitHub 源信息
|
|
106
|
+
* @param {string} filePath - 文件路径
|
|
107
|
+
* @returns {Promise<string | null>} 文件内容
|
|
108
|
+
*/
|
|
109
|
+
async function fetchGitHubFile(
|
|
110
|
+
source: GitHubSkillSource,
|
|
111
|
+
filePath: string
|
|
112
|
+
): Promise<string | null> {
|
|
113
|
+
const ref = source.commit || source.branch || 'main';
|
|
114
|
+
const url = `https://api.github.com/repos/${source.owner}/${source.repo}/contents/${filePath}?ref=${ref}`;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const response = await fetch(url, {
|
|
118
|
+
headers: {
|
|
119
|
+
'Accept': 'application/vnd.github.v3.raw',
|
|
120
|
+
'User-Agent': 'SkillMarket'
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return await response.text();
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 从 GitHub API 获取目录列表
|
|
136
|
+
*
|
|
137
|
+
* @param {GitHubSkillSource} source - GitHub 源信息
|
|
138
|
+
* @param {string} dirPath - 目录路径
|
|
139
|
+
* @returns {Promise<GitHubFile[]>} 文件列表
|
|
140
|
+
*/
|
|
141
|
+
async function fetchGitHubDir(
|
|
142
|
+
source: GitHubSkillSource,
|
|
143
|
+
dirPath: string = ''
|
|
144
|
+
): Promise<GitHubFile[]> {
|
|
145
|
+
const ref = source.commit || source.branch || 'main';
|
|
146
|
+
const url = `https://api.github.com/repos/${source.owner}/${source.repo}/contents/${dirPath}?ref=${ref}`;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const response = await fetch(url, {
|
|
150
|
+
headers: {
|
|
151
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
152
|
+
'User-Agent': 'SkillMarket'
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const data = await response.json();
|
|
161
|
+
return Array.isArray(data) ? data as GitHubFile[] : [];
|
|
162
|
+
} catch {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// -----------------------------------------------------------------------------
|
|
168
|
+
// Skill 检测
|
|
169
|
+
// -----------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
export interface DetectedSkill {
|
|
172
|
+
hasSkillMd: boolean;
|
|
173
|
+
hasPackageJson: boolean;
|
|
174
|
+
hasMetadataJson: boolean;
|
|
175
|
+
platforms: Platform[];
|
|
176
|
+
files: string[]; // 检测到的相关文件
|
|
177
|
+
skillId?: string;
|
|
178
|
+
displayName?: string;
|
|
179
|
+
description?: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 检测 GitHub 仓库中的 skill 信息
|
|
184
|
+
*
|
|
185
|
+
* @param {GitHubSkillSource} source - GitHub 源信息
|
|
186
|
+
* @returns {Promise<DetectedSkill>} 检测结果
|
|
187
|
+
*/
|
|
188
|
+
export async function detectSkillFromGitHub(
|
|
189
|
+
source: GitHubSkillSource
|
|
190
|
+
): Promise<DetectedSkill> {
|
|
191
|
+
const result: DetectedSkill = {
|
|
192
|
+
hasSkillMd: false,
|
|
193
|
+
hasPackageJson: false,
|
|
194
|
+
hasMetadataJson: false,
|
|
195
|
+
platforms: [],
|
|
196
|
+
files: []
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const basePath = source.path || '';
|
|
200
|
+
|
|
201
|
+
// 1. 检查 SKILL.md
|
|
202
|
+
const skillMdPath = basePath ? `${basePath}/SKILL.md` : 'SKILL.md';
|
|
203
|
+
const skillMdContent = await fetchGitHubFile(source, skillMdPath);
|
|
204
|
+
if (skillMdContent !== null) {
|
|
205
|
+
result.hasSkillMd = true;
|
|
206
|
+
result.files.push(skillMdPath);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 2. 检查 package.json
|
|
210
|
+
const packageJsonPath = basePath ? `${basePath}/package.json` : 'package.json';
|
|
211
|
+
const packageJsonContent = await fetchGitHubFile(source, packageJsonPath);
|
|
212
|
+
if (packageJsonContent !== null) {
|
|
213
|
+
result.hasPackageJson = true;
|
|
214
|
+
result.files.push(packageJsonPath);
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const pkg = JSON.parse(packageJsonContent);
|
|
218
|
+
result.skillId = pkg.name?.split('/').pop() || pkg.skillmarket?.id;
|
|
219
|
+
result.displayName = pkg.skillmarket?.displayName;
|
|
220
|
+
result.description = pkg.description;
|
|
221
|
+
|
|
222
|
+
// 提取支持的平台
|
|
223
|
+
if (pkg.skillmarket?.platforms) {
|
|
224
|
+
result.platforms = pkg.skillmarket.platforms as Platform[];
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
// JSON 解析失败,忽略
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 3. 检查 metadata.json
|
|
232
|
+
const metadataPath = basePath ? `${basePath}/metadata.json` : 'metadata.json';
|
|
233
|
+
const metadataContent = await fetchGitHubFile(source, metadataPath);
|
|
234
|
+
if (metadataContent !== null) {
|
|
235
|
+
result.hasMetadataJson = true;
|
|
236
|
+
result.files.push(metadataPath);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 4. 检查平台特定目录
|
|
240
|
+
const platformDirs = ['opencode', 'cursor', 'vscode', 'claude', 'codex', 'antigravity'];
|
|
241
|
+
for (const dir of platformDirs) {
|
|
242
|
+
const dirPath = basePath ? `${basePath}/${dir}` : dir;
|
|
243
|
+
const files = await fetchGitHubDir(source, dirPath);
|
|
244
|
+
if (files.length > 0) {
|
|
245
|
+
const platform = dir as Platform;
|
|
246
|
+
if (!result.platforms.includes(platform)) {
|
|
247
|
+
result.platforms.push(platform);
|
|
248
|
+
}
|
|
249
|
+
result.files.push(dirPath);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 5. 如果没有检测到平台,默认为 opencode
|
|
254
|
+
if (result.platforms.length === 0) {
|
|
255
|
+
result.platforms.push('opencode');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// -----------------------------------------------------------------------------
|
|
262
|
+
// 格式转换
|
|
263
|
+
// -----------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 为缺失的平台生成适配文件
|
|
267
|
+
*
|
|
268
|
+
* @param {string} skillId - Skill ID
|
|
269
|
+
* @param {Platform[]} existingPlatforms - 已存在的平台
|
|
270
|
+
* @param {Platform[]} targetPlatforms - 目标平台
|
|
271
|
+
* @param {string} sourceDir - 源目录(包含 SKILL.md)
|
|
272
|
+
* @returns {Promise<void>}
|
|
273
|
+
*/
|
|
274
|
+
async function generatePlatformAdapters(
|
|
275
|
+
skillId: string,
|
|
276
|
+
existingPlatforms: Platform[],
|
|
277
|
+
targetPlatforms: Platform[],
|
|
278
|
+
sourceDir: string
|
|
279
|
+
): Promise<void> {
|
|
280
|
+
const skillsDir = getSkillsDir();
|
|
281
|
+
const skillVersionDir = path.join(skillsDir, `${skillId}@github`);
|
|
282
|
+
|
|
283
|
+
for (const platform of targetPlatforms) {
|
|
284
|
+
if (existingPlatforms.includes(platform)) {
|
|
285
|
+
continue; // 平台已存在,跳过
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 创建平台目录
|
|
289
|
+
const platformDir = path.join(skillVersionDir, platform);
|
|
290
|
+
await fs.ensureDir(platformDir);
|
|
291
|
+
|
|
292
|
+
// 复制 SKILL.md(如果存在)
|
|
293
|
+
const sourceSkillMd = path.join(sourceDir, 'SKILL.md');
|
|
294
|
+
const targetSkillMd = path.join(platformDir, 'SKILL.md');
|
|
295
|
+
if (await fs.pathExists(sourceSkillMd)) {
|
|
296
|
+
await fs.copy(sourceSkillMd, targetSkillMd);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 创建平台特定的配置文件(如果需要)
|
|
300
|
+
if (platform === 'opencode' || platform === 'cursor' || platform === 'codex' || platform === 'antigravity') {
|
|
301
|
+
// OpenCode 兼容平台,只需要 SKILL.md
|
|
302
|
+
} else if (platform === 'vscode') {
|
|
303
|
+
// VSCode 可能需要 skill.json
|
|
304
|
+
const skillJson = {
|
|
305
|
+
name: skillId,
|
|
306
|
+
description: `Skill: ${skillId}`,
|
|
307
|
+
version: '1.0.0'
|
|
308
|
+
};
|
|
309
|
+
await fs.writeJson(path.join(platformDir, 'skill.json'), skillJson, { spaces: 2 });
|
|
310
|
+
} else if (platform === 'claude') {
|
|
311
|
+
// Claude Code 可能需要 skill.json
|
|
312
|
+
const skillJson = {
|
|
313
|
+
name: skillId,
|
|
314
|
+
description: `Skill: ${skillId}`,
|
|
315
|
+
version: '1.0.0'
|
|
316
|
+
};
|
|
317
|
+
await fs.writeJson(path.join(platformDir, 'skill.json'), skillJson, { spaces: 2 });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// -----------------------------------------------------------------------------
|
|
323
|
+
// 主安装函数
|
|
324
|
+
// -----------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
export interface GitHubInstallOptions {
|
|
327
|
+
platforms?: string[]; // 目标平台(留空则使用检测到的平台)
|
|
328
|
+
force?: boolean; // 强制覆盖
|
|
329
|
+
branch?: string; // 指定分支
|
|
330
|
+
commit?: string; // 指定 commit
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* 从 GitHub 安装 skill
|
|
335
|
+
*
|
|
336
|
+
* @param {string} input - GitHub URL 或 owner/repo 格式
|
|
337
|
+
* @param {GitHubInstallOptions} [options] - 安装选项
|
|
338
|
+
* @returns {Promise<void>}
|
|
339
|
+
*
|
|
340
|
+
* @example
|
|
341
|
+
* // 从 GitHub 安装
|
|
342
|
+
* await installFromGitHub('owner/repo');
|
|
343
|
+
*
|
|
344
|
+
* // 指定分支
|
|
345
|
+
* await installFromGitHub('owner/repo#dev');
|
|
346
|
+
*
|
|
347
|
+
* // 指定 commit
|
|
348
|
+
* await installFromGitHub('owner/repo@abc123');
|
|
349
|
+
*
|
|
350
|
+
* // 安装到特定平台
|
|
351
|
+
* await installFromGitHub('owner/repo', { platforms: ['opencode', 'vscode'] });
|
|
352
|
+
*/
|
|
353
|
+
export async function installFromGitHub(
|
|
354
|
+
input: string,
|
|
355
|
+
options?: GitHubInstallOptions
|
|
356
|
+
): Promise<void> {
|
|
357
|
+
// ==========================================================================
|
|
358
|
+
// 步骤 1: 解析 GitHub URL
|
|
359
|
+
// ==========================================================================
|
|
360
|
+
|
|
361
|
+
let source = parseGitHubUrl(input);
|
|
362
|
+
if (!source) {
|
|
363
|
+
throw new Error(`Invalid GitHub URL or format: ${input}`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 覆盖分支/commit(如果命令行指定)
|
|
367
|
+
if (options?.branch) source.branch = options.branch;
|
|
368
|
+
if (options?.commit) source.commit = options.commit;
|
|
369
|
+
|
|
370
|
+
console.log(`Installing from GitHub: ${source.owner}/${source.repo}`);
|
|
371
|
+
if (source.branch) console.log(` Branch: ${source.branch}`);
|
|
372
|
+
if (source.commit) console.log(` Commit: ${source.commit}`);
|
|
373
|
+
if (source.path) console.log(` Path: ${source.path}`);
|
|
374
|
+
|
|
375
|
+
// ==========================================================================
|
|
376
|
+
// 步骤 2: 检测 skill
|
|
377
|
+
// ==========================================================================
|
|
378
|
+
|
|
379
|
+
console.log('\nDetecting skill...');
|
|
380
|
+
const detected = await detectSkillFromGitHub(source);
|
|
381
|
+
|
|
382
|
+
if (!detected.hasSkillMd && !detected.hasPackageJson) {
|
|
383
|
+
throw new Error('No skill found in this repository (missing SKILL.md and package.json)');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
console.log(` SKILL.md: ${detected.hasSkillMd ? '✅' : '❌'}`);
|
|
387
|
+
console.log(` package.json: ${detected.hasPackageJson ? '✅' : '❌'}`);
|
|
388
|
+
console.log(` Detected platforms: ${detected.platforms.join(', ') || 'none'}`);
|
|
389
|
+
|
|
390
|
+
// ==========================================================================
|
|
391
|
+
// 步骤 3: 确定 skill ID 和版本
|
|
392
|
+
// ==========================================================================
|
|
393
|
+
|
|
394
|
+
const skillId = detected.skillId || source.repo;
|
|
395
|
+
const version = `github-${source.commit?.substring(0, 7) || source.branch || 'main'}`;
|
|
396
|
+
|
|
397
|
+
console.log(`\nSetting up skill: ${skillId}@${version}`);
|
|
398
|
+
|
|
399
|
+
// ==========================================================================
|
|
400
|
+
// 步骤 4: 下载文件到本地
|
|
401
|
+
// ==========================================================================
|
|
402
|
+
|
|
403
|
+
await ensureMarketDirs();
|
|
404
|
+
const skillsDir = getSkillsDir();
|
|
405
|
+
const skillVersionDir = path.join(skillsDir, `${skillId}@${version}`);
|
|
406
|
+
|
|
407
|
+
await fs.ensureDir(skillVersionDir);
|
|
408
|
+
|
|
409
|
+
console.log('Downloading files...');
|
|
410
|
+
|
|
411
|
+
// 下载 SKILL.md
|
|
412
|
+
const basePath = source.path || '';
|
|
413
|
+
const skillMdPath = basePath ? `${basePath}/SKILL.md` : 'SKILL.md';
|
|
414
|
+
const skillMdContent = await fetchGitHubFile(source, skillMdPath);
|
|
415
|
+
if (skillMdContent) {
|
|
416
|
+
await fs.writeFile(path.join(skillVersionDir, 'SKILL.md'), skillMdContent);
|
|
417
|
+
console.log(' ✅ SKILL.md');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// 下载 package.json
|
|
421
|
+
const packageJsonPath = basePath ? `${basePath}/package.json` : 'package.json';
|
|
422
|
+
const packageJsonContent = await fetchGitHubFile(source, packageJsonPath);
|
|
423
|
+
if (packageJsonContent) {
|
|
424
|
+
await fs.writeFile(path.join(skillVersionDir, 'package.json'), packageJsonContent);
|
|
425
|
+
console.log(' ✅ package.json');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// 下载 metadata.json
|
|
429
|
+
const metadataPath = basePath ? `${basePath}/metadata.json` : 'metadata.json';
|
|
430
|
+
const metadataContent = await fetchGitHubFile(source, metadataPath);
|
|
431
|
+
if (metadataContent) {
|
|
432
|
+
await fs.writeFile(path.join(skillVersionDir, 'metadata.json'), metadataContent);
|
|
433
|
+
console.log(' ✅ metadata.json');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ==========================================================================
|
|
437
|
+
// 步骤 5: 格式转换(如果不全)
|
|
438
|
+
// ==========================================================================
|
|
439
|
+
|
|
440
|
+
const targetPlatforms = (options?.platforms as Platform[]) || detected.platforms;
|
|
441
|
+
const existingPlatforms = detected.platforms;
|
|
442
|
+
|
|
443
|
+
const missingPlatforms = targetPlatforms.filter(p => !existingPlatforms.includes(p));
|
|
444
|
+
|
|
445
|
+
if (missingPlatforms.length > 0) {
|
|
446
|
+
console.log(`\nGenerating adapters for missing platforms: ${missingPlatforms.join(', ')}`);
|
|
447
|
+
await generatePlatformAdapters(skillId, existingPlatforms, targetPlatforms, skillVersionDir);
|
|
448
|
+
console.log(' ✅ Platform adapters generated');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ==========================================================================
|
|
452
|
+
// 步骤 6: 创建 latest 软链接
|
|
453
|
+
// ==========================================================================
|
|
454
|
+
|
|
455
|
+
const skillDir = path.join(skillsDir, skillId);
|
|
456
|
+
await fs.ensureDir(skillDir);
|
|
457
|
+
|
|
458
|
+
const latestLink = path.join(skillDir, 'latest');
|
|
459
|
+
try {
|
|
460
|
+
await fs.remove(latestLink);
|
|
461
|
+
await fs.symlink(skillVersionDir, latestLink, 'junction');
|
|
462
|
+
} catch {
|
|
463
|
+
await fs.copy(skillVersionDir, path.join(skillDir, 'latest'), { overwrite: true });
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ==========================================================================
|
|
467
|
+
// 步骤 6: 安装到目标平台
|
|
468
|
+
// ==========================================================================
|
|
469
|
+
|
|
470
|
+
console.log(`\nInstalling to ${targetPlatforms.length} platform(s)...\n`);
|
|
471
|
+
|
|
472
|
+
const results: { name: string; status: 'installed' | 'skipped' | 'failed'; error?: string }[] = [];
|
|
473
|
+
|
|
474
|
+
for (const platform of targetPlatforms) {
|
|
475
|
+
const adapter = getAdapterByPlatform(platform as Platform);
|
|
476
|
+
if (!adapter) {
|
|
477
|
+
console.log(`${platform.padEnd(12)} ❌ Unknown platform`);
|
|
478
|
+
results.push({ name: platform, status: 'failed', error: 'Unknown platform' });
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
const isInstalled = await adapter.isInstalled(skillId);
|
|
484
|
+
|
|
485
|
+
if (isInstalled && !options?.force) {
|
|
486
|
+
console.log(`${adapter.name.padEnd(12)} ⚠️ Already installed (use --force to overwrite)`);
|
|
487
|
+
results.push({ name: adapter.name, status: 'skipped' });
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// 安装 skill 到平台目录
|
|
492
|
+
await adapter.install(skillId, skillVersionDir);
|
|
493
|
+
console.log(`${adapter.name.padEnd(12)} ✅ Installed successfully`);
|
|
494
|
+
results.push({ name: adapter.name, status: 'installed' });
|
|
495
|
+
} catch (error) {
|
|
496
|
+
console.log(`${adapter.name.padEnd(12)} ❌ Failed: ${error}`);
|
|
497
|
+
results.push({ name: adapter.name, status: 'failed', error: String(error) });
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// 显示摘要
|
|
502
|
+
const installed = results.filter(r => r.status === 'installed').length;
|
|
503
|
+
const skipped = results.filter(r => r.status === 'skipped').length;
|
|
504
|
+
const failed = results.filter(r => r.status === 'failed').length;
|
|
505
|
+
|
|
506
|
+
console.log(`\n📊 Summary: ${installed} installed, ${skipped} skipped, ${failed} failed`);
|
|
507
|
+
|
|
508
|
+
// ==========================================================================
|
|
509
|
+
|
|
510
|
+
// 这里复用 install.ts 中的平台安装逻辑
|
|
511
|
+
// 由于跨模块依赖,我将简化实现
|
|
512
|
+
console.log(`\nInstalling to platforms: ${targetPlatforms.join(', ')}`);
|
|
513
|
+
console.log(' (Platform installation logic needs to be completed)');
|
|
514
|
+
|
|
515
|
+
// TODO: 调用平台适配器的 install 方法
|
|
516
|
+
|
|
517
|
+
// ==========================================================================
|
|
518
|
+
// 步骤 8: 更新注册表
|
|
519
|
+
// ==========================================================================
|
|
520
|
+
|
|
521
|
+
const registry = await loadRegistry();
|
|
522
|
+
|
|
523
|
+
registry.skills[skillId] = {
|
|
524
|
+
id: skillId,
|
|
525
|
+
version: version,
|
|
526
|
+
installedAt: new Date().toISOString(),
|
|
527
|
+
platforms: targetPlatforms
|
|
528
|
+
} as InstalledSkill;
|
|
529
|
+
|
|
530
|
+
await saveRegistry(registry);
|
|
531
|
+
|
|
532
|
+
// ==========================================================================
|
|
533
|
+
// 完成
|
|
534
|
+
// ==========================================================================
|
|
535
|
+
|
|
536
|
+
console.log(`\n✅ ${skillId}@${version} installed successfully from GitHub!`);
|
|
537
|
+
console.log(` Source: ${source.owner}/${source.repo}`);
|
|
538
|
+
}
|