Skip to content

Commit a809535

Browse files
catlog22claude
andcommitted
feat(ccw): 智能识别运行中的服务器,支持工作空间切换
- ccw view 自动检测服务器是否已运行 - 已运行时切换工作空间并打开浏览器,无需重启服务器 - 新增 /api/switch-path 和 /api/health 端点 - Dashboard 支持 URL path 参数加载指定工作空间 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 0f469e2 commit a809535

File tree

4 files changed

+144
-9
lines changed

4 files changed

+144
-9
lines changed

ccw/src/commands/view.js

Lines changed: 98 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,105 @@
11
import { serveCommand } from './serve.js';
2+
import { launchBrowser } from '../utils/browser-launcher.js';
3+
import { validatePath } from '../utils/path-resolver.js';
4+
import chalk from 'chalk';
25

36
/**
4-
* View command handler - starts dashboard server (unified with serve mode)
7+
* Check if server is already running on the specified port
8+
* @param {number} port - Port to check
9+
* @returns {Promise<boolean>} True if server is running
10+
*/
11+
async function isServerRunning(port) {
12+
try {
13+
const controller = new AbortController();
14+
const timeoutId = setTimeout(() => controller.abort(), 1000);
15+
16+
const response = await fetch(`http://localhost:${port}/api/health`, {
17+
signal: controller.signal
18+
});
19+
clearTimeout(timeoutId);
20+
21+
return response.ok;
22+
} catch {
23+
return false;
24+
}
25+
}
26+
27+
/**
28+
* Switch workspace on running server
29+
* @param {number} port - Server port
30+
* @param {string} path - New workspace path
31+
* @returns {Promise<Object>} Result with success status
32+
*/
33+
async function switchWorkspace(port, path) {
34+
try {
35+
const response = await fetch(
36+
`http://localhost:${port}/api/switch-path?path=${encodeURIComponent(path)}`
37+
);
38+
return await response.json();
39+
} catch (err) {
40+
return { success: false, error: err.message };
41+
}
42+
}
43+
44+
/**
45+
* View command handler - opens dashboard for current workspace
46+
* If server is already running, switches workspace and opens browser
47+
* If not running, starts a new server
548
* @param {Object} options - Command options
649
*/
750
export async function viewCommand(options) {
8-
// Forward to serve command with same options
9-
await serveCommand({
10-
path: options.path,
11-
port: options.port || 3456,
12-
browser: options.browser
13-
});
51+
const port = options.port || 3456;
52+
53+
// Resolve workspace path
54+
let workspacePath = process.cwd();
55+
if (options.path) {
56+
const pathValidation = validatePath(options.path, { mustExist: true });
57+
if (!pathValidation.valid) {
58+
console.error(chalk.red(`\n Error: ${pathValidation.error}\n`));
59+
process.exit(1);
60+
}
61+
workspacePath = pathValidation.path;
62+
}
63+
64+
// Check if server is already running
65+
const serverRunning = await isServerRunning(port);
66+
67+
if (serverRunning) {
68+
// Server is running - switch workspace and open browser
69+
console.log(chalk.blue.bold('\n CCW Dashboard\n'));
70+
console.log(chalk.gray(` Server already running on port ${port}`));
71+
console.log(chalk.cyan(` Switching workspace to: ${workspacePath}`));
72+
73+
const result = await switchWorkspace(port, workspacePath);
74+
75+
if (result.success) {
76+
console.log(chalk.green(` Workspace switched successfully`));
77+
78+
// Open browser with the new path
79+
const url = `http://localhost:${port}/?path=${encodeURIComponent(result.path)}`;
80+
81+
if (options.browser !== false) {
82+
console.log(chalk.cyan(' Opening in browser...'));
83+
try {
84+
await launchBrowser(url);
85+
console.log(chalk.green.bold('\n Dashboard opened!\n'));
86+
} catch (err) {
87+
console.log(chalk.yellow(`\n Could not open browser: ${err.message}`));
88+
console.log(chalk.gray(` Open manually: ${url}\n`));
89+
}
90+
} else {
91+
console.log(chalk.gray(`\n URL: ${url}\n`));
92+
}
93+
} else {
94+
console.error(chalk.red(`\n Failed to switch workspace: ${result.error}\n`));
95+
process.exit(1);
96+
}
97+
} else {
98+
// Server not running - start new server
99+
await serveCommand({
100+
path: workspacePath,
101+
port: port,
102+
browser: options.browser
103+
});
104+
}
14105
}

ccw/src/core/server.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,40 @@ export async function startServer(options = {}) {
126126
return;
127127
}
128128

129+
// API: Switch workspace path (for ccw view command)
130+
if (pathname === '/api/switch-path') {
131+
const newPath = url.searchParams.get('path');
132+
if (!newPath) {
133+
res.writeHead(400, { 'Content-Type': 'application/json' });
134+
res.end(JSON.stringify({ error: 'Path is required' }));
135+
return;
136+
}
137+
138+
const resolved = resolvePath(newPath);
139+
if (!existsSync(resolved)) {
140+
res.writeHead(404, { 'Content-Type': 'application/json' });
141+
res.end(JSON.stringify({ error: 'Path does not exist' }));
142+
return;
143+
}
144+
145+
// Track the path and return success
146+
trackRecentPath(resolved);
147+
res.writeHead(200, { 'Content-Type': 'application/json' });
148+
res.end(JSON.stringify({
149+
success: true,
150+
path: resolved,
151+
recentPaths: getRecentPaths()
152+
}));
153+
return;
154+
}
155+
156+
// API: Health check (for ccw view to detect running server)
157+
if (pathname === '/api/health') {
158+
res.writeHead(200, { 'Content-Type': 'application/json' });
159+
res.end(JSON.stringify({ status: 'ok', timestamp: Date.now() }));
160+
return;
161+
}
162+
129163
// API: Remove a recent path
130164
if (pathname === '/api/remove-recent-path' && req.method === 'POST') {
131165
handlePostRequest(req, res, async (body) => {

ccw/src/templates/dashboard-js/main.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,17 @@ document.addEventListener('DOMContentLoaded', async () => {
2020
// Server mode: load data from API
2121
try {
2222
if (window.SERVER_MODE) {
23-
await switchToPath(window.INITIAL_PATH || projectPath);
23+
// Check URL for path parameter (from ccw view command)
24+
const urlParams = new URLSearchParams(window.location.search);
25+
const urlPath = urlParams.get('path');
26+
const initialPath = urlPath || window.INITIAL_PATH || projectPath;
27+
28+
await switchToPath(initialPath);
29+
30+
// Clean up URL after loading (remove query param)
31+
if (urlPath && window.history.replaceState) {
32+
window.history.replaceState({}, '', window.location.pathname);
33+
}
2434
} else {
2535
renderDashboard();
2636
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "claude-code-workflow",
3-
"version": "6.0.2",
3+
"version": "6.0.3",
44
"description": "JSON-driven multi-agent development framework with intelligent CLI orchestration (Gemini/Qwen/Codex), context-first architecture, and automated workflow execution",
55
"type": "module",
66
"main": "ccw/src/index.js",

0 commit comments

Comments
 (0)