rushangle-cli 0.1.2 → 0.1.4

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.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "SkillHub CLI - 数智凯航技能市场命令行工具",
5
5
  "bin": {
6
6
  "rushangle": "./bin/rushangle.js"
@@ -13,7 +13,12 @@
13
13
  "scripts": {
14
14
  "start": "node bin/rushangle.js"
15
15
  },
16
- "keywords": ["rushangle", "skillhub", "cli", "skills"],
16
+ "keywords": [
17
+ "rushangle",
18
+ "skillhub",
19
+ "cli",
20
+ "skills"
21
+ ],
17
22
  "license": "MIT",
18
23
  "dependencies": {
19
24
  "commander": "^12.0.0",
package/src/api.js CHANGED
@@ -87,4 +87,7 @@ module.exports = {
87
87
 
88
88
  // Search
89
89
  search(query) { return apiCall('GET', `/api/search?q=${encodeURIComponent(query)}`); },
90
+
91
+ // Categories (no auth required)
92
+ getCategories() { return apiCall('GET', '/api/skills/categories'); },
90
93
  };
@@ -2,11 +2,70 @@ const { Command } = require('commander');
2
2
  const chalk = require('chalk');
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const readline = require('readline');
5
6
  const auth = require('../auth');
6
7
  const api = require('../api');
7
8
 
8
9
  const VALID_TYPES = ['skill', 'mcp', 'code', 'doc'];
9
10
 
11
+ async function askQuestion(query) {
12
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
13
+ return new Promise(resolve => rl.question(query, ans => { rl.close(); resolve(ans.trim()); }));
14
+ }
15
+
16
+ /**
17
+ * Fetch preset categories from server and attempt to match/resolve user's category.
18
+ * @returns {{ categories: string[], resolved: string }} - resolved is the final category to use
19
+ */
20
+ async function resolveCategory(type, userCategory, apiModule) {
21
+ const typeMap = { skill: 'skill', mcp: 'mcp', code: 'code', doc: 'doc' };
22
+ const t = typeMap[type] || 'skill';
23
+
24
+ let serverCategories = [];
25
+ try {
26
+ const data = await apiModule.getCategories();
27
+ serverCategories = data[t] || data.skill || [];
28
+ } catch {
29
+ // Server unreachable — just use whatever user provided
30
+ return { categories: [], resolved: userCategory };
31
+ }
32
+
33
+ if (!serverCategories.length) {
34
+ return { categories: [], resolved: userCategory };
35
+ }
36
+
37
+ // Case 1: user already specified a category that matches → use it
38
+ if (userCategory) {
39
+ const exact = serverCategories.find(c => c.toLowerCase() === userCategory.toLowerCase());
40
+ if (exact) {
41
+ console.log(chalk.gray(` 分类: ${exact}`));
42
+ return { categories: serverCategories, resolved: exact };
43
+ }
44
+ // Case 2: user specified but doesn't match → show mismatch, let them pick
45
+ console.log(chalk.yellow(` ⚠ 指定的分类 "${userCategory}" 不在预设列表中,请从以下选择:`));
46
+ }
47
+
48
+ // Show numbered list
49
+ console.log(chalk.cyan(`\n 请选择 ${type} 分类:`));
50
+ serverCategories.forEach((c, i) => {
51
+ console.log(chalk.white(` ${chalk.bold(String(i + 1))}. ${c}`));
52
+ });
53
+ console.log(chalk.gray(` ${serverCategories.length + 1}. 自定义(输入文本)`));
54
+
55
+ const answer = await askQuestion(chalk.green('\n 输入编号或分类名: '));
56
+ const num = parseInt(answer, 10);
57
+ if (num >= 1 && num <= serverCategories.length) {
58
+ console.log(chalk.gray(` 分类: ${serverCategories[num - 1]}`));
59
+ return { categories: serverCategories, resolved: serverCategories[num - 1] };
60
+ }
61
+ if (answer) {
62
+ console.log(chalk.gray(` 分类: ${answer}`));
63
+ return { categories: serverCategories, resolved: answer };
64
+ }
65
+ console.log(chalk.gray(` 分类: ${serverCategories[0]}`));
66
+ return { categories: serverCategories, resolved: serverCategories[0] };
67
+ }
68
+
10
69
  module.exports = new Command('publish')
11
70
  .description('发布技能/MCP配置/代码段/文档到 SkillHub 市场')
12
71
  .argument('[dir]', '项目目录路径', '.')
@@ -52,10 +111,12 @@ module.exports = new Command('publish')
52
111
  const name = opts.name || meta.name || path.basename(absDir);
53
112
  const description = opts.description || meta.description || '';
54
113
  const version = opts.version || meta.version || '0.1.0';
55
- const category = opts.category || meta.category || '';
56
114
  const tags = opts.tags ? opts.tags.split(',').map(t => t.trim()).filter(Boolean)
57
115
  : (meta.tags || []);
58
116
 
117
+ // Resolve category via server presets
118
+ const { resolved: category } = await resolveCategory(type, opts.category || meta.category || '', api);
119
+
59
120
  // Read README if exists
60
121
  let readme = '';
61
122
  for (const f of ['README.md', 'readme.md', 'README', 'readme']) {
package/src/index.js CHANGED
@@ -1,5 +1,9 @@
1
1
  const { Command } = require('commander');
2
2
  const pkg = require('../package.json');
3
+ const { checkUpdate } = require('./update-checker');
4
+
5
+ // 启动时异步检查更新(不阻塞命令执行)
6
+ checkUpdate();
3
7
 
4
8
  const program = new Command();
5
9
 
@@ -0,0 +1,107 @@
1
+ const https = require('https');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const chalk = require('chalk');
5
+
6
+ const CACHE_DIR = path.join(require('os').homedir(), '.rushangle');
7
+ const CACHE_FILE = path.join(CACHE_DIR, '.update-check');
8
+ const CHECK_INTERVAL = 1000 * 60 * 60 * 24; // 每 24 小时检查一次
9
+
10
+ function getCache() {
11
+ try {
12
+ if (fs.existsSync(CACHE_FILE)) {
13
+ return JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8'));
14
+ }
15
+ } catch {}
16
+ return null;
17
+ }
18
+
19
+ function setCache(data) {
20
+ try {
21
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
22
+ fs.writeFileSync(CACHE_FILE, JSON.stringify(data));
23
+ } catch {}
24
+ }
25
+
26
+ function npmLatest(pkgName) {
27
+ return new Promise((resolve, reject) => {
28
+ const req = https.get(
29
+ `https://registry.npmjs.org/${encodeURIComponent(pkgName)}/latest`,
30
+ { timeout: 5000 },
31
+ (res) => {
32
+ let body = '';
33
+ res.on('data', (chunk) => (body += chunk));
34
+ res.on('end', () => {
35
+ try {
36
+ const data = JSON.parse(body);
37
+ resolve(data.version);
38
+ } catch {
39
+ reject(new Error('Invalid response'));
40
+ }
41
+ });
42
+ }
43
+ );
44
+ req.on('error', reject);
45
+ req.on('timeout', () => {
46
+ req.destroy();
47
+ reject(new Error('Timeout'));
48
+ });
49
+ });
50
+ }
51
+
52
+ async function checkUpdate() {
53
+ try {
54
+ // 检查缓存是否有效
55
+ const cache = getCache();
56
+ if (cache && (Date.now() - cache.checkedAt) < CHECK_INTERVAL) {
57
+ return; // 缓存未过期,跳过
58
+ }
59
+
60
+ const pkg = require('../package.json');
61
+ const latest = await npmLatest(pkg.name);
62
+
63
+ // 更新缓存
64
+ setCache({ checkedAt: Date.now(), latest });
65
+
66
+ // 版本对比:去掉 v 前缀,语义化版本逐段对比
67
+ const current = pkg.version.replace(/^v/, '');
68
+ const latestVer = latest.replace(/^v/, '');
69
+
70
+ if (current !== latestVer) {
71
+ const parts = {
72
+ cur: current.split('.').map(Number),
73
+ lat: latestVer.split('.').map(Number),
74
+ };
75
+
76
+ const level =
77
+ parts.lat[0] > parts.cur[0] ? 'major'
78
+ : parts.lat[1] > parts.cur[1] ? 'minor'
79
+ : 'patch';
80
+
81
+ console.log('');
82
+ console.log(
83
+ chalk.yellow('╔══════════════════════════════════════════════════════════╗')
84
+ );
85
+ console.log(
86
+ chalk.yellow('║ 新版可用!') +
87
+ chalk.white(` ${pkg.name} `) +
88
+ chalk.green(`v${latestVer}`) +
89
+ chalk.white(`(当前 v${current})`) +
90
+ chalk.yellow(' ║')
91
+ );
92
+ console.log(
93
+ chalk.yellow('║ 运行 ') +
94
+ chalk.cyan('npm update -g ' + pkg.name) +
95
+ chalk.yellow(' 更新到最新版本 ║')
96
+ );
97
+ console.log(
98
+ chalk.yellow('╚══════════════════════════════════════════════════════════╝')
99
+ );
100
+ console.log('');
101
+ }
102
+ } catch {
103
+ // 网络错误等静默跳过,不影响正常使用
104
+ }
105
+ }
106
+
107
+ module.exports = { checkUpdate };