Files
claude-code-router/packages/cli/src/utils/index.ts
2025-12-27 22:23:37 +08:00

279 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import fs from "node:fs/promises";
import readline from "node:readline";
import JSON5 from "json5";
import path from "node:path";
import { createHash } from "node:crypto";
import {
CONFIG_FILE,
HOME_DIR, PID_FILE,
PLUGINS_DIR,
PRESETS_DIR,
REFERENCE_COUNT_FILE,
readPresetFile,
} from "@CCR/shared";
import { getServer } from "@CCR/server";
import { writeFileSync, existsSync, readFileSync } from "fs";
import { checkForUpdates, performUpdate } from "./update";
import { version } from "../../package.json";
import { spawn } from "child_process";
import {cleanupPidFile, isServiceRunning} from "./processCheck";
// Function to interpolate environment variables in config values
const interpolateEnvVars = (obj: any): any => {
if (typeof obj === "string") {
// Replace $VAR_NAME or ${VAR_NAME} with environment variable values
return obj.replace(/\$\{([^}]+)\}|\$([A-Z_][A-Z0-9_]*)/g, (match, braced, unbraced) => {
const varName = braced || unbraced;
return process.env[varName] || match; // Keep original if env var doesn't exist
});
} else if (Array.isArray(obj)) {
return obj.map(interpolateEnvVars);
} else if (obj !== null && typeof obj === "object") {
const result: any = {};
for (const [key, value] of Object.entries(obj)) {
result[key] = interpolateEnvVars(value);
}
return result;
}
return obj;
};
const ensureDir = async (dir_path: string) => {
try {
await fs.access(dir_path);
} catch {
await fs.mkdir(dir_path, { recursive: true });
}
};
export const initDir = async () => {
await ensureDir(HOME_DIR);
await ensureDir(PLUGINS_DIR);
await ensureDir(PRESETS_DIR);
await ensureDir(path.join(HOME_DIR, "logs"));
};
const createReadline = () => {
return readline.createInterface({
input: process.stdin,
output: process.stdout,
});
};
const question = (query: string): Promise<string> => {
return new Promise((resolve) => {
const rl = createReadline();
rl.question(query, (answer) => {
rl.close();
resolve(answer);
});
});
};
const confirm = async (query: string): Promise<boolean> => {
const answer = await question(query);
return answer.toLowerCase() !== "n";
};
export const readConfigFile = async () => {
try {
const config = await fs.readFile(CONFIG_FILE, "utf-8");
try {
// Try to parse with JSON5 first (which also supports standard JSON)
const parsedConfig = JSON5.parse(config);
// Interpolate environment variables in the parsed config
return interpolateEnvVars(parsedConfig);
} catch (parseError) {
console.error(`Failed to parse config file at ${CONFIG_FILE}`);
console.error("Error details:", (parseError as Error).message);
console.error("Please check your config file syntax.");
process.exit(1);
}
} catch (readError: any) {
if (readError.code === "ENOENT") {
// Config file doesn't exist, prompt user for initial setup
try {
// Initialize directories
await initDir();
// Backup existing config file if it exists
const backupPath = await backupConfigFile();
if (backupPath) {
console.log(
`Backed up existing configuration file to ${backupPath}`
);
}
const config = {
PORT: 3456,
Providers: [],
Router: {},
}
// Create a minimal default config file
await writeConfigFile(config);
console.log(
"Created minimal default configuration file at ~/.claude-code-router/config.json"
);
console.log(
"Please edit this file with your actual configuration."
);
return config
} catch (error: any) {
console.error(
"Failed to create default configuration:",
error.message
);
process.exit(1);
}
} else {
console.error(`Failed to read config file at ${CONFIG_FILE}`);
console.error("Error details:", readError.message);
process.exit(1);
}
}
};
export const backupConfigFile = async () => {
try {
if (await fs.access(CONFIG_FILE).then(() => true).catch(() => false)) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupPath = `${CONFIG_FILE}.${timestamp}.bak`;
await fs.copyFile(CONFIG_FILE, backupPath);
// Clean up old backups, keeping only the 3 most recent
try {
const configDir = path.dirname(CONFIG_FILE);
const configFileName = path.basename(CONFIG_FILE);
const files = await fs.readdir(configDir);
// Find all backup files for this config
const backupFiles = files
.filter(file => file.startsWith(configFileName) && file.endsWith('.bak'))
.sort()
.reverse(); // Sort in descending order (newest first)
// Delete all but the 3 most recent backups
if (backupFiles.length > 3) {
for (let i = 3; i < backupFiles.length; i++) {
const oldBackupPath = path.join(configDir, backupFiles[i]);
await fs.unlink(oldBackupPath);
}
}
} catch (cleanupError) {
console.warn("Failed to clean up old backups:", cleanupError);
}
return backupPath;
}
} catch (error) {
console.error("Failed to backup config file:", error);
}
return null;
};
export const writeConfigFile = async (config: any) => {
await ensureDir(HOME_DIR);
const configWithComment = `${JSON.stringify(config, null, 2)}`;
await fs.writeFile(CONFIG_FILE, configWithComment);
};
export const initConfig = async () => {
const config = await readConfigFile();
Object.assign(process.env, config);
return config;
};
export const run = async (args: string[] = []) => {
const isRunning = isServiceRunning()
if (isRunning) {
console.log('claude-code-router server is running');
return;
}
const server = await getServer();
const app = server.app;
// Save the PID of the background process
writeFileSync(PID_FILE, process.pid.toString());
app.post('/api/update/perform', async () => {
return await performUpdate();
})
app.get('/api/update/check', async () => {
return await checkForUpdates(version);
})
app.post("/api/restart", async () => {
setTimeout(async () => {
spawn("ccr", ["restart"], {
detached: true,
stdio: "ignore",
}).unref();
}, 100);
return { success: true, message: "Service restart initiated" }
});
// await server.start() to ensure it starts successfully and keep process alive
await server.start();
}
export const restartService = async () => {
// Stop the service if it's running
try {
const pid = parseInt(readFileSync(PID_FILE, "utf-8"));
process.kill(pid);
cleanupPidFile();
if (existsSync(REFERENCE_COUNT_FILE)) {
try {
await fs.unlink(REFERENCE_COUNT_FILE);
} catch (e) {
// Ignore cleanup errors
}
}
console.log("claude code router service has been stopped.");
} catch (e) {
console.log("Service was not running or failed to stop.");
cleanupPidFile();
}
// Start the service again in the background
console.log("Starting claude code router service...");
const cliPath = path.join(__dirname, "../cli.js");
const startProcess = spawn("node", [cliPath, "start"], {
detached: true,
stdio: "ignore",
});
startProcess.on("error", (error) => {
console.error("Failed to start service:", error);
throw error;
});
startProcess.unref();
console.log("✅ Service started successfully in the background.");
};
/**
* 获取设置文件的临时路径
* 对内容进行 hash如果临时目录中已存在该 hash 的文件则直接返回,否则创建新文件
* @param content 设置内容字符串
* @returns 临时文件的完整路径
*/
export const getSettingsPath = (content: string): string => {
// 对内容进行 hash使用 SHA256 算法)
const hash = createHash('sha256').update(content, 'utf-8').digest('hex');
const fileName = `ccr-settings-${hash}.json`;
const tempFilePath = path.join(HOME_DIR, fileName);
// 检查文件是否已存在
if (existsSync(tempFilePath)) {
return tempFilePath;
}
// 文件不存在,创建并写入内容
writeFileSync(tempFilePath, content, 'utf-8');
return tempFilePath;
}