diff --git a/src/cliproxy/sync/auto-sync-watcher.ts b/src/cliproxy/sync/auto-sync-watcher.ts index dd60d48b..bd264861 100644 --- a/src/cliproxy/sync/auto-sync-watcher.ts +++ b/src/cliproxy/sync/auto-sync-watcher.ts @@ -149,7 +149,15 @@ export async function stopAutoSyncWatcher(): Promise { } if (watcherInstance) { - await watcherInstance.close(); + const closePromise = watcherInstance.close(); + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Close timeout')), 5000) + ); + try { + await Promise.race([closePromise, timeoutPromise]); + } catch (err) { + log(`Warning: ${(err as Error).message}, forcing cleanup`); + } watcherInstance = null; log('Watcher stopped'); } diff --git a/src/cliproxy/sync/local-config-sync.ts b/src/cliproxy/sync/local-config-sync.ts index d52a03eb..1d980033 100644 --- a/src/cliproxy/sync/local-config-sync.ts +++ b/src/cliproxy/sync/local-config-sync.ts @@ -47,7 +47,20 @@ export function syncToLocalConfig(): { }; } - const configContent = fs.readFileSync(configPath, 'utf8'); + let configContent: string; + try { + configContent = fs.readFileSync(configPath, 'utf8'); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return { + success: false, + syncedCount: 0, + configPath, + error: 'CLIProxy config deleted during sync. Run ccs doctor to regenerate.', + }; + } + throw error; + } // Transform payload to config format const claudeApiKeys = payload.map(transformToConfigFormat); @@ -198,10 +211,12 @@ export function getLocalSyncStatus(): { if (fs.existsSync(configPath)) { try { const content = fs.readFileSync(configPath, 'utf8'); - const config = yaml.load(content) as Record; - const keys = config['claude-api-key']; - if (Array.isArray(keys)) { - currentKeyCount = keys.length; + const config = yaml.load(content) as Record | null; + if (config && typeof config === 'object') { + const keys = config['claude-api-key']; + if (Array.isArray(keys)) { + currentKeyCount = keys.length; + } } } catch { // Ignore parse errors diff --git a/src/cliproxy/sync/profile-mapper.ts b/src/cliproxy/sync/profile-mapper.ts index 9b3b6a6e..d9b8b995 100644 --- a/src/cliproxy/sync/profile-mapper.ts +++ b/src/cliproxy/sync/profile-mapper.ts @@ -104,6 +104,17 @@ export function mapProfileToClaudeKey(profile: SyncableProfile): ClaudeKey | nul if (!sanitizedName || sanitizedName === '') { return null; // Skip profiles with invalid names } + + // Skip if name is too long (>64 chars) + if (sanitizedName.length > 64) { + return null; + } + + // Skip if name has no alphanumeric characters (e.g., only special chars -> "-----") + if (!/[a-zA-Z0-9]/.test(sanitizedName)) { + return null; + } + const prefix = `${sanitizedName}-`; const claudeKey: ClaudeKey = { diff --git a/ui/src/hooks/use-cliproxy-sync.ts b/ui/src/hooks/use-cliproxy-sync.ts index 392effdf..e468c7b5 100644 --- a/ui/src/hooks/use-cliproxy-sync.ts +++ b/ui/src/hooks/use-cliproxy-sync.ts @@ -89,23 +89,36 @@ async function fetchSyncPreview(): Promise { * Execute sync to remote CLIProxy */ async function executeSync(): Promise { - const response = await fetch('/api/cliproxy/sync', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - }); + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 30000); - if (!response.ok) { - let message = 'Sync failed'; - try { - const data = await response.json(); - message = data.error || data.message || message; - } catch { - // Non-JSON response (e.g., 502 Bad Gateway) + try { + const response = await fetch('/api/cliproxy/sync', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + signal: controller.signal, + }); + + if (!response.ok) { + let message = 'Sync failed'; + try { + const data = await response.json(); + message = data.error || data.message || message; + } catch { + // Non-JSON response (e.g., 502 Bad Gateway) + } + throw new Error(message); } - throw new Error(message); - } - return response.json(); + return response.json(); + } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + throw new Error('Sync request timed out after 30 seconds'); + } + throw error; + } finally { + clearTimeout(timeoutId); + } } /**