From 4c74e92cc46afed9c8232944a2a443709b130a2c Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sat, 20 Dec 2025 21:07:27 -0500 Subject: [PATCH] feat(api): unify profile management with config-aware services - profile-reader: fix isApiProfileConfigured to check settings.json fallback - profile-reader: exclude 'default' profile (native Claude) from listings - profile-routes: use createApiProfile/removeApiProfile services - provider-presets: add category field (recommended/alternative) - recovery-manager: remove auto-creation of GLM/GLMT/Kimi profiles --- src/api/services/profile-reader.ts | 45 +++++++++++--- src/api/services/provider-presets.ts | 9 +++ src/management/recovery-manager.ts | 17 ++---- src/web-server/routes/profile-routes.ts | 79 +++++++++++-------------- 4 files changed, 86 insertions(+), 64 deletions(-) diff --git a/src/api/services/profile-reader.ts b/src/api/services/profile-reader.ts index d28b3149..fcc6d30e 100644 --- a/src/api/services/profile-reader.ts +++ b/src/api/services/profile-reader.ts @@ -33,14 +33,32 @@ export function apiProfileExists(name: string): boolean { */ export function isApiProfileConfigured(apiName: string): boolean { try { - if (isUnifiedMode()) { - const secrets = getProfileSecrets(apiName); - const token = secrets?.ANTHROPIC_AUTH_TOKEN || ''; - return token.length > 0 && !token.includes('YOUR_') && !token.includes('your-'); - } - // Legacy: check settings.json file const ccsDir = getCcsDir(); const settingsPath = path.join(ccsDir, `${apiName}.settings.json`); + + if (isUnifiedMode()) { + // Check secrets.yaml first + const secrets = getProfileSecrets(apiName); + const secretToken = secrets?.ANTHROPIC_AUTH_TOKEN || ''; + if ( + secretToken.length > 0 && + !secretToken.includes('YOUR_') && + !secretToken.includes('your-') + ) { + return true; + } + + // Fallback: check settings.json file (profiles created via UI store keys here) + if (fs.existsSync(settingsPath)) { + const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); + const token = settings?.env?.ANTHROPIC_AUTH_TOKEN || ''; + return token.length > 0 && !token.includes('YOUR_') && !token.includes('your-'); + } + + return false; + } + + // Legacy: check settings.json file if (!fs.existsSync(settingsPath)) return false; const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); @@ -53,6 +71,9 @@ export function isApiProfileConfigured(apiName: string): boolean { /** * List all API profiles + * + * Note: The 'default' profile (pointing to ~/.claude/settings.json) is excluded + * as it represents the user's native Claude subscription, not an API profile. */ export function listApiProfiles(): ApiListResult { const profiles: ApiProfileInfo[] = []; @@ -60,10 +81,14 @@ export function listApiProfiles(): ApiListResult { if (isUnifiedMode()) { const unifiedConfig = loadOrCreateUnifiedConfig(); - for (const name of Object.keys(unifiedConfig.profiles)) { + for (const [name, profile] of Object.entries(unifiedConfig.profiles)) { + // Skip 'default' profile - it's the user's native Claude settings + if (name === 'default' && profile.settings?.includes('.claude/settings.json')) { + continue; + } profiles.push({ name, - settingsPath: 'config.yaml', + settingsPath: profile.settings || 'config.yaml', isConfigured: isApiProfileConfigured(name), configSource: 'unified', }); @@ -79,6 +104,10 @@ export function listApiProfiles(): ApiListResult { } else { const config = loadConfig(); for (const [name, settingsPath] of Object.entries(config.profiles)) { + // Skip 'default' profile - it's the user's native Claude settings + if (name === 'default' && (settingsPath as string).includes('.claude/settings.json')) { + continue; + } profiles.push({ name, settingsPath: settingsPath as string, diff --git a/src/api/services/provider-presets.ts b/src/api/services/provider-presets.ts index 6b0411d2..597a102a 100644 --- a/src/api/services/provider-presets.ts +++ b/src/api/services/provider-presets.ts @@ -5,6 +5,8 @@ * Mirrors the UI presets in ui/src/lib/provider-presets.ts */ +export type PresetCategory = 'recommended' | 'alternative'; + export interface ProviderPreset { id: string; name: string; @@ -14,6 +16,7 @@ export interface ProviderPreset { defaultModel: string; apiKeyPlaceholder: string; apiKeyHint: string; + category: PresetCategory; /** Additional env vars for thinking mode, etc. */ extraEnv?: Record; /** Enable always thinking mode */ @@ -28,6 +31,7 @@ export const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1'; * NOTE: Keep in sync with ui/src/lib/provider-presets.ts */ export const PROVIDER_PRESETS: ProviderPreset[] = [ + // Recommended { id: 'openrouter', name: 'OpenRouter', @@ -37,7 +41,9 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ defaultModel: 'anthropic/claude-sonnet-4', apiKeyPlaceholder: 'sk-or-...', apiKeyHint: 'Get your API key at openrouter.ai/keys', + category: 'recommended', }, + // Alternative providers { id: 'glm', name: 'GLM', @@ -47,6 +53,7 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ defaultModel: 'glm-4.6', apiKeyPlaceholder: 'ghp_...', apiKeyHint: 'Get your API key from Z.AI', + category: 'alternative', }, { id: 'glmt', @@ -57,6 +64,7 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ defaultModel: 'glm-4.6', apiKeyPlaceholder: 'ghp_...', apiKeyHint: 'Same API key as GLM', + category: 'alternative', extraEnv: { ANTHROPIC_TEMPERATURE: '0.2', ANTHROPIC_MAX_TOKENS: '65536', @@ -76,6 +84,7 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ defaultModel: 'kimi-k2-thinking-turbo', apiKeyPlaceholder: 'sk-...', apiKeyHint: 'Get your API key from Moonshot AI', + category: 'alternative', alwaysThinkingEnabled: true, }, ]; diff --git a/src/management/recovery-manager.ts b/src/management/recovery-manager.ts index edff2f23..9c43dd1e 100644 --- a/src/management/recovery-manager.ts +++ b/src/management/recovery-manager.ts @@ -69,14 +69,9 @@ class RecoveryManager { } // Create default config (matches postinstall.js) - // NOTE: No 'default' entry - when no profile specified, CCS passes through - // to Claude's native auth without --settings flag + // NOTE: Empty profiles - users create profiles via `ccs api create` or UI const defaultConfig = { - profiles: { - glm: '~/.ccs/glm.settings.json', - glmt: '~/.ccs/glmt.settings.json', - kimi: '~/.ccs/kimi.settings.json', - }, + profiles: {}, }; const tmpPath = `${configPath}.tmp`; @@ -274,6 +269,9 @@ class RecoveryManager { /** * Run all recovery operations (lazy initialization) * Mirrors postinstall.js behavior + * + * NOTE: GLM/GLMT/Kimi profiles are NOT auto-created. + * Users should create them via `ccs api create --preset glm` or the UI. */ recoverAll(): boolean { this.recovered = []; @@ -283,11 +281,8 @@ class RecoveryManager { this.ensureSharedDirectories(); this.ensureClaudeSettings(); - // Config files + // Config files (core only - no GLM/GLMT/Kimi auto-creation) this.ensureConfigJson(); - this.ensureGlmSettings(); - this.ensureGlmtSettings(); - this.ensureKimiSettings(); // Shell completions this.ensureShellCompletions(); diff --git a/src/web-server/routes/profile-routes.ts b/src/web-server/routes/profile-routes.ts index db0c382f..311d5426 100644 --- a/src/web-server/routes/profile-routes.ts +++ b/src/web-server/routes/profile-routes.ts @@ -1,5 +1,7 @@ /** * Profile Routes - CRUD operations for user profiles and accounts + * + * Uses unified config (config.yaml) when available, falls back to legacy (config.json). */ import { Router, Request, Response } from 'express'; @@ -7,13 +9,9 @@ import * as fs from 'fs'; import * as path from 'path'; import { getCcsDir } from '../../utils/config-manager'; import { isReservedName, RESERVED_PROFILE_NAMES } from '../../config/reserved-names'; -import { - readConfigSafe, - writeConfig, - isConfigured, - createSettingsFile, - updateSettingsFile, -} from './route-helpers'; +import { createApiProfile, removeApiProfile } from '../../api/services/profile-writer'; +import { apiProfileExists, listApiProfiles } from '../../api/services/profile-reader'; +import { updateSettingsFile } from './route-helpers'; const router = Router(); @@ -23,13 +21,13 @@ const router = Router(); * GET /api/profiles - List all profiles */ router.get('/', (_req: Request, res: Response) => { - const config = readConfigSafe(); - const profiles = Object.entries(config.profiles).map(([name, settingsPath]) => ({ - name, - settingsPath, - configured: isConfigured(name, config), + const result = listApiProfiles(); + // Map isConfigured -> configured for UI compatibility + const profiles = result.profiles.map((p) => ({ + name: p.name, + settingsPath: p.settingsPath, + configured: p.isConfigured, })); - res.json({ profiles }); }); @@ -53,31 +51,26 @@ router.post('/', (req: Request, res: Response): void => { return; } - const config = readConfigSafe(); - - if (config.profiles[name]) { + // Check if profile already exists (uses unified config when available) + if (apiProfileExists(name)) { res.status(409).json({ error: 'Profile already exists' }); return; } - // Ensure .ccs directory exists - if (!fs.existsSync(getCcsDir())) { - fs.mkdirSync(getCcsDir(), { recursive: true }); - } - - // Create settings file with model mapping - const settingsPath = createSettingsFile(name, baseUrl, apiKey, { - model, - opusModel, - sonnetModel, - haikuModel, + // Create profile using unified-config-aware service + const result = createApiProfile(name, baseUrl, apiKey, { + default: model || '', + opus: opusModel || model || '', + sonnet: sonnetModel || model || '', + haiku: haikuModel || model || '', }); - // Update config - config.profiles[name] = settingsPath; - writeConfig(config); + if (!result.success) { + res.status(500).json({ error: result.error || 'Failed to create profile' }); + return; + } - res.status(201).json({ name, settingsPath }); + res.status(201).json({ name, settingsPath: result.settingsFile }); }); /** @@ -87,9 +80,8 @@ router.put('/:name', (req: Request, res: Response): void => { const { name } = req.params; const { baseUrl, apiKey, model, opusModel, sonnetModel, haikuModel } = req.body; - const config = readConfigSafe(); - - if (!config.profiles[name]) { + // Check if profile exists (uses unified config when available) + if (!apiProfileExists(name)) { res.status(404).json({ error: 'Profile not found' }); return; } @@ -108,22 +100,19 @@ router.put('/:name', (req: Request, res: Response): void => { router.delete('/:name', (req: Request, res: Response): void => { const { name } = req.params; - const config = readConfigSafe(); - - if (!config.profiles[name]) { + // Check if profile exists (uses unified config when available) + if (!apiProfileExists(name)) { res.status(404).json({ error: 'Profile not found' }); return; } - // Delete settings file - const settingsPath = path.join(getCcsDir(), `${name}.settings.json`); - if (fs.existsSync(settingsPath)) { - fs.unlinkSync(settingsPath); - } + // Remove profile using unified-config-aware service + const result = removeApiProfile(name); - // Remove from config - delete config.profiles[name]; - writeConfig(config); + if (!result.success) { + res.status(500).json({ error: result.error || 'Failed to delete profile' }); + return; + } res.json({ name, deleted: true }); });