rushangle-cli 0.3.0 → 0.4.1
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/package.json +1 -1
- package/src/api.js +58 -0
- package/src/commands/install.js +68 -1
- package/src/commands/list.js +24 -3
- package/src/commands/publish.js +62 -5
- package/src/commands/unpublish.js +3 -2
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -87,6 +87,64 @@ module.exports = {
|
|
|
87
87
|
deleteDoc(id) { return apiCall('DELETE', `/api/docs/${encodeURIComponent(id)}`); },
|
|
88
88
|
myDocs() { return apiCall('GET', '/api/docs/my'); },
|
|
89
89
|
|
|
90
|
+
// CLI Tools
|
|
91
|
+
listCli(params = {}) {
|
|
92
|
+
const qs = new URLSearchParams(params).toString();
|
|
93
|
+
return apiCall('GET', `/api/cli${qs ? '?' + qs : ''}`);
|
|
94
|
+
},
|
|
95
|
+
getCli(id) { return apiCall('GET', `/api/cli/${encodeURIComponent(id)}`); },
|
|
96
|
+
publishCli(data) { return apiCall('POST', '/api/cli', data); },
|
|
97
|
+
updateCli(id, data) { return apiCall('PUT', `/api/cli/${encodeURIComponent(id)}`, data); },
|
|
98
|
+
deleteCli(id) { return apiCall('DELETE', `/api/cli/${encodeURIComponent(id)}`); },
|
|
99
|
+
installCli(id) { return apiCall('POST', `/api/cli/${encodeURIComponent(id)}/install`); },
|
|
100
|
+
installedCli() { return apiCall('GET', '/api/cli/installed'); },
|
|
101
|
+
myCli() { return apiCall('GET', '/api/cli/my'); },
|
|
102
|
+
|
|
103
|
+
// CLI file upload (native FormData — compatible with backend busboy/multer)
|
|
104
|
+
async uploadCliFiles(cliId, files) {
|
|
105
|
+
const token = auth.getToken();
|
|
106
|
+
const form = new FormData();
|
|
107
|
+
for (const { fieldName, filePath } of files) {
|
|
108
|
+
const content = fs.readFileSync(filePath);
|
|
109
|
+
const blob = new Blob([content], { type: 'application/octet-stream' });
|
|
110
|
+
form.append(fieldName, blob, path.basename(filePath));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const res = await fetch(`${SITE_URL}/api/cli/${encodeURIComponent(cliId)}/files`, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
116
|
+
body: form,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const data = await res.json();
|
|
120
|
+
if (!res.ok) {
|
|
121
|
+
const err = new Error(data.error || 'Upload failed');
|
|
122
|
+
err.status = res.status;
|
|
123
|
+
err.data = data;
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
return data;
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
// CLI file download
|
|
130
|
+
async downloadCliFiles(cliId) {
|
|
131
|
+
const token = auth.getToken();
|
|
132
|
+
const res = await fetch(`${SITE_URL}/api/cli/${encodeURIComponent(cliId)}/download`, {
|
|
133
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (!res.ok) {
|
|
137
|
+
const data = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
|
|
138
|
+
const err = new Error(data.error || 'Download failed');
|
|
139
|
+
err.status = res.status;
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const contentType = res.headers.get('content-type') || '';
|
|
144
|
+
const arrayBuffer = await res.arrayBuffer();
|
|
145
|
+
return { buffer: Buffer.from(arrayBuffer), contentType };
|
|
146
|
+
},
|
|
147
|
+
|
|
90
148
|
// Search
|
|
91
149
|
search(query) { return apiCall('GET', `/api/search?q=${encodeURIComponent(query)}`); },
|
|
92
150
|
|
package/src/commands/install.js
CHANGED
|
@@ -21,7 +21,7 @@ function resolveInstallDir(opts) {
|
|
|
21
21
|
module.exports = new Command('install')
|
|
22
22
|
.description('安装技能/MCP/代码段')
|
|
23
23
|
.argument('[skill]', '技能名称或 ID')
|
|
24
|
-
.option('-t, --type <type>', '类型: skill | mcp | code | doc', 'skill')
|
|
24
|
+
.option('-t, --type <type>', '类型: skill | mcp | code | doc | cli', 'skill')
|
|
25
25
|
.option('-g, --global', '全局安装(默认)', true)
|
|
26
26
|
.option('-d, --dir <dir>', '自定义安装目录(覆盖 config 和默认值)')
|
|
27
27
|
.option('--register-only', '仅注册到服务器,不写入本地文件(适用于受限环境)')
|
|
@@ -295,6 +295,73 @@ module.exports = new Command('install')
|
|
|
295
295
|
console.log(chalk.green(`✓ 安装成功: ${item.name}`));
|
|
296
296
|
if (showLocalPath) console.log(chalk.gray(` 本地目录: ${installDir}`));
|
|
297
297
|
|
|
298
|
+
} else if (type === 'cli') {
|
|
299
|
+
// Try by ID first, then search by name
|
|
300
|
+
try {
|
|
301
|
+
item = await api.getCli(nameOrId);
|
|
302
|
+
} catch {
|
|
303
|
+
// Search by name
|
|
304
|
+
const data = await api.listCli({ search: nameOrId });
|
|
305
|
+
const items = data.items || [];
|
|
306
|
+
if (items.length > 0) {
|
|
307
|
+
const match = items.find(
|
|
308
|
+
i => i.name.toLowerCase() === nameOrId.toLowerCase() || i.id === nameOrId
|
|
309
|
+
) || items[0];
|
|
310
|
+
if (match) {
|
|
311
|
+
item = await api.getCli(match.id);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!item) {
|
|
317
|
+
console.log(chalk.red(`未找到 CLI 工具: ${nameOrId}`));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Register install on server
|
|
322
|
+
try { await api.installCli(item.id); } catch { /* non-critical */ }
|
|
323
|
+
|
|
324
|
+
// Download and extract files
|
|
325
|
+
let showLocalPath = false;
|
|
326
|
+
let installDir = '';
|
|
327
|
+
if (!opts.registerOnly) {
|
|
328
|
+
try {
|
|
329
|
+
installDir = path.join(skillDirBase, 'cli', item.id);
|
|
330
|
+
fs.mkdirSync(installDir, { recursive: true });
|
|
331
|
+
|
|
332
|
+
// Download the CLI package
|
|
333
|
+
const { buffer, contentType } = await api.downloadCliFiles(item.id);
|
|
334
|
+
if (contentType.includes('zip') && buffer.length > 10) {
|
|
335
|
+
const AdmZip = require('adm-zip');
|
|
336
|
+
const zip = new AdmZip(buffer);
|
|
337
|
+
zip.extractAllTo(installDir, true);
|
|
338
|
+
console.log(chalk.gray(` 从 ZIP 解压 ${zip.getEntryCount()} 个文件`));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Write rushangle.json metadata
|
|
342
|
+
fs.writeFileSync(
|
|
343
|
+
path.join(installDir, 'rushangle.json'),
|
|
344
|
+
JSON.stringify({ type: 'cli', ...item, installedAt: new Date().toISOString() }, null, 2)
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
showLocalPath = true;
|
|
348
|
+
|
|
349
|
+
// Show install command hint
|
|
350
|
+
if (item.install) {
|
|
351
|
+
console.log(chalk.gray(` 安装命令: ${item.install}`));
|
|
352
|
+
}
|
|
353
|
+
if (item.bin && Object.keys(item.bin).length > 0) {
|
|
354
|
+
const binNames = Object.keys(item.bin);
|
|
355
|
+
console.log(chalk.gray(` 入口: ${binNames.join(', ')}`));
|
|
356
|
+
}
|
|
357
|
+
} catch (fileErr) {
|
|
358
|
+
console.log(chalk.yellow(` 本地文件写入跳过(${fileErr.code || fileErr.message})`));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
console.log(chalk.green(`✓ 安装成功: ${item.name}`));
|
|
363
|
+
if (showLocalPath) console.log(chalk.gray(` 本地目录: ${installDir}`));
|
|
364
|
+
|
|
298
365
|
} else {
|
|
299
366
|
console.log(chalk.red(`不支持的类型: ${type}`));
|
|
300
367
|
}
|
package/src/commands/list.js
CHANGED
|
@@ -2,7 +2,7 @@ const { Command } = require('commander');
|
|
|
2
2
|
const chalk = require('chalk');
|
|
3
3
|
const Table = require('cli-table3');
|
|
4
4
|
const auth = require('../auth');
|
|
5
|
-
const { listSkills, mySkills, installedSkills, listMcp, myMcp, listCode, myCode, listDocs, myDocs } = require('../api');
|
|
5
|
+
const { listSkills, mySkills, installedSkills, listMcp, myMcp, listCode, myCode, listDocs, myDocs, listCli, myCli, installedCli } = require('../api');
|
|
6
6
|
|
|
7
7
|
function table(rows) {
|
|
8
8
|
if (rows.length === 0) return '';
|
|
@@ -17,8 +17,8 @@ function table(rows) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
module.exports = new Command('list')
|
|
20
|
-
.description('列出技能/MCP
|
|
21
|
-
.option('-t, --type <type>', '类型: skills | mcp | code | docs', 'skills')
|
|
20
|
+
.description('列出技能/MCP/代码/文档/CLI工具')
|
|
21
|
+
.option('-t, --type <type>', '类型: skills | mcp | code | docs | cli', 'skills')
|
|
22
22
|
.option('-i, --installed', '已安装')
|
|
23
23
|
.option('-p, --published', '我发布的')
|
|
24
24
|
.option('-s, --search <query>', '搜索')
|
|
@@ -107,6 +107,27 @@ module.exports = new Command('list')
|
|
|
107
107
|
} else {
|
|
108
108
|
console.log(table(items));
|
|
109
109
|
}
|
|
110
|
+
|
|
111
|
+
} else if (type === 'cli') {
|
|
112
|
+
let data;
|
|
113
|
+
if (opts.published) {
|
|
114
|
+
data = await myCli();
|
|
115
|
+
console.log(chalk.bold.cyan(`\n 我发布的 CLI 工具 (${data.total || data.items?.length || 0})`));
|
|
116
|
+
} else if (opts.installed) {
|
|
117
|
+
data = await installedCli();
|
|
118
|
+
console.log(chalk.bold.cyan(`\n 已安装的 CLI 工具 (${data.total || data.items?.length || 0})`));
|
|
119
|
+
} else {
|
|
120
|
+
const params = {};
|
|
121
|
+
if (opts.search) params.search = opts.search;
|
|
122
|
+
data = await listCli(params);
|
|
123
|
+
console.log(chalk.bold.cyan(`\n SkillHub CLI 工具市场 (${data.total || data.items?.length || 0})`));
|
|
124
|
+
}
|
|
125
|
+
const items = data.items || [];
|
|
126
|
+
if (items.length === 0) {
|
|
127
|
+
console.log(' ' + chalk.gray('暂无 CLI 工具'));
|
|
128
|
+
} else {
|
|
129
|
+
console.log(table(items));
|
|
130
|
+
}
|
|
110
131
|
}
|
|
111
132
|
|
|
112
133
|
// Tips
|
package/src/commands/publish.js
CHANGED
|
@@ -6,7 +6,7 @@ const readline = require('readline');
|
|
|
6
6
|
const auth = require('../auth');
|
|
7
7
|
const api = require('../api');
|
|
8
8
|
|
|
9
|
-
const VALID_TYPES = ['skill', 'mcp', 'code', 'doc'];
|
|
9
|
+
const VALID_TYPES = ['skill', 'mcp', 'code', 'doc', 'cli'];
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Simple YAML frontmatter parser for SKILL.md.
|
|
@@ -58,7 +58,7 @@ async function askQuestion(query) {
|
|
|
58
58
|
* @returns {{ categories: string[], resolved: string }} - resolved is the final category to use
|
|
59
59
|
*/
|
|
60
60
|
async function resolveCategory(type, userCategory, apiModule) {
|
|
61
|
-
const typeMap = { skill: 'skill', mcp: 'mcp', code: 'code', doc: 'doc' };
|
|
61
|
+
const typeMap = { skill: 'skill', mcp: 'mcp', code: 'code', doc: 'doc', cli: 'cli' };
|
|
62
62
|
const t = typeMap[type] || 'skill';
|
|
63
63
|
|
|
64
64
|
let serverCategories = [];
|
|
@@ -107,9 +107,9 @@ async function resolveCategory(type, userCategory, apiModule) {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
module.exports = new Command('publish')
|
|
110
|
-
.description('发布技能/MCP
|
|
110
|
+
.description('发布技能/MCP配置/代码段/文档/CLI工具到 SkillHub 市场')
|
|
111
111
|
.argument('[dir]', '项目目录路径', '.')
|
|
112
|
-
.option('-t, --type <type>', '类型: skill | mcp | code | doc', 'skill')
|
|
112
|
+
.option('-t, --type <type>', '类型: skill | mcp | code | doc | cli', 'skill')
|
|
113
113
|
.option('-n, --name <name>', '名称')
|
|
114
114
|
.option('-d, --description <desc>', '描述')
|
|
115
115
|
.option('-v, --version <ver>', '版本号')
|
|
@@ -269,12 +269,69 @@ module.exports = new Command('publish')
|
|
|
269
269
|
format: opts.format,
|
|
270
270
|
tags,
|
|
271
271
|
});
|
|
272
|
+
} else if (type === 'cli') {
|
|
273
|
+
// Read package.json for metadata
|
|
274
|
+
let pkg = {};
|
|
275
|
+
const pkgPath = path.join(absDir, 'package.json');
|
|
276
|
+
if (fs.existsSync(pkgPath)) {
|
|
277
|
+
try { pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); }
|
|
278
|
+
catch { /* ignore */ }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const bin = pkg.bin || meta.bin || {};
|
|
282
|
+
const dependencies = { ...(pkg.dependencies || {}), ...(meta.dependencies || {}) };
|
|
283
|
+
const platforms = meta.platforms || [];
|
|
284
|
+
const installCmd = meta.install || (pkg.name ? `npm install -g ${pkg.name}` : '');
|
|
285
|
+
|
|
286
|
+
result = await api.publishCli({
|
|
287
|
+
name: name || pkg.name,
|
|
288
|
+
version: version || pkg.version,
|
|
289
|
+
description: description || pkg.description || '',
|
|
290
|
+
category, tags,
|
|
291
|
+
bin, dependencies, platforms,
|
|
292
|
+
install: installCmd,
|
|
293
|
+
readme,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Upload CLI package (zip the directory)
|
|
297
|
+
try {
|
|
298
|
+
const uploadFiles = [];
|
|
299
|
+
|
|
300
|
+
// Look for README.md
|
|
301
|
+
for (const f of ['README.md', 'readme.md']) {
|
|
302
|
+
const fp = path.join(absDir, f);
|
|
303
|
+
if (fs.existsSync(fp)) {
|
|
304
|
+
uploadFiles.push({ fieldName: 'readme_md', filePath: fp });
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Look for bin files
|
|
310
|
+
const binDir = path.join(absDir, 'bin');
|
|
311
|
+
if (fs.existsSync(binDir) && fs.statSync(binDir).isDirectory()) {
|
|
312
|
+
const entries = fs.readdirSync(binDir);
|
|
313
|
+
for (const entry of entries) {
|
|
314
|
+
const fp = path.join(binDir, entry);
|
|
315
|
+
if (fs.statSync(fp).isFile()) {
|
|
316
|
+
uploadFiles.push({ fieldName: 'bin_files', filePath: fp });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (uploadFiles.length > 0) {
|
|
322
|
+
console.log(chalk.gray(` 上传 ${uploadFiles.length} 个文件...`));
|
|
323
|
+
await api.uploadCliFiles(result.id, uploadFiles);
|
|
324
|
+
console.log(chalk.green(` ✓ 文件上传完成`));
|
|
325
|
+
}
|
|
326
|
+
} catch (uploadErr) {
|
|
327
|
+
console.log(chalk.yellow(` ⚠ 文件上传失败: ${uploadErr.message}`));
|
|
328
|
+
}
|
|
272
329
|
}
|
|
273
330
|
|
|
274
331
|
console.log(chalk.green(`✓ 发布成功: ${result.name || result.id}`));
|
|
275
332
|
console.log(chalk.gray(` ID: ${result.id}`));
|
|
276
333
|
if (result.language) console.log(chalk.gray(` 语言: ${result.language}`));
|
|
277
|
-
console.log(chalk.gray(` 链接: ${api.SITE_URL}/${type === 'doc' ? 'docs' : type === 'code' ? 'code' : type === 'mcp' ? 'mcp' : 'skills'}/${result.id}`));
|
|
334
|
+
console.log(chalk.gray(` 链接: ${api.SITE_URL}/${type === 'doc' ? 'docs' : type === 'code' ? 'code' : type === 'mcp' ? 'mcp' : type === 'cli' ? 'cli' : 'skills'}/${result.id}`));
|
|
278
335
|
} catch (e) {
|
|
279
336
|
console.log(chalk.red(`发布失败: ${e.message}`));
|
|
280
337
|
if (e.data) console.log(chalk.gray(JSON.stringify(e.data)));
|
|
@@ -4,7 +4,7 @@ const readline = require('readline');
|
|
|
4
4
|
const auth = require('../auth');
|
|
5
5
|
const api = require('../api');
|
|
6
6
|
|
|
7
|
-
const VALID_TYPES = ['skill', 'mcp', 'code', 'doc'];
|
|
7
|
+
const VALID_TYPES = ['skill', 'mcp', 'code', 'doc', 'cli'];
|
|
8
8
|
|
|
9
9
|
async function askConfirm(query) {
|
|
10
10
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -16,13 +16,14 @@ const TYPE_MAP = {
|
|
|
16
16
|
mcp: { label: 'MCP代码段', my: 'myMcp', get: 'getMcp', del: 'deleteMcp', search: 'listMcp', field: 'items' },
|
|
17
17
|
code: { label: '代码段', my: 'myCode', get: 'getCode', del: 'deleteCode', search: 'listCode', field: 'items' },
|
|
18
18
|
doc: { label: '文档', my: 'myDocs', get: 'getDoc', del: 'deleteDoc', search: 'listDocs', field: 'items' },
|
|
19
|
+
cli: { label: 'CLI工具', my: 'myCli', get: 'getCli', del: 'deleteCli', search: 'listCli', field: 'items' },
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
module.exports = new Command('unpublish')
|
|
22
23
|
.alias('rm')
|
|
23
24
|
.description('从 SkillHub 下架已发布的内容(仅限自己发布的)')
|
|
24
25
|
.argument('<name-or-id>', '名称或 ID')
|
|
25
|
-
.option('-t, --type <type>', '类型: skill | mcp | code | doc', 'skill')
|
|
26
|
+
.option('-t, --type <type>', '类型: skill | mcp | code | doc | cli', 'skill')
|
|
26
27
|
.option('-y, --yes', '跳过确认,直接删除')
|
|
27
28
|
.action(async (nameOrId, opts) => {
|
|
28
29
|
const type = opts.type.toLowerCase();
|