itismyskillmarket 1.0.10

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.
@@ -0,0 +1,237 @@
1
+ /**
2
+ * =============================================================================
3
+ * SkillMarket 安装命令模块
4
+ * =============================================================================
5
+ *
6
+ * 本模块实现 `skm install` 命令,用于安装 skill 到本地。
7
+ *
8
+ * 安装流程:
9
+ * 1. 确保目录结构存在
10
+ * 2. 从 npm 获取包信息
11
+ * 3. 下载包到缓存
12
+ * 4. 解压并复制到 skills 目录
13
+ * 5. 创建 latest 软链接
14
+ * 6. 更新本地注册表
15
+ *
16
+ * 安装后的目录结构:
17
+ * ~/.skillmarket/
18
+ * ├── skills/
19
+ * │ └── <skillId>/
20
+ * │ ├── latest -> <version>/ (软链接)
21
+ * │ └── <version>/
22
+ * │ ├── SKILL.md
23
+ * │ └── metadata.json
24
+ * └── ...
25
+ *
26
+ * @module commands/install
27
+ */
28
+
29
+ // -----------------------------------------------------------------------------
30
+ // 导入依赖
31
+ // -----------------------------------------------------------------------------
32
+
33
+ import fs from 'fs-extra'; // 文件系统操作
34
+ import path from 'path'; // 路径处理
35
+ import { exec } from 'child_process'; // 执行 shell 命令
36
+ import { promisify } from 'util'; // Promise 化工具
37
+
38
+ // 模块导入
39
+ import { fetchNpmPackage } from './npm.js'; // npm 查询
40
+ import { loadRegistry, saveRegistry } from './registry.js'; // 注册表操作
41
+ import { getCacheDir, getSkillsDir, ensureMarketDirs } from '../utils/dirs.js'; // 目录工具
42
+ import { detectPlatform } from '../utils/platform.js'; // 平台检测
43
+ import { LATEST_LINK } from '../constants.js'; // 常量
44
+ import type { InstalledSkill } from '../types.js'; // 类型定义
45
+
46
+ // 将 exec 转为 Promise 形式
47
+ const execAsync = promisify(exec);
48
+
49
+ // -----------------------------------------------------------------------------
50
+ // 安装函数
51
+ // -----------------------------------------------------------------------------
52
+
53
+ /**
54
+ * 安装指定的 skill
55
+ *
56
+ * @param {string} skillId - Skill 标识符(支持短格式或 scoped 格式)
57
+ * @param {string} [version] - 指定版本号(可选,不指定则安装最新版本)
58
+ * @returns {Promise<void>}
59
+ *
60
+ * @example
61
+ * // 安装最新版本的 brainstorming
62
+ * await installSkill('brainstorming');
63
+ *
64
+ * // 安装指定版本
65
+ * await installSkill('brainstorming', '1.0.0');
66
+ *
67
+ * // 安装 scoped 包
68
+ * await installSkill('@custom/skill');
69
+ */
70
+ export async function installSkill(
71
+ skillId: string,
72
+ version?: string
73
+ ): Promise<void> {
74
+ // ==========================================================================
75
+ // 步骤 0: 准备
76
+ // ==========================================================================
77
+
78
+ // 确保所有必要的目录都已创建
79
+ await ensureMarketDirs();
80
+
81
+ // 转换包名格式
82
+ // 支持 @itismyskillmarket/ 和 @skillmarket/ 两种 scope
83
+ let packageName: string;
84
+ if (skillId.startsWith('@')) {
85
+ // 直接使用用户提供的 scoped 包名
86
+ packageName = skillId;
87
+ } else {
88
+ // 默认尝试 @itismyskillmarket/,失败后回退到 @skillmarket/
89
+ packageName = `@itismyskillmarket/${skillId}`;
90
+ }
91
+
92
+ console.log(`Installing ${packageName}${version ? `@${version}` : ''}...`);
93
+
94
+ // ==========================================================================
95
+ // 步骤 1: 获取包信息
96
+ // ==========================================================================
97
+
98
+ // 从 npm 查询包的元信息
99
+ const pkgInfo = await fetchNpmPackage(packageName);
100
+ if (!pkgInfo) {
101
+ throw new Error(`Package ${packageName} not found`);
102
+ }
103
+
104
+ // 确定要安装的版本(用户指定版本 > 最新版本)
105
+ const targetVersion = version || pkgInfo['dist-tags']?.latest;
106
+ if (!targetVersion) {
107
+ throw new Error(`No version found for ${packageName}`);
108
+ }
109
+
110
+ // ==========================================================================
111
+ // 步骤 2: 下载包到缓存
112
+ // ==========================================================================
113
+
114
+ const cacheDir = getCacheDir();
115
+
116
+ // 计算目标缓存目录路径
117
+ // 例如: ~/.skillmarket/cache/@skillmarket%2Fbrainstorming@1.0.0/
118
+ const targetDir = path.join(cacheDir, `${packageName}@${targetVersion}`);
119
+
120
+ // 如果缓存已存在,跳过下载
121
+ if (!(await fs.pathExists(targetDir))) {
122
+ console.log('Downloading package...');
123
+ await fs.ensureDir(cacheDir);
124
+
125
+ try {
126
+ // 使用 npm pack 下载包到指定目录
127
+ // npm pack 会生成 .tgz 文件
128
+ await execAsync(`npm pack ${packageName}@${targetVersion} --pack-destination ${cacheDir}`);
129
+
130
+ // 查找下载的 tarball 文件
131
+ const files = await fs.readdir(cacheDir);
132
+
133
+ // npm pack 生成的文件名格式: <package-name>-<version>.tgz
134
+ // scoped 包格式: @scope-package-name-<version>.tgz
135
+ const tarball = files.find(f =>
136
+ f.endsWith('.tgz') &&
137
+ f.includes(packageName.replace('/', '-'))
138
+ );
139
+
140
+ if (tarball) {
141
+ // 解压 tarball
142
+ await execAsync(`tar -xzf "${path.join(cacheDir, tarball)}" -C "${cacheDir}"`);
143
+
144
+ // 删除 tarball(不再需要)
145
+ await fs.remove(path.join(cacheDir, tarball));
146
+
147
+ // npm 解压后目录名固定为 'package',需要重命名为目标版本目录
148
+ const extractedDir = path.join(cacheDir, 'package');
149
+ const finalDir = targetDir;
150
+
151
+ // 移动并覆盖(如果已存在)
152
+ await fs.move(extractedDir, finalDir, { overwrite: true });
153
+ }
154
+ } catch (err) {
155
+ throw new Error(`Failed to download package: ${err}`);
156
+ }
157
+ }
158
+
159
+ // ==========================================================================
160
+ // 步骤 3: 复制到 skills 目录
161
+ // ==========================================================================
162
+
163
+ const skillsDir = getSkillsDir();
164
+
165
+ // 创建版本目录: ~/.skillmarket/skills/<skillId>@<version>/
166
+ const skillVersionDir = path.join(skillsDir, `${skillId}@${targetVersion}`);
167
+
168
+ console.log('Setting up skill...');
169
+ await fs.ensureDir(skillVersionDir);
170
+
171
+ // 从缓存复制必要的文件到 skills 目录
172
+ const pkgRoot = targetDir;
173
+
174
+ // 复制 SKILL.md(skill 定义文件)
175
+ if (await fs.pathExists(path.join(pkgRoot, 'SKILL.md'))) {
176
+ await fs.copy(
177
+ path.join(pkgRoot, 'SKILL.md'),
178
+ path.join(skillVersionDir, 'SKILL.md')
179
+ );
180
+ }
181
+
182
+ // 复制 metadata.json(可选元数据文件)
183
+ if (await fs.pathExists(path.join(pkgRoot, 'metadata.json'))) {
184
+ await fs.copy(
185
+ path.join(pkgRoot, 'metadata.json'),
186
+ path.join(skillVersionDir, 'metadata.json')
187
+ );
188
+ }
189
+
190
+ // ==========================================================================
191
+ // 步骤 4: 创建 latest 软链接
192
+ // ==========================================================================
193
+
194
+ // skill 主目录: ~/.skillmarket/skills/<skillId>/
195
+ const skillDir = path.join(skillsDir, skillId);
196
+ await fs.ensureDir(skillDir);
197
+
198
+ // latest 软链接路径: ~/.skillmarket/skills/<skillId>/latest
199
+ const latestLink = path.join(skillDir, LATEST_LINK);
200
+
201
+ try {
202
+ // 删除已存在的软链接(如果有)
203
+ await fs.remove(latestLink);
204
+
205
+ // 创建软链接指向版本目录
206
+ // 'junction' 类型在 Windows 上不需要管理员权限
207
+ await fs.symlink(skillVersionDir, latestLink, 'junction');
208
+ } catch {
209
+ // Windows 上 junction 可能失败,降级为目录复制
210
+ await fs.copy(skillVersionDir, path.join(skillDir, LATEST_LINK), { overwrite: true });
211
+ }
212
+
213
+ // ==========================================================================
214
+ // 步骤 5: 更新注册表
215
+ // ==========================================================================
216
+
217
+ const registry = await loadRegistry();
218
+ const currentPlatform = detectPlatform();
219
+
220
+ // 添加/更新注册表中的 skill 记录
221
+ registry.skills[skillId] = {
222
+ id: skillId,
223
+ version: targetVersion,
224
+ installedAt: new Date().toISOString(),
225
+ platforms: [currentPlatform]
226
+ } as InstalledSkill;
227
+
228
+ // 保存注册表
229
+ await saveRegistry(registry);
230
+
231
+ // ==========================================================================
232
+ // 完成
233
+ // ==========================================================================
234
+
235
+ console.log(`\n✅ ${skillId}@${targetVersion} installed successfully!`);
236
+ console.log(` Use "skm info ${skillId}" for more details`);
237
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * =============================================================================
3
+ * SkillMarket 列表命令模块
4
+ * =============================================================================
5
+ *
6
+ * 本模块实现 `skm ls` 命令,用于:
7
+ * - 列出 npm registry 上可用的 skills
8
+ * - 显示本地已安装的 skills
9
+ *
10
+ * @module commands/ls
11
+ */
12
+
13
+ // -----------------------------------------------------------------------------
14
+ // 导入依赖
15
+ // -----------------------------------------------------------------------------
16
+
17
+ import {
18
+ loadRegistry, // 加载本地注册表
19
+ getInstalledSkills // 获取已安装 skills 列表
20
+ } from './registry.js';
21
+
22
+ import {
23
+ searchSkillmarketPackages, // 搜索 npm 上的 skill 包
24
+ fetchNpmPackage // 获取单个包的详细信息
25
+ } from './npm.js';
26
+
27
+ // -----------------------------------------------------------------------------
28
+ // 类型定义
29
+ // -----------------------------------------------------------------------------
30
+
31
+ /**
32
+ * ls 命令选项接口
33
+ */
34
+ interface LsOptions {
35
+ /** 仅显示已安装的 skills */
36
+ installed?: boolean;
37
+
38
+ /** 检查更新(预留功能) */
39
+ updates?: boolean;
40
+ }
41
+
42
+ // -----------------------------------------------------------------------------
43
+ // 命令实现
44
+ // -----------------------------------------------------------------------------
45
+
46
+ /**
47
+ * 列出 skills
48
+ *
49
+ * 根据选项显示 npm registry 上可用的 skills
50
+ * 或本地已安装的 skills
51
+ *
52
+ * @param {LsOptions} options - 命令选项
53
+ * @returns {Promise<void>}
54
+ *
55
+ * @example
56
+ * // 列出 npm 上的可用 skills
57
+ * await listSkills({});
58
+ *
59
+ * // 列出已安装的 skills
60
+ * await listSkills({ installed: true });
61
+ */
62
+ export async function listSkills(options: LsOptions): Promise<void> {
63
+ const { installed, updates } = options;
64
+
65
+ // -------------------------------------------------------------------------
66
+ // 模式1: 显示已安装的 skills
67
+ // -------------------------------------------------------------------------
68
+ if (installed) {
69
+ const skills = await getInstalledSkills();
70
+
71
+ // 无已安装 skills 时给出提示
72
+ if (skills.length === 0) {
73
+ console.log('No skills installed yet. Run "skm --ls" to see available skills.');
74
+ return;
75
+ }
76
+
77
+ // 打印表头
78
+ console.log('Installed Skills:\n');
79
+
80
+ // 遍历并打印每个 skill 的详细信息
81
+ for (const skill of skills) {
82
+ // skill 名称和版本
83
+ console.log(` ${skill.id}@${skill.version}`);
84
+
85
+ // 支持的平台列表
86
+ console.log(` Platforms: ${skill.platforms.join(', ')}`);
87
+
88
+ // 安装时间
89
+ console.log(` Installed: ${skill.installedAt}`);
90
+
91
+ // 空行分隔
92
+ console.log();
93
+ }
94
+
95
+ return;
96
+ }
97
+
98
+ // -------------------------------------------------------------------------
99
+ // 模式2: 显示 npm 上可用的 skills
100
+ // -------------------------------------------------------------------------
101
+
102
+ // 提示用户正在搜索
103
+ console.log('Searching npm registry...\n');
104
+
105
+ try {
106
+ // 调用 npm search API 搜索 skillmarket 相关包
107
+ const packages = await searchSkillmarketPackages();
108
+
109
+ // 无搜索结果时
110
+ if (packages.length === 0) {
111
+ console.log('No skills found. Check back later!');
112
+ return;
113
+ }
114
+
115
+ // 打印找到的包数量
116
+ console.log(`Found ${packages.length} skill(s):\n`);
117
+
118
+ // 遍历每个包,获取详细信息并显示
119
+ for (const pkgName of packages) {
120
+ try {
121
+ const info = await fetchNpmPackage(pkgName);
122
+
123
+ if (!info) {
124
+ // 如果获取失败,仍然显示包名
125
+ console.log(`📦 ${pkgName} (信息获取失败)`);
126
+ console.log();
127
+ continue;
128
+ }
129
+
130
+ // 获取最新版本号
131
+ const latestVersion = info['dist-tags']?.latest || 'unknown';
132
+
133
+ // 获取该版本的详细信息
134
+ const pkg = info.versions?.[latestVersion];
135
+
136
+ // 获取 skillmarket 元数据
137
+ const skillMeta = pkg?.skillmarket;
138
+
139
+ // 打印包名和版本
140
+ console.log(`📦 ${info.name}@${latestVersion}`);
141
+
142
+ // 打印显示名称
143
+ const displayName = skillMeta?.displayName || info.name;
144
+ console.log(` 名称: ${displayName}`);
145
+
146
+ // 打印描述
147
+ console.log(` 描述: ${pkg?.description || 'N/A'}`);
148
+
149
+ // 打印支持平台
150
+ const platforms = skillMeta?.platforms || [];
151
+ console.log(` 平台: ${platforms.length > 0 ? platforms.join(', ') : 'N/A'}`);
152
+
153
+ // 打印 npm 链接
154
+ const npmLink = pkg?.links?.npm || `https://www.npmjs.com/package/${info.name}`;
155
+ console.log(` 链接: ${npmLink}`);
156
+
157
+ // 空行分隔
158
+ console.log();
159
+ } catch (e) {
160
+ // 错误时仍显示包名
161
+ console.log(`📦 ${pkgName} (获取失败: ${e})`);
162
+ console.log();
163
+ }
164
+ }
165
+ } catch (error) {
166
+ // 网络错误处理
167
+ console.log(`Error fetching skills: ${error}`);
168
+ }
169
+ }
@@ -0,0 +1,261 @@
1
+ /**
2
+ * =============================================================================
3
+ * SkillMarket NPM Registry 查询模块
4
+ * =============================================================================
5
+ *
6
+ * 本模块负责与 npm registry 通信,实现以下功能:
7
+ * - 获取指定 npm 包的信息
8
+ * - 搜索带有 skillmarket 关键字的包
9
+ *
10
+ * 使用原生 Node.js https 模块,无需额外依赖。
11
+ *
12
+ * npm Registry API 文档: https://docs.npmjs.com/cli/v8/using-npm/registry
13
+ *
14
+ * @module commands/npm
15
+ */
16
+
17
+ // -----------------------------------------------------------------------------
18
+ // 导入依赖
19
+ // -----------------------------------------------------------------------------
20
+
21
+ import https from 'https'; // Node.js 原生 HTTPS 模块,用于发送 HTTP 请求
22
+ import { URL } from 'url'; // URL 解析和构建工具
23
+
24
+ // -----------------------------------------------------------------------------
25
+ // 类型定义
26
+ // -----------------------------------------------------------------------------
27
+
28
+ /**
29
+ * npm 包版本信息
30
+ *
31
+ * 定义从 npm registry 返回的单个版本对象的结构
32
+ */
33
+ interface NpmPackage {
34
+ /** 包名称 */
35
+ name: string;
36
+
37
+ /** 版本号,遵循 semver 规范 */
38
+ version: string;
39
+
40
+ /** 包描述信息 */
41
+ description?: string;
42
+
43
+ /** npm 链接 */
44
+ links?: {
45
+ npm?: string;
46
+ homepage?: string;
47
+ repository?: string;
48
+ bugs?: string;
49
+ };
50
+
51
+ /**
52
+ * SkillMarket 特有的元数据
53
+ *
54
+ * 在 package.json 的 skillmarket 字段中定义,
55
+ * 包含 skill 的平台兼容性等信息
56
+ */
57
+ skillmarket?: {
58
+ /** Skill 唯一标识符 */
59
+ id?: string;
60
+
61
+ /** 友好显示名称 */
62
+ displayName?: string;
63
+
64
+ /** 详细描述 */
65
+ description?: string;
66
+
67
+ /** 支持的平台列表 */
68
+ platforms?: string[];
69
+
70
+ /** 默认版本标识 */
71
+ defaultVersion?: string;
72
+ };
73
+ }
74
+
75
+ /**
76
+ * npm registry API 响应结构
77
+ *
78
+ * 对应 npm registry 的完整包信息响应
79
+ */
80
+ interface NpmRegistryResponse {
81
+ /** 包名称 */
82
+ name: string;
83
+
84
+ /** 所有版本的字典,key 是版本号 */
85
+ versions: Record<string, NpmPackage>;
86
+
87
+ /** 发行标签,如 { latest: "1.0.0", beta: "1.1.0-beta.1" } */
88
+ 'dist-tags': Record<string, string>;
89
+ }
90
+
91
+ /**
92
+ * npm search API 响应中的包对象
93
+ */
94
+ interface SearchPackage {
95
+ /** 包名 */
96
+ name: string;
97
+ /** 版本 */
98
+ version: string;
99
+ }
100
+
101
+ // -----------------------------------------------------------------------------
102
+ // 获取包信息
103
+ // -----------------------------------------------------------------------------
104
+
105
+ /**
106
+ * 获取指定 npm 包的完整信息
107
+ *
108
+ * 向 npm registry API 发送请求,获取包的详细信息,
109
+ * 包括所有版本、描述、依赖等。
110
+ *
111
+ * API 端点: GET https://registry.npmjs.org/{package}
112
+ *
113
+ * @param {string} packageName - 包名称(支持 @scope/name 格式)
114
+ * @returns {Promise<NpmRegistryResponse | null>} 包信息,失败返回 null
115
+ *
116
+ * @example
117
+ * // 获取官方 commander 包信息
118
+ * const info = await fetchNpmPackage('commander');
119
+ * if (info) {
120
+ * console.log(`最新版本: ${info['dist-tags'].latest}`);
121
+ * }
122
+ *
123
+ * // 获取 scoped 包
124
+ * const scoped = await fetchNpmPackage('@skillmarket/brainstorming');
125
+ */
126
+ export async function fetchNpmPackage(packageName: string): Promise<NpmRegistryResponse | null> {
127
+ return new Promise((resolve, reject) => {
128
+ // 构建 npm registry URL
129
+ // scoped 包(如 @foo/bar)需要特殊处理,保留 @ 符号
130
+ // @foo/bar -> @foo%2Fbar (URL encoded)
131
+ const isScoped = packageName.startsWith('@');
132
+ let encodedName: string;
133
+
134
+ if (isScoped) {
135
+ // 对 scoped 包进行正确编码:@foo/bar -> @foo%2Fbar
136
+ const scopeAndName = packageName.substring(1); // 去掉 @
137
+ const slashIndex = scopeAndName.indexOf('/');
138
+ if (slashIndex > 0) {
139
+ const scope = scopeAndName.substring(0, slashIndex);
140
+ const name = scopeAndName.substring(slashIndex + 1);
141
+ encodedName = `@${encodeURIComponent(scope)}%2F${encodeURIComponent(name)}`;
142
+ } else {
143
+ encodedName = packageName; // fallback
144
+ }
145
+ } else {
146
+ encodedName = encodeURIComponent(packageName);
147
+ }
148
+
149
+ const url = new URL(`https://registry.npmjs.org/${encodedName}`);
150
+
151
+ // 发送 HTTPS GET 请求
152
+ const req = https.get(url.toString(), { timeout: 10000 }, (res) => {
153
+ let data = '';
154
+
155
+ // 收集响应数据
156
+ res.on('data', chunk => { data += chunk; });
157
+
158
+ res.on('end', () => {
159
+ try {
160
+ // 解析 JSON 响应
161
+ const parsed = JSON.parse(data);
162
+
163
+ // 检查 npm 返回的错误
164
+ // npm 对于不存在的包也返回 200,但 body 中有 error 字段
165
+ if (parsed.error) {
166
+ resolve(null);
167
+ return;
168
+ }
169
+
170
+ // 成功解析,返回包信息
171
+ resolve(parsed);
172
+ } catch {
173
+ // JSON 解析失败,返回 null
174
+ resolve(null);
175
+ }
176
+ });
177
+ });
178
+
179
+ // 处理网络错误
180
+ req.on('error', reject);
181
+
182
+ // 处理请求超时(10秒)
183
+ req.on('timeout', () => {
184
+ req.destroy();
185
+ reject(new Error('Request timeout'));
186
+ });
187
+ });
188
+ }
189
+
190
+ // -----------------------------------------------------------------------------
191
+ // 搜索包
192
+ // -----------------------------------------------------------------------------
193
+
194
+ /**
195
+ * 搜索 SkillMarket 相关的 npm 包
196
+ *
197
+ * 使用 npm search API 搜索带有 'skillmarket' 关键字的包
198
+ *
199
+ * API 端点: GET https://registry.npmjs.org/-/v1/search
200
+ *
201
+ * @returns {Promise<string[]>} 匹配的包名数组
202
+ *
203
+ * @example
204
+ * const packages = await searchSkillmarketPackages();
205
+ * console.log(`找到 ${packages.length} 个 skill 包`);
206
+ * packages.forEach(name => {
207
+ * console.log(`- ${name}`);
208
+ * });
209
+ */
210
+ export async function searchSkillmarketPackages(): Promise<string[]> {
211
+ const packages: string[] = [];
212
+
213
+ return new Promise((resolve, reject) => {
214
+ // 构建 search API URL
215
+ const url = new URL('https://registry.npmjs.org/-/v1/search');
216
+
217
+ // 设置搜索参数
218
+ // text: 搜索关键字
219
+ // size: 返回结果数量上限
220
+ url.searchParams.set('text', 'keywords:skillmarket');
221
+ url.searchParams.set('size', '100');
222
+
223
+ const req = https.get(url.toString(), { timeout: 10000 }, (res) => {
224
+ let data = '';
225
+
226
+ // 收集响应数据
227
+ res.on('data', chunk => { data += chunk; });
228
+
229
+ res.on('end', () => {
230
+ try {
231
+ // 解析搜索结果
232
+ const result = JSON.parse(data);
233
+
234
+ // 提取所有匹配的包名
235
+ // npm search 返回结构: { objects: [{ package: { name: "..." } }] }
236
+ if (result.objects) {
237
+ for (const item of result.objects) {
238
+ if (item?.package?.name) {
239
+ packages.push(item.package.name);
240
+ }
241
+ }
242
+ }
243
+
244
+ resolve(packages);
245
+ } catch {
246
+ // 解析失败返回空数组
247
+ resolve([]);
248
+ }
249
+ });
250
+ });
251
+
252
+ // 处理网络错误
253
+ req.on('error', reject);
254
+
255
+ // 处理请求超时
256
+ req.on('timeout', () => {
257
+ req.destroy();
258
+ reject(new Error('Request timeout'));
259
+ });
260
+ });
261
+ }