16 KiB
Target Adapters
Last Updated: 2026-02-16
Detailed documentation of the target adapter pattern and implementations.
Overview
The target adapter system enables CCS to dispatch credential-resolved profiles to different CLI implementations while maintaining a unified configuration and profile system.
Key insight: Profile resolution (detecting provider, loading auth, building credentials) is target-agnostic. Only the final credential delivery and process spawning differ per target.
Target Adapter Interface
Each CLI target implements the TargetAdapter contract:
export interface TargetAdapter {
readonly type: TargetType; // 'claude' | 'droid'
readonly displayName: string; // "Claude Code" | "Factory Droid"
/** Detect if the target CLI binary exists on system */
detectBinary(): TargetBinaryInfo | null;
/** Prepare credentials for delivery to target CLI */
prepareCredentials(creds: TargetCredentials): Promise<void>;
/** Build spawn arguments for the target CLI */
buildArgs(profile: string, userArgs: string[]): string[];
/** Build environment variables for the target CLI */
buildEnv(creds: TargetCredentials, profileType: string): NodeJS.ProcessEnv;
/** Spawn the target CLI process (replaces current process flow) */
exec(args: string[], env: NodeJS.ProcessEnv, options?: { cwd?: string }): void;
/** Check if a profile type is supported by this target */
supportsProfileType(profileType: string): boolean;
}
Type Definitions
export type TargetType = 'claude' | 'droid';
export interface TargetCredentials {
baseUrl: string; // API endpoint
apiKey: string; // Auth token
model?: string; // Model ID
provider?: 'anthropic' | 'openai' | 'generic-chat-completion-api';
envVars?: NodeJS.ProcessEnv; // Additional env vars
}
export interface TargetBinaryInfo {
path: string; // Full path to binary
needsShell: boolean; // Windows .cmd/.bat/.ps1?
}
Target Resolution
CCS resolves which adapter to use via priority-ordered checks:
Resolution Priority
1. --target flag (CLI argument) — highest priority
└─ ccs --target droid glm
2. Per-profile config (from ~/.ccs/config.yaml or settings.json)
└─ profiles:
glm:
target: droid
3. argv[0] detection (runtime alias pattern) — binary name mapping
└─ ccs-droid (explicit alias) → droid
└─ ccsd (legacy shortcut) → droid
└─ ccs (regular command) → default
4. Fallback: 'claude' — lowest priority
Implementation
// src/targets/target-resolver.ts
export function resolveTargetType(
args: string[],
profileConfig?: { target?: TargetType }
): TargetType {
// 1. Parse --target flags (supports --target value and --target=value)
// Repeated flags: last one wins.
const parsed = parseTargetFlags(args);
if (parsed.targetOverride) {
return parsed.targetOverride;
}
// 2. Check profile config
if (profileConfig?.target) {
return profileConfig.target;
}
// 3. Check argv[0] (binary name)
const binName = path.basename(process.argv[1] || process.argv0 || '').replace(/\.(cmd|bat|ps1|exe)$/i, '');
if (ARGV0_TARGET_MAP[binName]) {
return ARGV0_TARGET_MAP[binName];
}
// 4. Default to claude
return 'claude';
}
Claude Adapter
Implementation
// src/targets/claude-adapter.ts
export class ClaudeAdapter implements TargetAdapter {
readonly type: TargetType = 'claude';
readonly displayName = 'Claude Code';
detectBinary(): TargetBinaryInfo | null {
const info = getClaudeCliInfo();
if (!info) return null;
return { path: info.path, needsShell: info.needsShell };
}
async prepareCredentials(_creds: TargetCredentials): Promise<void> {
// No-op: Claude receives credentials via environment variables
}
buildArgs(_profile: string, userArgs: string[]): string[] {
return userArgs; // Pass through user arguments unchanged
}
buildEnv(creds: TargetCredentials, profileType: string): NodeJS.ProcessEnv {
const webSearchEnv = getWebSearchHookEnv();
// For native profiles, strip stale proxy env to prevent interference
const baseEnv =
profileType === 'account' || profileType === 'default'
? stripAnthropicEnv(process.env)
: process.env;
const env: NodeJS.ProcessEnv = { ...baseEnv, ...webSearchEnv };
if (creds.envVars) {
Object.assign(env, creds.envVars);
}
// Deliver credentials via environment variables
if (creds.baseUrl) env['ANTHROPIC_BASE_URL'] = creds.baseUrl;
if (creds.apiKey) env['ANTHROPIC_AUTH_TOKEN'] = creds.apiKey;
if (creds.model) env['ANTHROPIC_MODEL'] = creds.model;
return env;
}
exec(args: string[], env: NodeJS.ProcessEnv, _options?: { cwd?: string }): void {
const claudeCli = detectClaudeCli();
if (!claudeCli) {
void ErrorManager.showClaudeNotFound();
process.exit(1);
return;
}
// Handle Windows shell requirements
const isWindows = process.platform === 'win32';
const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(claudeCli);
let child: ChildProcess;
if (needsShell) {
const cmdString = [claudeCli, ...args].map(escapeShellArg).join(' ');
child = spawn(cmdString, { shell: true, stdio: 'inherit', env });
} else {
child = spawn(claudeCli, args, { stdio: 'inherit', env });
}
// Handle process termination
const onSigInt = () => child.kill('SIGINT');
const onSigTerm = () => child.kill('SIGTERM');
process.once('SIGINT', onSigInt);
process.once('SIGTERM', onSigTerm);
child.on('exit', () => {
process.removeListener('SIGINT', onSigInt);
process.removeListener('SIGTERM', onSigTerm);
});
}
supportsProfileType(profileType: string): boolean {
// Claude supports all profile types
return true;
}
}
Credential Delivery
Method: Environment variables
export ANTHROPIC_BASE_URL=https://api.anthropic.com
export ANTHROPIC_AUTH_TOKEN=sk-ant-...
export ANTHROPIC_MODEL=claude-opus-4-6
export WEBSEARCH_HOOK_ENV=... # Image analysis, websearch
Execution
# Direct invocation
ccs gemini
→ claude "args..."
with ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN set
# With --target override
ccs --target claude glm
→ claude "args..."
with ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN set
Droid Adapter
Implementation
// src/targets/droid-adapter.ts
export class DroidAdapter implements TargetAdapter {
readonly type: TargetType = 'droid';
readonly displayName = 'Factory Droid';
detectBinary(): TargetBinaryInfo | null {
const info = getDroidBinaryInfo();
if (!info) return null;
// Non-blocking version compatibility check
checkDroidVersion(info.path);
return info;
}
async prepareCredentials(creds: TargetCredentials): Promise<void> {
// Write custom model entry to ~/.factory/settings.json
await upsertCcsModel(creds.profile, {
model: creds.model || 'claude-opus-4-6',
displayName: `CCS ${creds.profile}`,
baseUrl: creds.baseUrl,
apiKey: creds.apiKey,
provider: creds.provider || 'anthropic',
});
}
buildArgs(profile: string, userArgs: string[]): string[] {
// Droid uses -m <model> syntax for model selection
return ['-m', `custom:ccs-${profile}`, ...userArgs];
}
buildEnv(_creds: TargetCredentials, _profileType: string): NodeJS.ProcessEnv {
// Droid reads from config file — minimal env needed
return { ...process.env };
}
exec(args: string[], env: NodeJS.ProcessEnv, _options?: { cwd?: string }): void {
const droidPath = detectDroidCli();
if (!droidPath) {
console.error('[X] Droid CLI not found. Install: npm i -g @factory/cli');
process.exit(1);
return;
}
// Handle Windows shell requirements
const isWindows = process.platform === 'win32';
const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(droidPath);
let child: ChildProcess;
if (needsShell) {
const cmdString = [droidPath, ...args].map(escapeShellArg).join(' ');
child = spawn(cmdString, { shell: true, stdio: 'inherit', env });
} else {
child = spawn(droidPath, args, { stdio: 'inherit', env });
}
// Handle process termination
const onSigInt = () => child.kill('SIGINT');
const onSigTerm = () => child.kill('SIGTERM');
process.once('SIGINT', onSigInt);
process.once('SIGTERM', onSigTerm);
child.on('exit', () => {
process.removeListener('SIGINT', onSigInt);
process.removeListener('SIGTERM', onSigTerm);
});
}
supportsProfileType(profileType: string): boolean {
// Droid currently supports direct settings/default paths only
return profileType === 'settings' || profileType === 'default';
}
}
Credential Delivery
Method: Config file (~/.factory/settings.json)
{
"customModels": [
{
"model": "claude-opus-4-6",
"displayName": "CCS gemini",
"baseUrl": "https://generativelanguage.googleapis.com/v1beta/openai/",
"apiKey": "AIza...",
"provider": "openai"
},
{
"model": "glm-4",
"displayName": "CCS glm",
"baseUrl": "https://open.bigmodel.cn/api/paas/v4/",
"apiKey": "your-glm-key",
"provider": "openai"
}
]
}
Execution
# Direct invocation
ccs gemini
→ droid -m custom:ccs-gemini "args..."
(credentials loaded from ~/.factory/settings.json)
# With --target override
ccs --target droid glm
→ droid -m custom:ccs-glm "args..."
(credentials loaded from ~/.factory/settings.json)
Runtime Alias Pattern
# Built-in package bin aliases
ccs-droid glm
→ Target: droid (forced by runtime alias)
→ droid -m custom:ccs-glm "args..."
# Legacy shortcut still works
ccsd glm
→ Target: droid (forced by runtime alias)
→ droid -m custom:ccs-glm "args..."
On Windows, ccs-droid.cmd, ccsd.cmd, ccsd.bat, ccsd.ps1, and ccsd.exe wrappers are also recognized.
Additional alias names can be configured at runtime after you create a matching
symlink or another launcher that preserves the invoked basename. Use CCS_TARGET_ALIASES (preferred,
target=alias1,alias2;...) or legacy CCS_DROID_ALIASES (comma-separated).
Example:
ln -s /path/to/ccs /path/to/mydroid
CCS_TARGET_ALIASES=droid=mydroid
Registry and Lookup
The target registry is a simple map-based store for adapters:
// src/targets/target-registry.ts
const adapters = new Map<TargetType, TargetAdapter>();
export function registerTarget(adapter: TargetAdapter): void {
adapters.set(adapter.type, adapter);
}
export function getTarget(type: TargetType): TargetAdapter {
const adapter = adapters.get(type);
if (!adapter) {
throw new Error(`Unknown target "${type}"`);
}
return adapter;
}
export function getDefaultTarget(): TargetAdapter {
return getTarget('claude');
}
Adapter Registration
At startup, adapters self-register:
// src/ccs.ts (initialization)
registerTarget(new ClaudeAdapter());
registerTarget(new DroidAdapter());
Execution Flow
Step-by-Step
1. Parse command-line arguments
└─ args: ['--target', 'droid', 'glm']
2. Resolve target type
└─ resolveTargetType(args) → 'droid'
└─ stripTargetFlag(args) → ['glm']
3. Detect and resolve profile
└─ detectProfile(['glm']) → { profile: 'glm', ... }
└─ Load credentials from config/CLIProxy/env
4. Build credentials object
└─ TargetCredentials {
baseUrl: '...',
apiKey: '...',
model: 'claude-opus-4-6',
envVars: { CCS_PROFILE_NAME: 'glm', ... }
}
5. Get target adapter
└─ getTarget('droid') → DroidAdapter instance
6. Prepare credentials
└─ adapter.prepareCredentials(creds)
└─ DroidAdapter: writes to ~/.factory/settings.json
7. Build spawn arguments
└─ adapter.buildArgs('glm', []) → ['-m', 'custom:ccs-glm']
8. Build environment
└─ adapter.buildEnv(creds, profileType) → process.env
9. Spawn target CLI
└─ adapter.exec(spawnArgs, env)
└─ exec spawn('droid', ['-m', 'custom:ccs-glm', ...])
10. Replace current process
└─ Child process inherits stdio
└─ Signal handlers propagate to child
Adding a New Target
To support a new CLI (e.g., MyAI CLI), follow this pattern:
1. Create Adapter Class
// src/targets/myai-adapter.ts
export class MyAiAdapter implements TargetAdapter {
readonly type: TargetType = 'myai';
readonly displayName = 'MyAI CLI';
detectBinary(): TargetBinaryInfo | null {
const path = which.sync('myai', { nothrow: true });
if (!path) return null;
return { path, needsShell: process.platform === 'win32' };
}
async prepareCredentials(creds: TargetCredentials): Promise<void> {
// Write to ~/.myai/config or similar
}
buildArgs(profile: string, userArgs: string[]): string[] {
return ['-p', profile, ...userArgs];
}
buildEnv(creds: TargetCredentials, _profileType: string): NodeJS.ProcessEnv {
return {
...process.env,
MYAI_API_KEY: creds.apiKey,
MYAI_API_URL: creds.baseUrl,
};
}
exec(args: string[], env: NodeJS.ProcessEnv): void {
const myaiPath = this.detectBinary()?.path;
if (!myaiPath) {
console.error('[X] MyAI CLI not found');
process.exit(1);
}
spawn(myaiPath, args, { stdio: 'inherit', env });
}
supportsProfileType(profileType: string): boolean {
return true; // or implement specific logic
}
}
2. Update Type Definition
// src/targets/target-adapter.ts
export type TargetType = 'claude' | 'droid' | 'myai';
3. Register in ccs.ts
registerTarget(new MyAiAdapter());
4. Update Documentation
- Add to Codebase Summary
- Update Code Standards adapter examples
- Document CLI-specific behavior
Cross-Platform Considerations
Windows Shell Detection
Both adapters check for shell-requiring binaries:
const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(binaryPath);
if (needsShell) {
const cmdString = [binaryPath, ...args].map(escapeShellArg).join(' ');
spawn(cmdString, { shell: true, stdio: 'inherit' });
} else {
spawn(binaryPath, args, { stdio: 'inherit' });
}
Environment Variable Escaping
Arguments passed to shell are escaped to prevent injection:
export function escapeShellArg(arg: string): string {
// Wrap in quotes and escape internal quotes
return `"${arg.replace(/"/g, '\\"')}"`;
}
Signal Handling
Both adapters propagate signals from parent to child:
const onSigInt = () => child.kill('SIGINT');
const onSigTerm = () => child.kill('SIGTERM');
process.once('SIGINT', onSigInt);
process.once('SIGTERM', onSigTerm);
child.on('exit', () => {
process.removeListener('SIGINT', onSigInt);
process.removeListener('SIGTERM', onSigTerm);
});
This ensures CTRL+C and graceful shutdowns work correctly.
Testing Target Adapters
Unit Tests
describe('ClaudeAdapter', () => {
it('detects Claude CLI', () => {
const adapter = new ClaudeAdapter();
const binary = adapter.detectBinary();
expect(binary).not.toBeNull();
});
it('builds env with credentials', () => {
const adapter = new ClaudeAdapter();
const env = adapter.buildEnv({
baseUrl: 'https://api.anthropic.com',
apiKey: 'sk-ant-...',
model: 'claude-opus-4-6',
}, 'cliproxy');
expect(env['ANTHROPIC_AUTH_TOKEN']).toBe('sk-ant-...');
});
});
Integration Tests
# Test Claude adapter
ccs --target claude help
# Test Droid adapter (if installed)
ccs --target droid help
# Test argv[0] detection
ccs-droid help
Related Documentation
- Codebase Summary — Module structure
- Code Standards — Adapter pattern guidelines
- System Architecture Index — Overall system design