Move src/ → apps/server/ and packages/web/ → apps/web/ to adopt standard monorepo conventions (apps/ for runnable apps, packages/ for reusable libraries). Update all config files, shared package imports, test fixtures, and documentation to reflect new paths. Key fixes: - Update workspace config to ["apps/*", "packages/*"] - Update tsconfig.json rootDir/include for apps/server/ - Add apps/web/** to vitest exclude list - Update drizzle.config.ts schema path - Fix ensure-schema.ts migration path detection (3 levels up in dev, 2 levels up in dist) - Fix tests/integration/cli-server.test.ts import paths - Update packages/shared imports to apps/server/ paths - Update all docs/ files with new paths
124 lines
3.3 KiB
TypeScript
124 lines
3.3 KiB
TypeScript
/**
|
|
* Log Manager
|
|
*
|
|
* Manages log directories and file paths for per-process logging.
|
|
*/
|
|
|
|
import { mkdir, readdir, rm, stat } from 'node:fs/promises';
|
|
import { homedir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import type { LogConfig, LogStream } from './types.js';
|
|
|
|
/**
|
|
* Default base directory for logs: ~/.cw/logs
|
|
*/
|
|
const DEFAULT_LOG_DIR = join(homedir(), '.cw', 'logs');
|
|
|
|
/**
|
|
* Manages log directory structure and file paths.
|
|
*
|
|
* Log directory structure:
|
|
* ~/.cw/logs/{processId}/stdout.log
|
|
* ~/.cw/logs/{processId}/stderr.log
|
|
*/
|
|
export class LogManager {
|
|
private readonly baseDir: string;
|
|
private readonly retainDays?: number;
|
|
|
|
constructor(config?: Partial<LogConfig>) {
|
|
this.baseDir = config?.baseDir ?? DEFAULT_LOG_DIR;
|
|
this.retainDays = config?.retainDays;
|
|
}
|
|
|
|
/**
|
|
* Ensures the base log directory exists.
|
|
* Creates it recursively if it doesn't exist.
|
|
*/
|
|
async ensureLogDir(): Promise<void> {
|
|
await mkdir(this.baseDir, { recursive: true });
|
|
}
|
|
|
|
/**
|
|
* Ensures the process-specific log directory exists.
|
|
* @param processId - The process identifier
|
|
*/
|
|
async ensureProcessDir(processId: string): Promise<void> {
|
|
const processDir = this.getProcessDir(processId);
|
|
await mkdir(processDir, { recursive: true });
|
|
}
|
|
|
|
/**
|
|
* Gets the directory path for a specific process's logs.
|
|
* @param processId - The process identifier
|
|
*/
|
|
getProcessDir(processId: string): string {
|
|
return join(this.baseDir, processId);
|
|
}
|
|
|
|
/**
|
|
* Gets the full path to a log file for a process and stream.
|
|
* @param processId - The process identifier
|
|
* @param stream - Either 'stdout' or 'stderr'
|
|
*/
|
|
getLogPath(processId: string, stream: LogStream): string {
|
|
return join(this.baseDir, processId, `${stream}.log`);
|
|
}
|
|
|
|
/**
|
|
* Lists all log directories (one per process).
|
|
* @returns Array of process IDs that have log directories
|
|
*/
|
|
async listLogs(): Promise<string[]> {
|
|
try {
|
|
const entries = await readdir(this.baseDir, { withFileTypes: true });
|
|
return entries
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => entry.name);
|
|
} catch (error) {
|
|
// If directory doesn't exist, return empty list
|
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
return [];
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes log directories older than the specified number of days.
|
|
* @param retainDays - Number of days to retain logs (uses config value if not provided)
|
|
* @returns Number of directories removed
|
|
*/
|
|
async cleanOldLogs(retainDays?: number): Promise<number> {
|
|
const days = retainDays ?? this.retainDays;
|
|
if (days === undefined) {
|
|
return 0;
|
|
}
|
|
|
|
const cutoffTime = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
const processIds = await this.listLogs();
|
|
let removedCount = 0;
|
|
|
|
for (const processId of processIds) {
|
|
const processDir = this.getProcessDir(processId);
|
|
try {
|
|
const stats = await stat(processDir);
|
|
if (stats.mtime.getTime() < cutoffTime) {
|
|
await rm(processDir, { recursive: true, force: true });
|
|
removedCount++;
|
|
}
|
|
} catch {
|
|
// Skip if we can't stat or remove the directory
|
|
}
|
|
}
|
|
|
|
return removedCount;
|
|
}
|
|
|
|
/**
|
|
* Gets the base directory path.
|
|
*/
|
|
getBaseDir(): string {
|
|
return this.baseDir;
|
|
}
|
|
}
|