mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 00:22:34 +00:00
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:
+18
-888
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user