bhg-helper 1.0.6 → 1.0.7

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/cli/cli.js CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { select, confirm, input } from '@inquirer/prompts'
8
- import { execSync, spawn } from 'node:child_process'
8
+ import { execSync } from 'node:child_process'
9
9
  import os from 'node:os'
10
10
  import path from 'node:path'
11
11
  import fs from 'node:fs'
@@ -24,44 +24,37 @@ const C = {
24
24
  const theme = { prefix: `${C.cyan}?${C.r}` }
25
25
 
26
26
  // ── 常量 ──────────────────────────────────────────────────────
27
- const VERSION = '1.0.2'
27
+ const VERSION = '1.0.6'
28
28
  const RELAY_PORT = 8787
29
29
  const API_URL = 'http://127.0.0.1:3001'
30
30
  const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
31
- const BHG_HELPER_DIR = path.join(os.homedir(), '.bhg-helper')
32
- const RELAY_CONFIG_FILE = path.join(BHG_HELPER_DIR, 'relay.config.json')
33
- const CLAUDE_SETTINGS_FILE = path.join(os.homedir(), '.claude', 'settings.json')
34
-
35
- const TOOLS_LIST = [
36
- { name: 'Claude Code', desc: 'Anthropic 官方 CLI 编程助手' },
37
- { name: 'OpenCode', desc: '开源 AI 编程助手' },
38
- { name: 'Gemini CLI', desc: 'Google 官方 CLI 编程助手' },
39
- { name: 'OpenAI Codex CLI', desc: 'OpenAI 官方 CLI 编程助手' },
40
- ]
31
+ const BHG_DIR = path.join(os.homedir(), '.bhg-helper')
32
+ const RELAY_CFG = path.join(BHG_DIR, 'relay.config.json')
33
+ const CLAUDE_SETTINGS = path.join(os.homedir(), '.claude', 'settings.json')
34
+
35
+ const DEFAULT_CONFIG = {
36
+ port: RELAY_PORT,
37
+ deepseekBaseUrl: 'https://api.deepseek.com',
38
+ modelMap: {
39
+ 'deepseek-v4-pro': 'deepseek-v4-pro',
40
+ 'deepseek-chat': 'deepseek-v4-pro',
41
+ },
42
+ spoofProvider: 'deepseek',
43
+ verbose: true,
44
+ }
41
45
 
42
- // ── ANSI 24 位色渐变 Logo ──────────────────────────────────────
46
+ // ── ANSI 渐变 Logo(蓝 → 紫 → 粉)────────────────────────────
43
47
  function rgb(r, g, b) {
44
48
  return `\x1b[38;2;${r};${g};${b}m`
45
49
  }
46
-
47
- // 渐变 10 行
48
- function colorLine(row, totalRows = 10) {
49
- const t = row / (totalRows - 1)
50
+ function logoColor(row, total = 10) {
51
+ const t = row / (total - 1)
50
52
  if (t < 0.5) {
51
53
  const p = t * 2
52
- return rgb(
53
- Math.round(80 + p * 70),
54
- Math.round(180 - p * 40),
55
- Math.round(255)
56
- )
57
- } else {
58
- const p = (t - 0.5) * 2
59
- return rgb(
60
- Math.round(150 + p * 95),
61
- Math.round(140 - p * 70),
62
- Math.round(255)
63
- )
54
+ return rgb(80 + p * 70, 180 - p * 40, 255)
64
55
  }
56
+ const p = (t - 0.5) * 2
57
+ return rgb(150 + p * 95, 140 - p * 70, 255)
65
58
  }
66
59
 
67
60
  const BHG_LOGO = [
@@ -72,7 +65,6 @@ const BHG_LOGO = [
72
65
  '██████╔╝ ██║ ██║ ███████║',
73
66
  '╚═════╝ ╚═╝ ╚═╝ ╚══════╝',
74
67
  ]
75
-
76
68
  const HELPER_LOGO = [
77
69
  '██╗ ██╗ ███████╗ ██╗ ██████╗ ███████╗ ██████╗ ',
78
70
  '██║ ██║ ██╔════╝ ██║ ██╔══██╗ ██╔════╝ ██╔══██╗',
@@ -85,9 +77,7 @@ const HELPER_LOGO = [
85
77
  function printLogo() {
86
78
  const gap = ' '
87
79
  for (let i = 0; i < BHG_LOGO.length; i++) {
88
- console.log(
89
- `${colorLine(i)}${C.bold}${BHG_LOGO[i]}${gap}${HELPER_LOGO[i]}${C.r}`
90
- )
80
+ console.log(`${logoColor(i)}${C.bold}${BHG_LOGO[i]}${gap}${HELPER_LOGO[i]}${C.r}`)
91
81
  }
92
82
  console.log()
93
83
  }
@@ -95,207 +85,170 @@ function printLogo() {
95
85
  // ── 工具定义 ──────────────────────────────────────────────────
96
86
  const TOOLS = {
97
87
  claude: {
98
- name: 'Claude Code',
99
- cmd: 'claude',
100
- desc: 'Anthropic 官方 CLI 编程助手',
88
+ name: 'Claude Code', cmd: 'claude', desc: 'Anthropic 官方 CLI 编程助手',
101
89
  install: 'npm install -g @anthropic-ai/claude-code',
102
90
  },
103
91
  gemini: {
104
- name: 'Gemini CLI',
105
- cmd: 'gemini',
106
- desc: 'Google 官方 CLI 编程助手',
92
+ name: 'Gemini CLI', cmd: 'gemini', desc: 'Google 官方 CLI 编程助手',
107
93
  install: 'npm install -g @anthropic-ai/gemini-cli',
108
94
  },
109
95
  opencode: {
110
- name: 'OpenCode',
111
- cmd: 'opencode',
112
- desc: '开源 AI 编程助手',
96
+ name: 'OpenCode', cmd: 'opencode', desc: '开源 AI 编程助手',
113
97
  install: 'npm install -g opencode-ai',
114
- configFile: (() => {
115
- if (process.platform === 'win32') return path.join(os.homedir(), 'AppData', 'Roaming', 'opencode', 'opencode.json')
116
- return path.join(os.homedir(), '.config', 'opencode', 'opencode.json')
117
- })(),
98
+ configFile: path.join(os.homedir(), 'AppData', 'Roaming', 'opencode', 'opencode.json'),
99
+ },
100
+ codex: {
101
+ name: 'OpenAI Codex CLI', cmd: 'codex', desc: 'OpenAI 官方 CLI 编程助手',
102
+ install: 'npm install -g @openai/codex',
118
103
  },
119
104
  cursor: {
120
- name: 'Cursor',
121
- cmd: null,
122
- desc: 'AI 代码编辑器',
123
- install: '从 https://cursor.com 下载',
105
+ name: 'Cursor', cmd: null, desc: 'AI 代码编辑器', install: '从 https://cursor.com 下载',
124
106
  },
125
107
  continue: {
126
- name: 'Continue (VS Code)',
127
- cmd: null,
128
- desc: 'VS Code / JetBrains AI 扩展',
108
+ name: 'Continue (VS Code)', cmd: null, desc: 'VS Code / JetBrains AI 扩展',
129
109
  install: 'VS Code 扩展搜索 "Continue"',
130
- configFile: path.join(os.homedir(), '.continue', 'config.json'),
131
110
  },
132
111
  trae: {
133
- name: 'Trae',
134
- cmd: null,
135
- desc: '字节跳动 AI 代码编辑器',
136
- install: '从 https://trae.ai 下载',
137
- },
138
- codex: {
139
- name: 'OpenAI Codex CLI',
140
- cmd: 'codex',
141
- desc: 'OpenAI 官方 CLI 编程助手',
142
- install: 'npm install -g @openai/codex',
112
+ name: 'Trae', cmd: null, desc: '字节跳动 AI 代码编辑器', install: '从 https://trae.ai 下载',
143
113
  },
144
114
  }
145
115
 
146
- // ── 读取 / 写入 relay config ─────────────────────────────────
116
+ /** 有命令行的 CLI 工具,用于头部展示 */
117
+ const CLI_TOOLS = Object.values(TOOLS).filter(t => t.cmd)
118
+
119
+ // ── 配置读写 ──────────────────────────────────────────────────
147
120
  function loadConfig() {
148
121
  try {
149
- if (fs.existsSync(RELAY_CONFIG_FILE)) {
150
- return JSON.parse(fs.readFileSync(RELAY_CONFIG_FILE, 'utf-8'))
151
- }
122
+ if (fs.existsSync(RELAY_CFG)) return JSON.parse(fs.readFileSync(RELAY_CFG, 'utf-8'))
152
123
  } catch { /* */ }
153
124
  return null
154
125
  }
155
126
 
156
127
  function saveConfig(cfg) {
157
- fs.mkdirSync(BHG_HELPER_DIR, { recursive: true })
158
- fs.writeFileSync(RELAY_CONFIG_FILE, JSON.stringify(cfg, null, 2) + '\n', 'utf-8')
128
+ fs.mkdirSync(BHG_DIR, { recursive: true })
129
+ fs.writeFileSync(RELAY_CFG, JSON.stringify(cfg, null, 2) + '\n', 'utf-8')
159
130
  }
160
131
 
161
- function hasApiConfig() {
132
+ function hasApiKey() {
162
133
  const cfg = loadConfig()
163
- return !!(cfg && cfg.deepseekApiKey && cfg.deepseekApiKey.length > 0)
134
+ return !!(cfg?.deepseekApiKey?.length > 0)
164
135
  }
165
136
 
166
- function getCurrentModel() {
167
- const cfg = loadConfig()
168
- if (!cfg) return 'deepseek-v4-pro'
169
- return cfg.currentModel || 'deepseek-v4-pro'
137
+ function currentModel() {
138
+ return loadConfig()?.currentModel || 'deepseek-v4-pro'
170
139
  }
171
140
 
172
- // ── 辅助 ──────────────────────────────────────────────────────
173
- function checkInstalled(cmd) {
174
- if (!cmd) return null
175
- try {
176
- execSync(`where ${cmd}`, { stdio: 'pipe' })
177
- return true
178
- } catch { return false }
141
+ function maskedKey() {
142
+ const k = loadConfig()?.deepseekApiKey || ''
143
+ return k.length > 8 ? k.slice(0, 4) + '****' + k.slice(-4) : k || '(empty)'
179
144
  }
180
145
 
181
- async function apiPost(pathname) {
182
- const res = await fetch(`${API_URL}${pathname}`, { method: 'POST' })
183
- return res.ok
146
+ // ── 辅助函数 ──────────────────────────────────────────────────
147
+ function isInstalled(cmd) {
148
+ if (!cmd) return null
149
+ try { execSync(`where ${cmd}`, { stdio: 'pipe' }); return true }
150
+ catch { return false }
184
151
  }
185
152
 
186
153
  async function apiGet(pathname) {
187
154
  try {
188
- const controller = new AbortController()
189
- const timer = setTimeout(() => controller.abort(), 800)
190
- const res = await fetch(`${API_URL}${pathname}`, { signal: controller.signal })
155
+ const ac = new AbortController()
156
+ const timer = setTimeout(() => ac.abort(), 800)
157
+ const res = await fetch(`${API_URL}${pathname}`, { signal: ac.signal })
191
158
  clearTimeout(timer)
192
- if (!res.ok) return null
193
- return await res.json().catch(() => null)
194
- } catch {
195
- return null
196
- }
159
+ return res.ok ? await res.json().catch(() => null) : null
160
+ } catch { return null }
197
161
  }
198
162
 
199
- async function apiHealth() {
163
+ async function apiPost(pathname) {
200
164
  try {
201
- const controller = new AbortController()
202
- const timer = setTimeout(() => controller.abort(), 800)
203
- const res = await fetch(`${API_URL}/api/health`, { signal: controller.signal })
165
+ const res = await fetch(`${API_URL}${pathname}`, { method: 'POST' })
166
+ return res.ok
167
+ } catch { return false }
168
+ }
169
+
170
+ async function healthCheck() {
171
+ try {
172
+ const ac = new AbortController()
173
+ const timer = setTimeout(() => ac.abort(), 800)
174
+ const res = await fetch(`${API_URL}/api/health`, { signal: ac.signal })
204
175
  clearTimeout(timer)
205
176
  return res.ok
206
- } catch {
207
- return false
208
- }
177
+ } catch { return false }
209
178
  }
210
179
 
211
180
  async function ensureBackend() {
212
- if (await apiHealth()) return true
181
+ if (await healthCheck()) return true
213
182
  try {
214
- if (process.platform === 'win32') {
215
- execSync('start "" /min cmd /c "npm run server:raw"', {
216
- cwd: PROJECT_ROOT,
217
- stdio: 'ignore',
218
- shell: true,
219
- })
220
- } else {
221
- const child = spawn('npm', ['run', 'server:raw'], {
222
- cwd: PROJECT_ROOT,
223
- detached: true,
224
- stdio: 'ignore',
225
- shell: false,
226
- })
227
- child.unref()
228
- }
229
- } catch {
230
- return false
231
- }
232
- await new Promise(resolve => setTimeout(resolve, 1500))
233
- return await apiHealth()
183
+ execSync('start "" /min cmd /c "npm run server:raw"', { cwd: PROJECT_ROOT, stdio: 'ignore', shell: true })
184
+ } catch { return false }
185
+ await new Promise(r => setTimeout(r, 1500))
186
+ return await healthCheck()
234
187
  }
235
188
 
236
189
  async function ensureRelay() {
237
- if (!(await ensureBackend())) return false
238
- return await apiPost('/api/relay/start')
190
+ return (await ensureBackend()) && await apiPost('/api/relay/start')
239
191
  }
240
192
 
241
- // ── 打印头部(ArkHelper 风格)─────────────────────────────────
193
+ // ── 头部(ArkHelper 风格)──────────────────────────────────────
242
194
  async function printHeader() {
243
195
  console.clear()
244
196
  printLogo()
245
-
246
197
  console.log(` v${VERSION} · DeepSeek LLM bridge for your AI coding tools`)
247
198
  console.log()
248
199
  console.log(` ${C.bold}一键配置${C.r}`)
249
- for (const t of TOOLS_LIST) {
200
+ for (const t of CLI_TOOLS) {
250
201
  console.log(` ${C.cyan}◆${C.r} ${t.name} ${C.dim}— ${t.desc}${C.r}`)
251
202
  }
252
203
  console.log()
253
204
 
254
- const apiOk = hasApiConfig()
255
- const backendOk = await apiHealth()
256
- const relayJson = await apiGet('/api/relay/status')
257
- const relayOk = !!(relayJson && relayJson.data?.running)
205
+ const [apiOk, backendOk, relayJson] = await Promise.all([
206
+ Promise.resolve(hasApiKey()),
207
+ healthCheck(),
208
+ apiGet('/api/relay/status'),
209
+ ])
210
+ const relayOk = !!(relayJson?.data?.running)
258
211
 
259
212
  console.log(
260
- ` ${C.dim}API:${C.r} ${apiOk ? C.green + '✓' + C.r : C.red + '✗' + C.r}`
261
- + ` ${C.dim}Backend:${C.r} ${backendOk ? C.green + '✓' + C.r : C.red + '✗' + C.r}`
262
- + ` ${C.dim}Relay:${C.r} ${relayOk ? C.green + '✓' + C.r : C.red + '✗' + C.r}`
213
+ ` ${C.dim}API:${C.r} ${apiOk ? C.green + '✓' : C.red + '✗'}${C.r}`
214
+ + ` ${C.dim}Backend:${C.r} ${backendOk ? C.green + '✓' : C.red + '✗'}${C.r}`
215
+ + ` ${C.dim}Relay:${C.r} ${relayOk ? C.green + '✓' : C.red + '✗'}${C.r}`
263
216
  )
264
-
265
217
  console.log()
266
218
  console.log(` ${C.dim}powered by BH6BHG${C.r}`)
267
219
  console.log()
268
220
  }
269
221
 
222
+ // ── 菜单分隔线 ────────────────────────────────────────────────
223
+ const SEP = { name: `${C.dim}──────────${C.r}`, value: '---', disabled: true }
224
+
270
225
  // ── 一级菜单 ──────────────────────────────────────────────────
271
226
  async function mainMenu() {
272
- const choice = await select({
227
+ return select({
273
228
  message: '请选择操作',
274
229
  choices: [
275
230
  { name: '模型选择', value: 'model', description: '切换模型 / 输入 API Key' },
276
231
  { name: '工具配置', value: 'tools', description: '配置各 AI 编程工具' },
277
232
  { name: '语言配置', value: 'lang', description: '界面语言设置' },
278
- { name: `${C.dim}──────────${C.r}`, value: '---', disabled: true },
233
+ SEP,
279
234
  { name: '退出', value: 'exit' },
280
235
  ],
281
236
  theme,
282
237
  })
283
- return choice
284
238
  }
285
239
 
286
- // ── 模型选择子菜单 ───────────────────────────────────────────
240
+ // ── 模型选择 ──────────────────────────────────────────────────
287
241
  async function modelMenu() {
288
242
  const cfg = loadConfig()
289
- const currentModel = getCurrentModel()
243
+ const now = currentModel()
290
244
 
291
245
  const choice = await select({
292
246
  message: '模型选择',
293
247
  choices: [
294
- { name: `切换模型 ${C.dim}(当前:${currentModel})${C.r}`, value: 'switch', description: '选择要使用的模型' },
295
- { name: `输入 API ${C.dim}(${hasApiConfig() ? C.green + '已配置' + C.r : C.red + '未配置' + C.r})${C.r}`, value: 'apikey', description: '设置 DeepSeek API Key' },
296
- { name: `${C.dim}──────────${C.r}`, value: '---', disabled: true },
297
- { name: '返回', value: 'back' },
298
- { name: '退出', value: 'exit' },
248
+ { name: `切换模型 ${C.dim}(current: ${now})${C.r}`, value: 'switch', description: '选择要使用的模型' },
249
+ { name: `输入 API ${C.dim}(${hasApiKey() ? C.green + 'configured' : C.red + 'not set'}${C.r})${C.r}`, value: 'apikey', description: '设置 DeepSeek API Key' },
250
+ SEP,
251
+ { name: '返回', value: 'back' }, { name: '退出', value: 'exit' },
299
252
  ],
300
253
  theme,
301
254
  })
@@ -303,270 +256,236 @@ async function modelMenu() {
303
256
  if (choice === 'exit') return 'exit'
304
257
  if (choice === 'back') return 'back'
305
258
 
259
+ // ── 切换模型 ──
306
260
  if (choice === 'switch') {
307
- const models = []
308
- if (cfg && cfg.modelMap) {
309
- const seen = new Set()
310
- for (const [from] of Object.entries(cfg.modelMap)) {
311
- if (!seen.has(from)) {
312
- seen.add(from)
313
- models.push(from)
314
- }
315
- }
316
- } else {
317
- models.push('deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek-chat')
318
- }
261
+ const models = (cfg?.modelMap ? Object.keys(cfg.modelMap) : ['deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek-chat'])
262
+ .filter((v, i, a) => a.indexOf(v) === i) // dedupe
319
263
 
320
- const modelChoice = await select({
264
+ const pick = await select({
321
265
  message: '选择模型',
322
266
  choices: [
323
267
  ...models.map(m => ({
324
- name: m === currentModel ? `${C.green}● ${m}${C.r}` : ` ${m}`,
268
+ name: m === now ? `${C.green}● ${m}${C.r}` : ` ${m}`,
325
269
  value: m,
326
270
  })),
327
- { name: `${C.dim}──────────${C.r}`, value: '---', disabled: true },
271
+ SEP,
328
272
  { name: '返回', value: 'back' },
329
273
  ],
330
274
  theme,
331
275
  })
332
276
 
333
- if (modelChoice && modelChoice !== 'back') {
334
- const next = { ...cfg, currentModel: modelChoice }
335
- saveConfig(next)
336
- console.log(`\n ${C.green}✓ 已切换模型:${modelChoice}${C.r}\n`)
337
- await confirm({ message: '按回车继续...', theme })
277
+ if (pick && pick !== 'back') {
278
+ saveConfig({ ...cfg, currentModel: pick })
279
+ console.log(`\n ${C.green}Model switched: ${pick}${C.r}\n`)
280
+ await confirm({ message: 'Press Enter to continue...', theme })
338
281
  }
339
282
  return 'back'
340
283
  }
341
284
 
285
+ // ── 输入 API ──
342
286
  if (choice === 'apikey') {
343
- const currentKey = cfg?.deepseekApiKey || ''
344
- const masked = currentKey.length > 8
345
- ? currentKey.slice(0, 4) + '****' + currentKey.slice(-4)
346
- : currentKey || '(空)'
347
-
348
- console.log(`\n 当前 API Key:${C.dim}${masked}${C.r}`)
349
- console.log(` ${C.dim}前往 https://platform.deepseek.com 申请${C.r}\n`)
287
+ console.log(`\n Current API Key: ${C.dim}${maskedKey()}${C.r}`)
288
+ console.log(` ${C.dim}Apply at https://platform.deepseek.com${C.r}\n`)
350
289
 
351
290
  const newKey = await input({
352
- message: '输入 DeepSeek API Keysk-...)',
291
+ message: 'Enter DeepSeek API Key (sk-...)',
353
292
  theme,
354
- validate: (v) => v.length > 0 ? true : 'API Key 不能为空',
293
+ validate: v => v.length > 0 || 'API Key cannot be empty',
355
294
  })
356
295
 
357
- const next = { ...(cfg || {}), deepseekApiKey: newKey, port: cfg?.port || RELAY_PORT, deepseekBaseUrl: cfg?.deepseekBaseUrl || 'https://api.deepseek.com', modelMap: cfg?.modelMap || { 'deepseek-v4-pro': 'deepseek-v4-pro', 'deepseek-chat': 'deepseek-v4-pro' }, spoofProvider: cfg?.spoofProvider || 'deepseek', verbose: cfg?.verbose ?? true }
358
- saveConfig(next)
359
- console.log(`\n ${C.green}✓ API Key 已保存${C.r}`)
296
+ saveConfig({
297
+ ...(cfg || {}),
298
+ deepseekApiKey: newKey,
299
+ ...DEFAULT_CONFIG,
300
+ })
301
+ console.log(`\n ${C.green}API Key saved.${C.r}`)
360
302
 
361
- console.log(` ${C.dim}正在启动中转服务...${C.r}`)
362
- const relayOk = await ensureRelay()
363
- console.log(` ${relayOk ? C.green + ' 中转服务已启动' + C.r : C.yellow + '中转服务启动失败(可稍后手动启动)' + C.r}\n`)
364
- await confirm({ message: '按回车继续...', theme })
303
+ console.log(` ${C.dim}Starting relay...${C.r}`)
304
+ const ok = await ensureRelay()
305
+ console.log(` ${ok ? C.green + 'Relay started.' : C.yellow + 'Relay failed (try again later).'}${C.r}\n`)
306
+ await confirm({ message: 'Press Enter to continue...', theme })
365
307
  return 'back'
366
308
  }
367
309
 
368
310
  return 'back'
369
311
  }
370
312
 
371
- // ── 工具配置子菜单 ───────────────────────────────────────────
313
+ // ── 工具配置 ──────────────────────────────────────────────────
372
314
  async function toolsMenu() {
373
315
  const choice = await select({
374
316
  message: '工具配置 — 选择要配置的 AI 编程工具',
375
317
  choices: [
376
- { name: `${C.bold}Claude Code${C.r}`, value: 'claude', description: 'Anthropic 官方 CLI 编程助手' },
377
- { name: `${C.bold}Gemini CLI${C.r}`, value: 'gemini', description: 'Google 官方 CLI 编程助手' },
378
- { name: 'OpenCode', value: 'opencode', description: '开源 AI 编程助手' },
379
- { name: 'OpenAI Codex CLI', value: 'codex', description: 'OpenAI 官方 CLI 编程助手' },
380
- { name: 'Cursor', value: 'cursor', description: 'AI 代码编辑器' },
381
- { name: 'Continue (VS Code)', value: 'continue', description: 'VS Code / JetBrains 扩展' },
382
- { name: 'Trae', value: 'trae', description: '字节跳动 AI 编辑器' },
383
- { name: `${C.dim}──────────${C.r}`, value: '---', disabled: true },
384
- { name: '返回', value: 'back' },
385
- { name: '退出', value: 'exit' },
318
+ { name: `${C.bold}Claude Code${C.r}`, value: 'claude', description: TOOLS.claude.desc },
319
+ { name: `${C.bold}Gemini CLI${C.r}`, value: 'gemini', description: TOOLS.gemini.desc },
320
+ { name: 'OpenCode', value: 'opencode', description: TOOLS.opencode.desc },
321
+ { name: 'OpenAI Codex CLI', value: 'codex', description: TOOLS.codex.desc },
322
+ { name: 'Cursor', value: 'cursor', description: TOOLS.cursor.desc },
323
+ { name: 'Continue (VS Code)', value: 'continue', description: TOOLS.continue.desc },
324
+ { name: 'Trae', value: 'trae', description: TOOLS.trae.desc },
325
+ SEP,
326
+ { name: '返回', value: 'back' }, { name: '退出', value: 'exit' },
386
327
  ],
387
328
  theme,
388
329
  })
389
330
 
390
331
  if (choice === 'exit') return 'exit'
391
332
  if (choice === 'back') return 'back'
392
-
393
- return await toolDetailMenu(choice)
333
+ return toolAction(choice)
394
334
  }
395
335
 
396
- // ── 单个工具详情 ──────────────────────────────────────────────
397
- async function toolDetailMenu(toolKey) {
398
- const tool = TOOLS[toolKey]
336
+ // ── 单个工具操作 ──────────────────────────────────────────────
337
+ async function toolAction(key) {
338
+ const t = TOOLS[key]
399
339
 
400
- if (!tool.cmd) {
401
- console.log(`\n ${C.bold}${tool.name}${C.r} — ${tool.desc}`)
402
- console.log(` ${C.dim}安装方式:${tool.install}${C.r}`)
403
- console.log(` ${C.yellow}当前纯命令行版暂不自动配置 ${tool.name}${C.r}\n`)
404
- await confirm({ message: '按回车继续...', theme })
340
+ // GUI-only tools
341
+ if (!t.cmd) {
342
+ console.log(`\n ${C.bold}${t.name}${C.r} — ${t.desc}`)
343
+ console.log(` ${C.dim}Install: ${t.install}${C.r}`)
344
+ console.log(` ${C.yellow}CLI mode does not auto-configure ${t.name}.${C.r}\n`)
345
+ await confirm({ message: 'Press Enter to continue...', theme })
405
346
  return 'back'
406
347
  }
407
348
 
408
- const installed = checkInstalled(tool.cmd)
409
-
410
- const choices = []
411
- if (!installed) {
412
- choices.push({ name: `安装 ${tool.name}`, value: 'install', description: tool.install })
413
- } else {
414
- choices.push({ name: `${C.green}启动 ${tool.name}${C.r}`, value: 'launch', description: '启动工具' })
415
- choices.push({ name: `${C.yellow}重装 ${tool.name}${C.r}`, value: 'reinstall', description: tool.install })
416
- }
417
-
418
- choices.push(
419
- { name: `${C.dim}──────────${C.r}`, value: '---', disabled: true },
420
- { name: '返回', value: 'back' },
421
- { name: '退出', value: 'exit' },
422
- )
423
-
349
+ const installed = isInstalled(t.cmd)
424
350
  const action = await select({
425
- message: `${tool.name} — ${tool.desc}${installed ? ` ${C.green}(已安装)${C.r}` : ` ${C.red}(未安装)${C.r}`}`,
426
- choices,
351
+ message: `${t.name} — ${t.desc}${installed ? ` ${C.green}[installed]${C.r}` : ` ${C.red}[not installed]${C.r}`}`,
352
+ choices: [
353
+ ...(installed
354
+ ? [{ name: `${C.green}Launch ${t.name}${C.r}`, value: 'launch' }, { name: `${C.yellow}Reinstall${C.r}`, value: 'reinstall' }]
355
+ : [{ name: `Install ${t.name}`, value: 'install', description: t.install }]
356
+ ),
357
+ SEP,
358
+ { name: '返回', value: 'back' }, { name: '退出', value: 'exit' },
359
+ ],
427
360
  theme,
428
361
  })
429
362
 
430
363
  if (action === 'exit') return 'exit'
431
364
  if (action === 'back') return 'back'
432
365
 
366
+ // 安装 / 重装
433
367
  if (action === 'install' || action === 'reinstall') {
434
- console.log(`\n 正在执行:${tool.install}`)
368
+ console.log(`\n Running: ${t.install}`)
435
369
  try {
436
- execSync(tool.install, { stdio: 'inherit', shell: true })
437
- console.log(`\n ${C.green}${tool.name} 安装完成${C.r}\n`)
370
+ execSync(t.install, { stdio: 'inherit', shell: true })
371
+ console.log(`\n ${C.green}${t.name} installed.${C.r}\n`)
438
372
  } catch {
439
- console.log(`\n ${C.red}安装失败,请手动执行:${tool.install}${C.r}\n`)
373
+ console.log(`\n ${C.red}Install failed. Run manually: ${t.install}${C.r}\n`)
440
374
  }
441
- await confirm({ message: '按回车继续...', theme })
375
+ await confirm({ message: 'Press Enter to continue...', theme })
442
376
  return 'back'
443
377
  }
444
378
 
379
+ // 启动
445
380
  if (action === 'launch') {
446
- if (!hasApiConfig()) {
447
- console.log(`\n ${C.red}API 未配置!请先在「模型选择」→「输入 API」中配置。${C.r}\n`)
448
- await confirm({ message: '按回车继续...', theme })
381
+ if (!hasApiKey()) {
382
+ console.log(`\n ${C.red}API not configured! Go to Model Settings -> Enter API first.${C.r}\n`)
383
+ await confirm({ message: 'Press Enter to continue...', theme })
449
384
  return 'back'
450
385
  }
451
386
 
452
- const relayOk = await ensureRelay()
453
- if (!relayOk) {
454
- console.log(`\n ${C.red}中转服务启动失败,请重新运行 BHG-helper。${C.r}\n`)
455
- await confirm({ message: '按回车继续...', theme })
387
+ if (!(await ensureRelay())) {
388
+ console.log(`\n ${C.red}Relay failed. Restart BHG-helper.${C.r}\n`)
389
+ await confirm({ message: 'Press Enter to continue...', theme })
456
390
  return 'back'
457
391
  }
458
392
 
459
- if (toolKey === 'claude') {
460
- const currentModel = getCurrentModel()
461
- const dir = path.dirname(CLAUDE_SETTINGS_FILE)
462
- fs.mkdirSync(dir, { recursive: true })
463
- const existing = fs.existsSync(CLAUDE_SETTINGS_FILE)
464
- ? JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_FILE, 'utf-8'))
465
- : {}
466
- fs.writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify({
467
- ...existing,
468
- hasCompletedOnboarding: true,
469
- env: {
470
- ...(existing.env || {}),
471
- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
472
- ANTHROPIC_BASE_URL: `http://127.0.0.1:${RELAY_PORT}`,
473
- ANTHROPIC_AUTH_TOKEN: 'bhg-helper',
474
- ANTHROPIC_DEFAULT_OPUS_MODEL: currentModel,
475
- ANTHROPIC_DEFAULT_SONNET_MODEL: currentModel,
476
- ANTHROPIC_DEFAULT_HAIKU_MODEL: currentModel,
477
- ANTHROPIC_MODEL: currentModel,
478
- },
479
- }, null, 2) + '\n')
480
- console.log(` ${C.green}✓ Claude Code 配置已写入${C.r}`)
481
- }
482
-
483
- if (toolKey === 'opencode') {
484
- const file = TOOLS.opencode.configFile
485
- const dir = path.dirname(file)
486
- fs.mkdirSync(dir, { recursive: true })
487
- const existing = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf-8')) : {}
488
- const currentModel = getCurrentModel()
489
- fs.writeFileSync(file, JSON.stringify({
490
- ...existing,
491
- model: { provider: 'openai', name: currentModel },
492
- providers: [
493
- ...(existing.providers || []).filter(p => p.name !== 'bhg-deepseek'),
494
- { name: 'bhg-deepseek', type: 'openai', baseURL: `http://127.0.0.1:${RELAY_PORT}/v1`, apiKey: 'bhg-helper' },
495
- ],
496
- }, null, 2) + '\n')
497
- console.log(` ${C.green}✓ OpenCode 配置已写入${C.r}`)
498
- }
499
-
500
- console.log(`\n 正在启动 ${tool.cmd}...`)
501
- try {
502
- execSync(tool.cmd, { stdio: 'inherit', shell: true })
503
- } catch { /* 退出 */ }
393
+ writeToolConfig(key)
394
+ console.log(`\n Launching ${t.cmd}...`)
395
+ try { execSync(t.cmd, { stdio: 'inherit', shell: true }) }
396
+ catch { /* exit */ }
504
397
  return 'back'
505
398
  }
506
399
 
507
400
  return 'back'
508
401
  }
509
402
 
403
+ // ── 写工具配置文件 ────────────────────────────────────────────
404
+ function writeToolConfig(key) {
405
+ const model = currentModel()
406
+ const relayUrl = `http://127.0.0.1:${RELAY_PORT}`
407
+
408
+ if (key === 'claude') {
409
+ fs.mkdirSync(path.dirname(CLAUDE_SETTINGS), { recursive: true })
410
+ let existing = {}
411
+ try { existing = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS, 'utf-8')) } catch { /* */ }
412
+ fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify({
413
+ ...existing,
414
+ hasCompletedOnboarding: true,
415
+ env: {
416
+ ...(existing.env || {}),
417
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
418
+ ANTHROPIC_BASE_URL: relayUrl,
419
+ ANTHROPIC_AUTH_TOKEN: 'bhg-helper',
420
+ ANTHROPIC_DEFAULT_OPUS_MODEL: model,
421
+ ANTHROPIC_DEFAULT_SONNET_MODEL: model,
422
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: model,
423
+ ANTHROPIC_MODEL: model,
424
+ },
425
+ }, null, 2) + '\n')
426
+ console.log(` ${C.green}Claude Code config written.${C.r}`)
427
+ }
428
+
429
+ if (key === 'opencode') {
430
+ const file = TOOLS.opencode.configFile
431
+ fs.mkdirSync(path.dirname(file), { recursive: true })
432
+ let existing = {}
433
+ try { existing = JSON.parse(fs.readFileSync(file, 'utf-8')) } catch { /* */ }
434
+ fs.writeFileSync(file, JSON.stringify({
435
+ ...existing,
436
+ model: { provider: 'openai', name: model },
437
+ providers: [
438
+ ...(existing.providers || []).filter(p => p.name !== 'bhg-deepseek'),
439
+ { name: 'bhg-deepseek', type: 'openai', baseURL: `${relayUrl}/v1`, apiKey: 'bhg-helper' },
440
+ ],
441
+ }, null, 2) + '\n')
442
+ console.log(` ${C.green}OpenCode config written.${C.r}`)
443
+ }
444
+ }
445
+
510
446
  // ── 语言配置 ──────────────────────────────────────────────────
511
447
  async function langMenu() {
512
448
  const choice = await select({
513
- message: '语言配置',
449
+ message: '语言配置 / Language',
514
450
  choices: [
515
- { name: '中文', value: 'zh' },
516
- { name: 'English', value: 'en' },
517
- { name: `${C.dim}──────────${C.r}`, value: '---', disabled: true },
518
- { name: '返回', value: 'back' },
451
+ { name: '中文', value: 'zh' }, { name: 'English', value: 'en' },
452
+ SEP, { name: '返回', value: 'back' },
519
453
  ],
520
454
  theme,
521
455
  })
522
-
523
- if (choice === 'exit') return 'exit'
524
456
  if (choice === 'back') return 'back'
525
-
526
- console.log(`\n ${C.green}✓ 语言已设为:${choice === 'zh' ? '中文' : 'English'}${C.r}\n`)
527
- await confirm({ message: '按回车继续...', theme })
457
+ console.log(`\n ${C.green}Language: ${choice === 'zh' ? '中文' : 'English'}${C.r}\n`)
458
+ await confirm({ message: 'Press Enter to continue...', theme })
528
459
  return 'back'
529
460
  }
530
461
 
531
462
  // ── 主循环 ────────────────────────────────────────────────────
532
463
  async function main() {
533
- let running = true
534
- while (running) {
464
+ while (true) {
535
465
  await printHeader()
536
466
  const choice = await mainMenu()
537
467
 
538
- switch (choice) {
539
- case 'exit':
540
- console.log(`\n ${C.dim}再见。${C.r}\n`)
541
- running = false
542
- break
543
-
544
- case 'model': {
545
- const result = await modelMenu()
546
- if (result === 'exit') { console.log(`\n ${C.dim}再见。${C.r}\n`); running = false }
547
- break
548
- }
549
-
550
- case 'tools': {
551
- const result = await toolsMenu()
552
- if (result === 'exit') { console.log(`\n ${C.dim}再见。${C.r}\n`); running = false }
553
- break
554
- }
555
-
556
- case 'lang': {
557
- const result = await langMenu()
558
- if (result === 'exit') { console.log(`\n ${C.dim}再见。${C.r}\n`); running = false }
559
- break
560
- }
468
+ if (choice === 'exit') {
469
+ console.log(`\n ${C.dim}Bye.${C.r}\n`)
470
+ break
471
+ }
472
+
473
+ const result = await (
474
+ choice === 'model' ? modelMenu()
475
+ : choice === 'tools' ? toolsMenu()
476
+ : choice === 'lang' ? langMenu()
477
+ : Promise.resolve('back')
478
+ )
479
+
480
+ if (result === 'exit') {
481
+ console.log(`\n ${C.dim}Bye.${C.r}\n`)
482
+ break
561
483
  }
562
484
  }
563
485
  }
564
486
 
565
487
  main().catch((err) => {
566
- if (err.name === 'ExitPromptError') {
567
- console.log(`\n ${C.dim}再见。${C.r}\n`)
568
- process.exit(0)
569
- }
488
+ if (err.name === 'ExitPromptError') { console.log(`\n ${C.dim}Bye.${C.r}\n`); process.exit(0) }
570
489
  console.error('Error:', err.message)
571
490
  process.exit(1)
572
491
  })