mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 12:09:03 +00:00
Epic P4. Migrates plain throw new Error to the typed-error classes in the four locked subdomains, and makes the taxonomy erasable-syntax-compatible so it can be adopted across UI-reachable code. Migration (cliproxy/auth, web-server/routes, auth): - 21 of 23 throws in the locked subdomains now use typed subclasses (ProfileError, AuthError, ConfigError, ValidationError, ProviderError). - Typed adoption in locked subdomains: 0/23 -> 21/23 (91.3%), > 40% target. - Overall typed adoption: 0.9% -> 8.6%. - Messages preserved exactly (message-based tests stable). Exit codes now differentiate via handleError (ProfileError=7, AuthError=4, ConfigError=2, ProviderError=6). ccs doctor 0/1 contract untouched (outside scope). Erasable-syntax fix (unblocks the migration in the UI build graph): - exit-codes.ts: enum ExitCode -> const object + union type (value and type usage both preserved; no Object.values(ExitCode) consumers). - error-types.ts: constructor parameter properties -> explicit readonly field declarations + body assignment. - The web UI build enforces erasableSyntaxOnly (ui/tsconfig.app.json) and reaches src/errors via the @shared -> src/auth graph; pre-erasable error-types blocked the build once profile-registry adopted typed errors. Compat audit: docs/reports/typed-error-exit-code-compat-audit.md (Q1 resolved: migrate freely; only documented contract is ccs doctor, which is untouched). Behavior-lock: src/errors/__tests__/typed-error-migration-exit-codes.test.ts (taxonomy -> exit-code mapping, instanceof chains, context fields). validate + validate:ci-parity green (incl. UI build).
177 lines
4.6 KiB
TypeScript
177 lines
4.6 KiB
TypeScript
/**
|
|
* CLIProxy Sync Routes - Local sync management for CLIProxy profiles
|
|
*/
|
|
|
|
import { Router, Request, Response } from 'express';
|
|
|
|
import {
|
|
generateSyncPayload,
|
|
generateSyncPreview,
|
|
getAutoSyncStatus,
|
|
restartAutoSyncWatcher,
|
|
syncToLocalConfig,
|
|
getLocalSyncStatus,
|
|
} from '../../cliproxy/sync';
|
|
import { mutateConfig } from '../../config/config-loader-facade';
|
|
import { ConfigError } from '../../errors/error-types';
|
|
import { createLogger } from '../../services/logging';
|
|
|
|
const router = Router();
|
|
const logger = createLogger('web-server:routes:cliproxy-sync');
|
|
|
|
/**
|
|
* GET /api/cliproxy/sync/status - Get local sync status
|
|
* Returns: { configExists, configPath, currentKeyCount, syncableProfileCount }
|
|
*/
|
|
router.get('/status', async (_req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const status = getLocalSyncStatus();
|
|
res.json({
|
|
configured: status.configExists,
|
|
configPath: status.configPath,
|
|
currentKeyCount: status.currentKeyCount,
|
|
syncableProfileCount: status.syncableProfileCount,
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ error: (error as Error).message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/cliproxy/sync/preview - Get sync preview (dry run)
|
|
* Returns: { profiles: SyncPreviewItem[], payload: ClaudeKey[] }
|
|
*/
|
|
router.get('/preview', async (_req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const preview = generateSyncPreview();
|
|
const payload = generateSyncPayload();
|
|
|
|
// Mask API keys in payload for preview
|
|
const maskedPayload = payload.map((key) => ({
|
|
...key,
|
|
'api-key': maskApiKey(key['api-key']),
|
|
}));
|
|
|
|
res.json({
|
|
profiles: preview,
|
|
payload: maskedPayload,
|
|
count: payload.length,
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ error: (error as Error).message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/cliproxy/sync - Execute sync to local CLIProxy config
|
|
* Returns: { success, syncedCount, configPath, error? }
|
|
*/
|
|
router.post('/', async (_req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const result = syncToLocalConfig();
|
|
|
|
if (!result.success) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: result.error,
|
|
configPath: result.configPath,
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (result.syncedCount === 0) {
|
|
res.json({
|
|
success: true,
|
|
syncedCount: 0,
|
|
message: 'No profiles to sync',
|
|
configPath: result.configPath,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const preview = generateSyncPreview();
|
|
res.json({
|
|
success: true,
|
|
syncedCount: result.syncedCount,
|
|
configPath: result.configPath,
|
|
profiles: preview.map((p) => p.name),
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: (error as Error).message,
|
|
});
|
|
}
|
|
});
|
|
|
|
// ==================== Auto-Sync ====================
|
|
|
|
/**
|
|
* GET /api/cliproxy/sync/auto-sync - Get auto-sync status
|
|
* Returns: { enabled, watching, syncing }
|
|
*/
|
|
router.get('/auto-sync', async (_req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const status = getAutoSyncStatus();
|
|
res.json(status);
|
|
} catch (error) {
|
|
res.status(500).json({ error: (error as Error).message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PUT /api/cliproxy/sync/auto-sync - Toggle auto-sync setting
|
|
* Body: { enabled: boolean }
|
|
* Returns: { success: true, enabled }
|
|
*/
|
|
router.put('/auto-sync', async (req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const { enabled } = req.body;
|
|
|
|
if (typeof enabled !== 'boolean') {
|
|
res.status(400).json({ error: 'Invalid field: enabled must be a boolean' });
|
|
return;
|
|
}
|
|
|
|
try {
|
|
mutateConfig((config) => {
|
|
if (!config.cliproxy) {
|
|
throw new ConfigError('CLIProxy config not initialized');
|
|
}
|
|
config.cliproxy.auto_sync = enabled;
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ error: `Failed to save config: ${(error as Error).message}` });
|
|
return;
|
|
}
|
|
|
|
// Restart watcher (separate operation)
|
|
try {
|
|
await restartAutoSyncWatcher();
|
|
} catch (watcherError) {
|
|
// Log but don't fail - config was saved successfully
|
|
const e = watcherError as Error;
|
|
logger.warn('auto_sync.watcher_restart_failed', 'Watcher restart failed after config save', {
|
|
enabled,
|
|
err: { name: e.name, message: e.message },
|
|
});
|
|
}
|
|
|
|
res.json({ success: true, enabled });
|
|
} catch (error) {
|
|
res.status(500).json({ error: (error as Error).message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Mask API key for display (show first 4 and last 4 chars).
|
|
*/
|
|
function maskApiKey(key: string): string {
|
|
if (key.length <= 12) {
|
|
return '***';
|
|
}
|
|
return `${key.slice(0, 4)}...${key.slice(-4)}`;
|
|
}
|
|
|
|
export default router;
|