mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-15 02:42:45 +08:00
refactor: decouple docs-site from ccw monorepo
- Remove docs-frontend.ts utility and docs proxy from server.ts - Remove docs startup logic from serve.ts - Delete HelpPage.tsx and /help route from frontend - Remove help navigation entry from Sidebar.tsx - Clean /docs proxy from vite.config.ts - Remove docs-related scripts from package.json files - Delete help.spec.ts E2E tests - Remove docs-site directory (migrated to D:\ccw-doc) The docs-site has been moved to D:\ccw-doc as a standalone Docusaurus project with baseUrl='/' for static hosting deployment (GitHub Pages/Netlify).
This commit is contained in:
@@ -2,7 +2,6 @@ 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';
|
||||
|
||||
@@ -58,31 +57,15 @@ export async function serveCommand(options: ServeOptions): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// Start Docusaurus docs site if React frontend is enabled
|
||||
// The docs site is proxied at /docs (via the CCW dashboard server and also via Vite in dev)
|
||||
let docsPort: number | undefined;
|
||||
if (frontend === 'react' || frontend === 'both') {
|
||||
const preferredDocsPort = Number(process.env.CCW_DOCS_PORT) || 3001;
|
||||
try {
|
||||
docsPort = await startDocsSite(preferredDocsPort);
|
||||
} 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 ${preferredDocsPort} --no-open\n`));
|
||||
docsPort = preferredDocsPort;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Start server
|
||||
console.log(chalk.cyan(' Starting server...'));
|
||||
const server = await startServer({
|
||||
port,
|
||||
host,
|
||||
const server = await startServer({
|
||||
port,
|
||||
host,
|
||||
initialPath,
|
||||
frontend,
|
||||
reactPort,
|
||||
docsPort
|
||||
reactPort
|
||||
});
|
||||
|
||||
const boundUrl = `http://${host}:${port}`;
|
||||
@@ -99,18 +82,8 @@ export async function serveCommand(options: ServeOptions): Promise<void> {
|
||||
if (frontend === 'both') {
|
||||
console.log(chalk.gray(` JS Frontend: ${boundUrl}`));
|
||||
console.log(chalk.gray(` React Frontend: http://${host}:${reactPort}`));
|
||||
console.log(chalk.gray(` Docs: ${browserUrl}/docs/`));
|
||||
console.log(chalk.gray(` Docs (zh): ${browserUrl}/docs/zh/`));
|
||||
if (docsPort) {
|
||||
console.log(chalk.gray(` Docs server: http://localhost:${docsPort}/docs/`));
|
||||
}
|
||||
} else if (frontend === 'react') {
|
||||
console.log(chalk.gray(` React Frontend: http://${host}:${reactPort}`));
|
||||
console.log(chalk.gray(` Docs: ${browserUrl}/docs/`));
|
||||
console.log(chalk.gray(` Docs (zh): ${browserUrl}/docs/zh/`));
|
||||
if (docsPort) {
|
||||
console.log(chalk.gray(` Docs server: http://localhost:${docsPort}/docs/`));
|
||||
}
|
||||
}
|
||||
|
||||
// Open browser
|
||||
@@ -144,7 +117,6 @@ export async function serveCommand(options: ServeOptions): Promise<void> {
|
||||
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);
|
||||
@@ -159,7 +131,6 @@ export async function serveCommand(options: ServeOptions): Promise<void> {
|
||||
console.error(chalk.gray(` Try a different port: ccw serve --port ${port + 1}\n`));
|
||||
}
|
||||
await stopReactFrontend();
|
||||
await stopDocsSite();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,6 @@ interface ServerOptions {
|
||||
open?: boolean;
|
||||
frontend?: 'js' | 'react' | 'both';
|
||||
reactPort?: number;
|
||||
docsPort?: number;
|
||||
}
|
||||
|
||||
type PostHandler = PostRequestHandler;
|
||||
@@ -454,13 +453,11 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
||||
const host = options.host ?? '127.0.0.1';
|
||||
const frontend = options.frontend || 'js';
|
||||
const reactPort = options.reactPort || serverPort + 1;
|
||||
const docsPort = options.docsPort || 3001;
|
||||
|
||||
// Log frontend configuration
|
||||
console.log(`[Server] Frontend mode: ${frontend}`);
|
||||
if (frontend === 'react' || frontend === 'both') {
|
||||
console.log(`[Server] React proxy configured: /react/* -> http://localhost:${reactPort}`);
|
||||
console.log(`[Server] Docs proxy configured: /docs/* -> http://localhost:${docsPort}`);
|
||||
}
|
||||
|
||||
const tokenManager = getTokenManager();
|
||||
@@ -877,68 +874,6 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
||||
}
|
||||
}
|
||||
|
||||
// Docs site proxy - proxy requests to Docusaurus dev server (port 3001)
|
||||
// Redirect /docs to /docs/ to match Docusaurus baseUrl
|
||||
if (pathname === '/docs') {
|
||||
res.writeHead(302, { 'Location': `/docs/${url.search}` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Proxy /docs/* requests to Docusaurus
|
||||
if (pathname.startsWith('/docs/')) {
|
||||
// Preserve the /docs prefix when forwarding to Docusaurus
|
||||
const docsUrl = `http://localhost:${docsPort}${pathname}${url.search}`;
|
||||
|
||||
console.log(`[Docs Proxy] Proxying ${pathname} -> ${docsUrl}`);
|
||||
|
||||
try {
|
||||
// Convert headers to plain object for fetch
|
||||
const proxyHeaders: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(req.headers)) {
|
||||
if (typeof value === 'string') {
|
||||
proxyHeaders[key] = value;
|
||||
} else if (Array.isArray(value)) {
|
||||
proxyHeaders[key] = value.join(', ');
|
||||
}
|
||||
}
|
||||
proxyHeaders['host'] = `localhost:${docsPort}`;
|
||||
|
||||
const docsResponse = await fetch(docsUrl, {
|
||||
method: req.method,
|
||||
headers: proxyHeaders,
|
||||
body: req.method !== 'GET' && req.method !== 'HEAD' ? await readRequestBody(req) : undefined,
|
||||
});
|
||||
|
||||
const contentType = docsResponse.headers.get('content-type') || 'text/html';
|
||||
const body = await docsResponse.text();
|
||||
|
||||
// Forward response headers
|
||||
const responseHeaders: Record<string, string> = {
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': 'no-cache',
|
||||
};
|
||||
|
||||
// Forward Set-Cookie headers if present
|
||||
const setCookieHeaders = docsResponse.headers.get('set-cookie');
|
||||
if (setCookieHeaders) {
|
||||
responseHeaders['Set-Cookie'] = setCookieHeaders;
|
||||
}
|
||||
|
||||
console.log(`[Docs Proxy] Response ${docsResponse.status}: ${contentType}`);
|
||||
|
||||
res.writeHead(docsResponse.status, responseHeaders);
|
||||
res.end(body);
|
||||
return;
|
||||
} catch (err) {
|
||||
console.error(`[Docs Proxy] Failed to proxy to ${docsUrl}:`, err);
|
||||
console.error(`[Docs Proxy] Error details:`, (err as Error).message);
|
||||
res.writeHead(502, { 'Content-Type': 'text/plain' });
|
||||
res.end(`Bad Gateway: Docs site not available at ${docsUrl}\nMake sure the Docusaurus server is running on port ${docsPort}.\nError: ${(err as Error).message}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Root path - serve JS frontend HTML (default or both mode)
|
||||
if (pathname === '/' || pathname === '/index.html') {
|
||||
const html = generateServerDashboard(initialPath);
|
||||
|
||||
@@ -1,396 +0,0 @@
|
||||
import { spawn, type ChildProcess } from 'child_process';
|
||||
import { createServer } from 'net';
|
||||
import { join, resolve } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
import chalk from 'chalk';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
let docsProcess: ChildProcess | null = null;
|
||||
let docsPort: number | null = null;
|
||||
|
||||
// Default Docusaurus port
|
||||
const DEFAULT_DOCS_PORT = 3001;
|
||||
|
||||
type DocsStartMode = 'serve' | 'start';
|
||||
|
||||
function normalizeDocsStartMode(mode: string | undefined): DocsStartMode {
|
||||
const normalized = (mode ?? '').trim().toLowerCase();
|
||||
return normalized === 'start' ? 'start' : 'serve';
|
||||
}
|
||||
|
||||
async function fetchStatus(url: string, timeoutMs: number): Promise<number | null> {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||
const response = await fetch(url, { signal: controller.signal });
|
||||
clearTimeout(timeoutId);
|
||||
return response.status;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function isPortAvailable(port: number, host: string = '127.0.0.1'): Promise<boolean> {
|
||||
return await new Promise<boolean>((resolve) => {
|
||||
const server = createServer();
|
||||
server.unref();
|
||||
server.on('error', () => resolve(false));
|
||||
server.listen(port, host, () => {
|
||||
server.close(() => resolve(true));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function findAvailablePort({
|
||||
preferredPort,
|
||||
maxAttempts,
|
||||
}: {
|
||||
preferredPort: number;
|
||||
maxAttempts: number;
|
||||
}): Promise<number | null> {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
const port = preferredPort + i;
|
||||
if (await isPortAvailable(port)) return port;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start Docusaurus documentation server.
|
||||
*
|
||||
* Notes:
|
||||
* - Docusaurus `start` serves a single locale; `/docs/zh/*` will 404 unless started with `--locale zh`.
|
||||
* - Docusaurus `serve --build` serves all locales (multi-locale i18n).
|
||||
*
|
||||
* @param port - Preferred port to run Docusaurus server on (default: 3001)
|
||||
* @returns Promise that resolves with the actual port used
|
||||
*/
|
||||
export async function startDocsSite(port: number = DEFAULT_DOCS_PORT): Promise<number> {
|
||||
// Check if already running (CCW-managed)
|
||||
if (docsProcess && docsPort === port) {
|
||||
console.log(chalk.yellow(` Docs site already running on port ${port}`));
|
||||
return port;
|
||||
}
|
||||
|
||||
const requestedMode = normalizeDocsStartMode(process.env.CCW_DOCS_MODE);
|
||||
const requestedLocale = process.env.CCW_DOCS_LOCALE?.trim();
|
||||
|
||||
// If something else is already listening on this port, avoid spawning a second server.
|
||||
// If zh routes are missing (common with `docusaurus start` default-locale), start CCW docs on a fallback port.
|
||||
let effectivePort = port;
|
||||
const portAvailable = await isPortAvailable(port);
|
||||
if (!portAvailable) {
|
||||
const docsStatus = await fetchStatus(`http://localhost:${port}/docs/`, 800);
|
||||
const zhStatus = docsStatus !== null ? await fetchStatus(`http://localhost:${port}/docs/zh/`, 800) : null;
|
||||
|
||||
if (docsStatus !== null) {
|
||||
console.log(chalk.yellow(` Docs server already running on port ${port} (GET /docs/ -> ${docsStatus}).`));
|
||||
|
||||
if (zhStatus === 200) {
|
||||
return port;
|
||||
}
|
||||
|
||||
console.log(chalk.yellow(` Note: GET /docs/zh/ -> ${zhStatus ?? 'no response'}.`));
|
||||
console.log(chalk.gray(` Docusaurus dev server (start) serves a single locale; /docs/zh/* will 404 unless started with --locale zh.`));
|
||||
console.log(chalk.gray(` Fix options:`));
|
||||
console.log(chalk.gray(` - Stop the existing process on port ${port} and restart CCW`));
|
||||
console.log(chalk.gray(` - Or run: cd ccw/docs-site && npm run serve -- --build --port ${port} --no-open`));
|
||||
console.log(chalk.gray(` - Or run a single-locale zh dev server: cd ccw/docs-site && npm run start -- --locale zh --port ${port} --no-open`));
|
||||
} else {
|
||||
console.log(chalk.yellow(` Port ${port} is already in use.`));
|
||||
}
|
||||
|
||||
const fallbackPort = await findAvailablePort({ preferredPort: port + 1, maxAttempts: 20 });
|
||||
if (!fallbackPort) {
|
||||
console.log(chalk.yellow(` Could not find a free port near ${port}. Reusing ${port}.`));
|
||||
return port;
|
||||
}
|
||||
|
||||
effectivePort = fallbackPort;
|
||||
console.log(chalk.yellow(` Starting CCW-managed docs site on fallback port ${effectivePort}...`));
|
||||
}
|
||||
|
||||
// Try to find docs-site directory (relative to ccw package)
|
||||
const possiblePaths = [
|
||||
join(__dirname, '../../docs-site'), // From dist/utils
|
||||
join(__dirname, '../docs-site'), // From src/utils (dev)
|
||||
join(process.cwd(), 'docs-site'), // Current working directory
|
||||
];
|
||||
|
||||
let docsDir: string | null = null;
|
||||
for (const path of possiblePaths) {
|
||||
const resolvedPath = resolve(path);
|
||||
try {
|
||||
const { existsSync } = await import('fs');
|
||||
if (existsSync(resolvedPath)) {
|
||||
docsDir = resolvedPath;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Continue to next path
|
||||
}
|
||||
}
|
||||
|
||||
if (!docsDir) {
|
||||
console.log(chalk.yellow(` Docs site directory not found. Skipping docs server startup.`));
|
||||
console.log(chalk.gray(` The /docs endpoint will not be available.`));
|
||||
return effectivePort;
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(` Starting Docusaurus docs site on port ${effectivePort}...`));
|
||||
console.log(chalk.gray(` Docs dir: ${docsDir}`));
|
||||
|
||||
// Check if package.json exists and has required scripts
|
||||
const packageJsonPath = join(docsDir, 'package.json');
|
||||
let effectiveMode: DocsStartMode = requestedMode;
|
||||
try {
|
||||
const { readFileSync, existsSync, readdirSync } = await import('fs');
|
||||
if (!existsSync(packageJsonPath)) {
|
||||
throw new Error('package.json not found in docs-site directory');
|
||||
}
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
||||
|
||||
const hasStart = Boolean(packageJson.scripts?.start);
|
||||
const hasServe = Boolean(packageJson.scripts?.serve);
|
||||
|
||||
const hasMultipleLocales = (() => {
|
||||
try {
|
||||
const i18nDir = join(docsDir, 'i18n');
|
||||
if (!existsSync(i18nDir)) return false;
|
||||
// Presence of any locale subfolder implies multi-locale setup.
|
||||
return readdirSync(i18nDir, { withFileTypes: true }).some((entry) => entry.isDirectory());
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
// Docusaurus `start` serves only 1 locale at a time.
|
||||
// If the site has multiple locales and no locale is explicitly requested,
|
||||
// prefer serving the built site so `/docs/zh/*` works.
|
||||
if (requestedMode === 'start' && !requestedLocale && hasMultipleLocales && hasServe) {
|
||||
effectiveMode = 'serve';
|
||||
console.log(chalk.yellow(` CCW_DOCS_MODE=start detected, but CCW_DOCS_LOCALE is not set.`));
|
||||
console.log(chalk.gray(` Falling back to "serve --build" so all locales (e.g. /docs/zh/) are available.`));
|
||||
console.log(chalk.gray(` Tip: set CCW_DOCS_LOCALE=zh if you specifically want a single-locale dev server.`));
|
||||
}
|
||||
|
||||
// If the requested script isn't available, fall back to the other script.
|
||||
if (effectiveMode === 'serve' && !hasServe && hasStart) {
|
||||
effectiveMode = 'start';
|
||||
} else if (effectiveMode === 'start' && !hasStart && hasServe) {
|
||||
effectiveMode = 'serve';
|
||||
}
|
||||
|
||||
if (effectiveMode === 'serve' && !hasServe) {
|
||||
throw new Error('No "serve" script found in package.json');
|
||||
}
|
||||
if (effectiveMode === 'start' && !hasStart) {
|
||||
throw new Error('No "start" script found in package.json');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(chalk.yellow(` Failed to validate docs-site setup: ${error}`));
|
||||
console.log(chalk.gray(` Skipping docs server startup.`));
|
||||
return effectivePort;
|
||||
}
|
||||
|
||||
const args: string[] = [];
|
||||
if (effectiveMode === 'serve') {
|
||||
// Serve the built site (all locales) at /docs/ (baseUrl)
|
||||
args.push(
|
||||
'run',
|
||||
'serve',
|
||||
'--',
|
||||
'--build',
|
||||
'--port',
|
||||
effectivePort.toString(),
|
||||
'--host',
|
||||
'localhost',
|
||||
'--no-open',
|
||||
);
|
||||
} else {
|
||||
// Start a single-locale dev server (use CCW_DOCS_LOCALE to pick locale)
|
||||
args.push(
|
||||
'run',
|
||||
'start',
|
||||
'--',
|
||||
'--port',
|
||||
effectivePort.toString(),
|
||||
'--host',
|
||||
'localhost',
|
||||
'--no-open',
|
||||
);
|
||||
|
||||
if (requestedLocale) {
|
||||
args.push('--locale', requestedLocale);
|
||||
}
|
||||
}
|
||||
|
||||
docsProcess = spawn('npm', args, {
|
||||
cwd: docsDir,
|
||||
stdio: 'pipe',
|
||||
shell: true,
|
||||
env: {
|
||||
...process.env,
|
||||
// Docusaurus uses COLUMNS for terminal width
|
||||
COLUMNS: '80',
|
||||
},
|
||||
});
|
||||
|
||||
docsPort = effectivePort;
|
||||
|
||||
// Wait for server to be ready
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let output = '';
|
||||
let errorOutput = '';
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
docsProcess?.kill();
|
||||
reject(new Error(
|
||||
`Docs site startup timeout (60s).\n` +
|
||||
`Output: ${output}\n` +
|
||||
`Errors: ${errorOutput}`,
|
||||
));
|
||||
}, 60000); // Docusaurus can take longer to start
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timeout);
|
||||
docsProcess?.stdout?.removeAllListeners();
|
||||
docsProcess?.stderr?.removeAllListeners();
|
||||
};
|
||||
|
||||
docsProcess?.stdout?.on('data', (data: Buffer) => {
|
||||
const chunk = data.toString();
|
||||
output += chunk;
|
||||
|
||||
// Log all Docusaurus output for debugging
|
||||
console.log(chalk.gray(` Docs: ${chunk.trim()}`));
|
||||
|
||||
const isServeReady =
|
||||
chunk.includes('Serving "build" directory at:') ||
|
||||
chunk.includes('Serving "build" directory at');
|
||||
|
||||
const isStartReady =
|
||||
chunk.includes('Compiled successfully') ||
|
||||
chunk.includes('Compiled with warnings') ||
|
||||
chunk.includes('The server is running at') ||
|
||||
chunk.includes(`http://localhost:${effectivePort}`) ||
|
||||
(chunk.includes('Docusaurus') && (chunk.includes('started') || chunk.includes('ready'))) ||
|
||||
chunk.includes('Local:');
|
||||
|
||||
// Check for ready signals (Docusaurus output format)
|
||||
if ((effectiveMode === 'serve' && isServeReady) || (effectiveMode === 'start' && isStartReady)) {
|
||||
cleanup();
|
||||
console.log(chalk.green(` Docs site ready at http://localhost:${effectivePort}/docs/`));
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
docsProcess?.stderr?.on('data', (data: Buffer) => {
|
||||
const chunk = data.toString();
|
||||
errorOutput += chunk;
|
||||
// Log warnings but don't fail
|
||||
if (chunk.toLowerCase().includes('warn') || chunk.toLowerCase().includes('warning')) {
|
||||
console.log(chalk.yellow(` Docs: ${chunk.trim()}`));
|
||||
}
|
||||
});
|
||||
|
||||
docsProcess?.on('error', (err: Error) => {
|
||||
cleanup();
|
||||
reject(err);
|
||||
});
|
||||
|
||||
docsProcess?.on('exit', (code: number | null) => {
|
||||
if (code !== 0 && code !== null) {
|
||||
cleanup();
|
||||
reject(new Error(`Docs process exited with code ${code}. Errors: ${errorOutput}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return effectivePort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop Docusaurus documentation server (only if CCW started it).
|
||||
*/
|
||||
export async function stopDocsSite(): Promise<void> {
|
||||
if (docsProcess) {
|
||||
console.log(chalk.yellow(' Stopping docs site...'));
|
||||
|
||||
const pid = docsProcess.pid;
|
||||
|
||||
// On Windows with shell: true, killing the shell process can orphan children.
|
||||
// Prefer taskkill to terminate the entire process tree.
|
||||
if (process.platform === 'win32' && pid) {
|
||||
try {
|
||||
const { exec } = await import('child_process');
|
||||
await new Promise<void>((resolve) => {
|
||||
exec(`taskkill /T /PID ${pid}`, () => resolve());
|
||||
});
|
||||
} catch {
|
||||
// Fall back to SIGTERM below
|
||||
}
|
||||
} else {
|
||||
// Try graceful shutdown first
|
||||
docsProcess.kill('SIGTERM');
|
||||
}
|
||||
|
||||
// Wait up to 5 seconds for graceful shutdown
|
||||
await new Promise<void>((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
resolve();
|
||||
}, 5000);
|
||||
|
||||
docsProcess?.once('exit', () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// Force kill if still running
|
||||
if (docsProcess && docsProcess.exitCode === null) {
|
||||
// On Windows with shell: true, we need to kill the entire process group
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
// Use taskkill to forcefully terminate the process tree
|
||||
const { exec } = await import('child_process');
|
||||
if (pid) {
|
||||
await new Promise<void>((resolve) => {
|
||||
exec(`taskkill /F /T /PID ${pid}`, (err) => {
|
||||
if (err) {
|
||||
// Fallback to SIGKILL if taskkill fails
|
||||
docsProcess?.kill('SIGKILL');
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Fallback to SIGKILL
|
||||
docsProcess.kill('SIGKILL');
|
||||
}
|
||||
} else {
|
||||
docsProcess.kill('SIGKILL');
|
||||
}
|
||||
}
|
||||
|
||||
// Wait a bit more for force kill to complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
docsProcess = null;
|
||||
docsPort = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get docs site status (CCW-managed only).
|
||||
*/
|
||||
export function getDocsSiteStatus(): { running: boolean; port: number | null } {
|
||||
return {
|
||||
running: docsProcess !== null && !docsProcess.killed,
|
||||
port: docsPort,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user