rushangle-cli 0.2.5 → 0.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rushangle-cli",
3
- "version": "0.2.5",
3
+ "version": "0.4.0",
4
4
  "description": "SkillHub CLI - 数智凯航技能市场命令行工具",
5
5
  "bin": {
6
6
  "rushangle": "./bin/rushangle.js"
package/src/api.js CHANGED
@@ -87,6 +87,63 @@ 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
104
+ async uploadCliFiles(cliId, files) {
105
+ const token = auth.getToken();
106
+ const FormData = require('form-data');
107
+ const form = new FormData();
108
+ for (const { fieldName, filePath } of files) {
109
+ form.append(fieldName, fs.createReadStream(filePath), path.basename(filePath));
110
+ }
111
+
112
+ const res = await fetch(`${SITE_URL}/api/cli/${encodeURIComponent(cliId)}/files`, {
113
+ method: 'POST',
114
+ headers: { ...form.getHeaders(), 'Authorization': `Bearer ${token}` },
115
+ body: form,
116
+ });
117
+
118
+ const data = await res.json();
119
+ if (!res.ok) {
120
+ const err = new Error(data.error || 'Upload failed');
121
+ err.status = res.status;
122
+ err.data = data;
123
+ throw err;
124
+ }
125
+ return data;
126
+ },
127
+
128
+ // CLI file download
129
+ async downloadCliFiles(cliId) {
130
+ const token = auth.getToken();
131
+ const res = await fetch(`${SITE_URL}/api/cli/${encodeURIComponent(cliId)}/download`, {
132
+ headers: { 'Authorization': `Bearer ${token}` },
133
+ });
134
+
135
+ if (!res.ok) {
136
+ const data = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
137
+ const err = new Error(data.error || 'Download failed');
138
+ err.status = res.status;
139
+ throw err;
140
+ }
141
+
142
+ const contentType = res.headers.get('content-type') || '';
143
+ const arrayBuffer = await res.arrayBuffer();
144
+ return { buffer: Buffer.from(arrayBuffer), contentType };
145
+ },
146
+
90
147
  // Search
91
148
  search(query) { return apiCall('GET', `/api/search?q=${encodeURIComponent(query)}`); },
92
149
 
@@ -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
+ });
@@ -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
- const INSTALL_DIR = path.join(os.homedir(), '.workbuddy', 'skills');
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
- .option('-t, --type <type>', '类型: skill | mcp | code | doc', 'skill')
24
+ .option('-t, --type <type>', '类型: skill | mcp | code | doc | cli', '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(INSTALL_DIR, item.name);
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(INSTALL_DIR, item.name);
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(INSTALL_DIR, 'mcp', item.id);
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(INSTALL_DIR, 'code', item.id);
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(INSTALL_DIR, 'docs', item.id);
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) {
@@ -284,6 +295,73 @@ module.exports = new Command('install')
284
295
  console.log(chalk.green(`✓ 安装成功: ${item.name}`));
285
296
  if (showLocalPath) console.log(chalk.gray(` 本地目录: ${installDir}`));
286
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
+
287
365
  } else {
288
366
  console.log(chalk.red(`不支持的类型: ${type}`));
289
367
  }
@@ -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
+ });
@@ -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
@@ -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配置/代码段/文档到 SkillHub 市场')
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();
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'));