fix(cli): clearer profile/proxy/api errors and rich not-found suggestions

Move resolveProfileAndTarget() inside the try block in ccs.ts so a missing
profile shows suggestions/available-profiles instead of a generic error. Use a
typed ProfileError for proxy start on an unknown profile (drops the irrelevant
install hint). api create: honor --yes for the OpenRouter model browser, show
ccs-codex/ccsx aliases for --target codex, and allow ANTHROPIC_EXTRA_MODELS on
import.
This commit is contained in:
Tam Nhu Tran committed 2026-06-22 12:06:58 -04:00
1 parent 191396149f
commit 9258aed1ff
4 files changed
+68 -34

No files matched your search

@@ -23,6 +23,8 @@ const ALLOWED_ANTHROPIC_ENV_KEYS = new Set<string>([
'ANTHROPIC_BASE_URL', 'ANTHROPIC_BASE_URL',
'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_AUTH_TOKEN',
'ANTHROPIC_API_KEY', 'ANTHROPIC_API_KEY',
// Written by profile-writer when --extra-models is supplied
'ANTHROPIC_EXTRA_MODELS',
...MODEL_ENV_KEYS, ...MODEL_ENV_KEYS,
]); ]);
+30 -24
View File
@@ -46,31 +46,37 @@ async function main(): Promise<void> {
return; return;
} }
// Phase C: profile + target detection (extracted to dispatcher/profile-resolver.ts)
const resolvedProfile = await resolveProfileAndTarget({ args, browserLaunchOverride, cliLogger });
const { profile, profileInfo, resolvedTarget, nativeClaudeRemainingArgs } = resolvedProfile;
// Dynamic imports needed by Phase E flows — preserve original load ordering.
const InstanceManagerModule = await import('./management/instance-manager');
const InstanceManager = InstanceManagerModule.default;
const ProfileRegistryModule = await import('./auth/profile-registry');
const ProfileRegistry = ProfileRegistryModule.default;
const AccountContextModule = await import('./auth/account-context');
const { resolveAccountContextPolicy, isAccountContextMetadata } = AccountContextModule;
const ProfileContinuityModule = await import('./auth/profile-continuity-inheritance');
const { resolveProfileContinuityInheritance } = ProfileContinuityModule;
// Build full dispatch context (Phase E)
const dispatchCtx = {
...resolvedProfile,
InstanceManager,
ProfileRegistry,
resolveAccountContextPolicy,
isAccountContextMetadata,
resolveProfileContinuityInheritance,
};
try { try {
// Phase C: profile + target detection — inside try so ProfileNotFoundError
// reaches the rich showProfileNotFound() handler below instead of falling
// through to the bare unhandledRejection handler.
const resolvedProfile = await resolveProfileAndTarget({
args,
browserLaunchOverride,
cliLogger,
});
const { profile, profileInfo, resolvedTarget, nativeClaudeRemainingArgs } = resolvedProfile;
// Dynamic imports needed by Phase E flows — preserve original load ordering.
const InstanceManagerModule = await import('./management/instance-manager');
const InstanceManager = InstanceManagerModule.default;
const ProfileRegistryModule = await import('./auth/profile-registry');
const ProfileRegistry = ProfileRegistryModule.default;
const AccountContextModule = await import('./auth/account-context');
const { resolveAccountContextPolicy, isAccountContextMetadata } = AccountContextModule;
const ProfileContinuityModule = await import('./auth/profile-continuity-inheritance');
const { resolveProfileContinuityInheritance } = ProfileContinuityModule;
// Build full dispatch context (Phase E)
const dispatchCtx = {
...resolvedProfile,
InstanceManager,
ProfileRegistry,
resolveAccountContextPolicy,
isAccountContextMetadata,
resolveProfileContinuityInheritance,
};
// 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
// through normal adapter dispatch logic. // through normal adapter dispatch logic.
+22 -8
View File
@@ -196,14 +196,17 @@ async function resolveModelConfiguration(
if (isOpenRouterUrl(baseUrl) && !providedModel) { if (isOpenRouterUrl(baseUrl) && !providedModel) {
console.log(''); console.log('');
console.log(info('OpenRouter detected!')); console.log(info('OpenRouter detected!'));
const useInteractive = await InteractivePrompt.confirm('Browse models interactively?', { // --yes/-y suppresses interactive model browser; use preset defaultModel instead
default: true, if (!yes) {
}); const useInteractive = await InteractivePrompt.confirm('Browse models interactively?', {
if (useInteractive) { default: true,
const selection = await pickOpenRouterModel(); });
if (selection) { if (useInteractive) {
openRouterModel = selection.model; const selection = await pickOpenRouterModel();
openRouterTierMapping = selection.tierMapping; if (selection) {
openRouterModel = selection.model;
openRouterTierMapping = selection.tierMapping;
}
} }
} }
console.log(''); console.log('');
@@ -571,6 +574,17 @@ export async function handleApiCreateCommand(args: string[]): Promise<void> {
console.log( console.log(
` ${color(`ccs ${name} --target claude "your prompt"`, 'command')} ${dim('# override to Claude')}` ` ${color(`ccs ${name} --target claude "your prompt"`, 'command')} ${dim('# override to Claude')}`
); );
} else if (target === 'codex') {
console.log(
` ${color(`ccs ${name} "your prompt"`, 'command')} ${dim('# uses Codex CLI by default')}`
);
console.log(
` ${color(`ccs-codex ${name} "your prompt"`, 'command')} ${dim('# explicit Codex alias')}`
);
console.log(` ${color(`ccsx ${name} "your prompt"`, 'command')} ${dim('# short alias')}`);
console.log(
` ${color(`ccs ${name} --target claude "your prompt"`, 'command')} ${dim('# override to Claude')}`
);
} else { } else {
console.log( console.log(
` ${color(`ccs ${name} "your prompt"`, 'command')} ${dim('# uses claude by default')}` ` ${color(`ccs ${name} "your prompt"`, 'command')} ${dim('# uses claude by default')}`
+14 -2
View File
@@ -10,7 +10,8 @@ import {
startOpenAICompatProxy, startOpenAICompatProxy,
stopOpenAICompatProxy, stopOpenAICompatProxy,
} from '../proxy'; } from '../proxy';
import { loadSettings } from '../config/config-loader-facade'; import { loadConfigSafe, loadSettings } from '../config/config-loader-facade';
import { ProfileError } from '../errors';
function parseOptionValue(args: string[], key: string): string | undefined { function parseOptionValue(args: string[], key: string): string | undefined {
const exactIndex = args.findIndex((arg) => arg === key); const exactIndex = args.findIndex((arg) => arg === key);
@@ -82,11 +83,22 @@ function showHelp(): number {
} }
function resolveProfile(profileName: string) { function resolveProfile(profileName: string) {
// Pre-check profile in config to avoid the generic error() + npm-install hint from getSettingsPath.
const config = loadConfigSafe();
if (!config.profiles[profileName]) {
throw new ProfileError(
`Profile "${profileName}" is not configured for an OpenAI-compatible endpoint`,
profileName
);
}
const settingsPath = expandPath(getSettingsPath(profileName)); const settingsPath = expandPath(getSettingsPath(profileName));
const settings = loadSettings(settingsPath); const settings = loadSettings(settingsPath);
const profile = resolveOpenAICompatProfileConfig(profileName, settingsPath, settings.env || {}); const profile = resolveOpenAICompatProfileConfig(profileName, settingsPath, settings.env || {});
if (!profile) { if (!profile) {
throw new Error(`Profile "${profileName}" is not configured for an OpenAI-compatible endpoint`); throw new ProfileError(
`Profile "${profileName}" is not configured for an OpenAI-compatible endpoint`,
profileName
);
} }
return profile; return profile;
} }