From 74868972ddbc9c4dcb689ea6a3e27a52893f6935 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 12 Feb 2026 11:01:49 +0700 Subject: [PATCH] fix(cliproxy): add session check on delete and atomic settings writes - Block variant deletion when CLIProxy has active sessions - Use temp file + rename pattern for atomic settings writes - Prevents partial writes on crash/interruption --- src/cliproxy/services/variant-service.ts | 24 +++++++++++++++++------ src/cliproxy/services/variant-settings.ts | 11 ++++++++--- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/cliproxy/services/variant-service.ts b/src/cliproxy/services/variant-service.ts index 2863d2e5..0ab7c1c8 100644 --- a/src/cliproxy/services/variant-service.ts +++ b/src/cliproxy/services/variant-service.ts @@ -15,7 +15,7 @@ import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader'; import { DEFAULT_BACKEND } from '../platform-detector'; import { isUnifiedMode } from '../../config/unified-config-loader'; import { deleteConfigForPort } from '../config-generator'; -import { deleteSessionLockForPort } from '../session-tracker'; +import { hasActiveSessions, deleteSessionLockForPort } from '../session-tracker'; import { warn } from '../../utils/ui'; import { createSettingsFile, @@ -154,6 +154,22 @@ export function createVariant( */ export function removeVariant(name: string): VariantOperationResult { try { + // First check if variant exists and has active sessions + const variants = listVariantsFromConfig(); + const existingVariant = variants[name]; + + if (!existingVariant) { + return { success: false, error: `Variant '${name}' not found` }; + } + + // Check for active sessions on this variant's port before deletion + if (existingVariant.port && hasActiveSessions(existingVariant.port)) { + return { + success: false, + error: `Cannot delete variant '${name}': CLIProxy is running with active sessions. Stop the session first.`, + }; + } + let variant: VariantConfig | null; if (isUnifiedMode()) { @@ -179,11 +195,7 @@ export function removeVariant(name: string): VariantOperationResult { } } - if (!variant) { - return { success: false, error: `Variant '${name}' not found` }; - } - - return { success: true, variant }; + return { success: true, variant: variant ?? undefined }; } catch (error) { return { success: false, error: (error as Error).message }; } diff --git a/src/cliproxy/services/variant-settings.ts b/src/cliproxy/services/variant-settings.ts index 01fc3e49..7f37853d 100644 --- a/src/cliproxy/services/variant-settings.ts +++ b/src/cliproxy/services/variant-settings.ts @@ -63,10 +63,13 @@ function ensureDir(dir: string): void { } /** - * Write settings file atomically + * Write settings file atomically using temp file + rename. + * The renameSync is atomic on POSIX systems, preventing partial writes on crash. */ function writeSettings(filePath: string, settings: SettingsFile): void { - fs.writeFileSync(filePath, JSON.stringify(settings, null, 2) + '\n', 'utf8'); + const tempPath = `${filePath}.tmp.${process.pid}`; + fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2) + '\n', 'utf8'); + fs.renameSync(tempPath, filePath); } /** @@ -248,7 +251,9 @@ export function updateSettingsModel(settingsPath: string, model: string): void { delete (settings.env as unknown as Record).ANTHROPIC_MODEL; } - fs.writeFileSync(resolvedPath, JSON.stringify(settings, null, 2) + '\n', 'utf8'); + const tempPath = `${resolvedPath}.tmp.${process.pid}`; + fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2) + '\n', 'utf8'); + fs.renameSync(tempPath, resolvedPath); } catch { // Ignore errors - settings file may be invalid }