mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-21 22:23:34 +00:00
fix(targets): sanitize invalid persisted target values
- fallback unknown persisted targets to claude in profile listing and runtime resolution - add regression test for invalid profile target fallback
This commit is contained in:
@@ -12,6 +12,15 @@ import { loadOrCreateUnifiedConfig, isUnifiedMode } from '../../config/unified-c
|
|||||||
import type { TargetType } from '../../targets/target-adapter';
|
import type { TargetType } from '../../targets/target-adapter';
|
||||||
import type { ApiProfileInfo, CliproxyVariantInfo, ApiListResult } from './profile-types';
|
import type { ApiProfileInfo, CliproxyVariantInfo, ApiListResult } from './profile-types';
|
||||||
|
|
||||||
|
const VALID_TARGETS: ReadonlySet<TargetType> = new Set<TargetType>(['claude', 'droid']);
|
||||||
|
|
||||||
|
function sanitizeTarget(target: unknown): TargetType {
|
||||||
|
if (typeof target === 'string' && VALID_TARGETS.has(target as TargetType)) {
|
||||||
|
return target as TargetType;
|
||||||
|
}
|
||||||
|
return 'claude';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if API profile exists in config
|
* Check if API profile exists in config
|
||||||
*/
|
*/
|
||||||
@@ -69,7 +78,7 @@ export function listApiProfiles(): ApiListResult {
|
|||||||
settingsPath: profile.settings || 'config.yaml',
|
settingsPath: profile.settings || 'config.yaml',
|
||||||
isConfigured: isApiProfileConfigured(name),
|
isConfigured: isApiProfileConfigured(name),
|
||||||
configSource: 'unified',
|
configSource: 'unified',
|
||||||
target: profile.target || 'claude',
|
target: sanitizeTarget(profile.target),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// CLIProxy variants
|
// CLIProxy variants
|
||||||
@@ -82,12 +91,12 @@ export function listApiProfiles(): ApiListResult {
|
|||||||
name,
|
name,
|
||||||
provider,
|
provider,
|
||||||
settings: variant?.settings || '-',
|
settings: variant?.settings || '-',
|
||||||
target: variant?.target || 'claude',
|
target: sanitizeTarget(variant?.target),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const config = loadConfigSafe();
|
const config = loadConfigSafe();
|
||||||
const legacyTargetMap = (config as { profile_targets?: Record<string, TargetType> })
|
const legacyTargetMap = (config as { profile_targets?: Record<string, unknown> })
|
||||||
.profile_targets;
|
.profile_targets;
|
||||||
for (const [name, settingsPath] of Object.entries(config.profiles)) {
|
for (const [name, settingsPath] of Object.entries(config.profiles)) {
|
||||||
// Skip 'default' profile - it's the user's native Claude settings
|
// Skip 'default' profile - it's the user's native Claude settings
|
||||||
@@ -99,18 +108,18 @@ export function listApiProfiles(): ApiListResult {
|
|||||||
settingsPath: settingsPath as string,
|
settingsPath: settingsPath as string,
|
||||||
isConfigured: isApiProfileConfigured(name),
|
isConfigured: isApiProfileConfigured(name),
|
||||||
configSource: 'legacy',
|
configSource: 'legacy',
|
||||||
target: legacyTargetMap?.[name] || 'claude',
|
target: sanitizeTarget(legacyTargetMap?.[name]),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// CLIProxy variants
|
// CLIProxy variants
|
||||||
if (config.cliproxy) {
|
if (config.cliproxy) {
|
||||||
for (const [name, v] of Object.entries(config.cliproxy)) {
|
for (const [name, v] of Object.entries(config.cliproxy)) {
|
||||||
const variant = v as { provider: string; settings: string; target?: TargetType };
|
const variant = v as { provider: string; settings: string; target?: unknown };
|
||||||
variants.push({
|
variants.push({
|
||||||
name,
|
name,
|
||||||
provider: variant.provider,
|
provider: variant.provider,
|
||||||
settings: variant.settings,
|
settings: variant.settings,
|
||||||
target: variant.target || 'claude',
|
target: sanitizeTarget(variant.target),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,9 +48,13 @@ interface ParsedTargetFlags {
|
|||||||
cleanedArgs: string[];
|
cleanedArgs: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isValidTarget(target: unknown): target is TargetType {
|
||||||
|
return typeof target === 'string' && VALID_TARGETS.has(target as TargetType);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeTargetValue(value: string): TargetType {
|
function normalizeTargetValue(value: string): TargetType {
|
||||||
const normalized = value.toLowerCase();
|
const normalized = value.toLowerCase();
|
||||||
if (VALID_TARGETS.has(normalized)) {
|
if (isValidTarget(normalized)) {
|
||||||
return normalized as TargetType;
|
return normalized as TargetType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,8 +124,8 @@ export function resolveTargetType(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check per-profile config
|
// 2. Check per-profile config
|
||||||
if (profileConfig?.target) {
|
if (profileConfig?.target !== undefined) {
|
||||||
return profileConfig.target;
|
return isValidTarget(profileConfig.target) ? profileConfig.target : 'claude';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Check argv[0] (busybox pattern)
|
// 3. Check argv[0] (busybox pattern)
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ describe('resolveTargetType', () => {
|
|||||||
expect(resolveTargetType([], { target: 'droid' })).toBe('droid');
|
expect(resolveTargetType([], { target: 'droid' })).toBe('droid');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should fallback to claude when persisted profile target is invalid', () => {
|
||||||
|
process.argv = ['node', 'ccs'];
|
||||||
|
expect(resolveTargetType([], { target: 'invalid-target' as never })).toBe('claude');
|
||||||
|
});
|
||||||
|
|
||||||
it('should prioritize --target flag over profile config', () => {
|
it('should prioritize --target flag over profile config', () => {
|
||||||
process.argv = ['node', 'ccs'];
|
process.argv = ['node', 'ccs'];
|
||||||
expect(resolveTargetType(['--target', 'claude'], { target: 'droid' })).toBe('claude');
|
expect(resolveTargetType(['--target', 'claude'], { target: 'droid' })).toBe('claude');
|
||||||
|
|||||||
Reference in New Issue
Block a user