refactor(cliproxy): extract service layer from cliproxy-command

- create cliproxy/services/ with 6 modules (1179 → 634 lines)

- variant-service, variant-settings, variant-config-adapter

- proxy-lifecycle-service, binary-service

- separate CLI parsing from business logic

Phase 5: Command Refactor (cliproxy-command.ts)
This commit is contained in:
kaitranntt
2025-12-19 11:48:45 -05:00
parent ecb6bb448a
commit b49b7d17b2
8 changed files with 1000 additions and 812 deletions
+176
View File
@@ -0,0 +1,176 @@
/**
* CLIProxy Binary Service
*
* Handles CLIProxyAPI binary version management:
* - Get current version status
* - Install specific versions
* - Install latest version
* - Version pinning
*/
import {
getInstalledCliproxyVersion,
installCliproxyVersion,
fetchLatestCliproxyVersion,
isCLIProxyInstalled,
getCLIProxyPath,
getPinnedVersion,
savePinnedVersion,
clearPinnedVersion,
isVersionPinned,
} from '../binary-manager';
import { CLIPROXY_FALLBACK_VERSION } from '../platform-detector';
/** Binary status result */
export interface BinaryStatusResult {
installed: boolean;
currentVersion: string | null;
pinnedVersion: string | null;
binaryPath: string;
fallbackVersion: string;
}
/** Install result */
export interface InstallResult {
success: boolean;
version: string;
error?: string;
wasPinned?: boolean;
}
/** Latest version check result */
export interface LatestVersionResult {
success: boolean;
latestVersion?: string;
currentVersion?: string;
updateAvailable?: boolean;
error?: string;
}
/**
* Get current binary status
*/
export function getBinaryStatus(): BinaryStatusResult {
return {
installed: isCLIProxyInstalled(),
currentVersion: getInstalledCliproxyVersion(),
pinnedVersion: getPinnedVersion(),
binaryPath: getCLIProxyPath(),
fallbackVersion: CLIPROXY_FALLBACK_VERSION,
};
}
/**
* Check for latest version
*/
export async function checkLatestVersion(): Promise<LatestVersionResult> {
try {
const latestVersion = await fetchLatestCliproxyVersion();
const currentVersion = getInstalledCliproxyVersion();
const updateAvailable = latestVersion !== currentVersion;
return {
success: true,
latestVersion,
currentVersion: currentVersion || undefined,
updateAvailable,
};
} catch (error) {
return {
success: false,
error: (error as Error).message,
};
}
}
/**
* Validate version format
*/
export function isValidVersionFormat(version: string): boolean {
return /^\d+\.\d+\.\d+$/.test(version);
}
/**
* Install a specific version and pin it
*/
export async function installVersion(version: string, verbose = false): Promise<InstallResult> {
if (!isValidVersionFormat(version)) {
return {
success: false,
version,
error: 'Invalid version format. Expected format: X.Y.Z (e.g., 6.5.53)',
};
}
try {
await installCliproxyVersion(version, verbose);
savePinnedVersion(version);
return {
success: true,
version,
wasPinned: true,
};
} catch (error) {
return {
success: false,
version,
error: (error as Error).message,
};
}
}
/**
* Install latest version and clear any pin
*/
export async function installLatest(verbose = false): Promise<InstallResult> {
try {
const latestVersion = await fetchLatestCliproxyVersion();
const currentVersion = getInstalledCliproxyVersion();
const wasPinned = isVersionPinned();
if (isCLIProxyInstalled() && latestVersion === currentVersion && !wasPinned) {
return {
success: true,
version: latestVersion,
error: `Already running latest version: v${latestVersion}`,
};
}
await installCliproxyVersion(latestVersion, verbose);
clearPinnedVersion();
return {
success: true,
version: latestVersion,
wasPinned,
};
} catch (error) {
return {
success: false,
version: '',
error: (error as Error).message,
};
}
}
/**
* Check if a version is pinned
*/
export function isPinned(): boolean {
return isVersionPinned();
}
/**
* Get pinned version if any
*/
export function getPinned(): string | null {
return getPinnedVersion();
}
/**
* Clear version pin
*/
export function clearPin(): void {
clearPinnedVersion();
}
+40
View File
@@ -0,0 +1,40 @@
/**
* CLIProxy Services
* Central export point for cliproxy service layer
*/
// Variant management
export {
validateProfileName,
variantExists,
listVariants,
createVariant,
removeVariant,
type VariantConfig,
type VariantOperationResult,
} from './variant-service';
// Proxy lifecycle
export {
getProxyStatus,
stopProxy,
isProxyRunning,
getActiveSessionCount,
type ProxyStatusResult,
type StopProxyResult,
} from './proxy-lifecycle-service';
// Binary management
export {
getBinaryStatus,
checkLatestVersion,
isValidVersionFormat,
installVersion,
installLatest,
isPinned,
getPinned,
clearPin,
type BinaryStatusResult,
type InstallResult,
type LatestVersionResult,
} from './binary-service';
@@ -0,0 +1,58 @@
/**
* CLIProxy Proxy Lifecycle Service
*
* Handles start/stop/status operations for CLIProxy instances.
* Delegates to session-tracker for actual process management.
*/
import {
stopProxy as stopProxySession,
getProxyStatus as getProxyStatusSession,
} from '../session-tracker';
/** Proxy status result */
export interface ProxyStatusResult {
running: boolean;
port?: number;
pid?: number;
sessionCount?: number;
startedAt?: string;
}
/** Stop proxy result */
export interface StopProxyResult {
stopped: boolean;
pid?: number;
sessionCount?: number;
error?: string;
}
/**
* Get current proxy status
*/
export function getProxyStatus(): ProxyStatusResult {
return getProxyStatusSession();
}
/**
* Stop the running CLIProxy instance
*/
export async function stopProxy(): Promise<StopProxyResult> {
return stopProxySession();
}
/**
* Check if proxy is currently running
*/
export function isProxyRunning(): boolean {
const status = getProxyStatusSession();
return status.running;
}
/**
* Get active session count
*/
export function getActiveSessionCount(): number {
const status = getProxyStatusSession();
return status.sessionCount ?? 0;
}
@@ -0,0 +1,182 @@
/**
* CLIProxy Variant Config Adapters
*
* Handles reading/writing variant config in both unified and legacy formats.
*/
import * as fs from 'fs';
import { getConfigPath, loadConfig } from '../../utils/config-manager';
import { CLIProxyProvider } from '../types';
import {
loadOrCreateUnifiedConfig,
saveUnifiedConfig,
isUnifiedMode,
} from '../../config/unified-config-loader';
/** Variant configuration structure */
export interface VariantConfig {
provider: string;
settings?: string;
account?: string;
model?: string;
}
/**
* Check if variant exists in config
*/
export function variantExistsInConfig(name: string): boolean {
try {
if (isUnifiedMode()) {
const config = loadOrCreateUnifiedConfig();
return !!(config.cliproxy?.variants && name in config.cliproxy.variants);
}
const config = loadConfig();
return !!(config.cliproxy && name in config.cliproxy);
} catch {
return false;
}
}
/**
* List variants from config
*/
export function listVariantsFromConfig(): Record<string, VariantConfig> {
try {
if (isUnifiedMode()) {
const unifiedConfig = loadOrCreateUnifiedConfig();
const variants = unifiedConfig.cliproxy?.variants || {};
const result: Record<string, VariantConfig> = {};
for (const name of Object.keys(variants)) {
const v = variants[name];
result[name] = { provider: v.provider, settings: v.settings, account: v.account };
}
return result;
}
const config = loadConfig();
const variants = config.cliproxy || {};
const result: Record<string, VariantConfig> = {};
for (const name of Object.keys(variants)) {
const v = variants[name] as { provider: string; settings: string; account?: string };
result[name] = { provider: v.provider, settings: v.settings, account: v.account };
}
return result;
} catch {
return {};
}
}
/**
* Save variant to unified config
*/
export function saveVariantUnified(
name: string,
provider: CLIProxyProvider,
settingsPath: string,
account?: string
): void {
const config = loadOrCreateUnifiedConfig();
if (!config.cliproxy) {
config.cliproxy = {
oauth_accounts: {},
providers: ['gemini', 'codex', 'agy', 'qwen', 'iflow'],
variants: {},
};
}
if (!config.cliproxy.variants) {
config.cliproxy.variants = {};
}
config.cliproxy.variants[name] = {
provider,
account,
settings: settingsPath,
};
saveUnifiedConfig(config);
}
/**
* Save variant to legacy JSON config
*/
export function saveVariantLegacy(
name: string,
provider: string,
settingsPath: string,
account?: string
): void {
const configPath = getConfigPath();
let config: { profiles: Record<string, string>; cliproxy?: Record<string, unknown> };
try {
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
} catch {
config = { profiles: {} };
}
if (!config.cliproxy) {
config.cliproxy = {};
}
const variantConfig: { provider: string; settings: string; account?: string } = {
provider,
settings: settingsPath,
};
if (account) {
variantConfig.account = account;
}
config.cliproxy[name] = variantConfig;
const tempPath = configPath + '.tmp';
fs.writeFileSync(tempPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
fs.renameSync(tempPath, configPath);
}
/**
* Remove variant from unified config
*/
export function removeVariantFromUnifiedConfig(name: string): VariantConfig | null {
const config = loadOrCreateUnifiedConfig();
if (!config.cliproxy?.variants || !(name in config.cliproxy.variants)) {
return null;
}
const variant = config.cliproxy.variants[name];
delete config.cliproxy.variants[name];
saveUnifiedConfig(config);
return { provider: variant.provider, settings: variant.settings };
}
/**
* Remove variant from legacy JSON config
*/
export function removeVariantFromLegacyConfig(name: string): VariantConfig | null {
const configPath = getConfigPath();
let config: { profiles: Record<string, string>; cliproxy?: Record<string, unknown> };
try {
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
} catch {
return null;
}
if (!config.cliproxy || !(name in config.cliproxy)) {
return null;
}
const variant = config.cliproxy[name] as { provider: string; settings: string };
delete config.cliproxy[name];
if (Object.keys(config.cliproxy).length === 0) {
delete config.cliproxy;
}
const tempPath = configPath + '.tmp';
fs.writeFileSync(tempPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
fs.renameSync(tempPath, configPath);
return variant;
}
+139
View File
@@ -0,0 +1,139 @@
/**
* CLIProxy Variant Service
*
* Handles CRUD operations for CLIProxy variant profiles.
* Supports both unified config (config.yaml) and legacy JSON format.
*/
import * as path from 'path';
import { CLIProxyProfileName } from '../../auth/profile-detector';
import { CLIProxyProvider } from '../types';
import { isReservedName } from '../../config/reserved-names';
import { isUnifiedMode } from '../../config/unified-config-loader';
import {
createSettingsFile,
createSettingsFileUnified,
deleteSettingsFile,
getRelativeSettingsPath,
} from './variant-settings';
import {
VariantConfig,
variantExistsInConfig,
listVariantsFromConfig,
saveVariantUnified,
saveVariantLegacy,
removeVariantFromUnifiedConfig,
removeVariantFromLegacyConfig,
} from './variant-config-adapter';
// Re-export VariantConfig from adapter
export type { VariantConfig } from './variant-config-adapter';
/** Result of variant operations */
export interface VariantOperationResult {
success: boolean;
error?: string;
variant?: VariantConfig;
settingsPath?: string;
}
/**
* Validate CLIProxy profile name
*/
export function validateProfileName(name: string): string | null {
if (!name) {
return 'Profile name is required';
}
if (!/^[a-zA-Z][a-zA-Z0-9._-]*$/.test(name)) {
return 'Name must start with letter, contain only letters, numbers, dot, dash, underscore';
}
if (name.length > 32) {
return 'Name must be 32 characters or less';
}
if (isReservedName(name)) {
return `'${name}' is a reserved name`;
}
return null;
}
/**
* Check if CLIProxy variant profile exists
*/
export function variantExists(name: string): boolean {
return variantExistsInConfig(name);
}
/**
* List all CLIProxy variants
*/
export function listVariants(): Record<string, VariantConfig> {
return listVariantsFromConfig();
}
/**
* Create a new CLIProxy variant
*/
export function createVariant(
name: string,
provider: CLIProxyProfileName,
model: string,
account?: string
): VariantOperationResult {
try {
let settingsPath: string;
if (isUnifiedMode()) {
settingsPath = createSettingsFileUnified(name, provider, model);
saveVariantUnified(
name,
provider as CLIProxyProvider,
getRelativeSettingsPath(provider, name),
account
);
} else {
settingsPath = createSettingsFile(name, provider, model);
saveVariantLegacy(name, provider, `~/.ccs/${path.basename(settingsPath)}`, account);
}
return {
success: true,
settingsPath,
variant: { provider, model, account },
};
} catch (error) {
return {
success: false,
error: (error as Error).message,
};
}
}
/**
* Remove a CLIProxy variant
*/
export function removeVariant(name: string): VariantOperationResult {
try {
let variant: VariantConfig | null;
if (isUnifiedMode()) {
const unifiedVariant = removeVariantFromUnifiedConfig(name);
if (unifiedVariant?.settings) {
deleteSettingsFile(unifiedVariant.settings);
}
variant = unifiedVariant;
} else {
variant = removeVariantFromLegacyConfig(name);
if (variant?.settings) {
deleteSettingsFile(variant.settings);
}
}
if (!variant) {
return { success: false, error: `Variant '${name}' not found` };
}
return { success: true, variant };
} catch (error) {
return { success: false, error: (error as Error).message };
}
}
+136
View File
@@ -0,0 +1,136 @@
/**
* CLIProxy Variant Settings File Manager
*
* Handles creation of settings.json files for CLIProxy variants.
* These files contain environment variables for Claude CLI integration.
*/
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { CLIProxyProfileName } from '../../auth/profile-detector';
import { getCcsDir } from '../../utils/config-manager';
import { getClaudeEnvVars, CLIPROXY_DEFAULT_PORT } from '../config-generator';
import { CLIProxyProvider } from '../types';
/** Environment settings structure */
interface SettingsEnv {
ANTHROPIC_BASE_URL: string;
ANTHROPIC_AUTH_TOKEN: string;
ANTHROPIC_MODEL: string;
ANTHROPIC_DEFAULT_OPUS_MODEL: string;
ANTHROPIC_DEFAULT_SONNET_MODEL: string;
ANTHROPIC_DEFAULT_HAIKU_MODEL: string;
}
interface SettingsFile {
env: SettingsEnv;
}
/**
* Build settings env object for a variant
*/
function buildSettingsEnv(provider: CLIProxyProfileName, model: string): SettingsEnv {
const baseEnv = getClaudeEnvVars(provider as CLIProxyProvider, CLIPROXY_DEFAULT_PORT);
return {
ANTHROPIC_BASE_URL: baseEnv.ANTHROPIC_BASE_URL || '',
ANTHROPIC_AUTH_TOKEN: baseEnv.ANTHROPIC_AUTH_TOKEN || '',
ANTHROPIC_MODEL: model,
ANTHROPIC_DEFAULT_OPUS_MODEL: model,
ANTHROPIC_DEFAULT_SONNET_MODEL: model,
ANTHROPIC_DEFAULT_HAIKU_MODEL: baseEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL || model,
};
}
/**
* Ensure directory exists
*/
function ensureDir(dir: string): void {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
/**
* Write settings file atomically
*/
function writeSettings(filePath: string, settings: SettingsFile): void {
fs.writeFileSync(filePath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
}
/**
* Get settings file path for a variant
*/
export function getSettingsFilePath(provider: CLIProxyProfileName, name: string): string {
const ccsDir = getCcsDir();
return path.join(ccsDir, `${provider}-${name}.settings.json`);
}
/**
* Get settings file name (without path)
*/
export function getSettingsFileName(provider: CLIProxyProfileName, name: string): string {
return `${provider}-${name}.settings.json`;
}
/**
* Get relative settings path with ~ prefix
*/
export function getRelativeSettingsPath(provider: CLIProxyProfileName, name: string): string {
return `~/.ccs/${getSettingsFileName(provider, name)}`;
}
/**
* Create settings.json file for CLIProxy variant (legacy mode)
*/
export function createSettingsFile(
name: string,
provider: CLIProxyProfileName,
model: string
): string {
const ccsDir = getCcsDir();
const settingsPath = getSettingsFilePath(provider, name);
const settings: SettingsFile = {
env: buildSettingsEnv(provider, model),
};
ensureDir(ccsDir);
writeSettings(settingsPath, settings);
return settingsPath;
}
/**
* Create settings.json file for CLIProxy variant (unified mode)
*/
export function createSettingsFileUnified(
name: string,
provider: CLIProxyProfileName,
model: string
): string {
const ccsDir = path.join(os.homedir(), '.ccs');
const settingsPath = path.join(ccsDir, getSettingsFileName(provider, name));
const settings: SettingsFile = {
env: buildSettingsEnv(provider, model),
};
ensureDir(ccsDir);
writeSettings(settingsPath, settings);
return settingsPath;
}
/**
* Delete settings file if it exists
*/
export function deleteSettingsFile(settingsPath: string): boolean {
const resolvedPath = settingsPath.replace(/^~/, os.homedir());
if (fs.existsSync(resolvedPath)) {
fs.unlinkSync(resolvedPath);
return true;
}
return false;
}
File diff suppressed because it is too large Load Diff
+10
View File
@@ -449,6 +449,16 @@ export function updateUnifiedConfig(updates: Partial<UnifiedConfig>): UnifiedCon
return updated;
}
/**
* Check if unified config mode is active.
* Returns true if config.yaml exists OR CCS_UNIFIED_CONFIG=1.
*
* Use this centralized function instead of duplicating the logic.
*/
export function isUnifiedMode(): boolean {
return hasUnifiedConfig() || isUnifiedConfigEnabled();
}
/**
* Get or set default profile name.
*/