@leynier/ccst
Advanced tools
+1
-1
| { | ||
| "name": "@leynier/ccst", | ||
| "version": "0.4.0", | ||
| "version": "0.5.0", | ||
| "description": "Claude Code Switch Tools for managing contexts", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
@@ -10,2 +10,3 @@ import { spawn } from "node:child_process"; | ||
| isProcessRunning, | ||
| killProcessTree, | ||
| writePid, | ||
@@ -33,15 +34,11 @@ } from "../../utils/daemon.js"; | ||
| console.log(pc.dim(`Stopping existing daemon (PID: ${existingPid})...`)); | ||
| try { | ||
| process.kill(existingPid, "SIGTERM"); | ||
| // Wait for process to terminate | ||
| const maxWait = 3000; | ||
| const startTime = Date.now(); | ||
| while (Date.now() - startTime < maxWait) { | ||
| if (!isProcessRunning(existingPid)) { | ||
| break; | ||
| } | ||
| await new Promise((resolve) => setTimeout(resolve, 100)); | ||
| await killProcessTree(existingPid, true); | ||
| // Wait for process to terminate | ||
| const maxWait = 3000; | ||
| const startTime = Date.now(); | ||
| while (Date.now() - startTime < maxWait) { | ||
| if (!isProcessRunning(existingPid)) { | ||
| break; | ||
| } | ||
| } catch { | ||
| // Process may have already exited | ||
| await new Promise((resolve) => setTimeout(resolve, 100)); | ||
| } | ||
@@ -58,4 +55,4 @@ } | ||
| stdio: ["ignore", logFd, logFd], | ||
| // On Windows, need shell: true for proper detachment | ||
| ...(process.platform === "win32" ? { shell: true } : {}), | ||
| // On Windows, need shell: true for proper detachment and windowsHide to hide console | ||
| ...(process.platform === "win32" ? { shell: true, windowsHide: true } : {}), | ||
| }); | ||
@@ -62,0 +59,0 @@ if (!child.pid) { |
+18
-20
| import pc from "picocolors"; | ||
| import { | ||
| CCS_PORTS, | ||
| getRunningDaemonPid, | ||
| isProcessRunning, | ||
| killProcessByPort, | ||
| killProcessTree, | ||
| removePid, | ||
@@ -14,10 +17,6 @@ } from "../../utils/daemon.js"; | ||
| const pid = await getRunningDaemonPid(); | ||
| if (pid === null) { | ||
| console.log(pc.yellow("CCS config daemon is not running")); | ||
| return; | ||
| } | ||
| try { | ||
| // Send SIGTERM for graceful shutdown, SIGKILL if --force | ||
| const signal = options?.force ? "SIGKILL" : "SIGTERM"; | ||
| process.kill(pid, signal); | ||
| let stopped = false; | ||
| // Phase 1: Kill by PID if exists | ||
| if (pid !== null) { | ||
| await killProcessTree(pid, options?.force); | ||
| // Wait for process to terminate (with timeout) | ||
@@ -34,16 +33,15 @@ const maxWait = options?.force ? 1000 : 5000; | ||
| console.log(pc.green(`CCS config daemon stopped (PID: ${pid})`)); | ||
| } catch (error) { | ||
| const err = error as NodeJS.ErrnoException; | ||
| if (err.code === "ESRCH") { | ||
| // Process doesn't exist - clean up stale PID file | ||
| removePid(); | ||
| console.log(pc.yellow("Process not found, cleaned up stale PID file")); | ||
| } else if (err.code === "EPERM") { | ||
| console.log( | ||
| pc.red("Permission denied. Try running with elevated privileges."), | ||
| ); | ||
| } else { | ||
| console.log(pc.red(`Failed to stop daemon: ${err.message}`)); | ||
| stopped = true; | ||
| } | ||
| // Phase 2: Kill processes by port (especially important on Windows) | ||
| for (const port of CCS_PORTS) { | ||
| const killed = await killProcessByPort(port, options?.force ?? true); | ||
| if (killed) { | ||
| console.log(pc.dim(`Cleaned up process on port ${port}`)); | ||
| stopped = true; | ||
| } | ||
| } | ||
| if (!stopped) { | ||
| console.log(pc.yellow("CCS config daemon is not running")); | ||
| } | ||
| }; |
+70
-0
@@ -84,1 +84,71 @@ import { existsSync, mkdirSync, unlinkSync } from "node:fs"; | ||
| }; | ||
| // Known CCS daemon ports | ||
| export const CCS_PORTS = [3000, 8317]; | ||
| // Kill process tree (on Windows, kills all child processes) | ||
| export const killProcessTree = async ( | ||
| pid: number, | ||
| force?: boolean, | ||
| ): Promise<boolean> => { | ||
| if (process.platform === "win32") { | ||
| const args = ["/PID", String(pid), "/T"]; | ||
| if (force) args.push("/F"); | ||
| const proc = Bun.spawn(["taskkill", ...args], { | ||
| stdout: "ignore", | ||
| stderr: "ignore", | ||
| }); | ||
| await proc.exited; | ||
| return proc.exitCode === 0; | ||
| } | ||
| const signal = force ? "SIGKILL" : "SIGTERM"; | ||
| try { | ||
| process.kill(pid, signal); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| }; | ||
| // Get PID of process listening on a port | ||
| export const getProcessByPort = async ( | ||
| port: number, | ||
| ): Promise<number | null> => { | ||
| if (process.platform === "win32") { | ||
| const proc = Bun.spawn(["cmd", "/c", `netstat -ano | findstr :${port}`], { | ||
| stdout: "pipe", | ||
| stderr: "ignore", | ||
| }); | ||
| const output = await new Response(proc.stdout).text(); | ||
| await proc.exited; | ||
| const lines = output.trim().split("\n"); | ||
| for (const line of lines) { | ||
| if (line.includes("LISTENING") || line.includes("ESTABLISHED")) { | ||
| const parts = line.trim().split(/\s+/); | ||
| const pid = Number.parseInt(parts[parts.length - 1], 10); | ||
| if (Number.isFinite(pid) && pid > 0) { | ||
| return pid; | ||
| } | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| const proc = Bun.spawn(["lsof", "-ti", `:${port}`], { | ||
| stdout: "pipe", | ||
| stderr: "ignore", | ||
| }); | ||
| const output = await new Response(proc.stdout).text(); | ||
| await proc.exited; | ||
| const pid = Number.parseInt(output.trim(), 10); | ||
| return Number.isFinite(pid) ? pid : null; | ||
| }; | ||
| // Kill process by port | ||
| export const killProcessByPort = async ( | ||
| port: number, | ||
| force?: boolean, | ||
| ): Promise<boolean> => { | ||
| const pid = await getProcessByPort(port); | ||
| if (pid === null) return false; | ||
| return killProcessTree(pid, force); | ||
| }; |
83186
2.07%2309
2.71%