667 lines
21 KiB
JavaScript
667 lines
21 KiB
JavaScript
|
|
#!/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();
|
|||
|
|
}
|