mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 20:17:45 +00:00
feat(cleanup): add age-based error log cleanup
- add --errors flag to target error-*.log files - add --days=N for age filtering (default: 7 days) - show preview of files to delete before confirmation - keep recent logs for debugging, only delete old ones
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
* Cleanup Command Handler
|
||||
*
|
||||
* Removes old CLIProxy logs to free up disk space.
|
||||
* Supports both main logs and error request logs with age-based filtering.
|
||||
* Logs can accumulate to several GB without user awareness.
|
||||
*/
|
||||
|
||||
@@ -10,6 +11,9 @@ import * as path from 'path';
|
||||
import { getCliproxyDir } from '../cliproxy/config-generator';
|
||||
import { info, ok, warn } from '../utils/ui';
|
||||
|
||||
/** Default age in days for error log cleanup */
|
||||
const DEFAULT_ERROR_LOG_AGE_DAYS = 7;
|
||||
|
||||
/** Get the CLIProxy logs directory */
|
||||
function getLogsDir(): string {
|
||||
return path.join(getCliproxyDir(), 'logs');
|
||||
@@ -95,6 +99,76 @@ function cleanDirectory(dirPath: string): { deleted: number; freedBytes: number
|
||||
return { deleted, freedBytes };
|
||||
}
|
||||
|
||||
/** Error log file info */
|
||||
interface ErrorLogInfo {
|
||||
name: string;
|
||||
path: string;
|
||||
size: number;
|
||||
mtime: Date;
|
||||
ageInDays: number;
|
||||
}
|
||||
|
||||
/** Get error log files with metadata */
|
||||
function getErrorLogFiles(logsDir: string): ErrorLogInfo[] {
|
||||
if (!fs.existsSync(logsDir)) return [];
|
||||
|
||||
const now = Date.now();
|
||||
const files: ErrorLogInfo[] = [];
|
||||
const entries = fs.readdirSync(logsDir);
|
||||
|
||||
for (const entry of entries) {
|
||||
// Only process error-*.log files
|
||||
if (!entry.startsWith('error-') || !entry.endsWith('.log')) continue;
|
||||
|
||||
const filePath = path.join(logsDir, entry);
|
||||
try {
|
||||
const stats = fs.lstatSync(filePath);
|
||||
if (stats.isFile() && !stats.isSymbolicLink()) {
|
||||
const ageMs = now - stats.mtime.getTime();
|
||||
files.push({
|
||||
name: entry,
|
||||
path: filePath,
|
||||
size: stats.size,
|
||||
mtime: stats.mtime,
|
||||
ageInDays: Math.floor(ageMs / (1000 * 60 * 60 * 24)),
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// File may have been deleted - skip
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by age, oldest first
|
||||
return files.sort((a, b) => b.ageInDays - a.ageInDays);
|
||||
}
|
||||
|
||||
/** Delete error logs older than specified days */
|
||||
function cleanErrorLogs(
|
||||
logsDir: string,
|
||||
maxAgeDays: number
|
||||
): { deleted: number; freedBytes: number; kept: number } {
|
||||
const files = getErrorLogFiles(logsDir);
|
||||
let deleted = 0;
|
||||
let freedBytes = 0;
|
||||
let kept = 0;
|
||||
|
||||
for (const file of files) {
|
||||
if (file.ageInDays >= maxAgeDays) {
|
||||
try {
|
||||
fs.unlinkSync(file.path);
|
||||
deleted++;
|
||||
freedBytes += file.size;
|
||||
} catch {
|
||||
// File may be locked or already deleted
|
||||
}
|
||||
} else {
|
||||
kept++;
|
||||
}
|
||||
}
|
||||
|
||||
return { deleted, freedBytes, kept };
|
||||
}
|
||||
|
||||
/** Print help for cleanup command */
|
||||
function printHelp(): void {
|
||||
console.log('');
|
||||
@@ -103,20 +177,19 @@ function printHelp(): void {
|
||||
console.log('Remove old CLIProxy logs to free up disk space.');
|
||||
console.log('');
|
||||
console.log('Options:');
|
||||
console.log(' --errors Clean error request logs (error-*.log files)');
|
||||
console.log(' --days=N Delete error logs older than N days (default: 7)');
|
||||
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(' ccs cleanup Interactive main log cleanup');
|
||||
console.log(' ccs cleanup --errors Clean error logs older than 7 days');
|
||||
console.log(' ccs cleanup --errors --days=3 Clean error logs older than 3 days');
|
||||
console.log(' ccs cleanup --errors --dry-run Preview error log cleanup');
|
||||
console.log(' ccs cleanup --dry-run Preview main log cleanup');
|
||||
console.log(' ccs cleanup --force Clean main logs without confirmation');
|
||||
console.log('');
|
||||
}
|
||||
|
||||
@@ -132,8 +205,125 @@ export async function handleCleanupCommand(args: string[]): Promise<void> {
|
||||
|
||||
const dryRun = args.includes('--dry-run');
|
||||
const force = args.includes('--force');
|
||||
const cleanErrors = args.includes('--errors');
|
||||
const logsDir = getLogsDir();
|
||||
|
||||
// Parse --days=N option
|
||||
let maxAgeDays = DEFAULT_ERROR_LOG_AGE_DAYS;
|
||||
const daysArg = args.find((arg) => arg.startsWith('--days='));
|
||||
if (daysArg) {
|
||||
const parsed = parseInt(daysArg.split('=')[1], 10);
|
||||
if (isNaN(parsed) || parsed < 1) {
|
||||
console.log(warn('Invalid --days value. Must be a positive integer.'));
|
||||
return;
|
||||
}
|
||||
maxAgeDays = parsed;
|
||||
}
|
||||
|
||||
// Route to error log cleanup or main log cleanup
|
||||
if (cleanErrors) {
|
||||
await handleErrorLogCleanup(logsDir, maxAgeDays, dryRun, force);
|
||||
} else {
|
||||
await handleMainLogCleanup(logsDir, dryRun, force);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle error log cleanup (error-*.log files)
|
||||
*/
|
||||
async function handleErrorLogCleanup(
|
||||
logsDir: string,
|
||||
maxAgeDays: number,
|
||||
dryRun: boolean,
|
||||
force: boolean
|
||||
): Promise<void> {
|
||||
// Check if logs directory exists
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
console.log(info('No CLIProxy logs directory found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get error log files
|
||||
const errorLogs = getErrorLogFiles(logsDir);
|
||||
if (errorLogs.length === 0) {
|
||||
console.log(info('No error logs found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate what would be deleted
|
||||
const toDelete = errorLogs.filter((f) => f.ageInDays >= maxAgeDays);
|
||||
const toKeep = errorLogs.filter((f) => f.ageInDays < maxAgeDays);
|
||||
const totalDeleteSize = toDelete.reduce((sum, f) => sum + f.size, 0);
|
||||
|
||||
console.log('');
|
||||
console.log(`Error Logs: ${logsDir}`);
|
||||
console.log(` Total: ${errorLogs.length} files`);
|
||||
console.log(
|
||||
` To delete: ${toDelete.length} files older than ${maxAgeDays} days (${formatBytes(totalDeleteSize)})`
|
||||
);
|
||||
console.log(` To keep: ${toKeep.length} files newer than ${maxAgeDays} days`);
|
||||
console.log('');
|
||||
|
||||
if (toDelete.length === 0) {
|
||||
console.log(info(`No error logs older than ${maxAgeDays} days.`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Show oldest files in dry-run or verbose mode
|
||||
if (dryRun || toDelete.length <= 5) {
|
||||
console.log('Files to delete:');
|
||||
for (const file of toDelete.slice(0, 10)) {
|
||||
console.log(` ${file.name} (${file.ageInDays}d old, ${formatBytes(file.size)})`);
|
||||
}
|
||||
if (toDelete.length > 10) {
|
||||
console.log(` ... and ${toDelete.length - 10} more`);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
console.log(info('Dry run - no files deleted.'));
|
||||
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 ${toDelete.length} error logs older than ${maxAgeDays} days (${formatBytes(totalDeleteSize)})? [y/N] `,
|
||||
resolve
|
||||
);
|
||||
});
|
||||
rl.close();
|
||||
|
||||
if (answer.toLowerCase() !== 'y') {
|
||||
console.log('Cancelled.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Perform cleanup
|
||||
const { deleted, freedBytes, kept } = cleanErrorLogs(logsDir, maxAgeDays);
|
||||
console.log(ok(`Deleted ${deleted} error logs, freed ${formatBytes(freedBytes)}`));
|
||||
if (kept > 0) {
|
||||
console.log(info(`Kept ${kept} recent error logs (less than ${maxAgeDays} days old)`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle main log cleanup (main.log and rotated files)
|
||||
*/
|
||||
async function handleMainLogCleanup(
|
||||
logsDir: string,
|
||||
dryRun: boolean,
|
||||
force: boolean
|
||||
): Promise<void> {
|
||||
// Check if logs directory exists
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
console.log(info('No CLIProxy logs found.'));
|
||||
|
||||
Reference in New Issue
Block a user