mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 06:15:03 +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:
1 parent
a807de6f4c
commit
0910a75850
10 files changed
+1153
-889
No files matched your search
+18
-888
@@ -1,84 +1,17 @@
|
|||||||
import './utils/fetch-proxy-setup';
|
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 { ErrorManager } from './utils/error-manager';
|
||||||
import {
|
import { fail } from './utils/ui';
|
||||||
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 centralized error handling
|
// Import centralized error handling
|
||||||
import { handleError, runCleanup } from './errors';
|
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 { createLogger, runWithRequestId } from './services/logging';
|
||||||
// Import target adapter system
|
// Import target adapter system
|
||||||
import {
|
import { registerTarget, ClaudeAdapter, DroidAdapter, CodexAdapter } from './targets';
|
||||||
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 extracted dispatcher modules
|
// Import extracted dispatcher modules
|
||||||
import { bootstrapAndParseEarlyCli } from './dispatcher/cli-argument-parser';
|
import { bootstrapAndParseEarlyCli } from './dispatcher/cli-argument-parser';
|
||||||
import { resolveNativeClaudeLaunchArgs } from './dispatcher/environment-builder';
|
import { type ProfileError, dispatchProfile } from './dispatcher/target-executor';
|
||||||
import { type ProfileError } from './dispatcher/target-executor';
|
|
||||||
import { runPreDispatchHandlers } from './dispatcher/pre-dispatch';
|
import { runPreDispatchHandlers } from './dispatcher/pre-dispatch';
|
||||||
import { resolveProfileAndTarget } from './dispatcher/profile-resolver';
|
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)
|
// Phase C: profile + target detection (extracted to dispatcher/profile-resolver.ts)
|
||||||
const {
|
const resolvedProfile = await resolveProfileAndTarget({ args, browserLaunchOverride, cliLogger });
|
||||||
profile,
|
const { profile, profileInfo, resolvedTarget, nativeClaudeRemainingArgs } = resolvedProfile;
|
||||||
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 });
|
|
||||||
|
|
||||||
// Re-assign mutable state that Phase E flows may update
|
// Dynamic imports needed by Phase E flows — preserve original load ordering.
|
||||||
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).
|
|
||||||
const InstanceManagerModule = await import('./management/instance-manager');
|
const InstanceManagerModule = await import('./management/instance-manager');
|
||||||
const InstanceManager = InstanceManagerModule.default;
|
const InstanceManager = InstanceManagerModule.default;
|
||||||
const ProfileRegistryModule = await import('./auth/profile-registry');
|
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 ProfileContinuityModule = await import('./auth/profile-continuity-inheritance');
|
||||||
const { resolveProfileContinuityInheritance } = ProfileContinuityModule;
|
const { resolveProfileContinuityInheritance } = ProfileContinuityModule;
|
||||||
|
|
||||||
|
// Build full dispatch context (Phase E)
|
||||||
|
const dispatchCtx = {
|
||||||
|
...resolvedProfile,
|
||||||
|
InstanceManager,
|
||||||
|
ProfileRegistry,
|
||||||
|
resolveAccountContextPolicy,
|
||||||
|
isAccountContextMetadata,
|
||||||
|
resolveProfileContinuityInheritance,
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Special case: headless delegation (-p/--prompt)
|
// Special case: headless delegation (-p/--prompt)
|
||||||
// Keep existing behavior for Claude targets only; non-claude targets must continue
|
// 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') {
|
// Phase E: dispatch to per-profile-type flow (all 6 branches now live in flows/)
|
||||||
// CLIPROXY FLOW: OAuth-based profiles (gemini, codex, agy, qwen) or user-defined variants
|
await dispatchProfile(dispatchCtx);
|
||||||
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);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as ProfileError;
|
const err = error as ProfileError;
|
||||||
// Check if this is a profile not found error with suggestions
|
// 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).
|
* Extracted from src/ccs.ts (lines 291-295, 394-426).
|
||||||
* Handles direct execution of native Codex flag passthrough (--help, --version, etc.)
|
* Handles direct execution of native Codex flag passthrough (--help, --version, etc.)
|
||||||
* before the main profile dispatch loop runs.
|
* 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 { fail, info } from '../utils/ui';
|
||||||
import { getTarget } from '../targets';
|
import { getTarget } from '../targets';
|
||||||
import { getNativeCodexPassthroughArgs } from './cli-argument-parser';
|
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 { TargetCredentials } from '../targets';
|
||||||
|
import type { ProfileDispatchContext } from './dispatcher-context';
|
||||||
|
|
||||||
// ========== Interfaces ==========
|
// ========== Interfaces ==========
|
||||||
|
|
||||||
@@ -54,3 +64,31 @@ export function execNativeCodexFlagCommand(args: string[]): void {
|
|||||||
const targetEnv = adapter.buildEnv(creds, 'default');
|
const targetEnv = adapter.buildEnv(creds, 'default');
|
||||||
adapter.exec(builtArgs, targetEnv, { binaryInfo });
|
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