bhg-helper 1.0.5 → 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,196 +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
213
- const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'
214
- const child = spawn(npmCmd, ['run', 'server:raw'], {
215
- cwd: PROJECT_ROOT,
216
- detached: true,
217
- stdio: 'ignore',
218
- shell: false,
219
- })
220
- child.unref()
221
- await new Promise(resolve => setTimeout(resolve, 1500))
222
- return await apiHealth()
181
+ if (await healthCheck()) return true
182
+ try {
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()
223
187
  }
224
188
 
225
189
  async function ensureRelay() {
226
- if (!(await ensureBackend())) return false
227
- return await apiPost('/api/relay/start')
190
+ return (await ensureBackend()) && await apiPost('/api/relay/start')
228
191
  }
229
192
 
230
- // ── 打印头部(ArkHelper 风格)─────────────────────────────────
193
+ // ── 头部(ArkHelper 风格)──────────────────────────────────────
231
194
  async function printHeader() {
232
195
  console.clear()
233
196
  printLogo()
234
-
235
197
  console.log(` v${VERSION} · DeepSeek LLM bridge for your AI coding tools`)
236
198
  console.log()
237
199
  console.log(` ${C.bold}一键配置${C.r}`)
238
- for (const t of TOOLS_LIST) {
200
+ for (const t of CLI_TOOLS) {
239
201
  console.log(` ${C.cyan}◆${C.r} ${t.name} ${C.dim}— ${t.desc}${C.r}`)
240
202
  }
241
203
  console.log()
242
204
 
243
- const apiOk = hasApiConfig()
244
- const backendOk = await apiHealth()
245
- const relayJson = await apiGet('/api/relay/status')
246
- 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)
247
211
 
248
212
  console.log(
249
- ` ${C.dim}API:${C.r} ${apiOk ? C.green + '✓' + C.r : C.red + '✗' + C.r}`
250
- + ` ${C.dim}Backend:${C.r} ${backendOk ? C.green + '✓' + C.r : C.red + '✗' + C.r}`
251
- + ` ${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}`
252
216
  )
253
-
254
217
  console.log()
255
218
  console.log(` ${C.dim}powered by BH6BHG${C.r}`)
256
219
  console.log()
257
220
  }
258
221
 
222
+ // ── 菜单分隔线 ────────────────────────────────────────────────
223
+ const SEP = { name: `${C.dim}──────────${C.r}`, value: '---', disabled: true }
224
+
259
225
  // ── 一级菜单 ──────────────────────────────────────────────────
260
226
  async function mainMenu() {
261
- const choice = await select({
227
+ return select({
262
228
  message: '请选择操作',
263
229
  choices: [
264
230
  { name: '模型选择', value: 'model', description: '切换模型 / 输入 API Key' },
265
231
  { name: '工具配置', value: 'tools', description: '配置各 AI 编程工具' },
266
232
  { name: '语言配置', value: 'lang', description: '界面语言设置' },
267
- { name: `${C.dim}──────────${C.r}`, value: '---', disabled: true },
233
+ SEP,
268
234
  { name: '退出', value: 'exit' },
269
235
  ],
270
236
  theme,
271
237
  })
272
- return choice
273
238
  }
274
239
 
275
- // ── 模型选择子菜单 ───────────────────────────────────────────
240
+ // ── 模型选择 ──────────────────────────────────────────────────
276
241
  async function modelMenu() {
277
242
  const cfg = loadConfig()
278
- const currentModel = getCurrentModel()
243
+ const now = currentModel()
279
244
 
280
245
  const choice = await select({
281
246
  message: '模型选择',
282
247
  choices: [
283
- { name: `切换模型 ${C.dim}(当前:${currentModel})${C.r}`, value: 'switch', description: '选择要使用的模型' },
284
- { name: `输入 API ${C.dim}(${hasApiConfig() ? C.green + '已配置' + C.r : C.red + '未配置' + C.r})${C.r}`, value: 'apikey', description: '设置 DeepSeek API Key' },
285
- { name: `${C.dim}──────────${C.r}`, value: '---', disabled: true },
286
- { name: '返回', value: 'back' },
287
- { 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' },
288
252
  ],
289
253
  theme,
290
254
  })
@@ -292,270 +256,236 @@ async function modelMenu() {
292
256
  if (choice === 'exit') return 'exit'
293
257
  if (choice === 'back') return 'back'
294
258
 
259
+ // ── 切换模型 ──
295
260
  if (choice === 'switch') {
296
- const models = []
297
- if (cfg && cfg.modelMap) {
298
- const seen = new Set()
299
- for (const [from] of Object.entries(cfg.modelMap)) {
300
- if (!seen.has(from)) {
301
- seen.add(from)
302
- models.push(from)
303
- }
304
- }
305
- } else {
306
- models.push('deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek-chat')
307
- }
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
308
263
 
309
- const modelChoice = await select({
264
+ const pick = await select({
310
265
  message: '选择模型',
311
266
  choices: [
312
267
  ...models.map(m => ({
313
- name: m === currentModel ? `${C.green}● ${m}${C.r}` : ` ${m}`,
268
+ name: m === now ? `${C.green}● ${m}${C.r}` : ` ${m}`,
314
269
  value: m,
315
270
  })),
316
- { name: `${C.dim}──────────${C.r}`, value: '---', disabled: true },
271
+ SEP,
317
272
  { name: '返回', value: 'back' },
318
273
  ],
319
274
  theme,
320
275
  })
321
276
 
322
- if (modelChoice && modelChoice !== 'back') {
323
- const next = { ...cfg, currentModel: modelChoice }
324
- saveConfig(next)
325
- console.log(`\n ${C.green}✓ 已切换模型:${modelChoice}${C.r}\n`)
326
- 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 })
327
281
  }
328
282
  return 'back'
329
283
  }
330
284
 
285
+ // ── 输入 API ──
331
286
  if (choice === 'apikey') {
332
- const currentKey = cfg?.deepseekApiKey || ''
333
- const masked = currentKey.length > 8
334
- ? currentKey.slice(0, 4) + '****' + currentKey.slice(-4)
335
- : currentKey || '(空)'
336
-
337
- console.log(`\n 当前 API Key:${C.dim}${masked}${C.r}`)
338
- 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`)
339
289
 
340
290
  const newKey = await input({
341
- message: '输入 DeepSeek API Keysk-...)',
291
+ message: 'Enter DeepSeek API Key (sk-...)',
342
292
  theme,
343
- validate: (v) => v.length > 0 ? true : 'API Key 不能为空',
293
+ validate: v => v.length > 0 || 'API Key cannot be empty',
344
294
  })
345
295
 
346
- 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 }
347
- saveConfig(next)
348
- 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}`)
349
302
 
350
- console.log(` ${C.dim}正在启动中转服务...${C.r}`)
351
- const relayOk = await ensureRelay()
352
- console.log(` ${relayOk ? C.green + ' 中转服务已启动' + C.r : C.yellow + '中转服务启动失败(可稍后手动启动)' + C.r}\n`)
353
- 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 })
354
307
  return 'back'
355
308
  }
356
309
 
357
310
  return 'back'
358
311
  }
359
312
 
360
- // ── 工具配置子菜单 ───────────────────────────────────────────
313
+ // ── 工具配置 ──────────────────────────────────────────────────
361
314
  async function toolsMenu() {
362
315
  const choice = await select({
363
316
  message: '工具配置 — 选择要配置的 AI 编程工具',
364
317
  choices: [
365
- { name: `${C.bold}Claude Code${C.r}`, value: 'claude', description: 'Anthropic 官方 CLI 编程助手' },
366
- { name: `${C.bold}Gemini CLI${C.r}`, value: 'gemini', description: 'Google 官方 CLI 编程助手' },
367
- { name: 'OpenCode', value: 'opencode', description: '开源 AI 编程助手' },
368
- { name: 'OpenAI Codex CLI', value: 'codex', description: 'OpenAI 官方 CLI 编程助手' },
369
- { name: 'Cursor', value: 'cursor', description: 'AI 代码编辑器' },
370
- { name: 'Continue (VS Code)', value: 'continue', description: 'VS Code / JetBrains 扩展' },
371
- { name: 'Trae', value: 'trae', description: '字节跳动 AI 编辑器' },
372
- { name: `${C.dim}──────────${C.r}`, value: '---', disabled: true },
373
- { name: '返回', value: 'back' },
374
- { 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' },
375
327
  ],
376
328
  theme,
377
329
  })
378
330
 
379
331
  if (choice === 'exit') return 'exit'
380
332
  if (choice === 'back') return 'back'
381
-
382
- return await toolDetailMenu(choice)
333
+ return toolAction(choice)
383
334
  }
384
335
 
385
- // ── 单个工具详情 ──────────────────────────────────────────────
386
- async function toolDetailMenu(toolKey) {
387
- const tool = TOOLS[toolKey]
336
+ // ── 单个工具操作 ──────────────────────────────────────────────
337
+ async function toolAction(key) {
338
+ const t = TOOLS[key]
388
339
 
389
- if (!tool.cmd) {
390
- console.log(`\n ${C.bold}${tool.name}${C.r} — ${tool.desc}`)
391
- console.log(` ${C.dim}安装方式:${tool.install}${C.r}`)
392
- console.log(` ${C.yellow}当前纯命令行版暂不自动配置 ${tool.name}${C.r}\n`)
393
- 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 })
394
346
  return 'back'
395
347
  }
396
348
 
397
- const installed = checkInstalled(tool.cmd)
398
-
399
- const choices = []
400
- if (!installed) {
401
- choices.push({ name: `安装 ${tool.name}`, value: 'install', description: tool.install })
402
- } else {
403
- choices.push({ name: `${C.green}启动 ${tool.name}${C.r}`, value: 'launch', description: '启动工具' })
404
- choices.push({ name: `${C.yellow}重装 ${tool.name}${C.r}`, value: 'reinstall', description: tool.install })
405
- }
406
-
407
- choices.push(
408
- { name: `${C.dim}──────────${C.r}`, value: '---', disabled: true },
409
- { name: '返回', value: 'back' },
410
- { name: '退出', value: 'exit' },
411
- )
412
-
349
+ const installed = isInstalled(t.cmd)
413
350
  const action = await select({
414
- message: `${tool.name} — ${tool.desc}${installed ? ` ${C.green}(已安装)${C.r}` : ` ${C.red}(未安装)${C.r}`}`,
415
- 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
+ ],
416
360
  theme,
417
361
  })
418
362
 
419
363
  if (action === 'exit') return 'exit'
420
364
  if (action === 'back') return 'back'
421
365
 
366
+ // 安装 / 重装
422
367
  if (action === 'install' || action === 'reinstall') {
423
- console.log(`\n 正在执行:${tool.install}`)
368
+ console.log(`\n Running: ${t.install}`)
424
369
  try {
425
- execSync(tool.install, { stdio: 'inherit', shell: true })
426
- 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`)
427
372
  } catch {
428
- 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`)
429
374
  }
430
- await confirm({ message: '按回车继续...', theme })
375
+ await confirm({ message: 'Press Enter to continue...', theme })
431
376
  return 'back'
432
377
  }
433
378
 
379
+ // 启动
434
380
  if (action === 'launch') {
435
- if (!hasApiConfig()) {
436
- console.log(`\n ${C.red}API 未配置!请先在「模型选择」→「输入 API」中配置。${C.r}\n`)
437
- 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 })
438
384
  return 'back'
439
385
  }
440
386
 
441
- const relayOk = await ensureRelay()
442
- if (!relayOk) {
443
- console.log(`\n ${C.red}中转服务启动失败,请重新运行 BHG-helper。${C.r}\n`)
444
- 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 })
445
390
  return 'back'
446
391
  }
447
392
 
448
- if (toolKey === 'claude') {
449
- const currentModel = getCurrentModel()
450
- const dir = path.dirname(CLAUDE_SETTINGS_FILE)
451
- fs.mkdirSync(dir, { recursive: true })
452
- const existing = fs.existsSync(CLAUDE_SETTINGS_FILE)
453
- ? JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_FILE, 'utf-8'))
454
- : {}
455
- fs.writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify({
456
- ...existing,
457
- hasCompletedOnboarding: true,
458
- env: {
459
- ...(existing.env || {}),
460
- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
461
- ANTHROPIC_BASE_URL: `http://127.0.0.1:${RELAY_PORT}`,
462
- ANTHROPIC_AUTH_TOKEN: 'bhg-helper',
463
- ANTHROPIC_DEFAULT_OPUS_MODEL: currentModel,
464
- ANTHROPIC_DEFAULT_SONNET_MODEL: currentModel,
465
- ANTHROPIC_DEFAULT_HAIKU_MODEL: currentModel,
466
- ANTHROPIC_MODEL: currentModel,
467
- },
468
- }, null, 2) + '\n')
469
- console.log(` ${C.green}✓ Claude Code 配置已写入${C.r}`)
470
- }
471
-
472
- if (toolKey === 'opencode') {
473
- const file = TOOLS.opencode.configFile
474
- const dir = path.dirname(file)
475
- fs.mkdirSync(dir, { recursive: true })
476
- const existing = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf-8')) : {}
477
- const currentModel = getCurrentModel()
478
- fs.writeFileSync(file, JSON.stringify({
479
- ...existing,
480
- model: { provider: 'openai', name: currentModel },
481
- providers: [
482
- ...(existing.providers || []).filter(p => p.name !== 'bhg-deepseek'),
483
- { name: 'bhg-deepseek', type: 'openai', baseURL: `http://127.0.0.1:${RELAY_PORT}/v1`, apiKey: 'bhg-helper' },
484
- ],
485
- }, null, 2) + '\n')
486
- console.log(` ${C.green}✓ OpenCode 配置已写入${C.r}`)
487
- }
488
-
489
- console.log(`\n 正在启动 ${tool.cmd}...`)
490
- try {
491
- execSync(tool.cmd, { stdio: 'inherit', shell: true })
492
- } catch { /* 退出 */ }
393
+ writeToolConfig(key)
394
+ console.log(`\n Launching ${t.cmd}...`)
395
+ try { execSync(t.cmd, { stdio: 'inherit', shell: true }) }
396
+ catch { /* exit */ }
493
397
  return 'back'
494
398
  }
495
399
 
496
400
  return 'back'
497
401
  }
498
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
+
499
446
  // ── 语言配置 ──────────────────────────────────────────────────
500
447
  async function langMenu() {
501
448
  const choice = await select({
502
- message: '语言配置',
449
+ message: '语言配置 / Language',
503
450
  choices: [
504
- { name: '中文', value: 'zh' },
505
- { name: 'English', value: 'en' },
506
- { name: `${C.dim}──────────${C.r}`, value: '---', disabled: true },
507
- { name: '返回', value: 'back' },
451
+ { name: '中文', value: 'zh' }, { name: 'English', value: 'en' },
452
+ SEP, { name: '返回', value: 'back' },
508
453
  ],
509
454
  theme,
510
455
  })
511
-
512
- if (choice === 'exit') return 'exit'
513
456
  if (choice === 'back') return 'back'
514
-
515
- console.log(`\n ${C.green}✓ 语言已设为:${choice === 'zh' ? '中文' : 'English'}${C.r}\n`)
516
- 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 })
517
459
  return 'back'
518
460
  }
519
461
 
520
462
  // ── 主循环 ────────────────────────────────────────────────────
521
463
  async function main() {
522
- let running = true
523
- while (running) {
464
+ while (true) {
524
465
  await printHeader()
525
466
  const choice = await mainMenu()
526
467
 
527
- switch (choice) {
528
- case 'exit':
529
- console.log(`\n ${C.dim}再见。${C.r}\n`)
530
- running = false
531
- break
532
-
533
- case 'model': {
534
- const result = await modelMenu()
535
- if (result === 'exit') { console.log(`\n ${C.dim}再见。${C.r}\n`); running = false }
536
- break
537
- }
538
-
539
- case 'tools': {
540
- const result = await toolsMenu()
541
- if (result === 'exit') { console.log(`\n ${C.dim}再见。${C.r}\n`); running = false }
542
- break
543
- }
544
-
545
- case 'lang': {
546
- const result = await langMenu()
547
- if (result === 'exit') { console.log(`\n ${C.dim}再见。${C.r}\n`); running = false }
548
- break
549
- }
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
550
483
  }
551
484
  }
552
485
  }
553
486
 
554
487
  main().catch((err) => {
555
- if (err.name === 'ExitPromptError') {
556
- console.log(`\n ${C.dim}再见。${C.r}\n`)
557
- process.exit(0)
558
- }
488
+ if (err.name === 'ExitPromptError') { console.log(`\n ${C.dim}Bye.${C.r}\n`); process.exit(0) }
559
489
  console.error('Error:', err.message)
560
490
  process.exit(1)
561
491
  })