claude-code-router/packages/core/src/profiles/service.ts
2026-07-05 19:57:39 +08:00

1597 lines
60 KiB
TypeScript

import { randomBytes } from "node:crypto";
import { chmodSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV, NO_AVAILABLE_GATEWAY_MODELS_MESSAGE, enforceSingleEnabledGlobalProfilePerAgent, hasAvailableGatewayModels, type ApiKeyConfig, type AppConfig, type ProfileApplyResult, type ProfileClientApplyStatus, type ProfileClientKind, type ProfileConfig } from "@ccr/core/contracts/app";
import { replacePersistedApiKeys } from "@ccr/core/config/api-key-store";
import { botGatewayProfileEnv } from "@ccr/core/agents/bot-gateway/env";
import { claudeCodeUtcTimezoneEnvOverride } from "@ccr/core/agents/claude-code/environment";
import { writeCodexCompatibleAppModelCatalog } from "@ccr/core/agents/codex/app-launch";
import { codexCliMiddlewareRuntimeScript } from "@ccr/core/agents/codex/cli-middleware-runtime";
import { codexModelCatalogJson } from "@ccr/core/agents/codex/model-catalog";
import { CONFIGDIR } from "@ccr/core/config/constants";
import { resolveZcodeConfigFile, writeZcodeGatewayConfig, zcodeHomeFromConfigFile } from "@ccr/core/agents/zcode/profile-config";
import { normalizeRouteSelector } from "@ccr/core/gateway/claude-code-router-plugin";
const managedRootStart = "# BEGIN CCR managed profile";
const managedRootEnd = "# END CCR managed profile";
const managedProviderStart = "# BEGIN CCR managed Codex provider";
const managedProviderEnd = "# END CCR managed Codex provider";
const originalBackupSuffix = ".ccr-original";
const originalMissingSuffix = ".ccr-original-missing";
const fallbackClientToken = "ccr-local";
const privateDirMode = 0o700;
const privateExecutableMode = 0o700;
const privateFileMode = 0o600;
const publicExecutableMode = 0o755;
export async function applyProfileConfig(config: AppConfig): Promise<ProfileApplyResult> {
cleanupGeneratedBinBackups();
const appliedAt = new Date().toISOString();
const profiles = profileEntries(config);
const result: ProfileApplyResult = {
appliedAt,
clients: [],
enabled: profiles.some((profile) => profile.enabled)
};
if (!result.enabled) {
result.clients = profiles.map(disabledProfileStatus);
return result;
}
if (!hasAvailableGatewayModels(config)) {
result.clients = profiles.map((profile) =>
profile.enabled
? unavailableModelStatus(profile, profilePath(profile))
: disabledProfileStatus(profile)
);
return result;
}
const profileApiKeys = await ensureProfileApiKeys(config, profiles);
for (const profile of profiles) {
const token = profileApiKeys.get(profile.id) ?? fallbackClientToken;
result.clients.push(
profile.agent === "claude-code"
? applyClaudeCodeProfile(config, profile, token, appliedAt)
: profile.agent === "zcode"
? applyZcodeProfile(config, profile, token, appliedAt)
: applyCodexProfile(config, profile, token, appliedAt)
);
}
result.clients.push(...restoreInactiveGlobalProfileConfigs(profiles));
return result;
}
export function applyProfileRuntimeConfig(config: AppConfig, profile: ProfileConfig, token: string): ProfileClientApplyStatus {
cleanupGeneratedBinBackups();
const appliedAt = new Date().toISOString();
return profile.agent === "claude-code"
? applyClaudeCodeProfile(config, profile, token, appliedAt)
: profile.agent === "zcode"
? applyZcodeProfile(config, profile, token, appliedAt)
: applyCodexProfile(config, profile, token, appliedAt);
}
function applyClaudeCodeProfile(config: AppConfig, profile: ProfileConfig, token: string, appliedAt: string): ProfileClientApplyStatus {
const settingsFile = resolveClaudeCodeSettingsFile(profile);
if (!profile.enabled) {
return restoreDisabledGlobalProfile(profile, settingsFile, "Claude Code profile is disabled.", isManagedClaudeCodeSettingsContent);
}
try {
const endpoint = gatewayEndpoint(config);
const settings = readJsonObject(settingsFile);
const env = {
...withoutBotGatewayEnv(Object.fromEntries(stringRecord(settings.env))),
...profileEnv(profile)
};
env.ANTHROPIC_BASE_URL = endpoint;
env.ANTHROPIC_API_BASE_URL = endpoint;
env.CLAUDE_AGENT_API_BASE_URL = endpoint;
delete env.ANTHROPIC_AUTH_TOKEN;
delete env.ANTHROPIC_API_KEY;
if (profile.model.trim()) {
const model = normalizeClientModel(profile.model);
env.ANTHROPIC_MODEL = model;
env.CCR_CLAUDE_CODE_MODEL = model;
env.CODEXL_CLAUDE_CODE_MODEL = model;
} else {
delete env.ANTHROPIC_MODEL;
delete env.CCR_CLAUDE_CODE_MODEL;
delete env.CODEXL_CLAUDE_CODE_MODEL;
}
if (profile.smallFastModel?.trim()) {
env.ANTHROPIC_SMALL_FAST_MODEL = normalizeClientModel(profile.smallFastModel);
} else {
delete env.ANTHROPIC_SMALL_FAST_MODEL;
}
Object.assign(env, claudeCodeUtcTimezoneEnvOverride());
const helperResult = writeClaudeCodeApiKeyHelper(profile, token);
const wrapperResult = writeClaudeCodeWrapper(config, profile, helperResult.file);
const nextSettings = {
...settings,
apiKeyHelper: process.platform === "win32" ? `"${helperResult.file}"` : helperResult.file,
env
};
const writeResult = writeFileWithBackup(settingsFile, `${JSON.stringify(nextSettings, null, 2)}\n`, { mode: privateFileMode });
const changed = writeResult.changed || helperResult.changed || wrapperResult.changed;
return {
appliedAt,
backupFile: writeResult.backupFile ?? helperResult.backupFile ?? wrapperResult.backupFile,
client: "claude-code",
enabled: true,
message: changed
? `Claude Code settings are managed by CCR (wrapper ${wrapperResult.file}).`
: "Claude Code settings already match CCR.",
ok: true,
path: settingsFile
};
} catch (error) {
return {
client: "claude-code",
enabled: true,
message: formatError(error),
ok: false,
path: settingsFile
};
}
}
function applyCodexProfile(config: AppConfig, profile: ProfileConfig, token: string, appliedAt: string): ProfileClientApplyStatus {
const clientName = codexCompatibleClientName(profile.agent);
const configFile = resolveCodexConfigFile(profile);
if (!profile.enabled) {
return restoreDisabledGlobalProfile(
profile,
configFile,
`${clientName} profile is disabled.`,
(content) => isManagedCodexConfigContent(content, sanitizeCodexProviderId(profile.providerId || "") || "claude-code-router")
);
}
try {
const endpoint = `${gatewayEndpoint(config).replace(/\/+$/g, "")}/v1`;
const providerId = sanitizeCodexProviderId(profile.providerId || "") || "claude-code-router";
const providerName = profile.providerName?.trim() || "Claude Code Router";
const model = normalizeClientModel(profile.model) || defaultClientModel(config);
const source = existsSync(configFile) ? readFileSync(configFile, "utf8") : "";
const configFormat = normalizeCodexConfigFormat(profile.configFormat);
const modelCatalogFile = codexModelCatalogFile(configFile);
const modelCatalogResult = writeFileWithBackup(modelCatalogFile, codexModelCatalogJson(config, model));
const appModelCatalogResult = writeCodexCompatibleAppModelCatalog(CONFIGDIR, { ...profile, model }, config);
const showAllSessions = profile.agent === "zcode" ? false : Boolean(profile.showAllSessions);
const nextConfig = buildCodexConfigToml(source, {
baseUrl: endpoint,
modelCatalogFile,
configFormat,
model,
providerId,
providerName,
showAllSessions,
token
});
const writeResult = writeFileWithBackup(configFile, nextConfig, { mode: privateFileMode });
const separateProfileResult = maybeWriteSeparateCodexProfileFile(configFile, source, {
configFormat,
model,
providerId,
showAllSessions
});
const middlewareResult = profile.cliMiddleware
? writeCodexCliMiddleware(config, profile, {
configFormat,
configFile,
modelCatalogFile,
model,
providerId
})
: undefined;
const changed = writeResult.changed ||
modelCatalogResult.changed ||
appModelCatalogResult.changed ||
Boolean(separateProfileResult?.changed) ||
Boolean(middlewareResult?.changed);
const extras = [
modelCatalogFile ? `catalog ${modelCatalogFile}` : "",
appModelCatalogResult.file ? `app catalog ${appModelCatalogResult.file}` : "",
separateProfileResult?.file ? `profile ${separateProfileResult.file}` : "",
middlewareResult?.file ? `middleware ${middlewareResult.file}` : ""
].filter(Boolean);
return {
appliedAt,
backupFile: writeResult.backupFile,
client: profile.agent,
enabled: true,
message: changed
? `${clientName} config is managed by CCR${extras.length ? ` (${extras.join(", ")})` : ""}.`
: `${clientName} config already matches CCR.`,
ok: true,
path: configFile
};
} catch (error) {
return {
client: profile.agent,
enabled: true,
message: formatError(error),
ok: false,
path: configFile
};
}
}
function applyZcodeProfile(config: AppConfig, profile: ProfileConfig, token: string, appliedAt: string): ProfileClientApplyStatus {
const configFile = resolveZcodeConfigFile(profile);
if (!profile.enabled) {
return restoreDisabledZcodeProfile(profile, configFile);
}
try {
const providerId = sanitizeCodexProviderId(profile.providerId || "") || "claude-code-router";
const model = normalizeClientModel(profile.model) || defaultClientModel(config);
const configResult = writeZcodeGatewayConfig(config, profile, token, { backup: true });
const middlewareResult = profile.cliMiddleware
? writeCodexCliMiddleware(config, profile, {
configFile,
configFormat: normalizeCodexConfigFormat(profile.configFormat),
model,
modelCatalogFile: zcodeMiddlewareModelCatalogFile(configFile),
providerId
})
: undefined;
const changed = configResult.changed || Boolean(middlewareResult?.changed);
const extras = [
middlewareResult?.file ? `middleware ${middlewareResult.file}` : ""
].filter(Boolean);
return {
appliedAt,
backupFile: configResult.backupFile,
client: "zcode",
enabled: true,
message: changed
? `ZCode config is managed by CCR${extras.length ? ` (${extras.join(", ")})` : ""}.`
: "ZCode config already matches CCR.",
ok: true,
path: configResult.file
};
} catch (error) {
return {
client: "zcode",
enabled: true,
message: formatError(error),
ok: false,
path: configFile
};
}
}
function profileEntries(config: AppConfig): ProfileConfig[] {
return enforceSingleEnabledGlobalProfilePerAgent(config.profile.profiles);
}
async function ensureProfileApiKeys(config: AppConfig, profiles: ProfileConfig[]): Promise<Map<string, string>> {
const apiKeys = [...(Array.isArray(config.APIKEYS) ? config.APIKEYS : [])];
const byId = new Map(apiKeys.map((apiKey, index) => [apiKey.id || `key-${index + 1}`, { apiKey, index }]));
const tokens = new Map<string, string>();
let changed = false;
for (const profile of profiles) {
const id = profileApiKeyId(profile);
const name = profileApiKeyName(profile);
const existing = byId.get(id);
if (existing?.apiKey.key.trim()) {
tokens.set(profile.id, existing.apiKey.key.trim());
if (existing.apiKey.name !== name) {
apiKeys[existing.index] = {
...existing.apiKey,
name
};
changed = true;
}
continue;
}
const apiKey: ApiKeyConfig = {
createdAt: new Date().toISOString(),
id,
key: generateProfileApiKey(),
name
};
apiKeys.push(apiKey);
byId.set(id, { apiKey, index: apiKeys.length - 1 });
tokens.set(profile.id, apiKey.key);
changed = true;
}
if (changed) {
config.APIKEYS = await replacePersistedApiKeys(apiKeys);
config.APIKEY = config.APIKEYS[0]?.key ?? "";
}
return tokens;
}
function profileApiKeyId(profile: ProfileConfig): string {
return `profile:${sanitizeProfilePathSegment(profile.id || profile.name || profile.agent) || "profile"}`;
}
function profileApiKeyName(profile: ProfileConfig): string {
return `Profile: ${profile.name?.trim() || profile.id || profile.agent}`;
}
function generateProfileApiKey(): string {
return `ccr-profile-${randomBase64Url(24)}`;
}
function randomBase64Url(byteLength: number): string {
return randomBytes(byteLength).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
}
function profilePath(profile: ProfileConfig): string {
return profile.agent === "claude-code"
? resolveClaudeCodeSettingsFile(profile)
: resolveCodexConfigFile(profile);
}
function resolveClaudeCodeSettingsFile(profile: ProfileConfig): string {
if (isGeneratedProfileScope(profile.scope)) {
return path.join(ccrManagedProfileDir(profile), "claude", "settings.json");
}
return resolveUserPath(profile.settingsFile || "~/.claude/settings.json");
}
function resolveCodexConfigFile(profile: ProfileConfig): string {
if (profile.agent === "zcode") {
return resolveZcodeConfigFile(profile);
}
if (isGeneratedProfileScope(profile.scope)) {
return path.join(ccrManagedProfileDir(profile), codexConfigSubdir(profile.agent), "config.toml");
}
const codexHome = profile.codexHome?.trim();
if (codexHome) {
return path.join(resolveUserPath(codexHome), "config.toml");
}
return resolveUserPath(profile.configFile || defaultCodexConfigFile(profile.agent));
}
function codexModelCatalogFile(configFile: string): string {
return path.join(path.dirname(configFile), "ccr-model-catalog.json");
}
function zcodeMiddlewareModelCatalogFile(configFile: string): string {
return path.join(path.dirname(configFile), "ccr-zcode-middleware-model-catalog.json");
}
function ccrManagedProfileDir(profile: ProfileConfig): string {
const slug = sanitizeProfilePathSegment(profile.id || profile.name || profile.agent);
const baseDir = path.join(CONFIGDIR, "profiles", slug || "profile");
return profile.scope === "custom" ? path.join(baseDir, "custom") : baseDir;
}
function buildCodexConfigToml(
source: string,
values: {
baseUrl: string;
modelCatalogFile: string;
configFormat: "legacy" | "separate_profile_files";
model: string;
providerId: string;
providerName: string;
showAllSessions: boolean;
token: string;
}
): string {
let content = removeManagedBlock(source, managedRootStart, managedRootEnd);
content = removeManagedBlock(content, managedProviderStart, managedProviderEnd);
content = removeCodexProviderTable(content, values.providerId);
if (values.configFormat === "separate_profile_files") {
content = removeCodexProfileTable(content, values.providerId);
}
const firstTableIndex = firstTomlTableIndex(content);
const rootSource = firstTableIndex === -1 ? content : content.slice(0, firstTableIndex);
const restSource = firstTableIndex === -1 ? "" : content.slice(firstTableIndex);
const cleanedRoot = removeRootTomlKeys(rootSource, ["model", "model_catalog_json", "model_provider", "profile", "show_all_sessions"]);
const rootBlock = [
managedRootStart,
`model_provider = ${tomlString(values.providerId)}`,
`model = ${tomlString(values.model)}`,
`model_catalog_json = ${tomlString(values.modelCatalogFile)}`,
...(values.showAllSessions ? ["show_all_sessions = true"] : []),
managedRootEnd,
""
].join("\n");
const providerBlock = [
"",
managedProviderStart,
`[model_providers.${tomlKey(values.providerId)}]`,
`name = ${tomlString(values.providerName)}`,
`base_url = ${tomlString(values.baseUrl)}`,
`experimental_bearer_token = ${tomlString(values.token)}`,
'wire_api = "responses"',
managedProviderEnd,
""
].join("\n");
return `${rootBlock}${trimLeadingBlankLines(cleanedRoot)}${restSource}${providerBlock}`.replace(/\n{4,}/g, "\n\n\n");
}
function maybeWriteSeparateCodexProfileFile(
configFile: string,
source: string,
values: {
configFormat: "legacy" | "separate_profile_files";
model: string;
providerId: string;
showAllSessions: boolean;
}
): { changed: boolean; file: string } | undefined {
if (values.configFormat !== "separate_profile_files") {
return undefined;
}
const file = path.join(path.dirname(configFile), `${values.providerId}.config.toml`);
const previous = existsSync(file)
? readFileSync(file, "utf8")
: legacyCodexProfileTableBody(source, values.providerId);
const next = buildSeparateCodexProfileToml(previous, values);
const writeResult = writeFileWithBackup(file, next, { mode: privateFileMode });
return {
changed: writeResult.changed,
file
};
}
function buildSeparateCodexProfileToml(
source: string,
values: {
model: string;
providerId: string;
showAllSessions: boolean;
}
): string {
const firstTableIndex = firstTomlTableIndex(source);
const rootSource = firstTableIndex === -1 ? source : source.slice(0, firstTableIndex);
const restSource = firstTableIndex === -1 ? "" : source.slice(firstTableIndex);
const cleanedRoot = removeRootTomlKeys(rootSource, ["model", "model_provider", "model_reasoning_effort", "show_all_sessions"]);
const rootBlock = [
`model_provider = ${tomlString(values.providerId)}`,
`model = ${tomlString(values.model)}`,
`model_reasoning_effort = "xhigh"`,
...(values.showAllSessions ? ["show_all_sessions = true"] : []),
""
].join("\n");
return ensureTrailingNewline(`${rootBlock}${trimLeadingBlankLines(cleanedRoot)}${restSource}`.replace(/\n{4,}/g, "\n\n\n"));
}
function writeClaudeCodeApiKeyHelper(profile: ProfileConfig, token: string): { backupFile?: string; changed: boolean; file: string } {
const binDir = path.join(CONFIGDIR, "bin");
mkdirSync(binDir, { mode: privateDirMode, recursive: true });
const file = path.join(binDir, claudeCodeApiKeyHelperFilename(profile));
const content = process.platform === "win32"
? claudeCodeApiKeyHelperCmdScript(token)
: claudeCodeApiKeyHelperShellScript(token);
const writeResult = writeGeneratedFileIfChanged(file, content, { mode: privateExecutableMode });
return {
changed: writeResult.changed,
file
};
}
function claudeCodeApiKeyHelperFilename(profile: ProfileConfig): string {
const slug = sanitizeProfilePathSegment(profile.id || profile.name || profile.agent) || "claude-code";
return process.platform === "win32"
? `ccr-claude-code-api-key-${slug}.cmd`
: `ccr-claude-code-api-key-${slug}`;
}
function claudeCodeApiKeyHelperShellScript(token: string): string {
return [
"#!/bin/sh",
`printf '%s\\n' ${shellQuote(token)}`,
""
].join("\n");
}
function claudeCodeApiKeyHelperCmdScript(token: string): string {
return [
"@echo off",
`echo ${cmdValue(token)}`,
""
].join("\r\n");
}
function writeClaudeCodeWrapper(config: AppConfig, profile: ProfileConfig, apiKeyHelperFile: string): { backupFile?: string; changed: boolean; file: string } {
const binDir = path.join(CONFIGDIR, "bin");
mkdirSync(binDir, { mode: privateDirMode, recursive: true });
const runtimeFile = path.join(binDir, codexMiddlewareRuntimeFilename());
const runtimeResult = writeGeneratedFileIfChanged(runtimeFile, codexCliMiddlewareRuntimeScript(), { mode: publicExecutableMode });
const file = path.join(binDir, claudeCodeWrapperFilename(profile));
const content = process.platform === "win32"
? claudeCodeWrapperCmdScript(config, profile, runtimeFile, apiKeyHelperFile)
: claudeCodeWrapperShellScript(config, profile, runtimeFile, apiKeyHelperFile);
const writeResult = writeGeneratedFileIfChanged(file, content, { mode: privateExecutableMode });
return {
changed: writeResult.changed || runtimeResult.changed,
file
};
}
function claudeCodeWrapperFilename(profile: ProfileConfig): string {
const slug = sanitizeProfilePathSegment(profile.id || profile.name || profile.agent).toLowerCase() || "claude-code";
return process.platform === "win32"
? `ccr-claude-code-wrapper-${slug}.cmd`
: `ccr-claude-code-wrapper-${slug}`;
}
function claudeCodeWrapperShellScript(config: AppConfig, profile: ProfileConfig, runtimeFile: string, apiKeyHelperFile: string): string {
const realClaude = profile.env?.CCR_CLAUDE_CODE_BIN?.trim() || "claude";
const surface = normalizeProfileSurface(profile.surface);
const remoteEndpoint = `${gatewayEndpoint(config)}/__ccr/remote`;
const settingsDir = path.dirname(resolveClaudeCodeSettingsFile(profile));
const envExports = Object.entries(profileEnv(profile))
.filter(([key]) => key !== "CCR_CLAUDE_CODE_BIN")
.map(([key, value]) => `export ${key}=${shellQuote(value)}`);
const botEnvExports = shellBotGatewayEnvExports(config, profile);
return [
"#!/bin/sh",
...envExports,
...shellEnvExports(claudeCodeRuntimeEnv(config, profile, settingsDir)),
...shellEnvExports(claudeCodeUtcTimezoneEnvOverride()),
`: "\${CCR_PROFILE_SURFACE:=${surface}}"`,
"export CCR_PROFILE_SURFACE",
...botEnvExports,
`export CCR_CLAUDE_CODE_WRAPPER=1`,
`export CCR_REAL_CLAUDE_CODE_BIN=${shellQuote(realClaude)}`,
`export CODEXL_CLAUDE_CODE_BIN=${shellQuote(realClaude)}`,
`if [ -z "\${CCR_REMOTE_SYNC_ENABLED:-}" ]; then CCR_REMOTE_SYNC_ENABLED=1; fi`,
`if [ -z "\${CCR_REMOTE_SYNC_ENDPOINT:-}" ]; then CCR_REMOTE_SYNC_ENDPOINT=${shellQuote(remoteEndpoint)}; fi`,
`if [ -z "\${CCR_REMOTE_SYNC_API_KEY_HELPER:-}" ]; then CCR_REMOTE_SYNC_API_KEY_HELPER=${shellQuote(apiKeyHelperFile)}; fi`,
`if [ -z "\${CCR_REMOTE_SYNC_PROFILE_ID:-}" ]; then CCR_REMOTE_SYNC_PROFILE_ID=${shellQuote(profile.id || profile.name || "claude-code")}; fi`,
`if [ -z "\${CCR_REMOTE_SYNC_PROFILE_NAME:-}" ]; then CCR_REMOTE_SYNC_PROFILE_NAME=${shellQuote(profile.name || profile.id || "Claude Code")}; fi`,
"export CCR_REMOTE_SYNC_ENABLED CCR_REMOTE_SYNC_ENDPOINT CCR_REMOTE_SYNC_API_KEY_HELPER CCR_REMOTE_SYNC_PROFILE_ID CCR_REMOTE_SYNC_PROFILE_NAME",
...nodeRuntimeShellExecLines(runtimeFile),
""
].join("\n");
}
function claudeCodeWrapperCmdScript(config: AppConfig, profile: ProfileConfig, runtimeFile: string, apiKeyHelperFile: string): string {
const realClaude = profile.env?.CCR_CLAUDE_CODE_BIN?.trim() || "claude";
const surface = normalizeProfileSurface(profile.surface);
const remoteEndpoint = `${gatewayEndpoint(config)}/__ccr/remote`;
const settingsDir = path.dirname(resolveClaudeCodeSettingsFile(profile));
const envExports = Object.entries(profileEnv(profile))
.filter(([key]) => key !== "CCR_CLAUDE_CODE_BIN")
.map(([key, value]) => cmdSetLine(key, value));
const botEnvExports = cmdBotGatewayEnvExports(config, profile);
return [
"@echo off",
...envExports,
...cmdEnvExports(claudeCodeRuntimeEnv(config, profile, settingsDir)),
...cmdEnvExports(claudeCodeUtcTimezoneEnvOverride()),
`if not defined CCR_PROFILE_SURFACE ${cmdSetLine("CCR_PROFILE_SURFACE", surface)}`,
...botEnvExports,
cmdSetLine("CCR_CLAUDE_CODE_WRAPPER", "1"),
cmdSetLine("CCR_REAL_CLAUDE_CODE_BIN", realClaude),
cmdSetLine("CODEXL_CLAUDE_CODE_BIN", realClaude),
`if not defined CCR_REMOTE_SYNC_ENABLED ${cmdSetLine("CCR_REMOTE_SYNC_ENABLED", "1")}`,
`if not defined CCR_REMOTE_SYNC_ENDPOINT ${cmdSetLine("CCR_REMOTE_SYNC_ENDPOINT", remoteEndpoint)}`,
`if not defined CCR_REMOTE_SYNC_API_KEY_HELPER ${cmdSetLine("CCR_REMOTE_SYNC_API_KEY_HELPER", apiKeyHelperFile)}`,
`if not defined CCR_REMOTE_SYNC_PROFILE_ID ${cmdSetLine("CCR_REMOTE_SYNC_PROFILE_ID", profile.id || profile.name || "claude-code")}`,
`if not defined CCR_REMOTE_SYNC_PROFILE_NAME ${cmdSetLine("CCR_REMOTE_SYNC_PROFILE_NAME", profile.name || profile.id || "Claude Code")}`,
...nodeRuntimeCmdExecLines(runtimeFile),
""
].join("\r\n");
}
function writeCodexCliMiddleware(
config: AppConfig,
profile: ProfileConfig,
values: {
configFormat: "legacy" | "separate_profile_files";
configFile: string;
modelCatalogFile: string;
model: string;
providerId: string;
}
): { changed: boolean; file: string } {
const binDir = path.join(CONFIGDIR, "bin");
mkdirSync(binDir, { mode: privateDirMode, recursive: true });
const runtimeFile = path.join(binDir, codexMiddlewareRuntimeFilename());
const runtimeResult = writeGeneratedFileIfChanged(runtimeFile, codexCliMiddlewareRuntimeScript(), { mode: publicExecutableMode });
const file = path.join(binDir, codexMiddlewareFilename(profile, values.providerId));
const content = process.platform === "win32"
? codexMiddlewareCmdScript(config, profile, values, runtimeFile)
: codexMiddlewareShellScript(config, profile, values, runtimeFile);
const writeResult = writeGeneratedFileIfChanged(file, content, { mode: privateExecutableMode });
return {
changed: writeResult.changed || runtimeResult.changed,
file
};
}
function claudeCodeRuntimeEnv(config: AppConfig, profile: ProfileConfig, settingsDir: string): Record<string, string> {
const endpoint = gatewayEndpoint(config);
const env: Record<string, string> = {
ANTHROPIC_API_BASE_URL: endpoint,
ANTHROPIC_BASE_URL: endpoint,
CLAUDE_AGENT_API_BASE_URL: endpoint,
CLAUDE_CONFIG_DIR: settingsDir
};
const model = normalizeClientModel(profile.model);
if (model) {
env.ANTHROPIC_MODEL = model;
env.CCR_CLAUDE_CODE_MODEL = model;
env.CODEXL_CLAUDE_CODE_MODEL = model;
}
const smallFastModel = normalizeClientModel(profile.smallFastModel);
if (smallFastModel) {
env.ANTHROPIC_SMALL_FAST_MODEL = smallFastModel;
}
return env;
}
function codexMiddlewareRuntimeFilename(): string {
return "ccr-codex-cli-middleware.js";
}
function codexMiddlewareFilename(profile: ProfileConfig, providerId: string): string {
const slug = sanitizeCodexProviderId(profile.id || profile.name || providerId) || "codex";
return process.platform === "win32"
? `ccr-codex-cli-stdio-${slug}.cmd`
: `ccr-codex-cli-stdio-${slug}`;
}
function shellProfileSurfaceExports(surface: "auto" | "cli" | "app"): string[] {
return [
"if [ -z \"${CCR_PROFILE_SURFACE:-}\" ]; then",
" case \"${1:-}\" in",
" app|app-server) CCR_PROFILE_SURFACE=app ;;",
` *) CCR_PROFILE_SURFACE=${shellQuote(surface)} ;;`,
" esac",
"fi",
"export CCR_PROFILE_SURFACE"
];
}
function shellCodexlProfileSurfaceExports(): string[] {
return [
"if [ -z \"${CODEXL_PROFILE_SURFACE:-}\" ]; then",
" CODEXL_PROFILE_SURFACE=$CCR_PROFILE_SURFACE",
"fi",
"export CODEXL_PROFILE_SURFACE"
];
}
function nodeRuntimeShellExecLines(runtimeFile: string): string[] {
return [
"if [ -n \"${CCR_NODE_BIN:-}\" ]; then",
` exec "$CCR_NODE_BIN" ${shellQuote(runtimeFile)} "$@"`,
"fi",
"if command -v node >/dev/null 2>&1; then",
` exec node ${shellQuote(runtimeFile)} "$@"`,
"fi",
`ELECTRON_RUN_AS_NODE=1 exec ${shellQuote(process.execPath)} ${shellQuote(runtimeFile)} "$@"`
];
}
function codexMiddlewareShellScript(
config: AppConfig,
profile: ProfileConfig,
values: {
configFormat: "legacy" | "separate_profile_files";
configFile: string;
modelCatalogFile: string;
model: string;
providerId: string;
},
runtimeFile: string
): string {
const codexCli = profile.codexCliPath?.trim() || defaultCodexCliCommand(profile.agent);
const codexHome = profile.codexHome?.trim() || defaultCodexCompatibleHome(profile.agent, values.configFile);
const resolvedCodexHome = resolveUserPath(codexHome);
const remoteFrontendMode = normalizeCodexRemoteFrontendMode(profile.remoteFrontendMode);
const surface = profile.agent === "zcode" ? "app" : normalizeProfileSurface(profile.surface);
const envExports = Object.entries(profileEnv(profile)).map(([key, value]) => `export ${key}=${shellQuote(value)}`);
const botEnvExports = shellBotGatewayEnvExports(config, profile);
const agentEnvExports = profile.agent === "zcode"
? [
`export ZCODE_HOME=${shellQuote(resolvedCodexHome)}`,
`export ZCODE_STORAGE_DIR=${shellQuote(resolvedCodexHome)}`,
"if [ -z \"${CCR_REAL_ZCODE_CLI_PATH:-}\" ]; then",
` CCR_REAL_ZCODE_CLI_PATH=${shellQuote(codexCli)}`,
"fi",
"export CCR_REAL_ZCODE_CLI_PATH",
`export CCR_ZCODE_PROFILE=${shellQuote(values.providerId)}`,
`export CCR_ZCODE_MODEL=${shellQuote(values.model)}`,
`export CCR_ZCODE_MODEL_CATALOG_FILE=${shellQuote(values.modelCatalogFile)}`,
`export CCR_ZCODE_MODEL_PROVIDER=${shellQuote(values.providerId)}`,
`export CCR_ZCODE_PROFILE_CONFIG_FORMAT=${shellQuote(values.configFormat)}`,
`export CCR_PROFILE_SCOPE=${shellQuote(normalizeProfileScope(profile.scope))}`,
`export CCR_ZCODE_REMOTE_FRONTEND_MODE=${shellQuote(remoteFrontendMode)}`,
"if [ -z \"${CODEXL_REAL_ZCODE_CLI_PATH:-}\" ]; then",
" CODEXL_REAL_ZCODE_CLI_PATH=$CCR_REAL_ZCODE_CLI_PATH",
"fi",
"export CODEXL_REAL_ZCODE_CLI_PATH",
`export CODEXL_ZCODE_PROFILE=${shellQuote(values.providerId)}`,
`export CODEXL_ZCODE_MODEL_CATALOG_FILE=${shellQuote(values.modelCatalogFile)}`,
`export CODEXL_ZCODE_MODEL_PROVIDER=${shellQuote(values.providerId)}`,
`export CODEXL_ZCODE_WORKSPACE_NAME=${shellQuote(profile.name || values.providerId)}`,
`export CODEXL_ZCODE_PROFILE_CONFIG_FORMAT=${shellQuote(values.configFormat)}`,
`export CODEXL_ZCODE_CORE_MODE=${shellQuote(remoteFrontendMode)}`
]
: [
`export CODEX_HOME=${shellQuote(resolvedCodexHome)}`,
"if [ -z \"${CCR_REAL_CODEX_CLI_PATH:-}\" ]; then",
` CCR_REAL_CODEX_CLI_PATH=${shellQuote(codexCli)}`,
"fi",
"export CCR_REAL_CODEX_CLI_PATH",
`export CCR_CODEX_PROFILE=${shellQuote(values.providerId)}`,
`export CCR_CODEX_MODEL=${shellQuote(values.model)}`,
`export CCR_CODEX_MODEL_CATALOG_FILE=${shellQuote(values.modelCatalogFile)}`,
`export CCR_CODEX_MODEL_PROVIDER=${shellQuote(values.providerId)}`,
`export CCR_CODEX_PROFILE_CONFIG_FORMAT=${shellQuote(values.configFormat)}`,
`export CCR_PROFILE_SCOPE=${shellQuote(normalizeProfileScope(profile.scope))}`,
`export CCR_CODEX_REMOTE_FRONTEND_MODE=${shellQuote(remoteFrontendMode)}`,
"if [ -z \"${CODEXL_REAL_CODEX_CLI_PATH:-}\" ]; then",
" CODEXL_REAL_CODEX_CLI_PATH=$CCR_REAL_CODEX_CLI_PATH",
"fi",
"export CODEXL_REAL_CODEX_CLI_PATH",
`export CODEXL_CODEX_PROFILE=${shellQuote(values.providerId)}`,
`export CODEXL_CODEX_MODEL_CATALOG_FILE=${shellQuote(values.modelCatalogFile)}`,
`export CODEXL_CODEX_MODEL_PROVIDER=${shellQuote(values.providerId)}`,
`export CODEXL_CODEX_WORKSPACE_NAME=${shellQuote(profile.name || values.providerId)}`,
`export CODEXL_CODEX_PROFILE_CONFIG_FORMAT=${shellQuote(values.configFormat)}`,
`export CODEXL_CODEX_CORE_MODE=${shellQuote(remoteFrontendMode)}`
];
return [
"#!/bin/sh",
...envExports,
...agentEnvExports,
...shellProfileSurfaceExports(surface),
...botEnvExports,
...shellCodexlProfileSurfaceExports(),
...nodeRuntimeShellExecLines(runtimeFile),
""
].join("\n");
}
function cmdProfileSurfaceExports(surface: "auto" | "cli" | "app"): string[] {
return [
"if not defined CCR_PROFILE_SURFACE (",
" if \"%~1\"==\"app\" (",
cmdSetLine("CCR_PROFILE_SURFACE", "app", " "),
" ) else if \"%~1\"==\"app-server\" (",
cmdSetLine("CCR_PROFILE_SURFACE", "app", " "),
" ) else (",
cmdSetLine("CCR_PROFILE_SURFACE", surface, " "),
" )",
")"
];
}
function cmdCodexlProfileSurfaceExports(): string[] {
return [
"if not defined CODEXL_PROFILE_SURFACE set \"CODEXL_PROFILE_SURFACE=%CCR_PROFILE_SURFACE%\""
];
}
function nodeRuntimeCmdExecLines(runtimeFile: string): string[] {
const quotedRuntime = cmdQuote(runtimeFile);
const quotedHost = cmdQuote(process.execPath);
return [
"if defined CCR_NODE_BIN (",
` "%CCR_NODE_BIN%" ${quotedRuntime} %*`,
" exit /b %ERRORLEVEL%",
")",
"where node >nul 2>nul",
"if %ERRORLEVEL%==0 (",
` node ${quotedRuntime} %*`,
" exit /b %ERRORLEVEL%",
")",
"set \"ELECTRON_RUN_AS_NODE=1\"",
`${quotedHost} ${quotedRuntime} %*`,
"exit /b %ERRORLEVEL%"
];
}
function codexMiddlewareCmdScript(
config: AppConfig,
profile: ProfileConfig,
values: {
configFormat: "legacy" | "separate_profile_files";
configFile: string;
modelCatalogFile: string;
model: string;
providerId: string;
},
runtimeFile: string
): string {
const codexCli = profile.codexCliPath?.trim() || defaultCodexCliCommand(profile.agent);
const codexHome = profile.codexHome?.trim() || defaultCodexCompatibleHome(profile.agent, values.configFile);
const resolvedCodexHome = resolveUserPath(codexHome);
const remoteFrontendMode = normalizeCodexRemoteFrontendMode(profile.remoteFrontendMode);
const surface = profile.agent === "zcode" ? "app" : normalizeProfileSurface(profile.surface);
const workspaceName = profile.name || values.providerId;
const envExports = Object.entries(profileEnv(profile)).map(([key, value]) => cmdSetLine(key, value));
const botEnvExports = cmdBotGatewayEnvExports(config, profile);
const agentEnvExports = profile.agent === "zcode"
? [
cmdSetLine("ZCODE_HOME", resolvedCodexHome),
cmdSetLine("ZCODE_STORAGE_DIR", resolvedCodexHome),
`if not defined CCR_REAL_ZCODE_CLI_PATH ${cmdSetLine("CCR_REAL_ZCODE_CLI_PATH", codexCli)}`,
cmdSetLine("CCR_ZCODE_PROFILE", values.providerId),
cmdSetLine("CCR_ZCODE_MODEL", values.model),
cmdSetLine("CCR_ZCODE_MODEL_CATALOG_FILE", values.modelCatalogFile),
cmdSetLine("CCR_ZCODE_MODEL_PROVIDER", values.providerId),
cmdSetLine("CCR_ZCODE_PROFILE_CONFIG_FORMAT", values.configFormat),
cmdSetLine("CCR_PROFILE_SCOPE", normalizeProfileScope(profile.scope)),
cmdSetLine("CCR_ZCODE_REMOTE_FRONTEND_MODE", remoteFrontendMode),
"if not defined CODEXL_REAL_ZCODE_CLI_PATH set \"CODEXL_REAL_ZCODE_CLI_PATH=%CCR_REAL_ZCODE_CLI_PATH%\"",
cmdSetLine("CODEXL_ZCODE_PROFILE", values.providerId),
cmdSetLine("CODEXL_ZCODE_MODEL_CATALOG_FILE", values.modelCatalogFile),
cmdSetLine("CODEXL_ZCODE_MODEL_PROVIDER", values.providerId),
cmdSetLine("CODEXL_ZCODE_WORKSPACE_NAME", workspaceName),
cmdSetLine("CODEXL_ZCODE_PROFILE_CONFIG_FORMAT", values.configFormat),
cmdSetLine("CODEXL_ZCODE_CORE_MODE", remoteFrontendMode)
]
: [
cmdSetLine("CODEX_HOME", resolvedCodexHome),
`if not defined CCR_REAL_CODEX_CLI_PATH ${cmdSetLine("CCR_REAL_CODEX_CLI_PATH", codexCli)}`,
cmdSetLine("CCR_CODEX_PROFILE", values.providerId),
cmdSetLine("CCR_CODEX_MODEL", values.model),
cmdSetLine("CCR_CODEX_MODEL_CATALOG_FILE", values.modelCatalogFile),
cmdSetLine("CCR_CODEX_MODEL_PROVIDER", values.providerId),
cmdSetLine("CCR_CODEX_PROFILE_CONFIG_FORMAT", values.configFormat),
cmdSetLine("CCR_PROFILE_SCOPE", normalizeProfileScope(profile.scope)),
cmdSetLine("CCR_CODEX_REMOTE_FRONTEND_MODE", remoteFrontendMode),
"if not defined CODEXL_REAL_CODEX_CLI_PATH set \"CODEXL_REAL_CODEX_CLI_PATH=%CCR_REAL_CODEX_CLI_PATH%\"",
cmdSetLine("CODEXL_CODEX_PROFILE", values.providerId),
cmdSetLine("CODEXL_CODEX_MODEL_CATALOG_FILE", values.modelCatalogFile),
cmdSetLine("CODEXL_CODEX_MODEL_PROVIDER", values.providerId),
cmdSetLine("CODEXL_CODEX_WORKSPACE_NAME", workspaceName),
cmdSetLine("CODEXL_CODEX_PROFILE_CONFIG_FORMAT", values.configFormat),
cmdSetLine("CODEXL_CODEX_CORE_MODE", remoteFrontendMode)
];
return [
"@echo off",
...envExports,
...agentEnvExports,
...cmdProfileSurfaceExports(surface),
...botEnvExports,
...cmdCodexlProfileSurfaceExports(),
...nodeRuntimeCmdExecLines(runtimeFile),
""
].join("\r\n");
}
function shellBotGatewayEnvExports(config: AppConfig, profile: ProfileConfig): string[] {
return [
'if [ "$CCR_PROFILE_SURFACE" = "app" ]; then',
...Object.entries(botGatewayProfileEnv(config, profile, "app")).map(([key, value]) => ` export ${key}=${shellQuote(value)}`),
"else",
...Object.entries(botGatewayProfileEnv(config, profile, "cli")).map(([key, value]) => ` export ${key}=${shellQuote(value)}`),
"fi"
];
}
function shellEnvExports(env: Record<string, string>): string[] {
return Object.entries(env).map(([key, value]) => `export ${key}=${shellQuote(value)}`);
}
function cmdBotGatewayEnvExports(config: AppConfig, profile: ProfileConfig): string[] {
return [
`if /I "%CCR_PROFILE_SURFACE%"=="app" (`,
...Object.entries(botGatewayProfileEnv(config, profile, "app")).map(([key, value]) => cmdSetLine(key, value, " ")),
") else (",
...Object.entries(botGatewayProfileEnv(config, profile, "cli")).map(([key, value]) => cmdSetLine(key, value, " ")),
")"
];
}
function cmdEnvExports(env: Record<string, string>): string[] {
return Object.entries(env).map(([key, value]) => cmdSetLine(key, value));
}
function withoutBotGatewayEnv(values: Record<string, string>): Record<string, string> {
return Object.fromEntries(Object.entries(values).filter(([key]) => !isBotGatewayEnvKey(key)));
}
function isBotGatewayEnvKey(key: string): boolean {
return key === "BOT_GATEWAY_STATE_DIR" ||
key.startsWith("CCR_BOT_") ||
key.startsWith("CODEXL_BOT_") ||
key === "CCR_BOT_GATEWAY_SDK_MODULE";
}
function removeRootTomlKeys(source: string, keys: string[]): string {
const keyPattern = keys.map(escapeRegExp).join("|");
const pattern = new RegExp(`^\\s*(?:${keyPattern})\\s*=.*(?:\\n|$)`, "gm");
return source.replace(pattern, "");
}
function removeCodexProviderTable(source: string, providerId: string): string {
return removeTomlTable(source, "model_providers", providerId);
}
function removeCodexProfileTable(source: string, providerId: string): string {
return removeTomlTable(source, "profiles", providerId);
}
function removeTomlTable(source: string, section: string, name: string): string {
const lines = source.split(/(?<=\n)/);
const headers = new Set([
`[${section}.${name}]`,
`[${section}.${tomlQuotedKey(name)}]`
]);
const kept: string[] = [];
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index];
if (!headers.has(line.trim())) {
kept.push(line);
continue;
}
index += 1;
while (index < lines.length && !/^\s*\[/.test(lines[index])) {
index += 1;
}
index -= 1;
}
return kept.join("");
}
function legacyCodexProfileTableBody(source: string, providerId: string): string {
const headers = new Set([
`[profiles.${providerId}]`,
`[profiles.${tomlQuotedKey(providerId)}]`
]);
const lines: string[] = [];
let inTarget = false;
for (const line of source.split(/\r?\n/)) {
const trimmed = line.trim();
if (/^\s*\[/.test(trimmed)) {
if (inTarget) {
break;
}
inTarget = headers.has(trimmed);
continue;
}
if (inTarget) {
lines.push(line);
}
}
return lines.join("\n").trim();
}
function removeManagedBlock(source: string, start: string, end: string): string {
const pattern = new RegExp(`\\n?${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}\\n?`, "g");
return source.replace(pattern, "\n");
}
function firstTomlTableIndex(source: string): number {
const match = source.match(/^\s*\[/m);
return match?.index ?? -1;
}
function readJsonObject(file: string): Record<string, unknown> {
if (!existsSync(file)) {
return {};
}
try {
const parsed = JSON.parse(readFileSync(file, "utf8")) as unknown;
return isRecord(parsed) ? parsed : {};
} catch {
return {};
}
}
function writeFileWithBackup(
file: string,
content: string,
options: { mode?: number } = {}
): { backupFile?: string; changed: boolean } {
mkdirSync(path.dirname(file), { recursive: true });
const previous = existsSync(file) ? readFileSync(file, "utf8") : undefined;
if (previous === content) {
chmodFileIfRequested(file, options.mode);
return { changed: false };
}
ensureOriginalSnapshot(file, previous, options.mode);
const backupFile = previous === undefined ? undefined : backupFilePath(file);
if (backupFile) {
copyFileSync(file, backupFile);
chmodFileIfRequested(backupFile, options.mode);
}
writeFileSync(file, content, options.mode === undefined ? "utf8" : { encoding: "utf8", mode: options.mode });
chmodFileIfRequested(file, options.mode);
return { backupFile, changed: true };
}
function writeGeneratedFileIfChanged(
file: string,
content: string,
options: { mode?: number } = {}
): { changed: boolean } {
mkdirSync(path.dirname(file), { recursive: true });
const previous = existsSync(file) ? readFileSync(file, "utf8") : undefined;
if (previous === content) {
chmodFileIfRequested(file, options.mode);
return { changed: false };
}
writeFileSync(file, content, options.mode === undefined ? "utf8" : { encoding: "utf8", mode: options.mode });
chmodFileIfRequested(file, options.mode);
return { changed: true };
}
export function cleanupGeneratedBinBackups(configDir = CONFIGDIR): number {
const binDir = path.join(configDir, "bin");
let entries: string[];
try {
entries = readdirSync(binDir);
} catch {
return 0;
}
let removed = 0;
for (const entry of entries) {
const baseName = generatedBinBackupBaseName(entry);
if (!baseName || !isManagedGeneratedBinFile(baseName)) {
continue;
}
try {
rmSync(path.join(binDir, entry), { force: true });
removed += 1;
} catch {
// Cleanup is best effort; stale backups should never block profile launch.
}
}
return removed;
}
function generatedBinBackupBaseName(entry: string): string | undefined {
const backupMarker = ".ccr-backup-";
const backupIndex = entry.indexOf(backupMarker);
if (backupIndex !== -1) {
return entry.slice(0, backupIndex);
}
for (const suffix of [originalMissingSuffix, originalBackupSuffix]) {
if (entry.endsWith(suffix)) {
return entry.slice(0, -suffix.length);
}
}
return undefined;
}
function isManagedGeneratedBinFile(fileName: string): boolean {
const normalized = fileName.replace(/\.cmd$/i, "");
return normalized === "ccr" ||
normalized === "ccr-cli.js" ||
normalized === codexMiddlewareRuntimeFilename() ||
normalized.startsWith("ccr-claude-code-api-key-") ||
normalized.startsWith("ccr-claude-code-wrapper-") ||
normalized.startsWith("ccr-codex-cli-stdio-");
}
type RestoreFileResult = {
backupFile?: string;
changed: boolean;
file: string;
missingBackup: boolean;
restored: boolean;
};
function restoreDisabledGlobalProfile(
profile: ProfileConfig,
file: string,
disabledMessage: string,
isManagedContent: (content: string) => boolean
): ProfileClientApplyStatus {
if (!isGlobalProfile(profile)) {
return disabledStatus(profile.agent, file, disabledMessage);
}
const restoreResult = restoreGlobalConfigFile(file, { isManagedContent, mode: privateFileMode });
return disabledRestoreStatus(profile.agent, file, disabledMessage, restoreResult, profile.name || profile.id || profile.agent);
}
function disabledProfileStatus(profile: ProfileConfig): ProfileClientApplyStatus {
if (profile.agent === "claude-code") {
return restoreDisabledGlobalProfile(profile, resolveClaudeCodeSettingsFile(profile), "Claude Code profile is disabled.", isManagedClaudeCodeSettingsContent);
}
if (profile.agent === "zcode") {
return restoreDisabledZcodeProfile(profile, resolveZcodeConfigFile(profile));
}
const providerId = sanitizeCodexProviderId(profile.providerId || "") || "claude-code-router";
return restoreDisabledGlobalProfile(
profile,
resolveCodexConfigFile(profile),
"Codex profile is disabled.",
(content) => isManagedCodexConfigContent(content, providerId)
);
}
export function restoreInactiveGlobalProfileConfigs(profiles: ProfileConfig[]): ProfileClientApplyStatus[] {
const statuses: ProfileClientApplyStatus[] = [];
if (!profiles.some((profile) => profile.agent === "claude-code" && profile.enabled && isGlobalProfile(profile))) {
for (const file of uniqueResolvedPaths([
"~/.claude/settings.json",
...profiles
.filter((profile) => profile.agent === "claude-code")
.map((profile) => profile.settingsFile || "")
.filter(Boolean)
])) {
const restoreResult = restoreGlobalConfigFile(file, {
isManagedContent: isManagedClaudeCodeSettingsContent,
mode: privateFileMode
});
if (restoreResult.changed || restoreResult.missingBackup) {
statuses.push(inactiveGlobalCleanupStatus("claude-code", file, restoreResult));
}
}
}
return statuses;
}
function inactiveGlobalCleanupStatus(
client: ProfileClientKind,
file: string,
restoreResult: RestoreFileResult
): ProfileClientApplyStatus {
return {
backupFile: restoreResult.backupFile,
client,
enabled: false,
message: restoreResult.missingBackup
? `No active global ${codexCompatibleClientName(client)} profile is configured, but the global config is managed by CCR and no original backup was found.`
: `${codexCompatibleClientName(client)} global config was restored because no active global profile is configured.`,
ok: !restoreResult.missingBackup,
path: resolveUserPath(file)
};
}
function uniqueResolvedPaths(paths: string[]): string[] {
const seen = new Set<string>();
const result: string[] = [];
for (const item of paths) {
const resolved = resolveUserPath(item);
const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
if (seen.has(key)) {
continue;
}
seen.add(key);
result.push(resolved);
}
return result;
}
function restoreDisabledZcodeProfile(profile: ProfileConfig, configFile: string): ProfileClientApplyStatus {
const disabledMessage = "ZCode profile is disabled.";
if (!isGlobalProfile(profile)) {
return disabledStatus("zcode", configFile, disabledMessage);
}
const providerId = sanitizeCodexProviderId(profile.providerId || "") || "claude-code-router";
const storageRoot = zcodeHomeFromConfigFile(configFile);
const files = [
configFile,
path.join(storageRoot, "v2", "config.json"),
path.join(storageRoot, "v2", "bots-model-cache.v2.json")
];
const results = files.map((file) =>
restoreGlobalConfigFile(file, {
isManagedContent: (content) => isManagedZcodeConfigContent(content, providerId),
mode: privateFileMode
})
);
const changed = results.some((result) => result.changed);
const restored = results.some((result) => result.restored);
const missingBackup = results.some((result) => result.missingBackup);
return {
backupFile: results.find((result) => result.backupFile)?.backupFile,
client: "zcode",
enabled: false,
message: missingBackup
? `${disabledMessage} No original ZCode config backup was found for ${profile.name || profile.id || "this profile"}.`
: restored
? changed
? "ZCode config was restored from the CCR backup because the global profile is disabled."
: "ZCode config already matches the CCR backup; profile is disabled."
: disabledMessage,
ok: !missingBackup,
path: resolveUserPath(configFile)
};
}
function disabledRestoreStatus(
client: ProfileClientKind,
file: string,
disabledMessage: string,
restoreResult: RestoreFileResult,
profileName: string
): ProfileClientApplyStatus {
return {
backupFile: restoreResult.backupFile,
client,
enabled: false,
message: restoreResult.missingBackup
? `${disabledMessage} No original ${codexCompatibleClientName(client)} config backup was found for ${profileName}.`
: restoreResult.restored
? restoreResult.changed
? `${codexCompatibleClientName(client)} config was restored from the CCR backup because the global profile is disabled.`
: `${codexCompatibleClientName(client)} config already matches the CCR backup; profile is disabled.`
: disabledMessage,
ok: !restoreResult.missingBackup,
path: resolveUserPath(file)
};
}
function restoreGlobalConfigFile(
file: string,
options: {
isManagedContent: (content: string) => boolean;
mode?: number;
}
): RestoreFileResult {
const current = existsSync(file) ? readFileSync(file, "utf8") : undefined;
const currentManaged = current !== undefined && options.isManagedContent(current);
if (current !== undefined && !currentManaged) {
return { changed: false, file, missingBackup: false, restored: false };
}
if (existsSync(originalMissingFilePath(file))) {
if (currentManaged) {
const backupFile = backupCurrentConfigFile(file, options.mode);
rmSync(file, { force: true });
return { backupFile, changed: true, file, missingBackup: false, restored: true };
}
return { changed: false, file, missingBackup: false, restored: current === undefined };
}
const snapshot = originalSnapshotCandidate(file, options.isManagedContent);
if (!snapshot) {
return {
changed: false,
file,
missingBackup: Boolean(currentManaged),
restored: false
};
}
if (current === snapshot.content) {
chmodFileIfRequested(file, options.mode);
return { changed: false, file, missingBackup: false, restored: true };
}
const backupFile = current === undefined ? undefined : backupCurrentConfigFile(file, options.mode);
mkdirSync(path.dirname(file), { recursive: true });
writeFileSync(file, snapshot.content, options.mode === undefined ? "utf8" : { encoding: "utf8", mode: options.mode });
chmodFileIfRequested(file, options.mode);
return { backupFile, changed: true, file, missingBackup: false, restored: true };
}
function originalSnapshotCandidate(
file: string,
isManagedContent: (content: string) => boolean
): { content: string; file: string } | undefined {
for (const candidate of [originalBackupFilePath(file), ...backupFiles(file)]) {
if (!existsSync(candidate)) {
continue;
}
const content = readFileSync(candidate, "utf8");
if (!isManagedContent(content)) {
return { content, file: candidate };
}
}
return undefined;
}
function backupCurrentConfigFile(file: string, mode: number | undefined): string {
const backupFile = backupFilePath(file);
copyFileSync(file, backupFile);
chmodFileIfRequested(backupFile, mode);
return backupFile;
}
function backupFiles(file: string): string[] {
const dir = path.dirname(file);
const prefix = `${path.basename(file)}.ccr-backup-`;
try {
return readdirSync(dir)
.filter((entry) => entry.startsWith(prefix))
.sort()
.map((entry) => path.join(dir, entry));
} catch {
return [];
}
}
function ensureOriginalSnapshot(file: string, previous: string | undefined, mode: number | undefined): void {
const originalBackup = originalBackupFilePath(file);
const originalMissing = originalMissingFilePath(file);
if (existsSync(originalBackup) || existsSync(originalMissing)) {
return;
}
if (previous === undefined) {
writeFileSync(originalMissing, "", "utf8");
chmodFileIfRequested(originalMissing, mode);
return;
}
copyFileSync(file, originalBackup);
chmodFileIfRequested(originalBackup, mode);
}
function chmodFileIfRequested(file: string, mode: number | undefined): void {
if (mode === undefined || process.platform === "win32") {
return;
}
try {
chmodSync(file, mode);
} catch {
// Best effort; the write itself should still succeed on filesystems without chmod.
}
}
function backupFilePath(file: string): string {
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
return `${file}.ccr-backup-${timestamp}`;
}
function originalBackupFilePath(file: string): string {
return `${file}${originalBackupSuffix}`;
}
function originalMissingFilePath(file: string): string {
return `${file}${originalMissingSuffix}`;
}
function disabledStatus(client: ProfileClientKind, file: string, message: string): ProfileClientApplyStatus {
return {
client,
enabled: false,
message,
ok: true,
path: resolveUserPath(file)
};
}
function unavailableModelStatus(profile: ProfileConfig, file: string): ProfileClientApplyStatus {
return {
client: profile.agent,
enabled: true,
message: NO_AVAILABLE_GATEWAY_MODELS_MESSAGE,
ok: false,
path: resolveUserPath(file)
};
}
function disabledProfileMessage(profile: ProfileConfig): string {
if (profile.agent === "claude-code") {
return "Claude Code profile is disabled.";
}
return `${codexCompatibleClientName(profile.agent)} profile is disabled.`;
}
function isGlobalProfile(profile: ProfileConfig): boolean {
return normalizeProfileScope(profile.scope) === "global";
}
function isManagedClaudeCodeSettingsContent(content: string): boolean {
const settings = parseJsonContent(content);
if (!settings) {
return false;
}
const apiKeyHelper = typeof settings.apiKeyHelper === "string" ? settings.apiKeyHelper : "";
if (apiKeyHelper.includes("ccr-claude-code-api-key-")) {
return true;
}
const env = isRecord(settings.env) ? settings.env : {};
return typeof env.ANTHROPIC_BASE_URL === "string" &&
typeof env.ANTHROPIC_API_BASE_URL === "string" &&
typeof env.CLAUDE_AGENT_API_BASE_URL === "string";
}
function isManagedCodexConfigContent(content: string, providerId: string): boolean {
if (content.includes(managedRootStart) || content.includes(managedProviderStart)) {
return true;
}
const escapedProvider = escapeRegExp(providerId);
return new RegExp(`^\\s*\\[model_providers\\.(?:${escapedProvider}|${escapeRegExp(tomlQuotedKey(providerId))})\\]`, "m").test(content);
}
function isManagedZcodeConfigContent(content: string, providerId: string): boolean {
const config = parseJsonContent(content);
if (!config) {
return false;
}
if (isRecord(config.provider) && hasOwn(config.provider, providerId)) {
return true;
}
if (isRecord(config.model) && typeof config.model.main === "string" && config.model.main.startsWith(`${providerId}/`)) {
return true;
}
for (const key of ["defaultModel", "lastUsed", "lastUsedModel"]) {
const modelRef = config[key];
if (isRecord(modelRef) && modelRef.providerId === providerId) {
return true;
}
}
return Array.isArray(config.providers) && config.providers.some((provider) =>
isRecord(provider) && provider.id === providerId
);
}
function parseJsonContent(content: string): Record<string, unknown> | undefined {
try {
const parsed = JSON.parse(content) as unknown;
return isRecord(parsed) ? parsed : undefined;
} catch {
return undefined;
}
}
function gatewayEndpoint(config: AppConfig): string {
const host = config.gateway.host === "0.0.0.0" ? "127.0.0.1" : config.gateway.host || "127.0.0.1";
const formattedHost = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
return `http://${formattedHost}:${config.gateway.port}`;
}
function defaultClientModel(config: AppConfig): string {
const preferred = config.Providers.find((provider) => provider.name === config.preferredProvider) ?? config.Providers[0];
if (preferred?.name && preferred.models[0]) {
return `${preferred.name}/${preferred.models[0]}`;
}
return "gpt-5-codex";
}
function normalizeClientModel(value: string | undefined): string {
return normalizeRouteSelector(value)?.trim() || "";
}
function resolveUserPath(value: string): string {
const trimmed = value.trim();
if (trimmed === "~") {
return os.homedir();
}
if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) {
return path.join(os.homedir(), trimmed.slice(2));
}
return path.resolve(trimmed || ".");
}
function sanitizeCodexProviderId(value: string): string {
return value.trim().replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
}
function sanitizeProfilePathSegment(value: string): string {
return value.trim().replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
}
function normalizeCodexConfigFormat(_value: ProfileConfig["configFormat"]): "legacy" | "separate_profile_files" {
return "separate_profile_files";
}
function normalizeCodexRemoteFrontendMode(value: ProfileConfig["remoteFrontendMode"]): "app" | "cli" | "claude-code" {
return value === "cli" || value === "claude-code" ? value : "app";
}
function normalizeProfileScope(value: ProfileConfig["scope"]): "ccr" | "global" | "custom" {
return value === "ccr" || value === "custom" ? value : "global";
}
function isGeneratedProfileScope(value: ProfileConfig["scope"]): boolean {
return value === "ccr" || value === "custom";
}
function normalizeProfileSurface(value: ProfileConfig["surface"]): "auto" | "cli" | "app" {
return value === "cli" || value === "app" ? value : "auto";
}
function codexCompatibleClientName(agent: ProfileConfig["agent"]): string {
if (agent === "claude-code") {
return "Claude Code";
}
return agent === "zcode" ? "ZCode" : "Codex";
}
function defaultCodexConfigFile(agent: ProfileConfig["agent"]): string {
return agent === "zcode" ? "~/.zcode/cli/config.json" : "~/.codex/config.toml";
}
function codexConfigSubdir(agent: ProfileConfig["agent"]): string {
return agent === "zcode" ? "zcode" : "codex";
}
function defaultCodexCliCommand(agent: ProfileConfig["agent"]): string {
return agent === "zcode" ? "zcode" : "codex";
}
function defaultCodexCompatibleHome(agent: ProfileConfig["agent"], configFile: string): string {
return agent === "zcode" ? zcodeHomeFromConfigFile(configFile) : path.dirname(configFile);
}
function profileEnv(profile: ProfileConfig): Record<string, string> {
return stringRecord(profile.env).filter(([key]) => isEnvName(key)).reduce<Record<string, string>>((result, [key, value]) => {
if (profile.agent !== "claude-code" && key === CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV) {
return result;
}
result[key] = value;
return result;
}, {});
}
function stringRecord(value: unknown): Array<[string, string]> {
if (!isRecord(value)) {
return [];
}
return Object.entries(value)
.map(([key, itemValue]) => [key.trim(), itemValue] as const)
.filter((entry): entry is [string, string] => Boolean(entry[0]) && typeof entry[1] === "string");
}
function isEnvName(value: string): boolean {
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
}
function tomlKey(value: string): string {
return /^[A-Za-z0-9_-]+$/.test(value) ? value : tomlQuotedKey(value);
}
function tomlQuotedKey(value: string): string {
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
}
function tomlString(value: string): string {
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r")}"`;
}
function tomlStringContent(value: string): string {
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
}
function shellQuote(value: string): string {
return `'${value.replace(/'/g, "'\\''")}'`;
}
function cmdSetLine(key: string, value: string, indent = ""): string {
return `${indent}set "${key}=${cmdValue(value)}"`;
}
function cmdQuote(value: string): string {
return `"${cmdValue(value)}"`;
}
function cmdValue(value: string): string {
return value
.replace(/\r?\n/g, " ")
.replace(/\^/g, "^^")
.replace(/%/g, "%%")
.replace(/"/g, '^"')
.replace(/[&|<>()]/g, "^$&");
}
function trimLeadingBlankLines(value: string): string {
return value.replace(/^\s*\n/g, "");
}
function ensureTrailingNewline(value: string): string {
return value.endsWith("\n") ? value : `${value}\n`;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function hasOwn(value: Record<string, unknown>, key: string): boolean {
return Object.prototype.hasOwnProperty.call(value, key);
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function formatError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}