mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 20:17:45 +00:00
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
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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<string, string>;
|
||||
/** 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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user