mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 18:18:43 +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 {
|
export function isApiProfileConfigured(apiName: string): boolean {
|
||||||
try {
|
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 ccsDir = getCcsDir();
|
||||||
const settingsPath = path.join(ccsDir, `${apiName}.settings.json`);
|
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;
|
if (!fs.existsSync(settingsPath)) return false;
|
||||||
|
|
||||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||||
@@ -53,6 +71,9 @@ export function isApiProfileConfigured(apiName: string): boolean {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* List all API profiles
|
* 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 {
|
export function listApiProfiles(): ApiListResult {
|
||||||
const profiles: ApiProfileInfo[] = [];
|
const profiles: ApiProfileInfo[] = [];
|
||||||
@@ -60,10 +81,14 @@ export function listApiProfiles(): ApiListResult {
|
|||||||
|
|
||||||
if (isUnifiedMode()) {
|
if (isUnifiedMode()) {
|
||||||
const unifiedConfig = loadOrCreateUnifiedConfig();
|
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({
|
profiles.push({
|
||||||
name,
|
name,
|
||||||
settingsPath: 'config.yaml',
|
settingsPath: profile.settings || 'config.yaml',
|
||||||
isConfigured: isApiProfileConfigured(name),
|
isConfigured: isApiProfileConfigured(name),
|
||||||
configSource: 'unified',
|
configSource: 'unified',
|
||||||
});
|
});
|
||||||
@@ -79,6 +104,10 @@ export function listApiProfiles(): ApiListResult {
|
|||||||
} else {
|
} else {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
for (const [name, settingsPath] of Object.entries(config.profiles)) {
|
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({
|
profiles.push({
|
||||||
name,
|
name,
|
||||||
settingsPath: settingsPath as string,
|
settingsPath: settingsPath as string,
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
* Mirrors the UI presets in ui/src/lib/provider-presets.ts
|
* Mirrors the UI presets in ui/src/lib/provider-presets.ts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export type PresetCategory = 'recommended' | 'alternative';
|
||||||
|
|
||||||
export interface ProviderPreset {
|
export interface ProviderPreset {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -14,6 +16,7 @@ export interface ProviderPreset {
|
|||||||
defaultModel: string;
|
defaultModel: string;
|
||||||
apiKeyPlaceholder: string;
|
apiKeyPlaceholder: string;
|
||||||
apiKeyHint: string;
|
apiKeyHint: string;
|
||||||
|
category: PresetCategory;
|
||||||
/** Additional env vars for thinking mode, etc. */
|
/** Additional env vars for thinking mode, etc. */
|
||||||
extraEnv?: Record<string, string>;
|
extraEnv?: Record<string, string>;
|
||||||
/** Enable always thinking mode */
|
/** 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
|
* NOTE: Keep in sync with ui/src/lib/provider-presets.ts
|
||||||
*/
|
*/
|
||||||
export const PROVIDER_PRESETS: ProviderPreset[] = [
|
export const PROVIDER_PRESETS: ProviderPreset[] = [
|
||||||
|
// Recommended
|
||||||
{
|
{
|
||||||
id: 'openrouter',
|
id: 'openrouter',
|
||||||
name: 'OpenRouter',
|
name: 'OpenRouter',
|
||||||
@@ -37,7 +41,9 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [
|
|||||||
defaultModel: 'anthropic/claude-sonnet-4',
|
defaultModel: 'anthropic/claude-sonnet-4',
|
||||||
apiKeyPlaceholder: 'sk-or-...',
|
apiKeyPlaceholder: 'sk-or-...',
|
||||||
apiKeyHint: 'Get your API key at openrouter.ai/keys',
|
apiKeyHint: 'Get your API key at openrouter.ai/keys',
|
||||||
|
category: 'recommended',
|
||||||
},
|
},
|
||||||
|
// Alternative providers
|
||||||
{
|
{
|
||||||
id: 'glm',
|
id: 'glm',
|
||||||
name: 'GLM',
|
name: 'GLM',
|
||||||
@@ -47,6 +53,7 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [
|
|||||||
defaultModel: 'glm-4.6',
|
defaultModel: 'glm-4.6',
|
||||||
apiKeyPlaceholder: 'ghp_...',
|
apiKeyPlaceholder: 'ghp_...',
|
||||||
apiKeyHint: 'Get your API key from Z.AI',
|
apiKeyHint: 'Get your API key from Z.AI',
|
||||||
|
category: 'alternative',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'glmt',
|
id: 'glmt',
|
||||||
@@ -57,6 +64,7 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [
|
|||||||
defaultModel: 'glm-4.6',
|
defaultModel: 'glm-4.6',
|
||||||
apiKeyPlaceholder: 'ghp_...',
|
apiKeyPlaceholder: 'ghp_...',
|
||||||
apiKeyHint: 'Same API key as GLM',
|
apiKeyHint: 'Same API key as GLM',
|
||||||
|
category: 'alternative',
|
||||||
extraEnv: {
|
extraEnv: {
|
||||||
ANTHROPIC_TEMPERATURE: '0.2',
|
ANTHROPIC_TEMPERATURE: '0.2',
|
||||||
ANTHROPIC_MAX_TOKENS: '65536',
|
ANTHROPIC_MAX_TOKENS: '65536',
|
||||||
@@ -76,6 +84,7 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [
|
|||||||
defaultModel: 'kimi-k2-thinking-turbo',
|
defaultModel: 'kimi-k2-thinking-turbo',
|
||||||
apiKeyPlaceholder: 'sk-...',
|
apiKeyPlaceholder: 'sk-...',
|
||||||
apiKeyHint: 'Get your API key from Moonshot AI',
|
apiKeyHint: 'Get your API key from Moonshot AI',
|
||||||
|
category: 'alternative',
|
||||||
alwaysThinkingEnabled: true,
|
alwaysThinkingEnabled: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -69,14 +69,9 @@ class RecoveryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create default config (matches postinstall.js)
|
// Create default config (matches postinstall.js)
|
||||||
// NOTE: No 'default' entry - when no profile specified, CCS passes through
|
// NOTE: Empty profiles - users create profiles via `ccs api create` or UI
|
||||||
// to Claude's native auth without --settings flag
|
|
||||||
const defaultConfig = {
|
const defaultConfig = {
|
||||||
profiles: {
|
profiles: {},
|
||||||
glm: '~/.ccs/glm.settings.json',
|
|
||||||
glmt: '~/.ccs/glmt.settings.json',
|
|
||||||
kimi: '~/.ccs/kimi.settings.json',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const tmpPath = `${configPath}.tmp`;
|
const tmpPath = `${configPath}.tmp`;
|
||||||
@@ -274,6 +269,9 @@ class RecoveryManager {
|
|||||||
/**
|
/**
|
||||||
* Run all recovery operations (lazy initialization)
|
* Run all recovery operations (lazy initialization)
|
||||||
* Mirrors postinstall.js behavior
|
* 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 {
|
recoverAll(): boolean {
|
||||||
this.recovered = [];
|
this.recovered = [];
|
||||||
@@ -283,11 +281,8 @@ class RecoveryManager {
|
|||||||
this.ensureSharedDirectories();
|
this.ensureSharedDirectories();
|
||||||
this.ensureClaudeSettings();
|
this.ensureClaudeSettings();
|
||||||
|
|
||||||
// Config files
|
// Config files (core only - no GLM/GLMT/Kimi auto-creation)
|
||||||
this.ensureConfigJson();
|
this.ensureConfigJson();
|
||||||
this.ensureGlmSettings();
|
|
||||||
this.ensureGlmtSettings();
|
|
||||||
this.ensureKimiSettings();
|
|
||||||
|
|
||||||
// Shell completions
|
// Shell completions
|
||||||
this.ensureShellCompletions();
|
this.ensureShellCompletions();
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Profile Routes - CRUD operations for user profiles and accounts
|
* 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';
|
import { Router, Request, Response } from 'express';
|
||||||
@@ -7,13 +9,9 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { getCcsDir } from '../../utils/config-manager';
|
import { getCcsDir } from '../../utils/config-manager';
|
||||||
import { isReservedName, RESERVED_PROFILE_NAMES } from '../../config/reserved-names';
|
import { isReservedName, RESERVED_PROFILE_NAMES } from '../../config/reserved-names';
|
||||||
import {
|
import { createApiProfile, removeApiProfile } from '../../api/services/profile-writer';
|
||||||
readConfigSafe,
|
import { apiProfileExists, listApiProfiles } from '../../api/services/profile-reader';
|
||||||
writeConfig,
|
import { updateSettingsFile } from './route-helpers';
|
||||||
isConfigured,
|
|
||||||
createSettingsFile,
|
|
||||||
updateSettingsFile,
|
|
||||||
} from './route-helpers';
|
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -23,13 +21,13 @@ const router = Router();
|
|||||||
* GET /api/profiles - List all profiles
|
* GET /api/profiles - List all profiles
|
||||||
*/
|
*/
|
||||||
router.get('/', (_req: Request, res: Response) => {
|
router.get('/', (_req: Request, res: Response) => {
|
||||||
const config = readConfigSafe();
|
const result = listApiProfiles();
|
||||||
const profiles = Object.entries(config.profiles).map(([name, settingsPath]) => ({
|
// Map isConfigured -> configured for UI compatibility
|
||||||
name,
|
const profiles = result.profiles.map((p) => ({
|
||||||
settingsPath,
|
name: p.name,
|
||||||
configured: isConfigured(name, config),
|
settingsPath: p.settingsPath,
|
||||||
|
configured: p.isConfigured,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.json({ profiles });
|
res.json({ profiles });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -53,31 +51,26 @@ router.post('/', (req: Request, res: Response): void => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = readConfigSafe();
|
// Check if profile already exists (uses unified config when available)
|
||||||
|
if (apiProfileExists(name)) {
|
||||||
if (config.profiles[name]) {
|
|
||||||
res.status(409).json({ error: 'Profile already exists' });
|
res.status(409).json({ error: 'Profile already exists' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure .ccs directory exists
|
// Create profile using unified-config-aware service
|
||||||
if (!fs.existsSync(getCcsDir())) {
|
const result = createApiProfile(name, baseUrl, apiKey, {
|
||||||
fs.mkdirSync(getCcsDir(), { recursive: true });
|
default: model || '',
|
||||||
}
|
opus: opusModel || model || '',
|
||||||
|
sonnet: sonnetModel || model || '',
|
||||||
// Create settings file with model mapping
|
haiku: haikuModel || model || '',
|
||||||
const settingsPath = createSettingsFile(name, baseUrl, apiKey, {
|
|
||||||
model,
|
|
||||||
opusModel,
|
|
||||||
sonnetModel,
|
|
||||||
haikuModel,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update config
|
if (!result.success) {
|
||||||
config.profiles[name] = settingsPath;
|
res.status(500).json({ error: result.error || 'Failed to create profile' });
|
||||||
writeConfig(config);
|
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 { name } = req.params;
|
||||||
const { baseUrl, apiKey, model, opusModel, sonnetModel, haikuModel } = req.body;
|
const { baseUrl, apiKey, model, opusModel, sonnetModel, haikuModel } = req.body;
|
||||||
|
|
||||||
const config = readConfigSafe();
|
// Check if profile exists (uses unified config when available)
|
||||||
|
if (!apiProfileExists(name)) {
|
||||||
if (!config.profiles[name]) {
|
|
||||||
res.status(404).json({ error: 'Profile not found' });
|
res.status(404).json({ error: 'Profile not found' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -108,22 +100,19 @@ router.put('/:name', (req: Request, res: Response): void => {
|
|||||||
router.delete('/:name', (req: Request, res: Response): void => {
|
router.delete('/:name', (req: Request, res: Response): void => {
|
||||||
const { name } = req.params;
|
const { name } = req.params;
|
||||||
|
|
||||||
const config = readConfigSafe();
|
// Check if profile exists (uses unified config when available)
|
||||||
|
if (!apiProfileExists(name)) {
|
||||||
if (!config.profiles[name]) {
|
|
||||||
res.status(404).json({ error: 'Profile not found' });
|
res.status(404).json({ error: 'Profile not found' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete settings file
|
// Remove profile using unified-config-aware service
|
||||||
const settingsPath = path.join(getCcsDir(), `${name}.settings.json`);
|
const result = removeApiProfile(name);
|
||||||
if (fs.existsSync(settingsPath)) {
|
|
||||||
fs.unlinkSync(settingsPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove from config
|
if (!result.success) {
|
||||||
delete config.profiles[name];
|
res.status(500).json({ error: result.error || 'Failed to delete profile' });
|
||||||
writeConfig(config);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
res.json({ name, deleted: true });
|
res.json({ name, deleted: true });
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user