diff --git a/src/ccs.ts b/src/ccs.ts index 3d8ae6b2..930bc247 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -252,6 +252,13 @@ async function main(): Promise { return; } + // Special case: cleanup command + if (firstArg === 'cleanup' || firstArg === '--cleanup') { + const { handleCleanupCommand } = await import('./commands/cleanup-command'); + await handleCleanupCommand(args.slice(1)); + return; + } + // Special case: migrate command if (firstArg === 'migrate' || firstArg === '--migrate') { const { handleMigrateCommand, printMigrateHelp } = await import('./commands/migrate-command'); diff --git a/src/cliproxy/config-generator.ts b/src/cliproxy/config-generator.ts index 6c0e0a51..3383d313 100644 --- a/src/cliproxy/config-generator.ts +++ b/src/cliproxy/config-generator.ts @@ -14,6 +14,7 @@ import { getCcsDir } from '../utils/config-manager'; import { warn } from '../utils/ui'; import { CLIProxyProvider, ProviderConfig, ProviderModelMapping } from './types'; import { getModelMappingFromConfig, getEnvVarsFromConfig } from './base-config-loader'; +import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader'; /** Settings file structure for user overrides */ interface ProviderSettings { @@ -33,8 +34,9 @@ export const CCS_CONTROL_PANEL_SECRET = 'ccs'; * Config version - bump when config format changes to trigger regeneration * v1: Initial config (port, auth-dir, api-keys only) * v2: Full-featured config with dashboard, quota mgmt, simplified key + * v3: Logging disabled by default (user opt-in via ~/.ccs/config.yaml) */ -export const CLIPROXY_CONFIG_VERSION = 2; +export const CLIPROXY_CONFIG_VERSION = 3; /** Provider display names (static metadata) */ const PROVIDER_DISPLAY_NAMES: Record = { @@ -115,6 +117,18 @@ export function getBinDir(): string { return path.join(getCliproxyDir(), 'bin'); } +/** + * Get CLIProxy logging settings from user config. + * Defaults to disabled to prevent disk bloat. + */ +function getLoggingSettings(): { loggingToFile: boolean; requestLog: boolean } { + const config = loadOrCreateUnifiedConfig(); + return { + loggingToFile: config.cliproxy.logging?.enabled ?? false, + requestLog: config.cliproxy.logging?.request_log ?? false, + }; +} + /** * Generate UNIFIED config.yaml content for ALL providers * This enables concurrent usage of gemini/codex/agy without config conflicts. @@ -125,6 +139,9 @@ function generateUnifiedConfigContent(port: number = CLIPROXY_DEFAULT_PORT): str // Convert Windows backslashes to forward slashes for YAML compatibility const authDirNormalized = authDir.split(path.sep).join('/'); + // Get logging settings from user config (disabled by default) + const { loggingToFile, requestLog } = getLoggingSettings(); + // Unified config with enhanced CLIProxyAPI features const config = `# CLIProxyAPI config generated by CCS v${CLIPROXY_CONFIG_VERSION} # Supports: gemini, codex, agy, qwen, iflow (concurrent usage) @@ -143,12 +160,20 @@ debug: false # ============================================================================= # Logging # ============================================================================= +# WARNING: Logs can grow to several GB if enabled! +# To enable logging, edit ~/.ccs/config.yaml: +# cliproxy: +# logging: +# enabled: true +# request_log: true +# Then run 'ccs doctor --fix' to regenerate this config. +# Use 'ccs cleanup' to remove old logs. # Write logs to file (stored in ~/.ccs/cliproxy/logs/) -logging-to-file: true +logging-to-file: ${loggingToFile} # Log individual API requests for debugging/analytics -request-log: true +request-log: ${requestLog} # ============================================================================= # Dashboard & Management diff --git a/src/commands/cleanup-command.ts b/src/commands/cleanup-command.ts new file mode 100644 index 00000000..bae4e192 --- /dev/null +++ b/src/commands/cleanup-command.ts @@ -0,0 +1,193 @@ +/** + * Cleanup Command Handler + * + * Removes old CLIProxy logs to free up disk space. + * Logs can accumulate to several GB without user awareness. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { getCliproxyDir } from '../cliproxy/config-generator'; +import { info, ok, warn } from '../utils/ui'; + +/** Get the CLIProxy logs directory */ +function getLogsDir(): string { + return path.join(getCliproxyDir(), 'logs'); +} + +/** Format bytes to human-readable size */ +function formatBytes(bytes: number): string { + if (bytes === 0) return '0 B'; + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`; +} + +/** Calculate total size of a directory */ +function getDirSize(dirPath: string): number { + if (!fs.existsSync(dirPath)) return 0; + + let totalSize = 0; + const files = fs.readdirSync(dirPath); + + for (const file of files) { + const filePath = path.join(dirPath, file); + try { + const stats = fs.lstatSync(filePath); // Use lstat to detect symlinks + if (stats.isFile() && !stats.isSymbolicLink()) { + totalSize += stats.size; + } else if (stats.isDirectory() && !stats.isSymbolicLink()) { + totalSize += getDirSize(filePath); + } + // Skip symlinks for safety + } catch { + // File may have been deleted between readdir and stat - skip + } + } + + return totalSize; +} + +/** Count files in a directory */ +function countFiles(dirPath: string): number { + if (!fs.existsSync(dirPath)) return 0; + let count = 0; + const entries = fs.readdirSync(dirPath); + + for (const entry of entries) { + const filePath = path.join(dirPath, entry); + try { + const stats = fs.lstatSync(filePath); + if (stats.isFile() && !stats.isSymbolicLink()) { + count++; + } + } catch { + // File may have been deleted - skip + } + } + return count; +} + +/** Delete all regular files in a directory (skips symlinks for safety) */ +function cleanDirectory(dirPath: string): { deleted: number; freedBytes: number } { + if (!fs.existsSync(dirPath)) return { deleted: 0, freedBytes: 0 }; + + let deleted = 0; + let freedBytes = 0; + const files = fs.readdirSync(dirPath); + + for (const file of files) { + const filePath = path.join(dirPath, file); + try { + const stats = fs.lstatSync(filePath); + + // Only delete regular files, skip symlinks for security + if (stats.isFile() && !stats.isSymbolicLink()) { + freedBytes += stats.size; + fs.unlinkSync(filePath); + deleted++; + } + } catch { + // File may have been deleted or inaccessible - skip + } + } + + return { deleted, freedBytes }; +} + +/** Print help for cleanup command */ +function printHelp(): void { + console.log(''); + console.log('Usage: ccs cleanup [options]'); + console.log(''); + console.log('Remove old CLIProxy logs to free up disk space.'); + console.log(''); + console.log('Options:'); + console.log(' --dry-run Show what would be deleted without deleting'); + console.log(' --force Skip confirmation prompt'); + console.log(' --help, -h Show this help message'); + console.log(''); + console.log('Examples:'); + console.log(' ccs cleanup Interactive cleanup with confirmation'); + console.log(' ccs cleanup --dry-run Preview cleanup without deleting'); + console.log(' ccs cleanup --force Clean without confirmation'); + console.log(''); + console.log('Note: CLIProxy logging is disabled by default.'); + console.log('To enable logging, edit ~/.ccs/config.yaml:'); + console.log(' cliproxy:'); + console.log(' logging:'); + console.log(' enabled: true'); + console.log(''); +} + +/** + * Handle cleanup command + */ +export async function handleCleanupCommand(args: string[]): Promise { + // Handle help + if (args.includes('--help') || args.includes('-h')) { + printHelp(); + return; + } + + const dryRun = args.includes('--dry-run'); + const force = args.includes('--force'); + const logsDir = getLogsDir(); + + // Check if logs directory exists + if (!fs.existsSync(logsDir)) { + console.log(info('No CLIProxy logs found.')); + return; + } + + // Calculate current size + const currentSize = getDirSize(logsDir); + const fileCount = countFiles(logsDir); + + if (fileCount === 0) { + console.log(info('No log files to clean.')); + return; + } + + console.log(''); + console.log(`CLIProxy Logs: ${logsDir}`); + console.log(` Files: ${fileCount}`); + console.log(` Size: ${formatBytes(currentSize)}`); + console.log(''); + + if (dryRun) { + console.log(info('Dry run - no files deleted.')); + console.log(`Would delete ${fileCount} files (${formatBytes(currentSize)})`); + return; + } + + // Confirm unless --force + if (!force) { + const readline = await import('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const answer = await new Promise((resolve) => { + rl.question(`Delete ${fileCount} log files (${formatBytes(currentSize)})? [y/N] `, resolve); + }); + rl.close(); + + if (answer.toLowerCase() !== 'y') { + console.log('Cancelled.'); + return; + } + } + + // Perform cleanup + const { deleted, freedBytes } = cleanDirectory(logsDir); + console.log(ok(`Deleted ${deleted} files, freed ${formatBytes(freedBytes)}`)); + + // Suggest disabling logging if it was enabled + if (deleted > 0) { + console.log(''); + console.log(warn('Tip: CLIProxy logging is now disabled by default.')); + console.log(' Run `ccs doctor --fix` to update your config.'); + } +} diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index 23c2af18..ff724df0 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -186,6 +186,7 @@ Claude Code Profile & Model Switcher`.trim(); // Diagnostics printSubSection('Diagnostics', [ ['ccs doctor', 'Run health check and diagnostics'], + ['ccs cleanup', 'Remove old CLIProxy logs'], ['ccs config', 'Open web configuration dashboard'], ['ccs config --port 3000', 'Use specific port'], ['ccs sync', 'Sync delegation commands and skills'], diff --git a/src/config/unified-config-loader.ts b/src/config/unified-config-loader.ts index 292ec9e5..0b4152bb 100644 --- a/src/config/unified-config-loader.ts +++ b/src/config/unified-config-loader.ts @@ -105,6 +105,11 @@ function mergeWithDefaults(partial: Partial): UnifiedConfig { oauth_accounts: partial.cliproxy?.oauth_accounts ?? defaults.cliproxy.oauth_accounts, providers: defaults.cliproxy.providers, // Always use defaults for providers variants: partial.cliproxy?.variants ?? defaults.cliproxy.variants, + logging: { + enabled: partial.cliproxy?.logging?.enabled ?? defaults.cliproxy.logging?.enabled ?? false, + request_log: + partial.cliproxy?.logging?.request_log ?? defaults.cliproxy.logging?.request_log ?? false, + }, }, preferences: { ...defaults.preferences, diff --git a/src/config/unified-config-types.ts b/src/config/unified-config-types.ts index ed313298..12e62eea 100644 --- a/src/config/unified-config-types.ts +++ b/src/config/unified-config-types.ts @@ -62,6 +62,18 @@ export interface CLIProxyVariantConfig { settings?: string; } +/** + * CLIProxy logging configuration. + * Controls whether CLIProxyAPI writes logs to disk. + * Logs can grow to several GB if left enabled. + */ +export interface CLIProxyLoggingConfig { + /** Enable logging to file (default: false to prevent disk bloat) */ + enabled?: boolean; + /** Enable request logging for debugging (default: false) */ + request_log?: boolean; +} + /** * CLIProxy configuration section. */ @@ -72,6 +84,8 @@ export interface CLIProxyConfig { providers: readonly string[]; /** User-defined provider variants */ variants: Record; + /** Logging configuration (disabled by default) */ + logging?: CLIProxyLoggingConfig; } /** @@ -130,6 +144,10 @@ export function createEmptyUnifiedConfig(): UnifiedConfig { oauth_accounts: {}, providers: ['gemini', 'codex', 'agy', 'qwen', 'iflow'], variants: {}, + logging: { + enabled: false, + request_log: false, + }, }, preferences: { theme: 'system',