Skip to content

Commit 2727340

Browse files
catlog22claude
andcommitted
feat: CCW Dashboard 增强 - 停止命令、浏览器修复和MCP多源配置
- 新增 ccw stop 命令支持优雅停止和强制终止 (--force) - 修复 ccw view 服务器检测时浏览器无法打开的问题 - MCP 配置现在从多个源读取: - ~/.claude.json (项目级) - ~/.claude/settings.json 和 settings.local.json (全局) - 各工作空间的 .claude/settings.json (工作空间级) - 新增全局 MCP 服务器显示区域 - 修复路径选择模态框样式问题 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent f429945 commit 2727340

File tree

8 files changed

+377
-21
lines changed

8 files changed

+377
-21
lines changed

ccw/src/cli.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Command } from 'commander';
22
import { viewCommand } from './commands/view.js';
33
import { serveCommand } from './commands/serve.js';
4+
import { stopCommand } from './commands/stop.js';
45
import { installCommand } from './commands/install.js';
56
import { uninstallCommand } from './commands/uninstall.js';
67
import { upgradeCommand } from './commands/upgrade.js';
@@ -68,6 +69,14 @@ export function run(argv) {
6869
.option('--no-browser', 'Start server without opening browser')
6970
.action(serveCommand);
7071

72+
// Stop command
73+
program
74+
.command('stop')
75+
.description('Stop the running CCW dashboard server')
76+
.option('--port <port>', 'Server port', '3456')
77+
.option('-f, --force', 'Force kill process on the port')
78+
.action(stopCommand);
79+
7180
// Install command
7281
program
7382
.command('install')

ccw/src/commands/stop.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import chalk from 'chalk';
2+
import { exec } from 'child_process';
3+
import { promisify } from 'util';
4+
5+
const execAsync = promisify(exec);
6+
7+
/**
8+
* Find process using a specific port (Windows)
9+
* @param {number} port - Port number
10+
* @returns {Promise<string|null>} PID or null
11+
*/
12+
async function findProcessOnPort(port) {
13+
try {
14+
const { stdout } = await execAsync(`netstat -ano | findstr :${port} | findstr LISTENING`);
15+
const lines = stdout.trim().split('\n');
16+
if (lines.length > 0) {
17+
const parts = lines[0].trim().split(/\s+/);
18+
return parts[parts.length - 1]; // PID is the last column
19+
}
20+
} catch {
21+
// No process found
22+
}
23+
return null;
24+
}
25+
26+
/**
27+
* Kill process by PID (Windows)
28+
* @param {string} pid - Process ID
29+
* @returns {Promise<boolean>} Success status
30+
*/
31+
async function killProcess(pid) {
32+
try {
33+
await execAsync(`taskkill /PID ${pid} /F`);
34+
return true;
35+
} catch {
36+
return false;
37+
}
38+
}
39+
40+
/**
41+
* Stop command handler - stops the running CCW dashboard server
42+
* @param {Object} options - Command options
43+
*/
44+
export async function stopCommand(options) {
45+
const port = options.port || 3456;
46+
const force = options.force || false;
47+
48+
console.log(chalk.blue.bold('\n CCW Dashboard\n'));
49+
console.log(chalk.gray(` Checking server on port ${port}...`));
50+
51+
try {
52+
// Try graceful shutdown via API first
53+
const healthCheck = await fetch(`http://localhost:${port}/api/health`, {
54+
signal: AbortSignal.timeout(2000)
55+
}).catch(() => null);
56+
57+
if (healthCheck && healthCheck.ok) {
58+
// CCW server is running - send shutdown signal
59+
console.log(chalk.cyan(' CCW server found, sending shutdown signal...'));
60+
61+
await fetch(`http://localhost:${port}/api/shutdown`, {
62+
method: 'POST',
63+
signal: AbortSignal.timeout(5000)
64+
}).catch(() => null);
65+
66+
// Wait a moment for shutdown
67+
await new Promise(resolve => setTimeout(resolve, 500));
68+
69+
console.log(chalk.green.bold('\n Server stopped successfully!\n'));
70+
return;
71+
}
72+
73+
// No CCW server responding, check if port is in use
74+
const pid = await findProcessOnPort(port);
75+
76+
if (!pid) {
77+
console.log(chalk.yellow(` No server running on port ${port}\n`));
78+
return;
79+
}
80+
81+
// Port is in use by another process
82+
console.log(chalk.yellow(` Port ${port} is in use by process PID: ${pid}`));
83+
84+
if (force) {
85+
console.log(chalk.cyan(' Force killing process...'));
86+
const killed = await killProcess(pid);
87+
88+
if (killed) {
89+
console.log(chalk.green.bold('\n Process killed successfully!\n'));
90+
} else {
91+
console.log(chalk.red('\n Failed to kill process. Try running as administrator.\n'));
92+
}
93+
} else {
94+
console.log(chalk.gray(`\n This is not a CCW server. Use --force to kill it:`));
95+
console.log(chalk.white(` ccw stop --force\n`));
96+
}
97+
98+
} catch (err) {
99+
console.error(chalk.red(`\n Error: ${err.message}\n`));
100+
}
101+
}

ccw/src/core/server.js

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ import { scanSessions } from './session-scanner.js';
88
import { aggregateData } from './data-aggregator.js';
99
import { resolvePath, getRecentPaths, trackRecentPath, removeRecentPath, normalizePathForDisplay, getWorkflowDir } from '../utils/path-resolver.js';
1010

11-
// Claude config file path
11+
// Claude config file paths
1212
const CLAUDE_CONFIG_PATH = join(homedir(), '.claude.json');
13+
const CLAUDE_SETTINGS_DIR = join(homedir(), '.claude');
14+
const CLAUDE_GLOBAL_SETTINGS = join(CLAUDE_SETTINGS_DIR, 'settings.json');
15+
const CLAUDE_GLOBAL_SETTINGS_LOCAL = join(CLAUDE_SETTINGS_DIR, 'settings.local.json');
1316

1417
// WebSocket clients for real-time notifications
1518
const wsClients = new Set();
@@ -160,6 +163,24 @@ export async function startServer(options = {}) {
160163
return;
161164
}
162165

166+
// API: Shutdown server (for ccw stop command)
167+
if (pathname === '/api/shutdown' && req.method === 'POST') {
168+
res.writeHead(200, { 'Content-Type': 'application/json' });
169+
res.end(JSON.stringify({ status: 'shutting_down' }));
170+
171+
// Graceful shutdown
172+
console.log('\n Received shutdown signal...');
173+
setTimeout(() => {
174+
server.close(() => {
175+
console.log(' Server stopped.\n');
176+
process.exit(0);
177+
});
178+
// Force exit after 3 seconds if graceful shutdown fails
179+
setTimeout(() => process.exit(0), 3000);
180+
}, 100);
181+
return;
182+
}
183+
163184
// API: Remove a recent path
164185
if (pathname === '/api/remove-recent-path' && req.method === 'POST') {
165186
handlePostRequest(req, res, async (body) => {
@@ -1006,22 +1027,82 @@ async function loadRecentPaths() {
10061027
// ========================================
10071028

10081029
/**
1009-
* Get MCP configuration from .claude.json
1030+
* Safely read and parse JSON file
1031+
* @param {string} filePath
1032+
* @returns {Object|null}
1033+
*/
1034+
function safeReadJson(filePath) {
1035+
try {
1036+
if (!existsSync(filePath)) return null;
1037+
const content = readFileSync(filePath, 'utf8');
1038+
return JSON.parse(content);
1039+
} catch {
1040+
return null;
1041+
}
1042+
}
1043+
1044+
/**
1045+
* Get MCP servers from a settings file
1046+
* @param {string} filePath
1047+
* @returns {Object} mcpServers object or empty object
1048+
*/
1049+
function getMcpServersFromSettings(filePath) {
1050+
const config = safeReadJson(filePath);
1051+
if (!config) return {};
1052+
return config.mcpServers || {};
1053+
}
1054+
1055+
/**
1056+
* Get MCP configuration from multiple sources:
1057+
* 1. ~/.claude.json (project-level MCP servers)
1058+
* 2. ~/.claude/settings.json and settings.local.json (global MCP servers)
1059+
* 3. Each workspace's .claude/settings.json and settings.local.json
10101060
* @returns {Object}
10111061
*/
10121062
function getMcpConfig() {
10131063
try {
1014-
if (!existsSync(CLAUDE_CONFIG_PATH)) {
1015-
return { projects: {} };
1064+
const result = { projects: {}, globalServers: {} };
1065+
1066+
// 1. Read from ~/.claude.json (primary source for project MCP)
1067+
if (existsSync(CLAUDE_CONFIG_PATH)) {
1068+
const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8');
1069+
const config = JSON.parse(content);
1070+
result.projects = config.projects || {};
10161071
}
1017-
const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8');
1018-
const config = JSON.parse(content);
1019-
return {
1020-
projects: config.projects || {}
1021-
};
1072+
1073+
// 2. Read global MCP servers from ~/.claude/settings.json and settings.local.json
1074+
const globalSettings = getMcpServersFromSettings(CLAUDE_GLOBAL_SETTINGS);
1075+
const globalSettingsLocal = getMcpServersFromSettings(CLAUDE_GLOBAL_SETTINGS_LOCAL);
1076+
result.globalServers = { ...globalSettings, ...globalSettingsLocal };
1077+
1078+
// 3. For each project, also check .claude/settings.json and settings.local.json
1079+
for (const projectPath of Object.keys(result.projects)) {
1080+
const projectClaudeDir = join(projectPath, '.claude');
1081+
const projectSettings = join(projectClaudeDir, 'settings.json');
1082+
const projectSettingsLocal = join(projectClaudeDir, 'settings.local.json');
1083+
1084+
// Merge MCP servers from workspace settings into project config
1085+
const workspaceServers = {
1086+
...getMcpServersFromSettings(projectSettings),
1087+
...getMcpServersFromSettings(projectSettingsLocal)
1088+
};
1089+
1090+
if (Object.keys(workspaceServers).length > 0) {
1091+
// Merge workspace servers with existing project servers (workspace takes precedence)
1092+
result.projects[projectPath] = {
1093+
...result.projects[projectPath],
1094+
mcpServers: {
1095+
...(result.projects[projectPath]?.mcpServers || {}),
1096+
...workspaceServers
1097+
}
1098+
};
1099+
}
1100+
}
1101+
1102+
return result;
10221103
} catch (error) {
10231104
console.error('Error reading MCP config:', error);
1024-
return { projects: {}, error: error.message };
1105+
return { projects: {}, globalServers: {}, error: error.message };
10251106
}
10261107
}
10271108

ccw/src/templates/dashboard-js/components/mcp-manager.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// ========== MCP State ==========
55
let mcpConfig = null;
66
let mcpAllProjects = {};
7+
let mcpGlobalServers = {};
78
let mcpCurrentProjectServers = {};
89
let mcpCreateMode = 'form'; // 'form' or 'json'
910

@@ -31,6 +32,7 @@ async function loadMcpConfig() {
3132
const data = await response.json();
3233
mcpConfig = data;
3334
mcpAllProjects = data.projects || {};
35+
mcpGlobalServers = data.globalServers || {};
3436

3537
// Get current project servers
3638
const currentPath = projectPath.replace(/\//g, '\\');
@@ -150,14 +152,24 @@ function updateMcpBadge() {
150152
function getAllAvailableMcpServers() {
151153
const allServers = {};
152154

155+
// Collect global servers first
156+
for (const [name, serverConfig] of Object.entries(mcpGlobalServers)) {
157+
allServers[name] = {
158+
config: serverConfig,
159+
usedIn: [],
160+
isGlobal: true
161+
};
162+
}
163+
153164
// Collect servers from all projects
154165
for (const [path, config] of Object.entries(mcpAllProjects)) {
155166
const servers = config.mcpServers || {};
156167
for (const [name, serverConfig] of Object.entries(servers)) {
157168
if (!allServers[name]) {
158169
allServers[name] = {
159170
config: serverConfig,
160-
usedIn: []
171+
usedIn: [],
172+
isGlobal: false
161173
};
162174
}
163175
allServers[name].usedIn.push(path);

0 commit comments

Comments
 (0)