diff --git a/src/cliproxy/sync/profile-mapper.ts b/src/cliproxy/sync/profile-mapper.ts index d9b8b995..40583fa4 100644 --- a/src/cliproxy/sync/profile-mapper.ts +++ b/src/cliproxy/sync/profile-mapper.ts @@ -7,6 +7,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { getCcsDir } from '../../utils/config-manager'; +import { expandPath } from '../../utils/helpers'; import { listApiProfiles, isApiProfileConfigured } from '../../api/services/profile-reader'; import type { ClaudeKey } from '../management-api-types'; @@ -31,6 +32,15 @@ interface SettingsJson { env?: Record; } +function resolveProfileSettingsPath(settingsPath: string): string { + const normalized = settingsPath.replace(/\\/g, '/'); + if (normalized.startsWith('~/.ccs/')) { + return path.join(getCcsDir(), normalized.slice('~/.ccs/'.length)); + } + + return expandPath(settingsPath); +} + /** * Load syncable API profiles from CCS config. * Filters to only configured profiles (with real API keys). @@ -45,9 +55,14 @@ export function loadSyncableProfiles(): SyncableProfile[] { continue; } + // Local CLIProxy sync writes Claude-compatible entries only. + // Profiles pinned to non-claude targets are intentionally skipped. + if (profile.target !== 'claude') { + continue; + } + // Load settings.json for env vars - const ccsDir = getCcsDir(); - const settingsPath = path.join(ccsDir, `${profile.name}.settings.json`); + const settingsPath = resolveProfileSettingsPath(profile.settingsPath); let env: Record | undefined; try { diff --git a/tests/unit/cliproxy/profile-mapper.test.ts b/tests/unit/cliproxy/profile-mapper.test.ts index 55ccb4e6..c95f2c71 100644 --- a/tests/unit/cliproxy/profile-mapper.test.ts +++ b/tests/unit/cliproxy/profile-mapper.test.ts @@ -4,9 +4,11 @@ */ import * as assert from 'assert'; +const fs = require('fs'); describe('Profile Mapper', () => { const profileMapper = require('../../../dist/cliproxy/sync/profile-mapper'); + const profileReader = require('../../../dist/api/services/profile-reader'); describe('mapProfileToClaudeKey', () => { it('returns null when env is missing', () => { @@ -86,6 +88,100 @@ describe('Profile Mapper', () => { const result = profileMapper.loadSyncableProfiles(); assert.ok(Array.isArray(result)); }); + + it('uses profile-provided settingsPath instead of reconstructing from profile name', () => { + const originalListApiProfiles = profileReader.listApiProfiles; + const originalExistsSync = fs.existsSync; + const originalReadFileSync = fs.readFileSync; + + const customSettingsPath = '/tmp/custom-sync-path.settings.json'; + const readPaths: string[] = []; + + try { + profileReader.listApiProfiles = () => ({ + profiles: [ + { + name: 'glm', + settingsPath: customSettingsPath, + isConfigured: true, + configSource: 'legacy', + target: 'claude', + }, + ], + variants: [], + }); + + fs.existsSync = (filePath: string) => filePath === customSettingsPath; + fs.readFileSync = (filePath: string) => { + readPaths.push(filePath); + return JSON.stringify({ + env: { + ANTHROPIC_AUTH_TOKEN: 'sk-test-key', + }, + }); + }; + + const result = profileMapper.loadSyncableProfiles(); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].settingsPath, customSettingsPath); + assert.deepStrictEqual(readPaths, [customSettingsPath]); + } finally { + profileReader.listApiProfiles = originalListApiProfiles; + fs.existsSync = originalExistsSync; + fs.readFileSync = originalReadFileSync; + } + }); + + it('skips profiles pinned to non-claude targets during local sync mapping', () => { + const originalListApiProfiles = profileReader.listApiProfiles; + const originalExistsSync = fs.existsSync; + const originalReadFileSync = fs.readFileSync; + + const claudePath = '/tmp/claude-target.settings.json'; + const droidPath = '/tmp/droid-target.settings.json'; + const readPaths: string[] = []; + + try { + profileReader.listApiProfiles = () => ({ + profiles: [ + { + name: 'claude-profile', + settingsPath: claudePath, + isConfigured: true, + configSource: 'legacy', + target: 'claude', + }, + { + name: 'droid-profile', + settingsPath: droidPath, + isConfigured: true, + configSource: 'legacy', + target: 'droid', + }, + ], + variants: [], + }); + + fs.existsSync = (filePath: string) => filePath === claudePath || filePath === droidPath; + fs.readFileSync = (filePath: string) => { + readPaths.push(filePath); + return JSON.stringify({ + env: { + ANTHROPIC_AUTH_TOKEN: 'sk-test-key', + }, + }); + }; + + const result = profileMapper.loadSyncableProfiles(); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].name, 'claude-profile'); + assert.deepStrictEqual(readPaths, [claudePath]); + } finally { + profileReader.listApiProfiles = originalListApiProfiles; + fs.existsSync = originalExistsSync; + fs.readFileSync = originalReadFileSync; + } + }); }); describe('generateSyncPayload', () => {