@leynier/ccst 0.5.2 → 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/AGENTS.md +105 -105
- package/CLAUDE.md +1 -1
- package/GEMINI.md +1 -1
- package/LICENSE +21 -21
- package/package.json +3 -2
- package/{README.md → readme.md} +321 -321
- package/src/commands/ccs/install.ts +136 -0
- package/src/commands/ccs/setup.ts +39 -0
- package/src/commands/ccs/start.ts +80 -19
- package/src/commands/ccs/stop.ts +7 -2
- package/src/index.ts +25 -1
- package/src/utils/daemon.ts +108 -10
- package/src/utils/interactive.ts +3 -0
|
@@ -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,18 +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,
|
|
12
|
+
getProcessByPort,
|
|
8
13
|
getRunningDaemonPid,
|
|
9
14
|
isProcessRunning,
|
|
10
15
|
killProcessTree,
|
|
16
|
+
truncateFile,
|
|
11
17
|
writePid,
|
|
18
|
+
writePorts,
|
|
12
19
|
} from "../../utils/daemon.js";
|
|
13
20
|
|
|
14
21
|
export type StartOptions = {
|
|
15
22
|
force?: boolean;
|
|
23
|
+
keepLogs?: boolean;
|
|
24
|
+
port?: number;
|
|
16
25
|
};
|
|
17
26
|
|
|
18
27
|
export const ccsStartCommand = async (
|
|
@@ -43,34 +52,86 @@ export const ccsStartCommand = async (
|
|
|
43
52
|
}
|
|
44
53
|
ensureDaemonDir();
|
|
45
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
|
+
);
|
|
46
80
|
const ccsPath = getCcsExecutable();
|
|
47
81
|
let pid: number | undefined;
|
|
48
82
|
if (process.platform === "win32") {
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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,
|
|
94
|
+
stdio: "ignore",
|
|
95
|
+
windowsHide: true,
|
|
96
|
+
});
|
|
97
|
+
proc.unref();
|
|
98
|
+
|
|
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)..."),
|
|
63
110
|
);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
}
|
|
122
|
+
|
|
123
|
+
if (foundPid === null) {
|
|
67
124
|
console.log(pc.red("Failed to start CCS config daemon"));
|
|
125
|
+
console.log(
|
|
126
|
+
pc.dim("Check if ccs is installed: npm install -g @kaitranntt/ccs"),
|
|
127
|
+
);
|
|
68
128
|
return;
|
|
69
129
|
}
|
|
130
|
+
pid = foundPid;
|
|
70
131
|
} else {
|
|
71
132
|
// On Unix, use regular spawn with detached mode
|
|
72
133
|
const logFd = openSync(logPath, "a");
|
|
73
|
-
const child = spawn(ccsPath, ["config"], {
|
|
134
|
+
const child = spawn(ccsPath, ["config", "--port", String(dashboardPort)], {
|
|
74
135
|
detached: true,
|
|
75
136
|
stdio: ["ignore", logFd, logFd],
|
|
76
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 (
|
|
@@ -114,24 +195,41 @@ export const getProcessByPort = async (
|
|
|
114
195
|
port: number,
|
|
115
196
|
): Promise<number | null> => {
|
|
116
197
|
if (process.platform === "win32") {
|
|
117
|
-
|
|
198
|
+
// Run netstat directly and parse ourselves for exact port matching
|
|
199
|
+
const proc = Bun.spawn(["netstat", "-ano", "-p", "tcp"], {
|
|
118
200
|
stdout: "pipe",
|
|
119
201
|
stderr: "ignore",
|
|
120
202
|
});
|
|
121
203
|
const output = await new Response(proc.stdout).text();
|
|
122
204
|
await proc.exited;
|
|
123
|
-
|
|
205
|
+
|
|
206
|
+
const lines = output.split("\n");
|
|
124
207
|
for (const line of lines) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
208
|
+
// Format: Proto Local Address Foreign Address State PID
|
|
209
|
+
// TCP 0.0.0.0:3000 0.0.0.0:0 LISTENING 12345
|
|
210
|
+
const parts = line.trim().split(/\s+/);
|
|
211
|
+
if (parts.length < 5) continue;
|
|
212
|
+
|
|
213
|
+
const state = parts[3];
|
|
214
|
+
const localAddr = parts[1];
|
|
215
|
+
const pidStr = parts[4];
|
|
216
|
+
if (state !== "LISTENING" || !localAddr || !pidStr) continue;
|
|
217
|
+
|
|
218
|
+
// Extract port from local address (last part after colon)
|
|
219
|
+
const portMatch = localAddr.match(/:(\d+)$/);
|
|
220
|
+
if (!portMatch?.[1]) continue;
|
|
221
|
+
|
|
222
|
+
const localPort = Number.parseInt(portMatch[1], 10);
|
|
223
|
+
if (localPort !== port) continue;
|
|
224
|
+
|
|
225
|
+
const pid = Number.parseInt(pidStr, 10);
|
|
226
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
227
|
+
return pid;
|
|
131
228
|
}
|
|
132
229
|
}
|
|
133
230
|
return null;
|
|
134
231
|
}
|
|
232
|
+
// Unix: use lsof
|
|
135
233
|
const proc = Bun.spawn(["lsof", "-ti", `:${port}`], {
|
|
136
234
|
stdout: "pipe",
|
|
137
235
|
stderr: "ignore",
|
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();
|