Files
Claude-Code-Workflow/ccw/src/commands/serve.ts

150 lines
5.4 KiB
TypeScript

import { startServer } from '../core/server.js';
import { launchBrowser } from '../utils/browser-launcher.js';
import { resolvePath, validatePath } from '../utils/path-resolver.js';
import { startReactFrontend, stopReactFrontend } from '../utils/react-frontend.js';
import { startDocsSite, stopDocsSite } from '../utils/docs-frontend.js';
import chalk from 'chalk';
import type { Server } from 'http';
interface ServeOptions {
port?: number;
path?: string;
host?: string;
browser?: boolean;
frontend?: 'js' | 'react' | 'both';
new?: boolean;
}
/**
* Serve command handler - starts dashboard server with live path switching
* @param {Object} options - Command options
*/
export async function serveCommand(options: ServeOptions): Promise<void> {
const port = Number(options.port) || 3456;
const host = options.host || '127.0.0.1';
// --new flag is shorthand for --frontend react
const frontend = options.new ? 'react' : (options.frontend || 'js');
// Validate project path
let initialPath = process.cwd();
if (options.path) {
const pathValidation = validatePath(options.path, { mustExist: true });
if (!pathValidation.valid || !pathValidation.path) {
console.error(chalk.red(`\n Error: ${pathValidation.error}\n`));
process.exit(1);
}
initialPath = pathValidation.path;
}
console.log(chalk.blue.bold('\n CCW Dashboard Server\n'));
console.log(chalk.gray(` Initial project: ${initialPath}`));
console.log(chalk.gray(` Host: ${host}`));
console.log(chalk.gray(` Port: ${port}`));
console.log(chalk.gray(` Frontend: ${frontend}\n`));
// Start React frontend if needed
let reactPort: number | undefined;
if (frontend === 'react' || frontend === 'both') {
reactPort = port + 1;
try {
await startReactFrontend(reactPort);
} catch (error) {
console.error(chalk.red(`\n Failed to start React frontend: ${error}\n`));
process.exit(1);
}
}
// Start Docusaurus docs site if React frontend is enabled
// The docs site is proxied through Vite at /docs endpoint
if (frontend === 'react' || frontend === 'both') {
try {
await startDocsSite(3001);
} catch (error) {
console.log(chalk.yellow(`\n Warning: Failed to start docs site: ${error}`));
console.log(chalk.gray(` The /docs endpoint will not be available.`));
console.log(chalk.gray(` You can start it manually: cd ccw/docs-site && npm run serve -- --build --port 3001 --no-open\n`));
}
}
try {
// Start server
console.log(chalk.cyan(' Starting server...'));
const server = await startServer({
port,
host,
initialPath,
frontend,
reactPort
});
const boundUrl = `http://${host}:${port}`;
const browserUrl = host === '0.0.0.0' || host === '::' ? `http://localhost:${port}` : boundUrl;
if (!['127.0.0.1', 'localhost', '::1'].includes(host)) {
console.log(chalk.yellow(`\n WARNING: Binding to ${host} exposes the server to network attacks.`));
console.log(chalk.yellow(' Ensure firewall is configured and never expose tokens publicly.\n'));
}
console.log(chalk.green(` Server running at ${boundUrl}`));
// Display frontend URLs
if (frontend === 'both') {
console.log(chalk.gray(` JS Frontend: ${boundUrl}`));
console.log(chalk.gray(` React Frontend: http://${host}:${reactPort}`));
console.log(chalk.gray(` Docs: http://${host}:${reactPort}/docs/`));
} else if (frontend === 'react') {
console.log(chalk.gray(` React Frontend: http://${host}:${reactPort}`));
console.log(chalk.gray(` Docs: http://${host}:${reactPort}/docs/`));
}
// Open browser
if (options.browser !== false) {
console.log(chalk.cyan(' Opening in browser...'));
try {
// Determine which URL to open based on frontend setting
let openUrl = browserUrl;
if (frontend === 'react' && reactPort) {
// React frontend: access via proxy path /react/
openUrl = `http://${host}:${port}/react/`;
} else if (frontend === 'both') {
// Both frontends: default to JS frontend at root
openUrl = browserUrl;
}
// Add path query parameter for workspace switching
const pathParam = initialPath ? `?path=${encodeURIComponent(initialPath)}` : '';
await launchBrowser(openUrl + pathParam);
console.log(chalk.green.bold('\n Dashboard opened in browser!'));
} catch (err) {
const error = err as Error;
console.log(chalk.yellow(`\n Could not open browser: ${error.message}`));
console.log(chalk.gray(` Open manually: ${browserUrl}`));
}
}
console.log(chalk.gray('\n Press Ctrl+C to stop the server\n'));
// Handle graceful shutdown
process.on('SIGINT', async () => {
console.log(chalk.yellow('\n Shutting down server...'));
await stopReactFrontend();
await stopDocsSite();
server.close(() => {
console.log(chalk.green(' Server stopped.\n'));
process.exit(0);
});
});
} catch (error) {
const err = error as Error & { code?: string };
console.error(chalk.red(`\n Error: ${err.message}\n`));
if (err.code === 'EADDRINUSE') {
console.error(chalk.yellow(` Port ${port} is already in use.`));
console.error(chalk.gray(` Try a different port: ccw serve --port ${port + 1}\n`));
}
await stopReactFrontend();
await stopDocsSite();
process.exit(1);
}
}