ribo-cli-setup 2.0.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 +74 -0
- package/bin/cli.js +352 -0
- package/package.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# ribo-cli-setup npx 一键安装器
|
|
2
|
+
|
|
3
|
+
员工无需手动下载二进制、无需 sudo,一条命令完成「检测平台 → 从网关下载对应二进制 → 接入 WorkBuddy/千问办公 → 实名授权」。
|
|
4
|
+
|
|
5
|
+
## 用法
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 最简:自动检测平台,从默认网关下载并接入 WorkBuddy
|
|
9
|
+
npx ribo-cli-setup
|
|
10
|
+
|
|
11
|
+
# 接入千问办公
|
|
12
|
+
npx ribo-cli-setup --qwenwork
|
|
13
|
+
|
|
14
|
+
# 指定内网网关地址(下载源 + CLI 运行地址)
|
|
15
|
+
npx ribo-cli-setup --gateway https://gw.ribo.internal
|
|
16
|
+
|
|
17
|
+
# 组合示例
|
|
18
|
+
npx ribo-cli-setup --gateway https://gw.ribo.internal --target qwenwork --bin-dir ~/bin
|
|
19
|
+
|
|
20
|
+
# 仅下载二进制,不自动授权
|
|
21
|
+
npx ribo-cli-setup --skip-auth
|
|
22
|
+
|
|
23
|
+
# 解除注册
|
|
24
|
+
npx ribo-cli-setup --uninstall --target workbuddy
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
也可用环境变量代替 `--gateway`:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
RIBO_GATEWAY_URL=https://gw.ribo.internal npx ribo-cli-setup
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 工作原理
|
|
34
|
+
|
|
35
|
+
1. **平台检测**:`process.platform`(darwin/linux/win32)+ `process.arch`(arm64/x64→amd64)
|
|
36
|
+
2. **拉取清单**:`GET <gateway>/download/manifest` → 各平台文件名、大小、SHA256、最新版本
|
|
37
|
+
3. **下载校验**:`GET <gateway>/download/<file>` 流式下载到 `~/.local/bin/ribo-cli`(Windows 为 `%LOCALAPPDATA%\ribo-cli-setup\ribo-cli.exe`),逐块计算 SHA256 并与清单比对,损坏即报错
|
|
38
|
+
4. **配置网关**:`ribo-cli config set-gateway <url>`
|
|
39
|
+
5. **接入客户端**:`ribo-cli install --workbuddy|--qwenwork`(写 mcp.json + 装 Skill 包)
|
|
40
|
+
6. **实名授权**:交互确认后执行 `ribo-cli auth init`(企微扫码/mock 确认页)
|
|
41
|
+
|
|
42
|
+
> 二进制由网关 `/download/**` 端点分发(见 `gateway/.../api/DistController.java`),文件名白名单 + 目录穿越防护,公开可下载(运行仍需实名授权)。
|
|
43
|
+
|
|
44
|
+
## 选项
|
|
45
|
+
|
|
46
|
+
| 选项 | 说明 |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `-g, --gateway <url>` | 网关地址,默认 `https://aigc.ribo-group.com/asushiye` |
|
|
49
|
+
| `-t, --target <name>` | `workbuddy` \| `qwenwork` |
|
|
50
|
+
| `--workbuddy` / `--qwenwork` | target 的快捷写法 |
|
|
51
|
+
| `--bin-dir <dir>` | 二进制安装目录,默认 `~/.local/bin` |
|
|
52
|
+
| `-f, --force` | 忽略本地已有版本,强制重新下载 |
|
|
53
|
+
| `--skip-auth` | 安装后不自动执行 auth init |
|
|
54
|
+
| `--uninstall` | 解除注册 |
|
|
55
|
+
| `-y, --yes` | 跳过确认 |
|
|
56
|
+
|
|
57
|
+
## 发布到 npm
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
cd npx
|
|
61
|
+
npm login
|
|
62
|
+
npm publish # 包名 ribo-cli-setup,bin 入口 bin/cli.js
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
发布后全公司即可 `npx ribo-cli-setup` 一键接入。若用私有 registry,员工侧加 `--registry` 或配置 `.npmrc`。
|
|
66
|
+
|
|
67
|
+
## 本地测试(无需发布)
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# 在项目根目录,用临时 HOME 隔离测试,不影响真实配置
|
|
71
|
+
HOME=/tmp/ribo-test node npx/bin/cli.js \
|
|
72
|
+
--gateway http://127.0.0.1:8081 --target qwenwork \
|
|
73
|
+
--bin-dir /tmp/ribo-test/bin --skip-auth --yes
|
|
74
|
+
```
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ribo-cli-setup 一键安装器(npx)
|
|
4
|
+
*
|
|
5
|
+
* 用法:
|
|
6
|
+
* npx ribo-cli-setup # 自动检测平台,从默认网关下载并接入 WorkBuddy
|
|
7
|
+
* npx ribo-cli-setup --qwenwork # 接入千问办公
|
|
8
|
+
* npx ribo-cli-setup --gateway <url> # 指定网关地址(下载源 + CLI 运行地址)
|
|
9
|
+
* npx ribo-cli-setup --target workbuddy|qwenwork
|
|
10
|
+
* npx ribo-cli-setup --force # 忽略本地已存在版本,强制重新下载
|
|
11
|
+
* npx ribo-cli-setup --uninstall # 解除注册
|
|
12
|
+
*
|
|
13
|
+
* 环境变量:RIBO_GATEWAY_URL 可替代 --gateway
|
|
14
|
+
* 流程:检测 os/arch → 拉 /download/manifest → 匹配平台二进制 → 下载校验 sha256
|
|
15
|
+
* → 安装到 ~/.local/bin(或 --bin-dir)→ 调 ribo-cli install / auth init
|
|
16
|
+
*/
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const https = require('https');
|
|
23
|
+
const http = require('http');
|
|
24
|
+
const crypto = require('crypto');
|
|
25
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
26
|
+
|
|
27
|
+
const DEFAULT_GATEWAY = 'https://aigc.ribo-group.com/asushiye';
|
|
28
|
+
const BIN_NAME = process.platform === 'win32' ? 'ribo-cli.exe' : 'ribo-cli';
|
|
29
|
+
|
|
30
|
+
// ---------- 参数解析 ----------
|
|
31
|
+
function parseArgs(argv) {
|
|
32
|
+
const opts = {
|
|
33
|
+
gateway: process.env.RIBO_GATEWAY_URL || DEFAULT_GATEWAY,
|
|
34
|
+
target: null,
|
|
35
|
+
binDir: null,
|
|
36
|
+
force: false,
|
|
37
|
+
uninstall: false,
|
|
38
|
+
skipAuth: false,
|
|
39
|
+
yes: false,
|
|
40
|
+
help: false,
|
|
41
|
+
};
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
const a = argv[i];
|
|
44
|
+
switch (a) {
|
|
45
|
+
case '--gateway': case '-g': opts.gateway = argv[++i]; break;
|
|
46
|
+
case '--target': case '-t': opts.target = argv[++i]; break;
|
|
47
|
+
case '--bin-dir': opts.binDir = argv[++i]; break;
|
|
48
|
+
case '--workbuddy': opts.target = 'workbuddy'; break;
|
|
49
|
+
case '--qwenwork': opts.target = 'qwenwork'; break;
|
|
50
|
+
case '--force': case '-f': opts.force = true; break;
|
|
51
|
+
case '--uninstall': opts.uninstall = true; break;
|
|
52
|
+
case '--skip-auth': opts.skipAuth = true; break;
|
|
53
|
+
case '--yes': case '-y': opts.yes = true; break;
|
|
54
|
+
case '--help': case '-h': opts.help = true; break;
|
|
55
|
+
default:
|
|
56
|
+
console.error(`未知参数: ${a}(用 --help 查看用法)`);
|
|
57
|
+
process.exit(2);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
opts.gateway = opts.gateway.replace(/\/+$/, '');
|
|
61
|
+
if (opts.target && !['workbuddy', 'qwenwork'].includes(opts.target)) {
|
|
62
|
+
console.error(`--target 仅支持 workbuddy 或 qwenwork`);
|
|
63
|
+
process.exit(2);
|
|
64
|
+
}
|
|
65
|
+
return opts;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function printHelp() {
|
|
69
|
+
console.log(`ribo-cli-setup 一键安装器
|
|
70
|
+
|
|
71
|
+
用法:
|
|
72
|
+
npx ribo-cli-setup [选项]
|
|
73
|
+
|
|
74
|
+
选项:
|
|
75
|
+
-g, --gateway <url> 网关地址(下载源+运行地址),默认 ${DEFAULT_GATEWAY}
|
|
76
|
+
也可用环境变量 RIBO_GATEWAY_URL
|
|
77
|
+
-t, --target <name> 接入目标: workbuddy | qwenwork(默认交互选择/workbuddy)
|
|
78
|
+
--workbuddy 等价于 --target workbuddy
|
|
79
|
+
--qwenwork 等价于 --target qwenwork
|
|
80
|
+
--bin-dir <dir> 二进制安装目录(默认 ~/.local/bin)
|
|
81
|
+
-f, --force 忽略本地已有版本,强制重新下载
|
|
82
|
+
--skip-auth 安装后不自动执行 auth init
|
|
83
|
+
--uninstall 解除注册(移除 mcp.json 条目与 Skill 包)
|
|
84
|
+
-y, --yes 跳过所有确认
|
|
85
|
+
-h, --help 显示本帮助`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------- 平台检测 ----------
|
|
89
|
+
function detectPlatform() {
|
|
90
|
+
const p = process.platform; // darwin | linux | win32
|
|
91
|
+
const a = process.arch; // arm64 | x64
|
|
92
|
+
const osName = p === 'darwin' ? 'darwin' : p === 'linux' ? 'linux' : p === 'win32' ? 'windows' : null;
|
|
93
|
+
const archName = a === 'arm64' ? 'arm64' : a === 'x64' ? 'amd64' : null;
|
|
94
|
+
if (!osName || !archName) {
|
|
95
|
+
console.error(`不支持的平台: ${p}/${a}(仅支持 darwin|linux|windows × amd64|arm64)`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
return { osName, archName, isWindows: p === 'win32' };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------- HTTP ----------
|
|
102
|
+
function httpGet(url, redirect = 0) {
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
if (redirect > 5) return reject(new Error('重定向次数过多'));
|
|
105
|
+
const mod = url.startsWith('https:') ? https : http;
|
|
106
|
+
const req = mod.get(url, { timeout: 30000 }, (res) => {
|
|
107
|
+
const loc = res.headers.location;
|
|
108
|
+
if ([301, 302, 303, 307, 308].includes(res.statusCode) && loc) {
|
|
109
|
+
res.resume();
|
|
110
|
+
const next = new URL(loc, url).toString();
|
|
111
|
+
return httpGet(next, redirect + 1).then(resolve, reject);
|
|
112
|
+
}
|
|
113
|
+
if (res.statusCode !== 200) {
|
|
114
|
+
let body = '';
|
|
115
|
+
res.on('data', (d) => (body += d));
|
|
116
|
+
res.on('end', () => reject(new Error(`HTTP ${res.statusCode}: ${body.slice(0, 200)}`)));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
resolve(res);
|
|
120
|
+
});
|
|
121
|
+
req.on('error', reject);
|
|
122
|
+
req.on('timeout', () => req.destroy(new Error('请求超时')));
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function getJSON(url) {
|
|
127
|
+
const res = await httpGet(url);
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
let body = '';
|
|
130
|
+
res.on('data', (d) => (body += d));
|
|
131
|
+
res.on('end', () => {
|
|
132
|
+
try { resolve(JSON.parse(body)); }
|
|
133
|
+
catch (e) { reject(new Error(`响应非 JSON(${url}): ${body.slice(0, 120)}`)); }
|
|
134
|
+
});
|
|
135
|
+
res.on('error', reject);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function downloadFile(url, destPath, expectedSha, expectedSize) {
|
|
140
|
+
const res = await httpGet(url);
|
|
141
|
+
const tmp = destPath + '.download';
|
|
142
|
+
await new Promise((resolve, reject) => {
|
|
143
|
+
const ws = fs.createWriteStream(tmp);
|
|
144
|
+
const hash = crypto.createHash('sha256');
|
|
145
|
+
let received = 0;
|
|
146
|
+
let lastPct = -10;
|
|
147
|
+
res.on('data', (d) => {
|
|
148
|
+
received += d.length;
|
|
149
|
+
hash.update(d);
|
|
150
|
+
if (expectedSize) {
|
|
151
|
+
const pct = Math.floor((received / expectedSize) * 100);
|
|
152
|
+
if (pct - lastPct >= 10) { process.stdout.write(`\r 下载中… ${pct}%`); lastPct = pct; }
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
res.pipe(ws);
|
|
156
|
+
ws.on('finish', () => {
|
|
157
|
+
if (expectedSize) process.stdout.write('\r 下载中… 100%\n');
|
|
158
|
+
ws.close(() => {
|
|
159
|
+
const actual = hash.digest('hex');
|
|
160
|
+
const headerSha = res.headers['x-ribo-sha256'];
|
|
161
|
+
const sha = expectedSha || headerSha;
|
|
162
|
+
if (sha && actual !== sha) {
|
|
163
|
+
fs.unlink(tmp, () => {});
|
|
164
|
+
return reject(new Error(`SHA256 校验失败\n 期望: ${sha}\n 实际: ${actual}`));
|
|
165
|
+
}
|
|
166
|
+
resolve();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
ws.on('error', reject);
|
|
170
|
+
res.on('error', reject);
|
|
171
|
+
});
|
|
172
|
+
fs.renameSync(tmp, destPath);
|
|
173
|
+
if (process.platform !== 'win32') fs.chmodSync(destPath, 0o755);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---------- 安装目录 ----------
|
|
177
|
+
function resolveBinDir(opts) {
|
|
178
|
+
if (opts.binDir) return path.resolve(opts.binDir);
|
|
179
|
+
if (process.platform === 'win32') {
|
|
180
|
+
const base = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
181
|
+
return path.join(base, 'ribo-cli-setup');
|
|
182
|
+
}
|
|
183
|
+
return path.join(os.homedir(), '.local', 'bin');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function localVersion(binPath) {
|
|
187
|
+
try {
|
|
188
|
+
const out = execFileSync(binPath, ['version'], { encoding: 'utf8', timeout: 8000 });
|
|
189
|
+
const m = out.match(/(\d+\.\d+\.\d+)/);
|
|
190
|
+
return m ? m[1] : null;
|
|
191
|
+
} catch { return null; }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---------- 交互确认 ----------
|
|
195
|
+
function confirm(question, assumeYes) {
|
|
196
|
+
if (assumeYes) return Promise.resolve(true);
|
|
197
|
+
return new Promise((resolve) => {
|
|
198
|
+
process.stdout.write(`${question} [y/N] `);
|
|
199
|
+
process.stdin.once('data', (d) => resolve(/^y/i.test(d.toString().trim())));
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------- 主流程 ----------
|
|
204
|
+
async function main() {
|
|
205
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
206
|
+
if (opts.help) { printHelp(); return; }
|
|
207
|
+
|
|
208
|
+
const plat = detectPlatform();
|
|
209
|
+
const binDir = resolveBinDir(opts);
|
|
210
|
+
const binPath = path.join(binDir, BIN_NAME);
|
|
211
|
+
|
|
212
|
+
console.log('┌─────────────────────────────────────────────┐');
|
|
213
|
+
console.log('│ ribo-cli-setup 一键安装器 │');
|
|
214
|
+
console.log('└─────────────────────────────────────────────┘');
|
|
215
|
+
console.log(` 平台: ${plat.osName}/${plat.archName}`);
|
|
216
|
+
console.log(` 网关: ${opts.gateway}`);
|
|
217
|
+
console.log(` 安装目录: ${binDir}`);
|
|
218
|
+
console.log();
|
|
219
|
+
|
|
220
|
+
// 拉取清单
|
|
221
|
+
let manifest;
|
|
222
|
+
try {
|
|
223
|
+
manifest = await getJSON(`${opts.gateway}/download/manifest`);
|
|
224
|
+
} catch (e) {
|
|
225
|
+
console.error(`✗ 无法从网关获取版本清单: ${e.message}`);
|
|
226
|
+
console.error(` 请确认网关已启动且 /download 可访问(--gateway 指定正确地址)`);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const target = manifest.files.find(
|
|
231
|
+
(f) => f.os === plat.osName && f.arch === plat.archName && f.version === manifest.latest_version
|
|
232
|
+
) || manifest.files.find((f) => f.os === plat.osName && f.arch === plat.archName);
|
|
233
|
+
|
|
234
|
+
if (!target) {
|
|
235
|
+
console.error(`✗ 网关未提供 ${plat.osName}/${plat.archName} 平台的二进制`);
|
|
236
|
+
console.error(` 可用: ${manifest.files.map((f) => `${f.os}/${f.arch}@${f.version}`).join(', ')}`);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 版本比较:本地已是最新且未 --force 则跳过下载
|
|
241
|
+
const existing = localVersion(binPath);
|
|
242
|
+
let needDownload = opts.force || existing !== target.version;
|
|
243
|
+
if (existing === target.version && !opts.force) {
|
|
244
|
+
console.log(`· 本地已是最新版本 ${existing},跳过下载(--force 可强制重装)`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 卸载分支
|
|
248
|
+
if (opts.uninstall) {
|
|
249
|
+
const tgt = opts.target || (await pickTarget(opts));
|
|
250
|
+
if (!tgt) { console.error('未指定接入目标,已取消'); process.exit(1); }
|
|
251
|
+
const flag = tgt === 'qwenwork' ? '--qwenwork' : '--workbuddy';
|
|
252
|
+
if (!fs.existsSync(binPath)) { console.error(`✗ 未找到 ${binPath},无法解除注册`); process.exit(1); }
|
|
253
|
+
console.log(`→ 解除 ${tgt} 注册…`);
|
|
254
|
+
run(binPath, ['install', flag, '--uninstall']);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (needDownload) {
|
|
259
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
260
|
+
const url = `${opts.gateway}/download/${target.file}`;
|
|
261
|
+
console.log(`→ 下载 ${target.file} (${(target.size / 1024 / 1024).toFixed(1)} MB)…`);
|
|
262
|
+
try {
|
|
263
|
+
await downloadFile(url, binPath, target.sha256, target.size);
|
|
264
|
+
} catch (e) {
|
|
265
|
+
console.error(`\n✗ 下载失败: ${e.message}`);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
console.log(`✓ 已安装 ${BIN_NAME} ${target.version} → ${binPath}`);
|
|
269
|
+
// 校验 sha256(下载函数已校验,这里再确认本地文件)
|
|
270
|
+
const diskSha = crypto.createHash('sha256').update(fs.readFileSync(binPath)).digest('hex');
|
|
271
|
+
if (target.sha256 && diskSha !== target.sha256) {
|
|
272
|
+
console.error(`✗ 本地文件 SHA256 不匹配,已损坏`);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
console.log(`· 使用本地二进制 ${binPath}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 网关地址写入 CLI 配置
|
|
280
|
+
console.log('→ 配置网关地址…');
|
|
281
|
+
run(binPath, ['config', 'set-gateway', opts.gateway]);
|
|
282
|
+
|
|
283
|
+
// 选择接入目标
|
|
284
|
+
const tgt = opts.target || (await pickTarget(opts));
|
|
285
|
+
if (!tgt) { console.error('未指定接入目标,已取消(仅完成二进制安装)'); notePathIfMissing(binDir); return; }
|
|
286
|
+
const flag = tgt === 'qwenwork' ? '--qwenwork' : '--workbuddy';
|
|
287
|
+
|
|
288
|
+
console.log(`→ 接入 ${tgt === 'qwenwork' ? '千问办公' : 'WorkBuddy'}…`);
|
|
289
|
+
run(binPath, ['install', flag]);
|
|
290
|
+
|
|
291
|
+
// 实名授权
|
|
292
|
+
if (!opts.skipAuth) {
|
|
293
|
+
const doAuth = opts.yes || (await confirm('是否现在执行实名授权(ribo-cli auth init)?', false));
|
|
294
|
+
if (doAuth) {
|
|
295
|
+
console.log('→ 发起实名授权(将打开浏览器/企微扫码,完成后回到终端)…');
|
|
296
|
+
runInteractive(binPath, ['auth', 'init']);
|
|
297
|
+
} else {
|
|
298
|
+
console.log('· 已跳过授权,稍后可手动执行: ribo-cli auth init');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
notePathIfMissing(binDir);
|
|
303
|
+
console.log();
|
|
304
|
+
console.log('✓ 全部完成。');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 选择接入目标(无 --target 时交互;非 TTY 默认 workbuddy)
|
|
308
|
+
async function pickTarget(opts) {
|
|
309
|
+
if (!process.stdin.isTTY || opts.yes) return 'workbuddy';
|
|
310
|
+
return new Promise((resolve) => {
|
|
311
|
+
console.log('请选择接入目标:');
|
|
312
|
+
console.log(' 1) WorkBuddy (~/.workbuddy)');
|
|
313
|
+
console.log(' 2) 千问办公 (~/.qwenworkcn)');
|
|
314
|
+
process.stdout.write('输入 1 或 2 [默认 1]: ');
|
|
315
|
+
process.stdin.once('data', (d) => {
|
|
316
|
+
const v = d.toString().trim();
|
|
317
|
+
resolve(v === '2' ? 'qwenwork' : 'workbuddy');
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function run(bin, args) {
|
|
323
|
+
const r = spawnSync(bin, args, { stdio: 'inherit' });
|
|
324
|
+
if (r.status !== 0 && r.status !== null) {
|
|
325
|
+
// install/config 非致命错误也继续(如已注册)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function runInteractive(bin, args) {
|
|
329
|
+
try { execFileSync(bin, args, { stdio: 'inherit' }); }
|
|
330
|
+
catch { /* auth init 可能因用户取消而非零退出 */ }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 若 binDir 不在 PATH,给出提示
|
|
334
|
+
function notePathIfMissing(binDir) {
|
|
335
|
+
const p = (process.env.PATH || '').split(path.delimiter);
|
|
336
|
+
if (!p.includes(binDir)) {
|
|
337
|
+
console.log();
|
|
338
|
+
console.log(`⚠ ${binDir} 不在 PATH 中,请手动加入:`);
|
|
339
|
+
if (process.platform === 'win32') {
|
|
340
|
+
console.log(` setx PATH "%PATH%;${binDir}"`);
|
|
341
|
+
} else {
|
|
342
|
+
const shell = process.env.SHELL || '/bin/bash';
|
|
343
|
+
const rc = shell.includes('zsh') ? '~/.zshrc' : '~/.bashrc';
|
|
344
|
+
console.log(` echo 'export PATH="${binDir}:$PATH"' >> ${rc} && source ${rc}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
main().catch((e) => {
|
|
350
|
+
console.error(`\n✗ 安装失败: ${e.message}`);
|
|
351
|
+
process.exit(1);
|
|
352
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ribo-cli-setup",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "ribo-cli 一键安装器:检测操作系统与 CPU 架构,从 ribo 网关下载对应平台 CLI 二进制,完成 WorkBuddy / 千问办公接入与实名授权",
|
|
5
|
+
"bin": {
|
|
6
|
+
"ribo-cli-setup": "bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/"
|
|
10
|
+
],
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=16"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"ribo",
|
|
16
|
+
"erp",
|
|
17
|
+
"mcp",
|
|
18
|
+
"cli"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"registry": "https://registry.npmjs.org/"
|
|
22
|
+
},
|
|
23
|
+
"license": "UNLICENSED",
|
|
24
|
+
"private": false
|
|
25
|
+
}
|