Files
getskills/index.js

667 lines
21 KiB
JavaScript
Raw Normal View History

#!/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<Array>} 技能列表包含 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<Object>} 技能详情
*/
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<string>} 本地仓库路径
*/
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 <URL> - 设置自定义 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 <URL> - 设置自定义 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();
}