feat(cliproxy): disable logging by default and add cleanup command

- Disable CLIProxy logging by default to prevent 5-12GB disk bloat
- Add cliproxy.logging config option for user opt-in via ~/.ccs/config.yaml
- Add `ccs cleanup` command to remove old CLIProxy logs
- Bump CLIProxy config version to v3 to trigger config regeneration

Closes #96
This commit is contained in:
kaitranntt
2025-12-13 23:27:23 -05:00
parent 03dca41b2b
commit e5cdf7c083
6 changed files with 252 additions and 3 deletions
+7
View File
@@ -252,6 +252,13 @@ async function main(): Promise<void> {
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');
+28 -3
View File
@@ -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<CLIProxyProvider, string> = {
@@ -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
+193
View File
@@ -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<void> {
// 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<string>((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.');
}
}
+1
View File
@@ -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'],
+5
View File
@@ -105,6 +105,11 @@ function mergeWithDefaults(partial: Partial<UnifiedConfig>): 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,
+18
View File
@@ -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<string, CLIProxyVariantConfig>;
/** 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',