Files
Claude-Code-Workflow/ccw/src/commands/stop.ts
catlog22 c3fd0624de Add benchmark results and tests for StandaloneLspManager path normalization
- Created a new JSON file with benchmark results from the run on 2026-02-09.
- Added tests for the StandaloneLspManager to verify path normalization on Windows, including handling of percent-encoded URIs and ensuring plain Windows paths remain unchanged.
2026-02-09 13:02:37 +08:00

199 lines
6.7 KiB
TypeScript

import chalk from 'chalk';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
interface StopOptions {
port?: number;
force?: boolean;
}
/**
* Find process using a specific port (Windows)
* @param {number} port - Port number
* @returns {Promise<string|null>} PID or null
*/
async function findProcessOnPort(port: number): Promise<string | null> {
try {
const { stdout } = await execAsync(`netstat -ano | findstr :${port} | findstr LISTENING`);
const lines = stdout.trim().split('\n');
if (lines.length > 0) {
const parts = lines[0].trim().split(/\s+/);
return parts[parts.length - 1]; // PID is the last column
}
} catch {
// No process found
}
return null;
}
/**
* Kill process by PID (Windows)
* @param {string} pid - Process ID
* @returns {Promise<boolean>} Success status
*/
async function killProcess(pid: string): Promise<boolean> {
try {
// Use PowerShell to avoid Git Bash path expansion issues with /PID
await execAsync(`powershell -Command "Stop-Process -Id ${pid} -Force -ErrorAction Stop"`);
return true;
} catch {
// Fallback to taskkill via cmd
try {
await execAsync(`cmd /c "taskkill /PID ${pid} /F"`);
return true;
} catch {
return false;
}
}
}
/**
* Clean up React frontend process on the given port
*/
async function cleanupReactFrontend(reactPort: number): Promise<void> {
const reactPid = await findProcessOnPort(reactPort);
if (reactPid) {
console.log(chalk.gray(` Cleaning up React frontend on port ${reactPort}...`));
const killed = await killProcess(reactPid);
if (killed) {
console.log(chalk.green(' React frontend stopped!'));
}
}
}
export async function stopCommand(options: StopOptions): Promise<void> {
const port = Number(options.port) || 3456;
const reactPort = port + 1; // React frontend runs on port + 1
const force = options.force || false;
console.log(chalk.blue.bold('\n CCW Dashboard\n'));
console.log(chalk.gray(` Checking server on port ${port} and ${reactPort}...`));
try {
// Try graceful shutdown via API first
const healthCheck = await fetch(`http://localhost:${port}/api/health`, {
signal: AbortSignal.timeout(2000)
}).catch(() => null);
if (healthCheck) {
// CCW server is running (may require authentication) - send shutdown signal
console.log(chalk.cyan(' CCW server found, sending shutdown signal...'));
let token: string | undefined;
try {
const tokenResponse = await fetch(`http://localhost:${port}/api/auth/token`, {
signal: AbortSignal.timeout(2000)
});
const tokenData = await tokenResponse.json() as { token?: string };
token = tokenData.token;
} catch {
// Ignore token acquisition errors; shutdown request will fail with 401.
}
const shutdownResponse = await fetch(`http://localhost:${port}/api/shutdown`, {
method: 'POST',
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
signal: AbortSignal.timeout(5000)
}).catch(() => null);
// Wait a moment for shutdown
await new Promise(resolve => setTimeout(resolve, 500));
if (shutdownResponse && 'ok' in shutdownResponse && shutdownResponse.ok) {
await cleanupReactFrontend(reactPort);
console.log(chalk.green.bold('\n Server stopped successfully!\n'));
process.exit(0);
}
// Best-effort verify shutdown (may still succeed even if shutdown endpoint didn't return ok)
const postCheck = await fetch(`http://localhost:${port}/api/health`, {
signal: AbortSignal.timeout(2000)
}).catch(() => null);
if (!postCheck) {
await cleanupReactFrontend(reactPort);
console.log(chalk.green.bold('\n Server stopped successfully!\n'));
process.exit(0);
}
const statusHint = shutdownResponse ? `HTTP ${shutdownResponse.status}` : 'no response';
console.log(chalk.yellow(` Shutdown request did not stop server (${statusHint}).`));
}
// No CCW server responding, check if port is in use
const pid = await findProcessOnPort(port);
if (!pid) {
console.log(chalk.yellow(` No server running on port ${port}\n`));
// Also check and clean up React frontend if it's still running
const reactPid = await findProcessOnPort(reactPort);
if (reactPid) {
console.log(chalk.yellow(` React frontend still running on port ${reactPort} (PID: ${reactPid})`));
if (force) {
console.log(chalk.cyan(' Cleaning up React frontend...'));
const killed = await killProcess(reactPid);
if (killed) {
console.log(chalk.green(' React frontend stopped!\n'));
} else {
console.log(chalk.red(' Failed to stop React frontend.\n'));
}
} else {
console.log(chalk.gray(`\n Use --force to clean it up:\n ccw stop --force\n`));
}
}
process.exit(0);
}
// Port is in use by another process
console.log(chalk.yellow(` Port ${port} is in use by process PID: ${pid}`));
if (force) {
console.log(chalk.cyan(' Force killing process...'));
const killed = await killProcess(pid);
if (killed) {
console.log(chalk.green(' Main server killed successfully!'));
// Also try to kill React frontend
console.log(chalk.gray(` Checking React frontend on port ${reactPort}...`));
const reactPid = await findProcessOnPort(reactPort);
if (reactPid) {
console.log(chalk.cyan(` Cleaning up React frontend (PID: ${reactPid})...`));
const reactKilled = await killProcess(reactPid);
if (reactKilled) {
console.log(chalk.green(' React frontend stopped!'));
} else {
console.log(chalk.yellow(' Failed to stop React frontend'));
}
} else {
console.log(chalk.gray(' No React frontend running'));
}
console.log(chalk.green.bold('\n All processes stopped successfully!\n'));
process.exit(0);
} else {
console.log(chalk.red('\n Failed to kill process. Try running as administrator.\n'));
process.exit(1);
}
} else {
// Also check React frontend port
const reactPid = await findProcessOnPort(reactPort);
if (reactPid) {
console.log(chalk.yellow(` React frontend running on port ${reactPort} (PID: ${reactPid})`));
}
console.log(chalk.gray(`\n This is not a CCW server. Use --force to kill it:`));
console.log(chalk.white(` ccw stop --force\n`));
process.exit(0);
}
} catch (err) {
const error = err as Error;
console.error(chalk.red(`\n Error: ${error.message}\n`));
process.exit(1);
}
}