@x-all-in-one/coding-helper 0.0.2 → 0.0.4

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.
@@ -1,16 +1,17 @@
1
- import inquirer from 'inquirer';
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
2
3
  import chalk from 'chalk';
3
- import ora from 'ora';
4
+ import inquirer from 'inquirer';
4
5
  import open from 'open';
6
+ import ora from 'ora';
5
7
  import terminalLink from 'terminal-link';
6
- import { configManager } from './config.js';
7
- import { toolManager, SUPPORTED_TOOLS } from './tool-manager.js';
8
- import { claudeCodeManager, DEFAULT_CONFIG } from './claude-code-manager.js';
9
- import { i18n } from './i18n.js';
10
8
  import { createBorderLine, createContentLine } from '../utils/string-width.js';
11
- import { execSync } from 'child_process';
12
- import { existsSync } from 'fs';
13
9
  import { validateApiKey } from './api-validator.js';
10
+ import { claudeCodeManager, DEFAULT_CONFIG } from './claude-code-manager.js';
11
+ import { configManager } from './config.js';
12
+ import { i18n } from './i18n.js';
13
+ import { OPENCODE_DEFAULT_CONFIG, openCodeManager } from './opencode-manager.js';
14
+ import { SUPPORTED_TOOLS, toolManager } from './tool-manager.js';
14
15
  // 常量定义
15
16
  const OFFICIAL_WEBSITE = 'https://code.x-aio.com';
16
17
  const API_KEYS_URL = 'https://code.x-aio.com/dashboard/keys';
@@ -29,7 +30,7 @@ export class Wizard {
29
30
  * Create a simple box with title using double-line border style
30
31
  */
31
32
  createBox(title) {
32
- console.log(chalk.cyan.bold('\n' + createBorderLine('╔', '╗', '═', this.BOX_WIDTH)));
33
+ console.log(chalk.cyan.bold(`\n${createBorderLine('╔', '╗', '═', this.BOX_WIDTH)}`));
33
34
  console.log(chalk.cyan.bold(createContentLine(title, '║', '║', this.BOX_WIDTH, 'center')));
34
35
  console.log(chalk.cyan.bold(createBorderLine('╚', '╝', '═', this.BOX_WIDTH)));
35
36
  console.log('');
@@ -40,9 +41,9 @@ export class Wizard {
40
41
  showOperationHints() {
41
42
  const hints = [
42
43
  chalk.gray(i18n.t('wizard.hint_navigate')),
43
- chalk.gray(i18n.t('wizard.hint_confirm'))
44
+ chalk.gray(i18n.t('wizard.hint_confirm')),
44
45
  ];
45
- console.log(chalk.gray('💡 ') + hints.join(chalk.gray(' | ')) + '\n');
46
+ console.log(`${chalk.gray('💡 ') + hints.join(chalk.gray(' | '))}\n`);
46
47
  }
47
48
  /**
48
49
  * Prompt wrapper that shows operation hints
@@ -58,12 +59,12 @@ export class Wizard {
58
59
  const emptyLine = createContentLine('', '║', '║', BANNER_WIDTH, 'center');
59
60
  const titleLine = createContentLine('Coding Helper v0.0.1', '║', '║', BANNER_WIDTH, 'center');
60
61
  // 官网链接行
61
- const websiteText = i18n.t('wizard.official_website') + ': ' + OFFICIAL_WEBSITE;
62
+ const websiteText = `${i18n.t('wizard.official_website')}: ${OFFICIAL_WEBSITE}`;
62
63
  const websiteLine = createContentLine(websiteText, '║', '║', BANNER_WIDTH, 'center');
63
64
  const asciiLines = [
64
65
  ' █ █ ▄▀▄ █ ▄▀▄ ',
65
66
  ' █ ▄▄ █▀█ █ █ █ ',
66
- ' █ █ █ █ █ ▀▄▀ '
67
+ ' █ █ █ █ █ ▀▄▀ ',
67
68
  ].map(line => createContentLine(line, '║', '║', BANNER_WIDTH, 'center'));
68
69
  const bannerLines = [
69
70
  createBorderLine('╔', '╗', '═', BANNER_WIDTH),
@@ -73,9 +74,9 @@ export class Wizard {
73
74
  titleLine,
74
75
  subtitleLine,
75
76
  websiteLine,
76
- createBorderLine('╚', '╝', '═', BANNER_WIDTH)
77
+ createBorderLine('╚', '╝', '═', BANNER_WIDTH),
77
78
  ];
78
- console.log(chalk.cyan.bold('\n' + bannerLines.join('\n')));
79
+ console.log(chalk.cyan.bold(`\n${bannerLines.join('\n')}`));
79
80
  }
80
81
  resetScreen() {
81
82
  console.clear();
@@ -84,8 +85,8 @@ export class Wizard {
84
85
  async runFirstTimeSetup() {
85
86
  // 清屏并显示欢迎信息
86
87
  this.resetScreen();
87
- console.log(chalk.cyan.bold('\n' + i18n.t('wizard.welcome')));
88
- console.log(chalk.gray(i18n.t('wizard.privacy_note') + '\n'));
88
+ console.log(chalk.cyan.bold(`\n${i18n.t('wizard.welcome')}`));
89
+ console.log(chalk.gray(`${i18n.t('wizard.privacy_note')}\n`));
89
90
  // Step 1: Select language
90
91
  await this.configLanguage();
91
92
  // Step 2: Input API key
@@ -94,35 +95,32 @@ export class Wizard {
94
95
  await this.showMainMenu();
95
96
  }
96
97
  async configLanguage() {
97
- while (true) {
98
- this.resetScreen();
99
- this.createBox(i18n.t('wizard.select_language'));
100
- const currentLanguage = i18n.getLocale();
101
- const { language } = await this.promptWithHints([
102
- {
103
- type: 'list',
104
- name: 'language',
105
- message: '✨ ' + i18n.t('wizard.select_language'),
106
- choices: [
107
- { name: '[EN] English' + (currentLanguage === 'en_US' ? chalk.green(' ✓ (' + i18n.t('wizard.current_active') + ')') : ''), value: 'en_US' },
108
- { name: '[CN] 中文' + (currentLanguage === 'zh_CN' ? chalk.green(' ✓ (' + i18n.t('wizard.current_active') + ')') : ''), value: 'zh_CN' },
109
- new inquirer.Separator(),
110
- { name: '<- ' + i18n.t('wizard.nav_return'), value: 'back' },
111
- { name: 'x ' + i18n.t('wizard.nav_exit'), value: 'exit' }
112
- ],
113
- default: 'zh_CN'
114
- }
115
- ]);
116
- if (language === 'exit') {
117
- console.log(chalk.green('\n👋 ' + i18n.t('wizard.goodbye_message')));
118
- process.exit(0);
119
- }
120
- else if (language === 'back') {
121
- return;
122
- }
123
- configManager.setLang(language);
124
- i18n.setLocale(language);
125
- return;
98
+ this.resetScreen();
99
+ this.createBox(i18n.t('wizard.select_language'));
100
+ const currentLanguage = i18n.getLocale();
101
+ const { language } = await this.promptWithHints([
102
+ {
103
+ type: 'list',
104
+ name: 'language',
105
+ message: `✨ ${i18n.t('wizard.select_language')}`,
106
+ choices: [
107
+ { name: `[EN] English${currentLanguage === 'en_US' ? chalk.green(` ✓ (${i18n.t('wizard.current_active')})`) : ''}`, value: 'en_US' },
108
+ { name: `[CN] 中文${currentLanguage === 'zh_CN' ? chalk.green(` ✓ (${i18n.t('wizard.current_active')})`) : ''}`, value: 'zh_CN' },
109
+ new inquirer.Separator(),
110
+ { name: `<- ${i18n.t('wizard.nav_return')}`, value: 'back' },
111
+ { name: `x ${i18n.t('wizard.nav_exit')}`, value: 'exit' },
112
+ ],
113
+ default: 'zh_CN',
114
+ },
115
+ ]);
116
+ if (language === 'exit') {
117
+ console.log(chalk.green(`\n👋 ${i18n.t('wizard.goodbye_message')}`));
118
+ process.exit(0);
119
+ }
120
+ else {
121
+ await i18n.setLocale(language);
122
+ console.log(chalk.green(`\n✨ ${i18n.t('wizard.language_set')}`));
123
+ await new Promise(resolve => setTimeout(resolve, 1500));
126
124
  }
127
125
  }
128
126
  async configApiKey() {
@@ -131,11 +129,11 @@ export class Wizard {
131
129
  this.createBox(i18n.t('wizard.config_api_key'));
132
130
  // 显示获取 API Key 的引导链接
133
131
  const apiKeyLink = terminalLink(i18n.t('wizard.get_api_key_link_text'), API_KEYS_URL, { fallback: () => API_KEYS_URL });
134
- console.log(chalk.blue(' ' + i18n.t('wizard.get_api_key_hint') + ' ' + apiKeyLink));
132
+ console.log(chalk.blue(` ${i18n.t('wizard.get_api_key_hint')} ${apiKeyLink}`));
135
133
  console.log('');
136
134
  const currentConfig = configManager.getConfig();
137
135
  if (currentConfig.api_key) {
138
- console.log(chalk.gray(' ' + i18n.t('wizard.config_api_key') + ' ') + chalk.gray(i18n.t('wizard.api_key_set') + ' (' + currentConfig.api_key.slice(0, 4) + '****)'));
136
+ console.log(chalk.gray(` ${i18n.t('wizard.config_api_key')} `) + chalk.gray(`${i18n.t('wizard.api_key_set')} (${currentConfig.api_key.slice(0, 4)}****)`));
139
137
  console.log('');
140
138
  }
141
139
  const { action } = await this.promptWithHints([
@@ -144,15 +142,15 @@ export class Wizard {
144
142
  name: 'action',
145
143
  message: i18n.t('wizard.select_action'),
146
144
  choices: [
147
- { name: '> ' + (currentConfig.api_key ? i18n.t("wizard.update_api_key") : i18n.t('wizard.input_api_key')), value: 'input' },
145
+ { name: `> ${currentConfig.api_key ? i18n.t('wizard.update_api_key') : i18n.t('wizard.input_api_key')}`, value: 'input' },
148
146
  new inquirer.Separator(),
149
- { name: '<- ' + i18n.t('wizard.nav_return'), value: 'back' },
150
- { name: 'x ' + i18n.t('wizard.nav_exit'), value: 'exit' }
151
- ]
152
- }
147
+ { name: `<- ${i18n.t('wizard.nav_return')}`, value: 'back' },
148
+ { name: `x ${i18n.t('wizard.nav_exit')}`, value: 'exit' },
149
+ ],
150
+ },
153
151
  ]);
154
152
  if (action === 'exit') {
155
- console.log(chalk.green('\n👋 ' + i18n.t('wizard.goodbye_message')));
153
+ console.log(chalk.green(`\n👋 ${i18n.t('wizard.goodbye_message')}`));
156
154
  process.exit(0);
157
155
  }
158
156
  else if (action === 'back') {
@@ -169,16 +167,16 @@ export class Wizard {
169
167
  message: i18n.t('wizard.input_your_api_key'),
170
168
  validate: (input) => {
171
169
  if (!input || input.trim().length === 0) {
172
- return '[!] ' + i18n.t('wizard.api_key_required');
170
+ return `[!] ${i18n.t('wizard.api_key_required')}`;
173
171
  }
174
172
  return true;
175
- }
176
- }
173
+ },
174
+ },
177
175
  ]);
178
176
  // Validate API Key
179
177
  const spinner = ora({
180
178
  text: i18n.t('wizard.validating_api_key'),
181
- spinner: 'star2'
179
+ spinner: 'star2',
182
180
  }).start();
183
181
  const validationResult = await validateApiKey(apiKey.trim());
184
182
  await new Promise(resolve => setTimeout(resolve, 800));
@@ -195,7 +193,7 @@ export class Wizard {
195
193
  configManager.setApiKey(apiKey.trim());
196
194
  // Clear cached models when API key changes
197
195
  this.cachedModels = [];
198
- spinner.succeed("✅ " + i18n.t('wizard.set_success'));
196
+ spinner.succeed(`✅ ${i18n.t('wizard.set_success')}`);
199
197
  await new Promise(resolve => setTimeout(resolve, 600));
200
198
  // 直接返回,不再跳转到模型选择
201
199
  return;
@@ -227,10 +225,10 @@ export class Wizard {
227
225
  const currentConfig = configManager.getConfig();
228
226
  const models = currentConfig;
229
227
  // Display current model configuration
230
- console.log(chalk.cyan.bold(i18n.t('wizard.models_config_title') + ':'));
231
- console.log(chalk.gray(' ' + i18n.t('wizard.current_haiku_model') + ': ') + (models.haikuModel ? chalk.green(models.haikuModel) : chalk.red(i18n.t('wizard.not_set'))));
232
- console.log(chalk.gray(' ' + i18n.t('wizard.current_sonnet_model') + ': ') + (models.sonnetModel ? chalk.green(models.sonnetModel) : chalk.red(i18n.t('wizard.not_set'))));
233
- console.log(chalk.gray(' ' + i18n.t('wizard.current_opus_model') + ': ') + (models.opusModel ? chalk.green(models.opusModel) : chalk.red(i18n.t('wizard.not_set'))));
228
+ console.log(chalk.cyan.bold(`${i18n.t('wizard.models_config_title')}:`));
229
+ console.log(chalk.gray(` ${i18n.t('wizard.current_haiku_model')}: `) + (models.haikuModel ? chalk.green(models.haikuModel) : chalk.red(i18n.t('wizard.not_set'))));
230
+ console.log(chalk.gray(` ${i18n.t('wizard.current_sonnet_model')}: `) + (models.sonnetModel ? chalk.green(models.sonnetModel) : chalk.red(i18n.t('wizard.not_set'))));
231
+ console.log(chalk.gray(` ${i18n.t('wizard.current_opus_model')}: `) + (models.opusModel ? chalk.green(models.opusModel) : chalk.red(i18n.t('wizard.not_set'))));
234
232
  console.log('');
235
233
  const { action } = await this.promptWithHints([
236
234
  {
@@ -238,15 +236,15 @@ export class Wizard {
238
236
  name: 'action',
239
237
  message: i18n.t('wizard.select_action'),
240
238
  choices: [
241
- { name: '> ' + i18n.t('wizard.configure_models'), value: 'configure' },
239
+ { name: `> ${i18n.t('wizard.configure_models')}`, value: 'configure' },
242
240
  new inquirer.Separator(),
243
- { name: '<- ' + i18n.t('wizard.nav_return'), value: 'back' },
244
- { name: 'x ' + i18n.t('wizard.nav_exit'), value: 'exit' }
245
- ]
246
- }
241
+ { name: `<- ${i18n.t('wizard.nav_return')}`, value: 'back' },
242
+ { name: `x ${i18n.t('wizard.nav_exit')}`, value: 'exit' },
243
+ ],
244
+ },
247
245
  ]);
248
246
  if (action === 'exit') {
249
- console.log(chalk.green('\n👋 ' + i18n.t('wizard.goodbye_message')));
247
+ console.log(chalk.green(`\n👋 ${i18n.t('wizard.goodbye_message')}`));
250
248
  process.exit(0);
251
249
  }
252
250
  else if (action === 'back') {
@@ -260,17 +258,22 @@ export class Wizard {
260
258
  /**
261
259
  * Step-by-step model selection with back navigation
262
260
  */
263
- async selectModels() {
261
+ async selectModels(toolName) {
262
+ // OpenCode 使用独立的模型选择流程
263
+ if (toolName === 'opencode') {
264
+ await this.selectModelsForOpenCode();
265
+ return;
266
+ }
264
267
  const apiKey = configManager.getApiKey();
265
268
  if (!apiKey) {
266
- console.log(chalk.red('\n' + i18n.t('wizard.no_models_available')));
269
+ console.log(chalk.red(`\n${i18n.t('wizard.no_models_available')}`));
267
270
  await new Promise(resolve => setTimeout(resolve, 1500));
268
271
  return;
269
272
  }
270
273
  // Fetch available models
271
274
  const spinner = ora({
272
275
  text: i18n.t('wizard.fetching_models'),
273
- spinner: 'star2'
276
+ spinner: 'star2',
274
277
  }).start();
275
278
  const availableModels = await this.fetchModels();
276
279
  if (availableModels.length === 0) {
@@ -281,21 +284,20 @@ export class Wizard {
281
284
  spinner.succeed(chalk.green(i18n.t('wizard.api_key_valid')));
282
285
  await new Promise(resolve => setTimeout(resolve, 300));
283
286
  const currentConfig = configManager.getConfig();
284
- const modelTypes = ['haiku', 'sonnet', 'opus'];
285
287
  const titleKeys = [
286
288
  'wizard.select_haiku_model',
287
289
  'wizard.select_sonnet_model',
288
- 'wizard.select_opus_model'
290
+ 'wizard.select_opus_model',
289
291
  ];
290
292
  const defaultModels = [
291
293
  DEFAULT_CONFIG.ANTHROPIC_DEFAULT_HAIKU_MODEL,
292
294
  DEFAULT_CONFIG.ANTHROPIC_DEFAULT_SONNET_MODEL,
293
- DEFAULT_CONFIG.ANTHROPIC_DEFAULT_OPUS_MODEL
295
+ DEFAULT_CONFIG.ANTHROPIC_DEFAULT_OPUS_MODEL,
294
296
  ];
295
297
  const currentModels = [
296
298
  currentConfig.haikuModel,
297
299
  currentConfig.sonnetModel,
298
- currentConfig.opusModel
300
+ currentConfig.opusModel,
299
301
  ];
300
302
  // Selected models (initialize with current config)
301
303
  const selectedModels = [...currentModels];
@@ -305,16 +307,16 @@ export class Wizard {
305
307
  this.createBox(i18n.t(titleKeys[step]));
306
308
  // Build choices with back option at top
307
309
  const choices = [
308
- { name: '<- ' + i18n.t('wizard.nav_go_back'), value: 'back' },
309
- new inquirer.Separator()
310
+ { name: `<- ${i18n.t('wizard.nav_go_back')}`, value: 'back' },
311
+ new inquirer.Separator(),
310
312
  ];
311
313
  // Add model choices
312
- availableModels.forEach(model => {
314
+ availableModels.forEach((model) => {
313
315
  const isCurrentActive = currentModels[step] === model;
314
316
  const isSelected = selectedModels[step] === model;
315
317
  let label = model;
316
318
  if (isCurrentActive) {
317
- label += chalk.green(' ✓ (' + i18n.t('wizard.current_active') + ')');
319
+ label += chalk.green(` ✓ (${i18n.t('wizard.current_active')})`);
318
320
  }
319
321
  else if (isSelected && !isCurrentActive) {
320
322
  label += chalk.cyan(' ✓');
@@ -328,8 +330,8 @@ export class Wizard {
328
330
  name: 'model',
329
331
  message: i18n.t(titleKeys[step]),
330
332
  choices,
331
- default: selectedModels[step] || defaultModels[step]
332
- }
333
+ default: selectedModels[step] || defaultModels[step],
334
+ },
333
335
  ]);
334
336
  if (model === 'back') {
335
337
  step--; // Go back to previous step
@@ -350,13 +352,121 @@ export class Wizard {
350
352
  // Save the configuration
351
353
  const saveSpinner = ora({
352
354
  text: i18n.t('wizard.saving_model_config'),
353
- spinner: 'star2'
355
+ spinner: 'star2',
354
356
  }).start();
355
357
  try {
356
358
  configManager.setModels({
357
359
  haikuModel: selectedModels[0],
358
360
  sonnetModel: selectedModels[1],
359
- opusModel: selectedModels[2]
361
+ opusModel: selectedModels[2],
362
+ });
363
+ await new Promise(resolve => setTimeout(resolve, 600));
364
+ saveSpinner.succeed(chalk.green(i18n.t('wizard.model_config_saved')));
365
+ await new Promise(resolve => setTimeout(resolve, 1500));
366
+ }
367
+ catch (error) {
368
+ saveSpinner.fail(i18n.t('wizard.model_config_failed'));
369
+ console.error(error);
370
+ await new Promise(resolve => setTimeout(resolve, 1500));
371
+ }
372
+ }
373
+ /**
374
+ * OpenCode 专用的模型选择流程(2 步:主模型 + 小模型)
375
+ */
376
+ async selectModelsForOpenCode() {
377
+ const apiKey = configManager.getApiKey();
378
+ if (!apiKey) {
379
+ console.log(chalk.red(`\n${i18n.t('wizard.no_models_available')}`));
380
+ await new Promise(resolve => setTimeout(resolve, 1500));
381
+ return;
382
+ }
383
+ // Fetch available models
384
+ const spinner = ora({
385
+ text: i18n.t('wizard.fetching_models'),
386
+ spinner: 'star2',
387
+ }).start();
388
+ const availableModels = await this.fetchModels();
389
+ if (availableModels.length === 0) {
390
+ spinner.fail(chalk.red(i18n.t('wizard.fetch_models_failed')));
391
+ await new Promise(resolve => setTimeout(resolve, 1500));
392
+ return;
393
+ }
394
+ spinner.succeed(chalk.green(i18n.t('wizard.api_key_valid')));
395
+ await new Promise(resolve => setTimeout(resolve, 300));
396
+ const currentConfig = configManager.getConfig();
397
+ // OpenCode: 使用独立的 openCodeModel 和 openCodeSmallModel
398
+ const titleKeys = [
399
+ 'wizard.select_model',
400
+ 'wizard.select_small_model',
401
+ ];
402
+ const defaultModels = [
403
+ OPENCODE_DEFAULT_CONFIG.DEFAULT_MODEL,
404
+ OPENCODE_DEFAULT_CONFIG.DEFAULT_SMALL_MODEL,
405
+ ];
406
+ const currentModels = [
407
+ currentConfig.openCodeModel,
408
+ currentConfig.openCodeSmallModel,
409
+ ];
410
+ // Selected models (initialize with current config)
411
+ const selectedModels = [...currentModels];
412
+ let step = 0;
413
+ while (step >= 0 && step < 2) {
414
+ this.resetScreen();
415
+ this.createBox(i18n.t(titleKeys[step]));
416
+ // Build choices with back option at top
417
+ const choices = [
418
+ { name: `<- ${i18n.t('wizard.nav_go_back')}`, value: 'back' },
419
+ new inquirer.Separator(),
420
+ ];
421
+ // Add model choices
422
+ availableModels.forEach((model) => {
423
+ const isCurrentActive = currentModels[step] === model;
424
+ const isSelected = selectedModels[step] === model;
425
+ let label = model;
426
+ if (isCurrentActive) {
427
+ label += chalk.green(` ✓ (${i18n.t('wizard.current_active')})`);
428
+ }
429
+ else if (isSelected && !isCurrentActive) {
430
+ label += chalk.cyan(' ✓');
431
+ }
432
+ choices.push({ name: label, value: model });
433
+ });
434
+ try {
435
+ const { model } = await this.promptWithHints([
436
+ {
437
+ type: 'list',
438
+ name: 'model',
439
+ message: i18n.t(titleKeys[step]),
440
+ choices,
441
+ default: selectedModels[step] || defaultModels[step],
442
+ },
443
+ ]);
444
+ if (model === 'back') {
445
+ step--; // Go back to previous step
446
+ }
447
+ else {
448
+ selectedModels[step] = model;
449
+ step++; // Move to next step
450
+ }
451
+ }
452
+ catch {
453
+ return; // User pressed Ctrl+C
454
+ }
455
+ }
456
+ // If step < 0, user went back from first step, exit without saving
457
+ if (step < 0) {
458
+ return;
459
+ }
460
+ // Save the configuration
461
+ const saveSpinner = ora({
462
+ text: i18n.t('wizard.saving_model_config'),
463
+ spinner: 'star2',
464
+ }).start();
465
+ try {
466
+ // OpenCode: 使用独立的配置键
467
+ configManager.setOpenCodeModels({
468
+ openCodeModel: selectedModels[0],
469
+ openCodeSmallModel: selectedModels[1],
360
470
  });
361
471
  await new Promise(resolve => setTimeout(resolve, 600));
362
472
  saveSpinner.succeed(chalk.green(i18n.t('wizard.model_config_saved')));
@@ -375,19 +485,19 @@ export class Wizard {
375
485
  const supportedTools = toolManager.getSupportedTools();
376
486
  const toolChoices = supportedTools.map(tool => ({
377
487
  name: `> ${tool.displayName}`,
378
- value: tool.name
488
+ value: tool.name,
379
489
  }));
380
- toolChoices.push(new inquirer.Separator(), { name: '<- ' + i18n.t('wizard.nav_return'), value: 'back' }, { name: 'x ' + i18n.t('wizard.nav_exit'), value: 'exit' });
490
+ toolChoices.push(new inquirer.Separator(), { name: `<- ${i18n.t('wizard.nav_return')}`, value: 'back' }, { name: `x ${i18n.t('wizard.nav_exit')}`, value: 'exit' });
381
491
  const { selectedTool } = await this.promptWithHints([
382
492
  {
383
493
  type: 'list',
384
494
  name: 'selectedTool',
385
495
  message: i18n.t('wizard.select_tool'),
386
- choices: toolChoices
387
- }
496
+ choices: toolChoices,
497
+ },
388
498
  ]);
389
499
  if (selectedTool === 'exit') {
390
- console.log(chalk.green('\n👋 ' + i18n.t('wizard.goodbye_message')));
500
+ console.log(chalk.green(`\n👋 ${i18n.t('wizard.goodbye_message')}`));
391
501
  process.exit(0);
392
502
  }
393
503
  else if (selectedTool === 'back') {
@@ -405,8 +515,8 @@ export class Wizard {
405
515
  type: 'confirm',
406
516
  name: 'shouldInstall',
407
517
  message: i18n.t('wizard.install_tool_confirm'),
408
- default: true
409
- }
518
+ default: true,
519
+ },
410
520
  ]);
411
521
  if (shouldInstall) {
412
522
  try {
@@ -427,9 +537,9 @@ export class Wizard {
427
537
  message: i18n.t('install.skip_install_confirm'),
428
538
  choices: [
429
539
  { name: i18n.t('install.skip_install_yes'), value: true },
430
- { name: i18n.t('install.skip_install_no'), value: false }
431
- ]
432
- }
540
+ { name: i18n.t('install.skip_install_no'), value: false },
541
+ ],
542
+ },
433
543
  ]);
434
544
  if (!skipInstall) {
435
545
  return;
@@ -452,26 +562,26 @@ export class Wizard {
452
562
  const currentCfg = configManager.getConfig();
453
563
  this.createBox(i18n.t('wizard.main_menu_title'));
454
564
  // 显示当前配置状态(仅 API Key)
455
- console.log(chalk.gray(' ' + i18n.t('wizard.config_api_key') + ': ') + (currentCfg.api_key ? chalk.gray(i18n.t('wizard.api_key_set') + ' (' + currentCfg.api_key.slice(0, 4) + '****)') : chalk.red(i18n.t('wizard.not_set'))));
565
+ console.log(chalk.gray(` ${i18n.t('wizard.config_api_key')}: `) + (currentCfg.api_key ? chalk.gray(`${i18n.t('wizard.api_key_set')} (${currentCfg.api_key.slice(0, 4)}****)`) : chalk.red(i18n.t('wizard.not_set'))));
456
566
  console.log('');
457
567
  const choices = [
458
- { name: '> ' + i18n.t('wizard.menu_config_language'), value: 'lang' },
459
- { name: '> ' + i18n.t('wizard.menu_config_api_key'), value: 'apikey' },
460
- { name: '> ' + i18n.t('wizard.menu_config_tool'), value: 'tool' },
568
+ { name: `> ${i18n.t('wizard.menu_config_language')}`, value: 'lang' },
569
+ { name: `> ${i18n.t('wizard.menu_config_api_key')}`, value: 'apikey' },
570
+ { name: `> ${i18n.t('wizard.menu_config_tool')}`, value: 'tool' },
461
571
  new inquirer.Separator(),
462
- { name: '> ' + i18n.t('wizard.menu_open_website'), value: 'website' },
463
- { name: 'x ' + i18n.t('wizard.menu_exit'), value: 'exit' }
572
+ { name: `> ${i18n.t('wizard.menu_open_website')}`, value: 'website' },
573
+ { name: `x ${i18n.t('wizard.menu_exit')}`, value: 'exit' },
464
574
  ];
465
575
  const { action } = await this.promptWithHints([
466
576
  {
467
577
  type: 'list',
468
578
  name: 'action',
469
579
  message: i18n.t('wizard.select_operation'),
470
- choices
471
- }
580
+ choices,
581
+ },
472
582
  ]);
473
583
  if (action === 'exit') {
474
- console.log(chalk.green('\n👋 ' + i18n.t('wizard.goodbye_message')));
584
+ console.log(chalk.green(`\n👋 ${i18n.t('wizard.goodbye_message')}`));
475
585
  process.exit(0);
476
586
  }
477
587
  else if (action === 'lang') {
@@ -491,111 +601,141 @@ export class Wizard {
491
601
  async openOfficialWebsite() {
492
602
  const spinner = ora({
493
603
  text: i18n.t('wizard.opening_website'),
494
- spinner: 'star2'
604
+ spinner: 'star2',
495
605
  }).start();
496
606
  try {
497
607
  await open(OFFICIAL_WEBSITE);
498
608
  spinner.succeed(chalk.green(i18n.t('wizard.website_opened')));
499
609
  }
500
- catch (error) {
610
+ catch {
501
611
  spinner.fail(chalk.red(i18n.t('wizard.website_open_failed')));
502
612
  // 显示可点击链接作为备选
503
613
  const link = terminalLink(OFFICIAL_WEBSITE, OFFICIAL_WEBSITE, {
504
- fallback: () => OFFICIAL_WEBSITE
614
+ fallback: () => OFFICIAL_WEBSITE,
505
615
  });
506
- console.log(chalk.blue(' ' + i18n.t('wizard.click_to_open') + ': ' + link));
616
+ console.log(chalk.blue(` ${i18n.t('wizard.click_to_open')}: ${link}`));
507
617
  }
508
618
  await new Promise(resolve => setTimeout(resolve, 1500));
509
619
  }
510
620
  async showToolMenu(toolName) {
511
621
  while (true) {
512
- // for claude code now
513
622
  this.resetScreen();
514
623
  const title = `${SUPPORTED_TOOLS[toolName].displayName} ${i18n.t('wizard.menu_title')}`;
515
624
  this.createBox(title);
516
- if (toolName === 'claude-code') {
517
- console.log(chalk.yellow.bold(i18n.t('wizard.global_config_warning', { tool: 'Claude Code' })));
518
- console.log('');
519
- }
625
+ // 显示全局配置警告
626
+ console.log(chalk.yellow.bold(i18n.t('wizard.global_config_warning', { tool: SUPPORTED_TOOLS[toolName].displayName })));
627
+ console.log('');
520
628
  let actionText = '';
521
629
  const chelperConfig = configManager.getConfig();
522
- const detectedConfig = claudeCodeManager.getModelConfig();
630
+ // 根据工具类型获取检测到的配置
631
+ let detectedConfig = null;
632
+ let configMatches = false;
633
+ if (toolName === 'claude-code') {
634
+ detectedConfig = claudeCodeManager.getModelConfig();
635
+ }
636
+ else if (toolName === 'opencode') {
637
+ detectedConfig = openCodeManager.getModelConfig();
638
+ }
523
639
  // 显示 chelper 配置
524
- console.log(chalk.cyan.bold(i18n.t('wizard.chelper_config_title') + ':'));
640
+ console.log(chalk.cyan.bold(`${i18n.t('wizard.chelper_config_title')}:`));
525
641
  if (chelperConfig.api_key) {
526
- console.log(chalk.gray(' ' + i18n.t('wizard.config_api_key') + ': ') + chalk.gray(i18n.t('wizard.api_key_set') + ' (' + chelperConfig.api_key.slice(0, 4) + '****)'));
642
+ console.log(chalk.gray(` ${i18n.t('wizard.config_api_key')}: `) + chalk.gray(`${i18n.t('wizard.api_key_set')} (${chelperConfig.api_key.slice(0, 4)}****)`));
643
+ }
644
+ else {
645
+ console.log(chalk.gray(` ${i18n.t('wizard.config_api_key')}: `) + chalk.red(i18n.t('wizard.not_set')));
646
+ }
647
+ // 根据工具类型显示不同的模型配置
648
+ if (toolName === 'opencode') {
649
+ // OpenCode: 使用独立的 openCodeModel 和 openCodeSmallModel
650
+ console.log(chalk.gray(` ${i18n.t('wizard.current_model')}: `) + (chelperConfig.openCodeModel ? chalk.green(chelperConfig.openCodeModel) : chalk.red(i18n.t('wizard.not_set'))));
651
+ console.log(chalk.gray(` ${i18n.t('wizard.current_small_model')}: `) + (chelperConfig.openCodeSmallModel ? chalk.green(chelperConfig.openCodeSmallModel) : chalk.red(i18n.t('wizard.not_set'))));
527
652
  }
528
653
  else {
529
- console.log(chalk.gray(' ' + i18n.t('wizard.config_api_key') + ': ') + chalk.red(i18n.t('wizard.not_set')));
654
+ // Claude Code: haiku + sonnet + opus
655
+ console.log(chalk.gray(` ${i18n.t('wizard.current_haiku_model')}: `) + (chelperConfig.haikuModel ? chalk.green(chelperConfig.haikuModel) : chalk.red(i18n.t('wizard.not_set'))));
656
+ console.log(chalk.gray(` ${i18n.t('wizard.current_sonnet_model')}: `) + (chelperConfig.sonnetModel ? chalk.green(chelperConfig.sonnetModel) : chalk.red(i18n.t('wizard.not_set'))));
657
+ console.log(chalk.gray(` ${i18n.t('wizard.current_opus_model')}: `) + (chelperConfig.opusModel ? chalk.green(chelperConfig.opusModel) : chalk.red(i18n.t('wizard.not_set'))));
530
658
  }
531
- console.log(chalk.gray(' ' + i18n.t('wizard.current_haiku_model') + ': ') + (chelperConfig.haikuModel ? chalk.green(chelperConfig.haikuModel) : chalk.red(i18n.t('wizard.not_set'))));
532
- console.log(chalk.gray(' ' + i18n.t('wizard.current_sonnet_model') + ': ') + (chelperConfig.sonnetModel ? chalk.green(chelperConfig.sonnetModel) : chalk.red(i18n.t('wizard.not_set'))));
533
- console.log(chalk.gray(' ' + i18n.t('wizard.current_opus_model') + ': ') + (chelperConfig.opusModel ? chalk.green(chelperConfig.opusModel) : chalk.red(i18n.t('wizard.not_set'))));
534
659
  console.log('');
535
- // 显示 Claude Code 当前配置
536
- console.log(chalk.yellow.bold(i18n.t('wizard.claude_code_config_title') + ':'));
660
+ // 显示工具当前配置
661
+ const toolConfigTitle = toolName === 'opencode' ? i18n.t('wizard.opencode_config_title') : i18n.t('wizard.claude_code_config_title');
662
+ console.log(chalk.yellow.bold(`${toolConfigTitle}:`));
537
663
  if (detectedConfig) {
538
- console.log(chalk.gray(' ' + i18n.t('wizard.config_api_key') + ': ') + chalk.gray(i18n.t('wizard.api_key_set') + ' (' + detectedConfig.apiKey.slice(0, 4) + '****)'));
539
- console.log(chalk.gray(' ' + i18n.t('wizard.current_haiku_model') + ': ') + chalk.green(detectedConfig.haikuModel));
540
- console.log(chalk.gray(' ' + i18n.t('wizard.current_sonnet_model') + ': ') + chalk.green(detectedConfig.sonnetModel));
541
- console.log(chalk.gray(' ' + i18n.t('wizard.current_opus_model') + ': ') + chalk.green(detectedConfig.opusModel));
664
+ console.log(chalk.gray(` ${i18n.t('wizard.config_api_key')}: `) + chalk.gray(`${i18n.t('wizard.api_key_set')} (${detectedConfig.apiKey.slice(0, 4)}****)`));
665
+ if (toolName === 'opencode') {
666
+ console.log(chalk.gray(` ${i18n.t('wizard.current_model')}: `) + chalk.green(detectedConfig.model));
667
+ console.log(chalk.gray(` ${i18n.t('wizard.current_small_model')}: `) + chalk.green(detectedConfig.smallModel));
668
+ }
669
+ else {
670
+ console.log(chalk.gray(` ${i18n.t('wizard.current_haiku_model')}: `) + chalk.green(detectedConfig.haikuModel));
671
+ console.log(chalk.gray(` ${i18n.t('wizard.current_sonnet_model')}: `) + chalk.green(detectedConfig.sonnetModel));
672
+ console.log(chalk.gray(` ${i18n.t('wizard.current_opus_model')}: `) + chalk.green(detectedConfig.opusModel));
673
+ }
542
674
  }
543
675
  else {
544
- console.log(chalk.gray(' ' + i18n.t('wizard.config_api_key') + ': ') + chalk.red(i18n.t('wizard.not_set')));
676
+ console.log(chalk.gray(` ${i18n.t('wizard.config_api_key')}: `) + chalk.red(i18n.t('wizard.not_set')));
545
677
  }
546
678
  console.log('');
547
679
  // 判断是否需要刷新配置
548
- const configMatches = detectedConfig &&
549
- detectedConfig.apiKey === chelperConfig.api_key &&
550
- detectedConfig.haikuModel === chelperConfig.haikuModel &&
551
- detectedConfig.sonnetModel === chelperConfig.sonnetModel &&
552
- detectedConfig.opusModel === chelperConfig.opusModel;
680
+ if (toolName === 'opencode') {
681
+ configMatches = detectedConfig
682
+ && detectedConfig.apiKey === chelperConfig.api_key
683
+ && detectedConfig.model === chelperConfig.openCodeModel
684
+ && detectedConfig.smallModel === chelperConfig.openCodeSmallModel;
685
+ }
686
+ else {
687
+ configMatches = detectedConfig
688
+ && detectedConfig.apiKey === chelperConfig.api_key
689
+ && detectedConfig.haikuModel === chelperConfig.haikuModel
690
+ && detectedConfig.sonnetModel === chelperConfig.sonnetModel
691
+ && detectedConfig.opusModel === chelperConfig.opusModel;
692
+ }
553
693
  if (detectedConfig && configMatches) {
554
- // 配置已同步
555
- console.log(chalk.green('✅ ' + i18n.t('wizard.config_synced')));
556
- actionText = i18n.t('wizard.action_refresh_config', { 'tool': SUPPORTED_TOOLS[toolName].displayName });
694
+ console.log(chalk.green(`✅ ${i18n.t('wizard.config_synced')}`));
695
+ actionText = i18n.t('wizard.action_refresh_config', { tool: SUPPORTED_TOOLS[toolName].displayName });
557
696
  }
558
697
  else if (detectedConfig) {
559
- // 配置不一致,需要刷新
560
- console.log(chalk.yellow('⚠️ ' + i18n.t('wizard.config_out_of_sync')));
561
- actionText = i18n.t('wizard.action_refresh_config', { 'tool': SUPPORTED_TOOLS[toolName].displayName });
698
+ console.log(chalk.yellow(`⚠️ ${i18n.t('wizard.config_out_of_sync')}`));
699
+ actionText = i18n.t('wizard.action_refresh_config', { tool: SUPPORTED_TOOLS[toolName].displayName });
562
700
  }
563
701
  else {
564
- // 未配置,需要装载
565
- console.log(chalk.blue('ℹ️ ' + i18n.t('wizard.config_not_loaded')));
566
- actionText = i18n.t('wizard.action_load_config', { 'tool': SUPPORTED_TOOLS[toolName].displayName });
702
+ console.log(chalk.blue(`ℹ️ ${i18n.t('wizard.config_not_loaded')}`));
703
+ actionText = i18n.t('wizard.action_load_config', { tool: SUPPORTED_TOOLS[toolName].displayName });
567
704
  }
568
705
  console.log('');
569
706
  const choices = [];
570
- // 配置模型选项(移到工具菜单)
571
- choices.push({ name: '> ' + i18n.t('wizard.action_config_models'), value: 'config_models' });
572
- choices.push({ name: '> ' + actionText, value: 'load_config' });
573
- // 如果已经配置了,显示卸载选项
707
+ // 根据工具类型显示不同的配置模型文案
708
+ if (toolName === 'opencode') {
709
+ choices.push({ name: `> ${i18n.t('wizard.action_config_models_opencode')}`, value: 'config_models' });
710
+ }
711
+ else {
712
+ choices.push({ name: `> ${i18n.t('wizard.action_config_models')}`, value: 'config_models' });
713
+ }
714
+ choices.push({ name: `> ${actionText}`, value: 'load_config' });
574
715
  if (detectedConfig) {
575
- choices.push({ name: '> ' + i18n.t('wizard.action_unload_config', { 'tool': SUPPORTED_TOOLS[toolName].displayName }), value: 'unload_config' });
716
+ choices.push({ name: `> ${i18n.t('wizard.action_unload_config', { tool: SUPPORTED_TOOLS[toolName].displayName })}`, value: 'unload_config' });
576
717
  }
577
- // 如果已经配置了,显示启动选项
578
718
  if (detectedConfig) {
579
- choices.push({ name: '> ' + i18n.t('wizard.start_tool', { 'tool': SUPPORTED_TOOLS[toolName].displayName, 'shell': SUPPORTED_TOOLS[toolName].command }), value: 'start_tool' });
719
+ choices.push({ name: `> ${i18n.t('wizard.start_tool', { tool: SUPPORTED_TOOLS[toolName].displayName, shell: SUPPORTED_TOOLS[toolName].command })}`, value: 'start_tool' });
580
720
  }
581
- choices.push(new inquirer.Separator(), { name: '<- ' + i18n.t('wizard.nav_return'), value: 'back' }, { name: 'x ' + i18n.t('wizard.nav_exit'), value: 'exit' });
721
+ choices.push(new inquirer.Separator(), { name: `<- ${i18n.t('wizard.nav_return')}`, value: 'back' }, { name: `x ${i18n.t('wizard.nav_exit')}`, value: 'exit' });
582
722
  const { action } = await this.promptWithHints([
583
723
  {
584
724
  type: 'list',
585
725
  name: 'action',
586
726
  message: i18n.t('wizard.select_action'),
587
- choices
588
- }
727
+ choices,
728
+ },
589
729
  ]);
590
730
  if (action === 'exit') {
591
- console.log(chalk.green('\n👋 ' + i18n.t('wizard.goodbye_message')));
731
+ console.log(chalk.green(`\n👋 ${i18n.t('wizard.goodbye_message')}`));
592
732
  process.exit(0);
593
733
  }
594
734
  else if (action === 'back') {
595
735
  return;
596
736
  }
597
737
  else if (action === 'config_models') {
598
- await this.selectModels();
738
+ await this.selectModels(toolName);
599
739
  }
600
740
  else if (action === 'load_config') {
601
741
  await this.loadModelConfig(toolName);
@@ -622,12 +762,12 @@ export class Wizard {
622
762
  name: 'startOption',
623
763
  message: i18n.t('wizard.select_start_directory'),
624
764
  choices: [
625
- { name: '> ' + i18n.t('wizard.start_from_current_dir') + ' (' + process.cwd() + ')', value: 'current' },
626
- { name: '> ' + i18n.t('wizard.start_from_custom_dir'), value: 'custom' },
765
+ { name: `> ${i18n.t('wizard.start_from_current_dir')} (${process.cwd()})`, value: 'current' },
766
+ { name: `> ${i18n.t('wizard.start_from_custom_dir')}`, value: 'custom' },
627
767
  new inquirer.Separator(),
628
- { name: '<- ' + i18n.t('wizard.nav_return'), value: 'back' }
629
- ]
630
- }
768
+ { name: `<- ${i18n.t('wizard.nav_return')}`, value: 'back' },
769
+ ],
770
+ },
631
771
  ]);
632
772
  if (startOption === 'back') {
633
773
  return;
@@ -647,14 +787,14 @@ export class Wizard {
647
787
  return i18n.t('wizard.directory_not_exist');
648
788
  }
649
789
  return true;
650
- }
651
- }
790
+ },
791
+ },
652
792
  ]);
653
793
  workingDir = customDir.trim();
654
794
  }
655
795
  const spinner = ora({
656
796
  text: i18n.t('wizard.starting_tool'),
657
- spinner: 'star2'
797
+ spinner: 'star2',
658
798
  }).start();
659
799
  try {
660
800
  execSync(tool.command, { stdio: 'inherit', cwd: workingDir });
@@ -668,7 +808,7 @@ export class Wizard {
668
808
  async loadModelConfig(toolName) {
669
809
  const spinner = ora({
670
810
  text: i18n.t('wizard.loading_config'),
671
- spinner: 'star2'
811
+ spinner: 'star2',
672
812
  }).start();
673
813
  try {
674
814
  const config = configManager.getConfig();
@@ -681,11 +821,16 @@ export class Wizard {
681
821
  const haikuModel = config.haikuModel || DEFAULT_CONFIG.ANTHROPIC_DEFAULT_HAIKU_MODEL;
682
822
  const sonnetModel = config.sonnetModel || DEFAULT_CONFIG.ANTHROPIC_DEFAULT_SONNET_MODEL;
683
823
  const opusModel = config.opusModel || DEFAULT_CONFIG.ANTHROPIC_DEFAULT_OPUS_MODEL;
684
- toolManager.loadModelConfig(toolName, {
824
+ // OpenCode models
825
+ const openCodeModel = config.openCodeModel || OPENCODE_DEFAULT_CONFIG.DEFAULT_MODEL;
826
+ const openCodeSmallModel = config.openCodeSmallModel || OPENCODE_DEFAULT_CONFIG.DEFAULT_SMALL_MODEL;
827
+ await toolManager.loadModelConfig(toolName, {
685
828
  apiKey: config.api_key,
686
829
  haikuModel,
687
830
  sonnetModel,
688
- opusModel
831
+ opusModel,
832
+ openCodeModel,
833
+ openCodeSmallModel,
689
834
  });
690
835
  await new Promise(resolve => setTimeout(resolve, 800));
691
836
  spinner.succeed(chalk.green(i18n.t('wizard.config_loaded', { tool: SUPPORTED_TOOLS[toolName].displayName })));
@@ -704,25 +849,31 @@ export class Wizard {
704
849
  type: 'confirm',
705
850
  name: 'confirm',
706
851
  message: i18n.t('wizard.confirm_unload_config', { tool: SUPPORTED_TOOLS[toolName].displayName }),
707
- default: false
708
- }
852
+ default: false,
853
+ },
709
854
  ]);
710
855
  if (!confirm) {
711
856
  return;
712
857
  }
713
858
  const spinner = ora({
714
859
  text: i18n.t('wizard.unloading_config'),
715
- spinner: 'star2'
860
+ spinner: 'star2',
716
861
  }).start();
717
862
  try {
718
863
  if (toolName === 'claude-code') {
719
- // 添加短暂延迟,让动画效果更流畅
720
864
  await new Promise(resolve => setTimeout(resolve, 300));
721
865
  claudeCodeManager.clearModelConfig();
722
866
  await new Promise(resolve => setTimeout(resolve, 500));
723
867
  spinner.succeed(chalk.green(i18n.t('wizard.config_unloaded')));
724
868
  await new Promise(resolve => setTimeout(resolve, 800));
725
869
  }
870
+ else if (toolName === 'opencode') {
871
+ await new Promise(resolve => setTimeout(resolve, 300));
872
+ openCodeManager.clearModelConfig();
873
+ await new Promise(resolve => setTimeout(resolve, 500));
874
+ spinner.succeed(chalk.green(i18n.t('wizard.config_unloaded')));
875
+ await new Promise(resolve => setTimeout(resolve, 800));
876
+ }
726
877
  else {
727
878
  spinner.fail(i18n.t('wizard.tool_not_supported'));
728
879
  await new Promise(resolve => setTimeout(resolve, 800));