mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 12:14:19 +00:00
Merge pull request #204 from kaitranntt/kai/fix/203-dashboard-accounts-unified-config
fix(dashboard): read accounts from unified config
This commit is contained in:
8 files changed
+197
-40
No files matched your search
@@ -332,6 +332,42 @@ export class ProfileRegistry {
|
||||
config.accounts[name].last_used = new Date().toISOString();
|
||||
saveUnifiedConfig(config);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// DRY Helper Methods (consolidated logic)
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Get all profiles merged from both legacy and unified config.
|
||||
* Unified config takes precedence for duplicate names.
|
||||
* DRY helper to consolidate merge logic used in multiple places.
|
||||
*/
|
||||
getAllProfilesMerged(): Record<string, ProfileMetadata> {
|
||||
const legacyProfiles = this.getAllProfiles();
|
||||
const unifiedAccounts = this.getAllAccountsUnified();
|
||||
|
||||
// Start with legacy profiles
|
||||
const merged: Record<string, ProfileMetadata> = { ...legacyProfiles };
|
||||
|
||||
// Override with unified config accounts (takes precedence)
|
||||
for (const [name, account] of Object.entries(unifiedAccounts)) {
|
||||
merged[name] = {
|
||||
type: 'account',
|
||||
created: account.created,
|
||||
last_used: account.last_used,
|
||||
};
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resolved default profile from unified config first, fallback to legacy.
|
||||
* DRY helper to consolidate default resolution logic.
|
||||
*/
|
||||
getDefaultResolved(): string | null {
|
||||
return this.getDefaultUnified() ?? this.getDefaultProfile();
|
||||
}
|
||||
}
|
||||
|
||||
export default ProfileRegistry;
|
||||
@@ -22,7 +22,8 @@ export function createFileWatcher(onChange: FileChangeCallback): FSWatcher {
|
||||
|
||||
const watcher = chokidar.watch(
|
||||
[
|
||||
path.join(ccsDir, 'config.json'),
|
||||
path.join(ccsDir, 'config.json'), // Legacy config
|
||||
path.join(ccsDir, 'config.yaml'), // Unified config
|
||||
path.join(ccsDir, '*.settings.json'),
|
||||
path.join(ccsDir, 'profiles.json'),
|
||||
path.join(ccsDir, 'cliproxy', 'sessions.json'), // Proxy session tracking
|
||||
@@ -41,7 +42,7 @@ export function createFileWatcher(onChange: FileChangeCallback): FSWatcher {
|
||||
const basename = path.basename(filePath);
|
||||
let type: FileChangeEvent['type'];
|
||||
|
||||
if (basename === 'config.json') {
|
||||
if (basename === 'config.json' || basename === 'config.yaml') {
|
||||
type = 'config-changed';
|
||||
} else if (basename === 'profiles.json') {
|
||||
type = 'profiles-changed';
|
||||
|
||||
@@ -142,6 +142,17 @@ export function fixHealthIssue(checkId: string): { success: boolean; message: st
|
||||
return { success: true, message: 'Created ~/.ccs directory' };
|
||||
|
||||
case 'config-file': {
|
||||
// Use appropriate config based on unified mode
|
||||
const { isUnifiedMode } = require('../config/unified-config-loader');
|
||||
if (isUnifiedMode()) {
|
||||
const {
|
||||
loadOrCreateUnifiedConfig,
|
||||
saveUnifiedConfig,
|
||||
} = require('../config/unified-config-loader');
|
||||
const config = loadOrCreateUnifiedConfig();
|
||||
saveUnifiedConfig(config);
|
||||
return { success: true, message: 'Created/updated config.yaml' };
|
||||
}
|
||||
const configPath = getConfigPath();
|
||||
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
||||
fs.writeFileSync(configPath, JSON.stringify({ profiles: {} }, null, 2) + '\n');
|
||||
@@ -149,6 +160,12 @@ export function fixHealthIssue(checkId: string): { success: boolean; message: st
|
||||
}
|
||||
|
||||
case 'profiles-file': {
|
||||
// Use appropriate storage based on unified mode
|
||||
const { isUnifiedMode: isUnified } = require('../config/unified-config-loader');
|
||||
if (isUnified()) {
|
||||
// In unified mode, accounts are stored in config.yaml
|
||||
return { success: true, message: 'Accounts stored in config.yaml (unified mode)' };
|
||||
}
|
||||
const profilesPath = path.join(ccsDir, 'profiles.json');
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
fs.writeFileSync(profilesPath, JSON.stringify({ profiles: {} }, null, 2) + '\n');
|
||||
|
||||
@@ -1,18 +1,57 @@
|
||||
/**
|
||||
* Configuration Health Checks
|
||||
*
|
||||
* Check config.json, settings files, and Claude settings.
|
||||
* Check config.json, config.yaml, settings files, and Claude settings.
|
||||
* Supports both legacy (config.json) and unified (config.yaml) modes.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { getConfigPath } from '../../utils/config-manager';
|
||||
import { isUnifiedMode, hasUnifiedConfig } from '../../config/unified-config-loader';
|
||||
import type { HealthCheck } from './types';
|
||||
|
||||
/**
|
||||
* Check config.json file
|
||||
* Check config file (config.json or config.yaml based on mode)
|
||||
*/
|
||||
export function checkConfigFile(): HealthCheck {
|
||||
// In unified mode, check config.yaml
|
||||
if (isUnifiedMode() || hasUnifiedConfig()) {
|
||||
const ccsDir = path.join(process.env.HOME || '', '.ccs');
|
||||
const yamlPath = path.join(ccsDir, 'config.yaml');
|
||||
|
||||
if (!fs.existsSync(yamlPath)) {
|
||||
return {
|
||||
id: 'config-file',
|
||||
name: 'config.yaml',
|
||||
status: 'warning',
|
||||
message: 'Not found (unified mode)',
|
||||
details: yamlPath,
|
||||
fixable: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
fs.readFileSync(yamlPath, 'utf8');
|
||||
return {
|
||||
id: 'config-file',
|
||||
name: 'config.yaml',
|
||||
status: 'ok',
|
||||
message: 'Valid (unified mode)',
|
||||
details: yamlPath,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
id: 'config-file',
|
||||
name: 'config.yaml',
|
||||
status: 'error',
|
||||
message: 'Cannot read file',
|
||||
details: yamlPath,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy mode: check config.json
|
||||
const configPath = getConfigPath();
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
|
||||
@@ -2,16 +2,52 @@
|
||||
* Profile Health Checks
|
||||
*
|
||||
* Check profiles, instances, and delegation.
|
||||
* Supports both legacy (config.json) and unified (config.yaml) modes.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { isUnifiedMode, loadUnifiedConfig } from '../../config/unified-config-loader';
|
||||
import type { HealthCheck } from './types';
|
||||
|
||||
/**
|
||||
* Check profiles configuration
|
||||
* Check profiles configuration (API profiles from config.json or config.yaml)
|
||||
*/
|
||||
export function checkProfiles(ccsDir: string): HealthCheck {
|
||||
// Check unified config first
|
||||
if (isUnifiedMode()) {
|
||||
const config = loadUnifiedConfig();
|
||||
if (!config) {
|
||||
return {
|
||||
id: 'profiles',
|
||||
name: 'Profiles',
|
||||
status: 'info',
|
||||
message: 'config.yaml not loaded',
|
||||
};
|
||||
}
|
||||
|
||||
const profileCount = Object.keys(config.profiles || {}).length;
|
||||
const profileNames = Object.keys(config.profiles || {}).join(', ');
|
||||
|
||||
if (profileCount === 0) {
|
||||
return {
|
||||
id: 'profiles',
|
||||
name: 'Profiles',
|
||||
status: 'ok',
|
||||
message: 'No API profiles (unified mode)',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'profiles',
|
||||
name: 'Profiles',
|
||||
status: 'ok',
|
||||
message: `${profileCount} configured (unified)`,
|
||||
details: profileNames.length > 40 ? profileNames.substring(0, 37) + '...' : profileNames,
|
||||
};
|
||||
}
|
||||
|
||||
// Legacy mode: check config.json
|
||||
const configPath = path.join(ccsDir, 'config.json');
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
|
||||
@@ -5,12 +5,11 @@
|
||||
*/
|
||||
|
||||
import { Router, Request, Response } from 'express';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { getCcsDir, loadConfig } from '../utils/config-manager';
|
||||
import { loadConfig } from '../utils/config-manager';
|
||||
import { runHealthChecks } from './health-service';
|
||||
import { getAllAuthStatus, initializeAccounts } from '../cliproxy/auth-handler';
|
||||
import { getVersion } from '../utils/version';
|
||||
import ProfileRegistry from '../auth/profile-registry';
|
||||
|
||||
export const overviewRoutes = Router();
|
||||
|
||||
@@ -64,12 +63,16 @@ overviewRoutes.get('/', async (_req: Request, res: Response) => {
|
||||
|
||||
function getAccountCount(): number {
|
||||
try {
|
||||
const profilesPath = path.join(getCcsDir(), 'profiles.json');
|
||||
const registry = new ProfileRegistry();
|
||||
|
||||
if (!fs.existsSync(profilesPath)) return 0;
|
||||
// Merge legacy (profiles.json) and unified (config.yaml) accounts
|
||||
const legacyProfiles = registry.getAllProfiles();
|
||||
const unifiedAccounts = registry.getAllAccountsUnified();
|
||||
|
||||
const data = JSON.parse(fs.readFileSync(profilesPath, 'utf8'));
|
||||
return Object.keys(data.profiles || {}).length;
|
||||
// Count unique accounts (unified takes precedence for duplicates)
|
||||
const allNames = new Set([...Object.keys(legacyProfiles), ...Object.keys(unifiedAccounts)]);
|
||||
|
||||
return allNames.size;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,38 +1,57 @@
|
||||
/**
|
||||
* Account Routes - CRUD operations for Claude accounts (profiles.json)
|
||||
* Account Routes - CRUD operations for Claude accounts
|
||||
*
|
||||
* Separated from profile-routes.ts to avoid dual-mounting conflicts.
|
||||
* Uses ProfileRegistry to read from both legacy (profiles.json)
|
||||
* and unified config (config.yaml) for consistent data with CLI.
|
||||
*/
|
||||
|
||||
import { Router, Request, Response } from 'express';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { getCcsDir } from '../../utils/config-manager';
|
||||
import ProfileRegistry from '../../auth/profile-registry';
|
||||
import { isUnifiedMode } from '../../config/unified-config-loader';
|
||||
|
||||
const router = Router();
|
||||
const registry = new ProfileRegistry();
|
||||
|
||||
/**
|
||||
* GET /api/accounts - List accounts from profiles.json
|
||||
* GET /api/accounts - List accounts from both profiles.json and config.yaml
|
||||
*/
|
||||
router.get('/', (_req: Request, res: Response): void => {
|
||||
try {
|
||||
const profilesPath = path.join(getCcsDir(), 'profiles.json');
|
||||
// Get profiles from both legacy and unified config (same logic as CLI)
|
||||
const legacyProfiles = registry.getAllProfiles();
|
||||
const unifiedAccounts = registry.getAllAccountsUnified();
|
||||
|
||||
if (!fs.existsSync(profilesPath)) {
|
||||
res.json({ accounts: [], default: null });
|
||||
return;
|
||||
// Merge profiles: unified config takes precedence
|
||||
const merged: Record<string, { type: string; created: string; last_used: string | null }> = {};
|
||||
|
||||
// Add legacy profiles first
|
||||
for (const [name, meta] of Object.entries(legacyProfiles)) {
|
||||
merged[name] = {
|
||||
type: meta.type || 'account',
|
||||
created: meta.created,
|
||||
last_used: meta.last_used || null,
|
||||
};
|
||||
}
|
||||
|
||||
const data = JSON.parse(fs.readFileSync(profilesPath, 'utf8'));
|
||||
const accounts = Object.entries(data.profiles || {}).map(([name, meta]) => {
|
||||
const metadata = meta as Record<string, unknown>;
|
||||
return {
|
||||
name,
|
||||
...metadata,
|
||||
// Override with unified config accounts (takes precedence)
|
||||
for (const [name, account] of Object.entries(unifiedAccounts)) {
|
||||
merged[name] = {
|
||||
type: 'account',
|
||||
created: account.created,
|
||||
last_used: account.last_used,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ accounts, default: data.default || null });
|
||||
// Convert to array format
|
||||
const accounts = Object.entries(merged).map(([name, meta]) => ({
|
||||
name,
|
||||
...meta,
|
||||
}));
|
||||
|
||||
// Get default from unified config first, fallback to legacy
|
||||
const defaultProfile = registry.getDefaultUnified() ?? registry.getDefaultProfile() ?? null;
|
||||
|
||||
res.json({ accounts, default: defaultProfile });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
@@ -50,14 +69,12 @@ router.post('/default', (req: Request, res: Response): void => {
|
||||
return;
|
||||
}
|
||||
|
||||
const profilesPath = path.join(getCcsDir(), 'profiles.json');
|
||||
|
||||
const data = fs.existsSync(profilesPath)
|
||||
? JSON.parse(fs.readFileSync(profilesPath, 'utf8'))
|
||||
: { profiles: {} };
|
||||
|
||||
data.default = name;
|
||||
fs.writeFileSync(profilesPath, JSON.stringify(data, null, 2) + '\n');
|
||||
// Use unified config if in unified mode, otherwise use legacy
|
||||
if (isUnifiedMode()) {
|
||||
registry.setDefaultUnified(name);
|
||||
} else {
|
||||
registry.setDefaultProfile(name);
|
||||
}
|
||||
|
||||
res.json({ default: name });
|
||||
} catch (error) {
|
||||
|
||||
@@ -222,7 +222,11 @@ router.post('/:profile/presets', (req: Request, res: Response): void => {
|
||||
};
|
||||
|
||||
settings.presets.push(preset);
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
||||
|
||||
// Atomic write: temp file + rename
|
||||
const tempPath = settingsPath + '.tmp';
|
||||
fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2) + '\n');
|
||||
fs.renameSync(tempPath, settingsPath);
|
||||
|
||||
res.status(201).json({ preset });
|
||||
} catch (error) {
|
||||
@@ -250,7 +254,11 @@ router.delete('/:profile/presets/:name', (req: Request, res: Response): void =>
|
||||
}
|
||||
|
||||
settings.presets = settings.presets.filter((p) => p.name !== name);
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
||||
|
||||
// Atomic write: temp file + rename
|
||||
const tempPath = settingsPath + '.tmp';
|
||||
fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2) + '\n');
|
||||
fs.renameSync(tempPath, settingsPath);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
|
||||
Reference in new issue
Block a user