mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,105 @@
|
||||
import { serveCommand } from './serve.js';
|
||||
import { launchBrowser } from '../utils/browser-launcher.js';
|
||||
import { validatePath } from '../utils/path-resolver.js';
|
||||
import chalk from 'chalk';
|
||||
|
||||
/**
|
||||
* View command handler - starts dashboard server (unified with serve mode)
|
||||
* Check if server is already running on the specified port
|
||||
* @param {number} port - Port to check
|
||||
* @returns {Promise<boolean>} True if server is running
|
||||
*/
|
||||
async function isServerRunning(port) {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 1000);
|
||||
|
||||
const response = await fetch(`http://localhost:${port}/api/health`, {
|
||||
signal: controller.signal
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch workspace on running server
|
||||
* @param {number} port - Server port
|
||||
* @param {string} path - New workspace path
|
||||
* @returns {Promise<Object>} Result with success status
|
||||
*/
|
||||
async function switchWorkspace(port, path) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://localhost:${port}/api/switch-path?path=${encodeURIComponent(path)}`
|
||||
);
|
||||
return await response.json();
|
||||
} catch (err) {
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* View command handler - opens dashboard for current workspace
|
||||
* If server is already running, switches workspace and opens browser
|
||||
* If not running, starts a new server
|
||||
* @param {Object} options - Command options
|
||||
*/
|
||||
export async function viewCommand(options) {
|
||||
// Forward to serve command with same options
|
||||
await serveCommand({
|
||||
path: options.path,
|
||||
port: options.port || 3456,
|
||||
browser: options.browser
|
||||
});
|
||||
const port = options.port || 3456;
|
||||
|
||||
// Resolve workspace path
|
||||
let workspacePath = process.cwd();
|
||||
if (options.path) {
|
||||
const pathValidation = validatePath(options.path, { mustExist: true });
|
||||
if (!pathValidation.valid) {
|
||||
console.error(chalk.red(`\n Error: ${pathValidation.error}\n`));
|
||||
process.exit(1);
|
||||
}
|
||||
workspacePath = pathValidation.path;
|
||||
}
|
||||
|
||||
// Check if server is already running
|
||||
const serverRunning = await isServerRunning(port);
|
||||
|
||||
if (serverRunning) {
|
||||
// Server is running - switch workspace and open browser
|
||||
console.log(chalk.blue.bold('\n CCW Dashboard\n'));
|
||||
console.log(chalk.gray(` Server already running on port ${port}`));
|
||||
console.log(chalk.cyan(` Switching workspace to: ${workspacePath}`));
|
||||
|
||||
const result = await switchWorkspace(port, workspacePath);
|
||||
|
||||
if (result.success) {
|
||||
console.log(chalk.green(` Workspace switched successfully`));
|
||||
|
||||
// Open browser with the new path
|
||||
const url = `http://localhost:${port}/?path=${encodeURIComponent(result.path)}`;
|
||||
|
||||
if (options.browser !== false) {
|
||||
console.log(chalk.cyan(' Opening in browser...'));
|
||||
try {
|
||||
await launchBrowser(url);
|
||||
console.log(chalk.green.bold('\n Dashboard opened!\n'));
|
||||
} catch (err) {
|
||||
console.log(chalk.yellow(`\n Could not open browser: ${err.message}`));
|
||||
console.log(chalk.gray(` Open manually: ${url}\n`));
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.gray(`\n URL: ${url}\n`));
|
||||
}
|
||||
} else {
|
||||
console.error(chalk.red(`\n Failed to switch workspace: ${result.error}\n`));
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
// Server not running - start new server
|
||||
await serveCommand({
|
||||
path: workspacePath,
|
||||
port: port,
|
||||
browser: options.browser
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,40 @@ export async function startServer(options = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
// API: Switch workspace path (for ccw view command)
|
||||
if (pathname === '/api/switch-path') {
|
||||
const newPath = url.searchParams.get('path');
|
||||
if (!newPath) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Path is required' }));
|
||||
return;
|
||||
}
|
||||
|
||||
const resolved = resolvePath(newPath);
|
||||
if (!existsSync(resolved)) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Path does not exist' }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Track the path and return success
|
||||
trackRecentPath(resolved);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
path: resolved,
|
||||
recentPaths: getRecentPaths()
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// API: Health check (for ccw view to detect running server)
|
||||
if (pathname === '/api/health') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ status: 'ok', timestamp: Date.now() }));
|
||||
return;
|
||||
}
|
||||
|
||||
// API: Remove a recent path
|
||||
if (pathname === '/api/remove-recent-path' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
|
||||
@@ -20,7 +20,17 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Server mode: load data from API
|
||||
try {
|
||||
if (window.SERVER_MODE) {
|
||||
await switchToPath(window.INITIAL_PATH || projectPath);
|
||||
// Check URL for path parameter (from ccw view command)
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const urlPath = urlParams.get('path');
|
||||
const initialPath = urlPath || window.INITIAL_PATH || projectPath;
|
||||
|
||||
await switchToPath(initialPath);
|
||||
|
||||
// Clean up URL after loading (remove query param)
|
||||
if (urlPath && window.history.replaceState) {
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
}
|
||||
} else {
|
||||
renderDashboard();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-code-workflow",
|
||||
"version": "6.0.2",
|
||||
"version": "6.0.3",
|
||||
"description": "JSON-driven multi-agent development framework with intelligent CLI orchestration (Gemini/Qwen/Codex), context-first architecture, and automated workflow execution",
|
||||
"type": "module",
|
||||
"main": "ccw/src/index.js",
|
||||
|
||||
Reference in New Issue
Block a user