/** * Guardian Monitor - Checks files against Guardian rules */ import axios from 'axios'; import chalk from 'chalk'; import fs from 'fs/promises'; import path from 'path'; import { checkFile, type EffectiveRule, type Violation, type CheckResult } from '../utils/rule-engine.js'; const CACHE_FILE = '.rigstate/rules-cache.json'; const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes interface CachedRules { timestamp: string; projectId: string; rules: EffectiveRule[]; settings: { lmax: number; lmax_warning: number }; } export interface GuardianMonitor { loadRules(): Promise; checkFile(filePath: string): Promise; getRuleCount(): number; getRules(): EffectiveRule[]; } export function createGuardianMonitor( projectId: string, apiUrl: string, apiKey: string ): GuardianMonitor { let rules: EffectiveRule[] = []; let lastFetch: number = 0; const loadRules = async (): Promise => { // Check if cache is fresh if (rules.length > 0 && Date.now() - lastFetch < CACHE_TTL_MS) { return; } try { // Try API first const response = await axios.get(`${apiUrl}/api/v1/guardian/rules`, { params: { project_id: projectId }, headers: { Authorization: `Bearer ${apiKey}` }, timeout: 10000 }); if (response.data.success && response.data.data.rules) { rules = response.data.data.rules; lastFetch = Date.now(); // Save to cache await saveCachedRules(projectId, rules); return; } } catch (error: any) { // Log error for debugging // If it was a local connection failure, try falling back to Rigstate Cloud if (apiUrl.includes('localhost') || apiUrl.includes('127.0.0.1')) { const cloudUrl = 'https://app.rigstate.com'; console.log(chalk.blue(` ☁️ Local API not found. Attempting Cloud Fallback (${cloudUrl})...`)); try { const cloudResponse = await axios.get(`${cloudUrl}/api/v1/guardian/rules`, { params: { project_id: projectId }, headers: { Authorization: `Bearer ${apiKey}` }, timeout: 5000 }); if (cloudResponse.data.success && cloudResponse.data.data.rules) { rules = cloudResponse.data.data.rules; console.log(chalk.green(` ✅ Successfully loaded rules from Rigstate Cloud!`)); lastFetch = Date.now(); await saveCachedRules(projectId, rules); return; } } catch (cloudError: any) { console.error(chalk.red(` ❌ Cloud Fallback failed: ${cloudError.message}`)); } } // Log local error if cloud fallback also fails console.error(chalk.red(` ⚠️ Failed to fetch rules from API: ${error.message}`)); if (error.response) { console.error(chalk.red(` Status: ${error.response.status} - ${JSON.stringify(error.response.data)}`)); } // Try cache fallback const cached = await loadCachedRules(projectId); if (cached) { console.log(chalk.yellow(' ℹ️ Using cached rules as fallback')); rules = cached.rules; lastFetch = Date.now(); return; } } // No rules available rules = []; }; const checkFileImpl = async (filePath: string): Promise => { // Ensure rules are loaded await loadRules(); if (rules.length === 0) { return { file: filePath, violations: [], passed: true }; } const absolutePath = path.resolve(process.cwd(), filePath); return checkFile(absolutePath, rules, process.cwd()); }; const getRuleCount = (): number => rules.length; const getRules = (): EffectiveRule[] => rules; return { loadRules, checkFile: checkFileImpl, getRuleCount, getRules }; } async function loadCachedRules(projectId: string): Promise { try { const cachePath = path.join(process.cwd(), CACHE_FILE); const content = await fs.readFile(cachePath, 'utf-8'); const cached: CachedRules = JSON.parse(content); if (cached.projectId !== projectId) { return null; } return cached; } catch { return null; } } async function saveCachedRules(projectId: string, rules: EffectiveRule[]): Promise { try { const cacheDir = path.join(process.cwd(), '.rigstate'); await fs.mkdir(cacheDir, { recursive: true }); const cached: CachedRules = { timestamp: new Date().toISOString(), projectId, rules, settings: { lmax: 400, lmax_warning: 350 } }; await fs.writeFile( path.join(cacheDir, 'rules-cache.json'), JSON.stringify(cached, null, 2) ); } catch { // Silently fail cache write } }