Merge pull request #148 from kaitranntt/dev

fix(cliproxy): register session on dashboard start and add port-based stop fallback
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2025-12-19 03:59:09 -05:00
commit c564b48fec
8 files changed
+54 -13

No files matched your search

+6
View File
@@ -1,3 +1,9 @@
## [6.6.1-dev.1](https://github.com/kaitranntt/ccs/compare/v6.6.0...v6.6.1-dev.1) (2025-12-19)
### Bug Fixes
* **cliproxy:** register session on dashboard start and add port-based stop fallback ([a3b172c](https://github.com/kaitranntt/ccs/commit/a3b172cc437c4a00612667edf44f8aa3b3ffa1ae))
## [6.6.0](https://github.com/kaitranntt/ccs/compare/v6.5.0...v6.6.0) (2025-12-19)
### ⚠ BREAKING CHANGES
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kaitranntt/ccs",
"version": "6.6.0",
"version": "6.6.1-dev.1",
"description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
"keywords": [
"cli",
+8
View File
@@ -22,6 +22,7 @@ import {
getCliproxyWritablePath,
} from './config-generator';
import { isCliproxyRunning } from './stats-fetcher';
import { registerSession } from './session-tracker';
/** Background proxy process reference */
let proxyProcess: ChildProcess | null = null;
@@ -219,6 +220,13 @@ export async function ensureCliproxyService(
}
log(`CLIProxy service started on port ${port}`);
// 5. Register session so stopProxy() can find and kill this process
if (proxyProcess.pid) {
registerSession(port, proxyProcess.pid);
log(`Session registered for PID ${proxyProcess.pid}`);
}
return { started: true, alreadyRunning: false, port };
}
+30 -3
View File
@@ -18,6 +18,8 @@ import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import { getCliproxyDir } from './config-generator';
import { getPortProcess, isCLIProxyProcess } from '../utils/port-utils';
import { CLIPROXY_DEFAULT_PORT } from './config-generator';
/** Session lock file structure */
interface SessionLock {
@@ -227,18 +229,43 @@ export function cleanupOrphanedSessions(port: number): void {
/**
* Stop the CLIProxy process and clean up session lock.
* Falls back to port-based detection if no session lock exists.
* @returns Object with success status and details
*/
export function stopProxy(): {
export async function stopProxy(): Promise<{
stopped: boolean;
pid?: number;
sessionCount?: number;
error?: string;
} {
}> {
const lock = readSessionLock();
if (!lock) {
return { stopped: false, error: 'No active CLIProxy session found' };
// No session lock - try to find process by port (legacy/untracked proxy)
const portProcess = await getPortProcess(CLIPROXY_DEFAULT_PORT);
if (!portProcess) {
return { stopped: false, error: 'No active CLIProxy session found' };
}
if (!isCLIProxyProcess(portProcess)) {
return {
stopped: false,
error: `Port ${CLIPROXY_DEFAULT_PORT} is in use by ${portProcess.processName}, not CLIProxy`,
};
}
// Found CLIProxy running without session lock - kill it
try {
process.kill(portProcess.pid, 'SIGTERM');
return { stopped: true, pid: portProcess.pid, sessionCount: 0 };
} catch (err) {
const error = err as NodeJS.ErrnoException;
if (error.code === 'ESRCH') {
return { stopped: false, error: 'CLIProxy process already terminated' };
}
return { stopped: false, pid: portProcess.pid, error: `Failed to stop: ${error.message}` };
}
}
// Check if proxy is running
+1 -1
View File
@@ -1069,7 +1069,7 @@ async function handleStop(): Promise<void> {
console.log(header('Stop CLIProxy'));
console.log('');
const result = stopProxy();
const result = await stopProxy();
if (result.stopped) {
console.log(ok(`CLIProxy stopped (PID ${result.pid})`));
+2 -2
View File
@@ -88,7 +88,7 @@ async function getPortProcessWindows(port: number): Promise<PortProcess | null>
}
// Parse netstat output to get PID (last column)
const match = netstatOut.match(/\\s+(\d+)\\s*$/m);
const match = netstatOut.match(/\s+(\d+)\s*$/m);
if (!match) {
return null;
}
@@ -100,7 +100,7 @@ async function getPortProcessWindows(port: number): Promise<PortProcess | null>
timeout: 3000,
});
const taskMatch = tasklistOut.match(/^([^\\s]+)/);
const taskMatch = tasklistOut.match(/^([^\s]+)/);
const processName = taskMatch ? taskMatch[1] : `PID-${pid}`;
return { pid, processName };
+2 -2
View File
@@ -1394,9 +1394,9 @@ apiRoutes.post('/cliproxy/proxy-start', async (_req: Request, res: Response): Pr
* POST /api/cliproxy/proxy-stop - Stop the CLIProxy service
* Returns: { stopped, pid?, sessionCount?, error? }
*/
apiRoutes.post('/cliproxy/proxy-stop', (_req: Request, res: Response): void => {
apiRoutes.post('/cliproxy/proxy-stop', async (_req: Request, res: Response): Promise<void> => {
try {
const result = stopProxy();
const result = await stopProxy();
res.json(result);
} catch (error) {
res.status(500).json({ error: (error as Error).message });
+4 -4
View File
@@ -299,13 +299,13 @@ describe('Session Tracker', function () {
});
describe('stopProxy', function () {
it('should return error when no lock exists', function () {
const result = stopProxy();
it('should return error when no lock exists', async function () {
const result = await stopProxy();
assert.strictEqual(result.stopped, false);
assert.strictEqual(result.error, 'No active CLIProxy session found');
});
it('should cleanup stale lock when proxy is not running', function () {
it('should cleanup stale lock when proxy is not running', async function () {
// Create lock with dead PID
const lock = {
port: testPort,
@@ -315,7 +315,7 @@ describe('Session Tracker', function () {
};
fs.writeFileSync(sessionLockPath, JSON.stringify(lock));
const result = stopProxy();
const result = await stopProxy();
assert.strictEqual(result.stopped, false);
assert.ok(result.error.includes('not running'));
assert.strictEqual(fs.existsSync(sessionLockPath), false);