Spaces:
Configuration error
Configuration error
| /** | |
| * Server Configuration Presets Utility | |
| * | |
| * Handles reading and writing server config presets. | |
| * Location: ~/.config/antigravity-proxy/server-presets.json | |
| */ | |
| import fs from 'fs/promises'; | |
| import path from 'path'; | |
| import os from 'os'; | |
| import { logger } from './logger.js'; | |
| import { DEFAULT_SERVER_PRESETS } from '../constants.js'; | |
| /** | |
| * Get the path to the server presets file | |
| * @returns {string} Absolute path to server-presets.json | |
| */ | |
| export function getServerPresetsPath() { | |
| return path.join(os.homedir(), '.config', 'antigravity-proxy', 'server-presets.json'); | |
| } | |
| /** | |
| * Read all server config presets. | |
| * Creates the file with default presets if it doesn't exist. | |
| * @returns {Promise<Array>} Array of preset objects | |
| */ | |
| export async function readServerPresets() { | |
| const presetsPath = getServerPresetsPath(); | |
| try { | |
| const content = await fs.readFile(presetsPath, 'utf8'); | |
| if (!content.trim()) return DEFAULT_SERVER_PRESETS; | |
| const userPresets = JSON.parse(content); | |
| // Merge: always include built-in presets (latest version), then user custom presets | |
| const builtInNames = new Set(DEFAULT_SERVER_PRESETS.map(p => p.name)); | |
| const customPresets = userPresets.filter(p => !builtInNames.has(p.name) && !p.builtIn); | |
| return [...DEFAULT_SERVER_PRESETS, ...customPresets]; | |
| } catch (error) { | |
| if (error.code === 'ENOENT') { | |
| try { | |
| await fs.mkdir(path.dirname(presetsPath), { recursive: true }); | |
| await fs.writeFile(presetsPath, JSON.stringify(DEFAULT_SERVER_PRESETS, null, 2), 'utf8'); | |
| logger.info(`[ServerPresets] Created presets file with defaults at ${presetsPath}`); | |
| } catch (writeError) { | |
| logger.warn(`[ServerPresets] Could not create presets file: ${writeError.message}`); | |
| } | |
| return DEFAULT_SERVER_PRESETS; | |
| } | |
| if (error instanceof SyntaxError) { | |
| logger.error(`[ServerPresets] Invalid JSON in presets at ${presetsPath}. Returning defaults.`); | |
| return DEFAULT_SERVER_PRESETS; | |
| } | |
| logger.error(`[ServerPresets] Failed to read presets at ${presetsPath}:`, error.message); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Save a custom server preset (add or update). | |
| * Rejects overwriting built-in presets. | |
| * @param {string} name - Preset name | |
| * @param {Object} config - Server configuration values | |
| * @param {string} [description] - Optional user description | |
| * @returns {Promise<Array>} Updated array of all presets | |
| */ | |
| export async function saveServerPreset(name, config, description) { | |
| // Reject overwriting built-in presets | |
| const builtInNames = new Set(DEFAULT_SERVER_PRESETS.map(p => p.name)); | |
| if (builtInNames.has(name)) { | |
| throw new Error(`Cannot overwrite built-in preset "${name}"`); | |
| } | |
| const presetsPath = getServerPresetsPath(); | |
| let allPresets = await readServerPresets(); | |
| // Find or create user custom preset | |
| const existingIndex = allPresets.findIndex(p => p.name === name && !p.builtIn); | |
| const newPreset = { name, config: { ...config } }; | |
| if (description && typeof description === 'string' && description.trim()) { | |
| newPreset.description = description.trim(); | |
| } | |
| if (existingIndex >= 0) { | |
| allPresets[existingIndex] = newPreset; | |
| logger.info(`[ServerPresets] Updated preset: ${name}`); | |
| } else { | |
| allPresets.push(newPreset); | |
| logger.info(`[ServerPresets] Created preset: ${name}`); | |
| } | |
| try { | |
| await fs.mkdir(path.dirname(presetsPath), { recursive: true }); | |
| await fs.writeFile(presetsPath, JSON.stringify(allPresets, null, 2), 'utf8'); | |
| } catch (error) { | |
| logger.error(`[ServerPresets] Failed to save preset:`, error.message); | |
| throw error; | |
| } | |
| return allPresets; | |
| } | |
| /** | |
| * Update metadata (name, description) of a custom server preset. | |
| * Rejects editing built-in presets. | |
| * @param {string} currentName - Current preset name | |
| * @param {Object} updates - Fields to update ({ name, description }) | |
| * @returns {Promise<Array>} Updated array of all presets | |
| */ | |
| export async function updateServerPreset(currentName, updates) { | |
| const builtInNames = new Set(DEFAULT_SERVER_PRESETS.map(p => p.name)); | |
| if (builtInNames.has(currentName)) { | |
| throw new Error(`Cannot edit built-in preset "${currentName}"`); | |
| } | |
| const presetsPath = getServerPresetsPath(); | |
| let allPresets = await readServerPresets(); | |
| const index = allPresets.findIndex(p => p.name === currentName && !p.builtIn); | |
| if (index < 0) { | |
| throw new Error(`Preset "${currentName}" not found`); | |
| } | |
| // Check new name doesn't collide with a built-in | |
| if (updates.name && builtInNames.has(updates.name)) { | |
| throw new Error(`Cannot use built-in preset name "${updates.name}"`); | |
| } | |
| // Check new name doesn't collide with another custom preset | |
| if (updates.name && updates.name !== currentName) { | |
| const conflict = allPresets.findIndex(p => p.name === updates.name && !p.builtIn); | |
| if (conflict >= 0) { | |
| throw new Error(`A preset named "${updates.name}" already exists`); | |
| } | |
| } | |
| if (updates.name) { | |
| allPresets[index].name = updates.name.trim(); | |
| } | |
| if (updates.description !== undefined) { | |
| if (updates.description && updates.description.trim()) { | |
| allPresets[index].description = updates.description.trim(); | |
| } else { | |
| delete allPresets[index].description; | |
| } | |
| } | |
| // Merge config updates if provided | |
| if (updates.config && typeof updates.config === 'object') { | |
| const allowedKeys = [ | |
| 'maxRetries', 'retryBaseMs', 'retryMaxMs', 'defaultCooldownMs', | |
| 'maxWaitBeforeErrorMs', 'maxAccounts', 'globalQuotaThreshold', | |
| 'rateLimitDedupWindowMs', 'maxConsecutiveFailures', 'extendedCooldownMs', | |
| 'maxCapacityRetries', 'switchAccountDelayMs', 'capacityBackoffTiersMs', | |
| 'accountSelection' | |
| ]; | |
| const existing = allPresets[index].config || {}; | |
| for (const key of allowedKeys) { | |
| if (updates.config[key] !== undefined) { | |
| if (key === 'accountSelection') { | |
| const updateAS = updates.config.accountSelection; | |
| if (!updateAS || typeof updateAS !== 'object') continue; | |
| // Deep merge accountSelection sub-objects to preserve fields not in partial update | |
| const existingAS = existing.accountSelection || {}; | |
| existing.accountSelection = { ...existingAS }; | |
| if (updateAS.strategy !== undefined) existing.accountSelection.strategy = updateAS.strategy; | |
| for (const subKey of ['healthScore', 'tokenBucket', 'quota', 'weights']) { | |
| if (updateAS[subKey] && typeof updateAS[subKey] === 'object') { | |
| existing.accountSelection[subKey] = { ...existingAS[subKey], ...updateAS[subKey] }; | |
| } | |
| } | |
| } else { | |
| existing[key] = updates.config[key]; | |
| } | |
| } | |
| } | |
| allPresets[index].config = existing; | |
| } | |
| const hasConfigChange = updates.config && Object.keys(updates.config).length > 0; | |
| const hasNameChange = updates.name && updates.name !== currentName; | |
| try { | |
| await fs.mkdir(path.dirname(presetsPath), { recursive: true }); | |
| await fs.writeFile(presetsPath, JSON.stringify(allPresets, null, 2), 'utf8'); | |
| logger.info(`[ServerPresets] Updated preset${hasConfigChange ? ' config' : ' metadata'}: ${currentName}${hasNameChange ? ` → ${updates.name}` : ''}`); | |
| } catch (error) { | |
| logger.error(`[ServerPresets] Failed to update preset:`, error.message); | |
| throw error; | |
| } | |
| return allPresets; | |
| } | |
| /** | |
| * Delete a custom server preset by name. | |
| * Rejects deletion of built-in presets. | |
| * @param {string} name - Preset name to delete | |
| * @returns {Promise<Array>} Updated array of all presets | |
| */ | |
| export async function deleteServerPreset(name) { | |
| // Reject deleting built-in presets | |
| const builtInNames = new Set(DEFAULT_SERVER_PRESETS.map(p => p.name)); | |
| if (builtInNames.has(name)) { | |
| throw new Error(`Cannot delete built-in preset "${name}"`); | |
| } | |
| const presetsPath = getServerPresetsPath(); | |
| let allPresets = await readServerPresets(); | |
| const originalLength = allPresets.length; | |
| allPresets = allPresets.filter(p => p.name !== name); | |
| if (allPresets.length === originalLength) { | |
| logger.warn(`[ServerPresets] Preset not found: ${name}`); | |
| return allPresets; | |
| } | |
| try { | |
| await fs.writeFile(presetsPath, JSON.stringify(allPresets, null, 2), 'utf8'); | |
| logger.info(`[ServerPresets] Deleted preset: ${name}`); | |
| } catch (error) { | |
| logger.error(`[ServerPresets] Failed to delete preset:`, error.message); | |
| throw error; | |
| } | |
| return allPresets; | |
| } | |