181 lines
5.4 KiB
TypeScript
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";
|