#!/usr/bin/env node const https = require('https'); const http = require('http'); const fs = require('fs'); const path = require('path'); const os = require('os'); const { exec } = require('child_process'); /** * 基础 URL 配置 * 可以通过环境变量 GETSKILL_BASE_URL 自定义 */ let BASE_URL = process.env.GETSKILL_BASE_URL || 'https://getskills.certer'; /** * 设置自定义 BASE_URL * @param {string} url - 自定义的基础 URL */ function setBaseUrl(url) { BASE_URL = url.replace(/\/$/, ''); // 移除末尾的斜杠 console.log(`BASE_URL 已设置为: ${BASE_URL}`); } /** * 获取当前 BASE_URL * @returns {string} 当前的基础 URL */ function getBaseUrl() { return BASE_URL; } /** * 检查 Git 是否已安装 */ async function checkGitInstalled() { try { await executeCommand('git --version'); return true; } catch (error) { return false; } } /** * 获取 Git 下载链接(根据操作系统) */ function getGitDownloadUrl() { const platform = os.platform(); const arch = os.arch(); switch (platform) { case 'win32': // Windows if (arch === 'x64') { return 'https://github.com/git-for-windows/git/releases/download/v2.44.0.windows.1/Git-2.44.0-64-bit.exe'; } else { return 'https://github.com/git-for-windows/git/releases/download/v2.44.0.windows.1/Git-2.44.0-32-bit.exe'; } case 'darwin': // macOS - 提示使用 Homebrew return 'homebrew'; // 特殊标记 case 'linux': // Linux - 提示使用包管理器 return 'package-manager'; // 特殊标记 default: return null; } } /** * 下载文件 */ function downloadFile(url, destPath) { return new Promise((resolve, reject) => { const urlObj = new URL(url); const protocol = urlObj.protocol === 'https:' ? https : http; const file = fs.createWriteStream(destPath); protocol.get(url, (response) => { // 处理重定向 if (response.statusCode === 301 || response.statusCode === 302) { file.close(); fs.unlinkSync(destPath); return downloadFile(response.headers.location, destPath) .then(resolve) .catch(reject); } if (response.statusCode !== 200) { file.close(); fs.unlinkSync(destPath); return reject(new Error(`下载失败,状态码: ${response.statusCode}`)); } const totalSize = parseInt(response.headers['content-length'], 10); let downloadedSize = 0; response.on('data', (chunk) => { downloadedSize += chunk.length; const percent = ((downloadedSize / totalSize) * 100).toFixed(2); process.stdout.write(`\r下载进度: ${percent}%`); }); response.pipe(file); file.on('finish', () => { file.close(); console.log('\n下载完成!'); resolve(destPath); }); }).on('error', (err) => { file.close(); fs.unlinkSync(destPath); reject(err); }); }); } /** * 安装 Git */ async function installGit() { const platform = os.platform(); const downloadUrl = getGitDownloadUrl(); console.log('检测到系统未安装 Git,正在准备安装...\n'); if (downloadUrl === 'homebrew') { console.log('macOS 系统检测到未安装 Git'); console.log('请使用以下命令安装 Git:\n'); console.log(' brew install git'); console.log('\n如果未安装 Homebrew,请访问: https://brew.sh/'); throw new Error('请先安装 Git 后再运行此命令'); } if (downloadUrl === 'package-manager') { console.log('Linux 系统检测到未安装 Git'); console.log('请使用系统包管理器安装 Git:\n'); console.log(' Ubuntu/Debian: sudo apt-get install git'); console.log(' CentOS/RHEL: sudo yum install git'); console.log(' Fedora: sudo dnf install git'); console.log(' Arch: sudo pacman -S git'); throw new Error('请先安装 Git 后再运行此命令'); } if (platform === 'win32') { console.log('Windows 系统检测到未安装 Git'); console.log('正在下载 Git 安装程序...\n'); const tempDir = os.tmpdir(); const installerPath = path.join(tempDir, 'git-installer.exe'); try { await downloadFile(downloadUrl, installerPath); console.log(`\nGit 安装程序已下载到: ${installerPath}`); console.log('\n正在启动安装程序,请按照提示完成安装...'); console.log('安装完成后,请重新运行此命令。\n'); // 启动安装程序 exec(`"${installerPath}"`, (error) => { if (error) { console.error('启动安装程序失败:', error.message); } }); throw new Error('请完成 Git 安装后重新运行此命令'); } catch (error) { console.error('\n下载 Git 安装程序失败:', error.message); console.log('\n您可以手动访问以下网址下载 Git:'); console.log('https://git-scm.com/download/win'); throw new Error('请先安装 Git 后再运行此命令'); } } } /** * 确保 Git 已安装 */ async function ensureGitInstalled() { const isInstalled = await checkGitInstalled(); if (!isInstalled) { await installGit(); } return true; } /** * 获取 OpenClaw skills 目录路径(根据操作系统) */ function getSkillsDirectory() { const platform = os.platform(); const homeDir = os.homedir(); switch (platform) { case 'win32': // Windows: %USERPROFILE%\.claude\skills return path.join(homeDir, '.claude', 'skills'); case 'darwin': // macOS: ~/.claude/skills return path.join(homeDir, '.claude', 'skills'); case 'linux': // Linux: ~/.claude/skills return path.join(homeDir, '.claude', 'skills'); default: // 默认使用 ~/.claude/skills return path.join(homeDir, '.claude', 'skills'); } } /** * 确保目录存在 */ function ensureDirectoryExists(dirPath) { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } } /** * HTTP/HTTPS 请求封装 */ function request(url, options = {}) { return new Promise((resolve, reject) => { const urlObj = new URL(url); const protocol = urlObj.protocol === 'https:' ? https : http; const reqOptions = { hostname: urlObj.hostname, port: urlObj.port, path: urlObj.pathname + urlObj.search, method: options.method || 'GET', headers: options.headers || {} }; const req = protocol.request(reqOptions, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { if (res.statusCode >= 200 && res.statusCode < 300) { resolve({ statusCode: res.statusCode, headers: res.headers, body: data }); } else { reject(new Error(`Request failed with status code ${res.statusCode}`)); } }); }); req.on('error', (err) => { reject(err); }); if (options.body) { req.write(options.body); } req.end(); }); } /** * 执行命令 */ function executeCommand(command, cwd) { return new Promise((resolve, reject) => { exec(command, { cwd, maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => { if (error) { reject(error); } else { resolve({ stdout, stderr }); } }); }); } /** * 搜索技能(从配置的 BASE_URL API) * @param {string} keyword - 搜索关键词 * @param {Object} options - 搜索选项 * @param {string} options.sort - 排序字段,默认 'updated' * @param {string} options.order - 排序顺序,默认 'desc' * @param {number} options.page - 页码,默认 1 * @param {number} options.limit - 每页数量,默认 20 * @returns {Promise} 技能列表(包含 git 地址) */ async function searchSkills(keyword, options = {}) { const { sort = 'updated', order = 'desc', page = 1, limit = 20 } = options; const url = `${BASE_URL}/repo/search?sort=${sort}&order=${order}&q=${encodeURIComponent(keyword)}&page=${page}&limit=${limit}`; try { const response = await request(url); const result = JSON.parse(response.body); // 解析返回格式: { ok: true, data: [{ repository: {...} }] } if (!result.ok || !result.data) { return []; } // 提取需要的字段 return result.data.map(item => { const repo = item.repository || {}; return { name: repo.full_name || '', full_name: repo.full_name || '', description: repo.description || '', html_url: repo.html_url || '', git_url: repo.html_url ? `${repo.html_url}.git` : '', clone_url: repo.clone_url || '' }; }); } catch (error) { console.error('搜索技能失败:', error.message); throw error; } } /** * 获取技能详情(包含 git 地址) * @param {string} skillId - 技能ID或名称 * @returns {Promise} 技能详情 */ async function getSkillDetail(skillId) { const url = `${BASE_URL}/repo/${encodeURIComponent(skillId)}`; try { const response = await request(url); const result = JSON.parse(response.body); // 如果返回的数据格式类似搜索接口,需要解析 repository if (result.repository) { const repo = result.repository; return { name: repo.full_name || repo.name || skillId, full_name: repo.full_name || '', description: repo.description || '', html_url: repo.html_url || '', git_url: repo.html_url ? `${repo.html_url}.git` : '', clone_url: repo.clone_url || '' }; } // 如果是直接返回数据(向后兼容) return { name: result.full_name || result.name || skillId, full_name: result.full_name || '', description: result.description || '', html_url: result.html_url || '', git_url: result.html_url ? `${result.html_url}.git` : (result.git_url || ''), clone_url: result.clone_url || '' }; } catch (error) { console.error('获取技能详情失败:', error.message); throw error; } } /** * 获取技能仓库缓存目录 */ function getSkillsCacheDirectory() { const homeDir = os.homedir(); return path.join(homeDir, '.claude', 'skills-cache'); } /** * 克隆或更新 Git 仓库 * @param {string} gitUrl - Git 仓库地址 * @param {string} skillName - 技能名称 * @returns {Promise} 本地仓库路径 */ async function cloneOrUpdateRepo(gitUrl, skillName) { // 首先检查并确保 Git 已安装 await ensureGitInstalled(); const cacheDir = getSkillsCacheDirectory(); ensureDirectoryExists(cacheDir); const repoPath = path.join(cacheDir, skillName); if (fs.existsSync(repoPath)) { // 仓库已存在,执行 git pull console.log(`更新仓库: ${skillName}...`); try { await executeCommand('git pull', repoPath); console.log(`仓库更新成功: ${repoPath}`); } catch (error) { console.error(`更新失败: ${error.message}`); throw error; } } else { // 克隆新仓库 console.log(`克隆仓库: ${gitUrl}...`); try { await executeCommand(`git clone "${gitUrl}" "${skillName}"`, cacheDir); console.log(`仓库克隆成功: ${repoPath}`); } catch (error) { console.error(`克隆失败: ${error.message}`); throw error; } } return repoPath; } /** * 从仓库复制技能文件到 skills 目录 * @param {string} repoPath - 仓库路径 * @param {string} skillName - 技能名称 */ function copySkillFiles(repoPath) { const skillsDir = getSkillsDirectory(); ensureDirectoryExists(skillsDir); // 查找仓库中的 .md 文件 const files = fs.readdirSync(repoPath); const mdFiles = files.filter(file => file.endsWith('.md') && !file.toLowerCase().startsWith('readme') ); if (mdFiles.length === 0) { throw new Error(`在仓库中未找到技能 .md 文件`); } const copiedFiles = []; mdFiles.forEach(file => { const sourcePath = path.join(repoPath, file); const targetPath = path.join(skillsDir, file); fs.copyFileSync(sourcePath, targetPath); console.log(`已复制: ${file} -> ${targetPath}`); copiedFiles.push(targetPath); }); return copiedFiles; } /** * 下载技能(通过 Git) * @param {string} skillIdOrName - 技能ID或名称 */ async function downloadSkill(skillIdOrName) { try { console.log(`获取技能信息: ${skillIdOrName}...`); const skillDetail = await getSkillDetail(skillIdOrName); // 支持 clone_url 或 git_url const gitUrl = skillDetail.clone_url || skillDetail.git_url; if (!gitUrl) { throw new Error('技能信息中未包含 git_url 或 clone_url'); } const skillName = skillDetail.name || skillIdOrName; const repoPath = await cloneOrUpdateRepo(gitUrl, skillName); const copiedFiles = copySkillFiles(repoPath); return { skillName, repoPath, files: copiedFiles }; } catch (error) { console.error(`下载技能失败: ${error.message}`); throw error; } } /** * 更新技能(通过 Git) * @param {string} skillName - 技能名称 */ async function updateSkill(skillName) { try { console.log(`正在更新技能: ${skillName}...`); const cacheDir = getSkillsCacheDirectory(); const repoPath = path.join(cacheDir, skillName); if (!fs.existsSync(repoPath)) { console.log(`技能仓库不存在,将重新下载...`); return await downloadSkill(skillName); } // 执行 git pull await executeCommand('git pull', repoPath); console.log(`Git 仓库已更新`); // 复制文件到 skills 目录 const copiedFiles = copySkillFiles(repoPath); console.log(`技能 ${skillName} 更新成功!`); return { skillName, repoPath, files: copiedFiles }; } catch (error) { console.error(`更新技能 ${skillName} 失败:`, error.message); throw error; } } /** * 列出本地已安装的技能 */ function listLocalSkills() { const skillsDir = getSkillsDirectory(); if (!fs.existsSync(skillsDir)) { console.log('技能目录不存在'); return []; } const files = fs.readdirSync(skillsDir); const skillFiles = files.filter(file => file.endsWith('.md')); return skillFiles; } /** * 命令行接口 */ async function cli() { const args = process.argv.slice(2); const command = args[0]; try { switch (command) { case 'search': if (!args[1]) { console.error('请提供搜索关键词'); console.log('用法: getskill search <关键词>'); process.exit(1); } const results = await searchSkills(args[1]); console.log(`找到 ${results.length} 个技能:\n`); results.forEach((skill, index) => { console.log(`${index + 1}. ${skill.full_name}`); if (skill.description) { console.log(` 描述: ${skill.description}`); } if (skill.git_url) { console.log(` Git: ${skill.git_url}`); } console.log(''); }); break; case 'install': case 'get': case 'download': if (!args[1]) { console.error('请提供技能ID或名称'); console.log('用法: getskill install <技能名称>'); process.exit(1); } const result = await downloadSkill(args[1]); console.log(`\n技能已安装到 skills 目录:`); result.files.forEach(file => console.log(` - ${file}`)); console.log(`\nGit 仓库缓存: ${result.repoPath}`); break; case 'update': if (!args[1]) { console.error('请提供技能名称'); console.log('用法: getskill update <技能名称>'); process.exit(1); } const updateResult = await updateSkill(args[1]); console.log(`\n技能已更新到 skills 目录:`); updateResult.files.forEach(file => console.log(` - ${file}`)); break; case 'list': const skills = listLocalSkills(); console.log(`本地已安装的技能 (${skills.length}):`); skills.forEach((skill, index) => { console.log(`${index + 1}. ${skill}`); }); break; case 'path': console.log(`技能目录: ${getSkillsDirectory()}`); console.log(`缓存目录: ${getSkillsCacheDirectory()}`); break; case 'config': if (args[1] === 'set' && args[2]) { setBaseUrl(args[2]); } else if (args[1] === 'get') { console.log(`当前 BASE_URL: ${getBaseUrl()}`); } else { console.log('用法:'); console.log(' getskill config set - 设置自定义 API 地址'); console.log(' getskill config get - 查看当前 API 地址'); } break; case 'clean': const cacheDir = getSkillsCacheDirectory(); if (fs.existsSync(cacheDir)) { fs.rmSync(cacheDir, { recursive: true, force: true }); console.log(`已清理缓存目录: ${cacheDir}`); } else { console.log('缓存目录不存在'); } break; default: console.log('GetSkill - OpenClaw 技能管理工具'); console.log(''); console.log('用法:'); console.log(' getskill search <关键词> - 从 API 搜索技能'); console.log(' getskill install <技能名称> - 通过 git clone 安装技能'); console.log(' getskill update <技能名称> - 通过 git pull 更新技能'); console.log(' getskill list - 列出本地技能'); console.log(' getskill path - 显示目录路径'); console.log(' getskill config set - 设置自定义 API 地址'); console.log(' getskill config get - 查看当前 API 地址'); console.log(' getskill clean - 清理 git 缓存'); console.log(''); console.log(`当前 API: ${getBaseUrl()}`); console.log(`技能目录: ${getSkillsDirectory()}`); console.log(`缓存目录: ${getSkillsCacheDirectory()}`); } } catch (error) { console.error('执行命令时出错:', error.message); process.exit(1); } } // 导出模块函数 module.exports = { searchSkills, getSkillDetail, downloadSkill, updateSkill, listLocalSkills, getSkillsDirectory, getSkillsCacheDirectory, cloneOrUpdateRepo, copySkillFiles, checkGitInstalled, ensureGitInstalled, setBaseUrl, getBaseUrl }; // 如果直接运行此文件,执行 CLI if (require.main === module) { cli(); }