refactor(dispatcher): extract per-profile flows from ccs.ts

Phases 5+6 of #1165. Final big extraction: collapses the 6-branch
profileInfo.type switch in main() into a single dispatchProfile call.

- src/dispatcher/dispatcher-context.ts (40 LOC): ProfileDispatchContext type
- src/dispatcher/flows/cliproxy-flow.ts (211 LOC)
- src/dispatcher/flows/copilot-flow.ts (66 LOC)
- src/dispatcher/flows/cursor-flow.ts (54 LOC)
- src/dispatcher/flows/settings-flow.ts (400 LOC)
- src/dispatcher/flows/settings-image-analysis-prep.ts (126 LOC):
  split out per plan to keep settings-flow control flow whole
- src/dispatcher/flows/account-flow.ts (63 LOC)
- src/dispatcher/flows/default-flow.ts (136 LOC)
- src/dispatcher/target-executor.ts (+38 LOC): dispatchProfile switch

settings-flow stays at 400 LOC — flow control (3 pre-flight blocks +
env construction + 2 dispatch paths) is maximally cohesive. Further
splitting would fragment dispatch logic per plan note.

process.exit parity preserved exactly (cliproxy non-claude path uses
process.exitCode=1; return; others use process.exit(1)).

ccs.ts: 1040 -> 170 LOC (-870). Full reduction across phases 1-6:
1775 -> 170 (-1605, -90%). main() body now ~100 LOC of awaited phase
calls. Behavior unchanged; full suite passes 1824/1824.

Refs #1165
This commit is contained in:
Tam Nhu Tran
2026-05-02 23:52:25 -04:00
parent a807de6f4c
commit 0910a75850
10 changed files with 1153 additions and 889 deletions
+18 -888
View File
@@ -1,84 +1,17 @@
import './utils/fetch-proxy-setup';
import { getSettingsPath, loadSettings } from './utils/config-manager';
import { expandPath } from './utils/helpers';
import {
validateGlmKey,
validateMiniMaxKey,
validateAnthropicKey,
} from './utils/api-key-validator';
import { ErrorManager } from './utils/error-manager';
import {
execClaudeWithCLIProxy,
CLIProxyProvider,
ensureCliproxyService,
isAuthenticated,
} from './cliproxy';
import { getEffectiveEnvVars, getCompositeEnvVars } from './cliproxy/config/env-builder';
import { CLIPROXY_DEFAULT_PORT } from './cliproxy/config/port-manager';
import {
ensureWebSearchMcpOrThrow,
displayWebSearchStatus,
getWebSearchHookEnv,
syncWebSearchMcpToConfigDir,
appendThirdPartyWebSearchToolArgs,
createWebSearchTraceContext,
} from './utils/websearch-manager';
import {
ensureImageAnalysisMcpOrThrow,
syncImageAnalysisMcpToConfigDir,
appendThirdPartyImageAnalysisToolArgs,
} from './utils/image-analysis';
import {
appendBrowserToolArgs,
ensureBrowserMcpOrThrow,
resolveOptionalBrowserAttachRuntime,
syncBrowserMcpToConfigDir,
} from './utils/browser';
import { getGlobalEnvConfig } from './config/unified-config-loader';
import {
ensureProfileHooks as ensureImageAnalyzerHooks,
removeImageAnalysisProfileHook,
} from './utils/hooks/image-analyzer-profile-hook-injector';
import {
applyImageAnalysisRuntimeOverrides,
getImageAnalysisHookEnv,
installImageAnalyzerHook,
prepareImageAnalysisFallbackHook,
resolveImageAnalysisRuntimeConnection,
resolveImageAnalysisRuntimeStatus,
} from './utils/hooks';
import { fail, info, warn } from './utils/ui';
import { fail } from './utils/ui';
// Import centralized error handling
import { handleError, runCleanup } from './errors';
// Import extracted utility functions
import { execClaude, stripAnthropicRoutingEnv, stripBrowserEnv } from './utils/shell-executor';
import { isDeprecatedGlmtProfileName, normalizeDeprecatedGlmtEnv } from './utils/glmt-deprecation';
import { createOpenAICompatLaunchSettings } from './utils/openai-compat-launch-settings';
import { maybeWarnAboutResumeLaneMismatch } from './auth/resume-lane-warning';
import { createLogger, runWithRequestId } from './services/logging';
// Import target adapter system
import {
registerTarget,
ClaudeAdapter,
DroidAdapter,
CodexAdapter,
evaluateTargetRuntimeCompatibility,
resolveDroidProvider,
type TargetCredentials,
} from './targets';
import { resolveCliproxyBridgeMetadata } from './api/services/cliproxy-profile-bridge';
import {
buildOpenAICompatProxyEnv,
resolveOpenAICompatProfileConfig,
startOpenAICompatProxy,
} from './proxy';
import { registerTarget, ClaudeAdapter, DroidAdapter, CodexAdapter } from './targets';
// Import extracted dispatcher modules
import { bootstrapAndParseEarlyCli } from './dispatcher/cli-argument-parser';
import { resolveNativeClaudeLaunchArgs } from './dispatcher/environment-builder';
import { type ProfileError } from './dispatcher/target-executor';
import { type ProfileError, dispatchProfile } from './dispatcher/target-executor';
import { runPreDispatchHandlers } from './dispatcher/pre-dispatch';
import { resolveProfileAndTarget } from './dispatcher/profile-resolver';
@@ -113,36 +46,10 @@ async function main(): Promise<void> {
}
// Phase C: profile + target detection (extracted to dispatcher/profile-resolver.ts)
const {
profile,
remainingArgs,
profileInfo,
resolvedTarget,
claudeCli,
targetAdapter,
targetBinaryInfo,
resolvedSettingsPath: initialResolvedSettingsPath,
resolvedSettings: initialResolvedSettings,
resolvedCliproxyBridge: initialResolvedCliproxyBridge,
targetRemainingArgs: initialTargetRemainingArgs,
nativeClaudeRemainingArgs: initialNativeClaudeRemainingArgs,
runtimeReasoningOverride: initialRuntimeReasoningOverride,
codexRuntimeConfigOverrides,
claudeBrowserExposure,
codexBrowserExposure: _codexBrowserExposure,
claudeAttachConfig,
detector: _detector,
} = await resolveProfileAndTarget({ args, browserLaunchOverride, cliLogger });
const resolvedProfile = await resolveProfileAndTarget({ args, browserLaunchOverride, cliLogger });
const { profile, profileInfo, resolvedTarget, nativeClaudeRemainingArgs } = resolvedProfile;
// Re-assign mutable state that Phase E flows may update
const resolvedSettingsPath = initialResolvedSettingsPath;
const resolvedSettings = initialResolvedSettings;
const resolvedCliproxyBridge = initialResolvedCliproxyBridge;
const targetRemainingArgs = initialTargetRemainingArgs;
const nativeClaudeRemainingArgs = initialNativeClaudeRemainingArgs;
const runtimeReasoningOverride = initialRuntimeReasoningOverride;
// Re-import dynamic modules needed by Phase E flows (preserving original ordering).
// Dynamic imports needed by Phase E flows — preserve original load ordering.
const InstanceManagerModule = await import('./management/instance-manager');
const InstanceManager = InstanceManagerModule.default;
const ProfileRegistryModule = await import('./auth/profile-registry');
@@ -152,6 +59,16 @@ async function main(): Promise<void> {
const ProfileContinuityModule = await import('./auth/profile-continuity-inheritance');
const { resolveProfileContinuityInheritance } = ProfileContinuityModule;
// Build full dispatch context (Phase E)
const dispatchCtx = {
...resolvedProfile,
InstanceManager,
ProfileRegistry,
resolveAccountContextPolicy,
isAccountContextMetadata,
resolveProfileContinuityInheritance,
};
try {
// Special case: headless delegation (-p/--prompt)
// Keep existing behavior for Claude targets only; non-claude targets must continue
@@ -166,795 +83,8 @@ async function main(): Promise<void> {
}
}
if (profileInfo.type === 'cliproxy') {
// CLIPROXY FLOW: OAuth-based profiles (gemini, codex, agy, qwen) or user-defined variants
const imageAnalysisMcpReady =
resolvedTarget === 'claude' ? ensureImageAnalysisMcpOrThrow() : true;
if (resolvedTarget === 'claude') {
ensureWebSearchMcpOrThrow();
}
const provider = profileInfo.provider || (profileInfo.name as CLIProxyProvider);
const expandedCliproxySettingsPath = profileInfo.settingsPath
? expandPath(profileInfo.settingsPath)
: undefined;
if (resolvedTarget === 'claude') {
if (imageAnalysisMcpReady) {
removeImageAnalysisProfileHook(profileInfo.name, expandedCliproxySettingsPath);
} else {
const imageAnalysisFallbackHookReady = prepareImageAnalysisFallbackHook();
ensureImageAnalyzerHooks({
profileName: profileInfo.name,
profileType: profileInfo.type,
cliproxyProvider: provider,
isComposite: profileInfo.isComposite,
settingsPath: expandedCliproxySettingsPath,
sharedHookInstalled: imageAnalysisFallbackHookReady,
});
}
}
const customSettingsPath = profileInfo.settingsPath; // undefined for hardcoded profiles
const variantPort = profileInfo.port; // variant-specific port for isolation
const cliproxyPort = variantPort || CLIPROXY_DEFAULT_PORT;
if (resolvedTarget !== 'claude') {
const adapter = targetAdapter;
if (!adapter) {
console.error(fail(`Target adapter not found for "${resolvedTarget}"`));
process.exitCode = 1;
return;
}
if (!adapter.supportsProfileType('cliproxy')) {
console.error(fail(`${adapter.displayName} does not support CLIProxy profiles`));
process.exitCode = 1;
return;
}
// Keep CLIProxy management/auth flags on Claude flow only.
const unsupportedCliproxyFlags = [
'--auth',
'--logout',
'--accounts',
'--add',
'--use',
'--config',
'--headless',
'--paste-callback',
'--port-forward',
'--nickname',
'--kiro-auth-method',
'--kiro-idc-start-url',
'--kiro-idc-region',
'--kiro-idc-flow',
'--backend',
'--proxy-host',
'--proxy-port',
'--proxy-protocol',
'--proxy-auth-token',
'--proxy-timeout',
'--local-proxy',
'--remote-only',
'--no-fallback',
'--allow-self-signed',
'--1m',
'--no-1m',
];
const providedUnsupportedFlag = unsupportedCliproxyFlags.find(
(flag) =>
targetRemainingArgs.includes(flag) ||
targetRemainingArgs.some((arg) => arg.startsWith(`${flag}=`))
);
if (providedUnsupportedFlag) {
console.error(
fail(
`${providedUnsupportedFlag} is only supported when running CLIProxy profiles on Claude target`
)
);
console.error(
info(`Run with Claude target: ccs ${profileInfo.name} --target claude ...`)
);
process.exitCode = 1;
return;
}
// For Droid execution path, require existing OAuth auth and running local proxy.
if (profileInfo.isComposite && profileInfo.compositeTiers) {
const compositeProviders = [
...new Set(Object.values(profileInfo.compositeTiers).map((tier) => tier.provider)),
] as CLIProxyProvider[];
const missingProvider = compositeProviders.find((p) => !isAuthenticated(p));
if (missingProvider) {
console.error(
fail(`Missing OAuth auth for composite tier provider: ${missingProvider}`)
);
console.error(info(`Authenticate first: ccs ${missingProvider} --auth`));
process.exitCode = 1;
return;
}
} else if (!isAuthenticated(provider)) {
console.error(fail(`No OAuth authentication found for provider: ${provider}`));
console.error(info(`Authenticate first: ccs ${provider} --auth`));
process.exitCode = 1;
return;
}
const ensureServiceResult = await ensureCliproxyService(
cliproxyPort,
targetRemainingArgs.includes('--verbose') || targetRemainingArgs.includes('-v')
);
if (!ensureServiceResult.started) {
console.error(
fail(ensureServiceResult.error || 'Failed to start local CLIProxy service')
);
process.exitCode = 1;
return;
}
const envVars =
profileInfo.isComposite && profileInfo.compositeTiers && profileInfo.compositeDefaultTier
? getCompositeEnvVars(
profileInfo.compositeTiers,
profileInfo.compositeDefaultTier,
cliproxyPort,
customSettingsPath
)
: getEffectiveEnvVars(provider, cliproxyPort, customSettingsPath);
const creds: TargetCredentials = {
profile: profileInfo.name,
baseUrl: envVars['ANTHROPIC_BASE_URL'] || '',
apiKey: envVars['ANTHROPIC_AUTH_TOKEN'] || '',
model: envVars['ANTHROPIC_MODEL'] || undefined,
provider: resolveDroidProvider({
provider: envVars['CCS_DROID_PROVIDER'] || envVars['DROID_PROVIDER'],
baseUrl: envVars['ANTHROPIC_BASE_URL'],
model: envVars['ANTHROPIC_MODEL'],
}),
reasoningOverride: runtimeReasoningOverride,
runtimeConfigOverrides: codexRuntimeConfigOverrides,
envVars,
};
if (!creds.baseUrl || !creds.apiKey) {
console.error(
fail(
`Missing CLIProxy runtime credentials for ${profileInfo.name} (ANTHROPIC_BASE_URL/AUTH_TOKEN)`
)
);
console.error(
info('Reconfigure with: ccs config > CLIProxy, or run ccs <provider> --config')
);
process.exitCode = 1;
return;
}
await adapter.prepareCredentials(creds);
const targetArgs = adapter.buildArgs(profileInfo.name, targetRemainingArgs, {
creds,
profileType: profileInfo.type,
binaryInfo: targetBinaryInfo || undefined,
});
const targetEnv = adapter.buildEnv(creds, profileInfo.type);
adapter.exec(targetArgs, targetEnv, { binaryInfo: targetBinaryInfo || undefined });
return;
}
await execClaudeWithCLIProxy(claudeCli, provider, remainingArgs, {
customSettingsPath,
port: cliproxyPort,
isComposite: profileInfo.isComposite,
compositeTiers: profileInfo.compositeTiers,
compositeDefaultTier: profileInfo.compositeDefaultTier,
profileName: profileInfo.name,
});
} else if (profileInfo.type === 'copilot') {
// COPILOT FLOW: GitHub Copilot subscription via copilot-api proxy
ensureWebSearchMcpOrThrow();
const imageAnalysisMcpReady = ensureImageAnalysisMcpOrThrow();
if (resolvedTarget === 'claude') {
if (imageAnalysisMcpReady) {
removeImageAnalysisProfileHook(profileInfo.name);
} else {
const imageAnalysisFallbackHookReady = prepareImageAnalysisFallbackHook();
ensureImageAnalyzerHooks({
profileName: profileInfo.name,
profileType: profileInfo.type,
sharedHookInstalled: imageAnalysisFallbackHookReady,
});
}
}
const { executeCopilotProfile } = await import('./copilot');
const copilotConfig = profileInfo.copilotConfig;
if (!copilotConfig) {
console.error(fail('Copilot configuration not found'));
process.exit(1);
}
const continuityInheritance = await resolveProfileContinuityInheritance({
profileName: profileInfo.name,
profileType: profileInfo.type,
target: resolvedTarget,
});
if (continuityInheritance.sourceAccount && process.env.CCS_DEBUG) {
console.error(
info(
`Continuity inheritance active: profile "${profileInfo.name}" -> account "${continuityInheritance.sourceAccount}"`
)
);
}
const exitCode = await executeCopilotProfile(
copilotConfig,
remainingArgs,
continuityInheritance.claudeConfigDir,
claudeCli
);
process.exit(exitCode);
} else if (profileInfo.type === 'cursor') {
// CURSOR FLOW: local Cursor daemon profile
ensureWebSearchMcpOrThrow();
installImageAnalyzerHook();
ensureImageAnalyzerHooks({
profileName: profileInfo.name,
profileType: profileInfo.type,
});
const { executeCursorProfile } = await import('./cursor');
const cursorConfig = profileInfo.cursorConfig;
if (!cursorConfig) {
console.error(fail('Cursor configuration not found'));
process.exit(1);
}
const continuityInheritance = await resolveProfileContinuityInheritance({
profileName: profileInfo.name,
profileType: profileInfo.type,
target: resolvedTarget,
});
if (continuityInheritance.sourceAccount && process.env.CCS_DEBUG) {
console.error(
info(
`Continuity inheritance active: profile "${profileInfo.name}" -> account "${continuityInheritance.sourceAccount}"`
)
);
}
const exitCode = await executeCursorProfile(
cursorConfig,
remainingArgs,
continuityInheritance.claudeConfigDir,
claudeCli
);
process.exit(exitCode);
} else if (profileInfo.type === 'settings') {
// Settings-based profiles (glm, glmt) are third-party providers
const imageAnalysisMcpReady =
resolvedTarget === 'claude' ? ensureImageAnalysisMcpOrThrow() : true;
const browserAttachRuntime =
resolvedTarget === 'claude' &&
claudeBrowserExposure?.exposeForLaunch &&
claudeAttachConfig?.enabled
? await resolveOptionalBrowserAttachRuntime(claudeAttachConfig)
: undefined;
const browserRuntimeEnv = browserAttachRuntime?.runtimeEnv;
if (browserAttachRuntime?.warning) {
process.stderr.write(`${warn(browserAttachRuntime.warning)}\n`);
}
if (resolvedTarget === 'claude') {
ensureWebSearchMcpOrThrow();
if (browserRuntimeEnv) {
ensureBrowserMcpOrThrow();
}
}
// Display WebSearch status (single line, equilibrium UX)
displayWebSearchStatus();
const continuityInheritance =
resolvedTarget === 'claude'
? await resolveProfileContinuityInheritance({
profileName: profileInfo.name,
profileType: profileInfo.type,
target: resolvedTarget,
})
: {};
if (continuityInheritance.sourceAccount && process.env.CCS_DEBUG) {
console.error(
info(
`Continuity inheritance active: profile "${profileInfo.name}" -> account "${continuityInheritance.sourceAccount}"`
)
);
}
const inheritedClaudeConfigDir = continuityInheritance.claudeConfigDir;
syncWebSearchMcpToConfigDir(inheritedClaudeConfigDir);
syncImageAnalysisMcpToConfigDir(inheritedClaudeConfigDir);
if (
browserRuntimeEnv &&
inheritedClaudeConfigDir &&
!syncBrowserMcpToConfigDir(inheritedClaudeConfigDir)
) {
throw new Error(
'Browser MCP is enabled, but CCS could not sync the browser MCP config into the inherited Claude instance.'
);
}
const expandedSettingsPath =
resolvedSettingsPath ??
(profileInfo.settingsPath
? expandPath(profileInfo.settingsPath)
: getSettingsPath(profileInfo.name));
const settings = resolvedSettings ?? loadSettings(expandedSettingsPath);
const cliproxyBridge = resolvedCliproxyBridge ?? resolveCliproxyBridgeMetadata(settings);
let imageAnalysisFallbackHookReady: boolean | undefined;
if (resolvedTarget === 'claude') {
if (imageAnalysisMcpReady) {
removeImageAnalysisProfileHook(profileInfo.name, expandedSettingsPath);
} else {
imageAnalysisFallbackHookReady = prepareImageAnalysisFallbackHook();
ensureImageAnalyzerHooks({
profileName: profileInfo.name,
profileType: profileInfo.type,
settingsPath: expandedSettingsPath,
settings,
cliproxyBridge,
sharedHookInstalled: imageAnalysisFallbackHookReady,
});
}
}
if (resolvedTarget !== 'claude') {
const compatibility = evaluateTargetRuntimeCompatibility({
target: resolvedTarget,
profileType: profileInfo.type,
cliproxyBridgeProvider: cliproxyBridge?.provider ?? null,
});
if (!compatibility.supported) {
console.error(
fail(
compatibility.reason ||
`${targetAdapter?.displayName || resolvedTarget} does not support this profile.`
)
);
if (compatibility.suggestion) {
console.error(info(compatibility.suggestion));
}
process.exit(1);
}
}
const rawSettingsEnv = profileInfo.env ?? settings.env ?? {};
const isDeprecatedGlmtProfile = isDeprecatedGlmtProfileName(profileInfo.name);
const glmtNormalization = isDeprecatedGlmtProfile
? normalizeDeprecatedGlmtEnv(rawSettingsEnv)
: null;
const settingsEnv = glmtNormalization?.env ?? rawSettingsEnv;
if (glmtNormalization) {
for (const message of glmtNormalization.warnings) {
console.error(warn(message));
}
}
// Pre-flight validation for Z.AI-compatible profiles.
if (profileInfo.name === 'glm' || isDeprecatedGlmtProfile) {
const apiKey = settingsEnv['ANTHROPIC_AUTH_TOKEN'];
if (apiKey) {
const validation = await validateGlmKey(apiKey, settingsEnv['ANTHROPIC_BASE_URL']);
if (!validation.valid) {
console.error('');
console.error(fail(validation.error || 'API key validation failed'));
if (validation.suggestion) {
console.error('');
console.error(validation.suggestion);
}
console.error('');
console.error(info('To skip validation: CCS_SKIP_PREFLIGHT=1 ccs glm "prompt"'));
process.exit(1);
}
}
}
if (profileInfo.name === 'mm') {
const apiKey = settingsEnv['ANTHROPIC_AUTH_TOKEN'];
if (apiKey) {
const validation = await validateMiniMaxKey(apiKey, settingsEnv['ANTHROPIC_BASE_URL']);
if (!validation.valid) {
console.error('');
console.error(fail(validation.error || 'API key validation failed'));
if (validation.suggestion) {
console.error('');
console.error(validation.suggestion);
}
console.error('');
console.error(info('To skip validation: CCS_SKIP_PREFLIGHT=1 ccs mm "prompt"'));
process.exit(1);
}
}
}
// Pre-flight validation for Anthropic direct profiles (ANTHROPIC_API_KEY + no BASE_URL)
{
const anthropicApiKey = settingsEnv['ANTHROPIC_API_KEY'];
const hasBaseUrl = !!settingsEnv['ANTHROPIC_BASE_URL'];
if (anthropicApiKey && !hasBaseUrl) {
const validation = await validateAnthropicKey(anthropicApiKey);
if (!validation.valid) {
console.error('');
console.error(fail(validation.error || 'API key validation failed'));
if (validation.suggestion) {
console.error('');
console.error(validation.suggestion);
}
console.error('');
console.error(
info(`To skip validation: CCS_SKIP_PREFLIGHT=1 ccs ${profileInfo.name} "prompt"`)
);
process.exit(1);
}
}
}
const webSearchEnv = getWebSearchHookEnv();
const imageAnalysisStatus = await resolveImageAnalysisRuntimeStatus({
profileName: profileInfo.name,
profileType: profileInfo.type,
settings,
cliproxyBridge,
sharedHookInstalled: imageAnalysisFallbackHookReady,
});
const runtimeConnection = resolveImageAnalysisRuntimeConnection();
let imageAnalysisEnv = getImageAnalysisHookEnv({
profileName: profileInfo.name,
profileType: profileInfo.type,
settings,
cliproxyBridge,
});
imageAnalysisEnv = applyImageAnalysisRuntimeOverrides(imageAnalysisEnv, {
backendId: imageAnalysisStatus.backendId,
model: imageAnalysisStatus.model,
runtimePath: imageAnalysisStatus.runtimePath,
baseUrl: runtimeConnection.baseUrl,
apiKey: runtimeConnection.apiKey,
allowSelfSigned: runtimeConnection.allowSelfSigned,
});
imageAnalysisEnv = {
...imageAnalysisEnv,
CCS_IMAGE_ANALYSIS_SKIP_HOOK:
resolvedTarget === 'claude' && imageAnalysisMcpReady ? '1' : '0',
};
const imageAnalysisProvider = imageAnalysisEnv['CCS_CURRENT_PROVIDER'];
if (
resolvedTarget === 'claude' &&
imageAnalysisEnv['CCS_IMAGE_ANALYSIS_SKIP'] !== '1' &&
imageAnalysisProvider
) {
const verboseProxyLaunch =
remainingArgs.includes('--verbose') ||
remainingArgs.includes('-v') ||
targetRemainingArgs.includes('--verbose') ||
targetRemainingArgs.includes('-v');
if (imageAnalysisStatus.effectiveRuntimeMode === 'native-read') {
console.error(
info(
`${imageAnalysisStatus.effectiveRuntimeReason || `Image analysis via ${imageAnalysisProvider} is unavailable.`} This session will use native Read.`
)
);
imageAnalysisEnv = {
...imageAnalysisEnv,
CCS_CURRENT_PROVIDER: '',
CCS_IMAGE_ANALYSIS_SKIP: '1',
CCS_IMAGE_ANALYSIS_RUNTIME_PATH: '',
CCS_IMAGE_ANALYSIS_RUNTIME_BASE_URL: '',
CCS_IMAGE_ANALYSIS_RUNTIME_API_KEY: '',
CCS_IMAGE_ANALYSIS_RUNTIME_ALLOW_SELF_SIGNED: '0',
};
} else if (imageAnalysisStatus.proxyReadiness === 'stopped') {
const ensureServiceResult = await ensureCliproxyService(
CLIPROXY_DEFAULT_PORT,
verboseProxyLaunch
);
if (!ensureServiceResult.started) {
console.error(
warn(
`Image analysis via ${imageAnalysisProvider} is unavailable because CCS could not start the local CLIProxy service. This session will use native Read.`
)
);
imageAnalysisEnv = {
...imageAnalysisEnv,
CCS_CURRENT_PROVIDER: '',
CCS_IMAGE_ANALYSIS_SKIP: '1',
};
}
}
}
// Get global env vars (DISABLE_TELEMETRY, etc.) for third-party profiles
const globalEnvConfig = getGlobalEnvConfig();
const globalEnv = globalEnvConfig.enabled ? globalEnvConfig.env : {};
// Log global env injection for visibility (debug mode only)
if (globalEnvConfig.enabled && Object.keys(globalEnv).length > 0 && process.env.CCS_DEBUG) {
const envNames = Object.keys(globalEnv).join(', ');
console.error(info(`Global env: ${envNames}`));
}
// For Claude target launches that already pass `--settings`, keep runtime
// env free of ANTHROPIC routing/auth while preserving non-routing profile
// env so nested Team/subagent sessions can still inherit model intent and
// other profile-scoped runtime flags.
const settingsRuntimeEnv = stripBrowserEnv({ ...globalEnv, ...settingsEnv });
const claudeRuntimeEnvVars: NodeJS.ProcessEnv = {
...stripAnthropicRoutingEnv(settingsRuntimeEnv),
...(inheritedClaudeConfigDir ? { CLAUDE_CONFIG_DIR: inheritedClaudeConfigDir } : {}),
...webSearchEnv,
...imageAnalysisEnv,
...(browserRuntimeEnv || {}),
CCS_PROFILE_TYPE: 'settings',
CCS_STRIP_INHERITED_ANTHROPIC_ENV: '1',
};
// Non-Claude targets still need effective credentials injected directly.
const envVars: NodeJS.ProcessEnv = {
...settingsRuntimeEnv,
...(inheritedClaudeConfigDir ? { CLAUDE_CONFIG_DIR: inheritedClaudeConfigDir } : {}),
...webSearchEnv,
...imageAnalysisEnv,
...(browserRuntimeEnv || {}),
CCS_PROFILE_TYPE: 'settings',
};
// Dispatch through target adapter for non-claude targets
if (resolvedTarget !== 'claude') {
const adapter = targetAdapter;
if (!adapter) {
console.error(fail(`Target adapter not found for "${resolvedTarget}"`));
process.exit(1);
}
const directAnthropicBaseUrl =
settingsEnv['ANTHROPIC_BASE_URL'] ||
(settingsEnv['ANTHROPIC_API_KEY'] ? 'https://api.anthropic.com' : '');
const creds: TargetCredentials = {
profile: profileInfo.name,
baseUrl: directAnthropicBaseUrl,
apiKey: settingsEnv['ANTHROPIC_AUTH_TOKEN'] || settingsEnv['ANTHROPIC_API_KEY'] || '',
model: settingsEnv['ANTHROPIC_MODEL'],
provider: resolveDroidProvider({
provider: settingsEnv['CCS_DROID_PROVIDER'] || settingsEnv['DROID_PROVIDER'],
baseUrl: directAnthropicBaseUrl,
model: settingsEnv['ANTHROPIC_MODEL'],
}),
reasoningOverride: runtimeReasoningOverride,
runtimeConfigOverrides: codexRuntimeConfigOverrides,
envVars,
};
await adapter.prepareCredentials(creds);
const targetArgs = adapter.buildArgs(profileInfo.name, targetRemainingArgs, {
creds,
profileType: profileInfo.type,
binaryInfo: targetBinaryInfo || undefined,
});
const targetEnv = adapter.buildEnv(creds, profileInfo.type);
adapter.exec(targetArgs, targetEnv, { binaryInfo: targetBinaryInfo || undefined });
return;
}
const imageAnalysisArgs = imageAnalysisMcpReady
? appendThirdPartyImageAnalysisToolArgs(nativeClaudeRemainingArgs)
: nativeClaudeRemainingArgs;
const browserArgs = browserRuntimeEnv
? appendBrowserToolArgs(imageAnalysisArgs)
: imageAnalysisArgs;
const openAICompatProfile = resolveOpenAICompatProfileConfig(
profileInfo.name,
expandedSettingsPath,
settingsEnv
);
if (openAICompatProfile) {
const proxyStart = await startOpenAICompatProxy(openAICompatProfile, {
insecure: openAICompatProfile.insecure,
});
if (!proxyStart.success) {
console.error(fail(proxyStart.error || 'Failed to start local OpenAI-compatible proxy'));
process.exit(1);
}
console.error(
info(
`Using local OpenAI-compatible proxy for "${profileInfo.name}" on port ${proxyStart.port}`
)
);
const proxyEnv = {
...envVars,
...buildOpenAICompatProxyEnv(
openAICompatProfile,
proxyStart.port,
proxyStart.authToken || '',
inheritedClaudeConfigDir
),
};
delete proxyEnv.ANTHROPIC_API_KEY;
const launchSettings = createOpenAICompatLaunchSettings(expandedSettingsPath, settings);
const launchArgs = [
'--settings',
launchSettings.settingsPath,
...appendThirdPartyWebSearchToolArgs(browserArgs),
];
const traceEnv = createWebSearchTraceContext({
launcher: 'ccs.settings-profile.proxy',
args: launchArgs,
profile: profileInfo.name,
profileType: profileInfo.type,
settingsPath: expandedSettingsPath,
});
execClaude(claudeCli, launchArgs, { ...proxyEnv, ...traceEnv }, launchSettings.cleanup);
return;
}
const launchArgs = [
'--settings',
expandedSettingsPath,
...appendThirdPartyWebSearchToolArgs(browserArgs),
];
const traceEnv = createWebSearchTraceContext({
launcher: 'ccs.settings-profile',
args: launchArgs,
profile: profileInfo.name,
profileType: profileInfo.type,
settingsPath: expandedSettingsPath,
});
execClaude(claudeCli, launchArgs, { ...claudeRuntimeEnvVars, ...traceEnv });
} else if (profileInfo.type === 'account') {
// NEW FLOW: Account-based profile (work, personal)
// All platforms: Use instance isolation with CLAUDE_CONFIG_DIR
const registry = new ProfileRegistry();
const instanceMgr = new InstanceManager();
const accountMetadata = isAccountContextMetadata(profileInfo.profile)
? profileInfo.profile
: undefined;
const isBareProfile =
typeof profileInfo.profile === 'object' &&
profileInfo.profile !== null &&
(profileInfo.profile as { bare?: unknown }).bare === true;
const contextPolicy = resolveAccountContextPolicy(accountMetadata);
// Ensure instance exists (lazy init if needed)
const instancePath = await instanceMgr.ensureInstance(profileInfo.name, contextPolicy, {
bare: isBareProfile,
});
// Update last_used timestamp (check unified config first, fallback to legacy)
if (registry.hasAccountUnified(profileInfo.name)) {
registry.touchAccountUnified(profileInfo.name);
} else {
registry.touchProfile(profileInfo.name);
}
// Execute Claude with instance isolation
// Skip WebSearch hook - account profiles use native server-side WebSearch
// Skip Image Analyzer hook - account profiles have native vision support
const envVars: NodeJS.ProcessEnv = {
CLAUDE_CONFIG_DIR: instancePath,
CCS_PROFILE_TYPE: 'account',
CCS_WEBSEARCH_SKIP: '1',
CCS_IMAGE_ANALYSIS_SKIP: '1',
};
await maybeWarnAboutResumeLaneMismatch(
profileInfo.name,
instancePath,
nativeClaudeRemainingArgs
);
const launchArgs = resolveNativeClaudeLaunchArgs(
nativeClaudeRemainingArgs,
'account',
instancePath
);
execClaude(claudeCli, launchArgs, envVars);
} else {
// DEFAULT: No profile configured, use Claude's own defaults
// Skip WebSearch hook - native Claude has server-side WebSearch
// Skip Image Analyzer hook - native Claude has native vision support
const envVars: NodeJS.ProcessEnv = {
CCS_PROFILE_TYPE: 'default',
CCS_WEBSEARCH_SKIP: '1',
CCS_IMAGE_ANALYSIS_SKIP: '1',
};
const browserAttachRuntime =
resolvedTarget === 'claude' &&
claudeBrowserExposure?.exposeForLaunch &&
claudeAttachConfig?.enabled
? await resolveOptionalBrowserAttachRuntime(claudeAttachConfig)
: undefined;
const browserRuntimeEnv = browserAttachRuntime?.runtimeEnv;
if (browserAttachRuntime?.warning) {
process.stderr.write(`${warn(browserAttachRuntime.warning)}\n`);
}
if (resolvedTarget === 'claude') {
if (browserRuntimeEnv) {
ensureBrowserMcpOrThrow();
Object.assign(envVars, browserRuntimeEnv);
}
const defaultContinuityInheritance = await resolveProfileContinuityInheritance({
profileName: profileInfo.name,
profileType: profileInfo.type,
target: resolvedTarget,
});
if (defaultContinuityInheritance.sourceAccount && process.env.CCS_DEBUG) {
console.error(
info(
`Continuity inheritance active: profile "${profileInfo.name}" -> account "${defaultContinuityInheritance.sourceAccount}"`
)
);
}
if (defaultContinuityInheritance.claudeConfigDir) {
envVars.CLAUDE_CONFIG_DIR = defaultContinuityInheritance.claudeConfigDir;
if (
browserRuntimeEnv &&
!syncBrowserMcpToConfigDir(defaultContinuityInheritance.claudeConfigDir)
) {
throw new Error(
'Browser MCP is enabled, but CCS could not sync the browser MCP config into the inherited Claude instance.'
);
}
}
}
// Dispatch through target adapter for non-claude targets
if (resolvedTarget !== 'claude') {
const adapter = targetAdapter;
if (!adapter) {
console.error(fail(`Target adapter not found for "${resolvedTarget}"`));
process.exit(1);
}
if (!adapter.supportsProfileType('default')) {
console.error(fail(`${adapter.displayName} does not support default profile mode`));
process.exit(1);
}
const creds: TargetCredentials = {
profile: 'default',
baseUrl: process.env['ANTHROPIC_BASE_URL'] || '',
apiKey: process.env['ANTHROPIC_AUTH_TOKEN'] || '',
model: process.env['ANTHROPIC_MODEL'],
provider: resolveDroidProvider({
provider: process.env['CCS_DROID_PROVIDER'] || process.env['DROID_PROVIDER'],
baseUrl: process.env['ANTHROPIC_BASE_URL'],
model: process.env['ANTHROPIC_MODEL'],
}),
reasoningOverride: runtimeReasoningOverride,
runtimeConfigOverrides: codexRuntimeConfigOverrides,
browserRuntimeEnv,
};
if (resolvedTarget === 'droid' && (!creds.baseUrl || !creds.apiKey)) {
console.error(
fail(
`${adapter.displayName} default mode requires ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN`
)
);
console.error(info('Use a settings-based profile instead: ccs glm --target droid'));
process.exit(1);
}
await adapter.prepareCredentials(creds);
const targetArgs = adapter.buildArgs('default', targetRemainingArgs, {
creds,
profileType: 'default',
binaryInfo: targetBinaryInfo || undefined,
});
const targetEnv = adapter.buildEnv(creds, 'default');
adapter.exec(targetArgs, targetEnv, { binaryInfo: targetBinaryInfo || undefined });
return;
}
const launchArgs = resolveNativeClaudeLaunchArgs(
browserRuntimeEnv
? appendBrowserToolArgs(nativeClaudeRemainingArgs)
: nativeClaudeRemainingArgs,
'default',
envVars.CLAUDE_CONFIG_DIR
);
execClaude(claudeCli, launchArgs, envVars);
}
// Phase E: dispatch to per-profile-type flow (all 6 branches now live in flows/)
await dispatchProfile(dispatchCtx);
} catch (error) {
const err = error as ProfileError;
// Check if this is a profile not found error with suggestions
+40
View File
@@ -0,0 +1,40 @@
/**
* Shared context type for Phase E profile dispatch flows.
*
* ProfileDispatchContext extends ResolvedProfile with the dynamic imports
* needed by all flows (InstanceManager, ProfileRegistry, AccountContext,
* ProfileContinuity), and the browser-exposure fields already computed by
* Phase C (profile-resolver.ts).
*/
import type { ResolvedProfile } from './profile-resolver';
import type { loadSettings } from '../utils/config-manager';
import type { getEffectiveClaudeBrowserAttachConfig } from '../utils/browser';
import type { resolveCliproxyBridgeMetadata } from '../api/services/cliproxy-profile-bridge';
import type { resolveTargetType } from '../targets/target-resolver';
// Re-export ResolvedProfile fields as ProfileDispatchContext —
// all browser-exposure fields are already present in ResolvedProfile.
export type ProfileDispatchContext = ResolvedProfile & {
/** Dynamic-imported modules needed by account/settings flows */
InstanceManager: Awaited<typeof import('../management/instance-manager')>['default'];
ProfileRegistry: Awaited<typeof import('../auth/profile-registry')>['default'];
resolveAccountContextPolicy: Awaited<
typeof import('../auth/account-context')
>['resolveAccountContextPolicy'];
isAccountContextMetadata: Awaited<
typeof import('../auth/account-context')
>['isAccountContextMetadata'];
resolveProfileContinuityInheritance: Awaited<
typeof import('../auth/profile-continuity-inheritance')
>['resolveProfileContinuityInheritance'];
};
// Re-export for convenience
export type { ResolvedProfile };
export type {
loadSettings,
getEffectiveClaudeBrowserAttachConfig,
resolveCliproxyBridgeMetadata,
resolveTargetType,
};
+63
View File
@@ -0,0 +1,63 @@
/**
* Account dispatch flow — account-based profile (work, personal) with instance isolation.
*
* Extracted from src/ccs.ts main() profileInfo.type === 'account' branch.
* Uses CLAUDE_CONFIG_DIR for per-profile instance isolation on all platforms.
*/
import { execClaude } from '../../utils/shell-executor';
import { maybeWarnAboutResumeLaneMismatch } from '../../auth/resume-lane-warning';
import { resolveNativeClaudeLaunchArgs } from '../environment-builder';
import type { ProfileDispatchContext } from '../dispatcher-context';
export async function runAccountFlow(ctx: ProfileDispatchContext): Promise<void> {
const {
profileInfo,
claudeCli,
nativeClaudeRemainingArgs,
InstanceManager,
ProfileRegistry,
resolveAccountContextPolicy,
isAccountContextMetadata,
} = ctx;
const registry = new ProfileRegistry();
const instanceMgr = new InstanceManager();
const accountMetadata = isAccountContextMetadata(profileInfo.profile)
? profileInfo.profile
: undefined;
const isBareProfile =
typeof profileInfo.profile === 'object' &&
profileInfo.profile !== null &&
(profileInfo.profile as { bare?: unknown }).bare === true;
const contextPolicy = resolveAccountContextPolicy(accountMetadata);
// Ensure instance exists (lazy init if needed)
const instancePath = await instanceMgr.ensureInstance(profileInfo.name, contextPolicy, {
bare: isBareProfile,
});
// Update last_used timestamp (check unified config first, fallback to legacy)
if (registry.hasAccountUnified(profileInfo.name)) {
registry.touchAccountUnified(profileInfo.name);
} else {
registry.touchProfile(profileInfo.name);
}
// Execute Claude with instance isolation.
// Skip WebSearch hook — account profiles use native server-side WebSearch.
// Skip Image Analyzer hook — account profiles have native vision support.
const envVars: NodeJS.ProcessEnv = {
CLAUDE_CONFIG_DIR: instancePath,
CCS_PROFILE_TYPE: 'account',
CCS_WEBSEARCH_SKIP: '1',
CCS_IMAGE_ANALYSIS_SKIP: '1',
};
await maybeWarnAboutResumeLaneMismatch(profileInfo.name, instancePath, nativeClaudeRemainingArgs);
const launchArgs = resolveNativeClaudeLaunchArgs(
nativeClaudeRemainingArgs,
'account',
instancePath
);
execClaude(claudeCli, launchArgs, envVars);
}
+211
View File
@@ -0,0 +1,211 @@
/**
* CLIProxy dispatch flow — OAuth-based profiles (gemini, codex, agy, qwen) or user-defined variants.
*
* Extracted from src/ccs.ts main() profileInfo.type === 'cliproxy' branch.
*/
import { expandPath } from '../../utils/helpers';
import { fail, info } from '../../utils/ui';
import {
execClaudeWithCLIProxy,
type CLIProxyProvider,
ensureCliproxyService,
isAuthenticated,
} from '../../cliproxy';
import { getEffectiveEnvVars, getCompositeEnvVars } from '../../cliproxy/config/env-builder';
import { CLIPROXY_DEFAULT_PORT } from '../../cliproxy/config/port-manager';
import { ensureWebSearchMcpOrThrow } from '../../utils/websearch-manager';
import { ensureImageAnalysisMcpOrThrow } from '../../utils/image-analysis';
import {
ensureProfileHooks as ensureImageAnalyzerHooks,
removeImageAnalysisProfileHook,
} from '../../utils/hooks/image-analyzer-profile-hook-injector';
import { prepareImageAnalysisFallbackHook } from '../../utils/hooks';
import { resolveDroidProvider, type TargetCredentials } from '../../targets';
import type { ProfileDispatchContext } from '../dispatcher-context';
export async function runCliproxyFlow(ctx: ProfileDispatchContext): Promise<void> {
const {
profileInfo,
resolvedTarget,
claudeCli,
targetAdapter,
targetRemainingArgs,
runtimeReasoningOverride,
codexRuntimeConfigOverrides,
remainingArgs,
} = ctx;
const imageAnalysisMcpReady =
resolvedTarget === 'claude' ? ensureImageAnalysisMcpOrThrow() : true;
if (resolvedTarget === 'claude') {
ensureWebSearchMcpOrThrow();
}
const provider = profileInfo.provider || (profileInfo.name as CLIProxyProvider);
const expandedCliproxySettingsPath = profileInfo.settingsPath
? expandPath(profileInfo.settingsPath)
: undefined;
if (resolvedTarget === 'claude') {
if (imageAnalysisMcpReady) {
removeImageAnalysisProfileHook(profileInfo.name, expandedCliproxySettingsPath);
} else {
const imageAnalysisFallbackHookReady = prepareImageAnalysisFallbackHook();
ensureImageAnalyzerHooks({
profileName: profileInfo.name,
profileType: profileInfo.type,
cliproxyProvider: provider,
isComposite: profileInfo.isComposite,
settingsPath: expandedCliproxySettingsPath,
sharedHookInstalled: imageAnalysisFallbackHookReady,
});
}
}
const customSettingsPath = profileInfo.settingsPath; // undefined for hardcoded profiles
const variantPort = profileInfo.port; // variant-specific port for isolation
const cliproxyPort = variantPort || CLIPROXY_DEFAULT_PORT;
if (resolvedTarget !== 'claude') {
const adapter = targetAdapter;
if (!adapter) {
console.error(fail(`Target adapter not found for "${resolvedTarget}"`));
process.exitCode = 1;
return;
}
if (!adapter.supportsProfileType('cliproxy')) {
console.error(fail(`${adapter.displayName} does not support CLIProxy profiles`));
process.exitCode = 1;
return;
}
// Keep CLIProxy management/auth flags on Claude flow only.
const unsupportedCliproxyFlags = [
'--auth',
'--logout',
'--accounts',
'--add',
'--use',
'--config',
'--headless',
'--paste-callback',
'--port-forward',
'--nickname',
'--kiro-auth-method',
'--kiro-idc-start-url',
'--kiro-idc-region',
'--kiro-idc-flow',
'--backend',
'--proxy-host',
'--proxy-port',
'--proxy-protocol',
'--proxy-auth-token',
'--proxy-timeout',
'--local-proxy',
'--remote-only',
'--no-fallback',
'--allow-self-signed',
'--1m',
'--no-1m',
];
const providedUnsupportedFlag = unsupportedCliproxyFlags.find(
(flag) =>
targetRemainingArgs.includes(flag) ||
targetRemainingArgs.some((arg) => arg.startsWith(`${flag}=`))
);
if (providedUnsupportedFlag) {
console.error(
fail(
`${providedUnsupportedFlag} is only supported when running CLIProxy profiles on Claude target`
)
);
console.error(info(`Run with Claude target: ccs ${profileInfo.name} --target claude ...`));
process.exitCode = 1;
return;
}
// For Droid execution path, require existing OAuth auth and running local proxy.
if (profileInfo.isComposite && profileInfo.compositeTiers) {
const compositeProviders = [
...new Set(Object.values(profileInfo.compositeTiers).map((tier) => tier.provider)),
] as CLIProxyProvider[];
const missingProvider = compositeProviders.find((p) => !isAuthenticated(p));
if (missingProvider) {
console.error(fail(`Missing OAuth auth for composite tier provider: ${missingProvider}`));
console.error(info(`Authenticate first: ccs ${missingProvider} --auth`));
process.exitCode = 1;
return;
}
} else if (!isAuthenticated(provider)) {
console.error(fail(`No OAuth authentication found for provider: ${provider}`));
console.error(info(`Authenticate first: ccs ${provider} --auth`));
process.exitCode = 1;
return;
}
const ensureServiceResult = await ensureCliproxyService(
cliproxyPort,
targetRemainingArgs.includes('--verbose') || targetRemainingArgs.includes('-v')
);
if (!ensureServiceResult.started) {
console.error(fail(ensureServiceResult.error || 'Failed to start local CLIProxy service'));
process.exitCode = 1;
return;
}
const envVars =
profileInfo.isComposite && profileInfo.compositeTiers && profileInfo.compositeDefaultTier
? getCompositeEnvVars(
profileInfo.compositeTiers,
profileInfo.compositeDefaultTier,
cliproxyPort,
customSettingsPath
)
: getEffectiveEnvVars(provider, cliproxyPort, customSettingsPath);
const creds: TargetCredentials = {
profile: profileInfo.name,
baseUrl: envVars['ANTHROPIC_BASE_URL'] || '',
apiKey: envVars['ANTHROPIC_AUTH_TOKEN'] || '',
model: envVars['ANTHROPIC_MODEL'] || undefined,
provider: resolveDroidProvider({
provider: envVars['CCS_DROID_PROVIDER'] || envVars['DROID_PROVIDER'],
baseUrl: envVars['ANTHROPIC_BASE_URL'],
model: envVars['ANTHROPIC_MODEL'],
}),
reasoningOverride: runtimeReasoningOverride,
runtimeConfigOverrides: codexRuntimeConfigOverrides,
envVars,
};
if (!creds.baseUrl || !creds.apiKey) {
console.error(
fail(
`Missing CLIProxy runtime credentials for ${profileInfo.name} (ANTHROPIC_BASE_URL/AUTH_TOKEN)`
)
);
console.error(
info('Reconfigure with: ccs config > CLIProxy, or run ccs <provider> --config')
);
process.exitCode = 1;
return;
}
await adapter.prepareCredentials(creds);
const targetArgs = adapter.buildArgs(profileInfo.name, targetRemainingArgs, {
creds,
profileType: profileInfo.type,
binaryInfo: ctx.targetBinaryInfo || undefined,
});
const targetEnv = adapter.buildEnv(creds, profileInfo.type);
adapter.exec(targetArgs, targetEnv, { binaryInfo: ctx.targetBinaryInfo || undefined });
return;
}
await execClaudeWithCLIProxy(claudeCli, provider, remainingArgs, {
customSettingsPath,
port: cliproxyPort,
isComposite: profileInfo.isComposite,
compositeTiers: profileInfo.compositeTiers,
compositeDefaultTier: profileInfo.compositeDefaultTier,
profileName: profileInfo.name,
});
}
+66
View File
@@ -0,0 +1,66 @@
/**
* Copilot dispatch flow — GitHub Copilot subscription via copilot-api proxy.
*
* Extracted from src/ccs.ts main() profileInfo.type === 'copilot' branch.
*/
import { fail, info } from '../../utils/ui';
import { ensureWebSearchMcpOrThrow } from '../../utils/websearch-manager';
import { ensureImageAnalysisMcpOrThrow } from '../../utils/image-analysis';
import {
ensureProfileHooks as ensureImageAnalyzerHooks,
removeImageAnalysisProfileHook,
} from '../../utils/hooks/image-analyzer-profile-hook-injector';
import { prepareImageAnalysisFallbackHook } from '../../utils/hooks';
import type { ProfileDispatchContext } from '../dispatcher-context';
export async function runCopilotFlow(ctx: ProfileDispatchContext): Promise<void> {
const {
profileInfo,
resolvedTarget,
claudeCli,
remainingArgs,
resolveProfileContinuityInheritance,
} = ctx;
ensureWebSearchMcpOrThrow();
const imageAnalysisMcpReady = ensureImageAnalysisMcpOrThrow();
if (resolvedTarget === 'claude') {
if (imageAnalysisMcpReady) {
removeImageAnalysisProfileHook(profileInfo.name);
} else {
const imageAnalysisFallbackHookReady = prepareImageAnalysisFallbackHook();
ensureImageAnalyzerHooks({
profileName: profileInfo.name,
profileType: profileInfo.type,
sharedHookInstalled: imageAnalysisFallbackHookReady,
});
}
}
const { executeCopilotProfile } = await import('../../copilot');
const copilotConfig = profileInfo.copilotConfig;
if (!copilotConfig) {
console.error(fail('Copilot configuration not found'));
process.exit(1);
}
const continuityInheritance = await resolveProfileContinuityInheritance({
profileName: profileInfo.name,
profileType: profileInfo.type,
target: resolvedTarget,
});
if (continuityInheritance.sourceAccount && process.env.CCS_DEBUG) {
console.error(
info(
`Continuity inheritance active: profile "${profileInfo.name}" -> account "${continuityInheritance.sourceAccount}"`
)
);
}
const exitCode = await executeCopilotProfile(
copilotConfig,
remainingArgs,
continuityInheritance.claudeConfigDir,
claudeCli
);
process.exit(exitCode);
}
+54
View File
@@ -0,0 +1,54 @@
/**
* Cursor dispatch flow — local Cursor daemon profile.
*
* Extracted from src/ccs.ts main() profileInfo.type === 'cursor' branch.
*/
import { fail, info } from '../../utils/ui';
import { ensureWebSearchMcpOrThrow } from '../../utils/websearch-manager';
import { ensureProfileHooks as ensureImageAnalyzerHooks } from '../../utils/hooks/image-analyzer-profile-hook-injector';
import { installImageAnalyzerHook } from '../../utils/hooks';
import type { ProfileDispatchContext } from '../dispatcher-context';
export async function runCursorFlow(ctx: ProfileDispatchContext): Promise<void> {
const {
profileInfo,
resolvedTarget,
claudeCli,
remainingArgs,
resolveProfileContinuityInheritance,
} = ctx;
ensureWebSearchMcpOrThrow();
installImageAnalyzerHook();
ensureImageAnalyzerHooks({
profileName: profileInfo.name,
profileType: profileInfo.type,
});
const { executeCursorProfile } = await import('../../cursor');
const cursorConfig = profileInfo.cursorConfig;
if (!cursorConfig) {
console.error(fail('Cursor configuration not found'));
process.exit(1);
}
const continuityInheritance = await resolveProfileContinuityInheritance({
profileName: profileInfo.name,
profileType: profileInfo.type,
target: resolvedTarget,
});
if (continuityInheritance.sourceAccount && process.env.CCS_DEBUG) {
console.error(
info(
`Continuity inheritance active: profile "${profileInfo.name}" -> account "${continuityInheritance.sourceAccount}"`
)
);
}
const exitCode = await executeCursorProfile(
cursorConfig,
remainingArgs,
continuityInheritance.claudeConfigDir,
claudeCli
);
process.exit(exitCode);
}
+136
View File
@@ -0,0 +1,136 @@
/**
* Default dispatch flow — no profile configured, use Claude's own defaults.
*
* Extracted from src/ccs.ts main() else branch (profileInfo.type === 'default').
* Skip WebSearch hook — native Claude has server-side WebSearch.
* Skip Image Analyzer hook — native Claude has native vision support.
*/
import { fail, info, warn } from '../../utils/ui';
import {
appendBrowserToolArgs,
ensureBrowserMcpOrThrow,
resolveOptionalBrowserAttachRuntime,
syncBrowserMcpToConfigDir,
} from '../../utils/browser';
import { execClaude } from '../../utils/shell-executor';
import { resolveDroidProvider, type TargetCredentials } from '../../targets';
import { resolveNativeClaudeLaunchArgs } from '../environment-builder';
import type { ProfileDispatchContext } from '../dispatcher-context';
export async function runDefaultFlow(ctx: ProfileDispatchContext): Promise<void> {
const {
profileInfo,
resolvedTarget,
claudeCli,
targetAdapter,
targetBinaryInfo,
targetRemainingArgs,
nativeClaudeRemainingArgs,
runtimeReasoningOverride,
codexRuntimeConfigOverrides,
claudeBrowserExposure,
claudeAttachConfig,
resolveProfileContinuityInheritance,
} = ctx;
const envVars: NodeJS.ProcessEnv = {
CCS_PROFILE_TYPE: 'default',
CCS_WEBSEARCH_SKIP: '1',
CCS_IMAGE_ANALYSIS_SKIP: '1',
};
const browserAttachRuntime =
resolvedTarget === 'claude' &&
claudeBrowserExposure?.exposeForLaunch &&
claudeAttachConfig?.enabled
? await resolveOptionalBrowserAttachRuntime(claudeAttachConfig)
: undefined;
const browserRuntimeEnv = browserAttachRuntime?.runtimeEnv;
if (browserAttachRuntime?.warning) {
process.stderr.write(`${warn(browserAttachRuntime.warning)}\n`);
}
if (resolvedTarget === 'claude') {
if (browserRuntimeEnv) {
ensureBrowserMcpOrThrow();
Object.assign(envVars, browserRuntimeEnv);
}
const defaultContinuityInheritance = await resolveProfileContinuityInheritance({
profileName: profileInfo.name,
profileType: profileInfo.type,
target: resolvedTarget,
});
if (defaultContinuityInheritance.sourceAccount && process.env.CCS_DEBUG) {
console.error(
info(
`Continuity inheritance active: profile "${profileInfo.name}" -> account "${defaultContinuityInheritance.sourceAccount}"`
)
);
}
if (defaultContinuityInheritance.claudeConfigDir) {
envVars.CLAUDE_CONFIG_DIR = defaultContinuityInheritance.claudeConfigDir;
if (
browserRuntimeEnv &&
!syncBrowserMcpToConfigDir(defaultContinuityInheritance.claudeConfigDir)
) {
throw new Error(
'Browser MCP is enabled, but CCS could not sync the browser MCP config into the inherited Claude instance.'
);
}
}
}
// Dispatch through target adapter for non-claude targets
if (resolvedTarget !== 'claude') {
const adapter = targetAdapter;
if (!adapter) {
console.error(fail(`Target adapter not found for "${resolvedTarget}"`));
process.exit(1);
}
if (!adapter.supportsProfileType('default')) {
console.error(fail(`${adapter.displayName} does not support default profile mode`));
process.exit(1);
}
const creds: TargetCredentials = {
profile: 'default',
baseUrl: process.env['ANTHROPIC_BASE_URL'] || '',
apiKey: process.env['ANTHROPIC_AUTH_TOKEN'] || '',
model: process.env['ANTHROPIC_MODEL'],
provider: resolveDroidProvider({
provider: process.env['CCS_DROID_PROVIDER'] || process.env['DROID_PROVIDER'],
baseUrl: process.env['ANTHROPIC_BASE_URL'],
model: process.env['ANTHROPIC_MODEL'],
}),
reasoningOverride: runtimeReasoningOverride,
runtimeConfigOverrides: codexRuntimeConfigOverrides,
browserRuntimeEnv,
};
if (resolvedTarget === 'droid' && (!creds.baseUrl || !creds.apiKey)) {
console.error(
fail(
`${adapter.displayName} default mode requires ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN`
)
);
console.error(info('Use a settings-based profile instead: ccs glm --target droid'));
process.exit(1);
}
await adapter.prepareCredentials(creds);
const targetArgs = adapter.buildArgs('default', targetRemainingArgs, {
creds,
profileType: 'default',
binaryInfo: targetBinaryInfo || undefined,
});
const targetEnv = adapter.buildEnv(creds, 'default');
adapter.exec(targetArgs, targetEnv, { binaryInfo: targetBinaryInfo || undefined });
return;
}
const launchArgs = resolveNativeClaudeLaunchArgs(
browserRuntimeEnv
? appendBrowserToolArgs(nativeClaudeRemainingArgs)
: nativeClaudeRemainingArgs,
'default',
envVars.CLAUDE_CONFIG_DIR
);
execClaude(claudeCli, launchArgs, envVars);
}
+400
View File
@@ -0,0 +1,400 @@
/**
* Settings dispatch flow — settings-based profiles (glm, glmt, kimi, etc.).
*
* Extracted from src/ccs.ts main() profileInfo.type === 'settings' branch.
* Image-analysis prep is large enough to split; see settings-image-analysis-prep.ts.
*/
import { getSettingsPath, loadSettings } from '../../utils/config-manager';
import { expandPath } from '../../utils/helpers';
import {
validateGlmKey,
validateMiniMaxKey,
validateAnthropicKey,
} from '../../utils/api-key-validator';
import {
ensureWebSearchMcpOrThrow,
displayWebSearchStatus,
getWebSearchHookEnv,
syncWebSearchMcpToConfigDir,
appendThirdPartyWebSearchToolArgs,
createWebSearchTraceContext,
} from '../../utils/websearch-manager';
import {
ensureImageAnalysisMcpOrThrow,
syncImageAnalysisMcpToConfigDir,
appendThirdPartyImageAnalysisToolArgs,
} from '../../utils/image-analysis';
import {
appendBrowserToolArgs,
ensureBrowserMcpOrThrow,
resolveOptionalBrowserAttachRuntime,
syncBrowserMcpToConfigDir,
} from '../../utils/browser';
import { getGlobalEnvConfig } from '../../config/unified-config-loader';
import {
ensureProfileHooks as ensureImageAnalyzerHooks,
removeImageAnalysisProfileHook,
} from '../../utils/hooks/image-analyzer-profile-hook-injector';
import { fail, info, warn } from '../../utils/ui';
import { execClaude, stripAnthropicRoutingEnv, stripBrowserEnv } from '../../utils/shell-executor';
import {
isDeprecatedGlmtProfileName,
normalizeDeprecatedGlmtEnv,
} from '../../utils/glmt-deprecation';
import { createOpenAICompatLaunchSettings } from '../../utils/openai-compat-launch-settings';
import {
resolveDroidProvider,
evaluateTargetRuntimeCompatibility,
type TargetCredentials,
} from '../../targets';
import { resolveCliproxyBridgeMetadata } from '../../api/services/cliproxy-profile-bridge';
import {
buildOpenAICompatProxyEnv,
resolveOpenAICompatProfileConfig,
startOpenAICompatProxy,
} from '../../proxy';
import { resolveSettingsImageAnalysisEnv } from './settings-image-analysis-prep';
import type { ProfileDispatchContext } from '../dispatcher-context';
export async function runSettingsFlow(ctx: ProfileDispatchContext): Promise<void> {
const {
profileInfo,
resolvedTarget,
claudeCli,
targetAdapter,
targetBinaryInfo,
targetRemainingArgs,
nativeClaudeRemainingArgs,
runtimeReasoningOverride,
codexRuntimeConfigOverrides,
claudeBrowserExposure,
claudeAttachConfig,
resolvedSettingsPath: preResolvedSettingsPath,
resolvedSettings: preResolvedSettings,
resolvedCliproxyBridge: preResolvedCliproxyBridge,
resolveProfileContinuityInheritance,
remainingArgs,
} = ctx;
const imageAnalysisMcpReady =
resolvedTarget === 'claude' ? ensureImageAnalysisMcpOrThrow() : true;
const browserAttachRuntime =
resolvedTarget === 'claude' &&
claudeBrowserExposure?.exposeForLaunch &&
claudeAttachConfig?.enabled
? await resolveOptionalBrowserAttachRuntime(claudeAttachConfig)
: undefined;
const browserRuntimeEnv = browserAttachRuntime?.runtimeEnv;
if (browserAttachRuntime?.warning) {
process.stderr.write(`${warn(browserAttachRuntime.warning)}\n`);
}
if (resolvedTarget === 'claude') {
ensureWebSearchMcpOrThrow();
if (browserRuntimeEnv) {
ensureBrowserMcpOrThrow();
}
}
// Display WebSearch status (single line, equilibrium UX)
displayWebSearchStatus();
const continuityInheritance =
resolvedTarget === 'claude'
? await resolveProfileContinuityInheritance({
profileName: profileInfo.name,
profileType: profileInfo.type,
target: resolvedTarget,
})
: {};
if (continuityInheritance.sourceAccount && process.env.CCS_DEBUG) {
console.error(
info(
`Continuity inheritance active: profile "${profileInfo.name}" -> account "${continuityInheritance.sourceAccount}"`
)
);
}
const inheritedClaudeConfigDir = continuityInheritance.claudeConfigDir;
syncWebSearchMcpToConfigDir(inheritedClaudeConfigDir);
syncImageAnalysisMcpToConfigDir(inheritedClaudeConfigDir);
if (
browserRuntimeEnv &&
inheritedClaudeConfigDir &&
!syncBrowserMcpToConfigDir(inheritedClaudeConfigDir)
) {
throw new Error(
'Browser MCP is enabled, but CCS could not sync the browser MCP config into the inherited Claude instance.'
);
}
const expandedSettingsPath =
preResolvedSettingsPath ??
(profileInfo.settingsPath
? expandPath(profileInfo.settingsPath)
: getSettingsPath(profileInfo.name));
const settings = preResolvedSettings ?? loadSettings(expandedSettingsPath);
const cliproxyBridge = preResolvedCliproxyBridge ?? resolveCliproxyBridgeMetadata(settings);
let imageAnalysisFallbackHookReady: boolean | undefined;
if (resolvedTarget === 'claude') {
if (imageAnalysisMcpReady) {
removeImageAnalysisProfileHook(profileInfo.name, expandedSettingsPath);
} else {
imageAnalysisFallbackHookReady = (
await import('../../utils/hooks')
).prepareImageAnalysisFallbackHook();
ensureImageAnalyzerHooks({
profileName: profileInfo.name,
profileType: profileInfo.type,
settingsPath: expandedSettingsPath,
settings,
cliproxyBridge,
sharedHookInstalled: imageAnalysisFallbackHookReady,
});
}
}
if (resolvedTarget !== 'claude') {
const compatibility = evaluateTargetRuntimeCompatibility({
target: resolvedTarget,
profileType: profileInfo.type,
cliproxyBridgeProvider: cliproxyBridge?.provider ?? null,
});
if (!compatibility.supported) {
console.error(
fail(
compatibility.reason ||
`${targetAdapter?.displayName || resolvedTarget} does not support this profile.`
)
);
if (compatibility.suggestion) {
console.error(info(compatibility.suggestion));
}
process.exit(1);
}
}
const rawSettingsEnv = profileInfo.env ?? settings.env ?? {};
const isDeprecatedGlmtProfile = isDeprecatedGlmtProfileName(profileInfo.name);
const glmtNormalization = isDeprecatedGlmtProfile
? normalizeDeprecatedGlmtEnv(rawSettingsEnv)
: null;
const settingsEnv = glmtNormalization?.env ?? rawSettingsEnv;
if (glmtNormalization) {
for (const message of glmtNormalization.warnings) {
console.error(warn(message));
}
}
// Pre-flight validation for Z.AI-compatible profiles.
if (profileInfo.name === 'glm' || isDeprecatedGlmtProfile) {
const apiKey = settingsEnv['ANTHROPIC_AUTH_TOKEN'];
if (apiKey) {
const validation = await validateGlmKey(apiKey, settingsEnv['ANTHROPIC_BASE_URL']);
if (!validation.valid) {
console.error('');
console.error(fail(validation.error || 'API key validation failed'));
if (validation.suggestion) {
console.error('');
console.error(validation.suggestion);
}
console.error('');
console.error(info('To skip validation: CCS_SKIP_PREFLIGHT=1 ccs glm "prompt"'));
process.exit(1);
}
}
}
if (profileInfo.name === 'mm') {
const apiKey = settingsEnv['ANTHROPIC_AUTH_TOKEN'];
if (apiKey) {
const validation = await validateMiniMaxKey(apiKey, settingsEnv['ANTHROPIC_BASE_URL']);
if (!validation.valid) {
console.error('');
console.error(fail(validation.error || 'API key validation failed'));
if (validation.suggestion) {
console.error('');
console.error(validation.suggestion);
}
console.error('');
console.error(info('To skip validation: CCS_SKIP_PREFLIGHT=1 ccs mm "prompt"'));
process.exit(1);
}
}
}
// Pre-flight validation for Anthropic direct profiles (ANTHROPIC_API_KEY + no BASE_URL)
{
const anthropicApiKey = settingsEnv['ANTHROPIC_API_KEY'];
const hasBaseUrl = !!settingsEnv['ANTHROPIC_BASE_URL'];
if (anthropicApiKey && !hasBaseUrl) {
const validation = await validateAnthropicKey(anthropicApiKey);
if (!validation.valid) {
console.error('');
console.error(fail(validation.error || 'API key validation failed'));
if (validation.suggestion) {
console.error('');
console.error(validation.suggestion);
}
console.error('');
console.error(
info(`To skip validation: CCS_SKIP_PREFLIGHT=1 ccs ${profileInfo.name} "prompt"`)
);
process.exit(1);
}
}
}
// Image analysis env resolution (split into sibling helper to stay under LOC budget)
const imageAnalysisEnv = await resolveSettingsImageAnalysisEnv({
profileInfo,
resolvedTarget,
settings,
cliproxyBridge,
imageAnalysisMcpReady,
imageAnalysisFallbackHookReady,
remainingArgs,
targetRemainingArgs,
});
const webSearchEnv = getWebSearchHookEnv();
// Get global env vars (DISABLE_TELEMETRY, etc.) for third-party profiles
const globalEnvConfig = getGlobalEnvConfig();
const globalEnv = globalEnvConfig.enabled ? globalEnvConfig.env : {};
// Log global env injection for visibility (debug mode only)
if (globalEnvConfig.enabled && Object.keys(globalEnv).length > 0 && process.env.CCS_DEBUG) {
const envNames = Object.keys(globalEnv).join(', ');
console.error(info(`Global env: ${envNames}`));
}
// For Claude target launches that already pass `--settings`, keep runtime env free of
// ANTHROPIC routing/auth while preserving non-routing profile env so nested Team/subagent
// sessions can still inherit model intent and other profile-scoped runtime flags.
const settingsRuntimeEnv = stripBrowserEnv({ ...globalEnv, ...settingsEnv });
const claudeRuntimeEnvVars: NodeJS.ProcessEnv = {
...stripAnthropicRoutingEnv(settingsRuntimeEnv),
...(inheritedClaudeConfigDir ? { CLAUDE_CONFIG_DIR: inheritedClaudeConfigDir } : {}),
...webSearchEnv,
...imageAnalysisEnv,
...(browserRuntimeEnv || {}),
CCS_PROFILE_TYPE: 'settings',
CCS_STRIP_INHERITED_ANTHROPIC_ENV: '1',
};
// Non-Claude targets still need effective credentials injected directly.
const envVars: NodeJS.ProcessEnv = {
...settingsRuntimeEnv,
...(inheritedClaudeConfigDir ? { CLAUDE_CONFIG_DIR: inheritedClaudeConfigDir } : {}),
...webSearchEnv,
...imageAnalysisEnv,
...(browserRuntimeEnv || {}),
CCS_PROFILE_TYPE: 'settings',
};
// Dispatch through target adapter for non-claude targets
if (resolvedTarget !== 'claude') {
const adapter = targetAdapter;
if (!adapter) {
console.error(fail(`Target adapter not found for "${resolvedTarget}"`));
process.exit(1);
}
const directAnthropicBaseUrl =
settingsEnv['ANTHROPIC_BASE_URL'] ||
(settingsEnv['ANTHROPIC_API_KEY'] ? 'https://api.anthropic.com' : '');
const creds: TargetCredentials = {
profile: profileInfo.name,
baseUrl: directAnthropicBaseUrl,
apiKey: settingsEnv['ANTHROPIC_AUTH_TOKEN'] || settingsEnv['ANTHROPIC_API_KEY'] || '',
model: settingsEnv['ANTHROPIC_MODEL'],
provider: resolveDroidProvider({
provider: settingsEnv['CCS_DROID_PROVIDER'] || settingsEnv['DROID_PROVIDER'],
baseUrl: directAnthropicBaseUrl,
model: settingsEnv['ANTHROPIC_MODEL'],
}),
reasoningOverride: runtimeReasoningOverride,
runtimeConfigOverrides: codexRuntimeConfigOverrides,
envVars,
};
await adapter.prepareCredentials(creds);
const targetArgs = adapter.buildArgs(profileInfo.name, targetRemainingArgs, {
creds,
profileType: profileInfo.type,
binaryInfo: targetBinaryInfo || undefined,
});
const targetEnv = adapter.buildEnv(creds, profileInfo.type);
adapter.exec(targetArgs, targetEnv, { binaryInfo: targetBinaryInfo || undefined });
return;
}
const imageAnalysisArgs = imageAnalysisMcpReady
? appendThirdPartyImageAnalysisToolArgs(nativeClaudeRemainingArgs)
: nativeClaudeRemainingArgs;
const browserArgs = browserRuntimeEnv
? appendBrowserToolArgs(imageAnalysisArgs)
: imageAnalysisArgs;
const openAICompatProfile = resolveOpenAICompatProfileConfig(
profileInfo.name,
expandedSettingsPath,
settingsEnv
);
if (openAICompatProfile) {
const proxyStart = await startOpenAICompatProxy(openAICompatProfile, {
insecure: openAICompatProfile.insecure,
});
if (!proxyStart.success) {
console.error(fail(proxyStart.error || 'Failed to start local OpenAI-compatible proxy'));
process.exit(1);
}
console.error(
info(
`Using local OpenAI-compatible proxy for "${profileInfo.name}" on port ${proxyStart.port}`
)
);
const proxyEnv = {
...envVars,
...buildOpenAICompatProxyEnv(
openAICompatProfile,
proxyStart.port,
proxyStart.authToken || '',
inheritedClaudeConfigDir
),
};
delete proxyEnv.ANTHROPIC_API_KEY;
const launchSettings = createOpenAICompatLaunchSettings(expandedSettingsPath, settings);
const launchArgs = [
'--settings',
launchSettings.settingsPath,
...appendThirdPartyWebSearchToolArgs(browserArgs),
];
const traceEnv = createWebSearchTraceContext({
launcher: 'ccs.settings-profile.proxy',
args: launchArgs,
profile: profileInfo.name,
profileType: profileInfo.type,
settingsPath: expandedSettingsPath,
});
execClaude(claudeCli, launchArgs, { ...proxyEnv, ...traceEnv }, launchSettings.cleanup);
return;
}
const launchArgs = [
'--settings',
expandedSettingsPath,
...appendThirdPartyWebSearchToolArgs(browserArgs),
];
const traceEnv = createWebSearchTraceContext({
launcher: 'ccs.settings-profile',
args: launchArgs,
profile: profileInfo.name,
profileType: profileInfo.type,
settingsPath: expandedSettingsPath,
});
execClaude(claudeCli, launchArgs, { ...claudeRuntimeEnvVars, ...traceEnv });
}
@@ -0,0 +1,126 @@
/**
* Image analysis environment preparation for settings-based profiles.
*
* Split from settings-flow.ts to keep that file under the 300 LOC budget.
* Resolves runtime status, connection, and env overrides for image analysis MCP.
*/
import { warn, info } from '../../utils/ui';
import {
applyImageAnalysisRuntimeOverrides,
getImageAnalysisHookEnv,
resolveImageAnalysisRuntimeConnection,
resolveImageAnalysisRuntimeStatus,
} from '../../utils/hooks';
import { ensureCliproxyService } from '../../cliproxy';
import { CLIPROXY_DEFAULT_PORT } from '../../cliproxy/config/port-manager';
import type { ProfileDetectionResult } from '../../auth/profile-detector';
import type { loadSettings } from '../../utils/config-manager';
import type { resolveCliproxyBridgeMetadata } from '../../api/services/cliproxy-profile-bridge';
import type { resolveTargetType } from '../../targets/target-resolver';
export interface SettingsImageAnalysisPrepContext {
profileInfo: ProfileDetectionResult;
resolvedTarget: ReturnType<typeof resolveTargetType>;
settings: ReturnType<typeof loadSettings>;
cliproxyBridge: ReturnType<typeof resolveCliproxyBridgeMetadata> | undefined;
imageAnalysisMcpReady: boolean;
imageAnalysisFallbackHookReady: boolean | undefined;
remainingArgs: string[];
targetRemainingArgs: string[];
}
/**
* Resolve the complete image analysis env vars for a settings-based profile launch.
* Handles native-read fallback and proxy-stopped recovery.
*/
export async function resolveSettingsImageAnalysisEnv(
ctx: SettingsImageAnalysisPrepContext
): Promise<NodeJS.ProcessEnv> {
const {
profileInfo,
resolvedTarget,
settings,
cliproxyBridge,
imageAnalysisMcpReady,
imageAnalysisFallbackHookReady,
remainingArgs,
targetRemainingArgs,
} = ctx;
const imageAnalysisStatus = await resolveImageAnalysisRuntimeStatus({
profileName: profileInfo.name,
profileType: profileInfo.type,
settings,
cliproxyBridge,
sharedHookInstalled: imageAnalysisFallbackHookReady,
});
const runtimeConnection = resolveImageAnalysisRuntimeConnection();
let imageAnalysisEnv = getImageAnalysisHookEnv({
profileName: profileInfo.name,
profileType: profileInfo.type,
settings,
cliproxyBridge,
});
imageAnalysisEnv = applyImageAnalysisRuntimeOverrides(imageAnalysisEnv, {
backendId: imageAnalysisStatus.backendId,
model: imageAnalysisStatus.model,
runtimePath: imageAnalysisStatus.runtimePath,
baseUrl: runtimeConnection.baseUrl,
apiKey: runtimeConnection.apiKey,
allowSelfSigned: runtimeConnection.allowSelfSigned,
});
imageAnalysisEnv = {
...imageAnalysisEnv,
CCS_IMAGE_ANALYSIS_SKIP_HOOK: resolvedTarget === 'claude' && imageAnalysisMcpReady ? '1' : '0',
};
const imageAnalysisProvider = imageAnalysisEnv['CCS_CURRENT_PROVIDER'];
if (
resolvedTarget === 'claude' &&
imageAnalysisEnv['CCS_IMAGE_ANALYSIS_SKIP'] !== '1' &&
imageAnalysisProvider
) {
const verboseProxyLaunch =
remainingArgs.includes('--verbose') ||
remainingArgs.includes('-v') ||
targetRemainingArgs.includes('--verbose') ||
targetRemainingArgs.includes('-v');
if (imageAnalysisStatus.effectiveRuntimeMode === 'native-read') {
console.error(
info(
`${imageAnalysisStatus.effectiveRuntimeReason || `Image analysis via ${imageAnalysisProvider} is unavailable.`} This session will use native Read.`
)
);
imageAnalysisEnv = {
...imageAnalysisEnv,
CCS_CURRENT_PROVIDER: '',
CCS_IMAGE_ANALYSIS_SKIP: '1',
CCS_IMAGE_ANALYSIS_RUNTIME_PATH: '',
CCS_IMAGE_ANALYSIS_RUNTIME_BASE_URL: '',
CCS_IMAGE_ANALYSIS_RUNTIME_API_KEY: '',
CCS_IMAGE_ANALYSIS_RUNTIME_ALLOW_SELF_SIGNED: '0',
};
} else if (imageAnalysisStatus.proxyReadiness === 'stopped') {
const ensureServiceResult = await ensureCliproxyService(
CLIPROXY_DEFAULT_PORT,
verboseProxyLaunch
);
if (!ensureServiceResult.started) {
console.error(
warn(
`Image analysis via ${imageAnalysisProvider} is unavailable because CCS could not start the local CLIProxy service. This session will use native Read.`
)
);
imageAnalysisEnv = {
...imageAnalysisEnv,
CCS_CURRENT_PROVIDER: '',
CCS_IMAGE_ANALYSIS_SKIP: '1',
};
}
}
}
return imageAnalysisEnv;
}
+39 -1
View File
@@ -1,15 +1,25 @@
/**
* Native target execution — short-circuit dispatch for passthrough flag commands.
* Native target execution — short-circuit dispatch for passthrough flag commands,
* and top-level profile dispatcher (Phase E switch).
*
* Extracted from src/ccs.ts (lines 291-295, 394-426).
* Handles direct execution of native Codex flag passthrough (--help, --version, etc.)
* before the main profile dispatch loop runs.
*
* dispatchProfile() collapses the 6-branch switch in main() to a single call.
*/
import { fail, info } from '../utils/ui';
import { getTarget } from '../targets';
import { getNativeCodexPassthroughArgs } from './cli-argument-parser';
import { runCliproxyFlow } from './flows/cliproxy-flow';
import { runCopilotFlow } from './flows/copilot-flow';
import { runCursorFlow } from './flows/cursor-flow';
import { runSettingsFlow } from './flows/settings-flow';
import { runAccountFlow } from './flows/account-flow';
import { runDefaultFlow } from './flows/default-flow';
import type { TargetCredentials } from '../targets';
import type { ProfileDispatchContext } from './dispatcher-context';
// ========== Interfaces ==========
@@ -54,3 +64,31 @@ export function execNativeCodexFlagCommand(args: string[]): void {
const targetEnv = adapter.buildEnv(creds, 'default');
adapter.exec(builtArgs, targetEnv, { binaryInfo });
}
// ========== Profile Dispatcher ==========
/**
* Dispatch to the correct per-profile-type flow.
*
* Collapses the 6-branch if/else-if switch that previously lived in main() into a
* single call site. The headless -p delegation short-circuit is handled in main()
* before this function is called.
*/
export async function dispatchProfile(ctx: ProfileDispatchContext): Promise<void> {
const { profileInfo } = ctx;
switch (profileInfo.type) {
case 'cliproxy':
return runCliproxyFlow(ctx);
case 'copilot':
return runCopilotFlow(ctx);
case 'cursor':
return runCursorFlow(ctx);
case 'settings':
return runSettingsFlow(ctx);
case 'account':
return runAccountFlow(ctx);
default:
return runDefaultFlow(ctx);
}
}