rushangle-cli 0.2.4 → 0.3.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/package.json +1 -1
- package/src/commands/config.js +49 -0
- package/src/commands/install.js +18 -7
- package/src/commands/link.js +67 -0
- package/src/commands/publish.js +51 -4
- package/src/config.js +42 -0
- package/src/index.js +2 -0
package/package.json
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const { getConfig, setConfig, listConfig } = require('../config');
|
|
4
|
+
|
|
5
|
+
module.exports = new Command('config')
|
|
6
|
+
.description('管理 CLI 配置')
|
|
7
|
+
.argument('<action>', 'get | set | delete | list')
|
|
8
|
+
.argument('[key]', '配置键名')
|
|
9
|
+
.argument('[value]', '配置值(set 时需要)')
|
|
10
|
+
.action((action, key, value) => {
|
|
11
|
+
switch (action) {
|
|
12
|
+
case 'list':
|
|
13
|
+
listConfig();
|
|
14
|
+
break;
|
|
15
|
+
case 'get': {
|
|
16
|
+
if (!key) {
|
|
17
|
+
console.log(chalk.yellow('用法: rushangle config get <key>'));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const cfg = getConfig();
|
|
21
|
+
if (cfg[key] !== undefined) {
|
|
22
|
+
console.log(cfg[key]);
|
|
23
|
+
} else {
|
|
24
|
+
console.log(chalk.gray('(未设置)'));
|
|
25
|
+
}
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
case 'set': {
|
|
29
|
+
if (!key || value === undefined) {
|
|
30
|
+
console.log(chalk.yellow('用法: rushangle config set <key> <value>'));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
setConfig(key, value);
|
|
34
|
+
console.log(chalk.green(`✓ ${key} = ${value}`));
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
case 'delete': {
|
|
38
|
+
if (!key) {
|
|
39
|
+
console.log(chalk.yellow('用法: rushangle config delete <key>'));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
setConfig(key, null);
|
|
43
|
+
console.log(chalk.green(`✓ 已删除 ${key}`));
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
default:
|
|
47
|
+
console.log(chalk.red(`未知操作: ${action}。支持: get | set | delete | list`));
|
|
48
|
+
}
|
|
49
|
+
});
|
package/src/commands/install.js
CHANGED
|
@@ -5,16 +5,28 @@ const path = require('path');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const auth = require('../auth');
|
|
7
7
|
const api = require('../api');
|
|
8
|
+
const { getConfig } = require('../config');
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
// SkillHub 自己的地盘,平台中立
|
|
11
|
+
const DEFAULT_INSTALL_DIR = path.join(os.homedir(), '.rushangle', 'skills');
|
|
12
|
+
|
|
13
|
+
function resolveInstallDir(opts) {
|
|
14
|
+
// 优先级: --dir > config installDir > 默认 ~/.rushangle/skills/
|
|
15
|
+
if (opts.dir) return opts.dir;
|
|
16
|
+
const config = getConfig();
|
|
17
|
+
if (config.installDir) return config.installDir;
|
|
18
|
+
return DEFAULT_INSTALL_DIR;
|
|
19
|
+
}
|
|
10
20
|
|
|
11
21
|
module.exports = new Command('install')
|
|
12
22
|
.description('安装技能/MCP/代码段')
|
|
13
23
|
.argument('[skill]', '技能名称或 ID')
|
|
14
24
|
.option('-t, --type <type>', '类型: skill | mcp | code | doc', 'skill')
|
|
15
25
|
.option('-g, --global', '全局安装(默认)', true)
|
|
26
|
+
.option('-d, --dir <dir>', '自定义安装目录(覆盖 config 和默认值)')
|
|
16
27
|
.option('--register-only', '仅注册到服务器,不写入本地文件(适用于受限环境)')
|
|
17
28
|
.action(async (nameOrId, opts) => {
|
|
29
|
+
const skillDirBase = resolveInstallDir(opts);
|
|
18
30
|
const token = auth.getToken();
|
|
19
31
|
if (!token) {
|
|
20
32
|
console.log(chalk.red('请先登录: rushangle login'));
|
|
@@ -67,7 +79,7 @@ module.exports = new Command('install')
|
|
|
67
79
|
const { buffer } = await api.downloadSkillFiles(item.id);
|
|
68
80
|
const AdmZip = require('adm-zip');
|
|
69
81
|
const zip = new AdmZip(buffer);
|
|
70
|
-
const skillDir = path.join(
|
|
82
|
+
const skillDir = path.join(skillDirBase, item.name);
|
|
71
83
|
zip.extractAllTo(skillDir, true);
|
|
72
84
|
console.log(chalk.green(`✓ 安装成功: ${item.name}@${item.version}`));
|
|
73
85
|
console.log(chalk.gray(` 本地目录: ${skillDir}`));
|
|
@@ -83,8 +95,7 @@ module.exports = new Command('install')
|
|
|
83
95
|
|
|
84
96
|
// Fallback: create SKILL.md from metadata if no files were downloaded
|
|
85
97
|
if (!extracted) {
|
|
86
|
-
const skillDir = path.join(
|
|
87
|
-
fs.mkdirSync(skillDir, { recursive: true });
|
|
98
|
+
const skillDir = path.join(skillDirBase, item.name);
|
|
88
99
|
|
|
89
100
|
// Generate SKILL.md with frontmatter from server metadata
|
|
90
101
|
const frontmatter = [
|
|
@@ -146,7 +157,7 @@ module.exports = new Command('install')
|
|
|
146
157
|
let installDir = '';
|
|
147
158
|
if (!opts.registerOnly) {
|
|
148
159
|
try {
|
|
149
|
-
installDir = path.join(
|
|
160
|
+
installDir = path.join(skillDirBase, 'mcp', item.id);
|
|
150
161
|
fs.mkdirSync(installDir, { recursive: true });
|
|
151
162
|
if (item.code) {
|
|
152
163
|
const ext = item.language === 'python' || item.language === 'py' ? '.py'
|
|
@@ -203,7 +214,7 @@ module.exports = new Command('install')
|
|
|
203
214
|
let installDir = '';
|
|
204
215
|
if (!opts.registerOnly) {
|
|
205
216
|
try {
|
|
206
|
-
installDir = path.join(
|
|
217
|
+
installDir = path.join(skillDirBase, 'code', item.id);
|
|
207
218
|
fs.mkdirSync(installDir, { recursive: true });
|
|
208
219
|
if (item.code) {
|
|
209
220
|
const extMap = { python: '.py', py: '.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' };
|
|
@@ -254,7 +265,7 @@ module.exports = new Command('install')
|
|
|
254
265
|
let installDir = '';
|
|
255
266
|
if (!opts.registerOnly) {
|
|
256
267
|
try {
|
|
257
|
-
installDir = path.join(
|
|
268
|
+
installDir = path.join(skillDirBase, 'docs', item.id);
|
|
258
269
|
fs.mkdirSync(installDir, { recursive: true });
|
|
259
270
|
// Write document content as markdown
|
|
260
271
|
if (item.content) {
|
|
@@ -0,0 +1,67 @@
|
|
|
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 { getConfig } = require('../config');
|
|
7
|
+
|
|
8
|
+
const DEFAULT_INSTALL_DIR = path.join(os.homedir(), '.rushangle', 'skills');
|
|
9
|
+
|
|
10
|
+
function resolveInstallDir() {
|
|
11
|
+
const config = getConfig();
|
|
12
|
+
return config.installDir || DEFAULT_INSTALL_DIR;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = new Command('link')
|
|
16
|
+
.description('将已安装的技能链接到目标目录(创建符号链接)')
|
|
17
|
+
.argument('<name>', '技能名称')
|
|
18
|
+
.argument('<target>', '目标目录路径')
|
|
19
|
+
.option('-t, --type <type>', '类型: skill | mcp | code | doc', 'skill')
|
|
20
|
+
.action((name, target, opts) => {
|
|
21
|
+
const installDir = resolveInstallDir();
|
|
22
|
+
|
|
23
|
+
let srcDir;
|
|
24
|
+
if (opts.type === 'skill') {
|
|
25
|
+
srcDir = path.join(installDir, name);
|
|
26
|
+
} else if (opts.type === 'mcp') {
|
|
27
|
+
srcDir = path.join(installDir, 'mcp', name);
|
|
28
|
+
} else if (opts.type === 'code') {
|
|
29
|
+
srcDir = path.join(installDir, 'code', name);
|
|
30
|
+
} else if (opts.type === 'doc') {
|
|
31
|
+
srcDir = path.join(installDir, 'docs', name);
|
|
32
|
+
} else {
|
|
33
|
+
srcDir = path.join(installDir, name);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!fs.existsSync(srcDir)) {
|
|
37
|
+
console.log(chalk.red(`未找到已安装的 ${opts.type}: ${name}`));
|
|
38
|
+
console.log(chalk.gray(` 源目录: ${srcDir}`));
|
|
39
|
+
console.log(chalk.gray(` 提示: 先用 rushangle install ${name} 安装`));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Resolve target to absolute path
|
|
44
|
+
const absTarget = path.resolve(target.replace(/^~/, os.homedir()));
|
|
45
|
+
if (!fs.existsSync(absTarget)) {
|
|
46
|
+
fs.mkdirSync(absTarget, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const destDir = path.join(absTarget, name);
|
|
50
|
+
if (fs.existsSync(destDir)) {
|
|
51
|
+
const stat = fs.lstatSync(destDir);
|
|
52
|
+
if (stat.isSymbolicLink()) {
|
|
53
|
+
console.log(chalk.yellow(`链接已存在: ${destDir} → ${fs.readlinkSync(destDir)}`));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
console.log(chalk.yellow(`目标已存在(非符号链接),跳过: ${destDir}`));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
fs.symlinkSync(srcDir, destDir, 'dir');
|
|
62
|
+
console.log(chalk.green(`✓ 链接成功`));
|
|
63
|
+
console.log(chalk.gray(` ${srcDir} → ${destDir}`));
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.log(chalk.red(`链接失败: ${e.message}`));
|
|
66
|
+
}
|
|
67
|
+
});
|
package/src/commands/publish.js
CHANGED
|
@@ -8,6 +8,46 @@ const api = require('../api');
|
|
|
8
8
|
|
|
9
9
|
const VALID_TYPES = ['skill', 'mcp', 'code', 'doc'];
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Simple YAML frontmatter parser for SKILL.md.
|
|
13
|
+
* Extracts key-value pairs from --- delimited frontmatter block.
|
|
14
|
+
*/
|
|
15
|
+
function parseFrontmatter(content) {
|
|
16
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
17
|
+
if (!match) return {};
|
|
18
|
+
const fm = {};
|
|
19
|
+
const lines = match[1].split(/\r?\n/);
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
const kv = line.match(/^(\w[\w-]*)\s*:\s*(.+)$/);
|
|
22
|
+
if (kv) {
|
|
23
|
+
const key = kv[1].trim();
|
|
24
|
+
let value = kv[2].trim();
|
|
25
|
+
// Strip quotes
|
|
26
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
27
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
28
|
+
value = value.slice(1, -1);
|
|
29
|
+
}
|
|
30
|
+
fm[key] = value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return fm;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Read SKILL.md and extract frontmatter + body content.
|
|
38
|
+
*/
|
|
39
|
+
function readSkillMd(absDir) {
|
|
40
|
+
for (const f of ['SKILL.md', 'skill.md', 'Skill.md']) {
|
|
41
|
+
const fp = path.join(absDir, f);
|
|
42
|
+
if (fs.existsSync(fp)) {
|
|
43
|
+
const content = fs.readFileSync(fp, 'utf-8');
|
|
44
|
+
const frontmatter = parseFrontmatter(content);
|
|
45
|
+
return { content, frontmatter, path: fp };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { content: '', frontmatter: {}, path: null };
|
|
49
|
+
}
|
|
50
|
+
|
|
11
51
|
async function askQuestion(query) {
|
|
12
52
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
13
53
|
return new Promise(resolve => rl.question(query, ans => { rl.close(); resolve(ans.trim()); }));
|
|
@@ -108,21 +148,28 @@ module.exports = new Command('publish')
|
|
|
108
148
|
}
|
|
109
149
|
}
|
|
110
150
|
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
const
|
|
151
|
+
// Read SKILL.md frontmatter for skill metadata
|
|
152
|
+
const skillMd = type === 'skill' ? readSkillMd(absDir) : null;
|
|
153
|
+
const fm = (skillMd && skillMd.frontmatter) || {};
|
|
154
|
+
|
|
155
|
+
const name = opts.name || meta.name || fm.name || path.basename(absDir);
|
|
156
|
+
const description = opts.description || meta.description || fm.description || '';
|
|
157
|
+
const version = opts.version || meta.version || fm.version || '0.1.0';
|
|
114
158
|
const tags = opts.tags ? opts.tags.split(',').map(t => t.trim()).filter(Boolean)
|
|
115
159
|
: (meta.tags || []);
|
|
116
160
|
|
|
117
161
|
// Resolve category via server presets
|
|
118
162
|
const { resolved: category } = await resolveCategory(type, opts.category || meta.category || '', api);
|
|
119
163
|
|
|
120
|
-
// Read README if exists
|
|
164
|
+
// Read README if exists, otherwise fall back to SKILL.md body
|
|
121
165
|
let readme = '';
|
|
122
166
|
for (const f of ['README.md', 'readme.md', 'README', 'readme']) {
|
|
123
167
|
const fp = path.join(absDir, f);
|
|
124
168
|
if (fs.existsSync(fp)) { readme = fs.readFileSync(fp, 'utf-8'); break; }
|
|
125
169
|
}
|
|
170
|
+
if (!readme && skillMd && skillMd.content) {
|
|
171
|
+
readme = skillMd.content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '').trim();
|
|
172
|
+
}
|
|
126
173
|
|
|
127
174
|
console.log(chalk.cyan(`正在发布 ${type}: ${name}@${version} ...`));
|
|
128
175
|
|
package/src/config.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
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 getConfig() {
|
|
9
|
+
try {
|
|
10
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
11
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
12
|
+
}
|
|
13
|
+
} catch { /* ignore corrupt config */ }
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function setConfig(key, value) {
|
|
18
|
+
const config = getConfig();
|
|
19
|
+
if (value == null) {
|
|
20
|
+
delete config[key];
|
|
21
|
+
} else {
|
|
22
|
+
config[key] = value;
|
|
23
|
+
}
|
|
24
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
25
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function listConfig() {
|
|
31
|
+
const config = getConfig();
|
|
32
|
+
const keys = Object.keys(config);
|
|
33
|
+
if (keys.length === 0) {
|
|
34
|
+
console.log('(无配置)');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
for (const k of keys) {
|
|
38
|
+
console.log(`${k} = ${config[k]}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { getConfig, setConfig, listConfig, CONFIG_FILE };
|
package/src/index.js
CHANGED
|
@@ -18,6 +18,8 @@ program.addCommand(require('./commands/logout'));
|
|
|
18
18
|
program.addCommand(require('./commands/publish'));
|
|
19
19
|
program.addCommand(require('./commands/unpublish'));
|
|
20
20
|
program.addCommand(require('./commands/install'));
|
|
21
|
+
program.addCommand(require('./commands/link'));
|
|
22
|
+
program.addCommand(require('./commands/config'));
|
|
21
23
|
program.addCommand(require('./commands/list'));
|
|
22
24
|
program.addCommand(require('./commands/search'));
|
|
23
25
|
program.addCommand(require('./commands/mcp'));
|