rushangle-cli 0.1.0 → 0.1.2

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 CHANGED
@@ -16,7 +16,8 @@ rushangle whoami # 查看当前用户
16
16
  rushangle publish # 发布技能到市场
17
17
  rushangle list --type skills # 浏览技能列表
18
18
  rushangle search <关键词> # 全局搜索
19
- rushangle install <技能名> # 安装技能
19
+ rushangle install <技能名> # 安装技能(含本地文件)
20
+ rushangle install <技能名> --register-only # 仅注册到服务器(适用于受限环境)
20
21
  ```
21
22
 
22
23
  ## 命令
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rushangle-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "SkillHub CLI - 数智凯航技能市场命令行工具",
5
5
  "bin": {
6
6
  "rushangle": "./bin/rushangle.js"
@@ -18,9 +18,7 @@
18
18
  "dependencies": {
19
19
  "commander": "^12.0.0",
20
20
  "chalk": "^4.1.2",
21
- "ora": "^8.0.0",
22
21
  "open": "^10.0.0",
23
- "conf": "^12.0.0",
24
22
  "cli-table3": "^0.6.5"
25
23
  }
26
24
  }
@@ -13,6 +13,7 @@ module.exports = new Command('install')
13
13
  .argument('[skill]', '技能名称或 ID')
14
14
  .option('-t, --type <type>', '类型: skill | mcp | code', 'skill')
15
15
  .option('-g, --global', '全局安装(默认)', true)
16
+ .option('--register-only', '仅注册到服务器,不写入本地文件(适用于受限环境)')
16
17
  .action(async (nameOrId, opts) => {
17
18
  const token = auth.getToken();
18
19
  if (!token) {
@@ -51,26 +52,35 @@ module.exports = new Command('install')
51
52
  return;
52
53
  }
53
54
 
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
55
+ // Register install on server FIRST (survives EPERM in sandbox)
70
56
  await api.installSkill(item.id);
71
57
 
72
- console.log(chalk.green(`✓ 安装成功: ${item.name}@${item.version}`));
73
- console.log(chalk.gray(` 目录: ${installDir}`));
58
+ // Local file install (best-effort, skip in restricted environments)
59
+ if (!opts.registerOnly) {
60
+ try {
61
+ const installDir = path.join(INSTALL_DIR, 'skills', item.id);
62
+ fs.mkdirSync(installDir, { recursive: true });
63
+ fs.writeFileSync(
64
+ path.join(installDir, 'rushangle.json'),
65
+ JSON.stringify({ type: 'skill', ...item, installedAt: new Date().toISOString() }, null, 2)
66
+ );
67
+ if (item.readme) {
68
+ fs.writeFileSync(path.join(installDir, 'README.md'), item.readme);
69
+ }
70
+ console.log(chalk.green(`✓ 安装成功: ${item.name}@${item.version}`));
71
+ console.log(chalk.gray(` 本地目录: ${installDir}`));
72
+ } catch (fileErr) {
73
+ // EPERM or other file errors in sandbox — server registration already succeeded
74
+ console.log(chalk.green(`✓ 服务器注册成功: ${item.name}@${item.version}`));
75
+ console.log(chalk.yellow(` 本地文件写入跳过(${fileErr.code || fileErr.message})`));
76
+ if (fileErr.code === 'EPERM' || fileErr.code === 'EACCES') {
77
+ console.log(chalk.gray(` 提示: 使用 --register-only 跳过本地文件操作`));
78
+ }
79
+ }
80
+ } else {
81
+ console.log(chalk.green(`✓ 服务器注册成功: ${item.name}@${item.version}`));
82
+ console.log(chalk.gray(` 已跳过本地文件写入(--register-only)`));
83
+ }
74
84
 
75
85
  } else if (type === 'mcp') {
76
86
  try {
@@ -90,34 +100,41 @@ module.exports = new Command('install')
90
100
  return;
91
101
  }
92
102
 
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);
103
+ // Local file install (best-effort)
104
+ let showLocalPath = false;
105
+ let installDir = '';
106
+ if (!opts.registerOnly) {
107
+ try {
108
+ installDir = path.join(INSTALL_DIR, 'mcp', item.id);
109
+ fs.mkdirSync(installDir, { recursive: true });
110
+ if (item.code) {
111
+ const ext = item.language === 'python' ? '.py'
112
+ : item.language === 'javascript' ? '.js'
113
+ : item.language === 'typescript' ? '.ts'
114
+ : item.language === 'json' ? '.json'
115
+ : '.txt';
116
+ fs.writeFileSync(path.join(installDir, `server${ext}`), item.code);
117
+ }
118
+ if (item.config) {
119
+ const configStr = typeof item.config === 'string'
120
+ ? item.config : JSON.stringify(item.config, null, 2);
121
+ fs.writeFileSync(path.join(installDir, 'mcp.json'), configStr);
122
+ }
123
+ fs.writeFileSync(
124
+ path.join(installDir, 'rushangle.json'),
125
+ JSON.stringify({ type: 'mcp', ...item, installedAt: new Date().toISOString() }, null, 2)
126
+ );
127
+ showLocalPath = true;
128
+ } catch (fileErr) {
129
+ console.log(chalk.yellow(` 本地文件写入跳过(${fileErr.code || fileErr.message})`));
130
+ if (fileErr.code === 'EPERM' || fileErr.code === 'EACCES') {
131
+ console.log(chalk.gray(` 提示: 使用 --register-only 跳过本地文件操作`));
132
+ }
133
+ }
111
134
  }
112
135
 
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
136
  console.log(chalk.green(`✓ 安装成功: ${item.name}@${item.version}`));
120
- console.log(chalk.gray(` 目录: ${installDir}`));
137
+ if (showLocalPath) console.log(chalk.gray(` 本地目录: ${installDir}`));
121
138
 
122
139
  } else if (type === 'code') {
123
140
  try {
@@ -137,25 +154,34 @@ module.exports = new Command('install')
137
154
  return;
138
155
  }
139
156
 
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);
157
+ // Local file install (best-effort)
158
+ let showLocalPath = false;
159
+ let installDir = '';
160
+ if (!opts.registerOnly) {
161
+ try {
162
+ installDir = path.join(INSTALL_DIR, 'code', item.id);
163
+ fs.mkdirSync(installDir, { recursive: true });
164
+ if (item.code) {
165
+ 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' };
166
+ const ext = extMap[item.language] || `.${item.language}` || '.txt';
167
+ fs.writeFileSync(path.join(installDir, `snippet${ext}`), item.code);
168
+ }
169
+ fs.writeFileSync(
170
+ path.join(installDir, 'rushangle.json'),
171
+ JSON.stringify({ type: 'code', ...item, installedAt: new Date().toISOString() }, null, 2)
172
+ );
173
+ showLocalPath = true;
174
+ } catch (fileErr) {
175
+ console.log(chalk.yellow(` 本地文件写入跳过(${fileErr.code || fileErr.message})`));
176
+ if (fileErr.code === 'EPERM' || fileErr.code === 'EACCES') {
177
+ console.log(chalk.gray(` 提示: 使用 --register-only 跳过本地文件操作`));
178
+ }
179
+ }
148
180
  }
149
181
 
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
182
  console.log(chalk.green(`✓ 安装成功: ${item.name}`));
157
183
  console.log(chalk.gray(` 语言: ${item.language}`));
158
- console.log(chalk.gray(` 目录: ${installDir}`));
184
+ if (showLocalPath) console.log(chalk.gray(` 本地目录: ${installDir}`));
159
185
 
160
186
  } else {
161
187
  console.log(chalk.red(`不支持的类型: ${type}`));
@@ -1,6 +1,5 @@
1
1
  const { Command } = require('commander');
2
2
  const chalk = require('chalk');
3
- const open = require('open');
4
3
  const http = require('http');
5
4
  const auth = require('../auth');
6
5
  const { SITE_URL } = require('../api');
@@ -65,7 +64,12 @@ p{color:#666;margin:4px 0}
65
64
 
66
65
  // Open browser to DingTalk OAuth login with CLI callback params
67
66
  const loginUrl = `${SITE_URL}/api/auth/dingtalk/login?cli=1&cli_port=${CLI_PORT}`;
68
- await open(loginUrl);
67
+ try {
68
+ const open = (await import('open')).default;
69
+ await open(loginUrl);
70
+ } catch {
71
+ // open module unavailable, fall through to manual URL prompt
72
+ }
69
73
  console.log(chalk.gray('请在浏览器中完成钉钉授权...'));
70
74
  console.log(chalk.yellow('等待授权完成... (如浏览器未打开,请手动访问: ' + loginUrl + ')'));
71
75
  });