mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 12:15:57 +00:00
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:
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user