@leynier/ccst 0.5.3 → 0.7.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 +2 -1
- package/src/commands/ccs/install.ts +136 -0
- package/src/commands/ccs/setup.ts +39 -0
- package/src/commands/ccs/start.ts +70 -12
- package/src/commands/ccs/stop.ts +7 -2
- package/src/index.ts +25 -1
- package/src/utils/daemon.ts +83 -2
- package/src/utils/interactive.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leynier/ccst",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Claude Code Switch Tools for managing contexts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"commander": "^12.1.0",
|
|
35
|
+
"get-port": "^7.1.0",
|
|
35
36
|
"jszip": "^3.10.1",
|
|
36
37
|
"picocolors": "^1.1.0"
|
|
37
38
|
},
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { promptInput } from "../../utils/interactive.js";
|
|
4
|
+
|
|
5
|
+
type PackageManager = {
|
|
6
|
+
name: string;
|
|
7
|
+
displayCommand: string;
|
|
8
|
+
command: string;
|
|
9
|
+
args: string[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const packageManagers: PackageManager[] = [
|
|
13
|
+
{
|
|
14
|
+
name: "bun",
|
|
15
|
+
displayCommand: "bun add -g @kaitranntt/ccs",
|
|
16
|
+
command: "bun",
|
|
17
|
+
args: ["add", "-g", "@kaitranntt/ccs"],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "npm",
|
|
21
|
+
displayCommand: "npm install -g @kaitranntt/ccs",
|
|
22
|
+
command: "npm",
|
|
23
|
+
args: ["install", "-g", "@kaitranntt/ccs"],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "pnpm",
|
|
27
|
+
displayCommand: "pnpm add -g @kaitranntt/ccs",
|
|
28
|
+
command: "pnpm",
|
|
29
|
+
args: ["add", "-g", "@kaitranntt/ccs"],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "yarn",
|
|
33
|
+
displayCommand: "yarn global add @kaitranntt/ccs",
|
|
34
|
+
command: "yarn",
|
|
35
|
+
args: ["global", "add", "@kaitranntt/ccs"],
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const selectPackageManager = async (): Promise<PackageManager | undefined> => {
|
|
40
|
+
const lines = packageManagers.map(
|
|
41
|
+
(pm, index) => `${index + 1}. ${pm.name} (${pm.displayCommand})`,
|
|
42
|
+
);
|
|
43
|
+
console.log("Select package manager to install @kaitranntt/ccs:");
|
|
44
|
+
console.log(lines.join("\n"));
|
|
45
|
+
|
|
46
|
+
const input = await promptInput("Select option (1-4)");
|
|
47
|
+
const index = Number.parseInt(input || "", 10);
|
|
48
|
+
|
|
49
|
+
if (!Number.isFinite(index) || index < 1 || index > packageManagers.length) {
|
|
50
|
+
console.log(pc.red("Invalid selection"));
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return packageManagers[index - 1];
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const verifyInstallation = async (): Promise<string | null> => {
|
|
58
|
+
const result = spawnSync("ccs", ["--version"], {
|
|
59
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
60
|
+
encoding: "utf8",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (result.status !== 0) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const version = result.stdout?.trim();
|
|
68
|
+
return version && version.length > 0 ? version : null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const promptForSetup = async (): Promise<boolean> => {
|
|
72
|
+
const response = await promptInput("Run ccs setup now? (y/n)");
|
|
73
|
+
return response?.toLowerCase() === "y";
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const ccsInstallCommand = async (): Promise<void> => {
|
|
77
|
+
// Step 1: Select package manager
|
|
78
|
+
const selectedPm = await selectPackageManager();
|
|
79
|
+
if (!selectedPm) {
|
|
80
|
+
console.log(pc.dim("Installation cancelled"));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Step 2: Execute installation
|
|
85
|
+
console.log(
|
|
86
|
+
pc.dim(
|
|
87
|
+
`Installing @kaitranntt/ccs using ${selectedPm.name}... (this may take a moment)`,
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
const installResult = spawnSync(selectedPm.command, selectedPm.args, {
|
|
91
|
+
stdio: "inherit",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (installResult.status !== 0) {
|
|
95
|
+
console.log(
|
|
96
|
+
pc.red(
|
|
97
|
+
`Error: Installation failed with exit code ${installResult.status}`,
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Step 3: Verify installation
|
|
104
|
+
console.log(pc.dim("Verifying installation..."));
|
|
105
|
+
const version = await verifyInstallation();
|
|
106
|
+
|
|
107
|
+
if (!version) {
|
|
108
|
+
console.log(
|
|
109
|
+
pc.yellow("Warning: ccs installed but could not verify installation"),
|
|
110
|
+
);
|
|
111
|
+
console.log(pc.dim("You may need to restart your terminal"));
|
|
112
|
+
console.log(pc.dim("Try running 'which ccs' or 'ccs --version' manually"));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(pc.green(`ccs installed successfully (${version})`));
|
|
117
|
+
|
|
118
|
+
// Step 4: Ask if user wants to run setup
|
|
119
|
+
const shouldRunSetup = await promptForSetup();
|
|
120
|
+
if (shouldRunSetup) {
|
|
121
|
+
console.log(pc.dim("Running ccs setup..."));
|
|
122
|
+
const setupResult = spawnSync("ccs", ["setup"], { stdio: "inherit" });
|
|
123
|
+
|
|
124
|
+
if (setupResult.status === 0) {
|
|
125
|
+
console.log(pc.green("Setup completed successfully"));
|
|
126
|
+
} else {
|
|
127
|
+
console.log(
|
|
128
|
+
pc.yellow(
|
|
129
|
+
`Setup exited with code ${setupResult.status} (this may not be an error)`,
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
console.log(pc.dim("You can run 'ccst ccs setup' later to configure ccs"));
|
|
135
|
+
}
|
|
136
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
|
|
4
|
+
export type SetupOptions = {
|
|
5
|
+
force?: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const ccsSetupCommand = async (
|
|
9
|
+
options?: SetupOptions,
|
|
10
|
+
): Promise<void> => {
|
|
11
|
+
// Check if ccs is installed
|
|
12
|
+
const which = spawnSync("which", ["ccs"], { stdio: "ignore" });
|
|
13
|
+
if (which.status !== 0) {
|
|
14
|
+
console.log(pc.red("Error: ccs command not found"));
|
|
15
|
+
console.log(pc.dim("Run 'ccst ccs install' to install it"));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Build arguments
|
|
20
|
+
const args = ["setup"];
|
|
21
|
+
if (options?.force) {
|
|
22
|
+
args.push("--force");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Execute ccs setup with real-time output
|
|
26
|
+
const result = spawnSync("ccs", args, {
|
|
27
|
+
stdio: "inherit",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Check exit code
|
|
31
|
+
if (result.status !== 0) {
|
|
32
|
+
console.log(
|
|
33
|
+
pc.red(`Error: ccs setup failed with exit code ${result.status}`),
|
|
34
|
+
);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(pc.green("Setup completed successfully"));
|
|
39
|
+
};
|
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { openSync } from "node:fs";
|
|
2
|
+
import { openSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import getPort from "get-port";
|
|
3
6
|
import pc from "picocolors";
|
|
4
7
|
import {
|
|
5
8
|
ensureDaemonDir,
|
|
6
9
|
getCcsExecutable,
|
|
10
|
+
getCliproxyPort,
|
|
7
11
|
getLogPath,
|
|
8
12
|
getProcessByPort,
|
|
9
13
|
getRunningDaemonPid,
|
|
10
14
|
isProcessRunning,
|
|
11
15
|
killProcessTree,
|
|
16
|
+
truncateFile,
|
|
12
17
|
writePid,
|
|
18
|
+
writePorts,
|
|
13
19
|
} from "../../utils/daemon.js";
|
|
14
20
|
|
|
15
21
|
export type StartOptions = {
|
|
16
22
|
force?: boolean;
|
|
23
|
+
keepLogs?: boolean;
|
|
24
|
+
port?: number;
|
|
17
25
|
};
|
|
18
26
|
|
|
19
27
|
export const ccsStartCommand = async (
|
|
@@ -44,28 +52,78 @@ export const ccsStartCommand = async (
|
|
|
44
52
|
}
|
|
45
53
|
ensureDaemonDir();
|
|
46
54
|
const logPath = getLogPath();
|
|
55
|
+
if (!options?.keepLogs) {
|
|
56
|
+
try {
|
|
57
|
+
await truncateFile(logPath);
|
|
58
|
+
} catch {
|
|
59
|
+
console.warn(
|
|
60
|
+
pc.yellow(
|
|
61
|
+
`Warning: could not truncate log; continuing (logs will be appended): ${logPath}`,
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Detect ports
|
|
67
|
+
const cliproxyPort = await getCliproxyPort();
|
|
68
|
+
const dashboardPort =
|
|
69
|
+
options?.port ??
|
|
70
|
+
(await getPort({
|
|
71
|
+
port: [3000, 3001, 3002, 8000, 8080],
|
|
72
|
+
}));
|
|
73
|
+
// Save ports for stop command
|
|
74
|
+
await writePorts({ dashboard: dashboardPort, cliproxy: cliproxyPort });
|
|
75
|
+
console.log(
|
|
76
|
+
pc.dim(
|
|
77
|
+
`Using dashboard port: ${dashboardPort}, CLIProxy port: ${cliproxyPort}`,
|
|
78
|
+
),
|
|
79
|
+
);
|
|
47
80
|
const ccsPath = getCcsExecutable();
|
|
48
81
|
let pid: number | undefined;
|
|
49
82
|
if (process.platform === "win32") {
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
|
|
83
|
+
// VBScript is the ONLY reliable way to run a process completely hidden on Windows
|
|
84
|
+
// WScript.Shell.Run with 0 = hidden window, False = don't wait
|
|
85
|
+
// Redirect output to log file using cmd /c with shell redirection
|
|
86
|
+
const escapedLogPath = logPath.replace(/\\/g, "\\\\");
|
|
87
|
+
const vbsContent = `CreateObject("WScript.Shell").Run "cmd /c ${ccsPath} config --port ${dashboardPort} >> ${escapedLogPath} 2>&1", 0, False`;
|
|
88
|
+
const vbsPath = join(tmpdir(), `ccs-start-${Date.now()}.vbs`);
|
|
89
|
+
await Bun.write(vbsPath, vbsContent);
|
|
90
|
+
|
|
91
|
+
// Run the vbs file (wscript itself doesn't show a window)
|
|
92
|
+
const proc = spawn("wscript", [vbsPath], {
|
|
93
|
+
detached: true,
|
|
53
94
|
stdio: "ignore",
|
|
54
95
|
windowsHide: true,
|
|
55
|
-
detached: true,
|
|
56
96
|
});
|
|
57
97
|
proc.unref();
|
|
58
98
|
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
99
|
+
// Clean up the vbs file after a short delay
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
try {
|
|
102
|
+
unlinkSync(vbsPath);
|
|
103
|
+
} catch {}
|
|
104
|
+
}, 1000);
|
|
105
|
+
|
|
106
|
+
// Poll for the port to become available
|
|
107
|
+
// ccs config takes ~6s to start (5s CLIProxy timeout + dashboard startup)
|
|
108
|
+
console.log(
|
|
109
|
+
pc.dim("Starting CCS config daemon (this may take a few seconds)..."),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const maxWaitMs = 15000; // 15 seconds max
|
|
113
|
+
const pollIntervalMs = 500;
|
|
114
|
+
const startTime = Date.now();
|
|
115
|
+
let foundPid: number | null = null;
|
|
116
|
+
|
|
117
|
+
while (Date.now() - startTime < maxWaitMs) {
|
|
118
|
+
foundPid = await getProcessByPort(dashboardPort);
|
|
119
|
+
if (foundPid !== null) break;
|
|
120
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
121
|
+
}
|
|
62
122
|
|
|
63
|
-
// Find the process by port 3000 (dashboard port)
|
|
64
|
-
const foundPid = await getProcessByPort(3000);
|
|
65
123
|
if (foundPid === null) {
|
|
66
124
|
console.log(pc.red("Failed to start CCS config daemon"));
|
|
67
125
|
console.log(
|
|
68
|
-
pc.dim("Check if ccs is installed: npm install -g @
|
|
126
|
+
pc.dim("Check if ccs is installed: npm install -g @kaitranntt/ccs"),
|
|
69
127
|
);
|
|
70
128
|
return;
|
|
71
129
|
}
|
|
@@ -73,7 +131,7 @@ export const ccsStartCommand = async (
|
|
|
73
131
|
} else {
|
|
74
132
|
// On Unix, use regular spawn with detached mode
|
|
75
133
|
const logFd = openSync(logPath, "a");
|
|
76
|
-
const child = spawn(ccsPath, ["config"], {
|
|
134
|
+
const child = spawn(ccsPath, ["config", "--port", String(dashboardPort)], {
|
|
77
135
|
detached: true,
|
|
78
136
|
stdio: ["ignore", logFd, logFd],
|
|
79
137
|
});
|
package/src/commands/ccs/stop.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import pc from "picocolors";
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
getPortsToKill,
|
|
4
4
|
getRunningDaemonPid,
|
|
5
5
|
isProcessRunning,
|
|
6
6
|
killProcessByPort,
|
|
7
7
|
killProcessTree,
|
|
8
8
|
removePid,
|
|
9
|
+
removePorts,
|
|
9
10
|
} from "../../utils/daemon.js";
|
|
10
11
|
|
|
11
12
|
export type StopOptions = {
|
|
@@ -32,13 +33,17 @@ export const ccsStopCommand = async (options?: StopOptions): Promise<void> => {
|
|
|
32
33
|
stopped = true;
|
|
33
34
|
}
|
|
34
35
|
// Phase 2: Kill processes by port (especially important on Windows)
|
|
35
|
-
|
|
36
|
+
// Use saved ports or fallback to defaults
|
|
37
|
+
const ports = await getPortsToKill();
|
|
38
|
+
for (const port of ports) {
|
|
36
39
|
const killed = await killProcessByPort(port, options?.force ?? true);
|
|
37
40
|
if (killed) {
|
|
38
41
|
console.log(pc.dim(`Cleaned up process on port ${port}`));
|
|
39
42
|
stopped = true;
|
|
40
43
|
}
|
|
41
44
|
}
|
|
45
|
+
// Clean up ports file
|
|
46
|
+
removePorts();
|
|
42
47
|
if (!stopped) {
|
|
43
48
|
console.log(pc.yellow("CCS config daemon is not running"));
|
|
44
49
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import pkg from "../package.json";
|
|
4
|
+
import { ccsInstallCommand } from "./commands/ccs/install.js";
|
|
4
5
|
import { ccsLogsCommand } from "./commands/ccs/logs.js";
|
|
6
|
+
import { ccsSetupCommand } from "./commands/ccs/setup.js";
|
|
5
7
|
import { ccsStartCommand } from "./commands/ccs/start.js";
|
|
6
8
|
import { ccsStatusCommand } from "./commands/ccs/status.js";
|
|
7
9
|
import { ccsStopCommand } from "./commands/ccs/stop.js";
|
|
@@ -188,8 +190,17 @@ const main = async (): Promise<void> => {
|
|
|
188
190
|
.command("start")
|
|
189
191
|
.description("Start CCS config as background daemon")
|
|
190
192
|
.option("-f, --force", "Force restart if already running")
|
|
193
|
+
.option("--keep-logs", "Keep existing log file (append)")
|
|
194
|
+
.option(
|
|
195
|
+
"-p, --port <number>",
|
|
196
|
+
"Dashboard port (auto-detect if not specified)",
|
|
197
|
+
)
|
|
191
198
|
.action(async (options) => {
|
|
192
|
-
await ccsStartCommand(
|
|
199
|
+
await ccsStartCommand({
|
|
200
|
+
force: options.force,
|
|
201
|
+
keepLogs: options.keepLogs,
|
|
202
|
+
port: options.port ? Number.parseInt(options.port, 10) : undefined,
|
|
203
|
+
});
|
|
193
204
|
});
|
|
194
205
|
ccsCommandGroup
|
|
195
206
|
.command("stop")
|
|
@@ -215,6 +226,19 @@ const main = async (): Promise<void> => {
|
|
|
215
226
|
lines: parseInt(options.lines, 10),
|
|
216
227
|
});
|
|
217
228
|
});
|
|
229
|
+
ccsCommandGroup
|
|
230
|
+
.command("setup")
|
|
231
|
+
.description("Run CCS initial setup")
|
|
232
|
+
.option("-f, --force", "Force setup even if already configured")
|
|
233
|
+
.action(async (options) => {
|
|
234
|
+
await ccsSetupCommand(options);
|
|
235
|
+
});
|
|
236
|
+
ccsCommandGroup
|
|
237
|
+
.command("install")
|
|
238
|
+
.description("Install CCS CLI tool")
|
|
239
|
+
.action(async () => {
|
|
240
|
+
await ccsInstallCommand();
|
|
241
|
+
});
|
|
218
242
|
try {
|
|
219
243
|
await program.parseAsync(process.argv);
|
|
220
244
|
} catch {
|
package/src/utils/daemon.ts
CHANGED
|
@@ -11,6 +11,11 @@ export const getPidPath = (): string => join(getDaemonDir(), "ccs-config.pid");
|
|
|
11
11
|
// Log file path
|
|
12
12
|
export const getLogPath = (): string => join(getDaemonDir(), "ccs-config.log");
|
|
13
13
|
|
|
14
|
+
// Truncate a file (or create it empty if missing)
|
|
15
|
+
export const truncateFile = async (filePath: string): Promise<void> => {
|
|
16
|
+
await Bun.write(filePath, "");
|
|
17
|
+
};
|
|
18
|
+
|
|
14
19
|
// Ensure daemon directory exists
|
|
15
20
|
export const ensureDaemonDir = (): void => {
|
|
16
21
|
const dir = getDaemonDir();
|
|
@@ -82,8 +87,84 @@ export const getCcsExecutable = (): string => {
|
|
|
82
87
|
return "ccs";
|
|
83
88
|
};
|
|
84
89
|
|
|
85
|
-
//
|
|
86
|
-
export const
|
|
90
|
+
// Default CCS daemon ports (fallback)
|
|
91
|
+
export const DEFAULT_DASHBOARD_PORT = 3000;
|
|
92
|
+
export const DEFAULT_CLIPROXY_PORT = 8317;
|
|
93
|
+
|
|
94
|
+
// Ports file path
|
|
95
|
+
export const getPortsPath = (): string => join(getDaemonDir(), "ports.json");
|
|
96
|
+
|
|
97
|
+
// Type for daemon ports
|
|
98
|
+
export type DaemonPorts = {
|
|
99
|
+
dashboard: number;
|
|
100
|
+
cliproxy: number;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Read saved ports
|
|
104
|
+
export const readPorts = async (): Promise<DaemonPorts | null> => {
|
|
105
|
+
const portsPath = getPortsPath();
|
|
106
|
+
if (!existsSync(portsPath)) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const content = await Bun.file(portsPath).text();
|
|
111
|
+
const ports = JSON.parse(content) as DaemonPorts;
|
|
112
|
+
if (
|
|
113
|
+
typeof ports.dashboard === "number" &&
|
|
114
|
+
typeof ports.cliproxy === "number"
|
|
115
|
+
) {
|
|
116
|
+
return ports;
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Write ports to file
|
|
125
|
+
export const writePorts = async (ports: DaemonPorts): Promise<void> => {
|
|
126
|
+
ensureDaemonDir();
|
|
127
|
+
await Bun.write(getPortsPath(), JSON.stringify(ports, null, 2));
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Remove ports file
|
|
131
|
+
export const removePorts = (): void => {
|
|
132
|
+
const portsPath = getPortsPath();
|
|
133
|
+
if (existsSync(portsPath)) {
|
|
134
|
+
unlinkSync(portsPath);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Read CLIProxy port from config.yaml
|
|
139
|
+
export const getCliproxyPort = async (): Promise<number> => {
|
|
140
|
+
const configPath = join(homedir(), ".ccs", "cliproxy", "config.yaml");
|
|
141
|
+
if (!existsSync(configPath)) {
|
|
142
|
+
return DEFAULT_CLIPROXY_PORT;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const content = await Bun.file(configPath).text();
|
|
146
|
+
// Simple YAML parsing for "port: XXXX"
|
|
147
|
+
const match = content.match(/^port:\s*(\d+)/m);
|
|
148
|
+
if (match?.[1]) {
|
|
149
|
+
const port = Number.parseInt(match[1], 10);
|
|
150
|
+
if (Number.isFinite(port) && port > 0) {
|
|
151
|
+
return port;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return DEFAULT_CLIPROXY_PORT;
|
|
155
|
+
} catch {
|
|
156
|
+
return DEFAULT_CLIPROXY_PORT;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Get ports to kill (from saved file or defaults)
|
|
161
|
+
export const getPortsToKill = async (): Promise<number[]> => {
|
|
162
|
+
const saved = await readPorts();
|
|
163
|
+
if (saved) {
|
|
164
|
+
return [saved.dashboard, saved.cliproxy];
|
|
165
|
+
}
|
|
166
|
+
return [DEFAULT_DASHBOARD_PORT, DEFAULT_CLIPROXY_PORT];
|
|
167
|
+
};
|
|
87
168
|
|
|
88
169
|
// Kill process tree (on Windows, kills all child processes)
|
|
89
170
|
export const killProcessTree = async (
|
package/src/utils/interactive.ts
CHANGED
|
@@ -71,6 +71,9 @@ const readStdinLine = async (): Promise<string> => {
|
|
|
71
71
|
const cleanup = () => {
|
|
72
72
|
process.stdin.off("data", onData);
|
|
73
73
|
process.stdin.off("end", onEnd);
|
|
74
|
+
if (process.stdin.isTTY) {
|
|
75
|
+
process.stdin.pause();
|
|
76
|
+
}
|
|
74
77
|
};
|
|
75
78
|
if (process.stdin.isTTY) {
|
|
76
79
|
process.stdin.resume();
|