@adversity/coding-tool-x 2.2.0
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/CHANGELOG.md +333 -0
- package/LICENSE +21 -0
- package/README.md +404 -0
- package/bin/ctx.js +8 -0
- package/dist/web/assets/index-D1AYlFLZ.js +3220 -0
- package/dist/web/assets/index-aL3cKxSK.css +41 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +14 -0
- package/dist/web/logo.png +0 -0
- package/docs/CHANGELOG.md +582 -0
- package/docs/DIRECTORY_MIGRATION.md +112 -0
- package/docs/PROJECT_STRUCTURE.md +396 -0
- package/docs/bannel.png +0 -0
- package/docs/home.png +0 -0
- package/docs/logo.png +0 -0
- package/docs/multi-channel-load-balancing.md +249 -0
- package/package.json +73 -0
- package/src/commands/channels.js +504 -0
- package/src/commands/cli-type.js +99 -0
- package/src/commands/daemon.js +286 -0
- package/src/commands/doctor.js +332 -0
- package/src/commands/list.js +222 -0
- package/src/commands/logs.js +259 -0
- package/src/commands/port-config.js +115 -0
- package/src/commands/proxy-control.js +258 -0
- package/src/commands/proxy.js +152 -0
- package/src/commands/resume.js +137 -0
- package/src/commands/search.js +190 -0
- package/src/commands/stats.js +224 -0
- package/src/commands/switch.js +48 -0
- package/src/commands/toggle-proxy.js +222 -0
- package/src/commands/ui.js +92 -0
- package/src/commands/workspace.js +454 -0
- package/src/config/default.js +40 -0
- package/src/config/loader.js +75 -0
- package/src/config/paths.js +121 -0
- package/src/index.js +373 -0
- package/src/reset-config.js +92 -0
- package/src/server/api/agents.js +248 -0
- package/src/server/api/aliases.js +36 -0
- package/src/server/api/channels.js +258 -0
- package/src/server/api/claude-hooks.js +480 -0
- package/src/server/api/codex-channels.js +312 -0
- package/src/server/api/codex-projects.js +91 -0
- package/src/server/api/codex-proxy.js +182 -0
- package/src/server/api/codex-sessions.js +491 -0
- package/src/server/api/codex-statistics.js +57 -0
- package/src/server/api/commands.js +245 -0
- package/src/server/api/config-templates.js +182 -0
- package/src/server/api/config.js +147 -0
- package/src/server/api/convert.js +127 -0
- package/src/server/api/dashboard.js +125 -0
- package/src/server/api/env.js +144 -0
- package/src/server/api/favorites.js +77 -0
- package/src/server/api/gemini-channels.js +261 -0
- package/src/server/api/gemini-projects.js +91 -0
- package/src/server/api/gemini-proxy.js +160 -0
- package/src/server/api/gemini-sessions.js +397 -0
- package/src/server/api/gemini-statistics.js +57 -0
- package/src/server/api/health-check.js +118 -0
- package/src/server/api/mcp.js +336 -0
- package/src/server/api/pm2-autostart.js +269 -0
- package/src/server/api/projects.js +124 -0
- package/src/server/api/prompts.js +279 -0
- package/src/server/api/proxy.js +235 -0
- package/src/server/api/rules.js +271 -0
- package/src/server/api/sessions.js +595 -0
- package/src/server/api/settings.js +61 -0
- package/src/server/api/skills.js +305 -0
- package/src/server/api/statistics.js +91 -0
- package/src/server/api/terminal.js +202 -0
- package/src/server/api/ui-config.js +64 -0
- package/src/server/api/workspaces.js +407 -0
- package/src/server/codex-proxy-server.js +538 -0
- package/src/server/dev-server.js +26 -0
- package/src/server/gemini-proxy-server.js +518 -0
- package/src/server/index.js +305 -0
- package/src/server/proxy-server.js +469 -0
- package/src/server/services/agents-service.js +354 -0
- package/src/server/services/alias.js +71 -0
- package/src/server/services/channel-health.js +234 -0
- package/src/server/services/channel-scheduler.js +234 -0
- package/src/server/services/channels.js +347 -0
- package/src/server/services/codex-channels.js +625 -0
- package/src/server/services/codex-config.js +90 -0
- package/src/server/services/codex-parser.js +322 -0
- package/src/server/services/codex-sessions.js +665 -0
- package/src/server/services/codex-settings-manager.js +397 -0
- package/src/server/services/codex-speed-test-template.json +24 -0
- package/src/server/services/codex-statistics-service.js +255 -0
- package/src/server/services/commands-service.js +360 -0
- package/src/server/services/config-templates-service.js +732 -0
- package/src/server/services/env-checker.js +307 -0
- package/src/server/services/env-manager.js +300 -0
- package/src/server/services/favorites.js +163 -0
- package/src/server/services/gemini-channels.js +333 -0
- package/src/server/services/gemini-config.js +73 -0
- package/src/server/services/gemini-sessions.js +689 -0
- package/src/server/services/gemini-settings-manager.js +263 -0
- package/src/server/services/gemini-statistics-service.js +253 -0
- package/src/server/services/health-check.js +399 -0
- package/src/server/services/mcp-service.js +1188 -0
- package/src/server/services/prompts-service.js +492 -0
- package/src/server/services/proxy-runtime.js +79 -0
- package/src/server/services/pty-manager.js +435 -0
- package/src/server/services/rules-service.js +401 -0
- package/src/server/services/session-cache.js +127 -0
- package/src/server/services/session-converter.js +577 -0
- package/src/server/services/sessions.js +757 -0
- package/src/server/services/settings-manager.js +163 -0
- package/src/server/services/skill-service.js +965 -0
- package/src/server/services/speed-test.js +545 -0
- package/src/server/services/statistics-service.js +386 -0
- package/src/server/services/terminal-commands.js +155 -0
- package/src/server/services/terminal-config.js +140 -0
- package/src/server/services/terminal-detector.js +306 -0
- package/src/server/services/ui-config.js +130 -0
- package/src/server/services/workspace-service.js +662 -0
- package/src/server/utils/pricing.js +41 -0
- package/src/server/websocket-server.js +557 -0
- package/src/ui/menu.js +129 -0
- package/src/ui/prompts.js +100 -0
- package/src/utils/format.js +43 -0
- package/src/utils/port-helper.js +94 -0
- package/src/utils/session.js +239 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const { startServer } = require('../server');
|
|
3
|
+
const open = require('open');
|
|
4
|
+
const { getProxyStatus } = require('../server/proxy-server');
|
|
5
|
+
const { loadConfig } = require('../config/loader');
|
|
6
|
+
|
|
7
|
+
async function handleUI() {
|
|
8
|
+
// 检查是否为 daemon 模式(PM2 启动)
|
|
9
|
+
const isDaemon = process.argv.includes('--daemon');
|
|
10
|
+
|
|
11
|
+
if (!isDaemon) {
|
|
12
|
+
console.clear();
|
|
13
|
+
console.log(chalk.cyan.bold('\n🌐 启动 Coding-Tool Web UI...\n'));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 从配置加载端口
|
|
17
|
+
const config = loadConfig();
|
|
18
|
+
const port = config.ports?.webUI || 19999;
|
|
19
|
+
const url = `http://localhost:${port}`;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await startServer(port);
|
|
23
|
+
|
|
24
|
+
// 自动打开浏览器(仅非 daemon 模式)
|
|
25
|
+
if (!isDaemon) {
|
|
26
|
+
setTimeout(async () => {
|
|
27
|
+
try {
|
|
28
|
+
await open(url);
|
|
29
|
+
console.log(chalk.green(`✅ 已在浏览器中打开: ${url}\n`));
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.log(chalk.yellow(`💡 请手动打开: ${url}\n`));
|
|
32
|
+
}
|
|
33
|
+
}, 1000);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 处理退出信号(仅非 daemon 模式)
|
|
37
|
+
if (!isDaemon) {
|
|
38
|
+
process.on('SIGINT', async () => {
|
|
39
|
+
console.log(chalk.yellow('\n\n👋 正在停止服务器...\n'));
|
|
40
|
+
|
|
41
|
+
// 检查代理状态并询问是否停止
|
|
42
|
+
try {
|
|
43
|
+
const proxyStatus = getProxyStatus();
|
|
44
|
+
if (proxyStatus.running) {
|
|
45
|
+
console.log(chalk.yellow('⚠️ 检测到代理服务正在运行'));
|
|
46
|
+
console.log(chalk.gray(' - 代理端口: ' + proxyStatus.port));
|
|
47
|
+
console.log(chalk.gray(' - 如需保持代理运行,请直接关闭此窗口\n'));
|
|
48
|
+
|
|
49
|
+
// 自动停止代理(3秒后)
|
|
50
|
+
console.log(chalk.cyan('⏳ 将在 3 秒后自动停止代理服务...'));
|
|
51
|
+
console.log(chalk.gray(' 按 Ctrl+C 再次可立即退出并保持代理运行\n'));
|
|
52
|
+
|
|
53
|
+
let stopProxy = true;
|
|
54
|
+
const secondSigint = () => {
|
|
55
|
+
stopProxy = false;
|
|
56
|
+
process.off('SIGINT', secondSigint);
|
|
57
|
+
};
|
|
58
|
+
process.on('SIGINT', secondSigint);
|
|
59
|
+
|
|
60
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
61
|
+
process.off('SIGINT', secondSigint);
|
|
62
|
+
|
|
63
|
+
if (stopProxy) {
|
|
64
|
+
const { stopProxyServer } = require('../server/proxy-server');
|
|
65
|
+
await stopProxyServer();
|
|
66
|
+
console.log(chalk.green('✅ 代理服务已停止\n'));
|
|
67
|
+
} else {
|
|
68
|
+
console.log(chalk.yellow('⚠️ 代理服务保持运行状态'));
|
|
69
|
+
console.log(chalk.gray(' - 如需停止,请运行: ctx proxy stop\n'));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
// 忽略错误
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log(chalk.green('✅ Web UI 已停止\n'));
|
|
77
|
+
process.exit(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
console.log(chalk.gray('按 Ctrl+C 停止服务器'));
|
|
81
|
+
} else {
|
|
82
|
+
// Daemon 模式:保持运行
|
|
83
|
+
console.log(chalk.green(`✅ Coding-Tool 服务已在后台启动 (端口: ${port})`));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error(chalk.red('启动服务器失败:'), error.message);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { handleUI };
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
// 工作区管理命令
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const inquirer = require('inquirer');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const workspaceService = require('../server/services/workspace-service');
|
|
7
|
+
const { getProjectsWithStats } = require('../server/services/sessions');
|
|
8
|
+
const { loadConfig } = require('../config/loader');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 列出所有工作区
|
|
12
|
+
*/
|
|
13
|
+
async function listWorkspaces() {
|
|
14
|
+
try {
|
|
15
|
+
const workspaces = workspaceService.listWorkspaces();
|
|
16
|
+
|
|
17
|
+
if (workspaces.length === 0) {
|
|
18
|
+
console.log(chalk.yellow('\n暂无工作区\n'));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(chalk.bold.cyan('\n工作区列表:\n'));
|
|
23
|
+
|
|
24
|
+
workspaces.forEach((ws, index) => {
|
|
25
|
+
const status = ws.exists ? chalk.green('✓') : chalk.red('✗');
|
|
26
|
+
console.log(`${index + 1}. ${status} ${chalk.bold(ws.name)}`);
|
|
27
|
+
|
|
28
|
+
if (ws.description) {
|
|
29
|
+
console.log(chalk.gray(` 描述: ${ws.description}`));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(chalk.gray(` 路径: ${ws.path}`));
|
|
33
|
+
console.log(chalk.gray(` 项目数: ${ws.projectCount}`));
|
|
34
|
+
console.log(chalk.gray(` 最后使用: ${new Date(ws.lastUsed).toLocaleString('zh-CN')}`));
|
|
35
|
+
console.log('');
|
|
36
|
+
});
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error(chalk.red(`\n❌ ${error.message}\n`));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 创建工作区
|
|
44
|
+
*/
|
|
45
|
+
async function createWorkspace() {
|
|
46
|
+
try {
|
|
47
|
+
const config = loadConfig();
|
|
48
|
+
|
|
49
|
+
// 1. 输入工作区名称
|
|
50
|
+
const { name } = await inquirer.prompt([
|
|
51
|
+
{
|
|
52
|
+
type: 'input',
|
|
53
|
+
name: 'name',
|
|
54
|
+
message: '工作区名称:',
|
|
55
|
+
validate: input => {
|
|
56
|
+
if (!input || !input.trim()) {
|
|
57
|
+
return '名称不能为空';
|
|
58
|
+
}
|
|
59
|
+
// 检查名称是否包含非法字符
|
|
60
|
+
if (!/^[a-zA-Z0-9_\-\u4e00-\u9fa5]+$/.test(input)) {
|
|
61
|
+
return '名称只能包含字母、数字、下划线、中划线和中文';
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
// 2. 输入描述(可选)
|
|
69
|
+
const { description } = await inquirer.prompt([
|
|
70
|
+
{
|
|
71
|
+
type: 'input',
|
|
72
|
+
name: 'description',
|
|
73
|
+
message: '工作区描述(可选):',
|
|
74
|
+
default: ''
|
|
75
|
+
}
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
// 3. 选择基础目录
|
|
79
|
+
const { baseDirOption } = await inquirer.prompt([
|
|
80
|
+
{
|
|
81
|
+
type: 'list',
|
|
82
|
+
name: 'baseDirOption',
|
|
83
|
+
message: '选择工作区存放位置:',
|
|
84
|
+
choices: [
|
|
85
|
+
{ name: '自动(第一个项目的父目录)', value: 'auto' },
|
|
86
|
+
{ name: '自定义路径', value: 'custom' }
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
let baseDir = '';
|
|
92
|
+
if (baseDirOption === 'custom') {
|
|
93
|
+
const { customPath } = await inquirer.prompt([
|
|
94
|
+
{
|
|
95
|
+
type: 'input',
|
|
96
|
+
name: 'customPath',
|
|
97
|
+
message: '输入基础目录路径:',
|
|
98
|
+
validate: input => {
|
|
99
|
+
if (!input || !input.trim()) {
|
|
100
|
+
return '路径不能为空';
|
|
101
|
+
}
|
|
102
|
+
const expanded = input.replace(/^~/, require('os').homedir());
|
|
103
|
+
if (!fs.existsSync(expanded)) {
|
|
104
|
+
return `路径不存在: ${expanded}`;
|
|
105
|
+
}
|
|
106
|
+
if (!fs.statSync(expanded).isDirectory()) {
|
|
107
|
+
return '必须是目录路径';
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
]);
|
|
113
|
+
baseDir = customPath.replace(/^~/, require('os').homedir());
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 4. 选择项目
|
|
117
|
+
const projects = [];
|
|
118
|
+
let continueAdding = true;
|
|
119
|
+
|
|
120
|
+
while (continueAdding) {
|
|
121
|
+
const availableProjects = getProjectsWithStats(config);
|
|
122
|
+
|
|
123
|
+
if (availableProjects.length === 0) {
|
|
124
|
+
console.log(chalk.yellow('\n没有可用的项目\n'));
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const projectChoices = availableProjects.map(proj => ({
|
|
129
|
+
name: `${proj.displayName} (${proj.sessionCount} 会话)`,
|
|
130
|
+
value: proj
|
|
131
|
+
}));
|
|
132
|
+
|
|
133
|
+
projectChoices.push(
|
|
134
|
+
new inquirer.Separator(chalk.gray('─'.repeat(14))),
|
|
135
|
+
{ name: chalk.gray('✓ 完成选择'), value: null }
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const { selectedProject } = await inquirer.prompt([
|
|
139
|
+
{
|
|
140
|
+
type: 'list',
|
|
141
|
+
name: 'selectedProject',
|
|
142
|
+
message: `选择项目 (${projects.length} 个已选):`,
|
|
143
|
+
pageSize: 15,
|
|
144
|
+
choices: projectChoices
|
|
145
|
+
}
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
if (!selectedProject) {
|
|
149
|
+
if (projects.length === 0) {
|
|
150
|
+
console.log(chalk.yellow('\n至少需要选择一个项目\n'));
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
continueAdding = false;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 检查是否已添加
|
|
158
|
+
if (projects.find(p => p.sourcePath === selectedProject.fullPath)) {
|
|
159
|
+
console.log(chalk.yellow('\n该项目已添加\n'));
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 获取项目路径
|
|
164
|
+
const sourcePath = selectedProject.fullPath;
|
|
165
|
+
|
|
166
|
+
// 输入软链接名称
|
|
167
|
+
const defaultLinkName = path.basename(sourcePath);
|
|
168
|
+
const { linkName } = await inquirer.prompt([
|
|
169
|
+
{
|
|
170
|
+
type: 'input',
|
|
171
|
+
name: 'linkName',
|
|
172
|
+
message: '软链接名称:',
|
|
173
|
+
default: defaultLinkName,
|
|
174
|
+
validate: input => {
|
|
175
|
+
if (!input || !input.trim()) {
|
|
176
|
+
return '名称不能为空';
|
|
177
|
+
}
|
|
178
|
+
if (projects.find(p => p.name === input)) {
|
|
179
|
+
return '该名称已被使用';
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
]);
|
|
185
|
+
|
|
186
|
+
// 检查是否是 git 仓库
|
|
187
|
+
const isGit = workspaceService.isGitRepo(sourcePath);
|
|
188
|
+
|
|
189
|
+
let createWorktree = false;
|
|
190
|
+
let branch = '';
|
|
191
|
+
|
|
192
|
+
if (isGit) {
|
|
193
|
+
const { shouldCreateWorktree } = await inquirer.prompt([
|
|
194
|
+
{
|
|
195
|
+
type: 'confirm',
|
|
196
|
+
name: 'shouldCreateWorktree',
|
|
197
|
+
message: '是否为此项目创建 git worktree?',
|
|
198
|
+
default: false
|
|
199
|
+
}
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
if (shouldCreateWorktree) {
|
|
203
|
+
const { branchName } = await inquirer.prompt([
|
|
204
|
+
{
|
|
205
|
+
type: 'input',
|
|
206
|
+
name: 'branchName',
|
|
207
|
+
message: '输入分支名:',
|
|
208
|
+
validate: input => {
|
|
209
|
+
if (!input || !input.trim()) {
|
|
210
|
+
return '分支名不能为空';
|
|
211
|
+
}
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
]);
|
|
216
|
+
|
|
217
|
+
createWorktree = true;
|
|
218
|
+
branch = branchName;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
projects.push({
|
|
223
|
+
sourcePath,
|
|
224
|
+
name: linkName,
|
|
225
|
+
createWorktree,
|
|
226
|
+
branch
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
console.log(chalk.green(`\n✓ 已添加: ${linkName}\n`));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (projects.length === 0) {
|
|
233
|
+
console.log(chalk.yellow('\n取消创建\n'));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 5. 确认创建
|
|
238
|
+
console.log(chalk.bold.cyan('\n工作区配置预览:\n'));
|
|
239
|
+
console.log(chalk.gray(`名称: ${name}`));
|
|
240
|
+
if (description) {
|
|
241
|
+
console.log(chalk.gray(`描述: ${description}`));
|
|
242
|
+
}
|
|
243
|
+
console.log(chalk.gray(`项目数: ${projects.length}`));
|
|
244
|
+
console.log(chalk.gray('\n包含项目:'));
|
|
245
|
+
projects.forEach((proj, index) => {
|
|
246
|
+
console.log(chalk.gray(` ${index + 1}. ${proj.name} → ${proj.sourcePath}`));
|
|
247
|
+
if (proj.createWorktree) {
|
|
248
|
+
console.log(chalk.gray(` (创建 worktree: ${proj.branch})`));
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
console.log('');
|
|
252
|
+
|
|
253
|
+
const { confirm } = await inquirer.prompt([
|
|
254
|
+
{
|
|
255
|
+
type: 'confirm',
|
|
256
|
+
name: 'confirm',
|
|
257
|
+
message: '确认创建工作区?',
|
|
258
|
+
default: true
|
|
259
|
+
}
|
|
260
|
+
]);
|
|
261
|
+
|
|
262
|
+
if (!confirm) {
|
|
263
|
+
console.log(chalk.yellow('\n取消创建\n'));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 创建工作区
|
|
268
|
+
console.log(chalk.cyan('\n正在创建工作区...\n'));
|
|
269
|
+
|
|
270
|
+
const workspace = workspaceService.createWorkspace({
|
|
271
|
+
name,
|
|
272
|
+
description,
|
|
273
|
+
baseDir,
|
|
274
|
+
projects
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
console.log(chalk.green(`\n✅ 工作区创建成功!\n`));
|
|
278
|
+
console.log(chalk.gray(`工作区路径: ${workspace.path}\n`));
|
|
279
|
+
console.log(chalk.gray(`提示: 可以在此路径下启动 Claude Code 以访问所有项目\n`));
|
|
280
|
+
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.error(chalk.red(`\n❌ ${error.message}\n`));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 查看工作区详情
|
|
288
|
+
*/
|
|
289
|
+
async function viewWorkspace() {
|
|
290
|
+
try {
|
|
291
|
+
const workspaces = workspaceService.listWorkspaces();
|
|
292
|
+
|
|
293
|
+
if (workspaces.length === 0) {
|
|
294
|
+
console.log(chalk.yellow('\n暂无工作区\n'));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const { workspace } = await inquirer.prompt([
|
|
299
|
+
{
|
|
300
|
+
type: 'list',
|
|
301
|
+
name: 'workspace',
|
|
302
|
+
message: '选择工作区:',
|
|
303
|
+
pageSize: 15,
|
|
304
|
+
choices: workspaces.map(ws => ({
|
|
305
|
+
name: `${ws.name} (${ws.projectCount} 个项目)`,
|
|
306
|
+
value: ws
|
|
307
|
+
}))
|
|
308
|
+
}
|
|
309
|
+
]);
|
|
310
|
+
|
|
311
|
+
const detail = workspaceService.getWorkspace(workspace.id);
|
|
312
|
+
|
|
313
|
+
console.log(chalk.bold.cyan(`\n工作区: ${detail.name}\n`));
|
|
314
|
+
|
|
315
|
+
if (detail.description) {
|
|
316
|
+
console.log(chalk.gray(`描述: ${detail.description}`));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
console.log(chalk.gray(`路径: ${detail.path}`));
|
|
320
|
+
console.log(chalk.gray(`状态: ${detail.exists ? chalk.green('存在') : chalk.red('不存在')}`));
|
|
321
|
+
console.log(chalk.gray(`创建时间: ${new Date(detail.createdAt).toLocaleString('zh-CN')}`));
|
|
322
|
+
console.log(chalk.gray(`最后使用: ${new Date(detail.lastUsed).toLocaleString('zh-CN')}`));
|
|
323
|
+
|
|
324
|
+
console.log(chalk.bold.cyan(`\n包含项目 (${detail.projects.length}):\n`));
|
|
325
|
+
|
|
326
|
+
detail.projects.forEach((proj, index) => {
|
|
327
|
+
const linkStatus = proj.linkExists ? chalk.green('✓') : chalk.red('✗');
|
|
328
|
+
const sourceStatus = proj.sourceExists ? chalk.green('✓') : chalk.red('✗');
|
|
329
|
+
|
|
330
|
+
console.log(`${index + 1}. ${linkStatus} ${chalk.bold(proj.name)}`);
|
|
331
|
+
console.log(chalk.gray(` 源路径: ${sourceStatus} ${proj.sourcePath}`));
|
|
332
|
+
|
|
333
|
+
if (proj.worktrees && proj.worktrees.length > 0) {
|
|
334
|
+
console.log(chalk.gray(` Worktrees (${proj.worktrees.length}):`));
|
|
335
|
+
proj.worktrees.forEach(wt => {
|
|
336
|
+
console.log(chalk.gray(` - ${wt.branch || 'detached'}: ${wt.path}`));
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
console.log('');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.error(chalk.red(`\n❌ ${error.message}\n`));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* 删除工作区
|
|
350
|
+
*/
|
|
351
|
+
async function deleteWorkspace() {
|
|
352
|
+
try {
|
|
353
|
+
const workspaces = workspaceService.listWorkspaces();
|
|
354
|
+
|
|
355
|
+
if (workspaces.length === 0) {
|
|
356
|
+
console.log(chalk.yellow('\n暂无工作区\n'));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const { workspace } = await inquirer.prompt([
|
|
361
|
+
{
|
|
362
|
+
type: 'list',
|
|
363
|
+
name: 'workspace',
|
|
364
|
+
message: '选择要删除的工作区:',
|
|
365
|
+
pageSize: 15,
|
|
366
|
+
choices: workspaces.map(ws => ({
|
|
367
|
+
name: `${ws.name} (${ws.projectCount} 个项目)`,
|
|
368
|
+
value: ws
|
|
369
|
+
}))
|
|
370
|
+
}
|
|
371
|
+
]);
|
|
372
|
+
|
|
373
|
+
const { removeFiles } = await inquirer.prompt([
|
|
374
|
+
{
|
|
375
|
+
type: 'confirm',
|
|
376
|
+
name: 'removeFiles',
|
|
377
|
+
message: '是否同时删除工作区目录和 worktrees?',
|
|
378
|
+
default: false
|
|
379
|
+
}
|
|
380
|
+
]);
|
|
381
|
+
|
|
382
|
+
const { confirm } = await inquirer.prompt([
|
|
383
|
+
{
|
|
384
|
+
type: 'confirm',
|
|
385
|
+
name: 'confirm',
|
|
386
|
+
message: chalk.yellow(`确认删除工作区 "${workspace.name}"?`),
|
|
387
|
+
default: false
|
|
388
|
+
}
|
|
389
|
+
]);
|
|
390
|
+
|
|
391
|
+
if (!confirm) {
|
|
392
|
+
console.log(chalk.yellow('\n取消删除\n'));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
workspaceService.deleteWorkspace(workspace.id, removeFiles);
|
|
397
|
+
console.log(chalk.green('\n✅ 工作区删除成功\n'));
|
|
398
|
+
|
|
399
|
+
} catch (error) {
|
|
400
|
+
console.error(chalk.red(`\n❌ ${error.message}\n`));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* 工作区管理主菜单
|
|
406
|
+
*/
|
|
407
|
+
async function workspaceMenu() {
|
|
408
|
+
let continueMenu = true;
|
|
409
|
+
|
|
410
|
+
while (continueMenu) {
|
|
411
|
+
const { action } = await inquirer.prompt([
|
|
412
|
+
{
|
|
413
|
+
type: 'list',
|
|
414
|
+
name: 'action',
|
|
415
|
+
message: '工作区管理:',
|
|
416
|
+
pageSize: 10,
|
|
417
|
+
choices: [
|
|
418
|
+
{ name: chalk.cyan('创建工作区'), value: 'create' },
|
|
419
|
+
{ name: chalk.green('查看工作区'), value: 'view' },
|
|
420
|
+
{ name: chalk.blue('列出所有工作区'), value: 'list' },
|
|
421
|
+
{ name: chalk.red('删除工作区'), value: 'delete' },
|
|
422
|
+
new inquirer.Separator(chalk.gray('─'.repeat(14))),
|
|
423
|
+
{ name: chalk.gray('返回主菜单'), value: 'back' }
|
|
424
|
+
]
|
|
425
|
+
}
|
|
426
|
+
]);
|
|
427
|
+
|
|
428
|
+
switch (action) {
|
|
429
|
+
case 'create':
|
|
430
|
+
await createWorkspace();
|
|
431
|
+
break;
|
|
432
|
+
case 'view':
|
|
433
|
+
await viewWorkspace();
|
|
434
|
+
break;
|
|
435
|
+
case 'list':
|
|
436
|
+
await listWorkspaces();
|
|
437
|
+
break;
|
|
438
|
+
case 'delete':
|
|
439
|
+
await deleteWorkspace();
|
|
440
|
+
break;
|
|
441
|
+
case 'back':
|
|
442
|
+
continueMenu = false;
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
module.exports = {
|
|
449
|
+
workspaceMenu,
|
|
450
|
+
listWorkspaces,
|
|
451
|
+
createWorkspace,
|
|
452
|
+
viewWorkspace,
|
|
453
|
+
deleteWorkspace
|
|
454
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// 默认配置
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_CONFIG = {
|
|
6
|
+
projectsDir: path.join(os.homedir(), '.claude', 'ctx', 'projects'),
|
|
7
|
+
defaultProject: null,
|
|
8
|
+
maxDisplaySessions: 100,
|
|
9
|
+
pageSize: 15,
|
|
10
|
+
currentCliType: 'claude', // 当前CLI工具类型: claude, codex, gemini
|
|
11
|
+
ports: {
|
|
12
|
+
webUI: 19999, // Web UI 页面端口 (同时用于 WebSocket)
|
|
13
|
+
proxy: 20088, // Claude 代理服务端口
|
|
14
|
+
codexProxy: 20089, // Codex 代理服务端口
|
|
15
|
+
geminiProxy: 20090 // Gemini 代理服务端口
|
|
16
|
+
},
|
|
17
|
+
maxLogs: 100,
|
|
18
|
+
statsInterval: 30,
|
|
19
|
+
pricing: {
|
|
20
|
+
claude: {
|
|
21
|
+
mode: 'auto',
|
|
22
|
+
input: 3,
|
|
23
|
+
output: 15,
|
|
24
|
+
cacheCreation: 3.75,
|
|
25
|
+
cacheRead: 0.30
|
|
26
|
+
},
|
|
27
|
+
codex: {
|
|
28
|
+
mode: 'auto',
|
|
29
|
+
input: 2.5,
|
|
30
|
+
output: 10
|
|
31
|
+
},
|
|
32
|
+
gemini: {
|
|
33
|
+
mode: 'auto',
|
|
34
|
+
input: 1.25,
|
|
35
|
+
output: 5
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
module.exports = DEFAULT_CONFIG;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// 配置加载和保存
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const DEFAULT_CONFIG = require('./default');
|
|
6
|
+
|
|
7
|
+
const CONFIG_FILE = path.join(__dirname, '../../config.json');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 展开 ~ 为用户主目录
|
|
11
|
+
*/
|
|
12
|
+
function expandHome(filepath) {
|
|
13
|
+
if (filepath.startsWith('~')) {
|
|
14
|
+
return path.join(os.homedir(), filepath.slice(1));
|
|
15
|
+
}
|
|
16
|
+
return filepath;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function mergePricing(defaultPricing, overrides = {}) {
|
|
20
|
+
const merged = {};
|
|
21
|
+
Object.keys(defaultPricing).forEach((key) => {
|
|
22
|
+
merged[key] = {
|
|
23
|
+
...defaultPricing[key],
|
|
24
|
+
...(overrides && overrides[key] ? overrides[key] : {})
|
|
25
|
+
};
|
|
26
|
+
if (!merged[key].mode) {
|
|
27
|
+
merged[key].mode = 'auto';
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return merged;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 加载配置
|
|
35
|
+
*/
|
|
36
|
+
function loadConfig() {
|
|
37
|
+
try {
|
|
38
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
39
|
+
const userConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
40
|
+
const config = { ...DEFAULT_CONFIG, ...userConfig };
|
|
41
|
+
config.projectsDir = expandHome(config.projectsDir);
|
|
42
|
+
|
|
43
|
+
// 合并 ports 配置
|
|
44
|
+
config.ports = { ...DEFAULT_CONFIG.ports, ...userConfig.ports };
|
|
45
|
+
config.pricing = mergePricing(DEFAULT_CONFIG.pricing, userConfig.pricing);
|
|
46
|
+
|
|
47
|
+
// 确保有 currentProject,使用 defaultProject 作为 currentProject
|
|
48
|
+
if (!config.currentProject && config.defaultProject) {
|
|
49
|
+
config.currentProject = config.defaultProject;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return config;
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error('加载配置文件失败,使用默认配置');
|
|
56
|
+
}
|
|
57
|
+
return { ...DEFAULT_CONFIG, currentProject: DEFAULT_CONFIG.defaultProject };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 保存配置
|
|
62
|
+
*/
|
|
63
|
+
function saveConfig(config) {
|
|
64
|
+
try {
|
|
65
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error('保存配置失败:', error.message);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
loadConfig,
|
|
73
|
+
saveConfig,
|
|
74
|
+
expandHome,
|
|
75
|
+
};
|