mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 12:09:03 +00:00
- Block variant deletion when CLIProxy has active sessions - Use temp file + rename pattern for atomic settings writes - Prevents partial writes on crash/interruption
261 lines
7.7 KiB
TypeScript
261 lines
7.7 KiB
TypeScript
/**
|
|
* 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 { expandPath } from '../../utils/helpers';
|
|
import { getClaudeEnvVars, CLIPROXY_DEFAULT_PORT } from '../config-generator';
|
|
import { CLIProxyProvider } from '../types';
|
|
import { CompositeTierConfig } from '../../config/unified-config-types';
|
|
import { ensureProfileHooks } from '../../utils/websearch/profile-hook-injector';
|
|
import { ensureProfileHooks as ensureImageAnalyzerHooks } from '../../utils/hooks/image-analyzer-profile-hook-injector';
|
|
import { getEffectiveApiKey } from '../auth-token-manager';
|
|
import { warn } from '../../utils/ui';
|
|
|
|
/** 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,
|
|
port: number = CLIPROXY_DEFAULT_PORT
|
|
): SettingsEnv {
|
|
const baseEnv = getClaudeEnvVars(provider as CLIProxyProvider, 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 using temp file + rename.
|
|
* The renameSync is atomic on POSIX systems, preventing partial writes on crash.
|
|
*/
|
|
function writeSettings(filePath: string, settings: SettingsFile): void {
|
|
const tempPath = `${filePath}.tmp.${process.pid}`;
|
|
fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
fs.renameSync(tempPath, filePath);
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
port: number = CLIPROXY_DEFAULT_PORT
|
|
): string {
|
|
const ccsDir = getCcsDir();
|
|
const settingsPath = getSettingsFilePath(provider, name);
|
|
|
|
const settings: SettingsFile = {
|
|
env: buildSettingsEnv(provider, model, port),
|
|
};
|
|
|
|
ensureDir(ccsDir);
|
|
writeSettings(settingsPath, settings);
|
|
|
|
// Inject WebSearch hooks into variant settings
|
|
ensureProfileHooks(`${provider}-${name}`);
|
|
|
|
// Inject Image Analyzer hooks into variant settings
|
|
ensureImageAnalyzerHooks(`${provider}-${name}`);
|
|
|
|
return settingsPath;
|
|
}
|
|
|
|
/**
|
|
* Create settings.json file for CLIProxy variant (unified mode)
|
|
*/
|
|
export function createSettingsFileUnified(
|
|
name: string,
|
|
provider: CLIProxyProfileName,
|
|
model: string,
|
|
port: number = CLIPROXY_DEFAULT_PORT
|
|
): string {
|
|
const ccsDir = getCcsDir(); // Use centralized function for CCS_HOME support
|
|
const settingsPath = path.join(ccsDir, getSettingsFileName(provider, name));
|
|
|
|
const settings: SettingsFile = {
|
|
env: buildSettingsEnv(provider, model, port),
|
|
};
|
|
|
|
ensureDir(ccsDir);
|
|
writeSettings(settingsPath, settings);
|
|
|
|
// Inject WebSearch hooks into variant settings
|
|
ensureProfileHooks(`${provider}-${name}`);
|
|
|
|
// Inject Image Analyzer hooks into variant settings
|
|
ensureImageAnalyzerHooks(`${provider}-${name}`);
|
|
|
|
return settingsPath;
|
|
}
|
|
|
|
/**
|
|
* Build settings env object for a composite variant.
|
|
* Uses root URL (no /api/provider/ path) for model-based routing.
|
|
*/
|
|
function buildCompositeSettingsEnv(
|
|
tiers: { opus: CompositeTierConfig; sonnet: CompositeTierConfig; haiku: CompositeTierConfig },
|
|
defaultTier: 'opus' | 'sonnet' | 'haiku',
|
|
port: number = CLIPROXY_DEFAULT_PORT
|
|
): SettingsEnv {
|
|
const defaultModel = tiers[defaultTier].model;
|
|
|
|
return {
|
|
// Root URL — CLIProxyAPI routes based on model name, no provider prefix
|
|
ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`,
|
|
ANTHROPIC_AUTH_TOKEN: getEffectiveApiKey(),
|
|
ANTHROPIC_MODEL: defaultModel,
|
|
ANTHROPIC_DEFAULT_OPUS_MODEL: tiers.opus.model,
|
|
ANTHROPIC_DEFAULT_SONNET_MODEL: tiers.sonnet.model,
|
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: tiers.haiku.model,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create settings.json file for a composite variant.
|
|
*/
|
|
export function createCompositeSettingsFile(
|
|
name: string,
|
|
tiers: { opus: CompositeTierConfig; sonnet: CompositeTierConfig; haiku: CompositeTierConfig },
|
|
defaultTier: 'opus' | 'sonnet' | 'haiku',
|
|
port: number = CLIPROXY_DEFAULT_PORT
|
|
): string {
|
|
const ccsDir = getCcsDir();
|
|
const settingsPath = path.join(ccsDir, `composite-${name}.settings.json`);
|
|
|
|
const settings: SettingsFile = {
|
|
env: buildCompositeSettingsEnv(tiers, defaultTier, port),
|
|
};
|
|
|
|
ensureDir(ccsDir);
|
|
writeSettings(settingsPath, settings);
|
|
|
|
// Inject WebSearch hooks into variant settings
|
|
ensureProfileHooks(`composite-${name}`);
|
|
|
|
// Inject Image Analyzer hooks into variant settings
|
|
ensureImageAnalyzerHooks(`composite-${name}`);
|
|
|
|
return settingsPath;
|
|
}
|
|
|
|
/**
|
|
* Get relative settings path for a composite variant
|
|
*/
|
|
export function getCompositeRelativeSettingsPath(name: string): string {
|
|
return `~/.ccs/composite-${name}.settings.json`;
|
|
}
|
|
|
|
/**
|
|
* Delete settings file if it exists.
|
|
* Uses expandPath() for cross-platform path handling.
|
|
*/
|
|
export function deleteSettingsFile(settingsPath: string): boolean {
|
|
const resolvedPath = expandPath(settingsPath);
|
|
if (fs.existsSync(resolvedPath)) {
|
|
fs.unlinkSync(resolvedPath);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Update model in an existing settings file
|
|
*/
|
|
export function updateSettingsModel(settingsPath: string, model: string): void {
|
|
const fileName = path.basename(settingsPath);
|
|
if (fileName.startsWith('composite-')) {
|
|
console.log(
|
|
warn('Cannot update model for composite variant. Edit config.yaml tiers directly.')
|
|
);
|
|
return;
|
|
}
|
|
|
|
const resolvedPath = settingsPath.replace(/^~/, os.homedir());
|
|
if (!fs.existsSync(resolvedPath)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const content = fs.readFileSync(resolvedPath, 'utf8');
|
|
const settings = JSON.parse(content) as SettingsFile;
|
|
|
|
if (model) {
|
|
settings.env = settings.env || ({} as SettingsEnv);
|
|
settings.env.ANTHROPIC_MODEL = model;
|
|
settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL = model;
|
|
settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL = model;
|
|
} else {
|
|
// Clear model settings to use defaults
|
|
delete (settings.env as unknown as Record<string, string>).ANTHROPIC_MODEL;
|
|
}
|
|
|
|
const tempPath = `${resolvedPath}.tmp.${process.pid}`;
|
|
fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
fs.renameSync(tempPath, resolvedPath);
|
|
} catch {
|
|
// Ignore errors - settings file may be invalid
|
|
}
|
|
}
|