All files / src/persistence store.ts

98.46% Statements 64/65
95.23% Branches 20/21
100% Functions 9/9
98.46% Lines 64/65

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 125 126 127 128 129 130 131 132 133 134 135 1361x                                                                                           1x 1x 1x 1x     126x 126x 126x     1x 7x 7x     1x 118x 118x 118x   118x 118x   118x 118x 118x     1x 7x 7x 7x 7x 7x   7x 7x 7x 8x 8x 8x 8x   1x 1x 8x 7x 7x     1x 2x 2x 2x     118x 118x 118x 118x 118x   118x   5x 5x 5x 5x     5x 5x     1x 3x 3x     1x 1x 1x 1x     1x 101x 101x  
/**
 * JSON file persistence for opensip-tools results.
 *
 * Stores session results in ~/.opensip-tools/sessions/ as individual JSON files.
 * Each run creates one file: {timestamp}-{tool}-{recipe}.json
 */
 
import { mkdirSync, readFileSync, readdirSync, writeFileSync, unlinkSync } from 'node:fs';
import { randomUUID } from 'node:crypto';
import { basename, join } from 'node:path';
import { homedir } from 'node:os';
 
export interface StoredSession {
  readonly id: string;
  readonly tool: 'fit' | 'asm' | 'sim';
  readonly timestamp: string;
  readonly cwd: string;
  readonly recipe?: string;
  readonly score: number;
  readonly passed: boolean;
  readonly summary: {
    readonly total: number;
    readonly passed: number;
    readonly failed: number;
    readonly errors: number;
    readonly warnings: number;
  };
  readonly checks: readonly {
    readonly checkSlug: string;
    readonly passed: boolean;
    readonly findings: readonly {
      readonly ruleId: string;
      readonly message: string;
      readonly severity: string;
      readonly filePath?: string;
      readonly line?: number;
      readonly column?: number;
      readonly suggestion?: string;
      readonly category?: string;
    }[];
    readonly durationMs: number;
  }[];
  readonly durationMs: number;
}
 
/** Root directory for all opensip-tools data */
export const TOOLS_HOME = join(homedir(), '.opensip-tools');
const STORE_DIR = join(TOOLS_HOME, 'sessions');
const REPORTS_DIR = join(TOOLS_HOME, 'reports');
const MAX_SESSIONS = 100;
 
/** Ensure directory exists — mkdirSync with recursive is idempotent */
function ensureDir(dir: string): void {
  mkdirSync(dir, { recursive: true });
}
 
/** Sanitize a string for use in a filename — strip path separators and special chars */
export function sanitizeForFilename(s: string): string {
  return s.replace(/[/\\:*?"<>|.]/g, '-').replace(/\.\./g, '-');
}
 
/** Save a session result to disk */
export function saveSession(session: StoredSession): string {
  ensureDir(STORE_DIR);
  const safeRecipe = session.recipe ? `-${sanitizeForFilename(session.recipe)}` : '';
  const filename = `${session.timestamp.replace(/[:.]/g, '-')}-${session.tool}${safeRecipe}.json`;
  // Ensure filename stays within the sessions directory
  const filepath = join(STORE_DIR, basename(filename));
  writeFileSync(filepath, JSON.stringify(session, null, 2), 'utf-8');
 
  pruneOldSessions();
  return filepath;
}
 
/** Load all sessions, newest first. Optional limit to avoid reading everything. */
export function loadSessions(limit?: number): StoredSession[] {
  ensureDir(STORE_DIR);
  const files = readdirSync(STORE_DIR)
    .filter(f => f.endsWith('.json'))
    .sort()
    .reverse();
 
  const toRead = limit ? files.slice(0, limit) : files;
  const sessions: StoredSession[] = [];
  for (const file of toRead) {
    try {
      const raw = readFileSync(join(STORE_DIR, file), 'utf-8');
      sessions.push(JSON.parse(raw) as StoredSession);
    } catch {
      // Warn about corrupted files on stderr — don't crash
      console.error(`Warning: skipping corrupted session file: ${file}`);
    }
  }
  return sessions;
}
 
/** Load the most recent session */
export function loadLatestSession(): StoredSession | null {
  const sessions = loadSessions(1);
  return sessions[0] ?? null;
}
 
/** Prune sessions beyond the max count */
function pruneOldSessions(): void {
  const files = readdirSync(STORE_DIR)
    .filter(f => f.endsWith('.json'))
    .sort()
    .reverse();
 
  if (files.length <= MAX_SESSIONS) return;
 
  for (const file of files.slice(MAX_SESSIONS)) {
    try {
      unlinkSync(join(STORE_DIR, file));
    } catch {
      // Best effort
    }
  }
}
 
/** Get the store directory path */
export function getStoreDir(): string {
  return STORE_DIR;
}
 
/** Get the reports directory path, creating it if needed */
export function getReportsDir(): string {
  ensureDir(REPORTS_DIR);
  return REPORTS_DIR;
}
 
/** Generate a unique session ID */
export function generateSessionId(): string {
  return randomUUID();
}