coder-link 0.0.9
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/README.md +200 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +684 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/amp-manager.d.ts +24 -0
- package/dist/lib/amp-manager.d.ts.map +1 -0
- package/dist/lib/amp-manager.js +46 -0
- package/dist/lib/amp-manager.js.map +1 -0
- package/dist/lib/claude-code-manager.d.ts +55 -0
- package/dist/lib/claude-code-manager.d.ts.map +1 -0
- package/dist/lib/claude-code-manager.js +408 -0
- package/dist/lib/claude-code-manager.js.map +1 -0
- package/dist/lib/crush-manager.d.ts +31 -0
- package/dist/lib/crush-manager.d.ts.map +1 -0
- package/dist/lib/crush-manager.js +262 -0
- package/dist/lib/crush-manager.js.map +1 -0
- package/dist/lib/factory-droid-manager.d.ts +36 -0
- package/dist/lib/factory-droid-manager.d.ts.map +1 -0
- package/dist/lib/factory-droid-manager.js +387 -0
- package/dist/lib/factory-droid-manager.js.map +1 -0
- package/dist/lib/kimi-manager.d.ts +34 -0
- package/dist/lib/kimi-manager.d.ts.map +1 -0
- package/dist/lib/kimi-manager.js +316 -0
- package/dist/lib/kimi-manager.js.map +1 -0
- package/dist/lib/opencode-manager.d.ts +31 -0
- package/dist/lib/opencode-manager.d.ts.map +1 -0
- package/dist/lib/opencode-manager.js +315 -0
- package/dist/lib/opencode-manager.js.map +1 -0
- package/dist/lib/pi-manager.d.ts +30 -0
- package/dist/lib/pi-manager.d.ts.map +1 -0
- package/dist/lib/pi-manager.js +196 -0
- package/dist/lib/pi-manager.js.map +1 -0
- package/dist/lib/tool-manager.d.ts +51 -0
- package/dist/lib/tool-manager.d.ts.map +1 -0
- package/dist/lib/tool-manager.js +113 -0
- package/dist/lib/tool-manager.js.map +1 -0
- package/dist/locales/en_US.json +49 -0
- package/dist/locales/zh_CN.json +49 -0
- package/dist/mcp-services.d.ts +3 -0
- package/dist/mcp-services.d.ts.map +1 -0
- package/dist/mcp-services.js +26 -0
- package/dist/mcp-services.js.map +1 -0
- package/dist/menu.d.ts +2 -0
- package/dist/menu.d.ts.map +1 -0
- package/dist/menu.js +1226 -0
- package/dist/menu.js.map +1 -0
- package/dist/utils/api-test.d.ts +32 -0
- package/dist/utils/api-test.d.ts.map +1 -0
- package/dist/utils/api-test.js +163 -0
- package/dist/utils/api-test.js.map +1 -0
- package/dist/utils/brand.d.ts +39 -0
- package/dist/utils/brand.d.ts.map +1 -0
- package/dist/utils/brand.js +195 -0
- package/dist/utils/brand.js.map +1 -0
- package/dist/utils/config.d.ts +100 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +483 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/exec.d.ts +5 -0
- package/dist/utils/exec.d.ts.map +1 -0
- package/dist/utils/exec.js +145 -0
- package/dist/utils/exec.js.map +1 -0
- package/dist/utils/i18n.d.ts +56 -0
- package/dist/utils/i18n.d.ts.map +1 -0
- package/dist/utils/i18n.js +42 -0
- package/dist/utils/i18n.js.map +1 -0
- package/dist/utils/keyboard.d.ts +32 -0
- package/dist/utils/keyboard.d.ts.map +1 -0
- package/dist/utils/keyboard.js +109 -0
- package/dist/utils/keyboard.js.map +1 -0
- package/dist/utils/logger.d.ts +14 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +55 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/output.d.ts +58 -0
- package/dist/utils/output.d.ts.map +1 -0
- package/dist/utils/output.js +93 -0
- package/dist/utils/output.js.map +1 -0
- package/dist/wizard.d.ts +2 -0
- package/dist/wizard.d.ts.map +1 -0
- package/dist/wizard.js +114 -0
- package/dist/wizard.js.map +1 -0
- package/package.json +65 -0
package/dist/menu.js
ADDED
|
@@ -0,0 +1,1226 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { configManager, isKimiLikePlan, CONFIG_DIR } from './utils/config.js';
|
|
7
|
+
import { i18n } from './utils/i18n.js';
|
|
8
|
+
import { toolManager } from './lib/tool-manager.js';
|
|
9
|
+
import { logger } from './utils/logger.js';
|
|
10
|
+
import { BUILTIN_MCP_SERVICES } from './mcp-services.js';
|
|
11
|
+
import { commandExists, runInteractive, runInteractiveWithEnv, runInNewTerminal } from './utils/exec.js';
|
|
12
|
+
import { testOpenAIChatCompletionsApi, testOpenAICompatibleApi, fetchOpenRouterModelInfo } from './utils/api-test.js';
|
|
13
|
+
import { printSplash, printHeader, printStatusBar, printNavigationHints, printConfigPathHint, planLabel, planLabelColored, maskApiKey, toolLabel, statusIndicator, truncateForTerminal, } from './utils/brand.js';
|
|
14
|
+
import { printError, printSuccess, printWarning, printInfo } from './utils/output.js';
|
|
15
|
+
function disableMouseTracking() {
|
|
16
|
+
if (!process.stdout.isTTY)
|
|
17
|
+
return;
|
|
18
|
+
// Disable common mouse tracking modes.
|
|
19
|
+
// See xterm mouse tracking: 9, 1000, 1002, 1003, 1005, 1006, 1015.
|
|
20
|
+
process.stdout.write('\x1b[?9l' +
|
|
21
|
+
'\x1b[?1000l' +
|
|
22
|
+
'\x1b[?1002l' +
|
|
23
|
+
'\x1b[?1003l' +
|
|
24
|
+
'\x1b[?1005l' +
|
|
25
|
+
'\x1b[?1006l' +
|
|
26
|
+
'\x1b[?1015l');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Some UI libs can enable terminal mouse tracking via ANSI (DECSET).
|
|
30
|
+
* When enabled, mouse movement/clicks become input and can:
|
|
31
|
+
* - move list selection
|
|
32
|
+
* - inject junk like "35;47;...M" into text inputs
|
|
33
|
+
*
|
|
34
|
+
* We prevent this by stripping *mouse-enable* escape sequences from stdout
|
|
35
|
+
* so mouse mode never turns on, while leaving stdin untouched (arrow keys work).
|
|
36
|
+
*/
|
|
37
|
+
function installStdoutMouseGuard() {
|
|
38
|
+
if (!process.stdout.isTTY)
|
|
39
|
+
return;
|
|
40
|
+
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
41
|
+
const enableRe = /\x1b\[\?(?:9|1000|1002|1003|1005|1006|1015)h/g;
|
|
42
|
+
process.stdout.write = (chunk, encoding, cb) => {
|
|
43
|
+
if (chunk == null)
|
|
44
|
+
return originalWrite(chunk, encoding, cb);
|
|
45
|
+
if (Buffer.isBuffer(chunk) || chunk instanceof Uint8Array) {
|
|
46
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
47
|
+
const s = buf.toString('latin1');
|
|
48
|
+
const cleaned = s.replace(enableRe, '');
|
|
49
|
+
return originalWrite(Buffer.from(cleaned, 'latin1'), encoding, cb);
|
|
50
|
+
}
|
|
51
|
+
const s = String(chunk);
|
|
52
|
+
const cleaned = s.replace(enableRe, '');
|
|
53
|
+
return originalWrite(cleaned, encoding, cb);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
installStdoutMouseGuard();
|
|
57
|
+
disableMouseTracking();
|
|
58
|
+
// ââ Helpers ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
59
|
+
function providerSummary(plan) {
|
|
60
|
+
if (!plan)
|
|
61
|
+
return '';
|
|
62
|
+
if (!isKimiLikePlan(plan))
|
|
63
|
+
return '';
|
|
64
|
+
const s = configManager.getProviderSettings(plan);
|
|
65
|
+
const parts = [];
|
|
66
|
+
if (s.baseUrl)
|
|
67
|
+
parts.push(s.baseUrl);
|
|
68
|
+
if (s.model)
|
|
69
|
+
parts.push(s.model);
|
|
70
|
+
return parts.length ? chalk.gray(` (${parts.join(' ¡ ')})`) : '';
|
|
71
|
+
}
|
|
72
|
+
function startCommand(tool) {
|
|
73
|
+
switch (tool) {
|
|
74
|
+
case 'claude-code':
|
|
75
|
+
return { cmd: 'claude', args: [] };
|
|
76
|
+
case 'opencode':
|
|
77
|
+
return { cmd: 'opencode', args: [] };
|
|
78
|
+
case 'crush':
|
|
79
|
+
return { cmd: 'crush', args: [] };
|
|
80
|
+
case 'factory-droid':
|
|
81
|
+
return commandExists('droid') ? { cmd: 'droid', args: [] } : { cmd: 'factory', args: [] };
|
|
82
|
+
case 'kimi':
|
|
83
|
+
return { cmd: 'kimi', args: [] };
|
|
84
|
+
case 'amp':
|
|
85
|
+
return { cmd: 'amp', args: [] };
|
|
86
|
+
case 'pi':
|
|
87
|
+
return { cmd: 'pi', args: [] };
|
|
88
|
+
default:
|
|
89
|
+
return { cmd: tool, args: [] };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function openUrlCommand(url) {
|
|
93
|
+
if (process.platform === 'win32')
|
|
94
|
+
return `start ${url}`;
|
|
95
|
+
if (process.platform === 'darwin')
|
|
96
|
+
return `open ${url}`;
|
|
97
|
+
return `xdg-open ${url}`;
|
|
98
|
+
}
|
|
99
|
+
function installHint(tool) {
|
|
100
|
+
switch (tool) {
|
|
101
|
+
case 'amp':
|
|
102
|
+
return { label: 'Install AMP Code', command: 'powershell -c "irm https://ampcode.com/install.ps1 | iex"' };
|
|
103
|
+
case 'pi':
|
|
104
|
+
return { label: 'Install Pi CLI', command: 'bun add -g @mariozechner/pi-coding-agent' };
|
|
105
|
+
case 'opencode':
|
|
106
|
+
return { label: 'Install OpenCode', command: 'bun add -g opencode-ai' };
|
|
107
|
+
case 'kimi':
|
|
108
|
+
return { label: 'Open Kimi CLI install page', command: openUrlCommand('https://kimi.moonshot.cn/') };
|
|
109
|
+
case 'claude-code':
|
|
110
|
+
return { label: 'Open Claude Code install page', command: openUrlCommand('https://docs.anthropic.com/claude-code') };
|
|
111
|
+
case 'crush':
|
|
112
|
+
return { label: 'Open Crush install page', command: openUrlCommand('https://crush.ai/') };
|
|
113
|
+
case 'factory-droid':
|
|
114
|
+
return { label: 'Open Factory Droid install page', command: openUrlCommand('https://factory.ai/') };
|
|
115
|
+
default:
|
|
116
|
+
return { label: 'Install instructions', command: undefined };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function pause(message = 'Press Enter to continue... (or q to quit)') {
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
console.log(chalk.gray(` ${message}`));
|
|
122
|
+
// Inquirer/readline can leave stdin paused after a prompt.
|
|
123
|
+
// If stdin is paused, a pending `once('data')` listener does not keep the
|
|
124
|
+
// event loop alive and the process may exit immediately.
|
|
125
|
+
if (process.stdin.isTTY)
|
|
126
|
+
process.stdin.resume();
|
|
127
|
+
const onData = (data) => {
|
|
128
|
+
cleanup();
|
|
129
|
+
const str = data.toString().trim();
|
|
130
|
+
if (str === 'q' || str === 'Q') {
|
|
131
|
+
console.log(chalk.gray('\n Goodbye!\n'));
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
resolve();
|
|
135
|
+
};
|
|
136
|
+
const onEnd = () => {
|
|
137
|
+
cleanup();
|
|
138
|
+
resolve();
|
|
139
|
+
};
|
|
140
|
+
const onError = () => {
|
|
141
|
+
cleanup();
|
|
142
|
+
resolve();
|
|
143
|
+
};
|
|
144
|
+
const cleanup = () => {
|
|
145
|
+
process.stdin.removeListener('data', onData);
|
|
146
|
+
process.stdin.removeListener('end', onEnd);
|
|
147
|
+
process.stdin.removeListener('error', onError);
|
|
148
|
+
};
|
|
149
|
+
process.stdin.on('data', onData);
|
|
150
|
+
process.stdin.once('end', onEnd);
|
|
151
|
+
process.stdin.once('error', onError);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// Safe spinner that doesn't overflow terminal width
|
|
155
|
+
function createSafeSpinner(text) {
|
|
156
|
+
const safeText = truncateForTerminal(text, (process.stdout.columns || 80) - 10);
|
|
157
|
+
return ora({ text: safeText, spinner: 'dots' });
|
|
158
|
+
}
|
|
159
|
+
// ââ Tool/Provider Compatibility ââââââââââââââââââââââââââââââââââââââââ
|
|
160
|
+
/**
|
|
161
|
+
* Check if a provider is compatible with a tool.
|
|
162
|
+
* Returns null if compatible, or an error message if incompatible.
|
|
163
|
+
*/
|
|
164
|
+
function getProviderIncompatibility(tool, plan) {
|
|
165
|
+
// Claude Code requires Anthropic-compatible API (/v1/messages)
|
|
166
|
+
// Kimi and NVIDIA only provide OpenAI-compatible API (/v1/chat/completions)
|
|
167
|
+
// LM Studio supports both OpenAI and Anthropic endpoints
|
|
168
|
+
if (tool === 'claude-code' && (plan === 'kimi' || plan === 'nvidia')) {
|
|
169
|
+
return 'Requires Anthropic API';
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
// ââ Provider Menu ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
174
|
+
const PROVIDER_CHOICES = [
|
|
175
|
+
{ name: 'GLM Coding Plan (Global)', value: 'glm_coding_plan_global' },
|
|
176
|
+
{ name: 'GLM Coding Plan (China)', value: 'glm_coding_plan_china' },
|
|
177
|
+
{ name: 'Kimi (Moonshot)', value: 'kimi' },
|
|
178
|
+
{ name: 'OpenRouter', value: 'openrouter' },
|
|
179
|
+
{ name: 'NVIDIA NIM', value: 'nvidia' },
|
|
180
|
+
{ name: 'LM Studio (Local)', value: 'lmstudio' },
|
|
181
|
+
];
|
|
182
|
+
const COMMON_MODELS = {
|
|
183
|
+
kimi: ['moonshot-ai/kimi-k2.5', 'moonshot-ai/kimi-k2-thinking'],
|
|
184
|
+
openrouter: ['moonshotai/kimi-k2.5', 'anthropic/claude-opus-4.6', 'poney-alpha', 'qwen/qwen3-coder-next'],
|
|
185
|
+
nvidia: ['moonshotai/kimi-k2.5', 'deepseek-ai/deepseek-v3.2', 'meta/llama-3.3-70b-instruct', 'meta/llama-4-maverick-17b-128e-instruct', 'qwen/qwen3-coder-480b-a35b-instruct', 'z-ai/glm4.7', 'nvidia/llama-3.3-nemotron-super-49b-v1.5'],
|
|
186
|
+
lmstudio: ['lmstudio-community', 'deepseek-coder-v3', 'codellama/13b', 'mistral-7b-instruct', 'qwen2.5-coder-7b'],
|
|
187
|
+
glm_coding_plan_global: ['glm-4.7', 'glm-4-coder', 'glm-4-plus', 'glm-4-air', 'glm-4-flash'],
|
|
188
|
+
glm_coding_plan_china: ['glm-4.7', 'glm-4-coder', 'glm-4-plus', 'glm-4-air', 'glm-4-flash'],
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* Enhanced prompt for model selection.
|
|
192
|
+
* Offers common models + custom input + back.
|
|
193
|
+
*/
|
|
194
|
+
async function selectModelId(plan, currentModel) {
|
|
195
|
+
const common = COMMON_MODELS[plan] || [];
|
|
196
|
+
const choices = [
|
|
197
|
+
...(currentModel ? [{ name: `${currentModel} ${chalk.green('(current)')}`, value: currentModel }] : []),
|
|
198
|
+
...common.filter(m => m !== currentModel).map(m => ({ name: m, value: m })),
|
|
199
|
+
{ name: 'âī¸ Enter custom model ID...', value: '__custom' },
|
|
200
|
+
new inquirer.Separator(),
|
|
201
|
+
{ name: chalk.gray('â Back'), value: '__back' },
|
|
202
|
+
];
|
|
203
|
+
const { selection } = await inquirer.prompt([{
|
|
204
|
+
type: 'list',
|
|
205
|
+
name: 'selection',
|
|
206
|
+
message: 'Select Model ID:',
|
|
207
|
+
choices,
|
|
208
|
+
}]);
|
|
209
|
+
if (selection === '__back')
|
|
210
|
+
return '__back';
|
|
211
|
+
if (selection === '__custom') {
|
|
212
|
+
const { custom } = await inquirer.prompt([{
|
|
213
|
+
type: 'input',
|
|
214
|
+
name: 'custom',
|
|
215
|
+
message: `Enter model ID (or 'b' to go back):`,
|
|
216
|
+
validate: (v) => v.trim().length > 0 || 'Model ID cannot be empty',
|
|
217
|
+
}]);
|
|
218
|
+
if (custom.trim().toLowerCase() === 'b')
|
|
219
|
+
return selectModelId(plan, currentModel);
|
|
220
|
+
return custom.trim();
|
|
221
|
+
}
|
|
222
|
+
return selection;
|
|
223
|
+
}
|
|
224
|
+
async function configureProfilesMenu() {
|
|
225
|
+
while (true) {
|
|
226
|
+
console.clear();
|
|
227
|
+
printHeader('Configure Profiles');
|
|
228
|
+
printNavigationHints();
|
|
229
|
+
const { plan } = await inquirer.prompt([{
|
|
230
|
+
type: 'list',
|
|
231
|
+
name: 'plan',
|
|
232
|
+
message: 'Select profile to configure:',
|
|
233
|
+
choices: [
|
|
234
|
+
...PROVIDER_CHOICES.map(c => {
|
|
235
|
+
const key = configManager.getApiKeyFor(c.value);
|
|
236
|
+
const status = key ? chalk.green(' (Configured)') : chalk.gray(' (Not set)');
|
|
237
|
+
return { name: `${c.name}${status}`, value: c.value };
|
|
238
|
+
}),
|
|
239
|
+
new inquirer.Separator(),
|
|
240
|
+
{ name: chalk.gray('â Back'), value: '__back' },
|
|
241
|
+
],
|
|
242
|
+
}]);
|
|
243
|
+
if (plan === '__back')
|
|
244
|
+
return;
|
|
245
|
+
await providerSetupFlow(plan);
|
|
246
|
+
console.log();
|
|
247
|
+
await pause();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Unified provider configuration flow:
|
|
252
|
+
* 1. Set endpoint + model (for kimi-like providers)
|
|
253
|
+
* 2. Set API key
|
|
254
|
+
*/
|
|
255
|
+
async function providerSetupFlow(plan) {
|
|
256
|
+
printInfo(`Configuring ${planLabel(plan)} profile...`);
|
|
257
|
+
console.log(chalk.gray(` (Enter 'b' at any text prompt to go back)\n`));
|
|
258
|
+
// Step 1 â Endpoint
|
|
259
|
+
const current = configManager.getProviderSettings(plan);
|
|
260
|
+
const { base_url } = await inquirer.prompt([{
|
|
261
|
+
type: 'input',
|
|
262
|
+
name: 'base_url',
|
|
263
|
+
message: `${planLabel(plan)} Base URL:`,
|
|
264
|
+
default: current.baseUrl,
|
|
265
|
+
validate: (v) => v.trim().length > 0 || 'Base URL cannot be empty',
|
|
266
|
+
}]);
|
|
267
|
+
if (base_url.trim().toLowerCase() === 'b') {
|
|
268
|
+
printInfo('Configuration cancelled');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
// Step 2 â Model
|
|
272
|
+
const model = await selectModelId(plan, current.model);
|
|
273
|
+
if (model === '__back')
|
|
274
|
+
return providerSetupFlow(plan);
|
|
275
|
+
// Try to fetch context size from OpenRouter API if applicable
|
|
276
|
+
let suggestedCtx = current.maxContextSize || (plan === 'nvidia' ? 4096 : plan === 'openrouter' ? 16384 : plan.includes('glm') ? 128000 : 262144);
|
|
277
|
+
let fetchedContextInfo = '';
|
|
278
|
+
if (plan === 'openrouter') {
|
|
279
|
+
// We need an API key to fetch model info - check if one exists already
|
|
280
|
+
const existingKey = configManager.getApiKeyFor(plan);
|
|
281
|
+
if (existingKey) {
|
|
282
|
+
const spinner = createSafeSpinner(`Fetching model info from OpenRouter...`).start();
|
|
283
|
+
try {
|
|
284
|
+
const modelInfo = await fetchOpenRouterModelInfo({
|
|
285
|
+
apiKey: existingKey,
|
|
286
|
+
modelId: model.trim(),
|
|
287
|
+
timeoutMs: 8000,
|
|
288
|
+
});
|
|
289
|
+
if (modelInfo?.contextLength) {
|
|
290
|
+
suggestedCtx = modelInfo.contextLength;
|
|
291
|
+
fetchedContextInfo = chalk.green(` (fetched from API: ${modelInfo.contextLength.toLocaleString()})`);
|
|
292
|
+
spinner.succeed(`Found model: ${modelInfo.name || modelInfo.id} (context: ${modelInfo.contextLength.toLocaleString()})`);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
spinner.fail('Could not fetch context size from OpenRouter, using default');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
spinner.fail('Failed to fetch model info from OpenRouter');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const { max_context_size_input } = await inquirer.prompt([{
|
|
304
|
+
type: 'input',
|
|
305
|
+
name: 'max_context_size_input',
|
|
306
|
+
message: `Max context size:${fetchedContextInfo} (or 'b')`,
|
|
307
|
+
default: String(suggestedCtx),
|
|
308
|
+
validate: (v) => {
|
|
309
|
+
if (v.trim().toLowerCase() === 'b')
|
|
310
|
+
return true;
|
|
311
|
+
const n = Number(v);
|
|
312
|
+
return Number.isInteger(n) && n > 0 || 'Enter a positive integer';
|
|
313
|
+
},
|
|
314
|
+
}]);
|
|
315
|
+
if (max_context_size_input.toLowerCase() === 'b')
|
|
316
|
+
return providerSetupFlow(plan);
|
|
317
|
+
configManager.setProviderProfile(plan, {
|
|
318
|
+
base_url: base_url.trim(),
|
|
319
|
+
model: model.trim(),
|
|
320
|
+
max_context_size: Number(max_context_size_input),
|
|
321
|
+
});
|
|
322
|
+
// Step 3 â API key
|
|
323
|
+
const existingKey = configManager.getApiKeyFor(plan);
|
|
324
|
+
const isLocalProvider = plan === 'lmstudio';
|
|
325
|
+
const keyMsg = existingKey
|
|
326
|
+
? `API key for ${planLabel(plan)} [current: ${maskApiKey(existingKey)}] (or 'b')${isLocalProvider ? ' [optional for local]' : ''}:`
|
|
327
|
+
: `API key for ${planLabel(plan)} (or 'b')${isLocalProvider ? ' [optional for local]' : ''}:`;
|
|
328
|
+
const { apiKey } = await inquirer.prompt([{
|
|
329
|
+
type: 'password',
|
|
330
|
+
name: 'apiKey',
|
|
331
|
+
message: keyMsg,
|
|
332
|
+
mask: '*',
|
|
333
|
+
validate: (v) => {
|
|
334
|
+
if (v.trim().toLowerCase() === 'b')
|
|
335
|
+
return true;
|
|
336
|
+
// Allow empty to keep existing key
|
|
337
|
+
if (existingKey && v.trim().length === 0)
|
|
338
|
+
return true;
|
|
339
|
+
// Allow empty for local providers
|
|
340
|
+
if (isLocalProvider && v.trim().length === 0)
|
|
341
|
+
return true;
|
|
342
|
+
return v.trim().length > 0 || 'API key cannot be empty';
|
|
343
|
+
},
|
|
344
|
+
}]);
|
|
345
|
+
if (apiKey.trim().toLowerCase() === 'b')
|
|
346
|
+
return providerSetupFlow(plan);
|
|
347
|
+
const finalKey = apiKey.trim() || (existingKey ?? (isLocalProvider ? 'lmstudio' : ''));
|
|
348
|
+
configManager.setApiKeyFor(plan, finalKey);
|
|
349
|
+
printSuccess(`${planLabel(plan)} profile updated`);
|
|
350
|
+
const s = configManager.getProviderSettings(plan);
|
|
351
|
+
console.log(chalk.gray(` Endpoint : ${s.baseUrl}`));
|
|
352
|
+
console.log(chalk.gray(` Model : ${s.model}`));
|
|
353
|
+
console.log(chalk.gray(` API Key : ${maskApiKey(finalKey)}`));
|
|
354
|
+
}
|
|
355
|
+
async function providerMenu() {
|
|
356
|
+
while (true) {
|
|
357
|
+
console.clear();
|
|
358
|
+
printHeader('Provider Configuration');
|
|
359
|
+
const auth = configManager.getAuth();
|
|
360
|
+
const plan = auth.plan;
|
|
361
|
+
printStatusBar(plan, auth.apiKey, plan ? providerSummary(plan).trim() : undefined);
|
|
362
|
+
printConfigPathHint(configManager.configPath);
|
|
363
|
+
printNavigationHints();
|
|
364
|
+
const choices = [
|
|
365
|
+
{ name: 'đ Select Global Default Provider', value: 'set_global' },
|
|
366
|
+
{ name: 'âī¸ Configure Provider Profiles (keys, endpoints, models)', value: 'configure' },
|
|
367
|
+
];
|
|
368
|
+
if (plan && auth.apiKey) {
|
|
369
|
+
choices.push({ name: 'đŦ Test API Connection', value: 'test' });
|
|
370
|
+
choices.push({ name: 'đ Revoke API Keys', value: 'revoke' });
|
|
371
|
+
}
|
|
372
|
+
const { action } = await inquirer.prompt([{
|
|
373
|
+
type: 'list',
|
|
374
|
+
name: 'action',
|
|
375
|
+
message: 'Action:',
|
|
376
|
+
choices: [
|
|
377
|
+
...choices,
|
|
378
|
+
new inquirer.Separator(),
|
|
379
|
+
{ name: chalk.gray('â Back'), value: '__back' },
|
|
380
|
+
],
|
|
381
|
+
}]);
|
|
382
|
+
if (action === '__back')
|
|
383
|
+
return;
|
|
384
|
+
try {
|
|
385
|
+
if (action === 'set_global') {
|
|
386
|
+
const { newPlan } = await inquirer.prompt([{
|
|
387
|
+
type: 'list',
|
|
388
|
+
name: 'newPlan',
|
|
389
|
+
message: 'Select Global Default Provider:',
|
|
390
|
+
choices: [
|
|
391
|
+
...PROVIDER_CHOICES.map(c => ({
|
|
392
|
+
...c,
|
|
393
|
+
name: c.value === plan ? `${c.name} ${chalk.green('â')}` : c.name,
|
|
394
|
+
})),
|
|
395
|
+
new inquirer.Separator(),
|
|
396
|
+
{ name: chalk.gray('â Back'), value: '__back' },
|
|
397
|
+
],
|
|
398
|
+
default: plan,
|
|
399
|
+
}]);
|
|
400
|
+
if (newPlan !== '__back') {
|
|
401
|
+
const key = configManager.getApiKeyFor(newPlan);
|
|
402
|
+
configManager.setAuth(newPlan, key || '');
|
|
403
|
+
printSuccess(`Global provider set to ${planLabel(newPlan)}`);
|
|
404
|
+
await pause();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else if (action === 'configure') {
|
|
408
|
+
await configureProfilesMenu();
|
|
409
|
+
}
|
|
410
|
+
else if (action === 'revoke') {
|
|
411
|
+
const { confirm } = await inquirer.prompt([{
|
|
412
|
+
type: 'confirm',
|
|
413
|
+
name: 'confirm',
|
|
414
|
+
message: chalk.yellow('Revoke all saved API keys?'),
|
|
415
|
+
default: false,
|
|
416
|
+
}]);
|
|
417
|
+
if (!confirm) {
|
|
418
|
+
printInfo('Cancelled');
|
|
419
|
+
await pause();
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
configManager.revokeAuth();
|
|
423
|
+
printSuccess('API keys revoked');
|
|
424
|
+
await pause();
|
|
425
|
+
}
|
|
426
|
+
else if (action === 'test') {
|
|
427
|
+
await testApiConnection();
|
|
428
|
+
await pause();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
logger.logError('menu.provider', error);
|
|
433
|
+
printError(error instanceof Error ? error.message : String(error), 'Run "coder-link auth" to configure API keys');
|
|
434
|
+
await pause();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async function testApiConnection() {
|
|
439
|
+
const auth = configManager.getAuth();
|
|
440
|
+
const plan = auth.plan;
|
|
441
|
+
const apiKey = auth.apiKey;
|
|
442
|
+
if (!plan || !apiKey) {
|
|
443
|
+
printWarning('No provider configured. Set one up first.');
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
let baseUrl;
|
|
447
|
+
let model;
|
|
448
|
+
if (isKimiLikePlan(plan)) {
|
|
449
|
+
const s = configManager.getProviderSettings(plan);
|
|
450
|
+
baseUrl = s.baseUrl;
|
|
451
|
+
model = s.model;
|
|
452
|
+
}
|
|
453
|
+
else if (plan === 'glm_coding_plan_global') {
|
|
454
|
+
baseUrl = 'https://api.z.ai/api/coding/paas/v4';
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
baseUrl = 'https://open.bigmodel.cn/api/coding/paas/v4';
|
|
458
|
+
}
|
|
459
|
+
const spinner = createSafeSpinner(`Testing: ${baseUrl}`).start();
|
|
460
|
+
const timeoutMs = 12000;
|
|
461
|
+
const isNvidia = plan === 'nvidia' || baseUrl.includes('integrate.api.nvidia.com');
|
|
462
|
+
const result = await (async () => {
|
|
463
|
+
if (isNvidia) {
|
|
464
|
+
return await testOpenAIChatCompletionsApi({
|
|
465
|
+
baseUrl,
|
|
466
|
+
apiKey,
|
|
467
|
+
model: model || 'moonshotai/kimi-k2.5',
|
|
468
|
+
timeoutMs,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
const modelsProbe = await testOpenAICompatibleApi({ baseUrl, apiKey, timeoutMs });
|
|
472
|
+
if (modelsProbe.ok)
|
|
473
|
+
return modelsProbe;
|
|
474
|
+
if (isKimiLikePlan(plan) && model && (modelsProbe.status === 404 || /not found/i.test(modelsProbe.detail))) {
|
|
475
|
+
return await testOpenAIChatCompletionsApi({ baseUrl, apiKey, model, timeoutMs });
|
|
476
|
+
}
|
|
477
|
+
return modelsProbe;
|
|
478
|
+
})();
|
|
479
|
+
if (result.ok) {
|
|
480
|
+
spinner.succeed(result.detail);
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
spinner.fail(result.detail);
|
|
484
|
+
if (result.status)
|
|
485
|
+
console.log(chalk.gray(` HTTP ${result.status}`));
|
|
486
|
+
}
|
|
487
|
+
console.log(chalk.gray(` URL: ${result.url}`));
|
|
488
|
+
}
|
|
489
|
+
// ââ MCP Menu âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
490
|
+
async function mcpMenu(tool) {
|
|
491
|
+
while (true) {
|
|
492
|
+
console.clear();
|
|
493
|
+
printHeader(`MCP ¡ ${toolLabel(tool)}`);
|
|
494
|
+
const auth = configManager.getAuth();
|
|
495
|
+
if (!auth.plan || !auth.apiKey) {
|
|
496
|
+
printWarning('No provider configured. Set one up from the main menu first.');
|
|
497
|
+
await pause();
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const installed = await toolManager.getInstalledMCPs(tool);
|
|
501
|
+
console.log(` ${chalk.gray('Installed:')} ${installed.length ? installed.join(', ') : chalk.yellow('None')}`);
|
|
502
|
+
console.log();
|
|
503
|
+
printNavigationHints();
|
|
504
|
+
const { action } = await inquirer.prompt([{
|
|
505
|
+
type: 'list',
|
|
506
|
+
name: 'action',
|
|
507
|
+
message: 'Action:',
|
|
508
|
+
choices: [
|
|
509
|
+
{ name: 'đĻ Install built-in MCP', value: 'install' },
|
|
510
|
+
...(installed.length ? [{ name: 'đ Uninstall MCP', value: 'uninstall' }] : []),
|
|
511
|
+
new inquirer.Separator(),
|
|
512
|
+
{ name: chalk.gray('â Back (Esc)'), value: '__back' },
|
|
513
|
+
],
|
|
514
|
+
}]);
|
|
515
|
+
if (action === '__back')
|
|
516
|
+
return;
|
|
517
|
+
try {
|
|
518
|
+
if (action === 'install') {
|
|
519
|
+
const { id } = await inquirer.prompt([{
|
|
520
|
+
type: 'list',
|
|
521
|
+
name: 'id',
|
|
522
|
+
message: 'Select MCP:',
|
|
523
|
+
choices: [
|
|
524
|
+
...BUILTIN_MCP_SERVICES.map(s => ({
|
|
525
|
+
name: `${installed.includes(s.id) ? 'â' : ' '} ${s.name} (${s.id})`,
|
|
526
|
+
value: s.id,
|
|
527
|
+
})),
|
|
528
|
+
new inquirer.Separator(),
|
|
529
|
+
{ name: chalk.gray('â Back'), value: '__back' },
|
|
530
|
+
],
|
|
531
|
+
}]);
|
|
532
|
+
if (id === '__back')
|
|
533
|
+
continue;
|
|
534
|
+
const { target } = await inquirer.prompt([{
|
|
535
|
+
type: 'list',
|
|
536
|
+
name: 'target',
|
|
537
|
+
message: 'Install to:',
|
|
538
|
+
choices: [
|
|
539
|
+
{ name: `Only ${toolLabel(tool)}`, value: 'this' },
|
|
540
|
+
{ name: 'Apply to ALL supported tools', value: 'all' },
|
|
541
|
+
],
|
|
542
|
+
}]);
|
|
543
|
+
const service = BUILTIN_MCP_SERVICES.find(s => s.id === id);
|
|
544
|
+
if (target === 'all') {
|
|
545
|
+
const tools = toolManager.getSupportedTools();
|
|
546
|
+
const spinner = createSafeSpinner(`Installing ${id} to all tools...`).start();
|
|
547
|
+
let success = 0;
|
|
548
|
+
for (const t of tools) {
|
|
549
|
+
try {
|
|
550
|
+
await toolManager.installMCP(t, service, auth.apiKey, auth.plan);
|
|
551
|
+
success++;
|
|
552
|
+
}
|
|
553
|
+
catch (e) {
|
|
554
|
+
// skip errors for individual tools
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
spinner.succeed(`Installed ${id} to ${success}/${tools.length} tools`);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
await toolManager.installMCP(tool, service, auth.apiKey, auth.plan);
|
|
561
|
+
printSuccess(`Installed ${id} to ${toolLabel(tool)}`);
|
|
562
|
+
}
|
|
563
|
+
await pause();
|
|
564
|
+
}
|
|
565
|
+
else if (action === 'uninstall') {
|
|
566
|
+
const { id } = await inquirer.prompt([{
|
|
567
|
+
type: 'list',
|
|
568
|
+
name: 'id',
|
|
569
|
+
message: 'Select MCP to uninstall:',
|
|
570
|
+
choices: [
|
|
571
|
+
...installed.map(x => ({ name: x, value: x })),
|
|
572
|
+
new inquirer.Separator(),
|
|
573
|
+
{ name: chalk.gray('â Back'), value: '__back' },
|
|
574
|
+
],
|
|
575
|
+
}]);
|
|
576
|
+
if (id === '__back')
|
|
577
|
+
continue;
|
|
578
|
+
await toolManager.uninstallMCP(tool, id);
|
|
579
|
+
printSuccess(`Uninstalled ${id}`);
|
|
580
|
+
await pause();
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
catch (error) {
|
|
584
|
+
logger.logError('menu.mcp', error);
|
|
585
|
+
printError(error instanceof Error ? error.message : String(error), 'Check tool-specific documentation for MCP requirements');
|
|
586
|
+
await pause();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// ââ Tool Menu ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
591
|
+
async function toolMenu(tool) {
|
|
592
|
+
while (true) {
|
|
593
|
+
console.clear();
|
|
594
|
+
printHeader(toolLabel(tool));
|
|
595
|
+
// Current chelper provider state
|
|
596
|
+
const auth = configManager.getAuth();
|
|
597
|
+
const chelperPlan = auth.plan;
|
|
598
|
+
const chelperKey = auth.apiKey;
|
|
599
|
+
// Get chelper model for kimi-like plans
|
|
600
|
+
let chelperModel;
|
|
601
|
+
if (chelperPlan && isKimiLikePlan(chelperPlan)) {
|
|
602
|
+
const settings = configManager.getProviderSettings(chelperPlan);
|
|
603
|
+
chelperModel = settings.model;
|
|
604
|
+
}
|
|
605
|
+
// Detect what's actually written in the tool's own config
|
|
606
|
+
let toolPlan = null;
|
|
607
|
+
let toolKey = null;
|
|
608
|
+
let toolModel;
|
|
609
|
+
try {
|
|
610
|
+
const detected = await toolManager.detectCurrentConfig(tool);
|
|
611
|
+
toolPlan = detected.plan;
|
|
612
|
+
toolKey = detected.apiKey;
|
|
613
|
+
toolModel = detected.model;
|
|
614
|
+
}
|
|
615
|
+
catch {
|
|
616
|
+
// ignore
|
|
617
|
+
}
|
|
618
|
+
// Sync state
|
|
619
|
+
const hasProvider = !!(chelperPlan && chelperKey);
|
|
620
|
+
const isConfigured = !!(toolPlan && toolKey);
|
|
621
|
+
const matchesGlobal = hasProvider && isConfigured && (toolPlan ?? '') === chelperPlan;
|
|
622
|
+
// Status display
|
|
623
|
+
console.log(chalk.gray(' Coder Link'));
|
|
624
|
+
printStatusBar(chelperPlan, chelperKey, chelperModel ? chalk.gray(`Model: ${chelperModel}`) : undefined);
|
|
625
|
+
console.log(chalk.gray(` ${toolLabel(tool)}`));
|
|
626
|
+
printStatusBar(toolPlan ?? undefined, toolKey ?? undefined, toolModel ? chalk.gray(`Model: ${toolModel}`) : undefined);
|
|
627
|
+
if (tool === 'factory-droid') {
|
|
628
|
+
const factoryKey = configManager.getFactoryApiKey();
|
|
629
|
+
console.log(` ${chalk.gray('Factory API Key:')} ${factoryKey ? chalk.green(maskApiKey(factoryKey)) : chalk.yellow('Not set')}`);
|
|
630
|
+
console.log();
|
|
631
|
+
}
|
|
632
|
+
// Status indicator - show if configured, not sync state
|
|
633
|
+
if (isConfigured) {
|
|
634
|
+
if (matchesGlobal) {
|
|
635
|
+
printSuccess(`Using global default provider`);
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
printInfo(`Using tool-specific provider (different from global)`);
|
|
639
|
+
}
|
|
640
|
+
console.log();
|
|
641
|
+
}
|
|
642
|
+
else if (hasProvider) {
|
|
643
|
+
printInfo('Not configured â sync with global or select a provider');
|
|
644
|
+
console.log();
|
|
645
|
+
}
|
|
646
|
+
// Warning
|
|
647
|
+
printInfo('Changes modify the tool\'s global configuration.');
|
|
648
|
+
console.log();
|
|
649
|
+
printNavigationHints();
|
|
650
|
+
const choices = [];
|
|
651
|
+
const lastTool = configManager.getLastUsedTool();
|
|
652
|
+
const { cmd } = startCommand(tool);
|
|
653
|
+
const installed = commandExists(cmd);
|
|
654
|
+
// Quick launch at top if this is the last used tool
|
|
655
|
+
if (lastTool === tool && installed) {
|
|
656
|
+
if (process.platform === 'win32') {
|
|
657
|
+
choices.push({ name: `đ Quick Launch ${toolLabel(tool)} (This Terminal)`, value: 'start_same' });
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
choices.push({ name: `đ Quick Launch ${toolLabel(tool)}`, value: 'start' });
|
|
661
|
+
}
|
|
662
|
+
choices.push(new inquirer.Separator());
|
|
663
|
+
}
|
|
664
|
+
// Sync with Global (first provider option)
|
|
665
|
+
if (hasProvider) {
|
|
666
|
+
const globalIncompat = getProviderIncompatibility(tool, chelperPlan);
|
|
667
|
+
if (globalIncompat) {
|
|
668
|
+
// Global provider is incompatible with this tool
|
|
669
|
+
choices.push({
|
|
670
|
+
name: chalk.gray(`đ Sync with Global (${planLabel(chelperPlan)}) ${chalk.red(`â ${globalIncompat}`)}`),
|
|
671
|
+
value: 'sync_global'
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
else if (matchesGlobal) {
|
|
675
|
+
choices.push({
|
|
676
|
+
name: chalk.gray(`đ Sync with Global (${planLabel(chelperPlan)}) â`),
|
|
677
|
+
value: 'sync_global'
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
choices.push({
|
|
682
|
+
name: `đ Sync with Global (${planLabel(chelperPlan)})`,
|
|
683
|
+
value: 'sync_global'
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
// Connect to Provider (Switch Profile)
|
|
688
|
+
choices.push({ name: 'đ Connect to Provider (Switch Profile)', value: 'switch_profile' });
|
|
689
|
+
// Change Model ID
|
|
690
|
+
if (isConfigured && toolPlan) {
|
|
691
|
+
choices.push({ name: 'đ§Ē Change Model ID', value: 'change_model' });
|
|
692
|
+
}
|
|
693
|
+
// Unload Configuration
|
|
694
|
+
if (isConfigured) {
|
|
695
|
+
choices.push({ name: 'đ Unload Configuration', value: 'unload' });
|
|
696
|
+
}
|
|
697
|
+
choices.push(new inquirer.Separator());
|
|
698
|
+
choices.push({ name: 'đ MCP Servers', value: 'mcp' });
|
|
699
|
+
// Show detection status inline
|
|
700
|
+
if (!installed) {
|
|
701
|
+
printWarning(`${toolLabel(tool)} was not detected on PATH`, installHint(tool).command ? `Install: ${installHint(tool).command}` : 'Please install it using the vendor instructions.');
|
|
702
|
+
}
|
|
703
|
+
// Launch options (only add if not shown as quick launch at top)
|
|
704
|
+
if (lastTool !== tool || !installed) {
|
|
705
|
+
choices.push(new inquirer.Separator());
|
|
706
|
+
if (process.platform === 'win32') {
|
|
707
|
+
choices.push({
|
|
708
|
+
name: installed ? `đ Launch ${toolLabel(tool)} (New Window)` : chalk.yellow(`đ Launch ${toolLabel(tool)} (New Window - not detected)`),
|
|
709
|
+
value: 'start_new',
|
|
710
|
+
});
|
|
711
|
+
choices.push({
|
|
712
|
+
name: installed ? `đ Launch ${toolLabel(tool)} (This Terminal)` : chalk.yellow(`đ Launch ${toolLabel(tool)} (This Terminal - not detected)`),
|
|
713
|
+
value: 'start_same',
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
choices.push({
|
|
718
|
+
name: installed ? `đ Launch ${toolLabel(tool)}` : chalk.yellow(`đ Launch ${toolLabel(tool)} (not detected)`),
|
|
719
|
+
value: 'start',
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
choices.push(new inquirer.Separator());
|
|
724
|
+
choices.push({ name: chalk.gray('â Back'), value: '__back' });
|
|
725
|
+
const { action } = await inquirer.prompt([{
|
|
726
|
+
type: 'list',
|
|
727
|
+
name: 'action',
|
|
728
|
+
message: 'Action:',
|
|
729
|
+
choices,
|
|
730
|
+
}]);
|
|
731
|
+
if (action === '__back')
|
|
732
|
+
return;
|
|
733
|
+
try {
|
|
734
|
+
if (action === 'switch_profile') {
|
|
735
|
+
const { selectedPlan } = await inquirer.prompt([{
|
|
736
|
+
type: 'list',
|
|
737
|
+
name: 'selectedPlan',
|
|
738
|
+
message: `Select provider for ${toolLabel(tool)}:`,
|
|
739
|
+
choices: [
|
|
740
|
+
...PROVIDER_CHOICES.map(c => {
|
|
741
|
+
const key = configManager.getApiKeyFor(c.value);
|
|
742
|
+
const status = key ? chalk.green(' â') : chalk.gray(' (not configured)');
|
|
743
|
+
const incompat = getProviderIncompatibility(tool, c.value);
|
|
744
|
+
if (incompat) {
|
|
745
|
+
// Show incompatible providers as disabled
|
|
746
|
+
return new inquirer.Separator(` ${chalk.gray.strikethrough(c.name)} ${chalk.red(`(${incompat})`)}`);
|
|
747
|
+
}
|
|
748
|
+
return { name: `${c.name}${status}`, value: c.value };
|
|
749
|
+
}),
|
|
750
|
+
new inquirer.Separator(),
|
|
751
|
+
{ name: chalk.gray('â Back'), value: '__back' },
|
|
752
|
+
],
|
|
753
|
+
}]);
|
|
754
|
+
if (selectedPlan === '__back')
|
|
755
|
+
continue;
|
|
756
|
+
let key = configManager.getApiKeyFor(selectedPlan);
|
|
757
|
+
if (!key) {
|
|
758
|
+
const { setupNow } = await inquirer.prompt([{
|
|
759
|
+
type: 'confirm',
|
|
760
|
+
name: 'setupNow',
|
|
761
|
+
message: `No credentials for ${planLabel(selectedPlan)}. Configure now?`,
|
|
762
|
+
default: true,
|
|
763
|
+
}]);
|
|
764
|
+
if (!setupNow)
|
|
765
|
+
continue;
|
|
766
|
+
await providerSetupFlow(selectedPlan);
|
|
767
|
+
key = configManager.getApiKeyFor(selectedPlan);
|
|
768
|
+
if (!key)
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
// After selecting provider, offer to select model for this tool
|
|
772
|
+
printInfo(`Select model for ${toolLabel(tool)} (current profile default: ${configManager.getProviderSettings(selectedPlan).model}):`);
|
|
773
|
+
const model = await selectModelId(selectedPlan, configManager.getProviderSettings(selectedPlan).model);
|
|
774
|
+
if (model === '__back')
|
|
775
|
+
continue;
|
|
776
|
+
const spinner = createSafeSpinner(`Applying ${planLabel(selectedPlan)} to ${toolLabel(tool)}...`).start();
|
|
777
|
+
try {
|
|
778
|
+
await toolManager.loadConfig(tool, selectedPlan, key, { model });
|
|
779
|
+
spinner.succeed(`Connected ${toolLabel(tool)} to ${planLabel(selectedPlan)} (${model})`);
|
|
780
|
+
}
|
|
781
|
+
catch (err) {
|
|
782
|
+
spinner.fail('Failed to apply configuration');
|
|
783
|
+
throw err;
|
|
784
|
+
}
|
|
785
|
+
await pause();
|
|
786
|
+
}
|
|
787
|
+
else if (action === 'sync_global') {
|
|
788
|
+
if (!hasProvider) {
|
|
789
|
+
printWarning('Configure a global provider first from the main menu.');
|
|
790
|
+
await pause();
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
const spinner = createSafeSpinner(`Applying ${planLabel(chelperPlan)} to ${toolLabel(tool)}...`).start();
|
|
794
|
+
try {
|
|
795
|
+
await toolManager.loadConfig(tool, chelperPlan, chelperKey);
|
|
796
|
+
spinner.succeed(`Now using ${planLabel(chelperPlan)}`);
|
|
797
|
+
}
|
|
798
|
+
catch (err) {
|
|
799
|
+
spinner.fail('Failed to apply configuration');
|
|
800
|
+
throw err;
|
|
801
|
+
}
|
|
802
|
+
await pause();
|
|
803
|
+
}
|
|
804
|
+
else if (action === 'change_model') {
|
|
805
|
+
if (!toolPlan)
|
|
806
|
+
continue;
|
|
807
|
+
const key = toolKey || configManager.getApiKeyFor(toolPlan);
|
|
808
|
+
if (!key) {
|
|
809
|
+
printWarning('Provider exists but no API key found. Configure it first.');
|
|
810
|
+
await pause();
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
const newModel = await selectModelId(toolPlan, toolModel || configManager.getProviderSettings(toolPlan).model);
|
|
814
|
+
if (newModel === '__back')
|
|
815
|
+
continue;
|
|
816
|
+
const spinner = createSafeSpinner(`Updating model to ${newModel}...`).start();
|
|
817
|
+
try {
|
|
818
|
+
await toolManager.loadConfig(tool, toolPlan, key, { model: newModel });
|
|
819
|
+
spinner.succeed('Model updated');
|
|
820
|
+
}
|
|
821
|
+
catch (err) {
|
|
822
|
+
spinner.fail('Failed to update model');
|
|
823
|
+
throw err;
|
|
824
|
+
}
|
|
825
|
+
await pause();
|
|
826
|
+
}
|
|
827
|
+
else if (action === 'unload') {
|
|
828
|
+
const { confirm } = await inquirer.prompt([{
|
|
829
|
+
type: 'confirm',
|
|
830
|
+
name: 'confirm',
|
|
831
|
+
message: chalk.yellow(`Unload configuration from ${toolLabel(tool)}?`),
|
|
832
|
+
default: false,
|
|
833
|
+
}]);
|
|
834
|
+
if (!confirm) {
|
|
835
|
+
printInfo('Cancelled');
|
|
836
|
+
await pause();
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
const spinner = createSafeSpinner('Unloading configuration...').start();
|
|
840
|
+
await toolManager.unloadConfig(tool);
|
|
841
|
+
spinner.succeed('Configuration unloaded');
|
|
842
|
+
await pause();
|
|
843
|
+
}
|
|
844
|
+
else if (action === 'mcp') {
|
|
845
|
+
await mcpMenu(tool);
|
|
846
|
+
}
|
|
847
|
+
else if (action === 'start') {
|
|
848
|
+
await launchTool(tool);
|
|
849
|
+
}
|
|
850
|
+
else if (action === 'start_new') {
|
|
851
|
+
await launchTool(tool, 'new');
|
|
852
|
+
}
|
|
853
|
+
else if (action === 'start_same') {
|
|
854
|
+
await launchTool(tool, 'same');
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
logger.logError('menu.toolAction', error);
|
|
859
|
+
printError(error instanceof Error ? error.message : String(error), 'Check "coder-link doctor" for configuration status');
|
|
860
|
+
await pause();
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
async function launchTool(tool, mode) {
|
|
865
|
+
const start = startCommand(tool);
|
|
866
|
+
configManager.setLastUsedTool(tool);
|
|
867
|
+
// Check sync status before launching
|
|
868
|
+
const auth = configManager.getAuth();
|
|
869
|
+
const chelperPlan = auth.plan;
|
|
870
|
+
const chelperKey = auth.apiKey;
|
|
871
|
+
const hasProvider = !!(chelperPlan && chelperKey);
|
|
872
|
+
// Get chelper model for kimi-like plans
|
|
873
|
+
let chelperModel;
|
|
874
|
+
if (chelperPlan && isKimiLikePlan(chelperPlan)) {
|
|
875
|
+
const settings = configManager.getProviderSettings(chelperPlan);
|
|
876
|
+
chelperModel = settings.model;
|
|
877
|
+
}
|
|
878
|
+
let toolPlan = null;
|
|
879
|
+
let toolKey = null;
|
|
880
|
+
let toolModel;
|
|
881
|
+
try {
|
|
882
|
+
const detected = await toolManager.detectCurrentConfig(tool);
|
|
883
|
+
toolPlan = detected.plan;
|
|
884
|
+
toolKey = detected.apiKey;
|
|
885
|
+
toolModel = detected.model;
|
|
886
|
+
}
|
|
887
|
+
catch {
|
|
888
|
+
// ignore
|
|
889
|
+
}
|
|
890
|
+
const isConfigured = !!(toolPlan && toolKey);
|
|
891
|
+
// Only prompt if tool is NOT configured but we have a global provider to offer
|
|
892
|
+
if (!isConfigured && hasProvider) {
|
|
893
|
+
console.log();
|
|
894
|
+
printInfo(`${toolLabel(tool)} is not configured.`);
|
|
895
|
+
console.log(chalk.gray(` Global default: ${chelperPlan}${chelperModel ? ` (${chelperModel})` : ''}`));
|
|
896
|
+
console.log();
|
|
897
|
+
const { action } = await inquirer.prompt([{
|
|
898
|
+
type: 'list',
|
|
899
|
+
name: 'action',
|
|
900
|
+
message: 'What would you like to do?',
|
|
901
|
+
choices: [
|
|
902
|
+
{ name: `đ Configure with ${planLabel(chelperPlan)} (global default)`, value: 'sync' },
|
|
903
|
+
{ name: 'đ Select a different provider...', value: 'select' },
|
|
904
|
+
{ name: chalk.gray('â Cancel'), value: 'cancel' },
|
|
905
|
+
],
|
|
906
|
+
}]);
|
|
907
|
+
if (action === 'cancel')
|
|
908
|
+
return;
|
|
909
|
+
if (action === 'select') {
|
|
910
|
+
// Fall through to tool menu for provider selection - just return and let user configure
|
|
911
|
+
printInfo('Use the tool menu to configure a provider first.');
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
// action === 'sync'
|
|
915
|
+
const spinner = createSafeSpinner(`Configuring with ${planLabel(chelperPlan)}...`).start();
|
|
916
|
+
try {
|
|
917
|
+
await toolManager.loadConfig(tool, chelperPlan, chelperKey);
|
|
918
|
+
spinner.succeed('Configuration applied');
|
|
919
|
+
}
|
|
920
|
+
catch (err) {
|
|
921
|
+
spinner.fail('Failed to apply configuration');
|
|
922
|
+
throw err;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
if (!commandExists(start.cmd)) {
|
|
926
|
+
console.log();
|
|
927
|
+
printWarning(`${toolLabel(tool)} was not detected on PATH.`);
|
|
928
|
+
const hint = installHint(tool);
|
|
929
|
+
const choices = [
|
|
930
|
+
{ name: 'đ Try launching anyway', value: 'anyway' },
|
|
931
|
+
];
|
|
932
|
+
if (hint.command) {
|
|
933
|
+
choices.push({ name: `đ Attempt to Install (${hint.label})`, value: 'install' });
|
|
934
|
+
}
|
|
935
|
+
choices.push({ name: chalk.gray('â Cancel'), value: 'cancel' });
|
|
936
|
+
const { failAction } = await inquirer.prompt([
|
|
937
|
+
{
|
|
938
|
+
type: 'list',
|
|
939
|
+
name: 'failAction',
|
|
940
|
+
message: 'What would you like to do?',
|
|
941
|
+
choices,
|
|
942
|
+
},
|
|
943
|
+
]);
|
|
944
|
+
if (failAction === 'cancel')
|
|
945
|
+
return;
|
|
946
|
+
if (failAction === 'install') {
|
|
947
|
+
console.log(`\n Running: ${chalk.cyan(hint.command)}\n`);
|
|
948
|
+
try {
|
|
949
|
+
await runInteractive(hint.command, []);
|
|
950
|
+
printSuccess('Installation command completed.');
|
|
951
|
+
}
|
|
952
|
+
catch (err) {
|
|
953
|
+
printError('Installation failed', err instanceof Error ? err.message : String(err));
|
|
954
|
+
}
|
|
955
|
+
await pause();
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
console.log(chalk.gray('\n Launching... (exit the tool to return here)\n'));
|
|
960
|
+
// Some interactive CLIs can freeze when launched inside the same terminal
|
|
961
|
+
// that is used for Inquirer prompts (especially on Windows).
|
|
962
|
+
// Offer launching in a new terminal window/tab.
|
|
963
|
+
let launchMode = mode || 'same';
|
|
964
|
+
if (!mode && process.platform === 'win32') {
|
|
965
|
+
const { mode: selectedMode } = await inquirer.prompt([
|
|
966
|
+
{
|
|
967
|
+
type: 'list',
|
|
968
|
+
name: 'mode',
|
|
969
|
+
message: 'Launch in:',
|
|
970
|
+
choices: [
|
|
971
|
+
{ name: 'New terminal window/tab', value: 'new' },
|
|
972
|
+
{ name: 'This terminal', value: 'same' },
|
|
973
|
+
],
|
|
974
|
+
default: 'new',
|
|
975
|
+
},
|
|
976
|
+
]);
|
|
977
|
+
launchMode = selectedMode;
|
|
978
|
+
}
|
|
979
|
+
if (tool === 'factory-droid') {
|
|
980
|
+
let factoryKey = configManager.getFactoryApiKey() || process.env.FACTORY_API_KEY;
|
|
981
|
+
if (!factoryKey) {
|
|
982
|
+
const { key } = await inquirer.prompt([
|
|
983
|
+
{
|
|
984
|
+
type: 'password',
|
|
985
|
+
name: 'key',
|
|
986
|
+
message: 'Factory API Key (FACTORY_API_KEY):',
|
|
987
|
+
mask: '*',
|
|
988
|
+
},
|
|
989
|
+
]);
|
|
990
|
+
const trimmed = key?.trim();
|
|
991
|
+
if (trimmed) {
|
|
992
|
+
const { save } = await inquirer.prompt([
|
|
993
|
+
{
|
|
994
|
+
type: 'confirm',
|
|
995
|
+
name: 'save',
|
|
996
|
+
message: 'Save to coder-link config?',
|
|
997
|
+
default: true,
|
|
998
|
+
},
|
|
999
|
+
]);
|
|
1000
|
+
if (save)
|
|
1001
|
+
configManager.setFactoryApiKey(trimmed);
|
|
1002
|
+
factoryKey = trimmed;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
if (launchMode === 'new') {
|
|
1006
|
+
const ok = runInNewTerminal(start.cmd, start.args, { FACTORY_API_KEY: factoryKey });
|
|
1007
|
+
if (!ok) {
|
|
1008
|
+
printWarning('Failed to open a new terminal window. Launching here instead.');
|
|
1009
|
+
await runInteractiveWithEnv(start.cmd, start.args, { FACTORY_API_KEY: factoryKey });
|
|
1010
|
+
}
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
await runInteractiveWithEnv(start.cmd, start.args, { FACTORY_API_KEY: factoryKey });
|
|
1014
|
+
}
|
|
1015
|
+
else {
|
|
1016
|
+
if (launchMode === 'new') {
|
|
1017
|
+
const ok = runInNewTerminal(start.cmd, start.args);
|
|
1018
|
+
if (!ok) {
|
|
1019
|
+
printWarning('Failed to open a new terminal window. Launching here instead.');
|
|
1020
|
+
await runInteractive(start.cmd, start.args);
|
|
1021
|
+
}
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
await runInteractive(start.cmd, start.args);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
// ââ Tool Select ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
1028
|
+
async function toolSelectMenu() {
|
|
1029
|
+
while (true) {
|
|
1030
|
+
console.clear();
|
|
1031
|
+
printHeader('Coding Tools');
|
|
1032
|
+
const auth = configManager.getAuth();
|
|
1033
|
+
printStatusBar(auth.plan, auth.apiKey);
|
|
1034
|
+
printConfigPathHint(configManager.configPath);
|
|
1035
|
+
printNavigationHints();
|
|
1036
|
+
const tools = toolManager.getSupportedTools();
|
|
1037
|
+
// Detect config state for each tool (in parallel)
|
|
1038
|
+
const toolStates = await Promise.all(tools.map(async (t) => {
|
|
1039
|
+
try {
|
|
1040
|
+
const d = await toolManager.detectCurrentConfig(t);
|
|
1041
|
+
const configured = !!(d.plan && d.apiKey);
|
|
1042
|
+
const { cmd } = startCommand(t);
|
|
1043
|
+
const installed = commandExists(cmd);
|
|
1044
|
+
return { tool: t, configured, installed };
|
|
1045
|
+
}
|
|
1046
|
+
catch {
|
|
1047
|
+
return { tool: t, configured: false, installed: false };
|
|
1048
|
+
}
|
|
1049
|
+
}));
|
|
1050
|
+
const { tool } = await inquirer.prompt([{
|
|
1051
|
+
type: 'list',
|
|
1052
|
+
name: 'tool',
|
|
1053
|
+
message: 'Select tool:',
|
|
1054
|
+
choices: [
|
|
1055
|
+
...toolStates.map(s => {
|
|
1056
|
+
const status = statusIndicator(s.configured);
|
|
1057
|
+
const inst = s.installed ? '' : chalk.yellow(' (not detected)');
|
|
1058
|
+
return {
|
|
1059
|
+
name: `${status} ${toolLabel(s.tool)}${inst}`,
|
|
1060
|
+
value: s.tool,
|
|
1061
|
+
};
|
|
1062
|
+
}),
|
|
1063
|
+
new inquirer.Separator(),
|
|
1064
|
+
{ name: chalk.gray('â Back'), value: '__back' },
|
|
1065
|
+
],
|
|
1066
|
+
}]);
|
|
1067
|
+
if (tool === '__back')
|
|
1068
|
+
return;
|
|
1069
|
+
await toolMenu(tool);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
async function diagnosticsMenu() {
|
|
1073
|
+
console.clear();
|
|
1074
|
+
printHeader('System Diagnostics');
|
|
1075
|
+
const auth = configManager.getAuth();
|
|
1076
|
+
const plan = auth.plan;
|
|
1077
|
+
const apiKey = auth.apiKey;
|
|
1078
|
+
console.log(` ${i18n.t('doctor.config_path', { path: configManager.configPath })}`);
|
|
1079
|
+
console.log(` ${i18n.t('doctor.current_auth')}`);
|
|
1080
|
+
if (plan && apiKey) {
|
|
1081
|
+
console.log(` ${i18n.t('doctor.plan')}: ${planLabelColored(plan)}`);
|
|
1082
|
+
console.log(` ${i18n.t('doctor.api_key')}: ${maskApiKey(apiKey)}`);
|
|
1083
|
+
}
|
|
1084
|
+
else {
|
|
1085
|
+
console.log(` ${chalk.yellow(i18n.t('doctor.not_set'))}`);
|
|
1086
|
+
}
|
|
1087
|
+
console.log('\n ' + i18n.t('doctor.tools_header'));
|
|
1088
|
+
const tools = toolManager.getSupportedTools();
|
|
1089
|
+
for (const tool of tools) {
|
|
1090
|
+
const status = await toolManager.isConfigured(tool);
|
|
1091
|
+
console.log(` ${statusIndicator(status)} ${toolLabel(tool)}`);
|
|
1092
|
+
}
|
|
1093
|
+
// MCP status
|
|
1094
|
+
const { kimiManager } = await import('./lib/kimi-manager.js');
|
|
1095
|
+
const mcpInstalled = kimiManager.getInstalledMCPs();
|
|
1096
|
+
console.log('\n ' + i18n.t('doctor.mcp_header'));
|
|
1097
|
+
if (mcpInstalled.length === 0) {
|
|
1098
|
+
console.log(` ${chalk.gray(i18n.t('doctor.none'))}`);
|
|
1099
|
+
}
|
|
1100
|
+
else {
|
|
1101
|
+
for (const id of mcpInstalled) {
|
|
1102
|
+
console.log(` ${chalk.green('â')} ${id}`);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
console.log();
|
|
1106
|
+
await pause();
|
|
1107
|
+
}
|
|
1108
|
+
async function logsMenu() {
|
|
1109
|
+
const LOG_DIR = join(CONFIG_DIR, 'logs');
|
|
1110
|
+
const LOG_FILE = join(LOG_DIR, 'error.log');
|
|
1111
|
+
while (true) {
|
|
1112
|
+
console.clear();
|
|
1113
|
+
printHeader('Error Logs');
|
|
1114
|
+
if (!existsSync(LOG_FILE)) {
|
|
1115
|
+
printInfo('No logs found.');
|
|
1116
|
+
}
|
|
1117
|
+
else {
|
|
1118
|
+
try {
|
|
1119
|
+
const logs = readFileSync(LOG_FILE, 'utf-8');
|
|
1120
|
+
const lines = logs.split('\n').filter(l => l.trim()).slice(-15);
|
|
1121
|
+
if (lines.length === 0) {
|
|
1122
|
+
printInfo('Log file is empty.');
|
|
1123
|
+
}
|
|
1124
|
+
else {
|
|
1125
|
+
console.log(chalk.gray(' Last 15 log entries:\n'));
|
|
1126
|
+
lines.forEach(line => {
|
|
1127
|
+
if (line.includes('[ERROR]')) {
|
|
1128
|
+
console.log(` ${chalk.red(line)}`);
|
|
1129
|
+
}
|
|
1130
|
+
else if (line.includes('[WARN]')) {
|
|
1131
|
+
console.log(` ${chalk.yellow(line)}`);
|
|
1132
|
+
}
|
|
1133
|
+
else {
|
|
1134
|
+
console.log(` ${chalk.gray(line)}`);
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
catch (e) {
|
|
1140
|
+
printError('Failed to read log file');
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
console.log();
|
|
1144
|
+
const { action } = await inquirer.prompt([{
|
|
1145
|
+
type: 'list',
|
|
1146
|
+
name: 'action',
|
|
1147
|
+
message: 'Action:',
|
|
1148
|
+
choices: [
|
|
1149
|
+
{ name: 'đ Clear Logs', value: 'clear' },
|
|
1150
|
+
new inquirer.Separator(),
|
|
1151
|
+
{ name: chalk.gray('â Back'), value: 'back' }
|
|
1152
|
+
]
|
|
1153
|
+
}]);
|
|
1154
|
+
if (action === 'back')
|
|
1155
|
+
return;
|
|
1156
|
+
if (action === 'clear' && existsSync(LOG_FILE)) {
|
|
1157
|
+
writeFileSync(LOG_FILE, '', 'utf-8');
|
|
1158
|
+
printSuccess('Logs cleared');
|
|
1159
|
+
await pause();
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
// ââ Main Menu ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
1164
|
+
export async function runMenu() {
|
|
1165
|
+
i18n.setLang(configManager.getLang());
|
|
1166
|
+
// Show splash only on first launch
|
|
1167
|
+
console.clear();
|
|
1168
|
+
printSplash();
|
|
1169
|
+
printConfigPathHint(configManager.configPath);
|
|
1170
|
+
await new Promise(r => setTimeout(r, 400)); // brief pause to appreciate it
|
|
1171
|
+
while (true) {
|
|
1172
|
+
console.clear();
|
|
1173
|
+
printSplash();
|
|
1174
|
+
const auth = configManager.getAuth();
|
|
1175
|
+
printStatusBar(auth.plan, auth.apiKey, auth.plan ? providerSummary(auth.plan).trim() : undefined);
|
|
1176
|
+
printNavigationHints();
|
|
1177
|
+
const mainChoices = [];
|
|
1178
|
+
mainChoices.push({ name: '⥠Provider & API Key', value: 'provider' }, { name: 'đ Coding Tools', value: 'tools' }, { name: 'đ Language', value: 'lang' }, new inquirer.Separator(), { name: 'đŦ System Diagnostics (Doctor)', value: 'doctor' }, { name: 'đ View Logs', value: 'logs' }, new inquirer.Separator(), { name: chalk.gray('Exit'), value: 'exit' });
|
|
1179
|
+
const { op } = await inquirer.prompt([{
|
|
1180
|
+
type: 'list',
|
|
1181
|
+
name: 'op',
|
|
1182
|
+
message: 'Main Menu:',
|
|
1183
|
+
choices: mainChoices,
|
|
1184
|
+
}]);
|
|
1185
|
+
if (op === 'exit') {
|
|
1186
|
+
console.log(chalk.gray('\n Goodbye!\n'));
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
try {
|
|
1190
|
+
if (op === 'provider') {
|
|
1191
|
+
await providerMenu();
|
|
1192
|
+
}
|
|
1193
|
+
else if (op === 'tools') {
|
|
1194
|
+
await toolSelectMenu();
|
|
1195
|
+
}
|
|
1196
|
+
else if (op === 'doctor') {
|
|
1197
|
+
await diagnosticsMenu();
|
|
1198
|
+
}
|
|
1199
|
+
else if (op === 'logs') {
|
|
1200
|
+
await logsMenu();
|
|
1201
|
+
}
|
|
1202
|
+
else if (op === 'lang') {
|
|
1203
|
+
const { lang } = await inquirer.prompt([{
|
|
1204
|
+
type: 'list',
|
|
1205
|
+
name: 'lang',
|
|
1206
|
+
message: 'Select language:',
|
|
1207
|
+
choices: [
|
|
1208
|
+
{ name: 'įŽäŊ䏿', value: 'zh_CN' },
|
|
1209
|
+
{ name: 'English', value: 'en_US' },
|
|
1210
|
+
],
|
|
1211
|
+
default: configManager.getLang(),
|
|
1212
|
+
}]);
|
|
1213
|
+
configManager.setLang(lang);
|
|
1214
|
+
i18n.setLang(lang);
|
|
1215
|
+
printSuccess(`Language set to ${lang === 'zh_CN' ? 'įŽäŊ䏿' : 'English'}`);
|
|
1216
|
+
await pause();
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
catch (error) {
|
|
1220
|
+
logger.logError('menu.main', error);
|
|
1221
|
+
printError(error instanceof Error ? error.message : String(error), 'Run "coder-link doctor" to check system configuration');
|
|
1222
|
+
await pause();
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
//# sourceMappingURL=menu.js.map
|