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
This commit is contained in:
Tam Nhu Tran committed 2026-02-12 11:01:49 +07:00
1 parent 6ff17d8480
commit 74868972dd
2 files changed
+26 -9

No files matched your search

+18 -6
View File
@@ -15,7 +15,7 @@ import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
import { DEFAULT_BACKEND } from '../platform-detector'; import { DEFAULT_BACKEND } from '../platform-detector';
import { isUnifiedMode } from '../../config/unified-config-loader'; import { isUnifiedMode } from '../../config/unified-config-loader';
import { deleteConfigForPort } from '../config-generator'; import { deleteConfigForPort } from '../config-generator';
import { deleteSessionLockForPort } from '../session-tracker'; import { hasActiveSessions, deleteSessionLockForPort } from '../session-tracker';
import { warn } from '../../utils/ui'; import { warn } from '../../utils/ui';
import { import {
createSettingsFile, createSettingsFile,
@@ -154,6 +154,22 @@ export function createVariant(
*/ */
export function removeVariant(name: string): VariantOperationResult { export function removeVariant(name: string): VariantOperationResult {
try { 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; let variant: VariantConfig | null;
if (isUnifiedMode()) { if (isUnifiedMode()) {
@@ -179,11 +195,7 @@ export function removeVariant(name: string): VariantOperationResult {
} }
} }
if (!variant) { return { success: true, variant: variant ?? undefined };
return { success: false, error: `Variant '${name}' not found` };
}
return { success: true, variant };
} catch (error) { } catch (error) {
return { success: false, error: (error as Error).message }; return { success: false, error: (error as Error).message };
} }
+8 -3
View File
@@ -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 { 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<string, string>).ANTHROPIC_MODEL; delete (settings.env as unknown as Record<string, string>).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 { } catch {
// Ignore errors - settings file may be invalid // Ignore errors - settings file may be invalid
} }