coding-tool-x 3.5.1 → 3.5.3
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 +8 -0
- package/package.json +2 -2
- package/src/commands/daemon.js +75 -3
- package/src/index.js +21 -10
- package/src/server/index.js +5 -1
- package/src/server/services/codex-settings-manager.js +48 -9
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
该项目遵循 [Semantic Versioning](https://semver.org/)。
|
|
6
6
|
|
|
7
|
+
## [3.5.3] - 2026-03-23
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **Windows 全局安装后 UI 启动失败** - 发布依赖中显式加入 `rxjs`,并移除无效的 `file:` 本地运行时依赖,修复 `ctx start` / `ctx ui --daemon` 在 Windows 上因 `inquirer` 缺少运行时依赖而启动失败的问题
|
|
11
|
+
- **非交互启动链路解耦交互依赖** - CLI 与服务入口改为按需加载 `inquirer`,即使交互菜单依赖异常,后台启动命令仍可进入服务启动流程
|
|
12
|
+
- **Codex PowerShell 环境同步降级处理** - Windows 上 `pwsh` 超时或缺失时,Codex 环境变量同步改为告警降级,不再直接中断代理恢复与服务启动
|
|
13
|
+
- **残留端口清理体验修复** - `ctx stop` 现在会额外清理受管端口上的残留进程,并给出更明确的端口占用提示,减少二次启动时的误报
|
|
14
|
+
|
|
7
15
|
## [3.3.8] - 2026-03-12
|
|
8
16
|
|
|
9
17
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coding-tool-x",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.3",
|
|
4
4
|
"description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -58,7 +58,6 @@
|
|
|
58
58
|
"@iarna/toml": "^2.2.5",
|
|
59
59
|
"adm-zip": "^0.5.16",
|
|
60
60
|
"ajv": "^8.17.1",
|
|
61
|
-
"cc-tool-web": "file:src/web",
|
|
62
61
|
"chalk": "^4.1.2",
|
|
63
62
|
"express": "^4.21.2",
|
|
64
63
|
"http-proxy": "^1.18.1",
|
|
@@ -67,6 +66,7 @@
|
|
|
67
66
|
"open": "^8.4.2",
|
|
68
67
|
"ora": "^5.4.1",
|
|
69
68
|
"pm2": "^6.0.14",
|
|
69
|
+
"rxjs": "^7.8.2",
|
|
70
70
|
"semver": "^7.6.0",
|
|
71
71
|
"toml": "^3.0.0",
|
|
72
72
|
"ws": "^8.18.3"
|
package/src/commands/daemon.js
CHANGED
|
@@ -3,7 +3,13 @@ const path = require('path');
|
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const { loadConfig } = require('../config/loader');
|
|
5
5
|
const { PATHS, ensureStorageDirMigrated } = require('../config/paths');
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
findProcessByPort,
|
|
8
|
+
killProcessByPort,
|
|
9
|
+
waitForPortRelease,
|
|
10
|
+
getPortToolIssue,
|
|
11
|
+
formatPortToolIssue
|
|
12
|
+
} = require('../utils/port-helper');
|
|
7
13
|
|
|
8
14
|
const PM2_APP_NAME = 'cc-tool';
|
|
9
15
|
|
|
@@ -83,6 +89,50 @@ function shouldTreatPortOwnershipAsReady(ownsPort) {
|
|
|
83
89
|
return ownsPort === true || ownsPort === null;
|
|
84
90
|
}
|
|
85
91
|
|
|
92
|
+
function getManagedPorts(config = loadConfig()) {
|
|
93
|
+
return [
|
|
94
|
+
config.ports?.webUI || 19999,
|
|
95
|
+
config.ports?.proxy || 20088,
|
|
96
|
+
config.ports?.codexProxy || 20089,
|
|
97
|
+
config.ports?.geminiProxy || 20090,
|
|
98
|
+
config.ports?.opencodeProxy || 20091
|
|
99
|
+
].filter((port, index, list) => Number.isInteger(port) && port > 0 && list.indexOf(port) === index);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function cleanupManagedPorts(config = loadConfig(), options = {}) {
|
|
103
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 3000;
|
|
104
|
+
const ports = getManagedPorts(config);
|
|
105
|
+
const released = [];
|
|
106
|
+
const forced = [];
|
|
107
|
+
const stillInUse = [];
|
|
108
|
+
|
|
109
|
+
for (const port of ports) {
|
|
110
|
+
if (await waitForPortRelease(port, timeoutMs)) {
|
|
111
|
+
released.push(port);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const killed = killProcessByPort(port);
|
|
116
|
+
if (killed) {
|
|
117
|
+
forced.push(port);
|
|
118
|
+
if (await waitForPortRelease(port, timeoutMs)) {
|
|
119
|
+
released.push(port);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
stillInUse.push(port);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
ports,
|
|
129
|
+
released,
|
|
130
|
+
forced,
|
|
131
|
+
stillInUse,
|
|
132
|
+
toolIssue: getPortToolIssue()
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
86
136
|
async function waitForServiceReady(port, timeoutMs = 15000, intervalMs = 500) {
|
|
87
137
|
const startAt = Date.now();
|
|
88
138
|
let lastProcess = null;
|
|
@@ -232,10 +282,21 @@ async function handleStart() {
|
|
|
232
282
|
async function handleStop() {
|
|
233
283
|
try {
|
|
234
284
|
await connectPM2();
|
|
285
|
+
const config = loadConfig();
|
|
235
286
|
|
|
236
287
|
const existing = await getCCToolProcess();
|
|
237
288
|
if (!existing) {
|
|
289
|
+
const cleanup = await cleanupManagedPorts(config, { timeoutMs: 3000 });
|
|
238
290
|
console.log(chalk.yellow('\n[WARN] 服务未在运行\n'));
|
|
291
|
+
|
|
292
|
+
if (cleanup.forced.length > 0) {
|
|
293
|
+
console.log(chalk.yellow(`[WARN] 已额外清理残留端口: ${cleanup.forced.join(', ')}`));
|
|
294
|
+
}
|
|
295
|
+
if (cleanup.stillInUse.length > 0) {
|
|
296
|
+
console.log(chalk.red(`[ERROR] 以下端口仍被占用: ${cleanup.stillInUse.join(', ')}`));
|
|
297
|
+
printPortToolIssue(cleanup.toolIssue);
|
|
298
|
+
console.log(chalk.yellow('[TIP] 请检查是否有外部进程仍占用这些端口\n'));
|
|
299
|
+
}
|
|
239
300
|
disconnectPM2();
|
|
240
301
|
return;
|
|
241
302
|
}
|
|
@@ -248,11 +309,21 @@ async function handleStop() {
|
|
|
248
309
|
}
|
|
249
310
|
|
|
250
311
|
// 删除进程
|
|
251
|
-
pm2.delete(PM2_APP_NAME, (err) => {
|
|
312
|
+
pm2.delete(PM2_APP_NAME, async (err) => {
|
|
252
313
|
if (err) {
|
|
253
314
|
console.error(chalk.red('删除进程失败:'), err.message);
|
|
254
315
|
} else {
|
|
316
|
+
const cleanup = await cleanupManagedPorts(config, { timeoutMs: 3000 });
|
|
255
317
|
console.log(chalk.green('\n[OK] Coding-Tool 服务已停止\n'));
|
|
318
|
+
|
|
319
|
+
if (cleanup.forced.length > 0) {
|
|
320
|
+
console.log(chalk.yellow(`[WARN] 已额外清理残留端口: ${cleanup.forced.join(', ')}`));
|
|
321
|
+
}
|
|
322
|
+
if (cleanup.stillInUse.length > 0) {
|
|
323
|
+
console.log(chalk.red(`[ERROR] 以下端口仍被占用: ${cleanup.stillInUse.join(', ')}`));
|
|
324
|
+
printPortToolIssue(cleanup.toolIssue);
|
|
325
|
+
console.log(chalk.yellow('[TIP] 请检查是否有外部进程仍占用这些端口\n'));
|
|
326
|
+
}
|
|
256
327
|
}
|
|
257
328
|
|
|
258
329
|
pm2.dump((err) => {
|
|
@@ -399,6 +470,7 @@ module.exports = {
|
|
|
399
470
|
handleRestart,
|
|
400
471
|
handleStatus,
|
|
401
472
|
_test: {
|
|
402
|
-
shouldTreatPortOwnershipAsReady
|
|
473
|
+
shouldTreatPortOwnershipAsReady,
|
|
474
|
+
getManagedPorts
|
|
403
475
|
}
|
|
404
476
|
};
|
package/src/index.js
CHANGED
|
@@ -6,30 +6,24 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const { loadConfig } = require('./config/loader');
|
|
9
|
-
const { showMainMenu } = require('./ui/menu');
|
|
10
|
-
const { handleList } = require('./commands/list');
|
|
11
|
-
const { handleSearch } = require('./commands/search');
|
|
12
|
-
const { switchProject } = require('./commands/switch');
|
|
13
9
|
const { resetConfig } = require('./reset-config');
|
|
14
|
-
const { handleChannelManagement, handleAddChannel, handleChannelStatus } = require('./commands/channels');
|
|
15
|
-
const { handleToggleProxy } = require('./commands/toggle-proxy');
|
|
16
|
-
const { handlePortConfig } = require('./commands/port-config');
|
|
17
|
-
const { handleSwitchCliType } = require('./commands/cli-type');
|
|
18
10
|
const { handleStart, handleStop, handleRestart, handleStatus } = require('./commands/daemon');
|
|
19
11
|
const { handleProxyStart: proxyStart, handleProxyStop: proxyStop, handleProxyRestart, handleProxyStatus: proxyStatus } = require('./commands/proxy-control');
|
|
20
12
|
const { handleLogs } = require('./commands/logs');
|
|
21
13
|
const { handleStats, handleStatsExport } = require('./commands/stats');
|
|
22
14
|
const { handleDoctor } = require('./commands/doctor');
|
|
23
15
|
const { handleUpdate } = require('./commands/update');
|
|
24
|
-
const { workspaceMenu } = require('./commands/workspace');
|
|
25
16
|
const { ensureStorageDirMigrated } = require('./config/paths');
|
|
26
17
|
const PluginManager = require('./plugins/plugin-manager');
|
|
27
18
|
const eventBus = require('./plugins/event-bus');
|
|
28
19
|
const chalk = require('chalk');
|
|
29
|
-
const inquirer = require('inquirer');
|
|
30
20
|
const path = require('path');
|
|
31
21
|
const fs = require('fs');
|
|
32
22
|
|
|
23
|
+
function getInquirer() {
|
|
24
|
+
return require('inquirer');
|
|
25
|
+
}
|
|
26
|
+
|
|
33
27
|
// 读取版本号
|
|
34
28
|
function getVersion() {
|
|
35
29
|
const packagePath = path.join(__dirname, '../package.json');
|
|
@@ -338,6 +332,7 @@ async function main() {
|
|
|
338
332
|
|
|
339
333
|
// port 命令 - 配置端口
|
|
340
334
|
if (args[0] === 'port') {
|
|
335
|
+
const { handlePortConfig } = require('./commands/port-config');
|
|
341
336
|
await handlePortConfig();
|
|
342
337
|
return;
|
|
343
338
|
}
|
|
@@ -400,6 +395,7 @@ async function main() {
|
|
|
400
395
|
|
|
401
396
|
while (true) {
|
|
402
397
|
// 显示主菜单
|
|
398
|
+
const { showMainMenu } = require('./ui/menu');
|
|
403
399
|
const action = await showMainMenu(config);
|
|
404
400
|
|
|
405
401
|
// 发送命令开始事件
|
|
@@ -409,7 +405,9 @@ async function main() {
|
|
|
409
405
|
|
|
410
406
|
switch (action) {
|
|
411
407
|
case 'list':
|
|
408
|
+
const { handleList } = require('./commands/list');
|
|
412
409
|
await handleList(config, async () => {
|
|
410
|
+
const { switchProject } = require('./commands/switch');
|
|
413
411
|
const switched = await switchProject(config);
|
|
414
412
|
if (switched) {
|
|
415
413
|
// 重新加载配置以获取最新的项目设置
|
|
@@ -420,7 +418,9 @@ async function main() {
|
|
|
420
418
|
break;
|
|
421
419
|
|
|
422
420
|
case 'search':
|
|
421
|
+
const { handleSearch } = require('./commands/search');
|
|
423
422
|
await handleSearch(config, async () => {
|
|
423
|
+
const { switchProject } = require('./commands/switch');
|
|
424
424
|
const switched = await switchProject(config);
|
|
425
425
|
if (switched) {
|
|
426
426
|
config = loadConfig();
|
|
@@ -430,11 +430,14 @@ async function main() {
|
|
|
430
430
|
break;
|
|
431
431
|
|
|
432
432
|
case 'switch':
|
|
433
|
+
const { switchProject } = require('./commands/switch');
|
|
433
434
|
const switched = await switchProject(config);
|
|
434
435
|
if (switched) {
|
|
435
436
|
config = loadConfig();
|
|
436
437
|
// 切换成功后自动进入会话列表
|
|
438
|
+
const { handleList } = require('./commands/list');
|
|
437
439
|
await handleList(config, async () => {
|
|
440
|
+
const { switchProject } = require('./commands/switch');
|
|
438
441
|
const switched = await switchProject(config);
|
|
439
442
|
if (switched) {
|
|
440
443
|
config = loadConfig();
|
|
@@ -445,26 +448,32 @@ async function main() {
|
|
|
445
448
|
break;
|
|
446
449
|
|
|
447
450
|
case 'workspace':
|
|
451
|
+
const { workspaceMenu } = require('./commands/workspace');
|
|
448
452
|
await workspaceMenu();
|
|
449
453
|
break;
|
|
450
454
|
|
|
451
455
|
case 'switch-cli-type':
|
|
456
|
+
const { handleSwitchCliType } = require('./commands/cli-type');
|
|
452
457
|
await handleSwitchCliType();
|
|
453
458
|
config = loadConfig(); // 重新加载配置以获取新的类型
|
|
454
459
|
break;
|
|
455
460
|
|
|
456
461
|
case 'switch-channel':
|
|
462
|
+
const { handleChannelManagement } = require('./commands/channels');
|
|
457
463
|
await handleChannelManagement();
|
|
458
464
|
break;
|
|
459
465
|
case 'channel-status':
|
|
466
|
+
const { handleChannelStatus } = require('./commands/channels');
|
|
460
467
|
await handleChannelStatus();
|
|
461
468
|
break;
|
|
462
469
|
|
|
463
470
|
case 'toggle-proxy':
|
|
471
|
+
const { handleToggleProxy } = require('./commands/toggle-proxy');
|
|
464
472
|
await handleToggleProxy();
|
|
465
473
|
break;
|
|
466
474
|
|
|
467
475
|
case 'add-channel':
|
|
476
|
+
const { handleAddChannel } = require('./commands/channels');
|
|
468
477
|
await handleAddChannel();
|
|
469
478
|
break;
|
|
470
479
|
|
|
@@ -475,6 +484,7 @@ async function main() {
|
|
|
475
484
|
}
|
|
476
485
|
|
|
477
486
|
case 'port-config':
|
|
487
|
+
const { handlePortConfig } = require('./commands/port-config');
|
|
478
488
|
await handlePortConfig();
|
|
479
489
|
break;
|
|
480
490
|
|
|
@@ -484,6 +494,7 @@ async function main() {
|
|
|
484
494
|
|
|
485
495
|
case 'plugin-menu': {
|
|
486
496
|
const { handlePluginCommand } = require('./commands/plugin');
|
|
497
|
+
const inquirer = getInquirer();
|
|
487
498
|
|
|
488
499
|
// Show plugin management submenu
|
|
489
500
|
const pluginAction = await inquirer.prompt([{
|
package/src/server/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const chalk = require('chalk');
|
|
4
|
-
const inquirer = require('inquirer');
|
|
5
4
|
const { loadConfig } = require('../config/loader');
|
|
6
5
|
const { PATHS, ensureStorageDirMigrated } = require('../config/paths');
|
|
7
6
|
const { startWebSocketServer: attachWebSocketServer } = require('./websocket-server');
|
|
@@ -25,6 +24,10 @@ const { startOpenCodeProxyServer, collectProxyModelList } = require('./opencode-
|
|
|
25
24
|
const { createRemoteMutationGuard } = require('./services/network-access');
|
|
26
25
|
const { createApiRequestLogger } = require('./services/request-logger');
|
|
27
26
|
|
|
27
|
+
function getInquirer() {
|
|
28
|
+
return require('inquirer');
|
|
29
|
+
}
|
|
30
|
+
|
|
28
31
|
function isInteractivePortConflictMode(options = {}) {
|
|
29
32
|
if (options.interactive === false) {
|
|
30
33
|
return false;
|
|
@@ -73,6 +76,7 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
73
76
|
shouldKill = true;
|
|
74
77
|
} else if (interactiveMode) {
|
|
75
78
|
// 询问用户是否关闭占用端口的进程
|
|
79
|
+
const inquirer = getInquirer();
|
|
76
80
|
const answer = await inquirer.prompt([
|
|
77
81
|
{
|
|
78
82
|
type: 'list',
|
|
@@ -37,6 +37,33 @@ function hasBackup() {
|
|
|
37
37
|
return fs.existsSync(getConfigBackupPath()) || fs.existsSync(getAuthBackupPath());
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function isRecoverableEnvSyncError(err) {
|
|
41
|
+
const message = String(err?.message || '');
|
|
42
|
+
return err?.code === 'ETIMEDOUT' ||
|
|
43
|
+
/timed out|spawnSync (?:powershell|pwsh)|No PowerShell executable available/i.test(message);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function trySyncCodexUserEnvironment(envMap, options) {
|
|
47
|
+
try {
|
|
48
|
+
return {
|
|
49
|
+
success: true,
|
|
50
|
+
result: syncCodexUserEnvironment(envMap, options),
|
|
51
|
+
warning: null
|
|
52
|
+
};
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (!isRecoverableEnvSyncError(err)) {
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.warn(`[Codex Settings] 跳过持久化环境变量同步: ${err.message}`);
|
|
59
|
+
return {
|
|
60
|
+
success: false,
|
|
61
|
+
result: null,
|
|
62
|
+
warning: err.message
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
40
67
|
|
|
41
68
|
// 读取 config.toml
|
|
42
69
|
function readConfig() {
|
|
@@ -191,7 +218,7 @@ function restoreSettings() {
|
|
|
191
218
|
fs.unlinkSync(getAuthBackupPath());
|
|
192
219
|
}
|
|
193
220
|
|
|
194
|
-
|
|
221
|
+
const envSync = trySyncCodexUserEnvironment({}, {
|
|
195
222
|
replace: false,
|
|
196
223
|
removeKeys: ['CC_PROXY_KEY']
|
|
197
224
|
});
|
|
@@ -200,7 +227,11 @@ function restoreSettings() {
|
|
|
200
227
|
delete process.env.CC_PROXY_KEY;
|
|
201
228
|
|
|
202
229
|
console.log('Codex settings restored from backup');
|
|
203
|
-
|
|
230
|
+
const result = { success: true };
|
|
231
|
+
if (envSync.warning) {
|
|
232
|
+
result.envSyncWarning = envSync.warning;
|
|
233
|
+
}
|
|
234
|
+
return result;
|
|
204
235
|
} catch (err) {
|
|
205
236
|
throw new Error('Failed to restore settings: ' + err.message);
|
|
206
237
|
}
|
|
@@ -246,22 +277,27 @@ function setProxyConfig(proxyPort) {
|
|
|
246
277
|
// 直接设置 process.env 确保从本进程派生的 Codex CLI 能读到 CC_PROXY_KEY
|
|
247
278
|
process.env.CC_PROXY_KEY = 'PROXY_KEY';
|
|
248
279
|
|
|
249
|
-
const
|
|
280
|
+
const envSync = trySyncCodexUserEnvironment({
|
|
250
281
|
CC_PROXY_KEY: 'PROXY_KEY'
|
|
251
282
|
}, {
|
|
252
283
|
replace: false
|
|
253
284
|
});
|
|
285
|
+
const envResult = envSync.result;
|
|
254
286
|
|
|
255
287
|
console.log(`Codex settings updated to use proxy on port ${proxyPort}`);
|
|
256
|
-
|
|
288
|
+
const result = {
|
|
257
289
|
success: true,
|
|
258
290
|
port: proxyPort,
|
|
259
|
-
envInjected:
|
|
260
|
-
isFirstTime: envResult
|
|
261
|
-
shellConfigPath: envResult
|
|
262
|
-
sourceCommand: envResult
|
|
263
|
-
reloadRequired: envResult
|
|
291
|
+
envInjected: envSync.success,
|
|
292
|
+
isFirstTime: envResult?.isFirstTime || false,
|
|
293
|
+
shellConfigPath: envResult?.shellConfigPath || null,
|
|
294
|
+
sourceCommand: envResult?.sourceCommand || null,
|
|
295
|
+
reloadRequired: envResult?.reloadRequired || false
|
|
264
296
|
};
|
|
297
|
+
if (envSync.warning) {
|
|
298
|
+
result.envSyncWarning = envSync.warning;
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
265
301
|
} catch (err) {
|
|
266
302
|
throw new Error('Failed to set proxy config: ' + err.message);
|
|
267
303
|
}
|
|
@@ -335,4 +371,7 @@ module.exports = {
|
|
|
335
371
|
setProxyConfig,
|
|
336
372
|
isProxyConfig,
|
|
337
373
|
getCurrentProxyPort,
|
|
374
|
+
_test: {
|
|
375
|
+
isRecoverableEnvSyncError
|
|
376
|
+
}
|
|
338
377
|
};
|