@liuzijian625/code-cli 1.0.6 → 1.0.8
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/bin/cli.js +276 -48
- package/lib/config.js +64 -43
- package/lib/installer.js +42 -0
- package/lib/remote.js +250 -0
- package/package.json +8 -1
- package/remote-server/README.md +56 -0
- package/remote-server/go.mod +3 -0
- package/remote-server/main.go +1436 -0
- package/.claude/settings.local.json +0 -10
package/bin/cli.js
CHANGED
|
@@ -1,43 +1,53 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import inquirer from 'inquirer';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import { applyConfig, clearConfig } from '../lib/config.js';
|
|
6
|
-
import { addPreset, listPresets, deletePreset, getPresets } from '../lib/preset.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { applyConfig, clearConfig } from '../lib/config.js';
|
|
6
|
+
import { addPreset, listPresets, deletePreset, getPresets } from '../lib/preset.js';
|
|
7
|
+
import { clearRemotePassword, fetchRemotePresets, getRemoteSettings, setRemotePassword, setRemoteUrl } from '../lib/remote.js';
|
|
8
|
+
import { getInstallCommand, installTool } from '../lib/installer.js';
|
|
9
|
+
|
|
10
|
+
const TOOL_NAMES = { codex: 'Codex', claude: 'Claude Code', gemini: 'Gemini CLI' };
|
|
11
|
+
|
|
12
|
+
async function mainMenu() {
|
|
13
|
+
console.clear();
|
|
12
14
|
console.log(chalk.cyan.bold('\ncode-cli - AI CLI 配置管理工具\n'));
|
|
13
15
|
|
|
14
16
|
const { action } = await inquirer.prompt([{
|
|
15
17
|
type: 'list',
|
|
16
|
-
name: 'action',
|
|
17
|
-
message: '请选择操作:',
|
|
18
|
-
choices: [
|
|
19
|
-
{ name: '1. 应用配置', value: 'apply' },
|
|
20
|
-
{ name: '2. 管理预设', value: 'manage' },
|
|
21
|
-
{ name: '3. 删除配置', value: 'clear' },
|
|
22
|
-
{ name: '4.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
18
|
+
name: 'action',
|
|
19
|
+
message: '请选择操作:',
|
|
20
|
+
choices: [
|
|
21
|
+
{ name: '1. 应用配置', value: 'apply' },
|
|
22
|
+
{ name: '2. 管理预设', value: 'manage' },
|
|
23
|
+
{ name: '3. 删除配置', value: 'clear' },
|
|
24
|
+
{ name: '4. 安装工具', value: 'install' },
|
|
25
|
+
{ name: '5. 远程配置', value: 'remote' },
|
|
26
|
+
{ name: '6. 退出', value: 'exit' }
|
|
27
|
+
]
|
|
28
|
+
}]);
|
|
29
|
+
|
|
30
|
+
switch (action) {
|
|
31
|
+
case 'apply':
|
|
32
|
+
await applyConfigMenu();
|
|
33
|
+
break;
|
|
34
|
+
case 'manage':
|
|
35
|
+
await managePresetMenu();
|
|
36
|
+
break;
|
|
37
|
+
case 'clear':
|
|
38
|
+
await clearConfigMenu();
|
|
39
|
+
break;
|
|
40
|
+
case 'install':
|
|
41
|
+
await installToolsMenu();
|
|
42
|
+
break;
|
|
43
|
+
case 'remote':
|
|
44
|
+
await remoteConfigMenu();
|
|
45
|
+
break;
|
|
46
|
+
case 'exit':
|
|
47
|
+
console.log(chalk.green('再见!'));
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
41
51
|
|
|
42
52
|
async function selectTool(message, includeAll = false) {
|
|
43
53
|
const choices = [
|
|
@@ -195,20 +205,238 @@ async function deletePresetMenu() {
|
|
|
195
205
|
console.log(chalk.green(`\n预设 '${preset}' 已删除!`));
|
|
196
206
|
}
|
|
197
207
|
|
|
198
|
-
async function clearConfigMenu() {
|
|
199
|
-
const tool = await selectTool('选择要清除配置的工具:', true);
|
|
200
|
-
if (tool === 'back') return mainMenu();
|
|
201
|
-
|
|
202
|
-
await clearConfig(tool);
|
|
203
|
-
console.log(chalk.green(`\n${tool === 'all' ? '所有工具' : TOOL_NAMES[tool]} 的配置已清除`));
|
|
204
|
-
await pause();
|
|
205
|
-
return mainMenu();
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
async function
|
|
209
|
-
await
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
async function clearConfigMenu() {
|
|
209
|
+
const tool = await selectTool('选择要清除配置的工具:', true);
|
|
210
|
+
if (tool === 'back') return mainMenu();
|
|
211
|
+
|
|
212
|
+
await clearConfig(tool);
|
|
213
|
+
console.log(chalk.green(`\n${tool === 'all' ? '所有工具' : TOOL_NAMES[tool]} 的配置已清除`));
|
|
214
|
+
await pause();
|
|
215
|
+
return mainMenu();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function installToolsMenu() {
|
|
219
|
+
const tool = await selectTool('选择要安装的工具:', true);
|
|
220
|
+
if (tool === 'back') return mainMenu();
|
|
221
|
+
|
|
222
|
+
const tools = tool === 'all' ? ['codex', 'claude', 'gemini'] : [tool];
|
|
223
|
+
const commands = tools.map(t => getInstallCommand(t)?.display).filter(Boolean);
|
|
224
|
+
|
|
225
|
+
console.log(chalk.cyan('\n将执行以下命令:\n'));
|
|
226
|
+
commands.forEach((c) => console.log(` ${c}`));
|
|
227
|
+
|
|
228
|
+
const { confirm } = await inquirer.prompt([{
|
|
229
|
+
type: 'confirm',
|
|
230
|
+
name: 'confirm',
|
|
231
|
+
message: '\n确认开始安装?',
|
|
232
|
+
default: true
|
|
233
|
+
}]);
|
|
234
|
+
|
|
235
|
+
if (!confirm) return mainMenu();
|
|
236
|
+
|
|
237
|
+
for (const t of tools) {
|
|
238
|
+
console.log(chalk.cyan(`\n开始安装 ${TOOL_NAMES[t]}...`));
|
|
239
|
+
try {
|
|
240
|
+
await installTool(t);
|
|
241
|
+
console.log(chalk.green(`${TOOL_NAMES[t]} 安装完成`));
|
|
242
|
+
} catch (err) {
|
|
243
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
244
|
+
console.log(chalk.red(`${TOOL_NAMES[t]} 安装失败: ${message}`));
|
|
245
|
+
if (tool !== 'all') break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
await pause();
|
|
250
|
+
return mainMenu();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function ensureRemoteSettings() {
|
|
254
|
+
let { url, password } = getRemoteSettings();
|
|
255
|
+
|
|
256
|
+
if (!url) {
|
|
257
|
+
const ans = await inquirer.prompt([{
|
|
258
|
+
type: 'input',
|
|
259
|
+
name: 'url',
|
|
260
|
+
message: '请输入远程预设文件 URL:',
|
|
261
|
+
validate: (val) => val && val.trim() ? true : 'URL 不能为空'
|
|
262
|
+
}]);
|
|
263
|
+
url = ans.url.trim();
|
|
264
|
+
setRemoteUrl(url);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!password) {
|
|
268
|
+
const ans = await inquirer.prompt([{
|
|
269
|
+
type: 'password',
|
|
270
|
+
name: 'password',
|
|
271
|
+
message: '请输入远程密码:',
|
|
272
|
+
mask: '*',
|
|
273
|
+
validate: (val) => val && val.trim() ? true : '密码不能为空'
|
|
274
|
+
}, {
|
|
275
|
+
type: 'confirm',
|
|
276
|
+
name: 'remember',
|
|
277
|
+
message: '是否记住密码(仅保存到本机)?',
|
|
278
|
+
default: true
|
|
279
|
+
}]);
|
|
280
|
+
|
|
281
|
+
password = ans.password;
|
|
282
|
+
if (ans.remember) {
|
|
283
|
+
setRemotePassword(password);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return { url, password };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function printPresetsSummary(presets) {
|
|
291
|
+
let hasAny = false;
|
|
292
|
+
for (const tool of ['codex', 'claude', 'gemini']) {
|
|
293
|
+
const list = presets[tool] || [];
|
|
294
|
+
if (list.length === 0) continue;
|
|
295
|
+
hasAny = true;
|
|
296
|
+
console.log(chalk.cyan(`\n${TOOL_NAMES[tool]} 预设:`));
|
|
297
|
+
list.forEach((p, i) => {
|
|
298
|
+
const maskedKey = p.key.length > 6 ? p.key.substring(0, 6) + '***' : '***';
|
|
299
|
+
console.log(` ${i + 1}. ${chalk.bold(p.name)}`);
|
|
300
|
+
console.log(` URL: ${p.url}`);
|
|
301
|
+
console.log(` Key: ${maskedKey}`);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!hasAny) {
|
|
306
|
+
console.log(chalk.yellow('\n远程暂无预设'));
|
|
307
|
+
}
|
|
308
|
+
console.log('');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function applyRemoteConfigMenu() {
|
|
312
|
+
const { url, password } = await ensureRemoteSettings();
|
|
313
|
+
let presets;
|
|
314
|
+
try {
|
|
315
|
+
presets = await fetchRemotePresets(url, password);
|
|
316
|
+
} catch (err) {
|
|
317
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
318
|
+
console.log(chalk.red(`\n${message}`));
|
|
319
|
+
await pause();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const tool = await selectTool('选择要配置的工具 (远程):', true);
|
|
324
|
+
if (tool === 'back') return;
|
|
325
|
+
|
|
326
|
+
if (tool === 'all') {
|
|
327
|
+
for (const t of ['codex', 'claude', 'gemini']) {
|
|
328
|
+
const list = presets[t] || [];
|
|
329
|
+
if (list.length === 0) {
|
|
330
|
+
console.log(chalk.yellow(`\n${TOOL_NAMES[t]} 暂无远程预设,跳过`));
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const preset = await selectPreset(t, list);
|
|
334
|
+
if (preset) {
|
|
335
|
+
await applyConfig(t, preset);
|
|
336
|
+
console.log(chalk.green(`${TOOL_NAMES[t]} 配置已应用`));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
await pause();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const list = presets[tool] || [];
|
|
344
|
+
if (list.length === 0) {
|
|
345
|
+
console.log(chalk.yellow(`\n${TOOL_NAMES[tool]} 暂无远程预设`));
|
|
346
|
+
await pause();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const preset = await selectPreset(tool, list);
|
|
351
|
+
if (!preset) return;
|
|
352
|
+
|
|
353
|
+
await applyConfig(tool, preset);
|
|
354
|
+
console.log(chalk.green(`\n远程配置已应用到 ${TOOL_NAMES[tool]}`));
|
|
355
|
+
await pause();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function previewRemotePresetsMenu() {
|
|
359
|
+
const { url, password } = await ensureRemoteSettings();
|
|
360
|
+
try {
|
|
361
|
+
const presets = await fetchRemotePresets(url, password);
|
|
362
|
+
printPresetsSummary(presets);
|
|
363
|
+
} catch (err) {
|
|
364
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
365
|
+
console.log(chalk.red(`\n${message}`));
|
|
366
|
+
}
|
|
367
|
+
await pause();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function remoteConfigMenu() {
|
|
371
|
+
const settings = getRemoteSettings();
|
|
372
|
+
console.clear();
|
|
373
|
+
console.log(chalk.cyan.bold('\n远程配置\n'));
|
|
374
|
+
console.log(`URL: ${settings.url ? chalk.green(settings.url) : chalk.yellow('(未设置)')}`);
|
|
375
|
+
console.log(`密码: ${settings.password ? chalk.green('(已保存)') : chalk.yellow('(未保存)')}`);
|
|
376
|
+
|
|
377
|
+
const { action } = await inquirer.prompt([{
|
|
378
|
+
type: 'list',
|
|
379
|
+
name: 'action',
|
|
380
|
+
message: '选择操作:',
|
|
381
|
+
choices: [
|
|
382
|
+
{ name: '1. 设置远程 URL', value: 'set_url' },
|
|
383
|
+
{ name: '2. 设置/更新密码', value: 'set_password' },
|
|
384
|
+
{ name: '3. 清除已保存密码', value: 'clear_password' },
|
|
385
|
+
{ name: '4. 预览远程预设', value: 'preview' },
|
|
386
|
+
{ name: '5. 应用远程配置', value: 'apply' },
|
|
387
|
+
{ name: '6. 返回', value: 'back' }
|
|
388
|
+
]
|
|
389
|
+
}]);
|
|
390
|
+
|
|
391
|
+
switch (action) {
|
|
392
|
+
case 'set_url': {
|
|
393
|
+
const ans = await inquirer.prompt([{
|
|
394
|
+
type: 'input',
|
|
395
|
+
name: 'url',
|
|
396
|
+
message: '请输入远程预设文件 URL:',
|
|
397
|
+
default: settings.url || '',
|
|
398
|
+
validate: (val) => val && val.trim() ? true : 'URL 不能为空'
|
|
399
|
+
}]);
|
|
400
|
+
setRemoteUrl(ans.url.trim());
|
|
401
|
+
console.log(chalk.green('\n远程 URL 已保存'));
|
|
402
|
+
await pause();
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
case 'set_password': {
|
|
406
|
+
const ans = await inquirer.prompt([{
|
|
407
|
+
type: 'password',
|
|
408
|
+
name: 'password',
|
|
409
|
+
message: '请输入远程密码:',
|
|
410
|
+
mask: '*',
|
|
411
|
+
validate: (val) => val && val.trim() ? true : '密码不能为空'
|
|
412
|
+
}]);
|
|
413
|
+
setRemotePassword(ans.password);
|
|
414
|
+
console.log(chalk.green('\n远程密码已保存'));
|
|
415
|
+
await pause();
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
case 'clear_password':
|
|
419
|
+
clearRemotePassword();
|
|
420
|
+
console.log(chalk.green('\n已清除保存的密码'));
|
|
421
|
+
await pause();
|
|
422
|
+
break;
|
|
423
|
+
case 'preview':
|
|
424
|
+
await previewRemotePresetsMenu();
|
|
425
|
+
break;
|
|
426
|
+
case 'apply':
|
|
427
|
+
await applyRemoteConfigMenu();
|
|
428
|
+
break;
|
|
429
|
+
case 'back':
|
|
430
|
+
return mainMenu();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return remoteConfigMenu();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function pause() {
|
|
437
|
+
await inquirer.prompt([{
|
|
438
|
+
type: 'input',
|
|
439
|
+
name: 'continue',
|
|
212
440
|
message: '按回车继续...'
|
|
213
441
|
}]);
|
|
214
442
|
}
|
package/lib/config.js
CHANGED
|
@@ -5,13 +5,37 @@ import { parse, stringify } from '@iarna/toml';
|
|
|
5
5
|
import { exec } from 'child_process';
|
|
6
6
|
import { promisify } from 'util';
|
|
7
7
|
|
|
8
|
-
const execAsync = promisify(exec);
|
|
9
|
-
const isWindows = os.platform() === 'win32';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
const isWindows = os.platform() === 'win32';
|
|
10
|
+
const CLAUDE_JSON_FILE = path.join(os.homedir(), '.claude.json');
|
|
11
|
+
|
|
12
|
+
function readJsonFileOrEmpty(filePath) {
|
|
13
|
+
if (!fs.existsSync(filePath)) return {};
|
|
14
|
+
try {
|
|
15
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
16
|
+
if (!raw.trim()) return {};
|
|
17
|
+
const parsed = JSON.parse(raw);
|
|
18
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};
|
|
19
|
+
return parsed;
|
|
20
|
+
} catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function writeJsonFile(filePath, data) {
|
|
26
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function ensureClaudeOnboardingComplete() {
|
|
30
|
+
const config = readJsonFileOrEmpty(CLAUDE_JSON_FILE);
|
|
31
|
+
config.hasCompletedOnboarding = true;
|
|
32
|
+
writeJsonFile(CLAUDE_JSON_FILE, config);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Codex 配置
|
|
36
|
+
async function applyCodex(preset) {
|
|
37
|
+
const codexDir = path.join(os.homedir(), '.codex');
|
|
38
|
+
const configFile = path.join(codexDir, 'config.toml');
|
|
15
39
|
const authFile = path.join(codexDir, 'auth.json');
|
|
16
40
|
|
|
17
41
|
if (!fs.existsSync(codexDir)) {
|
|
@@ -49,24 +73,22 @@ async function applyCodex(preset) {
|
|
|
49
73
|
}
|
|
50
74
|
|
|
51
75
|
// Claude Code 配置
|
|
52
|
-
async function applyClaude(preset) {
|
|
53
|
-
if (isWindows) {
|
|
54
|
-
// Windows: 修改 ~/.claude.json
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (!fs.existsSync(claudeDir)) {
|
|
76
|
+
async function applyClaude(preset) {
|
|
77
|
+
if (isWindows) {
|
|
78
|
+
// Windows: 修改 ~/.claude.json
|
|
79
|
+
const config = readJsonFileOrEmpty(CLAUDE_JSON_FILE);
|
|
80
|
+
config.hasCompletedOnboarding = true;
|
|
81
|
+
if (!config.env || typeof config.env !== 'object' || Array.isArray(config.env)) config.env = {};
|
|
82
|
+
config.env.ANTHROPIC_BASE_URL = preset.url;
|
|
83
|
+
config.env.ANTHROPIC_AUTH_TOKEN = preset.key;
|
|
84
|
+
writeJsonFile(CLAUDE_JSON_FILE, config);
|
|
85
|
+
} else {
|
|
86
|
+
ensureClaudeOnboardingComplete();
|
|
87
|
+
// Linux/macOS: 修改 ~/.claude/settings.json
|
|
88
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
89
|
+
const settingsFile = path.join(claudeDir, 'settings.json');
|
|
90
|
+
|
|
91
|
+
if (!fs.existsSync(claudeDir)) {
|
|
70
92
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
71
93
|
}
|
|
72
94
|
|
|
@@ -129,24 +151,23 @@ async function clearCodex() {
|
|
|
129
151
|
}
|
|
130
152
|
}
|
|
131
153
|
|
|
132
|
-
async function clearClaude() {
|
|
133
|
-
if (isWindows) {
|
|
134
|
-
// Windows: 修改 ~/.claude.json
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
let settings = JSON.parse(fs.readFileSync(settingsFile, 'utf-8'));
|
|
154
|
+
async function clearClaude() {
|
|
155
|
+
if (isWindows) {
|
|
156
|
+
// Windows: 修改 ~/.claude.json
|
|
157
|
+
const config = readJsonFileOrEmpty(CLAUDE_JSON_FILE);
|
|
158
|
+
config.hasCompletedOnboarding = true;
|
|
159
|
+
if (config.env && typeof config.env === 'object' && !Array.isArray(config.env)) {
|
|
160
|
+
delete config.env.ANTHROPIC_BASE_URL;
|
|
161
|
+
delete config.env.ANTHROPIC_AUTH_TOKEN;
|
|
162
|
+
}
|
|
163
|
+
writeJsonFile(CLAUDE_JSON_FILE, config);
|
|
164
|
+
} else {
|
|
165
|
+
ensureClaudeOnboardingComplete();
|
|
166
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
167
|
+
const settingsFile = path.join(claudeDir, 'settings.json');
|
|
168
|
+
|
|
169
|
+
if (fs.existsSync(settingsFile)) {
|
|
170
|
+
let settings = JSON.parse(fs.readFileSync(settingsFile, 'utf-8'));
|
|
150
171
|
if (settings.env) {
|
|
151
172
|
delete settings.env.ANTHROPIC_BASE_URL;
|
|
152
173
|
delete settings.env.ANTHROPIC_AUTH_TOKEN;
|
package/lib/installer.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
|
|
3
|
+
const NPM_COMMAND = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
4
|
+
|
|
5
|
+
const INSTALL_COMMANDS = {
|
|
6
|
+
codex: {
|
|
7
|
+
display: 'npm i -g @openai/codex',
|
|
8
|
+
args: ['i', '-g', '@openai/codex']
|
|
9
|
+
},
|
|
10
|
+
gemini: {
|
|
11
|
+
display: 'npm install -g @google/gemini-cli',
|
|
12
|
+
args: ['install', '-g', '@google/gemini-cli']
|
|
13
|
+
},
|
|
14
|
+
claude: {
|
|
15
|
+
display: 'npm install -g @anthropic-ai/claude-code',
|
|
16
|
+
args: ['install', '-g', '@anthropic-ai/claude-code']
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function getInstallCommand(tool) {
|
|
21
|
+
return INSTALL_COMMANDS[tool];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function installTool(tool) {
|
|
25
|
+
const cmd = getInstallCommand(tool);
|
|
26
|
+
if (!cmd) throw new Error(`未知工具: ${tool}`);
|
|
27
|
+
|
|
28
|
+
await new Promise((resolve, reject) => {
|
|
29
|
+
const child = spawn(NPM_COMMAND, cmd.args, {
|
|
30
|
+
stdio: 'inherit'
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
child.on('error', (err) => {
|
|
34
|
+
reject(err);
|
|
35
|
+
});
|
|
36
|
+
child.on('close', (code) => {
|
|
37
|
+
if (code === 0) return resolve();
|
|
38
|
+
reject(new Error(`安装失败 (exit code: ${code})`));
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|