Files
claude-code-router/src/utils/index.ts
2025-09-03 09:58:09 +08:00

181 lines
5.4 KiB
TypeScript

import fs from "node:fs/promises";
import readline from "node:readline";
import JSON5 from "json5";
import path from "node:path";
import {
CONFIG_FILE,
DEFAULT_CONFIG,
HOME_DIR,
PLUGINS_DIR,
} from "../constants";
import { cleanupLogFiles } from "./logCleanup";
// 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(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 { cleanupLogFiles };
// 导出更新功能
export { checkForUpdates, performUpdate } from "./update";