@leynier/ccst 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leynier/ccst",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Claude Code Switch Tools for managing contexts",
5
5
  "keywords": [
6
6
  "claude",
@@ -7,6 +7,7 @@ import {
7
7
  getLogPath,
8
8
  getRunningDaemonPid,
9
9
  isProcessRunning,
10
+ killProcessTree,
10
11
  writePid,
11
12
  } from "../../utils/daemon.js";
12
13
 
@@ -29,19 +30,15 @@ export const ccsStartCommand = async (
29
30
  // If force and running, stop first
30
31
  if (existingPid !== null && options?.force) {
31
32
  console.log(pc.dim(`Stopping existing daemon (PID: ${existingPid})...`));
32
- try {
33
- process.kill(existingPid, "SIGTERM");
34
- // Wait for process to terminate
35
- const maxWait = 3000;
36
- const startTime = Date.now();
37
- while (Date.now() - startTime < maxWait) {
38
- if (!isProcessRunning(existingPid)) {
39
- break;
40
- }
41
- await new Promise((resolve) => setTimeout(resolve, 100));
33
+ await killProcessTree(existingPid, true);
34
+ // Wait for process to terminate
35
+ const maxWait = 3000;
36
+ const startTime = Date.now();
37
+ while (Date.now() - startTime < maxWait) {
38
+ if (!isProcessRunning(existingPid)) {
39
+ break;
42
40
  }
43
- } catch {
44
- // Process may have already exited
41
+ await new Promise((resolve) => setTimeout(resolve, 100));
45
42
  }
46
43
  }
47
44
  ensureDaemonDir();
@@ -53,8 +50,8 @@ export const ccsStartCommand = async (
53
50
  const child = spawn(ccsPath, ["config"], {
54
51
  detached: true,
55
52
  stdio: ["ignore", logFd, logFd],
56
- // On Windows, need shell: true for proper detachment
57
- ...(process.platform === "win32" ? { shell: true } : {}),
53
+ // On Windows, need shell: true for proper detachment and windowsHide to hide console
54
+ ...(process.platform === "win32" ? { shell: true, windowsHide: true } : {}),
58
55
  });
59
56
  if (!child.pid) {
60
57
  console.log(pc.red("Failed to start CCS config daemon"));
@@ -1,7 +1,10 @@
1
1
  import pc from "picocolors";
2
2
  import {
3
+ CCS_PORTS,
3
4
  getRunningDaemonPid,
4
5
  isProcessRunning,
6
+ killProcessByPort,
7
+ killProcessTree,
5
8
  removePid,
6
9
  } from "../../utils/daemon.js";
7
10
 
@@ -11,14 +14,10 @@ export type StopOptions = {
11
14
 
12
15
  export const ccsStopCommand = async (options?: StopOptions): Promise<void> => {
13
16
  const pid = await getRunningDaemonPid();
14
- if (pid === null) {
15
- console.log(pc.yellow("CCS config daemon is not running"));
16
- return;
17
- }
18
- try {
19
- // Send SIGTERM for graceful shutdown, SIGKILL if --force
20
- const signal = options?.force ? "SIGKILL" : "SIGTERM";
21
- process.kill(pid, signal);
17
+ let stopped = false;
18
+ // Phase 1: Kill by PID if exists
19
+ if (pid !== null) {
20
+ await killProcessTree(pid, options?.force);
22
21
  // Wait for process to terminate (with timeout)
23
22
  const maxWait = options?.force ? 1000 : 5000;
24
23
  const startTime = Date.now();
@@ -30,18 +29,17 @@ export const ccsStopCommand = async (options?: StopOptions): Promise<void> => {
30
29
  }
31
30
  removePid();
32
31
  console.log(pc.green(`CCS config daemon stopped (PID: ${pid})`));
33
- } catch (error) {
34
- const err = error as NodeJS.ErrnoException;
35
- if (err.code === "ESRCH") {
36
- // Process doesn't exist - clean up stale PID file
37
- removePid();
38
- console.log(pc.yellow("Process not found, cleaned up stale PID file"));
39
- } else if (err.code === "EPERM") {
40
- console.log(
41
- pc.red("Permission denied. Try running with elevated privileges."),
42
- );
43
- } else {
44
- console.log(pc.red(`Failed to stop daemon: ${err.message}`));
32
+ stopped = true;
33
+ }
34
+ // Phase 2: Kill processes by port (especially important on Windows)
35
+ for (const port of CCS_PORTS) {
36
+ const killed = await killProcessByPort(port, options?.force ?? true);
37
+ if (killed) {
38
+ console.log(pc.dim(`Cleaned up process on port ${port}`));
39
+ stopped = true;
45
40
  }
46
41
  }
42
+ if (!stopped) {
43
+ console.log(pc.yellow("CCS config daemon is not running"));
44
+ }
47
45
  };
@@ -81,3 +81,73 @@ export const getCcsExecutable = (): string => {
81
81
  // Use 'ccs' from PATH - it should be globally installed
82
82
  return "ccs";
83
83
  };
84
+
85
+ // Known CCS daemon ports
86
+ export const CCS_PORTS = [3000, 8317];
87
+
88
+ // Kill process tree (on Windows, kills all child processes)
89
+ export const killProcessTree = async (
90
+ pid: number,
91
+ force?: boolean,
92
+ ): Promise<boolean> => {
93
+ if (process.platform === "win32") {
94
+ const args = ["/PID", String(pid), "/T"];
95
+ if (force) args.push("/F");
96
+ const proc = Bun.spawn(["taskkill", ...args], {
97
+ stdout: "ignore",
98
+ stderr: "ignore",
99
+ });
100
+ await proc.exited;
101
+ return proc.exitCode === 0;
102
+ }
103
+ const signal = force ? "SIGKILL" : "SIGTERM";
104
+ try {
105
+ process.kill(pid, signal);
106
+ return true;
107
+ } catch {
108
+ return false;
109
+ }
110
+ };
111
+
112
+ // Get PID of process listening on a port
113
+ export const getProcessByPort = async (
114
+ port: number,
115
+ ): Promise<number | null> => {
116
+ if (process.platform === "win32") {
117
+ const proc = Bun.spawn(["cmd", "/c", `netstat -ano | findstr :${port}`], {
118
+ stdout: "pipe",
119
+ stderr: "ignore",
120
+ });
121
+ const output = await new Response(proc.stdout).text();
122
+ await proc.exited;
123
+ const lines = output.trim().split("\n");
124
+ for (const line of lines) {
125
+ if (line.includes("LISTENING") || line.includes("ESTABLISHED")) {
126
+ const parts = line.trim().split(/\s+/);
127
+ const pid = Number.parseInt(parts[parts.length - 1], 10);
128
+ if (Number.isFinite(pid) && pid > 0) {
129
+ return pid;
130
+ }
131
+ }
132
+ }
133
+ return null;
134
+ }
135
+ const proc = Bun.spawn(["lsof", "-ti", `:${port}`], {
136
+ stdout: "pipe",
137
+ stderr: "ignore",
138
+ });
139
+ const output = await new Response(proc.stdout).text();
140
+ await proc.exited;
141
+ const pid = Number.parseInt(output.trim(), 10);
142
+ return Number.isFinite(pid) ? pid : null;
143
+ };
144
+
145
+ // Kill process by port
146
+ export const killProcessByPort = async (
147
+ port: number,
148
+ force?: boolean,
149
+ ): Promise<boolean> => {
150
+ const pid = await getProcessByPort(port);
151
+ if (pid === null) return false;
152
+ return killProcessTree(pid, force);
153
+ };