fix(cliproxy): respect configured local port instead of hardcoding 8317

All call sites that spawn or probe CLIProxyApiPlus now read
cliproxy_server.local.port from config via resolveLifecyclePort()
instead of using the hardcoded CLIPROXY_DEFAULT_PORT constant.

- Move resolveLifecyclePort helper to src/cliproxy/config/port-manager.ts
- Fix 7 call sites: ccs.ts, config-command.ts, copilot-executor.ts,
  lifecycle.ts, binary-manager.ts, cliproxy-stats-routes.ts,
  cliproxy-local-proxy.ts
- Remove duplicate resolveLocalCliproxyPort helper
- Cache port resolution in /proxy-status handler to avoid repeated I/O

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Molko
2026-05-03 07:40:48 -04:00
co-authored by Claude Opus 4.7
parent 0e83c916f9
commit 509bd5dbef
11 changed files with 34 additions and 45 deletions
+3 -3
View File
@@ -22,7 +22,7 @@ import {
isAuthenticated, isAuthenticated,
} from './cliproxy'; } from './cliproxy';
import { getEffectiveEnvVars, getCompositeEnvVars } from './cliproxy/config/env-builder'; import { getEffectiveEnvVars, getCompositeEnvVars } from './cliproxy/config/env-builder';
import { CLIPROXY_DEFAULT_PORT } from './cliproxy/config/port-manager'; import { resolveLifecyclePort } from './cliproxy/config/port-manager';
import { import {
ensureWebSearchMcpOrThrow, ensureWebSearchMcpOrThrow,
displayWebSearchStatus, displayWebSearchStatus,
@@ -929,7 +929,7 @@ async function main(): Promise<void> {
} }
const customSettingsPath = profileInfo.settingsPath; // undefined for hardcoded profiles const customSettingsPath = profileInfo.settingsPath; // undefined for hardcoded profiles
const variantPort = profileInfo.port; // variant-specific port for isolation const variantPort = profileInfo.port; // variant-specific port for isolation
const cliproxyPort = variantPort || CLIPROXY_DEFAULT_PORT; const cliproxyPort = variantPort || resolveLifecyclePort();
if (resolvedTarget !== 'claude') { if (resolvedTarget !== 'claude') {
const adapter = targetAdapter; const adapter = targetAdapter;
@@ -1385,7 +1385,7 @@ async function main(): Promise<void> {
}; };
} else if (imageAnalysisStatus.proxyReadiness === 'stopped') { } else if (imageAnalysisStatus.proxyReadiness === 'stopped') {
const ensureServiceResult = await ensureCliproxyService( const ensureServiceResult = await ensureCliproxyService(
CLIPROXY_DEFAULT_PORT, resolveLifecyclePort(),
verboseProxyLaunch verboseProxyLaunch
); );
if (!ensureServiceResult.started) { if (!ensureServiceResult.started) {
+3 -2
View File
@@ -8,7 +8,8 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { info, warn } from '../utils/ui'; import { info, warn } from '../utils/ui';
import { getBinDir, CLIPROXY_DEFAULT_PORT } from './config/config-generator'; import { getBinDir } from './config/config-generator';
import { resolveLifecyclePort } from './config/port-manager';
import { BinaryInfo, BinaryManagerConfig } from './types'; import { BinaryInfo, BinaryManagerConfig } from './types';
import { import {
BACKEND_CONFIG, BACKEND_CONFIG,
@@ -340,7 +341,7 @@ export async function installCliproxyVersion(
const result = await stopProxyFn(); const result = await stopProxyFn();
if (result.stopped) { if (result.stopped) {
// Wait for port to be fully released // Wait for port to be fully released
const portFree = await waitForPortFreeFn(CLIPROXY_DEFAULT_PORT, 5000); const portFree = await waitForPortFreeFn(resolveLifecyclePort(), 5000);
if (!portFree && verbose) { if (!portFree && verbose) {
console.log(formatWarn('Port did not free up in time, proceeding anyway...')); console.log(formatWarn('Port did not free up in time, proceeding anyway...'));
} }
+2 -2
View File
@@ -14,7 +14,7 @@ import {
import { downloadAndInstall, deleteBinary, getBinaryPath } from './installer'; import { downloadAndInstall, deleteBinary, getBinaryPath } from './installer';
import { info, warn } from '../../utils/ui'; import { info, warn } from '../../utils/ui';
import { isCliproxyRunning } from '../services/stats-fetcher'; import { isCliproxyRunning } from '../services/stats-fetcher';
import { CLIPROXY_DEFAULT_PORT } from '../config/config-generator'; import { resolveLifecyclePort } from '../config/port-manager';
import { import {
CLIPROXY_MAX_STABLE_VERSION, CLIPROXY_MAX_STABLE_VERSION,
CLIPROXY_FAULTY_RANGE, CLIPROXY_FAULTY_RANGE,
@@ -82,7 +82,7 @@ async function handleAutoUpdate(config: BinaryManagerConfig, verbose: boolean):
return; return;
} }
const proxyRunning = await isCliproxyRunning(CLIPROXY_DEFAULT_PORT); const proxyRunning = await isCliproxyRunning(resolveLifecyclePort());
const latestNote = isAboveMaxStable(latestVersion) ? ` (latest v${latestVersion} unstable)` : ''; const latestNote = isAboveMaxStable(latestVersion) ? ` (latest v${latestVersion} unstable)` : '';
const updateMsg = `${backendLabel} update: v${currentVersion} -> v${targetVersion}${latestNote}`; const updateMsg = `${backendLabel} update: v${currentVersion} -> v${targetVersion}${latestNote}`;
+13
View File
@@ -3,6 +3,9 @@
* Handles port number validation and default port resolution * Handles port number validation and default port resolution
*/ */
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
import type { UnifiedConfig } from '../../config/unified-config-types';
/** Default CLIProxy port */ /** Default CLIProxy port */
export const CLIPROXY_DEFAULT_PORT = 8317; export const CLIPROXY_DEFAULT_PORT = 8317;
@@ -57,3 +60,13 @@ export function normalizeProtocol(protocol: string | undefined): 'http' | 'https
// Invalid protocol (e.g., 'ftp') - default to http // Invalid protocol (e.g., 'ftp') - default to http
return 'http'; return 'http';
} }
/**
* Resolve the local CLIProxy lifecycle port from unified config.
* Falls back to default port when unset/invalid.
*/
export function resolveLifecyclePort(
config: Pick<UnifiedConfig, 'cliproxy_server'> = loadOrCreateUnifiedConfig()
): number {
return validatePort(config.cliproxy_server?.local?.port ?? CLIPROXY_DEFAULT_PORT);
}
@@ -11,7 +11,7 @@
import { initUI, header, color, dim, ok, warn, info } from '../../utils/ui'; import { initUI, header, color, dim, ok, warn, info } from '../../utils/ui';
import { getProxyStatus, startProxy, stopProxy } from '../../cliproxy/services'; import { getProxyStatus, startProxy, stopProxy } from '../../cliproxy/services';
import { detectRunningProxy } from '../../cliproxy/proxy/proxy-detector'; import { detectRunningProxy } from '../../cliproxy/proxy/proxy-detector';
import { resolveLifecyclePort } from './resolve-lifecycle-port'; import { resolveLifecyclePort } from '../../cliproxy/config/port-manager';
export async function handleStart(verbose = false): Promise<void> { export async function handleStart(verbose = false): Promise<void> {
await initUI(); await initUI();
@@ -1,15 +0,0 @@
import { CLIPROXY_DEFAULT_PORT, validatePort } from '../../cliproxy/config/port-manager';
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
import type { UnifiedConfig } from '../../config/unified-config-types';
type LifecyclePortConfig = Pick<UnifiedConfig, 'cliproxy_server'>;
/**
* Resolve the local CLIProxy lifecycle port from unified config.
* Falls back to default port when unset/invalid.
*/
export function resolveLifecyclePort(
config: LifecyclePortConfig = loadOrCreateUnifiedConfig()
): number {
return validatePort(config.cliproxy_server?.local?.port ?? CLIPROXY_DEFAULT_PORT);
}
+2 -2
View File
@@ -11,7 +11,7 @@ import open from 'open';
import { startServer } from '../web-server'; import { startServer } from '../web-server';
import { setupGracefulShutdown } from '../web-server/shutdown'; import { setupGracefulShutdown } from '../web-server/shutdown';
import { ensureCliproxyService } from '../cliproxy/service-manager'; import { ensureCliproxyService } from '../cliproxy/service-manager';
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/config-generator'; import { resolveLifecyclePort } from '../cliproxy/config/port-manager';
import { getDashboardAuthConfig } from '../config/unified-config-loader'; import { getDashboardAuthConfig } from '../config/unified-config-loader';
import { initUI, header, ok, info, warn, fail } from '../utils/ui'; import { initUI, header, ok, info, warn, fail } from '../utils/ui';
import { resolveNamedCommand, type NamedCommandRoute } from './named-command-router'; import { resolveNamedCommand, type NamedCommandRoute } from './named-command-router';
@@ -137,7 +137,7 @@ export async function handleConfigCommand(
// Ensure CLIProxy service is running for dashboard features // Ensure CLIProxy service is running for dashboard features
console.log(deps.info('Starting CLIProxy service...')); console.log(deps.info('Starting CLIProxy service...'));
const cliproxyResult = await deps.ensureCliproxyService(CLIPROXY_DEFAULT_PORT, verbose); const cliproxyResult = await deps.ensureCliproxyService(resolveLifecyclePort(), verbose);
logger.info('cliproxy.ensure_result', 'Config command checked CLIProxy availability', { logger.info('cliproxy.ensure_result', 'Config command checked CLIProxy availability', {
started: cliproxyResult.started, started: cliproxyResult.started,
alreadyRunning: cliproxyResult.alreadyRunning, alreadyRunning: cliproxyResult.alreadyRunning,
+2 -2
View File
@@ -10,7 +10,7 @@ import { CopilotConfig } from '../config/unified-config-types';
import { getGlobalEnvConfig } from '../config/unified-config-loader'; import { getGlobalEnvConfig } from '../config/unified-config-loader';
import { ensureCliproxyService } from '../cliproxy'; import { ensureCliproxyService } from '../cliproxy';
import { getEffectiveApiKey } from '../cliproxy/auth/auth-token-manager'; import { getEffectiveApiKey } from '../cliproxy/auth/auth-token-manager';
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-manager'; import { resolveLifecyclePort } from '../cliproxy/config/port-manager';
import { checkAuthStatus, isCopilotApiInstalled } from './copilot-auth'; import { checkAuthStatus, isCopilotApiInstalled } from './copilot-auth';
import { isDaemonRunning, startDaemon } from './copilot-daemon'; import { isDaemonRunning, startDaemon } from './copilot-daemon';
import { ensureCopilotApi } from './copilot-package-manager'; import { ensureCopilotApi } from './copilot-package-manager';
@@ -142,7 +142,7 @@ export async function resolveCopilotImageAnalysisEnv(
if (status.proxyReadiness === 'stopped') { if (status.proxyReadiness === 'stopped') {
const ensureServiceResult = await resolvedDeps.ensureCliproxyService( const ensureServiceResult = await resolvedDeps.ensureCliproxyService(
CLIPROXY_DEFAULT_PORT, resolveLifecyclePort(),
verbose verbose
); );
if (!ensureServiceResult.started) { if (!ensureServiceResult.started) {
+2 -12
View File
@@ -9,8 +9,7 @@
import http from 'http'; import http from 'http';
import { Request, Response, Router } from 'express'; import { Request, Response, Router } from 'express';
import { CLIPROXY_DEFAULT_PORT, validatePort } from '../../cliproxy/config/port-manager'; import { resolveLifecyclePort } from '../../cliproxy/config/port-manager';
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
import { requireLocalAccessWhenAuthDisabled } from '../middleware/auth-middleware'; import { requireLocalAccessWhenAuthDisabled } from '../middleware/auth-middleware';
export interface CliproxyLocalProxyDeps { export interface CliproxyLocalProxyDeps {
@@ -22,15 +21,6 @@ export interface CliproxyLocalProxyDeps {
/** Proxy request timeout in milliseconds (30 seconds) */ /** Proxy request timeout in milliseconds (30 seconds) */
const PROXY_TIMEOUT_MS = 30_000; const PROXY_TIMEOUT_MS = 30_000;
function resolveLocalCliproxyPort(): number {
try {
const config = loadOrCreateUnifiedConfig();
return validatePort(config.cliproxy_server?.local?.port ?? CLIPROXY_DEFAULT_PORT);
} catch {
return CLIPROXY_DEFAULT_PORT;
}
}
function isJsonContentType(contentType: string | string[] | undefined): boolean { function isJsonContentType(contentType: string | string[] | undefined): boolean {
const values = Array.isArray(contentType) ? contentType : [contentType]; const values = Array.isArray(contentType) ? contentType : [contentType];
return values.some((value) => value?.toLowerCase().includes('application/json') === true); return values.some((value) => value?.toLowerCase().includes('application/json') === true);
@@ -82,7 +72,7 @@ export function createCliproxyLocalProxyRouter(deps: CliproxyLocalProxyDeps = {}
'CLIProxy local proxy requires localhost access when dashboard auth is disabled.' 'CLIProxy local proxy requires localhost access when dashboard auth is disabled.'
)); ));
const createRequest = deps.request ?? http.request; const createRequest = deps.request ?? http.request;
const resolveTargetPort = deps.resolveTargetPort ?? resolveLocalCliproxyPort; const resolveTargetPort = deps.resolveTargetPort ?? resolveLifecyclePort;
router.use((req: Request, res: Response, next) => { router.use((req: Request, res: Response, next) => {
if (enforceAccess(req, res)) { if (enforceAccess(req, res)) {
@@ -52,7 +52,7 @@ import {
CLIPROXY_MAX_STABLE_VERSION, CLIPROXY_MAX_STABLE_VERSION,
CLIPROXY_FAULTY_RANGE, CLIPROXY_FAULTY_RANGE,
} from '../../cliproxy/binary/platform-detector'; } from '../../cliproxy/binary/platform-detector';
import { CLIPROXY_DEFAULT_PORT } from '../../cliproxy/config/port-manager'; import { resolveLifecyclePort } from '../../cliproxy/config/port-manager';
import { import {
MODEL_ENV_VAR_KEYS, MODEL_ENV_VAR_KEYS,
canonicalizeModelIdForProvider, canonicalizeModelIdForProvider,
@@ -321,7 +321,7 @@ router.get('/usage', handleStatsRequest);
*/ */
router.get('/status', async (_req: Request, res: Response): Promise<void> => { router.get('/status', async (_req: Request, res: Response): Promise<void> => {
try { try {
const running = await isCliproxyRunning(); const running = await isCliproxyRunning(resolveLifecyclePort());
res.json({ running }); res.json({ running });
} catch (error) { } catch (error) {
console.error(`[cliproxy-stats] ${(error as Error).message}`); console.error(`[cliproxy-stats] ${(error as Error).message}`);
@@ -345,15 +345,16 @@ router.get('/proxy-status', async (_req: Request, res: Response): Promise<void>
return; return;
} }
const port = resolveLifecyclePort();
// Session tracker says not running, but proxy might be running without session tracking // Session tracker says not running, but proxy might be running without session tracking
// (e.g., started before session persistence was implemented) // (e.g., started before session persistence was implemented)
const actuallyRunning = await isCliproxyRunning(); const actuallyRunning = await isCliproxyRunning(port);
if (actuallyRunning) { if (actuallyRunning) {
// Proxy running but no session lock - legacy/untracked instance // Proxy running but no session lock - legacy/untracked instance
res.json({ res.json({
running: true, running: true,
port: CLIPROXY_DEFAULT_PORT, port,
sessionCount: 0, // Unknown sessions sessionCount: 0, // Unknown sessions
// No pid/startedAt since we don't have session lock // No pid/startedAt since we don't have session lock
}); });
@@ -1,6 +1,5 @@
import { describe, expect, it } from 'bun:test'; import { describe, expect, it } from 'bun:test';
import { CLIPROXY_DEFAULT_PORT } from '../../../src/cliproxy/config/port-manager'; import { CLIPROXY_DEFAULT_PORT, resolveLifecyclePort } from '../../../src/cliproxy/config/port-manager';
import { resolveLifecyclePort } from '../../../src/commands/cliproxy/resolve-lifecycle-port';
describe('resolveLifecyclePort', () => { describe('resolveLifecyclePort', () => {
it('uses configured cliproxy_server.local.port', () => { it('uses configured cliproxy_server.local.port', () => {