# All files / src/logging manager.ts

96.77% Statements 30/31
90% Branches 9/10
100% Functions 10/10
96.77% Lines 30/31

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124

    11x

    47x
    47x

    2x

    24x
    24x

    30x

    58x

    8x
    8x
    6x
    9x
    8x

    2x
    2x

    5x
    5x
    1x

    4x
    4x
    4x

    4x
    4x
    4x
    4x
    4x
    2x
    2x

    4x

    3x

     /**
     * 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
          Eif ((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;
      }
    }