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 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.1",
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"
@@ -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 { findProcessByPort, getPortToolIssue, formatPortToolIssue } = require('../utils/port-helper');
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([{
@@ -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
- syncCodexUserEnvironment({}, {
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
- return { success: true };
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 envResult = syncCodexUserEnvironment({
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
- return {
288
+ const result = {
257
289
  success: true,
258
290
  port: proxyPort,
259
- envInjected: true,
260
- isFirstTime: envResult.isFirstTime,
261
- shellConfigPath: envResult.shellConfigPath,
262
- sourceCommand: envResult.sourceCommand,
263
- reloadRequired: envResult.reloadRequired
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
  };