rushangle-cli 0.1.0
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/README.md +35 -0
- package/bin/rushangle.js +2 -0
- package/package.json +26 -0
- package/src/api.js +90 -0
- package/src/auth.js +61 -0
- package/src/commands/code.js +146 -0
- package/src/commands/doc.js +122 -0
- package/src/commands/install.js +166 -0
- package/src/commands/list.js +125 -0
- package/src/commands/login.js +79 -0
- package/src/commands/logout.js +18 -0
- package/src/commands/mcp.js +134 -0
- package/src/commands/publish.js +139 -0
- package/src/commands/search.js +64 -0
- package/src/commands/whoami.js +40 -0
- package/src/index.js +21 -0
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# rushangle-cli
|
|
2
|
+
|
|
3
|
+
SkillHub 数智凯航技能市场命令行工具。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g rushangle-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 使用
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
rushangle login # 钉钉扫码登录
|
|
15
|
+
rushangle whoami # 查看当前用户
|
|
16
|
+
rushangle publish # 发布技能到市场
|
|
17
|
+
rushangle list --type skills # 浏览技能列表
|
|
18
|
+
rushangle search <关键词> # 全局搜索
|
|
19
|
+
rushangle install <技能名> # 安装技能
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 命令
|
|
23
|
+
|
|
24
|
+
| 命令 | 说明 |
|
|
25
|
+
|------|------|
|
|
26
|
+
| `login` | 登录 SkillHub(钉钉 OAuth) |
|
|
27
|
+
| `whoami` | 查看当前登录用户 |
|
|
28
|
+
| `logout` | 退出登录 |
|
|
29
|
+
| `publish` | 发布技能/MCP/代码段/文档 |
|
|
30
|
+
| `install` | 安装技能/MCP/代码段 |
|
|
31
|
+
| `list` | 浏览市场内容 |
|
|
32
|
+
| `search` | 全局搜索 |
|
|
33
|
+
| `mcp` | MCP 代码段管理 |
|
|
34
|
+
| `code` | 代码段管理 |
|
|
35
|
+
| `doc` | 文档管理 |
|
package/bin/rushangle.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rushangle-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SkillHub CLI - 数智凯航技能市场命令行工具",
|
|
5
|
+
"bin": {
|
|
6
|
+
"rushangle": "./bin/rushangle.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"src/",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"start": "node bin/rushangle.js"
|
|
15
|
+
},
|
|
16
|
+
"keywords": ["rushangle", "skillhub", "cli", "skills"],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"commander": "^12.0.0",
|
|
20
|
+
"chalk": "^4.1.2",
|
|
21
|
+
"ora": "^8.0.0",
|
|
22
|
+
"open": "^10.0.0",
|
|
23
|
+
"conf": "^12.0.0",
|
|
24
|
+
"cli-table3": "^0.6.5"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const auth = require('./auth');
|
|
2
|
+
|
|
3
|
+
const SITE_URL = process.env.RUSHANGLE_API || 'https://rushangle.cn';
|
|
4
|
+
|
|
5
|
+
async function apiCall(method, path, body) {
|
|
6
|
+
const token = auth.getToken();
|
|
7
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
8
|
+
if (token) {
|
|
9
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const res = await fetch(`${SITE_URL}${path}`, {
|
|
13
|
+
method,
|
|
14
|
+
headers,
|
|
15
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
let data;
|
|
19
|
+
try {
|
|
20
|
+
data = await res.json();
|
|
21
|
+
} catch {
|
|
22
|
+
data = { error: await res.text().catch(() => `HTTP ${res.status}`) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
const err = new Error(data.error || `API ${method} ${path} failed (${res.status})`);
|
|
27
|
+
err.status = res.status;
|
|
28
|
+
err.data = data;
|
|
29
|
+
throw err;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return data;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
SITE_URL,
|
|
37
|
+
apiCall,
|
|
38
|
+
|
|
39
|
+
// User
|
|
40
|
+
me() { return apiCall('GET', '/api/auth/me'); },
|
|
41
|
+
|
|
42
|
+
// Skills
|
|
43
|
+
listSkills(params = {}) {
|
|
44
|
+
const qs = new URLSearchParams(params).toString();
|
|
45
|
+
return apiCall('GET', `/api/skills${qs ? '?' + qs : ''}`);
|
|
46
|
+
},
|
|
47
|
+
getSkill(id) { return apiCall('GET', `/api/skills/${encodeURIComponent(id)}`); },
|
|
48
|
+
publishSkill(data) { return apiCall('POST', '/api/skills', data); },
|
|
49
|
+
updateSkill(id, data) { return apiCall('PUT', `/api/skills/${encodeURIComponent(id)}`, data); },
|
|
50
|
+
deleteSkill(id) { return apiCall('DELETE', `/api/skills/${encodeURIComponent(id)}`); },
|
|
51
|
+
installSkill(id) { return apiCall('POST', `/api/skills/${encodeURIComponent(id)}/install`); },
|
|
52
|
+
installedSkills() { return apiCall('GET', '/api/skills/installed'); },
|
|
53
|
+
mySkills() { return apiCall('GET', '/api/skills/my'); },
|
|
54
|
+
|
|
55
|
+
// MCP
|
|
56
|
+
listMcp(params = {}) {
|
|
57
|
+
const qs = new URLSearchParams(params).toString();
|
|
58
|
+
return apiCall('GET', `/api/mcp${qs ? '?' + qs : ''}`);
|
|
59
|
+
},
|
|
60
|
+
getMcp(id) { return apiCall('GET', `/api/mcp/${encodeURIComponent(id)}`); },
|
|
61
|
+
publishMcp(data) { return apiCall('POST', '/api/mcp', data); },
|
|
62
|
+
updateMcp(id, data) { return apiCall('PUT', `/api/mcp/${encodeURIComponent(id)}`, data); },
|
|
63
|
+
deleteMcp(id) { return apiCall('DELETE', `/api/mcp/${encodeURIComponent(id)}`); },
|
|
64
|
+
myMcp() { return apiCall('GET', '/api/mcp/my'); },
|
|
65
|
+
|
|
66
|
+
// Code
|
|
67
|
+
listCode(params = {}) {
|
|
68
|
+
const qs = new URLSearchParams(params).toString();
|
|
69
|
+
return apiCall('GET', `/api/code${qs ? '?' + qs : ''}`);
|
|
70
|
+
},
|
|
71
|
+
getCode(id) { return apiCall('GET', `/api/code/${encodeURIComponent(id)}`); },
|
|
72
|
+
publishCode(data) { return apiCall('POST', '/api/code', data); },
|
|
73
|
+
updateCode(id, data) { return apiCall('PUT', `/api/code/${encodeURIComponent(id)}`, data); },
|
|
74
|
+
deleteCode(id) { return apiCall('DELETE', `/api/code/${encodeURIComponent(id)}`); },
|
|
75
|
+
myCode() { return apiCall('GET', '/api/code/my'); },
|
|
76
|
+
|
|
77
|
+
// Docs
|
|
78
|
+
listDocs(params = {}) {
|
|
79
|
+
const qs = new URLSearchParams(params).toString();
|
|
80
|
+
return apiCall('GET', `/api/docs${qs ? '?' + qs : ''}`);
|
|
81
|
+
},
|
|
82
|
+
getDoc(id) { return apiCall('GET', `/api/docs/${encodeURIComponent(id)}`); },
|
|
83
|
+
publishDoc(data) { return apiCall('POST', '/api/docs', data); },
|
|
84
|
+
updateDoc(id, data) { return apiCall('PUT', `/api/docs/${encodeURIComponent(id)}`, data); },
|
|
85
|
+
deleteDoc(id) { return apiCall('DELETE', `/api/docs/${encodeURIComponent(id)}`); },
|
|
86
|
+
myDocs() { return apiCall('GET', '/api/docs/my'); },
|
|
87
|
+
|
|
88
|
+
// Search
|
|
89
|
+
search(query) { return apiCall('GET', `/api/search?q=${encodeURIComponent(query)}`); },
|
|
90
|
+
};
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = path.join(os.homedir(), '.rushangle');
|
|
6
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
7
|
+
|
|
8
|
+
function ensureDir() {
|
|
9
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
10
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readConfig() {
|
|
15
|
+
try {
|
|
16
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
17
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
18
|
+
}
|
|
19
|
+
} catch (_) {}
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function writeConfig(data) {
|
|
24
|
+
ensureDir();
|
|
25
|
+
const config = { ...readConfig(), ...data };
|
|
26
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
/** Save JWT token after login */
|
|
31
|
+
saveToken(token) {
|
|
32
|
+
writeConfig({ token, loginAt: new Date().toISOString() });
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
/** Get stored JWT token */
|
|
36
|
+
getToken() {
|
|
37
|
+
return readConfig().token || null;
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/** Clear stored token */
|
|
41
|
+
clearToken() {
|
|
42
|
+
const config = readConfig();
|
|
43
|
+
delete config.token;
|
|
44
|
+
delete config.loginAt;
|
|
45
|
+
writeConfig(config);
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
/** Decode JWT payload without verifying */
|
|
49
|
+
decodeToken(token) {
|
|
50
|
+
try {
|
|
51
|
+
const base64 = token.split('.')[1];
|
|
52
|
+
const decoded = JSON.parse(Buffer.from(base64, 'base64url').toString('utf-8'));
|
|
53
|
+
return decoded;
|
|
54
|
+
} catch (_) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
CONFIG_DIR,
|
|
60
|
+
CONFIG_FILE,
|
|
61
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const api = require('../api');
|
|
4
|
+
const auth = require('../auth');
|
|
5
|
+
|
|
6
|
+
const program = new Command('code');
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.description('代码段管理(发布、浏览、安装)')
|
|
10
|
+
.addCommand(
|
|
11
|
+
new Command('list')
|
|
12
|
+
.description('浏览代码段市场')
|
|
13
|
+
.option('-l, --language <lang>', '按语言过滤(python / javascript / go / rust 等)')
|
|
14
|
+
.action(async (opts) => {
|
|
15
|
+
try {
|
|
16
|
+
const params = {};
|
|
17
|
+
if (opts.language) params.language = opts.language;
|
|
18
|
+
const data = await api.list('code', params);
|
|
19
|
+
if (!data.items || data.items.length === 0) {
|
|
20
|
+
console.log(chalk.gray('\n 暂无代码段,快来发布第一个吧!\n'));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
console.log(chalk.bold(`\n📦 代码段市场 (${data.total} 个)\n`));
|
|
24
|
+
for (const item of data.items) {
|
|
25
|
+
const lang = chalk.cyan(`[${item.language}]`);
|
|
26
|
+
console.log(` ${chalk.bold(item.name)} ${lang}`);
|
|
27
|
+
if (item.description) console.log(` ${chalk.gray(item.description)}`);
|
|
28
|
+
console.log(` ${chalk.dim(`作者: ${item.authorName} | 下载: ${item.downloads || 0} | ID: ${item.id}`)}`);
|
|
29
|
+
console.log();
|
|
30
|
+
}
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.error(chalk.red(`获取代码段列表失败: ${e.message}`));
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
)
|
|
36
|
+
.addCommand(
|
|
37
|
+
new Command('get')
|
|
38
|
+
.description('查看代码段详情')
|
|
39
|
+
.argument('<id>', '代码段 ID')
|
|
40
|
+
.action(async (id) => {
|
|
41
|
+
try {
|
|
42
|
+
const item = await api.get('code', id);
|
|
43
|
+
if (!item) {
|
|
44
|
+
console.error(chalk.red('代码段不存在'));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
console.log(chalk.bold(`\n📄 ${item.name}`));
|
|
48
|
+
console.log(chalk.cyan(` 语言: ${item.language}`));
|
|
49
|
+
if (item.description) console.log(chalk.gray(` 描述: ${item.description}`));
|
|
50
|
+
if (item.tags && item.tags.length) console.log(chalk.yellow(` 标签: ${item.tags.join(', ')}`));
|
|
51
|
+
console.log(chalk.dim(` 作者: ${item.authorName} | 版本: ${item.version || '1.0.0'}`));
|
|
52
|
+
console.log(chalk.dim(` 下载: ${item.downloads || 0} | 安装: ${item.installs || 0}`));
|
|
53
|
+
console.log();
|
|
54
|
+
console.log(chalk.bold('─── 代码 ───'));
|
|
55
|
+
console.log(item.code);
|
|
56
|
+
console.log();
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.error(chalk.red(`获取代码段失败: ${e.message}`));
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
)
|
|
62
|
+
.addCommand(
|
|
63
|
+
new Command('publish')
|
|
64
|
+
.description('发布代码段')
|
|
65
|
+
.requiredOption('-n, --name <name>', '代码段名称')
|
|
66
|
+
.requiredOption('-l, --language <lang>', '语言(python / javascript / go / rust / java / shell 等)')
|
|
67
|
+
.option('-d, --description <desc>', '描述')
|
|
68
|
+
.option('-f, --file <path>', '从文件读取代码(不指定则从 stdin)')
|
|
69
|
+
.option('-t, --tags <tags>', '标签,逗号分隔')
|
|
70
|
+
.option('-c, --category <cat>', '分类')
|
|
71
|
+
.action(async (opts) => {
|
|
72
|
+
try {
|
|
73
|
+
auth.require();
|
|
74
|
+
let code = '';
|
|
75
|
+
if (opts.file) {
|
|
76
|
+
const fs = require('fs');
|
|
77
|
+
code = fs.readFileSync(opts.file, 'utf-8');
|
|
78
|
+
} else {
|
|
79
|
+
// Read from stdin
|
|
80
|
+
const chunks = [];
|
|
81
|
+
process.stdin.setEncoding('utf-8');
|
|
82
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
83
|
+
code = chunks.join('');
|
|
84
|
+
}
|
|
85
|
+
if (!code.trim()) {
|
|
86
|
+
console.error(chalk.red('代码内容不能为空'));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const payload = {
|
|
90
|
+
name: opts.name,
|
|
91
|
+
language: opts.language,
|
|
92
|
+
description: opts.description || '',
|
|
93
|
+
code,
|
|
94
|
+
tags: opts.tags ? opts.tags.split(',').map(t => t.trim()) : [],
|
|
95
|
+
category: opts.category || '',
|
|
96
|
+
};
|
|
97
|
+
const item = await api.create('code', payload);
|
|
98
|
+
console.log(chalk.green(`✅ 代码段已发布: ${item.name} (${item.id})`));
|
|
99
|
+
console.log(chalk.gray(` 语言: ${item.language}`));
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.error(chalk.red(`发布失败: ${e.message}`));
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
)
|
|
105
|
+
.addCommand(
|
|
106
|
+
new Command('update')
|
|
107
|
+
.description('更新代码段')
|
|
108
|
+
.argument('<id>', '代码段 ID')
|
|
109
|
+
.option('-n, --name <name>', '新名称')
|
|
110
|
+
.option('-d, --description <desc>', '新描述')
|
|
111
|
+
.option('-f, --file <path>', '用文件内容更新代码')
|
|
112
|
+
.option('-t, --tags <tags>', '新标签,逗号分隔')
|
|
113
|
+
.action(async (id, opts) => {
|
|
114
|
+
try {
|
|
115
|
+
auth.require();
|
|
116
|
+
const payload = {};
|
|
117
|
+
if (opts.name) payload.name = opts.name;
|
|
118
|
+
if (opts.description) payload.description = opts.description;
|
|
119
|
+
if (opts.file) {
|
|
120
|
+
const fs = require('fs');
|
|
121
|
+
payload.code = fs.readFileSync(opts.file, 'utf-8');
|
|
122
|
+
}
|
|
123
|
+
if (opts.tags) payload.tags = opts.tags.split(',').map(t => t.trim());
|
|
124
|
+
const item = await api.update('code', id, payload);
|
|
125
|
+
console.log(chalk.green(`✅ 代码段已更新: ${item.name}`));
|
|
126
|
+
} catch (e) {
|
|
127
|
+
console.error(chalk.red(`更新失败: ${e.message}`));
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
)
|
|
131
|
+
.addCommand(
|
|
132
|
+
new Command('delete')
|
|
133
|
+
.description('删除代码段')
|
|
134
|
+
.argument('<id>', '代码段 ID')
|
|
135
|
+
.action(async (id) => {
|
|
136
|
+
try {
|
|
137
|
+
auth.require();
|
|
138
|
+
await api.remove('code', id);
|
|
139
|
+
console.log(chalk.green('✅ 代码段已删除'));
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.error(chalk.red(`删除失败: ${e.message}`));
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
module.exports = program;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const auth = require('../auth');
|
|
6
|
+
const api = require('../api');
|
|
7
|
+
|
|
8
|
+
module.exports = new Command('doc')
|
|
9
|
+
.description('管理文档')
|
|
10
|
+
.argument('[action]', '操作: list | get | publish | update | delete', 'list')
|
|
11
|
+
.argument('[id]', '文档 ID 或名称')
|
|
12
|
+
.option('-n, --name <name>', '文档名称')
|
|
13
|
+
.option('-d, --description <desc>', '描述')
|
|
14
|
+
.option('-c, --category <cat>', '分类')
|
|
15
|
+
.option('-f, --format <fmt>', '格式: markdown | html | text', 'markdown')
|
|
16
|
+
.option('--file <file>', '文档文件路径')
|
|
17
|
+
.option('--tags <tags>', '标签(逗号分隔)')
|
|
18
|
+
.option('-s, --search <query>', '搜索')
|
|
19
|
+
.action(async (action, id, opts) => {
|
|
20
|
+
const token = auth.getToken();
|
|
21
|
+
|
|
22
|
+
if (action === 'list') {
|
|
23
|
+
try {
|
|
24
|
+
const params = {};
|
|
25
|
+
if (opts.search) params.search = opts.search;
|
|
26
|
+
if (opts.category) params.category = opts.category;
|
|
27
|
+
const data = await api.listDocs(params);
|
|
28
|
+
const items = data.items || [];
|
|
29
|
+
console.log(chalk.bold.cyan(`\n 文档 (${items.length})`));
|
|
30
|
+
if (items.length === 0) {
|
|
31
|
+
console.log(' ' + chalk.gray('暂无文档'));
|
|
32
|
+
console.log(chalk.gray(' 发布: rushangle doc publish'));
|
|
33
|
+
} else {
|
|
34
|
+
items.forEach(d => {
|
|
35
|
+
console.log(` ${chalk.white(d.name)} ${chalk.gray('v' + d.version)} ${chalk.gray(d.format)} ${chalk.gray(d.category || '')}`);
|
|
36
|
+
console.log(` ${chalk.gray((d.description || '').slice(0, 80))}`);
|
|
37
|
+
});
|
|
38
|
+
console.log('');
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.log(chalk.red(`获取失败: ${e.message}`));
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (action === 'get') {
|
|
47
|
+
if (!id) { console.log(chalk.red('请指定 ID: rushangle doc get <id>')); return; }
|
|
48
|
+
try {
|
|
49
|
+
const item = await api.getDoc(id);
|
|
50
|
+
console.log(chalk.bold.cyan(`\n${item.name}`));
|
|
51
|
+
console.log(chalk.gray(`版本: ${item.version} 格式: ${item.format} 分类: ${item.category}`));
|
|
52
|
+
console.log(chalk.gray(`作者: ${item.authorName} 更新: ${item.updated?.slice(0, 10)}`));
|
|
53
|
+
if (item.description) console.log(`\n${item.description}`);
|
|
54
|
+
if (item.content) {
|
|
55
|
+
console.log(chalk.bold.yellow('\n--- 内容 ---'));
|
|
56
|
+
console.log(item.content.slice(0, 2000));
|
|
57
|
+
if (item.content.length > 2000) console.log(chalk.gray(`\n... (共 ${item.content.length} 字符)`));
|
|
58
|
+
}
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.log(chalk.red(`获取失败: ${e.message}`));
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!token) { console.log(chalk.red('请先登录: rushangle login')); return; }
|
|
66
|
+
|
|
67
|
+
if (action === 'publish') {
|
|
68
|
+
const name = opts.name;
|
|
69
|
+
if (!name) { console.log(chalk.red('请指定名称: rushangle doc publish -n <name>')); return; }
|
|
70
|
+
|
|
71
|
+
let content = '';
|
|
72
|
+
if (opts.file) {
|
|
73
|
+
const fp = path.resolve(opts.file);
|
|
74
|
+
if (!fs.existsSync(fp)) { console.log(chalk.red(`文件不存在: ${fp}`)); return; }
|
|
75
|
+
content = fs.readFileSync(fp, 'utf-8');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const result = await api.publishDoc({
|
|
80
|
+
name,
|
|
81
|
+
description: opts.description || '',
|
|
82
|
+
category: opts.category || '文档',
|
|
83
|
+
format: opts.format,
|
|
84
|
+
content,
|
|
85
|
+
tags: opts.tags ? opts.tags.split(',').map(t => t.trim()).filter(Boolean) : [],
|
|
86
|
+
});
|
|
87
|
+
console.log(chalk.green(`✓ 发布成功: ${result.name} (${result.id})`));
|
|
88
|
+
} catch (e) {
|
|
89
|
+
console.log(chalk.red(`发布失败: ${e.message}`));
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (action === 'update') {
|
|
95
|
+
if (!id) { console.log(chalk.red('请指定 ID: rushangle doc update <id>')); return; }
|
|
96
|
+
const data = {};
|
|
97
|
+
if (opts.name) data.name = opts.name;
|
|
98
|
+
if (opts.description) data.description = opts.description;
|
|
99
|
+
if (opts.category) data.category = opts.category;
|
|
100
|
+
if (opts.tags) data.tags = opts.tags.split(',').map(t => t.trim()).filter(Boolean);
|
|
101
|
+
try {
|
|
102
|
+
const result = await api.updateDoc(id, data);
|
|
103
|
+
console.log(chalk.green(`✓ 更新成功: ${result.name}`));
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.log(chalk.red(`更新失败: ${e.message}`));
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (action === 'delete') {
|
|
111
|
+
if (!id) { console.log(chalk.red('请指定 ID: rushangle doc delete <id>')); return; }
|
|
112
|
+
try {
|
|
113
|
+
await api.deleteDoc(id);
|
|
114
|
+
console.log(chalk.green(`✓ 已删除: ${id}`));
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.log(chalk.red(`删除失败: ${e.message}`));
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log(chalk.yellow(`未知操作: ${action},可用: list | get | publish | update | delete`));
|
|
122
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const auth = require('../auth');
|
|
7
|
+
const api = require('../api');
|
|
8
|
+
|
|
9
|
+
const INSTALL_DIR = path.join(os.homedir(), '.rushangle', 'installed');
|
|
10
|
+
|
|
11
|
+
module.exports = new Command('install')
|
|
12
|
+
.description('安装技能/MCP/代码段')
|
|
13
|
+
.argument('[skill]', '技能名称或 ID')
|
|
14
|
+
.option('-t, --type <type>', '类型: skill | mcp | code', 'skill')
|
|
15
|
+
.option('-g, --global', '全局安装(默认)', true)
|
|
16
|
+
.action(async (nameOrId, opts) => {
|
|
17
|
+
const token = auth.getToken();
|
|
18
|
+
if (!token) {
|
|
19
|
+
console.log(chalk.red('请先登录: rushangle login'));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!nameOrId) {
|
|
24
|
+
console.log(chalk.yellow('请指定要安装的项: rushangle install <name-or-id>'));
|
|
25
|
+
console.log(chalk.gray('查看可用: rushangle list'));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const type = opts.type;
|
|
30
|
+
console.log(chalk.cyan(`正在安装 ${type}: ${nameOrId} ...`));
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
let item;
|
|
34
|
+
|
|
35
|
+
if (type === 'skill') {
|
|
36
|
+
// Try by ID first, then search by name
|
|
37
|
+
try {
|
|
38
|
+
item = await api.getSkill(nameOrId);
|
|
39
|
+
} catch {
|
|
40
|
+
const listData = await api.listSkills({ search: nameOrId });
|
|
41
|
+
if (listData.skills && listData.skills.length > 0) {
|
|
42
|
+
const match = listData.skills.find(
|
|
43
|
+
s => s.name === nameOrId || s.id === nameOrId
|
|
44
|
+
);
|
|
45
|
+
if (match) item = match;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!item) {
|
|
50
|
+
console.log(chalk.red(`未找到技能: ${nameOrId}`));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Download and install
|
|
55
|
+
const installDir = path.join(INSTALL_DIR, 'skills', item.id);
|
|
56
|
+
fs.mkdirSync(installDir, { recursive: true });
|
|
57
|
+
|
|
58
|
+
// Write metadata
|
|
59
|
+
fs.writeFileSync(
|
|
60
|
+
path.join(installDir, 'rushangle.json'),
|
|
61
|
+
JSON.stringify({ type: 'skill', ...item, installedAt: new Date().toISOString() }, null, 2)
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Write readme if exists
|
|
65
|
+
if (item.readme) {
|
|
66
|
+
fs.writeFileSync(path.join(installDir, 'README.md'), item.readme);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Register install on server
|
|
70
|
+
await api.installSkill(item.id);
|
|
71
|
+
|
|
72
|
+
console.log(chalk.green(`✓ 安装成功: ${item.name}@${item.version}`));
|
|
73
|
+
console.log(chalk.gray(` 目录: ${installDir}`));
|
|
74
|
+
|
|
75
|
+
} else if (type === 'mcp') {
|
|
76
|
+
try {
|
|
77
|
+
item = await api.getMcp(nameOrId);
|
|
78
|
+
} catch {
|
|
79
|
+
const listData = await api.listMcp({ search: nameOrId });
|
|
80
|
+
if (listData.items && listData.items.length > 0) {
|
|
81
|
+
const match = listData.items.find(
|
|
82
|
+
s => s.name === nameOrId || s.id === nameOrId
|
|
83
|
+
);
|
|
84
|
+
if (match) item = match;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!item) {
|
|
89
|
+
console.log(chalk.red(`未找到 MCP 代码段: ${nameOrId}`));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const installDir = path.join(INSTALL_DIR, 'mcp', item.id);
|
|
94
|
+
fs.mkdirSync(installDir, { recursive: true });
|
|
95
|
+
|
|
96
|
+
// Write code file
|
|
97
|
+
if (item.code) {
|
|
98
|
+
const ext = item.language === 'python' ? '.py'
|
|
99
|
+
: item.language === 'javascript' ? '.js'
|
|
100
|
+
: item.language === 'typescript' ? '.ts'
|
|
101
|
+
: item.language === 'json' ? '.json'
|
|
102
|
+
: '.txt';
|
|
103
|
+
fs.writeFileSync(path.join(installDir, `server${ext}`), item.code);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Write config if exists
|
|
107
|
+
if (item.config) {
|
|
108
|
+
const configStr = typeof item.config === 'string'
|
|
109
|
+
? item.config : JSON.stringify(item.config, null, 2);
|
|
110
|
+
fs.writeFileSync(path.join(installDir, 'mcp.json'), configStr);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Write metadata
|
|
114
|
+
fs.writeFileSync(
|
|
115
|
+
path.join(installDir, 'rushangle.json'),
|
|
116
|
+
JSON.stringify({ type: 'mcp', ...item, installedAt: new Date().toISOString() }, null, 2)
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
console.log(chalk.green(`✓ 安装成功: ${item.name}@${item.version}`));
|
|
120
|
+
console.log(chalk.gray(` 目录: ${installDir}`));
|
|
121
|
+
|
|
122
|
+
} else if (type === 'code') {
|
|
123
|
+
try {
|
|
124
|
+
item = await api.getCode(nameOrId);
|
|
125
|
+
} catch {
|
|
126
|
+
const listData = await api.listCode({ search: nameOrId });
|
|
127
|
+
if (listData.items && listData.items.length > 0) {
|
|
128
|
+
const match = listData.items.find(
|
|
129
|
+
s => s.name === nameOrId || s.id === nameOrId
|
|
130
|
+
);
|
|
131
|
+
if (match) item = match;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!item) {
|
|
136
|
+
console.log(chalk.red(`未找到代码段: ${nameOrId}`));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const installDir = path.join(INSTALL_DIR, 'code', item.id);
|
|
141
|
+
fs.mkdirSync(installDir, { recursive: true });
|
|
142
|
+
|
|
143
|
+
// Write code file
|
|
144
|
+
if (item.code) {
|
|
145
|
+
const extMap = { python: '.py', javascript: '.js', typescript: '.ts', go: '.go', rust: '.rs', java: '.java', c: '.c', cpp: '.cpp', shell: '.sh', bash: '.sh', json: '.json', yaml: '.yml', toml: '.toml', sql: '.sql', ruby: '.rb', php: '.php', swift: '.swift', kotlin: '.kt', scala: '.scala', r: '.r' };
|
|
146
|
+
const ext = extMap[item.language] || `.${item.language}` || '.txt';
|
|
147
|
+
fs.writeFileSync(path.join(installDir, `snippet${ext}`), item.code);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Write metadata
|
|
151
|
+
fs.writeFileSync(
|
|
152
|
+
path.join(installDir, 'rushangle.json'),
|
|
153
|
+
JSON.stringify({ type: 'code', ...item, installedAt: new Date().toISOString() }, null, 2)
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
console.log(chalk.green(`✓ 安装成功: ${item.name}`));
|
|
157
|
+
console.log(chalk.gray(` 语言: ${item.language}`));
|
|
158
|
+
console.log(chalk.gray(` 目录: ${installDir}`));
|
|
159
|
+
|
|
160
|
+
} else {
|
|
161
|
+
console.log(chalk.red(`不支持的类型: ${type}`));
|
|
162
|
+
}
|
|
163
|
+
} catch (e) {
|
|
164
|
+
console.log(chalk.red(`安装失败: ${e.message}`));
|
|
165
|
+
}
|
|
166
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const Table = require('cli-table3');
|
|
4
|
+
const auth = require('../auth');
|
|
5
|
+
const { listSkills, mySkills, installedSkills, listMcp, myMcp, listCode, myCode, listDocs, myDocs } = require('../api');
|
|
6
|
+
|
|
7
|
+
function table(rows) {
|
|
8
|
+
if (rows.length === 0) return '';
|
|
9
|
+
const t = new Table({
|
|
10
|
+
head: ['名称', '版本', '分类', '作者', '更新'],
|
|
11
|
+
style: { head: ['cyan'] },
|
|
12
|
+
colWidths: [24, 10, 12, 16, 12],
|
|
13
|
+
wordWrap: true,
|
|
14
|
+
});
|
|
15
|
+
rows.forEach(r => t.push([r.name, r.version, r.category || '-', r.authorName || r.author || '-', (r.updated || '').slice(0, 10)]));
|
|
16
|
+
return '\n' + t.toString() + '\n';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = new Command('list')
|
|
20
|
+
.description('列出技能/MCP/代码/文档')
|
|
21
|
+
.option('-t, --type <type>', '类型: skills | mcp | code | docs', 'skills')
|
|
22
|
+
.option('-i, --installed', '已安装')
|
|
23
|
+
.option('-p, --published', '我发布的')
|
|
24
|
+
.option('-s, --search <query>', '搜索')
|
|
25
|
+
.action(async (opts) => {
|
|
26
|
+
const token = auth.getToken();
|
|
27
|
+
|
|
28
|
+
if ((opts.installed || opts.published) && !token) {
|
|
29
|
+
console.log(chalk.red('请先登录: rushangle login'));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const type = opts.type;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
if (type === 'skills') {
|
|
37
|
+
let data;
|
|
38
|
+
if (opts.published) {
|
|
39
|
+
data = await mySkills();
|
|
40
|
+
console.log(chalk.bold.cyan(`\n 我发布的技能 (${data.total || data.skills?.length || 0})`));
|
|
41
|
+
} else if (opts.installed) {
|
|
42
|
+
data = await installedSkills();
|
|
43
|
+
console.log(chalk.bold.cyan(`\n 已安装的技能 (${data.total || data.skills?.length || 0})`));
|
|
44
|
+
} else {
|
|
45
|
+
const params = {};
|
|
46
|
+
if (opts.search) params.search = opts.search;
|
|
47
|
+
data = await listSkills(params);
|
|
48
|
+
console.log(chalk.bold.cyan(`\n SkillHub 技能市场 (${data.total || data.skills?.length || 0})`));
|
|
49
|
+
}
|
|
50
|
+
const skills = data.skills || [];
|
|
51
|
+
if (skills.length === 0) {
|
|
52
|
+
console.log(' ' + chalk.gray('暂无技能'));
|
|
53
|
+
} else {
|
|
54
|
+
console.log(table(skills));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
} else if (type === 'mcp') {
|
|
58
|
+
let data;
|
|
59
|
+
if (opts.published) {
|
|
60
|
+
data = await myMcp();
|
|
61
|
+
console.log(chalk.bold.cyan(`\n 我发布的 MCP 代码段 (${data.total || data.items?.length || 0})`));
|
|
62
|
+
} else {
|
|
63
|
+
const params = {};
|
|
64
|
+
if (opts.search) params.search = opts.search;
|
|
65
|
+
data = await listMcp(params);
|
|
66
|
+
console.log(chalk.bold.cyan(`\n SkillHub MCP 市场 (${data.total || data.items?.length || 0})`));
|
|
67
|
+
}
|
|
68
|
+
const items = data.items || [];
|
|
69
|
+
if (items.length === 0) {
|
|
70
|
+
console.log(' ' + chalk.gray('暂无 MCP 代码段'));
|
|
71
|
+
} else {
|
|
72
|
+
console.log(table(items));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
} else if (type === 'code') {
|
|
76
|
+
let data;
|
|
77
|
+
if (opts.published) {
|
|
78
|
+
data = await myCode();
|
|
79
|
+
console.log(chalk.bold.cyan(`\n 我发布的代码段 (${data.total || data.items?.length || 0})`));
|
|
80
|
+
} else {
|
|
81
|
+
const params = {};
|
|
82
|
+
if (opts.search) params.search = opts.search;
|
|
83
|
+
data = await listCode(params);
|
|
84
|
+
console.log(chalk.bold.cyan(`\n SkillHub 代码段市场 (${data.total || data.items?.length || 0})`));
|
|
85
|
+
}
|
|
86
|
+
const items = data.items || [];
|
|
87
|
+
if (items.length === 0) {
|
|
88
|
+
console.log(' ' + chalk.gray('暂无代码段'));
|
|
89
|
+
} else {
|
|
90
|
+
console.log(table(items));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
} else if (type === 'docs') {
|
|
94
|
+
let data;
|
|
95
|
+
if (opts.published) {
|
|
96
|
+
data = await myDocs();
|
|
97
|
+
console.log(chalk.bold.cyan(`\n 我发布的文档 (${data.total || data.items?.length || 0})`));
|
|
98
|
+
} else {
|
|
99
|
+
const params = {};
|
|
100
|
+
if (opts.search) params.search = opts.search;
|
|
101
|
+
data = await listDocs(params);
|
|
102
|
+
console.log(chalk.bold.cyan(`\n SkillHub 文档市场 (${data.total || data.items?.length || 0})`));
|
|
103
|
+
}
|
|
104
|
+
const items = data.items || [];
|
|
105
|
+
if (items.length === 0) {
|
|
106
|
+
console.log(' ' + chalk.gray('暂无文档'));
|
|
107
|
+
} else {
|
|
108
|
+
console.log(table(items));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Tips
|
|
113
|
+
if (token) {
|
|
114
|
+
console.log(chalk.gray(' 已登录'));
|
|
115
|
+
} else {
|
|
116
|
+
console.log(chalk.gray(' 未登录 | ') + chalk.cyan('rushangle login'));
|
|
117
|
+
}
|
|
118
|
+
console.log('');
|
|
119
|
+
} catch (e) {
|
|
120
|
+
console.log(chalk.yellow(`无法连接到服务器: ${e.message}`));
|
|
121
|
+
if (!token) {
|
|
122
|
+
console.log(chalk.gray(' 请先登录: rushangle login'));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const open = require('open');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const auth = require('../auth');
|
|
6
|
+
const { SITE_URL } = require('../api');
|
|
7
|
+
|
|
8
|
+
const CLI_PORT = 5199;
|
|
9
|
+
|
|
10
|
+
module.exports = new Command('login')
|
|
11
|
+
.description('登录 SkillHub 平台(钉钉 OAuth)')
|
|
12
|
+
.action(async () => {
|
|
13
|
+
// Already logged in?
|
|
14
|
+
const existing = auth.getToken();
|
|
15
|
+
const existingPayload = existing ? auth.decodeToken(existing) : null;
|
|
16
|
+
if (existingPayload && existingPayload.nick) {
|
|
17
|
+
console.log(chalk.green(`已登录: ${existingPayload.nick}`));
|
|
18
|
+
console.log(chalk.gray('如需重新登录请先执行 rushangle logout'));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(chalk.cyan('正在启动本地服务器以接收登录凭证...'));
|
|
23
|
+
|
|
24
|
+
// Start local HTTP server to receive the callback
|
|
25
|
+
const server = http.createServer((req, res) => {
|
|
26
|
+
const url = new URL(req.url, `http://localhost:${CLI_PORT}`);
|
|
27
|
+
|
|
28
|
+
if (url.pathname === '/callback' && url.searchParams.has('token')) {
|
|
29
|
+
const token = url.searchParams.get('token');
|
|
30
|
+
auth.saveToken(token);
|
|
31
|
+
const payload = auth.decodeToken(token);
|
|
32
|
+
|
|
33
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
34
|
+
res.end(`<!DOCTYPE html>
|
|
35
|
+
<html><head><meta charset="utf-8"><title>登录成功</title><style>
|
|
36
|
+
body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#f5f5f5}
|
|
37
|
+
.card{background:#fff;padding:40px;border-radius:12px;text-align:center;box-shadow:0 2px 8px rgba(0,0,0,.1)}
|
|
38
|
+
h1{color:#22c55e;margin-bottom:8px}
|
|
39
|
+
p{color:#666;margin:4px 0}
|
|
40
|
+
</style></head><body>
|
|
41
|
+
<div class="card"><h1>✓ 登录成功</h1>
|
|
42
|
+
<p>${payload?.nick ? `欢迎,${payload.nick}` : ''}</p>
|
|
43
|
+
<p>${payload?.org || ''}</p>
|
|
44
|
+
<p style="margin-top:16px;color:#999;font-size:13px">您可以关闭此窗口回到终端</p></div>
|
|
45
|
+
</body></html>`);
|
|
46
|
+
|
|
47
|
+
setTimeout(() => { server.close(); process.exit(0); }, 2000);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (url.pathname === '/callback') {
|
|
52
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
53
|
+
res.end(`<html><head><meta charset="utf-8"></head><body><p>登录失败,请重试</p></body></html>`);
|
|
54
|
+
setTimeout(() => { server.close(); process.exit(1); }, 2000);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
res.writeHead(404);
|
|
59
|
+
res.end();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
server.listen(CLI_PORT, '127.0.0.1', async () => {
|
|
63
|
+
console.log(chalk.gray(`本地服务器已启动: http://localhost:${CLI_PORT}`));
|
|
64
|
+
console.log(chalk.cyan('正在打开浏览器进行钉钉授权登录...'));
|
|
65
|
+
|
|
66
|
+
// Open browser to DingTalk OAuth login with CLI callback params
|
|
67
|
+
const loginUrl = `${SITE_URL}/api/auth/dingtalk/login?cli=1&cli_port=${CLI_PORT}`;
|
|
68
|
+
await open(loginUrl);
|
|
69
|
+
console.log(chalk.gray('请在浏览器中完成钉钉授权...'));
|
|
70
|
+
console.log(chalk.yellow('等待授权完成... (如浏览器未打开,请手动访问: ' + loginUrl + ')'));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Timeout after 120 seconds
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
console.log(chalk.red('登录超时,请重试'));
|
|
76
|
+
server.close();
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}, 120000);
|
|
79
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const auth = require('../auth');
|
|
4
|
+
|
|
5
|
+
module.exports = new Command('logout')
|
|
6
|
+
.description('退出登录')
|
|
7
|
+
.action(() => {
|
|
8
|
+
const token = auth.getToken();
|
|
9
|
+
if (!token) {
|
|
10
|
+
console.log(chalk.yellow('当前未登录'));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const payload = auth.decodeToken(token);
|
|
15
|
+
const name = payload?.nick || '未知用户';
|
|
16
|
+
auth.clearToken();
|
|
17
|
+
console.log(chalk.green(`已退出: ${name}`));
|
|
18
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const auth = require('../auth');
|
|
6
|
+
const api = require('../api');
|
|
7
|
+
|
|
8
|
+
module.exports = new Command('mcp')
|
|
9
|
+
.description('管理 MCP 代码段')
|
|
10
|
+
.argument('[action]', '操作: list | get | publish | update | delete', 'list')
|
|
11
|
+
.argument('[id]', '代码段 ID 或名称')
|
|
12
|
+
.option('-n, --name <name>', '名称')
|
|
13
|
+
.option('-d, --description <desc>', '描述')
|
|
14
|
+
.option('-l, --language <lang>', '语言: python | javascript | typescript | json | text')
|
|
15
|
+
.option('-c, --code <file>', '代码文件路径')
|
|
16
|
+
.option('--config <file>', '配置 JSON 文件路径')
|
|
17
|
+
.option('--tags <tags>', '标签(逗号分隔)')
|
|
18
|
+
.option('-s, --search <query>', '搜索')
|
|
19
|
+
.action(async (action, id, opts) => {
|
|
20
|
+
const token = auth.getToken();
|
|
21
|
+
|
|
22
|
+
if (action === 'list') {
|
|
23
|
+
try {
|
|
24
|
+
const params = {};
|
|
25
|
+
if (opts.search) params.search = opts.search;
|
|
26
|
+
if (opts.language) params.language = opts.language;
|
|
27
|
+
const data = await api.listMcp(params);
|
|
28
|
+
const items = data.items || [];
|
|
29
|
+
console.log(chalk.bold.cyan(`\n MCP 代码段 (${items.length})`));
|
|
30
|
+
if (items.length === 0) {
|
|
31
|
+
console.log(' ' + chalk.gray('暂无代码段'));
|
|
32
|
+
console.log(chalk.gray(' 发布: rushangle mcp publish'));
|
|
33
|
+
} else {
|
|
34
|
+
items.forEach(m => {
|
|
35
|
+
console.log(` ${chalk.white(m.name)} ${chalk.gray('v' + m.version)} ${chalk.gray(m.language || '')}`);
|
|
36
|
+
console.log(` ${chalk.gray(m.description || '')}`);
|
|
37
|
+
console.log(` ID: ${chalk.gray(m.id)} 作者: ${chalk.gray(m.authorName || '-')}`);
|
|
38
|
+
console.log('');
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.log(chalk.red(`获取失败: ${e.message}`));
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (action === 'get') {
|
|
48
|
+
if (!id) { console.log(chalk.red('请指定 ID: rushangle mcp get <id>')); return; }
|
|
49
|
+
try {
|
|
50
|
+
const item = await api.getMcp(id);
|
|
51
|
+
console.log(chalk.bold.cyan(`\n${item.name}`));
|
|
52
|
+
console.log(chalk.gray(`版本: ${item.version} 语言: ${item.language}`));
|
|
53
|
+
console.log(chalk.gray(`作者: ${item.authorName} 更新: ${item.updated?.slice(0, 10)}`));
|
|
54
|
+
if (item.description) console.log(`\n${item.description}`);
|
|
55
|
+
if (item.code) {
|
|
56
|
+
console.log(chalk.bold.yellow('\n--- 代码 ---'));
|
|
57
|
+
console.log(item.code);
|
|
58
|
+
}
|
|
59
|
+
if (item.config) {
|
|
60
|
+
console.log(chalk.bold.yellow('\n--- 配置 ---'));
|
|
61
|
+
console.log(typeof item.config === 'string' ? item.config : JSON.stringify(item.config, null, 2));
|
|
62
|
+
}
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.log(chalk.red(`获取失败: ${e.message}`));
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// publish / update / delete require auth
|
|
70
|
+
if (!token) { console.log(chalk.red('请先登录: rushangle login')); return; }
|
|
71
|
+
|
|
72
|
+
if (action === 'publish') {
|
|
73
|
+
const name = opts.name;
|
|
74
|
+
if (!name) { console.log(chalk.red('请指定名称: rushangle mcp publish -n <name>')); return; }
|
|
75
|
+
|
|
76
|
+
let code = '';
|
|
77
|
+
let config = null;
|
|
78
|
+
if (opts.code) {
|
|
79
|
+
const fp = path.resolve(opts.code);
|
|
80
|
+
if (!fs.existsSync(fp)) { console.log(chalk.red(`文件不存在: ${fp}`)); return; }
|
|
81
|
+
code = fs.readFileSync(fp, 'utf-8');
|
|
82
|
+
}
|
|
83
|
+
if (opts.config) {
|
|
84
|
+
const fp = path.resolve(opts.config);
|
|
85
|
+
if (!fs.existsSync(fp)) { console.log(chalk.red(`文件不存在: ${fp}`)); return; }
|
|
86
|
+
const raw = fs.readFileSync(fp, 'utf-8');
|
|
87
|
+
try { config = JSON.parse(raw); } catch { config = raw; }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const result = await api.publishMcp({
|
|
92
|
+
name,
|
|
93
|
+
description: opts.description || '',
|
|
94
|
+
language: opts.language || (opts.code ? path.extname(opts.code).slice(1) : 'json'),
|
|
95
|
+
code,
|
|
96
|
+
config,
|
|
97
|
+
tags: opts.tags ? opts.tags.split(',').map(t => t.trim()).filter(Boolean) : [],
|
|
98
|
+
});
|
|
99
|
+
console.log(chalk.green(`✓ 发布成功: ${result.name} (${result.id})`));
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.log(chalk.red(`发布失败: ${e.message}`));
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (action === 'update') {
|
|
107
|
+
if (!id) { console.log(chalk.red('请指定 ID: rushangle mcp update <id>')); return; }
|
|
108
|
+
const data = {};
|
|
109
|
+
if (opts.name) data.name = opts.name;
|
|
110
|
+
if (opts.description) data.description = opts.description;
|
|
111
|
+
if (opts.language) data.language = opts.language;
|
|
112
|
+
if (opts.tags) data.tags = opts.tags.split(',').map(t => t.trim()).filter(Boolean);
|
|
113
|
+
try {
|
|
114
|
+
const result = await api.updateMcp(id, data);
|
|
115
|
+
console.log(chalk.green(`✓ 更新成功: ${result.name}`));
|
|
116
|
+
} catch (e) {
|
|
117
|
+
console.log(chalk.red(`更新失败: ${e.message}`));
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (action === 'delete') {
|
|
123
|
+
if (!id) { console.log(chalk.red('请指定 ID: rushangle mcp delete <id>')); return; }
|
|
124
|
+
try {
|
|
125
|
+
await api.deleteMcp(id);
|
|
126
|
+
console.log(chalk.green(`✓ 已删除: ${id}`));
|
|
127
|
+
} catch (e) {
|
|
128
|
+
console.log(chalk.red(`删除失败: ${e.message}`));
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(chalk.yellow(`未知操作: ${action},可用: list | get | publish | update | delete`));
|
|
134
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const auth = require('../auth');
|
|
6
|
+
const api = require('../api');
|
|
7
|
+
|
|
8
|
+
const VALID_TYPES = ['skill', 'mcp', 'code', 'doc'];
|
|
9
|
+
|
|
10
|
+
module.exports = new Command('publish')
|
|
11
|
+
.description('发布技能/MCP配置/代码段/文档到 SkillHub 市场')
|
|
12
|
+
.argument('[dir]', '项目目录路径', '.')
|
|
13
|
+
.option('-t, --type <type>', '类型: skill | mcp | code | doc', 'skill')
|
|
14
|
+
.option('-n, --name <name>', '名称')
|
|
15
|
+
.option('-d, --description <desc>', '描述')
|
|
16
|
+
.option('-v, --version <ver>', '版本号')
|
|
17
|
+
.option('-c, --category <cat>', '分类')
|
|
18
|
+
.option('--tags <tags>', '标签(逗号分隔)')
|
|
19
|
+
.option('--language <lang>', '语言(代码段)')
|
|
20
|
+
.option('--code <file>', '代码文件路径')
|
|
21
|
+
.option('--config <file>', '配置文件路径(MCP)')
|
|
22
|
+
.option('--content <file>', '内容文件路径(文档)')
|
|
23
|
+
.option('--format <fmt>', '文档格式: markdown | html | text', 'markdown')
|
|
24
|
+
.action(async (dir, opts) => {
|
|
25
|
+
const token = auth.getToken();
|
|
26
|
+
if (!token) {
|
|
27
|
+
console.log(chalk.red('请先登录: rushangle login'));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const type = opts.type;
|
|
32
|
+
if (!VALID_TYPES.includes(type)) {
|
|
33
|
+
console.log(chalk.red(`无效类型: ${type},可选: ${VALID_TYPES.join(' | ')}`));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const absDir = path.resolve(dir);
|
|
38
|
+
let meta = {};
|
|
39
|
+
|
|
40
|
+
// Read metadata from file if exists
|
|
41
|
+
const skillJson = path.join(absDir, 'skill.json');
|
|
42
|
+
const pkgJson = path.join(absDir, 'package.json');
|
|
43
|
+
const rushJson = path.join(absDir, 'rushangle.json');
|
|
44
|
+
|
|
45
|
+
for (const f of [rushJson, skillJson, pkgJson]) {
|
|
46
|
+
if (fs.existsSync(f)) {
|
|
47
|
+
try { meta = JSON.parse(fs.readFileSync(f, 'utf-8')); break; }
|
|
48
|
+
catch { /* ignore parse errors */ }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const name = opts.name || meta.name || path.basename(absDir);
|
|
53
|
+
const description = opts.description || meta.description || '';
|
|
54
|
+
const version = opts.version || meta.version || '0.1.0';
|
|
55
|
+
const category = opts.category || meta.category || '';
|
|
56
|
+
const tags = opts.tags ? opts.tags.split(',').map(t => t.trim()).filter(Boolean)
|
|
57
|
+
: (meta.tags || []);
|
|
58
|
+
|
|
59
|
+
// Read README if exists
|
|
60
|
+
let readme = '';
|
|
61
|
+
for (const f of ['README.md', 'readme.md', 'README', 'readme']) {
|
|
62
|
+
const fp = path.join(absDir, f);
|
|
63
|
+
if (fs.existsSync(fp)) { readme = fs.readFileSync(fp, 'utf-8'); break; }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(chalk.cyan(`正在发布 ${type}: ${name}@${version} ...`));
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
let result;
|
|
70
|
+
|
|
71
|
+
if (type === 'skill') {
|
|
72
|
+
result = await api.publishSkill({
|
|
73
|
+
name, version, description, category, tags, readme,
|
|
74
|
+
});
|
|
75
|
+
} else if (type === 'mcp') {
|
|
76
|
+
let code = '';
|
|
77
|
+
let config = null;
|
|
78
|
+
if (opts.code) {
|
|
79
|
+
const cfp = path.resolve(opts.code);
|
|
80
|
+
if (fs.existsSync(cfp)) code = fs.readFileSync(cfp, 'utf-8');
|
|
81
|
+
else { console.log(chalk.red(`代码文件不存在: ${cfp}`)); return; }
|
|
82
|
+
}
|
|
83
|
+
if (opts.config) {
|
|
84
|
+
const cfp = path.resolve(opts.config);
|
|
85
|
+
if (fs.existsSync(cfp)) {
|
|
86
|
+
try { config = JSON.parse(fs.readFileSync(cfp, 'utf-8')); }
|
|
87
|
+
catch { config = fs.readFileSync(cfp, 'utf-8'); }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Auto-detect language from file extension
|
|
91
|
+
const language = opts.language || (opts.code
|
|
92
|
+
? path.extname(opts.code).slice(1) || 'text'
|
|
93
|
+
: 'json');
|
|
94
|
+
|
|
95
|
+
result = await api.publishMcp({
|
|
96
|
+
name, version, description, language, code,
|
|
97
|
+
config: config || meta.config || null,
|
|
98
|
+
tags, readme,
|
|
99
|
+
});
|
|
100
|
+
} else if (type === 'code') {
|
|
101
|
+
let code = '';
|
|
102
|
+
if (opts.code) {
|
|
103
|
+
const cfp = path.resolve(opts.code);
|
|
104
|
+
if (fs.existsSync(cfp)) code = fs.readFileSync(cfp, 'utf-8');
|
|
105
|
+
else { console.log(chalk.red(`代码文件不存在: ${cfp}`)); return; }
|
|
106
|
+
}
|
|
107
|
+
const language = opts.language || (opts.code
|
|
108
|
+
? path.extname(opts.code).slice(1) || 'text'
|
|
109
|
+
: meta.language || 'text');
|
|
110
|
+
|
|
111
|
+
result = await api.publishCode({
|
|
112
|
+
name, language, description,
|
|
113
|
+
code: code || readme,
|
|
114
|
+
tags, category, version,
|
|
115
|
+
});
|
|
116
|
+
} else if (type === 'doc') {
|
|
117
|
+
let content = '';
|
|
118
|
+
if (opts.content) {
|
|
119
|
+
const cfp = path.resolve(opts.content);
|
|
120
|
+
if (fs.existsSync(cfp)) content = fs.readFileSync(cfp, 'utf-8');
|
|
121
|
+
else { console.log(chalk.red(`内容文件不存在: ${cfp}`)); return; }
|
|
122
|
+
}
|
|
123
|
+
result = await api.publishDoc({
|
|
124
|
+
name, version, description, category: category || '文档',
|
|
125
|
+
content: content || readme,
|
|
126
|
+
format: opts.format,
|
|
127
|
+
tags,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log(chalk.green(`✓ 发布成功: ${result.name || result.id}`));
|
|
132
|
+
console.log(chalk.gray(` ID: ${result.id}`));
|
|
133
|
+
if (result.language) console.log(chalk.gray(` 语言: ${result.language}`));
|
|
134
|
+
console.log(chalk.gray(` 链接: ${api.SITE_URL}/${type === 'doc' ? 'docs' : type === 'code' ? 'code' : type === 'mcp' ? 'mcp' : 'skills'}/${result.id}`));
|
|
135
|
+
} catch (e) {
|
|
136
|
+
console.log(chalk.red(`发布失败: ${e.message}`));
|
|
137
|
+
if (e.data) console.log(chalk.gray(JSON.stringify(e.data)));
|
|
138
|
+
}
|
|
139
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const { search } = require('../api');
|
|
4
|
+
|
|
5
|
+
function snippet(text, maxLen) {
|
|
6
|
+
if (!text) return '';
|
|
7
|
+
const t = String(text).replace(/\s+/g, ' ').trim();
|
|
8
|
+
return t.length > maxLen ? t.slice(0, maxLen) + '...' : t;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = new Command('search')
|
|
12
|
+
.description('全局搜索技能/MCP/代码/文档')
|
|
13
|
+
.argument('<query>', '搜索关键词')
|
|
14
|
+
.action(async (query) => {
|
|
15
|
+
console.log(chalk.cyan(`搜索: "${query}" ...\n`));
|
|
16
|
+
try {
|
|
17
|
+
const data = await search(query);
|
|
18
|
+
|
|
19
|
+
if (data.skills && data.skills.length > 0) {
|
|
20
|
+
console.log(chalk.bold.yellow('📦 技能:'));
|
|
21
|
+
data.skills.forEach(s => {
|
|
22
|
+
console.log(` ${chalk.white(s.name)} ${chalk.gray('v' + s.version)} ${chalk.gray(s.category)}`);
|
|
23
|
+
if (s.description) console.log(` ${chalk.gray(snippet(s.description, 80))}`);
|
|
24
|
+
});
|
|
25
|
+
console.log('');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (data.mcp && data.mcp.length > 0) {
|
|
29
|
+
console.log(chalk.bold.green('🔧 MCP 配置:'));
|
|
30
|
+
data.mcp.forEach(m => {
|
|
31
|
+
console.log(` ${chalk.white(m.name)} ${chalk.gray('v' + m.version)} ${chalk.gray(m.language)}`);
|
|
32
|
+
if (m.description) console.log(` ${chalk.gray(snippet(m.description, 80))}`);
|
|
33
|
+
});
|
|
34
|
+
console.log('');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (data.code && data.code.length > 0) {
|
|
38
|
+
console.log(chalk.bold.magenta('💻 代码段:'));
|
|
39
|
+
data.code.forEach(c => {
|
|
40
|
+
console.log(` ${chalk.white(c.name)} ${chalk.gray(c.language)}`);
|
|
41
|
+
if (c.description) console.log(` ${chalk.gray(snippet(c.description, 80))}`);
|
|
42
|
+
});
|
|
43
|
+
console.log('');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (data.docs && data.docs.length > 0) {
|
|
47
|
+
console.log(chalk.bold.blue('📄 文档:'));
|
|
48
|
+
data.docs.forEach(d => {
|
|
49
|
+
console.log(` ${chalk.white(d.name)} ${chalk.gray('v' + d.version)} ${chalk.gray(d.format)}`);
|
|
50
|
+
if (d.description) console.log(` ${chalk.gray(snippet(d.description, 80))}`);
|
|
51
|
+
});
|
|
52
|
+
console.log('');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const total = (data.skills?.length || 0) + (data.mcp?.length || 0) + (data.code?.length || 0) + (data.docs?.length || 0);
|
|
56
|
+
if (total === 0) {
|
|
57
|
+
console.log(chalk.gray(' 未找到匹配结果'));
|
|
58
|
+
} else {
|
|
59
|
+
console.log(chalk.gray(` 共 ${total} 条结果`));
|
|
60
|
+
}
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.log(chalk.red(`搜索失败: ${e.message}`));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const auth = require('../auth');
|
|
4
|
+
|
|
5
|
+
module.exports = new Command('whoami')
|
|
6
|
+
.description('查看当前登录用户信息')
|
|
7
|
+
.alias('me')
|
|
8
|
+
.action(async () => {
|
|
9
|
+
const token = auth.getToken();
|
|
10
|
+
if (!token) {
|
|
11
|
+
console.log(chalk.yellow('未登录,请先运行: rushangle login'));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const payload = auth.decodeToken(token);
|
|
16
|
+
if (!payload) {
|
|
17
|
+
console.log(chalk.red('Token 无效,请重新登录'));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log('');
|
|
22
|
+
console.log(` ${chalk.bold(payload.nick || '未设置')}`);
|
|
23
|
+
if (payload.org) {
|
|
24
|
+
console.log(` ${chalk.gray(payload.org)}`);
|
|
25
|
+
}
|
|
26
|
+
console.log('');
|
|
27
|
+
if (payload.department) {
|
|
28
|
+
console.log(` ${chalk.gray('部门')} ${payload.department}`);
|
|
29
|
+
}
|
|
30
|
+
if (payload.title) {
|
|
31
|
+
console.log(` ${chalk.gray('职务')} ${payload.title}`);
|
|
32
|
+
}
|
|
33
|
+
if (payload.email) {
|
|
34
|
+
console.log(` ${chalk.gray('邮箱')} ${payload.email}`);
|
|
35
|
+
}
|
|
36
|
+
if (payload.mobile) {
|
|
37
|
+
console.log(` ${chalk.gray('手机')} ${payload.mobile}`);
|
|
38
|
+
}
|
|
39
|
+
console.log('');
|
|
40
|
+
});
|
package/src/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const pkg = require('../package.json');
|
|
3
|
+
|
|
4
|
+
const program = new Command();
|
|
5
|
+
|
|
6
|
+
program
|
|
7
|
+
.name('rushangle')
|
|
8
|
+
.description('SkillHub CLI — 数智凯航技能市场')
|
|
9
|
+
.version(pkg.version);
|
|
10
|
+
|
|
11
|
+
program.addCommand(require('./commands/login'));
|
|
12
|
+
program.addCommand(require('./commands/whoami'));
|
|
13
|
+
program.addCommand(require('./commands/logout'));
|
|
14
|
+
program.addCommand(require('./commands/publish'));
|
|
15
|
+
program.addCommand(require('./commands/install'));
|
|
16
|
+
program.addCommand(require('./commands/list'));
|
|
17
|
+
program.addCommand(require('./commands/search'));
|
|
18
|
+
program.addCommand(require('./commands/mcp'));
|
|
19
|
+
program.addCommand(require('./commands/code'));
|
|
20
|
+
program.addCommand(require('./commands/doc'));
|
|
21
|
+
program.parse();
|