@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.
@@ -0,0 +1,238 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ // X-AIO Provider ID
5
+ const XAIO_PROVIDER_ID = 'xaio';
6
+ // Default configuration
7
+ export const OPENCODE_DEFAULT_CONFIG = {
8
+ BASE_URL: 'https://code-api.x-aio.com/v1',
9
+ PROVIDER_NAME: 'Coding Plan By X-AIO',
10
+ DEFAULT_MODEL: 'MiniMax-M2',
11
+ DEFAULT_SMALL_MODEL: 'Qwen3-Coder-30B-A3B-Instruct',
12
+ // Default limits (hardcoded for v1, will be fetched from API in future)
13
+ DEFAULT_CONTEXT: 128000,
14
+ DEFAULT_OUTPUT: 64000,
15
+ // Vision model limits
16
+ VISION_CONTEXT: 64000,
17
+ VISION_OUTPUT: 32000,
18
+ };
19
+ export class OpenCodeManager {
20
+ static instance;
21
+ configPath;
22
+ cachedModels = [];
23
+ constructor() {
24
+ // OpenCode config file paths (cross-platform support)
25
+ // - macOS/Linux: ~/.config/opencode/opencode.json
26
+ // - Windows: %USERPROFILE%\.config\opencode\opencode.json
27
+ this.configPath = join(homedir(), '.config', 'opencode', 'opencode.json');
28
+ }
29
+ static getInstance() {
30
+ if (!OpenCodeManager.instance) {
31
+ OpenCodeManager.instance = new OpenCodeManager();
32
+ }
33
+ return OpenCodeManager.instance;
34
+ }
35
+ /**
36
+ * Ensure config directory exists
37
+ */
38
+ ensureDir(filePath) {
39
+ const dir = dirname(filePath);
40
+ if (!existsSync(dir)) {
41
+ mkdirSync(dir, { recursive: true });
42
+ }
43
+ }
44
+ /**
45
+ * Read opencode.json config
46
+ */
47
+ getConfig() {
48
+ try {
49
+ if (existsSync(this.configPath)) {
50
+ const content = readFileSync(this.configPath, 'utf-8');
51
+ return JSON.parse(content);
52
+ }
53
+ }
54
+ catch (error) {
55
+ console.warn('Failed to read OpenCode config:', error);
56
+ }
57
+ return {};
58
+ }
59
+ /**
60
+ * Save opencode.json config
61
+ */
62
+ saveConfig(config) {
63
+ try {
64
+ this.ensureDir(this.configPath);
65
+ writeFileSync(this.configPath, JSON.stringify(config, null, 2), 'utf-8');
66
+ }
67
+ catch (error) {
68
+ throw new Error(`Failed to save OpenCode config: ${error}`);
69
+ }
70
+ }
71
+ /**
72
+ * Fetch available models from API
73
+ */
74
+ async fetchAvailableModels(apiKey) {
75
+ try {
76
+ const controller = new AbortController();
77
+ const timeoutId = setTimeout(() => controller.abort(), 30000);
78
+ const response = await fetch(`${OPENCODE_DEFAULT_CONFIG.BASE_URL}/models`, {
79
+ method: 'GET',
80
+ headers: {
81
+ 'Authorization': `Bearer ${apiKey}`,
82
+ 'Content-Type': 'application/json',
83
+ },
84
+ signal: controller.signal,
85
+ });
86
+ clearTimeout(timeoutId);
87
+ if (!response.ok) {
88
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
89
+ }
90
+ const data = await response.json();
91
+ if (data && data.data && Array.isArray(data.data)) {
92
+ this.cachedModels = data.data.map((model) => ({
93
+ id: model.id,
94
+ name: model.name || model.id,
95
+ context_length: model.context_length,
96
+ max_output_tokens: model.max_output_tokens,
97
+ }));
98
+ return this.cachedModels;
99
+ }
100
+ return [];
101
+ }
102
+ catch (error) {
103
+ console.warn('Failed to fetch available models:', error);
104
+ return [];
105
+ }
106
+ }
107
+ /**
108
+ * Check if model is a vision model (contains "VL" or ends with "V")
109
+ */
110
+ isVisionModel(modelId) {
111
+ const upperCaseId = modelId.toUpperCase();
112
+ return upperCaseId.includes('VL') || upperCaseId.endsWith('V');
113
+ }
114
+ /**
115
+ * Build models object from fetched model list
116
+ */
117
+ buildModelsConfig(modelInfos) {
118
+ const models = {};
119
+ for (const info of modelInfos) {
120
+ const entry = {};
121
+ const isVision = this.isVisionModel(info.id);
122
+ // Set display name
123
+ if (info.name && info.name !== info.id) {
124
+ entry.name = info.name;
125
+ }
126
+ // Set limits (use API values if available, otherwise use defaults)
127
+ // TODO: In future versions, prioritize context_length/max_output_tokens from API
128
+ if (isVision) {
129
+ entry.limit = {
130
+ context: info.context_length || OPENCODE_DEFAULT_CONFIG.VISION_CONTEXT,
131
+ output: info.max_output_tokens || OPENCODE_DEFAULT_CONFIG.VISION_OUTPUT,
132
+ };
133
+ // Add modalities for vision models
134
+ entry.modalities = {
135
+ input: ['text', 'image', 'video'],
136
+ output: ['text'],
137
+ };
138
+ }
139
+ else {
140
+ entry.limit = {
141
+ context: info.context_length || OPENCODE_DEFAULT_CONFIG.DEFAULT_CONTEXT,
142
+ output: info.max_output_tokens || OPENCODE_DEFAULT_CONFIG.DEFAULT_OUTPUT,
143
+ };
144
+ }
145
+ models[info.id] = entry;
146
+ }
147
+ return models;
148
+ }
149
+ /**
150
+ * Save model config to OpenCode (merge, not overwrite)
151
+ */
152
+ async saveModelConfig(config) {
153
+ // Fetch all available models from API
154
+ let modelInfos = this.cachedModels;
155
+ if (modelInfos.length === 0) {
156
+ modelInfos = await this.fetchAvailableModels(config.apiKey);
157
+ }
158
+ // Build models config from API response
159
+ const modelsConfig = this.buildModelsConfig(modelInfos);
160
+ // If no models fetched, at least include the selected ones
161
+ if (Object.keys(modelsConfig).length === 0) {
162
+ modelsConfig[config.model] = {};
163
+ if (config.smallModel && config.smallModel !== config.model) {
164
+ modelsConfig[config.smallModel] = {};
165
+ }
166
+ }
167
+ // Read current config (preserve user's other settings)
168
+ const currentConfig = this.getConfig();
169
+ // Build xaio provider config
170
+ const xaioProvider = {
171
+ npm: '@ai-sdk/openai-compatible',
172
+ name: OPENCODE_DEFAULT_CONFIG.PROVIDER_NAME,
173
+ options: {
174
+ baseURL: config.baseUrl || OPENCODE_DEFAULT_CONFIG.BASE_URL,
175
+ apiKey: config.apiKey,
176
+ },
177
+ models: modelsConfig,
178
+ };
179
+ // Merge config: only update xaio provider, preserve other providers
180
+ const newConfig = {
181
+ ...currentConfig,
182
+ $schema: 'https://opencode.ai/config.json',
183
+ model: `${XAIO_PROVIDER_ID}/${config.model}`,
184
+ small_model: `${XAIO_PROVIDER_ID}/${config.smallModel}`,
185
+ provider: {
186
+ ...currentConfig.provider,
187
+ [XAIO_PROVIDER_ID]: xaioProvider,
188
+ },
189
+ };
190
+ this.saveConfig(newConfig);
191
+ }
192
+ /**
193
+ * Get current model config
194
+ */
195
+ getModelConfig() {
196
+ const config = this.getConfig();
197
+ // Check if xaio provider exists
198
+ const xaioProvider = config.provider?.[XAIO_PROVIDER_ID];
199
+ if (!xaioProvider) {
200
+ return null;
201
+ }
202
+ const apiKey = xaioProvider.options?.apiKey;
203
+ if (!apiKey) {
204
+ return null;
205
+ }
206
+ // Extract model names from "provider/model" format
207
+ const model = config.model?.replace(`${XAIO_PROVIDER_ID}/`, '') || '';
208
+ const smallModel = config.small_model?.replace(`${XAIO_PROVIDER_ID}/`, '') || '';
209
+ return {
210
+ apiKey,
211
+ model,
212
+ smallModel,
213
+ baseUrl: xaioProvider.options?.baseURL || '',
214
+ };
215
+ }
216
+ /**
217
+ * Clear model config (only remove xaio provider)
218
+ */
219
+ clearModelConfig() {
220
+ const currentConfig = this.getConfig();
221
+ // Remove xaio provider from config
222
+ if (currentConfig.provider) {
223
+ delete currentConfig.provider[XAIO_PROVIDER_ID];
224
+ if (Object.keys(currentConfig.provider).length === 0) {
225
+ delete currentConfig.provider;
226
+ }
227
+ }
228
+ // Clear model settings if they use xaio
229
+ if (currentConfig.model?.startsWith(`${XAIO_PROVIDER_ID}/`)) {
230
+ delete currentConfig.model;
231
+ }
232
+ if (currentConfig.small_model?.startsWith(`${XAIO_PROVIDER_ID}/`)) {
233
+ delete currentConfig.small_model;
234
+ }
235
+ this.saveConfig(currentConfig);
236
+ }
237
+ }
238
+ export const openCodeManager = OpenCodeManager.getInstance();
@@ -1,4 +1,4 @@
1
- import { ModelConfig } from './claude-code-manager.js';
1
+ import type { ModelConfig } from './claude-code-manager.js';
2
2
  export interface ToolInfo {
3
3
  name: string;
4
4
  command: string;
@@ -16,7 +16,7 @@ export declare class ToolManager {
16
16
  installTool(toolName: string): Promise<void>;
17
17
  getToolConfig(toolName: string): any;
18
18
  updateToolConfig(toolName: string, config: any): void;
19
- loadModelConfig(toolName: string, config: ModelConfig): void;
19
+ loadModelConfig(toolName: string, config: ModelConfig): Promise<void>;
20
20
  getInstalledTools(): string[];
21
21
  getSupportedTools(): ToolInfo[];
22
22
  isGitInstalled(): boolean;
@@ -1,29 +1,29 @@
1
- import { execSync } from 'child_process';
2
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
3
- import { join, dirname } from 'path';
4
- import { homedir } from 'os';
5
- import { claudeCodeManager } from './claude-code-manager.js';
6
- import { i18n } from './i18n.js';
7
- import inquirer from 'inquirer';
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
8
5
  import chalk from 'chalk';
6
+ import inquirer from 'inquirer';
7
+ import ora from 'ora';
9
8
  import terminalLink from 'terminal-link';
10
- import ora from "ora";
9
+ import { claudeCodeManager } from './claude-code-manager.js';
10
+ import { i18n } from './i18n.js';
11
+ import { openCodeManager } from './opencode-manager.js';
11
12
  export const SUPPORTED_TOOLS = {
12
13
  'claude-code': {
13
14
  name: 'claude-code',
14
15
  command: 'claude',
15
16
  installCommand: 'npm install -g @anthropic-ai/claude-code',
16
17
  configPath: join(homedir(), '.claude', 'settings.json'),
17
- displayName: 'Claude Code'
18
+ displayName: 'Claude Code',
18
19
  },
19
20
  'opencode': {
20
21
  name: 'opencode',
21
22
  command: 'opencode',
22
23
  installCommand: 'npm install -g opencode',
23
- configPath: join(homedir(), '.config', 'opencode', 'config.json'),
24
+ configPath: join(homedir(), '.config', 'opencode', 'opencode.json'),
24
25
  displayName: 'OpenCode',
25
- hidden: true // 暂不适配,隐藏不展示
26
- }
26
+ },
27
27
  };
28
28
  export class ToolManager {
29
29
  static instance;
@@ -66,41 +66,41 @@ export class ToolManager {
66
66
  // 检查是否是权限错误 (EACCES)
67
67
  // execSync 的错误信息可能在 stderr 中,需要检查多个来源
68
68
  const errorMessage = (error.message || '') + (error.stderr?.toString() || '') + (error.stdout?.toString() || '');
69
- const isPermissionError = errorMessage.includes('EACCES') ||
70
- errorMessage.includes('permission denied') ||
71
- errorMessage.includes('EPERM') ||
72
- error.status === 243; // npm 权限错误的退出码
69
+ const isPermissionError = errorMessage.includes('EACCES')
70
+ || errorMessage.includes('permission denied')
71
+ || errorMessage.includes('EPERM')
72
+ || error.status === 243; // npm 权限错误的退出码
73
73
  if (!isPermissionError) {
74
74
  // 如果不是权限错误,直接抛出
75
75
  throw new Error(`Failed to install ${tool.displayName}: ${error}`);
76
76
  }
77
- console.log('\n⚠️ ' + i18n.t('install.permission_detected') + '\n');
77
+ console.log(`\n⚠️ ${i18n.t('install.permission_detected')}\n`);
78
78
  // Windows 平台处理
79
79
  if (process.platform === 'win32') {
80
80
  try {
81
81
  // Windows: 尝试使用用户级安装(不需要管理员权限)
82
82
  const userInstallCommand = tool.installCommand.replace('npm install -g', 'npm install -g --force');
83
- console.log('🔧 ' + i18n.t('install.trying_solution', { num: '1', desc: i18n.t('install.using_force') }));
83
+ console.log(`🔧 ${i18n.t('install.trying_solution', { num: '1', desc: i18n.t('install.using_force') })}`);
84
84
  execSync(userInstallCommand, { stdio: 'inherit' });
85
- console.log('\n✅ ' + i18n.t('install.permission_fixed'));
85
+ console.log(`\n✅ ${i18n.t('install.permission_fixed')}`);
86
86
  return;
87
87
  }
88
88
  catch (retryError) {
89
89
  // 如果还是失败,显示解决方案并询问用户
90
90
  console.log(`Retry install error ${retryError}`);
91
- console.log('\n❌ ' + i18n.t('install.auto_fix_failed'));
92
- console.log(chalk.yellow('\n📌 ' + i18n.t('install.windows_solutions')));
91
+ console.log(`\n❌ ${i18n.t('install.auto_fix_failed')}`);
92
+ console.log(chalk.yellow(`\n📌 ${i18n.t('install.windows_solutions')}`));
93
93
  console.log('');
94
94
  // 方案 1: 以管理员身份运行
95
95
  console.log(chalk.cyan.bold(i18n.t('install.windows_solution_1_title')));
96
- console.log(chalk.gray(' ' + i18n.t('install.windows_solution_1_step1')));
97
- console.log(chalk.gray(' ' + i18n.t('install.windows_solution_1_step2')));
98
- console.log(chalk.gray(' ' + i18n.t('install.windows_solution_1_step3')));
96
+ console.log(chalk.gray(` ${i18n.t('install.windows_solution_1_step1')}`));
97
+ console.log(chalk.gray(` ${i18n.t('install.windows_solution_1_step2')}`));
98
+ console.log(chalk.gray(` ${i18n.t('install.windows_solution_1_step3')}`));
99
99
  console.log(chalk.white(` ${tool.installCommand}`));
100
100
  console.log('');
101
101
  // 方案 2: 用户级安装
102
102
  console.log(chalk.cyan.bold(i18n.t('install.windows_solution_2_title')));
103
- console.log(chalk.gray(' ' + i18n.t('install.windows_solution_2_command')));
103
+ console.log(chalk.gray(` ${i18n.t('install.windows_solution_2_command')}`));
104
104
  console.log(chalk.white(` ${tool.installCommand.replace('npm install -g', 'npm install -g --prefix=%APPDATA%\\npm')}`));
105
105
  console.log('');
106
106
  // 询问用户是否已完成安装
@@ -111,36 +111,36 @@ export class ToolManager {
111
111
  message: i18n.t('install.what_next'),
112
112
  choices: [
113
113
  { name: i18n.t('install.installed_continue'), value: 'continue' },
114
- { name: i18n.t('install.cancel_install'), value: 'cancel' }
115
- ]
116
- }
114
+ { name: i18n.t('install.cancel_install'), value: 'cancel' },
115
+ ],
116
+ },
117
117
  ]);
118
118
  if (action === 'cancel') {
119
119
  throw new Error(i18n.t('install.user_cancelled'));
120
120
  }
121
121
  // 验证是否真的安装成功
122
122
  if (!this.isToolInstalled(toolName)) {
123
- console.log(chalk.red('\n❌ ' + i18n.t('install.still_not_installed', { tool: tool.displayName })));
123
+ console.log(chalk.red(`\n❌ ${i18n.t('install.still_not_installed', { tool: tool.displayName })}`));
124
124
  throw new Error(`${tool.displayName} is not installed`);
125
125
  }
126
- console.log(chalk.green('\n✅ ' + i18n.t('install.verified_success', { tool: tool.displayName })));
126
+ console.log(chalk.green(`\n✅ ${i18n.t('install.verified_success', { tool: tool.displayName })}`));
127
127
  return;
128
128
  }
129
129
  }
130
130
  // macOS 和 Linux 平台处理 - 显示建议而不是自动执行
131
- console.log(chalk.yellow('\n📌 ' + i18n.t('install.unix_solutions')));
131
+ console.log(chalk.yellow(`\n📌 ${i18n.t('install.unix_solutions')}`));
132
132
  console.log('');
133
133
  // 方案 1: 使用 sudo
134
134
  console.log(chalk.cyan.bold(i18n.t('install.unix_solution_1_title')));
135
- console.log(chalk.gray(' ' + i18n.t('install.unix_solution_1_desc')));
135
+ console.log(chalk.gray(` ${i18n.t('install.unix_solution_1_desc')}`));
136
136
  console.log(chalk.white(` sudo ${tool.installCommand}`));
137
137
  console.log('');
138
138
  // 方案 2: 使用 nvm (推荐)
139
139
  const npmDocsUrl = 'https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally';
140
140
  const clickableLink = terminalLink(npmDocsUrl, npmDocsUrl, { fallback: () => npmDocsUrl });
141
141
  console.log(chalk.cyan.bold(i18n.t('install.unix_solution_2_title')));
142
- console.log(chalk.gray(' ' + i18n.t('install.unix_solution_2_desc')));
143
- console.log(chalk.blue(' 📖 ' + i18n.t('install.npm_docs_link') + ': ') + clickableLink);
142
+ console.log(chalk.gray(` ${i18n.t('install.unix_solution_2_desc')}`));
143
+ console.log(chalk.blue(` 📖 ${i18n.t('install.npm_docs_link')}: `) + clickableLink);
144
144
  console.log('');
145
145
  // 询问用户是否已完成安装
146
146
  const { action } = await inquirer.prompt([
@@ -150,20 +150,19 @@ export class ToolManager {
150
150
  message: i18n.t('install.what_next'),
151
151
  choices: [
152
152
  { name: i18n.t('install.installed_continue'), value: 'continue' },
153
- { name: i18n.t('install.cancel_install'), value: 'cancel' }
154
- ]
155
- }
153
+ { name: i18n.t('install.cancel_install'), value: 'cancel' },
154
+ ],
155
+ },
156
156
  ]);
157
157
  if (action === 'cancel') {
158
158
  throw new Error(i18n.t('install.user_cancelled'));
159
159
  }
160
160
  // 验证是否真的安装成功
161
161
  if (!this.isToolInstalled(toolName)) {
162
- console.log(chalk.red('\n❌ ' + i18n.t('install.still_not_installed', { tool: tool.displayName })));
162
+ console.log(chalk.red(`\n❌ ${i18n.t('install.still_not_installed', { tool: tool.displayName })}`));
163
163
  throw new Error(`${tool.displayName} is not installed`);
164
164
  }
165
- console.log(chalk.green('\n✅ ' + i18n.t('install.verified_success', { tool: tool.displayName })));
166
- return;
165
+ console.log(chalk.green(`\n✅ ${i18n.t('install.verified_success', { tool: tool.displayName })}`));
167
166
  }
168
167
  }
169
168
  getToolConfig(toolName) {
@@ -174,7 +173,13 @@ export class ToolManager {
174
173
  // Claude Code 使用专门的管理器
175
174
  if (toolName === 'claude-code') {
176
175
  return {
177
- settings: claudeCodeManager.getSettings()
176
+ settings: claudeCodeManager.getSettings(),
177
+ };
178
+ }
179
+ // OpenCode 使用专门的管理器
180
+ if (toolName === 'opencode') {
181
+ return {
182
+ config: openCodeManager.getConfig(),
178
183
  };
179
184
  }
180
185
  try {
@@ -200,6 +205,13 @@ export class ToolManager {
200
205
  }
201
206
  return;
202
207
  }
208
+ // OpenCode 使用专门的管理器
209
+ if (toolName === 'opencode') {
210
+ if (config.config) {
211
+ openCodeManager.saveConfig(config.config);
212
+ }
213
+ return;
214
+ }
203
215
  try {
204
216
  // 使用 dirname 获取目录路径,确保跨平台兼容(Windows/macOS/Linux)
205
217
  const configDir = dirname(tool.configPath);
@@ -212,7 +224,7 @@ export class ToolManager {
212
224
  throw new Error(`Failed to update config for ${toolName}: ${error}`);
213
225
  }
214
226
  }
215
- loadModelConfig(toolName, config) {
227
+ async loadModelConfig(toolName, config) {
216
228
  const tool = SUPPORTED_TOOLS[toolName];
217
229
  if (!tool) {
218
230
  throw new Error(`Unknown tool: ${toolName}`);
@@ -222,6 +234,16 @@ export class ToolManager {
222
234
  claudeCodeManager.saveModelConfig(config);
223
235
  return;
224
236
  }
237
+ // OpenCode 使用专门的管理器和独立的配置键
238
+ if (toolName === 'opencode') {
239
+ const openCodeConfig = {
240
+ apiKey: config.apiKey,
241
+ model: config.openCodeModel || '',
242
+ smallModel: config.openCodeSmallModel || '',
243
+ };
244
+ await openCodeManager.saveModelConfig(openCodeConfig);
245
+ return;
246
+ }
225
247
  // 其他工具的配置
226
248
  let existingConfig = this.getToolConfig(toolName) || {};
227
249
  existingConfig = {
@@ -229,7 +251,7 @@ export class ToolManager {
229
251
  apiKey: config.apiKey,
230
252
  haikuModel: config.haikuModel,
231
253
  sonnetModel: config.sonnetModel,
232
- opusModel: config.opusModel
254
+ opusModel: config.opusModel,
233
255
  };
234
256
  this.updateToolConfig(toolName, existingConfig);
235
257
  }
@@ -32,7 +32,11 @@ export declare class Wizard {
32
32
  /**
33
33
  * Step-by-step model selection with back navigation
34
34
  */
35
- selectModels(): Promise<void>;
35
+ selectModels(toolName?: string): Promise<void>;
36
+ /**
37
+ * OpenCode 专用的模型选择流程(2 步:主模型 + 小模型)
38
+ */
39
+ private selectModelsForOpenCode;
36
40
  selectAndConfigureTool(): Promise<void>;
37
41
  configureTool(toolName: any): Promise<void>;
38
42
  showMainMenu(): Promise<void>;